When I woke up this morning I didn’t expect to write a post on this topic, but then I found a kind email in my inbox from an old friend and colleague, Ishwar Nagwani, with some code he’d written and wanted to see posted. Ishwar had generated the test code in response to the following question from a member of the ADN team:
This question is to get the corresponding Z of a surface/solid, given a point of XY. This refers to the absolute coordinate, instead of the point on param space. I seem not to find a direct way except create a line along Z axis from XY and calculate the intersect point with the surface.
The question is really about letting the user provide a point in X,Y coordinates (presumably relative to the current UCS) and have the code detect the Z value that is needed for the point to be on a given surface.
Ishwar’s observation was that the Surface.RayTest() method could be used to determine this “height” value. He put together some code that allows you to select lines that are normal to the X,Y plane you care about and for the resultant point to be created on the selected surface. The code also shows how to highlight the portion of the surface containing the calculated point.
Here’s Ishwar’s C# code, although I’ve ended up reworking pieces of it (largely to fit the blog but also to simplify it somewhat):
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using AcDb = Autodesk.AutoCAD.DatabaseServices;
namespace SurfaceIntersection
{
public class Commands
{
[CommandMethod("SURFRAY")]
public void SurfaceRay()
{
var doc = Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
// Ask the user to pick a surface
var peo = new PromptEntityOptions("\nSelect a surface");
peo.SetRejectMessage("\nMust be a surface.");
peo.AddAllowedClass(typeof(AcDb.Surface), false);
var per = ed.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return;
var sf =
new SelectionFilter(
new TypedValue[] {
new TypedValue((int)DxfCode.Start, "LINE")
}
);
// Ask the user to select some lines
var pso = new PromptSelectionOptions();
pso.MessageForAdding = "Select lines";
pso.MessageForRemoval = "Remove lines";
var psr = ed.GetSelection(pso, sf);
if (psr.Status == PromptStatus.OK && psr.Value.Count > 0)
{
using (var tr = db.TransactionManager.StartTransaction())
{
// We'll start by getting the block table and modelspace
// (which are really only needed for results we'll add to
// the database)
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
var btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite
);
// Next we'll get our surface object
var obj = tr.GetObject(per.ObjectId, OpenMode.ForRead);
var surf = obj as AcDb.Surface;
if (surf == null)
{
// Should never happen, but anyway
ed.WriteMessage("\nFirst object must be a surface.");
}
else
{
DoubleCollection heights;
SubentityId[] ids;
// Fire ray for each selected line
foreach (var id in psr.Value.GetObjectIds())
{
var ln = tr.GetObject(id, OpenMode.ForRead) as Line;
if (ln != null)
{
surf.RayTest(
ln.StartPoint,
ln.StartPoint.GetVectorTo(ln.EndPoint),
0.01,
out ids,
out heights
);
if (ids.Length == 0)
{
ed.WriteMessage("\nNo intersections found.");
}
else
{
// Highlight each subentity and add point
// at intersection
for (int i = 0; i < ids.Length; i++)
{
var subEntityPath =
new FullSubentityPath(
new ObjectId[] { per.ObjectId },
ids[i]
);
// Highlight the sub entity
surf.Highlight(subEntityPath, true);
// Create a point at the line-surface
// intersection
var pt =
new DBPoint(
new Point3d(
ln.StartPoint.X,
ln.StartPoint.Y,
heights[i]
)
);
// Add the new object to the block table
// record and the transaction
btr.AppendEntity(pt);
tr.AddNewlyCreatedDBObject(pt, true);
}
}
}
}
}
tr.Commit();
}
}
}
}
}
Now there’s a reason for this post being labeled “Part 1”: the above code makes a number of deliberate assumptions, such as the fact that the lines the user selects (to determine the start point and direction of the ray we’ll use to test the surface) have to be normal to the WCS. The code is really to make sure the RayTest() method can be used effectively to determine the Z value of the intersection of the line and the surface. The code clearly won’t work on lines that aren’t normal to the X,Y plane of the WCS, but then it’s not supposed to.
We’ll look at implementing something that’s more robust – that allows the user to select an X,Y point in the current UCS and then draw a line between the selected point and the intersection of the surface and the ray cast from that point relative to the current UCS – in the next part in this series.
In the meantime, here’s the SURFRAY command in action. Here’s a basic surface Ishwar provided with some intersecting lines (which are, of course, normal to the plane of the WCS):
When we run the SURFRAY command and select the surface and the leftmost line, we see the surface segment in which they intersect gets highlighted (there’s no unhighlight command: you’ll probably need to switch visual styles to and from “2D Wireframe” to remove the highlighting from a 3D view). A point has also been created at the intersection point, but you’d need incredibly good eyes to detect that:
So, then, next time we’ll implement a command that asks the user to select first a surface and then a number of points that are “below” the surface, which will lead not only to the intersecting point being created but a line leading from it back to the originally selected point. All working in the currently active UCS, of course.