Wednesday, May 13, 2020

Prevent Certain Properties of an Entity from Being Changed

In the past I wrote about using ObjectOverrule to prevent entities in AutoCAD from being changed/modified and to force entities being changed in certain way.

An interesting question was raised recently in the Autodesk's discussion forum on how to disable geometry editing. To me, the question could have been a more general one: how to make some of properties of an entity not changeable, while other still can be changed? Once again, ObjectOverrule can play well in the game.

Here is the general idea of doing it with ObjectOverrule:

1. Overriding ObjectOverrule.Open() method: if an entity is opened for write, the values of some target properties, which we do not want them to be changed, will be saved, tagged with ObjectId.

2 Overriding ObjectOverrule.Close() method: check whether there is saved original property value for the entity (by its ObjectId), if found, restore the properties with original values.

Based on this idea, I designed a custom ObjectOverrule. Some explanations are following:

1. The Overrule applies to more general type Entity. It can be more specific, such as Curve, Line..., as needed;

2. No filter is set up. But if needed, proper filter would reduce the overhead in AutoCAD caused by Overrule, minor, or significant;

3. Paired Func/Action is injected into the Overrule whenever the Overrule is enabled. The pair of Func/Action plays the role of extracting original property values in Open() method, and restoring property values in Close() method. This way, the actual value extracting/restoring code is separated from the Overrule itself, so programmer can easily write code to decide what properties to target on different type of entities.

Note: It is even possible to abstract the paired Func/Action into an interface, and implement them in separate project, and make them configurable, so that the custom Overrule can be configured to target different properties, different entity types without having to update code at all. But I'll leave this out of this article.

Here are the code.

Class EntityTarget: it has EntityType property and the pair of Func/Action as properties, used to extract target entity's property values and restore them later.

public class EntityTarget
{
    public Type EntityType { setget; }
    public Func<EntityDictionary<stringobject>> PropExtractFunc
    { setget; }
    public Action<EntityDictionary<stringobject>> PropRestoreAction
    { setget; }
}

Class PropertyFreezeOverrule: its has overridden Open<> and Close() method, in which entity's target property values are extracted into a Dictionary, and then saved in Dictionary keyed with ObjectId at Overrule class level; when the entity is closed, the values of entity's target properties will be restored, if necessary. This, target properties of the entity become not changeable.

using System.Collections.Generic;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
 
namespace FreezePropertyOverrule
{
    public class PropertyFreezeOverrule : ObjectOverrule
    {
        private Dictionary<ObjectIdDictionary<stringobject>> _openedEntities = 
            new Dictionary<ObjectIdDictionary<stringobject>>();
        private List<EntityTarget> _targets = new List<EntityTarget>();
 
        private bool _overruling = false;
 
        private bool _started = false;
 
        public void Start(IEnumerable<EntityTargetoverruleTargets)
        {
            if (_started) Stop();
 
            _targets.Clear();
            _targets.AddRange(overruleTargets);
 
            _openedEntities.Clear();
 
            _overruling = Overrule.Overruling;
 
            Overrule.AddOverrule(RXClass.GetClass(typeof(Curve)), thistrue);
            Overruling = true;
            _started = true;
        }
 
        public void Stop()
        {
            Overrule.RemoveOverrule(RXClass.GetClass(typeof(Curve)), this);
            Overrule.Overruling = _overruling;
            _started = false;
        }
 
        public override void Open(DBObject dbObjectOpenMode mode)
        {
            if (mode == OpenMode.ForWrite)
            {
                ExtractTargetProperties(dbObject);
            }
            base.Open(dbObjectmode);
        }
 
        public override void Close(DBObject dbObject)
        {
            RestoreTargetProperties(dbObject);
            base.Close(dbObject);
        }
 
        #region private methods
 
        private void ExtractTargetProperties(DBObject dbObject)
        {
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass()==RXClass.GetClass(target.EntityType))
                {
                    var propData = target.PropExtractFunc(dbObject as Entity);
                    if (_openedEntities.ContainsKey(dbObject.ObjectId))
                    {
                        _openedEntities[dbObject.ObjectId] = propData;
                    }
                    else
                    {
                        _openedEntities.Add(dbObject.ObjectId, propData);
                    }
                    break;
                }
            }
        }
 
        private void RestoreTargetProperties(DBObject dbObject)
        {
            if (dbObject.IsUndoing) return;
            if (!dbObject.IsModified) return;
            if (dbObject.IsErased) return;
            if (!dbObject.IsWriteEnabled) return;
 
            if (!_openedEntities.ContainsKey(dbObject.ObjectId)) return;
 
            foreach (var target in _targets)
            {
                if (dbObject.GetRXClass() == RXClass.GetClass(target.EntityType))
                {
                    var propData = _openedEntities[dbObject.ObjectId];
                    if (propData!=null)
                    {
                        target.PropRestoreAction(dbObject as EntitypropData);
                        PropertyExtractRestoreUtils.SendMessageToCommandLine(
                            "\nWARNING: editing to this entity was overrule. No change is allowed!\n");
                    }
                    _openedEntities.Remove(dbObject.ObjectId);
                    break;
                }
            }
        }
        #endregion
    }
}

