Friday, June 28, 2019

Attach PDF Underlay - How to Handle Multiple Page PDF File

PDF underlay was introduced in AutoCAD 2010 (I believe, but I could be wrong). As late comer of an AutoCAD native object, the existing AutoCAD COM APIs only provide very limited means to handle it: one cannot create (i.e. attaching PDF underlay) it programmatically. Fortunately, AutoCAD .NET APIs give programmers full access to this new object, so that it can be created/updated/deleted with .NET API code properly.

There have been lots of efforts and tools made to convert/import graphics in PDF file into AutoCAD drawing as native AutoCAD entities in many years. Lately, AutoCAD finally added its own functionality to convert graphics in PDF file into native AutoCAD entities. This trend may reduce the need to use PDF as underlay in drawing. But, as long as PDF underlay remains in AutoCAD, there are always chances for AutoCAD programmers to deal with it in code.

There is not much information/sample code available on line regarding using .NET APIs to handle PDF underlay. Autodesk's ADN DevBlock - AutoCAD has this article, which is the only sample code I could find so far.

The real technical difficulty for us to deal with PDF underlay, as far as I can tell, occurs when the PDF file has multiple pages and only selected page or pages to be attached as underlay or underlays into a drawing. There was a question asked in AutoCAD's .NET discussion forum asked more than a year ago and re-asked again recently, which went unanswered so far. I have just spent a bit of time on coding PDF underlay, focusing on attaching selected page or pages in PDF file. So, I share the know-how I have learnt in this article.

Here are the things we, as programmer, need to pay attention:

1. The Relation Between PdfDefinition and PdfReference

In the surface, at least from CAD user's point of view, attaching PDF underlay into AutoCAD drawing is very similar to attaching another drawing as Xref, or inserting a block from a block file (*.dwg). So the relation between PdfDefinition and PdfReference is a bit similar to the relation between BlockTableRecord and BlockReference. However, for BlockTableRecord, it is stored in BlockTable, keyed with its name, while for PdfDefinition, it is stored in a named dictionary, call "ACAD_PDFDEFINITIONS". Each dictionary entry key is named by following convention of "[FileName] - [Page #]", if the PdfDefinition is created by AutoCAD built-in command, but it can also be any string value. PdfReference's name property is read/write-accessible, as opposed to BlockReference's name property as read-only (i.e. it is the name of the BlockTableRecord). Also, as programmer, we need to pay extra attention to PdfDefinition's read/write-able property "ItemName", and PdfReference's read/write-able property "NameOfSheet". These 2 properties somehow affect one another when one is set. More on this later.

