- 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.
11 KiB
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(withchoices),material(a theme material, optionally withchoices),text. Writedef:for the default (defaultis a reserved word in Rhai). - How the Studio places it:
shape: "line"(drag along the map: setsrotand the length propw),shape: "rect"(drag a rectangle: setswandd), 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 (level1fputs it at z = 1 ×storey_h).directed: true(withshape: "line"): the way matters, so the Studio turns the piece to run from where the drag starts (rot 0, 90, 180 or 270; conveyors). oholdsid, 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()andfocus()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
productgoods every 60 /rateseconds (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
speedm/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 ownitemeverygapmetres. - 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.
riselifts 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 levelgf(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
floorlayer; 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 checkreports 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
floorlayer (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.