- export_gltf.py: replace single-material fallback (only fired when len(appended)==1) with a universal sentinel that appends SCHAEFFLER_059999_FailedMaterial unconditionally and assigns it to every mesh object not matched by name-based lookup. Also adds in-memory magenta fallback if library append fails. Removes 2 temporary [DEBUG] print lines from investigation. - blender_render.py: add FailedMaterial assignment inside _apply_material_library() for unmatched parts (was log-only before). Includes copy-on-write guard (users > 1) matching existing pattern. Also added alias 'Stahl; Durotect CMT' (semicolon) → Durotect-Blue to cover STEP files using semicolon separator instead of comma. Verified: 23/25 objects matched correctly, 2 ISO8734 dowel pins (empty material) receive SCHAEFFLER_059999_FailedMaterial as sentinel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
8.4 KiB
Plan: FailedMaterial Sentinel for Unmatched Mesh Objects
Context
23/25 mesh objects in production GLB exports receive correct Schaeffler library materials. 2 ISO8734 dowel pins carry an empty material string → mat_map_lower.get() returns None → they fall through the entire matching block and keep their OCC palette color in the exported GLB.
The old single-material fallback in export_gltf.py (lines ~275–291) fires only when exactly 1 material is appended — it never fires for multi-material assemblies. blender_render.py logs unmatched parts but assigns nothing.
Fix: append SCHAEFFLER_059999_FailedMaterial unconditionally as a sentinel, then assign it to every unmatched mesh object in both scripts. Also remove 2 temporary [DEBUG] print lines left from investigation.
Affected Files
| File | Change |
|---|---|
render-worker/scripts/export_gltf.py |
Remove 2 DEBUG prints; add FAILED_MATERIAL_NAME constant; replace single-material fallback with universal sentinel |
render-worker/scripts/blender_render.py |
Add FailedMaterial assignment loop at end of _apply_material_library() |
Tasks (in order)
[x] Task 1: export_gltf.py — Remove DEBUG prints + add universal FailedMaterial sentinel
File: render-worker/scripts/export_gltf.py
Step 1a — Add FAILED_MATERIAL_NAME constant after the imports (near top of file, after import traceback):
FAILED_MATERIAL_NAME = "SCHAEFFLER_059999_FailedMaterial"
Step 1b — In the material assignment loop, remove the two [DEBUG] print lines and replace with a pass comment:
# BEFORE:
assigned += 1
assigned_names.add(obj.name)
print(f"[DEBUG] assigned '{mat_name}' → '{obj.name}' (lookup_key='{lower_base}')")
else:
print(f"[DEBUG] NO MATCH for obj='{obj.name}' lower_base='{lower_base}' mat_name={mat_name!r} in_appended={mat_name in appended if mat_name else False}")
# AFTER:
assigned += 1
assigned_names.add(obj.name)
else:
pass # unmatched → will receive FailedMaterial sentinel below
Step 1c — Replace the single-material fallback block (after print(f"Material substitution: ...")) with the universal sentinel:
# BEFORE (single-material fallback, only fires when len(appended)==1):
# Single-material fallback: if only one library material was loaded, ...
if len(appended) == 1:
default_mat_name, default_mat = next(iter(appended.items()))
if default_mat:
fallback = 0
for obj in mesh_objects:
if obj.name not in assigned_names:
if obj.data.users > 1:
obj.data = obj.data.copy()
obj.data.materials.clear()
obj.data.materials.append(default_mat)
fallback += 1
if fallback:
print(f"Single-material fallback: applied '{default_mat_name}' to {fallback} unmatched objects")
# AFTER (universal sentinel — fires regardless of how many materials were appended):
# Universal FailedMaterial sentinel: assign SCHAEFFLER_059999_FailedMaterial
# to every mesh object that was not matched by name-based lookup above.
failed_mat = None
try:
bpy.ops.wm.append(
filepath=f"{args.asset_library_blend}/Material/{FAILED_MATERIAL_NAME}",
directory=f"{args.asset_library_blend}/Material/",
filename=FAILED_MATERIAL_NAME,
link=False,
)
if FAILED_MATERIAL_NAME in bpy.data.materials:
failed_mat = bpy.data.materials[FAILED_MATERIAL_NAME]
print(f"Appended sentinel material: {FAILED_MATERIAL_NAME}")
else:
print(f"WARNING: sentinel '{FAILED_MATERIAL_NAME}' not in library — "
f"creating in-memory magenta fallback", file=sys.stderr)
except Exception as exc:
print(f"WARNING: failed to append sentinel '{FAILED_MATERIAL_NAME}': {exc}",
file=sys.stderr)
if failed_mat is None:
# Library append failed: create in-memory magenta so export is never silently wrong
failed_mat = bpy.data.materials.new(name=FAILED_MATERIAL_NAME)
failed_mat.use_nodes = True
bsdf = failed_mat.node_tree.nodes.get("Principled BSDF")
if bsdf:
bsdf.inputs["Base Color"].default_value = (1.0, 0.0, 1.0, 1.0) # magenta
fallback_count = 0
for obj in mesh_objects:
if obj.name not in assigned_names:
if obj.data.users > 1:
obj.data = obj.data.copy()
obj.data.materials.clear()
obj.data.materials.append(failed_mat)
fallback_count += 1
if fallback_count:
print(f"FailedMaterial sentinel: assigned '{FAILED_MATERIAL_NAME}' "
f"to {fallback_count} unmatched objects")
Acceptance gate:
grep -n "\[DEBUG\]" render-worker/scripts/export_gltf.py # must return nothing
grep -n "FAILED_MATERIAL_NAME" render-worker/scripts/export_gltf.py # must show constant + usage
After deploying and running a production GLB export:
- Log shows
FailedMaterial sentinel: assigned 'SCHAEFFLER_059999_FailedMaterial' to 2 unmatched objects - No
[DEBUG]lines in logs
Dependencies: none
[x] Task 2: blender_render.py — Add FailedMaterial fallback inside _apply_material_library()
File: render-worker/scripts/blender_render.py
At the end of _apply_material_library(), replace the logging-only unmatched block with one that also calls _assign_failed_material():
# BEFORE (end of _apply_material_library, lines ~483-485):
print(f"[blender_render] material assignment: {assigned_count}/{len(parts)} parts matched", flush=True)
if unmatched_names:
print(f"[blender_render] unmatched parts (palette fallback): {unmatched_names[:10]}", flush=True)
# AFTER:
print(f"[blender_render] material assignment: {assigned_count}/{len(parts)} parts matched", flush=True)
if unmatched_names:
print(f"[blender_render] unmatched parts → assigning {FAILED_MATERIAL_NAME}: {unmatched_names[:10]}", flush=True)
unmatched_set = set(unmatched_names)
for part in parts:
if part.name in unmatched_set:
if part.data.users > 1:
part.data = part.data.copy()
_assign_failed_material(part)
Note: _assign_failed_material() and FAILED_MATERIAL_NAME already exist in blender_render.py (line 31 and lines 151–166). No new imports needed.
Acceptance gate: Trigger a thumbnail render with a material_map that leaves one or more parts unmatched. Render log must include:
[blender_render] unmatched parts → assigning SCHAEFFLER_059999_FailedMaterial: [...]
Dependencies: none (independent of Task 1)
Migration Check
No migration required. Two render-worker scripts only. No DB, no backend, no frontend.
Order Recommendation
Tasks 1 and 2 are independent. Implement both in the same session, then:
docker compose cp render-worker/scripts/export_gltf.py render-worker:/render-scripts/export_gltf.py
docker compose cp render-worker/scripts/blender_render.py render-worker:/render-scripts/blender_render.py
→ trigger production GLB re-generation → verify sentinel fires for ISO8734 parts
Risks / Open Questions
assigned_namesusesobj.name(Blender-deduplicated, may include.001suffix) — the sentinel loop iterates the samemesh_objectslist and checksobj.name not in assigned_names, so the comparison is consistent. ✓_assign_failed_material()inblender_render.pydoes not include ausers > 1copy guard — adding it in Task 2 is correct and consistent with the existing assignment branch.- If
FAILED_MATERIAL_NAMEwas already appended as part ofneededinexport_gltf.py(e.g., if a part explicitly hasSCHAEFFLER_059999_FailedMaterialin its material map), thewm.appendcall deduplicates automatically. ✓