2026-02-16

This commit is contained in:
2026-03-17 15:25:32 -06:00
parent d5dd373de0
commit 60100fbab2
560 changed files with 33397 additions and 20776 deletions
@@ -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
@@ -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
@@ -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)