This question came in recently from a developer:
I've noticed a lot of talk about cad standards in my circles - any chance of a post on creating a CAD Standards plugin for use in the AutoCAD CAD Standards checker that companies could take and run with to produce their custom checks? For example, is the titleblock to company standard and inserted in the right space, are the xrefs inserted at 0,0 etc.
I thought this was an interesting one to handle: back when I was based in San Rafael I (along with members of my team) worked closely with the CAD Standards feature team (during the AutoCAD 2004 timeframe, if memory serves me correctly), to develop custom plugin samples showing how to use the CAD Standards API from C++ and Visual Basic. The core requirement was to develop plugins that checked actual geometry in the drawing - something the standard plugins did not, as they focused on symbol table records such as layers and linetypes. The concept we came up with, at the time, was contrived but fun: to compare circles inside a drawing with those stored in the associated CAD Standards template (.DWS) file, and suggest changing the colours of the "incorrect" circles to those of the most similar size in the DWS file.
While this concept is, of course, something that no-one in their right mind would ever want to implement in a real software package, the principles shown are generic enough in nature and (in my opinion, at least) quite relevant to anyone wanting to check geometry using a CAD Standards plugin - something in which the developer asking the above question is clearly interested.
The CAD Standards API is a COM API, so while we implemented the original plugin sample in C++, the main objective at the time was to create a sample in VB6. This meant some changes were needed to the original API implementation, as although it was a COM API, the original API specification relied on datatypes that were not usable from VB6 clients (there was at least one pure COM interface in the API set, and not Automation-compatible, if I recall correctly). That's also why the COM interface we need to implement for a CAD Standards plugin is IAcStPlugin2 rather than IAcStPlugin.
The original VB6 sample, along with an early port to VB.NET (in this case VB7, the first .NET implementation of Visual Basic) are available as part of this DevNote (accessible to ADN members). The DevNote is, admittedly, out-of-date... we're in the process of during a mass refresh of KB content on the ADN site, so this was a very timely request (and allows me to contribute in some way to the content migration effort :-). There's quite a lot to this sample, so here is the source along with the supporting files I created when writing this post.
Here's the C# code:
//
// AutoCAD CAD Standards API Sample
//
// CircleStandard.cs : CAD Standards Plugin Sample for C#
//
// This sample adds a custom plugin to the CAD Standards
// Drawing Checker.
//
// The sample plugin tests for a match between the color of a
// circle in the current drawing, and any of the colors of
// circles contained in the specified standards (.DWS) files.
// All the colors of the standard circles are considered as
// fix candidates of the circle being checked. The recommended
// fix object will be the standard circle having the nearest
// radius to the circle being checked.
using AcStMgr;
using Autodesk.AutoCAD.Interop.Common;
using MSXML2;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace CircleStandard
{
[ProgId("CircleStandard.CircleStandard")]
public class CircleStandard : IAcStPlugin2
{
// Declare variables
private ContextList m_contexts =
new ContextList();
private AcStManager m_mgr;
private CircleStandard m_plugin;
private AcadDatabase m_checkDb;
private AcadDatabase m_dwsDb;
private AcStError m_err;
private object m_fixArray;
private CircleCache[] m_cirCacheArray;
private int m_recFixIndex;
private int m_curIndex;
private int m_fixCnt;
private string m_propName;
// Initialize
// Initializes the plugin
public void Initialize(AcStManager mgr)
{
// This is the only member function in which
// the interface is passed an IAcStManager interface.
// Store pointer to Manager object
m_mgr = mgr;
m_plugin = this;
}
// GetObjectFilter
// Plugin populates the provided array with class names
// of objects that it can check
public object GetObjectFilter()
{
// In this case we're only interested in circles
string[] filtArray = new string[1];
filtArray[0] = "AcDbCircle";
return filtArray;
}
// SetupForAudit
// Sets the context for a plugin to check a drawing
public void SetupForAudit(
AcadDatabase db,
string pathName,
object objNameArray,
object objPathArray,
object objDbArray)
{
// This method defines the context in which a plug-in
// will operate, specifically the drawing to check and
// the DWS files that should be used to check the drawing.
// Here we cache our DWS standards definitions and make
// an initial cache of circles in the DWG to be checked.
// NOTE: AcadDatabase objects contained in objDbArray
// are ***not*** guaranteed to be valid after this call.
// They should not be cached!!!
if (db != null)
{
// Cache a pointer to the database
m_checkDb = db;
// pDb is the DWG to be checked
// Store list of circles in drawing in m_ObjIDArray
if (m_checkDb != null)
{
// Cache list of all circles in the current drawing
foreach (AcadObject obj in
m_mgr.get_ModelSpaceProxy(m_checkDb))
{
if (obj.ObjectName == "AcDbCircle")
{
m_contexts.Add(obj.ObjectID, true);
}
}
}
object[] dbArray = (object[])objDbArray;
string[] nameArray = (string[])objNameArray;
string[] pathArray = (string[])objPathArray;
int i = 0;
// Iterate over the DWSes and cache properties (color
// and radius) of standard circles
for (int iDWS = 0; iDWS < dbArray.Length; iDWS++)
{
// Get the DWS database
m_dwsDb = (AcadDatabase)dbArray[iDWS];
foreach (AcadCircle stdCircle in
m_mgr.get_ModelSpaceProxy(m_dwsDb))
{
CircleCache cirCache = new CircleCache();
// CircleCache is utility object for storing
// properties
// Cache properties (color and radius) of all
// circles in the DWS database
cirCache.color = stdCircle.color;
cirCache.radius = stdCircle.Radius;
cirCache.standardFileName = nameArray[iDWS];
// pFix contains fix information to be passed back
// to the manager later
AcStFix fix = new AcStFix();
fix.Description = "Color fix";
fix.StandardFileName =
cirCache.standardFileName;
fix.FixObjectName =
"Color: " +
StripAcPrefix(stdCircle.color.ToString());
if (fix.PropertyCount == 0)
{
fix.PropertyValuePut(
"Color",
stdCircle.color
);
}
cirCache.pFix = fix;
Array.Resize<CircleCache>(
ref m_cirCacheArray,
i+1
);
m_cirCacheArray[i++] = cirCache;
}
}
}
}
// SetContext
// Sets the objects to examine when iterating over errors
public void SetContext(object objIdArray, bool useDb)
{
// If useDb is set to "true" (default), or if
// objIdArray is blank, we use the database (we get
// all ids for the current drawing). Otherwise, we
// set supplied list of objIdArrays as our list.
m_contexts.SetContext(useDb, objIdArray);
}
// Start
// Initializes the error iterator mechanism
public void Start(AcStError err)
{
// If pStartError is set to an error object, we should
// only start checking from that error, not from the
// beginning. Mostly we will just go the Next item at
// this point...
if (err != null)
{
long badId;
badId = err.BadObjectId;
// Find the index for BadObjectId in m_objIDArray
for (
m_curIndex = 0;
m_curIndex < m_contexts.Count;
m_curIndex++
)
{
if (m_contexts[m_curIndex] == badId)
{
m_curIndex = (m_curIndex - 1);
Next();
}
}
}
else
{
// No AcStError object was passed in. Start checking
// from the very begining
m_curIndex = -1;
Next();
}
}
// Next
// Finds the next error in the current context
public void Next()
{
m_err = null;
if (m_contexts.Count > 0)
{
// Drawing contains AcDbCircle objects
AcadCircle circle;
bool foundErr;
if (m_cirCacheArray.Length > 0)
{
// If we've not reached end of list, we first
// increment current list index
if (m_curIndex < m_contexts.Count - 1)
{
m_curIndex++;
foundErr = false;
while (m_curIndex < m_contexts.Count)
{
// Don't iterate beyond end of list
// Retrieve object using its ObjectId
try
{
circle =
(AcadCircle)m_checkDb.ObjectIdToObject(
(int)m_contexts[m_curIndex]
);
// Try to find a circle with the same color from
// the cached standard circle (Iterate over cached
// standards)
for (
int iCache = 0;
iCache < m_cirCacheArray.Length;
iCache++
)
{
if (circle.color.CompareTo(
m_cirCacheArray[iCache].color
) != 0)
{
// If it doesn't match, we've found a potential
// error
foundErr = true;
}
else
{
// If it matches any one standard, then we can
// stop checking
foundErr = false;
break;
}
}
// Check for color differences
if (foundErr)
{
// We found an error so create a local error
// object
AcStError err = new AcStError();
err.Description = "Color is non-standard";
err.BadObjectId = circle.ObjectID;
err.BadObjectName =
StripAcPrefix(
circle.color.ToString()
);
err.Plugin = m_plugin;
err.ErrorTypeName = "Color ";
err.ResultStatus =
AcStResultStatus.acStResFlagsNone;
if (err.PropertyCount == 0)
{
err.PropertyValuePut(
"Color",
circle.color
);
}
m_err = err;
foundErr = false;
break;
}
}
catch
{
}
m_curIndex = (m_curIndex + 1);
}
}
}
}
}
// Done
// Returns true if there are no more errors
public bool Done()
{
return (m_err == null);
}
// GetError -- Returns the current error
public AcStError GetError()
{
return m_err;
}
// GetAllFixes
// Returns an array of IAcStFix objects for the given
// error (note: The caller is responsible for releasing
// the objects in this array)
public void GetAllFixes(
AcStError err,
ref object fixArray,
ref int recommendedFixIndex
)
{
if (err != null)
{
IAcStFix[] arr =
new IAcStFix[m_cirCacheArray.Length];
ACAD_COLOR vErrorVal;
recommendedFixIndex = -1;
m_fixCnt = 0;
// If we have a cache of fixes, then use that
if (m_cirCacheArray.Length > 0)
{
for (int i = 0; i < m_cirCacheArray.Length; i++)
{
vErrorVal =
(ACAD_COLOR)err.PropertyValueGet("Color");
if (vErrorVal.CompareTo(
m_cirCacheArray[i].color
) != 0)
{
// If color property of fix matches error, then
// add to list of fixes.
arr[i] = m_cirCacheArray[i].pFix;
}
}
fixArray = arr;
m_fixArray = fixArray;
// Find the recommendedFixIndex
// (we call this function to retrieve the index -
// we don't need the returned fix object here)
GetRecommendedFix(err);
recommendedFixIndex = m_recFixIndex;
}
// Did we find a recommended fix along the way?
if (recommendedFixIndex == -1)
{
// No recomended fix, so set the proper flag on the
// error object
err.ResultStatus =
AcStResultStatus.acStResNoRecommendedFix;
}
}
}
// GetRecommendedFix
// Retrieves a fix object that describes the
// recommended fix
public AcStFix GetRecommendedFix(AcStError err)
{
AcStFix recFix = new AcStFix();
if (m_cirCacheArray.Length == 0)
{
err.ResultStatus =
AcStResultStatus.acStResNoRecommendedFix;
}
else
{
// Get the objectId for this error
long tmpObjID = err.BadObjectId;
// Retrieve the object to fix from the DWG
AcadCircle tmpCircle =
(AcadCircle)m_checkDb.ObjectIdToObject(
(int)tmpObjID
);
double radiusToBeChecked = tmpCircle.Radius;
CircleCache cirCache = m_cirCacheArray[0];
double diff =
Math.Abs(radiusToBeChecked - cirCache.radius);
m_recFixIndex = 0;
// Attempt to get a fix color from the cached
// m_CircleCacheArray
// Rule: the color of the standard circle with the
// nearest radius as the one to be fixed
for (int i = 0; i < m_cirCacheArray.Length; i++)
{
if (diff >
Math.Abs(
radiusToBeChecked - m_cirCacheArray[i].radius
)
)
{
cirCache = m_cirCacheArray[i];
diff =
Math.Abs(radiusToBeChecked - cirCache.radius);
m_recFixIndex = i;
}
}
// Populate properties of the recommended fix object
recFix.Description = "Color fix";
recFix.StandardFileName =
m_cirCacheArray[m_recFixIndex].
standardFileName;
recFix.FixObjectName = "Color";
if (recFix.PropertyCount == 0)
{
recFix.PropertyValuePut(
"Color",
m_cirCacheArray[m_recFixIndex].color
);
}
}
return recFix;
}
// GetPropertyDiffs
// Populates the provided arrays with the names of
// properties that are present in the provided
// error and fix objects (used to populate the fix
// dialog with 'property name, current value, fix value')
public void GetPropertyDiffs(
AcStError err,
AcStFix fix,
ref object objPropNames,
ref object objErrorValues,
ref object objFixValues,
ref object objFixableStatuses)
{
if (err != null)
{
string[] propNames = new string[0];
string propName = "";
string[] errorValues = new string[0];
object objErrorVal = new object();
string[] fixValues = new string[0];
object objFixVal = new object();
bool[] fixableStatuses = new bool[0];
// Iterate error properties
for (int i = 0; i < err.PropertyCount; i++)
{
err.PropertyGetAt(i, ref propName, ref objErrorVal);
m_propName = propName;
// Retrieve corresponding Fix property value
try
{
fix.PropertyValueGet(propName, ref objFixVal);
ACAD_COLOR errVal = (ACAD_COLOR)objErrorVal;
ACAD_COLOR fixVal = (ACAD_COLOR)objFixVal;
// Fix object has the same prop, so see if they match
if (errVal.CompareTo(fixVal) != 0)
{
// Store error and fix properties in array ready to
// pass back to caller
Array.Resize<string>(ref propNames, i+1);
propNames[i] = propName;
Array.Resize<string>(ref errorValues, i+1);
errorValues[i] = StripAcPrefix(errVal.ToString());
Array.Resize<string>(ref fixValues, i+1);
fixValues[i] = StripAcPrefix(fixVal.ToString());
Array.Resize<bool>(ref fixableStatuses, i+1);
fixableStatuses[i] = true;
}
}
catch
{
}
}
// Initialize the arrays supplied by caller
objPropNames = propNames;
objErrorValues = errorValues;
objFixValues = fixValues;
objFixableStatuses = fixableStatuses;
m_fixCnt++;
}
}
// StripAcPrefix
// Helper function to make color names prettier
private string StripAcPrefix(string p)
{
if (p.StartsWith("ac"))
return p.Substring(2);
else
return p;
}
// FixError
// Takes an error and a fix object and attempts
// to fix the error
public void FixError(
AcStError err,
AcStFix fix,
out string failureReason)
{
failureReason = "";
if (err != null)
{
long badObjID = err.BadObjectId;
// Retrieve object to fix from DWG
AcadCircle badObj =
(AcadCircle)m_checkDb.ObjectIdToObject(
(int)badObjID
);
if (fix == null)
{
// If the fix object is null then attempt to get
// the recommended fix
AcStFix tmpFix =
GetRecommendedFix(err);
if (tmpFix == null)
{
// Set the error's result status to failed and
// noRecommendedFix
err.ResultStatus =
AcStResultStatus.acStResNoRecommendedFix;
}
else
{
fix = tmpFix;
}
}
if (fix != null)
{
// Fix the bad circle
object sFixVal = new object();
fix.PropertyValueGet(m_propName, ref sFixVal);
ACAD_COLOR fixVal = (ACAD_COLOR)sFixVal;
try
{
badObj.color = fixVal;
err.ResultStatus =
AcStResultStatus.acStResFixed;
}
catch
{
err.ResultStatus =
AcStResultStatus.acStResFixFailed;
}
}
}
}
// Clear
// Clears the plugin state and releases any cached
// objects
public void Clear()
{
// Called just before a plugin is released.
// Use this function to tidy up after yourself
m_plugin = null;
m_curIndex = -1;
m_recFixIndex = -1;
m_fixCnt = 0;
m_propName = "";
m_mgr = null;
m_dwsDb = null;
m_checkDb = null;
if (m_err != null)
{
m_err.Reset();
m_err = null;
}
if (m_cirCacheArray != null)
{
for (int i = 0; i < m_cirCacheArray.Length; i++)
{
if (m_cirCacheArray[i].pFix != null)
{
m_cirCacheArray[i].pFix.Reset();
m_cirCacheArray[i].pFix = null;
}
}
}
m_contexts.Clear();
}
// CheckSysvar
// Checks a system variable
public void CheckSysvar(
string sysvarName,
bool getAllFixes,
ref bool passFail)
{
}
// StampDatabase
// Returns whether the plugin uses information
// from the database for checking
public void StampDatabase(
AcadDatabase db,
ref bool stampIt
)
{
// If the DWS contains circles, we stamp it by
// returning stampIt as true, otherwise, returning
// stampIt as false
stampIt = false;
foreach (
AcadObject obj in
m_mgr.get_ModelSpaceProxy(db)
)
{
if (obj.ObjectName == "AcDbCircle")
{
stampIt = true;
break;
}
}
}
// UpdateStatus
// Updates the result status of the provided error
public void UpdateStatus(AcStError err)
{
}
// WritePluginInfo
// Takes an AcStPluginInfoSection node and creates a
// new AcStPluginInfo node below it (note: used by the
// Batch Standards Checker to get information about the
// plugin)
public void WritePluginInfo(object objSectionNode)
{
IXMLDOMNode section =
(IXMLDOMNode)objSectionNode;
IXMLDOMElement xml =
section.ownerDocument.createElement(
"AcStPluginInfo"
);
IXMLDOMElement info =
(IXMLDOMElement)section.appendChild(xml);
info.setAttribute("PluginName", Name);
info.setAttribute("Version", Version);
info.setAttribute(
"Description",
Description
);
info.setAttribute("Author", Author);
info.setAttribute("HRef", HRef);
info.setAttribute("DWSName", "");
info.setAttribute("Status", "1");
}
// Author
// Returns the name of the plugin's author
public string Author
{
get { return "Kean Walmsley, Autodesk, Inc."; }
}
// Description
// Returns a description of what the plugin checks
public string Description
{
get
{
return
"Checks that circles in a drawing have a color " +
"that matches those of a similar radius in an " +
"associated standards file.";
}
}
// HRef
// Returns a URL where the plugin can be obtained
public string HRef
{
get
{
return
"http://blogs.autodesk.com/through-the-interface";
}
}
// Icon
// Returns the HICON property Icon
public int Icon
{
get { return 1; }
}
// Name
// Returns the name of the plugin
public string Name
{
get { return "Circle color checker"; }
}
// Version
// Returns the version of the plugin
public string Version
{
get { return "2.0"; }
}
// CircleCache
// Caches "standard" circle properties (Color, Radius)
// from .DWS files, as well as pointers to the circle's
// relevant AcStFix object
private class CircleCache
{
public double radius;
public ACAD_COLOR color;
public string standardFileName;
public AcStFix pFix;
}
// ContextList
// Manages list of objects to check - either all in
// database, or just those recently added or modified
private class ContextList
{
// List of objects to use when not in database context
List<long> m_altIdArray =
new List<long>();
List<long> m_dbIdArray =
new List<long>();
// All objects in database
private bool m_useDb;
// Return item from correct context list
public long this[int index]
{
get
{
if (m_useDb)
return m_dbIdArray[index];
else
return m_altIdArray[index];
}
}
// Number of items in current list
public int Count
{
get
{
if (m_useDb)
return m_dbIdArray.Count;
else
return m_altIdArray.Count;
}
}
// Flag to determine which conext list to return element
// from
// Select all database or just modified items for checking
// (but also add any new ids to database array
public void SetContext(bool useDb, object objContextArray)
{
if (!useDb && objContextArray != null)
{
m_useDb = false;
int[] idArray = (int[])objContextArray;
for (int i = 0; i < idArray.Length; i++)
{
long val = (long)idArray[i];
m_altIdArray.Add(val);
// Have to keep database list up to date
m_dbIdArray.Add(val);
}
}
else
{
// Clear
m_useDb = true;
m_altIdArray.Clear();
}
}
public void Add(long id, bool useDb)
{
if (useDb)
m_dbIdArray.Add(id);
else
m_altIdArray.Add(id);
}
// Clear both lists
public void Clear()
{
m_altIdArray.Clear();
m_dbIdArray.Clear();
}
}
}
}
Here are some additional steps to turn the above code into a buildable project and then into a working plugin:
- Create a Windows Class Library project (in this case for C#)
- Copy and paste the code into a .cs file in the project (whether the default Class1.cs file or a new one)
- Add a project reference to the appropriate ObjectDBX Type Library (in this case "AutoCAD/ObjectDBX Common 17.0 Type Library")
- Add a project reference to the Microsoft XML Type Library (in my case I used "Microsoft XML, v6.0")
- Add a project reference to AcStMgr.dll. This is actually harder than just adding the reference via the IDE by browsing to the Type Library on the ObjectARX SDK. The IDE shows this error:
To get around this, we need to use the TLBIMP tool. Launch a "Visual Studio Command Prompt" (in my case by using Start -> All Programs -> Microsoft Visual Studio 2005 -> Visual Studio Tools -> Visual Studio 2005 Command Prompt), from which you should browse to the appropriate ObjectARX SDK include folder (in my case "c:\Program Files\Autodesk\ObjectARX 2009\inc-win32"), and enter "tlbimp acstmgr.tlb" into the command window:
This creates an interop library (AcStMgr.dll) that can be added as a project reference instead of the original Type Library. When you add it, keep "CopyLocal = True", which will allow the later assembly registration step to work properly. The other assembly references can safely be changed to "CopyLocal = False", if you wish.
At this point the project should now build. Yay! :-)
For the plugin to be loadable inside AutoCAD, a few more steps are needed, however.
- Open the project's AssemblyInfo.cs file (under Properties in the project browser window), and set the ComVisible assembly attribute to true:
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
- From our command prompt, navigate to the location of the built assembly, and run "regasm /codebase CircleStandard.dll" (where CircleStandard.dll is the name of the assembly, whatever you've chosen to call it). Remember to use the /codebase flag - without this the COM information stored in the Registry will not include a reference to our assembly (only to the .NET Framework DLL that takes loads/hosts .NET assemblies that are also COM servers).
- Create a .reg file with the following contents, and merge it into the Registry:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\Drawing Check\Plugins2\CircleStandard.CircleStandard]
@="CircleStandard"
Once you've done that, you should now be able to launch AutoCAD and run the STANDARDS command, and check that our plugin exists:
By the way - all the above steps are needed (at least as far as I'm aware) for the plugin to show up in the list. If you happen to see the "Error obtaining plugin description" message on the command-prompt, the chances are you've missed a step. In these cases I use a SysInternals (now Microsoft) tool called Process Monitor to diagnose the problem. This tool is the combines RegMon and FileMon - two fantastic tools I used to use very regularly, back in the day. It will help you determine whether AutoCAD is failing to find the information it needs from the Registry or from the file system.
Now let's see how we can use this sample.
Let's start by setting up a DWS file...
Inside a blank drawing, create a number of circles, each of different radii and colours. I simply used the colour index corresponding to the radius, but you can use whatever you like:
Save this as a DWS file (I called mine CircleColors.dws), which you can then add to a DWG containing circles you've created:
Then via the STANDARDS or CHECKSTANDARDS command you can start checking - and fixing - your circles' colours:
Ending up with a nice set of colourful circles:
You can also change the standards settings to display violation alerts and to automatically fix issues:
You should now see the alert message when new circles are created that don't have the correct colour:
Automatic fixing certainly streamlines the fixing of standards violations - changing the colour to the one recommended by the plugin - but it won't stop the user from having to click through a couple of dialogs.
Well, that's it for today... hopefully you will find this sample a useful basis for implementing more specific requirements for geometry-oriented standards-checking.
Update:
See this more recent blog post for an updated project and set of instructions for AutoCAD 2015.