Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"skills/procedural-materials-and-shaders/SKILL.md",
"skills/depsgraph-and-evaluated-data/SKILL.md",
"skills/drivers-and-app-handlers/SKILL.md",
"skills/bl-info-migration/SKILL.md"
"skills/bl-info-migration/SKILL.md",
"skills/vse-python/SKILL.md"
],
"rules": [
"rules/prefer-data-over-ops-in-loops.mdc",
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ 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 is 12 skills, 6 rules, 2 templates, 17 snippets, and 45
The content base is 13 skills, 6 rules, 2 templates, 17 snippets, and 45
examples (counts are CI-enforced against README.md and the manifest). The full
inventory tables and per-item purposes live in `CLAUDE.md`. Example anatomy
and authoring rules: copy `examples/bmesh-gear/`; the render look is specified
Expand All @@ -31,7 +31,7 @@ in `docs/VISUAL-STYLE.md`; the canonical run prompt is

```
Blender-Developer-Tools/
skills/<skill-name>/SKILL.md # 12 skill files
skills/<skill-name>/SKILL.md # 13 skill files
rules/<rule-name>.mdc # 6 rule files
templates/<template-name>/ # 2 starter templates
snippets/<snippet-name>.py # 17 standalone Python snippets
Expand Down
7 changes: 4 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ The **Blender Developer Tools** repository is at **v0.47.0**. It packages skills
## Repository Architecture

```
skills/<skill-name>/SKILL.md - AI workflow definitions, 12 total
skills/<skill-name>/SKILL.md - AI workflow definitions, 13 total
rules/<rule-name>.mdc - Anti-pattern rules, 6 total
templates/<template-name>/ - Starter projects, 2 total
snippets/<snippet-name>.py - Standalone code patterns, 17 total
Expand All @@ -28,7 +28,7 @@ docs/gallery/ - Committed generated gallery pages + hero render
VERSION - Source of truth for the repo version
```

## Skills (12)
## Skills (13)

| Skill | Purpose |
| --- | --- |
Expand All @@ -44,6 +44,7 @@ VERSION - Source of truth for the repo version
| 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 |

## Rules (6)

Expand Down Expand Up @@ -149,7 +150,7 @@ The AI consumes content via:

## 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, and the 5.1-vs-5.2 NodesModifier input split.
- **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.
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</p>

<p align="center">
<strong>12 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>45 examples</strong>
<strong>13 skills</strong> &nbsp;&bull;&nbsp; <strong>6 rules</strong> &nbsp;&bull;&nbsp; <strong>2 templates</strong> &nbsp;&bull;&nbsp; <strong>17 snippets</strong> &nbsp;&bull;&nbsp; <strong>45 examples</strong>
</p>

<p align="center">
Expand All @@ -36,13 +36,13 @@

## Overview

This repository ships **12 skills, 6 rules, 2 templates, 17 snippets, and 45 runnable examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.
This repository ships **13 skills, 6 rules, 2 templates, 17 snippets, and 45 runnable examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable.

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.

| Layer | Role |
| --- | --- |
| **Skills** | Guided workflows: scaffolding, operators, panels, properties, mesh and bmesh, headless batch, slotted actions, geometry nodes, procedural materials, depsgraph queries, drivers and handlers, `bl_info` migration |
| **Skills** | Guided workflows: scaffolding, operators, panels, properties, mesh and bmesh, headless batch, slotted actions, geometry nodes, procedural materials, depsgraph queries, drivers and handlers, `bl_info` migration, video sequencer |
| **Rules** | Guardrails for the most common AI mistakes: ops-in-loops, bmesh leaks, legacy `bl_info` only, prop assignments, deprecated context-copy override, per-element loops over bulk mesh data |
| **Templates** | A working Extensions Platform add-on starter and a headless batch script starter |
| **Snippets** | 17 small standalone Python files demonstrating canonical patterns |
Expand Down Expand Up @@ -887,7 +887,7 @@ Companion to [`color-attribute-wheel`](examples/color-attribute-wheel/) and
## How content is organized

```
skills/<name>/SKILL.md - 12 skill files, YAML frontmatter, one canonical pattern each
skills/<name>/SKILL.md - 13 skill files, YAML frontmatter, one canonical pattern each
rules/<name>.mdc - 6 rule files, anti-pattern + correction
templates/<name>/ - 2 template directories (extension-addon-template, headless-batch-script-template)
snippets/<name>.py - 17 standalone Python snippets, 5 to 50 lines each
Expand Down
3 changes: 2 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ derives the actual version from conventional-commit types.
| Examples and demos (smoke-gated) | 12 | 6 | 2 | 17 | Shipped (v0.3.0) |
| More examples (turntable, SDF remesh) | 12 | 6 | 2 | 17 | Shipped (v0.4.0) |
| 5.2 LTS targeting, GN modifier inputs | 12 | 6 | 2 | 17 | Shipped |
| VSE COLOR strip intrinsic size (undocumented 5.2) | 13 | 6 | 2 | 17 | Shipped |
| Modal operators, USD, mathutils | — | — | — | — | Upcoming |
| Stable | — | — | — | — | Upcoming |

Expand Down Expand Up @@ -100,7 +101,7 @@ Not committed; target list for the next content version. (v0.3.0 shipped the smo
- ~~Image save-format witness~~ **SHIPPED** as `examples/png-exr-alpha/` — float→PNG is RGBA16 and false-unpremultiplies as if associated-alpha (closed-form err 0.98 at RGB 0.02 / a=1/255); OpenEXR preserves float RGBA; byte→PNG is straight RGBA8; `EXR color_mode='RGB'` drops alpha
- ~~Attribute domain witness~~ **SHIPPED** as `examples/attribute-domain-shear/` — POINT vs CORNER semantics on an 8-wedge pinwheel with one shared hub: CORNER authoring exact (hub corners disagree by face, err ≤ 1e-6), naive per-wedge POINT loop shears to the last write at hub + ring verts, measured deviation == palette closed form (0.751031); byte-identical on 4.5.11 and 5.1.2; crisp-vs-smeared dual pinwheel still — companion to `color-attribute-wheel`
- ~~Light-linking collection witness~~ **SHIPPED** as `examples/light-link-studio/` — the API is `obj.light_linking` on the light OBJECT (`ld.light_linking` is an AttributeError on both versions); linked ratio 4.0x at projected-center luminance samples, unlink raises the decoy 244–251% with 0.0% hero drift in the same two-render check; EEVEE Next honors linking too (5.5x on 4.5.11, 5.9x on 5.1.2), Cycles pinned for deterministic samples
- ~~VSE sequences-to-strips witness~~ **SHIPPED** as `examples/vse-cut-list/` — `.sequences` removed on 5.x (4.5 bridges to `.strips`), `new_effect` end kwarg `frame_end=` (4.5) vs `length=` (5.x), `frame_final_*` deprecated in favor of `left_handle`/`right_handle`/`duration`, TRANSFORM effect type removed, GAMMA_CROSS clamps to the source overlap, effect strips consume inputs only when stacked above them, same-scene scene strips render transparent; save/reload round-trip + tiny-render pixel witness
- ~~VSE sequences-to-strips witness~~ **SHIPPED** as `examples/vse-cut-list/` — `.sequences` removed on 5.x (4.5 bridges to `.strips`), `new_effect` end kwarg `frame_end=` (4.5) vs `length=` (5.x), `frame_final_*` deprecated in favor of `left_handle`/`right_handle`/`duration`, TRANSFORM effect type removed, GAMMA_CROSS clamps to the source overlap, effect strips consume inputs only when stacked above them, same-scene scene strips render transparent; save/reload round-trip + tiny-render pixel witness. **5.2 COLOR `width`/`height` bake** (not in the python_api notes) taught in `skills/vse-python/`; the example `--check-pixels` path is the witness (set scene resolution before `new_effect`)
- ~~glTF export round-trip witness~~ **SHIPPED** as `examples/gltf-export-roundtrip/` — `export_yup` bakes `(x,y,z)→(x,z,−y)` into vertex data with no node rotation (probed identical on 4.5.11 and 5.1.2), `export_apply` ships the evaluated mesh (one disk vertex per evaluated loop), TEXCOORD_0 is V-flipped on disk, per-triangle material bindings survive; exporter/importer RNA signatures byte-identical between 4.5.11 and 5.1.2 (guarded against future renames); `Mesh.calc_normals()` removal on 5.x surfaced during authoring
- ~~LOD decimate chain witness~~ **SHIPPED** as `examples/lod-decimate-chain/` — Decimate COLLAPSE evaluated through the depsgraph is non-destructive (obj.data keeps closed-form counts), evaluated tris hit `ratio × base` within 5% (measured 0.0–0.44%), silhouette bbox survives within 1e-3 (measured 7.7e-6); a stacked Decimate halves the effective ratio, an aggressive 0.02 ratio collapses the nose tip — both caught failure modes
- ~~Vertex weight limit witness~~ **SHIPPED** as `examples/vertex-weight-limit/` — the 4-influence engine cap enforced via the data API (`v.groups` + `VertexGroup.remove` + renormalize); unit sums (measured 3e-8), pose preserved (4.9e-3), exact LBS from the mesh's own deform layer (2.7e-7), Root mount pinned
Expand Down
41 changes: 41 additions & 0 deletions docs/technical-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,47 @@

_Audit date: 2026-06-20 · Repo @ v0.5.0 · Auditor: Principal Eng review_

## Findings the release notes did not list

_Standing log. Notes-derived 5.1→5.2 (and later) deltas are incomplete by default._

The Blender `python_api` release notes are a starting set, not a closed
inventory. A sweep that only copies listed removals will miss behavior
changes that keep the same RNA names. Record those here when CI or
authoring finds them, even when upstream notes omit the break. Do not
treat a clean notes-diff as proof that two series are interchangeable.

### 2026-09-09 — COLOR strip intrinsic size (5.2, not in the notes)

**4.5 LTS / 5.1:** COLOR strips have no intrinsic size. `transform.scale_*`
is a fraction of the output frame. Creating at factory resolution then
rendering a tiny buffer is fine.

**5.2+:** `new_effect(..., type='COLOR')` bakes readonly `width` / `height`
from `scene.render.resolution_*` at creation. Scale and offset are in that
media space. Factory 1920×1080 + later 96×54 render → 0.36-scaled cells
larger than the output; the long-runner covers the frame.

A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is
media size vs a later output size, not "transform is ignored."

Set scene resolution **before** building strips.

- Taught: `skills/vse-python/SKILL.md`
- Witness: `examples/vse-cut-list/` `--check-pixels` (on 5.2 asserts
`A.width, A.height == (96, 54)`). No separate example — that script
already fails if the bake drifts.

### Contrast — NodesModifier dict assignment (5.2, was in the notes)

`mod[identifier] = value` raises `TypeError` on 5.2 (`id properties not
supported for this type`). Replacement: `getattr(mod.properties.inputs,
ident).value`. This one **was** listed in the 5.2 python_api notes.
Taught in `skills/geometry-nodes-python/SKILL.md`, witnessed by
`examples/gn-modifier-inputs/`. Keep both classes of finding in this
log: notes-listed removals that still need a taught split, and
behavior changes the notes never mentioned.

## 0. Important reframing

The audit brief assumed a runnable add-on (operators, UI panels, a socket/file-watcher
Expand Down
157 changes: 157 additions & 0 deletions skills/vse-python/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
---
name: vse-python
description: Build Video Sequence Editor timelines from Python. SequenceEditor.strips vs .sequences, new_effect length vs frame_end, and the 5.2 COLOR strip width/height bake from scene resolution.
standards-version: 1.10.0
---

# Video Sequence Editor in Python

## Trigger

Use this skill when the user:

- Builds or inspects a VSE / sequencer timeline from a script
- Mentions `sequence_editor`, `strips`, `sequences`, `new_effect`, COLOR strips, or `StripTransform`
- Hits `AttributeError: 'SequenceEditor' object has no attribute 'sequences'` on 5.x
- Hits `TypeError` on `new_effect` kwargs (`frame_end` vs `length`)
- Renders a sequencer composite whose COLOR cells ignore `transform.scale_*` after a resolution change

## High-level shape

```python
import bpy

scene = bpy.context.scene
se = scene.sequence_editor or scene.sequence_editor_create()
coll = se.strips if hasattr(se, "strips") else se.sequences

if bpy.app.version >= (5, 0, 0):
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=1, length=10,
)
else:
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=1, frame_end=11,
)
strip.color = (0.85, 0.10, 0.22)
```

Branch on `bpy.app.version`, never on `bpy.app.version_string`. An empty `bpy_prop_collection` is falsy — `se.strips or se.sequences` silently falls through to the legacy accessor on an empty timeline. Always branch on `hasattr`.

## Accessor: `.strips` vs `.sequences`

| | 4.5 LTS | 5.1 / 5.2+ |
| --- | --- | --- |
| Canonical collection | `.strips` (`.sequences` is a bridge to the same strips) | `.strips` only |
| `.sequences` | Present, same contents as `.strips` | `AttributeError` |

```python
def strips_coll(se):
return se.strips if hasattr(se, "strips") else se.sequences
```

Do not write `se.sequences` in new code. On 5.x it is gone; on 4.5 `.strips` already exists.

## `new_effect` end kwarg

`strips.new_effect(...)` ends a strip with **one** accepted end argument. The other raises `TypeError`.

```python
span = (1, 33) # end-exclusive [start, end)

if bpy.app.version >= (5, 0, 0):
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=span[0], length=span[1] - span[0],
)
else:
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=span[0], frame_end=span[1],
)
```

Timeline bounds: `frame_final_start` / `frame_final_end` / `frame_final_duration` on 4.5. On 5.x those names are deprecated aliases (removal announced for 6.0); canonical accessors are `left_handle` / `right_handle` / `duration`. Scene strips take four args — `new_scene(name, scene, channel, frame_start)` — with no `length` / `frame_end` kwarg on either version.

The `TRANSFORM` effect type is removed on 5.x. Place strips with per-strip `strip.transform` (`StripTransform`) on both versions. Effect inputs are `input_1` / `input_2` (creation kwargs `input1` / `input2`).

## COLOR strip intrinsic size (5.2)

4.5 LTS and 5.1 COLOR strips have **no** intrinsic size. `transform.scale_*` is a fraction of the **output frame**. Creating at factory resolution then rendering a tiny buffer is fine.

5.2 and later bake readonly `width` / `height` from **scene render resolution at `new_effect` time**. Scale and offset are then in that **media** space, not a later output size. Set scene resolution **before** building strips.

This split is **not** in the 5.2 `python_api` release notes. Witnessed by `examples/vse-cut-list/` `--check-pixels`.

```python
width, height = 96, 54
scene.render.resolution_x = width
scene.render.resolution_y = height

se = scene.sequence_editor or scene.sequence_editor_create()
coll = se.strips if hasattr(se, "strips") else se.sequences

if bpy.app.version >= (5, 0, 0):
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=1, length=10,
)
else:
strip = coll.new_effect(
name="A", type="COLOR", channel=1,
frame_start=1, frame_end=11,
)

if bpy.app.version >= (5, 2, 0):
# 5.2+: readonly width/height baked at new_effect from scene.render
assert (strip.width, strip.height) == (width, height)
# strip.width = 64 # TypeError — readonly
else:
# 4.5 / 5.1: no intrinsic size; hasattr(strip, "width") is False
pass

strip.transform.scale_x = 0.36
strip.transform.scale_y = 0.36
```

Creating at factory 1920×1080 then dropping the scene to 96×54 for a check render makes a 0.36-scaled COLOR cell larger than the output. On 5.2 the long-runner covers the frame; pixel samples read the wrong strip.

A lone COLOR strip **does** honor `transform.scale_*` on 5.2. The break is media size vs a later output size, not "transform is ignored."

## Common AI mistakes

1. **`sequence_editor.sequences` on 5.x.** `AttributeError`. Use `.strips` (with `hasattr` only as a 4.5 bridge).

2. **Wrong `new_effect` end kwarg.** `frame_end=` on 5.x and `length=` on 4.5 both raise `TypeError`.

3. **Creating COLOR strips, then changing render size** (the 5.2 trap):

```python
a = coll.new_effect(...) # bakes 1920×1080 on 5.2
scene.render.resolution_x = 96 # output changed; media size did not
```

Set `scene.render.resolution_*` **before** `new_effect`.

4. **`se.strips or se.sequences`.** An empty collection is falsy, so this falls through to `.sequences` on a fresh editor.

5. **`TRANSFORM` effect type on 5.x.** Gone. Use `strip.transform`.

6. **Scene strip pointing at its own scene.** Feedback loop; renders transparent. Source a **separate** scene.

7. **Effect strips consume their inputs** only when stacked on a channel **above** those inputs. Below, the inputs keep compositing independently. A `GAMMA_CROSS` asked to outlast its inputs' overlap is silently clamped to the overlap.

## See also

- Example `examples/vse-cut-list/` — accessor rename, `new_effect` kwargs, save/reload, and the 5.2 COLOR size bake (`--check-pixels` asserts `A.width, A.height == render size` on 5.2+).
- Example `examples/vse-gamma-cross/` — `GAMMA_CROSS` blend curve.
- Audit log: `docs/technical-audit.md` § Findings the release notes did not list.

## References

- `bpy.types.SequenceEditor`: https://docs.blender.org/api/current/bpy.types.SequenceEditor.html
- `bpy.types.Sequence`: https://docs.blender.org/api/current/bpy.types.Sequence.html
- 4.5 LTS `SequenceEditor`: https://docs.blender.org/api/4.5/bpy.types.SequenceEditor.html
- 5.1 `SequenceEditor`: https://docs.blender.org/api/5.1/bpy.types.SequenceEditor.html
Loading