---
name: blockbench-script
description: Create and revise Minecraft Blockbench models by writing complete JavaScript scripts for voxel geometry, textures, sprites, and animations.
---

# Blockbench Script Authoring

Create and revise Minecraft Blockbench models with two required complete JavaScript scripts per model and one optional animation script: `model.js` generates the geometry and texture pixel map, `texture.js` paints the textures, and `animation.js` generates animations only when the user explicitly requests motion. Execute each present script as its own Blockbench evaluation. The saved scripts are the source of truth: every later revision edits one of the saved files and reruns only the affected phase. Do not build models one MCP mutation call per cube, and do not encode a model as a data payload in one monolithic file: each script is a whole generation program with its own procedural code.

## When to use

- User asks to create, change, or extend a Minecraft block/item/entity model, texture, sprite, or animation in Blockbench.
- A saved `blockbench-scripts/<model_name>/` directory exists (`model.js`, `texture.js`, and optional `animation.js`) and the user asks to tweak geometry, texture pixels, or explicitly requested keyframes.
- When the request is to refactor this Pi skill, modify the skill, templates, references, or their tests; do not edit a model-specific saved script unless the user explicitly names that model.

## Prerequisites

- Install the official Blockbench MCP plugin through **File → Plugins → Load Plugin from URL** with `https://jasonjgardner.github.io/blockbench-mcp-plugin/mcp.js`.
- The Blockbench MCP server must be configured and reachable (see `references/mcp-tool-map.md` for the endpoint and `.pi/mcp.json` project config shape).
- Verify with the MCP connection check; if it fails, report the exact setup path (`.pi/mcp.json` or `~/.pi/agent/mcp.json`) and the Pi recovery commands `pi install npm:pi-mcp-extension` and `/mcp`.

## Workflow

Two phases, never mixed: **generate all in one go**, then **tweak and finalization**. Phase 1 produces the complete static model and textures, plus animations only when explicitly requested; phase 2 refines a single artifact by editing and rerunning its script.

### Phase 1 — Generate all in one go

1. **Confirm the MCP connection.** If it fails, stop and report the setup path above.
2. **Select or create the project.** Call `get_project_info`. If no compatible project exists, call `create_project` **once**: use `bedrock_block` by default for static models, and use `bedrock` only when the user explicitly requests animations. This setup call is the only allowed preparatory mutation; the scripts build everything else.
3. **Choose the model name and write the required scripts** at `blockbench-scripts/<model_name>/`:
   - Derive `model_name` by converting the supplied model name to lowercase `snake_case`. If the user does not provide a model name, creatively make one out of the user's prompt, then convert it to lowercase `snake_case`.
   - `model.js` — procedural geometry plus a canonical `createTextureAtlas` declaration. Use `atlas.addBox(spec, paint)` for solids and `atlas.addSprite(spec)` for mask-driven silhouettes; use the returned `finish()` texture and cubes as `MODEL`/`TEXTURES`.
   - `texture.js` — procedural painting of the textures. Run second.
   - `animation.js` — procedural keyframes only when the user explicitly requests animation or motion; omit it otherwise and create no animation groups or keyframes.
   - When the request mentions `glowmask`, glow, glowing, emissive, emission, luminous, neon, bioluminescence, or emitting light, `model.js` must also create one aligned auxiliary texture named `<model_name>_glowmask` with `createGlowMaskTexture`; `texture.js` paints its RGB without changing its alpha.

  Generated names are deliberately short and contain only lowercase `snake_case`: groups are `group_<group_id>`, cubes are `cube_<cube_id>`, animations are `<model_name>_animation_<animation_id>`, the diffuse atlas texture is exactly `<model_name>` (Blockbench may display or export it as `<model_name>.png`), and the glowmask is exactly `<model_name>_glowmask`. Never use a `bbpi:` name.
  - Semantic anatomy groups use exact local names `head`, `body`, and `tail` when present; omit absent anatomy rather than creating placeholder groups.

   Compose each present file from `templates/runtime.js` (paste its full content after the IIFE opening) plus the matching `templates/<kind>.js` skeleton and your procedural generation code. The runtime and `templates/*.js` files are annotated source skeletons, not verbatim executable scripts: incorporate the relevant body into the self-contained IIFE and remove every source comment before sending the final payload to `risky_eval`. Each file must be a single self-contained IIFE, comment-free and `console`-free, whose final expression is the summary object. The reusable helper path is the default; raw `MODEL`/`TEXTURES` construction remains an advanced escape hatch subject to the same validation.
