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:

Hilbert Cubes (Levels 1 to 6) - Plan

Now for 3D:

Hilbert Cubes (Levels 1 to 6) - 3D

For fun, I took the level 4 cube and drew a circle at one its end-points:

Hilbert Cube (Level 4) - Ready to extrude

Here's what happens when we EXTRUDE the circle along the Polyline3d path, setting the Visual Style to conceptual:

Hilbert Cube (Level 4) - Path used for extrusion

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.

2 responses to “Turtle fractals in AutoCAD using .NET - Part 4”

  1. Mark Johnston Avatar

    This is by no means to complain but just to share.
    This is a very interesting series of articles that I've been keeping an eye on. I couldn't resist trying it this time.
    When I ran DH at 50, 3 all was fine.
    Then I ran it at 80, 5.
    The cube was generated. I went to move it and AutoCAD stopped responding. Then my screen went completely black and my system went dead. Not even a blue screen.
    I should stick to what I understand. This is some powerful stuff!!

    I love your work and have learned a lot from these articles.
    Thanks.

    p.s. - System started back up fine.

  2. Hi Mark,

    Wow - interesting, and more than a little scary. My apologies for bringing down your system. ๐Ÿ™

    I didn't try moving any of the cubes I created, I have to admit. These are very (and I mean *very*) complex polylines, and so have pretty extreme memory and display requirements.

    I'm toying with ideas about breaking the polylines into separate ones for different pen colours and widths (extruding circular profiles whenever the pen width or colour changes), which will either help or make things worse... we'll see.

    This is a good reminder, though: if playing around with stuff like this, don't be working on anything significant in another window! ๐Ÿ™‚

    Regards,

    Kean

Leave a Reply to Mark Johnston Cancel reply

Your email address will not be published. Required fields are marked *