git.lucas.co / hou-control
SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git

viewer_states/keycam.py (26.8K)

  1 import hou
  2 from hc import HCCam, HCSettings, HCGeo, HCGuides, HCSceneViewer
  3 from hc.hcdrawmode import get_draw_mode_label
  4 
  5 
  6 # Required to pass in hou.SceneViewer object
  7 class State(object):
  8     def __init__(self, state_name, scene_viewer):
  9         self.options = {
 10             'center_on_geo': 1,
 11             'lock_cam': 1,
 12             'reset': 0
 13         }
 14 
 15         # Check for cam node
 16         if not hou.node('/obj/keycam'):
 17             cam_node = hou.node('/obj').createNode('cam')
 18             cam_node.setName('keycam')
 19             cam_node.parm('xOrd').set(0)
 20         self.cam_node = hou.node('/obj/keycam')
 21 
 22         # Get scene viewer
 23         self.scene_viewer = HCSceneViewer(scene_viewer)
 24 
 25         # Capture current viewport camera state BEFORE keycam takes over
 26         viewport = self.scene_viewer.currentViewport()
 27         self._captured_state = HCCam.captureViewportState(viewport)
 28 
 29         # Assign state name
 30         self.state_name = state_name
 31 
 32         # Setup hc cam (this locks and resets to origin)
 33         self.cam = HCCam(self.cam_node, self.scene_viewer)
 34         # Start from a square camera, then fit it to this viewport: the node
 35         # keeps whatever aspect the last session fitted, which may be for a
 36         # pane of a different size.
 37         self.cam.resetAspectRatio()
 38         self.cam.fitAspectRatio()
 39         # Restore captured viewport state so keycam starts where you were
 40         self.cam.initFromState(self._captured_state)
 41         units = HCSettings().keycam('units')
 42         self.cam.delta_r = units.get('delta_r')
 43         self.cam.delta_t = units.get('delta_t')
 44         # HCCam calls its zoom step delta_zoom; the setting is delta_z.
 45         self.cam.delta_zoom = units.get('delta_z')
 46         # drag_sensitivity is read live from settings each event (not cached)
 47         # so changes in HC Settings take effect immediately without reload
 48         self._settings = HCSettings()
 49 
 50 
 51 
 52         # Setup default cam
 53         # self.default_cam = DefaultCam(self, hou_default_cam)
 54 
 55         # Initialize empty objects
 56         self.kwargs = None
 57         self.guides = None
 58         self.hud = None
 59 
 60         # Status message
 61         context = self.scene_viewer.childCat()
 62         context_map = {
 63             'Object': 'Keycam: obj context',
 64             'Sop': 'Keycam: sop context',
 65             'Lop': 'Keycam: lop context'
 66         }
 67         hou.ui.setStatusMessage(context_map[context])
 68 
 69     def onGenerate(self, kwargs):
 70         self.kwargs = kwargs
 71         self.guides = HCGuides(self)
 72         # Prevent exiting state when current node changes
 73         self.kwargs['state_flags']['exit_on_node_select'] = False
 74         # Houdini turns on the current SOP's wireframe when a SOP state
 75         # enters, so a node picked in the network editor is drawn over the
 76         # display node. Keycam only navigates; show the display node alone.
 77         # Houdini restores the flag when the state exits.
 78         self.scene_viewer.hou_tab.showCurrentSopGeometry(False)
 79         self.guides.update()
 80         self.hud = Hud(self)
 81         self.hud.update()
 82 
 83         # Register viewer event callback for resizing
 84         self.scene_viewer.hou_tab.addEventCallback(self._onViewerEvent)
 85 
 86         # Deferred aspect ratio fit -- viewport dims may not be final yet
 87         state = self
 88 
 89         def _deferredInitFit():
 90             state.cam.fitAspectRatio()
 91             hou.ui.removeEventLoopCallback(_deferredInitFit)
 92             return False
 93 
 94         hou.ui.addEventLoopCallback(_deferredInitFit)
 95 
 96     def onDraw(self, kwargs):
 97         self.guides.draw(kwargs)
 98 
 99     def onExit(self, kwargs):
