Skip to content

Editor Plugins

Overview

Editor plugins extend the Zephyr3d Editor itself. A plugin can add:

  • Main menu items and context menu items
  • Toolbar buttons
  • Custom edit tools
  • Custom property accessors
  • Plugin settings and persistent state

The current UI entry is Project -> Plugin Manager.... Despite the name, this panel manages externally installed editor plugins stored in the editor's global database and shared by all local projects in the same browser profile.


Installing Plugins

Open Project -> Plugin Manager..., then use one of the following actions:

  • Install... to install a packaged plugin from a .zip file
  • Install Folder... to install an unpacked plugin folder
  • Link... to link a plugin root folder directly into the editor for development (desktop editor only)
  • New Template... to generate a starter plugin package inside the editor

After a plugin is installed you can:

  • Enable or disable it with the checkbox in the plugin list
  • Open Browse Files... to inspect or edit plugin files
  • Open Install Package... to add a third-party npm dependency
  • Open Settings... if the plugin exposes a settings schema
  • Remove the plugin from the editor

Notes:

  • Install... and Install Folder... are intended for regular installation and distribution
  • Link... is for the desktop development workflow only and is not available in the browser editor
  • Link... links the plugin root folder, not src/ and not dist/

Package Layout

The recommended format is a multi-file plugin package with a plugin.json manifest at the package root:

text
my-editor-plugin/
  plugin.json
  index.ts
  icons/
    tool.svg
  utils/
    commands.ts

Example plugin.json:

json
{
  "id": "com.example.demo-plugin",
  "name": "Demo Plugin",
  "version": "0.1.0",
  "description": "Example editor plugin for Zephyr3d.",
  "entry": "index.ts"
}

Manifest fields:

  • id: required and must be unique
  • entry: required, relative path to the plugin entry module
  • name, version, description: optional but recommended
  • dependencies: optional third-party packages used by the plugin

Desktop Development Mode

In the desktop editor, development mode lets the editor load plugin sources directly, without building dist and reinstalling after each change.

Recommended folder layout:

text
my-editor-plugin/
  plugin.dev.json
  plugin.json
  src/
    index.ts
  libs/
    deps.lock.json
    deps/
  dist/
    index.js
    plugin.json

Where:

  • plugin.dev.json: development manifest, used by the desktop Link... action
  • plugin.json: release manifest, used by installable packages or the dist output
  • src/: plugin source entry
  • libs/deps/: third-party dependencies cached by the editor for linked plugins
  • dist/: built release output

Example development manifest plugin.dev.json:

json
{
  "id": "com.example.demo-plugin",
  "name": "Demo Plugin",
  "version": "0.1.0",
  "description": "Example editor plugin for Zephyr3d.",
  "entry": "src/index.ts",
  "dependencies": {
    "nanoid": "^5.0.0"
  }
}

Development workflow:

  1. Start the desktop editor development environment
  2. Open Project -> Plugin Manager...
  3. Click Link...
  4. Select the plugin root folder
  5. After changing the sources, click Refresh manually

Development mode characteristics:

  • The editor loads sources under src directly; running npm install first is not required
  • If plugin.dev.json declares third-party packages in dependencies, the editor downloads and caches them into libs/deps/ on first use
  • Subsequent refreshes reuse the local libs/deps cache and libs/deps.lock.json
  • A development-mode Refresh only revalidates plugin.dev.json and the entry dependency graph; it does not rescan the whole plugin folder

Caveats:

  • Third-party packages used in development mode must be declared in plugin.dev.json.dependencies
  • Source changes are not hot-reloaded after Link...; click Refresh manually
  • If the first dependency download happens offline and there is no local libs/deps cache, plugin loading fails

Release Mode

For distribution, use the regular package/build output workflow.

Release workflow:

  1. Install the plugin build dependencies
  2. Run the build to produce dist/
  3. Verify that dist/plugin.json, dist/index.js and other outputs are complete
  4. Install the .zip package with Install..., or install the release folder with Install Folder...

Release mode characteristics:

  • Uses built JavaScript output, which usually loads faster than development-mode sources
  • Better suited for distributing to other users or delivering stable versions
  • plugin.json should describe the release entry, for example dist/index.js or the entry file inside the release folder

Minimal Plugin

Import plugin types from @zephyr3d/editor/editor-plugin and export a default plugin definition:

ts
import type { EditorPluginDefinition } from '@zephyr3d/editor/editor-plugin';
import { SceneNode } from '@zephyr3d/scene';

const plugin: EditorPluginDefinition = {
  activate(ctx) {
    ctx.registerMenuItems({
      location: 'main',
      items: [
        {
          id: 'com.example.demo-plugin.menu',
          label: 'Demo Plugin',
          subMenus: [
            {
              id: 'com.example.demo-plugin.about',
              label: 'About...',
              action: async () => {
                await ctx.ui.message('Demo Plugin', 'The plugin is active.');
              }
            }
          ]
        }
      ]
    });

    ctx.registerMenuItems({
      location: 'scene-hierarchy',
      items: (menuCtx) => [
        {
          id: 'com.example.demo-plugin.add-empty-child',
          label: 'Add Empty Child',
          visible: () => menuCtx.target instanceof SceneNode,
          action: async () => {
            if (!(menuCtx.target instanceof SceneNode) || !menuCtx.scene) {
              return;
            }
            await menuCtx.scene.commands.addChildNode(menuCtx.target, SceneNode);
            menuCtx.scene.refreshProperties();
            menuCtx.scene.notifySceneChanged();
          }
        }
      ]
    });
  }
};

export default plugin;

Plugin API

EditorPluginContext is the main entry point passed to activate(ctx).

Common capabilities:

  • ctx.project: read and write project files, create directories, open code files
  • ctx.system: save plugin-global state and settings
  • ctx.ui: show messages, confirmations, and project file/folder pickers
  • ctx.registerMenuItems(...): contribute menu items
  • ctx.registerToolbarItem(...): contribute toolbar buttons
  • ctx.registerEditTool(...): register custom scene edit tools
  • ctx.registerPropertyAccessors(...): extend the property panel
  • ctx.on(...): listen to editor events such as scene open, selection change, and node updates
  • ctx.log(...): write plugin logs

Menu contributions receive an EditorMenuContext with:

  • scene: available in scene-related menus
  • assets: available in asset browser menus
  • target: the clicked node, file, or other target object

When you mutate scene data directly, call:

  • ctx.refreshProperties()
  • ctx.notifySceneChanged()

Scene Commands

Scene-related operations should go through EditorSceneContext.commands.

Available helpers include:

  • addChildNode()
  • addShapeNode()
  • instantiatePrefab()
  • deleteNode()
  • reparentNode()
  • cloneNode()
  • executeCommand()
  • executeUserCallback()

This API is available from scene-aware contexts such as menuCtx.scene and editCtx.scene.

Example:

ts
await menuCtx.scene.commands.executeCommand(new MyCustomCommand(...));

For quick undoable operations without a dedicated command class:

ts
await menuCtx.scene.commands.executeUserCallback(
  async () => {
    target.visible = false;
  },
  async () => {
    target.visible = true;
  }
);

Settings And State

Plugins can declare a settings schema on the definition:

ts
const plugin: EditorPluginDefinition = {
  settings: {
    endpoint: {
      type: 'string',
      label: 'API Endpoint',
      description: 'Base URL used by the plugin.'
    },
    autoSync: {
      type: 'boolean',
      label: 'Auto Sync',
      default: true
    }
  },
  activate(ctx) {
    // ...
  }
};

At runtime:

  • ctx.system.getSettings() / ctx.system.saveSettings() store plugin-global settings
  • ctx.system.getState() / ctx.system.saveState() store plugin-global state data
  • ctx.project.getSettings() / ctx.project.saveSettings() store project-specific data

Use system settings for values shared across projects, and project settings for values that belong to the current project.


Third-Party Packages

If a plugin needs an npm package:

  1. Open Project -> Plugin Manager...
  2. Select the plugin
  3. Click Install Package...
  4. Enter a package spec such as nanoid or nanoid@5

After installation you can import the package directly:

ts
import { nanoid } from 'nanoid';

Installed package versions are tracked in plugin.json under dependencies.

For desktop development-mode (linked) plugins:

  • Declare dependencies in plugin.dev.json instead
  • When you click Refresh, the editor automatically syncs missing dependencies into libs/deps/
  • Running npm install manually is generally not needed

Released under the MIT License.