fix: eingeklappter Knoten vertritt seinen Teilbaum auf dem günstigsten Pfad
D38 hielt fest, ein eingeklappter Zweig dürfe Pfadknoten verbergen, „die Inversion an den sichtbaren Knoten bleibt richtig". Zu kurz gedacht: Die LINIE blieb eben nicht richtig. Sie fädelt durch die cheap-leaf-Knoten im DOM; ein eingeklappter Pfadknoten ist zwar `cheap`, aber kein Blatt (seine Pfad-Kinder existieren im Modell) — also bekam er keine Station, und die Linie umging den ganzen Zweig. Im Beispiel nachgemessen: „Concept" war `node cheap folded`, verbarg zwei Pfadknoten, trug keinen Punkt; die Linie begann erst bei „PWA". Das Bild behauptete damit, in diesem Zweig sei nichts zu tun — eine Aussage über den Plan, nicht über die Ansicht. Genau die Grenze, die D38 sonst zieht. Regel: Eingeklappt überspringt cheapCls() die Blatt-Prüfung und fragt den Teilbaum (hidesCheap). Liegt darin etwas auf dem Pfad, ist der Knoten dessen tiefste noch sichtbare Station. Beim Aufklappen geben die Kinder sie zurück — gemessen 6 -> 7 -> 6 Stationen beim Hin- und Herklappen. Auch ohne eigene Pfad-Mitgliedschaft: Ein per :#… gezogenes Ziel kann unter einem +-Knoten liegen, der selbst nicht gebraucht wird (D42). Eingeklappt ist dieser der einzige sichtbare Griff auf nötige Arbeit — er wird Station und zählt als cheap, tritt also nicht zurück. Ihn auszublassen wäre die Lüge. 4 neue Tests (208 gesamt). SPEC §9, D38 (Nachtrag) und frontend/CLAUDE.md nachgezogen; die alte „hinnehmbar"-Stelle in D38 ist als falsch markiert statt gelöscht. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6463a76e1e
commit
38104a4c5d
@@ -2223,6 +2223,7 @@ Und der günstigste Pfad rechnet unverändert über den ganzen Baum; seine
|
|||||||
Spline-Linie läuft ohnehin nur durch DOM-Knoten, führt also durch die
|
Spline-Linie läuft ohnehin nur durch DOM-Knoten, führt also durch die
|
||||||
sichtbaren Endknoten. Ein eingeklappter Zweig kann Pfadknoten verbergen —
|
sichtbaren Endknoten. Ein eingeklappter Zweig kann Pfadknoten verbergen —
|
||||||
hinnehmbar, die Inversion an den sichtbaren Knoten bleibt richtig.
|
hinnehmbar, die Inversion an den sichtbaren Knoten bleibt richtig.
|
||||||
|
**Das war falsch — siehe den Nachtrag am Ende dieses Eintrags.**
|
||||||
|
|
||||||
**Export und Druck:** Verborgene Kinder stehen nicht im DOM — Export,
|
**Export und Druck:** Verborgene Kinder stehen nicht im DOM — Export,
|
||||||
Stiel-Messung, Treppe und Pfadlinie sind damit ohne Zusatzcode konsistent
|
Stiel-Messung, Treppe und Pfadlinie sind damit ohne Zusatzcode konsistent
|
||||||
@@ -2231,6 +2232,38 @@ per CSS zu verstecken). Die Kennzeichnung „▸ n“ ist Teil des Knotentexts u
|
|||||||
wandert von selbst in den SVG-Export; das ▾ offener Knoten wird dort und im
|
wandert von selbst in den SVG-Export; das ▾ offener Knoten wird dort und im
|
||||||
Druck entfernt — es ist Bedienelement, keine Aussage über den Plan.
|
Druck entfernt — es ist Bedienelement, keine Aussage über den Plan.
|
||||||
|
|
||||||
|
**Nachtrag — der eingeklappte Knoten vertritt seinen Teilbaum auch auf dem
|
||||||
|
günstigsten Pfad.** Oben steht, ein eingeklappter Zweig dürfe Pfadknoten
|
||||||
|
verbergen, „die Inversion an den sichtbaren Knoten bleibt richtig". Das war zu
|
||||||
|
kurz gedacht, und der Nutzer hat es benannt: Die **Linie** blieb eben nicht
|
||||||
|
richtig. Sie fädelt durch die `cheap-leaf`-Knoten im DOM; ein eingeklappter
|
||||||
|
Pfadknoten ist `cheap`, aber kein Blatt (seine Pfad-Kinder sind ja im Modell
|
||||||
|
vorhanden) — also bekam er keine Station, und die Linie **umging den ganzen
|
||||||
|
Zweig**. Im mitgelieferten Beispiel nachgemessen: „Concept" war
|
||||||
|
`node cheap folded`, verbarg zwei Pfadknoten und trug keinen Punkt; die Linie
|
||||||
|
begann erst bei „PWA". Das Bild behauptete damit, in diesem Zweig sei nichts
|
||||||
|
zu tun — und das ist eine Aussage über den Plan, keine über die Ansicht. Genau
|
||||||
|
die Grenze, die dieser Eintrag sonst zieht.
|
||||||
|
|
||||||
|
**Regel: Eingeklappt ist der Knoten die tiefste noch sichtbare Station.**
|
||||||
|
`cheapCls(n, cheapSet, collapsed)` überspringt für eingeklappte Knoten die
|
||||||
|
Blatt-Prüfung und fragt stattdessen den Teilbaum: Liegt darin etwas auf dem
|
||||||
|
Pfad (oder der Knoten selbst), ist er `cheap cheap-leaf`. Beim Aufklappen
|
||||||
|
geben die Kinder die Station zurück — nachgemessen 6 → 7 → 6 Stationen beim
|
||||||
|
Hin- und Herklappen, Punkte jeweils gleich.
|
||||||
|
|
||||||
|
**Auch ohne eigene Pfad-Mitgliedschaft.** Ein per `:#…` gezogenes Ziel kann in
|
||||||
|
einem Zweig liegen, dessen Wurzel selbst nicht gebraucht wird (D42, etwa unter
|
||||||
|
einem `+`-Knoten). Ist dieser Zweig eingeklappt, ist seine Wurzel der einzige
|
||||||
|
sichtbare Griff auf nötige Arbeit — sie wird deshalb Station **und** zählt als
|
||||||
|
`cheap`, tritt also nicht zurück. Das ist keine Ausnahme, sondern dieselbe
|
||||||
|
Regel: Der eingeklappte Knoten **steht für** seinen Teilbaum, und der enthält
|
||||||
|
Nötiges. Ihn auszublassen wäre die Lüge.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
## D39 — Effektiver Status: die Farbe sagt die Wahrheit, die Marke die Box
|
## 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
|
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).
|
Statusbox steht (intrinsisch) und wie weit er wirklich ist (effektiv, §4).
|
||||||
|
|||||||
+10
-2
@@ -670,8 +670,16 @@ fokussierten Knoten (WAI-ARIA-Baum-Idiom).
|
|||||||
dauerhafte Aussage steht im Text (D34). Ein Dokumentwechsel setzt zurück.
|
dauerhafte Aussage steht im Text (D34). Ein Dokumentwechsel setzt zurück.
|
||||||
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
|
- Faltung ist **reine Ansicht**: Warnungen aus eingeklappten Teilbäumen werden
|
||||||
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
|
weiter gemeldet (sie gelten dem Text), und der günstigste Pfad rechnet
|
||||||
unverändert über den ganzen Baum — seine Linie zeigt nur die sichtbaren
|
unverändert über den ganzen Baum.
|
||||||
Endknoten.
|
- **Ein eingeklappter Knoten vertritt seinen Teilbaum auch auf dem günstigsten
|
||||||
|
Pfad:** Liegt darin etwas auf dem Pfad, ist er dessen tiefste noch sichtbare
|
||||||
|
Station — die Linie führt zu ihm und endet dort mit einem Stationspunkt
|
||||||
|
(„hier drin liegt noch Pfad"). Ohne das überspränge sie den ganzen Zweig, als
|
||||||
|
wäre dort nichts zu tun. Beim Aufklappen geben die Station wieder die
|
||||||
|
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.
|
||||||
- **Export und Druck folgen der sichtbaren Faltung** (dieselbe Regel wie beim
|
- **Export und Druck folgen der sichtbaren Faltung** (dieselbe Regel wie beim
|
||||||
„verworfene einblenden“-Filter): Verborgene Teilbäume fehlen, eingeklappte
|
„verworfene einblenden“-Filter): Verborgene Teilbäume fehlen, eingeklappte
|
||||||
Knoten behalten die Kennzeichnung „▸ n“ — das Bild behauptet damit keine
|
Knoten behalten die Kennzeichnung „▸ n“ — das Bild behauptet damit keine
|
||||||
|
|||||||
+9
-1
@@ -303,7 +303,15 @@ verworfene Elemente. Quelle sind ES-Module unter `src/`; `index.html` ist der
|
|||||||
Dokumentwechsel geleert) und übergibt `collapsedSet` an den Renderer; der
|
Dokumentwechsel geleert) und übergibt `collapsedSet` an den Renderer; der
|
||||||
lässt eingeklappte Kinder **weg** (nicht CSS-verstecken — Export, Messungen
|
lässt eingeklappte Kinder **weg** (nicht CSS-verstecken — Export, Messungen
|
||||||
und Pfadlinie bleiben so von selbst konsistent), meldet deren Warnungen aber
|
und Pfadlinie bleiben so von selbst konsistent), meldet deren Warnungen aber
|
||||||
weiter (`walkFolded`, zählt zugleich fürs „▸ n"). Umklappen: Klick aufs
|
weiter (`walkFolded`, zählt zugleich fürs „▸ n").
|
||||||
|
**Auf dem günstigsten Pfad vertritt ein eingeklappter Knoten seinen
|
||||||
|
Teilbaum** (D38-Nachtrag): `cheapCls(n, cheapSet, collapsed)` überspringt für
|
||||||
|
ihn die Blatt-Prüfung und fragt `hidesCheap()` — sonst umginge die Pfad-Linie
|
||||||
|
den ganzen Zweig, als wäre dort nichts zu tun. Gilt auch ohne eigene
|
||||||
|
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). Umklappen: Klick aufs
|
||||||
`.fold`-Zeichen (preventDefault — es sitzt bei Link-Knoten im `<a>`) oder
|
`.fold`-Zeichen (preventDefault — es sitzt bei Link-Knoten im `<a>`) oder
|
||||||
←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line`
|
←/→ am fokussierten Knoten; nach `render()` den Fokus per `data-line`
|
||||||
wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg
|
wiederherstellen. Export/Druck: „▸ n" bleibt, das ▾ offener Knoten fällt weg
|
||||||
|
|||||||
+19
-1
@@ -168,7 +168,25 @@ export function computeCheapSet(roots){
|
|||||||
}
|
}
|
||||||
/* CSS-Klassen für den günstigen Pfad. Leere `cheapSet` (Pfad aus) ⇒ ''.
|
/* CSS-Klassen für den günstigen Pfad. Leere `cheapSet` (Pfad aus) ⇒ ''.
|
||||||
Endknoten (kein Kind liegt auf dem Pfad) bekommt zusätzlich 'cheap-leaf'. */
|
Endknoten (kein Kind liegt auf dem Pfad) bekommt zusätzlich 'cheap-leaf'. */
|
||||||
export function cheapCls(n, cheapSet){
|
/* Liegt im Teilbaum eines Knotens etwas auf dem Pfad? Gebraucht für
|
||||||
|
eingeklappte Knoten (siehe `cheapCls`). Verworfene sind nie in `cheapSet`
|
||||||
|
(SPEC §9), es braucht also keine eigene Filterung. */
|
||||||
|
export function hidesCheap(n, cheapSet){
|
||||||
|
for(const k of n.children || []){
|
||||||
|
if(cheapSet.has(k) || hidesCheap(k, cheapSet)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cheapCls(n, cheapSet, collapsed){
|
||||||
|
/* Eingeklappt steht der Knoten stellvertretend für seinen ganzen Teilbaum
|
||||||
|
(SPEC §9/D38): Liegt darin etwas auf dem Pfad, ist er dessen tiefste noch
|
||||||
|
SICHTBARE Station — sonst überspränge die Linie den Zweig, als wäre dort
|
||||||
|
nichts zu tun. Das gilt auch, wenn er selbst nicht gebraucht wird, sein
|
||||||
|
Teilbaum aber schon (eine per `:#…` gezogene Alternative, D42): Er ist
|
||||||
|
dann der einzige sichtbare Griff auf nötige Arbeit und darf deshalb auch
|
||||||
|
nicht von der Inversion ausgeblasst werden. */
|
||||||
|
if(collapsed) return (cheapSet.has(n) || hidesCheap(n, cheapSet)) ? 'cheap cheap-leaf' : '';
|
||||||
if(!cheapSet.has(n)) return '';
|
if(!cheapSet.has(n)) return '';
|
||||||
const leaf = !pathChildren(n).some(k => cheapSet.has(k));
|
const leaf = !pathChildren(n).some(k => cheapSet.has(k));
|
||||||
return leaf ? 'cheap cheap-leaf' : 'cheap';
|
return leaf ? 'cheap cheap-leaf' : 'cheap';
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import { gateOf, needsBreakdown, visibleChildren, cheapCls } from './model.js';
|
|||||||
gegenüber der zuletzt gesehenen Fassung (D28, `freshSet` optional) und
|
gegenüber der zuletzt gesehenen Fassung (D28, `freshSet` optional) und
|
||||||
optionale Knoten (`+`, SPEC §3/D29 — trägt den hohlen Kreis am Abzweig). */
|
optionale Knoten (`+`, SPEC §3/D29 — trägt den hohlen Kreis am Abzweig). */
|
||||||
function extraCls(n, opts){
|
function extraCls(n, opts){
|
||||||
const cheap = cheapCls(n, opts.cheapSet);
|
/* Dieselbe Bedingung wie in `itemHtml`: eingeklappt ist ein Knoten nur, wenn
|
||||||
|
er überhaupt sichtbare Kinder hat. Der Pfad braucht sie hier, weil ein
|
||||||
|
eingeklappter Knoten seine verborgenen Pfad-Knoten vertritt (D38). */
|
||||||
|
const collapsed = !!(opts.collapsedSet && opts.collapsedSet.has(n))
|
||||||
|
&& visibleChildren(n, opts.showDiscarded).length > 0;
|
||||||
|
const cheap = cheapCls(n, opts.cheapSet, collapsed);
|
||||||
const fresh = opts.freshSet && opts.freshSet.has(n) ? 'fresh' : '';
|
const fresh = opts.freshSet && opts.freshSet.has(n) ? 'fresh' : '';
|
||||||
return [cheap, fresh, n.optional ? 'opt' : '', n.focus ? 'focusmark' : ''].filter(Boolean).join(' ');
|
return [cheap, fresh, n.optional ? 'opt' : '', n.focus ? 'focusmark' : ''].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,3 +126,64 @@ describe('Renderer — eingeklappte Teilbäume', () => {
|
|||||||
.toEqual(['A', 'A2', 'W']);
|
.toEqual(['A', 'A2', 'W']);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* Der eingeklappte Knoten vertritt seinen Teilbaum auch auf dem günstigsten
|
||||||
|
Pfad (SPEC §9, D38-Nachtrag): Sonst überspränge die Pfad-Linie den ganzen
|
||||||
|
Zweig, als wäre dort nichts zu tun. */
|
||||||
|
describe('Günstigster Pfad an eingeklappten Knoten', () => {
|
||||||
|
/* Nimmt die BEREITS geparsten Wurzeln: `collapsedSet` und `cheapSet` prüfen
|
||||||
|
auf Objektidentität — ein zweiter Parse-Durchlauf liefert andere Objekte
|
||||||
|
und die Mengen träfen nie zu (dieselbe Falle wie in D28). */
|
||||||
|
const cheapRender = (r, collapsedSet) =>
|
||||||
|
renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||||
|
cheapSet: computeCheapSet(r), collapsedSet: collapsedSet || new Set()}).html;
|
||||||
|
/* Labels der Stationen (`cheap-leaf`) in Dokumentreihenfolge — genau die
|
||||||
|
Menge, durch die drawCheapPath() die Linie fädelt. Gelesen aus dem
|
||||||
|
`aria-label` (erstes Glied = Label): Im Knoteninneren steht bei
|
||||||
|
eingeklappten Knoten das Falt-Zeichen vor dem Text. */
|
||||||
|
const stations = html =>
|
||||||
|
[...html.matchAll(/class="node[^"]*cheap-leaf[^"]*"[^>]*aria-label="([^",]*)/g)]
|
||||||
|
.map(m => m[1]);
|
||||||
|
|
||||||
|
const TXT = `[ ] W (XS)\n - [ ] A (S)\n - [ ] A1 (S)\n - [ ] A2 (S)\n - [ ] B (S)`;
|
||||||
|
|
||||||
|
it('macht den eingeklappten Knoten zur Station statt den Zweig zu überspringen', () => {
|
||||||
|
const r = roots(TXT);
|
||||||
|
const a = r[0].children[0];
|
||||||
|
expect(stations(cheapRender(r))).toEqual(['A1', 'A2', 'B']); /* offen */
|
||||||
|
expect(stations(cheapRender(r, new Set([a])))).toEqual(['A', 'B']); /* zu */
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gibt die Station beim Aufklappen wieder an die Kinder ab', () => {
|
||||||
|
const r = roots(TXT);
|
||||||
|
const a = r[0].children[0];
|
||||||
|
const zu = stations(cheapRender(r, new Set([a])));
|
||||||
|
const auf = stations(cheapRender(r, new Set()));
|
||||||
|
expect(zu).not.toContain('A1');
|
||||||
|
expect(auf).toContain('A1');
|
||||||
|
expect(auf).not.toContain('A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('vertritt auch einen Teilbaum, den der Knoten selbst nicht braucht', () => {
|
||||||
|
/* `+ O` ist entbehrlich und nicht auf dem Pfad — sein Kind wird aber per
|
||||||
|
Abhängigkeit gezogen (D42). Eingeklappt ist `O` der einzige sichtbare
|
||||||
|
Griff darauf und darf deshalb weder fehlen noch ausgeblasst sein. */
|
||||||
|
const txt = `[ ] W (XS)\n - [ ] N (S) :#t\n + [ ] O (S)\n - [ ] #t: T (S)`;
|
||||||
|
const r = roots(txt);
|
||||||
|
const o = r[0].children[1];
|
||||||
|
const set = computeCheapSet(r);
|
||||||
|
expect([...set].map(n => n.label).sort()).toEqual(['N', 'T', 'W']); /* O selbst nicht */
|
||||||
|
const {html} = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||||
|
cheapSet: set, collapsedSet: new Set([o])});
|
||||||
|
expect(stations(html)).toContain('O');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('macht einen eingeklappten Knoten ohne Pfad im Teilbaum NICHT zur Station', () => {
|
||||||
|
const txt = `[ ] W (XS)\n - [ ] N (S)\n + [ ] O (S)\n - [ ] P (S)`;
|
||||||
|
const r = roots(txt);
|
||||||
|
const o = r[0].children[1];
|
||||||
|
const {html} = renderTreeHtml(r, {t, showDiscarded: false, cheapPath: true,
|
||||||
|
cheapSet: computeCheapSet(r), collapsedSet: new Set([o])});
|
||||||
|
expect(stations(html)).toEqual(['N']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user