110 def set_camera(self, position, target):
111 """Set the camera position and target
114 position: Camera position as array [x, y, z]
115 target: Camera look-at target as array [x, y, z]
117 self.server.initial_camera.position = np.array(position, dtype=np.float64)
118 self.server.initial_camera.look_at = np.array(target, dtype=np.float64)
120 def load_mbm_environment(
122 problem_data: Dict[str, Any],
123 ignore_names: List[str] = [],
124 color=(0.8, 0.4, 0.2, 0.7),
125 padding: float = 0.0,
127 """Load environment from MBM problem format
130 problem_data: Dictionary containing 'sphere', 'cylinder', 'box' keys with obstacle data
131 ignore_names: List of obstacle names to ignore
132 color: RGBA color tuple (0-1 range) for obstacles
133 padding: Additional padding to add to obstacle sizes
136 # Helper function to convert euler angles to quaternion and create rotation matrix
137 def euler_to_rotation_matrix(euler_xyz):
138 """Convert euler angles (xyz) to rotation matrix"""
139 return Rotation.from_euler("xyz", euler_xyz).as_matrix()
142 for obj in problem_data.get("sphere", []):
143 if obj["name"] not in ignore_names:
144 position = np.array(obj["position"])
145 radius = obj["radius"] + padding
150 name=f"/sphere_{obj['name']}",
153 is_box_problem = problem_data.get("problem") == "box"
155 # Load cylinders (or as boxes if is_box_problem)
156 for obj in problem_data.get("cylinder", []):
157 if obj["name"] in ignore_names:
160 position = np.array(obj["position"])
161 orientation_euler = np.array(
162 obj.get("orientation_euler_xyz", [0.0, 0.0, 0.0])
164 rotation_matrix = euler_to_rotation_matrix(orientation_euler)
167 # Render cylinder as box (HACK for VAMP capsule overapproximation)
168 radius = obj["radius"] + padding
169 length = obj["length"]
170 half_extents = [radius, radius, length / 2.0]
173 half_extents=half_extents,
174 rotation_matrix=rotation_matrix,
176 name=f"/cylinder_as_box_{obj['name']}",
179 radius = obj["radius"] + padding
180 length = obj["length"]
185 rotation_matrix=rotation_matrix,
187 name=f"/cylinder_{obj['name']}",
190 for obj in problem_data.get("box", []):
191 if obj["name"] not in ignore_names:
192 position = np.array(obj["position"])
193 orientation_euler = np.array(
194 obj.get("orientation_euler_xyz", [0.0, 0.0, 0.0])
196 rotation_matrix = euler_to_rotation_matrix(orientation_euler)
197 half_extents = [h + padding / 2 for h in obj["half_extents"]]
200 half_extents=half_extents,
201 rotation_matrix=rotation_matrix,
203 name=f"/box_{obj['name']}",
209 color: Optional[np.ndarray] = None,
210 point_size: float = 0.01,
212 """Add a point cloud to the scene
215 points: Array of shape (N, 3) containing point coordinates
216 color: Optional array of shape (N, 3) containing RGB colors for each point
219 color = np.ones((points.shape[0], 3)) * np.array([1, 0, 0])
221 self.server.scene.add_point_cloud(
222 "/point_cloud", points=points, colors=color, point_size=point_size
243 def _rotation_to_wxyz(self, rotation_matrix: np.ndarray) -> np.ndarray:
244 """Convert rotation matrix to wxyz quaternion format
247 rotation_matrix: 3x3 rotation matrix
250 Quaternion in wxyz format
252 rotation = Rotation.from_matrix(rotation_matrix)
253 quat = rotation.as_quat() # Returns xyzw
254 wxyz = np.array([quat[3], quat[0], quat[1], quat[2]])
257 def _update_robot_config(
258 self, trajectory_idx: int, gripper_dof: float = 0.0
260 """Update the robot configuration in the visualization
263 trajectory_idx: Index into the current trajectory
264 gripper_dof: Value for extra gripper DOF if needed (default: 0.0)
266 if self._trajectory is None:
269 idx = min(trajectory_idx, len(self._trajectory) - 1)
270 plan_config = self._trajectory[idx].tolist()
271 config = self._map_plan_config_to_urdf(plan_config)
273 self.urdf_vis.update_cfg(config)
277 position: np.ndarray,
279 color=(1, 0, 0, 0.75),
280 name: Optional[str] = None,
282 """Add a sphere obstacle to the scene
285 position: 3D position [x, y, z]
286 radius: Sphere radius
287 color: RGBA color tuple (0-1 range)
288 name: Optional name for the sphere (auto-generated if not provided)
291 name = self._generate_name("/sphere_")
293 self.server.scene.add_icosphere(
295 position=tuple(position),
297 color=color[:3] if len(color) == 4 else color,
298 opacity=color[3] if len(color) == 4 else 1.0,
303 position: np.ndarray,
304 half_extents: List[float],
305 rotation_matrix: Optional[np.ndarray] = None,
306 color=(0.8, 0.4, 0.2, 0.75),
307 name: Optional[str] = None,
309 """Add a box obstacle to the scene
312 position: 3D position [x, y, z]
313 half_extents: Half extents [x, y, z] (full size will be 2x these values)
314 rotation_matrix: 3x3 rotation matrix (identity if None)
315 color: RGBA color tuple (0-1 range)
316 name: Optional name for the box (auto-generated if not provided)
319 name = self._generate_name("/box_")
321 # viser expects full extents, not half extents
322 full_extents = [h * 2 for h in half_extents]
324 if rotation_matrix is None:
325 rotation_matrix = np.eye(3)
327 wxyz = self._rotation_to_wxyz(rotation_matrix)
329 self.server.scene.add_box(
331 dimensions=tuple(full_extents),
332 position=tuple(position),
334 color=color[:3] if len(color) == 4 else color,
335 opacity=color[3] if len(color) == 4 else 1.0,
340 position: np.ndarray,
343 rotation_matrix: Optional[np.ndarray] = None,
344 color=(0.8, 0.4, 0.2, 0.75),
345 name: Optional[str] = None,
347 """Add a cylinder obstacle to the scene
350 position: 3D position [x, y, z]
351 radius: Cylinder radius
352 length: Cylinder length (height)
353 rotation_matrix: 3x3 rotation matrix (identity if None)
354 color: RGBA color tuple (0-1 range)
355 name: Optional name for the cylinder (auto-generated if not provided)
358 name = self._generate_name("/cylinder_")
360 if rotation_matrix is None:
361 rotation_matrix = np.eye(3)
363 wxyz = self._rotation_to_wxyz(rotation_matrix)
365 self.server.scene.add_cylinder(
369 position=tuple(position),
371 color=color[:3] if len(color) == 4 else color,
372 opacity=color[3] if len(color) == 4 else 1.0,
390 color: Optional[np.ndarray] = None,
391 point_size: float = 0.01,
393 """Add a point cloud to the scene
396 points: Array of shape (N, 3) containing point coordinates
397 color: Optional array of shape (N, 3) containing RGB colors for each point
400 color = np.ones((points.shape[0], 3)) * np.array([1, 0, 0])
402 self.server.scene.add_point_cloud(
403 "/point_cloud", points=points, colors=color, point_size=point_size
406 def _map_plan_config_to_urdf(self, plan_config: List[float]) -> List[float]:
407 """Map planning configuration to full URDF configuration
409 For robots like fetch that use a subset of joints in planning, this method
410 maps the planning DOFs to the correct positions in the full URDF configuration.
413 plan_config: Configuration from planner (e.g., 9 DOFs for fetch)
416 Full URDF configuration with all joints
419 if self.robot_name == "panda":
421 plan_config.append(0.05)
424 if self.joint_mapping is None:
425 # No mapping needed, use config as-is
428 # Get all joint names from URDF
430 all_joints = [joint.name for joint in self.robot_urdf.actuated_joints]
431 n_total_joints = len(all_joints)
433 # Create full configuration with zeros (neutral positions)
434 full_config = [0.0] * n_total_joints
436 # Map planning joints to their positions in URDF
437 for planning_idx, joint_name in enumerate(self.joint_mapping):
438 if joint_name in all_joints:
439 urdf_idx = all_joints.index(joint_name)
440 if planning_idx < len(plan_config):
441 full_config[urdf_idx] = plan_config[planning_idx]
443 # if fetch, open gripper
444 if self.robot_name == "fetch":
445 full_config[-1] = 0.035
446 full_config[-2] = 0.035
450 def visualize_trajectory(self, trajectory: np.ndarray):
451 """Visualize a robot trajectory with interactive controls
454 trajectory: Array of shape (timesteps, joints) containing joint configurations
456 if trajectory.ndim != 2:
458 f"Trajectory must be 2D array, got shape {trajectory.shape}"
461 if trajectory.shape[1] != self.dimension:
462 # Handle case where trajectory might have extra (gripper?) DOF
463 if trajectory.shape[1] == self.dimension + 1:
464 trajectory = trajectory[:, : self.dimension]
467 f"Trajectory has {trajectory.shape[1]} DOFs but robot has {self.dimension} DOFs"
470 self._trajectory = trajectory
472 self._slider = self.server.gui.add_slider(
473 "Timestep", min=0, max=len(trajectory) - 1, step=1, initial_value=0
476 self._playing = self.server.gui.add_checkbox("Playing", initial_value=True)
478 self._start_time = time.time()
480 def visualization_step(self):
481 """Perform one step of the visualization update"""
482 if self._trajectory is None:
485 if self._playing.value:
486 elapsed = time.time() - self._start_time
487 progress = (elapsed % self._playtime) / self._playtime
488 self._slider.value = int(progress * (len(self._trajectory) - 1))
490 # Update robot configuration with consistent gripper DOF of 0.0
491 slider_idx = self._slider.value
492 self._update_robot_config(slider_idx, gripper_dof=0.0)
522 def play_until_key_pressed(self, key="any", dt=0.1):
523 """Play visualization until specified key is pressed
526 key: Key to wait for. Use 'any' to stop on any key press (default: 'any')
527 dt: Time delay between frames in seconds (default: 0.1)
530 str: The key that was pressed
532 if self._trajectory is None:
533 print("No trajectory loaded. Call visualize_trajectory() first.")
537 f"Visualization running. Press {key if key != 'any' else 'any key'} to stop or click play button..."
541 stop_flag = threading.Event()
545 fd = sys.stdin.fileno()
546 old_settings = termios.tcgetattr(fd)
548 # Not a TTY, skip key handling
553 while not stop_flag.is_set():
555 if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
556 char = sys.stdin.read(1)
557 pressed_key[0] = char
558 if key == "any" or char == key:
564 except Exception as e:
565 print(f"Key listener error: {e}")
568 termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
572 # Start thread to wait for key
573 key_thread = threading.Thread(target=wait_for_key, daemon=True)
576 # Run visualization loop until key is pressed
577 # The play button controls auto-advancement of the slider
580 last_playing_state = False
581 last_slider_value = 0
583 while not stop_flag.is_set():
584 # Check if playing state just changed from False to True
585 if self._playing is not None:
586 current_playing_state = self._playing.value
587 if current_playing_state and not last_playing_state:
588 # Playing just activated, capture current slider position
589 if self._slider is not None:
590 frame_idx = self._slider.value
591 last_playing_state = current_playing_state
593 current_slider_value = (
594 self._slider.value if self._slider is not None else frame_idx
596 if current_slider_value != last_slider_value:
597 # User moved the slider, sync frame_idx to it
598 frame_idx = current_slider_value
599 last_slider_value = current_slider_value
601 # If playing is checked, auto-advance the slider
602 if self._playing is not None and self._playing.value:
603 if self._slider is not None:
604 self._slider.value = frame_idx
605 frame_idx = (frame_idx + 1) % len(self._trajectory)
607 # Always update robot position based on current slider value
608 if self._slider is not None:
609 current_idx = self._slider.value
611 current_idx = frame_idx
613 self._update_robot_config(current_idx, gripper_dof=0.0)
619 print(f"\nVisualization stopped. Key pressed: {pressed_key[0]}")
620 return pressed_key[0]