Launch OpenGPEX

Plugin API Reference

Complete reference for all hooks, context services, and APIs available to plugin developers.


Core Hooks

All hooks are imported from @opengpex/editor/core/context:

import {
  useEditorServices,
  useEditorState,
  usePluginCommands,
  usePluginSignals,
  usePluginSelfConfig,
  usePluginConfig,
  usePluginSelfBusy,
  usePluginResource,
  usePluginList,
  useVolatileInteraction,
} from '@opengpex/editor/core/context';

useEditorServices()

Returns static, stable service references. Never triggers React re-renders.

const { actions, pixels, geometry, layers, assets, clipboard, plugins, volatileRef } = useEditorServices();
Property Type Description
actions EditorActions Command execution, state mutations, advanced system facade
pixels PixelService Bitmap decode, rasterize, filter, Worker compositing
geometry GeometryService Coordinate transforms, bounding boxes, snap alignment
layers LayerService Layer CRUD with safety checks (expand composites, cascade delete)
assets AssetService Asset registration, URL resolution, lifecycle management
clipboard ClipboardService Clipboard read/write
plugins PluginService Plugin/command/shortcut registry queries
volatileRef MutableRefObject Direct mutable ref for 60fps fast-track state

actions.executeCommand(id, payload?)

Dispatches a command by UID. Within a plugin context, short IDs are auto-resolved:

// Within plugin scope — short ID auto-resolves to full UID
actions.executeCommand('cmd.apply_color', { color: '#ff0000' });

// Cross-plugin — must use full UID
actions.executeCommand('opengpex.drawers.image_info.cmd.download');

actions.adv.* — System Advanced Facade

For system-level operations, use the strongly-typed actions.adv facade instead of raw command IDs:

const { actions } = useEditorServices();

// Viewport operations
actions.adv.viewport.transform.rotate.execute({ direction: 'left' });
actions.adv.viewport.translate.fit.execute();
actions.adv.viewport.translate.zoom.execute({ level: 2.0 });

// Frame operations
await actions.adv.frame.create.trunk.execute({ source: file });
actions.adv.frame.resize.resizeCanvas.execute({ width: 1920, height: 1080 });

// Layer operations
actions.adv.layer.merge.down.execute();
actions.adv.layer.mask.toggle.execute();
actions.adv.layer.clip.copy.execute();

// System operations
const asset = await actions.adv.system.assets.register.execute(blob);

Each adv node provides: .execute(payload?), .name, .shortcutLabel, .id


useEditorState()

Subscribes to reactive editor state. Re-renders when subscribed values change.

const { state, activeFrame, activeLayer, getSignal } = useEditorState();
Property Type Description
state EditorState Full reducer state (frames, layers, pluginConfig, interaction)
activeFrame Frame | null Currently active frame/artboard
activeLayer Layer | null Currently active layer
getSignal(key, default?) (key, default?) => T Read signal (auto-prefixes plugin UID in plugin scope)

⚠️ Only destructure the specific fields you need — subscribing to the entire state object causes excessive re-renders.


usePluginCommands()

Returns all commands registered by the current plugin, mapped to camelCase + Cmd suffix keys.

const { applyColorCmd, resetAllCmd } = usePluginCommands();

// Each command is an object:
applyColorCmd.execute({ color: '#ff0000' });  // Execute with payload
applyColorCmd.name;                            // Human-readable name
applyColorCmd.shortcutLabel;                   // Formatted shortcut (e.g., "⌘Z")

Naming Rules

The key is derived from cmd.id:

  1. Split by ., _, -
  2. Remove leading "cmd" token (if present)
  3. CamelCase remaining tokens
  4. Append "Cmd" suffix
cmd.id Generated Key
cmd.apply_color applyColorCmd
cmd.reset_all_grading resetAllGradingCmd
cmd.begin_curves_edit beginCurvesEditCmd

usePluginSignals()

Returns all signals registered by the current plugin, mapped to camelCase + Signal suffix keys.

const { activeGradingToolSignal, openSignal } = usePluginSignals();

// Read reactive value
const currentTool = activeGradingToolSignal.value;

// Write new value
activeGradingToolSignal.set('curves');

Naming Rules

