PRIZM 4.0DSTA
Viewing C3 design language
RC3 · Components

Controller interface

Live operator input state — sticks, triggers, buttons. Read-only; the consumer wires input from their gamepad / WebSocket / physical controller pipeline. Useful for direct teleop confirmation, deadzone debugging, and binding reference.

Live

Animated demo — sticks, trigger, and the REC button move as if the operator is driving. Below: two static fixtures showing idle and active states with binding labels.

Animated
UGV-04
DRIVE+0.00 / +0.60
GIMBAL+0.00 / +0.20
ZOOM−0.00
ZOOM+0.50
AHORNBRECXMARKYRTL
Idle — sticks centred, no triggers, no buttons pressed. Operator's input is live but the operator is not driving.
UGV-04
DRIVE+0.00 / +0.00
GIMBAL+0.00 / +0.00
ZOOM−0.00
ZOOM+0.00
AHORNBRECXMARKYRTL
Active — drive stick forward, gimbal panning, right trigger zooming, REC button pressed.
UGV-04
DRIVE+0.10 / +0.78
GIMBAL-0.40 / +0.25
ZOOM−0.00
ZOOM+0.60
AHORNBRECXMARKYRTL

Anatomy

Horizontal strip. Each section renders only when the consumer passes its data. The platform marker carries the Ember signature; operational controls stay neutral.

Platform marker

Optional leading cell — Ember dot + mono identifier. Drop when the controller sits next to a video tile that already names the source.

Stick wells

Circular SVG wells with a faint dashed deadzone, a centre-to-position trace, and a neutral fg dot. Numerical X / Y values below for precise reading.

Trigger bars

Thin vertical bars filling from the bottom. 0 to 1 value displayed below. Use for continuous-value inputs (zoom, fine throttle).

Button pills

Small mono pills with optional binding subtitle. Pressed state inverts the pill (bg-fg / text-bg) for high-contrast read-at-a-glance.

Props

PropTypeDefaultDescription
platformstringOptional platform identifier — e.g. "UGV-04". Renders as the leading Ember-dotted cell.
leftStickStickStateLeft stick state. { x, y } normalised to -1 / 1 per axis.
leftStickLabelstring"STICK L"Label above the left stick well — e.g. "DRIVE".
rightStickStickStateRight stick state.
rightStickLabelstring"STICK R"Label above the right stick well — e.g. "GIMBAL".
leftTriggernumberLeft trigger value, 0 to 1.
leftTriggerLabelstring"TRIG L"Label above the left trigger bar.
rightTriggernumberRight trigger value, 0 to 1.
rightTriggerLabelstring"TRIG R"Label above the right trigger bar.
buttonsControllerButton[]Button row. Each button has an id, a display label, optional binding subtitle, and live pressed state.
classNamestringForwarded to the root container.

Types

StickState

Normalised stick position. Positive y is up by convention. Values outside [-1, 1] are clamped.

ts
interface StickState {
  x: number;  // -1 (full left) to 1 (full right)
  y: number;  // -1 (full down) to 1 (full up)
}
ControllerButton

Per-button state. The consumer drives `pressed` from the input pipeline; `binding` is the operator-facing what-it-does label.

ts
interface ControllerButton {
  id: string;        // stable identifier
  label: string;     // displayed (e.g. "A", "RB", "STOP")
  pressed?: boolean; // live press state — inverts the pill when true
  binding?: string;  // optional binding subtitle (e.g. "EMERGENCY STOP")
}

Wiring

Read-only. The consumer reads the gamepad (or WebSocket from a physical controller) and pushes state in on each tick. RC3 does not own the input pipeline — same separation as video tile and telemetry HUD.

tsx
// Wired to the browser Gamepad API
const pad = navigator.getGamepads()[0];

<ControllerInterface
  platform="UGV-04"
  leftStick={{ x: pad.axes[0], y: -pad.axes[1] }}
  leftStickLabel="DRIVE"
  rightStick={{ x: pad.axes[2], y: -pad.axes[3] }}
  rightStickLabel="GIMBAL"
  leftTrigger={pad.buttons[6].value}
  leftTriggerLabel="ZOOM−"
  rightTrigger={pad.buttons[7].value}
  rightTriggerLabel="ZOOM+"
  buttons={[
    { id: "a", label: "A", binding: "HORN", pressed: pad.buttons[0].pressed },
    { id: "b", label: "B", binding: "REC",  pressed: pad.buttons[1].pressed },
    { id: "x", label: "X", binding: "MARK", pressed: pad.buttons[2].pressed },
    { id: "y", label: "Y", binding: "RTL",  pressed: pad.buttons[3].pressed },
  ]}
/>

Behavioural rule

Invariant Five

Telemetry never silently stale

When a stick or trigger goes unresponsive, omit its prop rather than holding the last-known value. A frozen stick reading masquerading as live input is the same failure shape as a stale video frame — operators act on signal that is no longer true.

Read invariant

Accessibility

Live regionThe strip carries `role="status"` and `aria-live="polite"`. Screen readers announce input transitions without interrupting the operator.
Labelled by platformThe root `aria-label` is of the form `Controller input for {platform}` when present, otherwise just `Controller input`.
Button pillsEach button carries `role="status"` and an `aria-label` of the form `{label} — {binding}, {pressed | released}` so screen readers announce both the binding and the live state.
Decorative visualsStick well graphics and trigger bar fills are `aria-hidden`. Labels and numerical values carry the meaning.
Colour and meaningPressed state uses contrast inversion, not colour alone — a button reads as pressed because it flips bg / fg, not because it changes hue.

JavaFX

Ships in the PRIZM JavaFX library for thick-client C3 apps as Rc3ControllerInterface (extends HBox). Run the gallery to see it natively.

java
import design.prizm.fx.rc3.Rc3ControllerInterface;

Rc3ControllerInterface()
MemberTypeDefaultDescription
StickStaterecord(double x, double y)-1..1 per axis; y positive up.
ControllerButtonrecord(String id, label, boolean pressed, String binding)binding optional; pressed pills invert.
setPlatform(String) → voidEmber-dotted leading cell.
setLeftStick / setRightStick(StickState, String label) → voidStick well + axis readout.
setLeftTrigger / setRightTrigger(Double, String label) → voidVertical trigger bar (0..1).
setButtons(List<ControllerButton>) → voidButton pill row.
updateInputs(StickState, StickState, Double, Double) → voidLive-update sticks + triggers in one rebuild — for frame-rate input feeds.

Read-only input visualisation — the consumer wires the gamepad / WebSocket feed. Honours invariant 5 by extension (stop feeding a value rather than holding the last). Mirrors components/rc3/controller-interface.tsx.

Usage

Use whenever the operator is in direct teleop and benefits from seeing what their input is doing — deadzone confirmation, binding reference, and as a safety check that the input pipeline is live. Pass the binding labels even when the operator is experienced; the labels rot less than memory under stress. Touch / virtual-joystick input — where the operator drags the stick on the screen rather than on physical hardware — is a separate concern and not in this organism's scope.