diff --git a/AGENTS.md b/AGENTS.md index bf6281c..851ceba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,14 +6,15 @@ Guidance for AI coding agents working on the Blender Developer Tools repository. ## Repository overview -Skills, rules, snippets, and a starter template for Blender Python development. +Skills, rules, snippets, starter templates, and runnable smoke-gated examples +for Blender Python development. The repo targets **Blender 5.1** (current stable) with a **Blender 4.5 LTS** fallback. There is no MCP server. It ships a `.cursor-plugin/plugin.json` manifest so the ecosystem drift checker classifies it as a `cursor-plugin`. This is content the AI loads when the user asks Blender questions or works on Blender add-ons in Cursor or Claude Code. -The content base as of v0.2.0: +The content base (counts are CI-enforced against README.md and the manifest): - 12 skills covering scaffolding, operators, panels, properties, mesh and bmesh, headless batch scripts, slotted-actions animation (5.x), programmatic @@ -26,6 +27,11 @@ The content base as of v0.2.0: - 2 templates: `extension-addon-template` for Extensions Platform add-ons, and `headless-batch-script-template` for unattended batch jobs. - 17 snippets covering canonical patterns. +- 12 examples under `examples//`: runnable scripts that assert a real + API contract with deterministic checks, exit non-zero on failure, and + optionally render a still via `--output`. Each is executed headless on + Blender 4.5 LTS and 5.1 by `blender-smoke.yml`; its render ships in the + site gallery. Anatomy and authoring rules: copy `examples/bmesh-gear/`. ## Repository structure @@ -35,7 +41,11 @@ Blender-Developer-Tools/ rules/.mdc # 6 rule files templates// # 2 starter templates snippets/.py # 17 standalone Python snippets - .github/workflows/ # validate, drift-check, release, label-sync + examples// # 12 runnable smoke-gated examples (+ gallery.json) + scripts/build_gallery.py # generates docs/gallery/ (stdlib only) + scripts/site/ # vendored landing-page build (build_site.py + template) + docs/gallery/ # committed generated gallery pages + hero assets + .github/workflows/ # validate, blender-smoke, drift-check, release, pages, label-sync .github/dependabot.yml AGENTS.md, CLAUDE.md, README.md, ROADMAP.md, CHANGELOG.md CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md @@ -116,6 +126,10 @@ way, and a one-paragraph rationale. 30 to 80 lines is the right size. every skill, rule, snippet, template, and example on disk must be listed, and the manifest `version` must equal `VERSION`. The release pipeline owns the manifest `version` line (see `release.yml` below) — never hand-edit it. +- `blender-smoke.yml` executes every shipped example (check-only, no render) + plus snippet/template smoke tests inside REAL headless Blender, on both + 4.5 LTS and 5.1, on every PR and a weekly schedule. A new example is not + shipped until it has a step here. - `drift-check.yml` consumes `Developer-Tools-Directory/.github/actions/ drift-check@v1.15` to enforce ecosystem standards-version markers. - `release.yml` auto-bumps the version, tags, force-updates floating tags diff --git a/CLAUDE.md b/CLAUDE.md index cefe345..61df583 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -The **Blender Developer Tools** repository is at **v0.9.1**. It packages skills, rules, snippets, and starter templates for Blender Python development with Cursor and Claude Code. Coverage targets **Blender 5.1** (current stable) with **Blender 4.5 LTS** fallback. There is no MCP server; content is consumed directly by the AI when working in Blender add-on or scripting projects. +The **Blender Developer Tools** repository is at **v0.9.1**. 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.1** (current stable) with **Blender 4.5 LTS** fallback. There is no MCP server; content is consumed directly by the AI when working in Blender add-on or scripting projects. **Version:** 0.9.1 **License:** CC-BY-NC-ND-4.0 @@ -19,6 +19,10 @@ skills//SKILL.md - AI workflow definitions, 12 total rules/.mdc - Anti-pattern rules, 6 total templates// - Starter projects, 2 total snippets/.py - Standalone code patterns, 17 total +examples// - Runnable smoke-gated examples, 12 total (+ gallery.json) +scripts/build_gallery.py - Regenerates docs/gallery/ from gallery.json (stdlib only) +scripts/site/ - Vendored landing-page build (Jinja2) +docs/gallery/ - Committed generated gallery pages + hero renders VERSION - Source of truth for the repo version ``` @@ -76,9 +80,24 @@ v0.1.0: canonical object creation and deletion, depsgraph evaluated mesh, bmesh 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'`. +## Examples (12) + +Runnable scripts at `examples//`, each asserting a real API contract with +deterministic checks (exit non-zero on failure) and optionally rendering a still via +`--output`. All twelve run headless on Blender 4.5 LTS and 5.1 in `blender-smoke.yml`; +their renders ship in the site gallery at `docs/gallery/`. `examples/gallery.json` is the +gallery's source of truth. When authoring a new one, copy the anatomy of +`examples/bmesh-gear/` (script structure, README shape, dark-studio render recipe) and +wire all of: gallery.json entry, `.cursor-plugin/plugin.json` examples array (CI-gated), +a `blender-smoke.yml` step, a README gallery row, hero webp (1280×720) in +`docs/gallery/assets/` + preview webp (1200×675), then run `python scripts/build_gallery.py`. + ## Development Workflow -This is a content repository, no build step. Edit `SKILL.md`, `.mdc`, `.py`, and `.toml` files directly. +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. The AI consumes content via: @@ -109,7 +128,7 @@ The release pipeline is automated via `release.yml` on push to `main` for conten When adding content to a future version: -1. Add files under `skills/`, `rules/`, `snippets/`, or `templates/`. +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. diff --git a/README.md b/README.md index fa352c5..69d6b27 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ---

