After showing how to automatically attach xrefs at the origin inside AutoCAD, and then redoing the approach to take care of different unit systems, I then had the request from a couple of places to look at making the xrefs overlays and adjusting their paths to be relative rather than absolute.
Looking around, I found some code on the AutoCAD DevBlog that changes an attachment to an overlay, after the fact. Henrik Ericson found the code didn’t work for him, but did spot the db.OverlayXref() method which did. So I went ahead and made use of that for overlays. I factored out the logic into a single helper method which I now call from two commands: XAO for attachments and XOO for overlays.
For relative paths, I ended up making a couple of extension methods based on this DevBlog post and this one on Stack Overflow. I also threw in a simple helper class that makes sure an object is upgraded to being open for write for the duration of its existence. I could also have required the object to be open for write in the first place, but I felt like seeing if the approach worked.
Here’s the updated code with the XAO and XOO commands. Both now use relative paths by default.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System;
using System.IO;
namespace XrefAttachAtZero
{
// Simple helper to make sure an object is "open for write", downgrading
// its open status when finished. It's intended to be used in the same way
// as a document lock from a using() statement.
public class Upgrader : IDisposable
{
private bool _upgraded;
private DBObject _object;
public Upgrader(DBObject o)
{
_object = o;
_upgraded = o.IsReadEnabled;
if (_upgraded)
o.UpgradeOpen();
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (_upgraded && _object != null)
_object.DowngradeOpen();
_object = null;
}
}
}
public static class Extensions
{
/// <summary>
/// Generates a relative path from one string to another.
/// </summary>
/// <param name="to">The path to which to return a relative path.</param>
/// <returns>The relative path between this one and the destination.</returns>
public static string RelativePathTo(this string from, string to)
{
var fromUri = new Uri(from);
var toUri = new Uri(to);
// Convert to URIs for the sake of the relative path determination
var relUri = fromUri.MakeRelativeUri(toUri);
var relPath = Uri.UnescapeDataString(relUri.ToString());
// Then change back
return relPath.Replace('/', Path.DirectorySeparatorChar);
}
/// <summary>
/// Changes the path of an xref's block definition to have a relative path.
/// </summary>
/// <param name="root">Path from which to create the relative path.</param>
/// <returns>Whether the path was changed - only fails for non-xrefs.</returns>
public static bool ChangePathToRelative(
this BlockTableRecord btr, string root
)
{
var ret = false;
if (btr.IsFromExternalReference)
{
using (new Upgrader(btr))
{
btr.PathName = root.RelativePathTo(btr.PathName);
ret = true;
}
}
return ret;
}
/// <summary>
/// Attaches the specified Xref to the current space in the current drawing.
/// </summary>
/// <param name="path">Path to the drawing file to attach as an Xref.</param>
/// <param name="pos">Position of Xref in WCS coordinates.</param>
/// <param name="name">Optional name for the Xref.</param>
/// <returns>Whether the attach operation succeeded.</returns>
public static bool XrefAttachAndInsert(
this Database db, string path, Point3d pos,
string name = null, bool overlay = false
)
{
var ret = false;
if (!File.Exists(path))
return ret;
if (String.IsNullOrEmpty(name))
name = Path.GetFileNameWithoutExtension(path);
// We'll collect any xref definitions that need reloading after our
// transaction (there should be at most one)
var xIds = new ObjectIdCollection();
try
{
using (var tr = db.TransactionManager.StartOpenCloseTransaction())
{
// Attach or overlay the Xref - add it to the database's block table
var xId =
overlay ? db.OverlayXref(path, name) : db.AttachXref(path, name);
if (xId.IsValid)
{
// Open the newly created block, so we can get its units
var xbtr = (BlockTableRecord)tr.GetObject(xId, OpenMode.ForRead);
// Get the path of the current drawing
var loc = Path.GetDirectoryName(db.Filename);
if (xbtr.ChangePathToRelative(loc))
{
xIds.Add(xId);
}
// Determine the unit conversion between the xref and the target
// database
var sf = UnitsConverter.GetConversionFactor(xbtr.Units, db.Insunits);
// Create the block reference and scale it accordingly
var br = new BlockReference(pos, xId);
br.ScaleFactors = new Scale3d(sf);
// Add the block reference to the current space and the transaction
var btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId, OpenMode.ForWrite
);
btr.AppendEntity(br);
tr.AddNewlyCreatedDBObject(br, true);
ret = true;
}
tr.Commit();
}
// If we modified our xref's path, let's reload it
if (xIds.Count > 0)
{
db.ReloadXrefs(xIds);
}
}
catch (Autodesk.AutoCAD.Runtime.Exception)
{ }
return ret;
}
}
public static class Commands
{
[CommandMethod("XAO")]
public static void XrefAttachAtOrigin()
{
XrefAttachOrOverlayAtOrigin();
}
[CommandMethod("XOO")]
public static void XrefOverlayAtOrigin()
{
XrefAttachOrOverlayAtOrigin(true);
}
private static void XrefAttachOrOverlayAtOrigin(bool overlay = false)
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return;
var db = doc.Database;
var ed = doc.Editor;
// Ask the user to specify a file to attach
var opts = new PromptOpenFileOptions("Select Reference File");
opts.Filter = "Drawing (*.dwg)|*.dwg";
var pr = ed.GetFileNameForOpen(opts);
if (pr.Status == PromptStatus.OK)
{
// Overlay the specified file and insert it at the origin
var res =
db.XrefAttachAndInsert(
pr.StringResult, Point3d.Origin, null, overlay
);
ed.WriteMessage(
"External reference {0}{1} at the origin.",
res ? "" : "not ",
overlay ? "overlaid" : "attached"
);
}
}
}
}
Here’s the XOO command in action. We check the created xref using the XOO command, right afterwards.
I like the way the code has evolved during this series from something almost ridiculously simplistic to being much more useable. Please post a comment if you see something else that needs adding!