fix(render): production GLB sharp edges + materials (25/25)

Sharp edges:
- OCC→Blender coordinate transform was wrong: Blender(X,Y,Z) = OCC(X×0.001, -Z×0.001, Y×0.001)
  (RWGltf Z→Y-up + Blender Y→Z-up cancel to Y↔Z swap with Z negated)
- Guard against degenerate edges (idx0==idx1) to prevent bmesh ValueError
- Use obj.matrix_world @ v.co for world-space KD-tree (assembly node transforms)

Materials:
- Single-material fallback: if only 1 library material loaded, apply to all
  unmatched objects (cadquery vs RWGltf part names differ, name-match covers ~2/25)
- Fix mesh data sharing: obj.data.copy() before materials.clear() to avoid
  clearing shared data blocks
- Use obj.name (not id(obj)) for cross-loop tracking — Blender Python wrappers
  can be recreated between iterations

Both fixes applied to export_gltf.py (production GLB) and blender_render.py (thumbnails).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 10:29:08 +01:00
parent 8933d0be17
commit a1d140d30f
3 changed files with 556 additions and 491 deletions
+82 -2
View File
@@ -256,6 +256,73 @@ def _mark_sharp_and_seams(obj, smooth_angle_deg: float, sharp_edge_midpoints=Non
bpy.ops.object.mode_set(mode='OBJECT')
def _apply_sharp_edges_from_occ(parts, sharp_edge_pairs):
"""Mark edges sharp using OCC-derived vertex-pair data.
`sharp_edge_pairs` is a list of [[x0,y0,z0],[x1,y1,z1]] in mm.
Blender mesh coordinates are in metres (STEP mm * 0.001 scale applied).
We match each OCC vertex pair against bmesh vertex positions with a 0.5 mm
tolerance (0.0005 m) and mark the matched edge as sharp.
"""
if not sharp_edge_pairs:
return
import bmesh
import mathutils
SCALE = 0.001 # mm → m
TOL = 0.0005 # 0.5 mm in metres
# OCC STEP space (Z-up, mm) → Blender (Z-up, m):
# RWGltf applies Z→Y-up, Blender import applies Y→Z-up.
# Net: Blender(X, Y, Z) = OCC(X*0.001, -Z*0.001, Y*0.001)
occ_pairs = []
for pair in sharp_edge_pairs:
v0 = mathutils.Vector((pair[0][0] * SCALE, -pair[0][2] * SCALE, pair[0][1] * SCALE))
v1 = mathutils.Vector((pair[1][0] * SCALE, -pair[1][2] * SCALE, pair[1][1] * SCALE))
occ_pairs.append((v0, v1))
marked_total = 0
for obj in parts:
bm = bmesh.new()
bm.from_mesh(obj.data)
bm.verts.ensure_lookup_table()
bm.edges.ensure_lookup_table()
# Build KD-tree on vertices in WORLD space — OCC pairs are world coords,
# but mesh vertices are in local space (assembly node transform in GLB).
world_mat = obj.matrix_world
kd = mathutils.kdtree.KDTree(len(bm.verts))
for v in bm.verts:
kd.insert(world_mat @ v.co, v.index)
kd.balance()
marked = 0
for v0_occ, v1_occ in occ_pairs:
# Find closest Blender vertex to each OCC endpoint
_co0, idx0, dist0 = kd.find(v0_occ)
_co1, idx1, dist1 = kd.find(v1_occ)
if dist0 > TOL or dist1 > TOL:
continue
if idx0 == idx1:
continue # degenerate — both endpoints map to same vertex
# Find the edge shared by these two vertices
bv0 = bm.verts[idx0]
bv1 = bm.verts[idx1]
edge = bm.edges.get((bv0, bv1))
if edge is None:
edge = bm.edges.get((bv1, bv0))
if edge is not None and edge.smooth:
edge.smooth = False
marked += 1
bm.to_mesh(obj.data)
bm.free()
marked_total += marked
print(f"[blender_render] OCC sharp edges applied: {marked_total} edges marked across {len(parts)} parts", flush=True)
def _import_glb(glb_file):
"""Import OCC-generated GLB into Blender.
@@ -288,8 +355,14 @@ def _import_glb(glb_file):
max(v.y for v in all_corners),
max(v.z for v in all_corners)))
center = (mins + maxs) * 0.5
for p in parts:
p.location -= center
# Move root objects (parentless) to centre. Adjusting a child's local
# .location by a world-space vector gives wrong results when the GLB has
# Empty parent nodes (OCC assembly hierarchy). Shifting the root moves
# the entire hierarchy correctly.
all_imported = list(bpy.context.selected_objects)
root_objects = [o for o in all_imported if o.parent is None]
for obj in root_objects:
obj.location -= center
return parts
@@ -479,6 +552,10 @@ if use_template:
# selected object in a single C call — same effect as calling per-object
# but ~100× faster (0.2s vs 16s for 175 parts).
_apply_smooth_batch(parts, smooth_angle)
# If OCC extracted sharp edge vertex pairs, mark them explicitly.
_occ_pairs = _mesh_attrs.get("sharp_edge_pairs") or []
if _occ_pairs:
_apply_sharp_edges_from_occ(parts, _occ_pairs)
_lap("smooth_shading")
# Material assignment: library materials if available, otherwise palette
@@ -571,6 +648,9 @@ else:
import time as _time
_t_smooth_a = _time.time()
_apply_smooth_batch(parts, smooth_angle)
_occ_pairs = _mesh_attrs.get("sharp_edge_pairs") or []
if _occ_pairs:
_apply_sharp_edges_from_occ(parts, _occ_pairs)
for part in parts:
_assign_failed_material(part)
print(f"[blender_render] smooth+fallback-material: {len(parts)} parts ({_time.time()-_t_smooth_a:.2f}s)", flush=True)