Handles
Handles are a feature of a PythonPart, which improves the user's experience by allowing him to edit the geometry (or in some cases also non-geometrical properties) directly in the model view. The user can be provided a possibility to perform various operations e.g.:
- changing a dimension with dimension handle
- define a coordinate with coordinate handle
- execute action with button handle
- hiding or displaying a component with checkbox handle
- increment a dimension with increment handle
- input an angle with rotation handle
A handle can be assigned to a PythonPart parameter. If that is the case, it is possible to show an input field directly inside the viewport.
Definition
Handle is created by instantiating the class HandleProperties. Object of this class represents a handle. Handle processing differs depending on whether the PythonPart is an interactor or not. Please refer to the relevant subsection to learn more.
The composition of the HandleProperties class is shown below.
classDiagram
direction BT
class Point3D {
float X
float Y
float Z
}
class ElementHandleType{
<<Enumeration>>
HANDLE_ARROW
HANDLE_CIRCLE
HANDLE_SQUARE_BLUE
HANDLE_SQUARE_EMPTY
HANDLE_SQUARE_RED
HANDLE_SQUARE_RIGHT
}
class HandleDirection{
<<Enumeration>>
X_DIR
Y_DIR
Z_DIR
XY_DIR
XZ_DIR
YZ_DIR
XYZ_DIR
POINT_DIR
ANGLE
Z_COORD
PLANE_DIR
VECTOR_DIR
CLICK
}
class HandleParameterType{
<<Enumeration>>
X_DISTANCE
Y_DISTANCE
Z_DISTANCE
POINT
POINT_DISTANCE
ANGLE
Z_COORD
VECTOR_DISTANCE
CHECK_BOX
INCREMENT_BUTTON
DECREMENT_BUTTON
}
class HandleParameterData{
<<Dataclass>>
+str param_prop_name
+HandleParameterType param_type
}
class HandleProperties{
+str handle_id
+Point3D handle_point
+Point3D ref_point
+List[HandleParameterData] parameter_data
+HandleDirection handle_move_dir
+ElementHandleType handle_type
}
Point3D "2" ..> HandleProperties
ElementHandleType ..> HandleProperties
HandleDirection ..> HandleProperties
HandleParameterData ..> HandleProperties
HandleParameterType ..> HandleParameterData
Handle type
The enumeration class ElementHandleType defines the layout of the handle. The default value when instantiating the class HandleProperties is HANDLE_CIRCLE and can be changed to one of the following:
Layout | Type |
---|---|
HANDLE_ARROW | |
HANDLE_CIRCLE | |
HANDLE_SQUARE_BLUE | |
HANDLE_SQUARE_EMPTY | |
HANDLE_SQUARE_RED | |
HANDLE_SQUARE_RIGHT |
Handle parameter data
The data class HandleParameterData connects the handle to a specific PythonPart parameter defined in the .pyp file. Thanks to this, a parameter value is recalculated every time a connected handle is modified. The recalculation is performed depending on the defined HandleParameterType.
Handle parameter type
The enumeration class HandleParameterType defines how the value of the parameter property assigned to the handle should be recalculated, when the handle is modified. Please refer to the API reference to see, what recalculation methods are available.
Handle direction
The enumeration class HandleDirection defines the allowed direction, in which the handle can be moved when being modified. Please refer to the API reference to see the available options.
Processing
In standard PythonPart
Creation
In order to create a standard PythonPart with handles, organize the HandleProperties
objects representing the handles in a list, and assign them to the handles
attribute of the
CreateElementResult object, returned at the end of the
create_element()
function:
def create_element(build_ele, doc):
...
handle_1 = HandleProperties(...)
handle_2 = HandleProperties(...)
handle_list = [handle_1, handle_2]
...
return CreateElementResult(elements = model_elements_list,
handles = handle_list)
Modification
After every handle modification, the new value of the property must be written back in the property
palette and the element must be recreated. To handle this process, implement the function
move_handle()
in your script. Use the
HandlePropertiesService to update the parameter
values
Example
def move_handle(build_ele,
handle_prop: HandleProperties,
input_pnt: NemAll_Python_Geometry.Point3D,
doc: NemAll_Python_IFW_ElementAdapter.DocumentAdapter):
if handle_prop.handle_id == build_ele.MirrorCuboid.name: #(1)!
build_ele.MirrorCuboid.value = not build_ele.MirrorCuboid.value
else:
HandlePropertiesService.update_property_value(build_ele, handle_prop, input_pnt) #(3)!
return create_element(build_ele, doc) #(2)!
- You can process a specific handle (in this case the
MirrorCuboid
handle) differently. as the framework gives the handle name to themove_handle()
function. - The PythonPart must be recalculated with every movement of the handle.
- For a standard case you just need to implement this line of code in the
move_handle
function. The update_property_value method updates the values in the property palette after the handle is modified.
In Script Object
Implementing a handle functionality in a PythonPart implemented as a script object looks exactly the same, as in a standard PythonPart. Have a look on the paragraph above.
Handling the event of a handle being moved is also very similar: instead of a function,
you have to implement a method, i.e. move_handle
.
It gets called after the user selects a handle and moves it around. Each movement triggers
a call, so the geometry can be recalculated and redrawn on the run. The handle_prop
argument
contains the modified handle, and the input_point
the position of the mouse on the screen.
Like in a standard PythonPart, use the HandlePropertiesService
to update the parameter values inside the BuildingElement.
At the end, call the execute
method to recalculate the
geometry with these new values:
def move_handle(self,
handle_prop: HandleProperties,
input_pnt : AllplanGeo.Point3D) -> CreateElementResult:
HandlePropertiesService.update_property_value(self.build_ele, handle_prop, input_pnt)
return self.execute()
Example
The complete implementation is shown in the PythonPart Handles, located in:
- ...\Etc\PythonPartsExampleScripts\BasisExamples\General\Handles.py
- ...\Etc\Examples\PythonParts\BasisExamples\General\Handles.pyp
In interactor
In an interactor PythonPart, the class HandleModificationService can be used to implement the handle processing. The service can be created like
Depending on an defined InputMode, the HandleModificationService can be used for the selection and modification of the handles as follows
if input_mode == InputMode.RefPoint:
input_pnt = coord_input.GetInputPoint(mouse_msg, pnt, msg_info).GetPoint()
...
#----------------- Select a handle
else:
result = handle_modi_service.process_mouse_msg(mouse_msg, pnt, msg_info)
In the member function process_mouse_msg the selection and modification of the handle is executed and the value of the assigned property is checked and updated.
Example
A full implementation of the HandleModificationService processing, including how to start and stop the handle input, is provided in HandleUsageInInteractor located in:
- …\etc\Examples\PythonParts\InteractorExamplesGeneral\HandleUsageInInteractor.pyp
- …\etc\PythonPartsExampleScripts\InteractorExamplesGeneral\HandleUsageInInteractor.py
Examples
All the examples shown here are included into one PythonPart Handles, located in:
- ...\Etc\PythonPartsExampleScripts\BasisExamples\General\Handles.py
- ...\Etc\Examples\PythonParts\BasisExamples\General\Handles.pyp
Dimension handle
Let's say we have a simple geometry (a cube), and we want to let the user control a single dimension of it (the height) with a handle. The implementation can look like this:
handle_parameter_data = HandleParameterData("CubeHeight", HandleParameterType.Z_DISTANCE) #(1)!
handle_height = HandleProperties("HeightHandle",
AllplanGeo.Point3D(0,0, build_ele.CubeHeight.value),
AllplanGeo.Point3D(),
[handle_parameter_data], #(2)!
HandleDirection.Z_DIR) #(3)!
handle_list.append(handle_height)
- The handle is connected here to the parameter
CubeHeight
with theZ_DISTANCE
recalculation method, meaning that after every handle modification a distance vector between reference point and handle point is calculated and only the Z component is being put back a value to the parameterCubeHeigth
. - Here we could let one handle affect several parameters in the property palette, each in a different way.
- The handle should move only in local Z-direction of our PythonPart.
Coordinate handle
Let's say, we want to let the user control the ground view dimensions (length and width) of the PythonPart with a single handle.
handle_parameter_data_list = [HandleParameterData("CubeLength",
HandleParameterType.X_DISTANCE,
False), #(3)!
HandleParameterData("CubeWidth",
HandleParameterType.Y_DISTANCE,
False)] #(1)!
handle_length_width = HandleProperties("LengthWidthHandle",
AllplanGeo.Point3D(build_ele.CubeLength.value,
build_ele.CubeWidth.value,
0),
AllplanGeo.Point3D(),
handle_parameter_data_list,
HandleDirection.XY_DIR)
handle_length_width.info_text = "Ground view dimensions" #(2)!
- Two parameters should be assigned to this handle therefore a list is defined here.
- The string in the optional parameter
info_text
will be displayed ever time the user hovers the handle with the mouse. - Unlike in the first example, the input field in the model view is hidden.
Button handle
Let's say we want the handle to perform some action (mirroring the object). We can place a button in the palette or define a button handle, visible directly in the model view to do that.
<Parameter><!--(1)!-->
<Name>MirrorCuboid</Name>
<Value>False</Value>
<ValueType>CheckBox</ValueType>
</Parameter>
- To be able to temporarily save the mirrored-status of the PythonPart a hidden parameter must be defined. In an Interactor PythonPart we could save this information ex. in a property of the interactor class.
mirror_handle = HandleProperties("MirrorCuboid",
point5,
AllplanGeo.Point3D(),
[], #(1)!
HandleDirection.CLICK) #(2)!
mirror_handle.handle_type = AllplanIFW.ElementHandleType.HANDLE_ARROW
mirror_handle.info_text = "Mirror the cuboid"
mirror_handle.handle_angle = rot_angle #(3)!
handle_list.append(mirror_handle)
- Note that no parameter is assigned to the handle. The handle processing is done
entirely in
move_handle()
function by the assigned handle IDMirrorCuboid
. - By assigning the handle direction
CLICK
the handle cannot be moved. Themove_handle()
is called, when the handle is clicked. - The handle is rotated in XY plane to align with the rotation of the object.
def move_handle(build_ele,
handle_prop: HandleProperties,
input_pnt: AllplanGeo.Point3D,
doc: AllplanEleAdapter.DocumentAdapter):
if handle_prop.handle_id == "MirrorCuboid": #(1)!
build_ele.MirrorCuboid.value = not build_ele.MirrorCuboid.value #(2)!
return create_element(build_ele, doc)
- The name of the handle is stored in the property handle_id
- We use an invisible parameter to temporarily save the mirrored status. The value of the parameter is changed here every time the handle is clicked.
List parameter
Let's say we have a parameter defined as a python list containing Point3D and we want to define a handle for each of these points.
handle_list = []
for index, pnt in enumerate(build_ele.PolyPoints.value): #(2)!
handle_param_data = HandleParameterData("PolyPoints",
HandleParameterType.POINT,
False, #(4)!
list_index = index) #(1)!
handle_list.append(HandleProperties("PolyPoints",
pnt,
AllplanGeo.Point3D(),
[handle_param_data],
HandleDirection.XYZ_DIR))
handle_list[-1].info_text = "Point " + str(index + 1) #(3)!
- As the parameter is defined as list, this argument is now required. The parameter
PolyPoints
is a one-dimensional list, therefore thelist_index
is an integer. In case of a two-dimensional list, a list of two integer values must be provided:[index_row, index_column]
. - As separate handle must be generated for each point, implementation in a loop is the best solution.
- Here we change the info text for the appended handle.
- We don't want the input fields to be shown for these handles.
Value decrement / increment
Let's say we do not want to allow the user to set a value of a certain parameter (ex. wall thickness) freely, but only increment or decrement it by 100 mm instead.
<Parameter>
<Name>Thickness</Name>
<Text>Thickness</Text>
<Value>1000.</Value>
<ValueType>Length</ValueType>
<MinValue>0</MinValue>
<MaxValue>10000</MaxValue>
<IntervalValue>100</IntervalValue><!--(1)!-->
</Parameter>
- We do want the thickness to increment in 100 mm steps, therefore we also need to constrain the parameter in the property palette separately. Otherwise, the user could change the thickness freely using the palette.
#---------- incrementation handle
increm_handle_param_data = [HandleParameterData("Thickness", #(1)!
HandleParameterType.INCREMENT_BUTTON,
in_decrement_value = 100.)]
increment_handle = HandleProperties(handle_id= "IncrementHandle",
handle_point= point2 + AllplanGeo.Point3D(200, 100, 0),
ref_point= AllplanGeo.Point3D(), #(2)!
handle_param_data= increm_handle_param_data,
handle_move_dir= HandleDirection.CLICK #(3)!
info_text= "Increment thickness by 0.1m")
handle_list.append(increment_handle)
#---------- decrementation handle
decrem_handle_param_data = [HandleParameterData("Thickness", #(4)!
HandleParameterType.DECREMENT_BUTTON,
in_decrement_value = 100.)]
decrement_handle = HandleProperties(handle_id= "DecrementHandle",
handle_point= point2 + AllplanGeo.Point3D(200, -100, 0),
ref_point= AllplanGeo.Point3D(),
handle_param_data= decrem_handle_param_data,
handle_move_dir= HandleDirection.CLICK,
info_text= "Decrement thickness by 0.1m")
decrement_handle.handle_angle = AllplanGeo.Angle(math.pi)
handle_list.append(decrement_handle)
- Defining the handle parameter type this way implies adding the value of 100 mm to the parameter thickness.
- In case of a button handle, this argument is not relevant and can be set to (0,0,0).
- The increment button can be clicked, but not moved. This behaviour is defined here.
- Decrementing a value is a different recaltulation behaviour therefore a separate handle must be defined for that and it must be assigned to the same thickness parameter.
Rotation handle
Let's say we want to allow the user to input an angle (ex. to rotate a geometry) using handle. In this case we need to implement a handle like this:
rot_handle_param_data = HandleParameterData("RotAngleZ",
HandleParameterType.ANGLE) #(1)!
handle_z_rot = HandleProperties(handle_id= "Z_Rotation",
handle_point= poly_points[1], #(2)!
ref_point= poly_points[1], #(3)!
handle_param_data= [rot_handle_param_data],
handle_move_dir= HandleDirection.ANGLE, #(4)!
abs_value= True,
info_text= "Z rotation handle")
handle_z_rot.info_text = "Z rotation handle"
handle_list.append(handle_z_rot)
- This option implies, that an angle value is being calculated
between
handle_point
andref_point
- The handle will be drawn in this point.
-
Reference point for the value calculation. The value of the angle will be calculated between the local X-axis with the origin in the
ref_point
, and the line going through theref_point
and thehandle_point
. This implies a rotation around the local Z-Axis. By default, the directions of the local axes are the same as the global one and can be changed by providing an optional argumentangle_placement
, ex. like this: -
A handle can be moved in any direction, just like with
XYZ_DIR
, but an arc with arrow will be drawn when hovering with the mouse over the handle to indicate a rotation.