4. **Execute the present scripts in order**: always run `model.js`, then `texture.js`; run `animation.js` last only for an explicitly animated request. Send each exact file contents to upstream `risky_eval` with `{code}`. Parse the returned JSON summary and require schema version 2, the expected phase, and `modelId: "<model_name>"`. A static request stops after the texture phase. `risky_eval` is direct JavaScript execution, so execute only the reviewed saved script.
5. **Verify with readbacks** — MCP readbacks are authoritative, not the summaries alone: `get_project_info`, `find_elements_by_criteria` (generated groups/cubes), `list_textures` / `get_texture`, `capture_screenshot` (preview). Set the camera first with the `preview` object from the model summary. For animated requests, also verify animation tracks, loop closure, and animated overlaps; static requests must have no generated animations.
6. **Save the finished assets** after verification with the exact snake_case basenames: `<model_name>.bbmodel` for the native project, `<model_name>.png` for the diffuse texture, and `<model_name>_glowmask.png` when a glowmask exists. If animation was explicitly requested and generated, also export `<model_name>.animation.json`; do not invent an animation file for a static request. Use `list_export_formats` before `export_model` for the native project and Bedrock animation, and use each `get_texture` data URL to persist the corresponding PNG.

### Phase 2 — Tweak and finalization

- **Geometry or UV layout change** → edit `model.js`, rerun it, then rerun `texture.js`; if the model has an explicitly requested animation, rerun `animation.js` last because `model.js` rebuilds the textures and wipes animations.
- **Appearance change** (colors, gradients, detail) → edit `texture.js`, rerun only it. Colors may change; the alpha channel must not (see the texture contract).
- **Motion change** → edit `animation.js` and rerun it only for a model whose animation was explicitly requested. Animated-overlap validation runs against the current live model and textures.
- **Glow coverage or brightness change** → edit the glow pixel map in `model.js`, rerun `model.js`, then rerun `texture.js`; the glowmask alpha channel is geometry/coverage state. A glow hue-only change can edit `texture.js` and rerun only it.

- After any rerun, verify with the readbacks above. On a failed script, inspect live state before retrying or using `undo`; the upstream evaluator does not provide this skill's former phase-specific rollback guarantee.
- After any successful rerun, repeat the final asset save so `<model_name>.bbmodel`, `<model_name>.png`, optional `<model_name>_glowmask.png`, and any generated `<model_name>.animation.json` remain synchronized with the verified live project.

- **Final centering is mandatory after geometry cleanup**: use the bottom-surface center of the semantic `body` as the model center pivot, then align that pivot to world `[0, 0, 0]`. Author the `body` group pivot at that point (or the exact `body` cube origin when there is no body group). `runModelScript` performs the final rigid translation after overlap resolution and cleanup, preserves every relative offset and pivot, and fails if a declared body pivot does not coincide with the computed bottom-surface center. For a non-anatomical model with no semantic body, it centers the bottom of the complete solid model bounds instead.

## Authoring quality gates

Apply these gates while designing, not after a screenshot exposes the problem:
- **Default visual target**: pursue a realistic, vivid result whose complexity scales with the model's overall size and intended camera distance. Realistic means believable proportions, attachments, material boundaries, and light direction within the Minecraft voxel language; vivid means a readable silhouette, coherent high-contrast palette, and clustered surface detail. Small models get a few purposeful volumes and bold material regions; medium models get layered masses, recesses, and articulated structure; large models may add secondary structures and finer texture clusters. Never add geometry, pixels, or animation solely to increase complexity, and never invent animation when the user has not specified it.

- **Atlas first**: use one `createTextureAtlas` per model. `addBox` reserves the full `2*w+2*d` by `d+h` Box-UV rectangle; `addSprite` reserves the mask's planar rectangle. The builder packs deterministic shelves, inserts transparent padding, derives all UVs, and permits sharing only for one exact mirrored pair with one `mirrorUv: true` entry. Use `padding: 1` or greater when the texture detail pass will shade edges, so neighboring material regions cannot bleed into one another.

- **Minecraft block scale**: interpret user-facing “block” measurements as Minecraft block units, not one Blockbench coordinate. One block is exactly `16 × 16 × 16` integral Blockbench units; use `blockUnits(blockCount)` (`1 → 16`, `1/2 → 8`, `1/16 → 1`) rather than hardcoding conversions. Geometry coordinates remain Blockbench units, so negative offsets are valid and any conversion must still produce an integral unit.
- **Integral grid; fractional placement**: model resolution is integral. Every cuboid span (`to − from`), sprite mask dimension, UV coordinate, inflate value, and texture pixel stays on the integer Blockbench grid. `from`, `to`, origins, and group pivots may be fractional only to center an odd-width part, align a joint, or place a deliberately offset feature; preserve integral spans and state a concise `fractionalReason`. Partial transparency is allowed inside a pixel, but fractional pixels and anti-aliased edges are not.
- **World-centered body pivot**: the final model uses the semantic body's bottom-surface center as its center pivot. Body bounds come from the non-plate cubes directly parented to the exact `body` group, or from the exact `body` cube when no body group exists. Place that group/cube pivot at `[bodyCenterX, bodyBottomY, bodyCenterZ]`; do not use the torso midpoint. Finalization translates all cube bounds, cube origins, and group origins together until this point is world `[0, 0, 0]`, so attachments and animation pivots retain their relative positions.
- **Canonical Minecraft front**: inspect `Project.format.forward_direction` when available; for the default Bedrock Entity format it is negative Z (`-Z`). Author the unrotated model upright with `+Y` up, the face/nose/front extending toward north/`-Z`, the back/tail toward south/`+Z`, the model's right toward east/`+X`, and its left toward west/`-X`. Put front-facing texture features on north faces. Runtime entity yaw or a block's facing state may rotate the finished asset in-world; do not pre-rotate geometry to compensate. If an explicitly selected format reports another `forward_direction`, that format value is authoritative.

- **Representation pass — decide dimensionality before writing geometry**: evaluate every visible feature from the intended gameplay camera range and select the least-dimensional form that still produces the required silhouette, occlusion, and motion. Do this before `addBox`/`addSprite`; do not turn texture pixels into geometry afterward.

  | Visual requirement | Representation | Materialized authoring choice |
  | --- | --- | --- |
  | Reads as a mass from oblique views; needs rim depth, side faces, cast occlusion, collision volume, or a solid edge-on silhouette | **Cuboid** | One purposeful `addBox`; use a few shell boxes only when an actual thick interior/exterior wall is visible. |
  | Has a physical outline but no perceptible thickness: fin, leaf, membrane, web, flag, thin grill, pane, paper-like accessory, or a thin container wall | **Zero-thickness plate** | One `addSprite` mask; `_` pixels are physically absent. Put a moving plate in its own animated group. |
  | Is a hole, vent, window, notch, lattice opening, handle opening, or cut-out in a thin wall/container | **Erased plate pixels** | Start with `makeSolidMask`, call `eraseMaskRects`, and pass the result to `addSprite`. Never assemble a border of cuboids around every opening or fake a see-through opening with dark pixels. |
  | Is paint, a shallow color break, a printed marking, a seam without relief, a conformal patch, or a feature that must not alter the outline | **Atlas texture pixels** | Paint it in `texture.js`; do not add a coplanar plate or a surface cuboid. |
  | Is a genuinely thin item visible from both intended sides | **Two-sided plate** | Use `side: "both"` deliberately; otherwise use its one intended face. |

  A thin holed container is normally a mask-driven plate: the opaque mask pixels are its wall and `_` makes its openings. It may use several plates for separate visible walls, but never one cuboid per hole. Upgrade it to a few volumetric shell cuboids only when the viewer must see wall thickness, the inside floor/back/rim, or an edge-on profile; leave the opening as real negative space. A plate is also wrong when it must read as a solid bar from arbitrary camera angles, must occlude another object with visible depth, or must have a material thickness greater than one voxel.

  Treat the dimensionality decision as a visibility problem, not a category label: a flat painted symbol stays texture even on a container; a lace-like sheet stays a plate even when it has many holes; a thick basket, bucket, mouth, or open crate uses a small structural shell. Prefer one mask with deliberate holes over many pieces, one box with meaningful depth over stacked near-coplanar boxes, and texture over either when the outline does not change.
- **Volumetric geometry earns its cost**: add a cuboid only when the representation pass selects real volume. Build the primary mass from a few purpose-specific cuboids with stepped volumes and recessed structure; do not simulate texture detail with surface boxes, stack coplanar boxes to imitate a sheet, or construct one cube per pixel. Use `makeArticulatedSegment` for a physical limb, tail, stem, antenna, or connector, not visual noise.
- **Texture carries surface detail**: paint stripes, eyes and pupils, mouths, nostrils, scales, seams, spots, trim motifs, and material variation into the atlas texture. Give a detail physical geometry only when it changes the silhouette, needs real depth/occlusion, or moves independently; an eye is normally texture unless it is a deliberate protruding or animated form. Every major material region needs at least three tonal colors, directional highlight/shadow, edge accents, and small clustered variation. Use `applyTextureDetail` after material-specific painting; it preserves alpha and adds deterministic voxel-scale edge and cluster variation. Resolve texture-pass warnings instead of shipping a flat fill.
- **Glow/emission requests use a dedicated mask**: treat every semantic glow request as a request for a second texture, not yellow paint on the diffuse atlas and not an emissive-looking cuboid. The mask is atlas-aligned with the diffuse texture, has the exact suffix `<model_name>_glowmask`, uses `_` for every non-emissive pixel, and is kept as an intentional auxiliary texture even though no cube directly references it. Define its alpha in `model.js`: `0x00` is transparent, and larger alpha values are dim-to-bright emission. Define RGB for the intended emission color; `#RRGGBBAA` palette values support colored and translucent brightness. Use `makeTransparentPixelMap`, `writeGlowMaskRegion`, and `createGlowMaskTexture({source: atlasResult.texture, id: MODEL_ID + "_glowmask", prefix: PREFIX, palette, pixels})` so the runtime can reject glow pixels outside the diffuse atlas or an empty mask.
  - Keep glow pixels sparse and purposeful: core filaments, windows, eyes, runes, lava, screens, or bioluminescent markings may glow; ordinary shaded material must remain `_`. Do not run `applyTextureDetail` on the glowmask, do not blur or anti-alias it, and do not change its alpha in `texture.js`; use `repaintMapPreservingAlpha` only for glow RGB. A glowmask is an asset for an emissive-capable material/resource-pack pipeline; it does not create a physical light source in the Blockbench viewport or Minecraft by itself, so state that integration limitation when relevant.
  - Example shape (the rows must exactly match `placements["emitter"]` dimensions):
    ```js
    let glowPixels = makeTransparentPixelMap(atlasResult.texture.width, atlasResult.texture.height);
    glowPixels = writeGlowMaskRegion(glowPixels, placements["emitter"], glowRegionRows);
    const glowmask = createGlowMaskTexture({
      source: atlasResult.texture,
      id: MODEL_ID + "_glowmask",
      prefix: PREFIX,
      palette: { G: "#FFD35CFF", g: "#FFD35C66" },
      pixels: glowPixels,
    });
    EXTRA_TEXTURES.push(glowmask);
    ```

- **Plates are real thin silhouettes**: zero-thickness translucent sprite plates are single-sided by default; `side: "positive"` renders x/east, y/up, or z/south, while `side: "negative"` selects the opposite face and `side: "both"` is an explicit opt-in for a thin form genuinely seen from both intended sides. Use a plate for a thin outline or a perforated thin surface, never for a painted decal. A hole belongs in its mask as `_`, so it remains transparent through every later texture pass.
- **Plate orientation and arbitrary rotation**: author the mask in local coordinates with normal image orientation, not screen coordinates. Columns (`u`) increase along `planarAxes(axis)[0]`; rows (`v`) run downward from the upper edge, so row `0` starts at the maximum coordinate on `planarAxes(axis)[1]`. `axis` selects the local zero-thickness axis before rotation; any finite `rotation` may then orient the plate arbitrarily in 3D. For an arbitrarily oriented plate, use `makeSurfaceAttachment(anchor, [maskWidth, maskHeight, 0], normal, {inset: 0})` with `axis: "z"` and copy its pose. The runtime applies per-face UV rotation/flips, so do not transpose, rotate, or vertically invert mask rows to compensate for the viewing face.
- **Animation quality (only when explicitly requested)**: make motion vivid, organic, and readable rather than merely nonzero. Layer a primary action with delayed secondary motion, phase offsets, anticipation, follow-through, and a restrained idle/breathing accent where the anatomy supports it; use `makeVividTrack`, `sampleLoopKeyframes`, `organicWave`, and `easeInOutSine` from the runtime to sample smooth periodic curves on the tick grid. Treat the loop boundary as a cut through one continuous cycle, not as a beat: choose phase offsets so the seam usually lands during motion, and never ease every track to its neutral value merely to make the endpoint values match. Position and rotation channels use additive zero-rest values; scale channels use multiplicative positive factors around `[1, 1, 1]`, so keep every sampled scale component above zero and prefer bounded linear scale curves when catmullrom overshoot could collapse a part. Keep amplitudes bounded by the model's joints and validate the full animated sweep. Do not add an idle loop or any other animation by default.
- **Overlap closure**: define an explicit `resolveSurfaceOverlaps` rule table using local cube IDs. Each surface record gets exactly one `erase_plate_pixels`, `hide_face`, or `delete_element` rule; missing, duplicate, invalid, and stale rules fail closed. Interior records are ignored. `runModelScript` still performs final `verifyOverlaps`.
- **Animation closure (only when explicitly requested)**: for every loop, compare evaluated poses at `0`, `length`, and `2 * length`; every channel, including scale, must reproduce the same pose. Also compare the samples and velocities immediately before and after the seam: the incoming and outgoing interpolation, timing, and tangent must continue the same motion without a hold. The `length` keyframe is only the duplicated periodic sample needed by the format; do not add a neutral reset, endpoint ease, settling keyframe, or duplicate hold around it. `sampleLoopKeyframes` uses equally timed cyclic samples so wrapped catmullrom tangents remain coherent. Run the animated overlap sweep after the static model resolver has run.
- **Rotation**: plates and cuboids may use any finite Euler rotation when it clarifies anatomy, attachment, gesture, or the intended surface normal. Keep dimensions integral; rotation never justifies fractional voxels. Prefer 22.5-degree increments when they read equally well, but use the exact arbitrary angle required by an attachment and provide a concise `angleReason` for any non-22.5-degree static component. Use `makeArticulatedSegment`, `rotationFromNormal`, or `makeSurfaceAttachment` to derive attached poses instead of guessing Euler triples.

