Over the last few months I’ve had a number of people ask me for an update to this 6-year old post on implementing a CAD standards plugin for AutoCAD (which in turn was based on code from an older DevNote created from a much older VB6 sample).
Augusto Gonçalves from the ADN team very kindly made a start on this while I was out on vacation, providing a basic port that I’ve now just put a few finishing touches on. Very few changes were actually needed from the code in the original post, thankfully. Also, I did do my best to make sure the code still supports Win32 as well as x64, although admittedly I’ve only tested the code and process on 64-bit Windows. And as you need to reference a platform-specific COM component you won't (easily?) be able to build a single plugin that works on both.
Rather than repeating the bulk of the instructions from the previous post, here’s a Screencast in which I go through the process of building and loading the plugin (all it doesn’t show is downloading and extracting the plugin’s source project):
Here’s the updated 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 AXDBLib;
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
return new string[] { "AcDbCircle" };
}
// 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);
}
}
}
var dbArray = (object[])objDbArray;
var nameArray = (string[])objNameArray;
var 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
var 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)
{
var 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(
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
var 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)
{
var 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)
{
var recFix = new AcStFix();
if (m_cirCacheArray.Length == 0)
{
err.ResultStatus =
AcStResultStatus.acStResNoRecommendedFix;
}
else
{
// Get the objectId for this error
var tmpObjID = err.BadObjectId;
// Retrieve the object to fix from the DWG
var tmpCircle =
(AcadCircle)m_checkDb.ObjectIdToObject(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)
{
var propNames = new string[0];
var propName = "";
var errorValues = new string[0];
var objErrorVal = new object();
var fixValues = new string[0];
var objFixVal = new object();
var 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);
var errVal = (ACAD_COLOR)objErrorVal;
var 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)
{
var badObjID = err.BadObjectId;
// Retrieve object to fix from DWG
var badObj =
(AcadCircle)m_checkDb.ObjectIdToObject(badObjID);
if (fix == null)
{
// If the fix object is null then attempt to get
// the recommended fix
var 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
var sFixVal = new object();
fix.PropertyValueGet(m_propName, ref sFixVal);
var 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)
{
var section = (IXMLDOMNode)objSectionNode;
var xml =
section.ownerDocument.createElement("AcStPluginInfo");
var 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 from which context list to return element
// 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;
long[] idArray = (long[])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();
}
}
long IAcStPlugin2.Icon
{
get { return 0; }
}
}
}
Thanks to Augusto for his help getting this ready!