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.

8 responses to “A simple turtle graphics implementation in AutoCAD using .NET”

  1. Fernando Malard Avatar
    Fernando Malard

    Great Kean!

    I really like your AutoLogo or LogoCAD! ๐Ÿ™‚

    Now my old college Logo classes into Civil Engineering graduation came back to my mind!!!

    Great idea and maybe AutoCAD can be used to teach kids and pre-school children.

    Maybe the next steps would add an input point monitor to detect arrow clicks, space bar to PenUp/PenDown, maybe a "ALT+C" command to type a new pen color number and a "ALT+W" to type a new pen width.

    Regards,
    Fernando.

  2. Kean Walmsley Avatar

    Hmm - turn AutoCAD into an Etch-A-Sketch? That would indeed be fun. ๐Ÿ™‚

    Kean

  3. Fernando Malard Avatar
    Fernando Malard

    Yeah!

    To add a cherry to the cake you would draw a turtle as the command cursor. ๐Ÿ™‚

    AutoCAD for Kids!

  4. hi kean,

    did you ever see john walker's atlast and classwar projects? they included a turtle environment in autocad ... and a forth implementation, by the way ...

    cheers

    jeremy

  5. Kean Walmsley Avatar

    Hi Jeremy,

    No, I don't remember seeing those, although I did just find a link to Atlast here.

    Cheers,

    Kean

  6. hi kean,

    we actually used atlast as a basis for RoFirst, a forth-based interpreted and compiled language for generating parametric hvac parts for RoCAD such as ductwork and equipment from ascii source code managed in a relational database ...

    john used atlast as a basis for classwar, an object-oriented forth-based language built into autocad before the advent of C++ and ARX ... including a turtle graphics engine ๐Ÿ™‚

    cheers

    jeremy

  7. Loved this article! I did some cool things with Logo back in 1984 at age 10 also. I learned some basic and programmed text based games at that time also.

    Thanks for bringing back some memories. ๐Ÿ™‚

    You blog is top notch btw!

  8. (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. ๐Ÿ™‚

    I must have been busy and glazed over this post when I read it the first time. Change the age to about 11-12 and the Language to TRS-80 Basic and it could be me. My friend and I would work on the program at school, then take the code home and translate it into Atari Basic, for me, and Commodore Basic, for him.

Leave a Reply to David Osborne Cancel reply

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