#!/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)