Animating transient AutoCAD graphics using .NET

A developer had an interesting requirement that I thought I'd spend some time looking at: to animate transient graphics inside AutoCAD according to data they've pulled in from an external simulation system.

It's clear that AutoCAD is really not an animation platform – we have other products that are better suited to working in this way – but I thought it would be interesting to see what was possible.

I decided to take the implementation shown in this previous post and throw in some code to animate a few different things:

  • Change the per-vertex colours of our transient box
    • These are set to purely random values, but you could imagine setting them in a coherent way to animate some kind of flow across a mesh
  • Display a number of points in a 3D volume
    • These should be rendered as filled circles – to provide some level of size – rather than just relying on the Polypoint() method

We're also displaying some screen-fixed text at the bottom left, as that was in the previous project and it seemed helpful to leave it in.

The update of the data – which would be pulled in from an external system, but in our case is just randomly generated – is performed when AutoCAD is idle. This event isn't fired as often as it might be, so I've also added some code to force processing of Windows messages (much in the same way as I did with the Kinect jig implementation). This helps the messages – and events – to flow more smoothly, resulting in a smoother animation.

Here's the C# code to do this:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.GraphicsInterface;

using Autodesk.AutoCAD.Colors;

using System.Collections.Generic;

 

namespace TransientSelection

{

  public class TransientBox : Transient

  {

    // Internal state

 

    private TextStyle _style;

    Point3dCollection _verts;

    IntegerCollection _faces;

    EdgeData _edgeData;

    FaceData _faceData;

    VertexData _vertData;

 

    Point3dCollection _specks;

 

    public TransientBox(double side)

    {

      // Create the style for our text

 

      _style = new TextStyle();

      _style.Font =

        new FontDescriptor("Calibri", false, true, 0, 0);

      _style.TextSize = 10;

 

      // Add our vertices manually (8 of them for a box)

 

      _verts = new Point3dCollection();

 

      _verts.Add(Point3d.Origin);                // 0

      _verts.Add(new Point3d(side, 0, 0));       // 1

      _verts.Add(new Point3d(side, side, 0));    // 2

      _verts.Add(new Point3d(0, side, 0));       // 3

 

      _verts.Add(new Point3d(0, 0, side));       // 4

      _verts.Add(new Point3d(side, 0, side));    // 5

      _verts.Add(new Point3d(side, side, side)); // 6

      _verts.Add(new Point3d(0, side, side));    // 7

 

      // Our faces are defined in sets of 3 vertices

      // (listed below two per line, each making a square face)

 

      int[] polys =

      {

        0, 1, 2, 2, 3, 0, // Bottom

        0, 4, 5, 5, 1, 0, // Front

        1, 5, 6, 6, 2, 1, // Right

        2, 6, 7, 7, 3, 2, // Back

        0, 3, 7, 7, 4, 0, // Left

        4, 7, 6, 6, 5, 4  // Top

      };

      const int polySize = 3;

 

      // Create our faces from the polys array

 

      _faces = new IntegerCollection();

      for (int p = 0; p < (polys.Length / polySize); p++)

      {

        _faces.Add(polySize);

        for (int v = 0; v < polySize; v++)

        {

          _faces.Add(polys[p * polySize + v]);

        }

      }

 

      // Create our edge data

 

      _edgeData = new EdgeData();

 

      // Each face's edges should have the same colour as the face

 

      _edgeData.SetColors(

        new short[]

        {

          1, 1, 1, 2, 2, 2, 3, 3, 3,

          4, 4, 4, 5, 5, 5, 6, 6, 6,

          7, 7, 7, 8, 8, 8, 9, 9, 9,

          10, 10, 10, 11, 11, 11, 12, 12, 12

        }

      );

 

      // Create our face data

 

      _faceData = new FaceData();

 

      // The face colours match their edges

 

      _faceData.SetColors(

        new short[]

        {

          1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12

        }

      );

 

      // Create our vertex data

 

      _vertData = new VertexData();

      _vertData.OrientationFlag = OrientationType.Clockwise;

 

      _specks = new Point3dCollection();

    }

 

    private void GenerateSpecksAndColours()

    {

      // Generate "specks" in a volume of 10 x 10 x 100

 

      const int length = 100, width = 10, height = 10;

 

      // We'll add an offset to put them away from the origin

 

      Vector3d offset = new Vector3d(0, 50, 0);

 

      // Generate a thousand random specs in our 3D volume

 

      System.Random ran = new System.Random();

 

      _specks.Clear();

      for (int i = 0; i < 1000; i++)

      {

        _specks.Add(

          new Point3d(

            ran.NextDouble() * length,

            ran.NextDouble() * width,

            ran.NextDouble() * height

          ) + offset

        );

      }

 

      // Generate a list of eight random colors,

      // one for each corner of our cube

 

      List<EntityColor> vertCols = new List<EntityColor>(8);

      for (int i = 0; i < 8; i++)

      {

        vertCols.Add(

          new EntityColor(ColorMethod.ByAci, (short)ran.Next(12))

        );

      }

 

      // Set the

      _vertData.SetTrueColors(vertCols.ToArray());

    }

 

    protected override int SubSetAttributes(DrawableTraits traits)

    {

      // Let's make our circles filled

 

      traits.FillType = FillType.FillAlways;

 

      return (int)DrawableAttributes.None;

    }

 

    protected override void SubViewportDraw(ViewportDraw vd)

    {

      // Draw our screen-fixed text

 

      DrawText(vd.Geometry, "ViewportDraw");

    }

 

    protected override bool SubWorldDraw(WorldDraw wd)

    {

      // Draw our box

 

      wd.Geometry.Shell(

        _verts, _faces, _edgeData, _faceData, _vertData, false

      );

 

      GenerateSpecksAndColours();

 

      // Display our specks as circles

      // (filled via SubSetAttributes())

 

      foreach (Point3d pt in _specks)

      {

        wd.Geometry.Circle(pt, 0.1, Vector3d.YAxis);

      }

 

      // We might choose to display our "point cloud" with a

      // single pixel per point using Polypoint()

 

      // wd.Geometry.Polypoint(_specks, null, null);

 

      ForceMessage();

 

      // Draw our screen-fixed text

 

      DrawText(wd.Geometry, "WorldDraw");

      return true;

    }

 

    private void DrawText(Geometry g, string text)

    {

      // We make use of another interface to push our transforms

 

      if (g != null)

      {

        // Push our transforms onto the stack

 

        g.PushOrientationTransform(OrientationBehavior.Screen);

 

        g.PushPositionTransform(

          PositionBehavior.Screen,

          new Point2d(30, 30)

        );

 

        // Draw our screen-fixed text

 

        g.Text(

          new Point3d(0, 0, 0),  // Position

          new Vector3d(0, 0, 1), // Normal

          new Vector3d(1, 0, 0), // Direction

          text,                  // Text

          true,                  // Rawness

          _style                 // TextStyle

        );

 

        // Remember to pop our transforms off the stack< /p>

 

        g.PopModelTransform();

        g.PopModelTransform();

      }

    }

 

    private void ForceMessage()

    {

      // Set the cursor without ectually moving it - enough to

      // generate a Windows message

 

      System.Drawing.Point pt =

        System.Windows.Forms.Cursor.Position;

      System.Windows.Forms.Cursor.Position =

        new System.Drawing.Point(pt.X, pt.Y);

    }

 

    protected override void OnDeviceInput(DeviceInputEventArgs e)

    {

      base.OnDeviceInput(e);

    }

 

    protected override void OnPointInput(PointInputEventArgs e)

    {

      base.OnPointInput(e);

    }

  }

 

  public class Commands

  {

    TransientBox _tb = null;

 

    [CommandMethod("TB")]

    public void TransientBox()

    {

      _tb = new TransientBox(10);

 

      // Tell AutoCAD to call into this transient's extended

      // protocol when appropriate

 

      Transient.CapturedDrawable = _tb;

 

      // Go ahead and draw the transient

 

      TransientManager.CurrentTransientManager.AddTransient(

        _tb, TransientDrawingMode.Main,

        128, new IntegerCollection()

      );

 

      Application.Idle += new System.EventHandler(OnIdle);

    }

 

    void OnIdle(object sender, System.EventArgs e)

    {

      TransientManager.CurrentTransientManager.UpdateTransient(

        _tb, new IntegerCollection()

      );

    }

 

    [CommandMethod("TBR")]

    public void RemoveTransientBox()

    {

      // Erase the transient graphics and dispose of the transient

 

      if (_tb != null)

      {

        Application.Idle -= new System.EventHandler(OnIdle);

 

        TransientManager.CurrentTransientManager.EraseTransient(

          _tb,

          new IntegerCollection()

        );

        _tb.Dispose();

        _tb = null;

      }

    }

  }

}

Here's the code in action:

Transient animation

9 responses to “Animating transient AutoCAD graphics using .NET”

  1. Hi Kean,

    are there any other ways to force the Idle-Event to fire, instead of "changing" the cursor's position? Unfortunately, this leads to extensive cursor flickering for me.

    Thank you!

  2. Hi Matthias,

    Really? I didn't see any flicker when setting the cursor to be at the same location.

    Unfortunately I don't know of another way - you might try asking ADN or posting to the forums.

    Regards,

    Kean

  3. Hi Kean,
    The article is great! But there is a problem I can't figure out. If we run the command 'tb', and then 'tbc', and run 'tb' again, the CAD will crash for System.NullReferenceException. And I cannot find where it comes from. Could you have a check? Thanks.

    1. Kean Walmsley Avatar

      Do you mean TBC or TBR? TBC for me brings up ToolbarConfig.

      I tried with TB then TBR then TB then TBR (etc.) and couldn't repro a crash in AutoCAD 2016.

      I'll need more details to understand this...

      Kean

      1. Sorry for a mistake. In this example you only have 2 commands: 'tb' and 'tbr', I run 'tb' and 'tbr' and 'tb', then crash. My CAD is 2012. It can give out the effect you show, except the crash..

        1. Kean Walmsley Avatar

          Thanks for clarifying. It looks like I developed this back in the AutoCAD 2011/2012 timeframe, so in theory it should work. I unfortunately don't have time to install and test with AutoCAD 2012, so unless you're able to reproduce the problem with AutoCAD 2016 then I can't spend more time looking at this. Have you tried running it from the debugger?

          Kean

          1. You're right, AutoCAD 2016 doesn't crash on it, while 2012 does :(. I run it from debugger, but cannot trace to the crash site. CAD just crashes on :
            _tb = new TransientBox(10); and I cannot trace into public TransientBox(double side).

            1. Kean Walmsley Avatar

              It might help to define a Dispose() method that disposes of the TextStyle (or you make _style public and make a call to _tb._style.Dispose() before the call to _tb.Dispose() in the TBR command).

              Kean

              1. The problem seem to be related to Transient.Dispose. I comment out all the code in TransientBox, but still crash.

Leave a Reply to Matthias Cancel reply

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