Skip to content

Selection

Abstract

Single selection

Follow these implementation steps, when you want to introduce an input step of picking one element from the model:

Use classes of the SingleElementSelectInteractor module in your script object class as follows:

  1. Define instance attribute for saving the selection results. Is should be a SingleElementSelectResult. Do it in the constructor, like:

    def __init__(self, build_ele, script_object_data):
        ...
        self.selection_result = SingleElementSelectResult()
    
  2. Optionally set up a filter (learn more below )

  3. Initialize the selection by creating an instance of SingleElementSelectInteractor and assigning it to the script_object_interactor property of your script object class. Depending on when you want the selection to be started, implement it in an appropriate method (learn more here). In this example, the selection starts right after the PythonPart is started, so the right place is the start_input() method.

    def start_input(self):
        self.script_object_interactor = SingleElementSelectInteractor(
            self.selection_result,
            selection_filter,   # (1)!
            prompt_msg = "Select the element" #(2)!
        )
    
    1. If you have set up a filter in step 2, apply it here.
    2. Provide a meaningful prompt to the user for a better UX. It will be shown in the dialog line, where such instructions are expected to appear.
  4. Terminate the input by removing the reference to the SingleElementSelectInteractor from the script_object_interactor property. This needs to be done in the start_next_input()

    def start_next_input(self):
        self.script_object_interactor = None
    

To access the selected element, read the property sel_element of the result data class. To access the geometry of the selected element, read the property sel_geo_ele:

print(self.selection_result.sel_element)
print(self.selection_result.sel_geo_ele)

Example on GitHub

The entire implementation of a single element selection is shown in the example PolyhedronRepair ( PYP | PY)

  1. Optionally set up a filter. Learn more below .

  2. Initialize the selection. Do it at the beginning, in the constructor.

    Call InitFirstElementInput() like:

    def __init__(...):
        ...
        prompt_msg = AllplanIFW.InputStringConvert("Select the element") #(2)!
        self.coord_input.InitFirstElementInput(prompt_msg)
        self.coord_input.SetElementFilter(sel_filter) #(1)!
    
    1. If you've defined a filter in step 1, apply it here
    2. Provide a meaningful message to the user. Something better than this example.

    This shows the prompt message in the dialog line. It looks like this:

    Prompt message in dialog line

    Call InitFirstElementValueInput() like:

    def __init__(...):
        ...
        prompt_msg = AllplanIFW.InputStringConvert("Select the element or input value:") #(3)!
    
        input_control = AllplanIFW.ValueInputControlData(
            AllplanIFW.eValueInputControlType.eANGLE_COMBOBOX, #(1)!
            bSetFocus = False,
            bDisableCoord = False
        )
    
        self.coord_input.InitFirstElementValueInput(prompt_msg, input_control)
        self.coord_input.SetElementFilter(sel_filter) #(2)!
    
    1. See eValueInputControlType for available types of the input field.
    2. If you've defined a filter in step 1, apply it here.
    3. Provide a meaningful message to the user. Something better than this example.

    Input control in dialog line

    This shows the prompt message and an input field in the dialog line. You can call GetInputControlValue() any time in the script to retrieve this value

    Tip

    Any change in this input field triggers the on_preview_draw() event. Pressing Enter triggers the on_value_input_control_enter() event.

    At this stage, you can call other functions affecting the way, how the selection is done. You can e.g., allow selecting elements in the wizard window:

    self.coord_input.EnableAssistWndClick(value)
    
  3. Perform element search. You can either search for an element or for a geometry. Your choice will impact the UX (the highlighting behavior is a bit different, see GIFs below), but it won't impact the result of the selection. Regardless, which one you choose, implement it in process_mouse_msg() event:

    To search for an element, call SelectElement():

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):
        ...
        found = self.coord_input.SelectElement(
            mouse_msg, pnt, msg_info, 
            bHighlight = True,
            bSelAlways = False,
            bAllowCenter = False
        )
    

    Element search

    In this case, the entire element is highlighted when hovered. Additionally, the info box appears at the cross hair. From UX point of view, this kind of search is recommended, when the input is about selecting an element as a whole: geometry and its meta-data (attributes).

    To search for a geometry, call SelectGeometryElement().

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):
        ...
        found = self.coord_input.SelectGeometryElement(
            mouse_msg, pnt, msg_info, 
            bHIghlightCompleteElement = False #(1)!
        )
    
    1. By default, only the selected geometry is highlighted. Optionally, the entire element can be highlighted.

    Geometry search

    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 recommended, when the input is about selecting pure geometry of the element (e.g. you want to get a vertex, edge or face of an element) and not its meta-data.

  4. Retrieve the selected element by calling GetSelectedElement()

    Implement it directly after step 3, but only when the mouse was clicked and an element was found. For example like this:

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):
        ...
        found = self.coord_input.SelectElement(...)
    
        if self.coord_input.IsMouseMove(mouse_msg) or not found:
            return True
    
        selected_element = self.coord_input.GetSelectedElement()
    

    The selected_element now contains a BaseElementAdapter, which you can use for further processing. To learn more, what exactly is an adapter, refer to this article.

    Alternatively or additionally, you can call GetSelectedGeometryElement() to get the geometry of the element:

    selected_geometry = self.coord_input.GetSelectedElementGeometry()
    
  5. If the next action should be a selection of another element, reinitialize the selection with InitNextElementInput(). This ensures, that the next element will be searched in the same document, where the first selection was done. Implementation looks the same, as described in the first step.

    Warning

    Remember to always reinitialize the input at this point! Not doing so, may lead to an unexpected behavior or even a crash.

