Actions & Commands

OpenGPEX enforces a strict 4-layer dispatch architecture for all state mutations. This ensures unidirectional data flow, type safety, performance isolation, and clean undo/redo semantics.


Architecture Overview

              ┌─────────────────────────────────────────┐
              │     State Mutation Request                │
              └───────────────────┬─────────────────────┘
                                  │
     ┌───────────────┬────────────┼────────────┬────────────────┐
     ▼               ▼            ▼            ▼                │
 1. Atomic       2. Fast Track  3. Facade    4. Command Bus     │
                                                                │
 actions.update* actions.fast.* actions.adv.* executeCommand()  │
     │               │            │            │                │
     ▼               ▼            ▼            ▼                │
 [Reducer]      [Volatile]    [Auto Undo]  [Cross-plugin]      │
 [Undo patch]   [No undo]    [Type guard] [Loose coupling]    │
 └──────────────────────────────────────────────────────────────┘

Layer 1: Atomic State Operations

The lowest-level direct state mutation channel.

Use for: Simple property changes (opacity, layer name), UI state toggles, camera updates.

Characteristics:

  • Direct reducer dispatch via Immer produce()
  • Generates undo/redo patches automatically
  • Synchronous only — no async logic

API

Method Description
addFrame(frame, switch?) Add an artboard
updateFrame(id, patch) Update frame properties
removeFrame(ids, nextId) Remove frames
addLayers(frameId, layers, idx?) Insert layers
updateLayer(frameId, layerId, patch) Update layer properties
batchUpdateLayers(frameId, patches) Batch layer updates
removeLayers(frameId, ids) Delete layers
updateCamera(frameId, camera) Set camera state
setInteraction(patch) Update interaction mode
updateUI(patch) Toggle UI configuration

Example

// Direct property mutation — produces 1 undo step
actions.updateLayer(frameId, layerId, { opacity: 0.5 });

// Camera update
actions.updateCamera(frameId, { x: 0, y: 0, k: 1 });

Layer 2: Fast Track (Volatile)

A zero-latency channel for 60fps interactions (drag, resize, brush strokes).

Use for: Real-time position/size updates during pointer-move events.

Characteristics:

  • Writes to a mutable volatile ref — no React re-render
  • No undo patches during interaction
  • Single undo step generated on commit()
  • Must use InteractionTransaction wrapper (never call raw API)

Correct Usage

const tx = new InteractionTransaction(event);
tx.begin();

// During drag (60fps, no undo):
tx.update(layerId, { x: newX, y: newY });

// On pointer-up (commit → 1 undo step):
tx.commit();

⚠️ See Fast Track for the full deep-dive on volatile state architecture.


Layer 3: Advanced Facade (actions.adv)

A strongly-typed facade for complex multi-step operations that combine multiple atomic actions into a single logical undo step.

Use for: System-level compound operations (rotate, flip, physical crop, merge layers, register assets).

Characteristics:

  • Groups multiple mutations into one undo step
  • Full TypeScript generics for input/output types
  • Handles validation, error boundaries, and rollback

API Examples

Method Description
adv.rotateLayer(frameId, layerId, deg) Rotate with matrix recalculation
adv.flipLayer(frameId, layerId, axis) Flip horizontally/vertically
adv.physicalCrop(frameId, layerId, box) Destructive crop with asset rebuild
adv.mergeDown(frameId, layerId) Merge layer into the one below
adv.fitToFrame(frameId, layerId) Scale layer to fit frame bounds
adv.registerAsset(blob, meta) CAS-store an image asset

Example

// Rotate layer 90° — internally updates matrix, bounds, and asset
// Produces exactly 1 undo step despite multiple internal mutations
await actions.adv.rotateLayer(frameId, layerId, 90);

Layer 4: Command Bus (executeCommand)

A loosely-coupled event bus for cross-plugin communication and extensible command dispatch.

Use for: Plugin-defined commands, keyboard shortcuts, menu actions, inter-plugin messaging.

Characteristics:

  • Commands are identified by UID (globally unique)
  • Scoped auto-prefix within plugin context (short IDs resolve to own namespace)
  • Supports interceptors for command pipeline customization
  • Async-capable

Dispatching

// Within a plugin — short ID auto-prefixed with plugin UID:
actions.executeCommand('cmd.toggle_mode');
// Resolves to: "opengpex.drawers.my_tool.cmd.toggle_mode"

// Cross-plugin — must use full UID:
actions.executeCommand('opengpex.panels.layer_panel.cmd.select_layer', {
  layerId: 'abc123'
});

Defining Commands

// In commands.ts
export const commands: EditorCommand[] = [
  {
    id: 'cmd.reset',
    displayName: 'Reset Tool',
    execute: (ctx, payload) => {
      const { actions, scoped } = ctx;
      scoped.updateConfig({ mode: 'default' });
      actions.setInteraction({ activeTool: null });
    },
  },
];

Choosing the Right Layer

Scenario Layer Why
Change opacity slider Atomic Simple property, needs undo
Drag layer across canvas Fast Track 60fps, commit on drop
Rotate image 90° Facade Multi-step, single undo
Keyboard shortcut triggers tool Command Bus Decoupled, extensible
Plugin A notifies Plugin B Command Bus Cross-plugin messaging
Toggle sidebar visibility Atomic UI state, instant

Next Steps


Last updated: 2026-06-14