After introducing the series and looking at a sample 2D JavaScript application, it’s time to go 3D. Well, 2.5D, anyway. We’re going to implement a simple sample using the Isomer library that extracts bounding box information about 3D solids – which could be extended to get more detailed topological information, albeit with quite some work – and displays them in an isometric view in the HTML canvas.
This time we’re only going to have a single button in our UI allowing model updates to be refreshed in our isometric view. I decided to leave this as a manual operation, but it’s certainly possible to do make this more automatic, pulling down updated data when relevant changes have been made to the model. But that’s for another day.
The other notable point about the implementation is the (admittedly somewhat crude) approach I’ve taken to help the results appear properly. Isomer draws the solids in the order you ask it to, so as it stands you really need to manage “draw order” in your own application. The below code has some additional logic that sorts the solids – or at least their bounding boxes – in a list keyed off the distance between the camera point and the centre of their nearest face. This is based on the current camera position in the relevant AutoCAD drawing – assuming its Editor object can be accessed – so you can tweak the results by changing the view (even if the isometric view is always what we’d consider “south-west isometric” in AutoCAD parlance.
Otherwise the C# code is actually much simpler than for the Paper.js integration (and shares a fair amount of commonality with it).
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
namespace JavaScriptExtender
{
public class Commands
{
private PaletteSet _isops = null;
private static Document _launchDoc = null;
private static Point3d _camPos = Point3d.Origin;
[JavaScriptCallback("GetSolids")]
public string GetSolids(string jsonArgs)
{
var doc = GetActiveDocument(Application.DocumentManager);
// If we didn't find a document, return
if (doc == null)
return "";
// We could probably get away without locking the document
// - as we only need to read - but it's good practice to
// do it anyway
using (var dl = doc.LockDocument())
{
var db = doc.Database;
var ed = doc.Editor;
// We'll sort our list of extents objects based on a
// distance value
var sols = new SortedList<double, Extents3d>();
using (var tr = doc.TransactionManager.StartTransaction())
{
// Start by getting the modelspace
var ms =
(BlockTableRecord)tr.GetObject(
SymbolUtilityServices.GetBlockModelSpaceId(db),
OpenMode.ForRead
);
// If in palette mode we can get the camera from the
// Editor, otherwise we rely on what was provided when
// the HTML document was launched
var camPos = _camPos;
if (_launchDoc == null)
{
var view = ed.GetCurrentView();
_camPos = view.Target + view.ViewDirection;
}
// Get each Solid3d in modelspace and add its extents
// to the sorted list keyed off the distance from the
// closest face of the solid (not necessarily true,
// but this only really is a crude approximation)
foreach (var id in ms)
{
var obj = tr.GetObject(id, OpenMode.ForRead);
var sol = obj as Solid3d;
if (sol != null)
{
var ext = sol.GeometricExtents;
var tmp =
ext.MinPoint + 0.5 * (ext.MaxPoint - ext.MinPoint);
var mid = new Point3d(ext.MinPoint.X, tmp.Y, tmp.Z);
var dist = camPos.DistanceTo(mid);
sols.Add(dist, ext);
}
}
tr.Commit();
}
return GetSolidsString(sols);
}
}
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;
}
// Helper function to build a JSON string containing our
// sorted extents list
private string GetSolidsString(SortedList<double, Extents3d> lst)
{
var sb = new StringBuilder("{\"retCode\":0, \"result\":[");
var keys = lst.Keys;
for (int i = keys.Count - 1; i >= 0; i--)
{
if (i < keys.Count - 1)
sb.Append(",");
var ext = lst[keys[i]];
sb.Append(
string.Format(
"{{\"min\":{0},\"max\":{1}}}",
JsonConvert.SerializeObject(ext.MinPoint),
JsonConvert.SerializeObject(ext.MaxPoint)
)
);
}
sb.Append("]}");
return sb.ToString();
}
[CommandMethod("ISODOC")]
public static void IsomerDocument()
{
_launchDoc = Application.DocumentManager.MdiActiveDocument;
var view = _launchDoc.Editor.GetCurrentView();
_camPos = view.Target + view.ViewDirection;
_launchDoc.BeginDocumentClose +=
(s, e) => { _launchDoc = null; };
Application.DocumentWindowCollection.AddDocumentWindow(
"Isomer Document", GetHtmlPathIso()
);
}
[CommandMethod("ISOMER")]
public void IsomerPalette()
{
_launchDoc = null;
_camPos = Point3d.Origin;
_isops =
ShowPalette(
_isops,
new Guid("280D8D13-AA13-4A62-AD9F-33D479308DFD"),
"ISOMER",
"Isomer Examples",
GetHtmlPathIso()
);
}
// 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 GetHtmlPathIso()
{
return new Uri(GetHtmlPath() + "isosolids.html");
}
}
}
Here’s the code in action:
And now for the supporting files from the blog. Firstly, the HTML file:
<!doctype html>
<html>
<head>
<title>Isometric Solids</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<script src="js/isomer.min.js"></script>
<script
src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">
</script>
<script src="js/acadext.js"></script>
<script src="js/isosolids.js"></script>
</body>
</html>
This sample also uses an external JavaScript file:
var iso;
var Point = Isomer.Point;
var Path = Isomer.Path;
var Shape = Isomer.Shape;
var Color = Isomer.Color;
var offX = 0, offY = 0;
function init() {
// Get our solids from AutoCAD
sols = getSolidsFromAutoCAD();
// Get the overall bounding box
var minX = Infinity,
minY = Infinity,
minZ = Infinity,
maxX = -Infinity,
maxY = -Infinity,
maxZ = -Infinity;
for (var sol in sols) {
var s = sols[sol];
minX = Math.min(minX, s.min.X, s.max.X);
minY = Math.min(minY, s.min.Y, s.max.Y);
minZ = Math.min(minZ, s.min.Z, s.max.Y);
maxX = Math.max(maxX, s.min.X, s.max.X);
maxY = Math.max(maxY, s.min.Y, s.max.Y);
maxZ = Math.max(maxZ, s.min.Z, s.max.X);
}
var delX = Math.abs(maxX - minX),
delY = Math.abs(maxY - minY),
delZ = Math.abs(maxZ - minZ);
// Establish an offset for our geometry, so it all
// gets displayed properly
offX = -minX,
offY = -minY;
// Get the diagonal length from min to max
var diag = Math.sqrt(delX * delX + delY * delY + delZ * delZ);
// Scale the Isomer canvas based on the size of this diagonal
var canvas = document.createElement('canvas');
canvas.width = window.innerWidth - 25,
canvas.height = window.innerHeight - 65;
document.body.appendChild(canvas);
var smaller = Math.min(canvas.width, canvas.height);
var scale = smaller / diag;
iso = new Isomer(canvas, { scale: scale });
var container = document.createElement('div');
container.setAttribute('id', 'control');
container.style.background = '#FFFFFF';
document.body.appendChild(container);
}
function Commands() {}
Commands.refresh = function () {
iso.canvas.clear();
var sols = getSolidsFromAutoCAD();
for (var sol in sols) {
var s = sols[sol];
iso.add(
Shape.Prism(
new Point(
offX + s.min.X, offY + s.min.Y, s.min.Z
),
Math.abs(s.max.X - s.min.X),
Math.abs(s.max.Y - s.min.Y),
Math.abs(s.max.Z - s.min.Z)
)
);
}
};
(function () {
init();
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);
}
Commands.refresh();
})();
Here’s the accompanying extension to AutoCAD’s Shaping Layer:
function getSolidsFromAutoCAD() {
var jsonResponse =
exec(
JSON.stringify({
functionName: 'GetSolids',
invokeAsCommand: false,
functionParams: undefined
})
);
var jsonObj = JSON.parse(jsonResponse);
if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {
throw Error(jsonObj.retErrorString);
}
return jsonObj.result;
}
The stylesheet is the same as the one we we saw last time.
Clearly there are lots of places this could go. Supporting more elaborate topology is one area for enhancement, whether via native objects or (and this would be pretty darn cool) decomposing them into Minecraft-style blocks. The code is also not handling different orientations of geometry – we’re just displaying bounding boxes aligned with WCS: we could support object-level transformations as well as views from different angles (which would mean transforming the entire model before adding it to the Isomer canvas, presumably). And having more control over the zoom factor and position of the resultant geometry would be helpful, of course.
That’s it for this week. Next week we’re going to look at a sample that has a lot of similarities to this one at the code level – we use the same GetSolids() function, for instance – but this time we’re going to visualize the bounding boxes in full 3D using Three.js. We’ll also take the opportunity to take a sneak peak at some really interesting things Autodesk is doing in this space.