Friday, January 21, 2011

Mimicking AutoCAD's "AREA" Command With .NET Code 3 - Using Overrule

After posting the second article on this topic, the approach I used kept bothering me: repeatedly adding/removing a polyline/hatch to/from working database. There must be some better way to do it. Something then suddenly rang a bell in my head: Overrule. And I remembered Kean Walmsley had a post may be of help.

So, I modified the code to use DrawableOverrule to do this job (obviously, the code only works with AutoCAD 2010 and later). I also modified the code to allow pick a background color before picking the first point.

First, I removed the code that creates Hatch, so the MyAreaCmd class was restored back to almost the same as the first version when there is not background rendered when user does the picking. Only a polygon is drawn dynamically.

Then, I created a custom DrawableOverrule, which filters out the polygon as the overrule's target and renders the background.

Finally, I added two 2 lines of code to MyAreaCmd to enable the overrule after user picked first 2 points and disable the overrule when the picking is done. Following is the whole set of code (I have renamed the class to "MyNewAraeCmd").

Class "MyNewAreaCmd":

using System;
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 MyNewAreaCmd
    {
        private Document _dwg;
        private Editor _editor;

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

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

        private List _points;
        private bool _pickDone;

        private int _color = 1;

        public MyNewAreaCmd(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
            {
                //Enable custom Overrule
                MyPolylineOverrule.Instance.StartOverrule(_points, _color);

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

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

                if (_pline != null && _pickDone)
                {
                    Calculate();
                }
            }
            catch
            {
                throw;
            }
            finally
            {
                ClearTransientGraphics();

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

                //Disbale custom Overrule
                MyPolylineOverrule.Instance.EndOverrule();
            }

            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();

            while (true)
            {
                PromptPointOptions opt =
                    new PromptPointOptions("\nPick first corner: ");

                opt.Keywords.Add("Background");
                opt.AppendKeywordsToMessage = true;

                PromptPointResult res = _editor.GetPoint(opt);

                if (res.Status == PromptStatus.OK)
                {
                    pt = res.Value;
                    return true;
                }
                else if (res.Status == PromptStatus.Keyword)
                {
                    PromptIntegerOptions intOpt = new PromptIntegerOptions("\nEnter color number (1 to 7): ");
                    intOpt.AllowNegative = false;
                    intOpt.AllowZero = false;
                    intOpt.AllowArbitraryInput = false;
                    intOpt.UseDefaultValue = true;
                    intOpt.DefaultValue = 1;

                    PromptIntegerResult intRes = _editor.GetInteger(intOpt);

                    if (intRes.Status == PromptStatus.OK)
                    {
                        _color = intRes.Value;
                    }
                }
                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 )
            {
                TransientManager.CurrentTransientManager.EraseTransients(
                    TransientDrawingMode.DirectTopmost,
                    128, new IntegerCollection());

                _pline.Dispose();
                _pline = null;
            }
        }

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

            //Get mouse cursor location
            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;

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

        #endregion
    }
}
Class "MyPolylineOverrule":
using System;
using System.Collections.Generic;

using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;
using Autodesk.AutoCAD.Runtime;


namespace AreaCommand
{
    public class MyPolylineOverrule : DrawableOverrule
    {
        private static MyPolylineOverrule _instance = null;
        private bool _existingOverrulling;
        private int _color = 1;

        private List _points = null;

        public static MyPolylineOverrule Instance
        {
            get
            {
                if (_instance == null) _instance = new MyPolylineOverrule();
                return _instance;
            }
        }

        public int Color
        {
            set { _color = value; }
            get { return _color; }
        }

        public void StartOverrule(List points)
        {
            _points = points;

            _existingOverrulling = Overruling;

            //Add the custom overrule
            AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this, false);

            //Use custom filter, implemented in IsApplicable() method
            SetCustomFilter();

            //Make sure Overrule is enabled
            Overruling = true;
        }

        public void StartOverrule(List points, int color)
        {
            _color = color;

            _points = points;

            _existingOverrulling = Overruling;

            //Add the custom overrule
            AddOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this, false);

