Skip to content

Instantly share code, notes, and snippets.

@RH2
Created September 21, 2025 08:19
Show Gist options
  • Select an option

  • Save RH2/c53fc696a26ac62c13543f5f109847e5 to your computer and use it in GitHub Desktop.

Select an option

Save RH2/c53fc696a26ac62c13543f5f109847e5 to your computer and use it in GitHub Desktop.
json armature cache (restore head/tail/roll)
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