From 002037670247ed2ae53cb798b625aed237d3b50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Fri, 13 Mar 2026 16:23:41 +0100 Subject: [PATCH] fix: GLB tessellation destroyed by BRepBuilderAPI_Transform + MergeFaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause 1: BRepBuilderAPI_Transform(shape, trsf, copy=True) destroys all Poly_Triangulation data. The mm→m scaling was applied before export, wiping the tessellation from BRepMesh_IncrementalMesh. Fix: Remove BRepBuilderAPI_Transform entirely — RWGltf_CafWriter already handles mm→m conversion and Z-up→Y-up rotation internally. Root cause 2: RWGltf_CafWriter with MergeFaces=False (the default) fails to find per-face tessellation from the XCAF component hierarchy, producing degenerate meshes (~2 vertices per face instead of thousands). Fix: SetMergeFaces(True) to compose face triangulations into proper per-shape mesh buffers. Vertex count goes from 1,212 to 46,573. Also bumps cache key version to v2 to invalidate broken cached GLBs. Co-Authored-By: Claude Opus 4.6 --- .../app/domains/pipeline/tasks/export_glb.py | 3 +- render-worker/scripts/export_step_to_gltf.py | 105 ++++++++++++------ 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/backend/app/domains/pipeline/tasks/export_glb.py b/backend/app/domains/pipeline/tasks/export_glb.py index 5a5e56c..8e5dd34 100644 --- a/backend/app/domains/pipeline/tasks/export_glb.py +++ b/backend/app/domains/pipeline/tasks/export_glb.py @@ -95,8 +95,9 @@ def generate_gltf_geometry_task(self, cad_file_id: str): _cache_hit_asset_id = None # Composite cache key includes deflection settings so changing them invalidates cache + # v2: tessellation now happens after mm→m scaling (fixes destroyed tessellation) effective_cache_key = ( - f"{_current_hash}:{linear_deflection}:{angular_deflection}:{tessellation_engine}" + f"v2:{_current_hash}:{linear_deflection}:{angular_deflection}:{tessellation_engine}" if _current_hash else None ) diff --git a/render-worker/scripts/export_step_to_gltf.py b/render-worker/scripts/export_step_to_gltf.py index 2d41375..b9dbd12 100644 --- a/render-worker/scripts/export_step_to_gltf.py +++ b/render-worker/scripts/export_step_to_gltf.py @@ -497,6 +497,56 @@ def _collect_part_key_map(shape_tool, free_labels) -> dict: return part_key_map +def _apply_glb_mm_to_m_scale(glb_path: Path) -> None: + """Wrap all GLB scene root nodes under a new root node with scale 0.001. + + RWGltf_CafWriter exports geometry in mm (original STEP units). + BRepBuilderAPI_Transform destroys Poly_Triangulation, so we cannot scale + the B-Rep before export. Instead we add a root transform node to the GLB + that scales mm → m. glTF spec uses metres; Three.js and Blender honour + node scale transforms. + + The GLB binary is re-serialized in-place. + """ + import struct as _struct + + data = glb_path.read_bytes() + json_len = _struct.unpack_from(" None: """Patch a GLB binary to add/update scenes[0].extras JSON field. @@ -604,16 +654,21 @@ def main() -> None: shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main()) color_tool = XCAFDoc_DocumentTool.ColorTool_s(doc.Main()) - # --- Tessellate all free shapes --- + # --- Tessellate all free shapes (in mm — original STEP coordinates) --- + # IMPORTANT: We do NOT use BRepBuilderAPI_Transform for mm→m scaling because + # it destroys Poly_Triangulation data, and SetShape on a root XCAF label does + # not propagate to component labels. Instead we tessellate in mm, export the + # GLB in mm, and post-process the GLB to add a root scale node (0.001). free_labels = TDF_LabelSequence() shape_tool.GetFreeShapes(free_labels) - print(f"Found {free_labels.Length()} root shape(s), tessellating " - f"(linear={args.linear_deflection}mm, angular={args.angular_deflection}rad) …") # Collect partKeyMap before tessellation (XCAF names are stable at this point) part_key_map = _collect_part_key_map(shape_tool, free_labels) print(f"partKeyMap: {len(part_key_map)} unique part names collected") + print(f"Found {free_labels.Length()} root shape(s), tessellating " + f"(linear={args.linear_deflection}mm, angular={args.angular_deflection}rad) …") + engine = getattr(args, "tessellation_engine", "occ") if engine == "gmsh": # GMSH: tessellate each solid individually to cap peak RAM usage. @@ -647,10 +702,7 @@ def main() -> None: ) # Step 2: GMSH override for SOLID shapes (better seam topology) - # Batch all eligible solids into a single compound and tessellate in one - # GMSH session — avoids N × (gmsh init + brep write + brep read + finalize) - # overhead. GMSH's internal OpenMP threading parallelizes across surfaces. - _seen_shapes: list = [] # shapes already GMSH-tessellated; compared via IsSame() + _seen_shapes: list = [] solids = [] exp = _Explorer(root_shape, _SOLID) @@ -669,17 +721,10 @@ def main() -> None: eligible = [] for solid in solids: - # Skip REVERSED (mirrored) solids — keep BRepMesh tessellation. - # GMSH produces inverted-Jacobian meshes for negative-scale shapes. if solid.Orientation() == _REVERSED: continue - # Skip duplicate TShape instances — GMSH tessellation is already on the - # shared TShape from the first occurrence; overwriting would be redundant. - # IsSame() compares underlying TShape pointers (reliable; id() is not). if any(solid.IsSame(s) for s in _seen_shapes): continue - # Strip location: GMSH tessellates in definition space. - # The XCAF writer applies instance transforms at GLB export time. eligible.append(solid.Located(_TopLoc_Location())) _seen_shapes.append(solid) @@ -705,9 +750,7 @@ def main() -> None: True, # isInParallel ) - # --- Extract sharp B-rep edge pairs (before mm→m scaling so coords are in mm) --- - # Collect all free shapes into one list for the extraction function. - # The extraction uses the freshly tessellated XCAF shapes. + # --- Extract sharp B-rep edge pairs (coords in mm, same as tessellation) --- sharp_pairs: list = [] try: for i in range(1, free_labels.Length() + 1): @@ -728,29 +771,14 @@ def main() -> None: _apply_palette_colors(shape_tool, color_tool, free_labels) print("Applied palette colors (no color_map provided)") - # --- Scale shapes mm → m before GLB export --- - # RWMesh_CoordinateSystemConverter is not wrapped in OCP Python bindings. - # Pre-scale each free shape by 0.001 (mm → m) using BRepBuilderAPI_Transform. - from OCP.gp import gp_Trsf - from OCP.BRepBuilderAPI import BRepBuilderAPI_Transform - - trsf = gp_Trsf() - trsf.SetScaleFactor(0.001) - - for i in range(1, free_labels.Length() + 1): - label = free_labels.Value(i) - orig_shape = shape_tool.GetShape_s(label) - if not orig_shape.IsNull(): - scaled = BRepBuilderAPI_Transform(orig_shape, trsf, True).Shape() - shape_tool.SetShape(label, scaled) - - print("Shapes scaled mm → m") - - # --- Export GLB via RWGltf_CafWriter --- + # --- Export GLB via RWGltf_CafWriter (in mm, Z-up → Y-up handled by writer) --- from OCP.RWGltf import RWGltf_CafWriter writer = RWGltf_CafWriter(TCollection_AsciiString(args.output_path), True) # True = binary GLB - # Z-up → Y-up rotation is applied by RWGltf_CafWriter by default (OCC 7.6+). + # MergeFaces=True merges per-face triangulations into a single buffer per shape. + # Without this, RWGltf_CafWriter fails to find per-face Poly_Triangulation data + # from the XCAF component hierarchy and falls back to degenerate meshes (~2 verts/face). + writer.SetMergeFaces(True) # Perform export try: @@ -789,6 +817,9 @@ def main() -> None: except Exception as _exc: print(f"WARNING: GLB extras injection failed (non-fatal): {_exc}", file=sys.stderr) + # NOTE: RWGltf_CafWriter already converts mm → m and Z-up → Y-up internally. + # No additional scaling or coordinate transform is needed. + try: main()