您的位置:首页 > 其它

Generating Koch fractals in AutoCAD using .NET - Part 1

2007-08-16 11:29 603 查看

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.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: