Thursday, January 20, 2011

Mimicking AutoCAD's "AREA" Command With .NET Code 2 - Updated

(Note: latest bug fix update has been appended to the end of this article)

In the previous post leaves out a noticeable feature: showing a distinguishable background color of the selected area. It is due to difficulty for me to decide what entity that I can use as Transient Graphics that can show an region with solid color filled. It is natural to look at Hatch object.

However, due to the fact that when appending loop entities (closed Curves) the method Hatch.AppendLoop() takes a collection of ObjectId as input, the polyline used as the Hatch's loop must be added into curretn working database, so that in AppedLoop() method AutoCAD can find the closed curve as loop from the passed-in ObjectId. In the meantime, the Hatch object itself must also be added to the working database before loop can be appended and the hatch can be evaluated (Hatch.EvaluateHatch() being called after loops being appended).

Since the area need to be drawn dynamically and repeatedly in the PointMonitor event handler with every tiny mouse cursor move, I thought it would be bad thing to do to start atransaction, add new polyline to database, add hatch to database, draw them as Transient Graphics and then erased them whenever PointMonitor even fires. However, I looked hard into the AutoCAD .NET API classes and could not find anything that I can use to draw solid background color in the polygon, other than Hatch. So, I decided to give it a try. Here is the updated code MyAreaCmd class (I highlighted changes in red):

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.DatabaseServices;

namespace AreaCommand
{
    public class MyAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;
        private Autodesk.AutoCAD.DatabaseServices.Hatch _hatch = null;

        private List _points;
        private bool _pickDone;

        private Autodesk.AutoCAD.DatabaseServices.Transaction _tran = null;
        private BlockTableRecord _model = null;

        public MyAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline=null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X,pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                _tran = _dwg.Database.TransactionManager.StartTransaction();

                BlockTable bt = (BlockTable)_tran.GetObject(_dwg.Database.BlockTableId, OpenMode.ForRead);
                _model = (BlockTableRecord)_tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

                //Handling mouse cursor moving during picking
                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    _area = _pline.Area;
                    _perimeter = _pline.Length;
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                _tran.Abort();
                _tran.Dispose();

                _model = null;
            }

            return _pickDone;
        }

        #region private methods

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick first corner: ");
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2)
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.AppendKeywordsToMessage = true;
            }

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X,res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                        _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null || _hatch!=null)
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost, 
                    128, new IntegerCollection());

                if (_pline != null)
                {
                    if (!_pline.IsErased) _pline.Erase();
                    _pline.Dispose();
                    _pline = null;
                }

                if (_hatch != null)
                {
                    if (!_hatch.IsErased) _hatch.Erase();
                    _hatch.Dispose();
                    _hatch = null;
                }
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            //Draw polyline
            Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

            _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

            for (int i = 0; i < _points.Count; i++)
            {
                _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
            }

            _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
            _pline.Closed = true;

            _model.AppendEntity(_pline);
            _tran.AddNewlyCreatedDBObject(_pline, true);

            TransientManager.CurrentTransientManager.AddTransient(
                _pline, TransientDrawingMode.DirectTopmost, 
                128, new IntegerCollection());

            _hatch = new Hatch();
            _hatch.ColorIndex = 1; //set color to RED

            _model.AppendEntity(_hatch);
            _tran.AddNewlyCreatedDBObject(_hatch, true);

            ObjectIdCollection loops = new ObjectIdCollection();
            loops.Add(_pline.ObjectId);

            _hatch.SetHatchPattern(HatchPatternType.PreDefined, "Solid");
            _hatch.AppendLoop(HatchLoopTypes.Outermost, loops);
            _hatch.EvaluateHatch(true);

            TransientManager.CurrentTransientManager.AddTransient(
                _hatch, TransientDrawingMode.DirectTopmost,
                128, new IntegerCollection());
        }

        #endregion
    }
}
As one can see, I started a transaction after user picks second points, and the transaction always gets aborted and disposed in the "finally{...} block. Then the PointMonitor event handler, the polyline and hatch entities get re-created and used as Transient Graphics whenever PointMonitor event fires.

The visual effect looks roughly the same as the real "AREA" command in AutoCAD2010/2011, but flickering is likely noticeable, due to the fact that the 2 entities being added and erased from working database within the transaction, which are quite expensive operation without doubt. See video click here for its action. Nonetheless, it works closely like the AutoCAD built-in "AREA" command, and since picking points with this command is all AutoCAD does at the moment, being expensive process or not probably wouldn't concerns user to much, as long as the flickering is not too heavy.

Bug Fix Update

It turned out that repeatedly adding/erasing the polyline and the hatch to/from working database in the PointMonitor event handler is problematic, as I suspected: if user run this code to measure an existing entity's area, say a rectangle's area, user would definitely uses OSnap so that the picked point snaps to the intended location. The snapping causes the code in PointMonitor handler crash.

I cannot find technical resources to explain what effect OSnap would have to the PointMonitor event, so the solution I managed to find is to add a "try...catch{}" to ignore the deadly exception caused by OSnap in the PointMonitor event handler. Of course this may in turn lead other issue, such as the polyline/hatch drawn in the PointMonitor event handler not being added to working database, thus not being shown as Transient Graphics correctly. However, since the the polyline and hatch are redrawn with every tiny mouse move, merely used as visual hint to user, this wouldn't be a big issue, I figured.

I made a small improvement to the point picking prompt: making [Total] keyword as default keyword input after the third pick.

I have also fixed another bug: the area and perimeter are not calculated based on the actual points picked, not from the polygon drawn in the PointMonitor event handler.

Here is the updated code with updated line in blue:

