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.

Three.js integration with AutoCAD 

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 "/wp-content/uploads/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.

Leave a Reply

Your email address will not be published. Required fields are marked *