refactor(P1): complete pipeline cleanup — M1 dead code + M3 blender split

M1 dead code removal:
- admin.py: remove VALID_STL_QUALITIES + stl_quality (7 locations)
- frontend: remove stl_quality from 6 files (api/orders.ts, api/worker.ts,
  WorkerActivity.tsx, RenderInfoModal.tsx, helpTexts.ts, mocks/handlers.ts)
- blender_render.py: delete _mark_sharp_and_seams() — dead, never called (62 lines)
- step_processor.py: delete _render_via_service() + 2 elif renderer=="threejs" branches
- renderproblems_tmp/: remove 3 orphaned debug images

M3 blender_render.py decomposition (858 → 248 lines):
- _blender_gpu.py: activate_gpu(), configure_engine()
- _blender_import.py: import_glb(), apply_rotation()
- _blender_materials.py: FAILED_MATERIAL_NAME, assign_failed_material(),
  build_mat_map_lower(), apply_material_library()
- _blender_camera.py: setup_auto_camera(), setup_auto_lights()
- _blender_scene.py: ensure_collection(), apply_smooth_batch(),
  apply_sharp_edges_from_occ(), setup_shadow_catcher()
- Entry-point: sys.path.insert for submodule discovery; arg-parse + Mode A/B orchestration only

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 22:19:59 +01:00
parent 4f0fe2c8c7
commit 393e4b92a7
19 changed files with 876 additions and 1036 deletions
+139 -135
View File
@@ -1,12 +1,11 @@
# Plan: FailedMaterial Sentinel for Unmatched Mesh Objects
# Plan: Priority 1 — Pipeline Cleanup (M1 Dead Code + M3 blender_render Split)
## 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.
ROADMAP Priority 1 is "In Progress". M2 (`step_tasks.py` decomposed to `domains/pipeline/tasks/`) is **done**. Two milestones remain:
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.
- **M1**: Delete dead-code directories, remove `stl_quality` from admin/frontend surface, remove dead functions
- **M3**: Decompose `blender_render.py` (920 lines) into focused submodules
---
@@ -14,166 +13,171 @@ Fix: append `SCHAEFFLER_059999_FailedMaterial` unconditionally as a sentinel, th
| 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()` |
| `blender-renderer/` | DELETE directory |
| `threejs-renderer/` | DELETE directory |
| `renderproblems_tmp/` | DELETE directory |
| `backend/app/api/routers/admin.py` | Remove `stl_quality` + `VALID_STL_QUALITIES` (7 locations) |
| `frontend/src/api/orders.ts` | Remove `stl_quality?: string` |
| `frontend/src/api/worker.ts` | Remove `stl_quality?: string` |
| `frontend/src/pages/WorkerActivity.tsx` | Remove STL quality KV row |
| `frontend/src/components/renders/RenderInfoModal.tsx` | Remove STL quality display row |
| `frontend/src/help/helpTexts.ts` | Remove `setting.stl_quality` entry |
| `backend/app/services/step_processor.py` | Remove `_render_via_service()` + dead `elif renderer == "threejs"` |
| `render-worker/scripts/blender_render.py` | Remove `_mark_sharp_and_seams()`; thin to entry-point after submodule extraction |
| `render-worker/scripts/_blender_gpu.py` | CREATE — `activate_gpu()` |
| `render-worker/scripts/_blender_import.py` | CREATE — `import_glb()`, `apply_rotation()` |
| `render-worker/scripts/_blender_materials.py` | CREATE — `build_mat_map_lower()`, `apply_material_library()`, `assign_failed_material()` |
| `render-worker/scripts/_blender_camera.py` | CREATE — `setup_auto_camera()`, `setup_auto_lights()` |
| `render-worker/scripts/_blender_scene.py` | CREATE — `ensure_collection()`, `apply_smooth_batch()`, `apply_sharp_edges_from_occ()`, `setup_shadow_catcher()` |
---
## Tasks (in order)
### [x] Task 1: export_gltf.py — Remove DEBUG prints + add universal FailedMaterial sentinel
### [x] Task M1-1: Delete obsolete directories
**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
- **What**: `rm -rf blender-renderer/ threejs-renderer/ renderproblems_tmp/`
- **Acceptance gate**: `ls blender-renderer/ threejs-renderer/ renderproblems_tmp/` → all "No such file"
- **Dependencies**: none
- **Risk**: Zero — no active source files
---
### [x] Task 2: blender_render.py — Add FailedMaterial fallback inside `_apply_material_library()`
### [x] Task M1-2: Remove stl_quality from admin.py
**File**: `render-worker/scripts/blender_render.py`
- **File**: `backend/app/api/routers/admin.py`
- **What**: Delete all 7 references:
1. `VALID_STL_QUALITIES = {"low", "high"}` constant
2. `"stl_quality": "low"` from `SETTINGS_DEFAULTS`
3. `stl_quality: str = "low"` from `SettingsOut`
4. `stl_quality: str | None = None` from `SettingsUpdate`
5. `stl_quality=raw["stl_quality"],` from `_settings_to_out()`
6. `if body.stl_quality is not None and body.stl_quality not in VALID_STL_QUALITIES:` validation block
7. `if body.stl_quality is not None: updates["stl_quality"] = body.stl_quality` update block
- **Acceptance gate**: `grep -n "stl_quality\|VALID_STL_QUALITIES" backend/app/api/routers/admin.py` → 0 matches
- **Dependencies**: none
- **Risk**: Low — the DB key remains (harmless); pipeline internally still uses `gltf_*_linear_deflection`
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)
### [x] Task M1-3: Remove stl_quality from frontend
# 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)
```
- **Files**:
- `frontend/src/api/orders.ts` — remove `stl_quality?: string`
- `frontend/src/api/worker.ts` — remove `stl_quality?: string`
- `frontend/src/pages/WorkerActivity.tsx` — remove STL quality KV row
- `frontend/src/components/renders/RenderInfoModal.tsx` — remove STL quality row
- `frontend/src/help/helpTexts.ts` — remove `setting.stl_quality` entry
- **Acceptance gate**: `grep -rn "stl_quality" frontend/src/` → 0 matches; `npx tsc --noEmit` passes
- **Dependencies**: M1-2
- **Risk**: Low — all uses are optional fields (`?:`)
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: [...]
```
### [x] Task M1-4: Remove dead _mark_sharp_and_seams from blender_render.py
**Dependencies**: none (independent of Task 1)
- **File**: `render-worker/scripts/blender_render.py`
- **What**: Delete the `_mark_sharp_and_seams()` function (lines 196256 approx). It is defined but never called — `_apply_sharp_edges_from_occ()` is the active implementation.
- **Acceptance gate**: `grep -n "_mark_sharp_and_seams" render-worker/scripts/blender_render.py` → 0 matches
- **Dependencies**: none
- **Risk**: Zero — verifiably never called
---
### [x] Task M1-5: Remove dead code from step_processor.py
- **File**: `backend/app/services/step_processor.py`
- **What**: Delete `_render_via_service()` function and the `elif renderer == "threejs":` branch (which only logs a warning and falls through)
- **Acceptance gate**: `grep -n "_render_via_service\|renderer == .threejs" backend/app/services/step_processor.py` → 0 matches
- **Dependencies**: M1-1
- **Risk**: Low — function is only referenced from within the dead branch
---
### [x] Task M3-1: Create _blender_gpu.py
- **File**: `render-worker/scripts/_blender_gpu.py` (NEW)
- **What**: Extract `_activate_gpu()` from `blender_render.py` into a standalone module. Refactor to accept `cycles_device: str` parameter instead of reading a module-level global. Rename to `activate_gpu()`.
- **Key signature**: `def activate_gpu(cycles_device: str = "auto") -> str | None`
- **Acceptance gate**: `grep -c "def _activate_gpu" render-worker/scripts/blender_render.py` → 0; function callable as `from _blender_gpu import activate_gpu`
- **Dependencies**: M1-4
- **Risk**: Medium — must pass `sys.path` correctly so Blender Python finds the module
---
### [x] Task M3-2: Create _blender_import.py
- **File**: `render-worker/scripts/_blender_import.py` (NEW)
- **What**: Extract `_import_glb()` and `_apply_rotation()` into module. Rename to `import_glb()` / `apply_rotation()`.
- **Acceptance gate**: `grep -c "def _import_glb\|def _apply_rotation" render-worker/scripts/blender_render.py` → 0
- **Dependencies**: M1-4
- **Risk**: Low — no hidden globals beyond `bpy`, `math`, `Vector`
---
### [x] Task M3-3: Create _blender_materials.py
- **File**: `render-worker/scripts/_blender_materials.py` (NEW)
- **What**: Extract `_assign_failed_material()`, `_apply_material_library()`, and the `mat_map_lower` building loop. Consolidate the duplicated `mat_map_lower` logic (currently in Mode A and Mode B) into a single `build_mat_map_lower()` helper. `FAILED_MATERIAL_NAME` constant lives here.
- **Acceptance gate**: `grep -c "def _assign_failed_material\|def _apply_material_library" render-worker/scripts/blender_render.py` → 0
- **Dependencies**: M1-4
- **Risk**: Medium — `_apply_material_library()` currently uses `part_names_ordered` global; must convert to parameter
---
### [x] Task M3-4: Create _blender_camera.py
- **File**: `render-worker/scripts/_blender_camera.py` (NEW)
- **What**: Extract auto-camera placement block (bounding sphere computation, isometric positioning, clip plane setup, `ELEVATION_DEG`/`AZIMUTH_DEG` constants) and `setup_auto_lights()`.
- **Key signatures**: `def setup_auto_camera(parts, width, height) -> tuple[Vector, float]` (returns center + radius for reuse by lights); `def setup_auto_lights(bbox_center, bsphere_radius) -> None`
- **Acceptance gate**: `grep -c "ELEVATION_DEG\|AZIMUTH_DEG\|bsphere_radius" render-worker/scripts/blender_render.py` → 0
- **Dependencies**: M3-2
- **Risk**: Low — camera block is self-contained
---
### [x] Task M3-5: Create _blender_scene.py
- **File**: `render-worker/scripts/_blender_scene.py` (NEW)
- **What**: Extract `_ensure_collection()`, `_apply_smooth_batch()`, `_apply_sharp_edges_from_occ()`, shadow catcher setup into module.
- **Acceptance gate**: `grep -c "def _ensure_collection\|def _apply_smooth_batch\|def _apply_sharp_edges_from_occ" render-worker/scripts/blender_render.py` → 0
- **Dependencies**: M1-4
- **Risk**: Low
---
### [x] Task M3-6: Thin blender_render.py to entry-point
- **File**: `render-worker/scripts/blender_render.py`
- **What**: Replace all extracted function bodies with imports from submodules. Add `sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))` before imports so Blender Python finds the submodules. Result: argument parsing + Mode A/B orchestration + timing only. Target: < 200 lines.
- **Acceptance gate**: `wc -l render-worker/scripts/blender_render.py` → < 200; upload `81113-l_cut.stp` → thumbnail renders correctly
- **Dependencies**: M3-1, M3-2, M3-3, M3-4, M3-5
- **Risk**: High (integration step) — test immediately after deploy
---
## Migration Check
**No migration required.** Two render-worker scripts only. No DB, no backend, no frontend.
**No migration required.** `stl_quality` key stays in DB (harmless). No new columns or tables.
---
## 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
M1-1 (delete dirs) → M1-4 (dead func blender) → M1-5 (dead func step_processor)
→ M1-2 (admin.py) → M1-3 (frontend)
M3-1..M3-5 (create submodules in parallel where possible)
→ M3-6 (thin blender_render.py — integration, highest risk, test immediately)
```
Deploy after M1: `docker compose up -d --build backend`
Deploy after M3-6: `docker compose up -d --build render-worker`
---
## 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. ✓
1. **Blender `sys.path`**: Submodule files must be at `/render-scripts/` (the volume mount path). `sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))` is the safe way to ensure this regardless of CWD.
2. **`part_names_ordered` global**: Currently used across multiple functions in `blender_render.py`. Must be explicitly passed as a parameter to `apply_material_library()` in M3-3.
3. **M3 scope**: M3 is a pure refactor — no behaviour change. If time is limited, M1 (dead code removal) delivers clean value on its own. M3 can be deferred to a separate session.