Skip to content

feat(synoptik): echte GLB-Modelle eingehängt (Asset-Swap + Auto-Normalisierung) - #55

Merged
patricznr1 merged 1 commit into
mainfrom
feat/synoptik-3d-glb
Jun 25, 2026
Merged

feat(synoptik): echte GLB-Modelle eingehängt (Asset-Swap + Auto-Normalisierung)#55
patricznr1 merged 1 commit into
mainfrom
feat/synoptik-3d-glb

Conversation

@patricznr1

@patricznr1 patricznr1 commented Jun 25, 2026

Copy link
Copy Markdown
Owner

Was

Aktiviert die GLB-Swap-Naht der Live-3D-Linie: die fünf Maschinenklassen tragen jetzt echte, optimierte GLBs (Draco) statt Platzhalter — über das Klasse→Modell-Manifest, ohne Umbau am Datenfluss oder Klick-Vertrag. Das ist der „Asset-Swap, kein Renderer-Umbau", den die Platzhalter-Phase versprochen hat.

Gestackt auf #54 (Basis-Branch feat/synoptik-3d). Bitte #54 zuerst mergen, dann diesen rebasen/mergen.

Warum eine Auto-Normalisierung (statt blinder Transforms)

Die GLB-Inspektion zeigte völlig inkonsistente native Maßstäbe und Pivots:

Klasse native Größe (W×H×D) Pivot
feeder 0,067 × 0,079 × 0,055 ~Boden
servo_press 200 × 217 × 75 zentriert (min.y −124)
servo_axis 94 × 52 × 51
robot 6,9 × 12,2 × 9,5 schwebt (min.y +1,1)
vision_station 0,34 × 1,16 × 0,34 ~Boden

Ein Förderer mit 8 cm neben einer Presse mit 217 „units" wäre unbrauchbar. Statt fünf hand-getunter Transforms blind zu raten, normalisiert placeGlb (lib/synoptic3d/fit.ts, rein + getestet) jedes Modell uniform auf die Klassen-Zielhöhe und verankert es am Boden-Zentrum. Das ist der Render-Kohärenz-Schritt — und weil die effektive Höhe == Klassen-h ist, sitzt das Status-Beacon (h + 0,3) automatisch richtig.

Renderer-Naht vervollständigt (die 3 aus #54 verschobenen Review-Findings)

  • GLB-Disposal: geladene Modelle werden beim Unmount/Rebuild vollständig freigegeben (Geometrie + Material + Texturen, rekursiv) — kein GPU-Leck mehr.
  • Unmount/Rebuild-Race: lädt ein GLB noch, während die Szene abgebaut wird, wird das fertige Modell sofort disposed statt an eine tote Gruppe gehängt.
  • Klick-Vertrag: die GLB-Meshes werden mit machineId als Raycast-Ziele registriert → Klick auf das Modell führt weiter zur kanonischen Karte.

Assets & Decoder

  • GLBs unter public/synoptik/models/ (plain-Variante, Draco-komprimiert).
  • Draco-Decoder lokal unter public/synoptik/decoders/draco/ (nur das WASM-Paar — kein Runtime-CDN, keine externe Abhängigkeit). KTX2/Basis nicht nötig (plain-Variante, WebP-Texturen decodiert der Browser nativ).
  • public/** aus dem Lint genommen (statische/vendorte Assets); *.glb/*.wasm in .gitattributes als binär markiert (kein CRLF-Schaden).

Tests & Gates

  • lib/synoptic3d/fit.test.ts (Normalisierungs-Mathematik), components/synoptik/scene/glb.test.ts (Einsetzen/Dispose/Boden-Verankerung ohne WebGL), manifest.test.ts an die GLB-Einträge angepasst.
  • Gates grün: tsc 0 · eslint 0 · vitest 717 · tokens:check synchron · next build ok.

Offen (visuell, in der Laufumgebung)

  • Ausrichtung je Modell (ModelTransform.rotationY, heute Default 0) — falls ein Modell quer zur Flussrichtung steht, ist das ein Ein-Wert-Tweak im Manifest.
  • 60 fps-/Optik-Sichtprüfung — headless nicht seriös machbar; bitte beim Deploy/Review visuell abnehmen.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • GLB-Modelle werden nun für zentrale Maschinenklassen geladen und passend skaliert/positioniert.
    • 3D-Objekte werden korrekt am Boden zentriert dargestellt und nach dem Laden als anklickbare Ziele erkannt.
    • Ein neuer Draco-Decoder steht für 3D-Assets bereit.
  • Bug Fixes

    • Platzhalter bleiben während des Ladens sichtbar und bleiben bei Ladefehlern erhalten.
    • Abgebrochene oder verspätet fertig geladene Modelle werden sauber verworfen.
  • Tests

    • Erweiterte Tests decken die Modellplatzierung, das Entsorgen von 3D-Objekten und die Modellauflösung ab.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Die PR fügt eine GLB-basierte Modellpipeline für Synoptik hinzu: Modelle werden per Manifest auf GLB-Assets gemappt, passend skaliert und am Boden zentriert, im Scene-Loader asynchron geladen und mit Entsorgung sowie Pick-Target-Anbindung verarbeitet. Dazu kommen Asset-Regeln und Draco-Decoder-Bundling.

Changes

Synoptik GLB- und Decoder-Pipeline

Layer / File(s) Summary
Fit-Berechnung
frontend/lib/synoptic3d/fit.ts, frontend/lib/synoptic3d/fit.test.ts
computeFit berechnet Scale und Offset aus einer Bounding-Box; die Tests prüfen Zielhöhe, Bodenverankerung, x/z-Zentrierung und den Nullhöhen-Fallback.
Manifest-Quellen
frontend/lib/synoptic3d/manifest.ts, frontend/lib/synoptic3d/manifest.test.ts
Die Manifest-Definition liefert für die Hauptklassen GLB-Quellen unter /synoptik/models, lässt mixing_unit als Platzhalter stehen und wird durch Tests für die neuen Quellen und den Swap-Fall abgesichert.
GLB-Platzierung und Entsorgung
frontend/components/synoptik/scene/glb.ts, frontend/components/synoptik/scene/placeholders.ts, frontend/components/synoptik/scene/glb.test.ts
placeGlb setzt Modellskala und -position aus computeFit; Placeholder-Handles speichern angehängte Modelle, blenden Body-Meshes aus, liefern Pick-Ziele und entsorgen den angehängten Objektbaum.
Scene-Loader-Wiring
frontend/components/synoptik/synoptik-scene.tsx
SynoptikScene startet für GLB-Quellen eine asynchrone Laderoutine, setzt eine disposed-Guard-Flag, registriert spätere Pickables und entsorgt angehängte GLBs im Cleanup.
Binary-Assets und Draco-Decoder
.gitattributes, frontend/eslint.config.mjs, frontend/public/synoptik/decoders/draco/draco_wasm_wrapper.js
.glb und .wasm werden als binary behandelt, public/** ist vom ESLint-Scan ausgeschlossen, und der Draco-Decoder wird als UMD-Modul mit WASM-Ladevorgang, Bindings und Exports bereitgestellt.

Sequence Diagram(s)

sequenceDiagram
  participant SynoptikScene
  participant resolveModelSource
  participant loadGlbForMachine
  participant createModelLoader
  participant placeGlb
  participant PlaceholderHandle
  participant registerPickables

  SynoptikScene->>resolveModelSource: machineClass
  resolveModelSource-->>SynoptikScene: ModelSource
  SynoptikScene->>loadGlbForMachine: source.kind === "glb"
  loadGlbForMachine->>createModelLoader: load(model source)
  createModelLoader-->>loadGlbForMachine: Object3D
  loadGlbForMachine->>placeGlb: fit model to targetHeight
  loadGlbForMachine->>PlaceholderHandle: attachGlb(model)
  PlaceholderHandle-->>loadGlbForMachine: mesh list
  loadGlbForMachine->>registerPickables: add meshes with machineId
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • patricznr1/foreman#54: Baut die Synoptik-Model-Manifest-/Placeholder-Infrastruktur aus, auf der diese PR die GLB-Quellen, Platzierung und Entsorgung aufsetzt.

Poem

Ein Häschen springt durchs GLB-Revier,
am Boden sitzt das Modell nun hier.
Mit Draco singt das Mesh ganz sacht,
und alles wird schön festgemacht. 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Der Titel beschreibt treffend den Hauptinhalt: GLB-Modelle werden eingebunden und automatisch normalisiert.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/synoptik-3d-glb

Comment @coderabbitai help to get the list of available commands.

@patricznr1
patricznr1 force-pushed the feat/synoptik-3d-glb branch from f12d456 to 8352755 Compare June 25, 2026 19:52
@patricznr1

Copy link
Copy Markdown
Owner Author

ℹ️ CodeRabbit hat diesen PR übersprungen ("Auto reviews are disabled on base/target branches other than the default branch"), weil er auf feat/synoptik-3d statt main zielt. Sobald #54 nach main gemergt ist und dieser PR auf main umzielt, läuft das Review automatisch.

Dieser PR ist auf den gefixten Stand von #54 (f70b167) rebased und erbt damit alle dortigen CodeRabbit-Fixes (PMREM-Leck, Klick/Drag, Ring-Pick, SSR-Timeout). Die GLB-Renderer-Naht führt sie konsistent fort: vollständiges GLB-Dispose, Unmount/Rebuild-Race-Guard und Raycast-Registrierung der GLB-Meshes.

Gates grün: tsc 0 · eslint 0 · vitest 717 · next build ok.

@patricznr1
patricznr1 force-pushed the feat/synoptik-3d-glb branch from 8352755 to 86fdec8 Compare June 25, 2026 20:09
@patricznr1
patricznr1 force-pushed the feat/synoptik-3d-glb branch from 86fdec8 to 9138716 Compare June 25, 2026 20:19
Base automatically changed from feat/synoptik-3d to main June 25, 2026 20:29
…lisierung)

Aktiviert die GLB-Swap-Naht: die fünf Maschinenklassen tragen jetzt optimierte
GLBs (Draco) statt Platzhalter — über das Klasse→Modell-Manifest, ohne Umbau am
Datenfluss oder Klick-Vertrag. mixing_unit (kein Asset) bleibt Platzhalter.

- Auto-Normalisierung (lib/synoptic3d/fit.ts, rein/getestet): die nativen
  Hersteller-GLBs kommen in völlig verschiedenen Einheiten (feeder ~0,08 vs.
  servo_press ~217 „units") und Pivots (zentriert/schwebend). placeGlb skaliert
  jedes Modell uniform auf die Klassen-Zielhöhe und verankert es am Boden-Zentrum
  — der eigentliche Render-Kohärenz-Schritt. So sitzt auch das Status-Beacon
  (h + 0,3) automatisch richtig.
- Renderer-Naht vervollständigt (adressiert die 3 verschobenen Review-Findings):
  vollständiges GLB-Disposal (Geometrie/Material/Texturen), Unmount/Rebuild-Race-
  Schutz, und die GLB-Meshes werden als Raycast-Ziele mit machineId registriert
  (der Klick → kanonische Karte bleibt erhalten).
- Assets unter public/synoptik/models/ (plain .glb, Draco); Draco-Decoder lokal
  unter public/synoptik/decoders/draco/ (WASM-Paar, kein Runtime-CDN; KTX2/Basis
  nicht nötig). public/** aus dem Lint genommen; *.glb/*.wasm als binär markiert.

Offen (visuell, in der Laufumgebung): Ausrichtung je Modell (ModelTransform.rotationY,
Default 0) und der 60fps-/Optik-Sichtcheck. Gates grün: tsc 0, eslint 0,
vitest 717, tokens synchron, next build ok.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@patricznr1
patricznr1 force-pushed the feat/synoptik-3d-glb branch from 9138716 to 6c23e86 Compare June 25, 2026 20:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
frontend/components/synoptik/synoptik-scene.tsx (1)

66-70: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Ladefehler still verschluckt — Diagnose erschwert.

Der catch-Block verwirft den Fehler ohne jede Spur. Die Platzhalter-Degradation ist gewollt, aber ohne Log bleibt ein fehlendes oder defektes GLB-Asset (404, Draco-Decode-Fehler) in Produktion unsichtbar. Ein console.warn mit source.url würde die Fehlersuche deutlich erleichtern.

🔧 Vorschlag
   try {
     model = await loader.loadGlb(source.url);
-  } catch {
+  } catch (error) {
+    console.warn(`Synoptik: GLB-Laden fehlgeschlagen (${source.url}) — Platzhalter bleibt.`, error);
     return; // Ladefehler → Platzhalter bleibt stehen (ehrliche Degradation).
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/synoptik/synoptik-scene.tsx` around lines 66 - 70, The
try/catch around loader.loadGlb in synoptik-scene.tsx swallows GLB load failures
silently, making missing or broken assets hard to diagnose. Update the catch
block in the scene loading flow to keep the graceful fallback but also emit a
warning that includes source.url (for example via console.warn) so failures are
visible in production logs.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontend/components/synoptik/scene/glb.ts`:
- Around line 22-32: The fit calculation in the GLB scene setup is using a
bounding box computed before the model’s final Y rotation, which causes x/z
offset drift when rotationY is non-zero. Update the transform flow in the scene
logic around the model positioning so model.rotation.y is applied before calling
new THREE.Box3().setFromObject(model), then keep computeFit and the subsequent
scale/position assignment using that rotated bounding box.

---

Nitpick comments:
In `@frontend/components/synoptik/synoptik-scene.tsx`:
- Around line 66-70: The try/catch around loader.loadGlb in synoptik-scene.tsx
swallows GLB load failures silently, making missing or broken assets hard to
diagnose. Update the catch block in the scene loading flow to keep the graceful
fallback but also emit a warning that includes source.url (for example via
console.warn) so failures are visible in production logs.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0345f49c-9f47-4794-8f33-00d2d1af2661

📥 Commits

Reviewing files that changed from the base of the PR and between 2e1fd3e and 6c23e86.

⛔ Files ignored due to path filters (1)
  • frontend/public/synoptik/decoders/draco/draco_decoder.wasm is excluded by !**/*.wasm
📒 Files selected for processing (16)
  • .gitattributes
  • frontend/components/synoptik/scene/glb.test.ts
  • frontend/components/synoptik/scene/glb.ts
  • frontend/components/synoptik/scene/placeholders.ts
  • frontend/components/synoptik/synoptik-scene.tsx
  • frontend/eslint.config.mjs
  • frontend/lib/synoptic3d/fit.test.ts
  • frontend/lib/synoptic3d/fit.ts
  • frontend/lib/synoptic3d/manifest.test.ts
  • frontend/lib/synoptic3d/manifest.ts
  • frontend/public/synoptik/decoders/draco/draco_wasm_wrapper.js
  • frontend/public/synoptik/models/feeder.glb
  • frontend/public/synoptik/models/robot.glb
  • frontend/public/synoptik/models/servo_axis.glb
  • frontend/public/synoptik/models/servo_press.glb
  • frontend/public/synoptik/models/vision_station.glb

Comment on lines +22 to +32
const box = new THREE.Box3().setFromObject(model);
const effectiveHeight = targetHeight * transform.scale;
const fit = computeFit({ min: box.min, max: box.max }, effectiveHeight);

model.scale.setScalar(fit.scale);
model.position.set(
fit.offset.x + transform.offset.x,
fit.offset.y + transform.offset.y,
fit.offset.z + transform.offset.z,
);
model.rotation.y = transform.rotationY;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Three.js Object3D matrix composition order translation rotation scale TRS

💡 Result:

In Three.js, an Object3D's local transformation matrix is composed using the order Translation, Rotation, and Scale (TRS) [1][2]. When the engine automatically updates the local matrix (default behavior when matrixAutoUpdate is true), it calls internal methods that effectively combine these transformations into the local matrix property [1][3]. Specifically, the Matrix4.compose(position, quaternion, scale) method performs this operation [4][3]. Key aspects of this matrix composition include: 1. Local Matrix (matrix): This matrix represents the object's transformation relative to its parent [2][3]. It is composed from the object's position (Translation), quaternion (Rotation), and scale (Scale) [2][4]. 2. World Matrix (matrixWorld): This represents the object's transformation in world space [1][2]. For a root object, it is identical to the local matrix; for a child object, it is calculated by multiplying the parent's world matrix by the child's local matrix (ParentWorldMatrix * ChildLocalMatrix) [3]. 3. Manual Control: If matrixAutoUpdate is set to false, you are responsible for calling updateMatrix to refresh the local matrix after modifying position, quaternion, or scale [1][5]. Similarly, if matrixWorldAutoUpdate is false, you must manage the world matrix manually [1][6]. When manually constructing matrices using methods like Matrix4.multiply, remember that matrix multiplication is non-commutative [7]. The order in which you apply transformations depends on whether you are multiplying on the left or the right; for hierarchical or sequential transformations, applying them in the correct mathematical order is essential to achieving the intended result [7].

Citations:


🏁 Script executed:

sed -n '1,220p' frontend/components/synoptik/scene/glb.ts

Repository: patricznr1/foreman

Length of output: 2022


Rotation vor der Fit-Berechnung setzen Bei rotationY ≠ 0 driften die x/z-Offsets, weil die Bounding-Box vor der Rotation berechnet wird. model.rotation.y vor new THREE.Box3().setFromObject(model) setzen, damit computeFit die finale Ausrichtung berücksichtigt.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/components/synoptik/scene/glb.ts` around lines 22 - 32, The fit
calculation in the GLB scene setup is using a bounding box computed before the
model’s final Y rotation, which causes x/z offset drift when rotationY is
non-zero. Update the transform flow in the scene logic around the model
positioning so model.rotation.y is applied before calling new
THREE.Box3().setFromObject(model), then keep computeFit and the subsequent
scale/position assignment using that rotated bounding box.

@patricznr1
patricznr1 merged commit dddb5fe into main Jun 25, 2026
3 checks passed
@patricznr1
patricznr1 deleted the feat/synoptik-3d-glb branch June 25, 2026 20:53
patricznr1 added a commit that referenced this pull request Jun 25, 2026
…04) (#56)

Next.js output:"standalone" übernimmt public/ NICHT automatisch; das Dockerfile
kopierte bisher nur das Standalone-Bundle + die statischen Assets (Kommentar:
"kein public/"). Seit der 3D-Linie (#55) liegen die Maschinen-GLBs + der
Draco-Decoder unter frontend/public/synoptik/ → auf dem Deploy 404, der Renderer
fällt still auf Platzhalter zurück. Fix: public/ explizit ins Runner-Stage kopieren.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant