work: restore shift+spacebar for media play/pause
maybe put in maya config? idk what funiman's preference is
This commit is contained in:
@@ -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
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2026-04-21 12:47:39.126086",
|
||||
"backup_date": "April-21-2026",
|
||||
"last_check": "2026-05-27 14:41:11.263249",
|
||||
"backup_date": "May-27-2026",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "Animation Layers",
|
||||
"author": "Tal Hershkovich",
|
||||
"version" : (2, 3, 8),
|
||||
"version" : (2, 4, 0),
|
||||
"blender" : (3, 2, 0),
|
||||
"location": "View3D - Properties - Animation Panel",
|
||||
"description": "Simplifying the NLA editor into an animation layers UI and workflow",
|
||||
@@ -153,7 +153,6 @@ def update_panel(self, context):
|
||||
bpy.utils.unregister_class(panel)
|
||||
|
||||
for panel in panels:
|
||||
#print (panel.bl_category)
|
||||
panel.bl_category = context.preferences.addons[__name__].preferences.category
|
||||
bpy.utils.register_class(panel)
|
||||
|
||||
@@ -176,8 +175,9 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
|
||||
items = [('ANIMLAYERS', 'Anim Layers Settings', 'Use Anim Layers properties to adjust custom frame range'),
|
||||
('NLA', 'NLA Settings', 'Use the nla properties to adjust custom frame range')])
|
||||
|
||||
lock_nlatracks: bpy.props.BoolProperty(name="Automatically lock the nla tracks for safety measures", description="Automatically lock nla tracks when creating layers for safety", default = True)
|
||||
|
||||
lock_nlatracks: bpy.props.BoolProperty(name="Automatically lock the NLA tracks", description="Automatically lock nla tracks when creating layers for safety", default = True)
|
||||
auto_custom_range: bpy.props.BoolProperty(name="Switch automatically to custom frame range when editing NLA Strips", description="Automatically use custom frame range when adjusting NLA Strips manually", default = False)
|
||||
|
||||
#Property for ClearActiveAction
|
||||
proceed: bpy.props.EnumProperty(name="Choose how to proceed", description="Select an option how to proceed with Anim Layers", override = {'LIBRARY_OVERRIDABLE'},
|
||||
items = [
|
||||
@@ -260,8 +260,9 @@ class AnimLayersAddonPreferences(bpy.types.AddonPreferences):
|
||||
row.label(text = "Custom Frame Range Settings")
|
||||
row.prop(self, "frame_range_settings", text = '')
|
||||
|
||||
col.prop(self, "lock_nlatracks")
|
||||
|
||||
row = col.row()
|
||||
row.prop(self, "auto_custom_range")
|
||||
row.prop(self, "lock_nlatracks")
|
||||
|
||||
classes = (AnimLayersSettings, AnimLayersSceneSettings, AnimLayersItems, AnimLayersObjects)
|
||||
|
||||
|
||||
@@ -399,7 +399,7 @@ def get_fcu_layer_keyframes(obj, context, track):
|
||||
keyframes = []
|
||||
# fcurves = get_fcurves(track.strips[0].action)
|
||||
# fcurves = track.strips[0].action.fcurves
|
||||
fcurves = get_fcurves(obj, track.strips[0].action)
|
||||
fcurves = get_fcurves(obj, track.strips[0].action, obj.als.data_type)
|
||||
#store all the keyframe locations from the fcurves of the layer
|
||||
for fcu in fcurves:
|
||||
if fcu.group is not None:
|
||||
@@ -1546,6 +1546,8 @@ def strip_action_recalc(self, strip):
|
||||
|
||||
###################################################### HELPER FUNCTIONS ################################################
|
||||
def redraw_areas(areas):
|
||||
if not len(bpy.context.window_manager.windows):
|
||||
return
|
||||
for area in bpy.context.window_manager.windows[0].screen.areas:
|
||||
if area.type in areas:
|
||||
area.tag_redraw()
|
||||
@@ -1621,7 +1623,7 @@ def select_layer_bones(self, context):
|
||||
|
||||
###################################################### CLASSES ###########################################################
|
||||
class SelectBonesInLayer(bpy.types.Operator):
|
||||
"""Select bones with keyframes in the current layer"""
|
||||
"""Select bones with keyframes in the current layer, use shift to add to the current selection"""
|
||||
bl_idname = "anim.bones_in_layer"
|
||||
bl_label = "Select layer bones"
|
||||
bl_icon = "BONE_DATA"
|
||||
@@ -1864,18 +1866,9 @@ class AutoCustomFrameRange(bpy.types.Operator):
|
||||
# return {'CANCELLED'}
|
||||
|
||||
def restore(self, context):
|
||||
if hasattr(subscriptions, 'frame_range'):
|
||||
frame_start, frame_end = subscriptions.frame_range
|
||||
|
||||
else:
|
||||
frame_start, frame_end = subscriptions.get_frame_range(context.scene)
|
||||
print('restore')
|
||||
subscriptions.frameend_update_callback()
|
||||
|
||||
self.strip.repeat = 1 #change strip repeat but keep self.repeat value stored
|
||||
self.strip.use_reverse = False
|
||||
self.strip.frame_start = frame_start
|
||||
self.strip.scale = self.layer.speed
|
||||
self.strip.frame_end = frame_end
|
||||
# update_action_frame_range(frame_start, frame_end, layer, strip)
|
||||
subscriptions.subscriptions_add(context.scene)
|
||||
|
||||
def update_action_list(scene):
|
||||
@@ -2596,9 +2589,7 @@ class RemoveFcurves(bpy.types.Operator):
|
||||
if mod.type == 'CYCLES':
|
||||
fcu.modifiers.remove(mod)
|
||||
fcu.update()
|
||||
for area in context.window_manager.windows[0].screen.areas:
|
||||
if area.type == 'GRAPH_EDITOR' or area.type == 'VIEW_3D':
|
||||
area.tag_redraw()
|
||||
redraw_areas(['GRAPH_EDITOR', 'VIEW_3D'])
|
||||
break
|
||||
return {'FINISHED'}
|
||||
|
||||
@@ -3301,25 +3292,36 @@ def copy_action(action):
|
||||
|
||||
return new_action
|
||||
|
||||
def get_obj_slot(obj, action, data_type = 'OBJECT'):
|
||||
def get_obj_slot(obj, action, data_type = None):
|
||||
'''Get the slot in the action that this object is using either it's object, or shapekeys'''
|
||||
|
||||
if data_type is None:
|
||||
data_type = obj.als.data_type
|
||||
|
||||
if not hasattr(action, 'slots'):
|
||||
return None
|
||||
|
||||
if not len(action.slots):
|
||||
# If no slots exist, create one for the object and return it
|
||||
slot = add_action_slot(obj, action)
|
||||
return slot
|
||||
|
||||
# data_type = obj.als.data_type
|
||||
for slot in action.slots:
|
||||
if slot.target_id_type != data_type:
|
||||
continue
|
||||
# if obj.als.data_type == 'OBJECT' and obj in slot.users():
|
||||
# return slot
|
||||
|
||||
if data_type == 'KEY' and obj.data.shape_keys in slot.users():
|
||||
return slot
|
||||
elif obj in slot.users():
|
||||
return slot
|
||||
|
||||
return None
|
||||
|
||||
return add_action_slot(obj, action)
|
||||
|
||||
def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OBJECT'):
|
||||
def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = None):
|
||||
|
||||
if data_type is None:
|
||||
data_type = obj.als.data_type
|
||||
|
||||
if hasattr(action, 'layers'):
|
||||
slot = get_obj_slot(obj, action, data_type)
|
||||
@@ -3335,10 +3337,13 @@ def get_fcurves(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OB
|
||||
return action.fcurves
|
||||
return []
|
||||
|
||||
def get_channelbag(obj: bpy.types.Object, action: bpy.types.Action, data_type = 'OBJECT'):
|
||||
def get_channelbag(obj: bpy.types.Object, action: bpy.types.Action, data_type = None):
|
||||
'''Getting the container of the fcurves, either the action or channelbag
|
||||
Using this when adding a new group to the action'''
|
||||
|
||||
if data_type is None:
|
||||
data_type = obj.als.data_type
|
||||
|
||||
if hasattr(action, 'layers'):
|
||||
slot = get_obj_slot(obj, action, data_type)
|
||||
channelbag = None
|
||||
|
||||
+4
-4
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"last_check": "2026-04-21 12:47:39.126086",
|
||||
"backup_date": "March-27-2026",
|
||||
"last_check": "2026-05-27 14:41:11.263249",
|
||||
"backup_date": "April-21-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=dddd6932039b8a3e5fae3ce2de957f21a5942c84",
|
||||
"link": "https://gitlab.com/api/v4/projects/22294607/repository/archive.zip?sha=321d411a449bc9acee2a759e30cd3d0f36bbd2ab",
|
||||
"version": [
|
||||
2,
|
||||
4,
|
||||
0
|
||||
1
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -645,6 +645,7 @@ def AL_bake(frame_start, frame_end, nla_tracks, fcu_keys, additive, step, action
|
||||
baked_action = track.strips[0].action
|
||||
clean_no_user_slots(baked_action)
|
||||
#create the baked fcurve
|
||||
# baked_channelbag = anim_layers.get_channelbag(obj, baked_action, obj.als.data_type)
|
||||
baked_channelbag = anim_layers.get_channelbag(obj, baked_action)
|
||||
baked_fcurves = baked_channelbag.fcurves
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ def animlayers_frame(scene, context):
|
||||
scene['framerange_preview'] = scene.use_preview_range
|
||||
frameend_update_callback()
|
||||
return
|
||||
|
||||
frame_start, frame_end = bake_ops.frame_start_end(scene)
|
||||
# frame_start, frame_end = get_frame_range(scene)
|
||||
reset_subscription = False
|
||||
@@ -106,7 +105,6 @@ def animlayers_frame(scene, context):
|
||||
for i, track in enumerate(nla_tracks):
|
||||
if len(track.strips) != 1:
|
||||
continue
|
||||
|
||||
#checks if the layer has a custom frame range
|
||||
layer = obj.Anim_Layers[i]
|
||||
if layer.custom_frame_range:
|
||||
@@ -127,6 +125,7 @@ def animlayers_frame(scene, context):
|
||||
if strip.frame_start < 0:
|
||||
strip.frame_start = 0
|
||||
anim_layers.update_action_frame_range(0, frame_end, layer, strip)
|
||||
return
|
||||
anim_layers.update_action_frame_range(strip.frame_start, current + 10.0, layer, strip)
|
||||
strip.frame_end = current + 10.0
|
||||
|
||||
@@ -231,6 +230,9 @@ def track_layer_synchronization(obj, nla_tracks):
|
||||
if obj.als.layer_index > len(obj.Anim_Layers)-1:
|
||||
obj.als.layer_index = len(obj.Anim_Layers)-1
|
||||
|
||||
if not bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
|
||||
return
|
||||
|
||||
#update new layer with strip settings
|
||||
frame_start, frame_end = get_frame_range(bpy.context.scene)
|
||||
|
||||
@@ -243,7 +245,6 @@ def track_layer_synchronization(obj, nla_tracks):
|
||||
continue
|
||||
if (strip.frame_start, strip.frame_end) != (frame_start, frame_end):
|
||||
subscriptions_remove()
|
||||
# print(f'strip.frame_start {strip.frame_start} strip.frame_end {strip.frame_end} frame_start {frame_start} frame_end {frame_end}')
|
||||
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
|
||||
return
|
||||
|
||||
@@ -311,11 +312,16 @@ def sync_frame_range(scene, track, layer):
|
||||
return
|
||||
|
||||
#Turn on custom frame range if the current strip is not following the scene frame range
|
||||
# Should be activated when nla strips are edited manually in the nla editor, only when auto custom range is turned on, otherwise just update the strip frame range to the scene frame range
|
||||
if (round(strip.frame_start, 2), round(strip.frame_end, 2)) != (round(frame_start, 2), round(frame_end, 2)):
|
||||
subscriptions_remove()
|
||||
# print('315 custom frame range')
|
||||
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
|
||||
return
|
||||
if bpy.context.preferences.addons[__package__].preferences.auto_custom_range:
|
||||
subscriptions_remove()
|
||||
# print('321 custom frame range')
|
||||
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
|
||||
return
|
||||
else:
|
||||
frameend_update_callback()
|
||||
return
|
||||
|
||||
def sync_strip_range(scene):
|
||||
'''Checking all the strips if a value was changed in the nla (not including UI changes)
|
||||
@@ -356,7 +362,6 @@ def sync_strip_range(scene):
|
||||
if (strip_frame_start, round(strip_frame_end, 2)) != (frame_start, float(frame_end)):
|
||||
subscriptions_remove()
|
||||
# print('357 custom_frame_range_warning ')
|
||||
# print(f'strip_frame_start {strip_frame_start} strip_frame_end {round(strip_frame_end, 2)} frame_start {frame_start} frame_end {float(frame_end)}')
|
||||
bpy.ops.anim.custom_frame_range_warning('INVOKE_DEFAULT')
|
||||
return
|
||||
|
||||
@@ -567,7 +572,6 @@ def subscribe_to_preview_frame_end(scene):
|
||||
# Subscribing to preview frame end since it's not registering in the depsgraph
|
||||
subscribe_preview_end = scene.path_resolve("frame_preview_end", False)
|
||||
subscribe_use_preview = scene.path_resolve("use_preview_range", False)
|
||||
# print('subscribe_to_preview_frame_end')
|
||||
for subscribe in [subscribe_preview_end, subscribe_use_preview]:
|
||||
|
||||
bpy.msgbus.subscribe_rna(
|
||||
|
||||
BIN
Binary file not shown.
@@ -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)
|
||||
|
||||
@@ -71,6 +71,7 @@ def draw_wgt(scale, bone, shape = 'sphere'):
|
||||
shape = shape.lower()
|
||||
|
||||
mesh.from_pydata(np.array(shapes[shape]['vertices'])*scale , shapes[shape]['edges'], shapes[shape]['faces'])
|
||||
# print('draw shape scale ', scale)
|
||||
shape_obj['WGT_TempCtrl'] = True
|
||||
return shape_obj
|
||||
|
||||
@@ -204,28 +205,27 @@ def tempctrl_shape_type(self, context):
|
||||
ctrl.custom_shape_translation = (0, 0, 0)
|
||||
ctrl.custom_shape_rotation_euler = (0, 0, 0)
|
||||
continue
|
||||
|
||||
if org_bone.custom_shape_transform is not None:# and ctrl.custom_shape_transform is None:
|
||||
|
||||
if org_bone.custom_shape_transform is not None and ctrl.custom_shape_transform is None:
|
||||
#check if the transform bone shape already exists
|
||||
transform_bonename = org_bone.custom_shape_transform.name
|
||||
if transform_bonename in ctrl.id_data.pose.bones:
|
||||
# If it already exists then find it
|
||||
ctrl.custom_shape_transform = ctrl.id_data.pose.bones[transform_bonename]
|
||||
else:
|
||||
#add an extra transform bone, if it doesnt exist
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
boneshape = add_ctrl_bone(rig, org_bone.custom_shape_transform, rig.data.edit_bones[ctrl.name].parent, '')
|
||||
transform_bonename = boneshape.name
|
||||
add_bone_to_collection(rig, boneshape)
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
|
||||
continue
|
||||
#add an extra transform bone, if it doesnt exist
|
||||
rig = ctrl.id_data
|
||||
posebone = rig.pose.bones[transform_bonename]
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
boneshape = add_ctrl_bone(rig, org_bone.custom_shape_transform, rig.data.edit_bones[ctrl.name].parent, '')
|
||||
bone_name = boneshape.name
|
||||
add_bone_to_collection(rig, boneshape)
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
posebone = rig.pose.bones[bone_name]
|
||||
constraint_add(posebone, org_bone.custom_shape_transform, 'COPY_TRANSFORMS')
|
||||
ctrl.custom_shape_transform = posebone
|
||||
else:
|
||||
# scale = (length_avg / ctrl.length)*0.4 + 0.4
|
||||
scale = 1
|
||||
# print('drawing shape in scale of ', scale)
|
||||
ctrl.custom_shape = draw_wgt(scale, ctrl, shape = self.shape_type)
|
||||
ctrl.custom_shape_scale_xyz = tuple([self.shape_size]*3)
|
||||
ctrl.custom_shape_transform = None
|
||||
@@ -1466,11 +1466,6 @@ def smartbake_to_ik(context, posebones, chain, sb):
|
||||
|
||||
#create the fcurves and add them to smartfcurves
|
||||
for ctrl in ctrls:
|
||||
if ctrl == chain.ik_tip and ctrl != chain.ik_ctrl:
|
||||
continue
|
||||
# if chain.pole_bone and ctrl == chain.base_bone:
|
||||
# continue
|
||||
|
||||
ctrl_action = ctrl.id_data.animation_data.action
|
||||
path = ctrl.path_from_id()
|
||||
rot = Tools.rot_mode_to_channel(ctrl.rotation_mode)
|
||||
@@ -1909,7 +1904,7 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
|
||||
#Store the influence and set them to 0 before calculating the matrix without the constraint (to get the offset)
|
||||
con_influence = get_con_influence(constrained)
|
||||
|
||||
if chain and chain.pole_bone is not None:
|
||||
if chain:
|
||||
# Adding the pole bone to the end of the list
|
||||
bones_matrices.update({chain.pole_bone : None})
|
||||
bones_matrices[chain.pole_bone] = bones_matrices.pop(chain.pole_bone)
|
||||
@@ -1929,23 +1924,11 @@ def smartbake_write_keyframes(scene, smartframes, smartfcus, chain = None, bake_
|
||||
else:
|
||||
# Get the rest of the bones to check if they are children and if we need viewlayer update
|
||||
bones = set(bones_matrices.keys()).difference(pasted_bones)
|
||||
|
||||
|
||||
# Calculating pole target matrix after other bones were calcluated
|
||||
if chain and bone == chain.pole_bone:
|
||||
base_bone = chain.base_bone
|
||||
ik_tip = chain.ik_tip
|
||||
angle = chain.base_bone.vector.angle(chain.ik_tip.vector)
|
||||
|
||||
if not round(angle):
|
||||
# continue
|
||||
# If the angle is flat then use the position of the pole relative to the rest position base bone
|
||||
relative_matrix = base_bone.bone.matrix_local.inverted() @ chain.pole_bone.bone.matrix_local
|
||||
matrix = base_bone.matrix @ relative_matrix
|
||||
# print(frame, 'angle is 0 using base bone matrix')
|
||||
|
||||
else:
|
||||
bpy.context.view_layer.update()
|
||||
matrix = find_pole_vector(base_bone, ik_tip, chain.pole_bone, chain.step)
|
||||
bpy.context.view_layer.update()
|
||||
matrix = find_pole_vector(chain.base_bone, chain.ik_tip, chain.pole_bone, chain.step)
|
||||
|
||||
Tools.paste_bone_matrix(bone, matrix, constrained, bones, x_filter = False)
|
||||
pasted_bones.add(bone)
|
||||
@@ -4151,7 +4134,7 @@ def assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_i
|
||||
#Assign group, color, matrix and setup ids in pose mode
|
||||
for ctrl, bone in zip(ctrls, selected):
|
||||
ctrl.matrix = get_relative_ws_matrix(bone, ctrl)
|
||||
|
||||
|
||||
#assign the controller to a group
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
ctrl.bone_group = ctrl_group
|
||||
@@ -4193,7 +4176,6 @@ def assign_setup_ids(self, context, ctrls, selected, child_ctrls, setup, setup_i
|
||||
|
||||
#get custom shape and assign the matrix
|
||||
if bone.custom_shape_transform and self.shape_type == 'ORIGINAL':
|
||||
obj_mat = bone.id_data.matrix_world
|
||||
ot_bone = bone.custom_shape_transform.name
|
||||
#Get the original override transform bone
|
||||
target = bone.id_data.pose.bones[ot_bone]
|
||||
@@ -5328,16 +5310,14 @@ def pole_prop_edit(self, context):
|
||||
else:
|
||||
add_bone_to_collection(rig, rig.data.edit_bones[chain.base_bone.name], col_name = 'IK Ctrls', visible = True)
|
||||
#remove the pole bone and switch to base bone
|
||||
if chain.pole_bone is None:
|
||||
if not hasattr(chain, 'pole_bone'):
|
||||
continue
|
||||
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
|
||||
#remove from ctrls and append base bone instead
|
||||
if chain.pole_bone in ctrls:
|
||||
ctrls.remove(chain.pole_bone)
|
||||
ctrls.append(chain.base_bone)
|
||||
|
||||
chain.pole_bone = chain.pole_bone_name = None
|
||||
# del chain.pole_bone, chain.pole_bone_name
|
||||
del chain.pole_bone, chain.pole_bone_name
|
||||
|
||||
#Remove child ctrls
|
||||
if btc.child:
|
||||
@@ -5350,17 +5330,13 @@ def pole_prop_pose(self, context):
|
||||
if not btc.pole_target:
|
||||
chain.base_bone.btc.org = 'CTRL'
|
||||
|
||||
# if not hasattr(chain, 'pole_bone'):
|
||||
# continue
|
||||
if not chain.pole_bone:
|
||||
continue
|
||||
if not chain.pole_bone_name:
|
||||
continue
|
||||
chain.pole_bone = chain.pole_bone_name = None
|
||||
# del chain.pole_bone
|
||||
# if hasattr(chain,'pole_bone_name'):
|
||||
# chain.pole_bone_name
|
||||
# continue
|
||||
if not hasattr(chain, 'pole_bone'):
|
||||
continue
|
||||
|
||||
del chain.pole_bone
|
||||
if hasattr(chain,'pole_bone_name'):
|
||||
chain.pole_bone_name
|
||||
continue
|
||||
|
||||
pole_posebone_update(chain, ctrls)
|
||||
if chain.base_bone in ctrls:
|
||||
@@ -5437,9 +5413,7 @@ def ik_ctrl_orientation(self, context):
|
||||
|
||||
def pole_posebone_update(chain, ctrls):
|
||||
'''update pole bone properties in the pole prop and add ik prop'''
|
||||
# if not hasattr(chain, 'pole_bone_name'):
|
||||
# return
|
||||
if not chain.pole_bone_name:
|
||||
if not hasattr(chain, 'pole_bone_name'):
|
||||
return
|
||||
|
||||
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
|
||||
@@ -5473,8 +5447,6 @@ def update_axis_prop(self, context):
|
||||
|
||||
if not hasattr(chain, 'pole_bone_name'):
|
||||
continue
|
||||
if not chain.pole_bone_name:
|
||||
continue
|
||||
pole_matrix = find_pole_vector(base_bone, ik_tip, ik_tip, chain.step)
|
||||
|
||||
#pole matrix orientation to world by default
|
||||
@@ -5579,6 +5551,15 @@ def add_pole_bone(self, context, rig, chain):
|
||||
new_rot = mathutils.Euler((0, 0, 0), 'XYZ')
|
||||
pole_matrix = Matrix.LocRotScale(loc, new_rot, scale)
|
||||
|
||||
#In case of a flat IK skip adding a pole bone and use base bone instead
|
||||
# if pole_matrix.translation == chain.ik_tip.matrix.translation:
|
||||
# print('no pole bone')
|
||||
# self.report({'ERROR'}, "Can't add Pole Ctrl because of a flat IK chain")
|
||||
# chain.pole_bone = None
|
||||
# add_bone_to_collection(rig, chain.base_bone, col_name = 'IK Ctrls', visible = True)
|
||||
# btc['pole_target'] = False
|
||||
# return
|
||||
|
||||
pole_bone_name = chain.org_bones[-2].name + '_Pole' if len(chain.org_bones) > 1 else chain.org_bones[0].name + '_Pole'
|
||||
chain.pole_bone = rig.data.edit_bones.new(pole_bone_name)
|
||||
|
||||
@@ -5601,9 +5582,7 @@ def pole_offset(self, context):
|
||||
global ik_chains, ctrls, child_names
|
||||
|
||||
for chain in ik_chains:
|
||||
# if not hasattr(chain, 'pole_bone_name'):
|
||||
# continue
|
||||
if not chain.pole_bone_name:
|
||||
if not hasattr(chain, 'pole_bone_name'):
|
||||
continue
|
||||
chain.pole_bone = rig.pose.bones[chain.pole_bone_name]
|
||||
|
||||
@@ -5642,7 +5621,7 @@ def add_ik_prop_edit(self, context):
|
||||
chain.ik_tip_name = chain.ik_tip.name
|
||||
|
||||
#remove the pole bone
|
||||
if btc.pole_target and chain.pole_bone_name:
|
||||
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
|
||||
#removing and adding the pole_target without calling the whole pole prop update
|
||||
ctrls.remove(chain.pole_bone)
|
||||
rig.data.edit_bones.remove(rig.data.edit_bones[chain.pole_bone_name])
|
||||
@@ -5675,7 +5654,7 @@ def add_ik_prop_pose(self, context):
|
||||
chain.ik_ctrl.btc.org = 'CTRL'
|
||||
|
||||
chain.ik_ctrl.color.palette = context.scene.btc.color_set
|
||||
if btc.pole_target and chain.pole_bone_name: #hasattr(chain, 'pole_bone_name'):
|
||||
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
|
||||
pole_posebone_update(chain, ctrls)
|
||||
|
||||
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
|
||||
@@ -5803,7 +5782,7 @@ class TempIK(bpy.types.Operator):
|
||||
# for ctrl in ctrls:
|
||||
# add_bone_to_collection(rig, ctrl, col_name = 'ORG', visible = True)
|
||||
chain = IK_Chain(hierarchy, ctrls, root_ctrl)
|
||||
|
||||
|
||||
#if only one bone is selected in the chain then add an extra control automatically
|
||||
if len(hierarchy) == 1:
|
||||
add_ik_ctrl = True
|
||||
@@ -5812,7 +5791,7 @@ class TempIK(bpy.types.Operator):
|
||||
for i, ctrl in enumerate(ctrls[:-1]):
|
||||
ctrls[i+1].tail = ctrl.head
|
||||
context.view_layer.update()
|
||||
# return {'FINISHED'}
|
||||
|
||||
switch_layers(rig, chain.ik_tip, [31])
|
||||
add_bone_to_collection(rig, chain.ik_tip)
|
||||
add_bone_to_collection(rig, chain.ik_ctrl, col_name = 'IK Ctrls', visible = True)
|
||||
@@ -5821,7 +5800,7 @@ class TempIK(bpy.types.Operator):
|
||||
# rig.data.collections['ORG'].is_visible = False
|
||||
|
||||
ik_chains.append(chain)
|
||||
|
||||
|
||||
btc['add_ik_ctrl'] = add_ik_ctrl
|
||||
if add_ik_ctrl:
|
||||
add_ik_prop_edit(self, context)
|
||||
@@ -5845,7 +5824,8 @@ class TempIK(bpy.types.Operator):
|
||||
|
||||
####################################################################
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
|
||||
|
||||
|
||||
setup_ids = set()
|
||||
|
||||
if btc.root:
|
||||
@@ -5878,22 +5858,20 @@ class TempIK(bpy.types.Operator):
|
||||
# #assign the matrix
|
||||
distance = (bone.id_data.matrix_world @ bone.bone.matrix_local).inverted() @ ctrl.bone.matrix_local.copy()
|
||||
ctrl.matrix = bone.id_data.matrix_world @ bone.matrix @ distance
|
||||
# context.view_layer.update()
|
||||
|
||||
ctrl.color.palette = btc.color_set
|
||||
|
||||
add_bone_setup_id(chain.ik_ctrl, False, setup, 'CTRL', setup_id, org_id = chain.ik_ctrl.btc.org_id)
|
||||
|
||||
|
||||
if add_ik_ctrl:
|
||||
add_ik_prop_pose(self, context)
|
||||
|
||||
|
||||
context.view_layer.update()
|
||||
pole_prop_pose(self, context)
|
||||
|
||||
# child_prop_pose(context)
|
||||
if context.scene.btc.child:
|
||||
child_prop(context.scene.btc, context)
|
||||
|
||||
# return {'FINISHED'}
|
||||
self.ik_chains = ik_chains
|
||||
tempctrl_shape_type(self, context)
|
||||
|
||||
@@ -5907,7 +5885,7 @@ class TempIK(bpy.types.Operator):
|
||||
if 'ctrls' not in globals():
|
||||
self.report({'INFO'},'Not available. Please update the Temp Ctrls from the addon panel')
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
btc = context.scene.btc
|
||||
controlled_objs = {item.controlled for item in context.scene.btc.ctrl_items if item.controlled}
|
||||
hidden_objs = {obj for obj in context.view_layer.objects if obj and obj.hide_get()}
|
||||
@@ -5974,7 +5952,7 @@ class TempIK(bpy.types.Operator):
|
||||
rig.data.edit_bones[ctrl.name].parent = rig.data.edit_bones[chain.ctrls[i+1].name]
|
||||
|
||||
#updating the polebone position with the offset
|
||||
if btc.pole_target and chain.pole_bone_name: #hasattr(chain, 'pole_bone_name'):
|
||||
if btc.pole_target and hasattr(chain, 'pole_bone_name'):
|
||||
pole_bone = rig.data.edit_bones[chain.pole_bone_name]
|
||||
base_bone = rig.data.edit_bones[chain.base_bone_name]
|
||||
ik_tip = rig.data.edit_bones[chain.ik_tip_name]
|
||||
@@ -6103,8 +6081,7 @@ class TempIK(bpy.types.Operator):
|
||||
#add the ik constraint to the ctrls
|
||||
ik_con = constraint_add(chain.ik_tip, chain.ik_ctrl if not btc.child else chain.ik_child_ctrl, 'IK')
|
||||
|
||||
if chain.pole_bone:
|
||||
chain.pole_angle = get_pole_angle(chain.base_bone, chain.ik_tip, chain.pole_bone.matrix.translation)
|
||||
chain.pole_angle = get_pole_angle(chain.base_bone, chain.ik_tip, chain.pole_bone.matrix.translation)
|
||||
|
||||
ik_con.chain_count = chain.length(btc.add_ik_ctrl)
|
||||
#setup the ik constraint
|
||||
@@ -6339,8 +6316,6 @@ class IK_Chain:
|
||||
self.ik_ctrl_name = self.ik_ctrl.name
|
||||
self.base_bone_name = self.base_bone.name
|
||||
self.ik_tip_name = self.ik_tip.name
|
||||
self.pole_bone = None
|
||||
self.pole_bone_name = None
|
||||
|
||||
self.step = get_step(hierarchy[0].id_data)
|
||||
|
||||
@@ -6362,7 +6337,7 @@ class IK_Chain:
|
||||
self.parent = rig.pose.bones[self.parent_name]
|
||||
if hasattr(self, 'root_name'):
|
||||
self.root_ctrl = rig.pose.bones[self.root_name]
|
||||
if self.pole_bone_name:
|
||||
if hasattr(self, 'pole_bone_name'):
|
||||
self.pole_bone = rig.pose.bones[self.pole_bone_name]
|
||||
# if hasattr(self, 'pole_bone_ref_name'):
|
||||
# self.pole_bone_ref = rig.pose.bones[self.pole_bone_ref_name]
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
bl_info = {
|
||||
"name": "AnimToolBox",
|
||||
"author": "Tal Hershkovich",
|
||||
"version" : (0, 2, 3),
|
||||
"version" : (0, 2, 0),
|
||||
"blender" : (3, 2, 0),
|
||||
"location": "View3D - Properties - Animation Panel",
|
||||
"description": "A set of animation tools",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"last_check": "2026-02-09 15:54:54.419194",
|
||||
"backup_date": "February-9-2026",
|
||||
"last_check": "",
|
||||
"backup_date": "",
|
||||
"update_ready": false,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
|
||||
@@ -1,189 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
|
||||
import bpy
|
||||
|
||||
class GizmoSizeUp(bpy.types.Operator):
|
||||
"""Share keyframes between all the selected objects and bones"""
|
||||
bl_idname = "view3d.gizmo_size_up"
|
||||
bl_label = "Gizmo_Size_Up"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
context.preferences.view.gizmo_size += context.scene.animtoolbox.gizmo_size
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
class GizmoSizeDown(bpy.types.Operator):
|
||||
"""Share keyframes between all the selected objects and bones"""
|
||||
bl_idname = "view3d.gizmo_size_down"
|
||||
bl_label = "Gizmo_Size_Down"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
context.preferences.view.gizmo_size -= context.scene.animtoolbox.gizmo_size
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
########################################################################################################################
|
||||
|
||||
def clear_isolate_pose_mode(scene):
|
||||
if not len(scene.animtoolbox.isolated):
|
||||
return
|
||||
for obj in scene.animtoolbox.isolated:
|
||||
if not obj.hidden:
|
||||
continue
|
||||
obj.hidden.hide_set(False)
|
||||
scene.animtoolbox.isolated.clear()
|
||||
scene.animtoolbox.active_obj = None
|
||||
|
||||
def isolate_pose_mode(scene):
|
||||
context = bpy.context
|
||||
#return when going out of isolate pose or when active object is not in pose mode
|
||||
if not scene.animtoolbox.isolate_pose_mode or context.active_object.mode != 'POSE':
|
||||
clear_isolate_pose_mode(scene)
|
||||
return
|
||||
|
||||
#handler continue only if the active object is None otherwise it collects all armature objects
|
||||
if scene.animtoolbox.active_obj is None:
|
||||
scene.animtoolbox.active_obj = context.active_object
|
||||
else:
|
||||
return
|
||||
|
||||
isolated = scene.animtoolbox.isolated
|
||||
for obj in context.view_layer.objects:
|
||||
if obj.type != 'ARMATURE':
|
||||
continue
|
||||
if obj.hide_get():
|
||||
continue
|
||||
rig = isolated.add()
|
||||
if obj.mode == 'POSE':
|
||||
rig.selected = obj
|
||||
else:
|
||||
rig.hidden = obj
|
||||
obj.hide_set(True)
|
||||
|
||||
class IsolatePoseMode(bpy.types.Operator):
|
||||
"""Isolates armatures during pose mode"""
|
||||
bl_idname = "anim.isolate_pose_mode"
|
||||
bl_label = "Isolate Pose Mode"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def execute(self, context):
|
||||
|
||||
# If the modal is already running, then don't run it the second time
|
||||
scene = context.scene
|
||||
if scene.animtoolbox.isolate_pose_mode:
|
||||
if isolate_pose_mode in bpy.app.handlers.depsgraph_update_pre:
|
||||
clear_isolate_pose_mode(scene)
|
||||
bpy.app.handlers.depsgraph_update_pre.remove(isolate_pose_mode)
|
||||
scene.animtoolbox.isolate_pose_mode = False
|
||||
return {'FINISHED'}
|
||||
|
||||
scene.animtoolbox.isolate_pose_mode = True
|
||||
isolate_pose_mode(scene)
|
||||
if isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
|
||||
bpy.app.handlers.depsgraph_update_pre.append(isolate_pose_mode)
|
||||
return {'FINISHED'}
|
||||
|
||||
class SwitchBoneCollectionsVisibility(bpy.types.Operator):
|
||||
"""Turn all bone collections visible and then press again to switch back"""
|
||||
bl_idname = "anim.switch_collections_visibility"
|
||||
bl_label = "Bone Collections Visibility"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.app.version >= (4, 0, 0)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
if obj.type != 'ARMATURE':
|
||||
return {'CANCELLED'}
|
||||
if not obj.animation_data:
|
||||
self.report({'INFO'}, 'No animation is available')
|
||||
return {'CANCELLED'}
|
||||
|
||||
if not obj.animation_data.action:
|
||||
self.report({'INFO'}, 'No animation is available')
|
||||
return {'CANCELLED'}
|
||||
|
||||
collections = obj.data.collections
|
||||
|
||||
if not len(collections):
|
||||
self.report({'INFO'}, 'No collections are available')
|
||||
return {'CANCELLED'}
|
||||
#check if there are collections that are marked with
|
||||
tagged_col = ['atb' in col.keys() for col in collections]
|
||||
atb_ui = context.scene.animtoolbox
|
||||
|
||||
if any(tagged_col) and atb_ui.col_vis:
|
||||
#collections are already marked so return to previous collection visibilty
|
||||
for col in collections:
|
||||
if 'atb' in col.keys():
|
||||
col.is_visible = col['atb']
|
||||
del col['atb']
|
||||
atb_ui.col_vis = False
|
||||
else:
|
||||
#Mark visible collections and turn collections with animated bones on
|
||||
animated_bones = set()
|
||||
start = 'pose.bones["'
|
||||
end = '"]'
|
||||
#get all the animated bones from the fcurves
|
||||
for fcu in obj.animation_data.action.fcurves:
|
||||
start_index = fcu.data_path.find(start)
|
||||
end_index = fcu.data_path.find(end)
|
||||
#if it's not a posebone fcurve then skip
|
||||
if start_index == -1 or end_index == -1:
|
||||
continue
|
||||
animated_bones.add(fcu.data_path[start_index + len(start):end_index])
|
||||
|
||||
#check if the collecetion that is turned off has animated bones
|
||||
find_anim = []
|
||||
for col in collections:
|
||||
for bone in col.bones:
|
||||
if bone.name in animated_bones and not col.is_visible:
|
||||
# print(bone.name, 'in ', col.name)
|
||||
find_anim.append(col)
|
||||
break
|
||||
|
||||
if not find_anim:
|
||||
self.report({'INFO'}, 'No collections with animated bones and no visibility are found')
|
||||
return {'CANCELLED'}
|
||||
|
||||
#Turn on collections without visiblity
|
||||
for col in collections:
|
||||
if col in find_anim:
|
||||
#tag visibility
|
||||
col['atb'] = col.is_visible
|
||||
col.is_visible = True
|
||||
|
||||
atb_ui.col_vis = True
|
||||
|
||||
return {'FINISHED'}
|
||||
|
||||
classes = (GizmoSizeUp, GizmoSizeDown, IsolatePoseMode, SwitchBoneCollectionsVisibility)
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
||||
@@ -1,549 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
|
||||
import bpy
|
||||
from mathutils import Matrix, Vector
|
||||
from math import radians
|
||||
import numpy
|
||||
|
||||
def draw_wgt(boneLength, bone):
|
||||
suffix = bone.id_data.name + '_' + bone.name
|
||||
if 'WGTB_object' + suffix in bpy.data.objects:
|
||||
obj = bpy.data.objects['WGTB_object'] + suffix
|
||||
if 'WGTB_shape' + suffix in obj.data.name:
|
||||
return obj
|
||||
mesh = bpy.data.meshes.new('WGTB_shape_' + suffix)
|
||||
obj = bpy.data.objects.new('WGTB_object_' + suffix, mesh)
|
||||
#coordinates of the sphere widget shape
|
||||
sphere = {"edges": [[0, 1], [1, 2], [2, 3], [3, 4], [4, 5], [5, 6], [6, 7], [7, 8], [8, 9], [9, 10], [10, 11], [11, 12], [12, 13], [13, 14], [14, 15], [15, 16], [16, 17], [17, 18], [18, 19], [19, 20], [20, 21], [21, 22], [22, 23], [0, 23], [24, 25], [25, 26], [26, 27], [27, 28], [28, 29], [29, 30], [30, 31], [31, 32], [32, 33], [33, 34], [34, 35], [35, 36], [36, 37], [37, 38], [38, 39], [39, 40], [40, 41], [41, 42], [42, 43], [43, 44], [44, 45], [45, 46], [46, 47], [24, 47], [48, 49], [49, 50], [50, 51], [51, 52], [52, 53], [53, 54], [54, 55], [55, 56], [56, 57], [57, 58], [58, 59], [59, 60], [60, 61], [61, 62], [62, 63], [63, 64], [64, 65], [65, 66], [66, 67], [67, 68], [68, 69], [69, 70], [70, 71], [48, 71]],
|
||||
"vertices": [[0.0, 0.10000002384185791, 0.0], [-0.025881901383399963, 0.09659260511398315, 0.0], [-0.050000011920928955, 0.08660250902175903, 0.0], [-0.07071065902709961, 0.07071065902709961, 0.0], [-0.08660256862640381, 0.04999998211860657, 0.0], [-0.09659260511398315, 0.025881901383399963, 0.0], [-0.10000002384185791, 7.549793679118011e-09, 0.0], [-0.09659260511398315, -0.02588188648223877, 0.0], [-0.08660256862640381, -0.04999998211860657, 0.0], [-0.07071071863174438, -0.07071065902709961, 0.0], [-0.050000011920928955, -0.08660250902175903, 0.0], [-0.02588193118572235, -0.09659260511398315, 0.0], [-3.894143674187944e-08, -0.10000002384185791, 0.0], [0.025881856679916382, -0.09659260511398315, 0.0], [0.04999995231628418, -0.08660256862640381, 0.0], [0.07071065902709961, -0.07071071863174438, 0.0], [0.08660250902175903, -0.05000004172325134, 0.0], [0.09659254550933838, -0.025881946086883545, 0.0], [0.10000002384185791, -4.649123752642481e-08, 0.0], [0.09659260511398315, 0.025881856679916382, 0.0], [0.08660256862640381, 0.04999995231628418, 0.0], [0.07071071863174438, 0.07071065902709961, 0.0], [0.05000007152557373, 0.08660250902175903, 0.0], [0.025881975889205933, 0.09659254550933838, 0.0], [0.0, 7.450580596923828e-09, 0.10000002384185791], [-0.025881901383399963, 7.450580596923828e-09, 0.09659260511398315], [-0.050000011920928955, 7.450580596923828e-09, 0.08660250902175903], [-0.07071065902709961, 7.450580596923828e-09, 0.07071065902709961], [-0.08660256862640381, 3.725290298461914e-09, 0.04999998211860657], [-0.09659260511398315, 1.862645149230957e-09, 0.025881901383399963], [-0.10000002384185791, 8.881784197001252e-16, 7.549793679118011e-09], [-0.09659260511398315, -1.862645149230957e-09, -0.02588188648223877], [-0.08660256862640381, -3.725290298461914e-09, -0.04999998211860657], [-0.07071071863174438, -7.450580596923828e-09, -0.07071065902709961], [-0.050000011920928955, -7.450580596923828e-09, -0.08660250902175903], [-0.02588193118572235, -7.450580596923828e-09, -0.09659260511398315], [-3.894143674187944e-08, -7.450580596923828e-09, -0.10000002384185791], [0.025881856679916382, -7.450580596923828e-09, -0.09659260511398315], [0.04999995231628418, -7.450580596923828e-09, -0.08660256862640381], [0.07071065902709961, -7.450580596923828e-09, -0.07071071863174438], [0.08660250902175903, -3.725290298461914e-09, -0.05000004172325134], [0.09659254550933838, -1.862645149230957e-09, -0.025881946086883545], [0.10000002384185791, -3.552713678800501e-15, -4.649123752642481e-08], [0.09659260511398315, 1.862645149230957e-09, 0.025881856679916382], [0.08660256862640381, 3.725290298461914e-09, 0.04999995231628418], [0.07071071863174438, 7.450580596923828e-09, 0.07071065902709961], [0.05000007152557373, 7.450580596923828e-09, 0.08660250902175903], [0.025881975889205933, 7.450580596923828e-09, 0.09659254550933838], [-7.450580596923828e-09, 4.440892098500626e-16, 0.10000002384185791], [-9.313225746154785e-09, -0.025881901383399963, 0.09659260511398315], [-1.1175870895385742e-08, -0.050000011920928955, 0.08660250902175903], [-1.4901161193847656e-08, -0.07071065902709961, 0.07071065902709961], [-7.450580596923828e-09, -0.08660256862640381, 0.04999998211860657], [-7.450580596923828e-09, -0.09659260511398315, 0.025881901383399963], [-7.450580596923828e-09, -0.10000002384185791, 7.549793679118011e-09], [-7.450580596923828e-09, -0.09659260511398315, -0.02588188648223877], [0.0, -0.08660256862640381, -0.04999998211860657], [0.0, -0.07071071863174438, -0.07071065902709961], [3.725290298461914e-09, -0.050000011920928955, -0.08660250902175903], [5.587935447692871e-09, -0.02588193118572235, -0.09659260511398315], [7.450577044210149e-09, -3.894143674187944e-08, -0.10000002384185791], [9.313225746154785e-09, 0.025881856679916382, -0.09659260511398315], [1.1175870895385742e-08, 0.04999995231628418, -0.08660256862640381], [1.4901161193847656e-08, 0.07071065902709961, -0.07071071863174438], [7.450580596923828e-09, 0.08660250902175903, -0.05000004172325134], [7.450580596923828e-09, 0.09659254550933838, -0.025881946086883545], [7.450580596923828e-09, 0.10000002384185791, -4.649123752642481e-08], [7.450580596923828e-09, 0.09659260511398315, 0.025881856679916382], [0.0, 0.08660256862640381, 0.04999995231628418], [0.0, 0.07071071863174438, 0.07071065902709961], [-3.725290298461914e-09, 0.05000007152557373, 0.08660250902175903], [-5.587935447692871e-09, 0.025881975889205933, 0.09659254550933838]], "faces": []}
|
||||
mesh.from_pydata(numpy.array(sphere['vertices'])*[boneLength, boneLength, boneLength] , sphere['edges'], sphere['faces'])
|
||||
|
||||
return obj
|
||||
|
||||
def add_driver(obj, posebone, control, target, path, multiply = ''):
|
||||
|
||||
if isinstance(target, tuple):
|
||||
attr = posebone.driver_add(target[0], target[1])
|
||||
else:
|
||||
attr = posebone.driver_add(target)
|
||||
|
||||
var = attr.driver.variables.new()
|
||||
|
||||
var.targets[0].id = obj
|
||||
var.targets[0].data_path = 'pose.bones["' + control +'"].'+ path
|
||||
attr.driver.expression = var.name + multiply
|
||||
|
||||
def dup_values(source, target):
|
||||
if hasattr(source, 'parent'):
|
||||
target.parent = source.parent
|
||||
for prop in dir(source):
|
||||
if not hasattr(target, prop):
|
||||
continue
|
||||
value = getattr(source, prop)
|
||||
if type(value) not in {int, float, bool, str, Vector, Matrix, bpy.types.Object}:
|
||||
continue
|
||||
if '__' in prop[:2] and '__' in prop[-2:]:
|
||||
continue
|
||||
if target.is_property_readonly(prop):
|
||||
continue
|
||||
setattr(target, prop, value)
|
||||
|
||||
return target
|
||||
|
||||
def dup_constraints(source, target):
|
||||
if not source.constraints.items():
|
||||
return
|
||||
for source_con in source.constraints:
|
||||
target_con = target.constraints.new(source_con.type)
|
||||
dup_values(source_con, target_con)
|
||||
|
||||
def add_vis_bone_con(obj, bone_vis_name, bone_wgt_name):
|
||||
bone_vis = obj.pose.bones[bone_vis_name]
|
||||
con = bone_vis.constraints.new('STRETCH_TO')
|
||||
con.target = obj
|
||||
con.subtarget = bone_wgt_name
|
||||
|
||||
return bone_vis
|
||||
|
||||
class target:
|
||||
def __init__(self, bone):
|
||||
self.name = bone.name
|
||||
self.point = tuple(bone.tail)
|
||||
self.ctrl = 'TRGT_' + bone.name
|
||||
if bone.parent:
|
||||
self.parent = bone.parent.name
|
||||
#print('assign parent to target ', self.name, self.ctrl, self.parent)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.point < other.point
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.point)
|
||||
|
||||
def __eq__(self, other):
|
||||
#if not isinstance(other, type(self)):
|
||||
# return NotImplemented
|
||||
return self.point == other.point
|
||||
|
||||
class parent:
|
||||
def __init__(self, bone):
|
||||
self.name = bone.name
|
||||
self.point = tuple(bone.head)
|
||||
self.ctrl = 'CTRL_' + bone.name
|
||||
if bone.parent:
|
||||
self.parent = bone.parent.name
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.point < other.point
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.point)
|
||||
|
||||
def __eq__(self, other):
|
||||
#if not isinstance(other, type(self)):
|
||||
# return NotImplemented
|
||||
return self.point == other.point
|
||||
|
||||
class constraint_dup:
|
||||
def __init__(self, bone, con):
|
||||
self.name = con.name
|
||||
self.target = con.target
|
||||
self.subtarget = con.subtarget
|
||||
self.bone = bone.name
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.bone)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.bone == other.bone
|
||||
|
||||
def bone_orientation(source, target, value):
|
||||
source.align_orientation(target)
|
||||
x, y, z = source.matrix.to_3x3().col
|
||||
R = (Matrix.Translation(source.head) @ Matrix.Rotation(radians(value), 4, x) @ Matrix.Translation(-source.head))
|
||||
source.transform(R, roll = False)
|
||||
source.align_roll(target.vector)
|
||||
|
||||
def find_ctrl(bone, controls):
|
||||
i = list(controls).index(bone)
|
||||
bone.ctrl = list(controls)[i].ctrl
|
||||
|
||||
return bone.ctrl
|
||||
|
||||
def add_controlers(self, obj, parents, targets):
|
||||
|
||||
#controls = set(parents).union(targets)
|
||||
controls = set(parents + targets)
|
||||
|
||||
#create hierarchy
|
||||
for bone in controls:
|
||||
editbone = obj.data.edit_bones[bone.name]
|
||||
if editbone.parent is None:
|
||||
continue
|
||||
parentnames = [bone.name for bone in parents]
|
||||
#if a target and its parent are part of the hierarchy then linked to its own bone parent
|
||||
if bone in targets and bone not in parents and editbone.parent.name in parentnames:
|
||||
parentbone = parent(editbone)
|
||||
else:
|
||||
parentbone = parent(editbone.parent)
|
||||
|
||||
if parentbone in controls and parentbone != bone:
|
||||
bone.parent = find_ctrl(parentbone, controls)
|
||||
else:
|
||||
bone.parent = editbone.parent.name
|
||||
|
||||
|
||||
edit_bones = obj.data.edit_bones
|
||||
for bone in controls:
|
||||
editbone = edit_bones[bone.name]
|
||||
ctrl = obj.data.edit_bones.new(bone.ctrl)
|
||||
ctrl.head = bone.point
|
||||
ctrl.tail = bone.point
|
||||
ctrl.tail[2] = bone.point[2] + (editbone.length / 3)
|
||||
ctrl.bbone_x = editbone.bbone_x
|
||||
ctrl.bbone_z = editbone.bbone_z
|
||||
ctrl.use_deform = False
|
||||
if self.bone_align:
|
||||
angle = 90 if self.align_90 else 0
|
||||
bone_orientation(ctrl, editbone, angle)
|
||||
if angle == 90:
|
||||
ctrl.align_roll(editbone.vector)
|
||||
else:
|
||||
ctrl.roll = editbone.roll
|
||||
|
||||
#apply hierarchy
|
||||
for bone in parents:
|
||||
editbone = edit_bones[bone.name]
|
||||
ctrl_name = find_ctrl(bone, controls)
|
||||
ctrl = edit_bones[ctrl_name]
|
||||
editbone.parent = ctrl
|
||||
|
||||
for bone in controls:
|
||||
ctrl = edit_bones[bone.ctrl]
|
||||
if hasattr(bone, 'parent'):
|
||||
ctrl.parent = edit_bones[bone.parent]
|
||||
|
||||
return controls
|
||||
|
||||
def pose_bbone_setup(bone, posebone, bbone_group = None):
|
||||
#add the custom shape to the widget bones
|
||||
custom_shape = draw_wgt(bone['length'], posebone)
|
||||
posebone.custom_shape = custom_shape
|
||||
posebone.use_custom_shape_bone_size = False
|
||||
if bbone_group:
|
||||
posebone.bone_group = bbone_group
|
||||
posebone.rotation_mode = 'XZY'
|
||||
posebone.lock_rotation[0] = True
|
||||
posebone.lock_rotation[2] = True
|
||||
|
||||
#####MAIN####
|
||||
class BboneWidgets(bpy.types.Operator):
|
||||
"""Add Bbone widget controls to the selected bones"""
|
||||
bl_idname = "armature.add_bbone_widgets"
|
||||
bl_label = "Add_Bbone_widgets"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.context.object.type == 'ARMATURE'
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
|
||||
obj.data.display_type = 'BBONE'
|
||||
|
||||
bones = []
|
||||
parentlayers = [False if i != 24 else True for i in range(32)]
|
||||
wgtlayers = [True if i == 0 else False for i in range(32)]
|
||||
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
obj.data.use_mirror_x = False
|
||||
for bone in obj.data.edit_bones:
|
||||
if not bone.select:
|
||||
continue
|
||||
if bone.bbone_segments == 1:
|
||||
bone.bbone_segments = 10
|
||||
bone.bbone_handle_type_start = 'TANGENT'
|
||||
bone.bbone_handle_type_end = 'TANGENT'
|
||||
bone_name = bone.name
|
||||
#add parent bone to the Bbone widgets
|
||||
parent = obj.data.edit_bones.new('WGTB_parent_'+ bone_name)
|
||||
parent_name = parent.name
|
||||
dup_values(bone, parent)
|
||||
parent.name = parent_name
|
||||
parent.select = False
|
||||
parent.select_head = False
|
||||
parent.select_tail = False
|
||||
|
||||
#change layer of the parent bone
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
parent.layers = parentlayers
|
||||
|
||||
#add bbone widget bones
|
||||
head_widget = obj.data.edit_bones.new('Bhead_'+ bone.name)
|
||||
head_widget.parent = parent
|
||||
#head_widget.head = bone.head
|
||||
head_widget.head = bone.head + (bone.tail - bone.head) * 0.25
|
||||
#head_widget.tail = (bone.tail - bone.head)/10
|
||||
head_widget.length = bone.length * 0.1
|
||||
head_widget.bbone_x = bone.bbone_x
|
||||
head_widget.bbone_z = bone.bbone_z
|
||||
head_widget.align_orientation(bone)
|
||||
head_widget.inherit_scale = 'NONE'
|
||||
head_name = head_widget.name
|
||||
|
||||
tail_widget = obj.data.edit_bones.new('Btail_'+ bone.name)
|
||||
tail_widget.parent = parent
|
||||
#tail_widget.head = bone.tail
|
||||
tail_widget.head = bone.head + (bone.tail - bone.head) * 0.75
|
||||
#tail_widget.tail = bone.tail - (bone.tail - bone.head)/10
|
||||
tail_widget.length = bone.length * 0.1
|
||||
tail_widget.bbone_x = bone.bbone_x
|
||||
tail_widget.bbone_z = bone.bbone_z
|
||||
tail_widget.align_orientation(bone)
|
||||
tail_widget.inherit_scale = 'NONE'
|
||||
tail_name = tail_widget.name
|
||||
|
||||
#add vis bones
|
||||
head_vis = obj.data.edit_bones.new('Bhead_vis_'+ bone.name)
|
||||
head_vis.parent = parent
|
||||
head_vis.head = bone.head
|
||||
head_vis.tail = head_widget.head
|
||||
head_vis.bbone_x = bone.bbone_x*0.1
|
||||
head_vis.bbone_z = bone.bbone_z*0.1
|
||||
|
||||
#head_vis_name = head_vis.name
|
||||
head_vis.hide_select = True
|
||||
head_vis.use_deform = False
|
||||
tail_vis = obj.data.edit_bones.new('Btail_vis_'+ bone.name)
|
||||
tail_vis.parent = parent
|
||||
tail_vis.head = bone.tail
|
||||
tail_vis.tail = tail_widget.head
|
||||
tail_vis.bbone_x = bone.bbone_x*0.1
|
||||
tail_vis.bbone_z = bone.bbone_z*0.1
|
||||
|
||||
#tail_vis_name = tail_vis.name
|
||||
tail_vis.hide_select = True
|
||||
|
||||
tail_vis.use_deform = False
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
tail_widget.layers = wgtlayers
|
||||
head_widget.layers = wgtlayers
|
||||
head_vis.layers = wgtlayers
|
||||
tail_vis.layers = wgtlayers
|
||||
|
||||
bones.append({'name': bone_name, 'parent': parent_name, 'head': head_name, 'tail': tail_name, 'head_vis': head_vis.name, 'tail_vis': tail_vis.name, 'length': bone.length})
|
||||
|
||||
#####POSE MODE#######
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
bone_groups = obj.pose.bone_groups
|
||||
if 'BBone Widgets' not in bone_groups:
|
||||
bbone_group = bone_groups.new(name = 'BBone Widgets')
|
||||
bbone_group.color_set = 'THEME09'
|
||||
else:
|
||||
bbone_group = bone_groups['BBone Widgets']
|
||||
else:
|
||||
bbone_group = None
|
||||
|
||||
for bone in bones:
|
||||
posebone = obj.pose.bones[bone['name']]
|
||||
# Prepare parent bone in pose mode
|
||||
poseparent = obj.pose.bones[bone['parent']]
|
||||
#disable use deform
|
||||
obj.data.bones[bone['parent']].use_deform = False
|
||||
obj.data.bones[bone['head']].use_deform = False
|
||||
obj.data.bones[bone['tail']].use_deform = False
|
||||
|
||||
pose_bbone_setup(bone, obj.pose.bones[bone['head']], bbone_group)
|
||||
pose_bbone_setup(bone, obj.pose.bones[bone['tail']], bbone_group)
|
||||
|
||||
dup_constraints(posebone, poseparent)
|
||||
|
||||
#add all the drivers
|
||||
add_driver(obj, posebone, bone['head'], 'bbone_curveinx', 'location.x')
|
||||
add_driver(obj, posebone, bone['head'], 'bbone_curveinz', 'location.z')
|
||||
add_driver(obj, posebone, bone['head'], 'bbone_easein', 'location.y', '*5/'+ str(bone['length']))
|
||||
add_driver(obj, posebone, bone['head'], 'bbone_rollin', 'rotation_euler.y')
|
||||
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 0), 'scale.x')
|
||||
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 1), 'scale.y')
|
||||
add_driver(obj, posebone, bone['head'], ('bbone_scalein', 2), 'scale.z')
|
||||
|
||||
add_driver(obj, posebone, bone['tail'], 'bbone_curveoutx', 'location.x')
|
||||
add_driver(obj, posebone, bone['tail'], 'bbone_curveoutz', 'location.z')
|
||||
add_driver(obj, posebone, bone['tail'], 'bbone_easeout', 'location.y', '*-5/'+ str(bone['length']))
|
||||
add_driver(obj, posebone, bone['tail'], 'bbone_rollout', 'rotation_euler.y')
|
||||
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 0), 'scale.x')
|
||||
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 1), 'scale.y')
|
||||
add_driver(obj, posebone, bone['tail'], ('bbone_scaleout', 2), 'scale.z')
|
||||
|
||||
#add constraints to visual bones
|
||||
head_vis = add_vis_bone_con(obj, bone['head_vis'], bone['head'])
|
||||
tail_vis = add_vis_bone_con(obj, bone['tail_vis'], bone['tail'])
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
head_vis.bone_group = bbone_group
|
||||
tail_vis.bone_group = bbone_group
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class ChainControls(bpy.types.Operator):
|
||||
"""Add parent and target controls to the selected bones to create a chain control"""
|
||||
bl_idname = "armature.add_chain_ctrls"
|
||||
bl_label = "Add_Chain_Controls"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
parents: bpy.props.BoolProperty(name = 'Add Parents', description = "Align the controls 90 degrees to the original bones", default = True)
|
||||
targets: bpy.props.BoolProperty(name = 'Add Targets', description = "Align the controls 90 degrees to the original bones", default = True)
|
||||
keep_hierarchy: bpy.props.BoolProperty(name = 'Keep Hierarchy', description = "Keep the controls in the hierarchy of the original bones", default = True)
|
||||
bone_align: bpy.props.BoolProperty(name = 'Align to Bones', description = "Align the controls to the original bones", default = True)
|
||||
align_90: bpy.props.BoolProperty(name = '+90°', description = "Align the controls 90 degrees to the original bones", default = True)
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.context.object.type == 'ARMATURE'
|
||||
def invoke(self, context, event):
|
||||
#obj = context.object
|
||||
wm = context.window_manager
|
||||
return wm.invoke_props_dialog(self, width = 200)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
row = layout.row()
|
||||
row.label(text = 'Add Control Bones')
|
||||
row = layout.row()
|
||||
row.prop(self, 'parents') #text = 'Size'
|
||||
row.prop(self, 'targets')
|
||||
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
col.prop(self, 'keep_hierarchy')
|
||||
row = layout.row()
|
||||
row.prop(self, 'bone_align')
|
||||
if self.bone_align:
|
||||
row.prop(self, 'align_90', toggle=True)
|
||||
|
||||
def execute(self, context):
|
||||
obj = context.object
|
||||
targets = []
|
||||
parents = []
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
edit_bones = bpy.context.selected_editable_bones
|
||||
#create list of parent and target objects
|
||||
for bone in edit_bones:
|
||||
bone.use_connect = False
|
||||
if self.targets:
|
||||
targets.append(target(bone))
|
||||
if self.parents:
|
||||
parents.append(parent(bone))
|
||||
|
||||
controls = add_controlers(self, obj, parents, targets)
|
||||
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
|
||||
#Add the bone group for the ctrls if doesn't exist
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
bone_groups = obj.pose.bone_groups
|
||||
if 'Ctrl Bones' not in bone_groups:
|
||||
ctrl_group = bone_groups.new(name = 'Ctrl Bones')
|
||||
ctrl_group.color_set = 'THEME01'
|
||||
else:
|
||||
ctrl_group = bone_groups['Ctrl Bones']
|
||||
|
||||
for bone in controls:
|
||||
posebone = obj.pose.bones[bone.ctrl]
|
||||
if bpy.app.version < (4, 0, 0):
|
||||
posebone.bone_group = ctrl_group
|
||||
else:
|
||||
posebone.color.palette = 'THEME01'
|
||||
|
||||
if self.targets:
|
||||
for bone in targets:
|
||||
#update from the controls set
|
||||
ctrl = find_ctrl(bone, controls)
|
||||
posebone = obj.pose.bones[bone.name]
|
||||
|
||||
con = posebone.constraints.new('STRETCH_TO')
|
||||
con.target = obj
|
||||
|
||||
con.subtarget = ctrl
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
class MergeRigs(bpy.types.Operator):
|
||||
"""Merge selected rigs to active and keep hierarchy and constraints for shared bones"""
|
||||
bl_idname = "armature.merge"
|
||||
bl_label = "Merge_Rigs"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return bpy.context.object.type == 'ARMATURE'
|
||||
|
||||
def execute(self, context):
|
||||
target_obj = context.object
|
||||
|
||||
if target_obj.type != 'ARMATURE':
|
||||
return {"CANCELLED"}
|
||||
|
||||
target_bones = set([bone.name for bone in target_obj.data.bones])
|
||||
constraints = []
|
||||
childrens = {}
|
||||
|
||||
#Store children and constraints
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj.type != 'ARMATURE':
|
||||
continue
|
||||
if obj == target_obj:
|
||||
continue
|
||||
#create a set of all the similiar bones in all the rigs
|
||||
obj_bones = set([bone.name for bone in obj.data.bones])
|
||||
shared_bones = target_bones.intersection(obj_bones)
|
||||
|
||||
#find all the constraints and children
|
||||
for bone in obj.pose.bones:
|
||||
#store all the constraints
|
||||
for con in bone.constraints:
|
||||
if not hasattr(con, 'subtarget'):
|
||||
continue
|
||||
if con.target == obj and con.subtarget in shared_bones:
|
||||
constraints.append(constraint_dup(bone, con))
|
||||
if bone.name in shared_bones:
|
||||
for child in bone.children:
|
||||
if child.name in childrens:
|
||||
continue
|
||||
childrens.update({child.name : bone.name})
|
||||
|
||||
#remove shared bones
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
for obj in bpy.context.selected_objects:
|
||||
if obj.type != 'ARMATURE':
|
||||
continue
|
||||
if obj == target_obj:
|
||||
continue
|
||||
for bone in shared_bones:
|
||||
if bone not in obj.data.edit_bones:
|
||||
continue
|
||||
obj.data.edit_bones.remove(obj.data.edit_bones[bone])
|
||||
|
||||
bpy.ops.object.mode_set(mode = 'POSE')
|
||||
bpy.ops.object.join()
|
||||
|
||||
#restore constraints
|
||||
for con_dup in constraints:
|
||||
if con_dup.bone in target_bones:
|
||||
continue
|
||||
if con_dup.bone not in target_obj.pose.bones:
|
||||
continue
|
||||
#print('constraint on ',con_dup.bone, con_dup.name)
|
||||
posebone = target_obj.pose.bones[con_dup.bone]
|
||||
if con_dup.name not in posebone.constraints:
|
||||
continue
|
||||
con = posebone.constraints[con_dup.name]
|
||||
con.target = target_obj
|
||||
con.subtarget = con_dup.subtarget
|
||||
|
||||
#reparent all child bones
|
||||
bpy.ops.object.mode_set(mode = 'EDIT')
|
||||
for child, parent in childrens.items():
|
||||
target_obj.data.edit_bones[child].parent = target_obj.data.edit_bones[parent]
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
classes = (MergeRigs,BboneWidgets, ChainControls)
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
# bpy.utils.register_class(BboneWidgets)
|
||||
# bpy.utils.register_class(ChainControls)
|
||||
# bpy.utils.register_class(RiggerToolBox_PT_Panel)
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
||||
# bpy.utils.unregister_class(BboneWidgets)
|
||||
# bpy.utils.unregister_class(ChainControls)
|
||||
# bpy.utils.unregister_class(RiggerToolBox_PT_Panel)
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1,534 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
|
||||
bl_info = {
|
||||
"name": "AnimToolBox",
|
||||
"author": "Tal Hershkovich",
|
||||
"version" : (0, 0, 8),
|
||||
"blender" : (3, 2, 0),
|
||||
"location": "View3D - Properties - Animation Panel",
|
||||
"description": "A set of animation tools",
|
||||
"wiki_url": "",
|
||||
"category": "Animation"}
|
||||
|
||||
if "bpy" in locals():
|
||||
import importlib
|
||||
if "Rigger_Toolbox" in locals():
|
||||
importlib.reload(Rigger_Toolbox)
|
||||
if "TempCtrls" in locals():
|
||||
importlib.reload(TempCtrls)
|
||||
if "Tools" in locals():
|
||||
importlib.reload(Tools)
|
||||
if "Display" in locals():
|
||||
importlib.reload(Display)
|
||||
if "emp" in locals():
|
||||
importlib.reload(emp)
|
||||
if "multikey" in locals():
|
||||
importlib.reload(multikey)
|
||||
if "Rigger_Toolbox" in locals():
|
||||
importlib.reload(Rigger_Toolbox)
|
||||
if "ui" in locals():
|
||||
importlib.reload(ui)
|
||||
if "addon_updater_ops" in locals():
|
||||
importlib.reload(addon_updater_ops)
|
||||
|
||||
import bpy
|
||||
from . import addon_updater_ops
|
||||
from . import TempCtrls
|
||||
from . import Rigger_Toolbox
|
||||
from . import Tools
|
||||
from . import Display
|
||||
from . import emp
|
||||
from . import ui
|
||||
from . import multikey
|
||||
from . import Rigger_Toolbox
|
||||
from pathlib import Path
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
from bpy.app.handlers import persistent
|
||||
import os
|
||||
|
||||
|
||||
class TempCtrlsItems(bpy.types.PropertyGroup):
|
||||
#located at context.scene.btc.ctrl_items
|
||||
controlled: bpy.props.PointerProperty(name = "controlled object", description = "rigs and objects that are being controlled", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
controller: bpy.props.PointerProperty(name = "controller object", description = "rigs and objects that are controling", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
class TempCtrlsSceneSettings(bpy.types.PropertyGroup):
|
||||
#located at context.scene.btc
|
||||
root: bpy.props.BoolProperty(name = "Root Empty", description = "Add a root to the empties ", default = False, override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_prop)
|
||||
root_bone: bpy.props.StringProperty(name = "Root bone", description = "Root empty as a root bone ", override = {'LIBRARY_OVERRIDABLE'}, update = TempCtrls.root_update)
|
||||
root_object: bpy.props.PointerProperty(name = "Root object", description = "Root empty as a root object ", update = TempCtrls.root_update, type = bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
ctrl_type: bpy.props.EnumProperty(name = 'Controllers', description="Select empties or a bone with a new rig to bake to", items = [('BONE', 'Bone','Bake to bones','BONE_DATA', 0), ('EMPTY', 'Empty', 'Bake to empties', 'EMPTY_ARROWS', 1)])
|
||||
ctrl_items: bpy.props.CollectionProperty(type = TempCtrlsItems, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
|
||||
|
||||
bake_range_type: bpy.props.EnumProperty(name = 'Bake Range', description="Use either scene, actions length or custom frame range", default = 'KEYFRAMES', update= TempCtrls.update_range_type,
|
||||
items = [('SCENE', 'Scene Range', 'Bake to the scene range'), ('KEYFRAMES', 'Keyframes Range', 'Bake all the keyframes in the layers'), ('CUSTOM', 'Custom', 'Enter a custom frame range')], override = {'LIBRARY_OVERRIDABLE'})
|
||||
bake_range: bpy.props.IntVectorProperty(name='Frame Range', description='Bake to a custom frame range', size = 2, update= TempCtrls.update_bake_range)
|
||||
bake_layers: bpy.props.BoolProperty(name = "Bake Layers", description = "Use keyframes from all the layers to include in the bake", default = False)
|
||||
|
||||
target: bpy.props.EnumProperty(name = 'Affect', description="Cleanup created constraints and empties", default = 1,
|
||||
items = [('ALL', 'All Ctrl Rigs','Bake to all Ctrl Rigs', 0),
|
||||
('SELECTED', 'Selected Chains','Bake to only selected chain controlls', 1),
|
||||
('RELATIVE', 'Relative Ctrls Rig','Bake to the Relative Control rigs', 2)])
|
||||
# ('CONSTRAINTS', 'Constraints', 'Clean all the bone constraints', 3),
|
||||
# ('CONTROLLERS', 'Controllers', 'Remove all the baked empties', 4)])
|
||||
|
||||
selection: bpy.props.EnumProperty(name = 'Select', description="Select all controls, original bones or their relative", default = 'CONTROLLERS',
|
||||
items = [('RELATIVE_CTRLS', 'Relative Ctrls','Select the Relative controller to your current selection', 0),
|
||||
('RELATIVE_CONSTRAINED', 'Relative Constrained','Select the Relative original constrained bone to your current selection', 1),
|
||||
('CONTROLLERS', 'All Ctrls', 'Select all the controller bones or empties', 2), ('CONSTRAINED', 'All Constrained', 'Select all the original constrained bones', 3)])
|
||||
|
||||
#smartbake setting
|
||||
linksettings: bpy.props.BoolProperty(name = "Link Settings", description = "Link Settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bakesettings: bpy.props.BoolProperty(name = "bake settings", description = "bake settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
cleansettings: bpy.props.BoolProperty(name = "clean settings", description = "clean settings", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
smartbake: bpy.props.BoolProperty(name = "Smart Bake", description = "Keep Original Frame count", default = True, override = {'LIBRARY_OVERRIDABLE'})
|
||||
inbetween_keyframes: bpy.props.IntProperty(name = "Inbetween Keyframes", description = "Add inbetween keyframes", default = 0, override = {'LIBRARY_OVERRIDABLE'})
|
||||
from_origin: bpy.props.BoolProperty(name = "From Origin", description = "Use Keyframes from Original Bone", default = True, override = {'LIBRARY_OVERRIDABLE'})
|
||||
from_ctrl: bpy.props.BoolProperty(name = "From Controller", description = "Use Keyframes from Controller Bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
clean_ctrls: bpy.props.BoolProperty(name = "Remove Ctrls", description = "Remove Controls", default = True, override = {'LIBRARY_OVERRIDABLE'})
|
||||
clean_constraints: bpy.props.BoolProperty(name = "Remove Constraints", description = "Remove Constraints", default = True, override = {'LIBRARY_OVERRIDABLE'})
|
||||
rebake_to_org: bpy.props.BoolProperty(name = "ReBake connections to original bones", description = "ReBake ctrls from connected chains current anim to the original bones", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
link_to: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 1,
|
||||
items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
|
||||
# link_from: bpy.props.EnumProperty(name = 'Link to Chain', description="Link to begining of an active chain or the tip of the chain", default = 0,
|
||||
# items = [('BASE', 'Base','Link to the base of the active chain', 0), ('TIP', 'Tip', 'Link to the tip of the chain', 1)])
|
||||
|
||||
shape_size: bpy.props.FloatProperty(name='Size', description="Multiple factor for the shape size of the temp controls", update = TempCtrls.tempctrl_shapesize, min = 0.001, default = 1.5, override = {'LIBRARY_OVERRIDABLE'})#
|
||||
shape_type: bpy.props.EnumProperty(name = 'Shape Type', description="Display type for the controls", items = TempCtrls.ctrl_shape_items, update = TempCtrls.tempctrl_shape_type)
|
||||
color_set: bpy.props.EnumProperty(name="Bone Color Set", description="Choose a bone color set", items = TempCtrls.get_bone_color_sets, update = TempCtrls.update_bone_color, default = 9)
|
||||
|
||||
add_ik_ctrl: bpy.props.BoolProperty(name = 'Add an Extra IK Ctrl Bone', description = "Adds an extra bone ctrl as the ik ctrl", default = False, update = TempCtrls.add_ik_prop)
|
||||
pole_target: bpy.props.BoolProperty(name = 'Add Pole Target', description = "Adding Pole Target to the IK Chain", default = True, update = TempCtrls.pole_prop)
|
||||
pole_offset: bpy.props.FloatProperty(name="Offset", description="Offset the bone in the axis direction", default=1.0, update = TempCtrls.pole_offset)
|
||||
child: bpy.props.BoolProperty(name = 'Add extra child Ctrls', description = "Add an child control for an overlay control", default = False, update = TempCtrls.child_prop)
|
||||
orientation: bpy.props.BoolProperty(name = 'Use World Space Orientation', description = "Orient the bones to world space instead of to the original bones", default = True)
|
||||
|
||||
# enabled: bpy.props.BoolProperty(name = 'Switch On / Off', description = "Enabling and Disabling Temp Ctrls influence", default = True)
|
||||
|
||||
class TempCtrlsBoneSettings(bpy.types.PropertyGroup):
|
||||
#located at obj.pose.bones[##].btc
|
||||
root: bpy.props.BoolProperty(name = "Root Bone", description = "Bone is marked as the root bone", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
child: bpy.props.BoolProperty(name = "Child Bone", description = "Bone is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
|
||||
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
|
||||
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
|
||||
items = [('NONE', 'No Setup','No Setup Applied', 0),
|
||||
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
|
||||
('WORLDSPACE_CURSOR', 'World Space Cursor Ctrl','World Space Cursor pivot', 2),
|
||||
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 3),
|
||||
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 4),
|
||||
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 5),
|
||||
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 6),
|
||||
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 7),
|
||||
('ROOT', 'Root', 'Root Ctrl for all the setups', 8),
|
||||
('EMPTY', 'Root', 'Root Ctrl for all the setups', 9),
|
||||
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 10),
|
||||
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 11)])
|
||||
|
||||
#using org mostly to decide if it needs a custom shape
|
||||
org: bpy.props.EnumProperty(name = 'Org Type', description="Describes what if the function of the bone", override = {'LIBRARY_OVERRIDABLE'},
|
||||
items = [('CTRL', 'Controller Bone','Controller Bone', 0),
|
||||
('ORG', 'Original Bone','Original Bone', 1),
|
||||
('MCH', 'Mechanical Bone','Mechanical Bone', 2),
|
||||
('NONE', 'Nothing applied','Nothing applied', 3)])
|
||||
|
||||
shape: bpy.props.BoolProperty(name = "Apply shape", description = "Mark if the bone needs a shape", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
class TempCtrlsOrgIds(bpy.types.PropertyGroup):
|
||||
# A collection of the org ids used in each setup. Org ID is the direct connection
|
||||
# between original bones and ctrls
|
||||
pass
|
||||
|
||||
class TempCtrlsObjectSetups(bpy.types.PropertyGroup):
|
||||
#located at obj.animtoolbox.ctrl_setups
|
||||
#name: using string of the id
|
||||
setup: bpy.props.EnumProperty(name = 'Setup Type', description="Describes what kind of setup the bone is part of",
|
||||
items = [('NONE', 'No Setup','No Setup Applied', 0),
|
||||
('WORLDSPACE', 'World Space Ctrl','World Space Ctrl setup', 1),
|
||||
('WORLDSPACE_CURSOR', 'World Space Cursor Ctrl','World Space Cursor pivot', 2),
|
||||
('TEMPFK', 'Temporary FK setup','Temporary FK chain setup', 3),
|
||||
('TEMPFK_FLIP', 'Temporary flipped FK setup','Temporary flipped FK chain setup', 4),
|
||||
('TEMPIK', 'Temporary IK setup','Temporary IK setup', 5),
|
||||
('POLE', 'Temporary IK Pole setup','Temporary IK Pole', 6),
|
||||
('PARENTCTRL', 'Parent Ctrl from cursor setup','Parent Ctrl from cursor setup', 7),
|
||||
('ROOT', 'Root', 'Root Ctrl for all the setups', 8),
|
||||
('EMPTY', 'Root', 'Root Ctrl for all the setups', 9),
|
||||
('TRACK_TO', 'Track To','World Space Track to Ctrl setup', 10),
|
||||
('TRACK_TO_EMPTY', 'Track To Empty','World Space Track to Empty Ctrl setup', 11)])
|
||||
|
||||
org_ids: bpy.props.CollectionProperty(type = TempCtrlsOrgIds)
|
||||
|
||||
class MultikeyProperties(bpy.types.PropertyGroup):
|
||||
|
||||
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
|
||||
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
|
||||
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = multikey.scale_value)
|
||||
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = multikey.random_value)
|
||||
|
||||
class AnimToolBoxObjectSettings(bpy.types.PropertyGroup):
|
||||
|
||||
controlled: bpy.props.PointerProperty(name = 'Controlled Rig', description="Adding the rig object that is being controlled by the current object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
controller: bpy.props.PointerProperty(name = 'Controller Rig', description="Adding the rig object that is used as the temp control object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
ctrl_setups: bpy.props.CollectionProperty(type = TempCtrlsObjectSetups, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
|
||||
ctrls_enabled: bpy.props.BoolProperty(name = 'Temp Ctrls Switch', description = "Enabling and Disabling Temp Ctrls influence", default = True)
|
||||
# influence: bpy.props.FloatProperty(name = "Influence Slider for the Temp Ctrls", description = "Switching the influence slider for the temp ctrls", default = 1, min = 0.0, max = 1.0)
|
||||
|
||||
#Used for Bake to Empties
|
||||
# org_id: bpy.props.IntProperty(name = "Originate ID", description = "ID number of the bone the ctrl originates from", override = {'LIBRARY_OVERRIDABLE'})
|
||||
setup_id: bpy.props.IntProperty(name = "Setup ID", description = "ID number of the current chain setup", override = {'LIBRARY_OVERRIDABLE'})
|
||||
root: bpy.props.BoolProperty(name = "Root Empty", description = "Empty is marked as the root ctrl", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
child: bpy.props.BoolProperty(name = "Child Empty", description = "Empty is marked as a child bone inside the setup", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", default = 0)
|
||||
|
||||
class IsolatedRigs(bpy.types.PropertyGroup):
|
||||
|
||||
hidden: bpy.props.PointerProperty(name = "Hidden Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
selected: bpy.props.PointerProperty(name = "Selected Rigs", description = "List of Rigs that are hidden during pose mode isolation", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
class AnimToolBoxUILayout(bpy.types.PropertyGroup):
|
||||
'''Layout properties for the UI'''
|
||||
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
|
||||
copy_paste_matrix: bpy.props.BoolProperty(name = "Copy Matrix Menu", description = "Opens the menu for copy paste matrices", default = True)
|
||||
copy_paste_world: bpy.props.BoolProperty(name = "Copy Paste World Matrix", description = "Copy and Paste the World Matrix", default = False, update = Tools.copy_paste_world_update)
|
||||
copy_paste_relative: bpy.props.BoolProperty(name = "Copy Paste Relative Matrix", description = "Copy and Paste the Matrix relative to the active bone", default = False, update = Tools.copy_paste_relative_update)
|
||||
Inbetweens: bpy.props.BoolProperty(name = "Blendings/Inbetweens", description = "Opens the menu for Inbetweens", default = True)
|
||||
gizmo_size: bpy.props.BoolProperty(name = "Gizmo size", description = "Change the Gizmo size using alt +/- hotkeys", default = False)
|
||||
# temp_ctrls: bpy.props.BoolProperty(name = "Temp Ctrls", description = "Open Temp Ctrls", default = False)
|
||||
temp_ctrls_switch: bpy.props.BoolProperty(name = "Temp Ctrls Switch", description = "Temp Ctrls Switch", default = True)
|
||||
temp_ctrls_shapes: bpy.props.BoolProperty(name = "Temp Ctrls Shapes", description = "Temp Ctrls Shapes", default = True)
|
||||
|
||||
markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
relative_cursor: bpy.props.BoolProperty(name = "Relative Cursor Mode", description = "Cursor moves relative to the selection", default = False)
|
||||
|
||||
is_dragging: bpy.props.BoolProperty(default = False)
|
||||
#using Blending sliders in the window manager to avoid undo issues with modal operators
|
||||
inbetween_worldmatrix: bpy.props.FloatProperty(name='Inbetween World Matrix', description="Adds an inbetween of the World Matrix to the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, update = Tools.add_inbetween_worldmatrix, override = {'LIBRARY_OVERRIDABLE'})
|
||||
blend_mirror: bpy.props.FloatProperty(name='Blend Mirror', description="Blend into the mirrored pose", soft_min = 0, soft_max = 1, default=0, step = 1, update = Tools.blend_to_mirror, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
multikey: bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
class AnimToolBoxGlobalSettings(bpy.types.PropertyGroup):
|
||||
#context.scene.animtoolbox
|
||||
marker_frame_range: bpy.props.BoolProperty(name = "Marker Frame Range", description = "Flag when marker frame range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bake_frame_range: bpy.props.BoolProperty(name = "Bake Frame Range", description = "Flag when marker bake range turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# markers_retimer: bpy.props.BoolProperty(name = "Marker Retimer", description = "Flag when marker retimer turned on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
keyframes_offset: bpy.props.FloatProperty(name = "Keyframes Offset", description = "Interactive slider to offset keyframes back and forth ", soft_max = 2, soft_min = -2, default = 0, update = Tools.keyframes_offset_slider)
|
||||
rotation_mode: bpy.props.EnumProperty(name = 'Rotation Mode', description="Describes what kind of setup the bone is part of", override = {'LIBRARY_OVERRIDABLE'},
|
||||
items = [('QUATERNION', 'Quaternion','Quaternion Rotation Order - No Gimbal Lock', 0),
|
||||
('XYZ', 'XYZ', 'XYZ Rotation Order', 1), ('XZY', 'XZY','XZY Rotation Order', 2),
|
||||
('YXZ', 'YXZ','YXZ Rotation Order', 3), ('YZX', 'YZX', 'YZX Rotation Order', 4),
|
||||
('ZXY', 'ZXY', 'ZXY Rotation Order', 5), ('ZYX', 'ZYX', 'ZYX Rotation Order', 6),
|
||||
('AXIS_ANGLE', 'AXIS_ANGLE', 'Axis Angle Rotation Order', 7)])
|
||||
|
||||
isolate_pose_mode: bpy.props.BoolProperty(name = "Isolate rig in pose mode", description = "Isolates the rig during pose mode, rigs in object mode are hidden", default = False)
|
||||
active_obj: bpy.props.PointerProperty(name = "Active Object", description = "Current Active Object", type=bpy.types.Object, override = {'LIBRARY_OVERRIDABLE'})
|
||||
isolated: bpy.props.CollectionProperty(type = IsolatedRigs, override = {'LIBRARY_OVERRIDABLE', 'USE_INSERTION'})
|
||||
|
||||
#Blendings
|
||||
inbetweener : bpy.props.FloatProperty(name='Inbetween Keyframe', description="Adds an inbetween Keyframe between the Layer's neighbor keyframes", soft_min = -1, soft_max = 1, default=0.0, options = set(), update = Tools.add_inbetween_key, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
gizmo_size: bpy.props.IntProperty(name = "Add to Gizmo Size", description = "Addition to Gizmo Size", max = 100, min = -100, default = 10)
|
||||
|
||||
#Copy/Pase Matrix
|
||||
range_type: bpy.props.EnumProperty(name = 'Paste to Frames', description="Paste to current frame or a range of frames.", update = Tools.bake_range_type,
|
||||
items = [('CURRENT', 'Current Frame','Paste Matrix to only current frame', 0),
|
||||
('SELECTED', 'Selected Keyframe','Paste Matrix to only selected keyframes', 1),
|
||||
('RANGE', 'Frame Range','Paste Matrix to a Frame Range', 2)])
|
||||
bake_frame_start: bpy.props.IntProperty(name = "Bake Frame Start", description = "Define the start frame to paste the matrix", min = 0, update = Tools.bake_frame_start_limit)
|
||||
bake_frame_end: bpy.props.IntProperty(name = "Bake Frame End", description = "Define the end frame to paste the matrix", min = 0, update = Tools.bake_frame_end_limit)
|
||||
|
||||
filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
|
||||
filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(False, False, False, False), size = 4, options={'HIDDEN'}, update = Tools.filter_name_update)
|
||||
filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(False, False, False), size = 3, options={'HIDDEN'}, update = Tools.filter_name_update)
|
||||
#The name displayed on the filter button
|
||||
filter_name: bpy.props.StringProperty(name="Filter Name", description="Change the name of the button while chaging the filter options", default= "", options={'HIDDEN'})
|
||||
filter_custom_props: bpy.props.BoolProperty(name = "Filter Custom Properties", description = "Filter custom properties", default = False)
|
||||
filter_keyframes: bpy.props.BoolProperty(name = "Filter Aelected Keyframes", description = "Filter selected keyframes for specific tools", default = False)
|
||||
|
||||
col_vis: bpy.props.BoolProperty(name = "Animated collections visibility", description = "Display if animated collections are turned on or off", default = False)
|
||||
|
||||
@addon_updater_ops.make_annotations
|
||||
class AnimToolBoxPreferences(bpy.types.AddonPreferences):
|
||||
# this must match the addon name, use '__package__'
|
||||
# when defining this in a submodule of a python package.
|
||||
bl_idname = __package__
|
||||
|
||||
category: bpy.props.StringProperty(
|
||||
name="Tab Category",
|
||||
description="Choose a name for the category of the panel",
|
||||
default="Animation",
|
||||
update=ui.update_panel
|
||||
)
|
||||
|
||||
quick_menu: bpy.props.BoolProperty(name = "Use Quick Menu", description = "Opens header menu with only icon", default = False)
|
||||
riggertoolbox: bpy.props.BoolProperty(name = "RiggerToolBox", description = "Include RiggerToolbox (experimental)", default = False, update = ui.add_riggertoolbox)
|
||||
multikey: bpy.props.BoolProperty(name = "Multikey", description = "Include Multikey for adju\sting multiply keyframes", default = False, update = ui.add_multikey)
|
||||
|
||||
#Temp Ctrls properties
|
||||
in_front: bpy.props.BoolProperty(name = "Always In Front", description = "Set Temp Ctrls to be always in front", default = True)
|
||||
clear_setup : bpy.props.BoolProperty(name = "Clear Selection Before Creating New Temp Ctrls", description = "Clear old setup when adding Temp ctrls to an existing chain", default = False)
|
||||
|
||||
#Editable motion path
|
||||
keyframes_range: bpy.props.IntProperty(name = "Keyframe Range", description = "The range of distance from the keyframes while hovering over them", min = 5, max = 100, default = 15)
|
||||
mp_pref: bpy.props.BoolProperty(name = "Editable Motion Path Colors Theme", description = "Set the Color them of editable motion path visualization", default = False)
|
||||
mp_keyframe_color: bpy.props.FloatVectorProperty(name="Keyframes", subtype='COLOR', default=(1.0, 1.0, 0.0, 1.0), size=4, min=0.0, max=1.0, description="Handles selection color")
|
||||
mp_handle_color: bpy.props.FloatVectorProperty(name="Handles", subtype='COLOR', default=(1.0, 0.8, 0.2, 1.0), size=4, min=0.0, max=1.0, description="Handles selection color")
|
||||
mp_remove_color: bpy.props.FloatVectorProperty(name="Remove Keyframes", subtype='COLOR', default=(0.0, 0.5, 1.0, 1.0), size=4, min=0.0, max=1.0, description="Keyframe color displayed before removing")
|
||||
mp_hover_color: bpy.props.FloatVectorProperty(name="Hover", subtype='COLOR', default=(1.0, 0.4, 0.2, 1.0), size=4, min=0.0, max=1.0, description="Color during Hovering")
|
||||
mp_handle_selection_color: bpy.props.FloatVectorProperty(name="Handles Selection", subtype='COLOR', default=(0.8, 0.65, 0.6, 0.8), size=4, min=0.0, max=1.0, description="Handles selection color")
|
||||
mp_key_selection_color: bpy.props.FloatVectorProperty(name="Keyframe Selection", subtype='COLOR', default=(0.8, 0.8, 0.6, 0.8), size=4, min=0.0, max=1.0, description="Keyframe selection color")
|
||||
|
||||
# addon updater preferences from `__init__`, be sure to copy all of them
|
||||
auto_check_update: bpy.props.BoolProperty(
|
||||
name = "Auto-check for Update",
|
||||
description = "If enabled, auto-check for updates using an interval",
|
||||
default = True,
|
||||
)
|
||||
|
||||
updater_interval_months: bpy.props.IntProperty(
|
||||
name='Months',
|
||||
description = "Number of months between checking for updates",
|
||||
default=0,
|
||||
min=0
|
||||
)
|
||||
updater_interval_days: bpy.props.IntProperty(
|
||||
name='Days',
|
||||
description = "Number of days between checking for updates",
|
||||
default=7,
|
||||
min=0,
|
||||
|
||||
)
|
||||
updater_interval_hours: bpy.props.IntProperty(
|
||||
name='Hours',
|
||||
description = "Number of hours between checking for updates",
|
||||
default=0,
|
||||
min=0,
|
||||
max=23
|
||||
)
|
||||
updater_interval_minutes: bpy.props.IntProperty(
|
||||
name='Minutes',
|
||||
description = "Number of minutes between checking for updates",
|
||||
default=0,
|
||||
min=0,
|
||||
max=59
|
||||
)
|
||||
|
||||
#Draw the UI in the preferences
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
addon_updater_ops.update_settings_ui(self, context)
|
||||
|
||||
row = layout.row()
|
||||
col = row.column()
|
||||
|
||||
col.label(text="Tab Category:")
|
||||
col.prop(self, "category", text="")
|
||||
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
col.prop(self, "quick_menu", text="Use Quick Icons Menu")
|
||||
|
||||
layout.separator()
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.label(text = 'Temp Ctrls: ')
|
||||
row = box.row()
|
||||
row.prop(self, 'clear_setup')
|
||||
row.prop(self, 'in_front')
|
||||
|
||||
layout.separator()
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
col.prop(self, 'mp_pref', icon = 'DOWNARROW_HLT', text = 'Editable Motion Path Preferences')
|
||||
if self.mp_pref:
|
||||
col.prop(self, 'keyframes_range', text = 'Keyframe Distance Range')
|
||||
col.label(text = 'Colors Theme')
|
||||
row = box.row()
|
||||
row.prop(self, 'mp_keyframe_color')
|
||||
row.prop(self, 'mp_handle_color')
|
||||
row.prop(self, 'mp_remove_color')
|
||||
row = box.row()
|
||||
row.prop(self, 'mp_hover_color')
|
||||
row.prop(self, 'mp_key_selection_color')
|
||||
row.prop(self, 'mp_handle_selection_color')
|
||||
|
||||
layout.separator()
|
||||
col = layout.column()
|
||||
col.label(text = 'Include Extras: ')
|
||||
row = layout.row()
|
||||
row.prop(self, "multikey", text="Multikey - Edit Multiply keyframes")
|
||||
row.prop(self, "riggertoolbox", text="RiggerToolBox (Experimental)")
|
||||
|
||||
@persistent
|
||||
def loadanimtoolbox_pre(self, context):
|
||||
scene = bpy.context.scene
|
||||
dns = bpy.app.driver_namespace
|
||||
if scene.animtoolbox.bake_frame_range:
|
||||
scene.animtoolbox.bake_frame_range = False
|
||||
|
||||
if scene.emp.motion_path:
|
||||
scene.emp.motion_path = False
|
||||
bpy.context.workspace.status_text_set(None)
|
||||
if 'mp_dh' in dns:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
|
||||
bpy.app.driver_namespace.pop('mp_dh')
|
||||
bpy.context.scene.emp.selected_keyframes = '{}'
|
||||
|
||||
if 'markers_retimer_dh' in dns:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(dns['markers_retimer_dh'], 'WINDOW')
|
||||
bpy.app.driver_namespace.pop('markers_retimer_dh')
|
||||
|
||||
#remove the motion path app handler if it's still inside
|
||||
if emp.mp_value_update in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(emp.mp_value_update)
|
||||
if emp.mp_frame_change in bpy.app.handlers.frame_change_post:
|
||||
bpy.app.handlers.frame_change_post.remove(emp.mp_frame_change)
|
||||
if emp.mp_undo_update in bpy.app.handlers.undo_pre:
|
||||
bpy.app.handlers.undo_pre.remove(emp.mp_undo_update)
|
||||
|
||||
@persistent
|
||||
def loadanimtoolbox_post(self, context):
|
||||
scene = bpy.context.scene
|
||||
dns = bpy.app.driver_namespace
|
||||
if scene.animtoolbox.isolate_pose_mode:
|
||||
if Display.isolate_pose_mode not in bpy.app.handlers.depsgraph_update_pre:
|
||||
bpy.app.handlers.depsgraph_update_pre.append(Display.isolate_pose_mode)
|
||||
|
||||
if scene.emp.motion_path:
|
||||
scene.emp.motion_path = False
|
||||
bpy.context.workspace.status_text_set(None)
|
||||
if 'mp_dh' in dns:
|
||||
bpy.types.SpaceView3D.draw_handler_remove(dns['mp_dh'], 'WINDOW')
|
||||
bpy.app.driver_namespace.pop('mp_dh')
|
||||
# Reset keyframe selection for motion paths it is not used in window manager
|
||||
# Because it is used for undo
|
||||
bpy.context.scene.emp.selected_keyframes = '{}'
|
||||
|
||||
Tools.selection_order(self, context)
|
||||
|
||||
classes = (TempCtrlsItems, TempCtrlsOrgIds, TempCtrlsObjectSetups, TempCtrlsSceneSettings,TempCtrlsBoneSettings, MultikeyProperties,
|
||||
IsolatedRigs, AnimToolBoxObjectSettings, AnimToolBoxUILayout, AnimToolBoxGlobalSettings) + ui.classes
|
||||
|
||||
addon_keymaps = []
|
||||
|
||||
def register():
|
||||
# Note that preview collections returned by bpy.utils.previews
|
||||
# are regular py objects - you can use them to store custom data.
|
||||
|
||||
addon_updater_ops.register(bl_info)
|
||||
register_class(AnimToolBoxPreferences)
|
||||
addon_updater_ops.make_annotations(AnimToolBoxPreferences) # to avoid blender 2.8 warnings
|
||||
TempCtrls.register()
|
||||
Tools.register()
|
||||
Display.register()
|
||||
emp.register()
|
||||
|
||||
ui.register_custom_icon()
|
||||
|
||||
for cls in classes:
|
||||
# print(cls)
|
||||
register_class(cls)
|
||||
|
||||
bpy.types.Scene.btc = bpy.props.PointerProperty(type = TempCtrlsSceneSettings, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bpy.types.PoseBone.btc = bpy.props.PointerProperty(type = TempCtrlsBoneSettings, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bpy.types.Object.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxObjectSettings, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bpy.types.Scene.animtoolbox = bpy.props.PointerProperty(type = AnimToolBoxGlobalSettings, override = {'LIBRARY_OVERRIDABLE'})
|
||||
bpy.types.WindowManager.atb_ui = bpy.props.PointerProperty(type = AnimToolBoxUILayout, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
ui.update_panel(None, bpy.context)
|
||||
ui.add_multikey(None, bpy.context)
|
||||
ui.add_riggertoolbox(None, bpy.context)
|
||||
|
||||
if loadanimtoolbox_pre not in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.append(loadanimtoolbox_pre)
|
||||
if loadanimtoolbox_post not in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.append(loadanimtoolbox_post)
|
||||
|
||||
if Tools.selection_order not in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.append(Tools.selection_order)
|
||||
|
||||
#Make sure TAB hotkey in the NLA goes into full stack mode
|
||||
wm = bpy.context.window_manager
|
||||
kc = wm.keyconfigs.addon
|
||||
km = kc.keymaps.new(name= '3D View', space_type= 'VIEW_3D')
|
||||
if 'view3d.gizmo_size_up' not in km.keymap_items:
|
||||
kmi = km.keymap_items.new('view3d.gizmo_size_up', type= 'NUMPAD_PLUS', value= 'PRESS', alt = True, repeat = True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
if 'view3d.gizmo_size_down' not in km.keymap_items:
|
||||
kmi = km.keymap_items.new('view3d.gizmo_size_down', type= 'NUMPAD_MINUS', value= 'PRESS', alt = True, repeat = True)
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
#Add Tools to the Toolbar
|
||||
bpy.utils.register_tool(ui.KeyframeOffsetTool, separator=True)
|
||||
|
||||
#Add tools to the menu
|
||||
bpy.types.VIEW3D_MT_editor_menus.append(ui.draw_menu)
|
||||
|
||||
def unregister():
|
||||
for pcoll in ui.preview_collections.values():
|
||||
bpy.utils.previews.remove(pcoll)
|
||||
ui.preview_collections.clear()
|
||||
|
||||
#addon_updater_ops.unregister()
|
||||
addon_updater_ops.unregister()
|
||||
unregister_class(AnimToolBoxPreferences)
|
||||
|
||||
TempCtrls.unregister()
|
||||
# Rigger_Toolbox.unregister()
|
||||
Tools.unregister()
|
||||
Display.unregister()
|
||||
emp.unregister()
|
||||
|
||||
ui.add_multikey(None, bpy.context)
|
||||
ui.add_riggertoolbox(None, bpy.context)
|
||||
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
||||
|
||||
bpy.utils.unregister_tool(ui.KeyframeOffsetTool)
|
||||
|
||||
#Remove the header menu ui
|
||||
bpy.types.VIEW3D_MT_editor_menus.remove(ui.draw_menu)
|
||||
|
||||
del bpy.types.Scene.btc
|
||||
# del bpy.types.Bone.btc
|
||||
del bpy.types.Object.animtoolbox
|
||||
del bpy.types.Scene.animtoolbox
|
||||
if hasattr(bpy.types.Object, 'keyframes_offset'):
|
||||
del bpy.types.Object.keyframes_offset
|
||||
if hasattr(bpy.types.PoseBone, 'keyframes_offset'):
|
||||
del bpy.types.PoseBone.keyframes_offset
|
||||
|
||||
if loadanimtoolbox_pre in bpy.app.handlers.load_pre:
|
||||
bpy.app.handlers.load_pre.remove(loadanimtoolbox_pre)
|
||||
if loadanimtoolbox_post in bpy.app.handlers.load_post:
|
||||
bpy.app.handlers.load_post.remove(loadanimtoolbox_post)
|
||||
if Tools.selection_order in bpy.app.handlers.depsgraph_update_post:
|
||||
bpy.app.handlers.depsgraph_update_post.remove(Tools.selection_order)
|
||||
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
if __name__ == "__main__":
|
||||
register()
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
-16
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"last_check": "2026-02-09 15:54:54.419194",
|
||||
"backup_date": "September-23-2025",
|
||||
"update_ready": true,
|
||||
"ignore": false,
|
||||
"just_restored": false,
|
||||
"just_updated": false,
|
||||
"version_text": {
|
||||
"link": "https://gitlab.com/api/v4/projects/45739913/repository/archive.zip?sha=7a9ad24a463bf1c8ae095ec853409979344e0738",
|
||||
"version": [
|
||||
0,
|
||||
2,
|
||||
3
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,527 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
|
||||
import bpy
|
||||
import random
|
||||
import numpy as np
|
||||
from mathutils import Quaternion
|
||||
from . import Tools
|
||||
|
||||
def attr_default(obj, fcu_key):
|
||||
#check if the fcurve source belongs to a bone or obj
|
||||
if fcu_key[0][:10] == 'pose.bones':
|
||||
transform = fcu_key[0].split('.')[-1]
|
||||
attr = fcu_key[0].split('"')[-2]
|
||||
bone = fcu_key[0].split('"')[1]
|
||||
|
||||
if bone in obj.pose.bones:
|
||||
source = obj.pose.bones[bone]
|
||||
#if the bone not found still calculate the default based on the path
|
||||
elif '.rotation_quaternion' in fcu_key[0]:
|
||||
return [1.0, 0.0, 0.0, 0.0]
|
||||
elif '.scale' in fcu_key[0]:
|
||||
return [1.0, 1.0, 1.0]
|
||||
else:
|
||||
return [0]
|
||||
|
||||
#in case of shapekey animation
|
||||
elif fcu_key[0][:10] == 'key_blocks':
|
||||
attr = fcu_key[0].split('"')[1]
|
||||
if attr not in obj.data.shape_keys.key_blocks:
|
||||
return [0]
|
||||
shapekey = obj.data.shape_keys.key_blocks[attr]
|
||||
return [0] if shapekey.slider_min <= 0 else shapekey.slider_min
|
||||
#in case of transforms in object mode
|
||||
else:# fcu_key[0] in transform_types:
|
||||
source = obj
|
||||
transform = fcu_key[0]
|
||||
|
||||
#check when it's transform property of Blender
|
||||
if transform in source.bl_rna.properties.keys():
|
||||
if hasattr(source.bl_rna.properties[transform], 'default_array'):
|
||||
if len(source.bl_rna.properties[transform].default_array) > fcu_key[1]:
|
||||
attrvalue = source.bl_rna.properties[transform].default_array
|
||||
return attrvalue
|
||||
|
||||
#in case of property on object
|
||||
elif len(fcu_key[0].split('"')) > 1:
|
||||
if fcu_key[0].split('"')[1] in obj.keys():
|
||||
attr = fcu_key[0].split('"')[1]
|
||||
|
||||
if 'attr' not in locals():
|
||||
return [0]
|
||||
|
||||
#since blender 3 access to custom property settings changed
|
||||
if attr in source:
|
||||
if not isinstance(source[attr], float) and not isinstance(source[attr], int):
|
||||
return [0]
|
||||
id_attr = source.id_properties_ui(attr).as_dict()
|
||||
attrvalue = id_attr['default']
|
||||
return [attrvalue]
|
||||
|
||||
return [0]
|
||||
|
||||
def store_handles(key):
|
||||
#storing the distance between the handles bezier to the key value
|
||||
handle_r = key.handle_right[1] - key.co[1]
|
||||
handle_l = key.handle_left[1] - key.co[1]
|
||||
|
||||
return handle_r, handle_l
|
||||
|
||||
def apply_handles(key, handle_r, handle_l):
|
||||
key.handle_right[1] = key.co[1] + handle_r
|
||||
key.handle_left[1] = key.co[1] + handle_l
|
||||
|
||||
def selected_bones_filter(obj, fcu_data_path):
|
||||
if not bpy.context.window_manager.atb_ui.multikey.selectedbones:
|
||||
#if not obj.als.onlyselected:
|
||||
return False
|
||||
if obj.mode != 'POSE':
|
||||
return True
|
||||
transform_types = ['location', 'rotation_euler', 'rotation_quaternion', 'scale']
|
||||
#filter selected bones if option is turned on
|
||||
bones = [bone.path_from_id() for bone in bpy.context.selected_pose_bones]
|
||||
if fcu_data_path.split('].')[0]+']' not in bones and fcu_data_path not in transform_types:
|
||||
return True
|
||||
|
||||
# def filter_properties(obj, fcu):
|
||||
# 'Filter the W X Y Z attributes of the transform properties'
|
||||
# transform = fcu.data_path.split('"].')[1] if obj.mode == 'POSE' else fcu.data_path
|
||||
# index = fcu.array_index
|
||||
# if 'rotation' in transform:
|
||||
# if transform == 'rotation_euler':
|
||||
# index -= 1
|
||||
# transform = 'rotation'
|
||||
# transform = 'filter_' + transform
|
||||
# if not hasattr(bpy.context.scene.multikey, transform):
|
||||
# return False
|
||||
# attr = getattr(bpy.context.scene.multikey, transform)
|
||||
# #print(fcu.data_path, index, transform, attr[index])
|
||||
# return True if attr[index] else False
|
||||
|
||||
def add_value(key, value):
|
||||
if key.select_control_point:
|
||||
#store handle values in relative to the keyframe value
|
||||
handle_r, handle_l = store_handles(key)
|
||||
|
||||
key.co[1] += value
|
||||
apply_handles(key, handle_r, handle_l)
|
||||
|
||||
#calculate the difference between current value and the fcurve value
|
||||
def add_diff(fcurves, path, current_value, eval_array):
|
||||
array_value = current_value - eval_array
|
||||
if not any(array_value):
|
||||
return
|
||||
|
||||
for i, value in enumerate(array_value):
|
||||
fcu = fcurves.find(path, index = i)
|
||||
if fcu is None or Tools.filter_properties(bpy.context.scene.animtoolbox, fcu):
|
||||
continue
|
||||
for key in fcu.keyframe_points:
|
||||
add_value(key, value)
|
||||
fcu.update()
|
||||
|
||||
class ScaleValuesOp(bpy.types.Operator):
|
||||
"""Modal operator used while scale value is running before release"""
|
||||
bl_idname = "animtoolbox.multikey_scale_value"
|
||||
bl_label = "Scale Values"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
def invoke(self, context, event):
|
||||
#reset the values for dragging
|
||||
self.stop = False
|
||||
ui = context.window_manager.atb_ui
|
||||
ui['is_dragging'] = True
|
||||
self.avg_value = dict()
|
||||
#dictionary of the keyframes and their original INITIAL values
|
||||
self.keyframes_values = dict()
|
||||
self.keyframes_handle_right = dict()
|
||||
self.keyframes_handle_left = dict()
|
||||
|
||||
#the average value for each fcurve
|
||||
self.keyframes_avg_value = dict()
|
||||
|
||||
for obj in context.selected_objects:
|
||||
if obj.animation_data.action is None:
|
||||
continue
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
||||
for fcu in fcurves:
|
||||
if obj.mode == 'POSE':
|
||||
if selected_bones_filter(obj, fcu.data_path):
|
||||
continue
|
||||
if Tools.filter_properties(context.scene.animtoolbox, fcu):
|
||||
continue
|
||||
|
||||
#avg and value list per fcurve
|
||||
avg_value = []
|
||||
value_list = []
|
||||
for key in fcu.keyframe_points:
|
||||
if key.select_control_point:
|
||||
value_list.append(key.co[1])
|
||||
self.keyframes_values.update({key : key.co[1]})
|
||||
self.keyframes_handle_right.update({key : key.handle_right[1]})
|
||||
self.keyframes_handle_left.update({key : key.handle_left[1]})
|
||||
|
||||
if len(value_list)>1:
|
||||
#the average value with the scale property added to it
|
||||
avg_value = sum(value_list) / len(value_list)
|
||||
|
||||
for key in fcu.keyframe_points:
|
||||
if key.select_control_point:
|
||||
self.keyframes_avg_value.update({key : avg_value})
|
||||
|
||||
if not self.keyframes_avg_value:
|
||||
ui['is_dragging'] = False
|
||||
ui.multikey['scale'] = 1
|
||||
Tools.redraw_areas(['VIEW_3D'])
|
||||
return {'CANCELLED'}
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
def modal(self, context, event):
|
||||
|
||||
ui = context.window_manager.atb_ui
|
||||
scale = ui.multikey.scale
|
||||
|
||||
#Quit the modal operator when the slider is released
|
||||
if self.stop:
|
||||
ui['is_dragging'] = False
|
||||
ui.multikey['scale'] = 1
|
||||
Tools.redraw_areas(['VIEW_3D'])
|
||||
#modal is being cancelled because of undo issue with the modal running through the property
|
||||
return {'FINISHED'}
|
||||
|
||||
if event.value == 'RELEASE': # Stop the modal on next frame. Don't block the event since we want to exit the field dragging
|
||||
self.stop = True
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
for key, key_value in self.keyframes_values.items():
|
||||
if not key.select_control_point:
|
||||
continue
|
||||
if key not in self.keyframes_avg_value:
|
||||
continue
|
||||
avg_value = self.keyframes_avg_value[key]
|
||||
handle_right_value = self.keyframes_handle_right[key]
|
||||
handle_left_value = self.keyframes_handle_left[key]
|
||||
|
||||
#add the value of the distance from the average * scale factor
|
||||
key.co[1] = avg_value + ((key_value - avg_value)*scale)
|
||||
key.handle_right[1] = avg_value + ((handle_right_value - avg_value)*scale)
|
||||
key.handle_left[1] = avg_value + ((handle_left_value - avg_value)*scale)
|
||||
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
def scale_value(self, context):
|
||||
|
||||
ui = context.window_manager.atb_ui
|
||||
if ui.is_dragging:
|
||||
return
|
||||
obj = context.object
|
||||
|
||||
if obj is None:
|
||||
self['scale'] = 1
|
||||
return
|
||||
action = obj.animation_data.action
|
||||
|
||||
if action is None:
|
||||
self['scale'] = 1
|
||||
return
|
||||
|
||||
if context.mode == 'POSE' and not context.selected_pose_bones:
|
||||
self['scale'] = 1
|
||||
return
|
||||
|
||||
bpy.ops.animtoolbox.multikey_scale_value('INVOKE_DEFAULT')
|
||||
|
||||
def random_value(self, context):
|
||||
|
||||
for obj in context.selected_objects:
|
||||
if obj.animation_data.action is None:
|
||||
continue
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, obj.animation_data.action)
|
||||
for fcu in fcurves:
|
||||
if obj.mode == 'POSE':
|
||||
if selected_bones_filter(obj, fcu.data_path):
|
||||
continue
|
||||
if Tools.filter_properties(context.scene.animtoolbox, fcu):
|
||||
continue
|
||||
value_list = []
|
||||
threshold = bpy.context.window_manager.atb_ui.multikey.randomness
|
||||
for key in fcu.keyframe_points:
|
||||
if key.select_control_point == True:
|
||||
value_list.append(key.co[1])
|
||||
|
||||
if len(value_list) > 0:
|
||||
value = max(value_list)- min(value_list)
|
||||
for key in fcu.keyframe_points:
|
||||
add_value(key, value * random.uniform(-threshold, threshold))
|
||||
fcu.update()
|
||||
|
||||
self['randomness'] = 0.1
|
||||
|
||||
|
||||
def evaluate_combine(data_path, added_array, eval_array, array_default, influence):
|
||||
|
||||
if 'scale' in data_path:
|
||||
eval_array = eval_array * (added_array / array_default) ** influence
|
||||
elif 'rotation_quaternion' in data_path:
|
||||
#multiply first the influence with the w separatly
|
||||
added_array[0] = added_array[0] + (1- added_array[0])*(1 - influence)
|
||||
added_array[1:] *= influence
|
||||
eval_array = np.array(Quaternion(eval_array) @ Quaternion(added_array))# ** influence
|
||||
#if it's a custom property
|
||||
elif 'rotation_euler' not in data_path and 'location' not in data_path:
|
||||
eval_array = eval_array + (added_array - array_default) * influence
|
||||
|
||||
return eval_array
|
||||
|
||||
def evaluate_array(fcurves, fcu_path, frame, array_default = [0, 0, 0]):
|
||||
'''Create an array from all the indexes'''
|
||||
|
||||
array_len = len(array_default)
|
||||
|
||||
#assigning the default array in case
|
||||
fcu_array = array_default.copy()
|
||||
#get the missing arrays in case quaternion is not complete
|
||||
for i in range(array_len):
|
||||
fcu = fcurves.find(fcu_path, index = i)
|
||||
if fcu is None:
|
||||
continue
|
||||
fcu_array[i] = fcu.evaluate(frame)
|
||||
|
||||
if (fcu_array == array_default).all():
|
||||
return None
|
||||
return np.array(fcu_array)
|
||||
|
||||
def evaluate_layers(context, obj, anim_data, fcu, array_default):
|
||||
'''Calculate the evaluation of all the layers when using the nla'''
|
||||
|
||||
if not hasattr(anim_data, 'nla_tracks') or not anim_data.use_nla:
|
||||
return None
|
||||
nla_tracks = anim_data.nla_tracks
|
||||
if not len(nla_tracks):
|
||||
return None
|
||||
frame = context.scene.frame_current
|
||||
blend_types = {'ADD' : '+', 'SUBTRACT' : '-', 'MULTIPLY' : '*'}
|
||||
fcu_path = fcu.data_path
|
||||
|
||||
eval_array = array_default.copy()
|
||||
|
||||
for track in nla_tracks:
|
||||
if track.mute:
|
||||
continue
|
||||
if not len(track.strips):
|
||||
continue
|
||||
for strip in track.strips:
|
||||
if not strip.frame_start < frame < strip.frame_end:
|
||||
continue
|
||||
action = strip.action
|
||||
if action is None:
|
||||
continue
|
||||
blend_type = strip.blend_type
|
||||
|
||||
#get the influence value either from the attribute or the fcurve. function coming from bake
|
||||
influence = strip.influence
|
||||
if len(strip.fcurves):
|
||||
if not strip.fcurves[0].mute and len(strip.fcurves[0].keyframe_points):
|
||||
influence = strip.fcurves[0].evaluate(frame)
|
||||
|
||||
#evaluate the frame according to the strip settings
|
||||
frame_eval = frame
|
||||
#change the frame if the strip is on hold
|
||||
if frame < strip.frame_start:
|
||||
if strip.extrapolation == 'HOLD':
|
||||
frame_eval = strip.frame_start
|
||||
elif frame >= strip.frame_end:
|
||||
if strip.extrapolation == 'HOLD' or strip.extrapolation == 'HOLD_FORWARD':
|
||||
frame_eval = strip.frame_end
|
||||
|
||||
last_frame = strip.frame_start + (strip.frame_end - strip.frame_start) / strip.repeat
|
||||
|
||||
if strip.repeat > 1 and (frame) >= last_frame:
|
||||
action_range = (strip.action_frame_end * strip.scale - strip.action_frame_start * strip.scale)
|
||||
frame_eval = (((frame_eval - strip.frame_start) % (action_range)) + strip.frame_start)
|
||||
|
||||
if strip.use_reverse:
|
||||
frame_eval = last_frame - (frame_eval - strip.frame_start)
|
||||
offset = (strip.frame_start * 1/strip.scale - strip.action_frame_start) * strip.scale
|
||||
frame_eval = strip.frame_start * 1/strip.scale + (frame_eval - strip.frame_start) * 1/strip.scale - offset * 1/strip.scale
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
||||
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame_eval, influence, array_default, blend_type, blend_types)
|
||||
|
||||
#Adding an extra layer from the action outside and on top of the nla
|
||||
tweak_mode = anim_data.use_tweak_mode
|
||||
if tweak_mode:
|
||||
anim_data.use_tweak_mode = False
|
||||
|
||||
action = anim_data.action
|
||||
if action:
|
||||
influence = anim_data.action_influence
|
||||
blend_type = anim_data.action_blend_type
|
||||
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
||||
eval_array = evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence, array_default, blend_type, blend_types)
|
||||
anim_data.use_tweak_mode = tweak_mode
|
||||
|
||||
return eval_array
|
||||
|
||||
|
||||
def evaluate_blend_type(fcurves, eval_array, fcu_path, frame, influence,
|
||||
array_default, blend_type, blend_types):
|
||||
'''Calculate the value based on the blend type'''
|
||||
|
||||
fcu_array = evaluate_array(fcurves, fcu_path, frame, array_default)
|
||||
if fcu_array is None:
|
||||
return eval_array
|
||||
###EVALUATION###
|
||||
if blend_type =='COMBINE':
|
||||
if 'location' in fcu_path or 'rotation_euler' in fcu_path:
|
||||
blend_type = 'ADD'
|
||||
if blend_type =='REPLACE':
|
||||
eval_array = eval_array * (1 - influence) + fcu_array * influence
|
||||
elif blend_type =='COMBINE':
|
||||
eval_array = evaluate_combine(fcu_path, fcu_array, eval_array, array_default, influence)
|
||||
else:
|
||||
eval_array = eval('eval_array' + blend_types[blend_type] + 'fcu_array' + '*' + str(influence))
|
||||
|
||||
return eval_array
|
||||
|
||||
def evaluate_value(self, context):
|
||||
ui = context.window_manager.atb_ui
|
||||
for obj in context.selected_objects:
|
||||
|
||||
anim_data = obj.animation_data
|
||||
if anim_data is None:
|
||||
return
|
||||
if anim_data.action is None:
|
||||
return
|
||||
|
||||
action = obj.animation_data.action
|
||||
fcu_paths = []
|
||||
transformations = ["rotation_quaternion","rotation_euler", "location", "scale"]
|
||||
if obj.mode == 'POSE':
|
||||
bonelist = context.selected_pose_bones if ui.multikey.selectedbones else obj.pose.bones
|
||||
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
||||
for fcu in fcurves:
|
||||
if fcu in fcu_paths:
|
||||
continue
|
||||
current_value = None
|
||||
if Tools.filter_properties(context.scene.animtoolbox, fcu):
|
||||
continue
|
||||
if obj.mode == 'POSE':
|
||||
if selected_bones_filter(obj, fcu.data_path):
|
||||
continue
|
||||
|
||||
for bone in bonelist:
|
||||
#find the fcurve of the bone
|
||||
if fcu.data_path.rfind(bone.name) != 12 or fcu.data_path[12 + len(bone.name)] != '"':
|
||||
continue
|
||||
path_split = fcu.data_path.split('"].')
|
||||
|
||||
if len(path_split) <= 1:
|
||||
continue
|
||||
else:
|
||||
transform = fcu.data_path.split('"].')[1]
|
||||
if transform not in transformations:
|
||||
continue
|
||||
current_value = getattr(obj.pose.bones[bone.name], transform)
|
||||
else:
|
||||
transform = fcu.data_path
|
||||
current_value = getattr(obj, transform)
|
||||
#In case it was completly filtered out and not current value available
|
||||
if not current_value:
|
||||
continue
|
||||
|
||||
array_default = np.array(attr_default(obj, (fcu.data_path, fcu.array_index)))
|
||||
eval_array = evaluate_layers(context, obj, anim_data, fcu, array_default)
|
||||
if eval_array is None:
|
||||
fcurves = Tools.get_fcurves_channelbag(obj, action)
|
||||
eval_array = evaluate_array(fcurves, fcu.data_path, context.scene.frame_current, array_default)
|
||||
|
||||
#calculate the difference between current value and the fcurve value
|
||||
add_diff(fcurves, fcu.data_path, np.array(current_value), eval_array)
|
||||
|
||||
class MULTIKEY_OT_Multikey(bpy.types.Operator):
|
||||
"""Edit all selected keyframes"""
|
||||
bl_label = "Edit Selected Keyframes"
|
||||
bl_idname = "animtoolbox.multikey"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and context.active_object.animation_data and bpy.context.scene.tool_settings.use_keyframe_insert_auto == False
|
||||
|
||||
def execute(self, context):
|
||||
evaluate_value(self, context)
|
||||
return {'FINISHED'}
|
||||
|
||||
class MultikeyProperties(bpy.types.PropertyGroup):
|
||||
|
||||
selectedbones: bpy.props.BoolProperty(name="Selected Bones", description="Affect only selected bones", default=True, options={'HIDDEN'})
|
||||
handletype: bpy.props.BoolProperty(name="Keep handle types", description="Keep handle types", default=False, options={'HIDDEN'})
|
||||
scale: bpy.props.FloatProperty(name="Scale Factor", description="Scale percentage of the average value", default=1.0, update = scale_value)
|
||||
randomness: bpy.props.FloatProperty(name="Randomness", description="Random Threshold of keyframes", default=0.1, min=0.0, max = 1.0, update = random_value)
|
||||
|
||||
#filters
|
||||
# filter_location: bpy.props.BoolVectorProperty(name="Location", description="Filter Location properties", default=(True, True, True), size = 3, options={'HIDDEN'})
|
||||
# filter_rotation: bpy.props.BoolVectorProperty(name="Rotation", description="Filter Rotation properties", default=(True, True, True, True), size = 4, options={'HIDDEN'})
|
||||
# filter_scale: bpy.props.BoolVectorProperty(name="Scale", description="Filter Scale properties", default=(True, True, True), size = 3, options={'HIDDEN'})
|
||||
|
||||
# class FilterProperties(bpy.types.Operator):
|
||||
# """Filter Location Rotation and Scale Properties"""
|
||||
# bl_idname = "fcurves.filter"
|
||||
# bl_label = "Filter Properties W X Y Z"
|
||||
# bl_options = {'REGISTER', 'UNDO'}
|
||||
|
||||
# def invoke(self, context, event):
|
||||
# wm = context.window_manager
|
||||
# return wm.invoke_props_dialog(self, width = 200)
|
||||
|
||||
# def draw(self, context):
|
||||
# layout = self.layout
|
||||
# row = layout.row()
|
||||
# row.label(text = 'Location')
|
||||
# row.prop(context.scene.multikey, 'filter_location', text = '')
|
||||
# row = layout.row()
|
||||
# row.label(text = 'Rotation')
|
||||
# row.prop(context.scene.multikey, 'filter_rotation', text = '')
|
||||
# row = layout.row()
|
||||
# row.label(text = 'Scale')
|
||||
# row.prop(context.scene.multikey, 'filter_scale', text = '')
|
||||
|
||||
# def execute(self, context):
|
||||
# return {'CANCELLED'}
|
||||
|
||||
classes = (ScaleValuesOp, MULTIKEY_OT_Multikey)
|
||||
|
||||
#register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
# bpy.types.Scene.animtoolbox.multikey = bpy.props.PointerProperty(type = MultikeyProperties, options={'LIBRARY_EDITABLE'}, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
def unregister():
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
||||
# del bpy.types.Scene.animtoolbox.multikey
|
||||
@@ -1,725 +0,0 @@
|
||||
# ***** BEGIN GPL LICENSE BLOCK *****
|
||||
#
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software Foundation,
|
||||
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# ***** END GPL LICENCE BLOCK *****
|
||||
|
||||
import bpy
|
||||
from . import Rigger_Toolbox
|
||||
from . import multikey
|
||||
from . import Rigger_Toolbox
|
||||
from pathlib import Path
|
||||
from bpy.utils import register_class
|
||||
from bpy.utils import unregister_class
|
||||
import os
|
||||
|
||||
######################################################## MENUS ########################################################
|
||||
|
||||
class ANIMTOOLBOX_MT_Copy_Paste_Matrix(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Copy_Paste_Matrix'
|
||||
bl_label = "Copy Paste Matrix"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator('anim.copy_matrix', text = 'Copy World Matrix', icon ='DUPLICATE')
|
||||
layout.operator('anim.paste_matrix', text = 'Paste World Matrix', icon ='PASTEDOWN')
|
||||
layout.separator()
|
||||
layout.operator('anim.copy_relative_matrix', text = 'Copy Relative Matrix', icon ='DUPLICATE')
|
||||
layout.operator('anim.paste_relative_matrix', text = 'Paste Relative Matrix', icon ='PASTEDOWN')
|
||||
layout.separator()
|
||||
layout.prop_menu_enum(context.scene.animtoolbox, 'range_type', text = 'Frame Range Type')
|
||||
if context.scene.animtoolbox.range_type == 'RANGE':
|
||||
# split = layout.split(factor = 0.3)
|
||||
# layout.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
|
||||
# layout.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
|
||||
layout.operator("anim.markers_bakerange", icon = 'MARKER', text ='Frame Range Markers Widget', depress = context.scene.animtoolbox.bake_frame_range)
|
||||
|
||||
class ANIMTOOLBOX_MT_Temp_Ctrls(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Temp_Ctrls'
|
||||
bl_label = "Temp Ctrls"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
custom_icons = preview_collections["main"]
|
||||
layout.operator('anim.bake_to_ctrl', text="World Space Ctrls", icon = 'WORLD')
|
||||
# layout.operator('anim.worldspace_cursor', text="World Space Cursor Ctrls", icon ='ORIENTATION_CURSOR')
|
||||
layout.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
|
||||
layout.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
|
||||
layout.separator()
|
||||
layout.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
|
||||
layout.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'DECORATE_LINKED')
|
||||
layout.separator()
|
||||
layout.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = custom_icons["oven"].icon_id)
|
||||
layout.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
|
||||
layout.prop_menu_enum(context.scene.btc, 'target', text = 'Bake To', icon = 'MOD_ARMATURE')
|
||||
layout.separator()
|
||||
|
||||
ctrl = context.object.animtoolbox.controller if context.object.animtoolbox.controller else context.object
|
||||
if ctrl:
|
||||
layout.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off')
|
||||
|
||||
# layout.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
|
||||
# layout.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
|
||||
layout.separator()
|
||||
layout.prop_menu_enum(context.scene.btc, 'shape_type', icon = 'MESH_DATA')
|
||||
layout.prop(context.scene.btc, 'shape_size', slider = False, icon ='CON_SIZELIKE')
|
||||
layout.prop(context.scene.btc, 'color_set', text = '')
|
||||
# layout.prop(context.scene.btc, 'shape_type', icon = 'MESH_DATA', text ='')
|
||||
|
||||
class ANIMTOOLBOX_MT_Keyframe_Offset(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Keyframe_Offset'
|
||||
bl_label = "Interactive Keyframe Offset"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.prop(context.scene.animtoolbox, 'keyframes_offset', slider = False)
|
||||
layout.operator('anim.select_keyframes_offset', text="Select Offset Keyframes", icon ='RESTRICT_SELECT_OFF')
|
||||
layout.operator('anim.apply_keyframes_offset', text="Apply Offset", icon = 'ANIM')
|
||||
|
||||
class ANIMTOOLBOX_MT_Blendings(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Blendings'
|
||||
bl_label = "Blendings"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ui = context.window_manager.atb_ui
|
||||
layout.prop(ui, 'inbetween_worldmatrix', slider = True)
|
||||
layout.prop(context.scene.animtoolbox, 'inbetweener', slider = True)
|
||||
layout.prop(ui, 'blend_mirror', slider = True)
|
||||
|
||||
class ANIMTOOLBOX_MT_Multikey(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Multikey'
|
||||
bl_label = "Multikey"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
ui = context.window_manager.atb_ui
|
||||
layout.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
|
||||
layout.prop(ui.multikey, 'scale', slider = True)
|
||||
layout.prop(ui.multikey, 'randomness', slider = True)
|
||||
|
||||
class ANIMTOOLBOX_MT_Convert_Rotations(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_Convert_Rotations'
|
||||
bl_label = "Convert Rotations"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
|
||||
layout.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = 'Recommend Euler Rotation')
|
||||
layout.prop_menu_enum(context.scene.animtoolbox, 'rotation_mode', text = 'Convert To')
|
||||
|
||||
class ANIMTOOLBOX_MT_operators(bpy.types.Menu):
|
||||
bl_idname = 'ANIMTOOLBOX_MT_menu_operators'
|
||||
bl_label = "AnimToolBox"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return True
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
ui = context.window_manager.atb_ui
|
||||
custom_icons = preview_collections["main"]
|
||||
|
||||
if context.area.type == 'VIEW_3D':
|
||||
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls', text = 'Temp Ctrls', icon_value = custom_icons["puppet"].icon_id)
|
||||
layout.separator()
|
||||
layout.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
|
||||
layout.operator('anim.relative_cursor', text = 'Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
|
||||
layout.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
|
||||
layout.menu('ANIMTOOLBOX_MT_Convert_Rotations', text = 'Convert Rotations', icon = 'DRIVER_ROTATIONAL_DIFFERENCE')
|
||||
|
||||
layout.separator()
|
||||
layout.menu('ANIMTOOLBOX_MT_Keyframe_Offset', text = 'Interactive Keyframe Offset', icon_value = custom_icons["keyframe_offset"].icon_id)
|
||||
layout.menu('ANIMTOOLBOX_MT_Blendings', text = 'Blendings and inbetweens', icon_value = custom_icons["sliders"].icon_id)
|
||||
layout.separator()
|
||||
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix', text = 'Copy Paste Matrix', icon_value = custom_icons["copy_matrix"].icon_id)
|
||||
|
||||
if bpy.context.preferences.addons[__package__].preferences.multikey:
|
||||
layout.separator()
|
||||
layout.menu('ANIMTOOLBOX_MT_Multikey', text = 'Multikey', icon_value = custom_icons["multikey"].icon_id)
|
||||
|
||||
layout.separator(factor = 0.5)
|
||||
layout.operator('anim.switch_collections_visibility', icon = 'COLLECTION_COLOR_06', text = 'Animated Collections Visibilty')
|
||||
layout.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
|
||||
layout.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.emp.motion_path, icon_value = custom_icons["mt"].icon_id)
|
||||
layout.separator()
|
||||
layout.prop(context.preferences.addons[__package__].preferences, 'quick_menu', text = 'Use Quick Icons Menu')
|
||||
# layout.prop(context.window_manager.atb_ui, 'quick_menu', text = 'Use Quick Icons Menu')
|
||||
# elif context.area.type == 'DOPESHEET_EDITOR':
|
||||
# layout.operator('wm.call_menu_pie', text="Pie AnimOffset").name = 'ANIMAIDE_MT_pie_anim_offset'
|
||||
# layout.menu('ANIMAIDE_MT_anim_offset')
|
||||
# layout.menu('ANIMAIDE_MT_anim_offset_mask')
|
||||
|
||||
# elif context.area.type == 'GRAPH_EDITOR':
|
||||
# layout.menu('ANIMAIDE_MT_curve_tools_pie')
|
||||
|
||||
######################################################## WorkSpaceTools ########################################################
|
||||
|
||||
class KeyframeOffsetTool(bpy.types.WorkSpaceTool):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_context_mode = 'POSE'
|
||||
bl_idname = 'animtoolbox.keyframe_offset'
|
||||
bl_label = 'Keyframe Offset Tool'
|
||||
bl_description = (
|
||||
'Offset the keyframes of the selected bone\n'
|
||||
'Shift + Click to select the bones with an offset\n'
|
||||
'Ctrl + Alt + Click to Apply the offset'
|
||||
)
|
||||
bl_icon = (Path(__file__).parent / "icons" / "ops.anim.keyframe_offset").as_posix()
|
||||
bl_keymap = (
|
||||
('anim.keyframe_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG'}, None),
|
||||
# ('anim.select_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, None),
|
||||
('anim.apply_keyframes_offset', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'ctrl': True, 'alt': True}, None),
|
||||
|
||||
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True}, {'properties': [('mode', 'ADD')]}),
|
||||
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'ctrl': True}, {'properties': [('mode', 'SUB')]}),
|
||||
('view3d.select_box', {'type': 'LEFTMOUSE', 'value': 'CLICK_DRAG', 'shift': True, 'ctrl': True}, {'properties': [('mode', 'AND')]}),
|
||||
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK'}, {'properties': [('deselect_all', True)]}),
|
||||
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True}, {'properties': [('toggle', True)]}),
|
||||
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'alt': True}, {'properties': [('enumerate', True)]}),
|
||||
('view3d.select', {'type': 'LEFTMOUSE', 'value': 'CLICK', 'shift': True, 'alt': True}, {'properties': [('toggle', True), ('enumerate', True)]})
|
||||
|
||||
)
|
||||
|
||||
######################################################## PANELS ########################################################
|
||||
class ANIMTOOLBOX_PT_Panel:
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Animation"
|
||||
#bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.object is not None
|
||||
|
||||
class ANIMTOOLBOX_PT_MainPanel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
#bl_category = "Animation"
|
||||
bl_label = "AnimToolBox"
|
||||
bl_idname = "ANIMTOOLBOX_PT_MainPanel"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw_header(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
self.layout.label(text='', icon_value = custom_icons["animtoolbox"].icon_id)
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
if obj is None:
|
||||
return
|
||||
# layout = self.layout
|
||||
# row = layout.row(align = True)
|
||||
|
||||
# row.label(text = 'Support me on ')
|
||||
# row.operator("wm.url_open", text="Patreon", icon = 'FUND').url = "https://www.patreon.com/animtoolbox"
|
||||
|
||||
class TEMPCTRLS_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
#bl_category = "Animation"
|
||||
bl_label = "Temp Controls"
|
||||
bl_idname = "TEMPCTRLS_PT_Panel"
|
||||
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw_header(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
self.layout.label(text="", icon_value = custom_icons["puppet"].icon_id)
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
btc = context.scene.btc
|
||||
if obj is None:
|
||||
return
|
||||
custom_icons = preview_collections["main"]
|
||||
oven_icon = custom_icons["oven"]
|
||||
# row = layout.row()
|
||||
ui = context.window_manager.atb_ui
|
||||
layout = self.layout
|
||||
box = layout.box()
|
||||
col = box.column()
|
||||
row = col.row()
|
||||
row.operator('anim.bake_to_ctrl', text="WorldSpace Ctrls", icon ='WORLD') #
|
||||
row.operator('anim.add_empty_ctrl', text="", icon ='EMPTY_AXIS')
|
||||
# col.operator('anim.worldspace_cursor', text="WorldSpace Cursor Ctrls", icon ='ORIENTATION_CURSOR')
|
||||
col.operator('anim.bake_to_temp_fk', text="Temp FK Ctrls", icon = 'BONE_DATA')
|
||||
col.operator('anim.bake_to_temp_ik', text="Temp IK Ctrls", icon = 'CON_KINEMATIC')
|
||||
|
||||
# layout.separator(factor = 0.1)
|
||||
# box = layout.box()
|
||||
row = box.row()
|
||||
row.operator('anim.link_temp_chains', text="Link Temp Chains", icon = 'DECORATE_LINKED')
|
||||
row.prop(btc, 'linksettings', text = '', icon = 'SETTINGS')
|
||||
|
||||
if btc.linksettings:
|
||||
# insidebox = box.box()
|
||||
row = box.row()
|
||||
row.label(text = 'Link to Active Chain: ')
|
||||
row.prop(btc, 'link_to', text = '')
|
||||
row = box.row()
|
||||
row.operator('anim.unlink_temp_chains', text="UnLink Temp Chains", icon = 'UNLINKED')
|
||||
# row = insidebox.row()
|
||||
# row.label(text = 'Link from Chain: ')
|
||||
# row.prop(btc, 'link_from', text = '')
|
||||
# layout.separator(factor = 0.1)
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.operator('anim.bake_to_empties', text="WorldSpace Empties")
|
||||
row.operator('anim.empties_to_bones', text="", icon = 'EMPTY_AXIS')
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
# row.operator('anim.bake_constrained_bones', text="Quick Bake", icon = 'REC')
|
||||
row.operator('anim.bake_temp_ctrls', text="Bake Temp Ctrls", icon_value = oven_icon.icon_id)
|
||||
row.prop(btc, 'bakesettings', text = '', icon = 'SETTINGS')
|
||||
|
||||
if btc.bakesettings:
|
||||
insidebox = box.box()
|
||||
# split = insidebox.split(factor = 0.9)
|
||||
# split.prop(btc, 'smartbake', text = 'Smart Bake')
|
||||
# split.operator('fcurves.filter_ui', icon ='FILTER', text = '')
|
||||
# if btc.smartbake:
|
||||
# row.prop(btc, 'inbetween_keyframes')
|
||||
split = insidebox.split(factor = 0.3)
|
||||
split.label(text = 'Bake To:')
|
||||
# split.split(factor = 0.9)
|
||||
split_2 = split.split(factor = 0.9)
|
||||
split_2.prop(btc, 'target', text = '')
|
||||
split_2.operator('fcurves.filter_ui', icon ='FILTER', text = '')
|
||||
|
||||
split = insidebox.split(factor = 0.3)
|
||||
split.label(text = 'Bake Range:')
|
||||
split.prop(btc, 'bake_range_type', text = '')
|
||||
|
||||
if btc.bake_range_type == 'KEYFRAMES':
|
||||
row = insidebox.row()
|
||||
row.label(text = 'Keyframes: ')
|
||||
row.prop(btc, 'smartbake', text = 'Smart Bake')
|
||||
row.prop(btc, 'bake_layers', text = 'All Layers')
|
||||
row = insidebox.row()
|
||||
row.label(text = 'From:')
|
||||
row.prop(btc, 'from_origin', text = 'Origin')
|
||||
row.prop(btc, 'from_ctrl', text = 'Ctrls')
|
||||
|
||||
elif btc.bake_range_type == 'CUSTOM':
|
||||
row = insidebox.row()
|
||||
row.prop(btc, 'bake_range', text = '')
|
||||
|
||||
insidebox.separator(factor=0.1)
|
||||
row = insidebox.row()
|
||||
row.label(text = 'Clean: ')
|
||||
row.prop(btc, 'clean_constraints', text = 'Constraints')
|
||||
if btc.clean_constraints:
|
||||
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
|
||||
|
||||
row = box.row()
|
||||
row.operator('anim.remove_bones_constraints', text="Cleanup", icon_value = custom_icons["trash"].icon_id)
|
||||
row.prop(btc, 'cleansettings', text = '', icon = 'SETTINGS')
|
||||
|
||||
if btc.cleansettings:
|
||||
insidebox = box.box()
|
||||
split = insidebox.split(factor = 0.4)
|
||||
split.label(text = 'Target: ')
|
||||
split.prop(btc, 'target', text = '')
|
||||
row = insidebox.row()
|
||||
row.label(text = 'Clean: ')
|
||||
row.prop(btc, 'clean_constraints', text = 'Constraints')
|
||||
if btc.clean_constraints:
|
||||
row.prop(btc, 'clean_ctrls', text = 'Ctrls')
|
||||
if btc.target == 'SELECTED':
|
||||
split = insidebox.split(factor = 0.68)
|
||||
split.label(text = 'ReBake Link Ctrls to Original: ')
|
||||
split.prop(btc, 'rebake_to_org', text = '')
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
# row = layout.row()
|
||||
|
||||
# row.prop(obj.animtoolbox, 'influence', text = 'Influence', slider = True)
|
||||
ctrl = obj.animtoolbox.controller if obj.animtoolbox.controller else obj
|
||||
if ctrl:
|
||||
enabled_icon = 'HIDE_OFF' if ctrl.animtoolbox.ctrls_enabled else 'HIDE_ON'
|
||||
row.prop(ctrl.animtoolbox, 'ctrls_enabled', text ='Temp Ctrls On/Off', icon = enabled_icon)
|
||||
# row = box.row()
|
||||
# row.prop(ui, 'temp_ctrls_switch', icon = 'RESTRICT_VIEW_OFF', text = '')
|
||||
# row.label(text = 'Temp Ctrls Switch: ')
|
||||
# row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
|
||||
# if ui.temp_ctrls_switch:
|
||||
# # split = layout.split(factor = 0.225)
|
||||
# row = box.row()
|
||||
# row.operator('anim.enable_tempctrls', text="On" , icon = 'HIDE_OFF')#icon = 'CONSTRAINT_BONE'
|
||||
# row.operator('anim.disable_tempctrls', text="Off", icon = 'HIDE_ON')
|
||||
|
||||
box.separator(factor=0.1)
|
||||
row = box.row()
|
||||
row.prop(ui, 'temp_ctrls_shapes', icon = 'MESH_DATA', text = '')
|
||||
row.label(text = 'Temp Ctrls Shapes: ')
|
||||
row.prop(btc, 'target', text = '', icon = 'MOD_ARMATURE', icon_only = True)
|
||||
if ui.temp_ctrls_shapes:
|
||||
row = box.row()
|
||||
row.prop(btc, 'shape_size', slider = False)
|
||||
row.prop(btc, 'shape_type', text = '')
|
||||
row = layout.row()
|
||||
# split.label(text = 'Theme Color: ')
|
||||
row.prop(btc, 'color_set', text = '')
|
||||
|
||||
layout.separator()
|
||||
row = layout.row()
|
||||
row.operator('anim.btc_selections', text="Select", icon = 'CONSTRAINT_BONE')
|
||||
row.prop(btc, 'selection', text = '', icon = 'CON_ARMATURE')
|
||||
|
||||
class ANIMTOOLBOX_PT_Tools(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
bl_label = "Anim Tools"
|
||||
bl_idname = "ANIMTOOLBOX_PT_Tools"
|
||||
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw_header(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
layout = self.layout
|
||||
layout.label(text="", icon_value = custom_icons["toolbox"].icon_id)
|
||||
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
scene = context.scene
|
||||
atb = scene.animtoolbox
|
||||
if obj is None:
|
||||
return
|
||||
custom_icons = preview_collections["main"]
|
||||
layout = self.layout
|
||||
ui = context.window_manager.atb_ui
|
||||
row = layout.row()
|
||||
filter_text = 'Filter Tools Properties' if atb.filter_name == '' else 'Filters: ' + atb.filter_name
|
||||
filter_depress = False if atb.filter_name == '' else True
|
||||
row.operator('fcurves.filter_ui', icon ='FILTER', text = filter_text, depress = filter_depress)
|
||||
layout.separator()
|
||||
#layout.operator('anim.keyframes_offset', text="Offset Keyframes", icon = 'NEXT_KEYFRAME')
|
||||
split = layout.split(factor = 0.9)
|
||||
split.prop(context.scene.animtoolbox, 'keyframes_offset', slider = True)
|
||||
split.operator('anim.apply_keyframes_offset', text="", icon_value = custom_icons["apply"].icon_id)
|
||||
split.operator('anim.select_keyframes_offset', text="", icon_value = custom_icons["select"].icon_id)
|
||||
row = layout.row()
|
||||
row.operator('anim.share_keyframes', text = 'Share Keyframes', icon_value = custom_icons["sharekeys"].icon_id)
|
||||
row = layout.row()
|
||||
row.operator("anim.relative_cursor", text ='Relative Cursor Pivot', icon_value = custom_icons["relative_cursor"].icon_id, depress = ui.relative_cursor)
|
||||
row = layout.row()
|
||||
row.operator("anim.markers_retimer", icon_value = custom_icons["retime"].icon_id, text ='Markers Retimer', depress = ui.markers_retimer)
|
||||
layout.separator(factor = 0.5)
|
||||
|
||||
box = layout.box()
|
||||
# icon = 'DOWNARROW_HLT' if ui.copy_paste_matrix is True else 'RIGHTARROW_THIN'
|
||||
# split = box.split(factor = 0.9)
|
||||
# split.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
|
||||
# split.operator('fcurves.filter', icon ='FILTER', text = '')
|
||||
row = box.row()
|
||||
row.prop(ui, 'copy_paste_matrix', icon_value = custom_icons["copy_matrix"].icon_id, text = 'Copy Paste Matrices')
|
||||
if ui.copy_paste_matrix:
|
||||
row = box.row()
|
||||
row.label(text = 'Copy World Matrix :', icon = 'WORLD')
|
||||
row.operator('anim.copy_matrix', text = '', icon ='DUPLICATE')
|
||||
row.operator('anim.paste_matrix', text = '', icon ='PASTEDOWN')
|
||||
row = box.row()
|
||||
row.label(text = 'Copy Relative Matrix :', icon = 'LINKED')
|
||||
row.operator('anim.copy_relative_matrix', text = '', icon ='DUPLICATE')
|
||||
row.operator('anim.paste_relative_matrix', text = '', icon ='PASTEDOWN')
|
||||
split = box.split(factor = 0.32)
|
||||
split.label(text = 'Paste To')
|
||||
split.prop(context.scene.animtoolbox, 'range_type', text = '')
|
||||
if context.scene.animtoolbox.range_type == 'RANGE':
|
||||
row = box.row()
|
||||
row.prop(context.scene.animtoolbox, 'bake_frame_start', text = 'Start')
|
||||
row.prop(context.scene.animtoolbox, 'bake_frame_end', text = 'End')
|
||||
row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = context.scene.animtoolbox.bake_frame_range)
|
||||
layout.separator(factor = 0.5)
|
||||
|
||||
box = layout.box()
|
||||
# icon = 'DOWNARROW_HLT' if ui.Inbetweens is True else 'RIGHTARROW_THIN'
|
||||
# split = box.split(factor = 0.9)
|
||||
# split.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
|
||||
# split.operator('fcurves.filter', icon ='FILTER', text = '')
|
||||
row = box.row()
|
||||
row.prop(ui, 'Inbetweens', icon_value = custom_icons["sliders"].icon_id, text = 'Blendings / Inbetweens')
|
||||
|
||||
if ui.Inbetweens:
|
||||
col = box.column()
|
||||
col.prop(ui, 'inbetween_worldmatrix', slider = True)
|
||||
col.prop(scene.animtoolbox, 'inbetweener', slider = True)
|
||||
col.prop(ui, 'blend_mirror', slider = True)
|
||||
|
||||
layout.separator(factor = 1)
|
||||
# my_icon = custom_icons["rotations"]
|
||||
split = layout.split(factor = 0.6)
|
||||
split.operator("anim.convert_rotation_mode", icon = 'DRIVER_ROTATIONAL_DIFFERENCE', text = 'Convert Rotation To')
|
||||
# split.operator("anim.convert_rotation_mode", text = 'Convert Rotation To', icon_value = my_icon.icon_id)
|
||||
split = split.split(factor = 0.3)
|
||||
split.operator("anim.find_rotation_mode", icon = 'VIEWZOOM', text = '')
|
||||
split.prop(context.scene.animtoolbox, 'rotation_mode', text = '')
|
||||
|
||||
class MULTIKEY_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
"""Add random value to selected keyframes"""
|
||||
bl_label = "Multikey"
|
||||
bl_idname = "MULTIKEY_PT_Panel"
|
||||
# bl_space_type = 'VIEW_3D'
|
||||
# bl_region_type = 'UI'
|
||||
# bl_category= 'Animation'
|
||||
bl_parent_id = 'ANIMTOOLBOX_PT_Tools'
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw_header(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
layout = self.layout
|
||||
layout.label(text="", icon_value = custom_icons["multikey"].icon_id)
|
||||
|
||||
def draw(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
layout = self.layout
|
||||
ui = context.window_manager.atb_ui
|
||||
# layout.separator()
|
||||
split = layout.split(factor=0.1, align = True)
|
||||
split.prop(ui.multikey, 'selectedbones', icon_value = custom_icons["selected_bones"].icon_id, text = '')
|
||||
# split = split.split(factor=0.1, align = True)
|
||||
#split.operator('fcurves.filter', icon ='FILTER', text = '')
|
||||
split.operator("animtoolbox.multikey", icon = 'ACTION_TWEAK')
|
||||
layout.separator()
|
||||
row = layout.row(align = True)
|
||||
row.prop(ui.multikey, 'scale')
|
||||
row.prop(ui.multikey, 'randomness', slider = True)
|
||||
|
||||
class ANIMTOOLBOX_PT_Display(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
bl_label = "Display"
|
||||
bl_idname = "ANIMTOOLBOX_PT_Display"
|
||||
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw_header(self, context):
|
||||
custom_icons = preview_collections["main"]
|
||||
self.layout.label(text="", icon_value = custom_icons["display"].icon_id)
|
||||
|
||||
def draw(self, context):
|
||||
scene = context.scene
|
||||
layout = self.layout
|
||||
ui = context.window_manager.atb_ui
|
||||
custom_icons = preview_collections["main"]
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
icon = 'DOWNARROW_HLT' if ui.gizmo_size is True else 'RIGHTARROW_THIN'
|
||||
row.prop(ui, 'gizmo_size', icon = icon, text = 'Add to Gizmo Size:')
|
||||
if ui.gizmo_size:
|
||||
row = box.row()
|
||||
row.prop(scene.animtoolbox, 'gizmo_size', text = '')
|
||||
row.operator('view3d.gizmo_size_up', text="", icon ='ADD')
|
||||
row.operator('view3d.gizmo_size_down', text="", icon ='REMOVE')
|
||||
layout.separator(factor = 0.5)
|
||||
|
||||
row = layout.row()
|
||||
col_vis_icon = 'OUTLINER_COLLECTION' if scene.animtoolbox.col_vis else 'COLLECTION_COLOR_06'
|
||||
row.operator('anim.switch_collections_visibility', icon = col_vis_icon, text = 'Animated Collections Visibilty', depress = scene.animtoolbox.col_vis)
|
||||
layout.separator(factor = 0.5)
|
||||
row = layout.row()
|
||||
row.operator('anim.isolate_pose_mode', icon_value = custom_icons["isolate"].icon_id, depress = scene.animtoolbox.isolate_pose_mode)
|
||||
|
||||
layout.separator(factor = 0.5)
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.operator('object.motion_path_operator', text = 'Editable Motion Path', depress = scene.emp.motion_path, icon_value = custom_icons["mt"].icon_id)
|
||||
emp = scene.emp
|
||||
row.prop(scene.emp, 'settings', text = '', icon = 'SETTINGS')
|
||||
if scene.emp.settings:
|
||||
|
||||
split = box.split(factor = 0.4)
|
||||
split.label(text ='Frame Range')
|
||||
# row.prop(scene.animtoolbox, 'mp_frame_range', text = '')
|
||||
|
||||
split.prop(emp, 'frame_range', text = '')
|
||||
if emp.frame_range == 'MANUAL':
|
||||
row = box.row()
|
||||
row.prop(emp, 'frame_start', text = 'Start')
|
||||
row.prop(emp, 'frame_end', text = 'End')
|
||||
# row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
|
||||
|
||||
elif emp.frame_range == 'AROUND':
|
||||
row = box.row()
|
||||
row.prop(emp, 'before', text = 'Before')
|
||||
row.prop(emp, 'after', text = 'After')
|
||||
|
||||
box.separator(factor = 0.1)
|
||||
col = box.column()
|
||||
col.prop(emp, 'display_size', icon = 'DOWNARROW_HLT')
|
||||
if emp.display_size:
|
||||
col.prop(emp, 'thickness', text = 'Line Thickness')
|
||||
col.prop(emp, 'keyframe_size', text = 'Keyframes Size')
|
||||
col.prop(emp, 'frame_size', text = 'Frames Size')
|
||||
|
||||
box.separator(factor = 0.1)
|
||||
row = box.row()
|
||||
row.label(text = 'Visualization Type')
|
||||
row.prop(emp, 'vis_type', text = '')
|
||||
if emp.vis_type == 'VELOCITY':
|
||||
row = box.row()
|
||||
row.prop(emp, 'velocity_factor', text = '')
|
||||
row.prop(emp, 'clamp_min', text = '')
|
||||
row.prop(emp, 'clamp_max', text = '')
|
||||
|
||||
row = box.row()
|
||||
row.prop(emp, 'color_before', text = '')
|
||||
row.prop(emp, 'color_after', text = '')
|
||||
|
||||
row = box.row()
|
||||
row.prop(emp, 'points', text = 'Points')
|
||||
row.prop(emp, 'lines', text = 'Lines')
|
||||
row = box.row()
|
||||
row.prop(emp, 'handles', text = 'Handles')
|
||||
row.prop(emp, 'infront', text = 'In Front')
|
||||
row = box.row()
|
||||
row.prop(emp, 'display_frames', text = 'Display Keyframe Numbers')
|
||||
row.separator()
|
||||
row = box.row()
|
||||
row.operator('anim.go_to_keyframe')
|
||||
# row = box.row()
|
||||
# row.prop(context.preferences.addons[__package__].preferences, 'keyframes_range', text = 'Keyframe Distance Range')
|
||||
# if context.object is None:
|
||||
# return
|
||||
# row.operator("anim.markers_bakerange", icon = 'MARKER', text ='', depress = scene.animtoolbox.bake_frame_range)
|
||||
|
||||
class RIGGERTOOLBOX_PT_Panel(ANIMTOOLBOX_PT_Panel, bpy.types.Panel):
|
||||
bl_label = "Rigger Toolbox"
|
||||
bl_idname = "RIGGERTOOLBOX_PT_Panel"
|
||||
bl_parent_id = 'ANIMTOOLBOX_PT_MainPanel'
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
|
||||
def draw(self, context):
|
||||
obj = context.object
|
||||
if obj is None:
|
||||
return
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
|
||||
col.operator("armature.add_bbone_widgets", text = "Add Bbone Widgets", icon = 'IPO_BEZIER')
|
||||
col.separator()
|
||||
col.operator("armature.add_chain_ctrls", text = "Add Chain Controls", icon = 'OUTLINER_DATA_ARMATURE')
|
||||
col.separator()
|
||||
col.operator("armature.merge", text = "Merge Rigs", icon = 'MOD_ARMATURE')
|
||||
|
||||
# Add-ons Preferences Update Panel
|
||||
# Define Panel classes for updating
|
||||
panels = [ANIMTOOLBOX_PT_MainPanel, TEMPCTRLS_PT_Panel, ANIMTOOLBOX_PT_Tools, ANIMTOOLBOX_PT_Display]
|
||||
|
||||
def update_panel(self, context):
|
||||
message = "Animtoolbox: Updating Panel locations has failed"
|
||||
try:
|
||||
for panel in panels:
|
||||
if "bl_rna" in panel.__dict__:
|
||||
unregister_class(panel)
|
||||
|
||||
for panel in panels:
|
||||
panel.bl_category = context.preferences.addons[__package__].preferences.category
|
||||
register_class(panel)
|
||||
|
||||
except Exception as e:
|
||||
print("\n[{}]\n{}\n\nError:\n{}".format(__package__, message, e))
|
||||
pass
|
||||
|
||||
def add_multikey(self, context):
|
||||
if bpy.context.preferences.addons[__package__].preferences is None:
|
||||
return
|
||||
if bpy.context.preferences.addons[__package__].preferences.multikey:
|
||||
multikey.register()
|
||||
register_class(MULTIKEY_PT_Panel)
|
||||
register_class(ANIMTOOLBOX_MT_Multikey)
|
||||
panels.append(MULTIKEY_PT_Panel)
|
||||
#panels = panels + (MULTIKEY_PT_Panel)
|
||||
elif hasattr(bpy.types, 'MULTIKEY_PT_Panel'):
|
||||
multikey.unregister()
|
||||
panels.remove(MULTIKEY_PT_Panel)
|
||||
unregister_class(MULTIKEY_PT_Panel)
|
||||
unregister_class(ANIMTOOLBOX_MT_Multikey)
|
||||
|
||||
def add_riggertoolbox(self, context):
|
||||
if bpy.context.preferences.addons[__package__].preferences is None:
|
||||
return
|
||||
if bpy.context.preferences.addons[__package__].preferences.riggertoolbox:
|
||||
Rigger_Toolbox.register()
|
||||
register_class(RIGGERTOOLBOX_PT_Panel)
|
||||
panels.append(RIGGERTOOLBOX_PT_Panel)
|
||||
#panels = panels + (RIGGERTOOLBOX_PT_Panel)
|
||||
elif hasattr(bpy.types, 'RIGGERTOOLBOX_PT_Panel'):
|
||||
Rigger_Toolbox.unregister()
|
||||
panels.remove(RIGGERTOOLBOX_PT_Panel)
|
||||
unregister_class(RIGGERTOOLBOX_PT_Panel)
|
||||
|
||||
classes = (ANIMTOOLBOX_MT_Copy_Paste_Matrix, ANIMTOOLBOX_MT_Temp_Ctrls, ANIMTOOLBOX_MT_operators, ANIMTOOLBOX_MT_Keyframe_Offset,
|
||||
ANIMTOOLBOX_MT_Blendings, ANIMTOOLBOX_MT_Convert_Rotations) + tuple(panels)
|
||||
|
||||
preview_collections = {}
|
||||
#list of all the custom icons
|
||||
icons_list = {'relative_cursor' : 'relative_cursor.png', 'puppet' : 'puppet.png', 'animtoolbox' : 'animtoolbox.png',
|
||||
'sliders' : 'slider-navigation.png', 'world_space' : 'earth-rotation.png', 'isolate' : 'isolation.png',
|
||||
'mt' : 'curve.png', 'toolbox' : 'tools.png', 'oven' : 'oven.png', 'retime' : 'retime.png', 'copy_matrix' : 'copy_world.png',
|
||||
'sharekeys' : 'sharekeys.png', 'sliders' : 'sliders.png', 'keyframe_offset' : 'keyframes_offset.png', 'display' : 'display.png',
|
||||
'switch' : 'switch.png', 'trash' : 'trash.png', 'apply' : 'apply.png', 'select' : 'select.png', 'worldspace' : 'WorldSpace.png',
|
||||
'multikey' : 'multikey.png', 'selected_bones' : 'selected_bones.png'}
|
||||
|
||||
def register_custom_icon():
|
||||
# Note that preview collections returned by bpy.utils.previews
|
||||
# are regular py objects - you can use them to store custom data.
|
||||
import bpy.utils.previews
|
||||
custom_icons = bpy.utils.previews.new()
|
||||
# path to the folder where the icon is
|
||||
# the path is calculated relative to this py file inside the addon folder
|
||||
icons_dir = os.path.join(os.path.dirname(__file__), "icons")
|
||||
# load a preview thumbnail of a file and store in the previews collection
|
||||
for iconname, icon_file in icons_list.items():
|
||||
custom_icons.load(iconname, os.path.join(icons_dir, icon_file), 'IMAGE')
|
||||
preview_collections["main"] = custom_icons
|
||||
|
||||
def draw_menu(self, context):
|
||||
if context.mode == 'OBJECT' or context.mode == 'POSE':
|
||||
layout = self.layout
|
||||
scene = context.scene
|
||||
ui = context.window_manager.atb_ui
|
||||
|
||||
custom_icons = preview_collections["main"]
|
||||
layout.menu('ANIMTOOLBOX_MT_menu_operators') #, icon_value = custom_icons["toolbox"].icon_id, text =''
|
||||
|
||||
if not context.preferences.addons[__package__].preferences.quick_menu:
|
||||
return
|
||||
#Only Icons Menu
|
||||
temp_ctrls = custom_icons["puppet"]
|
||||
layout.menu('ANIMTOOLBOX_MT_Temp_Ctrls' , icon_value = temp_ctrls.icon_id, text ='')
|
||||
# layout.separator()
|
||||
layout.menu('ANIMTOOLBOX_MT_Copy_Paste_Matrix' , icon_value = custom_icons["copy_matrix"].icon_id, text ='')
|
||||
|
||||
layout.separator()
|
||||
layout.operator('anim.relative_cursor', text = '', depress = ui.relative_cursor, icon_value = custom_icons["relative_cursor"].icon_id)
|
||||
|
||||
layout.separator()
|
||||
layout.operator('anim.markers_retimer', text = '', depress = ui.markers_retimer, icon_value = custom_icons["retime"].icon_id)
|
||||
|
||||
layout.separator()
|
||||
layout.operator('anim.isolate_pose_mode', text = '', depress = scene.animtoolbox.isolate_pose_mode, icon_value = custom_icons["isolate"].icon_id)
|
||||
|
||||
layout.separator()
|
||||
layout.operator('object.motion_path_operator', text = '', depress = scene.emp.motion_path, icon_value = custom_icons["mt"].icon_id)
|
||||
BIN
Binary file not shown.
Binary file not shown.
@@ -2007,7 +2007,7 @@ def remove_draw_handlers(self):
|
||||
# Operator with modal and draw handler
|
||||
class MotionPathOperator(bpy.types.Operator):
|
||||
|
||||
"""Creates an editable motion path, Ctrl click to refresh"""
|
||||
"""Creates a custom motion path"""
|
||||
bl_idname = "object.motion_path_operator"
|
||||
bl_label = "Motion Path Operator"
|
||||
bl_options = {'REGISTER', 'UNDO'}
|
||||
@@ -2028,15 +2028,10 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
scene = context.scene
|
||||
emp = scene.emp
|
||||
|
||||
emp.refresh = False
|
||||
if emp.motion_path:
|
||||
if event.ctrl:
|
||||
# Refresh the motion path
|
||||
emp.refresh = True
|
||||
else:
|
||||
emp.motion_path = False
|
||||
emp.motion_path = False
|
||||
return {'CANCELLED'}
|
||||
|
||||
|
||||
if context.selected_pose_bones is None and context.selected_objects is None:
|
||||
emp.motion_path = False
|
||||
return {'CANCELLED'}
|
||||
@@ -2242,6 +2237,9 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
#return negative frame range to what it was
|
||||
if context.preferences.edit.use_negative_frames != nfr:
|
||||
context.preferences.edit.use_negative_frames = nfr
|
||||
|
||||
#assign the draw handler
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
#If overlays are turned off then skip drawing
|
||||
if self.mp_vis:
|
||||
@@ -2257,9 +2255,6 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
self.area.tag_redraw()
|
||||
# Tools.redraw_areas(['VIEW_3D'])
|
||||
|
||||
#assign the draw handler
|
||||
context.window_manager.modal_handler_add(self)
|
||||
|
||||
bpy.ops.ed.undo_push(message = 'Initialize Motion Path')
|
||||
return {'RUNNING_MODAL'}
|
||||
|
||||
@@ -2284,11 +2279,6 @@ class MotionPathOperator(bpy.types.Operator):
|
||||
if not emp.motion_path:
|
||||
quit_mp(self, context)
|
||||
return {'FINISHED'}
|
||||
|
||||
if emp.refresh:
|
||||
get_mp_points(self, context)
|
||||
emp.refresh = False
|
||||
return {'PASS_THROUGH'}
|
||||
|
||||
if not self.mp_vis:
|
||||
check_mp_vis_on(self, context)
|
||||
@@ -4874,8 +4864,6 @@ def disbale_non_smooth(self, context):
|
||||
class EditableMotionPathSettings(bpy.types.PropertyGroup):
|
||||
'''All the settings for Editable motion path'''
|
||||
motion_path: bpy.props.BoolProperty(name = "Motion Path", description = "Flag when Motion Path is on", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
refresh: bpy.props.BoolProperty(name = "Refresh Motion Path", description = "Refresh the motion paths with hotkey ctrl", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
|
||||
settings: bpy.props.BoolProperty(name = "Motion Path Settings", description = "Open the settings Menu", default = False, override = {'LIBRARY_OVERRIDABLE'})
|
||||
# mp_keyframe_scale: bpy.props.FloatProperty(name = "Scale Selecgted Keyframes Bounding Box", description = "Change the scale of the bounding box around the selected keyframes ", default = 0.1, step = 0.1, precision = 3)
|
||||
color_before: bpy.props.FloatVectorProperty(name="Motion Path Before Color", subtype='COLOR', default=(1.0, 0.0, 0.0), min=0.0, max=1.0, description="Motion path color before the current frame", update=update_prop_callback)
|
||||
|
||||
@@ -5,14 +5,14 @@
|
||||
bl_info = {
|
||||
"name": "Flamenco",
|
||||
"author": "Sybren A. Stüvel",
|
||||
"version": (3, 8, 5),
|
||||
"version": (3, 10),
|
||||
"blender": (3, 1, 0),
|
||||
"description": "Flamenco client for Blender.",
|
||||
"location": "Output Properties > Flamenco",
|
||||
"doc_url": "https://flamenco.blender.org/",
|
||||
"category": "System",
|
||||
"support": "COMMUNITY",
|
||||
"warning": "",
|
||||
"warning": "This is version 3.10-alpha0 of the add-on, which is not a stable release",
|
||||
}
|
||||
|
||||
from pathlib import Path
|
||||
@@ -20,14 +20,14 @@ from pathlib import Path
|
||||
__is_first_load = "operators" not in locals()
|
||||
if __is_first_load:
|
||||
from . import (
|
||||
operators,
|
||||
comms,
|
||||
gui,
|
||||
job_types,
|
||||
comms,
|
||||
manager_info,
|
||||
operators,
|
||||
preferences,
|
||||
projects,
|
||||
worker_tags,
|
||||
manager_info,
|
||||
)
|
||||
else:
|
||||
import importlib
|
||||
@@ -99,11 +99,18 @@ def register() -> None:
|
||||
bpy.app.handlers.save_pre.append(_unset_flamenco_job_name)
|
||||
bpy.app.handlers.save_post.append(_set_flamenco_job_name)
|
||||
|
||||
bpy.types.WindowManager.flamenco_can_abort = bpy.props.BoolProperty(
|
||||
name="Flamenco Can Abort",
|
||||
default=False,
|
||||
description="Whether the Flamenco submission can be aborted",
|
||||
)
|
||||
|
||||
bpy.types.WindowManager.flamenco_bat_status = bpy.props.EnumProperty(
|
||||
items=[
|
||||
("IDLE", "IDLE", "Not doing anything."),
|
||||
("SAVING", "SAVING", "Saving your file."),
|
||||
("INVESTIGATING", "INVESTIGATING", "Finding all dependencies."),
|
||||
("REWRITING", "REWRITING", "Rewriting blend files."),
|
||||
("TRANSFERRING", "TRANSFERRING", "Transferring all dependencies."),
|
||||
("COMMUNICATING", "COMMUNICATING", "Communicating with Flamenco Server."),
|
||||
("DONE", "DONE", "Not doing anything, but doing something earlier."),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
# This is the interface to BAT v1.x
|
||||
|
||||
|
||||
def bat_version() -> str:
|
||||
from .submodules import bat_toplevel
|
||||
|
||||
bat_version: str = bat_toplevel.__version__
|
||||
return bat_version
|
||||
|
||||
@@ -539,6 +539,13 @@ def _encode_original_filename_header(filename: str) -> str:
|
||||
"""
|
||||
|
||||
# This is a no-op when the filename is already in ASCII.
|
||||
fake_header = email.header.Header()
|
||||
fake_header = email.header.Header(maxlinelen=0)
|
||||
fake_header.append(filename, charset="utf-8")
|
||||
return fake_header.encode()
|
||||
encoded_header = fake_header.encode()
|
||||
|
||||
# Make sure that there are no newlines in the returned value.
|
||||
# HTTP Header line folding is obsolete, see RFC9112 section 5.2 in
|
||||
# https://www.rfc-editor.org/rfc/rfc9112#name-obsolete-line-folding
|
||||
assert "\n" not in encoded_header
|
||||
|
||||
return encoded_header
|
||||
|
||||
@@ -4,5 +4,6 @@ from .. import wheels
|
||||
_bat_modules = wheels.load_wheel(
|
||||
"blender_asset_tracer",
|
||||
("blendfile", "pack", "pack.progress", "pack.transfer", "pack.shaman", "bpathlib"),
|
||||
filename_prefix="blender_asset_tracer-1.",
|
||||
)
|
||||
bat_toplevel, blendfile, pack, progress, transfer, shaman, bpathlib = _bat_modules
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""BAT v2 support for Flamenco.
|
||||
|
||||
NOTE: This module uses late imports to avoid importing BAT v2 until it's really
|
||||
necessary. Functions should _only_ be called from Blender 5.1+ (Python 3.13+),
|
||||
as BAT uses language features that were not available in 5.0 (Python 3.11).
|
||||
"""
|
||||
|
||||
|
||||
def bat_version() -> str:
|
||||
from .submodules import bat_toplevel
|
||||
|
||||
return bat_toplevel.__version__
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""BAT v2 packing to the filesystem."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ("pack_start",)
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, TypeAlias
|
||||
|
||||
# Alias some types from blender_asset_tracer so that we can use type annotations
|
||||
# without having to import from BATv2.
|
||||
BATPackReporter: TypeAlias = Any
|
||||
BATPacker: TypeAlias = Any
|
||||
|
||||
|
||||
def pack_start(
|
||||
project_root: Path,
|
||||
reporter: BATPackReporter,
|
||||
*,
|
||||
use_relative_only: bool,
|
||||
pack_target_dir: Path,
|
||||
ignore_globs: set[str] = set(),
|
||||
) -> BATPacker:
|
||||
"""Investigate what's needed to create a BAT pack."""
|
||||
from .submodules import file_usage, pack
|
||||
|
||||
batpacker = pack.BATPacker(
|
||||
project_root,
|
||||
file_usage.Options(
|
||||
use_relative_only=use_relative_only,
|
||||
ignore_globs=ignore_globs,
|
||||
),
|
||||
reporter,
|
||||
pack_target_dir=pack_target_dir,
|
||||
)
|
||||
return batpacker
|
||||
@@ -0,0 +1,594 @@
|
||||
# SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
"""BAT v2 packing to a Shaman server."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = ("pack_start",)
|
||||
|
||||
import dataclasses
|
||||
import email.header
|
||||
import logging
|
||||
import random
|
||||
from functools import partial
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import TYPE_CHECKING, Any, TypeAlias
|
||||
|
||||
import bpy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# _BATPacker: TypeAlias = pack.BATPacker
|
||||
from ..manager import ApiClient as _ApiClient
|
||||
from ..manager.apis import ShamanApi as _ShamanApi
|
||||
from ..manager.models import (
|
||||
ShamanCheckoutResult as _ShamanCheckoutResult,
|
||||
)
|
||||
from ..manager.models import (
|
||||
ShamanFileSpec as _ShamanFileSpec,
|
||||
)
|
||||
from ..manager.models import (
|
||||
ShamanRequirementsRequest as _ShamanRequirementsRequest,
|
||||
)
|
||||
from .submodules.file_usage import FileInfo as _FileInfo
|
||||
from .submodules.pack import BATPacker as _BATPacker
|
||||
from .submodules.pack import QueueingExecutor as _QueueingExecutor
|
||||
else:
|
||||
_ApiClient = object
|
||||
_ShamanApi = object
|
||||
_ShamanCheckoutResult = object
|
||||
_ShamanRequirementsRequest = object
|
||||
_ShamanFileSpec = object
|
||||
_BATPacker = object
|
||||
_FileInfo = object
|
||||
_QueueingExecutor = object
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
MAX_DEFERRED_PATHS = 8
|
||||
MAX_FAILED_PATHS = 8
|
||||
HASH_STORAGE_PATH = Path(bpy.app.cachedir) / "flamenco/shaman"
|
||||
HASH_METHOD = "sha256"
|
||||
SHAMAN_JOBS_VARIABLE = "{jobs}"
|
||||
|
||||
HashableShamanFileSpec = tuple[str, int, str]
|
||||
"""Tuple of the 'sha', 'size', and 'path' fields of a ShamanFileSpec."""
|
||||
|
||||
# Alias some types from blender_asset_tracer so that we can use type annotations
|
||||
# without having to import from BATv2.
|
||||
BATPackReporter: TypeAlias = Any
|
||||
BATPacker: TypeAlias = Any
|
||||
|
||||
|
||||
def pack_start(
|
||||
project_root: Path,
|
||||
reporter: BATPackReporter,
|
||||
*,
|
||||
use_relative_only: bool,
|
||||
api_client: _ApiClient,
|
||||
checkout_path: PurePosixPath,
|
||||
ignore_globs: set[str] = set(),
|
||||
) -> BATPacker:
|
||||
"""Investigate what's needed to create a BAT pack."""
|
||||
from ..manager.apis import ShamanApi
|
||||
from .submodules import file_usage, pack
|
||||
|
||||
shaman_api = ShamanApi(api_client)
|
||||
executor = pack.QueueingExecutor()
|
||||
shaman_transferer = ShamanPacker(shaman_api, checkout_path, executor, reporter)
|
||||
|
||||
batpacker = pack.BATPacker(
|
||||
project_root,
|
||||
file_usage.Options(
|
||||
use_relative_only=use_relative_only,
|
||||
ignore_globs=ignore_globs,
|
||||
),
|
||||
reporter,
|
||||
file_transfer=shaman_transferer,
|
||||
)
|
||||
|
||||
return batpacker
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ShamanUploadProgress:
|
||||
# When another client is already uploading a file that we also want to
|
||||
# upload, we defer the file. That way, when we do get around to uploading
|
||||
# it, the other person may already have finished their upload, saving us
|
||||
# time.
|
||||
#
|
||||
# Mapping from 'path in pack' to the BAT FileInfo and Shaman FileSpec.
|
||||
deferred: dict[PurePosixPath, tuple[_FileInfo, _ShamanFileSpec]] = (
|
||||
dataclasses.field(default_factory=dict)
|
||||
)
|
||||
|
||||
# When a file doesn't want to get uploaded, it's stored here to retry. If
|
||||
# too many files fail, or the retry counter reaches its max, it'll get
|
||||
# reported as an actual error.
|
||||
# The string value is the error message.
|
||||
failures: dict[PurePosixPath, tuple[_FileInfo, _ShamanFileSpec, str]] = (
|
||||
dataclasses.field(default_factory=dict)
|
||||
)
|
||||
|
||||
retry_counter: int = 0
|
||||
max_retries: int = 50
|
||||
|
||||
def is_deferred(self, relpath_in_pack: PurePosixPath) -> bool:
|
||||
return relpath_in_pack in self.deferred
|
||||
|
||||
@property
|
||||
def num_deferred(self) -> int:
|
||||
return len(self.deferred)
|
||||
|
||||
@property
|
||||
def num_failed(self) -> int:
|
||||
return len(self.failures)
|
||||
|
||||
def defer(
|
||||
self,
|
||||
bat_file_info: _FileInfo,
|
||||
shaman_file_spec: _ShamanFileSpec,
|
||||
) -> None:
|
||||
# A file should only be deferred once. Once an upload has been deferred,
|
||||
# the next attempt shouldn't be deferred again.
|
||||
assert bat_file_info.relpath_in_pack not in self.deferred
|
||||
|
||||
self.deferred[bat_file_info.relpath_in_pack] = (bat_file_info, shaman_file_spec)
|
||||
|
||||
def failed(
|
||||
self,
|
||||
bat_file_info: _FileInfo,
|
||||
shaman_file_spec: _ShamanFileSpec,
|
||||
errormsg: str,
|
||||
) -> None:
|
||||
# A file should only be added to the 'failures' dict once. When its
|
||||
# upload is retried, it should be removed from the 'failures' dict first.
|
||||
assert bat_file_info.relpath_in_pack not in self.failures
|
||||
|
||||
self.failures[bat_file_info.relpath_in_pack] = (
|
||||
bat_file_info,
|
||||
shaman_file_spec,
|
||||
errormsg,
|
||||
)
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class ShamanPacker:
|
||||
shaman_api: _ShamanApi
|
||||
checkout_path: PurePosixPath
|
||||
executor: _QueueingExecutor
|
||||
reporter: BATPackReporter
|
||||
|
||||
# Shaman may decide to create the checkout at another path than requested.
|
||||
# This will be set to the actually-used path on the farm, relative to the
|
||||
# Shaman's "jobs" directory.
|
||||
#
|
||||
# NOTE: It is the checkout path of the job, NOT the path to the blend file.
|
||||
_checkout_path_final: PurePosixPath | None = None
|
||||
|
||||
_source_file_relpath_in_pack: PurePosixPath | None = None
|
||||
|
||||
_num_files_to_transfer_total: int = -1
|
||||
_num_files_to_transfer_done: int = 0
|
||||
|
||||
@property
|
||||
def is_succes(self) -> bool:
|
||||
"""Return whether the Shaman operation was completed succesfully."""
|
||||
return bool(self._checkout_path_final)
|
||||
|
||||
def start(self, batpacker: _BATPacker) -> None:
|
||||
files_to_copy = batpacker.all_files_to_copy()
|
||||
|
||||
# Initial value is the total number of files to copy. Once the Shaman
|
||||
# server has told us how many files to submit, this will be adjusted.
|
||||
self._num_files_to_transfer_total = len(files_to_copy)
|
||||
self._num_files_to_transfer_done = 0
|
||||
|
||||
# Remember where the main blend file sits in the BAT pack.
|
||||
source_file_info = batpacker.deps_repo.source_file_info()
|
||||
assert source_file_info.relpath_in_pack is not None
|
||||
self._source_file_relpath_in_pack = PurePosixPath(
|
||||
source_file_info.relpath_in_pack.as_posix()
|
||||
)
|
||||
|
||||
self.executor.queue(partial(self._step_queue_hashing, files_to_copy))
|
||||
|
||||
def step(self) -> bool:
|
||||
"""Perform a single step in the Shaman file transfer.
|
||||
|
||||
Returns whether there are more steps to do (True) or the process is done (False).
|
||||
"""
|
||||
if self.executor.is_done:
|
||||
return False
|
||||
self.executor.run_step()
|
||||
return not self.executor.is_done
|
||||
|
||||
def blendfile_location_in_pack(self) -> PurePosixPath:
|
||||
assert self._checkout_path_final is not None
|
||||
assert self._source_file_relpath_in_pack is not None
|
||||
return (
|
||||
PurePosixPath(SHAMAN_JOBS_VARIABLE)
|
||||
/ self._checkout_path_final
|
||||
/ self._source_file_relpath_in_pack
|
||||
)
|
||||
|
||||
def num_files_to_transfer(self) -> tuple[int, int]:
|
||||
"""Return the number of files that need to be transferred.
|
||||
|
||||
This is a tuple [total, done] with the total number of files to
|
||||
transfer, and the number of transferred files so far.
|
||||
|
||||
The number may change during the packing process, as it takes time
|
||||
for the Shaman protocol to get this information. Or some paths may
|
||||
turn out to be multiple paths (UDIMs for example).
|
||||
"""
|
||||
return self._num_files_to_transfer_total, self._num_files_to_transfer_done
|
||||
|
||||
def _step_queue_hashing(self, files_to_copy: dict[Path, _FileInfo]) -> None:
|
||||
from ..manager.models import ShamanRequirementsRequest
|
||||
|
||||
# Shaman Spec that's shared between the queued function calls. They
|
||||
# can all just append to the same list.
|
||||
shaman_spec = ShamanRequirementsRequest(files=[])
|
||||
assert isinstance(shaman_spec, ShamanRequirementsRequest)
|
||||
|
||||
# Tracks deferred files and failed uploads.
|
||||
upload_progress = ShamanUploadProgress()
|
||||
|
||||
# Queue up all the hash computations.
|
||||
for file_info in files_to_copy.values():
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_hash_file,
|
||||
file_info,
|
||||
shaman_spec,
|
||||
)
|
||||
)
|
||||
|
||||
# After the hashes are gathered in 'filespecs', send the spec to Shaman.
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_queue_uploads_of_files,
|
||||
files_to_copy,
|
||||
shaman_spec,
|
||||
upload_progress,
|
||||
)
|
||||
)
|
||||
|
||||
def _step_hash_file(
|
||||
self,
|
||||
file_info: _FileInfo,
|
||||
shaman_spec: _ShamanRequirementsRequest,
|
||||
) -> None:
|
||||
from _bpy_internal import disk_file_hash_service
|
||||
|
||||
from ..manager.models import ShamanFileSpec
|
||||
|
||||
path_to_pack = file_info.path_to_pack
|
||||
|
||||
if not path_to_pack.exists():
|
||||
# If the file is missing, there's little else to do than reporting
|
||||
# it as such and continue with the next file.
|
||||
if path_to_pack == file_info.source_path:
|
||||
log.info("File missing: %s", path_to_pack)
|
||||
else:
|
||||
log.info(
|
||||
"File missing after rewriting %s to %s",
|
||||
file_info.source_path,
|
||||
path_to_pack,
|
||||
)
|
||||
self.reporter.on_missing_file(
|
||||
file_info.source_path, file_info.relpath_in_pack
|
||||
)
|
||||
return
|
||||
|
||||
# It might be tempting to use the same Disk File Hash Service as BAT's
|
||||
# path rewriting system is using. However, that only hashes the files
|
||||
# that need rewriting, and the code below only deals with paths after
|
||||
# rewriting (or where rewriting was not necessary). That means that
|
||||
# there is no benefit in sharing the same database.
|
||||
dfhs = disk_file_hash_service.get_service(HASH_STORAGE_PATH)
|
||||
checksum = dfhs.get_hash(path_to_pack, HASH_METHOD)
|
||||
|
||||
filesize = path_to_pack.stat().st_size
|
||||
|
||||
filespec = ShamanFileSpec(
|
||||
sha=checksum,
|
||||
size=filesize,
|
||||
path=str(file_info.relpath_in_pack),
|
||||
)
|
||||
assert isinstance(filespec, ShamanFileSpec)
|
||||
shaman_spec.files.append(filespec)
|
||||
|
||||
def _step_queue_uploads_of_files(
|
||||
self,
|
||||
files_to_copy: dict[Path, _FileInfo],
|
||||
shaman_spec: _ShamanRequirementsRequest,
|
||||
upload_progress: ShamanUploadProgress,
|
||||
) -> None:
|
||||
"""Send the spec file to Shaman, and queue file uploads."""
|
||||
|
||||
# Query Shaman to figure out which files still need uploading.
|
||||
to_upload = self._send_spec_to_shaman(shaman_spec)
|
||||
if to_upload is None:
|
||||
# Errors have been reported already, so just stop.
|
||||
return
|
||||
|
||||
log.info(
|
||||
"Feeding %d/%d files to the Shaman", len(to_upload), len(shaman_spec.files)
|
||||
)
|
||||
self._num_files_to_transfer_total = len(to_upload)
|
||||
|
||||
# Create a mapping from the path in the pack (which is used in
|
||||
# `filespecs`) to the FileInfo.
|
||||
path_in_pack_to_abs: dict[str, _FileInfo] = {
|
||||
str(file_info.relpath_in_pack): file_info
|
||||
for file_info in files_to_copy.values()
|
||||
}
|
||||
|
||||
# Queue the file uploads.
|
||||
for index, file_spec in enumerate(to_upload):
|
||||
file_info = path_in_pack_to_abs[file_spec.path]
|
||||
is_last_file = index == len(to_upload)
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_queue_upload_file,
|
||||
file_info,
|
||||
file_spec,
|
||||
is_last_file,
|
||||
upload_progress,
|
||||
)
|
||||
)
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_check_upload_success,
|
||||
files_to_copy,
|
||||
shaman_spec,
|
||||
upload_progress,
|
||||
)
|
||||
)
|
||||
|
||||
def _step_queue_upload_file(
|
||||
self,
|
||||
file_info: _FileInfo,
|
||||
file_spec: _ShamanFileSpec,
|
||||
is_last_file: bool,
|
||||
upload_progress: ShamanUploadProgress,
|
||||
) -> None:
|
||||
# Pre-flight check. The generated API code will load the entire file
|
||||
# into memory before sending it to the Shaman. It's faster to do a check
|
||||
# at Shaman first, to see if we need uploading at all.
|
||||
check_resp = self.shaman_api.shaman_file_store_check(
|
||||
checksum=file_spec.sha,
|
||||
filesize=file_spec.size,
|
||||
)
|
||||
if check_resp.status.value == "stored":
|
||||
log.info(" %s: skipping, already on server", file_spec.path)
|
||||
return
|
||||
|
||||
# Do the 'start' reporting in a separate step, so that the Blender UI
|
||||
# can be updated for it. The 'done'/'error' reports are done at the end
|
||||
# of the file upload step, and so these don't need a separate step.
|
||||
self.executor.queue(partial(self._step_report_upload_file_start, file_info))
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_upload_file,
|
||||
file_info,
|
||||
file_spec,
|
||||
is_last_file,
|
||||
upload_progress,
|
||||
)
|
||||
)
|
||||
|
||||
def _step_report_upload_file_start(self, file_info: _FileInfo) -> None:
|
||||
self.reporter.on_copy_start(file_info.source_path, file_info.relpath_in_pack)
|
||||
|
||||
def _step_upload_file(
|
||||
self,
|
||||
file_info: _FileInfo,
|
||||
file_spec: _ShamanFileSpec,
|
||||
is_last_file: bool,
|
||||
upload_progress: ShamanUploadProgress,
|
||||
) -> None:
|
||||
from ..manager.exceptions import ApiException
|
||||
|
||||
# See whether we may be able to defer uploading this file or not.
|
||||
can_defer = bool(
|
||||
not is_last_file
|
||||
and upload_progress.num_deferred < MAX_DEFERRED_PATHS
|
||||
and not upload_progress.is_deferred(file_info.relpath_in_pack)
|
||||
)
|
||||
|
||||
filename_header = _encode_original_filename_header(file_spec.path)
|
||||
try:
|
||||
with file_info.path_to_pack.open("rb") as file_reader:
|
||||
self.shaman_api.shaman_file_store(
|
||||
checksum=file_spec.sha,
|
||||
filesize=file_spec.size,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=can_defer,
|
||||
x_shaman_original_filename=filename_header,
|
||||
)
|
||||
except ApiException as ex:
|
||||
if ex.status == 425:
|
||||
# Too Early, i.e. defer uploading this file.
|
||||
log.info(
|
||||
" %s: someone else is uploading this file, deferring",
|
||||
file_spec.path,
|
||||
)
|
||||
upload_progress.defer(file_info, file_spec)
|
||||
return
|
||||
elif ex.status == 417:
|
||||
# Expectation Failed; mismatch of checksum or file size.
|
||||
msg = "Error from Shaman uploading %s, code %d: %s" % (
|
||||
file_spec.path,
|
||||
ex.status,
|
||||
ex.body,
|
||||
)
|
||||
else: # Unknown error
|
||||
msg = "API exception\nHeaders: %s\nBody: %s\n" % (
|
||||
ex.headers,
|
||||
ex.body,
|
||||
)
|
||||
|
||||
log.error(msg)
|
||||
upload_progress.failed(file_info, file_spec, msg)
|
||||
return
|
||||
|
||||
self._num_files_to_transfer_done += 1
|
||||
self.reporter.on_copy_done(file_info.source_path, file_info.relpath_in_pack)
|
||||
|
||||
def _step_check_upload_success(
|
||||
self,
|
||||
files_to_copy: dict[Path, _FileInfo],
|
||||
shaman_spec: _ShamanRequirementsRequest,
|
||||
upload_progress: ShamanUploadProgress,
|
||||
) -> None:
|
||||
"""See if there were any deferred or failed files.
|
||||
|
||||
If there were, re-queue the uploading of the remaining files.
|
||||
Unless the number of retries has been exceeded, in which case the
|
||||
failures are final.
|
||||
"""
|
||||
|
||||
if upload_progress.num_deferred == 0 and upload_progress.num_failed == 0:
|
||||
# Nothing left to do, so move on to the next stage.
|
||||
self.executor.queue(partial(self._step_request_checkout, shaman_spec))
|
||||
return
|
||||
|
||||
upload_progress.retry_counter += 1
|
||||
if upload_progress.retry_counter >= upload_progress.max_retries:
|
||||
# Failed uploads have really failed now.
|
||||
#
|
||||
# Deferred uploads shouldn't be mentioned, because they only get
|
||||
# deferred on the first upload attempt. After that, if they fail,
|
||||
# they get into the failures.
|
||||
for fileinfo, _, errormsg in upload_progress.failures.values():
|
||||
self.reporter.on_copy_error(
|
||||
fileinfo.source_path, fileinfo.relpath_in_pack, errormsg
|
||||
)
|
||||
return
|
||||
|
||||
# Retry uploading.
|
||||
self.executor.queue(
|
||||
partial(
|
||||
self._step_queue_uploads_of_files,
|
||||
files_to_copy,
|
||||
shaman_spec,
|
||||
upload_progress,
|
||||
)
|
||||
)
|
||||
|
||||
def _step_request_checkout(self, shaman_spec: _ShamanRequirementsRequest) -> None:
|
||||
"""Ask the Shaman to create a checkout of this BAT pack."""
|
||||
assert self.checkout_path
|
||||
|
||||
from ..manager.exceptions import ApiException
|
||||
from ..manager.models import ShamanCheckout, ShamanCheckoutResult
|
||||
|
||||
log.info(
|
||||
"Requesting checkout at Shaman for checkout_path=%s", self.checkout_path
|
||||
)
|
||||
|
||||
checkoutRequest = ShamanCheckout(
|
||||
files=shaman_spec.files,
|
||||
checkout_path=str(self.checkout_path),
|
||||
)
|
||||
|
||||
try:
|
||||
result: ShamanCheckoutResult = self.shaman_api.shaman_checkout(
|
||||
checkoutRequest
|
||||
)
|
||||
except ApiException as ex:
|
||||
if ex.status == 424: # Files were missing
|
||||
msg = "We did not upload some files, checkout aborted"
|
||||
elif ex.status == 409: # Checkout already exists
|
||||
msg = "There is already an existing checkout at %s" % self.checkout_path
|
||||
else: # Unknown error
|
||||
msg = "API exception\nHeaders: %s\nBody: %s\n" % (
|
||||
ex.headers,
|
||||
ex.body,
|
||||
)
|
||||
log.error(msg)
|
||||
self.reporter.on_error(msg)
|
||||
return
|
||||
|
||||
log.info("Shaman created checkout at %s", result.checkout_path)
|
||||
self._checkout_path_final = result.checkout_path
|
||||
|
||||
def _send_spec_to_shaman(
|
||||
self,
|
||||
requirements: _ShamanRequirementsRequest,
|
||||
) -> list[_ShamanFileSpec] | None:
|
||||
"""Send the checkout definition file to the Shaman.
|
||||
|
||||
:return: A list of file specs that still need to be uploaded, or
|
||||
None if there was an error.
|
||||
"""
|
||||
from ..manager.exceptions import ApiException
|
||||
from ..manager.models import ShamanRequirementsResponse
|
||||
|
||||
requested_relpaths = {file.path for file in requirements.files}
|
||||
|
||||
try:
|
||||
resp = self.shaman_api.shaman_checkout_requirements(requirements)
|
||||
except ApiException as ex:
|
||||
# TODO: the body should be JSON of a predefined type, parse it to get the actual message.
|
||||
msg = "Error from Shaman, code %d: %s" % (ex.status, ex.body)
|
||||
log.error(msg)
|
||||
self.reporter.on_error(msg)
|
||||
return None
|
||||
assert isinstance(resp, ShamanRequirementsResponse)
|
||||
|
||||
# Go over the response, and create two queues for uploading. Any file
|
||||
# that's already being uploaded by somebody else will be put in the
|
||||
# low-priority queue.
|
||||
to_upload_normal_prio: list[_ShamanFileSpec] = []
|
||||
to_upload_low_prio: list[_ShamanFileSpec] = []
|
||||
for file_spec in resp.files:
|
||||
if file_spec.path not in requested_relpaths:
|
||||
msg = (
|
||||
"Shaman requested path we did not intend to upload: %r" % file_spec
|
||||
)
|
||||
log.error(msg)
|
||||
self.reporter.on_error(msg)
|
||||
return None
|
||||
|
||||
log.debug(" %s: %s", file_spec.status, file_spec.path)
|
||||
status = file_spec.status.value
|
||||
if status == "unknown":
|
||||
to_upload_normal_prio.append(file_spec)
|
||||
elif status == "uploading":
|
||||
to_upload_low_prio.append(file_spec)
|
||||
else:
|
||||
msg = "Unknown status in response from Shaman: %r" % file_spec
|
||||
log.error(msg)
|
||||
self.reporter.on_error(msg)
|
||||
return None
|
||||
|
||||
# Randomize the two lists, so that when two clients upload similar sets
|
||||
# of files, collissions are minimized.
|
||||
random.shuffle(to_upload_normal_prio)
|
||||
random.shuffle(to_upload_low_prio)
|
||||
|
||||
return to_upload_normal_prio + to_upload_low_prio
|
||||
|
||||
|
||||
def _encode_original_filename_header(filename: str) -> str:
|
||||
"""Encode the 'original filename' as valid HTTP Header.
|
||||
|
||||
See the specs for the X-Shaman-Original-Filename header in the OpenAPI
|
||||
operation `shamanFileStore`, defined in flamenco-openapi.yaml.
|
||||
"""
|
||||
|
||||
# This is a no-op when the filename is already in ASCII.
|
||||
fake_header = email.header.Header(maxlinelen=0)
|
||||
fake_header.append(filename, charset="utf-8")
|
||||
encoded_header = fake_header.encode()
|
||||
|
||||
# Make sure that there are no newlines in the returned value.
|
||||
# HTTP Header line folding is obsolete, see RFC9112 section 5.2 in
|
||||
# https://www.rfc-editor.org/rfc/rfc9112#name-obsolete-line-folding
|
||||
assert "\n" not in encoded_header
|
||||
|
||||
return encoded_header
|
||||
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
# SORRY FOR THE COMPLEXITY!
|
||||
#
|
||||
# Flamenco needs to be able to deal with BAT versions 1 and 2 at the same time,
|
||||
# by loading them from different wheel files, depending on which version of
|
||||
# Blender is running.
|
||||
#
|
||||
# At the same time, there's developers who will be really happy when mypy can do
|
||||
# its thing, and when Blender can load mypy and BAT from virtual environments.
|
||||
|
||||
WHEEL_MODULE = "blender_asset_tracer"
|
||||
WHEEL_FILENAME_PREFIX = "blender_asset_tracer-2."
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# When type-checking, BAT should be importable.
|
||||
import blender_asset_tracer as bat_toplevel
|
||||
from blender_asset_tracer import (
|
||||
file_usage,
|
||||
pack,
|
||||
path_rewriting,
|
||||
path_rewriting_process,
|
||||
)
|
||||
else:
|
||||
# For development only: if we can import BAT directly, just assume it's the
|
||||
# right version and go with it.
|
||||
if "VIRTUAL_ENV" in os.environ:
|
||||
import site
|
||||
from pathlib import Path
|
||||
|
||||
venv_path = Path(os.environ["VIRTUAL_ENV"])
|
||||
print(f"Reactivating virtualenv: {venv_path}")
|
||||
|
||||
# Add the virtual environments libraries.
|
||||
lib_dirs_posix = list(venv_path.rglob("lib/*/site-packages"))
|
||||
lib_dirs_windows = list(venv_path.rglob("Lib/site-packages"))
|
||||
for lib_dir in lib_dirs_posix + lib_dirs_windows:
|
||||
site.addsitedir(str(lib_dir))
|
||||
|
||||
try:
|
||||
import blender_asset_tracer as bat_toplevel
|
||||
from blender_asset_tracer import (
|
||||
file_usage,
|
||||
pack,
|
||||
path_rewriting,
|
||||
path_rewriting_process,
|
||||
)
|
||||
except ImportError:
|
||||
# At runtime, some trickery is necessary to load BAT from the bundled wheel file, without making
|
||||
# it available in `sys.modules` (to prevent interaction with other add-ons).
|
||||
from .. import wheels
|
||||
|
||||
# Load all the submodules we need from BAT in one go.
|
||||
_bat_modules = wheels.load_wheel(
|
||||
WHEEL_MODULE,
|
||||
("file_usage", "pack", "path_rewriting", "path_rewriting_process"),
|
||||
filename_prefix=WHEEL_FILENAME_PREFIX,
|
||||
)
|
||||
bat_toplevel, file_usage, pack, path_rewriting, path_rewriting_process = (
|
||||
_bat_modules
|
||||
)
|
||||
|
||||
# Expose the location of the wheel file to BAT by setting an environment
|
||||
# variable. This is necessary for BAT's path rewriting sub-process, in
|
||||
# order to know where to load its own sources from.
|
||||
wheel_filename: Path = wheels.filename(
|
||||
WHEEL_MODULE,
|
||||
filename_prefix=WHEEL_FILENAME_PREFIX,
|
||||
)
|
||||
os.environ["BAT_WHEEL"] = str(wheel_filename)
|
||||
@@ -1,16 +1,18 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
# <pep8 compliant>
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from . import preferences, job_types
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
|
||||
from . import job_types, preferences
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from flamenco.manager.models import (
|
||||
AvailableJobSetting as _AvailableJobSetting,
|
||||
)
|
||||
from flamenco.manager.models import (
|
||||
SubmittedJob as _SubmittedJob,
|
||||
)
|
||||
else:
|
||||
@@ -178,13 +180,18 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
|
||||
elif flamenco_status == "INVESTIGATING":
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Investigating your files")
|
||||
# row.operator(FLAMENCO_OT_abort.bl_idname, text="", icon="CANCEL")
|
||||
row.operator("flamenco.abort", text="", icon="CANCEL")
|
||||
elif flamenco_status == "REWRITING":
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Rewriting files")
|
||||
row.operator("flamenco.abort", text="", icon="CANCEL")
|
||||
elif flamenco_status == "COMMUNICATING":
|
||||
layout.label(text="Communicating with Flamenco Server")
|
||||
elif flamenco_status == "ABORTING":
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Aborting, please wait.")
|
||||
# row.operator(FLAMENCO_OT_abort.bl_idname, text="", icon="CANCEL")
|
||||
row.operator("flamenco.abort", text="", icon="CANCEL")
|
||||
|
||||
if flamenco_status == "TRANSFERRING":
|
||||
row = layout.row(align=True)
|
||||
row.prop(
|
||||
@@ -192,7 +199,7 @@ class FLAMENCO_PT_job_submission(bpy.types.Panel):
|
||||
"flamenco_bat_progress",
|
||||
text=context.window_manager.flamenco_bat_status_txt,
|
||||
)
|
||||
# row.operator(FLAMENCO_OT_abort.bl_idname, text="", icon="CANCEL")
|
||||
row.operator("flamenco.abort", text="", icon="CANCEL")
|
||||
elif (
|
||||
flamenco_status != "IDLE" and context.window_manager.flamenco_bat_status_txt
|
||||
):
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
import logging
|
||||
import platform
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import TYPE_CHECKING, Optional, Union
|
||||
import platform
|
||||
import logging
|
||||
|
||||
import bpy
|
||||
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
from .bat.submodules import bpathlib
|
||||
from . import manager_info
|
||||
from .bat.submodules import bpathlib
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .manager import ApiClient as _ApiClient
|
||||
from .manager.models import (
|
||||
AvailableJobType as _AvailableJobType,
|
||||
)
|
||||
from .manager.models import (
|
||||
Job as _Job,
|
||||
)
|
||||
from .manager.models import (
|
||||
SubmittedJob as _SubmittedJob,
|
||||
)
|
||||
else:
|
||||
@@ -32,8 +35,11 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def job_for_scene(scene: bpy.types.Scene) -> Optional[_SubmittedJob]:
|
||||
from flamenco.manager.models import SubmittedJob, JobMetadata
|
||||
from flamenco.manager.model.job_status import JobStatus
|
||||
from flamenco.manager.models import JobMetadata, SubmittedJob
|
||||
|
||||
# Do a late import, to make the order in which this file is imported less sensitive.
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
|
||||
propgroup = getattr(scene, "flamenco_job_settings", None)
|
||||
assert isinstance(propgroup, JobTypePropertyGroup), "did not expect %s" % (
|
||||
@@ -106,7 +112,7 @@ def submit_job(job: _SubmittedJob, api_client: _ApiClient) -> _Job:
|
||||
"""Send the given job to Flamenco Manager."""
|
||||
from flamenco.manager import ApiClient
|
||||
from flamenco.manager.api import jobs_api
|
||||
from flamenco.manager.models import SubmittedJob, Job
|
||||
from flamenco.manager.models import Job, SubmittedJob
|
||||
|
||||
assert isinstance(job, SubmittedJob), "got %s" % type(job)
|
||||
assert isinstance(api_client, ApiClient), "got %s" % type(api_client)
|
||||
@@ -122,7 +128,7 @@ def submit_job_check(job: _SubmittedJob, api_client: _ApiClient) -> None:
|
||||
"""Check the given job at Flamenco Manager to see if it is acceptable."""
|
||||
from flamenco.manager import ApiClient
|
||||
from flamenco.manager.api import jobs_api
|
||||
from flamenco.manager.models import SubmittedJob, Job
|
||||
from flamenco.manager.models import Job, SubmittedJob
|
||||
|
||||
assert isinstance(job, SubmittedJob), "got %s" % type(job)
|
||||
assert isinstance(api_client, ApiClient), "got %s" % type(api_client)
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"""
|
||||
|
||||
|
||||
__version__ = "3.8.5"
|
||||
__version__ = "3.10-alpha0"
|
||||
|
||||
# import ApiClient
|
||||
from flamenco.manager.api_client import ApiClient
|
||||
|
||||
@@ -76,7 +76,7 @@ class ApiClient(object):
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
self.user_agent = 'Flamenco/3.8.5 (Blender add-on)'
|
||||
self.user_agent = 'Flamenco/3.10-alpha0 (Blender add-on)'
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
@@ -404,7 +404,7 @@ conf = flamenco.manager.Configuration(
|
||||
"OS: {env}\n"\
|
||||
"Python Version: {pyversion}\n"\
|
||||
"Version of the API: 1.0.0\n"\
|
||||
"SDK Package Version: 3.8.5".\
|
||||
"SDK Package Version: 3.10-alpha0".\
|
||||
format(env=sys.platform, pyversion=sys.version)
|
||||
|
||||
def get_host_settings(self):
|
||||
|
||||
@@ -4,7 +4,7 @@ Render Farm manager API
|
||||
The `flamenco.manager` package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||
|
||||
- API version: 1.0.0
|
||||
- Package version: 3.8.5
|
||||
- Package version: 3.10-alpha0
|
||||
- Build package: org.openapitools.codegen.languages.PythonClientCodegen
|
||||
For more information, please visit [https://flamenco.blender.org/](https://flamenco.blender.org/)
|
||||
|
||||
|
||||
@@ -3,29 +3,33 @@
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib3.exceptions import HTTPError, MaxRetryError
|
||||
from pathlib import Path, PurePath, PurePosixPath
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
from urllib3.exceptions import HTTPError, MaxRetryError
|
||||
|
||||
from . import job_types, job_submission, preferences, manager_info
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
from . import job_submission, job_types, manager_info, preferences
|
||||
from .bat.submodules import bpathlib
|
||||
from .job_types_propgroup import JobTypePropertyGroup
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .bat.interface import (
|
||||
PackThread as _PackThread,
|
||||
Message as _Message,
|
||||
)
|
||||
from .manager.models import (
|
||||
Error as _Error,
|
||||
SubmittedJob as _SubmittedJob,
|
||||
from .bat.interface import (
|
||||
PackThread as _PackThread,
|
||||
)
|
||||
from .manager.api_client import ApiClient as _ApiClient
|
||||
from .manager.exceptions import ApiException as _ApiException
|
||||
from .manager.models import (
|
||||
Error as _Error,
|
||||
)
|
||||
from .manager.models import (
|
||||
SubmittedJob as _SubmittedJob,
|
||||
)
|
||||
else:
|
||||
_PackThread = object
|
||||
_Message = object
|
||||
@@ -34,6 +38,21 @@ else:
|
||||
_ApiException = object
|
||||
_Error = object
|
||||
|
||||
# Conditionally import BAT v2, as that version requires Blender 5.1+.
|
||||
#
|
||||
# Blender 5.1.0 still had some limitations, most importantly missing support for geometry nodes
|
||||
# simulation caches (https://projects.blender.org/blender/blender/issues/155953). That should be
|
||||
# fixed in 5.1.1 though.
|
||||
bat_v2: ModuleType | None
|
||||
if bpy.app.version >= (5, 1, 1) or TYPE_CHECKING:
|
||||
from . import bat_v2
|
||||
from .bat_v2.pack_fs import BATPacker
|
||||
|
||||
_BATPacker = BATPacker
|
||||
else:
|
||||
bat_v2 = None
|
||||
_BATPacker = object
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -111,15 +130,21 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
|
||||
job_name: bpy.props.StringProperty(name="Job Name") # type: ignore
|
||||
job: Optional[_SubmittedJob] = None
|
||||
temp_blendfile: Optional[Path] = None
|
||||
ignore_version_mismatch: bpy.props.BoolProperty( # type: ignore
|
||||
name="Ignore Version Mismatch",
|
||||
default=False,
|
||||
)
|
||||
|
||||
TIMER_PERIOD = 0.25 # seconds
|
||||
TIMER_PERIOD_BAT_V2 = 0.01 # seconds
|
||||
|
||||
timer: Optional[bpy.types.Timer] = None
|
||||
# For BAT v1:
|
||||
packthread: Optional[_PackThread] = None
|
||||
# For BAT v2:
|
||||
bat_v2_packer: _BATPacker | None = None
|
||||
bat_v2_packer_reported_error: bool = False
|
||||
bat_v2_packer_missing_files: list[Path]
|
||||
|
||||
log = _log.getChild(bl_idname)
|
||||
|
||||
@@ -131,55 +156,96 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
return job_type is not None
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
"""Submit the job files in a blocking way.
|
||||
|
||||
This allows scripted submission, which blocks the main thread until
|
||||
the process is done.
|
||||
"""
|
||||
|
||||
filepath, ok = self._presubmit_check(context)
|
||||
if not ok:
|
||||
return {"CANCELLED"}
|
||||
|
||||
is_running = self._submit_files(context, filepath)
|
||||
# Use BAT v2 if available.
|
||||
if bat_v2 is not None:
|
||||
is_running = self._submit_files_bat_v2(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
# self.bat_v2_packer is None when no packing is necessary, that is, when the file is
|
||||
# already on the shared storage.
|
||||
while self.bat_v2_packer and self.bat_v2_packer.step():
|
||||
pass
|
||||
|
||||
return self.bat_v2_packer_finalize_and_quit(context)
|
||||
|
||||
is_running = self._submit_files_bat_v1(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.packthread is None:
|
||||
# If there is no pack thread running, there isn't much we can do.
|
||||
self.report({"ERROR"}, "No pack thread running, please report a bug")
|
||||
self._quit(context)
|
||||
return {"CANCELLED"}
|
||||
# Keep handling messages from the background thread. That's only necessary if there is a
|
||||
# background thread.
|
||||
if self.packthread:
|
||||
while True:
|
||||
# Block for 5 seconds at a time. The exact duration doesn't matter,
|
||||
# as this while-loop is blocking the main thread anyway.
|
||||
msg = self.packthread.poll(timeout=5)
|
||||
if not msg:
|
||||
# No message received, is fine, just wait for another one.
|
||||
continue
|
||||
|
||||
# Keep handling messages from the background thread.
|
||||
while True:
|
||||
# Block for 5 seconds at a time. The exact duration doesn't matter,
|
||||
# as this while-loop is blocking the main thread anyway.
|
||||
msg = self.packthread.poll(timeout=5)
|
||||
if not msg:
|
||||
# No message received, is fine, just wait for another one.
|
||||
continue
|
||||
|
||||
result = self._on_bat_pack_msg(context, msg)
|
||||
if "RUNNING_MODAL" not in result:
|
||||
break
|
||||
result = self._on_bat_pack_msg(context, msg)
|
||||
if "RUNNING_MODAL" not in result:
|
||||
break
|
||||
self.packthread.join(timeout=5)
|
||||
|
||||
self._quit(context)
|
||||
self.packthread.join(timeout=5)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
|
||||
filepath, ok = self._presubmit_check(context)
|
||||
if not ok:
|
||||
return {"CANCELLED"}
|
||||
|
||||
is_running = self._submit_files(context, filepath)
|
||||
if bat_v2 is None:
|
||||
is_running = self._submit_files_bat_v1(context, filepath)
|
||||
else:
|
||||
is_running = self._submit_files_bat_v2(context, filepath)
|
||||
if not is_running:
|
||||
return {"CANCELLED"}
|
||||
|
||||
context.window_manager.modal_handler_add(self)
|
||||
if bat_v2:
|
||||
# Only BATv2 supports aborting the packing.
|
||||
self.report({"INFO"}, "Flamenco: Submitting files, press ESC to abort")
|
||||
|
||||
wm = context.window_manager
|
||||
self.timer = wm.event_timer_add(self.TIMER_PERIOD_BAT_V2, window=context.window)
|
||||
wm.modal_handler_add(self)
|
||||
|
||||
return {"RUNNING_MODAL"}
|
||||
|
||||
def modal(self, context: bpy.types.Context, event: bpy.types.Event) -> set[str]:
|
||||
wm = bpy.context.window_manager
|
||||
|
||||
# Only BAT v2 has support for aborting the packing operation.
|
||||
should_abort = event.type == "ESC" or wm.flamenco_bat_status == "ABORTING"
|
||||
if self.bat_v2_packer is not None and should_abort:
|
||||
self.report({"WARNING"}, "Flamenco: Job submission aborted")
|
||||
wm.flamenco_bat_status = "ABORTED"
|
||||
wm.flamenco_bat_status_txt = ""
|
||||
return self._quit(context)
|
||||
|
||||
# This function is called for TIMER events to poll the BAT pack thread.
|
||||
if event.type != "TIMER":
|
||||
return {"PASS_THROUGH"}
|
||||
|
||||
if self.bat_v2_packer is not None:
|
||||
# BAT v2 pack is underway.
|
||||
keep_going = self.bat_v2_packer.step()
|
||||
if keep_going:
|
||||
return {"RUNNING_MODAL"}
|
||||
return self.bat_v2_packer_finalize_and_quit(context)
|
||||
|
||||
if self.packthread is None:
|
||||
# If there is no pack thread running, there isn't much we can do.
|
||||
return self._quit(context)
|
||||
@@ -304,13 +370,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
We shouldn't overwrite the artist's file.
|
||||
We can compress, since this file won't be managed by SVN and doesn't need diffability.
|
||||
"""
|
||||
render = context.scene.render
|
||||
prefs = context.preferences
|
||||
|
||||
# Remember settings we need to restore after saving.
|
||||
old_use_file_extension = render.use_file_extension
|
||||
old_use_overwrite = render.use_overwrite
|
||||
old_use_placeholder = render.use_placeholder
|
||||
old_use_all_linked_data_direct = getattr(
|
||||
prefs.experimental, "use_all_linked_data_direct", None
|
||||
)
|
||||
@@ -318,44 +378,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
# TODO: see about disabling the denoiser (like the old Blender Cloud addon did).
|
||||
|
||||
try:
|
||||
# The file extension should be determined by the render settings, not necessarily
|
||||
# by the settings in the output panel.
|
||||
render.use_file_extension = True
|
||||
|
||||
# Rescheduling should not overwrite existing frames.
|
||||
render.use_overwrite = False
|
||||
render.use_placeholder = False
|
||||
|
||||
# To work around a shortcoming of BAT, ensure that all
|
||||
# To work around a shortcoming of BAT v1, ensure that all
|
||||
# indirectly-linked data is still saved as directly-linked.
|
||||
#
|
||||
# See `133dde41bb5b: Improve handling of (in)directly linked status
|
||||
# for linked IDs` in Blender's Git repository.
|
||||
if old_use_all_linked_data_direct is not None:
|
||||
if bat_v2 is None and old_use_all_linked_data_direct is not None:
|
||||
self.log.info(
|
||||
"Overriding prefs.experimental.use_all_linked_data_direct = True"
|
||||
)
|
||||
prefs.experimental.use_all_linked_data_direct = True
|
||||
|
||||
filepath = Path(context.blend_data.filepath)
|
||||
if job_submission.is_file_inside_job_storage(context, filepath):
|
||||
self.log.info(
|
||||
"Saving blendfile, already in shared storage: %s", filepath
|
||||
)
|
||||
if bpy.data.is_dirty:
|
||||
self.log.info("Saving blendfile: %s", filepath)
|
||||
bpy.ops.wm.save_as_mainfile()
|
||||
else:
|
||||
filepath = filepath.with_suffix(".flamenco.blend")
|
||||
self.log.info("Saving copy to temporary file %s", filepath)
|
||||
bpy.ops.wm.save_as_mainfile(
|
||||
filepath=str(filepath), compress=True, copy=True
|
||||
)
|
||||
self.temp_blendfile = filepath
|
||||
finally:
|
||||
# Restore the settings we changed, even after an exception.
|
||||
render.use_file_extension = old_use_file_extension
|
||||
render.use_overwrite = old_use_overwrite
|
||||
render.use_placeholder = old_use_placeholder
|
||||
|
||||
# Only restore if the property exists to begin with:
|
||||
if old_use_all_linked_data_direct is not None:
|
||||
prefs.experimental.use_all_linked_data_direct = (
|
||||
@@ -364,135 +403,183 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
|
||||
return filepath
|
||||
|
||||
def _bat_project_path(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> Path:
|
||||
"""BAT 'project' directory: preference root, or wider if links lie outside it.
|
||||
|
||||
Linked blends outside the configured project force BAT to use KEEP_PATH;
|
||||
Shaman packing can then fail path rewriting. Using a common ancestor of
|
||||
the submission blend and all linked library paths keeps assets inside
|
||||
the project tree for BAT.
|
||||
"""
|
||||
prefs = preferences.get(context)
|
||||
configured = bpathlib.make_absolute(
|
||||
Path(bpy.path.abspath(str(prefs.project_root())))
|
||||
)
|
||||
blend_abs = bpathlib.make_absolute(blendfile)
|
||||
|
||||
lib_paths: list[Path] = []
|
||||
for lib in bpy.data.libraries:
|
||||
if not lib.filepath:
|
||||
continue
|
||||
lib_paths.append(
|
||||
bpathlib.make_absolute(Path(bpy.path.abspath(lib.filepath)))
|
||||
)
|
||||
|
||||
def is_under(root: Path, path: Path) -> bool:
|
||||
try:
|
||||
path.resolve().relative_to(root.resolve())
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
need_widen = (not is_under(configured, blend_abs)) or any(
|
||||
not is_under(configured, lp) for lp in lib_paths
|
||||
)
|
||||
if not need_widen:
|
||||
return configured
|
||||
|
||||
all_paths = [blend_abs] + lib_paths
|
||||
try:
|
||||
common = Path(os.path.commonpath([str(p) for p in all_paths])).resolve()
|
||||
except ValueError:
|
||||
self.log.warning(
|
||||
"Could not compute common path for BAT project root, using preferences"
|
||||
)
|
||||
return configured
|
||||
|
||||
self.log.info(
|
||||
"BAT project root widened from %s to %s (assets outside preference project)",
|
||||
configured,
|
||||
common,
|
||||
)
|
||||
return common
|
||||
|
||||
def _convert_relpaths_to_absolute(self, context: bpy.types.Context) -> None:
|
||||
"""Convert all relative paths in the blend file to absolute paths.
|
||||
|
||||
Covers libraries, images, clips, sounds, fonts, volumes, and cache_files
|
||||
(point caches / GN caches). Allows the blend to be sent as-is without BAT.
|
||||
"""
|
||||
# Convert library paths to absolute
|
||||
for library in bpy.data.libraries:
|
||||
if library.filepath:
|
||||
old_path = library.filepath
|
||||
abs_path = bpy.path.abspath(library.filepath)
|
||||
library.filepath = abs_path
|
||||
self.log.debug("Converted library path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert image paths to absolute
|
||||
for image in bpy.data.images:
|
||||
if image.filepath and not image.packed_file:
|
||||
old_path = image.filepath
|
||||
abs_path = bpy.path.abspath(image.filepath)
|
||||
image.filepath = abs_path
|
||||
self.log.debug("Converted image path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert movie paths to absolute
|
||||
for movie in bpy.data.movieclips:
|
||||
if movie.filepath:
|
||||
old_path = movie.filepath
|
||||
abs_path = bpy.path.abspath(movie.filepath)
|
||||
movie.filepath = abs_path
|
||||
self.log.debug("Converted movie path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert sound paths to absolute
|
||||
for sound in bpy.data.sounds:
|
||||
if sound.filepath:
|
||||
old_path = sound.filepath
|
||||
abs_path = bpy.path.abspath(sound.filepath)
|
||||
sound.filepath = abs_path
|
||||
self.log.debug("Converted sound path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Convert font paths to absolute (skip VectorFont - its filepath is read-only)
|
||||
for font in bpy.data.fonts:
|
||||
if font.filepath:
|
||||
try:
|
||||
old_path = font.filepath
|
||||
abs_path = bpy.path.abspath(font.filepath)
|
||||
font.filepath = abs_path
|
||||
self.log.debug("Converted font path: %s -> %s", old_path, abs_path)
|
||||
except (TypeError, AttributeError):
|
||||
self.log.debug("Skipping font %s (filepath is read-only)", font.name)
|
||||
|
||||
# Convert volume paths to absolute
|
||||
for volume in bpy.data.volumes:
|
||||
if volume.filepath:
|
||||
old_path = volume.filepath
|
||||
abs_path = bpy.path.abspath(volume.filepath)
|
||||
volume.filepath = abs_path
|
||||
self.log.debug("Converted volume path: %s -> %s", old_path, abs_path)
|
||||
|
||||
# Point / mesh cache files (e.g. .pc2), geometry-nodes caches, etc.
|
||||
for cache_file in bpy.data.cache_files:
|
||||
if cache_file.filepath:
|
||||
old_path = cache_file.filepath
|
||||
abs_path = bpy.path.abspath(cache_file.filepath)
|
||||
cache_file.filepath = abs_path
|
||||
self.log.debug("Converted cache_file path: %s -> %s", old_path, abs_path)
|
||||
|
||||
def _submit_files(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
def _submit_files_bat_v2(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Ensure that the files are somewhere in the shared storage.
|
||||
|
||||
Returns True if a packing thread has been started, and False otherwise.
|
||||
"""
|
||||
prefs = preferences.get(context)
|
||||
if prefs.bat_bypass:
|
||||
return self._submit_files_bat_bypass(context, blendfile)
|
||||
|
||||
from .bat_v2 import bat_version, pack_fs, pack_shaman
|
||||
|
||||
self.log.info("Submitting files via BAT %s", bat_version())
|
||||
|
||||
# Reset state from any previous run.
|
||||
self.bat_v2_packer = None
|
||||
self.bat_v2_packer_reported_error = False
|
||||
self.bat_v2_packer_missing_files = []
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
return False
|
||||
|
||||
# Get the project root and double-check its existence.
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
assert project_path.is_absolute(), (
|
||||
"Expecting project path {!s} to be an absolute path".format(project_path)
|
||||
)
|
||||
if not project_path.exists():
|
||||
self.report(
|
||||
{"ERROR"}, "Project path {!s} does not exist".format(project_path)
|
||||
)
|
||||
raise FileNotFoundError()
|
||||
|
||||
if job_submission.is_file_inside_job_storage(context, blendfile):
|
||||
self.log.info(
|
||||
"File is already in job storage location, submitting it as-is"
|
||||
)
|
||||
self._use_blendfile_directly(context, blendfile)
|
||||
return True
|
||||
|
||||
if manager.shared_storage.shaman_enabled:
|
||||
# Pack to the Shaman server.
|
||||
self.log.info("Copying BAT pack to Shaman storage")
|
||||
batpacker = pack_shaman.pack_start(
|
||||
project_root=project_path,
|
||||
reporter=self,
|
||||
use_relative_only=prefs.use_relative_only,
|
||||
api_client=self.get_api_client(context),
|
||||
checkout_path=PurePosixPath(self.job_name),
|
||||
ignore_globs=prefs.ignore_globs(),
|
||||
)
|
||||
batpacker.start()
|
||||
|
||||
# When packing via Shaman, the Shaman server determines the final
|
||||
# location of the blend file, and so it's not known yet.
|
||||
self.blendfile_on_farm = None
|
||||
else:
|
||||
# Pack to the filesystem.
|
||||
unique_dir = "%s-%s" % (
|
||||
datetime.datetime.now().isoformat("-").replace(":", ""),
|
||||
self.job_name,
|
||||
)
|
||||
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
|
||||
self.log.info("Copying BAT pack to shared storage: %s", pack_target_dir)
|
||||
batpacker = pack_fs.pack_start(
|
||||
project_root=project_path,
|
||||
reporter=self,
|
||||
use_relative_only=prefs.use_relative_only,
|
||||
pack_target_dir=pack_target_dir,
|
||||
ignore_globs=prefs.ignore_globs(),
|
||||
)
|
||||
batpacker.start()
|
||||
|
||||
# When packing to the filesystem, the final path of the file on the
|
||||
# farm is known immediately.
|
||||
source_file_info = batpacker.source_file_info()
|
||||
abspath_on_farm = pack_target_dir / source_file_info.relpath_in_pack
|
||||
self.blendfile_on_farm = PurePosixPath(abspath_on_farm.as_posix())
|
||||
self.log.info(" %s", abspath_on_farm)
|
||||
|
||||
self.bat_v2_packer = batpacker
|
||||
|
||||
# Start the timer for periodic updates of the packing process. This
|
||||
# needs a relatively fast update cycle, as each file to be copied needs
|
||||
# its own update.
|
||||
#
|
||||
# TODO: if blocking the UI for each file copy gets too annoying, move
|
||||
# the process to a separate thread.
|
||||
wm = context.window_manager
|
||||
wm.flamenco_bat_status = "INVESTIGATING"
|
||||
wm.flamenco_can_abort = True # Only BAT v2 can abort.
|
||||
return True
|
||||
|
||||
def bat_v2_packer_finalize_and_quit(self, context: bpy.types.Context) -> set[str]:
|
||||
if self.bat_v2_packer_reported_error:
|
||||
# The errors themselves should have been reported already.
|
||||
context.window_manager.flamenco_bat_status = "ABORTED"
|
||||
return self._quit(context)
|
||||
|
||||
if self.bat_v2_packer is not None:
|
||||
# BAT v2 pack is done.
|
||||
self.blendfile_on_farm = self.bat_v2_packer.blendfile_location_in_pack()
|
||||
|
||||
self._submit_job(context)
|
||||
return self._quit(context)
|
||||
|
||||
# Reporter Protocol for our BAT v2 interface.
|
||||
# See `BATPackReporter` in BAT's `blender_asset_tracer/pack.py`.
|
||||
def on_error_on_error(self, errormsg: str, ex: Exception) -> None:
|
||||
import traceback
|
||||
|
||||
self.bat_v2_packer_reported_error = True
|
||||
|
||||
# This callback is only called on serious errors that likely indicate
|
||||
# bugs, namely when either `on_copy_error()` or `on_rewrite_error()`
|
||||
# caused an exception themselves.
|
||||
print(60 * "-")
|
||||
print("Flamenco ran into an error while sending files to the farm:")
|
||||
print()
|
||||
print(errormsg)
|
||||
print()
|
||||
traceback.print_exception(ex)
|
||||
bug_report_url = "https://flamenco.blender.org/get-involved"
|
||||
print("Please copy-paste the above into a bug report at", bug_report_url)
|
||||
print()
|
||||
print(60 * "-")
|
||||
self.report({"ERROR"}, "Flamenco: Error sending files, check the terminal")
|
||||
|
||||
def on_copy_start(self, src: Path, dest: PurePath) -> None:
|
||||
bpy.context.window_manager.flamenco_bat_status = "TRANSFERRING"
|
||||
self.log.info("Uploading %s", dest)
|
||||
bpy.context.window_manager.flamenco_bat_status_txt = "Uploading {!s}".format(
|
||||
dest.name
|
||||
)
|
||||
|
||||
def on_copy_done(self, src: Path, dest: PurePath) -> None:
|
||||
assert self.bat_v2_packer is not None
|
||||
num_total, num_done = self.bat_v2_packer.num_files_to_transfer()
|
||||
if num_total < 0:
|
||||
progress = 0
|
||||
else:
|
||||
progress = int(100 * num_done / num_total)
|
||||
|
||||
bpy.context.window_manager.flamenco_bat_progress = progress
|
||||
|
||||
def on_copy_error(self, src: Path, dest: PurePath, errormsg: str) -> None:
|
||||
self.bat_v2_packer_reported_error = True
|
||||
self.report({"ERROR"}, "Copying {!s} to {!s}: {!s}".format(src, dest, errormsg))
|
||||
|
||||
def on_rewrite_error(self, blendfile: Path, save_as: Path, errormsg: str) -> None:
|
||||
self.bat_v2_packer_reported_error = True
|
||||
self.report({"ERROR"}, "Rewriting {!s}: {!s}".format(blendfile, errormsg))
|
||||
|
||||
def on_rewrite_start(self, blendfile: Path, save_as: Path) -> None:
|
||||
self.report({"INFO"}, "Rewriting {!s}".format(blendfile.name))
|
||||
wm = bpy.context.window_manager
|
||||
wm.flamenco_bat_status = "REWRITING"
|
||||
wm.flamenco_bat_status_txt = blendfile.name
|
||||
|
||||
def on_rewrite_done(self, blendfile: Path, save_as: Path) -> None:
|
||||
pass
|
||||
|
||||
def on_missing_file(self, blendfile: Path, relpath_in_pack: PurePath) -> None:
|
||||
self.bat_v2_packer_missing_files.append(blendfile)
|
||||
self.report({"WARNING"}, "Missing file: {!s}".format(blendfile))
|
||||
|
||||
# End of Reporter Protocol.
|
||||
|
||||
def _submit_files_bat_v1(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Ensure that the files are somewhere in the shared storage.
|
||||
|
||||
Returns True if a packing thread has been started, and False otherwise.
|
||||
"""
|
||||
|
||||
from .bat import bat_version
|
||||
from .bat import interface as bat_interface
|
||||
|
||||
self.log.info("Submitting files via BAT %s", bat_version())
|
||||
|
||||
if bat_interface.is_packing():
|
||||
self.report({"ERROR"}, "Another packing operation is running")
|
||||
self._quit(context)
|
||||
@@ -517,190 +604,19 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"File is not already in job storage location, copying it there"
|
||||
)
|
||||
try:
|
||||
self.blendfile_on_farm = self._bat_pack_filesystem(context, blendfile)
|
||||
self.blendfile_on_farm = self._bat_v1_pack_filesystem(
|
||||
context, blendfile
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self._quit(context)
|
||||
return False
|
||||
|
||||
wm = context.window_manager
|
||||
self.timer = wm.event_timer_add(self.TIMER_PERIOD, window=context.window)
|
||||
wm.flamenco_can_abort = False # Only BAT v2 can abort.
|
||||
|
||||
return True
|
||||
|
||||
def _submit_files_bat_bypass(self, context: bpy.types.Context, blendfile: Path) -> bool:
|
||||
"""Bypass BAT: absolute paths, then upload or copy the blend only."""
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
return False
|
||||
|
||||
self.log.info("Converting all relative paths to absolute")
|
||||
self._convert_relpaths_to_absolute(context)
|
||||
|
||||
self.log.info("Saving blend file with absolute paths")
|
||||
blendfile = self._save_blendfile(context)
|
||||
blendfile = bpathlib.make_absolute(blendfile)
|
||||
|
||||
if manager.shared_storage.shaman_enabled:
|
||||
self.log.info("Uploading blend file directly to Shaman (bypassing BAT)")
|
||||
self._upload_blendfile_to_shaman(context, blendfile)
|
||||
self._quit(context)
|
||||
return False
|
||||
if job_submission.is_file_inside_job_storage(context, blendfile):
|
||||
self.log.info(
|
||||
"File is already in job storage location, submitting it as-is"
|
||||
)
|
||||
self._use_blendfile_directly(context, blendfile)
|
||||
return False
|
||||
self.log.info(
|
||||
"File is not already in job storage location, copying it there"
|
||||
)
|
||||
try:
|
||||
self._copy_blendfile_to_storage(context, blendfile)
|
||||
except FileNotFoundError:
|
||||
self._quit(context)
|
||||
return False
|
||||
return False
|
||||
|
||||
def _upload_blendfile_to_shaman(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Upload blend file directly to Shaman without BAT.
|
||||
|
||||
Creates a Shaman checkout with just the blend file, maintaining its
|
||||
relative path from the project root.
|
||||
"""
|
||||
from .bat import cache
|
||||
from .manager.apis import ShamanApi
|
||||
from .manager.models import (
|
||||
ShamanFileSpec,
|
||||
ShamanCheckout,
|
||||
)
|
||||
from .manager.exceptions import ApiException
|
||||
from . import preferences
|
||||
|
||||
api_client = self.get_api_client(context)
|
||||
shaman_api = ShamanApi(api_client)
|
||||
|
||||
# Get project root to calculate relative path
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
|
||||
|
||||
# Calculate relative path from project root
|
||||
try:
|
||||
blendfile_rel_path = blendfile.relative_to(project_path)
|
||||
# Convert to POSIX path for Shaman
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile_rel_path.as_posix())
|
||||
except ValueError:
|
||||
# Blend file is not under project root, use just the filename
|
||||
self.log.warning(
|
||||
"Blend file %s is not under project root %s, using filename only",
|
||||
blendfile,
|
||||
project_path,
|
||||
)
|
||||
blendfile_path_in_checkout = PurePosixPath(blendfile.name)
|
||||
|
||||
# Compute checksum and file size
|
||||
self.log.info("Computing checksum for %s", blendfile.name)
|
||||
checksum = cache.compute_cached_checksum(blendfile)
|
||||
filesize = blendfile.stat().st_size
|
||||
|
||||
# Upload the blend file to Shaman
|
||||
self.log.info("Uploading blend file to Shaman: %s", blendfile.name)
|
||||
try:
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=True,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
except ApiException as ex:
|
||||
if ex.status == 208:
|
||||
# File already known to Shaman
|
||||
self.log.info("Blend file already known to Shaman")
|
||||
elif ex.status == 425:
|
||||
# Defer upload - someone else is uploading
|
||||
self.log.info("Blend file is being uploaded by another client, deferring")
|
||||
# Retry after a short delay
|
||||
import time
|
||||
time.sleep(1)
|
||||
with blendfile.open("rb") as file_reader:
|
||||
shaman_api.shaman_file_store(
|
||||
checksum=checksum,
|
||||
filesize=filesize,
|
||||
body=file_reader,
|
||||
x_shaman_can_defer_upload=False,
|
||||
x_shaman_original_filename=blendfile.name,
|
||||
)
|
||||
else:
|
||||
self.log.error("Error uploading to Shaman: %s", ex)
|
||||
self.report({"ERROR"}, f"Error uploading to Shaman: {ex}")
|
||||
return
|
||||
|
||||
# Create checkout definition with just the blend file
|
||||
checkout_path = self._shaman_checkout_path()
|
||||
filespec = ShamanFileSpec(
|
||||
sha=checksum,
|
||||
size=filesize,
|
||||
path=str(blendfile_path_in_checkout), # Relative path from project root
|
||||
)
|
||||
|
||||
# Create the checkout
|
||||
self.log.info("Creating Shaman checkout: %s", checkout_path)
|
||||
self.log.info("Blend file path in checkout: %s", blendfile_path_in_checkout)
|
||||
checkout = ShamanCheckout(
|
||||
files=[filespec],
|
||||
checkout_path=str(checkout_path),
|
||||
)
|
||||
|
||||
try:
|
||||
result = shaman_api.shaman_checkout(checkout)
|
||||
self.actual_shaman_checkout_path = PurePosixPath(result.checkout_path)
|
||||
# The checkout itself is created in a unique subdirectory. The job's
|
||||
# blendfile must include that checkout path.
|
||||
self.blendfile_on_farm = (
|
||||
PurePosixPath("{jobs}")
|
||||
/ self.actual_shaman_checkout_path
|
||||
/ blendfile_path_in_checkout
|
||||
)
|
||||
self.log.info("Shaman checkout created: %s", self.actual_shaman_checkout_path)
|
||||
self._submit_job(context)
|
||||
except ApiException as ex:
|
||||
self.log.error("Error creating Shaman checkout: %s", ex)
|
||||
self.report({"ERROR"}, f"Error creating Shaman checkout: {ex}")
|
||||
return
|
||||
|
||||
def _copy_blendfile_to_storage(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
"""Copy blend file to job storage without BAT."""
|
||||
import shutil
|
||||
|
||||
manager = self._manager_info(context)
|
||||
if not manager:
|
||||
raise FileNotFoundError("Manager info not known")
|
||||
|
||||
unique_dir = "%s-%s" % (
|
||||
datetime.datetime.now().isoformat("-").replace(":", ""),
|
||||
self.job_name,
|
||||
)
|
||||
pack_target_dir = Path(manager.shared_storage.location) / unique_dir
|
||||
pack_target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
pack_target_file = pack_target_dir / blendfile.name
|
||||
self.log.info("Copying blend file to %s", pack_target_file)
|
||||
|
||||
shutil.copy2(blendfile, pack_target_file)
|
||||
|
||||
self.blendfile_on_farm = PurePosixPath(pack_target_file.as_posix())
|
||||
self.actual_shaman_checkout_path = None
|
||||
|
||||
self._submit_job(context)
|
||||
|
||||
def _bat_pack_filesystem(
|
||||
def _bat_v1_pack_filesystem(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> PurePosixPath:
|
||||
"""Use BAT to store the pack on the filesystem.
|
||||
@@ -709,7 +625,9 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"""
|
||||
from .bat import interface as bat_interface
|
||||
|
||||
project_path = self._bat_project_path(context, blendfile)
|
||||
# Get project path from addon preferences.
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
project_path = bpathlib.make_absolute(Path(bpy.path.abspath(str(project_path))))
|
||||
|
||||
if not project_path.exists():
|
||||
@@ -734,10 +652,8 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
base_blendfile=blendfile,
|
||||
project=project_path,
|
||||
target=str(pack_target_dir),
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
# False: relative_only=True can leave linked blends on KEEP_PATH and hit BAT
|
||||
# _rewrite_paths assertions with Shaman (same as stock 3.8.x + BAT 1.x).
|
||||
relative_only=False,
|
||||
exclusion_filter=prefs.exclusion_filter,
|
||||
relative_only=prefs.use_relative_only,
|
||||
)
|
||||
|
||||
return PurePosixPath(pack_target_file.as_posix())
|
||||
@@ -762,21 +678,23 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
"""
|
||||
from .bat import (
|
||||
interface as bat_interface,
|
||||
)
|
||||
from .bat import (
|
||||
shaman as bat_shaman,
|
||||
)
|
||||
|
||||
assert self.job is not None
|
||||
self.log.info("Sending BAT pack to Shaman")
|
||||
|
||||
project_path = self._bat_project_path(context, blendfile)
|
||||
prefs = preferences.get(context)
|
||||
project_path: Path = prefs.project_root()
|
||||
|
||||
self.packthread = bat_interface.copy(
|
||||
base_blendfile=blendfile,
|
||||
project=project_path,
|
||||
target="/", # Target directory irrelevant for Shaman transfers.
|
||||
exclusion_filter="", # TODO: get from GUI.
|
||||
# See _bat_pack_filesystem: avoid BAT+Shaman KEEP_PATH / _rewrite_paths failure.
|
||||
relative_only=False,
|
||||
exclusion_filter=prefs.exclusion_filter,
|
||||
relative_only=prefs.use_relative_only,
|
||||
packer_class=bat_shaman.Packer,
|
||||
packer_kwargs=dict(
|
||||
api_client=self.get_api_client(context),
|
||||
@@ -821,10 +739,6 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
def _use_blendfile_directly(
|
||||
self, context: bpy.types.Context, blendfile: Path
|
||||
) -> None:
|
||||
# The temporary '.flamenco.blend' file should not be deleted, as it
|
||||
# will be used directly by the render job.
|
||||
self.temp_blendfile = None
|
||||
|
||||
# The blend file is contained in the job storage path, no need to
|
||||
# copy anything.
|
||||
self.blendfile_on_farm = bpathlib.make_absolute(blendfile)
|
||||
@@ -872,10 +786,13 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
if not self._prepare_job_for_submission(context):
|
||||
return
|
||||
|
||||
context.window_manager.flamenco_bat_status = "COMMUNICATING"
|
||||
context.window_manager.flamenco_bat_status_txt = ""
|
||||
|
||||
api_client = self.get_api_client(context)
|
||||
try:
|
||||
submitted_job = job_submission.submit_job(self.job, api_client)
|
||||
except MaxRetryError as ex:
|
||||
except MaxRetryError:
|
||||
self.report({"ERROR"}, "Unable to reach Flamenco Manager")
|
||||
return
|
||||
except HTTPError as ex:
|
||||
@@ -895,7 +812,27 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
self.report({"ERROR"}, f"Could not submit job: {ex.reason}")
|
||||
return
|
||||
|
||||
self.report({"INFO"}, "Job %s submitted" % submitted_job.name)
|
||||
# Show a final report.
|
||||
if self.bat_v2_packer:
|
||||
num_missing_files = len(self.bat_v2_packer_missing_files)
|
||||
else:
|
||||
# Only BATv2 tracks missing files like this.
|
||||
num_missing_files = 0
|
||||
if num_missing_files:
|
||||
self.report(
|
||||
{"WARNING"},
|
||||
"Job {!s} submitted (with {:d} missing files)".format(
|
||||
submitted_job.name, num_missing_files
|
||||
),
|
||||
)
|
||||
context.window_manager.flamenco_bat_status_txt = (
|
||||
"Submitted with {:d} missing files".format(num_missing_files)
|
||||
)
|
||||
else:
|
||||
self.report({"INFO"}, "Job {!s} submitted".format(submitted_job.name))
|
||||
context.window_manager.flamenco_bat_status_txt = ""
|
||||
|
||||
context.window_manager.flamenco_bat_status = "DONE"
|
||||
|
||||
def _check_job(self, context: bpy.types.Context) -> bool:
|
||||
"""Use the Flamenco API to check the Job before submitting files.
|
||||
@@ -911,7 +848,7 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
api_client = self.get_api_client(context)
|
||||
try:
|
||||
job_submission.submit_job_check(self.job, api_client)
|
||||
except MaxRetryError as ex:
|
||||
except MaxRetryError:
|
||||
self.report({"ERROR"}, "Unable to reach Flamenco Manager")
|
||||
return False
|
||||
except HTTPError as ex:
|
||||
@@ -938,9 +875,10 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
Does neither check nor abort the BAT pack thread.
|
||||
"""
|
||||
|
||||
if self.temp_blendfile is not None:
|
||||
self.log.info("Removing temporary file %s", self.temp_blendfile)
|
||||
self.temp_blendfile.unlink(missing_ok=True)
|
||||
if self.bat_v2_packer is not None and not self.bat_v2_packer.is_done:
|
||||
self.log.info("Aborting BAT packer")
|
||||
self.bat_v2_packer.abort()
|
||||
self.bat_v2_packer = None
|
||||
|
||||
if self.timer is not None:
|
||||
context.window_manager.event_timer_remove(self.timer)
|
||||
@@ -948,6 +886,31 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FLAMENCO_OT_abort(bpy.types.Operator):
|
||||
bl_idname = "flamenco.abort"
|
||||
bl_label = "Abort"
|
||||
bl_description = (
|
||||
"Abort a running job submission.\nBlender make take a while to respond to this"
|
||||
)
|
||||
|
||||
ABORTABLE_STATES = {
|
||||
"INVESTIGATING",
|
||||
"REWRITING",
|
||||
"TRANSFERRING",
|
||||
"COMMUNICATING",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: bpy.types.Context) -> bool:
|
||||
wm = context.window_manager
|
||||
return wm.flamenco_can_abort and wm.flamenco_bat_status in cls.ABORTABLE_STATES
|
||||
|
||||
def execute(self, context: bpy.types.Context) -> set[str]:
|
||||
wm = context.window_manager
|
||||
wm.flamenco_bat_status = "ABORTING"
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class FLAMENCO3_OT_explore_file_path(bpy.types.Operator):
|
||||
"""Opens the given path in a file explorer.
|
||||
|
||||
@@ -963,8 +926,8 @@ class FLAMENCO3_OT_explore_file_path(bpy.types.Operator):
|
||||
)
|
||||
|
||||
def execute(self, context):
|
||||
import platform
|
||||
import pathlib
|
||||
import platform
|
||||
|
||||
# Possibly open a parent of the path
|
||||
to_open = pathlib.Path(self.path)
|
||||
@@ -1001,6 +964,7 @@ classes = (
|
||||
FLAMENCO_OT_ping_manager,
|
||||
FLAMENCO_OT_eval_setting,
|
||||
FLAMENCO_OT_submit_job,
|
||||
FLAMENCO_OT_abort,
|
||||
FLAMENCO3_OT_explore_file_path,
|
||||
)
|
||||
register, unregister = bpy.utils.register_classes_factory(classes)
|
||||
|
||||
@@ -5,7 +5,7 @@ from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
from . import projects, manager_info
|
||||
from . import manager_info, projects
|
||||
|
||||
|
||||
def discard_flamenco_client(context):
|
||||
@@ -65,16 +65,6 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
|
||||
items=_project_finder_enum_items,
|
||||
)
|
||||
|
||||
bat_bypass: bpy.props.BoolProperty( # type: ignore
|
||||
name="Bypass BAT",
|
||||
description=(
|
||||
"When enabled, submission skips Blender Asset Tracer: paths in the blend "
|
||||
"are written as absolute, then the blend is uploaded or copied without a BAT pack. "
|
||||
"When disabled, Flamenco uses the normal BAT pack (Shaman or job storage)"
|
||||
),
|
||||
default=True,
|
||||
)
|
||||
|
||||
# Property that gets its value from the above _job_storage, and cannot be
|
||||
# set. This makes it read-only in the GUI.
|
||||
job_storage_for_gui: bpy.props.StringProperty( # type: ignore
|
||||
@@ -86,6 +76,21 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
|
||||
get=lambda prefs: prefs._job_storage(),
|
||||
)
|
||||
|
||||
use_relative_only: bpy.props.BoolProperty( # type: ignore
|
||||
name="Relative Paths Only",
|
||||
default=True,
|
||||
description="When sending files to Flamenco, only include assets that are referenced by "
|
||||
"relative path. Absolute paths are then assumed to be valid on all Workers. When turned "
|
||||
"off, all files are sent, regardless of how they are referenced",
|
||||
)
|
||||
exclusion_filter: bpy.props.StringProperty( # type: ignore
|
||||
name="Exclusion Filter",
|
||||
default="",
|
||||
description="Space-separated list of file glob patterns. When sending files to Flamenco, "
|
||||
"exclude any file that matches a pattern in this list. For example: '*.abc *.vdb' to skip "
|
||||
"copying all Alembic and OpenVDB files",
|
||||
)
|
||||
|
||||
def draw(self, context: bpy.types.Context) -> None:
|
||||
layout = self.layout
|
||||
layout.use_property_decorate = False
|
||||
@@ -128,9 +133,9 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
|
||||
else:
|
||||
text_row(col, str(project_root))
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.label(text="Submission")
|
||||
col.prop(self, "bat_bypass")
|
||||
col = layout.column(heading="File Submission")
|
||||
col.prop(self, "use_relative_only")
|
||||
col.prop(self, "exclusion_filter")
|
||||
|
||||
def project_root(self) -> Path:
|
||||
"""Use the configured project finder to find the project root directory."""
|
||||
@@ -150,13 +155,17 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
|
||||
return "Unknown, refresh first."
|
||||
return str(info.shared_storage.location)
|
||||
|
||||
def ignore_globs(self) -> set[str]:
|
||||
"""Return exclusion filter as set of strings."""
|
||||
return set(self.exclusion_filter.strip().split())
|
||||
|
||||
|
||||
def get(context: bpy.types.Context) -> FlamencoPreferences:
|
||||
"""Return the add-on preferences."""
|
||||
prefs = context.preferences.addons["flamenco"].preferences
|
||||
assert isinstance(
|
||||
prefs, FlamencoPreferences
|
||||
), "Expected FlamencoPreferences, got %s instead" % (type(prefs))
|
||||
assert isinstance(prefs, FlamencoPreferences), (
|
||||
"Expected FlamencoPreferences, got %s instead" % (type(prefs))
|
||||
)
|
||||
return prefs
|
||||
|
||||
|
||||
|
||||
@@ -4,17 +4,34 @@
|
||||
|
||||
import contextlib
|
||||
import importlib
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Iterator, Iterable
|
||||
from typing import Iterable, Iterator
|
||||
|
||||
_my_dir = Path(__file__).parent
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
|
||||
def filename(
|
||||
module_name: str,
|
||||
*,
|
||||
filename_prefix: str = "",
|
||||
) -> Path:
|
||||
"""Returns the filename of the wheel file for this module."""
|
||||
|
||||
if not filename_prefix:
|
||||
filename_prefix = _fname_prefix_from_module_name(module_name)
|
||||
return _wheel_filename(filename_prefix)
|
||||
|
||||
|
||||
def load_wheel(
|
||||
module_name: str,
|
||||
submodules: Iterable[str],
|
||||
*,
|
||||
filename_prefix: str = "",
|
||||
) -> list[ModuleType]:
|
||||
"""Loads modules from a wheel file 'module_name*.whl'.
|
||||
|
||||
Loads `module_name`, and if submodules are given, loads
|
||||
@@ -25,8 +42,7 @@ def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
|
||||
Returns the loaded modules, so [module, submodule, submodule, ...].
|
||||
"""
|
||||
|
||||
fname_prefix = _fname_prefix_from_module_name(module_name)
|
||||
wheel = _wheel_filename(fname_prefix)
|
||||
wheel = filename(module_name, filename_prefix=filename_prefix)
|
||||
|
||||
loaded_modules: list[ModuleType] = []
|
||||
to_load = [module_name] + [f"{module_name}.{submodule}" for submodule in submodules]
|
||||
@@ -47,9 +63,9 @@ def load_wheel(module_name: str, submodules: Iterable[str]) -> list[ModuleType]:
|
||||
loaded_modules.append(module)
|
||||
_log.info("Loaded %s from %s", modname, module.__file__)
|
||||
|
||||
assert len(loaded_modules) == len(
|
||||
to_load
|
||||
), f"expecting to load {len(to_load)} modules, but only have {len(loaded_modules)}: {loaded_modules}"
|
||||
assert len(loaded_modules) == len(to_load), (
|
||||
f"expecting to load {len(to_load)} modules, but only have {len(loaded_modules)}: {loaded_modules}"
|
||||
)
|
||||
return loaded_modules
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# goo_physics
|
||||
StudioGoo bone physics addon
|
||||
@@ -0,0 +1,34 @@
|
||||
# This program is free software; you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation; either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTIBILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from bpy.utils import register_submodule_factory
|
||||
|
||||
bl_info = {
|
||||
"name": "Goo Physics",
|
||||
"author": "Cody Winchester (CodyWinch)",
|
||||
"description": "",
|
||||
"blender": (4, 1, 0),
|
||||
"version": (1, 0, 4),
|
||||
"location": "",
|
||||
"warning": "",
|
||||
"category": "Animation",
|
||||
}
|
||||
|
||||
|
||||
submodules = [
|
||||
"ui",
|
||||
"properties",
|
||||
"operators",
|
||||
]
|
||||
|
||||
register, unregister = register_submodule_factory(__name__, submodules)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"DEFAULTCLOTH": {
|
||||
"Name": "Default Cloth",
|
||||
"Description": "Default settings for cloth physics",
|
||||
"Settings": {
|
||||
"gp_chain_speed": 2.0,
|
||||
"gp_chain_mass": 0.3,
|
||||
"gp_chain_air_dampening": 0.5,
|
||||
"gp_chain_bend_damping": 0.5,
|
||||
"gp_chain_collision_dist": 0.025,
|
||||
"gp_chain_gravity": 1.0,
|
||||
"gp_sim_influence": 1.0
|
||||
}
|
||||
},
|
||||
"STIFFCLOTH": {
|
||||
"Name": "Stiff Cloth",
|
||||
"Description": "Best used for stiff chained rigs falling down, like a flag. First bone in the chain will not be simulated",
|
||||
"Settings": {
|
||||
"gp_chain_speed": 0.8,
|
||||
"gp_chain_mass": 0.3,
|
||||
"gp_chain_air_dampening": 2.0,
|
||||
"gp_chain_bend_damping": 10.0,
|
||||
"gp_chain_collision_dist": 0.025,
|
||||
"gp_chain_gravity": 1.0,
|
||||
"gp_sim_influence": 1.0
|
||||
}
|
||||
},
|
||||
"FLOPPYCLOTH": {
|
||||
"Name": "Floppy Cloth",
|
||||
"Description": "Best used for a heavy floppy chained rigs dangling down",
|
||||
"Settings": {
|
||||
"gp_chain_speed": 2.0,
|
||||
"gp_chain_mass": 1.0,
|
||||
"gp_chain_air_dampening": 2.0,
|
||||
"gp_chain_bend_damping": 2.0,
|
||||
"gp_chain_collision_dist": 0.025,
|
||||
"gp_chain_gravity": 1.0,
|
||||
"gp_sim_influence": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"DEFAULTGEONODES": {
|
||||
"Name": "Default Geo Nodes",
|
||||
"Description": "Default settings for geo nodes physics",
|
||||
"Settings": {
|
||||
"gp_chain_velocity": 1.0,
|
||||
"gp_chain_dampening": 0.1,
|
||||
"gp_chain_gravity": 0.02,
|
||||
"gp_chain_root_falloff": 0.25,
|
||||
"gp_chain_stiffness": 0.25,
|
||||
"gp_chain_stiff_end_fac": 0.25,
|
||||
"gp_chain_stiff_vel_fac": 0.2,
|
||||
"gp_chain_stiff_vel_min": 0.1,
|
||||
"gp_chain_stiff_vel_max": 1.0,
|
||||
"gp_chain_wind_strength": 0.25,
|
||||
"gp_chain_wind_noise_strength": 1.0,
|
||||
"gp_chain_wind_noise_scale": 1.0,
|
||||
"gp_chain_collision_dist": 0.01,
|
||||
"gp_chain_collision_friction": 0.25,
|
||||
"gp_sim_influence": 1.0
|
||||
}
|
||||
},
|
||||
"HAIRFRINGE": {
|
||||
"Name": "Hair Fringe",
|
||||
"Description": "Used for Bangs/Fringe, where the base doesnt move much and theres slight movement at the tips to prevent the character going off model",
|
||||
"Settings": {
|
||||
"gp_chain_velocity": 1.0,
|
||||
"gp_chain_dampening": 0.1,
|
||||
"gp_chain_gravity": 0.02,
|
||||
"gp_chain_root_falloff": 0.92,
|
||||
"gp_chain_stiffness": 0.5,
|
||||
"gp_chain_stiff_end_fac": 0.2,
|
||||
"gp_chain_stiff_vel_fac": 0.2,
|
||||
"gp_chain_stiff_vel_min": 0.1,
|
||||
"gp_chain_stiff_vel_max": 1.0,
|
||||
"gp_chain_wind_strength": 0.06,
|
||||
"gp_chain_wind_noise_strength": 2.1,
|
||||
"gp_chain_wind_noise_scale": 10.0,
|
||||
"gp_chain_collision_dist": 0.01,
|
||||
"gp_chain_collision_friction": 0.25,
|
||||
"gp_sim_influence": 1.0
|
||||
}
|
||||
},
|
||||
"HAIRSIDE": {
|
||||
"Name": "Hair Side",
|
||||
"Description": "Medium length hair strands, usually for side burns longer than the bangs. Can also be used for skirts",
|
||||
"Settings": {
|
||||
"gp_chain_velocity": 1.0,
|
||||
"gp_chain_dampening": 0.1,
|
||||
"gp_chain_gravity": 0.02,
|
||||
"gp_chain_root_falloff": 0.5,
|
||||
"gp_chain_stiffness": 0.4,
|
||||
"gp_chain_stiff_end_fac": 0.2,
|
||||
"gp_chain_stiff_vel_fac": 0.2,
|
||||
"gp_chain_stiff_vel_min": 0.1,
|
||||
"gp_chain_stiff_vel_max": 1.0,
|
||||
"gp_chain_wind_strength": 0.08,
|
||||
"gp_chain_wind_noise_strength": 2.0,
|
||||
"gp_chain_wind_noise_scale": 5.0,
|
||||
"gp_chain_collision_dist": 0.01,
|
||||
"gp_chain_collision_friction": 0.25,
|
||||
"gp_sim_influence": 0.5
|
||||
}
|
||||
},
|
||||
"HAIRPONYTAIL": {
|
||||
"Name": "Hair Ponytail",
|
||||
"Description": "Long length bone chains with lots of drag at the end. Can be used for dresses and capes as well",
|
||||
"Settings": {
|
||||
"gp_chain_velocity": 1.0,
|
||||
"gp_chain_dampening": 0.1,
|
||||
"gp_chain_gravity": 0.02,
|
||||
"gp_chain_root_falloff": 0.42,
|
||||
"gp_chain_stiffness": 0.0,
|
||||
"gp_chain_stiff_end_fac": 0.2,
|
||||
"gp_chain_stiff_vel_fac": 0.2,
|
||||
"gp_chain_stiff_vel_min": 0.1,
|
||||
"gp_chain_stiff_vel_max": 1.0,
|
||||
"gp_chain_wind_strength": 0.125,
|
||||
"gp_chain_wind_noise_strength": 2.02,
|
||||
"gp_chain_wind_noise_scale": 5.0,
|
||||
"gp_chain_collision_dist": 0.01,
|
||||
"gp_chain_collision_friction": 0.25,
|
||||
"gp_sim_influence": 0.5
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"DEFAULTJIGGLE": {
|
||||
"Name": "Default Jiggle",
|
||||
"Description": "Default settings for jiggle physics",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.8,
|
||||
"gp_sim_friction": 5.0,
|
||||
"gp_sim_mass": 0.15,
|
||||
"gp_sim_stiffness": 0.1,
|
||||
"gp_sim_damping": 8.0,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"JIGGLELOOSE": {
|
||||
"Name": "Jiggle Loose",
|
||||
"Description": "Location based Jiggle offset for Secondary Animation",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.77,
|
||||
"gp_sim_friction": 5.0,
|
||||
"gp_sim_mass": 0.2,
|
||||
"gp_sim_stiffness": 0.1,
|
||||
"gp_sim_damping": 10.06,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"JIGGLESTIFF": {
|
||||
"Name": "Jiggle Stiff",
|
||||
"Description": "Location based Jiggle offset for Secondary Animation with less bounce",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.83,
|
||||
"gp_sim_friction": 5.0,
|
||||
"gp_sim_mass": 0.15,
|
||||
"gp_sim_stiffness": 0.3,
|
||||
"gp_sim_damping": 10.06,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,494 @@
|
||||
{
|
||||
"DEFAULTSOFTBODY": {
|
||||
"Name": "Default Soft Body",
|
||||
"Description": "Default settings for soft body physics",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.8,
|
||||
"gp_sim_friction": 5.0,
|
||||
"gp_sim_mass": 0.15,
|
||||
"gp_sim_stiffness": 0.1,
|
||||
"gp_sim_damping": 8.0,
|
||||
"gp_sim_strength": 0.95,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"SLOWMO": {
|
||||
"Name": "Slowmo",
|
||||
"Description": "Preset for Slow motion or underwater scenarios",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.4,
|
||||
"gp_sim_friction": 10.0,
|
||||
"gp_sim_mass": 0.2,
|
||||
"gp_sim_stiffness": 0.22,
|
||||
"gp_sim_damping": 15.0,
|
||||
"gp_sim_strength": 0.68,
|
||||
"gp_sim_influence": 0.44,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"GOOFLAPPY": {
|
||||
"Name": "Goo Flappy",
|
||||
"Description": "Settings for very light objects with a lot of drag (end chains of hair/skirts)",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.8,
|
||||
"gp_sim_friction": 10.0,
|
||||
"gp_sim_mass": 0.2,
|
||||
"gp_sim_stiffness": 0.1,
|
||||
"gp_sim_damping": 1.0,
|
||||
"gp_sim_strength": 0.8,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"GOOSTIFF": {
|
||||
"Name": "Goo Stiff",
|
||||
"Description": "Settings for stiff objects with resistance, used for the first bone in long chains where the base shouldnt move much",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.8,
|
||||
"gp_sim_friction": 5.0,
|
||||
"gp_sim_mass": 0.15,
|
||||
"gp_sim_stiffness": 0.1,
|
||||
"gp_sim_damping": 8.0,
|
||||
"gp_sim_strength": 0.95,
|
||||
"gp_sim_influence": 0.5,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"METAL/JEWELRY": {
|
||||
"Name": "Metal/Jewelry",
|
||||
"Description": "Mainly for earrings and necklaces",
|
||||
"Settings": {
|
||||
"gp_sim_speed": 0.8,
|
||||
"gp_sim_friction": 10.0,
|
||||
"gp_sim_mass": 0.25,
|
||||
"gp_sim_stiffness": 1.0,
|
||||
"gp_sim_damping": 4.57,
|
||||
"gp_sim_strength": 0.43,
|
||||
"gp_sim_influence": 1.0,
|
||||
"gp_sim_gravity": 1.0
|
||||
}
|
||||
},
|
||||
"GOOSIM": {
|
||||
"Name": "Goo Sim_CHAIN",
|
||||
"Description": "Chain preset with Goo Stiff at the base and Goo Flappy with a influence falloff down the chain",
|
||||
"Settings": {
|
||||
"gp_sim_speed": [
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8,
|
||||
0.8
|
||||
],
|
||||
"gp_sim_friction": [
|
||||
5.0,
|
||||
6.018315315246582,
|
||||
7.02327823638916,
|
||||
7.975924968719482,
|
||||
8.800024032592773,
|
||||
9.410675048828125,
|
||||
9.774394989013672,
|
||||
9.93781852722168,
|
||||
9.989137649536133,
|
||||
9.999103546142578,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0,
|
||||
10.0
|
||||
],
|
||||
"gp_sim_mass": [
|
||||
0.15000000596046448,
|
||||
0.1601831614971161,
|
||||
0.17023277282714844,
|
||||
0.17975923418998718,
|
||||
0.18800023198127747,
|
||||
0.19410674273967743,
|
||||
0.1977439522743225,
|
||||
0.19937819242477417,
|
||||
0.19989138841629028,
|
||||
0.19999104738235474,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224,
|
||||
0.20000000298023224
|
||||
],
|
||||
"gp_sim_stiffness": [
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1,
|
||||
0.1
|
||||
],
|
||||
"gp_sim_damping": [
|
||||
8.0,
|
||||
6.574358940124512,
|
||||
5.1674113273620605,
|
||||
3.8337059020996094,
|
||||
2.679966688156128,
|
||||
1.8250558376312256,
|
||||
1.3158482313156128,
|
||||
1.0870535373687744,
|
||||
1.0152064561843872,
|
||||
1.0012555122375488,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
],
|
||||
"gp_sim_strength": [
|
||||
0.9499989748001099,
|
||||
0.9194496870040894,
|
||||
0.889301061630249,
|
||||
0.8607218265533447,
|
||||
0.835999071598053,
|
||||
0.8176796436309814,
|
||||
0.8067681789398193,
|
||||
0.8018654584884644,
|
||||
0.800325870513916,
|
||||
0.8000268936157227,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929,
|
||||
0.800000011920929
|
||||
],
|
||||
"gp_sim_influence": [
|
||||
0.00826446246355772,
|
||||
0.013331254944205284,
|
||||
0.018442191183567047,
|
||||
0.023726072162389755,
|
||||
0.029434900730848312,
|
||||
0.035850003361701965,
|
||||
0.04309063404798508,
|
||||
0.05104939639568329,
|
||||
0.059576407074928284,
|
||||
0.06869472563266754,
|
||||
0.07856345176696777,
|
||||
0.08924980461597443,
|
||||
0.10063616931438446,
|
||||
0.11259077489376068,
|
||||
0.12515480816364288,
|
||||
0.13848046958446503,
|
||||
0.15261253714561462,
|
||||
0.16742651164531708,
|
||||
0.18280872702598572,
|
||||
0.19881850481033325,
|
||||
0.2156010866165161,
|
||||
0.23317888379096985,
|
||||
0.25142043828964233,
|
||||
0.2702302634716034,
|
||||
0.28968575596809387,
|
||||
0.30992525815963745,
|
||||
0.33094877004623413,
|
||||
0.35261791944503784,
|
||||
0.3748553693294525,
|
||||
0.39775657653808594,
|
||||
0.42145299911499023,
|
||||
0.44592225551605225,
|
||||
0.47101902961730957,
|
||||
0.49668407440185547,
|
||||
0.5230309963226318,
|
||||
0.5501843690872192,
|
||||
0.578099250793457,
|
||||
0.606623649597168,
|
||||
0.6357163190841675,
|
||||
0.6655089855194092,
|
||||
0.6961193084716797,
|
||||
0.7274799346923828,
|
||||
0.7594319581985474,
|
||||
0.7919522523880005,
|
||||
0.8251906037330627,
|
||||
0.8592544794082642,
|
||||
0.8940246105194092,
|
||||
0.9292197227478027,
|
||||
0.9645878076553345,
|
||||
1.0
|
||||
],
|
||||
"gp_sim_gravity": [
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,689 @@
|
||||
from .functions import *
|
||||
|
||||
|
||||
#
|
||||
# UI
|
||||
#
|
||||
|
||||
|
||||
def draw_subpanel(layout, prefs, menu_name, label, icon):
|
||||
|
||||
box = layout.box()
|
||||
row = box.row()
|
||||
row.alignment = "LEFT"
|
||||
row.prop(
|
||||
prefs,
|
||||
menu_name,
|
||||
icon="TRIA_DOWN" if getattr(prefs, menu_name) else "TRIA_RIGHT",
|
||||
icon_only=True,
|
||||
emboss=False,
|
||||
)
|
||||
row.label(text=label, icon=icon)
|
||||
|
||||
return box, getattr(prefs, menu_name)
|
||||
|
||||
|
||||
#
|
||||
|
||||
|
||||
class GOO_PT_main_panel(bpy.types.Panel):
|
||||
"""Creates a Panel in the scene context of the properties editor"""
|
||||
|
||||
bl_idname = "GOO_PT_main_panel"
|
||||
bl_label = "Goo Animation Tools"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Goo Animation Tools"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
|
||||
class GP_PT_goophys_panel(Panel):
|
||||
"""Creates a Panel in the scene context of the properties editor"""
|
||||
|
||||
bl_idname = "GP_PT_goophys_panel"
|
||||
bl_label = "Goo Physics"
|
||||
bl_parent_id = "GOO_PT_main_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Goo Animation Tools"
|
||||
bl_order = 1
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
pb = context.active_pose_bone
|
||||
scn = context.scene
|
||||
prefs = context.preferences.addons["goo_physics"].preferences
|
||||
|
||||
if pb is not None:
|
||||
box = layout.box()
|
||||
|
||||
if (pb.gp_has_sb_physics or pb.gp_has_cl_physics or pb.gp_has_gn_physics or pb.gp_has_jg_physics) == False:
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text="Mode:")
|
||||
row = box.row(align=True)
|
||||
row.prop(prefs, "gp_physics_type", text="")
|
||||
|
||||
if prefs.gp_physics_type == "GEO_NODES":
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_geonodes_physics_to_selected",
|
||||
text="Add Physics",
|
||||
)
|
||||
row.scale_y = 2.5
|
||||
|
||||
if prefs.gp_physics_type == "SIMP_SOFTBODY":
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_soft_physics_to_selected",
|
||||
text="Add Physics",
|
||||
)
|
||||
row.scale_y = 2.5
|
||||
|
||||
if prefs.gp_physics_type == "CLOTH":
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_cloth_physics_to_selected",
|
||||
text="Add Physics",
|
||||
)
|
||||
row.scale_y = 2.5
|
||||
|
||||
if prefs.gp_physics_type == "JIGGLE":
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_jiggle_physics_to_selected",
|
||||
text="Add Physics",
|
||||
)
|
||||
row.scale_y = 2.5
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.prop(prefs, "gp_physics_active", text="Physics Active")
|
||||
row.alignment = "CENTER"
|
||||
|
||||
else:
|
||||
|
||||
#
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator(
|
||||
"goophys.remove_physics_from_selected",
|
||||
text="Remove Physics from Selected Bones",
|
||||
)
|
||||
row.scale_y = 1.5
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.bake_physics", text="Bake Bone Physics")
|
||||
row.scale_y = 2
|
||||
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.select_dynamic_bones", text="Select Physics Bones")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.select_last_dynamic_bones", text="Select Last Bone in Chains")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.prop(prefs, "gp_physics_active", text="Physics Active")
|
||||
row.alignment = "CENTER"
|
||||
|
||||
#
|
||||
|
||||
box = layout.box()
|
||||
|
||||
if pb.gp_has_cl_physics:
|
||||
label = "Cloth Physics Settings"
|
||||
icon = "MOD_CLOTH"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text="Presets:")
|
||||
row = box.row(align=True)
|
||||
row.prop(prefs, "cl_presets", text="")
|
||||
row.operator("goophys.add_preset", text="", icon="ADD")
|
||||
row.operator("goophys.delete_preset", text="", icon="REMOVE")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.apply_preset", text="Apply Preset")
|
||||
|
||||
if pb.gp_has_sb_physics:
|
||||
label = "Soft Body Physics Settings"
|
||||
icon = "MOD_SOFT"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text="Presets:")
|
||||
row = box.row(align=True)
|
||||
row.prop(prefs, "sb_presets", text="")
|
||||
row.operator("goophys.add_preset", text="", icon="ADD")
|
||||
row.operator("goophys.delete_preset", text="", icon="REMOVE")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.apply_preset", text="Apply Preset")
|
||||
|
||||
if pb.gp_has_jg_physics:
|
||||
label = "Jiggle Physics Settings"
|
||||
icon = "MOD_SCREW"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text="Presets:")
|
||||
row = box.row(align=True)
|
||||
row.prop(prefs, "jg_presets", text="")
|
||||
row.operator("goophys.add_preset", text="", icon="ADD")
|
||||
row.operator("goophys.delete_preset", text="", icon="REMOVE")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.apply_preset", text="Apply Preset")
|
||||
|
||||
if pb.gp_has_gn_physics:
|
||||
label = "Geo Nodes Physics Settings"
|
||||
icon = "GEOMETRY_NODES"
|
||||
|
||||
row = box.row(align=True)
|
||||
row.label(text="Presets:")
|
||||
row = box.row(align=True)
|
||||
row.prop(prefs, "gn_presets", text="")
|
||||
row.operator("goophys.add_preset", text="", icon="ADD")
|
||||
row.operator("goophys.delete_preset", text="", icon="REMOVE")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.apply_preset", text="Apply Preset")
|
||||
|
||||
boxx, expanded = draw_subpanel(box, prefs, "settings_menu", label, icon)
|
||||
if expanded:
|
||||
|
||||
if pb.gp_has_cl_physics:
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_chains")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "chain_menu", "Chain Settings", "RESTRICT_INSTANCED_OFF")
|
||||
if expanded:
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "cl_time_menu", "Time", "TIME")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_speed", text="Sim Speed")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "cl_physical_menu", "Physical", "MOD_SOLIDIFY")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_mass", text="Sim Mass")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_gravity", text="Gravity")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "cl_structure_menu", "Structure", "SELECT_INTERSECT")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_air_dampening", text="Sim Air Dampening")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_bend_damping", text="Sim Bend Damping")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "cl_collision_menu", "Collision", "MOD_PHYSICS")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_use_collision", text="Use Collision")
|
||||
|
||||
if pb.gp_chain_use_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_collision_dist", text="Collision Distance")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.refresh_collision_collection",
|
||||
text="Refresh Collision Objects")
|
||||
|
||||
if pb.gp_chain_limit_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Set Collisions to All")
|
||||
else:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Limit Collisions to Collection")
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_bones")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "bone_menu", "Bone Settings", "BONE_DATA")
|
||||
if expanded:
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_influence", text="Sim Influence")
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Linear Sim Influence Falloff",
|
||||
).type = 0
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Quadratic Sim Influence Falloff",
|
||||
).type = 1
|
||||
|
||||
if pb.gp_has_sb_physics:
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_bones")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "bone_menu", "Bone Settings", "BONE_DATA")
|
||||
if expanded:
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_time_menu", "Time", "TIME")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_speed", text="Sim Speed")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_physical_menu", "Physical", "MOD_SOLIDIFY")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_friction", text="Sim Friction")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_mass", text="Sim Mass")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_gravity", text="Gravity")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_structure_menu", "Structure", "SELECT_INTERSECT")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_stiffness", text="Sim Stiffness")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_damping", text="Sim Damping")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_strength", text="Sim Strength")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_collision_menu", "Collision", "MOD_PHYSICS")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_use_collision", text="Use Collision")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.refresh_collision_collection",
|
||||
text="Refresh Collision Objects")
|
||||
|
||||
if pb.gp_sim_limit_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Set Collisions to All")
|
||||
else:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Limit Collisions to Collection")
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_influence", text="Sim Influence")
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Linear Sim Influence Falloff",
|
||||
).type = 0
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Quadratic Sim Influence Falloff",
|
||||
).type = 1
|
||||
|
||||
if pb.gp_has_jg_physics:
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_bones")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "bone_menu", "Bone Settings", "BONE_DATA")
|
||||
if expanded:
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_time_menu", "Time", "TIME")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_speed", text="Sim Speed")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "sb_physical_menu", "Physical", "MOD_SOLIDIFY")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_friction", text="Sim Friction")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_mass", text="Sim Mass")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_gravity", text="Gravity")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "jg_physical_menu", "Physical", "MOD_SOLIDIFY")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_stiffness", text="Sim Stiffness")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_damping", text="Sim Damping")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "jg_collision_menu", "Collision", "MOD_PHYSICS")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_use_collision", text="Use Collision")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.refresh_collision_collection",
|
||||
text="Refresh Collision Objects")
|
||||
|
||||
if pb.gp_sim_limit_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Set Collisions to All")
|
||||
else:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Limit Collisions to Collection")
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_influence", text="Sim Influence")
|
||||
|
||||
if pb.gp_has_gn_physics:
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_chains")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "chain_menu", "Chain Settings", "RESTRICT_INSTANCED_OFF")
|
||||
if expanded:
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "gn_general_menu", "General", "OUTLINER_DATA_GP_LAYER")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_velocity", text="Velocity Scaler")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_dampening", text="Velocity Dampening")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_gravity", text="Gravity")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_root_falloff", text="Root Falloff")
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "gn_stiff_menu", "Stiffness", "AUTOMERGE_ON")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_stiffness", text="Stiffness")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(
|
||||
pb, "gp_chain_stiff_end_fac", text="Tip Stiffness Factor"
|
||||
)
|
||||
|
||||
boxxxxx, expanded = draw_subpanel(boxxxx, prefs, "gn_stiff_adv_menu", "Advanced Stiffness", "CON_TRANSLIKE")
|
||||
if expanded:
|
||||
|
||||
row = boxxxxx.row(align=True)
|
||||
row.prop(
|
||||
pb, "gp_chain_stiff_vel_fac", text="Stiffness Velocity Factor"
|
||||
)
|
||||
|
||||
row = boxxxxx.row(align=True)
|
||||
row.prop(
|
||||
pb, "gp_chain_stiff_vel_min", text="Stiffness Velocity Min"
|
||||
)
|
||||
|
||||
row = boxxxxx.row(align=True)
|
||||
row.prop(
|
||||
pb, "gp_chain_stiff_vel_max", text="Stiffness Velocity Max"
|
||||
)
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "gn_wind_menu", "Wind", "FORCE_WIND")
|
||||
if expanded:
|
||||
has_wind = False
|
||||
ob = bpy.data.objects.get(pb.gp_sim_object)
|
||||
mod = None
|
||||
if ob:
|
||||
mod = ob.modifiers.get("GP_Nodes Sim")
|
||||
if mod:
|
||||
has_wind = get_geo_nodes_input(mod, "Wind Object") is not None
|
||||
|
||||
if has_wind == False:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_wind_objects", text="Add Wind Controller to Chains"
|
||||
)
|
||||
row.scale_y = 2.0
|
||||
|
||||
boxxxx.separator()
|
||||
|
||||
if mod is not None:
|
||||
wind_ob_ctr, wind_ob_str = get_geo_nodes_input_str(mod, "Wind Object")
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(wind_ob_ctr, wind_ob_str, text="Wind Controller")
|
||||
|
||||
else:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_wind_strength", text="Wind Strength")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_wind_noise_strength", text="Wind Noise Strength")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_wind_noise_scale", text="Wind Noise Scale")
|
||||
|
||||
boxxxx.separator()
|
||||
|
||||
if mod is not None:
|
||||
wind_ob_ctr, wind_ob_str = get_geo_nodes_input_str(mod, "Wind Object")
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(wind_ob_ctr, wind_ob_str, text="Wind Controller")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.copy_active_wind_controller",
|
||||
text="Copy Controller to Selected Chains",
|
||||
)
|
||||
row.scale_y = 1.5
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.remove_wind_objects", text="Remove Controller from Chains"
|
||||
)
|
||||
row.scale_y = 2.0
|
||||
|
||||
boxxxx, expanded = draw_subpanel(boxxx, prefs, "gn_collision_menu", "Collision", "MOD_PHYSICS")
|
||||
if expanded:
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_use_collision", text="Use Collision")
|
||||
|
||||
if pb.gp_chain_use_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(pb, "gp_chain_collision_dist", text="Collision Distance")
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.prop(
|
||||
pb, "gp_chain_collision_friction", text="Collision Friction"
|
||||
)
|
||||
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.refresh_collision_collection",
|
||||
text="Refresh Collision Objects")
|
||||
|
||||
if pb.gp_chain_limit_collision:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Set Collisions to All")
|
||||
else:
|
||||
row = boxxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.limit_to_type",
|
||||
text="Limit Collisions to Collection")
|
||||
|
||||
row = boxx.row(align=True)
|
||||
row.prop(prefs, "apply_to_all_bones")
|
||||
|
||||
boxxx, expanded = draw_subpanel(boxx, prefs, "bone_menu", "Bone Settings", "BONE_DATA")
|
||||
if expanded:
|
||||
row = boxxx.row(align=True)
|
||||
row.prop(pb, "gp_sim_influence", text="Sim Influence")
|
||||
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Linear Sim Influence Falloff",
|
||||
).type = 0
|
||||
row = boxxx.row(align=True)
|
||||
row.operator(
|
||||
"goophys.apply_falloff_to_selected",
|
||||
text="Apply Quadratic Sim Influence Falloff",
|
||||
).type = 1
|
||||
|
||||
#
|
||||
|
||||
elif context.mode == "POSE":
|
||||
box = layout.box()
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.select_dynamic_bones", text="Select Physics Bones")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.operator("goophys.select_last_dynamic_bones", text="Select Last Bone in Chains")
|
||||
|
||||
row = box.row(align=True)
|
||||
row.alignment = "CENTER"
|
||||
row.prop(prefs, "gp_physics_active", text="Physics Active")
|
||||
row.alignment = "CENTER"
|
||||
|
||||
|
||||
class GP_PT_goophys_collider_panel(Panel):
|
||||
"""Creates a Panel in the scene context of the properties editor"""
|
||||
|
||||
bl_idname = "GP_PT_goophys_collider_panel"
|
||||
bl_label = "Colliders"
|
||||
bl_parent_id = "GP_PT_goophys_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Goo Animation Tools"
|
||||
bl_order = 1
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and context.mode == "OBJECT"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
"goophys.add_collider", text="Collider From Selected Objects"
|
||||
)
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
"goophys.colliders_from_mesh", text="Colliders From Mesh Deform Weights"
|
||||
)
|
||||
|
||||
|
||||
class GP_PT_goophys_advanced_panel(Panel):
|
||||
"""Creates a Panel in the scene context of the properties editor"""
|
||||
|
||||
bl_idname = "GP_PT_goophys_advanced_panel"
|
||||
bl_label = "Advanced"
|
||||
bl_parent_id = "GP_PT_goophys_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Goo Animation Tools"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 2
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return context.active_object and context.mode == "POSE"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
prefs = context.preferences.addons["goo_physics"].preferences
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.label(text="Bone Map Presets:")
|
||||
row = layout.row(align=True)
|
||||
row.prop(prefs, "bm_presets", text="")
|
||||
row.operator("goophys.add_bone_map_preset", text="", icon="ADD")
|
||||
row.operator("goophys.delete_bone_map_preset", text="", icon="REMOVE")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("goophys.apply_bone_map_preset", text="Apply Bone Map Preset")
|
||||
|
||||
|
||||
class GP_PT_goophys_trouble_panel(Panel):
|
||||
"""Creates a Panel in the scene context of the properties editor"""
|
||||
|
||||
bl_idname = "GP_PT_goophys_trouble_panel"
|
||||
bl_label = "Troubleshooting"
|
||||
bl_parent_id = "GP_PT_goophys_panel"
|
||||
bl_space_type = "VIEW_3D"
|
||||
bl_region_type = "UI"
|
||||
bl_category = "Goo Animation Tools"
|
||||
bl_options = {"DEFAULT_CLOSED"}
|
||||
bl_order = 3
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator("goophys.sync_frame_range", text="Sync Frame Range")
|
||||
|
||||
row = layout.row(align=True)
|
||||
row.operator(
|
||||
"goophys.refresh_physics_status", text="Refresh Bone Physics Status"
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
_classes = [
|
||||
GP_PT_goophys_panel,
|
||||
GP_PT_goophys_collider_panel,
|
||||
GP_PT_goophys_advanced_panel,
|
||||
GP_PT_goophys_trouble_panel,
|
||||
]
|
||||
|
||||
|
||||
_register, _unregister = register_classes_factory(_classes)
|
||||
|
||||
|
||||
def register():
|
||||
if hasattr(bpy.types, "GOO_PT_main_panel") == False:
|
||||
bpy.utils.register_class(GOO_PT_main_panel)
|
||||
_register()
|
||||
|
||||
|
||||
def unregister():
|
||||
_unregister()
|
||||
Reference in New Issue
Block a user