Thursday, October 17, 2024

A .NET API implementation of Express Tool's Text Explode Command (TXTEXP)

I have not done a blog article for quite long, for various reasons (one of them being a bit lazier😆). The code project in this article was mostly quite a while ago and I finally arranged a bit of time to sorted it clean for posting.

AutoCAD's Express Tool suite comes with LISP-defined command "TXTEXP", which can explode TEXT entity in drawing into a bunch of curve entities. 

As AutoCAD programmers, we may want to explode TEXT entity, as the "TXTEXP" does, in our coded process. Surely, we can call the LISP-defined command in our code, most likely, using SendStringToExecute()/SendCommand() method. However, is may cases, in our coded process, we not only want to the target TEXT entity being exploded into curve entities, but also want to hold onto the explosion-generated curve entities for further operation. 

In the past I have see a few threads in the AutoCAD programming discussion forum about this issue, such as this , and this.

However, since the "TXTEXP" is a LISP-defined command, calling it with SendStringToExecute()/SendCommand() make it being executed "asynchronously", which makes it difficult for our coded process to wait for the exploding to be completed and then collect the newly generated curve entities and then continue.

So, I decided to implement a .NET API version of "TXTEXP" operation, which can be used in a CommandMethod to do the same as the command "TXTEXP", or it can be used in a custom .NET API process synchronously. To do so, I studied the LISP routine, comes with Express Tool suite "txtexp.lsp" (in the folder "C:\Program Files\Autodesk\AutoCAD 20xx\Express\").

Here are the steps of how to explode TEXT entity into curve entities:

1. Select/identify a target TEXT entity in current drawing;

2. Export the selected TEXT entity to a WMF (Windows metafile, a image file format that can have both vector and bitmap graphic contents) with AutoCAD's COM API: AcadDocument.Export(). Since the Export() method requires an AcadSelectionSet as input, thus in the step 1, the target TEXT entity must be added into a COM API's AcadSelectionSet;

3. Import the WMF file into AutoCAD with COM API AcadDocument.Import(), which will create a block definition with name prefixed with "WMF" and a block reference at supplied location.

4. Explode the block reference, which would result in a bunch of curve entities (line, arc, circle, polyline) that form the original text visually.

So, the actually conversion of a TEXT entity to individual curves actually happens when the TEXT entity is VISUALLY exported into WMF file.

Based on these 4 steps, I quickly put together some code and yes, it works expected, well, if the TEXT entity is based on AutoCAD native, shape-based fonts. If the TEXT entity uses True-Type font, then it did not work. It turns out, for True-Type font, the WMF export only works (to convert TEXT entity to WFM image) if the TEXT entity is mirrored backwardly. If scanning the LISP code of "TXTEXP.lsp", one would see "MIRROR" command is called before and after WMF is exported and imported. So, I added code the mirror the target TEXT entity before exporting and mirror the backward text image block back after importing. The actually transformation is a bit more complicated than just mirroring, because the TEXT entity could be in different rotation angle. So, I first rotate it to horizontal, and mirror it. After importing, the apply the combined transformation inversely back.

Another factor to be considered is that the target TEXT entity could be in a crowded drawing with other noisy entities overlapping (at its rotated and mirrored position). So before exporting, I make a window selection with the window being the target TEXT entity (again, rotated and mirrored, so it is horizontal TEXT entity), and thus any selected entity to invisible (Entity.Visible = false), and turn them back visible after importing.

If the target text entity is a MTEXT, then simply explode the MTEXT into one or multiple TEXT entities and do the exploding process against each TEXT entity.

Following are the codes of the class TextExploder and a CommandClass showing how to use it:

Class TextExploer

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;
using Autodesk.AutoCAD.Colors;
 
namespace ExplodeText
{
    public class TextExploder
    {
        private Document _dwg = null;
        private Database _db = null;
        private Editor _ed = null;
 
        private Extents3d _txtExtents = new Extents3d();
        private List<string> _existingWmfBlockNames;
        private string _layer = null;
        private double _rotation = 0;
 
        public TextExploder(Document dwg, string explodeToLayer=null)
        {
            _dwg= dwg;
            _db = _dwg.Database;
            _ed = _dwg.Editor;
            _layer= explodeToLayer;
 
            if (!string.IsNullOrEmpty(_layer) && !LayerExists(_layer))
            {
                throw new ArgumentException(
                    $"Designate layer does not exist: {_layer}");
            }
        }
 
        public List<ObjectId> ExplodeMText(ObjectId mtextEntId)
        {
            if (mtextEntId.ObjectClass.DxfName.ToUpper() != "MTEXT")
            {
                throw new ArgumentException("No MText entity.");
            }
 
            var resultCurves = new List<ObjectId>();
 
            var txtIds = ExplodeMTextToDbTexts(mtextEntId);
 
            foreach (var txtId in txtIds)
            {
                var curveIds = DoDBTextExplode(txtId);
                if (curveIds!=null)
                {
                    resultCurves.AddRange(curveIds);
                }
            }
 
            return resultCurves;
        }
 
        public List<ObjectId> ExplodeDBText(ObjectId txtEntId)
        {
            if (txtEntId.ObjectClass.DxfName.ToUpper() != "TEXT")
            {
                throw new ArgumentException("No DBText entity.");
            }
 
            var curveIds = DoDBTextExplode(txtEntId);
            return curveIds;
        }
 
        #region private methods: misc
 
        private List<ObjectId> DoDBTextExplode(ObjectId txtEntId)
        {
            List<ObjectId> curveIds = null;
 
            string wmfFileName = null;
            string wmfFilePath = null;
 
            // generate WMF image file
            if (!ExportWmfImage(txtEntId, 
                out wmfFileName, out wmfFilePath, 
                out string layer, out _rotation)) return null;
 
            if (string.IsNullOrEmpty(_layer))
            {
                _layer = layer;
            }
 
            // import the WMF image as block
            var importFile = $"{wmfFilePath}{wmfFileName}.wmf";
            try
            {
                if (!ImportWmfImage(importFile, _txtExtents.MinPoint))
                {
                    return null;
                }
            }
            finally
            {
                if (File.Exists(importFile)) File.Delete(importFile);
            }
 
            // process WMF image block
            _existingWmfBlockNames = GetExistingWmfBlockNames();
            curveIds = PostWmfImportWork();
 
            // erase original text
            if (curveIds != null)
            {
                EraseTextEntity(txtEntId);
            }
 
            return curveIds;
        }
 
        private List<ObjectId> ExplodeMTextToDbTexts(ObjectId mtextId)
        {
            var textIds = new List<ObjectId>();
 
            using (var tran = _db.TransactionManager.StartTransaction())
            {
                var mtext = (MText)tran.GetObject(mtextId, OpenMode.ForWrite);
                var dbObjects = new DBObjectCollection();
                mtext.Explode(dbObjects);
                var space=(BlockTableRecord)tran.GetObject(mtext.BlockId, OpenMode.ForWrite);
                foreach (DBObject dbObject in dbObjects)
                {
                    if (dbObject is DBText)
                    {
                        var id = space.AppendEntity(dbObject as DBText);
                        tran.AddNewlyCreatedDBObject(dbObject, true);
                        textIds.Add(id);
                    }
                    else
                    {
                        dbObject.Dispose();
                    }
                }
                mtext.Erase();
                tran.Commit();
            }
 
            return textIds;
        }
 
        private void EraseTextEntity(ObjectId entId)
        {
            using (var tran = _db.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(entId, OpenMode.ForWrite);
                txt.Erase();
                tran.Commit();
            }
        }
 
        #endregion
 
        #region private methods: epxort DBText as WMF image
 
        private bool PrepareTextForExport(
            ObjectId txtEntId, 
            out Extents3d origExt,
            out Extents3d zoomExt,
            out string layer, 
            out double rotation)
        {
            origExt = new Extents3d();
            zoomExt = new Extents3d();
            layer = "";
            rotation = 0.0;
 
            var hasText = true;
            using (var tran=_db.TransactionManager.StartOpenCloseTransaction())
            {
                var txt = (DBText)tran.GetObject(txtEntId, OpenMode.ForWrite);
                if (txt.TextString.Trim().Length > 0)
                {
                    rotation = txt.Rotation;
                    origExt = txt.GeometricExtents;
                    layer = txt.Layer;
 
                    // rotate the text to horizontal
                    txt.TransformBy(
                        Matrix3d.Rotation(-txt.Rotation, Vector3d.ZAxis, txt.Position));
 
                    // mirror the text in order for TrueFont text to be converted to curves
                    using (var mirrorLine = new Line3d())
                    {
                        mirrorLine.Set(
                            txt.Position, 
                            new Point3d(txt.Position.X, txt.Position.Y+1000, txt.Position.Z));
                        txt.TransformBy(Matrix3d.Mirroring(mirrorLine));
                    }
 
                    zoomExt = txt.GeometricExtents; 
                }
                else
                {
                    _ed.WriteMessage("\nText entity has empty text string!");
                    hasText = false;
                }
                
                tran.Commit();
            }
            return hasText;
        }
 
        private bool LayerExists(string layer)
        {
            var exists = false;
            using (var tran = _db.TransactionManager.StartOpenCloseTransaction())
            {
                var layerTable = (LayerTable)tran.GetObject(
                    _db.LayerTableId, OpenMode.ForRead);
                exists = layerTable.Has(layer);
                tran.Commit();
            }
            return exists;
        }
 
        private bool ExportWmfImage(
            ObjectId txtId, 
            out string wmfFileName, out string wmfFilePath, 
            out string layer, out double rotation)
        {
            var exportOk = true;
 
            wmfFileName = string.Empty;
            wmfFilePath = string.Empty;
 
            var mirrText = (short)CadApp.GetSystemVariable("MIRRTEXT");
            try
            {
                CadApp.SetSystemVariable("MIRRTEXT", 1);
 
                exportOk = PrepareTextForExport(
                    txtId, out _txtExtents, out Extents3d zoomExt, out layer, out rotation);
                if (exportOk)
                {
                    using (var tempView = new WmfZoomedView(_ed, zoomExt))
                    {
                        CadApp.UpdateScreen();
 
                        dynamic preferences = CadApp.Preferences;
                        wmfFilePath = preferences.Files.TempFilePath;
                        wmfFileName = GetWmfFileName(txtId);
                        exportOk = ExportTextEntity(txtId, zoomExt, wmfFileName, wmfFilePath);
                    }
                }
            }
            finally
            {
                CadApp.SetSystemVariable("MIRRTEXT", mirrText);
            }
            return exportOk;
        }
 
        private string GetWmfFileName(ObjectId id)
        {
            var name = id.Handle.ToString();
            var i = 1;
            var fileName = "";
            while(true)
            {
                fileName = $"{name}_{i}";
                if (System.IO.File.Exists(fileName))
                {
                    i++;
                }
                else
                {
                    return fileName;
                }
            }
        }
 
        private bool ExportTextEntity(
            ObjectId txtId, Extents3d zoomExtents, string wmfFile, string wmfFilePath)
        {
            var ok = true;
            dynamic comDoc = _dwg.GetAcadDocument();
            dynamic ss = comDoc.SelectionSets.Add(txtId.Handle.ToString());
            var hiddenEnts = new List<ObjectId>();
            try
            {
                var pt1 = new double[]
                { 
                    zoomExtents.MinPoint.X, 
                    zoomExtents.MinPoint.Y, 
                    zoomExtents.MinPoint.Z 
                };
                var pt2 = new double[] 
                { 
                    zoomExtents.MaxPoint.X, 
                    zoomExtents.MaxPoint.Y, 
                    zoomExtents.MaxPoint.Z 
                };
                ss.Select(0, pt1, pt2);
                
                var count = ss.Count;
                if (count>1)
                {
                    hiddenEnts = HideExtraEntitiesInSelectionSet(ss);
                }
 
                var fName = $"{wmfFilePath}{wmfFile}.wmf";
                if (File.Exists(fName)) File.Delete(fName);
                comDoc.Export($"{wmfFilePath}{wmfFile}""wmf", ss);
            }
            catch(System.Exception ex)
            {
                _ed.WriteMessage(
                    $"\nExporting WMF file error:\n{ex.Message}");
                ok = false;
            }
            finally
            {
                ss.Delete();
                if (hiddenEnts.Count>0)
                {
                    TurnOffEntities(hiddenEnts, true);
                }
            }
 
            return ok;
        }
 
        private List<ObjectId> HideExtraEntitiesInSelectionSet(dynamic selectionSet)
        {
            var ids=new List<ObjectId>();
            foreach (dynamic ent in selectionSet)
            {
                string handleString = ent.Handle;
                var handle=new Handle(
                    long.Parse(handleString, System.Globalization.NumberStyles.HexNumber));
                if (_db.TryGetObjectId(handle, out ObjectId id))
                {
                    var dxfName = id.ObjectClass.DxfName.ToUpper();
                    if (dxfName!="TEXT" && dxfName!="MTEXT")
                    {
                        ids.Add(id);
                    }
                }
            }
 
            if (ids.Count>0)
            {
                TurnOffEntities(ids, false);
            }
 
            return ids;
        }
 
        private void TurnOffEntities(List<ObjectId> entityIds, bool visible)
        {
            using (var tran = _db.TransactionManager.StartTransaction())
            {
                foreach (var id in entityIds)
                {
                    var ent = (Entity)tran.GetObject(id, OpenMode.ForWrite);
                    ent.Visible = visible;
                }
                tran.Commit();
            }
        }
 
        #endregion
 
        #region private methods: import WMF image
 
        private List<string> GetExistingWmfBlockNames()
        {
            List<string> names;
            using (var tran = _db.TransactionManager.StartTransaction())
            {
                var blkTable = (BlockTable)tran.GetObject(
                    _db.BlockTableId, OpenMode.ForRead);
                names = blkTable.Cast<ObjectId>()
                    .Select(id => ((BlockTableRecord)tran.GetObject(id, OpenMode.ForRead)).Name)
                    .Where(name => name.ToUpper().StartsWith("WMF")).ToList();
                tran.Commit();
            }
            return names;
        }
 
        private bool ImportWmfImage(string wmfFile, Point3d insPt)
        {
            try
            {
                var pt = new double[] { insPt.X, insPt.Y, insPt.Z };
                dynamic comDoc = _dwg.GetAcadDocument();
                comDoc.Import(wmfFile, pt, 1.0);
                return true;
            }
            catch(System.Exception ex)
            {
                _ed.WriteMessage($"\nImportiing WMF image error:\n{ex.Message}");
                return false;
            }
        }
 
        #endregion
 
        #region private methods: Convert imported WMF block into curves
 
        private List<ObjectId> PostWmfImportWork()
        {
            List<ObjectId> curveIds = null;
            using (var tran = _db.TransactionManager.StartTransaction())
            {
                var space=(BlockTableRecord)tran.GetObject(
                    _db.CurrentSpaceId, OpenMode.ForWrite);
                var wmfBlkRef = FindWmfBlock(space, tran);
                if (wmfBlkRef!=null)
                {
                    wmfBlkRef.UpgradeOpen();        
                    curveIds = ConvertWmfBlockReferenceToCurves(wmfBlkRef, space, tran);
                    if (curveIds.Count>0)
                    {
                        try
                        {
                            wmfBlkRef.Erase();
 
                            var wmfBlkDef = (BlockTableRecord)tran.GetObject(
                                wmfBlkRef.BlockTableRecord, OpenMode.ForWrite);
                            wmfBlkDef.Erase();
                        }
                        catch { }
                    }
                }
                else
                {
                    _ed.WriteMessage(
                        $"\nCannot find imported WMF image block reference.");
                }
 
                tran.Commit();
            }
 
            return curveIds;
        }
 
        private BlockReference FindWmfBlock(BlockTableRecord space, Transaction tran)
        {
            var blks = new List<BlockReference>();
            foreach (ObjectId id in space)
            {
                if (id.ObjectClass.DxfName.ToUpper() != "INSERT"continue;
                var blk = (BlockReference)tran.GetObject(id, OpenMode.ForRead);
                if (blk.Name.ToUpper().StartsWith("WMF"))
                {
                    blks.Add(blk);
                }
            }
 
            if (blks.Count>0)
            {
                return (from b in blks 
                        orderby b.ObjectId.Handle.Value descending 
                        select b).First();
            }
            else
            {
                return null;
            }
        }
 
        private BlockTableRecord FindImportedWmfBlock(Transaction tran)
        {
            var blkTable = (BlockTable)tran.GetObject(
                _db.BlockTableId, OpenMode.ForRead);
            foreach (ObjectId blkId in blkTable)
            {
                var blk = (BlockTableRecord)tran.GetObject(
                    blkId, OpenMode.ForRead);
                if (blk.Name.ToUpper().StartsWith("WMF"))
                {
                    if (IsImportedWmfBlock(blk.Name)) return blk;
                }
            }
            return null;
        }
 
        private bool IsImportedWmfBlock(string blkName)
        {
            foreach (var name in _existingWmfBlockNames)
            {
                if (name.ToUpper() == blkName.ToUpper()) return false;
            }
            return true;
        }
 
        private List<ObjectId> ConvertWmfBlockReferenceToCurves(
            BlockReference blkRef, BlockTableRecord space, Transaction tran)
        {
            // mirror and rotate the block reference before exploding it
            using (var mirrorLine = new Line3d())
            {
                mirrorLine.Set(
                    blkRef.Position, 
                    new Point3d(blkRef.Position.X, blkRef.Position.Y + 1000, blkRef.Position.Z));
                blkRef.TransformBy(Matrix3d.Mirroring(mirrorLine));
            }
            blkRef.TransformBy(Matrix3d.Rotation(_rotation, Vector3d.ZAxis, blkRef.Position));
 
            // explode the block reference
            var ents = ExplodeWmfBlock(blkRef, tran);
 
            var wmfExtents = GetWmfExtents(ents);
            var scale = 
                (_txtExtents.MaxPoint.X - _txtExtents.MinPoint.X) / 
                (wmfExtents.MaxPoint.X - wmfExtents.MinPoint.X);
            var mtScale = Matrix3d.Scaling(scale, wmfExtents.MinPoint);
            var mtMove = Matrix3d.Displacement(
                wmfExtents.MinPoint.GetVectorTo(_txtExtents.MinPoint));
            
            var curveIds=new List<ObjectId>();
 
            foreach (var ent in ents)
            {
                if (!string.IsNullOrEmpty(_layer))
                {
                    ent.Layer = _layer;
                    ent.Color = Color.FromColorIndex(ColorMethod.ByLayer, 256);
                }
                ent.TransformBy(mtScale);
                ent.TransformBy(mtMove);
 
                var curveId = space.AppendEntity(ent);
                tran.AddNewlyCreatedDBObject(ent, true);
 
                curveIds.Add(curveId);
            }
 
            return curveIds;
        }
 
        private List<Entity> ExplodeWmfBlock(BlockReference blkRef, Transaction tran)
        {
            var ents=new List<Entity>();
            using (var objs = new DBObjectCollection())
            {
                blkRef.Explode(objs);
                foreach (DBObject obj in objs)
                {
                    var ent = (Entity)obj;
                    
                    ents.Add(ent);
                }
            }
            return ents;
        }
 
        private Extents3d GetWmfExtents(List<Entity> ents)
        {
            var ext = new Extents3d();
            foreach (var ent in ents)
            {
                ext.AddExtents(ent.GeometricExtents);
            }
            return ext;
        }
        #endregion
    }
}

Here is a helper class WmfZoomedView:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using System;
 
namespace ExplodeText
{
    public class WmfZoomedView : IDisposable
    {
        private readonly Editor _ed;
 
        private ViewTableRecord _currentView = null; 
 
        public WmfZoomedView(Editor ed, Extents3d zoomExtents)
        {
            _ed = ed;
            _currentView = _ed.GetCurrentView();
            ZoomToExtents(zoomExtents);
        }
 
        public void Dispose()
        {
            if (_currentView != null)
            {
                _ed.SetCurrentView(_currentView);
            }
        }
 
        private void ZoomToExtents(Extents3d zoomExtents)
        {
            var d = (zoomExtents.MaxPoint.X - zoomExtents.MinPoint.X) / 50;
 
            var pt1 = new[] 
            { 
                zoomExtents.MinPoint.X - d, 
                zoomExtents.MinPoint.Y - d, 
                zoomExtents.MinPoint.Z 
            };
            var pt2 = new[] 
            { 
                zoomExtents.MaxPoint.X + d, 
                zoomExtents.MaxPoint.Y + d, 
                zoomExtents.MaxPoint.Z 
            };
 
            dynamic comApp = Application.AcadApplication;
            comApp.ZoomWindow(pt1, pt2);
        }
 
    }
}

Here is the CommandClass/Method that drives the actual text exploding operation:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ExplodeText.MyCommands))]
 
namespace ExplodeText
{
    public class MyCommands 
    {
        [CommandMethod("BlowUpText")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            var txtId = SelectText(editor);
            if (!txtId.IsNull)
            {
                List<ObjectId> explodedCurves;
                var exploder = new TextExploder(dwg);
                if (txtId.ObjectClass.DxfName.ToUpper() == "TEXT")
                {
                    explodedCurves = exploder.ExplodeDBText(txtId);
                }
                else
                {
                    explodedCurves = exploder.ExplodeMText(txtId);
                }
 
                // Do something with explosion-generated curves
                editor.WriteMessage(
                    $"\n{explodedCurves.Count} curves generated from text explodion.\n");
            }
            else
            {
                editor.WriteMessage("\n*Cancel*");
            }
            editor.PostCommandPrompt();
        }
 
        private static ObjectId SelectText(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nSelect a TEXT/MTEXT entity:");
            opt.SetRejectMessage("\nInvalid: must be a TEXT/MTEXT entity.");
            opt.AddAllowedClass(typeof(DBText), true);
            opt.AddAllowedClass(typeof(MText), true);
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
            {
                return res.ObjectId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
    }
}


See following video showing the code in action:


If one feels difficult to read all the code posted here and more interested in to actually run/debug the code in detail, the entire Visual Studio solution can be accessed from GitHub here:

https://github.com/norman-yuan/ExplodeText.git

I am not sure/do not know if somebodies have done similar .NET API equivalents of the Express Tool's TXTEXP command. This could be served as an inspiration for someone to try to implement his/her own version of it while learning AutoCAD .NET API programming.





Sunday, March 24, 2024

Showing Intersecting Polygons/Areas in Jig's Dragging - Take 2

In my previous article, I used region entities to show the dragged polygon and the intersecting polygons/areas. Since the focus of the topic is to show intersecting areas during jig dragging, presenting the intersecting area in more visually eye-catching manner would be desired. Obviously, if the intersecting areas are filled/hatched in distinguishing color would be much better. 

So, I updated the Jig's code by showing the intersecting area as closed polyline, and then apply a DrawableOverrule to force the polyline to be shown as a color-filled polygon. Following is the code of the DrawableOverrule:

using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;
 
namespace AreaIntersectionJig
{
    public class PolylineToPolygonOverrule : DrawableOverrule
    {
        private static bool _originalOverruling;
        public PolylineToPolygonOverrule()
        {
            _originalOverruling = Overruling;
            AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this, true);
            SetCustomFilter();
            Overruling = true;
        }
 
