It seems like I’ve been living in JavaScript land (and no, I deliberately didn’t say “hell” – it’s actually been fun :-) for the last few weeks, between one thing or another. But I think I’ve finally put the finishing touches on the last of the JavaScript API samples I’ve prepared for AU 2014.
This sample was inspired by Jim Awe – an old friend and colleague – who is working on something similar for another platform. So I can’t take any credit for the way it works, just for the plumbing it took to make it work with AutoCAD.
It’s basically an HTML palette using a handy open source library called D3.js – for Data-Driven Documents – and d3pie, a layer on top of that to simplify creating pie charts. The palette connects to the active drawing and asks to .NET to provides some data on the entities inside modelspace. From our .NET code we use LINQ to query the types of object from the modelspace’s ObjectIds, which we then package up as JSON and return for display in HTML.
This is all that’s needed to get this data, in case – it can all be done via the ObjectId without opening any of the entities (just the modelspace). LINQ is really great at this kind of query.
var q =
from ObjectId o in ms
group o by o.ObjectClass.DxfName into counts
select new { Count = counts.Count(), Group = counts.Key };
When the user clicks on a wedge in the pie chart – representing the objects of a particular type – those objects get places in the pickfirst selection set, ready for something to be done with them.
Here’s a screencast of the application working:
Here’s the HTML…
<!doctype html>
<html>
<head>
<title>Chart</title>
<link rel="stylesheet" href="style.css">
<style>
html, body { height: 100%; width: 100%; margin: 0; padding: 0; }
body { display: table; }
</style>
<script
src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">
</script>
<script src="js/acadext2.js"></script>
<script src="js/d3.min.js"></script>
<script src="js/d3pie.min.js"></script>
<script src="js/chart.js"></script>
</head>
<body onload="init();">
<div id="pieChart" class="centered-on-page">
</div>
</body>
</html>
Here’s the JavaScript, including the Shaping Layer extensions…
function getObjectCountsFromAutoCAD() {
var jsonResponse =
exec(
JSON.stringify({
functionName: 'GetObjectCountData',
invokeAsCommand: false,
functionParams: undefined
})
);
var jsonObj = JSON.parse(jsonResponse);
if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {
throw Error(jsonObj.retErrorString);
}
return jsonObj.result;
}
function selectObjectsOfType(jsonArgs) {
var jsonResponse =
exec(
JSON.stringify({
functionName: 'SelectObjectsOfType',
invokeAsCommand: false,
functionParams: jsonArgs
})
);
var jsonObj = JSON.parse(jsonResponse);
if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {
throw Error(jsonObj.retErrorString);
}
return jsonObj.result;
}
var _pie = null;
function init() {
registerCallback("refpie", refreshPie);
loadPieData();
}
function refreshPie(args) {
loadPieData();
}
function loadPieData() {
var pieOpts = setupPieDefaults();
try {
var contents = getObjectCountsFromAutoCAD();
if (contents) {
pieOpts.data = contents;
if (_pie)
_pie.destroy();
_pie = new d3pie("pieChart", pieOpts);
}
}
catch (ex) {
_pie.destroy();
}
}
function clickPieWedge(evt) {
selectObjectsOfType(
{ "class": evt.data.label, "expanded": evt.expanded }
);
}
function setupPieDefaults() {
var pieDefaults = {
"header": {
"title": {
"text": "Object Types",
"fontSize": 24,
"font": "Calibri"
},
"subtitle": {
"text": "Quantities of objects in modelspace.",
"color": "#999999",
"fontSize": 12,
"font": "Calibri"
},
"titleSubtitlePadding": 9
},
"data": {
// nothing initially
},
"footer": {
"color": "#999999",
"fontSize": 10,
"font": "Calibri",
"location": "bottom-left"
},
"size": {
"canvasWidth": 400,
"pieInnerRadius": "49%",
"pieOuterRadius": "81%"
},
"labels": {
"outer": {
"pieDistance": 32
},
"inner": {
//"hideWhenLessThanPercentage": 3,
"format": "value"
},
"mainLabel": {
"fontSize": 11
},
"percentage": {
"color": "#ffffff",
"decimalPlaces": 0
},
"value": {
"color": "#adadad",
"fontSize": 11
},
"lines": {
"enabled": true
}
},
"effects": {
"pullOutSegmentOnClick": {
"effect": "linear",
"speed": 400,
"size": 8
}
},
"misc": {
"gradient": {
"enabled": true,
"percentage": 100
}
},
"callbacks": {
onClickSegment: clickPieWedge
}
};
return pieDefaults;
}
And here’s the C# code…
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Newtonsoft.Json.Linq;
using System;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace JavaScriptSamples
{
public class ChartCommands
{
private PaletteSet _chps = null;
private static Document _curDoc = null;
private bool _refresh = false;
[DllImport(
"AcJsCoreStub.crx", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "acjsInvokeAsync")]
extern static private int acjsInvokeAsync(
string name, string jsonArgs
);
[CommandMethod("CHART")]
public void ChartPalette()
{
// We're storing the "launch document" as we're attaching
// various event handlers to it
_curDoc =
Application.DocumentManager.MdiActiveDocument;
// Only attach event handlers if the palette isn't already
// there (in which case it will already have them)
var attachHandlers = (_chps == null);
_chps =
Utils.ShowPalette(
_chps,
new Guid("F76509E7-25E4-4415-8C67-2E92118F3B84"),
"CHART",
"D3.js Examples",
GetHtmlPathChart()
);
if (attachHandlers && _curDoc != null)
{
AddHandlers(_curDoc);
Application.DocumentManager.DocumentActivated +=
OnDocumentActivated;
_curDoc.BeginDocumentClose +=
(s, e) =>
{
RemoveHandlers(_curDoc);
_curDoc = null;
};
// When the PaletteSet gets destroyed we remove
// our event handlers
_chps.PaletteSetDestroy += OnPaletteSetDestroy;
}
}
[JavaScriptCallback("SelectObjectsOfType")]
public string SelectObjectsOfType(string jsonArgs)
{
// Default result is an error
var res = "{\"retCode\":1}";
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return res;
var ed = doc.Editor;
//ed.SetImpliedSelection(new ObjectId[]{});
// Extract the DXF name to select from the JSON arguments
var jo = JObject.Parse(jsonArgs);
var dxfName = jo.Property("class").Value.ToString();
var expanded = (bool)jo.Property("expanded").Value;
// We'll select all the entities of this class
var tvs =
new TypedValue[] {
new TypedValue((int)DxfCode.Start, dxfName)
};
// If the wedge is already expanded, we want to clear the
// pickfirst set (so the default value is null)
ObjectId[] ids = null;
if (!expanded)
{
// Perform the selection
var sf = new SelectionFilter(tvs);
var psr = ed.SelectAll(sf);
if (psr.Status != PromptStatus.OK)
return res;
// Get the results in our array
ids = psr.Value.GetObjectIds();
}
// Set or clear the pickfirst selection
ed.SetImpliedSelection(ids);
// Set the focus on the main window for the update to display
// (this works fine when floating, less well when docked)
Application.MainWindow.Focus();
// Return success
return "{\"retCode\":0}";
}
[JavaScriptCallback("GetObjectCountData")]
public string GetObjectData(string jsonArgs)
{
var doc = Application.DocumentManager.MdiActiveDocument;
if (doc == null)
return "{\"retCode\":1}";
// Initialize the JSON string to return the count information
var sb =
new StringBuilder("{\"retCode\":0, \"result\":");
sb.Append("{\"sortOrder\":\"value-desc\",\"content\":[");
using (
var tr = doc.TransactionManager.StartOpenCloseTransaction()
)
{
bool first = true;
var ms =
(BlockTableRecord)tr.GetObject(
SymbolUtilityServices.GetBlockModelSpaceId(doc.Database),
OpenMode.ForRead
);
// Use LINQ to count the objects in the modelspace,
// grouping the results by type (all done via ObjectIds,
// no need to open the objects themselves)
var q =
from ObjectId o in ms
group o by o.ObjectClass.DxfName into counts
select new { Count = counts.Count(), Group = counts.Key };
// Serialize the results out to JSON
foreach (var i in q)
{
if (!first)
sb.Append(",");
first = false;
sb.AppendFormat(
"{{\"label\":\"{0}\",\"value\":{1}}}", i.Group, i.Count
);
}
tr.Commit();
}
sb.Append("]}}");
return sb.ToString();
}
private void OnDocumentActivated(
object s, DocumentCollectionEventArgs e
)
{
if (_chps != null && e.Document != _curDoc)
{
// We're going to monitor when objects get added and
// erased. We'll use CommandEnded to refresh the
// palette at most once per command (might also use
// DocumentManager.DocumentLockModeWillChange)
// The document is dead...
RemoveHandlers(_curDoc);
// ... long live the document!
_curDoc = e.Document;
AddHandlers(_curDoc);
if (_curDoc != null)
{
// Refresh our palette by setting the flag and running
// a command (could be any command, we've chosen REGEN)
_refresh = true;
_curDoc.SendStringToExecute(
"_.REGEN ", false, false, false
);
}
else
{
acjsInvokeAsync("refpie", "{}");
}
}
}
private void AddHandlers(Document doc)
{
if (doc != null)
{
if (doc.Database != null)
{
doc.Database.ObjectAppended += OnObjectAppended;
doc.Database.ObjectErased += OnObjectErased;
}
doc.CommandEnded += OnCommandEnded;
}
}
private void RemoveHandlers(Document doc)
{
if (doc != null)
{
if (doc.Database != null)
{
doc.Database.ObjectAppended -= OnObjectAppended;
doc.Database.ObjectErased -= OnObjectErased;
}
doc.CommandEnded -= OnCommandEnded;
}
}
private void OnObjectAppended(object s, ObjectEventArgs e)
{
_refresh = true;
}
private void OnObjectErased(object s, ObjectErasedEventArgs e)
{
_refresh = true;
}
private void OnCommandEnded(object s, CommandEventArgs e)
{
// Invoke our JavaScript functions to refresh the palette
if (_refresh && _chps != null)
{
acjsInvokeAsync("refpie", "{}");
_refresh = false;
}
}
private void OnPaletteSetDestroy(object s, EventArgs e)
{
// When our palette is closed, detach the various
// event handlers
if (_curDoc != null)
{
RemoveHandlers(_curDoc);
_curDoc = null;
}
}
private static Uri GetHtmlPathChart()
{
return new Uri(Utils.GetHtmlPath() + "chart.html");
}
}
}
I’ve actually found this to be quite a useful little sample: not just from the way it shows interactions from HTML5/JavaScript to .NET and back but also from a user perspective. If you want to quickly select all the objects of a particular type from a drawing – perhaps to change their layer or erase them, then this tool could be very handy. It’s essentially a streamlined, graphical version of the QSELECT command.