Exploding AutoCAD text and manipulating the results using .NET

Another interesting question came in by email, this week. Fredrik Skeppstedt, a long-time user of the TXTEXP Express Tool, wanted to perform a similar operation using C#: to explode text objects – as TXTEXP does – but then be able to manipulate the resulting geometry from .NET.

TXTEXP is an interesting command: in order to explode text objects, it actually exports them to a Windows Metafile (.WMF) using the WMFOUT command, and then reimports the file back in using WMFIN. This, in itself, is trickier than it sounds, as WMFOUT creates the graphics in the file relative to the top left of the drawing area, and it takes some work to generate the WCS location to pass into the WMFIN command for the geometry to be in the same location.

So why does TXTEXP use a format such as WMF to do this? Well, to reduce text into its base geometry, AutoCAD needs to us its plotting pipeline to generate the primitives with a high degree of fidelity. Generating the graphics isn't enough, as text is generally displayed directly (yes, I'm simplifying this somewhat, but anyway) rather than being decomposed into underlying vectors. WMF is as good a way as any to do this – at least it was back in the late 1990s when this was implemented, and frankly I'm not aware of a better way to capture the data, today (please to post a comment if I'm missing something obvious, of course). Even the approach shown earlier in the week won't work, as it just gets "text" callbacks for each of the objects.

I went down a few warm-looking rabbit-holes while researching a solution to this problem: I looked into manually reading the graphics primitives from a WMF file using .NET – which would take a lot of work and GDI/GDI+ knowledge, apparently – as well as scripting the WMFOUT and WMFIN commands. I ended up realizing that the TXTEXP command does some very useful heavy lifting of its own, and if we're calling commands to do the work then we may as well call this one.

Which reduced the problem to being able to call the TXTEXP command (which happens to be defined using LISP) and then capture the results in order to work with them. I'm generally not a fan of calling commands, but avoiding doing so – especially with complex commands doing a lot of legwork – can often lead to you reinventing the wheel. There are still things to watch out for – such as fiber-based re-entrancy issues – but these are gradually going away (in the coming weeks we'll talk more about the pros and cons of calling commands vs. using "low-level" APIs).

As the TXTEXP command erases the source text objects, I decided to add a feature to copy the objects prior to calling the command, which leaves the originals unerased. I had previously tried just unerasing the originals, but TXTEXP does some really funky things to the text objects it explodes – such as mirroring them a few times and setting the direction to be backwards – so I found the copying approach to be both simpler and less dependent on the internal workings of TXTEXP.

That's enough of the preliminaries… here's the C# command defining our EXPVECS command:

using System.Linq;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

//using System.Reflection;

 

namespace GetTextVectors

{

  public class Commands

  {

    private ObjectIdCollection _entIds;

 

    [CommandMethod("EXPVECS")]

    public void ExplodeToVectors()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      _entIds = new ObjectIdCollection();

 

      var pso = new PromptSelectionOptions();

      pso.MessageForAdding = "Select text objects to explode";

 

      var psr = ed.GetSelection(pso);

      if (psr.Status != PromptStatus.OK)

        return;

 

      // Pass the objects to explode via the pickfirst selection

      // set and save them to be unerased in the next command

 

      var origIds = psr.Value.GetObjectIds();

      var msId = SymbolUtilityServices.GetBlockModelSpaceId(db);

 

      // We're going to copy the original text entities and then

      // pass the copies into TXTEXP, which will then go and erase

      // them, leaving the originals intact

      // If you want the originals erased, just pass origIds into

      // the call to SetImpliedSelection() and delete the lines

      // of code prior to it

 

      var map = new IdMapping();

      db.DeepCloneObjects(

        new ObjectIdCollection(origIds), msId, map, false

      );

 

      // Use a handy LINQ "map" to extract the ObjectId of the copy

      // for each of the input IDs

 

      var copiedIds =

       (from id in origIds select map[id].Value).ToArray<ObjectId>();

 

      ed.SetImpliedSelection(copiedIds);

 

      // We'll use COM's SendCommand, but using InvokeMember()

      // on the type rather than using the TypeLib

 

      var odoc = doc.GetAcadDocument();

      var docType = odoc.GetType();

 

      // Check for objects added to the database

 

      db.ObjectAppended += new ObjectEventHandler(ObjectAppended);

 

