September 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        








« Nesting instincts: getting more out of transactions inside AutoCAD using .NET | Main | Importing and pixelizing images inside AutoCAD using F# »

January 28, 2009

Implementing a simple graphing tool inside AutoCAD using F#

Well, I couldn't resist... as I mentioned in the last post - where we looked at creating a simple graph inside AutoCAD as an example of modifying objects inside nested transactions - the idea of graphing inside AutoCAD is a good fit for F#. This is for a number of reasons: F# is very mathematical in nature and excels at processing lists of data. I also spiced it up a bit by adding some code to parallelise some of the mathematical operations, but that didn't turn out to be especially compelling with my dual-core laptop. More on that later.

Here's the F# code:

// Use lightweight F# syntax


#light


// Declare a specific namespace and module name


module Grapher.Commands


// Import managed assemblies


open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.ApplicationServices

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.Geometry


// Define a common normalization function which makes sure

// our graph gets mapped to our grid


let normalize fn normFn x minInp maxInp maxOut =

  let res =

    fn ((maxInp - minInp) * x / maxOut)

  let normRes = normFn res

  if normRes >= 0.0 && normRes <= 1.0 then

    normRes * (maxOut - 1.0)

  else

    -1.0


// Define some shortcuts to the .NET Math library

// trigonometry functions


let sin x = System.Math.Sin x

let cos x = System.Math.Cos x       

let tan x = System.Math.Tan x       


// Implement our own normalized trig functions

// which each map to the size of the grid passed in


let normSin max x =

  let nf a = (a + 1.0) / 2.0 // Normalise to 0-1

  let res =

    normalize

      sin nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


let normCos max x =

  let nf a = (a + 1.0) / 2.0 // Normalise to 0-1

  let res =

    normalize

      cos nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


let normTan max x =

  let nf a = (a + 3.0) / 6.0 // Normalise differently for tan

  let res =

    normalize

      tan nf (Int32.to_float x)

      0.0 (2.0 * System.Math.PI) (Int32.to_float max)

  Int32.of_float res


// Now we declare our command


[<CommandMethod("graph")>]

let gridCommand() =


  // We'll time the command, so we can check the

  // sync vs. async efficiency


  let starttime = System.DateTime.Now


  // Let's get the usual helpful AutoCAD objects


  let doc =

    Application.DocumentManager.MdiActiveDocument

  let ed = doc.Editor

  let db = doc.Database


  // "use" has the same effect as "using" in C#


  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


  // Function to create a filled circle (hatch) at a

  // specific location


  // Note the valid use of tr and ms, as they are in scope


  let createCircle pt rad =

    let hat = new Hatch()

    hat.SetDatabaseDefaults();

    hat.SetHatchPattern

      (HatchPatternType.PreDefined,

       "SOLID")


    let id = ms.AppendEntity(hat)

    tr.AddNewlyCreatedDBObject(hat, true)


    // Now we create the loop, which we make db-resident

    // (appending a transient loop caused problems, so

    // we're going to use the circle and then erase it)


    let cir = new Circle()

    cir.Radius <- rad

    cir.Center <- pt


    let lid = ms.AppendEntity(cir)

    tr.AddNewlyCreatedDBObject(cir, true)


    // Have the hatch use the loop we created


    let loops = new ObjectIdCollection()

    loops.Add(lid) |> ignore

    hat.AppendLoop(HatchLoopTypes.Default, loops)

    hat.EvaluateHatch(true)


    // Now we erase the loop


    cir.Erase()

    id


  // Function to create our grid of circles


  let createGrid size rad offset =

    let ids = new ObjectIdCollection()

    for i = 0 to size - 1 do

      for j = 0 to size - 1 do

        let pt =

          new Point3d

            (offset * (Int32.to_float i),

            offset * (Int32.to_float j),

            0.0)

        let id = createCircle pt rad

        ids.Add(id) |> ignore

    ids


  // Function to change the colour of an entity


  let changeColour col (id : ObjectId) =

    if id.IsValid then

      let ent =

        tr.GetObject(id, OpenMode.ForWrite) :?> Entity

      ent.ColorIndex <- col


  // Shortcuts to make objects red and yellow


  let makeRed = changeColour 1

  let makeYellow = changeColour 2


  // Function to retrieve the contents of our

  // array of object IDs - this just calculates

  // the index based on the x & y values


  let getIndex fn size i =

    let res = fn size i

    if res >= 0 then

        (i * size) + res

    else

        -1


  // Apply our function synchronously for each value of x


  let applySyncBelowMax size fn =

    [| for i in [0..size-1] ->

       getIndex fn size i |]


  // Apply our function asynchronously for each value of x


  let applyAsyncBelowMax size fn =

    Async.Run

      (Async.Parallel

        [ for i in [0..size-1] ->

          async { return getIndex fn size i } ])


  // Hardcode the size of the grid and create it


  let size = 50

  let ids = createGrid size 0.5 1.2


  // Make the circles all red to start with


  Seq.iter makeRed (Seq.cast ids)


  // From a certain index in the list, get an object ID


  let getId i =

    if i >= 0 then

      ids.[i]

    else

      ObjectId.Null


  // Apply one of our trig functions, synchronously or

  // otherwise, to our grid


  applySyncBelowMax size normSin |>

    Array.map getId |>

      Array.iter makeYellow


  // Commit the transaction


  tr.Commit()


  // Check how long it took


  let elapsed =

      System.DateTime.op_Subtraction

        (System.DateTime.Now, starttime)


  ed.WriteMessage

    ("\nElapsed time: " +

    elapsed.ToString())

 

Here's what you see on AutoCAD's drawing canvas when you run the GRAPH command as it stands:

Sine from F#

If you want to play around with other functions, you can edit the call to applySyncBelowMax to pass normCos or normTan instead of normSin.

Cosine from F#

Tangent from F#

 

As I mentioned earlier, if you swap the call to be applyAsyncBelowMax instead of applySyncBelowMax you will actually run the mathematics piece as asynchronous tasks. These are CPU-bound operations - they don't call across the network or write to a hard-drive, which might have increased the benefit of calling them asynchronously - so right now the async version actually runs more slowly than the sync version. If I were to have more processing cores available to me, it might also give us more benefit, but right now with my dual-core machine there's more effort spent coordinating the tasks than you gain from the parallelism. But I'll let you play around with that yourselves... you may get better results. One other note on that piece of the code: at some point I'd like to make use of the Parallel Extensions for .NET (in particular the Task Parallel Library (TPL)), but for now I've continued with what I know, the asynchronous worklows capability which is now standard in F#.

I'm travelling in India this week (and working from our Bangalore office next week), so this is likely to be my last post of the week.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e2010536f53131970b

Listed below are links to weblogs that reference Implementing a simple graphing tool inside AutoCAD using F#:

blog comments powered by Disqus

Feed/Share

10 Random Posts