From 9cbdf5ab5848652298d8b3e049ba4a6c50d23c70 Mon Sep 17 00:00:00 2001 From: mhoennig Date: Sun, 23 Aug 2026 10:14:08 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20eine=20einzelne=20Station=20ist=20ein=20?= =?UTF-8?q?g=C3=BCltiger=20Pfad=20(D38-Nachtrag=203)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit drawCheapPath() stieg bei weniger als zwei Stationen ganz aus. Solange ein eingeklappter Knoten nur einen Zweig vertrat, fiel das nicht auf; klappt man den Wurzelknoten zu, bleibt genau eine sichtbare Station — und damit verschwand neben der Linie (richtig) auch der Stationspunkt (falsch), also der Pfad genau dort, wo die Faltung ihn am nötigsten vertreten muss. Die Zwei-Punkte-Schranke gehört an die Linie, nicht an die Zeichenfunktion; im Grafikexport dieselbe Trennung. Dabei gefunden: Der zugeklappte Wurzelknoten des Werkbaum-Plans ließ sich nicht wieder aufklappen. Dort ist der Zustand nicht in Marken ausdrückbar (das `<` in Zeile 160 holt seinen Teilbaum hervor), liegt also in der Sitzungs-Überlagerung. Beim Aufklappen ergab das einen unveränderten Text — replaceTextUndoable() meldete Erfolg, ohne Textänderung feuert aber kein input-Ereignis, also lief kein render(). Jetzt meldet sie „nichts geschrieben", und der Aufrufer zeichnet selbst neu. Außerdem: Faltmarken-Stellung auch in den Schreibregeln von llms.md. Co-Authored-By: Claude Fable 5 --- docs/DECISIONS.md | 31 +++++++++++++++++++++++++++++++ docs/SPEC.md | 6 +++++- frontend/CLAUDE.md | 11 +++++++++-- frontend/public/llms.md | 2 ++ frontend/src/app.js | 39 ++++++++++++++++++++++++++------------- 5 files changed, 73 insertions(+), 16 deletions(-) diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1eee53b..b35b947 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -2386,6 +2386,37 @@ Der Preis ist benannt und klein: Ein eingeklappter Zweig zeigt **eine** Station statt mehrerer. Das ist keine Ungenauigkeit, sondern die Aussage der Faltung — „hier drin liegt noch Pfad", genauer geht es ohne Aufklappen nicht. +**Nachtrag 3 — eine einzelne Station ist ein gültiger Pfad.** Der Nachtrag +oben behob den Fall „eingeklappter Zweig wird übersprungen“ an der +Klassenvergabe (`cheapCls`) — und ließ dabei eine zweite, ältere Schranke +stehen: `drawCheapPath()` stieg bei weniger als **zwei** Stationen ganz aus. +Solange jeder eingeklappte Knoten nur *einen* Zweig vertrat, fiel das nicht +auf. Es fällt auf, sobald weit oben gefaltet wird: Klappt man den +**Wurzelknoten** des Werkbaum-Plans zu, bleibt genau eine sichtbare Station — +und damit verschwand nicht nur die Linie (richtig, durch einen Punkt führt +keine), sondern auch der **Stationspunkt** (falsch). Der Pfad war ausgerechnet +dort ganz weg, wo die Faltung ihn am nötigsten vertreten muss. Nachgemessen +vor der Korrektur: Knoten `root-node cheap cheap-leaf folded`, Stationspunkte +**0**; danach 1 Punkt, 0 Linien, im Grafikexport dasselbe (1 Kreis, keine +Pfadlinie). + +Die Schranke gehört an die **Linie**, nicht an die Zeichenfunktion: `catmullRom` +braucht zwei Punkte, die Punkte brauchen einander nicht. Dieselbe Trennung im +Export (`cheapPts.length >= 2` nur noch für die Linie). + +**Dabei gefunden: der zugeklappte Wurzelknoten ließ sich nicht wieder +aufklappen.** Ein eigener Fehler, nur über denselben Weg erreichbar. Im +Werkbaum-Plan ist „Wurzel eingeklappt“ **nicht in Marken ausdrückbar** — das +`<` in Zeile 160 holt seinen Teilbaum immer wieder hervor —, der Zustand liegt +also in der Sitzungs-Überlagerung (der dritte Fall aus Nachtrag 2). Beim +**Auf**klappen berechnete `writeFoldToText()` dann einen Text, der mit dem +vorhandenen identisch war: nichts zu schreiben. `replaceTextUndoable()` meldete +dafür Erfolg — aber ohne Textänderung feuert kein `input`-Ereignis, also lief +kein `render()`, und das Bild blieb stehen, wie es war. Jeder weitere Klick tat +dasselbe. Jetzt meldet die Funktion „nichts geschrieben“, und der Aufrufer +zeichnet selbst neu. Nachgemessen: 144 Knoten/69 Punkte → 1/1 → 144/69 → 1/1 +über mehrere Klicks. + ## D39 — Effektiver Status: die Farbe sagt die Wahrheit, die Marke die Box Mit den Abhängigkeiten (D37) gibt es zwei Aussagen je Knoten: was in der Statusbox steht (intrinsisch) und wie weit er wirklich ist (effektiv, §4). diff --git a/docs/SPEC.md b/docs/SPEC.md index 580eb5e..a32e877 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -710,7 +710,11 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom). Endknoten darunter. Das gilt auch, wenn der eingeklappte Knoten selbst nicht gebraucht wird, sein Teilbaum aber schon (etwa ein per `:#…` gezogenes Ziel): Er ist dann der einzige sichtbare Griff auf nötige Arbeit und tritt deshalb - auch nicht zurück. + auch nicht zurück. Bleibt so nur **eine** sichtbare Station übrig (etwa bei + eingeklapptem Wurzelknoten), entfällt die **Linie** — durch einen einzelnen + Punkt führt keine —, der **Stationspunkt bleibt**; sonst verschwände der + Pfad ausgerechnet dort ganz, wo der Baum am dichtesten gefaltet ist. Gilt + auch im Grafikexport. - **Export und Druck folgen der sichtbaren Faltung** (dieselbe Regel wie beim „verworfene einblenden“-Filter): Verborgene Teilbäume fehlen, eingeklappte Knoten behalten die Kennzeichnung „▸ n“ — das Bild behauptet damit keine diff --git a/frontend/CLAUDE.md b/frontend/CLAUDE.md index 4111307..8d66a6e 100644 --- a/frontend/CLAUDE.md +++ b/frontend/CLAUDE.md @@ -316,7 +316,10 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der Pfad-Mitgliedschaft (ein per `:#…` gezogenes Ziel unter einem `+`-Knoten): Dann ist er der einzige sichtbare Griff darauf und zählt als `cheap`. `extraCls()` muss dieselbe Eingeklappt-Bedingung bilden wie `itemHtml` - (nur mit sichtbaren Kindern). + (nur mit sichtbaren Kindern). **Eine einzelne Station ist ein gültiger Pfad** + (D38-Nachtrag 3): `drawCheapPath()` steigt nur bei **null** Stationen aus, + die Zwei-Punkte-Schranke gilt allein der Linie (`catmullRom`) — im Export + ebenso. Sonst verschwindet der Pfad beim eingeklappten Wurzelknoten ganz. **Umklappen schreibt in den TEXT zurück** (`writeFoldToText`, D38-Nachtrag 2): Die Ableitung Text → Zustand ist nicht umkehrbar (mehrere Markensätze ergeben denselben Zustand; `<` faltet Knoten ohne eigene Marke), deshalb **minimal @@ -332,7 +335,11 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der Mobil die Tastatur hoch (`keyboardOnJump(true)`). Bei `src.readOnly` (Pad, D31) wird nicht geschrieben, dort trägt weiter `foldOverrides` — die Überlagerungen werden nach erfolgreichem Schreiben geleert, sonst maskieren - sie den Text. Umklappen: Klick aufs + sie den Text. **`replaceTextUndoable()` meldet bei unveränderten Text + `false`** (D38-Nachtrag 3): Ohne Textänderung feuert kein `input`, also läuft + kein `render()` — der Aufrufer muss dann selbst zeichnen. Sonst bleibt das + Bild stehen (aufgefallen beim Aufklappen eines nur per Überlagerung + gefalteten Knotens: der Klick tat sichtbar nichts mehr). Umklappen: Klick aufs `.fold`-Zeichen (preventDefault — es sitzt bei Link-Knoten im ``) oder ←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line` wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg diff --git a/frontend/public/llms.md b/frontend/public/llms.md index 06646e5..1aa670f 100644 --- a/frontend/public/llms.md +++ b/frontend/public/llms.md @@ -155,6 +155,8 @@ One node per line. Everything except the label is optional. - Never mix `|` or `=` with other gates in one sibling group. - Decompose everything `(M)` or larger. - Put a node ID in front of the title, separated by a colon: `#auth: Backend`. +- Put a fold mark after the status box, right before the label: + `- [x] > Concept`. Before the box it would shift the box by one indent step. - Give a node its own status only; express "blocked by" with `:#…` dependencies instead of understating the status. - Mark nice-to-haves with `+` — otherwise they inflate the cheapest path. diff --git a/frontend/src/app.js b/frontend/src/app.js index 700bd59..4b52955 100644 --- a/frontend/src/app.js +++ b/frontend/src/app.js @@ -275,7 +275,12 @@ function drawCheapPath(){ out.querySelectorAll('svg.cheap-overlay').forEach(e => e.remove()); if(!cheapPathOn) return; const leaves = [...out.querySelectorAll('.node.cheap-leaf')]; /* Dokument-Reihenfolge = Lese-Reihenfolge */ - if(leaves.length < 2) return; + /* EINE Station ist ein gültiger Pfad: Ein eingeklappter Knoten vertritt + seinen ganzen Teilbaum (D38-Nachtrag), oben im Baum bleibt davon leicht + nur eine einzige sichtbare Station übrig. Die Linie entfällt dann (durch + einen Punkt führt keine), der Stationspunkt darf es nicht — sonst + verschwände der Pfad genau dort ganz, wo er am dichtesten gefaltet ist. */ + if(!leaves.length) return; const outRect = out.getBoundingClientRect(); const z = effZoom() || 1; if(!outRect.width || !outRect.height) return; /* Panel eingeklappt */ @@ -284,16 +289,18 @@ function drawCheapPath(){ return {x:(r.left + r.width/2 - outRect.left)/z, y:(r.top + r.height/2 - outRect.top)/z}; }); const w = outRect.width/z, h = outRect.height/z; - const d = catmullRom(pts); + const d = pts.length > 1 ? catmullRom(pts) : null; /* kräftige Linie HINTER die Knoten (als erstes Kind → hinterste Paint-Ebene) */ - const back = overlaySvg('cheap-back', w, h); - back.appendChild(svgEl('path', {class:'cheap-path', d})); - out.insertBefore(back, out.firstChild); + if(d){ + const back = overlaySvg('cheap-back', w, h); + back.appendChild(svgEl('path', {class:'cheap-path', d})); + out.insertBefore(back, out.firstChild); + } /* davor: abgetönte Kopie (deutet den Verlauf über Knoten an) + Stationspunkte */ const front = overlaySvg('cheap-front', w, h); - front.appendChild(svgEl('path', {class:'cheap-path faint', d})); + if(d) front.appendChild(svgEl('path', {class:'cheap-path faint', d})); pts.forEach(p => front.appendChild( svgEl('circle', {class:'cheap-dot', cx:p.x.toFixed(1), cy:p.y.toFixed(1), r:10}))); out.appendChild(front); @@ -574,12 +581,13 @@ function diagramToSvg(){ parts.push(`1`); }); - /* 3b) Günstigster-Pfad: abgetönte Kopie über den Knoten + Stationspunkte */ - if(cheapPts.length >= 2){ - parts.push(cheapLine('0.2')); - cheapPts.forEach(p => parts.push( - ``)); - } + /* 3b) Günstigster-Pfad: abgetönte Kopie über den Knoten + Stationspunkte. + Die Linie braucht zwei Punkte, die Stationen nicht — bei stark gefaltetem + Baum bleibt leicht nur eine sichtbare Station übrig (siehe + `drawCheapPath`). */ + if(cheapPts.length >= 2) parts.push(cheapLine('0.2')); + cheapPts.forEach(p => parts.push( + ``)); /* 4) Geister-Knoten „Untergliederung fehlt“ */ out.querySelectorAll('.ghost-node').forEach(g => { @@ -898,7 +906,12 @@ function foldStateMatches(txt, want){ Stück zwischen gemeinsamem Anfang und Ende. */ function replaceTextUndoable(neu){ const alt = src.value; - if(alt === neu) return true; + /* Nichts zu schreiben heißt: kein `input`-Ereignis, also auch kein render(). + Deshalb false — der Aufrufer zeichnet dann selbst neu. Sonst bliebe das + Bild stehen, wie es war: Der Fall tritt auf, wenn die Marken den + gewünschten Zustand schon beschreiben (etwa beim Aufklappen eines Knotens, + dessen Faltung nur in der Sitzungs-Überlagerung stand). */ + if(alt === neu) return false; let s = 0; while(s < alt.length && s < neu.length && alt[s] === neu[s]) s++; let e = 0;