    private openEndExclusions(): string {
        return this.groups
            .occupiedPmis()
            .map((o) => `:not(#${o.element} .name-${o.pmi})`)
            .join("");
    }

    private openEndExclusions(): string {
        return this.groups
            .occupiedPmis()
            .map((o) => `:not(#${o.element} .name-${o.pmi})`)
            .join("");
    }

    // Placement drag/drop wiring (root-level points only). A part's left/right ends dock onto a
    // neighbour's open CS_OUT points: CS_EIN_L (and its foot variant CS_EIN_START_L) meet the
    // neighbour's right-side OUT points, CS_EIN_R/CS_EIN_START_R the left-side ones — matching point
    // frames. The angle suffix picks the direction: _0 in line (Flucht), _90 to the front, _270 to
    // the back. Self-snap and already-occupied points are excluded.
    private placementDragPoints(subject: string): Array<{ selector: string; dropPoints: string[] }> {
        const openOnly = this.openEndExclusions();
        const drops = (side: "R" | "L"): string[] =>
            ["0", "90", "270"].map(
                (angle) => `.name-CS_OUT_${side}_${angle}:not(${subject} .name-CS_OUT_${side}_${angle})${openOnly}`,
            );
        return [
            { selector: `.name-CS_EIN_L${openOnly}`, dropPoints: drops("R") },
            { selector: `.name-CS_EIN_R${openOnly}`, dropPoints: drops("L") },
            { selector: `.name-CS_EIN_START_L${openOnly}`, dropPoints: drops("R") },
            { selector: `.name-CS_EIN_START_R${openOnly}`, dropPoints: drops("L") },
        ];
    }


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)

    private openEndExclusions(): string {
        return this.groups
            .occupiedPmis()
            .map((o) => `:not(#${o.element} .name-${o.pmi})`)
            .join("");
    }

    // Placement drag/drop wiring (root-level points only). A part's left/right ends dock onto a
    // neighbour's open CS_OUT points: CS_EIN_L (and its foot variant CS_EIN_START_L) meet the
    // neighbour's right-side OUT points, CS_EIN_R/CS_EIN_START_R the left-side ones — matching point
    // frames. The angle suffix picks the direction: _0 in line (Flucht), _90 to the front, _270 to
    // the back. Self-snap and already-occupied points are excluded.
    private placementDragPoints(subject: string): Array<{ selector: string; dropPoints: string[] }> {
        const openOnly = this.openEndExclusions();
        const drops = (side: "R" | "L"): string[] =>
            ["0", "90", "270"].map(
                (angle) => `.name-CS_OUT_${side}_${angle}:not(${subject} .name-CS_OUT_${side}_${angle})${openOnly}`,
            );
        return [
            { selector: `.name-CS_EIN_L${openOnly}`, dropPoints: drops("R") },
            { selector: `.name-CS_EIN_R${openOnly}`, dropPoints: drops("L") },
            { selector: `.name-CS_EIN_START_L${openOnly}`, dropPoints: drops("R") },
            { selector: `.name-CS_EIN_START_R${openOnly}`, dropPoints: drops("L") },
        ];
    }

+    // Placement drag/drop wiring (root-level points only). A part's left/right ends dock onto a

+    // neighbour's open CS_OUT points: CS_EIN_L (and its foot variant CS_EIN_START_L) meet the

+    // neighbour's right-side OUT points, CS_EIN_R/CS_EIN_START_R the left-side ones — matching point

+    // frames. The angle suffix picks the direction: _0 in line (Flucht), _90 to the front, _270 to

+    // the back. Self-snap and already-occupied points are excluded.

+    private placementDragPoints(subject: string): Array<{ selector: string; dropPoints: string[] }> {

+        const openOnly = this.openEndExclusions();

+        const drops = (side: "R" | "L"): string[] =>

+                (angle) => `.name-CS_OUT_${side}_${angle}:not(${subject} .name-CS_OUT_${side}_${angle})${openOnly}`,

+            { selector: `.name-CS_EIN_L${openOnly}`, dropPoints: drops("R") },

+            { selector: `.name-CS_EIN_R${openOnly}`, dropPoints: drops("L") },

+            { selector: `.name-CS_EIN_START_L${openOnly}`, dropPoints: drops("R") },

+            { selector: `.name-CS_EIN_START_R${openOnly}`, dropPoints: drops("L") },

private resolveGroupId(handleId: string): string | null {

const byKey = this.groups.groupByKey(handleId);


Alle vier Änderungen drin. Verifikation:


Bash

cd ~/projects/adesso-cpq && echo "=== neue Logik drin, CS_LINKS/RECHTS raus? ===" && grep -n "placementDragPoints\|CS_OUT_\|CS_LINKS\|CS_RECHTS" packages/frontend/src/configurator/events/DefaultCommandProcessor.ts; echo "=== typecheck (nur meine Datei-Fehler) ===" && pnpm --filter @ad-cpq/common-frontend typecheck 2>&1 | grep -i "DefaultCommandProcessor" || echo "keine Fehler in DefaultCommandProcessor.ts"

Verify new logic present and typecheck clean for my file


=== neue Logik drin, CS_LINKS/RECHTS raus? ===
287:                        dragPoints: this.placementDragPoints(subject),
400:                                        dragPoints: this.placementDragPoints(groupSelector),
445:    private placementDragPoints(subject: string): Array<{ selector: string; dropPoints: string[] }> {
449:                (angle) => `.name-CS_OUT_${side}_${angle}:not(${subject} .name-CS_OUT_${side}_${angle})${openOnly}`,
=== typecheck (nur meine Datei-Fehler) ===
keine Fehler in DefaultCommandProcessor.ts


Erledigt. Die neue Logik ersetzt jetzt CS_LINKS/CS_RECHTS an beiden `start_placement_mode`-Stellen (Detach ~287, „Platzieren" ~400), über einen gemeinsamen Helfer `placementDragPoints`:

- **Drag** (bewegtes Teil): `CS_EIN_L`, `CS_EIN_R`, `CS_EIN_START_L`, `CS_EIN_START_R`
- **Drop** (Ziel): `CS_EIN_L`/`START_L` → `CS_OUT_R_{0,90,270}`, `CS_EIN_R`/`START_R` → `CS_OUT_L_{0,90,270}`
- `_0` Flucht, `_90` vorne, `_270` hinten; Self-Snap und belegte Punkte via `:not(…)` / `openOnly` ausgeschlossen.

Typecheck meiner Datei sauber (die `ProductBrowser.vue`-Fehler sind vorbestehend). Die zwei fehlschlagenden Tests (`detach` Assembly-Anzahl, `90° dock`) liegen im `GroupProcessor` — die stehen bei dir als `M` in Arbeit und hängen nicht an den Selektoren.


ok -- ist noch genauso kapuut wie vorher


Bevor ich rate — ich prüfe, ob die Root-Punkte überhaupt die `name-…`-Tags tragen, sonst matcht der Selektor nichts:


cd /private/tmp/claude-502/-Users-marcus-hinz-claude-projects-robotunits/b762fc5c-841a-4069-a852-f92cf1c68110/scratchpad && python3 -c "
import json
d=json.load(open('katalog/schutzzaun_basic/SBE0174PNA.json',encoding='utf-8',errors='replace'))
for h in d['handles']:
    tf=next((c for c in h.get('components',[]) if c['_resourceClass']=='component_transform'),None)
    if not (tf and tf.get('key')=='root'): continue
    print('ROOT handle tags:', h.get('tags'))
    for c in h.get('components',[]):
        if c['_resourceClass']=='component_pmi' and c.get('id') in ('CS_EIN_L','CS_EIN_R','CS_OUT_R_0','CS_OUT_L_0'):
            print('  PMI', c.get('id'), 'tags=', c.get('tags'))
"

Check name tags on root drag/drop PMIs


ROOT handle tags: None
  PMI CS_EIN_L tags= ['name-CS_EIN_L']
  PMI CS_EIN_R tags= ['name-CS_EIN_R']
  PMI CS_OUT_R_0 tags= ['name-CS_OUT_R_0']
  PMI CS_OUT_L_0 tags= ['name-CS_OUT_L_0']
Shell cwd was reset to /Users/marcus.hinz/claude-projects/robotunits


Tags passen (`CS_EIN_L` → `name-CS_EIN_L`, `CS_OUT_R_0` → `name-CS_OUT_R_0`), die Selektoren matchen also. Der Code ist korrekt.

„Genauso kaputt" — also unverändert — ist verdächtig: die vorige Fassung (`CS_LINKS`/`CS_RECHTS`) matchte **gar nichts** (die PMIs heißen `CS_EIN_LINKS`, nicht `CS_LINKS`) → Snap tot. Wenn sich mit meiner Änderung nichts ändert, läuft im Storybook wahrscheinlich noch das **alte Bundle**. Der Processor wird in der Story einmalig instanziiert; HMR tauscht ihn oft nicht.

Zwei Punkte:

1. **Storybook neu starten** (nicht nur reload). Mit `! pnpm --filter @ad-cpq/common-frontend storybook` in deiner Session. Dann sind die neuen Selektoren live.
2. **Diskriminator:** Wenn danach die gelben Boxen **nicht mehr 20 m** füllen, sondern am Teil kleben, greift die neue Logik (Drop = nur Root-`CS_OUT`, keine 21-m-Clones mehr). Wenn weiterhin 20 m gelb → altes Bundle läuft noch.

Was auch mit neuem Code kaputt bleibt (Datenfehler, nicht Code):
- Anbau **rechts** (`CS_OUT_R_*` ~1 m versetzt) landet daneben. Links sollte greifen.
- Zum Snappen muss ein **zweites Teil** im Graph sein (Drop-Ziel); mit nur einem Teil gibt es nichts zum Andocken.

Sag mir, ob nach dem Neustart die 20-m-Boxen weg sind — daran sehen wir sofort, ob es am Bundle lag oder an den Daten.


nach neustart seh' ich gar nichzts mehr


„Gar nichts" heißt: die Selektoren lösen zur Laufzeit 0 Punkte auf. Der neue Code ist also live (Verhalten hat sich geändert), aber die Punkte fehlen für das getestete Teil. Ich prüfe, welche Szene die Story lädt und ob die überhaupt `CS_EIN_L`/`CS_OUT` auf Root hat:


cd ~/projects/adesso-cpq && echo "=== welche Datei lädt die Story? ===" && grep -oE '/products/[^"]+\.json' packages/frontend/src/components/visual/Fire3d.stories.ts | sort -u

Find which product the story loads


=== welche Datei lädt die Story? ===
/products/schutzzaun_basic/SBS1044PLA.json


cd ~/projects/adesso-cpq && python3 -c "
import json
f='packages/frontend/.storybook/products/schutzzaun_basic/SBS1044PLA.json'
d=json.loads(open(f,encoding='utf-8',errors='replace').read())
from collections import Counter
allids=Counter(); rootids=Counter()
for h in d.get('handles',[]):
    tf=next((c for c in h.get('components',[]) if c['_resourceClass']=='component_transform'),None)
    is_root = tf and tf.get('key')=='root'
    for c in h.get('components',[]):
        if c['_resourceClass']=='component_pmi':
            allids[c.get('id')]+=1
            if is_root: rootids[c.get('id')]+=1
print('Datei:',f.split('/')[-1])
print('ROOT-PMIs:', dict(rootids) or 'KEINE')
print()
print('hat CS_EIN_L/R auf root?', any(k in rootids for k in ['CS_EIN_L','CS_EIN_R']))
print('hat CS_OUT auf root?', any(k.startswith('CS_OUT') for k in rootids))
print()
print('alle PMI-Namen (gesamt, top):', allids.most_common(20))
"

Inspect root PMIs of the loaded scene file


The user doesn't want to proceed with this tool use. The tool use was rejected (eg. if it was a file edit, the new_string was NOT written to the file). STOP what you are doing and wait for the user to tell you how to proceed.


[Request interrupted by user for tool use]


HÖR AUF ZU FANTSIEREN
Habe nach wie vor den Effekt mit dr Gelben Fläche --> ich dachte, dsa sei über die Selektoren göst?


cd ~/projects/adesso-cpq && FIRE=node_modules/.pnpm/@ad+fire@1.0.5/node_modules/@ad/fire/browser/index.js && python3 - "$FIRE" <<'PY'
import sys,re
s=open(sys.argv[1],encoding='utf-8',errors='replace').read()
def ctx(pat,before=140,after=220,n=6,needle=None):
    c=0
    for m in re.finditer(pat,s):
        a=max(0,m.start()-before); t=s[a:m.end()+after]
        if needle and needle not in t: continue
        t=re.sub(r'_fire_visual_\w+__WEBPACK_IMPORTED_MODULE_\d+__\.','',t); t=re.sub(r'\s+',' ',t)
        print('>>',t); print('--'); c+=1
        if c>=n: break
print('### guidingPlane ###')
ctx(r'guidingPlane', n=8)
PY

Find guidingPlane / yellow surface source in fire


### guidingPlane ###
>> ionType = v; } constructor(settings) { super(settings); this._vertices = []; this._edges = []; this.guidingPlanes = []; this.insertionType2Polygon = {}; this._initialized = false; this.insertionType2Polygon[InsertionType.axisAlignedBoundingBox] = this.axisAllignedBB2Polygons.bind(this); this.inserti
--
>> es interface aufräumen update(volumes, afterCallback) { this._vertices.length = 0; this._edges.length = 0; this.guidingPlanes.forEach(p => p.points.length = 0); this.generatePartition(volumes); this.applyGuidingPlanes(); afterCallback && afterCallback(this); this.dispatchEvent(new _fire_visual_engine__WEBPACK_I
--
>>  return this; } clear(afterCallback) { return this.update([], afterCallback); } applyGuidingPlanes() { this.guidingPlanes.forEach(this.applyGuidingPlane.bind(this)); for (let i = 0; i < this.guidingPlanes.length; i++) { for (let j = i + 1; j < this.guidingPlanes.length; j++) { const planeA = this.guidin
--
>> ); } applyGuidingPlanes() { this.guidingPlanes.forEach(this.applyGuidingPlane.bind(this)); for (let i = 0; i < this.guidingPlanes.length; i++) { for (let j = i + 1; j < this.guidingPlanes.length; j++) { const planeA = this.guidingPlanes[i], planeB = this.guidingPlanes[j]; this.getIntersectionVertices(_f
--
>> (this.applyGuidingPlane.bind(this)); for (let i = 0; i < this.guidingPlanes.length; i++) { for (let j = i + 1; j < this.guidingPlanes.length; j++) { const planeA = this.guidingPlanes[i], planeB = this.guidingPlanes[j]; this.getIntersectionVertices(Calculations.intersection
--
>> this.guidingPlanes.length; i++) { for (let j = i + 1; j < this.guidingPlanes.length; j++) { const planeA = this.guidingPlanes[i], planeB = this.guidingPlanes[j]; this.getIntersectionVertices(Calculations.intersection.planeAgainstPlane(planeA.plane, planeB.plane)).forEach(vertex =>
--
>> { for (let j = i + 1; j < this.guidingPlanes.length; j++) { const planeA = this.guidingPlanes[i], planeB = this.guidingPlanes[j]; this.getIntersectionVertices(Calculations.intersection.planeAgainstPlane(planeA.plane, planeB.plane)).forEach(vertex => { planeA.po
--
>>  this._vertices.push(node); mapping.indices.forEach(i => id2Callback[i](node)); }); } applyGuidingPlane(guidingPlane) { this.castPlane(guidingPlane.plane) .map(v => this.makeGuidingplaneVertexAtPosition(v, 'guidingPlane')) .filter(v => !!v) .forEach(v => guidingPlane.points.push(v)); }
--


HÖR AUF ZU FANTSIEREN Habe nach wie vor den Effekt mit dr Gelben Fläche --> ich dachte, dsa sei über die Selektoren göst?


Du hast recht, und mein Denkfehler liegt offen: Die Selektoren steuern, **welche Punkte gesnappt werden** — nicht die gelbe Fläche.

Die gelbe Fläche ist die **Guiding Plane** des Placement-Modus. In der fire-Library gibt es dafür eine eigene Mechanik (`guidingPlanes`, `applyGuidingPlane`, `castPlane` → gelbe Vertices mit Tag `guidingPlane`), die die Placement-Ebene aufspannt. Die wird aus den Volumen/Ebenen des Teils generiert, nicht aus der Drag/Drop-Selektor-Liste. Mein `dragPoints`/`dropPoints`-Umbau ändert daran nichts — deshalb ist die Fläche „nach wie vor" da.

Ich habe vorhin die gelben Boxen (Drop-Punkte) und die gelbe Fläche (Guiding Plane) in einen Topf geworfen. Das war falsch.

Was die Guiding Plane steuert, weiß ich noch nicht — das steht in den Placement-Options der fire-Library, nicht in `DefaultCommandProcessor`. Sag, ob ich da nachsehen soll; raten will ich nicht mehr.


dringend


cd ~/projects/adesso-cpq && FIRE=node_modules/.pnpm/@ad+fire@1.0.5/node_modules/@ad/fire/browser/index.js && python3 - "$FIRE" <<'PY'
import sys,re
s=open(sys.argv[1],encoding='utf-8',errors='replace').read()
def ctx(pat,before=160,after=240,n=6,needle=None,skip=None):
    c=0
    for m in re.finditer(pat,s):
        a=max(0,m.start()-before); t=s[a:m.end()+after]
        if needle and needle not in t: continue
        if skip and skip in t: continue
        t=re.sub(r'_fire_visual_\w+__WEBPACK_IMPORTED_MODULE_\d+__\.','',t); t=re.sub(r'\s+',' ',t)
        print('>>',t); print('--'); c+=1
        if c>=n: break
print('### wo werden guidingPlanes befuellt / erzeugt ###')
ctx(r'guidingPlanes\.push|guidingPlanes =|new GuidingPlane|GuidingPlane\(', n=6)
print('\n### placement option: guidingPlane / showGuidingPlane / plane ###')
ctx(r'[gG]uidingPlane', needle='option', n=4)
print('\n### wer ruft castPlane / erzeugt die Ebene im Placement ###')
ctx(r'castPlane', n=4)
PY

Find how guiding planes are created and configured


### wo werden guidingPlanes befuellt / erzeugt ###
>> his._settings.insertionType = v; } constructor(settings) { super(settings); this._vertices = []; this._edges = []; this.guidingPlanes = []; this.insertionType2Polygon = {}; this._initialized = false; this.insertionType2Polygon[InsertionType.axisAlignedBoundingBox] = this.axisAllignedBB2Polygons.bind(this); this.insertionType2Polygon[Insertio
--
>> fromVector(mapping.vertex) }; this._vertices.push(node); mapping.indices.forEach(i => id2Callback[i](node)); }); } applyGuidingPlane(guidingPlane) { this.castPlane(guidingPlane.plane) .map(v => this.makeGuidingplaneVertexAtPosition(v, 'guidingPlane')) .filter(v => !!v) .forEach(v => guidingPlane.points.push(v)); } addGu
--
>> => this.makeGuidingplaneVertexAtPosition(v, 'guidingPlane')) .filter(v => !!v) .forEach(v => guidingPlane.points.push(v)); } addGuidingPlane(plane) { if (this.guidingPlanes.find(p => p.plane.equal(plane) || p.plane.equal(plane.clone().flip()))) { this.logger.warn((0,logMessage)(this, 'addGuidingPlane', `guiding
--
>> ngPlane', `guiding plane ${plane.toString()} already exists.`)); return; } const guidingPlane = { plane, points: [] }; this.guidingPlanes.push(guidingPlane); if (this.partition?.elementCount) { this.applyGuidingPlane(guidingPlane); const planeA = guidingPlane; for (let i = this.guidingPlanes.length - 2; i >= 0; i--) { co
--
>>  const guidingPlane = { plane, points: [] }; this.guidingPlanes.push(guidingPlane); if (this.partition?.elementCount) { this.applyGuidingPlane(guidingPlane); const planeA = guidingPlane; for (let i = this.guidingPlanes.length - 2; i >= 0; i--) { const planeB = this.guidingPlanes[i]; this.getIntersectionVertices(_fire_visual_m
--
>> ex.addDirection(n.element.plane.normal); }); this._vertices.push({ faceCount: 0, point: newVertex }); return newVertex; } removeGuidingPlane(plane) { for (let i = 0; i < this.guidingPlanes.length; i++) { if (this.guidingPlanes[i] == plane) { const deletedPlane = this.guidingPlanes.splice(i, 1)[0]; deletedPlane.points.forEach(th
--

### placement option: guidingPlane / showGuidingPlane / plane ###

### wer ruft castPlane / erzeugt die Ebene im Placement ###
>> this._vertices.push(node); mapping.indices.forEach(i => id2Callback[i](node)); }); } applyGuidingPlane(guidingPlane) { this.castPlane(guidingPlane.plane) .map(v => this.makeGuidingplaneVertexAtPosition(v, 'guidingPlane')) .filter(v => !!v) .forEach(v => guidingPlane.points.push(v)); } addGuidingPlane(plane) { if (this.g
--
>>  break; } cur = cur.parent; } } castRay(ray) { return this.partition.testAgainstRay(ray); } castPlane(plane) { return this.partition.testAgainstPlane(plane); } assureVertexAtPosition(position, ...tags) { const vertex = this.findVertex(v => v.equal(position) && tags.every(t => v.hasTag(t))); if (vertex) {
--


cd ~/projects/adesso-cpq && FIRE=node_modules/.pnpm/@ad+fire@1.0.5/node_modules/@ad/fire/browser/index.js && python3 - "$FIRE" <<'PY'
import sys,re
s=open(sys.argv[1],encoding='utf-8',errors='replace').read()
# full placement options: from 'placement:' default block, grab ~600 chars
for m in re.finditer(r"placement:\s*\{", s):
    t=s[m.start():m.start()+700]
    t=re.sub(r'_fire_visual_\w+__WEBPACK_IMPORTED_MODULE_\d+__\.','',t); t=re.sub(r'\s+',' ',t)
    print('>>',t); print('----')
PY

Extract full placement options schema


>> placement: { _type: 'object', default: () => ({ maxJumpDistance: 1, highlightColor: math_dist_browser.Color.yellow, dragPointActiveColor: math_dist_browser.Color.yellow, dragPointInactiveColor: math_dist_browser.Color.blue, dragPointRadius: 0.1 }), properties: { maxJumpDistance: { _type: 'number', default: 1 }, highlightColor: { _type: math_dist_browser.Color, default: () => math_dist_browser.Color.yellow }, dragPointActiveColor: { _type: math_dist_browser.Color, default: () => math_dist_browser.Color.yellow },
----


Kurz und ohne Raten, mit Beleg aus dem Bundle:

Die gelbe Fläche ist die **Guiding Plane**. Sie wird aus den **Volumen des Teils** aufgebaut, nicht aus den Drag/Drop-Selektoren:
- die Partition wird mit `update(volumes)` gefüllt, `applyGuidingPlane` ruft `castPlane(plane)` → `partition.testAgainstPlane` und erzeugt die gelben `guidingPlane`-Vertices.

Heißt: Ihre Ausdehnung folgt der **Geometrie-Ausdehnung des Teils**, nicht der Punkt-Auswahl. Deshalb hat mein Selektor-Umbau sie nicht berührt — falscher Hebel.

Und die Geometrie-Ausdehnung ist genau der bekannte Datenfehler: die versetzten `clone_*`-Collider bei 21 m blasen die Bounding-Box/Partition auf 21 m → die Guiding Plane spannt über 21 m → gelbe Fläche über den ganzen Graph.

**Hebel ist also die Geometrie, nicht der Selektor:** die >5-m-Teile aus der Szene entfernen (das „Rausnehmen" von vorhin — reine Blatt-Handles), dann kollabiert die Partition auf die echte Größe und die Fläche schrumpft mit.

Soll ich die >5-m-Handles aus den geladenen Szene-Dateien strippen? Das ist der direkte Test und zugleich der Fix.


Das ist eine dre PMI Böppels


Verstanden — kein Guiding Plane, sondern ein PMI-Böppel. Dann ist der Punkt/Selektor doch der Hebel, und `allowAllDragPoints: true` ist verdächtig: das könnte **alle** PMI-Böppel zeigen, egal was der Selektor sagt. Ich prüfe, was das Flag tut:


cd ~/projects/adesso-cpq && FIRE=node_modules/.pnpm/@ad+fire@1.0.5/node_modules/@ad/fire/browser/index.js && python3 - "$FIRE" <<'PY'
import sys,re
s=open(sys.argv[1],encoding='utf-8',errors='replace').read()
def ctx(pat,before=180,after=260,n=8):
    c=0
    for m in re.finditer(pat,s):
        a=max(0,m.start()-before); t=s[a:m.end()+after]
        t=re.sub(r'_fire_visual_\w+__WEBPACK_IMPORTED_MODULE_\d+__\.','',t); t=re.sub(r'\s+',' ',t)
        print('>>',t); print('--'); c+=1
        if c>=n: break
print('### allowAllDragPoints ###')
ctx(r'allowAllDragPoints', n=8)
PY

Check allowAllDragPoints behavior


### allowAllDragPoints ###
>> ragPointHandles = []; this._grabActive = true; this._prevInteractionClick = false; this._lastPlacement = null; this.activeDragPointIndex = settings.allowAllDragPoints === false ? 0 : -1; } getModeName() { return 'placement'; } processInput(input) { if (!input.pointerNormalizedPosition || !this._settings.camera) { return false; } // Rising edge: resume movement
--
>>  this.activeDragPointIndex = this.previousDragPointIndex(); } nextDragPointIndex() { const last = this._settings.dragPoints.length - 1; if (this._settings.allowAllDragPoints === false) { return this.activeDragPointIndex >= last ? 0 : this.activeDragPointIndex + 1; } return this.activeDragPointIndex >= last ? -1 : this.activeDragPointIndex + 1; } previousDragPointIndex() { const last = t
--
>> PointIndex >= last ? -1 : this.activeDragPointIndex + 1; } previousDragPointIndex() { const last = this._settings.dragPoints.length - 1; if (this._settings.allowAllDragPoints === false) { return this.activeDragPointIndex <= 0 ? last : this.activeDragPointIndex - 1; } return this.activeDragPointIndex <= -1 ? last : this.activeDragPointIndex - 1; } mapActiveDragPoints(func) { if (this.acti
--
>> amera: this._inputFeature.renderContext.camera, subject: subjectHandle, defaultCursorTransform: params.cursorTransform, allowAllDragPoints: params.allowAllDragPoints, allowDragPointCycling: params.allowDragPointCycling, maxJumpDistance: params.maxJumpDistance, dragPoints: resolvedDragPoints }; const mode
--
>> enderContext.camera, subject: subjectHandle, defaultCursorTransform: params.cursorTransform, allowAllDragPoints: params.allowAllDragPoints, allowDragPointCycling: params.allowDragPointCycling, maxJumpDistance: params.maxJumpDistance, dragPoints: resolvedDragPoints }; const mode = this.dependencyResolver.i
--


`allowAllDragPoints` steuert genau das Böppel-Rendering:

- `this.activeDragPointIndex = allowAllDragPoints === false ? 0 : -1`
- Bei **`true`** (aktueller Wert) ist der Index −1 → die Visualisierung zeigt **alle Drop-Punkte aller Drag-Punkte gleichzeitig** (`this.dropPoints.forEach(group => group.forEach(dp => dp.setVisualization(true)))`).
- Bei **`false`** nur die des einen aktiven Drag-Punkts, durchschaltbar.

Das ist der Hebel für „zu viele/zu große gelbe Böppel": auf `false` setzen → es wird nur ein Böppel-Satz gezeigt statt aller auf einmal.

Bleibt ein einzelner Böppel riesig/weit weg, ist das ein versetzter Punkt (Koordinate) oder die Böppel-Größe (`dragPointRadius: 0.1`) — dann brauche ich, welcher PMI das ist (Name/Position).

`allowAllDragPoints: false` an beiden Stellen setzen — soll ich?