Files
HartOMat/plan.md
T
Hartmut d938c4db1b fix(materials): universal FailedMaterial sentinel for unmatched mesh objects
- 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>
2026-03-11 21:49:37 +01:00

180 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 ~275291) 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 151166). 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. ✓