Launch OpenGPEX

OpenGPEX 1.x documentation (legacy editor).

v2 Docs

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