diff --git a/LEARNINGS.md b/LEARNINGS.md index b4e1994..38f07f9 100644 --- a/LEARNINGS.md +++ b/LEARNINGS.md @@ -436,6 +436,21 @@ SQLAlchemy `Enum(create_type=False)` funktioniert nicht zuverlässig mit asyncpg --- +### 2026-03-07 | Blender 5.0 | `export_colors` in bpy.ops.export_scene.gltf entfernt +**Problem:** `bpy.ops.export_scene.gltf(export_colors=False)` → `keyword "export_colors" unrecognized` → exit code 1 → immer Trimesh-Fallback → nie Materialien, nie Sharp Edges, immer facettiert. Blender hat nie erfolgreich exportiert. +**Lösung:** `export_colors` aus dem Export-Call entfernen. Gültige Blender-5.0-Parameter: `export_format`, `export_apply`, `use_selection`, `export_materials`, `export_image_format`. +**Regel:** Beim Wechsel auf neue Blender-Versionen alle bpy.ops.*-Parameter gegen aktuelle Blender-Doku prüfen. Fehlende Parameter lassen Blender mit exit 1 fehlschlagen — OHNE aussagekräftige Fehlermeldung im Erfolgsfall. + +### 2026-03-07 | React | useRef mit if(!ref.current) Guard reagiert nicht auf Prop-Änderungen +**Problem:** `GlbModel` klonte `scene` in `useRef` mit `if (!cloned.current)`. Bei neuem `url`-Prop (regeneriertes GLB) blieb `cloned.current` das alte geklonte Objekt → altes Mesh wurde weiter angezeigt. React mounted/unmounts die Komponente nicht bei Prop-Änderungen. +**Lösung:** `key={glbBlobUrl}` auf `` → React remountet die Komponente bei jeder neuen URL → frischer `useRef` → neues Mesh. Alternativ: `useMemo` mit URL-Dependency statt `useRef`. +**Regel:** Wenn ein `useRef`-Initialisierungsmuster (`if (!ref.current)`) auf Prop-Änderungen reagieren muss → immer mit `key` oder `useEffect`/`useMemo` kombinieren. + +### 2026-03-07 | React | staleTime zu hoch verzögert Erkennung neuer API-Daten +**Problem:** `staleTime: 30_000` auf dem gltf_geometry-Assets-Query: Nach GLB-Generierung dauerte es bis zu 30 Sekunden bis der neue `download_url` im Browser ankam — obwohl `qc.invalidateQueries()` aufgerufen wurde. Invalidierung erzwingt Refetch, aber staleTime=30s verhindert ihn falls der Cache noch "frisch" gilt. +**Lösung:** `staleTime: 0` für Queries die bei Invalidierung sofort aktuell sein müssen. +**Regel:** `staleTime: 0` für Entitäten die nach Mutations sofort aktuell sein müssen. Höhere staleTime nur für read-heavy, selten ändernde Daten (z.B. Materialliste, Templates). + ### 2026-03-07 | GLB Export | Trimesh kennt keine Materialien — Blender-Pipeline ist Pflicht **Problem:** `generate_gltf_geometry_task` nutzte trimesh für STL→GLB. Trimesh ist eine reine Geometrie-Bibliothek: keine Material-Bibliotheken, kein OCC-Kantenmarking, kein Asset-Library-Support. Das erzeugte graue, facettierte GLB-Dateien ohne Materialien. **Lösung:** Task auf Blender headless (`export_gltf.py`) umgestellt. Übergibt: `material_map` (via `resolve_material_map()` aus `cad_part_materials`), `sharp_edges_json` (aus `mesh_attributes.sharp_edge_midpoints`), `asset_library_blend` (via `get_material_library_path()`). Trimesh nur noch als Fallback wenn Blender nicht verfügbar. diff --git a/frontend/src/components/cad/InlineCadViewer.tsx b/frontend/src/components/cad/InlineCadViewer.tsx index beaa838..98f8fa8 100644 --- a/frontend/src/components/cad/InlineCadViewer.tsx +++ b/frontend/src/components/cad/InlineCadViewer.tsx @@ -96,7 +96,7 @@ export default function InlineCadViewer({ const { data: gltfAssets } = useQuery({ queryKey: ['media-assets', cadFileId, 'gltf_geometry'], queryFn: () => getMediaAssets({ cad_file_id: cadFileId, asset_types: ['gltf_geometry'] }), - staleTime: 30_000, + staleTime: 0, refetchInterval: generating ? 4_000 : false, }) @@ -109,6 +109,8 @@ export default function InlineCadViewer({ useEffect(() => { if (!downloadUrl || !token) return + // Clear stale mesh immediately so the loading spinner shows instead of old geometry + setGlbBlobUrl(null) setLoadingGlb(true) let blobUrl = '' fetch(downloadUrl, { headers: { Authorization: `Bearer ${token}` } }) @@ -140,7 +142,7 @@ export default function InlineCadViewer({ - + diff --git a/plan.md b/plan.md index 35711fa..3a14f6e 100644 --- a/plan.md +++ b/plan.md @@ -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 A — Missing Thumbnails in Media Library -`` fails silently: the download endpoint requires JWT auth, but `` 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 B — No 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(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 `` → forces remount on each new URL. -### Bug C — No 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 D — GLB + 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 `` 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. diff --git a/render-worker/scripts/export_gltf.py b/render-worker/scripts/export_gltf.py index 3be4207..28ae6ea 100644 --- a/render-worker/scripts/export_gltf.py +++ b/render-worker/scripts/export_gltf.py @@ -124,6 +124,7 @@ def main() -> None: apply_asset_library_materials(args.asset_library_blend, material_map, link=False) # Export GLB with full PBR material data + # Note: export_colors was removed in Blender 4.x — do not pass it. try: bpy.ops.export_scene.gltf( filepath=args.output_path, @@ -132,7 +133,6 @@ def main() -> None: use_selection=False, export_materials="EXPORT", # export all materials (Principled BSDF → glTF PBR) export_image_format="AUTO", # embed textures (base color, normal, roughness maps) - export_colors=False, # skip vertex colors (we use library materials) ) except Exception as exc: print(f"GLB export failed: {exc}", file=sys.stderr)