The process was quite interesting – I’d created jigs from Python and Ruby, but not from F#, so this was a first for me. It’s also a multi-stage jig, which is fun: we acquire the outer radius of the pattern followed by the radius of the smaller circle and the distance of the pen from the smaller circle’s center. At each point I’ve fixed the later parameters relative to the earlier ones, so the pattern scales appropriately (otherwise it' gets a little confusing). It’s clearly possible to fix the proportions differently – which would create a different basic pattern – or to generalise the command to allow the parameters to be entered independently.

I’ve also used a technique whereby we generate a rough version of the pattern during the jig to improve performance and then refine it afterwards once the parameters have been acquired. Which should be a useful technique for other application areas, of course.

Here’s the updated F# code:

// Declare a specific namespace and module name

module Spirograph.Commands

// Import managed assemblies

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open System

// Return a sampling of points along a Spirograph's path

let pointsOnSpirograph cenX cenY inRad outRad a tStart tEnd num =

[|

for i in tStart .. tEnd * num do

let t = (float i) / (float num)

let diff = inRad - outRad

let ratio = inRad / outRad

let x =

diff * Math.Cos(ratio * t) +

a * Math.Cos((1.0 - ratio) * t)

let y =

diff * Math.Sin(ratio * t) -

a * Math.Sin((1.0 - ratio) * t)

yieldnew Point2d(cenX + x, cenY + y)

|]

// Different modes of acquisition for our jig

type AcquireMode =

| Inner

| Outer

| A

type SpiroJig(ent) as this = class

inherit EntityJig(ent)

// Our member variables

letmutable (_pl : Polyline) = ent

letmutable _cen = Point3d.Origin

letmutable _inner = 0.0

letmutable _outer = 0.0

letmutable _a = 0.0

letmutable _mode = Outer

member x.StartJig(ed : Editor, pt) =

// Set our center and start with the outer radius

_cen <- pt

_mode <- Outer

let stat = ed.Drag(this)

if stat.Status <> PromptStatus.Cancel then

// Next we get the inner radius

_mode <- Inner

let stat = ed.Drag(this)

if stat.Status <> PromptStatus.Cancel then

// And finally the pen distance

_mode <- A

ed.Drag(this)

else

stat

else

stat

// Our sampler function to acquire the various distances

override x.Sampler prompts =

// We're just acquiring distances

let jo = new JigPromptDistanceOptions()

jo.UseBasePoint <- true

jo.Cursor <- CursorType.RubberBand

// Local function to acquire a distance and return

// the appropriate status

let getDist (prompts : JigPrompts)

(opts : JigPromptDistanceOptions) oldVal =

let res = prompts.AcquireDistance(opts)

if res.Status <> PromptStatus.OK then

(SamplerStatus.Cancel, 0.0)

else

if oldVal = res.Value then

(SamplerStatus.NoChange, 0.0)

else

(SamplerStatus.OK, res.Value)

// Then we have slightly different behavior depending

// on the info we're acquiring

match _mode with

// The outer radius...

| Outer ->

jo.BasePoint <- _cen

jo.Message <- "\nRadius of outer circle: "

let (stat, res) = getDist prompts jo _outer

if stat = SamplerStatus.OK then

_outer <- res

stat

// The inner radius...

| Inner ->

jo.BasePoint <-

_cen + new Vector3d(_outer, 0.0, 0.0)

jo.Message <- "\nRadius of smaller circle: "

let (stat, res) = getDist prompts jo _inner

if stat = SamplerStatus.OK then

_inner <- res

stat

// The pen distance...

| A ->

jo.BasePoint <-

_cen + new Vector3d(_outer, 0.0, 0.0)

jo.Message <-

"\nPen distance from center of smaller circle: "

let (stat, res) = getDist prompts jo _a

if stat = SamplerStatus.OK then

_a <- res

stat

// Our update override

override x.Update() =

// If getting the outer radius fix the other

// parameters relative to it (as the inner radius

// comes later we only need to fix the pen distance

// against it)

if _mode = Outer then

let frac = _outer / 8.0

_inner <- frac

_a <- frac * 3.0

elseif _mode = Inner then

_a <- _inner / 3.0

// Generate the polyline with low accuracy

// (fewer segments == quicker)

x.Generate(2)

true

// Generate a more accurate polyline

member x.Perfect() =

x.Generate(10)

