1	# Nomenklatur der Root-Handle-Punkte — schutzzaun_basic
2
3	Quelle: `katalog_fix3.zip`, Familie `schutzzaun_basic`, 889 Entities.
4	Betrachtet werden **ausschließlich die Punkte auf dem Root-Handle** (Transform `key == "root"`). Punkte in Unterteilen sind hier nicht enthalten.
5
6	## Umfang
7
8	Auf Root-Handles kommen **24 Punkt-Namen** vor — keine Richtungswörter (LINKS/RECHTS/VORNE/HINTEN) und keine Zahlen-Tripel (`_x_y_z`); die liegen alle in Unterteilen.
9
10	## Aufbau der Namen
11
12	```
13	CS_<PRÄFIX>[_<SEITE>][_<EBENE>][_<WINKEL>][_B]
14	```
15
16	| Bestandteil | Werte | Bedeutung |
17	|---|---|---|
18	| Präfix | `BASE`, `EIN`, `OUT`, `SELECT` | Punkttyp |
19	| Seite | `L`, `R` | linkes / rechtes Feldende (Achse z) |
20	| Ebene | `START` | unten (y = 0) statt Referenzhöhe (y = 1100) |
21	| Winkel | `0`, `90`, `270` | Abgangsrichtung; `_270` nur bei OUT/SELECT, nicht bei EIN |
22	| `_B` | — | zweite/hintere Variante desselben OUT-Punkts |
23
24	## Die vier Typen
25
26	- **`CS_BASE` / `CS_BASE_PRE`** — Anker. In **allen 889** Teilen. Über diesen Punkt wird das Teil hingestellt.
27	- **`CS_EIN_*`** — Einbau-/Kupplungspunkte für die Feldverkettung. Nur in **153** Teilen (17 %, die verkettbaren Felder).
28	- **`CS_OUT_*`** — Umrisspunkte. Nur in denselben 153 Teilen.
29	- **`CS_SELECT_*`** — Auswahlpunkte (Selektionsbox). Nur in denselben 153 Teilen.
30
31	## Semantik der Kupplungspunkte (belegt)
32
33	- **`L` / `R`** = die beiden Enden entlang z. Links bei z ≈ 0, rechts bei z ≈ −Feldbreite. Der R-Punkt trägt zusätzlich 180°-Drehung (blickt entgegengesetzt).
34	- **`START`** = keine Seite, sondern die Höhe: `START_x` sitzt unten (y = 0), `CS_EIN_x` auf Referenzhöhe (y = 1100). Beide Seiten haben je einen START- und einen Höhenpunkt.
35	- **`_90`** = kein 90°-Drehung des Punkts, sondern seitlicher Versatz um die **Steherbreite (45 mm)** in −x — Andockstelle für ein rechtwinklig ankommendes Teil.
36	- Ein `_270` (Abgang nach hinten) existiert bei `CS_EIN` **nicht**, nur bei den Hilfspunkten `CS_OUT`/`CS_SELECT`.
37
38	Die Benennung ist regelmäßig und L/R-symmetrisch: jeder der 153 Kupplungsteile trägt exakt denselben Satz.
39
40	## Gültigkeit — Abstand zur echten Geometrie
41
42	Gemessen transformationskorrekt gegen die echte Bounding-Box (die 5–21-m-Ausreißer der Kappen-Unterteile ausgenommen). Median über die jeweiligen Teile.
43
44	| Punkt | Anzahl | Median | Max | > 100 mm weg |
45	|---|---:|---:|---:|---:|
46	| `CS_BASE` / `CS_BASE_PRE` | 889 | 0 | 271 | 2 % |
47	| `CS_EIN_START_L` | 153 | 0 | 0 | 0 % |
48	| `CS_EIN_L` | 153 | 0 | 0 | 0 % |
49	| `CS_EIN_L_90` | 8 | 21 | 21 | 0 % |
50	| `CS_OUT_L_*` | 153 | 0–30 | 71 | 0 % |
51	| `CS_SELECT_L_270` | 153 | 0 | 220 | 1 % |
52	| **`CS_EIN_START_R`** | 153 | **883** | 1928 | 93 % |
53	| **`CS_EIN_R`** | 153 | **863** | 1908 | 93 % |
54	| **`CS_EIN_R_90`** | 8 | **1019** | 1019 | 100 % |
55	| **`CS_OUT_R_*`** | 153 | **863–913** | 1958 | 93–96 % |
56	| **`CS_SELECT_R_*`** | 153 | **883–1183** | 2228 | 94–100 % |
57
58	(`CS_SELECT_L_0` / `_L_90` liegen mit ~150–300 mm bewusst etwas außerhalb — Selektionsbox.)
59
60	## Befund
61
62	- **Anker (`CS_BASE`) korrekt** in allen Teilen → Hinstellen funktioniert überall.
63	- **Linke Seite komplett korrekt**: `START_L`, `L`, `L_90`, alle `OUT_L`/`SELECT_L`.
64	- **Rechte Seite durchgängig versetzt**: **jeder** R-Punkt (`EIN`, `OUT`, `SELECT`) liegt um rund eine **Feldbreite (~860–1900 mm)** daneben. Das ist ein einzelner systematischer Fehler — die rechte Seite steht im falschen Ursprung, um das Feldmaß verschoben.
65
66	## Konsequenz
67
68	- Hinstellen (`CS_BASE`): geht.
69	- Geregelter Einbau nach links (eingehende Seite, `START_L`/`L`): geht.
70	- Geregelter Einbau nach rechts / Weiterreihen (`START_R`/`R`/`R_90`): **kaputt**, solange die rechte Seite nicht um das Feldmaß zurückkorrigiert wird.
71	- Die Nomenklatur selbst ist konsistent; der Fehler steckt nicht in den Namen, sondern in den Koordinaten der R-Punkte.
72

# Nomenklatur der Root-Handle-Punkte — schutzzaun_basic

Quelle: `katalog_fix3.zip`, Familie `schutzzaun_basic`, 889 Entities.
Betrachtet werden **ausschließlich die Punkte auf dem Root-Handle** (Transform `key == "root"`). Punkte in Unterteilen sind hier nicht enthalten.

## Umfang

Auf Root-Handles kommen **24 Punkt-Namen** vor — keine Richtungswörter (LINKS/RECHTS/VORNE/HINTEN) und keine Zahlen-Tripel (`_x_y_z`); die liegen alle in Unterteilen.

## Aufbau der Namen

```
CS_<PRÄFIX>[_<SEITE>][_<EBENE>][_<WINKEL>][_B]
```

| Bestandteil | Werte | Bedeutung |
|---|---|---|
| Präfix | `BASE`, `EIN`, `OUT`, `SELECT` | Punkttyp |
| Seite | `L`, `R` | linkes / rechtes Feldende (Achse z) |
| Ebene | `START` | unten (y = 0) statt Referenzhöhe (y = 1100) |
| Winkel | `0`, `90`, `270` | Abgangsrichtung; `_270` nur bei OUT/SELECT, nicht bei EIN |
| `_B` | — | zweite/hintere Variante desselben OUT-Punkts |