Class PropertyExtracRestoreUtils: it defines a series of paired Func/Action to extracting/restoring entity of specific type. I use the naming convention of GetXxxxProperties() and RestoreXxxxProperties(), where Xxxx is the entity type. For the simplicity, I only defined the pair methods for Line and Circle. But it is easy to add more pairs to target other entity types. Since I also only want to keep Line/Circle geometrically frozen, so, for Line, the properties to freeze are StartPoint and EndPoint; for Circle, Center and Radius.

public static class PropertyExtractRestoreUtils
{
    private const string LINE_START_POINT = "StartPoint";
    private const string LINE_END_POINT = "EndPoint";
 
    private const string CIRCLE_CENTER = "Center";
    private const string CIRCLE_RADIUS = "Radius";
 
    public static Dictionary<stringobjectGetLineProperties(Entity line)
    {
        Dictionary<stringobjectprops = null;
 
        var l = line as Line;
        if (l!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(LINE_START_POINT, l.StartPoint);
            props.Add(LINE_END_POINT, l.EndPoint);
        }
 
        return props;
    }
 
    public static void RestoreLineProperties(
        Entity lineDictionary<stringobjectproperties)
    {
        var l = line as Line;
        if (l != null)
        {
            if (properties.ContainsKey(LINE_START_POINT) &&
                properties.ContainsKey(LINE_END_POINT))
            {
                l.StartPoint = (Point3d)properties[LINE_START_POINT];
                l.EndPoint = (Point3d)properties[LINE_END_POINT];
            }
        }
    }
 
    public static Dictionary<stringobjectGetCircleProperties(Entity circle)
    {
        Dictionary<stringobjectprops = null;
        var c = circle as Circle;
        if (c!=null)
        {
            props = new Dictionary<stringobject>();
            props.Add(CIRCLE_CENTER, c.Center);
            props.Add(CIRCLE_RADIUS, c.Radius);
        }
 
        return props;
    }
 
    public static void RestoreCircleProperties(
        Entity circleDictionary<stringobjectproperties)
    {
        var c = circle as Circle;
        if (c != null)
        {
            if (properties.ContainsKey(CIRCLE_CENTER) &&
                properties.ContainsKey(CIRCLE_RADIUS))
            {
                c.Center = (Point3d)properties[CIRCLE_CENTER];
                c.Radius = (double)properties[CIRCLE_RADIUS];
            }
        }
    }
 
    public static void SendMessageToCommandLine(string msg)
    {
        var ed = Autodesk.AutoCAD.ApplicationServices.Application.
            DocumentManager.MdiActiveDocument.Editor;
        ed.WriteMessage(msg);
    }
}

Following CommandClass put all together into work:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(FreezePropertyOverrule.MyCommands))]
 
namespace FreezePropertyOverrule
{
    public class MyCommands 
    {
        private static PropertyFreezeOverrule _freezeOverrule = null;
 
        [CommandMethod("StartFreeze")]
        public static void StartMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            if (_freezeOverrule==null)
            {
                _freezeOverrule = new PropertyFreezeOverrule();
            }
 
            var targets = new EntityTarget[]
            {
                new EntityTarget
                {
                    EntityType=typeof(Line), 
                    PropExtractFunc=PropertyExtractRestoreUtils.GetLineProperties, 
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreLineProperties 
                },
                new EntityTarget
                {
                    EntityType=typeof(Circle),
                    PropExtractFunc=PropertyExtractRestoreUtils.GetCircleProperties,
                    PropRestoreAction=PropertyExtractRestoreUtils.RestoreCircleProperties
                }
            };
 
            _freezeOverrule.Start(targets);
            ed.WriteMessage(
                "\nEntity freezing overrule started\n");
        }
 
        [CommandMethod("StopFreeze")]
        public static void StopMyOverrule()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            var ed = dwg.Editor;
            if (_freezeOverrule!=null)
            {
                _freezeOverrule.Stop();
            }
 
            ed.WriteMessage(
                "\nEntity freezing overrule stopped.\n");
        }
    }
}

The video clip below shows how the code works: if the Overrule is enabled (started), Line entity cannot be extended, shortened, moved, rotated..., while Circle also cannot be enlarged, shrunk, or moved. However, their other non-geometric properties, such as Layer, Color..., can still be changed. Once the Overrule is disabled/stopped, Line and Circle can be fully modified.



