Deploy web Studio / deploy (push) Successful in 1m5s
Object frames turn by any angle (quarter turns stay exact, so scenes along the axes render byte for byte as before); a box in a turned frame is a prism with its own corners. Rotation is kept as given (normalised to 0..360), no longer snapped to quarter turns. Depth sort: sprites whose plan outline is not their box carry their convex outline; when boxes overlap, an outline edge that separates two sprites orders them. A long diagonal wall sorts right against both sides. Joints (line_meet, end_cut, cut_box in util.rhai): where a piece off the axes is involved, corners are mitred and T-junctions slanted; fences put a corner post or pillar in its own sprite and mitre stone bases; Y joints let the lowest id run through. Streets: every end part is a cut e + k*w, so side streets come in at any angle (slanted sidewalk gaps), two streets meet in a mitre with seam patches; fixes a curve's outer sidewalk that was skipped when its kerb and back lines had different point counts. Studio: runs and drags keep to x/y or snap to 45 degree diagonals, Ctrl gives any angle; a road run turning 45 degrees gets a 45 degree curve; Rotation is a number box. Streets demo: a diagonal lane with a hedge. MCP guidance, SCRIPTING/README/ROADMAP. Tests: Rust 76, Studio and web checks green.
272 lines
16 KiB
Markdown
272 lines
16 KiB
Markdown
# 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): 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 }, ...])` | 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).
|
||
|
||
### 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.
|
||
|
||
## 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.
|
||
|
||
## 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.
|