# draw.io Custom Shapes with JavaScript Logic — Architecture Analysis ## Executive Summary **Can you build custom shapes with JS logic and import them as a library into draw.io?** **YES** — via the **Plugin system**. draw.io supports fully programmable shapes written in JavaScript that can be loaded as plugins in both the desktop (Electron) app and self-hosted/web instances. These plugins can define shapes with conditional rendering, computed geometry, dynamic behavior, and expose them in the sidebar palette for drag-and-drop use. The standard "library import" (File > Open Library) only supports declarative XML — but plugins give you **more** power, not less. --- ## Table of Contents 1. [Shape Systems Overview](#shape-systems-overview) 2. [XML Stencils (Declarative)](#xml-stencils-declarative) 3. [JavaScript Shape Classes (Programmatic)](#javascript-shape-classes-programmatic) 4. [The Plugin Architecture](#the-plugin-architecture) 5. [Loading Plugins in Desktop App](#loading-plugins-in-desktop-app) 6. [Loading Plugins in Web/Self-Hosted](#loading-plugins-in-webself-hosted) 7. [Shape Registration Flow](#shape-registration-flow) 8. [Canvas API Reference](#canvas-api-reference) 9. [Limitations and Considerations](#limitations-and-considerations) 10. [Comparison Matrix](#comparison-matrix) --- ## Shape Systems Overview draw.io has **two completely separate shape definition systems**: | System | Format | JS Logic? | Import Method | |--------|--------|-----------|---------------| | XML Stencils | `.xml` stencil files | ❌ No (declarative only) | `mxStencilRegistry` / Open Library | | JS Shape Classes | `.js` files | ✅ Full programmatic | Plugin system / `mxCellRenderer.registerShape()` | When a cell is rendered, the resolution order is: ``` Cell style "shape=NAME" → 1. mxCellRenderer.defaultShapes[NAME] → JS Shape class 2. mxStencilRegistry.stencils[NAME] → XML Stencil instance 3. Dynamic loading (if enabled) → Load from library registry ``` --- ## XML Stencils (Declarative) Located in: `src/main/webapp/stencils/*.xml` XML stencils define shapes using a drawing vocabulary: ```xml ``` ### Capabilities - Drawing primitives: `move`, `line`, `quad`, `curve`, `arc`, `rect`, `roundrect`, `ellipse`, `close` - Styling: `strokecolor`, `fillcolor`, `fontcolor`, `alpha`, `strokewidth`, `dashed` - State: `save` / `restore` - Sub-shapes: `` ### Limitations - **No loops, conditionals, or computed geometry** - **No JavaScript execution** (except limited `allowEval` for text/image values) - Purely static vector drawing instructions --- ## JavaScript Shape Classes (Programmatic) Located in: `src/main/webapp/shapes/*.js` (~47 files) Examples: `mxBootstrap.js`, `mxKubernetes.js`, `mxElectrical.js`, `mxNetworks.js`, `mxRack.js` JS shapes extend `mxShape` and have **full programmatic power**: ```javascript function MyShape(bounds, fill, stroke, strokewidth) { mxShape.call(this); this.bounds = bounds; this.fill = fill; this.stroke = stroke; this.strokewidth = (strokewidth != null) ? strokewidth : 1; } mxUtils.extend(MyShape, mxShape); MyShape.prototype.paintVertexShape = function(c, x, y, w, h) { c.translate(x, y); // Full JS: conditionals, loops, math, style reading var mode = mxUtils.getValue(this.style, 'mode', 'default'); var segments = parseInt(mxUtils.getValue(this.style, 'segments', '4')); if (mode === 'alert') { c.setFillColor('#ff4444'); } // Computed geometry for (var i = 0; i < segments; i++) { var segH = h / segments; c.rect(0, i * segH, w, segH - 2); c.fillAndStroke(); } }; // Register globally mxCellRenderer.registerShape('mxgraph.myLib.myShape', MyShape); ``` ### Capabilities - Conditional rendering based on style properties - Loops and computed geometry - Math and dynamic calculations - Reading per-instance style parameters via `mxUtils.getValue(this.style, ...)` - Full canvas API access - Custom connection points - Override `getConstraints()` for dynamic connection point generation --- ## The Plugin Architecture ### Plugin Structure Every draw.io plugin follows this pattern: ```javascript // Shape definitions (execute immediately at load time) function CustomShape() { /* ... */ } mxUtils.extend(CustomShape, mxShape); CustomShape.prototype.paintVertexShape = function(c, x, y, w, h) { /* ... */ }; mxCellRenderer.registerShape('mxgraph.custom.shapeName', CustomShape); // UI integration (executes when draw.io is ready) Draw.loadPlugin(function(ui) { // ui = EditorUi instance — full access to the application // Add sidebar palette var fns = [ ui.sidebar.createVertexTemplateEntry( 'shape=mxgraph.custom.shapeName;fillColor=#dae8fc;', 120, 80, '', 'My Custom Shape', null, null, 'search keywords' ) ]; ui.sidebar.addPaletteFunctions('customPalette', 'My Custom Library', true, fns); // Can also: add menus, actions, event listeners, modify graph behavior }); ``` ### What `Draw.loadPlugin(callback)` Provides The callback receives `ui` (EditorUi instance) with access to: | Object | Purpose | |--------|---------| | `ui.editor.graph` | The mxGraph instance (model, view, styles) | | `ui.sidebar` | Sidebar/palette management | | `ui.menus` | Menu system | | `ui.actions` | Action registry | | `ui.editor` | Editor instance | | Global: `mxShape`, `mxCellRenderer`, `mxUtils`, `mxConstants` | All mxGraph APIs | ### Production Example: `rackF5.js` The `plugins/rackF5.js` file (1674 lines) is a real-world plugin that: - Defines ~20+ shape classes with full JS rendering - Uses conditionals based on style (`hasEars`, `isFront`, `isDC`, `psNum`) - Registers all shapes with `mxCellRenderer.registerShape()` - Adds a complete sidebar palette via `Draw.loadPlugin()` --- ## Loading Plugins in Desktop App The draw.io Electron app (desktop) has **full plugin support** via **Extras > Plugins**: ### Method 1: Built-in Plugins A dropdown of pre-packaged plugins (`App.publicPlugin`): - `ex` (explore), `tips`, `svgdata`, `number`, `sql`, `props`, `text`, `anim`, `update`, `trees`, `replay`, `anon`, `webcola`, `tags` ### Method 2: External Plugins (Custom `.js` Files) 1. **Enable external plugins** (one-time): - The app checks `requestSync('isPluginsEnabled')` - If disabled, it shows "pluginsDisabled" message - Enable via app preferences or configuration 2. **Install your plugin**: - Extras > Plugins → "Select File..." - Native file dialog opens, filtered for `.js` files - Plugin is copied to App Data folder (`installPlugin` action) - Registered in `mxSettings` 3. **Restart draw.io** — plugin loads on next startup ### Loading sequence (from source): ``` App startup → mxSettings.getPlugins() returns saved plugin list → App.initPluginCallback() creates Draw object + queue → For each plugin: ├─ Built-in (starts with ./plugins/) → load relative path └─ External → requestSync('getPluginFile') → load from file:// App Data → Scripts loaded via