feat: add superfig paper-figure toolkit
Standalone LaTeX/TikZ skill for non-tensor paper figures: node/edge macros, lint-on-warning build, golden examples, and negative fixtures.
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
# superfig.sty API
|
||||
|
||||
```tex
|
||||
\documentclass[border=10pt]{standalone}
|
||||
\usepackage[cjk]{superfig} % cjk: ctex + fandol (XeLaTeX). en: English rail labels.
|
||||
```
|
||||
|
||||
Build with `./scripts/build.sh fig.tex` — it puts `assets/` on `TEXINPUTS`.
|
||||
|
||||
The build first runs `scripts/lint.py`. It rejects ledger changes, more than four
|
||||
active hues, raw `\draw`/`\fill`/`\path`, a formula placed before the last row, a
|
||||
callout that is not an aside on a `\sfrow` band, a second callout, and a group
|
||||
with fewer than two members. Intentional galleries may put
|
||||
`% superfig-lint: allow-raw-tikz, allow-multiple-callouts` near the top; do not
|
||||
add an exemption to a deliverable merely to make it pass.
|
||||
|
||||
Rules for *when* to use each primitive live in `grammar.md` and `layout.md`.
|
||||
|
||||
## Ledgers
|
||||
|
||||
```tex
|
||||
\sfsetrole{input}{sfTeal} % role -> color. Macros take a ROLE, never a color.
|
||||
\sfsetlabels{A}{O}{M} % override the three meaning-box rail labels
|
||||
\sfsetrail{5.4em} % width of the bold label rail (`[en]` defaults wider)
|
||||
```
|
||||
|
||||
Colors: `sfTeal sfOrange sfCoral sfViolet sfGray sfInk`. An unknown role falls
|
||||
back to gray **and emits a package warning**, which `build.sh` turns into a
|
||||
failed build. Redeclaring a role with a different color warns and keeps the
|
||||
original mapping.
|
||||
|
||||
Lengths: `\sfgutter` (6 mm), `\sfrowgap` (3.5 mm), `\sfblockgap` (9 mm),
|
||||
`\sflinklen` (10 mm).
|
||||
|
||||
## Flow layout
|
||||
|
||||
Empty `at` places the object at the cursor. Hand-written offsets are for
|
||||
genuinely non-linear topology.
|
||||
|
||||
```tex
|
||||
\sfstage{SA}{stage heading}
|
||||
\sfrow{R1}{16mm}
|
||||
\sfnode[role=input]{x}{输入 $x$}{16mm}{12mm}
|
||||
\sfconn{e1}{预处理}
|
||||
\sfnode[role=model]{f}{模型 $f_\theta$}{18mm}{12mm}
|
||||
\sfrowend
|
||||
\sflane{R1}
|
||||
\sfcaption{x}{$x$}{原始输入}
|
||||
```
|
||||
|
||||
| macro | does |
|
||||
|---|---|
|
||||
| `\sfstage{name}{text}` | stage heading on the left rail, below all ink so far |
|
||||
| `\sfrow{name}{height}` | open a band; `height` is a length |
|
||||
| `\sfrowend` | `fit` the band into `name` |
|
||||
| `\sfnode[keys]{name}{label}{w}{h}` | one semantic object |
|
||||
| `\sfop{name}{glyph}` | operator in the flow; reserves its own width |
|
||||
| `\sfconn{name}{label}` | connector; empty label reserves `\sflinklen` of bare arrow |
|
||||
| `\sfgap{4mm}` / `\sfvgap{4mm}` | extra space, horizontal / vertical |
|
||||
| `\sfleftrail{x}` | move the left rail |
|
||||
| `\sfbbox{all}` | everything drawn so far, as one node |
|
||||
| `\sftopformula{F}{math}` | claim/formula, centered on what was actually drawn |
|
||||
| `\sftrack{node}` | fold a hand-placed TikZ node into the bbox and vertical cursor |
|
||||
| `\sflayoutreset` | start over |
|
||||
|
||||
`\sfnode` keys: `role=` (default `neutral`), `level=` (`1/2/3` → `!30/!55/!80`),
|
||||
`gap=` (space *before* this object in a row), `bracket=` (matrix-style arms),
|
||||
`at={}` (empty = cursor; a coordinate bypasses the row).
|
||||
|
||||
Call `\sftopformula` **after** the last `\sfrowend`. A formula placed first is
|
||||
centered on a figure whose width is not yet known.
|
||||
|
||||
## Fixed topology
|
||||
|
||||
```tex
|
||||
\sfnode[role=enc, at={(0,-22mm)}]{in}{输入}{16mm}{10mm}
|
||||
\sfarrow{in.east}{attn.west}
|
||||
\sfarrowlabel[bend left=18]{in.north}{add.north}{残差}
|
||||
```
|
||||
|
||||
`\sfarrow` / `\sfarrowlabel` take the same `to[]` options as TikZ (`bend left`,
|
||||
`out=south, in=north`). The label uses `auto` so a vertical edge does not sit
|
||||
the text on the shaft. Route is on the background layer.
|
||||
|
||||
Keep `at=` on a coarse grid or derive it from existing anchors
|
||||
(`at={($(te.east)!0.5!(ve.east)+(20mm,0)$)}`).
|
||||
|
||||
## Groups, captions, callouts
|
||||
|
||||
```tex
|
||||
\sfgroup[role=attn]{block}{(attn)(ffn)}{} % empty overlay; caption the group
|
||||
\sflane{R1}
|
||||
\sfcaption{block}{主路模块}{注意力 + 前馈}
|
||||
\sfcallout{N1}{38mm}{R1}{title}{body}
|
||||
```
|
||||
|
||||
- `\sfgroup` needs at least two members in the fit list. A single-node outline
|
||||
is a package warning. After a row, the group refits that row so `\sflane{R1}`
|
||||
hangs captions below the outline.
|
||||
- Prefer an empty overlay caption and `\sfcaption{group}{...}{...}`. An overlay
|
||||
sits on the north-west corner and collides with skip edges.
|
||||
- `\sfcallout{name}{width}{band}{title}{body}` hangs off a **finished** `\sfrow`
|
||||
band, one per figure. Inside an open row it is a package error; a non-band
|
||||
anchor or a second card is a warning.
|
||||
- `\sfcaptiontop{name}{text}` is the occasional label above a node.
|
||||
- `\sfnolane` turns the shared caption baseline off.
|
||||
|
||||
## Bottom
|
||||
|
||||
```tex
|
||||
\sfbbox{all}
|
||||
\sfmeaningbox{mb}{96mm}{all}{idea}{objects}{mechanism}
|
||||
\sfsignature{subject}{mb} % optional; subject only
|
||||
```
|
||||
|
||||
Arg 2 is the total box width. An empty `{}` row is dropped. `\usepackage[en]`
|
||||
switches the rails to Concept / Objects / Mechanism.
|
||||
|
||||
## Lint exemptions
|
||||
|
||||
`% superfig-lint: allow-raw-tikz, allow-multiple-callouts, allow-single-group`
|
||||
near the top of the source. Deliverables normally have none.
|
||||
Reference in New Issue
Block a user