151 lines
8.5 KiB
Python
151 lines
8.5 KiB
Python
"""Generate pinned Blender 5.2 PLY mapping and unknown-property fixtures for M12-07H."""
|
|
|
|
import hashlib
|
|
import json
|
|
import struct
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import bpy
|
|
|
|
|
|
SCALAR = {"char": "b", "uchar": "B", "short": "h", "ushort": "H", "int": "i", "uint": "I", "float": "f", "double": "d"}
|
|
|
|
|
|
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_scene():
|
|
bpy.ops.wm.read_factory_settings(use_empty=True)
|
|
mesh = bpy.data.meshes.new("M12 PLY Mapping Mesh")
|
|
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), (0, 2, 3)])
|
|
mesh.update()
|
|
colors = mesh.color_attributes.new(name="Col", type="FLOAT_COLOR", domain="POINT")
|
|
for value, color in zip(colors.data, ((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))):
|
|
value.color = color
|
|
temperature = mesh.attributes.new(name="temperature", type="FLOAT", domain="POINT")
|
|
label = mesh.attributes.new(name="label", type="INT", domain="POINT")
|
|
for index, (temp, tag) in enumerate(zip((10.0, 20.0, 30.0, 40.0), (1, 2, 3, 4))):
|
|
temperature.data[index].value = temp
|
|
label.data[index].value = tag
|
|
obj = bpy.data.objects.new("M12 PLY Mapping", mesh)
|
|
bpy.context.scene.collection.objects.link(obj)
|
|
obj.select_set(True)
|
|
bpy.context.view_layer.objects.active = obj
|
|
|
|
|
|
def export_ply(path, ascii_format):
|
|
result = bpy.ops.wm.ply_export(filepath=str(path), ascii_format=ascii_format, export_selected_objects=True, export_uv=False, export_normals=True, export_colors="SRGB", export_attributes=True, export_triangulated_mesh=True, forward_axis="NEGATIVE_Z", up_axis="Y", global_scale=1.0)
|
|
if "FINISHED" not in result:
|
|
raise RuntimeError("Blender PLY export did not finish: %s" % (result,))
|
|
|
|
|
|
def parse_header(payload):
|
|
marker = b"end_header\n"
|
|
end = payload.find(marker)
|
|
if end < 0:
|
|
raise RuntimeError("PLY header missing end_header")
|
|
header = payload[: end + len(marker)].decode("ascii")
|
|
elements = []
|
|
current = None
|
|
for line in header.splitlines():
|
|
parts = line.split()
|
|
if not parts:
|
|
continue
|
|
if parts[0] == "format":
|
|
fmt = parts[1]
|
|
elif parts[0] == "element":
|
|
current = {"name": parts[1], "count": int(parts[2]), "properties": []}
|
|
elements.append(current)
|
|
elif parts[0] == "property" and current is not None:
|
|
if parts[1] == "list": current["properties"].append({"kind": "list", "countType": parts[2], "valueType": parts[3], "name": parts[4]})
|
|
else: current["properties"].append({"kind": "scalar", "type": parts[1], "name": parts[2]})
|
|
return fmt, elements, end + len(marker)
|
|
|
|
|
|
def read_value(data, cursor, value_type):
|
|
code = SCALAR[value_type]
|
|
size = struct.calcsize("<" + code)
|
|
value = struct.unpack_from("<" + code, data, cursor)[0]
|
|
return value, cursor + size
|
|
|
|
|
|
def parse_semantic(path):
|
|
payload = path.read_bytes()
|
|
fmt, elements, offset = parse_header(payload)
|
|
records = {}
|
|
if fmt == "ascii":
|
|
lines = payload[offset:].decode("utf-8").splitlines()
|
|
cursor = 0
|
|
for element in elements:
|
|
rows = []
|
|
for _ in range(element["count"]):
|
|
tokens = lines[cursor].split(); cursor += 1; token_index = 0; row = {}
|
|
for prop in element["properties"]:
|
|
if prop["kind"] == "scalar": row[prop["name"]] = float(tokens[token_index]) if prop["type"] in ("float", "double") else int(tokens[token_index]); token_index += 1
|
|
else:
|
|
length = int(tokens[token_index]); token_index += 1; row[prop["name"]] = [int(tokens[token_index + i]) for i in range(length)]; token_index += length
|
|
rows.append(row)
|
|
records[element["name"]] = rows
|
|
else:
|
|
cursor = offset
|
|
for element in elements:
|
|
rows = []
|
|
for _ in range(element["count"]):
|
|
row = {}
|
|
for prop in element["properties"]:
|
|
if prop["kind"] == "scalar": row[prop["name"]], cursor = read_value(payload, cursor, prop["type"])
|
|
else:
|
|
length, cursor = read_value(payload, cursor, prop["countType"]); row[prop["name"]] = []
|
|
for _ in range(length): value, cursor = read_value(payload, cursor, prop["valueType"]); row[prop["name"]].append(value)
|
|
rows.append(row)
|
|
records[element["name"]] = rows
|
|
vertices = []
|
|
for row in records["vertex"]:
|
|
vertices.append({"position": [row["x"], row["y"], row["z"]], "normal": [row["nx"], row["ny"], row["nz"]], "color": [row["red"], row["green"], row["blue"], row["alpha"]], "customProperties": {name: row[name] for name in row if name not in {"x", "y", "z", "nx", "ny", "nz", "red", "green", "blue", "alpha"}}})
|
|
faces = [{"indices": row["vertex_indices"], "customProperties": {name: row[name] for name in row if name != "vertex_indices"}} for row in records.get("face", [])]
|
|
return {"format": fmt, "vertexCount": len(vertices), "faceCount": len(faces), "vertices": vertices, "faces": faces}
|
|
|
|
|
|
def create_unknown_ascii(source, target):
|
|
lines = source.read_text(encoding="utf-8").splitlines()
|
|
header_end = lines.index("end_header")
|
|
property_index = next(index for index, line in enumerate(lines[:header_end]) if line == "property float label")
|
|
lines.insert(property_index + 1, "property list uchar float unknown_values")
|
|
header_end += 1
|
|
vertex_count = next(int(line.split()[2]) for line in lines[:header_end + 1] if line.startswith("element vertex "))
|
|
for index in range(header_end + 1, header_end + 1 + vertex_count):
|
|
lines[index] += " 1 0.5"
|
|
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)
|
|
create_scene()
|
|
ascii_path = output_dir / "mapping-ascii.ply"; binary_path = output_dir / "mapping-binary-le.ply"; unknown_path = output_dir / "unknown-property-ascii.ply"
|
|
export_ply(ascii_path, True); export_ply(binary_path, False); create_unknown_ascii(ascii_path, unknown_path)
|
|
files = [{"name": item.name, "byteLength": item.stat().st_size, "sha256": sha256_file(item)} for item in (ascii_path, binary_path, unknown_path)]
|
|
report = {"schemaVersion": 1, "task": "M12-07H", "operation": "PLY_VERTEX_FACE_COLOR_CUSTOM_MAPPING", "runtime": runtime_identity(), "sourceAnchor": "blender-5.2.0/source/blender/io/ply", "operator": "wm.ply_export", "settings": {"exportSelectedObjects": True, "exportNormals": True, "exportUV": False, "exportColors": "SRGB", "exportAttributes": True, "exportTriangulatedMesh": True, "forwardAxis": "NEGATIVE_Z", "upAxis": "Y", "globalScale": 1.0}, "variants": [{"id": "PLY_ASCII_MAPPING", "file": ascii_path.name, "semantic": parse_semantic(ascii_path)}, {"id": "PLY_BINARY_LITTLE_ENDIAN_MAPPING", "file": binary_path.name, "semantic": parse_semantic(binary_path)}], "unknownProperty": {"file": unknown_path.name, "property": "unknown_values", "expectedCode": "PLY_UNKNOWN_PROPERTY"}, "files": files, "nextTask": "M12-07I"}
|
|
report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
print("ply-mapping-fixtures-generated vertices=%s faces=%s colors=rgba custom=2 unknown=PLY_UNKNOWN_PROPERTY next=%s" % (report["variants"][0]["semantic"]["vertexCount"], report["variants"][0]["semantic"]["faceCount"], 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-ply-mapping-fixtures.py -- OUTPUT_DIR REPORT")
|
|
main(args[0], args[1])
|