Sandbox

A blank, general-purpose plate for building your own small app by talking to the assistant — no built-in opinion about what it is for. It ships only a chassis: a 3D canvas with lights and a ground grid, a 2D/DOM overlay layer, and a wide-format drop zone. Everything else you add lives in TRACK STATE, which is what an assistant (or the MCP tools) can write — so the app is authored entirely by conversation, and nothing here needs a plate edit. What you can build, and the state slice each thing lives in: THE LOOK via `threejs.look`, one key for the whole render recipe — set it FIRST for anything shown to people, since without it the scene is lit by the plate's flat studio fill, and leave exposure, lights and post to it (`threejs.sky` and `threejs.fog` tune an outdoor look piece by piece); 3D geometry via `primitives.items` (parametric box (with rounded edges)/cylinder/cone/sphere/torus/plane/capsule/tube, plus lathe, extrude, sweep (pipes, rails), loft (blades), heightfield (terrain) and group; `parent` builds assemblies that move and spin as one, and `instances` / `scatter` draw hundreds of copies from ONE item, e.g. a forest standing on a heightfield; re-spawned from state on every load); imported models via the drop zone or the import tools; charts via `chart2d.charts`; data tables via `dataframe.tables`; pins and annotations via `markers.items`, `markup` and `sticky`; text, HTML and floating panels via `richtext`, `htmlelement` and `panel`; INTERACTIVE CONTROLS via `legend`, whose entries carry controls wired to any action; APP LOGIC via `interactions.bindings`, each mapping a source event to an action; LOGIC THAT A BINDING CANNOT EXPRESS (a loop, a computed layout, a counter, a small state machine) via `script.scripts`: sandboxed JavaScript-subset code that computes and then call()s other plugins' actions, triggered by script.run from a ui hook or binding, on load, on a timer or on a datasource field; LOGIC TOO BIG FOR A SCRIPT (a solver, a simulation, a layout algorithm) as a SANDBOXED PLUGIN of the organization's own: draft_plugin writes it, test_plugin runs it in a real Worker against a fixture, and `customPlugins: ["org:<org>/<slug>@<version>"]` in track state attaches it (it runs for org admins while a draft; a person approves it before a publish). Script or plugin, generated code NEVER DRAWS: it computes, then calls the actions of the plugins that do (primitives, chart2d, markers, dataframe, richtext, legend); SCENE NODES a woken plugin needs and this plate lacks via `sceneNodes: [{ "type": "<plugin>.<node>", "id": "..." }]` in track state, built under the scene root on load (list_plates with `plugins` says, per plugin, whether it needs one); live data via `datasource`, `binding`, `rules`, `telemetry` and `timeline`; layout via `layout2d` (a grid or a recursive split tree; `layout2d.placements.canvas = { "slot": "<regionId>" }` moves the 3D canvas into a region, since its surface id is `canvas` here); saved cameras and walkthroughs via `view` and `slideshow`; CLICKS AND HOVERS on scene objects via `interactions` sources { plugin:'picking', id, event:'select'|'dblclick'|'hover'|'hoverout' }, with `picking.hover` and `picking.tooltips` for a hover tint and tooltips. Any other plugin is reachable too: a non-empty state slice named after it activates it on the next load. Ships empty on purpose — there is no sample geometry, chart or pin to delete before you start. The inherited toolbar is state-gated: its annotation tools stay hidden in view mode until `state.presentation.tools` lists them — valid ids are 'markup', 'measure', 'sticky', 'animation', 'xr', 'snapshot' and 'feedback'. The drop zone sets no format allowlist, so it accepts every extension the importers plugin recognises. CHROME IS ALSO STATE on this plate: it sets `uiAuthorable`, so a top-level `ui` slice REPLACES the inherited toolbar with your own tree in the ui grammar (a header with live readouts, a scrollable control rail, a status bar), whose hooks may be any qualified `plugin.action`. Two traps: leave the shell's `content` slot EMPTY (it captures pointer events, so filling it kills orbit/zoom; put the viewport and charts in `layout2d` regions instead), and do NOT declare `layout2d.insets` for the shell's own bands, which layout2d already subtracts. One caveat: CSV drag-and-drop is unavailable on a 3D plate, so pass tabular data as JSON rows into `dataframe.tables` rather than as a file.