Clayground.Lab

The experiment kernel for Clayground Labs: turns any sandbox into an interactive, deterministic, agent-verifiable experimentation space.

Components

  • Lab (singleton) — registry connecting everything; agents read/write
  • parameters via Lab.p(name) / Lab.set(name, value) and fetch Lab.labInfo() through the inspector's eval action.
  • Parameter — named, ranged tunable; bind your system to value.
  • Probe — named observable sampled on a fixed sim-time grid.
  • SimClock — seeded deterministic clock; with a world attached, sim
  • time advances with the physics steps (exact under the inspector's time/step action); seeded random()/randomRange() are the only randomness a lab may use.
  • ParamPanel — auto-generated slider panel for all parameters.
  • Plot2D — live autoscaled strip chart of probes. Pass probes for a
  • fixed set, or series: [{probe, label, color}] when the plotted set is built at runtime: the lab then owns naming and colouring, an empty array draws placeholder instead of every probe, and legend entries become clickable (seriesClicked) so the user can drop a curve where it is named.
  • DataRecorder — probe samples to CSV (via Clayground.Text).
  • LabTheme / ThemeSwitch — every colour, shape and type token, in a
  • light and a dark palette that swap at runtime. The two are counterparts rather than inversions, and the rules that make them so live (and are tested) in palette.js — most importantly LabTheme.inkOn(fill), which every chip and badge must use instead of naming an ink, and LabTheme.step(c, amount), which moves a colour away from the ground in whichever direction the palette has room. node palette.test.js checks the relationships, not the colours.
  • LabLang / LangSwitch — runtime language switch for a published lab.
  • Whoever owns a vocabulary registers it (LabLang.register(dict) with {lang: {key: text}}), strings are ordinary bindings on LabLang.t(key) / tf(key, ...), and LabLang.num(v, digits) prints numbers in the language's notation (German gets a decimal comma; Plot2D, BudgetBar and ParamPanel already use it). Not qsTr: retranslating a live engine is a C++ call on the QQmlEngine, which a lab hosted by the dojo or exported to WASM does not own. Drop LangSwitch in a corner and it offers exactly the registered languages.
  • Scenario / ScenarioSet — named, scripted situations wiring the
  • scenarios()/applyScenario() inspector convention; applying resets the clock so runs are reproducible.
  • Flow / FlowStep / Narrator / FlowChip — the guided, narrated
  • walkthrough: verbs-as-data driven through the lab's own flowActions(), demo/task/watch steps, hint→solve escalation, checkpoint scrubbing. FlowChip is the on-screen offer to be taught, so the lesson is not hidden behind a key nobody knows.

    Chrome — what makes two labs look like one product

    Everything below was hand-rolled in two labs before it moved here.
  • LabPanel — the titled paper panel every HUD is made of; children
  • stack, or set a size and anchor to its body.
  • LabKeys / LabHelp — the canonical key map (1..9 presets, T flow,
  • F/0 view, Shift+R record, ? help) plus the lab's own keys as data. Declaring a key is what documents it: LabHelp renders the map from the same list, so the two can never drift.
  • HintBar — bottom-centre line that steps aside while a flow narrates
  • and stays width-capped against its neighbours.
  • ScenarioBar — clickable preset chips, each with the one-line reason
  • it exists (scenario.note.).
  • WatchMonitor — watch a thing, get a probe, a colour and a curve;
  • owns probe lifecycle, stable names and the one-quantity-per-axis rule.
  • WorldLabel — 2D paper chip pinned to a 3D point.
  • SelectionFrame3D — the shared hover/select language on the work
  • surface (thin outline hovering, full frame plus facing mark selected).
  • Compass — which way the work surface faces while you circle it.
  • GridMode — snap/free placement with grafli's contract (# cycles,
  • Alt inverts for one gesture).

    The determinism contract

    Every lab must (a) derive all randomness from SimClock (seeded), (b) run correctly under the inspector's time pause/step actions, (c) expose labInfo() — typically just return Lab.labInfo(). Same seed + same stepped frames ⇒ identical probe series.

    Minimal lab skeleton

    
    import QtQuick
    import Clayground.Lab
    
    Item {
        SimClock { id: clock; seed: 42 }
        Parameter { id: pGain; name: "gain"; value: 1; from: 0; to: 5 }
        Probe { name: "output"; expr: () => mySystem.output }
    
        ParamPanel { anchors.right: parent.right }
        Plot2D { anchors.bottom: parent.bottom; width: parent.width; height: 150 }
    
        ScenarioSet { id: scen; Scenario { name: "default"; script: () => {} } }
        function scenarios() { return scen.names() }
        function applyScenario(n) { scen.apply(n) }
        function labInfo() { return Lab.labInfo() }
        function flagInfo() { return Lab.labInfo() }
    }
    
    See demo/Sandbox.qml (kernel-only damped oscillator) and labs/electronics-101/ for a full lab.