- Before yielding, verify the atlas, diffuse and glowmask alpha coverage, element counts, static overlaps, the exact `<model_name>_glowmask` texture name, and a rendered screenshot from the live project. For explicitly animated requests, also verify animated overlaps, loop seam, and generated animation tracks; static requests must have no animations created.


## Script contracts

### model.js

- `MODEL_ID` — `^[a-z0-9]+(?:_[a-z0-9]+)*$`, excluding `__proto__`, `constructor`, and `prototype`; `PREFIX = MODEL_ID + "_"`. The default model path is:
  `const atlas = createTextureAtlas({id: MODEL_ID, prefix: PREFIX, palette, maxWidth, maxHeight, padding});`
  followed by `atlas.addBox(spec, paint)` and/or `atlas.addSprite(spec)`, then `const {cubes, texture, placements} = atlas.finish()`. Use `MODEL = {id: MODEL_ID, name: MODEL_ID, groups, cubes}` and `TEXTURES = [texture]`; `placements` is the authoritative UV record.
- Every local group, cube, texture, and animation ID matches `^[a-z0-9]+(?:_[a-z0-9]+)*$` and excludes the reserved identifiers above. The runtime derives simple snake_case node names as `group_<group_id>` and `cube_<cube_id>`, model-scoped animation names as `<model_name>_animation_<animation_id>`, and `<model_name>`/`<model_name>_glowmask` for textures; cleanup links groups and cubes through the model's diffuse texture. A group parent must reference a declared group; parent graphs must be acyclic, while declaration order is intentionally irrelevant.
- For an explicit glow request, keep `atlasResult.texture` as the diffuse source and append a `createGlowMaskTexture` result to `TEXTURES` (normally through `EXTRA_TEXTURES`). The helper requires `id: MODEL_ID + "_glowmask"`, matches the source dimensions and UVs, preserves `_` as transparent, requires at least one nonzero-alpha pixel, and rejects emission outside source alpha. Use the atlas `placements` record rather than guessed UVs. The mask is intentionally not assigned to cube faces; downstream emissive material setup consumes the separate texture.

- Geometry fields (`from`, `to`, `origin`, group origins, and attachment sizes) are raw Blockbench units. Treat one Minecraft block as `blockUnits(1) === 16`. Every span (`to − from`) stays integral; positions and pivots may be fractional only with `fractionalReason`, so an offset never silently raises the model's grid resolution.
- Coordinate orientation follows the selected `ModelFormat.forward_direction`. In the default Bedrock Entity format, the canonical unrotated front is `-Z`: heads, noses, eyes, mouths, chests, and other forward silhouette cues face or extend toward decreasing Z; backs and tails extend toward increasing Z. `+Y` is up, `+X` is the model's right, and `-X` is its left when it faces `-Z`.
- `addBox` accepts `{id,parent,from,to,origin,rotation,inflate,cullface,fractionalReason,angleReason,mirrorOf}`. Paint is exactly `{fill: "<palette char>"}` or a complete `pixels` rectangle. A cuboid may be arbitrarily rotated; keep its three spans integral and provide `angleReason` if an angle is not a multiple of `22.5`. Use `makeArticulatedSegment(joint, [width, height, length], direction, {inset: 1})` for tilted limbs and physical connectors: it keeps dimensions integral, pivots at the proximal joint, and provides the required reasons. Use `makeSurfaceAttachment(anchor, size, normal, {inset: 1})` for other attached volume. Spread the returned `from`, `to`, `origin`, `rotation`, and reason fields into this spec. `mirrorOf` names an earlier same-dimension box, reuses its reservation, forces `mirrorUv: true`, and forbids paint.
- `addSprite` accepts `{id,parent,axis,from,mask,origin,rotation,side,doubleSided,fractionalReason,angleReason,mirrorOf}`. Use it for a physical thin silhouette or a perforated thin surface—not a stripe, eye, or flat decoration. Build a holed surface as `const mask = eraseMaskRects(makeSolidMask(width, height, "A"), [{x, y, width, height}]);`; `_` is a real transparent cut-out and the only place opacity may be changed. For an arbitrarily angled plate, use `makeSurfaceAttachment(anchor, [maskWidth, maskHeight, 0], normal, {inset: 0})` and copy its `from`, `origin`, `rotation`, and reason fields; keep `axis: "z"` so local +Z is the plate normal. Mask columns follow `planarAxes(axis)[0]`; mask rows follow the upper-to-lower image direction (row `0` at the maximum `planarAxes(axis)[1]` coordinate). The runtime applies face-specific UV rotation/flips, so do not transpose or reverse rows. All non-`_` characters must be palette keys, at least one pixel must be opaque, and `to` extends `from` by the mask width/height on the two planar axes, leaving exactly `axis` at zero thickness. The result is a translucent one-face plate by default; use `side: "negative"` for the opposite face or `side: "both"` only when two-sided rendering is intentional. `doubleSided: true` is a compatibility shorthand for `side: "both"`. A mirrored sprite follows the same earlier-entry and no-mask rule.
- The builder emits succinct snake_case cubes, textures, and animations, deterministic shelf packing, transparent padding, power-of-two texture height, and fail-closed dimensions. Raw `MODEL`/`TEXTURES` remains an advanced escape hatch and must satisfy the same identity, hierarchy, palette, plate, UV, overlap, and resolution validation. Do not edit `blockbench-scripts/xenofish/` for reusable-system changes.

### texture.js

- Starts with `const records = beginTexturePaint(PREFIX);` — finds the model's textures, disables smoothing, snapshots the alpha channel, and opens one undo boundary. Keep all painting and `finishTextureScript(MODEL_ID, records)` in the template's `try`/`catch`, which calls `abortTextureScript(MODEL_ID, records)` and rethrows so a paint failure closes the runtime edit without claiming rollback.
- Painting may change colors but **never the alpha channel**: the model script's pixel map defines opacity and its erasures stay transparent. Paint visual-only features—eyes/pupils, stripes, markings, seams, and decorative motifs—onto the matching model.js atlas regions rather than adding surface geometry. Prefer `paintRectPreservingAlpha(ctx, x, y, w, h, "#RRGGBB")` or `repaintMapPreservingAlpha(ctx, w, h, palette, pixels)` for material passes, then run `applyTextureDetail(ctx, {seed, strength})` as the final deterministic edge/cluster pass; any raw canvas call must leave alpha untouched.
- `finishTextureScript(MODEL_ID, records)` verifies alpha is unchanged (throws `texture-alpha-changed` otherwise), reports low-tone/low-transition warnings, updates the texture sources, and refreshes. Its summary is `phase: "texture"`.
- For a glowmask record (`record.glowmask`), repaint only RGB with `repaintMapPreservingAlpha` or equivalent `ImageData` writes. Never call `applyTextureDetail`, `clearRect`, `fillRect`, or any raw operation that changes alpha on that record. The model phase owns emission coverage and brightness; `finishTextureScript` verifies alpha preservation and reports glow pixel count/max alpha separately from diffuse tone warnings.
  - A typical glow RGB pass is `const glow = records.find(record => record.glowmask); if (glow) repaintMapPreservingAlpha(glow.ctx, glow.width, glow.height, {G: "#FFD35C", g: "#FFF4B0"}, glowRgbPixels);`, where `glowRgbPixels` has the same full-atlas dimensions and `_` everywhere except the already-covered emissive pixels. The palette here supplies color only; its alpha is ignored so the model phase's brightness remains authoritative.

- Repaint every visible region with discrete pixel clusters and directional shading before the detail pass; preserve alpha coverage exactly. Treat the atlas as the source of visual detail and retain cuboids only for forms that must read as volume, silhouette, articulation, or real occlusion.
- Idempotent: rerunning repaints over the same canvases.

### animation.js (only when animation is explicitly requested)

