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.partsmay be an array or dictionary of part specifications.- Each part is a builder function (for default-style inheritance) or a dictionary with an optional
shapeand any style overrides. layer: "behind"paints a part before the base body and label;layer: "front"is the default.transformdisplaces 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-sizeoverrides inherited per-axis minima. - Font-relative components resolve against the surrounding text size before zoom, just like base-node geometry.
partsis still accepted as a legacy alias, butshape.partsis 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-anglemark-sizemark-thicknessmark-fillmark-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: absolutewidth/height.resolved-inset: absoluteleft/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, andbarereject 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.