Monday, December 30, 2024

Clipping Blocks

Can we clip a block (BlockReference) by using API code, as we use "CLIP" command? A recent post in AutoCAD .NET discussion forum asked this question. For whatever reasons, the post stayed quite a while without response.

There are 2 built-in commands in AutoCAD - "XClip" and "Clip", they basically does the same thing under the hood with slight differences of command-line options. In this post, I'll focus on clipping single block (BlockReference).

While this would be my first time coding "Clipping", there was a couple of articles from the most famous AutoCAD .NET API blogger Kean Walmsley looong time ago (more than 12 years ago!):

Adding a 2D spatial filter to perform a simple xclip on an external reference in AutoCAD using .NET

Querying for XCLIP information inside AutoCAD using .NET

So, my code here is mostly simple copy/paste of Kean's, with some necessary updates/changes. The code includes 2 CommandMethods: "ClipBlk" and "RemoveClip":

using Autodesk.AutoCAD.DatabaseServices.Filters;
using DriveCadWithCode2025.AutoCADUtilities;
 
[assemblyCommandClass(typeof(AcadMicsTests.MyCommands))]
 
namespace AcadMicsTests
{
    public class MyCommands 
    {
        private const string FILTER_DICT_NAME = "ACAD_FILTER";
        private const string SPATIAL_DICT_NAME = "SPATIAL";
 
        [CommandMethod("ClipBlk")]
        public static void ClipBlockReference()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect block reference to clip:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
 
            var opt = new PromptKeywordOptions("\nChoose clip boundary:");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Rectangle");
            opt.Keywords.Add("Polygon");
            opt.Keywords.Default = "Rectangle";
            var kres=ed.GetKeywords(opt);
            if (kres.Status != PromptStatus.OK) return;
 
            List<Point3d>? points = null;
            if (kres.StringResult == "Rectangle")
            {
                if (SelectClipWindow(edout Point3d pt1out Point3d pt2))
                {
                    points=new List<Point3d> { pt1pt2 };
                }
            }
            else
            {
                if (SelectClipPolygon(edout List<Point3d>? pts))
                {
                    if (pts != null)
                    {
                        points = pts;
                    }
                }
            }
 
            if (points == nullreturn;
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                if (blk.ExtensionDictionary.IsNull)
                {
                    blk.UpgradeOpen();
                    blk.CreateExtensionDictionary();
                }
 
                var extDict = (DBDictionary)tran.GetObject(
                    blk.ExtensionDictionary, OpenMode.ForWrite);
                DBDictionary filterDict;
                if (!extDict.Contains(FILTER_DICT_NAME))
                {
                    filterDict = new DBDictionary();
                    extDict.SetAt(FILTER_DICT_NAME, filterDict);
                    tran.AddNewlyCreatedDBObject(filterDicttrue);
                }
                else
                {
                    filterDict=(DBDictionary)tran.GetObject(
                        extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                }
 
                if (filterDict.Contains(SPATIAL_DICT_NAME))
                {
                    var id = filterDict.GetAt(SPATIAL_DICT_NAME);
                    filterDict.Remove(SPATIAL_DICT_NAME);
                    var spFilter = tran.GetObject(idOpenMode.ForWrite);
                    spFilter.Erase();
                }
 
                Point2dCollection clipPoints = GetPolygonBoundary(pointsblk.BlockTransform);
 
                var definition = new SpatialFilterDefinition(
                    clipPointsVector3d.ZAxis, 0.0,
                    double.PositiveInfinity, double.NegativeInfinity, true);
                var filter = new SpatialFilter();
                filter.Definition = definition;
                filterDict.SetAt(SPATIAL_DICT_NAME, filter);
                tran.AddNewlyCreatedDBObject(filtertrue);
 
                tran.Commit();
            }
 
            ed.Regen();
            ed.UpdateScreen();
        }
 
        [CommandMethod("RemoveClip")]
        public static void RemoveClip()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect a clipped block reference:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
        }
 
        private static bool SelectClipWindow(Editor edout Point3d pt1out Point3d pt2)
        {
            pt1 = Point3d.Origin;
            pt2 = Point3d.Origin;
 
            var res1 = ed.GetPoint("\nSelect lower-left corner:");
            if (res1.Status == PromptStatus.OK)
            {
                var res2 = ed.GetCorner("\nSelect upper-right corner:"res1.Value);
                if (res2.Status == PromptStatus.OK)
                {
                    pt1 = res1.Value;
                    pt2 = res2.Value;
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool SelectClipPolygon(Editor edout List<Point3d>? points)
        {
            var picker = new PolygonAreaPicker(ed, 1, 25);
            if (picker.PickPolygon())
            {
                points = picker.BoundaryPoints;
                return true;
            }
            else
            {
                points = null;
                return false;
            }
        }
 
        private static bool RemoveClipFromBlockReference(ObjectId blkId)
        {
            var removed = false;
            var db = blkId.Database;
            using (var tran=db.TransactionManager.StartTransaction())
            {
                var blk = tran.GetObject(blkIdOpenMode.ForRead);
                if (!blk.ExtensionDictionary.IsNull)
                {
                    var extDict = (DBDictionary)tran.GetObject(
                        blk.ExtensionDictionary, OpenMode.ForRead);
                    if (extDict.Contains(FILTER_DICT_NAME))
                    {
                        var filterDict = (DBDictionary)tran.GetObject(
                            extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                        if (filterDict.Contains(SPATIAL_DICT_NAME))
                        {
                            var filter = tran.GetObject(
                                filterDict.GetAt(SPATIAL_DICT_NAME), OpenMode.ForWrite);
                            filter.Erase();
                            filterDict.Remove(SPATIAL_DICT_NAME);
                        }
 
                        extDict.UpgradeOpen();
                        extDict.Remove(FILTER_DICT_NAME);
 
                        removed = true;
                    }
                }
 
                tran.Commit();
            }
            return removed;
        }
 
        private static Point2dCollection GetPolygonBoundary(
            IEnumerable<Point3dpointsMatrix3d blkTransform)
        {
            var pts = points
                .Select(pt => pt.TransformBy(blkTransform.Inverse()))
                .Select(pt => new Point2d(pt.X, pt.Y));
            Point2dCollection pt2ds = new Point2dCollection(pts.ToArray());
            return pt2ds;
        }
    }
}

The the private method SelectClipPolygon(), I used a class PolygonAreaPicker for selecting a polygon area. I posted the code of this class in my previously posted article here.

See the video below showing how the code works:



Here are somethings to point out:

1. While the clip boundary seems being created correctly, the boundary created by the code has a noticeable difference from the one created by "CLIP" command: the clip boundary generated by "CLIP" command has a grip, similar to the ones used for control dynamic properties of a dynamic BlockReference. By clicking this grip, the clipping can be flipped to show either external clipping, or internal clipping. However, the clip boundary created by my code does not have this "clipping-flip" grip. Obviously, command "CLIP" does more than just clipping the target BlockReference. Fortunately, the SpatialFilter has a read-write property "Inverted" that we can set by code. Maybe, it is possible to define a grip overrule to mimic the clip boundary created by "CLIP" command. I am not going to dig into it for now.

2. When a clip is applied to a BlockReference, the SpatialFilter that creates the clipping effect is persisted as ExtensionDictionary of the Blockreference. That is, we can examine a BlockReference's ExtensionDictionary to see if it contains a "ACAD_FILTER" dictionary and a DBDictionaryEntry keyed as "SPATIAL". So, theoretically, we can open the existing SpatialFilter object in a Transaction with the SpatialFilter's ObjectId stored in the DBDictionaryEntry. Then I thought, if I want to change the boundary of an existing clipped BlockReference, I could simply open the SpatialFilter in transaction and define a new SpatialFilterDefinition and set it to the SpatialFilter.Definition property, which is set/write-able. Well, as it turns out: yes, I can open the SpatialFilter object for read/write with the ObjectId from the ExtensionDictionary, but it seems the opened SpatialFilter object is read-only, that is, we can look up its "Definition" property to get the boundary's points; as soon as we try to assign the property a newly defined SpatialFilterDefinition, AutoCAD crashes. Therefore, if I want to updated an existing BlockReference's clipping boundary, I need to first delete the attached "ACAD_FILTER" ExtensionDictionary, and then create a new one with a newly defined SpatialFilterDefinition.

3. As described in 2, when updating an existing BlockReference clipping boundary, we need to remove existing one and then create a new one. I found out the 2 steps have to be done in separate Transactions. Or, the clipped BlockReference may not displayed properly.

All in all, the code of the 2 commands showed here basically demonstrates how to use spatial filter API to clip a BlockReference.











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.