When adding new BlockTableRecord to BLockTable, the BlockTableRecord's name must be unique. When a new BlockTableRecord is created by inserting a block drawing database, if the block name is the same as an existing BlockTableRecord, AutoCAD simply update the existing BlockTableRecord with the same name. However, since new PdfDefinition is added to DBDictionary.SetAt() with a string key, it is possible to create a new PdfDefinition (thus a new ObjectId) and set it to the PdfDefinition's NamedDictionary with the same key (i.e. previous PdfDefinition's ObjectId in the DBDictionaryEntry is replaced). If the previous PdfDefinition had a corresponding PdfReference, that PdfReference would not be updated to be the reference of the new PdfDefinition, because it had "hard link" to the previous PdfDefinition's ObjectId. The end result is that the PdfReference of previous PdfDefinition would remain visible in AutoCAD editor, but if selected, the Properties window would not show PdfUnderlay related properties (because of its definition is no longer found in PdfDefinition Named Dictionary). And if the drawing is closed and reopened in AutoCAD, the old PdfReference would no longer be there. One can run the code from the aforementioned article of Autodesk's ADN DevBlock - AutoCAD multiple times (make sure to add PdfReferences in different positions, so that to make things more visually verifiable).

What I have learnt here is, with BlockTableRecord, we can simply decide whether we want to add new one, or update existing one by its name (hence the usability of SymbolTable.Has() method). But for Pdf underlay, PdfDefinition's key in the NamedDictionary does not indicates the PdfDefinition is to the same PDF file as another PdfDefinition. So, when create PdbDefinition, in spite the convention is to use the file name (plus " - [page #]) as the key, we need to always check if the key is already used in the NamedDictionary to make sure the new PdfDefinition not replace an existing one, even the 2 definition may point to the same PDF source file.

2. Handle Multiple PDF Pages

Each PdfReference only shows one PDF page visually. Obviously, the attaching process (creating PdfReference object) needs to know which page in PDF file is to be the PdfReference instance. Here is what PdfDefinition.ItemName, PdfReference.NameOfSheet and PdfReference.Name come into play when the PDF source file has multiple pages:

a. Creating PdfDefinition without setting its ItemName property and creating a corresponding PdfReference without setting its NameOfSheet property.

In this case, the PdfReference shows the first page of the PDF file.

b. Creating PdfDefinition with setting its ItemName property to a valid page number (a string value), but creating corresponding PdfReference without setting its NameOfSheet property.

In this case, PdfReference's NameOfSheet property is set to the same as PdfDefinition's ItemName property (page number in string value) when the PdfReference.DefinitionId property is assigned to a PdfDefinition.

c. Creating PdfDefinition without setting its ItemName property, but creating corresponding PdfReference with its NameOfSheet property to a valid page number (string value).

In this case, the PdfDefinition's ItemName property would be set to the same value as NameOfSheet property of the PdfReference. Also interestingly, this could happen when the PdfDefinition is not opened explicitly in the transaction where the PdfReference is created and its NameOfShoeet property is set; meaning AutoCAD opens the PdfDefinition object behind the scene and updates its ItemName property automatically when PdfRefernece's NameOfSheet property is set.

d. When setting either PdfDefinition's ItemName property or PdfReference's NameOfSheet property, the string value number be a valid page number (from "1" to the max page number). If the value is not a numeric, an eInvalidValue exception is raised, while if the value is out valid page range, an eOutOfRange exception is raised. Obviously setting either ItemName property in PdfDefinition, or NameOfSheet property in PdfReference decides which PDF page showing as underlay. There is no need to set both properties, but if you do, the last one being set wins, by natural logic, it usually is NameOfSheet property of the PdfReference, because PdfReference can only be created properly after PdfDefinition being already created.

e. PdfDefinition does not have a "Name" property, while PdfReference's Name property is read/write-accessible. After a PdfReference is "newed" and before its DefinitionId is assigned to a PdfDefinition, PdfReference.Name cannot be set: an eNullObjectId error would be raised. Once DefinitionId is assigned, the PdfDefinition's key in named directory would be assigned to the PdfReference's Name property. However, if s PdfReference's DefinitionId is set and its Name property is set to whatever value, that value will be used to update the corresponding PdfDefinition's DNDictionaryEntry's key in "ACAD_PDFDEFINITIONS" named dictionary by AutoCAD behind the scene.

f. Since AutoCAD knows if the value (page number) for ItemName/NameOfSheet is valid or in range, it is obvious that when PdfDefinition's SourceFileName property is set, AutoCAD does something in the background (reading the PDF file, thus knowing how many pages in the PDF file). However, the .NET APIs does not provide anything in this regard to let our programming work easier. When we code our PDF underlay attaching process, we need to know how many pages in a PDF exist, so that we can attach desired page to as a PDF underlay, or allow the user to choose which page to attach.

So, the solution is outside AutoCAD .NET APIs, it is all up to us, the programmer, to know how many pages in a PDF file prior to the underlay attaching process. Fortunately, when working with MS .NET platform, it is rather easy for this task. There is well known open licensed PDF manipulation tool iTextSharp, which was developed originally for Java platform and being ported to .NET platform later. Lately, the iTextSharp's .NET version has been overhauled as iText.

With iTextSharp, or iText (or whatever PDF manipulation software of your choice), we can examine the PDF file to determine how many pages in the it; and then, we may present a UI, such as the one with AutoCAD's built-in UI for command "PDFATTACH". How cool the UI can be would depend on the PDF manipulation software you choose: from simply listing all page numbers, or listing preview image of all pages, for user to choose. In my code example, I used iText library for my own PDF underlay process.

3. Example of PDF Underlay Attaching Code

a. Class that determines the page count in a PDF file

public class PdfUtils
    {
        public static int GetPdfDocumentPageCount(string pdfFileName)
        {
            var count = 0;
            using(var doc = GetReadOnlyPdfDocumentFromFile(pdfFileName))
            {
                count = doc.GetNumberOfPages();
            }
 
            return count;
        }
 
        #region private methods
 
        private static PdfDocument GetReadOnlyPdfDocumentFromFile(string fileName)
        {
            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException(
                    $"Cannot find PDF file\n{fileName}.");
            }
 
            if (!Path.GetFileName(fileName).ToUpper().EndsWith(".PDF"))
            {
                throw new InvalidOperationException(
                    $"Not a *.PDF file: {Path.GetFileName(fileName)}");
            }
 
            var reader = new PdfReader(fileName);
            return new PdfDocument(reader);
        }
 
        #endregion
    }