- Starts with `const model = readLiveModel(PREFIX);` — snapshots the live groups, cubes, and texture alpha associated with the model's diffuse texture, so validation always runs against the current geometry (safe after any `model.js` tweak).
- Procedural code computes `ANIMATIONS` (`{ id, name: PREFIX + "animation:" + id, length, loop, tracks }[]`) only for an explicitly animated request. Tracks: `{ bone, channel, keyframes }`; keyframes: `{ time, value, interpolation }` with `value` a three-number vector. Position values are locations, so fractional values are permitted (matching fractional group origins); rotation values are unrestricted; scale values are multiplicative factors where `[1, 1, 1]` is identity and every component must stay finite and positive, including interpolated samples. Times are quantized to Minecraft's 20-tick grid (multiples of `0.05`). Prefer `makeVividTrack` for sampled `catmullrom` motion or pass `interpolation: "linear"` when the authored samples already contain the easing; use linear scale samples when a curve could overshoot through zero.
- Every animation track must target an existing live group, and an animation may contain exactly one track for each `(bone, channel)` pair. Treat a duplicate track as a validation error rather than relying on Blockbench’s keyframe conflict behavior.
- Make every loop visually intentional: use at least a primary motion, a delayed or counter-phased secondary motion, and one small settling/breathing accent where the anatomy supports it. Favor asymmetric phase offsets over synchronized sine waves, include anticipation before a strong beat and follow-through after it, and place the seam as a mid-motion cut through the periodic trajectory. The values at `0` and `length` are identical because they are the same cyclic sample, not because both were forced to a neutral/rest pose. This requirement applies only after the user requests animation.
- Inspect the loop at `0`, `length`, and `2 * length`, plus one sample immediately before and after each boundary. Every channel must reproduce the same pose and continue with compatible incoming/outgoing velocity; reject neutral reset padding, seam holds, unequal wrapped sample spacing, or a tangent change. Preview at least two consecutive cycles so a pause that is invisible in a single-cycle scrub is observable. Animation overlap validation must also pass after the static model resolver has run. `catmullrom` is evaluated by the runtime's overlap sweep; avoid it for scale when overshoot would approach or cross zero.
- Ends with `runAnimationScript(model, ANIMATIONS)` — validates the animations, loop coherence, and animated-pose overlaps, then replaces the prefix's animations. Its summary is `phase: "animation"`.
- Idempotent: rerunning removes the prefix's animations and recreates them. Never run this phase, create an empty `ANIMATIONS` list, or create placeholder keyframes for a static request.

## Rules the engine enforces (in `templates/runtime.js`)

- **Integral grid and scale contract**: one model unit per voxel, one sprite pixel per voxel, and one Minecraft block per `16` model units on every axis. `blockUnits(blockCount)` is the canonical conversion and rejects non-finite, out-of-range, fractional, or sub-unit non-zero counts. Cube spans, UVs, inflate, and sprite dimensions are integral; raw-model and atlas entries reject fractional placement unless it carries `fractionalReason`. Palette keys are one character other than `_`; values are exactly `#RRGGBB`, `#RRGGBBAA`, or four integer channels in `0..255`.
- **Texture resolution**: the runtime sets each created texture's `uv_width`/`uv_height` to its canvas dimensions and raises project resolution monotonically to the maxima of existing and new textures. It never depends on a global `Texture.prototype` patch.
- **Final centering**: after overlap resolution and cleanup, `centerModelOnBodyBottom(model)` rigidly translates every live cube and group so the body's bottom-surface center is world `[0, 0, 0]`. A semantic `body` group uses its directly parented solid cubes for the body bounds and must already pivot at that anchor; without a body group, an exact `body` cube supplies both bounds and pivot. Models with neither use the complete solid-model bottom center as a non-anatomical fallback. The model summary reports the source, original anchor, applied offset, and final pivot.
- **Surface-mask and attachment support**: `makeSolidMask(width, height, pixel)` creates an opaque thin-surface mask; `eraseMaskRects(mask, [{x, y, width, height}])` returns a copy with validated transparent holes and rejects out-of-bounds cut-outs. `rotationFromNormal` maps local +Z to a ZYX Euler rotation; `makeSurfaceAttachment` centers a cuboid/plate at an anchor and can inset solids into the host. It accepts arbitrary finite normals and returns the fractional-coordinate and non-standard-angle reasons required by the validation gates. `makeArticulatedSegment` specializes that pose for positive-length integral structural segments and defaults to a one-unit proximal inset.
- **Plates**: exactly one zero dimension, explicit `plateAxis` and `translucent`, no inflate. `addSprite` extends the other two axes by the mask width/height; mask `_` pixels remain alpha-zero through the texture phase and therefore make genuine openings. `cubeFaces` enables one positive-side face (`east`, `up`, or `south`) by default; `side: "negative"` selects `west`, `down`, or `north`, and `side: "both"` explicitly enables both. Plate face UVs include the Blockbench rotation/flip needed to keep atlas columns on local planar axis 0 and the mask's upper-to-lower rows on planar axis 1; row `0` is the upper edge, not the lower edge. The local plate may be arbitrarily rotated after this mapping; do not hand-correct the mask per face. Hiding one enabled side leaves any opposite-side overlap classification active.
- **Overlap handling**: call `resolveSurfaceOverlaps(model, records, rules)` with local cube IDs and exactly one ordered rule per surface record: `erase_plate_pixels`, `hide_face`, or `delete_element`. Interior records are ignored; missing, duplicate, invalid, and stale rules throw `unhandled-surface-overlap`, `ambiguous-surface-overlap`, or `unused-overlap-rule`. `runModelScript` retains final `verifyOverlaps`.
- **Animated poses**: the same checks run at every 20-tick sample, keyframe time, and loop endpoint for every animation; fast intervals are swept and subdivided. Position and rotation are applied additively; scale tracks are applied as multiplicative factors to group scale, with `[1, 1, 1]` as the neutral value. The overlap transform includes animated non-uniform scale and uses a conservative axis-aligned bound when a scaled child rotation is not an orthogonal box. Interior overlaps are allowed at every pose and deduped into `animatedInteriorOverlaps`; a surface overlap at any sampled pose throws `animated-overlap`.
- **Loops**: `loop: "loop"` animations need keyframes at `0` and `length` for every animated bone/channel, identical poses within `1e-6`, matching seam interpolation, and 20-tick-quantized times. Dynamic tracks must also have no repeated seam hold; linear tracks must preserve seam velocity, catmullrom tracks must use equal first/last sample intervals, and an animation may not ease every dynamic track into a slow neutral rest at the boundary. The seam is a periodic cut, not a reset frame.
- **Vanilla consistency**: stable snake_case names, mirrored paired limbs by default (`mirrorUv` shares the texture), opaque volumetric faces by default, no meshes or one-cube-per-pixel construction, flat materials (no PBR/emissive/gradient) unless requested, `cullface` only on opaque face-adjacent solids. Orphan groups, fully-transparent plates, all-faces-disabled cubes, and unreferenced non-auxiliary textures are removed before success; a validated `role: "glowmask"` texture is intentionally retained without cube assignment.

