Plugin API
Plugins interact with Play via the Host object exposed at runtime:
window.__PLAY_HOST__
This page enumerates the callable surface. For semantics, constraints, and examples, see Plugin contract.
Registration
A plugin module is loaded via dynamic import() and must export one of:
registerPlayPlugin(host): named exporta default export function
(host) => disposer
The register function may return:
a disposer function
() => void, oran object with
dispose(): void
Host object v1
The Host API is a plain JavaScript object. This TypeScript-like shape is meant as a developer reference:
Notes:
The Host object is frozen (
Object.freeze) to discourage plugins from mutating the contract surface.Plugins that need to stash state should use
host.extensions.
type PanelId = "left" | "right";
type UiMount =
| "leftPanel" | "rightPanel" | "overlayRoot"
| "leftPanelAfterFilePlugin" | "leftPanelPlugin" | "rightPanelPlugin";
interface ViewerControlsApi {
// Debug/inspection helpers. Not part of the stable plugin contract.
getBinding(id: string): unknown | null;
// Convenience helpers around the built-in UI spec.
listIds(prefix?: string): string[];
toggleControl(id: string, value?: unknown): Promise<void> | void;
getControl(id: string): UiControl | null;
loadXmlTextAsModel(xmlText: string, label?: string): Promise<void> | void;
}
interface ViewerRendererApi {
getStats(): Record<string, unknown>;
getContext(): unknown | null;
ensureLoop(): void;
renderScene(snapshot: unknown, state: unknown): void;
getOverlay3D(): unknown | null;
overlay3d: {
get(): unknown | null;
createScope(scopeId: string, options?: unknown): unknown | null;
getScope(scopeId: string): unknown | null;
};
}
interface ViewerStore<State = unknown> {
get(): State;
replace(next: State): void;
update(mutator: (state: State) => void): void; // mutates in-place
subscribe(fn: (state: State) => void): () => void;
}
interface UiPanelApi {
root: HTMLElement | null;
collapseAll(): { changed: number; collapsed: boolean };
expandAll(): { changed: number; collapsed: boolean };
toggleAll(): { changed: number; collapsed: boolean | null };
}
interface UiSectionHandle {
panel: PanelId;
sectionId: string;
sectionEl: HTMLElement;
body: HTMLElement;
setCollapsed(collapsed: boolean): void;
collapse(): void;
expand(): void;
toggle(): void;
dispose(): void;
}
interface UiSectionSpec {
panel?: PanelId;
sectionId: string; // must start with "plugin:"
title?: string;
defaultOpen?: boolean;
after?: string;
before?: string;
mount?: UiMount;
render(body: HTMLElement, ctx: { panel: PanelId; sectionId: string; sectionEl: HTMLElement; body: HTMLElement; host: PlayHostV1 }): (void | (() => void) | { dispose(): void });
}
interface UiSectionsApi {
register(spec: UiSectionSpec): UiSectionHandle;
unregister(sectionId: string): boolean;
get(sectionId: string): UiSectionHandle | null;
list(): string[];
}
interface UiKit {
namedRow(labelText: string, options?: { full?: boolean; half?: boolean }): { row: HTMLElement; label: HTMLLabelElement; field: HTMLElement };
fullRow(options?: { half?: boolean }): { row: HTMLElement; field: HTMLElement };
button(options: { label: string; variant?: "primary" | "secondary" | "pill"; testId?: string | null; onClick?: (ev: MouseEvent) => void }): HTMLButtonElement;
textbox(options?: { value?: string; placeholder?: string; testId?: string | null; onInput?: (ev: Event, value: string) => void; onChange?: (ev: Event, value: string) => void }): HTMLInputElement;
textarea(options?: { value?: string; placeholder?: string; rows?: number; variant?: "default" | "code"; testId?: string | null; onInput?: (ev: Event, value: string) => void; onChange?: (ev: Event, value: string) => void }): HTMLTextAreaElement;
select(options?: { value?: string; options?: string[]; testId?: string | null; onChange?: (ev: Event, value: string) => void }): HTMLSelectElement;
number(options?: { value?: number; min?: number; max?: number; step?: number; testId?: string | null; onInput?: (ev: Event, value: number) => void; onChange?: (ev: Event, value: number) => void }): HTMLInputElement;
range(options?: { value?: number; min?: number; max?: number; step?: number; testId?: string | null; onInput?: (ev: Event, value: number) => void; onChange?: (ev: Event, value: number) => void }): HTMLInputElement;
segmented(options: { options: Array<{ value: string; label: string }>; value: string; testId?: string | null; onChange?: (ev: Event, value: string) => void }): { root: HTMLElement; inputs: HTMLInputElement[]; value(): string; setValue(v: string): void };
codebox(options?: { value?: string; testId?: string | null }): HTMLElement;
boolButton(options: { label: string; value: boolean; disabled?: boolean; testId?: string | null; onChange?: (ev: Event, value: boolean) => void }): { root: HTMLElement; input: HTMLInputElement; text: HTMLElement };
}
interface UiApi {
panel(panel: PanelId): UiPanelApi;
sections: UiSectionsApi;
kit: UiKit;
}
interface PlayClockApi<Snapshot = unknown, State = unknown> {
onUiTick(fn: (ctx: { snapshot: Snapshot | null; state: State; nowMs: number }) => void): () => void;
onUiMainTick(fn: (ctx: { snapshot: Snapshot | null; state: State; nowMs: number }) => void): () => void; // alias of onUiTick
onUiControlsTick(fn: (ctx: { snapshot: Snapshot | null; state: State; nowMs: number }) => void): () => void;
onUiSlowTick(fn: (ctx: { snapshot: Snapshot | null; state: State; nowMs: number }) => void): () => void;
onSnapshot(fn: (ctx: { snapshot: Snapshot; state: State; nowMs: number }) => void): () => void;
onFrame(fn: (ctx: unknown) => void): () => void;
}
interface PlayHostV1 {
apiVersion: 1;
contract: { apiVersion: 1 };
capabilities: {
mounts: boolean;
ui: boolean;
store: boolean;
backend: boolean;
controls: boolean;
renderer: boolean;
clock: boolean;
overlay3d: boolean;
};
getCapability(name: string): boolean;
extensions: Record<string, unknown>;
mounts: {
leftPanel: HTMLElement | null;
rightPanel: HTMLElement | null;
overlayRoot: HTMLElement | null;
leftPanelAfterFilePlugin: HTMLElement | null;
leftPanelPlugin: HTMLElement | null;
rightPanelPlugin: HTMLElement | null;
};
ui: UiApi;
store: ViewerStore;
backend: unknown; // see :doc:`/api_reference/js_api`
controls: ViewerControlsApi;
renderer: ViewerRendererApi;
getSnapshot(): unknown | null;
clock: PlayClockApi;
logStatus(message: string, extra?: unknown): void;
logWarn(message: string, extra?: unknown): void;
logError(message: string, extra?: unknown): void;
strictCatch(err: unknown, context: string, options?: { allow?: boolean }): unknown;
}
Viewer state types
The viewer store state is a large object. A TypeScript definition is committed for tooling and serves as the most complete “field list” reference:
// Auto-generated by tools/generate_ui_artifacts.mjs. Do not edit by hand.
// Type definitions for the runtime state helpers located in viewer_state.mjs.
// This file lets TypeScript-aware tooling reason about the viewer store while
// keeping the browser-consumable implementation in plain JS.
export interface OverlayState {
help: boolean;
info: boolean;
profiler: boolean;
sensor: boolean;
fullscreen: boolean;
vsync: boolean;
busywait: boolean;
pauseUpdate: boolean;
}
export interface SimulationState {
run: boolean;
scrubIndex: number;
keyIndex: number;
realTimeIndex: number;
}
export interface SelectionState {
geom: number;
body: number;
joint: number;
name: string;
kind: string;
point: [number, number, number];
localPoint: [number, number, number];
normal: [number, number, number];
seq: number;
timestamp: number;
}
export interface PerturbState {
mode: string;
active: boolean;
}
export interface RuntimeState {
cameraIndex: number;
cameraLabel: string;
trackingGeom: number;
lastAction: string;
gesture: GestureState;
drag: DragState;
selection: SelectionState;
perturb: PerturbState;
lastAlign: AlignRuntimeState;
lastCopy: CopyRuntimeState;
}
export interface PanelState {
left: boolean;
right: boolean;
}
export interface PhysicsState {
disableFlags: Record<string, boolean>;
enableFlags: Record<string, boolean>;
actuatorGroups: Record<string, boolean>;
}
export interface ThemeState {
color: number;
spacing: number;
font: number;
}
export interface ViewerGroupState {
geom: boolean[];
site: boolean[];
joint: boolean[];
tendon: boolean[];
actuator: boolean[];
flex: boolean[];
skin: boolean[];
}
export interface RenderingOptionsState {
materials: { forceBasic: boolean };
instancing: { enabled: boolean };
transparency: { bins: number; sortMode: string };
}
export interface RenderingAppearanceState {
[key: string]: unknown;
}
export interface RenderingState {
voptFlags: boolean[];
sceneFlags: boolean[];
labelMode: number;
frameMode: number;
flexLayer: number;
bvhDepth: number;
assets: unknown | null;
groups: ViewerGroupState;
hideAllGeometry: boolean;
appearance: RenderingAppearanceState;
options: RenderingOptionsState;
}
export interface HudState {
time: number;
frames: number;
fps: number;
rate: number;
measuredSlowdown: number;
ngeom: number;
contacts: number;
pausedSource: string;
rateSource: string;
modelLabel: string;
info: Record<string, unknown> | null;
}
export interface HistoryState {
captureHz: number;
capacity: number;
count: number;
horizon: number;
scrubIndex: number;
live: boolean;
}
export interface WatchState {
field: string;
index: number;
value: number | null;
min: number | null;
max: number | null;
samples: number;
status: string;
valid: boolean;
summary: string;
sources: Record<string, { length?: number; label?: string }>;
}
export interface KeyframeState {
capacity: number;
count: number;
labels: string[];
slots: Array<{ index: number; label: string; kind: string; available: boolean }>;
lastSaved: number;
lastLoaded: number;
}
export interface ModelState {
opt: Record<string, unknown>;
vis: Record<string, unknown>;
stat: Record<string, unknown>;
visDefaults: Record<string, unknown>;
cameras: Array<Record<string, unknown>>;
geoms: Array<Record<string, unknown>>;
ctrl: number[];
optSupport: { supported: boolean; pointers: string[] };
[key: string]: unknown;
}
export interface ToastState {
message: string;
ts: number;
}
export interface DragState {
dx: number;
dy: number;
}
export interface GesturePointer {
x: number;
y: number;
dx: number;
dy: number;
buttons: number;
pressure: number;
}
export interface GestureState {
mode: string;
phase: string;
pointer?: GesturePointer | null;
}
export interface AlignRuntimeState {
seq: number;
center: [number, number, number];
radius: number;
timestamp: number;
source: string;
}
export interface CopyRuntimeState {
seq: number;
precision: string;
nq: number;
nv: number;
timestamp: number;
qposPreview: number[];
qvelPreview: number[];
complete: boolean;
}
export interface VisualBackupsState {
model: Record<string, unknown> | null;
presetSun: Record<string, unknown> | null;
presetMoon: Record<string, unknown> | null;
sceneFlagsModel: boolean[] | null;
sceneFlagsPresetSun: boolean[] | null;
sceneFlagsPresetMoon: boolean[] | null;
lightActiveModel: number[] | null;
lightActivePresetSun: number[] | null;
lightActivePresetMoon: number[] | null;
appearanceModel: Record<string, unknown> | null;
appearancePresetSun: Record<string, unknown> | null;
appearancePresetMoon: Record<string, unknown> | null;
}
export interface VisualBaselinesState {
model: Record<string, unknown> | null;
sceneFlagsModel: boolean[] | null;
presetSun: Record<string, unknown> | null;
presetMoon: Record<string, unknown> | null;
sceneFlagsPresetSun: boolean[] | null;
sceneFlagsPresetMoon: boolean[] | null;
lightActiveModel: number[] | null;
lightActivePresetSun: number[] | null;
lightActivePresetMoon: number[] | null;
appearanceModel: Record<string, unknown> | null;
appearancePresetSun: Record<string, unknown> | null;
appearancePresetMoon: Record<string, unknown> | null;
}
export interface ViewerState {
overlays: OverlayState;
simulation: SimulationState;
runtime: RuntimeState;
model: ModelState;
theme: ThemeState;
panels: PanelState;
physics: PhysicsState;
rendering: RenderingState;
hud: HudState;
toast: ToastState | null;
history: HistoryState;
watch: WatchState;
keyframes: KeyframeState;
visualSourceMode: 'model' | 'preset-sun' | 'preset-moon';
visualBackups: VisualBackupsState;
visualBaselines: VisualBaselinesState;
}
export interface UiControl {
item_id: string;
type: string;
label?: string;
name?: string;
binding?: string;
options?: string[] | string;
default?: unknown;
shortcut?: string[] | null;
}
export interface ViewerStore {
get(): ViewerState;
replace(state: Partial<ViewerState> | ViewerState): void;
update(mutator: (draft: ViewerState) => void): void;
subscribe(listener: (state: ViewerState) => void): () => void;
}
export interface BackendUiApplyPayload {
kind: 'ui';
id: string;
value: unknown;
control: UiControl;
}
export interface GestureApplyPayload {
kind: 'gesture';
mode: string;
phase?: string;
pointer?: Partial<GesturePointer>;
drag?: Partial<DragState>;
}
export type BackendApplyPayload = BackendUiApplyPayload | GestureApplyPayload;
export interface BackendSnapshot {
t: number;
rate: number;
measuredSlowdown?: number;
paused: boolean;
ngeom: number;
nq?: number;
nv?: number;
pausedSource?: string;
rateSource?: string;
gesture?: GestureState;
drag?: DragState;
voptFlags?: number[];
sceneFlags?: number[];
labelMode?: number;
frameMode?: number;
cameraMode?: number;
frameId?: number | null;
visual?: Record<string, unknown> | null;
visualDefaults?: Record<string, unknown> | null;
statistic?: Record<string, unknown> | null;
visualVersion?: number;
visualDefaultsVersion?: number;
statisticVersion?: number;
optionSupport?: { supported: boolean; pointers: string[] } | null;
renderAssets?: unknown | null;
groups?: ViewerGroupState | null;
cameras?: Array<Record<string, unknown>> | null;
geoms?: Array<Record<string, unknown>> | null;
geom_bodyid?: Int32Array | number[] | null;
body_parentid?: Int32Array | number[] | null;
body_jntadr?: Int32Array | number[] | null;
body_jntnum?: Int32Array | number[] | null;
jtype?: Int32Array | number[] | null;
nbody?: number;
njnt?: number;
scn_ngeom?: number;
nisland?: number;
info?: Record<string, unknown> | null;
contacts?: { n?: number; [key: string]: unknown } | null;
align?: AlignRuntimeState | null;
copyState?: CopyRuntimeState | null;
history?: {
captureHz?: number;
capacity?: number;
count?: number;
horizon?: number;
scrubIndex?: number;
live?: boolean;
} | null;
keyframes?: {
capacity?: number;
count?: number;
labels?: string[];
slots?: Array<{ index?: number; label?: string; kind?: string; available?: boolean }>;
lastSaved?: number;
lastLoaded?: number;
} | null;
watch?: {
field?: string;
index?: number;
value?: number | null;
min?: number | null;
max?: number | null;
samples?: number;
status?: string;
valid?: boolean;
summary?: string;
} | null;
watchSources?: Record<string, { length?: number; label?: string }>;
keyIndex?: number;
}
export interface ViewerBackend {
kind: string;
apply(payload: BackendApplyPayload): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
snapshot(): Promise<BackendSnapshot> | BackendSnapshot;
subscribe(listener: (snapshot: BackendSnapshot) => void): () => void;
step?(direction?: number): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
setCameraIndex?(index: number): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
setRunState?(run: boolean, source?: string): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
setRate?(rate: number, source?: string): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
setVisualState?(payload: { visual?: Record<string, unknown> | null; sceneFlags?: boolean[] | null }): Promise<BackendSnapshot | undefined> | BackendSnapshot | undefined;
dispose?(): void;
}
export {
DEFAULT_VIEWER_STATE,
createViewerStore,
applySpecAction,
applyGesture,
createBackend,
readControlValue,
cameraLabelFromIndex,
mergeBackendSnapshot,
switchVisualSourceMode,
} from './state.mjs';