Quellcode-Analyse · Reverse Engineering · Hands-on verifiziert

Blockbench 5.2.1 Wie die Web- und Desktop-3D-Editor-Suite für Minecraft-Modelle intern funktioniert – geklont, gebaut, im Browser gestartet, instrumentiert und durchgetestet.

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

Inhalt
  1. Eckdaten & Code-Metrik
  2. Build-System & Deployment
  3. Boot-Sequenz: was beim Start passiert
  4. Architektur & Modul-Landkarte
  5. Datenmodell: Nodes, Cubes, Properties
  6. Rendering-Pipeline (Three.js + Shader)
  7. Texturen, Painter & UV-Editor
  8. Codecs & Dateiformate
  9. Undo-System
  10. Animations-System
  11. UI-Schicht: Vue, Actions, Panels
  12. Plugin-System (mit Live-Beweis)
  13. Web vs. Desktop
  14. Bemerkenswerte Fundstücke
  15. Eigene Experimente & Messwerte
  16. Bewertung der Architektur
  17. Selbst starten

01Eckdaten & Code-Metrik

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.

134.590Zeilen JS/TS
174Module (97 × TS)
18.991Zeilen CSS
512Zeilen GLSL
21Sprachdateien
8,6 MBBundle (minifiziert)
2,1 sBuild-Zeit
43 MBRepo ohne node_modules

Verteilung über die funktionalen Bereiche