b. An UI for user to choose PDF page(s)


The UI is simplified with only one multi-select-able Listbox, listing all available pages in a PDF file. A suitable PDF viewer control could be placed on the UI to show page content visually, which would help user to make selection.

The form's code behind:

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows.Forms;
 
namespace PdfUnderlayAttach
{
    public partial class dlgSelectPdfPages : Form
    {
        public dlgSelectPdfPages()
        {
            InitializeComponent();
        }
 
        public void SetAvailablePages(int pages)
        {
            for (int i=1; i<=pages; i++)
            {
                lstPages.Items.Add("Page " + i);
            }
 
            lstPages.SelectedIndex = -1;
        }
 
        public IEnumerable<int> SelectedPages
        {
            get
            {
                return from index in lstPages.SelectedIndices.Cast<int>()
                       orderby index ascending
                       select index + 1;
            }
        }
 
        private void LstPages_SelectedIndexChanged(object sender, EventArgs e)
        {
            btnOK.Enabled = lstPages.SelectedItems.Count > 0;
        }
    }
}

c. Class that does the actual underlay attaching work

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using System.Linq;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(PdfUnderlayAttach.PdfUnderlayCommand))]
 
namespace PdfUnderlayAttach
{
    public class PdfUnderlayCommand
    {
        [CommandMethod("MyPdfAttach")]
        public static void InsertPdfUnderlay()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var pdfFile = SelectPdfFileForOpening();
            if (!string.IsNullOrEmpty(pdfFile))
            {
                try
                {
                    var pageCount = PdfUtils.GetPdfDocumentPageCount(pdfFile);
                    IEnumerable<int> selectedPageNumbers;
                    if (pageCount == 1)
                        selectedPageNumbers = new List<int>(new int[] { 1 });
                    else
                    {
                        selectedPageNumbers = AskUserForPageNumbers(pageCount);
                    }
 
                    if (selectedPageNumbers != null && selectedPageNumbers.Count() > 0)
                    {
                        AttachPdfUnderlays(dwg, pdfFile, selectedPageNumbers);
                    }
                }
                catch (System.Exception ex)
                {
                    ed.WriteMessage("\nError:\n{0}.", ex.Message);
                }
                finally
                {
                    
                }
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        #region private methods
 
        private static string SelectPdfFileForOpening()
        {
            string fName = null;
 
            using (var dlg = new System.Windows.Forms.OpenFileDialog())
            {
                dlg.Title = "Select PDF File";
                dlg.Filter = "PDF File (*.pdf)|*.pdf";
                dlg.Multiselect = false;
                dlg.InitialDirectory = @"C:\Temp\MultiPagePdf";
                if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    fName = dlg.FileName;
                }
            }
 
            return fName;
        }
 
