Scripted Manipulators

Manipulators are a type of helper object designed to support direct manipulation of parameters in the 3D viewports. They can be set up to manipulate parameters on objects, modifiers, controllers and nodes.

There is a "Manipulate" button on the main toolbar. When pressed, the system will show all the manipulators for selected objects. As the mouse passes over a manipulator, it turns red, and a tool tip pops up with the name of the current value of the parameter that is affected by that manipulator. To manipulate the value, you click down on the gizmo with the left mouse button and drag it. Most manipulators are designed so that the gizmo will track under the current mouse position, if possible.

Note: "Manipulate" is not a separate mode - it modifies whatever mode you are currently in (select, move, create, etc.) to show manipulators and allow manipulation. If you are in move mode, for example, you can still move things around as usual. The only difference is that if you click down on a manipulator, you will manipulate, instead of move.

There are two flavors of manipulators. The first are manipulators that are automatically created when you enter "Manipulate" mode. The hotspot manipulator is like that. The second kind is standalone objects, like the slider manipulator. These are usually used in conjunction with parameter wiring to get make them useful.

To create a standalone manipulator, you go to the "Helpers" section of the create panel, and select "Manipulators" in the drop-down list.

Standalone manipulators do not need to be selected in order to be manipulated. However, the automatically created manipulators, like the hotspot manipulator, are only displayed for selected objects.

Manipulators support both 3d gizmos and 2d (screen space) gizmos. The hotspot manipulator is an example of a 3d gizmo, and the slider is an example of a 2d gizmo.

Scripted Manipulators

Manipulators can be created either as standard MAX plug-ins in C++, or they can be implemented in MAXScript. In MAXScript, they are a type of Scripted Plug-in object, so if you want to write your own manipulator, it would be a good idea to read about Scripted Plug-ins first. They are very similar to scripted geometry objects.

3 scripted manipulators that you can use as sample code can be found in the "..\stdplugs\stdscripts" folder.

Radius Manipulator

The radius manipulator is fairly simple, so it makes a good example to describe the scripted manipulator system.

The code for the manipulator is included below, followed by a detailed description of how it works.

EXAMPLE

   --------------------------------------------------------------------
   -- Generic radius manipulator
   -- Written by Scott Morrison
   -- This manipulator sets the radius on any object or modifier with
   -- a parameter named "radius". It creates a circle gizmo of the appropriate
   -- radius centered at the origin in the XY plane.
   plugin simpleManipulator radiusManip
   name:"RadiusManip"
   invisible:true
   (
   -- Create the green and red colors for the gizmo
   local g = [0, 1, 0], r = [1, 0, 0]
   -- This manipulator manipulates any node with a "radius" property
   on canManipulate target return (findItem (getPropNames target) #radius) != 0
   -- Create the manipulator gizmo.
   -- This is called initially and whenever the manipulator target changes
   on updateGizmos do
   (
   -- Clear the current gizmo cache
   this.clearGizmos()
   -- Set the radius of circle gizmo a little bigger than the target radius
   giz = manip.makeCircle [0,0,0] (target.radius * 1.01) 28
   -- Add the circle to the manipulator
   this.addGizmoShape giz 0 g r
   -- return the ToolTip string
   return node.name + " radius = " + target.radius as string
   )
   -- mouseMove is called on every mouse move when dragging the manip
   -- It needs to convert the mouse position 'm' into a new value for the radius
   on mouseMove m which do
   (
   -- Create the XY plane.
   -- manip.makePlaneFromNormal takes a normal vector and a point
   -- and creates a plane passing through the point with the given normal
   local pl = manip.makePlaneFromNormal z_axis [0, 0, 0],
   projectedPoint = [0,0,0]
   -- Compute the hit-ray in local coordinates
   viewRay = this.getLocalViewRay m
   -- Intersect the plane with the view ray
   res = pl.intersect viewRay &projectedPoint
   -- If the intersection worked, set the radius
   if (res) then target.radius = (length projectedPoint) / 1.01
   )
   )
   -------------------------------------------------------------------- 

The header:

   plugin simpleManipulator radiusManip
   name:"RadiusManip"
   invisible:true

Indicates to MAXScript that this is a scripted manipulator plug-in called "RadiusManip". The "invisible:true" tells the system not to make a creation button for the manipulator in the create panel.

The body:

A set of handlers for various events.

   on canManipulate target return (findItem (getPropNames target) #radius) != 0

" canManipulate " is called on every manipulator, for every node that is selected when the "Manipulate" button is pressed. The " target " parameter is the object that we might potentially manipulate. It is called on the base object, all the modifiers on the object, and transform controllers on the object's node. Also, if the transform controller is a PRS controller, it calls " canManipulate " on the position, rotation and scale controllers.

In the case of the radius manipulator, it can manipulate any object that has a property named " radius ".

If you want to create a manipulator that works on a specific object type, say a sphere, you can say:

   on canManipulate target return classOf target == sphere

There is an alternative handler that may be needed in some cases called " canManipulateNode n ". This is called passing in the each selected node to the handler. This is not normally needed, but available if your manipulator wants to manipulate property of a node other than the ones that are passed as the " target " to " canManipulate ".

Note:

While a Scripted Manipulator Plug-in could implement both handlers at the same time, the resulting behavior could confuse the user. It is a good idea to implement only the one handler that suits your needs.

The next handler is called " updateGizmos ", and this is called whenever a manipulator need to build its gizmos. This happens when the manipulator is created, and whenever the target that it is manipulating changes.

When MAXScript creates a manipulator, it sets up some variables that are available inside the calls to its handlers. One of these is called " target " and it is the object, modifier or controller that is being manipulated. Another is called " this " and it is the manipulator itself. It also sets up some constant values that can be used as flags on gizmos. These will be described later. There is also a " node " value available which is the node that owns the target.

The first thing every manipulator must to in the " updateGizmos " handler is call " this.clearGizmos() ". This clears any currently cached gizmos.

Next it creates a set of gizmos that will be displayed in the viewport. In the case of the radius manipulator, it creates a single circle that represents the radius being manipulated.

   giz =manip.makeCircle [0,0,0] (target.radius * 1.01) 28

" manip " is a exported set of utility interface functions that manipulators can use. See the "Reference" section below for full details. In this case we are creating a circle, centered at the origin, with radius 1.01 times the radius we are manipulating, and 28 segments. The "1.01" factor was added so that the gizmo will stick out a little bit from the object it is manipulating. If we used the radius directly, then it might not be visible in the viewport, because it co-exists with the object it is manipulating.

Note that 3d gizmos are defined in the local coordinate system of the node that owns the manipulator target. The system will automatically compensate if the node is moved, rotated or scaled when displaying or hit-testing the manipulator.

Next, we add the gizmo to the manipulator:

   this.addGizmoShapegiz 0 g r

This tells the manipulator to add the shape gizmo to the manipulator, with no special flags values ie. the "0". All of the flags will be described below. Additionally, the command indicates to use green ("g") as the unselected color and red ("r") as the selected color. The selected color is used when the mouse passes over it, and the unselected color is used when the mouse isn't over it.

Finally, this method returns a string value that will be used as the tool tip when the mouse passes over the gizmo.

In general, gizmos can be made from meshes, shapes (wire frame), text and markers. The details are covered in the reference section below.

The next handler is called " mouseMove m which ". This is called on every mouse movement when the target is being manipulated. The " m " parameter holds the screen coordinates of the mouse location, and the " which " parameter is an index that indicates which gizmo is being dragged. The gizmos are numbered in the order of their creation in " updateGizmos ", starting at 0.

The mouseMove handler is usually the trickiest part of the manipulator to implement. It needs to update the value of the parameter being manipulated in such a way that the manipulator gizmo tracks under the mouse, if possible.

For manipulators that exist in 3d space, this is normally done by computing a " hit-ray ", which is a ray in 3d space that passes through the mouse position, and travels in the direction of the view.

This is computed as follows:

   viewRay = this.getLocalViewRaym

This view ray is then intersected with some plane, and the new parameter value is computed using the intersection point.

In the case of the radius manipulator, the plane we use is the XY plane, because the radius circle lies on the XY plane, in local coordinate space.

This plane is computed as follows:

   local pl = manip.makePlaneFromNormalz_axis [0, 0, 0],

This create "pl" which is a plane whose normal is the Z axis, and which passes through the origin ([0,0,0]).

To intersect this with the view ray, we use the "intersect" operation on planes:

   projectedPoint= [0,0,0]
   res =pl.intersect viewRay &projectedPoint

We set up " projectedPoint " first at the holder of the result of the intersection. The return value " res " is a boolean value that tells us if the intersection worked or not. If it returns true, the intersection worked, if it returns false, it failed. It can fail if the plane is parallel to the view ray.

Once we have the intersection point, in " projectedPoint ", then we use that to determine a new value for the radius. In this case we use the distance from the origin (" length projectedPoint ") as the new radius. We scale it by dividing by 1.01 to compensate for the fact that we made the gizmo 1.01 time bigger than the actual radius being manipulated.

That's it! Generally, you will need to use some trigonometry and linear algebra in your "mouseMove" handler. The idea behind direct manipulation is that the gizmo should track directly under the mouse.

For a more complicated example of a 3d manipulator, check the code in UVWManip.ms .

Standalone Manipulators

Standalone manipulators, like the 2d slider, require a bit more work.

The code in SliderManip.ms will be used for this discussion.

The header has a bit more information:

   plugins simpleManipulator sliderManipulator
   name:"Slider"
   classID:#(0x47db14ef, 0x4e9b5990)
   category:"Manipulators"

Since standalone manipulators can be saved in the MAX file, it needs a class ID. See the MAXScript reference for more information about that. It also does not include the " invisible:true " line, which means that the system will create a button for it in the " Create " panel. It will be placed in the " Manipulators " section of the helper create panel.

We also need to define a parameter block and rollout UI for the object. This is done in the same way as other scripted plug-ins.

Standalone manipulators manipulate themselves, so the " canManipulate " handler looks like this:

   on canManipulate target return (classOf target) == sliderManipulator

We also need to provide a creation tool that handles mouse interaction when creating the manipulator. This is handle exactly like other scripted plug-ins.

A standalone manipulator also needs some special code in its " updateGizmos " handler. For the slider this looks like:

   -- If this is not a standalone manip, get values from the manip target
   if (target != undefined) then
   (
   this.value = target.value
   this.minVal = target.minVal
   this.maxVal = target.maxVal
   this.xPos = target.xPos
   this.yPos = target.yPos
   this.width = target.width
   this.hide = target.hide
   this.sldName = target.sldName
   this.snapToggle = target.snapToggle
   this.snapVal = target.snapVal
   unselColor = greenColor
   )
   else
   (
   unselColor = yellowColor
   )

The test of " target != undefined " is to see if this if this is a manipulator, or standalone object. When the object is standalone, the value of " target " is undefined, and it uses the value of the parameters from its own parameter block. It target is defined, then this means it is manipulating. In that case, we want to copy the values of the parameters from the target that we are manipulating.

Reference

Scripted manipulators can have the following handlers:

on canManipulate target

This returns a value that says whether this manipulator manipulate the given target.

on canManipulateNode n

This returns a value that says whether this manipulator manipulate the given node.

Note:

A manipulator should only implement one of these handlers.

on updateGizmos

This is called whenever the manipulator needs to build its gizmos. It returns a string value that is used for the tool tip. If it returns an empty string, no tool tip is displayed.

on mouseMove m which

This is called when the user has grabbed a gizmo and is dragging it.

The " m " parameter hold the current screen coordinates of the mouse, and the "which" parameter indicates the 0-based index of the gizmo that is being dragged. This is what handles that actual manipulation.

Note:

Normally this will set a value of a parameter of the manipulation target based on the mouse location.

on mouseDown m which

Called when the user first clicks the mouse down on the gizmo.

on mouseUp m which

Called when the user releases the mouse after manipulation is done.

Helper Functions

There are some built-in functions that manipulators support, and a couple of helper packages with utility functions.

The simpleManipulator type itself has these functions available:

this.clearGizmos()

This must be called at the beginning of the "updateGizmos" handler in order to clear the current gizmo cache.

this.addGizmoMesh mesh flags unselColor selColor

This creates a gizmo from a mesh (or geometry in MAX lingo). The mesh can be any arbitrary MAX mesh, and created with the tools in MAXScript for creating meshes. One convenient way to do this is to create an instance of a primitive and get the mesh from it.

The flags can be 0, or one or more of these value. If you want more than one flag to apply, then you add the values together.

Here are all the flags, and whether that can be used with addGizmoText, addGizmoMarker, addGizmoShape and addGizmoMesh:

gizmoDontDisplay

Tells the system to not display the gizmo. It will be hit-tested, but not displayed, applies to all.

gizmoDontHitTest

Tells the system to not hit-test the gizmo. It will be displayed, but not hit-tested, applies to all.

gizmoActiveViewportOnly

Tells the system to only display and hit-test this gizmo in the active viewport, applies to all.

this.addGizmoShapegizmo Shape flags unselColor selColor

This creates a gizmo from a shape object. The gizmoShape can be created using functions from the "manip" package, described below.

The flags can take on the same value as for "addGizmoMesh", with two new values supported:

gizmoUseScreenSpace

This tells the system to interpret the coordinates of the shape as device coordinates in the viewport, not as 3d values. The values are still specified as 3d points, but the "Z" coordinate is ignored, applies to all but addGizmoMesh.

gizmoUseRelativeScreenSpace

This is like gizmoUseScreenSpace, but the coordinates are specified as values from 0.0 to 1.0, and interpreted in each viewport as a percentage of the width or height of the viewport, applies to all but addGizmoMesh.

addGizmoMarker markerType position flags unselColor selColor

The creates a gizmo using a marker. The value of the "markerType" parameter can be:

#point #hollowBox #plusSign #asterisk #xMarker #bigBox #circle #triangle #diamond #smallHollowBox #smallCircle #smallTriangle #smallDiamond #dot #smallDot

The position is a point in 3d space, or 2d screen space. The flags are the same ones supported by addGizmoShape.

addGizmoText text position flags unselColor selColor

This creates a gizmo that is text on the screen. Note that text cannot be hit-tested or grabbed by the mouse. It is used for display purposes only.

getLocalViewRay m

This function takes a mouse position and returns the ray that passes through that mouse position in the direction of the view. It is returned in the local coordinates of the node that owns the manipulator target.

The "manip" package

"manip" is a value in MAXScript that contains a set of useful interface functions for manipulators. The functions it exports are:

manip.makeSphere position radius segments

This returns a mesh sphere with the given position, radius, and segments.

manip.makeTorus position radius1 radius2 segments sides

Create a torus mesh with the given values.

manip.makeBox position length width height lengthSegs widthSegs heightSegs

Creates a box mesh with the given parameters.

manip.makePlaneFromNormal normal point

Creates a Plane object with the given normal that passes through the given point.

manip.makeCircle center radius segments

Creates a circle shape in the XY plane with the given parameters.

manip.makeGizmoShape()

Creates an empty gizmo shape. See the details of "GizmoShape" below for how to use this.

Plane objects

The plane objects returned from "manip.makePlaneFromNormal" have the following functions available:

intersectionPoint= [0,0,0]
plane.intersect ray &intersectionPoint

This intersects the given ray with the plane. It returns true, if it succeeds, and false if it doesn't If it works, then the intersection point is set in the "intersectionPoint" value, which must be initialized to a Point3 value in advance.

plane.mostOrthogonal ray otherPlane

This returns the plane that is most orthogonal to the given ray. This means the plane that is most "square" to the view direction.

Sometimes a manipulator might choose between two different planes on which to project a ray. It is usually best to project it to the plane which faces the view ray most directly, and this function determines that. See "UVWManip.ms" for an example of how to use this.

plane.getNormal()

Returns the normal of a plane.

plane.getPoint()

Return the point that the plane passes through.

plane.getPlaneConstant()

Return the value of the plane constant. This is the value of "D" in the equation that defines the plane:

Ax + By +Cz + D = 0

GizmoShape objects

A GizmoShape is a shape object that you can use to construct wire-frame gizmos. You can get an empty GizmoShape as follows:

local giz = manip.makeGizmoShape()

The functions available are:

giz.addPoint point

This adds a new point to the shape. The points are connected by line segments in order to create the wireframe. If you want a closed shape, you have to add the first point again as the last point.

giz.startNewLine()

This begins a new line segment in the shape.

giz.transform matrix

This transforms the gizmo by the given Matrix3 transform.

Note: See UVWManip.ms for an example of creating 3D gizmos with GizmoShape.