CLAUDE.md · diff
git:20261008.dc3f3cb to git:20261008.981b740
1 added, 1 removed. Audit A to A.
<!-- standards-version: 1.10.0 -->
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
**Division of labor:** this file carries repo-specific operational facts an agent needs at runtime — content inventory, Blender runtime discovery, git staging hazards, and the example-shipping quality gates. `AGENTS.md` carries fleet-standard governance and workflow rules — branching, commits, merge and CI-evidence policy, release automation, authoring standards. Read both; neither repeats the other.
## Project Overview
The **Blender Developer Tools** repository is at **v0.151.0**. It packages skills, rules, snippets, starter templates, and runnable smoke-gated examples for Blender Python development with Cursor and Claude Code. Coverage targets **Blender 5.2 LTS** (current stable) with **Blender 4.5 LTS** fallback. **Blender 5.1** is prior stable. There is no MCP server; content is consumed directly by the AI when working in Blender add-on or scripting projects.
**Version:** 0.151.0
**License:** CC-BY-NC-ND-4.0
**Author:** TM Hospitality Strategies
## Repository Architecture
```
skills/<skill-name>/SKILL.md - AI workflow definitions, 18 total
rules/<rule-name>.mdc - Anti-pattern rules, 9 total
templates/<template-name>/ - Starter projects, 3 total
snippets/<snippet-name>.py - Standalone code patterns, 29 total
examples/<name>/ - Runnable smoke-gated examples, 66 total (+ gallery.json)
- showcase/<name>/ - Budget-conformance props, 79 pieces, each with a gallery `category` (sibling of examples/; see showcase/README.md § Categories)
+ showcase/<name>/ - Budget-conformance props, 80 pieces, each with a gallery `category` (sibling of examples/; see showcase/README.md § Categories)
scripts/build_gallery.py - Regenerates docs/gallery/ from examples/gallery.json + showcase/gallery.json
scripts/site/ - Vendored landing-page build (Jinja2); tokens.css is the shared palette
tests/check_site_links.py - Internal link/anchor/alt gate over the built site (docs/)
docs/gallery/ - Committed generated gallery pages + hero renders
VERSION - Source of truth for the repo version
```
## Skills (18)
| Skill | Purpose |
| --- | --- |
| addon-scaffolding | Extensions Platform manifest, file layout, register/unregister symmetry |
| ai-mesh-cleanup | Ordered cleanup for imported generated meshes: units, transform apply, origin, normals, budget, collider |
| bake-high-to-low | Cycles cage-bake of high-poly detail onto a low-poly target as a tangent-space normal map |
| engine-export-presets | Unity, Godot and Unreal presets: glTF is +Y up and meters for every engine (export_yup=True; Unreal converts to cm on import), FBX uses axis_forward/axis_up plus centimeter scale |
| operators | `bpy.types.Operator` lifecycle, `bl_idname`, redo, defensive context handling |
| ui-panels | `bpy.types.Panel` declarative `draw()`, layout primitives, conditional UI |
| custom-properties | `bpy.props` annotations, PropertyGroup, PointerProperty, storage tradeoffs |
| mesh-editing-and-bmesh | When to use bpy.data vs bpy.ops vs bmesh, foreach_set, depsgraph eval |
| headless-batch-scripting | `blender --background --python`, temp_override, argparse after `--` |
| slotted-actions-animation | Blender 5.x Slotted Actions, channelbag, 4.5 LTS fallback bridge |
| geometry-nodes-python | Programmatic GN tree construction, interface sockets, NODES modifier |
| procedural-materials-and-shaders | Node tree construction for Principled BSDF, emissive, node groups, EEVEE Next vs Cycles |
| depsgraph-and-evaluated-data | `evaluated_get` / `to_mesh` / `to_mesh_clear` lifetime contract for exporters and measurement |
| drivers-and-app-handlers | Driver expressions, `driver_namespace`, application handlers including the new 5.1 `exit_pre` |
| bl-info-migration | Three-step migration from legacy `bl_info` to Extensions Platform, dual-format pattern |
| vse-python | VSE timeline from Python: `.strips` vs `.sequences`, `new_effect` kwargs, 5.2 COLOR `width`/`height` bake |
| extension-runtime-and-packaging | Bundled wheels, `extension_path_user` data that survives upgrades, `online_access`, `bl_ext` names, `--command extension` build in CI |
| timers-modal-and-threading | `bpy.app.timers` return values and `persistent`, modal operators on `event_timer_add`, worker thread + queue + main-thread drain, no timers in `--background` |
## Rules (9)
Every rule sets `alwaysApply: false`, so the Scope column below is the rule's
actual load condition, not a description: a rule enters context only when a file
matching its `globs` does. Changing a glob changes when the rule fires.
| Rule | Scope | What it flags |
| --- | --- | --- |
| prefer-data-over-ops-in-loops | `*.py` | `bpy.ops.*` calls inside iteration over many objects |
| always-free-bmesh | `*.py` | `bmesh.new()` without paired `bm.free()` in a `try`/`finally` block |
| target-extensions-platform-format | Add-on roots | Legacy `bl_info` only add-ons missing `blender_manifest.toml` |
| type-annotate-props-and-defend-context | `*.py` | `bpy.props` defined as assignments, unguarded `context.active_object` |
| prefer-temp-override-over-context-copy | `*.py` | `bpy.context.copy()` passed to operators (deprecated 3.2, removed 4.0) |
| use-foreach-set-for-bulk-data | `*.py` | Python loops over `mesh.vertices` setting bulk attributes one at a time |
| validate-imported-mesh-scale | `*.py` | glTF/FBX import then mesh work with no `transform_apply` and no unit-scale check |
| no-unapplied-modifiers-on-export | `*.py` | Export with live modifiers when the export does not request evaluated geometry |
| use-correct-axis-rna-per-exporter | `*.py` | `export_scene.gltf` with FBX `axis_forward`/`axis_up`, or `export_scene.fbx` with glTF `export_yup` |
## Templates (3)
`templates/extension-addon-template/` is a copy-paste-ready Blender extension demonstrating:
- `blender_manifest.toml` with `blender_version_min = "4.5.0"`
- `register_classes_factory` registration pattern
- One Operator, one Panel, one PropertyGroup
- A `PointerProperty` bound to `bpy.types.Scene`
- Symmetric `register()` / `unregister()` with property cleanup before class unregister
`templates/headless-batch-script-template/` is a working starter for unattended Blender batch jobs:
- `argparse` parsing of args after the `--` separator
- Iteration over every mesh object via `bpy.data.objects`
- Modifier application via `bpy.context.temp_override` (not the deprecated context-dict form)
- glTF export via `bpy.ops.export_scene.gltf`
- Explicit exit codes for CI integration
`templates/ai-asset-pipeline-template/` is a working starter for a headless GLB-in / engine-ready-out job:
- `argparse` parsing of args after the `--` separator
- Import, unit-scale check, transform apply, origin, normals, evaluated tris
- LOD chain and optional convex/box collider
- Unity / Godot / Unreal glTF export via the engine-export-presets contract
- Explicit exit codes matching `headless-batch-script-template` (0, then 2+)
## Snippets (29)
Small standalone `.py` files at `snippets/<name>.py`, each 5 to 75 lines.
v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh load-edit-free, temp_override context, foreach_set vertex bulk write, register_classes_factory, PointerProperty binding, cross-version property delete, and the `action_ensure_channelbag_for_slot` slotted-actions bridge.
v0.2.0: Principled BSDF material, driver-with-custom-function via `driver_namespace`, application handler registration, shader node group with cross-version `interface` API, `foreach_get` bulk vertex read, version-branch skeleton, and USD export with `evaluation_mode='RENDER'`.
AI asset pipeline track: `decimate_to_budget.py`, `convex_hull_collider.py`, `lod_chain.py` (helper duplicated, not imported), `gltf_draco_export.py`, `export_preset_unity.py`, `export_preset_godot.py`, `export_preset_unreal.py`, `setup_bake_target_image.py`, `bake_normal_high_to_low.py`, `save_baked_image.py`.
Extensions runtime: `extension-user-data.py` (`extension_path_user` settings file and an `online_access` guard). Long-running work: `thread-queue-timer.py` (worker thread, `queue.Queue`, main-thread drain timer).
## Examples (66)
Runnable scripts at `examples/<name>/`, each asserting a real API contract with
deterministic checks (exit non-zero on failure) and optionally rendering a still via
`--output`. All of them run headless on Blender 5.2 LTS and 4.5 LTS in `blender-smoke.yml` (5.1 on the weekly cron, the `needs-5.1` PR label, or manual dispatch);
**56 of the 66 ship a render in the site gallery** at `docs/gallery/`. The other
ten are **check-only**: they carry no `--output` path, no gallery entry, and no
hero asset. The criterion is whether the contract is expressible in pixels. An
example is check-only when its witness is a data or state fact that no scene
redesign can make visible — a datablock name, an RNA attribute's presence, a
post-exit sidecar, a topology count, export metadata — so a render would look
identical whether the API held or broke. That is a category, not a shortcut: a
subject whose render *could* be made legible must be rendered, per
`docs/new-example-prompt.md`. Smoke coverage is unaffected; every check-only
example still carries a `tests/smoke/catalog.json` row and a falsifier.
`examples/gallery.json` is the
gallery's source of truth. When authoring a new gallery example, copy the anatomy of
`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and
wire all of: gallery.json entry (with an `alt` sentence describing the still; the build fails without one),
a `tests/smoke/catalog.json` row, an `examples/skills.json` entry naming the skills it proves (`python scripts/build_examples_index.py` then regenerates `examples/index.json` and each skill's Runnable examples section; Validate fails on a missing entry), hero webp (1280×720) in
`docs/gallery/assets/` + its 640×360 card variant `<name>-hero-640.webp` (`python scripts/make_thumbs.py`; the gallery build exits 5 without it) + preview webp (1200×675), then run `python scripts/build_gallery.py`.
A check-only example wires only the
`tests/smoke/catalog.json` row, its `examples/skills.json` entry, and its README — it is deliberately absent from
`examples/gallery.json` and `docs/gallery/`.
Renders must conform to the gallery look spec at `docs/VISUAL-STYLE.md`.
Render paths gate framing through the shared helper `examples/gallery_framing.py` —
imported via a `__file__`-relative `sys.path` shim, the repo's only
cross-example import mechanism (see its docstring).
## Blender Runtime Discovery
- Local Blender binaries: check `.scratch/` at the repo root **first** — some machines have no system Blender install, and a prior agent run downloads official releases there (e.g. `.scratch/5.2/blender-5.2.x-.../blender[.exe]`). Then check system installs. Do not probe blindly; locate the binary, run it, and state the **exact binary path and the version the binary itself reports** in every report.
- **5.2 LTS is the local check version. 4.5 LTS is exercised by CI when unavailable locally.** 4.4 is not a substitute for 4.5 and must never be reported as 4.5.
- **CI floats within each series:** `blender-smoke.yml` resolves the highest published point release at run time (`sort -V | tail -1` on the download listing), so a local 4.5.x may lag CI — its 4.5 job ran 4.5.12 LTS on PR #107 while local `.scratch/` held 4.5.11. Either way, state the exact version the binary reports.
- If `.scratch/` lacks a needed version, download an official release from download.blender.org into it. `.scratch` is gitignored.
- In scripts, version-branch on the `bpy.app.version` tuple, never on `bpy.app.version_string` — it reads e.g. `"4.5.11 LTS"`, not bare semver.
## Live MCP vs Headless Harness
Two execution paths exist for Blender work. The headless harness (`.scratch/` or system binaries, check-only scripts, CI smoke) is the **only source of truth for correctness** — every witness, check, and CI gate runs headless. The official Blender Lab MCP server ([lab/blender_mcp](https://projects.blender.org/lab/blender_mcp)), when registered on the local machine, provides a live interactive session for exploration.
- **Use the live MCP for:** interactively inspecting a scene while designing a render, querying data relations, diagnosing why staged geometry or lighting looks wrong, and prototyping API calls before committing them to a script.
- **Never use the live MCP for:** anything a check depends on, anything reported as verification evidence, or anything CI must reproduce. A behavior observed in the live session must be re-proven in the headless script before it counts. Examples must never import from, depend on, or assume the MCP server.
- **Safety:** the server executes generated code in the running Blender instance without guards. Only attach it to scratch scenes, never to files you cannot afford to mutate.
- **Availability is machine-local and optional:** if the server is not registered, proceed headless without comment.
## Git Staging
Stage with **explicit paths only** — never `git add -A` or `git add .`. Cursor agent sessions inject a local guidance block into the working-copy `CLAUDE.md`; a bulk add sweeps it into the commit. It manifests as a `CLAUDE.md` hunk in `git diff` that you did not author (the file can be dirty before you touch anything). Leave it unstaged, and never stage `CLAUDE.md` unless you deliberately edited it.
## Quality Gates for Example Runs
- `docs/VISUAL-STYLE.md` is the **binding** render standard; deviations are defects.
- **Framing gate:** Layer 1 framing is measured, not eyeballed — the example's `--output` render path calls the shared helper `examples/gallery_framing.py` (`check_framing`, exit 10 on violation) before writing the still: hero fill 0.70–0.90 of frame in at least one axis, every element that matters clearing all four edges by ≥ 0.02. The check-only path never invokes it, so smoke runtimes are unaffected. The helper measures; call sites enforce. Bleed compositions call `measure_framing_deviation` and assert their own cap at the call site — `check_framing` has no `deviation=` flag.
- **Contact-sheet gate:** composite the candidate hero beside the pinned calibration set — currently `armature-bend`, `damped-track-aim`, `bmesh-gear` — commit the composite under `docs/gallery/contact-sheets/`, link it in the PR body, and report per-criterion verdicts (stage darkness, wedge warmth, subject fill, saturation, thumbnail legibility) including mean luminance versus the calibration images. A claim without the committed composite is not acceptable evidence. **This list is the canonical home of the pinned set** — update it here when a new example outclasses a member; `docs/new-example-prompt.md` points here rather than naming members. The longer "calibration references" list in `docs/VISUAL-STYLE.md` is a style reference, not this contact-sheet set. Tooling: `python scripts/render_hero.py --blender PATH --only NAME` renders the README `--output` command and writes the hero, its 640 card variant, and the preview webps (the one committed PNG-to-webp path); `python scripts/contact_sheet.py NAME` builds the composite in the committed format and prints each tile's luma, stage, wedge warmth and saturation; `python scripts/measure_hero_drift.py` checks committed heroes against what the code renders now.
- **Asset-sheet gate (asset-type examples — game props/kits):** composite the hero asset rendered alone (neutral three-quarter view, plain studio lighting, no staging tricks, no labels, no comparison props) beside the pinned asset-quality reference set — currently `collision-hull-proxy`, `socket-attach-points`, `vertex-color-ao`, `wheelbarrow`, `apothecary-shelf` — rendered the same way; commit under `docs/gallery/asset-sheets/`, link it in the PR body, and report a verdict. The asset ships only if it is not identifiable as the least-designed object in that lineup — a strong scene can carry a weak model; this gate removes the scene. **This list is the canonical home of the reference set** — update it here when a new asset outclasses a member. A reference may be an example or a showcase piece. Tooling: `python scripts/asset_sheet.py --blender PATH NAME` renders the candidate and this set (read from this line) through `scripts/asset_sheet_panel.py` and writes the composite. The measurable floors behind the gate (naming, material variation, edge treatment) live in `examples/gallery_asset_quality.py` — render path only, same call pattern as `gallery_framing`, exit 11 on violation — with the calibration table and dropped-floor evidence in `docs/VISUAL-STYLE.md` § Asset quality.
- **Falsification:** every check must be proven to fail once — break the contract, observe the non-zero exit, restore — with the probe and the measured error reported in the PR body. An assertion that cannot fail witnesses nothing.
- **After gallery regeneration** (`python scripts/build_gallery.py`), read the **generated HTML** character by character — the `<img alt>` text and witnesses callouts in `docs/gallery/index.html` and `docs/gallery/<name>/index.html` — not just `examples/gallery.json`. Precedent: the `teaches.split(".")[0]` bug truncated 14/21 card alts at dotted API paths like `bmesh.ops` while the source JSON looked fine (fixed in PR #68).
- **Playwright gallery captures:** gallery `<img>` tags lazy-load, so force them first (`document.querySelectorAll('img').forEach(i => i.loading = 'eager')`, then wait). **Scroll the target card into view and take a viewport capture** — `scrollIntoView({block:'center', behavior:'instant'})`, short wait, `browser_take_screenshot` with `fullPage` omitted. A `fullPage` capture is NOT a workaround: on a tall gallery page it renders every card image blank even when the images are verified loaded (`complete === true`, `naturalWidth === 1280`, `opacity === 1`) — measured on the 45-card grid at 1425x4516. Verify load state via `browser_evaluate` rather than trusting the pixels.
## Smoke skip and post-exit
The host runner is `tests/smoke/run_example.py`. Shipped examples are listed in
`tests/smoke/catalog.json` (not a new YAML step per example).
- **SKIP:** print `SMOKE_SKIP: <reason>` and `sys.exit(77)`. Legal only when the
catalog/runner `--min-version` is **above** this Blender. Exit 0 with that
marker is FAIL (vacuous). Skip on a version that should run is FAIL.
- **Post-exit sidecar:** opt-in `--expect-sidecar PATH`. The example writes
`$BDT_SMOKE_SIDECAR` (set by the runner). The harness asserts after Blender
exits. Not a gallery still.
- **Falsifiers in CI:** every catalog row carries `falsifiers`, at least one
`{"args": [...], "expect_exit": N}` (N not 0, 1, 2 or 77; 2 is argparse usage) or, for a contract
the sidecar check owns, `{"args": [...], "expect_sidecar_fail": true}`.
A falsifier that targets a version-specific API change takes
`"min_version": "5.2"` and is recorded as SKIP below it (its README must
say it exits 0 there).
`run_catalog.py` runs each after the row's happy path; it passes only on
exactly that exit. `tests/check_smoke_catalog.py` rejects a row without one
and any unknown key. The weekly cron also runs the full showcase sweep
(`tests/check_falsifier_targets.py --run`).
- **Summary:** `tests/smoke/summarize.py` prints passed/skipped/failed. Zero
PASSes makes the job red even if every example skipped cleanly. Falsifier
runs are tabled separately and never count toward that PASS.
## Example-Run Process
- The canonical example-creation prompt lives at `docs/new-example-prompt.md`; keep it in agreement with this file and `AGENTS.md`.
- The `ROADMAP.md` "Candidate pool" section is the subject source for example runs: remove a subject when it ships, and restock with subjects identified but not built.
- PR bodies label what was proven by live run versus established by inspection only (see `AGENTS.md` § Merge policy and CI evidence for the merge and post-merge verification rules).
## Development Workflow
This is a content repository, no build step for skills/rules/snippets/templates — edit
`SKILL.md`, `.mdc`, `.py`, and `.toml` files directly. The website is generated:
`scripts/build_gallery.py` (stdlib) regenerates `docs/gallery/` and must be re-run after
touching `examples/`; the landing page builds from `scripts/site/` at deploy time.
- **One palette:** `scripts/site/tokens.css` is the single `:root` for every published
page — the landing and 404 templates `{% include %}` it and `build_gallery.py` inlines
it. Change colors or type there, never in a hand-copied `:root`; a token edit changes
every gallery page, so regenerate and commit `docs/gallery/`.
- **Link gate:** after building both (`python scripts/site/build_site.py --repo-root . --out docs`
then `python scripts/build_gallery.py`), run `python tests/check_site_links.py`. It resolves
every internal `href`/`src`/`#anchor` across all pages and requires `alt` on every `<img>`;
the `validate-site` job and the deploy both run it. Gallery hash fragments with `=`
(`#tag=a,b`, `#k=showcase`, `#s=az`) are filter state, not anchors, and are skipped.
- **"What's new" needs history:** the landing page dates recent additions from `git log`;
a shallow clone hides the strip, which is why `pages.yml` checks out with `fetch-depth: 0`.
The AI consumes content via:
- **Cursor**: rules under `rules/` apply when scope globs match. Skills are referenced by name in chat.
- **Claude Code**: install the plugin (`.claude-plugin/`, README Quick start) for the skills. Claude Code does not read `.mdc` rules; it gets them through `claude/blender-rules.md`, generated from `rules/` by `scripts/build_claude_rules.py`.
## Key Conventions
- **Blender versions**: 5.2 LTS primary, 5.1 prior stable, 4.5 LTS fallback. Skills must show both code paths when 4.x and 5.x APIs diverge, including the 5.1-vs-5.2 NodesModifier input split and the 5.2 COLOR strip `width`/`height` bake.
- **Properties as annotations**: `my_prop: bpy.props.FloatProperty(...)` (correct), not `my_prop = bpy.props.FloatProperty(...)` (deprecated).
- **bmesh memory**: every `bmesh.new()` must be paired with `bm.free()` in a `try`/`finally`.
- **No `bpy.ops` in tight loops**: use `bpy.data.*` and `bmesh` for bulk work.
- **Extensions over `bl_info`**: new add-ons ship as Extensions with `blender_manifest.toml`. `bl_info` may appear alongside as a fallback only.
- **Defensive context**: any code touching `context.active_object` must guard with `if obj is None: return`.
## Blender Documentation Quick Reference
| Area | URL |
| --- | --- |
| Python API (5.2 LTS) | https://docs.blender.org/api/current/ |
| Python API (5.1) | https://docs.blender.org/api/5.1/ |
| Python API (4.5 LTS) | https://docs.blender.org/api/4.5/ |
| Extensions Platform | https://docs.blender.org/manual/en/latest/advanced/extensions/index.html |
| Release notes | https://developer.blender.org/ |
## Release Hygiene
The release pipeline is automated via `release.yml` on every push to `main`; it releases only for `feat:`/`fix:`/breaking commit subjects and only after `release-gate.sh` sees green CI. The `release-doc-sync@v1` step rewrites CHANGELOG.md, this CLAUDE.md `**Version:**` line, and ROADMAP.md `**Current:**` line on each release. Never hand-edit those lines, the action owns them.
When adding content to a future version:
1. Add files under `skills/`, `rules/`, `snippets/`, `templates/`, or `examples/`.
2. Update README.md aggregate counts (the `validate-counts` job enforces correctness).
3. Update ROADMAP.md candidate pool entries.
4. Use `feat:` for new content, `fix:` for corrections.
5. Push. The release pipeline handles VERSION, tags, CHANGELOG, the `**Version:**` line here, and the `**Current:**` line in ROADMAP.md.