ADR-012: Editor Plugin Extension Architecture
Status: Accepted Revision: v2 Implementation: Partial First accepted: 2026-01-01 · Last amended: 2026-07-26 Relates to: ADR-007 (plugin capabilities) · #600
Context
The KeenEyes Editor is being built as a modular, plugin-based application. Currently, editor features like the Inspector, Hierarchy, and Viewport panels are implemented as static classes tightly coupled to EditorApplication. This creates several problems:
Limited Extensibility
Third-party developers cannot add custom panels, property drawers, gizmos, or menu items without modifying editor source code.
Monolithic Structure
All editor features are bundled together. Users cannot disable unused features or load only what they need.
Testing Difficulty
Editor components are hard to test in isolation because they depend on the full EditorApplication context.
Inconsistent Patterns
The runtime IWorldPlugin system provides a clean capability-based pattern (ADR-007), but the editor has no equivalent architecture.
Decision
Introduce IEditorPlugin and IEditorContext interfaces that mirror the runtime plugin architecture, extended with editor-specific capabilities.
Core Interfaces
public interface IEditorPlugin
{
string Name { get; }
string Version { get; }
string? Description { get; }
void Initialize(IEditorContext context);
void Shutdown();
}
public interface IEditorContext
{
// Core services (read-only access, interface-typed)
IEditorWorldManager Worlds { get; }
ISelectionManager Selection { get; }
IUndoRedoManager UndoRedo { get; }
IAssetDatabase Assets { get; }
IWorld EditorWorld { get; }
ILogQueryable? Log { get; } // null when no queryable log provider is configured
// Extension storage (mirrors IPluginContext)
void SetExtension<T>(T extension) where T : class;
T GetExtension<T>() where T : class;
bool TryGetExtension<T>(out T? extension) where T : class;
bool RemoveExtension<T>() where T : class;
// Capability access (mirrors ADR-007)
T GetCapability<T>() where T : class, IEditorCapability;
bool TryGetCapability<T>(out T? capability) where T : class, IEditorCapability;
bool HasCapability<T>() where T : class, IEditorCapability;
// Event subscriptions
EventSubscription OnSceneOpened(Action<IWorld> handler);
EventSubscription OnSceneClosed(Action handler);
EventSubscription OnSelectionChanged(Action<IReadOnlyList<Entity>> handler);
EventSubscription OnPlayModeChanged(Action<EditorPlayState> handler);
}
Core services are exposed through interfaces (IEditorWorldManager, ISelectionManager, IUndoRedoManager, IAssetDatabase) rather than the concrete manager types originally sketched, and an ILogQueryable? Log service was added. Not yet implemented: the originally proposed EditorProject Project service — no EditorProject type exists.
Editor Capabilities
Following ADR-007's pattern, editor features are defined as capability interfaces. Nine ship: the seven originally proposed plus two added during implementation:
| Capability | Purpose |
|---|---|
IInspectorCapability |
Register property drawers, component inspectors |
IViewportCapability |
Add gizmo renderers, overlays, pick handlers |
IMenuCapability |
Add menu items, context menus, toolbar buttons |
IPanelCapability |
Register dockable panels |
IAssetCapability |
Custom asset importers, thumbnails |
IShortcutCapability |
Register keyboard shortcuts |
IToolCapability |
Register viewport tools (select, move, etc.) |
INotificationCapability |
Editor notifications/toasts (added post-proposal) |
IExtendedPanelCapability |
Extended panel management (added post-proposal) |
Capability Interface Definitions
IInspectorCapability
public interface IInspectorCapability : IEditorCapability
{
void RegisterPropertyDrawer(Type fieldType, PropertyDrawer drawer);
void RegisterPropertyDrawer<T>(PropertyDrawer drawer);
void RegisterDrawerForAttribute<TAttribute>(PropertyDrawer drawer)
where TAttribute : Attribute;
void RegisterComponentInspector<TComponent>(IComponentInspector inspector);
void RegisterComponentActions<TComponent>(IComponentActionProvider provider);
}
IViewportCapability
public interface IViewportCapability : IEditorCapability
{
void AddGizmoRenderer(IGizmoRenderer renderer);
void RemoveGizmoRenderer(IGizmoRenderer renderer);
void AddOverlay(string id, IViewportOverlay overlay);
void SetOverlayVisible(string id, bool visible);
void AddPickHandler(IPickHandler handler);
void RemovePickHandler(IPickHandler handler);
}
public interface IGizmoRenderer
{
int Order { get; }
bool IsVisible { get; }
void Render(GizmoRenderContext context);
}
IGizmoRenderer.Render receives the current selection through GizmoRenderContext rather than as a separate parameter. Not yet implemented: camera-mode registration (RegisterCameraMode(string id, ICameraMode mode)) from the original proposal — no ICameraMode type exists.
IMenuCapability
public interface IMenuCapability : IEditorCapability
{
void AddMenuItem(MenuPath path, EditorCommand command);
void AddContextMenuItem<T>(MenuPath path, EditorCommand<T> command);
void AddToolbarButton(ToolbarSection section, EditorCommand command);
bool RemoveMenuItem(MenuPath path); // true if the item existed
}
// MenuPath is a readonly struct of path segments,
// constructed via new MenuPath(params string[]) or parsed from a string:
public readonly struct MenuPath
{
public MenuPath(params string[] segments) { ... }
public static MenuPath Parse(string path) { ... } // e.g. "File/Export/Scene"
public MenuPath Parent { get; }
public string Name { get; }
public bool IsRoot { get; }
}
The originally sketched per-menu static factories (MenuPath.File(...), MenuPath.Edit(...), etc.) were not implemented; segment construction and Parse cover those cases.
IPanelCapability
public interface IPanelCapability : IEditorCapability
{
void RegisterPanel<T>(PanelDescriptor descriptor) where T : IEditorPanel, new();
void RegisterPanel(PanelDescriptor descriptor, Func<IEditorPanel> factory);
void OpenPanel(string id);
void ClosePanel(string id);
bool IsPanelOpen(string id);
void FocusPanel(string id);
}
public interface IEditorPanel : IDisposable
{
void Initialize(PanelContext context);
void Update(float deltaTime);
void Shutdown();
}
public class PanelDescriptor
{
public required string Id { get; init; }
public required string Title { get; init; }
public string? Icon { get; init; }
public PanelDockLocation DefaultLocation { get; init; }
public bool OpenByDefault { get; init; }
public float MinWidth { get; init; }
public float MinHeight { get; init; }
public float DefaultWidth { get; init; }
public float DefaultHeight { get; init; }
public string? Category { get; init; }
public ShortcutBinding? ToggleShortcut { get; init; }
}
As shipped, PanelDescriptor is an init-property class rather than the positional record originally sketched, and IEditorPanel follows an Initialize(PanelContext) / Update / Shutdown lifecycle instead of building UI directly via a CreateUI(IWorld, Entity, FontHandle) method. A factory-based RegisterPanel(descriptor, Func<IEditorPanel>) overload and FocusPanel(id) were added.
Source-Generated Extensions
Following the PluginExtensionAttribute pattern, provide typed access to editor extensions:
[AttributeUsage(AttributeTargets.Class)]
public sealed class EditorExtensionAttribute(string propertyName) : Attribute
{
public string PropertyName { get; } = propertyName;
}
// Usage:
[EditorExtension("Physics")]
public sealed class PhysicsEditorExtension
{
public void ShowColliderBounds(bool visible) { ... }
}
// Generated extension member:
extension(IEditorContext context)
{
public PhysicsEditorExtension Physics
=> context.GetExtension<PhysicsEditorExtension>();
}
Plugin Lifecycle
EditorApplication starts
↓
Load plugin assemblies
↓
For each IEditorPlugin:
├── Create IEditorContext
├── Call plugin.Initialize(context)
└── Track registered resources
↓
Editor runs
↓
On shutdown:
├── For each plugin (reverse order):
│ └── Call plugin.Shutdown()
└── Dispose tracked resources
The shipped lifecycle matches this flow and extends it beyond the original proposal:
- Dynamic discovery and loading in collectible, isolated load contexts (
PluginLoader,PluginLoadContext,PluginRepository) - Permission-based security —
PluginPermissionflags gate capability access viaPermissionManagerandSecurePluginContext, with signature verification (PluginSignatureVerifier) andPermissionDeniedExceptionon violations - Hot reload with state preservation — plugins implementing
IStatefulPluginsave and restore state across reloads EditorPluginBaseconvenience base class for plugin authors
See Editor Plugin Development for the full authoring guide.
Built-in Plugins
Core editor features are implemented as internal plugins:
| Plugin | Provides | Status |
|---|---|---|
CoreEditorPlugin |
Selection, undo/redo, basic commands | ✅ Implemented |
InspectorPlugin |
Component inspector, built-in property drawers | ✅ Implemented, unit-tested |
HierarchyPlugin |
Scene tree panel | ✅ Implemented, unit-tested |
ViewportPlugin |
3D/2D viewport, transform gizmos, grid | ✅ Implemented |
ConsolePlugin |
Log panel | ✅ Implemented |
ProfilerPlugin |
System timing panel | Not implemented — frame inspection lives in the static FrameInspectorPanel |
ProjectPlugin |
Asset browser panel | ✅ Implemented |
PluginManagerPlugin |
Plugin management panel (added post-proposal) | ✅ Implemented |
These serve as reference implementations for third-party plugins. Note, however, that EditorApplication has not yet migrated to them: the shipping editor still constructs its panels through the static panel classes (HierarchyPanel.Create, ViewportPanel.Create, InspectorPanel.Create, etc.), and the only plugins installed at startup via EditorPluginManager are the gizmo plugins (NavigationEditorPlugin, AnimationEditorPlugin). Completing the panel migration remains open work.
Consequences
Positive
- Extensibility - Third parties can add panels, drawers, gizmos, menu items
- Modularity - Editor features are isolated and independently testable
- Consistency - Same capability pattern as runtime plugins (ADR-007)
- Discoverability - Capability interfaces document available extension points
- Clean shutdown - Plugin resources are tracked and disposed properly
Negative
- Migration effort - Existing editor code needs refactoring to plugin pattern
- Indirection - Accessing features requires capability lookup
- Learning curve - Plugin authors must understand capability system
Neutral
- Performance - Interface dispatch overhead is negligible for editor code
- Gradual adoption - Can migrate panels one at a time
Implementation Phases
Phase 1: Core Abstractions ✅
- [x] Create
IEditorPlugin,IEditorContextinterfaces - [x] Create
IEditorCapabilitymarker interface - [x] Create
EditorPluginManagerfor lifecycle management (wired intoEditorApplication)
Phase 2: Capability Interfaces ✅
- [x]
IInspectorCapabilitywith PropertyDrawer registration - [x]
IMenuCapabilitywith menu/toolbar registration - [x]
IPanelCapabilitywith panel registration
Phase 3: Viewport Capabilities ✅ (except camera modes)
- [x]
IViewportCapabilityfor gizmos and overlays - [x]
IToolCapabilityfor viewport tools - [x]
IShortcutCapabilityfor keybindings - Not yet implemented: camera-mode registration (
RegisterCameraMode/ICameraMode)
Phase 4: Asset Capabilities ✅
- [x]
IAssetCapabilityfor importers - [x] Thumbnail generators
- [x] Drag-drop handlers
Phase 5: Built-in Plugin Refactoring (partial)
- [x] Convert InspectorPanel to InspectorPlugin
- [x] Convert HierarchyPanel to HierarchyPlugin
- [x] Convert ViewportPanel to ViewportPlugin
- Not yet implemented: installing these plugins in
EditorApplication— the live editor still builds panels via the static panel classes, so the plugin versions are written and tested but not yet the shipping code path
Related
- ADR-007: Capability-Based Plugin Architecture
- Scene Editor Architecture
- Editor Plugin Development — authoring guide for this architecture
- Editor Documentation
- Epic #600: Scene/World Editor
Changelog
- v2 — 2026-07-26 (living-ADR conversion): Status corrected Proposed → Accepted — the architecture is implemented and documented as the editor's supported extension surface. Implementation marked Partial:
RegisterCameraMode/ICameraMode,ProfilerPlugin, and theEditorProjectProjectcontext service were never built, and the built-in panel plugins, though implemented and unit-tested, are not yet installed byEditorApplication(the live editor still uses static panel classes). Body amended to as-built API shapes (interface-typedIEditorContextservices plusLog,MenuPathas a segment struct,PanelDescriptor/IEditorPanelInitialize/Update/Shutdown lifecycle,IGizmoRenderer.Render(GizmoRenderContext)), the two added capabilities (INotificationCapability,IExtendedPanelCapability), and the shipped lifecycle extensions (dynamic loading, permission-based security, hot reload). - v1 — 2026-01-01 (#600): Proposed — IEditorPlugin/IEditorContext capability-based editor plugin architecture mirroring the runtime plugin pattern (ADR-007), with capability interfaces, source-generated extensions, and built-in plugin refactoring plan.