2026-02-16
This commit is contained in:
@@ -1,62 +1,41 @@
|
||||
import bpy
|
||||
"""
|
||||
Remove Static FCurves - Blender Extension
|
||||
Removes animation channels with static (unchanging) values
|
||||
"""
|
||||
|
||||
bl_info = {
|
||||
"name": "Remove Static FCurves",
|
||||
"author": "lokimckay",
|
||||
"version": (0, 3, 0),
|
||||
"blender": (5, 0, 0),
|
||||
"location": "Graph Editor > Channel > Remove Static FCurves",
|
||||
"description": "Remove animation channels with static (unchanging) values",
|
||||
"category": "Animation",
|
||||
}
|
||||
|
||||
class RemoveStaticFcurvesOperator(bpy.types.Operator):
|
||||
"""Operator to remove static FCurves"""
|
||||
bl_idname = "graph.remove_static_fcurves"
|
||||
bl_label = "Remove Static FCurves"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
has_selection = any(obj.select_get()
|
||||
for obj in bpy.context.selected_objects)
|
||||
|
||||
if not has_selection:
|
||||
self.report({'ERROR_INVALID_INPUT'}, "Please select objects.")
|
||||
else:
|
||||
self.remove_static_fcurves()
|
||||
self.report({'INFO'}, "Removed static animation channels.")
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def is_static_fcurve(fcurve):
|
||||
"""Check if an FCurve is static (all keyframes have the same value)."""
|
||||
keyframes = fcurve.keyframe_points
|
||||
if len(keyframes) < 2:
|
||||
return True # A single keyframe is considered static
|
||||
|
||||
first_value = keyframes[0].co[1]
|
||||
return all(kf.co[1] == first_value for kf in keyframes)
|
||||
|
||||
@staticmethod
|
||||
def remove_static_fcurves():
|
||||
"""Remove static FCurves that have no data."""
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
action = obj.animation_data.action
|
||||
fcurves_to_remove = [
|
||||
fcurve for fcurve in action.fcurves if RemoveStaticFcurvesOperator.is_static_fcurve(fcurve)]
|
||||
|
||||
for fcurve in fcurves_to_remove:
|
||||
action.fcurves.remove(fcurve)
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
self.layout.operator(RemoveStaticFcurvesOperator.bl_idname)
|
||||
# Handle module reloading
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
if "operator" in locals():
|
||||
importlib.reload(operator)
|
||||
if "utils" in locals():
|
||||
importlib.reload(utils)
|
||||
print("[Remove Static FCurves] Reloading modules...")
|
||||
else:
|
||||
from . import operator
|
||||
from . import utils
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(RemoveStaticFcurvesOperator)
|
||||
bpy.types.GRAPH_MT_channel.append(menu_func)
|
||||
bpy.types.DOPESHEET_MT_channel.append(menu_func)
|
||||
print("[Remove Static FCurves] registered")
|
||||
"""Register all classes and handlers"""
|
||||
operator.register()
|
||||
print("[Remove Static FCurves] Registered")
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(RemoveStaticFcurvesOperator)
|
||||
bpy.types.GRAPH_MT_channel.remove(menu_func)
|
||||
bpy.types.DOPESHEET_MT_channel.remove(menu_func)
|
||||
print("[Remove Static FCurves] unregistered")
|
||||
"""Unregister all classes and handlers"""
|
||||
operator.unregister()
|
||||
print("[Remove Static FCurves] Unregistered")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -3,7 +3,7 @@ schema_version = "1.0.0"
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "remove_static_fcurves"
|
||||
version = "0.2.1"
|
||||
version = "0.4.0"
|
||||
name = "Remove Static FCurves"
|
||||
tagline = "Clean up animation channels that have no data"
|
||||
maintainer = "Loki McKay <lokimckay@gmail.com>"
|
||||
@@ -24,9 +24,7 @@ blender_version_min = "4.2.0"
|
||||
|
||||
# License conforming to https://spdx.org/licenses/ (use "SPDX: prefix)
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/licenses.html
|
||||
license = [
|
||||
"SPDX:GPL-3.0-or-later",
|
||||
]
|
||||
license = ["SPDX:GPL-3.0-or-later"]
|
||||
# # Optional: required by some licenses.
|
||||
# copyright = [
|
||||
# "2002-2024 Developer Name",
|
||||
@@ -72,4 +70,4 @@ license = [
|
||||
# "__pycache__/",
|
||||
# "/.git/",
|
||||
# "/*.zip",
|
||||
# ]
|
||||
# ]
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
"""
|
||||
Operators for removing static FCurves
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from . import utils
|
||||
|
||||
|
||||
class RemoveStaticFcurvesOperator(bpy.types.Operator):
|
||||
"""Operator to remove static FCurves"""
|
||||
bl_idname = "graph.remove_static_fcurves"
|
||||
bl_label = "Remove Static FCurves"
|
||||
bl_options = {"REGISTER", "UNDO"}
|
||||
|
||||
def execute(self, context):
|
||||
# Get objects with animation data
|
||||
objects_to_check = set()
|
||||
|
||||
# Add selected objects
|
||||
objects_to_check.update(
|
||||
obj for obj in context.selected_objects if obj.animation_data)
|
||||
|
||||
# Add visible objects from Graph Editor if we're in that context
|
||||
if context.area and context.area.type == 'GRAPH_EDITOR':
|
||||
for obj in context.scene.objects:
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
objects_to_check.add(obj)
|
||||
|
||||
# Also check the active object specifically
|
||||
if context.active_object and context.active_object.animation_data:
|
||||
objects_to_check.add(context.active_object)
|
||||
|
||||
if not objects_to_check:
|
||||
self.report({'WARNING'}, "No objects with animation data found.")
|
||||
return {'CANCELLED'}
|
||||
|
||||
print(f"\n=== Remove Static FCurves ===")
|
||||
print(f"Checking {len(objects_to_check)} object(s)")
|
||||
|
||||
removed_count = self.remove_static_fcurves(objects_to_check)
|
||||
|
||||
if removed_count > 0:
|
||||
self.report(
|
||||
{'INFO'}, f"Removed {removed_count} static animation channel(s).")
|
||||
else:
|
||||
self.report({'INFO'}, "No static FCurves found to remove.")
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@staticmethod
|
||||
def is_static_fcurve(fcurve):
|
||||
# If the curve has modifiers, keep it
|
||||
if fcurve.modifiers and len(fcurve.modifiers) > 0:
|
||||
return False
|
||||
|
||||
"""Check if an FCurve is static (all keyframes have the same value)."""
|
||||
keyframes = fcurve.keyframe_points
|
||||
if len(keyframes) < 2:
|
||||
return True # A single keyframe is considered static
|
||||
|
||||
first_value = keyframes[0].co[1]
|
||||
tolerance = 0.0001 # Small tolerance for floating point comparison
|
||||
return all(abs(kf.co[1] - first_value) < tolerance for kf in keyframes)
|
||||
|
||||
@staticmethod
|
||||
def remove_static_fcurves(objects):
|
||||
"""Remove static FCurves from the given objects."""
|
||||
removed_count = 0
|
||||
|
||||
for obj in objects:
|
||||
if not obj.animation_data or not obj.animation_data.action:
|
||||
continue
|
||||
|
||||
print(f"\nProcessing object: {obj.name}")
|
||||
action = obj.animation_data.action
|
||||
print(f" Action: {action.name}")
|
||||
|
||||
# Get FCurves using the compatibility function
|
||||
fcurves_data = utils.compat.get_fcurves_from_object(obj)
|
||||
|
||||
if not fcurves_data:
|
||||
print(f" No FCurves found")
|
||||
continue
|
||||
|
||||
print(f" Found {len(fcurves_data)} FCurve(s)")
|
||||
|
||||
# Check each FCurve
|
||||
for action, fcurve, slot in fcurves_data:
|
||||
is_static = RemoveStaticFcurvesOperator.is_static_fcurve(
|
||||
fcurve)
|
||||
|
||||
if is_static:
|
||||
data_path = fcurve.data_path
|
||||
array_idx = fcurve.array_index
|
||||
print(
|
||||
f" Removing static FCurve: {data_path}[{array_idx}]")
|
||||
|
||||
# Remove using the compatibility function
|
||||
if utils.compat.remove_action_fcurve(action, fcurve, slot):
|
||||
removed_count += 1
|
||||
else:
|
||||
print(f" Failed to remove FCurve")
|
||||
else:
|
||||
print(
|
||||
f" Keeping animated FCurve: {fcurve.data_path}[{fcurve.array_index}]")
|
||||
|
||||
return removed_count
|
||||
|
||||
|
||||
def menu_func(self, context):
|
||||
self.layout.operator(RemoveStaticFcurvesOperator.bl_idname)
|
||||
|
||||
|
||||
def register():
|
||||
bpy.utils.register_class(RemoveStaticFcurvesOperator)
|
||||
bpy.types.GRAPH_MT_channel.append(menu_func)
|
||||
bpy.types.DOPESHEET_MT_channel.append(menu_func)
|
||||
print("[RemoveStaticFcurvesOperator] registered")
|
||||
|
||||
|
||||
def unregister():
|
||||
bpy.utils.unregister_class(RemoveStaticFcurvesOperator)
|
||||
bpy.types.GRAPH_MT_channel.remove(menu_func)
|
||||
bpy.types.DOPESHEET_MT_channel.remove(menu_func)
|
||||
print("[RemoveStaticFcurvesOperator] unregistered")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
@@ -0,0 +1,14 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: remove-static-fcurves
|
||||
Version: 0.1.0
|
||||
Summary: Remove fcurves from Blender animations that don't change at all.
|
||||
Author-email: lokimckay <lokimckay@gmail.com>
|
||||
License: MIT
|
||||
Requires-Python: ==3.10.*
|
||||
Description-Content-Type: text/markdown
|
||||
License-File: LICENSE
|
||||
Dynamic: license-file
|
||||
|
||||
# Remove Static FCurves
|
||||
|
||||
Cleans up animation channels that don't contain any animation data
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
LICENSE
|
||||
README.md
|
||||
pyproject.toml
|
||||
src/__init__.py
|
||||
src/operator.py
|
||||
src/remove_static_fcurves.egg-info/PKG-INFO
|
||||
src/remove_static_fcurves.egg-info/SOURCES.txt
|
||||
src/remove_static_fcurves.egg-info/dependency_links.txt
|
||||
src/remove_static_fcurves.egg-info/top_level.txt
|
||||
src/utils/__init__.py
|
||||
src/utils/compat.py
|
||||
src/utils/version.py
|
||||
+1
@@ -0,0 +1 @@
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
__init__
|
||||
operator
|
||||
utils
|
||||
@@ -0,0 +1,18 @@
|
||||
"""
|
||||
Utility modules for Remove Static FCurves extension
|
||||
"""
|
||||
|
||||
# Handle module reloading
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
if "compat" in locals():
|
||||
importlib.reload(compat)
|
||||
if "version" in locals():
|
||||
importlib.reload(version)
|
||||
else:
|
||||
from . import compat
|
||||
from . import version
|
||||
|
||||
import bpy
|
||||
|
||||
__all__ = ["compat", "version"]
|
||||
@@ -0,0 +1,144 @@
|
||||
"""
|
||||
API compatibility functions for handling differences between Blender versions.
|
||||
"""
|
||||
import bpy
|
||||
from bpy_extras import anim_utils
|
||||
from . import version
|
||||
|
||||
|
||||
def get_action_fcurves(action):
|
||||
"""
|
||||
Get the fcurves collection from an action, handling version differences.
|
||||
|
||||
In Blender 4.x: action.fcurves
|
||||
In Blender 5.0+: Uses channelbag.fcurves via action slots
|
||||
|
||||
Args:
|
||||
action: The action object
|
||||
|
||||
Returns:
|
||||
Collection of fcurves, or None if not available
|
||||
"""
|
||||
if version.is_version_at_least(5, 0, 0):
|
||||
# Blender 5.0+ uses channelbags and slots
|
||||
# Try to get fcurves from all slots in the action
|
||||
all_fcurves = []
|
||||
|
||||
# Iterate through all slots in the action
|
||||
if hasattr(action, 'slots') and action.slots:
|
||||
for slot in action.slots:
|
||||
try:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
action, slot)
|
||||
if channelbag and hasattr(channelbag, 'fcurves'):
|
||||
all_fcurves.extend(channelbag.fcurves)
|
||||
except (AttributeError, RuntimeError):
|
||||
continue
|
||||
|
||||
return all_fcurves if all_fcurves else None
|
||||
else:
|
||||
# Blender 4.x uses direct fcurves attribute
|
||||
if hasattr(action, 'fcurves'):
|
||||
return action.fcurves
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_fcurves_from_object(obj):
|
||||
"""
|
||||
Get FCurves from an object's animation data, handling version differences.
|
||||
|
||||
Args:
|
||||
obj: The object with animation data
|
||||
|
||||
Returns:
|
||||
List of (action, fcurve, slot) tuples, or empty list
|
||||
"""
|
||||
if not obj.animation_data or not obj.animation_data.action:
|
||||
return []
|
||||
|
||||
action = obj.animation_data.action
|
||||
fcurves_list = []
|
||||
|
||||
if version.is_version_at_least(5, 0, 0):
|
||||
# Blender 5.0+: Get the channelbag for the object's current action slot
|
||||
if hasattr(obj.animation_data, 'action_slot') and obj.animation_data.action_slot:
|
||||
try:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
action,
|
||||
obj.animation_data.action_slot
|
||||
)
|
||||
if channelbag and hasattr(channelbag, 'fcurves'):
|
||||
for fcurve in channelbag.fcurves:
|
||||
fcurves_list.append(
|
||||
(action, fcurve, obj.animation_data.action_slot))
|
||||
except (AttributeError, RuntimeError) as e:
|
||||
print(f"Error getting channelbag for {obj.name}: {e}")
|
||||
else:
|
||||
# Fallback: try all slots if no specific slot is set
|
||||
if hasattr(action, 'slots') and action.slots:
|
||||
for slot in action.slots:
|
||||
try:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
action, slot)
|
||||
if channelbag and hasattr(channelbag, 'fcurves'):
|
||||
for fcurve in channelbag.fcurves:
|
||||
fcurves_list.append((action, fcurve, slot))
|
||||
except (AttributeError, RuntimeError):
|
||||
continue
|
||||
else:
|
||||
# Blender 4.x: Direct access to fcurves
|
||||
if hasattr(action, 'fcurves'):
|
||||
for fcurve in action.fcurves:
|
||||
fcurves_list.append((action, fcurve, None))
|
||||
|
||||
return fcurves_list
|
||||
|
||||
|
||||
def remove_action_fcurve(action, fcurve, slot=None):
|
||||
"""
|
||||
Remove an fcurve from an action, handling version differences.
|
||||
|
||||
Args:
|
||||
action: The action object
|
||||
fcurve: The fcurve to remove
|
||||
slot: (Blender 5.0+) The action slot containing the fcurve
|
||||
|
||||
Returns:
|
||||
bool: True if removal succeeded, False otherwise
|
||||
"""
|
||||
if version.is_version_at_least(5, 0, 0):
|
||||
# Blender 5.0+: Need to remove from the channelbag
|
||||
if slot is None:
|
||||
# Try to find which slot contains this fcurve
|
||||
if hasattr(action, 'slots'):
|
||||
for try_slot in action.slots:
|
||||
try:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
action, try_slot)
|
||||
if channelbag and hasattr(channelbag, 'fcurves'):
|
||||
if fcurve in channelbag.fcurves:
|
||||
channelbag.fcurves.remove(fcurve)
|
||||
return True
|
||||
except (ValueError, AttributeError, RuntimeError):
|
||||
continue
|
||||
else:
|
||||
# We know which slot to use
|
||||
try:
|
||||
channelbag = anim_utils.action_get_channelbag_for_slot(
|
||||
action, slot)
|
||||
if channelbag and hasattr(channelbag, 'fcurves'):
|
||||
channelbag.fcurves.remove(fcurve)
|
||||
return True
|
||||
except (ValueError, AttributeError, RuntimeError) as e:
|
||||
print(f"Error removing fcurve: {e}")
|
||||
else:
|
||||
# Blender 4.x: Direct removal
|
||||
if hasattr(action, 'fcurves'):
|
||||
try:
|
||||
action.fcurves.remove(fcurve)
|
||||
return True
|
||||
except (ValueError, AttributeError):
|
||||
pass
|
||||
|
||||
return False
|
||||
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Version detection and comparison utilities for multi-version Blender support.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
def get_blender_version():
|
||||
"""
|
||||
Returns the current Blender version as a tuple (major, minor, patch).
|
||||
|
||||
Returns:
|
||||
tuple: (major, minor, patch) version numbers
|
||||
"""
|
||||
return bpy.app.version
|
||||
|
||||
|
||||
def get_version_string():
|
||||
"""
|
||||
Returns the current Blender version as a string (e.g., "4.2.0").
|
||||
|
||||
Returns:
|
||||
str: Version string in format "major.minor.patch"
|
||||
"""
|
||||
version = get_blender_version()
|
||||
return f"{version[0]}.{version[1]}.{version[2]}"
|
||||
|
||||
|
||||
def is_version_at_least(major, minor=0, patch=0):
|
||||
"""
|
||||
Check if the current Blender version is at least the specified version.
|
||||
|
||||
Args:
|
||||
major (int): Major version number
|
||||
minor (int): Minor version number (default: 0)
|
||||
patch (int): Patch version number (default: 0)
|
||||
|
||||
Returns:
|
||||
bool: True if current version >= specified version
|
||||
"""
|
||||
current = get_blender_version()
|
||||
target = (major, minor, patch)
|
||||
|
||||
if current[0] != target[0]:
|
||||
return current[0] > target[0]
|
||||
if current[1] != target[1]:
|
||||
return current[1] > target[1]
|
||||
return current[2] >= target[2]
|
||||
|
||||
|
||||
def is_version_less_than(major, minor=0, patch=0):
|
||||
"""
|
||||
Check if the current Blender version is less than the specified version.
|
||||
|
||||
Args:
|
||||
major (int): Major version number
|
||||
minor (int): Minor version number (default: 0)
|
||||
patch (int): Patch version number (default: 0)
|
||||
|
||||
Returns:
|
||||
bool: True if current version < specified version
|
||||
"""
|
||||
return not is_version_at_least(major, minor, patch)
|
||||
|
||||
|
||||
def is_version_5_0():
|
||||
"""Check if running Blender 5.0 or later."""
|
||||
return is_version_at_least(5, 0, 0)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user