mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 18:16:40 +00:00
Fix #2437. You can now see the spatial hierarchy decomposed by classification.
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user