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