using System.Collections.Generic;

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.DatabaseServices;

namespace AreaCommand
{
    public class MyAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

        private double _area = 0.0;
        private double _perimeter = 0.0;

        private Autodesk.AutoCAD.DatabaseServices.Polyline _pline = null;
        private Autodesk.AutoCAD.DatabaseServices.Hatch _hatch = null;

        private List _points;
        private bool _pickDone;

        private Autodesk.AutoCAD.DatabaseServices.Transaction _tran = null;
        private BlockTableRecord _model = null;

        public MyAreaCmd(Document dwg)
        {
            _dwg = dwg;
            _editor = _dwg.Editor;
        }

        public double Area
        {
            get { return _area; }
        }

        public double Perimeter
        {
            get { return _perimeter; }
        }

        public bool GetArea()
        {
            _pline=null;

            //Pick first point
            Point3d pt1;
            if (!GetFirstPoint(out pt1)) return false;

            //Pick second point
            Point3d pt2;
            if (!GetSecondPoint(pt1, out pt2)) return false;

            _pickDone = false;

            _points = new List();
            _points.Add(new Point2d(pt1.X,pt1.Y));
            _points.Add(new Point2d(pt2.X, pt2.Y));

            try
            {
                _tran = _dwg.Database.TransactionManager.StartTransaction();

                BlockTable bt = (BlockTable)_tran.GetObject(_dwg.Database.BlockTableId, OpenMode.ForRead);
                _model = (BlockTableRecord)_tran.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite);

                //Handling mouse cursor moving during picking
                _editor.PointMonitor += 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                while (true)
                {
                    if (!PickNextPoint()) break;
                }

                if (_pline != null && _pickDone)
                {
                    //_area = _pline.Area;
                    //_perimeter = _pline.Length;
                    Calculate();
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

                //Remove PointMonitor handler
                _editor.PointMonitor -= 
                    new PointMonitorEventHandler(_editor_PointMonitor);

                _tran.Abort();
                _tran.Dispose();

                _model = null;
            }

            return _pickDone;
        }

        #region private methods

        private void Calculate()
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline p = 
                new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count);
            for (int i = 0; i < _points.Count; i++)
                p.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);

            p.Closed = true;

            _area = p.Area;
            _perimeter = p.Length;

            p.Dispose();
        }

        private bool GetFirstPoint(out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick first corner: ");
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool GetSecondPoint(Point3d basePt, out Point3d pt)
        {
            pt = new Point3d();

            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            opt.UseBasePoint = true;
            opt.BasePoint = basePt;
            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }

        private bool PickNextPoint()
        {
            PromptPointOptions opt = 
                new PromptPointOptions("\nPick next corner: ");
            if (_points.Count > 2)
            {
                opt.Keywords.Add("Undo");
                opt.Keywords.Add("Total");
                opt.Keywords.Default = "Total";
                opt.AppendKeywordsToMessage = true;
                opt.AllowArbitraryInput = false;
            }

            PromptPointResult res = _editor.GetPoint(opt);

            if (res.Status == PromptStatus.OK)
            {
                _points.Add(new Point2d(res.Value.X,res.Value.Y));
                return true;
            }
            else if (res.Status == PromptStatus.Keyword)
            {
                if (res.StringResult == "Undo")
                {
                    if (_points.Count > 2)
                    {
                        _points.RemoveAt(_points.Count - 1);
                    }
                    return true;
                }
                else
                {
                    _pickDone = true;
                    return false;
                }
            }
            else
            {
                _pickDone = false;
                return false;
            }
        }

        private void ClearTransientGraphics()
        {
            if (_pline != null || _hatch!=null)
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost, 
                    128, new IntegerCollection());

                if (_pline != null)
                {
                    if (!_pline.IsErased)
                    {
                        if (_pline.Database!=null) _pline.Erase();
                    }
                    _pline.Dispose();
                    _pline = null;
                }

                if (_hatch != null)
                {
                    if (!_hatch.IsErased)
                    {
                        if (_hatch.Database != null) _hatch.Erase();
                    }
                    _hatch.Dispose();
                    _hatch = null;
                }
            }
        }

        private void _editor_PointMonitor(object sender, PointMonitorEventArgs e)
        {
            ClearTransientGraphics();

            try
            {
                //Draw polyline
                Point2d pt = new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);

                _pline = new Autodesk.AutoCAD.DatabaseServices.Polyline(_points.Count + 1);

                for (int i = 0; i < _points.Count; i++)
                {
                    _pline.AddVertexAt(i, _points[i], 0.0, 0.0, 0.0);
                }

                _pline.AddVertexAt(_points.Count, pt, 0.0, 0.0, 0.0);
                _pline.Closed = true;

                _model.AppendEntity(_pline);
                _tran.AddNewlyCreatedDBObject(_pline, true);

                TransientManager.CurrentTransientManager.AddTransient(
                    _pline, TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());

                _hatch = new Hatch();
                _hatch.ColorIndex = 1;

                _model.AppendEntity(_hatch);
                _tran.AddNewlyCreatedDBObject(_hatch, true);

                ObjectIdCollection loops = new ObjectIdCollection();
                loops.Add(_pline.ObjectId);

                _hatch.SetHatchPattern(HatchPatternType.PreDefined, "Solid");
                _hatch.AppendLoop(HatchLoopTypes.Outermost, loops);
                _hatch.EvaluateHatch(true);

                TransientManager.CurrentTransientManager.AddTransient(
                    _hatch, TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());
            }
            catch 
            { 
                //Ignore the possible exception caused by OSnap
            }
        }

        #endregion
    }
}

No comments:

Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.