AutoCAD users who work with multiple reference files – whether DWG, DWF, DGN, PDF, PCG files or raster images – usually want them to be oriented in space (and to overlay) properly. One common way to make this happen is to set the various files up in world coordinates and then attach them at the origin of the referencing drawing’s WCS. A common issue related to this approach is if the user happens to be in a local UCS: the file will get attached relative to that UCS rather than to the WCS.
Glenn Ryan, who generously provided April’s very useful Plugin of the Month, XrefStates, has another cool little utility that we’re planning to publish in August: Reference UCS Spy, or RefUcsSpy, for short. Once loaded, this tool sits there and waits for the user to execute an “attach” command – whether XATTACH, IMAGEATTACH, DWFATTACH, DGNATTACH, PDFATTACH or POINTCLOUDATTACH – and, should the user be in a coordinate system other than WCS, the plugin will prompt the user whether to change the UCS to WCS for the duration of the command, changing back to the prior UCS afterwards.
There are – once again – some very interesting things about Glenn’s implementation, in particular the elegant way he’s opted to handle document-level events using a class that gets attached to each open document via its UserData property.
The below C# code is a “flattened” version of what will probably be the published project: I’ve merged various source files into one and replaced the use of string resources with literals. I’ve also not included the demand-loading set-up code and associated removal command, which will, of course, be in the published project.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.Windows;
using acadApp = Autodesk.AutoCAD.ApplicationServices.Application;
namespace RefUcsSpy
{
/// <summary>
/// General utility functions called throughout the application.
/// </summary>
internal class Utils
{
/// <summary>
/// Gets a value indicating whether a script is active.
/// </summary>
/// <returns>
/// True if a script is active, false otherwise.
/// </returns>
internal static bool ScriptActive
{
get
{
return (
((short)acadApp.GetSystemVariable("CMDACTIVE") & 4)
== 4
);
}
}
/// <summary>
/// Gets a value indicating whether the current UCS is
/// equivalent to the WCS.
/// </summary>
internal static bool WcsCurrent
{
get
{
return (short)acadApp.GetSystemVariable("WORLDUCS") == 1;
}
}
/// <summary>
/// Gets or sets a value indicating whether UCSFOLLOW is on or
/// off.
/// </summary>
internal static bool UcsFollow
{
get
{
return (short)acadApp.GetSystemVariable("UCSFOLLOW") == 1;
}
set
{
acadApp.SetSystemVariable("UCSFOLLOW", value ? 1 : 0);
}
}
}
/// <summary>
/// Per-document data and event registration.
/// </summary>
internal class DocData
{
/// <summary>
/// Gets or sets a value indicating the application ID
/// </summary>
internal static readonly Guid AppId;
/// <summary>
/// Gets the Document that data is stored for
/// and events are registered upon.
/// </summary>
internal readonly Document Document;
/// <summary>
/// List of command names that we monitor.
/// </summary>
private static readonly List<string> commandNames;
/// <summary>
/// Initializes static members of the DocData class.
/// </summary>
static DocData()
{
string cmdsToWatch =
"XATTACH,IMAGEATTACH,DWFATTACH,DGNATTACH," +
"PDFATTACH,POINTCLOUDATTACH";
DocData.commandNames =
new List<string>(cmdsToWatch.Split(','));
DocData.AppId = new Guid();
acadApp.DocumentManager.DocumentCreated += (sender, e) =>
{
e.Document.UserData.Add(
DocData.AppId, new DocData(e.Document)
);
};
}
/// <summary>
/// Initializes a new instance of the DocData class.
/// Main constructor. initialises all the event registrations.
/// </summary>
/// <param name="doc">Document to register events upon.</param>
internal DocData(Document doc)
{
this.Document = doc;
this.CurrentUCS = Matrix3d.Identity;
this.Document.CommandWillStart +=
new CommandEventHandler(this.Document_CommandWillStart);
this.Document.CommandCancelled += this.Doc_CommandFinished;
this.Document.CommandEnded += this.Doc_CommandFinished;
this.Document.CommandFailed += this.Doc_CommandFinished;
}
/// <summary>
/// Gets or sets the current UCS matrix for
/// restoration at completion of commands.
/// </summary>
private Matrix3d CurrentUCS { get; set; }
/// <summary>
/// Gets or sets a value indicating whether
/// the UCS has been changed.
/// </summary>
private bool ChangedUCS { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the
/// UCSFOLLOW system variable has been changed.
/// </summary>
private bool ChangedUcsFollow { get; set; }
/// <summary>
/// Called upon first time load to register events upon
/// all open documents.
/// </summary>
internal static void Initialise()
{
foreach (Document doc in acadApp.DocumentManager)
{
doc.UserData.Add(DocData.AppId, new DocData(doc));
}
}
/// <summary>
/// Event handler for CommandWillStart. It checks if the command
/// starting is of interest and if the WCS is not current, then
/// prompts the user to change the UCS.
/// </summary>
/// <param name="sender">Document this command started in</param>
/// <param name="e">
/// Arguments for the event including the name of the command
/// </param>
private void Document_CommandWillStart(
object sender, CommandEventArgs e
)
{
// Get the 'can we run' conditionals out of the way
// first up, is this a command of interest?
if (!DocData.commandNames.Contains(e.GlobalCommandName))
return;
// Next, is the WCS already current?
if (Utils.WcsCurrent)
return;
// Lastly, is a script active? If it is then we don't want
// to interrupt it with a dialog box
if (Utils.ScriptActive)
return;
// Check if UCSFOLLOW is on and turn it off
if (Utils.UcsFollow)
{
Utils.UcsFollow = false;
this.ChangedUcsFollow = true;
}
// To use a MessageBox implementation instead of a TaskDialog,
// uncomment the following MessageBox region and comment out
// the TaskDialog region as well as removing the reference to
// AdWindows.dll and the "using Autodesk.Windows;" statement
#region MessageBox warning implementation
/*
DialogResult ret = MessageBox.Show(
"The current UCS is not equivalent to the WCS.\n" +
"Do you want to change to the WCS for the duration " +
"of this command?",
"RefUcsSpy",
MessageBoxButtons.YesNo,
MessageBoxIcon.Question
);
if (ret == DialogResult.Yes)
{
// Grab the old coordsys to restore later
this.CurrentUCS =
this.Document.Editor.CurrentUserCoordinateSystem;
// Reset the current UCS to WCS
this.Document.Editor.CurrentUserCoordinateSystem =
Matrix3d.Identity;
// Update our flag to say we've made a change that
// needs restoring when the command finishes
this.ChangedUCS = true;
}
*/
#endregion
#region TaskDialog implementation
/* */
// Spin up a new taskdialog instead of a messagebox
TaskDialog td = new TaskDialog();
td.WindowTitle = "Reference UCS Spy - UCS Active";
td.MainIcon = TaskDialogIcon.Warning;
td.MainInstruction =
"The current UCS (User Coordinate System) does not " +
"match the WCS (World Coordinate System).\nWhat do " +
"you want to do?";
td.ContentText =
"Attaching reference files using a coordinate system " +
"other than the WCS can lead to undesirable results";
td.UseCommandLinks = true;
td.AllowDialogCancellation = true;
td.FooterIcon = TaskDialogIcon.Information;
td.FooterText =
"It is common to have reference files drawn to a " +
"specific coordinate system (WCS) and then inserted at " +
"0,0,0 to allow all files that share the same coordinate " +
"system to overlay each other";
td.Buttons.Add(
new TaskDialogButton(
0,
"Change UCS to WCS for this attachment"
)
);
td.Buttons.Add(
new TaskDialogButton(
1,
"Ignore current UCS and continue"
)
);
// Set default to change
td.DefaultButton = 0;
td.Callback =
(ActiveTaskDialog tskDlg,
TaskDialogCallbackArgs eventargs,
object s) =>
{
if (eventargs.Notification ==
TaskDialogNotification.ButtonClicked)
{
switch (eventargs.ButtonId)
{
case 0:
// Grab the old coordsys to restore later
this.CurrentUCS =
this.Document.Editor.CurrentUserCoordinateSystem;
// Reset the current UCS to WCS
this.Document.Editor.CurrentUserCoordinateSystem =
Matrix3d.Identity;
// Update our flag to say we've made a change that
// needs restoring when the command finishes
this.ChangedUCS = true;
break;
case 1:
// ignore it
break;
default:
break;
}
}
return false;
};
td.Show(acadApp.MainWindow.Handle);
/* */
#endregion
}
/// <summary>
/// Event handler for CommandEnded, CommandCancelled and
/// CommandFailed.
/// Resets the UCS to what was stored previously before
/// reference file attachment.
/// </summary>
/// <param name="sender">
/// The document this event is registered upon.
/// </param>
/// <param name="e">Arguments for this event.</param>
private void Doc_CommandFinished(
object sender, CommandEventArgs e
)
{
if (this.ChangedUCS)
{
this.ChangedUCS = false;
this.Document.Editor.CurrentUserCoordinateSystem =
this.CurrentUCS;
this.CurrentUCS = Matrix3d.Identity;
}
if (this.ChangedUcsFollow)
{
Utils.UcsFollow = true;
this.ChangedUcsFollow = false;
}
}
}
/// <summary>
/// Main entrypoint of the application. Initialises document
/// creation/destruction event hooks.
/// </summary>
public class Entrypoint : IExtensionApplication
{
#region IExtensionApplication members
/// <summary>
/// Startup member - called once when first loaded.
/// </summary>
void IExtensionApplication.Initialize()
{
DocData.Initialise();
}
/// <summary>
/// Shutdown member - called once when AutoCAD quits.
/// </summary>
void IExtensionApplication.Terminate()
{
}
#endregion
}
}
To see this code in action, build it into a DLL and NETLOAD it. Then you simply have to change the UCS to something non-standard (I tend to use 3DORBIT followed by “UCS V” to change the UCS to the current view) before launching XATTACH or one of the other monitored commands. You should then see this task dialog:
What happens next will depend on the choice made, of course, and should be obvious from the wording of the dialog (and from the contents of this post).
The task dialog implementation was introduced in AutoCAD 2009, and is part of the AdWindows.dll (which will need to be added as a project reference). If you’re using a prior version of AutoCAD, then all is not lost: you can comment out the task dialog implementation and uncomment the one using a standards Windows message-box, which will lead this being shown instead:
Thanks again for a great little utility, Glenn! If anyone has feedback or comments prior to the final release of this plugin, please do post a comment.