Update for blender 5+ with retro-compatibility
- Added version check to keep addon compatible with previous version - Some feature are still broken (e.g: realign, broken since 4.3's gpv3) - note: DIR_PATH string property do not accept relative path since Blender 5.0. preferences brush_path,output_path and playblast_path had "//" by default. those props are turned to stnadard string for now
This commit is contained in:
parent
9fd3b4af83
commit
9d2644eb9f
@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
|
||||
5.0.0
|
||||
|
||||
- added: Update for blender 5+. With retro-compatibility
|
||||
|
||||
4.2.0
|
||||
|
||||
- added: Delete Grease pencil strokes or points view bound (selection outside of viewport region is untouched)
|
||||
|
||||
@ -3,6 +3,7 @@ import bpy
|
||||
import re
|
||||
from mathutils import Vector, Matrix
|
||||
from math import radians, degrees
|
||||
from . import utils
|
||||
|
||||
# exemple for future improve: https://justinsbarrett.com/tweenmachine/
|
||||
|
||||
@ -21,7 +22,7 @@ def get_surrounding_points(fc, frame):
|
||||
|
||||
return p_pt, n_pt
|
||||
|
||||
## unused direct breackdown func
|
||||
## unused direct breakdown func
|
||||
def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale'), axe=(0,1,2)):
|
||||
cf = bpy.context.scene.frame_current# use operator context (may be unsynced timeline)
|
||||
axes_name = ('x', 'y', 'z')
|
||||
@ -42,7 +43,7 @@ def breakdown_keys(percentage=50, channels=('location', 'rotation_euler', 'scale
|
||||
|
||||
skipping = []
|
||||
|
||||
for fc in action.fcurves:
|
||||
for fc in utils.get_fcurves(obj):
|
||||
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
|
||||
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
|
||||
if fc.data_path in channels and fc.array_index in axe:# .split('.')[-1]
|
||||
@ -252,7 +253,7 @@ class OBJ_OT_breakdown_obj_anim(bpy.types.Operator):
|
||||
|
||||
## TODO for ob in context.selected objects, need to reduce list with upper filters...
|
||||
|
||||
for fc in action.fcurves:
|
||||
for fc in utils.get_fcurves(obj):
|
||||
# if fc.data_path.split('"')[1] in bone_names_filter:# bones
|
||||
# if fc.data_path.split('.')[-1] in channels and fc.array_index in axe:# bones
|
||||
if fc.data_path in self.channels:# .split('.')[-1]# and fc.array_index in axe
|
||||
|
||||
@ -180,10 +180,14 @@ def selection_changed():
|
||||
## Note: Same owner as layer manager (will be removed as well)
|
||||
def subscribe_object_change():
|
||||
subscribe_to = (bpy.types.LayerObjects, 'active')
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
owner = bpy.types.GreasePencil
|
||||
else:
|
||||
owner = bpy.types.GreasePencilv3
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
# owner of msgbus subcribe (for clearing later)
|
||||
owner=bpy.types.GreasePencilv3, # <-- attach to ID during it's lifetime.
|
||||
owner=owner, # <-- attach to ID during it's lifetime.
|
||||
args=(),
|
||||
notify=selection_changed,
|
||||
options={'PERSISTENT'},
|
||||
@ -222,4 +226,4 @@ def unregister():
|
||||
if cursor_follow.__name__ in [hand.__name__ for hand in bpy.app.handlers.frame_change_post]:
|
||||
bpy.app.handlers.frame_change_post.remove(cursor_follow)
|
||||
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
|
||||
@ -20,7 +20,8 @@ def remove_stroke_exact_duplications(apply=True, verbose=True, select=False):
|
||||
ct = 0
|
||||
if verbose:
|
||||
print('\nRemove redundant strokes in GP frames...')
|
||||
gp_datas = [gp for gp in bpy.data.grease_pencils_v3]
|
||||
gp_source = bpy.data.grease_pencils if bpy.app.version >= (5,0,0) else bpy.data.grease_pencils_v3
|
||||
gp_datas = [gp for gp in gp_source]
|
||||
for gp in gp_datas:
|
||||
for l in gp.layers:
|
||||
for f in l.frames:
|
||||
@ -221,6 +222,8 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
o.use_grease_pencil_lights = False
|
||||
|
||||
## Disabled animation
|
||||
# TODO : fix for Blender 5.0
|
||||
if bpy.app.version < (5,0,0):
|
||||
if fix.list_disabled_anim:
|
||||
fcu_ct = 0
|
||||
for act in bpy.data.actions:
|
||||
@ -330,7 +333,7 @@ class GPTB_OT_file_checker(bpy.types.Operator):
|
||||
|
||||
# ## Set onion skin filter to 'All type'
|
||||
# fix_kf_type = 0
|
||||
# for gp in bpy.data.grease_pencils_v3:#from data
|
||||
# for gp in bpy.data.grease_pencils: #from data
|
||||
# if not gp.is_annotation:
|
||||
# if gp.onion_keyframe_type != 'ALL':
|
||||
# gp.onion_keyframe_type = 'ALL'
|
||||
|
||||
@ -421,12 +421,12 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
||||
self.selection = event.shift
|
||||
return self.execute(context)
|
||||
|
||||
def set_action_mute(self, act):
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
def set_action_mute(self, bag):
|
||||
for i, fcu in enumerate(bag.fcurves):
|
||||
print(i, fcu.data_path, fcu.array_index)
|
||||
# fcu.group don't have mute attribute in api.
|
||||
fcu.mute = self.mute
|
||||
for g in act.groups:
|
||||
for g in bag.groups:
|
||||
g.mute = self.mute
|
||||
|
||||
def execute(self, context):
|
||||
@ -443,22 +443,15 @@ class GPTB_OT_toggle_mute_animation(Operator):
|
||||
if self.mode == 'CAMERA' and o.type != 'CAMERA':
|
||||
continue
|
||||
|
||||
# mute attribute animation for GP and cameras
|
||||
## Mute attribute animation for GP and cameras
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA') and o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
if data_channelbag := utils.get_active_channelbag(o.data):
|
||||
print(f'\n---{o.name} data:')
|
||||
self.set_action_mute(gp_act)
|
||||
|
||||
if not o.animation_data:
|
||||
continue
|
||||
act = o.animation_data.action
|
||||
if not act:
|
||||
continue
|
||||
self.set_action_mute(data_channelbag)
|
||||
|
||||
if object_channelbag := utils.get_active_channelbag(o):
|
||||
print(f'\n---{o.name}:')
|
||||
self.set_action_mute(act)
|
||||
|
||||
self.set_action_mute(object_channelbag)
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
@ -495,7 +488,7 @@ class GPTB_OT_toggle_hide_gp_modifier(Operator):
|
||||
class GPTB_OT_list_disabled_anims(Operator):
|
||||
bl_idname = "gp.list_disabled_anims"
|
||||
bl_label = "List Disabled Anims"
|
||||
bl_description = "List disabled animations channels in scene. (shit+clic to list only on seleciton)"
|
||||
bl_description = "List disabled animations channels in scene. (shit+clic to list only on selection)"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
mute : bpy.props.BoolProperty(default=False)
|
||||
@ -521,27 +514,22 @@ class GPTB_OT_list_disabled_anims(Operator):
|
||||
# continue
|
||||
|
||||
if o.type == 'GREASEPENCIL':
|
||||
if o.data.animation_data:
|
||||
gp_act = o.data.animation_data.action
|
||||
if gp_act:
|
||||
for i, fcu in enumerate(gp_act.fcurves):
|
||||
if fcurves := utils.get_fcurves(o.data):
|
||||
for i, fcu in enumerate(fcurves):
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
oblist.append(o)
|
||||
li.append(f'{o.name}:')
|
||||
li.append(f' - {fcu.data_path} {fcu.array_index}')
|
||||
|
||||
if not o.animation_data:
|
||||
bag = utils.get_active_channelbag(o)
|
||||
if not bag:
|
||||
continue
|
||||
act = o.animation_data.action
|
||||
if not act:
|
||||
continue
|
||||
|
||||
for g in act.groups:
|
||||
for g in bag.groups:
|
||||
if g.mute:
|
||||
li.append(f'{o.name} - group: {g.name}')
|
||||
|
||||
for i, fcu in enumerate(act.fcurves):
|
||||
for i, fcu in enumerate(bag.fcurves):
|
||||
# print(i, fcu.data_path, fcu.array_index)
|
||||
if fcu.mute:
|
||||
if o not in oblist:
|
||||
|
||||
@ -624,11 +624,15 @@ def obj_layer_name_callback():
|
||||
|
||||
def subscribe_layer_change():
|
||||
subscribe_to = (bpy.types.GreasePencilv3Layers, "active")
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
owner = bpy.types.GreasePencil
|
||||
else:
|
||||
owner = bpy.types.GreasePencilv3
|
||||
bpy.msgbus.subscribe_rna(
|
||||
key=subscribe_to,
|
||||
# owner of msgbus subcribe (for clearing later)
|
||||
# owner=handle,
|
||||
owner=bpy.types.GreasePencilv3, # <-- can attach to an ID during all it's lifetime...
|
||||
owner=owner, # <-- can attach to an ID during all it's lifetime...
|
||||
# Args passed to callback function (tuple)
|
||||
args=(),
|
||||
# Callback function for property update
|
||||
@ -783,4 +787,4 @@ def unregister():
|
||||
|
||||
# Delete layer index trigger
|
||||
# /!\ can remove msgbus made for other functions or other addons using same owner
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencilv3)
|
||||
bpy.msgbus.clear_by_owner(bpy.types.GreasePencil)
|
||||
@ -165,9 +165,9 @@ class GPTB_OT_import_obj_palette(Operator):
|
||||
# self.report({'WARNING'}, 'All materials are already in other selected object')
|
||||
|
||||
# unlink objects and their gp data
|
||||
data_source = bpy.data.grease_pencils if bpy.app.version >= (5, 0, 0) else bpy.data.grease_pencils_v3
|
||||
for src_ob in linked_objs:
|
||||
bpy.data.grease_pencils_v3.remove(src_ob.data)
|
||||
|
||||
data_source.remove(src_ob.data)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
@ -158,10 +158,10 @@ def align_all_frames(reproject=True, ref=None, all_strokes=True):
|
||||
chanel = 'rotation_quaternion' if o.rotation_mode == 'QUATERNION' else 'rotation_euler'
|
||||
|
||||
## double list keys
|
||||
rot_keys = [int(k.co.x) for fcu in o.animation_data.action.fcurves for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||
rot_keys = [int(k.co.x) for fcu in utils.get_fcurves(o) for k in fcu.keyframe_points if fcu.data_path == chanel]
|
||||
|
||||
## normal iter
|
||||
# for fcu in o.animation_data.action.fcurves:
|
||||
# for fcu in utils.get_fcurves(o):
|
||||
# if fcu.data_path != chanel :
|
||||
# continue
|
||||
# for k in fcu.keyframe_points():
|
||||
@ -268,12 +268,12 @@ class GPTB_OT_realign(bpy.types.Operator):
|
||||
|
||||
self.alert = ''
|
||||
o = context.object
|
||||
if o.animation_data and o.animation_data.action:
|
||||
act = o.animation_data.action
|
||||
fcurves = utils.get_fcurves(o)
|
||||
if fcurves:
|
||||
for chan in ('rotation_euler', 'rotation_quaternion'):
|
||||
if act.fcurves.find(chan):
|
||||
if fcurves.find(chan):
|
||||
self.alert = 'Animated Rotation (CONSTANT interpolation)'
|
||||
interpos = [p for fcu in act.fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
|
||||
interpos = [p for fcu in fcurves if fcu.data_path == chan for p in fcu.keyframe_points if p.interpolation != 'CONSTANT']
|
||||
if interpos:
|
||||
self.alert = f'Animated Rotation ! ({len(interpos)} key not constant)'
|
||||
break
|
||||
@ -323,8 +323,8 @@ class GPTB_OT_realign(bpy.types.Operator):
|
||||
oframe = context.scene.frame_current
|
||||
|
||||
o = bpy.context.object
|
||||
if o.animation_data and o.animation_data.action:
|
||||
if o.animation_data.action.fcurves.find('rotation_euler') or o.animation_data.action.fcurves.find('rotation_quaternion'):
|
||||
if fcurves := utils.get_fcurves(o):
|
||||
if fcurves.find('rotation_euler') or fcurves.find('rotation_quaternion'):
|
||||
align_all_frames(reproject=self.reproject)
|
||||
print(f'\nAnim realign ({time()-t0:.2f}s)')
|
||||
self.exit(context, oframe)
|
||||
|
||||
15
UI_tools.py
15
UI_tools.py
@ -771,9 +771,15 @@ GPTB_PT_palettes_list_ui, # subpanels
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
if bpy.app.version >= (5,0,0):
|
||||
bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
# bpy.types.GREASE_PENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
else:
|
||||
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
|
||||
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.append(expose_use_channel_color_pref)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.append(expose_use_channel_color_pref)
|
||||
|
||||
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
|
||||
@ -786,10 +792,15 @@ def unregister():
|
||||
# bpy.types.VIEW3D_HT_header.remove(interpolate_header_ui) # WIP
|
||||
|
||||
bpy.types.DOPESHEET_PT_grease_pencil_mode.remove(expose_use_channel_color_pref)
|
||||
if bpy.app.version >= (5,0,0):
|
||||
bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
# bpy.types.GREASE_PENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
else:
|
||||
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
# bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
|
||||
|
||||
# bpy.types.DOPESHEET_PT_gpencil_layer_display.remove(expose_use_channel_color_pref)
|
||||
|
||||
|
||||
# if bpy.app.version >= (3,0,0):
|
||||
# bpy.types.ASSETBROWSER_PT_metadata.remove(asset_browser_ui)
|
||||
|
||||
64
__init__.py
64
__init__.py
@ -4,7 +4,7 @@ bl_info = {
|
||||
"name": "GP toolbox",
|
||||
"description": "Tool set for Grease Pencil in animation production",
|
||||
"author": "Samuel Bernou, Christophe Seux",
|
||||
"version": (4, 2, 0),
|
||||
"version": (5, 0, 0),
|
||||
"blender": (4, 3, 0),
|
||||
"location": "Sidebar (N menu) > Gpencil > Toolbox / Gpencil properties",
|
||||
"warning": "",
|
||||
@ -160,23 +160,6 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
## output settings for automated renders
|
||||
output_parent_level = IntProperty(
|
||||
name='Parent level',
|
||||
description="Go up in folder to define a render path relative to the file in upper directotys",
|
||||
default=0,
|
||||
min=0,
|
||||
max=20
|
||||
)
|
||||
|
||||
output_path : StringProperty(
|
||||
name="Output path",
|
||||
description="Path relative to blend to place render",
|
||||
default="//render", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
playblast_path : StringProperty(
|
||||
name="Playblast Path",
|
||||
description="Path to folder for playblasts output",
|
||||
default="//playblast", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
use_env_palettes : BoolProperty(
|
||||
name="Use Project Palettes",
|
||||
@ -206,11 +189,40 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
default=True,
|
||||
)
|
||||
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# DIR_PATH type do not accept relative ("//") since blender 5
|
||||
## using standard string
|
||||
brush_path : StringProperty(
|
||||
name="Brushes directory",
|
||||
description="Path to brushes containing the blends holding the brushes",
|
||||
default="//", maxlen=0)
|
||||
|
||||
output_path : StringProperty(
|
||||
name="Output path",
|
||||
description="Path relative to blend to place render",
|
||||
default="//render", maxlen=0)
|
||||
|
||||
playblast_path : StringProperty(
|
||||
name="Playblast Path",
|
||||
description="Path to folder for playblasts output",
|
||||
default="//playblast", maxlen=0)
|
||||
|
||||
else:
|
||||
brush_path : StringProperty(
|
||||
name="Brushes directory",
|
||||
description="Path to brushes containing the blends holding the brushes",
|
||||
default="//", maxlen=0, subtype='DIR_PATH')#, update = set_palette_path
|
||||
|
||||
output_path : StringProperty(
|
||||
name="Output path",
|
||||
description="Path relative to blend to place render",
|
||||
default="//render", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
playblast_path : StringProperty(
|
||||
name="Playblast Path",
|
||||
description="Path to folder for playblasts output",
|
||||
default="//playblast", maxlen=0, subtype='DIR_PATH')
|
||||
|
||||
## namespace
|
||||
separator : StringProperty(
|
||||
name="Separator",
|
||||
@ -279,7 +291,7 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
)
|
||||
|
||||
## KF jumper
|
||||
kfj_use_shortcut: BoolProperty(
|
||||
kfj_use_shortcut : BoolProperty(
|
||||
name = "Use Keyframe Jump Shortcut",
|
||||
description = "Auto bind shotcut for keyframe jump (else you can bind manually using 'screen.gp_keyframe_jump' id_name)",
|
||||
default = True)
|
||||
@ -289,17 +301,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
description="Shortcut to trigger previous keyframe jump",
|
||||
default="F5")
|
||||
|
||||
kfj_prev_shift: BoolProperty(
|
||||
kfj_prev_shift : BoolProperty(
|
||||
name = "Shift",
|
||||
description = "add shift",
|
||||
default = False)
|
||||
|
||||
kfj_prev_alt: BoolProperty(
|
||||
kfj_prev_alt : BoolProperty(
|
||||
name = "Alt",
|
||||
description = "add alt",
|
||||
default = False)
|
||||
|
||||
kfj_prev_ctrl: BoolProperty(
|
||||
kfj_prev_ctrl : BoolProperty(
|
||||
name = "combine with ctrl",
|
||||
description = "add ctrl",
|
||||
default = False)
|
||||
@ -309,17 +321,17 @@ class GPTB_prefs(bpy.types.AddonPreferences):
|
||||
description="Shortcut to trigger keyframe jump",
|
||||
default="F6")
|
||||
|
||||
kfj_next_shift: BoolProperty(
|
||||
kfj_next_shift : BoolProperty(
|
||||
name = "Shift",
|
||||
description = "add shift",
|
||||
default = False)
|
||||
|
||||
kfj_next_alt: BoolProperty(
|
||||
kfj_next_alt : BoolProperty(
|
||||
name = "Alt",
|
||||
description = "add alt",
|
||||
default = False)
|
||||
|
||||
kfj_next_ctrl: BoolProperty(
|
||||
kfj_next_ctrl : BoolProperty(
|
||||
name = "combine with ctrl",
|
||||
description = "add ctrl",
|
||||
default = False)
|
||||
@ -704,8 +716,6 @@ def set_namespace_env(name_env, prop_group):
|
||||
n.is_project = n.tag in project_pfix
|
||||
|
||||
def set_env_properties():
|
||||
|
||||
|
||||
prefs = get_addon_prefs()
|
||||
|
||||
fps = os.getenv('FPS')
|
||||
|
||||
37
utils.py
37
utils.py
@ -1485,6 +1485,30 @@ def has_fully_enabled_anim(o):
|
||||
return False
|
||||
return True
|
||||
|
||||
def get_fcurves(obj):
|
||||
'''return f-curves of object's active action'''
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# action slot
|
||||
if obj.animation_data and obj.animation_data.action_slot:
|
||||
active_slot = obj.animation_data.action_slot
|
||||
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot).fcurves
|
||||
else:
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
return obj.animation_data.action.fcurves
|
||||
return []
|
||||
|
||||
def get_active_channelbag(obj):
|
||||
'''return active channelbag in blender 5, else action'''
|
||||
if bpy.app.version >= (5, 0, 0):
|
||||
# action slot
|
||||
if obj.animation_data and obj.animation_data.action_slot:
|
||||
active_slot = obj.animation_data.action_slot
|
||||
return active_slot.id_data.layers[0].strips[0].channelbag(active_slot)
|
||||
else:
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
return obj.animation_data.action
|
||||
|
||||
|
||||
def anim_status(objects) -> tuple((str, str)):
|
||||
'''Return a tutple of icon string status in ('ALL_ON', 'MIXED', 'ALL_OFF', 'NONE')'''
|
||||
|
||||
@ -1504,8 +1528,9 @@ def anim_status(objects) -> tuple((str, str)):
|
||||
# off_count += 1
|
||||
|
||||
### Consider All channels individually
|
||||
if o.animation_data and o.animation_data.action:
|
||||
for grp in o.animation_data.action.groups:
|
||||
|
||||
if channelbag := get_active_channelbag(o):
|
||||
for grp in channelbag.groups:
|
||||
## Check if groups are muted
|
||||
if grp.mute:
|
||||
off_count += 1
|
||||
@ -1514,7 +1539,7 @@ def anim_status(objects) -> tuple((str, str)):
|
||||
count += 1
|
||||
|
||||
|
||||
for fcu in o.animation_data.action.fcurves:
|
||||
for fcu in channelbag.fcurves:
|
||||
## Check if fcurves are muted
|
||||
if fcu.mute:
|
||||
off_count += 1
|
||||
@ -1524,12 +1549,8 @@ def anim_status(objects) -> tuple((str, str)):
|
||||
|
||||
if o.type in ('GREASEPENCIL', 'CAMERA'):
|
||||
datablock = o.data
|
||||
if datablock.animation_data is None:
|
||||
continue
|
||||
if not datablock.animation_data.action:
|
||||
continue
|
||||
## Check if object data attributes fcurves are muted
|
||||
for fcu in datablock.animation_data.action.fcurves:
|
||||
for fcu in get_fcurves(datablock):
|
||||
if fcu.mute:
|
||||
off_count += 1
|
||||
else:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user