import bpy import bpy_types 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') """ name = get_root_name() 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() 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] ### --- context manager - store / restore class attr_set(): '''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 ''' def __init__(self, attrib_list): self.store = [] # item = (prop, attr, [new_val]) for item in attrib_list: prop, attr = item[:2] self.store.append( (prop, attr, getattr(prop, attr)) ) if len(item) >= 3: setattr(prop, attr, item[2]) 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)