            //Use custom filter, implemented in IsApplicable() method
            SetCustomFilter();

            //Make sure Overrule is enabled
            Overruling = true;
        }

        public void EndOverrule()
        {
            //Remove this custom Overrule
            RemoveOverrule(RXObject.GetClass(
                typeof(Autodesk.AutoCAD.DatabaseServices.Polyline)), this);

            //restore to previous Overrule status (enabled or disabled)
            Overruling = _existingOverrulling;
        }

        public override bool WorldDraw(Drawable drawable, WorldDraw wd)
        {
            Point3dCollection pts = new Point3dCollection();
            for (int i = 0; i < _points.Count; i++)
            {
                pts.Add(new Point3d(_points[i].X, _points[i].Y, 0.0));
            }

            wd.SubEntityTraits.FillType = FillType.FillAlways;
            wd.SubEntityTraits.Color = Convert.ToInt16(_color);

            wd.Geometry.Polygon(pts);

            return base.WorldDraw(drawable, wd);
        }

        public override bool IsApplicable(
            Autodesk.AutoCAD.Runtime.RXObject overruledSubject)
        {
            Autodesk.AutoCAD.DatabaseServices.Polyline pl = overruledSubject 
                as Autodesk.AutoCAD.DatabaseServices.Polyline;

            if (pl != null)
            {
                //Only apply this overrule to the polyline
                //that has not been added to working database
                //e.g. created for the Transient Graphics
                if (pl.Database == null)
                    return true;
                else
                    return false;
            }
            else
            {
                return false;
            }
        }
    }
}
Command class "MyCommands":
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;

[assembly: CommandClass(typeof(AreaCommand.MyCommands))]

namespace AreaCommand
{
    public class MyCommands 
    {
        [CommandMethod("MyNewArea")]
        public static void GetNewArea()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            Editor ed = dwg.Editor;

            MyNewAreaCmd cmd = new MyNewAreaCmd(dwg);

            if (cmd.GetArea())
            {
                ed.WriteMessage("\nArea = {0}", cmd.Area);
                ed.WriteMessage("\nPerimeter = {0}", cmd.Perimeter);
            }
            else
            {
                ed.WriteMessage("\n*Cancelled*");
            }
        }
    }
}

See this video clip for its action.

One will notice that the background showing is different from AutoCAD built-in "AREA" command: it only covers the picked points, not the point to be picked when user is moving the mouse. In my opinion, my approach is better: the background-covered area is the real area the command would produce if user hit Enter to complete the command, which should not include the area user has not picked.

2 comments:

aks said...

Thank you Norman for posting your stuff in this blog. You have always been a help back at the VBA discussion back in the day. Now it is on to .NET. The area tool is something I need to remake in .NET so this is a great help. Obviously you created something that you do not use. In my mind it does not have the correct interface. For one the undo should be accomplished with a CTRL key or SPACE key combination click anywhere on the screen. The user need not take the hand off the mouse and the required key should be easy to reach. Furthermore the temporary area boundary needs to remain visible when the area is reported so that the user can the final insanity check. A following input, mouse or key, would remove the area boundary, if wanted. In most cases when measuring areas one wants the area boundary to remain visible as a marker of what has been measured so far. It is best if that area value is placed as text within the boundary. This last task a bit tricky to do because the centroid is not necessarily within the boundary. Thanks again for this blog. aks

Norman Yuan said...

Thanks for the comment and the suggestion of possible enhancement to make it a real user-friendly tool.

Yes, I am not a CAD user who uses something I created. The code I posted in my blog by no means are ready-to-use tools. Rather, I focus on showing some CAD programming tips, tricks from my experience. With that said, though, from your comment we can see, even a small tool like this, there is a lot room to improve it to meet user's expectation.

The particular good part of your suggestion is to show area dynamically during pick. While it can be done by drawing a text within boundary of the picked area, using tool tip might be easier to do, since there is alread an Editor_PointMonitor event handler. I'll see if I can find a bit time to update it later.

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.