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.
February 19, 2008 in AutoCAD, AutoCAD .NET, F#, Hatches | Permalink | Comments (0) | TrackBack
Robotic hatching inside AutoCAD using .NET
This may strike you as a fairly bizarre title for a post, but I was inspired to develop the below code by a robotic lawnmower we bought about a year ago. This fantastic tool bounces around our garden, changing direction randomly when it hits the lawn's boundary. I got to thinking how to implement a similar technique to hatch a boundary with a polyline. While this is mostly for fun, I can see a few interesting potential uses: you might use the technique to test randomly generated paths of, for example, a robot or you might simply want to supplement traditional hatching with something more random in nature.
The principle is actually very simple: we're going to take a point on the edge of the boundary, and fire a ray (an infinite line) in a random direction - but planar to the boundary - and find out where it intersects the boundary. The tricky piece is that - depending on how "jagged" your boundary is - the ray may actually intersect multiple (i.e. > 2) times. If we want to stay inside the boundary - a requirement for my little robotic hatcher - we need to exclude the segments that would lead us to exiting the boundary to reach the other end.
Excluding the "bad" segments actually proved to be the tricky part. I ended up taking a DevNote from the ADN site (Testing Whether A Point Lies Inside A Curve, for those of you who are ADN members), and converting the C++ code to C#. I won't comment much on the implementation - it's using a similar technique to the one I have in my own code, firing rays to determine intersections - but to be frank I'm only vaguely aware of how it works (ah, the joys of copy & paste development :-).
Here's the converted code. I have it saved in a file called point-in-curve.cs, but clearly the specific name doesn't matter, as long as it is included in your project.
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
namespace PointInCurve
{
public class Fns
{
enum IncidenceType
{
ToLeft = 0,
ToRight = 1,
ToFront = 2,
Unknown
};
static IncidenceType CurveIncidence(
Curve cur,
double param,
Vector3d dir,
Vector3d normal
)
{
Vector3d deriv1 =
cur.GetFirstDerivative(param);
if (deriv1.IsParallelTo(dir))
{
// Need second degree analysis
Vector3d deriv2 =
cur.GetSecondDerivative(param);
if (deriv2.IsZeroLength() ||
deriv2.IsParallelTo(dir))
return IncidenceType.ToFront;
else
if (deriv2.CrossProduct(dir).
DotProduct(normal) < 0)
return IncidenceType.ToRight;
else
return IncidenceType.ToLeft;
}
if (deriv1.CrossProduct(dir).
DotProduct(normal) < 0)
return IncidenceType.ToLeft;
else
return IncidenceType.ToRight;
}
static public bool IsInsideCurve(
Curve cur,
Point3d testPt
)
{
if (!cur.Closed)
// Cannot be inside
return false;
Polyline2d poly2d = cur as Polyline2d;
if (poly2d != null &&
poly2d.PolyType != Poly2dType.SimplePoly)
// Not supported
return false;
Point3d ptOnCurve =
cur.GetClosestPointTo(testPt, false);
if (Tolerance.Equals(testPt, ptOnCurve))
return true;
// Check it's planar
Plane plane = cur.GetPlane();
if (!cur.IsPlanar)
return false;
// Make the test ray from the plane
Vector3d normal = plane.Normal;
Vector3d testVector =
normal.GetPerpendicularVector();
Ray ray = new Ray();
ray.BasePoint = testPt;
ray.UnitDir = testVector;
Point3dCollection intersectionPoints =
new Point3dCollection();
// Fire the ray at the curve
cur.IntersectWith(
ray,
Intersect.OnBothOperands,
intersectionPoints,
0, 0
);
ray.Dispose();
int numberOfInters =
intersectionPoints.Count;
if (numberOfInters == 0)
// Must be outside
return false;
int nGlancingHits = 0;
double epsilon = 2e-6; // (trust me on this)
for (int i = 0; i < numberOfInters; i++)
{
// Get the first point, and get its parameter
Point3d hitPt = intersectionPoints[i];
double hitParam =
cur.GetParameterAtPoint(hitPt);
double inParam = hitParam - epsilon;
double outParam = hitParam + epsilon;
IncidenceType inIncidence =
CurveIncidence(cur, inParam, testVector, normal);
IncidenceType outIncidence =
CurveIncidence(cur, outParam, testVector, normal);
if ((inIncidence == IncidenceType.ToRight &&
outIncidence == IncidenceType.ToLeft) ||
(inIncidence == IncidenceType.ToLeft &&
outIncidence == IncidenceType.ToRight))
nGlancingHits++;
}
return ((numberOfInters + nGlancingHits) % 2 == 1);
}
}
}
I then implemented my own code to make use of this library (in this case saved in bounce-hatch.cs):
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using PointInCurve;
namespace BounceHatch
{
public class Commands
{
// Get a vector in a random direction
public Vector3d randomUnitVector(
PlanarEntity pl
)
{
// Create our random number generator
System.Random ran =
new System.Random();
// First we get the absolute value
// of our x and y coordinates
double x = ran.NextDouble();
double y = ran.NextDouble();
// Then we negate them, half of the time
if (ran.NextDouble() < 0.5)
x = -x;
if (ran.NextDouble() < 0.5)
y = -y;
// Create a 2D vector and return it as
// 3D on our plane
Vector2d v2 = new Vector2d(x, y);
return new Vector3d(pl, v2);
}
// Allow tracing in 3 colours, depending on
// whether the vector was accepted, rejected,
// or superseded by a better one
enum TraceType
{
Accepted = 0,
Rejected = 1,
Superseded = 2
};
void TraceSegment(
Point3d start,
Point3d end,
TraceType type
)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
int vecCol = 0;
switch (type)
{
case TraceType.Accepted:
vecCol = 3;
break;
case TraceType.Rejected:
vecCol = 1;
break;
case TraceType.Superseded:
vecCol = 2;
break;
}
Matrix3d trans =
ed.CurrentUserCoordinateSystem.Inverse();
ed.DrawVector(
start.TransformBy(trans),
end.TransformBy(trans),
vecCol,
false
);
}
// Test whether a segment goes outside our boundary
bool TestSegment(
Curve cur,
Point3d start,
Vector3d vec
)
{
// Test 10 points along the segment...
// (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.)
bool result = true;
for (int i = 1; i < 10; i++)
{
Point3d test = start + (vec * 0.1 * i);
// Call into our IsInsideCurve library function,
// "and"-ing the results
result &=
PointInCurve.Fns.IsInsideCurve(cur, test);
if (!result)
break;
}
return result;
}
// For a particular boundary, get the next vertex on the
// curve, found by firing a ray in a random direction
Point3d nextBoundaryPoint(
Curve cur,
Point3d start,
bool trace
)
{
// Create and define our ray
Ray ray = new Ray();
ray.BasePoint = start;
ray.UnitDir =
randomUnitVector(cur.GetPlane());
// Get the intersection points until we
// have at least 2 returned
// (will usually happen straightaway)
Point3dCollection pts =
new Point3dCollection();
do
{
cur.IntersectWith(
ray,
Intersect.OnBothOperands,
pts,
0, 0
);
if (pts.Count < 2)
{
ray.UnitDir =
randomUnitVector(cur.GetPlane());
}
}
while (pts.Count < 2);
ray.Dispose();
// 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
bool first = true;
double nextLen = 0.0;
Point3d nextPt = start;
foreach (Point3d pt in pts)
{
// Get the distance between this intersection
// and the last accepted point - both points
// are on our ray
Vector3d vec = pt - start;
double len = vec.Length;
// If the vector length is positive and either
// the first to be a candidate or closer than
// the previous one (we generally select the
// closest non-zero option) then check it out
// further
if (len > Tolerance.Global.EqualVector &&
(first || len < nextLen))
{
// Run our tests to make sure the segment is
// inside our boundary
if (TestSegment(cur, start, vec))
{
// Draw the previous segment before overwriting
if (trace)
TraceSegment(
start,
nextPt,
TraceType.Superseded
);
nextLen = len;
nextPt = pt;
first = false;
}
else
// This segment has been rejected,
// as it goes outside
if (trace)
TraceSegment(
start,
pt,
TraceType.Rejected
);
}
}
// Draw our accepted segment and return it
if (nextLen > Tolerance.Global.EqualVector)
{
if (trace)
TraceSegment(
start,
nextPt,
TraceType.Accepted
);
return nextPt;
}
else
// If we didn't find a good segment, throw an
// exception to be handled by the calling function
throw new Exception(
ErrorStatus.PointNotOnEntity,
"Could not find another intersection point."
);
}
[CommandMethod("BOUNCE")]
public void BounceHatch()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
bool doTrace = false;
// Get various bits of user input
PromptEntityOptions peo =
new PromptEntityOptions(
"\nSelect point on closed loop: "
);
PromptEntityResult per =
ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter number of segments: "
);
pio.DefaultValue = 500;
PromptIntegerResult pir =
ed.GetInteger(pio);
if (pir.Status != PromptStatus.OK)
return;
PromptKeywordOptions pko =
new PromptKeywordOptions(
"\nDisplay segment trace: "
);
pko.Keywords.Add("Yes");
pko.Keywords.Add("No");
pko.Keywords.Default = "Yes";
PromptResult pkr =
ed.GetKeywords(pko);
if (pkr.Status != PromptStatus.OK)
return;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
// Check the selected object - make sure it's
// a closed loop (could do some more checks here)
DBObject obj =
tr.GetObject(per.ObjectId, OpenMode.ForRead);
Curve cur = obj as Curve;
if (cur == null)
ed.WriteMessage("\nThis is not a curve.");
else
{
if (!cur.Closed)
ed.WriteMessage("\nLoop is not closed.");
else
{
// Extract parameters from our user-input...
// A flag for our vector tracing
doTrace = (pkr.StringResult == "Yes");
// The number of segments
int numBounces = pir.Value;
// The first vertex of our path
Point3d latest =
per.PickedPoint.
TransformBy(ed.CurrentUserCoordinateSystem).
OrthoProject(cur.GetPlane());
// Create our polyline path, adding the
// initial vertex
Polyline path = new Polyline();
path.Normal = cur.GetPlane().Normal;
path.AddVertexAt(
0,
latest.Convert2d(cur.GetPlane()),
0.0, 0.0, 0.0
);
// For each segment, get the next vertex
// and add it to the path
int i = 1;
while (i <= numBounces)
{
try
{
Point3d next =
nextBoundaryPoint(cur, latest, doTrace);
path.AddVertexAt(
i++,
next.Convert2d(cur.GetPlane()),
0.0, 0.0, 0.0
);
latest = next;
}
catch (Exception ex)
{
// If there's an exception we know about
// then ignore it and allow the loop to
// continue (we probably did not increment
// i in this case, as it will fail on
// nextBoundaryPoint)
if (ex.ErrorStatus !=
ErrorStatus.PointNotOnEntity)
throw ex;
}
}
// Open the modelspace
BlockTable bt =
(BlockTable)
tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)
tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
// 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);
tr.AddNewlyCreatedDBObject(path, true);
}
}
// Commit, whether we added a path or not.
tr.Commit();
// If we're tracing, pause for user input
// before regenerating the graphics
if (doTrace)
{
pko =
new PromptKeywordOptions(
"\nPress return to clear trace vectors: "
);
pko.AllowNone = true;
pko.AllowArbitraryInput = true;
pkr = ed.GetKeywords(pko);
ed.Regen();
}
}
}
}
}
As I've mentioned in the above code, this is not the most efficient possible implementation: we do several (currently 10) checks per potential vertex, to see whether it needs to be excluded. This number might well be reduced, or the library could be updated to provide a more efficient implementation. But for our purposes it works fine, so I'm not going to worry too much. Optimization is left as an exercise for the reader. :-)
Here's what happens when we run the BOUNCE command on a couple of boundaries. The first one I just created for testing - it's a lightweight polyline with three downward prongs that makes it likely that rays fired from the boundary will intersect it multiple (>2) times. For each of these examples I've composed three views: the boundary (pre-hatching), the trace vectors displayed to show the successful vectors (in green) and the excluded vectors (in red), and then the final hatch.
This first loop has been hatched with 100 segments, to clearly show the way the pattern works (or can work, as it's randomly generated).
The second loop I started drawing with straight segments and then switched to arcs. Somehow I ended up creating something that looks like it's out of Alien, although I wasn't (consciously, at least) in any way inspired by Giger (even though he's Swiss and has a museum in nearby Gruyères). Anyway, this one I hatched with 1000 segments, selecting the initial point on the boundary of the left-most leg: you can see that even with that many segments, not every piece of the boundary was reached using a random algorithm.
Update:
While working with this a little more, I realised I was not disposing the ray objects we were using for intersection calculations. I've now fixed the above code by inserting calls to ray.Dispose() in the appropriate places.
February 14, 2008 in AutoCAD, AutoCAD .NET, Hatches | Permalink | Comments (2) | TrackBack
Applying a gradient fill to an AutoCAD hatch using .NET
So, back in the saddle after an eventful week off, back in the UK. Aside from the extremely changeable weather (even by British standards) and the heightened security at UK airports, the week went swimmingly... :-)
It's now Friday night, and disappointingly I've spent nearly two days regaining control of my inbox. I really wanted to make a quick post before the weekend, so rather than diving into the issue I'd planned on tackling (Palettes), I decided to take a shot at a request I'd received by email a few weeks ago: to show how to create a gradient fill using .NET.
I started by reusing the code from this previous post: so much of the code I needed was there already, it had to be easy to adjust the hatch to use a gradient fill, right? Well, no - it took me much longer than I'd expected to work this one out. Maybe I should have stuck with Palettes, after all...
I decided to use a spherical gradient fill of two colours (the default colours used when you create a two-tone gradient fill using the UI), as this shows one of the trickier parts of the API, that of specifying the colours themselves. One other call it took me a while to work out was to set the HatchObjectType property - if you don't do that then you'll get an eAmbiguousOutput exception.
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Colors;
namespace HatchGradient
{
public class Commands
{
[CommandMethod("GFH")]
static public void GradientFillHatch()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Ask the user to select a hatch boundary
PromptEntityOptions opt =
new PromptEntityOptions(
 

Atom

