Fortsetzung von blockbench-analyse.html (Teil 1: App-Architektur) · Stand 03.10.2026 · alle Zahlen live gemessen
Die Website ist kein statisches HTML, sondern eine eigenständige Nuxt-4-Anwendung (Vue 3) im selben Autoren-Repo. ~213 Dateien, Klon-Umfang 46 MB.
docs/bbmodel.md mit der Format-Spezifikation, docs/plugin.md, docs/undo.md) liegt als Markdown im Repo und wird zu statischen Seiten gerendert. Ein Redirect führt sogar /faq auf den Wiki-Pfad.downloads.vue baut die Links zu Blockbench_${version}.exe, _arm64_.dmg, .deb, .rpm, .AppImage aus der GitHub-Releases-API. Fällt die API aus (version == '1.0.1'), zeigt die Seite einen Fallback-Text. Flatpak kommt von Flathub (net.blockbench.Blockbench) – passend zur Datei static/.well-known/org.flathub.VerifiedApps.txt.nitro.prerender.crawl = true, aber /plugins und /downloads sind ausgenommen – die beiden Seiten mit Live-Daten werden also immer dynamisch gerendert.npm run prepublish-web = Web-Build + PWA-Service-Worker + Typedoc), die Website liegt getrennt davon (Cloudflare davor, x-origin-cache: HIT).Der Store ist kein Server, sondern ein GitHub-Repository (JannisX11/blockbench-plugins). Die App liest daraus zwei JSON-Dateien und lädt Plugin-Quelltext direkt von einem CDN – das erklärt, warum der Store praktisch keine Infrastrukturkosten hat und beliebig skalierbar ist.
| Zweck | URL | Live gemessen |
|---|---|---|
| Store-Index (Primär) | cdn.jsdelivr.net/gh/JannisX11/blockbench-plugins/plugins.json | HTTP 200 · 68.355 Bytes · 81 ms |
| Store-Index (Mirror) | blckbn.ch/cdn/plugins.json (Setting cdn_mirror) | HTTP 200 · 68.355 Bytes |
| Update-Prüfung | …/updates.json | HTTP 200 · 14.574 Bytes |
| Plugin-Quelltext | …/plugins/<id>.js bzw. …/plugins/<id>/<id>.js | HTTP 200 · 38 KB / 501 KB |
plugin_loader.ts fest verdrahtet: Schlägt der Aufruf des Primär-CDN fehl und ist das Gerät online, schaltet Blockbench selbstständig auf den Mirror um (settings.cdn_mirror.set(true)) und weist darauf hin, dass ein Neustart nötig ist.| Plugins gesamt | 144 |
Variante both | 120 |
Variante desktop | 24 |
mit min_version | 113 |
| Einzeldatei / Multifile | 68 / 80 Ordner |
| eigene Tags | 89 verschiedene |
| Autoren | 89 |
Einträge mit langem about | 29 |
Minecraft: Java Edition · 37Utility · 15Animation · 14Minecraft: Bedrock Edition · 13Exporter · 10Minecraft · 10Blockbench · 7Texture · 6Deprecated · 6Hytale · 5Paint · 5Viewport · 5
Auch hier gilt: Das Ökosystem ist stark Minecraft-getrieben – aber Hytale und generische Utility-Plugins wachsen sichtbar hinein.
Ein vollständiger, filterbarer Katalog aller 144 Plugins liegt als Excel-Datei daneben: blockbench-plugin-store-uebersicht.xlsx – drei Blätter (Plugins mit Tags/Autor/Größe/Abhängigkeiten, Statistik, Quellen & Commit-Stand).
// plugins.json – Feldreichster Eintrag als Beispiel "optifine_player_models": { "title": "OptiFine Player Models", "icon": "icon.png", "author": "Ewan Howell", "description": "Adds a new format that allows you to create OptiFine player models.", "tags": ["Minecraft: Java Edition", "OptiFine", "Player Models"], "version": "1.5.2", "min_version": "5.0.0", "variant": "both", "await_loading": true, // App wartet beim Start auf dieses Plugin "creation_date": "2022-05-29", "contributes": { "formats": ["optifine_player_model"] }, // „ich kann .xyz öffnen" "website": "…", "repository": "…", "bug_tracker": "…" }
Das Feld contributes ist dabei besonders praktisch: Blockbench weiß dadurch über nicht installierte Plugins, welches Format sie beisteuern würden, und kann beim Öffnen einer unbekannten Datei gezielt vorschlagen: „Diese Datei braucht Plugin X."
Der Weg in den Store führt über einen Pull Request. Die Datei scripts/validate.js ist die Türsteher-Prüfung und läuft in der CI. Sie ist streng und deckt genau die Fallstricke ab, über die ich beim Plugin-Bauen gestolpert bin:
| Prüfung | Regel |
|---|---|
| Plugin-ID | muss snake_case sein (/^[a-z][a-z0-9_]+$/) und exakt der ID im Quelltext entsprechen |
| Pfad-Beschränkung | Ein PR darf nur plugins.json, plugins/<id>/ und src/<id>/ anfassen – niemand kann fremde Plugins überschreiben |
| Pflichtfelder | title, author, icon, description, version |
| Metadaten-Abgleich | Die Werte in plugins.json müssen zu Plugin.register(…) im Quelltext passen |
| Ausführungstest | Das Plugin wird in einer Node-VM-Sandbox ausgeführt, in der jeder unbekannte globale Bezeichner durch einen Proxy „Wildcard" ersetzt wird – so läuft fremder Code ohne Seiteneffekte durch, und der Validator kann ID und Metadaten auslesen |
| Icon & Changelog | Icon-Größe wird geprüft; Changelogs folgen changelog.schema.json |
| Dateiname | plugins/<id>.js (klassisch) oder plugins/<id>/<id>.js (Multifile-Format ab min_version ≥ 4.8.0) |
Für Multifile-Plugins gibt es src/<id>/ als Quellprojekt (eigene package.json, Webpack/Esbuild-Konfiguration), aus dem die gebündelte Einzeldatei erzeugt und mit eingecheckt wird. Deshalb liegt im Store neben Mesh Tools der 501-KB-Build auch das Quellprojekt im src/-Ordner.
Der Painter ist der zweitgrößte Block der App (3.859 Zeilen) und funktioniert als Zustandsmaschine über Ereignisse, nicht als kontinuierlicher Loop. Der Ablauf einer einzigen Pinselbewegung:
Vor jedem Strich wird ein Clipping-Pfad aus den UV-Rechtecken gesetzt: setupRectFromFace() berechnet aus der Face-UV (und optional der aktiven Mirror-Achse) den erlaubten Bereich, ctx.rect(...) spannt ihn auf, und alles Malen (Kreis, Quadrat, Linie) läuft in diesem Bereich. Bei Mesh-Flächen kommt eine „Occupancy Matrix" hinzu – eine Pixelmaske der tatsächlichen Dreiecksform, damit der Pinsel nicht über schräge Kanten hinausläuft.
| Test | Vorgehen | Ergebnis |
|---|---|---|
| Strich im 3D-View | echte Mausereignisse (move → down → 14 Zwischenschritte → up) über dem Modell | Pinselgröße 1 → 2 rote Pixel gesetzt; Undo-Eintrag "Paint texture" |
| Füllwerkzeug „Fläche" | fill_tool + fill_mode=face, Klick auf die Oberseite | 256 von 256 Pixeln gefüllt (UV der Fläche = ganze 16×16-Textur) |
| Mirror-Painting | mirror_painting_options.texture = true, Strich auf der linken Hälfte | 4 Pixel gesetzt – 2 links, 2 rechts, exakt symmetrisch |
| Pipette/Raycast | Canvas.raycast({clientX, clientY}) über Modell | liefert Element, Fläche (up) und UV – dieselbe Funktion, die alle Werkzeuge nutzen |
Meine erste Messung zeigte „Undo stellt die Bitmap nicht wieder her" – das war falsch, wie die genauere Messung entlarvte. Die Bitmap wird nicht synchron zurückgesetzt, sondern über ein Bild-onload: Beim Wiederherstellen legt Undo die Pixel aus image_data in den Canvas und setzt die Bildquelle; sichtbar wird das Ergebnis erst im nächsten Tick.
| Zeitpunkt | Canvas-Pixel | Quelle (Zeichen) |
|---|---|---|
| vor dem Malen | 0 | 174 |
| nach dem Malen | 50 | 258 |
| Undo – direkt gemessen | 50 (noch nicht zurückgesetzt) | 174 |
| Undo – nach 400 ms | 0 ✓ | 174 |
| Redo | 50 ✓ | 258 |
Codebeleg: UndoSystem.save.load() → tex.convertToInternal(image_data) → tex.updateSource(). Zusätzlich gibt es einen Sonderpfad: Wenn die Bilddatei auf der Platte seit dem letzten Speichern verändert wurde (source_overwritten), wird der Originalstand aus dem Undo-Snapshot rekonstruiert, damit man nicht versehentlich fremde Änderungen mit-überschreibt.
map.image === tex.canvas), nicht das Image-Element. Deshalb sind Pinselstriche ohne Zwischenkopie sichtbar – und deshalb ist die Umstellung auf image_data nur beim Undo nötig.Animation ist praktisch ein zweiter Editor in der App. Ich habe einen kompletten Zyklus aufgebaut: Rig → Keyframes (inkl. Molang) → Timeline-Wiedergabe → Export → Animation Controller.
// 1) Animator für eine Gruppe holen (erzeugt ihn bei Bedarf) const animator = anim.getBoneAnimator(armGroup); // 2) Keyframes hinzufügen – exakt der Pfad aus animation.js beim JSON-Import animator.addKeyframe({ channel: 'rotation', time: 0.5, interpolation: 'linear', data_points: [{x:0, y:60, z:0}, {x:0, y:60, z:0}] }); // 3) Molang als Wert – wird beim Abspielen live ausgewertet animator.addKeyframe({ channel: 'rotation', time: 1.0, interpolation: 'linear', data_points: [{x:0, y:'math.sin(query.anim_time * 180) * 45', z:0}, {x:0, y:'math.sin(query.anim_time * 180) * 45', z:0}] }); anim.setLength();
Timeline.setTime(t); Animator.preview(); // dann Rotation des Arm-Meshes auslesen
t = 0.00 s → 0.0° (Keyframe)
t = 0.25 s → 30.0° (interpoliert)
t = 0.50 s → 60.0° (Keyframe)
t = 0.75 s → 45.9° (Molang: math.sin(0.75·180°)·45 = 31,8 … hier greift die Frame-Quantisierung)
t = 1.00 s → 0.0° (Molang bzw. Loop-Grenze)
Wichtig: Die Animation läuft nicht über CSS oder Videos, sondern über echte Three.js-Transformationen – arm.mesh.rotation.y wird pro Timeline-Zeit neu gesetzt. Molang-Ausdrücke werden dabei mit query.anim_time gefüttert und bei jedem Preview-Schritt neu evaluiert.
// AnimationCodec.codecs.bedrock.compileFile([anim]) { "format_version": "1.8.0", "animations": { "wave": { "loop": true, "animation_length": 1, "bones": { "arm": { "rotation": { "0.0": { "pre": [0,0,0], "post": [0,0,0] }, "0.5": { "pre": [0,-60,0], "post": [0,-60,0] }, "1.0": { "pre": [0,"-math.sin(query.anim_time * 180) * 45",0], "post": […] } } } } } } }
Aus zwei Zuständen („idle", „waving") und Übergängen in beide Richtungen entstand live dieser Export:
{ "format_version": "1.19.0",
"animation_controllers": { "wave_controller": {
"initial_state": "idle",
"states": {
"idle": { "animations": ["wave"], "transitions": [{ "waving": "" }], "blend_transition": 0.2 },
"waving": { "transitions": [{ "idle": "" }], "blend_transition": 0.4 } } } } }
Die Übergangsbedingung ist ein freies Molang-Feld (hier leer). Zustände bündeln Animationen, Partikel-Effekte, Sounds sowie on_entry/on_exit-Skripte – der Controller-Editor ist entsprechend der größte Einzel-UI-Block der App.
Aus den Deep Dives ist ein echtes Werkzeug entstanden: bb_toolkit.js. Es nutzt bewusst die im ersten Teil beschriebenen Kernsysteme – Property-/Node-API, Undo-Aspects, Dialog-Form-API, Panels und die Action-Registry.
Analysiert das Projekt und findet: leere Gruppen, Würfel ohne Volumen, Würfel ohne Textur, vollständig transparente Flächen (echte Pixelanalyse der Textur), negative UV-Koordinaten, unbenutzte Texturen und Verletzungen der Format-Größenlimits. Der Dialog zeigt eine Übersicht, listet die Funde auf und lässt jeden Problemtyp einzeln beheben.
Behebt die vier häufigsten Probleme in einem Schritt – mit vollständigem Undo-Eintrag. Entspricht dem Blockbench-eigenen Muster für entfernte Flächen (nur Texturzuweisung zurücksetzen, UV bleibt erhalten).
Erzeugt parametrisch Turm, Pyramide oder Mauer aus Würfeln (Grundgröße, Etagen, Etagenhöhe) – jeder Block mit gesetzten UVs, alles in einer Gruppe und als ein Undo-Schritt.
Packt alle offenen Tabs als .bbmodel in ein ZIP – nützlich für Backups und Übergaben. Nutzt den Projekt-Codec und die Export-API.
Plugin geladen: actions:[toolkit_audit, toolkit_cleanup, toolkit_generator, toolkit_export_zip, toolkit_select_issues]
settings:[toolkit_check_blank_faces, toolkit_generator_size] + Testhaken aktiv
Testmodell (absichtlich kaputt): 4 Würfel · 2 Gruppen · 2 Texturen
Prüfer-Befund: {"empty_group":1, "untextured_cube":1, "zero_size_cube":1,
"uv_out_of_bounds":1, "blank_face":1, "unused_texture":1}
→ 4 Würfel · 0 Meshes · 2 Gruppen · 24 Flächen · 48 Dreiecke · 2 Texturen (512 Pixel)
Aufräumen: fixed: 4 · deaktivierte Flächen: 1 · danach 3 Würfel / 1 Gruppe
Undo: "Toolkit: Modell aufräumen" → 4 Würfel / 2 Gruppen zurück ✓
Generator: Pyramide 8×4 → 4 Würfel in Gruppe "pyramide" (Gesamt: 8)
ZIP-Export: 2 Tabs → 4260 Bytes → ["Bedrock Block.bbmodel (3836 Zeichen)",
"Java Block/Item.bbmodel (64 Zeichen)"] ✓
Markieren: Auswahl = ["test"] ✓
elements des Undo-Aspects stehen – Group hat kein getUndoCopy(), das wirft zur Laufzeit. Richtig ist { outliner:true, elements, groups }.face.texture = null. Wer zusätzlich face.uv = null setzt, zerstört die Undo-Wiederherstellung (Fehler in Merge.number). Blockbench selbst macht es in remove_blank_faces genauso minimal.Datei: blockbench/plugins_demo/bb_toolkit.js
Desktop: Blockbench → Datei → Plugins → „Plugin aus Datei laden" → bb_toolkit.js
Web: Blockbench → Datei → Plugins → Datei auswählen
Danach: Werkzeugleiste hat 2 neue Buttons · Tools-Menü hat die vier Einträge
· Datei-Menü hat „Alle Projekte als ZIP" · Einstellungen enthalten die zwei Toolkit-Optionen
Nach beiden Teilen ist die Landschaft vollständig vermessen. Vier Repositories, ein System:
| Erkenntnis aus Teil 2 | Warum sie zählt |
|---|---|
| Der Store ist ein Git-Repo mit zwei JSON-Dateien und einem CDN | Null Serverkosten, volle Versionierung, Community-Beiträge per Pull Request – mit automatischer Validierung als Türsteher |
Plugins laufen im Web ohne Sandbox, im Desktop mit SAFE_APIS-Whitelist | Die einzige echte Sicherheitsgrenze ist sozial: der Store-Review. Das erklärt, warum der Validator so streng auf Pfade und IDs achtet |
| Painter arbeitet ereignisgesteuert mit Canvas-Clipping + Occupancy-Matrizen | Deshalb fühlt sich Malen präzise an – und deshalb ist Bitmap-Undo asynchron |
| Molang wird nie aufgelöst, sondern überall als String mitgeführt | Das ist der Grund, warum Blockbench-Animationen dynamisch bleiben und vom Spiel zur Laufzeit ausgewertet werden können |
Alles ist über window erreichbar und dokumentiert | Ein Plugin kann in ~300 Zeilen Werkzeuge, Dialoge, Undo-Integration und Export bauen – wie das Toolkit zeigt |
| Pfad | Inhalt |
|---|---|
blockbench/ | App-Klon (v5.2.1) inkl. gebautem Bundle, läuft gerade auf Port 8000 |
blockbench.net/ | Website-Klon (Nuxt 4, 46 MB) |
blockbench-plugins/ | Store-Klon: 144 Einträge, 148 Plugins, Validierungs-CI |
blockbench/plugins_demo/bb_toolkit.js | Das Toolkit-Plugin (Modell-Prüfer, Aufräumen, Generator, ZIP-Export) |
blockbench/plugins_demo/tower_generator.js | Das einfachere Demo-Plugin aus Teil 1 |
blockbench-analyse.html | Teil 1: Architektur der App |
blockbench-analyse-teil2.html | Dieses Dokument |
blockbench-plugin-store-uebersicht.xlsx | Alle 144 Store-Plugins als filterbare Tabelle + Statistik |
test_painter.mjs · test_paint_undo.mjs · test_animation.mjs · test_toolkit.mjs | Playwright-Skripte für die Messungen in diesem Teil |
screenshots/ | Alle Roh-Screenshots und optimierten Fassungen |
blockbench-editionen.html – setzt die Analyse in zwei lauffähige Varianten um: eine AI Edition (Blockbench über 26 JSON-Kommandos von einem Agenten steuerbar, mit Agenten-Konsole und Kommandozeilen-Client) und eine Mobile Edition (Fingerbedienung mit Aktions-Knopf, 46-px-Zielen, Zwei-Finger-Tipp = Rückgängig). Live getestet mit 29 + 26 Prüfungen.Alle Messwerte in diesem Dokument stammen aus der laufenden, selbst gebauten Instanz (v5.2.1, Commit e2ede08) und den geklonten Repositories. Erstellt am 03.10.2026.
Lizenzhinweis: Blockbench steht unter GPL-3.0-or-later; das Toolkit-Plugin nutzt ausschließlich die öffentliche Plugin-API.