Files
workinf_Blender_Wasm/tools/web/generate-b6-composite-fixture.py
mes123456 f3e21c6b98
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
chore: checkpoint web Blender migration source, tools and docs
Accumulated uncommitted work: GHOST Web backend, WebGL/GPU web layer,
web workflow engine and protocol, tooling, docs and task records.
Large generated evidence, user-testing archives, build snapshots and
imported test assets remain untracked.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-10 11:18:01 +08:00

87 lines
6.9 KiB
Python

"""Prepare B6 inputs with native Blender operators; no acceptance checks."""
import bpy, json, sys, hashlib, xml.etree.ElementTree as ET
from pathlib import Path
from mathutils import Matrix, Euler, Vector
kind, source, destination, asset_root = sys.argv[sys.argv.index("--") + 1:]
source, destination, asset_root = map(lambda p: Path(p).resolve(), (source, destination, asset_root))
bpy.ops.wm.open_mainfile(filepath=str(source))
scene = bpy.context.scene
if kind not in {"minimal", "production"}: raise ValueError("Unsupported B6 fixture kind")
def digest(path): return hashlib.sha256(path.read_bytes()).hexdigest()
source_bindings = {str(source): digest(source)}
def pose(element):
xyz = tuple(map(float, element.get("xyz", "0 0 0").split())) if element is not None else (0, 0, 0)
rpy = tuple(map(float, element.get("rpy", "0 0 0").split())) if element is not None else (0, 0, 0)
return Matrix.Translation(Vector(xyz)) @ Euler(rpy, "XYZ").to_matrix().to_4x4()
if kind == "production":
provenance = asset_root / "provenance.json"
expected = {item["path"]: item["sha256"] for item in json.loads(provenance.read_text())["files"]}
def asset(name):
path = asset_root / name; actual = digest(path)
if expected.get(name) != actual: raise RuntimeError(f"Asset provenance mismatch: {name}")
source_bindings[str(path)] = actual
return path
source_bindings[str(provenance)] = digest(provenance)
robot = ET.parse(asset("abb_irb120.urdf")).getroot()
visuals = [(link.get("name"), visual, asset(visual.find("geometry/mesh").get("filename").removeprefix("package://"))) for link in robot.findall("link") for visual in link.findall("visual")]
for obj in list(scene.objects):
if obj.type == "MESH": bpy.data.objects.remove(obj, do_unlink=True)
links = {}
for link in robot.findall("link"):
name = link.get("name"); obj = bpy.data.objects.new("ABB_" + name, None)
scene.collection.objects.link(obj); links[name] = obj
for joint in robot.findall("joint"):
child = links[joint.find("child").get("link")]; child.parent = links[joint.find("parent").get("link")]
child.matrix_basis = pose(joint.find("origin"))
for name, visual, mesh_path in visuals:
bpy.ops.object.select_all(action="DESELECT")
result = bpy.ops.wm.stl_import(filepath=str(mesh_path))
if result != {"FINISHED"}: raise RuntimeError(f"Native STL import failed: {mesh_path}")
obj = bpy.context.active_object; obj.name = "ABB_" + name + "_mesh"
obj.parent = links[name]; obj.matrix_basis = pose(visual.find("origin"))
obj.scale = tuple(map(float, visual.find("geometry/mesh").get("scale", "1 1 1").split()))
rgba = tuple(map(float, visual.find("material/color").get("rgba").split()))
material = bpy.data.materials.new("ABB_" + name); material.diffuse_color = rgba
material.use_nodes = True; material.node_tree.nodes.get("Principled BSDF").inputs["Base Color"].default_value = rgba
obj.data.materials.append(material)
target = bpy.data.objects["ABB_link_6_mesh"]; root_name = links["base_link"].name
else:
target = bpy.data.objects["BasicCube"]; root = bpy.data.objects.new("B6_Root", None)
scene.collection.objects.link(root); target.parent = root; root_name = root.name
scene.frame_start = 1; scene.frame_end = 24
target.animation_data_clear(); base_location = target.location.copy()
for frame, delta in [(1, 0.0), (24, 0.05)]:
scene.frame_set(frame); target.location = base_location + Vector((delta, 0, 0))
target.keyframe_insert(data_path="location", frame=frame)
action = target.animation_data.action; action.name = "B6_" + kind + "_Motion"; action.use_fake_user = True
for layer in action.layers:
for strip in layer.strips:
for bag in strip.channelbags:
for curve in bag.fcurves:
for key in curve.keyframe_points: key.interpolation = "LINEAR"
scene.frame_set(1); bpy.ops.object.select_all(action="DESELECT")
target.select_set(True); bpy.context.view_layer.objects.active = target
if scene.camera is None: raise RuntimeError("B6 seed requires its existing camera")
if kind == "production":
scene.camera.location = (1.6, -1.8, 1.1); scene.camera.rotation_euler = (Vector((0.2, 0, 0.4)) - scene.camera.location).to_track_quat("-Z", "Y").to_euler()
bpy.context.view_layer.update()
for screen in bpy.data.screens:
for area in screen.areas:
for space in area.spaces:
if space.type == "VIEW_3D" and space.region_3d: space.region_3d.view_distance = 2.2 if kind == "production" else 8; space.region_3d.view_location = (0.2, 0, 0.4) if kind == "production" else (0, 0, 0)
objects = sorted(scene.objects, key=lambda obj: obj.name)
metadata = {"kind": kind, "source": str(source), "fixture": str(destination), "sourceBindings": source_bindings, "target": target.name, "root": root_name, "action": action.name, "frameStart": 1, "frameEnd": 24, "motionDelta": 0.05}
metadata["objects"] = [{"name": obj.name, "type": obj.type, "parent": obj.parent.name if obj.parent else None, "location": list(obj.location), "mesh": {"vertices": len(obj.data.vertices), "polygons": len(obj.data.polygons)} if obj.type == "MESH" else None, "materials": [mat.name for mat in getattr(obj.data, "materials", []) if mat]} for obj in objects]
materials = {mat for obj in objects for mat in getattr(obj.data, "materials", []) if mat}
metadata["materials"] = [{"name": mat.name, "color": list(mat.diffuse_color)} for mat in sorted(materials, key=lambda mat: mat.name)]
metadata["camera"] = {"name": scene.camera.name, "lens": scene.camera.data.lens, "clipStart": scene.camera.data.clip_start, "clipEnd": scene.camera.data.clip_end}
metadata["assembly"] = {"completeURDFVisuals": kind == "production", "sourceLinks": len(robot.findall("link")) if kind == "production" else 0, "sourceJoints": len(robot.findall("joint")) if kind == "production" else 0, "meshObjects": sum(obj.type == "MESH" for obj in objects)}
metadata["render"] = {"resolution": [scene.render.resolution_x, scene.render.resolution_y, scene.render.resolution_percentage], "format": scene.render.image_settings.file_format, "world": scene.world.name if scene.world else None}
metadata["producer"] = {"version": bpy.app.version_string, "binary": bpy.app.binary_path, "binarySha256": digest(Path(bpy.app.binary_path)), "generatorSha256": digest(Path(__file__))}
if destination.exists() or destination.with_suffix(".json").exists(): raise RuntimeError("Preserve existing B6 inputs; choose a new destination")
destination.parent.mkdir(parents=True, exist_ok=True)
bpy.ops.file.pack_all()
if bpy.ops.wm.save_as_mainfile(filepath=str(destination), check_existing=False, compress=True) != {"FINISHED"}: raise RuntimeError("Native B6 fixture save failed")
metadata["fixtureSha256"] = digest(destination)
destination.with_suffix(".json").write_text(json.dumps(metadata, ensure_ascii=False) + "\n"); print(json.dumps({"kind": kind, "objects": len(objects), "meshes": metadata["assembly"]["meshObjects"], "fixture": str(destination)}))