Fix #2437. You can now see the spatial hierarchy decomposed by classification.

This commit is contained in:
Dion Moult
2024-09-03 18:42:47 +10:00
parent 44ab38cb24
commit 3675429385
5 changed files with 171 additions and 57 deletions
@@ -293,34 +293,61 @@ class SelectDecomposedElements(bpy.types.Operator, tool.Ifc.Operator):
return self.execute(context) return self.execute(context)
def _execute(self, context): def _execute(self, context):
ifc_class = relating_type = None
is_untyped = False
ifc_file = tool.Ifc.get() ifc_file = tool.Ifc.get()
if self.should_filter: container = ifc_file.by_id(self.container)
active_element = context.scene.BIMSpatialDecompositionProperties.active_element props = context.scene.BIMSpatialDecompositionProperties
element_type = active_element.type element_filter = props.element_filter
if element_type == "CLASS": active_element = props.active_element
ifc_class = active_element.name
elif element_type == "TYPE": if not self.should_filter and not element_filter:
ifc_class = active_element.ifc_class tool.Spatial.select_products(tool.Spatial.get_decomposed_elements(container))
if ifc_id := active_element.ifc_definition_id: return
relating_type = ifc_file.by_id(ifc_id)
else: # OCCURRENCE if props.element_mode == "TYPE":
occurrence = ifc_file.by_id(active_element.ifc_definition_id) if active_element.type == "OCCURRENCE":
obj = tool.Ifc.get_object(occurrence) if obj := tool.Ifc.get_object(ifc_file.by_id(active_element.ifc_definition_id)):
assert isinstance(obj, bpy.types.Object) tool.Blender.set_active_object(obj)
tool.Blender.set_active_object(obj)
return return
element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter ifc_class = relating_type = None
core.select_decomposed_elements( is_untyped = False
tool.Spatial,
container=ifc_file.by_id(self.container), if self.should_filter:
ifc_class=ifc_class, if active_element.type == "CLASS":
relating_type=relating_type, ifc_class = active_element.name
is_untyped=is_untyped, elif active_element.type == "TYPE":
element_filter=element_filter, 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
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))
class SetDefaultContainer(bpy.types.Operator, tool.Ifc.Operator): class SetDefaultContainer(bpy.types.Operator, tool.Ifc.Operator):
@@ -112,6 +112,7 @@ class BIMContainer(PropertyGroup):
class Element(PropertyGroup): class Element(PropertyGroup):
name: StringProperty(name="Name") name: StringProperty(name="Name")
ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'") ifc_class: StringProperty(name="Name", description="Type or element IFC class, empty if 'type' is 'CLASS'")
identification: StringProperty(name="Identification", description="Identification if 'type' is 'CLASSIFICATION'")
ifc_definition_id: IntProperty( ifc_definition_id: IntProperty(
name="IFC Definition ID", name="IFC Definition ID",
description="ID of the element type / occurrence. 0 if 'type' is 'CLASS' or if it's 'TYPE' but it represents untyped elements", description="ID of the element type / occurrence. 0 if 'type' is 'CLASS' or if it's 'TYPE' but it represents untyped elements",
@@ -125,6 +126,7 @@ class Element(PropertyGroup):
items=( items=(
("CLASS", "CLASS", "CLASS"), ("CLASS", "CLASS", "CLASS"),
("TYPE", "TYPE", "TYPE"), ("TYPE", "TYPE", "TYPE"),
("CLASSIFICATION", "CLASSIFICATION", "CLASSIFICATION"),
("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"), ("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"),
), ),
) )
@@ -145,6 +147,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
items=( items=(
("TYPE", "Class > Type > Occurrence", "Show a breakdown by IfcClass, relating type, and occurrence"), ("TYPE", "Class > Type > Occurrence", "Show a breakdown by IfcClass, relating type, and occurrence"),
("DECOMPOSITION", "Element Decomposition", "Show a breakdown by element decomposition"), ("DECOMPOSITION", "Element Decomposition", "Show a breakdown by element decomposition"),
("CLASSIFICATION", "Element Classification", "Show a breakdown by element classification"),
), ),
update=update_element_mode, update=update_element_mode,
) )
+2 -14
View File
@@ -278,27 +278,15 @@ class BIM_UL_elements(UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag): def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index, fit_flag):
if item: if item:
row = layout.row(align=True) row = layout.row(align=True)
item_type = item.type
for _ in range(item.level): for _ in range(item.level):
row.label(text="", icon="BLANK1") row.label(text="", icon="BLANK1")
if item.has_children: if item.has_children:
self.draw_toggle(row, item.is_expanded, index) self.draw_toggle(row, item.is_expanded, index)
if item_type == "CLASS": row.label(text=item.name)
row.label(text=item.name) if item.total:
col = row.column() col = row.column()
col.alignment = "RIGHT" col.alignment = "RIGHT"
col.label(text=str(item.total)) col.label(text=str(item.total))
elif item_type == "TYPE":
row.label(text=item.name)
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
elif item_type == "OCCURRENCE":
row.label(text=item.name)
if item.total:
col = row.column()
col.alignment = "RIGHT"
col.label(text=str(item.total))
def draw_filter(self, context, layout): def draw_filter(self, context, layout):
row = layout.row() row = layout.row()
-13
View File
@@ -146,19 +146,6 @@ def toggle_container_element(spatial: tool.Spatial, element_index: int, is_recur
spatial.load_contained_elements() spatial.load_contained_elements()
def select_decomposed_elements(
spatial: tool.Spatial,
container: ifcopenshell.entity_instance,
ifc_class: str | None = None,
relating_type: ifcopenshell.entity_instance | None = None,
is_untyped: bool = False,
element_filter: str | None = None,
) -> None:
elements = spatial.get_decomposed_elements(container)
elements = spatial.filter_elements(elements, ifc_class, relating_type, is_untyped, element_filter)
spatial.select_products(elements)
def select_decomposed_element(ifc: tool.Ifc, spatial: tool.Spatial, element: ifcopenshell.entity_instance) -> None: def select_decomposed_element(ifc: tool.Ifc, spatial: tool.Spatial, element: ifcopenshell.entity_instance) -> None:
spatial.set_active_object(ifc.get_object(element)) spatial.set_active_object(ifc.get_object(element))
+114 -5
View File
@@ -26,6 +26,7 @@ import ifcopenshell.geom
import ifcopenshell.util.element import ifcopenshell.util.element
import ifcopenshell.util.placement import ifcopenshell.util.placement
import ifcopenshell.util.representation import ifcopenshell.util.representation
import ifcopenshell.util.classification
import ifcopenshell.util.shape_builder import ifcopenshell.util.shape_builder
import ifcopenshell.util.type import ifcopenshell.util.type
import ifcopenshell.util.unit import ifcopenshell.util.unit
@@ -199,7 +200,52 @@ class Spatial(bonsai.core.tool.Spatial):
src_obj.location.xy = destination_obj.location.xy src_obj.location.xy = destination_obj.location.xy
@classmethod @classmethod
def get_grouped_elements_in_container(cls, container: ifcopenshell.entity_instance) -> dict: def get_container_elements_grouped_by_classification(cls, container: ifcopenshell.entity_instance) -> dict:
props = bpy.context.scene.BIMSpatialDecompositionProperties
results = {}
if props.should_include_children:
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=True)
else:
elements = ifcopenshell.util.element.get_contained(container)
flat_results: dict[str, Any] = {}
reference_names: dict[str, str] = {}
for element in elements:
if element.is_a("IfcOpeningElement") or tool.Root.is_spatial_element(element):
continue
references = ifcopenshell.util.classification.get_references(element)
if not references:
flat_results.setdefault("Unclassified", []).append(element)
else:
for reference in references:
identification = reference[1] or ""
reference_names[identification] = reference[2]
flat_results.setdefault(identification, []).append(element)
for flat_key in sorted(flat_results.keys()):
current_results = results
while True:
has_parent = None
for key in current_results:
if flat_key.startswith(key):
has_parent = True
new_current_results = current_results[key]["children"]
break
if has_parent:
current_results = new_current_results
else:
break
current_results[flat_key] = {
"Name": "Unclassified" if flat_key == "Unclassified" else reference_names[flat_key],
"elements": flat_results[flat_key],
"children": {},
}
return results
@classmethod
def get_container_elements_grouped_by_type(cls, container: ifcopenshell.entity_instance) -> dict:
props = bpy.context.scene.BIMSpatialDecompositionProperties props = bpy.context.scene.BIMSpatialDecompositionProperties
results: defaultdict[str, dict[int, Any]] = defaultdict(dict) results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children: if props.should_include_children:
@@ -230,11 +276,13 @@ class Spatial(bonsai.core.tool.Spatial):
cls.load_contained_elements_by_type(container) cls.load_contained_elements_by_type(container)
elif props.element_mode == "DECOMPOSITION": elif props.element_mode == "DECOMPOSITION":
cls.load_contained_elements_by_decomposition(container) cls.load_contained_elements_by_decomposition(container)
elif props.element_mode == "CLASSIFICATION":
cls.load_contained_elements_by_classification(container)
@classmethod @classmethod
def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None: def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties props = bpy.context.scene.BIMSpatialDecompositionProperties
results = cls.get_grouped_elements_in_container(container) results = cls.get_container_elements_grouped_by_type(container)
expanded_elements = json.loads(props.expanded_elements) expanded_elements = json.loads(props.expanded_elements)
expanded_classes = expanded_elements.get("CLASS", []) expanded_classes = expanded_elements.get("CLASS", [])
@@ -295,9 +343,8 @@ class Spatial(bonsai.core.tool.Spatial):
def add_elements(elements, level=0): def add_elements(elements, level=0):
for element in sorted(elements, key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"): for element in sorted(elements, key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"):
ifc_definition_id = element.id() ifc_definition_id = element.id()
name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
new = props.elements.add() new = props.elements.add()
new.name = name new.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
new.level = level new.level = level
new.ifc_definition_id = ifc_definition_id new.ifc_definition_id = ifc_definition_id
new.type = "OCCURRENCE" new.type = "OCCURRENCE"
@@ -310,11 +357,46 @@ class Spatial(bonsai.core.tool.Spatial):
new.has_children = True new.has_children = True
new.total = len(children) new.total = len(children)
if ifc_definition_id in expanded_ifc_ids: if ifc_definition_id in expanded_ifc_ids:
new.is_expanded = True
add_elements(children, level=level + 1) add_elements(children, level=level + 1)
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=False) elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=False)
add_elements(elements) add_elements(elements)
@classmethod
def load_contained_elements_by_classification(cls, container: ifcopenshell.entity_instance) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
expanded_elements = json.loads(props.expanded_elements)
expanded_classifications = expanded_elements.get("CLASSIFICATION", [])
expanded_classifications_r = expanded_elements.get("CLASSIFICATION_R", [])
def add_elements(results, level=0):
for identification in sorted(results.keys()):
data = results[identification]
new = props.elements.add()
new.name = f"{identification}:{data['Name']}"
new.type = "CLASSIFICATION"
new.identification = identification
new.has_children = True
new.level = level
is_expanded = identification in expanded_classifications
is_expanded_r = any([c for c in expanded_classifications_r if identification.startswith(c)])
if is_expanded or is_expanded_r:
new.is_expanded = True
add_elements(data["children"], level=level + 1)
for element in sorted(data["elements"], key=lambda x: f"{x.is_a()}/{x.Name or 'Unnamed'}"):
new2 = props.elements.add()
new2.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
new2.type = "OCCURRENCE"
new2.level = level + 1
new2.ifc_definition_id = element.id()
results = cls.get_container_elements_grouped_by_classification(container)
add_elements(results)
@classmethod @classmethod
def filter_elements( def filter_elements(
cls, cls,
@@ -418,6 +500,8 @@ class Spatial(bonsai.core.tool.Spatial):
cls.toggle_container_element_by_type(element_index, is_recursive) cls.toggle_container_element_by_type(element_index, is_recursive)
elif props.element_mode == "DECOMPOSITION": elif props.element_mode == "DECOMPOSITION":
cls.toggle_container_element_by_decomposition(element_index, is_recursive) cls.toggle_container_element_by_decomposition(element_index, is_recursive)
elif props.element_mode == "CLASSIFICATION":
cls.toggle_container_element_by_classification(element_index, is_recursive)
@classmethod @classmethod
def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None: def toggle_container_element_by_type(cls, element_index: int, is_recursive: bool) -> None:
@@ -448,7 +532,7 @@ class Spatial(bonsai.core.tool.Spatial):
if is_recursive and element.type == "CLASS": if is_recursive and element.type == "CLASS":
container = tool.Ifc.get().by_id(props.active_container.ifc_definition_id) container = tool.Ifc.get().by_id(props.active_container.ifc_definition_id)
results = cls.get_grouped_elements_in_container(container) results = cls.get_container_elements_grouped_by_type(container)
for ifc_class in results.keys(): for ifc_class in results.keys():
if ifc_class != element.name: if ifc_class != element.name:
continue continue
@@ -501,6 +585,31 @@ class Spatial(bonsai.core.tool.Spatial):
props.expanded_elements = json.dumps(expanded_elements) props.expanded_elements = json.dumps(expanded_elements)
@classmethod
def toggle_container_element_by_classification(cls, element_index: int, is_recursive: bool) -> None:
props = bpy.context.scene.BIMSpatialDecompositionProperties
expanded_elements: dict[str, list[str]] = json.loads(props.expanded_elements)
expanded_elements_list: list[str] = expanded_elements.setdefault("CLASSIFICATION", [])
element = props.elements[element_index]
identification = element.identification
if identification in expanded_elements_list:
expanded_elements_list.remove(identification)
should_expand = False
else:
expanded_elements_list.append(identification)
should_expand = True
if is_recursive:
expanded_elements_list = expanded_elements.setdefault("CLASSIFICATION_R", [])
if should_expand is False and identification in expanded_elements_list:
expanded_elements_list.remove(identification)
elif should_expand is True and identification not in expanded_elements_list:
expanded_elements_list.append(identification)
props.expanded_elements = json.dumps(expanded_elements)
# HERE STARTS SPATIAL TOOL # HERE STARTS SPATIAL TOOL
@classmethod @classmethod