feat: Labels brechen balanciert um, ID-Zeile über dem Titel, Falt-Chip — D64

Lange Knotentitel brechen bei ~40 Zeichen um, gleichmäßig auf die Zeilen
verteilt — vom Renderer (wrapLabel, headless getestet) statt text-wrap:balance,
damit der Kasten auf die längste balancierte Zeile schrumpft. Der
23-px-Abzweig ankert an der ersten Zeile; der Optional-Kreis sitzt fest auf
der Abzweighöhe. Der Grafikexport misst die gerenderten Zeilen (labelLines)
und gibt je Zeile ein <text> aus. Dazu: die eingeblendete ID (D56) steht in
einer eigenen Zeile über dem Titel, und das Falt-Zeichen ist ein gerahmter
19×16-px-Chip mit gedeckelter Höhe. Plan: #ed.render.wrap, #ed.render M→L
(D62-Prüfung schlug korrekt an).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-24 17:45:11 +02:00
co-authored by Claude Fable 5
parent 1debb7d9da
commit 52232f2c20
10 changed files with 318 additions and 32 deletions
+3
View File
@@ -19,6 +19,9 @@ reverse.
## 2026-08-24
- Long node titles wrap into evenly balanced lines of at most forty characters
- With ids shown, the id sits on its own line above the title
- The fold sign is a framed chip now — a click target you can actually hit
- Typing `:#` suggests the document's ids — arrows choose, Enter inserts
- When the caret's node is folded away, the nearest visible ancestor takes the highlight — also on Alt+click from the text
- A size that cannot hold its sub-packages turns its badge amber and gets a warning — sizes are read as ranges, so only sure conflicts are reported
+73
View File
@@ -4769,3 +4769,76 @@ Tests in `tests/autocomplete.test.js` (397 gesamt): Kontext-Erkennung
D59, Ersetzen über die Schreibmarke hinaus), Sammeln (Dokumentreihenfolge,
verworfene, D60-Knoten ohne Titel) und Sortierung (Präfix vor Teilstring,
case-insensitiv, exclude).
## D64 — Lange Labels brechen um: balanciert, ~40 Zeichen, erste Zeile als Anker
Breite Knotenkästen trieben den Baum in die Breite — ein einziger langer Titel
kostete eine ganze Spalte davon. Jetzt brechen Labels um: höchstens ~40
Zeichen je Zeile (Nutzer-Vorgabe), zentriert, und die Zeichen **gleichmäßig
auf die Zeilen verteilt** (ausdrücklicher Nutzer-Nachtrag): Der gierige
Umbruch machte aus 44 Zeichen eine volle Zeile plus ein einsames Wort.
**Die Umbrüche setzt der Renderer, nicht CSS.** `text-wrap:balance` wäre eine
Zeile gewesen und ist verworfen: Es balanciert nur **innerhalb** der einmal
bestimmten Kastenbreite — der Kasten bliebe auf `max-width` stehen, mit
Leerraum um zwei kurze Zeilen. `wrapLabel()` in render.js bricht selbst
(Zeilenzahl = ⌈Länge/40⌉, Ziel = gleichmäßig, gebrochen wird an der Stelle,
die dem Ziel am nächsten kommt, nie mitten im Wort) und schreibt echte `\n`
ins Markup; `white-space:pre-line` macht sie sichtbar, und der Kasten
schrumpft auf die längste **balancierte** Zeile. Nebengewinn: Die Regel ist
headless testbar (Hausregel D54-Nachtrag 3) und in jedem Browser gleich.
`max-width:40ch` + `overflow-wrap` bleiben als Rückhalt für das einzelne Wort
über der Grenze. Bewusst `\n` statt `<br>`: Der `textContent` behielte sonst
keine Wortgrenze, und alles, was den Knotentext liest (Export,
Fokusmarken-Schlüssel), bekäme zusammengeklebte Wörter.
**Die Geometrie ankert an der ersten Zeile.** Der 23-px-Abzweig der
gestapelten Anordnungen (§9: 5 px Listenabstand + halbe einzeilige
Knotenhöhe) bleibt fest — bei mehrzeiligen Knoten trifft er damit die Mitte
der **ersten Zeile**, das gewohnte Idiom jeder Baumansicht. Eine gemessene
„wahre Mitte" hätte die 23-px-Konstante an einem Dutzend CSS-Stellen dynamisch
gemacht — für einen Unterschied, den das Auge als falsch gar nicht liest. Zwei
Stellen mussten mitziehen:
- Der **Optional-Kreis** (D29) saß bei `top:50%` der Knotenhöhe — für
einzeilige Knoten dasselbe wie die Abzweighöhe, für mehrzeilige nicht mehr.
Er sitzt jetzt fest bei 18 px (= Abzweighöhe), **auf dem Abzweig statt auf
der Knotenmitte**; die eine Ausnahme sind die vertikal zentrierten
all-of-Zwischenknoten, deren Abzweig wirklich die Mitte trifft (dort bleibt
50 %). Nachgemessen am dreizeiligen Optional-Knoten im vertikalen Modus:
Kreismitte = Abzweighöhe, 0 px Abweichung.
- Der **Grafikexport** zeichnete das Label als ein `<text>` — ein SVG-`<text>`
bricht nicht von selbst. `labelLines()` misst die gerenderten Zeilen am
Live-Knoten (zeichenweise per Range: neue Zeilen-Oberkante = neue Zeile —
das deckt auch den `overflow-wrap`-Bruch mitten im langen Wort ab, den eine
Nachbildung der Wortlogik verfehlte) und gibt je Zeile ein `<text>` an der
gemessenen Position aus. Die Linienführung des Exports zielte schon immer
auf die gemessene Knotenmitte und blieb unberührt.
**Zwei Nachträge aus derselben Runde, beide Nutzer-Wünsche:**
- **Die eingeblendete ID steht in einer eigenen Zeile ÜBER dem Titel**
(ändert D56). Vorher stand sie inline davor (`#some.id: Titel`) und machte
gerade die Knoten am breitesten, die ohnehin lange Titel tragen. Der
Trenn-Doppelpunkt entfällt: Er trennte ID und Titel in **derselben** Zeile —
hier trennt der Umbruch, und `#auth:` allein auf einer Zeile läse sich wie
ein Block-Kopf (§1). Der Export folgt von selbst (er misst Zeilen).
- **Das Falt-Zeichen ist jetzt ein gerahmter Chip** (ändert die D38-Optik):
Das nackte ▾ war ~10 px klein und schwer zu treffen. Der Chip (19×16 px,
Rahmen, dezente Füllung) ist das Klickziel, das er immer sein sollte. Seine
Höhe ist gedeckelt (`line-height:14px`, kein vertikales Padding), denn ein
Inline-Block, der höher ist als die Zeilenbox, höbe die feste Zeilenhöhe an,
an der die 23-px-Geometrie hängt — mit 15 px Zeilenhöhe wuchs der Knoten
nachgemessen von 34,3 auf 35,0 px, mit 14 px bleibt er exakt bei 34,3.
**Nachgemessen** am mitgelieferten Plan (horizontal, Pfad an): 166 Knoten,
0 Warnungen, 27 Stationen (unverändert), `--stem-x` weiterhin punktgenau auf
der Knotenmitte (130,8 = 130,8 px) — und am Testbaum: 79 Zeichen ergeben zwei
Zeilen zu ~40/39 statt 40+Rest, drei Zeilen bei ~100; im exportierten SVG
stehen die zwei Zeilen des langen Titels 18,3 px auseinander vollständig im
52,6-px-Kasten, die ID-Zeile als eigene Textzeile. Falten am Chip klappt
weiter um (166 → 140 → 166 Knoten). 405 Tests, davon 8 neue in
`tests/wrap.test.js` (Balance, keine Zeile über 40, kein Zeichenverlust, nie
mitten im Wort, `\n` im Knotentext); die showIds-Tests sind auf die eigene
Zeile umgeschrieben. Der neue Plan-Knoten `#ed.render.wrap (S)` kippte prompt
die Größenprüfung des eigenen Plans (`#ed.render (M)` mit nun 4×S, D62) —
ehrlich nachgezogen auf `(L)`, danach wieder 0 Warnungen.
+19 -7
View File
@@ -499,6 +499,15 @@ Siehe D29.
deterministisch auf Knotenmitte liegen (Offset 23 px = 5 px Listenabstand
+ halbe Knotenhöhe). Abzweige zielen auf den **Knoten**, nie auf die Mitte
des Teilbaums.
- **Lange Labels brechen um** (D64): höchstens ~40 Zeichen je Zeile, die
Zeichen **gleichmäßig** auf die Zeilen verteilt (nicht gierig — sonst stünde
unter einer vollen Zeile ein einsames Wort). Die Umbrüche setzt der
**Renderer**, deterministisch und nur an Leerzeichen; der Kasten schrumpft
auf die längste Zeile, der Text steht zentriert. Bei **mehrzeiligen** Knoten
trifft der 23-px-Abzweig die Mitte der **ersten Zeile** (die Zeilenhöhe
bleibt fest); der Optional-Kreis (§9) sitzt auf dem Abzweig, nicht auf der
Knotenmitte. Der Grafikexport misst die gerenderten Zeilen und gibt sie als
einzelne Textzeilen aus.
- Dasselbe gilt **waagerecht**: Im horizontalen Fächer trifft der Stiel die
**Knotenmitte**, nicht die Mitte der Zelle. Beides fällt nur zusammen, solange
der Knoten in seiner Zelle zentriert steht — ein Knoten mit any-of-Kindern
@@ -837,10 +846,11 @@ verknüpft (siehe D25):
Druck.
### Knoten-IDs im Diagramm einblenden (`#`)
Ein Umschalter im Diagramm-Kopf stellt die Knoten-ID **vor den Titel**, in
derselben Schreibweise wie im Text (§1/D36): `#some.id: Titel`. Dargestellt in
der Mono-Schrift des Textfelds und zurückgenommen gefärbt — die ID ist die
Adresse, der Titel führt die Zeile weiter an. Der Zustand wird gemerkt.
Ein Umschalter im Diagramm-Kopf stellt die Knoten-ID in eine **eigene Zeile
über den Titel** (`#some.id`, ohne den Trenn-Doppelpunkt — der trennt ID und
Titel in derselben Zeile, hier trennt der Umbruch; D56, geändert mit D64).
Dargestellt in der Mono-Schrift des Textfelds und zurückgenommen gefärbt — die
ID ist die Adresse, der Titel bleibt die Hauptzeile. Der Zustand wird gemerkt.
**Grafikexport und Druck folgen dem Umschalter** (wie beim „verworfene
einblenden"-Filter und der Faltung): Ist er an, stehen die IDs auch im
@@ -942,9 +952,11 @@ gehört und lesbar bleibt. Kein Blinken.
Grafikexport: Sie sagt „schau jetzt hierhin", nicht „so ist der Plan".
### Ein- und ausklappbare Teilbäume (`>` / `<`, §1)
Jeder Knoten mit sichtbaren Kindern trägt ein kleines **Falt-Zeichen** vor dem
Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knoten).
Klick auf das Zeichen klappt um — der einfache Klick auf den Knoten selbst
Jeder Knoten mit sichtbaren Kindern trägt ein **Falt-Zeichen** vor dem
Label: **▾** offen, **„▸ n“** eingeklappt (n = Zahl der verborgenen Knoten)
als kleiner **gerahmter Chip**, damit das Klickziel zu treffen ist (D64; das
nackte Glyph war zu klein). Klick auf das Zeichen klappt um — der einfache
Klick auf den Knoten selbst
bleibt der Link (§6); Tastatur: **←** klappt zu, **→** klappt auf am
fokussierten Knoten (WAI-ARIA-Baum-Idiom).
+7 -1
View File
@@ -34,10 +34,11 @@
- [~] #ed: Editor (XXL) https://werkbaum.javagil.de
- [^] #ed.live: Live diagram from text (L)
- [^] #ed.parser: Parser (S)
- [^] #ed.render: Renderer, three layout modes (M)
- [^] #ed.render: Renderer, three layout modes (L)
- [^] #ed.render.horiz: Horizontal fan (S)
- [^] #ed.render.vert: Vertical, transposed (S)
- [^] #ed.render.comp: Compact (S)
- [x] #ed.render.wrap: Long titles wrap into balanced lines (S) %% max ~40 chars, see D64
- [^] #ed.deplink: Cross links for dependencies (M)
- [^] #ed.deplink.calm: Visually secondary — the tree carries the message (S)
- [^] #ed.deplink.focus: Highlight what a selected node needs and feeds (S)
@@ -368,6 +369,11 @@
Everything runs downward, so the tree stays narrow. Here the line style
alone carries the gate: solid for and, dashed for or.
#ed.render.wrap
A single long title used to cost a whole column of width. Titles beyond
forty characters now break into evenly balanced lines, set by the renderer
so the box shrinks to the longest line; connectors anchor on the first line.
#ed.deplink
Dependencies drawn as curved dotted edges on their own layer behind the
nodes — the first line type that does not follow the decomposition.