Example

Here is the full code of a simple implementation of single element selection. After an element is selected, some action is introduced and the selection is restarted. No filter is applied for the selection, so there is no step 1.

class MyInteractor(BaseInteractor):

    def __init__(self, coord_input, ...):

        self.coord_input = coord_input

        # step 2 - initialize selection
        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):
        # step 3 - element search
        found = self.coord_input.SelectElement(
            mouse_msg, pnt, msg_info,
            True, True, True
        )

        if self.coord_input.IsMouseMove(mouse_msg) or not found:
            return True

        # step 4 - retrieving the element
        selected_element = self.coord_input.GetSelectedElement()

        # introducing some actions
        print(selected_element)

        # step 5 - reinitialize
        prompt_msg = AllplanIFW.InputStringConvert("Select next element")
        self.coord_input.InitFirstElementInput(prompt_msg)

        return True

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

Multiple selection

To introduce an input step, in which the user can select multiple model elements, e.g. by drawing a rectangle or by using the bracket or fence functionality (activated by right mouse click during the selection), follow these implementation steps:

Use classes of the MultiElementSelectInteractor module in your script object class as follows:

  1. Define instance attribute for saving the selection results. It should be a MultiElementSelectInteractorResult. Do it in the constructor, like:

    def __init__(self, build_ele, script_object_data):
        ...
        self.selection_result = MultiElementSelectInteractorResult()
    
  2. Optionally set up a filter (learn more below )

  3. Initialize the selection by creating an instance of MultiElementSelectInteractor and assigning it to the script_object_interactor property of your script object class. Depending on when you want the selection to be started, implement it in an appropriate method (learn more here). In this example, the selection starts right after the PythonPart is started, so the right place is the start_input() method.

    def start_input(self):
        self.script_object_interactor = MultiElementSelectInteractor(
            self.selection_result,
            selection_filter,   # (1)!
            prompt_msg = "Select the elements" #(2)!
        )
    
    1. If you have set up a filter in step 2, apply it here.
    2. Provide a meaningful prompt to the user for a better UX. It will be shown in the dialog line, where such instructions are expected to appear.
  4. Terminate the input by removing the reference to the MultiElementSelectInteractor from the script_object_interactor property. This needs to be done in the start_next_input()

    def start_next_input(self):
        self.script_object_interactor = None
    

To access the selected elements, read the property sel_elements of the result data class.

print(self.selection_result.sel_elements)

Example on GitHub

The entire implementation of a single element selection is shown in the example ChangeAttributes ( PYP | PY)

  1. At the beginning of the script flow, in the constructor of your interactor:

    • define instance attribute for saving the selection results. Use dedicated class PostElementSelection.
    • Set up a filter (learn more below ). If you don't want to filter anything, create an empty filter, as shown below.
    def __init__(self, coord_input, ...):
        self.coord_input = coord_input
        self.post_element_selection = AllplanIFW.PostElementSelection()       
        self.selection_filter = AllplanIFW.ElementSelectFilterSetting()
    

    Warning

    During the selection, the user can define his own filter using ALLPLAN filtering functionality. If he does so, it will overwrite your filter! Therefore, you must take into account a case, in which the final list of selected elements contains elements, that you wouldn't expect there!

  2. Start the selection by calling InputFunctionStarter.StartElementSelect() In this example, the selection is started when the PythonPart is started, so the call is done in the constructor. Your implementation might be different:

    def __init__(self, coord_input, ...):
        ...
        AllplanIFW.InputFunctionStarter.StartElementSelect(
            "Select elements", #(1)!
            self.selection_filter, 
            self.post_element_selection,
            markSelectedElements = True
        )
    
    1. This is the user prompt, that will be shown in the dialog line, when the selection is started. Provide a meaningful message to your users for a good UX.

    Info

    After this call, the process_mouse_msg event is overloaded. Whatever you've implemented there, it's not executed until the selection is completed!

    Failure

    Make sure not to start a selection function, when one is already running! 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!

  3. Retrieve the selected elements by calling GetSelectedElements() on the result class. Do it in the process_mouse_msg() event:

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):
        document = self.coord_input.GetInputViewDocument()
        elements = self.post_element_selection.GetSelectedElements(document)
    

    As mentioned earlier, process_mouse_msg() is overloaded during the selection and called only after it's completed. But a selection is completed also, when the user has drawn a rectangle and nothing was in it. It is therefore recommended to restart the selection, when the list of selected elements is empty:

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):
        ...
        document = self.coord_input.GetInputViewDocument()
        elements = self.post_element_selection.GetSelectedElements(document)
    
        if len(elements) == 0:  
            AllplanIFW.InputFunctionStarter.StartElementSelect(...)
            return True
    

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

class MyInteractor(BaseInteractor):

    def __init__(self, coord_input, ...):

        # step 1 - preparing attributes
        self.post_element_selection = AllplanIFW.PostElementSelection()
        self.sel_filter             = AllplanIFW.ElementSelectFilterSetting()
        self.coord_input            = coord_input

        # step 2 - start the selection
        self.start_selection()

    def process_mouse_msg(self, mouse_msg, pnt, msg_info):

        # step 3 - retrieve the selected elements
        document = self.coord_input.GetInputViewDocument()
        elements = self.post_element_selection.GetSelectedElements(document)

        if len(elements) == 0:  #(2)!
            self.start_selection()
            return True

        # introduce some action
        print(elements)

        self.start_selection()
        return True

    def start_selection(self):
        AllplanIFW.InputFunctionStarter.StartElementSelect(
            "Select elements",
            self.sel_filter,
            self.post_element_selection,
            markSelectedElements = True
        )

Example on GitHub

For the entire implementation, refer to the example MultiSelection ( PYP | PY).

Face selection

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)!
  1. This option will highlight the selected face
  2. 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

In general, filtering the elements for selection may be done by defining:

  • a list of allowed element types - very simple, but also not very flexible way of filtering
  • a callable with one argument being the BaseElementAdapter and a boolean return value, saying whether this element adapter is valid for selection (True) or not (False) - more complicated, but way more flexible way of defining a filter

Each element type (beam, wall, rebar) is identified by a unique GUID. You can refer to these GUIDs using meaningful aliases defined as attributes in the NemAll_Python_IFW_ElementAdapter module. All the aliases end with ...TypeUUID, so its easy to recognize them.

Example

To filter only columns and beams, define a list like:

allowed_types = [AllplanElementAdapter.Beam_TypeUUID,
                 AllplanElementAdapter.Column_TypeUUID]

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

You can define a callable, that takes exactly one argument of type BaseElementAdapter and returns True when the element is valid for selection, False when not. This can be a simple function or a class with __call__() method.

Example

You can define a filter as an anonymous lambda function. The following one allows only elements with a geometry being a polyhedron:

is_polyhedron = lambda ele: isinstance(ele.GetModelGeometry(), AllplanGeometry.Polyhedron3D)

For more complex conditions, you can define a filter with a function. The following one allows only elements with a geometry being a polyhedron, but with exactly 6 faces:

def has_six_faces(element: AllplanElementAdapter.BaseElementAdapter) -> bool:
    if not isinstance(geo := element.GetModelGeometry(), AllplanGeometry.Polyhedron3D):
        return False
    return geo.GetFacesCount() == 6

For even more flexibility, you can define a class with a __call__() member. The following one allows you to define, how many faces should be valid for selection:

class PolyhedronFacesFilter(BaseFilterObject):  #(1)!
    def __init__(self, number_of_faces: int):
        self.number_of_faces = number_of_faces

    def __call__(self, element: AllplanElementAdapter.BaseElementAdapter) -> bool:
        if not isinstance(geo := element.GetModelGeometry(), AllplanGeometry.Polyhedron3D):
            return False
        return geo.GetFacesCount() == self.number_of_faces

has_seven_faces = PolyhedronFacesFilter(7)
  1. You can use the BaseFilterObject as a base class to make sure, your implementation is valid.

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.

Combining filters

You can combine multiple filters with OR operator using FilterCollection. For example, to allow beams, columns or any other element with a geometry being a polyhedron with 6 faces, define the filter as follows:

from Utils.ElementFilter.FilterCollection import FilterCollection
...
complex_filter = FilterCollection()
query_types = [AllplanIFW.QueryTypeID(ele_type) for ele_type in allowed_types]
complex_filter.append(AllplanIFW.SelectionQuery(query_types))
complex_filter.append(has_six_faces)

Applying the filter

In a script object, you apply the filter when creating the selection interactor. The good news: you can use the list of allowed element types, callable or a filter collection directly:

def start_input(self):
    self.script_object_interactor = SingleElementSelectInteractor(
        self.selection_result,
        ele_filter = allowed_types, # (1)!
        prompt_msg = "Select the element"
    )
  1. You can also provide the anonymous lambda function:

    ele_filter = lambda ele: isinstance(ele.GetModelGeometry(), AllplanGeometry.Polyhedron3D)
    

    or the function has_six_faces or a callable instance has_seven_faces, like:

    ele_filter = has_six_faces
    

In an interactor, the selection filter must be an ElementSelectFilterSetting. This object has three components:

Component of ElementSelectFilterSetting Description
eDocumentSnoopType In which DF to search for elements for (active, passive, both)
eLayerSnoopType In which layer to search for elements for (active, passive, both)
SelectionQuery Objects valid for selection
classDiagram
    direction BT

    class ElementSelectFilterSetting
    class eDocumentSnoopType{
        <<Enumeration>>
        eSnoopActiveDocuments
        eSnoopPassiveDocuments
        eSnoopAllDocuments
    }
    class eLayerSnoopType{
        <<Enumeration>>
        eSnoopActiveLayers
        eSnoopPassiveLayers
        eSnoopAllLayers
    }
    class SelectionQuery{
        \_\_init\_\_(list[QueryTypeID | CustomFilter] query)
    }

    class MyFilter

    class BaseFilterObject{
        <<Abstract>>
        \_\_call\_\_(BaseElementAdapter)
    }

    BaseFilterObject --* SelectionQuery
    MyFilter --|> BaseFilterObject
    SelectionQuery --* "1" ElementSelectFilterSetting
    eDocumentSnoopType --* "1" ElementSelectFilterSetting
    eLayerSnoopType --* "1" ElementSelectFilterSetting

If your filter is a list of allowed element types, you need to:

query_types = [AllplanIFW.QueryTypeID(ele_type) for ele_type in allowed_types]
selection_query = AllplanIFW.SelectionQuery(query_types)
selection_filter = AllplanIFW.ElementSelectFilterSetting(
    selection_query,
    eDocumentSnoopType.eSnoopActiveDocuments,
    eLayerSnoopType.eSnoopActiveLayers
)

If your filter is a callable or a filter collection, you need to : - convert it to SelectionQuery - use it to create the ElementSelectFilterSetting

selection_query = AllplanIFW.SelectionQuery(has_six_faces)
selection_filter = AllplanIFW.ElementSelectFilterSetting(
    selection_query,
    eDocumentSnoopType.eSnoopActiveDocuments,
    eLayerSnoopType.eSnoopActiveLayers
)
Placeholder