How to use
- Copy the installation command and review the source package before installing it in your own agent environment.
- Check the source document for required tools, credentials and supported hosts.
- Describe your input and desired deliverable; review the output against the source material.
npx skills add https://github.com/jasonjgardner/blockbench-mcp-project --skill blockbench-pluginsBefore you start
- Installation does not include a model subscription, API allowance or permission to access your files.
- Repository stars and installation counts describe activity, not an independent quality or security test.
Rendered from the source repository. Headings, examples, code, tables, links, and referenced images are preserved.
Blockbench Plugin Development
Overview
Blockbench runs on Electron (desktop) and as a web PWA, using THREE.js for 3D rendering and Vue 2 for reactive UI. Plugins have full access to global APIs within an isolated execution context.
Quick Reference
| Task | Approach |
|---|---|
| Create clickable command | new Action() - add to menus/toolbars |
| Show form/dialog | new Dialog() with form fields |
| Add sidebar panel | new Panel() with Vue component |
| Modify model elements | Use Undo.initEdit() → modify → Undo.finishEdit() |
| Custom import/export | new Codec() + new ModelFormat() |
| React to changes | Blockbench.on('event_name', callback) |
Plugin File Structure
plugins/
└── my_plugin/
├── my_plugin.js # Main file (required, ID must match filename)
├── about.md # Extended docs (optional)
└── icon.png # 48x48 icon (optional)Plugin Registration Template
(function() {
// Store references for cleanup
let myAction, myPanel, myDialog, eventCallback;
Plugin.register('my_plugin', {
title: 'My Plugin',
author: 'Author Name',
description: 'Short description',
icon: 'extension', // Material icon name
version: '1.0.0',
variant: 'both', // 'desktop', 'web', or 'both'
min_version: '4.8.0',
tags: ['Utility'],
onload() {
// Initialize all components here
},
onunload() {
// CRITICAL: Delete ALL components here
},
oninstall() {
Blockbench.showQuickMessage('Installed!');
}
});
})();Actions
Actions are clickable commands for menus, toolbars, and keybindings.
myAction = new Action('my_action_id', {
name: 'Action Name',
description: 'Tooltip text',
icon: 'star',
category: 'edit', // For keybind settings
condition: () => Cube.selected.length > 0,
keybind: new Keybind({ key: 'k', ctrl: true }),
click(event) {
// Action logic
}
});
MenuBar.addAction(myAction, 'filter'); // Add to Filter menu
// Cleanup
myAction.delete();Menu locations: 'file', 'edit', 'transform', 'filter', 'tools', 'view', 'help'
Action variants:
// Toggle (on/off state)
new Toggle('toggle_id', {
name: 'Feature',
default: false,
onChange(value) { /* handle */ }
});
// Tool (viewport interaction)
new Tool('tool_id', {
name: 'My Tool',
cursor: 'crosshair',
onCanvasClick(data) { /* handle */ },
onCanvasDrag(data) { /* handle */ }
});Dialogs
myDialog = new Dialog({
id: 'my_dialog',
title: 'Dialog Title',
width: 540,
form: {
name: { label: 'Name', type: 'text', value: 'default' },
count: { label: 'Count', type: 'number', value: 10, min: 1, max: 100 },
enabled: { label: 'Enabled', type: 'checkbox', value: true },
mode: {
label: 'Mode',
type: 'select',
options: { a: 'Option A', b: 'Option B' },
value: 'a'
},
color: { label: 'Color', type: 'color', value: '#ff0000' },
// Conditional field
advanced: {
label: 'Advanced',
type: 'text',
condition: (form) => form.enabled
}
},
onConfirm(formData) {
console.log(formData.name, formData.count);
this.hide();
}
});
myDialog.show();
// Quick dialogs
Blockbench.textPrompt('Enter Value', 'default', (text) => { });
Blockbench.showMessageBox({ title: 'Alert', message: 'Text', buttons: ['OK'] });Panels
Panels appear in sidebars with Vue components.
myPanel = new Panel('my_panel', {
name: 'My Panel',
icon: 'dashboard',
condition: () => Format.animation_mode,
default_position: {
slot: 'left_bar', // 'left_bar', 'right_bar', 'bottom'
height: 300
},
component: {
template: `
<div>
<h3>{{ title }}</h3>
<ul><li v-for="item in items">{{ item.name }}</li></ul>
<button @click="refresh">Refresh</button>
</div>
`,
data() {
return { title: 'Items', items: [] };
},
methods: {
refresh() {
this.items = Cube.selected.map(c => ({ name: c.name }));
}
}
}
});
// Cleanup
myPanel.delete();Model Manipulation (with Undo)
CRITICAL: Always wrap modifications in Undo for user reversibility.
// Start tracking
Undo.initEdit({ elements: Cube.selected });
// Modify elements
Cube.selected.forEach(cube => {
cube.from[0] += 5;
cube.to[1] = 20;
cube.rotation[1] = 45;
});
// Update view
Canvas.updateView({
elements: Cube.selected,
element_aspects: { geometry: true, transform: true }
});
// Commit
Undo.finishEdit('Move cubes');Creating Elements
// Cube
let cube = new Cube({
name: 'my_cube',
from: [0, 0, 0],
to: [16, 16, 16],
origin: [8, 8, 8],
rotation: [0, 45, 0]
}).init();
cube.addTo(Group.selected[0]); // Add to group
// Group (bone)
let group = new Group({
name: 'bone_arm',
origin: [0, 12, 0]
}).init();
group.addTo(); // Add to root
// Texture
let texture = new Texture({ name: 'my_texture' });
texture.fromPath('/path/to/file.png'); // or .fromDataURL()
texture.add(true); // true = add to undoGlobal Collections
| Collection | Description |
|---|---|
Cube.all / Cube.selected | All cubes / selected cubes |
Group.all / Group.selected | All groups / selected groups |
Mesh.all / Mesh.selected | All meshes / selected meshes |
Texture.all / Texture.selected | All textures / selected |
Animation.all / Animation.selected | All animations / selected |
Outliner.elements | All outliner elements |
Event System
// Subscribe
eventCallback = (data) => { /* handle */ };
Blockbench.on('update_selection', eventCallback);
// Unsubscribe (use SAME function reference)
Blockbench.removeListener('update_selection', eventCallback);Common events: update_selection, select_project, new_project, load_project, save_project, close_project, add_cube, add_group, add_texture, add_animation, select_animation, render_frame, undo, redo, finish_edit
See references/events.md for full list.
Custom Menus
let menu = new Menu([
'existing_action_id',
myAction,
'_', // Separator
{
name: 'Custom Item',
icon: 'star',
click() { /* handle */ }
},
{
name: 'Submenu',
children: [ /* more items */ ]
}
]);
menu.open(event); // Open at mouse positionFormat and Codec (Import/Export)
const myCodec = new Codec('my_codec', {
name: 'My Format',
extension: 'mymodel',
compile(options) {
// Model → file content
let data = { bones: [] };
Group.all.forEach(g => {
data.bones.push({
name: g.name,
pivot: g.origin,
cubes: g.children.filter(c => c instanceof Cube).map(c => ({
from: c.from, to: c.to
}))
});
});
return JSON.stringify(data, null, 2);
},
parse(content, path) {
// File content → model
let data = JSON.parse(content);
newProject(myFormat);
data.bones.forEach(b => {
let group = new Group({ name: b.name, origin: b.pivot }).init();
b.cubes.forEach(c => {
new Cube({ from: c.from, to: c.to }).init().addTo(group);
});
});
Canvas.updateAll();
}
});
const myFormat = new ModelFormat('my_format', {
id: 'my_format',
name: 'My Format',
icon: 'icon_name',
codec: myCodec,
box_uv: true,
bone_rig: true,
animation_mode: true
});
// Cleanup
myFormat.delete();
myCodec.delete();Condition Patterns
// Function
condition: () => Format.id === 'bedrock' && Cube.selected.length > 0
// Object (combined with AND)
condition: {
formats: ['bedrock', 'java_block'],
modes: ['edit', 'paint'],
project: true,
selected: { cube: 1 },
method: () => customCheck()
}Code Style Conventions
| Type | Convention | Examples |
|---|---|---|
| Classes | PascalCase | OutlinerElement, Animation |
| Methods | camelCase | updateTransform, getSelectedFaces |
| Properties | snake_case | data_points, uv_offset |
| Event/Action IDs | snake_case | select_project, my_action |
Critical Cleanup Pattern
MUST delete all components in `onunload()` to prevent memory leaks.
let action, panel, dialog, toolbar, format, codec, eventCallback, css;
Plugin.register('my_plugin', {
onload() {
action = new Action('id', { /* ... */ });
panel = new Panel('id', { /* ... */ });
format = new ModelFormat('id', { /* ... */ });
codec = new Codec('id', { /* ... */ });
eventCallback = (data) => { };
Blockbench.on('update_selection', eventCallback);
css = Blockbench.addCSS('.my-class { color: blue; }');
},
onunload() {
action.delete();
panel.delete();
format.delete();
codec.delete();
Blockbench.removeListener('update_selection', eventCallback);
css.delete();
}
});Anti-Patterns
// ❌ No stored reference = memory leak
onload() { new Action('leaked', { }); }
// ❌ No Undo = users can't reverse
Cube.selected.forEach(c => c.from[0] += 1);
// ❌ Bundling THREE.js (already global)
import * as THREE from 'three';
// ❌ Global pollution
window.myData = {};Built-in Libraries (Do Not Bundle)
- THREE — Three.js 3D rendering
- Vue — Vue 2 reactive UI
- JSZip — ZIP handling
- marked — Markdown parsing
- Molang — Molang expression parser
Icons
icon: 'star' // Material icon
icon: 'fa-bone' // Font Awesome
icon: 'icon-player' // Blockbench custom
icon: 'data:image/...' // Base64 imageTypeScript Setup
npm i --save-dev blockbench-types/// <reference types="blockbench-types" />Template Files
assets/plugin_template.js— Complete starter pluginassets/format_plugin.js— Custom format/codec example
References
references/events.md— Full event listreferences/api.md— Detailed API referencereferences/elements.md— Element types and properties