- Skills, rules, snippets, and a starter template for Blender Python development + Skills, rules, snippets, templates, and runnable examples for Blender Python development

@@ -17,14 +17,14 @@

- 12 skills  •  6 rules  •  2 templates  •  17 snippets + 12 skills  •  6 rules  •  2 templates  •  17 snippets  •  12 examples

--- ## Overview -This repository ships **12 skills, 6 rules, 2 templates, and 17 snippets** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. +This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 12 runnable examples** for Blender Python development targeting Blender 5.1 (current stable) with Blender 4.5 LTS fallback support. The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly. diff --git a/ROADMAP.md b/ROADMAP.md index 4874057..7d5bfd8 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -89,7 +89,7 @@ Audit pass on v0.1.0 content: standards-version markers bumped from `1.9.1` to ` Not committed; target list for the next content version. (v0.3.0 shipped the smoke-gated `examples/` track.) -- **Fleet Pages facelift + examples support** (infra) -- ship as ONE coordinated change to the meta-repo `site-template/template.html.j2` + `build_site.py`, since both need a template edit and two fleet-wide pushes is worse than one. (a) examples discovery: load `examples/gallery.json` and render an Examples grid + a nav link to it (the nav link closes the landing->gallery cross-link gap, impossible today without a template edit); (b) landing facelift adopting the direction proven by this repo's gallery (shared light/dark tokens, fluid hero type scale, the card system, `:focus-visible`, reduced-motion). This repo's local gallery (`examples/gallery.json` + `docs/gallery/`, see `docs/gallery/DESIGN_NOTES.md`) is the **prototype and convergence target**: once the shared template reads the same `gallery.json` schema, the local generator (`scripts/build_gallery.py`) and page are retired -- a lift-and-shift, not a rewrite. The Option-2 cycle must read the full template-consumer set, account for floating-main consumption (every repo updates on next deploy), and prove backward compatibility (a repo with no `gallery.json` renders unchanged) before any meta-repo merge. No confirmed rendering bug today (the suspected Skills/Rules overlap was verified to be a normal collapsed accordion), so the cross-link discoverability gap is the main driver. +- ~~Fleet Pages facelift + examples support~~ **RESOLVED differently (2026-07-03)**: the meta-repo migration was dropped — the fleet template only scaffolds new tools, and each tool's site evolves independently after that. This repo vendored the site build into `scripts/site/`, redesigned landing + gallery as the Blender-viewport system (see `docs/gallery/DESIGN_NOTES.md`), added the examples grid, nav link, and full hero stats locally. `scripts/build_gallery.py` and `examples/gallery.json` are now permanent, not a prototype awaiting lift-and-shift. - `modal-operators` skill -- `invoke` returning `RUNNING_MODAL`, the `modal()` event handler, modal cancellation patterns - `usd-pipelines` skill -- USD export options, `evaluation_mode`, instancing, the USD vs glTF tradeoffs - `mathutils-patterns` skill -- `mathutils.Vector`, `Matrix`, `Quaternion`, common transforms, the `@` operator diff --git a/docs/gallery/assets/gn-instance-grid-hero.webp b/docs/gallery/assets/gn-instance-grid-hero.webp index 4a19136..8a6ccac 100644 Binary files a/docs/gallery/assets/gn-instance-grid-hero.webp and b/docs/gallery/assets/gn-instance-grid-hero.webp differ diff --git a/docs/gallery/assets/temp-override-join-hero.webp b/docs/gallery/assets/temp-override-join-hero.webp index 6428d88..3207149 100644 Binary files a/docs/gallery/assets/temp-override-join-hero.webp and b/docs/gallery/assets/temp-override-join-hero.webp differ diff --git a/docs/gallery/curve-bevel-arc/index.html b/docs/gallery/curve-bevel-arc/index.html index dc67c9c..c1c80d5 100644 --- a/docs/gallery/curve-bevel-arc/index.html +++ b/docs/gallery/curve-bevel-arc/index.html @@ -205,9 +205,11 @@

