Skip to content

Editor Plugins

The editor has a plugin system built on @web-engine-dev/editor-core's ExtensionRegistry. Plugins contribute to well-known extension points to add panels, toolbar actions, gizmos, component editors, context menus, and more.

Extension Registry

The ExtensionRegistry is a type-safe, signal-driven registry where plugins register contributions at named extension points. It tracks a version signal that increments on every change, enabling reactive UI updates.

typescript
import { ExtensionRegistry } from '@web-engine-dev/editor-core';

const registry = new ExtensionRegistry();

// Register a contribution
const unregister = registry.register('panels', {
  id: 'my-panel',
  title: 'My Panel',
  defaultPosition: 'left',
  create: (container) => {
    container.textContent = 'Hello from my plugin!';
    return { dispose: () => { container.textContent = ''; } };
  },
}, 'my-plugin-id');

// Query all contributions at a point
const panels = registry.getAll('panels');

// Query contributions by plugin
const myExtensions = registry.getByPlugin('my-plugin-id');

// Remove all contributions from a plugin
registry.unregisterPlugin('my-plugin-id');

// Unregister a single contribution
unregister();

Extension Points

The editor defines 15 extension points in the EditorExtensionPoints type map. Each point accepts a specific contribution interface.

panels

Register custom panels that appear in the dockview layout.

typescript
interface PanelContribution {
  id: string;
  title: string;
  icon?: string;
  defaultPosition?: 'left' | 'right' | 'bottom' | 'center';
  create: (container: HTMLElement) => { dispose: () => void };
}

componentEditors

Custom property editors for specific component types, replacing or augmenting the default inspector view.

typescript
interface ComponentEditorContribution {
  componentId: number;            // Component type ID
  priority?: number;              // Higher wins when multiple match
  create: (container: HTMLElement, props: ComponentEditorProps) => {
    dispose: () => void;
  };
}

toolbar

Actions added to the editor toolbar.

typescript
interface ToolbarContribution {
  id: string;
  label: string;
  icon?: string;
  section?: string;
  execute: () => void;
  isEnabled?: () => boolean;
}

gizmos

Visual handles rendered in the viewport for specific component types.

typescript
interface GizmoContribution {
  id: string;
  name: string;
  componentIds: number[];
  create: () => { dispose: () => void };
}

contextMenus

Additional items for context menus in specific locations.

typescript
interface ContextMenuContribution {
  location: string;  // 'hierarchy', 'viewport', 'inspector', etc.
  items: Array<{
    id: string;
    label: string;
    icon?: string;
    action: () => void;
    when?: string;   // When-expression for visibility
  }>;
}

viewportOverlays

Overlays rendered on top of the viewport canvas.

typescript
interface ViewportOverlayContribution {
  id: string;
  name: string;
  zOrder?: number;
  create: (container: HTMLElement) => { dispose: () => void };
}

assetImporters

Custom importers for file types not handled by built-in importers.

typescript
interface AssetImporterContribution {
  extensions: string[];      // e.g., ['.png', '.jpg']
  name?: string;
  priority?: number;         // Higher wins when multiple match
  import: (file: File) => Promise<AssetData>;
}

shortcuts

Register keyboard shortcuts that trigger commands.

typescript
interface ShortcutContribution {
  shortcuts: Array<{
    id: string;
    keys: string;            // e.g., 'Ctrl+S', 'Ctrl+Shift+Z'
    command: string;         // Command ID to execute
    when?: string;           // Activation context
    label?: string;
  }>;
}

commands

Register commands that can be invoked from the command palette, menus, or shortcuts.

typescript
interface CommandContribution {
  commands: Array<{
    id: string;
    title: string;
    icon?: string;
    category?: string;
    execute: (...args: unknown[]) => void | Promise<void>;
    isEnabled?: () => boolean;
  }>;
}

statusBar

Items displayed in the editor's status bar.

typescript
interface StatusBarContribution {
  create: () => {
    id: string;
    text: string;
    tooltip?: string;
    icon?: string;
    alignment: 'left' | 'right';
    priority?: number;
    onClick?: () => void;
  };
}

themes

Custom editor themes.

typescript
interface ThemeContribution {
  themes: Array<{
    id: string;
    name: string;
    base: 'dark' | 'light';
    tokens: Record<string, string>;
  }>;
}

buildHooks

Hooks into the build pipeline for pre-build configuration and post-build processing.

typescript
interface BuildHookContribution {
  preBuild?(context: BuildHookContext): Promise<void> | void;
  postBuild?(context: BuildHookContext, result: BuildHookResult): Promise<void> | void;
}

The BuildHookContext provides profileId, target, outputDir, development flag, and a mutable config object that hooks can modify.

assetPostProcessors

Process assets during the build pipeline.

typescript
interface AssetPostProcessorContribution {
  readonly extensions: string[];
  process(asset: BuildAsset, context: BuildHookContext): Promise<BuildAsset> | BuildAsset;
}

inspectionProviders

Custom data providers for the inspector panel.

inspectionRenderers

Custom renderers for inspector sections.

Plugin Lifecycle

Plugins are managed through the editor's plugin infrastructure. Each plugin has a manifest that declares its identity and capabilities:

typescript
interface PluginManifest {
  id: string;
  name: string;
  version: string;
  description?: string;
  author?: string;
  editorVersion?: string;
  dependencies?: Record<string, string>;
  capabilities?: {
    propertyEditors?: boolean;
    gizmos?: boolean;
    assetImporters?: boolean;
    menus?: boolean;
    toolbar?: boolean;
    panels?: boolean;
    components?: boolean;
    systems?: boolean;
  };
  configSchema?: {
    properties: Record<string, {
      type: 'string' | 'number' | 'boolean';
      default?: unknown;
      description?: string;
    }>;
  };
}

Plugin states flow through: unloaded -> loaded -> activated (or degraded / error).

The Plugin Manager panel displays all plugins with their current state and provides activate/deactivate controls.

Example: Complete Plugin

Here is a minimal plugin that registers a toolbar button and a command:

typescript
import type { ExtensionRegistry } from '@web-engine-dev/editor-core';

export function registerMyPlugin(registry: ExtensionRegistry): () => void {
  const disposers: Array<() => void> = [];

  // Register a command
  disposers.push(
    registry.register('commands', {
      commands: [{
        id: 'myPlugin.sayHello',
        title: 'Say Hello',
        category: 'My Plugin',
        execute: () => {
          console.log('Hello from my plugin!');
        },
      }],
    }, 'my-plugin'),
  );

  // Register a toolbar button
  disposers.push(
    registry.register('toolbar', {
      id: 'myPlugin.helloButton',
      label: 'Hello',
      icon: 'wand',
      section: 'tools',
      execute: () => {
        console.log('Toolbar button clicked!');
      },
    }, 'my-plugin'),
  );

  // Return cleanup function
  return () => {
    for (const dispose of disposers) dispose();
  };
}

Querying Extensions

Use getAll() to retrieve all contributions at an extension point:

typescript
// Get all registered panels
const panels = registry.getAll('panels');

// Get all toolbar items
const toolbarItems = registry.getAll('toolbar');

// Get all commands for the command palette
const commands = registry.getAll('commands');

The version signal on the registry increments on every registration change, so reactive frameworks can re-render when extensions are added or removed:

typescript
import { createEffect } from 'solid-js';

createEffect(() => {
  // Re-runs whenever extensions change
  registry.version();
  const panels = registry.getAll('panels');
  // Update UI...
});

Proprietary software. All rights reserved.