Plugin System
How OpenGPEX achieves full extensibility through a metadata-driven plugin registry — every UI element, tool, and feature is a plugin, including all built-in functionality. This subsystem carries over from v1 largely intact; v2 extends it with dynamic loading, safe mode, and version gating.
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) │ │
│ └─────────────┘ └───────┬───────┘ │
└───────────────────────────┼───────────┘
│ registration chain
┌───────────────────┼────────────────────┐
▼ ▼ ▼
[LayersDrawer] [ColorOptions] [CloudMenu]
[SmartGuides] [FileLoader] [TabDock]
... ...
Registration Chain
At workspace init, plugins register in a fixed order:
- Core registry — 33 built-in plugins, generated by a build-tool sync step (
pnpm sync-plugins) and registered first. - User registry — local plugins from the repository's user registry file.
- Dynamic user plugins — fetched at runtime via an internal plugin list endpoint and imported as ES modules; shared modules are exposed on a well-known window global.
Safety Modes
?safe-mode=trueloads core plugins only.- User plugins are disabled by default and individually opt-in (persisted in localStorage).
- Each plugin declares core-version compatibility; incompatible plugins are gated off at load.
- Uninstall paths uniformly deregister commands, shortcuts, and contributions.
Plugin Descriptor Contract
Every plugin exports a descriptor containing:
manifest— identity (author, id, version, description); runtime UID derives from the author idslot— mount point (see table below)side/order— drawer side and sort order; crossing multiples of 100 auto-inserts separatorscomponent— React component for the plugin's UIcommands— dispatchable command definitions (shortcuts, menu actions)signals— reactive state signals the plugin subscribes tocontributions— additional slot mounts beyond the primary slotinitialConfig— defaults incl. preferred sidebar width; scoped persistence per plugin
Slot System
v2 keeps the slot model and refines the physical layout. Slots map to page regions as follows:
| Slot | Page location | Examples |
|---|---|---|
SIDE_BAR |
Floating left/right drawer panels (320px default, do not squeeze the canvas) | Layers, Adjustment, AI Tools drawers |
OPTION_BAR |
Top centered pill toolbar, content follows the active tool | Color / Clip / Viewport options |
TOOL_MENU / TOOL_SETTINGS |
Main menu panel and its settings submenu | Smart guides toggle, settings panels |
XTEND_SLOT |
Top-right extend button popover | Cloud menu |
DOCK |
Full viewport-area overlay (pointer-events none by default) | Tab dock |
TL/TR/BL/BR |
Viewport corner HUD | Zoom controls |
VIEWPORT_OVERLAY |
Over the canvas viewport | Smart guides |
STAGE_OVERLAY / STAGE_GIZMOS |
Tool interaction layer above the transformed canvas | Brush, text editor, crop handles |
ROOT_OVERLAY |
Workspace root | File loader, onboarding |
HIDDEN |
No UI — backstage services | Font loader, TimeTraveler |
Rendering is centralized in one PluginSlot component: it collects plugins by slot plus contributions, sorts by order, and wraps each in an error boundary + Suspense + plugin context. Visibility is gated on frame existence (e.g. show: "frame-required").
Floating UI never pushes the canvas around ad hoc: all spatial negotiation goes through a LayoutContext in which regions register push/overlay slots, a stability state machine settles measurements, and the resulting safe rectangle is bridged back into Redux and CSS variables.
Command System
Commands are identified by scoped UIDs:
- Within a plugin: short IDs auto-resolve to the plugin's namespace (e.g.
cmd.reset→opengpex.drawers.my_tool.cmd.reset) - Cross-plugin: full UID required
Commands support interceptors, async execution, and automatic keyboard-shortcut registration from the manifest.
Interaction Handlers
Plugins register gesture handlers with priority-based routing:
| 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 render failures are caught per-slot and cannot cascade.
- Scoped state — each plugin gets isolated config storage.
- Action broadcasting — plugin action hooks run asynchronously and cannot block the dispatch pipeline.
- Busy state — long-running operations expose a standardized busy indicator without freezing the editor.
- Services, not singletons — plugins receive stable service handles (engine, assets, files, geometry) through context; they never import engine internals directly.
Last updated: 2026-10-12