- Rust 50.9%
- HTML 24.5%
- CSS 15.7%
- JavaScript 8.5%
- Dockerfile 0.4%
Brings back the blue floor wash inside rooms on PNG export (both plain and with-dimensions), matching the canvas. Flips room_fill back to true for the export RenderOpts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| assets | ||
| design-ref | ||
| docs | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| DECISIONS.md | ||
| Dockerfile | ||
| index.html | ||
| nginx.conf | ||
| README.md | ||
| Trunk.toml | ||
Atelier — 2D Studio Floor Planner
A lightweight 2D floor-plan tool for laying out and furnishing an artist's
studio. Draw rooms, drop in studio essentials (easel, drafting table, desk,
stool, shelves, flat-file, sink, window, door), measure them in metric or
imperial, and export the result. Written in Rust, compiled to
WebAssembly, and rendered with Yew + a raw <canvas>. Works on desktop
and tablets, with full Apple Pencil / stylus support.
Features
- Rooms — draw as rectangles or closed polygons on a snapping grid; select, move, resize, rotate, and delete; edit walls by dragging corner vertices.
- Studio item palette — place the nine studio kinds (easel, drafting table, desk, stool, shelves, flat-file, sink, window, door) from the Assets tab; move, rotate, resize, and delete them.
- Live dimensions with a metric ⇄ imperial toggle (geometry is stored in meters; default grid 0.5 m).
- Pan & zoom — two-finger pan/zoom on touch, mouse wheel on desktop, plus a floating zoom pill (in / out / fit).
- Stylus — Pointer Events with pressure-aware preview, coalesced-event pen
smoothing, palm rejection (touch ignored while a pen is down), and
touch-action: noneon the canvas. - Undo / redo — full command stack (Ctrl/Cmd+Z, Shift+Cmd+Z or Ctrl+Y), Delete/Backspace to remove the selection, Escape to cancel a draw gesture, and V / R / P tool shortcuts.
- Persistence — autosave to browser
localStorage(restored on reload) plus named Save / Load slots. - Export — JSON (re-editable), PNG (rasterized from the model), and SVG (vector); Import JSON round-trips a saved design back in.
- Theme & density — dark (default) / light theme toggle and pointer / touch density, both persisted; the canvas reads its colors from the live theme tokens, so light/dark are honored on every redraw.
- Layers panel — the left dock's Layers tab lists every room and item; clicking a row selects it on the canvas.
Prerequisites
- Rust (stable) — https://rustup.rs
- The wasm target:
rustup target add wasm32-unknown-unknown - Trunk (0.21+):
cargo install trunk --locked
On some platforms Trunk's auto-downloaded
wasm-bindgenbinary is unavailable (e.g. Windows-on-ARM). If a build fails fetchingwasm-bindgen, install the CLI matching the locked crate version:cargo install wasm-bindgen-cli --version 0.2.122.
Run locally (dev)
trunk serve --open # http://127.0.0.1:8080, hot reload
Production build
trunk build --release --public-url ./
Static files land in ./dist (index.html, the JS shim, the .wasm, and
hashed CSS). The relative --public-url ./ makes the bundle deployable under any
subpath on any static host (nginx, S3, Netlify, Forgejo/GitHub Pages, …).
Test
cargo test # pure-logic unit tests (native, no browser)
This compiles and runs the pure core only — src/model/*,
src/interaction/{command,pointer,mod}, and src/render/{viewport,mod} — which
carry no web-sys/wasm-bindgen deps. Everything browser-coupled (view/,
render/scene, storage/, editor.rs, app.rs) is gated behind
#[cfg(target_arch = "wasm32")] in src/lib.rs, so cargo test stays
browser-free and fast. Current suite: 29 tests across geometry, units, item,
room, document (serde round-trip + version handling), command/undo, pointer, and
viewport.
Headless e2e smoke
The app exposes a thin JS test API on window.__atelier that drives the same
editor instance the UI uses, through the same autosave path — no behavior
change to the UI. A headless browser (Playwright, the gstack /browse daemon,
etc.) can exercise a full create → resize → place → reload → assert-persisted
flow against the real app:
// against a running `trunk serve`
__atelier.clear_all();
__atelier.create_room(0, 0, 4, 3); // x, y, w, h in meters
__atelier.resize_selected(5, 3); // resize current selection
__atelier.place_item("Easel"); // ItemKind label, case-insensitive
__atelier.room_count(); // -> 1
__atelier.item_count(); // -> 1
// reload the page, then:
__atelier.autosave_present(); // -> true (autosave restored)
No automated e2e runner is committed in this repo (the SPEC scoped v1 to manual browser verification); the hooks are in place so one can be added without touching the UI.
Verify checklist (manual)
- Rect & polygon room drawing with live dimensions and snap.
- Select / move / resize / rotate / delete; corner-drag wall editing.
- Place items from the Assets palette; undo/redo (Ctrl/Cmd+Z); Delete key.
- Units toggle; grid/snap toggle; zoom in / out / fit.
- Theme toggle (dark/light) recolors the canvas; density toggle (pointer/touch).
- Two-finger pan/zoom and pen palm rejection on a tablet.
- Save slot + reload restores autosave; Export JSON / PNG / SVG download; Import JSON round-trips; New clears the document.
Architecture
Three layers, deliberately separated so the logic is testable without a browser:
src/
model/ pure geometry, units, item, room, document + serde [tested]
interaction/ command/undo, pointer set + gestures, Tool/Selection [tested]
render/
viewport.rs world↔screen transform, pan/zoom/fit [tested]
scene.rs imperative canvas draw (grid/rooms/items/handles) [wasm]
editor.rs mutable Editor state (doc, undo, viewport, gesture) [wasm]
view/ Yew chrome: toolbar, palette, properties, statusbar,
canvas mount, inline-SVG icons [wasm]
storage/ localStorage slots + autosave; JSON/PNG/SVG export [wasm]
app.rs root App component; owns the Rc<RefCell<Editor>>;
wires chrome ↔ canvas; installs the smoke-test hooks [wasm]
- Yew renders the reactive chrome (app bar, tool group, docks, inspector,
status bar); a single
Rc<RefCell<Editor>>is shared by every component and the canvas. - A raw
<canvas>is drawn imperatively viaweb-sysfor full Pointer Events control (pressure,pointerType, coalesced events,touch-action) — things an immediate-mode GUI would abstract away. - The pure core (
model/,render/viewport,interaction/command) has no browser deps and is unit-tested natively withcargo test.
Design system
The visual language — tokens, components, the 3-pane layout, and the dark/light
palette — comes from the design handoff in
design-ref/project/Atelier/. assets/tokens.css and assets/components.css
are reused verbatim from that handoff; assets/app.css adds the .app grid
and canvas chrome. Lucide-style icons are inlined as SVG (src/view/icons.rs)
rather than loaded from a CDN, so the app is offline-robust with no console
errors from icon-load races.
See docs/superpowers/DESIGN-BRIEF.md for the distilled visual rules and
DECISIONS.md for how SPEC / PLAN / design conflicts were resolved.
CI
.forgejo/workflows/build.yml runs on a Forgejo Runner: it checks out the
repo, installs the Rust toolchain + wasm32-unknown-unknown target + Trunk, runs
cargo test (pure core), builds the release bundle with
trunk build --release --public-url ./, and uploads dist/ as an artifact. The
syntax is GitHub-Actions-compatible — move the file to .github/workflows/ to
run the same pipeline on GitHub.
Out of scope (v1) / future ideas
Not built in v1: 3D view, multiple floors, true CAD precision, real-time collaboration, and cloud accounts. Windows and doors are placeable/rotatable items today; wall-snapping doors/windows are a natural next step, along with freehand-sketch → room conversion, standalone measurement annotations, and more item kinds.
Build notes / known deviations
- The crate ships a distinct binary target
atelier-app(src/main.rs) while the library crate staysatelier. This avoids a wasm artifact name collision between thecdyliband the bin (both would otherwise beatelier, which Trunk 0.21 can't disambiguate);index.htmlpoints the Trunkrustlink atdata-bin="atelier-app". Theatelier::app::Apppath andcargo testare unaffected. SeeDECISIONS.md.