Repository: github.com/JannisX11/blockbench · Commit e2ede08 („v5.2.1 [ci-build]", 21.09.2026) ·
Lizenz: GPL-3.0-or-later · Autor: Jannis Petersen (JannisX11) · Analyse erstellt am 03.10.2026
Blockbench ist ein Low-Poly-3D-Editor, der ursprünglich für Minecraft-Modelle (Java-Blöcke, Bedrock-Entities, Skins) gebaut wurde, inzwischen aber auch generische Formate (OBJ, glTF, FBX, Collada, STL) und Animationen verarbeitet. Derselbe Quellcode erzeugt sowohl die Web-App (web.blockbench.net, als PWA installierbar) als auch die Electron-Desktop-App für Windows, macOS und Linux.
| Bereich | Zeilen | Dateien | Inhalt |
|---|---|---|---|
js/formats/ | 23.668 | 20 | Java-Blöcke, Bedrock (Entity/Block/Legacy/Voxel-Shape/Multi-File), OptiFine, Skins, OBJ/glTF/FBX/Collada/STL, Bild-Format |
js/interface/ | 15.977 | 25 | Actions, Menüs, Panels, Toolbars, Settings-UI, Dialoge, Start-Screen, Themes, Keybinds |
js/texturing/ | 14.584 | 8 | Texture-Klasse, Painter (3.859 Z.), Layer, Texture-Groups (PBR), GIF/Flipbook, Farb-Tools |
js/outliner/ | 13.899 | 17 | Node-/Element-Basisklassen und Typen: Group, Cube, Mesh, Locator, Armature, SplineMesh … |
js/animations/ | 13.913 | 12 | Timeline, Keyframes, Animators, Animation-Controller, Molang, Bone-Rigs |
js/modeling/ | 11.455 | 23 | Transform-Gizmos, Mesh-Editing (Extrude/Loop-Cut/Knife), Mirroring, Weight-Paint, Vertex-Snap |
js/preview/ | 8.661 | 7 | Three.js-Szene, Canvas, OrbitControls, Preview-Szenen (Overworld, Nether, …), Screenshots |
js/lib/ | 8.285 | 12 | Vendored: jQuery-UI, Spectrum-Colorpicker, LZUTF8, GIF-Encoder, APNG, CSS3DRenderer, GLTFExporter |
js/uv/ | 5.635 | 2 | UV-Editor inkl. Face-UV-Mapping, Auto-UV, Copy-Paste-Tool |
js/io/ | 3.864 | 7 | Codec-, Format-, Projekt-, Model-Loader-, Share- und Backup-Logik |
js/util/ | 2.856 | 16 | Event-System, Condition-Resolver, Property-System, StateMemory, YAML/JSON, Math/Array-Helfer |
js/*.ts|js (Wurzel) | ~11.000 | ~20 | main.ts, boot_loader, api, plugin_loader (2.249 Z.), undo (1.100 Z.), desktop/web, native_apis |
enable_beta.js) und die sehr aktive Community.Das komplette Build-System ist eine einzige Datei: build.js (~150 Zeilen, esbuild-basiert).
// build.js – Kern entryPoints: ['./js/main.ts'], define: { isApp: isApp.toString(), appVersion: `"${pkg.version}"` }, // Version wird zur Buildzeit eingesetzt bundle: true, minify: !dev_mode, outfile: './dist/bundle.js', loader: { '.bbtheme': 'text', '.png': 'dataurl' }, plugins: [ // ersetzt Imports per Regex-Pfad – das ist der Web/Desktop-Umschalter! conditionalImportPlugin(1, { filter: /desktop/, file: isApp ? 'desktop.ts' : 'web.ts' }), conditionalImportPlugin(2, { filter: /native_apis/, file: isApp ? 'native_apis.ts' : 'native_apis_web.ts' }), conditionalImportPlugin(3, { filter: /vue.js/, file: dev_mode ? 'vue.js' : 'vue.min.js', library: true }), createJsonPlugin('.bbkeymap', 'bbkeymap'), // Blender/Maya/Cinema4D-Keymaps als JSON vuePlugin(), // .vue-Single-File-Components glsl({ minify }), // Shader werden als Strings ins Bundle inlined createElectronPlugin() // bei --watch --launch: startet Electron automatisch ]
| npm-Skript | Wirkung |
|---|---|
npm run serve | Web-Build + esbuild-Dev-Server (Standard-Port 8000) – so habe ich den Klon laufen lassen |
npm run dev | Electron-Build im Watch-Modus, startet die Desktop-App automatisch |
npm run build-web / build-electron | minifiziertes Produktions-Bundle (ohne / mit Electron-Ziel) |
npm run pwa | Workbox-basierter Service-Worker für die installierbare Web-App |
npm run generate-types | TypeScript-Deklarationen aus dem JS-Code (allowJs) → Plugin-API-Typen |
npm run publish-* | electron-builder für Windows (NSIS), macOS (notarisiert), Linux (deb/rpm/AppImage) |
Das Ergebnis-Bundle enthält alles – Three.js, Vue, jQuery, Spectrum, GLTF-Exporter, shaders – und ist 8,6 MB minifiziert (18 MB mit Sourcemap, die mit ausgeliefert wird). Ein voller Build dauert auf dieser Maschine 2,1 Sekunden.
index.html ist bewusst mager: nur CSS-Links, ein Fehler-Overlay (#loading_error_message) und ein <script type="module" src="dist/bundle.js">. Alles Weitere passiert im Bundle. Der Einstieg ist js/main.ts, das rund 150 Module in explizit festgelegter Reihenfolge importiert – diese Reihenfolge ist gleichzeitig die Abhängigkeitsordnung (Imports haben Nebenwirkungen wie Klassen-Registrierung).
boot_loader.js ist der eigentliche Startvorgang:
// Auszug boot_loader.js Interface.page_wrapper = document.getElementById('page_wrapper'); // DOM-Verdrahtung CustomTheme.setup(); StateMemory.init('dialog_paths','object'); Blockbench.browser = 'electron'; // Browser-Sniffing im Web-Build setupPreviews(); initCanvas(); // Three.js-Szene, Renderer, Shader BARS.setupActions(); BARS.setupToolbars(); BARS.setupVue(); MenuBar.setup(); translateUI(); loadThemes(); initReferenceImages(); animate(); // Startet den Render-Loop (requestAnimationFrame) loadInstalledPlugins().then(proceed); // Plugins laden (mit Timeout-Fallback) setStartScreen(true); AutoBackup.initialize(); Blockbench.setup_successful = true; // Globales Ready-Flag für das Fehler-Overlay
Blockbench.version = "5.2.1", setup_successful = true, THREE.REVISION = "129",
16 registrierte Codecs, 462 BarItems, 137 Settings, 18 Panels, 5 Modi, 10 Formate.
Typische Eigenheiten beim Selbst-Hosting: Service-Worker-404 und CORS-Fehler zu blckbn.ch (Plugin-Statistiken) – beides ohne Funktionsverlust.
Die Architektur folgt einem konsistenten Muster: Registry-Klassen + globale Singletons. Fast alles ist über window erreichbar, weil Plugins (und die Konsole) direkt darauf zugreifen sollen.
Blockbench (Version, Flags, Events, Plattform-Infos)
Project / ModelProject (offenes Dokument)
Format / Formats · Codecs
Canvas / Preview / Texture
Outliner · Undo · Painter
Panels · Toolbars · BarItems · Keybinds
Modes · Settings · StateMemory
OutlinerNode → OutlinerElement → Cube, Mesh, Locator …
Group (Container, kann Bones/Rigs tragen)
Face → CubeFace / MeshFace / BillboardFace
NodePreviewController (Modell ↔ Three.js)
Codec · ModelFormat · AnimationCodec
Action → BarItem, Tool, Toggle
Panel · Toolbar · Menu · Setting · Property
1. Condition() (util/util.js) entscheidet überall, ob ein UI-Element sichtbar/aktiv ist – Actions, Toolbars, Menüpunkte, Properties, Modes. Sie akzeptiert Funktionen, Wahrheitswerte oder ein deklaratives Objekt:
Condition = function(condition, context) {
if (condition.modes && !condition.modes.includes(Modes.id)) return false;
if (condition.formats && !condition.formats.includes(Format.id)) return false;
if (condition.tools && !condition.tools.includes(Toolbox.selected.id)) return false;
if (condition.features && condition.features.find(f => !Format[f])) return false;
if (condition.selected) { /* animation, keyframe, group, texture, element, … */ }
if (condition.project && !Project) return false;
if (condition.method instanceof Function) return !!condition.method(context);
return true;
}
Condition.mutuallyExclusive = function(a, b) { /* Toolbar-Konflikte auflösen */ }
2. EventSystem (util/event_system.ts) – Plugins hängen sich hier ein. on() akzeptiert mehrere Events als leerzeichengetrennte Liste und liefert ein Objekt mit delete() zum Abhängen; dispatchEvent() gibt den letzten Rückgabewert zurück (damit Handler Entscheidungen wie „Edit erlaubt?" treffen können):
Undo.initEdit(aspects) → dispatchEvent('init_edit', {aspects, amended, save})
Undo.finishEdit(msg, aspects) → dispatchEvent('finish_edit', {aspects, message})
Canvas.updateAll() → dispatchEvent('update_all') … usw.
Alles im Modell ist ein OutlinerNode: Gruppen, Cubes, Meshes, Locators, Null-Objects, Armaturen, Spline-Meshes, Bounding-Boxes, Textur-Meshes. Der Wurzelknoten ist 'root'. Kernmethoden:
node.addTo(target, index) // einhängen, mit Typ-Prüfung via getTypeBehavior('parent_types' / 'child_types') node.init() // registrieren (UUID, Outliner-Gruppe, Preview-Objekt) node.select() / unselect() // Selektion ist Teil des globalen Zustands node.getUndoCopy(aspects) // serialisierbarer Schnappschuss für das Undo-System
Ein Cube speichert Geometrie nicht direkt, sondern in einem eingefrorenen _static-Objekt mit Gettern/Settern – so bleiben Referenzen stabil, während Undo/Redo ganze Objekte austauschen können:
constructor(data) { size = Settings.get('default_cube_size'); this._static = Object.freeze({ properties: { faces: { north: new CubeFace('north', null, this), east: …, south: …, west: …, up: …, down: … }, from: [0,0,0], to: [size,size,size], rotation: [0,0,0], origin: [0,0,0] }}); this.inflate = 0; this.stretch = [1,1,1]; this.autouv = 0; this.box_uv = Project.box_uv; this.extend(data); // Merge.* – toleranter Importer für alle Dateiformate } get from() { return this._static.properties.from } // + passende Setter
Jede der sechs Flächen ist ein CubeFace mit eigener UV-Region [u1,v1,u2,v2], Rotation und fester Vertex-Reihenfolge – das ist der Grund, warum Blockbench Minecraft-UV-Layouts pixelgenau treffen kann:
getVertexIndices() {
switch (this.direction) {
case 'north': return [1, 4, 6, 3]; case 'east': return [0, 1, 3, 2];
case 'south': return [5, 0, 2, 7]; case 'west': return [4, 5, 7, 6];
case 'up': return [4, 1, 0, 5]; case 'down': return [7, 2, 3, 6];
}
}
Eigenschaften wie inflate, visibility oder cullface werden deklarativ beschrieben und erscheinen dadurch automatisch im Element-Panel UND in Dialogen (aber ggf. nur an einer Stelle – inputs.shared):
new Property(Cube, 'string', 'name', { exposed: true, default: 'cube' }) // PropertyOptions: default, condition, exposed, export, copy_value, description, label, // options, values, merge, reset, merge_validation, // inputs: { element_panel|dialog: {input, shared, onChange} } // Typen: string | enum | molang | number | boolean | array | object | instance | vector2/4/3
Blockbench nutzt Three.js r129 (bewusst alt, aus Kompatibilität) plus CI-eigene Materialien. Die Shader liegen als .glsl-Dateien vor und werden per esbuild-Plugin in das Bundle inlined:
| Shader | Zweck |
|---|---|
solid.vert/frag | Standard-Material für Cubes/Gruppen (flache Low-Poly-Optik, Directional Shading) |
texture.frag | Texturiertes Material (UV-Sampling) |
layered.* | Mehrschicht-Texturen (Texture-Layers / PBR-Channels) |
marker.* | Farbige Element-Marker/Kanten |
uv_helper.* | UV-Hervorhebung auf dem 3D-Mesh (Face-Auswahl im UV-Editor) |
brush_outline.* | Pinsel-Vorschau beim Painten |
In preview/canvas.js liegt die globale Szene (THREE.Scene, Sonnenlicht, Grid, Ground-Plane, Gizmos). Auffällig: ein Objekt-Pool „Reusable" mit 8 Vektoren, 3 Quaternionen und 3 Eulern, um im Render-/Interaktionspfad Garbage-Collection zu vermeiden – typisch für ein Tool, das mit 60 fps auf Mausbewegungen reagieren muss.
export const Canvas = {
scene, // die globale Three.js-Szene
outlineMaterial: new THREE.LineBasicMaterial({ depthTest: settings.seethrough_outline.value == false, … }),
meshOutlineMaterial, splinePathLineMaterial, gizmos: [], show_element_markers: true, …
}
seethrough_outline → depthTest). Das erklärt, warum viele Optionen in Blockbench sofort sichtbar greifen – es gibt keine Abstraktionsschicht zwischen Setting und Renderzustand.Eine Texture kapselt drei Dinge gleichzeitig: ein eigenes 2D-Canvas (Malfläche), ein Image (Quelle) und eine THREE.Texture (GPU):
constructor() { this.width = 0; this.height = 0; // werden NICHT automatisch gefüllt! this.canvas = document.createElement('canvas'); this.canvas.width = this.canvas.height = 16; this.ctx = this.canvas.getContext('2d', {willReadFrequently: true}); this.img = new Image(); let tex = new THREE.Texture(this.canvas); // Canvas ist die GPU-Quelle … } load(cb) { this.error = 0; this.img.src = this.source; … } // nur Bildquelle setzen updateSource(url) { if (!url) url = this.source; this.source = url; this.img.src = url; this.updateMaterial(); }
fromDataURL(), load() und updateSource() setzen weder width noch height – sie bleiben 0×0, bis der Import-Pfad (oder der Painter) sie befüllt. Wer programmatisch eine Textur erzeugt, muss Größe und Canvas-Inhalt selbst setzen. Genau das brauchte mein Test-Skript, um eine funktionierende 16×16-Textur zu bekommen.Mit 3.859 Zeilen ist texturing/painter.js die größte Einzeldatei außerhalb der Formate. Sie enthält alle Malwerkzeuge (Brush, Füllen, Formen, Verlauf, Pipette), Mirror-Painting über Modelachse oder Texturmitte, Blend-Modes, sowie zwei interessante Algorithmen-Kategorien:
projectScreenSpaceBrush() rechnet die Mausposition auf Bildschirm/3D-Oberfläche in Texturpixel um, inklusive UV-Islands (getMeshUVIsland) und Face-Erkennung.scanCanvas(), scanlineConvexPolygon(), editCircle/editSquare – eigene Scanline-Algorithmen statt Canvas-Pfaden, weil Weichzeichnen/Undo-fähige Pixel-Eingriffe nötig sind.Der UV-Editor (uv/uv.js, 5.388 Zeilen) ist ein eigener Editor-Modus mit eigenem Vue-UI, Toolkit (Wählen, Rotieren um 90°, Spiegeln, Auto-UV, Copy-Paste-Tool) und einer wichtigen Brücke: texelToLocalMatrix() in CubeFace wandelt Texturkoordinaten in lokale 3D-Koordinaten um – damit kann der Painter beim Malen im 3D-Fenster präzise auf die Fläche mappen.
Die IO-Schicht trennt sauber zwischen Format (was der Nutzer wählt: „Bedrock Entity") und Codec (wie gelesen/geschrieben wird). Ein Codec definiert u. a. load()/parse(), compile()/export(), export_options (generiert automatisch einen Dialog aus der Form-API), load_filter und fileName().
new Codec('bedrock', { name: 'Bedrock Entity', extension: 'json', remember: true, load_filter: { extensions: ['json','bbmodel'], type: 'json' }, export_options: { /* Dialogfelder für den Export */ }, compile(options) { … }, // Modell → String/JSON parse(data, path) { … }, // Datei → Modell })
free (Generic Model) · java_block · bedrock (Bedrock Entity) · bedrock_block · bedrock_old (Legacy) · modded_entity · optifine_entity · optifine_part · skin (Minecraft Skin) · image
project (.bbmodel) · java_block · bedrock_voxel_shape · bedrock_entity_file · bedrock · bedrock_old · obj · gltf · fbx · collada · stl · modded_entity · optifine_entity · optifine_part · skin_model · image
Ein Format kann mehrere Codecs bündeln (z. B. Bedrock: Entity-Datei + Voxel-Shape + Multi-File-Projekt) und umgekehrt verweisen mehrere Formate auf denselben Codec (bedrock und bedrock_block).
Ich habe programmatisch zwei Cubes in einer Gruppe erzeugt, rotiert und deren UVs gesetzt, dann Codecs.bedrock.compile() aufgerufen. Die Ausgabe (gekürzt) zeigt, wie Blockbench das interne Modell in echte Bedrock-Geometry übersetzt – inklusive des Vorzeichen-Tricks uv_size: [-8,-8] für die Unterseite:
{
"format_version": "1.12.0",
"minecraft:geometry": [{
"description": { "identifier": "geometry.unknown", "texture_width": 16, "texture_height": 16,
"visible_bounds_width": 3, "visible_bounds_height": 1.5, "visible_bounds_offset": [0,0.25,0] },
"bones": [{
"name": "body", "pivot": [0,0,0],
"cubes": [{
"origin": [-8, 0, 0], "size": [6, 4, 8], "pivot": [0,0,0], "rotation": [0, -22.5, 0],
"uv": {
"north": { "uv": [0,5], "uv_size": [8,8] }, "east": { "uv": [0,5], "uv_size": [8,8] },
"south": { "uv": [0,5], "uv_size": [8,8] }, "west": { "uv": [0,5], "uv_size": [8,8] },
"up": { "uv": [8,14], "uv_size": [-8,-8] }, "down": { "uv": [8,14], "uv_size": [-8,-8] }
} }] }] }] }
Das Projektformat .bbmodel ist selbst ein Codec (formats/bbmodel.js) mit eigener Formatversion (5.0) und einer ausführlichen processCompatibility()-Funktion, die Modelle aus alten Versionen migriert (Z-Achsen-Inversion vor 3.2, UV-Spiegelung vor 4.5, Molang-Positionen vor 5.0 …). Texturen werden darin per LZUTF8 komprimiert gespeichert.
Das Undo-System ist zentral für die Glaubwürdigkeit des Editors und arbeitet mit Aspects: Man gibt beim Start einer Änderung an, was betroffen ist; das System legt vorher/nachher-Schnappschüsse an.
| Aspect | Erfasst |
|---|---|
elements | Ausgewählte Elemente (Cubes, Meshes …) – jeweils via getUndoCopy() |
outliner | komplette Baumstruktur (Outliner.toJSON()) |
groups / group | Gruppen(kopien) – inkl. Pivot, Rotation, Rig-Bindung |
selection | Selektion (eigener selectionSave, wird nur bei echter Änderung gespeichert) |
textures, texture_groups, layers | Texturliste, PBR-Gruppen, Ebenen |
animations, animation_controllers, animation_controller_state | Keyframes & Controller-Zustände |
collections, uv_mode, display_slots, settings | Sammlungen, UV-Zustand, Display-Einstellungen, Settings |
Das kanonische Muster für „neues Element erzeugen" (aus dem Quellcode übernommen und in meinem Plugin getestet):
Undo.initEdit({outliner: true, elements: [], selection: true});
let cube = new Cube({…}).init();
cube.addTo(group);
Undo.finishEdit('Add cube', {outliner: true, elements: [cube]}); // ← neu erzeugte Elemente mitgeben!
Undo.undo() entfernt alle vier Cubes, Undo.redo() bringt sie exakt zurück; der Historieneintrag heißt "Turm generieren". Interessantes Detail: Die (leere) Gruppe bleibt nach dem Undo bestehen – weil die Gruppe im outliner-Schnappschuss vor der Änderung existiert und das System Struktur und Elemente getrennt behandelt.Weitere Eigenschaften: einstellbares undo_limit, amend-Modus (letzten Eintrag nachträglich umschreiben, statt neuen anzulegen – für Drag-Operationen), Fail-Safe für Gruppenänderungen, und Sperren gegen Änderungen während laufender Paint-/Transform-Operationen. Netter Detailfund: Der Code warnt aktiv, wenn man noch das alte Aspect cubes benutzt („deprecated, use elements").
Das Animations-Subsystem ist praktisch ein kompletter zweiter Editor innerhalb der App (Animationsmodus) und besteht aus:
Animation hat animators (pro Node), Keyframes speichern Datenpunkte je Achse (KeyframeDataPoint) mit Molang-Ausdrücken, Interpolationskurven, Easing (lib/easing.js) und Quaternion-Modus.timeline.js (2.317 Z.), Playhead, Raster, Onion-Skin, Marken, Zoom; Timeline-Animators kennen Loop-Modi (once/hold/loop), Blend-Delays und Molang-getriebene Werte.BoneAnimator, NullObjectAnimator, EffectAnimator – plus animation_transform.js für Verschieben/Skalieren von Keyframes.animations/molang.js + util/molang.ts + molangjs integrieren die Bedrock-Ausdruckssprache in Eigenschaften, Keyframes und sogar in molang-Properties des Property-Systems; invertMolang() kehrt Ausdrücke für die Migration um.animation_controller_codec.js, 2.382 Z. UI) inkl. Vorhersage/Playback der Zustandsübergänge.fabrik.ts – Inverse Kinematics für Armaturen/Bones (Minecraft-Bedrock-Rigs).Die Oberfläche ist ein Hybrid aus drei Generationen – sichtbar im Code, aber funktional sauber getrennt:
| Generation | Technik | Beispiele |
|---|---|---|
| Vue 2.7 | Single-File-Components + Vuex-artige Zustandsobjekte (inside_vue) | Outliner, Timeline, Texturliste, Start-Screen, Tab-Leiste, Plugins-Fenster |
| jQuery-UI | Dialoge, Drag & Drop, Resize, Spectrum-Colorpicker | Formulare, Farbwahl, Panel-Docking |
| Plain JS | Neue Menü- und Toolbar-Struktur (interface/menu.ts, toolbars.ts) | Menüleiste, Toolbars, Kontextmenüs, Command-Palette |
Es gibt 462 BarItems (Actions, Tools, Toggles, Slider …). Sie werden über Kategorien, Icons und Condition-Regeln deklarativ registriert und sind sowohl in Toolbars/Menüs als auch in der Action-Suche (ActionControl, Strg+K) verfügbar. Diese Command-Palette ist zugleich ein Settings-Editor: sie zeigt Toggles direkt als Checkboxen an.
ActionControl = { type:'action_selector', recently_used: [], max_length: 32, max_recently_used: 8,
select(input) { … öffnet Suchdialog, führt bei Enter die Action aus } }
outlinertransformelementanimationsbonetimelinekeyframeanimation_controllerstextureslayersuvdisplaycolorpalettecollectionsvariable_placeholderschatskin_pose
Panels können angedockt, verschoben, geschwebt oder versteckt werden; die Anordnung speichert das StateMemory. Toolbars sind zweigeteilt organisiert: Toolbars ist die Registry (Instanzen), BARS verwaltet persistierte Nutzeranpassungen (localStorage: 'toolbars') und den Postload-Mechanismus für Toolbar-Einträge, die erst später (durch Plugins) bekannt werden.
Modes: edit, paint, display, animate, pose. Ein Mode kann Toolbars/Seitenleisten/Statusleiste ausblenden, Knotentypen im Outliner verstecken und eigene Vue-Komponenten einhängen – so entsteht aus einer Codebasis ein jeweils aufgeräumtes Werkzeug.
i18n: tl(key, variables, default) mit zwei Eigenheiten: Strings über 100 Zeichen werden unverändert zurückgegeben (Performance-Schutz für Fließtext), und Platzhalter werden einfach per %0, %1 … ersetzt. Basis sind 21 Sprachdateien (lang/*.json) mit flachen Dotted-Keys (format.bedrock.desc, panel.outliner, uv_editor.copy_paste_tool.rotate …).
Plugins sind das Herzstück der Erweiterbarkeit und überraschend schlicht gebaut: Der Store liefert JavaScript-Dateien, die per new Function('requireNativeModule', 'require', code) ausgeführt werden – im Web also ohne Sandbox mit vollem Zugriff auf window, im Desktop mit einem eingeschränkten require aus einer Whitelist.
class Plugin {
async load() { … #runCachedFile(path) : #runPluginFile(path) … }
#runCode(code) {
const func = new Function('requireNativeModule', 'require',
code + `\n//# sourceURL=PLUGINS/(Plugin):${this.id}.js`);
const scoped_require = isApp ? getPluginScopedRequire(this) : undefined;
func(scoped_require, scoped_require);
}
}
// Desktop: SAFE_APIS-Whitelist für Plugin-require:
const SAFE_APIS = ['path','crypto','events','zlib','timers','url','string_decoder',
'querystring','constants','buffer','stream', …];
Dazu kommen: ein Permissions-System (Plugins deklarieren Berechtigungen, die im Plugin-Dialog angezeigt und per Aktion „Revoke permissions" entzogen werden können – mit Hinweis „Restart to apply"), Abhängigkeiten zwischen Plugins (dependencies), Versionsbereiche (min_version/max_version), Varianten (desktop/web/both), Reload für lokale/URL-Plugins und ein IndexedDB-Quelltext-Cache (plugin_sources) für die Web-App.
Ich habe ein funktionsfähiges Demo-Plugin geschrieben (blockbench/plugins_demo/tower_generator.js) und über den originalen Lade-Pfad (Datei-Fetch → Plugin.loadFromFile() → #runCode() → Plugin.register() → runOnLoad()) in die laufende Instanz injiziert. Es registriert eine Einstellung, zwei Actions, eine Toolbar und einen Menüeintrag und baut auf Klick einen Turm aus Cubes – inklusive korrektem Undo-Eintrag.
Plugin.register('tower_generator', { // ID muss zum Plugin-Objekt passen!
title: 'Tower Generator', icon: 'castle', version: '1.0.0', variant: 'both',
onload() {
new Setting('tower_floors', { name:'Turm – Anzahl Etagen', type:'number', value:4, category:'edit' });
new Action('tower_generate', { name:'Turm generieren', icon:'castle', category:'edit',
condition: { formats: ['bedrock','bedrock_block','java_block','free'] },
click() {
Undo.initEdit({outliner:true, elements:[], selection:true});
let group = new Group('tower').init(), created = [];
for (let y = 0; y < settings.tower_floors.value; y++) {
let inset = (y % 2) ? 0.5 : 0;
let cube = new Cube({name:`floor_${y+1}`, from:[-2+inset, y*3, -2+inset], to:[2-inset, y*3+3, 2-inset]}).init();
cube.addTo(group); created.push(cube);
Object.values(cube.faces).forEach(f => { f.uv = [0,0,8,8]; });
}
group.addTo('root');
Undo.finishEdit('Turm generieren', {outliner:true, elements:created, groups:[group]});
Canvas.updateAll();
Blockbench.showQuickMessage(`${settings.tower_floors.value} Etagen erzeugt`, 1500);
}});
new Toolbar('tower_toolbar', { name:'Turm-Generator', children:['tower_generate','tower_options'] });
Toolbars.tools.add('tower_generate'); Toolbars.tools.update();
MenuBar.menus.tools.structure.push({ name:'Turm generieren', icon:'castle',
click: () => BarItems.tower_generate.trigger() });
}
});
Plugin-Objekt trägt, greift der Loader still auf Plugins.registered.unknown zurück und onload läuft nie – ohne sichtbare Fehlermeldung im UI.MenuItem ist keine Klasse mehr: Menüeinträge sind schlichte Objekte ({name, icon, click}); new MenuItem(…) wirft zur Laufzeit.finishEdit() müssen erzeugte Elemente im Aspect mitgegeben werden ({outliner:true, elements:[…]}), sonst kann Undo sie nicht entfernen.Der Umschalter ist der conditionalImportPlugin aus dem Build: Derselbe Import import './native_apis' wird für Web gegen native_apis_web.ts (alle Exporte null) und für Desktop gegen native_apis.ts aufgelöst.
| Web-Build | Desktop-Build (Electron) | |
|---|---|---|
| Entry | web.ts: initializeWebApp(), Service-Worker, URL-Parameter | desktop.ts: Recent Projects, Auto-Backup, „Open with", externer Bildeditor |
| Dateisystem | File System Access API / Uploads | fs, PathModule, chokidar-Watcher für Texturen |
| Native APIs | keine | @electron/remote, shell, clipboard, dialog, zlib, https, child_process |
Plugin-require | undefined | Scoped require über SAFE_APIS + requireNativeModule |
| Update | Cache/Service-Worker | electron-updater gegen GitHub-Releases |
Sicherheitsdetails im Desktop-Build: delete window.process nach dem Zwischenspeichern, electronFuses.runAsNode = false, NSIS-Installer mit Lizenzseite, macOS-Hardened-Runtime + Notarization. Außerdem existiert eine scoped_fs.ts, die Plugins nur Zugriff auf ihre erlaubten Pfade gibt.
Reusable.vec1…vec8, Quaternionen und Euler im Renderpfad – sonst würde bei Mausbewegungen der GC zuschlagen.edit_sessions.js + peerjs erlauben Live-Sessions („Live Editing") – inklusive sendAll('command','undo') im Undo-System, damit Undos synchronisiert werden.validator.js prüft Modelle gegen Formatregeln (z. B. Größenlimits, falsche Pivots) und bietet Korrektur-Aktionen an.auto_backup.ts schreibt zyklisch Sicherungen und kann sie beim Start anbieten („Recover backup").bbmodel.js, version_util.ts und unzähligen Format-Modulen steckt jahrelange Kompatibilitätsarbeit – das ist der eigentliche Wert des Projekts.globals.js ist eine reine Alias-Datei („Deprecated"), und alte APIs warnen aktiv in der Konsole – gut für Plugin-Autoren.<meta name="robots" content="noindex"> – die App soll nicht indexiert werden.index.html Fehlermeldung, Version, Reload- und Factory-Reset-Button (localStorage.clear()).Alles in diesem Bericht ist nicht nur gelesen, sondern an einer laufenden Instanz überprüft. Vorgehen:
| Test | Ergebnis |
|---|---|
| Boot | setup_successful=true, Version 5.2.1, Three.js r129, keine JS-Fehler (nur Service-Worker-404 + CORS zu blckcdn, weil selbst gehostet) |
| Projekt/Codepfad | newProject('bedrock_block') → Format bedrock_block, Modus edit |
| Modellbau | Gruppe body + 2 Cubes inkl. inflate, Preview-Mesh im Three.js-Scene vorhanden |
| Undo | Verschieben/Rotieren → undo → exakt vorheriger Zustand → redo korrekt |
| Texture (programmatisch) | 16×16-Canvas → DataURL → Texture; musste width/height und Canvas-Inhalt manuell setzen; danach zeigt das Three.js-Material width:16, height:16 |
| Codec-Export | Codecs.bedrock.compile() liefert valides Bedrock-Geometry-JSON (siehe Abschnitt 08) |
| Plugin | Über den Original-Loader geladen; Actions/Settings/Toolbar registriert; Action erzeugt 4 Cubes; Undo-Eintrag „Turm generieren"; Screenshot 04 |
export_options automatisch generiert. Wird im Modus „Exportieren" geöffnet.Condition + Property machen Features ohne UI-Code sichtbar/unsichtbar.main.ts ist ein 150-Zeilen-Import-Skript; falsche Reihenfolge = Laufzeitfehler.window – bequem, aber kollisionsanfällig.@electron/remote.Fazit: Blockbench ist ein beeindruckendes Beispiel für „long-lived pragmatic software": Die Architektur ist nicht elegant im akademischen Sinn, aber sie ist extrem langlebig, erweiterbar und nachvollziehbar. Die drei tragenden Ideen – deklarative Registries, Condition-gesteuerte UI und ein sauber aspectsiertes Undo-System – sind der Grund, warum eine 1-Personen-Codebasis über Jahre zu einem kompletten Minecraft-Modell-Studio mit großem Plugin-Ökosystem wachsen konnte. Wer die App verstehen will, muss drei Dateien lesen: main.ts (Reihenfolge), boot_loader.js (Start) und util/util.js (Condition) – von da aus ist alles weitere nach demselben Muster aufgebaut.
blockbench/ – vollständiger Klon (Commit e2ede08, v5.2.1) inkl. gebautem dist/bundle.jsblockbench/plugins_demo/tower_generator.js – funktionsfähiges Demo-Plugin (lässt sich in jeder Blockbench-Installation über File → Plugins → Load from File verwenden)check_boot.mjs, drive_bb.mjs, test_plugin.mjs – Playwright-Skripte für Boot-, Modell-, Codec- und Plugin-Testsscreenshots/ – Roh-PNGs und optimierte JPEGs dieser Sitzungblockbench-analyse.html – dieses Dokumentcd blockbench npm install npm run serve # Web-App lokal auf http://localhost:8000 npm run dev # Desktop-App (Electron) mit Hot-Build npm run build-web # Produktions-Bundle → dist/bundle.js
Blockbench steht unter GPL-3.0-or-later. Eigene Änderungen, die verbreitet werden, müssen ebenfalls unter dieser Lizenz offengelegt werden; Plugins, die nur die öffentliche Plugin-API nutzen, sind davon in der Praxis nicht betroffen, eigene Forks der App schon. Für die private Analyse und Weiterbildung gibt es ohnehin keine Einschränkung.
blockbench-analyse-teil2.html mit der Analyse der Website, des Plugin-Stores (beide zusätzlich geklont), Messungen zu Painter und Animations-Engine sowie dem fertigen Toolkit-Plugin (blockbench/plugins_demo/bb_toolkit.js) und einer Excel-Übersicht aller 144 Store-Plugins.Erstellt am 03.10.2026 · Alle Aussagen durch Quellcode-Lektüre und Ausführung des geklonten Builds verifiziert. Messwerte (Build-Zeit, Bundle-Größe, Registries, JSON-Ausgaben) stammen aus der laufenden Instanz in dieser Umgebung.
blockbench-editionen.html – setzt die Analyse in zwei lauffähige Varianten um: eine AI Edition (Blockbench per JSON-Kommando von einem Agenten steuerbar, 26 Kommandos) und eine Mobile Edition (Fingerbedienung: 46-px-Werkzeugkacheln, Aktions-Knopf, Zwei-Finger-Tipp = Rückgängig). Beide laufen unter http://127.0.0.1:8001 bzw. :8002 und sind mit 29 + 26 Prüfungen live getestet.