« 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:
- 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.
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")

Atom