        private static IEnumerable<int> AskUserForPageNumbers(int pageCount)
        {
            IEnumerable<int> numbers = null;
 
            using (var dlg=new dlgSelectPdfPages())
            {
                dlg.SetAvailablePages(pageCount);
                var res = CadApp.ShowModalDialog(CadApp.MainWindow.Handle, dlg, false);
                if (res== System.Windows.Forms.DialogResult.OK)
                {
                    numbers = dlg.SelectedPages;
                }
            }
 
            return numbers;
        }
 
        private static void AttachPdfUnderlays(
            Document dwg, string pdfFile, IEnumerable<int> pageNumbers)
        {
            foreach (var pageNumber in pageNumbers)
            {
                if (!SelectInsertionPoint(dwg.Editor,pageNumber,  out Point3d position))
                {
                    dwg.Editor.WriteMessage("\n*Cancel*");
                    return;
                }
                AttachPdfUnderlay(dwg.Database, pdfFile, pageNumber, position);
            }
        }
 
        private static ObjectId AttachPdfUnderlay(
            Database db, string pdfFileName, int pageNumber, Point3d insPoint)
        {
            var pdfRefId = ObjectId.Null;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var pdfDefId = CreatePdfDefinition(db, pdfFileName, pageNumber, tran);
                pdfRefId = CreatePdfReference(pdfDefId, db, pdfFileName, pageNumber, insPoint, tran);
                tran.Commit();
            }
 
            return pdfRefId;
        }
 
        private static bool SelectInsertionPoint(Editor ed, int pageNumber, out Point3d point)
        {
            point = Point3d.Origin;
 
            var res = ed.GetPoint($"\nSelect insertion point for page {pageNumber} of the PDF file:");
            if (res.Status== PromptStatus.OK)
            {
                point = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private static ObjectId CreatePdfDefinition(
            Database db, string pdfFileName, int pageNumber, Transaction tran)
        {
            var defId = ObjectId.Null;
 
            var pdfDicId = GetPdfDefinitionDictionary(db, tran);
            var dic = (DBDictionary)tran.GetObject(pdfDicId, OpenMode.ForWrite);
            var fName = System.IO.Path.GetFileNameWithoutExtension(pdfFileName);
            var defKey =  fName + " - " + pageNumber;
 
            int i = 0;
            while (dic.Contains(defKey))
            {
                i++;
                defKey = fName + "_" + i.ToString().PadLeft(2, '0') + " - " + pageNumber;
            }
 
            var pdfDef = new PdfDefinition();
            pdfDef.SourceFileName = pdfFileName;
 
            defId = dic.SetAt(defKey, pdfDef);
            tran.AddNewlyCreatedDBObject(pdfDef, true);
 
            return defId;
        }
 
        private static ObjectId GetPdfDefinitionDictionary(Database db, Transaction tran)
        {
            ObjectId dicId = ObjectId.Null;
 
            var namedDic = (DBDictionary)tran.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForRead);
            var pdfDicKey = UnderlayDefinition.GetDictionaryKey(typeof(PdfDefinition));
            if (!namedDic.Contains(pdfDicKey))
            {
                var dic = new DBDictionary();
                namedDic.UpgradeOpen();
                dicId = namedDic.SetAt(pdfDicKey, dic);
                tran.AddNewlyCreatedDBObject(dic, true);
            }
            else
            {
                dicId = namedDic.GetAt(pdfDicKey);
            }
 
            return dicId;
        }
 
        private static ObjectId CreatePdfReference(
            ObjectId pdfDefId, Database db, string pdfFileName, int pageNumber, Point3d position, Transaction tran)
        {
            var pdfId = ObjectId.Null;
 
            var space = (BlockTableRecord)tran.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
 
            var pdf = new PdfReference();
            pdf.DefinitionId = pdfDefId;
            pdf.SetDatabaseDefaults(db);
            pdf.Position = position;
 
            pdf.NameOfSheet = pageNumber.ToString();
 
            pdfId = space.AppendEntity(pdf);
            tran.AddNewlyCreatedDBObject(pdf, true);
 
            return pdfId;
        }
 
        #endregion
    }
}

c. Add references to iText libraries into AutoCAD add-in project

As aforementioned, in order to know page count in a PDF prior to attaching underlay process, we need some software tool that can deal with PDF file. Since iText is the latest replacement for iTextSharp, I used iText in this project. To add iText reference to this project, go to NuGet Package Manager UI, search for "itext", then choose "itext7" package to be added to the project.

While iText is meant for replacing iTextSharp, one thing that does not please me is iText comes with a group of 12 Dll-files, as opposed to old good single Dll-file of iTextSharp (with bigger file size, of course), all of them have to be deployed with the small AutoCAD add-in Dll-file; not to mention more possible Dll-files may also come into play, if I want to incorporate a PDF viewing component in the UI for user to select PDF pages.

As usual, see this video clip showing how the code works.


Wednesday, February 27, 2019

Visually Select A Segment Of Polyline

This post is inspired by a recent discussion in Autodesk's .NET discussion forum.

With .NET API, when user picks a polyline, it is fairly easy to figure out where on the polyline use picked, that is, which segment between 2 vertices is picked. However, if we code a process and ask user to select certain segment of a polyline, we need to provide good visual hint for user interaction, so that use would be able to explicitly make his/her selection. In the case of asking user to select a certain segment of polyline, there is no direct and simple API methods to use, but I thought it could achieved with a bit of coding, when I saw the question was raised in the discussion forum, and decided I would give it a try if no one would offer concrete solution later.

Now, I have found a bit time and completed some code which does what I thought is the answer to the original question. This time, let see the video showing how the code visually indicates a segment of polyline is selected first. Here is the video clip.

Here are some considerations to bear in mind:

1. Use Editor.GetEntity() for picking, with filter set to only allow polyline being selected;
2. Start handling Editor.PointMonitor event right before Editor.GetEntity() is called, so that when user moves cursor for selecting, the code would have chance to calculate which segment of the polyline the mouse cursor is hovering on (or not hover on a polyline at all); then draw transient graphics of the segment of the polyline to provide user a visual hint;
3. Use Polyline.GetSplitCurves() to obtain an non-DB residing entity to draw transient graphics and return as the segment selecting result (up to the method caller to either adding the segment entity to database, or dispose it).

Let us look at the code. First the class PartialPolylinePicker:
using System;
using System.Collections.Generic;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.GraphicsInterface;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SelectPartialPolyline
{
    public class PartialPolylinePicker : IDisposable
    {
        private Document _dwg = null;
        private Editor _ed = null;
 
        private ObjectId _polyId = ObjectId.Null;
        private TransientManager _tsMgr = TransientManager.CurrentTransientManager;
        private List<Drawable> _drawables = new List<Drawable>();
 
        public void Dispose()
        {
            ClearTransientGraphics();
        }
 
        public Entity PickPartialPolyline(Document dwg)
        {
            _dwg = dwg;
            _ed = dwg.Editor;
 
            Entity curve = null;
 
            ClearTransientGraphics();
 
            try
            {
                _ed.PointMonitor += Editor_PointMonitor;
 
                var opt = new PromptEntityOptions(
                    "\nSelect polyline:");
                opt.SetRejectMessage("\nInvalid: not a polyline.");
                opt.AddAllowedClass(typeof(Autodesk.AutoCAD.DatabaseServices.Polyline), true);
                
                var res = _ed.GetEntity(opt);
                if (res.Status== PromptStatus.OK)
                {
                    var pickPt = res.PickedPoint;
                    var polyId = res.ObjectId;
                    
                    var vertexIndexes = GetVertexIndexes(pickPt, polyId);
                    curve = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
                  curve=GetPolylineSegmentCurve(pickPt, polyId);
                }             }             finally             {                 _ed.PointMonitor -= Editor_PointMonitor;             }             return curve;         }         private void Editor_PointMonitor(object sender, PointMonitorEventArgs e)         {             ClearTransientGraphics();             var curPt = e.Context.RawPoint;             var polyId = GetClosestPolylineAtPoint(_ed, curPt);             if (!polyId.IsNull)             {                 HighlightPolyline(polyId, curPt);             }         }         #region private methods
private Curve GetPolylineSegmentCurve(
          Point3d pickedPoint, ObjectId polyId, bool hightlight=false)
{
    Autodesk.AutoCAD.DatabaseServices.Curve curve = null;
 
    using (var tran = polyId.Database.TransactionManager.StartTransaction())
    {
        var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)tran.GetObject(polyId, OpenMode.ForRead);
        var index = GetSegmentIndex(pickedPoint, poly);
        curve = GetSegmentCurve(poly, index);
        if (curve!=null && hightlight)
        {
            curve.ColorIndex = poly.ColorIndex != 2 ? 2 : 1;
        }
        tran.Commit();
    }
    
    return curve;
}
 
private int GetSegmentIndex(
           Point3d pickedPosition, Autodesk.AutoCAD.DatabaseServices.Polyline poly)
{
    var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
    var param = poly.GetParameterAtPoint(closestPoint);
    return Convert.ToInt32(Math.Floor(param));
}
 
private Curve GetSegmentCurve(
           Autodesk.AutoCAD.DatabaseServices.Polyline poly, int index)
{
    Curve3d geCurve = null;
    var segType = poly.GetSegmentType(index);
    switch (segType)
    {
        case SegmentType.Line:
            geCurve = poly.GetLineSegmentAt(index);
            break;
        case SegmentType.Arc:
            geCurve=poly.GetArcSegmentAt(index);
            break;
    }
 
    if (geCurve!=null)
    {
        return Curve.CreateFromGeCurve(geCurve);
    }
    else
    {
        return null;
    }
}
        private Tuple<int, int> GetVertexIndexes(Point3d pickedPosition, ObjectId polyId)
        {
            int first = 0;
            int second = 0;
 
            using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())
            {
                var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                    tran.GetObject(polyId, OpenMode.ForRead);
 
                var closestPoint = poly.GetClosestPointTo(pickedPosition, false);
                var len = poly.GetDistAtPoint(closestPoint);
 
                for (int i = 1; i < poly.NumberOfVertices - 1; i++)
                {
                    var pt1 = poly.GetPoint3dAt(i);
                    var l1 = poly.GetDistAtPoint(pt1);
 
                    var pt2 = poly.GetPoint3dAt(i + 1);
                    var l2 = poly.GetDistAtPoint(pt2);
 
                    if (len > l1 && len < l2)
                    {
                        first = i;
                        second = i + 1;
                        break;
                    }
                }
 
                tran.Commit();
            }
 
            return new Tuple<int, int>(first, second);
        }
 
        private void ClearTransientGraphics()
        {
            if (_drawables.Count>0)
            {
                foreach (var d in _drawables)
                {
                    _tsMgr.EraseTransient(d, new IntegerCollection());
                    d.Dispose();
                }
 
                _drawables.Clear();
            }
        }
 
        private ObjectId GetClosestPolylineAtPoint(Editor ed, Point3d position)
        {
            var returnId = ObjectId.Null;
 
            var selResult = SelectAtPickBox(ed, position);
            if (selResult.Status== PromptStatus.OK)
            {
                var ids = new List<ObjectId>();
                foreach (ObjectId id in selResult.Value.GetObjectIds())
                {
                    if (id.ObjectClass.DxfName.ToUpper()=="LWPOLYLINE")
                    {
                        ids.Add(id);
                    }
                }
 
                if (ids.Count > 0)
                {
                    if (ids.Count == 1)
                    {
                        returnId = ids[0];
                    }
                    else
                    {
                        // If the pick box hover on multiple polyline, find the one that is
                        // closest to the pick box center
                        returnId = FindClosestPolyline(ids, position, ed.Document.Database);
                    }
                }
            }
 
            return returnId;
        }
 
        private PromptSelectionResult SelectAtPickBox(Editor ed, Point3d pickBoxCentre)
        {
            //Get pick box's size on screen
            System.Windows.Point screenPt = ed.PointToScreen(pickBoxCentre, 1);
     
            //Get pickbox's size. Note, the number obtained from
            //system variable "PICKBOX" is actually the half of
            //pickbox's width/height
            object pBox = CadApp.GetSystemVariable("PICKBOX");
            int pSize = Convert.ToInt32(pBox);
    
            //Define a Point3dCollection for CrossingWindow selecting
            Point3dCollection points = new Point3dCollection();
    
            System.Windows.Point p;
            Point3d pt;
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y - pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X + pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt);
    
            p = new System.Windows.Point(screenPt.X - pSize, screenPt.Y + pSize);
            pt = ed.PointToWorld(p, 1);
            points.Add(pt );
    
            return ed.SelectCrossingPolygon(points);
        }
 
        private ObjectId FindClosestPolyline(IEnumerable<ObjectId> ids, Point3d position, Database db)
        {
            ObjectId polyId = ObjectId.Null;
            double dist = double.MaxValue;
 
            using (var tran = db.TransactionManager.StartOpenCloseTransaction())
            {
                foreach (var id in ids)
                {
                    var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)
                        tran.GetObject(id, OpenMode.ForRead);
                    var pt = poly.GetClosestPointTo(position, false);
                    var d = pt.DistanceTo(position);
                    if (d < dist)
                    {
                        polyId = id;
                        dist = d;
                    }
                }
 
                tran.Commit();
            }
 
            return polyId;
        }
 
        #endregion
 
        #region private methods: highlight polyline's segment
 
        private void HighlightPolyline(ObjectId polyId, Point3d curPt)
        {
            var vertexIndexes = GetVertexIndexes(curPt, polyId);
            var ent = GetSplitCurveAtVertex(polyId, vertexIndexes.Item1);
           var ent=GetPolylineSegmentCurve(curPt, polyId, true);
            if (ent != null)             {                 _drawables.Add(ent);                 _tsMgr.AddTransient(ent, TransientDrawingMode.DirectTopmost, 128, new IntegerCollection());             }         }         private Entity GetSplitCurveAtVertex(ObjectId polyId, int vertexIndex)         {             Entity curve = null;             using (var tran = polyId.Database.TransactionManager.StartOpenCloseTransaction())             {                 var poly = (Autodesk.AutoCAD.DatabaseServices.Polyline)                     tran.GetObject(polyId, OpenMode.ForRead);                 var vertices = new Point3dCollection();                 for (int i=0; i<poly.NumberOfVertices; i++)                 {                     vertices.Add(poly.GetPoint3dAt(i));                 }                 using (var dbObjs = poly.GetSplitCurves(vertices))                 {                     for (int i=0; i<dbObjs.Count; i++)                     {                         if (i==vertexIndex)                         {                             curve = (Entity)dbObjs[i];                         }                         else                         {                             dbObjs[i].Dispose();                         }                     }                     dbObjs.Clear();                 }                 if (curve!=null)                 {                     curve.ColorIndex = poly.ColorIndex != 1 ? 1 : 2;                 }                 tran.Commit();             }             return curve;         }                  #endregion     } }

