Created
September 21, 2025 08:19
-
-
Save RH2/c53fc696a26ac62c13543f5f109847e5 to your computer and use it in GitHub Desktop.
json armature cache (restore head/tail/roll)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| import json | |
| import os | |
| bl_info = { | |
| "name": "Armature Cache SAVE/Restore", | |
| "blender": (3, 0, 0), | |
| "category": "Object", | |
| } | |
| def get_json_path(): | |
| if bpy.data.filepath: | |
| base_dir = os.path.dirname(bpy.data.filepath) | |
| else: | |
| base_dir = os.path.expanduser("~") | |
| return os.path.join(base_dir, "bones2.json") | |
| # ---------- Save Operator ---------- | |
| class ARMATURE_OT_SaveBones(bpy.types.Operator): | |
| """Save bone transforms (head/tail/roll) + custom properties to bones2.json""" | |
| bl_idname = "armature.save_bones" | |
| bl_label = "Save Armature (with roll)" | |
| def execute(self, context): | |
| obj = context.object | |
| if not obj or obj.type != "ARMATURE": | |
| self.report({"ERROR"}, "Select an armature") | |
| return {"CANCELLED"} | |
| # ensure object is active | |
| context.view_layer.objects.active = obj | |
| original_mode = obj.mode | |
| try: | |
| # enter edit mode to access edit_bones.roll | |
| if original_mode != 'EDIT': | |
| bpy.ops.object.mode_set(mode='EDIT') | |
| data = {} | |
| for eb in obj.data.edit_bones: | |
| name = eb.name | |
| # collect transforms from EditBone | |
| bone_data = { | |
| "head": [float(v) for v in eb.head], | |
| "tail": [float(v) for v in eb.tail], | |
| "roll": float(eb.roll), | |
| } | |
| # collect custom properties from the Bone data-block (not EditBone) | |
| bone_db = obj.data.bones.get(name) | |
| if bone_db: | |
| props = {} | |
| for k, v in bone_db.items(): | |
| if k == "_RNA_UI": | |
| continue | |
| props[k] = v | |
| if props: | |
| bone_data["props"] = props | |
| data[name] = bone_data | |
| # write json | |
| path = get_json_path() | |
| with open(path, "w") as f: | |
| json.dump(data, f, indent=4) | |
| self.report({"INFO"}, f"Saved {len(data)} bones to {path}") | |
| return {"FINISHED"} | |
| finally: | |
| # restore original mode | |
| try: | |
| bpy.ops.object.mode_set(mode=original_mode) | |
| except Exception: | |
| # best-effort restore; ignore if it fails | |
| pass | |
| # ---------- Match Operator ---------- | |
| class ARMATURE_OT_MatchBones(bpy.types.Operator): | |
| """Force overwrite bones with data from bones2.json if names match""" | |
| bl_idname = "armature.match_bones" | |
| bl_label = "Match Armature (force overwrite)" | |
| def execute(self, context): | |
| obj = context.object | |
| if not obj or obj.type != "ARMATURE": | |
| self.report({"ERROR"}, "Select an armature") | |
| return {"CANCELLED"} | |
| path = get_json_path() | |
| if not os.path.exists(path): | |
| self.report({"ERROR"}, "bones2.json not found") | |
| return {"CANCELLED"} | |
| with open(path, "r") as f: | |
| try: | |
| data = json.load(f) | |
| except Exception as e: | |
| self.report({"ERROR"}, f"Failed to parse JSON: {e}") | |
| return {"CANCELLED"} | |
| context.view_layer.objects.active = obj | |
| original_mode = obj.mode | |
| matched = 0 | |
| try: | |
| if original_mode != 'EDIT': | |
| bpy.ops.object.mode_set(mode='EDIT') | |
| # For each edit bone, if we have saved data for its name, overwrite transforms | |
| for eb in obj.data.edit_bones: | |
| name = eb.name | |
| if name in data: | |
| bdata = data[name] | |
| # force overwrite transforms | |
| try: | |
| eb.head = bdata.get("head", eb.head) | |
| eb.tail = bdata.get("tail", eb.tail) | |
| eb.roll = bdata.get("roll", eb.roll) | |
| except Exception as e: | |
| # in case types/length mismatch, skip transform set for that bone | |
| self.report({"WARNING"}, f"Could not set transform on bone '{name}': {e}") | |
| # force overwrite custom properties on the Bone data-block | |
| bone_db = obj.data.bones.get(name) | |
| if bone_db is not None: | |
| # clear existing custom props except _RNA_UI | |
| for key in list(bone_db.keys()): | |
| if key != "_RNA_UI": | |
| try: | |
| del bone_db[key] | |
| except Exception: | |
| pass | |
| # set saved props | |
| for k, v in bdata.get("props", {}).items(): | |
| try: | |
| bone_db[k] = v | |
| except Exception: | |
| pass | |
| matched += 1 | |
| self.report({"INFO"}, f"Force-matched {matched} bones from JSON") | |
| return {"FINISHED"} | |
| finally: | |
| try: | |
| bpy.ops.object.mode_set(mode=original_mode) | |
| except Exception: | |
| pass | |
| # ---------- UI Panel ---------- | |
| class ARMATURE_PT_BoneUtility(bpy.types.Panel): | |
| bl_label = "Armature Cache SAVE/Restore" | |
| bl_idname = "ARMATURE_PT_bone_utility" | |
| bl_space_type = "VIEW_3D" | |
| bl_region_type = "UI" | |
| bl_category = "Armature" | |
| def draw(self, context): | |
| layout = self.layout | |
| layout.operator("armature.save_bones", icon="EXPORT") | |
| layout.operator("armature.match_bones", icon="IMPORT") | |
| # ---------- Registration ---------- | |
| classes = ( | |
| ARMATURE_OT_SaveBones, | |
| ARMATURE_OT_MatchBones, | |
| ARMATURE_PT_BoneUtility, | |
| ) | |
| def register(): | |
| for c in classes: | |
| bpy.utils.register_class(c) | |
| def unregister(): | |
| for c in reversed(classes): | |
| bpy.utils.unregister_class(c) | |
| if __name__ == "__main__": | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment