Checkpoint web parity through Chromium input tasks
This commit is contained in:
183
tools/web/generate-library-link-fixture.py
Normal file
183
tools/web/generate-library-link-fixture.py
Normal file
@@ -0,0 +1,183 @@
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import struct
|
||||
import sys
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
ROOT_OBJECT = "M12 Link Object"
|
||||
MESH_NAME = "M12 Link Mesh"
|
||||
MATERIAL_NAME = "M12 Link Material"
|
||||
IMAGE_NAME = "M12 Link Image"
|
||||
IMAGE_NODE_NAME = "M12 Link Image Node"
|
||||
SOURCE_MARKER = "M12-03F"
|
||||
|
||||
|
||||
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"] = SOURCE_MARKER
|
||||
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 inspect_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": f"Object/{ROOT_OBJECT}", "relation": "OBJECT_DATA", "to": f"Mesh/{MESH_NAME}"},
|
||||
{"from": f"Mesh/{MESH_NAME}", "relation": "MATERIAL_SLOT[0]", "to": f"Material/{MATERIAL_NAME}"},
|
||||
{"from": f"Material/{MATERIAL_NAME}", "relation": f"NODE_IMAGE[{IMAGE_NODE_NAME}]", "to": f"Image/{IMAGE_NAME}"},
|
||||
],
|
||||
"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 link_object(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")
|
||||
bpy.context.scene.collection.objects.link(data_to.objects[0])
|
||||
before_save = inspect_graph()
|
||||
if any(item["library"] is None or item["isLibraryOverride"] for item in before_save["ids"].values()):
|
||||
raise RuntimeError("linked dependency closure did not retain the source library")
|
||||
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_graph()
|
||||
if reopened != before_save:
|
||||
raise RuntimeError("linked 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-link-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_link_source.blend"
|
||||
target = output_dir / "m12_link_target.blend"
|
||||
|
||||
create_source(source)
|
||||
source_graph = inspect_graph()
|
||||
linked_graph = link_object(source, target)
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-03F",
|
||||
"operation": "LINK",
|
||||
"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}"],
|
||||
"sourceGraph": source_graph,
|
||||
"linkedGraph": linked_graph,
|
||||
"stableMapping": [
|
||||
{"source": f"Object/{ROOT_OBJECT}", "local": f"Object/{ROOT_OBJECT}", "owner": "SOURCE_LIBRARY", "readOnly": True},
|
||||
{"source": f"Mesh/{MESH_NAME}", "local": f"Mesh/{MESH_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True},
|
||||
{"source": f"Material/{MATERIAL_NAME}", "local": f"Material/{MATERIAL_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True},
|
||||
{"source": f"Image/{IMAGE_NAME}", "local": f"Image/{IMAGE_NAME}", "owner": "SOURCE_LIBRARY", "readOnly": True},
|
||||
],
|
||||
"nextTask": "M12-03G",
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print(
|
||||
f"library-link-fixture-ok roots={len(report['selectedRoots'])} "
|
||||
f"mapping={len(report['stableMapping'])} source={report['source']['sha256']} "
|
||||
f"target={report['target']['sha256']} next={report['nextTask']}"
|
||||
)
|
||||
|
||||
|
||||
main()
|
||||
Reference in New Issue
Block a user