Files
isokit/docs/SCRIPTING.md
T
grabowski 18dd324543
Deploy web Studio / deploy (push) Successful in 1m4s
Conveyors and producers: goods flow from presses down the belts
- Producer (category Industry): a press making cartons, crates, drums, sacks or parts at a set
  rate per minute; its ram goes down once per item, a green / red light shows running / stopped.
- Conveyor: drawn the way it runs (new prefab meta `directed`: the Studio turns the piece from
  where the drag starts), belt width, height, rise (inclines), speed, goods and spacing; legs,
  rails and a drive motor. Corners join without overlap (the receiving belt runs through, the
  feeding one stops at its side).
- A conveyor starting on a producer's output side carries its goods at its rate; one starting at
  another's end carries on with what that one carries. The gap is speed / rate. A stopped press or
  belt stops the goods after it. Tooltips show speed, rate and source.
- The goods ride a group moving one gap per loop (no reordering), first fading in, last out.
- Script API: world_of(o, u, v), info_row(key, value); objects_on falls back to half of p.d.
- Theme: industry colours, LED colours (the marker's LED had none).
- examples/factory + Factory demo in the web Studio; MCP instructions on production lines.

Checks: 68 Rust tests (conveyor joints, feed and spacing, every view of the factory demo),
Studio 108/108 (draw direction, feed from a producer, rate change), web 89/89.
2026-10-24 21:46:10 +07:00

11 KiB
Raw Blame History

Scripting and prefabs

Scenes and prefabs are written in Rhai, 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:

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)
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)
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
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 }, ...]) 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)
seq([[x, y, |q| …], …]) items drawn back to front by their anchor
group(cls[, css], |q| …), begin(cls[, css]) … end() <g> around parts (animation)
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).

Production lines

producer and conveyor (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).

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.

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 they stay still.
  • Street furniture stays off drive areas, and pipe runs end on walls or supports.

Themes

theme.toml stacks on the built-in palette:

[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.