Command & Actions
Specifications for the dispatching layer (actions, actions.adv, and actions.fast).
Core State Actions (Layer 1: Atomic)
Atomic mutations on the state tree. These methods produce undo patches.
Frame Operations
| Method | Description |
|---|---|
actions.addFrame(frame, switch?) |
Add an artboard to the project |
actions.updateFrame(id, patch) |
Update frame properties (canvas size, name, etc.) |
actions.removeFrame(ids, nextId) |
Remove frames from the project |
actions.switchFrame(id) |
Switch active frame |
Layer Operations
| Method | Description |
|---|---|
actions.addLayers(frameId, layers, idx?) |
Insert layers at specified index |
actions.updateLayer(frameId, layerId, patch) |
Update layer properties |
actions.batchUpdateLayers(frameId, patches) |
Batch update multiple layers (patches: Record<string, Partial<Layer>> keyed by layer id) |
actions.removeLayers(frameId, ids) |
Delete layers |
actions.reorderLayers(frameId, oldIndex, newIndex) |
Rearrange Z-order |
UI & Interaction
| Method | Description |
|---|---|
actions.updateCamera(frameId, camera) |
Set camera state (pan, zoom) |
actions.setInteraction(patch) |
Update interaction mode |
actions.setStateSignal(key, value) |
Write a signal value to the interaction state |
actions.updatePluginConfig(uid, patch) |
Write plugin configuration |
Plugin Management
| Method | Description |
|---|---|
actions.registerPlugin(plugin) |
Register a new plugin into the runtime |
actions.executeCommand(id, payload?) |
Dispatch a command by UID |
Example
// Direct property mutation — produces 1 undo step
actions.updateLayer(frameId, layerId, { opacity: 0.5 });
// Batch update — produces 1 undo step for all changes
actions.batchUpdateLayers(frameId, {
l1: { visible: false },
l2: { blendMode: 'multiply' },
});
Advanced Facades (Layer 3: actions.adv)
Strongly-typed, multi-step operations wrapped as command facades. Each facade produces exactly one undo step regardless of internal complexity.
⚠️ Always use
actions.adv.*for system-level operations. Do NOT call system commands by raw string ID.
Namespace Tree
actions.adv
├── viewport
│ ├── transform
│ │ ├── rotate.execute({ direction: 'left' | 'right' })
│ │ ├── rotateLeft.execute()
│ │ ├── rotateRight.execute()
│ │ ├── flip.execute({ direction: 'horizontal' | 'vertical' })
│ │ ├── flipH.execute()
│ │ ├── flipV.execute()
│ │ └── reset.execute()
│ └── translate
│ ├── fit.execute()
│ ├── actualSize.execute()
│ └── zoom.execute(k) # payload is a number
├── frame
│ ├── create
│ │ ├── trunk.execute({ source, switchFrame?, extra? })
│ │ ├── branch
│ │ │ ├── fromFile.execute({ source, extra? })
│ │ │ └── fromSelection.execute()
│ │ ├── revert.execute()
│ │ ├── remove.execute(id?)
│ │ ├── export.execute(frame)
│ │ └── import.execute({ state, assetBlobs, replaceId?, switchFrame? })
│ └── resize
│ ├── resizeCanvas.execute()
│ ├── resample.execute({ targetDim, dpi? })
│ └── replace.execute({ source, dpi? })
├── layer
│ ├── toggle
│ │ ├── all.execute()
│ │ └── others.execute(activeLayerId)
│ ├── clip
│ │ ├── cut.execute()
│ │ ├── copy.execute()
│ │ ├── paste.execute(metadata? | { e? })
│ │ ├── drill.execute({ feather? })
│ │ └── toMask.execute({ layerId?, feather? })
│ ├── cmdj
│ │ ├── copy.execute({ feather? })
│ │ └── cut.execute({ feather? })
│ ├── peel
│ │ ├── peelToExchange.execute({ isCopy })
│ │ └── discardExchange.execute()
│ ├── mask
│ │ ├── toggle.execute({ layerId, maskId, frameId? })
│ │ ├── invert.execute({ layerId, frameId? })
│ │ ├── remove.execute({ layerId, maskId, frameId? })
│ │ └── clearAll.execute({ layerId, frameId? })
│ ├── bitmapMask
│ │ ├── add.execute({ layerId, src, assetId, bounds, frameId? })
│ │ ├── update.execute({ layerId, maskId, patch, frameId? })
│ │ ├── toggle.execute({ layerId, maskId, frameId? })
│ │ ├── remove.execute({ layerId, maskId, frameId? })
│ │ └── clearAll.execute({ layerId, frameId? })
│ └── merge
│ ├── mergeHost.execute()
│ ├── down.execute()
│ ├── visible.execute()
│ └── rasterize.execute({ layerId? })
└── system
└── assets
└── sync.execute({ force? })
Each adv Node Properties
| Property | Type | Description |
|---|---|---|
.execute(payload?) |
Function | Execute the system command |
.execute.noundo(payload?) |
Function | Execute without the pre-execution SIGNAL_COMMIT undo checkpoint (for gestures that own their own undo boundary) |
.name |
string | Human-readable command name (from PluginService) |
.shortcutLabel |
string | Formatted shortcut (e.g., "⌘Z", "Ctrl+Shift+J") |
.id |
string | Underlying command UID (for debugging only) |
Example
const { actions } = useEditorServices();
// Create a new frame from a file
const frameId = await actions.adv.frame.create.trunk.execute({ source: file });
// Viewport operations
actions.adv.viewport.translate.fit.execute();
actions.adv.viewport.transform.rotateLeft.execute();
actions.adv.viewport.translate.zoom.execute(2.0);
// Layer operations
await actions.adv.layer.merge.down.execute();
await actions.adv.layer.clip.toMask.execute({ feather: 2 });
actions.adv.layer.peel.peelToExchange.execute({ isCopy: true });
actions.adv.layer.mask.toggle.execute({ layerId, maskId });
await actions.adv.system.assets.sync.execute({ force: true });
Low-Latency Fast Track (Layer 2: actions.fast)
60fps volatile operations that bypass React completely during interactions. Used for drag, zoom, brush strokes, and any high-frequency pointer tracking.
Read Methods (Safe — no side effects)
| Method | Returns | Description |
|---|---|---|
fast.latestLayer(frameId, layerId) |
Layer | null |
Read merged (persistent + volatile) layer |
fast.latestFrame(id) |
Frame | null |
Read merged frame |
fast.latestCamera(id) |
CameraState |
Read merged camera |
fast.isInteracting() |
boolean |
Check if a transaction is active |
fast.getTransient(key) |
Record<string, unknown> | undefined |
Read ephemeral helper data |
fast.getCursor() |
string | null |
Read cursor override |
Write Methods (During interaction only)
| Method | Description |
|---|---|
fast.override(frameId, id, props, type?) |
Write volatile override (layer/frame/project). ⚠️ Deprecated for plugin use — prefer the InteractionTransaction wrapper |
fast.commit(id?, type?) |
Fold volatile → store (produces 1 undo step). ⚠️ Deprecated for plugin use — prefer InteractionTransaction |
fast.setTransient(key, data) |
Store ephemeral helper data |
fast.setCursor(cursor) |
Set cursor override (null = default logic) |
fast.setHover(layerId, isHoveringActive?) |
Set hovered layer |
fast.signal(frameId) |
Force canvas repaint |
fast.reset() |
Clear all volatile state |
fast.subscribeInteraction(key, cb) |
Subscribe to a volatile interaction field change; returns unsubscribe function |
Example
// During pointer-move (60fps, bypasses React):
actions.fast.override(frameId, layerId, { cx: newCx, cy: newCy }, 'layer');
// On pointer-up (fold into store → 1 undo step):
actions.fast.commit(layerId, 'layer');
Command Bus (Layer 4: executeCommand)
Loosely-coupled cross-plugin communication via the unified command dispatch.
Dispatch Behavior
// Within a plugin scope — short ID auto-prefixed with plugin UID:
actions.executeCommand('cmd.toggle_mode');
// → resolves to: "author.plugin_id.cmd.toggle_mode"
// Cross-plugin — use full UID (import from target plugin's protocols.ts):
import { CLIP_OPTIONS_CMD_RESET_BOX } from '../options/ClipOptions/protocols';
actions.executeCommand(CLIP_OPTIONS_CMD_RESET_BOX);
Defining Commands in Plugin
// commands.ts
import type { EditorCommand, EditorContextValue } from '@opengpex/editor/core/types';
export const commands: EditorCommand[] = [
{
id: 'cmd.reset',
displayName: 'Reset Tool',
undoable: true,
execute: (ctx: EditorContextValue, payload?: { soft?: boolean }) => {
// Access plugin-scoped context
ctx.scoped!.setSelfConfig({ mode: 'default' });
ctx.scoped!.setSignal('signal.active_tool', 'brush');
},
},
];
Command Execution Flow
executeCommand(id, payload)
├── 1. Run plugin interceptors (beforeExecute, try/caught; skipped for 'adv.*' ids)
├── 2. Resolve command in global registry by UID
├── 3. If undoable and a frame is active → dispatch SIGNAL_COMMIT (undo checkpoint)
├── 4. Locate owning plugin → build scopedContext
│ (selfConfig, setSelfConfig, getSignal, setSignal, toggleSignal, setBusy)
└── 5. command.execute(ctx, payload) — try/caught, errors never propagate
Last updated: 2026-08-30