Skip to content

Latest commit

 

History

History
275 lines (220 loc) · 27.5 KB

CLAUDE.md

File metadata and controls

275 lines (220 loc) · 27.5 KB

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.158.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. The repo ships no MCP server of its own: an opt-in second plugin, blender-live-session (live-session/), registers the official Blender Lab server (see Live MCP vs Headless Harness). Content is consumed directly by the AI when working in Blender add-on or scripting projects.

Version: 0.158.0 License: CC-BY-NC-ND-4.0 Author: TM Hospitality Strategies

Repository Architecture

skills/<skill-name>/SKILL.md   - AI workflow definitions, 25 total
rules/<rule-name>.mdc          - Anti-pattern rules, 12 total
templates/<template-name>/     - Starter projects, 3 total
snippets/<snippet-name>.py     - Standalone code patterns, 31 total
examples/<name>/               - Runnable smoke-gated examples, 69 total (+ gallery.json)
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 (25)

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
porting-to-blender-5 Index of every 4.x → 5.x break the pack covers (bgl, action.fcurves, use_auto_smooth, Principled sockets, .sequences, ID properties, compositor, NodesModifier inputs, ...), each linked to the skill, rule or example that owns the fix; headless triage recipe
gpu-drawing-and-overlays gpu instead of the removed bgl: draw_handler_add/remove, builtin shader names, gpu.state blending and depth, POLYLINE_* line width, blf text, GPUOffScreen with 5.2's gpu.init() headless
grease-pencil-v3 Grease Pencil v3: grease_pencils_v3 (4.5) vs grease_pencils (5.x), layers/frames/drawings, point attributes, Line Art modifier
constraints-and-parenting .parent vs matrix_parent_inverse, stale matrix_world, constraints via constraints.new checked on the evaluated pose, SKT_* socket empties
images-and-pixels Image.pixels layout and bulk foreach_*, byte vs float buffers, colorspace, save() vs save_render(), PNG vs EXR alpha
curves-and-text Curve and TextCurve datablocks: splines, bevel/extrude closed forms, body, Bfont, evaluated mesh read-back
compositor-nodes 4.5 scene.node_tree vs 5.x scene.compositing_node_group, Glare node enum-to-menu-socket change, use_compositing

Rules (12)

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
no-bgl *.py import bgl or any bgl. call (removed in 5.x; the add-on fails to register)
no-removed-normals-api *.py use_auto_smooth, auto_smooth_angle, calc_normals_split() and the other normals calls removed in 4.1
no-legacy-vse-sequences *.py sequence_editor.sequences / .sequences_all (removed in 5.x; .strips / .strips_all exist on 4.5 too)

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 stacks baked with Mesh.new_from_object on the evaluated object (no operator, so stack order holds); bpy.context.temp_override only where an operator is genuinely needed
  • 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 (31)

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). Porting to 5.x: gpu-draw-handler.py (viewport overlay with a stored, removable draw handler) and set-sharp-from-angle.py (the 4.1+ replacement for use_auto_smooth).

Examples (69)

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 69 ship a render in the site gallery at docs/gallery/. The other thirteen 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), when registered on the local machine, provides a live interactive session for exploration. The opt-in blender-live-session plugin ships it (#483): live-session/.claude-plugin/plugin.json and live-session/.cursor-plugin/plugin.json set "mcpServers": "./mcp.json", which runs uvx --exclude-newer <date> --from git+https://projects.blender.org/lab/blender_mcp.git@<sha>#subdirectory=mcp blender-mcp; the core plugin must not declare a server. tests/check_manifest_meta.py requires a full 40-hex commit pin and --exclude-newer, build_plugin_dist.py ships live-session/ and fails if a manifest's path is missing, and mcp-server.yml runs tests/check_mcp_server.py (install + initialize + tools/list over stdio) weekly and on every change to live-session/. To bump it, review the upstream diff, replace the SHA, set --exclude-newer to the day after that commit, and run python tests/check_mcp_server.py. The plugin's live-blender-session skill carries this policy to plugin users. The server is GPL-3.0: reference it, never vendor it. The add-on needs Blender 5.1+.

  • 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. Re-rendering a hero means regenerating its sheet: tests/check_contact_sheets.py (Validate) fails when a sheet's candidate tile no longer matches the hero that ships (#487), and an asset-type piece's asset sheet must be re-rendered in the same commit. 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 --shard K/4, four jobs; dispatch with sweep: true to run it by hand), and a red or cancelled weekly run opens or comments on a "Weekly Blender smoke run failed" issue.
  • Content smoke (#447): content-smoke.yml runs every snippets/*.py in place (its check and falsifier live in tests/smoke/snippet_checks.py) and every SKILL.md ```python block tagged <!-- smoke: run -->, via `tests/smoke/run_content.py`, on 5.2 and 4.5. It has a positive `paths` filter because blender-smoke ignores `**.md`; `release-gate.sh` waits for it on pushes touching skills or snippets. Validate's `tests/check_content_catalog.py` compiles every SKILL.md block and requires a catalog entry per snippet.
  • 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). The GitHub repo description deliberately carries no counts: nothing can check it, and it went stale twice (#448, #486). Keep numbers out of it.
  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.