Monday, July 24, 2023

Selecting Split Point of a Line with Visual Aid

Background

This article is inspired by a question asked in Autodesk .NET API discussion forum. The OP expect to use a JIG as visual aid for user to select a point on a Polyline so that the Polyline could be split into 2. Obviously, selecting a point on Polyline itself is really simple, especially if OSNAP is enabled. But to select a point accurately that makes business sense, it would be better for the command to provide sufficient visual hints about where the expected point should be and make the accurate pick easy. In the case of the posted question, the OP wants to show the length of potentially split 2 segments where user moves the mouse to pick point.

While this surely can be done with custom Jig class (likely derived from DrawJig), I thought it would be as simple as wrap the Editor.GetPoint() in between with add/removing Editor.PointMonitor event handler. After all, the nature of the operation is to get an accurate split point on the Polyline. Once an expected split point is obtained, use would not have to worry how the Polyline gets split.

Note: the OP not only wants to split the Polyline into 2 segments, but also makes the 2 segments overlap to each other in a given range. However, for simplicity, in this article, I ignore the "overlap" requirement and only focus on how to make helpful, user-friendly visual aid while user is trying to select the split point.

Design of The Code

Firstly, I make some assumptions to make the code sample simple:

  • the entity to be split is a Line
  • user intends to select the split point based on the center point of the Line
  • the desired/possible split point would be apart from center point with given gap incrementally 
Therefore, the goals of my code are to achieve are:
  • Ask user to select the target Line
  • Once a Line entity is selected, the center point of the Line and potential split points along the Line, before and after the center point is visually shown
  • While user moved the mouse, one the potential split point that is closest to the mouse cursor would be visually prompted
  • When the user clicks the mouse, the mouse cursor does not need to be accurately at the prompted split point, thus the point selecting is really user-friendly
The Code

The class LineSpliter:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
using System.Collections.Generic;
using System.Linq;
 
namespace DragToSplitLine
{
    public class LineSplitter : IDisposable
    {
        private Document _dwg;
        private Editor _ed;
        private ObjectId _lineId = ObjectId.Null;
 
        private Line _line1 = null;
        private Line _line2 = null;
        private List<DBPoint> _incrementPoints = null;
        private DBText _text1 = null;
        private DBText _text2 = null;
        private Point3d _splitPoint = Point3d.Origin;
        private double _increment = 0.0;
        private double _textHeight = 1.0;
 
        private TransientManager _tsManager = TransientManager.CurrentTransientManager;
 
        public LineSplitter(Document dwg, ObjectId lineId)
        {
            _dwg= dwg;
            _ed = _dwg.Editor;
            _lineId= lineId;
        }
 
        public void Dispose()
        {
            ClearTransientDrawables();
            
        }
 
        public Point3d? DragSplitter()
        {
            var ok = false;
            var originalPdMode = Convert.ToInt32(Application.GetSystemVariable("PDMODE"));
            var originalPdSize = Convert.ToDouble(Application.GetSystemVariable("PDSIZE"));
            try
            {
                // Set DBPoint display mode
                Application.SetSystemVariable("PDMODE", 2);
                Application.SetSystemVariable("PDSIZE", -1.0);
 
                using (var tran = _dwg.TransactionManager.StartTransaction())
                {
                    var line = (Line)tran.GetObject(_lineId, OpenMode.ForRead);
 
                    // Get initial increment, which could be any value
                    _increment = (Math.Floor(line.Length / 10) * 10) / 20;
                    _textHeight = line.Length / 100;
                    CreateTransientDrawables(line);
                    
                    do
                    {
                        var changeIncrement = false;
                        try
                        {
                            AddTransientDrawables();
                            _ed.PointMonitor += Editor_PointMonitor;
 
                            var opt = new PromptPointOptions(
                                "\nSelect split point on selected line, or change increment:");
                            opt.AllowNone = true;
                            opt.Keywords.Add("Increment");
                            opt.Keywords.Default = "Increment";
 
                            var res = _ed.GetPoint(opt);
                            if (res.Status == PromptStatus.OK)
                            {
                                ok = true;
                                break;
                            }
                            else if (res.Status == PromptStatus.Keyword)
                            {
                                changeIncrement = true;
                            }
                            else
                            {
                                break;
                            }
                        }
                        finally
                        {
                            ClearTransientDrawables();
                            _ed.PointMonitor -= Editor_PointMonitor;
                        }
 
                        if (changeIncrement)
                        {
                            if (ChangeIncrement(out double inc))
                            {
                                _increment = inc;
                                ClearTransientDrawables();
                                CreateTransientDrawables(line);
                            }
                            else
                            {
                                break;
                            }
                        }
 
                    } while (true);
 
                    tran.Commit();
                }
            }
            finally
            {
                // Restore DBPoint diaplay mode
                Application.SetSystemVariable("PDMODE", originalPdMode);
                Application.SetSystemVariable("PDSIZE", originalPdSize);
                _ed.UpdateScreen();
            }
 
            if (ok)
            {
                return _splitPoint;
            }
            else
            {
                return null;
            }
        }
 
        #region private methods
 
        private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            _splitPoint = GetSplitPoint(e.Context.RawPoint);
            SetLines();
            SetLengthTexts();
            UpdateTransientDrawables();
        }
 
        private void CreateTransientDrawables(Line line)
        {
            _incrementPoints = new List<DBPoint>();
 
            // Get center point
            var pt = line.GetPointAtDist(line.Length / 2.0);
            var dbPt = new DBPoint(pt);
            dbPt.ColorIndex = 1;
            _incrementPoints.Add(dbPt);
            _splitPoint = pt;
 
            // Add DBPoints on first half of the line
            var l = line.Length / 2.0 - _increment;
            while (l > 0)
            {
                pt = line.GetPointAtDist(l);
                dbPt = new DBPoint(pt);
                dbPt.ColorIndex = 2;
                _incrementPoints.Insert(0, dbPt);
 
                l -= _increment;
            }
 
            // Add DBPoint on second half of the line
            l = line.Length / 2.0 + _increment;
            while (l < line.Length)
            {
                pt = line.GetPointAtDist(l);
                dbPt = new DBPoint(pt);
                dbPt.ColorIndex = 2;
                _incrementPoints.Add(dbPt);
 
                l += _increment;
            }
 
            // add 2 lines
            _line1 = new Line(line.StartPoint, _splitPoint);
            _line1.ColorIndex = 5;
 
            _line2=new Line(line.EndPoint, _splitPoint);
            _line2.ColorIndex = 6;
 
            // add 2 DBTexts
            _text1 = CreateLineLengthText(_line1);
            _text2 = CreateLineLengthText(_line2);
        }
 
        private DBText CreateLineLengthText(Line line)
        {
            var txt = new DBText();
            txt.TextString = line.Length.ToString("######0.0");
            txt.ColorIndex = 7;
            txt.Height = _textHeight;
            txt.Rotation = line.Angle;
            txt.Position = line.GetPointAtDist(line.Length / 2.0);
            txt.Justify = AttachmentPoint.BottomCenter;
            txt.AdjustAlignment(_dwg.Database);
            if (txt.Rotation >= Math.PI && txt.Rotation < Math.PI * 2.0)
            {
                txt.TransformBy(
                    Matrix3d.Rotation(Math.PI, Vector3d.ZAxis, txt.AlignmentPoint));
            }
 
            return txt;
        }
 
        private Point3d GetSplitPoint(Point3d movePoint)
        {
            // Reset DBPoints' color
            for (int i=0; i<_incrementPoints.Count; i++)
            {
                var pt= _incrementPoints[i];
                if (i == (_incrementPoints.Count - 1) / 2)
                {
                    pt.ColorIndex = 1;
                }
                else
                {
                    pt.ColorIndex = 2;
                }
            }
 
            var dbPt = (from p in _incrementPoints
                        orderby p.Position.DistanceTo(movePoint) ascending
                        select p).First();
            dbPt.ColorIndex = 4;
            return dbPt.Position;
        }
 
        private void SetLines()
        {
            _line1.EndPoint = _splitPoint;
            _line2.EndPoint = _splitPoint;
        }
 
        private void SetLengthTexts()
        {
            _text1.TextString= _line1.Length.ToString("######0.0");
            _text1.Position = _line1.GetPointAtDist(_line1.Length / 2.0);
 
            _text2.TextString = _line2.Length.ToString("######0.0");
            _text2.Position = _line2.GetPointAtDist(_line2.Length / 2.0);
        }
 
        private void AddTransientDrawables()
        {
            foreach (var dbPt in _incrementPoints)
            {
                _tsManager.AddTransient(
                    dbPt, TransientDrawingMode.DirectTopmost, 128, new IntegerCollection());
            }
 
            _tsManager.AddTransient(
                _line1, TransientDrawingMode.DirectShortTerm, 128, new IntegerCollection());
            _tsManager.AddTransient(
                _line2, TransientDrawingMode.DirectShortTerm, 128, new IntegerCollection());
 
            _tsManager.AddTransient(
                _text1, TransientDrawingMode.DirectShortTerm, 128, new IntegerCollection());
            _tsManager.AddTransient(
                _text2, TransientDrawingMode.DirectShortTerm, 128, new IntegerCollection());
        }
 
        private void UpdateTransientDrawables()
        {
            foreach (var dbPt in _incrementPoints)
            {
                _tsManager.UpdateTransient(dbPt, new IntegerCollection());
            }
 
            _tsManager.UpdateTransient(_line1, new IntegerCollection());
            _tsManager.UpdateTransient(_line2, new IntegerCollection());
 
            _tsManager.UpdateTransient(_text1, new IntegerCollection());
            _tsManager.UpdateTransient(_text2, new IntegerCollection());
        }
 
        private void ClearTransientDrawables()
        {
            foreach (var dbPt in _incrementPoints)
            {
                if (!dbPt.IsDisposed)
                {
                    _tsManager.EraseTransient(dbPt, new IntegerCollection());
                    dbPt.Dispose();
                }
            }
            _incrementPoints.Clear();
 
            if (_line1 != null && !_line1.IsDisposed)
            {
                _tsManager.EraseTransient(_line1, new IntegerCollection());
                _line1.Dispose();
            }
            if (_line2 != null && !_line2.IsDisposed)
            {
                _tsManager.EraseTransient(_line2, new IntegerCollection());
                _line2.Dispose();
            }
 
            if (_text1 != null && !_text1.IsDisposed)
            {
                _tsManager.EraseTransient(_text1, new IntegerCollection());
                _text1.Dispose();
            }
            if (_text2 != null && !_text2.IsDisposed)
            {
                _tsManager.EraseTransient(_text2, new IntegerCollection());
                _text2.Dispose();
            }
        }
 
        private bool ChangeIncrement(out double increment)
        {
            increment = _increment;
 
            var opt = new PromptDoubleOptions(
                "\nEnter increment value:");
            opt.AllowZero = false;
            opt.AllowNegative = false;
            opt.DefaultValue = _increment;
 
            var res=_ed.GetDouble(opt);
            if (res.Status== PromptStatus.OK)
            {
                increment = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        #endregion 
    }
}

The command method that runs the LineSpliter:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(DragToSplitLine.MyCommands))]
 
namespace DragToSplitLine
{
    public class MyCommands
    {
        [CommandMethod("SplitLine")]
        public static void RunCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var opt = new PromptEntityOptions("\nSelect a line:");
            opt.SetRejectMessage("\nInvalid selection: must be a line.");
            opt.AddAllowedClass(typeof(Line), true);
            var res = ed.GetEntity(opt);
            if (res.Status!= PromptStatus.OK)
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            Point3d? splitPoint = null;
            using (var splitter = new LineSplitter(dwg, res.ObjectId))
            {
                splitPoint = splitter.DragSplitter();
            }
 
            if (splitPoint.HasValue)
            {
                // Do whatever is needed to the selected line
                ed.WriteMessage($"\nSplit Point: {splitPoint.Value}\n");
            }
            else
            {
                ed.WriteMessage("\n*Cancel*\n");
            }
        }
    }
}

The Video Clip Showing The Code Running Effect:

Point of Interests

  • As I mentioned the article is only meant to show the techniques to adding visual aids for the simply task of selecting point, thus I choose to target a Line entity for simplicity. It can be easily enhanced to target other types of entity (Arc, Polyline, or in general Curve). One only needs to show the potential split points in the same way showed here, and then use Curve.GetSplitCurves() to generate 2 visual curves
  • The length DBText entities could have been displayed more appropriately by moving it above the split visual segments