From cd3e2326413487eb12950672d4648e0a38fe196d Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 18 Jun 2026 14:51:09 -0400 Subject: [PATCH] fix: correct version-accuracy defects across skills and snippets Re-verified empirically on Blender 4.5.10 LTS and 5.1.1 plus the 4.4/4.5/5.0 release notes. Four confirmed defects corrected: 1. EEVEE engine id was inverted. Correct mapping: 4.2-4.5 use 'BLENDER_EEVEE_NEXT'; 5.0+ reclaimed 'BLENDER_EEVEE' (5.0 release notes, confirmed by introspection: 5.1.1 rejects _NEXT, 4.5.10 rejects plain). Fixed version-branch-skeleton.py get_eevee_engine_id(), the headless-batch-scripting detect snippet/choices/prose, and the procedural-materials version table. 2. Slotted Actions framing and fabricated 4.5 shim. The slotted data model shipped in 4.4 (not 5.0); legacy action.fcurves was removed in 5.0; action_ensure_channelbag_for_slot is new in 5.0 and absent on 4.4/4.5 (no auto-detecting shim). Rewrote slotted-actions-animation: real version-branching helper (ensure-fn on 5.0+, strip.channelbag(ensure=True) on 4.4/4.5), removed the hasattr(action,'slots') "5.x sniff", corrected the compatibility matrix and the snippet header comment. 3. save_pre/save handler signature. Arg0 is the file path string, not a Scene (4.5 docs + introspection on both versions). Fixed the handler table and worked examples in drivers-and-app-handlers and app-handler-registration.py so handlers reach scenes via bpy.data, never treating arg0 as a Scene. 4. Geometry Nodes node version tags. For Each Element (4.3) and Mesh to SDF Grid (4.3) were mis-tagged "5.0+"; fixed labels and changed has_for_each_element() to gate on >= (4,3,0). NodeSocketBundle/has_bundles left at 5.0 (confirmed: interface socket works in 5.1, TypeError in 4.5). No content added or removed; counts unchanged (12/6/2/17). All snippets and templates py_compile. Corrected snippets verified to run on 4.5.10 and 5.1.1. Co-Authored-By: Claude Opus 4.8 Signed-off-by: fOuttaMyPaint --- skills/drivers-and-app-handlers/SKILL.md | 41 ++++++--- skills/geometry-nodes-python/SKILL.md | 9 +- skills/headless-batch-scripting/SKILL.md | 14 +-- .../procedural-materials-and-shaders/SKILL.md | 2 +- skills/slotted-actions-animation/SKILL.md | 90 +++++++++++-------- snippets/action-ensure-channelbag-for-slot.py | 18 ++-- snippets/app-handler-registration.py | 15 ++-- snippets/version-branch-skeleton.py | 10 ++- 8 files changed, 124 insertions(+), 75 deletions(-) diff --git a/skills/drivers-and-app-handlers/SKILL.md b/skills/drivers-and-app-handlers/SKILL.md index f396596..80ef6d5 100644 --- a/skills/drivers-and-app-handlers/SKILL.md +++ b/skills/drivers-and-app-handlers/SKILL.md @@ -145,15 +145,17 @@ The handlers live as lists at `bpy.app.handlers.`. To register, append; t | Handler | Signature | Fires when | | --- | --- | --- | -| `save_pre` | `(scene)` (some 4.x), `(scene, filepath)` (5.x) | Before the .blend is written. Use to clean up data you don't want serialized. | -| `save_post` | Same | After the .blend is written. Use for post-save bookkeeping. | -| `load_pre` | `(scene)` | Before a .blend is loaded. The current scene is still the old one. | -| `load_post` | `(scene)` | After a .blend is loaded. Use to validate or migrate add-on data. | +| `save_pre` | `(filepath: str)` | Before the .blend is written. The argument is the file being saved (empty string for the startup file), **not** a Scene. Use to clean up data you don't want serialized. | +| `save_post` | `(filepath: str)` | After the .blend is written. Same single filepath argument. | +| `load_pre` | `(filepath: str)` | Before a .blend is loaded. The argument is the file being loaded. | +| `load_post` | `(filepath: str)` | After a .blend is loaded. Use to validate or migrate add-on data. | | `depsgraph_update_pre` | `(scene, depsgraph)` | Before a depsgraph evaluation pass. | | `depsgraph_update_post` | `(scene, depsgraph)` | After a depsgraph evaluation pass. Fires very frequently; must be O(1) or near-O(1). | | `frame_change_pre` | `(scene, depsgraph)` | Before frame is set. | | `frame_change_post` | `(scene, depsgraph)` | After frame is set. | -| `exit_pre` (new in 5.1) | `(scene)` | Before Blender shuts down. Use for resource cleanup, telemetry flush, etc. | +| `exit_pre` (new in 5.1) | `(*args)` | Before Blender shuts down. Use for resource cleanup, telemetry flush, etc. The argument is not a Scene; accept `*args`. | + +The save/load handlers (`save_pre`, `save_post`, `load_pre`, `load_post`) all receive the **file path as a string** as their single argument, **not** a Scene. (Verified empirically on 4.5.10 LTS and 5.1.1, and against `bpy.app.handlers` docs: "Accepts one argument: the file being saved/loaded.") Only the depsgraph/frame-change handlers receive `(scene, depsgraph)`. The `exit_pre` handler in 5.1 is particularly useful for add-ons that need to release external resources (sockets, log files, child processes) deterministically before the process terminates. @@ -165,10 +167,15 @@ from bpy.app.handlers import persistent @persistent -def on_save_pre(scene, filepath): - """Clear temporary cache data before save so it doesn't bloat the .blend.""" - if 'my_addon_cache' in scene: - del scene['my_addon_cache'] +def on_save_pre(filepath): + """Clear temporary cache data before save so it doesn't bloat the .blend. + + The handler argument is the path being saved (a string), not a Scene, so + reach the scene(s) through bpy.data. + """ + for scene in bpy.data.scenes: + if 'my_addon_cache' in scene: + del scene['my_addon_cache'] def register(): @@ -201,7 +208,10 @@ from bpy.app.handlers import persistent @persistent -def increment_save_count(scene, filepath): +def increment_save_count(filepath): + # save_post passes the saved file path (a string) as its only argument. + # Store the running count on the current scene. + scene = bpy.context.scene counts = scene.get('save_counts', {}) counts[filepath] = counts.get(filepath, 0) + 1 scene['save_counts'] = counts @@ -224,8 +234,11 @@ from bpy.app.handlers import persistent @persistent -def cleanup_on_exit(scene): - """Release the external log file handle before the process terminates.""" +def cleanup_on_exit(*args): + """Release the external log file handle before the process terminates. + + exit_pre's argument is unused here; accept *args to stay signature-proof. + """ global _log_handle if _log_handle is not None: _log_handle.close() @@ -250,14 +263,14 @@ The `exit_pre` handler list is new in Blender 5.1. On 4.5 LTS, fall back to OS-l - **Doing real work inside `depsgraph_update_post`.** This handler fires on every depsgraph evaluation, which is many times per second during playback or interaction. Anything more than O(1) bookkeeping causes user-visible slowdown. - **Recursively modifying the scene from a depsgraph handler.** The modification triggers another depsgraph evaluation, which calls the handler, which modifies the scene. Infinite loop, often manifesting as a hang. - **Asymmetric register/unregister.** The handler is appended on register but not removed on unregister. Disabling the add-on leaves the callback in place. After enable/disable cycles, the callback runs N times per event. -- **Assuming the `save_pre` signature.** It changed across the 4.x to 5.x window. Use `(scene, filepath=None)` defensively, or `*args` if you don't need the values. +- **Treating the `save_pre` argument as a Scene.** The save/load handlers receive the **file path string** (empty for the startup file), not a Scene. Name the parameter `filepath` (or take `*args`), and reach scenes via `bpy.context.scene` / `bpy.data.scenes`. A membership test like `'key' in arg0` against the path string is silently wrong, and `del arg0['key']` raises `TypeError`. ## Version correctness | Topic | 4.5 LTS | 5.1 stable | | --- | --- | --- | | `exit_pre` handler | Not available | New in 5.1; use `atexit` fallback for 4.x | -| `save_pre` signature | `(scene)` in 4.0, varies | `(scene, filepath)` in 5.x | +| `save_pre` / `save_post` signature | `(filepath)` — a string | `(filepath)` — a string (unchanged) | | `driver_namespace` | Available | Same | | Driver security | Already restrictive | Same | diff --git a/skills/geometry-nodes-python/SKILL.md b/skills/geometry-nodes-python/SKILL.md index 9b057fb..c399dd4 100644 --- a/skills/geometry-nodes-python/SKILL.md +++ b/skills/geometry-nodes-python/SKILL.md @@ -154,10 +154,10 @@ The argument is the exact RNA `bl_idname` of the node class. A few you'll reach | Vector Math | `ShaderNodeVectorMath` | | Mix | `ShaderNodeMix` | | Noise Texture | `ShaderNodeTexNoise` | -| Mesh to SDF Grid (5.0+) | `GeometryNodeMeshToSDFGrid` | +| Mesh to SDF Grid (4.3+) | `GeometryNodeMeshToSDFGrid` | | SDF Grid to Mesh / Volume to Mesh | `GeometryNodeVolumeToMesh` | | Repeat Input / Output | `GeometryNodeRepeatInput`, `GeometryNodeRepeatOutput` | -| For Each Element Input / Output (5.0+) | `GeometryNodeForeachGeometryElementInput`, `GeometryNodeForeachGeometryElementOutput` | +| For Each Element Input / Output (4.3+) | `GeometryNodeForeachGeometryElementInput`, `GeometryNodeForeachGeometryElementOutput` | To list all available Geometry node types in your Blender version: @@ -243,8 +243,9 @@ def has_bundles(): return major >= 5 def has_for_each_element(): - major, _minor, _patch = bpy.app.version - return major >= 5 + # The For Each Element zone shipped in Blender 4.3, so it is available on + # the whole 4.5 LTS / 5.x supported range. (Bundles, by contrast, are 5.0+.) + return bpy.app.version >= (4, 3, 0) ``` ## Common AI mistakes diff --git a/skills/headless-batch-scripting/SKILL.md b/skills/headless-batch-scripting/SKILL.md index e3b7f56..81a43b4 100644 --- a/skills/headless-batch-scripting/SKILL.md +++ b/skills/headless-batch-scripting/SKILL.md @@ -161,11 +161,15 @@ def main(): parser = argparse.ArgumentParser() parser.add_argument("--output", required=True) parser.add_argument("--frames", default="1") - parser.add_argument("--engine", default="CYCLES", choices=["CYCLES", "BLENDER_EEVEE_NEXT"]) + parser.add_argument("--engine", default="CYCLES", choices=["CYCLES", "EEVEE"]) args = parser.parse_args(argv) scene = bpy.context.scene - scene.render.engine = args.engine + if args.engine == "EEVEE": + # EEVEE's engine id is 'BLENDER_EEVEE' on 5.0+, 'BLENDER_EEVEE_NEXT' on 4.2-4.5. + scene.render.engine = 'BLENDER_EEVEE' if bpy.app.version >= (5, 0, 0) else 'BLENDER_EEVEE_NEXT' + else: + scene.render.engine = args.engine scene.render.image_settings.file_format = 'PNG' output_dir = os.path.dirname(args.output) or "." @@ -185,7 +189,7 @@ if __name__ == "__main__": Notes: - `bpy.ops.render.render(write_still=True)` is one of the few `bpy.ops` calls that work in `--background`. -- `BLENDER_EEVEE_NEXT` is the 5.x EEVEE engine identifier. On 4.5 LTS, use `BLENDER_EEVEE`. Detect via `bpy.app.version`. +- EEVEE's engine identifier is `BLENDER_EEVEE` on Blender 5.0+ and `BLENDER_EEVEE_NEXT` on 4.2-4.5 LTS (the id was reclaimed in 5.0 after legacy EEVEE was removed in 4.2). Detect via `bpy.app.version`. ## Detecting Blender version in scripts @@ -195,9 +199,9 @@ import bpy major, minor, _patch = bpy.app.version if (major, minor) >= (5, 0): - eevee_engine = 'BLENDER_EEVEE_NEXT' -else: eevee_engine = 'BLENDER_EEVEE' +else: + eevee_engine = 'BLENDER_EEVEE_NEXT' ``` `bpy.app.version` is a `(major, minor, patch)` tuple, always reliable. diff --git a/skills/procedural-materials-and-shaders/SKILL.md b/skills/procedural-materials-and-shaders/SKILL.md index 51712a5..091436a 100644 --- a/skills/procedural-materials-and-shaders/SKILL.md +++ b/skills/procedural-materials-and-shaders/SKILL.md @@ -162,7 +162,7 @@ When the same subgraph appears across multiple materials, factor it into a `Shad | --- | --- | --- | --- | | Principled BSDF | `ShaderNodeBsdfPrincipled` | Same | Some inputs renamed in 5.0; `Specular` -> `Specular IOR Level`. Use string lookup with the 5.x name. | | Node group socket interface | `group.inputs.new` / `group.outputs.new` | `group.interface.new_socket` | Different APIs, see snippet `shader-node-group.py`. | -| EEVEE engine string | `'BLENDER_EEVEE'` | `'BLENDER_EEVEE_NEXT'` | EEVEE Next is the default in 5.x; legacy EEVEE was retired. | +| EEVEE engine string | `'BLENDER_EEVEE_NEXT'` | `'BLENDER_EEVEE'` | Legacy EEVEE was removed in 4.2. EEVEE Next used the id `'BLENDER_EEVEE_NEXT'` on 4.2-4.5, then reclaimed the plain `'BLENDER_EEVEE'` id in 5.0. | | Layered Textures | Not present | Not present in 5.1 | Roadmap pushed to 2027. Do not generate code referencing it. | EEVEE Next stabilized in Blender 5.1 with material caching and feature parity improvements; most Principled BSDF graphs render identically across EEVEE Next and Cycles. diff --git a/skills/slotted-actions-animation/SKILL.md b/skills/slotted-actions-animation/SKILL.md index cce726b..7eccc3a 100644 --- a/skills/slotted-actions-animation/SKILL.md +++ b/skills/slotted-actions-animation/SKILL.md @@ -1,6 +1,6 @@ --- name: slotted-actions-animation -description: Animate from Python under the Blender 5.x Slotted Actions architecture. Action contains Layers contain Strips contain Channelbags. The action_ensure_channelbag_for_slot bridge utility for 4.5 LTS and 5.x compatibility. +description: Animate from Python under the Slotted Actions architecture (data model shipped in Blender 4.4). Action contains Layers contain Strips contain Channelbags. Cross-version channelbag access - action_ensure_channelbag_for_slot is new in 5.0; on 4.4/4.5 LTS use strip.channelbag(slot, ensure=True) or the still-present legacy action.fcurves. standards-version: 1.10.0 --- @@ -17,9 +17,14 @@ Use this skill when the user: ## What changed and why -Blender 5.0 introduced **Slotted Actions** to support non-linear, multi-target animation. The pre-5.0 Action data model was a flat list of F-curves owned directly by the Action. That made it impossible for one Action to drive two different IDs (e.g. an armature pose and the armature's custom property channels at the same time) without overwriting each other. +Blender **4.4** introduced the **Slotted Actions** data model to support non-linear, multi-target animation. The old model was a flat list of F-curves owned directly by the Action, which made it impossible for one Action to drive two different IDs (e.g. an armature pose and the armature's custom property channels at the same time) without overwriting each other. -The 5.x model: +Two version boundaries matter: + +- **Blender 4.4 / 4.5 LTS**: the slotted data model (`action.slots`, `action.layers`, `strip.channelbag`) is present **alongside** the legacy flat API (`action.fcurves`, `action.groups`, `action.id_root`), which still works as a proxy. The helper `bpy_extras.anim_utils.action_get_channelbag_for_slot(action, slot)` exists; `action_ensure_channelbag_for_slot` does **not**. +- **Blender 5.0+**: the legacy flat API was **removed** (`action.fcurves` raises `AttributeError`). The new helper `bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot)` is added. + +The slotted model: ``` Action @@ -30,44 +35,54 @@ Action FCurves[] ``` -Pre-5.0 code that walked `action.fcurves` directly does not find anything in a 5.x Action because the F-curves live inside a Channelbag inside a Strip inside a Layer. +On 5.0+, code that walks `action.fcurves` directly raises `AttributeError` because that property was removed; the F-curves live inside a Channelbag inside a Strip inside a Layer. On 4.4 / 4.5 LTS `action.fcurves` still works as a proxy. -## The bridge utility: `action_ensure_channelbag_for_slot` +## Getting a channelbag across versions -`bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot)` is the cross-version helper that: +`bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot)` is **new in Blender 5.0**. It ensures the Action has a Layer, a Strip in that Layer, and a Channelbag for the given Slot, then returns the Channelbag. It does **not** exist on 4.4 / 4.5 LTS — there is no auto-detecting shim, and importing-and-calling it on 4.5 raises `AttributeError`. -- On 5.x: ensures the Action has a Layer, a Strip in that Layer, and a Channelbag for the given Slot, then returns the Channelbag. -- On 4.5 LTS where there is no Slotted Actions concept: returns a shim object that exposes `.fcurves` pointing at `action.fcurves`, so the same calling code works. +On 4.4 / 4.5 LTS, ensure the channelbag yourself with `strip.channelbag(slot, ensure=True)` (the slotted model is present there), or just use the still-present legacy `action.fcurves`. So a genuinely cross-version helper must branch on `bpy.app.version`: -Import path verified against the Blender 5.1 API reference: `bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot)`. See [`bpy_extras.anim_utils`](https://docs.blender.org/api/current/bpy_extras.anim_utils.html). +Import path (5.0+) verified against the Blender 5.1 API reference: `bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot)`. See [`bpy_extras.anim_utils`](https://docs.blender.org/api/current/bpy_extras.anim_utils.html). ```python import bpy -from bpy_extras.anim_utils import action_ensure_channelbag_for_slot + + +def get_channelbag_for_slot(action, slot): + """Return the Channelbag for `slot`, creating layer/strip/channelbag as needed. + + Verified on Blender 4.5.10 LTS and 5.1.1. + """ + if bpy.app.version >= (5, 0, 0): + from bpy_extras.anim_utils import action_ensure_channelbag_for_slot + return action_ensure_channelbag_for_slot(action, slot) + # 4.4 / 4.5 LTS: the ensure-helper does not exist; build the path explicitly. + layer = action.layers[0] if action.layers else action.layers.new("Layer") + strip = layer.strips[0] if layer.strips else layer.strips.new(type='KEYFRAME') + return strip.channelbag(slot, ensure=True) def get_channelbag_for_object(obj): - """Return the Channelbag (5.x) or fcurves shim (4.5 LTS) for obj's animation.""" + """Return the Channelbag for obj's animation, creating Action and Slot if needed.""" if obj.animation_data is None: obj.animation_data_create() - if obj.animation_data.action is None: + action = obj.animation_data.action + if action is None: action = bpy.data.actions.new(name=f"{obj.name}_Action") obj.animation_data.action = action - else: - action = obj.animation_data.action - # On 5.x, slot may not be set; pick or create one for this ID. + # Slots exist on 4.4+; create and bind one for this ID if not set. slot = obj.animation_data.action_slot - if slot is None and hasattr(action, "slots"): - # Create a slot bound to OBJECT type, then assign it. + if slot is None: slot = action.slots.new(id_type='OBJECT', name=obj.name) obj.animation_data.action_slot = slot - return action_ensure_channelbag_for_slot(action, slot) + return get_channelbag_for_slot(action, slot) ``` -The `hasattr(action, "slots")` check is the version sniff: present in 5.x, absent in 4.5 LTS. +The version boundary that matters is `bpy.app.version >= (5, 0, 0)` (legacy removed / ensure-helper added), **not** `hasattr(action, "slots")` — `action.slots` is present on 4.4 and 4.5 too, so that check does not distinguish 4.5 from 5.x. ## Inserting keyframes the cross-version way @@ -94,7 +109,7 @@ For programmatic curve construction (importing motion capture, writing a curve s ```python import bpy -from bpy_extras.anim_utils import action_ensure_channelbag_for_slot +# get_channelbag_for_slot is the cross-version helper defined above. def get_or_create_fcurve(obj, data_path, index): @@ -107,11 +122,11 @@ def get_or_create_fcurve(obj, data_path, index): obj.animation_data.action = action slot = obj.animation_data.action_slot - if slot is None and hasattr(action, "slots"): + if slot is None: slot = action.slots.new(id_type='OBJECT', name=obj.name) obj.animation_data.action_slot = slot - cbag = action_ensure_channelbag_for_slot(action, slot) + cbag = get_channelbag_for_slot(action, slot) fcurve = next( (fc for fc in cbag.fcurves if fc.data_path == data_path and fc.array_index == index), @@ -137,11 +152,11 @@ def bake_translation(obj, frame_value_pairs): Notes: - `options={'FAST'}` skips per-insertion sorting and curve update; call `fc.update()` once at the end. -- `cbag.fcurves` is the unified access point. On 4.5 LTS, the bridge returns a shim whose `.fcurves` is `action.fcurves`. On 5.x, it's the real Channelbag's F-curves. +- `cbag.fcurves` is the unified access point on both 4.4/4.5 LTS and 5.x. The `get_channelbag_for_slot` helper above returns the slot's real Channelbag on every version (via `strip.channelbag(slot, ensure=True)` on 4.4/4.5, the ensure-helper on 5.0+). -## The 4.5 LTS path explicitly +## The legacy `action.fcurves` path (4.4 / 4.5 LTS only) -If you are not yet on 5.x and want pre-bridge code: +On 4.4 and 4.5 LTS the legacy flat API is still present and is the simplest path if you do not need to target 5.x: ```python import bpy @@ -158,23 +173,28 @@ fcurve.keyframe_points.insert(1, 0.0) fcurve.keyframe_points.insert(24, 5.0) ``` -This compiles on 4.5 LTS but on 5.x the `action.fcurves` attribute is absent or empty (depending on the exact 5.x version), and curves added there do not drive the animation. +This works on 4.4 / 4.5 LTS, but on **5.0+** the `action.fcurves` attribute was removed entirely, so `action.fcurves.new(...)` raises `AttributeError`. For 5.x, go through the channelbag (`get_channelbag_for_slot` above). -## Detecting Slotted Actions support +## Detecting the version boundaries ```python import bpy -def has_slotted_actions(): - major, minor, _ = bpy.app.version - return (major, minor) >= (5, 0) +def has_slotted_model(): + # action.slots / layers / strip.channelbag: shipped in Blender 4.4. + return bpy.app.version >= (4, 4, 0) + +def legacy_fcurves_removed(): + # action.fcurves / groups / id_root removed, and + # action_ensure_channelbag_for_slot added: Blender 5.0. + return bpy.app.version >= (5, 0, 0) ``` -Use the bridge utility unconditionally if you can. Use the version sniff only when the bridge cannot help (e.g. enumerating slots, which is meaningful only on 5.x). +The slotted data model (and `action.slots`) is meaningful on 4.4 and 4.5 too, so do not gate slot code behind `>= (5, 0)`. The boundary that decides legacy-removal and which channelbag helper exists is `>= (5, 0, 0)` — that is the check `get_channelbag_for_slot` uses above. ## Common AI mistakes -1. **Using pre-5.0 `action.fcurves.new(...)` in 5.x code**. The call may not raise but the curve will not drive anything because the slot/layer routing isn't set up. +1. **Using legacy `action.fcurves.new(...)` in 5.x code**. On Blender 5.0+ the `action.fcurves` property was removed, so this raises `AttributeError: 'Action' object has no attribute 'fcurves'`. Go through a channelbag instead. 2. **Forgetting the slot binding**: @@ -197,12 +217,12 @@ Use the bridge utility unconditionally if you can. Use the version sniff only wh ## Compatibility paths summary -| Operation | 4.5 LTS | 5.x | Cross-version | +| Operation | 4.4 / 4.5 LTS | 5.x | Cross-version | | --- | --- | --- | --- | | Insert a single keyframe | `obj.keyframe_insert("location", frame=1)` | Same | High-level API works on both | -| Get the F-curves for an Action+Object | `action.fcurves` | Channelbag inside Layer/Strip/Slot | `action_ensure_channelbag_for_slot(action, slot)` | +| Get the F-curves for an Action+Slot | `strip.channelbag(slot, ensure=True)`, or legacy `action.fcurves` | `action_ensure_channelbag_for_slot(action, slot)` | `get_channelbag_for_slot(action, slot)` (branches on version) | | Create an Action | `bpy.data.actions.new(...)` | Same | Same | -| Bind Action to ID | `obj.animation_data.action = action` | Same plus `action_slot` | Set both, the slot setter is a no-op on 4.5 LTS | +| Bind Action to ID | `obj.animation_data.action = action` + `action_slot` | Same | `action_slot` is settable on 4.4+; set both | ## Related diff --git a/snippets/action-ensure-channelbag-for-slot.py b/snippets/action-ensure-channelbag-for-slot.py index 4be1605..72633b5 100644 --- a/snippets/action-ensure-channelbag-for-slot.py +++ b/snippets/action-ensure-channelbag-for-slot.py @@ -1,12 +1,14 @@ -# Slotted Actions bridge: action_ensure_channelbag_for_slot. -# In Blender 5.x, an Action contains Layers contain Strips contain -# Channelbags, and F-curves live inside Channelbags keyed by an -# animation slot. The bridging utility ensures the Channelbag exists -# for the slot bound to the given ID and returns it; on 4.x it returns -# a compatibility shim that exposes the same .fcurves.new(...) API. +# Cross-version channelbag access for Slotted Actions. # -# Verify the import path against your Blender version's docs. The -# canonical location at the time of writing is bpy_extras.anim_utils. +# The Slotted Actions data model (Action -> Layers -> Strips -> Channelbags, +# with F-curves living inside a Channelbag keyed by an animation slot) shipped +# in Blender 4.4. Two version paths, branched on bpy.app.version below: +# - 5.0+: legacy action.fcurves was removed; use the new +# bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot). +# - 4.4 / 4.5 LTS: that ensure-helper does NOT exist (no shim). The legacy +# action.fcurves API is still present, so use it directly. +# +# Verified on Blender 4.5.10 LTS and 5.1.1. Import path (5.0+): # # Reference: # https://docs.blender.org/api/current/bpy_extras.anim_utils.html diff --git a/snippets/app-handler-registration.py b/snippets/app-handler-registration.py index b250828..eb4702a 100644 --- a/snippets/app-handler-registration.py +++ b/snippets/app-handler-registration.py @@ -8,11 +8,16 @@ @persistent -def on_save_pre(scene, *args): - """Strip ephemeral cache before save. Signature is defensive against - the (scene) vs (scene, filepath) variation across 4.x and 5.x.""" - if 'my_addon_cache' in scene: - del scene['my_addon_cache'] +def on_save_pre(filepath, *args): + """Strip ephemeral cache before save. + + save_pre passes the file path being saved (a string), NOT a Scene -- the + same is true on 4.5 LTS and 5.x. Reach the scene(s) via bpy.data; never + treat the first argument as a Scene. Verified on Blender 4.5.10 and 5.1.1. + """ + for scene in bpy.data.scenes: + if 'my_addon_cache' in scene: + del scene['my_addon_cache'] def register(): diff --git a/snippets/version-branch-skeleton.py b/snippets/version-branch-skeleton.py index 0af8cc3..5cbe51d 100644 --- a/snippets/version-branch-skeleton.py +++ b/snippets/version-branch-skeleton.py @@ -21,7 +21,11 @@ def clear_property(obj, key): def get_eevee_engine_id(): - """EEVEE engine identifier renamed from BLENDER_EEVEE to BLENDER_EEVEE_NEXT in 5.x.""" + """EEVEE engine identifier across versions. + + Legacy EEVEE was removed in 4.2; EEVEE Next used the id 'BLENDER_EEVEE_NEXT' + on 4.2 through 4.5 LTS, then reclaimed the plain 'BLENDER_EEVEE' id in 5.0. + """ if bpy.app.version >= (5, 0, 0): - return 'BLENDER_EEVEE_NEXT' - return 'BLENDER_EEVEE' + return 'BLENDER_EEVEE' + return 'BLENDER_EEVEE_NEXT'