Selection
Info
All the implementation examples and code snippets shown in this article can be used only in an Interactor PythonPart.
Single selection
To perform an element selection, where the user is allowed to click in the viewport and select only one object, introduce following steps:
- Initialize the input. This is done at the beginning of the workflow, i.e. must be implemented in the constructor method of your interactor class
- Optionally apply a filter. See below, how to set up one.
- Perform an element search. This must be done with every mouse movement, i.e. must be implemented in the process_mouse_msg method of the interactor class.
-
Get the element. This action should also be be implemented in the process_mouse_msg method, but only if the mouse was clicked.
-
Now you can introduce any actions e.g., modify the elements.
Info
If the next action should be a selection of another element, reinitialize the selection by calling the InitNextElementInput. This will ensure, that the selection is possible only in the same document, in which the first selection was done.
-
After the actions are done, reinitialize the input by jumping back to the first step. Alternatively, terminate the PythonPart by calling CancelInput
Warning
If the PythonPart should not terminate after the actions are done, it is important to always reinitialize the input at this point! Not doing so, may lead to an unexpected behavior or even a crash.
Example
Here is a simple implementation of a workflow consisting of just one element selection. After an element is selected, some action is introduced and the selection is restarted. No filter is applied for the selection.
flowchart TB
InitInput["initialize
selection"]
SetFilter[apply filter]
ElementSearch["search for
element"]
IsClick{Mouse\nclicked?}
ElementFound{"element
found?"}
GetElement[get the element]
Action[do something...]
ReinitInput["reinitialize
selection"]
End[return]
style Action stroke-dasharray: 5 5
subgraph __init__
direction LR
InitInput --> SetFilter
end
subgraph process_mouse_msg
direction LR
ElementSearch --> ElementFound
ElementFound -->|No| End
ElementFound -->|Yes| IsClick
IsClick -->|Moved| End
subgraph do_something
direction TB
GetElement --> Action
Action --> ReinitInput
end
IsClick -->|Clicked| do_something
do_something --> End
end
__init__ -- "mouse message\nsent" --> process_mouse_msg
class MyInteractor(BaseInteractor):
def __init__(self, coord_input, ...):
self.coord_input = coord_input
prompt_msg = AllplanIFW.InputStringConvert("Select the element")
self.coord_input.InitFirstElementInput(prompt_msg)
self.coord_input.SetElementFilter(sel_filter)
...
def process_mouse_msg(self, mouse_msg, pnt, msg_info):
element_found = self.coord_input.SelectElement(mouse_msg, pnt, msg_info,
True, True, True)
if self.coord_input.IsMouseMove(mouse_msg) or not element_found: #(1)!
return True
selected_element = self.coord_input.GetSelectedElement()
####### here introduce some action #########
self.coord_input.InitFirstElementInput(
AllplanIFW.InputStringConvert("Select the element")) #(2)!
return True
- By calling IsMouseMove you can obtain, whether a mouse click was performed or the mouse was just moved.
- Remember to reinitialize the input after the action is completed!
Start selection
An element selection can be started by calling the InitFirstElementInput like this:
prompt_msg = AllplanIFW.InputStringConvert("Select the element")
self.coord_input.InitFirstElementInput(prompt_msg)
self.coord_input.SetElementFilter(sel_filter) #(1)!
- At this point you can optionally apply a selection filter. See below, how to set up one
This will result in a prompt message appearing in the dialog line (by default located in the bottom left corner of the ALLPLAN UI). In this way you can communicate to the user, what your script is expecting from him.
An element selection can also be started by calling the InitFirstElementValueInput like this:
prompt_msg = AllplanIFW.InputStringConvert("Select the element; input value:")
input_control_type = AllplanIFW.eValueInputControlType.eANGLE_COMBOBOX #(1)!
input_control = AllplanIFW.ValueInputControlData(input_control_type,
bSetFocus = False,
bDisableCoord = False)
self.coord_input.InitFirstElementValueInput(prompt_msg, input_control)
self.coord_input.SetElementFilter(sel_filter) #(2)!
- To see, what input controls are possible and what is their behavior, refer to the documentation of eValueInputControlType and have a look on the example SingleSelection
- At this point you can optionally apply a selection filter. See below, how to set up one
This will result in not only a prompt message, but also an input control appearing in the dialog line. This is a useful feature, when your PythonPart is so simple, that it requires only one or two parameter. In this case it is possible to do the workflow without the property palette.
To read the entered value call the method GetInputControlValue
Tip
Any change in this input control triggers the on_preview_draw event. Pressing Enter triggers the on_value_input_control_enter event.
Element search
Performing an element search does two things:
- gives you bool value, whether an element was found or not
- highlights the element (optionally)
You can either search for an element or for an element geometry
Element search is triggered by calling the SelectElement method.
In this case, the element is found, when the user hovers over the entire element. When found, element info box appears at the cross hair and (optionally) the entire element is highlighted.
This is the standard way of how you should search for elements.
Geometry search is triggered by calling the SelectGeometryElement method.
In this case, the element is found, when the user hovers over the edges of the element, but not the inside of the element. The difference is clearly visible in case of architecture elements in the ground view. This kind of search is particularly useful, when you are interested in the geometry of the element (e.g. you want to get a vertex, edge or face of an element), but not in the element itself.
By default, only the selected geometry is highlighted. Optionally, the entire element can be highlighted.
Getting the element
Getting the element is done by calling the GetSelectedElement method. This actually reads the data from the found element and, for better performance, should be be done only when needed e.g., when the mouse was clicked and an element was found.
def process_mouse_msg(self, ...)
...
if not self.coord_input.IsMouseMove(mouse_msg) and ele_found:
selected_element = self.coord_input.GetSelectedElement()
As a result, you will get a BaseElementAdapter - a type of object, which we describe in detail here.
Tip
If you are interested only in the geometry of the element, you can call the GetSelectedGeometryElement method instead. This is an equivalent to getting the adapter in the first step and geometry from the adapter with GetGeometry in the second step.
Info
The result if the element selection is always the same, regardless of the method used for the element search
Example on GitHub
To try out the different element search methods or see, what impact on the element
selection do the options of CoordinateInput
have. Try out the example
SingleSelection ( PYP | PY)
Multiple selection
If your PythonPart requires a selection of multiple elements and you want to enable the user to select them by specifying a region rather than clicking individual elements, use the InputFunctionStarter class. It provides methods to start and terminate the default element selection functionality of ALLPLAN, where the user can draw a rectangle in the viewport, and all elements inside it will be selected. He can also use the bracket or fence functionality.
Info
After calling StartElementSelect
the selection function is started and it the overloads the process_mouse_msg
event! It means
that as long as the selection is not completed by the user, this event is not called!
Failure
Make sure, that you don't start a selection function, when one is already running, as this will lead to a crash!
To restart an already running selection function, call the RemoveFunction first and then start a new one. Bare in mind, that calling RemoveFunction when no selection function is running also leads to a crash!
Warning
During the selection, the user can define his own filter using ALLPLAN filtering functionality. If he does so, it will overwrite the ElementSelectFilterSetting passed to the StartElementSelect method! Therefore, you must take into account a case, in which the final list of selected elements contains elements that your filter would exclude!
Example
Here is a simple implementation of a workflow consisting of a selection of multiple elements. After at least one element is selected, an action is introduced and the selection is then restarted.
flowchart LR
Init([__init__])
StartSelection["start\nselection"]
ListEmpty{List\nempty?}
GetElements[get\nelement\nlist]
Action[do something...]
style Action stroke-dasharray: 5 5
Init --> StartSelection
StartSelection -->|successful\nselection| GetElements
GetElements --> ListEmpty
ListEmpty -- No --> Action
ListEmpty -- Yes --> StartSelection
Action --> StartSelection
class MyInteractor(BaseInteractor):
def __init__(self, coord_input, ...):
self.post_element_selection = AllplanIFW.PostElementSelection() #(1)!
self.sel_filter = AllplanIFW.ElementSelectFilterSetting()
self.document = coord_input.GetInputViewDocument()
self.start_selection()
...
def process_mouse_msg(self, mouse_msg, pnt, msg_info):
elements = self.post_element_selection.GetSelectedElements(self.document)
if len(elements) == 0: #(2)!
self.start_selection()
return True
####### here introduce some action #########
self.start_selection()
return True
def start_selection(self):
AllplanIFW.InputFunctionStarter.StartElementSelect("Select elements",
self.sel_filter, #(3)!
self.post_element_selection,
markSelectedElements = True)
- This is a special object, where the result of the selection will be saved after the selection is completed
- It is possible, that a selection was successful, but no elements were selected. This is the case, when e.g. the user has drawn a selection rectangle, but no objects were in it. In such case, it makes sense to restart the selection right away.
-
Here is where you can apply a selection filter. In this example, the filter is empty. See below to learn how to set up a filter
Bare in mind, that when the user uses ALLPLAN filtering functionality, this filter will be overwritten!
Face selection
To search for a face of a polyhedron, modify the 4th step from this list. First, getting the element must be performed with every mouse movement in order to correctly highlight the face. Directly after the element adapter is retrieved with GetSelectedElement method, call the SelectPolyhedronFace.
selected_element = self.coord_input.GetSelectedElement()
if polyhedron_ele.IsNull():
return True
is_selected, face_polygon, intersect_result = \
AllplanBaseEle.FaceSelectService.SelectPolyhedronFace(
selected_element,
pnt,
True, #(1)!
self.coord_input.GetViewWorldProjection(),
self.coord_input.GetInputViewDocument(),
False) #(2)!
- This option will highlight the selected face
- If you want to allow the face selection in a UVS, set this to True. If you want to allow the selection only in a UVS, use a dedicated method SelectPolyhedronFaceInUVS
As a result, you will get three elements:
- a boolean value indicating that a face was successfully selected
- a Polygon3D representing the selected face
- an IntersectRayPolyhedron containing information such as normal vector of the selected face or the point on the face that was clicked.
Example on GitHub
See the complete implementation in the example PolyhedronFaceSelection ( PYP | PY).
Tip
For selection of a wall face, we recommend to use a dedicated method SelectWallFace. It is similar to one for selecting polyhedron faces, but snoops only to walls. It also guarantees that the face is always an outer face of the wall. Refer to the example WallFaceSelection to see the full implementation.
Setting up a filter
The selection filter is represented by the ElementSelectFilterSetting class. It contains settings regarding what elements should be valid for selection. These settings have their representation, here's a short overview:
Setting description | Represented by |
---|---|
In which DF to search for elements for (active, passive, both) | eDocumentSnoopType |
In which layer to search for elements for (active, passive, both) | eLayerSnoopType |
Objects valid for selection | SelectionQuery |
classDiagram
direction BT
class ElementSelectFilterSetting{
__init__(SelectionQuery, eDocumentSnoopType, eLayerSnoopType)
SetDocumentSelectType(eDocumentSnoopType)
SetLayerSelectType(eLayerSnoopType)
}
class eDocumentSnoopType{
<<Enumeration>>
eSnoopActiveDocuments
eSnoopPassiveDocuments
eSnoopAllDocuments
}
class eLayerSnoopType{
<<Enumeration>>
eSnoopActiveLayers
eSnoopPassiveLayers
eSnoopAllLayers
}
class SelectionQuery{
__init__(list[QueryTypeID | CustomFilter] query)
}
class QueryTypeID{
__init__(GUID)
}
class CustomFilter
class BaseFilterObject{
<<Abstract>>
__call__(BaseElementAdapter)
}
class GUID
GUID --* "1" QueryTypeID
QueryTypeID --* "0..*" SelectionQuery
CustomFilter --* "0..*" SelectionQuery
BaseFilterObject <|-- CustomFilter
SelectionQuery --* "1" ElementSelectFilterSetting
eDocumentSnoopType --* "1" ElementSelectFilterSetting
eLayerSnoopType --* "1" ElementSelectFilterSetting
Selection query
SelectionQuery is the object responsible for filtering: checking individual elements whether they can be selected or not. It can be a list of element types valid for selection (column, 2D-line, filling, etc...) or callable classes returning a bool value or a mixture of both.
When you want to filter by element types, bare in mind that each type has it`s own GUID (1). What you have to do is create a list of QueryTypeID objects. Each QueryTypeID points to a specific GUID representing the element type allowed for selection.
- The GUIDs have their aliases, so you can refer to them by a readable name. The aliases
are defined as module attributes in the [NemAll_Python_IFW_ElementAdapter] module.
You can recognize them easily, as they all end with
TypeUUID
. E.g. to refer to the UUID of the beam type, callAllplanElementAdapter.Beam_TypeUUID
Example
In the following example, elements valid for selection are only columns and beams, located on active documents and layers:
type_uuids = [AllplanIFW.QueryTypeID(AllplanElementAdapter.Beam_TypeUUID), #(1)!
AllplanIFW.QueryTypeID(AllplanElementAdapter.Column_TypeUUID)]
selection_query = AllplanIFW.SelectionQuery(type_uuids)
ele_select_filter = AllplanIFW.ElementSelectFilterSetting(selection_query,
bSnoopAllElements = False)
- The GUIDs of all possible element types are defined as module attributes inside the NemAll_Python_IFW_ElementAdapter module.
Tip
The element types have nothing to do with Python classes. It may happen, that two different python objects of the same class represent two elements with different Type_UUIDs in the DF. An example of that is ModelElement3D:
- when the geometry object used to construct it was a polyhedron the resulting element will be of type Volume3D_TypeUUID
- when the geometry object used to construct it was a brep the resulting element will be of type BRep3D_Volume_TypeUUID
You can check the type of the element by calling GetElementAdapterType.GetTypeName on the element adapter
If you want to filter elements by a callable class, simply define a class
with a __call__
member (1). It must have one argument of type
BaseElementAdapter
and must return a bool value: True when the element is valid for selection,
False otherwise. You can use the BaseFilterObject
as a base class to make sure, your implementation is valid.
- We called it CustomFilter on the graph above, but name it as you want.
Example
In the following example, elements valid for selection are the one represented with a polyhedron with exactly 6 faces. The elements must be located on active documents and layers:
class CustomFilter():
def __call__(self, element: AllplanElementAdapter.BaseElementAdapter) -> bool:
if isinstance(geo := element.GetModelGeometry(), AllplanGeometry.Polyhedron3D):
return geo.GetFacesCount() == 6
return False
selection_query = AllplanIFW.SelectionQuery(CustomFilter())
ele_select_filter = AllplanIFW.ElementSelectFilterSetting(selection_query,
bSnoopAllElements = False)
You can also mix the list of element types and your custom filter.
Example
In the following example, elements valid for selection are only columns and beams, represented by a polyhedron with exactly 6 faces. The elements must be located on active documents and layers:
type_uuids = [AllplanIFW.QueryTypeID(AllplanElementAdapter.Beam_TypeUUID),
AllplanIFW.QueryTypeID(AllplanElementAdapter.Column_TypeUUID)]
class CustomFilter():
def __call__(self, element: AllplanElementAdapter.BaseElementAdapter) -> bool:
if isinstance(geo := element.GetModelGeometry(), AllplanGeometry.Polyhedron3D):
return geo.GetFacesCount() == 6
return False
selection_query = AllplanIFW.SelectionQuery([CustomFilter()] + type_uuids) #(1)!
ele_select_filter = AllplanIFW.ElementSelectFilterSetting(selection_query,
bSnoopAllElements = False)
-
With this statement, the list with element types and
CustomFilter
will be added together with AND operand: a valid element must be a beam or column AND be a polyhedron with 6 faces.If you want to combine the filters with an OR operand, you can use the FilterCollection and implement the filter like this:
Tip
PythonParts framework offers some predefined filters, e.g. for filtering out 2D/3D curves only. Have a look at the ElementFilter module - you might find a filter suiting your use-case there and avoid reinventing the wheel.