Kean专题(13)—Geometry
本帖最后由 lzh741206 于 2010-12-19 18:30 编辑http://through-the-interface.typepad.com/through_the_interface/fractals/
一、生成Koch分形
July 30, 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 theKoch 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;
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,
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.
August 02, 2007
Generating Koch fractals in AutoCAD using .NET - Part 2
This post continues on from the last one, which introduced some code that creates "Koch curves" inside AutoCAD. Not in itself something you'll want to do to your drawings, but the techniques shown may well prove helpful for your applications.
Last time we implemented support for Lines and Arcs - in this post we extend that to Polylines. These are a different animal, as rather than replacing the original entities with 4 times as many for each "level", in this case we add 3 new segments to each original segment. So we don't then mark the entities for erasure, either.
In addition to the code needed to support polylines, I also had to make some minor modifications. I've marked them below in red. Basically there were a few areas where I wasn't as careful as I might have been when it comes to supporting modifying planar entities in arbitrary 3D spaces. So I fixed a few bugs, and also reimplemented the helper function that converts CircularArc3ds to Arcs.
Here's the updated 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;
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,
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;
}
Polyline pl = ent as Polyline;
if (pl != null)
{
Kochize(pl, 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;
Vector3d full = pt5 - pt1;
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, tmpArc.Normal, pt2);
else
tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt2);
Arc arc2 = circArc2Arc(tmpArc);
arc2.SetPropertiesFrom(arc);
arc2.Thickness = arc.Thickness;
toAdd.Add(arc2);
// Create the third arc
tmpArc.Set(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt4);
else
tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, 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)
{
Point3d center = circArc.Center;
Vector3d normal = circArc.Normal;
Vector3d refVec = circArc.ReferenceVector;
Plane plane = new Plane(center, normal);
double ang = refVec.AngleOnPlane(plane);
return new Arc(
center,
normal,
circArc.Radius,
circArc.StartAngle + ang,
circArc.EndAngle + ang
);
}
private void Kochize(
Polyline pl,
ObjectIdCollection modified,
ObjectIdCollection toErase,
List<Entity> toAdd,
bool bLeft
)
{
pl.UpgradeOpen();
if (pl.ObjectId != ObjectId.Null &&
!modified.Contains(pl.ObjectId))
{
modified.Add(pl.ObjectId);
}
for(int vn = 0; vn < pl.NumberOfVertices; vn++)
{
SegmentType st = pl.GetSegmentType(vn);
if (st != SegmentType.Line && st != SegmentType.Arc)
continue;
double sw = pl.GetStartWidthAt(vn),
ew = pl.GetEndWidthAt(vn);
if (st == SegmentType.Line)
{
if (vn + 1 == pl.NumberOfVertices)
continue;
LineSegment2d ls = pl.GetLineSegment2dAt(vn);
Point2d pt1 = ls.StartPoint,
pt5 = ls.EndPoint;
Vector2d vec = pt5 - pt1;
double d_3 = vec.Length / 3;
Point2d pt2 = pt1 + (vec.GetNormal() * d_3),
pt4 = pt1 + (vec.GetNormal() * 2 * d_3);
Vector2d vec2 = pt4 - pt2;
if (bLeft)
vec2 = vec2.RotateBy(Math.PI / 3);
else
vec2 = vec2.RotateBy(5 * Math.PI / 3);
Point2d pt3 = pt2 + vec2;
pl.AddVertexAt(++vn, pt2, 0, sw, ew);
pl.AddVertexAt(++vn, pt3, 0, sw, ew);
pl.AddVertexAt(++vn, pt4, 0, sw, ew);
}
else if (st == SegmentType.Arc)
{
CircularArc3d ca = pl.GetArcSegmentAt(vn);
double oldBulge = pl.GetBulgeAt(vn);
// Build a standard arc and use that for the calcs
Arc arc = circArc2Arc(ca);
// Get the main 5 points
Point3d pt1 = arc.StartPoint,
pt5 = arc.EndPoint;
double ln = arc.GetDistAtPoint(pt5);
Point3d pt2 = arc.GetPointAtDist(ln / 3),
pt4 = arc.GetPointAtDist(2 * ln / 3);
Point3d mid = arc.GetPointAtDist(ln / 2);
CircularArc3d tmpArc = new CircularArc3d(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt4);
else
tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, pt4);
Point3d pt3 = tmpArc.StartPoint;
// Now add the new segments, setting the bulge
// for the existing one and the new ones to a third
// (as the segments are a third as big as the old one)
CoordinateSystem3d ecs = pl.Ecs.CoordinateSystem3d;
Plane pn = new Plane(ecs.Origin, pl.Normal);
double bu = oldBulge / 3;
pl.SetBulgeAt(vn, bu);
pl.AddVertexAt(++vn, pt2.Convert2d(pn), bu, sw, ew);
pl.AddVertexAt(++vn, pt3.Convert2d(pn), bu, sw, ew);
pl.AddVertexAt(++vn, pt4.Convert2d(pn), bu, sw, ew);
}
}
pl.DowngradeOpen();
}
}
}Here are the results of running the KA command on a drawing containing a single polyline with arc and line segments, choosing a recursion depth of 6:
二、长事务
August 06, 2007
A handy .NET class to help manage long operations in AutoCAD
This post was almost called "Generating Koch fractals in AutoCAD using .NET - Part 3", following on from Parts 1 & 2 of the series. But by the time I'd completed the code, I realised it to be of more general appeal and decided to provide it with a more representative title.
I started off by adding a progress meter and an escape key handler to the code in the last post. Then, while refactoring the code, I decided to encapsulate the functionality in a standalone class that could be dropped into pretty much any AutoCAD .NET project (although I've implemented it in C#, as usual).
So what we have is a new class called LongOperationManager, which does the following:
Displays and updates a progress meter (at the bottom left of AutoCAD's window)
Allowing you to set an arbitrary message and total number of operations
Listens for "escape" in case the user wants to interrupt the current operation
Here's the class implementation:
public class LongOperationManager :
IDisposable, System.Windows.Forms.IMessageFilter
{
// The message code corresponding to a keypress
const int WM_KEYDOWN = 0x0100;
// The number of times to update the progress meter
// (for some reason you need 600 to tick through
//for each percent)
const int progressMeterIncrements = 600;
// Internal members for metering progress
private ProgressMeter pm;
private long updateIncrement;
private long currentInc;
// External flag for checking cancelled status
public bool cancelled = false;
// Constructor
public LongOperationManager(string message)
{
System.Windows.Forms.Application.
AddMessageFilter(this);
pm = new ProgressMeter();
pm.Start(message);
pm.SetLimit(progressMeterIncrements);
currentInc = 0;
}
// System.IDisposable.Dispose
public void Dispose()
{
pm.Stop();
pm.Dispose();
System.Windows.Forms.Application.
RemoveMessageFilter(this);
}
// Set the total number of operations
public void SetTotalOperations(long totalOps)
{
// We really just care about when we need
// to update the timer
updateIncrement =
(totalOps > progressMeterIncrements ?
totalOps / progressMeterIncrements :
totalOps
);
}
// This function is called whenever an operation
// is performed
public bool Tick()
{
if (++currentInc == updateIncrement)
{
pm.MeterProgress();
currentInc = 0;
System.Windows.Forms.Application.DoEvents();
}
// Check whether the filter has set the flag
if (cancelled)
pm.Stop();
return !cancelled;
}
// The message filter callback
public bool PreFilterMessage(
ref System.Windows.Forms.Message m
)
{
if (m.Msg == WM_KEYDOWN)
{
// Check for the Escape keypress
System.Windows.Forms.Keys kc =
(System.Windows.Forms.Keys)(int)m.WParam &
System.Windows.Forms.Keys.KeyCode;
if (m.Msg == WM_KEYDOWN &&
kc == System.Windows.Forms.Keys.Escape)
{
cancelled = true;
}
// Return true to filter all keypresses
return true;
}
// Return false to let other messages through
return false;
}
}
In terms of how to use the class... first of all you create an instance of it, setting the string to be shown on the progress meter (just like AutoCAD's ProgressMeter class). As the LongOperationManager implements IDisposable, then at the end you should either call Dispose or manage it's scope with the using() statement.
I chose to separate the setting of the total number of operations to be completed from the object's construction, as in our example we need to an initial pass before we know how many objects we're working with (and we want to at least put the label on the progress meter while we perform that initial pass).
Then we just call the Tick() method whenever we perform an operation - this updates the progress meter and checks for use of the escape key. The idea is that you set the total number of operations and then call Tick() for each one of those individual operations - the class takes care of how often it needs to update the progress meter. If it finds escape has been used, the Tick() method will return false.
That's about it, aside from the fact you can also query the "cancelled" property to see whether escape has been used.
Here's the basic approach:
LongOperationManager lom =
new LongOperationManager("Fractalizing entities");
using (lom)
{
...
lom.SetTotalOperations(totalOps);
...
while (true)
{
...
if (!lom.Tick())
{
ed.WriteMessage("\nFractalization cancelled.\n");
break;
}
}
}Here's the code integrated into the previous exampleusing 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;
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;
// Create and add our long operation handler
LongOperationManager lom =
new LongOperationManager("Fractalizing entities");
using (lom)
{
// 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,
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
);
}
// The number of operations is...
//The number of complex entities multiplied
//by the recursion level (they each get
//"kochized" once per level,
//even if that's a long operation)
// plus
//(4^0 + 4^1 + 4^2 + 4^3... + 4^n) multiplied
//by the number of db-resident ents
// where n is the recursion level. Phew!
long totalOps =
modified.Count * recursionLevel +
operationCount(recursionLevel) * toErase.Count;
lom.SetTotalOperations(totalOps);
// 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)
{
if (!lom.Tick())
{
ed.WriteMessage(
"\nFractalization cancelled.\n"
);
break;
}
Entity ent =
(Entity)tr.GetObject(
objId,
OpenMode.ForRead
);
Kochize(
ent,
modified,
toErase,
newerEntities,
bLeft
);
}
// Kochize all the non-db resident entities
if (!lom.cancelled)
{
foreach (Entity ent in newEntities)
{
if (!lom.Tick())
{
ed.WriteMessage(
"\nFractalization cancelled.\n"
);
break;
}
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;
}
lom.Tick();
if (!lom.cancelled)
{
// Erase each of the replaced db-resident ents
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();
}
}
}
}
}
static long
operationCount(int nRecurse)
{
if (1 >= nRecurse)
return 1;
return
(long)Math.Pow(
newEntsPerOldEnt,
nRecurse - 1
)
+ operationCount(nRecurse - 1);
}
// 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;
}
Polyline pl = ent as Polyline;
if (pl != null)
{
Kochize(pl, 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;
Vector3d full = pt5 - pt1;
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, tmpArc.Normal, pt2);
else
tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt2);
Arc arc2 = circArc2Arc(tmpArc);
arc2.SetPropertiesFrom(arc);
arc2.Thickness = arc.Thickness;
toAdd.Add(arc2);
// Create the third arc
tmpArc.Set(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(5 * Math.PI / 3, tmpArc.Normal, pt4);
else
tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, 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)
{
Point3d center = circArc.Center;
Vector3d normal = circArc.Normal;
Vector3d refVec = circArc.ReferenceVector;
Plane plane = new Plane(center, normal);
double ang = refVec.AngleOnPlane(plane);
return new Arc(
center,
normal,
circArc.Radius,
circArc.StartAngle + ang,
circArc.EndAngle + ang
);
}
private void Kochize(
Polyline pl,
ObjectIdCollection modified,
ObjectIdCollection toErase,
List<Entity> toAdd,
bool bLeft
)
{
pl.UpgradeOpen();
if (pl.ObjectId != ObjectId.Null &&
!modified.Contains(pl.ObjectId))
{
modified.Add(pl.ObjectId);
}
for(int vn = 0; vn < pl.NumberOfVertices; vn++)
{
SegmentType st = pl.GetSegmentType(vn);
if (st != SegmentType.Line && st != SegmentType.Arc)
continue;
double sw = pl.GetStartWidthAt(vn),
ew = pl.GetEndWidthAt(vn);
if (st == SegmentType.Line)
{
if (vn + 1 == pl.NumberOfVertices)
continue;
LineSegment2d ls = pl.GetLineSegment2dAt(vn);
Point2d pt1 = ls.StartPoint,
pt5 = ls.EndPoint;
Vector2d vec = pt5 - pt1;
double d_3 = vec.Length / 3;
Point2d pt2 = pt1 + (vec.GetNormal() * d_3),
pt4 = pt1 + (vec.GetNormal() * 2 * d_3);
Vector2d vec2 = pt4 - pt2;
if (bLeft)
vec2 = vec2.RotateBy(Math.PI / 3);
else
vec2 = vec2.RotateBy(5 * Math.PI / 3);
Point2d pt3 = pt2 + vec2;
pl.AddVertexAt(++vn, pt2, 0, sw, ew);
pl.AddVertexAt(++vn, pt3, 0, sw, ew);
pl.AddVertexAt(++vn, pt4, 0, sw, ew);
}
else if (st == SegmentType.Arc)
{
CircularArc3d ca = pl.GetArcSegmentAt(vn);
double oldBulge = pl.GetBulgeAt(vn);
// Build a standard arc and use that for the calcs
Arc arc = circArc2Arc(ca);
// Get the main 5 points
Point3d pt1 = arc.StartPoint,
pt5 = arc.EndPoint;
double ln = arc.GetDistAtPoint(pt5);
Point3d pt2 = arc.GetPointAtDist(ln / 3),
pt4 = arc.GetPointAtDist(2 * ln / 3);
Point3d mid = arc.GetPointAtDist(ln / 2);
CircularArc3d tmpArc =
new CircularArc3d(pt2, mid, pt4);
if (bLeft)
tmpArc.RotateBy(5*Math.PI/3, tmpArc.Normal, pt4);
else
tmpArc.RotateBy(Math.PI / 3, tmpArc.Normal, pt4);
Point3d pt3 = tmpArc.StartPoint;
// Now add the new segments, setting the bulge
// for the existing one and the new ones to a third
// (as the segs are a third as big as the old one)
CoordinateSystem3d ecs = pl.Ecs.CoordinateSystem3d;
Plane pn = new Plane(ecs.Origin, pl.Normal);
double bu = oldBulge / 3;
pl.SetBulgeAt(vn, bu);
pl.AddVertexAt(
++vn, pt2.Convert2d(pn), bu, sw, ew);
pl.AddVertexAt(
++vn, pt3.Convert2d(pn), bu, sw, ew);
pl.AddVertexAt(
++vn, pt4.Convert2d(pn), bu, sw, ew);
}
}
pl.DowngradeOpen();
}
public class LongOperationManager :
IDisposable, System.Windows.Forms.IMessageFilter
{
// The message code corresponding to a keypress
const int WM_KEYDOWN = 0x0100;
// The number of times to update the progress meter
// (for some reason you need 600 to tick through
//for each percent)
const int progressMeterIncrements = 600;
// Internal members for metering progress
private ProgressMeter pm;
private long updateIncrement;
private long currentInc;
// External flag for checking cancelled status
public bool cancelled = false;
// Constructor
public LongOperationManager(string message)
{
System.Windows.Forms.Application.
AddMessageFilter(this);
pm = new ProgressMeter();
pm.Start(message);
pm.SetLimit(progressMeterIncrements);
currentInc = 0;
}
// System.IDisposable.Dispose
public void Dispose()
{
pm.Stop();
pm.Dispose();
System.Windows.Forms.Application.
RemoveMessageFilter(this);
}
// Set the total number of operations
public void SetTotalOperations(long totalOps)
{
// We really just care about when we need
// to update the timer
updateIncrement =
(totalOps > progressMeterIncrements ?
totalOps / progressMeterIncrements :
totalOps
);
}
// This function is called whenever an operation
// is performed
public bool Tick()
{
if (++currentInc == updateIncrement)
{
pm.MeterProgress();
currentInc = 0;
System.Windows.Forms.Application.DoEvents();
}
// Check whether the filter has set the flag
if (cancelled)
pm.Stop();
return !cancelled;
}
// The message filter callback
public bool PreFilterMessage(
ref System.Windows.Forms.Message m
)
{
if (m.Msg == WM_KEYDOWN)
{
// Check for the Escape keypress
System.Windows.Forms.Keys kc =
(System.Windows.Forms.Keys)(int)m.WParam &
System.Windows.Forms.Keys.KeyCode;
if (m.Msg == WM_KEYDOWN &&
kc == System.Windows.Forms.Keys.Escape)
{
cancelled = true;
}
// Return true to filter all keypresses
return true;
}
// Return false to let other messages through
return false;
}
}
}
}
三、
June 18, 2008
A simple turtle graphics implementation in AutoCAD using .NET
Like many thirty-something Brits (and possible non-Brits, for all I know) my earliest introduction to the world of graphics programming was via a language calledLogo running on theBBC Microcomputer.
This machine and its educational software were commonplace in UK schools in the 1980s, and I have clear memories of staying late at primary school (which I suppose means I was somewhere between 8 and 10) to fool around withBBC BASIC. With a friend I used to try to write text-based adventures ("you are in a cave, there are exits North, South, East and West" - you know the kind of thing) which we padded with loads and loads of comments to stop them from loading embarrassingly quickly from tape. Oh, the memories. :-)
Anyway, I digress. Logo provided a really great introduction to vector graphics: even as we were being taught trigonometry we were exposed to an environment that allowed us to translate these concepts into graphics on a computer screen. Too cool.
Thinking back about all this, I decided to implement a simpleturtle graphics engine inside AutoCAD, to allow eventual integration of a Logo-like language into it. The idea was this: to implement an engine exposing a series of methods (Move(), Turn(), PenUp(), PenDown(), SetPenColor() and SetPenWidth(), to start with) that could be used to implement a subset of the Logo language. I decided to write the engine in C#, whose object-orientation makes it well-suited to this type of task, and then look into using F# for implementing the language to drive it in a future post.
F# is very good for implementing new or existing programming languages: the whole area ofDomain Specific Languages (DSLs) is a major selling point for functional languages, generally. They're very well-suited to the tasks that make up the interpretation/compilation process:lexical analysis,syntactic analysis, etc., as much of the hard work is around tokenizing strings, processing lists and mapping behaviours (and these activities are most functional programming languages' "bread and butter").
Only after deciding this approach did I stumble across an existing Logo implementation in F#, so it appears that integrating the Logo language may prove simpler than I expected.
A little more on the implementation of the turtle graphics engine: I decided to implement a 2D system, to start with, generating geometry as a series of Polyline objects. Every time the PenUp() function is called we tell the system to create a new object. We also do this for SetPenColor() in the cases where the colour is changed, as Polylines do not support per-segment colours (if we were to implement a custom object that did so, we could change the implementation to keep contiguous, multi-coloured segments in a single object). The current implementation reopens the previous object each time, and therefore creates a new transaction for every single Move() operation. This is clearly sub-optimal, but I quite like the old-school effect of watching the graphics get revealed segment by segment. :-) A more optimal technique would be to keep the object open while the pen is down and the engine is in use, and this is currently left as an exercise for the reader (or until I hit a use case where I can no longer be bothered to wait for the execution to complete :-).
It would be quite simple to take this engine into 3D, and I may well do so at some point in the future. There seem to be a number of successful 3D Logo implementations out there, and they really have great potential when generating organic models, as you can see from this page. Languages such as Logo can be applied very effectively to the task of generating fractal geometry by interpreting (and implementing)Lindenmayer (or L-) systems, for instance.
OK, enough background information... 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 Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine
{
// Private members
private ObjectId m_currentObject;
private Pen m_pen;
private Point3d m_position;
private Vector3d m_direction;
// Public properties
public Point3d Position
{
get { return m_position; }
set { m_position = value; }
}
public Vector3d Direction
{
get { return m_direction; }
set { m_direction = value; }
}
// Constructor
public TurtleEngine()
{
m_pen = new Pen();
m_currentObject = ObjectId.Null;
m_position = Point3d.Origin;
m_direction = new Vector3d(1.0, 0.0, 0.0);
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
Vector3d.ZAxis,
Position
);
Direction =
Direction.TransformBy(mat);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = Position;
Position += Direction * distance;
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, Position);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the next
// use of the pen
m_currentObject =
ObjectId.Null;
}
public void SetPenWidth(double width)
{
m_pen.Width = width;
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
m_currentObject =
ObjectId.Null;
m_pen.Color = col;
}
}
// Internal helper to generate geometry
// (this could be optimised to keep the
// object we're generating open, rather
// than having to reopen it each time)
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
Transaction tr =
tm.StartTransaction();
using (tr)
{
Polyline pl;
Plane plane;
// Create the current object, if there is none
if (m_currentObject == ObjectId.Null)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)tr.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
pl = new Polyline();
pl.Color = m_pen.Color;
// Define its plane
plane = new Plane(
pl.Ecs.CoordinateSystem3d.Origin,
pl.Ecs.CoordinateSystem3d.Zaxis
);
// Add the first vertex
pl.AddVertexAt(
0, oldPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Add the polyline to the database
m_currentObject =
ms.AppendEntity(pl);
tr.AddNewlyCreatedDBObject(pl, true);
}
else
{
// Get the current object, if there is one
pl =
(Polyline)tr.GetObject(
m_currentObject,
OpenMode.ForWrite
);
// Calculate its plane
plane = new Plane(
pl.Ecs.CoordinateSystem3d.Origin,
pl.Ecs.CoordinateSystem3d.Zaxis
);
}
// Now we have our current object open,
// add the new vertex
pl.AddVertexAt(
pl.NumberOfVertices,
newPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Display the graphics, to avoid long,
// black-box operations
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
tr.Commit();
}
}
}
public class Commands
{
// A command to create some simple geometry
static public void DrawTurtleGraphics()
{
TurtleEngine te = new TurtleEngine();
// Draw a red circle
te.SetPenColor(1);
te.SetPenWidth(7);
te.PenDown();
for (int i = 0; i < 360; i++)
{
te.Move(2);
te.Turn(Math.PI / 180);
}
// Move to the next space
te.PenUp();
te.Move(200);
// Draw a green square
te.SetPenColor(3);
te.SetPenWidth(5);
te.PenDown();
for (int i = 0; i < 4; i++)
{
te.Move(230);
te.Turn(Math.PI / 2);
}
// Move to the next space
te.PenUp();
te.Move(300);
// Draw a blue triangle
te.SetPenColor(5);
te.SetPenWidth(3);
te.PenDown();
for (int i = 0; i < 3; i++)
{
te.Move(266);
te.Turn(2 * Math.PI / 3);
}
// Move to the next space
te.PenUp();
te.Move(400);
te.Turn(Math.PI / 2);
te.Move(115);
te.Turn(Math.PI / -2);
// Draw a multi-colored, spirograph-like shape
te.SetPenWidth(1);
te.PenDown();
for (int i = 0; i < 36; i++)
{
te.Turn(Math.PI / 18);
te.SetPenColor(i);
for (int j = 0; j < 360; j++)
{
te.Move(1);
te.Turn(Math.PI / 180);
}
}
}
}
}
Here are the results of running the DTG command, which simply calls into the TurtleEngine to test out its capabilities, creating a series of shapes of different pen colours and widths:
The first three objects are single objects, but are multi-segment polylines (don't expect the engine to generate circles, for instance: most turtle graphics code to generate circles actually create objects with 360 segments). The fourth object is a series of 36 circles: as mentioned earlier, Polyline objects do not support per-segment colours, but if this was all a uniform colour (by commenting out the call to te.SetPenColor(i)), it would all be a single Polyline with 36 * 360 segments.
四、Turtle 分形
June 23, 2008
Turtle fractals in AutoCAD using .NET - Part 1
This topic started in this recent post, where I introduced a simple turtle graphics engine for AutoCAD implemented in C#. My eventual aim is still to implement a subset of the Logo programming language in F#, calling through to this C# engine, but for now I've been side-tracked: I'm simply having too much fun with the engine itself and exploring the possibilities of turtle graphics inside AutoCAD - especially around the use of recursive algorithms to generate fractals.
This post and the next (and maybe more, depending on how much fun I continue to have) will be devoted to recursive algorithms for fractal generation with a cursor-based, turtle graphics system: in this post we'll start with performance-related improvements to the engine alluded to in the last post, and move on to code to generate precise fractals. The next post will take this further by looking at adding randomness to fractals, allowing us to create more organic forms.
So let's start with the engine enhancements: I mentioned last time that the previous implementation was sub-optimal, as it used a new transaction for each segment created. I did some benchmarking, to see what improvement gains were to be had by changing the implementation, and - interestingly - there was very little change when I modified the engine to maintain a single transaction when testing with the DTG command in the previous post. It turns out that the bottleneck for that particular scenario was the graphics update: refreshing the graphics after each segment drawn is extremely time-consuming, so the overhead of using separate transactions per segment didn't really impact performance. As we move on to much larger models - as we will in this post - it makes sense to perform two optimizations to the engine:
Turn off display of graphics during update
We will not flush any graphics and simply let AutoCAD take care of updating the display when we're done
A single transaction per execution
We will maintain references to a Transaction and a Polyline within our engine, instead of an ObjectId
Implementing this is very simple, as you can see below, and the client code only really needs to make one change: it needs to start its own transaction and pass that into the constructor of the TurtleEngine. This will be used, as needed, and must, of course, be committed and disposed of by the client at the end (we'll use the using statement to take care of the disposal).
Once the graphics flushing has been disabled (again, with a trivial change: I've added a Boolean variable - with a default value of false - to check before updating the graphics), we really start to see the relevance of maintaining a separate transaction, as our previous performance bottleneck has been removed. More on this later.
Onto the fractal algorithm we're implementing today, to put the engine through its paces: some of you may remember this previous pair of posts, where we looked at an implementation for generating Koch curves inside AutoCAD. In this case we're going to generate something called theKoch snowflake, which is basically a triangle with each segment divided a series of times:
The formula for calculating the number of sides is: 3 * 4^l, where l is the recursion level (at least from our perspective, a triangle is level 0, while the right-most shape above is level 5). I have kept two implementations of the engine in my project, for comparison: when using the old implementation, a level 7 Koch snowflake - which for us means a Polyline of 49,152 segments - takes 5 minutes to generate. With the new approach of maintaining a single transaction, this reduces to under 2 seconds. Which is clearly a significant change.
A level 8 snowflake took just 7 seconds for my system to generate with the new version of the engine. I limited the level selection to 8, not because it would take too long to generate, but because unless you disable rollover highlighting AutoCAD is likely to grind to a halt for a number of seconds when you hover over an object of this complexity: a level 8 snowflake does have 196,608 segments, after all.
Here's the C# code with the modified engine and the algorithm for creating Koch snowflakes:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine
{
// Private members
private Transaction m_trans;
private Polyline m_poly;
private Pen m_pen;
private Point3d m_position;
private Vector3d m_direction;
private bool m_updateGraphics;
// Public properties
public Point3d Position
{
get { return m_position; }
set { m_position = value; }
}
public Vector3d Direction
{
get { return m_direction; }
set { m_direction = value; }
}
// Constructor
public TurtleEngine(Transaction tr)
{
m_pen = new Pen();
m_trans = tr;
m_poly = null;
m_position = Point3d.Origin;
m_direction = new Vector3d(1.0, 0.0, 0.0);
m_updateGraphics = false;
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
Vector3d.ZAxis,
Position
);
Direction =
Direction.TransformBy(mat);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = Position;
Position += Direction * distance;
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, Position);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the
// next use of the pen
m_poly = null;
}
public void SetPenWidth(double width)
{
m_pen.Width = width;
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
m_poly = null;
m_pen.Color = col;
}
}
// Internal helper to generate geometry
// (this could be optimised to keep the
// object we're generating open, rather
// than having to reopen it each time)
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
Plane plane;
// Create the current object, if there is none
if (m_poly == null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
m_poly = new Polyline();
m_poly.Color = m_pen.Color;
// Define its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
// Add the first vertex
m_poly.AddVertexAt(
0, oldPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Add the polyline to the database
ms.AppendEntity(m_poly);
m_trans.AddNewlyCreatedDBObject(m_poly, true);
}
else
{
// Calculate its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
}
// Now we have our current object open,
// add the new vertex
m_poly.AddVertexAt(
m_poly.NumberOfVertices,
newPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Display the graphics, to avoid long,
// black-box operations
if (m_updateGraphics)
{
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
}
}
}
public class Commands
{
static void KochIsland(
TurtleEngine te,
double size,
int level
)
{
for (int i = 0; i < 3; i++)
{
// Draw a side
KSide(te, size, level);
// Turn 120 degrees to the left
te.Turn(2 * Math.PI / 3);
}
}
static void KSide(
TurtleEngine te,
double size,
int level
)
{
// When at level 0, draw the side and exit
if (level == 0)
te.Move(size);
else
{
// Else recurse for each segment of the side
KSide(te, size / 3, level - 1);
te.Turn(Math.PI / -3);
KSide(te, size / 3, level - 1);
te.Turn(2 * Math.PI / 3);
KSide(te, size / 3, level - 1);
te.Turn(Math.PI / -3);
KSide(te, size / 3, level - 1);
}
}
static public void KochSnowFlake()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
// Ask for the recursion level
int level = 6;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter recursion level <6>: "
);
pio.AllowNone = true;
pio.LowerLimit = 0;
pio.UpperLimit = 8;
PromptIntegerResult pir =
ed.GetInteger(pio);
if (pir.Status != PromptStatus.OK &&
pir.Status != PromptStatus.None)
return;
if (pir.Status == PromptStatus.OK)
level = pir.Value;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a Koch snowflake
te.SetPenColor(0);
te.SetPenWidth(0);
te.PenDown();
// 100 is an arbitrary value:
// you could also prompt the user for this
KochIsland(te, 100, level);
tr.Commit();
}
}
}
}
Here are the results of the KSF command, when used to generate a level 6 snowflake. Although frankly you wouldn't see a difference between a level 5 and a level 10 snowflake at this resolution. :-)
Next time we'll look at some recursive code to generate less precise, more natural forms.
June 25, 2008
Turtle fractals in AutoCAD using .NET - Part 2
This series start with this initial post, where we looked at an implementation of a simple turtle graphics engine inside AutoCAD, and followed on with this previous post, where we refined the engine and looked at how we could use it to generate complex fractals with relatively little code.
In this post we take a further look at fractal generation using the turtle graphic engine, with the particular focus on introducing randomness to generate more realistic, organic forms. On a side note, fractals and the use of randomness in design are two of my favourite topics, so this post is hitting a sweet spot, for me. :-)
So where to start when generating organic forms? The simplest, "classic" example, in my view, is to generate trees. Trees lend themselves to automatic generation, as - in 2D, at least - they are a sequence of simple 2-way forks (at least the way I draw them, they are :-).
I picked up some simple Logo code from this site (yes, it does indeed say "fractals for children" in the title :-)
if :distance < 5
forward :distance
right 30
tree :distance-10
left 60
tree :distance-10
right 30
back :distance
This is easy to turn into C# code harnessing our TurtleEngine, with the addition of a proportional trunk/branch width (we take the width as a tenth of the length). See the Tree() function in the code listing below. The results of this procedure (which you call via the FT command) are interesting enough, if a little perfect:
You will notice that the tree is created as a single Polyline, which is a result of us back-tracking over previous segments with the pen down, rather than up. You can see this from this image showing the tree selected:
The FT command will create the same results every single time (assuming you specify the same tree length), which may or may not be what you're after.
So let's go and add some randomness, to make life a little more interesting. This modified function in the below code, named RandomTree(), generates a separate random factor to apply to the trunk/branch length (and therefore the width, as this is proportional to the length), and to the angle of each of the two branches sprouting from the current trunk or branch. The "variability" is specified by the user for all our random factors, but we could go further and tweak it for the length and for each of the angles.
Here's the C# code, including command definitions and the TurtleEngine we refined in the last post:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine
{
// Private members
private Transaction m_trans;
private Polyline m_poly;
private Pen m_pen;
private Point3d m_position;
private Vector3d m_direction;
private bool m_updateGraphics;
// Public properties
public Point3d Position
{
get { return m_position; }
set { m_position = value; }
}
public Vector3d Direction
{
get { return m_direction; }
set { m_direction = value; }
}
// Constructor
public TurtleEngine(Transaction tr)
{
m_pen = new Pen();
m_trans = tr;
m_poly = null;
m_position = Point3d.Origin;
m_direction = new Vector3d(1.0, 0.0, 0.0);
m_updateGraphics = false;
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
Vector3d.ZAxis,
Position
);
Direction =
Direction.TransformBy(mat);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = Position;
Position += Direction * distance;
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, Position);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the next
// use of the pen
m_poly = null;
}
public void SetPenWidth(double width)
{
m_pen.Width = width;
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
m_poly = null;
m_pen.Color = col;
}
}
// Internal helper to generate geometry
// (this could be optimised to keep the
// object we're generating open, rather
// than having to reopen it each time)
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
Plane plane;
// Create the current object, if there is none
if (m_poly == null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
m_poly = new Polyline();
m_poly.Color = m_pen.Color;
// Define its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
// Add the first vertex
m_poly.AddVertexAt(
0, oldPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Add the polyline to the database
ms.AppendEntity(m_poly);
m_trans.AddNewlyCreatedDBObject(m_poly, true);
}
else
{
// Calculate its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
}
// Make sure the previous vertex has its
// width set appropriately
if (m_pen.Width > 0.0)
{
m_poly.SetStartWidthAt(
m_poly.NumberOfVertices - 1,
m_pen.Width
);
m_poly.SetEndWidthAt(
m_poly.NumberOfVertices - 1,
m_pen.Width
);
}
// Add the new vertex
m_poly.AddVertexAt(
m_poly.NumberOfVertices,
newPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Display the graphics, to avoid long,
// black-box operations
if (m_updateGraphics)
{
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
}
}
}
public class Commands
{
static public bool GetTreeInfo(
out Point3d position,
out double treeLength
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
treeLength = 0;
position = Point3d.Origin;
PromptPointOptions ppo =
new PromptPointOptions(
"\nSelect base point of tree: "
);
PromptPointResult ppr =
ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return false;
position = ppr.Value;
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter tree length <70>: "
);
pdo.AllowNone = true;
PromptDoubleResult pdr =
ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.None &&
pdr.Status != PromptStatus.OK)
return false;
if (pdr.Status == PromptStatus.OK)
treeLength = pdr.Value;
else
treeLength = 70;
return true;
}
static void Tree(
TurtleEngine te,
double distance
)
{
if (distance < 5.0)
return;
// Width of the trunk/branch is a tenth of
// of the length
te.SetPenWidth(distance / 10);
// Draw the main trunk/branch
te.Move(distance);
// Draw the left-hand sub-tree
te.Turn(Math.PI / 6);
Tree(te, distance - 10);
// Draw the right-hand sub-tree
te.Turn(Math.PI / -3);
Tree(te, distance - 10);
// Turn back to the original angle
te.Turn(Math.PI / 6);
// Draw back down to the start of this sub-
// tree, with the same thickness, as this
// may have changed in deeper sub-trees
te.SetPenWidth(distance / 10);
te.Move(-distance);
}
static void RandomTree(
TurtleEngine te,
double distance,
int variability
)
{
if (distance < 5.0)
return;
// Generate 3 random factors, each on the same basis:
//a base amount = 100 - half the variability
//+ a random amount from 0 to the variability
// So a variability of 20 results in 90 to 110 (0.9-1.1)
Random rnd = new Random();
int basic = 100 - (variability / 2);
int num = rnd.Next(variability);
double factor = (basic + num) / 100.0;
num = rnd.Next(variability);
double factor1 = (basic + num) / 100.0;
num = rnd.Next(variability);
double factor2 = (basic + num) / 100.0;
// Multiple out the various items by the factors
double distance1 = factor * distance;
double angle1 = factor1 * Math.PI / 6;
double angle2 = factor2 * Math.PI / -3;
// The last angle is the total angle
double angle3 = angle1 + angle2;
// Width of the trunk/branch is a tenth of
// of the length
te.SetPenWidth(distance1 / 10);
// Draw the main trunk/branch
te.Move(distance1);
// Draw the left-hand sub-tree
te.Turn(angle1);
RandomTree(te, distance - 10, variability);
// Draw the right-hand sub-tree
te.Turn(angle2);
RandomTree(te, distance - 10, variability);
// Turn back to the original angle
te.Turn(-angle3);
// Draw back down to the start of this sub-
// tree, with the same thickness, as this
// may have changed in deeper sub-trees
te.SetPenWidth(distance1 / 10);
te.Move(-distance1);
}
static public void FractalTree()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
double treeLength;
Point3d position;
if (!GetTreeInfo(out position, out treeLength))
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a fractal tree
te.Position = position;
te.SetPenColor(0);
te.SetPenWidth(0);
te.Turn(Math.PI / 2);
te.PenDown();
Tree(te, treeLength);
tr.Commit();
}
}
static public void RandomFractalTree()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
double treeLength;
Point3d position;
if (!GetTreeInfo(out position, out treeLength))
return;
int variability = 20;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter variability percentage <20>: "
);
pio.AllowNone = true;
PromptIntegerResult pir =
ed.GetInteger(pio);
if (pir.Status != PromptStatus.None &&
pir.Status != PromptStatus.OK)
return;
if (pir.Status == PromptStatus.OK)
variability = pir.Value;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a random fractal tree
te.Position = position;
te.SetPenColor(0);
te.SetPenWidth(0);
te.Turn(Math.PI / 2);
te.PenDown();
RandomTree(te, treeLength, variability);
tr.Commit();
}
}
}
}
This is the first time the turtle engine has been used to apply widths to segments, so I did make a very minor change in the GenerateSegment() function: we need to apply the current pen width to the previous Polyline vertex, and not just the one we're adding. A minor change, but one that makes the engine behave in a more expected way.
When we run the RFT command, we can see a variety of trees get created - here's a quick sample:
These were created with the standard options (tree length of 70, variability of 20), but with different choices here you can get quite different results.
I hope this demonstrates the interesting capabilities turtle graphics bring to the area of modeling organic models via recursive fractals + randomness: while this was deliberately quite a simple example, this type of approach could be used/extended to generate other, more elaborate types of "natural" design in two and three dimensions.
June 27, 2008
Turtle fractals in AutoCAD using .NET - Part 3
In the introductory post we first looked at a simple turtle graphics engine for AutoCAD, which was followed up by this series looking at using it to generate fractals (here are parts 1 & 2).
This post continues the organic fractal theme, by looking at another fractal found in nature, the humble fern. I found some simple Logo code ina presentation on the web:
to fern :size
if :size < 4
fd :size / 25
lt 90 fern :size * .3
rt 90
rt 90 fern :size * .3
lt 90 fern :size * .85
bk :size / 25
endThis - when translated to use our turtle engine inside AutoCAD - creates a somewhat straight, unnatural-looking fern:
To make things a little more interesting, I generalised out some of the parameters to allow easy tweaking within the code. Here's the C# code including the complete TurtleEngine implementation, as before:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine
{
// Private members
private Transaction m_trans;
private Polyline m_poly;
private Pen m_pen;
private Point3d m_position;
private Vector3d m_direction;
private bool m_updateGraphics;
// Public properties
public Point3d Position
{
get { return m_position; }
set { m_position = value; }
}
public Vector3d Direction
{
get { return m_direction; }
set { m_direction = value; }
}
// Constructor
public TurtleEngine(Transaction tr)
{
m_pen = new Pen();
m_trans = tr;
m_poly = null;
m_position = Point3d.Origin;
m_direction = new Vector3d(1.0, 0.0, 0.0);
m_updateGraphics = false;
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
Vector3d.ZAxis,
Position
);
Direction =
Direction.TransformBy(mat);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = Position;
Position += Direction * distance;
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, Position);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the next
// use of the pen
m_poly = null;
}
public void SetPenWidth(double width)
{
m_pen.Width = width;
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
m_poly = null;
m_pen.Color = col;
}
}
// Internal helper to generate geometry
// (this could be optimised to keep the
// object we're generating open, rather
// than having to reopen it each time)
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
Plane plane;
// Create the current object, if there is none
if (m_poly == null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
m_poly = new Polyline();
m_poly.Color = m_pen.Color;
// Define its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
// Add the first vertex
m_poly.AddVertexAt(
0, oldPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Add the polyline to the database
ms.AppendEntity(m_poly);
m_trans.AddNewlyCreatedDBObject(m_poly, true);
}
else
{
// Calculate its plane
plane = new Plane(
m_poly.Ecs.CoordinateSystem3d.Origin,
m_poly.Ecs.CoordinateSystem3d.Zaxis
);
}
// Make sure the previous vertex has its
// width set appropriately
if (m_pen.Width > 0.0)
{
m_poly.SetStartWidthAt(
m_poly.NumberOfVertices - 1,
m_pen.Width
);
m_poly.SetEndWidthAt(
m_poly.NumberOfVertices - 1,
m_pen.Width
);
}
// Add the new vertex
m_poly.AddVertexAt(
m_poly.NumberOfVertices,
newPos.Convert2d(plane),
0.0, m_pen.Width, m_pen.Width
);
// Display the graphics, to avoid long,
// black-box operations
if (m_updateGraphics)
{
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
}
}
}
public class Commands
{
static void Fern(
TurtleEngine te,
double distance
)
{
const double minDist = 0.3;
const double widthFactor = 0.1;
const double stemFactor = 0.04;
const double restFactor = 0.85;
const double branchFactor = 0.3;
const int stemSegs = 5;
const int stemSegAngle = 1;
if (distance < minDist)
return;
// Width of the trunk/branch is a fraction
// of the length
te.SetPenWidth(
distance * stemFactor * widthFactor
);
// Draw the stem
for (int i = 0; i < stemSegs; i++)
{
te.Move(distance * stemFactor / stemSegs);
if (i < stemSegs - 1)
te.Turn(-stemSegAngle * Math.PI / 180);
}
// Draw the left-hand sub-fern
te.Turn(Math.PI / 2);
Fern(te, distance * branchFactor);
// Draw the right-hand sub-fern
te.Turn(-Math.PI);
Fern(te, distance * branchFactor);
// Draw the rest of the fern to the front
te.Turn(Math.PI / 2);
Fern(te, distance * restFactor);
// Draw back down to the start of this sub-
// fern, with the same thickness, as this
// may have changed in deeper sub-ferns
te.SetPenWidth(
distance * stemFactor * widthFactor
);
for (int i = 0; i < stemSegs; i++)
{
te.Move(-distance * stemFactor / stemSegs);
if (i < stemSegs - 1)
te.Turn(stemSegAngle * Math.PI / 180);
}
}
static public bool GetFernInfo(
out Point3d position,
out double treeLength
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
treeLength = 0;
position = Point3d.Origin;
PromptPointOptions ppo =
new PromptPointOptions(
"\nSelect base point of fern: "
);
PromptPointResult ppr =
ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return false;
position = ppr.Value;
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter fern length <100>: "
);
pdo.AllowNone = true;
PromptDoubleResult pdr =
ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.None &&
pdr.Status != PromptStatus.OK)
return false;
if (pdr.Status == PromptStatus.OK)
treeLength = pdr.Value;
else
treeLength = 100;
return true;
}
static public void FractalFern()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
double fernLength;
Point3d position;
if (!GetFernInfo(out position, out fernLength))
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a fractal fern
te.Position = position;
te.SetPenColor(92);
te.SetPenWidth(0);
te.Turn(Math.PI / 2);
te.PenDown();
Fern(te, fernLength);
tr.Commit();
}
}
}
}
When we run the FF command, selecting a location and the default tree length, we see a more natural (although in no way random) form:
Tweaking the stemSegAngle constant to be -2 instead of 1 gives a further differentiated result:
Incidentally, to get the original, "straight" fern, simply change the stemSegs constant to 1. The curved ferns will each take n times as much space in memory/on disk as the straight ones (where n is the value of stemSegs). This is because we're not storing actual curves, but using multiple straight line segments.
Any of the constants in the Fern() function could be presented for the user to enter, of course (i.e. populated by the GetFernInfo() function and passed as parameters into the Fern() function).
<p>July 15, 2008<br/>Turtle fractals in AutoCAD using .NET - Part 4<br/>I just couldn't resist coming back to this fun (at least for me :-) series... for reference here are parts 1, 2 and 3, while the series really started here.</p><p>There are two main places I wanted to take this implementation: firstly it really needed to be made 3D, which is the focus of this post, and I still then want to take it further by implementing a turtle graphics-oriented language (one could probably call it a Domain Specific Language, the domain being turtle graphics), which is likely to be a subset or variant of Logo. This is likely to be done using F#, but we'll see when I get around to it... :-)</p><p>Firstly, some concepts: with a 2D turtle graphics system we only really need two operations, Move and Turn. As we update our implementation to cope with that pesky third dimension we need to extend the set of operations to include Pitch and Roll. So our "turtle" needs to have more than just a position and a direction, it needs its own positioning matrix (essentially its own coordinate system). Think of the turtle as having its own little UCS icon travelling around with it (the direction being the X axis): we can then implement Turn as a rotation around its current Z axis, Pitch as a rotation around its Y axis and Roll as a rotation around its X axis. Each of these operations will, of course, update the coordinate system so that it's pointing somewhere different.</p><p>The below implementation maintains the Direction property, but it's now read-only: the underlying implementation is now via a CoordinateSystem3d object member variable (m_ecs). Each of the Move, Turn, Pitch and Roll operations adjusts the coordinate system, as does setting the Position property.</p><p>As for the geometry that we create via the turtle's movements: Polyline objects are inherently 2D, so the new implementation makes use of Polyline3d objects instead. Polyline3d objects contain PolylineVertex3d objects, and have slightly different requirements about database residency as we're adding our vertices, but the fundamental approach is not really any different. A couple of points to note... as pen thickness doesn't really make sense in 3D (at least for Polyline3d objects - we could, of course, get clever with extruding profiles along our paths, if we really wanted to), I've decided to ignore it, for now. The same is true of pen colour. These are both left as enhancements for the future.</p><p>Here's the updated C# code:</p><p>using Autodesk.AutoCAD.ApplicationServices;</p><p>using Autodesk.AutoCAD.DatabaseServices;</p><p>using Autodesk.AutoCAD.EditorInput;</p><p>using Autodesk.AutoCAD.Runtime;</p><p>using Autodesk.AutoCAD.Geometry;</p><p>using Autodesk.AutoCAD.Colors;</p><p>using System;</p><p></p><p>namespace TurtleGraphics</p><p>{</p><p> // This class encapsulates pen</p><p> // information and will be</p><p> // used by our TurtleEngine</p><p></p><p> class Pen</p><p> {</p><p> // Private members</p><p></p><p> private Color m_color;</p><p> private double m_width;</p><p> private bool m_down;</p><p></p><p> // Public properties</p><p></p><p> public Color Color</p><p> {</p><p> get { return m_color; }</p><p> set { m_color = value; }</p><p> }</p><p></p><p> public double Width</p><p> {</p><p> get { return m_width; }</p><p> set { m_width = value; }</p><p> }</p><p></p><p> public bool Down</p><p> {</p><p> get { return m_down; }</p><p> set { m_down = value; }</p><p> }</p><p></p><p> // Constructor</p><p></p><p> public Pen()</p><p> {</p><p> m_color =</p><p> Color.FromColorIndex(ColorMethod.ByAci, 0);</p><p> m_width = 0.0;</p><p> m_down = false;</p><p> }</p><p> }</p><p></p><p> // The main Turtle Graphics engine</p><p></p><p> class TurtleEngine</p><p> {</p><p> // Private members</p><p></p><p> private Transaction m_trans;</p><p> private Polyline3d m_poly;</p><p> private Pen m_pen;</p><p> private CoordinateSystem3d m_ecs;</p><p> private bool m_updateGraphics;</p><p></p><p> // Public properties</p><p></p><p> public Point3d Position</p><p> {</p><p> get { return m_ecs.Origin; }</p><p> set {</p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> value,</p><p> m_ecs.Xaxis,</p><p> m_ecs.Yaxis</p><p> );</p><p> }</p><p> }</p><p></p><p> public Vector3d Direction</p><p> {</p><p> get { return m_ecs.Xaxis; }</p><p> }</p><p></p><p> // Constructor</p><p></p><p> public TurtleEngine(Transaction tr)</p><p> {</p><p> m_pen = new Pen();</p><p> m_trans = tr;</p><p> m_poly = null;</p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> Point3d.Origin,</p><p> Vector3d.XAxis,</p><p> Vector3d.YAxis</p><p> );</p><p> m_updateGraphics = false;</p><p> }</p><p></p><p> // Public methods</p><p></p><p> public void Turn(double angle)</p><p> {</p><p> // Rotate our direction by the</p><p> // specified angle</p><p></p><p> Matrix3d mat =</p><p> Matrix3d.Rotation(</p><p> angle,</p><p> m_ecs.Zaxis,</p><p> Position</p><p> );</p><p></p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> m_ecs.Origin,</p><p> m_ecs.Xaxis.TransformBy(mat),</p><p> m_ecs.Yaxis.TransformBy(mat)</p><p> );</p><p> }</p><p></p><p> public void Pitch(double angle)</p><p> {</p><p> // Pitch in our direction by the</p><p> // specified angle</p><p></p><p> Matrix3d mat =</p><p> Matrix3d.Rotation(</p><p> angle,</p><p> m_ecs.Yaxis,</p><p> m_ecs.Origin</p><p> );</p><p></p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> m_ecs.Origin,</p><p> m_ecs.Xaxis.TransformBy(mat),</p><p> m_ecs.Yaxis</p><p> );</p><p> }</p><p></p><p> public void Roll(double angle)</p><p> {</p><p> // Roll along our direction by the</p><p> // specified angle</p><p></p><p> Matrix3d mat =</p><p> Matrix3d.Rotation(</p><p> angle,</p><p> m_ecs.Xaxis,</p><p> m_ecs.Origin</p><p> );</p><p></p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> m_ecs.Origin,</p><p> m_ecs.Xaxis,</p><p> m_ecs.Yaxis.TransformBy(mat)</p><p> );</p><p> }</p><p></p><p> public void Move(double distance)</p><p> {</p><p> // Move the cursor by a specified</p><p> // distance in the direction in</p><p> // which we're pointing</p><p></p><p> Point3d oldPos = m_ecs.Origin;</p><p> Point3d newPos = oldPos + m_ecs.Xaxis * distance;</p><p></p><p> m_ecs =</p><p> new CoordinateSystem3d(</p><p> newPos,</p><p> m_ecs.Xaxis,</p><p> m_ecs.Yaxis</p><p> );</p><p></p><p> // If the pen is down, we draw something</p><p></p><p> if (m_pen.Down)</p><p> GenerateSegment(oldPos, newPos);</p><p> }</p><p></p><p> public void PenDown()</p><p> {</p><p> m_pen.Down = true;</p><p> }</p><p></p><p> public void PenUp()</p><p> {</p><p> m_pen.Down = false;</p><p></p><p> // We'll start a new entity with the next</p><p> // use of the pen</p><p></p><p> m_poly = null;</p><p> }</p><p></p><p> public void SetPenWidth(double width)</p><p> {</p><p> // Pen width is not currently implemented in 3D</p><p></p><p> //m_pen.Width = width;</p><p> }</p><p></p><p> public void SetPenColor(int idx)</p><p> {</p><p> // Right now we just use an ACI,</p><p> // to make the code simpler</p><p></p><p> Color col =</p><p> Color.FromColorIndex(</p><p> ColorMethod.ByAci,</p><p> (short)idx</p><p> );</p><p></p><p> // If we have to change the color,</p><p> // we'll start a new entity</p><p> // (if the entity type we're creating</p><p> // supports per-segment colors, we</p><p> // don't need to do this)</p><p></p><p> if (col != m_pen.Color)</p><p> {</p><p> m_poly = null;</p><p> m_pen.Color = col;</p><p> }</p><p> }</p><p></p><p> // Internal helper to generate geometry</p><p> // (this could be optimised to keep the</p><p> // object we're generating open, rather</p><p> // than having to reopen it each time)</p><p></p><p> private void GenerateSegment(</p><p> Point3d oldPos, Point3d newPos)</p><p> {</p><p> Document doc =</p><p> Application.DocumentManager.MdiActiveDocument;</p><p> Database db = doc.Database;</p><p> Editor ed = doc.Editor;</p><p></p><p> Autodesk.AutoCAD.ApplicationServices.</p><p> TransactionManager tm =</p><p> doc.TransactionManager;</p><p></p><p> // Create the current object, if there is none</p><p></p><p> if (m_poly == null)</p><p> {</p><p> BlockTable bt =</p><p> (BlockTable)m_trans.GetObject(</p><p> db.BlockTableId,</p><p> OpenMode.ForRead</p><p> );</p><p> BlockTableRecord ms =</p><p> (BlockTableRecord)m_trans.GetObject(</p><p> bt,</p><p> OpenMode.ForWrite</p><p> );</p><p></p><p> // Create the polyline</p><p></p><p> m_poly = new Polyline3d();</p><p> m_poly.Color = m_pen.Color;</p><p></p><p> // Add the polyline to the database</p><p></p><p> ms.AppendEntity(m_poly);</p><p> m_trans.AddNewlyCreatedDBObject(m_poly, true);</p><p> // Add the first vertex</p><p></p><p> PolylineVertex3d vert =</p><p> new PolylineVertex3d(oldPos);</p><p></p><p> m_poly.AppendVertex(vert);</p><p> m_trans.AddNewlyCreatedDBObject(vert, true);</p><p> }</p><p></p><p> // Add the new vertex</p><p></p><p> PolylineVertex3d vert2 =</p><p> new PolylineVertex3d(newPos);</p><p></p><p> m_poly.AppendVertex(vert2);</p><p> m_trans.AddNewlyCreatedDBObject(vert2, true);</p><p></p><p> // Display the graphics, to avoid long,</p><p> // black-box operations</p><p></p><p> if (m_updateGraphics)</p><p> {</p><p> tm.QueueForGraphicsFlush();</p><p> tm.FlushGraphics();</p><p> ed.UpdateScreen();</p><p> }</p><p> }</p><p> }</p><p></p><p> public class Commands</p><p> {</p><p> </p><p> static public void Cube()</p><p> {</p><p> Document doc =</p><p> Application.DocumentManager.MdiActiveDocument;</p><p></p><p> Transaction tr =</p><p> doc.TransactionManager.StartTransaction();</p><p> using (tr)</p><p> {</p><p> TurtleEngine te = new TurtleEngine(tr);</p><p></p><p> // Draw a simple 3D cube</p><p></p><p> te.PenDown();</p><p> for (int i=0; i < 4; i++)</p><p> {</p><p> for (int j=0; j < 4; j++)</p><p> {</p><p> te.Move(100);</p><p> te.Turn(Math.PI / 2);</p><p> }</p><p> te.Move(100);</p><p> te.Pitch(Math.PI / -2);</p><p> }</p><p> tr.Commit();</p><p> }</p><p> }</p><p></p><p> static private int CubesPerLevel(int level)</p><p> {</p><p> if (level == 0)</p><p> return 0;</p><p> else</p><p> return 2 * CubesPerLevel(level - 1) + 1;</p><p> }</p><p></p><p> static public bool GetHilbertInfo(</p><p> out Point3d position,</p><p> out double size,</p><p> out int level</p><p> )</p><p> {</p><p> Document doc =</p><p> Application.DocumentManager.MdiActiveDocument;</p><p> Editor ed = doc.Editor;</p><p></p><p> size = 0;</p><p> level = 0;</p><p> position = Point3d.Origin;</p><p></p><p> PromptPointOptions ppo =</p><p> new PromptPointOptions(</p><p> "\nSelect base point of Hilbert cube: "</p><p> );</p><p></p><p> PromptPointResult ppr =</p><p> ed.GetPoint(ppo);</p><p></p><p> if (ppr.Status != PromptStatus.OK)</p><p> return false;</p><p></p><p> position = ppr.Value;</p><p></p><p> PromptDoubleOptions pdo =</p><p> new PromptDoubleOptions(</p><p> "\nEnter size <100>: "</p><p> );</p><p></p><p> pdo.AllowNone = true;</p><p></p><p> PromptDoubleResult pdr =</p><p> ed.GetDouble(pdo);</p><p></p><p> if (pdr.Status != PromptStatus.None &&</p><p> pdr.Status != PromptStatus.OK)</p><p> return false;</p><p></p><p> if (pdr.Status == PromptStatus.OK)</p><p> size = pdr.Value;</p><p> else</p><p> size = 100;</p><p></p><p> PromptIntegerOptions pio =</p><p> new PromptIntegerOptions(</p><p> "\nEnter level <5>: "</p><p> );</p><p></p><p> pio.AllowNone = true;</p><p> pio.LowerLimit = 1;</p><p> pio.UpperLimit = 10;</p><p></p><p> PromptIntegerResult pir =</p><p> ed.GetInteger(pio);</p><p></p><p> if (pir.Status != PromptStatus.None &&</p><p> pir.Status != PromptStatus.OK)</p><p> return false;</p><p></p><p> if (pir.Status == PromptStatus.OK)</p><p> level = pir.Value;</p><p> else</p><p> level = 5;</p><p></p><p> return true;</p><p> }</p><p></p><p> private static void Hilbert(</p><p> TurtleEngine te, double size, int level)</p><p> {</p><p> if (level > 0)</p><p> {</p><p> int newLevel = level - 1;</p><p> te.Pitch(Math.PI / -2); // Down Pitch 90</p><p> te.Roll(Math.PI / -2); // Left Roll 90</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Move(size); // Forward Size</p><p> te.Pitch(Math.PI / -2); // Down Pitch 90</p><p> te.Roll(Math.PI / -2); // Left Roll 90</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Move(size); // Forward Size</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Turn(Math.PI / -2); // Left Turn 90</p><p> te.Move(size); // Forward Size</p><p> te.Pitch(Math.PI / -2); // Down Pitch 90</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Move(size); // Forward Size</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Pitch(Math.PI / 2); // Up Pitch 90</p><p> te.Move(size); // Forward Size</p><p> te.Turn(Math.PI / 2); // Right Turn 90</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Move(size); // Forward Size</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Turn(Math.PI / -2); // Left Turn 90</p><p> te.Move(size); // Forward Size</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> Hilbert(te, size, newLevel); // Recurse</p><p> te.Turn(Math.PI / -2); // Left Turn 90</p><p> te.Roll(Math.PI / 2); // Right Roll 90</p><p> }</p><p> }</p><p></p><p> </p><p> static public void DrawHilbert()</p><p> {</p><p> Document doc =</p><p> Application.DocumentManager.MdiActiveDocument;</p><p></p><p> double size;</p><p> int level;</p><p> Point3d position;</p><p></p><p> if (!GetHilbertInfo(out position, out size, out level))</p><p> return;</p><p></p><p> Transaction tr =</p><p> doc.TransactionManager.StartTransaction();</p><p> using (tr)</p><p> {</p><p> TurtleEngine te = new TurtleEngine(tr);</p><p></p><p> // Draw a Hilbert cube</p><p></p><p> te.Position = position;</p><p></p><p> te.PenDown();</p><p> Hilbert(te, size / CubesPerLevel(level), level);</p><p> tr.Commit();</p><p> }</p><p> }</p><p> }</p><p>}</p><p>To try out this new engine, I implemented a few commands: one draws a simply cube (the CB command), while the other does something much more interesting - it draws a Hilbert curve in 3D (I've called it a Hilbert cube, although that's not really the official terminology). Check out the DH command above and its results, below.</p><p>Here are the results of running the DH command for levels 1-6. Level 7 is as close to a solid black cube as you can get without zooming very, very closely, so that's where I stopped.</p><p>First the plan view:</p><p></p><p>Now for 3D:</p><p></p><p>For fun, I took the level 4 cube and drew a circle at one its end-points:</p><p></p><p>Here's what happens when we EXTRUDE the circle along the Polyline3d path, setting the Visual Style to conceptual:</p><p></p><p>A final note, to close out today's topic: a very useful (and fascinating) reference for me during this implementation has been The Algorithmic Beauty of Plants, a volume by Przemyslaw Prusinkiewicz and Aristid Lindenmayer. While it is unfortunately no longer in print, it is thankfully available as a free download.</p><p></p> July 15, 2008
Turtle fractals in AutoCAD using .NET - Part 4
I just couldn't resist coming back to this fun (at least for me :-) series... for reference here are parts 1, 2 and 3, while the series really started here.
There are two main places I wanted to take this implementation: firstly it really needed to be made 3D, which is the focus of this post, and I still then want to take it further by implementing a turtle graphics-oriented language (one could probably call it a Domain Specific Language, the domain being turtle graphics), which is likely to be a subset or variant of Logo. This is likely to be done using F#, but we'll see when I get around to it... :-)
Firstly, some concepts: with a 2D turtle graphics system we only really need two operations, Move and Turn. As we update our implementation to cope with that pesky third dimension we need to extend the set of operations to include Pitch and Roll. So our "turtle" needs to have more than just a position and a direction, it needs its own positioning matrix (essentially its own coordinate system). Think of the turtle as having its own little UCS icon travelling around with it (the direction being the X axis): we can then implement Turn as a rotation around its current Z axis, Pitch as a rotation around its Y axis and Roll as a rotation around its X axis. Each of these operations will, of course, update the coordinate system so that it's pointing somewhere different.
The below implementation maintains the Direction property, but it's now read-only: the underlying implementation is now via a CoordinateSystem3d object member variable (m_ecs). Each of the Move, Turn, Pitch and Roll operations adjusts the coordinate system, as does setting the Position property.
As for the geometry that we create via the turtle's movements: Polyline objects are inherently 2D, so the new implementation makes use of Polyline3d objects instead. Polyline3d objects contain PolylineVertex3d objects, and have slightly different requirements about database residency as we're adding our vertices, but the fundamental approach is not really any different. A couple of points to note... as pen thickness doesn't really make sense in 3D (at least for Polyline3d objects - we could, of course, get clever with extruding profiles along our paths, if we really wanted to), I've decided to ignore it, for now. The same is true of pen colour. These are both left as enhancements for the future.
Here's the updated C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine
{
// Private members
private Transaction m_trans;
private Polyline3d m_poly;
private Pen m_pen;
private CoordinateSystem3d m_ecs;
private bool m_updateGraphics;
// Public properties
public Point3d Position
{
get { return m_ecs.Origin; }
set {
m_ecs =
new CoordinateSystem3d(
value,
m_ecs.Xaxis,
m_ecs.Yaxis
);
}
}
public Vector3d Direction
{
get { return m_ecs.Xaxis; }
}
// Constructor
public TurtleEngine(Transaction tr)
{
m_pen = new Pen();
m_trans = tr;
m_poly = null;
m_ecs =
new CoordinateSystem3d(
Point3d.Origin,
Vector3d.XAxis,
Vector3d.YAxis
);
m_updateGraphics = false;
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Zaxis,
Position
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis.TransformBy(mat),
m_ecs.Yaxis.TransformBy(mat)
);
}
public void Pitch(double angle)
{
// Pitch in our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Yaxis,
m_ecs.Origin
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis.TransformBy(mat),
m_ecs.Yaxis
);
}
public void Roll(double angle)
{
// Roll along our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Xaxis,
m_ecs.Origin
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis,
m_ecs.Yaxis.TransformBy(mat)
);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = m_ecs.Origin;
Point3d newPos = oldPos + m_ecs.Xaxis * distance;
m_ecs =
new CoordinateSystem3d(
newPos,
m_ecs.Xaxis,
m_ecs.Yaxis
);
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, newPos);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the next
// use of the pen
m_poly = null;
}
public void SetPenWidth(double width)
{
// Pen width is not currently implemented in 3D
//m_pen.Width = width;
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
m_poly = null;
m_pen.Color = col;
}
}
// Internal helper to generate geometry
// (this could be optimised to keep the
// object we're generating open, rather
// than having to reopen it each time)
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
// Create the current object, if there is none
if (m_poly == null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
m_poly = new Polyline3d();
m_poly.Color = m_pen.Color;
// Add the polyline to the database
ms.AppendEntity(m_poly);
m_trans.AddNewlyCreatedDBObject(m_poly, true);
// Add the first vertex
PolylineVertex3d vert =
new PolylineVertex3d(oldPos);
m_poly.AppendVertex(vert);
m_trans.AddNewlyCreatedDBObject(vert, true);
}
// Add the new vertex
PolylineVertex3d vert2 =
new PolylineVertex3d(newPos);
m_poly.AppendVertex(vert2);
m_trans.AddNewlyCreatedDBObject(vert2, true);
// Display the graphics, to avoid long,
// black-box operations
if (m_updateGraphics)
{
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
}
}
}
public class Commands
{
static public void Cube()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a simple 3D cube
te.PenDown();
for (int i=0; i < 4; i++)
{
for (int j=0; j < 4; j++)
{
te.Move(100);
te.Turn(Math.PI / 2);
}
te.Move(100);
te.Pitch(Math.PI / -2);
}
tr.Commit();
}
}
static private int CubesPerLevel(int level)
{
if (level == 0)
return 0;
else
return 2 * CubesPerLevel(level - 1) + 1;
}
static public bool GetHilbertInfo(
out Point3d position,
out double size,
out int level
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
size = 0;
level = 0;
position = Point3d.Origin;
PromptPointOptions ppo =
new PromptPointOptions(
"\nSelect base point of Hilbert cube: "
);
PromptPointResult ppr =
ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return false;
position = ppr.Value;
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter size <100>: "
);
pdo.AllowNone = true;
PromptDoubleResult pdr =
ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.None &&
pdr.Status != PromptStatus.OK)
return false;
if (pdr.Status == PromptStatus.OK)
size = pdr.Value;
else
size = 100;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter level <5>: "
);
pio.AllowNone = true;
pio.LowerLimit = 1;
pio.UpperLimit = 10;
PromptIntegerResult pir =
ed.GetInteger(pio);
if (pir.Status != PromptStatus.None &&
pir.Status != PromptStatus.OK)
return false;
if (pir.Status == PromptStatus.OK)
level = pir.Value;
else
level = 5;
return true;
}
private static void Hilbert(
TurtleEngine te, double size, int level)
{
if (level > 0)
{
int newLevel = level - 1;
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / -2); // Left Roll 90
Hilbert(te, size, newLevel);// Recurse
te.Move(size); // Forward Size
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / -2); // Left Roll 90
Hilbert(te, size, newLevel);// Recurse
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.Turn(Math.PI / -2); // Left Turn 90
te.Move(size); // Forward Size
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / 2); // Right Roll 90
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.Pitch(Math.PI / 2); // Up Pitch 90
te.Move(size); // Forward Size
te.Turn(Math.PI / 2); // Right Turn 90
te.Roll(Math.PI / 2); // Right Roll 90
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.Turn(Math.PI / -2); // Left Turn 90
te.Move(size); // Forward Size
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.Turn(Math.PI / -2); // Left Turn 90
te.Roll(Math.PI / 2); // Right Roll 90
}
}
static public void DrawHilbert()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
double size;
int level;
Point3d position;
if (!GetHilbertInfo(out position, out size, out level))
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
// Draw a Hilbert cube
te.Position = position;
te.PenDown();
Hilbert(te, size / CubesPerLevel(level), level);
tr.Commit();
}
}
}
}
To try out this new engine, I implemented a few commands: one draws a simply cube (the CB command), while the other does something much more interesting - it draws aHilbert curve in 3D (I've called it a Hilbert cube, although that's not really the official terminology). Check out the DH command above and its results, below.
Here are the results of running the DH command for levels 1-6. Level 7 is as close to a solid black cube as you can get without zooming very, very closely, so that's where I stopped.
First the plan view:
Now for 3D:
For fun, I took the level 4 cube and drew a circle at one its end-points:
Here's what happens when we EXTRUDE the circle along the Polyline3d path, setting the Visual Style to conceptual:
A final note, to close out today's topic: a very useful (and fascinating) reference for me during this implementation has beenThe Algorithmic Beauty of Plants, a volume by Przemyslaw Prusinkiewicz and Aristid Lindenmayer. While it is unfortunately no longer in print, it is thankfully available as a free download.
July 17, 2008
Turtle fractals in AutoCAD using .NET - Part 5
Once again I've ended up extending this series in a way I didn't originally expect to (and yes, that's a good thing :-). Here are parts 1, 2, 3 and 4, as well as the post that started it all.
After thinking about my initial 3D implementation in Part 4, I realised that implementing pen colours and widths would actually be relatively easy. Here's the idea:
Each section of a different width and/or pen colour is actually a separate extruded solid
Whenever we start a new section we start off by creating a circular profile of the current pen width at the start
When we terminate that section - by changing the pen colour or width - we extrude the profile along the Polyline3d defining the section's path
This extruded Solid3d will be the colour of the pen, of course
We then erase the original polyline
In order to achieve this, we now have a TerminateCurrentSection() helper function, which we call whenever the pen width or colour changes, and when we are done with the TurtleEngine, of course. For this last part we've changed the TurtleEngine to implement IDisposable: this gives us the handy Dispose() method to implement (which simply calls TerminateCurrentSection()), and we can the add the using() statement to control the TurtleEngine's lifetime. One important point: we need to Dispose of the TurtleEngine before we commit the transaction, otherwise it won't work properly.
Here's the modified C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System;
namespace TurtleGraphics
{
// This class encapsulates pen
// information and will be
// used by our TurtleEngine
class Pen
{
// Private members
private Color m_color;
private double m_width;
private bool m_down;
// Public properties
public Color Color
{
get { return m_color; }
set { m_color = value; }
}
public double Width
{
get { return m_width; }
set { m_width = value; }
}
public bool Down
{
get { return m_down; }
set { m_down = value; }
}
// Constructor
public Pen()
{
m_color =
Color.FromColorIndex(ColorMethod.ByAci, 0);
m_width = 0.0;
m_down = false;
}
}
// The main Turtle Graphics engine
class TurtleEngine : IDisposable
{
// Private members
private Transaction m_trans;
private Polyline3d m_poly;
private Circle m_profile;
private Pen m_pen;
private CoordinateSystem3d m_ecs;
private bool m_updateGraphics;
// Public properties
public Point3d Position
{
get { return m_ecs.Origin; }
set {
m_ecs =
new CoordinateSystem3d(
value,
m_ecs.Xaxis,
m_ecs.Yaxis
);
}
}
public Vector3d Direction
{
get { return m_ecs.Xaxis; }
}
// Constructor
public TurtleEngine(Transaction tr)
{
m_pen = new Pen();
m_trans = tr;
m_poly = null;
m_profile = null;
m_ecs =
new CoordinateSystem3d(
Point3d.Origin,
Vector3d.XAxis,
Vector3d.YAxis
);
m_updateGraphics = false;
}
public void Dispose()
{
TerminateCurrentSection();
}
// Public methods
public void Turn(double angle)
{
// Rotate our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Zaxis,
Position
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis.TransformBy(mat),
m_ecs.Yaxis.TransformBy(mat)
);
}
public void Pitch(double angle)
{
// Pitch in our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Yaxis,
m_ecs.Origin
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis.TransformBy(mat),
m_ecs.Yaxis
);
}
public void Roll(double angle)
{
// Roll along our direction by the
// specified angle
Matrix3d mat =
Matrix3d.Rotation(
angle,
m_ecs.Xaxis,
m_ecs.Origin
);
m_ecs =
new CoordinateSystem3d(
m_ecs.Origin,
m_ecs.Xaxis,
m_ecs.Yaxis.TransformBy(mat)
);
}
public void Move(double distance)
{
// Move the cursor by a specified
// distance in the direction in
// which we're pointing
Point3d oldPos = m_ecs.Origin;
Point3d newPos = oldPos + m_ecs.Xaxis * distance;
m_ecs =
new CoordinateSystem3d(
newPos,
m_ecs.Xaxis,
m_ecs.Yaxis
);
// If the pen is down, we draw something
if (m_pen.Down)
GenerateSegment(oldPos, newPos);
}
public void PenDown()
{
m_pen.Down = true;
}
public void PenUp()
{
m_pen.Down = false;
// We'll start a new entity with the next
// use of the pen
TerminateCurrentSection();
}
public void SetPenWidth(double width)
{
m_pen.Width = width;
TerminateCurrentSection();
}
public void SetPenColor(int idx)
{
// Right now we just use an ACI,
// to make the code simpler
Color col =
Color.FromColorIndex(
ColorMethod.ByAci,
(short)idx
);
// If we have to change the color,
// we'll start a new entity
// (if the entity type we're creating
// supports per-segment colors, we
// don't need to do this)
if (col != m_pen.Color)
{
TerminateCurrentSection();
m_pen.Color = col;
}
}
// Internal helper to generate geometry
private void GenerateSegment(
Point3d oldPos, Point3d newPos)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
Autodesk.AutoCAD.ApplicationServices.
TransactionManager tm =
doc.TransactionManager;
// Create the current object, if there is none
if (m_poly == null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Create the polyline
m_poly = new Polyline3d();
m_poly.Color = m_pen.Color;
// Add the polyline to the database
ms.AppendEntity(m_poly);
m_trans.AddNewlyCreatedDBObject(m_poly, true);
// Add the first vertex
PolylineVertex3d vert =
new PolylineVertex3d(oldPos);
m_poly.AppendVertex(vert);
m_trans.AddNewlyCreatedDBObject(vert, true);
m_profile =
new Circle(oldPos, Direction, m_pen.Width);
ms.AppendEntity(m_profile);
m_trans.AddNewlyCreatedDBObject(m_profile, true);
m_profile.DowngradeOpen();
}
// Add the new vertex
PolylineVertex3d vert2 =
new PolylineVertex3d(newPos);
m_poly.AppendVertex(vert2);
m_trans.AddNewlyCreatedDBObject(vert2, true);
// Display the graphics, to avoid long,
// black-box operations
if (m_updateGraphics)
{
tm.QueueForGraphicsFlush();
tm.FlushGraphics();
ed.UpdateScreen();
}
}
// Internal helper to generate 3D geometry
private void TerminateCurrentSection()
{
if (m_profile != null && m_poly != null)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
try
{
// Generate a Region from our circular profile
DBObjectCollection col =
new DBObjectCollection();
col.Add(m_profile);
DBObjectCollection res =
Region.CreateFromCurves(col);
Region reg =
res as Region;
if (reg != null)
{
BlockTable bt =
(BlockTable)m_trans.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
BlockTableRecord ms =
(BlockTableRecord)m_trans.GetObject(
bt,
OpenMode.ForWrite
);
// Extrude our Region along the Polyline3d path
Solid3d sol = new Solid3d();
sol.ExtrudeAlongPath(reg, m_poly, 0.0);
sol.Color = m_pen.Color;
// Add the generated Solid3d to the database
ms.AppendEntity(sol);
m_trans.AddNewlyCreatedDBObject(sol, true);
// Get rid of the Region, profile and path
reg.Dispose();
m_profile.UpgradeOpen();
m_profile.Erase();
m_poly.Erase();
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nException: {0}",
ex.Message
);
}
}
m_profile = null;
m_poly = null;
}
}
public class Commands
{
static public void Cube()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
using (te)
{
// Draw a simple 3D cube
te.SetPenWidth(5.0);
te.PenDown();
for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 4; j++)
{
// Only draw some of the segments
// (this stops overlap)
if (i % 2 == 0 || j % 2 == 0)
te.PenDown();
else
te.PenUp();
te.SetPenColor(i+j+1);
te.Move(100);
te.Turn(Math.PI / 2);
}
te.PenUp();
te.Move(100);
te.Pitch(Math.PI / -2);
}
}
tr.Commit();
}
}
static private int CubesPerLevel(int level)
{
if (level == 0)
return 0;
else
return 2 * CubesPerLevel(level - 1) + 1;
}
static public bool GetHilbertInfo(
out Point3d position,
out double size,
out int level
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
size = 0;
level = 0;
position = Point3d.Origin;
PromptPointOptions ppo =
new PromptPointOptions(
"\nSelect base point of Hilbert cube: "
);
PromptPointResult ppr =
ed.GetPoint(ppo);
if (ppr.Status != PromptStatus.OK)
return false;
position = ppr.Value;
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nEnter size <100>: "
);
pdo.AllowNone = true;
PromptDoubleResult pdr =
ed.GetDouble(pdo);
if (pdr.Status != PromptStatus.None &&
pdr.Status != PromptStatus.OK)
return false;
if (pdr.Status == PromptStatus.OK)
size = pdr.Value;
else
size = 100;
PromptIntegerOptions pio =
new PromptIntegerOptions(
"\nEnter level <5>: "
);
pio.AllowNone = true;
pio.LowerLimit = 1;
pio.UpperLimit = 10;
PromptIntegerResult pir =
ed.GetInteger(pio);
if (pir.Status != PromptStatus.None &&
pir.Status != PromptStatus.OK)
return false;
if (pir.Status == PromptStatus.OK)
level = pir.Value;
else
level = 5;
return true;
}
private static void Hilbert(
TurtleEngine te, double size, int level)
{
if (level > 0)
{
te.SetPenColor(level);
int newLevel = level - 1;
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / -2); // Left Roll 90
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Move(size); // Forward Size
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / -2); // Left Roll 90
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.Turn(Math.PI / -2); // Left Turn 90
te.Move(size); // Forward Size
te.Pitch(Math.PI / -2); // Down Pitch 90
te.Roll(Math.PI / 2); // Right Roll 90
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Pitch(Math.PI / 2); // Up Pitch 90
te.Move(size); // Forward Size
te.Turn(Math.PI / 2); // Right Turn 90
te.Roll(Math.PI / 2); // Right Roll 90
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Move(size); // Forward Size
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Turn(Math.PI / -2); // Left Turn 90
te.Move(size); // Forward Size
te.Roll(Math.PI / 2); // Right Roll 90
Hilbert(te, size, newLevel);// Recurse
te.SetPenColor(level);
te.Turn(Math.PI / -2); // Left Turn 90
te.Roll(Math.PI / 2); // Right Roll 90
}
}
static public void DrawHilbert()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
double size;
int level;
Point3d position;
if (!GetHilbertInfo(out position, out size, out level))
return;
int cbl = CubesPerLevel(level);
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
TurtleEngine te = new TurtleEngine(tr);
using (te)
{
// Draw a Hilbert cube
te.Position = position;
te.SetPenWidth(10.0 / cbl);
te.PenDown();
Hilbert(te, size / cbl, level);
}
tr.Commit();
}
}
}
}
Here are the results of the modified CB command, which now has coloured segments with a width:
Here's what we get from the DH command. This command now runs pretty slowly for the higher levels - it is doing a lot of work, after all - and only runs at all because we're using separate sections by changing the colour regularly. You'll notice that the pen width is set according to the level, as the finer the detail, the finer the pen width needed.
First the plan view:
Then the full 3D view:
Here's a close-up of the level 5 cube:
A word of caution: some of these higher levels are extremely resource-intensive. Please do not attempt to play around with something like this while working on something you don't want to lose: there is always a slim chance of the application (and even the system, if you're really unlucky) being brought down when system resources become scarce.
页:
[1]
2