Files
isokit/docs/SCRIPTING.md
T
grabowski 19f4c83ae8
Deploy web Studio / deploy (push) Successful in 1m27s
Multi-select, select joined walls and fences, pond with swimming fish
Selection: S.sel stays the object picked last; S.selIds holds everything
selected.
- In the object list, Ctrl+click adds or removes an object and
  Shift+click selects a range.
- On the map, Ctrl/Shift+click toggles an object and Shift+drag draws a
  box that selects what lies wholly inside it (locked and hidden objects
  are left out).
- Ctrl+A selects everything that is not locked; Esc selects nothing.
- Clicking a wall or fence a second time selects every piece of the same
  type joined to it; another click goes back to that one piece.
- Dragging one selected object moves them all, together with the windows
  and doors on them, as one undo step. Arrow keys, R (turns the group
  about its middle), Ctrl+D, Delete, Lock and Hide all work on the whole
  selection.
- When all selected objects share a type, the property form edits them
  all; fields where they differ show "mixed".
- The agent's system prompt lists the selected objects.

The viewer outlines several objects with selectObj([ids]) and passes the
click event to onPick. A batch op now answers `ids`, one per operation,
so the copies from a group duplicate get selected.

Pond (Nature): natural, round or square, with a stone or grass edge,
fish, lily pads, spreading ripples and reed clumps. The fish come from a
new Swim primitive (script: s.swim). One matrix maps the object's frame
to the screen. Each fish turns round its ellipse with a CSS rotation,
so off-screen pausing and reduced motion apply as for the other
animations. The house demo gets a koi pond in the back garden.

Tests: ponds in every shape and rotation have the right number of fish,
no NaN and no depth-sort cycles, and stay inside their rectangle.
check_studio covers list and map multi-select, a group drag undone in
one step, joined-fence selection, a mixed-value form applied to all, box
select, delete and undo, Ctrl+A, Esc, and a new pond's fish.
2026-10-24 18:36:39 +07:00

177 lines
10 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`).
- `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 |
| `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)` | 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).
## 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:
```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.