September 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        








« Turtle fractals in AutoCAD using .NET - Part 4 | Main | AutoCAD 2009 Update 1 now available »

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[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);


        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[0] as Region;

          if (reg != null)

          {

            BlockTable bt =

              (BlockTable)m_trans.GetObject(

                db.BlockTableId,

                OpenMode.ForRead

              );

            BlockTableRecord ms =

              (BlockTableRecord)m_trans.GetObject(

                bt[BlockTableRecord.ModelSpace],

                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

  {

    [CommandMethod("CB")]

    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

      }

    }


    [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;


      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:

3D cube

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:

Hilbert Cubes (Levels 1 to 5) - 3D plan

Then the full 3D view:

Hilbert Cubes (Levels 1 to 5) - Full 3D

Here's a close-up of the level 5 cube:

Hilbert Cube (Level 5) - Full, scary 3D

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.

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00d83452464869e200e553bf8a408834

Listed below are links to weblogs that reference Turtle fractals in AutoCAD using .NET - Part 5:

blog comments powered by Disqus

Feed/Share

10 Random Posts