How To ... Draw a Freehand Spline

3ds Max does not provide a built-in function for drawing splines freely with the mouse. Surprisingly enough, the old Autodesk 3D Studio DOS had such a feature.

Using the power of MAXScript, we can implement this feature as a simple MacroScript. It will let us draw a spline in the ground plane using the mouse.

Related Topics:

Defining MacroScripts

SplineShape : Shape

Spline Shape Common Properties, Operators, and Methods

Picking Points in the Viewports

MAXScript Message and Query Dialogs

NATURAL LANGUAGE

Package the code as macroScript to be able to use as a button, menu item or shortcut.

Define a function to measure the distance between the last and current mouse position and add a new knot to the spline when a threshold is exceeded.

Define a drawing function to capture the mouse movement using the built-in pickPoint function.

Put the complete creation code in an undo context to be able to undo the drawing.

Create a new splineShape object.

Let the user click in the viewport to define the start of the spline.

If the user right-clicked, cancel the operation

If the user left-clicked, add a knot to the spline and call the draw function.

As long as the user moves the mouse, the pickPoint callback will keep on measuring the segment lengths and adding new knots when needed.

If the user clicked again, ask about closing the spline.

If answer is yes, close the spline, select it and exit the script

If answer is no, just select the spline and exit the script.

MAXSCRIPT

macroscript FreeSpline category:"HowTo" tooltip:"FreeSpline"
(
local old_pos
local new_spline
local second_knot_set

fn get_mouse_pos pen_pos old_pen_pos =
(
 if old_pos == undefined then old_pos = old_pen_pos
 if distance pen_pos old_pos > 10 then
 (
  if second_knot_set then
   addKnot new_spline 1 #smooth #curve pen_pos
  else
  (
   setKnotPoint new_spline 1 2 pen_pos
   second_knot_set = true
  )
  old_pos = pen_pos
  updateShape new_spline
 )-- end if
)-- end fn
fn draw_new_line old_pen_pos =
(
 pickPoint mouseMoveCallback:#(get_mouse_pos,old_pen_pos)
)

undo"Free Spline"on
(
 new_spline = splineShape ()
 old_pen_pos = pickPoint ()
 if old_pen_pos == #RightClick then
 (
  delete new_spline
 )
 else
 (
  select new_spline
  new_spline.pos = old_pen_pos
  addNewSpline new_spline
  addKnot new_spline 1 #smooth #curve old_pen_pos
  addKnot new_spline 1 #smooth #curve old_pen_pos
  second_knot_set = false
  draw_new_line old_pen_pos
  q = querybox "Close Spline?" title:"Free Spline"
  if q then
  (
   close new_spline 1
   updateshape new_spline
  ) 
  select new_spline
 )--end else
)--end undo
)--end script

Step-By-Step

