| """Scene construction from a task's YAML: object placement, containers, markers, randomization. |
| |
| All the asset quirks discovered the hard way are handled here once, so a task YAML never has to |
| know about them: |
| |
| * RoboTwin GLBs are authored Y-up -- a cup spawns upside-down and a basket on its side, so every |
| asset carries an `rpy` and the default for RoboTwin containers is a +90 deg roll; |
| * the GLB->USD converter reads RoboTwin's `model_data` scale but does not bake it in, so the |
| scale must be applied at spawn or a basket arrives 1.9 m wide; |
| * objects written in at a fixed height DROP onto the table and tall ones topple, so every object |
| is re-seated at its measured height before the episode starts; |
| * a container spawned on the arm's home pose silently blocks all motion, so placements are |
| checked against it and a warning is printed. |
| """ |
| from __future__ import annotations |
|
|
| import numpy as np |
|
|
| TABLE_TOP = 0.45 |
| |
| HOME_EEF_WORLD = np.array([0.078, -0.19]) |
| HOME_EEF_Z = 0.60 |
|
|
|
|
| def euler_quat(roll_deg=0.0, pitch_deg=0.0, yaw_deg=0.0): |
| r, p, y = np.radians([roll_deg, pitch_deg, yaw_deg]) |
| cr, sr = np.cos(r/2), np.sin(r/2) |
| cp, sp = np.cos(p/2), np.sin(p/2) |
| cy, sy = np.cos(y/2), np.sin(y/2) |
| return np.array([cr*cp*cy+sr*sp*sy, sr*cp*cy-cr*sp*sy, cr*sp*cy+sr*cp*sy, cr*cp*sy-sr*sp*cy]) |
|
|
|
|
| class Randomizer: |
| """Per-episode scene randomization, driven by the YAML and a seed. |
| |
| A task declares nominal poses plus jitter ranges; the same seed reproduces an episode exactly, |
| which is what makes a "it worked once" result checkable. |
| """ |
|
|
| def __init__(self, seed=0, enabled=True): |
| self.rng = np.random.default_rng(seed) |
| self.enabled = enabled |
| self.log = {} |
|
|
| def xy(self, name, nominal, jitter): |
| base = np.asarray(nominal, float) |
| if not self.enabled or jitter in (None, 0): |
| self.log[name] = base.tolist(); return base |
| j = np.asarray(jitter, float) |
| if j.ndim == 0: |
| j = np.array([float(j), float(j)]) |
| out = base + self.rng.uniform(-j, j) |
| self.log[name] = out.tolist() |
| return out |
|
|
| def scalar(self, name, nominal, jitter): |
| if not self.enabled or not jitter: |
| self.log[name] = float(nominal); return float(nominal) |
| out = float(nominal) + float(self.rng.uniform(-jitter, jitter)) |
| self.log[name] = out |
| return out |
|
|
|
|
| class SceneBuilder: |
| """Places YAML-declared objects and props into a live Isaac Lab scene.""" |
|
|
| def __init__(self, env, origin, rand: Randomizer): |
| self.env = env |
| self.u = env.unwrapped |
| self.origin = np.asarray(origin, float) |
| self.rand = rand |
| self.objects = {} |
| self.articulations = {} |
| self._artic_ext = {} |
| self.regions = {} |
|
|
| |
| def place_object(self, spec, settle_steps=70, step_fn=None): |
| """Place one YAML object: xy (+jitter), orientation, then re-seat it on the table.""" |
| import torch |
| name = spec["name"] |
| if name not in self.u.scene.rigid_objects: |
| print(f"[scene] object {name!r} is not registered in the env -- skipped", flush=True) |
| return None |
| obj = self.u.scene.rigid_objects[name] |
| xy = self.rand.xy(name, spec.get("xy", [0.0, 0.0]), spec.get("xy_jitter")) |
| yaw = self.rand.scalar(f"{name}.yaw", spec.get("yaw", 0.0), spec.get("yaw_jitter")) |
| quat = euler_quat(spec.get("roll", 0.0), spec.get("pitch", 0.0), yaw) |
| dev = obj.data.root_pos_w.device |
| obj.write_root_pose_to_sim(torch.tensor( |
| np.concatenate([self.origin+np.array([xy[0], xy[1], 0.58]), quat]), |
| dtype=torch.float32, device=dev).view(1, 7)) |
| obj.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev)) |
| self.objects[name] = obj |
| |
| |
| return {"name": name, "xy": xy, "quat": quat, "z_offset": float(spec.get("z_offset", 0.0))} |
|
|
| def _prim_aabb(self, name): |
| """World AABB of an articulation AT ITS CURRENT POSE, plus that pose's z. |
| |
| Queried before the fixture is moved, so the numbers describe the asset as authored: the |
| difference between the root z and the AABB's floor is how far the origin sits above the |
| bottom of the body, which is exactly the lift needed to stand it on the table. |
| """ |
| try: |
| import omni.usd |
| from pxr import UsdGeom, Usd |
| stage = omni.usd.get_context().get_stage() |
| bb = UsdGeom.BBoxCache(Usd.TimeCode.Default(), |
| [UsdGeom.Tokens.default_, UsdGeom.Tokens.render]) |
| art = self.u.scene.articulations[name] |
| pp = art.root_physx_view.prim_paths[0] |
| rng = bb.ComputeWorldBound(stage.GetPrimAtPath(pp)).ComputeAlignedRange() |
| root_z = float(art.data.root_pos_w[0, 2].item()) |
| return (np.array(rng.GetMin()), np.array(rng.GetMax())), root_z |
| except Exception as exc: |
| print(f"[scene] could not measure {name}: {exc}", flush=True) |
| return None, 0.0 |
|
|
| def fixture_extent(self, name): |
| """Measured size of a jointed fixture, for tasks that need its face positions.""" |
| return self._artic_ext.get(name) |
|
|
| def place_articulation(self, spec): |
| """Seat a JOINTED fixture (a door, a drawer, a lid) and record its joint layout. |
| |
| Articulations are not rigid objects: they live in `scene.articulations`, their pose is |
| written to the root body, and the thing a task cares about is a JOINT value, not the |
| object's centre. They are converted with a fixed base, so once written in they stay put |
| while the arm works against the joint. |
| """ |
| import torch |
| name = spec["name"] |
| if name not in self.u.scene.articulations: |
| print(f"[scene] articulation {name!r} is not registered in the env -- skipped", flush=True) |
| return None |
| art = self.u.scene.articulations[name] |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.20]), spec.get("xy_jitter")) |
| yaw = self.rand.scalar(f"{name}.yaw", spec.get("yaw", 0.0), spec.get("yaw_jitter")) |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| roll = float(spec.get("roll", 90.0)) |
| bbox, scale = spec.get("bbox"), float(spec.get("scale", 1.0)) |
| if bbox is not None: |
| mn = np.asarray(bbox[0], float)*scale |
| mx = np.asarray(bbox[1], float)*scale |
| if abs(roll-90.0) < 1e-6: |
| lift, ext = -float(mn[1]), np.array([mx[0]-mn[0], mx[2]-mn[2], mx[1]-mn[1]]) |
| else: |
| lift, ext = -float(mn[2]), mx-mn |
| self._artic_ext[name] = ext |
| print(f"[scene] {name} stands {ext[2]*100:.1f} cm tall " |
| f"({ext[0]*100:.1f} x {ext[1]*100:.1f} cm), seated +{lift:.3f} m", flush=True) |
| else: |
| lift = float(spec.get("seat", 0.0)) |
| z = TABLE_TOP+lift+float(spec.get("z_offset", 0.0)) |
| quat = euler_quat(roll, spec.get("pitch", 0.0), yaw) |
| dev = art.data.root_pos_w.device |
| art.write_root_pose_to_sim(torch.tensor( |
| np.concatenate([self.origin+np.array([xy[0], xy[1], z]), quat]), |
| dtype=torch.float32, device=dev).view(1, 7)) |
| art.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev)) |
| if spec.get("joint_init") is not None: |
| q = art.data.joint_pos.clone() |
| for j, v in dict(spec["joint_init"]).items(): |
| q[0, int(j)] = float(v) |
| art.write_joint_state_to_sim(q, torch.zeros_like(q)) |
| self.articulations[name] = art |
| names = list(getattr(art.data, "joint_names", []) or []) |
| self.regions[name] = {"xy": xy, "half": float(spec.get("half", 0.12)), |
| "top_z": z, "yaw": yaw} |
| print(f"[scene] articulation {name} at ({xy[0]:.3f},{xy[1]:.3f}) joints={names}", flush=True) |
| return {"name": name, "xy": xy, "quat": quat, "joints": names} |
|
|
| def link_index(self, name, link_name): |
| """Index of a link BY NAME. A generated chain interleaves massless spacer links between |
| its segments, so body index 13 is not seg13 -- indexing by number silently reads the |
| wrong body.""" |
| names = list(getattr(self.articulations[name].data, "body_names", []) or []) |
| return names.index(link_name) if link_name in names else None |
|
|
| def joint_pos(self, name, index=0): |
| """Live value of one joint (radians for a hinge, metres for a slider).""" |
| return float(self.articulations[name].data.joint_pos[0, int(index)].item()) |
|
|
| def link_pos(self, name, index): |
| """World xyz of one link's body, relative to the env origin -- e.g. a door's panel.""" |
| return (self.articulations[name].data.body_pos_w[0, int(index)].cpu().numpy()-self.origin) |
|
|
| def reseat_objects(self, placed, step_fn, settle_steps=80): |
| """Second pass: drop each object to its own measured height so nothing topples. |
| |
| Objects are written in above the table; a tall one (cup, bottle, peg) lands on its side |
| and no top-down grasp can recover it. |
| """ |
| import torch |
| for _ in range(settle_steps): |
| step_fn() |
| for p in placed: |
| if p is None: |
| continue |
| name = p["name"] |
| obj = self.objects[name] |
| ext = self.object_size(name) |
| dev = obj.data.root_pos_w.device |
| obj.write_root_pose_to_sim(torch.tensor( |
| np.concatenate([self.origin+np.array([p["xy"][0], p["xy"][1], |
| TABLE_TOP+float(ext[2])/2.0+0.004 |
| + float(p.get("z_offset", 0.0))]), p["quat"]]), |
| dtype=torch.float32, device=dev).view(1, 7)) |
| obj.write_root_velocity_to_sim(torch.zeros((1, 6), device=dev)) |
| print(f"[scene] seated {name} at ({p['xy'][0]:.3f},{p['xy'][1]:.3f}) size={np.round(ext,3)}", |
| flush=True) |
| for _ in range(settle_steps): |
| step_fn() |
|
|
| def object_size(self, name): |
| """Authored bbox SIZE. Pose-independent, so it is only valid for extents, never poses.""" |
| try: |
| import omni.usd |
| from pxr import UsdGeom, Usd |
| stage = omni.usd.get_context().get_stage() |
| bb = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_, UsdGeom.Tokens.render]) |
| pp = self.u.scene.rigid_objects[name].root_physx_view.prim_paths[0] |
| rng = bb.ComputeWorldBound(stage.GetPrimAtPath(pp)).ComputeAlignedRange() |
| return np.array(rng.GetMax())-np.array(rng.GetMin()) |
| except Exception as e: |
| print(f"[scene] size probe failed for {name}: {e}", flush=True) |
| return np.array([0.05, 0.05, 0.05]) |
|
|
| def object_pos(self, name): |
| """LIVE position from physics -- always use this for grasp targets, never the bbox.""" |
| return self.u.scene.rigid_objects[name].data.root_pos_w[0].cpu().numpy()-self.origin |
|
|
| |
| def spawn_container(self, spec): |
| """A RoboTwin container/stand: kinematic, scaled, rolled upright, standing ON the table.""" |
| import isaaclab.sim as sim_utils |
| import os |
| usd_root = os.environ.get("ROBOTWIN_USD", "/home/yu/internship_yu/robotwin_usd") |
| name = spec.get("name", "container") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.25]), spec.get("xy_jitter")) |
| scale = float(spec.get("scale", 1.0)) |
| rpy = spec.get("rpy", [90.0, 0.0, 0.0]) |
| usd_path = f"{usd_root}/{spec['usd']}" |
| if not os.path.exists(usd_path): |
| |
| avail = sorted(p.name for p in __import__("pathlib").Path(usd_root).glob("*/*.usd"))[:8] |
| raise SystemExit( |
| f"[scene] container asset missing: {usd_path}\n" |
| f" convert it first, e.g.\n" |
| f" ROBOTWIN_USD={usd_root} python scripts/robotwin_convert.py --headless \\\n" |
| f" --collision none --suffix _mesh --jobs {spec['usd'].split('/')[0]}:base0\n" |
| f" (containers need the _mesh variant so their cavity is hollow)\n" |
| f" nearby files: {avail}") |
| cfg = sim_utils.UsdFileCfg(usd_path=usd_path, |
| rigid_props=sim_utils.RigidBodyPropertiesCfg(kinematic_enabled=True), |
| scale=(scale,)*3) |
| prim = f"/World/envs/env_0/{name}" |
| cfg.func(prim, cfg, translation=tuple((self.origin+np.array([xy[0], xy[1], TABLE_TOP])).tolist()), |
| orientation=tuple(float(v) for v in euler_quat(*rpy))) |
| top_z, half = self._stand_on_table(prim) |
| self.regions[name] = {"xy": xy, "half": half, "top_z": top_z} |
| self._warn_if_blocking(name, xy, half, top_z) |
| return self.regions[name] |
|
|
| def _stand_on_table(self, prim_path): |
| """Raise a spawned prim so its bbox bottom meets the table; return (top_z, half_span).""" |
| import omni.usd |
| from pxr import UsdGeom, Usd, Gf |
| stage = omni.usd.get_context().get_stage() |
| prim = stage.GetPrimAtPath(prim_path) |
| bb = UsdGeom.BBoxCache(Usd.TimeCode.Default(), [UsdGeom.Tokens.default_, UsdGeom.Tokens.render]) |
| rng = bb.ComputeWorldBound(prim).ComputeAlignedRange() |
| dz = float(self.origin[2]+TABLE_TOP)-float(rng.GetMin()[2]) |
| for op in UsdGeom.Xformable(prim).GetOrderedXformOps(): |
| if op.GetOpType() == UsdGeom.XformOp.TypeTranslate: |
| t = op.Get(); op.Set(Gf.Vec3d(float(t[0]), float(t[1]), float(t[2])+dz)); break |
| rng = bb.ComputeWorldBound(prim).ComputeAlignedRange() |
| ext = np.array(rng.GetMax())-np.array(rng.GetMin()) |
| top_z = float(rng.GetMax()[2])-self.origin[2] |
| print(f"[scene] {prim_path.split('/')[-1]}: raised {dz:+.3f} m to stand on the table, " |
| f"extents={np.round(ext,3)} top_z={top_z:.3f}", flush=True) |
| return top_z, float(min(ext[0], ext[1]))/2.0 |
|
|
| def build_box(self, spec): |
| """A primitive open box/tray from cuboids (license-clean, exact dimensions).""" |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "box") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.25]), spec.get("xy_jitter")) |
| S = float(spec.get("span", 0.26)); H = float(spec.get("wall_h", 0.05)); T = 0.010 |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
|
|
| def cub(tag, size, off, color=(0.55, 0.38, 0.22)): |
| c = sim_utils.CuboidCfg(size=tuple(size), |
| visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=color), |
| collision_props=sim_utils.CollisionPropertiesCfg()) |
| c.func(f"/World/envs/env_0/{name}_{tag}", c, translation=tuple((base+np.array(off)).tolist())) |
|
|
| cub("floor", (S, S, T), (0, 0, T/2)) |
| cub("xp", (T, S, H), (S/2, 0, H/2)); cub("xn", (T, S, H), (-S/2, 0, H/2)) |
| cub("yp", (S, T, H), (0, S/2, H/2)); cub("yn", (S, T, H), (0, -S/2, H/2)) |
| self.regions[name] = {"xy": xy, "half": S/2, "top_z": TABLE_TOP+H} |
| self._warn_if_blocking(name, xy, S/2, TABLE_TOP+H) |
| print(f"[scene] primitive box {name} at ({xy[0]:.3f},{xy[1]:.3f}) span={S} wall_h={H}", flush=True) |
| return self.regions[name] |
|
|
| def set_table_friction(self, static=1.2, dynamic=1.0, restitution=0.0, size=0.52, depth=None): |
| """Give the work area a high-friction surface for tasks that push things ALONG it. |
| |
| The stock lab table is slippery, which is fine for pick-and-place but wrong for sweeping: |
| debris skitters ahead of the tool and keeps going after the stroke ends. |
| |
| This lays a thin mat rather than re-binding the table's own material. The table USD is |
| `table_instanceable.usd`, and a physics material cannot be bound onto an instanced prim |
| -- `bind_physics_material` walks the hierarchy, finds only the instance proxy, and warns |
| that it applied to nothing. The mat is 2 mm thick, so it does not change any working |
| height noticeably. |
| """ |
| import isaaclab.sim as sim_utils |
| |
| |
| mat = sim_utils.CuboidCfg( |
| size=(float(size), float(depth if depth is not None else size*0.68), 0.002), |
| visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.34, 0.33, 0.35), |
| roughness=0.98), |
| collision_props=sim_utils.CollisionPropertiesCfg(), |
| physics_material=sim_utils.RigidBodyMaterialCfg( |
| static_friction=float(static), dynamic_friction=float(dynamic), |
| restitution=float(restitution)), |
| ) |
| mat.func("/World/envs/env_0/work_mat", mat, |
| translation=tuple((self.origin+np.array([0.0, 0.0, TABLE_TOP+0.001])).tolist())) |
| print(f"[scene] work mat laid: mu_s={static} mu_d={dynamic} size={size} m", flush=True) |
| return True |
|
|
| def build_shelf(self, spec): |
| """An open cupboard: back panel, two sides, a top and one raised shelf. |
| |
| Open toward -y (the robot). The reachable target is the SHELF surface, not the floor of |
| the unit -- placing there means clearing the shelf lip on the way in, which is what makes |
| this different from dropping into an open-topped box. |
| """ |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "cupboard") |
| xy = self.rand.xy(name, spec.get("xy", [-0.28, -0.05]), spec.get("xy_jitter")) |
| W = float(spec.get("width", 0.22)) |
| D = float(spec.get("depth", 0.16)) |
| SH = float(spec.get("shelf_h", 0.085)) |
| H = float(spec.get("height", 0.20)) |
| T = 0.010 |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
| color = tuple(spec.get("color", (0.62, 0.45, 0.28))) |
|
|
| def cub(tag, size, off): |
| c = sim_utils.CuboidCfg(size=tuple(size), |
| visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=color), |
| collision_props=sim_utils.CollisionPropertiesCfg()) |
| c.func(f"/World/envs/env_0/{name}_{tag}", c, translation=tuple((base+np.array(off)).tolist())) |
|
|
| cub("back", (W, T, H), (0, D/2, H/2)) |
| cub("xp", (T, D, H), (W/2, 0, H/2)) |
| cub("xn", (T, D, H), (-W/2, 0, H/2)) |
| |
| |
| |
| |
| if spec.get("roof", True): |
| cub("top", (W, D, T), (0, 0, H-T/2)) |
| cub("shelf", (W-2*T, D, T), (0, 0, SH)) |
|
|
| |
| self.regions[name] = {"xy": xy, "half": min(W, D)/2-0.02, "top_z": TABLE_TOP+SH+T/2, |
| "shelf_z": TABLE_TOP+SH+T/2, "clearance_z": TABLE_TOP+H} |
| self._warn_if_blocking(name, xy, max(W, D)/2, TABLE_TOP+H) |
| print(f"[scene] cupboard {name} at ({xy[0]:.3f},{xy[1]:.3f}) shelf_z={SH:.3f} h={H}", flush=True) |
| return self.regions[name] |
|
|
| def build_post(self, spec): |
| """A vertical post standing on the table -- the peg a ring has to be dropped over.""" |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "post") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.18]), spec.get("xy_jitter")) |
| R = float(spec.get("radius", 0.011)) |
| H = float(spec.get("height", 0.13)) |
| BR = float(spec.get("base_radius", 0.050)) |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
| color = tuple(spec.get("color", (0.35, 0.35, 0.40))) |
|
|
| for tag, cfg, off in [ |
| ("base", sim_utils.CylinderCfg(radius=BR, height=0.010), (0, 0, 0.005)), |
| ("rod", sim_utils.CylinderCfg(radius=R, height=H), (0, 0, 0.010+H/2)), |
| ]: |
| cfg.visual_material = sim_utils.PreviewSurfaceCfg(diffuse_color=color) |
| cfg.collision_props = sim_utils.CollisionPropertiesCfg() |
| cfg.func(f"/World/envs/env_0/{name}_{tag}", cfg, |
| translation=tuple((base+np.array(off)).tolist())) |
|
|
| self.regions[name] = {"xy": xy, "half": BR, "top_z": TABLE_TOP+0.010, |
| "tip_z": TABLE_TOP+0.010+H, "radius": R} |
| self._warn_if_blocking(name, xy, BR, TABLE_TOP+0.010+H) |
| print(f"[scene] post {name} at ({xy[0]:.3f},{xy[1]:.3f}) r={R} h={H}", flush=True) |
| return self.regions[name] |
|
|
| def build_sleeve(self, spec): |
| """A block with a through-channel along x: a bar slides in it and can be drawn out. |
| |
| Open at both x ends and capped on top, so the only way to move the bar is a straight |
| pull along the channel -- lifting or turning jams it. That constraint IS the task. |
| """ |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "sleeve") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, 0.0]), spec.get("xy_jitter")) |
| L = float(spec.get("length", 0.11)) |
| W = float(spec.get("width", 0.046)) |
| H = float(spec.get("height", 0.046)) |
| T = float(spec.get("thickness", 0.014)) |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
| color = tuple(spec.get("color", (0.42, 0.28, 0.16))) |
|
|
| def cub(tag, size, off): |
| c = sim_utils.CuboidCfg(size=tuple(size), |
| visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=color), |
| collision_props=sim_utils.CollisionPropertiesCfg()) |
| c.func(f"/World/envs/env_0/{name}_{tag}", c, translation=tuple((base+np.array(off)).tolist())) |
|
|
| |
| cub("floor", (L, W+2*T, T), (0, 0, T/2)) |
| cub("yp", (L, T, H), (0, (W+T)/2, T+H/2)) |
| cub("yn", (L, T, H), (0, -(W+T)/2, T+H/2)) |
| cub("top", (L, W+2*T, T), (0, 0, T+H+T/2)) |
|
|
| self.regions[name] = {"xy": xy, "half": L/2, "top_z": TABLE_TOP+2*T+H, |
| "channel_z": TABLE_TOP+T, "length": L, "clear": W} |
| self._warn_if_blocking(name, xy, max(L, W+2*T)/2, TABLE_TOP+2*T+H) |
| print(f"[scene] sleeve {name} at ({xy[0]:.3f},{xy[1]:.3f}) channel {L}x{W}x{H}", flush=True) |
| return self.regions[name] |
|
|
| def build_whiteboard(self, spec): |
| """A whiteboard lying on the table: white writing surface inside a black frame. |
| |
| The frame is raised above the surface, so it also stops a wiped smudge (or the eraser) |
| from sliding off the edge -- the board is the work area, not just a coloured decal. |
| """ |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "board") |
| xy = self.rand.xy(name, spec.get("xy", [0.02, -0.12]), spec.get("xy_jitter")) |
| W = float(spec.get("width", 0.30)) |
| D = float(spec.get("depth", 0.22)) |
| FR = float(spec.get("frame", 0.016)) |
| TH = float(spec.get("thickness", 0.008)) |
| FH = float(spec.get("frame_h", 0.014)) |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
|
|
| def cub(tag, size, off, color): |
| c = sim_utils.CuboidCfg(size=tuple(size), |
| visual_material=sim_utils.PreviewSurfaceCfg( |
| diffuse_color=color, roughness=float(spec.get("rough", 0.35))), |
| collision_props=sim_utils.CollisionPropertiesCfg()) |
| c.func(f"/World/envs/env_0/{name}_{tag}", c, translation=tuple((base+np.array(off)).tolist())) |
|
|
| white = tuple(spec.get("color", (0.97, 0.97, 0.98))) |
| black = tuple(spec.get("frame_color", (0.06, 0.06, 0.07))) |
| cub("panel", (W, D, TH), (0, 0, TH/2), white) |
| cub("fxp", (FR, D, FH), ((W-FR)/2, 0, TH+FH/2-0.001), black) |
| cub("fxn", (FR, D, FH), (-(W-FR)/2, 0, TH+FH/2-0.001), black) |
| cub("fyp", (W, FR, FH), (0, (D-FR)/2, TH+FH/2-0.001), black) |
| cub("fyn", (W, FR, FH), (0, -(D-FR)/2, TH+FH/2-0.001), black) |
|
|
| |
| self.regions[name] = {"xy": xy, "half": min(W, D)/2-FR, "top_z": TABLE_TOP+TH, |
| "width": W-2*FR, "depth": D-2*FR} |
| self._warn_if_blocking(name, xy, max(W, D)/2, TABLE_TOP+TH+FH) |
| print(f"[scene] whiteboard {name} at ({xy[0]:.3f},{xy[1]:.3f}) {W}x{D} " |
| f"writable half={self.regions[name]['half']:.3f}", flush=True) |
| return self.regions[name] |
|
|
| def build_marker(self, spec): |
| """A paper-thin target region drawn on the table (visual goal, no obstruction).""" |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "target") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.20]), spec.get("xy_jitter")) |
| size = float(spec.get("size", 0.11)) |
| yaw = self.rand.scalar(f"{name}.yaw", spec.get("yaw", 0.0), spec.get("yaw_jitter")) |
| m = sim_utils.CuboidCfg(size=(size, size*float(spec.get("aspect", 1.0)), 0.0015), |
| visual_material=sim_utils.PreviewSurfaceCfg( |
| diffuse_color=tuple(spec.get("color", (0.20, 0.65, 0.30))))) |
| |
| z = float(spec.get("z", TABLE_TOP+0.001)) |
| m.func(f"/World/envs/env_0/{name}", m, |
| translation=tuple((self.origin+np.array([xy[0], xy[1], z])).tolist()), |
| orientation=tuple(float(v) for v in euler_quat(0, 0, yaw))) |
| self.regions[name] = {"xy": xy, "half": size/2.0, "top_z": z+0.001, "yaw": yaw} |
| print(f"[scene] target region {name} at ({xy[0]:.3f},{xy[1]:.3f}) size={size} yaw={yaw:.0f}", |
| flush=True) |
| return self.regions[name] |
|
|
| def build_socket(self, spec): |
| """Four walls leaving a square hole whose floor is the table (peg insertion).""" |
| import isaaclab.sim as sim_utils |
| name = spec.get("name", "socket") |
| xy = self.rand.xy(name, spec.get("xy", [0.0, -0.22]), spec.get("xy_jitter")) |
| hole = float(spec.get("hole", 0.042)); WT = 0.045; WH = 0.05 |
| HW = hole/2.0; SPAN = hole+2*WT |
| base = self.origin+np.array([xy[0], xy[1], TABLE_TOP]) |
|
|
| def wall(tag, size, off): |
| c = sim_utils.CuboidCfg(size=tuple(size), |
| visual_material=sim_utils.PreviewSurfaceCfg(diffuse_color=(0.35, 0.38, 0.45)), |
| collision_props=sim_utils.CollisionPropertiesCfg()) |
| c.func(f"/World/envs/env_0/{name}_{tag}", c, translation=tuple((base+np.array(off)).tolist())) |
|
|
| wall("xp", (WT, SPAN, WH), (HW+WT/2, 0, WH/2)); wall("xn", (WT, SPAN, WH), (-HW-WT/2, 0, WH/2)) |
| wall("yp", (SPAN, WT, WH), (0, HW+WT/2, WH/2)); wall("yn", (SPAN, WT, WH), (0, -HW-WT/2, WH/2)) |
| self.regions[name] = {"xy": xy, "half": HW, "top_z": TABLE_TOP+WH, "hole": hole} |
| print(f"[scene] socket {name} at ({xy[0]:.3f},{xy[1]:.3f}) hole={hole}", flush=True) |
| return self.regions[name] |
|
|
| def _warn_if_blocking(self, name, xy, half, top_z=None): |
| """Warn only for props TALL enough to foul the arm at its home pose. |
| |
| A 5 cm tray under the wrist is harmless; a 23 cm rack in the same spot stops the arm |
| dead on its first move, which previously looked like a mysterious tracking failure. |
| """ |
| d = float(np.linalg.norm(np.asarray(xy, float)-HOME_EEF_WORLD)) |
| tall = top_z is None or float(top_z) > HOME_EEF_Z-0.06 |
| if d < half+0.05 and tall: |
| print(f"[scene] WARNING: {name} at ({xy[0]:.3f},{xy[1]:.3f}) top_z={top_z} overlaps the " |
| f"arm's home pose {HOME_EEF_WORLD.tolist()} (gap {d-half:+.3f} m) and is tall " |
| f"enough to foul it. The arm may be blocked from its first move.", flush=True) |
|
|