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>
87 lines
6.9 KiB
Python
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)}))
|