Or otherwise named “Creating an AutoCAD jig to dynamically display a guilloché pattern using F#”. But then why pass up the chance for a Jerry Maguire reference? :-)

I understand the difficulty in understanding the nature of the geometry being created in the previous version… the fact that I’d named the original variables R, r, p, Q, m and n (as per the equation in the post that inspired this app) probably didn’t help with the understandability of the app, all things considered. It was certainly easier to name the various parameters in that way rather than work out more human-friendly labels.

Which has given rise to this version of the app, with an additional GUIJIG command to complement GUILLOCHE. We’re also now storing the previous selections – whether via the jig- or the prompt-based version – so there’s an accompanying GUIDEF command to reset these default values should they become confused or confusing (and that’s quite easy to do with this app).

So here’s the updated F# code. Bear in mind this is still very much in the experimental stage, so expect quirks. If you have some ideas on how to improve it (such as an alternative name for the “Wiggle” parameter :-) then please let me know.

module Guillocher.Commands

open Autodesk.AutoCAD.ApplicationServices.Core

open Autodesk.AutoCAD.Runtime

open Autodesk.AutoCAD.DatabaseServices

open Autodesk.AutoCAD.EditorInput

open Autodesk.AutoCAD.Geometry

open System

// Save our various default values in mutable state

let mutable _R = 50.0

let mutable _r = -0.2

let mutable _p = 25.0

let mutable _Q = 3.0

let mutable _m = 1.0

let mutable _n = 6.0

let mutable _segs = 300

let mutable _perfSegs = 1000

// User prompting helper functions

let getIntegerWithDefault (ed : Editor) msg min max def =

let pio = new PromptIntegerOptions(msg)

pio.LowerLimit <- min

pio.UpperLimit <- max

pio.DefaultValue <- def

pio.UseDefaultValue <- true

let pir = ed.GetInteger(pio)

if pir.Status = PromptStatus.OK then

Some(pir.Value)

else

None

let getDoubleWithDefault (ed : Editor) msg neg zero def =

let pdo = new PromptDoubleOptions(msg)

pdo.AllowNegative <- neg

pdo.AllowZero <- zero

pdo.DefaultValue <- def

pdo.UseDefaultValue <- true

let pdr = ed.GetDouble(pdo)

if pdr.Status = PromptStatus.OK then

Some(pdr.Value)

else

None

// Get the various values we need from the user for this command

let getGuillocheInput ed =

let R = getDoubleWithDefault ed "\nR" true false _R

if R = None then

None

else

let r = getDoubleWithDefault ed "\nr" true false _r

if r = None then

None

else

let p = getDoubleWithDefault ed "\np" true false _p

if p = None then

None

else

let Q = getDoubleWithDefault ed "\nQ" true true _Q

if Q = None then

None

else

let m = getDoubleWithDefault ed "\nm" true false _m

if m = None then

None

else

let n = getDoubleWithDefault ed "\nn" true false _n

if n = None then

None

else

let segs =

getIntegerWithDefault

ed "\nNumber of control points" 500 32767 _segs

if segs = None then

None

else

let ppr = ed.GetPoint("\nSelect center point")

if ppr.Status = PromptStatus.OK then

// Set the selected values as our new defaults

// (stored in mutable global state)

_R <- R.Value

_r <- r.Value

_p <- p.Value

_Q <- Q.Value

_m <- m.Value

_n <- n.Value

_segs <- segs.Value

// Return these to the calling function for it to

// create the guilloche

Some(R, r, p, Q, m, n, segs, ppr.Value)

else

None

let pointsOnGuilloche (cen : Point3d) R r p Q m n segs =

[|

let period = Math.PI * 2.0;

for theta in 0.0..period/(float segs)..period do

let rr = R + r

let rp = r + p

let rror = rr / r

let mth = m * theta

let nth = n * theta

let k = rror * mth

let x =

rr * Math.Cos(mth) + rp * Math.Cos(k) + Q * Math.Cos(nth)

let y =

rr * Math.Sin(mth) - rp * Math.Sin(k) + Q * Math.Sin(nth)

yield cen + new Vector3d(x, y, 0.0)

|]

// Different modes of acquisition for our jig

type AcquireMode =

| RADIUS

| MAJOR

| MINOR

| MULTIPLIER

| WIGGLE

type GuillocheJig() as this = class

inherit DrawJig()

// Our mutable member state

let mutable (_sp : Spline) = new Spline()

let mutable _cen = Point3d.Origin

let mutable _norm = Vector3d.ZAxis

let mutable _locp = _p

let mutable _locR = _R

let mutable _locr = _r

let mutable _locQ = _Q

let mutable _locm = _m

let mutable _mode = RADIUS

// Calculate some ratios, so that we keep proportions

// as we jig the initial values

let _rOverP = _r / _p

let _ROverP = _R / _p

let _rOverR = _r / _R

member x.StartJig(ed : Editor, pt) =

// Set our center and start with the radius

_cen <- pt

_mode <- RADIUS

_norm <- ed.CurrentUserCoordinateSystem.CoordinateSystem3d.Zaxis

let stat = ed.Drag(this)

if stat.Status = PromptStatus.OK then

// Next we get the major ripple

_mode <- MAJOR

let stat = ed.Drag(this)

if stat.Status = PromptStatus.OK then

// Next the minor ripple

_mode <- MINOR

let stat = ed.Drag(this)

if stat.Status = PromptStatus.OK then

// Next the angle multiplier

_mode <- MULTIPLIER

let stat = ed.Drag(this)

if stat.Status = PromptStatus.OK then

// Next the wiggle

_mode <- WIGGLE

ed.Drag(this)

else

stat

else

stat

else

stat

else

stat

// Helper function to acquire a distance and return

// the appropriate status

member private x.GetDist (prompts : JigPrompts)

(opts : JigPromptDistanceOptions) oldVal =

opts.DefaultValue <- oldVal

let res = prompts.AcquireDistance(opts)

if res.Status <> PromptStatus.OK then

(SamplerStatus.Cancel, 0.0)

else

if oldVal = res.Value then

(SamplerStatus.NoChange, 0.0)

else

(SamplerStatus.OK, res.Value)

// Our Sampler function to acquire the various distances

override x.Sampler prompts =

// We're just acquiring distances

let jo = new JigPromptDistanceOptions()

jo.BasePoint <- _cen

jo.Cursor <- CursorType.RubberBand

jo.UseBasePoint <- true

jo.UserInputControls <-

UserInputControls.NoZeroResponseAccepted

// Then we have slightly different behavior depending

// on the info we're acquiring

match _mode with

// p...

| RADIUS ->

jo.Message <- "\nRadius"

let (stat, res) = x.GetDist prompts jo _locp

if stat = SamplerStatus.OK then

_locp <- res

stat

// R...

| MAJOR ->

jo.Message <- "\nMajor ripple"

let (stat, res) = x.GetDist prompts jo _locR

if stat = SamplerStatus.OK then

_locR <- res

stat

// r...

| MINOR ->

jo.Message <- "\nMinor ripple"

let (stat, res) = x.GetDist prompts jo _locr

if stat = SamplerStatus.OK then

_locr <- res

stat

// m...

| MULTIPLIER ->

jo.Message <- "\nAngle multiplier"

let (stat, res) = x.GetDist prompts jo _locm

if stat = SamplerStatus.OK then

_locm <- res

stat

// Q...

| WIGGLE ->

jo.Message <- "\nWiggle"

let (stat, res) = x.GetDist prompts jo _locQ

if stat = SamplerStatus.OK then

_locQ <- res

stat

// Our WorldDraw function to display the guilloche and

// the related temporary graphics

override x.WorldDraw

(draw : Autodesk.AutoCAD.GraphicsInterface.WorldDraw) =

// We'll actually only draw a green circle for our radius

if _mode = RADIUS then

let col = draw.SubEntityTraits.Color

draw.SubEntityTraits.Color <- (int16 3)

draw.Geometry.Circle(_cen, _locp, _norm) |> ignore

draw.SubEntityTraits.Color <- col

// Check the RegenAbort flag...

// If it's set then we drop out of the function

if not draw.RegenAbort then

// Generate the spline with low accuracy

// (fewer control points == quicker)

match _mode with

| RADIUS ->

// Make sure we don't have a p of 0

if _locp = 0.0 then _locp <- 0.001

x.Generate

(_locp * _ROverP, _locp * _rOverP, _locp,

_locQ, _locm, _n, _segs)

| MAJOR ->

x.Generate

(_locR, _locR * _rOverR, _locp, _locQ, _locm, _n, _segs)

| _ ->

x.Generate(_locR, _locr, _locp, _locQ, _locm, _n, _segs)

if not draw.RegenAbort then

draw.Geometry.Draw(_sp) |> ignore

true

// Set the global defaults based on the last set of successful

// input values

member x.SetDefaults() =

_R <- _locR

_p <- _locp

_r <- _locr

_Q <- _locQ

_m <- _locm

// Generate a more accurate spline

member x.Perfect() =

x.Generate(_R, _r, _p, _Q, _m, _n, _perfSegs)

// Generate a spline

member x.Generate(R, r, p, Q, m, n, num) =

// Generate control points based on the accuracy

let pts = pointsOnGuilloche _cen R r p Q m n num

if _sp <> null then

_sp.Dispose()

_sp <- new Spline(new Point3dCollection(pts), 1, 0.)

// Accessor for the entity

member x.GetEntity() = _sp

// Let the caller clean-up when cancelling

member x.CleanUp() = _sp.Dispose()

end

// Our jig-based command

[<CommandMethod("GUIJIG")>]

let guillochejig() =

// Let's get the usual helpful AutoCAD objects

let doc = Application.DocumentManager.MdiActiveDocument

let ed = doc.Editor

let db = doc.Database

// Prompt the user for the center of the spiro

let cenRes = ed.GetPoint("\nSelect center point: ")

if cenRes.Status = PromptStatus.OK then

let cen = cenRes.Value

// Create the spline and run the jig

let jig = new GuillocheJig()

let res = jig.StartJig(ed, cen)

if res.Status = PromptStatus.OK then

// Perfect the spline created, smoothing it up

jig.SetDefaults()

jig.Perfect()

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.ForWrite)

:?> BlockTableRecord

// Add our spline to the modelspace

let sp = jig.GetEntity()

let id = ms.AppendEntity(sp)

tr.AddNewlyCreatedDBObject(sp, true)

tr.Commit()

else

jig.CleanUp()

// Set the values back to the program defaults

[<CommandMethod("GUIDEFS")>]

let resetGuillocheDefaults() =

_R <- 50.0

_r <- -0.2

_p <- 25.0

_Q <- 3.0

_m <- 1.0

_n <- 6.0

[<CommandMethod("GUILLOCHE")>]

let guilloche() =

// Let's get the usual helpful AutoCAD objects

let doc = Application.DocumentManager.MdiActiveDocument

let ed = doc.Editor

let db = doc.Database

// First we need some user input

match getGuillocheInput ed with

| None -> ()

| Some(R, r, p, Q, m, n, segs, cen) ->

// Next we get a sampling of points along the Guilloche geometry

let pts =

pointsOnGuilloche

Point3d.Origin

R.Value r.Value p.Value Q.Value m.Value n.Value segs.Value

// Use the points as control points on a spline

let sp = new Spline(new Point3dCollection(pts), 1, 0.)

// Move the geometry to the selected point

sp.TransformBy(Matrix3d.Displacement(cen.GetAsVector()))

// Use a transaction to add our spline to the model-space

use tr = db.TransactionManager.StartTransaction()

// Get appropriately-typed BlockTableRecord

let btr =

tr.GetObject(db.CurrentSpaceId, OpenMode.ForWrite)

:?> BlockTableRecord

// Add our curve to the model-space

let id = btr.AppendEntity(sp)

tr.AddNewlyCreatedDBObject(sp, true)

// Commit the transaction

tr.Commit()

Here’s an example guilloché pattern being created via GUIJIG. You’re going to have a tough time creating a specific pattern that you’re aiming for, but it’s worth persevering – every so often you strike gold. :-)

Doug also mentioned filling a certain area with a pattern. In this case, I just drew a rectangular polyline using RECTANG and then used TRIM to remove the parts of the spline outside of it. It took a few crossing window selections, but it got there eventually.

## Recent Comments

## Archives

More...