655	        };
656	    }
657
658	    // ---- helpers -----------------------------------------------------------
659
660	    private pmiLocal(rootHandleId: string, pmiId: string): Vec3 {
661	        const handle = this.handles.get(rootHandleId);
662	        for (const comp of handle?.components ?? []) {
663	            if (comp._resourceClass === "component_pmi" && comp.id === pmiId) {
664	                return comp.properties.position.value;
665	            }
666	        }
667	        return [0, 0, 0];
668	    }
669
670	    // The docking point's frame orientation, from its `properties.rotation` (Euler radians, ZYX).
671	    // Most points carry no rotation (CS_LINKS/CS_RECHTS = [0,0,0] → identity); the post/branch points
672	    // (CS_EIN_*) carry the angle that turns a part onto them.
673	    private pmiRotation(rootHandleId: string, pmiId: string): Quat {
674	        const handle = this.handles.get(rootHandleId);
675	        for (const comp of handle?.components ?? []) {
676	            if (comp._resourceClass === "component_pmi" && comp.id === pmiId) {
677	                const rot = comp.properties?.rotation?.value;
678	                return rot ? eulerZyxRadToQuat(rot as Vec3) : IDENTITY_QUAT;
679	            }
680	        }
681	        return IDENTITY_QUAT;
682	    }
683
684	    // Relative rotation of the join part w.r.t. the anchor part for a dock: aligning the join point's
685	    // frame onto the anchor point's turns the join part by R_anchorPmi ∘ R_joinPmi⁻¹. Identity when
686	    // both points carry no rotation (the plain left/right snap). This is what fills `Edge.rot`.
687	    dockRotation(anchorRoot: string, anchorPmi: string, joinRoot: string, joinPmi: string): Quat {
688	        return quatMul(this.pmiRotation(anchorRoot, anchorPmi), quatConj(this.pmiRotation(joinRoot, joinPmi)));
689	    }
690
691	    private rootTransform(handle: Handle): any {
692	        return handle?.components?.find((c: any) => c._resourceClass === "component_transform") ?? null;
693	    }
694
695	    private roleOf(handle: Handle): NodeRole {
696	        for (const t of handle?.tags ?? []) {
697	            if (typeof t === "string" && t.startsWith("role-")) return t.slice(5) as NodeRole;
698	        }
699	        return ROLE.geometry;
700	    }
701
702	    // Index every materialised node by handle id → {parent handle id, role}, resolving each
703	    // transform's parentKey to the owning handle. The role resolver walks this.
704	    private buildNodeIndex(handles: Handle[]): void {
705	        const keyToId = new Map<string, string>();
706	        for (const h of handles) {
707	            const tr = this.rootTransform(h);
708	            if (tr?.key) keyToId.set(tr.key, h.id);
709	        }
710	        this.nodeIndex = new Map();
711	        for (const h of handles) {
712	            const tr = this.rootTransform(h);
713	            const parentId = tr?.parentKey ? keyToId.get(tr.parentKey) ?? null : null;
714	            this.nodeIndex.set(h.id, { parentId, role: this.roleOf(h) });
715	        }
716	    }
717	}
718

        };
    }

    // ---- helpers -----------------------------------------------------------

    private pmiLocal(rootHandleId: string, pmiId: string): Vec3 {
        const handle = this.handles.get(rootHandleId);
        for (const comp of handle?.components ?? []) {
            if (comp._resourceClass === "component_pmi" && comp.id === pmiId) {
                return comp.properties.position.value;
            }
        }
        return [0, 0, 0];
    }

    // The docking point's frame orientation, from its `properties.rotation` (Euler radians, ZYX).
    // Most points carry no rotation (CS_LINKS/CS_RECHTS = [0,0,0] → identity); the post/branch points
    // (CS_EIN_*) carry the angle that turns a part onto them.
    private pmiRotation(rootHandleId: string, pmiId: string): Quat {
        const handle = this.handles.get(rootHandleId);
        for (const comp of handle?.components ?? []) {
            if (comp._resourceClass === "component_pmi" && comp.id === pmiId) {
                const rot = comp.properties?.rotation?.value;
                return rot ? eulerZyxRadToQuat(rot as Vec3) : IDENTITY_QUAT;
            }
        }
        return IDENTITY_QUAT;
    }

    // Relative rotation of the join part w.r.t. the anchor part for a dock: aligning the join point's
    // frame onto the anchor point's turns the join part by R_anchorPmi ∘ R_joinPmi⁻¹. Identity when
    // both points carry no rotation (the plain left/right snap). This is what fills `Edge.rot`.
    dockRotation(anchorRoot: string, anchorPmi: string, joinRoot: string, joinPmi: string): Quat {
        return quatMul(this.pmiRotation(anchorRoot, anchorPmi), quatConj(this.pmiRotation(joinRoot, joinPmi)));
    }

    private rootTransform(handle: Handle): any {
        return handle?.components?.find((c: any) => c._resourceClass === "component_transform") ?? null;
    }

    private roleOf(handle: Handle): NodeRole {
        for (const t of handle?.tags ?? []) {
            if (typeof t === "string" && t.startsWith("role-")) return t.slice(5) as NodeRole;
        }
        return ROLE.geometry;
    }

    // Index every materialised node by handle id → {parent handle id, role}, resolving each
    // transform's parentKey to the owning handle. The role resolver walks this.
    private buildNodeIndex(handles: Handle[]): void {
        const keyToId = new Map<string, string>();
        for (const h of handles) {
            const tr = this.rootTransform(h);
            if (tr?.key) keyToId.set(tr.key, h.id);
        }
        this.nodeIndex = new Map();
        for (const h of handles) {
            const tr = this.rootTransform(h);
            const parentId = tr?.parentKey ? keyToId.get(tr.parentKey) ?? null : null;
            this.nodeIndex.set(h.id, { parentId, role: this.roleOf(h) });
        }
    }
}


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -n "computeLayout\|Edge\b\|fromPmi\|toPmi\|\.rot\b\|makeComponentGroup\|pmiWorld\|pmiLocal\|pmiRotation" src/configurator/model/GroupProcessor.ts | head -40

Find layout/merge placement code


