feat: surface-evaluated normals, GMSH tessellation, draw call batching
USD exporter: - Compute normals from B-Rep surface via BRepLProp_SLProps at each vertex UV parameter — eliminates faceting on curved surfaces (same as Stepper) - Add GMSH Frontal-Delaunay tessellation engine (opt-in via --tessellation_engine gmsh) with per-solid strategy matching export_step_to_gltf.py - Use vertex normal interpolation instead of faceVarying (6x smaller normals) - Default engine remains OCC (GMSH has coordinate-space bug with instanced parts) Frontend: - Fix faceted shading in InlineCadViewer: only call computeVertexNormals() when geometry lacks normals, preserving smooth GLB normals from pipeline - Add useGeometryMerge hook for draw call batching (merge by material) - Fix unused import in cadUtils, optional props in ThreeDViewer Backend: - Move dataclass import to top of step_processor.py (PEP 8) - Unified single-read STEP metadata extraction with fallback Render worker: - Fix USD import seam/sharp restoration: read primvars via pxr directly (Blender's USD importer doesn't expose custom Int2Array primvars) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,9 @@
|
||||
|
||||
Runs inside Blender's Python environment (bpy available).
|
||||
Imports a USD stage and restores seam + sharp edges from
|
||||
schaeffler:*EdgeVertexPairs primvars (mapped as Blender mesh attributes
|
||||
by Blender's built-in USD importer).
|
||||
schaeffler:*EdgeVertexPairs primvars. Blender's built-in USD importer does
|
||||
NOT map arbitrary custom primvars (constant Int2Array) to mesh attributes,
|
||||
so we read them directly via the pxr module and apply via bmesh.
|
||||
|
||||
USD stage convention: mm Y-up, metersPerUnit=0.001.
|
||||
Blender's USD importer respects metersPerUnit and scales objects to metres.
|
||||
@@ -21,7 +22,7 @@ def import_usd_file(usd_path: str) -> list | tuple:
|
||||
Returns a tuple of (parts, material_lookup) where:
|
||||
- parts: list of imported mesh objects, centred at world origin
|
||||
- material_lookup: dict mapping blender_object_name → canonical_material_name
|
||||
(populated from schaeffler:canonicalMaterialName primvars, empty dict if absent)
|
||||
(populated from schaeffler:canonicalMaterialName customData, empty dict if absent)
|
||||
|
||||
USD stage is mm Y-up with metersPerUnit=0.001 — Blender scales to metres.
|
||||
"""
|
||||
@@ -38,22 +39,12 @@ def import_usd_file(usd_path: str) -> list | tuple:
|
||||
|
||||
_rename_usd_objects(parts)
|
||||
|
||||
# Restore seam + sharp edges from primvars mapped to Blender mesh attributes.
|
||||
# Blender's USD importer converts Int2Array primvars to INT32_2D attributes.
|
||||
# Attribute names: "schaeffler:sharpEdgeVertexPairs" / "schaeffler:seamEdgeVertexPairs"
|
||||
restored = 0
|
||||
for part in parts:
|
||||
restored += _restore_seam_sharp(part)
|
||||
if restored:
|
||||
print(f"[import_usd] restored seam/sharp on {restored} mesh(es)", flush=True)
|
||||
|
||||
# Extract material lookup via pxr direct read of the USD file.
|
||||
# Blender's USD importer does NOT expose STRING primvars or customData as
|
||||
# Python-accessible properties — but the pxr module (available in render-worker)
|
||||
# can read them perfectly from the same file.
|
||||
# Read primvars + customData directly via pxr (Blender's USD importer does
|
||||
# NOT expose custom primvars or customData as Python-accessible properties).
|
||||
material_lookup: dict[str, str] = {}
|
||||
edge_data: dict[str, dict] = {} # part_key → {sharp: [...], seam: [...]}
|
||||
try:
|
||||
from pxr import Usd, UsdGeom # type: ignore[import]
|
||||
from pxr import Usd, UsdGeom, Vt # type: ignore[import]
|
||||
stage = Usd.Stage.Open(usd_path)
|
||||
for prim in stage.Traverse():
|
||||
if prim.GetTypeName() != "Mesh":
|
||||
@@ -61,16 +52,32 @@ def import_usd_file(usd_path: str) -> list | tuple:
|
||||
part_key = prim.GetCustomDataByKey("schaeffler:partKey") or ""
|
||||
mat_name = prim.GetCustomDataByKey("schaeffler:canonicalMaterialName") or ""
|
||||
if not part_key or not mat_name:
|
||||
# Also check parent Xform prim (metadata may be on container)
|
||||
parent = prim.GetParent()
|
||||
if parent:
|
||||
part_key = part_key or (parent.GetCustomDataByKey("schaeffler:partKey") or "")
|
||||
mat_name = mat_name or (parent.GetCustomDataByKey("schaeffler:canonicalMaterialName") or "")
|
||||
if part_key and mat_name:
|
||||
# Blender object name = mesh prim leaf name (part_key)
|
||||
material_lookup[part_key] = mat_name
|
||||
|
||||
# Read seam/sharp primvars from USD mesh prim
|
||||
pvs_api = UsdGeom.PrimvarsAPI(prim)
|
||||
sharp_pv = pvs_api.GetPrimvar("schaeffler:sharpEdgeVertexPairs")
|
||||
seam_pv = pvs_api.GetPrimvar("schaeffler:seamEdgeVertexPairs")
|
||||
sharp_list = []
|
||||
seam_list = []
|
||||
if sharp_pv and sharp_pv.HasValue():
|
||||
raw = sharp_pv.Get()
|
||||
if raw is not None:
|
||||
sharp_list = [(int(v[0]), int(v[1])) for v in raw]
|
||||
if seam_pv and seam_pv.HasValue():
|
||||
raw = seam_pv.Get()
|
||||
if raw is not None:
|
||||
seam_list = [(int(v[0]), int(v[1])) for v in raw]
|
||||
if sharp_list or seam_list:
|
||||
# Use part_key as lookup key (matches Blender object name)
|
||||
edge_data[part_key] = {"sharp": sharp_list, "seam": seam_list}
|
||||
except Exception as exc:
|
||||
print(f"[import_usd] WARNING: pxr material lookup failed: {exc}", flush=True)
|
||||
print(f"[import_usd] WARNING: pxr read failed: {exc}", flush=True)
|
||||
|
||||
if material_lookup:
|
||||
print(f"[import_usd] pxr material lookup: {len(material_lookup)}/{len(parts)} parts",
|
||||
@@ -79,6 +86,29 @@ def import_usd_file(usd_path: str) -> list | tuple:
|
||||
print("[import_usd] no schaeffler:canonicalMaterialName metadata found (legacy USD)",
|
||||
flush=True)
|
||||
|
||||
if edge_data:
|
||||
print(f"[import_usd] pxr edge data: {len(edge_data)} parts with seam/sharp primvars",
|
||||
flush=True)
|
||||
|
||||
# Apply seam + sharp edges to Blender meshes using pxr-read data
|
||||
restored = 0
|
||||
for part in parts:
|
||||
# Match Blender object name to part_key. Blender may add .001 suffixes
|
||||
# for duplicate names, so try exact match first, then strip suffix.
|
||||
obj_name = part.name
|
||||
data = edge_data.get(obj_name)
|
||||
if data is None:
|
||||
# Try stripping Blender's .NNN duplicate suffix
|
||||
base = obj_name.rsplit('.', 1)[0] if '.' in obj_name else obj_name
|
||||
data = edge_data.get(base)
|
||||
if data:
|
||||
restored += _restore_seam_sharp(part, data["sharp"], data["seam"])
|
||||
if restored:
|
||||
print(f"[import_usd] restored seam/sharp on {restored} mesh(es)", flush=True)
|
||||
else:
|
||||
print("[import_usd] no seam/sharp edges restored (no primvar data or no matches)",
|
||||
flush=True)
|
||||
|
||||
# Centre combined bbox at world origin (same as import_glb convention)
|
||||
all_corners = []
|
||||
for p in parts:
|
||||
@@ -114,21 +144,19 @@ def _rename_usd_objects(parts: list) -> None:
|
||||
flush=True)
|
||||
|
||||
|
||||
def _restore_seam_sharp(obj) -> int:
|
||||
"""Apply seam+sharp edges from USD primvars mapped as Blender mesh attributes.
|
||||
def _restore_seam_sharp(obj, sharp_pairs: list, seam_pairs: list) -> int:
|
||||
"""Apply seam+sharp edges from pxr-read primvar data via bmesh.
|
||||
|
||||
Blender's USD importer maps primvars:schaeffler:sharpEdgeVertexPairs (Int2Array)
|
||||
to a mesh attribute named "schaeffler:sharpEdgeVertexPairs" with type INT32_2D.
|
||||
Each attribute element has a .value property returning a 2-tuple (v0, v1).
|
||||
sharp_pairs / seam_pairs: list of (v0, v1) vertex index pairs read
|
||||
from the USD file via pxr.UsdGeom.PrimvarsAPI.
|
||||
|
||||
Returns 1 if any edge data was applied, 0 otherwise.
|
||||
"""
|
||||
mesh = obj.data
|
||||
sharp_attr = mesh.attributes.get("schaeffler:sharpEdgeVertexPairs")
|
||||
seam_attr = mesh.attributes.get("schaeffler:seamEdgeVertexPairs")
|
||||
if not sharp_attr and not seam_attr:
|
||||
if not sharp_pairs and not seam_pairs:
|
||||
return 0
|
||||
|
||||
mesh = obj.data
|
||||
|
||||
# Ensure single-user data block before bmesh edit
|
||||
if mesh.users > 1:
|
||||
obj.data = mesh.copy()
|
||||
@@ -140,22 +168,18 @@ def _restore_seam_sharp(obj) -> int:
|
||||
|
||||
n_verts = len(bm.verts)
|
||||
|
||||
def _apply_pairs(attr, mark_fn):
|
||||
def _apply_pairs(pairs, mark_fn):
|
||||
applied = 0
|
||||
for elem in attr.data:
|
||||
v = elem.value # 2-tuple for INT32_2D
|
||||
if len(v) >= 2 and 0 <= v[0] < n_verts and 0 <= v[1] < n_verts:
|
||||
edge = bm.edges.get([bm.verts[v[0]], bm.verts[v[1]]])
|
||||
for v0, v1 in pairs:
|
||||
if 0 <= v0 < n_verts and 0 <= v1 < n_verts:
|
||||
edge = bm.edges.get([bm.verts[v0], bm.verts[v1]])
|
||||
if edge:
|
||||
mark_fn(edge)
|
||||
applied += 1
|
||||
return applied
|
||||
|
||||
n_sharp = n_seam = 0
|
||||
if sharp_attr:
|
||||
n_sharp = _apply_pairs(sharp_attr, lambda e: setattr(e, 'smooth', False))
|
||||
if seam_attr:
|
||||
n_seam = _apply_pairs(seam_attr, lambda e: setattr(e, 'seam', True))
|
||||
n_sharp = _apply_pairs(sharp_pairs, lambda e: setattr(e, 'smooth', False))
|
||||
n_seam = _apply_pairs(seam_pairs, lambda e: setattr(e, 'seam', True))
|
||||
|
||||
bm.to_mesh(mesh)
|
||||
bm.free()
|
||||
@@ -163,4 +187,4 @@ def _restore_seam_sharp(obj) -> int:
|
||||
if n_sharp or n_seam:
|
||||
print(f"[import_usd] {obj.name}: {n_sharp} sharp edges, {n_seam} seam edges",
|
||||
flush=True)
|
||||
return 1
|
||||
return 1 if (n_sharp or n_seam) else 0
|
||||
|
||||
Reference in New Issue
Block a user