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. :-)]