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:
dela
2026-08-17 09:40:12 +08:00
commit db5598fbf7
39 changed files with 1732 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
build/
out/
*.aux
*.log
*.out
*.fls
*.fdb_latexmk
*.synctex.gz
+86
View File
@@ -0,0 +1,86 @@
# superfig
A paper-figure toolkit for non-tensor diagrams: a LaTeX/TikZ macro package, a
build pipeline that fails on silent corruption, worked examples, and an agent
skill that ties them together.
It is the generic-node sibling of `supertensor`. Same house style, same
role/cursor/warning discipline. The primitives are nodes, edges and groups,
not tensor faces. Use `supertensor` when axis lengths and shape identities
are the claim.
This repository is a **child** of SuperPaper, the family parent that
routes paper notes to the right figure toolkit. Clone SuperPaper with
`--recurse-submodules` for the whole family; use this directory alone
when you only need node/edge figures.
```
SKILL.md the skill entry point (lean; loads references on demand)
references/ grammar, layout, style, api, checklist, antipatterns
assets/superfig.sty the macro package
scripts/preflight.sh is the TikZ + CJK path available?
scripts/lint.py reject source-level invariant escapes
scripts/build.sh lint, compile, audit the log, export pdf/svg/png/thumb
scripts/test.sh positive examples plus negative package/lint fixtures
examples/ four golden examples + an anti-pattern gallery
```
## Quick start
```bash
./scripts/preflight.sh # 0 = full path, 1 = degraded, 2 = no LaTeX
./scripts/build.sh examples/pipeline.tex # -> examples/build/pipeline.{pdf,svg,png}
./scripts/test.sh
```
A minimal figure:
```tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{input}{sfTeal}
\sfsetrole{model}{sfOrange}
\begin{document}\begin{tikzpicture}
\sfstage{S}{one band, placed by cursor}
\sfrow{R1}{14mm}
\sfnode[role=input]{x}{输入 $x$}{16mm}{12mm}
\sfconn{e1}{预处理}
\sfnode[role=model]{f}{模型 $f_\theta$}{18mm}{12mm}
\sfrowend
\sflane{R1}
\sfcaption{x}{$x$}{原始输入}
\sfcaption{f}{$f_\theta$}{可学习参数}
\end{tikzpicture}\end{document}
```
See `references/api.md` for the full macro list.
## Examples
| file | shows |
|---|---|
| `pipeline.tex` | cursor flow; loss as a side object, not a station on the forward path |
| `branch-architecture.tex` | residual skip; a group captioned on the shared lane |
| `state-flow.tex` | time/state step; one `\sfcallout` on a finished band |
| `dependency-graph.tex` | dual-encoder join, fusion placed from existing anchors |
| `antipatterns.tex` | four figures that compile cleanly and still teach something false |
## Using it as an agent skill
`SKILL.md` is the entry point; the `references/` files are loaded on demand.
Point your agent runtime at this directory. The skill assumes `scripts/` and
`assets/` sit beside it.
## Why the build script fails on warnings
`Missing character` drops a CJK glyph silently. `Overfull \hbox` lets a label
escape its lane. `Package superfig Warning` is an undeclared role, a
single-member group, a band overflow, a second callout, or a callout hanging
off a node. `build.sh` greps for all of them and exits non-zero.
A clean build also proves the source avoided raw TikZ drawing, a formula
placed before the last row, and more than four active hues. It still cannot
prove that the relationships are right; that is what `references/checklist.md`
is for.
+82
View File
@@ -0,0 +1,82 @@
---
name: superfig
description: Create or refine clean paper-style figures for non-tensor concepts — architecture and block diagrams, pipelines and data flow, state/time flows, dependency graphs, and conceptual mechanism diagrams. Use whenever a paper idea should be explained with clear nodes, edges, grouping, captions, and a short meaning box in a muted slide-ready style. Not for tensor/matrix/shape semantics (use supertensor) or plotting numeric data.
---
# superfig
Turn one paper claim or mechanism into one dense, slide-ready figure with three zones:
1. **Top — idea.** The compact claim or formula. Optional; call it after the drawing.
2. **Middle — structure.** Semantic nodes, directed edges, grouped composites, and captions.
3. **Bottom — meaning.** What the idea is, what the objects are, what the mechanism does.
`assets/superfig.sty` enforces the house style and the common cursor/role discipline.
Draw with `\sfnode` / `\sfconn` / `\sfarrow` / `\sfgroup`; do not hand-roll TikZ rectangles
and arrows unless the linter explicitly allows it.
## When to use
- Architecture, block diagram, pipeline, data flow, dependency graph, state or time flow.
- A conceptual mechanism that is not a tensor shape: nodes, arrows, containment, lanes.
- A figure that should look like `supertensor` in palette and text hierarchy.
Do **not** use for tensor faces, axes, contractions, sharding or broadcasting; route those to
`supertensor`. Do not use for loss curves, benchmark bars, scatter plots, or dashboards.
## Workflow
1. **Preflight.** `./scripts/preflight.sh`. Exit 0 = TikZ+CJK path. Exit 1 = degraded
(say so). Exit 2 = no LaTeX; read `references/fallback.md` and name the lost guarantees.
2. **Reduce** the paper to one primary claim. Drop parallel objectives, optional modules,
diagnostics and secondary paths unless the user asks for them.
3. **Build a semantics ledger** before drawing:
- *nodes* — each block is one semantic object: kind, domain/range, owner, lifecycle.
- *edges* — each arrow is data flow, control flow, dependency, or causality; never decoration.
- *groups* — an outline only when it names a real composite object.
See `references/grammar.md`.
4. **Choose the smallest grammar** that exposes the mechanism:
- one object = `\sfnode[role=..., level=...]{name}{label}{width}{height}`
- horizontal flow = `\sfstage` + `\sfrow` … `\sfrowend`
- horizontal connector with label = `\sfconn{name}{label}`
- fixed/branching edge = `\sfarrow[options]{from}{to}` or `\sfarrowlabel`
- composite = `\sfgroup[role=...]{name}{(member1)(member2)}{}` then `\sfcaption`
- a whole-band aside = `\sfcallout`
- bottom explanation = `\sfmeaningbox`
Full macro list: `references/api.md`. Worked figures: `examples/`.
5. **Build and audit.** `./scripts/build.sh fig.tex` runs lint, TeX checks, and exports.
Then run `references/checklist.md` against the PNG full-size and thumbnail. Redraw on
any mandatory violation.
## Non-negotiables
- **Semantics** — one node, one object. An edge says one relationship. A group is a real
composite, not a decorative border.
- **Layout** — use the cursor in flow rows; use `at={<coord>}` only when the topology is
genuinely non-linear. Never hand-tune a neighbor against a magic offset.
- **Style** — muted palette, one hue per role, at most four active hues plus gray,
three separated lightness levels, no saturated primaries, no dashboard clutter.
- **Build** — `Missing character`, overfull/underfull boxes, and `Package superfig Warning`
are build failures, not cosmetic warnings.
`references/antipatterns.md` shows the common ways a clean compile still tells the reader
something false.
## Output
Default to editable TikZ. `scripts/build.sh` emits PDF, SVG, white-background PNG,
transparent PNG and a 360 px thumbnail. Deliver the PNG preview, a one-paragraph mechanism
explanation, and the `.tex` source plus vector artifact.
- **Chinese figures:** `\usepackage[cjk]{superfig}` (XeLaTeX + portable Fandol). Keep
standard English terms where natural (`softmax`, `logits`, `gather`).
- **English figures:** `\usepackage[en]{superfig}` — same geometry, English meaning-box rails.
- Keep math in LaTeX, not raw Unicode.
- Add `\sfsignature{<subject>}{<box>}` only when the user or house template asks for it.
## Iterating
When the user asks for a change, do not restart the figure. Edit the role/ledger or the one
macro call that owns the offending object, rebuild, and re-audit. If a fix requires shrinking
type, closing the gutter, or covering another object, move the stage to another row instead.
Ask before dropping an object or edge the user named.
+4
View File
@@ -0,0 +1,4 @@
interface:
display_name: "Superfig"
short_description: "Create clean paper-style architecture and flow figures"
default_prompt: "Use $superfig to turn this paper idea into an editable figure with nodes, edges and a meaning box."
+385
View File
@@ -0,0 +1,385 @@
%% superfig.sty -- general paper-figure toolkit
%% A smaller sibling of supertensor.sty. It keeps the house style, role/color
%% discipline, cursor layout, build-time warning discipline and audit mindset,
%% but its primitives are generic nodes and edges, not tensor faces.
%%
%% Options: cjk load ctex with the portable fandol fontset (XeLaTeX)
%% en English rail labels in the meaning box (default: zh)
\NeedsTeXFormat{LaTeX2e}
\ProvidesPackage{superfig}[2026/08/16 v1.0 general paper figure toolkit]
\newif\iffig@cjk\fig@cjkfalse
\newif\iffig@en\fig@enfalse
\DeclareOption{cjk}{\fig@cjktrue}
\DeclareOption{en}{\fig@entrue}
\DeclareOption{zh}{\fig@enfalse}
\ProcessOptions\relax
\RequirePackage{amsmath}
\RequirePackage{amssymb}
\RequirePackage{xcolor}
\RequirePackage{etoolbox}
\RequirePackage{array}
\RequirePackage{tikz}
\usetikzlibrary{calc,positioning,arrows.meta,backgrounds,fit,decorations.pathreplacing}
\iffig@cjk
\RequirePackage[UTF8,fontset=fandol]{ctex}
\fi
% ---------------------------------------------------------------- layers ----
\pgfdeclarelayer{sfbg}
\pgfdeclarelayer{sffg}
\pgfsetlayers{sfbg,main,sffg}
% ---------------------------------------------------------------- palette ---
\definecolor{sfTeal}{HTML}{4F8FA5}
\definecolor{sfOrange}{HTML}{EE995B}
\definecolor{sfCoral}{HTML}{C95B5B}
\definecolor{sfViolet}{HTML}{8A74B5}
\definecolor{sfGray}{HTML}{85898F}
\definecolor{sfInk}{HTML}{1A1A1A}
% Role registry: drawing macros take a ROLE, never a color.
\newcommand{\sfsetrole}[2]{%
\edef\sf@newrole{#2}%
\ifcsname sf@role@#1\endcsname
\edef\sf@oldrole{\csname sf@role@#1\endcsname}%
\ifx\sf@oldrole\sf@newrole\else
\PackageWarning{superfig}{Role `#1' was already mapped to
`\sf@oldrole' and cannot be remapped to `\sf@newrole'}%
\fi
\else
\expandafter\gdef\csname sf@role@#1\endcsname{#2}%
\fi}
\newcommand{\sfrole}[1]{%
\ifcsname sf@role@#1\endcsname\csname sf@role@#1\endcsname\else sfGray\fi}
\newcommand{\sfcheckrole}[1]{%
\ifcsname sf@role@#1\endcsname\else
\PackageWarning{superfig}{Undeclared role `#1' -- drawn in neutral gray.
Declare it with \string\sfsetrole\space so the hue budget stays visible}%
\fi}
\sfsetrole{neutral}{sfGray}
\newcommand{\sflevelpct}[1]{\ifcase#1 0\or30\or55\or80\else55\fi}
% --------------------------------------------------------- type hierarchy ---
\tikzset{
sf sym/.style = {font=\small, text=sfInk, inner sep=1pt},
sf shape/.style = {font=\scriptsize, text=black!55, inner sep=1pt},
sf stage/.style = {font=\small\bfseries, text=black!55, inner sep=2pt},
sf op/.style = {font=\Large, text=sfInk, inner sep=2pt},
sf note/.style = {font=\scriptsize, text=black!55, inner sep=2pt},
sf arrow/.style = {-{Stealth[length=2.2mm,width=1.6mm]}, draw=black!45, line width=0.5pt},
sf box/.style = {draw=black!60, line width=0.5pt, rounded corners=1.5pt,
inner sep=5pt, font=\small, align=center, text=sfInk},
}
% --------------------------------------------------------- flow layout ------
\newlength{\sfgutter}\setlength{\sfgutter}{6mm}
\newlength{\sfrowgap}\setlength{\sfrowgap}{3.5mm}
\newlength{\sfblockgap}\setlength{\sfblockgap}{9mm}
\newlength{\sflinklen}\setlength{\sflinklen}{10mm}
\newlength{\sf@railx}\newlength{\sf@cx}\newlength{\sf@cy}
\newlength{\sf@ycur}\newlength{\sf@bandh}\newlength{\sf@tmpx}\newlength{\sf@tmpy}
\newif\iffig@inrow
\newif\iffig@first
\newif\iffig@bracket
\newcount\sf@nmem
\newcount\sf@ncallouts
\newcommand{\sflayoutreset}{%
\global\sf@railx=0pt \global\sf@ycur=0pt
\global\sf@cx=0pt \global\sf@cy=0pt \global\sf@bandh=0pt
\global\fig@inrowfalse
\global\sf@ncallouts=0
\gdef\sf@rowlist{}\gdef\sf@alllist{}\gdef\sf@rowname{}%
\gdef\sf@lastnode{}\gdef\sf@linklabel{}\gdef\sf@linkprev{}}
\sflayoutreset
\newcommand{\sfleftrail}[1]{\global\sf@railx=\dimexpr#1\relax}
\newcommand{\sfvgap}[1]{\global\advance\sf@ycur by -\dimexpr#1\relax}
\newcommand{\sf@lower}[1]{%
\pgfextracty{\sf@tmpy}{\pgfpointanchor{#1}{south}}%
\ifdim\sf@tmpy<\sf@ycur \global\sf@ycur=\sf@tmpy \fi}
\newcommand{\sf@regall}[1]{\xdef\sf@alllist{\sf@alllist(#1)}}
\newcommand{\sf@regrow}[1]{%
\xdef\sf@rowlist{\sf@rowlist(#1)}\sf@regall{#1}%
\sf@drawpendinglink{#1}%
\gdef\sf@lastnode{#1}}
\newcommand{\sf@needrow}[1]{%
\iffig@inrow\else
\PackageError{superfig}{\string#1\space needs an open \string\sfrow}%
{Flow placement only works between \string\sfrow\space and
\string\sfrowend. Pass at={<coord>} instead.}%
\fi}
\newcommand{\sf@leadgap}[1]{%
\iffig@first
\global\fig@firstfalse
\else
\global\advance\sf@cx by \dimexpr#1\relax
\fi}
\newcommand{\sf@flow}[2]{%
\dimen0=\dimexpr#1\relax
\dimen2=\sf@cx \advance\dimen2 by 0.5\dimen0
\edef\sf@pos{(\the\dimen2,\the\sf@cy)}%
\global\advance\sf@cx by \dimen0}
\newcommand{\sf@checkh}[2]{%
\dimen0=\dimexpr#2\relax \advance\dimen0 by -\sf@bandh
\ifdim\dimen0>4mm
\PackageWarning{superfig}{Object `#1' overflows its band by
\the\dimen0. Raise the height declared in \string\sfrow\space or give it
its own band}%
\fi}
\newcommand{\sfstage}[2]{%
\dimen0=\sf@ycur \advance\dimen0 by -\sfblockgap
\node[sf stage, anchor=north west] (#1) at (\the\sf@railx,\the\dimen0) {#2};
\sf@regall{#1}\sf@lower{#1}}
\newcommand{\sfrow}[2]{%
\gdef\sf@rowlist{}\gdef\sf@rowname{#1}\gdef\sf@lastnode{}%
\gdef\sf@linklabel{}\gdef\sf@linkprev{}%
\expandafter\gdef\csname sf@isrow@#1\endcsname{1}%
\global\fig@inrowtrue\global\fig@firsttrue
\pgfmathsetlengthmacro{\sf@bh}{#2}%
\global\sf@bandh=\sf@bh
\global\sf@cx=\sf@railx
\dimen0=\sf@ycur \advance\dimen0 by -\sfrowgap \advance\dimen0 by -0.5\sf@bandh
\global\sf@cy=\dimen0}
\newcommand{\sfrowend}{%
\ifdefempty{\sf@rowlist}%
{\PackageWarning{superfig}{Empty \string\sfrow\space `\sf@rowname'}}%
{\edef\sf@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
fit={\sf@rowlist}] (\sf@rowname) {};}\sf@do
\sf@regall{\sf@rowname}\sf@lower{\sf@rowname}}%
\global\fig@inrowfalse}
% --------------------------------------------------------------- nodes ------
\pgfkeys{
/sf/node/.cd,
role/.store in=\sf@role,
level/.store in=\sf@level,
gap/.store in=\sf@gap,
bracket/.is if=fig@bracket,
at/.store in=\sf@at,
role=neutral, level=2, gap=\sfgutter, bracket=false, at={},
}
% \sfnode[keys]{name}{label}{width}{height}
% A semantic object: one node, one fill, one role. In an open \sfrow an empty
% at places it at the cursor; at={<coord>} bypasses the cursor for branchy
% architecture figures. Width/height are real lengths or unit expressions.
\newcommand{\sfnode}[5][]{%
\begingroup
\pgfkeys{/sf/node/.cd,#1}%
\pgfmathsetlengthmacro{\sf@w}{#4}%
\pgfmathsetlengthmacro{\sf@h}{#5}%
\sfcheckrole{\sf@role}%
\edef\sf@col{\sfrole{\sf@role}}%
\edef\sf@fill{\sf@col!\sflevelpct{\sf@level}}%
\ifdefempty{\sf@at}{%
\sf@needrow{\sfnode}%
\sf@checkh{#2}{#5}%
\sf@leadgap{\sf@gap}%
\iffig@bracket\pgfmathsetlengthmacro{\sf@tw}{\sf@w+6.8mm}\else\let\sf@tw\sf@w\fi
\sf@flow{\sf@tw}{\sf@h}%
}{%
\edef\sf@pos{\sf@at}%
}%
\expandafter\node[sf box, fill=\sf@fill, minimum width=\sf@w,
minimum height=\sf@h, at={\sf@pos}] (#2) {#3};%
\iffig@bracket
\draw[black!55, line width=0.5pt]
($(#2.north west)+(-1.1mm,0.6mm)$) -- ++(-1.1mm,0)
-- ($(#2.south west)+(-2.2mm,-0.6mm)$) -- ++(1.1mm,0);
\draw[black!55, line width=0.5pt]
($(#2.north east)+(1.1mm,0.6mm)$) -- ++(1.1mm,0)
-- ($(#2.south east)+(2.2mm,-0.6mm)$) -- ++(-1.1mm,0);
\fi
\ifdefempty{\sf@at}{%
\sf@regrow{#2}%
}{%
\sf@regall{#2}\sf@lower{#2}%
}%
\endgroup}
% Operator or small annotation in a flow row.
\newcommand{\sfop}[2]{%
\iffig@inrow\else
\PackageError{superfig}{\string\sfop\space needs an open \string\sfrow}%
{Operators are flow objects. Use \string\sfarrow\space for a fixed edge.}%
\fi
\sf@leadgap{\sfgutter}%
\node[sf op, anchor=west] (#1) at (\the\sf@cx,\the\sf@cy) {#2};%
\pgfextractx{\sf@tmpx}{\pgfpointanchor{#1}{east}}%
\global\sf@cx=\sf@tmpx
\sf@regrow{#1}}
\newcommand{\sfgap}[1]{\global\advance\sf@cx by \dimexpr#1\relax}
% ------------------------------------------------------------ connectors ----
\newcommand{\sf@drawpendinglink}[1]{%
\ifdefempty{\sf@linkprev}{}{%
\begin{pgfonlayer}{sfbg}
\ifdefempty{\sf@linklabel}%
{\draw[sf arrow] (\sf@linkprev.east) -- (#1.west);}%
{\draw[sf arrow] (\sf@linkprev.east) -- (\sf@linklabel.west);
\draw[sf arrow] (\sf@linklabel.east) -- (#1.west);}%
\end{pgfonlayer}
\gdef\sf@linkprev{}\gdef\sf@linklabel{}}}
% \sfconn{name}{label} -- horizontal edge through the cursor. The label reserves
% its own width, so it cannot be wider than the edge it labels.
\newcommand{\sfconn}[2]{%
\sf@needrow{\sfconn}%
\xdef\sf@linkprev{\sf@lastnode}%
\ifblank{#2}%
{\gdef\sf@linklabel{}\sfgap{\sflinklen}}%
{\sf@leadgap{\sfgutter}%
\node[sf note, anchor=west, inner sep=1pt] (#1) at (\the\sf@cx,\the\sf@cy) {#2};
\pgfextractx{\sf@tmpx}{\pgfpointanchor{#1}{east}}%
\global\sf@cx=\sf@tmpx
\gdef\sf@linklabel{#1}\sf@regall{#1}}}
% Fixed directed edge between two already-placed objects.
\newcommand{\sfarrow}[3][]{%
\begin{pgfonlayer}{sfbg}
\draw[sf arrow] (#2) to[#1] (#3);
\end{pgfonlayer}}
\newcommand{\sfarrowlabel}[4][]{%
\begin{pgfonlayer}{sfbg}
\draw[sf arrow] (#2) to[#1] node[sf note, auto, fill=white, inner sep=1pt] {#4} (#3);
\end{pgfonlayer}}
% ----------------------------------------------------------- bounding box ---
\newcommand{\sfbbox}[1]{%
\edef\sf@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
fit={\sf@alllist}] (#1) {};}\sf@do}
\newcommand{\sftrack}[1]{\sf@regall{#1}\sf@lower{#1}}
\newcommand{\sftopformula}[2]{%
\sfbbox{sf@bbt}%
\node[above=6mm of sf@bbt, anchor=south] (#1) {#2};
\sf@regall{#1}}
% -------------------------------------------------------------- captions ----
\def\sf@lane{}
\newcommand{\sflane}[1]{\def\sf@lane{#1}}
\newcommand{\sfnolane}{\def\sf@lane{}}
\newcommand{\sfcaption}[3]{%
\ifdefempty{\sf@lane}%
{\node[sf sym, below=1.6mm of #1] (#1-sym) {#2};}%
{\node[sf sym, anchor=north]
at ($(#1.center |- \sf@lane.south)+(0,-1.6mm)$) (#1-sym) {#2};}%
\node[sf shape, below=0.6mm of #1-sym] (#1-shape) {#3};%
\sf@regall{#1-shape}\sf@lower{#1-shape}}
\newcommand{\sfcaptiontop}[2]{%
\node[sf sym, above=1.6mm of #1] (#1-top) {#2};%
\sf@regall{#1-top}}
% --------------------------------------------------------------- grouping ---
\pgfkeys{
/sf/group/.cd,
role/.store in=\sf@role,
role=neutral,
}
% Count an explicit fit list like {(a)(b)}. A trailing empty () is the sentinel.
\def\sf@countmembers#1{\sf@nmem=0 \sf@eatmem#1()\relax}
\def\sf@eatmem(#1)#2\relax{%
\if\relax\detokenize{#1}\relax
\else
\advance\sf@nmem by 1
\sf@eatmem#2\relax
\fi}
% \sfgroup[role=...]{name}{(member1)(member2)}{caption}
% The fit list is explicit so the group can wrap any placed nodes. The outline
% names one composite object, never decoration. An overlay caption is optional;
% prefer \sfcaption{name}{...}{...} so the label sits on the caption lane.
\newcommand{\sfgroup}[4][]{%
\begingroup
\pgfkeys{/sf/group/.cd,#1}%
\sfcheckrole{\sf@role}%
\sf@countmembers{#3}%
\ifnum\sf@nmem<2
\PackageWarning{superfig}{Group `#2' wraps a single object.
Bind at least two members; an outline around one node is decoration}%
\fi
\edef\sf@col{\sfrole{\sf@role}}%
\node[draw=\sf@col!65, line width=0.6pt, rounded corners=3pt,
inner sep=6pt, fit={#3}] (#2) {};%
\ifblank{#4}{}{%
\node[anchor=north west, font=\scriptsize\bfseries,
text=\sf@col!85!black, inner sep=1pt]
at ([yshift=2pt]#2.north west) {#4};}%
\sf@regall{#2}\sf@lower{#2}%
% Expand the last row's fit so \sflane{row} hangs captions below the outline.
\ifdefempty{\sf@rowname}{}{%
\edef\sf@do{\noexpand\node[inner sep=0pt, outer sep=0pt,
fit={(\sf@rowname)(#2)}] (\sf@rowname) {};}\sf@do
\sf@lower{\sf@rowname}}%
\endgroup}
% --------------------------------------------------------------- callout ---
\newcommand{\sfcallout}[5]{%
\iffig@inrow
\PackageError{superfig}{\string\sfcallout\space inside an open
\string\sfrow}%
{A callout is an aside, not an object. Close the band first.}%
\fi
\ifcsname sf@isrow@#3\endcsname\else
\PackageWarning{superfig}{Callout `#1' is anchored to `#3', which is not a
\string\sfrow\space band. A card hanging off a single object reads as a step}%
\fi
\global\advance\sf@ncallouts by 1
\ifnum\sf@ncallouts>1
\PackageWarning{superfig}{Callout `#1' is the second card in the figure.
The budget is one callout per figure}%
\fi
\node[anchor=north west, draw=black!18, fill=black!3, rounded corners=1.5pt,
inner xsep=7pt, inner ysep=6pt, text width=#2] (#1)
at ([xshift=\sfgutter]#3.north east) {%
\scriptsize\raggedright
\ifblank{#4}{}{\textbf{#4}\par\vspace{1.5pt}}%
#5\par};
\sftrack{#1}}
% ------------------------------------------------------------ meaning box ---
\iffig@en
\def\sf@lblaxes{Concept}\def\sf@lblobj{Objects}\def\sf@lblmech{Mechanism}
\else
\def\sf@lblaxes{概念}\def\sf@lblobj{对象}\def\sf@lblmech{机制}
\fi
\newcommand{\sfsetlabels}[3]{\def\sf@lblaxes{#1}\def\sf@lblobj{#2}\def\sf@lblmech{#3}}
\newlength{\sf@raillen}
\iffig@en
\setlength{\sf@raillen}{5.4em}
\else
\setlength{\sf@raillen}{3.2em}
\fi
\newcommand{\sfsetrail}[1]{\sf@raillen=\dimexpr#1\relax}
% \sfmeaningbox{name}{total width}{anchor}{idea}{objects}{mechanism}
% One low-contrast reading box. Pass {} to drop a row.
\newcommand{\sfmeaningbox}[6]{%
\node[below=4mm of #3, anchor=north,
draw=black!18, fill=black!3, rounded corners=1.5pt,
inner xsep=9pt, inner ysep=8pt, text width=#2] (#1) {%
\small
\setlength{\tabcolsep}{0pt}%
\renewcommand{\arraystretch}{1.25}%
\begin{tabular}{@{}p{\sf@raillen}@{\hspace{7pt}}p{\dimexpr#2-\sf@raillen-7pt\relax}@{}}
\ifblank{#4}{}{\textbf{\sf@lblaxes} & \raggedright\arraybackslash #4 \\}
\ifblank{#5}{}{\textbf{\sf@lblobj} & \raggedright\arraybackslash #5 \\}
\ifblank{#6}{}{\textbf{\sf@lblmech} & \raggedright\arraybackslash #6 \\}
\end{tabular}};}
% ------------------------------------------------------------- signature ----
\newcommand{\sfsignature}[2]{%
\node[below=2.2mm of #2, font=\scriptsize, text=black!45] (#2-sig) {#1};}
\endinput
+82
View File
@@ -0,0 +1,82 @@
% Anti-pattern gallery -- four figures that compile cleanly and still teach
% the reader something false. Each pair is wrong / right.
% ../scripts/build.sh antipatterns.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{a}{sfTeal}
\sfsetrole{b}{sfOrange}
\sfsetrole{c}{sfCoral}
\sfsetrole{d}{sfViolet}
\newcommand{\bad}[1]{{\color{sfCoral}$\times$}\;#1}
\newcommand{\good}[1]{{\color{sfTeal}$\checkmark$}\;#1}
\begin{document}
\begin{tikzpicture}
% (1) A decorative arrow vs a data-flow edge.
\sfnode[role=a, at={(0,0)}]{A1}{编码器}{16mm}{10mm}
\sfnode[role=b, at={(32mm,0)}]{A2}{指标卡}{16mm}{10mm}
\sfarrow{A1}{A2}
\sfnode[role=a, at={(64mm,0)}]{A3}{编码器}{16mm}{10mm}
\sfnode[role=b, at={(96mm,0)}]{A4}{解码器}{16mm}{10mm}
\sfarrowlabel{A3.east}{A4.west}{隐状态}
\node[sf stage, anchor=south west] at ($(A1.north west)+(0,6mm)$)
{(1) 装饰箭头};
\node[sf shape, anchor=north] (A1c) at ($(A1.south)!0.5!(A2.south)+(0,-3mm)$)
{\bad{箭头不承载关系}};
\node[sf shape, anchor=north] (A3c) at ($(A3.south)!0.5!(A4.south)+(0,-3mm)$)
{\good{隐状态从编码流向解码}};
% (2) A group that is only a visual border vs a real composite.
\sfnode[role=a, at={(0,-32mm)}]{B1}{输入}{14mm}{10mm}
\sfnode[role=d, at={(24mm,-32mm)}]{B2}{输出}{14mm}{10mm}
\sfgroup[role=a]{Bg}{(B1)(B2)}{}
\sfnode[role=b, at={(56mm,-32mm)}]{B3}{注意力}{16mm}{10mm}
\sfnode[role=b, at={(82mm,-32mm)}]{B4}{前馈}{14mm}{10mm}
\sfgroup[role=b]{Bb}{(B3)(B4)}{}
\sfarrow{B3.east}{B4.west}
\node[sf stage, anchor=south west] at ($(B1.north west)+(0,8mm)$)
{(2) 组框只是装饰边};
\node[sf shape, anchor=north] (B1c) at ($(B1.south)!0.5!(B2.south)+(0,-7mm)$)
{\bad{首尾框在一起,不是一个模块}};
\node[sf shape, anchor=north] (B3c) at ($(B3.south)!0.5!(B4.south)+(0,-7mm)$)
{\good{主路模块是真实复合对象}};
% (3) One node doing two jobs vs a split.
\sfnode[role=b, at={(0,-68mm)}]{C1}{注意力 + LN + 残差}{38mm}{12mm}
\sfnode[role=b, at={(56mm,-68mm)}]{C2}{注意力}{16mm}{10mm}
\sfnode[role=c, at={(84mm,-68mm)}]{C3}{残差加}{16mm}{10mm}
\sfarrow{C2.east}{C3.west}
\node[sf stage, anchor=south west] at ($(C1.north west)+(0,6mm)$)
{(3) 一个节点做两件事};
\node[sf shape, anchor=north] (C1c) at ($(C1.south)+(0,-3mm)$)
{\bad{机制被一口吞掉}};
\node[sf shape, anchor=north] (C2c) at ($(C2.south)!0.5!(C3.south)+(0,-3mm)$)
{\good{一步一个对象}};
% (4) A new hue for the same object vs one role, two levels.
\sfnode[role=a, level=2, at={(0,-100mm)}]{D1}{$x$}{14mm}{10mm}
\sfnode[role=c, level=2, at={(24mm,-100mm)}]{D2}{$x$ 归一化}{20mm}{10mm}
\sfnode[role=a, level=1, at={(64mm,-100mm)}]{D3}{$x$}{14mm}{10mm}
\sfnode[role=a, level=3, at={(90mm,-100mm)}]{D4}{$x$ 归一化}{20mm}{10mm}
\node[sf stage, anchor=south west] at ($(D1.north west)+(0,6mm)$)
{(4) 同一对象换了色相};
\node[sf shape, anchor=north] (D1c) at ($(D1.south)!0.5!(D2.south)+(0,-3mm)$)
{\bad{换色相像换了对象}};
\node[sf shape, anchor=north] (D3c) at ($(D3.south)!0.5!(D4.south)+(0,-3mm)$)
{\good{同一角色,深浅分层}};
\node[inner sep=0pt, fit=(A1)(A4)(D1)(D4)(A1c)(D3c)(Bg)(Bb)] (all) {};
\sfmeaningbox{mb}{118mm}{all}
{}
{四张错图都能干净编译。编译器不检查图讲的事情对不对}
{交付前按 \texttt{references/checklist.md} 做一次人眼审图}
\end{tikzpicture}
\end{document}
+48
View File
@@ -0,0 +1,48 @@
% superfig golden example 2 -- a small branchy architecture, not a tensor
% shape diagram. Main path uses the cursor; the residual is a skip edge;
% the group is captioned on the shared lane, not as an overlay.
% ../scripts/build.sh branch-architecture.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{enc}{sfTeal}
\sfsetrole{attn}{sfOrange}
\sfsetrole{ffn}{sfViolet}
\sfsetrole{out}{sfCoral}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{双分支结构:主路 + 残差}
\sfvgap{7mm}
\sfrow{R1}{12mm}
\sfnode[role=enc]{in}{输入}{16mm}{10mm}
\sfconn{e1}{}
\sfnode[role=attn]{attn}{注意力}{18mm}{10mm}
\sfconn{e2}{特征}
\sfnode[role=ffn]{ffn}{前馈}{18mm}{10mm}
\sfgap{\sflinklen}
\sfnode[role=enc]{add}{融合}{14mm}{10mm}
\sfconn{e4}{}
\sfnode[role=out]{out}{输出}{14mm}{10mm}
\sfrowend
% No overlay caption: it would sit on the residual. Caption the group below.
\sfgroup[role=attn]{block}{(attn)(ffn)}{}
\sfarrow{block.east}{add.west}
\sfarrowlabel[bend left=18]{in.north}{add.north}{残差}
\sflane{R1}
\sfcaption{in}{输入}{源数据}
\sfcaption{block}{主路模块}{注意力 + 前馈}
\sfcaption{add}{融合}{残差相加}
\sfcaption{out}{输出}{目标表示}
\sfbbox{all}
\sfmeaningbox{mb}{108mm}{all}
{主路逐层处理,残差边跳过主路}
{输入、主路模块、融合节点、输出}
{残差把输入直接送入融合,减轻深层优化负担}
\end{tikzpicture}
\end{document}
+56
View File
@@ -0,0 +1,56 @@
% superfig golden example 4 -- a dual-encoder dependency, not a linear pipeline.
% Two encoder rows share a rail; fusion is placed from their east anchors.
% ../scripts/build.sh dependency-graph.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{text}{sfTeal}
\sfsetrole{img}{sfOrange}
\sfsetrole{fuse}{sfViolet}
\sfsetrole{out}{sfCoral}
\begin{document}
\begin{tikzpicture}
\sfstage{S1}{双塔编码}
\sfrow{R1}{12mm}
\sfnode[role=text]{txt}{文本}{14mm}{10mm}
\sfconn{e1}{}
\sfnode[role=text]{te}{文本编码}{18mm}{10mm}
\sfrowend
\sflane{R1}
\sfcaption{txt}{text}{token 序列}
\sfcaption{te}{$E_t$}{文本向量}
\sfrow{R2}{12mm}
\sfnode[role=img]{img}{图像}{14mm}{10mm}
\sfconn{e2}{}
\sfnode[role=img]{ve}{视觉编码}{18mm}{10mm}
\sfrowend
\sflane{R2}
\sfcaption{img}{image}{视觉输入}
\sfcaption{ve}{$E_v$}{视觉向量}
% Fusion sits to the right of the two encoders, on their shared east line.
\sfnode[role=fuse, at={($(te.east)!0.5!(ve.east)+(20mm,0)$)}]{fu}{融合}{16mm}{10mm}
\sfnode[role=out, at={($(fu.east)+(16mm,0)$)}]{dec}{解码}{16mm}{10mm}
\sfnode[role=out, at={($(dec.east)+(16mm,0)$)}]{out}{输出}{14mm}{10mm}
\sfarrow{te.east}{fu.west}
\sfarrow{ve.east}{fu.west}
\sfarrow{fu.east}{dec.west}
\sfarrow{dec.east}{out.west}
\sfnolane
\sfcaption{fu}{fuse}{晚融合}
\sfcaption{dec}{decode}{条件生成}
\sfcaption{out}{out}{目标文本}
\sfbbox{all}
\sftopformula{F}{%
$y = \mathrm{Dec}\bigl(\mathrm{Fuse}(E_t(x),\, E_v(I))\bigr)$}
\sfmeaningbox{mb}{112mm}{all}
{两个编码器互不共享权重,只在融合节点会合}
{文本塔、视觉塔、融合、解码}
{各塔独立编码;融合后才进入解码,所以任一侧的依赖都经过融合节点}
\end{tikzpicture}
\end{document}
+47
View File
@@ -0,0 +1,47 @@
% superfig golden example 1 -- one horizontal paper-figure pipeline.
% Main path is input -> model -> prediction. Loss is a side object, not a
% station on the forward path.
% ../scripts/build.sh pipeline.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{input}{sfTeal}
\sfsetrole{model}{sfOrange}
\sfsetrole{loss}{sfCoral}
\sfsetrole{output}{sfViolet}
\begin{document}
\begin{tikzpicture}
\sfstage{SA}{推理流程:一次前向}
\sfrow{R1}{16mm}
\sfnode[role=input]{x}{输入 $x$}{16mm}{12mm}
\sfconn{e1}{预处理}
\sfnode[role=model]{f}{模型 $f_\theta$}{18mm}{12mm}
\sfconn{e2}{logits}
\sfnode[role=output]{y}{预测 $\hat y$}{16mm}{12mm}
\sfrowend
% Loss compares the prediction with the target; it is not on the main path.
% Hang it below ŷ with enough shaft that no caption sits on the arrow.
\sfnode[role=loss, at={($(y.south)+(0,-22mm)$)}]{s}{损失 $L$}{14mm}{12mm}
\sfarrowlabel{y.south}{s.north}{$L(\hat y,y)$}
\sflane{R1}
\sfcaption{x}{$x$}{原始输入}
\sfcaption{f}{$f_\theta$}{可学习参数}
\sfnolane
\sfcaption{s}{$L$}{与真值比较}
\sfbbox{all}
\sftopformula{F}{%
$x \;\xrightarrow{\;f_\theta\;}\; \hat y,\qquad
\min_\theta\; L\bigl(f_\theta(x),\,y\bigr)$}
\sfmeaningbox{mb}{96mm}{all}
{一次从输入到预测的前向;损失在预测之后单独计算}
{数据、模型参数、预测、损失}
{预处理后送入模型;模型产生 logits 得到预测;损失比较预测与真值,梯度再回到参数}
\sfsignature{推理流程示意}{mb}
\end{tikzpicture}
\end{document}
+46
View File
@@ -0,0 +1,46 @@
% superfig golden example 3 -- a state/time flow.
% One decode step: previous KV plus a new token produce the next state
% and the emitted token. The callout is an aside on the finished state band.
% ../scripts/build.sh state-flow.tex
\documentclass[border=10pt]{standalone}
\usepackage[cjk]{superfig}
\sfsetrole{state}{sfTeal}
\sfsetrole{step}{sfOrange}
\sfsetrole{tok}{sfViolet}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{解码一步:旧状态 + 新 token}
\sfvgap{26mm}
\sfrow{R1}{14mm}
\sfnode[role=state]{kv}{KV$_t$}{16mm}{11mm}
\sfconn{c1}{读}
\sfnode[role=step]{dec}{解码}{16mm}{11mm}
\sfconn{c2}{追加}
\sfnode[role=state]{kv2}{KV$_{t+1}$}{18mm}{11mm}
\sfrowend
\sflane{R1}
\sfcaption{kv}{KV$_t$}{已缓存键值}
\sfcaption{dec}{一步}{读 KV,写新列}
\sfcaption{kv2}{KV$_{t+1}$}{状态推进}
\sfcallout{N1}{38mm}{R1}{为何保留 KV}{%
下一步只读已有列、只追加 $x_t$ 这一列。重算前缀是另一条路径,这张图不画。}
% Tokens enter from above so the caption lane below the state row stays clear.
\sfnode[role=tok, at={($(dec.north west)+(-4mm,14mm)$)}]{xt}{$x_t$}{14mm}{10mm}
\sfnode[role=tok, at={($(dec.north east)+(4mm,14mm)$)}]{yt}{$y_t$}{14mm}{10mm}
\sfarrow{xt.south}{dec.north}
\sfarrow{dec.north}{yt.south}
\sfbbox{all}
\sftopformula{F}{%
$(\mathrm{KV}_{t+1},\, y_t) = \mathrm{Decode}(\mathrm{KV}_t,\, x_t)$}
\sfmeaningbox{mb}{108mm}{all}
{自回归一步:状态沿时间推进}
{缓存 KV、解码算子、本步 token}
{解码读 $\mathrm{KV}_t$ 与 $x_t$,写出 $y_t$,并把新列追加为 $\mathrm{KV}_{t+1}$}
\end{tikzpicture}
\end{document}
+21
View File
@@ -0,0 +1,21 @@
# Anti-patterns
Each of these can compile cleanly and still teach the reader something false.
Rendered pairs: `examples/antipatterns.tex`.
- **A decorative arrow.** The arrow has no data/control/dependency meaning. Remove it or
make the relationship explicit.
- **A group that is only a visual border.** The outline does not correspond to a real
module/composite in the paper.
- **One node doing two jobs.** "attention + layer norm + residual" in one block hides the
mechanism the figure was meant to explain.
- **A new hue for a slightly different view of the same object.** Reuse the role or use
lightness, not a fresh color family.
- **A caption repeated in the meaning box.** Captions name the objects; the meaning box
explains what they do and why.
- **Solving crowding by shrinking type.** The type hierarchy is a hard floor. Move the
stage to another row or split the figure.
- **Magic offsets.** Every hand-tuned coordinate is valid only for the current label. The
next edit will land a longer label on a neighbor.
- **A floating commentary card between objects.** If it is not an object or edge, it belongs
in the stage subtitle, meaning box, or one `\sfcallout` beside a finished band.
+122
View File
@@ -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.
+40
View File
@@ -0,0 +1,40 @@
# Delivery checklist
A clean build only proves TeX and package invariants. Inspect the figure before delivery.
## Semantics
- [ ] Every node is one object with one role.
- [ ] Every edge is data flow, control flow, dependency, or causality.
- [ ] Every group names a real composite, binds at least two members, and all members belong to it.
- [ ] At most one `\sfcallout`, hanging off a finished band, not off a single node.
- [ ] Captions explain objects or relationships, not decoration.
## Layout
- [ ] No node covers another node, caption, edge label, or group border.
- [ ] Flow rows start on the same left rail.
- [ ] Edge labels sit on their own edge and do not cover unrelated objects.
- [ ] Fixed coordinates are on a coarse grid, not tuned against one label width.
- [ ] Captions in a row share one baseline.
- [ ] Meaning box is one column, at most three rows, no overflow.
## Style
- [ ] One role keeps one hue across the whole figure.
- [ ] At most four active hue families plus gray.
- [ ] Three visibly separated lightness levels where objects need hierarchy; no uniform pastel.
- [ ] Borders are neutral; group outlines carry their members' role hue.
- [ ] No saturated primaries, shadows, banners, or decorative cards.
## Thumbnail
- [ ] Open `*-thumb.png` (360 px). The main claim is still legible.
- [ ] Hues remain distinct and muted; text has not collapsed into gray mush.
## Delivery
- [ ] PNG preview shown.
- [ ] One-paragraph mechanism explanation in prose.
- [ ] `.tex` source and PDF/SVG linked.
- [ ] Any degraded path or inferred relationship stated explicitly.
+17
View File
@@ -0,0 +1,17 @@
# Fallback without LaTeX
Use this path only when `scripts/preflight.sh` exits 2. State explicitly that the
`superfig.sty` guarantees — role registry, cursor layout, package warnings, and the build
pipeline — are unavailable.
## Preserve manually
1. Build the semantics ledger before drawing.
2. One hue per role; at most four hues plus gray; three lightness levels.
3. Every node is one object; every edge has one relationship; groups are real composites.
4. Derive placement from previous bounding boxes plus one gutter constant, not per-label
tuning.
5. Export SVG and PNG, then inspect full-size and at 360 px using `checklist.md`.
Prefer SVG for editability. Do not imitate package compliance in the delivery: name the
fallback renderer and list any inferred relationship or reduced guarantee.
+48
View File
@@ -0,0 +1,48 @@
# Visual grammar
Choose the smallest grammar that exposes the paper's mechanism. Adding a second grammar
must add information.
| Fact to expose | Grammar |
|---|---|
| one semantic object | `\sfnode` |
| linear sequence inside one band | `\sfstage` + `\sfrow` … `\sfrowend` |
| an edge between two objects | `\sfconn` for flow, `\sfarrow` for fixed topology |
| an edge label | `\sfconn{name}{label}` or `\sfarrowlabel` |
| a real composite (module, subsystem, shared owner) | `\sfgroup` |
| an operator inside a flow row | `\sfop` |
| a side note about a finished band | `\sfcallout` |
| the bottom explanation | `\sfmeaningbox` |
## Node semantics
One block is one semantic object. If you need two verbs in one block, split it.
- Label is the object's name or role, not a sentence.
- Fill color encodes a role, not importance.
- Width/height may differ to reflect a visual hierarchy, but do not use size to invent
quantitative meaning unless the figure says so.
## Edge semantics
Every arrow must be one of:
- **data flow** — the output of A becomes the input of B.
- **control flow** — A decides whether or when B runs.
- **dependency** — B needs A to exist or to have run.
- **causality** — A causes B.
A decorative arrow is an error. If an edge does not carry one of those meanings, remove it
or replace it with a grouping/caption.
## Group semantics
`\sfgroup` names a composite object. The members inside must actually belong to that
composite in the paper. Do not draw an outline around nearby nodes merely because it looks
balanced. The fit list is explicit: `{(node1)(node2)}`.
## Captions and meaning box
- `\sfcaption{name}{symbol}{detail}`: one short symbol line plus one muted detail line.
- `\sfmeaningbox`: at most three rows — idea, objects, mechanism. Prefer removing content
over shrinking type or adding columns.
+52
View File
@@ -0,0 +1,52 @@
# Layout rules
## Cursor flow
Inside `\sfrow` … `\sfrowend`, objects are placed from left to right by the cursor.
The first object starts on the left rail; every later object reserves the standing gutter.
- Declare the band height once in `\sfrow`. An object that overflows it is a build warning.
- Use `\sfconn` for a labelled edge in the flow; the label reserves its own width.
- Do not add magic-number `xshift`s between flow objects.
## Fixed topology
Use `\sfnode[at={(x,y)}]{...}` only when the diagram is genuinely non-linear: branches,
loops, vertical/horizontal stacks, skip edges.
- Keep coordinates on a coarse grid; prefer multiples of `\sfgutter` or explicit anchors.
- Use `\sfarrow` between placed nodes. Route edges on the background layer.
- Put edge labels on the edge itself, not floating nearby.
## Captions
Use `\sflane{row}` after `\sfrowend` to give all captions in that row one shared baseline.
Symbol and detail lines are two reserved lanes; do not place other text between them.
## Groups
A group outline adds inner padding, so place it after its members. It must not cover unrelated
nodes. Members must be adjacent in the semantic sense; the explicit fit list prevents the
package from inventing a group around whatever is near. A group must bind at least two
members.
After a `\sfrow`, `\sfgroup` refits that row so `\sflane{row}` hangs captions below the
outline. Prefer an empty overlay caption and `\sfcaption{group}{...}{...}`: an overlay at
the north-west corner lands on skip edges. A connector that should leave the composite
starts on the group node (`\sfarrow{block.east}{next.west}`), not on a member — otherwise
the shaft crosses an outline that is not its endpoint.
## Formula
Call `\sftopformula` after the last `\sfrowend`. The line is centered on `\sfbbox`, whose
width is only known once the bands exist.
## Callouts
`\sfcallout` hangs off a finished `\sfrow` band, one per figure. It is not an object in
the flow and must not be anchored to a single node.
## Meaning box
The meaning box hangs below the full figure bbox (`\sfbbox`). It is one column, at most
three rows. If it is too long, remove content; do not widen it past the figure or shrink type.
+52
View File
@@ -0,0 +1,52 @@
# House style
The package ships these defaults; this file explains what to preserve and what must still
be decided.
## Canvas
White background, natural `standalone` crop. No forced 16:9. No title by default. The top
holds at most two compact claim/formula lines.
Each stage is one horizontal row with a shared baseline. Use the fewest stages that preserve
the primary claim. No unrelated branches, no dashboard panels.
## Type hierarchy
| element | size | macro / style |
|---|---|---|
| top formula/claim | `\large` | `\sftopformula` |
| stage label | `\small\bfseries`, muted | `\sfstage` |
| node label | `\small` | `\sfnode` |
| caption symbol | `\small` | `\sfcaption` |
| caption detail | `\scriptsize`, muted | `\sfcaption` |
| edge label | `\scriptsize`, muted | `\sfconn` / `\sfarrowlabel` |
| meaning box | `\small` | `\sfmeaningbox` |
| optional signature | `\scriptsize`, low contrast | `\sfsignature` |
Never shrink below this hierarchy to make something fit. Move it to another row instead.
## Color
Palette: `sfTeal #4F8FA5`, `sfOrange #EE995B`, `sfCoral #C95B5B`, `sfViolet #8A74B5`,
`sfGray #85898F`. Muted, mid-chroma, paper-like. Do not add saturated primaries.
- One semantic color per role, held everywhere: `\sfsetrole{input}{sfTeal}`.
- Drawing macros take a **role**, never a color.
- At most four active hue families per figure, plus gray.
- Contrast comes from lightness separation, not saturation. Use `level=1/2/3` for
`role!30 / role!55 / role!80`; do not use uniform pastel fills.
- If sign matters, use hue for sign and intensity for magnitude.
## Edges and nodes
- Nodes are rounded rectangles with thin neutral borders; no saturated colored outlines.
- Group outlines use `role!65` because the outline itself names an object.
- Edges are thin neutral arrows on the background layer; text stays on the foreground layer.
- Keep all text in LaTeX or CJK text, never rasterized labels.
## Never
Metric insets not present in the primary claim. Decorative pills, banners, shadows,
repeated separators, extra explanatory cards beyond one `\sfcallout`. Raw TikZ `\draw` /
`\fill` / `\path` unless the lint directive allows it.
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Lint, compile, and export superfig deliverables.
# ./scripts/build.sh figure.tex [outdir]
set -euo pipefail
SRC="${1:?usage: build.sh figure.tex [outdir]}"
[[ -f "$SRC" ]] || { echo "no such file: $SRC" >&2; exit 2; }
SRCDIR="$(cd "$(dirname "$SRC")" && pwd)"
BASE="$(basename "$SRC" .tex)"
OUT="${2:-$SRCDIR/build}"
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
mkdir -p "$OUT"
if [[ "${SF_SKIP_LINT:-0}" != "1" ]]; then
echo "==> lint $BASE"
python3 "$ROOT/scripts/lint.py" "$SRC"
fi
echo "==> xelatex $BASE"
TEXINPUTS="$ROOT/assets:$SRCDIR:" \
xelatex -halt-on-error -interaction=nonstopmode \
-output-directory="$OUT" "$SRC" >/dev/null 2>&1 \
|| { echo "!! xelatex failed; last errors:" >&2
grep -n -A4 -m3 '^!' "$OUT/$BASE.log" >&2 || tail -30 "$OUT/$BASE.log" >&2
exit 1; }
LOG="$OUT/$BASE.log"
status=0
if grep -q "Missing character" "$LOG"; then
echo "!! missing glyphs:" >&2
grep -m5 "Missing character" "$LOG" >&2
status=1
fi
if grep -qE "^(Overfull|Underfull) \\\\[hv]box" "$LOG"; then
echo "!! overfull/underfull boxes:" >&2
grep -m5 -E "^(Overfull|Underfull) \\\\[hv]box" "$LOG" >&2
status=1
fi
if grep -q "Package superfig Warning" "$LOG"; then
echo "!! superfig warnings:" >&2
grep -m5 -A2 "Package superfig Warning" "$LOG" >&2
status=1
fi
if command -v pdftocairo >/dev/null 2>&1; then
echo "==> exporting svg / png"
pdftocairo -svg "$OUT/$BASE.pdf" "$OUT/$BASE.svg"
pdftocairo -png -r 300 -singlefile "$OUT/$BASE.pdf" "$OUT/$BASE"
pdftocairo -png -r 300 -singlefile -transp "$OUT/$BASE.pdf" "$OUT/$BASE-alpha"
pdftocairo -png -scale-to-x 360 -scale-to-y -1 -singlefile \
"$OUT/$BASE.pdf" "$OUT/$BASE-thumb"
else
echo "!! pdftocairo missing: PDF only, no SVG/PNG" >&2
status=1
fi
echo "==> artifacts in $OUT"
ls -1 "$OUT/$BASE"*.{pdf,svg,png} 2>/dev/null | sed 's/^/ /'
if [[ $status -ne 0 ]]; then
echo "==> BUILD DIRTY: fix the warnings above before delivering." >&2
else
echo "==> clean. Now do the visual audit (references/checklist.md)."
fi
exit $status
+159
View File
@@ -0,0 +1,159 @@
#!/usr/bin/env python3
"""Static source checks for superfig figures.
The package owns layout and colors at render time; this linter catches the
source-level escapes TeX cannot see: undeclared roles, too many active hue
families, hand-rolled TikZ drawing, a formula placed before the last row,
a callout that is not an aside on a band, and a group that wraps one node.
"""
from __future__ import annotations
import argparse
import re
import sys
from pathlib import Path
ROLE_DECL_RE = re.compile(r"\\sfsetrole\{([^{}]+)\}\{([^{}]+)\}")
ROLE_KEY_RE = re.compile(r"role\s*=\s*([A-Za-z0-9_-]+)")
RAW_TIKZ_RE = re.compile(r"\\(?:draw|fill|path)\b")
ROW_NAME_RE = re.compile(r"\\sfrow\{([^{}]+)\}")
CALLOUT_RE = re.compile(r"\\sfcallout\{([^{}]+)\}\{[^{}]+\}\{([^{}]+)\}")
GROUP_RE = re.compile(
r"\\sfgroup(?:\[[^\]]*\])?\{([^{}]+)\}\{([^{}]*)\}\{([^{}]*)\}"
)
def strip_comments(source: str) -> str:
return re.sub(r"(?<!\\)%.*$", "", source, flags=re.MULTILINE)
def line_of(source: str, offset: int) -> int:
return source.count("\n", 0, offset) + 1
def directives(source: str) -> set[str]:
found: set[str] = set()
for match in re.finditer(r"^\s*%\s*superfig-lint:\s*(.+)$", source, re.MULTILINE):
found.update(item.strip() for item in match.group(1).split(","))
return found
def lint(path: Path) -> list[str]:
raw = path.read_text(encoding="utf-8")
allowed = directives(raw)
source = strip_comments(raw)
errors: list[str] = []
declared: dict[str, str] = {"neutral": "sfGray"}
for match in ROLE_DECL_RE.finditer(source):
name, color = (part.strip() for part in match.groups())
previous = declared.get(name)
if previous is not None and previous != color and name != "neutral":
errors.append(
f"line {line_of(source, match.start())}: role {name!r} changes "
f"from {previous!r} to {color!r}"
)
else:
declared[name] = color
used_colors = {"sfGray"}
for match in ROLE_KEY_RE.finditer(source):
role = match.group(1)
color = declared.get(role)
if color is None:
errors.append(
f"line {line_of(source, match.start())}: role {role!r} is used "
f"before \\sfsetrole"
)
else:
used_colors.add(color)
active = used_colors - {"sfGray"}
if len(active) > 4:
errors.append(
f"figure uses {len(active)} active hue families "
f"({', '.join(sorted(active))}); maximum is 4 plus gray"
)
if "allow-raw-tikz" not in allowed:
for match in RAW_TIKZ_RE.finditer(source):
errors.append(
f"line {line_of(source, match.start())}: hand-rolled TikZ drawing "
f"bypasses \\sfnode/\\sfarrow; add allow-raw-tikz only for a "
f"genuinely non-linear detail"
)
formula_positions = [m.start() for m in re.finditer(r"\\sftopformula\b", source)]
row_ends = [m.start() for m in re.finditer(r"\\sfrowend\b", source)]
if formula_positions and row_ends and formula_positions[-1] < row_ends[-1]:
errors.append(
f"line {line_of(source, formula_positions[-1])}: \\sftopformula "
f"must follow the last \\sfrowend"
)
band_names = set(ROW_NAME_RE.findall(source))
callout_anchors: dict[str, str] = {}
first_callout: str | None = None
for match in CALLOUT_RE.finditer(source):
name, anchor = (part.strip() for part in match.groups())
line = line_of(source, match.start())
if anchor not in band_names:
errors.append(
f"line {line}: callout {name!r} is anchored to {anchor!r}, which "
f"is not a \\sfrow band; a card hanging off a single object "
f"reads as a step"
)
elif anchor in callout_anchors:
errors.append(
f"line {line}: callout {name!r} is the second card on band "
f"{anchor!r} (after {callout_anchors[anchor]!r}); one aside "
f"per band"
)
elif first_callout is not None and "allow-multiple-callouts" not in allowed:
errors.append(
f"line {line}: callout {name!r} is the second card in the "
f"figure (after {first_callout!r}); the budget is one callout "
f"per figure"
)
callout_anchors.setdefault(anchor, name)
if first_callout is None:
first_callout = name
if "allow-single-group" not in allowed:
for match in GROUP_RE.finditer(source):
name, fit, _caption = match.groups()
members = re.findall(r"\(([^()]+)\)", fit)
if len(members) < 2:
errors.append(
f"line {line_of(source, match.start())}: group {name!r} "
f"wraps {len(members)} member(s); bind at least two"
)
return errors
def main() -> int:
parser = argparse.ArgumentParser(description="lint a superfig .tex source")
parser.add_argument("sources", nargs="+", type=Path)
args = parser.parse_args()
failed = False
for path in args.sources:
if not path.is_file():
print(f"superfig-lint: no such file: {path}", file=sys.stderr)
failed = True
continue
errors = lint(path)
if errors:
failed = True
print(f"!! {path}", file=sys.stderr)
for error in errors:
print(f" {error}", file=sys.stderr)
else:
print(f" lint {path}")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(main())
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# Toolchain preflight for superfig.
# Exit codes: 0 full path, 1 degraded (no CJK/export), 2 no LaTeX.
set -uo pipefail
QUIET=0
[[ "${1:-}" == "--quiet" ]] && QUIET=1
say() { [[ $QUIET -eq 1 ]] || echo -e "$*"; }
ok=0; warn=0; fail=0; cjk_warn=0; export_warn=0
check() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then say " ok $name"; ok=$((ok+1)); return 0
else say " MISS $name"; return 1; fi
}
say "superfig preflight"
say "--- required ---"
check "xelatex" command -v xelatex || fail=$((fail+1))
check "standalone.cls" kpsewhich standalone.cls || fail=$((fail+1))
check "tikz.sty" kpsewhich tikz.sty || fail=$((fail+1))
check "array.sty" kpsewhich array.sty || fail=$((fail+1))
check "etoolbox.sty" kpsewhich etoolbox.sty || fail=$((fail+1))
say "--- chinese figures ---"
check "ctex.sty" kpsewhich ctex.sty || { warn=$((warn+1)); cjk_warn=1; }
check "fandol font" kpsewhich FandolSong-Regular.otf || { warn=$((warn+1)); cjk_warn=1; }
say "--- raster / vector export ---"
check "pdftocairo" command -v pdftocairo || { warn=$((warn+1)); export_warn=1; }
if [[ $fail -gt 0 ]]; then
say ""
say "RESULT: no usable LaTeX path."
say "Fall back to SVG/HTML and state that superfig package guarantees are unavailable."
exit 2
fi
if [[ $warn -gt 0 ]]; then
say ""
say "RESULT: degraded."
[[ $cjk_warn -eq 1 ]] && say " - missing ctex/fandol -> English-label figures only."
[[ $export_warn -eq 1 ]] && say " - missing pdftocairo -> PDF only, no PNG/SVG."
exit 1
fi
say ""
say "RESULT: full path available (TikZ + CJK + vector/raster export)."
exit 0
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Build every valid example, then prove invalid TeX and lint fixtures fail.
#
# ./scripts/test.sh
set -uo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
fail=0
for f in "$ROOT"/tests/*.tex "$ROOT"/examples/*.tex; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
if "$ROOT/scripts/build.sh" "$f" >/dev/null 2>&1; then
echo " ok $name"
else
echo " FAIL $name"
fail=$((fail+1))
fi
done
for f in "$ROOT"/tests/invalid/*.tex; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
if SF_SKIP_LINT=1 "$ROOT/scripts/build.sh" "$f" >/dev/null 2>&1; then
echo " FAIL $name (invalid fixture built cleanly)"
fail=$((fail+1))
else
echo " ok $name (rejected by package/build)"
fi
done
for f in "$ROOT"/tests/lint-invalid/*.tex; do
[[ -e "$f" ]] || continue
name="$(basename "$f")"
if python3 "$ROOT/scripts/lint.py" "$f" >/dev/null 2>&1; then
echo " FAIL $name (invalid fixture passed lint)"
fail=$((fail+1))
else
echo " ok $name (rejected by lint)"
fi
done
if [[ $fail -gt 0 ]]; then
echo "$fail failing check(s); rerun the reported build or lint command to inspect" >&2
exit 1
fi
echo "all positive and negative checks passed"
+26
View File
@@ -0,0 +1,26 @@
% Cursor extras: gap, leftover rail, a tracked absolute node.
\documentclass[border=8pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\sfsetrole{y}{sfViolet}
\begin{document}
\begin{tikzpicture}
\sfleftrail{4mm}
\sfrow{R1}{12mm}
\sfnode[role=x]{A}{$A$}{12mm}{10mm}
\sfgap{8mm}
\sfnode[role=y]{B}{$B$}{12mm}{10mm}
\sfrowend
\sfnode[role=x, at={($(B.south)+(0,-14mm)$)}]{C}{$C$}{12mm}{10mm}
\sfarrow{B.south}{C.north}
\sflane{R1}
\sfcaption{A}{$A$}{rail}
\sfnolane
\sfcaptiontop{B}{after gap}
\sfcaption{C}{$C$}{absolute, tracked}
\sfbbox{all}
\sftopformula{F}{$C \leftarrow B$}
\end{tikzpicture}
\end{document}
+32
View File
@@ -0,0 +1,32 @@
% Group expands the last row so captions clear the outline.
% One callout hangs off the finished band.
\documentclass[border=8pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{q}{sfTeal}
\sfsetrole{w}{sfOrange}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{composite plus aside}
\sfrow{R1}{12mm}
\sfnode[role=q]{A}{$A$}{14mm}{10mm}
\sfconn{e1}{}
\sfnode[role=q]{B}{$B$}{14mm}{10mm}
\sfgap{\sflinklen}
\sfnode[role=w]{C}{$C$}{14mm}{10mm}
\sfrowend
\sfgroup[role=q]{G}{(A)(B)}{}
\sfarrow{G.east}{C.west}
\sflane{R1}
\sfcaption{G}{$AB$}{one composite}
\sfcaption{C}{$C$}{consumer}
\sfcallout{N1}{32mm}{R1}{aside}{The card hangs off the band, not off $C$.}
\sfbbox{all}
\sftopformula{F}{$C = f(AB)$}
\sfmeaningbox{mb}{108mm}{all}
{a group is a real composite}
{pair $AB$, consumer $C$}
{captions sit below the outline because the group refits the row}
\end{tikzpicture}
\end{document}
+10
View File
@@ -0,0 +1,10 @@
% A callout hanging off a node, not a band, must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{12mm}{8mm}
\sfrowend
\sfcallout{N}{3cm}{A}{aside}{anchored to a node}
\end{tikzpicture}\end{document}
+14
View File
@@ -0,0 +1,14 @@
% A second callout in the figure must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{12mm}{8mm}
\sfrowend
\sfrow{R2}{10mm}
\sfnode[role=x]{B}{B}{12mm}{8mm}
\sfrowend
\sfcallout{N1}{3cm}{R1}{one}{first}
\sfcallout{N2}{3cm}{R2}{two}{second}
\end{tikzpicture}\end{document}
+10
View File
@@ -0,0 +1,10 @@
% A callout inside an open row is a package error.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{12mm}{8mm}
\sfcallout{N}{3cm}{R1}{t}{b}
\sfrowend
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
% An empty band must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfrowend
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
% A group around one node is decoration and must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfnode[role=x, at={(0,0)}]{A}{A}{12mm}{8mm}
\sfgroup[role=x]{G}{(A)}{}
\end{tikzpicture}\end{document}
+9
View File
@@ -0,0 +1,9 @@
% A node taller than its band by more than 4mm must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{8mm}
\sfnode[role=x]{A}{tall}{16mm}{16mm}
\sfrowend
\end{tikzpicture}\end{document}
+8
View File
@@ -0,0 +1,8 @@
% Remapping a role to a different color must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\sfsetrole{x}{sfOrange}
\begin{document}\begin{tikzpicture}
\sfnode[role=x, at={(0,0)}]{A}{A}{12mm}{8mm}
\end{tikzpicture}\end{document}
+6
View File
@@ -0,0 +1,6 @@
% An undeclared role falls back to gray and must dirty the build.
\documentclass[border=2pt]{standalone}
\usepackage[en]{superfig}
\begin{document}\begin{tikzpicture}
\sfnode[role=ghost, at={(0,0)}]{A}{A}{12mm}{8mm}
\end{tikzpicture}\end{document}
+9
View File
@@ -0,0 +1,9 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{8mm}{8mm}
\sfrowend
\sfcallout{N}{3cm}{A}{aside}{on a node}
\end{tikzpicture}\end{document}
+13
View File
@@ -0,0 +1,13 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{8mm}{8mm}
\sfrowend
\sfrow{R2}{10mm}
\sfnode[role=x]{B}{B}{8mm}{8mm}
\sfrowend
\sfcallout{N1}{3cm}{R1}{one}{first}
\sfcallout{N2}{3cm}{R2}{two}{second}
\end{tikzpicture}\end{document}
+9
View File
@@ -0,0 +1,9 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sftopformula{F}{$x$}
\sfrow{R1}{10mm}
\sfnode[role=x]{A}{A}{8mm}{8mm}
\sfrowend
\end{tikzpicture}\end{document}
+7
View File
@@ -0,0 +1,7 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\begin{document}\begin{tikzpicture}
\sfnode[role=x, at={(0,0)}]{A}{A}{8mm}{8mm}
\sfgroup[role=x]{G}{(A)}{}
\end{tikzpicture}\end{document}
+14
View File
@@ -0,0 +1,14 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\sfsetrole{a}{sfTeal}
\sfsetrole{b}{sfOrange}
\sfsetrole{c}{sfCoral}
\sfsetrole{d}{sfViolet}
\sfsetrole{e}{sfInk}
\begin{document}\begin{tikzpicture}
\sfnode[role=a, at={(0,0)}]{A}{A}{8mm}{8mm}
\sfnode[role=b, at={(12mm,0)}]{B}{B}{8mm}{8mm}
\sfnode[role=c, at={(24mm,0)}]{C}{C}{8mm}{8mm}
\sfnode[role=d, at={(36mm,0)}]{D}{D}{8mm}{8mm}
\sfnode[role=e, at={(48mm,0)}]{E}{E}{8mm}{8mm}
\end{tikzpicture}\end{document}
+5
View File
@@ -0,0 +1,5 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\begin{document}\begin{tikzpicture}
\draw (0,0) -- (1,0);
\end{tikzpicture}\end{document}
+5
View File
@@ -0,0 +1,5 @@
\documentclass{standalone}
\usepackage[en]{superfig}
\begin{document}\begin{tikzpicture}
\sfnode[role=ghost, at={(0,0)}]{A}{A}{8mm}{8mm}
\end{tikzpicture}\end{document}
+27
View File
@@ -0,0 +1,27 @@
% English-label smoke: cursor row, operator, meaning box, formula last.
\documentclass[border=8pt]{standalone}
\usepackage[en]{superfig}
\sfsetrole{x}{sfTeal}
\sfsetrole{y}{sfOrange}
\begin{document}
\begin{tikzpicture}
\sfstage{S}{one band}
\sfrow{R1}{12mm}
\sfnode[role=x]{A}{$A$}{14mm}{10mm}
\sfop{op}{$\circ$}
\sfnode[role=y]{B}{$B$}{14mm}{10mm}
\sfrowend
\sflane{R1}
\sfcaption{A}{$A$}{left}
\sfcaption{B}{$B$}{right}
\sfbbox{all}
\sftopformula{F}{$B = A \circ f$}
\sfmeaningbox{mb}{92mm}{all}
{one composition}
{two objects}
{the operator sits in the flow and reserves its own width}
\sfsignature{Smoke test}{mb}
\end{tikzpicture}
\end{document}