Skip to content

Commit 3a99af5

Browse files
committed
Add Move/Rotate mode toggle for teleop gizmo
Two gizmos (translate-only and rotate-only) swap visibility on toggle. Both sync position/orientation so switching is seamless. Ghost hand is a child of the move gizmo. Reduces 6DOF gizmo to 3DOF per mode for much easier mouse interaction.
1 parent f2139d1 commit 3a99af5

1 file changed

Lines changed: 72 additions & 22 deletions

File tree

src/mj_viser/teleop_panel.py

Lines changed: 72 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -223,26 +223,43 @@ def setup(self, gui: viser.GuiApi, viewer: MujocoViewer) -> None:
223223

224224
# Gizmo (hidden initially) — unique name per arm
225225
arm_name = self._arm.config.name
226-
gizmo_name = f"/teleop/{arm_name}/gizmo"
226+
gizmo_base = f"/teleop/{arm_name}"
227227
ee_pose = self._arm.get_ee_pose()
228-
self._gizmo = scene.add_transform_controls(
229-
gizmo_name,
228+
init_wxyz = xmat_to_wxyz(ee_pose[:3, :3].flatten())
229+
init_pos = tuple(ee_pose[:3, 3])
230+
231+
# Two gizmos: translate-only and rotate-only. Toggle between them.
232+
self._gizmo_move = scene.add_transform_controls(
233+
f"{gizmo_base}/gizmo_move",
234+
scale=0.3,
235+
depth_test=False,
236+
disable_rotations=True,
237+
wxyz=init_wxyz,
238+
position=init_pos,
239+
visible=False,
240+
)
241+
self._gizmo_rotate = scene.add_transform_controls(
242+
f"{gizmo_base}/gizmo_rotate",
230243
scale=0.3,
231244
depth_test=False,
232-
wxyz=xmat_to_wxyz(ee_pose[:3, :3].flatten()),
233-
position=tuple(ee_pose[:3, 3]),
245+
disable_axes=True,
246+
disable_sliders=True,
247+
wxyz=init_wxyz,
248+
position=init_pos,
234249
visible=False,
235250
)
251+
self._gizmo_mode = "move" # "move" or "rotate"
252+
# Active gizmo reference for convenience
253+
self._gizmo = self._gizmo_move
236254

237-
# Ghost hand as child of gizmo — moves automatically when gizmo moves
255+
# Ghost hand as child of the move gizmo
238256
self._ghost = GhostHand(
239257
server, self._model, self._data,
240258
self._gripper_prefix, self._arm.ee_site_id,
241-
name=f"{gizmo_name}/ghost",
259+
name=f"{gizmo_base}/gizmo_move/ghost",
242260
)
243261

244-
# Wire gizmo callbacks
245-
@self._gizmo.on_update
262+
# Shared callback for both gizmos
246263
def _on_gizmo_update(event) -> None:
247264
if not self._is_teleop_active:
248265
return
@@ -255,13 +272,20 @@ def _on_gizmo_update(event) -> None:
255272
[2*(x*z - w*y), 2*(y*z + w*x), 1 - 2*(x*x + y*y)],
256273
])
257274
pose[:3, 3] = event.target.position
258-
# Only buffer the target — stepping happens in _teleop_loop thread
259275
self._controller.set_target_pose(pose)
276+
# Sync the other gizmo's position so toggling is seamless
277+
other = self._gizmo_rotate if event.target is self._gizmo_move else self._gizmo_move
278+
other.wxyz = event.target.wxyz
279+
other.position = event.target.position
280+
281+
self._gizmo_move.on_update(_on_gizmo_update)
282+
self._gizmo_rotate.on_update(_on_gizmo_update)
260283

261284
# GUI controls — wrapped in a folder per arm
262285
with gui.add_folder(self._arm_label):
263286
self._activate_btn = gui.add_button("Activate", color="green")
264287
self._status_md = gui.add_markdown("⚪ **Idle**")
288+
self._mode_btn = gui.add_button("Mode: Move")
265289
self._snap_btn = gui.add_button("Snap to EE")
266290
self._gripper_btn = gui.add_button("Toggle Gripper")
267291

@@ -285,12 +309,30 @@ def _on_activate(event) -> None:
285309
else:
286310
self._activate_teleop()
287311

312+
@self._mode_btn.on_click
313+
def _on_mode_toggle(event) -> None:
314+
if self._gizmo_mode == "move":
315+
self._gizmo_mode = "rotate"
316+
self._mode_btn.name = "Mode: Rotate"
317+
if self._is_teleop_active:
318+
self._gizmo_move.visible = False
319+
self._gizmo_rotate.visible = True
320+
else:
321+
self._gizmo_mode = "move"
322+
self._mode_btn.name = "Mode: Move"
323+
if self._is_teleop_active:
324+
self._gizmo_rotate.visible = False
325+
self._gizmo_move.visible = True
326+
288327
@self._snap_btn.on_click
289328
def _on_snap(event) -> None:
290-
if self._gizmo is not None:
291-
ee_pose = self._arm.get_ee_pose()
292-
self._gizmo.wxyz = xmat_to_wxyz(ee_pose[:3, :3].flatten())
293-
self._gizmo.position = tuple(ee_pose[:3, 3])
329+
ee_pose = self._arm.get_ee_pose()
330+
wxyz = xmat_to_wxyz(ee_pose[:3, :3].flatten())
331+
pos = tuple(ee_pose[:3, 3])
332+
self._gizmo_move.wxyz = wxyz
333+
self._gizmo_move.position = pos
334+
self._gizmo_rotate.wxyz = wxyz
335+
self._gizmo_rotate.position = pos
294336

295337
@self._gripper_btn.on_click
296338
def _on_gripper(event) -> None:
@@ -317,10 +359,18 @@ def _activate_teleop(self) -> None:
317359
ee_pose = self._controller.activate()
318360
self._is_teleop_active = True
319361

320-
if self._gizmo is not None:
321-
self._gizmo.wxyz = xmat_to_wxyz(ee_pose[:3, :3].flatten())
322-
self._gizmo.position = tuple(ee_pose[:3, 3])
323-
self._gizmo.visible = True
362+
wxyz = xmat_to_wxyz(ee_pose[:3, :3].flatten())
363+
pos = tuple(ee_pose[:3, 3])
364+
self._gizmo_move.wxyz = wxyz
365+
self._gizmo_move.position = pos
366+
self._gizmo_rotate.wxyz = wxyz
367+
self._gizmo_rotate.position = pos
368+
369+
# Show the active mode's gizmo
370+
if self._gizmo_mode == "move":
371+
self._gizmo_move.visible = True
372+
else:
373+
self._gizmo_rotate.visible = True
324374

325375
if self._ghost is not None:
326376
self._ghost.set_visible(True)
@@ -338,8 +388,8 @@ def _deactivate_teleop(self) -> None:
338388
self._is_teleop_active = False # signals thread to stop
339389
self._controller.deactivate()
340390

341-
if self._gizmo is not None:
342-
self._gizmo.visible = False
391+
self._gizmo_move.visible = False
392+
self._gizmo_rotate.visible = False
343393
if self._ghost is not None:
344394
self._ghost.set_visible(False)
345395

@@ -381,8 +431,8 @@ def _teleop_loop(self) -> None:
381431

382432
# Loop exited (abort, error, or deactivate) — reset panel UI
383433
self._is_teleop_active = False
384-
if self._gizmo is not None:
385-
self._gizmo.visible = False
434+
self._gizmo_move.visible = False
435+
self._gizmo_rotate.visible = False
386436
if self._ghost is not None:
387437
self._ghost.set_visible(False)
388438
self._activate_btn.name = "Activate"

0 commit comments

Comments
 (0)