I'm currently waiting to get my RealDWG license through, so I'll interrupt the previous series on side databases to focus on something a little different. I'll get back to it, in due course, I promise. :-)
A long time ago, back during my first few years at Autodesk (which logically must have been some time in the mid- to late-90s, but I forget now), I developed an ObjectARX application to create fractals from linear geometry. I first got interested in the subject when I stumbled across something called the Koch curve: a very basic fractal - in fact one of the first ever described, back in the early 20th century - which also happens to be very easy to have AutoCAD generate.
Let's take a quick look at what a Koch curve is. Basically it's what you get when you take a line and split it into 3 segments of equal length. You keep the ones at either end, but replace the middle segment with 2 more segments the same length as all the others, each rotated outwards by 60 degrees to form the other two sides of an equilateral triangle. So for each "level" you get 4 lines from a single line.
Here it is in pictures.
A line...
... becomes four lines...
... which, in turn, becomes sixteen...
... etc. ...
... etc. ...
... etc. ...
From here on you don't see much change at this resolution. :-)
I also worked out how to perform the same process on arcs:
The original ObjectARX application I wrote implemented a few different commands which could work either on the whole drawing or on selected objects. Both types of command asked the user for two pieces of information:
- The direction of the operation
- Left means that the pointy bit will be added to the left of the line or arc, going from start to end point
- Right means the opposite
- The level of the recursion
- I call it recursion, but it's actually performed iteratively. But the point is, the algorithm loops, replacing layers of geometry with their decomposed (or "Kochized") equivalents
Aside from the fun aspect of this (something I like to have in my samples, when I can), the project taught me a number of ObjectARX fundamentals:
- Geometry library - how to use the ObjectARX geometry library to perform calculations and transform AutoCAD geometry
- Deep operations on transient geometry - how to work on lots (and I mean lots) of intermediate, non-database resident AutoCAD geometry, only adding the "results" (the final output) to the AutoCAD database
- Protocol extensions - how to extend the built-in protocol of existing classes (AcDbLine, AcDbArc, etc.) to create an extensible plugin framework (for example)
- In my original implementation I implemented Protocol Extensions for a number of objects, allowing to "Kochize" anything from an entire DWG down to individual lines, arcs and polylines. This would also have allowed someone to come in and hook their own modules into my commands, allowing them to also work on custom objects (or on standard objects I hadn't implemented).
- Progress meters - how to implement a UI that kept the user informed of progress and gave them the option to cancel long operations
Today I spent some time converting the code across to .NET. A few notes on this:
- A mechanism that's comparable with ObjectARX Protocol Extensions is not currently available in .NET (I believe something similar is coming in Visual Studio 2008/C# 3.0/VB 9, where we'll get extension methods)
- I ended up creating a very basic set of functions with a similar protocol, and using the one accepting an Entity to dispatch calls to the different versions, depending on the object type.
- I've just focused on Lines and Arcs in the initial port, but plan on adding support for complex (Polyline) entities soon
- Ditto for the progress meter - I've left long operations to complete in their own sweet time, for now, but plan on hooking the code into AutoCAD's progress meter at some point
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.Collections.Generic;
using System;
namespace Kochizer
{
public class Commands
{
// We generate 4 new entities for every old entity
// (unless a complex entity such as a polyline)
const int newEntsPerOldEnt = 4;
[CommandMethod("KA")]
public void KochizeAll()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Acquire user input - whether to create the
// new geometry to the left or the right...
PromptKeywordOptions pko =
new PromptKeywordOptions(
"\nCreate fractal to side (Left/<Right>): "
);
pko.Keywords.Add("Left");
pko.Keywords.Add("Right");
PromptResult pr =
ed.GetKeywords(pko);
bool bLeft = false;
if (pr.Status != PromptStatus.None &&
pr.Status != PromptStatus.OK)
return;
if ((string)pr.StringResult == "Left")
bLeft = true;
// ... and the recursion depth for the command.
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter recursion level <1>: "
);
pio.AllowZero = false;
pio.AllowNegative = false;
pio.AllowNone = true;
PromptIntegerResult pir =
ed.GetInteger(pio);
int recursionLevel = 1;
if (pir.Status != PromptStatus.None &&
pir.Status != PromptStatus.OK)
return;
if (pir.Status == PromptStatus.OK)
recursionLevel = pir.Value;
// Note: strictly speaking we're not recursing,
// we're iterating, but the effect to the user
// is the same.
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
using (bt)
{
// No need to open the block table record
// for write, as we're just reading data
// for now
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForRead
);
using (btr)
{
// List of changed entities
// (will contain complex entities, such as
// polylines"
ObjectIdCollection modified =
new ObjectIdCollection();
// List of entities to erase
// (will contain replaced entities)
ObjectIdCollection toErase =
new ObjectIdCollection();
// List of new entitites to add
// (will be processed recursively or
// assed to the open block table record)
List<Entity> newEntities =
new List<Entity>(
db.ApproxNumObjects * newEntsPerOldEnt
);
// Kochize each entity in the open block
// table record
foreach (ObjectId objId in btr)
{
Entity ent =
(Entity)tr.GetObject(
objId,
OpenMode.ForRead
);
Kochize(
ent,
modified,
toErase,
newEntities,
bLeft
);
}
// If we need to loop,
// work on the returned entities
while (--recursionLevel > 0)
{
// Create an output array
List<Entity> newerEntities =
new List<Entity>(
newEntities.Count * newEntsPerOldEnt
);
// Kochize all the modified (complex) entities
foreach (ObjectId objId in modified)
{
Entity ent =
(Entity)tr.GetObject(
objId,
OpenMode.ForRead
);
Kochize(
ent,
modified,
toErase,
newerEntities,
bLeft
);
}
// Kochize all the non-db resident entities
foreach (Entity ent in newEntities)
{
Kochize(
ent,
modified,
toErase,
newerEntities,
bLeft
);
}
// We now longer need the intermediate entities
// previously output for the level above,
// we replace them with the latest output
newEntities.Clear();
newEntities = newerEntities;
}
// Erase each of the replaced db-resident entities
foreach (ObjectId objId in toErase)
{
Entity ent =
(Entity)tr.GetObject(
objId,
OpenMode.ForWrite
);
ent.Erase();
}
// Add the new entities
btr.UpgradeOpen();
foreach (Entity ent in newEntities)
{
btr.AppendEntity(ent);
tr.AddNewlyCreatedDBObject(ent, true);
}
tr.Commit();
}
}
}
}
// Dispatch function to call through to various per-type
// functions
private void Kochize(
Entity ent,
ObjectIdCollection modified,
ObjectIdCollection toErase,
List<Entity> toAdd,
bool bLeft
)
{
Line ln = ent as Line;
if (ln != null)
{
Kochize(ln, modified, toErase, toAdd, bLeft);
return;
}
Arc arc = ent as Arc;
if (arc != null)
{
Kochize(arc, modified, toErase, toAdd, bLeft);
return;
}
}
// Create 4 new lines from a line passed in
private void Kochize(
Line ln,
ObjectIdCollection modified,
ObjectIdCollection toErase,
List<Entity> toAdd,
bool bLeft
)
{
// Get general info about the line
// and calculate the main 5 points
Point3d pt1 = ln.StartPoint,
pt5 = ln.EndPoint;
Vector3d vec1 = pt5 - pt1,
norm1 = vec1.GetNormal();
double d_3 = vec1.Length / 3;
Point3d pt2 = pt1 + (norm1 * d_3),
pt4 = pt1 + (2 * norm1 * d_3);
Vector3d vec2 = pt4 - pt2;
if (bLeft)
vec2 =
vec2.RotateBy(
Math.PI / 3, new Vector3d(0, 0, 1)
);
else
vec2 =
vec2.RotateBy(
5 * Math.PI / 3, new Vector3d(0, 0, 1)
);
Point3d pt3 = pt2 + vec2;
// Mark the original to be erased
if (ln.ObjectId != ObjectId.Null)
toErase.Add(ln.ObjectId);
// Create the first line
Line ln1 = new Line(pt1, pt2);
ln1.SetPropertiesFrom(ln);
ln1.Thickness = ln.Thickness;
toAdd.Add(ln1);
// Create the second line
Line ln2 = new Line(pt2, pt3);
ln2.SetPropertiesFrom(ln);
ln2.Thickness = ln.Thickness;
toAdd.Add(ln2);
// Create the third line
Line ln3 = new Line(pt3, pt4);
ln3.SetPropertiesFrom(ln);
ln3.Thickness = ln.Thickness;
toAdd.Add(ln3);
// Create the fourth line
Line ln4 = new Line(pt4, pt5);
ln4.SetPropertiesFrom(ln);
ln4.Thickness = ln.Thickness;
toAdd.Add(ln4);
}
// Create 4 new arcs from an arc passed in
private void Kochize(
Arc arc,
ObjectIdCollection modified,
ObjectIdCollection toErase,
List<Entity> toAdd,
bool bLeft
)
{
// Get general info about the arc
// and calculate the main 5 points
Point3d pt1 = arc.StartPoint,
pt5 = arc.EndPoint;
double length = arc.GetDistAtPoint(pt5),
angle = arc.StartAngle;
//bool bLocalLeft = false;
Vector3d full = pt5 - pt1;
//if (full.GetAngleTo(Vector3d.XAxis) > angle)
//bLocalLeft = true;
Point3d pt2 = arc.GetPointAtDist(length / 3),
pt4 = arc.GetPointAtDist(2 * length / 3);
// Mark the original to be erased
if (arc.ObjectId != ObjectId.Null)
toErase.Add(arc.ObjectId);
// Create the first arc
Point3d mid = arc.GetPointAtDist(length / 6);
CircularArc3d tmpArc = new CircularArc3d(pt1, mid, pt2);
Arc arc1 = circArc2Arc(tmpArc);
arc1.SetPropertiesFrom(arc);
arc1.Thickness = arc.Thickness;
toAdd.Add(arc1);
// Create the second arc
mid = arc.GetPointAtDist(length / 2);
tmpArc.Set(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(Math.PI / 3, Vector3d.ZAxis, pt2);
else
tmpArc.RotateBy(5 * Math.PI / 3, Vector3d.ZAxis, pt2);
Arc arc2 = circArc2Arc(tmpArc);
arc2.SetPropertiesFrom(arc);
arc2.Thickness = arc.Thickness;
toAdd.Add(arc2);
// Create the third arc
mid = arc.GetPointAtDist(length / 2);
tmpArc.Set(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(5 * Math.PI / 3, Vector3d.ZAxis, pt4);
else
tmpArc.RotateBy(Math.PI / 3, Vector3d.ZAxis, pt4);
Arc arc3 = circArc2Arc(tmpArc);
arc3.SetPropertiesFrom(arc);
arc3.Thickness = arc.Thickness;
toAdd.Add(arc3);
// Create the fourth arc
mid = arc.GetPointAtDist(5 * length / 6);
Arc arc4 =
circArc2Arc(new CircularArc3d(pt4, mid, pt5));
arc4.SetPropertiesFrom(arc);
arc4.Thickness = arc.Thickness;
toAdd.Add(arc4);
}
Arc circArc2Arc(CircularArc3d circArc)
{
double ang, start, end;
ang =
circArc.ReferenceVector.GetAngleTo(Vector3d.XAxis);
ang =
(circArc.ReferenceVector.Y < 0 ? -ang : ang);
start = circArc.StartAngle + ang;
end = circArc.EndAngle + ang;
return (
new Arc(
circArc.Center,
circArc.Normal,
circArc.Radius,
start,
end
)
);
}
}
}
Here's how it works for lines and arcs in a drawing. I took the example of an equilateral triangle (and something quite like it, made out of arcs), which is the classic case that makes a Koch snowflake or Koch star. I used a recursion level of 6 - once again, more detail than is needed at this resolution.
Next time I'll look at some of the missing pieces - perhaps adding the progress meter or support for complex types, such as polylines. Or then again I may switch back to the RealDWG sample, if I get the license through.