We have a number of candidate “Plugins of the Month” currently in the pipeline – including Inventor versions of Screenshot and Clipboard Manager as well as a tool to streamline batch plotting from AutoCAD – but unfortunately none were looking ready enough to count on for January’s posting. So yesterday I dipped into the plugins that have generously been proposed/provided by external parties and I put together a C# version of a tool submitted by our old friend Jon Smith from COINS. Jon provided a number of C++ tools that COINS has made available for free, one of which was a handy little command called “FacetCurve”.
The tool allows you to break any AutoCAD curve – whether a line, arc, polyline, circle, ellipse, spline, etc. – into a series of line segments or facets (although you probably wouldn’t bother using it on lines, as the results aren’t very interesting :-). The command has three modes of operation: by number of segments, by maximum segment length and by fixed segment length. It can be configured to generate lines or polylines (whether they are lightweight or 3D polylines depends on whether the source curve is planar or not). This version of the tool exposes a command-line – rather than a dialog-based – user-interface.
Given the fact many people (including me!) will be taking an extended break over the holidays, the plugin probably won’t get posted before the second week of January, so there’s time for me to incorporate feedback, if you have it (whether submitted by email or as a comment on this blog).
Here’s the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using System.Configuration;
using DemandLoading;
namespace FacetCurve
{
public class FacetCurveApplication : IExtensionApplication
{
// Define a class for our custom data
public enum FacetOpType
{
ByNumberOfSegments = 0,
ByMaximumSegmentLength = 1,
ByFixedSegmentLength = 2
}
public class AppData : ApplicationSettingsBase
{
[UserScopedSetting()]
[DefaultSettingValue("ByNumberOfSegments")]
public FacetOpType FacetType
{
get { return ((FacetOpType)this["FacetType"]); }
set { this["FacetType"] = (FacetOpType)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("5")]
public int NumberOfSegments
{
get { return ((int)this["NumberOfSegments"]); }
set { this["NumberOfSegments"] = (int)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("100.0")]
public double MaximumSegmentLength
{
get { return ((double)this["MaximumSegmentLength"]); }
set { this["MaximumSegmentLength"] = (double)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("10.0")]
public double FixedSegmentLength
{
get { return ((double)this["FixedSegmentLength"]); }
set { this["FixedSegmentLength"] = (double)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("false")]
public bool TransferProperties
{
get { return ((bool)this["TransferProperties"]); }
set { this["TransferProperties"] = (bool)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("false")]
public bool CreatePolyline
{
get { return ((bool)this["CreatePolyline"]); }
set { this["CreatePolyline"] = (bool)value; }
}
[UserScopedSetting()]
[DefaultSettingValue("false")]
public bool EraseOriginalCurve
{
get { return ((bool)this["EraseOriginalCurve"]); }
set { this["EraseOriginalCurve"] = (bool)value; }
}
}
public FacetCurveApplication()
{
}
[CommandMethod("ADNPLUGINS", "REMOVEFC", CommandFlags.Modal)]
static public void RemoveFacetCurve()
{
DemandLoading.RegistryUpdate.UnregisterForDemandLoading();
Editor ed =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nThe FacetCurve plugin will not be loaded" +
" automatically in future editing sessions.");
}
[CommandMethod("ADNPLUGINS", "FACETCURVE", CommandFlags.Modal)]
static public void FacetCurve()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Retrieve our application settings (or create new ones)
AppData ad = new AppData();
ad.Reload();
if (ad != null)
{
bool settingschosen;
PromptEntityResult per;
do
{
settingschosen = false;
// Ask the user for the screen window to capture
PrintMode(ed, ad);
PrintSettings(ed, ad);
PromptEntityOptions peo =
new PromptEntityOptions(
"\nSelect curve or " +
"[Mode/Settings]: ",
"Mode Settings"
);
peo.SetRejectMessage(
"\nSelected entity must be an arc, circle, spline, " +
"polyline, or other type of curve."
);
peo.AddAllowedClass(typeof(Curve), false);
// Get a curve or a keyword
per = ed.GetEntity(peo);
if (per.Status == PromptStatus.Keyword)
{
if (per.StringResult == "Mode")
{
if (GetMode(ed, ad))
ad.Save();
settingschosen = true;
}
if (per.StringResult == "Settings")
{
if (GetSettings(ed, ad))
ad.Save();
settingschosen = true;
}
}
}
while (settingschosen); // Loop if settings were modified
if (per.Status == PromptStatus.OK)
{
// Now we facet our curve
try
{
FacetCurve(doc, per.ObjectId, ad);
}
catch (Exception ex)
{
ed.WriteMessage(
"\nProblem faceting this curve: {0}",
ex
);
}
}
}
}
// Print the current application mode to the command-line
private static void PrintMode(Editor ed, AppData ad)
{
ed.WriteMessage("\nCurrent mode: ");
if (ad.FacetType == FacetOpType.ByNumberOfSegments)
{
ed.WriteMessage(
"By number of segments, number={0}",
ad.NumberOfSegments
);
}
else if (ad.FacetType ==
FacetOpType.ByMaximumSegmentLength)
{
ed.WriteMessage(
"By maximum segment length, length={0}",
ad.MaximumSegmentLength
);
}
else if (ad.FacetType ==
FacetOpType.ByFixedSegmentLength)
{
ed.WriteMessage(
"By fixed segment length, length={0}",
ad.FixedSegmentLength
);
}
}
// Print the current application settings to the command-line
private static void PrintSettings(Editor ed, AppData ad)
{
ed.WriteMessage(
"\nCurrent settings: Create polyline={0}, " +
"Transfer entity properties={1}," +
"\nErase original curve={2}",
ad.CreatePolyline ? "Yes" : "No",
ad.TransferProperties ? "Yes" : "No",
ad.EraseOriginalCurve ? "Yes" : "No"
);
}
// Ask the user to modify the application mode
private static bool GetMode(Editor ed, AppData ad)
{
// At our top-level settings prompt, make the default
// to exit back up
PromptKeywordOptions pko =
new PromptKeywordOptions(
"\nSelect mode [NumberOfSegments/" +
"MaximumSegmentLength/FixedSegmentLength]: ",
"NumberOfSegments MaximumSegmentLength FixedSegmentLength"
);
switch (ad.FacetType)
{
case FacetOpType.ByMaximumSegmentLength:
pko.Keywords.Default = "MaximumSegmentLength";
break;
case FacetOpType.ByFixedSegmentLength:
pko.Keywords.Default = "FixedSegmentLength";
break;
default:
pko.Keywords.Default = "NumberOfSegments";
break;
}
PromptResult pr;
bool settingschanged = false;
// Start by printing the current settings
PrintMode(ed, ad);
pr = ed.GetKeywords(pko);
if (pr.Status == PromptStatus.OK)
{
if (pr.StringResult == "NumberOfSegments")
{
// If NumberOfSegments is selected, ask for the number
if (ad.FacetType != FacetOpType.ByNumberOfSegments)
{
ad.FacetType = FacetOpType.ByNumberOfSegments;
settingschanged = true;
}
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter number of segments: "
);
pio.DefaultValue = ad.NumberOfSegments;
pio.UseDefaultValue = true;
PromptIntegerResult pir = ed.GetInteger(pio);
if (pir.Status == PromptStatus.OK)
{
if (ad.NumberOfSegments != pir.Value)
{
ad.NumberOfSegments = pir.Value;
settingschanged = true;
}
}
}
else if (pr.StringResult == "MaximumSegmentLength")
{
// If MaximumSegmentLength is selected, ask for the length
if (ad.FacetType != FacetOpType.ByMaximumSegmentLength)
{
ad.FacetType = FacetOpType.ByMaximumSegmentLength;
settingschanged = true;
}
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter maximum segment length: "
);
pdo.DefaultValue = ad.MaximumSegmentLength;
pdo.UseDefaultValue = true;
PromptDoubleResult pdr = ed.GetDouble(pdo);
if (pdr.Status == PromptStatus.OK)
{
if (ad.MaximumSegmentLength != pdr.Value)
{
ad.MaximumSegmentLength = pdr.Value;
settingschanged = true;
}
}
}
else if (pr.StringResult == "FixedSegmentLength")
{
// If FixedSegmentLength is selected, ask for the length
if (ad.FacetType != FacetOpType.ByFixedSegmentLength)
{
ad.FacetType = FacetOpType.ByFixedSegmentLength;
settingschanged = true;
}
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter maximum segment length: "
);
pdo.DefaultValue = ad.FixedSegmentLength;
pdo.UseDefaultValue = true;
PromptDoubleResult pdr = ed.GetDouble(pdo);
if (pdr.Status == PromptStatus.OK)
{
if (ad.FixedSegmentLength != pdr.Value)
{
ad.FixedSegmentLength = pdr.Value;
settingschanged = true;
}
}
}
}
return settingschanged;
}
// Ask the user to modify the application settings
private static bool GetSettings(Editor ed, AppData ad)
{
// At our top-level settings prompt, make the default
// to exit back up
PromptKeywordOptions pko =
new PromptKeywordOptions(
"\nSetting to change " +
"[CreatePolyline/TransferProperties/EraseOriginal/Exit]: ",
"CreatePolyline TransferProperties EraseOriginal Exit"
);
pko.Keywords.Default = "Exit";
PromptResult pr;
bool settingschanged = false;
do
{
// Start by printing the current settings
PrintSettings(ed, ad);
pr = ed.GetKeywords(pko);
if (pr.Status == PromptStatus.OK)
{
if (pr.StringResult == "CreatePolyline")
{
// If CreatePolyline is different, ask whether to
// create lines or polylines
bool different =
GetYesOrNo(
ed,
"\nCreate a polyline rather than lines for " +
"multi-segment results",
ad.CreatePolyline
);
if (different)
{
ad.CreatePolyline = !ad.CreatePolyline;
settingschanged = true;
}
}
else if (pr.StringResult == "TransferProperties")
{
// If TransferProperties is different, ask whether to
// copy entity properties from the original
bool different =
GetYesOrNo(
ed,
"\nTransfer entity properties (layer, linetype, " +
"etc.) from the original curve",
ad.TransferProperties
);
if (different)
{
ad.TransferProperties = !ad.TransferProperties;
settingschanged = true;
}
}
else if (pr.StringResult == "EraseOriginal")
{
// If EraseOriginal is different, ask whether to
// erase original curve
bool different =
GetYesOrNo(
ed,
"\nErase original curve",
ad.EraseOriginalCurve
);
if (different)
{
ad.EraseOriginalCurve = !ad.EraseOriginalCurve;
settingschanged = true;
}
}
}
}
while (
pr.Status == PromptStatus.OK &&
pr.StringResult != "Exit"
); // Loop until Exit or cancel
return settingschanged;
}
// Ask the user to enter yes or no to a particular question,
// setting the default option appropriately
private static bool GetYesOrNo(
Editor ed,
string prompt,
bool defval
)
{
bool changed = false;
PromptKeywordOptions pko =
new PromptKeywordOptions(prompt + " [Yes/No]: ", "Yes No");
// The default depends on our current settings
pko.Keywords.Default =
(defval ? "Yes" : "No");
PromptResult pr = ed.GetKeywords(pko);
if (pr.Status == PromptStatus.OK)
{
// Change the settings, as needed
bool newval =
(pr.StringResult == "Yes");
if (defval != newval)
{
changed = true;
}
}
return changed;
}
private static void FacetCurve(
Document doc, ObjectId curId, AppData ad
)
{
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr = doc.TransactionManager.StartTransaction();
using (tr)
{
// Open our curve
DBObject obj = tr.GetObject(curId, OpenMode.ForRead);
Curve cur = obj as Curve;
if (cur != null)
{
// We'll gather the points along the curve in a collection
Point3dCollection pts = null;
if (ad.FacetType == FacetOpType.ByNumberOfSegments)
{
// "By number of segments" means a simple function call
pts = VectorizeCurve(cur, ad.NumberOfSegments);
}
else if (ad.FacetType ==
FacetOpType.ByMaximumSegmentLength)
{
// "By maximum segment length" needs more work
// Start by getting the length of the curve
double startDist =
cur.GetDistanceAtParameter(cur.StartParam);
double endDist =
cur.GetDistanceAtParameter(cur.EndParam);
double curLen = endDist - startDist;
// If shorter than the maximum segment length,
// then there's little to do
if (curLen < ad.MaximumSegmentLength)
{
ed.WriteMessage(
"\nMaximum segment length too high for " +
"this length of curve."
);
return;
}
// We'll start by assuming twice as many segments
// as the number found by dividing the curve length
// (should be an adequate starting point)
int startSegs =
(int)(2 * curLen / ad.MaximumSegmentLength);
// Loop back from this number, decrementing each time,
// to find the maximum number of segments where all
// segments are less than the maximum segment length
for (int i = startSegs; i > 0; i--)
{
Point3dCollection tmppts = VectorizeCurve(cur, i);
if (tmppts.Count < 2)
continue;
// Check all lengths in the array, looking for
// any that are longer than the maximum
// (at which point we break)
bool allshorter = true;
for (int j = 0; j < tmppts.Count - 1; j++)
{
if (tmppts[j].DistanceTo(tmppts[j + 1]) >
ad.MaximumSegmentLength)
{
allshorter = false;
break;
}
}
// If all were shorter, save the points: if the next
// pass through finds any segment longer than the
// maximum, we'll use them
if (allshorter)
pts = tmppts;
else
break;
}
}
else if (ad.FacetType == FacetOpType.ByFixedSegmentLength)
{
// "By fixed segment length" also needs some work
// The algorithm uses planar intersection, so cannot
// work with non-planar curves
if (!cur.IsPlanar)
{
ed.WriteMessage(
"\nFixed segment mode only works with" +
" planar curves."
);
return;
}
// If planar, get the plane
Plane p = cur.GetPlane();
// Initialize our results collection, add the 1st point
pts = new Point3dCollection();
pts.Add(cur.StartPoint);
// Loop along the length of the curve
bool last = false;
while (!last)
{
// We check the intersection between the curve and a
// circle with the fixed segment length as its radius
Circle c =
new Circle(
pts[pts.Count-1], p.Normal, ad.FixedSegmentLength
);
Point3dCollection intPts = new Point3dCollection();
cur.IntersectWith(
c, Intersect.ExtendArgument, intPts, 0, 0
);
// We'll look for the closest of the intersection
// points to the base point
Point3d closest;
if (intPts.Count < 1)
{
// Found no intersections:
// use the curve's end point
closest = cur.EndPoint;
last = true;
}
else
{
// Found one or more intersections:
// take the closest of them
double baseParam =
cur.GetParameterAtPoint(pts[pts.Count-1]);
double minParam = 999999; // Big number
bool found = false;
foreach (Point3d pt in intPts)
{
// Check the point's parameter
double param = cur.GetParameterAtPoint(pt);
// If it's larger than the base parameter but
// smaller than the minumum found so far, use it
if (param > baseParam && param < minParam)
{
minParam = param;
closest = pt;
found = true;
// If it's the same as the curve's end point,
// no need to loop again
last = (pt == cur.EndPoint);
}
}
// If we didn't find a close intersection, it means
// we're at the end. Use the curve's end-point for
// the last vertex
if (!found)
{
closest = cur.EndPoint;
last = true;
}
}
pts.Add(closest);
}
}
// Now we can go and create our lines/polyline
if (pts != null && pts.Count > 0)
{
// Open the current space for write
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
if (!ad.CreatePolyline || pts.Count <= 2)
{
// Create a sequence of line entities
if (pts.Count >= 2)
{
for (int i = 0; i < pts.Count - 1; i++)
{
Line ln = new Line();
if (ad.TransferProperties)
ln.SetPropertiesFrom(cur);
ln.StartPoint = pts[i];
ln.EndPoint = pts[i + 1];
btr.AppendEntity(ln);
tr.AddNewlyCreatedDBObject(ln, true);
}
}
}
else
{
// Create a polyline (either lightweight or 3D)
if (cur.IsPlanar)
{
// Create a lightweight 2D polyline
Plane p = cur.GetPlane();
Polyline pl = new Polyline(pts.Count);
pl.Normal = p.Normal;
if (ad.TransferProperties)
pl.SetPropertiesFrom(cur);
// Add each of the vertices to the polyline,
// converting them to the correct plane
foreach (Point3d pt in pts)
{
pl.AddVertexAt(
pl.NumberOfVertices, pt.Convert2d(p),
0.0, 0.0, 0.0
);
}
// Transform the polyline to get it to the right
// place
pl.TransformBy(
Matrix3d.Displacement(
p.GetCoordinateSystem().Origin - Point3d.Origin
)
);
// Add it to the drawing
btr.AppendEntity(pl);
tr.AddNewlyCreatedDBObject(pl, true);
}
else
{
// Create a 3D polyline
Polyline3d pl =
new Polyline3d(Poly3dType.SimplePoly, pts, false);
if (ad.TransferProperties)
pl.SetPropertiesFrom(cur);
btr.AppendEntity(pl);
tr.AddNewlyCreatedDBObject(pl, true);
}
}
}
// Erase the original curve if requested
if (ad.EraseOriginalCurve)
{
cur.UpgradeOpen();
cur.Erase();
}
}
tr.Commit();
}
}
private static Point3dCollection VectorizeCurve(
Curve cur, int numSeg
)
{
// Collect points along our curve
Point3dCollection pts = new Point3dCollection();
// Split the curve's parameter space into
// equal parts
double startParam = cur.StartParam;
double segLen =
(cur.EndParam - startParam) / numSeg;
// Loop along it, getting points each time
for (int i = 0; i < numSeg + 1; i++)
{
Point3d pt =
cur.GetPointAtParameter(startParam + segLen * i);
pts.Add(pt);
}
return pts;
}
// IExtensionApplication protocol
public void Initialize()
{
try
{
// Create Registry entries for automatic loading
RegistryUpdate.RegisterForDemandLoading();
}
catch
{ }
}
public void Terminate()
{
}
}
}
To build it you will need to incorporate the demand-loading code from this previous post or one of the previous plugins of the month.
If you run the FACETCURVE command you will be presented with these choices:
Command: FACETCURVE
Current mode: By number of segments, number=5
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Select curve or [Mode/Settings]:
To facet a curve using the default mode and settings, just select it. To change the mode you can enter the “Mode” keyword (or just “M”) and select the appropriate mode of operation. Here we cycle through the different modes and change back to the first one, albeit with more segments:
Select curve or [Mode/Settings]: M
Current mode: By number of segments, number=5
Select mode [NumberOfSegments/MaximumSegmentLength/FixedSegmentLength]
<NumberOfSegments>: M
Enter maximum segment length <100.0000>: 1
Current mode: By maximum segment length, length=1
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Select curve or [Mode/Settings]: M
Current mode: By maximum segment length, length=1
Select mode [NumberOfSegments/MaximumSegmentLength/FixedSegmentLength]
<MaximumSegmentLength>: F
Enter maximum segment length <10.0000>: 3
Current mode: By fixed segment length, length=3
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Select curve or [Mode/Settings]: M
Current mode: By fixed segment length, length=3
Select mode [NumberOfSegments/MaximumSegmentLength/FixedSegmentLength]
<FixedSegmentLength>: N
Enter number of segments <5>: 10
Current mode: By number of segments, number=10
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Select curve or [Mode/Settings]:
To change the settings – whether to create a polyline rather than lines, to transfer entity properties from the original to the resultant entity/entities or to erase the original curve – select “Settings” (or “S”):
Select curve or [Mode/Settings]: S
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Setting to change [CreatePolyline/TransferProperties/EraseOriginal/Exit]
<Exit>: C
Create a polyline rather than lines for multi-segment results [Yes/No] <No>:
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Setting to change [CreatePolyline/TransferProperties/EraseOriginal/Exit]
<Exit>: T
Transfer entity properties (layer, linetype, etc.) from the original curve
[Yes/No] <No>:
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Setting to change [CreatePolyline/TransferProperties/EraseOriginal/Exit]
<Exit>: E
Erase original curve [Yes/No] <No>:
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Setting to change [CreatePolyline/TransferProperties/EraseOriginal/Exit] <Exit>:
Current mode: By number of segments, length=10
Current settings: Create polyline=No, Transfer entity properties=No,
Erase original curve=No
Select curve or [Mode/Settings]:
Now let’s see the results of faceting a curve using with various modes & settings. Here’s the original:
Here’s the faceted version with 10 segments – the segments will vary in length, as the curve’s parameter space is divided equally by the number of segments, which doesn’t necessarily result in equally sized segments.
If we use the “maximum segment length” mode, the curve is faceted multiple times using the “by number of segments” method until none of the segments exceed the maximum segment length. Here we’ve used a segment length of 5 (for a spline-length of around 31).
If we want a standard segment length – except for the last segment which may prove to be shorter, of course – then we can use the “fixed segment length” mode. Here’s we’ve used a fixed segment length of 3.
And, of course, we can choose use the settings to to erase the original curve and use its standard properties (layer, linetype, etc.).
Thanks again to COINS and to Jon for providing the original application that formed the basis for this plugin. And please let me know if you have any feedback!