layout
(container arrangement) and place (sibling relations), and the
solver turns relations into world coordinates. Explicit
(wx, wy, wz) offsets remain available as a fine-tune escape hatch.
For auto-layout from a textual graph, see DSL_D2.md
instead.
JSON is also accepted (same shape, JSON encoding); the parser
sniffs the leading byte. We document YAML below.
Document structure
scene with
shape: composite. That node holds all the visible parts. The library
also accepts a single-node document with any name — see
USAGE.md for doc.Scene().
Canvas
grid: iso draws a dashed-diamond ground plane that aligns with the
iso projection — gives the scene a sense of place. Skip the field
entirely to get a transparent background.
projection: top renders a flat top-down plan view instead of the
2.5D isometric one — each part as its footprint rectangle with
orthogonal connectors, height dropped. Use it to read the layout/flow as
a normal 2D diagram. Default (iso, or omitting the key) is unchanged.
The CLI --projection flag and the Studio view toggle override it
per-render without touching the source.
gridStep doubles as the cell unit used by layout/place
gaps, so arranged parts and orthogonal connectors land exactly on
the grid texture.
Theme
A theme is a defaultStyle plus optional per-shape overrides:
Node
ANode is the top-level element. The most useful shape is
composite (a container holding Parts + optional Connectors):
shape: cylinder etc.) renders a single
iso element — convenient for sticker-sheet style outputs.
CompositePart
One entry insidenodes.scene.parts:
geom) are explicit and semantic — heroes big, tiles small.
Positions come from place/layout (next section); an offset: {wx, wy, wz} field is applied as a delta on top of the solved
position (or as the absolute position when no relation is given —
legacy documents render unchanged).
The world axes form a left-handed iso frame:
- wx runs to the right and down (after projection) —
rightOfmoves along +wx, - wy runs to the left and down —
inFrontOfmoves along +wy (toward the viewer), - wz is vertical screen up.
behind = away from the viewer
(smaller wy), inFrontOf = toward the viewer (bigger wy), and
bigger wz = higher off the ground.
Shapes
Native iso shapes (seeisotopo capabilities):
Composition primitives
place — position relative to a sibling
The default way to position free-standing parts. One constraint per
ground axis; the solver resolves chains topologically:
References must be siblings (same
parts: list). Dangling refs
and cycles are validation errors; if the solved scene still has
overlapping siblings you get a warning naming the exact pair.
layout — auto-arrange a container’s children
On a group (or the root composite). Children need no positions at
all; a layout group’s substrate auto-sizes around the content,
so its geom.w/d may be omitted:
group — labeled translucent container
Wrap N parts in a translucent rounded substrate with a corner label.
Position children with layout (above) or per-child place; the
substrate auto-sizes around the solved content:
offset relative to the
group, no auto-size) remain supported for legacy documents.
stack — auto-replicate N times
Stamp a part N times vertically with a Z-gap. The bottom copy keeps
the declared id; layers above are suffixed ~1, ~2, …:
gap is omitted, the renderer defaults to part.geom.h + 4 so
layers visually sit atop each other.
connectors — directed lines between parts
labelFontFamily: Source Sans Pro.
labelFontWeight supports 400, 600 (default), 700, normal, and bold;
labelFontSize defaults to 13 in iso and 10 in top. The embedded resource is
shared by measurement and SVG/PNG output. Explicit newlines, blank lines and
spaces are preserved when the entire block is supported. Other families or
unsupported scripts retain estimated browser-font geometry. Unset font fields
preserve legacy output; an explicit size is now also honored in top projection.
Measured pills are placed after all routes and arrows are known. The whole
previous AABB arrangement is retained first. Already-clear labels keep their
exact anchors; blocked labels search continuous intervals on their own path
while protecting every other label’s current box. Body and arrow bounding
boxes remain conservative. Other routes use swept stroke envelopes that
include round caps/joins and a bounded quadratic approximation error,
releasing the empty corners of diagonal route boxes. An already-safe position
keeps its existing exact curve-bounds certificate, including when the curve
subdivision cap makes the new envelope more conservative. Clearance is 6 SVG
units. Substrates are excluded as body obstacles; their content,
effects and arbitrary-font ink are outside this clearance scope. Quadratic
labels stay on the submitted curve. Initial placement is deterministic and
greedy. If individual refinement finds no feasible interval, the
full text keeps its baseline anchor (the path midpoint when the baseline
search itself failed). Final status is checked against the whole arrangement;
render --report records a
connector-label-placement warning for every affected measured label. Increase spacing or adjust the route;
the renderer never silently shrinks or truncates the text to make it fit.
For eligible iso scenes, an additional pass can release empty corners of body
bounding boxes. It combines filled shape faces, actual stroke widths and
curve error with measured node text and icon bounds; concave notches remain
usable where no real outline crosses them. Only still-blocked labels move,
and every peer pill, foreign route and arrow remains protected. Successful
labels declare a body-region,content-quad,... scope in the report. This also
protects measured substrate content, while allowing labels on substrate fills.
The extra pass requires bounded content throughout the scene; unknown fonts
or shapes, screen labels, annotations, legacy connector pills and unsupported
effects retain the previous layout. Person/circle captions and cloud labels
without icons now use embedded Source Sans Pro metrics for complete supported
blocks at weights 400, 600 and 700. These captions preserve authored
newlines and font size, expanding the export canvas when needed; this does not
certify that a long caption fits inside its node or avoids other objects.
Cloud captions without icons keep their original centroid; clouds with icons
retain the existing bounded face layout. A partially unsupported block still
falls back. See the API clearance contract
for the exact scope and fallbacks. No route, font size or authored text is
changed by this refinement. The optional pass has bounded search work; if its
budget runs out, unresolved labels retain their prior position and status.
A subsequent bounded joint pass can release labels that block each other’s
available slots. Already-clear labels stay fixed. Still-blocked labels may share
a corridor by choosing different safe positions on their existing routes;
every changed label must clear all final peer positions, including labels that
remain unresolved. It preserves node/route geometry and full text, and retains
only fully checked assignments if the work budget runs out. Its finite choices
do not guarantee a globally optimal packing. Unknown iso content/effects, legacy
pills, and plan node titles using ambient fonts retain the previous arrangement.
render --report identifies the body, route, arrow or peer label involved in
each measured blockage. Its structured typography.conflicts list retains
connection indexes (including parallel edges) and a conservative
clearance_shortfall in SVG units. This measures intrusion into the declared
6-unit safety gap; it is not pixel overlap area or a complete relocation
solution. Routing repair cannot trade an existing blockage for a new
counterpart or deepen its measured shortfall merely because total readability
cost decreases. Detailed lists are collected for reports and repair candidates;
ordinary RenderScene previews keep the same geometry without that payload.
orthogonal bends along the iso ground axes (every segment projects
to exactly ±30°, in register with canvas.grid: iso) — prefer it
for architecture flows. bezier draws a single soft quadratic arc —
reads as async/replication traffic.
Manual routing (usually written by the Studio edge-drag, but valid
to hand-author): an orthogonal connector may carry explicit interior
corners so the line takes a path you choose while both endpoints stay
docked to their nodes:
waypoints is a list of interior corner points in world coordinates
(source → target); the route threads through them and is kept
iso-axis-aligned. bend: {wx, wy} is the older single-corner form
(a world offset applied to the auto route’s interior); waypoints
supersedes it.
For a route that preserves height, use waypoints3d (Studio segment
drags now write this field):
wz. Endpoints are
resolved from from and to at their current heights. When consecutive
points differ on multiple axes, the route inserts bridges in X, then Y,
then Z order. Authored corners are retained. Plan view uses the XY
projection of the same intent and collapses vertical legs.
A nonempty waypoints3d overrides waypoints, bend and automatic
routing. The old fields retain their original semantics and can remain in
the source; removing waypoints3d restores that earlier intent. This field
is active with orthogonal or orthogonal2; switching to straight or
Bezier keeps the saved corners inactive and reports a warning, so switching
back restores them. It allows at most 512 interior corners and finite
coordinates within the geometry bounds. Explicit routes can cross
bodies; choosing corners is not an obstacle-clearance guarantee.
Legacy anchor selectors include api.right-mid, api.top, and
api.front. Orthogonal routes choose a facing side for bare IDs;
straight and bezier routes start from the default top centre.
For a fixed visible attachment, use api.port-left, api.port-back,
api.port-right, or api.port-front. Studio creates these references
with orthogonal2 routing. The renderer and editor resolve the same
body boundary, and fan-out never moves the selected endpoint. In plan
view, the same names attach to footprint-side midpoints. A stack instance
remains addressable, for example workers~2.port-right; plan view shows
its connection on the collapsed stack footprint.
Fixed ports describe projected contour attachments, not physical 3D face
centres. Array/rack shapes currently use a semantic envelope; text-only
parts use legacy anchors. A forced straight line, curve, bend or waypoint
can still cross a body: preserving an endpoint does not prove route
visibility or obstacle clearance. Multiple edges on one fixed port share
a junction; use bare IDs when automatic endpoint distribution is desired.
annotations — screen-space callouts
Document-level field (NOT inside a node):
\n.
Annotation boxes and orient: screen labels are
collision-managed: they never cross or touch a part’s projected
silhouette (6px clearance), never overlap each other, and when the
requested spot is occupied the search escalates distance WITHIN the
preferred direction first (so sibling labels keep a consistent
relative position) before falling to the next side, peripheral sides
first. The leader line starts on the silhouette edge FACING the box —
never across the node’s own body. side/distance are treated as
the preferred candidate, not an absolute position.
Worked example
Note: not a single hand-written coordinate.Round-trip-able per-part fragments
For every CompositePart with anid, the CLI emits
out/nodes/<id>.yaml — a self-contained Document with just that one
part. Drop it back into isotopo render to get the standalone iso of
that single element. Useful for building icon libraries from a scene.
See OUTPUTS.md for the full output structure.
Validating before render
place refs
(with nearest-id suggestion), place cycles, conflicting
constraints, and post-solve sibling overlaps (warning naming the
exact pair and overlap size). JSON output with paths is ready for
agent self-correction. Exit codes: 0 clean, 2 warnings only, 3
errors.