Today, we are going to take a look at the ‘ExtractBorder()’ method defined in the ITerrainSurface interface implemented by ‘TinSurface’ and ‘GridSurface’. The method is very basic. Similarly to the ‘ExtractGridded()’ method, ‘ExtractBorder()’ returns an ‘ObjectIdCollection’ with ids pointing to ‘Polyline3d’ objects that represent the surface border. The reason a collection is returned is because a surface may have areas in between which are not part of the surface. Take a look at the following screen shot.
What you see in the screenshot is a single surface representing two islands. If you were to select the surface, you will see both quadrilaterals are selected and are part of the same surface (you can find this drawing, ‘islands.dwg’ posted with the source). In a scenario like this, the border is represented by two polylines (Polyline3d objects).
Finding the Closest Surface Point to a Point Outside of the Surface
The Surfaces API provides several properties and methods to retrieve information about points in a surface. You can find the elevation at a specified X/Y values. You can find a triangle, point, or edge at a specified X/Y, etc. But most of these methods will throw an exception if the point is not located on top of the surface, which makes more difficult to find surface information from external points.
Augusto Goncalves wrote a wonderful post explaining how you can find the closest edge at an X/Y. His method first uses ‘FindEdgeAtXY()’ in case the point is on top of the surface, and if that fails, then searches for the edge himself.
I got some reports of performance issues with Augusto’s implementation, so I started investigating and finally, I talked to Dr. John Lewis, who around this town is known as “The Surface Master”. Jokes aside, John has worked on the surface engine for many years, and if there is someone who knows about its performance and best approaches is him. As he explained to me, the Surface engine used by Civil 3D is optimized for operations that work inside of the surface. but when you try to do the work yourself and support other functionality, it may be worth it to use other means. He gave me the idea for the implementation below.
DISCLAIMER: I have not compared performance of Augusto’s implementation vs. the code I will show below, but I have not seen any significant performance impact in my implementation. Also, the results are not exactly the same, so keep reading.
The first difference between the solution I will provide and Augusto’s is that I do not return a ‘TinSurfaceEdge’ object; I just return a ‘Point3d’. You have to take this into account and evaluate it in the context of the problem you are trying to solve. With my result, you can still find the edge using the existing API if you really need the edge object; although, you may have to adjust by some tolerance value, but I’ll leave that as an exercise to the reader.
The second difference is that my implementation assumes the point is not on top of the surface. If you know the point is outside of the surface, this is a straight shot at it. Otherwise, you can combine Augusto’s implementation with mine to write a function that tests for both.
Let me start by showing you the code, and then I will discuss the implementation.
C#
[CommandMethod("CDS_FindClosestPointOnSurface")]
public void CDS_FindClosestPointOnSurface()
{
ObjectId surfaceId = promptForTinSurface();
if (surfaceId.IsNull)
{
write("\nNo TIN Surface selected.");
return;
}
PromptPointResult result = _editor.GetPoint(
"\nSelect point outside surface: ");
if (result.Status != PromptStatus.OK)
{
write("\nNo point selected.");
return;
}
Point3d selectedPoint = result.Value;
Point3d closestPointFound = Point3d.Origin;
double shortestDistanceSoFar = Double.MaxValue;
using (Transaction tr = startTransaction())
{
TinSurface surface = surfaceId.GetObject(OpenMode.ForRead)
as TinSurface;
write("\nSelected surface: " + surface.Name);
write("\nSelected point: " + selectedPoint.ToString());
ObjectIdCollection borders = surface.ExtractBorder(
SurfaceExtractionSettingsType.Model);
foreach (ObjectId borderId in borders)
{
Polyline3d border = borderId.GetObject(OpenMode.ForRead)
as Polyline3d;
Point3d closestToBorder =
border.GetClosestPointTo(selectedPoint, false);
double distance = selectedPoint.DistanceTo(closestToBorder);
if (distance < shortestDistanceSoFar)
{
closestPointFound = closestToBorder;
shortestDistanceSoFar = distance;
}
}
}
write("\nClosest point found: " + closestPointFound.ToString());
using (Transaction tr = startTransaction())
{
BlockTableRecord btr = tr.GetObject(_database.CurrentSpaceId,
OpenMode.ForWrite) as BlockTableRecord;
Line line = new Line(selectedPoint, closestPointFound);
btr.AppendEntity(line);
tr.AddNewlyCreatedDBObject(line, true);
tr.Commit();
}
}
VB.NET
<CommandMethod("CDS_FindClosestPointOnSurface")> _
Public Sub CDS_FindClosestPointOnSurface()
Dim surfaceId As ObjectId = promptForTinSurface()
If surfaceId.IsNull Then
write(vbLf & "No TIN Surface selected.")
Return
End If
Dim result As PromptPointResult = _editor.GetPoint(
vbLf & "Select point outside surface: ")
If result.Status <> PromptStatus.OK Then
write(vbLf & "No point selected.")
Return
End If
Dim selectedPoint As Point3d = result.Value
Dim closestPointFound As Point3d = Point3d.Origin
Dim shortestDistanceSoFar As Double = [Double].MaxValue
Using tr As Transaction = startTransaction()
Dim surface As TinSurface = TryCast(
surfaceId.GetObject(OpenMode.ForRead), TinSurface)
write(vbLf & "Selected surface: " & surface.Name)
write(vbLf & "Selected point: " & selectedPoint.ToString())
Dim borders As ObjectIdCollection = surface.ExtractBorder(
SurfaceExtractionSettingsType.Model)
For Each borderId As ObjectId In borders
Dim border As Polyline3d = TryCast(
borderId.GetObject(OpenMode.ForRead), Polyline3d)
Dim closestToBorder As Point3d =
border.GetClosestPointTo(selectedPoint, False)
Dim distance As Double = selectedPoint.DistanceTo(closestToBorder)
If distance < shortestDistanceSoFar Then
closestPointFound = closestToBorder
shortestDistanceSoFar = distance
End If
Next
End Using
write(vbLf & "Closest point found: " & closestPointFound.ToString())
Using tr As Transaction = startTransaction()
Dim btr As BlockTableRecord = TryCast(tr.GetObject(
_database.CurrentSpaceId, OpenMode.ForWrite), BlockTableRecord)
Dim line As New Line(selectedPoint, closestPointFound)
btr.AppendEntity(line)
tr.AddNewlyCreatedDBObject(line, True)
tr.Commit()
End Using
End Sub
The command prompts the user to select a surface and a external point. Because the point is not on top of the surface, we know the closest point is going to be at the border, so we use ‘ExtractBorder()’, which is a very efficient operation executed in the surface engine, to collect the ‘Polyline3d’ objects representing the border.
Once we have the ids for the ‘Polyline3d’ objects, we open them and use the ‘GetClosestPointTo()’ method to find the closest point between the ‘Polyline3d’ and our specified point.
Then, we use ‘Point3d.DistanceTo()’ to calculate the distance between the two points, and if this is the shortest distance so far, we keep that value and cache the point found.
Once we have processed all the borders, ‘closestPointFound’ will contain the closest point and ‘shortestDistanceSoFar’ will contain the distance to that point. The command adds a line between this point and the one we specified, so we can easily see the result.
The performance, even with large surfaces, is stellar, so give it a shot and let me know if you have any questions or issues. As always, you can download the entire source code from BitBucket.
Comments
You can follow this conversation by subscribing to the comment feed for this post.