Skip to content

Script object

The script object PythonPart can be described as a combination of a Standard PythonPart and an Interactor PythonPart, because:

  • it allows you to introduce additional input steps into the workflow,
  • does not require you to build each of these steps from scratch.

The individual input steps are pre-implemented as so called script object interactors. What you have to implement, are two functions and one class.

Implementation

First, define the PYP file exactly as if it were a Standard PythonPart: the tag <Interactor> must be set to False (or left undefined). Then, in the PY file, define following two functions:

create_script_object

create_script_object(
    build_ele: BuildingElement, script_object_data: BaseScriptObjectData
) -> BaseScriptObject

Parameters:

Returns:

Source code in src\PythonPartsScriptTemplates\ScriptObjectPythonPart.py
def create_script_object(build_ele         : BuildingElement,
                         script_object_data: BaseScriptObjectData) -> BaseScriptObject:
    """ Creation of the script object

    Args:
        build_ele:          building element with the parameter properties
        script_object_data: script object data

    Returns:
        created script object
    """

    return ScriptObject(build_ele, script_object_data)

Example

from BaseScriptObject import BaseScriptObject

def create_script_object(build_ele         : BuildingElement,
                         script_object_data: BaseScriptObjectData) -> BaseScriptObject:

    return MyScriptObject(build_ele, script_object_data)  #(1)!
  1. The name of the class is just an example. It is your implementation: name it as you want it.

Note, that the framework gives you much more data, than it is the case with the standard PythonPart:

  • the build_ele and contains the parameter with their values input by the user in the property palette
  • the script_object_data is a data class containing a couple of useful objects, a.o.:

    Refer to the documentation of BaseScriptObjectData

Script object class

Now, define a class in your script. Make sure, it uses the BaseScriptObject as a base class.

class MyScriptObject(BaseScriptObject):
    """My implementation of script object PythonPart"""

Doing so will enforce the contract in the right way, as the BaseScriptObject class contains all the methods the framework will look for. The mandatory methods (1) are implemented as abstract methods, and the optional ones as stub methods. You should therefore override(2) them in your implementation.

  1. Only one method is mandatory, and that is execute.
  2. Except for the __init__() method, that we suggest to extend, rather than override.

Beside methods, you will inherit properties from BaseScriptObject, that the framework expects. The key ones are:


The diagram below shows, what should be the relationships between your implementation of the script object class (shown as MyScriptObject) and other classes. The key points are:

classDiagram
    direction BT

    class BaseScriptObjectData{
        <<Dataclass>>
        coord_input AllplanIFW.CoordinateInput
        modification_ele_list ModificationElementList
        is_only_update bool
        control_props_util ControlPropertiesUtil
    }

    class BaseScriptObject{
        <<Abstract>>
        script_object_interactor BaseScriptObjectInteractor | None
        document DocumentAdapter
        execute()
        start_input()
        start_next_input()
        modify_element_property()
        on_control_event()
        on_cancel_function()
    }

    class MyScriptObject{
        execute()
    }

    class BaseScriptObjectInteractor{
        <<Abstract>>
        process_mouse_msg()
        on_preview_draw()
        on_mouse_leave()
        on_cancel_function()
    }

    class SingleElementSelectInteractor
    class MultiElementSelectInteractor
    class PointInteractor
    class LineInteractor

    class SingleElementSelectResult
    class MultiElementSelectInteractorResult
    class PointInteractorResult
    class LineInteractorResult

    BaseScriptObject --|> BaseScriptObjectData
    MyScriptObject --|> BaseScriptObject

    SingleElementSelectInteractor --|> BaseScriptObjectInteractor
    MultiElementSelectInteractor --|> BaseScriptObjectInteractor
    PointInteractor --|> BaseScriptObjectInteractor
    LineInteractor --|> BaseScriptObjectInteractor

    BaseScriptObjectInteractor "0..1" *-- MyScriptObject

    SingleElementSelectResult .. SingleElementSelectInteractor : uses
    MultiElementSelectInteractorResult .. MultiElementSelectInteractor : uses
    PointInteractorResult .. PointInteractor : uses
    LineInteractorResult .. LineInteractor : uses

    style MyScriptObject stroke:#333,stroke-width:3px

