Showing posts with label AutoCAD .NET API. Show all posts
Showing posts with label AutoCAD .NET API. Show all posts

Saturday, February 6, 2021

Drag & Drop In AutoCAD (2) - Drag From External Application And Drop OnTo AutoCAD

This is the second of this series on Drag & Drop operation in AutoCAD. See previous ones here:

    Drag & Drop In AutoCAD (1) - The Scenarios

In this post I look into the scenario of dragging from external application and dropping onto a running AutoCAD session.

There are 2 types of external applications user could be dragging something from:

1. Applications that we, as programmers, do not have much control on how the dragging is handled. For example, we cannot control what happens when dragging document content from a Word document into AutoCAD, or we do not have control when the document content is drooped onto AutoCAD editor. The behaviours of drag & drop from both applications (Word and AutoCAD) are designed/built in them and exposed as API for us to intercept into the drag & drop process to manipulate it. So, I'll leave this case out of the discussion here. 

2. Applications we developed, thus, we  have the control at the dragging end of the drag & drop operation. That is, we can choose to use UI components that support drag operation (i.e. having MouseMove event and support DoDragDrop() method) to let drag & drop operation begins at dragging end (our application side). However, at the drop end, AutoCAD runs as a different application, thus, when the drag & drop operation is initialized from our application, we do not have control on how AutoCAD receive the data being dragged and dropped onto it: AutoCAD will do what is was designed to do. There are some dropping behaviours of AutoCAD are known to most AutoCAD users:

  • Insert block
  • Open drawing
  • Insert Text/MText entity
  • Insert OLE object
There could be other behaviours that I am not aware of, but inserting block, opening drawing and inserting MText are the most encountered dropping task when AutoCAD being at the dropping end of drag & drop operation.

Here I built a WinForm desktop application to demonstrate how file name and text string can be dragged from the application, dropped onto AutoCAD and what happens in AutoCAD.

The WinForm application's UI includes a Listbox that can be filled with file names, and a Textbox where user can input text string. There are different options (represented by Radiobuttons) for dragging content in Listbox and/or Textbox that would result in different dropping effect at AutoCAD end.



Here is the code behind the form:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.IO;
 
namespace DragFromExeToAcad
{
    public partial class MainForm : Form
    {
        private readonly string _tempTextFile = "";
 
        public MainForm()
        {
            InitializeComponent();
 
            string folder =
            Path.Combine(Environment.GetFolderPath(
                Environment.SpecialFolder.LocalApplicationData), "Temp");
            _tempTextFile = folder + "\\Dragged_Text.txt";
        }
 
        private void SelectFiles()
        {
            IEnumerable<string> files = null;
            using (var dlg = new OpenFileDialog()
            {
                Title="Select Files",
                Multiselect=true
            })
            {
                if (dlg.ShowDialog()== DialogResult.OK)
                {
                    files = dlg.FileNames;
                }
            }
 
            if (files!=null)
            {
                DragFileListBox.Items.Clear();
                foreach (var f in files)
                {
                    DragFileListBox.Items.Add(f);
                }
            }
 
        }
 
        private void SaveAsTempFile(string textString)
        {
            if (!string.IsNullOrEmpty(_tempTextFile))
            {
                try
                {
                    File.Delete(_tempTextFile);
                }
                catch { }
            }
 
            File.WriteAllText(_tempTextFile, textString);
        }
 
        
 
        private void ExitButton_Click(object sender, EventArgs e)
        {
            this.Close();
        }
 
        private void SelectFileButton_Click(object sender, EventArgs e)
        {
            SelectFiles();
        }
 
        private void DragTextBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (DragTextBox.Text.Trim().Length == 0) return;
            if (e.Button != MouseButtons.Left) return;
 
            string txt = DragTextBox.SelectionLength == 0 ?
                DragTextBox.Text.Trim() : DragTextBox.SelectedText.Trim();
 
            DataObject data;
            if (TextFileRadioButton.Checked)
            {
                // drag the text string as a temporary *.txt file
                SaveAsTempFile(txt);
                data = new DataObject();
                data.SetFileDropList(
                    new System.Collections.Specialized.StringCollection { _tempTextFile });
            }
            else
            {
                data = new DataObject(txt);
            }
            
            DragTextBox.DoDragDrop(data, DragDropEffects.All);
        }
 
        private void DragFileListBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (DragFileListBox.SelectedItems.Count == 0) return;
            if (e.Button != MouseButtons.Left) return;
 
            var data = new DataObject();
 
            var files = new System.Collections.Specialized.StringCollection();
            foreach (var item in DragFileListBox.SelectedItems)
            {
                files.Add(item.ToString());
            }
            data.SetFileDropList(files);
 
            // start drag & drop operation
            DragFileListBox.DoDragDrop(data, DragDropEffects.All);
        }
 
        private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (!string.IsNullOrEmpty(_tempTextFile))
            {
                try
                {
                    File.Delete(_tempTextFile);
                }
                catch { }
            }
        }
    }
}

The mostly used drag & and drop operation from external application onto AutoCAD would be dragging file name, or dragging text string. Here are some explanations about the code and dropping behaviours of AutoCAD.

1. Dragging file name

When dragging file name (single, or multiple), the file name should be stuffed into DataObject with method SetDropFileList(StringCollection), instead of one of the overloaded SetData() methods.

The argument of DragDropEffects used in DoDragDrop() method not only paly the role of showing a different mouse cursor image, but would also result in AutoCAD doing different things at dropping end. For example, when a file name, which is not a plain text/ASCII file, is dropped onto AutoCAD editor, DragDropEffects.Move makes AutoCAD insert a DWG file as block, or as OLE entity, if applicable; however, DragDropEffects.Copy makes AutoCAD open DWG file, and ignore other types of file. Also, regardless DragDropEffects, dropping DWG file onto anywhere of AutoCAD other than editor, AutoCAD would open the drawing file or files. I recommend to use DragDropEffects.All in general to that AutoCAD would do whatever it is designed to do (or not to do) at dropping end and only try other DragDropEffects specific for your special need, if any.

It is worth being pointed out that when dragging a plain text file (typically as *.txt file), AutoCAD will insert the content of the text file as MText.

2. Dragging text string

When text string is filed into DataObject by calling DataObject.SetData(string), or passed to DataObject via its Constructor (new DataObject(string)), AutoCAD receives it and create an MText entity. However, the MText is not created at the location where it is dropped (when mouse' left button releases), instead, it is created at the upper-left corner of current view of AutoCAD editor.

The trick to fix this issue (i.e. let AutoCAD create the MText at where the text string is dropped) is to save the text string into a temporary plain text file, and add this temporary file name to the DataObject by its SetDropFileList(StringCollection) method. That is why you see the extra code that create text file and clean it up when the UI is closed.

Here is the video clip showing drag & drop operation from an external application to AutoCAD;

The next article I will discuss on dragging from AutoCAD and dropping onto modeless form/window of a plugin application in AutoCAD.










Wednesday, February 3, 2021

Drag & Drop In AutoCAD (1) - The Scenarios

Drag & drop is a common computer UI operation that most computer users know. The most used drag & drop operation is drag one or more files or folders in Windows Explorer to copy/move files/folders, or drag file and drop it into an application to open it. All AutoCAD users should know that when a file is dragged from Windows Explorer and dropped into AutoCAD, it is then be either inserted into currently opened drawing in AutoCAD as block, or be opened in AutoCAD for editing/viewing.

As AutoCAD programmers, we are, of course, more interested in how to incorporate drag & drop operation into our own AutoCAD custom applications.

Firstly I want to make it clear what is standard drag & drop operation known by average computer user: user selects something on an application's UI by pressing down mouse' left button and then holds the left button down while moving the mouse - this is drag; then user moves the mouse to a desired location on the same application, or another running application, releases the mouse left button - this is drop. During the dragging, the mouse cursor should show some some kind of visual hint to indicate whether the dragged data is valid to be dropped at certain location.

In terms of drag & drop in AutoCAD use, there are 5 scenarios that might interest us, as AutoCAD programmers:

  • Drag something from AutoCAD and drop onto an external application, be it developed by ourselves or not
  • Drag something from external application, again, be it developed by ourselves or not, and drop onto AutoCAD
  • Drag something from AutoCAD and drop onto our custom plugin UI (obviously, the UI has to be modeless form/window, or we wouldn't be able to drag something from AutoCAD)
  • Drag something from our custom plugin UI and drop onto AutoCAD. In this case, our custom plugin UI can be either modal or modeless
  • Drag something from our custom plugin UI to external application developed by ourselves, or vice versa

In Windows platform, the data exchange in drag & drop process uses IDataObject interface (System.Windows.Forms.IDataObject for WinForm, and System.Windows.IDataObject for WPF). What happens is that when dragging begins, if the UI component, where the dragging begins, has a MouseMove event and has a DoDragDrop() method, then we can handle the MouseMove event to test if the left button of the move is pressed; if yes, we can then create an instance of IDataObject and stuff some data into it (say, call its SetData() method), followed by calling DoDragDrop() method, which takes the data-filled IDataObject as argument. When the mouse' left button is released at a location of UI component of the same application, or another application, if the UI component allows dropping (AllowDrop property of the UI component), we can handle Drop event, where the data is passed in as IDataObject in the event handler's argument, calling IDataObject.GetData() can retrieve the data. Obviously, depending on the data type/format that was filled into IDataObject, the code on dropping side must be able to recognize the data type/format in order to retrieve/use it as expected. Based on these understandings, now we can see what we can do with each of the 4 aforementioned scenarios.

1. Drag something from AutoCAD (entities in AutoCAD's editor) and drop onto external application. 

In this scenario, there could be 2 different cases in terms of external application: an application has no, or very little accessible API for you, as programmer, to intercept into the drag & drop operation; or an application that we developed, thus we could, it seems at least, control what/how the data dragged from AutoCAD be dropped into your application.

For the former, there is nothing we can do, obviously. 

For the latter, even we have control at the dropping/receiving end, we do not have control what/how data get into IDataObject when user drags from AutoCAD editor. In code debugging process, we can peek into the IDataObject before it is dropped, where we could find quite a few pieces of data and the data most relevant to AutoCAD among them is in the format of System.IO.MemoryStream, which we have no way to translate it into meaningful information without Autodesk exposing a suitable API for it.

Therefore, this drag & drop scenario is not a "do-able" scenario.

2. Drag something from external application and drop onto AutoCAD (editor and/or other droppable area beyond editor).

Again, there could be 2 cases: an application we do not have much control as for what/how the data is placed in IDataObject when dragging begins; and an application we build, hence have control to stuff whatever data we desire into the IDataObject for the dragging.

For the former, again, what happens for the drag & drop operation is totally depends on AutoCAD and the application. For example, when dragging *.dwg file name from Windows Explorer into AutoCAD editor triggers AutoCAD's block insertion, or opens the drawing file in AutoCAD; dropping a plain *.txt file would trigger AutoCAD to insert an MText entity; dropping most other types of file would insert an OLE entity. There is nothing we can control here. It is all depends on how the external application is developed to handle its MouseMove event and how AutoCAD is designed to react to the dropping event.

For the latter, while we can control what to be packaged into IDataObject, AutoCAD on the receiving end would only do what it is built to do, which I am no sure I know all of them, but at least I know:

  • inserting a block/or opening a drawing when a *.dwg file name is dropped
  • inserting MText when a plain text file name or text string is dropped
So, when we develop our own desktop application, if we need somehow to drag something over to an running AutoCAD session, at least we know we could do these things when the business workflow requires them.

3. Drag entities from AutoCAD's editor and drop onto our custom developed plugin's modeless UI. While AutoCAD's API does not provide something to allow us a chance to stuff IDataObject with the data we need, therefore the usual way of using IDataObject to transfer data in drag & drop operation does not work. Fortunately, since our custom plugin works inside AutoCAD process, its .NET API provide different way to let us receive dragged data when they are dropped onto our custom UI. So, it is definitely a do-able task.

4. Drag something from our custom plugin UI and drop onto AutoCAD's editor and let AutoCAD generate whatever data in the drawing, be it database-residing objects or not (most likely, they would be entities). In this case, our UI can be either modeless or modal.

5. Drag something from one side of UI and drop on the other side of UI between our custom plugin application in AutoCAD and external application developed by ourselves. In this case, as long as we make sure both ends understand data inside IDataObject. One thing to be remembered is that since the data types defined in AutoCAD managed APIs cannot be used outside AutoCAD, thus the data transferred between our plugins and our custom external application cannot contain these data type. 

I'll follow this up with a series of posts to demonstrate some code on each of the "do-able" scenarios. Stay tuned.









Sunday, November 29, 2020

Find Line's Intersection Points With Block (BlockReference)

 I recently replied a question posted in AutoCAD .NET discussion forum, in which I proposed a code workflow. Then another question in similar context came up. So I thought I might as well write some code to demonstrate my idea in the reply to the first question, which would indirectly answer the second question: after all, once the intersection points of line and the block reference are known, trimming the line would be simple next step.

Firstly to simplify the case, I limit the discussion only on Line and BlockReference with nested entities being Curve type only.

As we know all classes derived from Entity have overloaded method IntersectWith(), which can be used to find intersection points between 2 entities. BlockReference, being derived from Entity, inherently also has its IntersectWith() method. However, because BlockReference is a reference of a composite object with many different entities nested, its IntersectWtith() method is implemented in its own way: it uses its bounding box (GeometricExtents) as its boundary to calculate its intersection points with other entity. Following code demonstrate this:

#region Command to find entity's intersecting point with block: block's bounding box
 
[CommandMethod("BlkIntersect1")]
public static void TestBlockIntersection1()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                // create a rectangle polyline to show the block's GeometricExtents
                var boundBox = CreateBoundBox(blk);
                space.AppendEntity(boundBox);
                tran.AddNewlyCreatedDBObject(boundBox, true);
                tran.TransactionManager.QueueForGraphicsFlush();
 
                while (true)
                {
                    var eRes = ed.GetEntity(
                        "\nSelect an entity intersecting the block:");
                    if (eRes.Status == PromptStatus.OK)
                    {
                        FindBlockIntersectionPoint(eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void FindBlockIntersectionPoint(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
 
    var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
 
    var pts = new Point3dCollection();
    blk.IntersectWith(ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
 
    if (pts.Count > 0)
    {
        foreach (Point3d pt in pts)
        {
            var dbPt = new DBPoint(pt);
            space.AppendEntity(dbPt);
            tran.AddNewlyCreatedDBObject(dbPt, true);
            tran.TransactionManager.QueueForGraphicsFlush();
        }
    }
 
}
 
private static Polyline CreateBoundBox(BlockReference blk)
{
    var ext = blk.GeometricExtents;
    var poly = new Polyline(4);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MinPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MaxPoint.Y),
        0.0, 0.0, 0.0);
    poly.AddVertexAt(
        0, new Point2d(ext.MaxPoint.X, ext.MinPoint.Y),
        0.0, 0.0, 0.0);
    poly.Closed = true;
    poly.ColorIndex = 2;
    return poly;
}
 
#endregion

The code draws a rectangle as the block's bounding box and draws point at intersecting point of a line and the block reference. As following video shows the intersection point is at the location where the line and the bounding box intersect to each other.


Now here is the code I proposed to the first and/or second question: find actual intersection point of a line to the block's "real" boundary (outmost entity nested in the block).

#region command to find entity's intersecting point: block's real boundary
 
[CommandMethod("BlkIntersect2")]
public static void TestBlockIntersection2()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var res = ed.GetEntity("\nSelect block:");
    if (res.Status == PromptStatus.OK)
    {
        if (res.ObjectId.ObjectClass.DxfName.ToUpper() != "INSERT")
        {
            ed.WriteMessage("\nNot a block!");
            return;
        }
 
        dwg.Database.Pdmode = 34;
 
        try
        {
            CadHelper.Highlight(res.ObjectId, true);
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(
                    res.ObjectId, OpenMode.ForRead);
                var space = (BlockTableRecord)tran.GetObject(
                    dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
 
                while (true)
                {
                    var opt = new PromptEntityOptions(
                        "\nSelect a line intersecting with the block:");
                    opt.SetRejectMessage("\nInvalid: not a line!");
                    opt.AddAllowedClass(typeof(Line), true);
                    var eRes = ed.GetEntity(opt);
                    if (eRes.Status == PromptStatus.OK)
                    {
                        GetBlockIntersectionPoints(
                            eRes.ObjectId, blk, space, tran);
                    }
                    else
                    {
                        break;
                    }
                }
 
                tran.Commit();
            }
        }
        finally
        {
            CadHelper.Highlight(res.ObjectId, false);
        }
    }
}
 
private static void GetBlockIntersectionPoints(
    ObjectId entId, BlockReference blk, BlockTableRecord space, Transaction tran)
{
    var line = (Line)tran.GetObject(entId, OpenMode.ForRead);
    if (FindOutmostIntersectingPoints(line, blk, out Point3d pt1, out Point3d pt2))
    {
        var dbPt = new DBPoint(pt1);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
 
        dbPt = new DBPoint(pt2);
        space.AppendEntity(dbPt);
        tran.AddNewlyCreatedDBObject(dbPt, true);
        tran.TransactionManager.QueueForGraphicsFlush();
    }
}
 
private static bool FindOutmostIntersectingPoints(
    Line line, BlockReference blk, out Point3d pt1, out Point3d pt2)
{
    pt1 = Point3d.Origin;
    pt2 = Point3d.Origin;
 
    var points = GetAllIntersectingPoints(line, blk);
    if (points.Count>0)
    {
        pt1 = (from p in points
                orderby p.DistanceTo(line.StartPoint)
                select p).First();
 
        pt2 = (from p in points
                orderby p.DistanceTo(line.EndPoint)
                select p).First();
 
        return true;
    }
    else
    {
        return false;
    }
}
 
private static List<Point3d> GetAllIntersectingPoints(
    Line line, BlockReference blk)
{
    var points = new List<Point3d>();
 
    using (var ents = new DBObjectCollection())
    {
        blk.Explode(ents);
        foreach (DBObject o in ents)
        {
            var ent = (Entity)o;
            var pts = new Point3dCollection();
            line.IntersectWith(
                ent, Intersect.OnBothOperands, pts, IntPtr.Zero, IntPtr.Zero);
            foreach(Point3d p in pts)
            {
                points.Add(p);
            }
            o.Dispose();
        }
    }
 
    return points;
}
 
#endregion

The video below show the code is able to find the 2 intersection points of a line that pass through a block reference, which fall on the block's outmost entity or entities. Obviously this indirectly answers the second question: with these 2 points, the line could ne trimmed easily.


As aforementioned, I limit the code to only apply to Line and block with only Curve type as nested entities. Extra considerations are needed in other cases, such as:

1. If a nested entity in block is a BlockReference, Text/MText/AttributeReference, then the intersection point is on their bounding box. Recursive exploding BlockReference might be needed.

2. If start or end point, or both the intersecting Line/Curve locate inside the block, it might be quite difficult to determine the start/end point is only inside the block's bounding box but outside the outmost entities, or not.

3. If the intersecting entity with block is not a Line, it could intersect with the block more than 2 times.

So writing code to cover all possible cases would be quite some work, if it is possible at all.

Update

As the comment pointed out, I forgot to post the code of Highlight(ObjectId, bool). Here is the code:

public static void Highlight(ObjectId entId, bool highlight)
{
    using (var tran = entId.Database.TransactionManager.StartOpenCloseTransaction())
    {
        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
        if (highlight)
        {
            ent.Highlight();
        }
        else
        {
            ent.Unhighlight();
        }
        tran.Commit();
    }
}

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.



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.