OFFENE-PUNKTE + Memory: Verbau-/fire-visual-Stände, Placement-Mode-Memory

This commit is contained in:
marcus.hinz
2026-06-27 18:56:49 +02:00
parent b77f74a001
commit 062f3eb845
4 changed files with 86 additions and 0 deletions
+1
View File
@@ -5,6 +5,7 @@
"Edit(~/projects/geko-core/**)",
"Edit(~/projects/adesso-cpq/**)",
"Edit(~/projects/geko-infra/**)",
"Edit(~/projects/fire-visual/**)",
"Edit(~/claude-projects/robotunits/**)"
],
"additionalDirectories": [
+6
View File
@@ -2,3 +2,9 @@
- [ ] **Paul / Schutzzaun "Element":** In `adesso-cpq/packages/frontend/src/components/building-blocks/ProductBrowser.vue` ist die kataloggruppe `Element` im `schutzzaun_basic` per Filter rausgenommen (TODO (Paul) an der Filterzeile). Element ist kaputt, muss richtig gefixt werden.
- [ ] **Marcel / Selection:** In `adesso-cpq/packages/frontend/src/components/visual/DefaultCommandProzessor.ts` sind für die Events `selection` und `selection-click` `FIXME (Marcel)`-Kommentare gesetzt. Selection-Logik sauber in den Griff bekommen.
- [ ] **Marcel / Scale-Hack vs. PlacementMode — wie lösen?** `Fire3D.vue` skaliert per `scaleScene` jedes Root-Handle mit `0.002` (Laufzeit). Der PlacementMode überschreibt die Subject-Matrix mit `Vector3.one` (`subject.matrix = bestMatrix · dragPointTransform⁻¹`); der DragPoint-Transform trägt den Faktor `S`, sein Inverses `1/S`, das Subject bekommt `1/S` → Geometrie netto `S·1/S = 1` → ~500×. Ergebnis: **jeder** einheitliche Laufzeit-Scale (Root, Wrapper, „kleine" Selection) überlebt den Snap nicht. Hack und PlacementMode sind auf demselben Subject unvereinbar. Einziger überlebender Wert: `S=1`, d. h. reale kleine Koordinaten in den Daten selbst — Geometrie **und** PMIs konsistent, kein Laufzeit-Scale — plus Kamera-Config (`camera-controller` `defaultPosition`/`fixPoint`, Ortho-Frustum auf mm). Offen/ungeprüft: ob fire-visual dafür einen vorgesehenen Mechanismus hat (Unit-System `unitConverter`/`environment.units`, FBX `UnitScaleFactor` greift bisher nur beim Import, nicht auf fertige JSON-SceneGraphen). Entscheidung nötig: Daten in realer Einheit liefern vs. Lib-Eingriff (PlacementMode behält Subject-Scale).
- [ ] **Marcel (Montag) / Lib-Bug movementMode — an fire-visual melden:** `fire-visual` `threed/interaction/module/src/interactionMode/movementMode.ts` `updateArrowHighlight` (Z. 295299) greift `(this.interactionHelperHandle.find('.xy-plane') as Handle).renderer…` **ohne** null-Prüfung zu — während dieselbe Funktion die Achsen (Z. 305318) sehr wohl absichert (`if (!arrowElement) return; if (arrowHandle?.renderer)`). Fehlt das Plane-Kind (Gizmo-Helper existiert, Planes weg — z. B. wenn ein voller `setScene` das Gizmo bei aktivem Move abräumt), wirft es `Cannot read properties of null (reading 'renderer')` **in jedem rAF-Frame** → kompletter Freeze. Lib ist nicht unser Code → upstream an fire-visual: Z. 295299 konsistent zu Z. 305318 absichern. Cpq-seitig vorerst umschifft (siehe unten), aber Upstream-Fix nötig.
- [ ] **Marcel (Montag) / funktionierendes Bundle (`@ad/fire` mit Relations-Resolver):** cpq lädt `@ad/fire@1.0.5` (gebündelt in `packages/runtime/visual` via `tsup`, gelinkt nach `packages/frontend/node_modules/@ad-cpq/visual-runtime`). Diese Version hat `RelationsFeature` + `createRelation`, aber **nicht** `resolveRelations` bzw. das Command `resolve_relations` (im Bundle 0 Vorkommen) — der Resolver kam in der fire-visual-Quelle (`~/projects/fire-visual`) erst nach 1.0.5 dazu. Folge: das Constraint-Attachment (`attachment_relations` mit `constraint_position`) wird beim Placement zwar in den Szenegraph geschrieben und die Relation instanziiert, bleibt aber **inert** (kein Resolver, kein Trigger) → verbundene Teile ziehen beim Move nicht mit. Nötig: `@ad/fire` auf einen Stand mit Resolver bringen — fire-visual lokal bauen (`build:local`, yarn-workspaces-Monorepo) → neues `@ad/fire`, cpq's `@ad/fire@1.0.5` darauf umbiegen (pnpm-override / `file:`-Link), `packages/runtime/visual` neu bauen. Achtung API-Drift neuere fire-visual vs. cpq/Runtime. Zusätzlich offen, sobald Resolver da: (1) Auto-Trigger fehlt auch in der Quelle — `resolveRelations` wird nur via Command aufgerufen, kein Update-Hook/Transform-Abo; sauber wäre der Trigger im Move-/Placement-Mode der Engine (mit gezogenem Teil gepinnt), sonst muss cpq es von außen treiben. (2) Persistenz: aufgelöste Nachbar-Transforms leben in der Engine, nicht im cpq-eventStore → Readback (`get_transformation`) nötig, sonst Rücksprung beim nächsten `setScene`.
- [ ] **Marcel / fire-visual — sichtbarer Gap beim Verbau-Ziehen:** Beim Ziehen eines verbundenen Teils hängen die Follower sichtbar hinterher. Ursache: der `resolutionScheduler` löst **deferred** auf — das gegriffene Teil bewegt der Gizmo im Input/Update-Phase desselben Frames, die Relationen werden aber nur **markiert** (`_onTransformChange`) und erst in der **Process-Phase** (`_resolveNow`) aufgelöst, also einen Schritt später. Damit trailen die Follower konstant um die Bewegung eines Frames (Gap skaliert mit Ziehgeschwindigkeit; bei Stillstand holen sie im nächsten Frame auf → „im Block"). Kein Timer/Throttle, `MAX_ITERATIONS=10`. Erwartung: fire-visual soll den Gap rausnehmen (Auflösung synchron zum Move) **oder** das Nachziehen sauber animieren. Cpq-seitig nicht lösbar ohne den Resolver gegen die Lib synchron zu treiben.
- [ ] **Marcel / fire-visual — Null-Matrix-Crash beim `setScene`-Teardown (mit Relationen + Rotation):** Nach mehreren Rotationen verbundener Teile (jede über cpq-`setScene` → Rebuild) crasht es: `Uncaught Error: Matrix4_4(...zeros...) - inverse: can't be calculated` in `VolumeContext.updateWTL``ColliderComponent.syncTransformation``SynchronizeQueue.process` ← rAF, **jeden Frame** → Freeze. Davor flutet der Log mit `FeatureComponent - deregister` (Teardown der alten Szene). Ein Collider synchronisiert während/nach dem `setScene`-Teardown gegen eine bereits abgeräumte/**genullte** Transform → Null-Matrix → Inverse unmöglich. Reproduzierbar mit aktiven `attachment_relations` (Position+Rotation) und wiederholtem `setScene`; kumulativ (tritt nach einigen Rebuilds auf, gleiche Transform-Paarung). **Nicht** das Quaternion-Format (`values` ist `[w,x,y,z]`, verifiziert) und **nicht** der Resolver/`localA`/`constraint_rotation` (mit vollständigem Constraint-Satz unverändert). Während des Ziehens (Engine-Live-Resolve) ist alles korrekt — nur der cpq-`setScene`-Rebuild beim Loslassen triggert es. Gleiche Klasse wie der movementMode-null-renderer-Freeze: Lib räumt beim Rebuild nicht sauber ab bzw. der Collider-Sync läuft gegen eine tote Transform. Upstream-Fix nötig. Cpq-seitig vorerst umschifft: `handleRotateEnd` macht **kein** `setScene` mehr (die Engine hat den Rotations-Endzustand live schon korrekt; Event wird nur recorded). Offen bleibt: ein **nachfolgendes** `setScene` (z. B. Placement nach Rotation) replayt die Rotation und könnte denselben Teardown-Crash auslösen — der eigentliche Fix ist upstream oder ein **inkrementelles** Transform-Update (`set_transformation`) statt Full-`setScene` für Move/Rotate.
- [ ] **cpq / Verbau-Code refactoren (nach den Klärungen):** `DefaultCommandProzessor`/`SceneReplayProcessor`/`EventStore`/`eventDataUtil` sind über die Klärungsrunden gewachsen (A-Modell: Move-Ende recorded bewegtes Teil + transitive Follower als eine `pushGroup`-Gruppe; Placement schreibt EIN gemeinsames `attachment_relations` wegen Clobber im `relationsProcessor`; Position-Korrektur `pos = event.position dragPMI-Offset`; Subject-Ausschluss in DropPoints via `:not`). Funktioniert (Kette ≥4, Undo/Redo gruppiert), aber aufräumen.
+1
View File
@@ -7,3 +7,4 @@
- [Kein nextTick](feedback-kein-nexttick.md) — Vue nextTick hartes Verbot; Timing reaktiv über watch/v-if lösen
- [cpq Storybook starten](cpq-storybook-start.md) — pnpm --filter @ad-cpq/common-frontend storybook (Port 6006)
- [Git nur Fast-Forward](feedback-git-nur-fast-forward.md) — kein rebase/merge/force; bei Divergenz melden statt auflösen
- [fire-visual Placement-Mode](fire-visual-placement-mode.md) — snap-to-point: DragPoints/DropPoints, processInput-Algorithmus, start_placement_mode-Command, PMI-DropPoints
@@ -0,0 +1,78 @@
---
name: fire-visual-placement-mode
description: "fire-visual Placement-Mode (snap-to-point) — Architektur, Settings, Algorithmus, DropPoints, Commands; Pfade im threed/interaction- und threed/pmi-Modul"
metadata:
node_type: memory
type: project
originSessionId: 733f7054-ac76-49cd-ba04-516ef015d4f3
---
Projekt `fire-visual` (`~/projects/fire-visual`, erlaubter Ordner). Placement-Mode = snap-to-point-Platzierung eines Subjects über Drag-Points/Drop-Points. Teil des Moduls `@fire-visual/interaction`.
## Pfade
- Mode: `threed/interaction/module/src/interactionMode/placementMode.ts`
- Verdrahtung/Command: `threed/interaction/module/src/feature/interactionFeature.ts` (ab Zeile 259)
- Options-Typen: `threed/interaction/module/src/options/modeOptions.ts`
- Modul-Doku: `threed/interaction/MODULE.md`
- Test: `threed/interaction/module/tests/placementMode.test.ts`
- DropPoint-Implementierungen (PMI): `threed/pmi/module/src/dropPoint/{pmiDropPoint,planeDropPoint,pointDropPoint,lineDropPoint}.ts`
- DropPoint-Factory: `threed/pmi/module/src/component/pmiComponent.ts``getDropPoint()` (Zeile 422)
## Grundbegriffe
- **Subject**: das zu platzierende Objekt (`Handle`).
- **Drag-Point**: Punkt am Subject (lokaler `transform: Matrix4_4`), an dem „angefasst" wird. Visualisiert als Kugel (Sphere). Aktiv = gelb, inaktiv = blau.
- **Drop-Point**: Ziel-Snap-Geometrie in der Szene; liefert über `assessRay` eine Bewertung. Implementiert Interface `DropPoint`.
- **activeDragPointIndex**: aktiver Drag-Point; `-1` = „alle Drag-Points" (nur erlaubt wenn `allowAllDragPoints !== false`). Start: `-1`, oder `0` falls `allowAllDragPoints === false`.
## PlacementModeSettings
- `camera: CameraComponent`, `subject: Handle`
- `dragPoints[]`: je `{ transform, dropPoints: Partial<DropPointProvider>[], selector? }`
- `defaultCursorTransform?` (Fallback = `dragPoints[0].transform`)
- `allowAllDragPoints?` (Default true) — false: Index -1 verboten, Cycling hält immer einen konkreten DP aktiv
- `allowDragPointCycling?` (Default true) — false: User kann aktiven DP nicht ändern, „alle DP" bleibt aktiv
- `maxJumpDistance?` — überschreibt globalen Wert aus `ModeOptionProvider`
## Globale Optionen (ModeOptionProvider.options.placement, Typ PlacementModeOptions)
`maxJumpDistance`, `highlightColor` (Default Color.yellow), `dragPointActiveColor` (Default yellow), `dragPointInactiveColor` (Default blue), `dragPointRadius` (Default 0.1). Plus `gizmoScale` (Default 1) skaliert Kugelradius.
## Algorithmus processInput(input) je Frame
Input: `pointerNormalizedPosition`, `nextDragPoint`, `previousDragPoint`, `interactionClick`.
1. Ohne Pointer/Camera → false.
2. **Grab-Gating**: Rising edge von `interactionClick` setzt `_grabActive = true`. Solange nicht grab-aktiv: nur prevClick merken, return true (Subject bewegt sich nicht).
3. Cycling: bei `nextDragPoint`/`previousDragPoint` Index zyklen (wenn Cycling erlaubt).
4. Alle Drop-Point-Visualisierungen zurücksetzen (erst `false`+Highlight null), dann die zum aktiven DP gehörenden aktivieren (Index -1 ⇒ alle).
5. Ray von Kamera durch Pointer; Schnitt mit XZ-Ebene (`Plane` durch Ursprung, Normale up) → `intersectionPoint`.
6. Für jeden aktiven Drag-Point × dessen Drop-Points: Ray um `dropPoint.predictDragPointRotation(ray) * (dragPoint.transform⁻¹ * cursorTransform) * 0` versetzen, dann `dropPoint.assessRay(offsetRay)` → RayAssessment.
7. Beste Assessment über `betterRayAssessment`: kleinste `jumpDistance`; bei Gleichstand (Toleranz 0.05) kleinste `rayDistance`.
8. Wenn `best.jumpDistance < maxJumpDistance`: Drop-Point highlighten und Subject snappen: `subject.matrix = compose(best.position, best.rotation, 1) * dragPointTransform⁻¹`. Sonst: Subject auf `intersectionPoint` auf XZ-Ebene setzen (`* cursorTransform⁻¹`), rotation identity.
9. `_lastPlacement` (PlacementUpdateData) merken: position, rotation, dragPointSelector, dropPointSelector (jeweils null bei XZ-Fallback).
10. Drag-Point-Kugeln neu positionieren (`subjectMatrix * dragPoint.transform * 0`), Material gelb/blau je aktiv.
11. **Falling edge** von `interactionClick`: `_grabActive = false`, `_lastPlacement` per `emitUpdateEvent` (subject als selectorElementReferenceRepresentation) ausgeben.
## Cycling-Logik
- allowAllDragPoints true: Reihenfolge … last → -1 (alle) → 0 → … (wrap über -1).
- allowAllDragPoints false: -1 wird übersprungen, Wrap last → 0.
## Lifecycle
- `startMode`: Drop-Points je Drag-Point aus Providern extrahieren (`getDropPoint()`), Drag-Point-Visuals (Sphere-Handles, BasicMaterial transparent opacity 0.75) per `addInteractionScene` erzeugen, Handles via `find('.drag-point-i')`.
- `stopMode`: dropPoints/dragPointHandles leeren.
## Command (Environment): start_placement_mode(params)
params: `subject: string`, `cursorTransform?`, `allowAllDragPoints?`, `allowDragPointCycling?`, `maxJumpDistance?`, `dragPoints: Array<{ selector?, transform?:{position,rotation}, dropPoints: string[] }>`.
Auflösung in interactionFeature:
- Subject-Selector → Handle (wirft wenn nicht gefunden).
- Pro Drag-Point: `dropPoints`-Selektoren via `selectorEngine.findAll` → DropPointProvider.
- Ohne `selector`: ein Drag-Point mit `transform` (aus position/rotation, sonst identity), selector undefined.
- Mit `selector`: `findAll(selector, subjectHandle)` → je Sub-Entity ein Drag-Point mit `transform = subject.LTW⁻¹ * dpe.LTW * transform`, selector = `selectorEngine.selector(dpe)`.
Weitere Commands: `placement_mode_next_drag_point`, `placement_mode_previous_drag_point` (werfen TypeError wenn aktueller Mode kein PlacementMode), `stop_interaction_mode`, `observe_interaction_events`.
## DropPoint-Interface
`assessRay(ray): RayAssessment {jumpDistance, rayDistance, position, rotation}`, `predictDragPointRotation(ray): Matrix4_4`, `setVisualization(state)`, `setVisualizationHighlight(color|null)`, `selector(): string|null`.
PMI-Implementierungen (`PmiDropPoint` Basis, hält `LTW`, `dragPointRotation = decompose(LTW)[1]`, predictRotation = konstant):
- **PointDropPoint** (PMIType point/matrix): position = LTW·0; jumpDistance = Abstand Punkt↔nächster Punkt auf Strahl.
- **LineDropPoint** (line): Segment LTW·0 → parentLTW·endpoint; closest segment-on-line.
- **PlaneDropPoint** (plane): Ebene aus LTW; Ray-Schnitt, in Ebenen-Koords auf `size`/2 geclampt, zurück nach Welt; jumpDistance = clampedPoint↔rayPoint.
`PMIComponent.getDropPoint()` mappt PMIType → Klasse (point/matrix→Point, line→Line, plane→Plane, sonst null).
Verwandt: [[cpq-verbau-architektur]] (CPQ-seitige Verbau/Platzierung).