Add flow layout: cursor placement, left rail, declared band heights

Figures were positioned by hand-written offsets. Every gap was a magic
number tuned against the content that happened to be there, so a label
that grew two characters landed on the next tensor, and two stages
started from two different x shared no rail. Both failures compile
cleanly.

Replace it with a cursor. Objects placed with an empty coordinate
argument reserve their own width -- including a stack's offset sheets
and a bracket's overhang -- and gaps are declared once (\stgutter,
\strowgap, \stblockgap). The gap belongs to the object that follows it
and the first object in a band gets none, so every band starts flush on
a shared rail and gap=0pt states that two shards tile exactly.

\stlink makes a connector's label a flow object, which is what removes
the label-wider-than-its-arrow failure entirely. \stcol is a vertical
sub-flow for a split along the contracted axis. \strow declares its
height, so an object that does not fit -- or a column that does not add
up to what it declared, and is therefore drawn off-center -- becomes a
package warning, which build.sh fails on.

Absolute placement is unchanged: passing a coordinate takes the original
code path, and \sttrack folds a hand-placed node back into the cursor.

All three golden examples and the new tests/flow.tex are converted and
build clean.
This commit is contained in:
dela
2026-08-05 15:55:21 +08:00
parent 7a22bef9e3
commit 7b59c81d02
11 changed files with 672 additions and 223 deletions
+35 -19
View File
@@ -4,11 +4,29 @@ Everything on the canvas is a bounding box: tensors, full offset stacks, bracket
operators, arrow labels, annotations, symbols, shape labels, stage headings, the meaning
box, the signature. **Tangency counts as collision.**
## Place by cursor, not by coordinate
The invariants below are *statements about the finished figure*; the flow layout in
`api.md` is how you get them without checking each one by hand. Use it by default:
`\ststage` / `\strow` … `\strowend` / `\stcol` … `\stcolend`, with an empty coordinate
argument on every face.
The failure it removes is specific. With hand-written offsets, each gap is a magic number
tuned against the *current* content, so the day a label grows by two characters it silently
lands on the next tensor — the figure still compiles and still looks clean. With the
cursor, every object reserves its own width, so growing one object can only push the rest
apart. And because a band declares its height, an object that does not fit becomes a
`Package supertensor Warning`, which `build.sh` turns into a failed build.
Hand placement remains available for the cases the cursor cannot express. When you use it,
wrap the node in `\sttrack` so the bounding box and the vertical cursor still see it.
## Gutters
Define one base gutter `g ≥ 1 em`. Unrelated boxes stay at least `g` apart; stage bands at
least `1.5g`. In practice: `right=5mm–9mm` between an operator and its operands, `7mm–9mm`
between the last caption of one row and the next stage heading.
Define one base gutter `g ≥ 1 em`; that is what `\stgutter` (6 mm) is. Unrelated boxes stay
at least `g` apart; stage bands at least `1.5g` (`\strowgap`, `\stblockgap`). Set them once
at the top of the figure rather than per call — a per-call `gap=` is for stating a
*relationship* (`gap=0pt` = these shards tile), not for nudging.
Overlap is allowed only inside one declared composite:
@@ -33,26 +51,22 @@ shapes <- \stcaption arg 3
```
Faces of different heights would otherwise hang their captions at different depths. Fix it
with a shared baseline:
with a shared baseline — `\strowend` arms one automatically:
```tex
\node[inner sep=0pt, fit=(Q)(KT)(S)] (rowA) {};
\stlane{rowA}
\strow{rowA}{T}
...
\strowend
\stcaption{Q}{$\mathbf Q^{(i)}$}{$h\times T\times d_h$}
...
\stnolane
```
Every `\stcaption` between `\stlane` and `\stnolane` hangs from the bottom of `rowA`, so
symbols and shapes form two flat lanes.
Under absolute placement, arm it yourself with `\stlane{rowA}` … `\stnolane`. Either way
every `\stcaption` inside hangs from the bottom of `rowA`, so symbols and shapes form two
flat lanes.
Stage headings share one left rail. Anchor each heading below the previous row but at the
previous *heading's* x, not at the previous row's content:
```tex
\coordinate (cy) at ($(I-shape.south)+(0,-9mm)$);
\node[st stage, anchor=north west] (SC) at (SB.west |- cy) {...};
```
Stage headings share one left rail. `\ststage` puts them there: the rail is a single stored
`x` (`\stleftrail` to move it), and the `y` is derived from the lowest ink drawn so far, so
a heading can neither drift right nor collide with the row above it.
Explanatory prose belongs in the stage subtitle, the bottom box, or above its own
connector. Never drop a floating commentary card between two operands unless it is a real
@@ -67,8 +81,10 @@ never cover a face. Two consequences you still own:
- A connector may not cross a box that is not one of its endpoints. Move the row, don't
route over.
- A label's white underlay may cover only its own connector — never a tensor, never
another label. If the label is wider than the arrow, shorten the label or widen the gap.
This is the single most common failure after a first draft.
another label. This is the single most common failure after a first draft, and `\stlink`
is the fix: it makes the label itself a flow object and draws the arrow to whatever
lands on either side of it, so the label cannot be wider than its connector.
`\starrowlabel` between two hand-placed nodes still has the old failure mode.
## Stacks