From f937df95e066343d319db278bb6a6d13dc2dccc8 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Thu, 19 Jun 2025 10:04:15 +0100 Subject: [PATCH] first pass --- src/speckleifc/__main__.py | 18 ++-- .../converter/collection_to_speckle.py | 29 +++++++ .../converter/data_object_converter.py | 15 ++-- .../converter/geometry_converter.py | 3 +- .../converter/spatial_element_to_speckle.py | 32 +++++++ ...iterator.py => ifc_geometry_processing.py} | 24 +++++- src/speckleifc/importer.py | 84 ++++++++++++------- src/speckleifc/root_object_builder.py | 81 ++++++++++++++++++ 8 files changed, 238 insertions(+), 48 deletions(-) create mode 100644 src/speckleifc/converter/collection_to_speckle.py create mode 100644 src/speckleifc/converter/spatial_element_to_speckle.py rename src/speckleifc/{ifc_iterator.py => ifc_geometry_processing.py} (51%) create mode 100644 src/speckleifc/root_object_builder.py diff --git a/src/speckleifc/__main__.py b/src/speckleifc/__main__.py index e84b81c..21b616b 100644 --- a/src/speckleifc/__main__.py +++ b/src/speckleifc/__main__.py @@ -1,4 +1,5 @@ import time + from specklepy.api.operations import send from specklepy.core.api.client import SpeckleClient from specklepy.core.api.credentials import get_accounts_for_server @@ -6,7 +7,8 @@ from specklepy.core.api.inputs.version_inputs import CreateVersionInput from specklepy.core.api.models import Version from specklepy.transports.server import ServerTransport -from speckleifc.importer import convert_file +from speckleifc.ifc_geometry_processing import open_ifc +from speckleifc.importer import ImportJob ### @@ -22,12 +24,16 @@ def main() -> Version: MODEL_ID = "0e23cfdea3" SERVER_URL = "app.speckle.systems" # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\60mins.ifc" - FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\hillside_house_meters.ifc" - # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" + # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\hillside_house_meters.ifc" # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" + FILE_PATH = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" start = time.time() - data = convert_file(FILE) + + ifc_file = open_ifc(FILE_PATH) + import_job = ImportJob(ifc_file) + data = import_job.convert() + print(f"File conversion complete after {(time.time() - start) * 1000}ms") start = time.time() @@ -35,7 +41,7 @@ def main() -> Version: remote_transport = ServerTransport(PROJECT_ID, account=account) - root_id = send(data, transports=[remote_transport]) + root_id = send(data, transports=[remote_transport], use_default_cache=False) print(f"Sending to speckle complete after: {(time.time() - start) * 1000}ms") start = time.time() @@ -52,4 +58,6 @@ def main() -> Version: if __name__ == "__main__": + start = time.time() main() + print(f"Total time: {(time.time() - start) * 1000}ms") diff --git a/src/speckleifc/converter/collection_to_speckle.py b/src/speckleifc/converter/collection_to_speckle.py new file mode 100644 index 0000000..9db29c0 --- /dev/null +++ b/src/speckleifc/converter/collection_to_speckle.py @@ -0,0 +1,29 @@ +from typing import cast + +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement +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 +) -> DataObject: + + geometry = cast(Triangulation, shape.geometry) + display_value = geometry_to_speckle(geometry) + + data_object = DataObject( + applicationId=cast(str, shape.guid), + properties={}, + name=cast(str, shape.name) or cast(str, shape.guid), + displayValue=display_value, + ) + # TODO: children as "elements" + # data_object["@elements"] = children_converter.convert_children(shape, ifc_model) + + data_object["ifcType"] = cast(str, shape.type) + data_object["expressId"] = cast(int, shape.id) + data_object["description"] = cast(str, step_element.Description) + return data_object diff --git a/src/speckleifc/converter/data_object_converter.py b/src/speckleifc/converter/data_object_converter.py index 4c81913..5a4077b 100644 --- a/src/speckleifc/converter/data_object_converter.py +++ b/src/speckleifc/converter/data_object_converter.py @@ -1,16 +1,18 @@ from typing import cast -from ifcopenshell import file +from ifcopenshell.entity_instance import entity_instance from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement from specklepy.objects.data_objects import DataObject from speckleifc.converter.geometry_converter import geometry_to_speckle -def data_object_to_speckle(shape: TriangulationElement, ifc_model: file) -> DataObject: +def data_object_to_speckle( + shape: TriangulationElement, step_element: entity_instance +) -> DataObject: geometry = cast(Triangulation, shape.geometry) - display_value = geometry_to_speckle(geometry, ifc_model) + display_value = geometry_to_speckle(geometry) data_object = DataObject( applicationId=cast(str, shape.guid), @@ -21,8 +23,7 @@ def data_object_to_speckle(shape: TriangulationElement, ifc_model: file) -> Data # TODO: children as "elements" # data_object["@elements"] = children_converter.convert_children(shape, ifc_model) - data_object["ifcType"] = cast(str, shape.type) - data_object["expressId"] = cast(str, shape.id) - data_object["ownerId"] = cast(str, shape.parent_id) - data_object["description"] = cast(str, shape.unique_id) + data_object["ifcType"] = step_element.is_a() + data_object["expressId"] = step_element.id() + data_object["description"] = cast(str | None, step_element.Description) return data_object diff --git a/src/speckleifc/converter/geometry_converter.py b/src/speckleifc/converter/geometry_converter.py index ebc04a3..27035da 100644 --- a/src/speckleifc/converter/geometry_converter.py +++ b/src/speckleifc/converter/geometry_converter.py @@ -2,13 +2,12 @@ from collections import defaultdict from collections.abc import Sequence from typing import cast -from ifcopenshell import file from ifcopenshell.ifcopenshell_wrapper import Triangulation from specklepy.objects import Base from specklepy.objects.geometry import Mesh -def geometry_to_speckle(geometry: Triangulation, ifc_model: file) -> list[Base]: +def geometry_to_speckle(geometry: Triangulation) -> list[Base]: materials = cast(Sequence[int], geometry.materials) MESH_COUNT = max(len(materials), 1) diff --git a/src/speckleifc/converter/spatial_element_to_speckle.py b/src/speckleifc/converter/spatial_element_to_speckle.py new file mode 100644 index 0000000..70d1e69 --- /dev/null +++ b/src/speckleifc/converter/spatial_element_to_speckle.py @@ -0,0 +1,32 @@ +from typing import cast + +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.ifcopenshell_wrapper import Triangulation +from specklepy.objects.data_objects import DataObject + +from speckleifc.converter.geometry_converter import geometry_to_speckle +from speckleifc.ifc_geometry_processing import get_shape + + +def spatial_element_to_speckle(step_element: entity_instance) -> DataObject: + + if step_element.Representation is not None: + shape = get_shape(step_element) + geometry = cast(Triangulation, shape.geometry) + display_value = geometry_to_speckle(geometry) + else: + display_value = [] + + data_object = DataObject( + applicationId=cast(str, shape.guid), + properties={}, + name=cast(str, shape.name) or cast(str, shape.guid), + displayValue=display_value, + ) + # TODO: children as "elements" + # data_object["@elements"] = children_converter.convert_children(shape, ifc_model) + + data_object["ifcType"] = step_element.is_a() + data_object["expressId"] = step_element.id() + data_object["description"] = cast(str | None, step_element.Description) + return data_object diff --git a/src/speckleifc/ifc_iterator.py b/src/speckleifc/ifc_geometry_processing.py similarity index 51% rename from src/speckleifc/ifc_iterator.py rename to src/speckleifc/ifc_geometry_processing.py index 0405577..1e2e7d8 100644 --- a/src/speckleifc/ifc_iterator.py +++ b/src/speckleifc/ifc_geometry_processing.py @@ -1,11 +1,13 @@ import multiprocessing +from typing import cast from ifcopenshell import file, ifcopenshell_wrapper, open, sqlite -from ifcopenshell.geom import iterator, settings +from ifcopenshell.geom import create_shape, iterator, settings +from ifcopenshell.ifcopenshell_wrapper import TriangulationElement from specklepy.logging.exceptions import SpeckleException -def _create_settings() -> settings: +def _create_base_settings() -> settings: ifc_settings = settings() ifc_settings.set("triangulation-type", ifcopenshell_wrapper.TRIANGLE_MESH) ifc_settings.set("weld-vertices", False) @@ -14,6 +16,16 @@ def _create_settings() -> settings: return ifc_settings +def _create_iterator_settings() -> settings: + ifc_settings = _create_base_settings() + + return ifc_settings + + +_IFC_ITERATOR_SETTINGS = _create_iterator_settings() +_IFC_SETTINGS = _create_base_settings() + + def open_ifc(file_path: str) -> file: ifc_file = open(file_path) @@ -24,6 +36,10 @@ def open_ifc(file_path: str) -> file: def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: - settings = _create_settings() - return iterator(settings, ifc_file, multiprocessing.cpu_count() // 2) + return iterator(_IFC_ITERATOR_SETTINGS, ifc_file, multiprocessing.cpu_count() // 2) + + +def get_shape(element) -> TriangulationElement: + shape = create_shape(_IFC_SETTINGS, element) + return cast(TriangulationElement, shape) diff --git a/src/speckleifc/importer.py b/src/speckleifc/importer.py index 8ff1a99..cd29b68 100644 --- a/src/speckleifc/importer.py +++ b/src/speckleifc/importer.py @@ -1,36 +1,60 @@ -from specklepy.logging.exceptions import SpeckleException # noqa: I001 -from ifcopenshell import file +from typing import cast + +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.geom import file from ifcopenshell.ifcopenshell_wrapper import TriangulationElement -from speckleifc.converter.data_object_converter import data_object_to_speckle -from speckleifc.ifc_iterator import create_geometry_iterator, open_ifc +from specklepy.logging.exceptions import SpeckleException from specklepy.objects import Base from specklepy.objects.models.collections.collection import Collection - -def convert_file(file_path: str) -> Collection: - file = open_ifc(file_path) - iterator = create_geometry_iterator(file) - - if not iterator.initialize(): - raise SpeckleException("Iterator failed to initialize") - - converted_geometry: list[Base] = [] - - while True: - element = iterator.get() - assert isinstance(element, TriangulationElement) - - converted = convert_geometry_element(element, file) - converted_geometry.append(converted) - - if not iterator.next(): - break - - return Collection(name="root collection", elements=converted_geometry) +from speckleifc.converter.data_object_converter import data_object_to_speckle +from speckleifc.ifc_geometry_processing import create_geometry_iterator +from speckleifc.root_object_builder import RootObjectBuilder -def convert_geometry_element( - geometry_element: TriangulationElement, ifc_model: file -) -> Base: - # step_entity = ifc_model.by_id(geometry_element.id()) - return data_object_to_speckle(geometry_element, ifc_model) +class ImportJob: + + def __init__(self, ifc_file: file): + self._ifc_file = ifc_file + self.builder = RootObjectBuilder() + + def convert(self) -> Collection: + self._convert_spatial_elements() + + self._convert_geometry() + + root = Collection(name="root collection") # todo: replace with project + self.builder.build_commit_object(root) + 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_shape(converted, shape) + + if not geometry_iterator.next(): + break + + def _convert_spatial_elements(self) -> None: + spatial_elements = self._ifc_file.by_type("IfcSpatialElement") + + for element in spatial_elements: + 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) diff --git a/src/speckleifc/root_object_builder.py b/src/speckleifc/root_object_builder.py new file mode 100644 index 0000000..7017b81 --- /dev/null +++ b/src/speckleifc/root_object_builder.py @@ -0,0 +1,81 @@ +from collections.abc import Sequence +from typing import cast + +from attrs import define +from ifcopenshell.ifcopenshell_wrapper import Element +from specklepy.objects.base import Base +from specklepy.objects.graph_traversal.commit_object_builder import ( + get_detached_prop, + set_detached_prop, +) + +ROOT: int = -1 +ELEMENTS = "elements" + +PARENT_INFO = tuple[int | None, str] + + +@define(slots=True) +class RootObjectBuilder: + converted: dict[int, Base] + _parent_infos: dict[int, Sequence[PARENT_INFO]] + + def __init__(self) -> None: + self.converted = {} + self._parent_infos = {} + + def include_shape(self, conversion_result: Base, shape: Element) -> None: + step_id = cast(int, shape.id) + + self.converted[step_id] = conversion_result + + self.set_relationship( + step_id, ((cast(int, shape.parent_id), ELEMENTS), (ROOT, ELEMENTS)) + ) + + def build_commit_object(self, root_commit_object: Base) -> None: + self.apply_relationships(root_commit_object) + + def set_relationship( + self, step_id: int, parent_info: Sequence[PARENT_INFO] + ) -> None: + self._parent_infos[step_id] = parent_info + + def apply_relationships(self, root_commit_object: Base) -> None: + for step_id, c in self.converted.items(): + try: + self.apply_relationship(c, step_id, root_commit_object) + except Exception as ex: + print(f"Failed to add object {type(c)} to commit object: {ex}") + + def apply_relationship( + self, current: Base, step_id: int, root_commit_object: Base + ) -> None: + + parents = self._parent_infos[step_id] + + for parent_id, prop_name in parents: + if not parent_id: + continue + + parent: Base | None + if parent_id == ROOT: + parent = root_commit_object + else: + parent = self.converted.get(parent_id, None) + + if not parent: + continue + + elements = get_detached_prop(parent, prop_name) + if not isinstance(elements, list): + elements = [] + set_detached_prop(parent, prop_name, elements) + + elements.append(current) + return + + raise Exception( + f"Could not find a valid parent for object of type {type(current)}." + f"Checked {len(parents)} potential parent, and non were converted!" + )