16  Shape Reference

See Shape Builders for the narrative version and the rendered gallery.

16.1 Direct builders

Builder Behavior
typ.shapes.empty No shape, no label.
typ.shapes.bare Label only, no outline.
typ.shapes.circle Contains the measured label’s corners, including asymmetric padding shifts; preserves larger minima and 1.2-axis clearance. Rotation-invariant.
typ.shapes.ellipse Independently fitted horizontal/vertical radii. Rejects nonzero rotate.
typ.shapes.stadium Maximally rounded fitted rectangle. Rejects nonzero rotate.
typ.shapes.rect Fitted rectangle using style.radius. Rejects nonzero rotate.
typ.shapes.square Square fitted to the larger axis. Rejects nonzero rotate.
typ.shapes.triangle Equilateral by default, with triangle("isosceles", ratio: ..) and triangle("angles", angles: ..) modes. Honors flip.
typ.shapes.flat-triangle Wide, directional. Honors flip.
typ.shapes.broad-triangle Tall, directional. Honors flip; useful for state/effect-like nodes.
typ.shapes.trapezoid Uses style.slant. Honors flip.
typ.shapes.arrow Uses style.tip. Honors flip.
typ.shapes.diamond Independently fitted rhombus. Polygon-based; supports rotation.
typ.shapes.hexagon Regular six-sided, established clearance. Supports rotation.

triangle, flat-triangle, broad-triangle, trapezoid, and arrow mirror across the local y-axis (style.flip) before style.rotate is applied — see Directional shapes and flip.

When a built-in polygon’s fitted axis is zero, that axis gets a 1pt fallback before the shape’s proportions/rotation are applied. Explicit nonzero dimensions are unchanged. Custom polygon-outline() still requires non-degenerate points; it does not repair malformed templates.

16.2 Triangle variants

typ.shapes.triangle is a family:

typ.shapes.triangle() // -> equilateral (default)
typ.shapes.triangle("isosceles", ratio: 2.0)
typ.shapes.triangle("angles", angles: (0deg, 120deg, 240deg))

When given "isosceles", the ratio argument (positive number) scales the horizontal width relative to the base equilateral geometry. "angles" uses three vertex directions on a circumcircle, in screen convention, not the triangle’s interior angles. Their triangle must contain the node origin strictly inside. For example, (30deg, 70deg, 80deg) puts all vertices in one quadrant and fails when the builder is used, even though those angles sum to 180 degrees.

16.3 Composite shape styling: shape.parts and marks

A style can layer multiple outlines in a single node through shape.parts (dictionary key).

  • shape.parts may be an array or dictionary of part specifications.
  • Each part is a builder function (for default-style inheritance) or a dictionary with an optional shape and any style overrides.
  • layer: "behind" paints a part before the base body and label; layer: "front" is the default.
  • transform displaces a part by a (dx, dy) length pair or by an (x: .., y: ..) dictionary, in the same coordinate frame as the fitted node bounds.
  • Part-local minima, inset, radius length, stroke thickness, and transform offsets scale with diagram/group zoom. Percentage radii remain proportional, and a part-local min-size overrides inherited per-axis minima.
  • Font-relative components resolve against the surrounding text size before zoom, just like base-node geometry.
  • parts is still accepted as a legacy alias, but shape.parts is the canonical name.

Parts inherit the resolved base style, then apply their own overrides. Their builder receives a zero-sized label measurement: the base draws the node label once. Parts enlarge visual bounds, but clipping and gate ports use only the base silhouette. Dictionary keys name entries for the author; they are not independently addressable node names. The whole part collection is replaced by a later style layer; it is not merged by entry name. Do not mix parts and "shape.parts" across layers: the resolved style rejects having both keys.

Marks are also emitted as parts. mark: "cross" adds a centered cross glyph; mark: "measurement" adds an upper semicircle and angled arrow positioned from the base shape’s top boundary and paints it behind the base body. Its arrowhead is one miter-joined contour, so heavy mark weights do not expose seams between its arms. Both marks respect mark-specific keys:

  • mark-angle
  • mark-size
  • mark-thickness
  • mark-fill
  • mark-stroke

Absolute mark sizes, thicknesses, and strokes scale with the diagram. A cross mark-size percentage is relative to the resolved base silhouette, so 100% continues to fit targets smaller than 6pt; the 6pt default is used only for an automatic cross that has no larger reference size.

Example styles:

style: (
  "shape.parts": (
    (shape: typ.shapes.circle, fill: white, stroke: none),
    (shape: typ.shapes.circle, inset: 3pt, fill: none, stroke: black),
  ),
  mark: "cross",
  mark-size: 7pt,
  mark-stroke: 0.6pt + black,
)

16.4 Factories

typ.shapes.regular(vertices: 3, rotate: 0deg, clearance: auto)

vertices is an integer ≥ 3. Factory rotate is precomputed once; a node’s own style.rotate composes afterward. clearance: auto derives breathing room from the circumradius/apothem ratio; a positive number overrides it.

typ.shapes.polygon(vertices, anchor: auto, rotate: 0deg, clearance: 1, label-offset: (0, 0))

vertices is ≥ 3 unitless numeric point pairs, uniformly scaled to fit while preserving aspect ratio. anchor: auto uses the template bounding-box centre; pass an explicit point when that centre would land outside a concave silhouette (the node origin must be strictly inside — wire clipping casts a ray from it). rotate: rotates the template before fitting. clearance: is a number or (x, y) pair. label-offset: is a template-space vector, scaled/rotated with the polygon. Concave polygons are supported; self-intersecting ones are not.

16.5 Custom builder contract

Conceptual signature: (measured-label, resolved-inset, resolved-style) => outline.

  • measured-label: absolute width/height.
  • resolved-inset: absolute left/right/top/bottom.
  • resolved-style: every merged style value; core geometric fields already scaled to absolute units, unknown custom fields passed through unchanged.

Supported return kinds:

(kind: "empty")
(kind: "bare")
(kind: "circle", radius: <non-negative length>)
(kind: "ellipse", half-width: <length>, half-height: <length>)
(kind: "rect", half-width: <length>, half-height: <length>, radius: <length>)
(kind: "polygon", points: <absolute point array>, label-offset: <absolute pair>)

circle/ellipse/rect may include an optional absolute label-offset; polygon requires it; empty/bare reject one entirely. The renderer adds the standard per-side inset displacement afterward and unions the shifted label with the silhouette for diagram bounds.

Helpers: typ.fit-box(label, pad, style, clear-x: 1, clear-y: 1, square: false) returns a fitted (width, height). typ.polygon-outline(points, label-offset: (0pt, 0pt)) validates absolute-length points and returns a complete polygon outline. typ.rotate-point(point, angle) is exported for custom geometry that needs it directly. typ.shapes.build-outline(builder, measured-label, resolved-inset, style) is the renderer’s own validator, useful for testing a builder in isolation (needs a context — see Custom shape builders for a worked example).

16.6 Limitations

  • Axis-aligned ellipse, stadium, rect, square, and bare reject nonzero rotation outright; use a polygon or custom builder for rotated geometry.
  • Rectangular outlines support one uniform corner radius, not per-corner radii.
  • Arbitrary polygons must be simple and non-degenerate, with their anchor strictly inside; concave shapes work, self-intersections don’t.
  • The custom-builder protocol covers closed circle/ellipse/rectangle/polygon silhouettes plus empty/bare. An open path or a shape with a hole needs a new core outline kind — see Extending typograph.