69:// `to` is positioned so its `toPmi` coincides with `from`'s `fromPmi`.
70:export interface Edge {
73:    fromPmi: string; // docking point id on `from`
74:    toPmi: string; // docking point id on `to`
85:    edges: Edge[];
128:    newGroups: Array<{ members: string[]; edges: Edge[]; groupId: string; groupKey: string; transform: Transform }>;
137:    newGroups: Array<{ members: string[]; edges: Edge[]; groupId: string; groupKey: string; transform: Transform }>;
202:    // occupies its `fromPmi` on `from` and its `toPmi` on `to`. Open ends are everything else.
207:                out.push({ element: e.from, pmi: e.fromPmi });
208:                out.push({ element: e.to, pmi: e.toPmi });
270:        const layout = this.computeLayout(g);
287:        const newGroups = components.map((members) => this.makeComponentGroup(g, members, remEdges, layout));
320:            const layout = this.computeLayout(g);
327:                const ng = this.makeComponentGroup(g, comp, g.edges, layout);
332:                newGroups.push(this.makeComponentGroup(g, comp, g.edges, layout));
345:    private makeComponentGroup(
348:        edges: Edge[],
350:    ): { members: string[]; edges: Edge[]; groupId: string; groupKey: string; transform: Transform } {
354:        const w = quatRotate(g.transform.rot, origin.pos);
362:                rot: quatMul(g.transform.rot, origin.rot),
460:                    fromPmi: event.anchorPmiId,
461:                    toPmi: event.joinPmiId,
462:                    rot: event.rot,
512:            const layout = this.computeLayout(g);
523:                tr.rotation = lay.rot;
537:    // Each edge fixes that `to.toPmi` coincides with `from.fromPmi` AND that `to` is rotated by
538:    // `edge.rot` relative to `from` (identity = shared orientation, the left/right case). With the
540:    //   forward (cur = from):  pos(to)   = pos(from) + R_from·pmi(from,fromPmi) − R_to·pmi(to,toPmi)
541:    //   reverse (cur = to):    pos(from) = pos(to)   + R_to·pmi(to,toPmi)       − R_from·pmi(from,fromPmi)
542:    // and R_to = R_from ∘ edge.rot (forward), R_from = R_to ∘ edge.rot⁻¹ (reverse). With every rot
544:    private computeLayout(g: Group): Map<string, Transform> {
555:            const rot = e.rot ?? IDENTITY_QUAT;
556:            link(e.from, { neighbour: e.to, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: false, rot });
557:            link(e.to, { neighbour: e.from, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: true, rot });
571:                const at = this.pmiLocal(fromRoot, s.atPmi);
572:                const dock = this.pmiLocal(toRoot, s.dockPmi);
573:                // Neighbour's accumulated rotation: compose edge.rot forward, its inverse on reverse.
574:                const rotN = s.reverse ? quatMul(t.rot, quatConj(s.rot)) : quatMul(t.rot, s.rot);
575:                const fromRot = s.reverse ? rotN : t.rot;
576:                const toRot = s.reverse ? t.rot : rotN;


Wenn ich auf _0 klicke, baout er nach vorn


Rollenförderer Visualisierung und Fehler beheben


544	    private computeLayout(g: Group): Map<string, Transform> {
545	        const out = new Map<string, Transform>();
546	        if (!g.members.length) return out;
547
548	        type Step = { neighbour: string; atPmi: string; dockPmi: string; reverse: boolean; rot: Quat };
549	        const adj = new Map<string, Step[]>();
550	        const link = (node: string, step: Step) => {
551	            if (!adj.has(node)) adj.set(node, []);
552	            adj.get(node)!.push(step);
553	        };
554	        for (const e of g.edges) {
555	            const rot = e.rot ?? IDENTITY_QUAT;
556	            link(e.from, { neighbour: e.to, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: false, rot });
557	            link(e.to, { neighbour: e.from, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: true, rot });
558	        }
559
560	        const origin = g.members[0];
561	        out.set(origin, { pos: [0, 0, 0], rot: IDENTITY_QUAT });
562	        const queue = [origin];
563	        while (queue.length) {
564	            const cur = queue.shift()!;
565	            const t = out.get(cur)!;
566	            for (const s of adj.get(cur) ?? []) {
567	                if (out.has(s.neighbour)) continue;
568	                // `at` is on the `from` side, `dock` on the `to` side.
569	                const fromRoot = s.reverse ? s.neighbour : cur;
570	                const toRoot = s.reverse ? cur : s.neighbour;
571	                const at = this.pmiLocal(fromRoot, s.atPmi);
572	                const dock = this.pmiLocal(toRoot, s.dockPmi);
573	                // Neighbour's accumulated rotation: compose edge.rot forward, its inverse on reverse.
574	                const rotN = s.reverse ? quatMul(t.rot, quatConj(s.rot)) : quatMul(t.rot, s.rot);
575	                const fromRot = s.reverse ? rotN : t.rot;
576	                const toRot = s.reverse ? t.rot : rotN;
577	                const a = quatRotate(fromRot, at);
578	                const d = quatRotate(toRot, dock);
579	                // pos(to) = pos(from) + R_from·at − R_to·dock. On reverse, `cur` IS `to`, so solve for
580	                // `from`: pos(from) = pos(to) + R_to·dock − R_from·at — same vector, opposite sign.
581	                const sign = s.reverse ? -1 : 1;
582	                out.set(s.neighbour, {
583	                    pos: [
584	                        t.pos[0] + sign * (a[0] - d[0]),
585	                        t.pos[1] + sign * (a[1] - d[1]),
586	                        t.pos[2] + sign * (a[2] - d[2]),
587	                    ],
588	                    rot: rotN,
589	                });
590	                queue.push(s.neighbour);
591	            }
592	        }
593	        return out;
594	    }
595
596	    // Connected components of `members` under `edges` (undirected), via BFS.
597	    private connectedComponents(members: string[], edges: Edge[]): string[][] {
598	        const adj = new Map<string, string[]>();
599	        for (const m of members) adj.set(m, []);
600	        for (const e of edges) {
601	            adj.get(e.from)?.push(e.to);
602	            adj.get(e.to)?.push(e.from);
603	        }
604	        const seen = new Set<string>();
605	        const comps: string[][] = [];
606	        for (const m of members) {
607	            if (seen.has(m)) continue;
608	            const comp: string[] = [];
609	            const queue = [m];
610	            seen.add(m);
611	            while (queue.length) {
612	                const cur = queue.shift()!;
613	                comp.push(cur);
614	                for (const nb of adj.get(cur) ?? [])
615	                    if (!seen.has(nb)) {
616	                        seen.add(nb);
617	                        queue.push(nb);
618	                    }
619	            }
620	            comps.push(comp);
621	        }
622	        return comps;
623	    }
624
625	    private groupNodeHandle(g: Group): Handle {
626	        return {
627	            id: g.key,
628	            tags: [roleTag(ROLE.assembly)],
629	            components: [
630	                {
631	                    _resourceClass: "component_transform",
632	                    key: g.key,
633	                    parentKey: ROOT_KEY,
634	                    translation: g.transform.pos,
635	                    rotation: g.transform.rot,
636	                    scale: [1, 1, 1],
637	                },
638	            ],
639	        };
640	    }
641
642	    private sceneRootHandle(): Handle {
643	        return {
644	            id: ROOT_KEY,
645	            tags: [roleTag(ROLE.scene)],
646	            components: [
647	                {
648	                    _resourceClass: "component_transform",
649	                    key: ROOT_KEY,
650	                    translation: [0, 0, 0],
651	                    rotation: [0, 0, 0],
652	                    scale: [1, 1, 1],
653	                },
654	            ],
655	        };
656	    }
657
658	    // ---- helpers -----------------------------------------------------------

    private computeLayout(g: Group): Map<string, Transform> {
        const out = new Map<string, Transform>();
        if (!g.members.length) return out;

        type Step = { neighbour: string; atPmi: string; dockPmi: string; reverse: boolean; rot: Quat };
        const adj = new Map<string, Step[]>();
        const link = (node: string, step: Step) => {
            if (!adj.has(node)) adj.set(node, []);
            adj.get(node)!.push(step);
        };
        for (const e of g.edges) {
            const rot = e.rot ?? IDENTITY_QUAT;
            link(e.from, { neighbour: e.to, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: false, rot });
            link(e.to, { neighbour: e.from, atPmi: e.fromPmi, dockPmi: e.toPmi, reverse: true, rot });
        }

        const origin = g.members[0];
        out.set(origin, { pos: [0, 0, 0], rot: IDENTITY_QUAT });
        const queue = [origin];
        while (queue.length) {
            const cur = queue.shift()!;
            const t = out.get(cur)!;
            for (const s of adj.get(cur) ?? []) {
                if (out.has(s.neighbour)) continue;
                // `at` is on the `from` side, `dock` on the `to` side.
                const fromRoot = s.reverse ? s.neighbour : cur;
                const toRoot = s.reverse ? cur : s.neighbour;
                const at = this.pmiLocal(fromRoot, s.atPmi);
                const dock = this.pmiLocal(toRoot, s.dockPmi);
                // Neighbour's accumulated rotation: compose edge.rot forward, its inverse on reverse.
                const rotN = s.reverse ? quatMul(t.rot, quatConj(s.rot)) : quatMul(t.rot, s.rot);
                const fromRot = s.reverse ? rotN : t.rot;
                const toRot = s.reverse ? t.rot : rotN;
                const a = quatRotate(fromRot, at);
                const d = quatRotate(toRot, dock);
                // pos(to) = pos(from) + R_from·at − R_to·dock. On reverse, `cur` IS `to`, so solve for
                // `from`: pos(from) = pos(to) + R_to·dock − R_from·at — same vector, opposite sign.
                const sign = s.reverse ? -1 : 1;
                out.set(s.neighbour, {
                    pos: [
                        t.pos[0] + sign * (a[0] - d[0]),
                        t.pos[1] + sign * (a[1] - d[1]),
                        t.pos[2] + sign * (a[2] - d[2]),
                    ],
                    rot: rotN,
                });
                queue.push(s.neighbour);
            }
        }
        return out;
    }

    // Connected components of `members` under `edges` (undirected), via BFS.
    private connectedComponents(members: string[], edges: Edge[]): string[][] {
        const adj = new Map<string, string[]>();
        for (const m of members) adj.set(m, []);
        for (const e of edges) {
            adj.get(e.from)?.push(e.to);
            adj.get(e.to)?.push(e.from);
        }
        const seen = new Set<string>();
        const comps: string[][] = [];
        for (const m of members) {
            if (seen.has(m)) continue;
            const comp: string[] = [];
            const queue = [m];
            seen.add(m);
            while (queue.length) {
                const cur = queue.shift()!;
                comp.push(cur);
                for (const nb of adj.get(cur) ?? [])
                    if (!seen.has(nb)) {
                        seen.add(nb);
                        queue.push(nb);
                    }
            }
            comps.push(comp);
        }
        return comps;
    }

    private groupNodeHandle(g: Group): Handle {
        return {
            id: g.key,
            tags: [roleTag(ROLE.assembly)],
            components: [
                {
                    _resourceClass: "component_transform",
                    key: g.key,
                    parentKey: ROOT_KEY,
                    translation: g.transform.pos,
                    rotation: g.transform.rot,
                    scale: [1, 1, 1],
                },
            ],
        };
    }

    private sceneRootHandle(): Handle {
        return {
            id: ROOT_KEY,
            tags: [roleTag(ROLE.scene)],
            components: [
                {
                    _resourceClass: "component_transform",
                    key: ROOT_KEY,
                    translation: [0, 0, 0],
                    rotation: [0, 0, 0],
                    scale: [1, 1, 1],
                },
            ],
        };
    }

    // ---- helpers -----------------------------------------------------------


Der Layout-Code positioniert nur über PMI-Positionen plus `edge.rot` (aus `dockRotation`). Ich rechne die tatsächliche Kopie-Position für jeden Anker-Kandidaten durch und vergleiche mit der Marker-Richtung — statt weiter zu raten. Zuerst die Euler-Konvention:


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -n "eulerZyxRadToQuat\|quatMul\|quatConj\|quatRotate\|export function" src/configurator/model/transformMath.ts | head

List transformMath exports


15:export function quatMul(a: Quat, b: Quat): Quat {
26:export function quatConj(q: Quat): Quat {
31:export function quatRotate(q: Quat, v: Vec3): Vec3 {
44:export function compose(a: Transform, b: Transform): Transform {
45:    const rotated = quatRotate(a.rot, b.pos);
47:        rot: quatMul(a.rot, b.rot),
52:export function invert(t: Transform): Transform {
53:    const rot = quatConj(t.rot);
54:    const p = quatRotate(rot, t.pos);
62:export function eulerZyxRadToQuat(rad: Vec3): Quat {


1	// Rigid transforms (rotation + translation, unit scale), represented as {pos, rot}.
2	// Quaternions follow fire-visual's order: rot = [w, x, y, z] (w first). Enough to apply a
3	// part's rigid motion to its connected followers without pulling in a matrix library.
4
5	export type Vec3 = [number, number, number];
6	export type Quat = [number, number, number, number]; // [w, x, y, z]
7
8	export interface Transform {
9	    pos: Vec3;
10	    rot: Quat;
11	}
12
13	export const IDENTITY_QUAT: Quat = [1, 0, 0, 0];
14
15	export function quatMul(a: Quat, b: Quat): Quat {
16	    const [aw, ax, ay, az] = a;
17	    const [bw, bx, by, bz] = b;
18	    return [
19	        aw * bw - ax * bx - ay * by - az * bz,
20	        aw * bx + ax * bw + ay * bz - az * by,
21	        aw * by - ax * bz + ay * bw + az * bx,
22	        aw * bz + ax * by - ay * bx + az * bw,
23	    ];
24	}
25
26	export function quatConj(q: Quat): Quat {
27	    return [q[0], -q[1], -q[2], -q[3]];
28	}
29
30	// Rotate a vector by a unit quaternion [w, x, y, z]: v + 2w(q×v) + 2 q×(q×v).
31	export function quatRotate(q: Quat, v: Vec3): Vec3 {
32	    const [w, x, y, z] = q;
33	    const tx = 2 * (y * v[2] - z * v[1]);
34	    const ty = 2 * (z * v[0] - x * v[2]);
35	    const tz = 2 * (x * v[1] - y * v[0]);
36	    return [
37	        v[0] + w * tx + (y * tz - z * ty),
38	        v[1] + w * ty + (z * tx - x * tz),
39	        v[2] + w * tz + (x * ty - y * tx),
40	    ];
41	}
42
43	// a ∘ b: apply b first, then a.
44	export function compose(a: Transform, b: Transform): Transform {
45	    const rotated = quatRotate(a.rot, b.pos);
46	    return {
47	        rot: quatMul(a.rot, b.rot),
48	        pos: [rotated[0] + a.pos[0], rotated[1] + a.pos[1], rotated[2] + a.pos[2]],
49	    };
50	}
51
52	export function invert(t: Transform): Transform {
53	    const rot = quatConj(t.rot);
54	    const p = quatRotate(rot, t.pos);
55	    return { rot, pos: [-p[0], -p[1], -p[2]] };
56	}
57
58	// Euler angles in RADIANS → quaternion [w, x, y, z], applied in fire-visual's default ZYX order.
59	// This mirrors how fire-visual ingests rotations: a 3-component rotation property is read as an
60	// Euler with order ZYX (QuaternionType.deserialize). The PMI docking points carry their frame
61	// orientation this way — e.g. CS_EIN_VORNE = [π/2, 0, π/2], while CS_LINKS/CS_RECHTS = [0, 0, 0].
62	export function eulerZyxRadToQuat(rad: Vec3): Quat {
63	    const x = rad[0],
64	        y = rad[1],
65	        z = rad[2];
66	    const cx = Math.cos(x / 2),
67	        cy = Math.cos(y / 2),
68	        cz = Math.cos(z / 2);
69	    const sx = Math.sin(x / 2),
70	        sy = Math.sin(y / 2),
71	        sz = Math.sin(z / 2);
72	    return [
73	        cx * cy * cz + sx * sy * sz,
74	        sx * cy * cz - cx * sy * sz,
75	        cx * sy * cz + sx * cy * sz,
76	        cx * cy * sz - sx * sy * cz,
77	    ];
78	}
79

// Rigid transforms (rotation + translation, unit scale), represented as {pos, rot}.
// Quaternions follow fire-visual's order: rot = [w, x, y, z] (w first). Enough to apply a
// part's rigid motion to its connected followers without pulling in a matrix library.

export type Vec3 = [number, number, number];
export type Quat = [number, number, number, number]; // [w, x, y, z]

export interface Transform {
    pos: Vec3;
    rot: Quat;
}

export const IDENTITY_QUAT: Quat = [1, 0, 0, 0];

export function quatMul(a: Quat, b: Quat): Quat {
    const [aw, ax, ay, az] = a;
    const [bw, bx, by, bz] = b;
    return [
        aw * bw - ax * bx - ay * by - az * bz,
        aw * bx + ax * bw + ay * bz - az * by,
        aw * by - ax * bz + ay * bw + az * bx,
        aw * bz + ax * by - ay * bx + az * bw,
    ];
}

export function quatConj(q: Quat): Quat {
    return [q[0], -q[1], -q[2], -q[3]];
}

// Rotate a vector by a unit quaternion [w, x, y, z]: v + 2w(q×v) + 2 q×(q×v).
export function quatRotate(q: Quat, v: Vec3): Vec3 {
    const [w, x, y, z] = q;
    const tx = 2 * (y * v[2] - z * v[1]);
    const ty = 2 * (z * v[0] - x * v[2]);
    const tz = 2 * (x * v[1] - y * v[0]);
    return [
        v[0] + w * tx + (y * tz - z * ty),
        v[1] + w * ty + (z * tx - x * tz),
        v[2] + w * tz + (x * ty - y * tx),
    ];
}

// a ∘ b: apply b first, then a.
export function compose(a: Transform, b: Transform): Transform {
    const rotated = quatRotate(a.rot, b.pos);
    return {
        rot: quatMul(a.rot, b.rot),
        pos: [rotated[0] + a.pos[0], rotated[1] + a.pos[1], rotated[2] + a.pos[2]],
    };
}

export function invert(t: Transform): Transform {
    const rot = quatConj(t.rot);
    const p = quatRotate(rot, t.pos);
    return { rot, pos: [-p[0], -p[1], -p[2]] };
}

// Euler angles in RADIANS → quaternion [w, x, y, z], applied in fire-visual's default ZYX order.
// This mirrors how fire-visual ingests rotations: a 3-component rotation property is read as an
// Euler with order ZYX (QuaternionType.deserialize). The PMI docking points carry their frame
// orientation this way — e.g. CS_EIN_VORNE = [π/2, 0, π/2], while CS_LINKS/CS_RECHTS = [0, 0, 0].
export function eulerZyxRadToQuat(rad: Vec3): Quat {
    const x = rad[0],
        y = rad[1],
        z = rad[2];
    const cx = Math.cos(x / 2),
        cy = Math.cos(y / 2),
        cz = Math.cos(z / 2);
    const sx = Math.sin(x / 2),
        sy = Math.sin(y / 2),
        sz = Math.sin(z / 2);
    return [
        cx * cy * cz + sx * sy * sz,
        sx * cy * cz - cx * sy * sz,
        cx * sy * cz + sx * cy * sz,
        cx * cy * sz - sx * sy * cz,
    ];
}


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
python3 << 'EOF'
import math
def qmul(a,b):
    aw,ax,ay,az=a; bw,bx,by,bz=b
    return [aw*bw-ax*bx-ay*by-az*bz,
            aw*bx+ax*bw+ay*bz-az*by,
            aw*by-ax*bz+ay*bw+az*bx,
            aw*bz+ax*by-ay*bx+az*bw]
def qconj(q): return [q[0],-q[1],-q[2],-q[3]]
def qrot(q,v):
    w,x,y,z=q
    tx=2*(y*v[2]-z*v[1]); ty=2*(z*v[0]-x*v[2]); tz=2*(x*v[1]-y*v[0])
    return [v[0]+w*tx+(y*tz-z*ty), v[1]+w*ty+(z*tx-x*tz), v[2]+w*tz+(x*ty-y*tx)]
def euler(r):
    x,y,z=r
    cx,cy,cz=math.cos(x/2),math.cos(y/2),math.cos(z/2)
    sx,sy,sz=math.sin(x/2),math.sin(y/2),math.sin(z/2)
    return [cx*cy*cz+sx*sy*sz, sx*cy*cz-cx*sy*sz, cx*sy*cz+sx*cy*sz, cx*cy*sz-sx*sy*cz]

# PMI local positions & rotations (from the part JSON)
pos={
 'CS_EIN_L':[-20.0,1067.0,-20.0],
 'CS_EIN_R':[1064.0,1067.0,-20.0],
 'CS_OUT_R_0':[1114.0,1067.0,-20.0],
 'CS_OUT_R_90':[1064.0,1067.0,-110.0],
 'CS_OUT_R_270':[1064.0,1067.0,30.0],
}
rot={
 'CS_EIN_L':[-0.0,-1.5707963267948966,0.0],
 'CS_EIN_R':[-0.0,1.5707963267948966,0.0],
 'CS_OUT_R_0':[-0.0,-1.5707963267948966,0.0],
 'CS_OUT_R_90':[0.0,-2.449e-16,0.0],
 'CS_OUT_R_270':[3.141592653589793,3.674e-16,3.141592653589793],
}
def dockRotation(anchorPmi,joinPmi):
    return qmul(euler(rot[anchorPmi]), qconj(euler(rot[joinPmi])))

# origin(anchor element) at identity. copy joined forward: from=anchor, to=copy
# copy.rot = edge.rot ; copy.pos = R_from*at - R_to*dock  (origin pos 0, R_from identity)
joinPmi='CS_EIN_L'
for anchorPmi in ['CS_OUT_R_0','CS_OUT_R_90','CS_OUT_R_270']:
    edge=dockRotation(anchorPmi,joinPmi)
    at=pos[anchorPmi]            # on anchor (identity frame)
    dock=pos[joinPmi]           # on copy
    Rto=edge
    d=qrot(Rto,dock)
    cpos=[at[0]-d[0], at[1]-d[1], at[2]-d[2]]
    # copy's ends in world:
    einL=[cpos[i]+qrot(edge,pos['CS_EIN_L'])[i] for i in range(3)]
    einR=[cpos[i]+qrot(edge,pos['CS_EIN_R'])[i] for i in range(3)]
    center=[(einL[i]+einR[i])/2 for i in range(3)]
    print(f'{anchorPmi:14} copyPos=({cpos[0]:7.0f},{cpos[1]:6.0f},{cpos[2]:7.0f})  EIN_L->({einL[0]:6.0f},{einL[1]:5.0f},{einL[2]:6.0f})  EIN_R->({einR[0]:6.0f},{einR[1]:5.0f},{einR[2]:6.0f})  center=({center[0]:6.0f},{center[1]:5.0f},{center[2]:6.0f})')

print()
print('marker dirs: R_0 -> right(+x)=[1384,,-20]; R_90 -> front(-z)=[1084,,-340]; R_270 -> back(+z)=[1084,,300]')
print('anchor CS_EIN_R world should coincide with CS_OUT_R_0 pos [1114,1067,-20] for the straight case')
EOF

Simulate copy placement per anchor candidate


CS_OUT_R_0     copyPos=(   1134,     0,      0)  EIN_L->(  1114, 1067,   -20)  EIN_R->(  2198, 1067,   -20)  center=(  1656, 1067,   -20)
CS_OUT_R_90    copyPos=(   1084,     0,   -130)  EIN_L->(  1064, 1067,  -110)  EIN_R->(  1064, 1067, -1194)  center=(  1064, 1067,  -652)
CS_OUT_R_270   copyPos=(   1044,     0,     50)  EIN_L->(  1064, 1067,    30)  EIN_R->(  1064, 1067,  1114)  center=(  1064, 1067,   572)

marker dirs: R_0 -> right(+x)=[1384,,-20]; R_90 -> front(-z)=[1084,,-340]; R_270 -> back(+z)=[1084,,300]
anchor CS_EIN_R world should coincide with CS_OUT_R_0 pos [1114,1067,-20] for the straight case


[Request interrupted by user]


Macht es nicht viel mehr Sinn, die Rotation der Kopie einfach aus dem Ziel zu bestimmen?


Ich rechne die Variante durch, die du meinst: die Kopie direkt aus dem Ziel-PMI (`CS_SELECT_*`) bestimmen — Position **und** Rotation aus dessen Frame, statt über `CS_OUT`.


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
python3 << 'EOF'
import math
def qmul(a,b):
    aw,ax,ay,az=a; bw,bx,by,bz=b
    return [aw*bw-ax*bx-ay*by-az*bz, aw*bx+ax*bw+ay*bz-az*by,
            aw*by-ax*bz+ay*bw+az*bx, aw*bz+ax*by-ay*bx+az*bw]
def qconj(q): return [q[0],-q[1],-q[2],-q[3]]
def qrot(q,v):
    w,x,y,z=q
    tx=2*(y*v[2]-z*v[1]); ty=2*(z*v[0]-x*v[2]); tz=2*(x*v[1]-y*v[0])
    return [v[0]+w*tx+(y*tz-z*ty), v[1]+w*ty+(z*tx-x*tz), v[2]+w*tz+(x*ty-y*tx)]
def euler(r):
    x,y,z=r
    cx,cy,cz=math.cos(x/2),math.cos(y/2),math.cos(z/2)
    sx,sy,sz=math.sin(x/2),math.sin(y/2),math.sin(z/2)
    return [cx*cy*cz+sx*sy*sz, sx*cy*cz-cx*sy*sz, cx*sy*cz+sx*cy*sz, cx*cy*sz-sx*sy*cz]

pos={'CS_EIN_L':[-20.,1067.,-20.],'CS_EIN_R':[1064.,1067.,-20.],
 'CS_SELECT_R_0':[1384.,1067.,-20.],'CS_SELECT_R_90':[1084.,1067.,-340.],'CS_SELECT_R_270':[1084.,1067.,300.]}
rot={'CS_EIN_L':[-0.,-1.5708,0.],'CS_EIN_R':[-0.,1.5708,0.],
 'CS_SELECT_R_0':[-0.,1.5708,0.],'CS_SELECT_R_90':[math.pi,0,math.pi],'CS_SELECT_R_270':[0.,0.,0.]}

def dock(anchorPmi,joinPmi):
    return qmul(euler(rot[anchorPmi]), qconj(euler(rot[joinPmi])))

joinPmi='CS_EIN_L'
print("Ziel-PMI als Anker (Position+Rotation aus CS_SELECT):")
for a in ['CS_SELECT_R_0','CS_SELECT_R_90','CS_SELECT_R_270']:
    edge=dock(a,joinPmi)
    at=pos[a]; dock_=pos[joinPmi]
    d=qrot(edge,dock_)
    cpos=[at[i]-d[i] for i in range(3)]
    einL=[cpos[i]+qrot(edge,pos['CS_EIN_L'])[i] for i in range(3)]
    einR=[cpos[i]+qrot(edge,pos['CS_EIN_R'])[i] for i in range(3)]
    center=[(einL[i]+einR[i])/2 for i in range(3)]
    print(f'{a:16} EIN_L->({einL[0]:6.0f},{einL[2]:6.0f})  EIN_R->({einR[0]:6.0f},{einR[2]:6.0f})  center=({center[0]:6.0f},{center[2]:6.0f})')
print()
print("Original CS_EIN_R (rechtes Ende) liegt bei (1064,-20). Straight sollte ab dort +x weiterlaufen.")
EOF

Simulate docking using CS_SELECT frame as anchor


Ziel-PMI als Anker (Position+Rotation aus CS_SELECT):
CS_SELECT_R_0    EIN_L->(  1384,   -20)  EIN_R->(   300,   -20)  center=(   842,   -20)
CS_SELECT_R_90   EIN_L->(  1084,  -340)  EIN_R->(  1084,   744)  center=(  1084,   202)
CS_SELECT_R_270  EIN_L->(  1084,   300)  EIN_R->(  1084,  -784)  center=(  1084,  -242)

Original CS_EIN_R (rechtes Ende) liegt bei (1064,-20). Straight sollte ab dort +x weiterlaufen.


Nein — ich hab's durchgerechnet, und aus dem `CS_SELECT`-Frame direkt bestimmt läuft die Kopie in den originalen Teil hinein:

- `CS_SELECT_R_0` als Anker → Kopie `EIN_L→(1384)`, `EIN_R→(300)`, Center x≈842 → **überlappt** das Original (läuft nach links zurück).
- Die `CS_SELECT`-Rotationen sind Marker-Frames, keine Andock-Frames für `CS_EIN`.

Die saubere Zuordnung liefert dagegen der **`CS_OUT`-Anker mit gleichem Suffix** (Simulation):

| Anker | Kopie-Center | Richtung |
|---|---|---|
| `CS_OUT_R_0` | (1656, −20) | rechts (+x) — gerade |
| `CS_OUT_R_90` | (1064, −652) | vorn (−z) |
| `CS_OUT_R_270` | (1064, +572) | hinten (+z) |

Das ist exakt die Richtung, in der die jeweiligen Marker sitzen (`R_0` rechts, `R_90` vorn, `R_270` hinten). Damit war mein 90/270-Tausch der Fehler — die 1:1-Zuordnung `CS_SELECT_*_x → CS_OUT_*_x` stimmt geometrisch. Ich nehme den Tausch zurück.


Edit

        this.replay();
        // Map the select marker to its dock anchor. The 90/270 branch frames are swapped between the
        // CS_SELECT points and the CS_OUT points (a CS_SELECT_*_90 carries the CS_OUT_*_270 frame and
        // vice versa), so swap the angle suffix; _0 stays. dockRotation then orients the copy.
        const base = pmi.replace("CS_SELECT_", "CS_OUT_");
        const anchorPmi = base.endsWith("_90") ? base.slice(0, -3) + "_270"
            : base.endsWith("_270") ? base.slice(0, -4) + "_90"
            : base;
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";


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

        this.replay();
        // Map the select marker to its dock anchor. The 90/270 branch frames are swapped between the
        // CS_SELECT points and the CS_OUT points (a CS_SELECT_*_90 carries the CS_OUT_*_270 frame and
        // vice versa), so swap the angle suffix; _0 stays. dockRotation then orients the copy.
        const base = pmi.replace("CS_SELECT_", "CS_OUT_");
        const anchorPmi = base.endsWith("_90") ? base.slice(0, -3) + "_270"
            : base.endsWith("_270") ? base.slice(0, -4) + "_90"
            : base;
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

// dockRotation. Occupied ends do nothing. Selection stays on the original so its markers re-render.

private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {

-        // Map the select marker to its dock anchor. The 90/270 branch frames are swapped between the

-        // CS_SELECT points and the CS_OUT points (a CS_SELECT_*_90 carries the CS_OUT_*_270 frame and

-        // vice versa), so swap the angle suffix; _0 stays. dockRotation then orients the copy.

-        const base = pmi.replace("CS_SELECT_", "CS_OUT_");

-        const anchorPmi = base.endsWith("_90") ? base.slice(0, -3) + "_270"

+        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>

+        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds

+        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).

+        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");

const groupId = this.groups.groupIdOfElement(element);

const scene = this.groups.sceneOfElement(element);


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


done


Zurückgesetzt auf `CS_SELECT_*_x → CS_OUT_*_x`. Typecheck sauber.

Ein Vorbehalt: meine Rechnung ist ein Modell von `computeLayout`. Laut Modell baut `_0` gerade nach rechts, nicht nach vorn. Wenn `_0` im Browser trotzdem nach vorn baut, liegt es nicht an der Suffix-Zuordnung, sondern an der Ausrichtung/Facing der Kopie (bzw. `dockRotation`/`joinPmi`) — das wäre der nächste Punkt.


Erbaut nach vorn
aber genau das meinte ich
kannst du mal des Select PMI und out_PMI loggenn?


Ich logge beim Marker-Klick den geklickten `CS_SELECT`-PMI und den gemappten `CS_OUT`-PMI mit Position und Rotation.


Edit

    private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {
        this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

    private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {
        const scene = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        const elHandle = scene.handles?.find((h: any) => h.id === element);
        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);
        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;
        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));
        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));


    private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {
        this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

    private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {
        const scene = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        const elHandle = scene.handles?.find((h: any) => h.id === element);
        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);
        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;
        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));
        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));

// or CS_EIN_R (left side), and the branch orientation (0/90/270) follows from the PMI frames via

+        const elHandle = scene.handles?.find((h: any) => h.id === element);

+        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);

+        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;

+        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));

+        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


Erbaut nach vorn aber genau das meinte ich kannst du mal des Select PMI und out_PMI loggenn?


src/configurator/events/DefaultCommandProcessor.ts(194,15): error TS2451: Cannot redeclare block-scoped variable 'scene'.
src/configurator/events/DefaultCommandProcessor.ts(207,15): error TS2451: Cannot redeclare block-scoped variable 'scene'.
done


Edit

        const scene = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>

        const replayed = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>


        const scene = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>

        const replayed = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>

// dockRotation. Occupied ends do nothing. Selection stays on the original so its markers re-render.

private handleMarkerBuild(element: string, pmi: string, stop: UIReaction): UIReaction[] {

+        const replayed = this.replay();


const elHandle = scene.handles?.find((h: any) => h.id === element);

const elHandle = replayed.handles?.find((h: any) => h.id === element);


-        const elHandle = scene.handles?.find((h: any) => h.id === element);

+        const elHandle = replayed.handles?.find((h: any) => h.id === element);

const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);

const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;

console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


Logs sind drin. Beim Klick auf einen Marker erscheinen im Browser:

```
[markerBuild] select: CS_SELECT_R_0 {pos: [...], rot: [...]}
[markerBuild] anchor: CS_OUT_R_0 {pos: [...], rot: [...]} join: CS_EIN_L {pos: [...], rot: [...]}
```

Position und Rotation jeweils aus dem selektierten Element. Typecheck sauber.


also
SELECT_R_0 geklicke :: passt
CS_OUT_R_O als Ziel --> passt
Einbau auf C_EIN_L


also
SELECT_R_0 geklicke :: passt
CS_OUT_R_O als Ziel --> passt
Einbau auf C_EIN_L --> passt auch
er rendert aber nach vorn
obwohl scheibnabr der richrige Slot Bessetzt wird
das Problem gibt es mit dem Shoartćut nicht

ich gebe die mal alle deri Stadien des Graphen?


Gefunden. Der Bug ist die `dockRotation`-Aufruf: ich übergebe als Join-Root `createEvent.rootHandleId` — die **frische Kopie, die noch nicht replayed ist**, also nicht in `this.handles` liegt. `pmiRotation` findet sie nicht und liefert `IDENTITY`. Damit wird

```
dockRotation = R(CS_OUT_R_0 = −90°Y) ∘ IDENTITY⁻¹ = R(−90°Y)
```

statt Identity — die Kopie kippt um 90° → „nach vorn". Der Shortcut hat das umgangen, weil er `rot: IDENTITY_QUAT` fest übergibt.

Fix: Die Kopie ist typgleich mit dem Element (sie *ist* dessen Kopie), also beide PMI-Rotationen aus `element` lesen:


Edit

            anchorPmiId: anchorPmi,
            joinPmiId: joinPmi,
            rot: this.groups.dockRotation(element, anchorPmi, createEvent.rootHandleId, joinPmi),
        } as GroupEvent);

            anchorPmiId: anchorPmi,
            joinPmiId: joinPmi,
            // Join-Root is `element`, not the fresh copy: the copy is an identical clone (same PMI
            // frames) and isn't replayed into the handle map yet, so reading its join PMI would fall
            // back to IDENTITY and tilt the copy. Reading both frames off the original is exact.
            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);


            anchorPmiId: anchorPmi,
            joinPmiId: joinPmi,
            rot: this.groups.dockRotation(element, anchorPmi, createEvent.rootHandleId, joinPmi),
        } as GroupEvent);

            anchorPmiId: anchorPmi,
            joinPmiId: joinPmi,
            // Join-Root is `element`, not the fresh copy: the copy is an identical clone (same PMI
            // frames) and isn't replayed into the handle map yet, so reading its join PMI would fall
            // back to IDENTITY and tilt the copy. Reading both frames off the original is exact.
            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);

-            rot: this.groups.dockRotation(element, anchorPmi, createEvent.rootHandleId, joinPmi),

+            // Join-Root is `element`, not the fresh copy: the copy is an identical clone (same PMI

+            // frames) and isn't replayed into the handle map yet, so reading its join PMI would fall

+            // back to IDENTITY and tilt the copy. Reading both frames off the original is exact.

+            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),


also SELECT_R_0 geklicke :: passt CS_OUT_R_O als Ziel --> passt Einbau auf C_EIN_L --> passt auch er rendert aber nach vorn obwohl scheibnabr der richrige Slot Bessetzt wird das Problem gibt es mit de…


Jetzt die Diagnose-Logs wieder raus:


        const replayed = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        const elHandle = replayed.handles?.find((h: any) => h.id === element);
        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);
        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;
        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));
        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));

        this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";


        const replayed = this.replay();
        // The select marker maps 1:1 to its dock anchor of the same suffix: CS_SELECT_<side>_<angle>
        // → CS_OUT_<side>_<angle>. That anchor's frame makes dockRotation orient the copy so it builds
        // in the direction the marker sits (0 = straight, 90 = front, 270 = back).
        const anchorPmi = pmi.replace("CS_SELECT_", "CS_OUT_");
        const joinPmi = pmi.includes("_R_") ? "CS_EIN_L" : "CS_EIN_R";

        const elHandle = replayed.handles?.find((h: any) => h.id === element);
        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);
        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;
        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));
        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));

// or CS_EIN_R (left side), and the branch orientation (0/90/270) follows from the PMI frames via

-        const replayed = this.replay();

-        const elHandle = replayed.handles?.find((h: any) => h.id === element);

-        const pmiOf = (id: string) => elHandle?.components?.find((c: any) => c._resourceClass === "component_pmi" && c.id === id);

-        const fmt = (c: any) => c ? { pos: c.properties?.position?.value, rot: c.properties?.rotation?.value } : null;

-        console.log("[markerBuild] select:", pmi, fmt(pmiOf(pmi)));

-        console.log("[markerBuild] anchor:", anchorPmi, fmt(pmiOf(anchorPmi)), "join:", joinPmi, fmt(pmiOf(joinPmi)));

const groupId = this.groups.groupIdOfElement(element);

const scene = this.groups.sceneOfElement(element);


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


done


Fix drin, Logs raus, Typecheck sauber.

Kern: `dockRotation` bekam die noch nicht replayte Kopie als Join-Root → deren `CS_EIN_L`-Frame fiel auf `IDENTITY` zurück → 90°-Kippung. Jetzt beide Frames aus `element` gelesen (Kopie ist typgleich), also `_0` → Identity (gerade), `_90`/`_270` → echte Verzweigung. Graph brauchst du nicht schicken.


ok --scheint zu gehen
jetzt ein wenig aufräumen
1 - Die kugeln auch ausbauen, wenn etwas angebaut wird
2 - Nur auf die Selektoren einbauen, auf die man auch ein Element einbauen kann


Beides umgesetzt.

**1 — Kugeln nach dem Anbau entfernen:** `handleMarkerBuild` leert die Selektion, dann rendert `selectionReactions` die Szene ohne Marker und selektiert nichts.


Edit

            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);
        return this.selectionReactions(stop);
    }

            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);
        // Drop the selection so the markers disappear once the copy is attached.
        this.selection = [];
        return this.selectionReactions(stop);
    }


            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);
        return this.selectionReactions(stop);
    }

            rot: this.groups.dockRotation(element, anchorPmi, element, joinPmi),
        } as GroupEvent);
        // Drop the selection so the markers disappear once the copy is attached.
        this.selection = [];
        return this.selectionReactions(stop);
    }

// back to IDENTITY and tilt the copy. Reading both frames off the original is exact.

+        // Drop the selection so the markers disappear once the copy is attached.


**2 — Nur Marker an freien Andockstellen:** In `withSelectionMarkers` den zugehörigen `CS_OUT`-Anker prüfen und belegte überspringen.


        const roots = new Set(this.selectionElementRoots());
        if (!roots.size) return scene;
        const markers: any[] = [];
        for (const h of scene.handles ?? []) {
            if (!roots.has(h.id)) continue;
            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
            if (!tr?.key) continue;
            for (const c of h.components ?? []) {
                if (c._resourceClass !== "component_pmi") continue;
                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

        const roots = new Set(this.selectionElementRoots());
        if (!roots.size) return scene;
        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));
        const markers: any[] = [];
        for (const h of scene.handles ?? []) {
            if (!roots.has(h.id)) continue;
            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
            if (!tr?.key) continue;
            for (const c of h.components ?? []) {
                if (c._resourceClass !== "component_pmi") continue;
                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.
                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");
                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];


        const roots = new Set(this.selectionElementRoots());
        if (!roots.size) return scene;
        const markers: any[] = [];
        for (const h of scene.handles ?? []) {
            if (!roots.has(h.id)) continue;
            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
            if (!tr?.key) continue;
            for (const c of h.components ?? []) {
                if (c._resourceClass !== "component_pmi") continue;
                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

        const roots = new Set(this.selectionElementRoots());
        if (!roots.size) return scene;
        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));
        const markers: any[] = [];
        for (const h of scene.handles ?? []) {
            if (!roots.has(h.id)) continue;
            const tr = h.components?.find((c: any) => c._resourceClass === "component_transform");
            if (!tr?.key) continue;
            for (const c of h.components ?? []) {
                if (c._resourceClass !== "component_pmi") continue;
                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.
                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");
                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

private withSelectionMarkers(scene: any): any {

+        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));

+                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.

+                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");

+                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;


ok --scheint zu gehen jetzt ein wenig aufräumen 1 - Die kugeln auch ausbauen, wenn etwas angebaut wird 2 - Nur auf die Selektoren einbauen, auf die man auch ein Element einbauen kann


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


Beides drin, Typecheck sauber.

- **1:** Nach dem Anbau wird die Selektion geleert → Marker verschwinden, nichts bleibt selektiert.
- **2:** Marker erscheinen nur noch an `CS_SELECT`-Punkten, deren zugehöriger `CS_OUT`-Anker laut `occupiedPmis()` frei ist.

Anmerkung zu 2: Belegung wird pro konkretem `CS_OUT`-Punkt geführt (wie bei `placementDragPoints`). Ein Andocken „gerade" (`CS_OUT_R_0`) blendet also nur den `_0`-Marker aus; `_90`/`_270` am selben Ende bleiben, solange deren Anker frei sind. Falls ein belegtes Ende alle drei Marker ausblenden soll, wäre das eine zusätzliche Regel.


er zeigt immer noch alle drei Marker an, auch wenn C_OUT schon belegt ist
Einfaches Element:
ich baue rechts an --> erwartung Recht wird nur noch 90 und 270 gezeigt IST 0 erscheint weiterhin


`occupiedPmis()` meldet `CS_OUT_R_0` offenbar nicht. Ich schaue, wie der `group-merge` die Edge (from/fromPmi) setzt.


443	            }
444	            case "group-merge": {
445	                const target = this.groups.get(event.targetGroupId);
446	                const moving = this.groups.get(event.movingGroupId);
447	                if (!target || !moving) break;
448	                // A docking point must never be booked twice. openEndExclusions already keeps the
449	                // placement UI off occupied points, but guard here too: if the anchor or join point is
450	                // already in use, drop the merge rather than let a double-booked edge into the graph.
451	                const occupied = this.occupiedPmis();
452	                const busy = (element: string, pmi: string) =>
453	                    occupied.some((o) => o.element === element && o.pmi === pmi);
454	                if (busy(event.anchorRootId, event.anchorPmiId) || busy(event.joinRootId, event.joinPmiId)) break;
455	                target.members.push(...moving.members);
456	                target.edges.push(...moving.edges);
457	                target.edges.push({
458	                    from: event.anchorRootId,
459	                    to: event.joinRootId,
460	                    fromPmi: event.anchorPmiId,
461	                    toPmi: event.joinPmiId,
462	                    rot: event.rot,
463	                });
464	                for (const member of moving.members) this.elementToGroup.set(member, target.id);
465	                this.groups.delete(event.movingGroupId);
466	                break;
467	            }
468	            case "group-move":
469	            case "group-rotate": {
470	                const g = this.groups.get(event.groupId);
471	                if (!g) break;
472	                g.transform = { pos: event.position, rot: event.rotation };
473	                break;
474	            }
475	            case "group-detach": {
476	                if (!this.groups.has(event.sourceGroupId)) break;
477	                for (const ng of event.newGroups) {
478	                    this.groups.set(ng.groupId, {
479	                        id: ng.groupId,
480	                        key: ng.groupKey,
481	                        members: [...ng.members],
482	                        edges: ng.edges.map((e) => ({ ...e })),
483	                        transform: ng.transform,
484	                    });
485	                    for (const m of ng.members) this.elementToGroup.set(m, ng.groupId);
486	                }
487	                this.groups.delete(event.sourceGroupId);
488	                break;
489	            }
490	            case "group-regroup": {
491	                for (const sid of event.sourceGroupIds) this.groups.delete(sid);
492	                for (const ng of event.newGroups) {
493	                    this.groups.set(ng.groupId, {
494	                        id: ng.groupId,
495	                        key: ng.groupKey,
496	                        members: [...ng.members],
497	                        edges: ng.edges.map((e) => ({ ...e })),
498	                        transform: ng.transform,
499	                    });
500	                    for (const m of ng.members) this.elementToGroup.set(m, ng.groupId);
501	                }
502	                break;
503	            }
504	        }
505	    }
506
507	    private materialize(baseScene: Scene): Scene {
508	        const handles: Handle[] = [...(baseScene?.handles ?? [])];
509	        if (this.groups.size > 0) handles.push(this.sceneRootHandle());
510
511	        for (const g of this.groups.values()) {
512	            const layout = this.computeLayout(g);

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

    private materialize(baseScene: Scene): Scene {
        const handles: Handle[] = [...(baseScene?.handles ?? [])];
        if (this.groups.size > 0) handles.push(this.sceneRootHandle());

        for (const g of this.groups.values()) {
            const layout = this.computeLayout(g);


[Request interrupted by user]


Stimm nicht
Einfaches Element
einbua rechts
dann Rechtes element wählen
Erwartung:
am rechten Element RECHTS alle drei Selectpunkte Am rechtene Element LINKS nur 90 und 270: 0 ist ja durch das Unsprungelement belegt


Klar — der Denkfehler: das linke Ende der Kopie ist über ihren **`CS_EIN_L`** (Join-Punkt) belegt, nicht über `CS_OUT_L_0`. Meine Prüfung sah nur den `CS_OUT`-Anker. Der Straight-Slot einer Seite ist aber belegt, wenn **entweder** `CS_OUT_<Seite>_0` **oder** `CS_EIN_<Seite>` benutzt ist; die Branch-Slots (90/270) nur über ihren jeweiligen `CS_OUT`.


Edit

                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.
                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");
                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

                // Only the select PMIs (CS_SELECT_<side>_<angle>) get a marker, not the base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                const parts = String(c.id).split("_"); // [CS, SELECT, R|L, 0|90|270]
                const side = parts[2];
                const angle = parts.slice(3).join("_");
                // A slot is open only if a copy can dock there. The straight slot (0) is taken by either
                // its CS_OUT anchor OR the end's CS_EIN join point (this element docked onto a neighbour
                // there); the branch slots (90/270) only by their own CS_OUT anchor.
                const anchorPmi = `CS_OUT_${side}_${angle}`;
                const blocked = occupied.has(`${h.id} ${anchorPmi}`)
                    || (angle === "0" && occupied.has(`${h.id} CS_EIN_${side}`));
                if (blocked) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];


                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.
                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");
                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

                // Only the select PMIs (CS_SELECT_<side>_<angle>) get a marker, not the base points.
                if (!String(c.id).startsWith("CS_SELECT")) continue;
                const parts = String(c.id).split("_"); // [CS, SELECT, R|L, 0|90|270]
                const side = parts[2];
                const angle = parts.slice(3).join("_");
                // A slot is open only if a copy can dock there. The straight slot (0) is taken by either
                // its CS_OUT anchor OR the end's CS_EIN join point (this element docked onto a neighbour
                // there); the branch slots (90/270) only by their own CS_OUT anchor.
                const anchorPmi = `CS_OUT_${side}_${angle}`;
                const blocked = occupied.has(`${h.id} ${anchorPmi}`)
                    || (angle === "0" && occupied.has(`${h.id} CS_EIN_${side}`));
                if (blocked) continue;
                const pos = c.properties?.position?.value ?? [0, 0, 0];

-                // Only the select PMIs (CS_SELECT_*) get a marker, not the docking/base points.

+                // Only the select PMIs (CS_SELECT_<side>_<angle>) get a marker, not the base points.

-                // Only where a copy can actually dock: the matching CS_OUT anchor must still be open.

-                const anchorPmi = String(c.id).replace("CS_SELECT_", "CS_OUT_");

-                if (occupied.has(`${h.id} ${anchorPmi}`)) continue;

+                const parts = String(c.id).split("_"); // [CS, SELECT, R|L, 0|90|270]

+                const angle = parts.slice(3).join("_");

+                // A slot is open only if a copy can dock there. The straight slot (0) is taken by either

+                // its CS_OUT anchor OR the end's CS_EIN join point (this element docked onto a neighbour

+                // there); the branch slots (90/270) only by their own CS_OUT anchor.

+                const anchorPmi = `CS_OUT_${side}_${angle}`;

+                const blocked = occupied.has(`${h.id} ${anchorPmi}`)

+                    || (angle === "0" && occupied.has(`${h.id} CS_EIN_${side}`));


Stimm nicht Einfaches Element einbua rechts dann Rechtes element wählen Erwartung: am rechten Element RECHTS alle drei Selectpunkte Am rechtene Element LINKS nur 90 und 270: 0 ist ja durch das Unsprun…


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep "DefaultCommandProcessor" | head; echo done

Typecheck


done


Typecheck sauber. Durchgespielt:

- **Ursprungselement, rechts angebaut, dann selektiert:** rechts `CS_OUT_R_0` belegt → `R_0` weg, `R_90`/`R_270` bleiben; links alles frei.
- **Rechtes Element (Kopie) selektiert:** links `CS_EIN_L` belegt → `L_0` weg, `L_90`/`L_270` bleiben; rechts alle drei.

Passt zur Erwartung.


ok -- Geht - andere Frage
die drei Einbau kugeln werden gezeigt, auch wenn sie eigentlich vom element verdeckt sein müssten


Das liegt am `depthTest: false` im Marker-Material — dadurch rendern die Kugeln immer obenauf, ohne Verdeckung. Auf `true` stellen:


Edit

material: { _resourceClass: "material_basic", diffuse: [0, 1, 0], depthTest: false },

material: { _resourceClass: "material_basic", diffuse: [0, 1, 0], depthTest: true },


geometry: { _resourceClass: "geometry_parametric", parameters: { factoryName: "sphere-3d", diameter: 120 } },

-                            material: { _resourceClass: "material_basic", diffuse: [0, 1, 0], depthTest: false },

+                            material: { _resourceClass: "material_basic", diffuse: [0, 1, 0], depthTest: true },

// Box collider (shapeType 4) around the PMI — this @ad/fire build registers only

// box/capsule/mesh shape descriptors (no sphere), so shapeType 2 has no descriptor and


`depthTest: true` — jetzt werden die Kugeln von davorliegender Geometrie verdeckt.


sehr gut
jetzt nur noch ein dumme idee:
Element an element macht Sinn
Tür an Tür nicht
könnte man in der DockingRules eine Elementypen für das Weiterbauen festlegen?


Ja, machbar.

Aktuell fragt `handleMarkerBuild` die DockingRules gar nicht — es kopiert stur das Element selbst (`sceneOfElement(element)`). Deshalb baut eine Tür eine Tür an.

Zwei Ebenen, die man in den Rules festlegen könnte:

1. **Ob überhaupt weitergebaut wird** — schon mit der bestehenden Struktur: Marker (bzw. der Selbst-Anbau) nur zeigen, wenn `dockingRules[class][class]` existiert, also die Klasse auf ihre eigene Klasse docken darf. `schutzzaun_basic-tuer` hat nur `tuer→element`, kein `tuer→tuer` → keine Weiterbau-Kugeln an der Tür.

2. **Was gebaut wird** (echter „Elementtyp fürs Weiterbauen") — dafür müssten die Rules erweitert werden: heute enthalten sie nur Punkt-Mappings (`dragPoint → dropPoint[]`), keine Produkt-/Scene-Referenz. Man würde pro Klasse einen Ziel-Produkttyp ergänzen (z. B. `continueWith: "<produktId/class>"`), den `handleMarkerBuild` dann statt der Selbstkopie instanziiert.

Für „Tür an Tür nein, Element an Element ja" reicht Variante 1 mit der vorhandenen Struktur.


sehr gut jetzt nur noch ein dumme idee: Element an element macht Sinn Tür an Tür nicht könnte man in der DockingRules eine Elementypen für das Weiterbauen festlegen?


autoContinueType oder so wäre gut
ABER - es müssten MÖGLICHST VIELE eigenschaften Übernommen werden (maetrial / hähe / Breite -> die könnten auch spezifisch konfiguriert sein


Bevor ich das designe, eine Sache, die den ganzen Ansatz bestimmt: wie liegen Höhe/Breite/Material an einem Element vor — als freie Parameter am Handle oder als fixe Produkt-/glb-Variante? Ich schau kurz in ein Produkt.


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
python3 -c "
import json
d=json.load(open('.storybook/products/schutzzaun_basic/SBS1044PRA.json'))
def walk(o,path=''):
    out=[]
    if isinstance(o,dict):
        rc=o.get('_resourceClass')
        if rc in ('material_reference','material_standard','sceneGraph_reference'):
            out.append((rc, {k:v for k,v in o.items() if k in ('path','name','diffuse')}))
        if 'properties' in o and isinstance(o['properties'],dict):
            for k in o['properties']:
                if any(t in k.lower() for t in ('height','width','hoeh','breit','size','laeng','length','material','dim')):
                    out.append(('prop:'+k, o['properties'][k]))
        for v in o.values(): out+=walk(v)
    elif isinstance(o,list):
        for v in o: out+=walk(v)
    return out
res=walk(d)
seen=set()
for t,v in res:
    key=str(t)+str(v)[:40]
    if key in seen: continue
    seen.add(key)
    print(t, json.dumps(v)[:120])
print('--- top-level handle tags ---')
for h in d.get('handles',[]):
    print(h.get('id'), h.get('tags'))
" 2>&1 | head -40

Inspect material and size representation in a product


sceneGraph_reference {"path": "katalog/schutzzaun_basic/SBS1044PRA/clone_26646718.glb"}
material_reference {"path": "pil_eloxiert_natur.json"}
material_reference {"path": "kunststoff_schwarz.json"}
material_reference {"path": "kunststoff_grau.json"}
--- top-level handle tags ---
X-2c421002-b10c-4d17-88e6-8dfdb96124f7 ['class-schutzzaun_basic-tuer']
X-3a900833-fbef-482f-923c-3d645dc659c9 ['name-assembly-root']
X-5367742c-6932-44ba-b8c5-042392a5799d ['name-EINZELTUER']
X-f7599ae5-32dd-4110-b8c8-a5bbf64eb8cd ['name-generic', 'part-STEHERPROFIL_LINKS']
X-e7e9d979-2121-426b-9d3a-1949704c8b15 ['name-generic', 'pmi-STEHERPROFIL_LINKS']
X-39a0a410-7de6-4c8c-8b20-de18a465e689 ['name-generic', 'part-STEHERPROFIL_RECHTS']
X-396ba18b-3dfb-4058-9fe8-bc9b856883a8 ['name-generic', 'pmi-STEHERPROFIL_RECHTS']
X-09e1a001-e874-44fd-8c3c-8a5eb7a6a696 ['name-generic', 'part-ELEMENTSTEHERPROFIL_LINKS']
X-250ad9ca-6ff4-4006-b735-e4e0ee0fa8d5 ['name-generic', 'pmi-ELEMENTSTEHERPROFIL_LINKS']
X-cc0d2a5d-6c72-4404-82e6-9a51d70e1210 ['name-generic', 'part-ELEMENTSTEHERPROFIL_RECHTS']
X-bc33255f-a7d2-4651-882f-a4c9836d776a ['name-generic', 'pmi-ELEMENTSTEHERPROFIL_RECHTS']
X-f844af04-78ae-45d7-9686-bcdf9546b345 ['name-generic', 'part-ABDECKKAPPE_LINKS']
X-c2c5330b-9a68-4157-a6f9-23abd5e7ae26 ['name-generic', 'pmi-ABDECKKAPPE_LINKS']
X-2a90df5c-3b8b-4e83-9d13-4d07abd70000 ['name-generic', 'part-ABDECKKAPPE_RECHTS']
X-ca754272-f515-4603-a9d2-b4d509bbdf8d ['name-generic', 'pmi-ABDECKKAPPE_RECHTS']
X-36aa9b83-a8ef-4d0e-9638-cc7a2b570eeb ['name-generic', 'part-ABDECKKAPPE_LINKS_ELEMENT_OBEN']
X-c8a4af6d-d6ce-4731-8859-473cd8c95408 ['name-generic', 'pmi-ABDECKKAPPE_LINKS_ELEMENT_OBEN']
X-d6a577b9-d4db-4d68-bba8-9e5eb9024401 ['name-generic', 'part-ABDECKKAPPE_RECHTS_ELEMENT_OBEN']
X-c2d54e24-4194-4057-962a-1320b549aa0d ['name-generic', 'pmi-ABDECKKAPPE_RECHTS_ELEMENT_OBEN']
X-5a6bb09c-2bfe-4b8e-90f0-1f9142513d5f ['name-generic', 'part-ABDECKKAPPE_LINKS_ELEMENT_UNTEN']
X-ef6567f6-133f-48f5-9577-34f4cec74e30 ['name-generic', 'pmi-ABDECKKAPPE_LINKS_ELEMENT_UNTEN']
X-0945837b-41dd-435f-a493-0cfe30999271 ['name-generic', 'part-ABDECKKAPPE_RECHTS_ELEMENT_UNTEN']
X-35b7bd92-2509-4898-83e2-9969a5934c55 ['name-generic', 'pmi-ABDECKKAPPE_RECHTS_ELEMENT_UNTEN']
X-47323a72-0db7-49dc-b99c-0e18ce9edb97 ['name-generic', 'part-SCHUTZZAUNFUSS_LINKS']
X-1fa0d0b1-3fee-49c0-906c-e90378167c74 ['name-generic', 'pmi-SCHUTZZAUNFUSS_LINKS']
X-3b876969-b35f-4392-8b7f-8dcf0135b694 ['name-generic', 'part-SCHUTZZAUNFUSS_LINKS_VORNE']
X-523e2d78-d8a8-4171-9c20-5c71a8b05baa ['name-generic', 'pmi-SCHUTZZAUNFUSS_LINKS_VORNE']
X-f8256241-1513-4e4d-9d29-17298e26a14d ['name-generic', 'part-SCHUTZZAUNFUSS_RECHTS']
X-33ed6c2b-b505-4c55-ac2b-9456cdcd6f07 ['name-generic', 'pmi-SCHUTZZAUNFUSS_RECHTS']
X-06085d4f-908f-4695-9962-701b391bbb9d ['name-generic', 'part-SCHUTZZAUNFUSS_RECHTS_VORNE']
X-b9ee0940-241c-4bee-a0b2-566f268cfa50 ['name-generic', 'pmi-SCHUTZZAUNFUSS_RECHTS_VORNE']
X-7bbfbbfc-ee6b-43c9-8047-2ff944648eeb ['name-generic', 'part-QUERPROFIL_OBEN']
X-cc6e6ca4-0e10-4453-a45f-c469819cd0fe ['name-generic', 'pmi-QUERPROFIL_OBEN']
X-2f29863a-135b-451b-9a2d-350ee325eb0b ['name-generic', 'part-TUERSTURZ']
X-784fa4f3-11e5-4e65-8851-b06485ea00fc ['name-generic', 'pmi-TUERSTURZ']


Ich hab in ein Produkt geschaut, das entscheidet den Ansatz:

- **Geometrie/Maße:** stecken als `sceneGraph_reference` → glb (`clone_….glb`). Es gibt **keine** freien `height/width`-Properties an den Handles. Höhe/Breite = Produkt-/glb-Variante, kein Parameter.
- **Material:** `material_reference` (`pil_eloxiert_natur.json`, `kunststoff_schwarz.json`, …) pro Part, in der Szene.

Daraus folgt für `autoContinueType`:

**1 — Das Feld selbst:** trivial. Optional pro Klasse in den Rules; fehlt es → heutiges Selbst-Kopieren (dann kommt ohnehin *alles* mit, weil identisches Produkt). Ist es gesetzt → `handleMarkerBuild` instanziiert diesen Typ statt der Selbstkopie.

**2 — Eigenschaften übernehmen — zerfällt in zwei Fälle:**

- **Material (und was als Szenen-Override vorliegt):** übertragbar. Man mappt `material_reference`/`overrideMaterials` der Quelle über die Part-Rollen (`part-<ROLLE>`) auf die neue Instanz. Klappt sauber, solange die Rollen im Zieltyp gleich heißen.
- **Höhe/Breite:** **nicht** aus der Szene übernehmbar, weil sie im glb baked sind. „Übernehmen" heißt hier: die Variante des `autoContinueType` wählen, die dieselbe Dimension hat → braucht einen **Katalog-Lookup mit Dimensions-Metadaten**. Als reiner Rule-String / Szenen-Clone nicht lösbar.

Wenn es eine instanzspezifische Konfiguration gibt („könnten auch spezifisch konfiguriert sein"), liegt die nicht im Basis-Produkt, sondern in der Configurator-Override-Ebene (Events, [[cpq-configurator-modul-registry]]) — Vererbung = die Overrides der Quelle auf die neue Instanz erneut anwenden.

Der Knackpunkt, der „Höhe/Breite übernehmen" bei Typwechsel überhaupt möglich macht: Gibt es einen Produktkatalog mit Dimensions-Metadaten, über den sich `(autoContinueType, Maße der Quelle) → konkretes Produkt` auflösen lässt — oder sollen Höhe/Breite echte Parameter werden? Ohne das eine oder andere bleibt bei Typwechsel nur die Material-/Override-Übernahme.


autoContinueType oder so wäre gut ABER - es müssten MÖGLICHST VIELE eigenschaften Übernommen werden (maetrial / hähe / Breite -> die könnten auch spezifisch konfiguriert sein


dann lassen wir das jetzt mal


Ok.


wir habe mit dem seitenanbau noch ein thema
wenn zwei Element verbunden sind, gibt es der Theorie nach ZWEI Steher dazwischen, na denen nch vorne oder hinten Verbaut werden könnte
davon DARF aber nur einer belegt sein und es macht Sinn dafür eine über den Graphen konstante Regel zu haben
Sowohl, was mögliche SnapPoint (Droppints) beinm Positionieren als auch die Anzeige der EinbauSelektoren angeht


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -rn "openEndExclusion\|exclusion\|Exclusion\|coincid\|junction\|mirror\|_B\b\|_90_B\|_270_B" src/configurator/ | head -30

Find existing exclusion/junction handling


src/configurator/model/transformMath.ts:59:// This mirrors how fire-visual ingests rotations: a 3-component rotation property is read as an
src/configurator/model/GroupProcessor.ts:69:// `to` is positioned so its `toPmi` coincides with `from`'s `fromPmi`.
src/configurator/model/GroupProcessor.ts:108:    joinPmiId: string; // docking point id on the join, placed to coincide with the anchor's
src/configurator/model/GroupProcessor.ts:448:                // A docking point must never be booked twice. openEndExclusions already keeps the
src/configurator/model/GroupProcessor.ts:537:    // Each edge fixes that `to.toPmi` coincides with `from.fromPmi` AND that `to` is rotated by
src/configurator/model/GroupProcessor.ts:539:    // accumulated part rotation R, a member's docking point sits at world R·pmi, so coincidence gives:


Das gehört an **eine** Stelle, dann greift es automatisch für beides.

Beide Verbraucher lesen Belegung heute aus `occupiedPmis()`:
- `placementDragPoints` (Drop-Points beim Positionieren)
- `withSelectionMarkers` (Anzeige der Einbau-Selektoren)

Also gehört die Regel in eine **graph-abgeleitete Ausschluss-Menge** neben `occupiedPmis()` — nennen wir sie `blockedBranchPmis()` —, die aus den Kanten die Junctions bestimmt. Dann sind Drop-Points und Selektoren per Konstruktion konsistent (die „über den Graphen konstante Regel", die du meinst). Der Kommentar bei `group-merge` verweist schon auf so ein `openEndExclusions`-Konzept, das aber nie implementiert wurde.

Ableitung: Für jede **gerade** Element↔Element-Kante (`CS_OUT_*_0 ↔ CS_EIN_*`) liegen an der Junction die Branch-Anker der beiden benachbarten Steher:
- linkes Element rechts: `CS_OUT_R_90` / `CS_OUT_R_270`
- rechtes Element links: `CS_OUT_L_90` / `CS_OUT_L_270`

Diese vier bilden **eine Ausschlussgruppe**: sobald einer belegt ist, gelten die anderen als belegt — sowohl als Drop-Point-Ausschluss als auch als ausgeblendeter Selektor.

Bevor ich das baue, eine Sache, die die Gruppierung festlegt: „nur einer belegt" heißt —

- **A)** pro Junction **genau ein** Branch insgesamt (alle vier teilen sich einen Slot), oder
- **B)** vorne und hinten unabhängig, aber die je zwei deckungsgleichen Steher-Punkte (R_90≙L_90, R_270≙L_270) teilen sich ihren Slot?

Deine Formulierung „von den zwei Stehern darf nur einer belegt sein" klingt nach **A**. Welche gilt?


wir habe mit dem seitenanbau noch ein thema wenn zwei Element verbunden sind, gibt es der Theorie nach ZWEI Steher dazwischen, na denen nch vorne oder hinten Verbaut werden könnte davon DARF aber nur…


Aslo
von den beiden steher darf nur einmal 90 und einaml 270 belegt sein


Klar — Variante **B**: 90 und 270 unabhängig, aber die zwei deckungsgleichen Steher-Punkte je Richtung teilen sich einen Slot (`R_90≙L_90`, `R_270≙L_270`).

Plan (2 Dateien): In `GroupProcessor` eine `blockedPmis()` neben `occupiedPmis()` — sie spiegelt an jeder **geraden** Element↔Element-Junction die Branch-Belegung auf den Nachbar-Steher. `withSelectionMarkers` und `placementDragPoints` lesen dann `blockedPmis()` statt `occupiedPmis()`, damit Selektoren und Drop-Points dieselbe Regel sehen.


201	    // Docking points already in use, derived purely from the edges (no stored state). Each edge
202	    // occupies its `fromPmi` on `from` and its `toPmi` on `to`. Open ends are everything else.
203	    occupiedPmis(): { element: string; pmi: string }[] {
204	        const out: { element: string; pmi: string }[] = [];
205	        for (const g of this.groups.values()) {
206	            for (const e of g.edges) {
207	                out.push({ element: e.from, pmi: e.fromPmi });
208	                out.push({ element: e.to, pmi: e.toPmi });
209	            }
210	        }
211	        return out;
212	    }
213
214	    groupCount(): number {
215	        return this.groups.size;
216	    }
217
218	    roleOfId(handleId: string): NodeRole | null {

    // Docking points already in use, derived purely from the edges (no stored state). Each edge
    // occupies its `fromPmi` on `from` and its `toPmi` on `to`. Open ends are everything else.
    occupiedPmis(): { element: string; pmi: string }[] {
        const out: { element: string; pmi: string }[] = [];
        for (const g of this.groups.values()) {
            for (const e of g.edges) {
                out.push({ element: e.from, pmi: e.fromPmi });
                out.push({ element: e.to, pmi: e.toPmi });
            }
        }
        return out;
    }

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

    roleOfId(handleId: string): NodeRole | null {


Edit

        return out;
    }

    groupCount(): number {

        return out;
    }

    // occupiedPmis plus the junction rule: where two elements meet straight (an edge on CS_OUT_*_0),
    // the two abutting posts share a branch slot per direction — only one 90 and one 270 may sit in
    // that gap. So a branch occupied on one post's CS_OUT_<side>_<90|270> also blocks the mirror
    // CS_OUT_<otherSide>_<same> on the neighbour. Both the placement drop points and the select
    // markers read this, so the rule is graph-constant across positioning and display.
    blockedPmis(): { element: string; pmi: string }[] {
        const occ = this.occupiedPmis();
        const set = new Set(occ.map((o) => `${o.element} ${o.pmi}`));
        const extra: { element: string; pmi: string }[] = [];
        for (const g of this.groups.values()) {
            for (const e of g.edges) {
                const m = /^CS_OUT_([LR])_0$/.exec(e.fromPmi); // straight element↔element junction only
                if (!m) continue;
                const sideA = m[1];
                const sideB = e.toPmi.endsWith("_L") ? "L" : "R";
                for (const d of ["90", "270"]) {
                    const aP = `CS_OUT_${sideA}_${d}`;
                    const bP = `CS_OUT_${sideB}_${d}`;
                    if (set.has(`${e.from} ${aP}`)) extra.push({ element: e.to, pmi: bP });
                    if (set.has(`${e.to} ${bP}`)) extra.push({ element: e.from, pmi: aP });
                }
            }
        }
        return [...occ, ...extra];
    }

    groupCount(): number {


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

        return out;
    }

    groupCount(): number {

        return out;
    }

    // occupiedPmis plus the junction rule: where two elements meet straight (an edge on CS_OUT_*_0),
    // the two abutting posts share a branch slot per direction — only one 90 and one 270 may sit in
    // that gap. So a branch occupied on one post's CS_OUT_<side>_<90|270> also blocks the mirror
    // CS_OUT_<otherSide>_<same> on the neighbour. Both the placement drop points and the select
    // markers read this, so the rule is graph-constant across positioning and display.
    blockedPmis(): { element: string; pmi: string }[] {
        const occ = this.occupiedPmis();
        const set = new Set(occ.map((o) => `${o.element} ${o.pmi}`));
        const extra: { element: string; pmi: string }[] = [];
        for (const g of this.groups.values()) {
            for (const e of g.edges) {
                const m = /^CS_OUT_([LR])_0$/.exec(e.fromPmi); // straight element↔element junction only
                if (!m) continue;
                const sideA = m[1];
                const sideB = e.toPmi.endsWith("_L") ? "L" : "R";
                for (const d of ["90", "270"]) {
                    const aP = `CS_OUT_${sideA}_${d}`;
                    const bP = `CS_OUT_${sideB}_${d}`;
                    if (set.has(`${e.from} ${aP}`)) extra.push({ element: e.to, pmi: bP });
                    if (set.has(`${e.to} ${bP}`)) extra.push({ element: e.from, pmi: aP });
                }
            }
        }
        return [...occ, ...extra];
    }

    groupCount(): number {

+    // occupiedPmis plus the junction rule: where two elements meet straight (an edge on CS_OUT_*_0),

+    // the two abutting posts share a branch slot per direction — only one 90 and one 270 may sit in

+    // that gap. So a branch occupied on one post's CS_OUT_<side>_<90|270> also blocks the mirror

+    // CS_OUT_<otherSide>_<same> on the neighbour. Both the placement drop points and the select

+    // markers read this, so the rule is graph-constant across positioning and display.

+    blockedPmis(): { element: string; pmi: string }[] {

+        const occ = this.occupiedPmis();

+        const set = new Set(occ.map((o) => `${o.element} ${o.pmi}`));

+        const extra: { element: string; pmi: string }[] = [];

+        for (const g of this.groups.values()) {

+                const m = /^CS_OUT_([LR])_0$/.exec(e.fromPmi); // straight element↔element junction only

+                const sideB = e.toPmi.endsWith("_L") ? "L" : "R";

+                    const aP = `CS_OUT_${sideA}_${d}`;

+                    const bP = `CS_OUT_${sideB}_${d}`;

+                    if (set.has(`${e.from} ${aP}`)) extra.push({ element: e.to, pmi: bP });

+                    if (set.has(`${e.to} ${bP}`)) extra.push({ element: e.from, pmi: aP });


Aslo  von den beiden steher darf nur einmal 90 und einaml 270 belegt sein


Jetzt beide Verbraucher auf `blockedPmis()` umstellen:


const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));

const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element} ${o.pmi}`));


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

private withSelectionMarkers(scene: any): any {

const roots = new Set(this.selectionElementRoots());

-        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));

+        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element} ${o.pmi}`));


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -n "occupiedPmis\|blockedPmis" src/configurator/events/DefaultCommandProcessor.ts
echo "--- typecheck ---"
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep -E "DefaultCommandProcessor|GroupProcessor" | head; echo done

Verify usages and typecheck


--- typecheck ---
done


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -rn "occupiedPmis\|blockedPmis" src/configurator/events/DefaultCommandProcessor.ts src/configurator/model/GroupProcessor.ts

Re-check occupied/blocked usages


src/configurator/model/GroupProcessor.ts:203:    occupiedPmis(): { element: string; pmi: string }[] {
src/configurator/model/GroupProcessor.ts:214:    // occupiedPmis plus the junction rule: where two elements meet straight (an edge on CS_OUT_*_0),
src/configurator/model/GroupProcessor.ts:219:    blockedPmis(): { element: string; pmi: string }[] {
src/configurator/model/GroupProcessor.ts:220:        const occ = this.occupiedPmis();
src/configurator/model/GroupProcessor.ts:477:                const occupied = this.occupiedPmis();


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
grep -n "Pmis\|this.groups\." src/configurator/events/DefaultCommandProcessor.ts | head -40

Grep Pmis and groups calls in file


(Bash completed with no output)


wc -l /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts
grep -c "groups" /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts
grep -n "blockedPmis\|occupiedPmis" /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts

Absolute-path checks on the file


665 /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts


grep -an "blockedPmis\|occupiedPmis\|this.groups" /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts | head

grep -a treating file as text


37:        return this.groups.replay(
95:        const createEvent = this.groups.buildCreateEvent({ handles: event.handles }, position);
111:        const activeRoot = active ? this.groups.elementRootOf(active) : null;
112:        const groupId = activeRoot ? this.groups.groupIdOfElement(activeRoot) : null;
116:            const t = this.groups.transformOf(groupId);
132:        if (this.groups.occupiedPmis().some((o) => o.element === activeRoot && o.pmi === anchorPmi)) {
135:        const scene = this.groups.sceneOfElement(activeRoot);
137:        const createEvent = this.groups.buildCreateEvent(scene as never, [0, 0, 0]);
173:        const chain = this.groups.selectableAncestors(pickedId);
200:        const groupId = this.groups.groupIdOfElement(element);


Confirm blocked/occupied usages with -a


132:        if (this.groups.occupiedPmis().some((o) => o.element === activeRoot && o.pmi === anchorPmi)) {
203:        if (this.groups.occupiedPmis().some((o) => o.element === element && o.pmi === anchorPmi)) {
265:        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element} ${o.pmi}`));
620:        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element}${o.pmi}`));


Line 620 (placementDragPoints) wurde nicht mitgetauscht — die Datei enthält ein Byte, das grep/Edit als binär behandelt. Ich ändere es gezielt mit Kontext.


616	        const klass = this.classOf(subject);
617	        const rules = klass ? this.dockingRules?.[klass] : undefined;
618	        if (!rules) return [];
619
620	        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element}${o.pmi}`));
621	        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;
622	        const ownMembers = new Set(this.groups.membersOfKey(ownKey));
623

        const klass = this.classOf(subject);
        const rules = klass ? this.dockingRules?.[klass] : undefined;
        if (!rules) return [];

        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element}${o.pmi}`));
        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;
        const ownMembers = new Set(this.groups.membersOfKey(ownKey));


Edit

        if (!rules) return [];

        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));
        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;

        if (!rules) return [];

        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element} ${o.pmi}`));
        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;


<tool_use_error>String to replace not found in file.
String:         if (!rules) return [];

        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));
        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;</tool_use_error>

Error: String to replace not found in file.
String:         if (!rules) return [];

        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element} ${o.pmi}`));
        const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;


Bash

cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
awk 'NR==620' src/configurator/events/DefaultCommandProcessor.ts | cat -A

Show exact bytes of line 620


Error: Exit code 1
cat: illegal option -- A
usage: cat [-belnstuv] [file ...]


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
awk 'NR==620' src/configurator/events/DefaultCommandProcessor.ts | od -c | head

Octal/char dump of line 620


0000000                                    c   o   n   s   t       o   c
0000020    c   u   p   i   e   d       =       n   e   w       S   e   t
0000040    (   t   h   i   s   .   g   r   o   u   p   s   .   o   c   c
0000060    u   p   i   e   d   P   m   i   s   (   )   .   m   a   p   (
0000100    (   o   )       =   >       `   $   {   o   .   e   l   e   m
0000120    e   n   t   }  \n
0000125


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
awk 'NR==620' src/configurator/events/DefaultCommandProcessor.ts | od -c | sed -n '5,12p'

Dump rest of line 620


0000100    (   o   )       =   >       `   $   {   o   .   e   l   e   m
0000120    e   n   t   }  \n
0000125


Der Match scheitert an einem Sonderbyte später in der Zeile. Ich ersetze nur das saubere Teilstück:


Edit

const occupied = new Set(this.groups.occupiedPmis().map(

const occupied = new Set(this.groups.blockedPmis().map(


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

const rules = klass ? this.dockingRules?.[klass] : undefined;

-        const occupied = new Set(this.groups.occupiedPmis().map((o) => `${o.element}${o.pmi}`));

+        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element}${o.pmi}`));

const ownKey = subject.startsWith("#") ? subject.slice(1) : subject;

const ownMembers = new Set(this.groups.membersOfKey(ownKey));


Bash

grep -an "blockedPmis\|occupiedPmis" /Users/marcus.hinz/projects/adesso-cpq/packages/frontend/src/configurator/events/DefaultCommandProcessor.ts
echo "--- typecheck ---"
cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
pnpm exec vue-tsc --noEmit -p tsconfig.json 2>&1 | grep -E "DefaultCommandProcessor|GroupProcessor" | head; echo done

Confirm both consumers use blockedPmis and typecheck


132:        if (this.groups.occupiedPmis().some((o) => o.element === activeRoot && o.pmi === anchorPmi)) {
203:        if (this.groups.occupiedPmis().some((o) => o.element === element && o.pmi === anchorPmi)) {
265:        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element} ${o.pmi}`));
620:        const occupied = new Set(this.groups.blockedPmis().map((o) => `${o.element}${o.pmi}`));
--- typecheck ---
done


Beide Verbraucher nutzen jetzt `blockedPmis()` (Zeile 265 = Selektoren, 620 = Drop-Points). Typecheck sauber.

Die Junction-Regel liegt damit an einer Stelle (`GroupProcessor.blockedPmis()`): an jeder geraden Element↔Element-Verbindung wird die 90- bzw. 270-Belegung eines Stehers auf den deckungsgleichen Nachbar-Steher gespiegelt. Positionieren (Snap-Points) und Anzeige der Einbau-Selektoren sehen dieselbe Regel.

Die beiden `.some()`-Checks (Shortcut, Marker-Build-Guard) bleiben bewusst auf `occupiedPmis()` — sie prüfen wörtliche Belegung; die Spiegelung greift schon vorher über die ausgeblendeten Selektoren/Drop-Points.

Randnotiz: Die Datei enthält ein Sonderbyte (grep behandelt sie als binär), deshalb griff der Sammel-Replace nur teils — jetzt korrigiert.


ok das gleice gilt natürlich auch in 0- Richtung, wenn auf 90 und / oder 270 eingebaut ist
auch hier darf von den drei Stehern NUR EINER (der Mittlere ) belegt sein
Gilt ebenso für Snap wir für die Selektore Kugeln


Bevor ich das falsch baue — die PMI-Daten geben hier einen starken Hinweis, den ich kurz absichern will:


cd /Users/marcus.hinz/projects/adesso-cpq/packages/frontend
python3 -c "
import json
d=json.load(open('.storybook/products/schutzzaun_basic/SBS1044PRA.json'))
def walk(o):
    out=[]
    if isinstance(o,dict):
        if o.get('_resourceClass')=='component_pmi': out.append(o)
        for v in o.values(): out+=walk(v)
    elif isinstance(o,list):
        for v in o: out+=walk(v)
    return out
for p in walk(d):
    i=p['id']
    if i.startswith('CS_OUT_R'):
        print(f\"{i:16} pos={p['properties']['position']['value']}\")
"

Compare CS_OUT_R positions incl. _B variants


CS_OUT_R_0       pos=[1114.0, 1067.0, -20.0]
CS_OUT_R_90      pos=[1064.0, 1067.0, -110.0]
CS_OUT_R_90_B    pos=[1114.0, 1067.0, -20.0]
CS_OUT_R_270     pos=[1064.0, 1067.0, 30.0]
CS_OUT_R_270_B   pos=[1114.0, 1067.0, -20.0]


ok das gleice gilt natürlich auch in 0- Richtung, wenn auf 90 und / oder 270 eingebaut ist auch hier darf von den drei Stehern NUR EINER (der Mittlere ) belegt sein Gilt ebenso für Snap wir für die Se…


Die `_B`-Punkte bestätigen es: `CS_OUT_R_90_B` und `CS_OUT_R_270_B` liegen **exakt** auf der `CS_OUT_R_0`-Position `[1114,1067,-20]`. Ein Branch besetzt also den mittleren (0-)Steher mit → er sperrt die 0-Richtung und den jeweils anderen Branch.

Das ist asymmetrisch zu deiner früheren Vorgabe, und beides zusammen ergibt eindeutig:
- **0 gebaut** (gerader Anbau) → nur 0 weg, 90/270 bleiben (wie bisher).
- **90 gebaut** → 90 + mittlerer Steher belegt → 0 **und** 270 gesperrt.
- **270 gebaut** → 270 + Mitte belegt → 0 **und** 90 gesperrt.

Also pro Ende, sobald ein Branch steht: „nur einer (der mittlere)". Ich erweitere `blockedPmis` entsprechend (inkl. Junction-Spiegelung):