fix: eine einzelne Station ist ein gültiger Pfad (D38-Nachtrag 3)

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 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-23 10:14:08 +02:00
co-authored by Claude Fable 5
parent be729239ce
commit 9cbdf5ab58
5 changed files with 73 additions and 16 deletions
+31
View File
@@ -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).
+5 -1
View File
@@ -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
+9 -2
View File
@@ -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 `<a>`) oder
←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line`
wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg
+2
View File
@@ -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.
+21 -8
View File
@@ -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) */
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(`<text x="${m.x.toFixed(1)}" y="${(m.y+3).toFixed(1)}" text-anchor="middle" fill="#6B7A8C" font-size="9" font-weight="600">1</text>`);
});
/* 3b) Günstigster-Pfad: abgetönte Kopie über den Knoten + Stationspunkte */
if(cheapPts.length >= 2){
parts.push(cheapLine('0.2'));
/* 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(
`<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="10" fill="#0F766E" fill-opacity="0.2" stroke="#0F766E" stroke-opacity="0.35" stroke-width="1.5"/>`));
}
/* 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;