Part I · Principles
5 Visual grammar
How a graph is drawn: the canvas, the shape and size of every kind of node, pins, wires, colour and what is drawn over what.
Normative · Level 3Draft 0.120 requirements · Roswaal meets 18
A renderer conforming at level 3 (§1.3) draws a graph as this chapter says. Its numbers are canvas units (§1.4).
Where a length is given, a renderer MUST use it, so that a graph drawn by one implementation can be laid over the same graph drawn by another and every pin meets the same wire.
5.1 The canvas and its grid
A graph is drawn on an unbounded canvas that pans and zooms. Nodes and comments have positions on it; wires are computed from those positions and are never stored (§9.2).
| Value | Number | Meaning |
|---|---|---|
| Grid spacing | 24 | The distance between grid points, and the step positions snap to. |
| Coarse grid | every 5th | Where a ruled grid draws a heavier line. |
| Fine grid hidden below | 55% zoom | Below this, a renderer SHOULD stop drawing the fine grid rather than draw it as noise. |
| Zoom range | 20% to 250% | The furthest out and in a renderer that lets the reader zoom SHOULD allow. |
| Zoom step | × 1.1 | One step of the wheel or a zoom key. |
Generated from core/canvasLayers.ts.
The grid MAY be drawn as dots at each grid point or as ruled lines, and at more than one strength; Roswaal offers both, with dots the default.
5.2 Node geometry
An ordinary node is a header and a body of rows. Its size and the place of every pin follow from these numbers alone, never from measuring text, so a node is the same size in every implementation, every font and at every zoom.
| Name | Value | Meaning |
|---|---|---|
width | 216 | An ordinary node's width. |
headerHeight | 30 | The header, with a title only. |
headerHeightTall | 46 | The header when the node shows a second line under its title. |
rowHeight | 24 | One row of the body: one input, one output, or one of each. |
footer | 10 | Space under the last row. |
tab | 30 | The width of the category tab in the header's corner (§5.4). |
radius | 8 | A step's corner. A pure node's is twice this (§5.3). |
nodeStroke | 1 | The node's border. |
pinSlot | 16 | The square a pin is drawn in. |
pinLane | 10 | How much of a row a pin's layout takes inside the node. |
execAspect | 0.866 | A flow pin's width as a fraction of its height: an equilateral triangle. |
execGap | 5 | The space between a flow pin and the node's edge. |
compactHeight | 28 | A getter capsule's height. |
rerouteSize | 22 | A reroute knot's diameter. |
operatorRadius | 14 | An operator pill's corner. |
Generated from core/nodeMetrics.ts.
Height
A node's height is its header's height, plus rowHeight for each row, plus footer. The number of rows is the larger of its inputs and its outputs, leaving out the flow pins that ride on the header (§5.5). A node whose only pins ride on the header has no rows; a node with no pins at all has one.
Where a pin is
- The nth row's centre is
headerHeight + n × rowHeight + rowHeight / 2below the node's top, counting from 0. - A flow pin on the header is centred
headerHeight / 2below the node's top. - A data pin is centred on the node's left or right edge.
- A flow pin's triangle is 13.86 wide (
pinSlot × execAspect) and standsexecGapclear of the edge, outside the node. Its wire meets it at the triangle's centre,execGap + width / 2from the edge.
A wire MUST end where its pin is, by these rules. A renderer that drew the pin elsewhere would show a wire ending in empty space.
5.3 Node kinds and shapes
Shape is the first thing read about a node, so each kind has one of its own.
| Kind | Shape | Why |
|---|---|---|
| Step | A card: a header with a tinted bar and the category tab (§5.4), a body of rows, radius corners. | It happens, in order, once. Its flow pins are on its header. |
| Value | A card twice as round, with no header bar: its title and glyph are drawn in the category's colour, and the body takes a faint wash of it. | It has no order: it is worked out where it is used (§7.2). |
| Getter | A capsule 28 tall with fully round ends, its name inside and one output on its right. Its width is estimated from its name, never less than 96. | Reading a variable is the commonest value and the smallest. |
| Operator | A pill with operatorRadius corners, its symbol in the middle, its inputs down the left and its one result centred on the right. | An expression such as a + b reads as the expression. |
| Reroute knot | A circle rerouteSize across, with both pins at its centre. | A bend in a wire, not a node with sides. |
| Comment | A translucent box in a chosen colour with a title bar, drawn behind the nodes it holds. | It groups and labels; it never runs. |
A renderer MAY offer square corners as a preference. A value's corner then squares with a step's, so the two keep their difference in proportion; a capsule, a pill and a knot keep their shapes, which are what say what they are.
5.4 Headers and the category tab
- A step's header is a tint of its category's colour over the node's body: 14% of it in a light theme and 24% in a dark one, with a line at twice that along its lower edge. Its title is drawn in the theme's text colour.
- In the header's left corner is the category tab:
tabwide, the full height of the header, filled with the category's colour, holding the category's glyph at 18 units in white. Glyphs are drawn as strokes 2.2 wide on a 24-unit grid, scaled to fit. - The title starts 8 units after the tab and is cut short with an ellipsis rather than wrapped. A second line, where a node has one, is set smaller under the title, in a monospaced face.
- A value has no tab: its glyph, at 15 units, and its title are drawn in the category's colour, mixed with the text colour enough to stay readable on the body.
A renderer MUST NOT fill a header with the category's colour at full strength and MUST NOT put the category's colour behind the title text. The colour belongs to the tab.
A node that holds code SHOULD show the profile's mark for that kind of code in its tab, in place of a glyph, as the profile's code editor marks it. Roswaal's are { } for statements, ƒx for an expression and <T> for a type, in the theme's code colours on the code editor's surface.
A function definition's second line SHOULD be its signature as types alone, each in its type's colour, such as (Model, BasePart) → boolean: the names are on its pins, and with them the line is too long to read.
Which categories exist, their colours and their glyphs belong to the profile (§10.1). A node that starts or ends a flow, or declares a function, takes the profile's colour and glyph for that, whatever its category, so a graph's entry points can be found first.
5.5 Pins, shapes and type chips
Flow pins
- A flow pin is an equilateral triangle pointing right, drawn outside the node (§5.2), in the colour of a flow wire. It is drawn hollow until a wire is connected and solid after.
- A node's first input, if it is an unnamed flow pin, rides on the header, and so does its first output. Every other flow pin, and every named one, such as Body or True, has a row, because its name is what says where that flow goes.
Data pins
A data pin is centred on the node's edge, in its type's colour, and its shape is its type's family (§6.2):
| Family | Shape | Holds |
|---|---|---|
value | circle | A plain value: a number, a string, a Vector3 |
object | rounded square | A thing in the game: an Instance or any class |
table | diamond | A table, an array or a dictionary |
function | dot in a ring | A function, to call or to pass along |
signal | hexagon | Something to listen to: an event |
Generated from core/typeFamily.ts.
A renderer MUST draw these shapes, so a pin's kind can be read without telling colours apart (§8.1).
A reroute knot's pin is drawn as a circle whatever it carries.
Each shape is drawn open while nothing is wired to it and filled once something is, with a thin ring of a dark well colour around it, so it keeps its colour against the node's body on one side and the canvas on the other.
Labels and type chips
- A pin's name is drawn beside it, inside the node, nearly in the text colour.
- An output also shows its type, as a chip: a short name in a rounded box tinted with the type's colour. The chip is left out when the pin is untyped or generic, and when the pin's name already says the type, so a type is never said twice.
- A long type name MAY be shortened on the chip; the profile says how (§10.1).
- A reroute knot, a getter and an operator pill are too small to hold a chip beside their one output, so theirs is centred under them, outside the node, where it takes no part in the node's size or its wires.
The full name of a type on a chip MUST be shown when the reader asks, by pointing at the chip or tapping it, with what the type is.
The canvas does. The documentation's pictures draw the chips but cannot open their card, and do not yet draw a getter's, an operator's or a knot's type under it.
5.6 Wires and how they route
- A wire runs from an output to an input. A flow wire is drawn in the theme's flow-wire colour; a data wire in the colour of the pin it leaves.
- A flow wire is drawn heavier than a data wire.
Where a data wire joins pins of different colours, as when a number feeds a string, it SHOULD fade from one colour to the other along its length.
Roswaal offers three routes, as a preference: curved, a cubic curve that leaves and enters its pins horizontally, its control points at least 70 units out; rigid, horizontal and vertical runs; and angular, which cuts its corners. Curved is the default.
5.7 Colour tokens and themes
Every colour on the canvas comes from a theme, a set of named colours, except a category's colour and a type's, which come from the profile. A theme names these roles:
| Role | Token | What |
|---|---|---|
app | --bg-app | The window behind everything |
panel | --bg-panel | Sidebars, toolbars, dialogs |
canvas | --bg-canvas | The graph surface |
input | --bg-input | Text fields and dropdowns |
text | --fg | Body text |
subText | --fg-muted | Labels and secondary lines |
dimText | --fg-faint | Disabled and placeholder text |
border | --border | Ordinary dividers |
borderStrong | --border-strong | Emphasised edges |
nodeBody | --node-body | A node's fill |
nodeBorder | --node-border | A node's outline |
wireExec | --wire-exec | Execution wires |
accent | --accent | Primary buttons, focus, the active toggle |
danger | --danger | Errors and destructive actions |
warning | --warning | Warnings |
ok | --ok | Success, and a clean compile |
select | --select | The selection outline on the canvas |
pure | --pure-edge | A hint in the outline of a node with no side effects |
capsule | --capsule-bg | Inline chips and badges |
capsuleBorder | --capsule-border | Their outline |
Generated from core/theme.ts.
A theme says whether it is light or dark, and the translucent overlays (hover, the grid, the header tint) are worked out from that rather than chosen, so no theme can make one invisible.
5.8 States and layering
What is drawn over what
| Layer | Order | Holds |
|---|---|---|
background | 0 | The canvas colour. |
grid | 10 | The grid. |
watermark | 20 | The graph's name, in a corner of the canvas. |
world | 25 | The layer the graph is drawn in. Every value below is ordered inside it. |
comment | 30 | Comment boxes, behind the nodes they group. |
commentHeader | 35 | Their title bars, still reachable when a node overlaps the box. |
wire | 40 | Wires, behind nodes, so no pin is covered by its own wire. |
wireDrag | 45 | The wire being dragged out of a pin. |
node | 50 | Nodes. |
nodeSelected | 55 | Selected nodes, above the rest. |
problem | 58 | Problem notes, above every node, selected or not. |
marquee | 60 | The selection rectangle, while it is drawn. |
lock | 65 | The cover over the graph while it compiles. |
menu | 70 | Menus and popovers. |
Generated from core/canvasLayers.ts.
A renderer MUST keep this order. The numbers are Roswaal's; another implementation MAY use any numbers that keep the same order.
The panels around the canvas are outside this stack, above it.
Problems are seen first
A node with an error MUST be marked: its border in the theme's danger colour and a count of its errors on its corner.
The first problem's sentence MUST be written under the node, in a note, with how many more there are. A pin a problem names MUST be marked in the node.
Notes MUST be drawn above every node, comment and wire, selected or not, so nothing in the graph covers what is wrong with it. Only the panels around the canvas, its menus and the compile cover MAY cover one.
A note MUST be drawn in a layer of its own rather than inside its node: a node is drawn as a unit, so nothing inside one can rise above the node next to it.
Where two notes overlap, the one belonging to a selected node SHOULD be drawn on top.
Other states
| State | Drawn as |
|---|---|
| Selected | The border in the selection colour, with a ring of it outside. |
| The anchor of a selection | A heavier ring: the node the others line up on (§4.3). |
| Wire in flight | Every pin it cannot land on fades; the ones it can are ringed. |
| A node a wire would grow | Ringed in the success colour, when dropping the wire on it would add a pin. |
| Pointed at, or selected | Controls that change the node, such as adding a pin, appear (§4.5). |
Open question