Recursive F# code to generate random point clouds inside AutoCAD

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.

Leave a Reply

Your email address will not be published. Required fields are marked *