On this page |
|
Python viewer states
Overview
Viewer states control interaction in the viewport. For example, the Rotate tool is a view state. The
Handles tool allows access to the viewer state associated with the current node. Houdini lets you create and register your own custom view states in Python.
A custom state can, for example:
-
Display handles and let the user manipulate the handles to change parameters or settings.
-
Interact with geometry to, for example, let you draw on a surface, or display information about what’s under the mouse cursor.
-
Display guide geometry.
-
Respond to user interaction with state parameters.
-
Let the user select geometry.
-
Display a custom right-click menu
-
Respond to low-level mouse, keyboard, and tablet events.
-
Wrap changes in undo blocks.
-
Run across multiple context-levels.
A Houdini digital asset can specify a state (for the Handles tool to use when a node of that type is current). Eventually you will be able to programmatically enter a state by calling hou.SceneViewer.setCurrentState(), for example in a shelf tool script (for tools that don’t create a node, such as an inspection tool).
To start playing with the code right away, see implementing a state below, which shows how to implement, register, and launch a minimal viewer state.
Tip
To see viewer states in action, check out the demo scene and asset files under $HH/viewer_states/examples
. These samples provide a detailed coverage of the viewer state features.
Limitations
-
Currently Python states are available in the following contexts: SOP (geometry), OBJ, DOP and LOP levels.
-
Handles cannot be bound to nodeless states.
State names
A state has an internal name and a human readable label that appears in the UI. If you create a new custom state, the internal name you choose must be unique: if two authors use the same state name, one of the states will fail to register. If you consider that a state or asset you create might someday be shared with other users/studios or even sold as a product, you should take the time to ensure proper uniqueness.
There is the additional complication that Houdini automatically creates generic states for each asset, so you can’t use the name of an existing node type as a state name.
-
If the state is shared between more than one node, or not tied to a node, you must still ensure that the state name does not conflict with the generic state of any asset. The best way to do this is to incorporate the same namespace string you would use for an asset.
For example, if you work at the Example.com movie studio and use
examplecom::
as the namespace prefix for your assets, when you want to create a "scrub" state, you would use the state nameexamplecom::scrub
. -
State names do not have the same character restrictions as node type names and node names. A state name is more or less an arbitrary string.
-
You may need to use the name as a file/directory name (for example, if you're storing the code in a file)
Tool + state vs. self-contained state
Many, if not most "native" Houdini node states do not handle their own selection or create the node. Instead, they rely on a shelf tool script to ask for a selection and create the node. The state is then simply responsible for displaying handles.
You can use this workflow with a custom Python state as well, especially when the state is closely associated with an asset. You can write a Shelf tool script to ask for a selection and create the node (and filling in the node’s Group field with the selection).
Alternatively, your state can be self contained: it can create a node when invoked from the viewer and have its own selector.
(If your state doesn’t require a node (for example, an inspector-type tool), see writing nodeless state for more information.)
-
If you need to ask for more than one type of selections (for example, "Select some curves" followed by "Select some points on those curves"), use a tool script. Currently, a Python state can only accept a single type of selection.
-
If the code needs to know what’s in the selection before it creates the node, use a tool script.
See working with a node for more information on manipulating a node instance in a state.
Installing a state in Houdini
The most convenient and easiest way of creating and installing a viewer state in Houdini is to use the Viewer State Code Generator dialog. This code generator will get you a fully functional state in no time and will help you understand all the details involved in writing a viewer state from scratch.
There are currently two ways to make a custom state available in Houdini: embed the state’s code with an asset (HDA viewer state), or put a Python file containing the code in the right directory on the Houdini path.
Embedding a state in an asset
For creating HDA viewer states, use the code generator available in the Viewer State Editor tab of the Operator Type Properties window.
The following instructions create an asset and a minimal state so you can try out the code.
-
Start with an asset you want to add a state to. To simplify the example, let’s start by using an OBJ asset.
Note
If you just want to start with a blank asset, you can to create a SOP asset or for an OBJ asset. The viewer state created with the code generator should work for both asset types.
To quickly create a "blank" SOP asset to experiment with:
-
At the Object level, use the ⇥ Tab menu to create a
Geo object.
-
Double-click the
geo1
node to dive into the Geometry network inside. -
Use the ⇥ Tab menu to create a Subnetwork node.
-
Right-click the
subnet1
node and choose Create digital asset. -
Set the Operator name to
statedemo
, the Operator Label toState Demo
, and Save to library toEmbedded
.Setting the library location to
Embedded
saves the asset with the current scene file instead of in an asset library.
To quickly create a "blank" OBJ asset to experiment with:
-
At the Object level, use the ⇥ Tab menu to create a Subnetwork node.
-
Right-click the
subnet1
node and choose Create digital asset. -
Set the Operator name to
statedemo
, the Operator Label toState Demo
, and Save to library toEmbedded
.Setting the library location to
Embedded
saves the asset with the current scene file instead of in an asset library.
-
-
Open the type properties window for the asset. (Right click an instance of the asset type and choose Type properties).
-
Click the Interactive|Viewer State tab.
-
Click the New… button to generate the state code.
-
Select the onMouseEvent event handler and click Accept.
You should now have a functional state listed in the Viewer State Browser tree as Statedemo
.
import hou import viewerstate.utils as su class State(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): """ Process mouse events """ ui_event = kwargs["ui_event"] dev = ui_event.device() self.log("Mouse:", dev.mouseX(), dev.mouseY(), dev.isLeftButton()) # Must return True to consume the event return False def createViewerStateTemplate(): """ Mandatory entry point to create and return the viewer state template to register. """ state_typename = kwargs["type"].definition().sections()["DefaultState"].contents() state_label = "Statedemo" state_cat = hou.objNodeTypeCategory() template = hou.ViewerStateTemplate(state_typename, state_label, state_cat) template.bindFactory(State) template.bindIcon(kwargs["type"].icon()) return template
See implementing a state below for more information on adding functionality to the state class.
To test the state, select the asset in the network editor. Move the mouse into the scene viewer and press Enter. The mouse coordinates should be logged in the Viewer State Browser console as you move the mouse.
Loading a state from the Houdini path
For creating file viewer states, use the code generator in the Viewer State Browser window.
The following instructions create a file viewer state that can be "shared" between multiple assets. File viewer states are automatically registered by Houdini on startup.
-
Open the Viewer State Browser window with the New Pane Tab Type ▸ Viewer State Browser menu.
-
Select the Object category from the toolbar.
-
Open the Viewer State Code Generator with the File ▸ New State… menu.
-
Enter
statedemo
as the state’s name in the Name field. -
Select the onMouseEvent event handler and click Accept.
The new file viewer state should be saved as $HOUDINI_USER_PREF_DIR/viewer_states/statedemo.py
, listed in the Viewer State Browser tree as Statedemo
.
You should now have a functional state listed in the Viewer State Browser tree as Statedemo
.
import hou import viewerstate.utils as su class State(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): """ Process mouse events """ ui_event = kwargs["ui_event"] dev = ui_event.device() self.log("Mouse:", dev.mouseX(), dev.mouseY(), dev.isLeftButton()) # Must return True to consume the event return False def createViewerStateTemplate(): """ Mandatory entry point to create and return the viewer state template to register. """ state_typename = "statedemo" state_label = "Statedemo" state_cat = hou.objNodeTypeCategory() template = hou.ViewerStateTemplate(state_typename, state_label, state_cat) template.bindFactory(State) template.bindIcon("MISC_python") return template
To test the state, find an asset you want to add a state to, or create a blank SOP asset with or for an OBJ asset. To simplify the example, create an OBJ asset.
To quickly create a "blank" SOP asset to experiment with:
-
At the Object level, use the ⇥ Tab menu to create a
Geo object.
-
Double-click the
geo1
node to dive into the Geometry network inside. -
Use the ⇥ Tab menu to create a Subnetwork node.
-
Right-click the
subnet1
node and choose Create digital asset. -
Set the Operator name to
statedemo
, the Operator Label toState Demo
, and Save to library toEmbedded
.Setting the library location to
Embedded
saves the asset with the current scene file instead of in an asset library.
To quickly create a "blank" OBJ asset to experiment with:
-
At the Object level, use the ⇥ Tab menu to create a Subnetwork node.
-
Right-click the
subnet1
node and choose Create digital asset. -
Set the Operator name to
statedemo
, the Operator Label toState Demo
, and Save to library toEmbedded
.Setting the library location to
Embedded
saves the asset with the current scene file instead of in an asset library.
-
Open the type properties window for the asset. (Right click an instance of the asset type and choose Type properties).
-
Click the Node tab.
-
Set the Default state field to the state name (for example, in the code above, this is
statedemo
). -
At the bottom of the type properties window, click Accept.
-
If the asset was locked, Houdini will prompt you to unlock the asset so you can save your changes.
-
-
To start testing, select the asset in the network editor. Move the mouse into the scene viewer and press Enter. The mouse coordinates should be logged in the Viewer State Browser console as you move the mouse.
-
The toolbar at the top of the viewer should display the state’s label on the left.
-
On startup, Houdini will call createViewerStateTemplate
to access the state template and perform the registration. If omitted, Houdini will simply skip the state registration.
See implementing a state below for more information on adding functionality to the state class.
Tip
To reload a file state during a Houdini session, on the state name listed in the Viewer State Browser tree and select Reload in
the context menu. This will allow you to test changes to your state without restarting Houdini.
You can also reload the state with python by calling hou.ui.reloadViewerState() with the name of the state.
Implementing the state
The following section shows a high-level overview of the methods you can add to the class implementing your state.
For guides to implementing specific functionality, see the following pages:
Python viewer states
- Python state creating and editing nodes
- Python state nodeless states
- Python state selections
- Python state handles
- Python state parameters
- Python state guide geometry
- Python state user interface events
- Python state context menu
- Python state Drag and Drop
- Python states supporting undo
- Python state interface guidelines
Initializer (required)
def __init__(self, state_name, scene_viewer):
state_name
The state name string this state was registered under.
scene_viewer
A hou.SceneViewer object representing the scene viewer the tool is operating in. This object has many useful methods you can use to implement your state, for example hou.SceneViewer.currentGeometrySelection() and hou.SceneViewer.setCurrentGeometrySelection().
In general, you’ll just want to store the arguments in object attributes in case other methods need them:
class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer # Event handlers # ...
Event handlers
Event handlers are called with a single argument, a dictionary containing various useful items. The dictionary will typically (but not always) contain the following items:
node
Contains a hou.Node instance representing the node being operated on by the current state.
Menu items
The dictionary also contains the current values associated with menu items, using the menu item name as the key.
state_parms
Contains the names representing the state parameters bound to the current state. This dictionary is used for modifying the parameter states. See the details here.
state_flags
A dictionary containing various flags associated to the state. State flags can be set by all state handlers via their kwargs
argument.
Flag |
Notes |
---|---|
|
Controls whether a mouse drag event (with the When If mouse drag events are not required by your state, consider setting class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer onEnter( self, kwargs): kwargs['state_flags']['mouse_drag'] = False ... |
|
The flag triggers a redraw of the viewport on a mouse move or mouse wheel event when it’s set to By default the viewport always redraw. To reduce performance issues with large scenes, class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer onMouseEvent( self, kwargs): kwargs['state_flags']['redraw'] = False if __some_redraw_test__: kwargs['state_flags']['redraw'] = True ... |
interrupt_state
The name of the state that is interrupting the python state when a volatile state is activated, or an empty string.
The following tables list the event handlers you can define on your state class.
Lifecycle event handlers
Method name |
Called by Houdini |
Notes |
---|---|---|
|
state is entered from the network |
This method is called when the state is activated by the user creating a new node, or selecting an existing node and pressing Enter in the viewport. |
|
state is interrupted |
This method is called when:
Note: |
|
state is about to be exited |
This method is called when:
Note: |
|
interruption ends |
This method is called when:
Note: |
|
state is entered without an existing node |
This method is called when the state is activated by the user entering the state not from an existing node, for example a shelf tool script calling hou.SceneViewer.setCurrentState(). See nodeless states for information on states that work without a node. For states associated with assets, you could theoretically create a node in this method, however we strongly recommend creating the node in the tool script instead. See working with a node in a Python state for more information. The dictionary passed to this method does not contain the |
Additional notes:
-
Houdini calls either
onEnter
(if the state should work on an existing node) oronGenerate
(if the should create a new node) when the tool activates, never both. The dictionary passed toonEnter
has anode
item to access the existing node as a hou.Node object. See editing nodes for more information. -
Houdini will call
onEnter
/onGenerate
when the state begins, even if the mouse pointer is not over the viewer at the time. If you want to display screen visuals around the cursor, wait for anonMouseEvent
call to show them. -
While the state is "interrupted" it does not receive UI events.
-
You cannot rely on every
onInterrupt
call being followed by a correspondingonResume
. If the user exits the state while it’s interrupted, you will not receive anonResume
call, justonExit
. -
If the user opens the state’s context menu, when the pointer moves over the menu and leaves the menu you will receive
onInterrupt
andonResume
events. -
If your state is current but interrupted because Houdini is in the background, and the user switches to Houdini, even if the mouse pointer is over the viewer, your state will not receive an
onResume
call until the user makes some input (such as moving the mouse).
UI event handlers
See UI event handling for more information.
The dictionary passed to these methods has the following extra item:
ui_event
Contains a hou.ViewerEvent instance with information about the event (for example, for a mouse event, the current mouse coordinates and whether a button was clicked).
Method name |
Called by Houdini |
Notes |
---|---|---|
|
mouse moves/clicks |
See mouse handling. |
|
Mouse double clicks |
See mouse handling. |
|
mouse wheel scroll |
hou.UIEventDevice.mouseWheel() returns See mouse wheel handling. |
|
For key events |
See reading the keyboard device for more. |
|
For key transition events |
See reading the keyboard device for more. |
|
context menu choice |
|
|
update menu state |
The handler
The identifier of the menu to update.
A dictionary of menu states holding update values. Use the States supported
A dictionary of menu item states holding update values. Use a menu item handle to index the dictionary. The value part holds a dictionary of states for the menu item. States supported
Note
|
|
State parameter events |
|
|
general purpose command events |
Called by invoking hou.SceneViewer.runStateCommand(). The handler
The command string identifier.
A |
Additional notes:
-
onCommand
is used for implementing specific actions on the state. For instance, you can useonCommand
to set state parameters or to implement a custom notification mechanism. For details see hou.SceneViewer.runStateCommand().
Handle event handlers
Houdini will call these methods if you have bound dynamic handles to your state. See Python state handles for more information.
Method name |
Called by Houdini |
Notes |
---|---|---|
|
user interaction with a handle |
This lets you update node parameters (and/or the state/display) when a handle changes. The dictionary passed to this method contains the following extra items:
The string ID of the handle.
A dictionary containing the new handle parameter values.
A list of of the names of parameters that changed.
A dictionary containing the previous handle parameter values. This can be useful for computing deltas.
hou.UIEvent object to know about the handle status such as start dragging or stop dragging. |
|
node parameters change |
This lets you update handle parameters when node parameters change. The dictionary passed to this method contains the following extra items:
The string ID of the handle.
A dictionary containing the new node parameter values. |
|
start of user interaction with a handle |
This lets you know when the user has started to manipulate a handle. The dictionary passed to this method contains the following extra items:
The string ID of the handle.
hou.UIEvent object to know about the handle status such as start dragging or stop dragging. |
|
end of user interaction with a handle |
This lets you know when the user has ended the manipulation a handle. The dictionary passed to this method contains the following extra items:
The string ID of the handle.
hou.UIEvent object to know about the handle status such as start dragging or stop dragging. |
Selection event handlers
Houdini will call these methods if you have bound selectors to your state. See selection handling for more information.
Method name |
Called by Houdini |
Notes |
---|---|---|
|
user starts selecting |
The dictionary passed to this handler has the following item:
The name of the current active selector (see hou.ViewerStateTemplate.bindGeometrySelector()). |
|
user selected geometry |
The dictionary passed to this handler has the following extra items:
A hou.GeometrySelection object representing the completed selection.
The name of the current active selector. You should Return True from this method to signal your state "accepts" the current selection and stop the selector. If the method returns any other value (or doesn’t contain a |
|
user stops selecting |
This is called when the state "accepts" a selection by returning The dictionary passed to this handler has the following item:
The name of the current active selector. |
Drag and Drop event handlers
Houdini calls the methods below for handling the drag drop
events occurring in the viewer when a
python state is active. See Drag and Drop handling for more.
Method name |
Note |
---|---|
|
Called when the user initiates a |
|
Called to build a list of |
|
Handles the selected |
Drawing event handlers
These methods are called when drawing events are generated. For instance, these methods would be implemented for processing hou.AdvancedDrawable objects.
Method name |
Called by Houdini |
Notes |
---|---|---|
|
drawing events |
The method is called when:
|
|
drawing events |
The method is called when:
|
Additional notes:
-
onDraw
is required for advanced drawables. The handle returned bykwargs["draw_handle"]
must be passed to drawables for performing the render operation.def onDraw(self, kwargs): handle = kwargs['draw_handle'] params = { 'translate': hou.Vector3(self.mouse_pos[0],self.mouse_pos[1],self.mouse_pos[2]) } self.cursor.render(handle, params ) params = { 'translate': hou.Vector3(self.translate[0],self.translate[1],self.translate[2]), 'color1': hou.Color(self.rgba[0],self.rgba[1],self.rgba[2],self.rgba[3]), 'blur_width': self.blur_width } self.face_drawable.render(handle, params)
-
onDrawInterrupt
is optional and only used with advanced drawables when drawing is required during a state interruption.
Inspecting viewer states
Houdini lets you view all registered viewer states with the Viewer State Browser window. The browser can be opened via the Python Panel menu.
Debugging tips
Houdini provides some support for debugging python viewer states. The most useful is the viewer state browser which offers integrated tools for displaying debugging information. You can still use a more basic solution like the python print function, but the browser offers more functionality with regards to debugging.
Displaying debugging information
The most basic but still useful form of debugging is to print information, such as the contents of variables, as the script runs. Houdini provides a few ways of displaying this type of information for python states.
The main advantage of using prints is that you can output a lot of information, including multi-line blocks of text (such as the current Python call stack), and scroll back to read it later.
self.log method
The log
method is available with your python state class and is similar to the python print function. Instead of printing to the standard output,
it prints messages to the viewer state browser console.
-
The browser lets you control when to log messages by toggling the
Debug log
button. This allows you to keep thelog
method call in your code instead of commenting out when debugging is not required. -
Houdini adds the
log
method dynamically to the python state after the object was created.log
is therefore not yet available when__init__
is called. The workaround is to useviewerstate.utils.log
from__init__
for logging messages in the Viewer State Browser console.
import traceback class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): # Log the mouse position ui_event = kwargs["ui_event"] device = ui_event.device() self.log("Mouse position x=", device.mouseX(), "y=", device.mouseY()) # Log the current Python call stack self.log(''.join(traceback.format_stack()))
DebugAid
The utility class viewerstate.utils.DebugAid
provides debugging support for viewer states. This utility provides a logging mechanism like self.log
and
all the debugging features currently available in the viewer state browser as methods:
-
Inspect the active viewer state.
-
Add markers.
-
Enable a debug trace.
-
Enable logging console
-
Reload the running viewer state.
See also python handle DebugAid.
import traceback from viewerstate.utils import DebugAid class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer self.dbg = DebugAid(self) def onEnter(self, kwargs): # Inspect the state self.dbg.marker() # inspect this viewer sate self.dbg.inspect() # start a trace self.dbg.trace() def onMouseEvent(self, kwargs): # Log the mouse position ui_event = kwargs["ui_event"] device = ui_event.device() self.dbg.marker() self.dbg.log("Mouse position x=", device.mouseX(), "y=", device.mouseY()) # Log the current Python call stack self.dbg.log(''.join(traceback.format_stack()))
Python print function
The output appears in a console window, or Houdini’s Python shell window, or the shell you started Houdini from, depending on how you started Houdini and what windows are open.
Tip
It’s a good idea to use from __future__ import print
to get used to using print
as a function instead of as a statement. The function is easier to use and has more functionality.
from __future__ import print import traceback class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): # Print the mouse position ui_event = kwargs["ui_event"] device = ui_event.device() print("Mouse position x=", device.mouseX(), "y=", device.mouseY()) # Print the current Python call stack traceback.print_stack()
hou.SceneViewer.setPromptMessage
The hou.SceneViewer.setPromptMessage() and hou.SceneViewer.clearPromptMessage() functions let you display prompts for user interaction in the status line at the bottom of the main Houdini window. You can "abuse" these functions to display debugging information.
-
The advantage of this method is that the information is front-and-center in the window as you interact with Houdini.
-
The disadvantage is that the status line can only show one line of information at a time and when you change it the previous message is lost.
class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): # Display the mouse position in the status line ui_event = kwargs["ui_event"] device = ui_event.device() message = "Mouse x=%d y=%d" % (device.mouseX(), device.mouseY()) self.scene_viewer.setPromptMessage(message)
hou.ui.displayMessage
The hou.ui.displayMessage() function pops up a message window and waits for the user to click OK or Cancel.
-
This has a few advantages for debugging. One is that it pauses the script while it waits, which may be useful if you're trying to investigate changes that happen very quickly. It also lets you give some feedback to the script based on which button you click. Another is that the function has keyword arguments that let you attach a block of "details" text that is hidden by default but can be expanded. This is useful, for example, for displaying the current Python call stack.
-
You probably don’t want to use this function in a loop, where it would be annoying to have to click to dismiss multiple message windows.
import traceback import hou class MyState(object): def __init__(self, state_name, scene_viewer): self.state_name = state_name self.scene_viewer = scene_viewer def onMouseEvent(self, kwargs): ui_event = kwargs["ui_event"] device = ui_event.device() # Only diplay the message on a click if device.isLeftButton(): message = "Mouse x=%d y=%d" % (device.mouseX(), device.mouseY()) # Get the call stack at this point in the script and format it # in a string so we can attach it to the message as "details" details = "".join(traceback.format_stack()) # Display a message and wait for the user to click a button in # the message window clicked = hou.ui.displayMessage( message, buttons=("OK", "Error"), details_label="Current call stack", details=details ) # If user clicked the "Error" button, raise an error if clicked == 1: raise Exception("Don't blame me!")
Reloading states
For states defined on disk, if the files on disk change, you can tell Houdini to reload the state using hou.ui.reloadViewerState(). Use the Viewer State Editor to reload HDA embedded states.
Debug context menu
HOM API
Here are a few of the HOM
API related to python viewer states.
Python viewer states