October 2014

Sun Mon Tue Wed Thu Fri Sat
      1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31  










October 03, 2014

Connecting Three.js to an AutoCAD model – Part 2

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 "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.

October 02, 2014

Connecting Three.js to an AutoCAD model – Part 1

As part of my preparations for AU, I’ve been extending this Three.js integration sample to make it more responsive to model changes: I went ahead and implemented event handlers in .NET – much as we saw in the last post – to send interaction information through to JavaScript so that it can update the HTML palette view.

The code is in pretty good shape, but I still need to decide whether to post it separately or with the other JavaScript samples I’m working on (I’ll also be showing Paper.js and Isomer integrations during my AU talk, as well as a special demo bringing ShapeShifter models into AutoCAD).

In the meantime, here’s a screencast of the Three.js updated integration in action.




My apologies for the sound quality: I’ve managed to lose my external mic and my new MacBook’s internal one pics up a lot of noise from the fan, once it gets going.

Also, if the command-list provided by Screencast is getting in the way, if you go to full-screen mode it should be easier to see what’s going on.

September 30, 2014

Displaying the area of the last AutoCAD entity in an HTML palette using JavaScript and .NET

The title of this one is a little specific – the post actually deals with the scenario of passing data from .NET to an HTML-defined palette, as well as some other tips & tricks – but it’s something I wanted to show.

Here’s the basic idea: whenever a closed curve gets added to the drawing, we want to display its area as the only item in an HTML palette. We also want the palette to update when objects get erased, etc., which makes life somewhat trickier.

To set the scene, here’s a quick screencast of the finished application in action (I didn’t record audio – it should be obvious what’s happening, though):




The “brief” came from Matthew Shaxted at SOM Chicago: they want to use an HTML palette to display more advanced area calculations – not just the area of the last entity added – but this is a good starting point for them. On a side note, these guys are doing some really cool stuff with WebGL inside AutoCAD, something similar to (but way beyond) the approach shown in this previous post. It’s amazing what can be done with this mechanism.

Right, back to the topic at hand. In most of the previous AutoCAD+JavaScript-related posts, we call through to .NET commands from HTML/JavaScript. We want to do this here, too, but we also want data to travel in the reverse direction, and not just passed back by .NET functions: we need to push data to the palette, updating it when (for instance) new entities are created.

During the research for this post, I iterated through a few different approaches which I feel are worth sharing for context:

  • Reload the palette, forcing the page load to call back through from JavaScript to .NET to get the required data
    • Very heavy-handed: I literally had to close and re-launch the palette, which caused some ugly UI flicker
  • Define a hidden JavaScript command (with “no history” and “no undo marker” defined) that we call from our event handler using SendStringToExecute()
    • It worked, but added complexity as we’re having to marshal data via Editor.GetString() (etc.) methods, which also meant setting NOMUTT
  • Register a JavaScript method using registerCallback() and call it using Application.Invoke() from .NET
    • This is the approach recommended by the online help, which it turns out is out-of-date… there is no connection between JavaScript and acedInvoke() and its equivalents
  • Use the same approach but invoke it by calling acjsInvokeAsync() via P/Invoke
    • We need to get a native .NET method exposed for this, but at least this approach works (hurray!)
    • Thanks to Albert Szilvasy for providing pointers that got me this far :-)

Aside from this there were some other structural issues to deal with: we want to detect when objects get added to and erased from the chosen drawing – ObjectAppended and ObjectErased events are great for this, of course – but we clearly need to wait until CommandEnded to do anything of significance.

Beyond that, though, we also need to be careful not to trample on the undo file. As explained in this very helpful DevBlog post, if you open anything for write during CommandEnded of an undo-related command, this will cause the famous “nothing to redo” problem when someone tries to redo the undone actions. While we don’t open anything directly for write, we do use Editor.SelectLast() to get the most recently created entity in the drawing. Even if we call this via SendStringToExecute() it causes the problem.

So the code implements a rudimentary object list – which tracks curves that are added and erased while it’s in operation – so that we can get the most recently added entity from there. I have no doubt there are complexities we’re not addressing with this fairly crude approach, but then we’re only using it during undo, which mitigates the risk somewhat. For instance, we don’t populate the history when we load the app into a drawing that contains geometry, so if we undo back past the time the app was loaded, we don’t display any area information.

I think that’s enough preamble. Here’s the HTML page defining our palette – I’ve embedded the various JavaScript code, including the extension to the shaping layer – for simplicity:

<!doctype html>

<html>

  <head>

    <title>Last Area</title>

    <style>

      html, body {

        height: 100%;

        width: 100%;

        margin: 0;

        padding: 0;

      }

 

      body {

        display: table;

      }

 

      .centered-on-page {

        text-align: center;

        display: table-cell;

        vertical-align: middle;

        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

        font-size: xx-large;

        font-weight: bold;

      }

    </style>

    <script

      src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">

    </script>

    <script type="text/javascript">

      function displayValue(prop, val) {

 

        // Display the specified value in our div for the specified

        // property

 

        var div = document.getElementById(prop);

        if (div != null) {

          div.innerHTML = val;

        }

      }

 

      function updatePalette(args) {

 

        // Simply unpack the arguments from JSON and pass

        // them through to the generic display function

 

        var obj = JSON.parse(args);

        displayValue(obj.propName, obj.propValue);

      }

 

      // Shaping layer extension

 

      function lastAreaFromAutoCAD() {

        var jsonResponse =

          exec(

            JSON.stringify({

              functionName: 'LastArea',

              invokeAsCommand: false,

              functionParams: undefined

            })

          );

        var jsonObj = JSON.parse(jsonResponse);

        if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {

          throw Error(jsonObj.retErrorString);

        }

        return jsonObj.result;

      }

    </script>

  </head>

  <body>

    <div id="area" class="centered-on-page"/>

    <script type="text/javascript">

      (function () {

 

        registerCallback("updval", updatePalette);

 

        // On load we call through to .NET to get the area of

        // the last entity and then display it

        // (we could also have the .NET could reinvoke

        // JavaScript if we wanted to keep the display-specific

        // logic in one module)

 

        try {

          var area = lastAreaFromAutoCAD();

          displayValue(

            "area", area > 0.0 ? area.toFixed(2) : "No area"

          );

        }

        catch (ex) {

          displayValue("area", "No area");

        }

      })();

    </script>

  </body>

</html>

Here’s the loading C# code which gets called from the palette but also calls through to it:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using System;

using System.IO;

using System.Reflection;

using System.Runtime.InteropServices;

 

namespace LastAreaPalette

{

  public class Commands

  {

    // Member variables

 

    private PaletteSet _areaps = null;          // Our palette

    private static Document _launchDoc = null// Doc launched from

    private static bool _update = false;        // Flag for refresh

    private static ObjectIdCollection _ids =    // List to help

      new ObjectIdCollection();                 // refresh on UNDO

 

    [DllImport(

      "AcJsCoreStub.crx", CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "acjsInvokeAsync")]

    extern static private int acjsInvokeAsync(

      string name, string jsonArgs

    );

 

    // JavaScript-exposed method allowing our HTML page to query

    // the area of the last entity as it loads

 

    [JavaScriptCallback("LastArea")]

    public string LastArea(string jsonArgs)

    {

      // Default return value is failure

 

      var res = "{\"retCode\":1}";

 

      var doc = GetActiveDocument(Application.DocumentManager);

 

      // If we didn't find a document, return

 

      if (doc == null)

        return res;

 

      // We could probably get away without locking the document

      // - as we only need to read - but it's good practice to

      // do it anyway

 

      try {

        using (var dl = doc.LockDocument())

        {

          // Call our internal method and package the results

          // as JSON if successful

 

          var area = GetAreaOfLastEntity(doc, ObjectId.Null);

          if (area > 0.0)

          {

            res =

              String.Format(

                "{{\"retCode\":0, \"result\":{0}}}", area

              );

          }

        }

      }

      catch { }

 

      return res;

    }

 

    // Helper function to retrieve the area of the last entity

    // (assuming it's a closed curve). We have an "optional"

    // ObjectId argument (we couldn't define ObjectId.Null as the

    // default value, as that's not a compile-time constant)

    // which we use when undoing (as SelectLast() invalidates

    // the undo file if used then)

 

    private double GetAreaOfLastEntity(

      Document doc, ObjectId id

    )

    {

      var db = doc.Database;

      var ed = doc.Editor;

 

      var res = 0.0;

 

      if (id == ObjectId.Null)

      {

        // Get the last entity (which returns a selection set)

 

        var psr = ed.SelectLast();

 

        if (psr.Status != PromptStatus.OK || psr.Value.Count != 1)

          return res;

 

        id = psr.Value[0].ObjectId;

 

        // If our list of IDs doesn't yet contain it, append it

 

        if (!_ids.Contains(id))

        {

          _ids.Add(id);

        }

      }

 

      // Use open/close as we're often called in an event handler

 

      var tr = doc.TransactionManager.StartOpenCloseTransaction();

      using (tr)

      {

        var c = tr.GetObject(id, OpenMode.ForRead) as Curve;

        if (c.Closed)

        {

          res = c.Area;

        }

        tr.Commit();

      }

      return res;

    }

 

    // Helper to get the document a palette was launched from

    // in the case where the active document is null

 

    private Document GetActiveDocument(DocumentCollection dm)

    {

      var doc = dm.MdiActiveDocument;

      if (doc == null)

      {

        doc = _launchDoc;

      }

      return doc;

    }

 

    [CommandMethod("LAREA")]

    public void LastAreaPalette()

    {

      // We're storing the "launch document" as we're attaching

      // various event handlers to it

 

      _launchDoc =

        Application.DocumentManager.MdiActiveDocument;

 

      _areaps =

        ShowPalette(

          _areaps,

          new Guid("4169EEA9-E3BA-49C4-9197-265A2E42E4B5"),

          "LAREA",

          "Last Area",

          GetHtmlPathArea(),

          true

        );

 

      if (_launchDoc != null)

      {

        // When the document we're connected to is closed,

        // we want to close the palette

 

        _launchDoc.BeginDocumentClose +=

          (s, e) =>

          {

            if (_areaps != null)

            {

              _areaps.Close();

              _areaps.Dispose();

              _areaps = null;

            }

            _launchDoc = null;

          };

 

        // 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)

 

        _launchDoc.Database.ObjectAppended += OnObjectAppended;

        _launchDoc.Database.ObjectErased += OnObjectErased;

        _launchDoc.CommandEnded += OnCommandEnded;

 

        // When the PaletteSet gets destroyed we remove

        // our event handlers

 

        _areaps.PaletteSetDestroy += OnPaletteSetDestroy;

      }

    }

 

    void OnObjectAppended(object s, ObjectEventArgs e)

    {

      // If we have a curve, flag the palette for refresh

      // and add the curve's ID to our list

 

      if (e != null && e.DBObject is Curve)

      {

        _update = true;

        _ids.Add(e.DBObject.ObjectId);

      }

    }

 

    void OnObjectErased(object s, ObjectErasedEventArgs e)

    {

      // If we have a curve, flag the palette for refresh

      // and then either add or remove the curve's ID to/from our

      // list, depending on whether we're erasing or unerasing

 

      if (e != null && e.DBObject is Curve)

      {

        _update = true;

 

        var id = e.DBObject.ObjectId;

        if (e.Erased)

        {

          if (_ids.Contains(id))

          {

            _ids.RemoveAt(_ids.IndexOf(id));

          }

        }

        else if (!e.Erased)

        {

          _ids.Add(id);

        }

      }

    }

 

    void OnCommandEnded(object s, CommandEventArgs e)

    {

      if (_update)

      {

        // When we need to update the display in the palette,

        // get the last area and pass it through to our hidden

        // UPDPAL command (specifying the "area" div)

 

        _update = false;

 

        // We don't want our refresh function to use

        // Editor.SelectLast() if we're undoing,

        // so check for that

 

        var isUndoing =

          (e.GlobalCommandName == "U" ||

           e.GlobalCommandName == "UNDO");

 

        var doc = (Document)s;

 

        var id = ObjectId.Null;

        var area = 0.0;

 

        if (!isUndoing || _ids.Count > 0)

        {

          // If we're undoing, pass the ID of the object at the

          // top of our "stack" (which should be valid as

          // OnObjectErased() will have popped any being erased)

 

          if (isUndoing)

          {

            id = _ids[_ids.Count - 1];

          }

          area = GetAreaOfLastEntity(doc, id);

        }

 

        // Invoke our JavaScript function to update the palette

 

        acjsInvokeAsync(

          "updval",

          "{\"propName\":\"area\",\"propValue\":" +

          (area > 0.0 ?

            Math.Round(area, 2).ToString() :

            "\"No area\"") + "}"

        );

      }

    }

 

