import { describe, it, expect } from "vitest";
import { DefaultCommandProcessor } from "../events/DefaultCommandProcessor";
import { GekoPlacementModule, type SteherRules, type SteherPartResolver } from "./GekoPlacementModule";
import productData from "../../../.storybook/products/schutzzaun_basic/SBS1044PRA.json";

// The fixture is a schutzzaun_basic-tuer. It stands in as both the anchor class and (via the resolver,
// reusing the same scene) the derived "Steher" part: a Steher is auto-docked onto every tuer's open
// CS_OUT_R_0 end via CS_EIN_L. That is enough to exercise the derivation machinery end-to-end.
const RULES: SteherRules = [{ anchorClass: "schutzzaun_basic-tuer", joins: { CS_OUT_R_0: "CS_EIN_L" } }];

// Stand-in resolver: geko owns part choice. Here it returns the fixture scene, or null when the slot's
// override says "suppress".
const resolver: SteherPartResolver = (ctx) => (ctx.override === "suppress" ? null : productData);

const product = () => structuredClone(productData);
const base = () => ({ _resourceClass: "sceneGraph_entity", handles: [] as any[] });
const sceneOf = (r: any[]) => (r.find((x: any) => x.cmd === "setScene") as any)?.args[0];
const parts = (scene: any): string[] =>
    scene.handles.filter((h: any) => (h.tags ?? []).includes("role-part")).map((h: any) => h.id);
const groupNodeOf = (scene: any, part: string) =>
    scene.handles.find((h: any) => h.id === part).components.find((c: any) => c._resourceClass === "component_transform")
        .parentKey;
const steherOf = (scene: any) => parts(scene).find((id) => id.startsWith("h-steher"));
const anchorOf = (scene: any) => parts(scene).find((id) => !id.startsWith("h-steher"));

function setup() {
    const proc = new DefaultCommandProcessor();
    proc.register(new GekoPlacementModule({}, RULES, resolver));
    proc.setInitialScene(base());
    const scene = sceneOf(proc.handleEvent({ eventTypeName: "add-product", handles: product().handles }));
    const anchor = anchorOf(scene)!;
    const slotId = `steher@${anchor}:CS_OUT_R_0`;
    return { proc, scene, anchor, slotId };
}

describe("Steher derivation", () => {
    it("derives a Steher coupled to the anchor's group", () => {
        const { scene, anchor } = setup();
        const p = parts(scene);
        expect(p).toHaveLength(2); // anchor + derived Steher
        const steher = steherOf(scene)!;
        // Coupled: the Steher sits in the anchor's group node → a group-move on the base carries it along.
        expect(groupNodeOf(scene, steher)).toBe(groupNodeOf(scene, anchor));
    });

    it("is never recorded — no undo step exists for the derived Steher", () => {
        const { proc, scene } = setup();
        expect(parts(scene)).toHaveLength(2);
        // Only the add-product is on the stack; undo removes the anchor (and with it the derived Steher).
        const back = sceneOf(proc.handleEvent({ eventTypeName: "undo" }));
        expect(parts(back)).toHaveLength(0);
    });

    it("a suppress override removes the Steher and is undo-able", () => {
        const { proc, slotId } = setup();
        const suppressed = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true }));
        expect(parts(suppressed)).toHaveLength(1); // Steher gone, anchor stays
        const undone = sceneOf(proc.handleEvent({ eventTypeName: "undo" }));
        expect(parts(undone)).toHaveLength(2); // override reverted → derived again
    });

    it("clearing the override brings the Steher back with the SAME id (deterministic across replays)", () => {
        const { proc, scene, slotId } = setup();
        const steherId = steherOf(scene)!;
        proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true });
        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId })); // payload → clear
        expect(parts(cleared)).toHaveLength(2);
        expect(steherOf(cleared)).toBe(steherId); // re-derived with identical id, not churned
    });
});

-        const suppressed = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true }));

+        const suppressed = sceneOf(proc.handleEvent({ eventTypeName: "steher-override", slotId, payload: "suppress" }));


Edit

        proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true });
        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId })); // payload → clear

        proc.handleEvent({ eventTypeName: "steher-override", slotId, payload: "suppress" });
        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-override", slotId })); // payload absent → clear


The file /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/modules/GekoSteher.test.ts has been updated successfully. (file state is current in your context — no need to Read it back)

        proc.handleEvent({ eventTypeName: "steher-override", slotId, payload: "suppress" });
        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-override", slotId })); // payload absent → clear

import { describe, it, expect } from "vitest";
import { DefaultCommandProcessor } from "../events/DefaultCommandProcessor";
import { GekoPlacementModule, type SteherRules, type SteherPartResolver } from "./GekoPlacementModule";
import productData from "../../../.storybook/products/schutzzaun_basic/SBS1044PRA.json";

// The fixture is a schutzzaun_basic-tuer. It stands in as both the anchor class and (via the resolver,
// reusing the same scene) the derived "Steher" part: a Steher is auto-docked onto every tuer's open
// CS_OUT_R_0 end via CS_EIN_L. That is enough to exercise the derivation machinery end-to-end.
const RULES: SteherRules = [{ anchorClass: "schutzzaun_basic-tuer", joins: { CS_OUT_R_0: "CS_EIN_L" } }];

// Stand-in resolver: geko owns part choice. Here it returns the fixture scene, or null when the slot's
// override says "suppress".
const resolver: SteherPartResolver = (ctx) => (ctx.override === "suppress" ? null : productData);

const product = () => structuredClone(productData);
const base = () => ({ _resourceClass: "sceneGraph_entity", handles: [] as any[] });
const sceneOf = (r: any[]) => (r.find((x: any) => x.cmd === "setScene") as any)?.args[0];
const parts = (scene: any): string[] =>
    scene.handles.filter((h: any) => (h.tags ?? []).includes("role-part")).map((h: any) => h.id);
const groupNodeOf = (scene: any, part: string) =>
    scene.handles.find((h: any) => h.id === part).components.find((c: any) => c._resourceClass === "component_transform")
        .parentKey;
const steherOf = (scene: any) => parts(scene).find((id) => id.startsWith("h-steher"));
const anchorOf = (scene: any) => parts(scene).find((id) => !id.startsWith("h-steher"));

function setup() {
    const proc = new DefaultCommandProcessor();
    proc.register(new GekoPlacementModule({}, RULES, resolver));
    proc.setInitialScene(base());
    const scene = sceneOf(proc.handleEvent({ eventTypeName: "add-product", handles: product().handles }));
    const anchor = anchorOf(scene)!;
    const slotId = `steher@${anchor}:CS_OUT_R_0`;
    return { proc, scene, anchor, slotId };
}

describe("Steher derivation", () => {
    it("derives a Steher coupled to the anchor's group", () => {
        const { scene, anchor } = setup();
        const p = parts(scene);
        expect(p).toHaveLength(2); // anchor + derived Steher
        const steher = steherOf(scene)!;
        // Coupled: the Steher sits in the anchor's group node → a group-move on the base carries it along.
        expect(groupNodeOf(scene, steher)).toBe(groupNodeOf(scene, anchor));
    });

    it("is never recorded — no undo step exists for the derived Steher", () => {
        const { proc, scene } = setup();
        expect(parts(scene)).toHaveLength(2);
        // Only the add-product is on the stack; undo removes the anchor (and with it the derived Steher).
        const back = sceneOf(proc.handleEvent({ eventTypeName: "undo" }));
        expect(parts(back)).toHaveLength(0);
    });

    it("a suppress override removes the Steher and is undo-able", () => {
        const { proc, slotId } = setup();
        const suppressed = sceneOf(proc.handleEvent({ eventTypeName: "steher-override", slotId, payload: "suppress" }));
        expect(parts(suppressed)).toHaveLength(1); // Steher gone, anchor stays
        const undone = sceneOf(proc.handleEvent({ eventTypeName: "undo" }));
        expect(parts(undone)).toHaveLength(2); // override reverted → derived again
    });

    it("clearing the override brings the Steher back with the SAME id (deterministic across replays)", () => {
        const { proc, scene, slotId } = setup();
        const steherId = steherOf(scene)!;
        proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true });
        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId })); // payload → clear
        expect(parts(cleared)).toHaveLength(2);
        expect(steherOf(cleared)).toBe(steherId); // re-derived with identical id, not churned
    });
});

-        proc.handleEvent({ eventTypeName: "steher-set-part", slotId, suppressed: true });

-        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-set-part", slotId })); // payload → clear

+        proc.handleEvent({ eventTypeName: "steher-override", slotId, payload: "suppress" });

+        const cleared = sceneOf(proc.handleEvent({ eventTypeName: "steher-override", slotId })); // payload absent → clear


schutzzaun-allroaund/Element gute Frage -> standard rechts / links (da kommt noch mehr logik) der passende ein_l ein_r der Steher ist immer der Sther, der dei passende Anzahl an slots für die anliegen…


Bash

pnpm --filter @ad-cpq/common-frontend test 2>&1 | grep -E 'Test Files|Tests '; pnpm --filter @ad-cpq/common-frontend typecheck 2>&1 | grep -vE 'ProductBrowser\.vue' | grep -E 'error TS' | head; echo "---done---"

Run tests and typecheck after resolver refactor


 Test Files  9 passed (9)
      Tests  116 passed (116)
---done---


Der Prop-Durchgriff ist noch unvollständig — `ProductConfigurator` reicht Regeln durch, aber nicht den Resolver (das dritte Konstruktor-Argument). Ohne Resolver bleibt `derive()` inert. Ich ergänze den Resolver-Prop.


Edit

import { GekoPlacementModule, type DockingRules, type SteherRules } from "../../configurator/modules/GekoPlacementModule";

const props = defineProps<{
    productService?: ProductService;
    // Placement/docking rules supplied by the cpq client (domain knowledge lives there, not in cpq).
    dockingRules?: DockingRules;
    // Steher-derivation rules (auto-inserted Stehers). Empty/absent → no derivation.
    steherRules?: SteherRules;
}>();

