Files
isokit/docs/SCRIPTING.md
T
grabowski 2810477f1b
Deploy web Studio / deploy (push) Successful in 1m4s
Data centres: multi-storey shell, racks and pods, cooling, fire safety, power
datacentre.rhai (palette groups Data centre, Cooling, Fire safety, Power):

- dc_building: multi-storey shell (storeys of storey_h), walls in pieces,
  slabs, roof with parapet, louvres, front door and loading dock; levels
  cut it open storey by storey
- raised_floor; rack_row and dc_pod (cabinets by U, width, depth,
  equipment, fill, doors, LEDs; in-row coolers; cold / hot aisle
  containment with vent tiles, roof, end doors, ceiling duct, airflow);
  dc_cage for colocation
- crah, chiller (air-cooled, dry, adiabatic), cooling_tower, pipe_run
- smoke_detectors (grid + coverage), fire_pipe (VESDA, sprinkler, gas),
  gas_bank (Novec 1230, FM-200, Inergen sized for its halls), fire_device
  (panels, call points, abort switches, beacons, extinguishers)
- power_cab (UPS, battery, PDU, switchgear, RPP), busway, cable_tray,
  transformer
- tooltips sum IT load, kW/m2, cooling redundancy per storey, UPS load,
  battery runtime, plant cooling and agent quantity (warning when short)

Engine and stdlib:

- part_hidden(o) in house.rhai: parts on closed storeys inside a
  dc_building are not drawn (they only made sort cycles under the slab
  above); house parts, furniture and people use it. Existing examples
  render byte-identical.
- overhead services sort in their own "ceiling" layer with tight boxes
- sort records which sprites each broken cycle involved; isokit check
  names them (Rendered.cycle_at)

examples/datacentre (three storeys and a roof plant), Data centre demo in
the web Studio, tests, SCRIPTING/README/ROADMAP, MCP instructions.
2026-10-27 17:08:39 +07:00

373 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Scripting and prefabs
Scenes and prefabs are written in [Rhai](https://rhai.rs/book/), a small
scripting language embedded in isokit. Nothing is compiled: save the file and
the Studio redraws.
## A prefab
A prefab is an object type that can be placed in the Studio. It is two
functions in a module that is listed in `isokit.toml` under `prelude`:
```rust
fn meta_kiosk() { // label, palette group and property form
#{ label: "Gate kiosk", category: "Site", icon: "⌂", help: "…",
props: [
#{ key: "h", type: "float", def: 2.8, min: 2.2, max: 4.0, step: 0.1, label: "Height (m)" },
#{ key: "boom", type: "bool", def: true, label: "Boom barrier" },
] }
}
fn prefab_kiosk(o) { // draw around (0, 0) in the object's own frame
let s = sprite(); // sprite ids default to the object id
s.box(-1.4, -1.1, 1.4, 1.1, 0.0, o.p.h, "brick", #{ top: "roof3" });
}
```
- Property types: `float`, `int`, `bool`, `choice` (with `choices`),
`material` (a theme material, optionally with `choices`), `text`.
Write `def:` for the default (`default` is a reserved word in Rhai).
- How the Studio places it: `shape: "line"` (drag along the map: sets `rot`
and the length prop `w`), `shape: "rect"` (drag a rectangle: sets `w` and
`d`), default a click. `mount: ["house_wall"]`: the prefab sits on objects of
those types; the Studio snaps it onto the nearest one and moves, turns,
copies and deletes it with its host. `storey: true`: placed on the storey the
view shows (level `1f` puts it at z = 1 × `storey_h`). `directed: true` (with
`shape: "line"`): the way matters, so the Studio turns the piece to run from
where the drag starts (rot 0, 90, 180 or 270; conveyors).
- `o` holds `id, type, name, x, y, z, rot, p` (the props with defaults
filled in), `pts`, `tag`, `info`, `level`.
- The engine sets the object's frame before calling the prefab. All
coordinates are local: the origin is the object's position, and local x and y
turn with `rot`. `vis()`, `dv()`, `tag()` and `focus()` use the frame too.
- A prefab with the same name in the project overrides the built-in one.
- If a prefab fails, the engine logs a warning for that object and still draws the rest.
## Drawing API
Numbers can be ints or floats. Classes are CSS classes without the `iso-`
prefix (`"wall"` becomes `iso-wall`). A material sets the face colour, which is
shaded automatically by face direction.
### Scene
| Function | |
|---|---|
| `sprite()` / `sprite(id)` / `sprite(id, cls)` | new sprite (the unit of the depth sort) in the current layer |
| `ground()` | the shared unsorted ground sprite |
| `layer("ground" \| "floor" \| "main")`, `layer(name, #{sort, zfirst, cls, before})` | where new sprites go |
| `tag(x, y, z, text[, prio[, id[, kind]]])` | label; `kind` "signtag" / "warntag" always shows |
| `info(id, name[, detail])`, `info(id, #{name, detail, rows: [[k, v], …]})` | tooltip |
| `info_row(key, value)` | one row more in the tooltip of the object being drawn (a belt's speed) |
| `focus(x, y, z)`, `bounds(x0, y0, x1, y1[, depth])`, `defs(svg)`, `warn(msg)` | framing and extras |
| `level()`, `rot()`, `cfg()`, `cfg(name)`, `objects()` | inputs (`objects()`: every document object, props resolved) |
| `objects_of(types)` | objects of a type or a list of types shown on this level |
| `objects_at(types, x, y)` | … whose footprint (`p.w` × `p.d` around the origin) holds the world point |
| `objects_on(types, x, y, tol)` | … whose centre line (local x, length `p.w`) passes within `p.t / 2 + tol` of the point, with `u, v` = the point in their frame |
| `mount_of(o, hosts[, tol])` | the host o sits on (nearest centre line within `p.t / 2 + tol`, default 0.3 m, same height), with `u, v` = o's position in the host's frame; `()` if none |
| `mounted(h, types, hosts[, tol])` | the objects of `types` whose host is h, each with `u, v` |
| `frame(o)`, `frame(x, y, z, rot)` | draw in another frame from here on (a window draws in its wall's frame); `rot` is any angle in degrees (quarter turns stay exact) |
| `to_world(x, y)`, `to_local(x, y)`, `local_of(o, x, y)`, `world_of(o, u, v)` | between the current frame (or o's) and world plan coordinates |
| `vis(nx, ny)` | is a face with that outward normal turned to the viewer? |
| `dv(dx, dy, dz)` | screen vector of a world vector (CSS motion) |
| `along_x()` | should a mover run along x in this rotation? |
| `hash(x)`, `fmt(n)`, `round1(x)`, `fmin`, `fmax`, `clamp`, `px()` | helpers |
| `rect(x0, y0, x1, y1)`, `ellipse(cx, cy, a, b, n)`, `rsegs(r)`, `sides(x0, y0, x1, y1)` | geometry |
### Sprite methods (chainable)
| Method | |
|---|---|
| `box(x0, y0, x1, y1, z0, z1, mat[, #{id, top, side, no_top, no_side}])` | axis-aligned box |
| `prism(pts, z0, z1, mat[, opts])` | convex counter-clockwise footprint; z0/z1 one value or one per vertex |
| `cyl(cx, cy, r, z0, z1, mat[, opts])`, `frust(cx, cy, r0, r1, z0, z1, mat[, sides[, opts]])` | round things |
| `slope(x0, x1, y0, y1, za, zb, th, mat)` | ramp / stair flight |
| `face(pts3, mat[, #{id, two_sided}])` | any planar polygon, shaded and culled by its normal (roof slopes, gables) |
| `beam(a, b, n, w, mat)` | square bar `w` wide between 3D points a and b, `n` a direction across it (robot arms, trusses) |
| `poly(polys3, cls[, id])` | 3D polygons as one path (solar modules) |
| `flat(poly or polys, z, cls[, id])` | flat polygon(s); later ones cut holes |
| `open(a, b, [[u0, u1, z0, z1], …], cls[, id])` | windows / doors on the face a→b (outward normal on the right) |
| `lines(segs, z, cls)`, `disc(cx, cy, z, r, cls[, id])` | markings |
| `polyline(lines, z, cls)` | open lines through plan points at height z, or 3D points `[x, y, z]` (antlers): a dashed line stays one dash run round a curve |
| `circle(x, y, z, r_px, cls[, dx, dy])`, `stem(x, y, z0, z1, w_px, cls)` | screen-space details (crowns, poles) |
| `person(x, y, z[, cls])`, `sit(x, y, z, cls)`, `head(x, y, z, r)`, `led(x, y, z[, cls])` | figures |
| `puffs(x, y, z[, kind[, n, dur]])`, `fan(x, y, z, r)` | animated steam / smoke / fans |
| `swim(x, y, z, [#{ rx, ry, dur, delay, cw, len, cls[, shape] }, ...])` | fish going round ellipses (radii rx, ry along the object's axes) about (x, y) on the plane at z, one round per `dur` s, clockwise when `cw`; `cls` koi / koiw / koig (the pond); `shape` "bird" (wings `len` across, flapping), "fly" (a butterfly) or "bee" makes it a flock |
| `seq([[x, y, \|q\| …], …])` | items drawn back to front by their anchor |
| `group(cls[, css], \|q\| …)`, `begin(cls[, css])` … `end()` | `<g>` around parts (animation) |
| `travel(a, b, dur, [[s, e], …])` … `end()` | a mover on a straight piece: what is drawn (at a) slides to b during each window (fractions of the `dur`-second cycle, may wrap past 1) and is hidden otherwise |
| `ext_reset()` | forget the sort extent drawn so far (set it with `ext` afterwards) |
| `ext(x, y, z)`, `ext_box(…)` | widen the sort box |
| `raw([x, y, z,] svg)` | raw SVG (`{uid}` becomes the view's id) |
Library helpers (`stdlib/*.rhai`): `s.tree_at(x, y, r)`, `s.car(x0, y0, x1, y1, cls)`,
`s.drive(dx, dy, dur, delay)` … `s.end()`, `cut_walls(...)`, `win_list(...)`,
`floor_of(level)`, `building_windows`, `gable_roof`, `flat_roof_kit`; for house
parts (`stdlib/house.rhai`, `stdlib/furniture.rhai`): `storey_of(z)`,
`house_view(o)` (0 closed, 1 on the storey cut open, 2 above it: leave out),
`wall_top(w)`, `opening_at(o)`, `s.slab(a, b, th, z0, z1, mat)`,
`s.chair_at(x, y, bx, by, mat)`, `s.office_chair(x, y, mat)`.
## Joints of walls and fences
`line_joint(o, e, same, stops)` (util.rhai) tells a line piece (centre line
along local x, length `p.w`) how it meets others at its end `e` (-1 or 1):
`#{ ext, post }`, how far it reaches past the end and whether it draws its end
post. `same` are the types it joins with, `stops` the types it ends against;
half thicknesses come from `line_ht(o)` (`p.t / 2`, for fences the design's).
Alone a piece reaches half its thickness past the end; on a straight run the
lower piece runs through and draws the shared post; at a corner the thicker
piece (equal: the one along world x) runs through and the other stops at its
face; at a T the stem stops at the face without an end post; against a `stops`
piece it stops at the face and keeps its post. Pieces never overlap, which the
depth sort needs. `fence` (designs `style`: mesh, wood, bars, half stone,
cobble, brick) and the site `wall` use it; `house_wall` has its own version
(`wall_ext`, house.rhai).
### Diagonal pieces
Objects turn by any angle (`rot` in degrees); a box drawn in a turned frame
becomes a prism with its own four corners. Where a piece off the axes is
involved, `line_meet(o, e, same, stops)` (util.rhai) replaces the rules above
for that end: `#{ kind, ext, k, ... }` with kind `axis` (every piece there
keeps to the axes: use the old rules, so such scenes draw as before),
`free`, `run` (in line), `corner` (a mitre on the bisector; `owner` draws a
fence's corner post) or `tee` (stop at the other's face). The end is
slanted: the line `v` to the piece's left reaches `ext + k * v` past it.
`end_cut(w, e, m)` turns that into `#{ x, k }` and `s.cut_box(x0, k0, x1, k1,
y0, y1, z0, z1, mat[, opts])` draws a box with such ends. Fences put their
corner post (or brick pillar) in its own sprite and stop their panels at it;
stone bases and cobble walls are mitred. Helpers: `on_axes(rot)`,
`dir_of(o)`, `dot2(a, b)`.
The depth sort keeps diagonal pieces right through outlines: a sprite whose
plan outline is not its box (turned or mitred pieces) carries its convex
outline, and when two boxes overlap an outline edge that separates them
orders them, the one on the viewer's side going later. A long diagonal wall
therefore sorts right against everything on either side of it, and mitred
pieces against each other.
## Animals
`wildlife.rhai` draws animals as low-poly solids from faces. `s.loft(spine,
rs, n, [side, cap0, cap1])` sweeps an n-sided ring (radius r, or `[across,
up]`) along 3D points, keeping its turn round bends; `s.tube(a, b, ra, rb, n,
mat)` is a loft of two rings. `face` shades each face by its direction and
leaves out the ones turned away, so a solid needs no sorting of its own
faces; separate parts (legs, body, neck, head, antlers) are drawn back to
front by `draw_parts`, which sorts them by the depth of their anchors in the
view (`depth(x, y) + z`). The class `beast` edges each face in its own shade
instead of the dark outline, so a body reads as one surface.
`animal_parts(kind, sub)` returns `#{ body, up, down }` for an animal facing
+x: the parts that stay and two head poses. With grazing on, the poses sit in
two groups `iso-a iso-pose2 iso-pose0` and `iso-a iso-pose2` that take turns
(the CSS shows the first unanimated); each animal starts at its own point of
the cycle. `herd(o, kind, mix, graze)` scatters `n` animals in a sunflower
spread within `r` of the object, each facing its rotation give or take 70°,
each its own sprite. Flocks of birds, butterflies and bees use `swim` with a
`shape`.
## Points (`pts`)
An object's `pts` are plan points relative to its x, y in its turned frame. A prefab that draws
them as its outline sets `outline: true` in its meta (Area): the Studio then offers Edit corners,
which turns its `w` x `d` rectangle into four points, and Rectangle again, which removes them.
Every selected object with `pts` shows handles: drag a point, drag the + of a side to add one,
double-click a point to remove it (an open route keeps 2, a closed outline 3). Route points say
whether they close with `props.closed`; any other `pts` are a closed outline.
## Groups and the drawing order
Objects of type `group` are never passed to scripts (they draw nothing), nor is anything inside a
hidden group (`Doc::hidden_in_groups`); prefabs never see the `group` field's effect. The order of
`scene.json` is the drawing order of the `ground` layer (unsorted): later ground pieces lie on
top. Sorted layers ignore it except for ties. The Studio keeps each group's members just before
the group object (`tree`, `flatten` in studio.js) and sends tree edits as one `order` op.
## Routes
`routes.rhai` (`route` prefab, meta `shape: "path"`: the Studio draws it point by point and
stores the points in the object's `pts`, relative to its x, y). The movers' path is one closed
line: a loop is offset to the left of the drawn line by the kind's lane (`keep: "right"` the
other way); an open route runs out on one side and back on the other, so it is a loop too, and
its two strips never overlap. Corners are rounded into pieces of at most 45° (`rt_round`;
people keep sharp corners, they look the same from every side).
Each straight piece is its own sprite in the piece's frame: for every group of movers that look
alike and are never on the piece together, `travel([0,0,0], [len,0,0], dur, wins)` with the
mover drawn at the start, then `ext_reset()` and the lane strip as its extent, mitred at both
ends so neighbouring pieces never overlap. Mover k is on a piece that starts `at` metres along
a loop of `total` metres while `(t / dur - at / total + k / n) mod 1` lies in `[0, len / total]`.
The renderer turns each travel into keyframes (visibility and translation, named by a hash of
their content, all in one `<style>` per view). Movers carry `iso-travel`, not `iso-a`: views
without animation (reduced motion, stopped, ghosted) pause them where they are instead of
hiding them.
## Production lines
`producer`, `conveyor` and `robot_arm` (stdlib/industry.rhai) work together
through the document, like house parts:
- A producer makes one of its `product` goods every 60 / `rate` seconds (its
ram goes down once per item) and puts them out on its local +x side.
- A conveyor carries goods along its local +x at `speed` m/s. When its start
lies on a producer's output side, it carries that producer's goods at its
rate; when its start is at another conveyor's end, it carries on with what
that one carries (followed back up to 12 belts). The gap between goods is
speed / rate, so a faster belt spreads them out. A stopped producer or belt
stops the goods after it. Alone a conveyor carries its own `item` every
`gap` metres.
- Belts meeting at a corner do not overlap: the belt a feeding belt runs into
runs through the corner and the feeding one stops at its side; at a T the
belt stops at the side. `rise` lifts the end (an incline).
- The goods ride a group that moves one gap per loop (`iso-ride`), so their
drawing order never changes; the first fades in, the last fades out.
Tooltips show speed, rate and where the goods come from (`info_row`).
- A robot arm picks goods off the end of the nearest conveyor within `reach`
(+0.4 m) and puts them on the start of another conveyor within reach, or on
a pallet a quarter turn on (`pick` / `put`: auto or a side of the arm). It
makes one move per item coming in (`cycle` 0) or every `cycle` seconds, and
waits while the line before it stands. The belt it puts on carries its goods
and starts its loop when they land, so arm and belts stay in step.
- The arm is a flipbook: 16 poses (two-link inverse kinematics, links drawn
with `beam`), each shown for a sixteenth of the cycle by a stepped CSS
animation (`iso-flip`); a rotation cannot turn a 3D arm in this projection.
Poses pointing away from the viewer are drawn before the base, the others
after it.
## Streets
`road`, `road_curve` and `roundabout` (stdlib/streets.rhai; `road` lives in
ground.rhai) meet by their centre lines:
- Each street is a centre line (a polyline or a true arc) with the
carriageway `d` wide around it; `walk` (none, both, left, right, looking
along local +x) adds sidewalks `walk_w` wide behind the kerbs: 12 cm high
paving with the kerb face drawn where it is turned to the viewer. Arcs stay
arcs when cut and offset, so a curve meets a straight road exactly.
- End to end (a curve continuing a road, a road continuing a roundabout arm)
the pieces carry on, overlapping by 4 cm so no hairline shows.
- A street ending on a road's centre line makes a T, at any angle: the road
leaves a gap in its kerb and sidewalk on that side (slanted along the
street coming in), the street ends its asphalt on the road's centre line,
its centre line and kerbs at the road's edge and its sidewalks at the
road's sidewalk.
- Two streets ending at one point at any angle meet in a mitre on the
bisector (thin seam patches reach over it, so no hairline shows).
- Every part of a street end is a cut `#{ e, k }`: the line `w` to the left
looking out reaches `e + k * w` past the end (`street_join`); gaps are
`#{ lo, hi, c }` ranges that slant with the side offset (`street_gaps`).
- A roundabout's arms end `r + arm_len` from its centre, along its local
+x, +y, -x, -y. Its kerbs flare into the ring with a fillet; splitter
islands and give-way lines sit on the entry side for `drive` left or right.
- The prefab meta `corner: "road_curve"` on `road` tells the Studio to round
each corner of a road run with a curve (up to its default radius, less
where the pieces next to a corner are short).
- `street_ends(o)`, `street_join(o, end, others)`, `street_gaps(o, others)`
and `street_path(o, line, j0, j1, gaps)` are there for your own street
prefabs; tooltips list the streets each one meets (`Meets`).
## House parts
`house_wall`, `window`, `door`, `floor`, `roof`, `stairs` and the furniture
prefabs work together through the document, not through a parent object:
- A window or door at a point on a wall's centre line (within the wall's half
thickness + 0.3 m, same height) sits on that wall (`mount_of`). The wall
leaves a gap for it (`mounted`), the window draws the opening in the wall's
frame with the wall's height, thickness and material. On no wall it stands
alone as a short wall piece. Its rotation relative to the wall picks a
door's swing side (0, 90: towards the wall's +y side) and hinge (0, 180:
at the low end).
- Wall ends adapt to the walls they meet, so no two walls overlap (the depth
sort needs them apart): along a line two walls just meet, at a T the stem
stops at the other wall's face, at a corner the wall along world x runs
through and the other stops at it.
- A part's storey is `floor((z + 0.5) / storey_h)` (`storey_h`: project
parameter, default 3 m). On level `gf` (`1f`, …) the parts on that storey
are cut open: a wall stays full height only when a floor lies on the
viewer's side of it and none behind it (it is a back wall), all others are
cut to 1 m. Roofs on that storey become an outline on the floor, parts on
storeys above are left out, the storeys below stay closed.
- Ground-level floors and rugs go to the `floor` layer; upper floors are slabs
whose top is at the object's z. Furniture starts 0.02 m above its z.
## Data centres
`datacentre.rhai` lays out multi-storey data centres from parts that read
each other through the document, like house parts:
- `dc_building` is the shell: `storeys` of `storey_h` (the project
parameter; 5 m suits data halls), outer walls split into pieces of at most
15 m, a slab per storey and a flat roof with a parapet. On level `kf` the
storeys below are closed, the walls facing the viewer are cut 1.2 m above
storey k, the others reach its ceiling, and everything above is left out.
- `part_hidden(o)` (house.rhai) leaves out a part above the storey a level
cuts open, and a part on a closed storey inside a `dc_building`: the shell
hides it, and drawing it would only make sort cycles with what stands on
the slab above. House walls, doors, floors, stairs, furniture, people and
every data centre part use it (`dc_skip` is the same rule).
- `dc_lift(o)`: the top of a `raised_floor` under o on its storey; racks,
pods, CRAH units, cabinets, gas banks, devices and people stand on it.
- A rack line is one sprite with a box per cabinet (`seq`) and the faces
painted with `open()` per class (equipment, LED strips, rear PDUs), so a
pod of 20 racks costs a few dozen nodes. In-row coolers (`inrow`) are
0.3 m units between groups of racks. A pod is three sprites (two rows and
the aisle between them: vent tiles, airflow puffs, end doors, roof, duct),
which never overlap.
- Overhead services (`cable_tray`, `busway`, `fire_pipe`, `smoke_detectors`)
draw in the layer `ceiling` (`dc_ceiling_layer()`: sorted, z first) after
everything on the floor, with sort boxes as tight as the run itself
(hangers do not count): a tray beside a pod with a busway over the pod and
under the tray has no valid order as boxes in one layer. Keep runs that
cross or lie side by side at heights that do not overlap. The project
parameter `ceiling` = false leaves them out.
- Totals: `dc_it(storey)` (IT load, racks; every storey for -1),
`dc_cool(storey)` (CRAH duty and standby, in-row coolers),
`dc_redundancy(cooling, kw)` ("N+1, 140 kW spare"), `dc_plant()` (chillers
and towers), `dc_agent(agent, V, t, litres)` (Novec 1230 at 4.5 %, FM-200
at 7 % by NFPA 2001 / ISO 14520, Inergen at 39.9 %), `dc_gas_volume(o)`.
A gas bank that is short puts a `warntag` on the view.
- Wall devices facing away from the viewer on the storey cut open hang on
the far side of a wall that is cut low: they draw ghosted.
`isokit check` names where the depth sort broke a cycle ("a before b, c":
sprite ids, `Rendered.cycle_at`), which points at the sprites to split.
## Rules that keep the depth sort right
These are lessons from the CNX/Mooban views, and the engine relies on them:
- **One sprite per thing that can stand in front of another.** Split long walls,
fences and solar rows into pieces (≤ 30 m). One big sprite overlapping many
others creates sort cycles (`isokit check` reports them).
- **Things under a cover belong in the cover's sprite**: draw cars under a carport
before its posts and roof (see `prefab_carport`).
- **Floors of cut-open storeys go in the `floor` layer** (ground floor) or in
the same sprite as the storey below. Start walls and furniture standing on
them a little above the floor (+0.02 m).
- **Movers** (`drive`) run along the axis the sort compares second
(`along_x()`). Along the other axis (and on diagonals) they stay still.
- **Diagonal pieces** sort by their outlines (see Diagonal pieces), so they
need no extra splitting; things standing exactly on a wall's line still
interpenetrate it and sort by their centres.
- Street furniture stays off drive areas, and pipe runs end on walls or supports.
## Themes
`theme.toml` stacks on the built-in palette:
```toml
[palette.dark]
brick = "#8a4f3e"
[palette.light]
brick = "#b8664c"
[materials]
brick = "brick" # class iso-brick, colour var(--iso-brick)
[rules]
".iso-svg .iso-sign2" = "fill: #ffcc00; stroke: none"
```
`isokit check` warns about any class in the drawing that has no style.