    void OnPaletteSetDestroy(object s, EventArgs e)

    {

      // When our palette is closed, detach the various

      // event handlers

 

      if (_launchDoc != null)

      {

        _launchDoc.Database.ObjectAppended -= OnObjectAppended;

        _launchDoc.Database.ObjectErased -= OnObjectErased;

        _launchDoc.CommandEnded -= OnCommandEnded;

      }

    }

 

    // Helper function to show a palette

 

    private 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[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 GetHtmlPathArea()

    {

      return new Uri(GetHtmlPath() + "lastarea.html");

    }

  }

}

Something else to note about this, before we finish: rather than create an updarea() function in JavaScript that only updates the area in the palette, I did my best to make this a bit more generic. The updval() method has both the property name and value passed as arguments to it, which means we can use it to update other “divs” in the HTML, should we need to. You can imagine a much more complex palette populated with various fields coming from AutoCAD, for instance, all updated using calls to updval().

September 25, 2014

TEDxCERN: Mind = Blown

I was expecting yesterday’s TEDx event at CERN to be good, but it was way beyond that.

The overall theme of the event was “Forward: Charting the Future with Science.” It comprised 17 separate sessions grouped into 3 sections – Adapt, Change & Create – with Brian Cox as host.

Brian Cox

The event was planned and executed very well: even when the occasional minor glitch occurred it only ended up adding fun to the proceedings.

The agenda was structured nicely, with a good progression of themes between sessions. I really liked the inclusion of 3 TED-Ed sessions on climate change, matter & anti-matter and cosmic rays, which apparently met the usual requirement for TEDx events also to show material from TED.com.

To give you a sense of the quality of the event, here’s a single sentence on each of the 17 live sessions.

Adapt

Robert CreaseRobert Crease talked about the challenges around using science to influence public policy and how best to succeed.

Tamsin Edwards

Tamsin Edwards talked about embracing and effectively communicating uncertainty around climate science.

Marcia Barbosa

Marcia Barbosa described the complexity of that most essential of resources – water – and how nano-tubes and materials inspired by African beetles can help make clean water more widely available.Nina Federoff

Nina Federoff talked about the role of GMOs in feeding a future global population of 10 billion.

Nitin Sawhney and Nicki Wells

One of my favourite musician’s, Nitin Sawhney, talked about the connection between music and mathematics (he even used vedic mathematics to solve the cube root of 132,651 live on stage… awesome!) and performed with the very talented Nicki Wells.

Change

Julia Greer

Julia Greer talked about her team’s research into lightweight 3D nanostructures, and how materials at the nanoscale can have surprising properties.

Sonia Trigueros

Sonia Trigueros talked about how nanomedecine holds the promise to fight cancer in a much more targeted way than existing therapies.

Srikumar Bannerjee

Srikumar Bannerjee described how modern reactors and spent fuel reprocessing make nuclear energy the best choice for clean, abundant energy.

John Mighton

John Mighton talked about the difficulties he had learning mathematics as a child and how that inspired him to launch JUMP Math.

Andrew Nemr

Andrew Nemr closed the second session with a tap dance accompanied by an explanation of why he dislikes labels.

Create

Hayat Sindi started the second session by describing the importance of marrying science with social innovation.

Arthur Zang's CardioPad

Arthur Zang, a 26-year-old entrepreneur, presented a device he developed to help Cameroun’s cardiologists diagnose patients remotely.

Topher White

Topher White talked about an innovative approach to alerting rangers in the rainforest to illegal logging activity – repurposing old mobile phones, powered by recycled solar panels, to detect humanly inaudible chainsaws.

Danielle Fong

Danielle Fong presented a technology her company has developed using compressed air to store energy harvested from intermittent, renewable sources.

Julien Lesgourges discussed how spectral analysis can be applied to pretty much anything – sound, images and even the universe – resulting in cosmologists having a better understanding of its composition.

Jamie Edwards

Jamie Edwards, a 14-year-old schoolboy from Lancashire, talked entertainingly about how he convinced his school to allow – and even fund – his creation of a small nuclear reactor in its science lab.

Tim Exile

Tim Exile remixed sounds from CERN and TEDxCERN itself as part of his live performance closing this fantastic event.

Oh yes, here’s a quick aerial shot showing where I was glued to my seat for the ~5 hours of presentations…

View from the roof

At the following social event I had a nice time chatting with Matteo Mazzeri, one of the organisers of TEDxGeneva. I'm looking forward to attending that event in April. And it seems I just missed TEDxBern, unfortunately... maybe it's time to think about helping organise a TEDxNeuchatel? :-).

All images © 2014 CERN

September 24, 2014

Off to TEDxCERN

TEDxCERN 2014I last went to CERN back in 1996 (although it may have been late 1995) for an ADGE conference. I think that ADGE stood for “Autodesk Developer Group Europe”, but the A might also have been for AUGI. Any old-timers out there who can confirm? (I’m sure Jeremy Tammik remembers. :-)

Anyway, the ADGE conference that year was held at CERN, which was really cool. As part of the entertainment we had a tour around the facilities, including the accelerator complex – although some time before construction of the Large Hadron Collider was started.

So here I find myself about to head back there this afternoon for TEDxCERN. This will be my second TEDx event this year, having attended TEDxLausanne back in February. Understandably, given the location, today’s programme looks to be a bit more “sciencey”, which is great. And there are at least a couple of names I already know from the list of speakers, which is another good sign.

September 23, 2014

123D Catch available for Android

I was very happy to read the news on Shaan’s blog: 123D Catch is now available on Android. I blogged about its availability via the web and on iOS, a couple of years ago, but there’s now an Android version and it’s pretty slick.123D Catch on Google Play