## Die vier Typen

- **`CS_BASE` / `CS_BASE_PRE`** — Anker. In **allen 889** Teilen. Über diesen Punkt wird das Teil hingestellt.
- **`CS_EIN_*`** — Einbau-/Kupplungspunkte für die Feldverkettung. Nur in **153** Teilen (17 %, die verkettbaren Felder).
- **`CS_OUT_*`** — Umrisspunkte. Nur in denselben 153 Teilen.
- **`CS_SELECT_*`** — Auswahlpunkte (Selektionsbox). Nur in denselben 153 Teilen.

## Semantik der Kupplungspunkte (belegt)

- **`L` / `R`** = die beiden Enden entlang z. Links bei z ≈ 0, rechts bei z ≈ −Feldbreite. Der R-Punkt trägt zusätzlich 180°-Drehung (blickt entgegengesetzt).
- **`START`** = keine Seite, sondern die Höhe: `START_x` sitzt unten (y = 0), `CS_EIN_x` auf Referenzhöhe (y = 1100). Beide Seiten haben je einen START- und einen Höhenpunkt.
- **`_90`** = kein 90°-Drehung des Punkts, sondern seitlicher Versatz um die **Steherbreite (45 mm)** in −x — Andockstelle für ein rechtwinklig ankommendes Teil.
- Ein `_270` (Abgang nach hinten) existiert bei `CS_EIN` **nicht**, nur bei den Hilfspunkten `CS_OUT`/`CS_SELECT`.

Die Benennung ist regelmäßig und L/R-symmetrisch: jeder der 153 Kupplungsteile trägt exakt denselben Satz.

## Gültigkeit — Abstand zur echten Geometrie

Gemessen transformationskorrekt gegen die echte Bounding-Box (die 5–21-m-Ausreißer der Kappen-Unterteile ausgenommen). Median über die jeweiligen Teile.

| Punkt | Anzahl | Median | Max | > 100 mm weg |
|---|---:|---:|---:|---:|
| `CS_BASE` / `CS_BASE_PRE` | 889 | 0 | 271 | 2 % |
| `CS_EIN_START_L` | 153 | 0 | 0 | 0 % |
| `CS_EIN_L` | 153 | 0 | 0 | 0 % |
| `CS_EIN_L_90` | 8 | 21 | 21 | 0 % |
| `CS_OUT_L_*` | 153 | 0–30 | 71 | 0 % |
| `CS_SELECT_L_270` | 153 | 0 | 220 | 1 % |
| **`CS_EIN_START_R`** | 153 | **883** | 1928 | 93 % |
| **`CS_EIN_R`** | 153 | **863** | 1908 | 93 % |
| **`CS_EIN_R_90`** | 8 | **1019** | 1019 | 100 % |
| **`CS_OUT_R_*`** | 153 | **863–913** | 1958 | 93–96 % |
| **`CS_SELECT_R_*`** | 153 | **883–1183** | 2228 | 94–100 % |

(`CS_SELECT_L_0` / `_L_90` liegen mit ~150–300 mm bewusst etwas außerhalb — Selektionsbox.)

## Befund

- **Anker (`CS_BASE`) korrekt** in allen Teilen → Hinstellen funktioniert überall.
- **Linke Seite komplett korrekt**: `START_L`, `L`, `L_90`, alle `OUT_L`/`SELECT_L`.
- **Rechte Seite durchgängig versetzt**: **jeder** R-Punkt (`EIN`, `OUT`, `SELECT`) liegt um rund eine **Feldbreite (~860–1900 mm)** daneben. Das ist ein einzelner systematischer Fehler — die rechte Seite steht im falschen Ursprung, um das Feldmaß verschoben.

## Konsequenz

- Hinstellen (`CS_BASE`): geht.
- Geregelter Einbau nach links (eingehende Seite, `START_L`/`L`): geht.
- Geregelter Einbau nach rechts / Weiterreihen (`START_R`/`R`/`R_90`): **kaputt**, solange die rechte Seite nicht um das Feldmaß zurückkorrigiert wird.
- Die Nomenklatur selbst ist konsistent; der Fehler steckt nicht in den Namen, sondern in den Koordinaten der R-Punkte.


These memory docs are highly relevant to the refactoring. Let me read the architecture/registry ones.


<system-reminder>This memory is 6 days old. Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated. Verify against current code before asserting as fact.</system-reminder>
1	---
2	name: cpq-configurator-modul-registry
3	description: "GEKO-76/16-Konzept: Modul-Registry im cpq-Configurator — Injection über DefaultCommandProcessor, Hook nach replay(), Heilung = Ableitung, Overrides = Events"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 8d7f85b8-4927-4426-9e04-a8be7def3b5e
8	---
9
10	Konzept-Entscheidung (2026-07-03) für die cpq/geko-Abgrenzung der Schutzzaun-Tickets GEKO-76 (Steher-Logik) und GEKO-16 (Zaun zwischen zwei Punkten):
11
12	- **Registry-Ort:** `packages/frontend/src/configurator/` (neues Verzeichnis `modules/`), kein neues Package. cpq bleibt domänen-agnostisch; geko liefert Module (Steher, Zaun-Zug) von außen.
13	- **Injection:** Konstruktor des `DefaultCommandProcessor`; `Fire3D.vue` nimmt bereits `props.processor` entgegen — die geko-App baut den Processor mit ihren Modulen und reicht ihn durch.
14	- **Zentraler Hook:** nach `groups.replay(...)` in `DefaultCommandProcessor.replay()` — einziger Engpass für Live, Undo/Redo und Placement; Module können Live/Replay nicht auseinanderlaufen lassen.
15	- **Semantik:** Selbstheilung (Bauform A–G, Höhe) ist reine Ableitung aus der Topologie (`occupiedPmis()`, `Edge.rot`) und landet nie im EventStore; manuelle Detailmenü-Overrides sind echte, undo-bare Events (Modul-eigene Event-Typen).
16	- **Mit aufzuräumen:** hartkodierte `.name-CS_EIN_*`-Selektoren im `DefaultCommandProcessor` → Placement-Konfiguration des Moduls; Löschen ist im Processor noch unbehandelt → Kaskaden-Hook von Anfang an als Modul-Vertrag entwerfen.
17	- Ticket-Quellen lokal: `~/claude-projects/robotunits/GEKO-76.xml`, `GEKO-16.xml`; DrawIO-Abgrenzung: `GEKO-76-steher-cpq-geko.drawio`.
18
19	Siehe [[cpq-verbau-architektur]], [[cpq-verbau-90grad-steher]].
20

---
name: cpq-configurator-modul-registry
description: "GEKO-76/16-Konzept: Modul-Registry im cpq-Configurator — Injection über DefaultCommandProcessor, Hook nach replay(), Heilung = Ableitung, Overrides = Events"
metadata:
  node_type: memory
  type: project
  originSessionId: 8d7f85b8-4927-4426-9e04-a8be7def3b5e
---

Konzept-Entscheidung (2026-07-03) für die cpq/geko-Abgrenzung der Schutzzaun-Tickets GEKO-76 (Steher-Logik) und GEKO-16 (Zaun zwischen zwei Punkten):

- **Registry-Ort:** `packages/frontend/src/configurator/` (neues Verzeichnis `modules/`), kein neues Package. cpq bleibt domänen-agnostisch; geko liefert Module (Steher, Zaun-Zug) von außen.
- **Injection:** Konstruktor des `DefaultCommandProcessor`; `Fire3D.vue` nimmt bereits `props.processor` entgegen — die geko-App baut den Processor mit ihren Modulen und reicht ihn durch.
- **Zentraler Hook:** nach `groups.replay(...)` in `DefaultCommandProcessor.replay()` — einziger Engpass für Live, Undo/Redo und Placement; Module können Live/Replay nicht auseinanderlaufen lassen.
- **Semantik:** Selbstheilung (Bauform A–G, Höhe) ist reine Ableitung aus der Topologie (`occupiedPmis()`, `Edge.rot`) und landet nie im EventStore; manuelle Detailmenü-Overrides sind echte, undo-bare Events (Modul-eigene Event-Typen).
- **Mit aufzuräumen:** hartkodierte `.name-CS_EIN_*`-Selektoren im `DefaultCommandProcessor` → Placement-Konfiguration des Moduls; Löschen ist im Processor noch unbehandelt → Kaskaden-Hook von Anfang an als Modul-Vertrag entwerfen.
- Ticket-Quellen lokal: `~/claude-projects/robotunits/GEKO-76.xml`, `GEKO-16.xml`; DrawIO-Abgrenzung: `GEKO-76-steher-cpq-geko.drawio`.

Siehe [[cpq-verbau-architektur]], [[cpq-verbau-90grad-steher]].


<system-reminder>This memory is 15 days old. Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated. Verify against current code before asserting as fact.</system-reminder>
1	---
2	name: cpq-verbau-architektur
3	description: "adesso-cpq Verbau/Modell-Architektur (Product/Modell/Graph, BLAST-Validierung) + nächster Schritt für morgen früh"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: d789290f-6631-426e-9639-e7ea7a60e8f9
8	---
9
10	Stand der Architektur-Diskussion adesso-cpq (Schutzzaun-Konfigurator). Repos: ~/projects/adesso-cpq (frontend/CPQ), ~/projects/geko-core (Speedmaxx-Backend). Constraint-Typen folgen noch; jetzt erst Infrastruktur.
11
12	## Kernmodell (erarbeitet, trägt)
13	- **Product = zweigeteilt:** (1) Graph mit PMIs/Collidern/Transforms = der „wertlose" Graph (keine Autorität, nur Darstellung + Approximationsdaten); (2) Variablen = Config + Constraints.
14	- **Product = Entity** (BaseEntity, BLAST-validiert). **Konstrukt aus Produkten = auch Entity = Modell.** Rekursiv: Modell = Produkt (Blatt) | Konstrukt aus Modellen. Aus den Entities wird am Ende der wertlose Graph projiziert.
15	- **Quellen (mehrere!):** Speedmaxx ist führend (~60%), daneben eigene Modelle (optimaler gestaltbar) + weitere Lieferanten. Darum muss BLAST gegen ein **normalisiertes** Modell validieren, nicht gegen Quell-Spezifika oder den rohen Graphen. Im Zweifel ein **Converter**, der Speedmaxx & whatever normalisiert.
16
17	## Speedmaxx-Realität (Quelle der Wahrheit, NICHT der cpq-Mock)
18	- Branch geko-core `feature/get-scene-from-speedmaxx`: `executeCommand` → `ExecuteCommandResponse { ui: Record<string,SpeedmaxxVariable>, viewing, error }`.
19	- `ui` = Config+Constraints: `domain.{min,max,step,valuelist,default}`, `required/visible/editable/multivalue/datatype/unit/...`. Hier stecken „≤6m / ≤10 Teile".
20	- `viewing` = die Szene (wertloser Graph). Controller `loadScene` ruft `combo1_cmd`, ProductCategory "Schutzzaun", Variables wizard → gibt `result.viewing`.
21	- Format des Graphen (= Alttool = `assets/scenes/zaun.json`): `sceneGraph_entity { handles[] }`; Hierarchie flach über `component_transform.key/parentKey` (root → asm_root → produkt-node → ~50 Blatt-Teile); `component_pmi` (z.B. CS_SVS/CS_BASIS am Produkt-Knoten) = Andockpunkte; `component_collider`; Länge teils in `scale.z` eingebacken.
22	- **FALSCH in cpq:** `packages/frontend/src/util/speedMaxx2Visual.ts` erfindet `{environmentId, nodes:[{id,label}]}` — gibt es bei Speedmaxx nicht. Muss korrigiert werden.
23
24	## Validierung / BLAST (entschieden)
25	- Variablen → BLAST direkt (per-Produkt + Querregeln).
26	- Geometrie-Constraints (≤10 Teile, Gesamtlänge, PMI-Kompatibilität, Platz/Kollision) stehen NICHT in Variablen → ergeben sich aus PMIs/Collidern/Transforms.
27	- Geometrie-RECHNUNG läuft in der Engine (nicht im BLAST-Kern, der ist geometrie-agnostisch). Aber: **Engine wird AUS BLAST getriggert.** BLAST ist der Orchestrator und reaktiv (Änderung → sofort wirksam), Cache sparse pro Entity-ID → Engine läuft nur für den betroffenen Bauzug.
28	- Zwei Topos-Ebenen: per Einzelteil + konsolidiert (Bauzug). Konsolidierter Topos ruft die Engine, holt Verdikt, validiert.
29	- Hebel = Abhängigkeitsschärfe (welcher Topos hängt woran). Wird über Constraint-Typen geregelt (kommt noch).
30	- Roher Graph geht NIE nach BLAST; nur Variablen + destillierte Graph-Fakten/Verdikte.
31
32	## Schichten (Zielbild)
33	- **UI** spricht nur mit dem **Scene/Model-Service** (Name noch offen), gibt dessen Ergebnis als Graph/Commands an fire.
34	- **Scene/Model-Service** (core): hält Modell/Bauzüge, PMI-Verbindungen, rechnet Constraints, projiziert Modell → Graph. Intern getrennt: Modell/Constraint (entscheidet, kennt Platz) vs. Projektion (stumpf, entscheidet nichts).
35	- **ProductService** (gehört nach **core**, FE-agnostisch): getProduct/updateProduct, liefert nur Einzelteile, **quelle-agnostisch** über die Asset-Abstraktion (datamanager-Connectors in environment.config.json: path+typeKey-Templates; dev=Assets, live=Server). Darf NICHT wissen, woher.
36	- **fire/Graph**: rendert, ist außerhalb unserer Engine interaktiv und arbeitet mit Näherungen (Collider). Wir haben das letzte Wort; nach jeder Interaktion **Vollaustausch** des Graphen.
37	- **Gemeinsame Constraint-Sprache nötig** (3x bestätigt): zwischen zwei gemeldeten Interaktionen handelt fire autonom; es braucht dieselben Constraints. Lösung: **deklarative Daten im Graph** (Collider/PMI/Grenzen), kein geteilter Code. Offen: diese deklarativen Constraints und die exakten BLAST-Regeln müssen aus EINER Quelle stammen, sonst Drift.
38
39	## Bestand an Typen (adesso-cpq)
40	- shared: `BaseEntity { id }`. core: nur `EntityStore`, keine Typen.
41	- blast: `ObjectSchema<T extends BaseEntity>`, `ToposKey`, `BehaviourResolver`, `HintResolver`, `Diagnostic`, `Severity('error'|'warn'|'info')`, `Behaviours`, `Hint`, `FieldState`, `ItemResult`, `BlastValidator`.
42	- visual-runtime: `Visual3D` (setScene/execute/subscribe/camera), `CameraTransformation{translation,rotation,scale}`, `ConnectorConfig`, `Visual3DConfig`, `Observable<T>`.
43	- **Szenegraph-Format-Typen (SceneGraph/Handle/Component, component_transform/pmi/collider) sind HEIMATLOS** — nur in `utils/sceneMerger.ts` (fliegt raus) und als `any` in Fire3D.vue/DefaultCommandProzessor/SceneReplayProcessor. Brauchen EINEN Besitzer (Fundament für Product/Scene/fire).
44	- `utils/sceneMerger.ts`: addProductInstance/mergeScene/generateUniqueIds/positionScene/calculateNextPosition — fliegt raus (falscher Ort `utils`, zu groß, vermischt Mechanik/Platzierung/Instanz). generateUniqueIds vom User als „Mist" markiert.
45
46	## Offene Baustelle aus dieser Session
47	- `packages/frontend/src/services/product-service.ts` habe ich angelegt (getProduct/updateProduct, AssetLoader, lokaler SceneGraph-Typ). User-Kritik: zu nah an UI (gehört nach core), zuviel vorab definiert (Interfaces/Typen ohne Plan). Morgen entscheiden: behalten/verschieben/löschen. NICHT eigenmächtig Typen streuen.
48
49	## MORGEN FRÜH ZUERST
50	1. GENAU auf die **Entities für BLAST** schauen: was ist exakt die normalisierte Entity (Product, Modell). Wahrscheinlich ein **Converter** rein, der Speedmaxx (ui+viewing) und andere Quellen normalisiert. Das MUSS sitzen, sonst Schmerzen.
51	2. Dann Infrastruktur/Schichten verorten (ProductService → core, Format-Typen-Heimat).
52	3. Constraint-Typen erst danach.
53
54	## Arbeitsregeln des Users (hart)
55	- Sehr knapp, exakt das Gefragte, KEINE ungefragten Optionen/Vorschläge/Typen. Eins nach dem anderen, langsam. Nicht 3 Schritte vorausbauen. Keine Antwort mit Rückfrage/Angebot beenden. Kein Claude-Hinweis in Commits. Vor verändernden Aktionen kurz Plan + Go abwarten.
56	- Branch+MR-Workflow (adesso-CLAUDE.md); frontend→core ist paketübergreifend → nur mit explizitem Go. Git-Remotes heron.at: HTTPS+Token (osxkeychain), SSH (Port 22) ist geblockt.
57

---
name: cpq-verbau-architektur
description: "adesso-cpq Verbau/Modell-Architektur (Product/Modell/Graph, BLAST-Validierung) + nächster Schritt für morgen früh"
metadata:
  node_type: memory
  type: project
  originSessionId: d789290f-6631-426e-9639-e7ea7a60e8f9
---

Stand der Architektur-Diskussion adesso-cpq (Schutzzaun-Konfigurator). Repos: ~/projects/adesso-cpq (frontend/CPQ), ~/projects/geko-core (Speedmaxx-Backend). Constraint-Typen folgen noch; jetzt erst Infrastruktur.

## Kernmodell (erarbeitet, trägt)
- **Product = zweigeteilt:** (1) Graph mit PMIs/Collidern/Transforms = der „wertlose" Graph (keine Autorität, nur Darstellung + Approximationsdaten); (2) Variablen = Config + Constraints.
- **Product = Entity** (BaseEntity, BLAST-validiert). **Konstrukt aus Produkten = auch Entity = Modell.** Rekursiv: Modell = Produkt (Blatt) | Konstrukt aus Modellen. Aus den Entities wird am Ende der wertlose Graph projiziert.
- **Quellen (mehrere!):** Speedmaxx ist führend (~60%), daneben eigene Modelle (optimaler gestaltbar) + weitere Lieferanten. Darum muss BLAST gegen ein **normalisiertes** Modell validieren, nicht gegen Quell-Spezifika oder den rohen Graphen. Im Zweifel ein **Converter**, der Speedmaxx & whatever normalisiert.

## Speedmaxx-Realität (Quelle der Wahrheit, NICHT der cpq-Mock)
- Branch geko-core `feature/get-scene-from-speedmaxx`: `executeCommand` → `ExecuteCommandResponse { ui: Record<string,SpeedmaxxVariable>, viewing, error }`.
- `ui` = Config+Constraints: `domain.{min,max,step,valuelist,default}`, `required/visible/editable/multivalue/datatype/unit/...`. Hier stecken „≤6m / ≤10 Teile".
- `viewing` = die Szene (wertloser Graph). Controller `loadScene` ruft `combo1_cmd`, ProductCategory "Schutzzaun", Variables wizard → gibt `result.viewing`.
- Format des Graphen (= Alttool = `assets/scenes/zaun.json`): `sceneGraph_entity { handles[] }`; Hierarchie flach über `component_transform.key/parentKey` (root → asm_root → produkt-node → ~50 Blatt-Teile); `component_pmi` (z.B. CS_SVS/CS_BASIS am Produkt-Knoten) = Andockpunkte; `component_collider`; Länge teils in `scale.z` eingebacken.
- **FALSCH in cpq:** `packages/frontend/src/util/speedMaxx2Visual.ts` erfindet `{environmentId, nodes:[{id,label}]}` — gibt es bei Speedmaxx nicht. Muss korrigiert werden.

## Validierung / BLAST (entschieden)
- Variablen → BLAST direkt (per-Produkt + Querregeln).
- Geometrie-Constraints (≤10 Teile, Gesamtlänge, PMI-Kompatibilität, Platz/Kollision) stehen NICHT in Variablen → ergeben sich aus PMIs/Collidern/Transforms.
- Geometrie-RECHNUNG läuft in der Engine (nicht im BLAST-Kern, der ist geometrie-agnostisch). Aber: **Engine wird AUS BLAST getriggert.** BLAST ist der Orchestrator und reaktiv (Änderung → sofort wirksam), Cache sparse pro Entity-ID → Engine läuft nur für den betroffenen Bauzug.
- Zwei Topos-Ebenen: per Einzelteil + konsolidiert (Bauzug). Konsolidierter Topos ruft die Engine, holt Verdikt, validiert.
- Hebel = Abhängigkeitsschärfe (welcher Topos hängt woran). Wird über Constraint-Typen geregelt (kommt noch).
- Roher Graph geht NIE nach BLAST; nur Variablen + destillierte Graph-Fakten/Verdikte.

## Schichten (Zielbild)
- **UI** spricht nur mit dem **Scene/Model-Service** (Name noch offen), gibt dessen Ergebnis als Graph/Commands an fire.
- **Scene/Model-Service** (core): hält Modell/Bauzüge, PMI-Verbindungen, rechnet Constraints, projiziert Modell → Graph. Intern getrennt: Modell/Constraint (entscheidet, kennt Platz) vs. Projektion (stumpf, entscheidet nichts).
- **ProductService** (gehört nach **core**, FE-agnostisch): getProduct/updateProduct, liefert nur Einzelteile, **quelle-agnostisch** über die Asset-Abstraktion (datamanager-Connectors in environment.config.json: path+typeKey-Templates; dev=Assets, live=Server). Darf NICHT wissen, woher.
- **fire/Graph**: rendert, ist außerhalb unserer Engine interaktiv und arbeitet mit Näherungen (Collider). Wir haben das letzte Wort; nach jeder Interaktion **Vollaustausch** des Graphen.
- **Gemeinsame Constraint-Sprache nötig** (3x bestätigt): zwischen zwei gemeldeten Interaktionen handelt fire autonom; es braucht dieselben Constraints. Lösung: **deklarative Daten im Graph** (Collider/PMI/Grenzen), kein geteilter Code. Offen: diese deklarativen Constraints und die exakten BLAST-Regeln müssen aus EINER Quelle stammen, sonst Drift.

## Bestand an Typen (adesso-cpq)
- shared: `BaseEntity { id }`. core: nur `EntityStore`, keine Typen.
- blast: `ObjectSchema<T extends BaseEntity>`, `ToposKey`, `BehaviourResolver`, `HintResolver`, `Diagnostic`, `Severity('error'|'warn'|'info')`, `Behaviours`, `Hint`, `FieldState`, `ItemResult`, `BlastValidator`.
- visual-runtime: `Visual3D` (setScene/execute/subscribe/camera), `CameraTransformation{translation,rotation,scale}`, `ConnectorConfig`, `Visual3DConfig`, `Observable<T>`.
- **Szenegraph-Format-Typen (SceneGraph/Handle/Component, component_transform/pmi/collider) sind HEIMATLOS** — nur in `utils/sceneMerger.ts` (fliegt raus) und als `any` in Fire3D.vue/DefaultCommandProzessor/SceneReplayProcessor. Brauchen EINEN Besitzer (Fundament für Product/Scene/fire).
- `utils/sceneMerger.ts`: addProductInstance/mergeScene/generateUniqueIds/positionScene/calculateNextPosition — fliegt raus (falscher Ort `utils`, zu groß, vermischt Mechanik/Platzierung/Instanz). generateUniqueIds vom User als „Mist" markiert.

## Offene Baustelle aus dieser Session
- `packages/frontend/src/services/product-service.ts` habe ich angelegt (getProduct/updateProduct, AssetLoader, lokaler SceneGraph-Typ). User-Kritik: zu nah an UI (gehört nach core), zuviel vorab definiert (Interfaces/Typen ohne Plan). Morgen entscheiden: behalten/verschieben/löschen. NICHT eigenmächtig Typen streuen.

## MORGEN FRÜH ZUERST
1. GENAU auf die **Entities für BLAST** schauen: was ist exakt die normalisierte Entity (Product, Modell). Wahrscheinlich ein **Converter** rein, der Speedmaxx (ui+viewing) und andere Quellen normalisiert. Das MUSS sitzen, sonst Schmerzen.
2. Dann Infrastruktur/Schichten verorten (ProductService → core, Format-Typen-Heimat).
3. Constraint-Typen erst danach.

## Arbeitsregeln des Users (hart)
- Sehr knapp, exakt das Gefragte, KEINE ungefragten Optionen/Vorschläge/Typen. Eins nach dem anderen, langsam. Nicht 3 Schritte vorausbauen. Keine Antwort mit Rückfrage/Angebot beenden. Kein Claude-Hinweis in Commits. Vor verändernden Aktionen kurz Plan + Go abwarten.
- Branch+MR-Workflow (adesso-CLAUDE.md); frontend→core ist paketübergreifend → nur mit explizitem Go. Git-Remotes heron.at: HTTPS+Token (osxkeychain), SSH (Port 22) ist geblockt.