BereichZeilenDateienInhalt
js/formats/23.66820Java-Blöcke, Bedrock (Entity/Block/Legacy/Voxel-Shape/Multi-File), OptiFine, Skins, OBJ/glTF/FBX/Collada/STL, Bild-Format
js/interface/15.97725Actions, Menüs, Panels, Toolbars, Settings-UI, Dialoge, Start-Screen, Themes, Keybinds
js/texturing/14.5848Texture-Klasse, Painter (3.859 Z.), Layer, Texture-Groups (PBR), GIF/Flipbook, Farb-Tools
js/outliner/13.89917Node-/Element-Basisklassen und Typen: Group, Cube, Mesh, Locator, Armature, SplineMesh …
js/animations/13.91312Timeline, Keyframes, Animators, Animation-Controller, Molang, Bone-Rigs
js/modeling/11.45523Transform-Gizmos, Mesh-Editing (Extrude/Loop-Cut/Knife), Mirroring, Weight-Paint, Vertex-Snap
js/preview/8.6617Three.js-Szene, Canvas, OrbitControls, Preview-Szenen (Overworld, Nether, …), Screenshots
js/lib/8.28512Vendored: jQuery-UI, Spectrum-Colorpicker, LZUTF8, GIF-Encoder, APNG, CSS3DRenderer, GLTFExporter
js/uv/5.6352UV-Editor inkl. Face-UV-Mapping, Auto-UV, Copy-Paste-Tool
js/io/3.8647Codec-, Format-, Projekt-, Model-Loader-, Share- und Backup-Logik
js/util/2.85616Event-System, Condition-Resolver, Property-System, StateMemory, YAML/JSON, Math/Array-Helfer
js/*.ts|js (Wurzel)~11.000~20main.ts, boot_loader, api, plugin_loader (2.249 Z.), undo (1.100 Z.), desktop/web, native_apis
Beobachtung zur Code-Basis: Der Code ist erstaunlich „handgeschrieben". Es gibt kaum Abstraktions-Frameworks, fast keine Build-Magie und – bemerkenswert für 134k Zeilen – keine Unit-Tests. Die GitHub-Workflows bauen nur Artefakte; Qualitätssicherung läuft offenbar über Beta-Kanäle („manifest-beta", enable_beta.js) und die sehr aktive Community.

02Build-System & Deployment

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-SkriptWirkung
npm run serveWeb-Build + esbuild-Dev-Server (Standard-Port 8000) – so habe ich den Klon laufen lassen
npm run devElectron-Build im Watch-Modus, startet die Desktop-App automatisch
npm run build-web / build-electronminifiziertes Produktions-Bundle (ohne / mit Electron-Ziel)
npm run pwaWorkbox-basierter Service-Worker für die installierbare Web-App
npm run generate-typesTypeScript-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.

03Boot-Sequenz: was beim Start passiert

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).

main.ts ├─ libs (jQuery, Vue, Three.js, JSZip, Prism, gifenc …) → Object.assign(window, …) ├─ util/* Event-System, Condition, Property, Math ├─ interface/* Menu, Actions, Keyboard, Panels, Settings, Themes ├─ outliner/* Node-Typen + Preview-Controller ├─ preview/*, modeling/*, texturing/*, uv/*, animations/* ├─ io/* + formats/* Codecs & Formate registrieren sich selbst ├─ plugin_loader └─ boot_loader ← startet die App

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
Live verifiziert: Der geklonte Build bootet fehlerfrei. Ausgelesener Zustand nach dem Start: 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.
Boot-Screenshot
Erster Screenshot des selbst gebauten Klons: Der Start-Screen mit Format-Auswahl lädt vollständig.

04Architektur & Modul-Landkarte

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.

Zentrale Singletons

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

Basis-Klassen

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

Zwei universelle Helfer, die die ganze App zusammenhalten

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.

05Datenmodell: Nodes, Cubes, Properties

Outliner-Baum

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

Cube und CubeFace

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];
  }
}

Property-System

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

06Rendering-Pipeline (Three.js + Shader)

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:

ShaderZweck
solid.vert/fragStandard-Material für Cubes/Gruppen (flache Low-Poly-Optik, Directional Shading)
texture.fragTexturiertes 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, …
}
Einstellungen wirken direkt in Materialien hinein (z. B. seethrough_outline → depthTest). Das erklärt, warum viele Optionen in Blockbench sofort sichtbar greifen – es gibt keine Abstraktionsschicht zwischen Setting und Renderzustand.

07Texturen, Painter & UV-Editor

Die Texture-Klasse

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(); }
Stolperfalle (live gefunden): 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.

Painter

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:

UV-Editor

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.

08Codecs & Dateiformate

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
})

Live aus dem laufenden Programm ausgelesen

Formate (10)

free (Generic Model) · java_block · bedrock (Bedrock Entity) · bedrock_block · bedrock_old (Legacy) · modded_entity · optifine_entity · optifine_part · skin (Minecraft Skin) · image

Codecs (16)

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).

Beweis: echtes Compile-Ergebnis meines Testmodells

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.

09Undo-System

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.

AspectErfasst
elementsAusgewählte Elemente (Cubes, Meshes …) – jeweils via getUndoCopy()
outlinerkomplette Baumstruktur (Outliner.toJSON())
groups / groupGruppen(kopien) – inkl. Pivot, Rotation, Rig-Bindung
selectionSelektion (eigener selectionSave, wird nur bei echter Änderung gespeichert)
textures, texture_groups, layersTexturliste, PBR-Gruppen, Ebenen
animations, animation_controllers, animation_controller_stateKeyframes & Controller-Zustände
collections, uv_mode, display_slots, settingsSammlungen, 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!
Live-Test: Ich habe einen Turm aus 4 Cubes über diesen Pfad gebaut. Ergebnis: 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").

10Animations-System

Das Animations-Subsystem ist praktisch ein kompletter zweiter Editor innerhalb der App (Animationsmodus) und besteht aus:

11UI-Schicht: Vue, Actions, Panels

Die Oberfläche ist ein Hybrid aus drei Generationen – sichtbar im Code, aber funktional sauber getrennt:

GenerationTechnikBeispiele
Vue 2.7Single-File-Components + Vuex-artige Zustandsobjekte (inside_vue)Outliner, Timeline, Texturliste, Start-Screen, Tab-Leiste, Plugins-Fenster
jQuery-UIDialoge, Drag & Drop, Resize, Spectrum-ColorpickerFormulare, Farbwahl, Panel-Docking
Plain JSNeue Menü- und Toolbar-Struktur (interface/menu.ts, toolbars.ts)Menüleiste, Toolbars, Kontextmenüs, Command-Palette

Actions & 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 } }

Panels (18, live ausgelesen)

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.

Modi & Lokalisierung

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 …).

12Plugin-System (mit Live-Beweis)

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.

Beweis: eigenes Plugin, live über den echten Loader geladen

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() });
  }
});
Vom eigenen Plugin generierter Turm
Live-Beweis: Vom eigenen Plugin erzeugte 4-Etagen-Turm im geklonten Client. Unten rechts läuft der Render-Loop mit 59 fps; der Quick-Message-Toast stammt aus der Plugin-Action, die Gruppe „tower" steht im Outliner. Der Wireframe im Texturen-Panel oben links ist ein Nebeneffekt der programmatisch erzeugten Demo-Textur.
Drei Fallstricke, die ich beim Plugin-Schreiben live gefunden habe:
  1. ID-Abgleich: Registriert das Skript eine andere ID als das Plugin-Objekt trägt, greift der Loader still auf Plugins.registered.unknown zurück und onload läuft nie – ohne sichtbare Fehlermeldung im UI.
  2. MenuItem ist keine Klasse mehr: Menüeinträge sind schlichte Objekte ({name, icon, click}); new MenuItem(…) wirft zur Laufzeit.
  3. Undo braucht die neuen Elemente: Bei finishEdit() müssen erzeugte Elemente im Aspect mitgegeben werden ({outliner:true, elements:[…]}), sonst kann Undo sie nicht entfernen.

13Web vs. Desktop – eine Basis, zwei Laufzeiten

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-BuildDesktop-Build (Electron)
Entryweb.ts: initializeWebApp(), Service-Worker, URL-Parameterdesktop.ts: Recent Projects, Auto-Backup, „Open with", externer Bildeditor
DateisystemFile System Access API / Uploadsfs, PathModule, chokidar-Watcher für Texturen
Native APIskeine@electron/remote, shell, clipboard, dialog, zlib, https, child_process
Plugin-requireundefinedScoped require über SAFE_APIS + requireNativeModule
UpdateCache/Service-Workerelectron-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.

14Bemerkenswerte Fundstücke

15Eigene Experimente & Messwerte

Alles in diesem Bericht ist nicht nur gelesen, sondern an einer laufenden Instanz überprüft. Vorgehen:

git clone --depth 1 github.com/JannisX11/blockbench → 43 MB Repo npm install → 778 Pakete, 601 MB node_modules npm run build-web → 8,6 MB Bundle in 2,1 s python3 -m http.server 8000 → Klon läuft auf localhost playwright-core + headless Chromium → Automatisierung & Screenshots check_boot.mjs → Boot-Zustand, Registries, Konsolenfehler messen drive_bb.mjs → Modell bauen, Undo testen, Textur erzeugen, Codec ausführen test_plugin.mjs → eigenes Plugin über den echten Loader laden und ausführen
TestErgebnis
Bootsetup_successful=true, Version 5.2.1, Three.js r129, keine JS-Fehler (nur Service-Worker-404 + CORS zu blckcdn, weil selbst gehostet)
Projekt/CodepfadnewProject('bedrock_block') → Format bedrock_block, Modus edit
ModellbauGruppe body + 2 Cubes inkl. inflate, Preview-Mesh im Three.js-Scene vorhanden
UndoVerschieben/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-ExportCodecs.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

Screenshots der Sitzung

Start-Screen
Start-Screen des selbst gebauten Klons: Format-Kategorien, „Recent"-Bereich, Theme-Auswahl.
Modell im Editor
Programmatisch gebautes Bedrock-Block-Projekt: Gruppe „body" mit zwei Cubes, Transform-Panel rechts, UV-Editor links (16×16-Raster).
Export-Dialog
Export-Dialog des Bedrock-Codecs – aus den export_options automatisch generiert. Wird im Modus „Exportieren" geöffnet.

16Bewertung der Architektur

Stärken

  • Ein Codebase, zwei Laufzeiten – der Regex-basierte Conditional-Import ist simpel und effektiv.
  • Registry-Muster überall (Formats, Codecs, Actions, Panels, Toolbars, Properties, Settings): Erweiterung = Registrierung, kein Umbau.
  • Deklarative UI-Gates: Condition + Property machen Features ohne UI-Code sichtbar/unsichtbar.
  • Ernsthaftes Undo-System mit feiner Aspect-Granularität, das auch Plugins korrekt benutzen können.
  • Plugin-Ökosystem mit echter Reichweite – ein Plugin kann Werkzeuge, Formate, Panels und Importeure beisteuern.
  • Pragmatismus: keine Framework-Überfrachtung, Build in 2 s, Debugging direkt im Bundle.

Schwächen & Risiken

  • Implizite Modulreihenfolge: main.ts ist ein 150-Zeilen-Import-Skript; falsche Reihenfolge = Laufzeitfehler.
  • Globale Namespaces: hunderte Objekte auf window – bequem, aber kollisionsanfällig.
  • Keine Tests im Repo; Regressionen werden über Beta-Kanäle gefunden.
  • Plugins im Web ohne Sandbox: Code aus dem Store läuft mit vollen Rechten der Seite – Vertrauensmodell rein sozial.
  • Technologie-Mix altert: Vue 2.7 (EOL), jQuery + jQuery-UI, Three.js r129, @electron/remote.
  • 8,6 MB Bundle (nicht gecacht: 18 MB incl. Sourcemap) für einen Editor – im Web spürbar, wenn auch PWA-Cache hilft.
  • Drei UI-Generationen parallel erhöhen die Einstiegshürde für Beiträge.

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.

17Selbst starten & weiterarbeiten

Im Workspace vorhanden

Befehle

cd 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

Lizenzhinweis

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.


Fortsetzung: Teil 2 liegt bereit – 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.

Und jetzt gebaut: Teil 3 – 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.