On this page |
Overview
(See Viewer handles for the basics of how to implement a Python handle.)
To properly support node parameter changes interactively, Python handles must interpret various low-level events like mouse moves, mouse clicks, parameter changes, keyboard events, and so on. See input events for more details on how to interpret mouse events.
Mouse handling
onMouseEvent
onMouseEvent
does most of the work for implementing the Python handle interactivity.
-
Use
onMouseEvent
and hou.ViewerEvent to track down low-level mouse interactions, detect mouse downs, etc… -
Mouse movements can be easily constrained to axes, planes and the construction grid with hou.ViewerHandleDragger.
-
Unlike Python state’s
onMouseEvent
, there is no need to perform a geometry intersection to find which gadget needs to be picked or located, Houdini takes care of it for you. -
The results of picking and locating gadgets is stored in a hou.ViewerHandleContext object which is typically used to choose the action associated to a given gadget.
-
onMouseEvent
is only called when the mouse is over a gadget withor
down.
-
onMouseEvent
is always processed before Python state’sonMouseEvent
gets processed.
Here’s an example to demonstrate how to translate parameters with a handle dragger.
import viewerhandle.utils as hu def createViewerHandleTemplate(): handle_type = 'handle_example' handle_label = 'Handle example' handle_cat = [hou.sopNodeTypeCategory()] template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat) template.bindFactory(Handle) # Define the handle parameters template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) # Export all parameters template.exportParameters(["tx", "ty", "tz"]) # Bind the pivot gadget template.bindGadget( hou.drawableGeometryType.Face, "pivot" ) return template class Handle(base_class.Handle): def __init__(self, **kwargs): # Creates a dragger for translating the handle self.translate_dragger = hou.ViewerHandleDragger("translate_dragger") # Utility class to support the handle transform operations self.xform_aid = hu.TransformAid(self, parm_names={"translate":["tx","ty","tz"]}) def onMouseEvent( self, kwargs ): """ Called when a gadget is picked and dragged. """ if self.handle_context.gadget() == "pivot": ui_event = kwargs["ui_event"] reason = ui_event.reason() # The pivot gadget is what the user drags to change the translate parameters. if reason == hou.uiEventReason.Start: # Get the the handle tx,ty,tz parameters as a hou.Vector3 object handle_pos = self.xform_aid.parm3("translate") # Start the handle translate with a hou.ViewerHandleDragger object. # The dragger has various methods for specialized operations such as # dragging along a line or along a plane. Here we just move the pivot # in world space. self.translate_dragger.startDrag(ui_event, handle_pos) elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]: # drag the pivot continously drag_values = self.translate_dragger.drag(ui_event) # Update the handle tx, ty, tz parameters with the position returned by the dragger. self.xform_aid.addToParm3("translate", drag_values["delta_position"]) # Note: The transform will be computed in onDrawSetup. if reason == hou.uiEventReason.Changed: # We are done, exit the drag. self.translate_dragger.endDrag() # Consume the event return True return False
This implementation pattern is pretty generic and can be reused for most of your Python handles.
Since onMouseEvent
is always called when a gadget is picked, there is no need to check if the mouse
is down first, instead you check which gadget is active
and acts upon it.
hou.uiEventReason.Start
tells us the mouse was pressed down, you need to setup the dragger at this
point by calling startDrag
with the handle translate
position (xyz parameters).
We should call the drag
method when the UI event is set to hou.uiEventReason.Changed
or hou.uiEventReason.Active
,
and update the handle’s translate
parameters with the delta value returned by the dragger. Once the mouse
has been released (hou.uiEventReason.Changed
), we terminate the dragger with a call to endDrag
.
You probably notice that self.handle_context
is not explicitly created in the __init__
function.
This is a class attribute created by Houdini, you don’t need to create one yourself. self.handle_context
is set with a hou.ViewerHandleContext
object which holds the state of gadgets with regards to picking
and locating. This object is managed by Houdini to make sure it is constantly updated with the latest
contextual information.
onMouseWheelEvent
Responding to the mouse wheel is achieved with onMouseWheelEvent
. This handler works pretty much the
same as the Python state onMouseWheelEvent
handler.
There is an important difference between the two however. The Python handle’s onMouseWheelEvent
handler is only called on located gadgets, hence the mouse cursor should always be over a gadget for a mouse wheel event to be fired. Sometimes, keeping the mouse over a located gadget can be finicky, so you might want to design your Python handle accordingly, with either a dedicated or a stationary gadget to enable a mouse wheel event.
Also, Houdini always call the Python handle’s onMouseWheelEvent
before the Python state’s
onMouseWheelEvent
handler. Make sure the Python handle’s onMouseWheelEvent
consumes the event
to avoid processing the Python state’s handler by mistake.
Parameters
Parameters are an important part of a Python handle. Without parameters, a Python handle cannot be used for changing node parameters in the viewport.
Defining the handle parameters is part of the registration process, use hou.ViewerHandleTemplate.bindParameter() to add parameters to a Python handle template. All defined parameters can then be manipulated in the viewport interactively only if they have been properly exported. Parameters are exported with hou.ViewerHandleTemplate.exportParameters(), this step allows Python states to bind a Python handle parameters to a node parameters. If you choose not to export a specific parameter, it will not be available to the outside.
In addition to Python handle parameters, you can also add setting parameters to a Python handle with
hou.ViewerHandleTemplate.bindSetting(). These setting
parameters are typically used as configuration
settings for a Python handle. For instance, you might want to create a setting parameter to hold different
color values for a gadget or a scaling value, etc… Python handle parameters and settings can be modified
from the Handle Parameter Dialog
and from the Python handle’s handle_parms
class attribute. However, setting
parameters are not exportable and therefore cannot be changed interactively
from the viewport.
Note
Parameter and setting values are saved with the scene and restored to the last saved value when the scene is loaded back.
Responding to parameter changes
Use the onParmChangeEvent
handler to respond to parameter changes. Houdini calls the handler after
a Python handle’s parameter or setting parameter has been changed. For more details see onParmChangeEvent
.
This sample demonstrates how to define the parameters of a Python handle and respond to parameter changes.
import viewerhandle.utils as hu def createViewerHandleTemplate(): handle_type = 'handle_example' handle_label = 'Handle example' handle_cat = [hou.sopNodeTypeCategory()] template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat) template.bindFactory(Handle) # Handle parameters are typically used for controlling gadgets and binding node parms. template.bindParameter( hou.parmTemplateType.Float, name="tx", label="Tx", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) template.bindParameter( hou.parmTemplateType.Float, name="ty", label="Ty", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) # ... they also need to be exported, export only the ones you want Houdini to use. # The exported parameters can be used in a python state for binding node parameters # e.g. hou.ViewerStateTemplate.bindHandleStatic. template.exportParameters(["tx", "ty", "tz"]) # Settings are parameters used for controlling the handle behavior. template.bindSetting( hou.parmTemplateType.Toggle, name="face", label="Face", default_value=True, align=True ) template.bindSetting( hou.parmTemplateType.Toggle, name="wire", label="Wire", default_value=True, align=True ) template.bindSetting( hou.parmTemplateType.Toggle, name="pivot", label="Pivot", default_value=True, align=True ) template.bindSetting( hou.parmTemplateType.Toggle, name="knob", label="Knob", default_value=True, align=False ) # Bind gadgets template.bindGadget( hou.drawableGeometryType.Face, "pivot" ) template.bindGadget( hou.drawableGeometryType.Face, "face" ) template.bindGadget( hou.drawableGeometryType.Line, "wire" ) template.bindGadget( hou.drawableGeometryType.Point, "knob" ) return template class Handle(base_class.Handle): def __init__(self, **kwargs): self.__dict__.update(kwargs) self.xform_aid = hu.TransformAid(self, parm_names={"translate":["tx","ty","tz"]}) # skip gadget initialization for brevity def onParmChangeEvent(self, kwargs): """ Called when a handle parameter or setting has changed. """ parm_name = kwargs['parm_name'] parm_value = kwargs['parm_value'] if parm_name in ["tx","ty","tz"]: # update the handle transform with the new values self.xform_aid.updateTransform() elif parm_name == "face": kwargs["handle_gadgets"]["face"].show(parm_value) elif parm_name == "wire": kwargs["handle_gadgets"]["wire"].show(parm_value) elif parm_name == "pivot": kwargs["handle_gadgets"]["pivot"].show(parm_value) elif parm_name == "knob": kwargs["handle_gadgets"]["knob"].show(parm_value) # update the viewport self.scene_viewer.curViewport().draw()
Drawing
A Python handle uses up to 2 drawing handlers:
-
onDraw
:Used for drawing the Python handle gadgets and other drawables. It is pretty straight forward to implement and should basically delegate to the handle gadgets and drawables
draw
method. The handler is similar to the Python statesonDraw
handler. -
onDrawSetup
:Python handles need to be scaled dynamically in order to maintain a fixed size independent of the current camera position.
onDrawSetup
is typically used for computing the handle scale with the help of hou.ViewerHandleContext.scaleFactor(). With a newly computed scale value, Python handles should also update their transformation matrices along with their drawable guides transformation matrices.
def onDrawSetup(self, kwargs): # Use the current handle position to compute the scale value hpos = self.xform_aid.parm3("translate") # Compute the scale value fixed_scale_value = 100.0 scale = self.handle_context.scaleFactor(hpos)*fixed_scale_value # Rebuild the handle transform matrix with the new scale xform = self.xform_aid.updateTransform(s=[scale,scale,scale]) # Update the gadgets transform kwargs["handle_gadgets"]["face"].setTransform(xform) kwargs["handle_gadgets"]["wire"].setTransform(xform) kwargs["handle_gadgets"]["pivot"].setTransform(xform) kwargs["handle_gadgets"]["knob"].setTransform(xform)