Some quick background info: I do my best to stay OS agnostic, as a rule. My primary desktop OS is Windows (7 or 8.1, depending) but I also use OS X Mavericks and play with a few different Linux distributions when I get time. I have an old iPad 2 that has a cracked – but still functional – screen and use a Nexus 4 phone currently running Android 4.4.4 (as you can see, I tend not to follow the expected 2-year mobile refresh cycle, if I can avoid it… there are too many other fun gadgets to spend money on :-).

While living like this (embracing tech pluralism) causes some degree of pain – things don’t “just work” a lot of the time – it feels good to surf the waves caused by the ebb and flow of the tech industry. And, anyway, I hate the view from walled gardens – I want to see the horizon! ;-)

So yes, the main device I walk around with, these days, runs Android and has been waiting patiently for 123D Catch to show up on Google Play.

I started by creating a project and using the app to take snaps of a stone tea-light holder I received for my 30th birthday (I think it was, anyway).Photographing the stone

Then I went ahead and uploaded the project for processing:

Transmitting and processing photos

Once completed, I followed the prompts to review the 3D model and then frame it for publication.

Reviewing, framing and publishing

The results were decent, although I did miss a bit of photo coverage on the back of the candle, leading to a couple of small holes. Nothing that couldn’t be fixed using meshmixer, Project Memento or the 123D cleanup tool, though.


If you have an Android device, take the new app for a spin and see what you think!

September 19, 2014

Exploding nested AutoCAD blocks using .NET

Some time ago I posted about how to use Entity.Explode() to do something similar to AutoCAD’s EXPLODE command. At the time it was mentioned in the comments that BlockReference.ExplodeToOwnerSpace() had some relative benefits, but it’s taken me some time to code up a simple sample to show how you might use it (Patrick’s recent comment reminded me I ought to, though).

Anyway, to end the week I thought I’d throw together a quick sample. BlockReference.ExplodeToOwnerSpace() doesn’t return a list of created objects, so I opted to capture this using a Database.ObjectAppended event handler and then recursively call our custom ExplodeBlock() function for any nested blocks that get created. We also then erase the originating entity (or entities, if called recursively), just as the EXPLODE command might.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

 

namespace Explosions

{

  public class Commands

  {

    [CommandMethod("EB")]

    public void ExplodeBock()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      // Ask the user to select the block

 

      var peo = new PromptEntityOptions("\nSelect block to explode");

      peo.SetRejectMessage("Must be a block.");

      peo.AddAllowedClass(typeof(BlockReference), false);

 

      var per = ed.GetEntity(peo);

 

      if (per.Status != PromptStatus.OK)

        return;

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        // Call our explode function recursively, starting

        // with the top-level block reference

        // (you can pass false as a 4th parameter if you

        // don't want originating entities erased)

 

        ExplodeBlock(tr, db, per.ObjectId);

 

        tr.Commit();

      }

    }

 

    private void ExplodeBlock(

      Transaction tr, Database db, ObjectId id, bool erase = true

    )

    {

      // Open out block reference - only needs to be readable

      // for the explode operation, as it's non-destructive

 

      var br = (BlockReference)tr.GetObject(id, OpenMode.ForRead);

 

      // We'll collect the BlockReferences created in a collection

 

      var toExplode = new ObjectIdCollection();

 

      // Define our handler to capture the nested block references

 

      ObjectEventHandler handler =

        (s, e) =>

        {

          if (e.DBObject is BlockReference)

          {

            toExplode.Add(e.DBObject.ObjectId);

          }

        };

 

      // Add our handler around the explode call, removing it

      // directly afterwards

 

      db.ObjectAppended += handler;

      br.ExplodeToOwnerSpace();

      db.ObjectAppended -= handler;

 

      // Go through the results and recurse, exploding the

      // contents

 

      foreach (ObjectId bid in toExplode)

      {

        ExplodeBlock(tr, db, bid, erase);

      }

 

      // We might also just let it drop out of scope

 

      toExplode.Clear();

 

      // To replicate the explode command, we're delete the

      // original entity

 

      if (erase)

      {

        br.UpgradeOpen();

        br.Erase();

        br.DowngradeOpen();

      }

    }

  }

}

That’s it for this week. Monday is a holiday in Neuchatel, so I’ll be back online on Tuesday. And then on Wednesday I’m heading to TEDxCERN – which promises to be really cool. Can’t wait!

September 17, 2014

Reminder: Exchange Apps Hackathon this weekend

There’s still time to participate in the Autodesk Exchange Apps Hackathon, a virtual event taking place this weekend (September 20-21). The point of this event is to encourage developers to post apps to the Autodesk Exchange Apps store, and we’re even paying cool, hard cash ($50 or $100, whether free or paid) for each app that gets published.

Hackathon

Presentations and discussions will include:

  • How to architect your app for Exchange:
    • How to build your AutoCAD® app
    • How to build your Autodesk® Revit® app
    • How to build your Autodesk® Inventor® app
    • How to build your Autodesk® 3ds Max® and Autodesk® Maya® apps
  • Handling IPN notifications
  • Implementing simple copy protection for your app
  • Architecting your app to sell on monthly subscription
  • Selling online web services on Exchange Apps
  • Making use of Autodesk ‘cloud’ services such as our new, web-based, zero-client 3D model viewer
  • Sustainability apps

Lots of great information to help kick-start your app development. Sign up now!

September 15, 2014

Exporting Minecraft data from AutoCAD

After last week’s post on importing Minecraft data – in this case from Tinkercad – into AutoCAD, in today’s post we’re going to focus on the ultimately more interesting use case of generating Minecraft data from AutoCAD. We’re going to see some code to dice up 3D AutoCAD geometry and generate blocks in a .schematics file using Substrate.

Our “dicing” process – a term I’ve just coined for iterating through a 3D space, chunk by chunk – is going to use a couple of different approaches for determining there’s any 3D geometry in each grid location. Firstly, though, we going generate a spatial index from the contents of the modelspace – a basic list of bounding boxes with the owning entity’s ObjectId (which could be optimised further by sorting based on location) – to decide whether we want to take a closer look at the geometry we find there.