      // Call SendCommand passing in both the Express Tools'

      // TXTEXP command and then the custom TXTFIX command

      // to fix the results

 

      //var args = new object[] { "TXTEXP\nTXTFIX\n" };

      //docType.InvokeMember(

      //  "SendCommand", BindingFlags.InvokeMethod, null, odoc, args

      //);

 

      // Actually, no. I've switched back to SendStringToExecute,

      // as in any case the mode we're using is asynchronous

 

      doc.SendStringToExecute(

        "TXTEXP\nTXTFIX\n", false, false, false

      );

    }

 

    [CommandMethod("TXTFIX", CommandFlags.NoHistory)]

    public void FixText()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      var db = doc.Database;

      var ed = doc.Editor;

 

      // Remove the event handler

 

      db.ObjectAppended -= new ObjectEventHandler(ObjectAppended);

 

      if (_entIds.Count == 0)

      {

        ed.WriteMessage(

          "\nCould not find any entities created by TXTEXP."

        );

        return;

      }

 

      // To show we can, let's change the colour of each of

      // the entities generated by TXTEXP to red

 

      using (var tr = doc.TransactionManager.StartTransaction())

      {

        foreach (ObjectId id in _entIds)

        {

          // TXTEXP erases quite a few temporary objects, so be

          // sure to check their erased status first

 

          if (!id.IsErased)

          {

            var ent = (Entity)tr.GetObject(id, OpenMode.ForWrite);

            ent.ColorIndex = 1;

          }

        }

 

< span style="line-height: 140%">        tr.Commit();

      }

    }

 

    void ObjectAppended(object sender, ObjectEventArgs e)

    {

      if (e.DBObject is Entity)

      {

        _entIds.Add(e.DBObject.ObjectId);

      }

    }

  }

}

We use a "database reactor" (or the .NET equivalent, an event handler off the Database object) to capture the objects that get created while the command executes. We also use COM's SendCommand() to launch the TXTEXP command (and then the TXTFIX one – more on that in a tick) but we don't use the COM TypeLib to do so: we use a technique that Viru Aithal showed me when he wrote the sample that evolved into ScriptPro 2.0, calling InvokeMember() to essentially use reflection to bind to the method dynamically.

We use a separate command called TXTFIX to manipulate the results because SendCommand() – while synchronous – doesn't execute quite soon enough for our database reactor to pick them up. This two-command technique is interesting for current versions of AutoCAD but shouldn't be necessary once fibers finally go the way of the dodo.

Here's a quick view on some AutoCAD text before calling the EXPVECS command…

Our text prior to calling EXPVECS

… and then after calling it:

Our text after calling EXPVECS

TXTFIX adjusts the resultant geometry to be red, but it could do all sorts of other things instead, of course (that was just a "for instance"). And you could also apply this general mechanism to all kinds of other problems where you have commands generating database-resident data but that provide no direct .NET API to use.

Update:

After a quick discussion over the weekend with Tony Tanzillo, via this post's comments, I realised I'd been using SendCommand() when I'd otherwise not have: I'd previously been using it to call WMFOUT and WMFIN synchronously but when I'd come to the conclusion that was not going to work – and had introduced the TXTFIX command – I'd forgotten to switch across to use SendStringToExecute() when I went with a more asynchronous approach. I've updated the above code, commenting out the (still very interesting when appropriate) technique of calling SendCommand() via InvokeMember().

8 responses to “Exploding AutoCAD text and manipulating the results using .NET”

  1. WPF can do this, in one of two ways.

    You can call FormattedText.GetPathGeometry()
    to get the true geometric description of
    the text outline, which can be decomposed
    into a collection of objects that derive from System.Windows.Media.PathSegment:

    ArcSegment
    BezierSegment
    LineSegment
    PolyBezierSegment
    PolyLineSegment
    PolyQuadraticBezierSegment
    QuadraticBezierSegment

    Which can then (with quite a bit of work) be
    converted to equialent AutoCAD geometry (which would include splines).

    The other way, is to decompose the path
    into a an approximation composed line segments, via PathGeometry.GetFlattenedPathGeometry()
    with a specified tolerance.

    Unless you need to do complex editing of the
    resulting geometry (or extrude it into 3D solid objects, which is not uncommon), the second option (which is far less complicated ) may work.

  2. "Which reduced the problem to being able to call the TXTEXP command (which happens to be defined using LISP) and then capture the results in order to work with them. I’m generally not a fan of calling commands, but avoiding doing so – especially with complex commands doing a lot of legwork – can often lead to you reinventing the wheel. There are still things to watch out for – such as fiber-based re-entrancy issues – but these are gradually going away (in the coming weeks we’ll talk more about the pros and cons of calling commands vs. using “low-level” APIs)."'

    Kludge Police Here....

    There's no argument with using a kludge in the absence of a better approach. But let's not confuse not seeing the better approach (for whatever reason) with incorrectly assuming that none exists.

    You could have done this using Application.Invoke() which can invoke any LISP function that is defined as a command (e.g., with a C: prefix).

    The asynchronous methods of running commands falls apart in most real-world usage scenarios where something else must be done after the scripted commands are finished. The evidence supporting that is right there in black and white on Autodesk's discussion groups, as well as here on your blog (for example, calling SendStringToExecute() from a PaletteSet event, even though there may be no open documents to send the command to).

    I'm not sure what reentrancy has to do with the Cooperative Fiber Mode threading model, but the issue with reentrancy is about scripting commands that themselves script other commands. For most basic command scripting needs where the commands being scripted are built-in core commands (as opposed to commands in verticals, which often do script other core commands), reentrance is a total non-issue, and if there were even a remote chance of it happening, it can be eliminated by defining the command that does the scripting of other commands using the LispFunction attribute with a C: prefix for the command name, and accepting optional arguments so that it can be called from LISP as a function rather than via (command).

  3. Tony, your first method outlined above is the same I used for the Text to Geometry routine posted here:

    apps.exchange.autodesk.com/AMECH/en/Detail/Index?id=appstore.exchange.autodesk.com%3aText-to-Geometry%3aen

    As is, the routine does not replicate TXTEXP, exactly, but the deconstruction process could be used subsequent to some Text/Mtext analysis. Accounting for non-TTFs would be a bit of a hassle, no doubt.

  4. Nice to hear from you, Tony. It's been a while.

    One question I have - and this may also be for Sean - is around getting the formatted text and how comparable it is to what's in AutoCAD? Or perhaps this also works from AutoCAD plot output (i.e. WMF), so fidelity isn't an issue?

    All in all this seems very interesting, although more work than I'm interested in doing to solve the problem, personally.

    Kean

  5. Ah yes - I'd forgotten that as a LISP command I could also have used Invoke() - thanks for the reminder.

    I had originally used SendCommand() as I was scripting multiple commands (WMFOUT & WMFIN) and attempting to do so synchronously, but then ended up queueing up multiple commands instead, as the results weren't actually posted to the DB in a timely enough manner. I would probably have just used SendStringToExecute() if I'd thought more about it (and maybe even Invoke() if I'd really, really thought hard about it, but maybe not).

    In the back of my mind when coding/posting this was the fact I'll be able to use it as a counter example - "here's the kind of thing we had to do in the bad old days" - when things get better (watch this space).

    Kean

  6. Hi Sean.

    Interesting. Did you actually do all of the work needed to accurately convert all of the WPF PathSegment types into equivalent AutoCAD geometry?

  7. Oops... Application.Invoke() only works when the commands being called accept arguments, in lieu of prompting for input, which many do not do, so maybe I was mistaken about it being useful here. Many of the commands that shipped with AutoCAD that were originally implemented via the acedDefun() mechanism did that, but a great number of those have long since changed to ARX commands that can be called via LISP's (command) function.

  8. “Did you actually do all of the work needed to accurately convert all of the WPF PathSegment types into equivalent AutoCAD geometry?”

    I did. For the most part, though, I just had to deal with Polylines and 4 point Beziers segments. Most True Type Fonts are just combinations of those.
    The source code for a very early version can be found here:

    http://www.theswamp.org/index.php?topic=31435.msg369842#msg369842

    “. . . .is around getting the formatted text and how comparable it is to what's in AutoCAD? . . . .”

    The way I got around that was to avoid the issue entirely. I use a WinForm, and force the user to enter the text string at runtime. Dealing with database resident text entities seemed more troublesome than it was worth.

Leave a Reply

Your email address will not be published. Required fields are marked *