1	---
2	name: fire-visual-placement-mode
3	description: "fire-visual Placement-Mode (snap-to-point) — Architektur, Settings, Algorithmus, DropPoints, Commands; Pfade im threed/interaction- und threed/pmi-Modul"
4	metadata:
5	  node_type: memory
6	  type: project
7	  originSessionId: 733f7054-ac76-49cd-ba04-516ef015d4f3
8	---
9
10	**WICHTIG: `fire-visual` ist KEIN eigener Code.** Es ist eine Kopie fremden Codes (Drittanbieter), die nur zum Reverse Engineering / Verstehen dient — nicht ändern, nicht als unser Produkt behandeln. Daraus gebaut wird das Paket `@ad/fire` (Repo `~/projects/geko-fire`, Remote `git.heron.at/pzm/geko/geko-fire.git`, nur Build: `browser/index.js` + `types/`, u.a. `import-fire-visual-modules.d.ts`). `fire-visual` selbst ist kein Git-Repo. adesso-cpq konsumiert `@ad/fire` als Dependency (`packages/frontend/package.json: "@ad/fire": "^1.0.6"`). Änderungen an der Visualisierung/Placement gehören NICHT hier hinein.
11
12	Placement-Mode = snap-to-point-Platzierung eines Subjects über Drag-Points/Drop-Points. Teil des Moduls `@fire-visual/interaction`. Quelle: `~/projects/fire-visual`.
13
14	## Pfade
15	- Mode: `threed/interaction/module/src/interactionMode/placementMode.ts`
16	- Verdrahtung/Command: `threed/interaction/module/src/feature/interactionFeature.ts` (ab Zeile 259)
17	- Options-Typen: `threed/interaction/module/src/options/modeOptions.ts`
18	- Modul-Doku: `threed/interaction/MODULE.md`
19	- Test: `threed/interaction/module/tests/placementMode.test.ts`
20	- DropPoint-Implementierungen (PMI): `threed/pmi/module/src/dropPoint/{pmiDropPoint,planeDropPoint,pointDropPoint,lineDropPoint}.ts`
21	- DropPoint-Factory: `threed/pmi/module/src/component/pmiComponent.ts` → `getDropPoint()` (Zeile 422)
22
23	## Grundbegriffe
24	- **Subject**: das zu platzierende Objekt (`Handle`).
25	- **Drag-Point**: Punkt am Subject (lokaler `transform: Matrix4_4`), an dem „angefasst" wird. Visualisiert als Kugel (Sphere). Aktiv = gelb, inaktiv = blau.
26	- **Drop-Point**: Ziel-Snap-Geometrie in der Szene; liefert über `assessRay` eine Bewertung. Implementiert Interface `DropPoint`.
27	- **activeDragPointIndex**: aktiver Drag-Point; `-1` = „alle Drag-Points" (nur erlaubt wenn `allowAllDragPoints !== false`). Start: `-1`, oder `0` falls `allowAllDragPoints === false`.
28
29	## PlacementModeSettings
30	- `camera: CameraComponent`, `subject: Handle`
31	- `dragPoints[]`: je `{ transform, dropPoints: Partial<DropPointProvider>[], selector? }`
32	- `defaultCursorTransform?` (Fallback = `dragPoints[0].transform`)
33	- `allowAllDragPoints?` (Default true) — false: Index -1 verboten, Cycling hält immer einen konkreten DP aktiv
34	- `allowDragPointCycling?` (Default true) — false: User kann aktiven DP nicht ändern, „alle DP" bleibt aktiv
35	- `maxJumpDistance?` — überschreibt globalen Wert aus `ModeOptionProvider`
36
37	## Globale Optionen (ModeOptionProvider.options.placement, Typ PlacementModeOptions)
38	`maxJumpDistance`, `highlightColor` (Default Color.yellow), `dragPointActiveColor` (Default yellow), `dragPointInactiveColor` (Default blue), `dragPointRadius` (Default 0.1). Plus `gizmoScale` (Default 1) skaliert Kugelradius.
39
40	## Algorithmus processInput(input) je Frame
41	Input: `pointerNormalizedPosition`, `nextDragPoint`, `previousDragPoint`, `interactionClick`.
42	1. Ohne Pointer/Camera → false.
43	2. **Grab-Gating**: Rising edge von `interactionClick` setzt `_grabActive = true`. Solange nicht grab-aktiv: nur prevClick merken, return true (Subject bewegt sich nicht).
44	3. Cycling: bei `nextDragPoint`/`previousDragPoint` Index zyklen (wenn Cycling erlaubt).
45	4. Alle Drop-Point-Visualisierungen zurücksetzen (erst `false`+Highlight null), dann die zum aktiven DP gehörenden aktivieren (Index -1 ⇒ alle).
46	5. Ray von Kamera durch Pointer; Schnitt mit XZ-Ebene (`Plane` durch Ursprung, Normale up) → `intersectionPoint`.
47	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.
48	7. Beste Assessment über `betterRayAssessment`: kleinste `jumpDistance`; bei Gleichstand (Toleranz 0.05) kleinste `rayDistance`.
49	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.
50	9. `_lastPlacement` (PlacementUpdateData) merken: position, rotation, dragPointSelector, dropPointSelector (jeweils null bei XZ-Fallback).
51	10. Drag-Point-Kugeln neu positionieren (`subjectMatrix * dragPoint.transform * 0`), Material gelb/blau je aktiv.
52	11. **Falling edge** von `interactionClick`: `_grabActive = false`, `_lastPlacement` per `emitUpdateEvent` (subject als selectorElementReferenceRepresentation) ausgeben.
53
54	## Cycling-Logik
55	- allowAllDragPoints true: Reihenfolge … last → -1 (alle) → 0 → … (wrap über -1).
56	- allowAllDragPoints false: -1 wird übersprungen, Wrap last → 0.
57
58	## Lifecycle
59	- `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')`.
60	- `stopMode`: dropPoints/dragPointHandles leeren.
61
62	## Command (Environment): start_placement_mode(params)
63	params: `subject: string`, `cursorTransform?`, `allowAllDragPoints?`, `allowDragPointCycling?`, `maxJumpDistance?`, `dragPoints: Array<{ selector?, transform?:{position,rotation}, dropPoints: string[] }>`.
64	Auflösung in interactionFeature:
65	- Subject-Selector → Handle (wirft wenn nicht gefunden).
66	- Pro Drag-Point: `dropPoints`-Selektoren via `selectorEngine.findAll` → DropPointProvider.
67	- Ohne `selector`: ein Drag-Point mit `transform` (aus position/rotation, sonst identity), selector undefined.
68	- Mit `selector`: `findAll(selector, subjectHandle)` → je Sub-Entity ein Drag-Point mit `transform = subject.LTW⁻¹ * dpe.LTW * transform`, selector = `selectorEngine.selector(dpe)`.
69	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`.
70
71	## DropPoint-Interface
72	`assessRay(ray): RayAssessment {jumpDistance, rayDistance, position, rotation}`, `predictDragPointRotation(ray): Matrix4_4`, `setVisualization(state)`, `setVisualizationHighlight(color|null)`, `selector(): string|null`.
73
74	PMI-Implementierungen (`PmiDropPoint` Basis, hält `LTW`, `dragPointRotation = decompose(LTW)[1]`, predictRotation = konstant):
75	- **PointDropPoint** (PMIType point/matrix): position = LTW·0; jumpDistance = Abstand Punkt↔nächster Punkt auf Strahl.
76	- **LineDropPoint** (line): Segment LTW·0 → parentLTW·endpoint; closest segment-on-line.
77	- **PlaneDropPoint** (plane): Ebene aus LTW; Ray-Schnitt, in Ebenen-Koords auf `size`/2 geclampt, zurück nach Welt; jumpDistance = clampedPoint↔rayPoint.
78	`PMIComponent.getDropPoint()` mappt PMIType → Klasse (point/matrix→Point, line→Line, plane→Plane, sonst null).
79
80	Verwandt: [[cpq-verbau-architektur]] (CPQ-seitige Verbau/Platzierung).
81

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

**WICHTIG: `fire-visual` ist KEIN eigener Code.** Es ist eine Kopie fremden Codes (Drittanbieter), die nur zum Reverse Engineering / Verstehen dient — nicht ändern, nicht als unser Produkt behandeln. Daraus gebaut wird das Paket `@ad/fire` (Repo `~/projects/geko-fire`, Remote `git.heron.at/pzm/geko/geko-fire.git`, nur Build: `browser/index.js` + `types/`, u.a. `import-fire-visual-modules.d.ts`). `fire-visual` selbst ist kein Git-Repo. adesso-cpq konsumiert `@ad/fire` als Dependency (`packages/frontend/package.json: "@ad/fire": "^1.0.6"`). Änderungen an der Visualisierung/Placement gehören NICHT hier hinein.

Placement-Mode = snap-to-point-Platzierung eines Subjects über Drag-Points/Drop-Points. Teil des Moduls `@fire-visual/interaction`. Quelle: `~/projects/fire-visual`.

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


504	    private apply(event: GroupEvent): void {
505	        switch (event.type) {
506	            case "group-create": {
507	                const cloned = event.handles.map((h) => structuredClone(h));
508	                for (const handle of cloned) this.handles.set(handle.id, handle);
509	                this.elementHandles.set(event.rootHandleId, cloned);
510	                this.groups.set(event.groupId, {
511	                    id: event.groupId,
512	                    key: event.groupKey,
513	                    members: [event.rootHandleId],
514	                    edges: [],
515	                    transform: { pos: event.position, rot: event.rotation },
516	                });
517	                this.elementToGroup.set(event.rootHandleId, event.groupId);
518	                break;
519	            }
520	            case "group-merge": {
521	                const target = this.groups.get(event.targetGroupId);
522	                const moving = this.groups.get(event.movingGroupId);
523	                if (!target || !moving) break;
524	                // A docking point must never be booked twice. openEndExclusions already keeps the
525	                // placement UI off occupied points, but guard here too: if the anchor or join point is
526	                // already in use, drop the merge rather than let a double-booked edge into the graph.
527	                const occupied = this.occupiedPmis();
528	                const busy = (element: string, pmi: string) =>
529	                    occupied.some((o) => o.element === element && o.pmi === pmi);
530	                if (busy(event.anchorRootId, event.anchorPmiId) || busy(event.joinRootId, event.joinPmiId)) break;
531	                target.members.push(...moving.members);
532	                target.edges.push(...moving.edges);
533	                target.edges.push({
534	                    from: event.anchorRootId,
535	                    to: event.joinRootId,
536	                    fromPmi: event.anchorPmiId,
537	                    toPmi: event.joinPmiId,
538	                    rot: event.rot,
539	                });
540	                for (const member of moving.members) this.elementToGroup.set(member, target.id);
541	                this.groups.delete(event.movingGroupId);
542	                break;
543	            }
544	            case "group-move":
545	            case "group-rotate": {
546	                const g = this.groups.get(event.groupId);
547	                if (!g) break;
548	                g.transform = { pos: event.position, rot: event.rotation };
549	                break;
550	            }
551	            case "group-detach": {
552	                if (!this.groups.has(event.sourceGroupId)) break;
553	                for (const ng of event.newGroups) {
554	                    this.groups.set(ng.groupId, {
555	                        id: ng.groupId,
556	                        key: ng.groupKey,
557	                        members: [...ng.members],
558	                        edges: ng.edges.map((e) => ({ ...e })),
559	                        transform: ng.transform,
560	                    });
561	                    for (const m of ng.members) this.elementToGroup.set(m, ng.groupId);
562	                }
563	                this.groups.delete(event.sourceGroupId);

    private apply(event: GroupEvent): void {
        switch (event.type) {
            case "group-create": {
                const cloned = event.handles.map((h) => structuredClone(h));
                for (const handle of cloned) this.handles.set(handle.id, handle);
                this.elementHandles.set(event.rootHandleId, cloned);
                this.groups.set(event.groupId, {
                    id: event.groupId,
                    key: event.groupKey,
                    members: [event.rootHandleId],
                    edges: [],
                    transform: { pos: event.position, rot: event.rotation },
                });
                this.elementToGroup.set(event.rootHandleId, event.groupId);
                break;
            }
            case "group-merge": {
                const target = this.groups.get(event.targetGroupId);
                const moving = this.groups.get(event.movingGroupId);
                if (!target || !moving) break;
                // A docking point must never be booked twice. openEndExclusions already keeps the
                // placement UI off occupied points, but guard here too: if the anchor or join point is
                // already in use, drop the merge rather than let a double-booked edge into the graph.
                const occupied = this.occupiedPmis();
                const busy = (element: string, pmi: string) =>
                    occupied.some((o) => o.element === element && o.pmi === pmi);
                if (busy(event.anchorRootId, event.anchorPmiId) || busy(event.joinRootId, event.joinPmiId)) break;
                target.members.push(...moving.members);
                target.edges.push(...moving.edges);
                target.edges.push({
                    from: event.anchorRootId,
                    to: event.joinRootId,
                    fromPmi: event.anchorPmiId,
                    toPmi: event.joinPmiId,
                    rot: event.rot,
                });
                for (const member of moving.members) this.elementToGroup.set(member, target.id);
                this.groups.delete(event.movingGroupId);
                break;
            }
            case "group-move":
            case "group-rotate": {
                const g = this.groups.get(event.groupId);
                if (!g) break;
                g.transform = { pos: event.position, rot: event.rotation };
                break;
            }
            case "group-detach": {
                if (!this.groups.has(event.sourceGroupId)) break;
                for (const ng of event.newGroups) {
                    this.groups.set(ng.groupId, {
                        id: ng.groupId,
                        key: ng.groupKey,
                        members: [...ng.members],
                        edges: ng.edges.map((e) => ({ ...e })),
                        transform: ng.transform,
                    });
                    for (const m of ng.members) this.elementToGroup.set(m, ng.groupId);
                }
                this.groups.delete(event.sourceGroupId);


Ich habe alle vier Punkte gefunden. Hier die strukturierten Fakten zur Codestruktur.

---

## 1. DefaultCommandProcessor

**Datei:** `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts` (659 Zeilen)
Basisklasse/Interface: `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/CommandProcessor.ts` (Z. 3-11) — nur zwei überschreibbare Methoden `setInitialScene(scene)` und `handleEvent(event): UIReaction[]`.

**Klasse** (Z. 13-29):
```ts
export class DefaultCommandProcessor extends CommandProcessor {
    private eventStore = new EventStore();
    private groups = new GroupProcessor();
    private selection: { pickedId: string; level: number }[] = [];
    private pendingRegroup: GroupEvent | null = null;
    constructor(private readonly dockingRules?: DockingRules) { super(); }
```

**Zentrales Dispatch** ist ein `switch` über `event.eventTypeName` in `handleEvent` (Z. 43-74), das an private `handle*`-Methoden delegiert. Event-Typen: `interaction-mode`, `add-product`, `keyboard-build`, `detach-element`, `escalate-selection`, `prepare-action`, `undo`, `redo`, `esc`, `context-click`, `selection-click`, `selection`.

**Kern-Engpass** `private replay()` (Z. 35-41): ruft `groups.replay(initialState, events)` — laut Memory der geplante Hook-Punkt für Module.

**Registrierungs-Mechanismus:** Aktuell **keiner**. Es gibt kein `register`/`use`/`addHandler`. Erweiterung läuft heute nur über zwei Wege: (a) das feste `switch`, (b) den Konstruktor-Parameter `dockingRules` (reine Daten-Injektion). Der geplante Registry-Ansatz ist bisher nur konzeptionell dokumentiert (siehe Punkt 3 / Memory `cpq-configurator-modul-registry.md`), nicht im Code.

**Tests:** `DefaultCommandProcessor.test.ts` (gleicher Ordner).

---

## 2. Dreipunkt-/Placement-Logik

Diese Logik ist **verteilt auf drei Ebenen**:

**a) Fremdcode (`@ad/fire`, nicht editieren):** Der eigentliche Snap-to-Point-Algorithmus `processInput` und die `PmiDropPoint`-Klassen liegen im Drittanbieter-Paket, gebaut nach `/Users/marcus.hinz/projects/geko-fire/browser/index.js`. Vollständige Beschreibung in Memory `/Users/marcus.hinz/claude-projects/robotunits/memory/fire-visual-placement-mode.md` (Placement-Mode, Drag-/Drop-Points, `pmiDropPoint`, Command `start_placement_mode`).

**b) CPQ-Steuerung im DefaultCommandProcessor** (eingebettet, nicht separat):
- `handleInteractionMode` (Z. 307-321) mit `case "placement": return this.handlePlacementEnd(event)`
- `handlePlacementEnd` (Z. 408-422)
- `placementEvent` (Z. 457-488): erzeugt bei Snap einen `group-merge` (Dreipunkt: anchorPmi ← dropPoint, joinPmi ← dragPoint, plus `rot` aus `dockRotation`), sonst `group-move`
- `handleDetachElement` (Z. 426-454): löst Teil aus Gruppe, startet `start_placement_mode`
- `placementDragPoints` (Z. 609-636): baut Drag-/Drop-Selektoren (`.name-CS_EIN_*` / `#<id> .name-<dp>`) aus den `dockingRules`
- Zusätzlicher „Anbau"-Weg per Klick auf Select-Marker: `handleSelectionClickEvent` (Z. 157-187) und `handleMarkerBuild` (Z. 193-226) — dockt eine Kopie an `CS_OUT_*`/`CS_EIN_*`; sowie Prototyp `handleKeyboardBuild` (Z. 108-155)
- Datenextraktion in `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/eventData.ts`: `extractPlacementData` (Z. 16-31, liest `dragPointSelector`/`dropPointSelector`, `lastHandleId`/`lastPmiId`).

**c) Geometrie/Dreipunkt-Mathematik im GroupProcessor:** `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/model/GroupProcessor.ts`
- `dockRotation(anchorRoot, anchorPmi, joinRoot, joinPmi)` (Z. 763-765) und `pmiRotation`/`pmiLocal` (Z. 736-758)
- `computeLayout` (Z. 620-670): der eigentliche Rigid-Transform-Solver über den Kanten-Graph (Punkt-Koinzidenz + Relativrotation)
- `apply`/`case "group-merge"` (Z. 520-542): fügt die Edge (from/to/fromPmi/toPmi/rot) ein
- `occupiedPmis`/`blockedPmis` (Z. 203-255): welche PMIs belegt/gesperrt sind (Junction-Regel für 90°/270°-Steher).