Here is the command class:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(SelectPartialPolyline.MyCommand))]
 
namespace SelectPartialPolyline
{
    public class MyCommand 
    {
        [CommandMethod("PickPoly")]
        public static void DoPartialPolylineSelection()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                Entity curve = null;
 
                using (var picker = new PartialPolylinePicker())
                {
                    curve = picker.PickPartialPolyline(dwg);
                }
 
                if (curve!=null)
                {
                    ed.WriteMessage("\nPartial polyline picking suceeded.\n");
 
                    // Do whatever with the curve segment from the polyline,
                    // which is not database residing at the moment
                    // if it is not to be added to database, make sure to dispose it
                    curve.Dispose();
                }
                else
                {
                    ed.WriteMessage("\nPartial polyline picking was cancelled\n");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage("\nError: {0}", ex.Message);
            }
            finally
            {
                Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
            }
        }
    }
}

Download the source code here.
Also, see this video clip here showing how the code works:



Update:

While this bug fix update is quite late, I did it anyway: now it works with closed polyline as expected. Actually the updated code is much simpler than the buggy old code. I do not remember what I was thinking then😅 The source code for download has also been updated. By the way, I have a later article, that was also about nearly the same topic same topic here.


Friday, November 30, 2018

Exporting Shape From AutoCAD Civil/Map, 2D or 3D?

It is very common practice when using AutoCAD Civil/Map that user needs to export AutoCAD geometries as Shape file (*.shp). AutoCAD Map provides built-in command "MapExport" for doing this. AutoCAD Map also comes with ObjectARX Map APIs that includes Autodesk.Gis.Map.ImportExport namespace, which allows the shape exporting to be done programmatically. I have a few custom applications written and used from many years since AutoCAD Map 2006, exporting 2D shape files. These applications had been worked as expected, until our recent upgrade from Civil2015 to Civil2018: suddenly these custom applications exported shape file in 3D, which is not we want (for regulatory data submission).

When using "MapExport" command to export shape file, user is given a chance to decide exporting shape as 2D or 3D, as picture below shows:


In our practice at work, when manually exporting shape, we never needed to click "Driver Options.." button to decide exporting to 2D or 3D. AutoCAD Map always defaults to 2D exporting. This is also the case for my Map API code, where I had never needed to have code to set exporting driver option for the object Autodesk.Gis.Map.ImportExport.Exporter, well, until we moved to Civil/Map 2018. However, the built-in command still always defaults to 2D export, even you re-run the command after the previous command where you chose to export to 3D.

So, I had to modify my code of more than 10 years of old to force 2D export, using the methods GetDriveOptions()/SetDriverOptions() of Autodesk.Gis.Map.ImportExport.Exporter class.

Due to the poor AutoCAD Map API documentation, I ran into a obstacle: what is the value for the method SetDriveOptions()'s argument of Autodesk.Gis.Map.ImportExport.NameValueCollection type, in order to export in 2D or 3D? I searched all over the Internet and came back empty-handed. But I eventually figured it out by saving an exporting profile (*.epf file), as shown in picture below:


*.epf file is actually an XML file. The "Driver Options" for 2D/3D exporting can be easily spotted by opening this *.epf file in NotePad, as the picture shows below:



With this information finally available, I went ahead modifying my shape exporting code like this (in red):

public class  MyShapeExporter
{
    private Exporter _exporter = null;
 
    public MyShapeExporter()
    {
 
    }
 
    public void ExportClosedPolylines(IEnumerable<ObjectId> entIds, string shapeFileName)
    {
        try
        {
            _exporter = HostMapApplicationServices.Application.Exporter;
            _exporter.Init("SHP", shapeFileName);
            _exporter.SetStorageOptions(StorageType.FileOneEntityType, GeometryType.Polygon, null);
            _exporter.ClosedPolylinesAsPolygons = true;
            _exporter.SetSelectionSet(new ObjectIdCollection(entIds.ToArray()));
            var options = _exporter.GetDriverOptions();
            var opt = new Autodesk.Gis.Map.Utilities.StringPair("FDO_SHAPE_DIMENSION", "2D");
            if (!options.Contains(opt))
            {
                options.Add(opt);
            }
            _exporter.SetDriverOptions(options);
            _exporter.Export(true);
 
        }
        finally
        {
            _exporter = null;
        }
    }
}


In summary, the code I showed here in red was never needed until we moved from AutoCAD Civil/Map2015 to AutoCAD Civil/Map2018 (it could be since 2016 or 2017, which I never actually used/tested), which indicates some AutoCAD Map API behaviour change, although the manual process with built-in command "MapExport" remains the same from early version to AutoCAD Civil/Map 2018.

Obviously, with this added a few lines of code, we can now explicitly decide to export shape as 2D or 3D.




Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.