Consolidate selection and visibility tools in the spatial manager into a single location with a single function for filtering

There were a number of inconsistencies between the modes and how keyword
filtering affected results. This should resolve it.
This commit is contained in:
Dion Moult
2025-02-08 16:38:29 +11:00
parent 59dd105e99
commit eabdb5d281
3 changed files with 150 additions and 85 deletions
@@ -317,14 +317,14 @@ class SelectDecomposedElement(bpy.types.Operator):
class SelectDecomposedElements(bpy.types.Operator):
bl_idname = "bim.select_decomposed_elements"
bl_label = "Select Children"
bl_label = "Select Elements"
bl_options = {"REGISTER", "UNDO"}
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
container: bpy.props.IntProperty()
@classmethod
def description(cls, context, operator):
return "Select all contained elements filtered by this type" + "\nALT+CLICK to select all contained elements"
return "Select elements in the viewport based on the active item in the list" + "\nALT+CLICK to select all listed elements"
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
@@ -332,61 +332,7 @@ class SelectDecomposedElements(bpy.types.Operator):
return self.execute(context)
def execute(self, context):
ifc_file = tool.Ifc.get()
container = ifc_file.by_id(self.container)
props = context.scene.BIMSpatialDecompositionProperties
element_filter = props.element_filter
active_element = props.active_element
if not self.should_filter and not element_filter:
tool.Spatial.select_products(tool.Spatial.get_decomposed_elements(container))
return {"CANCELLED"}
if props.element_mode == "TYPE":
if active_element.type == "OCCURRENCE":
if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)):
tool.Blender.set_active_object(obj)
return {"CANCELLED"}
ifc_class = relating_type = None
is_untyped = False
if self.should_filter:
if active_element.type == "CLASS":
ifc_class = active_element.name
elif active_element.type == "TYPE":
ifc_class = active_element.ifc_class
if ifc_id := active_element.ifc_definition_id:
relating_type = ifc_file.by_id(ifc_id)
elements = tool.Spatial.get_decomposed_elements(container)
elements = tool.Spatial.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter)
tool.Spatial.select_products(elements)
elif props.element_mode == "DECOMPOSITION":
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
elements = ifcopenshell.util.element.get_decomposition(occurrence)
elements.add(occurrence)
tool.Spatial.select_products(elements)
elif props.element_mode == "CLASSIFICATION":
if active_element.type == "OCCURRENCE":
if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)):
tool.Blender.set_active_object(obj)
return {"CANCELLED"}
if active_element.type == "CLASSIFICATION":
identification = active_element.identification
elements = tool.Spatial.get_decomposed_elements(container)
def filter_element(element: ifcopenshell.entity_instance) -> bool:
references = ifcopenshell.util.classification.get_references(element)
if identification == "Unclassified":
if not references:
return True
elif any([r for r in references if r[1].startswith(identification)]):
return True
return False
tool.Spatial.select_products(filter(filter_element, elements))
tool.Spatial.select_products(tool.Spatial.get_filtered_elements(self.should_filter))
return {"FINISHED"}
@@ -407,7 +353,7 @@ class SetContainerVisibility(bpy.types.Operator):
bl_label = "Set Container Visibility"
bl_options = {"REGISTER", "UNDO"}
container: bpy.props.IntProperty()
should_include_children: bpy.props.BoolProperty(name="Should Include Children", default=True, options={"SKIP_SAVE"})
should_filter: bpy.props.BoolProperty(name="Should Filter", default=True, options={"SKIP_SAVE"})
mode: bpy.props.StringProperty(name="Mode")
@classmethod
@@ -420,7 +366,7 @@ class SetContainerVisibility(bpy.types.Operator):
def invoke(self, context, event):
if event.type == "LEFTMOUSE" and event.alt:
self.should_include_children = False
self.should_filter = False
return self.execute(context)
def execute(self, context):
@@ -433,9 +379,7 @@ class SetContainerVisibility(bpy.types.Operator):
else:
should_hide = self.mode == "HIDE"
container = tool.Ifc.get().by_id(self.container)
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=self.should_include_children)
for element in elements:
for element in tool.Spatial.get_filtered_elements(self.should_filter):
if obj := tool.Ifc.get_object(element):
obj.hide_set(should_hide)
for collection in obj.users_collection:
+16 -19
View File
@@ -143,24 +143,6 @@ class BIM_PT_spatial_decomposition(Panel):
op = col.operator("bim.set_default_container", icon="OUTLINER_COLLECTION", text="Set Default")
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
op.mode = "ISOLATE"
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="HIDE_OFF", text="")
op.mode = "SHOW"
op.container = ifc_definition_id
col = row.column(align=True)
col.enabled = non_ifc_project_active
op = col.operator("bim.set_container_visibility", icon="HIDE_ON", text="")
op.mode = "HIDE"
op.container = ifc_definition_id
# The only operator that's enabled for IfcProject.
col = row.column(align=True)
col.operator("bim.select_container", icon="OBJECT_DATA", text="").container = ifc_definition_id
@@ -193,11 +175,26 @@ class BIM_PT_spatial_decomposition(Panel):
row.prop(self.props, "should_include_children", text="", icon="OUTLINER")
return
row = self.layout.row(align=True)
row = self.layout.row()
row.label(
text=f"{self.props.active_container.ifc_class} > {self.props.total_elements} Elements",
icon="FILE_3D",
)
row = self.layout.row(align=True)
op = row.operator("bim.set_container_visibility", icon="FULLSCREEN_EXIT", text="Isolate")
op.mode = "ISOLATE"
op.container = ifc_definition_id
op = row.operator("bim.set_container_visibility", icon="HIDE_OFF", text="")
op.mode = "SHOW"
op.container = ifc_definition_id
op = row.operator("bim.set_container_visibility", icon="HIDE_ON", text="")
op.mode = "HIDE"
op.container = ifc_definition_id
row.operator("bim.assign_container", icon="FOLDER_REDIRECT", text="").container = ifc_definition_id
op = row.operator("bim.select_decomposed_element", icon="OBJECT_DATA", text="")
if active_element := self.props.active_element:
+128 -4
View File
@@ -212,7 +212,9 @@ class Spatial(bonsai.core.tool.Spatial):
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
flat_results: dict[str, Any] = {}
reference_names: dict[str, str] = {}
for element in elements:
@@ -257,14 +259,20 @@ class Spatial(bonsai.core.tool.Spatial):
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
for element in elements:
if element.is_a("IfcOpeningElement") or tool.Root.is_spatial_element(element):
continue
element_type = ifcopenshell.util.element.get_type(element)
ifc_class = element.is_a()
ifc_definition_id = element_type.id() if element_type else 0
type_name = element_type.Name or "Unnamed" if element_type else f"Untyped {element.is_a()}"
type_name = (
element_type.is_a() + "/" + element_type.Name or "Unnamed"
if element_type
else f"Untyped {element.is_a()}"
)
class_data = results.setdefault(ifc_class, {})
type_data = class_data.setdefault(ifc_definition_id, {"type_name": type_name, "elements": []})
type_data["elements"].append(element)
@@ -348,6 +356,8 @@ class Spatial(bonsai.core.tool.Spatial):
def add_elements(elements, level=0):
for element in sorted(elements, key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"):
if not props.should_include_children and tool.Root.is_spatial_element(element):
continue
ifc_definition_id = element.id()
new = props.elements.add()
new.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
@@ -406,7 +416,7 @@ class Spatial(bonsai.core.tool.Spatial):
@classmethod
def filter_elements(
cls,
elements: List[ifcopenshell.entity_instance],
elements: Iterable[ifcopenshell.entity_instance],
ifc_class: str | None,
relating_type: ifcopenshell.entity_instance | None,
is_untyped: bool,
@@ -1232,3 +1242,117 @@ class Spatial(bonsai.core.tool.Spatial):
):
props = bpy.context.active_object.BIMObjectSpatialProperties
props.container_obj = obj
@classmethod
def get_filtered_elements(cls, should_filter: bool = True) -> Iterable[ifcopenshell.entity_instance]:
ifc_file = tool.Ifc.get()
props = bpy.context.scene.BIMSpatialDecompositionProperties
container = ifc_file.by_id(props.active_container.ifc_definition_id)
element_filter = props.element_filter
active_element = props.active_element
if not should_filter:
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
if not element_filter:
return elements
keyword = element_filter.lower()
if props.element_mode == "TYPE":
filtered_occurrences = set()
filtered_classes = set()
filtered_types = set()
for item in props.elements:
if item.type == "CLASS" and not item.is_expanded and keyword in item.name.lower():
filtered_classes.add(item.name)
elif item.type == "TYPE" and not item.is_expanded and keyword in item.name.lower():
if item.ifc_definition_id:
filtered_types.add(ifc_file.by_id(item.ifc_definition_id))
else:
filtered_types.add(item.name.split(" ")[1])
elif item.type == "OCCURRENCE" and keyword in item.name.lower():
filtered_occurrences.add(ifc_file.by_id(item.ifc_definition_id))
return {
e
for e in elements
if e.is_a() in filtered_classes
or ((e_type := ifcopenshell.util.element.get_type(e)) and e_type in filtered_types)
or (not e_type and e.is_a() in filtered_types)
} | filtered_occurrences
elif props.element_mode == "DECOMPOSITION":
return [ifc_file.by_id(i.ifc_definition_id) for i in props.elements if keyword in i.name.lower()]
elif props.element_mode == "CLASSIFICATION":
filtered_classifications = set()
filtered_occurrences = set()
for item in props.elements:
if item.type == "CLASSIFICATION" and not item.is_expanded and keyword in item.name.lower():
filtered_classifications.add(item.identification)
elif item.type == "OCCURRENCE" and keyword in item.name.lower():
filtered_occurrences.add(ifc_file.by_id(item.ifc_definition_id))
for element in elements:
if refs := ifcopenshell.util.classification.get_references(element):
for ref in refs:
for filtered_classification in filtered_classifications:
if ref[1].startswith(filtered_classification):
filtered_occurrences.add(element)
elif "Unclassified" in filtered_classifications:
filtered_occurrences.add(element)
return filtered_occurrences
return elements
if not active_element:
return []
if props.element_mode == "TYPE":
if active_element.type == "OCCURRENCE":
return {ifc_file.by_id(active_element.ifc_definition_id)}
ifc_class = relating_type = None
is_untyped = False
if active_element.type == "CLASS":
ifc_class = active_element.name
elif active_element.type == "TYPE":
ifc_class = active_element.ifc_class
if ifc_id := active_element.ifc_definition_id:
relating_type = ifc_file.by_id(ifc_id)
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
return cls.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter)
elif props.element_mode == "DECOMPOSITION":
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
elements = ifcopenshell.util.element.get_decomposition(occurrence, is_recursive=True)
elements.add(occurrence)
return elements
elif props.element_mode == "CLASSIFICATION":
if active_element.type == "OCCURRENCE":
return {ifc_file.by_id(active_element.ifc_definition_id)}
if active_element.type == "CLASSIFICATION":
identification = active_element.identification
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = set(ifcopenshell.util.element.get_contained(container))
for e in elements:
elements.update(ifcopenshell.util.element.get_decomposition(e))
def filter_element(element: ifcopenshell.entity_instance) -> bool:
references = ifcopenshell.util.classification.get_references(element)
if identification == "Unclassified":
if not references:
return True
elif any([r for r in references if r[1].startswith(identification)]):
return True
return False
return filter(filter_element, elements)