A big thanks to Stephen Preston, who manages DevTech Americas and coordinates our worldwide AutoCAD workgroup as well as spending time working with the AutoCAD Engineering team (phew!), for providing this sample. Stephen originally put it together for our annual Developer Days tour late last year: I took the original sample, converted it from VB.NET to C# and made some minor changes to the code. The VB.NET version is available from the ADN website, in case.
The Free-Form Design feature in AutoCAD 2010 is one of the coolest enhancements to the product (I really like the Parametric Drawing feature, too, although as the API for that is currently C++-only it’s unfortunately going to get a little less air-time on this blog). This post looks at how to automate free-form design operations: starting with a traditional Solid3d object, converting it to a sub-division mesh (SubDMesh) which we then manipulate in a number of interesting ways before converting back to a Solid3d.
Here’s the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Colors;
using System.Collections.Generic;
using System;
namespace FreeformModeling
{
public class Commands
{
// Add a new mesh to the current space,
// setting it to a sphere primitive -
// no smoothing.
[CommandMethod("SPHM")]
public void SphericalMesh()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
SubDMesh mySubD = new SubDMesh();
mySubD.SetSphere(20, 8, 8, 0);
mySubD.SetDatabaseDefaults();
btr.AppendEntity(mySubD);
tr.AddNewlyCreatedDBObject(mySubD, true);
tr.Commit();
}
}
// Make clone of solid, inflate it by a fraction of its
// extents, create a mesh from the clone, erase the clone.
private ObjectId cloneAndInflateSolid(
Transaction tr, ObjectId solidId
)
{
// Now copy the solid
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
ObjectId newId;
// Inflate solid by user specified fraction
// (0.05 works well with sample drawing)
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nSpecify inflation as fraction of bounds:"
);
PromptDoubleResult pdr =
ed.GetDouble(pdo);
// Return if something went wrong.
if (pdr.Status != PromptStatus.OK)
return ObjectId.Null;
double frac = pdr.Value;
ObjectIdCollection ids = new ObjectIdCollection();
ids.Add(solidId);
IdMapping im = new IdMapping();
db.DeepCloneObjects(ids, db.CurrentSpaceId, im, false);
newId = im.Lookup(solidId).Value;
// We now have the ObjectID of the newly cloned
// solid in newId
// Inflate by length of diagonal vector across bounds
// multiplied by inflation fraction
Solid3d sol =
(Solid3d)tr.GetObject(newId, OpenMode.ForWrite);
Extents3d ext = (Extents3d)sol.Bounds;
Vector3d vec = ext.MaxPoint - ext.MinPoint;
sol.OffsetBody(vec.Length * frac);
// Define params governing mesh generation algorithm
// (See ObjectARX helpfiles for explanation of params)
MeshFaceterData fd =
new MeshFaceterData(
0.01 * vec.Length,
40 * Math.PI / 180,
2, 2, 15, 5, 5, 0
);
// Get the mesh data from the solid
// (with smoothing level 1).
MeshDataCollection mdc =
SubDMesh.GetObjectMesh(sol, fd);
// Create the mesh defined by this data.
SubDMesh sd = new SubDMesh();
sd.SetDatabaseDefaults();
sd.SetSubDMesh(mdc.VertexArray, mdc.FaceArray, 1);
// Add the mesh to database.
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
newId = btr.AppendEntity(sd);
tr.AddNewlyCreatedDBObject(sd, true);
// Erase the cloned solid
sol.Erase();
return newId;
}
// Add creases to user selected faces
// Returns true if successful
private bool addCreases(
Transaction tr, ObjectId meshId
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
PromptSelectionOptions pso =
new PromptSelectionOptions();
pso.ForceSubSelections = true;
pso.AllowDuplicates = false;
pso.MessageForAdding = "\nSelect edges to crease: ";
PromptSelectionResult selRes =
doc.Editor.GetSelection(pso);
// If the user didn't make valid selection, we return
if (selRes.Status != PromptStatus.OK)
return false;
SelectionSet ss = selRes.Value;
// They may have picked more than one object - we need
// the subobjects of the object with the right ObjectId
SelectedObject so = null;
foreach (SelectedObject o in ss)
{
if (o.ObjectId == meshId)
so = o;
}
// If they picked on the wrong object, then just don't
// add any extrusions
if (so == null)
return false;
// We got to here so selection was valid
// This will store selected edges
List<FullSubentityPath> creasePaths =
new List<FullSubentityPath>();
DBObject obj =
tr.GetObject(so.ObjectId, OpenMode.ForRead);
SubDMesh sd = obj as SubDMesh;
if (sd != null)
{
sd.UpgradeOpen();
// Add all selected subentities to the collection
SelectedSubObject[] subEnts =
so.GetSubentities();
foreach (SelectedSubObject subEnt in subEnts)
{
if (subEnt.FullSubentityPath.SubentId.Type
== SubentityType.Edge)
creasePaths.Add(subEnt.FullSubentityPath);
}
// Add infinite creases to all those edges
// -1 means 'always' crease
sd.SetCrease(creasePaths.ToArray(), -1);
return true;
}
return false;
}
// Extrude each mesh face by a random amount, up to a
// user specified maximum. Returns true if successful.
private bool extrudeFaces(
Transaction tr, ObjectId meshId
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
// Now we want to stretch a few faces to add a bit of
// randomness. Better just stretch outwards.
// Prompt for max random bump (0.02 works well for
// sample drawing).
PromptDoubleOptions pdo =
new PromptDoubleOptions(
"\nSpecify bump size as fraction of bounds: "
);
PromptDoubleResult pdr =
doc.Editor.GetDouble(pdo);
// If user cancels value entry, then we return.
if (pdr.Status != PromptStatus.OK)
return false;
double extrudelen = pdr.Value;
PromptSelectionOptions pso =
new PromptSelectionOptions();
pso.ForceSubSelections = true;
pso.MessageForAdding = "\nSelect faces to bump: ";
PromptSelectionResult psr =
doc.Editor.GetSelection(pso);
// If user cancels selection, then we assume they want
// to continue, but that they don't want to extrude any
// surfaces
if (psr.Status != PromptStatus.OK)
return false;
SelectionSet ss = psr.Value;
// They may have picked more than one object - we need
// the subobjects of the object with the right ObjectId
SelectedObject so = null;
foreach (SelectedObject o in ss)
{
if (o.ObjectId == meshId)
so = o;
}
if (so == null)
return false;
// If they picked on the wrong object, then just don't
// add any extrusions
// We got to here so selection was valid
// Stores faces we selected for random extrusion
List<FullSubentityPath> bumpPaths =
new List<FullSubentityPath>();
DBObject obj =
tr.GetObject(so.ObjectId, OpenMode.ForRead);
SubDMesh sd = obj as SubDMesh;
if (sd != null)
{
sd.UpgradeOpen();
SelectedSubObject[] subEnts =
so.GetSubentities();
// We'll be extruding faces by a random amount, so
// instantiate random number generator here ready
// to use inside loop
Random rnd = new Random();
// Process each selected face in turn
foreach (SelectedSubObject subEnt in subEnts)
{
// Is it a face?
if (subEnt.FullSubentityPath.SubentId.Type
== SubentityType.Face)
{
FullSubentityPath[] faces =
new FullSubentityPath[1];
faces[0] = subEnt.FullSubentityPath;
// Find normal for this face
Plane fPlane =
sd.GetFacePlane(faces[0].SubentId);
Vector3d norm = fPlane.Normal;
// Get length of diagonal across bounds to use
// in calculating bump scale
Extents3d ext = (Extents3d)sd.Bounds;
Vector3d vec = ext.MaxPoint - ext.MinPoint;
Matrix3d mat =
Matrix3d.Displacement(
norm * 0.5 * extrudelen *
rnd.NextDouble() * vec.Length
);
sd.TransformSubentityPathsBy(faces, mat);
}
}
return true;
}
return false;
}
// Set random color for each mesh face.
private void colorFaces(
Transaction tr, ObjectId meshId
)
{
Random rnd = new Random();
DBObject obj =
tr.GetObject(meshId, OpenMode.ForRead);
SubDMesh sd = obj as SubDMesh;
if (sd != null)
{
sd.UpgradeOpen();
// Iterate all faces and set to a random color
for (int i = 0; i <= sd.NumberOfFaces - 1; i++)
{
SubentityId sId =
new SubentityId(SubentityType.Face, i);
byte[] rgb = new byte[3];
rnd.NextBytes(rgb);
Color col =
Color.FromRgb(rgb[0], rgb[1], rgb[2]);
sd.SetSubentColor(sId, col);
}
}
}
private ObjectId convertToSolid(
Transaction tr, ObjectId meshId
)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
ObjectId newId = ObjectId.Null;
DBObject obj =
tr.GetObject(meshId, OpenMode.ForRead);
SubDMesh sd = obj as SubDMesh;
if (sd != null)
{
sd.UpgradeOpen();
// ConvertToSolid will throw an exception if
// it can't create a solid from the mesh.
Solid3d sol = null;
try
{
sol = sd.ConvertToSolid(true, true);
}
catch
{
ed.WriteMessage(
"\nMesh was too complex to turn into a solid."
);
}
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
OpenMode.ForWrite
);
newId = btr.AppendEntity(sol);
tr.AddNewlyCreatedDBObject(sol, true);
}
return newId;
}
// Translates entity along x-axis by factor multiplied by
// the entity's extent along the x-axis.
private void translateEntity(
Transaction tr, ObjectId objId, double factor
)
{
Entity ent =
(Entity)tr.GetObject(
objId,
OpenMode.ForWrite
);
Extents3d ext = (Extents3d)ent.Bounds;
Point3d pt1 =
new Point3d(ext.MaxPoint.X, ext.MinPoint.Y, ext.MinPoint.Z);
Vector3d transVec =
factor * (pt1 - ext.MinPoint);
Matrix3d mat = Matrix3d.Displacement(transVec);
ent.TransformBy(mat);
}
// Creates a SubDMesh based on an 'inflated' clone of the
// selected solid. The user then gets to add creases and
// select which faces will be given a random extrusion.
// It colors each face of the mesh and then converts it to
// a smooth solid.
[CommandMethod("CC")]
public void CreateCasing()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
ObjectId meshId;
// First select the solid (just one)
PromptEntityOptions peo =
new PromptEntityOptions("\nSelect a solid: ");
peo.SetRejectMessage("\nObject must be a 3D solid.");
peo.AddAllowedClass(typeof(Solid3d), false);
PromptEntityResult per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
meshId =
cloneAndInflateSolid(tr, per.ObjectId);
// If the above call returned a null ObjectId,
// then something went wrong - we need to stop now
if (meshId == ObjectId.Null)
return;
// Now we have our original solid and the mesh
// created from the cloned solid
// Add creases
if (!addCreases(tr, meshId))
return;
// Randomly extrude faces
if (!extrudeFaces(tr, meshId))
return;
// Give faces random colors
colorFaces(tr, meshId);
// Convert the mesh to a solid.
ObjectId solId =
convertToSolid(tr, meshId);
// Move the entities so they don't sit on
// top of one other
translateEntity(tr, meshId, 1.5);
translateEntity(tr, solId, 3);
doc.TransactionManager.QueueForGraphicsFlush();
tr.Commit();
}
}
}
}
Here’s a sample 3D model – consisting of a single, composite 3D solid:
Here’s what happens when we run the CC command, selecting this solid and applying the recommended values (0.2 – i.e. 20% – for the inflation percentage and 0.05 – i.e. 5% – for the maximum random bump). I selected a number of edges and faces at random after which the code assigned random colours to all the faces, so please don’t get frustrated trying to reproduce these exact results. :-)
The objects are (going from right to left):
- The original solid.
- A sub-division mesh created by inflating the original solid and applying creases to certain edges and extruding certain faces.
- A traditional AutoCAD solid created from the sub-division mesh.
The best way to understand the overall functionality of the application is to scan the CC command – defined by the CreateCasing function found at the bottom of the listing – and then looking into individual functions it uses. The application also defines another command – SPHM – which shows how to create a simple, spherical sub-division mesh.