isokit: project layer, Studio GUI, MCP server, viewer, CLI, demo project and docs

This commit is contained in:
grabowski committed 2026-10-22 15:29:07 +07:00
1 parent 95c80ec6b1
commit 564aeb75f6
47 files changed
+5978 -366

No files matched your search

+53
View File
@@ -0,0 +1,53 @@
# Roadmap
Each phase ends with tests, a Studio/viewer check in Edge, commit and push.
## Phase 1: engine, Studio, MCP (done)
- [x] Core: projection with 4 rotations, primitives (box, prism, cylinder, frustum, face, poly, flat,
openings, lines, disc, circle, stem, figures, puffs, fan, raw, groups, sequences), sprites with
90° object frames, depth sort ported from the JS views, SVG writer, tags, tooltips
- [x] Theme: built-in palette, materials and rules converted from the Mooban/CNX CSS; TOML overrides
- [x] Rhai runtime: drawing API, module imports from memory, errors with file:line, safety limits
- [x] Prefab library: site, area, road, parking, wall, fence, lamp, marker, tree, tree row, hedge,
building (storeys, windows, door, flat/gable roof, solar, AC, cut-open levels), carport, shed,
car (driving), truck, motorbike, person, crowd, tank, container, generator, solar field, chimney
- [x] Scene document + prefab catalog with property schemas
- [x] Project folder, ops (add/update/remove/duplicate/reorder/set_cfg/replace), Workspace (save, undo,
reload on outside changes), check, static HTML page, SVG export
- [x] Viewer JS: port of the views' mount() (pan, pinch, wheel, declutter, tooltips, full screen) for
prerendered, server and future WASM sources
- [x] Studio GUI: palette, object list, placing, drag-to-move with snap, keyboard, forms from schemas,
levels, undo/redo, live follow of agent edits, MCP connection panel
- [x] MCP: 15 tools over stdio and HTTP (`/mcp`), validation messages for agents
- [x] Demo project, 52 Rust tests, Playwright Studio + MCP test
## Phase 2: richer editing
- [ ] Interior prefabs (desk, table with chairs, shelf, machine, bed, sofa, partition walls) and a
building `rooms` prop, so the GF/1F levels show something
- [ ] Path objects in the Studio: click points for areas, roads (polyline with width), fences, walls,
pipe runs; vertex handles
- [ ] Multi-select (box select, Shift-click), group move, align/distribute
- [ ] Copy/paste between projects; prefab thumbnails in the palette (rendered icons)
- [ ] Measure tool and grid overlay; numeric resize handles on the selection
- [ ] Studio editor for `theme.toml` (colour pickers) and for project parameters
## Phase 3: embedding in the other projects
- [ ] WASM build of isokit-script + core: the same scene recomputes in the browser when a host
page's calculator changes the parameters (cnx_vinyl fleet calculator)
- [ ] `isokit build --fragment`: `<style>` + `<script>` blocks for the Python build scripts of the
host pages (replace `inline.py`)
- [ ] Host API parity with CNXIso (`mount`, `update(cfg)`, `setLevel`, `focus`, `info`, `onPick`)
- [ ] Port the existing scenes: mooban estate, CNX Scuba centre (section cut), CNX Vinyl plant;
check each against its old view with the QA checklist from the skill
## Phase 4: output and QA
- [ ] Section cut layer (underground scenes) from iso-scuba: cut plane, clip-path per cavity
- [ ] PNG export (static style resolution + resvg) so agents get an image from `render_view`
- [ ] `isokit check` rules from the QA checklist: props on paved areas, runs ending in the air,
covers drawn over their contents, tag overlaps
- [ ] Performance budget test (render ms per view, 60 fps pan) on a 3,500-node scene
- [ ] Packaged desktop app (Tauri) for users without a terminal; hosted Studio behind login
+117
View File
@@ -0,0 +1,117 @@
# 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).
- `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 |
| `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 |
| `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`.
## 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.