I had too much fun with the last post just to let it drop: I decided to port the main command to F#, to show how it's possible to combine C# and F# inside a single project.
The premise I started with was that the point-in-curve.cs library is something that we know works - and don't want to re-write - but would like to use from a new application we're developing in F#. This also gives us the chance to compare the performance between C# and F# when solving the same problem (although as we'll be calling through to some C# code from F# this isn't a pure comparison, in truth).
Anyway, on the train heading for Zurich, before flying back out to San Francisco again (yes, I'm back in San Rafael for another couple of days), I finished up the F# equivalent code for the last post's bounce-hatch.cs file.
Here's the F# code:
// Use lightweight F# syntax
#light
// Declare a specific namespace and module name
module BounceHatch.Commands
// Import managed assemblies
#I @"C:\Program Files\Autodesk\AutoCAD 2008"
#I @".\PointInCurve\bin\Debug"
#r "acdbmgd.dll"
#r "acmgd.dll"
#R "PointInCurve.dll" // R = CopyFile is true
open Autodesk.AutoCAD.Runtime
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.EditorInput
open Autodesk.AutoCAD.Geometry
open PointInCurve
// Get a random vector on a plane
let randomUnitVector pl =
// Create our random number generator
let ran = new System.Random()
// First we get the absolute value
// of our x and y 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
// Create a 2D vector and return it as
// 3D on our plane
let v2 = new Vector2d(x, y)
new Vector3d(pl, v2)
type traceType =
| Accepted
| Rejected
| Superseded
// Draw one of three types of trace vector
let traceSegment (start:Point3d) (endpt:Point3d) trace =
let ed =
Application.DocumentManager.MdiActiveDocument.Editor
let vecCol =
match trace with
| Accepted -> 3
| Rejected -> 1
| Superseded -> 2
let trans =
ed.CurrentUserCoordinateSystem.Inverse()
ed.DrawVector
(start.TransformBy(trans),
endpt.TransformBy(trans),
vecCol,
false)
// Test a segment to make sure it is within our boundary
let testSegment cur (start:Point3d) (vec:Vector3d) =
// (This is inefficient, but it's not a problem for
// this application. Some of the redundant overhead
// of firing rays for each iteration could be factored
// out, among other enhancements, I expect.)
let pts =
[for i in 1..10 -> start + (vec * 0.1 * Int32.to_float i)]
// Call into our IsInsideCurve library function,
// "and"-ing the results
let inside pt =
PointInCurve.Fns.IsInsideCurve(cur, pt)
List.for_all inside pts
// For a particular boundary, get the next vertex on the
// curve, found by firing a ray in a random direction
let nextBoundaryPoint (cur:Curve)
(start:Point3d) trace =
// Get the intersection points until we
// have at least 2 returned
// (will usually happen straightaway)
let rec getIntersect (cur:Curve)
(start:Point3d) vec =
let plane = cur.GetPlane()
// Create and define our ray
let ray = new Ray()
ray.BasePoint <- start
ray.UnitDir <- vec
let pts = new Point3dCollection()
cur.IntersectWith
(ray,
Intersect.OnBothOperands,
pts,
0, 0)
ray.Dispose()
if (pts.Count < 2) then
let vec2 = randomUnitVector plane
getIntersect cur start vec2
else
pts
// For each of the intersection points - which
// are points elsewhere on the boundary - let's
// check to make sure we don't have to leave the
// area to reach them
let plane =
cur.GetPlane()
let pts =
randomUnitVector plane |> getIntersect cur start
// Get the distance between two points
let getDist fst snd =
let (vec:Vector3d) = fst - snd
vec.Length
// Compare two (dist, pt) tuples to allow sorting
// based on the distance parameter
let compDist fst snd =
let (dist1, pt1) = fst
let (dist2, pt2) = snd
if dist1 = dist2 then
0
else if dist1 < dist2 then
-1
else // dist1 > dist2
1
// From the list of points we create a list
// of (dist, pt) pairs, which we then sort
let sorted =
[ for pt in pts -> (getDist start pt, pt) ] |>
List.sort compDist
// A test function to check whether a segment
// is within our boundary. It draws the appropriate
// trace vectors, depending on success
let testItem dist =
let (distval, pt) = dist
let vec = pt - start
if (distval > Tolerance.Global.EqualVector) then
if testSegment cur start vec then
if trace then
traceSegment start pt traceType.Accepted
Some(dist)
else
if trace then
traceSegment start pt traceType.Rejected
None
else
None
// Get the first item - which means the shortest
// non-zero segment, as the list is sorted on distance
// - that satisifies our condition of being inside
// the boundary
let ret = List.first testItem sorted
match ret with
| Some(d,p) -> p
| None -> failwith "Could not get point"
// We're using a different command name, so we can compare
[<CommandMethod("fb")>]
let bounceHatch() =
let doc =
Application.DocumentManager.MdiActiveDocument
let db = doc.Database
let ed = doc.Editor
// Get various bits of user input
let getInput =
let peo =
new PromptEntityOptions
("\nSelect point on closed loop: ")
let per = ed.GetEntity(peo)
if per.Status <> PromptStatus.OK then
None
else
let pio =
new PromptIntegerOptions
("\nEnter number of segments: ")
pio.DefaultValue <- 500
let pir = ed.GetInteger(pio)
if pir.Status <> PromptStatus.OK then
None
else
let pko =
new PromptKeywordOptions
("\nDisplay segment trace: ")
pko.Keywords.Add("Yes")
pko.Keywords.Add("No")
pko.Keywords.Default <- "Yes"
let pkr = ed.GetKeywords(pko)
if pkr.Status <> PromptStatus.OK then
None
else
Some
(per.ObjectId,
per.PickedPoint,
pir.Value,
pkr.StringResult.Contains("Yes"))
match getInput with
| None -> ignore()
| Some(oid, picked, numBounces, doTrace) ->
// Capture the start time for performance
// measurement
let starttime = System.DateTime.Now
use tr =
db.TransactionManager.StartTransaction()
// Check the selected object - make sure it's
// a closed loop (could do some more checks here)
let obj =
tr.GetObject(oid, OpenMode.ForRead)
match obj with
| :? Curve ->
let cur = obj :?> Curve
if cur.Closed then
let latest =
picked.
TransformBy(ed.CurrentUserCoordinateSystem).
OrthoProject(cur.GetPlane())
// Create our polyline path, adding the
// initial vertex
let path = new Polyline()
path.Normal <- cur.GetPlane().Normal
path.AddVertexAt
(0,
latest.Convert2d(cur.GetPlane()),
0.0, 0.0, 0.0)
// A recursive function to get the points
// for our path
let rec definePath start times =
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
// Another recursive function to add the vertices
// to the path
let rec addVertices (path:Polyline)
index (pts:Point3d list) =
match pts with
| [] -> []
| a::[] ->
path.AddVertexAt
(index,
a.Convert2d(cur.GetPlane()),
0.0, 0.0, 0.0)
[]
| a::b ->
path.AddVertexAt
(index,
a.Convert2d(cur.GetPlane()),
0.0, 0.0, 0.0)
addVertices path (index+1) b
// Plug our two functions together, ignoring
// the results
definePath picked numBounces |>
addVertices path 1 |>
ignore
// Now we'll add our polyline to the drawing
let bt =
tr.GetObject
(db.BlockTableId,
OpenMode.ForRead) :?> BlockTable
let btr =
tr.GetObject
(bt.[BlockTableRecord.ModelSpace],
OpenMode.ForWrite) :?> BlockTableRecord
// We need to transform the path polyline so
// that it's over our boundary
path.TransformBy
(Matrix3d.Displacement
(cur.StartPoint - Point3d.Origin))
// Add our path to the modelspace
btr.AppendEntity(path) |> ignore
tr.AddNewlyCreatedDBObject(path, true)
// Commit, whether we added a path or not.
tr.Commit()
// Print how much time has elapsed
let elapsed =
System.DateTime.op_Subtraction
(System.DateTime.Now, starttime)
ed.WriteMessage
("\nElapsed time: " + elapsed.ToString())
// If we're tracing, pause for user input
// before regenerating the graphics
if doTrace then
let pko =
new PromptKeywordOptions
("\nPress return to clear trace vectors: ")
pko.AllowNone <- true
pko.AllowArbitraryInput <- true
let pkr = ed.GetKeywords(pko)
ed.Regen()
| _ ->
ed.WriteMessage("\nObject is not a curve.")
I should make a few points about this
- The code is pretty rough - while I tried to solve the problem in a "functional" style, I was re-writing an "imperative" application, so my thinking was a little entrenched. That said, I was able to use some functional techniques to solve certain bits of the problem more elegantly, I believe.
- The performance is on a par (and at times slightly quicker) than the equivalent C# code
- That said, when trying to bounce 400 or more times (on my system, at least) I get a fatal error. I suspect some stack limit is being reached: when running from the debugger this is not hit, although the performance is much slower.
I need to do some more work on this at some point, but I though I'd post it now along with the complete project. I'm going to have a fairly hectic few days here, but will try to post something more at the end of the week.