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:

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

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.