Same as commands but with "Signal" suffix:

  1. Split by ., _, -
  2. Remove leading "signal" token (if present)
  3. CamelCase remaining tokens
  4. Append "Signal" suffix
sig.id Generated Key
signal.active_grading_tool activeGradingToolSignal
signal.is_generating isGeneratingSignal

usePluginSelfConfig()

Reads and updates the current plugin's configuration. Zero arguments — auto-resolves from PluginContext.

const [config, setConfig] = usePluginSelfConfig<ColorPickerConfig>();

// Read
console.log(config.currentColor);

// Merge-patch update
setConfig({ currentColor: '#00ff00' });

usePluginConfig(pluginId)

Reads another plugin's configuration by its UID:

const [otherConfig, setOtherConfig] = usePluginConfig<OtherConfig>('opengpex.drawers.adjustment');

⚠️ Modifying another plugin's config directly is discouraged. Prefer the Command Bus for cross-plugin communication.


usePluginSelfBusy()

Returns the reactive busy state of the current plugin. Used to show loading spinners.

const isBusy = usePluginSelfBusy();
// → true when ctx.scoped.setBusy(true) has been called

When busy, the sidebar icon automatically shows a rainbow gradient breathing animation.


usePluginResource(relativePath)

Resolves a static asset path bundled with the plugin:

const logoUrl = usePluginResource('visuals/logo.svg');
return <img src={logoUrl} alt="Logo" />;

Path resolution: /api/plugins/serve/${_folderName}/${relativePath}

Works identically in development (Track 1) and production (Track 2) — the serve API implements dual-path fallback.


usePluginList()

Returns a reactive snapshot of all registered plugins (for building management UIs):

const allPlugins = usePluginList();
// Re-renders when plugins are registered/unregistered

useVolatileInteraction(key)

Subscribe to high-frequency interaction state without global re-renders:

const hoveredLayerId = useVolatileInteraction('hoveredLayerId');
const cursor = useVolatileInteraction('cursorOverride');

Command Context (ctx in execute)

When a command's execute function runs, it receives the full EditorContextValue:

execute: (ctx: EditorContextValue, payload?: P) => {
  // Core services (same as useEditorServices)
  ctx.actions      // State mutations, command dispatch
  ctx.layers       // Layer CRUD (prefer over raw actions for safety)
  ctx.geometry     // Coordinate transforms
  ctx.pixels       // Pixel operations & Worker bridge
  ctx.assets       // Asset management
  ctx.clipboard    // Clipboard operations
  ctx.plugins      // Plugin registry queries

  // Plugin-scoped namespace (only present when command belongs to a plugin)
  ctx.scoped!.selfConfig              // Current plugin config
  ctx.scoped!.setSelfConfig(patch)    // Update plugin config
  ctx.scoped!.getSignal(key, default) // Read signal (auto-prefixes UID)
  ctx.scoped!.setSignal(key, value)   // Write signal
  ctx.scoped!.toggleSignal(key)       // Toggle boolean signal
  ctx.scoped!.setBusy(boolean)        // Set plugin busy state
}

Prefer ctx.layers over ctx.actions

When manipulating layers in commands, prefer the high-level ctx.layers service:

Method Benefit over raw actions
ctx.layers.addLayer(frameId, layer) Auto-expands composite layers, validates structure
ctx.layers.activate(frameId, layerId) Guards against activating non-activatable layers (masks)
ctx.layers.removeLayers(frameId, ids) Cascade-deletes children, auto-migrates focus

Only use ctx.actions.addLayers etc. when you intentionally need to bypass safety checks.


Performance Guidelines

Hook Re-render Behavior
useEditorServices() Never re-renders — purely static references
useEditorState() Re-renders on state changes (scope with destructuring)
usePluginCommands() Re-renders only when plugin scope changes (rare)
usePluginSignals() Re-renders when any plugin signal changes
usePluginSelfConfig() Re-renders when this plugin's config changes
usePluginSelfBusy() Re-renders when busy state changes

Fast-Track (60fps Interactions)

For drag, zoom, and brush interactions — never dispatch Redux actions in pointermove:

const { volatileRef } = useEditorServices();
// Direct mutable write — bypasses React, zero re-renders
volatileRef.current.cx = nextX;
volatileRef.current.cy = nextY;

Next Steps


Last updated: 2026-07-30