- **Glowmask engine rules**: glowmask textures use `role: "glowmask"` and `sourceTexture` metadata, share the source texture's dimensions/UV coordinate system, and are validated against source alpha. `createGlowMaskTexture` rejects invalid IDs, alpha-zero palette entries, empty masks, unmapped emission, and non-integral regions. The texture phase includes glowmask textures in its alpha snapshot, but its quality heuristics do not demand diffuse-style tone transitions.

- Each script wraps its mutation in one `Undo.initEdit(...)` / `Undo.finishEdit(...)` boundary and refreshes with `Canvas.updateAll()` (or `Canvas.updateView(...)`). Caught model-build, animation-build, and texture-finalization failures finalize that runtime edit with a failed label; this records partial state for undo but does not roll it back. The upstream `risky_eval` tool may add its own edit boundary, so this package makes no exact undo-count guarantee.

## Revising a model (rerun)

1. Read the relevant `blockbench-scripts/<model_name>/<kind>.js` file.
2. Inspect current state with the read tools (`get_project_info`, `find_elements_by_criteria`, `list_textures`, `get_texture`) and the generated snake_case node names.
3. Edit the procedural code in that one file.
4. Re-read the whole file and execute it once through `risky_eval({code})`; parse and validate its schema-v2 summary, then rerun dependent phases per the rules above.
5. For a glow request, verify `list_textures` and `get_texture` show both the diffuse atlas and the exact `<model_name>_glowmask` asset; check that transparent pixels remain transparent and colored/translucent emission pixels retain their intended alpha.
6. Verify names, counts, textures, and animations are stable: generated groups use `group_<group_id>`, cubes use `cube_<cube_id>`, animations use `<model_name>_animation_<animation_id>`, and no duplicate generated nodes exist.

## Safety and fallbacks

- Execute `risky_eval` only after explicit model-edit intent and only with the reviewed saved script. It is direct JavaScript execution, not a sandbox.
- Each script deletes only the model's diffuse-texture-linked `group_`/`cube_` nodes, scoped animation names `<model_name>_animation_`, or the exact texture names `<model_name>`/`<model_name>_glowmask`; unrelated content is untouched.
- A caught model-build, animation-build, or texture-finalization error closes the runtime edit as a failed operation, but does not restore partial state. After any post-mutation error, inspect the live project with readbacks before retrying or using the MCP `undo` tool.
- Never silently downgrade an animation request to a static format; if the project format lacks animation mode the animation script fails before mutation.
- If `risky_eval` is unavailable, the documented batch MCP tools (`create_texture`, `create_animation`, `manage_keyframes`, `place_cube`, `modify_cube`) are a clearly reported, less-atomic fallback; never present the fallback as one script run.

## Reference

- `templates/runtime.js` — the shared engine (validation, overlap classification, resolvers, live-state readback, drivers).
- `templates/model.js`, `templates/texture.js`, `templates/animation.js` — per-script skeletons.
- `references/blockbench-runtime-api.md` — Cube, Group, Texture, ModelFormat, Undo, Canvas facts plus the live-state readback.
- `references/animation-api.md` — Animation, BoneAnimator, KeyframeOptions, loop rules.
- `references/mcp-tool-map.md` — Blockbench MCP tools, `risky_eval` contract, endpoint config.
