update addons (for 5.2)
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
# Claude Code
|
||||
.claude/
|
||||
|
||||
# Python cache
|
||||
__pycache__/
|
||||
*.pyc
|
||||
|
||||
# Non-addon folders
|
||||
diagnostics/
|
||||
diagnostics.zip
|
||||
animation_layers_updater/
|
||||
|
||||
# Experimental, not shipped
|
||||
ultra_bake.py
|
||||
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "Animation Layers",
|
||||
"author": "Tal Hershkovich",
|
||||
"version" : (2, 4, 1),
|
||||
"version" : (2, 4, 2),
|
||||
"blender" : (3, 2, 0),
|
||||
"location": "View3D - Properties - Animation Panel",
|
||||
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
|
||||
@@ -110,7 +110,9 @@ class AnimLayersSettings(bpy.types.PropertyGroup):
|
||||
upper_stack : bpy.props.BoolProperty(name="Upper Stack Evaluation", description="Checks if tweak mode uses upper stack", default=False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
#Tools
|
||||
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = 0, soft_max = 1, default=0.5, options = set(), override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.add_inbetween_key)
|
||||
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = 0.0, soft_max = 1.0, default=0.5, options = set(), override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.add_inbetween_key, get = anim_layers.get_inbetweener, set = anim_layers.set_inbetweener)
|
||||
inbetweener_push : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes, further values push past the neighbor keyframes", soft_min = -0.5, soft_max = 1.5, default=0.5, options = set(), override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.add_inbetween_key, get = anim_layers.get_inbetweener, set = anim_layers.set_inbetweener)
|
||||
push: bpy.props.BoolProperty(name ='Toggle Push', description = 'Changing the min and max values further for pushing', default = False)
|
||||
share_layer_keys: bpy.props.EnumProperty(name = 'Share Layer Keys', description="Share keyframes positions between layers", items = anim_layers.share_layerkeys_items, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
influence_hide: bpy.props.BoolProperty(name="Hide Influence", description="Hide Influence Fcurves", default=False, update = anim_layers.influence_hide_keyframes, override = {'LIBRARY_OVERRIDABLE'})
|
||||
@@ -145,6 +147,12 @@ class AnimLayersItems(bpy.types.PropertyGroup):
|
||||
assigned_group: bpy.props.EnumProperty(name='Parent', description='Set this layer\'s parent (None = root)',
|
||||
items=anim_layers.layer_group_enum_items, get=anim_layers.layer_group_get, set=anim_layers.layer_group_set,
|
||||
options={'HIDDEN'})
|
||||
# Set once per file-load/turn-on by anim_layers.tag_unusable_layers: True on
|
||||
# rows that come from a linked-library source file and have no matching NLA
|
||||
# track locally. Such rows can't be removed here (the library owns them), so
|
||||
# the row<->track index mapping skips them until the layer is deleted in the
|
||||
# rig source file.
|
||||
not_usable: bpy.props.BoolProperty(name="Not Usable", description="Layer stored in the linked source file without a matching NLA track. Remove the layer in the rig source file to get rid of it", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# Legacy field for pre-migration data. Read by migrate_object_to_hierarchical
|
||||
# and ignored thereafter. Schema retained so old .blend files load cleanly.
|
||||
type: bpy.props.EnumProperty(name="Item Type (legacy)", default='LAYER',
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2026-06-22 15:11:23.788257",
|
||||
"backup_date": "May-27-2026",
|
||||
"last_check": "2026-07-14 14:42:39.577911",
|
||||
"backup_date": "July-14-2026",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "Animation Layers",
|
||||
"author": "Tal Hershkovich",
|
||||
"version" : (2, 4, 0),
|
||||
"version" : (2, 4, 1),
|
||||
"blender" : (3, 2, 0),
|
||||
"location": "View3D - Properties - Animation Panel",
|
||||
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
|
||||
@@ -73,7 +73,10 @@ class AnimLayersSceneSettings(bpy.types.PropertyGroup):
|
||||
|
||||
class AnimLayersSettings(bpy.types.PropertyGroup):
|
||||
turn_on: bpy.props.BoolProperty(name="Turn Animation Layers On", description="Turn on and start Animation Layers", default=False, options={'HIDDEN'}, update = anim_layers.turn_animlayers_on, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# Active row in obj.Anim_Layers. Post-migration, 1:1 with NLA-track index.
|
||||
layer_index: bpy.props.IntProperty(update = anim_layers.update_layer_index, options={'LIBRARY_EDITABLE'}, default = 0, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# Schema version. 0 = pre-hierarchical (legacy GROUP rows); 1 = hierarchical (parent_layer refs).
|
||||
schema_version: bpy.props.IntProperty(name="Schema Version", default=0, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
linked: bpy.props.BoolProperty(name="Linked", description="Duplicate a layer with a linked action", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
#Bake settings
|
||||
@@ -125,13 +128,31 @@ class AnimLayersItems(bpy.types.PropertyGroup):
|
||||
|
||||
action_range: bpy.props.FloatVectorProperty(name='action range', description="used to check if layer needs to update frame range", override = {'LIBRARY_OVERRIDABLE'}, size = 2)
|
||||
custom_frame_range: bpy.props.BoolProperty(name="Custom Frame Range", description="Use a custom frame range per layer instead of the scene frame range", default=False, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_range)
|
||||
|
||||
|
||||
frame_start: bpy.props.FloatProperty(name='Action Start Frame', description="First frame of the layer's action",min = 0, default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_start)
|
||||
frame_end: bpy.props.FloatProperty(name='Action End Frame', description="End frame of the layer's action", default=0, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_frame_end)
|
||||
speed: bpy.props.FloatProperty(name='Speed of the action', description="Speed of the action strip", default = 1, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_speed)
|
||||
offset: bpy.props.FloatProperty(name='Offset when the action starts', description="Offseting the whole layer animation", default = 0, precision = 2, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_offset)
|
||||
repeat: bpy.props.FloatProperty(name="Repeat", description="Repeat the action", min = 0.1, default = 1, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'}, update = anim_layers.layer_repeat)
|
||||
|
||||
# Hierarchical layer fields. Every layer is NLA-backed; a layer is a
|
||||
# "group" iff something else points to it via parent_layer.
|
||||
expanded: bpy.props.BoolProperty(name="Expanded", description="Show this layer's children in the UI list", default=True, options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
parent_layer: bpy.props.StringProperty(name="Parent Layer", description="Name of this layer's parent (empty = root)", default="", options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
group_color: bpy.props.FloatVectorProperty(name="Group Color", subtype='COLOR', size=4, min=0.0, max=1.0, default=(0.3, 0.5, 0.8, 1.0), options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# Dynamic-enum dropdown for picking parent_layer in the UI. The setter
|
||||
# refuses cycles (self + descendants are excluded from the options).
|
||||
assigned_group: bpy.props.EnumProperty(name='Parent', description='Set this layer\'s parent (None = root)',
|
||||
items=anim_layers.layer_group_enum_items, get=anim_layers.layer_group_get, set=anim_layers.layer_group_set,
|
||||
options={'HIDDEN'})
|
||||
# Legacy field for pre-migration data. Read by migrate_object_to_hierarchical
|
||||
# and ignored thereafter. Schema retained so old .blend files load cleanly.
|
||||
type: bpy.props.EnumProperty(name="Item Type (legacy)", default='LAYER',
|
||||
items=[('LAYER', 'Layer', 'NLA-backed layer'),
|
||||
('GROUP', 'Group', 'Legacy phantom group row')],
|
||||
options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
parent_group: bpy.props.StringProperty(name="Parent Group (legacy)", description="Pre-migration field. Read once by the schema-v1 migration, then unused.", default="", options={'HIDDEN'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
|
||||
class AnimLayersObjects(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+4
-4
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"last_check": "2026-05-27 14:41:11.263249",
|
||||
"backup_date": "April-21-2026",
|
||||
"last_check": "2026-07-14 14:42:39.577911",
|
||||
"backup_date": "May-27-2026",
|
||||
"update_ready": true,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
"just_updated": false,
|
||||
"version_text": {
|
||||
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=321d411a449bc9acee2a759e30cd3d0f36bbd2ab",
|
||||
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=e32318861bdc35e0b02381eb2119f4492051027d",
|
||||
"version": [
|
||||
2,
|
||||
4,
|
||||
1
|
||||
2
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -180,10 +180,11 @@ def smart_bake(context):
|
||||
fcurves = anim_layers.get_fcurves(obj, track.strips[0].action)
|
||||
total_iterations += len(fcurves)
|
||||
|
||||
wm.progress_begin(0, total_iterations)
|
||||
wm.progress_begin(0, total_iterations)
|
||||
processed = 0
|
||||
|
||||
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if track.mute:
|
||||
continue
|
||||
if len(track.strips) != 1 or track.strips[0].action is None:
|
||||
@@ -242,9 +243,9 @@ def smart_bake(context):
|
||||
smartkeys = smart_start_end(smartkeys, strip.frame_start, strip.frame_end)
|
||||
smartkeys = remove_outofrange_keys(smartkeys, strip.frame_start, strip.frame_end)
|
||||
|
||||
#if the strip is cutting with a different strip, then add keyframes in the cut
|
||||
#if the strip is cutting with a different strip, then add keyframes in the cut
|
||||
for layercut in obj.Anim_Layers:
|
||||
if layercut.mute or not layercut.custom_frame_range or layercut == layer:
|
||||
if layercut.type == 'GROUP' or layercut.mute or not layercut.custom_frame_range or layercut == layer:
|
||||
continue
|
||||
if strip_start < layercut.frame_start < strip_end:
|
||||
smartkeys = smart_start_end(smartkeys, (layercut.frame_start-1), strip.frame_end-1)
|
||||
@@ -407,7 +408,7 @@ def unmute_modifiers(obj, nla_tracks, modifier_rec):
|
||||
for mod in fcu.modifiers:
|
||||
if mod in modifier_rec:
|
||||
mod.mute = False
|
||||
elif obj.als.mergefcurves and track == nla_tracks[obj.als.layer_index]:
|
||||
elif obj.als.mergefcurves and track == nla_tracks[anim_layers.nla_idx(obj)]:
|
||||
mod.mute = True
|
||||
|
||||
def invisible_layers(b_layers):
|
||||
@@ -426,7 +427,14 @@ def select_keyframed_bones(self, context, obj):
|
||||
if obj.mode != 'POSE':
|
||||
bpy.ops.object.posemode_toggle()
|
||||
bpy.ops.pose.select_all(action='DESELECT')
|
||||
for i in range(0, obj.als.layer_index+1):
|
||||
# Iterate over LAYER rows up to (and including) the active row, skipping
|
||||
# group headers (they have no NLA track and no bones to select).
|
||||
current_row = obj.als.layer_index
|
||||
for i, it in enumerate(obj.Anim_Layers):
|
||||
if i > current_row:
|
||||
break
|
||||
if it.type != 'LAYER':
|
||||
continue
|
||||
obj.als['layer_index'] = i
|
||||
anim_layers.select_layer_bones(self, context)
|
||||
|
||||
@@ -446,7 +454,7 @@ def smartbake_apply(obj, nla_tracks, fcu_keys, extrapolations):
|
||||
#apply smartbake for blenders bake
|
||||
#smart bake - delete unnecessery keyframes:
|
||||
# transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
|
||||
strip = nla_tracks[obj.als.layer_index].strips[0]
|
||||
strip = nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
# if strip.action is None:
|
||||
# return
|
||||
|
||||
@@ -641,7 +649,7 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
|
||||
return
|
||||
anim_data = anim_layers.anim_data_type(obj)
|
||||
# baked_action = anim_data.action
|
||||
track = nla_tracks[obj.als.layer_index]
|
||||
track = nla_tracks[anim_layers.nla_idx(obj)]
|
||||
baked_action = track.strips[0].action
|
||||
clean_no_user_slots(baked_action)
|
||||
#create the baked fcurve
|
||||
@@ -1065,10 +1073,18 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
bl_idname = "anim.layers_merge_down"
|
||||
bl_label = "Merge_Layers_Down"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
|
||||
step: bpy.props.IntProperty(name='Step', description='Bake every number of frame steps', default=1)
|
||||
actioncopy: bpy.props.BoolProperty(name='Copy original merged action', description='Create a copy of the original action that is being overwritten', default = False)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
obj = context.object
|
||||
if obj is None:
|
||||
return False
|
||||
# Disable when active row is a group header — merge only applies to NLA-backed layers.
|
||||
return anim_layers.is_layer_row_active(obj)
|
||||
|
||||
def invoke(self, context, event):
|
||||
obj = context.object
|
||||
bake_range_type(context.scene.als, context)
|
||||
@@ -1143,7 +1159,8 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
|
||||
# Incase the strips are shorter then the keyframe range (because scene is shorter)
|
||||
# Then updating the strips length
|
||||
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if layer.custom_frame_range:
|
||||
continue
|
||||
if len(track.strips) != 1:
|
||||
@@ -1181,7 +1198,7 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
if obj.als.direction == 'DOWN':
|
||||
obj.als.layer_index = 0
|
||||
baked_layer = None
|
||||
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
|
||||
strip = anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
action = strip.action
|
||||
if hasattr(strip, 'action_slot'):
|
||||
action_slot = strip.action_slot
|
||||
@@ -1190,21 +1207,29 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
#if baking to a new layer then setup the new index and layer
|
||||
elif obj.als.operator == 'NEW':
|
||||
self.actioncopy = False
|
||||
# `add_at_nla` is the NLA-track index passed to add_animlayer.
|
||||
if obj.als.direction == 'UP' and additive and 'REPLACE' in blendings:
|
||||
obj.als.layer_index = layer_index + blendings.index('REPLACE') - 1
|
||||
add_at_nla = layer_index + blendings.index('REPLACE') - 1
|
||||
elif obj.als.direction == 'UP' or obj.als.direction == 'ALL':
|
||||
obj.als.layer_index = len(obj.Anim_Layers)-1
|
||||
add_at_nla = anim_layers.nla_layer_count(obj) - 1
|
||||
else:
|
||||
add_at_nla = anim_layers.nla_idx(obj)
|
||||
|
||||
layer_names = [layer.name for layer in obj.Anim_Layers]
|
||||
baked_layer = anim_layers.add_animlayer(layer_name = anim_layers.unique_name(layer_names, 'Baked_Layer') , duplicate = False, index = obj.als.layer_index, blend_type = blend)
|
||||
layer_names = [layer.name for layer in obj.Anim_Layers if layer.type == 'LAYER']
|
||||
baked_layer = anim_layers.add_animlayer(layer_name = anim_layers.unique_name(layer_names, 'Baked_Layer') , duplicate = False, index = add_at_nla, blend_type = blend)
|
||||
anim_layers.register_layers(obj, nla_tracks)
|
||||
|
||||
obj.als.layer_index += 1
|
||||
|
||||
# Point layer_index at the newly-added baked layer's collection row.
|
||||
if baked_layer is not None:
|
||||
for ridx, it in enumerate(obj.Anim_Layers):
|
||||
if it.type == 'LAYER' and it.name == baked_layer.name:
|
||||
obj.als.layer_index = ridx
|
||||
break
|
||||
|
||||
#remove subsciption again after adding a layer there was new subsciption applied
|
||||
subscriptions.subscriptions_remove()
|
||||
|
||||
track = nla_tracks[obj.als.layer_index]
|
||||
track = nla_tracks[anim_layers.nla_idx(obj)]
|
||||
#use internal bake
|
||||
if obj.als.baketype =='NLA':
|
||||
modifier_rec, extrapolations = mute_modifiers(obj, nla_tracks)
|
||||
@@ -1282,7 +1307,7 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
strip.action_slot = anim_layers.get_obj_slot(obj, action)
|
||||
|
||||
#reset layer settings
|
||||
baked_layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
baked_layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
baked_layer.repeat, baked_layer.speed, baked_layer.offset = 1, 1, 0
|
||||
strip.use_sync_length = False
|
||||
if baked_layer.custom_frame_range:
|
||||
|
||||
@@ -106,7 +106,10 @@ def animlayers_frame(scene, context):
|
||||
if len(track.strips) != 1:
|
||||
continue
|
||||
#checks if the layer has a custom frame range
|
||||
layer = obj.Anim_Layers[i]
|
||||
row_idx = anim_layers.layer_to_row_index(obj, i)
|
||||
if row_idx < 0 or row_idx >= len(obj.Anim_Layers):
|
||||
continue
|
||||
layer = obj.Anim_Layers[row_idx]
|
||||
if layer.custom_frame_range:
|
||||
continue
|
||||
if not reset_subscription:
|
||||
@@ -169,6 +172,10 @@ def check_handler(scene):
|
||||
return
|
||||
anim_layers.add_obj_to_animlayers(obj, [item.object for item in scene.AL_objects])
|
||||
nla_tracks = anim_data.nla_tracks
|
||||
# When the active UIList row is a group header (no NLA track of its own),
|
||||
# skip the LAYER-specific syncs below — they assume a real layer.
|
||||
if not anim_layers.is_layer_row_active(obj):
|
||||
return
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
active_action_update(obj, anim_data, nla_tracks)
|
||||
#check if a keyframe was removed
|
||||
@@ -189,7 +196,7 @@ def check_handler(scene):
|
||||
if track_layer_synchronization(obj, nla_tracks):
|
||||
return
|
||||
|
||||
track = nla_tracks[obj.als.layer_index]
|
||||
track = nla_tracks[anim_layers.nla_idx(obj)]
|
||||
|
||||
sync_frame_range(scene, track, layer)
|
||||
# sync_strip_range(scene)
|
||||
@@ -217,18 +224,20 @@ def check_handler(scene):
|
||||
anim_layers.hide_view_all_keyframes(obj, anim_data)
|
||||
check_selected_bones(obj)
|
||||
|
||||
influence_check(nla_tracks[obj.als.layer_index])
|
||||
influence_check(nla_tracks[anim_layers.nla_idx(obj)])
|
||||
|
||||
def track_layer_synchronization(obj, nla_tracks):
|
||||
'''check if track and layers are synchronized, running only when adding/removing tracks via the nla'''
|
||||
|
||||
if len(nla_tracks) == len(obj.Anim_Layers):
|
||||
if len(nla_tracks) == anim_layers.nla_layer_count(obj):
|
||||
return False
|
||||
|
||||
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in obj.Anim_Layers))
|
||||
|
||||
layer_items = [layer for layer in obj.Anim_Layers if layer.type == 'LAYER']
|
||||
new_layers_names = set(track.name for track in nla_tracks).difference(set(layer.name for layer in layer_items))
|
||||
anim_layers.visible_layers(obj, nla_tracks)
|
||||
if obj.als.layer_index > len(obj.Anim_Layers)-1:
|
||||
obj.als.layer_index = len(obj.Anim_Layers)-1
|
||||
row_count = len(obj.Anim_Layers)
|
||||
if row_count and obj.als.layer_index > row_count - 1:
|
||||
obj.als.layer_index = row_count - 1
|
||||
|
||||
if not bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
|
||||
return
|
||||
@@ -261,9 +270,9 @@ def active_action_update(obj, anim_data, nla_tracks):
|
||||
anim_data.action = None
|
||||
subscriptions_add(bpy.context.scene)
|
||||
return
|
||||
if anim_data.action == nla_tracks[obj.als.layer_index].strips[0].action:
|
||||
if anim_data.action == nla_tracks[anim_layers.nla_idx(obj)].strips[0].action:
|
||||
return
|
||||
if not len(nla_tracks[obj.als.layer_index].strips):
|
||||
if not len(nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
return
|
||||
if not anim_data.action or anim_data.is_property_readonly('action'):
|
||||
return
|
||||
@@ -434,7 +443,7 @@ def influence_sync(scene, obj, nla_tracks):
|
||||
if action.name == scene.name + 'Action' and not len(scene.animation_data.nla_tracks) and not len(fcurves):
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
strip = nla_tracks[obj.als.layer_index].strips[0]
|
||||
strip = nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
if strip.fcurves[0].mute:
|
||||
return
|
||||
strip.fcurves[0].lock = False
|
||||
@@ -546,9 +555,10 @@ def frameend_update_callback():
|
||||
for anim_data in anim_datas:
|
||||
if anim_data is None:
|
||||
continue
|
||||
if len(anim_data.nla_tracks) != len(obj.Anim_Layers):
|
||||
if len(anim_data.nla_tracks) != anim_layers.nla_layer_count(obj):
|
||||
continue
|
||||
for layer, track in zip(obj.Anim_Layers, anim_data.nla_tracks):
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if layer.custom_frame_range:
|
||||
continue
|
||||
if len(track.strips) != 1:
|
||||
@@ -653,10 +663,12 @@ def action_name_callback():
|
||||
nla_tracks = anim_data.nla_tracks
|
||||
if not len(nla_tracks):
|
||||
return
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
if not len(nla_tracks[obj.als.layer_index].strips):
|
||||
if not anim_layers.is_layer_row_active(obj):
|
||||
return
|
||||
action = nla_tracks[obj.als.layer_index].strips[0].action
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
if not len(nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
return
|
||||
action = nla_tracks[anim_layers.nla_idx(obj)].strips[0].action
|
||||
if action is None:
|
||||
return
|
||||
if not obj.als.auto_rename or layer.name == action.name:
|
||||
@@ -774,10 +786,10 @@ def slot_update_callback():
|
||||
if not len(obj.Anim_Layers):
|
||||
return
|
||||
|
||||
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
|
||||
if not len(anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
return
|
||||
|
||||
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
|
||||
strip = anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
anim_data.action_slot = strip.action_slot
|
||||
|
||||
|
||||
@@ -848,9 +860,11 @@ def strip_settings_callback():
|
||||
return
|
||||
|
||||
# sync_strip_range()
|
||||
if not len(anim_data.nla_tracks[obj.als.layer_index].strips):
|
||||
if not anim_layers.is_layer_row_active(obj):
|
||||
return
|
||||
strip = anim_data.nla_tracks[obj.als.layer_index].strips[0]
|
||||
if not len(anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
return
|
||||
strip = anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
|
||||
update_strip_layer_settings(strip, layer)
|
||||
|
||||
BIN
Binary file not shown.
@@ -183,7 +183,7 @@ def smart_bake(context):
|
||||
wm.progress_begin(0, total_iterations)
|
||||
processed = 0
|
||||
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER' and not l.not_usable]
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if track.mute:
|
||||
continue
|
||||
@@ -433,7 +433,7 @@ def select_keyframed_bones(self, context, obj):
|
||||
for i, it in enumerate(obj.Anim_Layers):
|
||||
if i > current_row:
|
||||
break
|
||||
if it.type != 'LAYER':
|
||||
if it.type != 'LAYER' or it.not_usable:
|
||||
continue
|
||||
obj.als['layer_index'] = i
|
||||
anim_layers.select_layer_bones(self, context)
|
||||
@@ -1141,12 +1141,17 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
return {'CANCELLED'}
|
||||
anim_data = anim_layers.anim_data_type(obj)
|
||||
nla_tracks = anim_data.nla_tracks
|
||||
|
||||
if obj.als.direction == 'DOWN' and not obj.als.layer_index:
|
||||
|
||||
# Track-space index of the active layer (skips not_usable rows).
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1:
|
||||
return {'CANCELLED'}
|
||||
|
||||
if obj.als.direction == 'DOWN' and not nla_index:
|
||||
return {'CANCELLED'}
|
||||
|
||||
#disable baking up from Blender's bake
|
||||
if obj.als.direction == 'UP' and obj.als.layer_index == len(nla_tracks)-1:
|
||||
if obj.als.direction == 'UP' and nla_index == len(nla_tracks)-1:
|
||||
return {'CANCELLED'}
|
||||
|
||||
if obj.als.baketype == 'NLA':
|
||||
@@ -1159,7 +1164,7 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
|
||||
# Incase the strips are shorter then the keyframe range (because scene is shorter)
|
||||
# Then updating the strips length
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER' and not l.not_usable]
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if layer.custom_frame_range:
|
||||
continue
|
||||
@@ -1175,7 +1180,7 @@ class MergeAnimLayerDown(bpy.types.Operator):
|
||||
if context.scene.frame_current > frame_end:
|
||||
context.scene.frame_current = frame_end
|
||||
|
||||
layer_index = obj.als.layer_index
|
||||
layer_index = nla_index
|
||||
blendings = [track.strips[0].blend_type for track in nla_tracks[layer_index:] if len(track.strips) == 1]
|
||||
|
||||
#define if the new baked layer is going to be additive or replace
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
schema_version = "1.0.0"
|
||||
|
||||
# Example of manifest file for a Blender extension
|
||||
# Change the values according to your extension
|
||||
id = "animation_layers"
|
||||
version = "2.4.1"
|
||||
name = "Animation Layers"
|
||||
tagline = "Simplifying the NLA editor into an animation layers UI and workflow"
|
||||
maintainer = "Tal Hershkovich <dancingshapes@gmail.com>"
|
||||
# Supported types: "add-on", "theme"
|
||||
type = "add-on"
|
||||
|
||||
# # Optional: link to documentation, support, source files, etc
|
||||
website = "https://blendermarket.com/products/animation-layers/docs"
|
||||
|
||||
# Tag list defined by Blender and server, see:
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/tags.html
|
||||
tags = ["Animation"]
|
||||
|
||||
blender_version_min = "4.2.0"
|
||||
# # Optional: Blender version that the extension does not support, earlier versions are supported.
|
||||
# # This can be omitted and defined later on the extensions platform if an issue is found.
|
||||
# blender_version_max = "5.1.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",
|
||||
]
|
||||
# # Optional: required by some licenses.
|
||||
# copyright = [
|
||||
# "2002-2024 Developer Name",
|
||||
# "1998 Company Name",
|
||||
# ]
|
||||
|
||||
# # Optional: list of supported platforms. If omitted, the extension will be available in all operating systems.
|
||||
# platforms = ["windows-x64", "macos-arm64", "linux-x64"]
|
||||
# # Other supported platforms: "windows-arm64", "macos-x64"
|
||||
|
||||
# # Optional: bundle 3rd party Python modules.
|
||||
# # https://docs.blender.org/manual/en/dev/advanced/extensions/python_wheels.html
|
||||
# wheels = [
|
||||
# "./wheels/hexdump-3.3-py3-none-any.whl",
|
||||
# "./wheels/jsmin-3.0.1-py3-none-any.whl",
|
||||
# ]
|
||||
|
||||
# # Optional: add-ons can list which resources they will require:
|
||||
# # * files (for access of any filesystem operations)
|
||||
# # * network (for internet access)
|
||||
# # * clipboard (to read and/or write the system clipboard)
|
||||
# # * camera (to capture photos and videos)
|
||||
# # * microphone (to capture audio)
|
||||
# #
|
||||
# # If using network, remember to also check `bpy.app.online_access`
|
||||
# # https://docs.blender.org/manual/en/dev/advanced/extensions/addons.html#internet-access
|
||||
# #
|
||||
# # For each permission it is important to also specify the reason why it is required.
|
||||
# # Keep this a single short sentence without a period (.) at the end.
|
||||
# # For longer explanations use the documentation or detail page.
|
||||
#
|
||||
# [permissions]
|
||||
# network = "Need to sync motion-capture data to server"
|
||||
# files = "Import/export FBX from/to disk"
|
||||
# clipboard = "Copy and paste bone transforms"
|
||||
|
||||
# Build settings — exclude the bundled addon-updater (Blender's extension system
|
||||
# handles updates), runtime caches, and the updater's own backup folder.
|
||||
# https://docs.blender.org/manual/en/dev/advanced/extensions/command_line_arguments.html#command-line-args-extension-build
|
||||
[build]
|
||||
paths_exclude_pattern = [
|
||||
"__pycache__/",
|
||||
"/.git/",
|
||||
"/*.zip",
|
||||
"/animation_layers_updater/",
|
||||
]
|
||||
@@ -196,7 +196,10 @@ def check_handler(scene):
|
||||
if track_layer_synchronization(obj, nla_tracks):
|
||||
return
|
||||
|
||||
track = nla_tracks[anim_layers.nla_idx(obj)]
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1:
|
||||
return
|
||||
track = nla_tracks[nla_index]
|
||||
|
||||
sync_frame_range(scene, track, layer)
|
||||
# sync_strip_range(scene)
|
||||
@@ -223,8 +226,8 @@ def check_handler(scene):
|
||||
if obj.als.view_all_keyframes:
|
||||
anim_layers.hide_view_all_keyframes(obj, anim_data)
|
||||
check_selected_bones(obj)
|
||||
|
||||
influence_check(nla_tracks[anim_layers.nla_idx(obj)])
|
||||
|
||||
influence_check(track)
|
||||
|
||||
def track_layer_synchronization(obj, nla_tracks):
|
||||
'''check if track and layers are synchronized, running only when adding/removing tracks via the nla'''
|
||||
@@ -270,9 +273,10 @@ def active_action_update(obj, anim_data, nla_tracks):
|
||||
anim_data.action = None
|
||||
subscriptions_add(bpy.context.scene)
|
||||
return
|
||||
if anim_data.action == nla_tracks[anim_layers.nla_idx(obj)].strips[0].action:
|
||||
nla_idx = anim_layers.nla_idx(obj)
|
||||
if nla_idx == -1 or not len(nla_tracks[nla_idx].strips):
|
||||
return
|
||||
if not len(nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
if anim_data.action == nla_tracks[nla_idx].strips[0].action:
|
||||
return
|
||||
if not anim_data.action or anim_data.is_property_readonly('action'):
|
||||
return
|
||||
@@ -443,7 +447,10 @@ def influence_sync(scene, obj, nla_tracks):
|
||||
if action.name == scene.name + 'Action' and not len(scene.animation_data.nla_tracks) and not len(fcurves):
|
||||
bpy.data.actions.remove(action)
|
||||
|
||||
strip = nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1 or not len(nla_tracks[nla_index].strips):
|
||||
return
|
||||
strip = nla_tracks[nla_index].strips[0]
|
||||
if strip.fcurves[0].mute:
|
||||
return
|
||||
strip.fcurves[0].lock = False
|
||||
@@ -557,7 +564,7 @@ def frameend_update_callback():
|
||||
continue
|
||||
if len(anim_data.nla_tracks) != anim_layers.nla_layer_count(obj):
|
||||
continue
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER']
|
||||
layer_items = [l for l in obj.Anim_Layers if l.type == 'LAYER' and not l.not_usable]
|
||||
for layer, track in zip(layer_items, anim_data.nla_tracks):
|
||||
if layer.custom_frame_range:
|
||||
continue
|
||||
@@ -617,15 +624,22 @@ def track_update_callback():
|
||||
if not len(nla_tracks):# or len(nla_tracks[:-1]) != len(obj.Anim_Layers):
|
||||
return
|
||||
override_tracks = anim_layers.check_override_tracks(obj, anim_data)
|
||||
# Pair each track with its layer row, skipping not_usable rows (layers
|
||||
# from a linked source file without a track) so names don't shift onto
|
||||
# the wrong row.
|
||||
layer_rows = [it for it in obj.Anim_Layers if not it.not_usable]
|
||||
for i, track in enumerate(nla_tracks):
|
||||
if anim_data != current_anim_data:
|
||||
continue
|
||||
if i >= len(layer_rows):
|
||||
break
|
||||
layer = layer_rows[i]
|
||||
#make sure there are no duplicated names
|
||||
if track.name != obj.Anim_Layers[i].name:
|
||||
if track.name != layer.name:
|
||||
#If its an override track, then make sure the reference object name is also synchronized
|
||||
if obj.Anim_Layers[i].name in override_tracks:
|
||||
override_tracks[obj.Anim_Layers[i].name].name = track.name
|
||||
obj.Anim_Layers[i].name = track.name
|
||||
if layer.name in override_tracks:
|
||||
override_tracks[layer.name].name = track.name
|
||||
layer.name = track.name
|
||||
if len(track.strips) == 1:
|
||||
track.strips[0].name = track.name
|
||||
|
||||
@@ -666,9 +680,10 @@ def action_name_callback():
|
||||
if not anim_layers.is_layer_row_active(obj):
|
||||
return
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
if not len(nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1 or not len(nla_tracks[nla_index].strips):
|
||||
return
|
||||
action = nla_tracks[anim_layers.nla_idx(obj)].strips[0].action
|
||||
action = nla_tracks[nla_index].strips[0].action
|
||||
if action is None:
|
||||
return
|
||||
if not obj.als.auto_rename or layer.name == action.name:
|
||||
@@ -709,7 +724,9 @@ def influence_update_callback():
|
||||
return
|
||||
if not len(anim_data.nla_tracks):
|
||||
return
|
||||
i = obj.als.layer_index
|
||||
i = anim_layers.nla_idx(obj)
|
||||
if i == -1:
|
||||
return
|
||||
track = anim_data.nla_tracks[i]
|
||||
if len(track.strips) != 1:
|
||||
return
|
||||
@@ -786,10 +803,11 @@ def slot_update_callback():
|
||||
if not len(obj.Anim_Layers):
|
||||
return
|
||||
|
||||
if not len(anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1 or not len(anim_data.nla_tracks[nla_index].strips):
|
||||
return
|
||||
|
||||
strip = anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
strip = anim_data.nla_tracks[nla_index].strips[0]
|
||||
anim_data.action_slot = strip.action_slot
|
||||
|
||||
|
||||
@@ -862,9 +880,10 @@ def strip_settings_callback():
|
||||
# sync_strip_range()
|
||||
if not anim_layers.is_layer_row_active(obj):
|
||||
return
|
||||
if not len(anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips):
|
||||
nla_index = anim_layers.nla_idx(obj)
|
||||
if nla_index == -1 or not len(anim_data.nla_tracks[nla_index].strips):
|
||||
return
|
||||
strip = anim_data.nla_tracks[anim_layers.nla_idx(obj)].strips[0]
|
||||
strip = anim_data.nla_tracks[nla_index].strips[0]
|
||||
layer = obj.Anim_Layers[obj.als.layer_index]
|
||||
|
||||
update_strip_layer_settings(strip, layer)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,20 @@
|
||||
|
||||
All notable changes to **Maya Config Pro** are listed here. Versions match `blender_manifest.toml`.
|
||||
|
||||
## [1.8.3] — 2026-07-14
|
||||
|
||||
### Fixed
|
||||
- **Keymaps:** Fixed crash on blend file load from stale addon keymap RNA handles; deferred `load_post` re-sync, skip modal maps in the addon layer, use Blender’s keymap import for nested operator props.
|
||||
|
||||
### Changed
|
||||
- **Blender support:** Added **5.2 LTS**; minimum target is now **4.5 LTS** (4.2–4.4 dropped).
|
||||
- **Marking menu:** Loop/ring multi-select uses Blender 5.1+ operators; paint-mode submenu limited to mesh objects.
|
||||
|
||||
## [1.8.2] — 2026-07-08
|
||||
|
||||
### Fixed
|
||||
- **Quad Menu:** Space pie ortho views (Left/Right/Top/etc.) now affect only the active 3D viewport, not every viewport in multi-view layouts.
|
||||
|
||||
## [1.8.1] — 2026-06-16
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Maya Config Pro — Install the release ZIP (Blender 4.2+)
|
||||
Maya Config Pro — Install the release ZIP (Blender 4.5+)
|
||||
========================================================
|
||||
|
||||
This package is a Blender *extension* (blender_manifest.toml). Do not unzip
|
||||
|
||||
@@ -75,14 +75,14 @@ Maya Config Pro transforms Blender's interface and workflow to match Maya's fami
|
||||
This is the new modern way to install Maya Config Pro - no file copying required!
|
||||
|
||||
#### Requirements
|
||||
- Blender 4.2 LTS or newer
|
||||
- Blender 4.5 LTS or newer
|
||||
|
||||
#### Steps
|
||||
|
||||
1. **Download the Extension**
|
||||
- Download the MayaConfigPro ZIP file from Gumroad or your purchase location
|
||||
|
||||
2. **Install in Blender** (Blender 4.2+; extension with `blender_manifest.toml`)
|
||||
2. **Install in Blender** (Blender 4.5+; extension with `blender_manifest.toml`)
|
||||
- Open Blender
|
||||
- Go to **`Edit → Preferences → Get Extensions`**
|
||||
- Use the **top-right `⋯` menu → `Install from Disk`**, *or* **drag the ZIP onto the Preferences window**
|
||||
@@ -242,13 +242,13 @@ For older Blender versions (pre-4.2) or if you prefer the original mod approach:
|
||||
|
||||
| Blender Version | Support Level | Recommended Install |
|
||||
|-----------------|---------------|---------------------|
|
||||
| 4.2.x LTS | Full | Extension |
|
||||
| 4.3.x | Full | Extension |
|
||||
| 4.4.x | Full | Extension |
|
||||
| 4.5.x LTS | Full | Extension |
|
||||
| 5.0.x | Full | Extension |
|
||||
| 3.0.x - 4.1.x | Legacy Only | Mod Installation |
|
||||
| 2.83 - 2.93 | Legacy Only | Mod Installation |
|
||||
| 5.1.x | Full | Extension |
|
||||
| 5.2.x LTS | Full | Extension |
|
||||
| 4.2.x – 4.4.x | Unsupported | — |
|
||||
| 3.0.x – 4.1.x | Legacy Only | Mod Installation |
|
||||
| 2.83 – 2.93 | Legacy Only | Mod Installation |
|
||||
|
||||
---
|
||||
|
||||
@@ -262,7 +262,7 @@ For older Blender versions (pre-4.2) or if you prefer the original mod approach:
|
||||
|
||||
### Version 1.6 / Extension 1.7.0
|
||||
- **Extension Format:** Complete port to modern Blender Extension platform
|
||||
- **Multi-Version Support:** Compatible with Blender 4.2, 4.5 LTS, and 5.0+
|
||||
- **Multi-Version Support:** Compatible with Blender 4.5 LTS, 5.0, 5.1, and 5.2 LTS
|
||||
- **No File Copying:** Everything applies at runtime, no installation directory modifications
|
||||
- **Safe Registration:** Error handling for all version-specific API differences
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Maya Config Pro - Blender Extension
|
||||
A Maya-like configuration, shortcuts, and UI elements for Blender.
|
||||
|
||||
Version: 1.8.1
|
||||
Version: 1.8.3
|
||||
Author: Form Affinity (Jesse Doyle)
|
||||
"""
|
||||
|
||||
@@ -12,8 +12,8 @@ Author: Form Affinity (Jesse Doyle)
|
||||
bl_info = {
|
||||
"name": "Maya Config Pro",
|
||||
"author": "Form Affinity (Jesse Doyle)",
|
||||
"version": (1, 8, 1),
|
||||
"blender": (4, 2, 0),
|
||||
"version": (1, 8, 3),
|
||||
"blender": (4, 5, 0),
|
||||
"location": "View3D > Sidebar > Maya Config",
|
||||
"description": "Maya-like configuration, shortcuts, UI panels, and keymap presets",
|
||||
"warning": "",
|
||||
|
||||
@@ -3,12 +3,12 @@ schema_version = "1.0.0"
|
||||
id = "form_affinity_maya_config_pro"
|
||||
name = "Maya Config Pro"
|
||||
tagline = "Maya-like configuration, shortcuts, and UI elements for Blender"
|
||||
version = "1.8.1"
|
||||
version = "1.8.3"
|
||||
type = "add-on"
|
||||
|
||||
maintainer = "Form Affinity"
|
||||
license = ["GPL-3.0-or-later"]
|
||||
blender_version_min = "4.2.0"
|
||||
blender_version_min = "4.5.0"
|
||||
|
||||
website = "https://github.com/FormAffinity/MayaConfigPro"
|
||||
|
||||
|
||||
@@ -9,14 +9,14 @@ that layer without restarting.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
from bl_keymap_utils.io import keymap_init_from_data
|
||||
from ..utils import compat
|
||||
from ..utils import keymap_fix
|
||||
|
||||
# Import the keyconfig data
|
||||
from . import fa_hotkeys_data
|
||||
|
||||
# Addon keymaps: (keymap, keymap_item) and created keymaps to remove for clean sync
|
||||
addon_keymaps: list = []
|
||||
# Addon keymaps we created (for unregister while handles are still valid)
|
||||
addon_keymap_objects: list = []
|
||||
|
||||
# Set from __init__.register to match this add-on's module id
|
||||
@@ -24,6 +24,41 @@ _addon_id: str = "form_affinity_maya_config_pro"
|
||||
|
||||
# Message bus: react to keyconfig changes from Preferences
|
||||
_msgbus_owner = object()
|
||||
_load_post_timer = None
|
||||
|
||||
|
||||
def _clear_keymap_tracking() -> None:
|
||||
addon_keymap_objects.clear()
|
||||
|
||||
|
||||
def _keymap_signature(km_name: str, km_args: dict) -> tuple:
|
||||
return (
|
||||
km_name,
|
||||
km_args.get("space_type", "EMPTY"),
|
||||
km_args.get("region_type", "WINDOW"),
|
||||
bool(km_args.get("modal", False)),
|
||||
)
|
||||
|
||||
|
||||
def _purge_tracked_keymaps(kc) -> None:
|
||||
"""Remove MCP addon keymaps without stale RNA handles (safe after file load)."""
|
||||
if not kc or not hasattr(fa_hotkeys_data, "keyconfig_data"):
|
||||
_clear_keymap_tracking()
|
||||
return
|
||||
|
||||
signatures = {
|
||||
_keymap_signature(name, args)
|
||||
for name, args, _ in fa_hotkeys_data.keyconfig_data
|
||||
if not args.get("modal", False)
|
||||
}
|
||||
for km in list(kc.keymaps):
|
||||
sig = (km.name, km.space_type, km.region_type, km.is_modal)
|
||||
if sig in signatures:
|
||||
try:
|
||||
kc.keymaps.remove(km)
|
||||
except Exception:
|
||||
pass
|
||||
_clear_keymap_tracking()
|
||||
|
||||
|
||||
def set_addon_id(addon_id: str) -> None:
|
||||
@@ -62,97 +97,88 @@ def register_keymaps():
|
||||
if not kc:
|
||||
return
|
||||
|
||||
try:
|
||||
if hasattr(fa_hotkeys_data, "keyconfig_data"):
|
||||
for km_name, km_args, km_content in fa_hotkeys_data.keyconfig_data:
|
||||
km = kc.keymaps.new(name=km_name, **km_args)
|
||||
addon_keymap_objects.append(km)
|
||||
if not hasattr(fa_hotkeys_data, "keyconfig_data"):
|
||||
return
|
||||
|
||||
if "items" in km_content:
|
||||
for item in km_content["items"]:
|
||||
if len(item) >= 2:
|
||||
idname = item[0]
|
||||
keymap_item = item[1]
|
||||
props = item[2] if len(item) > 2 else None
|
||||
|
||||
kmi = km.keymap_items.new(
|
||||
idname,
|
||||
type=keymap_item.get("type", "A"),
|
||||
value=keymap_item.get("value", "PRESS"),
|
||||
any=keymap_item.get("any", False),
|
||||
shift=keymap_item.get("shift", 0),
|
||||
ctrl=keymap_item.get("ctrl", 0),
|
||||
alt=keymap_item.get("alt", 0),
|
||||
oskey=keymap_item.get("oskey", False),
|
||||
key_modifier=keymap_item.get("key_modifier", "NONE"),
|
||||
repeat=keymap_item.get("repeat", False),
|
||||
head=keymap_item.get("head", False),
|
||||
)
|
||||
|
||||
if props and "properties" in props:
|
||||
for prop_name, prop_value in props["properties"]:
|
||||
if hasattr(kmi.properties, prop_name):
|
||||
setattr(kmi.properties, prop_name, prop_value)
|
||||
|
||||
if props and "active" in props:
|
||||
kmi.active = props["active"]
|
||||
|
||||
addon_keymaps.append((km, kmi))
|
||||
except Exception as e:
|
||||
print(f"Maya Config Pro: Could not register keymaps: {e}")
|
||||
for km_name, km_args, km_content in fa_hotkeys_data.keyconfig_data:
|
||||
if km_args.get("modal", False):
|
||||
continue
|
||||
try:
|
||||
km = kc.keymaps.new(name=km_name, **km_args)
|
||||
addon_keymap_objects.append(km)
|
||||
keymap_init_from_data(km, km_content["items"], is_modal=False)
|
||||
except Exception as e:
|
||||
print(f"Maya Config Pro: Could not register keymap '{km_name}': {e}")
|
||||
|
||||
|
||||
def unregister_keymaps():
|
||||
def unregister_keymaps(*, drop_stale: bool = False) -> None:
|
||||
"""Remove all MCP addon keymap items and the keymap objects we created."""
|
||||
global addon_keymap_objects
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
|
||||
if not kc:
|
||||
_clear_keymap_tracking()
|
||||
return
|
||||
|
||||
for km, kmi in list(addon_keymaps):
|
||||
try:
|
||||
km.keymap_items.remove(kmi)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
addon_keymaps.clear()
|
||||
if drop_stale or not addon_keymap_objects:
|
||||
_purge_tracked_keymaps(kc)
|
||||
return
|
||||
|
||||
removed_any = False
|
||||
for km in reversed(addon_keymap_objects):
|
||||
try:
|
||||
kc.keymaps.remove(km)
|
||||
removed_any = True
|
||||
except Exception:
|
||||
pass
|
||||
addon_keymap_objects.clear()
|
||||
|
||||
if not removed_any:
|
||||
_purge_tracked_keymaps(kc)
|
||||
else:
|
||||
_clear_keymap_tracking()
|
||||
|
||||
|
||||
def sync_addon_keymaps() -> None:
|
||||
def sync_addon_keymaps(*, after_file_load: bool = False) -> None:
|
||||
"""
|
||||
If "Auto-load FA Keymap" is on and the active preset is `fa_hotkeys`, register
|
||||
the addon keymap layer; otherwise remove it. Safe to call often (switcher, prefs, msgbus).
|
||||
"""
|
||||
prefs = _get_prefs()
|
||||
if not prefs or not getattr(prefs, "auto_load_keymap", False):
|
||||
unregister_keymaps()
|
||||
unregister_keymaps(drop_stale=after_file_load)
|
||||
return
|
||||
if is_fa_keyconfig_active():
|
||||
unregister_keymaps()
|
||||
unregister_keymaps(drop_stale=after_file_load)
|
||||
register_keymaps()
|
||||
try:
|
||||
keymap_fix.patch_node_editor_alt_rmb("FA_HOTKEYS")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
unregister_keymaps()
|
||||
unregister_keymaps(drop_stale=after_file_load)
|
||||
|
||||
|
||||
@bpy.app.handlers.persistent
|
||||
def _mcp_load_post_sync(_a, _b) -> None:
|
||||
try:
|
||||
sync_addon_keymaps()
|
||||
except Exception as e:
|
||||
print(f"Warning: Maya Config Pro: keymap sync after load: {e}")
|
||||
global _load_post_timer
|
||||
# File load invalidates stored keymap RNA handles — never touch them here.
|
||||
_clear_keymap_tracking()
|
||||
if _load_post_timer is not None:
|
||||
try:
|
||||
bpy.app.timers.unregister(_load_post_timer)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _sync_after_load():
|
||||
global _load_post_timer
|
||||
_load_post_timer = None
|
||||
try:
|
||||
sync_addon_keymaps(after_file_load=True)
|
||||
except Exception as e:
|
||||
print(f"Warning: Maya Config Pro: keymap sync after load: {e}")
|
||||
return None
|
||||
|
||||
_load_post_timer = bpy.app.timers.register(_sync_after_load, first_interval=0.0)
|
||||
|
||||
|
||||
def _msgbus_keyconfig(_):
|
||||
@@ -179,6 +205,13 @@ def register_keymap_listeners() -> None:
|
||||
|
||||
|
||||
def unregister_keymap_listeners() -> None:
|
||||
global _load_post_timer
|
||||
if _load_post_timer is not None:
|
||||
try:
|
||||
bpy.app.timers.unregister(_load_post_timer)
|
||||
except Exception:
|
||||
pass
|
||||
_load_post_timer = None
|
||||
try:
|
||||
bpy.msgbus.clear_by_owner(_msgbus_owner)
|
||||
except Exception:
|
||||
|
||||
@@ -8,6 +8,13 @@ from ..utils import compat
|
||||
from bpy.types import Menu
|
||||
|
||||
|
||||
def _draw_multi_loop_select(layout, *, ring: bool, icon: str) -> None:
|
||||
op_id = compat.mesh_multi_loop_select_op(ring=ring)
|
||||
op = layout.operator(op_id, text="", icon=icon)
|
||||
if op and op_id == "mesh.loop_multi_select":
|
||||
op.ring = ring
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Sub Menus
|
||||
# ------------------------------------------------------------------------
|
||||
@@ -46,10 +53,14 @@ class MCP_MT_SubMode(Menu):
|
||||
layout = self.layout
|
||||
layout.label(text="Mode:")
|
||||
layout.separator()
|
||||
layout.operator("object.mode_set", text="Sculpt", icon="SCULPTMODE_HLT").mode='SCULPT'
|
||||
layout.operator("object.mode_set", text="Vertex Paint", icon="VPAINT_HLT").mode='VERTEX_PAINT'
|
||||
layout.operator("object.mode_set", text="Weight Paint", icon="WPAINT_HLT").mode='WEIGHT_PAINT'
|
||||
layout.operator("object.mode_set", text="Texture Paint", icon="TPAINT_HLT").mode='TEXTURE_PAINT'
|
||||
obj = context.object
|
||||
if obj is None or obj.type != "MESH":
|
||||
layout.label(text="Requires a mesh object")
|
||||
return
|
||||
layout.operator("object.mode_set", text="Sculpt", icon="SCULPTMODE_HLT").mode = "SCULPT"
|
||||
layout.operator("object.mode_set", text="Vertex Paint", icon="VPAINT_HLT").mode = "VERTEX_PAINT"
|
||||
layout.operator("object.mode_set", text="Weight Paint", icon="WPAINT_HLT").mode = "WEIGHT_PAINT"
|
||||
layout.operator("object.mode_set", text="Texture Paint", icon="TPAINT_HLT").mode = "TEXTURE_PAINT"
|
||||
|
||||
|
||||
class MCP_MT_SubFreeze(Menu):
|
||||
@@ -130,12 +141,8 @@ class MCP_MT_MarkingMenu(Menu):
|
||||
op = other_menu.operator("mesh.select_all", text="", icon="SELECT_SUBTRACT")
|
||||
if op:
|
||||
op.action = "DESELECT"
|
||||
op = other_menu.operator("mesh.loop_multi_select", text="", icon="ANCHOR_LEFT")
|
||||
if op:
|
||||
op.ring = False
|
||||
op = other_menu.operator("mesh.loop_multi_select", text="", icon="ANCHOR_TOP")
|
||||
if op:
|
||||
op.ring = True
|
||||
_draw_multi_loop_select(other_menu, ring=False, icon="ANCHOR_LEFT")
|
||||
_draw_multi_loop_select(other_menu, ring=True, icon="ANCHOR_TOP")
|
||||
other_menu.operator("mesh.select_more", text="", icon="ADD")
|
||||
other_menu.operator("mesh.select_less", text="", icon="REMOVE")
|
||||
|
||||
|
||||
@@ -8,6 +8,37 @@ from ..utils import compat
|
||||
from bpy.types import Menu
|
||||
|
||||
|
||||
def _set_active_viewport_ortho_axis(context, axis: str):
|
||||
"""Apply an axis view in orthographic mode to the invoking 3D viewport only."""
|
||||
area = context.area
|
||||
if area is None or area.type != "VIEW_3D":
|
||||
return {"CANCELLED"}
|
||||
|
||||
space = context.space_data
|
||||
if space is None or space.type != "VIEW_3D":
|
||||
space = area.spaces.active
|
||||
if space is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
region = context.region
|
||||
if region is None:
|
||||
region = next((r for r in area.regions if r.type == "WINDOW"), None)
|
||||
if region is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
with context.temp_override(
|
||||
window=context.window,
|
||||
screen=context.screen,
|
||||
area=area,
|
||||
region=region,
|
||||
space_data=space,
|
||||
):
|
||||
bpy.ops.view3d.view_axis(type=axis)
|
||||
|
||||
space.region_3d.view_perspective = "ORTHO"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
# Main Quad Menu
|
||||
# ------------------------------------------------------------------------
|
||||
@@ -56,13 +87,7 @@ class MCP_OT_LeftOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='LEFT')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "LEFT")
|
||||
|
||||
|
||||
class MCP_OT_RightOrtho(bpy.types.Operator):
|
||||
@@ -72,13 +97,7 @@ class MCP_OT_RightOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='RIGHT')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "RIGHT")
|
||||
|
||||
|
||||
class MCP_OT_BottomOrtho(bpy.types.Operator):
|
||||
@@ -88,13 +107,7 @@ class MCP_OT_BottomOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='BOTTOM')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "BOTTOM")
|
||||
|
||||
|
||||
class MCP_OT_TopOrtho(bpy.types.Operator):
|
||||
@@ -104,13 +117,7 @@ class MCP_OT_TopOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='TOP')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "TOP")
|
||||
|
||||
|
||||
class MCP_OT_FrontOrtho(bpy.types.Operator):
|
||||
@@ -120,13 +127,7 @@ class MCP_OT_FrontOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='FRONT')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "FRONT")
|
||||
|
||||
|
||||
class MCP_OT_BackOrtho(bpy.types.Operator):
|
||||
@@ -136,13 +137,7 @@ class MCP_OT_BackOrtho(bpy.types.Operator):
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
bpy.ops.view3d.view_axis(type='BACK')
|
||||
C = bpy.context
|
||||
for i, a in enumerate(C.screen.areas):
|
||||
if a.type == "VIEW_3D":
|
||||
space = a.spaces.active
|
||||
space.region_3d.view_perspective = 'ORTHO'
|
||||
return {'FINISHED'}
|
||||
return _set_active_viewport_ortho_axis(context, "BACK")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
This module provides API compatibility functions for handling differences
|
||||
between Blender 4.2, 4.4, 4.5, and 5.0.
|
||||
between supported Blender versions (4.5 LTS through 5.2 LTS).
|
||||
"""
|
||||
|
||||
import bpy
|
||||
@@ -50,9 +50,9 @@ def get_tools_region_type():
|
||||
In Blender 2.8+, 'TOOLS' was replaced by 'UI'.
|
||||
|
||||
Returns:
|
||||
str: 'UI' (for all supported versions 4.2+)
|
||||
str: 'UI' (for all supported versions 4.5+)
|
||||
"""
|
||||
# For Blender 4.2 and later, always use 'UI'
|
||||
# For Blender 4.5 and later, always use 'UI'
|
||||
return 'UI'
|
||||
|
||||
|
||||
@@ -72,8 +72,16 @@ def temp_override(context, **kwargs):
|
||||
# Blender 3.2+ uses temp_override
|
||||
return context.temp_override(**kwargs)
|
||||
else:
|
||||
# Older versions - this shouldn't happen for 4.2+ minimum
|
||||
# Older versions - this shouldn't happen for 4.5+ minimum
|
||||
# but keeping for safety
|
||||
override = context.copy()
|
||||
override.update(kwargs)
|
||||
return override
|
||||
|
||||
|
||||
def mesh_multi_loop_select_op(*, ring: bool) -> str:
|
||||
"""Operator id for loop (ring=False) or ring (ring=True) multi-select."""
|
||||
if version.is_version_at_least(5, 1, 0):
|
||||
return "mesh.select_edge_ring_multi" if ring else "mesh.select_edge_loop_multi"
|
||||
return "mesh.loop_multi_select"
|
||||
|
||||
|
||||
@@ -1,53 +1,32 @@
|
||||
"""
|
||||
This module provides version detection and comparison utilities for
|
||||
multi-version Blender support (4.2, 4.4, 4.5, and 5.0).
|
||||
Version detection and comparison utilities for Blender 4.5 LTS through 5.2 LTS.
|
||||
"""
|
||||
|
||||
import bpy
|
||||
|
||||
# Version constants
|
||||
VERSION_4_2 = (4, 2, 0)
|
||||
VERSION_4_4 = (4, 4, 0)
|
||||
VERSION_4_5 = (4, 5, 0)
|
||||
VERSION_5_0 = (5, 0, 0)
|
||||
VERSION_5_1 = (5, 1, 0)
|
||||
VERSION_5_2 = (5, 2, 0)
|
||||
|
||||
|
||||
def get_blender_version():
|
||||
"""
|
||||
Returns the current Blender version as a tuple (major, minor, patch).
|
||||
|
||||
Returns:
|
||||
tuple: (major, minor, patch) version numbers
|
||||
"""
|
||||
"""Return the current Blender version as (major, minor, patch)."""
|
||||
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"
|
||||
"""
|
||||
"""Return the current Blender version as '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
|
||||
"""
|
||||
"""True when the running Blender version is >= major.minor.patch."""
|
||||
current = get_blender_version()
|
||||
target = (major, minor, patch)
|
||||
|
||||
|
||||
if current[0] != target[0]:
|
||||
return current[0] > target[0]
|
||||
if current[1] != target[1]:
|
||||
@@ -56,61 +35,55 @@ def is_version_at_least(major, minor=0, patch=0):
|
||||
|
||||
|
||||
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
|
||||
"""
|
||||
"""True when the running Blender version is < major.minor.patch."""
|
||||
return not is_version_at_least(major, minor, patch)
|
||||
|
||||
|
||||
def get_version_category():
|
||||
"""
|
||||
Returns the version category string for the current Blender version.
|
||||
|
||||
Returns:
|
||||
str: '4.2', '4.4', '4.5', or '5.0' based on the current version
|
||||
Return a coarse support bucket for the running Blender version.
|
||||
|
||||
Supported targets: 4.5, 5.0, 5.1, 5.2.
|
||||
"""
|
||||
version = get_blender_version()
|
||||
major, minor = version[0], version[1]
|
||||
|
||||
|
||||
if major == 4:
|
||||
if minor < 4:
|
||||
return '4.2'
|
||||
elif minor < 5:
|
||||
return '4.4'
|
||||
else:
|
||||
return '4.5'
|
||||
elif major >= 5:
|
||||
return '5.0'
|
||||
else:
|
||||
# Fallback for older versions
|
||||
return f"{major}.{minor}"
|
||||
|
||||
|
||||
def is_version_4_2():
|
||||
"""Check if running Blender 4.2 (4.2.x only, not 4.3 or 4.4)."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 4 and version[1] == 2
|
||||
if minor < 5:
|
||||
return "4.4"
|
||||
return "4.5"
|
||||
if major >= 5:
|
||||
if minor < 1:
|
||||
return "5.0"
|
||||
if minor < 2:
|
||||
return "5.1"
|
||||
return "5.2"
|
||||
return f"{major}.{minor}"
|
||||
|
||||
|
||||
def is_version_4_4():
|
||||
"""Check if running Blender 4.4 (4.4.x only, not 4.5)."""
|
||||
"""True on Blender 4.4.x."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 4 and version[1] == 4
|
||||
|
||||
|
||||
def is_version_4_5():
|
||||
"""Check if running Blender 4.5 LTS."""
|
||||
"""True on Blender 4.5 LTS."""
|
||||
return is_version_at_least(4, 5, 0) and is_version_less_than(5, 0, 0)
|
||||
|
||||
|
||||
def is_version_5_0():
|
||||
"""Check if running Blender 5.0 or later."""
|
||||
return is_version_at_least(5, 0, 0)
|
||||
"""True on Blender 5.0.x."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 5 and version[1] == 0
|
||||
|
||||
|
||||
def is_version_5_1():
|
||||
"""True on Blender 5.1.x."""
|
||||
version = get_blender_version()
|
||||
return version[0] == 5 and version[1] == 1
|
||||
|
||||
|
||||
def is_version_5_2():
|
||||
"""True on Blender 5.2 LTS and newer 5.x."""
|
||||
return is_version_at_least(5, 2, 0)
|
||||
|
||||
Reference in New Issue
Block a user