From 8edc0d5d78cdcbaed1272d0df66e5f55b02cecb6 Mon Sep 17 00:00:00 2001 From: Jedd Morgan <45512892+JR-Morgan@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:54:46 +0000 Subject: [PATCH] feat(ifc)!: Display Value proxies for IFC importer (#466) * First pass * Add applicationId for proxyInstance * renamed revit instances * renamed collection again * again * reverted main changes for manual testing * small refactor of function def * format matix --- .../converter/geometry_converter.py | 14 ++-- src/speckleifc/ifc_geometry_processing.py | 6 +- src/speckleifc/importer.py | 75 ++++++++++++++++--- src/speckleifc/proxy_managers/__init__.py | 0 .../proxy_managers/instance_proxy_manager.py | 43 +++++++++++ .../level_proxy_manager.py | 0 .../render_material_proxy_manager.py | 0 src/specklepy/objects/proxies.py | 6 +- 8 files changed, 120 insertions(+), 24 deletions(-) create mode 100644 src/speckleifc/proxy_managers/__init__.py create mode 100644 src/speckleifc/proxy_managers/instance_proxy_manager.py rename src/speckleifc/{ => proxy_managers}/level_proxy_manager.py (100%) rename src/speckleifc/{ => proxy_managers}/render_material_proxy_manager.py (100%) diff --git a/src/speckleifc/converter/geometry_converter.py b/src/speckleifc/converter/geometry_converter.py index ae05946..467a446 100644 --- a/src/speckleifc/converter/geometry_converter.py +++ b/src/speckleifc/converter/geometry_converter.py @@ -4,21 +4,21 @@ from typing import cast from ifcopenshell.ifcopenshell_wrapper import ( Triangulation, - TriangulationElement, colour, style, ) -from speckleifc.render_material_proxy_manager import RenderMaterialProxyManager +from speckleifc.proxy_managers.render_material_proxy_manager import ( + RenderMaterialProxyManager, +) from specklepy.objects.base import Base from specklepy.objects.geometry import Mesh from specklepy.objects.other import RenderMaterial def geometry_to_speckle( - shape: TriangulationElement, render_material_manager: RenderMaterialProxyManager + geometry: Triangulation, render_material_manager: RenderMaterialProxyManager ) -> list[Base]: - geometry = cast(Triangulation, shape.geometry) materials = cast(Sequence[style], geometry.materials) MESH_COUNT = max(len(materials), 1) @@ -33,7 +33,7 @@ def geometry_to_speckle( # Not really expected, but occasionally some meshes fail to triangulate return [] - mapped_meshes = _pre_alloc_mesh_lists(shape, material_ids, MESH_COUNT) + mapped_meshes = _pre_alloc_mesh_lists(geometry, material_ids, MESH_COUNT) for i, mesh in enumerate(mapped_meshes): material = _material_to_speckle(materials[i]) render_material_manager.add_mesh_material_mapping(material, mesh) @@ -103,14 +103,14 @@ def _color_to_argb(colour: colour) -> int: def _pre_alloc_mesh_lists( - shape: TriangulationElement, material_ids: Sequence[int], MESH_COUNT: int + geometry: Triangulation, material_ids: Sequence[int], MESH_COUNT: int ) -> list[Mesh]: """ This is a performance optimisation to pre-size the lists since we're expecting potential hundreds of thousands of verts in a single model This is very much in the hot path, so worth the extra bit of convoluted logic """ - appId = cast(str, shape.guid) + appId = cast(str, geometry.id) material_face_counts = defaultdict(int) for mat_id in material_ids: diff --git a/src/speckleifc/ifc_geometry_processing.py b/src/speckleifc/ifc_geometry_processing.py index 242c750..8a564b1 100644 --- a/src/speckleifc/ifc_geometry_processing.py +++ b/src/speckleifc/ifc_geometry_processing.py @@ -12,8 +12,10 @@ def _create_iterator_settings() -> settings: ifc_settings.set("triangulation-type", ifcopenshell_wrapper.TRIANGLE_MESH) # no need to weld verts ifc_settings.set("weld-vertices", False) - # Speckle meshes are all in world coords - ifc_settings.set("use-world-coords", True) + # + ifc_settings.set("use-world-coords", False) + ifc_settings.set("permissive-shape-reuse", True) + # Tiny performance improvement, ifc_settings.set("no-wire-intersection-check", True) # Rendermaterials inherit the material names instead of type + unique id diff --git a/src/speckleifc/importer.py b/src/speckleifc/importer.py index 1ed9e4c..43329a0 100644 --- a/src/speckleifc/importer.py +++ b/src/speckleifc/importer.py @@ -1,10 +1,10 @@ import time from dataclasses import dataclass, field -from typing import cast +from typing import List, cast from ifcopenshell.entity_instance import entity_instance from ifcopenshell.geom import file -from ifcopenshell.ifcopenshell_wrapper import TriangulationElement +from ifcopenshell.ifcopenshell_wrapper import Triangulation, TriangulationElement from speckleifc.converter.data_object_converter import data_object_to_speckle from speckleifc.converter.geometry_converter import geometry_to_speckle @@ -12,27 +12,38 @@ 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.level_proxy_manager import LevelProxyManager -from speckleifc.render_material_proxy_manager import RenderMaterialProxyManager +from speckleifc.proxy_managers.instance_proxy_manager import InstanceProxyManager +from speckleifc.proxy_managers.level_proxy_manager import LevelProxyManager +from speckleifc.proxy_managers.render_material_proxy_manager import ( + RenderMaterialProxyManager, +) from specklepy.logging.exceptions import SpeckleException from specklepy.objects import Base from specklepy.objects.data_objects import DataObject +from specklepy.objects.models.collections.collection import Collection +from specklepy.objects.proxies import InstanceProxy @dataclass class ImportJob: ifc_file: file - cached_display_values: dict[int, list[Base]] = field(default_factory=dict) # noqa: F821 + _render_material_manager: RenderMaterialProxyManager = field( default_factory=lambda: RenderMaterialProxyManager() ) _level_proxy_manager: LevelProxyManager = field( default_factory=lambda: LevelProxyManager() ) + _instance_proxy_manager: InstanceProxyManager = field( + default_factory=lambda: InstanceProxyManager() + ) geometries_count: int = 0 geometries_used: int = 0 _current_storey_data_object: DataObject | None = field(default=None, init=False) + _display_value_cache: dict[int, list[Base]] = field(default_factory=dict) + """Maps an instance step ID to a list of instances""" + def convert_element(self, step_element: entity_instance) -> Base: try: return self._convert_element(step_element) @@ -48,14 +59,14 @@ class ImportJob: previous_storey_data_object = self._current_storey_data_object if step_element.is_a("IfcBuildingStorey"): # Convert the building storey to a DataObject for the level proxy - storey_display_value = self.cached_display_values.get(step_element.id(), []) + storey_display_value = self._display_value_cache.get(step_element.id(), []) self._current_storey_data_object = data_object_to_speckle( storey_display_value, step_element, [] ) children = self._convert_children(step_element) id = step_element.id() - display_value = self.cached_display_values.get(id, []) + display_value = self._display_value_cache.get(id, []) if display_value: self.geometries_used += 1 @@ -127,12 +138,9 @@ class ImportJob: shape = cast(TriangulationElement, iterator.get()) self.geometries_count += 1 id = cast(int, shape.id) - try: - display_value = geometry_to_speckle( - shape, self._render_material_manager - ) - self.cached_display_values[id] = display_value + display_value = self._create_display_value(shape) + self._display_value_cache[id] = display_value except Exception as ex: raise SpeckleException( f"Failed to convert geometry with id: {id}" @@ -140,6 +148,37 @@ class ImportJob: if not iterator.next(): break + def _create_display_value(self, shape: TriangulationElement) -> List[Base]: + geometry = cast(Triangulation, shape.geometry) + display_value_geometry = geometry_to_speckle( + geometry, self._render_material_manager + ) + + definition_ids = self._instance_proxy_manager.add_display_value_definitions( + display_value_geometry + ) + matrix = shape.transformation.matrix + transposed = [ + matrix[0], matrix[4], matrix[8], matrix[12], + matrix[1], matrix[5], matrix[9], matrix[13], + matrix[2], matrix[6], matrix[10], matrix[14], + matrix[3], matrix[7], matrix[11], matrix[15], + ] # fmt: skip + + return [ + cast( + Base, + InstanceProxy( + units="m", + definitionId=definition_id, + transform=transposed, + maxDepth=0, + applicationId=f"{shape.guid}:{definition_id}", + ), + ) + for definition_id in definition_ids + ] + def _convert_project_tree(self) -> Base: projects = self.ifc_file.by_type("IfcProject", False) if len(projects) != 1: @@ -147,10 +186,22 @@ class ImportJob: project = projects[0] tree = self.convert_element(project) + if not isinstance(tree, Collection): + raise TypeError("Expected root object to convert to a Collection") + tree["renderMaterialProxies"] = list( self._render_material_manager.render_material_proxies.values() ) tree["levelProxies"] = list(self._level_proxy_manager.level_proxies.values()) + tree["instanceDefinitionProxies"] = list( + self._instance_proxy_manager.instance_definition_proxies.values() + ) + tree.elements.append( + Collection( + name="definitionGeometry", + elements=list(self._instance_proxy_manager.instance_geometry.values()), + ) + ) tree["version"] = 3 return tree diff --git a/src/speckleifc/proxy_managers/__init__.py b/src/speckleifc/proxy_managers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/speckleifc/proxy_managers/instance_proxy_manager.py b/src/speckleifc/proxy_managers/instance_proxy_manager.py new file mode 100644 index 0000000..e699d43 --- /dev/null +++ b/src/speckleifc/proxy_managers/instance_proxy_manager.py @@ -0,0 +1,43 @@ +from typing import Sequence + +from specklepy.objects.base import Base +from specklepy.objects.proxies import InstanceDefinitionProxy + + +class InstanceProxyManager: + def __init__(self): + self._instance_definition_proxies: dict[str, InstanceDefinitionProxy] = {} + """definition proxies to be added directly to the root""" + self._instance_geometry: dict[str, Base] = {} + """The geometry that will be added in it's own collection under the root""" + + @property + def instance_definition_proxies(self) -> dict[str, InstanceDefinitionProxy]: + return self._instance_definition_proxies + + @property + def instance_geometry(self) -> dict[str, Base]: + return self._instance_geometry + + def add_display_value_definitions(self, geometry: Sequence[Base]) -> list[str]: + result: list[str] = [] + for m in geometry: + if not m.applicationId: + raise ValueError("geometry with no applicationId cannot be proxied ") + definition_id = f"DEFINITION:{m.applicationId}" + result.append(definition_id) + self._add_definition(definition_id, [m.applicationId], 0) + self._instance_geometry[m.applicationId] = m + + return result + + def _add_definition( + self, definition_id: str, objects: list[str], max_depth: int + ) -> None: + proxy = InstanceDefinitionProxy( + applicationId=definition_id, + name=definition_id, + objects=objects, + maxDepth=max_depth, + ) + self._instance_definition_proxies[definition_id] = proxy diff --git a/src/speckleifc/level_proxy_manager.py b/src/speckleifc/proxy_managers/level_proxy_manager.py similarity index 100% rename from src/speckleifc/level_proxy_manager.py rename to src/speckleifc/proxy_managers/level_proxy_manager.py diff --git a/src/speckleifc/render_material_proxy_manager.py b/src/speckleifc/proxy_managers/render_material_proxy_manager.py similarity index 100% rename from src/speckleifc/render_material_proxy_manager.py rename to src/speckleifc/proxy_managers/render_material_proxy_manager.py diff --git a/src/specklepy/objects/proxies.py b/src/specklepy/objects/proxies.py index bc58b26..67935c6 100644 --- a/src/specklepy/objects/proxies.py +++ b/src/specklepy/objects/proxies.py @@ -33,9 +33,9 @@ class InstanceProxy( IHasUnits, speckle_type="Speckle.Core.Models.Instances.InstanceProxy", ): - definition_id: str + definitionId: str transform: List[float] - max_depth: int + maxDepth: int @dataclass(kw_only=True) @@ -45,7 +45,7 @@ class InstanceDefinitionProxy( detachable={"objects"}, ): objects: List[str] - max_depth: int + maxDepth: int name: str