2021-04-05 01:35:12 +02:00
|
|
|
import bpy
|
2022-04-08 19:35:20 +02:00
|
|
|
import bpy_types
|
2021-04-05 01:35:12 +02:00
|
|
|
import re
|
2021-04-08 19:25:05 +02:00
|
|
|
import numpy as np
|
2022-03-29 18:46:33 +02:00
|
|
|
from mathutils import Matrix, Vector, Color
|
2021-04-05 01:35:12 +02:00
|
|
|
|
|
|
|
def get_addon_prefs():
|
|
|
|
'''
|
|
|
|
function to read current addon preferences properties
|
|
|
|
access with : get_addon_prefs().super_special_option
|
|
|
|
'''
|
|
|
|
import os
|
|
|
|
addon_name = os.path.splitext(__name__)[0]
|
|
|
|
preferences = bpy.context.preferences
|
|
|
|
addon_prefs = preferences.addons[addon_name].preferences
|
|
|
|
return (addon_prefs)
|
|
|
|
|
2022-04-13 18:38:03 +02:00
|
|
|
def open_addon_prefs():
|
|
|
|
'''Open addon prefs windows with focus on current addon'''
|
|
|
|
from .__init__ import bl_info
|
|
|
|
wm = bpy.context.window_manager
|
|
|
|
wm.addon_filter = 'All'
|
|
|
|
if not 'COMMUNITY' in wm.addon_support: # reactivate community
|
|
|
|
wm.addon_support = set([i for i in wm.addon_support] + ['COMMUNITY'])
|
|
|
|
wm.addon_search = bl_info['name']
|
|
|
|
bpy.context.preferences.active_section = 'ADDONS'
|
|
|
|
bpy.ops.preferences.addon_expand(module=__package__)
|
|
|
|
bpy.ops.screen.userpref_show('INVOKE_DEFAULT')
|
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
def helper(name: str = '') -> str:
|
|
|
|
'''Return name and arguments from calling obj as str
|
|
|
|
:name: - replace definition name by your own str
|
|
|
|
'''
|
|
|
|
if not get_addon_prefs().debug:
|
|
|
|
return
|
|
|
|
import inspect
|
|
|
|
func = inspect.currentframe().f_back
|
|
|
|
name = name or f'def {func.f_code.co_name}'
|
|
|
|
args = inspect.getargvalues(func).locals
|
|
|
|
arguments = ', '.join([f'{k}={v}' for k, v in args.items()])
|
|
|
|
return(f'{name}({arguments})')
|
|
|
|
|
2021-04-08 19:25:05 +02:00
|
|
|
def convertAttr(Attr):
|
|
|
|
'''Convert given value to a Json serializable format'''
|
2022-03-29 18:46:33 +02:00
|
|
|
if type(Attr) in [type(Vector()),type(Color())]:
|
2021-04-08 19:25:05 +02:00
|
|
|
if len(Attr) == 3:
|
|
|
|
return([Attr[0],Attr[1],Attr[2]])
|
|
|
|
elif len(Attr) == 2:
|
|
|
|
return([Attr[0],Attr[1]])
|
|
|
|
elif len(Attr) == 1:
|
|
|
|
return([Attr[0]])
|
|
|
|
elif len(Attr) == 4:
|
|
|
|
return([Attr[0],Attr[1],Attr[2],Attr[3]])
|
2022-03-29 18:46:33 +02:00
|
|
|
elif type(Attr) == type(Matrix()):
|
2021-04-08 19:25:05 +02:00
|
|
|
return (np.matrix(Attr).tolist())
|
|
|
|
else:
|
|
|
|
return(Attr)
|
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
|
|
|
|
def get_gnd():
|
2022-04-27 12:01:39 +02:00
|
|
|
gnd = bpy.context.scene.anim_cycle_settings.gnd
|
|
|
|
if not gnd:
|
|
|
|
for o in bpy.context.scene.objects:
|
|
|
|
if o.type == 'MESH' and o.name.lower() in ('ground', 'gnd'):
|
|
|
|
return o
|
|
|
|
if not gnd:
|
|
|
|
# nothing found
|
|
|
|
print('ERROR', 'No "gnd" object found')
|
|
|
|
return gnd
|
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
|
|
|
|
|
|
|
|
def get_follow_curve_from_armature(arm):
|
2022-04-11 19:46:22 +02:00
|
|
|
"""Return curve and constraint or a tuple of string ('error', 'message')"""
|
2021-04-05 01:35:12 +02:00
|
|
|
|
2022-04-11 19:46:22 +02:00
|
|
|
name = get_root_name()
|
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
parents = []
|
2022-04-11 19:46:22 +02:00
|
|
|
const = None
|
2021-04-06 18:30:25 +02:00
|
|
|
# root = b.id_data.pose.bones.get(name)
|
|
|
|
root = arm.pose.bones.get(name)
|
2022-04-11 19:46:22 +02:00
|
|
|
if not root:
|
|
|
|
return ('ERROR', f'No bone named {name}')
|
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
for c in root.constraints:
|
|
|
|
if c.type == 'FOLLOW_PATH':
|
|
|
|
const = c
|
2022-04-11 19:46:22 +02:00
|
|
|
""" # old method compatible with child of (using animation on parented object)
|
2021-04-05 01:35:12 +02:00
|
|
|
if c.type == 'CHILD_OF':
|
|
|
|
print(f'found child-of on {name}')
|
|
|
|
if c.target:
|
|
|
|
parents.append(c.target)
|
|
|
|
|
|
|
|
if not const:
|
|
|
|
for p in parents:
|
|
|
|
for c in p.constraints:
|
|
|
|
if c.type == 'FOLLOW_PATH':
|
|
|
|
print('INFO', f'follow_path found on "{p.name}" parent object')
|
|
|
|
const = c
|
|
|
|
break
|
2022-04-11 19:46:22 +02:00
|
|
|
"""
|
2021-04-05 01:35:12 +02:00
|
|
|
if not const:
|
|
|
|
return ('ERROR', 'No constraints founds')
|
|
|
|
|
|
|
|
curve = const.target
|
|
|
|
if not curve:
|
|
|
|
return ('ERROR', f'no target set for {curve.name}')
|
|
|
|
|
|
|
|
return curve, const
|
|
|
|
|
2021-04-06 18:30:25 +02:00
|
|
|
# --- ACTIONS
|
|
|
|
|
2022-04-21 17:44:35 +02:00
|
|
|
def get_nla_strip(ob):
|
|
|
|
# get all strips in all tracks (can only be one active)
|
|
|
|
strips = [s for t in ob.animation_data.nla_tracks for s in t.strips]
|
|
|
|
if len(strips) == 1:
|
|
|
|
return strips[0].action
|
|
|
|
|
|
|
|
if len(strips) > 1:
|
|
|
|
# return active strip
|
|
|
|
for s in strips:
|
|
|
|
if s.active:
|
|
|
|
return s.action
|
|
|
|
|
|
|
|
def get_obj_action(ob):
|
2021-04-05 01:35:12 +02:00
|
|
|
print(helper())
|
2022-04-21 17:44:35 +02:00
|
|
|
act = ob.animation_data
|
2021-04-05 01:35:12 +02:00
|
|
|
if not act:
|
2022-04-21 17:44:35 +02:00
|
|
|
print('ERROR', f'no animation data on {ob.name}')
|
2021-04-05 01:35:12 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
act = act.action
|
|
|
|
if not act:
|
2022-04-21 17:44:35 +02:00
|
|
|
# check NLA
|
|
|
|
strip = get_nla_strip(ob)
|
|
|
|
|
|
|
|
if strip:
|
|
|
|
return strip
|
|
|
|
|
|
|
|
# there are multiple strips but no active
|
|
|
|
if len([s for t in ob.animation_data.nla_tracks for s in t.strips]):
|
|
|
|
print('ERROR', f'no active strip on NLA for {ob.name}')
|
|
|
|
return
|
|
|
|
|
|
|
|
print('ERROR', f'no action on {ob.name}')
|
2021-04-05 01:35:12 +02:00
|
|
|
return
|
2022-04-21 17:44:35 +02:00
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
return act
|
|
|
|
|
|
|
|
def set_generated_action(obj):
|
2022-04-12 11:21:00 +02:00
|
|
|
'''Backup object action and return a new action suffixed '_pinned'
|
2021-04-05 01:35:12 +02:00
|
|
|
associated with the object
|
|
|
|
'''
|
|
|
|
print(helper())
|
|
|
|
act = get_obj_action(obj)
|
|
|
|
if not act: return
|
|
|
|
|
2022-04-12 11:21:00 +02:00
|
|
|
regen = re.compile(r'_pinned\.?\d{0,3}$')
|
2021-04-05 01:35:12 +02:00
|
|
|
|
|
|
|
if regen.search(act.name):
|
|
|
|
# is an autogenerated one
|
|
|
|
org_action_name = regen.sub('', act.name)
|
|
|
|
org_action = bpy.data.actions.get(org_action_name)
|
|
|
|
if not org_action:
|
|
|
|
print('ERROR', f'{org_action_name} not found')
|
|
|
|
return
|
|
|
|
|
|
|
|
obj.animation_data.action = org_action
|
|
|
|
bpy.data.actions.remove(act)
|
|
|
|
|
|
|
|
act = org_action
|
|
|
|
|
|
|
|
# backup action before doing anything crazy
|
|
|
|
act.use_fake_user = True
|
|
|
|
new_act = act.copy()
|
2022-04-12 11:21:00 +02:00
|
|
|
new_act.name = act.name + '_pinned'
|
2021-04-05 01:35:12 +02:00
|
|
|
obj.animation_data.action = new_act
|
|
|
|
return new_act
|
|
|
|
|
|
|
|
|
2022-04-12 11:21:00 +02:00
|
|
|
def set_baked_action(obj):
|
2021-04-06 18:30:25 +02:00
|
|
|
'''Backup object action and return a new action
|
|
|
|
associated with the object
|
|
|
|
'''
|
|
|
|
print(helper())
|
|
|
|
|
2022-04-21 17:44:35 +02:00
|
|
|
re_baked = re.compile(r'_baked\.?\d{0,3}$')
|
2021-04-06 18:30:25 +02:00
|
|
|
act = obj.animation_data.action
|
|
|
|
|
2022-04-21 17:44:35 +02:00
|
|
|
if re_baked.search(act.name):
|
2021-04-06 18:30:25 +02:00
|
|
|
# is an autogenerated one
|
2022-04-21 17:44:35 +02:00
|
|
|
org_action_name = re_baked.sub('', act.name)
|
2021-04-06 18:30:25 +02:00
|
|
|
org_action = bpy.data.actions.get(org_action_name)
|
|
|
|
if not org_action:
|
|
|
|
print('ERROR', f'{org_action_name} not found')
|
|
|
|
return
|
|
|
|
|
|
|
|
obj.animation_data.action = org_action
|
|
|
|
bpy.data.actions.remove(act)
|
|
|
|
|
|
|
|
act = org_action
|
|
|
|
|
|
|
|
# backup action before doing anything crazy
|
|
|
|
act.use_fake_user = True
|
|
|
|
new_act = act.copy()
|
2022-04-12 11:21:00 +02:00
|
|
|
new_act.name = act.name + '_baked'
|
2021-04-06 18:30:25 +02:00
|
|
|
obj.animation_data.action = new_act
|
|
|
|
return new_act
|
|
|
|
|
2022-04-25 18:12:24 +02:00
|
|
|
def get_origin_action(act):
|
|
|
|
'''Return original action if found, else return same'''
|
|
|
|
if '_baked' in act.name:
|
|
|
|
base_act_name = act.name.split('_baked')[0]
|
|
|
|
base_act = bpy.data.actions.get(base_act_name)
|
|
|
|
if base_act:
|
|
|
|
act = base_act
|
|
|
|
print(f'Using for action {base_act_name} as reference')
|
|
|
|
else:
|
|
|
|
print(f'No base action found (searching for {base_act_name})')
|
|
|
|
return act
|
2021-04-06 18:30:25 +02:00
|
|
|
|
2021-04-05 01:35:12 +02:00
|
|
|
def get_curve_length(ob):
|
2022-03-29 18:46:33 +02:00
|
|
|
'''Get a curve object, return a float representing world space length'''
|
2021-04-05 01:35:12 +02:00
|
|
|
dg = bpy.context.evaluated_depsgraph_get()
|
|
|
|
obeval = ob.evaluated_get(dg)#.copy()
|
|
|
|
baked_me = obeval.to_mesh(preserve_all_data_layers=False, depsgraph=dg)
|
|
|
|
mat = obeval.matrix_world
|
|
|
|
total_length = 0
|
|
|
|
for i, _v in enumerate(baked_me.vertices):
|
|
|
|
if i == 0:
|
|
|
|
continue
|
|
|
|
total_length += ((mat @ baked_me.vertices[i-1].co) - (mat @ baked_me.vertices[i].co)).length
|
|
|
|
|
|
|
|
print("total_length", total_length)#Dbg
|
2022-03-29 18:46:33 +02:00
|
|
|
return total_length
|
|
|
|
|
|
|
|
|
|
|
|
def scale_matrix_from_vector(scale):
|
|
|
|
# recreate a neutral mat scale
|
|
|
|
matscale_x = Matrix.Scale(scale[0], 4,(1,0,0))
|
|
|
|
matscale_y = Matrix.Scale(scale[1], 4,(0,1,0))
|
|
|
|
matscale_z = Matrix.Scale(scale[2], 4,(0,0,1))
|
|
|
|
matscale = matscale_x @ matscale_y @ matscale_z
|
|
|
|
return matscale
|
|
|
|
|
|
|
|
def compose_matrix(loc, rot, scale):
|
|
|
|
loc_mat = Matrix.Translation(loc)
|
|
|
|
rot_mat = rot.to_matrix().to_4x4()
|
|
|
|
scale_mat = scale_matrix_from_vector(scale)
|
|
|
|
return loc_mat @ rot_mat @ scale_mat
|
2022-03-31 17:07:04 +02:00
|
|
|
|
|
|
|
def get_direction_vector_from_enum(string) -> Vector:
|
|
|
|
orient_vectors = {
|
|
|
|
'FORWARD_X' : Vector((1,0,0)),
|
|
|
|
'FORWARD_Y' : Vector((0,1,0)),
|
|
|
|
'FORWARD_Z' : Vector((0,0,1)),
|
|
|
|
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
|
|
|
|
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
|
|
|
|
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
|
|
|
|
}
|
|
|
|
return orient_vectors[string]
|
|
|
|
|
|
|
|
def orentation_track_from_vector(input_vector) -> str:
|
|
|
|
'''return closest world track orientation name from passed vector direction'''
|
|
|
|
orient_vectors = {
|
|
|
|
'FORWARD_X' : Vector((1,0,0)),
|
|
|
|
'FORWARD_Y' : Vector((0,1,0)),
|
|
|
|
'FORWARD_Z' : Vector((0,0,1)),
|
|
|
|
'TRACK_NEGATIVE_X' : Vector((-1,0,0)),
|
|
|
|
'TRACK_NEGATIVE_Y' : Vector((0,-1,0)),
|
|
|
|
'TRACK_NEGATIVE_Z' : Vector((0,0,-1))
|
|
|
|
}
|
|
|
|
|
|
|
|
orient = None
|
|
|
|
min_angle = 10000
|
|
|
|
for track, v in orient_vectors.items():
|
|
|
|
angle = input_vector.angle(v)
|
|
|
|
if angle < min_angle:
|
|
|
|
min_angle = angle
|
|
|
|
orient = track
|
|
|
|
|
|
|
|
return orient
|
|
|
|
|
|
|
|
def get_root_name(context=None):
|
|
|
|
'''return name of rig root name'''
|
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
context = context or bpy.context
|
2022-04-13 18:38:03 +02:00
|
|
|
|
2022-03-31 17:07:04 +02:00
|
|
|
prefs = get_addon_prefs()
|
2022-04-13 18:38:03 +02:00
|
|
|
|
|
|
|
# Auto set bone with pref default value if nothing entered
|
|
|
|
settings = context.scene.anim_cycle_settings
|
|
|
|
if not settings.tgt_bone:
|
|
|
|
settings.tgt_bone = prefs.tgt_bone
|
|
|
|
|
|
|
|
## auto-detect mode ?
|
|
|
|
|
|
|
|
return settings.tgt_bone
|
2022-03-31 17:07:04 +02:00
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
## --- curve funcs
|
|
|
|
|
2022-03-31 17:07:04 +02:00
|
|
|
def generate_curve(location=(0,0,0), direction=(1,0,0), name='curve_path', enter_edit=True, context=None):
|
|
|
|
'''Create curve at provided location and direction vector'''
|
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
context = context or bpy.context
|
2022-03-31 17:07:04 +02:00
|
|
|
|
|
|
|
## using ops (dirty)
|
|
|
|
# bpy.ops.curve.primitive_bezier_curve_add(radius=1, enter_editmode=enter_edit, align='WORLD', location=location, scale=(1, 1, 1))
|
|
|
|
# curve = context.object
|
|
|
|
# curve.name = 'curve_path'
|
|
|
|
# # fast straighten
|
|
|
|
# bpy.ops.curve.handle_type_set(type='VECTOR')
|
|
|
|
# bpy.ops.curve.handle_type_set(type='ALIGNED')
|
|
|
|
# bpy.ops.transform.translate(value=(1, 0, 0), orient_type='LOCAL',
|
|
|
|
# orient_matrix=((1, 0, 0), (0, 1, 0), (0, 0, 1)), orient_matrix_type='LOCAL',
|
|
|
|
# constraint_axis=(True, False, False), mirror=True, use_proportional_edit=False)
|
|
|
|
|
|
|
|
## using data
|
|
|
|
curve_data = bpy.data.curves.new(name, 'CURVE') # ('CURVE', 'SURFACE', 'FONT')
|
|
|
|
curve_data.dimensions = '3D'
|
|
|
|
curve_data.use_path = True
|
|
|
|
|
|
|
|
curve = bpy.data.objects.new(name, curve_data)
|
|
|
|
spl = curve_data.splines.new('BEZIER') # ('POLY', 'BEZIER', 'NURBS')
|
|
|
|
spl.bezier_points.add(1) # one point already exists
|
|
|
|
for i in range(2):
|
|
|
|
spl.bezier_points[i].handle_left_type = 'VECTOR' # ('FREE', 'VECTOR', 'ALIGNED', 'AUTO')
|
|
|
|
spl.bezier_points[i].handle_right_type = 'VECTOR'
|
|
|
|
spl.bezier_points[1].co = direction
|
|
|
|
|
|
|
|
# Back to aligned mode
|
|
|
|
for i in range(2):
|
|
|
|
spl.bezier_points[i].handle_right_type = spl.bezier_points[i].handle_left_type = 'ALIGNED'
|
|
|
|
|
|
|
|
# Select second point
|
|
|
|
spl.bezier_points[1].select_control_point = True
|
|
|
|
spl.bezier_points[1].select_left_handle = True
|
|
|
|
spl.bezier_points[1].select_right_handle = True
|
|
|
|
|
|
|
|
# link
|
|
|
|
context.scene.collection.objects.link(curve)
|
|
|
|
|
|
|
|
|
|
|
|
# curve object settings
|
|
|
|
curve.location = location
|
|
|
|
curve.show_in_front = True
|
|
|
|
|
|
|
|
# enter edit
|
|
|
|
if enter_edit and context.mode == 'OBJECT':
|
|
|
|
curve.select_set(True)
|
|
|
|
context.view_layer.objects.active = curve
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT', toggle=False) # EDIT_CURVE
|
|
|
|
|
|
|
|
## set viewport overlay visibility for better view
|
|
|
|
if context.space_data.type == 'VIEW_3D':
|
|
|
|
context.space_data.overlay.show_curve_normals = True
|
|
|
|
context.space_data.overlay.normals_length = 0.2
|
|
|
|
|
2022-04-01 12:12:05 +02:00
|
|
|
return curve
|
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
|
|
|
|
def create_follow_path_constraint(ob, curve, follow_curve=True):
|
|
|
|
prefs = get_addon_prefs()
|
|
|
|
root_name = prefs.tgt_bone
|
|
|
|
root = ob.pose.bones.get(root_name)
|
|
|
|
|
|
|
|
if not root:
|
|
|
|
return ('ERROR', f'posebone {root_name} not found in armature {ob.name} check addon preferences to change name')
|
|
|
|
|
|
|
|
# Clear bone follow path constraint
|
|
|
|
exiting_fp_constraints = [c for c in root.constraints if c.type == 'FOLLOW_PATH']
|
|
|
|
for c in exiting_fp_constraints:
|
|
|
|
root.constraints.remove(c)
|
|
|
|
|
|
|
|
# loc = ob.matrix_world @ root.matrix.to_translation()
|
|
|
|
if root.name == ('world', 'root') and root.location != (0,0,0):
|
|
|
|
old_loc = root.location
|
|
|
|
root.location = (0,0,0)
|
|
|
|
print(f'root moved from {old_loc} to (0,0,0) to counter follow curve offset')
|
|
|
|
|
|
|
|
const = root.constraints.new('FOLLOW_PATH')
|
|
|
|
const.target = curve
|
|
|
|
# axis only in this case, should be in addon to prefs
|
|
|
|
|
|
|
|
## determine which axis to use... maybe found orientation in world space from matrix_basis ?
|
|
|
|
root_world_base_direction = root.bone.matrix_local @ get_direction_vector_from_enum(bpy.context.scene.anim_cycle_settings.forward_axis)
|
|
|
|
const.forward_axis = orentation_track_from_vector(root_world_base_direction) # 'TRACK_NEGATIVE_Y' # bpy.context.scene.anim_cycle_settings.forward_axis # 'FORWARD_X'
|
|
|
|
print('const.forward_axis: ', const.forward_axis)
|
|
|
|
const.use_curve_follow = True
|
|
|
|
return curve, const
|
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
def shrinkwrap_on_object(source, target, apply=True):
|
|
|
|
# shrinkwrap or cast on ground
|
|
|
|
mod = source.modifiers.new('Shrinkwrap', 'SHRINKWRAP')
|
|
|
|
# mod.wrap_method = 'TARGET_PROJECT'
|
|
|
|
mod.wrap_method = 'PROJECT'
|
|
|
|
mod.wrap_mode = 'ON_SURFACE'
|
|
|
|
mod.use_project_z = True
|
|
|
|
mod.use_negative_direction = True
|
|
|
|
mod.use_positive_direction = True
|
|
|
|
mod.target = target
|
|
|
|
|
|
|
|
if apply:
|
|
|
|
# Apply and decimate
|
|
|
|
switch = False
|
|
|
|
if bpy.context.mode == 'EDIT_CURVE':
|
|
|
|
switch = True
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
bpy.ops.object.modifier_apply({'object': source}, modifier="Shrinkwrap", report=False)
|
|
|
|
if switch:
|
|
|
|
bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
|
|
|
|
|
|
|
|
def snap_curve(create_copy=False):
|
2022-04-19 15:33:06 +02:00
|
|
|
obj = bpy.context.object
|
2022-04-27 14:54:05 +02:00
|
|
|
gnd = get_gnd()
|
|
|
|
if not gnd:
|
|
|
|
return
|
2022-04-19 15:33:06 +02:00
|
|
|
|
|
|
|
curve = const = None
|
|
|
|
if obj.type == 'ARMATURE':
|
|
|
|
curve, const = get_follow_curve_from_armature(obj)
|
|
|
|
|
|
|
|
to_follow = bpy.context.scene.anim_cycle_settings.path_to_follow
|
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
curve_act = None
|
|
|
|
anim_frame = None
|
2022-04-27 12:01:39 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
if create_copy:
|
|
|
|
# get curve from field
|
|
|
|
if not curve and not to_follow:
|
|
|
|
return ('ERROR', f'No curve pointed by "Path" filed')
|
|
|
|
|
|
|
|
if not curve:
|
|
|
|
curve, const = create_follow_path_constraint(obj, to_follow)
|
|
|
|
if isinstance(curve, str):
|
|
|
|
return (curve, const) # those are error message
|
2022-04-19 15:33:06 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
# if obj.type == 'CURVE':
|
|
|
|
# return ('ERROR', f'Select the armature related to curve {obj.name}')
|
|
|
|
# else:
|
|
|
|
# return ('ERROR', 'Not an armature object')
|
|
|
|
|
|
|
|
|
2022-04-19 15:33:06 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
# if it's on a snap curve, fetch original
|
|
|
|
if '_snap' in curve.name:
|
|
|
|
org_name = re.sub(r'_snap\.?\d{0,3}$', '', curve.name)
|
|
|
|
org_curve = bpy.context.scene.objects.get(org_name)
|
|
|
|
|
|
|
|
if org_curve:
|
|
|
|
const.target = org_curve
|
|
|
|
|
|
|
|
# keep action
|
|
|
|
if curve.data.animation_data and curve.data.animation_data.action:
|
|
|
|
curve_act = curve.data.animation_data.action
|
|
|
|
|
|
|
|
anim_frame = curve.data.path_duration
|
|
|
|
# delete old snap
|
|
|
|
bpy.data.objects.remove(curve)
|
|
|
|
# assign old curve as main one
|
|
|
|
curve = org_curve
|
|
|
|
|
|
|
|
nc = curve.copy()
|
|
|
|
name = re.sub(r'\.\d{3}$', '', curve.name) + '_snap'
|
|
|
|
const.target = nc
|
|
|
|
nc.name = name
|
|
|
|
nc.data = curve.data.copy()
|
|
|
|
nc.data.name = name + '_data'
|
|
|
|
|
|
|
|
if curve_act:
|
|
|
|
nc.data.animation_data_create()
|
|
|
|
nc.data.animation_data.action = curve_act
|
|
|
|
if anim_frame:
|
|
|
|
nc.data.path_duration = anim_frame
|
|
|
|
|
|
|
|
curve.users_collection[0].objects.link(nc)
|
|
|
|
|
|
|
|
else:
|
|
|
|
if not curve:
|
|
|
|
return ('ERROR', 'Path not found')
|
|
|
|
nc = curve
|
2022-04-19 15:33:06 +02:00
|
|
|
|
|
|
|
## If object mode is Curve subdivide it (TODO if nurbs needs conversion)
|
|
|
|
#-# subdivide the curve (if curve is not nurbs)
|
|
|
|
|
|
|
|
# bpy.ops.object.mode_set(mode='EDIT')
|
|
|
|
# bpy.ops.curve.select_all(action='SELECT')
|
|
|
|
# bpy.ops.curve.subdivide(number_cuts=4)
|
|
|
|
# bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
shrinkwrap_on_object(nc, gnd)
|
2022-04-19 15:33:06 +02:00
|
|
|
bpy.context.scene.anim_cycle_settings.path_to_follow = nc
|
|
|
|
# return 0, nc
|
|
|
|
|
|
|
|
## --- action funcs
|
|
|
|
|
2022-04-01 12:12:05 +02:00
|
|
|
def update_action(act):
|
|
|
|
'''update fcurves (often broken after generation through API)'''
|
|
|
|
|
|
|
|
# update fcurves
|
|
|
|
for fcu in act.fcurves:
|
|
|
|
fcu.update()
|
2022-04-21 17:44:35 +02:00
|
|
|
|
2022-04-01 12:12:05 +02:00
|
|
|
# redraw graph area
|
|
|
|
for window in bpy.context.window_manager.windows:
|
|
|
|
screen = window.screen
|
|
|
|
for area in screen.areas:
|
|
|
|
if area.type == 'GRAPH_EDITOR':
|
|
|
|
area.tag_redraw()
|
|
|
|
|
2022-04-07 14:36:47 +02:00
|
|
|
|
2022-04-08 19:35:20 +02:00
|
|
|
def get_offset_fcu(act=None):
|
|
|
|
'''Get an action, object, pose_bone or a bone constraint (if nothing is passed use active object)
|
|
|
|
return offset fcurve or a string describing error
|
|
|
|
'''
|
|
|
|
ob = None
|
|
|
|
bone_name = None
|
|
|
|
const_name = None
|
|
|
|
if act is None:
|
|
|
|
if not bpy.context.object:
|
|
|
|
return 'No active object'
|
|
|
|
act = bpy.context.object
|
|
|
|
|
|
|
|
if isinstance(act, bpy.types.FollowPathConstraint):
|
|
|
|
ob = act.id_data
|
|
|
|
const = act
|
|
|
|
bones = [b for b in ob.pose.bones for c in b.constraints if b.constraints if c == act]
|
|
|
|
if not bones:
|
|
|
|
return f'no bone found with constraint {ob.name}'
|
|
|
|
bone = bones[0]
|
|
|
|
bone_name = bone.name
|
|
|
|
const_name = const.name
|
|
|
|
|
|
|
|
if isinstance(act, bpy_types.PoseBone):
|
|
|
|
bone = act
|
|
|
|
bone_name = act.name
|
|
|
|
ob = act = act.id_data # fall_back to armature object
|
|
|
|
if not const_name:
|
|
|
|
consts = [c for c in bone.constraints if isinstance(c, bpy.types.FollowPathConstraint)]
|
|
|
|
if not consts:
|
|
|
|
return f'no follow path constraint on bone {bone_name}'
|
|
|
|
const_name = consts[0].name
|
|
|
|
|
|
|
|
if isinstance(act, bpy.types.Object):
|
|
|
|
ob = act
|
|
|
|
if not ob.animation_data:
|
|
|
|
return f'{ob.name} has no animation_data'
|
|
|
|
act = ob.animation_data
|
|
|
|
|
|
|
|
if isinstance(act, bpy.types.AnimData):
|
|
|
|
ob = act.id_data
|
|
|
|
if not act.action:
|
|
|
|
return f'{ob.name} has animation_data but no action'
|
|
|
|
act = act.action
|
|
|
|
|
|
|
|
|
|
|
|
if bone_name and const_name:
|
|
|
|
offset_data_path = f'pose.bones["{bone_name}"].constraints["{const_name}"].offset'
|
|
|
|
fcu = act.fcurves.find(offset_data_path)
|
|
|
|
if not fcu:
|
|
|
|
return f'No fcurve found with data_path {offset_data_path}'
|
|
|
|
return fcu
|
|
|
|
|
|
|
|
# bone_name = get_root_name()
|
|
|
|
|
|
|
|
# find from determined action
|
|
|
|
fcus = [fcu for fcu in act.fcurves if all(x in fcu.data_path for x in ('pose.bones', 'constraints', 'offset'))]
|
|
|
|
if not fcus:
|
|
|
|
return f'no offset fcurves found for: {act.name}'
|
|
|
|
|
|
|
|
if len(fcus) > 1:
|
|
|
|
print(f'/!\ multiple fcurves seem to have a follow path constraint')
|
|
|
|
|
|
|
|
return fcus[0]
|
|
|
|
|
|
|
|
|
2022-04-11 19:46:22 +02:00
|
|
|
def fuzzy_match(s1, s2, tol=0.8, case_sensitive=False):
|
|
|
|
'''Tell if two strings are similar using a similarity ratio (0 to 1) value passed as third arg'''
|
|
|
|
from difflib import SequenceMatcher
|
|
|
|
# can also use difflib.get_close_matches(word, possibilities, n=3, cutoff=0.6)
|
|
|
|
if case_sensitive:
|
|
|
|
similarity = SequenceMatcher(None, s1, s2)
|
|
|
|
else:
|
|
|
|
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
|
|
|
|
return similarity.ratio() > tol
|
|
|
|
|
|
|
|
def fuzzy_match_ratio(s1, s2, case_sensitive=False):
|
|
|
|
'''Tell how much two passed strings are similar 1.0 being exactly similar'''
|
|
|
|
from difflib import SequenceMatcher
|
|
|
|
if case_sensitive:
|
|
|
|
similarity = SequenceMatcher(None, s1, s2)
|
|
|
|
else:
|
|
|
|
similarity = SequenceMatcher(None, s1.lower(), s2.lower())
|
|
|
|
return similarity.ratio()
|
2022-04-08 19:35:20 +02:00
|
|
|
|
2022-04-11 19:46:22 +02:00
|
|
|
def flip_suffix_side_name(name):
|
|
|
|
return re.sub(r'([-._])(R|L)', lambda x: x.group(1) + ('L' if x.group(2) == 'R' else 'R'), name)
|
2022-04-08 19:35:20 +02:00
|
|
|
|
2022-04-11 19:46:22 +02:00
|
|
|
def get_flipped_name(name):
|
|
|
|
import re
|
|
|
|
|
|
|
|
def flip(match, start=False):
|
|
|
|
if not match.group(1) or not match.group(2):
|
|
|
|
return
|
|
|
|
|
|
|
|
sides = {
|
|
|
|
'R' : 'L',
|
|
|
|
'r' : 'l',
|
|
|
|
'L' : 'R',
|
|
|
|
'l' : 'r',
|
|
|
|
}
|
|
|
|
|
|
|
|
if start:
|
|
|
|
side, sep = match.groups()
|
|
|
|
return sides[side] + sep
|
|
|
|
else:
|
|
|
|
sep, side, num = match.groups()
|
|
|
|
return sep + sides[side] + (num or '')
|
|
|
|
|
|
|
|
start_reg = re.compile(r'^(l|r)([-_.])', flags=re.I)
|
|
|
|
|
|
|
|
if start_reg.match(name):
|
|
|
|
return start_reg.sub(lambda x: flip(x, True), name)
|
|
|
|
|
|
|
|
else:
|
|
|
|
return re.sub(r'([-_.])(l|r)(\.\d+)?$', flip, name, flags=re.I)
|
2022-04-08 19:35:20 +02:00
|
|
|
|
|
|
|
### --- context manager - store / restore
|
2022-04-07 14:36:47 +02:00
|
|
|
|
|
|
|
class attr_set():
|
2022-04-08 19:35:20 +02:00
|
|
|
'''Receive a list of tuple [(data_path, "attribute" [, wanted value)] ]
|
|
|
|
entering with-statement : Store existing values, assign wanted value (if any)
|
|
|
|
exiting with-statement: Restore values to their old values
|
2022-04-07 14:36:47 +02:00
|
|
|
'''
|
|
|
|
|
|
|
|
def __init__(self, attrib_list):
|
|
|
|
self.store = []
|
2022-04-08 19:35:20 +02:00
|
|
|
# item = (prop, attr, [new_val])
|
|
|
|
for item in attrib_list:
|
|
|
|
prop, attr = item[:2]
|
2022-04-07 14:36:47 +02:00
|
|
|
self.store.append( (prop, attr, getattr(prop, attr)) )
|
2022-04-08 19:35:20 +02:00
|
|
|
if len(item) >= 3:
|
|
|
|
setattr(prop, attr, item[2])
|
|
|
|
|
2022-04-07 14:36:47 +02:00
|
|
|
def __enter__(self):
|
|
|
|
return self
|
2022-04-08 19:35:20 +02:00
|
|
|
|
2022-04-07 14:36:47 +02:00
|
|
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
|
|
for prop, attr, old_val in self.store:
|
|
|
|
setattr(prop, attr, old_val)
|
2022-04-11 19:46:22 +02:00
|
|
|
|
2022-04-13 18:38:03 +02:00
|
|
|
# fcurve modifiers
|
|
|
|
|
2022-04-26 12:14:09 +02:00
|
|
|
def remove_all_cycles_modifier(ob=None):
|
|
|
|
ob = ob or bpy.context.object
|
|
|
|
ct = 0
|
|
|
|
for fc in ob.animation_data.action.fcurves:
|
2022-04-13 18:38:03 +02:00
|
|
|
# if fc.data_path.split('"')[1] in selected_names:
|
|
|
|
for m in reversed(fc.modifiers):
|
|
|
|
if m.type == 'CYCLES':
|
2022-04-26 12:14:09 +02:00
|
|
|
ct += 1
|
|
|
|
# print(f'Remove cycle mod on fcurve: {fc.data_path}')
|
2022-04-13 18:38:03 +02:00
|
|
|
fc.modifiers.remove(m)
|
2022-04-26 12:14:09 +02:00
|
|
|
fc.update()
|
|
|
|
print(f'Remove cyclic modifiers on {ct} fcurve(s)')
|
|
|
|
return ct
|
2022-04-13 18:38:03 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
def get_only_pose_keyable_fcurves(ob, action=None):
|
|
|
|
'''Can action providing another action (must be for the same object)'''
|
2022-04-13 18:38:03 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
act = action or ob.animation_data.action
|
|
|
|
## skip bones that are on protected layers ?
|
2022-04-13 18:38:03 +02:00
|
|
|
# protected = [i for i, l in enumerate(ob.data.layers_protected) if l]
|
|
|
|
# for b in ob.data.bones:
|
|
|
|
# if b.use_deform: # don't affect deform bones
|
|
|
|
# continue
|
|
|
|
## b_layers = [i for i, l in enumerate(b.layers) if l]
|
|
|
|
|
|
|
|
name_list = [b.name for b in ob.data.bones] # if not b.use_deform (too limiting)
|
2022-04-27 14:54:05 +02:00
|
|
|
fcus = []
|
2022-04-26 11:04:44 +02:00
|
|
|
re_prefix = re.compile(r'^(mch|def|org|vis|fld|ctp)[\._-]', flags=re.I)
|
2022-04-27 14:54:05 +02:00
|
|
|
for fc in act.fcurves:
|
2022-04-25 18:12:24 +02:00
|
|
|
# skip offset
|
|
|
|
if fc.data_path.endswith('.offset') and 'constraint' in fc.data_path:
|
|
|
|
continue
|
2022-04-26 11:04:44 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
# skip fcus that are not bones
|
|
|
|
if not '"' in fc.data_path:
|
|
|
|
continue
|
|
|
|
|
2022-04-13 18:38:03 +02:00
|
|
|
b_name = fc.data_path.split('"')[1]
|
2022-04-27 14:54:05 +02:00
|
|
|
if b_name not in name_list:
|
|
|
|
continue
|
|
|
|
|
2022-04-26 11:04:44 +02:00
|
|
|
if re_prefix.match(b_name):
|
2022-04-13 18:38:03 +02:00
|
|
|
continue
|
2022-04-27 14:54:05 +02:00
|
|
|
fcus.append(fc)
|
|
|
|
|
|
|
|
return fcus
|
2022-04-26 11:04:44 +02:00
|
|
|
|
2022-04-27 14:54:05 +02:00
|
|
|
def create_cycle_modifiers(ob=None):
|
|
|
|
ob = ob or bpy.context.object
|
|
|
|
ct = 0
|
|
|
|
keyable_fcurves = get_only_pose_keyable_fcurves(ob)
|
|
|
|
for fc in keyable_fcurves:
|
|
|
|
if [m for m in fc.modifiers if m.type == 'CYCLES']:
|
|
|
|
# skip if already existing modifier
|
2022-04-13 18:38:03 +02:00
|
|
|
continue
|
2022-04-13 19:34:12 +02:00
|
|
|
# print(f'Adding cycle modifier {fc.data_path}')
|
|
|
|
_m = fc.modifiers.new(type='CYCLES')
|
2022-04-26 12:14:09 +02:00
|
|
|
ct += 1
|
2022-04-19 15:33:06 +02:00
|
|
|
fc.update()
|
2022-04-26 12:14:09 +02:00
|
|
|
|
|
|
|
print(f'Added cyclic modifiers on {ct} fcurve(s)')
|
|
|
|
return ct
|
2022-04-19 15:33:06 +02:00
|
|
|
|
|
|
|
## Get collection, create if necessary
|
|
|
|
def get_col(name, parent=None, create=True):
|
|
|
|
parent = parent or bpy.context.scene.collection
|
|
|
|
col = bpy.data.collections.get(name)
|
|
|
|
if not col and create:
|
|
|
|
col = bpy.data.collections.new(name)
|
|
|
|
|
|
|
|
if col not in parent.children[:]:
|
|
|
|
parent.children.link(col)
|
|
|
|
|
|
|
|
return col
|
|
|
|
|
|
|
|
def go_edit_mode(ob, context=None):
|
|
|
|
'''set mode to object, set passed obhject as active and go Edit'''
|
|
|
|
|
|
|
|
context = context or bpy.context
|
|
|
|
bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
|
|
|
|
ob.select_set(True)
|
|
|
|
context.view_layer.objects.active = ob
|
2022-04-25 18:12:24 +02:00
|
|
|
bpy.ops.object.mode_set(mode='EDIT', toggle=False)
|
|
|
|
|
|
|
|
|
|
|
|
def get_visible_bones(ob):
|
|
|
|
'''Get name of all editable bones (unhided *and* on a visible bone layer'''
|
|
|
|
# visible bone layer index
|
|
|
|
visible_layers_indexes = [i for i, l in enumerate(ob.data.layers) if l]
|
|
|
|
# check if layers overlaps
|
|
|
|
visible_bones = [b for b in ob.data.bones \
|
|
|
|
if not b.hide \
|
|
|
|
if any(i for i, l in enumerate(b.layers) if l and i in visible_layers_indexes)]
|
|
|
|
|
|
|
|
return visible_bones
|
|
|
|
|
|
|
|
|
|
|
|
def get_x_pos_of_visible_keys(ob, act):
|
|
|
|
'''Get an object and associated action
|
|
|
|
return x.coordinate of all fcurves.keys of all visible bones
|
|
|
|
'''
|
|
|
|
|
|
|
|
## just skip offset
|
|
|
|
# return [k.co.x for fc in act.fcurves if not '.offset' in fc.data_path for k in fc.keyframe_points]
|
|
|
|
|
|
|
|
## skip offset + fcu related to invisible bones
|
|
|
|
viz_bones = get_visible_bones(ob)
|
|
|
|
visible_bone_names = [b.name for b in viz_bones]
|
|
|
|
|
|
|
|
keys = []
|
|
|
|
for fc in act.fcurves:
|
|
|
|
if '.offset' in fc.data_path:
|
|
|
|
continue
|
|
|
|
if not '"' in fc.data_path:
|
|
|
|
continue
|
|
|
|
if not fc.data_path.split('"')[1] in visible_bone_names:
|
|
|
|
continue
|
|
|
|
keys += [k.co.x for k in fc.keyframe_points]
|
|
|
|
|
|
|
|
return keys
|
2022-10-15 19:13:50 +02:00
|
|
|
|
|
|
|
# def clear_duplicated_keys_in_fcurves()
|
|
|
|
def clean_fcurve(action=None):
|
|
|
|
'''clear duplicated keys at same frame in fcurves'''
|
|
|
|
|
|
|
|
cleaned = 0
|
|
|
|
problems = 0
|
|
|
|
if action is None:
|
|
|
|
bpy.context.object.animation_data.action
|
|
|
|
for fcu in action.fcurves:
|
|
|
|
prev = None
|
|
|
|
for k in reversed(fcu.keyframe_points):
|
|
|
|
if not prev:
|
|
|
|
prev = k
|
|
|
|
continue
|
|
|
|
if prev.co.x == k.co.x:
|
|
|
|
if prev.co.y == k.co.y:
|
|
|
|
print(f'autoclean: 2 idential keys at {k.co.x} ', fcu.data_path, fcu.array_index)
|
|
|
|
fcu.keyframe_points.remove(prev)
|
|
|
|
cleaned += 1
|
|
|
|
else:
|
|
|
|
print(f'/!\ 2 keys with different value at {k.co.x} ! : ', fcu.data_path, fcu.array_index)
|
|
|
|
problems += 1
|
|
|
|
prev = k
|
|
|
|
|
|
|
|
if problems:
|
|
|
|
return ('ERROR', f'{problems} keys are overlapping (see console)')
|
|
|
|
if cleaned:
|
|
|
|
return ('WARNING', f'{cleaned} keys autocleaned')
|
|
|
|
|