I’ve been toying for some time with the idea of writing some code to turn AutoCAD into a Spirograph, a device which I’m sure fascinated and inspired many of you as children (just as it did me). I chose to write the application in F# for a couple of reasons: this type of task is fundamentally mathematical in nature – so a functional programming language should be well-suited to the task – and I needed to dust off my F# skills in time for my F# class at AU. Searching the web I came across this helpful post providing some functional C# code to plot points along the path followed by a Spirograph (in this case for UI automation), which in turn referenced a post with the underlying mathematics stated relatively simply.

I won’t bother reproducing the relevant contents of these two posts: please visit them if you’re interested in the background to the code. The changes I’ve made have largely been around gathering the appropriate information needed to customize each path. The code creates a simple polyline with plain-old linear segments (we’ll use enough to easily approximate a curve, although choosing this “simple” approach means we need to create more segments than would otherwise been needed to show the path smoothly, which clearly means heavier objects).

Here’s some F# code to create Spirograph-style patterns in AutoCAD:

// 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 =

[|

for i in tStart .. tEnd * 10 do

let t = (float i) * 0.1

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)

yield new Point2d(cenX + x, cenY + y)

|]

// Our command

[<CommandMethod("spi")>]

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

// 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()

When we run our SPI command we get prompted for a number of parameters: the centre of the path, the radius of the outer circle, the radius of the smaller circle which will “roll” around the outer one, and finally the position of the pen relative to the centre of the smaller circle:

Command: SPI

Select center point:

Enter radius of outer circle:

Enter radius of smaller circle:

Enter pen distance from center of smaller circle:

Here’s a simple script you can execute to create a number of simple paths (by copy and pasting the contents into a text file, saving it with the .SCR extension, and running it inside AutoCAD via the SCRIPT command):

SPI 0,0,0 10 5 3

SPI 12,0,0 10 6 4

SPI 28,0,0 10 3 2

SPI 48,0,0 8 1 3

SPI 68,0,0 10 3 1

SPI 84,0,0 8 2 4

SPI 100,0,0 8 1 2

SPI 116,0,0 8 2 2

SPI 132,0,0 10 3 3

SPI 150,0,0 10 2 .3

SPI 168,0,0 10 3 .5

SPI 185,0,0 12 7 3

SPI 202,0,0 12 7 4

Which creates these fairly simple Spirograph paths:

By just playing around selecting values you can come up with much more complex shapes:

[A quick aside regarding the command’s user-input: the various distances are input relative to the centre of the Spirograph, which isn’t ideal. I would probably have preferred to select the last two items - the smaller circle radius and the pen distance – relative to the center of the smaller circle. But as we’re using GetDistance() to get the outer radius we’d have to choose an arbitrary location for this circle, which would actually have made the selection process more confusing. If we knew where the distance point was actually selected we could implement this easily, but using GetPoint() instead of GetDistance() (along with some simple vector arithmetic to calculate the underlying distance between the two circles’ centres) means the user couldn’t then enter a numerical value for the outer radius (which would make it all more confusing still). Implementing a jig to display temporary circles etc. as the locations get selected would probably be the way to go, should we want to turn this into a real application, but as it’s all really just a bit of fun I’m going to leave it at that. :-)]