fix(glb): remove invalid export_colors param + fix viewer stale mesh

4 root causes fixed:

1. export_colors=False was removed in Blender 4.x — caused every Blender
   export to fail (exit 1) and always fall back to trimesh. Remove it.
   Blender now runs the full pipeline: materials + sharp edges.

2. GlbModel cloned ref never reset on url change — key={glbBlobUrl} forces
   React to remount GlbModel on each new blob URL, resetting the ref so
   fresh geometry is always loaded.

3. glbBlobUrl not cleared before re-fetch — setGlbBlobUrl(null) added at
   start of downloadUrl effect so spinner shows instead of stale mesh.

4. staleTime: 30_000 delayed picking up new MediaAsset after generation.
   Changed to staleTime: 0 so invalidation always triggers immediate refetch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 15:46:42 +01:00
parent 2377cb192a
commit 3eba7b2d37
4 changed files with 77 additions and 61 deletions
+57 -58
View File
@@ -1,81 +1,80 @@
# Plan: 4 Bug Fixes — Media Thumbnails, Product Dimensions, Inline 3D Viewer, GLB Export
# Plan: Fix GLB Export Pipeline + Viewer Staleness
## Root Cause Analysis
### Bug AMissing Thumbnails in Media Library
`<img src="/api/media/{id}/download">` fails silently: the download endpoint requires JWT auth, but `<img>` tags don't send auth headers → 401 → `imgError=true` → gray icon.
For `thumbnail` type assets: fallback works via `get_thumbnail_url()``/api/cad/{cad_file_id}/thumbnail` (no-auth endpoint). For `still` type: no cad_file_id/product_id → no fallback → gray icon shown.
### Bug 1`export_colors` not valid in Blender 5.0 (CRITICAL)
**File**: `render-worker/scripts/export_gltf.py`
`bpy.ops.export_scene.gltf(export_colors=False)` → Blender exits code 1:
`keyword "export_colors" unrecognized`
→ Blender path always fails → always falls back to trimesh → no materials, no sharp edges, faceted mesh.
This is confirmed in every single log entry. Blender has never successfully exported a GLB.
### Bug BNo Dimensions in "Product Details" Card
The `cad_mesh_attributes.dimensions_mm` block exists in the CAD File section (right sidebar), NOT in the "Product Details" card. User wants it in Product Details.
### Bug 2`GlbModel` `cloned` ref never resets on URL change (CRITICAL)
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
`cloned = useRef<THREE.Group | null>(null)` with guard `if (!cloned.current)` only clones once.
When `glbBlobUrl` changes (new GLB generated), React does NOT remount `GlbModel` (same position in tree),
so `cloned.current` still holds the old geometry → old mesh shown forever.
Fix: add `key={glbBlobUrl}` to `<GlbModel>` → forces remount on each new URL.
### Bug CNo Embedded 3D Viewer
"View 3D" navigates to `/cad/:id` (full page). User wants an inline viewer in the product page CAD card that auto-loads when a `gltf_geometry` asset exists.
### Bug 3`glbBlobUrl` not cleared between fetches (UX)
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
When `downloadUrl` changes, cleanup revokes the old blob URL, but `glbBlobUrl` state still holds
the (now revoked) old URL → `GlbModel` tries to render a revoked URL for the duration of the new fetch.
Fix: `setGlbBlobUrl(null)` at the start of the effect before fetching.
### Bug DGLB + Colors Error
`trimesh` is in `pyproject.toml` but the backend container was not rebuilt → `ModuleNotFoundError: No module named 'trimesh'`. Needs rebuild.
### Bug 4`staleTime: 30_000` delays detecting new GLB (UX)
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
After "Generate GLB" the task completes and a new MediaAsset is written to DB,
but the assets query is cached for 30 seconds → `downloadUrl` stays stale → viewer fetches old GLB.
Fix: reduce `staleTime` to `0` so the query always refetches on focus/mount after invalidation.
---
## Betroffene Dateien
## Affected Files
| File | Change | Bug |
|------|--------|-----|
| `frontend/src/pages/MediaBrowser.tsx` | `useAuthBlob` hook + use in AssetCard | A |
| `backend/app/domains/rendering/tasks.py` | `publish_asset` populates product_id + cad_file_id | A |
| `frontend/src/pages/ProductDetail.tsx` | Add dimensions to Product Details card + inline viewer | B, C |
| `frontend/src/components/cad/InlineCadViewer.tsx` | New compact 3D viewer component | C |
| `backend/` (docker rebuild) | Rebuild to install trimesh | D |
| `render-worker/scripts/export_gltf.py` | Remove invalid `export_colors=False` | 1 |
| `frontend/src/components/cad/InlineCadViewer.tsx` | key={glbBlobUrl} on GlbModel + clear state + staleTime=0 | 2, 3, 4 |
---
## Tasks (in Reihenfolge)
## Tasks
### Task A1: Backend — publish_asset populates product_id + cad_file_id
- **Datei**: `backend/app/domains/rendering/tasks.py`
- **Was**: In `publish_asset`, after loading the OrderLine, also load `line.product_id` and the product's `cad_file_id`. Set these on the new MediaAsset. This enables the `_resolve_thumbnails_bulk` fallback and `get_thumbnail_url()` for still assets.
- **Akzeptanzkriterium**: New still assets have product_id and cad_file_id set in DB.
### Task 1: Fix Blender GLTF export parameters
**File**: `render-worker/scripts/export_gltf.py`
Remove `export_colors=False` from `bpy.ops.export_scene.gltf()` call.
Keep `export_materials="EXPORT"` and `export_image_format="AUTO"` — these are valid in Blender 5.0.
**Acceptance**: Blender exits 0, GLB file is created with materials.
**Requires rebuild**: yes — scripts are COPY'd into container.
### Task A2: Frontend — useAuthBlob hook in MediaBrowser
- **Datei**: `frontend/src/pages/MediaBrowser.tsx`
- **Was**: Add `useAuthBlob(url)` hook that fetches the URL with Authorization header and returns a blob URL. Use it in `AssetCard` instead of `asset.download_url` for image rendering. Revoke blob URL on unmount.
- **Akzeptanzkriterium**: Still images visible in media library grid.
### Task 2: Fix GlbModel stale mesh on regeneration
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
Add `key={glbBlobUrl}` on the `<GlbModel>` element inside the Canvas.
This forces React to unmount+remount GlbModel whenever the blob URL changes,
resetting the `cloned` ref and loading the fresh geometry.
**Acceptance**: After generating a new GLB, the viewer shows the new mesh, not the old one.
### Task B: Frontend — Dimensions in Product Details card
- **Datei**: `frontend/src/pages/ProductDetail.tsx`
- **Was**: In the "Product Details" card (around line 409-433), after the Notes field, add a read-only "Dimensions" row if `product.cad_mesh_attributes?.dimensions_mm` exists. Format: "X × Y × Z mm" with a Ruler icon and small "from CAD" label.
- **Akzeptanzkriterium**: Dimensions visible in Product Details card when mesh_attributes populated.
### Task 3: Clear stale blob URL before new fetch
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
At the top of the `useEffect([downloadUrl, token])` body, add `setGlbBlobUrl(null)` before the fetch.
This shows the loading spinner instead of a broken/stale model during re-fetch.
**Acceptance**: After regeneration, viewer shows spinner while new GLB loads.
### Task C: Frontend — Inline 3D Viewer in CAD card
- **Datei**: `frontend/src/components/cad/InlineCadViewer.tsx` (new), `frontend/src/pages/ProductDetail.tsx`
- **Was**:
1. Create `InlineCadViewer` component that:
- Accepts `cadFileId: string`
- Queries `getMediaAssets({ cad_file_id, asset_types: ['gltf_geometry'] })`
- If asset found: fetches GLB with auth (axios → arraybuffer → blob URL) → renders Three.js canvas (OrbitControls, auto-fit camera)
- While loading: shows spinner
- If no asset: shows "Generate GLB" button + thumbnail fallback
2. In ProductDetail: replace the 128×128 thumbnail box with `InlineCadViewer` (make it ~300px tall)
- Move thumbnail fallback inside InlineCadViewer
- Keep "View 3D" as "View Full Screen" link below viewer
- Remove standalone "View 3D" button (or keep as secondary link)
- **Akzeptanzkriterium**: Inline 3D model visible in product page without clicking "View 3D".
### Task D: Backend rebuild — install trimesh
- **Was**: Run `docker compose up -d --build backend` to install trimesh from pyproject.toml
- **Akzeptanzkriterium**: `docker compose exec backend python3 -c "import trimesh; print('ok')"` succeeds. GLB + Colors download works.
### Task 4: Remove staleTime delay on asset query
**File**: `frontend/src/components/cad/InlineCadViewer.tsx`
Change `staleTime: 30_000``staleTime: 0` on the `gltf_geometry` assets query.
The `qc.invalidateQueries()` call after generating already forces a refetch,
but staleTime=0 also ensures refetch on window focus/tab return.
**Acceptance**: New MediaAsset is picked up within seconds of task completion.
---
## Migrations-Check
Keine DB-Migrationen nötig. `product_id` und `cad_file_id` sind bereits Spalten in `media_assets`.
## Reihenfolge
Task 1 (rebuild) + Tasks 2/3/4 (frontend hot-reload) in parallel.
## Reihenfolge-Empfehlung
A1 + A2 + B + C parallel (alle unabhängig).
D parallel (nur Docker rebuild, kein Code).
## Risiken / Offene Fragen
- `useAuthBlob` creates blob URLs per asset — 50+ assets in grid could trigger many fetches. Add `limit` or lazy load (only fetch when card is visible).
- InlineCadViewer: GLB fetch for large files may take 5-30s. Show skeleton/spinner.
- `useGLTF` from drei expects a URL string. Blob URLs work fine.
- ThreeDViewer has `onClose` required prop — InlineCadViewer should be a new simpler component.
## Risiken
- `export_materials="EXPORT"` and `export_image_format="AUTO"` may also be invalid in Blender 5.0.
If so, remove them too and test with bare minimum params (format + apply only).
- If the Schaeffler .blend library materials use custom node groups instead of Principled BSDF,
the GLTF exporter will still export flat grey — that requires material baking, out of scope here.