Houdini 20.0 Python scripting

Python handle gadgets

How to render a Viewer Handle in the viewport with gadget drawables.

On this page

Overview

(See Viewer handles for the basics of how to implement a Python handle.)

A Gadget is a geometry representing a visual component of a Python handle. For example, in a custom translate handle, a gadget could be used to represent the handle pivot for translating an object. In a slider handle, a gadget is a knob to change a parameter. A Python handle will typically use one or several gadgets to render its visuals.

Gadget drawable

The hou.GadgetDrawable class represents a Python handle’s gadget. A gadget drawable is basically a specialized geometry drawable API with extra locating and picking functionalities that are essential for Python handles to identify which object the user is picking or which object under the cursor is pointing at.

Like geometry drawables, drawable gadgets can be set with any mesh geometries supported in Houdini, either by using verbs or even a geometry generated by a SOP network. However, for performance reasons, it’s best to assign Python handle gadgets with geometries defined through verbs.

Unlike geometry drawables, a gadget drawable must be registered first before use. With this information, Houdini can create the gadget instances of a Python handle by itself and perform the low-level picking and locating operations on the gadget’s underlying geometry.

For convenience, the gadget instances created by Houdini are stored in the handle_gadgets attribute of the Python handle class.

Using gadget drawables

Using gadget drawables is no different than using geometry drawables. Houdini makes it easier for the developer by creating the gadget objects automatically. A python handle implementation will typically set its gadgets with a geometry and a proper set of parameter values.

import resourceutils as ru

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)
    template.bindIcon('$HFS/houdini/pic/minimizedicon.pic')

    # Register the gadgets
    template.bindGadget( hou.drawableGeometryType.Line, "line" )
    template.bindGadget( hou.drawableGeometryType.Point, "point" )

    return template

class Handle(object):

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

        # Set the gadgets with the Houdini standard colors
        self.color_options = ru.ColorOptions(self.scene_viewer)

        # Set the "line" gadget
        sops = hou.sopNodeTypeCategory()
        verb = sops.nodeVerb("box")
        verb.setParms({"type" : 1, "divrate":(2,2,2), "size":(0.9,0.9,0.9)})
        geo = hou.Geometry()
        verb.execute(geo, [])

        # Houdini uses `draw_color` for normal drawing, `pick_color` to draw the gadget when picking 
        # is enabled and `locate_color` for highlighting the gadget. 
        self.line = self.handle_gadgets["line"]
        self.line.setParams({"draw_color":self.color_options.colorFromName("YaxesColor")})
        self.line.setParams({"pick_color":self.color_options.colorFromName("PickedHandleColor")})
        self.line.setParams({"locate_color":self.color_options.colorFromName("HandleLocateColor", alpha_name="HandleLocateAlpha")})
        self.line.setGeometry(geo)

        # Set the "point" gadget
        self.point = self.handle_gadgets["point"]
        verb.setParms({"type" : 1, "divrate":(2,2,2)})
        verb.execute(geo, [])

        self.point.setParams({"draw_color":self.color_options.colorFromName("XaxesColor")})
        self.point.setParams({"pick_color":self.color_options.colorFromName("PickedHandleColor")})
        self.point.setParams({"locate_color":self.color_options.colorFromName("HandleLocateColor", alpha_name="HandleLocateAlpha")})
        self.point.setParams({"radius":7.0})
        self.point.setGeometry(geo)

    def onDraw(self, kwargs):
        # draw each gadget
        draw_handle = kwargs["draw_handle"]

        self.line.draw(draw_handle)
        self.point.draw(draw_handle)

The __init_ function uses a resourceutils.ColorOptions attribute for setting gadget colors. ColorOptions is a utility class for retrieving Houdini standard colors. It is strongly suggested to use this utility for selecting color values and make your Python handle coherent with other Houdini builtin handles. Colors fetched with resourceutils.ColorOptions will also follow the viewport color scheme when the background theme is modified.

Tip

The glow setting used for locating the handle component is set by default but can be customized with the locate_glow param, see hou.GadgetDrawable for details.

Gadgets and parameters binding

As for any builtin handles, node parameters can be bound to python handles either interactively with the Handle Bindings dialog or programmatically with a python state. The gadgets used in a python handle implementation to interact visually with the bound parameter(s) will then be enabled for drawing, interacting, etc.

But what if the user chooses to bound only some of the python handle parameters and leaves the rest unbound ? The gadgets that require these “unbound” parameters in a python handle implementation are of no use by the user since they can’t modify parameters, we certainly don’t want the user to believe these gadgets are still usable in the viewport.

What the python handle implementation should do in this case is to simply hide these "unbound" gadgets. The way for a python handle to know if a gadget is available or not at runtime is to provide a list of required parameters with hou.ViewerHandleTemplate.bindGadget. With this information in hand, Houdini will be able to make a gadget available when all its required parameters are bound or make it unavailable otherwise.

Registering a gadget with its list of required handle parameters is optional but strongly recommended. It will make your handle more dynamic and friendly to users.

Here’s a more advanced example to demonstrate the implementation of a python handle with "binding aware" gadgets.

Note

The example seems a bit intimidating but the code used in onMouseEvent is very repetitive. This is done on purpose to better show the detail of a python handle implementation.

import resourceutils as ru

def createViewerHandleTemplate():
    handle_type = kwargs["type"].definition().sections()["ViewerHandleName"].contents()    
    handle_label = 'Handle example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)
    template.bindIcon('$HFS/houdini/pic/minimizedicon.pic')

    # Register the handle parameters
    template.bindParameter( hou.parmTemplateType.Float, name="X", default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="Y", default_value=0.0 )

    # Register the gadgets with a list of required handle parameters
    template.bindGadget( hou.drawableGeometryType.Line, "x_gadget", parms=["X"] )
    template.bindGadget( hou.drawableGeometryType.Line, "y_gadget", parms=["Y"] )

    # Export the parameters to Houdini
    template.exportParameters(["X","Y"])

    return template

class Handle(object):

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

        self.current_pos = hou.Vector3()
        self.xy_dragger = hou.ViewerHandleDragger("xy_dragger")

        # Set the gadgets with the Houdini standard colors
        self.color_options = ru.ColorOptions(self.scene_viewer)

        # Initialize the gadgets only if they are available
        sop_cat = hou.sopNodeTypeCategory()

        self.x_gadget = self._gadget("x_gadget")
        if self.x_gadget:
            verb = sop_cat.nodeVerb("line")
            verb.setParms(
                {   "origin" : (0,0,0),
                    "dir" : (1,0,0),
                    "dist": 1.0,
                    "points": 2
                })
            geo = hou.Geometry()        
            verb.execute(geo, [])

            self.x_gadget.setParams({"draw_color":self.color_options.colorFromName("XaxesColor")})
            self.x_gadget.setGeometry(geo)

        self.y_gadget = self._gadget("y_gadget")
        if self.y_gadget:
            verb = sop_cat.nodeVerb("line")
            verb.setParms(
                {   "origin" : (0,0,0),
                    "dir" : (0,1,0),
                    "dist": 1.0,
                    "points": 2
                })
            geo = hou.Geometry()        
            verb.execute(geo, [])

            self.y_gadget.setParams({"draw_color":self.color_options.colorFromName("YaxesColor")})
            self.y_gadget.setGeometry(geo)

    def _gadget(self, gadget_name):
        try:
            return self.handle_gadgets[gadget_name]
        except:
            return None

    def _showGadget(self, value):
        if self.x_gadget:
            self.x_gadget.show(value)

        if self.y_gadget:
            self.y_gadget.show(value)

    def onActivate(self, kwargs):
        self._showGadget.show(True)

    def onDeactivate(self, kwargs):
        self._showGadget.show(False)

    def onMouseEvent(self, kwargs):

        ui_event = kwargs["ui_event"]
        reason = ui_event.reason()

        consumed = False

        # current_gadget_name should be set to None if the gadget is not available.
        current_gadget_name = self.handle_context.gadget()
        current_gadget = self._gadget(current_gadget_name)

        if current_gadget_name in ["x_gadget"]:

            # Handle the x gadget
            if reason == hou.uiEventReason.Start:

                # Move along x axis
                self.xy_dragger.startDragAlongLine(ui_event, self.current_pos, 
                    hou.Vector3(1, 0, 0))
                current_gadget.show(True)

                if self.y_gadget:
                    self.y_gadget.show(False)

            elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
                drag_values = self.xy_dragger.drag(ui_event)
                delta_pos = drag_values["delta_position"]

                # update the handle parms while dragging
                self.current_pos += delta_pos
                self.handle_parms["X"]["value"] += delta_pos[0]

                # update the gadget transform
                xform = current_gadget.transform() * hou.hmath.buildTranslate(delta_pos)
                current_gadget.setTransform(xform)

                if reason == hou.uiEventReason.Changed:
                    # ends dragging     
                    self.xy_dragger.endDrag()

                    if self.y_gadget:
                        xform = hou.hmath.buildTranslate(self.current_pos)
                        self.y_gadget.setTransform(xform)                                        
                        self.y_gadget.show(True)

            consumed = True

        elif current_gadget_name in ["y_gadget"]:

            # Handle the y gadget
            if reason == hou.uiEventReason.Start:

                # Move along y axis
                self.xy_dragger.startDragAlongLine(ui_event, self.current_pos, 
                    hou.Vector3(0, 1, 0))
                current_gadget.show(True)

                if self.x_gadget:
                    self.x_gadget.show(False)

            elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
                drag_values = self.xy_dragger.drag(ui_event)
                delta_pos = drag_values["delta_position"]

                # update the handle parms while dragging
                self.current_pos += delta_pos
                self.handle_parms["Y"]["value"] += delta_pos[1]

                # update the gadget transform
                xform = current_gadget.transform() * hou.hmath.buildTranslate(delta_pos)
                current_gadget.setTransform(xform)

                if reason == hou.uiEventReason.Changed:
                    # ends dragging
                    self.xy_dragger
                    self.xy_dragger.endDrag()

                    # update the x gadget line
                    if self.x_gadget:
                        xform = hou.hmath.buildTranslate(self.current_pos)
                        self.x_gadget.setTransform(xform)                    
                        self.x_gadget.show(True)

            consumed = True

        return consumed

    def onDraw(self, kwargs):
        # draw each gadget
        draw_handle = kwargs["draw_handle"]

        if self.x_gadget:
            self.x_gadget.draw(draw_handle)

        if self.y_gadget:
            self.y_gadget.draw(draw_handle)

The following example uses a modular approach to simplify the implementation of the previous example.

import resourceutils as ru

def createViewerHandleTemplate():
    handle_type = kwargs["type"].definition().sections()["ViewerHandleName"].contents()    
    handle_label = 'Handle modular example'
    handle_cat = [hou.sopNodeTypeCategory()]

    template = hou.ViewerHandleTemplate(handle_type, handle_label, handle_cat)
    template.bindFactory(Handle)
    template.bindIcon('$HFS/houdini/pic/minimizedicon.pic')

    # Register the handle parameters
    template.bindParameter( hou.parmTemplateType.Float, name="X", default_value=0.0 )
    template.bindParameter( hou.parmTemplateType.Float, name="Y", default_value=0.0 )

    # Register the gadgets with a list of required handle parameters
    template.bindGadget( hou.drawableGeometryType.Line, "x_gadget", parms=["X"] )
    template.bindGadget( hou.drawableGeometryType.Line, "y_gadget", parms=["Y"] )

    # Export the parameters to Houdini
    template.exportParameters(["X","Y"])

    return template

class Handle(object):

    def __init__(self, **kwargs):
        self.__dict__.update(kwargs)

        self.xform = hou.Matrix4(1)
        self.gadgets = []

        self.gadgets.append(LineGadget(self, self.scene_viewer, 
            {   "gadget_name": "x_gadget",
                "gadget_dir": hou.Vector3(1,0,0),
                "gadget_color": "XaxesColor",
                "parm_name": "X",
                "parm_index": 0
            })
        )

        self.gadgets.append(LineGadget(self, self.scene_viewer, 
            {   "gadget_name": "y_gadget",
                "gadget_dir": hou.Vector3(0,1,0),
                "gadget_color": "YaxesColor",
                "parm_name": "Y",
                "parm_index": 1
            })
        )

    def onActivate(self, kwargs):
        for gadget in self.gadgets:
            gadget.show(True)

    def onDeactivate(self, kwargs):
        for gadget in self.gadgets:
            gadget.show(False)

    def onDraw(self, kwargs):
        draw_handle = kwargs["draw_handle"]
        for gadget in self.gadgets:
            gadget.onDraw(draw_handle) 

    def onMouseEvent(self, kwargs):

        ui_event = kwargs["ui_event"]
        reason = ui_event.reason()
        current_gadget_name = self.handle_context.gadget()

        for gadget in self.gadgets:
            if reason == hou.uiEventReason.Start:
                gadget.onBeginInteracting(kwargs, current_gadget_name)

            elif reason in [hou.uiEventReason.Changed, hou.uiEventReason.Active]:
                gadget.onInteracting(kwargs, current_gadget_name)

                if reason == hou.uiEventReason.Changed:
                    gadget.onEndInteracting(kwargs, current_gadget_name)

        return True


class LineGadget(object):
    """ Implement the support for a line dragging gadget.
    """
    def __init__(self, handle, scene_viewer, parms):
        super(LineGadget,self).__init__()

        self.handle = handle
        self.scene_viewer = scene_viewer
        self.dragger = hou.ViewerHandleDragger("dragger")
        self.gadget_dir = parms["gadget_dir"]
        self.gadget_name = parms["gadget_name"]
        self.gadget_color = parms["gadget_color"]
        self.parm_name = parms["parm_name"]
        self.parm_index = parms["parm_index"]

        self.color_options = ru.ColorOptions(self.scene_viewer)

        # Initialize the gadget only if available        
        try:
            self.gadget = self.handle.handle_gadgets[self.gadget_name]
        except:
            self.gadget = None

        if self.gadget:
            sop_cat = hou.sopNodeTypeCategory()        
            verb = sop_cat.nodeVerb("line")
            verb.setParms(
                {   "origin" : (0,0,0),
                    "dir" : (self.gadget_dir[0],self.gadget_dir[1],self.gadget_dir[2]),
                    "dist": 1.0,
                    "points": 2
                })
            geo = hou.Geometry()        
            verb.execute(geo, [])

            self.gadget.setParams({"draw_color": self.color_options.colorFromName(self.gadget_color)})
            self.gadget.setGeometry(geo)

    def _accept(self, gadget_name):
        return gadget_name == self.gadget_name

    def show(self, value):
        try:
            self.gadget.show(value)
        except:
            pass

    def onBeginInteracting(self, kwargs, gadget_name):        
        if not self._accept(gadget_name):
            self.show(False)
            return False

        ui_event = kwargs["ui_event"]
        current_pos = self.handle.xform.extractTranslates()
        self.dragger.startDragAlongLine(ui_event, current_pos, self.gadget_dir)
        self.show(True)

        return True

    def onInteracting(self, kwargs, gadget_name):
        if not self._accept(gadget_name):
            return False

        ui_event = kwargs["ui_event"]
        drag_values = self.dragger.drag(ui_event)
        delta_pos = drag_values["delta_position"]

        self.handle.handle_parms[self.parm_name]["value"] += delta_pos[self.parm_index]

        # update the gadget transform
        xform = self.gadget.transform() * hou.hmath.buildTranslate(delta_pos)
        self.gadget.setTransform(xform)
        self.handle.xform = xform

        return True

    def onEndInteracting(self, kwargs, gadget_name):
        if not self._accept(gadget_name):
            if self.gadget:
                current_pos = self.handle.xform.extractTranslates()
                xform = hou.hmath.buildTranslate(current_pos)
                self.gadget.setTransform(xform)                    
                self.gadget.show(True)

            return False

        self.dragger.endDrag()

        return True

    def onDraw(self, draw_handle):
        try:
            self.gadget.draw(draw_handle)
        except:
            pass

Python scripting

Getting started

Next steps

Reference

  • hou

    Module containing all the sub-modules, classes, and functions to access Houdini.

Guru level

Python viewer states

Python viewer handles

Plugin types