        public override bool IsApplicable(RXObject overruledSubject)
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline pl = overruledSubject
                as Autodesk.AutoCAD.DatabaseServices.Polyline;
 
            if (pl != null && pl.Closed)
            {
                //Only apply this overrule to the polyline
                //that has not been added to working database
                //e.g. created temp polyline as intersecting area during jigging
                if (pl.Database == null)
                    return true;
                else
                    return false;
            }
            else
            {
                return false;
            }
        }
 
        public override bool WorldDraw(Drawable drawable, WorldDraw wd)
        {
            var poly = drawable as Autodesk.AutoCAD.DatabaseServices.Polyline;
            if (poly != null)
            {
                Point3dCollection pts = new Point3dCollection();
                for (int i = 0; i < poly.NumberOfVertices; i++)
                {
                    pts.Add(poly.GetPoint3dAt(i));
                }
 
                var oldFillType=wd.SubEntityTraits.FillType;
                
                wd.SubEntityTraits.FillType = FillType.FillAlways;
                wd.Geometry.Polygon(pts);
 
                wd.SubEntityTraits.FillType=oldFillType;
            }
 
            return base.WorldDraw(drawable, wd);
        }
 
        protected override void Dispose(bool disposing)
        {
            RemoveOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this);
            Overruling = _originalOverruling;
 
            base.Dispose(disposing);
        }
    }
}

Pay attention to the overridden IsApplicable() method. Because the target polyline to be overruled is the dynamically created polyline showing the intersecting area, which is not database-residing, thus, only polyline with its Database property is null is targeted.

The Jig itself is not much changed in comparison to the code in the previous article. Here is the Jig class (since it is in the same project as the previous Jig class, so, I give it a new class name "AreaIntersectingJig", the changed code is highlighted in red here):

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using System;
using System.Collections.Generic;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace AreaIntersectionJig
{
    public class AreaIntersectingJig : DrawJig, IDisposable
    {
        private List<Region> _regions;
 
        private Region _movingRegion;
        private List<CadDb.Polyline> _intersectingAreas = new List<CadDb.Polyline>();
 
        private Point3d _basePoint;
        private Point3d _currPoint;
 
        private PolylineToPolygonOverrule _polygonOverrule = null;
 
        public AreaIntersectingJig(
            CadDb.Polyline polygon, Point3d basePoint, List<Region> regions)
        {
            _regions = regions;
            _movingRegion = CreateRegionFromPolyline(polygon);
            _basePoint = basePoint;
            _currPoint = basePoint;
 
            _polygonOverrule = new PolylineToPolygonOverrule();
        }
 
        public Point3d JigPoint => _currPoint;
 
        public void Dispose()
        {
            ClearIntersectingAreas();
            _movingRegion.Dispose();
            _polygonOverrule.Dispose();
        }
 
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            var opt = new JigPromptPointOptions("\nMove to:");
            opt.UseBasePoint = true;
            opt.BasePoint = _basePoint;
            opt.Cursor = CursorType.RubberBand;
 
            var res = prompts.AcquirePoint(opt);
            if (res.Status == PromptStatus.OK)
            {
                if (res.Value.Equals(_currPoint))
                {
                    return SamplerStatus.NoChange;
                }
                else
                {
                    var mt = Matrix3d.Displacement(_currPoint.GetVectorTo(res.Value));
                    _movingRegion.TransformBy(mt);
                    _currPoint = res.Value;
 
                    GenerateIntersectingAreas();
 
                    return SamplerStatus.OK;
                }
            }
            else
            {
                return SamplerStatus.Cancel;
            }
        }
 
        protected override bool WorldDraw(WorldDraw draw)
        {
            draw.Geometry.Draw(_movingRegion);
 
            if (_intersectingAreas.Count > 0)
            {
                foreach (var item in _intersectingAreas)
                {
                    draw.Geometry.Draw(item);
                }
            }
            return true;
        }
 
        private Region CreateRegionFromPolyline(CadDb.Polyline poly)
        {
            var dbCol = new DBObjectCollection() { poly };
            var regs = Region.CreateFromCurves(dbCol);
            if (regs.Count > 0)
            {
                var region = (Region)regs[0];
                return region;
            }
            else
            {
                throw new ArgumentException(
                    "Selected polyline cannot form a Region object.");
            }
        }
 
        private void ClearIntersectingAreas()
        {
            if (_intersectingAreas.Count > 0)
            {
                foreach (var item in _intersectingAreas) item.Dispose();
            }
        }
 
        private void GenerateIntersectingAreas()
        {
            ClearIntersectingAreas();
 
            var colorIndex = 1;
            foreach (var r in _regions)
            {
                using (var tempRegion = _movingRegion.Clone() as Region)
                {
                    var intersectingRegion = r.Clone() as Region;
                    intersectingRegion.BooleanOperation(
                        BooleanOperationType.BoolIntersect, tempRegion);
                    if (!intersectingRegion.IsNull)
                    {
                        var polygon = intersectingRegion.ToBoundaryPolyline();
                        if (polygon != null)
                        {
                            polygon.ColorIndex = colorIndex;
                            _intersectingAreas.Add(polygon);
 
                            colorIndex++;
                            if (colorIndex > 6) colorIndex = 1;
                        }
                    }
                }
            }
            CadApp.UpdateScreen();
        }
    }
}

To be complete, here are the codes for the command method and the extension method that get region's boundary as polyline.

Region extension method:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using System;
using System.Collections.Generic;
 
namespace AreaIntersectionJig
{
    // Following code is mainly based on the code from Mr. Gilles Chanteau,
    // posted in Autodesk's AutoCAD .NET API discussion forum
    // https://forums.autodesk.com/t5/net/create-the-outermost-border-for-the-curves-boundary/td-p/12164598
    // I modified the code slightly to make it an Extension Method to be used in my article
    public static class RegionExtension
    {
        private struct Segment
        {
            public Point2d StartPt { get; set; }
            public Point2d EndPt { get; set; }
            public double Bulge { get; set; }
        }
        public static Polyline ToBoundaryPolyline(this Region reg)
        {
            var segments = new DBObjectCollection();
            reg.Explode(segments);
 
            var segs = new List<Segment>();
            var plane = new Plane(Point3d.Origin, reg.Normal);
            for (int i = 0; i < segments.Count; i++)
            {
                if (segments[i] is Region r)
                {
                    r.Explode(segments);
                    continue;
                }
                Curve crv = (Curve)segments[i];
                Point3d start = crv.StartPoint;
                Point3d end = crv.EndPoint;
                double bulge = 0.0;
                if (crv is Arc arc)
                {
                    double angle = arc.Center.GetVectorTo(start).
                        GetAngleTo(arc.Center.GetVectorTo(end), arc.Normal);
                    bulge = Math.Tan(angle / 4.0);
                }
                segs.Add(
                    new Segment 
                    { 
                        StartPt = start.Convert2d(plane), 
                        EndPt = end.Convert2d(plane), 
                        Bulge = bulge 
                    });
            }
 
            foreach (DBObject o in segments) o.Dispose();
 
            var pline = new Polyline();
            pline.AddVertexAt(0, segs[0].StartPt, segs[0].Bulge, 0.0, 0.0);
            Point2d pt = segs[0].EndPt;
            segs.RemoveAt(0);
            int vtx = 1;
            while (true)
            {
                int i = segs.FindIndex((s) => s.StartPt.IsEqualTo(pt) || s.EndPt.IsEqualTo(pt));
                if (i < 0) break;
                Segment seg = segs[i];
                if (seg.EndPt.IsEqualTo(pt))
                    seg = new Segment { StartPt = seg.EndPt, EndPt = seg.StartPt, Bulge = -seg.Bulge };
                pline.AddVertexAt(vtx, seg.StartPt, seg.Bulge, 0.0, 0.0);
                pt = seg.EndPt;
                segs.RemoveAt(i);
                vtx++;
            }
            pline.Closed = true;
            return pline;
        }
    }
}

CommandMethod:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(AreaIntersectionJig.MyCommands))]
 
namespace AreaIntersectionJig
{
    public class MyCommands
    {
        [CommandMethod("MyJig")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var editor = dwg.Editor;
 
            var poly = SelectPolygon(editor);
            if (poly.entId.IsNull)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            var regionIds = SelectRegions(editor);
            if (regionIds==null)
            {
                editor.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var movingPolygon = (Polyline)tran.GetObject(poly.entId, OpenMode.ForWrite);
                var regions = GetRegions(regionIds, tran);
                var jigOk = false;
 
                //using (var jig = new PolygonIntersectingJig(movingPolygon, poly.basePt, regions))
                using (var jig = new AreaIntersectingJig(movingPolygon, poly.basePt, regions))
                {
                    var res = editor.Drag(jig);
                    if (res.Status == PromptStatus.OK)
                    {
                        var mt = Matrix3d.Displacement(poly.basePt.GetVectorTo(jig.JigPoint));
                        movingPolygon.TransformBy(mt);
 
                        jigOk = true;
                    }
                }
                
                if (jigOk)
                {
                    tran.Commit();
                }
                else
                {
                    tran.Abort();
                }
            }
            editor.UpdateScreen();
        }
 
        private static (ObjectId entId, Point3d basePt) SelectPolygon(Editor ed)
        {
            var opt = new PromptEntityOptions("\nSelect a closed polyline:");
            opt.SetRejectMessage("\nInvalid: not a polyline.");
            opt.AddAllowedClass(typeof(Polyline), true);
 
            var res = ed.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                var pRes = ed.GetPoint("\nSelect base point for moving:");
                if (pRes.Status == PromptStatus.OK)
                {
                    return (res.ObjectId, pRes.Value);
                }
                else
                {
                    return (ObjectId.Null, new Point3d());
                }
            }
            else
            {
                return (ObjectId.Null, new Point3d());
            }
        }
 
        private static IEnumerable<ObjectId> SelectRegions(Editor ed)
        {
            var vals = new TypedValue[] { new TypedValue((int)DxfCode.Start, "REGION") };
            var filter = new SelectionFilter(vals);
 
            var opt = new PromptSelectionOptions();
            opt.MessageForAdding = "\nSelect regions:";
 
            var res = ed.GetSelection(opt, filter);
            if (res.Status== PromptStatus.OK)
            {
                return res.Value.GetObjectIds();
            }
            else
            {
                return null;
            }
        }
 
        private static List<Region> GetRegions(IEnumerable<ObjectId> regionIds, Transaction tran)
        {
            var regions= new List<Region>();
 
            foreach (var regionId in regionIds)
            {
                var region = (Region)tran.GetObject(regionId, OpenMode.ForRead);
                regions.Add(region);
                
            }
 
            return regions;
        }
    }
}

See video clip below:


Regardless of the practical usefulness of this Jig, the visual effect is much better, for sure, which is the focus of the topic of this article.

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.