« January 2008 | Main | March 2008 »

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.

February 29, 2008 in AutoCAD, AutoCAD .NET, F# | Permalink | Comments (0) | TrackBack

Using Reflector to diagnose tail call optimization in F#

I've talked about Lutz Roeder's Reflector tool a couple of times and it's proven to be very useful to me, once again.

I mentioned in my last post about some problems I was having with tail recursion, and my choice to replace certain recursive functions with iterative versions. Today we're going to use Reflector to take a look under the hood of some compiled assemblies, to determine which recursive functions have been optimised correctly and which have not.

Let's start by taking a look at the two recursive functions I mentioned last time, starting with the most simple:

// 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

Here's what we see when we open the compiled assembly in Reflector and disassemble the equivalent function into C#:

[Serializable]

internal class drawPointList@140 : FastFunc<List<Point3d>, Unit>

{

  // Fields

  public Editor ed;


  // Methods

  public drawPointList@140(Editor ed)

  {

    this.ed = ed;

  }


  public override Unit Invoke(List<Point3d> x)

  {

    while (true)

    {

      List<Point3d> list = x;

      if (!(list is List<Point3d>._Cons))

      {

          break;

      }

      List<Point3d> list2 = ((List<Point3d>._Cons) list)._Cons2;

      Point3d from = ((List<Point3d>._Cons) list)._Cons1;

      this.ed.DrawVector(from, from, 1, true);

      x = list2;

    }

    return null;

  }

}



You can see that the implementation of Invoke contains a while loop, which loops forever (until the list is empty), rather than calling recursively into itself.

Now let's take a look at the other, more complex function, which generates a list of points via recursion:

// 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

This function, when compiled by the F# compiler integrated into Visual Studio and disassembled by Reflector, equates to this C# code:

[Serializable]

internal class getNPoints@101T<T> : OptimizedClosures.FastFunc2<int, Solid3d, List<T>>

{

  // Fields

  public TypeFunc _self0;


  // Methods

  public getNPoints@101T(TypeFunc _self0)

  {

    this._self0 = _self0;

  }


  public override List<T> Invoke(int n, Solid3d sol)

  {

    TypeFunc func = this._self0;

    int num = n;

    int t = 0;

    if ((num > t) ^ true)

    {

      return List<T>.Nil_uniq;

    }

    Solid3dMassProperties h = sol.MassProperties;

    Plane plane = new Plane();

    plane.Set(h.Centroid, MyApplication.randomVector3d<int>(sol.ObjectId.OldId));

    Region section = sol.GetSection(plane);

    Ray entityPointer = new Ray();

    entityPointer.BasePoint = h.Centroid;

    entityPointer.UnitDir = MyApplication.randomVectorOnPlane<Plane>(plane);

    Point3dCollection points = new Point3dCollection();

    section.IntersectWith(entityPointer, Intersect.OnBothOperands, points, 0, 0);

    plane.Dispose();

    section.Dispose();

    entityPointer.Dispose();

    List<T> list = Seq.untyped_to_list<Point3dCollection, T>(points);

    Solid3d u = sol;

    int num2 = n - points.Count;

    return Pervasives.op_Append<T>(FastFunc<int, Solid3d>.InvokeFast2<List<T>>((FastFunc<int, FastFunc<Solid3d, List<T>>>) func.Specialize<T>(), num2, u), list);

  }

}

While a little harder to follow (please don't worry about the details), we can see that the recursive tail call has not been optimised out, as we don't have an iterative construct (e.g. a while loop). Instead we see a call to InvokeFast2 (which will result in Invoke being called recursively), prior to the value being returned.

Why exactly this case hasn't been handled isn't clear to me - in fact I'm pretty sure it's a bug in F#, which I will go ahead and report - but it is clear that (at least for now) we need to change the implementation if we're to avoid a stack overflow. And that's fine: the last post contains an iterative version we can use.

Finallly we're going to take a look at parts of the assembly created by building the code in the recent Robotic Hatching posts. As I mentioned previously, I ended up hitting problems when creating 400 or so segments in the polyline hatch when using my F# implementation. Rather than automatically changing each of the recursive functions (those prefixed by "let rec") to iterative versions, I checked the disassembly listings one by one until I found the problematic one:

// A recursive function to get the points

// for our path


let

  if times <= 0 then

    []

  else

    try

      let pt =

        nextBoundaryPoint cur start doTrace

      (pt :: definePath pt (times-1))

    with exn ->

      if exn.Message = "Could not get point" then

        definePath start times

      else

        failwith exn.Message

rec definePath start times =

Here's how the compiled (and then disassembled/reassembled) function looks in C#:

[Serializable]

internal class definePath@281 : OptimizedClosures.FastFunc2<Point3d, int, List<Point3d>>

{

  // Fields

  public Curve cur;

  public bool doTrace;


  // Methods

  public definePath@281(bool doTrace, Curve cur)

  {

    this.doTrace = doTrace;

    this.cur = cur;

  }


  public override List<Point3d> Invoke(Point3d start, int times)

  {

    int num = times;

    int num2 = 0;

    if ((num > num2) ^ true)

    {

      return List<Point3d>.Nil_uniq;

    }

    try

    {

      Point3d pt = Commands.nextBoundaryPoint(this.cur, start, this.doTrace);

      return new List<Point3d>._Cons(pt, FastFunc<Point3d, int>.InvokeFast2<List<Point3d>>(this, pt, times - 1));

    }

    catch (object obj1)

    {

      Exception exn = (Exception) obj1;

      string message = exn.Message;

      string b = "Could not get point";

      if (string.Equals(message, b))

      {

        return FastFunc<Point3d, int>.InvokeFast2<List<Point3d>>(this, start, times);

      }

      return Operators.failwith<List<Point3d>>(exn.Message);

    }

  }

}

This one is actually only recurses when we catch an exception we expect to occur (and actually have thrown ourselves elsewhere in the code) occasionally during run-time. A better implementation - one that F#'s compiler is able to tail call optimise and meets my aesthetic requirements - is here:

// An iterative function to get the points

// for our path


let definePath start times =

  let pts = new Point3dCollection()

  pts.Add(start) |> ignore


  let mutable i = 0

  while i < times do

    try

      let pt =

        nextBoundaryPoint

          cur pts.[pts.Count-1] doTrace runAsync

      pts.Add(pt) |> ignore

      i <- i + 1

    with exn ->

      if exn.Message <> "Could not get point" then

        failwith exn.Message


  Seq.untyped_to_list pts

I won't bother showing this function's C# equivalent, as determined by Reflector, you should just trust me that it's better. :-)

I did change the implementation slightly, so we no longer add our first vertex when we create the polyline - we simply add it to the array of vertices, which is cleaner - and we also have to pass a start index of 0 into the definePath function, rather than 1. Very minor changes, but they actually do make the code slightly simpler.

Anyway, this kind of analysis and control is made possible by the fact that F# compiles to IL, just like the other .NET languages. This allows us to take advantage of existing tools for code analysis & obfuscation, and ultimately give us better, more consistent control than we would get from a functional programming implementation outside of .NET.

February 27, 2008 in AutoCAD, AutoCAD .NET, F#, Visual Studio | Permalink | Comments (7) | TrackBack

Pointing at clouds: more random musings on AutoCAD and F#

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:

  1. Create a random 3D vector
  2. Generate a random plane intersecting the solid (by using the centroid of the solid as the origin and the generated vector as the normal)
  3. Create a region representing the section of the solid and the plane
  4. Create another random vector, this time on the plane
  5. Use this vector to fire a ray
  6. Get the intersection of the ray and the region
  7. 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.

F_point_clouds_2

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.

February 25, 2008 in AutoCAD, AutoCAD .NET, F# | Permalink | Comments (0) | TrackBack

Parallelizing robotic AutoCAD hatching with F# and .NET

In the last post we saw some code combining F# with C# to create a random "hatch" - a polyline path that bounces around within a boundary.

This post extends the code to make use of Asynchronous Workflows in F# to parallelize the testing of points along a segment. In the initial design of the application I decided to test 10 points along each segment, to see whether it remained entirely within our boundary: the idea being that this granularity makes it very likely the segment will fail the test, should it happen to leave the boundary at any point. Not 100% guaranteed, but a high probability event. What this code does is take the 10 tests and queue them up for concurrent processing (where the system is capable of it).

Asynchronous Workflows - as suggested by the name - were intended to fire off and manage asynchronous tasks (ones that access network resources, for instance). The segment testing activity is actually very local and processor-bound, so it's not really what the mechanism was intended for, but I thought it would be interesting to try. One interesting point: while testing this code I noticed that it actually ran slower on a single processor machine, which is actually quite logical: only one core is available for processing, so the amount of sequential processing is not reduced but the overhead of synchronizing the various tasks is added. So it was fairly inevitable it would take longer. In the post I first talked about Asynchronous Workflows I showed a sample that queried multiple RSS sites for data: even on a single processor machine this was significantly quicker, as parallelizing the network latency led to a clear gain.

Anyway, as it was slower on this machine I decided only to enable the parallel version of the code in cases where the computer's NUMBER_OF_PROCESSORS environment variable is greater than 1. I checked quickly on a colleague's dual-core machine, and sure enough this variable is set to 2 on his system. I haven't, however, tested the code on a dual- or multi-core system, but I'll be getting a new system in a matter of weeks, which will give me the chance to test it out.

Here's the F# code, with the line numbers highlighting the changes in red:

    1 // Use lightweight F# syntax

    2

    3 #light

    4

    5 // Declare a specific namespace and module name

    6

    7 module BounceHatchAsync.Commands

    8

    9 // Import managed assemblies

   10

   11 #I @"C:\Program Files\Autodesk\AutoCAD 2008"

   12 #I @".\PointInCurve\bin\Debug"

   13

   14 #r "acdbmgd.dll"

   15 #r "acmgd.dll"

   16

   17 #R "PointInCurve.dll" // R = CopyFile is true

   18

   19 open System

   20 open Autodesk.AutoCAD.Runtime

   21 open Autodesk.AutoCAD.ApplicationServices

   22 open Autodesk.AutoCAD.DatabaseServices

   23 open Autodesk.AutoCAD.EditorInput

   24 open Autodesk.AutoCAD.Geometry

   25 open PointInCurve

   26

   27 // Get a random vector on a plane

   28

   29 let randomUnitVector pl =

   30

   31   // Create our random number generator

   32   let ran = new System.Random()

   33

   34   // First we get the absolute value

   35   // of our x and y coordinates

   36

   37   let absx = ran.NextDouble()

   38   let absy = ran.NextDouble()

   39

   40   // Then we negate them, half of the time

   41

   42   let x = if (ran.NextDouble() < 0.5) then -absx else absx

   43   let y = if (ran.NextDouble() < 0.5) then -absy else absy

   44

   45   // Create a 2D vector and return it as

   46   //  3D on our plane

   47

   48   let v2 = new Vector2d(x, y)

   49   new Vector3d(pl, v2)

   50

   51 type traceType =

   52   | Accepted

   53   | Rejected

   54   | Superseded

   55

   56 // Draw one of three types of trace vector

   57

   58 let traceSegment (start:Point3d) (endpt:Point3d) trace =

   59

   60   let ed =

   61     Application.DocumentManager.MdiActiveDocument.Editor

   62

   63   let vecCol =

   64     match trace with

   65     | Accepted -> 3

   66     | Rejected -> 1

   67     | Superseded -> 2

   68   let trans =

   69     ed.CurrentUserCoordinateSystem.Inverse()

   70   ed.DrawVector

   71     (start.TransformBy(trans),

   72     endpt.TransformBy(trans),

   73     vecCol,

   74     false)

   75

   76 // Test a segment to make sure it is within our boundary

   77

   78 let testSegment cur (start:Point3d) (vec:Vector3d) =

   79

   80   // (This is inefficient, but it's not a problem for

   81   //  this application. Some of the redundant overhead

   82   //  of firing rays for each iteration could be factored

   83   //  out, among other enhancements, I expect.)

   84

   85   let pts =

   86     [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]

   87

   88   // Call into our IsInsideCurve library function,

   89   // "and"-ing the results

   90

   91   let inside pt =

   92     PointInCurve.Fns.IsInsideCurve(cur, pt)

   93

   94   List.for_all inside pts

   95

   96 // Test a segment to make sure it is within our boundary

   97

   98 let testSegmentAsync cur (start:Point3d) (vec:Vector3d) =

   99

  100   let pts =

  101     [for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]

  102

  103   // Call into our IsInsideCurve library function,

  104   // "and"-ing the results

  105

  106   let inside pt =

  107     async { return PointInCurve.Fns.IsInsideCurve(cur, pt) }

  108

  109   let tests =

  110     Async.Run

  111       (Async.Parallel

  112         [for pt in pts -> inside pt])

  113

  114   Array.for_all (fun x -> x) tests

  115

  116 // For a particular boundary, get the next vertex on the

  117 // curve, found by firing a ray in a random direction

  118

  119 let nextBoundaryPoint (cur:Curve)

  120     (start:Point3d) trace runAsync =

  121

  122   // Get the intersection points until we

  123   // have at least 2 returned

  124   // (will usually happen straightaway)

  125

  126   let rec getIntersect (cur:Curve)

  127     (start:Point3d) vec =

  128

  129     let plane = cur.GetPlane()

  130

  131     // Create and define our ray

  132

  133     let ray = new Ray()

  134     ray.BasePoint <- start

  135     ray.UnitDir <- vec

  136

  137     let pts = new Point3dCollection()

  138     cur.IntersectWith

  139       (ray,

  140       Intersect.OnBothOperands,

  141       pts,

  142       0, 0)

  143

  144     ray.Dispose()

  145

  146     if (pts.Count < 2) then

  147       let vec2 = randomUnitVector plane

  148       getIntersect cur start vec2

  149     else

  150       pts

  151

  152   // For each of the intersection points - which

  153   // are points elsewhere on the boundary - let's

  154   // check to make sure we don't have to leave the

  155   // area to reach them

  156

  157   let plane =

  158     cur.GetPlane()

  159   let pts =

  160     randomUnitVector plane |> getIntersect cur start

  161

  162   // Get the distance between two points

  163

  164   let getDist fst snd =

  165     let (vec:Vector3d) = fst - snd

  166     vec.Length

  167

  168   // Compare two (dist, pt) tuples to allow sorting

  169   // based on the distance parameter

  170

  171   let compDist fst snd =

  172     let (dist1, pt1) = fst

  173     let (dist2, pt2) = snd

  174     if dist1 = dist2 then

  175       0

  176     else if dist1 < dist2 then

  177       -1

  178     else // dist1 > dist2

  179       1

  180

  181   // From the list of points we create a list

  182   // of (dist, pt) pairs, which we then sort

  183

  184   let sorted =

  185     [ for pt in pts -> (getDist start pt, pt) ] |>

  186       List.sort compDist

  187

  188   // A test function to check whether a segment

  189   // is within our boundary. It draws the appropriate

  190   // trace vectors, depending on success

  191

  192   let testItem dist =

  193     let (distval, pt) = dist

  194     let vec = pt - start

  195     if (distval > Tolerance.Global.EqualVector) then

  196       let res =

  197         if runAsync then

  198           testSegmentAsync cur start vec

  199         else

  200           testSegment cur start vec

  201       if res then

  202         if trace then

  203           traceSegment start pt traceType.Accepted

  204         Some(dist)

  205       else

  206         if trace then

  207           traceSegment start pt traceType.Rejected

  208         None

  209     else

  210       None

  211

  212   // Get the first item - which means the shortest

  213   // non-zero segment, as the list is sorted on distance

  214   // - that satisifies our condition of being inside

  215   // the boundary

  216

  217   let ret = List.first testItem sorted

  218   match ret with

  219   | Some(d,p) -> p

  220   | None -> failwith "Could not get point"

  221

  222 // We're using a different command name, so we can compare

  223

  224 [<CommandMethod("fba")>]

  225 let bounceHatch() =

  226   let doc =

  227     Application.DocumentManager.MdiActiveDocument

  228   let db = doc.Database

  229   let ed = doc.Editor

  230

  231   // Get various bits of user input

  232

  233   let getInput =

  234     let peo =

  235       new PromptEntityOptions

  236         ("\nSelect point on closed loop: ")

  237     let per = ed.GetEntity(peo)

  238     if per.Status <> PromptStatus.OK then

  239       None

  240     else

  241       let pio =

  242         new PromptIntegerOptions

  243           ("\nEnter number of segments: ")

  244       pio.DefaultValue <- 500

  245       let pir = ed.GetInteger(pio)

  246       if pir.Status <> PromptStatus.OK then

  247         None

  248       else

  249         let pko =

  250           new PromptKeywordOptions

  251             ("\nDisplay segment trace: ")

  252         pko.Keywords.Add("Yes")