Robotic hatching inside AutoCAD using F# and .NET

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.

Leave a Reply

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