August 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            








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

      try

        RegistryUpdate.RegisterForDemandLoading()   

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

      ed.CurrentUserCoordinateSystem.CoordinateSystem3d.Zaxis

 

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

      stat

 

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

      draw.Geometry.Circle

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

      draw.Geometry.Circle

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

 

        x.Generate(2)     

        if not draw.RegenAbort then

 

          // And then draw it

 

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

            |> ignore

 

    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 jig-based command

 

[<CommandMethod("ADNPLUGINS", "SPI", 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()

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

 

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

 

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

let removeSpirograph() =

 

  try

    RegistryUpdate.UnregisterForDemandLoading()

 

    let doc =

      Application.DocumentManager.MdiActiveDocument

    doc.Editor.WriteMessage

      ("\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

Feed/Share

10 Random Posts