Checkpoint web parity through Chromium input tasks
This commit is contained in:
285
tools/web/generate-obj-multi-negative-fixtures.py
Normal file
285
tools/web/generate-obj-multi-negative-fixtures.py
Normal file
@@ -0,0 +1,285 @@
|
||||
"""Generate the pinned Blender 5.2 OBJ multi-object and negative fixtures for M12-07B."""
|
||||
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import bpy
|
||||
|
||||
|
||||
OBJECTS = ("M12 OBJ Left", "M12 OBJ Right")
|
||||
MATERIALS = ("M12 OBJ Left Material", "M12 OBJ Right Material")
|
||||
TEXTURE_NAME = "m12_obj_texture.png"
|
||||
|
||||
|
||||
def sha256_file(path):
|
||||
digest = hashlib.sha256()
|
||||
with open(path, "rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def text(value):
|
||||
return value.decode("utf-8") if isinstance(value, bytes) else value
|
||||
|
||||
|
||||
def runtime_identity():
|
||||
binary = Path(bpy.app.binary_path)
|
||||
return {
|
||||
"blenderVersion": text(bpy.app.version_string),
|
||||
"versionTuple": list(bpy.app.version),
|
||||
"buildDate": text(bpy.app.build_date),
|
||||
"buildTime": text(bpy.app.build_time),
|
||||
"buildHash": text(bpy.app.build_hash),
|
||||
"buildBranch": text(bpy.app.build_branch),
|
||||
"buildPlatform": text(bpy.app.build_platform),
|
||||
"buildType": text(bpy.app.build_type),
|
||||
"binarySha256": sha256_file(binary),
|
||||
}
|
||||
|
||||
|
||||
def create_texture(path):
|
||||
image = bpy.data.images.new("M12 OBJ Texture", width=2, height=2, alpha=True, float_buffer=False)
|
||||
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, 0.0, 1.0,
|
||||
]
|
||||
image.filepath_raw = str(path)
|
||||
image.file_format = "PNG"
|
||||
image.save()
|
||||
return image
|
||||
|
||||
|
||||
def material(name, image):
|
||||
value = bpy.data.materials.new(name)
|
||||
value.use_nodes = True
|
||||
nodes = value.node_tree.nodes
|
||||
links = value.node_tree.links
|
||||
principled = nodes.get("Principled BSDF")
|
||||
texture = nodes.new("ShaderNodeTexImage")
|
||||
texture.image = image
|
||||
links.new(texture.outputs["Color"], principled.inputs["Base Color"])
|
||||
return value
|
||||
|
||||
|
||||
def mesh_object(name, offset, material_value):
|
||||
mesh = bpy.data.meshes.new(name + " Mesh")
|
||||
mesh.from_pydata(
|
||||
[(offset - 0.75, -0.75, 0.0), (offset + 0.75, -0.75, 0.0), (offset + 0.0, 0.75, 0.0)],
|
||||
[],
|
||||
[(0, 1, 2)],
|
||||
)
|
||||
mesh.update()
|
||||
uv = mesh.uv_layers.new(name="UVMap")
|
||||
for loop, value in zip(mesh.loops, ((0.0, 0.0), (1.0, 0.0), (0.5, 1.0))):
|
||||
uv.data[loop.index].uv = value
|
||||
mesh.materials.append(material_value)
|
||||
obj = bpy.data.objects.new(name, mesh)
|
||||
bpy.context.scene.collection.objects.link(obj)
|
||||
return obj
|
||||
|
||||
|
||||
def create_scene(output_dir):
|
||||
bpy.ops.wm.read_factory_settings(use_empty=True)
|
||||
image = create_texture(output_dir / TEXTURE_NAME)
|
||||
left_material = material(MATERIALS[0], image)
|
||||
right_material = material(MATERIALS[1], image)
|
||||
left = mesh_object(OBJECTS[0], -1.0, left_material)
|
||||
right = mesh_object(OBJECTS[1], 1.0, right_material)
|
||||
for obj in (left, right):
|
||||
obj.select_set(True)
|
||||
for polygon in obj.data.polygons:
|
||||
polygon.use_smooth = False
|
||||
bpy.context.view_layer.objects.active = left
|
||||
return left, right
|
||||
|
||||
|
||||
def export_obj(output_path):
|
||||
result = bpy.ops.wm.obj_export(
|
||||
filepath=str(output_path),
|
||||
export_selected_objects=True,
|
||||
apply_modifiers=False,
|
||||
apply_transform=False,
|
||||
export_eval_mode="DAG_EVAL_VIEWPORT",
|
||||
export_uv=True,
|
||||
export_normals=True,
|
||||
export_colors=False,
|
||||
export_materials=True,
|
||||
export_pbr_extensions=False,
|
||||
export_material_groups=True,
|
||||
export_object_groups=True,
|
||||
export_vertex_groups=False,
|
||||
export_smooth_groups=False,
|
||||
export_triangulated_mesh=False,
|
||||
export_curves_as_nurbs=False,
|
||||
global_scale=1.0,
|
||||
forward_axis="NEGATIVE_Z",
|
||||
up_axis="Y",
|
||||
path_mode="RELATIVE",
|
||||
)
|
||||
if "FINISHED" not in result:
|
||||
raise RuntimeError("Blender OBJ export did not finish: %s" % (result,))
|
||||
|
||||
|
||||
def parse_obj(path):
|
||||
positions = []
|
||||
texcoords = []
|
||||
normals = []
|
||||
faces = []
|
||||
material_libraries = []
|
||||
objects = []
|
||||
groups = []
|
||||
current_object = None
|
||||
current_material = None
|
||||
current_groups = []
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split()
|
||||
kind = parts[0]
|
||||
if kind == "v":
|
||||
positions.append([float(value) for value in parts[1:4]])
|
||||
elif kind == "vt":
|
||||
texcoords.append([float(value) for value in parts[1:3]])
|
||||
elif kind == "vn":
|
||||
normals.append([float(value) for value in parts[1:4]])
|
||||
elif kind == "mtllib":
|
||||
material_libraries.extend(parts[1:])
|
||||
elif kind == "o":
|
||||
current_object = " ".join(parts[1:])
|
||||
objects.append(current_object)
|
||||
elif kind == "g":
|
||||
current_groups = parts[1:]
|
||||
for group in current_groups:
|
||||
if group not in groups:
|
||||
groups.append(group)
|
||||
if group.endswith("_Mesh") and group not in objects:
|
||||
current_object = group
|
||||
objects.append(group)
|
||||
elif kind == "usemtl":
|
||||
current_material = " ".join(parts[1:])
|
||||
elif kind == "f":
|
||||
vertices = []
|
||||
for token in parts[1:]:
|
||||
indices = token.split("/")
|
||||
vertices.append({
|
||||
"position": int(indices[0]),
|
||||
"texcoord": int(indices[1]) if len(indices) > 1 and indices[1] else None,
|
||||
"normal": int(indices[2]) if len(indices) > 2 and indices[2] else None,
|
||||
})
|
||||
faces.append({"object": current_object, "groups": list(current_groups), "material": current_material, "vertices": vertices})
|
||||
return {
|
||||
"materialLibraries": material_libraries,
|
||||
"objects": objects,
|
||||
"groups": groups,
|
||||
"positions": positions,
|
||||
"texcoords": texcoords,
|
||||
"normals": normals,
|
||||
"faces": faces,
|
||||
}
|
||||
|
||||
|
||||
def parse_mtl(path):
|
||||
materials = []
|
||||
current = None
|
||||
for raw_line in path.read_text(encoding="utf-8").splitlines():
|
||||
line = raw_line.strip()
|
||||
if not line or line.startswith("#"):
|
||||
continue
|
||||
parts = line.split()
|
||||
if parts[0] == "newmtl":
|
||||
current = {"name": " ".join(parts[1:]), "mapKd": None}
|
||||
materials.append(current)
|
||||
elif current is not None and parts[0] == "map_Kd":
|
||||
current["mapKd"] = " ".join(parts[1:])
|
||||
return materials
|
||||
|
||||
|
||||
def negative_index_obj(source, target):
|
||||
lines = []
|
||||
for raw_line in source.read_text(encoding="utf-8").splitlines():
|
||||
if not raw_line.startswith("f "):
|
||||
lines.append(raw_line)
|
||||
continue
|
||||
converted = []
|
||||
for token in raw_line.split()[1:]:
|
||||
position, texcoord, normal = token.split("/")
|
||||
converted.append("%d/%d/%d" % (-int(position), -int(texcoord), -int(normal)))
|
||||
lines.append("f " + " ".join(converted))
|
||||
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def malformed_obj(source, target):
|
||||
lines = []
|
||||
replaced = False
|
||||
for raw_line in source.read_text(encoding="utf-8").splitlines():
|
||||
if raw_line.startswith("f ") and not replaced:
|
||||
lines.append("f 1/1/1 2/2/1")
|
||||
replaced = True
|
||||
else:
|
||||
lines.append(raw_line)
|
||||
target.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def main(output_dir, report_path):
|
||||
output_dir = Path(output_dir).resolve()
|
||||
report_path = Path(report_path).resolve()
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
obj_path = output_dir / "multi-object.obj"
|
||||
create_scene(output_dir)
|
||||
export_obj(obj_path)
|
||||
mtl_path = obj_path.with_suffix(".mtl")
|
||||
if not mtl_path.exists():
|
||||
raise RuntimeError("Blender OBJ export did not write the MTL sidecar")
|
||||
negative_path = output_dir / "negative-index.obj"
|
||||
malformed_path = output_dir / "malformed-face.obj"
|
||||
negative_index_obj(obj_path, negative_path)
|
||||
malformed_obj(obj_path, malformed_path)
|
||||
semantic = parse_obj(obj_path)
|
||||
semantic["materials"] = parse_mtl(mtl_path)
|
||||
files = []
|
||||
for name in (obj_path.name, mtl_path.name, TEXTURE_NAME, negative_path.name, malformed_path.name):
|
||||
item = output_dir / name
|
||||
files.append({"name": name, "byteLength": item.stat().st_size, "sha256": sha256_file(item)})
|
||||
report = {
|
||||
"schemaVersion": 1,
|
||||
"task": "M12-07B",
|
||||
"operation": "DESKTOP_OBJ_MULTI_OBJECT_AND_NEGATIVE_FIXTURES",
|
||||
"runtime": runtime_identity(),
|
||||
"sourceAnchor": "blender-5.2.0/source/blender/io/wavefront_obj",
|
||||
"operator": "wm.obj_export",
|
||||
"settings": {
|
||||
"forwardAxis": "NEGATIVE_Z",
|
||||
"upAxis": "Y",
|
||||
"globalScale": 1.0,
|
||||
"exportUV": True,
|
||||
"exportNormals": True,
|
||||
"exportMaterials": True,
|
||||
"exportMaterialGroups": True,
|
||||
"exportObjectGroups": True,
|
||||
"pathMode": "RELATIVE",
|
||||
},
|
||||
"files": files,
|
||||
"semantic": semantic,
|
||||
"negativeIndex": {"file": negative_path.name, "expectedStatus": "ACCEPT_WITH_NEGATIVE_INDICES", "faceCount": 2},
|
||||
"malformedFace": {"file": malformed_path.name, "expectedCode": "OBJ_FACE_ARITY_INVALID"},
|
||||
"textureOrigin": {"mtlMapKd": [material["mapKd"] for material in semantic["materials"]], "relative": True, "textureFile": TEXTURE_NAME},
|
||||
"nextTask": "M12-07C",
|
||||
}
|
||||
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
||||
print("obj-multi-negative-fixtures-generated objects=%s faces=%s negative=true malformed=true next=%s" % (len(semantic["objects"]), len(semantic["faces"]), report["nextTask"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[sys.argv.index("--") + 1 :] if "--" in sys.argv else []
|
||||
if len(args) != 2:
|
||||
raise SystemExit("usage: blender --background --python generate-obj-multi-negative-fixtures.py -- OUTPUT_DIR REPORT")
|
||||
main(args[0], args[1])
|
||||
Reference in New Issue
Block a user