feat(ifc)!: Display Value proxies for IFC importer (#466)
Publish Python Package / test (push) Has been cancelled
Publish Python Package / Build and Publish Python Package (push) Has been cancelled

* 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
This commit is contained in:
Jedd Morgan
2025-10-31 13:54:46 +00:00
committed by GitHub
parent 78b3e99475
commit 8edc0d5d78
8 changed files with 120 additions and 24 deletions
@@ -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:
+4 -2
View File
@@ -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
+63 -12
View File
@@ -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
@@ -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
+3 -3
View File
@@ -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