diff --git a/.gitignore b/.gitignore index 326ee0d..69139b1 100644 --- a/.gitignore +++ b/.gitignore @@ -115,3 +115,4 @@ venv.bak/ # other scratch.py settings.json +*.prof \ No newline at end of file diff --git a/src/speckleifc/__main__.py b/src/speckleifc/__main__.py index 4a355ad..3ffd65b 100644 --- a/src/speckleifc/__main__.py +++ b/src/speckleifc/__main__.py @@ -1,45 +1,94 @@ +import json import time +import traceback +from argparse import ArgumentParser +from os import getenv -from specklepy.api.operations import send from specklepy.core.api.client import SpeckleClient -from specklepy.core.api.credentials import get_accounts_for_server +from specklepy.core.api.credentials import Account, get_accounts_for_server from specklepy.core.api.inputs.version_inputs import CreateVersionInput -from specklepy.core.api.models import Version +from specklepy.core.api.models.current import Version +from specklepy.core.api.operations import send from specklepy.transports.server import ServerTransport from speckleifc.ifc_geometry_processing import open_ifc from speckleifc.importer import ImportJob -### -# TODO: tomorrow, either we optimise n-gon PR, or we throw it away. -# 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 iterator is pretty slow +def cmd_line_import() -> None: + + parser = ArgumentParser( + prog="speckleifc", + description="imports a file", + ) + parser.add_argument("file_path") + parser.add_argument("output_path") + parser.add_argument("project_id") + parser.add_argument("version_message") + parser.add_argument("model_id") + # parser.add_argument("model_name") + # parser.add_argument("region_name") + + args = parser.parse_args() + + TOKEN = getenv("USER_TOKEN") + assert TOKEN is not None + SERVER_URL = getenv("SPECKLE_SERVER_URL") or "http://127.0.0.1:3000" + account = Account.from_token(TOKEN, SERVER_URL) + + try: + version = open_and_convert_file( + args.file_path, + args.project_id, + args.version_message, + args.model_id, + account, + ) + with open(args.output_path, "w") as f: + json.dump({"success": True, "commitId": version.id}, f) + except Exception as e: + error_msg = f"IFC Importer failed with exception:\n{traceback.format_exc()}" + print(error_msg) + + # Write error result + with open(args.output_path, "w") as f: + json.dump({"success": False, "error": str(e)}, f) -def main() -> Version: +def manual_import() -> None: PROJECT_ID = "f3a42bdf24" MODEL_ID = "0e23cfdea3" SERVER_URL = "app.speckle.systems" - # FILE = "C:\\Users\\Jedd\\Desktop\\openshell\\60mins.ifc" + # FILE_PATH = "C:\\Users\\Jedd\\Desktop\\openshell\\60mins.ifc" # 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() + account = get_accounts_for_server(SERVER_URL)[0] - ifc_file = open_ifc(FILE_PATH) + open_and_convert_file(FILE_PATH, PROJECT_ID, None, MODEL_ID, account) + + +def open_and_convert_file( + file_path: str, + project_id: str, + version_message: str | None, + model_id: str, + account: Account, +) -> Version: + + start = time.time() + very_start = start + + 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() - account = get_accounts_for_server(SERVER_URL)[0] - remote_transport = ServerTransport(PROJECT_ID, account=account) + remote_transport = ServerTransport(project_id, account=account) root_id = send(data, transports=[remote_transport], use_default_cache=False) print(f"Sending to speckle complete after: {(time.time() - start) * 1000}ms") @@ -49,15 +98,22 @@ def main() -> Version: client.authenticate_with_account(account) create_version = CreateVersionInput( - object_id=root_id, model_id=MODEL_ID, project_id=PROJECT_ID + object_id=root_id, + model_id=model_id, + project_id=project_id, + message=version_message, ) version = client.version.create(create_version) - print(f"Version committed after: {(time.time() - start) * 1000}ms") + end = time.time() + print(f"Version committed after: {(end - start) * 1000}ms") + + print(f"Total time (to commit): {(end - very_start) * 1000}ms") + del ifc_file return version if __name__ == "__main__": start = time.time() - main() - print(f"Total time: {(time.time() - start) * 1000}ms") + cmd_line_import() + print(f"Total time (including cleanup): {(time.time() - start) * 1000}ms") diff --git a/src/speckleifc/converter/geometry_converter.py b/src/speckleifc/converter/geometry_converter.py index 2e5eb5a..84edef3 100644 --- a/src/speckleifc/converter/geometry_converter.py +++ b/src/speckleifc/converter/geometry_converter.py @@ -25,6 +25,13 @@ def geometry_to_speckle( material_ids = cast(Sequence[int], geometry.material_ids) faces = cast(Sequence[int], geometry.faces) verts = cast(Sequence[float], geometry.verts) + normals = cast(Sequence[float], geometry.normals) + + FACE_COUNT = len(material_ids) + + if len(faces) != FACE_COUNT * 3: + # Not really expected, but occasionally some meshes fail to triangulate + return [] mapped_meshes = _pre_alloc_mesh_lists(shape, material_ids, MESH_COUNT) for i, mesh in enumerate(mapped_meshes): @@ -35,10 +42,6 @@ def geometry_to_speckle( mapped_vertices_pointers = [0] * MESH_COUNT mapped_index_counters = [0] * MESH_COUNT - FACE_COUNT = len(material_ids) - - assert len(faces) == FACE_COUNT * 3 - i = 0 face_index = 0 while i < FACE_COUNT: @@ -60,6 +63,10 @@ def geometry_to_speckle( mesh.vertices[mapped_vert_offset + 1] = verts[vert_index + 1] mesh.vertices[mapped_vert_offset + 2] = verts[vert_index + 2] + mesh.vertexNormals[mapped_vert_offset] = normals[vert_index] + mesh.vertexNormals[mapped_vert_offset + 1] = normals[vert_index + 1] + mesh.vertexNormals[mapped_vert_offset + 2] = normals[vert_index + 2] + i += 1 face_index += 3 # number of items in the faces list we just jumped over @@ -115,6 +122,7 @@ def _pre_alloc_mesh_lists( mesh = Mesh( units="m", vertices=[-1] * (face_count * 9), + vertexNormals=[-1] * (face_count * 9), faces=[-1] * (face_count * 4), # 1 marker + 3 vertex indices applicationId=f"{appId}_mat{mat_id}", ) diff --git a/src/speckleifc/ifc_geometry_processing.py b/src/speckleifc/ifc_geometry_processing.py index e69c880..3a98e5d 100644 --- a/src/speckleifc/ifc_geometry_processing.py +++ b/src/speckleifc/ifc_geometry_processing.py @@ -13,6 +13,18 @@ def _create_iterator_settings() -> settings: ifc_settings.set("weld-vertices", False) # Speckle meshes are all in world coords ifc_settings.set("use-world-coords", True) + # Tiny performance improvement, + ifc_settings.set("no-wire-intersection-check", True) + + # IfcOpenshell defaults to 0.001mm here, which leads to very dense meshes. + # lowering the mesh quality a bit here leads to meshes + # that are still much higher quality than webifc + + # We still need to experiment with the affect on memory usage + # It may be desirable to lower this further, and increase the angular deflection + # to compensate. This would allow large meshes to be lower quality, + # while keeping small meshes relatively similar. + ifc_settings.set("mesher-linear-deflection", 0.2) return ifc_settings @@ -27,6 +39,4 @@ def open_ifc(file_path: str) -> file: def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: - return iterator( - _create_iterator_settings(), ifc_file, multiprocessing.cpu_count() // 2 - ) + return iterator(_create_iterator_settings(), ifc_file, multiprocessing.cpu_count()) diff --git a/src/speckleifc/importer.py b/src/speckleifc/importer.py index aab3323..6114f6c 100644 --- a/src/speckleifc/importer.py +++ b/src/speckleifc/importer.py @@ -1,3 +1,4 @@ +import time from typing import cast from ifcopenshell.entity_instance import entity_instance @@ -20,11 +21,16 @@ class ImportJob: self._ifc_file = ifc_file self.cached_display_values: dict[int, list[Base]] = {} self._render_material_manager = RenderMaterialProxyManager() + self.geometries_count = 0 + self.geometries_used = 0 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 display_value is not None: + self.geometries_used += 1 + if step_element.is_a("IfcProject"): return project_to_speckle(step_element, children) elif step_element.is_a("IfcSpatialStructureElement"): @@ -36,10 +42,15 @@ class ImportJob: return [self.convert_element(i) for i in get_children(step_element)] def convert(self) -> Base: + start = time.time() self.pre_process_geometry() + print(f"Geometry conversion complete after {(time.time() - start) * 1000}ms") + print(f"Created {self.geometries_count} geometries") + start = time.time() root = self._convert_project_tree() - + print(f"Object tree conversion complete after {(time.time() - start) * 1000}ms") + print(f"Used {self.geometries_used} geometries") return root def pre_process_geometry(self) -> None: @@ -48,10 +59,10 @@ class ImportJob: raise SpeckleException( "geometry iterator failed to initialize for the given file" ) - + self.geometries_count = 0 while True: shape = cast(TriangulationElement, iterator.get()) - + self.geometries_count += 1 id = cast(int, shape.id) display_value = geometry_to_speckle(shape, self._render_material_manager)