member x.Generate(num) =

// Generate points based on the accuracy

let pts =

pointsOnSpirograph

_cen.X _cen.Y _inner _outer _a 0 300 num

// Remove all existing vertices but the first

// (we need at least one, it seems)

while _pl.NumberOfVertices > 1 do

_pl.RemoveVertexAt(0)

// Add the new vertices to our polyline

for i in 0 .. pts.Length-1 do

_pl.AddVertexAt(i, pts.[i], 0.0, 0.0, 0.0)

// Remove the first (original) vertex

if _pl.NumberOfVertices > 1 then

_pl.RemoveVertexAt(0)

end

// Our basic non-jig command

[<CommandMethod("ADNPlugins", "SPI", CommandFlags.Modal)>]

let spirograph() =

// Let's get the usual helpful AutoCAD objects

let doc =

Application.DocumentManager.MdiActiveDocument

let ed = doc.Editor

let db = doc.Database

// Prompt the user for the center of the spirograph

let cenRes = ed.GetPoint("\nSelect center point: ")

if cenRes.Status = PromptStatus.OK then

let cen = cenRes.Value

// Now the radius of the outer circle

let pdo =

new PromptDistanceOptions

("\nEnter radius of outer circle: ")

pdo.BasePoint <- cen

pdo.UseBasePoint <- true

let radRes = ed.GetDistance(pdo)

if radRes.Status = PromptStatus.OK then

let outerRad = radRes.Value

// And the radius of the smaller circle

pdo.Message <-

"\nEnter radius of smaller circle: "

let loopRes = ed.GetDistance(pdo)

if loopRes.Status = PromptStatus.OK then

let innerRad = loopRes.Value

// And finally the value of "a", the distance of the

// "pen" from the center of the smaller circle

pdo.Message <-

"\nEnter pen distance from center of smaller circle: "

let aRes = ed.GetDistance(pdo)

if aRes.Status = PromptStatus.OK then

let a = aRes.Value

// Now we can get a sampling of points along our path

let pts =

pointsOnSpirograph

cen.X cen.Y innerRad outerRad a 0 300 10

// And we'll add a simple polyline with these points

use tr =

db.TransactionManager.StartTransaction()

// Get appropriately-typed BlockTable and BTRs

let bt =

tr.GetObject

(db.BlockTableId,OpenMode.ForRead)

:?> BlockTable

let ms =

tr.GetObject

(bt.[BlockTableRecord.ModelSpace],

OpenMode.ForWrite)

:?> BlockTableRecord

// Create our polyline

let pl = new Polyline(pts.Length)

pl.SetDatabaseDefaults()

// Add the various vertices to the polyline

for i in 0 .. pts.Length-1 do

pl.AddVertexAt(i, pts.[i], 0.0, 0.0, 0.0)

// Add our polyline to the modelspace

let id = ms.AppendEntity(pl)

tr.AddNewlyCreatedDBObject(pl, true)

tr.Commit()

// Our jig-based command

[<CommandMethod("ADNPlugins", "SPIG", CommandFlags.Modal)>]

let spirojig() =

// Let's get the usual helpful AutoCAD objects

let doc =

Application.DocumentManager.MdiActiveDocument

let ed = doc.Editor

let db = doc.Database

// Prompt the user for the center of the spirograph

let cenRes = ed.GetPoint("\nSelect center point: ")

if cenRes.Status = PromptStatus.OK then

let cen = cenRes.Value

// Create the polyline and run the jig

let pl = new Polyline()

let jig = new SpiroJig(pl)

let res = jig.StartJig(ed, cen)

if res.Status = PromptStatus.OK then

// Perfect the polyline created, smoothing it up

jig.Perfect()

use tr =

db.TransactionManager.StartTransaction()

// Get appropriately-typed BlockTable and BTRs

let bt =

tr.GetObject

(db.BlockTableId,OpenMode.ForRead)

:?> BlockTable

let ms =

tr.GetObject

(bt.[BlockTableRecord.ModelSpace],

OpenMode.ForWrite)

:?> BlockTableRecord

// Add our polyline to the modelspace

let id = ms.AppendEntity(pl)

tr.AddNewlyCreatedDBObject(pl, true)

tr.Commit()

Now let’s try our new SPIG (short for Spiro-Jig) command.

First we get to select the outer radius:

Then the inner radius relative to a point on the outer circle’s circumference: