Thanks for Balaji Ramamoorthy – who recently joined our team in India – and Adam Nagy for helping generate the code for this post.
There are lots of reasons people might want to tessellate a 3D solid in AutoCAD to generate a mesh. The code in today’s post uses the Boundary Representation (Brep) API in AutoCAD to do just that, generating a set of 3D faces.
A few points about the implementation:
- I’ve only made a small number of settings to control the mesh generation: more are available for you to experiment with.
- It should be simple enough to generate a SubDMesh, rather than a set of faces, but that’s left as an exercise for the reader.
- I only tested with a very simple solid (a sphere). The settings may need tweaking depending on the complexity of the solid you’re meshing.
Here’s the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.BoundaryRepresentation;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
namespace MeshSolid
{
public class MeshCreator
{
// Mesh a selected solid
[CommandMethod("SOLMESH")]
static public void MeshFromSolid()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Ask the user to select a solid
PromptEntityOptions peo =
new PromptEntityOptions("Select a 3D solid");
peo.SetRejectMessage("\nA 3D solid must be selected.");
peo.AddAllowedClass(typeof(Solid3d), true);
PromptEntityResult per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
Transaction tr =
db.TransactionManager.StartTransaction();
using (tr)
{
BlockTable bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead,
false
);
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite,
false
);
Solid3d sol =
tr.GetObject(
per.ObjectId,
OpenMode.ForRead
) as Solid3d;
// Calculate the approximate size of our solid
double length =
sol.GeometricExtents.MinPoint.GetVectorTo(
sol.GeometricExtents.MaxPoint
).Length;
try
{
using (Brep brp = new Brep(sol))
{
// Create and set our mesh control object
using (Mesh2dControl mc = new Mesh2dControl())
{
// These settings seem extreme, but only result
// in ~500 faces for a sphere (during my testing,
// anyway). Other control settings are available
mc.MaxNodeSpacing = length / 10000;
mc.MaxSubdivisions = 100000000;
// Create a mesh filter object
using (Mesh2dFilter mf = new Mesh2dFilter())
{
// Use it to map our control settings to the Brep
mf.Insert(brp, mc);
// Generate a mesh using the filter
using (Mesh2d m = new Mesh2d(mf))
{
// Extract individual faces from the mesh data
foreach (Element2d e in m.Element2ds)
{
Point3dCollection pts = new Point3dCollection();
foreach (Node n in e.Nodes)
{
pts.Add(n.Point);
n.Dispose();
}
e.Dispose();
// A face could be a triangle or a quadrilateral
// (the Booleans indicate the edge visibility)
AcDb.Face face = null;
if (pts.Count == 3)
face =
new AcDb.Face(
pts[0], pts[1], pts[2],
true, true, true, true
);
else if (pts.Count == 4)
face =
new AcDb.Face(
pts[0], pts[1], pts[2], pts[3],
true, true, true, true
);
// If we have a valid face, add it to the
// database and the transaction
if (face != null)
{
// Make each face yellow for visibility
face.ColorIndex = 2;
btr.AppendEntity(face);
tr.AddNewlyCreatedDBObject(face, true);
}
}
}
}
}
}
tr.Commit();
}
catch (System.Exception ex)
{
ed.WriteMessage("Exception: {0}", ex.Message);
}
}
}
}
}
Here’s what happens when we run the SOLMESH command, selecting a simple sphere (which I later moved to the side, for comparison):
And now with the realistic visual style set, just to show we have opacity:
Update:
As mentioned in the comments, the code was randomly crashing on larger models. Viru Aithal and Balaji Ramamoorthy tracked down the problem, which is due to a common issue with AutoCAD objects: the enumerators for Mesh2dElement2dCollection and Element2dNodeCollection both return newly-created objects (a Node and an Element2d, respectively) which need to be disposed of before the .NET Finalizer (which works on a background thread) attempts to do so for you. Two lines (with calls to n.Dispose() and e.Dispose()) have been added to the above code to address this issue.