Saturday, May 9, 2020

Cleaning Up Duplicate Vertices of MPolygon

MPolygon, which behaves like a closed Polyline (LwPolyline) with hatch, is often used in AutoCAD for GIS/Mapping type of business operation, representing as an area. One of the common GIS/Mapping requirements for polygon as area is that the polygon should not have duplicate vertices (i.e. 2 vertices should not be too close to each other). Therefore, most GIS/Mapping tools need to have ways to examine polygons for duplicate vertices and remove them, if found.

There is recently a question asked in AutoCAD Map discussion forum on how to do this with MPolygon. There are very few discussions/code samples on MPolygon that one can find online. So, I post the code out of my study here.

Here are things related to solving this issue:

1. An MPolygon has, just like a Hatch, one or more loops (class of MPolygonLoop);
2. MPolygonLoop can be accessed by MPolygon.GetMPolygonLoopAt(int) method;
3. An MPolygonLoop is a collection of BulgeVertex object (class of BulgeVertexCollection);
4. With BulgeVertex object, one can calculate whether 2 BulgeVertex is duplicated (too closed);
5. If duplicate is found, one of the BulgeVertex can be removed from the MPolygonLoop;
6. HOWEVER, simply remove a BulgeVertex from an MPolygonLoop of an MPolygon DOES NOT change the MPolygon, because MPolygon's loop is somehow "immutable". One need to remove the loop from the MPolygon, change the loop (adding/removing/changing one or more BulgeVertices as needed), then append the loop back to the MPolygon.

Here is the code of cleaning duplicate vertices in MPolygon, which is an extension class:

using Autodesk.AutoCAD.DatabaseServices;
 
namespace MPolyUtil
{
    public static class MPolygonExtension
    {
        public static int RemoveDuplicateVertices(
            this MPolygon mpolydouble tolerance=1.0)
        {
            if (!mpoly.IsWriteEnabled) mpoly.UpgradeOpen();
 
            var count = 0;
            var hasDup = true;
            while(hasDup)
            {
                hasDup = PurgeDuplicatedVertex(mpolytolerance);
                if (hasDupcount++;
            }
 
            return count;
        }
        public static int GetVertexCount(this MPolygon mpoly)
        {
            var count = 0;
 
            for (int i=0; i<mpoly.NumMPolygonLoops; i++)
            {
                var loop = mpoly.GetMPolygonLoopAt(i);
                count += loop.Count;
            }
 
            return count;
        }
        private static bool PurgeDuplicatedVertex(
            MPolygon mpolydouble tolerance)
        {
            var purged = false;
 
            for (int i = 0; i < mpoly.NumMPolygonLoops; i++)
            {
                var loop = mpoly.GetMPolygonLoopAt(i);
                if (LoopChanged(looptolerance))
                {
                    mpoly.RemoveMPolygonLoopAt(i);
                    mpoly.AppendMPolygonLoop(looptrue, 0.0);
                    purged = true;
                    break;
                }
            }
 
            return purged;
        }
        private static bool LoopChanged(
            MPolygonLoop loopdouble tolerance)
        {
            var changed = false;
 
            for (int i = 0; i < loop.Count - 1; i++)
            {
                var vertex1 = loop[i];
                var vertex2 = loop[i + 1];
 
                var dist = vertex1.Vertex.GetDistanceTo(vertex2.Vertex);
                if (dist < tolerance)
                {
                    loop.Remove(vertex2);
                    changed = true;
                }
            }
 
            return changed;
        }
    }
}



Here is the CommandClass that put above code into work:

using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(MPolyUtil.MyCommands))]
 
namespace MPolyUtil
{
    public class MyCommands
    {
        [CommandMethod("CleanMPoly")]
        public static void RunMyCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                var mpolyId = SelectMPolygon(ed);
                if (!mpolyId.IsNull)
                {
                    var removedCount = 0;
                    using (var tran = dwg.TransactionManager.StartTransaction())
                    {
                        var mpoly = (MPolygon)tran.GetObject(
                            mpolyIdOpenMode.ForWrite);
                        removedCount = mpoly.RemoveDuplicateVertices(10.0);
 
                        tran.Commit();
                    }
 
                    ed.WriteMessage(
                        $"\nDuplicate vertices found and removed: {removedCount}");
                }
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        private static ObjectId SelectMPolygon(Editor ed)
        {
            var opt = new PromptEntityOptions(
                "\nSelect an MPolygon:");
            opt.SetRejectMessage("\nInvalid: not an MPolygon!");
            opt.AddAllowedClass(typeof(MPolygon), true);
            var res = ed.GetEntity(opt);
            if (res.Status == PromptStatus.OK)
                return res.ObjectId;
            else
                return ObjectId.Null;
        }
    }
}

The video clip below shows the effect of running the code:



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.