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[BlockTableRecord.ModelSpace], 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 { [CommandMethod("CB")] 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 } } [CommandMethod("DH")] 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 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. 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 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. |