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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user