# 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()` | `` 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`. ## 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 `