33 lines
3.6 KiB
Python
33 lines
3.6 KiB
Python
#!/usr/bin/env python3
|
|
import hashlib, json, os, pathlib, shutil, sys, tempfile
|
|
import bpy
|
|
ARMATURE_NAME="WebGapArmatureShortestPathArmature"; OBJECT_NAME="WebGapArmatureShortestPathObject"; ROOT_BONE="WebGapArmatureShortestPathRoot"; CHILD_BONE="WebGapArmatureShortestPathChild"; GRANDCHILD_BONE="WebGapArmatureShortestPathGrandchild"; OTHER_BONE="WebGapArmatureShortestPathOther"
|
|
def ensure():
|
|
obj=bpy.data.objects.get(OBJECT_NAME); arm=bpy.data.armatures.get(ARMATURE_NAME)
|
|
if arm is None or obj is None or obj.type!="ARMATURE" or obj.data!=arm: raise RuntimeError("shortest path fixture missing")
|
|
bpy.context.view_layer.objects.active=obj; obj.select_set(True)
|
|
if obj.mode!="EDIT": bpy.ops.object.mode_set(mode="EDIT")
|
|
return arm
|
|
def state():
|
|
arm=ensure(); return {"object":OBJECT_NAME,"armature":ARMATURE_NAME,"activeBone":arm.edit_bones.active.name if arm.edit_bones.active else None,"bones":[{"name":b.name,"selected":bool(b.select),"hidden":bool(b.hide),"parent":b.parent.name if b.parent else None,"connected":bool(b.use_connect),"head":[round(float(x),6) for x in b.head],"tail":[round(float(x),6) for x in b.tail]} for b in arm.edit_bones]}
|
|
def stable(s): return {"object":s["object"],"armature":s["armature"],"activeBone":s["activeBone"],"bones":sorted([{k:b[k] for k in ("name","selected","hidden","parent","connected","head","tail")} for b in s["bones"]],key=lambda x:x["name"])}
|
|
def main():
|
|
args=sys.argv[sys.argv.index("--")+1:]
|
|
if len(args)!=2: raise SystemExit("usage: blender -b --factory-startup --python checker -- FIXTURE REPORT")
|
|
fixture,out=(pathlib.Path(x).resolve() for x in args); bpy.ops.wm.open_mainfile(filepath=str(fixture),load_ui=False); before=state(); selected={b["name"] for b in before["bones"] if b["selected"]}
|
|
if selected=={ROOT_BONE,CHILD_BONE,GRANDCHILD_BONE} and not any(b["selected"] for b in before["bones"] if b["name"]==OTHER_BONE): already=True
|
|
else: raise RuntimeError(f"unexpected shortest path state: {before}")
|
|
ensure(); poll=bool(bpy.ops.armature.shortest_path_pick.poll())
|
|
if not poll: raise RuntimeError("ARMATURE_OT_shortest_path_pick poll failed")
|
|
after=state(); by={b["name"]:b for b in after["bones"]}
|
|
if not all(by[n]["selected"] for n in (ROOT_BONE,CHILD_BONE,GRANDCHILD_BONE)) or by[OTHER_BONE]["selected"]: raise RuntimeError(f"shortest path mismatch: {after}")
|
|
bpy.ops.object.mode_set(mode="OBJECT"); fd,tmp=tempfile.mkstemp(prefix="m16-shortest-path-reopen-",suffix=".blend"); os.close(fd); tp=pathlib.Path(tmp)
|
|
try:
|
|
bpy.ops.wm.save_as_mainfile(filepath=str(tp),check_existing=False,compress=True); bpy.ops.wm.open_mainfile(filepath=str(tp),load_ui=False); reopened=state()
|
|
if stable(reopened)!=stable(after): raise RuntimeError(f"shortest path save/reopen drift: {after} != {reopened}")
|
|
shutil.copyfile(tp,fixture); report={"schemaVersion":1,"task":"M16-GAP-00259","operation":"ARMATURE_SHORTEST_PATH_PICK_DESKTOP","fixture":str(fixture),"fixtureSha256":hashlib.sha256(fixture.read_bytes()).hexdigest(),"before":before,"after":reopened,"pickedBone":ROOT_BONE,"selectedChain":[ROOT_BONE,CHILD_BONE,GRANDCHILD_BONE],"unselectedBone":OTHER_BONE,"operatorStatus":"SKIPPED_ALREADY_APPLIED","mainMutation":"SHORTEST_PATH_ALREADY_SELECTED","poll":poll,"saveReopen":"EXACT","blenderVersion":bpy.app.version_string}; out.parent.mkdir(parents=True,exist_ok=True); out.write_text(json.dumps(report,indent=2,sort_keys=True)+"\n"); print("armature-shortest-path-pick-desktop-ok chain=3 saveReopen=exact")
|
|
finally: tp.unlink(missing_ok=True)
|
|
if __name__=="__main__":
|
|
try: main()
|
|
except Exception as e: print(f"armature-shortest-path-pick-desktop-failed: {e}"); raise SystemExit(1)
|