Through the Interface: Using a DrawJig from F# to create Spirograph patterns in AutoCAD

May 2015

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


« Plugin of the Month featured in the March/April edition of AUGIWorld | Main | Loading multiple linetypes into AutoCAD using .NET »

March 12, 2010

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

Last week we looked at a preliminary version of this application that made use of an EntityJig to display a Spirograph as we provided the values needed to define it. While that was a good start, I decided it would be better to show additional graphics during the jig process, to give a clearer idea of the meaning of the information being requested from the user. I wanted, for instance, to show temporary circles indicating the radii of the outer and inner circles, mainly to make it clearer how the various parameters affect the display of the resultant Spirograph pattern.

Anyway, my next step in this process was going to be a post showing how to implement IExtensionApplication from an F# application, but it turns out I’ve done that already <sigh>. But that’s good, as it leaves the coast clear for me to get into the rest of the implementation. I needed IExtensionApplication’s Initialize() callback to execute the demand-loading creation code provided in this recent post.

Here’s the source file I used for that:

// Declare a specific namespace and module name


namespace Spirograph


// Import managed assemblies


open Autodesk.AutoCAD.Runtime

open DemandLoading


type App() =

  interface IExtensionApplication with 

    override x.Initialize() =



      with _ -> ()

    override x.Terminate() =


The application itself now works slightly differently, but using the same principles, overall. I’ve removed the old prompt-based SPI command, renaming the jig-based SPIG command to be SPI. The new jig has a little more to it – as we’re using a DrawJig to draw additional geometry – but it shouldn’t be any harder to understand than the last one. I’m also using a tip suggested by Fenton Webb to improve the performance of a complex jig: it’s good practice to check WorldDraw.RegenAbort during your WorldDraw, and if the flag is set you should exit immediately. Not doing so can make your jig appear sluggish. I’ve done this in a number of places, but I’ve also kept the segment count low when drawing our Spirograph pattern inside the jig, just to decrease the likelihood that we’ll have to cancel the draw operation.

Here’s the application’s main F# source file:

// 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 Autodesk.AutoCAD.GraphicsInterface

open System

open DemandLoading


// 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() as this = class

  inherit DrawJig()


  // Our member variables


  let mutable (_pl : Polyline) = null

  let mutable _cen = Point3d.Origin

  let mutable _norm = new Vector3d(0.0,0.0,1.0)

  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, pl) =


    // Set our center and start with the outer radius


    _cen <- pt

    _pl <- pl

    _mode <- Outer

    _norm <-



    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







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


        if oldVal = res.Value then

          (SamplerStatus.NoChange, 0.0)


          (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



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



    // The pen distance...


    | A ->

      jo.BasePoint <-

        _cen + new Vector3d(_outer - _inner, 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



  // Our WorldDraw function to display the Spirograph and

  // the related temporary graphics


  override x.WorldDraw(draw : WorldDraw) =


    // Save our current colour, to reset later


    let col = draw.SubEntityTraits.Color


    // Make our construction geometry red


    draw.SubEntityTraits.Color <- (int16 1)


    match _mode with


    | Outer ->  // Draw the outer circle


      draw.Geometry.Circle(_cen, _outer, _norm)

        |> ignore


    | Inner ->  // Draw the outer and inner circles


      draw.Geometry.Circle(_cen, _outer, _norm)

        |> ignore


        (_cen + new Vector3d(_outer - _inner, 0.0, 0.0),

        _inner, _norm)

          |> ignore


    | A ->  // Draw the outer and inner circles


      draw.Geometry.Circle(_cen, _outer, _norm)

        |> ignore


        (_cen + new Vector3d(_outer - _inner, 0.0, 0.0),

        _inner, _norm)

          |> ignore


    // Check the RegenAbort flag...

    // If it's set then we drop out of the function


    if not draw.RegenAbort then


      draw.SubEntityTraits.Color <- col


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


      if not draw.RegenAbort then


        // Generate our polyline



        if not draw.RegenAbort then


          // And then draw it


          draw.Geometry.Polyline(_pl, 0, _pl.NumberOfVertices-1)

            |> ignore




  // Generate a more accurate polyline


  member x.Perfect() =




  member x.Generate(num) =


    // Generate points based on the accuracy


    let pts =


        _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



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





// Our jig-based command


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

let spirojig() =


  // Let's get the usual helpful AutoCAD objects


  let doc =


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

    let res = jig.StartJig(ed, cen, pl)


    if res.Status = PromptStatus.OK then


      // Perfect the polyline created, smoothing it up




      use tr =



      // Get appropriately-typed BlockTable and BTRs


      let bt =



          :?> BlockTable


      let ms =




          :?> BlockTableRecord


      // Add our polyline to the modelspace


      let id = ms.AppendEntity(pl)

      tr.AddNewlyCreatedDBObject(pl, true)




[<CommandMethod("ADNPLUGINS", "REMOVESP", CommandFlags.Modal)>]

let removeSpirograph() =





    let doc =



      ("\nThe Spirograph plugin will not be loaded" +

      " automatically in future editing sessions.")

  with _ -> ()

When we run the SPI command, we see a red construction geometry being drawn along with our Spirograph pattern.

We start with the radius of the outer circle:

Defining our outer radius

Followed by the inner circle, which we can make small:

Defining our inner radius (smaller)

Or large:

Defining our inner radius (larger)

And we then define the distance of the pen from the inner circle’s centre, whether close to it:

Defining our pen distance (smaller)

Or further away:

Defining our pen distance (larger)

You’ll probably have noticed that I’ve structured the application as a potential Plugin of the Month. I haven’t yet decided if it should become one – as it’s really just for fun – but I decided to structure it as such, just in case.

blog comments powered by Disqus


10 Random Posts