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