As promised in the last post and based on the overwhelming feedback in the one before that, today we’re starting a series on how to transform AutoCAD geometry.
Before developing a fancy modeless GUI to make this really easy, we need a base command that can do the hard work. What’s needed from our basic command is the following:
- Get a single entity from the pickfirst set (which will help us when calling the command from our modeless UI)
- If there isn’t one selected, ask the user for it
- Get the property name to transform
- Only for writeable Point3d and Vector3d properties
- The list of valid properties will be populated in our GUI, so we shouldn’t need much validation
- If none is entered, we just transform the whole entity
- Only for writeable Point3d and Vector3d properties
- Get the matrix contents as a comma-delimited string
- We’ll then decompose it into the 16 doubles required to define a Matrix3d
- Transform the property (or the whole entity) by the provided matrix
- We will use Reflection to get and set the Point3d/Vector3d property value
To understand some of the underlying concepts, let’s talk a little about transformation matrices.
We need 4 x 4 matrices when working in 3D space to allow us to perform a full range of transformations: translation, rotation, scaling, mirroring and projection. We could achieve some of these using 3 x 3 matrices, but some of these – particular translation, but probably some of the others (I’m not 100% certain of the specifics) – need the additional cells.
We’ll be looking into different transformation matrix types in more detail when we have a simple UI to play around with them, but for now let’s focus on a simple scaling matrix.
2 | 0 | 0 | 0 |
0 | 2 | 0 | 0 |
0 | 0 | 2 | 0 |
0 | 0 | 0 | 1 |
When we apply this transformation to an entity, it is basically used to multiply the relevant properties (and basically scales them by a factor of 2).
Let’s see what that means by applying this scaling transformation to the 3D point (5, 5, 0), which could be the centre point of a circle (for instance). We need to add a unit entry (1) to the point, to make it compatible with a 4 x 4 matrix.
2 | 0 | 0 | 0 | 5 | |
0 | 2 | 0 | 0 | * | 5 |
0 | 0 | 2 | 0 | 0 | |
0 | 0 | 0 | 1 | 1 |
Now if we follow the rules of matrix multiplication, we can see that our resultant point is calculated like this:
a | b | c | d | r | a*r + b*s + c*t + d*u | ||
e | f | g | h | * | s | = | e*r + f*s + g*t + h*u |
i | j | k | l | t | i*r + j*s + k*t + l*u | ||
n | o | p | q | u | n*r + o*s + p*t + q*u |
This page has a nice graphical representation of multiplying a matrix with a vector.
Which means for us, specifically:
2 | 0 | 0 | 0 | 5 | 10 + 0 + 0 + 0 | ||
0 | 2 | 0 | 0 | * | 5 | = | 0 + 10 + 0 + 0 |
0 | 0 | 2 | 0 | 0 | 0 + 0 + 0 + 0 | ||
0 | 0 | 0 | 1 | 1 | 0 + 0 + 0 + 1 |
And so our transformed point – which is the top three values of the resultant 4-cell matrix – is (10, 10, 0).
Now let’s see the C# code to transform an entity by a user-specified matrix:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Reflection;
namespace Transformer
{
public class Commands
{
[CommandMethod("TRANS", CommandFlags.UsePickSet)]
static public void TransformEntity()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Our selected entity (only one supported, for now)
ObjectId id;
// First query the pickfirst selection set
PromptSelectionResult psr = ed.SelectImplied();
if (psr.Status != PromptStatus.OK || psr.Value == null)
{
// If nothing selected, ask the user
PromptEntityOptions peo =
new PromptEntityOptions(
"\nSelect entity to transform: "
);
PromptEntityResult per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
id = per.ObjectId;
}
else
{
// If the pickfirst set has one entry, take it
SelectionSet ss = psr.Value;
if (ss.Count != 1)
{
ed.WriteMessage(
"\nThis command works on a single entity."
);
return;
}
ObjectId[] ids = ss.GetObjectIds();
id = ids[0];
}
PromptResult pr = ed.GetString("\nEnter property name: ");
if (pr.Status != PromptStatus.OK)
return;
string prop = pr.StringResult;
// Now let's ask for the matrix string
pr = ed.GetString("\nEnter matrix values: ");
if (pr.Status != PromptStatus.OK)
return;
// Split the string into its individual cells
string[] cells = pr.StringResult.Split(new char[] { ',' });
if (cells.Length != 16)
{
ed.WriteMessage("\nMust contain 16 entries.");
return;
}
try
{
// Convert the array of strings into one of doubles
double[] data = new double[cells.Length];
for (int i = 0; i < cells.Length; i++)
{
data[i] = double.Parse(cells[i]);
}
// Create a 3D matrix from our cell data
Matrix3d mat = new Matrix3d(data);
// Now we can transform the selected entity
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
Entity ent =
tr.GetObject(id, OpenMode.ForWrite)
as Entity;
if (ent != null)
{
bool transformed = false;
// If the user specified a property to modify
if (!string.IsNullOrEmpty(prop))
{
// Query the property's value
object val =
ent.GetType().InvokeMember(
prop, BindingFlags.GetProperty, null, ent, null
);
// We only know how to transform points and vectors
if (val is Point3d)
{
// Cast and transform the point result
Point3d pt = (Point3d)val,
res = pt.TransformBy(mat);
// Set it back on the selected object
ent.GetType().InvokeMember(
prop, BindingFlags.SetProperty, null,
ent, new object[] { res }
);
transformed = true;
}
else if (val is Vector3d)
{
// Cast and transform the vector result
Vector3d vec = (Vector3d)val,
res = vec.TransformBy(mat);
// Set it back on the selected object
ent.GetType().InvokeMember(
prop, BindingFlags.SetProperty, null,
ent, new object[] { res }
);
transformed = true;
}
}
// If we didn't transform a property,
// do the whole object
if (!transformed)
ent.TransformBy(mat);
}
tr.Commit();
}
}
catch (Autodesk.AutoCAD.Runtime.Exception ex)
{
ed.WriteMessage(
"\nCould not transform entity: {0}", ex.Message
);
}
}
}
}
Now let’s use the TRANS command to transform a couple of entities:
We’ll use TRANS to apply the above scaling transformation matrix to the whole circle and then to the EndPoint of the line:
Command: TRANS
Select entity to transform: <selected the circle>
Enter property name:
Enter matrix values: 2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,1
Command: TRANS
Select entity to transform: <selected the line>
Enter property name: EndPoint
Enter matrix values: 2,0,0,0,0,2,0,0,0,0,2,0,0,0,0,1
With these results:
I understand this is quite a tricky topic, so I’d appreciate your feedback: does this initial explanation help, at all? Does the level of detail work for you?
In the coming posts we’ll be looking at more complex transformation matrices – and using a GUI to play around with them – but hopefully this introductory post is a reasonably helpful start.