Skip to content

Commit 350b75d

Browse files
TMHSDigitalclaude
andauthored
fix(snippets): bind the action slot on the 4.5 LTS channelbag path (#406)
On 4.5 LTS the snippet wrote keys through legacy action.fcurves.new() on a slotless Action. That creates a "Legacy Slot" but never assigns it, so animation_data.action_slot stays None and the object never animates; no error is raised. Measured on 4.5.11: z stayed 0.0 at frame 24 instead of 2.0. The snippet now binds the slot on both versions and builds the 4.5 layer/strip/channelbag path with strip.channelbag(slot, ensure=True), mirroring the skill's get_channelbag_for_slot helper. The skill's legacy section now binds the slot explicitly and says why, and its table and cross-version intro stop recommending bare action.fcurves. Closes #385 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent bdaeb95 commit 350b75d

2 files changed

Lines changed: 29 additions & 18 deletions

File tree

‎skills/slotted-actions-animation/SKILL.md‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ On 5.0+, code that walks `action.fcurves` directly raises `AttributeError` becau
4141

4242
`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`.
4343

44-
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`:
44+
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`:
4545

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

@@ -156,24 +156,29 @@ Notes:
156156

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

159-
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:
159+
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:
160160

161161
```python
162162
import bpy
163163

164164
obj = bpy.context.active_object
165+
if obj is None:
166+
raise RuntimeError("no active object")
165167
if obj.animation_data is None:
166168
obj.animation_data_create()
167169
if obj.animation_data.action is None:
168170
obj.animation_data.action = bpy.data.actions.new(name=f"{obj.name}_Action")
169171

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

176-
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).
181+
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`.
177182

178183
## Detecting the version boundaries
179184

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

207-
Without a slot binding, the Action has no idea which datablock it drives. Symptom: animation plays back as if it isn't there.
212+
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.
208213

209214
3. **Hardcoding the import path** to where it was in some older 5.x. Verify against current docs:
210215
- 5.1+: `from bpy_extras.anim_utils import action_ensure_channelbag_for_slot`
@@ -220,7 +225,7 @@ The slotted data model (and `action.slots`) is meaningful on 4.4 and 4.5 too, so
220225
| Operation | 4.4 / 4.5 LTS | 5.x | Cross-version |
221226
| --- | --- | --- | --- |
222227
| Insert a single keyframe | `obj.keyframe_insert("location", frame=1)` | Same | High-level API works on both |
223-
| 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) |
228+
| 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) |
224229
| Create an Action | `bpy.data.actions.new(...)` | Same | Same |
225230
| Bind Action to ID | `obj.animation_data.action = action` + `action_slot` | Same | `action_slot` is settable on 4.4+; set both |
226231

‎snippets/action-ensure-channelbag-for-slot.py‎

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
# in Blender 4.4. Two version paths, branched on bpy.app.version below:
66
# - 5.0+: legacy action.fcurves was removed; use the new
77
# bpy_extras.anim_utils.action_ensure_channelbag_for_slot(action, slot).
8-
# - 4.4 / 4.5 LTS: that ensure-helper does NOT exist (no shim). The legacy
9-
# action.fcurves API is still present, so use it directly.
8+
# - 4.4 / 4.5 LTS: that ensure-helper does NOT exist (no shim). Build the
9+
# layer / strip / channelbag path explicitly with strip.channelbag(...).
1010
#
11-
# Verified on Blender 4.5.10 LTS and 5.1.1. Import path (5.0+):
11+
# Bind the slot on BOTH versions. On 4.5, legacy action.fcurves.new() on a
12+
# slotless Action creates a "Legacy Slot" but never assigns it, so the keys
13+
# exist and the object never animates (action_slot stays None).
14+
#
15+
# Verified on Blender 4.5.11 LTS and 5.2.1 LTS. Import path (5.0+):
1216
# from bpy_extras import anim_utils
1317
#
1418
# Reference:
@@ -28,19 +32,21 @@ def add_z_keyframe(obj, frame=1, value=0.0):
2832
action = bpy.data.actions.new(name=f"{obj.name}_Action")
2933
obj.animation_data.action = action
3034

35+
slot = obj.animation_data.action_slot
36+
if slot is None:
37+
slot = action.slots.new(id_type='OBJECT', name=obj.name)
38+
obj.animation_data.action_slot = slot
39+
3140
if bpy.app.version >= (5, 0, 0):
32-
slot = obj.animation_data.action_slot
33-
if slot is None:
34-
slot = action.slots.new(id_type='OBJECT', name=obj.name)
35-
obj.animation_data.action_slot = slot
3641
channelbag = anim_utils.action_ensure_channelbag_for_slot(action, slot)
37-
fcurve = channelbag.fcurves.find(data_path="location", index=2)
38-
if fcurve is None:
39-
fcurve = channelbag.fcurves.new(data_path="location", index=2)
4042
else:
41-
fcurve = action.fcurves.find(data_path="location", index=2)
42-
if fcurve is None:
43-
fcurve = action.fcurves.new(data_path="location", index=2)
43+
layer = action.layers[0] if action.layers else action.layers.new("Layer")
44+
strip = layer.strips[0] if layer.strips else layer.strips.new(type='KEYFRAME')
45+
channelbag = strip.channelbag(slot, ensure=True)
46+
47+
fcurve = channelbag.fcurves.find(data_path="location", index=2)
48+
if fcurve is None:
49+
fcurve = channelbag.fcurves.new(data_path="location", index=2)
4450

4551
fcurve.keyframe_points.insert(frame, value)
4652
fcurve.update()

0 commit comments

Comments
 (0)