feat: draw.io custom shapes analysis + buildable example plugin
- ANALYSIS.md: Full architecture analysis of draw.io shape systems (XML stencils vs JS shapes, plugin loading, desktop/web support) - Example plugin with 5 custom shapes demonstrating JS logic: - StatusIndicator: conditional rendering based on status param - ProgressBar: computed geometry (percentage-based fill) - DataFlowArrow: dynamic arrows with direction/speed params - HexagonCluster: loop-based hexagonal grid generation - MetricGauge: radial gauge with arc computation + zones - Webpack build pipeline (npm run build / build:dev / watch) - Pre-built dist/drawio-custom-shapes.js ready for installation - Full README with installation instructions for desktop + web
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
dist/*.map
|
||||||
+422
@@ -0,0 +1,422 @@
|
|||||||
|
# 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
|
||||||
|
<shapes name="mxgraph.rack.Oracle">
|
||||||
|
<shape name="Server" w="19.5" h="121" aspect="variable">
|
||||||
|
<connections>
|
||||||
|
<constraint x="0.5" y="0" perimeter="0" name="top"/>
|
||||||
|
<constraint x="0.5" y="1" perimeter="0" name="bottom"/>
|
||||||
|
</connections>
|
||||||
|
<background>
|
||||||
|
<rect x="0" y="0" w="19.5" h="121"/>
|
||||||
|
</background>
|
||||||
|
<foreground>
|
||||||
|
<fillstroke/>
|
||||||
|
<rect x="2" y="5" w="15.5" h="20"/>
|
||||||
|
<stroke/>
|
||||||
|
</foreground>
|
||||||
|
</shape>
|
||||||
|
</shapes>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Capabilities
|
||||||
|
- Drawing primitives: `move`, `line`, `quad`, `curve`, `arc`, `rect`, `roundrect`, `ellipse`, `close`
|
||||||
|
- Styling: `strokecolor`, `fillcolor`, `fontcolor`, `alpha`, `strokewidth`, `dashed`
|
||||||
|
- State: `save` / `restore`
|
||||||
|
- Sub-shapes: `<include-shape name="...">`
|
||||||
|
|
||||||
|
### 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 <script> tags
|
||||||
|
→ Draw.loadPlugin(callback) pushes to queue
|
||||||
|
→ Once UI ready: queue callbacks invoked with EditorUi instance
|
||||||
|
```
|
||||||
|
|
||||||
|
### Security
|
||||||
|
- Paths containing `..` are rejected
|
||||||
|
- External plugins stored in sandboxed App Data folder
|
||||||
|
- Non-built-in plugins blocked unless explicitly enabled
|
||||||
|
|
||||||
|
### App Data locations:
|
||||||
|
- **Linux**: `~/.config/draw.io/plugins/`
|
||||||
|
- **Windows**: `%APPDATA%\draw.io\plugins\`
|
||||||
|
- **macOS**: `~/Library/Application Support/draw.io/plugins/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Loading Plugins in Web/Self-Hosted
|
||||||
|
|
||||||
|
### URL Parameter
|
||||||
|
```
|
||||||
|
https://your-drawio.com/?p=pluginKey
|
||||||
|
```
|
||||||
|
Where `pluginKey` maps to a path in `App.pluginRegistry`.
|
||||||
|
|
||||||
|
### Configuration JSON
|
||||||
|
In draw.io configuration (Extras > Configuration):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": ["https://your-server.com/path/to/plugin.js"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Self-Hosted Docker
|
||||||
|
Set environment variable:
|
||||||
|
```
|
||||||
|
DRAWIO_PLUGINS_ALLOW_CUSTOM=true
|
||||||
|
```
|
||||||
|
|
||||||
|
Then configure via URL params or configuration JSON.
|
||||||
|
|
||||||
|
### `ALLOW_CUSTOM_PLUGINS`
|
||||||
|
In `Init.js`, `ALLOW_CUSTOM_PLUGINS` must be `true` for non-built-in plugin URLs. Self-hosted instances can set this; app.diagrams.net (cloud) restricts it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shape Registration Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────────────────────┐
|
||||||
|
│ Plugin .js file loaded (via <script> or eval) │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. Shape class defined (extends mxShape) │
|
||||||
|
│ 2. paintVertexShape() implemented with JS logic │
|
||||||
|
│ 3. mxCellRenderer.registerShape(name, Constructor) │
|
||||||
|
│ → stored in mxCellRenderer.defaultShapes[name] │
|
||||||
|
│ │
|
||||||
|
│ 4. Draw.loadPlugin(function(ui) { ... }) │
|
||||||
|
│ → Callback queued until UI ready │
|
||||||
|
│ → When invoked: │
|
||||||
|
│ a. Creates palette entries referencing shape names │
|
||||||
|
│ b. ui.sidebar.addPaletteFunctions(...) │
|
||||||
|
│ c. Shapes appear in sidebar for drag-and-drop │
|
||||||
|
│ │
|
||||||
|
├────────────────────────────────────────────────────────────┤
|
||||||
|
│ At render time: │
|
||||||
|
│ Cell style: "shape=mxgraph.myLib.myShape;param=value;" │
|
||||||
|
│ → mxCellRenderer looks up defaultShapes['mxgraph.myLib.myShape'] │
|
||||||
|
│ → Instantiates shape, calls paintVertexShape(c,x,y,w,h) │
|
||||||
|
│ → Shape reads style params, draws conditionally │
|
||||||
|
└────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Canvas API Reference
|
||||||
|
|
||||||
|
The `c` parameter in `paintVertexShape(c, x, y, w, h)` is an `mxAbstractCanvas2D` instance:
|
||||||
|
|
||||||
|
### Drawing
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `c.begin()` | Start a new path |
|
||||||
|
| `c.moveTo(x, y)` | Move to point |
|
||||||
|
| `c.lineTo(x, y)` | Line to point |
|
||||||
|
| `c.quadTo(x1, y1, x2, y2)` | Quadratic curve |
|
||||||
|
| `c.curveTo(x1, y1, x2, y2, x3, y3)` | Cubic curve |
|
||||||
|
| `c.arcTo(rx, ry, angle, largeArc, sweep, x, y)` | Arc |
|
||||||
|
| `c.close()` | Close path |
|
||||||
|
| `c.rect(x, y, w, h)` | Rectangle |
|
||||||
|
| `c.roundrect(x, y, w, h, dx, dy)` | Rounded rectangle |
|
||||||
|
| `c.ellipse(x, y, w, h)` | Ellipse |
|
||||||
|
|
||||||
|
### Rendering
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `c.fill()` | Fill current path |
|
||||||
|
| `c.stroke()` | Stroke current path |
|
||||||
|
| `c.fillAndStroke()` | Fill and stroke |
|
||||||
|
|
||||||
|
### Style
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `c.setFillColor(color)` | Set fill color |
|
||||||
|
| `c.setStrokeColor(color)` | Set stroke color |
|
||||||
|
| `c.setStrokeWidth(width)` | Set stroke width |
|
||||||
|
| `c.setFontColor(color)` | Set text color |
|
||||||
|
| `c.setFontSize(size)` | Set font size |
|
||||||
|
| `c.setFontFamily(family)` | Set font family |
|
||||||
|
| `c.setAlpha(alpha)` | Set opacity (0-1) |
|
||||||
|
| `c.setDashed(dashed)` | Toggle dashing |
|
||||||
|
| `c.setDashPattern(pattern)` | Set dash pattern |
|
||||||
|
| `c.setLineCap(cap)` | Line cap style |
|
||||||
|
| `c.setLineJoin(join)` | Line join style |
|
||||||
|
| `c.setShadow(shadow)` | Toggle shadow |
|
||||||
|
| `c.setGradient(c1, c2, x, y, w, h, dir, a1, a2)` | Set gradient fill |
|
||||||
|
|
||||||
|
### Transform & State
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `c.translate(x, y)` | Translate origin |
|
||||||
|
| `c.rotate(theta, flipH, flipV, cx, cy)` | Rotate |
|
||||||
|
| `c.scale(s)` | Scale |
|
||||||
|
| `c.save()` | Save state |
|
||||||
|
| `c.restore()` | Restore state |
|
||||||
|
|
||||||
|
### Text & Images
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `c.text(x, y, w, h, str, align, valign, wrap, format, overflow, clip, rotation, dir)` | Draw text |
|
||||||
|
| `c.image(x, y, w, h, src, aspect, flipH, flipV)` | Draw image |
|
||||||
|
|
||||||
|
### Reading Style Values
|
||||||
|
```javascript
|
||||||
|
var value = mxUtils.getValue(this.style, 'paramName', 'defaultValue');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Limitations and Considerations
|
||||||
|
|
||||||
|
### What Plugins CAN Do
|
||||||
|
- ✅ Define shapes with full JavaScript logic (conditionals, loops, computed geometry)
|
||||||
|
- ✅ Read per-instance style parameters for dynamic rendering
|
||||||
|
- ✅ Add sidebar palettes for drag-and-drop
|
||||||
|
- ✅ Add menus, actions, keyboard shortcuts
|
||||||
|
- ✅ Listen to graph events (selection, edit, etc.)
|
||||||
|
- ✅ Modify export behavior
|
||||||
|
- ✅ Override core graph methods
|
||||||
|
- ✅ Access and manipulate the full graph model
|
||||||
|
|
||||||
|
### What Plugins CANNOT Do
|
||||||
|
- ❌ Be loaded via "File > Open Library" (that's XML-only)
|
||||||
|
- ❌ Run in sandboxed/restricted mode on app.diagrams.net (cloud)
|
||||||
|
- ❌ Persist custom shape data in the `.drawio` file beyond style strings
|
||||||
|
- ❌ Add new file format support easily
|
||||||
|
- ❌ Work across draw.io versions without potential API breakage (no stable plugin API contract)
|
||||||
|
|
||||||
|
### Security Notes
|
||||||
|
- Plugins execute with full page privileges
|
||||||
|
- External plugins require explicit user opt-in
|
||||||
|
- The desktop app sandboxes plugin storage to App Data
|
||||||
|
- Self-hosted instances control plugin trust via `ALLOW_CUSTOM_PLUGINS`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison Matrix
|
||||||
|
|
||||||
|
| Feature | XML Library | XML Stencil | JS Plugin |
|
||||||
|
|---------|-------------|-------------|-----------|
|
||||||
|
| Conditional rendering | ❌ | ❌ | ✅ |
|
||||||
|
| Computed geometry | ❌ | ❌ | ✅ |
|
||||||
|
| Loop-based drawing | ❌ | ❌ | ✅ |
|
||||||
|
| Style-reactive | ❌ | Limited | ✅ |
|
||||||
|
| Sidebar palette | ✅ | ✅ | ✅ |
|
||||||
|
| Drag-and-drop | ✅ | ✅ | ✅ |
|
||||||
|
| File > Open Library | ✅ | ⚠️ (via registry) | ❌ |
|
||||||
|
| Desktop app support | ✅ | ✅ | ✅ |
|
||||||
|
| Web app support | ✅ | ✅ | ⚠️ (config needed) |
|
||||||
|
| User install difficulty | Easy | Moderate | Moderate |
|
||||||
|
| Custom connection points | ❌ (fixed) | ✅ (XML) | ✅ (dynamic) |
|
||||||
|
| Event handling | ❌ | ❌ | ✅ |
|
||||||
|
| Menu integration | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
For building custom shapes **with JavaScript logic** that behave as a reusable library in draw.io:
|
||||||
|
|
||||||
|
1. **Write a plugin** (`.js` file) that defines shape classes and registers them
|
||||||
|
2. **Load it** via Extras > Plugins (desktop) or configuration JSON (web/self-hosted)
|
||||||
|
3. **Shapes appear** in the sidebar palette, fully interactive with JS-powered rendering
|
||||||
|
|
||||||
|
The plugin system is the officially supported, production-proven mechanism — used internally by draw.io for shapes like F5 rack equipment, and by the ~47 shape library files bundled with the application.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Analysis performed on draw.io source (https://github.com/jgraph/drawio) — commit at time of analysis.*
|
||||||
@@ -1,3 +1,159 @@
|
|||||||
# drawio-custom-shapes
|
# draw.io Custom Shapes Example Plugin
|
||||||
|
|
||||||
Analysis and example plugin for building custom draw.io shapes with JavaScript logic
|
A fully buildable example plugin demonstrating how to create custom draw.io shapes with JavaScript logic.
|
||||||
|
|
||||||
|
## What This Is
|
||||||
|
|
||||||
|
A draw.io plugin that registers custom shapes with **programmatic rendering** — shapes that use conditionals, loops, computed geometry, and style-reactive behavior. Unlike XML stencils (which are purely declarative), these shapes have full JavaScript power.
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Development build (with source maps)
|
||||||
|
npm run build:dev
|
||||||
|
|
||||||
|
# Production build (minified)
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Watch mode (rebuild on changes)
|
||||||
|
npm run watch
|
||||||
|
```
|
||||||
|
|
||||||
|
Output: `dist/drawio-custom-shapes.js`
|
||||||
|
|
||||||
|
## Installation in draw.io
|
||||||
|
|
||||||
|
### Desktop App (Electron)
|
||||||
|
|
||||||
|
1. Build the plugin: `npm run build`
|
||||||
|
2. Open draw.io desktop
|
||||||
|
3. **Extras > Plugins**
|
||||||
|
4. Enable external plugins if prompted
|
||||||
|
5. Click **"Select File..."** → navigate to `dist/drawio-custom-shapes.js`
|
||||||
|
6. Restart draw.io
|
||||||
|
7. Find "Custom Shapes Example" palette in the sidebar
|
||||||
|
|
||||||
|
### Self-Hosted (Docker)
|
||||||
|
|
||||||
|
1. Build the plugin: `npm run build`
|
||||||
|
2. Copy `dist/drawio-custom-shapes.js` to your draw.io plugins directory
|
||||||
|
3. Set environment: `DRAWIO_PLUGINS_ALLOW_CUSTOM=true`
|
||||||
|
4. Add to configuration:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"plugins": ["/plugins/drawio-custom-shapes.js"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
5. Restart the container
|
||||||
|
|
||||||
|
### Web (URL parameter)
|
||||||
|
|
||||||
|
If you host the plugin file at a URL:
|
||||||
|
```
|
||||||
|
https://your-drawio.com/?p=YOUR_PLUGIN_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
├── src/
|
||||||
|
│ ├── index.js # Plugin entry point (Draw.loadPlugin wrapper)
|
||||||
|
│ ├── shapes/
|
||||||
|
│ │ ├── StatusIndicator.js # Conditional rendering based on status
|
||||||
|
│ │ ├── ProgressBar.js # Computed geometry (percentage-based)
|
||||||
|
│ │ ├── DataFlowArrow.js # Dynamic arrow with animation markers
|
||||||
|
│ │ ├── HexagonCluster.js # Loop-based hexagonal grid
|
||||||
|
│ │ └── MetricGauge.js # Radial gauge with computed arcs
|
||||||
|
│ └── palette.js # Sidebar palette registration
|
||||||
|
├── dist/ # Build output
|
||||||
|
├── webpack.config.js # Build configuration
|
||||||
|
├── package.json
|
||||||
|
├── ANALYSIS.md # Full architecture analysis
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Shape Catalog
|
||||||
|
|
||||||
|
### 1. Status Indicator (`customShapes.statusIndicator`)
|
||||||
|
A shape that changes color and icon based on a `status` style parameter.
|
||||||
|
- Styles: `status=ok|warning|error|unknown`
|
||||||
|
- Demonstrates: conditional rendering, style reading
|
||||||
|
|
||||||
|
### 2. Progress Bar (`customShapes.progressBar`)
|
||||||
|
A horizontal bar that fills based on a percentage value.
|
||||||
|
- Styles: `progress=0..100`, `barColor=#4CAF50`
|
||||||
|
- Demonstrates: computed geometry, value-based rendering
|
||||||
|
|
||||||
|
### 3. Data Flow Arrow (`customShapes.dataFlowArrow`)
|
||||||
|
A directional arrow with animated flow indicators.
|
||||||
|
- Styles: `direction=right|left|both`, `flowSpeed=fast|medium|slow`
|
||||||
|
- Demonstrates: dynamic path generation, pattern rendering
|
||||||
|
|
||||||
|
### 4. Hexagon Cluster (`customShapes.hexCluster`)
|
||||||
|
A grid of hexagons that adapts to the cell size.
|
||||||
|
- Styles: `hexCount=7`, `hexFill=#E3F2FD`
|
||||||
|
- Demonstrates: loop-based drawing, mathematical geometry
|
||||||
|
|
||||||
|
### 5. Metric Gauge (`customShapes.metricGauge`)
|
||||||
|
A radial gauge (speedometer-style) showing a value within a range.
|
||||||
|
- Styles: `value=75`, `minVal=0`, `maxVal=100`, `zones=green,yellow,red`
|
||||||
|
- Demonstrates: arc computation, gradient zones, text rendering
|
||||||
|
|
||||||
|
## Customizing
|
||||||
|
|
||||||
|
### Adding a New Shape
|
||||||
|
|
||||||
|
1. Create a new file in `src/shapes/YourShape.js`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { registerShape, getValue } from '../utils';
|
||||||
|
|
||||||
|
export function YourShapeName() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(YourShapeName, mxShape);
|
||||||
|
|
||||||
|
YourShapeName.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
// Read style parameters
|
||||||
|
var myParam = getValue(this.style, 'myParam', 'default');
|
||||||
|
|
||||||
|
// Draw with full JS logic
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(0, 0);
|
||||||
|
c.lineTo(w, h);
|
||||||
|
c.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register the shape
|
||||||
|
registerShape('customShapes.yourShape', YourShapeName);
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Import it in `src/index.js`
|
||||||
|
3. Add a palette entry in `src/palette.js`
|
||||||
|
4. Rebuild: `npm run build`
|
||||||
|
|
||||||
|
### Style Parameters
|
||||||
|
|
||||||
|
Shapes read parameters from the cell's style string:
|
||||||
|
```
|
||||||
|
shape=customShapes.yourShape;myParam=value;fillColor=#fff;
|
||||||
|
```
|
||||||
|
|
||||||
|
Access in code:
|
||||||
|
```javascript
|
||||||
|
var val = mxUtils.getValue(this.style, 'myParam', 'default');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture Notes
|
||||||
|
|
||||||
|
See [ANALYSIS.md](./ANALYSIS.md) for the full technical analysis of how draw.io's shape systems work, including the stencil registry, cell renderer, and plugin loading mechanisms.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Generated
+1437
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "drawio-custom-shapes",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Example draw.io plugin with custom JavaScript shapes",
|
||||||
|
"main": "dist/drawio-custom-shapes.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --mode production",
|
||||||
|
"build:dev": "webpack --mode development",
|
||||||
|
"watch": "webpack --mode development --watch",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"keywords": ["drawio", "draw.io", "plugin", "shapes", "mxgraph"],
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {
|
||||||
|
"webpack": "^5.90.0",
|
||||||
|
"webpack-cli": "^5.1.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* draw.io Custom Shapes Example Plugin
|
||||||
|
*
|
||||||
|
* This is the main entry point. It:
|
||||||
|
* 1. Defines and registers custom shape classes (immediately)
|
||||||
|
* 2. Wraps sidebar palette setup in Draw.loadPlugin (deferred until UI ready)
|
||||||
|
*
|
||||||
|
* Global dependencies (provided by draw.io at runtime):
|
||||||
|
* - mxShape, mxUtils, mxCellRenderer, mxConstants (mxGraph library)
|
||||||
|
* - Draw.loadPlugin (draw.io plugin API)
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Import shape definitions — each registers itself via mxCellRenderer.registerShape()
|
||||||
|
import './shapes/StatusIndicator';
|
||||||
|
import './shapes/ProgressBar';
|
||||||
|
import './shapes/DataFlowArrow';
|
||||||
|
import './shapes/HexagonCluster';
|
||||||
|
import './shapes/MetricGauge';
|
||||||
|
|
||||||
|
// Import and execute palette registration
|
||||||
|
import { registerPalette } from './palette';
|
||||||
|
|
||||||
|
// Register the plugin with draw.io
|
||||||
|
Draw.loadPlugin(function(ui) {
|
||||||
|
registerPalette(ui);
|
||||||
|
console.log('[CustomShapes] Plugin loaded successfully — 5 shapes registered');
|
||||||
|
});
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* Palette Registration
|
||||||
|
*
|
||||||
|
* Creates a sidebar palette section with all custom shapes available
|
||||||
|
* for drag-and-drop onto the canvas.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function registerPalette(ui) {
|
||||||
|
var sb = ui.sidebar;
|
||||||
|
|
||||||
|
// Define palette entries — each creates a draggable template
|
||||||
|
var fns = [
|
||||||
|
// Status Indicator variants
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.statusIndicator;status=ok;showLabel=1;fillColor=none;strokeColor=none;',
|
||||||
|
60, 40, '', 'Status: OK',
|
||||||
|
null, null, 'status indicator ok green check'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.statusIndicator;status=warning;showLabel=1;fillColor=none;strokeColor=none;',
|
||||||
|
60, 40, '', 'Status: Warning',
|
||||||
|
null, null, 'status indicator warning orange'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.statusIndicator;status=error;showLabel=1;fillColor=none;strokeColor=none;',
|
||||||
|
60, 40, '', 'Status: Error',
|
||||||
|
null, null, 'status indicator error red'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.statusIndicator;status=unknown;showLabel=1;fillColor=none;strokeColor=none;',
|
||||||
|
60, 40, '', 'Status: Unknown',
|
||||||
|
null, null, 'status indicator unknown grey'
|
||||||
|
),
|
||||||
|
// Wide pill variant
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.statusIndicator;status=ok;showLabel=1;fillColor=none;strokeColor=none;',
|
||||||
|
120, 30, '', 'Status Pill: OK',
|
||||||
|
null, null, 'status pill indicator wide'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Progress Bar variants
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.progressBar;progress=75;barColor=#4CAF50;showPercent=1;fillColor=none;strokeColor=none;',
|
||||||
|
200, 30, '', 'Progress Bar (75%)',
|
||||||
|
null, null, 'progress bar percentage loading'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.progressBar;progress=30;barColor=#FF9800;showPercent=1;fillColor=none;strokeColor=none;',
|
||||||
|
200, 30, '', 'Progress Bar (30%)',
|
||||||
|
null, null, 'progress bar low warning'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.progressBar;progress=90;barColor=#4CAF50;showPercent=1;animated=1;fillColor=none;strokeColor=none;',
|
||||||
|
200, 30, '', 'Progress Bar Animated (90%)',
|
||||||
|
null, null, 'progress bar animated stripes'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Data Flow Arrow variants
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.dataFlowArrow;direction=right;flowSpeed=medium;flowColor=#1976D2;fillColor=none;strokeColor=none;',
|
||||||
|
180, 40, '', 'Data Flow → Right',
|
||||||
|
null, null, 'data flow arrow right direction'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.dataFlowArrow;direction=left;flowSpeed=medium;flowColor=#1976D2;fillColor=none;strokeColor=none;',
|
||||||
|
180, 40, '', 'Data Flow ← Left',
|
||||||
|
null, null, 'data flow arrow left direction'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.dataFlowArrow;direction=both;flowSpeed=fast;flowColor=#7B1FA2;fillColor=none;strokeColor=none;',
|
||||||
|
180, 40, '', 'Data Flow ↔ Bidirectional',
|
||||||
|
null, null, 'data flow arrow bidirectional both'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Hexagon Cluster variants
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.hexCluster;hexCount=3;hexFill=#E3F2FD;hexStroke=#1565C0;fillColor=none;strokeColor=none;',
|
||||||
|
120, 120, '', 'Hex Cluster (3x)',
|
||||||
|
null, null, 'hexagon cluster grid honeycomb'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.hexCluster;hexCount=5;hexFill=#FFF3E0;hexStroke=#E65100;fillColor=none;strokeColor=none;',
|
||||||
|
200, 150, '', 'Hex Cluster (5x)',
|
||||||
|
null, null, 'hexagon cluster large grid'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.hexCluster;hexCount=4;hexFill=#E8F5E9;hexStroke=#2E7D32;filled=0,2,5;fillColor=none;strokeColor=none;',
|
||||||
|
160, 140, '', 'Hex Cluster (partial fill)',
|
||||||
|
null, null, 'hexagon cluster partial highlight'
|
||||||
|
),
|
||||||
|
|
||||||
|
// Metric Gauge variants
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.metricGauge;value=75;minVal=0;maxVal=100;label=CPU;fillColor=none;strokeColor=none;',
|
||||||
|
140, 100, '', 'Metric Gauge (75/100)',
|
||||||
|
null, null, 'gauge meter speedometer metric'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.metricGauge;value=45;minVal=0;maxVal=100;zones=#4CAF50,#FF9800,#F44336;label=Memory;fillColor=none;strokeColor=none;',
|
||||||
|
140, 100, '', 'Metric Gauge (45/100)',
|
||||||
|
null, null, 'gauge meter memory usage'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.metricGauge;value=92;minVal=0;maxVal=100;zones=#4CAF50,#FF9800,#F44336;label=Disk;fillColor=none;strokeColor=none;',
|
||||||
|
140, 100, '', 'Metric Gauge Critical (92/100)',
|
||||||
|
null, null, 'gauge meter disk critical high'
|
||||||
|
),
|
||||||
|
sb.createVertexTemplateEntry(
|
||||||
|
'shape=customShapes.metricGauge;value=3.5;minVal=0;maxVal=5;zones=#F44336,#FF9800,#4CAF50;label=Rating;fillColor=none;strokeColor=none;',
|
||||||
|
140, 100, '', 'Metric Gauge (3.5/5 Rating)',
|
||||||
|
null, null, 'gauge rating stars score'
|
||||||
|
)
|
||||||
|
];
|
||||||
|
|
||||||
|
sb.addPaletteFunctions('customShapesExample', 'Custom Shapes Example', false, fns);
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* DataFlowArrow Shape
|
||||||
|
*
|
||||||
|
* A directional arrow with flow indicator dots/dashes.
|
||||||
|
* Demonstrates: dynamic path generation, direction-aware rendering.
|
||||||
|
*
|
||||||
|
* Style parameters:
|
||||||
|
* - direction: 'right' | 'left' | 'both' (default: 'right')
|
||||||
|
* - flowSpeed: 'fast' | 'medium' | 'slow' (default: 'medium')
|
||||||
|
* - flowColor: hex color (default: '#1976D2')
|
||||||
|
* - thickness: number (default: '3')
|
||||||
|
*/
|
||||||
|
|
||||||
|
function DataFlowArrowShape() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(DataFlowArrowShape, mxShape);
|
||||||
|
|
||||||
|
DataFlowArrowShape.prototype.cst = {
|
||||||
|
SHAPE: 'customShapes.dataFlowArrow'
|
||||||
|
};
|
||||||
|
|
||||||
|
DataFlowArrowShape.prototype.customProperties = [
|
||||||
|
{
|
||||||
|
name: 'direction',
|
||||||
|
dispName: 'Direction',
|
||||||
|
type: 'enum',
|
||||||
|
defVal: 'right',
|
||||||
|
enumList: [
|
||||||
|
{ val: 'right', dispName: 'Right' },
|
||||||
|
{ val: 'left', dispName: 'Left' },
|
||||||
|
{ val: 'both', dispName: 'Bidirectional' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'flowSpeed',
|
||||||
|
dispName: 'Flow Speed',
|
||||||
|
type: 'enum',
|
||||||
|
defVal: 'medium',
|
||||||
|
enumList: [
|
||||||
|
{ val: 'fast', dispName: 'Fast' },
|
||||||
|
{ val: 'medium', dispName: 'Medium' },
|
||||||
|
{ val: 'slow', dispName: 'Slow' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
DataFlowArrowShape.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
var direction = mxUtils.getValue(this.style, 'direction', 'right');
|
||||||
|
var flowSpeed = mxUtils.getValue(this.style, 'flowSpeed', 'medium');
|
||||||
|
var flowColor = mxUtils.getValue(this.style, 'flowColor', '#1976D2');
|
||||||
|
var thickness = parseInt(mxUtils.getValue(this.style, 'thickness', '3'));
|
||||||
|
|
||||||
|
var midY = h / 2;
|
||||||
|
var arrowSize = Math.min(h / 2, 12);
|
||||||
|
var bodyStart = (direction === 'left' || direction === 'both') ? arrowSize : 0;
|
||||||
|
var bodyEnd = (direction === 'right' || direction === 'both') ? w - arrowSize : w;
|
||||||
|
|
||||||
|
// Draw main arrow body
|
||||||
|
c.setStrokeColor(flowColor);
|
||||||
|
c.setStrokeWidth(thickness);
|
||||||
|
c.setFillColor(flowColor);
|
||||||
|
|
||||||
|
// Arrow body line
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(bodyStart, midY);
|
||||||
|
c.lineTo(bodyEnd, midY);
|
||||||
|
c.stroke();
|
||||||
|
|
||||||
|
// Right arrowhead
|
||||||
|
if (direction === 'right' || direction === 'both') {
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(w - arrowSize, midY - arrowSize);
|
||||||
|
c.lineTo(w, midY);
|
||||||
|
c.lineTo(w - arrowSize, midY + arrowSize);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Left arrowhead
|
||||||
|
if (direction === 'left' || direction === 'both') {
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(arrowSize, midY - arrowSize);
|
||||||
|
c.lineTo(0, midY);
|
||||||
|
c.lineTo(arrowSize, midY + arrowSize);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flow indicator dots along the arrow body
|
||||||
|
var speeds = { fast: 8, medium: 14, slow: 22 };
|
||||||
|
var spacing = speeds[flowSpeed] || speeds.medium;
|
||||||
|
var dotRadius = Math.max(2, thickness * 0.8);
|
||||||
|
|
||||||
|
c.setFillColor('#FFFFFF');
|
||||||
|
c.setStrokeWidth(0);
|
||||||
|
|
||||||
|
for (var i = bodyStart + spacing; i < bodyEnd - spacing / 2; i += spacing) {
|
||||||
|
c.ellipse(i - dotRadius, midY - dotRadius, dotRadius * 2, dotRadius * 2);
|
||||||
|
c.fill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outer glow/pipe effect
|
||||||
|
c.setStrokeColor(flowColor);
|
||||||
|
c.setAlpha(0.15);
|
||||||
|
c.setStrokeWidth(thickness * 3);
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(bodyStart, midY);
|
||||||
|
c.lineTo(bodyEnd, midY);
|
||||||
|
c.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
mxCellRenderer.registerShape(DataFlowArrowShape.prototype.cst.SHAPE, DataFlowArrowShape);
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
/**
|
||||||
|
* HexagonCluster Shape
|
||||||
|
*
|
||||||
|
* A grid of hexagons that adapts to the cell dimensions.
|
||||||
|
* Demonstrates: loop-based drawing, mathematical geometry, adaptive layout.
|
||||||
|
*
|
||||||
|
* Style parameters:
|
||||||
|
* - hexCount: number of hexagons across (default: '3')
|
||||||
|
* - hexFill: hex color for hexagon fill (default: '#E3F2FD')
|
||||||
|
* - hexStroke: hex color for hexagon stroke (default: '#1565C0')
|
||||||
|
* - filled: which hexagons are filled (comma-separated indices, default: all)
|
||||||
|
*/
|
||||||
|
|
||||||
|
function HexagonClusterShape() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(HexagonClusterShape, mxShape);
|
||||||
|
|
||||||
|
HexagonClusterShape.prototype.cst = {
|
||||||
|
SHAPE: 'customShapes.hexCluster'
|
||||||
|
};
|
||||||
|
|
||||||
|
HexagonClusterShape.prototype.customProperties = [
|
||||||
|
{
|
||||||
|
name: 'hexCount',
|
||||||
|
dispName: 'Hex Count (across)',
|
||||||
|
type: 'int',
|
||||||
|
min: 1,
|
||||||
|
max: 10,
|
||||||
|
defVal: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hexFill',
|
||||||
|
dispName: 'Hex Fill Color',
|
||||||
|
type: 'color',
|
||||||
|
defVal: '#E3F2FD'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'hexStroke',
|
||||||
|
dispName: 'Hex Stroke Color',
|
||||||
|
type: 'color',
|
||||||
|
defVal: '#1565C0'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draw a single hexagon centered at (cx, cy) with given radius
|
||||||
|
*/
|
||||||
|
HexagonClusterShape.prototype.drawHexagon = function(c, cx, cy, radius) {
|
||||||
|
c.begin();
|
||||||
|
for (var i = 0; i < 6; i++) {
|
||||||
|
var angle = (Math.PI / 3) * i - Math.PI / 6; // flat-top orientation
|
||||||
|
var px = cx + radius * Math.cos(angle);
|
||||||
|
var py = cy + radius * Math.sin(angle);
|
||||||
|
if (i === 0) {
|
||||||
|
c.moveTo(px, py);
|
||||||
|
} else {
|
||||||
|
c.lineTo(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
HexagonClusterShape.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
var hexCount = parseInt(mxUtils.getValue(this.style, 'hexCount', '3'));
|
||||||
|
var hexFill = mxUtils.getValue(this.style, 'hexFill', '#E3F2FD');
|
||||||
|
var hexStroke = mxUtils.getValue(this.style, 'hexStroke', '#1565C0');
|
||||||
|
var filledStr = mxUtils.getValue(this.style, 'filled', '');
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
hexCount = Math.max(1, Math.min(10, hexCount));
|
||||||
|
|
||||||
|
// Calculate hex size based on available width
|
||||||
|
var padding = 4;
|
||||||
|
var availW = w - padding * 2;
|
||||||
|
var availH = h - padding * 2;
|
||||||
|
|
||||||
|
// For flat-top hexagons: width = sqrt(3) * radius, height = 2 * radius
|
||||||
|
var hexWidth = availW / hexCount;
|
||||||
|
var radius = hexWidth / Math.sqrt(3);
|
||||||
|
|
||||||
|
// How many rows fit
|
||||||
|
var rowHeight = radius * 1.5;
|
||||||
|
var rows = Math.max(1, Math.floor((availH - radius * 0.5) / rowHeight));
|
||||||
|
|
||||||
|
// Parse filled indices (which hexagons get highlight color)
|
||||||
|
var filledSet = {};
|
||||||
|
if (filledStr) {
|
||||||
|
filledStr.split(',').forEach(function(idx) {
|
||||||
|
filledSet[parseInt(idx.trim())] = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
|
||||||
|
c.setStrokeColor(hexStroke);
|
||||||
|
c.setStrokeWidth(1.5);
|
||||||
|
|
||||||
|
for (var row = 0; row < rows; row++) {
|
||||||
|
var colsInRow = (row % 2 === 0) ? hexCount : hexCount - 1;
|
||||||
|
var offsetX = (row % 2 === 0) ? 0 : hexWidth / 2;
|
||||||
|
|
||||||
|
for (var col = 0; col < colsInRow; col++) {
|
||||||
|
var cx = padding + offsetX + hexWidth / 2 + col * hexWidth;
|
||||||
|
var cy = padding + radius + row * rowHeight;
|
||||||
|
|
||||||
|
// Skip if out of bounds
|
||||||
|
if (cy + radius > h) continue;
|
||||||
|
|
||||||
|
// Determine fill: highlighted or default
|
||||||
|
var isHighlighted = filledStr === '' || filledSet[index];
|
||||||
|
c.setFillColor(isHighlighted ? hexFill : '#FAFAFA');
|
||||||
|
c.setAlpha(isHighlighted ? 1.0 : 0.5);
|
||||||
|
|
||||||
|
this.drawHexagon(c, cx, cy, radius * 0.9); // slight gap between hexagons
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
c.setAlpha(1.0);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Border around entire cluster
|
||||||
|
c.setStrokeColor(hexStroke);
|
||||||
|
c.setStrokeWidth(1);
|
||||||
|
c.setAlpha(0.3);
|
||||||
|
c.setDashed(true);
|
||||||
|
c.rect(0, 0, w, h);
|
||||||
|
c.stroke();
|
||||||
|
};
|
||||||
|
|
||||||
|
mxCellRenderer.registerShape(HexagonClusterShape.prototype.cst.SHAPE, HexagonClusterShape);
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
/**
|
||||||
|
* MetricGauge Shape
|
||||||
|
*
|
||||||
|
* A radial gauge (speedometer-style) showing a value within a range.
|
||||||
|
* Demonstrates: arc computation, trigonometry, zone-based coloring, text rendering.
|
||||||
|
*
|
||||||
|
* Style parameters:
|
||||||
|
* - value: current value (default: '75')
|
||||||
|
* - minVal: minimum value (default: '0')
|
||||||
|
* - maxVal: maximum value (default: '100')
|
||||||
|
* - zones: comma-separated zone colors from low to high (default: '#4CAF50,#FF9800,#F44336')
|
||||||
|
* - label: text label below the value (default: '')
|
||||||
|
* - arcWidth: width of the gauge arc (default: '8')
|
||||||
|
*/
|
||||||
|
|
||||||
|
function MetricGaugeShape() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(MetricGaugeShape, mxShape);
|
||||||
|
|
||||||
|
MetricGaugeShape.prototype.cst = {
|
||||||
|
SHAPE: 'customShapes.metricGauge'
|
||||||
|
};
|
||||||
|
|
||||||
|
MetricGaugeShape.prototype.customProperties = [
|
||||||
|
{
|
||||||
|
name: 'value',
|
||||||
|
dispName: 'Value',
|
||||||
|
type: 'float',
|
||||||
|
defVal: 75
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'minVal',
|
||||||
|
dispName: 'Min Value',
|
||||||
|
type: 'float',
|
||||||
|
defVal: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'maxVal',
|
||||||
|
dispName: 'Max Value',
|
||||||
|
type: 'float',
|
||||||
|
defVal: 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'zones',
|
||||||
|
dispName: 'Zone Colors (comma-sep)',
|
||||||
|
type: 'string',
|
||||||
|
defVal: '#4CAF50,#FF9800,#F44336'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'label',
|
||||||
|
dispName: 'Label',
|
||||||
|
type: 'string',
|
||||||
|
defVal: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
MetricGaugeShape.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
var value = parseFloat(mxUtils.getValue(this.style, 'value', '75'));
|
||||||
|
var minVal = parseFloat(mxUtils.getValue(this.style, 'minVal', '0'));
|
||||||
|
var maxVal = parseFloat(mxUtils.getValue(this.style, 'maxVal', '100'));
|
||||||
|
var zonesStr = mxUtils.getValue(this.style, 'zones', '#4CAF50,#FF9800,#F44336');
|
||||||
|
var label = mxUtils.getValue(this.style, 'label', '');
|
||||||
|
var arcWidth = parseInt(mxUtils.getValue(this.style, 'arcWidth', '8'));
|
||||||
|
|
||||||
|
var zones = zonesStr.split(',');
|
||||||
|
|
||||||
|
// Gauge geometry — 180° arc (semicircle)
|
||||||
|
var cx = w / 2;
|
||||||
|
var cy = h * 0.7; // center slightly below middle to leave room for text
|
||||||
|
var radius = Math.min(w / 2, h * 0.65) - arcWidth;
|
||||||
|
var startAngle = Math.PI; // left (180°)
|
||||||
|
var endAngle = 0; // right (0°)
|
||||||
|
var totalArc = Math.PI; // 180 degrees
|
||||||
|
|
||||||
|
// Draw zone arcs (background)
|
||||||
|
var zoneCount = zones.length;
|
||||||
|
var zoneArc = totalArc / zoneCount;
|
||||||
|
|
||||||
|
c.setStrokeWidth(arcWidth);
|
||||||
|
c.setLineCap('round');
|
||||||
|
|
||||||
|
for (var i = 0; i < zoneCount; i++) {
|
||||||
|
c.setStrokeColor(zones[i]);
|
||||||
|
c.setAlpha(0.3); // Background zones are translucent
|
||||||
|
|
||||||
|
var segStart = startAngle - i * zoneArc;
|
||||||
|
var segEnd = segStart - zoneArc;
|
||||||
|
|
||||||
|
// Draw arc segment using small line segments (canvas API approximation)
|
||||||
|
var steps = 20;
|
||||||
|
c.begin();
|
||||||
|
for (var s = 0; s <= steps; s++) {
|
||||||
|
var angle = segStart + (segEnd - segStart) * (s / steps);
|
||||||
|
var px = cx + radius * Math.cos(angle);
|
||||||
|
var py = cy - radius * Math.sin(angle);
|
||||||
|
if (s === 0) {
|
||||||
|
c.moveTo(px, py);
|
||||||
|
} else {
|
||||||
|
c.lineTo(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw active value arc (the "filled" portion)
|
||||||
|
var normalizedValue = Math.max(0, Math.min(1, (value - minVal) / (maxVal - minVal)));
|
||||||
|
var valueAngle = startAngle - totalArc * normalizedValue;
|
||||||
|
|
||||||
|
// Determine which zone the value falls in
|
||||||
|
var zoneIndex = Math.min(zoneCount - 1, Math.floor(normalizedValue * zoneCount));
|
||||||
|
var activeColor = zones[zoneIndex];
|
||||||
|
|
||||||
|
c.setStrokeColor(activeColor);
|
||||||
|
c.setAlpha(1.0);
|
||||||
|
c.setStrokeWidth(arcWidth);
|
||||||
|
|
||||||
|
var activeSteps = Math.max(2, Math.floor(40 * normalizedValue));
|
||||||
|
c.begin();
|
||||||
|
for (var s = 0; s <= activeSteps; s++) {
|
||||||
|
var angle = startAngle + (valueAngle - startAngle) * (s / activeSteps);
|
||||||
|
var px = cx + radius * Math.cos(angle);
|
||||||
|
var py = cy - radius * Math.sin(angle);
|
||||||
|
if (s === 0) {
|
||||||
|
c.moveTo(px, py);
|
||||||
|
} else {
|
||||||
|
c.lineTo(px, py);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.stroke();
|
||||||
|
|
||||||
|
// Draw needle
|
||||||
|
c.setStrokeColor('#333333');
|
||||||
|
c.setStrokeWidth(2);
|
||||||
|
c.setAlpha(1.0);
|
||||||
|
|
||||||
|
var needleLength = radius * 0.85;
|
||||||
|
var needleX = cx + needleLength * Math.cos(valueAngle);
|
||||||
|
var needleY = cy - needleLength * Math.sin(valueAngle);
|
||||||
|
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(cx, cy);
|
||||||
|
c.lineTo(needleX, needleY);
|
||||||
|
c.stroke();
|
||||||
|
|
||||||
|
// Needle center dot
|
||||||
|
c.setFillColor('#333333');
|
||||||
|
c.ellipse(cx - 4, cy - 4, 8, 8);
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
// Value text
|
||||||
|
c.setFontColor('#333333');
|
||||||
|
c.setFontSize(Math.min(radius * 0.4, 24));
|
||||||
|
c.setFontStyle(1); // bold
|
||||||
|
c.text(cx, cy + radius * 0.25, 0, 0, String(Math.round(value * 10) / 10),
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
|
||||||
|
|
||||||
|
// Label text
|
||||||
|
if (label) {
|
||||||
|
c.setFontColor('#666666');
|
||||||
|
c.setFontSize(Math.min(radius * 0.2, 12));
|
||||||
|
c.setFontStyle(0); // normal
|
||||||
|
c.text(cx, cy + radius * 0.45, 0, 0, label,
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min/Max labels
|
||||||
|
c.setFontColor('#999999');
|
||||||
|
c.setFontSize(Math.min(radius * 0.15, 10));
|
||||||
|
c.text(cx - radius, cy + 12, 0, 0, String(minVal),
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP, 0, null, 0, 0, 0);
|
||||||
|
c.text(cx + radius, cy + 12, 0, 0, String(maxVal),
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_TOP, 0, null, 0, 0, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
mxCellRenderer.registerShape(MetricGaugeShape.prototype.cst.SHAPE, MetricGaugeShape);
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* ProgressBar Shape
|
||||||
|
*
|
||||||
|
* A horizontal progress bar that fills based on a percentage value.
|
||||||
|
* Demonstrates: computed geometry, value-based rendering, gradient.
|
||||||
|
*
|
||||||
|
* Style parameters:
|
||||||
|
* - progress: 0-100 (default: 50)
|
||||||
|
* - barColor: hex color (default: '#4CAF50')
|
||||||
|
* - showPercent: '1' | '0' (default: '1')
|
||||||
|
* - animated: '1' | '0' — adds visual markers (default: '0')
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ProgressBarShape() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(ProgressBarShape, mxShape);
|
||||||
|
|
||||||
|
ProgressBarShape.prototype.cst = {
|
||||||
|
SHAPE: 'customShapes.progressBar'
|
||||||
|
};
|
||||||
|
|
||||||
|
ProgressBarShape.prototype.customProperties = [
|
||||||
|
{
|
||||||
|
name: 'progress',
|
||||||
|
dispName: 'Progress (%)',
|
||||||
|
type: 'int',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
defVal: 50
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'barColor',
|
||||||
|
dispName: 'Bar Color',
|
||||||
|
type: 'color',
|
||||||
|
defVal: '#4CAF50'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
ProgressBarShape.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
var progress = parseInt(mxUtils.getValue(this.style, 'progress', '50'));
|
||||||
|
var barColor = mxUtils.getValue(this.style, 'barColor', '#4CAF50');
|
||||||
|
var showPercent = mxUtils.getValue(this.style, 'showPercent', '1');
|
||||||
|
var animated = mxUtils.getValue(this.style, 'animated', '0');
|
||||||
|
|
||||||
|
// Clamp progress
|
||||||
|
progress = Math.max(0, Math.min(100, progress));
|
||||||
|
|
||||||
|
var borderRadius = Math.min(h / 2, 8);
|
||||||
|
var padding = 2;
|
||||||
|
|
||||||
|
// Background track
|
||||||
|
c.setFillColor('#E0E0E0');
|
||||||
|
c.setStrokeColor('#BDBDBD');
|
||||||
|
c.setStrokeWidth(1);
|
||||||
|
c.roundrect(0, 0, w, h, borderRadius, borderRadius);
|
||||||
|
c.fillAndStroke();
|
||||||
|
|
||||||
|
// Progress fill
|
||||||
|
var fillWidth = ((w - padding * 2) * progress) / 100;
|
||||||
|
|
||||||
|
if (fillWidth > 0) {
|
||||||
|
c.setStrokeWidth(0);
|
||||||
|
c.setFillColor(barColor);
|
||||||
|
|
||||||
|
// Color shifts based on progress level
|
||||||
|
if (progress < 30) {
|
||||||
|
c.setFillColor('#F44336'); // Red for low
|
||||||
|
} else if (progress < 70) {
|
||||||
|
c.setFillColor(barColor); // Default color for mid
|
||||||
|
} else {
|
||||||
|
c.setFillColor('#2E7D32'); // Dark green for high
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw filled portion with rounded left edge
|
||||||
|
c.roundrect(padding, padding, fillWidth, h - padding * 2,
|
||||||
|
borderRadius - 1, borderRadius - 1);
|
||||||
|
c.fill();
|
||||||
|
|
||||||
|
// Animated stripe markers
|
||||||
|
if (animated === '1' && fillWidth > 10) {
|
||||||
|
c.setStrokeColor('rgba(255,255,255,0.3)');
|
||||||
|
c.setStrokeWidth(2);
|
||||||
|
var stripeSpacing = 12;
|
||||||
|
for (var i = stripeSpacing; i < fillWidth - 4; i += stripeSpacing) {
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(padding + i, padding);
|
||||||
|
c.lineTo(padding + i - 4, h - padding);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Percentage text
|
||||||
|
if (showPercent === '1') {
|
||||||
|
c.setFontColor(progress > 50 ? '#FFFFFF' : '#333333');
|
||||||
|
c.setFontSize(Math.min(h * 0.6, 14));
|
||||||
|
c.setFontStyle(1); // bold
|
||||||
|
c.text(w / 2, h / 2, 0, 0, progress + '%',
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mxCellRenderer.registerShape(ProgressBarShape.prototype.cst.SHAPE, ProgressBarShape);
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* StatusIndicator Shape
|
||||||
|
*
|
||||||
|
* A shape that visually indicates status through color and icon.
|
||||||
|
* Demonstrates: conditional rendering, style-parameter reading.
|
||||||
|
*
|
||||||
|
* Style parameters:
|
||||||
|
* - status: 'ok' | 'warning' | 'error' | 'unknown' (default: 'unknown')
|
||||||
|
* - showLabel: '1' | '0' (default: '1')
|
||||||
|
*/
|
||||||
|
|
||||||
|
function StatusIndicatorShape() {
|
||||||
|
mxShape.call(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
mxUtils.extend(StatusIndicatorShape, mxShape);
|
||||||
|
|
||||||
|
StatusIndicatorShape.prototype.cst = {
|
||||||
|
SHAPE: 'customShapes.statusIndicator'
|
||||||
|
};
|
||||||
|
|
||||||
|
StatusIndicatorShape.prototype.customProperties = [
|
||||||
|
{
|
||||||
|
name: 'status',
|
||||||
|
dispName: 'Status',
|
||||||
|
type: 'enum',
|
||||||
|
defVal: 'unknown',
|
||||||
|
enumList: [
|
||||||
|
{ val: 'ok', dispName: 'OK' },
|
||||||
|
{ val: 'warning', dispName: 'Warning' },
|
||||||
|
{ val: 'error', dispName: 'Error' },
|
||||||
|
{ val: 'unknown', dispName: 'Unknown' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
StatusIndicatorShape.prototype.paintVertexShape = function(c, x, y, w, h) {
|
||||||
|
c.translate(x, y);
|
||||||
|
|
||||||
|
var status = mxUtils.getValue(this.style, 'status', 'unknown');
|
||||||
|
var showLabel = mxUtils.getValue(this.style, 'showLabel', '1');
|
||||||
|
|
||||||
|
// Status-dependent colors
|
||||||
|
var colors = {
|
||||||
|
ok: { fill: '#4CAF50', stroke: '#2E7D32', icon: '✓' },
|
||||||
|
warning: { fill: '#FF9800', stroke: '#E65100', icon: '⚠' },
|
||||||
|
error: { fill: '#F44336', stroke: '#B71C1C', icon: '✗' },
|
||||||
|
unknown: { fill: '#9E9E9E', stroke: '#424242', icon: '?' }
|
||||||
|
};
|
||||||
|
|
||||||
|
var cfg = colors[status] || colors.unknown;
|
||||||
|
|
||||||
|
// Outer rounded rectangle (the indicator body)
|
||||||
|
c.setFillColor(cfg.fill);
|
||||||
|
c.setStrokeColor(cfg.stroke);
|
||||||
|
c.setStrokeWidth(2);
|
||||||
|
|
||||||
|
// Draw shape — circle if square-ish, pill if wide
|
||||||
|
if (w > h * 1.5) {
|
||||||
|
// Pill shape
|
||||||
|
var radius = h / 2;
|
||||||
|
c.begin();
|
||||||
|
c.moveTo(radius, 0);
|
||||||
|
c.lineTo(w - radius, 0);
|
||||||
|
c.arcTo(radius, radius, 0, 0, 1, w - radius, h);
|
||||||
|
c.lineTo(radius, h);
|
||||||
|
c.arcTo(radius, radius, 0, 0, 1, radius, 0);
|
||||||
|
c.close();
|
||||||
|
c.fillAndStroke();
|
||||||
|
} else {
|
||||||
|
// Circle
|
||||||
|
var size = Math.min(w, h);
|
||||||
|
var cx = w / 2;
|
||||||
|
var cy = h / 2;
|
||||||
|
c.ellipse(cx - size / 2, cy - size / 2, size, size);
|
||||||
|
c.fillAndStroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner icon/symbol
|
||||||
|
if (showLabel === '1') {
|
||||||
|
c.setFontColor('#FFFFFF');
|
||||||
|
c.setFontSize(Math.min(w, h) * 0.4);
|
||||||
|
c.setFontStyle(1); // bold
|
||||||
|
c.text(w / 2, h / 2, 0, 0, cfg.icon,
|
||||||
|
mxConstants.ALIGN_CENTER, mxConstants.ALIGN_MIDDLE, 0, null, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pulse ring for 'error' status (visual emphasis)
|
||||||
|
if (status === 'error') {
|
||||||
|
c.setStrokeColor(cfg.fill);
|
||||||
|
c.setStrokeWidth(1);
|
||||||
|
c.setAlpha(0.3);
|
||||||
|
var padding = 4;
|
||||||
|
c.ellipse(-padding, -padding, w + padding * 2, h + padding * 2);
|
||||||
|
c.stroke();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
mxCellRenderer.registerShape(StatusIndicatorShape.prototype.cst.SHAPE, StatusIndicatorShape);
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
module.exports = (env, argv) => ({
|
||||||
|
entry: './src/index.js',
|
||||||
|
output: {
|
||||||
|
filename: 'drawio-custom-shapes.js',
|
||||||
|
path: path.resolve(__dirname, 'dist'),
|
||||||
|
// No library wrapper — draw.io plugins are plain scripts
|
||||||
|
// that call Draw.loadPlugin() as a side effect
|
||||||
|
iife: true
|
||||||
|
},
|
||||||
|
devtool: argv.mode === 'development' ? 'source-map' : false,
|
||||||
|
optimization: {
|
||||||
|
minimize: argv.mode === 'production'
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user