Plugin System
How OpenGPEX achieves full extensibility through a metadata-driven plugin registry — where every UI element, tool, and feature is a plugin, including all built-in functionality.
Design Philosophy
OpenGPEX follows strict Inversion of Control (IoC): the core framework provides layout slots and lifecycle management, while plugins provide all business functionality. The editor ships zero hardcoded UI — every panel, tool, overlay, and backstage service is a plugin.
┌──────────────────────────────────────┐
│ Core Workspace (Framework) │
│ │
│ ┌─────────────┐ ┌───────────────┐ │
│ │ Slot Mgr │ │ Registry │ │
│ │ (renders) │◄─┤ (discovers) │ │
│ └─────────────┘ └───────┬───────┘ │
│ │ │
└───────────────────────────┼───────────┘
│ auto-scan
┌───────────────────┼───────────────────┐
▼ ▼ ▼
[LayerPanel] [CropTool] [FileLoader]
[UndoPlugin] [TextTool] [AdjustDrawer]
... ...
Dual-Track Registry
Plugins are registered in two independent registries, loaded in sequence:
| Registry | Location | Purpose |
|---|---|---|
| Core Registry | core/plugin/registry.ts |
Built-in official plugins, build-tool maintained |
| User Registry | plugins/registry-user.ts |
Local/community plugins, auto-scanned |
The PluginService unifies both registries for lifecycle management (init → activate → deactivate → dispose).
Plugin Metadata Contract
Every plugin exports an EditorPlugin object containing:
manifest— Identity (author, id, version, description). Runtime UID =author.idslot— Where the plugin renders (sidebar, overlay, options bar, hidden, etc.)component— React component for the plugin's UI (if any)commands— Dispatchable command definitions (keyboard shortcuts, menu actions)interactions— Gesture handlers with priority-based routingonAction— Interceptor for global state actions (reactive side-effects)config— Default configuration with scoped persistence
Slot System
Plugins are rendered into physical layout slots:
| Slot | Location | Examples |
|---|---|---|
| SIDE_BAR | Left/right panel area | Layer Panel, Adjustment Drawer |
| OPTION_BAR | Top toolbar | Crop Options, Viewport Options |
| VIEWPORT_OVERLAY | Over the canvas | Crop handles, Transform gizmos |
| STAGE_OVERLAY | Behind layers | Pixel grid, Smart guides |
| HIDDEN | No UI (background service) | TimeTraveler, AutoSave |
| DOCK | Bottom dock | Tab navigation |
Command System
Plugins define commands identified by scoped UIDs:
- Within a plugin: Short IDs auto-resolve to own namespace (e.g.,
cmd.reset→opengpex.drawers.my_tool.cmd.reset) - Cross-plugin: Full UID required (e.g.,
opengpex.panels.layer_panel.cmd.select_layer)
Commands support interceptors, async execution, and keyboard shortcut binding.
Interaction Handlers
Plugins register gesture handlers with a priority-based routing system:
| Priority Range | Use Case |
|---|---|
| 100–200 | Overlay handles (crop box, resize corners) |
| 50–99 | Active tool interactions (brush, text) |
| 10–49 | Layer selection & move |
| 0–9 | Viewport pan/zoom (fallback) |
The dispatcher tests handlers in priority order; first match wins and owns the entire gesture lifecycle (start → move → end).
Safety & Isolation
- Error boundaries: Plugin rendering errors are caught per-slot, preventing cascade crashes
- Scoped state: Each plugin gets isolated config storage via
scoped.updateConfig() - Action broadcasting: Plugin
onActionhooks run asynchronously, cannot block the dispatch pipeline - Busy state: Long-running plugin operations expose a standardized busy indicator without blocking the editor
Last updated: 2026-07-30