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
InteractionTransactionwrapper (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
- Fast Track — Deep-dive into volatile state and synchronization
- State Management — The normalized store architecture
- Plugin API Reference — Command definition contract
Last updated: 2026-06-14