On my way back from the US last week, I started thinking more about uses for random numbers inside AutoCAD: especially ones that allow me to try out some possible application areas for F#.
There's something deliciously perverse about using random numbers in Engineering systems, where it's really important for outcomes to be deterministic (i.e. predictable) & precise. And that perversity appeals to me quite strongly, for some reason. Feel free to drop me a mail if you have an idea why that might be, any amateur psychologists out there... ;-)
So, I got to thinking... an interesting domain area for our development partners is that of laser scanning. These systems often generate huge (and I mean huge) data-sets: millions upon millions of points are generated by laser scanners (often known as point clouds), and these need to be managed in some way by software - often by solutions that are integrated with Autodesk products.
So I thought, wouldn't it be fun to go through an AutoCAD model and generated huge arrays of points representing the shell of solid objects in the model - essentially generating random point clouds - to see how F# deals with such humungous data-sets.
The general approach I decided to use was to go through all of the entities in the model-space, open them up and - for those that are of type Solid3d - generate 100,000 points (say) that are found on the shell of the object. To find points on the shell, I adopted the following technique:
- Create a random 3D vector
- Generate a random plane intersecting the solid (by using the centroid of the solid as the origin and the generated vector as the normal)
- Create a region representing the section of the solid and the plane
- Create another random vector, this time on the plane
- Use this vector to fire a ray
- Get the intersection of the ray and the region
- Repeat until we have enough points
To display each point we just draw a zero-length vector, which I decided to make red.
In my initial implementation, I used a classic functional programming technique, that of tail recursion. As a bit of a purist, I try to use functional (as opposed to imperative) techniques whenever I can when writing F# code. The problem is that tail recursion can significantly drain your stack space, as it results in a new function call - and a new level in the call stack - for every item in your list (and after all we're talking about huge numbers of points). Some compilers/evaluation systems can optimise out the recursive function call, reducing it to a simple branch, but F# doesn't currently appear to do so with my code (it is supposed to, so there may be something about the way my recursive functions are structured that prevents them from being identified as tail-recursive).
Here are a couple of examples of tail-recursive functions - one to generate a list of points, the other to draw them:
// A function that accepts an ObjectId and returns
// a list of random points on its surface
let rec getNPoints n (sol:Solid3d) =
if n <= 0 then
[]
else
let mp = sol.MassProperties
let pl = new Plane()
pl.Set(mp.Centroid,randomVector3d sol.ObjectId.OldId)
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()
let ptlist = Seq.untyped_to_list pts
ptlist @ getNPoints (n - pts.Count) sol
// A recursive function to show the contents of a list
let rec drawPointList (x:Point3d list) =
match x with
| h :: t ->
ed.DrawVector(h,h,1,true)
drawPointList t
| [] -> ()
While more elegant from a functional perspective, this elegance results in sub-optimal execution. The way to "fix" this is to introduce iterative code - whether a for, foreach or while loop - to avoid the recursion.
Here's the complete F# code that makes use of iterative techniques rather than recursion:
// Use lightweight F# syntax
#light
// Declare a specific namespace and module name
module MyNamespace.MyApplication
// Import managed assemblies
#I @"C:\Program Files\Autodesk\AutoCAD 2008"
#r "acdbmgd.dll"
#r "acmgd.dll"
open System
open Autodesk.AutoCAD.Runtime
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.Geometry
// 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
// Note: _ is only used to make sure this function gets
// executed when it is called... if we have no argument
// it's a value that doesn't require repeated execution
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)
// 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 getNPoints n (sol:Solid3d) =
let ptcol = new Point3dCollection()
while ptcol.Count < n do
let mp = sol.MassProperties
let pl = new Plane()
pl.Set
(mp.Centroid, randomVector3d n)
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()
for pt in pts do
ptcol.Add pt |> ignore
Seq.untyped_to_list ptcol
let generatePoints numPoints (x : ObjectId) =
let obj = tr.GetObject(x,OpenMode.ForRead)
match obj with
| :? Solid3d ->
let sol = (obj :?> Solid3d)
getNPoints numPoints sol
| _ -> []
// An iterative function to draw a point list
let drawPointList (x:Point3d list) =
for pt in x do
ed.DrawVector(pt,pt,1,true)
// Let's generate 10K points per solid
let points = generatePoints 100000
// Here's where we plug everything together...
Seq.untyped_to_list ms |> // ObjectIds from modelspace
List.map points |> // Get points for each object
List.concat |> // No need for the outer list
drawPointList // Draw the resultant points
// As usual, committing is cheaper than aborting
tr.Commit()
Here are the results of the PTS command called on a set of 6 Solid3d objects, essentially generating and drawing 600,000 points. The code doesn't currently result in any persistent graphics at all - if you change views the points all disappear. At some point I'd like to extend this to store the generated points persistently, but for now the point of the exercise (no pun intended :-) is more to see how F# performs with large data-sets, rather than how we deal with the issue of persistence.
A final note: I now realise the use of tail recursion was almost certainly the wall I hit with the F# implementation in this previous post.