The ability to enable the XLINE command’s offset functionality to work with xrefs (and blocks)

The changes to enable these enhancements are very minor, which is always a pleasant surprise. Well, in the first case it isn’t much of a surprise: xrefs are actually just a special kind of block, so we really only just have to stop checking it’s that kind of block. Easy. The second case was just as easy: we can add a check for the XLINE command, not just the OFFSET command, when we attach our event handlers.

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;

using DemandLoading;

namespace XrefOffset

{

publicclassXrefOffsetApplication : IExtensionApplication

{

// Maintain a list of temporary objects that require removal

ObjectIdCollection _ids;

staticbool _placeOnCurrentLayer = true;

public XrefOffsetApplication()

{

_ids = newObjectIdCollection();

}

[CommandMethod("ADNPLUGINS", "REMOVEXO", CommandFlags.Modal)]

staticpublicvoid RemoveXrefOffset()

{

DemandLoading.RegistryUpdate.UnregisterForDemandLoading();

Editor ed =

Autodesk.AutoCAD.ApplicationServices.Application.

DocumentManager.MdiActiveDocument.Editor;

ed.WriteMessage(

"\nThe OffsetInXref plugin will not be loaded" +

" automatically in future editing sessions.");

}

[CommandMethod("ADNPLUGINS", "XOFFSETLAYER", CommandFlags.Modal)]

staticpublicvoid XrefOffsetLayer()

{

Document doc =

Application.DocumentManager.MdiActiveDocument;

Database db = doc.Database;

Editor ed = doc.Editor;

PromptKeywordOptions pko =

newPromptKeywordOptions(

"\nEnter layer option for offset objects"

);

conststring option1 = "Current";

conststring 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("ADNPLUGINS", "XOFFSETCPLAYS", CommandFlags.Modal)]

staticpublicvoid 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 =

newList<string>();

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

ObjectIdCollection entsToUpdate =

newObjectIdCollection();

// 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 isBlockReference))

{

// 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 =

newList<Database>();

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

ObjectIdCollection laysToClone =

newObjectIdCollection();

// 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)

{

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 = newDatabase(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 = newIdMapping();

db.WblockCloneObjects(

laysToClone,

db.LayerTableId,

idMap,

DuplicateRecordCloning.Ignore,

false

);

// Dispose each of our xrefed databases

foreach (Database xdb in xrefs)

{

xdb.Dispose();

}

// 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();

}

}

publicvoid 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

);

}

}

};

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

// important events it fires

dm.DocumentCreated +=

delegate(

object sender, DocumentCollectionEventArgs e

)

{

e.Document.CommandWillStart +=

newCommandEventHandler(OnCommandWillStart);

e.Document.CommandEnded +=

newCommandEventHandler(OnCommandFinished);

e.Document.CommandCancelled +=

newCommandEventHandler(OnCommandFinished);

e.Document.CommandFailed +=

newCommandEventHandler(OnCommandFinished);

};

// Do the same for any documents existing on application

// initialization

foreach (Document doc in dm)

{

doc.CommandWillStart +=

newCommandEventHandler(OnCommandWillStart);

doc.CommandEnded +=

newCommandEventHandler(OnCommandFinished);

doc.CommandCancelled +=

newCommandEventHandler(OnCommandFinished);

doc.CommandFailed +=

newCommandEventHandler(OnCommandFinished);

}

try

{

RegistryUpdate.RegisterForDemandLoading();

}

catch

{ }

}

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

// manipulating event-handler

void OnCommandWillStart(object sender, CommandEventArgs e)

{

if (e.GlobalCommandName == "OFFSET" ||

e.GlobalCommandName == "XLINE")

{

Document doc = (Document)sender;

doc.Editor.PromptForEntityEnding +=

newPromptForEntityEndingEventHandler(

OnPromptForEntityEnding

);

}

}

// And when the command ends, remove it

void OnCommandFinished(object sender, CommandEventArgs e)

{

if (e.GlobalCommandName == "OFFSET" ||

e.GlobalCommandName == "XLINE")

{

Document doc = (Document)sender;

doc.Editor.PromptForEntityEnding -=

newPromptForEntityEndingEventHandler(

OnPromptForEntityEnding

);

}

}

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

void OnPromptForEntityEnding(

object sender, PromptForEntityEndingEventArgs e

)

{

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

{

Editor ed = sender asEditor;

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)

asBlockReference;

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)

asBlockTableRecord;

if (btr != null)

{

// Removing this check should allow standard blocks

// to work

//if (btr.IsFromExternalReference)

{

// If so, then we programmatically select the object

// underneath the pick-point already used

PromptNestedEntityOptions pneo =

newPromptNestedEntityOptions("");

pneo.NonInteractivePickPoint =

e.Result.PickedPoint;

pneo.UseNonInteractivePickPoint = true;

PromptNestedEntityResult pner =

ed.GetNestedEntity(pneo);

if (pner.Status == PromptStatus.OK)

{

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 isPolylineVertex3d || obj isVertex2d)

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 isMText || obj isDBText ||

!(obj isEntity))

return;

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

// later

Entity ent = (Entity)obj;

LayerTableRecord ltr =

(LayerTableRecord)tr.GetObject(

ent.LayerId,

OpenMode.ForRead

);

string layName = ltr.Name;

// Clone the selected object

object o = ent.Clone();

Entity clone = o asEntity;

// 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(

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)

asBlockReference;

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

) asBlockTableRecord;

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 =

newSelectedObject(

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();

}

}

}

publicvoid Terminate()

{

}

}

}

I haven’t marked the modified lines, but if you search the above code for the strings “XLINE” and “IsFromExternalReference” you’ll find them, easily enough. Here is an updated version of the project with built versions of the plugin. Once it's been tested by a few more people I expect to post it via Autodesk Labs.

Once loaded, the module will enable both the OFFSET and the XLINE commands to work with xrefs and blocks. Maybe the plugin needs renaming to OffsetInBlocksAndXrefs? :-)