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:
-
This function is called at the very beginning to check, whether the PythonPart can be started in the current ALLPLAN version. Implementation is exactly the same, as in the Standard PythonPart
-
This function is called directly after the version check. It should only construct and return your script object.
create_script_object
create_script_object(
build_ele: BuildingElement, script_object_data: BaseScriptObjectData
) -> BaseScriptObject
Parameters:
-
build_ele
(BuildingElement
) –building element with the parameter properties
-
script_object_data
(BaseScriptObjectData
) –script object data
Returns:
-
BaseScriptObject
–created script object
Source code in src\PythonPartsScriptTemplates\ScriptObjectPythonPart.py
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)!
- 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.:- CoordinateInput represents the user input in ALLPLAN viewport
- ControlPropertiesUtil can be used to alter the properties of palette controls (learn more in this article)
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.
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.
- Only one method is mandatory, and that is
execute
. - 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:
- script_object_interactor contains the interactor representing the currently executed input step.
- document contains the current document.
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:
- Your script object class must inherit from BaseScriptObject.
- You must implement the execute method (see basic implementation below).
- You can introduce additional input steps, like point input, element selection, etc.
- These input steps are user interactions represented by script object interactor classes (e.g. PointInteractor represents input of a single point).
- All of them inherit from BaseScriptObjectInteractor.
- The script object interactor uses a dedicated data class to save the results of the interaction. E.g. PointInteractor saves the picked point in the PointInteractorResult data class.
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)
- 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:
...
- 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:
- When the property
script_object_interactor
isNone
, script object PythonPart behaves like a standard PythonPart - 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:
-
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)!
- We will be implementing a selection of a single element
- This data class will hold the selection result: BaseElementAdapter
-
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 attributescript_object_interactor
: -
Implement the
start_next_input
method - it gets called after the user interaction assigned toscript_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 assignNone
.- When
script_object_interactor
equals toNone
, framework calls theexecute
method. See flow diagram below.
- When
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(...)
- 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:
- Define an instance attribute for the interaction results as described in the point 1 in the previous paragraph
-
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 theon_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)!
- In our example, the input should be started, when the user presses the button with
an
<EventId>
of 1001. That is why we implement theon_control_event
method. - When starting the input outside the
start_input
method, thestart_input
method of the interactor must be called extra.
- In our example, the input should be started, when the user presses the button with
an
-
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(...)
- 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
- Esc hit when there is no element selection interactor running, will create the elements and terminate the PythonPart completely
- 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.