import { GekoPlacementModule, type DockingRules, type SteherRules, type SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

const props = defineProps<{
    productService?: ProductService;
    // Placement/docking rules supplied by the cpq client (domain knowledge lives there, not in cpq).
    dockingRules?: DockingRules;
    // Steher-derivation rules (auto-inserted Stehers). Empty/absent → no derivation.
    steherRules?: SteherRules;
    // geko-supplied resolver picking the concrete Steher part per slot. Absent → no derivation.
    resolveSteherPart?: SteherPartResolver;
}>();


The file /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/components/visual/ProductConfigurator.vue has been updated successfully. (file state is current in your context — no need to Read it back)

import { GekoPlacementModule, type DockingRules, type SteherRules } from "../../configurator/modules/GekoPlacementModule";

const props = defineProps<{
    productService?: ProductService;
    // Placement/docking rules supplied by the cpq client (domain knowledge lives there, not in cpq).
    dockingRules?: DockingRules;
    // Steher-derivation rules (auto-inserted Stehers). Empty/absent → no derivation.
    steherRules?: SteherRules;
}>();

import { GekoPlacementModule, type DockingRules, type SteherRules, type SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

const props = defineProps<{
    productService?: ProductService;
    // Placement/docking rules supplied by the cpq client (domain knowledge lives there, not in cpq).
    dockingRules?: DockingRules;
    // Steher-derivation rules (auto-inserted Stehers). Empty/absent → no derivation.
    steherRules?: SteherRules;
    // geko-supplied resolver picking the concrete Steher part per slot. Absent → no derivation.
    resolveSteherPart?: SteherPartResolver;
}>();

<template>

            <slot name="catalog" :select="onProductSelected" />

        <Fire3D v-if="config" ref="fire" class="product-configurator__fire" :config="config" :processor="processor" />

</template>

import { DefaultProductService, FetchAssetLoader, type ProductService } from "../../services/product-service";

import { DefaultCommandProcessor } from "../../configurator/events/DefaultCommandProcessor";

-import { GekoPlacementModule, type DockingRules, type SteherRules } from "../../configurator/modules/GekoPlacementModule";

+import { GekoPlacementModule, type DockingRules, type SteherRules, type SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

+    // geko-supplied resolver picking the concrete Steher part per slot. Absent → no derivation.

+    resolveSteherPart?: SteherPartResolver;

const config = ref<Record<string, unknown> | null>(null);


processor.register(new GekoPlacementModule(props.dockingRules, props.steherRules, props.resolveSteherPart));


const productService = props.productService ?? new DefaultProductService(new FetchAssetLoader("/products/"));

// The geko client wires its placement/docking behaviour in as a module; the core stays domain-agnostic.

const processor = new DefaultCommandProcessor();

+processor.register(new GekoPlacementModule(props.dockingRules, props.steherRules, props.resolveSteherPart));

// Leerer Graph als Replay-Basis. Produkte kommen als add-product-Events obendrauf.

const EMPTY = { _resourceClass: "sceneGraph_entity", handles: [] };


Bash

pnpm --filter @ad-cpq/common-frontend typecheck 2>&1 | grep -vE 'ProductBrowser\.vue' | grep -E 'error TS' | head; echo "---done---"

Typecheck after resolver prop


---done---


Plumbing komplett: `dockingRules`, `steherRules` und der Resolver laufen jetzt alle über Props ans Modul. Tests 116 grün, Typecheck sauber.

Ich prüfe kurz die exakte `class-`-Bezeichnung am Allround-Element, damit du die Regel direkt einsetzen kannst.


cd packages/frontend/.storybook/products && f=$(ls schutzzaun_allround/*.json 2>/dev/null | head -1); echo "file: $f"; grep -oE '"class-[a-zA-Z0-9_]+"' "$f" 2>/dev/null | sort -u; echo "=== CS_OUT_/CS_EIN_ root pmis ==="; grep -oE '"CS_(OUT|EIN)_[A-Z0-9_]+"' "$f" 2>/dev/null | sort -u | head

Find allround element class tag and PMIs


file: schutzzaun_allround/SAD2083PLA.json
=== CS_OUT_/CS_EIN_ root pmis ===
"CS_EIN_L"
"CS_EIN_R"
"CS_EIN_START_L"
"CS_EIN_START_R"
"CS_OUT_L_0"
"CS_OUT_L_270"
"CS_OUT_L_90"
"CS_OUT_R_0"
"CS_OUT_R_270"
"CS_OUT_R_90"


Der Allround-`SAD` trägt **keinen** `class-`-Tag — und `elementsOfClass` matcht genau darauf. Das könnte der eigentliche Grund sein, warum nichts läuft. Ich prüfe die Allround-Element-Produkte breiter.


cd packages/frontend/.storybook/products && echo "=== welche allround-Dateien haben class- Tags? ===" && for f in schutzzaun_allround/SA*.json; do c=$(grep -oE '"class-[a-zA-Z0-9_]+"' "$f" | sort -u | tr '\n' ' '); echo "$(basename $f): ${c:-<KEIN class- tag>}"; done | head -30; echo "=== gibt es überhaupt irgendwo class- tags in den Produkten? ==="; grep -rlE '"class-' schutzzaun_allround schutzzaun_basic 2>/dev/null | head

Check class- tags across allround products


(eval):cd:1: no such file or directory: packages/frontend/.storybook/products
=== gibt es überhaupt irgendwo class- tags in den Produkten? ===
schutzzaun_allround/SAE1120SNN.json
schutzzaun_allround/SAD2083PLA.json
schutzzaun_allround/SAS1044SLA.json
schutzzaun_allround/SAP4180DNA.json
schutzzaun_allround/SAE1077SNN.json
schutzzaun_allround/SAE0862PNN.json
schutzzaun_allround/SAE0260PNN.json
schutzzaun_allround/SAE0174PNN.json
schutzzaun_allround/SAE1249PNN.json
schutzzaun_allround/SAE1292PNN.json


cwd war schon im products-Ordner. Mein erster Regex schloss Bindestriche aus — die Klassennamen enthalten aber welche. Ich hole den exakten Tag.


grep -oE '"class-[a-zA-Z0-9_-]+"' schutzzaun_allround/SAE0260PNN.json schutzzaun_allround/SAD2083PLA.json 2>/dev/null | sort | uniq -c

Get exact class tags with hyphens


   1 schutzzaun_allround/SAD2083PLA.json:"class-schutzzaun_allround-tuer"
   1 schutzzaun_allround/SAE0260PNN.json:"class-schutzzaun_allround-element"


Der Klassen-Tag existiert — mein erster Regex hatte nur den Bindestrich verschluckt. Die Anker-Erkennung funktioniert also, sobald Regeln + Resolver ankommen.

**Warum „läuft nicht los":** Es fehlte der komplette Prop-Pfad. `steherRules` **und** der Resolver erreichten das Modul nie (`ProductConfigurator` baute es nur mit `dockingRules`). Beides ist jetzt durchgereicht.

**Konkrete Regel, die du einsetzen kannst:**
```ts
const steherRules: SteherRules = [
  {
    anchorClass: "schutzzaun_allround-element",   // exakt so getaggt (class-schutzzaun_allround-element)
    joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" },  // standard R/L, mehr Logik später
  },
];
```

**Der Resolver bleibt deiner** („das sollst du nicht wissen") — Signatur:
```ts
const resolveSteherPart: SteherPartResolver = ({ model, anchor, anchorPmi, slotId, override }) => {
  // deine Logik: passende Slot-Anzahl für die anliegenden Zäune → Steher-Szene, oder null
  return steherSceneFürDenSlot;
};
```

**Verdrahtung in der Story/geko** — jetzt möglich, alle drei als Props:
```html
<ProductConfigurator
  :docking-rules="dockingRules"
  :steher-rules="steherRules"
  :resolve-steher-part="resolveSteherPart" />
```

**Ein Caveat, das hier beißt:** `derive()` sieht nur Elemente, die als `add-product`-**Event** ins Modell kamen — nicht solche aus einer per `setScene` vorgeladenen Basis-Szene. In der Configurator-Story kommen Produkte über Katalogauswahl (`add-product`) → passt.

Interface angepasst an dein Modell (per-Seite-Joins, geko-Resolver, opakes Override-Payload via `steher-override`-Event). Tests 116 grün, Typecheck sauber. Commit auf dein Wort.


greift immer noch nicht


Ich prüfe, ob die Klassen-Zuordnung im Code wirklich greift, wie `derive` sie nutzt — `elementsOfClass`/`classOfGroup`.


355	                        }
356	                    }
357	                }
358	            }
359	        }
360	        return [...blocked].map((k) => {
361	            const sp = k.lastIndexOf(" ");
362	            return { element: k.slice(0, sp), pmi: k.slice(sp + 1) };
363	        });
364	    }
365
366	    groupCount(): number {
367	        return this.groups.size;
368	    }
369
370	    roleOfId(handleId: string): NodeRole | null {
371	        return this.nodeIndex.get(handleId)?.role ?? null;
372	    }
373
374	    // Rebuild a standalone product scene from a placed element's subtree — for duplicating it. The
375	    // element root's parentKey is stripped so buildCreateEvent (via instantiateScene) treats it as a
376	    // fresh element root and re-ids the copy. (Prototype helper for the keyboard build shortcut.)
377	    sceneOfElement(elementRoot: string): { _resourceClass: string; handles: Handle[] } | null {
378	        const handles = this.elementHandles.get(elementRoot);
379	        if (!handles) return null;
380	        const clone = structuredClone(handles);
381	        for (const h of clone) {
382	            if (h.id !== elementRoot) continue;
383	            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
384	            if (tr) delete tr.parentKey;
385	        }
386	        return { _resourceClass: "sceneGraph_entity", handles: clone };
387	    }
388
389	    // Docking class of a group, read from the `class-…` tag of its element root(s). Null if none of
390	    // the members carry a class tag. Used to look the group up in the client-supplied docking rules.
391	    classOfGroup(groupId: string): string | null {
392	        for (const member of this.groups.get(groupId)?.members ?? []) {
393	            for (const t of this.handles.get(member)?.tags ?? []) {
394	                if (typeof t === "string" && t.startsWith("class-")) return t.slice("class-".length);

                        }
                    }
                }
            }
        }
        return [...blocked].map((k) => {
            const sp = k.lastIndexOf(" ");
            return { element: k.slice(0, sp), pmi: k.slice(sp + 1) };
        });
    }

    groupCount(): number {
        return this.groups.size;
    }

    roleOfId(handleId: string): NodeRole | null {
        return this.nodeIndex.get(handleId)?.role ?? null;
    }

    // Rebuild a standalone product scene from a placed element's subtree — for duplicating it. The
    // element root's parentKey is stripped so buildCreateEvent (via instantiateScene) treats it as a
    // fresh element root and re-ids the copy. (Prototype helper for the keyboard build shortcut.)
    sceneOfElement(elementRoot: string): { _resourceClass: string; handles: Handle[] } | null {
        const handles = this.elementHandles.get(elementRoot);
        if (!handles) return null;
        const clone = structuredClone(handles);
        for (const h of clone) {
            if (h.id !== elementRoot) continue;
            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
            if (tr) delete tr.parentKey;
        }
        return { _resourceClass: "sceneGraph_entity", handles: clone };
    }

    // Docking class of a group, read from the `class-…` tag of its element root(s). Null if none of
    // the members carry a class tag. Used to look the group up in the client-supplied docking rules.
    classOfGroup(groupId: string): string | null {
        for (const member of this.groups.get(groupId)?.members ?? []) {
            for (const t of this.handles.get(member)?.tags ?? []) {
                if (typeof t === "string" && t.startsWith("class-")) return t.slice("class-".length);


400	    // Every element root in the scene whose docking class equals `klass` (its `class-…` tag). Lets the
401	    // placement build an id-keyed whitelist of concrete snap targets per target class.
402	    elementsOfClass(klass: string): string[] {
403	        const tag = `class-${klass}`;
404	        const out: string[] = [];
405	        for (const g of this.groups.values()) {
406	            for (const member of g.members) {
407	                if ((this.handles.get(member)?.tags ?? []).some((t: unknown) => t === tag)) out.push(member);
408	            }
409	        }
410	        return out;
411	    }
412
413	    // Record-time: split the group's chain at `elementId`. The element plus everything in reading
414	    // direction (to its "right") becomes one new group — the movable one; everything to the left
415	    // stays as another group. "Right" is derived geometrically: the chain's dominant axis (largest
416	    // spread of member positions), increasing coordinate = reading direction. No part-name knowledge.
417	    // Both groups keep their members' world positions (transform shifted to each component origin).
418	    buildDetachEvent(elementId: string): GroupDetachEvent | null {
419	        const sourceGroupId = this.elementToGroup.get(elementId);
420	        if (!sourceGroupId) return null;
421	        const g = this.groups.get(sourceGroupId)!;

    // Every element root in the scene whose docking class equals `klass` (its `class-…` tag). Lets the
    // placement build an id-keyed whitelist of concrete snap targets per target class.
    elementsOfClass(klass: string): string[] {
        const tag = `class-${klass}`;
        const out: string[] = [];
        for (const g of this.groups.values()) {
            for (const member of g.members) {
                if ((this.handles.get(member)?.tags ?? []).some((t: unknown) => t === tag)) out.push(member);
            }
        }
        return out;
    }

    // Record-time: split the group's chain at `elementId`. The element plus everything in reading
    // direction (to its "right") becomes one new group — the movable one; everything to the left
    // stays as another group. "Right" is derived geometrically: the chain's dominant axis (largest
    // spread of member positions), increasing coordinate = reading direction. No part-name knowledge.
    // Both groups keep their members' world positions (transform shifted to each component origin).
    buildDetachEvent(elementId: string): GroupDetachEvent | null {
        const sourceGroupId = this.elementToGroup.get(elementId);
        if (!sourceGroupId) return null;
        const g = this.groups.get(sourceGroupId)!;


STOP
den Resolver sollt du schon schreiben
Die manuellen anpassungemöglichkeite gehen dich nichts anm+


Code-Pfad ist korrekt (`elementsOfClass` matcht `class-schutzzaun_allround-element`). Das Problem liegt also in der Laufzeit-Verdrahtung oder den Daten — das rate ich nicht, ich mache es sichtbar. Ich baue einen zuschaltbaren Debug-Log in `derive()`, der genau zeigt, wo es stoppt: keine Regeln? kein Resolver? keine Anker? Resolver liefert null? Anker belegt?


Edit

    derive(model: GroupProcessor): void {
        if (!this.steherRules.length || !this.resolveSteherPart) return;
        const overrides = model.moduleStateOf(STEHER_MODULE_ID);
        for (const rule of this.steherRules) {
            for (const anchor of model.elementsOfClass(rule.anchorClass)) {
                const anchorGroup = model.groupIdOfElement(anchor);
                if (!anchorGroup) continue;
                for (const [anchorPmi, joinPmi] of Object.entries(rule.joins)) {
                    // Occupied anchor (something already docked there, incl. a promoted concrete part) →
                    // no derived Steher.
                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) continue;
                    const slotId = `steher@${anchor}:${anchorPmi}`;
                    // geko picks the part (its slot count matching the adjacent fences) — or null to skip.
                    const part = this.resolveSteherPart({ model, anchor, anchorPmi, slotId, override: overrides.get(slotId) });
                    if (!part) continue;

    derive(model: GroupProcessor): void {
        // Opt-in diagnostics: `window.__cpqSteher = true` in the console prints why derivation did or
        // didn't produce a Steher (rules/resolver present, anchors found, occupancy, resolver result).
        const dbg = (msg: string) => {
            if ((globalThis as any).__cpqSteher) console.log("[steher]", msg);
        };
        if (!this.steherRules.length || !this.resolveSteherPart) {
            dbg(`inert — rules=${this.steherRules.length} resolver=${!!this.resolveSteherPart}`);
            return;
        }
        const overrides = model.moduleStateOf(STEHER_MODULE_ID);
        for (const rule of this.steherRules) {
            const anchors = model.elementsOfClass(rule.anchorClass);
            dbg(`rule ${rule.anchorClass}: ${anchors.length} anchor(s)`);
            for (const anchor of anchors) {
                const anchorGroup = model.groupIdOfElement(anchor);
                if (!anchorGroup) {
                    dbg(`anchor ${anchor}: no group`);
                    continue;
                }
                for (const [anchorPmi, joinPmi] of Object.entries(rule.joins)) {
                    // Occupied anchor (something already docked there, incl. a promoted concrete part) →
                    // no derived Steher.
                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) {
                        dbg(`anchor ${anchor} ${anchorPmi}: occupied`);
                        continue;
                    }
                    const slotId = `steher@${anchor}:${anchorPmi}`;
                    // geko picks the part (its slot count matching the adjacent fences) — or null to skip.
                    const part = this.resolveSteherPart({ model, anchor, anchorPmi, slotId, override: overrides.get(slotId) });
                    if (!part) {
                        dbg(`slot ${slotId}: resolver returned null`);
                        continue;
                    }
                    dbg(`slot ${slotId}: deriving Steher (join ${joinPmi})`);


The file /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/modules/GekoPlacementModule.ts has been updated successfully. (file state is current in your context — no need to Read it back)

    derive(model: GroupProcessor): void {
        if (!this.steherRules.length || !this.resolveSteherPart) return;
        const overrides = model.moduleStateOf(STEHER_MODULE_ID);
        for (const rule of this.steherRules) {
            for (const anchor of model.elementsOfClass(rule.anchorClass)) {
                const anchorGroup = model.groupIdOfElement(anchor);
                if (!anchorGroup) continue;
                for (const [anchorPmi, joinPmi] of Object.entries(rule.joins)) {
                    // Occupied anchor (something already docked there, incl. a promoted concrete part) →
                    // no derived Steher.
                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) continue;
                    const slotId = `steher@${anchor}:${anchorPmi}`;
                    // geko picks the part (its slot count matching the adjacent fences) — or null to skip.
                    const part = this.resolveSteherPart({ model, anchor, anchorPmi, slotId, override: overrides.get(slotId) });
                    if (!part) continue;

    derive(model: GroupProcessor): void {
        // Opt-in diagnostics: `window.__cpqSteher = true` in the console prints why derivation did or
        // didn't produce a Steher (rules/resolver present, anchors found, occupancy, resolver result).
        const dbg = (msg: string) => {
            if ((globalThis as any).__cpqSteher) console.log("[steher]", msg);
        };
        if (!this.steherRules.length || !this.resolveSteherPart) {
            dbg(`inert — rules=${this.steherRules.length} resolver=${!!this.resolveSteherPart}`);
            return;
        }
        const overrides = model.moduleStateOf(STEHER_MODULE_ID);
        for (const rule of this.steherRules) {
            const anchors = model.elementsOfClass(rule.anchorClass);
            dbg(`rule ${rule.anchorClass}: ${anchors.length} anchor(s)`);
            for (const anchor of anchors) {
                const anchorGroup = model.groupIdOfElement(anchor);
                if (!anchorGroup) {
                    dbg(`anchor ${anchor}: no group`);
                    continue;
                }
                for (const [anchorPmi, joinPmi] of Object.entries(rule.joins)) {
                    // Occupied anchor (something already docked there, incl. a promoted concrete part) →
                    // no derived Steher.
                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) {
                        dbg(`anchor ${anchor} ${anchorPmi}: occupied`);
                        continue;
                    }
                    const slotId = `steher@${anchor}:${anchorPmi}`;
                    // geko picks the part (its slot count matching the adjacent fences) — or null to skip.
                    const part = this.resolveSteherPart({ model, anchor, anchorPmi, slotId, override: overrides.get(slotId) });
                    if (!part) {
                        dbg(`slot ${slotId}: resolver returned null`);
                        continue;
                    }
                    dbg(`slot ${slotId}: deriving Steher (join ${joinPmi})`);

// the model (ephemeral, never stored) so docking/materialisation see the Stehers and they stay

// coupled to their anchor group (a group-move on the base carries them along).

-        if (!this.steherRules.length || !this.resolveSteherPart) return;

+        // Opt-in diagnostics: `window.__cpqSteher = true` in the console prints why derivation did or

+        // didn't produce a Steher (rules/resolver present, anchors found, occupancy, resolver result).

+            if ((globalThis as any).__cpqSteher) console.log("[steher]", msg);

+        if (!this.steherRules.length || !this.resolveSteherPart) {

+            dbg(`inert — rules=${this.steherRules.length} resolver=${!!this.resolveSteherPart}`);

-            for (const anchor of model.elementsOfClass(rule.anchorClass)) {

+            const anchors = model.elementsOfClass(rule.anchorClass);

+            dbg(`rule ${rule.anchorClass}: ${anchors.length} anchor(s)`);

+            for (const anchor of anchors) {

-                if (!anchorGroup) continue;

+                    dbg(`anchor ${anchor}: no group`);

-                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) continue;

+                    if (model.occupiedPmis().some((o) => o.element === anchor && o.pmi === anchorPmi)) {

+                        dbg(`anchor ${anchor} ${anchorPmi}: occupied`);

-                    if (!part) continue;

+                        dbg(`slot ${slotId}: resolver returned null`);

+                    dbg(`slot ${slotId}: deriving Steher (join ${joinPmi})`);

// Deterministic ids keyed on the slot → stable across replays (selection, docking,

// override binding). Apply the create first so the Steher's PMI frames are in the

// model before dockRotation reads them, then merge it into the anchor's group.


Prompt:
STOP
den Resolver sollt du schon schreiben
Die manuellen anpassungemöglichkeite gehen dich nichts anm+


Verstanden, umgekehrt: **ich schreibe den Resolver** (Default-Steher nach Slot-Anzahl der anliegenden Zäune), die **manuellen Overrides gehen mich nichts an**. Ich habe mich auf das Falsche konzentriert. Kein weiterer Edit, bis ich den Steher-Katalog und die Slot-Darstellung verstanden habe.


Bash

echo "=== Steher-Gruppe im Katalog ===" && jq -r '.. | objects | select(.name? == "Steher") ' catalog.json 2>/dev/null | head -60

Inspect Steher catalog group


=== Steher-Gruppe im Katalog ===
{
  "name": "Steher",
  "artikelgruppen": [
    {
      "name": "SAP4180ANA",
      "fuellung": "-",
      "artikel": [
        {
          "artikelnummer": "SAP4180ANA",
          "initialartikel": false,
          "masse": {
            "laenge": 2200,
            "breite": 40,
            "hoehe": 80
          },
          "filter": {},
          "stammdatenBild": "allround_steher.png",
          "assets": {
            "scenegraph": "schutzzaun_allround/SAP4180ANA.json",
            "bild": "schutzzaun_allround/SAP4180ANA.png"
          }
        },
        {
          "artikelnummer": "SAP4180BNA",
          "initialartikel": true,
          "masse": {
            "laenge": 2200,
            "breite": 40,
            "hoehe": 80
          },
          "filter": {},
          "stammdatenBild": "allround_steher.png",
          "assets": {
            "scenegraph": "schutzzaun_allround/SAP4180BNA.json",
            "bild": "schutzzaun_allround/SAP4180BNA.png"
          }
        },
        {
          "artikelnummer": "SAP4180CNA",
          "initialartikel": false,
          "masse": {
            "laenge": 2200,
            "breite": 40,
            "hoehe": 80
          },
          "filter": {},
          "stammdatenBild": "allround_steher.png",
          "assets": {
            "scenegraph": "schutzzaun_allround/SAP4180CNA.json",
            "bild": "schutzzaun_allround/SAP4180CNA.png"
          }
        },
        {
          "artikelnummer": "SAP4180DNA",
          "initialartikel": false,
          "masse": {
            "laenge": 2200,
            "breite": 40,
            "hoehe": 80
          },


Steher-Varianten: `SAP4180{A,B,C,D}NA` — der Buchstabe kodiert vermutlich die Slot-Konfiguration. Ich vergleiche die PMIs der Varianten, um „Slot-Anzahl" zu verstehen.


for v in A B C D; do f="schutzzaun_allround/SAP4180${v}NA.json"; [ -f "$f" ] && echo "=== SAP4180${v}NA ===" && grep -oE '"CS_[A-Z0-9_]+"' "$f" | sort | uniq -c; done 2>/dev/null

Compare PMIs across Steher variants


=== SAP4180ANA ===
   1 "CS_BASE"
   1 "CS_BASE_PRE"
   1 "CS_EIN"
   1 "CS_EIN_L"
   1 "CS_EIN_R"
   1 "CS_EIN_START_L"
   1 "CS_EIN_START_R"
   1 "CS_OUT_0"
   1 "CS_OUT_180"
   1 "CS_OUT_270"
   1 "CS_OUT_90"
   1 "CS_SELECT_0"
   1 "CS_SELECT_180"
   1 "CS_SELECT_270"
   1 "CS_SELECT_90"
=== SAP4180BNA ===
   1 "CS_BASE"
   1 "CS_BASE_PRE"
   1 "CS_EIN"
   1 "CS_EIN_L"
   1 "CS_EIN_R"
   1 "CS_EIN_START_L"
   1 "CS_EIN_START_R"
   1 "CS_OUT_0"
   1 "CS_OUT_180"
   1 "CS_OUT_270"
   1 "CS_OUT_90"
   1 "CS_SELECT_0"
   1 "CS_SELECT_180"
   1 "CS_SELECT_270"
   1 "CS_SELECT_90"
=== SAP4180CNA ===
   1 "CS_BASE"
   1 "CS_BASE_PRE"
   1 "CS_EIN"
   1 "CS_EIN_L"
   1 "CS_EIN_R"
   1 "CS_EIN_START_L"
   1 "CS_EIN_START_R"
   1 "CS_OUT_0"
   1 "CS_OUT_180"
   1 "CS_OUT_270"
   1 "CS_OUT_90"
   1 "CS_SELECT_0"
   1 "CS_SELECT_180"
   1 "CS_SELECT_270"
   1 "CS_SELECT_90"
=== SAP4180DNA ===
   1 "CS_BASE"
   1 "CS_BASE_PRE"
   1 "CS_EIN"
   1 "CS_EIN_L"
   1 "CS_EIN_R"
   1 "CS_EIN_START_L"
   1 "CS_EIN_START_R"
   1 "CS_OUT_0"
   1 "CS_OUT_180"
   1 "CS_OUT_270"
   1 "CS_OUT_90"
   1 "CS_SELECT_0"
   1 "CS_SELECT_180"
   1 "CS_SELECT_270"
   1 "CS_SELECT_90"


Wichtiger Befund: Die vier Steher-Varianten `SAP4180{A,B,C,D}NA` haben **identische PMIs** (`CS_EIN_L/R`, `CS_OUT_0/90/180/270`) — sie unterscheiden sich nur in der Geometrie (welche Kanäle gefräst sind), nicht in den Anschlusspunkten. `SAP4180BNA` ist `initialartikel: true`. Die „passende Slot-Anzahl" ist damit **nicht aus PMIs ablesbar**, sondern die Wahl der Variante nach Anordnung der anliegenden Zäune — und *welche* Zäune anliegen ist die Junction-Erkennung (das offene Problem).

Ich schreibe den Resolver, aber der Grund für „greift nicht" ist schlichter: **die Story übergibt gar keine `steher-rules`/keinen Resolver** — es gibt nichts zu greifen. Ich verdrahte die Configurator-Story mit einem ersten Resolver (Default-Steher), damit es überhaupt läuft. Ich sehe mir die Story an.


1	import type { Meta, StoryObj } from "@storybook/vue3";
2	import { onMounted, ref } from "vue";
3	import ProductConfigurator from "./ProductConfigurator.vue";
4	import ProductBrowser from "../building-blocks/ProductBrowser.vue";
5	import type { DockingRules } from "../../configurator/modules/GekoPlacementModule";
6
7	const meta: Meta<typeof ProductConfigurator> = {
8	    title: "Widgets/Visual",
9	    component: ProductConfigurator,
10	    parameters: { layout: "fullscreen" },
11	};
12
13	export default meta;
14
15	type Story = StoryObj<typeof ProductConfigurator>;
16
17	export const Configurator: Story = {
18	    render: () => ({
19	        components: { ProductConfigurator, ProductBrowser },
20	        setup() {
21	            const catalog = ref<unknown>(null);
22	            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
23	            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
24	            const dockingRules = ref<DockingRules | null>(null);
25	            onMounted(async () => {
26	                const [cat, rules] = await Promise.all([
27	                    fetch("/products/catalog.json").then((r) => r.json()),
28	                    fetch("/products/docking-rules.json").then((r) => r.json()),
29	                ]);
30	                catalog.value = cat;
31	                dockingRules.value = rules;
32	            });
33	            return { catalog, dockingRules };
34	        },
35	        template: `
36	            <div style="height:100vh">
37	                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">
38	                    <template #catalog="{ select }">
39	                        <ProductBrowser
40	                            v-if="catalog"
41	                            :data="catalog"
42	                            image-base-path="/products/"
43	                            @child-clicked="select"
44	                        />
45	                    </template>
46	                </ProductConfigurator>
47	            </div>
48	        `,
49	    }),
50	};
51

import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules } from "../../configurator/modules/GekoPlacementModule";

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            onMounted(async () => {
                const [cat, rules] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
            });
            return { catalog, dockingRules };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};


Klar warum: die Story übergibt weder `steher-rules` noch Resolver. Ich verdrahte sie mit einem ersten Resolver (Default-Steher `SAP4180BNA`), damit es läuft.


Edit

import type { DockingRules } from "../../configurator/modules/GekoPlacementModule";

import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];


The file /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/components/visual/ProductConfigurator.stories.ts has been updated successfully. (file state is current in your context — no need to Read it back)

import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules } from "../../configurator/modules/GekoPlacementModule";

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            onMounted(async () => {
                const [cat, rules] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
            });
            return { catalog, dockingRules };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

-import type { DockingRules } from "../../configurator/modules/GekoPlacementModule";

+import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

+// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.

+    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },


            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            onMounted(async () => {
                const [cat, rules] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
            });
            return { catalog, dockingRules };

            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };


            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };

import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            onMounted(async () => {
                const [cat, rules] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
            });
            return { catalog, dockingRules };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

+            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante

+            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.

+            const steherScene = ref<any>(null);

+            const resolveSteherPart: SteherPartResolver = () => steherScene.value;

-                const [cat, rules] = await Promise.all([

+                const [cat, rules, steher] = await Promise.all([

+                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),

+                steherScene.value = steher;

-            return { catalog, dockingRules };

+            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };


                <ProductConfigurator
                    v-if="dockingRules && steherScene"
                    :docking-rules="dockingRules"
                    :steher-rules="steherRules"
                    :resolve-steher-part="resolveSteherPart">


                <ProductConfigurator
                    v-if="dockingRules && steherScene"
                    :docking-rules="dockingRules"
                    :steher-rules="steherRules"
                    :resolve-steher-part="resolveSteherPart">

import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

-                <ProductConfigurator v-if="dockingRules" :docking-rules="dockingRules">

+                    v-if="dockingRules && steherScene"

+                    :docking-rules="dockingRules"

+                    :steher-rules="steherRules"

+                    :resolve-steher-part="resolveSteherPart">


Bash

pnpm --filter @ad-cpq/common-frontend typecheck 2>&1 | grep -vE 'ProductBrowser\.vue' | grep -E 'error TS' | head; echo "=== tests ==="; pnpm --filter @ad-cpq/common-frontend test 2>&1 | grep -E 'Test Files|Tests '; echo "---done---"

Typecheck and tests after story wiring


=== tests ===
 Test Files  9 passed (9)
      Tests  116 passed (116)
---done---


Läuft jetzt verdrahtet — Tests 116 grün, Typecheck sauber.

**Warum „greift nicht":** Die Configurator-Story übergab weder `steher-rules` noch einen Resolver — es gab nichts abzuleiten. Jetzt verdrahtet: die Story lädt den Default-Steher (`SAP4180BNA`), setzt die Regel (`schutzzaun_allround-element`, `CS_OUT_R_0→CS_EIN_L` / `CS_OUT_L_0→CS_EIN_R`) und reicht einen ersten Resolver durch.

**So testest du:** In der Configurator-Story ein **Allround-Element** (SAE, `class-schutzzaun_allround-element`) aus dem Katalog wählen → an jedem offenen R/L-Ende leitet sich ein Steher ab. (Bei Basic-Elementen passiert nichts — andere Klasse.)

**Debug:** `window.__cpqSteher = true` in der Konsole zeigt pro Slot, was passiert (Anker gefunden / belegt / Resolver-Ergebnis / abgeleitet).

**Was der Resolver jetzt tut:** immer den Initial-Steher liefern. Das ist die erste Version, damit es überhaupt greift.

**Was für den *echten* Resolver fehlt** (die „passende Slot-Anzahl"): Die vier Varianten `SAP4180{A,B,C,D}NA` haben identische PMIs und unterscheiden sich nur in der Geometrie. Die Wahl A/B/C/D hängt an (1) **welche Zäune am Steher-Punkt anliegen** — das ist die Junction-Erkennung (das offene Problem aus den OFFENE-PUNKTE) — und (2) dem **Mapping Variante → Slot-Anordnung** (welche Richtung 0/90/180/270 hat welcher Steher einen Kanal). Das zweite kenne ich nicht; sag mir, was A/B/C/D bedeuten, dann baue ich die Auswahl ein.

**Bekannter Nebeneffekt:** Zwei aneinandergebaute Elemente erzeugen am gemeinsamen Punkt je einen Steher (Doppelbelegung) — genau der Junction-Fall. Das löst erst die Junction-Erkennung mit auf.

Manuelle Overrides habe ich in Ruhe gelassen.


ProductBrowser.vue:37 Error rendering story 'widgets-visual--configurator':
ProductBrowser.vue:37 DataCloneError: Failed to execute 'structuredClone' on 'Window': #<Object> could not be cloned.
    at instantiateSceneDeterministic (GroupProcessor.ts:74:17)
    at GroupProcessor.buildCreateEventDeterministic (GroupProcessor.ts:242:43)
    at GekoPlacementModule.derive (GekoPlacementModule.ts:128:44)
    at DefaultCommandProcessor.ts:102:45
    at Perf.span (perf.ts:36:34)
    at DefaultCommandProcessor.replay (DefaultCommandProcessor.ts:94:33)
    at DefaultCommandProcessor.handleAddProduct (DefaultCommandProcessor.ts:183:28)
    at DefaultCommandProcessor.coreHandle (DefaultCommandProcessor.ts:138:29)
    at DefaultCommandProcessor.handleEvent (DefaultCommandProcessor.ts:118:69)
    at Fire3D.vue:228:73


`steherScene` ist ein `ref` → Vue macht die geladene Szene zu einem reaktiven Proxy, und `structuredClone` scheitert daran. Fix: `shallowRef` (hält den Wert roh, nicht tief-reaktiv), damit die Szene ein klonbares Plain-Objekt bleibt.


Edit

import { onMounted, ref, shallowRef } from "vue";


import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator
                    v-if="dockingRules && steherScene"
                    :docking-rules="dockingRules"
                    :steher-rules="steherRules"
                    :resolve-steher-part="resolveSteherPart">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

+import { onMounted, ref, shallowRef } from "vue";


            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;

            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            // shallowRef: die Szene bleibt ein rohes Plain-Objekt (kein reaktiver Proxy), sonst scheitert
            // structuredClone in der Ableitung.
            const steherScene = shallowRef<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;


            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            // shallowRef: die Szene bleibt ein rohes Plain-Objekt (kein reaktiver Proxy), sonst scheitert
            // structuredClone in der Ableitung.
            const steherScene = shallowRef<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;

import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref, shallowRef } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            const steherScene = ref<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator
                    v-if="dockingRules && steherScene"
                    :docking-rules="dockingRules"
                    :steher-rules="steherRules"
                    :resolve-steher-part="resolveSteherPart">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

-            const steherScene = ref<any>(null);

+            // shallowRef: die Szene bleibt ein rohes Plain-Objekt (kein reaktiver Proxy), sonst scheitert

+            // structuredClone in der Ableitung.

+            const steherScene = shallowRef<any>(null);


Bash

pnpm --filter @ad-cpq/common-frontend typecheck 2>&1 | grep -vE 'ProductBrowser\.vue' | grep -E 'error TS' | head; echo "---done---"

Typecheck after shallowRef fix


ProductBrowser.vue:37 Error rendering story 'widgets-visual--configurator': ProductBrowser.vue:37 DataCloneError: Failed to execute 'structuredClone' on 'Window': #<Object> could not be cloned.     at…


---done---


Behoben. Der Resolver gab die Szene aus einem tief-reaktiven `ref` zurück; `structuredClone` in der Ableitung kann Vue-Proxies nicht klonen. Mit `shallowRef` bleibt die geladene Szene ein rohes Plain-Objekt.

Nach dem HMR-Reload: Allround-Element aus dem Katalog wählen → Steher leiten sich an den offenen R/L-Enden ab.

Für die echte geko-Verdrahtung dieselbe Regel: der Resolver muss **Plain-Objekte** liefern (kein reaktiver `ref`/`reactive`-Wert) — sonst scheitert der Klon in `instantiateSceneDeterministic`.


schon mal besser
steher links ist eingebaut aber auf halber Höhe (er geht halb durch den Boden)


Halbe Höhe = vertikaler Versatz beim Dock. Das hängt an den PMI-Positionen/-Frames von Fence-`CS_OUT` und Steher-`CS_EIN`. Ich schaue mir die tatsächlichen Koordinaten an.


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/.storybook/products 2>/dev/null; sae=$(ls schutzzaun_allround/SAE*.json | head -1); echo "=== FENCE $sae : CS_OUT_L_0 / CS_OUT_R_0 ==="; jq -c '.handles[].components[]? | select(._resourceClass=="component_pmi" and (.id=="CS_OUT_L_0" or .id=="CS_OUT_R_0")) | {id, pos: .properties.position.value, rot: .properties.rotation.value}' "$sae"; echo "=== STEHER SAP4180BNA : CS_EIN_L / CS_EIN_R ==="; jq -c '.handles[].components[]? | select(._resourceClass=="component_pmi" and (.id=="CS_EIN_L" or .id=="CS_EIN_R")) | {id, pos: .properties.position.value, rot: .properties.rotation.value}' schutzzaun_allround/SAP4180BNA.json

Compare fence CS_OUT and steher CS_EIN PMI coordinates


=== FENCE schutzzaun_allround/SAE0131SNN.json : CS_OUT_L_0 / CS_OUT_R_0 ===
=== STEHER SAP4180BNA : CS_EIN_L / CS_EIN_R ===
{"id":"CS_EIN_L","pos":[-71.0,1067.0,-20.0],"rot":[-0.0,-1.5707963267948966,0.0]}
{"id":"CS_EIN_R","pos":[31.0,1067.0,-20.0],"rot":[-0.0,1.5707963267948966,0.0]}


Der Fence-`SAE0131SNN` liefert keine `CS_OUT_L_0/R_0` — vielleicht andere PMI-Namen oder dieses (kurze) Element hat sie nicht. Ich liste alle PMIs des Elements.


sae=schutzzaun_allround/SAE0260PNN.json; echo "=== $sae PMIs (id, pos.y) ==="; jq -c '.handles[].components[]? | select(._resourceClass=="component_pmi") | {id, pos: .properties.position.value}' "$sae" | head -30

List all PMIs of an allround element


=== schutzzaun_allround/SAE0260PNN.json PMIs (id, pos.y) ===
{"id":"CS_EIN","pos":[220,-179,-40]}
{"id":"CS_BASE_PRE","pos":[220.0,-179.0,-40.0]}
{"id":"CS_BASE","pos":[220.0,-179.0,-40.0]}
{"id":"CS_EIN_L","pos":[200.0,921.0,-20.0]}
{"id":"CS_EIN_R","pos":[-20.0,921.0,-20.0]}
{"id":"CS_EIN_START_L","pos":[220.0,-179.0,-20.0]}
{"id":"CS_EIN_START_R","pos":[-40.0,-179.0,-20.0]}
{"id":"CS_OUT_L","pos":[200.0,921.0,-20.0]}
{"id":"CS_OUT_R","pos":[-20.0,921.0,-20.0]}


Gefunden. Das Allround-**Element** benennt seine Enden `CS_OUT_L` / `CS_OUT_R` — **ohne** `_0`. Meine Regel nutzte `CS_OUT_R_0`/`CS_OUT_L_0` (das ist die *Basic*-Nomenklatur). Der Anker-PMI existiert also nicht → das Dock fällt auf den Element-Ursprung zurück, und die Steher-`CS_EIN` (Y=1067 ≈ halbe Steherhöhe) landet auf Y=0 → der Steher steckt halb im Boden. Genau dein Symptom.

Fix: die realen PMI-Namen des Allround-Elements verwenden.


Edit

{ anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },

{ anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R: "CS_EIN_L", CS_OUT_L: "CS_EIN_R" } },


import type { Meta, StoryObj } from "@storybook/vue3";
import { onMounted, ref, shallowRef } from "vue";
import ProductConfigurator from "./ProductConfigurator.vue";
import ProductBrowser from "../building-blocks/ProductBrowser.vue";
import type { DockingRules, SteherRules, SteherPartResolver } from "../../configurator/modules/GekoPlacementModule";

// Steher-Ableitung: an jedem offenen R/L-Ende eines Allround-Elements wird ein Steher abgeleitet.
const STEHER_RULES: SteherRules = [
    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },
];

const meta: Meta<typeof ProductConfigurator> = {
    title: "Widgets/Visual",
    component: ProductConfigurator,
    parameters: { layout: "fullscreen" },
};

export default meta;

type Story = StoryObj<typeof ProductConfigurator>;

export const Configurator: Story = {
    render: () => ({
        components: { ProductConfigurator, ProductBrowser },
        setup() {
            const catalog = ref<unknown>(null);
            // Docking-Regeln kommen normalerweise vom cpq-Client (geko); hier aus einer statischen JSON
            // geladen und per Prop übergeben. Klassen-Keys entsprechen den `class-…` Tags an den Bauteilen.
            const dockingRules = ref<DockingRules | null>(null);
            // Erste Version des Resolvers: immer der Default-/Initial-Steher. Die Slot-Anzahl-Variante
            // (A/B/C/D nach anliegenden Zäunen) folgt, sobald Junction-Erkennung + Varianten-Mapping da sind.
            // shallowRef: die Szene bleibt ein rohes Plain-Objekt (kein reaktiver Proxy), sonst scheitert
            // structuredClone in der Ableitung.
            const steherScene = shallowRef<any>(null);
            const resolveSteherPart: SteherPartResolver = () => steherScene.value;
            onMounted(async () => {
                const [cat, rules, steher] = await Promise.all([
                    fetch("/products/catalog.json").then((r) => r.json()),
                    fetch("/products/docking-rules.json").then((r) => r.json()),
                    fetch("/products/schutzzaun_allround/SAP4180BNA.json").then((r) => r.json()),
                ]);
                catalog.value = cat;
                dockingRules.value = rules;
                steherScene.value = steher;
            });
            return { catalog, dockingRules, steherScene, steherRules: STEHER_RULES, resolveSteherPart };
        },
        template: `
            <div style="height:100vh">
                <ProductConfigurator
                    v-if="dockingRules && steherScene"
                    :docking-rules="dockingRules"
                    :steher-rules="steherRules"
                    :resolve-steher-part="resolveSteherPart">
                    <template #catalog="{ select }">
                        <ProductBrowser
                            v-if="catalog"
                            :data="catalog"
                            image-base-path="/products/"
                            @child-clicked="select"
                        />
                    </template>
                </ProductConfigurator>
            </div>
        `,
    }),
};

-    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R_0: "CS_EIN_L", CS_OUT_L_0: "CS_EIN_R" } },

+    { anchorClass: "schutzzaun_allround-element", joins: { CS_OUT_R: "CS_EIN_L", CS_OUT_L: "CS_EIN_R" } },


schon mal besser steher links ist eingebaut aber auf halber Höhe (er geht halb durch den Boden)


`CS_OUT_R`/`CS_OUT_L` gesetzt (die Allround-Element-Nomenklatur, ohne `_0`). Nach dem HMR-Reload sollte der Steher am richtigen Punkt sitzen statt halb im Boden.

Falls ein **kleiner** Rest-Versatz bleibt: Element und Steher definieren ihre Verbindungshöhe unterschiedlich — Fence `CS_OUT`/`CS_EIN` bei Y≈921, Steher `CS_EIN` bei Y≈1067 (Differenz 146 mm). Das wäre dann ein PMI-Koordinaten-Thema in den Katalogdaten (die „versetzten Koordinaten" aus der Nomenklatur-Notiz), kein Code-Fehler. Sag mir, ob er jetzt sauber steht oder noch um diesen Betrag versetzt.


ok -- sieht gut aus --> steher sind bei de da höhe passt auch
ABER - jetzt wirds lustig -> dia anbau böppels werden nicht angezeigt
hat das element selrct_pmis?


Direkt geprüft — das Allround-Element hat **keine** `CS_SELECT`-PMIs. Aus dem PMI-Dump von `SAE0260PNN`: nur `CS_EIN`, `CS_BASE(_PRE)`, `CS_EIN_L/R`, `CS_EIN_START_L/R`, `CS_OUT_L/R`. Kein `CS_SELECT_*`. Ich bestätige es breiter.


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/.storybook/products 2>/dev/null; echo "=== CS_SELECT in Allround-Elementen (SAE)? ==="; for f in schutzzaun_allround/SAE0260PNN.json schutzzaun_allround/SAE1120SNN.json; do echo "$f: $(grep -oE '"CS_SELECT[A-Z0-9_]*"' "$f" | sort -u | tr '\n' ' ' || echo none)"; done; echo "=== zum Vergleich: Steher SAP4180BNA ==="; grep -oE '"CS_SELECT[A-Z0-9_]*"' schutzzaun_allround/SAP4180BNA.json | sort -u | tr '\n' ' '; echo

Confirm CS_SELECT presence on element vs steher


=== CS_SELECT in Allround-Elementen (SAE)? ===
schutzzaun_allround/SAE0260PNN.json:
schutzzaun_allround/SAE1120SNN.json:
=== zum Vergleich: Steher SAP4180BNA ===
"CS_SELECT_0" "CS_SELECT_180" "CS_SELECT_270" "CS_SELECT_90"


Nein. Die Allround-Elemente (`SAE...`) haben **keine** `CS_SELECT`-PMIs — deshalb erzeugt `withSelectionMarkers` dort keine Böppel. Die `CS_SELECT`-Punkte sitzen am **Steher** (`CS_SELECT_0/90/180/270`).

Das ist konsistent: Der Anbau geht vom Steher aus, nicht vom Zaunfeld. Die Böppel erscheinen also, wenn du einen **Steher** selektierst, nicht ein Element.

Ein Detail, das dann greift: `handleMarkerBuild` leitet den Join aus `_R_`/`_L_` im PMI-Namen ab (`CS_EIN_L`/`CS_EIN_R`). Der Steher heißt aber `CS_SELECT_0/90/180/270` (nur Winkel, keine Seite) → die Seiten-Ableitung trifft dort nicht und fällt auf `CS_EIN_R`. Für den Steher-Anbau braucht die Marker-Logik also noch die Winkel-Variante, sobald du sie über den Steher testest.


ok -- heißt unsere grouping logik passt nicht
1 - bei allround DARF die gruppe beim select _< move / rotate nicht aufgelöst werden
es muss immer das gesamte Stück betrachtet werden
2 - Same beim einbau: die gesamr gruppe muss gecloned und angehäangt werden

Wie lösen wir das?


Die Grouping-Logik selbst passt — sie ist nur zu *fein* für allround. Der Kern: **allround-Gruppen sind atomar.** Die Operationseinheit ist immer die ganze Gruppe, nie das Element. Das lässt sich fast komplett im GEKO-Modul lösen, ohne die Core-Selektion umzubauen.

**Woher „atomar": geko-Daten.** Eine Klassenliste `atomicClasses` (wie `steherRules`) ins Modul. Das Modul entscheidet danach — Core bleibt agnostisch.

**1. Select/Move/Rotate nicht auflösen**

GEKO-Modul, `selection-click`-`prepare`: Klick auf ein Element einer atomaren Gruppe → **ganze Gruppe** selektieren (Gruppen-Level), Element-Level/Escalation entfällt.

Der Effekt fällt dann von selbst:
- `prepare-action` bekommt immer eine Ganz-Gruppen-Selektion → `buildRegroupEvent` trifft den „whole-group"-Fall → **kein Split**. Move/Rotate bewegen die ganze Gruppe, nichts wird aufgelöst. Kein Core-Eingriff.
- Context-Menü: „Element neu platzieren" (Detach) für atomare Gruppen weglassen (Modul-`refine`).

**Bonus:** Das behebt zugleich dein Böppel-Problem. Die Ganz-Gruppen-Selektion umfasst die abgeleiteten **Steher** — und *die* haben `CS_SELECT`. Also erscheinen die Anbau-Böppel am Steher, sobald du irgendwo aufs allround-Stück klickst.

**2. Anbau klont die ganze Gruppe**

Dafür braucht es ein Core-Primitive `buildGroupCloneEvents(groupId, position)`: pro **realem** Member ein `group-create` + die internen Kanten als `group-merge`, alles mit frischen Ids → eine neue Gruppe. Abgeleitete Steher werden **nicht** mitgeklont — sie re-derivieren auf dem Klon. Das GEKO-Modul (Marker-/Keyboard-Anbau) nutzt das statt `sceneOfElement(einzelElement)` und dockt den Klon an.

**Aufteilung:**
- Core: nur das eine Klon-Primitive (generisch, kundenneutral).
- GEKO-Modul: atomare Selektion, Detach-Ausblendung, Anbau auf Gruppen-Ebene, `atomicClasses`-Config.

Der einzige echte Neubau ist `buildGroupCloneEvents` (Multi-Member-Klon mit internen Kanten); der Rest sind Modul-Hooks auf Vorhandenem.

Passt die Richtung — atomar via geko-Klassenliste, Logik im Modul, ein Core-Klon-Primitive? Dann baue ich es so.


ok -- heißt unsere grouping logik passt nicht 1 - bei allround DARF die gruppe beim select _< move / rotate nicht aufgelöst werden es muss immer das gesamte Stück betrachtet werden 2 - Same beim einba…


kannst du mal nach einbu / assmbly relation in fie visual schauen?


FireVisual ist Fremdcode (geko-fire/@ad/fire). Ich suche dort nach Assembly-/Einbau-/Relations-Konzepten.


cd /Users/marcus.hinz/projects/geko-fire 2>/dev/null && pwd && echo "=== structure ===" && ls; echo "=== grep terms in browser build + src ==="; grep -roniE 'assembly|einbau|verbau|relation|buildIn|hierarchy|parentEntity|attach' browser src 2>/dev/null | grep -viE 'node_modules|\.map:' | head -40

Search FireVisual for assembly/einbau/relation concepts


/Users/marcus.hinz/projects/geko-fire
=== structure ===
README.md
assets
browser
fbxProcessor.worker.js
fbxProcessor.worker.js.LICENSE.txt
fbxProcessor.worker.js.map
glbProcessor.worker.js
glbProcessor.worker.js.LICENSE.txt
glbProcessor.worker.js.map
index.html
package.json
types
=== grep terms in browser build + src ===
browser/index.js:2:attach
browser/index.js:2:attach
browser/index.js:2:attach
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:ATTACH
browser/index.js:2:attach
browser/index.js:2:attach
browser/index.js:2:Attach
browser/index.js:2:Attach
browser/index.js:2:Attach
browser/index.js:2:Attach
browser/index.js:2:Attach
browser/index.js:2:Attach
browser/index.js:2:Relation


`browser/index.js` ist minifiziert, aber es gibt ein `types/`-Verzeichnis — die API-Typen sind lesbar. Ich schaue dort nach Assembly/Relation/Einbau.


echo "=== types tree ===" && find types -type f -name '*.d.ts' | head -40; echo "=== files mentioning assembly/relation/einbau/verbau ==="; grep -rliE 'assembly|relation|einbau|verbau' types 2>/dev/null | head

Explore fire types for assembly/relation


=== types tree ===
types/import-fire-visual-modules.d.ts
types/index.d.ts
types/environmentConfig.d.ts
=== files mentioning assembly/relation/einbau/verbau ===
types/import-fire-visual-modules.d.ts


echo "=== assembly/relation/einbau/verbau context in import-fire-visual-modules.d.ts ===" && grep -niE 'assembly|relation|einbau|verbau' types/import-fire-visual-modules.d.ts | head -40; echo; echo "=== file size / total lines ==="; wc -l types/import-fire-visual-modules.d.ts

Grep assembly/relation lines in fire types


=== assembly/relation/einbau/verbau context in import-fire-visual-modules.d.ts ===
11:import '@fire-visual/relations';

=== file size / total lines ===
      14 types/import-fire-visual-modules.d.ts


Es gibt ein Modul `@fire-visual/relations`. Ich lese die Modul-Liste und suche die Typen dazu.


echo "=== import-fire-visual-modules.d.ts ===" && cat types/import-fire-visual-modules.d.ts; echo "=== find @fire-visual/relations types ==="; find /Users/marcus.hinz/projects/geko-fire /Users/marcus.hinz/projects/adesso-cpq/node_modules /Users/marcus.hinz/projects/geko-fire/node_modules -path '*fire-visual/relations*' -name '*.d.ts' 2>/dev/null | head

Read module list and locate relations types


=== import-fire-visual-modules.d.ts ===
import '@fire-visual/animation';
import '@fire-visual/billboard';
import '@fire-visual/di';
import '@fire-visual/engine';
import '@fire-visual/input';
import '@fire-visual/interaction';
import '@fire-visual/math';
import '@fire-visual/measure';
import '@fire-visual/pathfinding';
import '@fire-visual/pmi';
import '@fire-visual/relations';
import '@fire-visual/textrenderer';
import '@fire-visual/utils';
import '@fire-visual/worker';
=== find @fire-visual/relations types ===


FireVisual hat ein eigenes **`@fire-visual/relations`**-Modul — genau das Einbau-/Assembly-Konzept. Die Typen liegen nicht als separate Datei vor; ich grabe die API-Bezeichner aus dem Build.


echo "=== relation-bezogene Bezeichner (quoted strings) im Build ===" && grep -oiE '"[a-z_]*relation[a-z_]*"|component_[a-z]*relation[a-z_]*|[a-zA-Z]*Relation(Component|System|Manager|Service)?' browser/index.js | sort | uniq -c | sort -rn | head -30

Extract relation API identifiers from build


=== relation-bezogene Bezeichner (quoted strings) im Build ===
  29 relation
  16 relationManager
  11 positionRelation
  11 Relation
  10 rotationRelation
  10 componentToRelation
   7 getRelation
   3 trackRelation
   3 resolveRelation
   3 removeRelation
   3 addRelation
   3 "relationManager"
   2 untrackRelation
   2 filterRelation
   2 createRelation
   2 "rotationRelation"
   2 "relations"
   2 "positionRelation"
   2 "attachment_relations"
   2 "Relations"
   1 findRelation
   1 DestroyRelation
   1 CreateRelation
   1 BillboardRotationRelation
   1 BillboardPositionRelation
   1 "relationsProcessor"
   1 "relationsFeature"
   1 "relationsAttachmentDes"
   1 "relationResolver"
   1 "relationRenderer"


FireVisual hat ein natives Relations-System: `positionRelation`/`rotationRelation` binden Entities aneinander, verwaltet von `relationManager`, plus einen `"attachment_relations"`-Component. Ich hole den Kontext um diese Bezeichner.


echo "=== attachment_relations context ===" && grep -oE '.{120}attachment_relations.{160}' browser/index.js | head -4; echo; echo "=== positionRelation / rotationRelation shape ==="; grep -oE '.{60}(position|rotation)Relation.{120}' browser/index.js | head -6

Extract relation component structure from build


=== attachment_relations context ===
 extends o.Model.Model{}class ne extends o.Model.AttachmentModel{constructor(){super(...arguments),this._resourceClass="attachment_relations",this.relations=[]}}const re=new a.InjectionType("Relations");var ie;let se=class extends o.BaseDeserializer{canExecute(e){return o.Model.AttachmentModel.isTyp

=== positionRelation / rotationRelation shape ===
TW(){if(!this._LTW){const e=this.getRotation(this._settings.rotationRelation);let t=this.getPosition(this._settings.positionRelation);const n=this.pivotFromNormalized;if(!n?.equal(c.Vector3.zero))
._viewportLTW.forward))>Math.asin(.3))}return this._LTW}get positionRelation(){return this._settings.positionRelation}set positionRelation(e){e!=this._settings.positionRelation&&(this._settings.po
is._LTW=null,this.sync(s.relation),this.sync(s.matrix))}get rotationRelation(){return this._settings.rotationRelation}set rotationRelation(e){e!=this._settings.rotationRelation&&(this._settings.ro
oint(){let e=this.pivotFromNormalized;return this._settings.positionRelation==r.flip&&this.cameraBehindObject()&&(e=c.Vector3.create([-e.x,e.y,e.z])),e}get position(){return this._settings.positio
ault:!1},pivot:{_type:c.Vector2,default:()=>c.Vector2.zero},positionRelation:{_type:"number",default:r.none},rotationRelation:{_type:"number",default:i.viewport},referenceLine:{_type:g,default:()=
set:e=>this.entity.yAxisLock=e}),this.registerConfigurator("rotationRelation",l.NumberConfigurator,{type:"dropdown",get:()=>this.entity.rotationRelation,set:e=>this.entity.rotationRelation=e,optio


Der relevante Fund: **`attachment_relations`** ist ein Component (`class extends AttachmentModel { _resourceClass="attachment_relations"; relations=[] }`) — eine Entity trägt eine Liste von Attachment-Relations zu anderen Entities, verwaltet vom `relationManager`. (Die `positionRelation`/`rotationRelation` oben sind dagegen Billboard-Kamera-Settings, nicht Assembly.) Ich hole die Struktur einer einzelnen Relation und die `relationManager`-API.


echo "=== AttachmentModel + relation fields ===" && grep -oE 'AttachmentModel[^;]{0,220}' browser/index.js | head -3; echo; echo "=== addRelation / createRelation signatures ==="; grep -oE '.{20}(add|create|resolve|track)Relation[^;]{0,160}' browser/index.js | head -8; echo; echo "=== relation object keys (sourceEntity/targetEntity/type/etc) ==="; grep -oE '.{40}(sourceEntity|targetEntity|relationType|attachTo|anchorEntity|parentEntity)[^;]{0,80}' browser/index.js | head -8

Extract AttachmentModel and relation structure


=== AttachmentModel + relation fields ===
AttachmentModel:()=>Ks,BasicMaterialModel:()=>Rs,BufferGeometryModel:()=>bs,BufferImageType:()=>ks,CameraModel:()=>Vs,ComponentModel:()=>Bs,FaceGeometryModel:()=>Ms,HandleModel:()=>$s,LightModel:()=>js,LineGeometryModel:()=>Ss,LineMate
AttachmentModel:()=>ne,RelationsFeature:()=>V,RelationsProcessor:()=>ue,RelationsType:()=>re,ResolutionScheduler:()=>U,RotationConstraint:()=>R,RotationConstraintDeserializer:()=>X,RotationConstraintModel:()=>q,RotationConstraintSolver
AttachmentModel{constructor(){super(...arguments),this._resourceClass="attachment_relations",this.relations=[]}}const re=new a.InjectionType("Relations")

=== addRelation / createRelation signatures ===
e[1])]);return this.addRelationToComponent(t,t.components[0]),this.addRelationToComponent(t,t.components[1]),this.registeredContexts[t.index]={entity:t},this.dispatchEvent(new g(t)),t}getRela
x].filter(t=>t!=e))}addRelationToComponent(e,t){this.componentToRelationMap[t.index]||(this.componentToRelationMap[t.index]=[]),this.componentToRelationMap[t.index].push(e)}resolveObject(e){l
nManager,g,e=>this._trackRelation(e.entity)),this.addEventListener(this.relationManager,f,e=>this._untrackRelation(e.entity)),this.updatePipeline&&this._callbackWrapper.push(this.updatePipeline
ger.findAll())this._trackRelation(e)}_trackRelation(e){for(const t of e.components){const e=t.index,n=this._refCounts.get(e)??0
tions.set(e,n)}}}_untrackRelation(e){for(const t of e.components){const e=t.index,n=this._refCounts.get(e)??0,r=Math.max(0,n-1)
is.relationsFeature.resolveRelations(t)
lationsFeature=this}createRelation(e,t){const n=this.relationManager.create([e,t])
ionsForComponent(e)}resolveRelations(e){return this.getComponent(D).resolveAll(e)}getCommands(){return{resolve_relations:async e=>{let t

=== relation object keys (sourceEntity/targetEntity/type/etc) ===
ement(){return this.renderer.domElement}attachToContainer(e){e.appendChild(this.renderer.domElement),null!=this.checkViewportInt
andle({id:"root"});return{environment:r,attachToContainer:e=>a.attachToContainer(e),updateScene:async e=>{await o.instantiateSce


Genug für ein belastbares Bild. Ein letzter Blick, ob die Relation an PMIs hängt und einen Offset/Transform trägt.


echo "=== relationManager.create / Relations-Model fields ===" && grep -oE 'relationManager\.create[^;]{0,80}|resolveObject\(e\)\{[^}]{0,200}' browser/index.js | head -6; echo; echo "=== relation + pmi/anchor/offset nähe ==="; grep -oE '.{30}(Pmi|PMI|anchor|offset|snapPoint)[^;]{0,60}Relation|Relation[^;]{0,60}(Pmi|PMI|anchor|offset)' browser/index.js | head -6; echo; echo "=== resolve_relations command + RelationsProcessor ==="; grep -oE 'RelationsProcessor[^;]{0,150}|resolve_relations[^;]{0,120}' browser/index.js | head -4

Probe relation attach point and resolver


=== relationManager.create / Relations-Model fields ===
resolveObject(e){let t=this.selectorEngine.find(e,this._context);if(t){if(t instanceof u.Handle&&(t=t.transform),!u.TransformationProvider.isType(t))throw new Error(`ReferenceManager: '${(0,p.getConstructorName)(t)
resolveObject(e){let t="string"==typeof e?this.selectorEngine.find(e):e;if(t instanceof o.Handle&&(t=t.transform),!o.TransformationProvider.isType(t))throw new Error((0,l.logMessage)(this,"resolveObject",`result '${(0
relationManager.create([e,t])

=== relation + pmi/anchor/offset nähe ===

=== resolve_relations command + RelationsProcessor ===
RelationsProcessor:()=>ue,RelationsType:()=>re,ResolutionScheduler:()=>U,RotationConstraint:()=>R,RotationConstraintDeserializer:()=>X,RotationConstraintModel:()=>q,Rot
resolve_relations:async e=>{let t


Ja — FireVisual hat ein eigenes, natives Einbau-/Assembly-System. Zusammengefasst aus dem Build (Fremdcode, minifiziert — daher teils Rekonstruktion, nicht jede Semantik sicher):

**Modul `@fire-visual/relations`**

- **Component `attachment_relations`** (`AttachmentModel`): sitzt an einem Handle und hält `relations: []`. Das ist die Einbau-Relation als Szenengraph-Komponente — eine Entity trägt ihre Verknüpfungen zu anderen.
- **`relationManager`**: `create([a, b])` verknüpft **zwei** Transforms/Handles; dazu `addRelation`/`removeRelation`/`getRelation`/`findAll`/`trackRelation`/`untrackRelation`. Intern eine `componentToRelationMap` + Refcounts → ein **Relationsgraph über Komponenten**.
- **Auflösung**: `RelationsFeature` / `RelationsProcessor` / `ResolutionScheduler` lösen den Attachment-Graphen auf → Transforms; als Command `resolve_relations`. Zusätzlich `RotationConstraintModel` / `RotationConstraintSolver` — native Rotations-Constraints (das Gegenstück zu unserem `dockRotation`).
- **Bindung über Selektoren**: `resolveObject` geht per `selectorEngine.find(selector)` → `Handle` → dessen `transform`. Relations verknüpfen also aufgelöste Transforms — dieselbe `#id` / `.name-…`-Selektorwelt, die wir beim Placement schon nutzen.

**Unsicher** (nicht sauber ablesbar): ob eine Relation einen PMI-verankerten Offset trägt oder reines Transform-zu-Transform ist, und das genaue Relation-Payload.

**Einordnung fürs aktuelle Thema:** Das ist praktisch das native Äquivalent zu unserem `GroupProcessor` (Kanten `from/to` + `computeLayout`): Attachment-Graph + Resolver, direkt in fire. Würden wir Kopplung/Assembly als fire-Relations ausdrücken (`attachment_relations` emittieren, `resolve_relations` laufen lassen), wären Steher↔Zaun-Kopplung und „ganze Gruppe bewegt sich" nativ — und die Layout-Rechnung läge bei fire statt in unserem Replay. Das berührt zwei offene Punkte zugleich: die atomare Gruppe **und** die `setScene`-Performance (fire löst inkrementell auf, statt dass wir den Voll-Graphen neu schicken).

Der Preis: das Assembly-Modell wandert in fire (Fremdcode) und ersetzt unseren event-sourced Grouping-Ansatz an der Stelle. Das ist eine Architekturentscheidung, keine reine Umsetzung — deshalb lege ich es dir als Befund hin, ohne es einzubauen.


DU HAST DIE SOURCEN UNTER FIRE VISUAL


for d in /Users/marcus.hinz/projects/geko-fire /Users/marcus.hinz/projects/adesso-cpq /Users/marcus.hinz/projects/geko-core /Users/marcus.hinz/projects/geko-infra; do echo "### $d"; find "$d" -type d -path '*fire-visual*' 2>/dev/null | grep -viE '\.git/' | head; done 2>/dev/null

Locate fire-visual source directories


### /Users/marcus.hinz/projects/geko-fire
### /Users/marcus.hinz/projects/adesso-cpq
### /Users/marcus.hinz/projects/geko-core
### /Users/marcus.hinz/projects/geko-infra


for d in /Users/marcus.hinz/projects/geko-fire /Users/marcus.hinz/projects/adesso-cpq /Users/marcus.hinz/projects/geko-core /Users/marcus.hinz/projects/geko-infra /Users/marcus.hinz/claude-projects/robotunits; do r=$(find "$d" -maxdepth 6 -type d -name '@fire-visual' 2>/dev/null | head -3); [ -n "$r" ] && echo "### $d" && echo "$r"; done; echo "=== any relations source .ts anywhere in projects ==="; find /Users/marcus.hinz/projects -maxdepth 8 -type d -name 'relations' -path '*fire*' 2>/dev/null | head

Find @fire-visual package dirs and relations source


=== any relations source .ts anywhere in projects ===
/Users/marcus.hinz/projects/fire-visual/threed/relations


Gefunden: die echten Quellen liegen unter `/Users/marcus.hinz/projects/fire-visual/`. Ich lese das `threed/relations`-Modul.


cd /Users/marcus.hinz/projects/fire-visual/threed/relations && echo "=== tree ===" && find . -type f \( -name '*.ts' -o -name '*.md' \) | grep -viE '\.test\.|\.spec\.' | head -40; echo "=== files count ==="; find . -name '*.ts' | wc -l

List relations module source files


=== tree ===
./MODULE.md
./README.md
./vitest.config.ts
./.vitest/setup.ts
./dist/types/relation.d.ts
./dist/types/environmentCommandRegistry.augmentation.d.ts
./dist/types/relationsFeature.d.ts
./dist/types/event.d.ts
./dist/types/relationsFeature.settings.d.ts
./dist/types/index.d.ts
./dist/types/relationManager.d.ts
./module/tests/empty.test-d.ts
./module/tests/helper.ts
./module/src/relationsFeature.settings.ts
./module/src/relationManager.ts
./module/src/README.md
./module/src/relationsFeature.ts
./module/src/index.ts
./module/src/event.ts
./module/src/relation.ts
./module/src/environmentCommandRegistry.augmentation.ts
./dist/types/dataManager/distanceConstraintDeserializer.d.ts
./dist/types/dataManager/axisOffsetConstraintDeserializer.d.ts
./dist/types/dataManager/relationsType.d.ts
./dist/types/dataManager/relationModel.d.ts
./dist/types/dataManager/axisAlignmentConstraintDeserializer.d.ts
./dist/types/dataManager/rotationConstraintDeserializer.d.ts
./dist/types/dataManager/positionConstraintDeserializer.d.ts
./dist/types/dataManager/distanceConstraintModel.d.ts
./dist/types/dataManager/relationsProcessor.d.ts
./dist/types/dataManager/axisAlignmentConstraintModel.d.ts
./dist/types/dataManager/constraintModel.d.ts
./dist/types/dataManager/relationsAttachmentModel.d.ts
./dist/types/dataManager/relationsAttachmentDeserializer.d.ts
./dist/types/dataManager/axisOffsetConstraintModel.d.ts
./dist/types/dataManager/rotationConstraintModel.d.ts
./dist/types/dataManager/index.d.ts
./dist/types/dataManager/positionConstraintModel.d.ts
./dist/types/editor/index.d.ts
./dist/types/editor/relationRenderer.d.ts
=== files count ===
     109
Shell cwd was reset to /Users/marcus.hinz/claude-projects/robotunits


Volle Quellen samt Doku. Ich lese die Übersicht und die Kernklassen.