« June 2007 | Main | August 2007 »
Generating Koch fractals in AutoCAD using .NET - Part 1
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.
July 30, 2007 in AutoCAD, AutoCAD .NET, Drawing structure | Permalink | Comments (3) | TrackBack
YASA (Yet Another Simpsons Avatar)
For those of you that haven't noticed the current obsession in the CAD blogosphere of creating Simpsons avatars of oneself, check out this post from the Autodesk Labs blog for some background.
Now, to enter the fray myself. I spent some time a week or so ago playing around with the avatar creation tool on The Simpsons Movie website, but was pretty disappointed with the results:
But then I discovered the Simpsonize Me website, and my life changed. :-)
This site allows you to upload a photo, and it generates an avatar for you pretty much automatically (you have to specify a few parameters and can tweak the output, but this was a huge improvement, at least for someone with my extremely modest artistic talent):
The resolution of the output isn't as good as with the other tool, but you can get a fairly decent headshot for IM:
July 27, 2007 in Personal | Permalink | Comments (1) | TrackBack
Updating a specific attribute inside a folder of AutoCAD drawings using .NET
In the last post we looked at some code to search the current drawing for a particular attribute and update its value. In this post - as promised - we're going to look at how to extend this application to work on a folder of drawings, updating those that contain the attribute and saving them to a new filename.
Rather than implement a fancy, graphical user interface, I've stuck with my approach of using the command-line for input and output. If you wish to implement your own UI, please do - it's really easy using .NET. I get the occasional request to do this myself, but I prefer to keep my posts focused - there are other posts on this blog that focus specifically on UI-related issues.
I ended up changing the code somewhat - I had been printing the results of the batching process from within the UpdateAttributesInDatabase() function. As I get closer to extracting the functionality to a RealDWG application, I've started reducing the dependencies on the Editor (as this will not be available later on). The WriteMessage() calls are now in the commands themselves, instead of in the processing functions.
In addition to the previous UA (UpdateAttribute) command, I've added a new one called UAIF (UpdateAttributeInFiles). This one queries for some additional data (such as the path to the folder to process), and then uses some handy .NET Framework functionality to iterate through the drawings stored in a particular location. Rather than overwrite the originals, the updated files get saved to a new name (with "-updated" added to the filename). It is left as an exercise to put these in a separate folder or to save back to the original location (I'd rather not get blamed for overwriting valuable data :-).
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.IO;
using System;
namespace AttributeUpdater
{
public class Commands
{
[CommandMethod("UAIF")]
public void UpdateAttributeInFiles()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Have the user choose the block and attribute
// names, and the new attribute value
PromptResult pr =
ed.GetString(
"\nEnter folder containing DWGs to process: "
);
if (pr.Status != PromptStatus.OK)
return;
string pathName = pr.StringResult;
pr =
ed.GetString(
"\nEnter name of block to search for: "
);
if (pr.Status != PromptStatus.OK)
return;
string blockName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter tag of attribute to update: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter new value for attribute: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbValue = pr.StringResult;
string[] fileNames =
Directory.GetFiles(pathName,"*.dwg");
// We'll use some counters to keep track
// of how the processing is going
int processed = 0, saved = 0, problem = 0;
foreach (string fileName in fileNames)
{
if (fileName.EndsWith(
".dwg",
StringComparison.CurrentCultureIgnoreCase
)
)
{
string outputName =
fileName.Substring(
0,
fileName.Length - 4) +
"_updated.dwg";
Database db = new Database(false, false);
using (db)
{
try
{
ed.WriteMessage(
"\n\nProcessing file: " + fileName
);
db.ReadDwgFile(
fileName,
FileShare.ReadWrite,
false,
""
);
int attributesChanged =
UpdateAttributesInDatabase(
db,
blockName,
attbName,
attbValue
);
// Display the results
ed.WriteMessage(
"\nUpdated {0} instance{1} of " +
"attribute {2}.",
attributesChanged,
attributesChanged == 1 ? "" : "s",
attbName
);
// Only save if we changed something
if (attributesChanged > 0)
{
ed.WriteMessage(
"\nSaving to file: {0}", outputName
);
db.SaveAs(
outputName,
DwgVersion.Current
);
saved++;
}
processed++;
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nProblem processing file: {0} - \"{1}\"",
fileName,
ex.Message
);
problem++;
}
}
}
}
ed.WriteMessage(
"\n\nSuccessfully processed {0} files, of which {1} had " +
"attributes to update and an additional {2} had errors " +
"during reading/processing.",
processed,
saved,
problem
);
}
[CommandMethod("UA")]
public void UpdateAttribute()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Have the user choose the block and attribute
// names, and the new attribute value
PromptResult pr =
ed.GetString(
"\nEnter name of block to search for: "
);
if (pr.Status != PromptStatus.OK)
return;
string blockName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter tag of attribute to update: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbName = pr.StringResult.ToUpper();
pr =
ed.GetString(
"\nEnter new value for attribute: "
);
if (pr.Status != PromptStatus.OK)
return;
string attbValue = pr.StringResult;
ed.WriteMessage(
"\nProcessing file: " + db.Filename
);
int count =
UpdateAttributesInDatabase(
db,
blockName,
attbName,
attbValue
);
ed.Regen();
// Display the results
ed.WriteMessage(
"\nUpdated {0} instance{1} of " +
"attribute {2}.",
count,
count == 1 ? "" : "s",
attbName
);
}
private int UpdateAttributesInDatabase(
Database db,
string blockName,
string attbName,
string attbValue
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Get the IDs of the spaces we want to process
// and simply call a function to process each
ObjectId msId, psId;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
msId =
bt[BlockTableRecord.ModelSpace];
psId =
bt[BlockTableRecord.PaperSpace];
// Not needed, but quicker than aborting
tr.Commit();
}
int msCount =
UpdateAttributesInBlock(
msId,
blockName,
attbName,
attbValue
);
int psCount =
UpdateAttributesInBlock(
psId,
blockName,
attbName,
attbValue
);
return msCount + psCount;
}
private int UpdateAttributesInBlock(
ObjectId btrId,
string blockName,
string attbName,
string attbValue
)
{
// Will return the number of attributes modified
int changedCount = 0;
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
btrId,
OpenMode.ForRead
);
// Test each entity in the container...
foreach (ObjectId entId in btr)
{
Entity ent =
tr.GetObject(entId, OpenMode.ForRead)
as Entity;
if (ent != null)
{
BlockReference br = ent as BlockReference;
if (br != null)
{
BlockTableRecord bd =
(BlockTableRecord)tr.GetObject(
br.BlockTableRecord,
OpenMode.ForRead
);
// ... to see whether it's a block with
// the name we're after
if (bd.Name.ToUpper() == blockName)
{
// Check each of the attributes...
foreach (
ObjectId arId in br.AttributeCollection
)
{
DBObject obj =
tr.GetObject(
arId,
OpenMode.ForRead
);
AttributeReference ar =
obj as AttributeReference;
if (ar != null)
{
// ... to see whether it has
// the tag we're after
if (ar.Tag.ToUpper() == attbName)
{
// If so, update the value
// and increment the counter
ar.UpgradeOpen();
ar.TextString = attbValue;
ar.DowngradeOpen();
changedCount++;
}
}
}
}
// Recurse for nested blocks
changedCount +=
UpdateAttributesInBlock(
br.BlockTableRecord,
blockName,
attbName,
attbValue
);
}
}
}
tr.Commit();
}
return changedCount;
}
}
}
Here's what happens when I run it on my temp folder (which is full of crud, and that's putting it politely):
Command: UAIF
Enter folder containing DWGs to process: c:\temp
Enter name of block to search for: TEST
Enter tag of attribute to update: ONE
Enter new value for attribute: 1234
Processing file: c:\temp\-old_recover.dwg
Problem processing file: c:\temp\-old_recover.dwg - "eBadDwgHeader"
Processing file: c:\temp\4076612-2.DWG
Updated 0 instances of attribute ONE.
Processing file: c:\temp\attributes.dwg
Updated 5 instances of attribute ONE.
Saving to file: c:\temp\attributes_updated.dwg
[Deleted a lot of uninteresting reports]
Successfully processed 42 files, of which 1 had attributes to update and an
additional 3 had errors during reading/processing.
You'll see that some DWGs have failed to load: hopefully because they're in need of recovery rather than the workings of this code. The above technique should handle this gracefully, allowing you to go back and fix the problematic ones.
Update 1:
Norman Yuan pointed out a mistake in this code - I've been using the TransactionManager from the current document, rather than from the db. This isn't dramatically bad, but would have become more of a problem as we move to RealDWG. What I should really have done sooner is remove the doc and ed variables from the UpdateAttributesIn...() functions, so that I would not have been in a position to use them by mistake. I now pass in the Database as a parameter, which allows us to use it to get the appropriate TransactionManager. One other benefit this code enables is the choice of constructing the database without an associated document:
Database db = new Database(false, true);
This makes the code work more efficiently (as well as being cleaner).
Here are the updated functions - not the entire code:
private int UpdateAttributesInDatabase(
Database db,
string blockName,
string attbName,
string attbValue
)
{
// Get the IDs of the spaces we want to process
// and simply call a function to process each
ObjectId msId, psId;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
msId =
bt[BlockTableRecord.ModelSpace];
psId =
bt[BlockTableRecord.PaperSpace];
// Not needed, but quicker than aborting
tr.Commit();
}
int msCount =
UpdateAttributesInBlock(
db,
msId,
blockName,
attbName,
attbValue
);
int psCount =
UpdateAttributesInBlock(
db,
psId,
blockName,
attbName,
attbValue
);
return msCount + psCount;
}
private int UpdateAttributesInBlock(
Database db,
ObjectId btrId,
string blockName,
string attbName,
string attbValue
)
{
// Will return the number of attributes modified
int changedCount = 0;
Transaction tr =
db.TransactionManager.StartTransaction();
&

Atom







