diff --git a/skills/slotted-actions-animation/SKILL.md b/skills/slotted-actions-animation/SKILL.md index 7eccc3a4..97d8632c 100644 --- a/skills/slotted-actions-animation/SKILL.md +++ b/skills/slotted-actions-animation/SKILL.md @@ -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). @@ -156,12 +156,14 @@ 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: @@ -169,11 +171,14 @@ if obj.animation_data.action is None: 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 @@ -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` @@ -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 | diff --git a/snippets/action-ensure-channelbag-for-slot.py b/snippets/action-ensure-channelbag-for-slot.py index b763a56c..df836f8a 100644 --- a/snippets/action-ensure-channelbag-for-slot.py +++ b/snippets/action-ensure-channelbag-for-slot.py @@ -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: @@ -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()