Basic implementation

Let's implement a script object contract without any additional input steps. In this case, it is enough to implement the execute method:

class MyScriptObject(BaseScriptObject):

    def execute(self) -> CreateElementResult:

        elements = [...] #(1)!

        return CreateElementResult(elements)
  1. Implement the creation of the elements here

An implementation like this will result in the same behavior, as a standard PythonPart implementation. Just like the create_element method, the execute method returns the CreateElementResult with all the elements to be created in the DF.


To access the parameter values defined in the property palette, extend the constructor inherited from the BaseScriptObject class, like:

class MyScriptObject(BaseScriptObject):
    def __init__(self, 
                 build_ele: BuildingElement
                 script_object_data: BaseScriptObjectData):

        super().__init__(script_object_data)
        self.build_ele = build_ele #(1)!

    def execute(self) -> CreateElementResult:
        ...
  1. Now you can access the values of the parameter defined in the PYP file like: my_parameter = self.build_ele.MyParameter.value

Introducing input steps

General rule are:

  1. When the property script_object_interactor is None, script object PythonPart behaves like a standard PythonPart
  2. When the property script_object_interactor is a an object inheriting from BaseScriptObjectInteractor, the interaction represented by this object is executed

Hence, to start a user interaction, construct any object, that inherits from BaseScriptObjectInteractor and assign it to this property.

First, choose the right script object interactor class, depending on what kind of input you want to introduce to the workflow. The table below is an overview of the predefined interactors, you can choose from. You can of course implement your own interactor.

Input type Script object interactor
Selection of a single element SingleElementSelectInteractor
Selection of multiple elements MultiElementSelectInteractor
Input of a single point PointInteractor
Input of a line LineInteractor
Input of a polygon PolygonInteractor
Input of a point on an architectural component ArchPointInteractor
Input of a point on an architectural component with offset input ArchOffsetPointInteractor

At the beginning of the workflow

To introduce a workflow step directly after the PythonPart is started, follow these steps:

  1. Define an instance attribute, where the results of the interaction should be saved:

    from ScriptObjectInteractors.SingleElementSelectInteractor import \
        SingleElementSelectInteractor, SingleElementSelectResult #(1)!
    ...
    class MyScriptObject(BaseScriptObject):
    
        def __init__(self, 
                    build_ele: BuildingElement
                    script_object_data: BaseScriptObjectData):
    
            super().__init__(script_object_data)
            self.build_ele = build_ele #(1)!
            self.selection_result = SingleElementSelectResult() #(2)!
    
    1. We will be implementing a selection of a single element
    2. This data class will hold the selection result: BaseElementAdapter
  2. Implement the start_input method - it will be called directly after your script object gets constructed by the framework (see script flow diagram below). Inside it, construct the script object interactor of your choice and assign it to the attribute script_object_interactor:

    def start_input(self):
        self.script_object_interactor = SingleElementSelectInteractor(self.selection_result)
    
  3. Implement the start_next_input method - it gets called after the user interaction assigned to script_object_interactor is completed by the user. You can now introduce another input step by assigning new interactor to it. In this example, we don't want that, so we assign None.

    def start_next_input(self):
        self.script_object_interactor = None #(1)!
    
    1. When script_object_interactor equals to None, framework calls the execute method. See flow diagram below.

From now on your PythonPart behaves like a standard PythonPart, but you have the access to the result of the user interaction (in our example: to the selected element):

def execute(self) -> CreateElementResult:

    common_props = self.selection_result.sel_element.GetCommonProperties() #(1)!

    # implement the element creation here

    return CreateElementResult(...)
  1. After successful user interaction, the result is saved in the data class defined in the first step. You can retrieve any data from the selected element.

During the workflow

You can also introduce the input step later in the workflow, e.g. when the user presses a button in the property palette. To do it, follow these steps:

  1. Define an instance attribute for the interaction results as described in the point 1 in the previous paragraph
  2. Like in the point 2 in the previous paragraph, construct the interactor object of your choice and assign it to the attribute script_object_interactor. The difference is, that you do it in a method that handles the event of pressing a button, which is the on_control_event:

    def on_control_event(self, event_id: int):
        if event_id == 1001:    #(1)!
            self.script_object_interactor = SingleElementSelectInteractor(self.selection_result)
            self.script_object_interactor.start_input(self.coord_input) #(2)!
    
    1. In our example, the input should be started, when the user presses the button with an <EventId> of 1001. That is why we implement the on_control_event method.
    2. When starting the input outside the start_input method, the start_input method of the interactor must be called extra.
  3. Lastly, implement the start_next_input method the same way, as shown in the point 3 in the previous paragraph.