Der Begriff „Dreipunkt/three-point" taucht als String **nicht** im Code auf; die Logik firmiert als „placement / snap / dock / merge".

---

## 3. PMI ↔ reiner Einbau (CPQ-Seite)

**dockingRules** ist der Vertrag zwischen domänen-agnostischem CPQ und dem geko-Client:
- Typdefinition: `DockingRules = Record<string, Record<string, Record<string, string[]>>>` in `DefaultCommandProcessor.ts` Z. 8-11 (movingClass → targetClass → dragPoint → dropPoints[]); Re-Export in `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/index.ts` Z. 18.
- Konsumiert nur in `placementDragPoints` (Z. 609-636).
- Injektion über Prop: `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/components/visual/ProductConfigurator.vue` (Z. 5-17: `new DefaultCommandProcessor(props.dockingRules)`).

**„Reiner Einbau über PMIs"** = die geregelte Feldverkettung über die `CS_EIN_*`/`CS_OUT_*`-Punkte. Diese ist nicht als eigener CPQ-Codepfad implementiert, sondern ergibt sich aus (a) den Docking-Regeln (Daten) und (b) der PMI-Geometrie im Katalog. Dokumentiert/spezifiziert in:
- `/Users/marcus.hinz/claude-projects/robotunits/katalog-schutzzaun_basic-root-nomenklatur.md` — PMI-Nomenklatur `CS_BASE/EIN/OUT/SELECT`, Semantik L/R/START/_90, und der Befund, dass der „geregelte Einbau nach rechts" wegen versetzter R-Punkt-Koordinaten kaputt ist (Z. 26-71).
- Katalog-Rohdaten (PMI-Definitionen `component_pmi`): `/Users/marcus.hinz/claude-projects/robotunits/katalog/{schutzzaun_basic,schutzzaun_allround,rollenfoerderer}/*.json`.
- Architektur-/Abgrenzungskonzept (CPQ vs. geko, PMI-Verbindungen, geplante Modul-Registry als Ort für „reinen Einbau"): Memories `/Users/marcus.hinz/claude-projects/robotunits/memory/cpq-verbau-architektur.md`, `cpq-verbau-90grad-steher.md`, `cpq-configurator-modul-registry.md`.

Wichtig laut Memory `cpq-configurator-modul-registry.md` (Z. 12-16): Die hartkodierten `.name-CS_EIN_*`-Selektoren im DefaultCommandProcessor sollen in eine Modul-Placement-Konfiguration ausgelagert werden; Selbstheilung/Ableitung soll nie in den EventStore, manuelle Overrides schon (als undo-bare Modul-Events). Das ist die Refactoring-Stoßrichtung — aktuell nur konzeptionell.

---

## 4. Storybook-Stories mit CommandProcessor / Placement

Zwei relevante Story-Dateien (beide `/Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/components/visual/`):

- **`Fire3d.stories.ts`** — importiert und instanziiert direkt `new DefaultCommandProcessor()` (Z. 4, 22, 40). Zwei Stories: `Default` (Z. 18-34, lädt Szene `SBS1044PLA.json`) und `MultipleZaun` (Z. 36-70, `add-product`-Dispatch). Kein Docking (kein dockingRules-Arg).

- **`ProductConfigurator.stories.ts`** — importiert Typ `DockingRules` (Z. 5), lädt `catalog.json` + `docking-rules.json` per fetch (Z. 22-31) und reicht sie als Prop `:docking-rules` an `ProductConfigurator.vue` (das intern den Processor baut). Story `Configurator` (Z. 17-50). Das ist die Story, die die vollständige Placement/Docking-Kette abbildet.

Weitere `.stories.*` im Projekt betreffen nur UI-Basiskomponenten (BaseButton, Inputs, Dialoge etc.) und sind für den CommandProcessor/Placement irrelevant.