Having introduced this series, it’s time to look at some code. This first sample shows how to create and host a web-page that uses an external graphics library – in our case Paper.js – within an AutoCAD application. The main “trick” to this is going to be getting the data from the HTML page into AutoCAD, which we’ll do by extending AutoCAD’s shaping layer. Bear in mind that this code will work with AutoCAD 2015, but I can’t guarantee it will do so with 2014 (the JavaScript API was very much a “preview” in that release).
Something I should say, right off the bat, is that the samples you’ll see in the coming posts each have different origins, whether being based on different SDK samples or just from me coding them at different times. So – despite some effort from my side – there will be inconsistencies: some use jQuery, some do not, some have JavaScript embedded in the HTML page while others only use external files.
The samples all use some common approaches – especially with the AutoCAD-resident code – but you shouldn’t take my samples as “best practice” for working with HTML/JavaScript as much as demonstrations of possibilities.
As the main HTML/CSS/JavaScript code is all hosted on my blog – and can be accessed there by the application – let’s start with the piece that you’ll need to build into a local, AutoCAD-resident .NET DLL. You can also download the various files and place them in your DLL folder and load them locally by uncommenting a few lines of code. In fact, this is the main way I’ve been developing/testing this code, which is why I permitted myself to omit the code we saw in this previous post to check whether a URL is valid and loadable before trying to load the HTML page it points to.
Our C# code has ended up doing a lot more that I initially expected it to – please don’t be put off by its length. (If the details don’t interest you, please do scroll down to check out the embedded YouTube video with the code in action.)
It implements a couple of commands to load the web-page into an HTML-hosting palette or document (called PAPER and PAPDOC, respectively) and also defines a function that will be called from our JavaScript code – via an extension to the Shaping Layer, more on that later – with the Paper.js project encoded as a JSON string passed in as an argument.
This function – which is tagged with the JavaScriptCallback attribute making it callable from JavaScript – extracts the “path” data from the JSON file and generates corresponding Polyline entities that get added to a new block which gets inserted into the active document (or drawing document that launched the initial command it in the case where the HTML page is loaded into its own document window). A lot of the work was to make sure we create the block at the right size – both based on the extents of the paths we get from Paper.js but also the current zoom factor in AutoCAD – and that it’s centered on the cursor. The INSERT command will let you scale and rotate, but I wanted the initial size and position to make some sense.
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Newtonsoft.Json.Linq;
using System;
using System.IO;
using System.Reflection;
namespace JavaScriptExtender
{
public class Commands
{
private PaletteSet _pps = null;
private static Document _launchDoc = null;
private static double _fac;
[JavaScriptCallback("CreatePath")]
public string CreatePath(string jsonArgs)
{
var dm = Application.DocumentManager;
var doc = GetActiveDocument(dm);
// If we didn't find a document, return
if (doc == null)
return "";
// Lock the document, as we will be modifying it
using (var dl = doc.LockDocument())
{
var db = doc.Database;
var ed = doc.Editor;
// Unescape the string and remove surrounding quotes
// (could use a less manual approach for this, but hey)
var str = jsonArgs.Replace("\\\"", "\"");
if (str.StartsWith("\""))
str = str.Substring(1);
if (str.EndsWith("\"\n"))
str = str.Remove(str.Length - 2);
// Parse the JSON to extract the info we want
var a = JArray.Parse(str);
var kids = a[0][1]["children"];
if (kids == null)
return "";
// We'll collect Polylines in a collection
var paths = new DBObjectCollection();
// Aggregate the various vertices in a single point
// that we divide by the number of points to get the
// mean (the centroid of the shape)
var totalPoints = 0;
var total = Point3d.Origin;
// Also collect the bounding box of the polyline
var ext = new Extents3d();
// Create our Polylines relative to WCS
// (we'll transform them later, once we've
// collected the centroid)
var plane = new Plane(Point3d.Origin, Vector3d.ZAxis);
var pathNum = 0;
foreach (var path in kids)
{
// Get the next path
var p = path[1];
// We currently only care about the segments, but
// could also use the other information
// (linewidth, etc.)
var segments = p["segments"];
var width = p["strokeWidth"];
var cap = p["strokeCap"];
var join = p["strokeJoin"];
var pl = new Polyline();
var last = Point3d.Origin;
var segNum = 0;
foreach (var seg in segments)
{
// Points are grouped in sets of 3 (although the
// first 3 are together and then the next 2 plus
// one previous point make up the groups)
if (segNum > 0 && segNum % 2 == 0)
{
// Get our 3 points - we don't care about the
// tangent information as we'll just construct
// GeArcs instead
var pt1 = seg.Previous.Previous[0];
var pt2 = seg.Previous[0];
var pt3 = seg[0];
// Create corresponding Point3d objects
var start =
new Point3d((double)pt1[0], -(double)pt1[1], 0);
var mid =
new Point3d((double)pt2[0], -(double)pt2[1], 0);
var end =
new Point3d((double)pt3[0], -(double)pt3[1], 0);
last = end;
// Include our vertices in the extents
// (the end point is the start of the next -
// we'll use the last variable to get the
// last end point)
ext.AddPoint(start);
ext.AddPoint(mid);
// Aggregate all the start points so we can get
// the average at the end
total = start + total.GetAsVector();
totalPoints++;
// Create a CircularArc3d
var ca = new CircularArc3d(start, mid, end);
// Calculate the bulge factor using it
var b =
Math.Tan(0.25 * (ca.EndAngle - ca.StartAngle)) *
ca.Normal.Z;
// Add our vertex
pl.AddVertexAt(
pl.NumberOfVertices,
ca.StartPoint.Convert2d(plane),
b, 0, 0
);
}
segNum++;
}
// Add the final vertex
pl.AddVertexAt(
pl.NumberOfVertices, last.Convert2d(plane), 0, 0, 0
);
// Add the last vertex for completeness
total = last + total.GetAsVector();
totalPoints++;
// Also to our extents object
ext.AddPoint(last);
// Add our Polyline to the path collection
paths.Add(pl);
pathNum++;
}
// Now we can get the average vertex to make that
// the insertion point (i.e. the centroid)
total = total / totalPoints;
// We want the geometry to be created based on the
// current zoom factor. So let's try to get that
// from the Editor (which will fail in HTML document
// mode, hence the try-catch block)
double fac =
_launchDoc == null ? GetCurrentZoomFactor(doc) : _fac;
// Create a transformation to both displace the
// geometry - to use our centroid as the insertion
// point - and to scale the geometry to be nice and
// small (the INSERT command allows scaling upwards)
var totVec = total.GetAsVector();
var diag = ext.MaxPoint - ext.MinPoint;
var mat =
Matrix3d.Scaling(fac / diag.Length, Point3d.Origin).
PostMultiplyBy(Matrix3d.Displacement(-totVec));
// If we have some Polylines, add them to our block
// or the current space
if (paths.Count > 0)
{
var blockRoot = "CLOUD";
bool createAndInsert = !String.IsNullOrEmpty(blockRoot);
string blockName = null;
using (var tr = doc.TransactionManager.StartTransaction())
{
BlockTableRecord btr;
// If we're creating and inserting, starting by
// creating the blank block definition
if (createAndInsert)
{
// Get the block table, initially for read
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId, OpenMode.ForRead
);
// Loop until we have a valid (unused) block name
var num = 0;
do
{
blockName = String.Format(blockRoot + "{0}", num);
num++;
}
while (bt.Has(blockName));
// Create our block definition, opening the block
// table for write in order to add it
btr = new BlockTableRecord();
btr.Name = blockName;
bt.UpgradeOpen();
bt.Add(btr);
bt.DowngradeOpen();
tr.AddNewlyCreatedDBObject(btr, true);
}
else
{
// If we're adding to the current space (rather than
// a new block), just open it for write
btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId, OpenMode.ForWrite
);
}
// Loop through our collection, adding all the entities
// (after transforming them for displacement and scale)
// and disposing of anything else (should be nothing)
foreach (DBObject o in paths)
{
var ent = o as Entity;
if (ent != null)
{
ent.TransformBy(mat);
btr.AppendEntity(ent);
tr.AddNewlyCreatedDBObject(ent, true);
}
else
{
o.Dispose();
}
}
// At this stage we're done with the drawing changes,
// so commit the transaction
tr.Commit();
}
// Our Editor-based code is wrapped in a try-catch
// block as accessing the Editor will cause
// an exception to be thrown in HTML document mode
try
{
// Let's try to insert our block, if we created one
if (createAndInsert && !String.IsNullOrEmpty(blockName))
{
// If running in our own document window,
// activate the associated drawing first
if (dm.MdiActiveDocument != doc)
dm.MdiActiveDocument = doc;
// We'll use SendStringToExecute() as it works
// well for these purposes
doc.SendStringToExecute(
"_.-INSERT " + blockName + "\n", true, true, false
);
}
else
{
// If we're adding to the current space, just
// have the screen redraw to show the results
ed.UpdateScreen();
}
}
catch { }
}
}
return "{\"retCode\":0}";
}
private static Matrix3d Dcs2Wcs(AbstractViewTableRecord v)
{
return
Matrix3d.Rotation(-v.ViewTwist, v.ViewDirection, v.Target) *
Matrix3d.Displacement(v.Target - Point3d.Origin) *
Matrix3d.PlaneToWorld(v.ViewDirection);
}
// Get the current amount of zoom - I probably shouldn't use
// the term "zoom factor" as it tends to mean something else.
// But it's a factor - in drawing units - that shows how
// zoomed into a model we are
private static double GetCurrentZoomFactor(Document doc)
{
var fac = 0.0;
try
{
// Let's try to get the Editor - will fail if called from
// an HTML document
var ed = doc.Editor;
// Get the view and a diagonal vector across the extents
var vtr = ed.GetCurrentView();
var vec = new Vector3d(vtr.Width, vtr.Height, 0);
// Capture the visible extents in an object
var screenExt = new Extents3d();
// Get the centre of the screen in WCS and use it
// with the diagonal vector to add the corners to the
// extents object
var ctr =
new Point3d(vtr.CenterPoint.X, vtr.CenterPoint.Y, 0);
var dcs = Dcs2Wcs(vtr);
screenExt.AddPoint((ctr + 0.5 * vec).TransformBy(dcs));
screenExt.AddPoint((ctr - 0.5 * vec).TransformBy(dcs));
// Calculate the length of the diagonal in WCS
// then return a tenth of that as our factor
var diag2 =
(screenExt.MaxPoint - screenExt.MinPoint).Length;
fac = diag2 / 10;
}
catch { }
return fac;
}
private Document GetActiveDocument(DocumentCollection dm)
{
// If we're called from an HTML document, the active
// document may be null
var doc = dm.MdiActiveDocument;
if (doc == null)
{
doc = _launchDoc;
}
return doc;
}
[CommandMethod("PAPDOC")]
public static void PaperDocument()
{
_launchDoc = Application.DocumentManager.MdiActiveDocument;
_fac = GetCurrentZoomFactor(_launchDoc);
_launchDoc.BeginDocumentClose +=
(s, e) => { _launchDoc = null; };
Application.DocumentWindowCollection.AddDocumentWindow(
"Paper.js Document", GetHtmlPathPaper()
);
}
[CommandMethod("PAPER")]
public void PaperPalette()
{
_launchDoc = null;
_pps =
ShowPalette(
_pps,
new Guid("B0B176D0-6402-4D1A-8D44-4CB2EA8F29C0"),
"PAPER",
"Paper.js Example",
GetHtmlPathPaper()
);
}
// Helper function to show a palette
private PaletteSet ShowPalette(
PaletteSet ps, Guid guid, string cmd, string title, Uri uri
)
{
if (ps == null)
{
ps = new PaletteSet(cmd, guid);
}
else
{
if (ps.Visible)
return ps;
}
if (ps.Count != 0)
{
ps[0].PaletteSet.Remove(0);
}
ps.Add(title, uri);
ps.Visible = true;
return ps;
}
// Helper function to get the path to our HTML files
private static string GetHtmlPath()
{
// Use this approach if loading the HTML from the same
// location as your .NET module
//var asm = Assembly.GetExecutingAssembly();
//return Path.GetDirectoryName(asm.Location) + "\\";
return "http://through-the-interface.typepad.com/files/";
}
private static Uri GetHtmlPathPaper()
{
return new Uri(GetHtmlPath() + "paperclouds.html");
}
}
}
Again, none of this is better than what we already have with the REVCLOUD command inside AutoCAD, but it does show the interoperability potential of an HTML/JavaScript app that generates geometry inside AutoCAD.
Here’s the code in action, generating a few revision clouds both from an AutoCAD-based palette and a separate document window (using a sample drawing borrowed from here):
If you’re interested in the code that’s hosted on my blog and accessed from there, read on!
Here’s the HTML page that uses Paper.js to draw our clouds: the “Paperscript” code that does this is incredibly compact. The function at the bottom is one you’ll see in each of the samples in this series (and is a technique I borrowed from the Isomer samples) – it just goes through and generates a set of buttons dynamically based on the JavaScript “commands” that are in the code.
<!doctype html>
<html>
<head>
<meta charset="UTF-8">
<title>Clouds</title>
<link rel="stylesheet" href="style.css">
<script type="text/javascript" src="js/paper-full.min.js">
</script>
<script
src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">
</script>
<script src="js/acadext.js"></script>
<script type="text/paperscript" canvas="canvas">
function init() {
project.currentStyle = {
strokeColor: 'black',
strokeWidth: 5,
strokeJoin: 'round',
strokeCap: 'round'
};
}
// The user has to drag the mouse at least 30pt before the
// mouse drag event is fired:
tool.minDistance = 30;
var path;
function onMouseDown(event) {
path = new Path();
path.add(event.point);
}
function onMouseDrag(event) {
path.arcTo(event.point, true);
}
function Commands() {}
Commands.clear = function () {
project.activeLayer.removeChildren();
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
ctx.clearRect(0, 0, c.width, c.height);
};
Commands.insert = function () {
sendPathToAutoCAD(project.exportJSON());
};
(function () {
init();
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth - 25,
canvas.height = window.innerHeight - 65;
var panel = document.getElementById('control');
for (var fn in Commands) {
var button = document.createElement('div');
button.classList.add('cmd-btn');
button.innerHTML = fn;
button.onclick = (function (fn) {
return function () { fn(); };
})(Commands[fn]);
panel.appendChild(button);
panel.appendChild(document.createTextNode('\u00a0'));
}
})();
</script>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="control"></div>
</body>
</html>
Here’s the extension to the Shaping Layer that is called from this code, which just passes through the provided JSON directly as an argument (and in reality won’t care very much about the result).
function sendPathToAutoCAD(jsonArgs) {
var jsonResponse =
exec(
JSON.stringify({
functionName: 'CreatePath',
invokeAsCommand: false,
functionParams: jsonArgs
})
);
var jsonObj = JSON.parse(jsonResponse);
if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {
throw Error(jsonObj.retErrorString);
}
return jsonObj.result;
}
And for completeness, here’s the CSS that (mainly) shows the nice buttons at the bottom:
#canvas {
display: block;
margin: 0 auto;
}
#control {
position: absolute;
}
.cmd-btn {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
font-size: 24px;
border: 3px solid #555;
border-radius: 5px;
color: #555;
background: transparent;
text-transform: uppercase;
padding: 15px;
width: 120px;
text-align: center;
display: inline
}
.cmd-btn:hover {
cursor: pointer;
color: #fff;
background-color: #555;
}
That’s it for the initial Paper.js and AutoCAD integration. At some point I’d quite like to see investigate taking 2D geometry from AutoCAD and manipulating it using Paper.js (it has some nice path simplification capabilities for hand-drawn curves, for instance).
But next up is a look at integrating Isomer to display 2D isometric views of 3D AutoCAD geometry.