From 0c618cd4e1a0f31202b8833c6744282e675cf27e Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Thu, 19 Jun 2025 20:46:12 +0100 Subject: [PATCH] Second pass, manual traversal --- .../converter/data_object_converter.py | 22 +++-- .../converter/spatial_element_converter.py | 11 ++- src/speckleifc/ifc_geometry_processing.py | 11 ++- src/speckleifc/ifc_openshell_helpers.py | 29 +++++- src/speckleifc/importer.py | 96 +++++-------------- 5 files changed, 80 insertions(+), 89 deletions(-) diff --git a/src/speckleifc/converter/data_object_converter.py b/src/speckleifc/converter/data_object_converter.py index b9d8695..4e39aa8 100644 --- a/src/speckleifc/converter/data_object_converter.py +++ b/src/speckleifc/converter/data_object_converter.py @@ -2,26 +2,34 @@ from typing import cast from ifcopenshell.entity_instance import entity_instance from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement +from specklepy.objects.base import Base from specklepy.objects.data_objects import DataObject from speckleifc.converter.geometry_converter import geometry_to_speckle def data_object_to_speckle( - shape: TriangulationElement, step_element: entity_instance + shape: TriangulationElement | None, + step_element: entity_instance, + children: list[Base], ) -> DataObject: - geometry = cast(Triangulation, shape.geometry) - display_value = geometry_to_speckle(geometry) + if shape: + geometry = cast(Triangulation, shape.geometry) + display_value = geometry_to_speckle(geometry) + else: + display_value = [] + + guid = cast(str, step_element.GlobalId) + name = cast(str, step_element.Name or guid) data_object = DataObject( - applicationId=cast(str, shape.guid), + applicationId=guid, properties={}, - name=cast(str, shape.name) or cast(str, shape.guid), + name=name or guid, displayValue=display_value, ) - # TODO: children as "elements" - # data_object["@elements"] = children_converter.convert_children(shape, ifc_model) + data_object["@elements"] = children data_object["ifcType"] = step_element.is_a() data_object["expressId"] = step_element.id() data_object["description"] = cast(str | None, step_element.Description) diff --git a/src/speckleifc/converter/spatial_element_converter.py b/src/speckleifc/converter/spatial_element_converter.py index a337d52..e04c893 100644 --- a/src/speckleifc/converter/spatial_element_converter.py +++ b/src/speckleifc/converter/spatial_element_converter.py @@ -1,21 +1,21 @@ from typing import cast from ifcopenshell.entity_instance import entity_instance -from ifcopenshell.ifcopenshell_wrapper import Triangulation +from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement from specklepy.objects.base import Base from specklepy.objects.data_objects import DataObject from specklepy.objects.models.collections.collection import Collection from speckleifc.converter.geometry_converter import geometry_to_speckle -from speckleifc.ifc_geometry_processing import try_get_shape def spatial_element_to_speckle( + shape: TriangulationElement | None, step_element: entity_instance, relational_children: list[Base], ) -> Collection: - direct_geometry = _convert_as_data_object(step_element) + direct_geometry = _convert_as_data_object(shape, step_element) all_children = [direct_geometry] + relational_children guid = cast(str, step_element.GlobalId) @@ -28,12 +28,13 @@ def spatial_element_to_speckle( return data_object -def _convert_as_data_object(step_element: entity_instance) -> DataObject: +def _convert_as_data_object( + shape: TriangulationElement | None, step_element: entity_instance +) -> DataObject: # Some types of SpatialElements, like IfcSite have a geometry representation # Using get_shape is not as efficient as the using the geometry iterator, # like is used for most of the geometry conversion, but for a few IfcSites is fine. - shape = try_get_shape(step_element) if shape is not None: geometry = cast(Triangulation, shape.geometry) display_value = geometry_to_speckle(geometry) diff --git a/src/speckleifc/ifc_geometry_processing.py b/src/speckleifc/ifc_geometry_processing.py index 5e3984f..0a2bdd8 100644 --- a/src/speckleifc/ifc_geometry_processing.py +++ b/src/speckleifc/ifc_geometry_processing.py @@ -41,7 +41,16 @@ def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: def try_get_shape(element: entity_instance) -> TriangulationElement | None: - if element.Representation is None: + representation = getattr(element, "Representation", None) + if representation is None: + return None + + has_body = any( + getattr(r, "RepresentationIdentifier", "").lower() == "body" + for r in representation.Representations + ) + + if not has_body: return None shape = create_shape(_IFC_SETTINGS, element) diff --git a/src/speckleifc/ifc_openshell_helpers.py b/src/speckleifc/ifc_openshell_helpers.py index 6f10016..56c96d4 100644 --- a/src/speckleifc/ifc_openshell_helpers.py +++ b/src/speckleifc/ifc_openshell_helpers.py @@ -1,9 +1,32 @@ from collections.abc import Generator, Iterable +from itertools import chain from typing import cast from ifcopenshell.entity_instance import entity_instance -def get_aggregates(step_element: entity_instance) -> Generator[entity_instance]: - for relation in cast(Iterable[entity_instance], step_element.IsDecomposedBy): - yield from cast(Iterable[entity_instance], relation.RelatedObjects) +def get_children(step_element: entity_instance) -> Generator[entity_instance]: + + yield from chain( + get_spatial_children(step_element), get_aggregate_children(step_element) + ) + + +def get_spatial_children(step_element: entity_instance) -> Generator[entity_instance]: + spatial_relations = cast( + Iterable[entity_instance] | None, + getattr(step_element, "ContainsElements", None), + ) + if spatial_relations is not None: + for relation in spatial_relations: + yield from cast(Iterable[entity_instance], relation.RelatedElements) + + +def get_aggregate_children(step_element: entity_instance) -> Generator[entity_instance]: + aggregate_relations = cast( + Iterable[entity_instance] | None, + getattr(step_element, "IsDecomposedBy", None), + ) + if aggregate_relations is not None: + for relation in aggregate_relations: + yield from cast(Iterable[entity_instance], relation.RelatedObjects) diff --git a/src/speckleifc/importer.py b/src/speckleifc/importer.py index 570771e..c7af534 100644 --- a/src/speckleifc/importer.py +++ b/src/speckleifc/importer.py @@ -1,18 +1,13 @@ -from collections.abc import Iterable -from typing import cast - from ifcopenshell.entity_instance import entity_instance from ifcopenshell.geom import file -from ifcopenshell.ifcopenshell_wrapper import TriangulationElement from specklepy.logging.exceptions import SpeckleException from specklepy.objects import Base -from specklepy.objects.models.collections.collection import Collection from speckleifc.converter.data_object_converter import data_object_to_speckle from speckleifc.converter.project_converter import project_to_speckle from speckleifc.converter.spatial_element_converter import spatial_element_to_speckle -from speckleifc.ifc_geometry_processing import create_geometry_iterator -from speckleifc.ifc_openshell_helpers import get_aggregates +from speckleifc.ifc_geometry_processing import try_get_shape +from speckleifc.ifc_openshell_helpers import get_children from speckleifc.root_object_builder import RootObjectBuilder @@ -21,79 +16,34 @@ class ImportJob: self._ifc_file = ifc_file self.builder = RootObjectBuilder() - def convert(self) -> Collection: - # we're doing a bit of a hybrid approach to traversing the IFC graph. - # First we convert the aggregates graph of spatial elements using a depth first - # traversal of the aggregate relationships, starting from the project. - # This will convert the IfcProject, IfcSite, IfcBuilding, IfcBuildingStorey etc.. - # Note that some of these, like IfcSite may still have geometry... - # - # This DFS approach is similar to how the v2 and v3 web-ifc based importers worked - # But here, is only doing Spatial elements, not walls - root = self._convert_project_tree() + def convert_element(self, step_element: entity_instance) -> Base: + children = self._convert_children(step_element) + shape = try_get_shape(step_element) - # Then, geometry is converted using the geometry iterator, this is efficient - # but returns objects in a non-reliable order, so we use the RootObjectBuilder - # to differ building the rest of the Speckle objects tree - self._convert_geometry() - self.builder.build_commit_object(root) + if step_element.is_a("IfcProject"): + return project_to_speckle(step_element, children) + elif step_element.is_a("IfcSpatialStructureElement"): + return spatial_element_to_speckle(shape, step_element, children) + else: + return data_object_to_speckle(shape, step_element, children) + + def _convert_children(self, step_element: entity_instance) -> list[Base]: + return [self.convert_element(i) for i in get_children(step_element)] + + ## OLD + + def convert(self) -> Base: + + root = self._convert_project_tree() return root - def _convert_geometry(self) -> None: - geometry_iterator = create_geometry_iterator(self._ifc_file) - - if not geometry_iterator.initialize(): - raise SpeckleException("Iterator failed to initialize") - - while True: - shape = geometry_iterator.get() - assert isinstance(shape, TriangulationElement) - - step_id = cast(int, shape.id) - step_element = self._ifc_file.by_id(step_id) - - converted = self.convert_geometry_element(shape, step_element) - self.builder.include_object(converted, step_element, shape) - - if not geometry_iterator.next(): - break - - def _convert_spatial_elements_tree( - self, step_element: entity_instance - ) -> Collection: - - children = self._convert_aggregates(step_element) - - result = spatial_element_to_speckle(step_element, children) - - # Include object in the converted dictionary, - # but no need to set relationships, since we're correctly handling those already - # the the spatial elements traversed here - self.builder.converted[step_element.id()] = result - return result - - def _convert_project_tree(self) -> Collection: + def _convert_project_tree(self) -> Base: projects = self._ifc_file.by_type("IfcProject", False) if len(projects) != 1: raise SpeckleException("Expected exactly one IfcProject in file") project = projects[0] - children = self._convert_aggregates(project) - result = project_to_speckle(project, children) + tree = self.convert_element(project) - self.builder.converted[project.id()] = result - - return result - - def _convert_aggregates(self, step_element: entity_instance) -> list[Base]: - return [ - self._convert_spatial_elements_tree(i) for i in get_aggregates(step_element) - ] - - @staticmethod - def convert_geometry_element( - geometry_element: TriangulationElement, step_element: entity_instance - ) -> Base: - # step_entity = ifc_model.by_id(geometry_element.id()) - return data_object_to_speckle(geometry_element, step_element) + return tree