If we get a “hit” from the spatial index, we can test the associated entity for whether the specific point we’re interested in actual does contain geometry. The specific test will vary based on the type of 3D object we find…

If it’s a Solid3d we can perform a simple test using the CheckInterference() method, passing in a cubic Solid3d occupying the location to test. This works fine, but will generate hits for internal cubes, too (i.e. we end up with a fully solid object, rather than just having blocks representing the shell). Ideally we would union the two solids and check the resultant volume to see if it’s an internal cube or not (if the volume doesn’t change then its internal), but that’s likely to be expensive. The ObjectARX equivalent, AcDb3dSolid::checkInterference() does allow this, but it’s more complicated from .NET. Right now we simply create blocks for internal locations, as well, which may also be what the user wants in many cases.

For Surface objects there’s a bit more to do: here we use ProjectOnToSurface(), passing in a DBPoint, to see whether there’s a point in the block that’s close to the surface. We do this for each of the location cube’s vertices, which may be overkill but seems to give the best results for complex surfaces. Needless to say, we stop checking for “clashes” the first time we get a hit in a particular location – there’s no need to keep looking (although we might want to if we wanted to get the best possible material for a block… for now we’re not worrying about materials at all).

To put the code through its paces, I went ahead and rebuilt a space shuttle model using the code in this previous post (although I performed the loft operations by hand – for some reason these don’t work for me anymore).

I then went and used the EMC command – changing the default block size to 0.1, to make the model more detailed – and then used IMC to reimport it into AutoCAD:

Shuttle export and reimport

You should bear in mind that AutoCAD’s representation of an imported model is a lot heavier that it would be in Minecraft – each block is a block reference or a 3D solid, depending – so complex models that have trouble being loaded back into AutoCAD will often import just fine into Minecraft.

Here’s the space shuttle schematics file imported into a new world in MCEdit. I chose “diamond” as the material (just to add a bit of a bling factor for my kids), but you could easily hardcode another choice of material or select it based on the geometry’s layer (etc.).

Diamond shuttle in MCEdit

The surface analysis algorithm could use some tweaking – you can see some holes on the sides where the surface is close to vertical, and the base is very thick – but it’s good enough for my purposes.

Here’s the C# code defining the EMC and IMC commands:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using AcDb = Autodesk.AutoCAD.DatabaseServices;

using Substrate;

using System;

using System.Collections.Generic;

 

namespace Minecraft

{

  public class GeometryIndex

  {

    // We need a list of extents vs. ObjectIds

    // (some kind of spatial sorting might help with

    // performance, but for now it's just a flat list)

 

    List<Tuple<Extents3d, ObjectId>> _extList;

 

    public GeometryIndex()

    {

      _extList = new List<Tuple<Extents3d, ObjectId>>();

    }

 

    public int Size

    {

      get { return _extList.Count; }

    }

 

    public void PopulateIndex(BlockTableRecord ms, Transaction tr)

    {

      foreach (var id in ms)

      {

        var ent =

          tr.GetObject(id, OpenMode.ForRead) as

            Autodesk.AutoCAD.DatabaseServices.Entity;

        if (ent != null)

        {

          _extList.Add(

            new Tuple<Extents3d, ObjectId>(

              ent.GeometricExtents, id

            )

          );

        }

      }

    }

 

    internal ObjectIdCollection PotentialClashes(

      Point3d pt, double step

    )

    {

      var res = new ObjectIdCollection();

 

      foreach (var item in _extList)

      {

        var ext = item.Item1;

 

        if (

          pt.X + step >= ext.MinPoint.X &&

          pt.X <= ext.MaxPoint.X + step &&

          pt.Y + step >= ext.MinPoint.Y &&

          pt.Y <= ext.MaxPoint.Y + step &&

          pt.Z + step >= ext.MinPoint.Z &&

          pt.Z <= ext.MaxPoint.Z + step

        )

        {

          res.Add(item.Item2);

        }

      }

 

      return res;

    }

  }

 

  public class Commands

  {

    // Members that will be set by the EMC command and

    // picked up by the IMC command

 

    private double _blockSize = 1.0;

    private Point3d _origin = Point3d.Origin;

 

    [CommandMethod("IMC")]

    public void ImportMinecraft()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      // Request the name of the file to import

 

      var opts =

        new PromptOpenFileOptions(

          "Import from Minecraft"

        );

      opts.Filter =

        "Minecraft schematic (*.schematic)|*.schematic|" +

        "All files (*.*)|*.*";

      var pr = ed.GetFileNameForOpen(opts);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      // Read in the selected  Schematic file

 

      var schem =

        Substrate.ImportExport.Schematic.Import(pr.StringResult);

 

      if (schem == null)

      {

        ed.WriteMessage("\nCould not find Minecraft schematic.");

        return;

      }

 

      // Let the user choose the location of the geometry

 

      ed.WriteMessage(

        "\nDefault insert is {0}", _origin

      );

      var ppo = new PromptPointOptions("\nInsertion point or ");

      ppo.Keywords.Add("Default");

      ppo.AllowNone = true;

 

      var ppr = ed.GetPoint(ppo);

 

      Vector3d offset;

 

      if (ppr.Status == PromptStatus.Keyword)

      {

        offset = _origin.GetAsVector();

      }

      else if (ppr.Status == PromptStatus.OK)

      {

        offset = ppr.Value.GetAsVector();

      }

      else

      {

        return;

      }

 

      // Let the user choose the size of the block

 

      var pdo = new PromptDoubleOptions("\nEnter block size");

      pdo.AllowNegative = false;

      pdo.AllowNone = true;

      pdo.DefaultValue = _blockSize;

      pdo.UseDefaultValue = true;

 

      var pdr = ed.GetDouble(pdo);

 

      if (pdr.Status != PromptStatus.OK)

        return;

 

      _blockSize = pdr.Value;

      var step = _blockSize;

 

      // We only really care about the blocks

 

      var blks = schem.Blocks;

 

      // We can either create Solid3d objects for each Minecraft

      // block, or we can create a BlockTableRecord containing

      // a single Solid3d that we reference for each block

      // (if useBlock is set to true)

 

      var blkId = ObjectId.Null;

      var useBlock = true;

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var bt =

          (BlockTable)tr.GetObject(

            db.BlockTableId, OpenMode.ForRead

          );

 

        if (useBlock)

        {

          bt.UpgradeOpen();

 

          // Create our block and add it to the db & transaction

 

          var btr = new BlockTableRecord();

          btr.Name = "Minecraft Block";

 

          blkId = bt.Add(btr);

          tr.AddNewlyCreatedDBObject(btr, true);

 

          // Create our cube and add it to the block & transaction

 

          var cube = new Solid3d();

          cube.CreateBox(step, step, step);

 

          btr.AppendEntity(cube);

          tr.AddNewlyCreatedDBObject(cube, true);

 

          bt.DowngradeOpen();

        }

 

        var ms =

          tr.GetObject(

            bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite

          ) as BlockTableRecord;

        if (ms != null)

        {

          using (var pm = new ProgressMeter())

          {

            pm.Start("Importing Minecraft schematic");

            pm.SetLimit(blks.XDim * blks.YDim * blks.ZDim);

 

            // Create a cubic solid for each block

 

            for (int x = 0; x < blks.XDim; ++x)

            {

              for (int y = 0; y < blks.YDim; ++y)

              {

                for (int z = 0; z < blks.ZDim; ++z)

                {

                  var blk = blks.GetBlock(x, y, z);

                  if (blk != null && blk.Info.Name != "Air")

                  {

                    // Minecraft has a right-handed coordinate

                    // system with Z & Y swapped and Z negated

 

                    var disp =

                      new Point3d(x * step, -z * step, y * step) +

                      offset;

 

                    AcDb.Entity ent;

 

                    if (useBlock)

                    {

                      ent = new BlockReference(disp, blkId);

                    }

                    else

                    {

                      var sol = new Solid3d();

                      sol.CreateBox(step, step, step);

                      sol.TransformBy(

                        Matrix3d.Displacement(disp.GetAsVector())

                      );

                      ent = sol;

                    }

 

                    // Assign the layer based on the material

 

                    ent.LayerId =

                      LayerForMaterial(tr, db, blk.Info.Name);

 

                    ms.AppendEntity(ent);

                    tr.AddNewlyCreatedDBObject(ent, true);

                  }

                  pm.MeterProgress();

                  System.Windows.Forms.Application.DoEvents();

                }

              }

            }

            pm.Stop();

            System.Windows.Forms.Application.DoEvents();

          }

          tr.Commit();

        }

      }

 

      // Zoom to the model's extents

 

      ed.Command("_.ZOOM", "_EXTENTS");

    }

 

    private ObjectId LayerForMaterial(

      Transaction tr, Database db, string layname

    )

    {

      // If a layer with the material's name exists, return its id

 

      var lt =

        (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);

      if (lt.Has(layname))

      {

        return lt[layname];

      }

 

      // Otherwise create a new layer for this material

 

      var ltr = new LayerTableRecord();

      ltr.Name = layname;

 

      lt.UpgradeOpen();

      var ltrId = lt.Add(ltr);

      lt.DowngradeOpen();

 

      tr.AddNewlyCreatedDBObject(ltr, true);

 

      return ltrId;

    }

 

    [CommandMethod("EMC")]

    public void ExportMinecraft()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

      var msId = SymbolUtilityServices.GetBlockModelSpaceId(db);

 

      // Request the name of the file to export to

 

      var opts =

        new PromptSaveFileOptions(

          "Export to Minecraft"

        );

      opts.Filter =

        "Minecraft schematic (*.schematic)|*.schematic|" +

        "All files (*.*)|*.*";

      var pr = ed.GetFileNameForSave(opts);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      var idx = new GeometryIndex();

 

      var emin = db.Extmin;

      var emax = db.Extmax;

 

      // Let the user choose the size of the block - offer a

      // default of a 50th of the diagonal length of the 3D extents

 

      var defSize =

        emax.GetAsVector().Subtract(emin.GetAsVector()).Length / 50;

 

      var pdo = new PromptDoubleOptions("\nEnter block size");

      pdo.AllowNegative = false;

      pdo.AllowNone = true;

      pdo.DefaultValue = defSize;

      pdo.UseDefaultValue = true;

 

      var pdr = ed.GetDouble(pdo);

 

      if (pdr.Status != PromptStatus.OK)

        return;

 

      _blockSize = pdr.Value;

      var step = _blockSize;

 

      _origin = new Point3d(emin.X, emax.Y, emin.Z);

 

      ed.WriteMessage(

        "\nExporting with block size of {0} at {1}.",

        step, _origin

      );

 

      // Set up our empty schematic container

 

      var schem =

        new Substrate.ImportExport.Schematic(

          (int)Math.Ceiling((emax.X - emin.X) / step),

          (int)Math.Ceiling((emax.Z - emin.Z) / step),

          (int)Math.Ceiling((emax.Y - emin.Y) / step)

        );

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        // Get our modelspace

 

        var ms =

          tr.GetObject(msId, OpenMode.ForRead) as BlockTableRecord;

        if (ms != null)

        {

          // Start by populating the spatial index based on the

          // contents of the modelspace

 

          idx.PopulateIndex(ms, tr);

 

          // We'll just use two materials - air and the model

          // material (which for now is "diamond")

 

          var air = new AlphaBlock(BlockType.AIR);

          var diamond = new AlphaBlock(BlockType.DIAMOND_BLOCK);

 

          using (var cube = new Solid3d())

          {

            // We'll use a single cube to test interference

 

            cube.CreateBox(step, step, step);

 

            var std2 = step / 2.0;

            var vecs =

              new Vector3d[]

              {

                new Vector3d(std2,std2,std2),

                new Vector3d(std2,std2,-std2),

                new Vector3d(std2,-std2,std2),

                new Vector3d(std2,-std2,-std2),

                new Vector3d(-std2,std2,std2),

                new Vector3d(-std2,std2,-std2),

                new Vector3d(-std2,-std2,std2),

                new Vector3d(-std2,-std2,-std2)

              };

            var blks = schem.Blocks;

 

            using (var pm = new ProgressMeter())

            {

              pm.Start("Exporting Minecraft schematic");

              pm.SetLimit(blks.XDim * blks.YDim * blks.ZDim);

 

              for (int x = 0; x < blks.XDim; ++x)

              {

                for (int y = 0; y < blks.YDim; ++y)

                {

                  for (int z = 0; z < blks.ZDim; ++z)

                  {

                    // Get the WCS point to test modespace contents

 

                    var wcsX = emin.X + step * x;

                    var wcsY = emax.Y + step * -z;

                    var wcsZ = emin.Z + step * y;

 

                    var pt = new Point3d(wcsX, wcsY, wcsZ);

 

                    // Check our point against bounding boxes

                    // to detect potential clashes

 

                    var ents = idx.PotentialClashes(pt, step);

 

                    // If we have some, verify using a more precise,

                    // per-entity interference check

 

                    if (ents.Count > 0)

                    {

                      var disp = pt.GetAsVector();

 

                      // Displace our interference cube to the

                      // location we want to test

 

                      cube.TransformBy(Matrix3d.Displacement(disp));

 

                      bool found = false;

 

                      // Check each of the potentially clashing

                      // entities against our test cube

 

                      foreach (ObjectId id in ents)

                      {

                        // For Solid3ds we simply check interference

                        // with our cube

 

                        var obj = tr.GetObject(id, OpenMode.ForRead);

                        var sol = obj as Solid3d;

                        if (sol != null)

                        {

                          if (sol.CheckInterference(cube))

                          {

                            // When we've found one we don't need to

                            // test the others

 

                            found = true;

                            break;

                          }

                        }

                        else

                        {

                          // For Surfaces we don't use the cube:

                          // we create a point at the location

                          // and project it onto the surface. If

                          // the resulting point is less than a

                          // step away, we assume we create the

                          // block at this location

 

                          var sur = obj as AcDb.Surface;

                          if (sur != null)

                          {

                            foreach (var v in vecs)

                            {

                              found =

                                SurfaceClash(sur, pt + v, step);

                              if (found)

                                break;

                            }

                          }

                        }

                      }

 

                      // Whether we've found a clash will drive

                      // whether we set the block to be stone or air

 

                      blks.SetBlock(x, y, z, found ? diamond : air);

 

                      // Displace the cube back again, ready for the

                      // next test

 

                      cube.TransformBy(Matrix3d.Displacement(-disp));

                    }

                    pm.MeterProgress();

                    System.Windows.Forms.Application.DoEvents();

                  }

                }

              }

              pm.Stop();

              System.Windows.Forms.Application.DoEvents();

            }

          }

 

          // Finally we write the block information to a schematics

          // file

 

          schem.Export(pr.StringResult);

        }

        tr.Commit();

      }

    }

 

    private static bool SurfaceClash(

      AcDb.Surface sur, Point3d pt, double step

    )

    {

      try

      {

        var found = false;

 

        using (var dbp = new DBPoint(pt))

        {

          var ps =

            sur.ProjectOnToSurface(

              dbp, Vector3d.ZAxis

            );

 

          if (ps.Length > 0)

          {

            foreach (var p in ps)

            {

              var dbp2 = p as DBPoint;

              if (!found && dbp2 != null)

              {

                // If a discovered point is within 2 block's width

                // we consider it a hit

 

                var dist = dbp2.Position - dbp.Position;

                found = (dist.Length < 2.0 * step);

              }

              p.Dispose();

            }

          }

 

          if (found)

          {

            return true;

          }

        }

      }

      catch { }

 

      return false;

    }

  }

}

That’s all I currently have planned in terms of Minecraft-related posts, but if anyone has additional suggestions on where to take this, please do share them.

Unrelatedly, Minecraft is in the news a lot at the moment with the prospective acquisition of Mojang by Microsoft. It seems a lot of fans are concerned by this prospect, but one way or another it’ll be interesting to see how it all plays out…

September 11, 2014

Importing Minecraft data into AutoCAD

A mere 2 among 100 million registered users, my boys are crazy about Minecraft. I’ve been looking into how I might be able to help them use Autodesk tools (well, AutoCAD) to generate Minecraft content. In this post we’ll take a look at importing Minecraft data into AutoCAD, but ultimately the creation/export story is clearly more interesting (something we’ll look at in the next post, I expect).

To investigate dealing with Minecraft data – bearing in mind I didn’t actually know anything much about its file formats – I took a look at the Minecraft export you can perform from Tinkercad, which has been part of that product for just over a year. I took one of my algorithmically-created Tinkercad designs and clicked on “Download for Minecraft”:Algorithmic objects in Tinkercad

This created a local “more_knots.schematic” file, which presumably has information that Minecraft can make sense of. To check this out, I went and installed MCEdit and imported the schematic file into a new world. It was quite fun to see the Tinkercad geometry appear in a Minecraft-like environment:

Tinkercad geometry in MCEdit

Next step, then, was to work out how to get access to the “.schematics” format from .NET. A quick web-search led me to Substrate. I cloned it from GitHub and built it into an AutoCAD plug-in that uses the ImportExport capability to bring in a Schematic file.

It was then a reasonably simple matter to access the blocks and create cubic solids at the right locations to represent them. The only tricky piece, here, is that Minecraft uses a right-handed coordinate system with Z and Y swapped – from our perspective, anyway – and then the Y-axis negated… so it’s X, –Z, Y, I suppose. Because the Y axis is negated – and the geometry will be relative to an origin that isn’t specified in the .schematics file – the position of the model may well need to be moved if you want to check its overlap with source geometry. That’s why the user can select the position and the block size in the import command (which we will set in memory from our export command, making it really easy for the user to export and then reimport to check the quality).

Rather than just creating hundreds or thousands of cubic Solid3d objects, I’ve coded the (default) option to create a single Solid3d in a BlockTableRecord and then create a BlockReference for each Minecraft block. This has advantages both from a file size and memory consumption perspective (AutoCAD’s 3D graphics system is optimised for instanced geometry such as block references).

The code adds blocks to layers based on the names of their materials (I’ve also neglected adding “Air” blocks to the drawing, for obvious reasons). It’s then up to the user to assign appropriate colours to the various layers, as they see fit.

