To start off my series of more in-depth looks at the new APIs provided in AutoCAD 2009, I decided to extend some recently posted F# code to generate and draw transient point clouds to be slightly less transient: we'll see how to use the new transient graphics API in AutoCAD to display a cache of transient graphics, even after the view has been changed.
Some of you may be wondering about the amount of code I'm posting in F#. I find the technology extremely interesting and am also increasingly productive with it, so I've found myself gravitating towards using it more for my blog samples. I understand it's not for everyone, and I will definitely continue to post C# on a regular basis, but as I'm currently spending quite a lot of time on F# I'm somewhat selfishly posting what I'm doing, rather than duplicating effort.
A few people have asked me by email "so should I be learning F# now, rather than C#?". I generally recommend to people to carry on learning how to program in C# (or VB.NET, for that matter, although I personally prefer the syntax in C#), as this skill is currently more relevant in the industry than F# programming. I really like F#, but for me it's another (for now, secondary) tool for solving certain classes of problem. I will say, however, that learning functional programming makes you a better programmer overall, and FP techniques are making their way into more mainstream languages, such as VB.NET and C#. For instance, today we're going to be using a lambda expression to register an event-handler: anonymous functions or lambda expressions are now part of C#.
Why does FP make you a better programmer? Because it leads you away from relying on shared state and side-effects. I won't get into the details of that now, but reducing your reliance on shared state is a good thing for your code: at some point in the future it will more easily harness parallel processing capabilities such as multicore chips. So even if you don't use F# on a daily basis, the way that you look at problems after you've understood its fundamental approach could - one day - have significant implications on your code's performance.
I digress, although this has reminded me that I've been meaning to post a comparative piece on programming technologies, sometime soon.
Here's the F# code from this previous post, modified to make use of the transient graphics API in AutoCAD 2009 to draw its points (rather than using Editor.DrawVector() with a zero-length vector, which is how we did it last time).
// Use lightweight F# syntax
#light
// Declare a specific namespace and module name
module MyNamespaceRecursive.MyApplication
// Import managed assemblies
#I @"C:\Program Files\Autodesk\AutoCAD 2009"
#r "acdbmgd.dll"
#r "acmgd.dll"
open Autodesk.AutoCAD.Runtime
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.Geometry
open Autodesk.AutoCAD.GraphicsInterface
// Get a random vector on a plane
let randomVectorOnPlane pl =
// Create our random number generator
let ran = new System.Random()
// First we get the absolute value
// of our x, y and z coordinates
let absx = ran.NextDouble()
let absy = ran.NextDouble()
// Then we negate them, half of the time
let x = if (ran.NextDouble() < 0.5) then -absx else absx
let y = if (ran.NextDouble() < 0.5) then -absy else absy
let v2 = new Vector2d(x,y)
new Vector3d(pl,v2)
// Get a random vector in 3D space
let randomVector3d() =
// Create our random number generator
let ran = new System.Random()
// First we get the absolute value
// of our x, y and z coordinates
let absx = ran.NextDouble()
let absy = ran.NextDouble()
let absz = ran.NextDouble()
// Then we negate them, half of the time
let x = if (ran.NextDouble() < 0.5) then -absx else absx
let y = if (ran.NextDouble() < 0.5) then -absy else absy
let z = if (ran.NextDouble() < 0.5) then -absz else absz
new Vector3d(x, y, z)
// Create some state to store information about
// the current view. We use this to determine
// when we need to update our transient
// graphics.
let mutable vd = new Vector3d(0.0,0.0,0.0)
let mutable vt = 0.0
let mutable vh = 0.0
// Check the view against our stored info:
// if anything has changed, update the
// cache and return true.
let viewChanged (vtr : ViewTableRecord) =
if (vd <> vtr.ViewDirection ||
vt <> vtr.ViewTwist ||
vh <> vtr.Height) then
vd <- vtr.ViewDirection
vt <- vtr.ViewTwist
vh <- vtr.Height
true
else
false
// Here's where we'll store our list of DBPoint objects
// to be redrawn
let mutable savedpts = []
// Now we declare our command
[<CommandMethod("pts")>]
let createPoints () =
// 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.ForRead)
:?> BlockTableRecord
// A function that accepts an ObjectId and returns
// a list of random points on its surface
let rec getNPoints n (sol:Solid3d) ptlist =
if n <= 0 then
ptlist
else
let mp = sol.MassProperties
let pl = new Plane()
pl.Set(mp.Centroid,randomVector3d())
let reg = sol.GetSection(pl)
let ray = new Ray()
ray.BasePoint <- mp.Centroid
ray.UnitDir <- randomVectorOnPlane pl
let pts = new Point3dCollection()
reg.IntersectWith
(ray,
Intersect.OnBothOperands,
pts,
0, 0)
pl.Dispose()
reg.Dispose()
ray.Dispose()
getNPoints
(n - pts.Count) sol
(ptlist @ Seq.untyped_to_list pts)
let generatePoints numPoints (x : ObjectId) =
let obj = tr.GetObject(x,OpenMode.ForRead)
match obj with
| :? Solid3d ->
let sol = (obj :?> Solid3d)
getNPoints numPoints sol []
| _ -> []
// Create a DBPoint from a Point3d
let to_db_point pt =
let dbp = new DBPoint(pt)
dbp.ColorIndex <- 1
dbp
// Add a single point (or any "drawable" object, for that
// matter) to the transient graphics manager.
let drawTransient x =
let tm = TransientManager.CurrentTransientManager
let ic = new IntegerCollection()
tm.AddTransient
(x, TransientDrawingMode.DirectShortTerm, 0, ic)
|> ignore
// We'll generate 100K points per solid
// (the below line simply defined a new function
// by currying (fixing one argument for) another
// function)
let points = generatePoints 100000
// Save the points we generate in our mutable state
savedpts <-
Seq.untyped_to_list ms |> // ObjectIds from modelspace
List.map points |> // Get points for each object
List.concat |> // No need for the outer list
List.map to_db_point // Get DBPoints
// And then add each point to the transient graphics system
List.iter drawTransient savedpts
// As usual, committing is cheaper than aborting
tr.Commit()
// Add an event handler to respond to the doc-lock changed
// event. This happens after every doc-centric command
// (for instance), so we check whether the view has changed
// before starting a potentially time-consuming operation.
Application.DocumentManager.DocumentLockModeChanged.Add
(fun _ ->
if viewChanged (ed.GetCurrentView()) then
for pt in savedpts do
let tm = TransientManager.CurrentTransientManager
let ic = new IntegerCollection()
tm.UpdateTransient(pt, ic) |> ignore)
Some interesting points about this code:
- We now store a list (potentially a very big list) of points in memory, in the savedpts variable
- These are DBPoints, as they need to be "drawable" to be managed by the transient graphics subsystem
- The use of the new transient graphics API is in the drawTransient function, which does the equivalent of this C# call:
- Autodesk.AutoCAD.GraphicsInterface. TransientManager.CurrentTransientManager.AddTransient(pt, TransientDrawingMode.DirectShortTerm, 0, new IntegerCollection());
- This API can be used to draw any "drawable" object - it doesn't have to be one that would typically be stored in the DWG file. It can be used to display custom glyphs and tooltips, for instance
- Check out the ObjectARX (C++) sample under ObjectARX 2009\samples\graphics\AsdkTransientGraphicsSampFolder for more details
- We register an event handler to update the display of these points when the view has changed
- We do not currently have a viewChanged event exposed through the managed API, so we check for DocumentLockChanged and then see whether the view has changed there
- We store some state about the previous view, so we know when it has changed
- This event handler calls the equivalent of this C# code:
- Autodesk.AutoCAD.GraphicsInterface. TransientManager.CurrentTransientManager.UpdateTransient(pt, new IntegerCollection());
- See how easy it is to register an event handler in F#: the use of lambda expressions makes this really trivial (no need to define a function that we specify as a delegate). We also use the underscore ("_") to state we don't care about the arguments passed to the event handler, in this particular situation. Very neat.
- We do not currently have a viewChanged event exposed through the managed API, so we check for DocumentLockChanged and then see whether the view has changed there
Here's what happens when we run the PTS command on a set of 6 solids:
We can see the points disappear when we perform a 3DORBIT:
When we exit the orbit, our old point graphics are displayed...
Until the event handler kicks in and updates the display of our points: