To follow on from yesterday’s post, today we’re going to look at two C# source files that work with the HTML page – and referenced JavaScript files – which I will leave online rather than reproducing here.
As a brief reminder of the functionality – if you haven’t yet watched the screencast shown last time – this version of the app shows an embedded 3D view that reacts to the creation – and deletion – of geometry from the associated AutoCAD model. You will see the bounding boxes for geometry appear in the WebGL view (powered by Three.js) as you’re modeling.
The code is a bit different to the approach we took to display the last area, earlier in the week: we do look for entities that are added to/removed from the document we care about, but we pass through the list of those added/removed by each command, not just the area of the latest. On the JavaScript side of things we add the handle of the associated entity as the Three.js name, allowing us to retrieve the object again in case it gets erased.
This is ultimately a more interesting approach for people wanting to track more detailed information about modeling operations (although admittedly we’re still only passing geometric extents and the handle – we’re not dealing with more complicated data in this “simple” sample).
Here’s the first of the C# source files, which defines the AutoCAD commands to create a palette or an HTML document inside AutoCAD (this latter one is now a bit boring in comparison: it creates a static snapshot of the launching document, but doesn’t track any changes afterwards… the palette is a lot more fun :-).
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.Runtime.InteropServices;
namespace JavaScriptSamples
{
public class ThreeCommands
{
private PaletteSet _3ps = null;
private static Document _curDoc = null;
private static ObjectIdCollection _add =
new ObjectIdCollection();
private static ObjectIdCollection _remove =
new ObjectIdCollection();
[DllImport(
"AcJsCoreStub.crx", CharSet = CharSet.Auto,
CallingConvention = CallingConvention.Cdecl,
EntryPoint = "acjsInvokeAsync")]
extern static private int acjsInvokeAsync(
string name, string jsonArgs
);
[CommandMethod("THREE")]
public void ThreePalette()
{
// 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 = (_3ps == null);
_3ps =
Utils.ShowPalette(
_3ps,
new Guid("9CEE43FF-FDD7-406A-89B2-6A48D4169F71"),
"THREE",
"Three.js Examples",
GetHtmlPathThree()
);
if (attachHandlers && _curDoc != null) {
Application.DocumentManager.DocumentActivated +=
OnDocumentActivated;
_curDoc.BeginDocumentClose +=
(s, e) =>
{
RemoveHandlers(_curDoc);
_curDoc = null;
};
_3ps.SizeChanged += OnPaletteSizeChanged;
// When the PaletteSet gets destroyed we remove
// our event handlers
_3ps.PaletteSetDestroy += OnPaletteSetDestroy;
}
}
[CommandMethod("THREEDOC")]
public void ThreeDocument()
{
_curDoc = Application.DocumentManager.MdiActiveDocument;
if (_curDoc != null)
{
_curDoc.BeginDocumentClose +=
(s, e) => _curDoc = null;
}
Application.DocumentWindowCollection.AddDocumentWindow(
"Three.js Document", GetHtmlPathThree()
);
}
[JavaScriptCallback("ViewExtents")]
public string ViewExtents(string jsonArgs)
{
// Default return value is failure
var res = "{\"retCode\":1}";
if (_curDoc != null)
{
var vw = _curDoc.Editor.GetCurrentView();
var ext = Utils.ScreenExtents(vw);
res =
String.Format(
"{{\"retCode\":0, \"result\":" +
"{{\"min\":{0},\"max\":{1}}}}}",
JsonConvert.SerializeObject(ext.MinPoint),
JsonConvert.SerializeObject(ext.MaxPoint)
);
}
return res;
}
[JavaScriptCallback("ThreeSolids")]
public string ThreeSolids(string jsonArgs)
{
return Utils.GetSolids(_curDoc, Point3d.Origin);
}
private void OnPaletteSizeChanged(
object s, PaletteSetSizeEventArgs e
)
{
Refresh();
}
private void OnDocumentActivated(
object s, DocumentCollectionEventArgs e
)
{
if (_3ps != 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);
_add.Clear();
_remove.Clear();
// ... long live the document!
_curDoc = e.Document;
AddHandlers(_curDoc);
Refresh();
}
}
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)
{
if (e != null && e.DBObject is Solid3d)
{
_add.Add(e.DBObject.ObjectId);
}
}
private void OnObjectErased(object s, ObjectErasedEventArgs e)
{
if (e != null && e.DBObject is Solid3d)
{
var id = e.DBObject.ObjectId;
if (e.Erased)
{
if (!_remove.Contains(id))
{
_remove.Add(id);
}
}
else
{
if (!_add.Contains(id))
{
_add.Add(e.DBObject.ObjectId);
}
}
}
}
private void OnCommandEnded(object s, CommandEventArgs e)
{
// Invoke our JavaScript functions to update the palette
if (_add.Count > 0)
{
if (_3ps != null)
{
var sols =
Utils.SolidInfoForCollection(
(Document)s, Point3d.Origin, _add
);
acjsInvokeAsync("addsols", Utils.SolidsString(sols));
}
_add.Clear();
}
if (_remove.Count > 0)
{
if (_3ps != null)
{
acjsInvokeAsync("remsols", Utils.GetHandleString(_remove));
_remove.Clear();
}
}
}
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 void Refresh()
{
if (_3ps != null && _3ps.Count > 0)
{
acjsInvokeAsync("refsols", "{}");
}
}
private static Uri GetHtmlPathThree()
{
return new Uri(Utils.GetHtmlPath() + "threesolids2.html");
}
}
}
This file depends on a shared Utils.cs file:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Text;
namespace JavaScriptSamples
{
internal class Utils
{
// Helper to get the document a palette was launched from
// in the case where the active document is null
internal static Document GetActiveDocument(
DocumentCollection dm, Document launchDoc = null
)
{
// 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;
}
internal static string GetSolids(
Document launchDoc, Point3d camPos, bool sort = false
)
{
var doc =
Utils.GetActiveDocument(
Application.DocumentManager,
launchDoc
);
// 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;
var ids = new ObjectIdCollection();
using (
var tr = doc.TransactionManager.StartOpenCloseTransaction()
)
{
// 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
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)
{
ids.Add(id);
}
tr.Commit();
}
var sols = SolidInfoForCollection(doc, camPos, ids, sort);
return SolidsString(sols);
}
}
internal static List<Tuple<double,string, Extents3d>>
SolidInfoForCollection(
Document doc, Point3d camPos, ObjectIdCollection ids,
bool sort = false
)
{
// We'll sort our list of extents objects based on a
// distance value
var sols =
new List<Tuple<double, string, Extents3d>>();
using (
var tr = doc.TransactionManager.StartOpenCloseTransaction()
)
{
foreach (ObjectId id in ids)
{
var obj = tr.GetObject(id, OpenMode.ForRead);
var sol = obj as Entity;//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(
new Tuple<double, string, Extents3d>(
dist, sol.Handle.ToString(), ext
)
);
}
}
}
if (sort)
{
sols.Sort((sol1,sol2)=>sol2.Item1.CompareTo(sol1.Item1));
}
return sols;
}
// Helper function to build a JSON string containing a
// sorted extents list
internal static string SolidsString(
List<Tuple<double, string, Extents3d>> lst)
{
var sb = new StringBuilder("{\"retCode\":0, \"result\":[");
var first = true;
foreach (var tup in lst)
{
if (!first)
sb.Append(",");
first = false;
var hand = tup.Item2;
var ext = tup.Item3;
sb.AppendFormat(
"{{\"min\":{0},\"max\":{1},\"handle\":\"{2}\"}}",
JsonConvert.SerializeObject(ext.MinPoint),
JsonConvert.SerializeObject(ext.MaxPoint),
hand
);
}
sb.Append("]}");
return sb.ToString();
}
// Helper function to build a JSON string containing a
// list of handles
internal static string GetHandleString(ObjectIdCollection _ids)
{
var sb = new StringBuilder("{\"handles\":[");
bool first = true;
foreach (ObjectId id in _ids)
{
if (!first)
{
sb.Append(",");
}
first = false;
sb.AppendFormat(
"{{\"handle\":\"{0}\"}}",
id.Handle.ToString()
);
}
sb.Append("]}");
return sb.ToString();
}
// Helper function to show a palette
internal static PaletteSet ShowPalette(
PaletteSet ps, Guid guid, string cmd, string title, Uri uri,
bool reload = false
)
{
// If the reload flag is true we'll force an unload/reload
// (this isn't strictly needed - given our refresh function -
// but I've left it in for possible future use)
if (reload && ps != null)
{
// Close the palette and make sure we process windows
// messages, otherwise sizing is a problem
ps.Close();
System.Windows.Forms.Application.DoEvents();
ps.Dispose();
ps = null;
}
if (ps == null)
{
ps = new PaletteSet(cmd, guid);
}
else
{
if (ps.Visible)
return ps;
}
if (ps.Count != 0)
{
ps.Remove(0);
}
ps.Add(title, uri);
ps.Visible = true;
return ps;
}
internal static Matrix3d Dcs2Wcs(AbstractViewTableRecord v)
{
return
Matrix3d.Rotation(-v.ViewTwist, v.ViewDirection, v.Target) *
Matrix3d.Displacement(v.Target - Point3d.Origin) *
Matrix3d.PlaneToWorld(v.ViewDirection);
}
internal static Extents3d ScreenExtents(
AbstractViewTableRecord vtr
)
{
// Get the centre of the screen in WCS and use it
// with the diagonal vector to add the corners to the
// extents object
var ext = new Extents3d();
var vec = new Vector3d(0.5 * vtr.Width, 0.5 * vtr.Height, 0);
var ctr =
new Point3d(vtr.CenterPoint.X, vtr.CenterPoint.Y, 0);
var dcs = Utils.Dcs2Wcs(vtr);
ext.AddPoint((ctr + vec).TransformBy(dcs));
ext.AddPoint((ctr - vec).TransformBy(dcs));
return ext;
}
// Helper function to get the path to our HTML files
internal 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/";
}
}
}
I’ve been banging away at the app to get it to fail: the latest version seems fairly solid, but do let me know if you come across any issues with it.
If I’m right, the kind of responsiveness this sample shows should enable all kinds of interesting HTML palette-based applications inside AutoCAD.