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
15 changes: 10 additions & 5 deletions skills/slotted-actions-animation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ On 5.0+, code that walks `action.fcurves` directly raises `AttributeError` becau

`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 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`:
On 4.4 / 4.5 LTS, ensure the channelbag yourself with `strip.channelbag(slot, ensure=True)` (the slotted model is present there). The still-present legacy `action.fcurves` is a trap on a fresh Action: it creates a slot but does not bind it (see the legacy section below). So a genuinely cross-version helper must branch on `bpy.app.version`:

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).

Expand Down Expand Up @@ -156,24 +156,29 @@ Notes:

## The legacy `action.fcurves` path (4.4 / 4.5 LTS only)

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:
On 4.4 and 4.5 LTS the legacy flat API is still present, but on a fresh Action it does **not** bind a slot. `action.fcurves.new(...)` creates a slot named `Legacy Slot` and leaves `obj.animation_data.action_slot` as `None`, so the keys exist and the object never animates. Bind the slot yourself after the first legacy write:

```python
import bpy

obj = bpy.context.active_object
if obj is None:
raise RuntimeError("no active object")
if obj.animation_data is None:
obj.animation_data_create()
if obj.animation_data.action is None:
obj.animation_data.action = bpy.data.actions.new(name=f"{obj.name}_Action")

action = obj.animation_data.action
fcurve = action.fcurves.new(data_path="location", index=0)
if obj.animation_data.action_slot is None:
# The legacy write created the slot but did not assign it.
obj.animation_data.action_slot = action.slots[0]
fcurve.keyframe_points.insert(1, 0.0)
fcurve.keyframe_points.insert(24, 5.0)
```

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).
Measured on 4.5.11 LTS: without the `action_slot` line, `frame_set(24)` leaves the object at its rest value. Prefer the channelbag path (`get_channelbag_for_slot` above), which binds the slot before writing on every version. On **5.0+** the `action.fcurves` attribute was removed entirely, so `action.fcurves.new(...)` raises `AttributeError`.

## Detecting the version boundaries

Expand Down Expand Up @@ -204,7 +209,7 @@ The slotted data model (and `action.slots`) is meaningful on 4.4 and 4.5 too, so
# Missing: setting obj.animation_data.action_slot
```

Without a slot binding, the Action has no idea which datablock it drives. Symptom: animation plays back as if it isn't there.
Without a slot binding, the Action has no idea which datablock it drives. Symptom: animation plays back as if it isn't there. On 4.5 LTS the legacy `action.fcurves.new(...)` path hits this silently: it creates a slot but never assigns it.

3. **Hardcoding the import path** to where it was in some older 5.x. Verify against current docs:
- 5.1+: `from bpy_extras.anim_utils import action_ensure_channelbag_for_slot`
Expand All @@ -220,7 +225,7 @@ The slotted data model (and `action.slots`) is meaningful on 4.4 and 4.5 too, so
| 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+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) |
| Get the F-curves for an Action+Slot | `strip.channelbag(slot, ensure=True)` (legacy `action.fcurves` only with an explicit `action_slot` bind) | `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` + `action_slot` | Same | `action_slot` is settable on 4.4+; set both |

Expand Down
32 changes: 19 additions & 13 deletions snippets/action-ensure-channelbag-for-slot.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
# 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.
# - 4.4 / 4.5 LTS: that ensure-helper does NOT exist (no shim). Build the
# layer / strip / channelbag path explicitly with strip.channelbag(...).
#
# Verified on Blender 4.5.10 LTS and 5.1.1. Import path (5.0+):
# Bind the slot on BOTH versions. On 4.5, legacy action.fcurves.new() on a
# slotless Action creates a "Legacy Slot" but never assigns it, so the keys
# exist and the object never animates (action_slot stays None).
#
# Verified on Blender 4.5.11 LTS and 5.2.1 LTS. Import path (5.0+):
# from bpy_extras import anim_utils
#
# Reference:
Expand All @@ -28,19 +32,21 @@ def add_z_keyframe(obj, frame=1, value=0.0):
action = bpy.data.actions.new(name=f"{obj.name}_Action")
obj.animation_data.action = action

slot = obj.animation_data.action_slot
if slot is None:
slot = action.slots.new(id_type='OBJECT', name=obj.name)
obj.animation_data.action_slot = slot

if bpy.app.version >= (5, 0, 0):
slot = obj.animation_data.action_slot
if slot is None:
slot = action.slots.new(id_type='OBJECT', name=obj.name)
obj.animation_data.action_slot = slot
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
fcurve = channelbag.fcurves.find(data_path="location", index=2)
if fcurve is None:
fcurve = channelbag.fcurves.new(data_path="location", index=2)
else:
fcurve = action.fcurves.find(data_path="location", index=2)
if fcurve is None:
fcurve = action.fcurves.new(data_path="location", index=2)
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')
channelbag = strip.channelbag(slot, ensure=True)

fcurve = channelbag.fcurves.find(data_path="location", index=2)
if fcurve is None:
fcurve = channelbag.fcurves.new(data_path="location", index=2)

fcurve.keyframe_points.insert(frame, value)
fcurve.update()
Expand Down
Loading