At the beginning of the week, we looked at some iterative F# code to generate random point clouds inside AutoCAD. We then took the time to use Reflector to dig under the hood and understand why the previous recursive implementation was causing stack problems.
For completeness (and - I admit it - being driven slightly by laziness, as this is a quick post to crank out :-) here's the recursive version of the random point cloud generation code in F#:
// Use lightweight F# syntax
#light
// Declare a specific namespace and module name
module MyNamespaceRecursive.MyApplication
// Import managed assemblies
#I @"C:\Program Files\Autodesk\AutoCAD 2008"
#r "acdbmgd.dll"
#r "acmgd.dll"
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("ptsr")>]
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 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()
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 []
| _ -> []
// 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
// Let's generate 100K 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()
This code is much more elegant from a functional perspective, and F#'s tail call optimization means the resultant code runs just as efficiently as if we'd used iterative code with mutable state. To make the code optimizable, I had to adjust the arguments to the genNPoints function to accept an accumulator object (being the list of points generated thus far). Appending to this list, rather than the one being returned by the next recursion call, allows F# to optimize the recursion into a loop.
Thanks to Namin for his comments on the last post - they were very helpful to my understanding of the problem.
Next week I'm going to try to spend some time diving into the new APIs coming with AutoCAD 2009.