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)
def _execute(self, context):
ifc_class = relating_type = None
is_untyped = False
ifc_file = tool.Ifc.get()
if self.should_filter:
active_element = context.scene.BIMSpatialDecompositionProperties.active_element
element_type = active_element.type
if element_type == "CLASS":
ifc_class = active_element.name
elif 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)
else: # OCCURRENCE
occurrence = ifc_file.by_id(active_element.ifc_definition_id)
obj = tool.Ifc.get_object(occurrence)
assert isinstance(obj, bpy.types.Object)
tool.Blender.set_active_object(obj)
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
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
element_filter = context.scene.BIMSpatialDecompositionProperties.element_filter
core.select_decomposed_elements(
tool.Spatial,
container=ifc_file.by_id(self.container),
ifc_class=ifc_class,
relating_type=relating_type,
is_untyped=is_untyped,
element_filter=element_filter,
)
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
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):
@@ -112,6 +112,7 @@ class BIMContainer(PropertyGroup):
class Element(PropertyGroup):
name: StringProperty(name="Name")
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(
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",
@@ -125,6 +126,7 @@ class Element(PropertyGroup):
items=(
("CLASS", "CLASS", "CLASS"),
("TYPE", "TYPE", "TYPE"),
("CLASSIFICATION", "CLASSIFICATION", "CLASSIFICATION"),
("OCCURRENCE", "OCCURRENCE", "OCCURRENCE"),
),
)
@@ -145,6 +147,7 @@ class BIMSpatialDecompositionProperties(PropertyGroup):
items=(
("TYPE", "Class > Type > Occurrence", "Show a breakdown by IfcClass, relating type, and occurrence"),
("DECOMPOSITION", "Element Decomposition", "Show a breakdown by element decomposition"),
("CLASSIFICATION", "Element Classification", "Show a breakdown by element classification"),
),
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):
if item:
row = layout.row(align=True)
item_type = item.type
for _ in range(item.level):
row.label(text="", icon="BLANK1")
if item.has_children:
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.alignment = "RIGHT"
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):
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()
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:
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.placement
import ifcopenshell.util.representation
import ifcopenshell.util.classification
import ifcopenshell.util.shape_builder
import ifcopenshell.util.type
import ifcopenshell.util.unit
@@ -199,7 +200,52 @@ class Spatial(bonsai.core.tool.Spatial):
src_obj.location.xy = destination_obj.location.xy
@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
results: defaultdict[str, dict[int, Any]] = defaultdict(dict)
if props.should_include_children:
@@ -230,11 +276,13 @@ class Spatial(bonsai.core.tool.Spatial):
cls.load_contained_elements_by_type(container)
elif props.element_mode == "DECOMPOSITION":
cls.load_contained_elements_by_decomposition(container)
elif props.element_mode == "CLASSIFICATION":
cls.load_contained_elements_by_classification(container)
@classmethod
def load_contained_elements_by_type(cls, container: ifcopenshell.entity_instance) -> None:
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_classes = expanded_elements.get("CLASS", [])
@@ -295,9 +343,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'}"):
ifc_definition_id = element.id()
name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
new = props.elements.add()
new.name = name
new.name = f"{element.is_a()}/{element.Name or 'Unnamed'}"
new.level = level
new.ifc_definition_id = ifc_definition_id
new.type = "OCCURRENCE"
@@ -310,11 +357,46 @@ class Spatial(bonsai.core.tool.Spatial):
new.has_children = True
new.total = len(children)
if ifc_definition_id in expanded_ifc_ids:
new.is_expanded = True
add_elements(children, level=level + 1)
elements = ifcopenshell.util.element.get_decomposition(container, is_recursive=False)
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
def filter_elements(
cls,
@@ -418,6 +500,8 @@ class Spatial(bonsai.core.tool.Spatial):
cls.toggle_container_element_by_type(element_index, is_recursive)
elif props.element_mode == "DECOMPOSITION":
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
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":
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():
if ifc_class != element.name:
continue
@@ -501,6 +585,31 @@ class Spatial(bonsai.core.tool.Spatial):
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
@classmethod