Skip to content

PythonPart

A PythonPart as an element in the DF can be understood as a container, encapsulating Allplan elements such as:

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 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:

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)!
  1. 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
  2. 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
  3. The BuildingElement object containing all the parameters and their values is necessary for the creation of the PythonPart. This ensures, that e.g.

Views

PythonPart view visibility

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:

from PythonPartUtil import PythonPartUtil
...
python_part_util = PythonPartUtil()

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)!
  1. With this setting, the view will be displayed, when the set scale is lower than or equal to 50!
  2. 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) 
  1. 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:

python_part = python_part_util.create_pythonpart(build_ele)

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.

PythonPart attribute modification

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 the local_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

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:

sub-script
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())
  1. Define the views, attributes and other stuff here.
  2. 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.

  1. 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:

group script
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)!
  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:

pythonpart = sub_script.create_pythonpart(sub_build_ele) #(1)!

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

PythonPart 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.

PythonPart display name in object manager

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"
    )
  1. 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

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>.

<ExcludeIdentical>True</ExcludeIdentical>

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:

pyp_transaction = PythonPartTransaction(self.coord_input.GetInputViewDocument())

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)!
  1. 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.
  2. 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.
  3. 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.
  4. When set to True, the elements are rotated accordingly to consider the crosshair rotation during the creation.
  5. 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