Back to Home

Creating a “custom” custom object for AutoCAD that works without Object Enabler

autocad · objectarx · c #

Creating a “custom” custom object for AutoCAD that works without Object Enabler

Good afternoon.
Those who deal with AutoCAD and third-party solutions for it, for sure, have encountered the problem of proxy objects, for displaying or moving them you need to install the libraries with which these objects or the so-called Objects Enablers were created , from the same developers. This is quite inconvenient. For example, you received a document from a customer / subcontractor and you see only squares.

I want to share with you my personal experience in creating a "non-traditional" object for AutoCAD. The basis is an anonymous block. Object properties are stored separately in BlockReference :: ExtensionDictionary.This makes it possible for a third-party application or script to access them and read them, and if you want to change them, without the presence of the original libraries. Primitives inside a block are always rendered according to their state. Anyway, the stability of AutoCAD is much higher. From the side, everything looks simple. But when trying to implement this mechanism, various "pitfalls" were identified. About this in order.


First you need to implement the Jig inherited from EntityJig - to set the input order for creating the object. In it, we must determine how many states we need, in my case it looks like this:
public enum MyJigState
{
    EnteringBasePoint,
    EnteringEndPoint,
    Done
}

The main methods of this class will be Update () and Sampler (), as a result, I got this class:
public class MyJig : EntityJig
    {
        public enum MyJigState
        {
            EnteringBasePoint,
            EnteringEndPoint,
            Done
        }
        MyJigState _state = MyJigState.EnteringBasePoint;
        PointSampler _basePoint = new PointSampler(AcGe.Point3d.Origin);
        PointSampler _endPoint = new PointSampler(new AcGe.Point3d(10, 10, 0));
        LevelMark _levelMark;
        public MyJig(LevelMark levelmark, BlockReference reference)
            : base(reference)
        {
            this._levelMark = levelmark;
        }
        protected override bool Update()
        {
            try
            {
                Document doc = Autodesk.AutoCAD.ApplicationServices.Application.DocumentManager.MdiActiveDocument;
                using (DocumentLock dl = doc.LockDocument(DocumentLockMode.ProtectedAutoWrite, null, null, true))
                {
                    using (var transaction = Acad.TransactionManager.StartTransaction())
                    {
                        BlockReference reference = (BlockReference)transaction.GetObject(this.Entity.Id, OpenMode.ForWrite, true);
                        reference.Erase(false);
                        reference.Position = this._levelMark.InsertionPoint;
                        reference.BlockUnit = Acad.Database.Insunits;
                        transaction.Commit();
                    }
                    this._levelMark.UpdateEntities();
                    this._levelMark.BlockRecord.UpdateAnonymousBlocks();
                }
            }
            catch (System.Exception ex)
            {
                return false;
            }
            return true;
        }
        public MyJigState State
        {
            get
            {
                return this._state;
            }
            set
            {
                this._state = value;
            }
        }
        protected override SamplerStatus Sampler(JigPrompts prompts)
        {
            try
            {
                switch (_state)
                {
                    case MyJigState.EnteringBasePoint:
                        return _basePoint.Acquire(prompts, "\nВведите точку вставки:", value =>
                        {
                            Matrix3d ucs = Acad.Editor.CurrentUserCoordinateSystem;
                            this._levelMark.InsertionPoint = value
                        });
                    case MyJigState.EnteringEndPoint:
                        return _endPoint.Acquire(prompts, "\nВведите конечную точку:", value =>
                        {
                            Matrix3d ucs = Acad.Editor.CurrentUserCoordinateSystem;
                            this._levelMark.EndPoint = value
                        });
                    default:
                        return SamplerStatus.NoChange;
                }
            }
            catch
            {
                return SamplerStatus.NoChange;
            }
        }
    }


I inherited the main class of the object from EntityOverride and redefined Entities:

private Lazy line = new Lazy(() => new AcDb.Line(Point3d.Origin, new Point3d(10, 0, 0)));
        private Lazy simbolPoly = new Lazy(() => new AcDb.Polyline());
        private Lazy arrowPoly = new Lazy(() => new AcDb.Polyline());
        private Lazy text = new Lazy(() => new AcDb.MText());
        private Lazy note = new Lazy(() => new AcDb.MText());
 public override IEnumerable Entities
        {
            get
            {
                yield return line.Value;
                yield return simbolPoly.Value;
                yield return arrowPoly.Value;
                yield return text.Value;
                yield return note.Value;
            }
        }


Which makes it possible not to bathe about rendering objects included in the block. Also in this class, I implemented a function that, on demand, recounts the position and distribution of primitives within a block.

After creating an instance of our class, a function is called to create a BlockReference , which is immediately deleted by Erase (true) - this makes it possible to automatically delete the object when the file is interrupted when the input is saved.

        static BlockReference CreateBlock(ref LevelMark lm, ObjectContextCollection occ, ObjectId layerId)
        {
            ObjectId id;
            BlockReference reference;
            using (AcAp.Application.DocumentManager.MdiActiveDocument.LockDocument())
            {
                using (var transaction = Acad.TransactionManager.StartTransaction())
                {
                    using (var blockTable = Acad.Database.BlockTableId.Write())
                    {
                        var blockId = blockTable.Add(lm.BlockRecord);
                        reference = new AcDb.BlockReference(lm.InsertionPoint, blockId);
                        using (var modelSpace = Acad.Database.CurrentSpaceId/*modelSpaceId*/.Write())
                        {
                            Matrix3d ucs = Acad.Editor.CurrentUserCoordinateSystem;
                            reference.TransformBy(ucs);
                            id = modelSpace.AppendEntity(reference);
                            ResultBuffer xData = new ResultBuffer(new TypedValue[] {
                                            new TypedValue((int)DxfCode.ExtendedDataRegAppName, MyPlugin.CurrentDictionaryName),
                                                new TypedValue((int)DxfCode.ExtendedDataAsciiString, "This is a " + MyPlugin.CurrentDictionaryName) });
                            reference.XData = xData;
                            reference.LayerId = layerId;
                            xData.Dispose();
                        }
                        transaction.AddNewlyCreatedDBObject(reference, true);
                        reference.Erase(true);
                        transaction.AddNewlyCreatedDBObject(lm.BlockRecord, true);
                    }
                    transaction.Commit();
                }
                if (id != null)
                {
                    lm.BlockId = id;
                    lm.UpdateParameters(id);
                }
            }
            return reference;
        }


Along the way, this function puts information in XData that this is our object - for subsequent search for it among other blocks.

It seems that with the creation of the object itself, everything is simple (although on the way to this a couple of times I had to change the approach to implementation). The first difficulties arose when adding to the block your grips (pens) and managing them, as well as when canceling the changes.
Having done everything according to Autodesk documentation, having inherited the GripOverrule grips class, I ran into difficulties. They consisted in the fact that I lost the ability to undo the changes made to the object. Therefore, I had to create a second instance based on the data from the original and make changes with it, and if the result is positive, transfer all the changes to the parent object. And also had to use the TransientManager. Everything would be fine, but it turned out that an instance of the object ( Entity ) was passed as null to the overridden MoveGripPointsAt function , which entailed the creation of a custom class GripData , which stores the Id of the object.

The next stone was the simultaneous change of a large number of objects at the same time. I guessed about him before. But not on such a scale. The object’s property panel is implemented through the standard mechanism, and it involves working through Com (in AutoCAD 2013 it seems to have been taken out in ARX), plus, closing transactions after changing the object turned out to be insanely long. Also, the constant call to redraw the object using standard tools turned out to be unsatisfactory in terms of speed. For these reasons, it was decided to use LISP functions that work much faster. But this resulted in a job of finding them and wrapping up in a normal look. It is worth noting that some of them are platform-dependent.

        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr acedNEntSelP(string p1, long[] name, Point2d p3, bool p, Matrix3d p4, IntPtr p5);
        public static ResultBuffer NEntSelP(string p1, ObjectId id, Point2d p3, bool p, Matrix3d p4, ref ResultBuffer p5)
        {
            long[] adsName = new long[2];
            if (acdbGetAdsName(adsName, id) != 0)
                return null;
            IntPtr ip = acedNEntSelP(p1, adsName, p3, p, p4, p5.UnmanagedObject);
            if (ip != IntPtr.Zero)
                return ResultBuffer.Create(ip, false);
            return null;
        }
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern IntPtr acdbEntGet(long[] name);
        public static ResultBuffer EntGet(ObjectId id)
        {
            long[] adsName = new long[2];
            if (acdbGetAdsName(adsName, id) != 0)
                return null;
            IntPtr ip = acdbEntGet(adsName);
            if (ip != IntPtr.Zero)
                return ResultBuffer.Create(ip, false);
            return null;
        }
        [DllImport("acdb18.dll", CallingConvention = CallingConvention.Cdecl)]
        private static extern int acdbGetObjectId(ref ObjectId objId, long[] name);
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern int acdbEntMakeX(IntPtr resBuf, long[] adsName);
        public static ObjectId EntMakeX(ResultBuffer resBuf)
        {
            long[] adsName = new long[2];
            int ip = acdbEntMakeX(resBuf.UnmanagedObject, adsName);
            if (ip == RTNORM)
            {
                ObjectId objId = new ObjectId();
                acdbGetObjectId(ref objId, adsName);
                return objId;
            }
            else
            {
                return ObjectId.Null;
            }
        }
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern int acdbEntDel(long[] name);
        public static int EntDel(ObjectId id)
        {
            long[] adsName = new long[2];
            if (acdbGetAdsName(adsName, id) != 0)
                return RTERROR;
            return acdbEntDel(adsName);
        }
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern int acdbDictAdd(long[] dictName, string symName, long[] objName);
        public static int DictAdd(ObjectId dictNameId, string symName, ObjectId objNameId)
        {
            long[] dictName = new long[2];
            if (acdbGetAdsName(dictName, dictNameId) != 0)
                return RTERROR;
            long[] objName = new long[2];
            if (acdbGetAdsName(objName, objNameId) != 0)
                return RTERROR;
            byte[] srcb = System.Text.UnicodeEncoding.Unicode.GetBytes(symName);
            System.Text.ASCIIEncoding ue = new System.Text.ASCIIEncoding();
            string dst = ue.GetString(srcb);
            return acdbDictAdd(dictName, dst, objName);
        }
        const short RTNORM = 5100;
        const short RTERROR = -5001;
#if WIN32
        [DllImport("acdb18.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AAY01JVAcDbObjectId@@@Z")]
        public static extern int acdbGetAdsName(long[] objName, ObjectId objId);
        //public static extern Autodesk.AutoCAD.Runtime.ErrorStatus acdbGetAdsName(out long adsName, ObjectId id);
#else
        [DllImport("acdb18.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acdbGetAdsName@@YA?AW4ErrorStatus@Acad@@AEAY01_JVAcDbObjectId@@@Z")]
        public static extern int acdbGetAdsName(long[] objName, ObjectId objId);
#endif
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        public static extern int acdbEntUpd(long[] ent);
        public static long[] GetAdsName(ObjectId id)
        {
            long[] adsName = new long[1];
            acdbGetAdsName(adsName, id);
            return adsName;
        }
        public static bool EntityUpdate(long[] adsName)
        {
            return (acdbEntUpd(adsName) == RTNORM);
        }
        public static bool EntityUpdate(ObjectId id)
        {
            long[] adsName = new long[1];
            if (acdbGetAdsName(adsName, id) != 0)
                return false;
            return (acdbEntUpd(adsName) == RTNORM);
        }
        [DllImport("acad.exe", CallingConvention = CallingConvention.Cdecl)]
        private static extern int acdbEntMod(System.IntPtr resbuf);
        public static int EntMod(ResultBuffer resultBuffer)
        {
            return acdbEntMod(resultBuffer.UnmanagedObject);
        }
#if WIN32
        [DllImport("acad.exe",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Cdecl,
            EntryPoint = "?acedSetStatusBarProgressMeter@@YAHPB_WHH@Z")]
        public static extern int acedSetStatusBarProgressMeter(string label, int minPos, int maxPos);
        [DllImport("acad.exe",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Cdecl,
            EntryPoint = "?acedSetStatusBarProgressMeterPos@@YAHH@Z")]
        public static extern int acedSetStatusBarProgressMeterPos(int pos);
        [DllImport("acad.exe",
            CharSet = CharSet.Auto,
            CallingConvention = CallingConvention.Cdecl,
            EntryPoint = "?acedRestoreStatusBar@@YAXXZ")]
        public static extern int acedRestoreStatusBar();
#else
    [DllImport("acad.exe",
        CharSet = CharSet.Auto,
        CallingConvention = CallingConvention.Cdecl,
        EntryPoint = "?acedSetStatusBarProgressMeter@@YAHPEB_WHH@Z")]
    public static extern int acedSetStatusBarProgressMeter(string label, int minPos, int maxPos);
    [DllImport("acad.exe",
        CharSet = CharSet.Auto,
        CallingConvention = CallingConvention.Cdecl,
        EntryPoint = "?acedSetStatusBarProgressMeterPos@@YAHH@Z")]
    public static extern int acedSetStatusBarProgressMeterPos(int pos);
    [DllImport("acad.exe",
        CharSet = CharSet.Auto,
        CallingConvention = CallingConvention.Cdecl,
        EntryPoint = "?acedRestoreStatusBar@@YAXXZ")]
    public static extern int acedRestoreStatusBar();
#endif


Additionally, I had to implement reading / editing of XRecord through these functions.
Which significantly increased productivity. At the moment, if you create, for example, 1000 objects, then when you change the properties of all of them at the same time, the speed is higher than when working with 1000 custom objects created using standard methods.
The principle is stated very briefly, but if anyone is interested, you can contact via skype: vsegodvadcatsimvolov or here.
I hope someone will be useful.

An example of a ready-made library, implements the work of a level mark

Read Next