← Back to Docs
  • index.html
  • Clayground
  • Clayground.Lab
  • LabKeys
  • Clayground 2026.7
  • LabKeys QML Type

    The canonical lab key map, and the help text that describes it. More...

    Import Statement: import Clayground.Lab

    Properties

    Signals

    Methods

    Detailed Description

    Every lab reserves the same keys for the same things - scenarios on 1..9, T for the guided flow, F/\c 0 for the view, Shift+R to record - and every lab used to re-implement them in one long Keys.onPressed. This owns the reserved half, takes the lab's own keys as data, and can then describe the whole map, which is how a lab stops hiding its features behind undocumented letters.

    The navigation half, on a camera that has the exploration layer (panBy / goalDistance - see OrbitCamera3D): arrows and WASD move across the scene, Shift+arrows turn it, + / - zoom, F frames the selection and 0 or Home frames everything. With a jump wired, f (no Shift) starts keyboard selection and framing moves to Shift+F - f acquires, F frames. WASD is reserved for the same reason the arrows are - it is the gesture every viewer already knows - which is why a lab may not spend those four letters on anything else. The arrows used to turn, which is what a drag already did; travelling was the thing a keyboard could not do at all. While a flow runs, → and ← belong to the flow, so the arrows are the camera's only when nothing is narrating.

    The interaction half, on a lab that hands over its pointer: Space pans on the left button while it is held, which is the one gesture the left button ever lends the camera. There is no build key, because there is no build mode - a tool is something you pick up. With an hands belt wired up, H takes the next instrument (and past the last one, puts everything down), P keeps the reading it is showing, Backspace takes the last point back and Esc ends the measurement. All of them are described here for the same reason as everything else - a key nobody can find is a key the lab does not have.

    Non-visual: keep focus handling where it is and call handle() from the lab's own key handler, and handleRelease() from Keys.onReleased.

    Item {
        focus: true
        Keys.onPressed: (ev) => keymap.handle(ev)
        Keys.onReleased: (ev) => keymap.handleRelease(ev)
    
        LabKeys {
            id: keymap
            lab: root
            camera: rig
            pointer: nav
            flow: introFlow
            recorder: recorder
            keys: [
                { key: "R", label: "key.simulate", action: () => root.toggleSim() },
                { key: "V", label: "key.values",   action: () => root.showValues = !root.showValues }
            ]
        }
    }

    See also LabHelp, ScenarioBar, and Flow.

    Property Documentation

    camera : var

    An OrbitCamera3D (or anything with orbitBy/zoomBy).


    entries : var [read-only]

    The complete map as [{key, label}] - reserved keys included.


    flow : var

    The lab's Flow, if it has one.


    frameAll : var

    Called on 0; defaults to the lab's frameAll().


    frameSelection : var

    Called on F; defaults to the lab's frameSelection().


    handKey : string

    The letter that walks the belt.


    handKeys : bool [read-only]

    There is a belt to take instruments from.


    hands : var

    An InstrumentBelt - the keys that take, edit and keep a reading.

    H walks the belt, P keeps the reading, Backspace (and Delete) takes the last point back and Esc ends the measurement - the editing keys only while something is actually in the hand, which is what lets a lab keep Del for deleting the thing it builds.


    helpVisible : bool

    Toggled by ?, rendered by LabHelp.


    hints : var

    A HintBar for refusals - a key that cannot act says why.

    A refused key must never be silent (and must never touch the clock, the RNG or the view state): with a bar wired, "no earlier view" is a flashed line instead of nothing happening. refused fires either way, for a lab that wants its own channel.


    jump : var

    A HintJump - keyboard selection on f.

    Wiring one SPLITS the frame key: f (no Shift) puts the jump labels up, Shift+F frames the selection - the map's first Shift-differentiated letter pair, after the precedent of Shift+R and the Shift-arrows. A lab without a jump keeps plain F framing, so nothing existing changes until a lab opts in. While the labels are up every key is routed to the jump - a text-entry context, like the pin prompt, not a mode.


    keys : var

    The lab's own keys: [{key, label, action, hidden}].

    key is the printable letter ("V"), label a LabLang key describing it, action the function to run. Entries appear in entries and therefore in LabHelp, so declaring a key is the same act as documenting it.

    These are dispatched before the travel keys, so W, A, S and D are effectively reserved: claiming one wins, and the lab silently loses that pan direction. Pick another letter - it is why watching a thing sits on Q rather than W.


    lab : var

    The sandbox root - supplies scenarios(), applyScenario() and the framing functions.


    measureKeys : bool [read-only]

    A measurement is being taken right now, so its editing keys are live.


    There is a pointer, so Space can lend it to the camera.


    orbitStep : real

    Degrees of yaw per Shift+Left / Shift+Right.


    panStep : real

    One arrow-key pan, as a fraction of the camera's distance.

    Relative rather than absolute so the same key press covers the same part of the picture at every zoom - a fixed step in world units either crawls when you are far out or throws you off the board when you are close in.


    pinKey : string

    The letter that keeps a reading.


    pointer : var

    An OrbitInput3D, for the one key that touches the mouse.

    Space lends the left button to the camera while it is held - a quasimode, which is why this lives here rather than in the lab: a key RELEASE has to be seen too. Wire handleRelease from the lab's Keys.onReleased.

    Space is shared with a running flow, which keeps it: while a narration is on screen, Space is "next step" in every lab, and the temporary hand is not what a reader is reaching for. Nothing else in the map claims it.


    recorder : var

    A DataRecorder toggled by Shift+R.


    scaleKeys : bool

    Handle Ctrl+Plus / Ctrl+Minus / Ctrl+0 for LabTheme::uiScale.

    On by default, so every lab that already has a keymap gained the text size control for free the day it landed.


    scenarioNames : var

    Names bound to 1..9; defaults to the lab's own scenario list.


    selection : var

    The selected part's card as a keyboard target.

    An adapter the lab supplies: active (something is selected), moveFocus(d), adjust(d) -> bool and operate() -> bool. While active, j/\c k walk the card's control rows, h/\c l adjust the focused one (step a value, flip a toggle, cycle an option) and Enter operates the part's actuator. These live only under a selection - the nearest-context rule, which is also how h can belong to the card here and to the belt otherwise (Esc clears the selection first). adjust/\c operate return false to refuse, and the refusal is spoken through hints.


    selectionKeys : bool [read-only]

    A card is selected, so its keys are live.


    viewKeys : bool

    Handle the arrow/zoom/frame keys.


    Signal Documentation

    refused(string reason)

    A reserved key was pressed and could not act; reason is the LabLang key describing what was missing.

    Note: The corresponding handler is onRefused.


    Method Documentation

    bool handle(var event)

    Runs the key's action; returns true when the key was consumed.

    While a flow runs, Space / → advance it and ← steps back, so the narration reads the same in every lab. The lab's own keys keep working throughout: what a running flow takes is the board (see Flow::control), not the key map - the view, the text size, the help and the transport are the reader's at every moment. The one exception is the selected part's card, whose h / l / Enter go through selection and are refused for a part the flow has not lent out.


    bool handleRelease(var event)

    The other half of the quasimode; call it from Keys.onReleased.

    Only Space needs it, and only while there is a pointer. Auto-repeat is ignored: a held key repeats as press-release pairs on some platforms, and taking those at face value makes the hand flicker.


    string helpText()

    One line describing the map, for a hint bar.


    void releaseSprings()

    Drops any held-key mode. For when the lab stops being the one receiving keys.