PythonPart
A PythonPart as an element in the DF can be understood as a container, encapsulating Allplan elements such as:
- 3D elements like: 3D-lines, surfaces, solids
- 2D elements: 2D-lines, 2D-polylines
The key feature of a PythonPart is that it can be modified parametrically.
PythonPart also has properties typical for other Allplan elements, like Common properties or Attributes. In addition, a PythonPart can be hierarchically connected to other elements with a Parent-Child relationship. Learn more about that below
Note
Note, that the general 2D and 3D objects are stored directly inside the container, whereas the elements mentioned above are saved in the DF as separate objects and linked with a Parent-Child connection. These two different types of relationships matter during the evaluation with e.g., reports.
Data structure
Abstract
In this paragraph we will explain the background of a PythonPart. This knowledge might be helpful to, but is not required to work with PythonParts. If you want, you can jump directly to this paragraph to learn how to create a PythonPart.
The data structure behind a PythonPart is similar to the one behind a smart object (aka macro), which means it is represented in the model by two objects:
-
The PythonPart definition represented by a MacroElement and containing:
- multiple views (aka slides or foils) (2D or 3D) each encapsulating 3D elements and/or 2D elements
- a unique hash
-
The PythonPart placement represented by a MacroPlacementElement and containing:
- Information about position and orientation in global coordinate system defined with a placement matrix
- Common properties
- Optionally Attributes
- Optionally child elements
The relationships described above, together with the cardinalities, are shown on the ER diagram below.
erDiagram
PyP-PLACEMENT }|--|| PyP-DEFINITION: places
PyP-DEFINITION ||--|{ PyP-VIEW : contains
PyP-VIEW ||--|{ MODEL-ELEMENT-3D : contains
PyP-VIEW ||--|{ MODEL-ELEMENT-2D : contains
PyP-PLACEMENT |o..o{ FIXTURE : links
PyP-PLACEMENT |o..o{ REINFORCEMENT : links
PyP-PLACEMENT {
str Name
Matrix3D PlacementMatrix
Attributes Attributes
CommonProperties CommonProperties
}
PyP-DEFINITION {
str Name
str Hash
}
PyP-VIEW {
bool visibility2d
bool visibility3d
float start_scale
float end_scale
}
MODEL-ELEMENT-3D {
Any Geometry
CommonProperties CommonProperties
}
MODEL-ELEMENT-2D {
Any Geometry
Attributes Attributes
CommonProperties CommonProperties
}
Note
Note that the 2D and 3D elements, as well as PythonPart placement contain Attributes. But relevant are only the attributes of the placement! Attributes of the elements inside the container cannot be accessed by Allplan reports and legends.
Example
Let's assume we have two identical PythonParts in the DF consisting of two views: one 2D and one 3D view. According to the diagram above, this constellation will be represented in DF with:
- two PythonPart placements referring to the same...
- one PythonPart definition, containing...
- two PythonPart views
Create PythonPart
The easiest way to create a PythonPart is to use the PythonPartUtil class. It will take care of tasks necessary for a PythonPart, such as:
- creating the appropriate data structure with MacroPlacementElement and MacroElement
- calculating the unique hash
- appending all the attribute parameter defined in the property palette
Example
Let's assume, that the variable cube
is a ModelElement3D
representing a cube. Here is, how we create a very simple PythonPart:
from PythonPartUtil import PythonPartUtil
...
python_part_util = PythonPartUtil(common_prop) #(1)!
python_part_util.add_pythonpart_view_2d3d(cube) #(2)!
python_part = python_part_util.create_pythonpart(build_ele) #(3)!
- At this stage we can control the Common properties of the PythonPart placement. This argument is optional. If not provided, currently common properties settings will be applied
- This will add the cube to a view, that is displayed in all scales and in both ground and isometric view. We could also provide a ModelEleList
-
The BuildingElement object containing all the parameters and their values is necessary for the creation of the PythonPart. This ensures, that e.g.
- a unique hash to distinguish identical PythonParts is generated
- all the attribute parameter defined in the palette are appended to the pythonpart
Views
Because a PythonPart has a data structure like a smart symbol, we can append multiple views to it and control the visibility of each of them individually, based on:
- the art of the view: ground view or isometric view
- currently set scale: a more detailed representation can be shown in lower scale
These settings are stored in a PythonPartViewData object. Apply them on a view by passing the object as an optional argument to the add_pythonpart_view method.
Example
Let's assume, that we want to display our PythonPart as a:
- sphere, when the scale ranges from 1:1 to 1:50
- cube, when the scale ranges from 1:50 to 1:9999
We have created two 3d model elements for that
purpose: cube_model_ele
and sphere_model_ele
. Let's now use them to create a PythonPart.
Firstly, we create the utility just as usual:
Now let's create the first view with the sphere:
sphere_view_data = PythonPartViewData()
sphere_view_data.start_scale = 0
sphere_view_data.end_scale = 50 #(1)!
python_part_util.add_pythonpart_view_2d3d(elements = sphere_model_ele,
view_data = sphere_view_data) #(2)!
- With this setting, the view will be displayed, when the set scale is lower than or equal to 50!
- Here is, where we apply the view settings. this argument is optional and if not provided, the default settings defines in PythonPartViewData will be used
... and the second view with the cube:
cube_view_data = PythonPartViewData()
cube_view_data.start_scale = 50 #(1)!
cube_view_data.end_scale = 9999
python_part_util.add_pythonpart_view_2d3d(elements = cube_model_ele,
view_data = cube_view_data)
- With this setting, the view will be displayed, when the set scale is greater than 50
At the end we create the PythonPart as usual:
The visibility of the views can also be determined by the art of the view. Sometimes it is useful to display the PythonPart in the ground view with simplified 2D view consisting of lines, and use complex 3D solids only in the isometric view.
To achieve this, use the appropriate method of the PythonPartUtil to create the view:
Used function | Behavior |
---|---|
add_pythonpart_view_2d | the view is visible in the ground view only |
add_pythonpart_view_3d | the view is visible in the isometric view only |
add_pythonpart_view_2d3d | the view is always visible |
Note
Note, that the usage of the above mentioned methods makes it irrelevant, what is set in the properties visible_in_2d or visible_in_3d of the PythonPartViewData.
Failure
The following settings of the PythonPartViewData have at the moment no influence on the visibility behavior of the created PythonPart:
Example
The functionalities mentioned above are shown in the example ViewSettings, located in:
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\ViewSetting.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\ViewSettings.pyp
Attributes
When creating a PythonPart using PythonPartUtil, all the parameters with an attribute defined in the .pyp file are automatically appended to the PythonPart as attributes, without introducing any additional steps inside the script.
If we use this kind of parameter inside the script to determine the geometry of a PythonPart, we achieve an additional behavior: modifying the attribute linked to this parameter outside the PythonPart modification mode (e.g. with Allplan's native function Modify attributes) will influence the geometry of the PythonPart the next time the PythonPart is activated. This is because the new attribute value is read back into the script and used for geometry creation. This links the attribute value and the geometry of the PythonPart together.
Example
Have a look at the example PythonPartWithAttributes, located in:
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
There is a parameter layer thickness. This parameter is linked to an attribute called LayerThickness. Place the PythonPart in the model and then modify the value of this attribute using Allplan native function Modify attributes. Then reactivate the PythonPart again. You will see, that the thickness of the bottom part of the PythonPart changes accordingly.
When we want to save a value calculated inside the script in an attribute of the PythonPart, we can use the add_attribute_list method to do that:
Example
Let's assume, that the user enters the length of a PythonPart, but the width and height are calculated based on that inside the script. Because these values are not parameters in the palette, we have to add them explicitly in the script. Let's save them in the attributes nr. @221@ (width) and @222@ (height). To do that, we introduce following step in the script:
attribute_list = BuildingElementAttributeList()
attribute_list.add_attribute(221, width)
attribute_list.add_attribute(222, height)
pyp_util.add_attribute_list(attribute_list)
Have a look at the example PythonPartWithAttributes, located in:
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
When the check box Append geometry attributes is checked, tha geometrical parameters (length, width, height) are appended to the PythonPart as attributes.
Child elements
A PythonPart can be linked with other elements with a Parent-Child relationship, PythonPart being the Parent element. The table below shows an overview of the elements, that can be connected like this, and the function that must be used to do so.
Child element | Function to use |
---|---|
Architecture objects | add_architecture_elements |
Reinforcement | add_reinforcement_elements |
Library objects | add_library_elements |
Fixtures | add_fixture_elements |
Info
Avoid including these elements directly into the PythonPart views. Although it is possible, it may lead to unexpected behavior of the PythonPart.
Bug
At the moment, there is a bug resulting in all the child-objects of the PythonPart being deleted, when the latter is modified using the default Allplan property palette (e.g. changing the pen or color).
Example
The functionality mentioned above is shown in the example PythonPartWithSubObjects, located in:
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\PythonPartWithSubObjects.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\PythonPartWithSubObjects.pyp
To create a child element, check the corresponding checkbox and place the PythonPart in the model. Check the behavior, when moving the PythonPart and the sub object. Check the relationships in the Allplan Object manager.
Placement
To control, how the PythonPart is placed in the model, the method create_pythonPart offers two optional arguments:
placement_matrix
local_placement_matrix
These arguments require a Matrix3D describing rotation and/or translation (see this article to learn how to construct it) to place the whole PythonPart container in the desired position and orientation in the model.
pyp_util = PythonPartUtil()
...
python_part = pyp_util.create_pythonpart(build_ele,
placement_matrix = placement_mat)
Info
You can construct one matrix and pass it to one of the arguments (as shown above) or two matrices (e.g. one with rotation and second one with translation) and pass them to both arguments separately. Keep following in mind:
- order: the transformation described in
placement_matrix
is applied at first, followed by thelocal_placement_matrix
- the
local_placement_matrix
is saved in the attribute @1035@,placement_matrix
is not saved in any attribute
Tip
Use this feature to rotate or move the entire PythonPart container rather than rotating/moving the individual elements inside the container. This will ensure the right behavior of the created PythonPart.
Example
A typical use-case for this feature is, when you want to give the user control over the orientation of the PythonPart, when placing it. In this case, you rotate the whole PythonPart. It is shown in the example IdenticalRotatedPlate, where the user can rotate the PythonPart around all three XYZ axes. It is located in:
- …\etc\Examples\PythonParts\BasisExamples\IdenticalRotatedPlate.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\IdenticalRotatedPlate.py
PythonPart group
Multiple PythonPart placements can be grouped together into a PythonPart group. The user interaction with a group is the same as with an individual PythonPart, but the individual group members can be evaluated by Allplan reports. This allows you to create model elements that are seen by the user as one, but by Allplan as many elements. A typical use-case are set-like components, that must be invoiced individually (e.g. screw, nut and washer).
Abstract
The individual PythonParts inside a group are equal in the hierarchy: all are child-elements of the PythonPart group, where the group itself does not have any geometrical representation. It is not possible to introduce a parent-child relationship between individual PythonParts within the group.
In the following code snippets we will implement the individual PythonParts and the group as a standard PythonPart. Of course, you can also implement it as an interactor.
To create a PythonPart group, prepare its members first. These must be fully functional,
regular PythonParts, i.e. they must have a PYP file and a script (python module). Just for the sake
of this article we will refer to them as sub-PYP and sub-script. To be able to put them in
a group, make sure that, beside all the required functions (like create_element
), there is a
function/method that returns a PythonPart object in the sub-script.
We will need it in the script for the group. If you are using PythonPartUtil,
you can use the get_pythonpart method.
This is how the implementation can look like:
def create_pythonpart(build_ele: BuildingElement) -> PythonPart:
python_part_util = PythonPartUtil()
... #(1)!
return python_part_util.get_pythonpart(build_ele)
def create_element(build_ele, _doc) -> CreateElementResult:
python_part = create_pythonpart(build_ele) #(2)!
return CreateElementResult(python_part.create())
- Define the views, attributes and other stuff here.
- You can use the PythonPart object you created later in the create_element function for the creation of the individual PythonPart.
With this little tweak, the sub-script creates an individual PythonPart when the user starts
the sub-PYP, but now we can use the function create_pythonpart
inside the script of the
PythonPart group, to get the PythonPart object and group it.
The PythonPart group has its own PYP file and script (python module). Let's refer to them as group-PYP and group script. The parameters inside the group-PYP are defined completely independently from the sub-PYP(1). Therefore, in the group script we need both BuildingElements: with parameters from sub-PYP and from the group-PYP. The latter is given by the PythonPart framework inside the create_element function. The latter we must get by reading the sub-PYP file.
- Depending on the use-case, a group parameter may have an influence on the parameters of the individual members. For example, in a screw-nut-washer set all the individual components will have the parameter diameter. When placing them as individual PythonParts, you could set a different diameter for each component. But in a group, there will only by one diameter, which will then influence all three diameters of the sub components: the dimeter of the screw, nut and washer.
We also need to call the create_pythonpart
function from the sub-script to get the
PythonPart objects and group them. Therefore, we need to import
the sub-script as a python module
Both tasks can be done by calling the read_build_ele_from_pyp function:
def create_element(build_ele, _doc) -> CreateElementResult:
result, sub_script, sub_build_ele = BuildingElementService.read_build_ele_from_pyp(path_to_sub_pyp)
sub_build_ele.Diameter.value = build_ele.Diameter.value #(1)!
- The
sub_build_ele
will contain parameters of the individual PythonParts with their default values (as defined in the sub-PYP). You can alter these values, e.g. based on the parameter values from the group-PYP, like this.
Now, having the sub_build_ele
with the right parameter values, we can construct the individual
PythonPart objects, by calling the function create_pythonpart
from the sub_script
module:
Lastly, construct a PythonPartGroup using the from_build_ele method. Use the BuildingElement representing the group-PYP (the one you were given by the framework) to do it. Then append the individual PythonParts to it. Then, use the create method to get a model element list that can be put into the CreateElementResult data class and you are done.
pythonpart_group = PythonPartGroup.from_build_ele(build_ele)
pythonpart_group.append(pythonpart)
return CreateElementResult(pythonpart_group.create())
Tip
A PythonPart group created like this is modifiable with a double-click as one component, although it consist of multiple. However, the user can use Allplan functionality called unlink smart symbol to remove the group but leave the components. After that, the components are modifiable as if they were placed individually.
Example
The entire implementation is shown in the example PythonPartGroup, that uses the PythonPartWithAttribute as a subordinate PythonPart. Both are located in:
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\PythonPartGroup.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\PythonPartGroup.pyp
- …\etc\Examples\PythonParts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\PythonParts\PythonPartWithAttributes.pyp
Display name
A PythonPart element can represent any component: from a screw to an abutment. To enable the user to easily distinguish between different types of PythonParts inside the viewport you can assign a customized display name to your PythonPart.
This customized name will be then displayed to the user, when he hovers the mouse over the PythonPart inside a viewport (as shown on the image above) which improves the UX during the element selection. Furthermore, the user will be able to group the PythonParts by this name inside the object manager, when he chooses the Type criterion (as shown on the image to the left).
To achieve this, provide the display name, when calling the create_pythonpart method:
elements = pyp_util.create_pythonpart(
build_ele,
type_uuid = "c0398407-1d54-4087-a8da-7d6aaffb25ec", #(1)!
type_display_name = "PythonPart Plate"
)
- Beside the display name, you can provide a UUID for the element type, your PythonPart
represents. It will override the
PythonPart_TypeUUID
in the created element. This will allow you to set-up a filter for selecting only your type of PythonPart, which can be very useful when building a script for modification.
Info
The same functionality is available also for PythonPart groups inside the constructor of the PythonPartGroup class.
Update of identical PythonParts
After a PythonPart has been changed, Allplan searches through the DF for other PythonParts with the same hash. If some are found, a dialog box, as shown on the right, appears. After the user hits yes, Allplan recreates these PythonParts, applying the same values to their parameters, as the ones specified for the originally modified PythonPart, ensuring all the identical PythonParts remains identical after the modification.
In some cases, we want two PythonParts to be considered identical, despite some parameter values
being different. E.g. a parameter responsible for rotation is not relevant, because two PythonParts
rotated differently, are still the same. In this case, it makes sense to exclude these rotation
parameters from the identical check. To achieve this, add the <ExcludeIdentical>
tag to the <Parameter>
.
Example
An example is shown in the IdenticalRotatedPlate PythonPart, located in:
- …\etc\Examples\PythonParts\BasisExamples\IdenticalRotatedPlate.pyp
- …\etc\PythonPartsExampleScripts\BasisExamples\IdenticalRotatedPlate.py
In this example, a plate can be rotated around all three axes. The rotation angle is entered
in the palette as a parameter. If all parameters would be used for an identical check, plates
with identical geometry, but not identical rotation angles will not be considered identical.
Setting the <ExcludeIdentical>
tag to True in the rotation parameter solves
this issue.
Further information can be found in the example files.
PythonPart transaction
In some situations it's required to undertake some actions after the elements are created. A good example is the rearrangement of the reinforcement mark numbers. We can do this only after the reinforcement is created, because only then Allplan is able to recognize identical shapes.
The class PythonPartTransaction takes care of exactly that. It creates model elements and subsequently introduces additional steps, so that you as a developer doesn't have to implement them. These steps can involve:
- taking care of correct creation of PrecastElements
- recreating reinforcement labels added with native Allplan function
- connecting existing elements and the PythonPart
- rearrangement of the reinforcement mark numbering
- creation of the undo step
Note
When you create your script as a Standard PythonPart, the transaction is executed by the framework for you. In an Interactor PythonPart you have to implement it yourself.
Example
Before executing the transaction, the object has to be constructed first:
After constructing the transaction, we can call the execute method. We call it at the point,
where we would create the elements from the model_ele_list
in the drawing file:
pyp_transaction.execute(placement_matrix = AllplanGeo.Matrix3D(),
view_world_projection = self.coord_input.GetViewWorldProjection(),
model_ele_list = model_element_list, #(1)!
modification_ele_list = self.modification_ele_list, #(2)!
rearrange_reinf_pos_nr = ReinforcementRearrange(), #(5)!
append_reinf_pos_nr = True,
uuid_parameter_name = "", #(3)!
use_system_angle = True) #(4)!
- Here we pass the list of model elements that we want to create. For elements, that requires additional actions after the creation (like e.g. a PrecastElement), these actions will be introduced by the framework, as they are already implemented in the execute method.
- In a modification mode, this list should contain UUIDs of the modified elements, so that these elements preserves their other unique identifications after the modification (e.g. IfcID). In a PythonPart script constructed as an Interactor you get this list the create_interactor function.
- If you want to save the UUID of the PythonPart, you create, in one of its parameters, you can provide the name of this parameter here. This might be important, if you create a PythonPart that should react accordingly, when being modified after being copied by the user. Because after being copied, the PythonPart becomes a new UUID, but its parameters remain. This is how you can make the copied PythonPart remember the UUID of the PythonPart it originated from.
- When set to True, the elements are rotated accordingly to consider the crosshair rotation during the creation.
- See the documentation of ReinforcementRearrange class to learn about possible settings.
After calling the execute method, elements from
the model_ele_list
are created in the model. Subsequently, a series of actions are performed.
See the documentation of the method for more details.
For a complete usage of PythonPartTransaction, see the example ModelPolygonExtrudeInteractor located in:
- …\etc\Examples\PythonParts\InteractorExamples\General\ModelPolygonExtrudeInteractor.pyp
- …\etc\PythonPartsExampleScripts\InteractorExamples\General\ModelPolygonExtrudeInteractor.py