SideFX Houdini customization package
git clone https://git.lucas.co/hou-control.git
python3.13libs/hc/hcsceneviewer.py (13.9K)
1 import hou, re, types
2 from .hcdrawmode import DRAW_MODE_LABELS, DRAW_MODE_SEQUENCE, get_draw_mode_label, next_draw_mode
3 from .hcpathtab import HCPathTab
4 from .hcviewport import HCViewport
5 from .hccommands import command
6
7
8 def _hcPrintSceneViewerEvent(**kwargs):
9 event_type = kwargs.get("event_type")
10 viewer = kwargs.get("viewer")
11 viewer_name = viewer.name() if viewer is not None else "?"
12 extras = {k: v for k, v in kwargs.items() if k not in ("event_type", "desktop", "viewer")}
13 extras_str = " ".join(f"{k}={v!r}" for k, v in extras.items())
14 print(f"[HCSceneViewer {viewer_name}] {event_type} {extras_str}".rstrip())
15
16 # How each hou.geometryViewportLayout tiles the tab, as a split tree over
17 # hou.SceneViewer.viewports() slots (0 Right, 1 Front, 2 Top, 3 Perspective
18 # by default; a layout shows slots, whatever type they have been changed
19 # to). ('h', ...) lays its children out left to right, ('v', ...) top to
20 # bottom; leaves read in the natural order. Read off screenshots, since
21 # hou.GeometryViewport.size() reports every viewport at (0, 0).
22 LAYOUT_CELLS = {
23 "Single": 3,
24 "DoubleSide": ("h", 2, 3),
25 "DoubleStack": ("v", 3, 0),
26 "Quad": ("v", ("h", 2, 3), ("h", 1, 0)),
27 "QuadBottomSplit": ("v", 3, ("h", 2, 1, 0)),
28 "QuadLeftSplit": ("h", ("v", 2, 1, 0), 3),
29 "TripleBottomSplit": ("v", 3, ("h", 1, 0)),
30 "TripleLeftSplit": ("h", ("v", 2, 1), 3),
31 }
32
33 # hou.geometryViewportLayout names, in the order nextLayout() cycles them.
34 LAYOUT_NAMES = (
35 "DoubleSide",
36 "DoubleStack",
37 "Quad",
38 "QuadBottomSplit",
39 "QuadLeftSplit",
40 "TripleBottomSplit",
41 "TripleLeftSplit",
42 "Single",
43 )
44
45
46 def _spaced(name):
47 """'QuadBottomSplit' -> 'Quad Bottom Split'."""
48 return re.sub(r"(?<=[a-z])(?=[A-Z])", " ", name)
49
50
51 # (panel label, value) pairs for the two scene viewer dropdowns.
52 LAYOUTS = tuple((_spaced(name), name) for name in LAYOUT_NAMES)
53 DRAW_MODES = tuple((DRAW_MODE_LABELS[mode], mode) for mode in DRAW_MODE_SEQUENCE)
54
55
56 class HCSceneViewer(HCPathTab):
57 def __init__(self, hou_tab):
58 self.hou_tab = hou_tab
59
60
61 """ Geometry """
62
63 def allDisplaySets(self):
64 displaySets = []
65 for hc_viewport in self.allViewports():
66 settings = hc_viewport.settings()
67 displaySet = settings.displaySet(hou.displaySetType.DisplayModel)
68 displaySets.append(displaySet)
69 return displaySets
70
71 # The display-set toggles all follow one rule: if any viewport shows the
72 # thing, hide it everywhere; otherwise show it everywhere. Each used to
73 # spell that out by hand, so the panel's checkbox now reads the same
74 # "any viewport" answer the toggle acts on.
75
76 def _anyDisplaySetShowing(self, getter):
77 return any(getattr(ds, getter)() for ds in self.allDisplaySets())
78
79 def _showOnAllDisplaySets(self, setter, value):
80 for display_set in self.allDisplaySets():
81 getattr(display_set, setter)(value)
82
83 def _toggleDisplaySets(self, getter, setter):
84 self._showOnAllDisplaySets(setter, not self._anyDisplaySetShowing(getter))
85
86 def isShowingBackfaces(self):
87 return self._anyDisplaySetShowing("isShowingPrimBackfaces")
88
89 @command("Backfaces", state="isShowingBackfaces")
90 def toggleBackface(self):
91 self._toggleDisplaySets("isShowingPrimBackfaces", "showPrimBackfaces")
92
93 def isShowingPointMarkers(self):
94 return self._anyDisplaySetShowing("isShowingPointMarkers")
95
96 @command("Point Markers", state="isShowingPointMarkers")
97 def togglePointMarkers(self):
98 self._toggleDisplaySets("isShowingPointMarkers", "showPointMarkers")
99
100 def isShowingPointNormals(self):
101 return self._anyDisplaySetShowing("isShowingPointNormals")
102
103 @command("Point Normals", state="isShowingPointNormals")
104 def togglePointNormals(self):
105 self._toggleDisplaySets("isShowingPointNormals", "showPointNormals")
106
107 def isShowingPointNumbers(self):
108 return self._anyDisplaySetShowing("isShowingPointNumbers")
109
110 @command("Point Numbers", state="isShowingPointNumbers")
111 def togglePointNumbers(self):
112 self._toggleDisplaySets("isShowingPointNumbers", "showPointNumbers")
113
114 def isShowingPrimNormals(self):
115 return self._anyDisplaySetShowing("isShowingPrimNormals")
116
117 @command("Prim Normals", state="isShowingPrimNormals")
118 def togglePrimNormals(self):
119 self._toggleDisplaySets("isShowingPrimNormals", "showPrimNormals")
120
121 def isShowingPrimNumbers(self):
122 return self._anyDisplaySetShowing("isShowingPrimNumbers")
123
124 @command("Prim Numbers", state="isShowingPrimNumbers")
125 def togglePrimNumbers(self):
126 self._toggleDisplaySets("isShowingPrimNumbers", "showPrimNumbers")
127
128 def isShowingVectors(self):
129 return any(v.settings().vectorScale() != 0 for v in self.allViewports())
130
131 @command("Vectors", state="isShowingVectors")
132 def toggleVectors(self):
133 scale = 0 if self.isShowingVectors() else 1
134 for viewport in self.allViewports():
135 viewport.settings().setVectorScale(scale)
136
137 @command("Draw Mode", state="drawMode", choices=DRAW_MODES)
138 def setDrawMode(self, mode_name):
139 mode = getattr(hou.glShadingType, mode_name)
140 for display_set in self.allDisplaySets():
141 display_set.setShadedMode(mode)
142
143 def cycleDrawMode(self):
144 self.setDrawMode(next_draw_mode(self.drawMode()))
145
146 def drawMode(self):
147 for display_set in self.allDisplaySets():
148 return display_set.shadedMode().name()
149
150 return None
151
152 def drawModeLabel(self):
153 return get_draw_mode_label(self.drawMode())
154
155 """ Grids """
156
157 def referencePlane(self):
158 return self.hou_tab.referencePlane()
159
160 def isGridVisible(self):
161 return self.referencePlane().isVisible()
162
163 @command("Grid", state="isGridVisible")
164 def toggleGrid(self):
165 self.referencePlane().setIsVisible(not self.isGridVisible())
166
167
168 """ Layout """
169
170 def layout(self):
171 return self.hou_tab.viewportLayout()
172
173 def layoutName(self):
174 return self.layout().name()
175
176 def layouts(self):
177 return tuple(getattr(hou.geometryViewportLayout, name) for name in LAYOUT_NAMES)
178
179 def nextLayout(self):
180 layouts = self.layouts()
181 layout = self.layout()
182 try:
183 index = layouts.index(layout)
184 except ValueError:
185 index = 0
186 index = (index+1) % len(layouts)
187 self.setLayout(layouts[index])
188 return
189
190 @command("Viewport Layout", state="layoutName", choices=LAYOUTS)
191 def setLayout(self, layout):
192 if isinstance(layout, str):
193 layout = getattr(hou.geometryViewportLayout, layout, layout)
194 self.hou_tab.setViewportLayout(layout)
195
196
197 """ UI """
198
199 def isVisibleDisplayBar(self):
200 return self.hou_tab.isShowingDisplayOptionsBar()
201
202 def isVisibleGroupList(self):
203 return self.hou_tab.isGroupListVisible()
204
205 def isVisibleOperationBar(self):
206 return self.hou_tab.isShowingOperationBar()
207
208 def isVisibleSelectionBar(self):
209 return self.hou_tab.isShowingSelectionBar()
210
211 def isAnyBarVisible(self):
212 return bool(
213 self.isVisibleOperationBar()
214 or self.isVisibleDisplayBar()
215 or self.isVisibleSelectionBar()
216 )
217
218 def showDisplayBar(self, value):
219 self.hou_tab.showDisplayOptionsBar(value)
220
221 def showGroupList(self, value):
222 self.hou_tab.setGroupListVisible(value)
223
224 def showOperationBar(self, value):
225 self.hou_tab.showOperationBar(value)
226
227 def showSelectionBar(self, value):
228 self.hou_tab.showSelectionBar(value)
229
230 @command("Display Bar", state="isVisibleDisplayBar")
231 def toggleDisplayBar(self):
232 self.showDisplayBar(not self.isVisibleDisplayBar())
233
234 @command("Group List", state="isVisibleGroupList")
235 def toggleGroupList(self):
236 self.showGroupList(not self.isVisibleGroupList())
237
238 @command("Operation Bar", state="isVisibleOperationBar")
239 def toggleOperationBar(self):
240 self.showOperationBar(not self.isVisibleOperationBar())
241
242 @command("Selection Bar", state="isVisibleSelectionBar")
243 def toggleSelectionBar(self):
244 self.showSelectionBar(not self.isVisibleSelectionBar())
245
246 def isChromeVisible(self):
247 return bool(super().isChromeVisible() or self.isAnyBarVisible())
248
249 def showChrome(self, visible):
250 super().showChrome(visible)
251 self.showOperationBar(visible)
252 self.showDisplayBar(visible)
253 self.showSelectionBar(visible)
254
255 @command("Bars", state="isAnyBarVisible")
256 def toggleBars(self):
257 show = not self.isAnyBarVisible()
258 self.showOperationBar(show)
259 self.showDisplayBar(show)
260 self.showSelectionBar(show)
261
262 @command("Close Scene Viewer Toolbars")
263 def closeToolbars(self):
264 """Hide all currently visible toolbars/bars in this scene viewer pane."""
265 closed = 0
266
267 if self.hou_tab.isShowingOperationBar():
268 self.hou_tab.showOperationBar(False)
269 closed += 1
270
271 if self.hou_tab.isShowingDisplayOptionsBar():
272 self.hou_tab.showDisplayOptionsBar(False)
273 closed += 1
274
275 if self.hou_tab.isShowingSelectionBar():
276 self.hou_tab.showSelectionBar(False)
277 closed += 1
278
279 if hasattr(self.hou_tab, 'isShowingColorCorrectionBar') and self.hou_tab.isShowingColorCorrectionBar():
280 self.hou_tab.showColorCorrectionBar(False)
281 closed += 1
282
283 if hasattr(self.hou_tab, 'isShowingMemoryBar') and self.hou_tab.isShowingMemoryBar():
284 self.hou_tab.showMemoryBar(False)
285 closed += 1
286
287 hou.ui.setStatusMessage('Closed {} scene viewer toolbar{}'.format(
288 closed,
289 '' if closed == 1 else 's'
290 ), hou.severityType.Message)
291
292
293 """ Events """
294
295 def addEventListener(self):
296 if _hcPrintSceneViewerEvent not in self.hou_tab.eventCallbacks():
297 self.hou_tab.addEventCallback(_hcPrintSceneViewerEvent)
298
299 def removeEventListener(self):
300 if _hcPrintSceneViewerEvent in self.hou_tab.eventCallbacks():
301 self.hou_tab.removeEventCallback(_hcPrintSceneViewerEvent)
302
303 def isListening(self):
304 return _hcPrintSceneViewerEvent in self.hou_tab.eventCallbacks()
305
306 @command("Event Listener", state="isListening")
307 def toggleEventListener(self):
308 if self.isListening():
309 self.removeEventListener()
310 else:
311 self.addEventListener()
312
313
314 """ Utils """
315
316 def type(self):
317 return "HCSceneViewer"
318
319 @command("Keycam")
320 def keycam(self):
321 contexts = ('Object', 'Sop', "Lop")
322 context = self.childCat()
323 if context in contexts:
324 self.hou_tab.setCurrentState('keycam')
325 else:
326 hou.ui.setStatusMessage("No obj, sop or lop context", hou.severityType.Error)
327
328
329 """ Viewports """
330
331 @command("Home Viewports")
332 def homeAllViewports(self):
333 for viewport in self.allViewports():
334 viewport.home()
335
336 def currentViewport(self):
337 return self.hou_tab.curViewport()
338
339 def allViewports(self):
340 viewports = []
341 for viewport in self.hou_tab.viewports():
342 viewports.append(HCViewport(viewport))
343 return viewports
344
345 def viewportRects(self):
346 """(hou.GeometryViewport, QRect) for each viewport the current layout
347 shows, in reading order, in the screen space of qtScreenGeometry().
348
349 Only a viewport's width and height are trusted; where a cell sits is
350 the layout's business (LAYOUT_CELLS), and the splitter gaps are
351 whatever the tab has left over, shared equally.
352 """
353 from PySide6.QtCore import QRect
354 slots = self.hou_tab.viewports()
355
356 def extent(node, axis):
357 while isinstance(node, tuple):
358 node = node[1]
359 return slots[node].size()[2 + axis]
360
361 def place(node, rect, out):
362 if not isinstance(node, tuple):
363 out.append((slots[node], rect))
364 return
365 axis = 0 if node[0] == "h" else 1
366 kids = node[1:]
367 sizes = [extent(kid, axis) for kid in kids]
368 total = rect.width() if axis == 0 else rect.height()
369 gap = max(total - sum(sizes), 0) / max(len(kids) - 1, 1)
370 pos = rect.x() if axis == 0 else rect.y()
371 for kid, size in zip(kids, sizes):
372 if axis == 0:
373 cell = QRect(round(pos), rect.y(), size, rect.height())
374 else:
375 cell = QRect(rect.x(), round(pos), rect.width(), size)
376 place(kid, cell, out)
377 pos += size + gap
378
379 out = []
380 place(LAYOUT_CELLS[self.layoutName()], self.hou_tab.qtScreenGeometry(), out)
381 return out
382
383 def _cycleViewport(self, step):
384 """Warp the pointer to the centre of the viewport ``step`` cells on
385 from the one under it (or the current one, when it is elsewhere)."""
386 from PySide6.QtGui import QCursor
387 from .hcviewregions import move_pointer
388 cells = self.viewportRects()
389 if not cells:
390 return None
391 pointer = QCursor.pos()
392 index = next((i for i, (_, rect) in enumerate(cells) if rect.contains(pointer)), None)
393 if index is None:
394 current = self.hou_tab.curViewport()
395 index = next((i for i, (vp, _) in enumerate(cells) if vp.name() == current.name()), 0)
396 # Not over the tab: land on that one rather than the one after.
397 step = 0 if step > 0 else step + 1
398 viewport, rect = cells[(index + step) % len(cells)]
399 move_pointer(rect.center())
400 return viewport
401
402 @command("Next Viewport")
403 def focusNextViewport(self):
404 return self._cycleViewport(1)
405
406 @command("Previous Viewport")
407 def focusPrevViewport(self):
408 return self._cycleViewport(-1)
409
410 @command("Frame")
411 def frameAllViewports(self):
412 for viewport in self.allViewports():
413 viewport.frameAll()