fix(render): fix double-transform bug in USD mesh extraction causing wrong part positions
In _extract_mesh(), BRep_Tool.Triangulation_s(face, face_loc) returns a face_loc that already encodes the instance's full placement transform when a compound shape is tessellated with BRepMesh_IncrementalMesh. Applying shape_trsf on top doubled every rotation/translation, causing multiple roller elements to collapse to the same wrong world position (e.g. Z(-75°)×2 ≡ Z(+105°)×2 mod 360° → identical positions). Fix: use elif so shape_loc is only applied as a fallback when face_loc is identity. Adds seam edge extraction (UV seam primvar) and improves _traverse_xcaf doc. docs: learning erfasst - OCC face_loc double-transform in compound tessellation Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -273,14 +273,62 @@ def _extract_sharp_edge_pairs(shape, sharp_threshold_deg: float = 20.0) -> list:
|
||||
return sharp_pairs
|
||||
|
||||
|
||||
def _extract_seam_edge_pairs(shape) -> list:
|
||||
"""Extract seam edges (periodic-surface boundary edges) as segment pairs (mm, Z-up).
|
||||
|
||||
Seam edges are detected via BRep_Tool.IsClosed_s(edge) — edges that are
|
||||
topologically closed (start == end vertex). This includes the UV seams of
|
||||
periodic surfaces (cylinders, cones, spheres) but also full circles on flat
|
||||
faces and bore rims.
|
||||
TODO: Use ShapeAnalysis_Edge().IsSeam(edge, face) to restrict to true UV seams
|
||||
when UV-unwrapped texture mapping is needed (future phase).
|
||||
"""
|
||||
from OCP.BRep import BRep_Tool
|
||||
from OCP.TopExp import TopExp_Explorer
|
||||
from OCP.TopAbs import TopAbs_EDGE
|
||||
from OCP.BRepAdaptor import BRepAdaptor_Curve
|
||||
from OCP.GCPnts import GCPnts_UniformAbscissa
|
||||
|
||||
seam_pairs: list = []
|
||||
n_seam = 0
|
||||
exp = TopExp_Explorer(shape, TopAbs_EDGE)
|
||||
while exp.More():
|
||||
edge = exp.Current()
|
||||
exp.Next()
|
||||
if not BRep_Tool.IsClosed_s(edge):
|
||||
continue
|
||||
try:
|
||||
curve = BRepAdaptor_Curve(edge)
|
||||
# Use arc-length step (0.3 mm) matching the sharp edge sampler,
|
||||
# so segments are short enough for _world_to_index_pairs (tol=0.5 mm).
|
||||
sampler = GCPnts_UniformAbscissa()
|
||||
sampler.Initialize(curve, 0.3, 1e-6)
|
||||
if not sampler.IsDone() or sampler.NbPoints() < 2:
|
||||
continue
|
||||
pts = []
|
||||
for i in range(1, sampler.NbPoints() + 1):
|
||||
p = curve.Value(sampler.Parameter(i))
|
||||
pts.append([p.X(), p.Y(), p.Z()])
|
||||
for k in range(len(pts) - 1):
|
||||
seam_pairs.append([pts[k], pts[k + 1]])
|
||||
n_seam += 1
|
||||
except Exception:
|
||||
continue
|
||||
print(f"Seam edge extraction: {n_seam} seam edges, {len(seam_pairs)} segment pairs total")
|
||||
return seam_pairs
|
||||
|
||||
|
||||
# ── XCAF traversal ────────────────────────────────────────────────────────────
|
||||
|
||||
def _traverse_xcaf(shape_tool, color_tool, label, path_prefix, existing_keys, depth=0):
|
||||
"""Yield one dict per leaf shape in the XCAF hierarchy.
|
||||
|
||||
Phase 1 limitation: for deeply nested assemblies, transforms from
|
||||
intermediate reference labels are not composed — world-space positions
|
||||
may be off for non-flat assemblies. Single-level assemblies are correct.
|
||||
Transform composition: `GetShape_s(reference_label)` returns the shape with
|
||||
the reference's own location already composed in. For standard Schaeffler flat
|
||||
assemblies (1–2 levels deep) this is correct. Deeply nested sub-assembly
|
||||
transforms (3+ levels) accumulate naturally because each recursive call
|
||||
receives a component label from the *referred* definition, so each level's
|
||||
location is composed by the next GetShape_s call.
|
||||
"""
|
||||
from OCP.TDF import TDF_LabelSequence, TDF_Label
|
||||
from OCP.TDataStd import TDataStd_Name
|
||||
@@ -363,8 +411,13 @@ def _extract_mesh(shape) -> tuple[list, list]:
|
||||
for i in range(1, tri.NbNodes() + 1):
|
||||
node = tri.Node(i)
|
||||
if face_has_loc:
|
||||
# face_loc from BRep_Tool.Triangulation_s already encodes the
|
||||
# instance placement for compound-tessellated shapes — applying
|
||||
# shape_loc on top would double-transform every vertex.
|
||||
node = node.Transformed(face_loc.Transformation())
|
||||
if shape_has_loc:
|
||||
elif shape_has_loc:
|
||||
# Only fall back to shape_loc when face_loc is identity (e.g.
|
||||
# shapes tessellated individually rather than as a compound).
|
||||
node = node.Transformed(shape_trsf)
|
||||
vertices.append((node.X(), node.Y(), node.Z()))
|
||||
|
||||
@@ -488,6 +541,17 @@ def main() -> None:
|
||||
except Exception as exc:
|
||||
print(f"WARNING: sharp edge extraction failed (non-fatal): {exc}", file=sys.stderr)
|
||||
|
||||
# ── Seam edge pairs (world-space mm, Z-up) ────────────────────────────────
|
||||
seam_pairs_mm: list = []
|
||||
try:
|
||||
for i in range(1, free_labels.Length() + 1):
|
||||
root_shape = shape_tool.GetShape_s(free_labels.Value(i))
|
||||
if not root_shape.IsNull():
|
||||
seam_pairs_mm.extend(_extract_seam_edge_pairs(root_shape))
|
||||
print(f"Total seam segment pairs: {len(seam_pairs_mm)}")
|
||||
except Exception as exc:
|
||||
print(f"WARNING: seam edge extraction failed (non-fatal): {exc}", file=sys.stderr)
|
||||
|
||||
# ── Apply colors ──────────────────────────────────────────────────────────
|
||||
if color_map:
|
||||
try:
|
||||
@@ -583,17 +647,30 @@ def main() -> None:
|
||||
r, g, b = _hex_to_rgb01(hex_color)
|
||||
mesh.CreateDisplayColorAttr(Vt.Vec3fArray([Gf.Vec3f(r, g, b)]))
|
||||
|
||||
# ── Index-space sharp edge primvar ────────────────────────────
|
||||
# Lookup is in OCC Z-up space; sharp_pairs_mm are also Z-up — no swap needed.
|
||||
# ── Index-space sharp + seam edge primvars ───────────────────
|
||||
# Lookup is in OCC Z-up space; pairs are also Z-up — no swap needed.
|
||||
# Both `vertices` and `*_pairs_mm` are in OCC Z-up mm space with the
|
||||
# full per-shape location already applied — same coordinate frame required
|
||||
# by _world_to_index_pairs for the nearest-vertex lookup (tol=0.5 mm).
|
||||
primvars_api = UsdGeom.PrimvarsAPI(mesh)
|
||||
if sharp_pairs_mm:
|
||||
idx_pairs = _world_to_index_pairs(vertices, sharp_pairs_mm)
|
||||
if idx_pairs:
|
||||
pv = UsdGeom.PrimvarsAPI(mesh).CreatePrimvar(
|
||||
pv = primvars_api.CreatePrimvar(
|
||||
"schaeffler:sharpEdgeVertexPairs",
|
||||
Sdf.ValueTypeNames.Int2Array,
|
||||
UsdGeom.Tokens.constant,
|
||||
)
|
||||
pv.Set(Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in idx_pairs]))
|
||||
if seam_pairs_mm:
|
||||
seam_idx_pairs = _world_to_index_pairs(vertices, seam_pairs_mm)
|
||||
if seam_idx_pairs:
|
||||
pv_seam = primvars_api.CreatePrimvar(
|
||||
"schaeffler:seamEdgeVertexPairs",
|
||||
Sdf.ValueTypeNames.Int2Array,
|
||||
UsdGeom.Tokens.constant,
|
||||
)
|
||||
pv_seam.Set(Vt.Vec2iArray([Gf.Vec2i(a, b) for a, b in seam_idx_pairs]))
|
||||
|
||||
# ── Material placeholder + binding ────────────────────────────
|
||||
mat_name = _prim_name(source_name) if source_name else f"mat_{part_key}"
|
||||
|
||||
Reference in New Issue
Block a user