Connections
Thanks to connections, a PythonPart element can interact with other, native ALLPLAN elements or react to changes done on them. There are several types of connections resulting in different behavior.
-
Association
A PythonPart element can be associated with another element (also another PythonPart) so that any modification of this element triggers the update of the connected PythonPart.
-
Docking points
Docking points in a PythonPart element allows the user to connect other native elements, such as associative dimension lines, with these points. After modification of the PythonPart, these elements gets updated
-
Plane connection
A PythonPart element can be connected with planes defined in the ALLPLAN Floor Manager. This will result in the PythonPart being updated whenever the user changes the planes in the Floor Manager
Association
Association is a directional relationship. This means, that there is always a source element (aka parent) and a target element (aka child).
Modification of the source triggers the update of the target
flowchart LR
Source["`**Source**
any element`"]
Target["`**Target**
PythonPart`"]
Source-->|association| Target
style Source stroke:#0000CC
style Target stroke:#CC0000
Because an update can be performed only on a PythonPart and not on a native ALLPLAN element (such as wall or 3D object), the PythonPart will always be the target.
Info
Under the hood, during the update the framework simulates reactivation of the PythonPart and terminating it directly after, just like the user would double-click on PythonPart and then immediately hit Esc. The palette and handles are suppressed during this process.
Associate with any element
Here are the steps you need to follow, to create a one-directional association between your PythonPart (target) and other element (source) of any kind.
Step 1: Define some parameter
In the third step, you will be saving a string derived from the source element in a parameter. Therefore, define beforehand a string parameter for it e.g., like:
<Parameter>
<Name>ConnectedElementHash</Name>
<Value></Value>
<ValueType>String</ValueType>
</Parameter>
Why?
Why do I need to save some data from the source element?
ALLPLAN checks, whether a PythonPart was changed, based on parameter values. If all parameters stays the same, PythonPart won't get updated.
Imagine following scenario: The user has placed the PythonPart and associated it with an element. Now he modifies the element, which triggers the PythonPart update. But because none of the PythonPart parameters are changed (only the source element), it's not updated.
Solution: the change of the source element must result in a change of any of the PythonPart parameters. It's up to you, what kind of changes in the source element should influence this parameter. E.g. you may only want to react to changes in geometry, but not in attributes.
Step 2: Get the base element adapter to the source element
In the creation mode, you want the user to select the source element, so you need to introduce an element selection. If you are creating your PythonPart as a script object, refer to this article to learn, how to do it. If you are creating an interactor, refer to this article.
Whatever your case is, let's assume for further considerations, that the element adapter is
saved under self.source_element
In modification mode, the PythonPart is already associated with the source element so
you can get the BaseElementAdapter
object bu using the associative framework, like:
if self.is_modification_mode: #(2)!
pythonpart_uuid = self.modification_ele_list.get_base_element_adapter(self.document).GetModelElementUUID()
observed_elements = AllplanBaseElements.AssociationService.GetObservedElements(self.document, pythonpart_uuid)
if len(observed_elements) != 0:
self.source_element = observed_elements[0] #(1)!
- You can associate your PythonPart with more than one element, thus
observed_elements
is a list. We are considering a simple example so let's assume that there's only one element in the list. -
This statement is valid for a script object. In an interactor use:
Using the associative framework for the job has the advantage, that you don't have to handle edge cases, like e.g., what happens when both PythonPart and the associated elements are copied. This will be handled by the associative framework.
Step 3: Save something related to the source element in PythonPart parameter
This something can really be anything: a string, integer, etc. The main principle: it must change only, when you want your PythonPart to be updated.
For example: if you want the PythonPart to update, when the geometry of the source element changes, get the geometry, turn it to a string, hash it and save it in a parameter, like:
import hashlib
...
geometry_string = repr(self.source_element.GetGeometry()) #(1)!
build_ele.SourceElementHash.value = hashlib.sha224(geometry_string.encode()).hexdigest()
- For a geometry of type
Polyhedron3D
orBRep3D
, it is better to use theWriteToStream()
method because the repr-string may not contain all the data.
In this way, the hash will change when the geometry of the source element changes and not when e.g. the attributes of format properties change. The PythonPart will then be updated only when really needed.
Step 4: Create PythonPart
Now introduce the business logic and calculate the geometry of your PythonPart. You can use any data from the source element e.g., its geometry:
self.source_element
is aBaseElementAdapter
. Refer to this article to learn more what information can you get from it and how to handle it.
Step 5: Associate the Python Part with the source element
Construct a ConnectToElements object and save the UUID of the source element in it. :
- It is possible to associate the PythonPart with multiple elements, thus provide a list. Modification of any of these elements would trigger the update of the PythonPart.
Bug
For now, please make sure to associate only in the creation mode, e.g. like:
ConnectToElements([str(self.source_element.GetModelElementUUID())] is self.is_modification_mode else [])
Re-associating again with every modification could result in PythonPart being updated multiple times.
In a script object PythonPart, when constructing the
CreateElementResult in the
execute()
method, provide additional data fields
connect_to_ele
and uuid_parameter_name
, like:
In an interactor PythonPart, do not use the CreateElements function to create the elements. Instead, use the PythonPartTransaction (also described in this article), like:
pyp_transaction = PythonPartTransaction(self.document,
connect_to_ele = connect_to_elements) #(1)!
...
pyp_transaction.execute(placement_matrix,
coord_input.GetViewWorldProjection(),
model_element_list,
self.modification_ele_list)
- Here is where you provide the
ConnectToElements
object
Tip
Steps 1-3 can be implemented in the __init__()
method of your script object or interactor.
Step 4 is up to you - it is your business logic. Step 5 should be implemented in the
execute()
method in case of script object or,
in case of an interactor, wherever the element creation is implemented (presumably in
the on_cancel_function)
Troubleshooting
Connecting a PythonPart to another element may cause problems in certain situations, so it is crucial to consider them beforehand, during the design of a PythonPart. Pay attention to the following:
-
Prepare your PythonPart for being updated
Make sure that reactivating your PythonPart with double-click and hitting Esc directly after that leads to clean termination. If that is not the case, ALLPLAN will get stuck during the update.
-
Test all modification scenarios
Most of the time, there is more than one way how a native ALLPLAN element can be modified. For example, a wall can be modified by: handles, using stretching entities function, changing plane references, using join components function, etc. Test your PythonPart by reproducing all possible modification scenarios and check, if the PythonPart update is done correctly.
-
Test undo function
After each modification of the source element, try to undo the changes and check, if both the modification of the source element, as well as the connected PythonPart is undone successfully
-
Prepare your PythonPart for the source element being deleted or moved to another DF
ALLPLAN does not handle these cases automatically. Your PythonPart will remain undeleted, but after reactivating it, the element adapter in the see step 2 will be a null object.
If you notice any unexpected behavior of ALLPLAN, please report the problem.
Example on GitHub
The complete implementation is shown in the example PythonPartElementConnection ( PYP | PY).
Docking points
Docking points in ALLPLAN are geometrically relevant points in the 3D space, like e.g. end point of an edge, middle point of a line, crossing point of two lines, etc. The idea is: a PythonPart docked to these points should adapt, when one of them moves. Example: the red line is a PythonPart. The start and end points of it are docked to the mid-points of the two black lines. Changing one black line triggers the update of the PythonPart.
-
Below you can learn how to create docking points in your PythonPart, so that other elements (e.g. dimension lines) can dock to it.
-
Further below you can learn how to dock your PythonPart to other, already existing elements using docking points.
How to create docking points
How the docking points needs to be created depends on two things:
- number of geometry elements in the PythonPart
- number and order of points in each of these elements
Based on that, there are three cases to be considered. Choose yours and dive into it:
-
In a PythonPart, in which both the number of geometry elements and number of points in each of them are static, the docking points are created automatically without further action needed.
-
When the number of geometry elements is dynamic, but number of the points in each of these elements is static, scroll down below
-
When both the number of geometry elements and number of the points in each of these elements are dynamic, scroll further down below
Dynamic element count
In a PythonPart, in which:
- the geometry element count and order are dynamic AND
- the point count and order inside the geometry elements is static
The docking points must be made unique. To do it, a docking point key must be assigned to each model element of the PythonPart. This must be implemented in the script using the SetDockingPointsKey method:
polyhedron_element = AllplanBasisEle.ModelElement3D(com_prop, polyhed)
polyhedron_element.SetDockingPointsKey("Polyhed")
The docking point keys should be assigned in a way that the elements, whose points can be used for meaningful dimension lines, always become the same key.
Example
In the following example, a PythonPart is created with a dynamic count of polyhedrons.
The docking point keys are created from the left and right side, meaning that the
far-left and far-right polyhedrons always become the same docking point key,
the L0
and R0
respectively. The objects next to them become the keys
L1
and R1
respectively and so one...
This results in last polyhedrons having the same docking point key, regardless of the total number of the polyhedrons. This makes it possible to place an associative dimension line from the far-left to the far-right polyhedron, which will remain after changing the count of polyhedrons:
Dynamic PythonPart
In a PythonPart, in which:
- the geometry element count and order are dynamic AND
- the point count and order inside the geometry elements is dynamic
The function create_docking_points must be implemented in the script. This function must be used only for creating the docking points of the dynamic elements. For the creation of the docking points of the other elements, only the docking point key must be set.
Example
The following example shows a PythonPart element with possible recesses at the left and right side. The docking point keys are represented by a unique name of the element and the point index, as shown below. The number of docking points is reduced to the docking points that are still present in the case where a recess is missing. This allows to adjust the dimension lines in case of a missing recess:
Example on GitHub
The complete implementation of creating docking points is shown in the example DockingPoints ( PYP | PY).
To evaluate the functionality of the docking points, perform following steps:
- start ALLPLAN and create a DockingPoints PythonPart
- create associative dimension lines using the docking points of the PythonPart
- change the created PythonPart by modifying the size, number of elements or by disabling the recesses
- see, how the associative dimension lines are adjusted
How to dock to a docking point
Docking point as placement point
If you want to offer the possibility to place your PythonPart on a docking point, simply add following parameter to the PYP:
<Parameter>
<Name>__PlacementPointConnection__</Name><!--(1)!-->
<Text>Placement point connection</Text>
<Value>False</Value>
<ValueType>PointConnection</ValueType>
</Parameter>
- The name must be
__PlacementPointConnection__
. This tells the PythonPart framework, that this docking point should be used as the placement point of the PythonPart.
The parameter creates a button on the palette. Pressing it while placing the PythonPart, activates the docking point search. When hovering over some relevant point in the viewport (like intersection of two lines), a blue target symbol is shown (see on the right). This means, that a valid docking point has been recognized. Clicking it will result in the PythonPart being docked to this point.
When the docking point changes, the PythonPart automatically moves to the new position.
Example on GitHub
The full implementation is shown in the example PythonPartPlacementPointConnection ( PYP | PY).
Using docking points in the script
You might want to use the docking point in the calculation of your PythonPart, rather than only as a placement point. E.g. you might prompt for a start and end point and use them to span an element between them. For this use-case, follow these steps:
-
Define docking point parameter(s) in the PYP file. For example:
<Parameter> <Name>StartPointConnection</Name><!--(1)!--> <Text>Start point connection</Text> <Value>False</Value> <ValueType>PointConnection</ValueType> </Parameter>
- In this case, use any name you want except for
__PlacementPointConnection__
, as this is reserved for placement point connection
- In this case, use any name you want except for
-
Use the
DockingPointInteractor
to start the input:from ScriptObjectInteractors.DockingPointInteractor import DockingPointInteractor ... def start_input(self): if self.is_modification_mode: #(1)! return self.script_object_interactor = DockingPointInteractor( self.build_ele.StartPointConnection #(2)! self.preview_function) #(3)!
- Remember to start the input only in the creation mode.
- Tell the interactor, in which parameter you want to save the specified docking point
-
Optionally you can provide a preview function. It will get called each time the user hovers over a valid docking point. A very simple preview function may look like this:
def preview_function(self): if not self.build_ele.StartPointConnection.value.is_valid() return line = AllplanGeo.Line3D(if self.build_ele.StartPointConnection.value.point, AllplanGeo.Point3D()) model_ele_list = ModelEleList() model_ele_list.set_color(6) model_ele_list.append_geometry_3d(line) AllplanBaseElements.DrawElementPreview(self.document, AllplanGeo.Matrix3D(), model_ele_list, False, None)
In the
start_next_input()
method, validate that the selected docking point and terminate the selection only if it is the case:def start_next_input(self): if not self.build_ele.StartPointConnection.value.is_valid(): return self.script_object_interactor = None
Now the docking point is in the
build_ele
. The parameter value is of typePointConnection
. To get a Point3D for further processing, get thepoint
property, like:To get a docking Point in an interactor, you need to build a point input in a very similar way to the one described in this article. Here are the steps, you need to introduce:
- This step looks the same, as for the point input
-
To search for the docking point in the
process_mouse_msg
call theGetInputDockingPoint
, like: -
When the mouse was clicked, validate the point and save it in the parameter:
from ValueTypes.Data.PointConnection import PointConnection ... if not self.__coord_input.IsMouseMove(mouse_msg) and docking_point.IsValid() parameter_value = PointConnection(self.docking_point) #(1)! self.build_ele.StartPointConnection.value = parameter_value
- Wrap the
DockingPoint
in aPointConnection
object, since this is the right type for the parameter defined as<ValueType>PointConnection</ValueType>
.
- Wrap the
Following steps look exactly the same, as in the article about point input
Warning
When the docking point is changed, the PythonPart docked to it gets updated. This mean, that you have to prepare your PythonPart for being updated!
Make sure, that reactivating it with double-click and hitting Esc directly afterwards leads to a clean script termination.
Plane connection
Planes in ALLPLAN can represent e.g. the top or bottom of a floor ina building. A PythonPart can refer to these planes, meaning that modifying a plane will result in the PythonPart to adapt automatically.
A reference to a plane is represented by a PlaneReferences object. You can add a parameter of this type to your .pyp file (learn how). Then, in your script, access the object like any other parameter:
Now, you can read the bottom elevation and the height of the current floor from plane_ref
to create
a geometrical element that fits into the floor:
cuboid_geo = AllplanGeo.Polyhedron3D.CreateCuboid(
build_ele.Width.value,
build_ele.Depth.value,
plane_ref.Height #(1)!
)
cuboid_geo = AllplanGeo.Move(
cuboid_geo,
AllplanGeo.Vector3D(0, 0, plane_ref.AbsBottomElevation) #(2)!
)
- Create a cuboid with height equal to the floor height
- Move the cuboid so that its bottom matches the bottom of the floor
Warning
When the planes are changed by the user in ALLPLAN's floor manager, all PythonParts having this type of parameter are checked and recalculated, so
prepare your PythonPart for being updated!.
Do this by making sure, that reactivating your PythonPart with double-click and hitting Esc directly afterwards leads to a clean script termination. Otherwise, ALLPLAN can get stuck during the update.
Surface planes
The referred plane is usually horizontal and have constant elevation. But it also can be sloped flat plane (like the red one on the image to the right) or arbitrary (the green one). If you want your PythonPart to adapt also to this kind of planes, you need to read their geometry, and these can by of type:
- Plane3D
- Polyhedron3D or
- BRep3D or
Use the BottomTopPlaneService
to get the underlying PlaneReferences
object,
like: