d938c4db1b
- 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>
180 lines
8.4 KiB
Markdown
180 lines
8.4 KiB
Markdown
# 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`):
|
||
```python
|
||
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:
|
||
```python
|
||
# 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:
|
||
```python
|
||
# 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**:
|
||
```bash
|
||
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()`:
|
||
|
||
```python
|
||
# 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
|
||
|
||
1. `assigned_names` uses `obj.name` (Blender-deduplicated, may include `.001` suffix) — the sentinel loop iterates the same `mesh_objects` list and checks `obj.name not in assigned_names`, so the comparison is consistent. ✓
|
||
2. `_assign_failed_material()` in `blender_render.py` does not include a `users > 1` copy guard — adding it in Task 2 is correct and consistent with the existing assignment branch.
|
||
3. If `FAILED_MATERIAL_NAME` was already appended as part of `needed` in `export_gltf.py` (e.g., if a part explicitly has `SCHAEFFLER_059999_FailedMaterial` in its material map), the `wm.append` call deduplicates automatically. ✓
|