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 should interpret various low-level events like mouse moves, mouse clicks and parameter changes. Other UI events support like keyboard and menu events is also possible, see the input events documentation for more details.
Mouse handling ¶
Houdini lets you handle mouse events with three different handlers: onMouseEvent
,onMouseIndirectEvent
and onMouseWheelEvent
.
onMouseEvent ¶
The handler is used for implementing most of your Python handle interactive workflow.
-
onMouseEvent
lets you handle and events when the mouse is over a handle gadget. -
onMouseEvent
is always processed before the active state’sonMouseEvent
. -
Use hou.ViewerEvent (
kwargs["ui_event"]
) to track down low-level mouse interactions, detect mouse downs, etc… -
Use hou.ViewerHandleContext (
self.handle_context
) to access the state of a gadget. -
Before calling
onMouseEvent
, Houdini does a pass to find out which geometry is under the mouse and updatesself.handle_context
with the (active) gadget associated to the geometry. There is no need to implement a geometry intersection to find the gadget by yourself. -
The active gadget can then be accessed from
self.handle_context
inonMouseEvent
to choose which action to execute. -
Python handle actions can be implemented with hou.ViewerHandleDragger to constrain mouse movements along axes, planes or across the construction grid.
Here’s an example to demonstrate how to translate parameters with a handle dragger and gadget.
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): self.__dict__.update(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. """ # Get the active gadget being located or picked. 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.
onMouseIndirectEvent ¶
This handler is used for handling events if the mouse is not located over one of the python handle gadgets.
-
onMouseIndirectEvent
is called for the handle closest to the mouse. -
Use hou.ViewerEvent (
kwargs["ui_event"]
) to track down low-level mouse interactions. -
Houdini will only call
onMouseIndirectEvent
for the following events:hou.uiEventReason.Start
,hou.uiEventReason.Active
,hou.uiEventReason.Changed
. -
Dragging the mouse with or will not trigger the handler.
-
onMouseIndirectEvent
must return True when the event has been consumed.
This example demonstrates how to translate the handle by dragging the mouse with .
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): PIVOT_SIZE = 0.3 def __init__(self, **kwargs): self.__dict__.update(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"]}) # Pivot gadget sops = hou.sopNodeTypeCategory() verb = sops.nodeVerb("box") psize = Handle.PIVOT_SIZE verb.setParms( { "type" : 1, "size" : (psize,psize,psize), "divrate": (2,2,2) }) pivot = hou.Geometry() verb.execute(pivot, []) self.pivot = self.handle_gadgets["pivot"] self.pivot.setGeometry(pivot) self.pivot.show(True) def onMouseInteractEvent(self, kwargs): """ Called when the mouse is dragged with MMB. """ ui_event = kwargs["ui_event"] reason = ui_event.reason() consumed = False # The example moves the pivot when MMB is down. # # The implementation is not that different from `onMouseEvent`. # # We don't need to check for the active gadget as the mouse cannot # be over a gadget when onMouseInteractEvent is called. # # Using a "constraint free" dragger will compute the handle position # relative to the world space position under the mouse. if reason == hou.uiEventReason.Start: # Get the handle tx,ty,tz parameters as a hou.Vector3 object handle_pos = self.xform_aid.parm3("translate") # init the dragger self.translate_dragger.startDrag(ui_event, handle_pos) consumed = True 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() consumed = True return consumed def onMouseEvent( self, kwargs ): """ Called when a gadget is picked and dragged. """ consumed = False # Get the active gadget being located or picked. 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) consumed = True 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() consumed = True return consumed def onDraw( self, kwargs ): self.pivot.draw(kwargs["draw_handle"])
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()
Accessing parameter values ¶
Python handle’s parameters and settings can be accessed from the handle_parms dictionary. The dictionary is accessible from any Python handle handlers.
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. 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=1.0 ) template.bindParameter( hou.parmTemplateType.Float, name="tz", label="Tz", min_limit=-10.0, max_limit=10.0, default_value=0.0 ) template.bindParameter( hou.parmTemplateType.Float, name="float3"", label="Float 3", min_limit=-10.0, max_limit=10.0, default_value=[1.0,1.0,1.0], num_components=3 ) # Handle settings. template.bindSetting( hou.parmTemplateType.Toggle, name="ui_guides", label="Draw UI guides", default_value=True) return template class Handle(object): def __init__(self, **kwargs): self.__dict__.update(kwargs) def onActivate(self, kwargs): self.log("Handle tx parm value", kwargs["handle_parms"]["tx"]["value"]) self.log("Handle ty parm value", kwargs["handle_parms"]["ty"]["value"]) self.log("Handle tz parm value", kwargs["handle_parms"]["tz"]["value"]) self.log("Handle float3 parm value", kwargs["handle_parms"]["float3"]["value"]) self.log("UI guides value", kwargs["handle_parms"]["ui_guides"]["value"]) # output 'Handle tx parm value' 0.0 'Handle ty parm value' 1.0 'Handle tz parm value' 0.0 'Handle float3 parm value' [1.0, 1.0, 1.0] 'UI guides value' 1
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)