macroscript FreeSpline category:"HowTo" tooltip:"FreeSpline"
(

The macroScript will be called FreeSpline . To use the script, you can go to Customize... and drag the script from the category "HowTo" to a toolbar, a menu, a quad menu or assign to a keyboard shortcut.

Defining Macro Scripts

local old_pos
local new_spline
local second_knot_set

We will need some local variables to store the last knot position and the new object created by the script. The third variable will be used as a flag to tell us whether we just started creation of the spline.

These variables will exist only inside the macroScript’s context as we declare them as local.

Scope of Variables

fn get_mouse_pos pen_pos old_pen_pos =
(

We create our own function to handle the drawing of new knots in the splineShape. This function receives two parameters – the current position of the mouse, and the result of the first click captured by the pickPoint function (see below).

Defining Custom Functions

if old_pos == undefined then old_pos = old_pen_pos

If we don’t know of an existing Knot (creation just started!), we set the value of the old_pos local variable to the first clicked point provided by the old_pen_pos parameter.

If Expression

Undefined Value

if distance pen_pos old_pos > 10 then
(

Next we check the distance between the last known knot’s position and the current mouse position. If the distance is longer than 10 units...

If Expression

Distance_Point3

if second_knot_set then

The flag variable we defined will contrain either true or false. When the variable contains false, spline creation has just started. Because a spline must have at least two knots (vertices), we will create the first knot twice and then set this flag to false. When the next knot position is to be set, instead of creating a new one, we will reposition the existing second knot to the new coordinates and then set the flag to true.

When the flag is set to true, we will just create the next new knot - see next line...

addKnot new_spline 1 #smooth #curve pen_pos

...we go on and place a new knot, thus adding a new segment to the spline shape. The addKnot function requires a splineShape object, the index of the spline (we have just 1), the type of the knot (we use #smooth in our case), and the position to put the knot at.

SplineShape : Shape

else
(
setKnotPoint new_spline 1 2 pen_pos

When the flag is set to false, we have to move the existing second knot instead of creating a new one...

second_knot_set = true

Then we set the flag to true. Next time, a new knot will be created instead...

)
old_pos = pen_pos

Then we remember the new knot’s position to use by the next mouse movement check.

updateShape new_spline

To see the changes made to the spline, we update the splineShape object.

SplineShape : Shape

) -- end if
) -- end fn
fn draw_new_line old_pen_pos =
(

We create another function to handle the capturing of the mouse motion.

Defining Custom Functions

pickPoint mouseMoveCallback:#(get_mouse_pos,old_pen_pos)

Our function calls only the built-in pickPoint function which provides an optional callback which calls the function we just defined (get_mouse_point) and passes the current mouse position and any user parameters supplied (in our case, old_pen_pos).

As long as the mouse is moving and no clicks are encountered, the pickPoint’s mouseMoveCallback will constantly call our function and thus create new knots at distance of 10 units...

Picking Points in the Viewports

)
undo "Free Spline" on
(

We will put the complete code in an undo context to be able to undo the whole drawing result.

undo

new_spline = splineShape ()

When starting the script, we first construct an empty splineShape object.

SplineShape : Shape

old_pen_pos = pickpoint ()

Then we go into pickPoint mode (but this time without a mouse callback). The function will wait for a click in the viewport and return the position of the picked point, or #RightClick if the Right Mouse Button has been pressed.

Picking Points in the Viewports

if old_pen_pos == #RightClick then
(
delete new_spline
)

If the Right Mouse Button has been actually pressed, we just delete the newly created splineShape and finish the script.

If Expression

else
(
select new_spline

If the pickButton has returned a position value and not #RightClick, we can select the splineShape...

new_spline.pos = old_pen_pos

Set the position of the splineShape to the clicked point...

addNewSpline new_spline

...add a new spline to the empty splineShape...

SplineShape : Shape

addKnot new_spline 1 #smooth #curve old_pen_pos
addKnot new_spline 1 #smooth #curve old_pen_pos

...and add two new knots to the newly create spline in the splineShape. As mentioned above, a spline should have at least two knots, so we create the first two with the same position and will then move the second knot instead of creating a new one.

SplineShape : Shape

second_knot_set = false

We "lower the flag" by setting it to false. This way, the script will know that we just created the first two knots and the second one has to be set to the next mouse position captured by the script. When this is done in the get_mouse_pos function, we will "raise the flag" again by setting it to true and thus know that the initial spline creation steps are over...

draw_new_line old_pen_pos

Now we can call our draw_new_line function and pass the first clicked point as parameter. That function will start monitoring the mouse movements until a new click occurs.

Defining Custom Functions

q = querybox "Close Spline?" title:"Free Spline"

If a new click has been captured by the pickPoint function, the draw_new_line function will return to this point. Now we can ask the user whether he wants to close the newly drawn spline.

MAXScript Message and Query Dialogs

if q then
(
close new_spline 1
updateshape new_spline

If the answer to the query is Yes, we close the first and only spline in the splineShape and update the internal data structures to reflect the changes.

If Expression

SplineShape : Shape

) 
select new_spline

Finally, we select the newly-created splineShape in the scene before exiting.

)--end else
)--end undo
)--end script

Using the Script

Evaluate the script. To use it, you can use Customize... to drag the script from the category "HowTo" to a toolbar, a menu, a quad menu or to assign to a keyboard shortcut.

Execute the FreeSpline script and left-click in the top or perspective viewport. Drag the mouse freely to draw a spline. Left-click again to finish drawing. Answer the prompt with Yes to close or No to leave the spline open.

Where to go from here

This is just some basic code. You could try to add a UI with controls for segment length threshold, spline segment and knot types (linear, smooth, bezier etc.)

Back to

"How To" Tutorials Index Page