fix: GLB tessellation destroyed by BRepBuilderAPI_Transform + MergeFaces
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 <noreply@anthropic.com>
This commit is contained in:
@@ -95,8 +95,9 @@ def generate_gltf_geometry_task(self, cad_file_id: str):
|
|||||||
_cache_hit_asset_id = None
|
_cache_hit_asset_id = None
|
||||||
|
|
||||||
# Composite cache key includes deflection settings so changing them invalidates cache
|
# 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 = (
|
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
|
if _current_hash else None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -497,6 +497,56 @@ def _collect_part_key_map(shape_tool, free_labels) -> dict:
|
|||||||
return part_key_map
|
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("<I", data, 12)[0]
|
||||||
|
json_type = _struct.unpack_from("<I", data, 16)[0]
|
||||||
|
if json_type != 0x4E4F534A: # "JSON"
|
||||||
|
return
|
||||||
|
|
||||||
|
j = json.loads(data[20: 20 + json_len])
|
||||||
|
|
||||||
|
if "scenes" not in j or not j["scenes"]:
|
||||||
|
return
|
||||||
|
|
||||||
|
scene = j["scenes"][0]
|
||||||
|
old_roots = scene.get("nodes", [])
|
||||||
|
if not old_roots:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create a new root node with mm→m scale
|
||||||
|
nodes = j.setdefault("nodes", [])
|
||||||
|
new_root_idx = len(nodes)
|
||||||
|
nodes.append({
|
||||||
|
"name": "__mm_to_m_root__",
|
||||||
|
"scale": [0.001, 0.001, 0.001],
|
||||||
|
"children": old_roots,
|
||||||
|
})
|
||||||
|
scene["nodes"] = [new_root_idx]
|
||||||
|
|
||||||
|
new_json = json.dumps(j, separators=(",", ":"))
|
||||||
|
pad = (4 - len(new_json) % 4) % 4
|
||||||
|
new_json_bytes = new_json.encode() + b" " * pad
|
||||||
|
|
||||||
|
rest = data[20 + json_len:] # BIN chunk and anything after
|
||||||
|
new_chunk = _struct.pack("<II", len(new_json_bytes), 0x4E4F534A) + new_json_bytes
|
||||||
|
new_total = 12 + len(new_chunk) + len(rest)
|
||||||
|
new_header = _struct.pack("<III", 0x46546C67, 2, new_total)
|
||||||
|
glb_path.write_bytes(new_header + new_chunk + rest)
|
||||||
|
|
||||||
|
|
||||||
def _inject_glb_extras(glb_path: Path, extras: dict, part_key_map: dict | None = None) -> None:
|
def _inject_glb_extras(glb_path: Path, extras: dict, part_key_map: dict | None = None) -> None:
|
||||||
"""Patch a GLB binary to add/update scenes[0].extras JSON field.
|
"""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())
|
shape_tool = XCAFDoc_DocumentTool.ShapeTool_s(doc.Main())
|
||||||
color_tool = XCAFDoc_DocumentTool.ColorTool_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()
|
free_labels = TDF_LabelSequence()
|
||||||
shape_tool.GetFreeShapes(free_labels)
|
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)
|
# Collect partKeyMap before tessellation (XCAF names are stable at this point)
|
||||||
part_key_map = _collect_part_key_map(shape_tool, free_labels)
|
part_key_map = _collect_part_key_map(shape_tool, free_labels)
|
||||||
print(f"partKeyMap: {len(part_key_map)} unique part names collected")
|
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")
|
engine = getattr(args, "tessellation_engine", "occ")
|
||||||
if engine == "gmsh":
|
if engine == "gmsh":
|
||||||
# GMSH: tessellate each solid individually to cap peak RAM usage.
|
# 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)
|
# Step 2: GMSH override for SOLID shapes (better seam topology)
|
||||||
# Batch all eligible solids into a single compound and tessellate in one
|
_seen_shapes: list = []
|
||||||
# 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()
|
|
||||||
|
|
||||||
solids = []
|
solids = []
|
||||||
exp = _Explorer(root_shape, _SOLID)
|
exp = _Explorer(root_shape, _SOLID)
|
||||||
@@ -669,17 +721,10 @@ def main() -> None:
|
|||||||
|
|
||||||
eligible = []
|
eligible = []
|
||||||
for solid in solids:
|
for solid in solids:
|
||||||
# Skip REVERSED (mirrored) solids — keep BRepMesh tessellation.
|
|
||||||
# GMSH produces inverted-Jacobian meshes for negative-scale shapes.
|
|
||||||
if solid.Orientation() == _REVERSED:
|
if solid.Orientation() == _REVERSED:
|
||||||
continue
|
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):
|
if any(solid.IsSame(s) for s in _seen_shapes):
|
||||||
continue
|
continue
|
||||||
# Strip location: GMSH tessellates in definition space.
|
|
||||||
# The XCAF writer applies instance transforms at GLB export time.
|
|
||||||
eligible.append(solid.Located(_TopLoc_Location()))
|
eligible.append(solid.Located(_TopLoc_Location()))
|
||||||
_seen_shapes.append(solid)
|
_seen_shapes.append(solid)
|
||||||
|
|
||||||
@@ -705,9 +750,7 @@ def main() -> None:
|
|||||||
True, # isInParallel
|
True, # isInParallel
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- Extract sharp B-rep edge pairs (before mm→m scaling so coords are in mm) ---
|
# --- Extract sharp B-rep edge pairs (coords in mm, same as tessellation) ---
|
||||||
# Collect all free shapes into one list for the extraction function.
|
|
||||||
# The extraction uses the freshly tessellated XCAF shapes.
|
|
||||||
sharp_pairs: list = []
|
sharp_pairs: list = []
|
||||||
try:
|
try:
|
||||||
for i in range(1, free_labels.Length() + 1):
|
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)
|
_apply_palette_colors(shape_tool, color_tool, free_labels)
|
||||||
print("Applied palette colors (no color_map provided)")
|
print("Applied palette colors (no color_map provided)")
|
||||||
|
|
||||||
# --- Scale shapes mm → m before GLB export ---
|
# --- Export GLB via RWGltf_CafWriter (in mm, Z-up → Y-up handled by writer) ---
|
||||||
# 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 ---
|
|
||||||
from OCP.RWGltf import RWGltf_CafWriter
|
from OCP.RWGltf import RWGltf_CafWriter
|
||||||
|
|
||||||
writer = RWGltf_CafWriter(TCollection_AsciiString(args.output_path), True) # True = binary GLB
|
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
|
# Perform export
|
||||||
try:
|
try:
|
||||||
@@ -789,6 +817,9 @@ def main() -> None:
|
|||||||
except Exception as _exc:
|
except Exception as _exc:
|
||||||
print(f"WARNING: GLB extras injection failed (non-fatal): {_exc}", file=sys.stderr)
|
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:
|
try:
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user