September 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        








« AutoCAD .NET Training DevTV Session 4: Database Events, PaletteSet | Main | Photo Scene Editor for Project Photofly 2.0 »

May 30, 2011

Sweeping an AutoCAD solid dynamically using Kinect

After focusing on a manual process for sweeping a solid inside AutoCAD in this previous post, it seemed to make sense to attempt to automate more of that process. In the code in this post, we take a standard circular profile – of a user-specified radius – and sweep it along the path defined by the user’s hand movements. Back in the original Kinect integration example we created a 3D polyline path linking every position of the user’s hand detected by Kinect. This makes a very jittery path for our purposes, so we’re going to define a spline along fit points that are at least 100 drawing units apart.

The jig will continually recreate the solid by sweeping the circular profile along the spline path, but clearly as the path grows, the sweep operation will get longer (and the interactivity will drop pretty drastically). This is quite clear from the results in the video:

It should be possible to increase the responsiveness by sweeping the solid in sections (for instance) and then creating the final contiguous solid at the end, but that kind of performance optimisation has been left out of the current implementation.

The implementation watches for exceptions during the sweep operation, swapping the spline path in to replace the solid if it wasn’t possible to create it.

Here’s the C# code:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using AcGi = Autodesk.AutoCAD.GraphicsInterface;

using System.Runtime.InteropServices;

using System.Collections.Generic;

using System.Windows.Media;

using System.Diagnostics;

using System.Reflection;

using System.Linq;

using System.IO;

using System;

using NKinect;

 

namespace KinectIntegration

{

  public class KinectSweepJig : DrawJig

  {

    [DllImport("acad.exe", CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "?acedPostCommand@@YAHPB_W@Z"

     )]

    extern static private int acedPostCommand(string strExpr);

 

    // Draw our transient spline in red

 

    const short transPathColor = 1;

 

    // Our transient solids (cursor sphere & tube) are yellow

 

    const short transSolColor = 2;

 

    // Our final solids will be green

 

    const short finalSolColor = 3;

 

    // A transaction and database to add solids

 

    private Transaction _tr;

    private Document _doc;

 

    // We need our nKinect sensor

 

    private Sensor _kinect = null;

 

    // A list of points captured by the sensor

    // (for eventual export)

 

    private List<ColorVector3> _vecs;

 

    // A list of points to be displayed

    // (we use this for the jig)

 

    private Point3dCollection _points;

 

    // A list of vertices to draw our spline between

 

    private Point3dCollection _vertices;

 

    // The most recent vertex being captured/drawn

 

    private Point3d _curPt;

 

    // Our cursor sphere

 

    private Entity _cursor;

 

    // Entities to create our solid

 

    private Entity _profile;   // A profile circle

    private Spline _path;      // A spline path

    private Solid3d _tube;     // The solid itself

    private bool _sweepBroken; // Can the sweep continue?

 

    // The sweep options for creating the solid

 

    private SweepOptions _sweepOpts;

 

    // The radius of the profile circle to create

 

    private double _profRad;

 

    // An offset value we use to move the mouse back

    // and forth by one screen unit

 

    private int _offset;

 

    // Flags to indicate Kinect gesture modes

 

    private bool _firstCall;   // First skeleton callback

    private bool _calibrating; // Calibrating - show red

    private int _calibTicks;   // Number of frames to show red

    private bool _drawing;     // Drawing mode active

    private bool _finished;    // Finished - want to exit

 

    public bool Finished

    {

      get { return _finished; }

    }

 

    public KinectSweepJig(

      Document doc, Transaction tr, double profRad

    )

    {

      // Initialise the various members

 

      _doc = doc;

      _tr = tr;

      _points = new Point3dCollection();

      _vertices = new Point3dCollection();

      _cursor = null;

      _offset = 1;

      _calibrating = false;

      _calibTicks = 3;

      _drawing = false;

      _finished = false;

      _profile = null;

      _sweepOpts = null;

      _path = null;

      _tube = null;

      _profRad = profRad;

      _firstCall = true;

      _sweepBroken = false;

 

      // Create our sensor object - the constructor takes

      // three callbacks to receive various data:

      // - skeleton movement

      // - rgb data

      // - depth data

 

      _kinect =

        new Sensor(

          s =>

          {

            if (_firstCall)

            {

              _calibrating = true;

              _firstCall = false;

            };

 

            if (!_calibrating && !_finished)

            {

              _drawing = (s.LeftHand.Y < s.LeftHip.Y);

 

              // Get the current position of the hands

 

              Point3d right =

                new Point3d(

                  s.RightHand.X,

                  s.RightHand.Y,

                  s.RightHand.Z

                );

 

              Point3d left =

                new Point3d(

                  s.LeftHand.X,

                  s.LeftHand.Y,

                  s.LeftHand.Z

                );

 

              if (left.DistanceTo(right) < 50.0)

              {

                _drawing = false;

                _finished = true;

              }

 

              if (_drawing)

              {

                // If we have at least one prior vertex...

 

                if (_vertices.Count > 0)

                {

                  Point3d lastVert =

                    _vertices[_vertices.Count - 1];

                  if (lastVert.DistanceTo(right) > 100)

                  {

                    // Add the new vertex to our list

 

                    _vertices.Add(right);

                  }

                }

                else

                {

                  // Add the first vertex to our list

 

                  _vertices.Add(right);

                }

              }

            }

          },

          r =>

          {

          },

          d =>

          {

          }

        );

    }

 

    public void StartSensor()

    {

      if (_kinect != null)

      {

        _kinect.Start();

      }

    }

 

    public void StopSensor()

    {

      if (_kinect != null)

      {

        _kinect.Stop();

        _kinect.Dispose();

        _kinect = null;

      }

    }

 

    public void Cleanup()

    {

      if (_path != null)

      {

        _path.Dispose();

        _path = null;

      };

      if (_profile != null)

      {

        _profile.Dispose();

        _profile = null;

      };

      if (_tube != null)

      {

        _tube.Dispose();

        _tube = null;

      };

 

      _sweepOpts = null;

      _vertices.Clear();

    }

 

    protected override SamplerStatus Sampler(JigPrompts prompts)

    {

      // Se don't really need a point, but we do need some

      // user input event to allow us to loop, processing

      // for the Kinect input

 

      PromptPointResult ppr =

        prompts.AcquirePoint("\nClick to capture: ");

      if (ppr.Status == PromptStatus.OK)

      {

        if (_finished)

        {

          acedPostCommand("CANCELCMD");

          return SamplerStatus.Cancel;

        }

 

        if (!_drawing && (_path != null ||_tube != null))

        {

          AddSolidOrPath();

        }

 

        // Generate a point cloud via nKinect

 

        try

        {

          List<ColorVector3> vecList =

            _kinect.GeneratePointCloud();

 

          // Apply a bounding box filter, if one is defined

 

          if (KinectCommands.Extents.HasValue)

          {

            Extents3d ext = KinectCommands.Extents.Value;

 

            // Use LINQ to get the points within the

            // bounding box

 

            var vecSet =

              from ColorVector3 vec in vecList

              where

                vec.X > ext.MinPoint.X &&

                vec.X < ext.MaxPoint.X &&

                vec.Y > ext.MinPoint.Y &&

                vec.Y < ext.MaxPoint.Y &&

                vec.Z > ext.MinPoint.Z &&

                vec.Z < ext.MaxPoint.Z

              select vec;

 

            // Convert our IEnumerable<> into a List<>

 

            _vecs = vecSet.ToList<ColorVector3>();

          }

          else

          {

            _vecs = vecList;

          }

 

          // Extract the points for display in the jig

          // (note we only take 1 in 5)

 

          _points.Clear();

 

          for (int i = 0; i < _vecs.Count; i += 10)

          {

            ColorVector3 vec = _vecs[i];

            _points.Add(

              new Point3d(vec.X, vec.Y, vec.Z)

            );

          }

 

          // Let's move the mouse slightly to avoid having

          // to do it manually to keep the input coming

 

          System.Drawing.Point pt =

            System.Windows.Forms.Cursor.Position;

          System.Windows.Forms.Cursor.Position =

            new System.Drawing.Point(

              pt.X, pt.Y + _offset

            );

          _offset = -_offset;

 

        }

        catch {}

 

        return SamplerStatus.OK;

      }

      return SamplerStatus.Cancel;

    }

 

    protected override bool WorldDraw(AcGi.WorldDraw draw)

    {

      short origCol = draw.SubEntityTraits.Color;

 

      if (_calibrating)

      {

        draw.SubEntityTraits.Color = transPathColor;

        if (--_calibTicks == 0)

          _calibrating = false;

      }

 

      // This simply draws our points

 

      draw.Geometry.Polypoint(_points, null, null);

 

      if (_drawing)

      {

        try

        {

          // Let's start by creating our spline path

 

          if ((_path == null && _vertices.Count > 1) ||

              (_path != null &&

                _vertices.Count > _path.NumFitPoints))

          {

            if (_path != null)

              _path.Dispose();

 

            _path = new Spline(_vertices, 0, 0.0);

 

            // And our sweep profile, if we don't have one

 

            if (_profile != null)

              _profile.Dispose();

 

            _profile =

              new Circle(

                _vertices[0],

                _vertices[1] - _vertices[0],

                _profRad

              );

 

            // And our sweep options, if we don't have one

 

            if (_sweepOpts == null)

            {

              SweepOptionsBuilder sob =

                new SweepOptionsBuilder();

 

              // Align the entity to sweep to the path

 

              sob.Align =

                SweepOptionsAlignOption.AlignSweepEntityToPath;

 

              // The base point is the start of the path

 

              sob.BasePoint = _path.StartPoint;

 

              // The profile will rotate to follow the path

 

              sob.Bank = true;

              _sweepOpts = sob.ToSweepOptions();

            }

 

            // Finally create a blank solid, if it's null

 

            if (_tube == null)

              _tube = new Solid3d();

 

            // And sweep our profile along our path

 

            _tube.CreateSweptSolid(_profile, _path, _sweepOpts);

          }

        }

        catch (Autodesk.AutoCAD.Runtime.Exception ex)

        {

          _sweepBroken = true;

          _tube.Dispose();

          _tube = null;

          _doc.Editor.WriteMessage(

            "\nException: {0}", ex.Message

          );

        }

 

        // Draw our path, if we have one

 

        if (_path != null)

        {

          draw.SubEntityTraits.Color = transPathColor;

          _path.WorldDraw(draw);

        }

 

        // And our solid

 

        if (_tube != null)

        {

          draw.SubEntityTraits.Color = transSolColor;

          _tube.WorldDraw(draw);

        }

 

        if (_vertices.Count > 0)

        {

          Point3d lastPt = _vertices[_vertices.Count - 1];

 

          if (_cursor == null)

          {

            // Create a cursor sphere

 

            _cursor = new Solid3d();

            ((Solid3d)_cursor).CreateSphere(_profRad);

            _curPt = Point3d.Origin;

          }

          _cursor.TransformBy(

            Matrix3d.Displacement(lastPt - _curPt)

          );

          _curPt = lastPt;

 

          // Draw the cursor

 

          draw.SubEntityTraits.Color =

            (_sweepBroken ? transPathColor : transSolColor);

 

          _cursor.WorldDraw(draw);

        }

      }

      draw.SubEntityTraits.Color = origCol;

 

      return true;

    }

 

    public void AddSolidOrPath()

    {

      if (_tube != null || _path != null)

      {

 

        // Create a true database-resident 3D solid

        // (and let it be green)

 

        Entity ent;

 

        if (_tube == null)

        {

          ent = _path;

          _path = null;

        }

        else

        {

          ent = _tube;

          _tube = null;

        }

 

        BlockTableRecord btr =

          (BlockTableRecord)_tr.GetObject(

            _doc.Database.CurrentSpaceId,

            OpenMode.ForWrite

          );

 

        ent.ColorIndex = finalSolColor;

 

        btr.AppendEntity(ent);

        _tr.AddNewlyCreatedDBObject(ent, true);

 

      }

 

      Cleanup();

 

      _vertices.Clear();

 

      _sweepBroken = false;

    }

 

    public void ExportPointCloud(string filename)

    {

      if (_vecs.Count > 0)

      {

        using (StreamWriter sw = new StreamWriter(filename))

        {

          // For each pixel, write a line to the text file:

          // X, Y, Z, R, G, B

 

          foreach (ColorVector3 pt in _vecs)

          {

            sw.WriteLine(

              "{0}, {1}, {2}, {3}, {4}, {5}",

              pt.X, pt.Y, pt.Z, pt.R, pt.G, pt.B

            );

          }

        }

      }

    }

  }

 

  public class SweepCommands

  {

    [CommandMethod("ADNPLUGINS", "KINEXT", CommandFlags.Modal)]

    public void ImportFromKinect()

    {

      Document doc =

        Autodesk.AutoCAD.ApplicationServices.

          Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

 

      Transaction tr =

        doc.TransactionManager.StartTransaction();

 

      PromptDoubleOptions pdo =

        new PromptDoubleOptions("\nEnter profile radius");

      pdo.AllowZero = false;

      pdo.AllowNegative = false;

      pdo.AllowNone = false;

      pdo.DefaultValue = 40;

      pdo.UseDefaultValue = true;

      PromptDoubleResult pdr = ed.GetDouble(pdo);

 

      if (pdr.Status != PromptStatus.OK)

        return;

 

      KinectSweepJig kj =

        new KinectSweepJig(doc, tr, pdr.Value);

      kj.StartSensor();

      PromptResult pr = ed.Drag(kj);

      kj.StopSensor();

 

      if (pr.Status != PromptStatus.OK && !kj.Finished)

      {

        kj.Cleanup();

        tr.Dispose();

        return;

      }

 

      kj.AddSolidOrPath();

      tr.Commit();

 

      // Manually dispose to avoid scoping issues with

      // other variables

 

      tr.Dispose();

 

      // We'll store most local files in the temp folder.

      // We get a temp filename, delete the file and

      // use the name for our folder

 

      string localPath = Path.GetTempFileName();

      File.Delete(localPath);

      Directory.CreateDirectory(localPath);

      localPath += "\\";

 

      // Paths for our temporary files

 

      string txtPath = localPath + "points.txt";

      string lasPath = localPath + "points.las";

 

      // Our PCG file will be stored under My Documents

 

      string outputPath =

        Environment.GetFolderPath(

          Environment.SpecialFolder.MyDocuments

        ) + "\\Kinect Point Clouds\\";

 

      if (!Directory.Exists(outputPath))

        Directory.CreateDirectory(outputPath);

 

      // We'll use the title as a base filename for the PCG,

      // but will use an incremented integer to get an unused

      // filename

 

      int cnt = 0;

      string pcgPath;

      do

      {

        pcgPath =

          outputPath + "Kinect" +

          (cnt == 0 ? "" : cnt.ToString()) + ".pcg";

        cnt++;

      }

      while (File.Exists(pcgPath));

 

      // The path to the txt2las tool will be the same as the

      // executing assembly (our DLL)

 

      string exePath =

        Path.GetDirectoryName(

          Assembly.GetExecutingAssembly().Location

        ) + "\\";

 

      if (!File.Exists(exePath + "txt2las.exe"))

      {

        ed.WriteMessage(

          "\nCould not find the txt2las tool: please make sure " +

          "it is in the same folder as the application DLL."

        );

        return;

      }

 

      // Export our point cloud from the jig

 

      ed.WriteMessage(

        "\nSaving TXT file of the captured points.\n"

      );

 

      kj.ExportPointCloud(txtPath);

 

      // Use the txt2las utility to create a .LAS

      // file from our text file

 

      ed.WriteMessage(

        "\nCreating a LAS from the TXT file.\n"

      );

 

      ProcessStartInfo psi =

        new ProcessStartInfo(

          exePath + "txt2las",

          "-i \"" + txtPath +

          "\" -o \"" + lasPath +

          "\" -parse xyzRGB"

        );

      psi.CreateNoWindow = false;

      psi.WindowStyle = ProcessWindowStyle.Hidden;

 

      // Wait up to 20 seconds for the process to exit

 

      try

      {

        using (Process p = Process.Start(psi))

        {

          p.WaitForExit();

        }

      }

      catch

      { }

 

      // If there's a problem, we return

 

      if (!File.Exists(lasPath))

      {

        ed.WriteMessage(

          "\nError creating LAS file."

        );

        return;

      }

 

      File.Delete(txtPath);

 

      ed.WriteMessage(

        "Indexing the LAS and attaching the PCG.\n"

      );

 

      // Index the .LAS file, creating a .PCG

 

      string lasLisp = lasPath.Replace('\\', '/'),

              pcgLisp = pcgPath.Replace('\\', '/');

 

      doc.SendStringToExecute(

        "(command \"_.POINTCLOUDINDEX\" \"" +

          lasLisp + "\" \"" +

          pcgLisp + "\")(princ) ",

        false, false, false

      );

 

      // Attach the .PCG file

 

      doc.SendStringToExecute(

        "_.WAITFORFILE \"" +

        pcgLisp + "\" \"" +

        lasLisp + "\" " +

        "(command \"_.-POINTCLOUDATTACH\" \"" +

        pcgLisp +

        "\" \"0,0\" \"1\" \"0\")(princ) ",

        false, false, false

      );

 

      doc.SendStringToExecute(

        "_.-VISUALSTYLES _C _Conceptual ",

        false, false, false

      );

    }

 

    // Return whether a file is accessible

 

    private bool IsFileAccessible(string filename)

    {

      // If the file can be opened for exclusive access it means

      // the file is accesible

      try

      {

        FileStream fs =

          File.Open(

            filename, FileMode.Open,

            FileAccess.Read, FileShare.None

          );

        using (fs)

        {

          return true;

        }

      }

      catch (IOException)

      {

        return false;

      }

    }

 

    // A command which waits for a particular PCG file to exist

 

    [CommandMethod(

      "ADNPLUGINS", "WAITFORFILE", CommandFlags.NoHistory

     )]

    public void WaitForFileToExist()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      HostApplicationServices ha =

        HostApplicationServices.Current;

 

      PromptResult pr = ed.GetString("Enter path to PCG: ");

      if (pr.Status != PromptStatus.OK)

        return;

      string pcgPath = pr.StringResult.Replace('/', '\\');

 

      pr = ed.GetString("Enter path to LAS: ");

      if (pr.Status != PromptStatus.OK)

        return;

      string lasPath = pr.StringResult.Replace('/', '\\');

 

      ed.WriteMessage(

        "\nWaiting for PCG creation to complete...\n"

      );

 

      // Check the write time for the PCG file...

      // if it hasn't been written to for at least half a second,

      // then we try to use a file lock to see whether the file

      // is accessible or not

 

      const int ticks = 50;

      TimeSpan diff;

      bool cancelled = false;

 

      // First loop is to see when writing has stopped

      // (better than always throwing exceptions)

 

      while (true)

      {

        if (File.Exists(pcgPath))

        {

          DateTime dt = File.GetLastWriteTime(pcgPath);

          diff = DateTime.Now - dt;

          if (diff.Ticks > ticks)

            break;

        }

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

      }

 

      // Second loop will wait until file is finally accessible

      // (by calling a function that requests an exclusive lock)

 

      if (!cancelled)

      {

        int inacc = 0;

        while (true)

        {

          if (IsFileAccessible(pcgPath))

            break;

          else

            inacc++;

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

        }

        ed.WriteMessage("\nFile inaccessible {0} times.", inacc);

 

        try

        {

          CleanupTmpFiles(lasPath);

        }

        catch

        { }

      }

    }

 

    internal void CleanupTmpFiles(string txtPath)

    {

      if (File.Exists(txtPath))

        File.Delete(txtPath);

      Directory.Delete(

        Path.GetDirectoryName(txtPath)

      );

    }

  }

}

I’ve made a start on a navigation-focused implementation, but I have to make some changes: right now it jigs a point cloud – just as the previous implementations have done – but as we’re going to be adjusting the view the points would need careful transformation to be at the right place on the screen. So some kind of glyph or avatar that represents the user’s gestures or direction of movement would be simpler (and ultimately less confusing from a visual perspective, I expect).

blog comments powered by Disqus

Feed/Share

10 Random Posts