Here’s the Tinkercad data brought into AutoCAD (with my own layer colouring) using the IMC command:

Tinkercad geometry in AutoCAD

Here’s the C# code that implements the IMC command, performing a simple import of Minecraft data:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

 

namespace Minecraft

{

  public class Commands

  {

    // Members that will be set by the EMC command and

    // picked up by the IMC command

 

    private double _blockSize = 1.0;

    private Point3d _origin = Point3d.Origin;

 

    [CommandMethod("IMC")]

    public void ImportMinecraft()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

      var ed = doc.Editor;

      var db = doc.Database;

 

      // Request the name of the file to import

 

      var opts =

        new PromptOpenFileOptions(

          "Import from Minecraft"

        );

      opts.Filter =

        "Minecraft schematic (*.schematic)|*.schematic|" +

        "All files (*.*)|*.*";

      var pr = ed.GetFileNameForOpen(opts);

 

      if (pr.Status != PromptStatus.OK)

        return;

 

      // Read in the selected  Schematic file

 

      var schem =

        Substrate.ImportExport.Schematic.Import(pr.StringResult);

 

      if (schem == null)

      {

        ed.WriteMessage("\nCould not find Minecraft schematic.");

        return;

      }

 

      // Let the user choose the location of the geometry

 

      ed.WriteMessage(

        "\nDefault insert is {0}", _origin

      );

      var ppo = new PromptPointOptions("\nInsertion point or ");

      ppo.Keywords.Add("Default");

      ppo.AllowNone = true;

 

      var ppr = ed.GetPoint(ppo);

 

      Vector3d offset;

 

      if (ppr.Status == PromptStatus.Keyword)

      {

        offset = _origin.GetAsVector();

      }

      else if (ppr.Status == PromptStatus.OK)

      {

        offset = ppr.Value.GetAsVector();

      }

      else

      {

        return;

      }

 

      // Let the user choose the size of the block

 

      var pdo = new PromptDoubleOptions("\nEnter block size");

      pdo.AllowNegative = false;

      pdo.AllowNone = true;

      pdo.DefaultValue = _blockSize;

      pdo.UseDefaultValue = true;

 

      var pdr = ed.GetDouble(pdo);

 

      if (pdr.Status != PromptStatus.OK)

        return;

 

      _blockSize = pdr.Value;

      var step = _blockSize;

 

      // We only really care about the blocks

 

      var blks = schem.Blocks;

 

      // We can either create Solid3d objects for each Minecraft

      // block, or we can create a BlockTableRecord containing

      // a single Solid3d that we reference for each block

      // (if useBlock is set to true)

 

      var blkId = ObjectId.Null;

      var useBlock = true;

 

      using (var tr = db.TransactionManager.StartTransaction())

      {

        var bt =

          (BlockTable)tr.GetObject(

            db.BlockTableId, OpenMode.ForRead

          );

 

        if (useBlock)

        {

          bt.UpgradeOpen();

 

          // Create our block and add it to the db & transaction

 

          var btr = new BlockTableRecord();

          btr.Name = "Minecraft Block";

 

          blkId = bt.Add(btr);

          tr.AddNewlyCreatedDBObject(btr, true);

 

          // Create our cube and add it to the block & transaction

 

          var cube = new Solid3d();

          cube.CreateBox(step, step, step);

 

          btr.AppendEntity(cube);

          tr.AddNewlyCreatedDBObject(cube, true);

 

          bt.DowngradeOpen();

        }

 

        var ms =

          tr.GetObject(

            bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite

          ) as BlockTableRecord;

        if (ms != null)

        {

          using (var pm = new ProgressMeter())

          {

            pm.Start("Importing Minecraft schematic");

            pm.SetLimit(blks.XDim * blks.YDim * blks.ZDim);

 

            // Create a cubic solid for each block

 

            for (int x = 0; x < blks.XDim; ++x)

            {

              for (int y = 0; y < blks.YDim; ++y)

              {

                for (int z = 0; z < blks.ZDim; ++z)

                {

                  var blk = blks.GetBlock(x, y, z);

                  if (blk != null && blk.Info.Name != "Air")

                  {

                    // Minecraft has a right-handed coordinate

                    // system with Z & Y swapped and Z negated

 

                    var disp =

                      new Point3d(x * step, -z * step, y * step) +

                      offset;

 

                    AcDb.Entity ent;

 

                    if (useBlock)

                    {

                      ent = new BlockReference(disp, blkId);

                    }

                    else

                    {

                      var sol = new Solid3d();

                      sol.CreateBox(step, step, step);

                      sol.TransformBy(

                        Matrix3d.Displacement(disp.GetAsVector())

                    );

                      ent = sol;

                    }

 

                    // Assign the layer based on the material

 

                    ent.LayerId =

                      LayerForMaterial(tr, db, blk.Info.Name);

 

                    ms.AppendEntity(ent);

                    tr.AddNewlyCreatedDBObject(ent, true);

                  }

                  pm.MeterProgress();

                  System.Windows.Forms.Application.DoEvents();

                }

              }

            }

            pm.Stop();

            System.Windows.Forms.Application.DoEvents();

          }

          tr.Commit();

        }

      }

 

      // Zoom to the model's extents

 

      ed.Command("_.ZOOM", "_EXTENTS");

    }

 

    private ObjectId LayerForMaterial(

      Transaction tr, Database db, string layname

    )

    {

      // If a layer with the material's name exists, return its id

 

      var lt =

        (LayerTable)tr.GetObject(db.LayerTableId, OpenMode.ForRead);

      if (lt.Has(layname))

      {

        return lt[layname];

      }

 

      // Otherwise create a new layer for this material

 

      var ltr = new LayerTableRecord();

      ltr.Name = layname;

 

      lt.UpgradeOpen();

      var ltrId = lt.Add(ltr);

      lt.DowngradeOpen();

 

      tr.AddNewlyCreatedDBObject(ltr, true);

 

      return ltrId;

    }

  }

}

So far so good! It’s not the best way to bring data from Tinkercad into AutoCAD, but then that’s not the point, of course. This is just about getting access to Minecraft data before we look at the more interesting use case of dicing the current AutoCAD model and generating a .schematic output file.

Feed/Share

10 Random Posts