After my initial fooling around with turning AutoCAD into a Spirograph using F#, I decided to come back to this and bolt a jig on the front to make the act of making these objects more visual and discoverable.
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)
yield new 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
let mutable (_pl : Polyline) = ent
let mutable _cen = Point3d.Origin
let mutable _inner = 0.0
let mutable _outer = 0.0
let mutable _a = 0.0
let mutable _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
else if _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:
Which we can clearly make larger:
And finally we get to choose an appropriate pen distance: