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:
Pullusb 2025-12-15 23:57:57 +01:00
parent 9fd3b4af83
commit 9d2644eb9f
11 changed files with 153 additions and 107 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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,17 +222,19 @@ class GPTB_OT_file_checker(bpy.types.Operator):
o.use_grease_pencil_lights = False
## Disabled animation
if fix.list_disabled_anim:
fcu_ct = 0
for act in bpy.data.actions:
if not act.users:
continue
for fcu in act.fcurves:
if fcu.mute:
fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
# 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:
if not act.users:
continue
for fcu in act.fcurves:
if fcu.mute:
fcu_ct += 1
print(f"muted: {act.name} > {fcu.data_path}")
if fcu_ct:
problems.append(f'{fcu_ct} anim channel disabled (details in console)')
## Object visibility conflict
if fix.list_obj_vis_conflict:
@ -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'

View File

@ -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
print(f'\n---{o.name}:')
self.set_action_mute(act)
self.set_action_mute(data_channelbag)
if object_channelbag := utils.get_active_channelbag(o):
print(f'\n---{o.name}:')
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 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 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:

View File

@ -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)

View File

@ -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"}

View File

@ -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)

View File

@ -771,9 +771,15 @@ GPTB_PT_palettes_list_ui, # subpanels
def register():
for cls in classes:
bpy.utils.register_class(cls)
bpy.types.GPENCIL_MT_material_context_menu.append(palette_manager_menu)
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.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_gpencil_layer_display.append(expose_use_channel_color_pref)
# bpy.types.VIEW3D_HT_header.append(interpolate_header_ui) # WIP
@ -786,9 +792,14 @@ 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)
bpy.types.GPENCIL_MT_material_context_menu.remove(palette_manager_menu)
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.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)
# if bpy.app.version >= (3,0,0):

View File

@ -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,10 +189,39 @@ class GPTB_prefs(bpy.types.AddonPreferences):
default=True,
)
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
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(
@ -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')

View File

@ -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: