Importing AutoCAD layers from xrefs using .NET

This post extends the last one which looked at a basic implementation to allow AutoCAD's standard OFFSET command to work on the contents of external references. I haven't flagged the specific changes, but the old code (which is almost identical to that in the last post) starts with the Initialize() function.

The previous example created geometry on a temporary layer that exists only as long as the source xref is attached: detaching the xref generally caused dangling layer references. This post evolves the approach and provides a choice to the user (via the XOFFSETLAYER command): to either create the geometry on the current layer (the default behaviour) or to clone across the layers, plus their various linetypes etc., needed for the geometry to survive independently of their originating xref.

I decided to put the code to copy across the layers in a separate command called XOFFSETCPLAYS, which has the advantage of being callable separately from our OFFSET routine (and is generally a bit safer – performing significant operations on AutoCAD databases from event handlers can lead to problems, depending on what exactly you're doing). The command gets called using SendStringToExecute() so that it gets executed up after control has returned to the AutoCAD application.

The copying of symbol table records from xrefs is a little involved, but I've don what I can to comment the approach below. Using Database.wblockCloneObjects() is the cleanest approach, but there is work involved to load the xrefed DWGs into their own Database objects before using this.

Here's the updated C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System.Collections.Generic;

 

namespace XrefOffset

{

  public class XrefOffsetApplication : IExtensionApplication

  {

    // Maintain a list of temporary objects that require removal

 

    ObjectIdCollection _ids;

    static bool _placeOnCurrentLayer = true;

 

    public XrefOffsetApplication()

    {

      _ids = new ObjectIdCollection();

    }

 

    [CommandMethod("XOFFSETLAYER")]

    static public void XrefOffsetLayer()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      PromptKeywordOptions pko =

        new PromptKeywordOptions(

          "\nEnter layer option for offset objects"

        );< /p>

      const string option1 = "Current";

      const string option2 = "Source";

 

      pko.AllowNone = true;

      pko.Keywords.Add(option1);

      pko.Keywords.Add(option2);

      pko.Keywords.Default =

        (_placeOnCurrentLayer ? option1 : option2);

 

      PromptResult pkr =

        ed.GetKeywords(pko);

 

      if (pkr.Status == PromptStatus.OK)

        _placeOnCurrentLayer =

          (pkr.StringResult == option1);

    }

 

    [CommandMethod("XOFFSETCPLAYS")]

    static public void XrefOffsetCopyLayers()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Database db = doc.Database;

      Editor ed = doc.Editor;

 

      Transaction tr =

        doc.TransactionManager.StartTransaction();

      using (tr)

      {

        BlockTableRecord btr =

          (BlockTableRecord)tr.GetObject(

            db.CurrentSpaceId,

            OpenMode.ForRead

          );

 

        // We will collect the layers used by the various entities

 

        List<string> layerNames =

          new List<string>();

 

        // And store a list of the entities to come back and update

 

        ObjectIdCollection entsToUpdate =

          new ObjectIdCollection();

 

        // Loop through the contents of the active space, and look

        // for entities that are on dependent layers (i.e. ones

        // that come from attached xrefs)

 

        foreach (ObjectId entId in btr)

        {

          Entity ent =

            (Entity)tr.GetObject(entId, OpenMode.ForRead);

 

          // Check the dependent status of the entity's layer

 

          LayerTableRecord ltr =

            (LayerTableRecord)tr.GetObject(

              ent.LayerId,

              OpenMode.ForRead

            );

 

          if (ltr.IsDependent && !(ent is BlockReference))

          {

            // Add it to our list and flag the entity for updating

 

            string layName = ltr.Name;

            if (!layerNames.Contains(layName))

            {

              layerNames.Add(layName);

            }

            entsToUpdate.Add(ent.ObjectId);

          }

        }

        // Sorting the list will allow us to minimise the number

        // of external drawings we need to load (many layers

        // will be from the same xref)

 

        layerNames.Sort();

 

        // Get the xref graph, which allows us to get at the

        // names of the xrefed drawings more easily

 

       
XrefGraph xg = db.GetHostDwgXrefGraph(false);

 

        // We're going to store a list of our xrefed databases

        // for later disposal

 

        List<Database> xrefs =

          new List<Database>();

 

        // Collect a list of the layers we want to clone across

 

        ObjectIdCollection laysToClone =

          new ObjectIdCollection();

 

        // Loop through the list of layers, only loading xrefs

        // in when they haven't been already

 

        string currentXrefName = "";

        foreach(string layName in layerNames)

        {

          Database xdb = null;

 

          // Make sure we have our mangled layer name

 

          if (layName.Contains("|"))

          {

            // Split it up, so we know the xref name

            // and the root layer name

 

            int sepIdx = layName.IndexOf("|");

            string xrefName =

              layName.Substring(0, sepIdx);

            string rootName =

              layName.Substring(sepIdx + 1);

 

            // If the xref is the same as the last loaded,

            // this saves us some effort

 

            if (xrefName == currentXrefName)

            {

           
60;  xdb = xrefs[xrefs.Count-1];

            }

            else

            {

              // Otherwise we get the node for our xref,

              // so we can get its filename

 

              XrefGraphNode xgn =

                xg.GetXrefNode(xrefName);

 

              if (xgn != null)

              {

                // Create an xrefed database, loading our

                // drawing into it

 

                xdb = new Database(false, true);

                xdb.ReadDwgFile(

                  xgn.Database.Filename,

                  System.IO.FileShare.Read,

                  true,

                  null

                );

 

                // Add it to the list for later disposal

                // (we do this after the clone operation)

 

                xrefs.Add(xdb);

              }

              xgn.Dispose();

            }

 

            if (xdb != null)

            {

              // Start a transaction in our loaded database

              // to get at the layer name

 

              Transaction tr2 =

                xdb.TransactionManager.StartTransaction();

              using (tr2)

              {

                // Open the layer table

 

                LayerTable lt2 =

                  (LayerTable)tr2.GetObject(

                    xdb.LayerTableId,

                    OpenMode.ForRead

                  );

 

                // Add our layer (which we get via its

                // unmangled name) to the list to clone

 

                if (lt2.Has(rootName))

                {

                  laysToClone.Add(lt2[rootName]);

                }

 

                // Committing is cheaper

 

                tr2.Commit();

              }

            }

          }

        }

 

        // If we have layers to clone, do so

 

        if (laysToClone.Count > 0)

        {

          // We use wblockCloneObjects to clone between DWGs

 

          IdMapping idMap = new IdMapping();

          db.WblockCloneObjects(

            laysToClone,

            db.LayerTableId,

            idMap,

            DuplicateRecordCloning.Ignore,

            false

          );

 

          // Dispose each of our xrefed databases

 

          foreach (Database xdb in xrefs)

          {

            xdb.Dispose();

          }< /span>

 

          // Open the resultant layer table, so we can check

          // for the existance of our new layers

 

          LayerTable lt =

            (LayerTable)tr.GetObject(

              db.LayerTableId,

              OpenMode.ForRead

            );

 

          // Loop through the entities to update

 

          foreach (ObjectId id in entsToUpdate)

          {

            // Open them each for write, and then check their

            // current layer

 

            Entity ent =

              (Entity)tr.GetObject(id, OpenMode.ForWrite);

            LayerTableRecord ltr =

              (LayerTableRecord)tr.GetObject(

                ent.LayerId,

                OpenMode.ForRead

              );

 

            // We split the name once again (could use a function

            // for this, but hey)

 

            string layName = ltr.Name;

            int sepIdx = layName.IndexOf("|");

            string xrefName =

              layName.Substring(0, sepIdx);

            string rootName =

              layName.Substring(sepIdx + 1);

 

            // If we now have the layer in our database, use it

 

            if (lt.Has(rootName))

              ent.LayerId = lt[rootName];

          }

        }

        tr.Commit();

      }

    }

 

    public void Initialize()

    {

      DocumentCollection dm =

        Application.DocumentManager;

 

      // Remove any temporary objects at the end of the command

 

      dm.DocumentLockModeWillChange +=

        delegate(

          object sender, DocumentLockModeWillChangeEventArgs e

        )

        {

          if (_ids.Count > 0)

          {

            Transaction tr =

              e.Document.TransactionManager.StartTransaction();

            using (tr)

            {

              foreach (ObjectId id in _ids)

              {

                DBObject obj =

                  tr.GetObject(id, OpenMode.ForWrite, true);

                obj.Erase();

              }

              tr.Commit();

            }

            _ids.Clear();

 

            // Launch a command to bring across our layers

 

            if (!_placeOnCurrentLayer)

            {

              e.Document.SendStringToExecute(

                "_.XOFFSETCPLAYS ", false, false, false

              );

            }

   
60;      }

        };

 

      // When a document is created, make sure we handle the

      // important events it fires

 

      dm.DocumentCreated +=

        delegate(

          object sender, DocumentCollectionEventArgs e

        )

        {

          e.Document.CommandWillStart +=

            new CommandEventHandler(OnCommandWillStart);

          e.Document.CommandEnded +=

            new CommandEventHandler(OnCommandFinished);

          e.Document.CommandCancelled +=

            new CommandEventHandler(OnCommandFinished);

          e.Document.CommandFailed +=

            new CommandEventHandler(OnCommandFinished);

        };

 

      // Do the same for any documents existing on application

      // initialization

 

      foreach (Document doc in dm)

      {

        doc.CommandWillStart +=

          new CommandEventHandler(OnCommandWillStart);

        doc.CommandEnded +=

          new CommandEventHandler(OnCommandFinished);

        doc.CommandCancelled +=

          new CommandEventHandler(OnCommandFinished);

        doc.CommandFailed +=

          new CommandEventHandler(OnCommandFinished);

      }

    }

 

    // When the OFFSET command starts, let's add our selection

    // manipulating event-handler

 

    void OnCommandWillStart(object sender, CommandEventArgs e)

    {

      if (e.GlobalCommandName == "OFFSET")

      {

        Document doc = (Document)sender;

        doc.Editor.PromptForEntityEnding +=

          new PromptForEntityEndingEventHandler(

            OnPromptForEntityEnding

          );

      }

    }

 

    // And when the command ends, remove it

 

    void OnCommandFinished(object sender, CommandEventArgs e)

    {

      if (e.GlobalCommandName == "OFFSET")

      {

        Document doc = (Document)sender;

        doc.Editor.PromptForEntityEnding -=

          new PromptForEntityEndingEventHandler(

            OnPromptForEntityEnding

          );

      }

    }

 

    // Here's where the heavy lifting happens...

 

    void OnPromptForEntityEnding(

      object sender, PromptForEntityEndingEventArgs e

    )

    {

      if (e.Result.Status == PromptStatus.OK)

      {

        Editor ed = sender as Editor;

        ObjectId objId = e.Result.ObjectId;

        Database db = objId.Database;

 

        Transaction tr =

          db.TransactionManager.StartTransaction();

        using (tr)

      
;  {

          // First get the currently selected object

          // and check whether it's a block reference

 

          BlockReference br =

            tr.GetObject(objId, OpenMode.ForRead)

              as BlockReference;

          if (br != null)

          {

            // If so, we check whether the block table record

            // to which it refers is actually from an XRef

 

            ObjectId btrId = br.BlockTableRecord;

            BlockTableRecord btr =

              tr.GetObject(btrId, OpenMode.ForRead)

                as BlockTableRecord;

            if (btr != null)

            {

              if (btr.IsFromExternalReference)

              {

                // If so, then we programmatically select the object

                // underneath the pick-point already used

 

                PromptNestedEntityOptions pneo =

                  new PromptNestedEntityOptions("");

                pneo.NonInteractivePickPoint =

                  e.Result.PickedPoint;

                pneo.UseNonInteractivePickPoint = true;

 

                PromptNestedEntityResult pner =

                  ed.GetNestedEntity(pneo);

 

                if (pner.Status == PromptStatus.OK)

                {

               &#
160; 
try

                  {

                    ObjectId selId = pner.ObjectId;

 

                    // Let's look at this programmatically-selected

                    // object, to see what it is

 

                    DBObject obj =

                      tr.GetObject(selId, OpenMode.ForRead);

 

                    // If it's a polyline vertex, we need to go one

                    // level up to the polyline itself

 

                    if (obj is PolylineVertex3d || obj is Vertex2d)

                      selId = obj.OwnerId;

 

                    // We don't want to do anything at all for

                    // textual stuff, let's also make sure we

                    // are dealing with an entity (should always

                    // be the case)

 

                    if (obj is MText || obj is DBText ||

                      !(obj is Entity))

                      return;

 

                    // Now let's get the name of the layer, to use

                    // later

 

                    Entity ent = (Entity)obj;

                    LayerTableRecord ltr =

                      (LayerTableRecord)tr.GetObject(

                   
0;    ent.LayerId,

                        OpenMode.ForRead

                      );

                    string layName = ltr.Name;

 

                    // Clone the selected object

 

                    object o = ent.Clone();

                    Entity clone = o as Entity;

 

                    // We need to manipulate the clone to make sure

                    // it works

 

                    if (clone != null)

                    {

                      // Setting the properties from the block

                      // reference helps certain entities get the

                      // right references (and allows them to be

                      // offset properly)

 

                      clone.SetPropertiesFrom(br);

 

                      // But we then need to get the layer

                      // information from the database to set the

                      // right layer (at least) on the new entity

 

                      if (_placeOnCurrentLayer)

                      {

                        clone.LayerId = db.Clayer;

                      }

                      else

                      {

                        LayerTable lt =

                          (LayerTable)tr.GetObject(

 
0;                          db.LayerTableId,

                            OpenMode.ForRead

                          );

                        if (lt.Has(layName))

                          clone.LayerId = lt[layName];

                      }

 

                      // Now we need to transform the entity for

                      // each of its Xref block reference containers

                      // If we don't do this then entities in nested

                      // Xrefs may end up in the wrong place

 

                      ObjectId[] conts =

                        pner.GetContainers();

                      foreach (ObjectId contId in conts)

                      {

                        BlockReference cont =

                          tr.GetObject(contId, OpenMode.ForRead)

                            as BlockReference;

                        if (cont != null)

                          clone.TransformBy(cont.BlockTransform);

                      }

 

                      // Let's add the cloned entity to the current

                      // space

 

                      BlockTableRecord space =

                        tr.GetObject(

                          db.CurrentSpaceId,

                          OpenMode.ForWrite

                        ) as BlockTableRecord;

                      if (space == null)

                      {

                        clone.Dispose();

                        return;

                      }

 

                      ObjectId cloneId = space.AppendEntity(clone);

                      tr.AddNewlyCreatedDBObject(clone, true);

 

                      // Now let's flush the graphics, to help our

                      // clone get displayed

 

                      tr.TransactionManager.QueueForGraphicsFlush();

 

                      // And we add our cloned entity to the list

                      // for deletion

 

                      _ids.Add(cloneId);

 

                      // Created a non-graphical selection of our

                      // newly created object and replace it with

                      // the selection of the container Xref

 

                      SelectedObject so =

                        new SelectedObject(

                          cloneId,

                          SelectionMethod.NonGraphical,

                          -1

                        );

 

                      e.ReplaceSelectedObject(so);

                    }

                  }

                 
catch

                  {

                    // A number of innocuous things could go wrong

                    // so let's not worry about the details

 

                    // In the worst case we are simply not trying

                    // to replace the entity, so OFFSET will just

                    // reject the selected Xref

                  }

                }

              }

            }

          }

          tr.Commit();

        }

      }

    }

 

    public void Terminate()

    {

    }

  }

}

Depending on the mode selected, the results should be comparable with those from the code in the last post.

Here's a portion of an xref:

A portion of our external reference

Here are the results of using OFFSET on some of the xref's contents after loading our module (to get these results you would also need to use XOFFSETLAYER to make sure the original layers get copied across):

Offset contents of our external reference

Update:

I've modified the implementation of the XOFFSETLAYER command to be consistent with the prompts in the OFFSET command (under the Layer option – not sure how I missed that one :-). The ideal would have been to access that setting directly, but that doesn't seem to be possible, as far as I can tell.

10 responses to “Importing AutoCAD layers from xrefs using .NET”

  1. Thanks for the "Offset in Xref" plugin. Simple question for a start: why are there two identical readme.txt files in the ZIP and apparently two identical .dll files? Thanks.

  2. Hi Patrick,

    I'm glad you found it (even before it gets announced publicly on Tuesday :-).

    The ReadMe is duplicated because I wanted it to be outside the source folder, but also in the project (so it appears in the solution explorer window inside Visual Studio). When you add a file to a project that's outside the project folder, Visual Studio copies it into the project folder. So it was a choice: only have it in the project folder (bad), don't have it in the project (a little less bad), or have the file duplicated (the choice I went for).

    You'll hopefully find the DLLs only to be "apparently" identical: one is for use in 32-bit and the other is for use in 64-bit AutoCAD versions. The source used to build the DLLs is indeed identical, but you need to include different reference assemblies (i.e. different versions of both AcMgd.dll & AcDbMgd.dll) depending on whether you build for a 32- or 64-bit platform. So one (the one found in the win32 folder) should only work on 32-bit AutoCAD, and the other (the one in the x64 folder) should only work on 64-bit AutoCAD.

    Regards,

    Kean

  3. Hi
    Is it possible to have a .Net application, similar to NCOPY lisp, that would copy entities as well as it's layers from xref to working drawing? any solution in this regard would be infinitely appreciated.
    Thanks,
    Johny

  4. Hi Johny,

    It's absolutely possible, and the code in the XrefOffsetCopyLayers() function is a good starting place (although it would also be worth experimenting with Wblock() and Insert(), to see whether that streamlines the process if we're also bringing across entities).

    I'll add this to my list of things to look at, when I have time, but there are no guarantees I'll be able to reach it soon, I'm afraid.

    Regards,

    Kean

  5. Thanks for the plug-in, but does it only work on line's or am I just getting an error. I can only copy lines, I cannot copy 2d or 3d polyline.

  6. This application works with "simple" entities, such as lines, arcs, circles, and new-style polylines. Complex entities - such as old-style 2D polylines - may need some additional work to support, as the lowest level entity that gets returned by the selection process is likely to be a vertex. If you can make use of the enhanced, lightweight polylines, then that's more likely to work without modification.

    Incidentally OFFSET only works with 2D polylines - not 3D polylines - so that's not going to be enabled by this application.

    Kean

  7. Hello Kean,

    I really like your site about automating AutoCAD, so thank you for that!
    But i am a enthausiast and not a professional. I am used to wrting my code in VB.net. Mostly this is not a problem as there are code-converters available on the web.
    The code above however does not convert correctly. For example the 'sub initialize' present's a problem for me.

    Would it be possible for you to post the VB.NET code for this topic?

    Kind regards,

    Wouter

  8. Hello Wouter,

    I expect it's the delegates that have been used to implement event handlers... you may need to move that code into separate functions and then use AddHandler() in VB.NET to register the event handlers.

    I'm afraid I'm busy preparing for a trip, so I won't be able to do this myself. You might try posting a link to this post to the AutoCAD .NET Discussion Group, in case someone there can help.

    Regards,

    Kean

  9. Hi Is there a way to get the layer blocks inside the xref given that you know the layer you wish to clone to the current database? Basically I want to copy the blocks of a layer in an xref.

  10. Yes, you should be able to go through the block table of the host DWG and establish the blocks that exist in the Xrefed DWG (checking which layers they're on). Although you may need to go to the Xref to get the IDs of the source records... but anyway - it's doable.

    Kean

Leave a Reply

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