October 2014

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  










« Real-time positioning | Main | Creating demand-loading entries automatically for your AutoCAD application using C#, F# or VB.NET »

March 05, 2010

Using a jig from F# to create Spirograph patterns in AutoCAD

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:

Acquiring our outer radius

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

A small inner radius

Which we can clearly make larger:

A larger inner radius

And finally we get to choose an appropriate pen distance:

Adjusting our pen distance

blog comments powered by Disqus

Feed/Share

10 Random Posts