@@ -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