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:
@@ -15,9 +15,9 @@ dimension. **Fix:** swap the arguments — `\ststack{KT}{...}{dh}{T}{3}`. See `g
|
||||
## 2. Shards that do not tile their parent
|
||||
|
||||
Two shards drawn with a gap, or stretched to fill a parent whose other shards were elided.
|
||||
Both assert a width that the tensor does not have. **Fix:** place each shard from the
|
||||
previous one's edge (`($(W1a.east)+(2*\stunit,0)$)`), and draw an ellipsis for anything
|
||||
omitted. See `geometry.md` §5–6.
|
||||
Both assert a width that the tensor does not have. **Fix:** draw the shards in one band
|
||||
and give every shard after the first `gap=0pt`, which *states* that they are adjacent
|
||||
instead of arranging for it; draw an ellipsis for anything omitted. See `geometry.md` §5–6.
|
||||
|
||||
## 3. An index drawn as a heatmap
|
||||
|
||||
@@ -41,12 +41,18 @@ See `style.md`.
|
||||
reads the resulting stripe as real structure. `pattern=dense` avoids it; if you write
|
||||
your own filler, check that rows 1, 2, 4, 5 of a tall face are not identical.
|
||||
- **A label wider than its connector.** The white underlay then covers the target tensor.
|
||||
Shorten the label or widen the gap — never let it sit on a face. See `layout.md`.
|
||||
`\stlink` makes this unrepresentable: the label reserves its own width in the band and
|
||||
the arrow is drawn to whatever lands beside it. See `layout.md`.
|
||||
- **A new hue for a regrouped view of the same data.** `X` and the per-expert buffers
|
||||
gathered out of `X` are the same object in a different order; a second hue claims they
|
||||
are different tensors.
|
||||
- **Captions hanging at different depths** because the faces in a row have different
|
||||
heights. Use `\stlane`.
|
||||
heights. Use `\strow`/`\strowend`, which arms `\stlane` for you.
|
||||
- **Hand-tuned offsets.** Each one is a magic number valid only for the content that was
|
||||
there when you tuned it; the figure that breaks is the *next* one, when a label grows two
|
||||
characters and lands on a face. Use the cursor. See `layout.md`.
|
||||
- **A formula line placed first.** It can only be centered on a figure whose width is not
|
||||
known yet, so it ends up visibly off-center. Call `\sttopformula` after the bands.
|
||||
- **A floating commentary card between two operands.** If it is not a real operation, it
|
||||
belongs in the stage subtitle or the bottom box.
|
||||
- **A meaning box that repeats the shapes.** The shapes are already under every block. The
|
||||
|
||||
+97
-10
@@ -23,17 +23,85 @@ Colors: `stTeal stOrange stCoral stViolet stGray stInk`. An unknown role falls b
|
||||
|
||||
Lengths: `\stunit` (one cell, 4.6 mm) and `\sttilegap` (white gutter, 0.5 mm).
|
||||
|
||||
## Flow layout
|
||||
|
||||
**This is the default. Leave the coordinate argument empty and the object is placed by a
|
||||
cursor.** Hand-written offsets are the main source of layout bugs in these figures: every
|
||||
gap becomes a tuned magic number, so a label that grows by two characters silently lands on
|
||||
the next tensor, and two stages started from two different `x` share no rail.
|
||||
|
||||
```tex
|
||||
\ststage{SA}{stage heading} % on the left rail, below the previous band
|
||||
\strow{rowA}{T} % open a band, declared height T
|
||||
\stface[role=q]{Q}{}{T}{d} % empty coord = place at the cursor
|
||||
\stglyph{mA}{$\times$} % operator glyph; reserves its own width
|
||||
\stface[role=w]{W}{}{d}{d}
|
||||
\stlink{lA}{softmax} % connector whose LABEL is a flow object
|
||||
\stface[role=s]{S}{}{T}{d}
|
||||
\strowend % fit the band, arm the caption lanes
|
||||
\stcaption{Q}{$\mathbf Q$}{$T\times d$}
|
||||
```
|
||||
|
||||
| macro | does |
|
||||
|---|---|
|
||||
| `\ststage{name}{text}` | stage heading on the left rail, below all ink so far |
|
||||
| `\strow{name}{height}` | open a band; `height` is an axis name or an integer |
|
||||
| `\strowend` | `fit` the band into `name`, then `\stlane` it |
|
||||
| `\stcol{name}{height}` … `\stcolend` | vertical sub-flow filling one slot of the band |
|
||||
| `\stglyph{name}{$\times$}` | operator glyph (not `\stop` — plain TeX owns that name) |
|
||||
| `\stcomm{name}{All-Reduce}` | collective node |
|
||||
| `\stnode[style]{name}{text}` | any node, placed and measured by the cursor |
|
||||
| `\stlink{name}{label}` | connector; empty label reserves `\stlinklen` of bare arrow |
|
||||
| `\stgap{4mm}` / `\stvgap{4mm}` | one-off extra space, horizontal / vertical |
|
||||
| `\stbbox{all}` | everything drawn so far, as one node, for `\stmeaningbox` |
|
||||
| `\sttopformula{F}{math}` | the formula line, centered on what was actually drawn |
|
||||
| `\sttrack{node}` | fold a hand-placed node into the bbox and the vertical cursor |
|
||||
| `\stleftrail{x}` / `\stlayoutreset` | move the rail / start over |
|
||||
|
||||
Gaps are declared once: `\stgutter` (6 mm, between objects in a band), `\strowgap` (3.5 mm,
|
||||
above a band), `\stblockgap` (9 mm, above a stage heading), `\stlinklen` (10 mm).
|
||||
|
||||
Four properties follow by construction, and each of them is a bug class removed:
|
||||
|
||||
- **The gap belongs to the object that *follows* it**, and the first object in a band gets
|
||||
none — so every band starts flush on the same rail, and `gap=0pt` means *exactly
|
||||
adjacent*, which is how shards are made to tile their parent.
|
||||
- **Every object reserves its own width**, including a stack's offset sheets and a bracket's
|
||||
overhang. `right=6mm of X` reserves nothing, so the next face is free to land on top.
|
||||
- **A connector's label is a flow object**, so a label can never be wider than its arrow.
|
||||
- **A band declares its height**, so an object that does not fit is a *build failure*
|
||||
(`Package supertensor Warning`), not something the reader discovers.
|
||||
|
||||
Call `\sttopformula` **after** the bands. A formula placed first can only be centered on a
|
||||
figure whose width is not yet known — that is how the top line ends up visibly off-center.
|
||||
|
||||
`\stcol` is what a split along the contracted axis looks like:
|
||||
|
||||
```tex
|
||||
\stcol{W2}{dff} % declared total height
|
||||
\stface[role=r1]{W2a}{}{dffl}{d}
|
||||
\stface[role=r2, gap=0pt]{W2b}{}{dffl}{d} % tiles W2a exactly
|
||||
\stcolend
|
||||
```
|
||||
|
||||
A column that consumes a height other than the one it declares is drawn off-center, so that
|
||||
is a warning too.
|
||||
|
||||
Absolute placement still works everywhere — pass a coordinate instead of `{}`. Mix freely,
|
||||
but wrap hand-placed nodes in `\sttrack` so the cursor knows about them.
|
||||
|
||||
## Faces
|
||||
|
||||
```tex
|
||||
\stface[keys]{name}{(coord)}{rows}{cols}
|
||||
\stface[keys]{name}{}{rows}{cols} % flow
|
||||
\stface[keys]{name}{(coord)}{rows}{cols} % absolute
|
||||
\ststack[keys]{name}{(coord)}{rows}{cols}{sheets}
|
||||
\stindexface[keys]{name}{(coord)}{rows}{cols}{entries}
|
||||
```
|
||||
|
||||
`rows`/`cols` accept a declared axis name or a raw integer. `(coord)` must include its own
|
||||
parentheses — `{(0,0)}`, `{($(A.east)+(1.5,0)$)}`. `name` becomes a TikZ node you can
|
||||
anchor against; `\ststack` also defines `name-front`.
|
||||
`rows`/`cols` accept a declared axis name or a raw integer. A non-empty `(coord)` must
|
||||
include its own parentheses — `{(0,0)}`, `{($(A.east)+(1.5,0)$)}`. `name` becomes a TikZ
|
||||
node you can anchor against; `\ststack` also defines `name-front`.
|
||||
|
||||
Keys:
|
||||
|
||||
@@ -46,6 +114,7 @@ Keys:
|
||||
| `bracket=` | `false` | thin neutral matrix brackets |
|
||||
| `border=` | `true` | outer `black!60` border |
|
||||
| `tiles=` | `true` | `false` = one flat filled rectangle |
|
||||
| `gap=` | `\stgutter` | flow only: space *before* this object. `gap=0pt` = exactly adjacent |
|
||||
|
||||
`\stindexface` entries are row-major, `rows*cols` of them; `.` leaves a cell blank.
|
||||
It deliberately has no lightness ramp — see `semantics.md`.
|
||||
@@ -58,12 +127,21 @@ It deliberately has no lightness ramp — see `semantics.md`.
|
||||
|
||||
## Captions
|
||||
|
||||
`\strowend` calls `\stlane` for you, so in flow mode captions just follow the band:
|
||||
|
||||
```tex
|
||||
\strowend
|
||||
\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane
|
||||
\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face
|
||||
```
|
||||
|
||||
Arming the lane by hand (absolute placement):
|
||||
|
||||
```tex
|
||||
\node[inner sep=0pt, fit=(A)(B)(C)] (rowA) {};
|
||||
\stlane{rowA}
|
||||
\stcaption{A}{$\mathbf A$}{$T\times d$} % symbol lane, shape lane
|
||||
\stcaption{A}{$\mathbf A$}{$T\times d$}
|
||||
\stnolane
|
||||
\stcaptiontop{A}{\stshapefont{token}} % occasional label above a face
|
||||
```
|
||||
|
||||
`\stcaption` defines `name-sym` and `name-shape` nodes; anchor the next stage heading
|
||||
@@ -71,8 +149,10 @@ against `name-shape.south`.
|
||||
|
||||
## Operators, connectors, nodes
|
||||
|
||||
Prefer `\stglyph` / `\stcomm` / `\stlink` (above). The raw forms are for absolute placement:
|
||||
|
||||
```tex
|
||||
\node[st op, right=6mm of A] (m) {$\times$};
|
||||
\node[st op, right=6mm of A] (m) {$\times$}; % reserves nothing -- see layout.md
|
||||
\node[st comm, right=9mm of P] (ar) {All-Reduce};
|
||||
\starrow{P.east}{ar.west}
|
||||
\starrowlabel{M.east}{A.west}{softmax}
|
||||
@@ -85,13 +165,14 @@ Connectors route on the background layer automatically.
|
||||
## Bottom
|
||||
|
||||
```tex
|
||||
\node[inner sep=0pt, fit=(F)(rowA)(rowB)(Y-shape)] (all) {};
|
||||
\stbbox{all} % flow: everything drawn so far
|
||||
\stmeaningbox{mb}{16.6cm}{all}{axes text}{objects text}{mechanism text}
|
||||
\stsignature{因果多头注意力(掩码 + 拼接投影)}{mb}
|
||||
```
|
||||
|
||||
Arg 2 is the total box width; arg 3 is the node it hangs below — include every caption and
|
||||
top label in that `fit` or the box will overlap them. An empty `{}` row is dropped.
|
||||
Arg 2 is the total box width; arg 3 is the node it hangs below. `\stbbox` already contains
|
||||
every caption and top label; if you build the `fit` by hand, include them yourself or the
|
||||
box will overlap them. An empty `{}` row is dropped.
|
||||
|
||||
## Gotchas
|
||||
|
||||
@@ -101,3 +182,9 @@ top label in that `fit` or the box will overlap them. An empty `{}` row is dropp
|
||||
- `\foreach {\macro,...,1}` cannot infer its direction from an unexpanded macro.
|
||||
- `\strole` is expandable on purpose (it is used inside `\edef`); the warning lives in
|
||||
`\stcheckrole`.
|
||||
- Neither the TikZ path parser nor the `calc` library expands a macro sitting where it
|
||||
expects `(`. Every cursor-computed coordinate therefore reaches the parser as literal
|
||||
text, via `\edef ... \noexpand`. Same class of trap: pgfmath cannot digest `\stresolve`'s
|
||||
`\ifcsname`, so a ledger lookup must be pre-resolved with `\edef` before it is measured.
|
||||
- `\sttopformula` puts a group around its argument, which breaks TikZ's own `\\`. For more
|
||||
than one line, wrap the math in amsmath's `gathered`.
|
||||
|
||||
@@ -26,8 +26,13 @@ Work through it against the rendered PNG. Any mandatory violation means redraw,
|
||||
|
||||
## 4. Full-size visual audit
|
||||
|
||||
Open the PNG at 100 %.
|
||||
Open the PNG at 100 %. Most of the first three items are automatic under the flow layout;
|
||||
they still need looking at, because the cursor only guarantees that objects do not *push*
|
||||
into each other, not that the figure reads correctly.
|
||||
|
||||
- [ ] No absolute coordinate that a `\strow`/`\stcol` could have expressed. Every remaining
|
||||
hand-placed node is wrapped in `\sttrack`.
|
||||
- [ ] `\sttopformula` called after the bands, so the formula is centered on the real figure.
|
||||
- [ ] No forbidden intersection, tangency, clipping or occlusion — including stack offset
|
||||
sheets, brackets, arrow labels and the meaning box.
|
||||
- [ ] Every connector's white label underlay covers only its own connector.
|
||||
|
||||
+35
-19
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user