Source

Witnesses that renderable tubes are authored on `bpy.types.Curve` directly (`splines.new('BEZIER')`, `bezier_points`, `bevel_depth`, `use_fill_caps`) — not by meshing first or calling curve operators. The check asserts the -closed-form point count, bevel depth, filled-cap topology, and that the -depsgraph-evaluated mesh has a Z span of a tube whose centerline sits at -`z = bevel_depth` (resting on the floor). +closed-form point count and bevel depth, the closed-form Z span (tube +centerline at `z = bevel_depth`, resting on the floor) and X span, plus the +evaluated vert/face counts as a MEASURED regression gate — curve tessellation +has no simple closed form, so those two constants pin today's behavior (see +EXPECT_VERTS below for how to re-measure if a future Blender retessellates). By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: @@ -222,7 +224,10 @@

Source

BEVEL = 0.15 BEVEL_RES = 4 RES_U = 12 -# measured for the parameters above with use_fill_caps=True — identical on 4.4 and 5.1 +# MEASURED regression constants, not closed-form: curve-to-mesh tessellation +# (rings x bevel segments + cap fans) has no simple formula. Verified identical +# on 4.4, 4.5 LTS, and 5.1. If a future Blender changes tessellation, re-measure +# by printing len(em.vertices)/len(em.polygons) in check() and update these. EXPECT_VERTS = 1044 EXPECT_FACES = 1028 diff --git a/docs/gallery/gn-instance-grid/index.html b/docs/gallery/gn-instance-grid/index.html index 78e4796..ca9da74 100644 --- a/docs/gallery/gn-instance-grid/index.html +++ b/docs/gallery/gn-instance-grid/index.html @@ -405,9 +405,9 @@

Source

light("Rim", (1.5, 4.5, 2.0), 480.0, 4.0, (1.0, 0.75, 0.45)) cam_data = bpy.data.cameras.new("Cam") - cam_data.lens = 55.0 + cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (3.6, -4.2, 2.8) + cam.location = (4.6, -5.4, 3.4) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO') diff --git a/docs/gallery/temp-override-join/index.html b/docs/gallery/temp-override-join/index.html index be85552..f7a0d69 100644 --- a/docs/gallery/temp-override-join/index.html +++ b/docs/gallery/temp-override-join/index.html @@ -250,15 +250,11 @@

Source

