"""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)}))