Sunday, November 29, 2020

Find Line's Intersection Points With Block (BlockReference)

 I recently replied a question posted in AutoCAD .NET discussion forum, in which I proposed a code workflow. Then another question in similar context came up. So I thought I might as well write some code to demonstrate my idea in the reply to the first question, which would indirectly answer the second question: after all, once the intersection points of line and the block reference are known, trimming the line would be simple next step.

Firstly to simplify the case, I limit the discussion only on Line and BlockReference with nested entities being Curve type only.

As we know all classes derived from Entity have overloaded method IntersectWith(), which can be used to find intersection points between 2 entities. BlockReference, being derived from Entity, inherently also has its IntersectWith() method. However, because BlockReference is a reference of a composite object with many different entities nested, its IntersectWtith() method is implemented in its own way: it uses its bounding box (GeometricExtents) as its boundary to calculate its intersection points with other entity. Following code demonstrate this:

#region Command to find entity's intersecting point with block: block's bounding box
 
[CommandMethod("BlkIntersect1")]
public static void TestBlockIntersection1()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                // create a rectangle polyline to show the block's GeometricExtents
                var boundBox = CreateBoundBox(blk);
                space.AppendEntity(boundBox);
                tran.AddNewlyCreatedDBObject(boundBox, true);
                tran.TransactionManager.QueueForGraphicsFlush();
 
                while (true)
                {
                    var eRes = ed.GetEntity(
                        "\nSelect an entity intersecting the block:");
                    if (eRes.Status == PromptStatus.OK)
                    {
                        FindBlockIntersectionPoint(eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void FindBlockIntersectionPoint(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
 
    var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
 
    var pts = new Point3dCollection();
    blk.IntersectWith(ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
 
    if (pts.Count > 0)
    {
        foreach (Point3d pt in pts)
        {
            var dbPt = new DBPoint(pt);
            space.AppendEntity(dbPt);
            tran.AddNewlyCreatedDBObject(dbPt, true);
            tran.TransactionManager.QueueForGraphicsFlush();
        }
    }
 
}
 
private static Polyline CreateBoundBox(BlockReference blk)
{
    var ext = blk.GeometricExtents;
    var poly = new Polyline(4);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.Closed = true;
    poly.ColorIndex = 2;
    return poly;
}
 
#endregion

The code draws a rectangle as the block's bounding box and draws point at intersecting point of a line and the block reference. As following video shows the intersection point is at the location where the line and the bounding box intersect to each other.


Now here is the code I proposed to the first and/or second question: find actual intersection point of a line to the block's "real" boundary (outmost entity nested in the block).

#region command to find entity's intersecting point: block's real boundary
 
[CommandMethod("BlkIntersect2")]
public static void TestBlockIntersection2()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                while (true)
                {
                    var opt = new PromptEntityOptions(
                        "\nSelect a line intersecting with the block:");
                    opt.SetRejectMessage("\nInvalid: not a line!");
                    opt.AddAllowedClass(typeof(Line), true);
                    var eRes = ed.GetEntity(opt);
                    if (eRes.Status == PromptStatus.OK)
                    {
                        GetBlockIntersectionPoints(
                            eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void GetBlockIntersectionPoints(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
    var line = (Line)tran.GetObject(entId, OpenMode.ForRead);
    if (FindOutmostIntersectingPoints(line, blk, out Point3d pt1, out Point3d pt2))
    {
        var dbPt = new DBPoint(pt1);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
 
        dbPt = new DBPoint(pt2);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
    }
}
 
private static bool FindOutmostIntersectingPoints(
    Line line, BlockReference blk, out Point3d pt1, out Point3d pt2)
{
    pt1 = Point3d.Origin;
    pt2 = Point3d.Origin;
 
    var points = GetAllIntersectingPoints(line, blk);
    if (points.Count>0)
    {
        pt1 = (from p in points
                orderby p.DistanceTo(line.StartPoint)
                select p).First();
 
        pt2 = (from p in points
                orderby p.DistanceTo(line.EndPoint)
                select p).First();
 
        return true;
    }
    else
    {
        return false;
    }
}
 
private static List<Point3d> GetAllIntersectingPoints(
    Line line, BlockReference blk)
{
    var points = new List<Point3d>();
 
    using (var ents = new DBObjectCollection())
    {
        blk.Explode(ents);
        foreach (DBObject o in ents)
        {
            var ent = (Entity)o;
            var pts = new Point3dCollection();
            line.IntersectWith(
                ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
            foreach(Point3d p in pts)
            {
                points.Add(p);
            }
            o.Dispose();
        }
    }
 
    return points;
}
 
#endregion

The video below show the code is able to find the 2 intersection points of a line that pass through a block reference, which fall on the block's outmost entity or entities. Obviously this indirectly answers the second question: with these 2 points, the line could ne trimmed easily.


As aforementioned, I limit the code to only apply to Line and block with only Curve type as nested entities. Extra considerations are needed in other cases, such as:

1. If a nested entity in block is a BlockReference, Text/MText/AttributeReference, then the intersection point is on their bounding box. Recursive exploding BlockReference might be needed.

2. If start or end point, or both the intersecting Line/Curve locate inside the block, it might be quite difficult to determine the start/end point is only inside the block's bounding box but outside the outmost entities, or not.

3. If the intersecting entity with block is not a Line, it could intersect with the block more than 2 times.

So writing code to cover all possible cases would be quite some work, if it is possible at all.

Update

As the comment pointed out, I forgot to post the code of Highlight(ObjectId, bool). Here is the code:

public static void Highlight(ObjectId entId, bool highlight)
{
    using (var tran = entId.Database.TransactionManager.StartOpenCloseTransaction())
    {
        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
        if (highlight)
        {
            ent.Highlight();
        }
        else
        {
            ent.Unhighlight();
        }
        tran.Commit();
    }
}

Tuesday, November 10, 2020

A User-Friendly Command To Copy Part of Polyline

 I recently replied a question asked in Autodesk's AutoCAD .NET API user forum, about how to copy part of Polyline. Naturally, I also wrote some quick test code to make sure my reply does make sense. Afterwards, I though why don't I polish the code a bit more to make it a user-friendly command, hence this post.

The operation requires 3 user inputs: a polyline, two points on the polyline. The output is a copy of the portion of this polyline between the 2 points.

We can easily split a Polyline (or a Curve in general) with Curve.GetSplitCurves(Point3dCollection). In our case, we just need to pass the 2 points as Point3DCollection to Curve.GetSplitCurves() method and pick out the portion of this polyline between the 2 points from the returned DBObjectCollection. However, there are a couple of things to be handled carefully:

1. The points in the argument Point3DCollection of GetSplitCurves() method must be in correct order: the points should be sorted according to its distance from the polyline's start point; When user selects 2 points on polyline, he/she can do the selection along different direction of the Polyline, thus the need of sorting.

2. User could select one of the Polyline's end (StartPoint or EndPoint). User could even choose to select the Polyline's StartPoint and EndPoint (i.e. selecting entire polyline), 

Here is a class PatialPolylineSelector that guide user to select a Polyline, then select 2 points on it. Once 2 points are selected, a non-database-residing Polyline (a clone of the portion of the selected Polyline between the 2 points) is returned to calling process. To make the point selection very user-friendly, when user moves mouse cursor for selecting second point, a ghost polyline is drawn as Transient Graphics to show the output portion of the Polyline.

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace GetPartialPolyline
{
    public class PartialPolylineSelector
    {
        private readonly Document _dwg;
        private readonly Editor _ed;
 
        private Point3d _firstPoint;
        private ObjectId _polylineId = ObjectId.Null;
 
        private TransientManager _tsManager = 
            TransientManager.CurrentTransientManager;
        private CadDb.Polyline _ghostPolyline = null;
 
        public PartialPolylineSelector(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
        }
 
        public CadDb.Polyline SelectPartialPolyline()
        {
            var polyId = SelectPolyline(out Point3d pickPt1);
            if (polyId.IsNull)
            {
                _ed.WriteMessage("\n*Cancel*");
                return null;
            }
 
            if (!SelectTwoPointsOnPolyline(
                polyId, pickPt1, 
                out Point3d startPt, out Point3d endPt))
            {
                _ed.WriteMessage("\n*Cancel*");
                return null;
            }
 
            var poly = GeneratePartialPolyline(polyId, startPt, endPt);
            return poly;
        }
 
        #region private methods
 
        private ObjectId SelectPolyline(out Point3d pickPoint)
        {
            pickPoint = Point3d.Origin;
            var opt = new PromptEntityOptions(
                "\nSelect a polyline:");
            opt.SetRejectMessage("\nInvalid: not a polyline!");
            opt.AddAllowedClass(typeof(CadDb.Polyline), true);
 
            var res = _ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                using (var tran = 
                    res.ObjectId.Database.TransactionManager.StartTransaction())
                {
                    var poly = (CadDb.Polyline)tran.GetObject(
                        res.ObjectId, OpenMode.ForRead);
                    //make sure the output point is on the polyline
                    pickPoint = poly.GetClosestPointTo(res.PickedPoint, false);
                    tran.Commit();
                }
 
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
 
        private bool SelectTwoPointsOnPolyline(
            ObjectId polyId, Point3d prevPoint,
            out Point3d startPt, out Point3d endPt)
        {
            startPt = Point3d.Origin;
            endPt = Point3d.Origin;
 
            _firstPoint = prevPoint;
            var nextPoint = Point3d.Origin;
            _polylineId = polyId;
 
            bool ok = false;
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var poly = (CadDb.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
 
                while (true)
                {
                    ClearGhostPolyline();
                    var opt = new PromptPointOptions(
                        "\nSelect second point on the polyline:")
                    {
                        AppendKeywordsToMessage = true,
                        AllowNone = true
                    };
                    opt.Keywords.Add("First point");
                    opt.Keywords.Default = "First point";
 
                    PromptPointResult res;
                    try
                    {
                        // when selecting another point on polyline
                        // show the part of polyline to be cloned
                        // as Transient Graphics
                        _ed.PointMonitor += Editor_PointMonitor;
 
                        res = _ed.GetPoint(opt);
                    }
                    finally
                    {
                        _ed.PointMonitor -= Editor_PointMonitor;
                        ClearGhostPolyline();
                    }
 
                    if (res.Status == PromptStatus.OK)
                    {
                        nextPoint = poly.GetClosestPointTo(res.Value, false);
                        ok = true;
                        break;
                    }
                    else if (res.Status == PromptStatus.Keyword)
                    {
                        // re-select the first point on polyline
                        var cancel = false;
                        while (true)
                        {
                            var op = new PromptPointOptions(
                                "\nSelect first point on polyline:");
                            var rs = _ed.GetPoint(op);
                            if (rs.Status == PromptStatus.OK)
                            {
                                _firstPoint = poly.GetClosestPointTo(rs.Value, false);
                                break;
                            }
                            else
                            {
                                cancel = true;
                                break;
                            }
                        }
 
                        if (cancel)
                        {
                            ok = false;
                            break;
                        }
                    }
                    else
                    {
                        ok = false;
                        break;
                    }
                }
 
                if (ok)
                {
                    SortPickedPoints(
                        poly, _firstPoint, nextPoint, out startPt, out endPt);
                }
 
                tran.Commit();
            }
 
            return ok;
        }
 
        private void SortPickedPoints(
            CadDb.Polyline poly, Point3d picked1, Point3d picked2,
            out Point3d startPt, out Point3d endPt)
        {
            var dist1 = poly.GetDistAtPoint(picked1);
            var dist2 = poly.GetDistAtPoint(picked2);
            if (dist1 < dist2)
            {
                startPt = picked1;
                endPt = picked2;
            }
            else
            {
                startPt = picked2;
                endPt = picked1;
            }
        }
 
        private CadDb.Polyline GeneratePartialPolyline(
            ObjectId polylineId, Point3d startPt, Point3d endPt)
        {
            CadDb.Polyline poly = null;
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var pline = (CadDb.Polyline)tran.GetObject(polylineId, OpenMode.ForRead);
 
                Point3dCollection points = new Point3dCollection();
                if (startPt.IsEqualTo(pline.StartPoint) &&
                    endPt.IsEqualTo(pline.EndPoint))
                {
                    poly = pline.Clone() as CadDb.Polyline;
                }
                else
                {
                    if (startPt.IsEqualTo(pline.StartPoint))
                    {
                        points.Add(endPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 2)
                        {
                            poly = dbObjects[0] as CadDb.Polyline;
                            dbObjects[1].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                    else if (endPt.IsEqualTo(pline.EndPoint))
                    {
                        points.Add(startPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 2)
                        {
                            poly = dbObjects[1] as CadDb.Polyline;
                            dbObjects[0].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                    else
                    {
                        points.Add(startPt);
                        points.Add(endPt);
 
                        var dbObjects = pline.GetSplitCurves(points);
                        if (dbObjects.Count == 3)
                        {
                            poly = dbObjects[1] as CadDb.Polyline;
                            dbObjects[0].Dispose();
                            dbObjects[2].Dispose();
                        }
                        else
                        {
                            foreach (DBObject obj in dbObjects)
                            {
                                obj.Dispose();
                            }
                        }
                    }
                }
 
                tran.Commit();
            }
 
            return poly;
        }
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearGhostPolyline();
 
            var poly = _polylineId.GetObject(OpenMode.ForRead) as CadDb.Polyline;
            var nextPoint = poly.GetClosestPointTo(e.Context.RawPoint, false);
            var dist = nextPoint.DistanceTo(e.Context.RawPoint);
            if (dist<poly.Length/10.0)
            {
                SortPickedPoints(
                    poly, _firstPoint, nextPoint,
                    out Point3d startPt, out Point3d endPt);
                var ghost = GeneratePartialPolyline(_polylineId, startPt, endPt);
                if (ghost!=null)
                {
                    _ghostPolyline = ghost;
                    _ghostPolyline.ColorIndex = 1;
                    _tsManager.AddTransient(
                        _ghostPolyline, 
                        TransientDrawingMode.DirectTopmost, 
                        128, 
                        new IntegerCollection());
                }
 
                e.AppendToolTipText(
                    $"Selected polyline length = {_ghostPolyline.Length}");
            }
        }
 
        private void ClearGhostPolyline()
        {
            if (_ghostPolyline!=null)
            {
                _tsManager.EraseTransient(
                    _ghostPolyline, new IntegerCollection());
                _ghostPolyline.Dispose();
            }
        }
 
        #endregion
    }
}


This the CommandClass to run the process:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(GetPartialPolyline.MyCommands))]
 
namespace GetPartialPolyline
{
    public class MyCommands
    {
        [CommandMethod("PartialPoly")]
        public static void CreatePartialPolyline()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var selector = new PartialPolylineSelector(dwg);
                var poly = selector.SelectPartialPolyline();
                if (poly!=null)
                {
                    poly.ColorIndex = 2;
                    AddPolylineToDb(dwg.Database, poly);
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nExecution error:\n{ex.Message}\n");
            }
        }
 
        private static void AddPolylineToDb(Database db, Polyline poly)
        {
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var space = (BlockTableRecord)tran.GetObject(
                    db.CurrentSpaceId, OpenMode.ForWrite);
                space.AppendEntity(poly);
                tran.AddNewlyCreatedDBObject(poly, true);
                tran.Commit();
            }
 
        }
    }
}

This the video clip showing the command execution effect: