9 Architecture
9.1 Why the renderer doesn’t know what a Z-spider is
typograph is a neutral diagram renderer: layout, style resolution, clipping, bounds, curve geometry. It has no dependency on any theme — including typograph-zx, which is a normal consumer of typograph’s public API instead: it depends on the generic contracts in node.typ, edge.typ, shape.typ, and theme.typ; nothing in the other direction. Nothing from a theme package is applied to a diagram unless a document explicitly binds it with theme:.
The test for whether something belongs in the renderer or in a theme is simple: would a non-ZX diagram language need it? A layout/style/geometry concept — “nodes have shapes,” “edges clip to silhouettes,” “styles resolve in layers” — belongs in the renderer. A specific answer to that concept — “a Z-spider is a green circle” — belongs in a theme. This is also why box and gate live in typograph itself rather than in a theme package, despite looking, at first glance, like they’d want domain-specific styling: a labelled rectangle and a port-capable node with legs are generically useful shapes for any diagram language, so they get small, functional, deliberately neutral default styling at the constructor level — enough that a document with no theme at all still gets a visible, usable box — while remaining exactly as themeable as any theme-defined kind, through the ordinary node-presets mechanism (see Theming). “Lives in the engine” and “has a specific appearance” are independent questions; conflating them is the most common way to misread this package.
9.2 The diagram item protocol
Every value a document builds inside a diagram({ .. }) body — a node, an edge, a piece of placed content — is represented the same way: a length-one array containing one tagged dictionary, (type: "node", ..) / (type: "edge", ..) / (type: "content", ..). utility.typ defines the tag predicates (is-node, is-edge, is-content) that the rest of the package uses to dispatch on an item without caring which constructor produced it.
Two things about this shape are load-bearing, not incidental:
- Arrays, not bare dictionaries. A Typst code block joins its statements’ values with
+. Arrays concatenate under+; bare dictionaries don’t have a sensible+at all. Wrapping every item in a length-one array is what makes{ z(0,0); x(1,0) }“just work” as item collection, with no explicitpush/accumulator anywhere in a document — see Core Concepts for the document-facing version of this. - A
typetag, not a Typst-native distinction. Nodes, edges, and content are structurally similar dictionaries; the tag is what letsis-node/is-edge/is-contentstay a single field comparison instead of duck-typing on which keys happen to be present.
group() (in content.typ) is the clearest consumer of this protocol: it pattern-matches on item type to decide what an affine transform means for that item — moving a node’s (x, y), transforming an edge’s waypoints and deferred endpoints, translating placed content — entirely through is-node/is-edge/is-content, with no knowledge of which constructor produced any given item.
9.3 The coordinate flip, and where it happens exactly once
Diagram coordinates use the mathematical convention (y up); Typst’s own layout primitives use the screen convention (y down). Every other module works in diagram-space; the flip from one to the other happens in exactly one function, geometry.typ’s to-screen(p, unit):
#let to-screen(p, unit) = (p.at(0) * unit, -p.at(1) * unit)Every place the renderer eventually calls place(dx:, dy:) or builds a curve.* point goes through this. Centralizing it here means the sign flip only has to be gotten right once, and every other module — including a custom shape builder or a project’s own geometry helper — never has to think about which convention it’s in, because it’s always diagram-space until the last possible moment.
This is also the source of the one recurring “gotcha” documented throughout the guide: shape-builder angles (style.rotate) are Typst’s screen convention (+90deg visually clockwise) because builders return outlines that feed straight into Typst’s native circle/rect/polygon elements, while group()’s rotate: and edge direction angles stay in diagram-space math convention, because they operate on coordinates before to-screen runs. Both conventions are internally consistent; they just answer to different layers, which is why a node whose shape should visually track a group(rotate: angle) needs style: (rotate: -angle) on that node — see Fragments and Equations.