100         # Cleanup callback
101         self.scene_viewer.hou_tab.removeEventCallback(self._onViewerEvent)
102         for viewport in self.scene_viewer.allViewports():
103             viewport.lockCameraToView(False)
104         # Leave the camera square: outside keycam nothing refits it when the
105         # pane resizes, so a fitted aspect only distorts the next look-through.
106         self.cam.resetAspectRatio()
107 
108     def _onViewerEvent(self, **kwargs):
109         event_type = kwargs.get('event_type')
110         if event_type == hou.sceneViewerEvent.SizeChanged:
111             self.cam.fitAspectRatio()
112             self.guides.update()
113         elif event_type == hou.sceneViewerEvent.LayoutChanged:
114             # A layout change (ctrl+1..4, or keycam's own layout actions)
115             # raises SizeChanged and LayoutChanged while the viewports still
116             # have their old size; they are resized one event-loop tick later
117             # with no further event, so fit from there.
118             state = self
119 
120             def _deferredFit():
121                 state.cam.fitAspectRatio()
122                 state.guides.update()
123                 hou.ui.removeEventLoopCallback(_deferredFit)
124                 return False
125 
126             hou.ui.addEventLoopCallback(_deferredFit)
127 
128     def onKeyEvent(self, kwargs):
129         """Keys are now handled via PluginHotkeyDefinitions and onMenuAction."""
130         return False
131 
132     def onMouseEvent(self, kwargs):
133         """Handle click-and-drag for camera rotation."""
134         ui_event = kwargs.get('ui_event')
135         if not ui_event:
136             return False
137 
138         device = ui_event.device()
139         reason = ui_event.reason()
140 
141         # Only respond to left-button drag
142         if not device.isLeftButton():
143             return False
144 
145         # Start drag: record initial position
146         if reason == hou.uiEventReason.Start:
147             self._drag_start = (device.mouseX(), device.mouseY())
148             self._drag_last = self._drag_start
149             self._dragging = True
150             return True
151 
152         # Active drag: rotate based on mouse delta
153         if getattr(self, '_dragging', False) and reason in (hou.uiEventReason.Active, hou.uiEventReason.Changed):
154             mx, my = device.mouseX(), device.mouseY()
155             lx, ly = getattr(self, '_drag_last', (mx, my))
156 
157             dx = mx - lx
158             dy = my - ly
159 
160             if dx != 0 or dy != 0:
161                 # Horizontal movement -> yaw (left/right)
162                 # Vertical movement   -> pitch (up/down)
163                 # Sensitivity 0..1: higher = more degrees per pixel (more responsive)
164                 # Maps to approx 0.03 deg/px (smooth) .. 3.0 deg/px (twitchy)
165                 sensitivity = self._settings.keycam().get('drag_sensitivity', 0.25)
166                 scale = 0.03 + sensitivity * 2.97
167 
168                 if abs(dx) >= 1:
169                     angle_y = dx * scale
170                     self._rotateByDelta(yaw=angle_y)
171 
172                 if abs(dy) >= 1:
173                     angle_x = -dy * scale  # invert so dragging down rotates down
174                     self._rotateByDelta(pitch=angle_x)
175 
176                 self._drag_last = (mx, my)
177                 self.guides.update()
178                 self.hud.update()
179 
180             return True
181 
182         return False
183 
184     def _rotateByDelta(self, pitch=0, yaw=0):
185         """Apply incremental rotation by pitch (x-axis) and/or yaw (y-axis) in degrees."""
186         self.cam._syncFromCameraNode()
187         if pitch != 0:
188             m = hou.hmath.buildRotateAboutAxis(self.cam.local_x, pitch)
189             self.cam.t -= self.cam.p
190             self.cam.t *= m
191             self.cam.t += self.cam.p
192             self.cam.local_x *= m
193             self.cam.local_y *= m
194             self.cam.local_z *= m
195             self.cam.r += hou.Vector3(pitch, 0, 0)
196         if yaw != 0:
197             m = hou.hmath.buildRotateAboutAxis(self.cam.global_y, yaw)
198             self.cam.t -= self.cam.p
199             self.cam.t *= m
200             self.cam.t += self.cam.p
201             self.cam.local_x *= m
202             self.cam.local_y *= m
203             self.cam.local_z *= m
204             self.cam.r += hou.Vector3(0, yaw, 0)
205 
206     def onKeyboardEvent(self, kwargs):
207         return self.onKeyEvent(kwargs)
208 
209     def onMenuAction(self, kwargs):
210         menu_item = kwargs['menu_item']
211 
212         if menu_item in ('zoom_persp_in', 'zoom_persp_out', 'zoom_ortho_in', 'zoom_ortho_out'):
213             if self.cam.projection == 'ortho':
214                 swap_map = {
215                     'zoom_persp_in': 'zoom_ortho_in',
216                     'zoom_persp_out': 'zoom_ortho_out',
217                     'zoom_ortho_in': 'zoom_persp_in',
218                     'zoom_ortho_out': 'zoom_persp_out',
219                 }
220                 menu_item = swap_map[menu_item]
221 
222         action_map = {
223             'frame':          self.cam.frame,
224             'reset':          self.cam.reset,
225             'reset_aspect_ratio': self.cam.resetAspectRatio,
226             'rotate_left':    self.cam.rotateLeft,
227             'rotate_right':   self.cam.rotateRight,
228             'rotate_up':      self.cam.rotateUp,
229             'rotate_down':    self.cam.rotateDown,
230             'trans_left':     self.cam.translateLeft,
231             'trans_right':    self.cam.translateRight,
232             'trans_up':       self.cam.translateUp,
233             'trans_down':     self.cam.translateDown,
234             'toggle_proj':    self.cam.toggleProjection,
235             'cycle_layout':   self.scene_viewer.nextLayout,
236             'set_layout_single':           lambda: self._setLayout('Single'),
237             'set_layout_doubleside':       lambda: self._setLayout('DoubleSide'),
238             'set_layout_doublestack':      lambda: self._setLayout('DoubleStack'),
239             'set_layout_quad':             lambda: self._setLayout('Quad'),
240             'set_layout_quadbottomsplit':  lambda: self._setLayout('QuadBottomSplit'),
241             'set_layout_quadleftsplit':    lambda: self._setLayout('QuadLeftSplit'),
242             'set_layout_triplebottomsplit':lambda: self._setLayout('TripleBottomSplit'),
243             'set_layout_tripleleftsplit':  lambda: self._setLayout('TripleLeftSplit'),
244             'zoom_persp_in':  lambda: self.cam.zoom('in'),
245             'zoom_persp_out': lambda: self.cam.zoom('out'),
246             'zoom_ortho_in':  lambda: self.cam.zoomOrtho('in'),
247             'zoom_ortho_out': lambda: self.cam.zoomOrtho('out'),
248             'cycle_draw_mode': self.scene_viewer.cycleDrawMode,
249             'view_top':       lambda: self._setView('top'),
250             'view_bottom':    lambda: self._setView('bottom'),
251             'view_front':     lambda: self._setView('front'),
252             'view_back':      lambda: self._setView('back'),
253             'view_left':      lambda: self._setView('left'),
254             'view_right':     lambda: self._setView('right'),
255         }
256 
257         if menu_item in action_map:
258             res = action_map[menu_item]()
259             self.guides.update()
260             self.hud.update()
261             return res
262         
263         # Handle Guide toggles
264         if hasattr(self.guides, menu_item):
265             # Update the local attribute
266             setattr(self.guides, menu_item, kwargs[menu_item])
267             self.guides.update()
268             return True
269             
270         return False
271 
272     def _setView(self, view_name):
273         self.cam.view = view_name
274         self.cam.setView()
275         return True
276 
277     def _setLayout(self, layout_name):
278         # The aspect refit rides on the LayoutChanged event (_onViewerEvent).
279         self.scene_viewer.setLayout(layout_name)
280         return True
281 
282     def onMenuPreOpen(self, kwargs):
283         menu_id = kwargs['menu']
284         menu_item_states = kwargs['menu_item_states']
285         guides = self.guides
286 
287         # Set checkbox states when opening guide menu
288         if menu_id == 'guide_menu':
289             menu_item_states['cam_axis']['value'] = bool(guides.cam_axis)
290             menu_item_states['pivot_axis']['value'] = bool(guides.pivot_axis)
291             menu_item_states['bbox']['value'] = bool(guides.bbox)
292             menu_item_states['perim']['value'] = bool(guides.perim)
293             menu_item_states['pivot_2d']['value'] = bool(guides.pivot_2d)
294             menu_item_states['pivot_3d']['value'] = bool(guides.pivot_3d)
295             menu_item_states['ray']['value'] = bool(guides.ray)
296 
297     def onParmChangeEvent(self, kwargs):
298         parm_map = {
299             't':          (self.cam, 't'),
300             'p':          (self.cam, 'p'),
301             'r':          (self.cam, 'r'),
302             'ow':         (self.cam, 'ow'),
303             'zoom':       (self.cam, 'zoom'),
304             'target':     (self.cam, 'target'),
305             'delta_r':    (self.cam, 'delta_r'),
306             'delta_t':    (self.cam, 'delta_t'),
307             'delta_zoom': (self.cam, 'delta_zoom'),
308             # 'delta_ow':   (self.cam, 'delta_ow'),
309             # 'layout':     (self.cam, 'layout'),
310             'viewport':   (self.cam, 'viewport'),
311             # 'view':       (self.cam, 'view'),
312             # 'camera':     (self.cam, 'camera'),
313         }
314         obj = parm_map[kwargs['parm_name']][0]
315         attr = parm_map[kwargs['parm_name']][1]
316         setattr(obj, attr, kwargs['parm_value'])
317         # self.guides.update
318 
319 
320 class Hud:
321     def __init__(self, state):
322         self.state = state
323         self.scene_viewer = state.scene_viewer
324         self.template = {
325             'title': 'Keycam',
326             'rows': [
327                 {
328                     'id':    'draw_mode',
329                     'type':  'plain',
330                     'label': 'Draw',
331                     'value': 'Wireframe',
332                     'key':   'Ctrl + D'
333                 },
334                 {
335                     'id':    'layout',
336                     'type':  'plain',
337                     'label': 'Layout',
338                     'value': 'Single',
339                     'key':   'Ctrl + L'
340                 },
341                 {
342                     'id':    'layout_g',
343                     'type':  'choicegraph',
344                     'count': 8
345                 },
346                 {
347                     'id':    'viewport',
348                     'type':  'plain',
349                     'label': 'Viewport',
350                     'value': '0',
351                     'key':   'Ctrl + V'
352                 },
353                 {
354                     'id':    'viewport_g',
355                     'type':  'choicegraph',
356                     'count': 4
357                 },
358                 {
359                     'id':    'view',
360                     'type':  'plain',
361                     'label': 'View',
362                     'value': 'Perspective',
363                     'key':   'V'
364                 },
365                 {
366                     'id':    'view_g',
367                     'type':  'choicegraph',
368                     'count': 8
369                 },
370                 {
371                     'id':    'target',
372                     'type':  'plain',
373                     'label': 'Target',
374                     'value': 'Camera',
375                     'key':   'T'
376                 },
377                 {
378                     'id':    'target_g',
379                     'type':  'choicegraph',
380                     'count': 2
381                 },
382                 {
383                     'id':    'vis',
384                     'type':  'plain',
385                     'label': 'Vis'
386                 },
387                 {
388                     'id':    'focus',
389                     'type':  'plain',
390                     'label': 'Focus',
391                     'value': 0
392                 },
393                 {
394                     'id':    'focus_g',
395                     'type':  'choicegraph',
396                     'count': 10
397                 }
398             ]
399         }
400 
401     def _draw_mode_label(self):
402         if hasattr(self.scene_viewer, 'drawModeLabel'):
403             return self.scene_viewer.drawModeLabel()
404 
405         for display_set in self.scene_viewer.allDisplaySets():
406             return get_draw_mode_label(display_set.shadedMode().name())
407 
408         return get_draw_mode_label(None)
409 
410     def update(self):
411         layout_map = {
412             'geometryViewportLayout.DoubleSide':        2,
413             'geometryViewportLayout.DoubleStack':       2,
414             'geometryViewportLayout.Quad':              4,
415             'geometryViewportLayout.QuadBottomSplit':   4,
416             'geometryViewportLayout.QuadLeftSplit':     4,
417             'geometryViewportLayout.TripleBottomSplit': 3,
418             'geometryViewportLayout.TripleLeftSplit':   3,
419             'geometryViewportLayout.Single':            1,
420         }
421         layout = self.scene_viewer.layout()
422         n_viewports = layout_map.get(str(layout), 1)
423         self.template['rows'][4]['count'] = n_viewports
424 
425         # Updates
426         hou_scene_viewer = self.scene_viewer.hou_tab
427         hou_scene_viewer.hudInfo(template=self.template)
428         updates = {
429             'layout':    str(self.scene_viewer.layout())[23:],
430             'layout_g':  self.scene_viewer.layouts().index(self.scene_viewer.layout()),
431             'draw_mode': self._draw_mode_label(),
432         }
433         hou_scene_viewer.hudInfo(hud_values=updates)
434 
435 
436 def createViewerStateTemplate():
437     template = hou.ViewerStateTemplate(
438         type_name='keycam', label='keycam',
439         # SOP is the home category: showCurrentSopGeometry() is a no-op in a
440         # state registered under Object, even when SOP is an extra context.
441         category=hou.sopNodeTypeCategory(),
442         contexts=[hou.objNodeTypeCategory()]
443     )
444 
445     template.bindFactory(State)
446     template.bindIcon('DESKTOP_application_sierra')
447 
448     # Define Commands and Default Bindings
449     hotkeys = hou.PluginHotkeyDefinitions()
450     ctx = 'h.pane.gview.state.sop.keycam'
451     hotkeys.addContext(ctx, 'HC Keycam', 'Hotkeys for the HC Keycam viewer state')
452     
453     commands = {
454         'rotate_left':   ('Rotate Left',   ['h']),
455         'rotate_right':  ('Rotate Right',  ['l']),
456         'rotate_up':     ('Rotate Up',     ['k']),
457         'rotate_down':   ('Rotate Down',   ['j']),
458         'trans_left':    ('Translate Left', ['Shift+h']),
459         'trans_right':   ('Translate Right',['Shift+l']),
460         'trans_up':      ('Translate Up',   ['Shift+k']),
461         'trans_down':    ('Translate Down', ['Shift+j']),
462         'frame':         ('Frame',         ['f']),
463         'toggle_proj':   ('Toggle Projection', ['o']),
464         'cycle_layout':  ('Cycle Layout',      ['Ctrl+l']),
465         'zoom_persp_in': ('Zoom In (Persp)',   ['=']),
466         'zoom_persp_out':('Zoom Out (Persp)',  ['-']),
467         'zoom_ortho_in': ('Zoom In (Ortho)',   ['Shift+=']),
468         'zoom_ortho_out':('Zoom Out (Ortho)',  ['Shift+-']),
469         'cycle_draw_mode': ('Cycle Draw Mode', ['Ctrl+d']),
470         'view_top':      ('Top View',          ['1']),
471         'view_bottom':   ('Bottom View',       ['2']),
472         'view_front':    ('Front View',        ['3']),
473         'view_back':     ('Back View',         ['4']),
474         'view_left':     ('Left View',         ['5']),
475         'view_right':    ('Right View',        ['6']),
476         'reset':         ('Reset',             ['r']),
477         'reset_aspect_ratio': ('Reset Aspect Ratio', []),
478         'set_layout_single':           ('Single',           []),
479         'set_layout_doubleside':       ('DoubleSide',       []),
480         'set_layout_doublestack':      ('DoubleStack',      []),
481         'set_layout_quad':             ('Quad',             []),
482         'set_layout_quadbottomsplit':  ('QuadBottomSplit',  []),
483         'set_layout_quadleftsplit':    ('QuadLeftSplit',    []),
484         'set_layout_triplebottomsplit':('TripleBottomSplit', []),
485         'set_layout_tripleleftsplit':  ('TripleLeftSplit',  []),
486     }
487     
488     for cmd_id, (label, defaults) in commands.items():
489         symbol = f"{ctx}.{cmd_id}"
490         hotkeys.addCommand(symbol, label, "")
491         hotkeys.addDefaultBinding(ctx, symbol, defaults)
492     
493     template.bindHotkeyDefinitions(hotkeys)
494 
495     # Parameters
496 
497     template.bindParameter(
498         hou.parmTemplateType.Menu,
499         name='layout',
500         label='Layout',
501         default_value='single',
502         menu_items=[
503             ('doubleside',        'DoubleSide'),
504             ('doublestack',       'DoubleStack'),
505             ('quad',              'Quad'),
506             ('quadbottomsplit',   'QuadBottomSplit'),
507             ('quadleftsplit',     'QuadLeftSplit'),
508             ('single',            'Single'),
509             ('triplebottomsplit', 'TripleBottomSplit'),
510             ('tripleleftsplit',   'TripleLeftSplit')
511         ]
512     )
513 
514     template.bindParameter(
515         hou.parmTemplateType.Menu,
516         name='viewport',
517         label='Viewport',
518         default_value='center',
519         menu_items=[('center', 'Center')]
520     )
521 
522     template.bindParameter(
523         hou.parmTemplateType.Menu,
524         name='view',
525         label='View',
526         default_value='persp',
527         menu_items=[
528             ('persp',  'Perspective'),
529             ('top',    'Top'),
530             ('front',  'Front'),
531             ('right',  'Right'),
532             ('uv',     'UV'),
533             ('bottom', 'Bottom'),
534             ('back',   'Back'),
535             ('left',   'Left')
536         ]
537     )
538 
539     template.bindParameter(
540         hou.parmTemplateType.Menu,
541         name='camera',
542         label='Camera',
543         default_value='keycam',
544         menu_items=[
545             ('keycam',  'Keycam'),
546             ('default', 'Default'),
547             ('other',   'Other')
548         ],
549         toolbox=False
550     )
551 
552     template.bindParameter(
553         hou.parmTemplateType.Menu,
554         name='target',
555         label='Target',
556         default_value='cam',
557         menu_items=[
558             ('cam',   'Camera'),
559             ('pivot', 'Pivot')
560         ],
561         toolbox=False
562     )
563 
564     template.bindParameter(
565         hou.parmTemplateType.Separator,
566         toolbox=False
567     )
568 
569     template.bindParameter(
570         hou.parmTemplateType.Float,
571         name='t',
572         label='Translation',
573         num_components=3,
574         toolbox=False
575     )
576 
577     # template.bindParameter(
578     #     hou.parmTemplateType.Float,
579     #     name='r',
580     #     label='Rotation',
581     #     num_components=3,
582     #     toolbox=False
583     # )
584 
585     template.bindParameter(
586         hou.parmTemplateType.Float,
587         name='p',
588         label='Pivot',
589         num_components=3,
590         toolbox=False
591     )
592 
593     template.bindParameter(
594         hou.parmTemplateType.Float,
595         name='zoom',
596         label='Zoom',
597         num_components=1,
598         toolbox=False
599     )
600 
601     template.bindParameter(
602         hou.parmTemplateType.Float,
603         name='ow',
604         label='Ortho Width',
605         num_components=1,
606         toolbox=False
607     )
608 
609     template.bindParameter(
610         hou.parmTemplateType.Separator,
611         toolbox=False
612     )
613 
614     template.bindParameter(
615         hou.parmTemplateType.Float,
616         name='delta_t',
617         label='Delta T',
618         default_value=1.0,
619         min_limit=0,
620         max_limit=10.0
621     )
622 
623     template.bindParameter(
624         hou.parmTemplateType.Float,
625         name='delta_r',
626         label='Delta R',
627         default_value=15.0,
628         min_limit=-180.0,
629         max_limit=180.0
630     )
631 
632     template.bindParameter(
633         hou.parmTemplateType.Float,
634         name='delta_zoom',
635         label='Delta Zoom',
636         default_value=1.0,
637         min_limit=0,
638         max_limit=10.0
639     )
640 
641     template.bindParameter(
642         hou.parmTemplateType.Float,
643         name='delta_ow',
644         label='Delta OW',
645         default_value=1.0,
646         min_limit=0,
647         max_limit=10.0
648     )
649 
650     """ Context Menu """
651 
652     menu = hou.ViewerStateMenu('keycam_menu', 'Keycam Menu')
653     ctx = 'h.pane.gview.state.sop.keycam'
654     
655     menu.addActionItem('frame', 'Frame', hotkey=f'{ctx}.frame')
656     menu.addActionItem('reset', 'Reset', hotkey=f'{ctx}.reset')
657     menu.addActionItem('reset_aspect_ratio', 'Reset Aspect Ratio', hotkey=f'{ctx}.reset_aspect_ratio')
658 
659     set_view_menu = hou.ViewerStateMenu('set_view_menu', 'Set View')
660     set_view_menu.addActionItem('view_top',    'Top',    hotkey=f'{ctx}.view_top')
661     set_view_menu.addActionItem('view_bottom', 'Bottom', hotkey=f'{ctx}.view_bottom')
662     set_view_menu.addActionItem('view_front',  'Front',  hotkey=f'{ctx}.view_front')
663     set_view_menu.addActionItem('view_back',   'Back',   hotkey=f'{ctx}.view_back')
664     set_view_menu.addActionItem('view_left',   'Left',   hotkey=f'{ctx}.view_left')
665     set_view_menu.addActionItem('view_right',  'Right',  hotkey=f'{ctx}.view_right')
666     menu.addMenu(set_view_menu)
667 
668     guide_menu = hou.ViewerStateMenu('guide_menu', 'Guides')
669     guide_menu.addToggleItem('bbox',       'Bbox',        0)
670     guide_menu.addToggleItem('cam_axis',   'Camera Axis', 0)
671     guide_menu.addToggleItem('pivot_axis', 'Pivot Axis',  0)
672     guide_menu.addToggleItem('perim',      'Perimeter',   0)
673     guide_menu.addToggleItem('pivot_2d',   '2D Pivot',    0)
674     guide_menu.addToggleItem('pivot_3d',   '3D Pivot',    0)
675     guide_menu.addToggleItem('ray',        'Ray',         0)
676     menu.addMenu(guide_menu)
677 
678     layout_menu = hou.ViewerStateMenu('layout_menu', 'Layout')
679     layout_menu.addActionItem('cycle_layout',  'Cycle Layout', hotkey=f'{ctx}.cycle_layout')
680     layout_menu.addSeparator()
681     layout_menu.addActionItem('set_layout_single',           'Single',           hotkey=f'{ctx}.set_layout_single')
682     layout_menu.addActionItem('set_layout_doubleside',       'DoubleSide',       hotkey=f'{ctx}.set_layout_doubleside')
683     layout_menu.addActionItem('set_layout_doublestack',      'DoubleStack',      hotkey=f'{ctx}.set_layout_doublestack')
684     layout_menu.addActionItem('set_layout_quad',             'Quad',             hotkey=f'{ctx}.set_layout_quad')
685     layout_menu.addActionItem('set_layout_quadbottomsplit',  'QuadBottomSplit',  hotkey=f'{ctx}.set_layout_quadbottomsplit')
686     layout_menu.addActionItem('set_layout_quadleftsplit',    'QuadLeftSplit',    hotkey=f'{ctx}.set_layout_quadleftsplit')
687     layout_menu.addActionItem('set_layout_triplebottomsplit','TripleBottomSplit', hotkey=f'{ctx}.set_layout_triplebottomsplit')
688     layout_menu.addActionItem('set_layout_tripleleftsplit',  'TripleLeftSplit',  hotkey=f'{ctx}.set_layout_tripleleftsplit')
689     menu.addMenu(layout_menu)
690 
691     menu.addToggleItem('toggle_proj', 'Toggle Projection', 0, hotkey=f'{ctx}.toggle_proj')
692     menu.addActionItem('cycle_draw_mode', 'Cycle Draw Mode', hotkey=f'{ctx}.cycle_draw_mode')
693     
694     # We add the remaining hotkey actions to a submenu to keep them registered
695     # but out of the main menu if desired.
696     nav_menu = hou.ViewerStateMenu('nav_menu', 'Navigation')
697     nav_menu.addActionItem('rotate_left',   'Rotate Left',    hotkey=f'{ctx}.rotate_left')
698     nav_menu.addActionItem('rotate_right',  'Rotate Right',   hotkey=f'{ctx}.rotate_right')
699     nav_menu.addActionItem('rotate_up',     'Rotate Up',      hotkey=f'{ctx}.rotate_up')
700     nav_menu.addActionItem('rotate_down',   'Rotate Down',    hotkey=f'{ctx}.rotate_down')
701     nav_menu.addSeparator()
702     nav_menu.addActionItem('trans_left',    'Translate Left',  hotkey=f'{ctx}.trans_left')
703     nav_menu.addActionItem('trans_right',   'Translate Right', hotkey=f'{ctx}.trans_right')
704     nav_menu.addActionItem('trans_up',      'Translate Up',    hotkey=f'{ctx}.trans_up')
705     nav_menu.addActionItem('trans_down',    'Translate Down',  hotkey=f'{ctx}.trans_down')
706     nav_menu.addSeparator()
707     nav_menu.addActionItem('zoom_persp_in',  'Zoom In (Persp)',  hotkey=f'{ctx}.zoom_persp_in')
708     nav_menu.addActionItem('zoom_persp_out', 'Zoom Out (Persp)', hotkey=f'{ctx}.zoom_persp_out')
709     nav_menu.addActionItem('zoom_ortho_in',  'Zoom In (Ortho)',  hotkey=f'{ctx}.zoom_ortho_in')
710     nav_menu.addActionItem('zoom_ortho_out', 'Zoom Out (Ortho)', hotkey=f'{ctx}.zoom_ortho_out')
711     
712     menu.addMenu(nav_menu)
713 
714     menu.addSeparator()
715 
716     template.bindMenu(menu)
717 
718     # Ok
719     return template