diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b0e8cab..764b374 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -30,3 +30,4 @@ jobs: speckle_function_id: ${{ secrets.SPECKLE_FUNCTION_ID }} speckle_function_input_schema_file_path: ${{ env.FUNCTION_SCHEMA_FILE_NAME }} speckle_function_command: "python -u main.py run" + speckle_function_recommended_memory_mi: 10000 diff --git a/main.py b/main.py index 49dec44..4ee8339 100644 --- a/main.py +++ b/main.py @@ -178,7 +178,13 @@ def automate_function( ifc_filename = f"{file_name}_{timestamp}.ifc" ifc.write(ifc_filename) - automate_context.store_file_result(f"./{ifc_filename}") + print(f"\n💾 IFC file written: {ifc_filename}") + try: + automate_context.mark_run_success("Success! You can download the IFC file below.") + automate_context.store_file_result(f"./{ifc_filename}") + except Exception as e: + print(f" ⚠️ Could not upload file result (network issue?): {e}") + automate_context.mark_run_failed(f"Something went wrong when storing file result. Exception detail: {e}") print(f"\n{'=' * 60}") print(f" Export complete!") diff --git a/utils/geometry.py b/utils/geometry.py index ff05182..6a72828 100644 --- a/utils/geometry.py +++ b/utils/geometry.py @@ -10,6 +10,7 @@ # ============================================================================= import ifcopenshell +from collections import defaultdict from specklepy.objects.base import Base @@ -70,7 +71,6 @@ def _find_connected_components(snapped_faces: list) -> list: else: edge_to_face[edge] = fi - from collections import defaultdict groups: dict = defaultdict(list) for fi in range(n): groups[find(fi)].append(fi) @@ -384,15 +384,19 @@ def mesh_to_ifc( obj_scale = _resolve_scale(obj, scale) # ------------------------------------------------------------------ # - # Pass 1: collect all scaled vertices to compute world origin + # Pass 1: unpack vertices once per mesh, collect all scaled coords + # to compute world origin. Cache (verts, ms) for Pass 2. # ------------------------------------------------------------------ # + mesh_cache = [] # [(verts_list, ms)] or None per mesh all_scaled = [] for mesh in meshes: raw_verts = _get(mesh, "vertices") or [] verts = unwrap_chunks(list(raw_verts)) if not verts: + mesh_cache.append(None) continue ms = _resolve_scale(mesh, obj_scale) + mesh_cache.append((verts, ms)) for i in range(0, len(verts) - 2, 3): all_scaled.extend([ float(verts[i]) * ms, @@ -406,21 +410,20 @@ def mesh_to_ifc( ox, oy, oz = compute_origin(all_scaled) # ------------------------------------------------------------------ # - # Pass 2: one brep per mesh (so each can have its own material style) + # Pass 2: one brep per mesh — reuse cached verts, only unpack faces # ------------------------------------------------------------------ # brep_items = [] - for mesh in meshes: - raw_verts = _get(mesh, "vertices") or [] + for mesh, cached in zip(meshes, mesh_cache): + if cached is None: + continue + verts, ms = cached raw_faces = _get(mesh, "faces") or [] - verts = unwrap_chunks(list(raw_verts)) faces_raw = unwrap_chunks(list(raw_faces)) - if not verts or not faces_raw: + if not faces_raw: continue - ms = _resolve_scale(mesh, obj_scale) - try: face_groups = decode_faces(faces_raw) except Exception as e: diff --git a/utils/instances.py b/utils/instances.py index a962d2d..767e640 100644 --- a/utils/instances.py +++ b/utils/instances.py @@ -74,9 +74,8 @@ def build_definition_map(root: Base) -> dict: # Diagnostic: dump first 3 instanceDefinitionProxies to understand structure print("\n [PROXY DIAG] First 3 instanceDefinitionProxies from root:") - proxies_raw2 = _get(root, "instanceDefinitionProxies") - if proxies_raw2: - sample = proxies_raw2 if isinstance(proxies_raw2, list) else [proxies_raw2] + if proxies_raw: + sample = proxies_raw if isinstance(proxies_raw, list) else [proxies_raw] for i, proxy in enumerate(sample[:3]): app_id = _get(proxy, "applicationId") or "?" name = _get(proxy, "name") or "?" @@ -239,6 +238,10 @@ def _make_ifc_placement(ifc, tx, ty, tz, x_axis, z_axis): _stats = {"found": 0, "not_found": 0} _dbg_cnt = [0] +# Cache: mesh id → (verts_flat, faces_raw_flat, ms) to avoid re-unpacking +# the same definition mesh across many instances that share it. +_mesh_data_cache: dict = {} + _MM_SCALES = { "mm": 1.0, "millimeter": 1.0, "millimeters": 1.0, @@ -313,21 +316,28 @@ def instance_to_ifc(ifc, body_context, obj: Base, definition_map: dict, # One brep per mesh so each can have its own material style brep_items = [] for mesh in meshes: - raw_verts = _get(mesh, "vertices") or [] - raw_faces = _get(mesh, "faces") or [] - verts = unwrap_chunks(list(raw_verts)) - faces_raw = unwrap_chunks(list(raw_faces)) - if not verts or not faces_raw: - continue + mesh_id = _get(mesh, "id") or _get(mesh, "applicationId") + if mesh_id and mesh_id in _mesh_data_cache: + verts, face_groups, ms = _mesh_data_cache[mesh_id] + else: + raw_verts = _get(mesh, "vertices") or [] + raw_faces = _get(mesh, "faces") or [] + verts = unwrap_chunks(list(raw_verts)) + faces_raw = unwrap_chunks(list(raw_faces)) + if not verts or not faces_raw: + continue - mesh_units = _get(mesh, "units") or _get(mesh, "_units") or ("m" if ifc_format else "mm") - ms = _MM_SCALES.get(mesh_units.lower().strip(), 1.0) + mesh_units = _get(mesh, "units") or _get(mesh, "_units") or ("m" if ifc_format else "mm") + ms = _MM_SCALES.get(mesh_units.lower().strip(), 1.0) - try: - face_groups = decode_faces(faces_raw) - except Exception as e: - print(f" ⚠️ Instance face decode: {e}") - continue + try: + face_groups = decode_faces(faces_raw) + except Exception as e: + print(f" ⚠️ Instance face decode: {e}") + continue + + if mesh_id: + _mesh_data_cache[mesh_id] = (verts, face_groups, ms) # Pre-compute world coords for all vertices in this mesh verts_world = []