git.lucas.co / hou-control
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()