From now on, you have the access to the result of the user interaction (in our example: to the selected element):

def execute(self) -> CreateElementResult:

    if not self.selection_result.sel_element.IsNull() #(1)!
        common_props = self.selection_result.sel_element.GetCommonProperties() #(1)!
    else:
        common_props = AllplanBaseElements.CommonProperties()

    # implement the element creation here

    return CreateElementResult(...)
  1. Note, that the execute method gets called also when the element selection was not done yet. For that case, we must build in this if-condition.

Handling ESC button hit

At any point of the interaction, user can press Esc. You should handle this event with the on_cancel_function. Depending on the returned value (which should be the OnCancelFunctionResult), framework introduces different actions. This is shown on the diagram below.

For example, in a PythonPart with multiple input steps, the user expects hitting Esc will terminate only the current input step, not the entire PythonPart. Let's say, that you implemented the element selection as described in the previous paragraph. In a general case, hitting Esc should create the element and terminate the PythonPart. However, when hit during the element selection, it should terminate only the selection step. To achieve this behavior, handle it like:

def on_cancel_function(self) -> OnCancelFunctionResult:
    if self.script_object_interactor is None:
        return OnCancelFunctionResult.CREATE_ELEMENTS  #(1)!
    return OnCancelFunctionResult.CONTINUE_INPUT
  1. Esc hit when there is no element selection interactor running, will create the elements and terminate the PythonPart completely
  2. When an element selection is running, hitting Esc will terminate the element selection, continue the PythonPart running.

Warning

In current state of the script object, the property palette cannot be made dynamic by using the ControlPropertiesUtil, as this object is not passed to the create_script_object function.

Script flow

The diagram below shows the script flow - the order in which the framework calls methods of your script object class.

flowchart TB
    start((start)):::end_point
    init("__init__()"):::function_call
    start_input("start_input()"):::function_call
    start_next_input("start_next_input()"):::function_call
    on_cancel("on_cancel_function()"):::function_call
    is_script_object_interactor{script_object_\ninteractor\nis None ?}:::decision
    execute("execute()"):::function_call
    execute_interaction[/do the user\ninteraction/]:::action
    create[/create\nelements/]:::action
    stop((end)):::end_point

    start --> init
    init --> start_input
    start_input --> is_script_object_interactor
    is_script_object_interactor -->|False| execute_interaction
    is_script_object_interactor -->|True| execute
    execute_interaction --> |completed| start_next_input
    start_next_input --> is_script_object_interactor
    execute --> execute
    execute -->|ESC\npressed| on_cancel
    execute_interaction -->|interrupted\nwith ESC| on_cancel
    on_cancel -->|RESTART| start_input
    on_cancel -->|CONTINUE_INPUT| start_next_input
    on_cancel -->|CREATE_ELEMENT| create --> stop
    on_cancel -->|CANCEL_INPUT| stop

    classDef end_point stroke:#333, fill:#CCC
    classDef function_call stroke:#FF4500
    classDef decision stroke:#008000
    classDef action stroke:#4169E1

Here are some key takeaways to keep in mind:

  • The start_input is called directly after your script object gets constructed. Hence, if you want to introduce a user interaction directly after the PythonPart is started, do it here.
  • Otherwise, as long as the property script_object_interactor remains empty, framework calls the execute method.
  • The execute method is called over and over again, so it is not the best place to introduce expensive actions like database queries, network requests or computationally intensive calculations.
  • When you introduce an input step by assigning a script object interactor to the script_object_interactor property, framework executes this interaction until it's either completed or interrupted by ESC.
  • When the interaction is completed, the start_next_input is called. Here you should either introduce another input step or clear the script_object_interactor property.
  • You can handle the event of pressing Esc with the on_cancel_function. Depending on its return value, framework will introduce different actions.
Placeholder