Создание "нестандартного" кастомного объекта для AutoCAD, работающего без Object Enabler

Источник: habrahabr
Zanoza

Добрый день.
Те, кто имеет дело с AutoCAD и сторонними решениями для него, наверняка, сталкивались с проблемой прокси объектов, для отображения или перемещения которых требуется установить библиотеки, с помощью которых были созданы данные объекты или так называемые Objects Enablers, от тех же разработчиков. Это достаточно неудобно. Например, получили вы документ от заказчика/субподрядчика и видите только квадраты.

Хочу поделиться с Вами личным опытом создания "нетрадиционного" объекта для AutoCAD. За основу взят анонимный блок. Свойства объекта хранятся отдельно в BlockReference::ExtensionDictionary. Что дает возможность стороннему приложению или скрипту получить к ним доступ и считать их, а при желании изменить, без наличия оригинальных библиотек. Примитивы внутри блока всегда отрисовываются в соответствии со своим состоянием. Да и вообще, устойчивость работы AutoCAD значительно выше. Со стороны все выглядит просто. Но при попытке реализовать данный механизм были выявлены различные "подводные камни". Об этом по порядку.

Для начала требуется реализовать Jig, унаследованный от EntityJig - для задания порядка ввода данных, необходимых для создания объекта. В нем мы должны определить, сколько состояний нам требуется, в моем случае это выглядит так:

public enum MyJigState
{
    EnteringBasePoint,
    EnteringEndPoint,
    Done
}

Основными методами данного класса будут Update() и Sampler() в итоге у меня получился такой класс:
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;
            }
        }
    }

Основной класс объекта я унаследовал от EntityOverride и переопределил Entities:

private Lazy<AcDb.Line> line = new Lazy<AcDb.Line>(() => new AcDb.Line(Point3d.Origin, new Point3d(10, 0, 0)));
        private Lazy<AcDb.Polyline> simbolPoly = new Lazy<AcDb.Polyline>(() => new AcDb.Polyline());
        private Lazy<AcDb.Polyline> arrowPoly = new Lazy<AcDb.Polyline>(() => new AcDb.Polyline());
        private Lazy<AcDb.MText> text = new Lazy<AcDb.MText>(() => new AcDb.MText());
        private Lazy<AcDb.MText> note = new Lazy<AcDb.MText>(() => new AcDb.MText());

 public override IEnumerable<AcDb.Entity> Entities
        {
            get
            {
                yield return line.Value;
                yield return simbolPoly.Value;
                yield return arrowPoly.Value;
                yield return text.Value;
                yield return note.Value;
            }
        }

Что дает возможность не париться об отрисовке объектов входящих в состав блока. Также в данном классе я реализовал функцию, которая по требованию пересчитывает положение и распределение примитивов внутри блока.

После создания экземпляра нашего класса вызывается функция для создания BlockReference, который сразу же удаляется Erase(true) - это дает возможность при прерывании ввода автоматически удалить объект при сохранении файла.

        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<AcDb.BlockTable>())
                    {
                        var blockId = blockTable.Add(lm.BlockRecord);
                        reference = new AcDb.BlockReference(lm.InsertionPoint, blockId);
                        using (var modelSpace = Acad.Database.CurrentSpaceId/*modelSpaceId*/.Write<AcDb.BlockTableRecord>())
                        {
                            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;
        }

Попутно данная функция помещает в XData информацию о том, что это наш объект - для последующего поиска его среди других блоков.

Вроде с созданием самого объекта все просто (хотя на пути к этому пару раз пришлось менять подход к реализации). Первые трудности возникли при добавлении к блоку своих грипов (ручек) и управлении ими, а так же при отмене изменений.
Сделав все по документации Autodesk, унаследовав класс работы с грипами от GripOverrule, столкнулся с трудностями. Они заключались в том, что я потерял возможность отменить изменения, произведенные с объектом. Поэтому мне пришлось создать второй экземпляр, основанный на данных из оригинала и проводить изменения с ним, а при положительном результате переносить все изменения в родительский объект. А также пришлось задействовать TransientManager. Все бы ничего, но оказалось, что в переопределенную функцию MoveGripPointsAt экземпляр объекта (Entity) передается как null, что повлекло за собой создание кастомного класса GripData, который хранит в себе Id объекта.

Следующим камнем оказалось одновременное изменение большого количества объектов одновременно. О нем я догадывался и ранее. Но не в таких масштабах. Панель свойст объекта реализована через стандартный механизм, а он предполагает работу через Com (в AutoCAD 2013 вроде вынесли в ARX), плюс к этому, закрытие транзакций после изменения объекта оказалось безумно долгим. Также постоянный вызов перерисовки объекта стандартными средствами оказался неудовлетворяющим потребностям, по скорости работы. По этим причинам было принято решение использовать ЛИСП функции, которые работают намного шустрее. Но это вылилось в работу по их поиску и оборачиванию в нормальный вид. Здесь стоит заметить, что некоторые из них платформозависимые.

        [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

Дополнительно пришлось реализовать чтение/редактирование XRecord через данные функции.
Что значительно повысило производительность. На данный момент, если создать, к примеру, 1000 объектов, то при изменении свойств одновременно у них всех, скорость выше, чем при работе с 1000 кастомных объектов, созданных стандартными методами.
Принцип изложен очень кратко, но если кого-то заинтересует, можете обращаться через скайп:vsegodvadcatsimvolov или здесь.
Надеюсь кому-то окажется полезным.

Пример уже готовой библиотеки, реализует работу отметки уровня


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=30836