def join_with_temp_override(target, sources): - """The contract this example witnesses: temp_override, not context.copy().""" - view_layer = bpy.context.view_layer - for obj in bpy.data.objects: - obj.select_set(False) - target.select_set(True) - for src in sources: - src.select_set(True) - view_layer.objects.active = target + """The contract this example witnesses: temp_override, not context.copy(). + The override alone fabricates the whole operator context — no select_set, + no view_layer.objects.active. That is the point: the scene's real + selection state stays untouched.""" with bpy.context.temp_override( active_object=target, selected_objects=[target, *sources], @@ -374,7 +370,7 @@

Source

cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (4.2, -5.2, 4.0) + cam.location = (5.0, -6.4, 4.6) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO') diff --git a/examples/curve-bevel-arc/curve_bevel_arc.py b/examples/curve-bevel-arc/curve_bevel_arc.py index 11d1f4e..132ad61 100644 --- a/examples/curve-bevel-arc/curve_bevel_arc.py +++ b/examples/curve-bevel-arc/curve_bevel_arc.py @@ -3,9 +3,11 @@ Witnesses that renderable tubes are authored on `bpy.types.Curve` directly (`splines.new('BEZIER')`, `bezier_points`, `bevel_depth`, `use_fill_caps`) — not by meshing first or calling curve operators. The check asserts the -closed-form point count, bevel depth, filled-cap topology, and that the -depsgraph-evaluated mesh has a Z span of a tube whose centerline sits at -`z = bevel_depth` (resting on the floor). +closed-form point count and bevel depth, the closed-form Z span (tube +centerline at `z = bevel_depth`, resting on the floor) and X span, plus the +evaluated vert/face counts as a MEASURED regression gate — curve tessellation +has no simple closed form, so those two constants pin today's behavior (see +EXPECT_VERTS below for how to re-measure if a future Blender retessellates). By default it runs only the correctness check (no render) — the CI smoke check. Pass --output to also render a still: @@ -20,7 +22,10 @@ BEVEL = 0.15 BEVEL_RES = 4 RES_U = 12 -# measured for the parameters above with use_fill_caps=True — identical on 4.4 and 5.1 +# MEASURED regression constants, not closed-form: curve-to-mesh tessellation +# (rings x bevel segments + cap fans) has no simple formula. Verified identical +# on 4.4, 4.5 LTS, and 5.1. If a future Blender changes tessellation, re-measure +# by printing len(em.vertices)/len(em.polygons) in check() and update these. EXPECT_VERTS = 1044 EXPECT_FACES = 1028 diff --git a/examples/gn-instance-grid/gn_instance_grid.py b/examples/gn-instance-grid/gn_instance_grid.py index 607a16c..462a855 100644 --- a/examples/gn-instance-grid/gn_instance_grid.py +++ b/examples/gn-instance-grid/gn_instance_grid.py @@ -203,9 +203,9 @@ def light(name, loc, energy, size, col): light("Rim", (1.5, 4.5, 2.0), 480.0, 4.0, (1.0, 0.75, 0.45)) cam_data = bpy.data.cameras.new("Cam") - cam_data.lens = 55.0 + cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (3.6, -4.2, 2.8) + cam.location = (4.6, -5.4, 3.4) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO') diff --git a/examples/gn-instance-grid/preview.webp b/examples/gn-instance-grid/preview.webp index f9ce1b9..94a9c35 100644 Binary files a/examples/gn-instance-grid/preview.webp and b/examples/gn-instance-grid/preview.webp differ diff --git a/examples/temp-override-join/preview.webp b/examples/temp-override-join/preview.webp index e93b2e1..0ba9b3a 100644 Binary files a/examples/temp-override-join/preview.webp and b/examples/temp-override-join/preview.webp differ diff --git a/examples/temp-override-join/temp_override_join.py b/examples/temp-override-join/temp_override_join.py index 03d033e..eaff520 100644 --- a/examples/temp-override-join/temp_override_join.py +++ b/examples/temp-override-join/temp_override_join.py @@ -48,15 +48,11 @@ def build_cubes(): def join_with_temp_override(target, sources): - """The contract this example witnesses: temp_override, not context.copy().""" - view_layer = bpy.context.view_layer - for obj in bpy.data.objects: - obj.select_set(False) - target.select_set(True) - for src in sources: - src.select_set(True) - view_layer.objects.active = target + """The contract this example witnesses: temp_override, not context.copy(). + The override alone fabricates the whole operator context — no select_set, + no view_layer.objects.active. That is the point: the scene's real + selection state stays untouched.""" with bpy.context.temp_override( active_object=target, selected_objects=[target, *sources], @@ -172,7 +168,7 @@ def light(name, loc, energy, size, col): cam_data = bpy.data.cameras.new("Cam") cam_data.lens = 50.0 cam = bpy.data.objects.new("Cam", cam_data) - cam.location = (4.2, -5.2, 4.0) + cam.location = (5.0, -6.4, 4.6) scene.collection.objects.link(cam) scene.camera = cam track = cam.constraints.new('TRACK_TO')