A blog for developers programming with Autodesk platforms, particularly AutoCAD and Forge. With a special focus on AR/VR and IoT.

March 10, 2008

Using AutoCAD 2009's new transient graphics API to show point clouds from F#

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.

letmutable vd = new Vector3d(0.0,0.0,0.0)

letmutable vt = 0.0

letmutable 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

letmutable 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

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

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.

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: