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:
- Split by
.,_,- - Remove leading
"cmd"token (if present) - CamelCase remaining tokens
- 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:
- Split by
.,_,- - Remove leading
"signal"token (if present) - CamelCase remaining tokens
- 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
- Your First Plugin — Tutorial using these APIs
- Signals & Cross-Plugin Communication — Deeper dive into signals
- Plugin System Overview — Architecture context
Last updated: 2026-07-30