338 lines
10 KiB
Python
338 lines
10 KiB
Python
import bpy
|
|
import re
|
|
import numpy as np
|
|
from mathutils import Matrix, Vector, Color
|
|
|
|
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)
|
|
|
|
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})')
|
|
|
|
def convertAttr(Attr):
|
|
'''Convert given value to a Json serializable format'''
|
|
if type(Attr) in [type(Vector()),type(Color())]:
|
|
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]])
|
|
elif type(Attr) == type(Matrix()):
|
|
return (np.matrix(Attr).tolist())
|
|
else:
|
|
return(Attr)
|
|
|
|
|
|
def get_gnd():
|
|
for o in bpy.context.scene.objects:
|
|
if o.type == 'MESH' and o.name.lower() in ('ground', 'gnd'):
|
|
return o
|
|
# nothing found
|
|
print('ERROR', 'No "gnd" object found')
|
|
|
|
|
|
def get_follow_curve_from_armature(arm):
|
|
"""Return curve and constraint or a tuple of string ('error', 'message')
|
|
"""
|
|
pref = get_addon_prefs()
|
|
name = pref.tgt_bone
|
|
|
|
parents = []
|
|
# root = b.id_data.pose.bones.get(name)
|
|
root = arm.pose.bones.get(name)
|
|
for c in root.constraints:
|
|
if c.type == 'FOLLOW_PATH':
|
|
const = c
|
|
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
|
|
|
|
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
|
|
|
|
# --- ACTIONS
|
|
|
|
def get_obj_action(obj):
|
|
print(helper())
|
|
act = obj.animation_data
|
|
if not act:
|
|
print('ERROR', f'no animation data on {obj.name}')
|
|
return
|
|
|
|
act = act.action
|
|
if not act:
|
|
print('ERROR', f'no action on {obj.name}')
|
|
return
|
|
return act
|
|
|
|
def set_generated_action(obj):
|
|
'''Backup object action and return a new action suffixed '_autogen'
|
|
associated with the object
|
|
'''
|
|
print(helper())
|
|
act = get_obj_action(obj)
|
|
if not act: return
|
|
|
|
regen = re.compile(r'_autogen\.?\d{0,3}$')
|
|
|
|
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()
|
|
new_act.name = act.name + '_autogen'
|
|
obj.animation_data.action = new_act
|
|
return new_act
|
|
|
|
|
|
def set_expanded_action(obj):
|
|
'''Backup object action and return a new action
|
|
associated with the object
|
|
'''
|
|
print(helper())
|
|
|
|
rexpand = re.compile(r'_expanded\.?\d{0,3}$')
|
|
act = obj.animation_data.action
|
|
|
|
if rexpand.search(act.name):
|
|
# is an autogenerated one
|
|
org_action_name = rexpand.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()
|
|
new_act.name = act.name + '_expanded'
|
|
obj.animation_data.action = new_act
|
|
return new_act
|
|
|
|
|
|
def get_curve_length(ob):
|
|
'''Get a curve object, return a float representing world space length'''
|
|
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
|
|
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
|
|
|
|
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'''
|
|
|
|
## auto-detect ?
|
|
## TODO root need to be user defined (or at least quickly adjustable)
|
|
## need to expose a scene prop from anim_cycle_settings
|
|
|
|
if context is None:
|
|
context = bpy.context
|
|
# settings = context.scene.anim_cycle_settings
|
|
## maybe use a field on interface
|
|
prefs = get_addon_prefs()
|
|
return prefs.tgt_bone
|
|
|
|
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'''
|
|
|
|
if context is None:
|
|
context = bpy.context
|
|
|
|
## 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
|
|
|
|
return curve
|
|
|
|
def update_action(act):
|
|
'''update fcurves (often broken after generation through API)'''
|
|
|
|
# update fcurves
|
|
for fcu in act.fcurves:
|
|
fcu.update()
|
|
|
|
# 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()
|
|
|
|
### --- context manager - store / restore
|
|
|
|
|
|
class attr_set():
|
|
'''Receive a list of tuple [(data_path:python_obj, "attribute":str, "wanted value":str)]
|
|
before with statement : Store existing values, assign wanted value
|
|
after with statement: Restore values to their old values
|
|
'''
|
|
|
|
def __init__(self, attrib_list):
|
|
self.store = []
|
|
for prop, attr, new_val in attrib_list:
|
|
self.store.append( (prop, attr, getattr(prop, attr)) )
|
|
if new_val == '_undefined': # None -> what if we want to apply None state ?
|
|
continue
|
|
setattr(prop, attr, new_val)
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
|
for prop, attr, old_val in self.store:
|
|
setattr(prop, attr, old_val)
|