If you're reading this, we've just migrated servers! If anything looks broken please email dion@thinkmoult.com :)

Blender BIM hide isolating in context with Python

I was trying the IFC Search panel,
I wanted to see if I could make a very simple script, which automatically hide isolates the elements. Instead of clicking an extra button. Also for my own learning purposes.
This was my attempt, trying to hide isolate an IfcWall

When I change the class from IfcWall to IfcBeam it says the context is incorrect, probably because I already have something selected.

RuntimeError: Operator bpy.ops.object.hide_view_clear.poll() failed, context is incorrect
Error: Python script failed, check the message in the system console

After searching I found this post.

I found this function in the documentation to check if the context is correct.

        @classmethod
        def poll(cls, context):
            return context.object is not None

How would I use this function? What values should go into the parameters cls and context?

Comments

  • cls and context are mandatory signature for poll functions, basically you may give the first argument the name you want, but as this method is a "class" method, and does not require an instance of the operator to run, by convention we use "cls" to refer to the class of the method - by opposition to "self" for instances methods.

    When blender check if the operator is in the right context, it does pass context as argument and the function must return true or false if the context is correct for the operation.

    Isolate mode in blender "local_view" is available as shortcut : numpad "/", but looks like you are trying to achieve the opposite ?

  • edited October 2021

    Thanks for your answer, I am not trying to hide the object, but isolate them and show them.
    I am trying to do something really simple ( I think), but I get confused by these forum posts:

    https://blenderartists.org/t/blender-2-8-python-hide-unhide-objects/1141228
    https://blender.stackexchange.com/questions/36281/bpy-context-selected-objects-context-object-has-no-attribute-selected-objects

    What I am trying to achieve,

    • Select an IFC Class, then isolate it in the view. (Shift+H) "bpy.ops.object.hide_view_set(unselected=True)"
    • When changing the IFC class in the script, the view should hide reset (Alt+H) "bpy.ops.object.hide_view_clear()
      " and show instantly the new IFC class.

    I tried to put bpy.context in poll function, it does not work. I am doing something fundamentally wrong I think.

  • edited October 2021

    Basically poll() get context from blender at call time, so you have the context without bpy. things required.
    The operator hide_view probably require an active 3d view (mouse cursor over the area) in order to work so if you have a button anywhere else than in 3d view, you must override the operator's context and pass the right area.
    ctx = context.copy()
    ctx['area'] = ... the 3d view area
    ctx['selected_objects'] = ..
    bpy.ops.object.hide_view_set(ctx, unselected=True)

    Another solution may be to rely on visibility state of objects instead,
    o.hide_set(state=True)

    Coen
  • If you feel like copy pasting code, @stephen_l 's suggestion can be seen in use in the BCF module where we have to efficiently hide and isolate elements for each BCF viewpoint:

    Coenbrunopostle
  • You should always try to avoid using operators in code. They are really finnicky because they usually require a particular context, and they are extremely slow because they force a reevaluation (and even a redraw of the interface ?) every time they are evaluated. Usually you can get away with lower-level API like obj.hide_set(state) and obj.hide_get().

  • edited October 2021

    @Gorgious that is advice I would also encourage in the majority of situations. However, I recall when writing that particular portion, when dealing with the visibility of typically 10,000-100,000 objects as you would in a federated model, a Python loop with a lower level call was unfortunately magnitudes slower than the operator-based context hack. I would love to know a faster way especially as toggling BCF viewpoints is a big part of the user experience in model coordination.

    Edit: oh, just realised I left a comment in there because I figured other devs might have the same concern: https://github.com/IfcOpenShell/IfcOpenShell/blob/v0.7.0/src/blenderbim/blenderbim/bim/module/bcf/operator.py#L877-L878

  • edited October 2021

    @Moult Have you tried foreach_get and foreach_set ?
    https://docs.blender.org/api/latest/bpy.types.bpy_prop_collection.html#bpy.types.bpy_prop_collection.foreach_set
    I read that they're as fast as C to batch-change or batch-get values from bpy_prop_collection objects.
    Coupled with the numpy library I think we could replace calls to operators.
    Very simple example :
    bpy.data.objects.foreach_set("hide_viewport", (True,) * len(bpy.data.objects))
    Will hide every object from viewport.
    Only weird thing is it doesn't update the viewport. Toggling the parent collection does the trick.
    bpy.data.collections["Collection"].hide_viewport = True
    bpy.data.collections["Collection"].hide_viewport = False

    Tested with ~12.000 objects :
    for obj in bpy.data.objects:
    obj.hide_viewport = True
    takes 7,5 sec
    bpy.data.objects.foreach_set("hide_viewport", (True,) * len(bpy.data.objects)) takes 0,03 sec

  • @Gorgious aaah yes I didn't try with foreach_get/set! I think you're right that could be a better approach. Would you be interested in checking if it is faster than the current operator implementation? That comment should definitely be updated to say "TODO: try out foreach".

  • Yeah sure I'll poke around ! :)

  • Informative thread :-)

    I tried this:

        import bpy
        bpy.data.objects.foreach_set("hide_viewport", (True,) * len(bpy.ops.bim.select_ifc_class(ifc_class="IfcWindow")))
    

    It selects the windows

    But I get this message in the console

       Error: Array length mismatch (got 1, expected more)
       Traceback (most recent call last):
         File "\select_ifc_class_hide_isolate.py", line 6, in <module>
        RuntimeError: internal error setting the array
        Error: Python script failed, check the message in the system console
    

    What is the correct syntax? Do I need to add an extra argument in the foreach_set ? Or am I using the function wrong?

  • Turns out it's more complicated than I expected and since the eye icon visibility is governed per view layer and not the object, we can't use foreach_get or foreach_set. Same thing for the selection state. Meaning an object can have n(view layers) visibility states and selection states.
    That being said the problem there is you need to provide a boolean array with exactly the same length as the collection that's being iterated over. Moreover operators never return objects, but a set giving information whether it executed correctly.
    I would rather write :

    bpy.ops.bim.select_ifc_class(ifc_class="IfcWindow")
    bpy.data.objects.foreach_set("hide_viewport", [o.select_get() for o in bpy.data.objects])
    # We need this otherwise the view doesn't get updated :
    for i in range(2):
        bpy.data.collections[0].hide_viewport = not bpy.data.collections[0].hide_viewport
    

    Since it's not as straightforward as I initially thought and I don't have the data to test against thousands of Ifc objects, I can't say whether or not it's worth the hassle... :)

    MoultvpajicCoen
  • So if you have a list of ifcguids in Python.
    What's the fastest way to select all the IFC elements and hide all the unselected IFC elements in Blender in a Python script?

  • edited December 2021

    I now select it like this:

    def select_IFC_elements_in_blender(guid_list):
    
        for guid in guid_list:
            bpy.ops.bim.select_global_id(global_id=guid)
    

    Takes quite a while for this IFC model, for example, selecting the IfcCovering takes 66.707590341568 secondsseconds:

    Is there a faster way of selecting a group of IfcGuids, how does the IFC search function does it?

  • edited December 2021

    How does this fare ?

    import bpy
    import blenderbim.tool as tool
    
    guids = []  # Your guid list here
    
    for obj in bpy.context.view_layer.objects:
        element = tool.Ifc.get_entity(obj)
        data = element.get_info()
        obj.select_set(data.get("GlobalId", False) in guids)
    

    Or maybe this, which hides all the other objects :

    import bpy
    import blenderbim.tool as tool
    
    guids = []  # Your guid list here
    
    bpy.ops.object.select_all(action='DESELECT')
    
    for obj in bpy.context.view_layer.objects:
        element = tool.Ifc.get_entity(obj)
        if element is None:        
            obj.hide_viewport = True
            continue
        data = element.get_info()
        obj.hide_viewport = data.get("GlobalId", False) not in guids
    
    bpy.ops.object.select_all(action='SELECT')
    
    Coen
  • @Gorgious said:
    How does this fare ?

    Thank you for your answer, I am getting confused though.
    When I use

    for guid in guid_list:
           bpy.ops.bim.select_global_id(global_id=guid)
    

    It selects the elements, but slow, this took two seconds:

    After everything is selected I use shift + H to hide the unselected elements.

    After a mouse click in the view I can click and inspect the elements as I wish.

    It's a slow method so I tried this

        bpy.ops.object.select_all(action='DESELECT')
        for obj in bpy.context.view_layer.objects:
            element = tool.Ifc.get_entity(obj)
            if element is None:        
                obj.hide_viewport = True
                continue
            data = element.get_info()
            obj.hide_viewport = data.get("GlobalId", False) in guid_list
    
        bpy.ops.object.select_all(action='SELECT')
    

    It's indeed faster and I can see in the outline it indeed does hides the elements from the guid list. However I needed the inverse.

    I wanted to highlight the elements from the guid list, not hide them. That script seems to select everything and it's faster. That's also confusing.
    When I used Shift + H to hide the remaining objects I get to see nothing (obviously). But when pressing Alt + H to reveal everything the elements from the guid list are still hidden.

  • edited December 2021

    Oh, right sorry my bad, it should be obj.hide_viewport = data.get("GlobalId", False) not in guid_list (or obj.hide_viewport = not data.get("GlobalId", False) in guid_list).

    The reason it doesn't get un-hidden when using ALT H is because hide_viewport property is a global hidden property (monitor icon), whereas the eye icon is a local hidden property. If you've ever used Autocad it's kind of the same as the light bulb icon and the sun icon. To the point, ALT H will locally unhide objects, but it won't change their global hiden-ness (or however it's called ? ><) .

    By the way, to get access to the monitor icon in the outliner expand the sieve icon in the top right and check the monitor icon.
    Then :

    Note this is the exact same property as the one you toggle in the object's visibility properties :

    The eye icon is a little bit more complicated to use, since it can also rely on a specific view layer, but by default it's the active one. Here's a suggestion :

    import bpy
    import blenderbim.tool as tool
    
    guids = []  # Your guid list here
    
    bpy.ops.object.select_all(action='DESELECT')
    
    for obj in bpy.context.view_layer.objects:
        element = tool.Ifc.get_entity(obj)
        if element is None:        
            obj.hide_set(True)
            continue
        data = element.get_info()    
        obj.hide_set(data.get("GlobalId", False) not in guids)
    
    bpy.ops.object.select_all(action='SELECT')
    

    The reason it seems (and certainly is) faster is because using blender operators (expression begginning with bpy.ops) is notoriously slow. They are by essence user interface operators, and they kind of force a redraw of the interface and a re-calculation of some parameters every time they are executed. You won't notice it when you execute it 1, 10 or 20 times, but for hundreds of calls the hiccup will be significant. Whenever possible it's usually recommended to use lower level API calls. That's also one of the reasons why the BlenderBim logic is getting uncoupled from blender operators by Dion since a few commits already. This allows users to bypass the limitations of blender operators.

    Coen
  • @Gorgious

    Thank you so much! I never knew about the screen icon. Only started learning Blender three months ago.
    It works.

    But if ALT H doesn't work. How do I make everything visible again?

  • edited December 2021

    Haha yeah, I wouldn't worry about it, I've been using it extensively for 3 years and I still discover things every other day :)

    I don't know if you noticed, but collections also have visibility toggles. Using SHIFT + Click on a collection toggle will propagate to all its children. So you can SHIFT + double click on a collection toggle to uncheck then recheck all its children (alternatively, and it may be faster if you have a lot of objects in the collection, you can simply click on the collection toggle to deactivate it and only it, then SHIFT + Click on the toggle to unhide objects.)

    Or via python

    for obj in bpy.data.objects:
        obj.hide_viewport = False
    
    Coen
  • How would I toggle the monitor icon with Python?
    I am using
    bpy.context.space_data.show_restrict_column_viewport = False
    or
    bpy.context.space_data.show_restrict_column_viewport = True
    But the Blender console is giving me this error

    AttributeError: 'SpaceTextEditor' object has no attribute 'show_restrict_column_viewport'
    Error: Python script failed, check the message in the system console
    
  • edited December 2021

    Do you mean toggling the display of the icon inside the outliner, or toggle the state of a particular object ? If it's the object :

    obj.hide_viewport = True # or False

    If it's the outliner toggle visibility, it's a little more involved :
    First get the outliner editor area
    Then get the outliner space (it only has one)

    outliner = next(a for a in bpy.context.screen.areas if a.type == "OUTLINER") 
    outliner.spaces[0].show_restrict_column_viewport = not outliner.spaces[0].show_restrict_column_viewport
    

    You motivated me to post a Q&A on BSE. :)

    Coen
  • Very nice, you have been extremely helpful and I learned a lot from this discussion.
    The function now works exactly as I intended.

  • My pleasure :)

    Don't hesitate to ping me if you have any Blender related questions !

    Coen
Sign In or Register to comment.