Merge pull request #1 from specklesystems/jrm/spatial-elements
feat(conversion): Add support for converting spatial elements with correct tree structure
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+60
-22
@@ -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]] = {}
|
||||
|
||||
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(), [])
|
||||
|
||||
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)
|
||||
|
||||
def _convert_children(self, step_element: entity_instance) -> list[Base]:
|
||||
return [self.convert_element(i) for i in get_children(step_element)]
|
||||
|
||||
def convert(self) -> Base:
|
||||
|
||||
self.pre_process_geometry()
|
||||
|
||||
root = self._convert_project_tree()
|
||||
|
||||
return root
|
||||
|
||||
def pre_process_geometry(self) -> None:
|
||||
|
||||
iterator = create_geometry_iterator(self._ifc_file)
|
||||
if not iterator.initialize():
|
||||
raise SpeckleException("Iterator failed to initialize")
|
||||
|
||||
converted_geometry: list[Base] = []
|
||||
raise SpeckleException(
|
||||
"geometry iterator failed to initialize for the given file"
|
||||
)
|
||||
|
||||
while True:
|
||||
element = iterator.get()
|
||||
assert isinstance(element, TriangulationElement)
|
||||
shape = cast(TriangulationElement, iterator.get())
|
||||
geometry = cast(Triangulation, shape.geometry)
|
||||
id = cast(int, shape.id)
|
||||
|
||||
converted = convert_geometry_element(element, file)
|
||||
converted_geometry.append(converted)
|
||||
display_value = geometry_to_speckle(geometry)
|
||||
self.cached_display_values[id] = display_value
|
||||
|
||||
if not iterator.next():
|
||||
break
|
||||
pass
|
||||
|
||||
return Collection(name="root collection", elements=converted_geometry)
|
||||
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)
|
||||
|
||||
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)
|
||||
return tree
|
||||
|
||||
@@ -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!"
|
||||
)
|
||||
Reference in New Issue
Block a user