182 lines
6.9 KiB
Python
182 lines
6.9 KiB
Python
import hashlib
|
|
import json
|
|
import pathlib
|
|
import struct
|
|
import sys
|
|
|
|
import bpy
|
|
|
|
|
|
ROOT_OBJECT = "M12 Append Object"
|
|
MESH_NAME = "M12 Append Mesh"
|
|
MATERIAL_NAME = "M12 Append Material"
|
|
IMAGE_NAME = "M12 Append Image"
|
|
IMAGE_NODE_NAME = "M12 Append Image Node"
|
|
|
|
|
|
def sha256_file(path: pathlib.Path) -> str:
|
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
|
|
|
|
def pixel_sha256(image: bpy.types.Image) -> str:
|
|
values = list(image.pixels)
|
|
return hashlib.sha256(struct.pack(f"<{len(values)}f", *values)).hexdigest()
|
|
|
|
|
|
def reset() -> None:
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
|
|
|
|
def create_source(path: pathlib.Path) -> None:
|
|
reset()
|
|
image = bpy.data.images.new(IMAGE_NAME, width=2, height=2, alpha=True, float_buffer=False)
|
|
image.colorspace_settings.name = "sRGB"
|
|
image.pixels = [
|
|
1.0, 0.0, 0.0, 1.0,
|
|
0.0, 1.0, 0.0, 1.0,
|
|
0.0, 0.0, 1.0, 1.0,
|
|
1.0, 1.0, 1.0, 0.5,
|
|
]
|
|
image.pack()
|
|
|
|
material = bpy.data.materials.new(MATERIAL_NAME)
|
|
material.use_nodes = True
|
|
node_tree = material.node_tree
|
|
principled = node_tree.nodes.get("Principled BSDF")
|
|
image_node = node_tree.nodes.new("ShaderNodeTexImage")
|
|
image_node.name = IMAGE_NODE_NAME
|
|
image_node.label = IMAGE_NODE_NAME
|
|
image_node.image = image
|
|
image_node.interpolation = "Closest"
|
|
image_node.extension = "REPEAT"
|
|
node_tree.links.new(image_node.outputs["Color"], principled.inputs["Base Color"])
|
|
|
|
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)
|
|
uv_layer = mesh.uv_layers.new(name="UVMap")
|
|
for loop, uv in zip(uv_layer.data, [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]):
|
|
loop.uv = uv
|
|
mesh.update()
|
|
|
|
obj = bpy.data.objects.new(ROOT_OBJECT, mesh)
|
|
obj["m12_source_marker"] = "M12-03C"
|
|
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:
|
|
return {
|
|
"idType": id_type,
|
|
"name": value.name,
|
|
"nameFull": value.name_full,
|
|
"library": None if value.library is None else value.library.filepath,
|
|
"isLibraryOverride": value.override_library is not None,
|
|
}
|
|
|
|
|
|
def inspect_local_graph() -> dict:
|
|
obj = bpy.data.objects[ROOT_OBJECT]
|
|
mesh = obj.data
|
|
material = mesh.materials[0]
|
|
image_node = material.node_tree.nodes[IMAGE_NODE_NAME]
|
|
image = image_node.image
|
|
ids = {
|
|
"OBJECT": data_block("OBJECT", obj),
|
|
"MESH": data_block("MESH", mesh),
|
|
"MATERIAL": data_block("MATERIAL", material),
|
|
"IMAGE": data_block("IMAGE", image),
|
|
}
|
|
return {
|
|
"root": ids["OBJECT"],
|
|
"ids": ids,
|
|
"edges": [
|
|
{"from": "Object/M12 Append Object", "relation": "OBJECT_DATA", "to": "Mesh/M12 Append Mesh"},
|
|
{"from": "Mesh/M12 Append Mesh", "relation": "MATERIAL_SLOT[0]", "to": "Material/M12 Append Material"},
|
|
{"from": "Material/M12 Append Material", "relation": f"NODE_IMAGE[{IMAGE_NODE_NAME}]", "to": "Image/M12 Append Image"},
|
|
],
|
|
"geometry": {
|
|
"vertices": len(mesh.vertices),
|
|
"edges": len(mesh.edges),
|
|
"polygons": len(mesh.polygons),
|
|
"loops": len(mesh.loops),
|
|
"uvLayers": [layer.name for layer in mesh.uv_layers],
|
|
"materialSlots": [item.name for item in mesh.materials],
|
|
},
|
|
"image": {
|
|
"size": list(image.size),
|
|
"channels": image.channels,
|
|
"colorspace": image.colorspace_settings.name,
|
|
"packed": image.packed_file is not None,
|
|
"pixelFloat32Sha256": pixel_sha256(image),
|
|
},
|
|
"sourceMarker": obj["m12_source_marker"],
|
|
}
|
|
|
|
|
|
def append_object(source: pathlib.Path, target: pathlib.Path) -> dict:
|
|
reset()
|
|
with bpy.data.libraries.load(str(source), link=False) 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 append did not return one object")
|
|
bpy.context.scene.collection.objects.link(data_to.objects[0])
|
|
before_save = inspect_local_graph()
|
|
if any(item["library"] is not None or item["isLibraryOverride"] for item in before_save["ids"].values()):
|
|
raise RuntimeError("appended dependency closure is not fully local")
|
|
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_local_graph()
|
|
if reopened != before_save:
|
|
raise RuntimeError("appended dependency mapping 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-append-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_append_source.blend"
|
|
target = output_dir / "m12_append_target.blend"
|
|
|
|
create_source(source)
|
|
source_graph = inspect_local_graph()
|
|
appended_graph = append_object(source, target)
|
|
report = {
|
|
"schemaVersion": 1,
|
|
"task": "M12-03C",
|
|
"operation": "APPEND",
|
|
"blenderVersion": "5.2.0",
|
|
"source": {"file": source.name, "sha256": sha256_file(source)},
|
|
"target": {"file": target.name, "sha256": sha256_file(target)},
|
|
"selectedRoots": ["Object/M12 Append Object"],
|
|
"sourceGraph": source_graph,
|
|
"appendedGraph": appended_graph,
|
|
"stableMapping": [
|
|
{"source": "Object/M12 Append Object", "local": "Object/M12 Append Object", "owner": "LOCAL_MAIN", "readOnly": False},
|
|
{"source": "Mesh/M12 Append Mesh", "local": "Mesh/M12 Append Mesh", "owner": "LOCAL_MAIN", "readOnly": False},
|
|
{"source": "Material/M12 Append Material", "local": "Material/M12 Append Material", "owner": "LOCAL_MAIN", "readOnly": False},
|
|
{"source": "Image/M12 Append Image", "local": "Image/M12 Append Image", "owner": "LOCAL_MAIN", "readOnly": False},
|
|
],
|
|
"nextTask": "M12-03D",
|
|
}
|
|
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print(
|
|
f"library-append-fixture-ok roots={len(report['selectedRoots'])} "
|
|
f"mapping={len(report['stableMapping'])} source={report['source']['sha256']} "
|
|
f"target={report['target']['sha256']} next={report['nextTask']}"
|
|
)
|
|
|
|
|
|
main()
|