Saturday, December 21, 2024

Using External Text Editor to Modify DBText/MText

When editing DBText or MText, AutoCAD provides "Inplace Text Editor" as default way to edit the text string of DBText/MText entities. AutoCAD also allow external text editor to be used, which can be set up via "Options" dialog box and the option is saved as the system variable "MTEXTED". As the name of this system variable suggested, this text editing option only applies to MText. 

As AutoCAD programmers, we may want to as user to edit DBText/MText entities during our code execution. That is, our code triggers DBText/MText editing, which holds the code execution until user completed the editing. So, how do we do that?

If we want the user to do the DBText/MText editing with AutoCAD built-in InplaceTextEditor, it is rather simple: the InplaceTextEditor class has overloaded methods Invoke() to start the editing inside AutoCAD editor, and hold the the code execution until the user completes the editing. Following code shows how to do it:

[CommandMethod("EditTextInternal")]
public static void EditTextWithInternalEditor()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptEntityOptions("\nSelect TEXT/MTEXT:");
    opt.SetRejectMessage("\nInvalid selection: must be TEXT?MTEXT.");
    opt.AddAllowedClass(typeof(DBText), true);
    opt.AddAllowedClass(typeof(MText), true);
    var res = ed.GetEntity(opt);
    if (res.Status != PromptStatus.OK) return;
 
    using (var tran = dwg.TransactionManager.StartTransaction())
    {
        var textEnt = tran.GetObject(res.ObjectId, OpenMode.ForRead);
        if (textEnt is DBText txt)
        {
            ObjectId[] ids = new ObjectId[] { };
            InplaceTextEditor.Invoke(txt,ref ids);
        }
        else if (textEnt is MText mtxt)
        {
            InplaceTextEditor.Invoke(mtxtnew InplaceTextEditorSettings());
        }
 
        tran.Commit();
    }
}

To use external text editor, such as NotePad, which is just a stand-alone EXE, we need to let our code execution start the external EXE process, so that the external EXE can pick up the text string we want to be edited. The code execution in AutoCAD then is put on hold until the user closes the external EXE process. When the external EXE is closed, our code execution in AutoCAD should get notified and then pick up the text string modified by the external EXE. The whole thing sounds quite complicated, but it is actually quite easy to do. There is a recent post in Autodesk's .NET API forum asking a question on this topic. The OP almost gets it done, except missing the code of holding up the code execution to wait for the external EXE to end. Here is the code of doing it:

[CommandMethod("EditTextExternal")]
public static void EditTextWithExternalEditor()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptEntityOptions("\nSelect TEXT/MTEXT:");
    opt.SetRejectMessage("\nInvalid selection: must be TEXT?MTEXT.");
    opt.AddAllowedClass(typeof(DBText), true);
    opt.AddAllowedClass(typeof(MText), true);
    var res = ed.GetEntity(opt);
    if (res.Status != PromptStatus.OK) return;
 
    using (var tran = dwg.TransactionManager.StartTransaction())
    {
        var textEnt = tran.GetObject(res.ObjectId, OpenMode.ForRead);
        string text = "";
        bool isMText = false;
        if (textEnt is DBText txt)
        {
            text = txt.TextString;
        }
        else if (textEnt is MText mtxt)
        {
            text = mtxt.Contents;
            isMText = true;
        }
        if (!string.IsNullOrEmpty(text))
        {
            var editedText = EditTextWithNotePadPlus(textisMText);
            if (editedText != text)
            {
                textEnt.UpgradeOpen();
                if (textEnt is DBText)
                {
                    ((DBText)textEnt).TextString=editedText;
                }
                else if (textEnt is MText)
                {
                    ((MText)textEnt).Contents=editedText;
                }
            }
        }
 
        tran.Commit();
    }
}
 
private static string EditTextWithNotePadPlus(string entityTextbool isMText)
{
    var tempFile=$"C:\\Temp\\{Guid.NewGuid()}.txt";
    string inputText;
    if (isMText)
    {
        inputText = entityText.Replace("\\P""\r\n");
    }
    else
    {
        inputText = entityText;
    }
    File.WriteAllText(tempFileinputText);
 
    var notePadPlus = @"C:\Program Files\Notepad++\notepad++.exe";
    string editedText = inputText;
    Process process = new Process();
    process.StartInfo.FileName = notePadPlus;
    process.StartInfo.Arguments = tempFile;
    process.EnableRaisingEvents = true;
    process.Exited += (oe) =>
    {
        editedText=File.ReadAllText(tempFile);
        File.Delete(tempFile);
    };
 
    process.Start();
    process.WaitForExit();
    if (!isMText)
    {
        return editedText.Replace("\n""").Replace("\r""");
    }
    else
    {
        return editedText.Replace("\r\n","\\P");
    }
}

As the code shows, the steps are:

1. Retrieve the text string from the DBText/MText entity for editing;

2. Save the retrieved text string to a temporary text file;

3. Create a Process object with the temporary data file path as the arguments, so that when the Process starts, it will read the text string for editing into its working editor;

4. Handle Process.Exited event, in which the text string being edited by the external EXE will be picked up by reading the temporary text file. Make sure to set Process.EnableRaiseEvents property to true.

5. Start the external EXE Process and call its WaitForExit() method, so that the code follows it will be not be executed until the external EXE Process exits;

6. Read the text value from the temporary file, and then delete the file;

7. Compare the text value read back from the file to the value placed into the file before external EXE processes starts. If the text value has been changed, go ahead to update the DBText/MText.

Some Points to Pay Attention:

1. When using code to trigger text editing, our code can decide to use either InplaceTextEditor or external editor regardless the system variable "MTEXTED". Of course, you can also choose which editor according to the "MTEXTED" system variable. However, AutoCAD's built-in behaviour for DBText editing is to always use InplaceTextEditor, while our code can use external editor to edit DBText. 

2. When using external editor, we may have to deal with some extra issues. For example, DBText is a single-line text, while almost all EXE editor would do multiple-line text editing. So, we have remove "NewLine" and "CarriageReturn" ("\n" and "\r") characters from the edited text string before it is updated to DBText. That is why AutoCAD always use InplaceTextEditor for DBText editing. For MText editing, we have to deal with all the special markups, if the external EXE editor doesn't do it for us (I do not know if there is one that can place MText' markups). In my code here, I did a simple handling on this by replacing "\r\n" and "\\P" accordingly, just for simplicity purpose.

Because of the point 2, I can hardly see there would be many cases where users want to use external EXE editor. But, if there is legitimate reasons to programmatically start external text editor, the code here shows it can be done easily.



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.





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.