Last week we looked at a preliminary version of this application that made use of an EntityJig to display a Spirograph as we provided the values needed to define it. While that was a good start, I decided it would be better to show additional graphics during the jig process, to give a clearer idea of the meaning of the information being requested from the user. I wanted, for instance, to show temporary circles indicating the radii of the outer and inner circles, mainly to make it clearer how the various parameters affect the display of the resultant Spirograph pattern.
Anyway, my next step in this process was going to be a post showing how to implement IExtensionApplication from an F# application, but it turns out I’ve done that already <sigh>. But that’s good, as it leaves the coast clear for me to get into the rest of the implementation. I needed IExtensionApplication’s Initialize() callback to execute the demand-loading creation code provided in this recent post.
Here’s the source file I used for that:
// Declare a specific namespace and module name
namespace Spirograph
// Import managed assemblies
open Autodesk.AutoCAD.Runtime
open DemandLoading
type App() =
interface IExtensionApplication with
override x.Initialize() =
try
RegistryUpdate.RegisterForDemandLoading()
with _ -> ()
override x.Terminate() =
()
The application itself now works slightly differently, but using the same principles, overall. I’ve removed the old prompt-based SPI command, renaming the jig-based SPIG command to be SPI. The new jig has a little more to it – as we’re using a DrawJig to draw additional geometry – but it shouldn’t be any harder to understand than the last one. I’m also using a tip suggested by Fenton Webb to improve the performance of a complex jig: it’s good practice to check WorldDraw.RegenAbort during your WorldDraw, and if the flag is set you should exit immediately. Not doing so can make your jig appear sluggish. I’ve done this in a number of places, but I’ve also kept the segment count low when drawing our Spirograph pattern inside the jig, just to decrease the likelihood that we’ll have to cancel the draw operation.
Here’s the application’s main F# source file:
// Declare a specific namespace and module name
module Spirograph.Commands
// Import managed assemblies
open Autodesk.AutoCAD.ApplicationServices
open Autodesk.AutoCAD.Runtime
open Autodesk.AutoCAD.DatabaseServices
open Autodesk.AutoCAD.EditorInput
open Autodesk.AutoCAD.Geometry
open Autodesk.AutoCAD.GraphicsInterface
open System
open DemandLoading
// Return a sampling of points along a Spirograph's path
let pointsOnSpirograph cenX cenY inRad outRad a tStart tEnd num =
[|
for i in tStart .. tEnd * num do
let t = (float i) / (float num)
let diff = inRad - outRad
let ratio = inRad / outRad
let x =
diff * Math.Cos(ratio * t) +
a * Math.Cos((1.0 - ratio) * t)
let y =
diff * Math.Sin(ratio * t) -
a * Math.Sin((1.0 - ratio) * t)
yield new Point2d(cenX + x, cenY + y)
|]
// Different modes of acquisition for our jig
type AcquireMode =
| Inner
| Outer
| A
type SpiroJig() as this = class
inherit DrawJig()
// Our member variables
let mutable (_pl : Polyline) = null
let mutable _cen = Point3d.Origin
let mutable _norm = new Vector3d(0.0,0.0,1.0)
let mutable _inner = 0.0
let mutable _outer = 0.0
let mutable _a = 0.0
let mutable _mode = Outer
member x.StartJig(ed : Editor, pt, pl) =
// Set our center and start with the outer radius
_cen <- pt
_pl <- pl
_mode <- Outer
_norm <-
ed.CurrentUserCoordinateSystem.CoordinateSystem3d.Zaxis
let stat = ed.Drag(this)
if stat.Status <> PromptStatus.Cancel then
// Next we get the inner radius
_mode <- Inner
let stat = ed.Drag(this)
if stat.Status <> PromptStatus.Cancel then
// And finally the pen distance
_mode <- A
ed.Drag(this)
else
stat
else
stat
// Our Sampler function to acquire the various distances
override x.Sampler prompts =
// We're just acquiring distances
let jo = new JigPromptDistanceOptions()
jo.UseBasePoint <- true
jo.Cursor <- CursorType.RubberBand
// Local function to acquire a distance and return
// the appropriate status
let getDist (prompts : JigPrompts)
(opts : JigPromptDistanceOptions) 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)
// Then we have slightly different behavior depending
// on the info we're acquiring
match _mode with
// The outer radius...
| Outer ->
jo.BasePoint <- _cen
jo.Message <- "\nRadius of outer circle: "
let (stat, res) = getDist prompts jo _outer
if stat = SamplerStatus.OK then
_outer <- res
stat
// The inner radius...
| Inner ->
jo.BasePoint <-
_cen + new Vector3d(_outer, 0.0, 0.0)
jo.Message <- "\nRadius of smaller circle: "
let (stat, res) = getDist prompts jo _inner
if stat = SamplerStatus.OK then
_inner <- res
stat
// The pen distance...
| A ->
jo.BasePoint <-
_cen + new Vector3d(_outer - _inner, 0.0, 0.0)
jo.Message <-
"\nPen distance from center of smaller circle: "
let (stat, res) = getDist prompts jo _a
if stat = SamplerStatus.OK then
_a <- res
stat
// Our WorldDraw function to display the Spirograph and
// the related temporary graphics
override x.WorldDraw(draw : WorldDraw) =
// Save our current colour, to reset later
let col = draw.SubEntityTraits.Color
// Make our construction geometry red
draw.SubEntityTraits.Color <- (int16 1)
match _mode with
| Outer -> // Draw the outer circle
draw.Geometry.Circle(_cen, _outer, _norm)
|> ignore
| Inner -> // Draw the outer and inner circles
draw.Geometry.Circle(_cen, _outer, _norm)
|> ignore
draw.Geometry.Circle
(_cen + new Vector3d(_outer - _inner, 0.0, 0.0),
_inner, _norm)
|> ignore
| A -> // Draw the outer and inner circles
draw.Geometry.Circle(_cen, _outer, _norm)
|> ignore
draw.Geometry.Circle
(_cen + new Vector3d(_outer - _inner, 0.0, 0.0),
_inner, _norm)
|> ignore
// Check the RegenAbort flag...
// If it's set then we drop out of the function
if not draw.RegenAbort then
draw.SubEntityTraits.Color <- col
// If getting the outer radius fix the other
// parameters relative to it (as the inner radius
// comes later we only need to fix the pen distance
// against it)
if _mode = Outer then
let frac = _outer / 8.0
_inner <- frac
_a <- frac * 3.0
else if _mode = Inner then
_a <- _inner / 3.0
// Generate the polyline with low accuracy
// (fewer segments == quicker)
if not draw.RegenAbort then
// Generate our polyline
x.Generate(2)
if not draw.RegenAbort then
// And then draw it
draw.Geometry.Polyline(_pl, 0, _pl.NumberOfVertices-1)
|> ignore
true
// Generate a more accurate polyline
member x.Perfect() =
x.Generate(10)
member x.Generate(num) =
// Generate points based on the accuracy
let pts =
pointsOnSpirograph
_cen.X _cen.Y _inner _outer _a 0 300 num
// Remove all existing vertices but the first
// (we need at least one, it seems)
while _pl.NumberOfVertices > 1 do
_pl.RemoveVertexAt(0)
// Add the new vertices to our polyline
for i in 0 .. pts.Length-1 do
_pl.AddVertexAt(i, pts.[i], 0.0, 0.0, 0.0)
// Remove the first (original) vertex
if _pl.NumberOfVertices > 1 then
_pl.RemoveVertexAt(0)
end
// Our jig-based command
[<CommandMethod("ADNPLUGINS", "SPI", CommandFlags.Modal)>]
let spirojig() =
// 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 spirograph
let cenRes = ed.GetPoint("\nSelect center point: ")
if cenRes.Status = PromptStatus.OK then
let cen = cenRes.Value
// Create the polyline and run the jig
let pl = new Polyline()
let jig = new SpiroJig()
let res = jig.StartJig(ed, cen, pl)
if res.Status = PromptStatus.OK then
// Perfect the polyline created, smoothing it up
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 polyline to the modelspace
let id = ms.AppendEntity(pl)
tr.AddNewlyCreatedDBObject(pl, true)
tr.Commit()
[<CommandMethod("ADNPLUGINS", "REMOVESP", CommandFlags.Modal)>]
let removeSpirograph() =
try
RegistryUpdate.UnregisterForDemandLoading()
let doc =
Application.DocumentManager.MdiActiveDocument
doc.Editor.WriteMessage
("\nThe Spirograph plugin will not be loaded" +
" automatically in future editing sessions.")
with _ -> ()
When we run the SPI command, we see a red construction geometry being drawn along with our Spirograph pattern.
We start with the radius of the outer circle:
Followed by the inner circle, which we can make small:
Or large:
And we then define the distance of the pen from the inner circle’s centre, whether close to it:
Or further away:
You’ll probably have noticed that I’ve structured the application as a potential Plugin of the Month. I haven’t yet decided if it should become one – as it’s really just for fun – but I decided to structure it as such, just in case.