import hashlib import json import pathlib import sys import bpy ROOT_OBJECT = "M12 Override Object" MESH_NAME = "M12 Override Mesh" MATERIAL_NAME = "M12 Override Material" SOURCE_MARKER = "M12-03J" PROJECT_ID = "m12-03j-project" OVERRIDE_X = 2.5 OVERRIDE_PROPERTY_PATH = '["m12_override_value"]' def sha256_file(path: pathlib.Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() def reset() -> None: bpy.ops.wm.read_factory_settings(use_empty=True) def create_source(path: pathlib.Path) -> None: reset() material = bpy.data.materials.new(MATERIAL_NAME) material.diffuse_color = (0.15, 0.65, 0.35, 1.0) mesh = bpy.data.meshes.new(MESH_NAME) mesh.from_pydata([(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], [], [(0, 1, 2, 3)]) mesh.materials.append(material) obj = bpy.data.objects.new(ROOT_OBJECT, mesh) obj["m12_source_marker"] = SOURCE_MARKER obj["m12_override_value"] = 1.0 bpy.context.scene.collection.objects.link(obj) bpy.ops.wm.save_as_mainfile(filepath=str(path), check_existing=False) def data_block(id_type: str, value: object) -> dict: library = value.library return { "idType": id_type, "name": value.name, "nameFull": value.name_full, "library": None if library is None else pathlib.Path(library.filepath).name, "isLibraryOverride": value.override_library is not None, } def override_property(value: object) -> dict: override = value.override_library if override is None: raise RuntimeError("override library metadata is missing") properties = list(override.properties) matches = [item for item in properties if item.rna_path == OVERRIDE_PROPERTY_PATH] if not matches: print("DEBUG_OVERRIDE_PROPERTIES", [(item.rna_path, len(item.operations)) for item in properties], dir(override.properties)) raise RuntimeError("custom property override path is missing") return { "rnaPath": OVERRIDE_PROPERTY_PATH, "index": 0, "value": float(value["m12_override_value"]), "operationCount": sum(len(item.operations) for item in matches), "propertyCount": len(properties), } def inspect_override() -> dict: obj = bpy.data.objects[ROOT_OBJECT] override = obj.override_library if override is None or override.reference is None: raise RuntimeError("desktop override reference is missing") reference = override.reference if reference.library is None: raise RuntimeError("override reference did not retain source library") if obj.library is not None: raise RuntimeError("override object must be locally owned") return { "reference": { "dataBlockId": f"Object/{reference.name}", "idType": "OBJECT", "library": pathlib.Path(reference.library.filepath).name, "owner": "SOURCE_LIBRARY", "readOnly": True, "isLibraryOverride": False, }, "local": { "dataBlockId": f"Object/{obj.name}", "idType": "OBJECT", "library": None, "owner": "LOCAL_OVERRIDE", "projectId": PROJECT_ID, "readOnly": False, "referenceSourceDataBlockId": f"Object/{reference.name}", "hierarchyRootDataBlockId": f"Object/{obj.name}", "isLibraryOverride": True, }, "propertyOverride": override_property(obj), "sourceMarker": reference.get("m12_source_marker"), } def create_override(source: pathlib.Path, target: pathlib.Path) -> dict: reset() with bpy.data.libraries.load(str(source), link=True) as (data_from, data_to): if ROOT_OBJECT not in data_from.objects: raise RuntimeError("source root object is missing") data_to.objects = [ROOT_OBJECT] if len(data_to.objects) != 1 or data_to.objects[0] is None: raise RuntimeError("desktop link did not return one object") linked = data_to.objects[0] bpy.context.scene.collection.objects.link(linked) bpy.context.view_layer.objects.active = linked linked.select_set(True) if bpy.ops.object.make_override_library() != {"FINISHED"}: raise RuntimeError("desktop override operator did not finish") obj = bpy.data.objects[ROOT_OBJECT] obj["m12_override_value"] = OVERRIDE_X prop = obj.override_library.properties.add(OVERRIDE_PROPERTY_PATH) prop.operations.add("REPLACE") before_save = inspect_override() if before_save["propertyOverride"]["value"] != OVERRIDE_X: raise RuntimeError("override property did not apply") bpy.ops.wm.save_as_mainfile(filepath=str(target), check_existing=False) bpy.ops.wm.open_mainfile(filepath=str(target), load_ui=False) reopened = inspect_override() if reopened != before_save: raise RuntimeError("desktop override metadata drifted after save/reopen") return reopened def main() -> None: if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2: raise SystemExit("usage: blender --background --factory-startup --python generate-library-override-fixture.py -- OUTPUT_DIR REPORT") output_arg, report_arg = sys.argv[sys.argv.index("--") + 1 :] output_dir = pathlib.Path(output_arg).resolve() report_path = pathlib.Path(report_arg).resolve() output_dir.mkdir(parents=True, exist_ok=True) report_path.parent.mkdir(parents=True, exist_ok=True) source = output_dir / "m12_override_source.blend" target = output_dir / "m12_override_target.blend" create_source(source) override_graph = create_override(source, target) report = { "schemaVersion": 1, "task": "M12-03J", "operation": "LIBRARY_OVERRIDE", "blenderVersion": "5.2.0", "source": {"file": source.name, "sha256": sha256_file(source)}, "target": {"file": target.name, "sha256": sha256_file(target)}, "selectedRoots": [f"Object/{ROOT_OBJECT}"], "overrideGraph": override_graph, "nextTask": "M12-03K", } report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") print( f"library-override-fixture-ok roots={len(report['selectedRoots'])} " f"reference={override_graph['reference']['library']} owner={override_graph['local']['owner']} " f"path={override_graph['propertyOverride']['rnaPath']} next={report['nextTask']}" ) main()