diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9c3433 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Videos/ \ No newline at end of file diff --git a/CAfP.zip b/CAfP.zip new file mode 100644 index 0000000..31ee075 Binary files /dev/null and b/CAfP.zip differ diff --git a/CAfP/.vscode/.ropeproject/config.py b/CAfP/.vscode/.ropeproject/config.py new file mode 100644 index 0000000..dee2d1a --- /dev/null +++ b/CAfP/.vscode/.ropeproject/config.py @@ -0,0 +1,114 @@ +# The default ``config.py`` +# flake8: noqa + + +def set_prefs(prefs): + """This function is called before opening the project""" + + # Specify which files and folders to ignore in the project. + # Changes to ignored resources are not added to the history and + # VCSs. Also they are not returned in `Project.get_files()`. + # Note that ``?`` and ``*`` match all characters but slashes. + # '*.pyc': matches 'test.pyc' and 'pkg/test.pyc' + # 'mod*.pyc': matches 'test/mod1.pyc' but not 'mod/1.pyc' + # '.svn': matches 'pkg/.svn' and all of its children + # 'build/*.o': matches 'build/lib.o' but not 'build/sub/lib.o' + # 'build//*.o': matches 'build/lib.o' and 'build/sub/lib.o' + prefs['ignored_resources'] = ['*.pyc', '*~', '.ropeproject', + '.hg', '.svn', '_svn', '.git', '.tox'] + + # Specifies which files should be considered python files. It is + # useful when you have scripts inside your project. Only files + # ending with ``.py`` are considered to be python files by + # default. + # prefs['python_files'] = ['*.py'] + + # Custom source folders: By default rope searches the project + # for finding source folders (folders that should be searched + # for finding modules). You can add paths to that list. Note + # that rope guesses project source folders correctly most of the + # time; use this if you have any problems. + # The folders should be relative to project root and use '/' for + # separating folders regardless of the platform rope is running on. + # 'src/my_source_folder' for instance. + # prefs.add('source_folders', 'src') + + # You can extend python path for looking up modules + # prefs.add('python_path', '~/python/') + + # Should rope save object information or not. + prefs['save_objectdb'] = True + prefs['compress_objectdb'] = False + + # If `True`, rope analyzes each module when it is being saved. + prefs['automatic_soa'] = True + # The depth of calls to follow in static object analysis + prefs['soa_followed_calls'] = 0 + + # If `False` when running modules or unit tests "dynamic object + # analysis" is turned off. This makes them much faster. + prefs['perform_doa'] = True + + # Rope can check the validity of its object DB when running. + prefs['validate_objectdb'] = True + + # How many undos to hold? + prefs['max_history_items'] = 32 + + # Shows whether to save history across sessions. + prefs['save_history'] = True + prefs['compress_history'] = False + + # Set the number spaces used for indenting. According to + # :PEP:`8`, it is best to use 4 spaces. Since most of rope's + # unit-tests use 4 spaces it is more reliable, too. + prefs['indent_size'] = 4 + + # Builtin and c-extension modules that are allowed to be imported + # and inspected by rope. + prefs['extension_modules'] = [] + + # Add all standard c-extensions to extension_modules list. + prefs['import_dynload_stdmods'] = True + + # If `True` modules with syntax errors are considered to be empty. + # The default value is `False`; When `False` syntax errors raise + # `rope.base.exceptions.ModuleSyntaxError` exception. + prefs['ignore_syntax_errors'] = False + + # If `True`, rope ignores unresolvable imports. Otherwise, they + # appear in the importing namespace. + prefs['ignore_bad_imports'] = False + + # If `True`, rope will insert new module imports as + # `from import ` by default. + prefs['prefer_module_from_imports'] = False + + # If `True`, rope will transform a comma list of imports into + # multiple separate import statements when organizing + # imports. + prefs['split_imports'] = False + + # If `True`, rope will remove all top-level import statements and + # reinsert them at the top of the module when making changes. + prefs['pull_imports_to_top'] = True + + # If `True`, rope will sort imports alphabetically by module name instead + # of alphabetically by import statement, with from imports after normal + # imports. + prefs['sort_imports_alphabetically'] = False + + # Location of implementation of + # rope.base.oi.type_hinting.interfaces.ITypeHintingFactory In general + # case, you don't have to change this value, unless you're an rope expert. + # Change this value to inject you own implementations of interfaces + # listed in module rope.base.oi.type_hinting.providers.interfaces + # For example, you can add you own providers for Django Models, or disable + # the search type-hinting in a class hierarchy, etc. + prefs['type_hinting_factory'] = ( + 'rope.base.oi.type_hinting.factory.default_type_hinting_factory') + + +def project_opened(project): + """This function is called after opening the project""" + # Do whatever you like here! diff --git a/CAfP/.vscode/.ropeproject/objectdb b/CAfP/.vscode/.ropeproject/objectdb new file mode 100644 index 0000000..0a47446 Binary files /dev/null and b/CAfP/.vscode/.ropeproject/objectdb differ diff --git a/CAfP/__init__.py b/CAfP/__init__.py index 6fa60f4..0ccd94b 100644 --- a/CAfP/__init__.py +++ b/CAfP/__init__.py @@ -16,7 +16,7 @@ "author" : "daksitha", "description" : "", "blender" : (2, 83, 0), - "version" : (0, 0, 1), + "version" : (0, 1, 2), "location" : "View3D", "warning" : "", "category" : "Animation" @@ -25,6 +25,9 @@ import bpy from bpy.utils import unregister_class from bpy.utils import register_class +import os +import json +from bpy.app.handlers import save_post #from . damping_tools import DampAnimation #from . offset_tools import AngularOffsetAnimation @@ -32,11 +35,24 @@ from . time_warping import CreateTimeWarpingCurve from . time_warping import TimeWarper -from . file_op import LoadPoseBone from . file_op import ExportPoseBone - +from . file_op import LoadPoseBone + import CAfP.global_config as global_config +from . import file_op +from . import bone_motion + +from bpy.app.handlers import persistent +################# Globals ############# + +# @persistent +# def change_param_file_name(scene): +# blender_path =bpy.data.filepath +# blen_file_name = file_op.simple_path(blender_path) +# global_config.BONE_PARAMETERS_FILE_NAME = blen_file_name + +# bpy.app.handlers.save_post.append(change_param_file_name) ################## bone properties ################### def update_bone_animation(self, context): #print("bone animator", self) @@ -137,6 +153,15 @@ def update_bone_animation(self, context): ) +#range selected flag +bpy.types.PoseBone.is_range_selected = bpy.props.BoolProperty( + name = "isRangeSelected", + description="check whethere the range to be edited is selected", + default=False, + + ) + + ################# NLA Opporation variable ################## nla_strip_list = [] def get_nla_strips_list(scene, context): @@ -160,7 +185,6 @@ def get_nla_strips_list(scene, context): # if obj.animation_data is not None: # nla_strip = obj.animation_data.nla_tracks # if len(nla_strip): - def update_timewarping(self,context): @@ -198,10 +222,172 @@ def update_timewarping(self,context): description="initial location of timewarping control curve", ) + + + +class SELECTRange(bpy.types.Operator): + """ + Select the keyframe range to edit in the current animation. + """ + bl_label = 'Select the keyframe range to edit' + bl_idname = 'cafp.select_posebone_range' + + + @classmethod + def poll(cls, context): + + if not (context.mode == 'POSE'): + return False + + obj = context.active_object # type: bpy.types.Object + active_bone = bpy.context.active_pose_bone # type: bpy.types.PoseBone + if obj is not None: + if obj.type == 'ARMATURE': + anim_data = obj.animation_data + if anim_data is not None: + act = anim_data.action + if act is not None: + if active_bone is not None: + return True + + return False + + def execute(self,context): + import json + active_armature = bpy.context.active_object + active_bone = bpy.context.active_pose_bone # type: bpy.types.PoseBone + + if not len(global_config.BONE_PARAMETERS_JSON): + path_ =bpy.path.abspath("//{}".format(global_config.BONE_PARAMETERS_FILE_NAME)) + global_config.BONE_PARAMETERS_JSON = bone_motion.getAllPoseBoneParameters(self,active_armature,path_) + + active_bone.is_range_selected = True + + # try: + # #save a file in the current directory + # path =bpy.path.abspath("//CAfP_Temp_BProperties.json") + # #print(path) + # out_data = {} + # out_data['armature_name'] = '{}'.format(obj.name) + # if not isInCurrDirectory('CAfP_Temp_BProperties.json'): + # with open(path, 'w') as out_file: + # json.dump(obj=out_data, fp=out_file, indent=2) + # self.report({'INFO'}, 'Create a CAfP temp_bone.json at {}'.format(path)) + + # active_bone.is_range_selected = True + # except OSError as err: + # self.report('WARNING','Error {} while creating CAfP_temp_bone.json. Please save the blend file'.format(err)) + # active_bone.is_range_selected = False + # except IOError as err: + # active_bone.is_range_selected = False + # if path != "": + # self.report({'WARNING'},"{},while creating {}. Please save the blend file".format(err,bpy.path.basename(path))) + # except json.JSONDecodeError: + # active_bone.is_range_selected = False + # self.report({'WARNING'},"Errors in json file: {}".format(bpy.path.basename(path))) + + + + + return {'FINISHED'} + + +class GoBackToRangeSelector(bpy.types.Operator): + """ + Save the current changes for this range and go back to range panel. + """ + bl_label = 'Go back to range selector panel' + bl_idname = 'cafp.back_to_range_selection' + + @classmethod + def poll(cls, context): + + if not (context.mode == 'POSE'): + return False + + obj = context.active_object # type: bpy.types.Object + active_bone = bpy.context.active_pose_bone # type: bpy.types.PoseBone + if obj is not None: + if obj.type == 'ARMATURE': + anim_data = obj.animation_data + if anim_data is not None: + act = anim_data.action + if act is not None: + if active_bone is not None: + return True + + def execute(self,context): + import json + import time + + active_armature = bpy.context.active_object + path =bpy.path.abspath("//{}".format(global_config.BONE_PARAMETERS_FILE_NAME)) + if not len(global_config.BONE_PARAMETERS_JSON): + global_config.BONE_PARAMETERS_JSON = bone_motion.getAllPoseBoneParameters(self,active_armature,path) + + + active_bone = bpy.context.active_pose_bone + #bone_motion.appendBoneData(self,active_bone, path) + + active_bone.is_range_selected = False + + + return {'FINISHED'} + +class ApplyBoneParameters(bpy.types.Operator): + """ + Cash/Save pose bone configurations for future use . + """ + bl_label = 'Save PoseBone configuration' + bl_idname = 'cafp.save_posebone_configuration' + + @classmethod + def poll(cls, context): + + if not (context.mode == 'POSE'): + return False + + obj = context.active_object # type: bpy.types.Object + active_bone = bpy.context.active_pose_bone # type: bpy.types.PoseBone + if obj is not None: + if obj.type == 'ARMATURE': + anim_data = obj.animation_data + if anim_data is not None: + act = anim_data.action + if act is not None: + if active_bone is not None: + return True + + def execute(self,context): + import json + import time + + + #obj = bpy.context.active_object + #save a file in the current directory + path =bpy.path.abspath("//{}".format(global_config.BONE_PARAMETERS_FILE_NAME)) + active_armature = bpy.context.active_object + if not len(global_config.BONE_PARAMETERS_JSON): + global_config.BONE_PARAMETERS_JSON = bone_motion.getAllPoseBoneParameters(self,active_armature,path) + + active_bone = bpy.context.active_pose_bone + param_json = {} + param_json,isDefaultValue = file_op.get_bone_parameters_forJson(param_json,active_bone) + + bone_motion.appendBoneData(self,active_bone, param_json,path) + + return {'FINISHED'} + + + + + ############################## main panel ########################################### isArmWithAnim = False isArmWithNLA = False + + class VIEW3D_PT_cafpmainpanle(bpy.types.Panel): bl_label = "CAfP_Panel" bl_space_type = 'VIEW_3D' @@ -219,6 +405,7 @@ def draw(self, context): scene = context.scene box_info = self.layout.box() + if active_object is not None: if active_object.type == 'ARMATURE': @@ -254,7 +441,8 @@ def draw(self, context): isArmWithNLA = False - #Assume posemode only active for armature types + + if isArmWithAnim: if context.mode == 'POSE': @@ -275,53 +463,73 @@ def draw(self, context): if global_config.gui_status.startswith("ACTIVE"): active_bone = bpy.context.active_pose_bone - #TODO:Active still it has no bone selected - #bone_name = active_bone.basename layout.row() + #active_armature = context.active_object #layout.row().label(text="Active Bone: {}".format(bone_name), icon= 'BONE_DATA') - #Range - settings_row = layout.row(align=True) - settings_box = settings_row.box() - settings_box.label(text="Keyframe Range to Change", icon='SETTINGS') - #damping - damp_row = layout.row(align=True) - damping_box = damp_row.box() - damping_box.label(text = "Animation Amplitude Modifier:",icon='FORCE_HARMONIC') - - #offset - offset_row = layout.row() - offset_box = offset_row.box() - offset_box.label(text = "Animation Angular Offset:",icon = 'ORIENTATION_GIMBAL') - - if active_bone is not None: + + - set_col = settings_box.column(align=True) - set_col.prop(active_bone, "anim_start", expand=True) - set_col.prop(active_bone, "anim_ignore_from_start", expand=True) - set_col.prop(active_bone, "anim_ignore_from_end", expand=True) - col = damping_box.column(align=True) - col.prop(active_bone, "damping_x_scale",toggle=True, expand=True) - col.prop(active_bone, "damping_y_scale",toggle=True, expand=True) - col.prop(active_bone, "damping_z_scale",toggle=True, expand=True) + + #go back to range selector + + + if active_bone is not None: - col2 = offset_box.column(align=True) - col2.prop(active_bone, "angular_offset_x",toggle=True, expand=True) - col2.prop(active_bone, "angular_offset_y",toggle=True, expand=True) - col2.prop(active_bone, "angular_offset_z",toggle=True, expand=True) + if active_bone.is_range_selected: + #damping + damp_row = layout.row(align=True) + damping_box = damp_row.box() + damping_box.label(text = "Animation Amplitude Modifier:",icon='FORCE_HARMONIC') + + #offset + offset_row = layout.row() + offset_box = offset_row.box() + offset_box.label(text = "Animation Angular Offset:",icon = 'ORIENTATION_GIMBAL') + + col = damping_box.column(align=True) + col.prop(active_bone, "damping_x_scale",toggle=True, expand=True) + col.prop(active_bone, "damping_y_scale",toggle=True, expand=True) + col.prop(active_bone, "damping_z_scale",toggle=True, expand=True) + + col2 = offset_box.column(align=True) + col2.prop(active_bone, "angular_offset_x",toggle=True, expand=True) + col2.prop(active_bone, "angular_offset_y",toggle=True, expand=True) + col2.prop(active_bone, "angular_offset_z",toggle=True, expand=True) + + col3 = offset_box.row(align=True) + col3.operator('cafp.save_posebone_configuration', text= 'Save') + col3.operator('cafp.back_to_range_selection', text= 'Back') + else: + #Range + settings_row = layout.row(align=True) + settings_box = settings_row.box() + settings_box.label(text="Keyframe Range for Change", icon='SETTINGS') + + set_col = settings_box.column(align=True) + bn_name = set_col.row() + bn_name.label(text='Active Bone: {}'.format(active_bone.basename)) + #set_col.prop(active_bone, "anim_start", expand=True) + set_col.prop(active_bone, "anim_ignore_from_start", expand=True) + set_col.prop(active_bone, "anim_ignore_from_end", expand=True) + + set_col.operator('cafp.select_posebone_range', text= 'Next') + else: - infor_damp = damping_box.row() - info_off = offset_box.row() - info_off.label(text= "select a bone") - infor_damp.label(text= "select a bone") + warning_ = layout.row(align=True) + warning_box = warning_.box() + warning_box.label(text="WARNING", icon='BONE_DATA') + warning_box.row().label(text="Select single bonea") + + #expoert and import exp_rw = layout.row(align=True) exp_box = exp_rw.box() - exp_box.label(text="Export PoseBone changes", icon='EXPORT') - export_butt_rw = exp_box.row() - export_butt_rw.operator('cafp.export_posebone_data',text='Export') + exp_box.label(text="Load PoseBone Values", icon='EXPORT') + #export_butt_rw = exp_box.row() + #export_butt_rw.operator('cafp.export_posebone_data',text='Export') import_butt_rw = exp_box.row() import_butt_rw.operator('cafp.import_posebone_data',text='Import') @@ -374,7 +582,8 @@ def draw(self, context): -classes = (VIEW3D_PT_cafpmainpanle,BoneAnimatorOffDamp,CreateTimeWarpingCurve,TimeWarper,ExportPoseBone,LoadPoseBone) +classes = (VIEW3D_PT_cafpmainpanle,BoneAnimatorOffDamp,CreateTimeWarpingCurve,TimeWarper,ExportPoseBone,\ + LoadPoseBone,SELECTRange,GoBackToRangeSelector,ApplyBoneParameters) def register(): for clas in classes: diff --git a/CAfP/__pycache__/__init__.cpython-37.pyc b/CAfP/__pycache__/__init__.cpython-37.pyc index 957d640..f519f2c 100644 Binary files a/CAfP/__pycache__/__init__.cpython-37.pyc and b/CAfP/__pycache__/__init__.cpython-37.pyc differ diff --git a/CAfP/__pycache__/bone_motion.cpython-37.pyc b/CAfP/__pycache__/bone_motion.cpython-37.pyc index db71b1d..38c059c 100644 Binary files a/CAfP/__pycache__/bone_motion.cpython-37.pyc and b/CAfP/__pycache__/bone_motion.cpython-37.pyc differ diff --git a/CAfP/__pycache__/file_op.cpython-37.pyc b/CAfP/__pycache__/file_op.cpython-37.pyc index 8b68c18..f3a4dba 100644 Binary files a/CAfP/__pycache__/file_op.cpython-37.pyc and b/CAfP/__pycache__/file_op.cpython-37.pyc differ diff --git a/CAfP/__pycache__/global_config.cpython-37.pyc b/CAfP/__pycache__/global_config.cpython-37.pyc index 476f59e..10ce50d 100644 Binary files a/CAfP/__pycache__/global_config.cpython-37.pyc and b/CAfP/__pycache__/global_config.cpython-37.pyc differ diff --git a/CAfP/__pycache__/time_warping.cpython-37.pyc b/CAfP/__pycache__/time_warping.cpython-37.pyc index 3d876c0..5f34743 100644 Binary files a/CAfP/__pycache__/time_warping.cpython-37.pyc and b/CAfP/__pycache__/time_warping.cpython-37.pyc differ diff --git a/CAfP/bone_motion.py b/CAfP/bone_motion.py index 78c9a2c..48914d1 100644 --- a/CAfP/bone_motion.py +++ b/CAfP/bone_motion.py @@ -5,8 +5,10 @@ from mathutils import Euler from mathutils import Vector import math +import json import CAfP.global_config as global_config +from . import file_op def bone_animator(self, act: bpy.types.Action, bone_name: str, damp_weights: Vector, angular_offset: Vector, tail_crop=0, head_crop=0, crop_start=0) -> None: @@ -96,9 +98,10 @@ def bone_animator(self, act: bpy.types.Action, bone_name: str, damp_weights: Vec else: self.report({'WARNING'}, "Unexpected keyframe crop. n_keyframes {0}, crop (start_kf:{1},head_crop_kf:{2},tail_crop_kf:{3} "\ .format(n_keyframes_w,crop_start,head_crop,tail_crop)) - minKeyframe = 0 - maxKeyframe = n_keyframes_w - self.report({'INFO'}, "Applying to all keyframes from {} till {}".format(minKeyframe,maxKeyframe) ) + # minKeyframe = 0 + # maxKeyframe = n_keyframes_w + # self.report({'INFO'}, "Applying to all keyframes from {} till {}".format(minKeyframe,maxKeyframe) ) + return for i in range(minKeyframe,maxKeyframe): @@ -175,6 +178,111 @@ def get_bone_properties(bone): edit_range = [s_crop,head_crop,tail_crop] return damping_weights,angular_offset,edit_range +def getAllPoseBoneParameters(self, activeArm:bpy.types.Armature, path): + out_data = {} + poseBones = activeArm.pose.bones + try: + #get parameters that are saved + + out_data["name"] = "{}".format(activeArm.name) + + if file_op.isInCurrDirectory(global_config.BONE_PARAMETERS_FILE_NAME): + self.report({"INFO"},"Extracting posebone parameters from {} ...".format(global_config.BONE_PARAMETERS_FILE_NAME)) + with open(path, 'r') as j_file: + + j_database = json.loads(j_file.read()) + + if "name" in j_database: + if j_database["name"] != activeArm.name: + self.report({"WARNING"}, " Armature name missmatch {} != {}".format(activeArm.name,j_database["name"])) + + out_data = j_database + else: + count = 0 + for bone in poseBones: # type: bpy.types.PoseBone + param_json = {} + #range + param_json,isDefaultValue = file_op.get_bone_parameters_forJson(param_json, bone) + if not isDefaultValue: + if param_json is not None: + out_data['{0}'.format(bone.basename)] = [] + out_data['{0}'.format(bone.basename)].append(param_json) + count+=count + else: + self.report({"ERROR"}, "getAllPoseBoneParameters Error: param_json {},isDefaultValue {} ".format(param_json,isDefaultValue)) + + self.report({"INFO"}, "There are {} number of bones with non default values".format(count)) + + + with open(path, 'w') as out_file: + json.dump(obj=out_data, fp=out_file, indent=2) + print('Creating {0} at {1} ..'.format(global_config.BONE_PARAMETERS_FILE_NAME, file_op.simple_path(path))) + + except OSError as err: + self.report({"ERROR"}, "getAllPoseBoneParameters Error: {} ".format(err)) + #print('Error {} while creating CAfP_temp_bone.json. Please save the blend file'.format(err)) + + except IOError as err: + if bpy.data.is_saved: + self.report({"ERROR"}, "getAllPoseBoneParameters Error: {},while creating {}. Maybe save the blend file".format(err,bpy.path.basename(path))) + except json.JSONDecodeError: + self.report({"ERROR"}, "getAllPoseBoneParameters Error: in json file: {}".format(bpy.path.basename(path))) + + return out_data + + +def appendBoneData(self, active_bone,param_json, path): + try: + bone_dictionary = global_config.BONE_PARAMETERS_JSON + bone_name = active_bone.name + isRangeExist = False + #param_json = {} + #param_json,isDefaultValue = file_op.get_bone_parameters_forJson(param_json,active_bone) + + if bone_name in bone_dictionary: + bone_array = bone_dictionary["{}".format(bone_name)] #Json Array + + for item in bone_array: + if item["anim_ignore_from_start"] == param_json["anim_ignore_from_start"] and item["anim_ignore_from_end"] \ + == param_json["anim_ignore_from_end"]: + isRangeExsit = True + item["damping_x_scale"] = param_json["damping_x_scale"] + item["damping_y_scale"] = param_json["damping_y_scale"] + item["damping_z_scale"] = param_json["damping_z_scale"] + + item["angular_offset_x"] = param_json["angular_offset_x"] + item["angular_offset_y"] = param_json["angular_offset_y"] + item["angular_offset_z"] = param_json["angular_offset_z"] + + self.report({'INFO'},"{} Replace range: start:{} end:{}".\ + format(bone_name,item["anim_ignore_from_start"],item["anim_ignore_from_end"] )) + break + + + if not isRangeExist: + bone_array.append(param_json) + self.report({'INFO'},"{} new range start:{} end:{}".format(bone_name,param_json["anim_ignore_from_start"],param_json["anim_ignore_from_end"] )) + else: + bone_dictionary["{}".format(bone_name)] = [] + bone_dictionary["{}".format(bone_name)].append(param_json) + self.report({'INFO'},"New bone added: {} parameter file".format(bone_name)) + + global_config.BONE_PARAMETERS_JSON = bone_dictionary + out_data = bone_dictionary + with open(path, 'w') as output: + json.dump(obj=out_data, fp=output, indent=2) + + except OSError as err: + self.report('ERROR','appendBoneData Error {} while creating CAfP_temp_bone.json. Please save the blend file'.format(err)) + except IOError as err: + if bpy.data.is_saved: + self.report({'ERROR'},"appendBoneData Error {},while creating {}. Please save the blend file".format(err,bpy.path.basename(path))) + except json.JSONDecodeError as err: + self.report({'ERROR'},"appendBoneData Error {}in json file: {}".format(err, bpy.path.basename(path))) + + + + class BoneAnimatorOffDamp(bpy.types.Operator): """Rotational translate animation curves""" bl_idname = "cafp.boneanimator" @@ -199,12 +307,10 @@ def poll(cls, context): def execute(self, context): - - # Active Armature - obj = context.active_object # type: bpy.types.Object - if obj.type != 'ARMATURE': - self.report({'ERROR'},"Selected object {} is not ARMATURE type".format(obj)) + activeArmature = context.active_object # type: bpy.types.Object + if activeArmature.type != 'ARMATURE': + self.report({'ERROR'},"Selected object {} is not ARMATURE type".format(activeArmature)) return {'CANCELLED'} #active item @@ -215,9 +321,9 @@ def execute(self, context): #animation - action = obj.animation_data.action + action = activeArmature.animation_data.action - if active_bone is not None and not global_config.IMPORT_DATA: + if active_bone is not None and not global_config.IMPORT_DATA_FLAG: bone_name = active_bone.basename damping_weights,angular_offset,edit_range = get_bone_properties(active_bone) @@ -226,14 +332,68 @@ def execute(self, context): angular_offset= angular_offset,tail_crop=edit_range[2], head_crop=edit_range[1], crop_start=edit_range[0]) else: - self.report({'ERROR'},"Armature {0} has no animation data".format(obj)) + self.report({'ERROR'},"Armature {0} has no animation data".format(activeArmature)) return {'CANCELLED'} - elif global_config.IMPORT_DATA: - for bone_ in load_data_bones: - bone_name = bone_.basename - damping_weights,angular_offset,edit_range = get_bone_properties(bone_) - bone_animator(self, act=action,bone_name= bone_name,damp_weights= damping_weights,\ - angular_offset= angular_offset,tail_crop=edit_range[2], head_crop=edit_range[1], crop_start=edit_range[0]) + + elif global_config.IMPORT_DATA_FLAG: + global_config.IMPORT_DATA_FLAG=False + activeBones = activeArmature.pose.bones + impBones = global_config.IMPORT_DATA_ + + if impBones is not None: + self.report({'INFO'},"{} Calling import function on:".format(activeArmature.name)) + for imKey, imVal in impBones.items(): + for currBone in activeBones: + + curr_bn = currBone.basename + im_bn = imKey + #This manual operation is needed because Mixamo rig starts with strings which are + #different from one another. for example mixamorigxx:spine where xx is a number + if im_bn.startswith('mixamorig'): + split_im = im_bn.split(":",1) + if len(split_im)==2: + im_bn = split_im[1] + + if curr_bn.startswith('mixamorig'): + split_curr = curr_bn.split(":",1) + if len(split_curr) ==2: + curr_bn = split_curr[1] + + if curr_bn == im_bn: + for item in imVal: #consit of json object for each range + #setattr(currBone,im_props,prp_value) + if item is not None: + + x_dp = item["damping_x_scale"] + y_dp = item["damping_y_scale"] + z_dp = item["damping_z_scale"] + damping_weights = Vector((x_dp,y_dp,z_dp)) + + x_an = item["angular_offset_x"] + y_an = item["angular_offset_y"] + z_an = item["angular_offset_z"] + angular_offset = Vector((x_an,y_an,z_an)) + + s_crop = 0 + head_crop = item["anim_ignore_from_start"] + tail_crop = item["anim_ignore_from_end"] + self.report({'INFO'},"Importing {0} bone for range: head_crop:{1} tail_crop:{2} :".format(curr_bn,head_crop,tail_crop)) + #change the bone + bone_animator(self, act=action,bone_name= currBone.basename,damp_weights= damping_weights,\ + angular_offset= angular_offset,tail_crop=tail_crop, head_crop=head_crop, crop_start=s_crop) + #append the value to the file + path =bpy.path.abspath("//{}".format(global_config.BONE_PARAMETERS_FILE_NAME)) + appendBoneData(self,active_bone= currBone, param_json= item, path=path) + else: + self.report({'WARNING'},"{} imported empty property values".format(curr_bn)) + + else: + self.report({'ERROR'},"IMPORT_DATA_ is empty") + + + + + else: self.report({'ERROR'},"No active_bone") diff --git a/CAfP/file_op.py b/CAfP/file_op.py index 63ef03c..8601976 100644 --- a/CAfP/file_op.py +++ b/CAfP/file_op.py @@ -18,6 +18,73 @@ def simple_path(input_path, use_basename=True, max_len=50): return input_path +def load_json_data(self, json_path, description=None): + + try: + time1 = time.time() + with open(json_path, "r") as j_file: + j_database = json.load(j_file) + if not description: + self.report({'INFO'},"Json database {} loaded in {} secs".format\ + (simple_path(json_path), time.time()-time1)) + else: + self.report({'INFO'},"{} loaded from {} in {} secs".format\ + (description, simple_path(json_path), time.time()-time1)) + return j_database + except IOError: + if simple_path(json_path) != "": + self.report({'WARNING'},"File not found: {}".format(simple_path(json_path))) + except json.JSONDecodeError: + self.report({'WARNING'},"Errors in json file: {}".format(simple_path(json_path))) + return None + +def isInCurrDirectory(file_name): + filepath = bpy.data.filepath + directory = os.path.dirname(filepath) + _, _, filenames = next(os.walk(directory)) + for files_ in filenames: + if files_ == file_name: + print('file {} already in the direcotry'.format(files_)) + return True + return False + +def get_bone_parameters_forJson(param_json, bone): + + isDefaultValue = True + #param_json['anim_start'] = bone.anim_start + param_json["anim_ignore_from_start"] = bone.anim_ignore_from_start + param_json["anim_ignore_from_end"] = bone.anim_ignore_from_end + + x_scale = round(bone.damping_x_scale,3) + y_scale = round(bone.damping_y_scale,3) + z_scale = round(bone.damping_z_scale,3) + + scales = [x_scale,y_scale,z_scale] + for s in scales: + if s != 1.0: + isDefaultValue = False + + param_json["damping_x_scale"] = x_scale + param_json["damping_y_scale"] = y_scale + param_json["damping_z_scale"] = z_scale + + x_offset = round(bone.angular_offset_x,3) + y_offset =round( bone.angular_offset_y,3) + z_offset = round(bone.angular_offset_z,3) + + offsets = [x_offset,y_offset,z_offset] + for f in offsets: + if f != 0: + isDefaultValue = False + + param_json["angular_offset_x"] = x_offset + param_json["angular_offset_y"] = y_offset + param_json["angular_offset_z"] = z_offset + + return param_json,isDefaultValue + + + class LoadPoseBone(bpy.types.Operator, ImportHelper): """ Load cafp posebone properties from a JSON. @@ -47,40 +114,17 @@ def execute(self, context): self.ShowMessageBox(message = "May not a valid file !") return {'FINISHED'} #-------------------- - scene_poseB = context.visible_pose_bones - im_poseBones = self.load_json_data(self.filepath) - #Setting the IMPORT_DATA flag - #global_config.IMPORT_DATA = True + #scene_poseB = context.visible_pose_bones + scene_poseB = context.selected_pose_bones_from_active_object + im_poseBones = load_json_data(self, self.filepath) + #Setting the IMPORT_DATA flag if im_poseBones is not None: - for im_name, im_val_json in im_poseBones.items(): - #print("key {0} value{1}".format(im_name,im_val_json)) - for curr_bone in scene_poseB: - curr_bn = curr_bone.basename - im_bn = im_name - - - #BUG: mixamorig start with a different character - if im_bn.startswith('mixamorig'): - split_im = im_bn.split(":",1) - if len(split_im)==2: - im_bn = split_im[1] - #im_rg_name = split_im[0] - if curr_bn.startswith('mixamorig'): - split_curr = curr_bn.split(":",1) - if len(split_curr) ==2: - curr_bn = split_curr[1] - #curr_rg_name = split_curr[0] - - - if curr_bn == im_bn: - for im_props, prp_value in im_val_json[0].items(): - setattr(curr_bone,im_props,prp_value) - - #print("Set {0}: {1} = {2}".format(curr_bone.basename,im_props,prp_value)) - #run the oporator to update the changes - #bpy.ops.cafp.boneanimator() - #global_config.IMPORT_DATA=False + global_config.IMPORT_DATA_FLAG = True + global_config.IMPORT_DATA_ = im_poseBones + #run the oporator to update the changes + bpy.ops.cafp.boneanimator() + return {'FINISHED'} @@ -90,26 +134,9 @@ def draw(self, context): self.layout.label(text=message) bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) - def load_json_data(self, json_path, description=None): - - try: - time1 = time.time() - with open(json_path, "r") as j_file: - j_database = json.load(j_file) - if not description: - self.report({'INFO'},"Json database {} loaded in {} secs".format\ - (simple_path(json_path), time.time()-time1)) - else: - self.report({'INFO'},"{} loaded from {} in {} secs".format\ - (description, simple_path(json_path), time.time()-time1)) - return j_database - except IOError: - if simple_path(json_path) != "": - self.report({'WARNING'},"File not found: {}".format(simple_path(json_path))) - except json.JSONDecodeError: - self.report({'WARNING'},"Errors in json file: {}".format(simple_path(json_path))) - return None +#################### Export function ################## +#not really needed this since we save parameters anyways class ExportPoseBone(bpy.types.Operator): "Export all posebone angular offset and amplitude parameter to a json file" @@ -142,35 +169,12 @@ def execute(self, context): out_data = {} #metadeta - out_data['rig_name'] =obj.name + out_data['name'] =obj.name for bone in poseBones: # type: bpy.types.PoseBone param_json = {} #range - - param_json['anim_start'] = bone.anim_start - param_json['anim_ignore_from_start'] = bone.anim_ignore_from_start - param_json['anim_ignore_from_end'] = bone.anim_ignore_from_end - - x_scale = round(bone.damping_x_scale,3) - y_scale = round(bone.damping_y_scale,3) - z_scale = round(bone.damping_z_scale,3) - - param_json['damping_x_scale'] = x_scale - param_json['damping_y_scale'] = y_scale - param_json['damping_z_scale'] = z_scale - - - x_offset = round(bone.angular_offset_x,3) - y_offset =round( bone.angular_offset_y,3) - z_offset = round(bone.angular_offset_z,3) - - - param_json['angular_offset_x'] = x_offset - param_json['angular_offset_y'] = y_offset - param_json['angular_offset_z'] = z_offset - - + param_json,isDefaultValue = get_bone_parameters_forJson(param_json, bone) if len(param_json) != 0: self.report({'INFO'},"Exporting Posebone {0}".format(bone.basename)) @@ -185,7 +189,7 @@ def execute(self, context): with open(self.filepath, 'w') as out_file: json.dump(obj=out_data, fp=out_file, indent=2) else: - self.report({'ERROR'}, "There are no changes to the selected bone(s)") + self.report({'ERROR'}, "Export out_data is empty") return {'FINISHED'} diff --git a/CAfP/global_config.py b/CAfP/global_config.py index f170b49..68b4303 100644 --- a/CAfP/global_config.py +++ b/CAfP/global_config.py @@ -1,3 +1,6 @@ gui_status = "ACTIVE_SESSION" gui_err_msg = "" -IMPORT_DATA= False +IMPORT_DATA_FLAG= False +IMPORT_DATA_ = {} +BONE_PARAMETERS_JSON = {} +BONE_PARAMETERS_FILE_NAME = "CAfP_AllPoseBoneParameters.json"