Kean Walmsley

July 2009

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 31  

Twitter Updates

    follow me on Twitter



    « 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:

    Comments

    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.

    Hmm - turn AutoCAD into an Etch-A-Sketch? That would indeed be fun. :-)

    Kean

    Yeah!

    To add a cherry to the cake you would draw a turtle as the command cursor. :)

    AutoCAD for Kids!

    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

    Hi Jeremy,

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

    Cheers,

    Kean

    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

    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!

    Verify your Comment

    Previewing your Comment

    This is only a preview. Your comment has not yet been posted.

    Working...
    Your comment could not be posted. Error type:
    Your comment has been posted. Post another comment

    The letters and numbers you entered did not match the image. Please try again.

    As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

    Having trouble reading this image? View an alternate.

    Working...

    Post a comment

    Feed & Share

    Search