diff --git a/src/speckleifc/__main__.py b/src/speckleifc/__main__.py index 29606f0..1bebb28 100644 --- a/src/speckleifc/__main__.py +++ b/src/speckleifc/__main__.py @@ -7,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 ### @@ -15,7 +16,7 @@ from speckleifc.importer import convert_file # I'm hoping that POLYGONS_WITHOUT_HOLES is faster than TRIANGLES # but nothing concretely confirmed # tldr: send is not much slower in py from C# -# geometry itterator is pretty slow +# geometry iterator is pretty slow def main() -> Version: @@ -23,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" # noqa: E501 - # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" # noqa: E501 + FILE_PATH = "C:\\Users\\Jedd\\Desktop\\openshell\\hillside_house_meters.ifc" + # FILE_PATH = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" # noqa: E501 + # FILE_PATH = "C:\\Users\\Jedd\\Desktop\\openshell\\GRAPHISOFT_Archicad_Sample_Project-S-Office_v1.0_AC25.ifc" # noqa: E501 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() @@ -36,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() @@ -53,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/data_object_converter.py b/src/speckleifc/converter/data_object_converter.py index efc4d3f..bb49f37 100644 --- a/src/speckleifc/converter/data_object_converter.py +++ b/src/speckleifc/converter/data_object_converter.py @@ -1,27 +1,28 @@ from typing import cast -from ifcopenshell import file -from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement +from ifcopenshell.entity_instance import entity_instance +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( + display_value: list[Base], + step_element: entity_instance, + children: list[Base], +) -> DataObject: -def data_object_to_speckle(shape: TriangulationElement, ifc_model: file) -> DataObject: - geometry = cast(Triangulation, shape.geometry) - display_value = geometry_to_speckle(geometry, ifc_model) + 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["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["@elements"] = children + 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 4a9145c..3c88c71 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/project_converter.py b/src/speckleifc/converter/project_converter.py new file mode 100644 index 0000000..00b849a --- /dev/null +++ b/src/speckleifc/converter/project_converter.py @@ -0,0 +1,26 @@ +from typing import cast + +from ifcopenshell.entity_instance import entity_instance +from specklepy.objects.base import Base +from specklepy.objects.models.collections.collection import Collection + + +def project_to_speckle( + step_element: entity_instance, children: list[Base] +) -> Collection: + + guid = cast(str, step_element.GlobalId) + name = cast(str, step_element.Name or step_element.LongName or guid) + + project = Collection(applicationId=guid, name=name, elements=children) + # TODO: children as "elements" + # data_object["@elements"] = children_converter.convert_children(shape, ifc_model) + + project["expressId"] = step_element.id() + project["ifcType"] = step_element.is_a() + project["description"] = cast(str | None, step_element.Description) + project["objectType"] = step_element.ObjectType + project["longName"] = step_element.LongName + project["phase"] = step_element.Phase + + return project diff --git a/src/speckleifc/converter/spatial_element_converter.py b/src/speckleifc/converter/spatial_element_converter.py new file mode 100644 index 0000000..6351acd --- /dev/null +++ b/src/speckleifc/converter/spatial_element_converter.py @@ -0,0 +1,48 @@ +from typing import cast + +from ifcopenshell.entity_instance import entity_instance +from specklepy.objects.base import Base +from specklepy.objects.data_objects import DataObject +from specklepy.objects.models.collections.collection import Collection + + +def spatial_element_to_speckle( + display_value: list[Base], + step_element: entity_instance, + relational_children: list[Base], +) -> Collection: + + direct_geometry = _convert_as_data_object(display_value, step_element) + all_children = [direct_geometry] + relational_children + + guid = cast(str, step_element.GlobalId) + name = cast(str, step_element.Name or step_element.LongName or guid) + + data_object = Collection(applicationId=guid, name=name, elements=all_children) + data_object["expressId"] = step_element.id() + data_object["ifcType"] = step_element.is_a() + + return data_object + + +def _convert_as_data_object( + display_value: list[Base], step_element: entity_instance +) -> DataObject: + + guid = cast(str, step_element.GlobalId) + name = cast(str, step_element.Name or step_element.LongName or guid) + data_object = DataObject( + applicationId=guid, + properties={}, + name=name, + displayValue=display_value, + ) + + data_object["expressId"] = step_element.id() + data_object["ifcType"] = step_element.is_a() + data_object["description"] = cast(str | None, step_element.Description) + data_object["objectType"] = step_element.ObjectType + data_object["compositionType"] = step_element.CompositionType + data_object["longName"] = step_element.LongName + + return data_object diff --git a/src/speckleifc/ifc_geometry_processing.py b/src/speckleifc/ifc_geometry_processing.py new file mode 100644 index 0000000..0a2bdd8 --- /dev/null +++ b/src/speckleifc/ifc_geometry_processing.py @@ -0,0 +1,57 @@ +import multiprocessing +from typing import cast + +from ifcopenshell import file, ifcopenshell_wrapper, open, sqlite +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.geom import create_shape, iterator, settings +from ifcopenshell.ifcopenshell_wrapper import TriangulationElement +from specklepy.logging.exceptions import SpeckleException + + +def _create_base_settings() -> settings: + ifc_settings = settings() + ifc_settings.set("triangulation-type", ifcopenshell_wrapper.TRIANGLE_MESH) + ifc_settings.set("weld-vertices", False) + ifc_settings.set("use-world-coords", True) + + 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) + + if isinstance(ifc_file, file): + return ifc_file + else: + raise SpeckleException(f"file at {file_path} is not a compatible ifc file type") + + +def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: + return iterator(_IFC_ITERATOR_SETTINGS, ifc_file, multiprocessing.cpu_count() // 2) + + +def try_get_shape(element: entity_instance) -> TriangulationElement | 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) + return cast(TriangulationElement, shape) diff --git a/src/speckleifc/ifc_iterator.py b/src/speckleifc/ifc_iterator.py deleted file mode 100644 index 0405577..0000000 --- a/src/speckleifc/ifc_iterator.py +++ /dev/null @@ -1,29 +0,0 @@ -import multiprocessing - -from ifcopenshell import file, ifcopenshell_wrapper, open, sqlite -from ifcopenshell.geom import iterator, settings -from specklepy.logging.exceptions import SpeckleException - - -def _create_settings() -> settings: - ifc_settings = settings() - ifc_settings.set("triangulation-type", ifcopenshell_wrapper.TRIANGLE_MESH) - ifc_settings.set("weld-vertices", False) - ifc_settings.set("use-world-coords", True) - - return ifc_settings - - -def open_ifc(file_path: str) -> file: - ifc_file = open(file_path) - - if isinstance(ifc_file, file): - return ifc_file - else: - raise SpeckleException(f"file at {file_path} is not a compatible ifc file type") - - -def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: - settings = _create_settings() - - return iterator(settings, ifc_file, multiprocessing.cpu_count() // 2) diff --git a/src/speckleifc/ifc_openshell_helpers.py b/src/speckleifc/ifc_openshell_helpers.py new file mode 100644 index 0000000..56c96d4 --- /dev/null +++ b/src/speckleifc/ifc_openshell_helpers.py @@ -0,0 +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_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 8ff1a99..08ad588 100644 --- a/src/speckleifc/importer.py +++ b/src/speckleifc/importer.py @@ -1,36 +1,74 @@ -from specklepy.logging.exceptions import SpeckleException # noqa: I001 -from ifcopenshell 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 typing import cast + +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.geom import file +from ifcopenshell.ifcopenshell_wrapper import Triangulation, 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.geometry_converter import geometry_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_children +from speckleifc.root_object_builder import RootObjectBuilder -def convert_file(file_path: str) -> Collection: - file = open_ifc(file_path) - iterator = create_geometry_iterator(file) +class ImportJob: + def __init__(self, ifc_file: file): + self._ifc_file = ifc_file + self.builder = RootObjectBuilder() + self.cached_display_values: dict[int, list[Base]] = {} - if not iterator.initialize(): - raise SpeckleException("Iterator failed to initialize") + def convert_element(self, step_element: entity_instance) -> Base: + children = self._convert_children(step_element) + display_value = self.cached_display_values.get(step_element.id(), []) - converted_geometry: list[Base] = [] + if step_element.is_a("IfcProject"): + return project_to_speckle(step_element, children) + elif step_element.is_a("IfcSpatialStructureElement"): + return spatial_element_to_speckle(display_value, step_element, children) + else: + return data_object_to_speckle(display_value, step_element, children) - while True: - element = iterator.get() - assert isinstance(element, TriangulationElement) + def _convert_children(self, step_element: entity_instance) -> list[Base]: + return [self.convert_element(i) for i in get_children(step_element)] - converted = convert_geometry_element(element, file) - converted_geometry.append(converted) + def convert(self) -> Base: - if not iterator.next(): - break + self.pre_process_geometry() - return Collection(name="root collection", elements=converted_geometry) + root = self._convert_project_tree() + return root -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) + def pre_process_geometry(self) -> None: + + iterator = create_geometry_iterator(self._ifc_file) + if not iterator.initialize(): + raise SpeckleException( + "geometry iterator failed to initialize for the given file" + ) + + while True: + shape = cast(TriangulationElement, iterator.get()) + geometry = cast(Triangulation, shape.geometry) + id = cast(int, shape.id) + + display_value = geometry_to_speckle(geometry) + self.cached_display_values[id] = display_value + + if not iterator.next(): + break + pass + + 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] + + tree = self.convert_element(project) + + return tree diff --git a/src/speckleifc/root_object_builder.py b/src/speckleifc/root_object_builder.py new file mode 100644 index 0000000..abc6e52 --- /dev/null +++ b/src/speckleifc/root_object_builder.py @@ -0,0 +1,94 @@ +from collections.abc import Sequence +from typing import cast + +from attrs import define +from ifcopenshell.entity_instance import entity_instance +from ifcopenshell.ifcopenshell_wrapper import Element +from ifcopenshell.util.element import get_container +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_object( + self, + conversion_result: Base, + step_element: entity_instance, + shape: Element | None, + ) -> None: + step_id = step_element.id() + self.converted[step_id] = conversion_result + + if shape is None: + parent = get_container(step_element) + parent_id = parent.id() if parent else None + else: + parent_id = cast(int, shape.parent_id) + + self.set_relationship(step_id, ((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(): + + if step_id not in self._parent_infos: + continue + + 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!" + )