various changes (#4)

This commit is contained in:
Jedd Morgan
2025-07-01 12:22:14 +01:00
committed by GitHub
parent b32d3c3805
commit 897235a47d
5 changed files with 115 additions and 29 deletions
+1
View File
@@ -115,3 +115,4 @@ venv.bak/
# other # other
scratch.py scratch.py
settings.json settings.json
*.prof
+75 -19
View File
@@ -1,45 +1,94 @@
import json
import time 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.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.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 specklepy.transports.server import ServerTransport
from speckleifc.ifc_geometry_processing import open_ifc from speckleifc.ifc_geometry_processing import open_ifc
from speckleifc.importer import ImportJob from speckleifc.importer import ImportJob
###
# TODO: tomorrow, either we optimise n-gon PR, or we throw it away. def cmd_line_import() -> None:
# I'm hoping that POLYGONS_WITHOUT_HOLES is faster than TRIANGLES
# but nothing concretely confirmed parser = ArgumentParser(
# tldr: send is not much slower in py from C# prog="speckleifc",
# geometry iterator is pretty slow 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" PROJECT_ID = "f3a42bdf24"
MODEL_ID = "0e23cfdea3" MODEL_ID = "0e23cfdea3"
SERVER_URL = "app.speckle.systems" 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\\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
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) import_job = ImportJob(ifc_file)
data = import_job.convert() data = import_job.convert()
print(f"File conversion complete after {(time.time() - start) * 1000}ms") print(f"File conversion complete after {(time.time() - start) * 1000}ms")
start = time.time() 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) root_id = send(data, transports=[remote_transport], use_default_cache=False)
print(f"Sending to speckle complete after: {(time.time() - start) * 1000}ms") print(f"Sending to speckle complete after: {(time.time() - start) * 1000}ms")
@@ -49,15 +98,22 @@ def main() -> Version:
client.authenticate_with_account(account) client.authenticate_with_account(account)
create_version = CreateVersionInput( 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) 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 return version
if __name__ == "__main__": if __name__ == "__main__":
start = time.time() start = time.time()
main() cmd_line_import()
print(f"Total time: {(time.time() - start) * 1000}ms") print(f"Total time (including cleanup): {(time.time() - start) * 1000}ms")
+12 -4
View File
@@ -25,6 +25,13 @@ def geometry_to_speckle(
material_ids = cast(Sequence[int], geometry.material_ids) material_ids = cast(Sequence[int], geometry.material_ids)
faces = cast(Sequence[int], geometry.faces) faces = cast(Sequence[int], geometry.faces)
verts = cast(Sequence[float], geometry.verts) 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) mapped_meshes = _pre_alloc_mesh_lists(shape, material_ids, MESH_COUNT)
for i, mesh in enumerate(mapped_meshes): for i, mesh in enumerate(mapped_meshes):
@@ -35,10 +42,6 @@ def geometry_to_speckle(
mapped_vertices_pointers = [0] * MESH_COUNT mapped_vertices_pointers = [0] * MESH_COUNT
mapped_index_counters = [0] * MESH_COUNT mapped_index_counters = [0] * MESH_COUNT
FACE_COUNT = len(material_ids)
assert len(faces) == FACE_COUNT * 3
i = 0 i = 0
face_index = 0 face_index = 0
while i < FACE_COUNT: 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 + 1] = verts[vert_index + 1]
mesh.vertices[mapped_vert_offset + 2] = verts[vert_index + 2] 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 i += 1
face_index += 3 # number of items in the faces list we just jumped over 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( mesh = Mesh(
units="m", units="m",
vertices=[-1] * (face_count * 9), vertices=[-1] * (face_count * 9),
vertexNormals=[-1] * (face_count * 9),
faces=[-1] * (face_count * 4), # 1 marker + 3 vertex indices faces=[-1] * (face_count * 4), # 1 marker + 3 vertex indices
applicationId=f"{appId}_mat{mat_id}", applicationId=f"{appId}_mat{mat_id}",
) )
+13 -3
View File
@@ -13,6 +13,18 @@ def _create_iterator_settings() -> settings:
ifc_settings.set("weld-vertices", False) ifc_settings.set("weld-vertices", False)
# Speckle meshes are all in world coords # Speckle meshes are all in world coords
ifc_settings.set("use-world-coords", True) 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 return ifc_settings
@@ -27,6 +39,4 @@ def open_ifc(file_path: str) -> file:
def create_geometry_iterator(ifc_file: file | sqlite) -> iterator: def create_geometry_iterator(ifc_file: file | sqlite) -> iterator:
return iterator( return iterator(_create_iterator_settings(), ifc_file, multiprocessing.cpu_count())
_create_iterator_settings(), ifc_file, multiprocessing.cpu_count() // 2
)
+14 -3
View File
@@ -1,3 +1,4 @@
import time
from typing import cast from typing import cast
from ifcopenshell.entity_instance import entity_instance from ifcopenshell.entity_instance import entity_instance
@@ -20,11 +21,16 @@ class ImportJob:
self._ifc_file = ifc_file self._ifc_file = ifc_file
self.cached_display_values: dict[int, list[Base]] = {} self.cached_display_values: dict[int, list[Base]] = {}
self._render_material_manager = RenderMaterialProxyManager() self._render_material_manager = RenderMaterialProxyManager()
self.geometries_count = 0
self.geometries_used = 0
def convert_element(self, step_element: entity_instance) -> Base: def convert_element(self, step_element: entity_instance) -> Base:
children = self._convert_children(step_element) children = self._convert_children(step_element)
display_value = self.cached_display_values.get(step_element.id(), []) 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"): if step_element.is_a("IfcProject"):
return project_to_speckle(step_element, children) return project_to_speckle(step_element, children)
elif step_element.is_a("IfcSpatialStructureElement"): elif step_element.is_a("IfcSpatialStructureElement"):
@@ -36,10 +42,15 @@ class ImportJob:
return [self.convert_element(i) for i in get_children(step_element)] return [self.convert_element(i) for i in get_children(step_element)]
def convert(self) -> Base: def convert(self) -> Base:
start = time.time()
self.pre_process_geometry() 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() 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 return root
def pre_process_geometry(self) -> None: def pre_process_geometry(self) -> None:
@@ -48,10 +59,10 @@ class ImportJob:
raise SpeckleException( raise SpeckleException(
"geometry iterator failed to initialize for the given file" "geometry iterator failed to initialize for the given file"
) )
self.geometries_count = 0
while True: while True:
shape = cast(TriangulationElement, iterator.get()) shape = cast(TriangulationElement, iterator.get())
self.geometries_count += 1
id = cast(int, shape.id) id = cast(int, shape.id)
display_value = geometry_to_speckle(shape, self._render_material_manager) display_value = geometry_to_speckle(shape, self._render_material_manager)