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        








« Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET | Main | Two years and counting... »

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 called Logo running on the BBC 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 with BBC 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 simple turtle 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 of Domain 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[BlockTableRecord.ModelSpace],

              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


    [CommandMethod("DTG")]

    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:

2D turtle graphics

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.

TrackBack

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

Listed below are links to weblogs that reference A simple turtle graphics implementation in AutoCAD using .NET:

blog comments powered by Disqus

Feed/Share

10 Random Posts