---
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).
