frontend: optionale Abzweige gestrichelt + Stiel trifft die Knotenmitte

Zwei Nachbesserungen aus dem ersten Blick auf `+` (D29).

**Gestrichelter Abzweig.** Der hohle Kreis allein war zu leise. Der Einwand
gegen einen dritten Linienstil bleibt richtig, greift aber nicht so weit wie
gedacht: Gestrichelt wird NUR der Abzweig zum Knoten (nicht die Sammelleiste —
die gehört der ganzen Gruppe) und zwar in TINTE, während die any-of-Linien
gestrichelt in Grau sind. Entscheidend ist, dass beide sich nie am selben
Verteiler treffen können: | darf nach SPEC §3 nicht mit -/+ gemischt werden.
Der Kreis bleibt zusätzlich — er sagt, WELCHER Knoten gemeint ist.

Umgesetzt an den vorhandenen Pseudoelementen, ohne neue Zeichenebene: im
Fächer der senkrechte Stiel (border-left von ::after, beim letzten Kind
border-right von ::before), gestapelt der waagerechte Ast (border-top von
::before); die jeweils andere Kante ist die Leiste und bleibt durchgezogen.
Dafür braucht auch das <li> die Klasse `opt` — den Abzweig zeichnet es, nicht
der Knoten. Der SVG-Export zieht mit (dash || isOpt).

**Stiel trifft die Knotenmitte auch waagerecht (`--stem-x`).** Dabei fiel ein
älterer Fehler auf, gemeldet als „die Linie zu Wahl trifft den Knoten nicht":
Im horizontalen Fächer lief der Stiel zu einem Knoten mit any-of-Kindern neben
dem Knoten vorbei (gemessen 13,4 px). Dieselbe Verwechslung, die D10 senkrecht
schon behoben hat, nur in der anderen Achse — der Stiel saß bei 50 % der ZELLE,
und das ist nur die Knotenmitte, solange der Knoten darin zentriert steht.
`li.has-or` ist aber flex-start (der Knoten steht links, damit die
any-of-Sammelleiste unter ihm aufsetzt), und die Zelle ist so breit wie der
Teilbaum.

Rein in CSS nicht lösbar: gebraucht wird die Knotenbreite, und kein Selektor
macht sie einer Elternregel zugänglich (Anchor Positioning ist Chrome-only).
`alignStems()` misst deshalb nach jedem render() und in applyLayout() die
Knotenmitte der betroffenen Zellen und setzt `--stem-x`; die Pseudoelemente
rechnen über left:var(--stem-x, 50%) bzw. right:calc(100% - var(--stem-x, 50%)).
Der Rückfallwert 50 % hält alle übrigen Zellen ohne Messung richtig, die
transponierten Modi setzen left/right ohnehin fest. Messwerte werden wie in
drawCheapPath() durch `zoom` zurückgerechnet.

Verifiziert: Vitest 60/60 (2 neue Tests: `opt` auch am <li> neben dem Gate der
eigenen Kinder; <li> ohne Attribut, wenn weder Kinder noch optional).
Im Browser gemessen: Abweichung Stiel↔Knotenmitte bei „Wahl" 13,4 px → 0,0 px,
alle übrigen Zellen unverändert 0,0 px ohne gesetzte Variable. Angesehen in
horizontal und kompakt: gestrichelter Ast in Tinte zum Kreis, Sammelleiste
durchgezogen, deutlich unterscheidbar vom gestrichelt-grauen any-of-Ast
daneben. SVG-Export gerendert geprüft: 2 von 9 Tinte-Linien gestrichelt, beide
Kreise am Auftreffpunkt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-07-27 21:11:20 +02:00
co-authored by Claude Opus 4.8
parent aa3e087ee6
commit 091e8849d8
8 changed files with 157 additions and 26 deletions
+27 -4
View File
@@ -570,12 +570,19 @@
content:'';position:absolute;top:0;left:50%;height:14px;border-left:2px solid var(--line);
}
ul.and>li{padding:16px 12px 0}
/* Der Stiel liegt bei `--stem-x` — normalerweise 50 % des <li>, was die
Knotenmitte ist, solange der Knoten im <li> zentriert steht. Bei
`li.has-or` ist er das NICHT (linksbündig, damit die any-of-Leiste unter
ihm aufsetzt) und der Teilbaum macht das <li> breiter als den Knoten: der
Stiel liefe daneben vorbei. `alignStems()` in app.js misst dort die
Knotenmitte und setzt `--stem-x` — D10 in der Waagerechten. */
ul.and>li::before,
ul.and>li::after{
content:'';position:absolute;top:0;right:50%;
border-top:2px solid var(--line);width:50%;height:16px;
content:'';position:absolute;top:0;
left:0;right:calc(100% - var(--stem-x, 50%));
border-top:2px solid var(--line);height:16px;
}
ul.and>li::after{right:auto;left:50%;border-left:2px solid var(--line)}
ul.and>li::after{left:var(--stem-x, 50%);right:0;border-left:2px solid var(--line)}
ul.and>li:only-child{padding-top:14px}
ul.and>li:only-child::before,ul.and>li:only-child::after{border-top:none}
ul.and>li:only-child::after{border-left:2px solid var(--line);height:14px}
@@ -631,7 +638,23 @@
font-weight:400;
}
/* --- OPTIONAL (`+`, SPEC §3/D29): hohler Kreis am Abzweig ---
/* --- OPTIONAL (`+`, SPEC §3/D29): gestrichelter Abzweig + hohler Kreis ---
Gestrichelt wird NUR der Abzweig zum Knoten, nicht die Sammelleiste — die
gehört der ganzen Geschwistergruppe. Im Fächer ist der Abzweig der
senkrechte Stiel (border-left von ::after, beim letzten Kind border-right
von ::before), in den gestapelten Anordnungen der waagerechte Ast
(border-top von ::before); die Leiste ist jeweils die andere Kante.
Gestrichelt in TINTE — any-of ist gestrichelt in Grau (D15), die Farbe
hält beide auseinander, der Kreis unten bestätigt es. */
ul.and>li.opt::after{border-left-style:dashed}
ul.and>li.opt:last-child::before{border-right-style:dashed}
.tree.vertical ul.and>li.opt::before,
.tree.kompakt ul.and>li.opt::before,
.tree:not(.vertical):not(.kompakt) ul.or>li.has-and>ul.and>li.opt::before{
border-top-style:dashed;
}
/* Hohler Kreis am Abzweig ---
Kein dritter Linienstil: gestrichelt gehört any-of, und im kompakten Modus
codiert allein der Stil das Gate (D15). Stattdessen die FODA-Konvention —
gefüllter Punkt = erforderlich (hier: gar keiner), hohler Punkt = optional.