«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API

в 16:04, , рубрики: .net, api, C#, CAD/CAM, multicad, nanoCAD, Программирование, проектирование, строительство

На мой взгляд один из лучших способов научится чему-то это поделится знаниями с другими людьми.

В этот раз мне понадобилось понять, как создаются пользовательские объекты в NanoCAD с помощью MultiCAD.NET API. В блоге компании Нанософт есть статья от 2013 года, в которой объясняются базовые вопросы создания пользовательских примитивов. Но согласитесь было бы не интересно, просто воспроизвести эту статью, поэтому мы ее немного дополним.

В нашем случае мы создадим псевдотрехмерную дверь, которая к тому же будет уметь открываться и закрываться. А чтобы нашей маленькой дверке не было одиноко, мы создадим ей подружку — такую же стенку.

Под понятием «псевдо-3D» в данном случае я имею ввиду, что наши объекты не будут обладать свойствами модели твёрдого тела, то есть это будет просто набор связанных геометрических примитивов в трёхмерной системе координат. Может это не совсем корректный термин, но я пока лучше ничего не подобрал.

К сожалению, программистом при всем желании меня сейчас назвать нельзя, поэтому данная статья будет в стиле от новичка-новичкам и наверняка весь код, и приемы, которые вы в ней встретите можно будет улучшить.

Так или иначе если вы интересуетесь: проектированием, САПР, NanoCAD, разработкой под .NET и в частности на C#, а также овцами и Улицей Сезам, то возможно эта статья как раз для вас.

Вам тоже интересно причем тут овцы и Улица Сезам? Тогда милости прошу под кат.

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 1

Не буду нагнетать драматическую паузу, овцы – это некоторая метафора поясняющая бессмысленность, того чем мы сегодня займемся (картинка чуть позже).

Ну, а Улица Сезам, здесь просто потому, что я недавно про нее вспомнил и меня разбила жуткая ностальгия по куклам «Маппетам», так что они помогут нам выдержать единую стилистику повествования.

Наверное, глупо было так быстро раскрыть всю интригу? Но я надеюсь, что вы все же продолжите читать статью.

Содержание:
Часть I: С новым CADом! (Введение).
Часть II: Пишем код под NanoCAD 8.5
Часть III: Пробуем адаптировать код под бесплатный NanoCAD 5.1.
Часть IV: МультиКукиш (Заключение)

1. С новым CADом! (Введение).

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 2

Начать хотелось бы с того, что на портале для разработчиков NanoCAD стала доступна стабильная версия свежего NanoCAD 8.5 SDK и в этот раз мы будем ориентироваться именно на нее.

В своей прошлой статье ориентированной на NanoCAD 8.1, я поделился своим мнением относительно платформы, мы разобрали процесс подготовки проекта к сборке и написали простенькую команду, поэтому если Вы её пропустили и совсем незнакомы с NanoCAD и разработкой с помощью MultiCAD .NET API, то можно начать со статьи «Лицо без шрама» или первые шаги в Multicad.NET API 7 (для Nanocad 8.1)

В этот раз я планирую поменьше «лить воды» и побольше уделить внимания технической стороне.

Единственное перед тем как перейти к разработке наших объектов скажу, что до начала подготовки этой статьи, я по сути пользовался только бесплатной версий NanoCAD (NC 5.1), которая была выпущена аж в 2013 году.

Причиной этому были две: она полностью бесплатна для любых целей, а вторая причина — у меня весьма слабенький комп, поэтому AutoCAD на нем подтормаживает и чертовски долго грузится.

Но поскольку перед тем как писать эту статью, мне надо был потренироваться «ручками» чертить объект, а также разобратся как рабтает трехмерный просмотр объекта, ну и самое главное 10000 раз перезапустить CAD в процессе отладки, то я успел чуть-чуть рассмотреть и NanoCAD 8.5.

Так вот на первый взгляд могу сказать следующее, чертить приятней чем в старой бесплатной версии, а грузится также быстро как старый NanoCAD 5.1, то есть NC 8.5 стартует в несколько раз быстрее чем его сверстник — AutoCAD 2017 (если кому любопытно пишите в комментарии, сделаю замер с секундомером). Остается только надеется, что однажды компания обновит бесплатную версию, перенеся в нее новые API и новые фишки в части функций «электронного кульмана».

Ну и последнее, как я понимаю в версиях NanoCAD доступных для разработчиков, включен модуль трехмерного твердотельного моделирования, но я не смог так сходу разобраться с API к нему, особенно для объектов, создаваемых пользователем. Может быть в другой раз мы его изучим. А пока мы будем довольствоваться «псевдо-3D» объектами.

В этом даже есть свое преимущество, наша с вами библиотека после небольшой адаптации запустится и в бесплатном NanoCAD 5.1, где твердотельного моделирования нет и в помине. Но об этом чуть позже.

2. Пишем код под NanoCAD 8.5

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 3

Да, да, граф фон Знак всё верно сосчитал! Забегая вперед именно столько овец, стен и дверей мы получим в конце. Теперь у графа Знака будет новое задание — считать просмотры и голоса за статью. Я уже прям слышу это: «один, один просмотр, два – два просмотра, три…»
Как обычно, полный код классов и пример dwg фалов вы найдете на GitHub.

А сейчас мы начнем разбирать его по частям. Я не стал прилагать готовые сборки, думаю вы сможете собрать проект самостоятельно, в прошлой статье , я подробно с картинками рассказывал, как создать и настроить проект под MS Visual Studio 2015, для NanoCAD 8.1, так вот с того момента ничего сильно не поменялось.

Поэтому в этот раз я лишь кратко упомяну порядок действий для сборки под Nanocad 8.5:

  1. Создать новый проект выбрать платформу .NET Framework 4, в качестве шаблона выбрать библиотеку классов C#.
  2. Для версии Нанокада x64 (а у меня такая) из папки SDKinclude-x64 добавить в проект ссылки на: mapibasetypes.dll, mapimgd.dll, imapimgd.dll. Не забудьте для всех трех библиотек свойство копировать локально установить в False.
  3. Также добавим ссылки на сборки от Микрософт: System.Windows.Forms.dll, System.Drawing.dll.
  4. В свойствах проекта, на вкладке «Отладка», в качестве действия при запуске выберем «открывать во внешней программе» и укажем путь к исполняемому файлу NC 8.5 (у меня — C:Program FilesNanosoftnanoCAD x64 Plus 8.5nCad.exe)
  5. Создадим два класса DoorPseudo3D.cs и WalllPseudo3D.cs для двери и стены соответственно.
  6. Перейдем по адресу C:ProgramDataNanosoftnanoCAD x64 Plus 8.5DataRW (у вас может отличаться) и найдем или создадим файл load.config следующего содержания
<root>
    <list>
		<module path="C:Users...binDebugnanodoor2.dll"/>
    </list>
</root>

У вас естественно название проекта и путь к нему могут отличаться.

Ну вот и все мы готовы к разработке, теперь у нас по нажатию F5 автоматически запускается NC 8.5 и сразу подгружается наша сборка, останется только вводить разработанные команды.

Еще раз оговорюсь, я не программист, поэтому скорей всего в коде будет куча огрехов: сбои при сохранении, перемещении или копировании объектов, да и просто неоптимальные решения. Если кто-то, не сильно усложняя код сможет эго довести до ума – «земной поклон».

Но так или иначе, на базе этого кода мы с вами сможем немного разобраться в том, как создавать свои объекты, а значит свою главную цель он выполняет.

Ну и безусловно надо сказать большое спасибо Александру Полховскому с форума разработчиков NanoCAD, он мне очень помог с переопределением функционала, связанного с перемещением и поворотом объекта (пригодилось для открытия/закрытия двери). Да и всем другим участникам форума тоже спасибо, напомню это на данный момент один из самых доступных источников информации по MultiCAD.NET API.

Начнем мы с вами со стенки, потому, что она попроще в исполнении.

Для начала добавим пространства имен.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;

Затем создадим класс пользовательского объекта.


namespace nanowall2
{
    //change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
    // Be careful GUID for door and wall classes must be different! 
    // Otherwise there will be problems with saving and moving
    [CustomEntity(typeof(WalllPseudo3D), "8b0986c0-4163-42a4-b005-187111b499d7", "WalllPseudo3D", "WalllPseudo3D Sample Entity")]
        [Serializable]
        public class WalllPseudo3D : McCustomBase
        {

Название класса возьмите какое хотите (можно оставить и мое), главное чтобы он наследовал от McCustomBase.

Все атрибуты класса – обязательны, я если честно параметры атрибута CustomEntity не до конца понимаю, поэтому тупо переделал по аналогии.

«8b0986c0-4163-42a4-b005-187111b499d7» — в моем примере это GUID, я видимо «прощелкал» тот момент, где в документации по .NET объяснялось как с ним работать. Могу сказать только одно, я для простоты брал его из файла настроек сборки, заменяя последнюю цифру для обеспечения уникальности. Если GUID у классов двери и стены будет полностью одинаковым, то начнутся чудеса: стены при копировании будут превращаться в двери, а двери после сохранения файла терять свой функционал, у себя я это вроде исправил надеюсь и у вас проблем не будет.

Определим поля класса.


            private Point3d _pnt1 = new Point3d(100, 100, 0);
            private Point3d _pnt2 = new Point3d(500, 100, 0);
            private double _h = 2085;

Поля _pnt1 и _pnt12, это базовые точки по которым будет строится геометрия нашей стены (длина стены по сути), _h это высота стены по умолчанию (после создания объекта можно будет поправить).

Дальше создадим команду по вызову, которой будет обрисовываться наша дверь.

DrawWall в атрибуте CommandMethod, это имя команды, которое вы будете вводить в командную строку, чтобы вызвать объект, вы можете его сократить например на DWall, без потери функциональности.


        [CommandMethod("DrawWall", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
        public void DrawWall() {

            WalllPseudo3D wall = new WalllPseudo3D();
            wall.PlaceObject();
      
        }

Мы в классе реализующем нашу команду создаем новый экземпляр класса стена (если этого не сделать, то у меня все стенки начинают восприниматься как одна «убер» стенка). А метод PlaceObject мы определим чуть позже.

Определим процедуру отрисовки объекта.


   public override void OnDraw(GeometryBuilder dc)
        {
            dc.Clear();

Я не полностью понимаю этот кусок, но так или иначе в API есть класс GeometryBuilder, на основании, которого мы и будем дальше ваять нашу стенку.
Dc.Clear, по всей видимости очищает каждый раз всю ранее построенную для экземпляра класса геометрию.

Дальше проще.

     Point3d pnt1 = _pnt1;
            Point3d pnt2 = new Point3d(_pnt2.X, pnt1.Y, 0);
            Point3d pnt3 = new Point3d(pnt2.X, pnt1.Y+150, 0);
            Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0);
            // Set the color to ByObject value
            dc.Color = McDbEntity.ByObject;
            Vector3d hvec = new Vector3d(0, 0, _h);

Мы определяем четыре базовые точки на основании которых будет строится основание и верхушка стены, причем первая и вторая точка увязываются с полями класса, а значит именно ими мы потом и будем манипулировать. Наша стенка в длину будет строиться по расстоянию между точками _pnt1 и pnt2, а вот ширина стенки задана жестко её поправить нельзя (так сделано для простоты), но вы легко можете переопределить логику работы по аналогии.

dc.Color – похоже задает свойство «цвет по блоку» для объекта.

Вектор hvec это высота нашей стенки, которую мы будем прибавлять к основанию, чтобы построить верхушку.

Дальше чертим нижнюю и верхнюю стороны стенки.

            dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, _pnt1 });
            dc.DrawPolyline(new Point3d[] { _pnt1.Add(hvec),
            pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});

Соединяем низ и верх ребрами.

            dc.DrawLine(pnt1, pnt1.Add(hvec));
            dc.DrawLine(pnt2, pnt2.Add(hvec));
            dc.DrawLine(pnt3, pnt3.Add(hvec));
            dc.DrawLine(pnt4, pnt4.Add(hvec));

Создаем контур из поллиний для штриховки и затем заливаем его штриховкой кирпич (названия штриховок можно посмотреть в самой программе). Причем у меня не получилось заштриховать через список полилиний пришлось повторять процедуру два раза. Думаю, я что-то не учел.

Мы с вами штрихуем только 2 поверхности стенки — самые длинные, если захотите можете самостоятельно заштриховать остальное.


            // Create contours for the front and rear sides and hatch them
            // In this demo, we hatch only two sides, you can tailor the others yourself
            List<Polyline3d> c1 = new List<Polyline3d>();
            c1.Add(new Polyline3d(
               new List<Point3d>() { pnt1, pnt1.Add(hvec), pnt2.Add(hvec), pnt2, pnt1, }));         
            dc.DrawGeometry(new Hatch(c1, "BRICK", 0, 20, false, HatchStyle.Normal, PatternType.PreDefined, 30), 1);

            List<Polyline3d> c2 = new List<Polyline3d>();
            c2.Add(new Polyline3d(
              new List<Point3d>() { pnt4, pnt4.Add(hvec), pnt3.Add(hvec), pnt3, pnt4, }));
            dc.DrawGeometry(new Hatch(c2, "BRICK", 0, 20, false, HatchStyle.Normal, PatternType.PreDefined, 30), 1);
}

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


//Define the custom properties of the object
        [DisplayName("Height")]
        [Description("Height of wall")]
        [Category("Wall options")]
        public double HWall
        {
            get
            {
                return _h;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                _h = value;

            }
        }

По атрибутам [DisplayName(«Height»)] – имя которое будет в окне свойств, [Description(«Height of wall»)], это описание, но я не понял, где оно отображается, [Category(«Wall options»)] – категория полей, как вы позже увидите на примере дверей, наши поля модно сгруппировать для удобства.
Ну а дальше идет обычно свойство, если вы когда-нибудь делали публичные свойства в Unity 3D, то механизм похож, можем иметь доступ к полям класса прямо из редактора (в нашем случае из САПР).

TryModify() – это обязательный метод, его надо вызывать перед каждым изменением свойств объекта, как я понял. Мы с ним еще пару раз встретимся.

Дальше переопределяем метод отвечающий за размещение объекта на чертеже (помните мы его раньше командой вызывали).

        public override hresult PlaceObject(PlaceFlags lInsertType)
        {
            InputJig jig = new InputJig();

            // Get the first box point from the jig
            InputResult res = jig.GetPoint("Select first point:");
            if (res.Result != InputResult.ResultCode.Normal)
                return hresult.e_Fail;
            _pnt1 = res.Point;

            // Add the object to the database
            this.DbEntity.AddToCurrentDocument();
            
            //Exclude the object from snap points
            jig.ExcludeObject(ID);

            // Monitoring mouse moving and interactive entity redrawing 
            jig.MouseMove = (s, a) => { TryModify(); _pnt2 = a.Point; this.DbEntity.Update(); };

            // Get the second box point from the jig
            res = jig.GetPoint("Select second point:");
            if (res.Result != InputResult.ResultCode.Normal)
            {
                this.DbEntity.Erase();
                return hresult.e_Fail;
            }
            _pnt2 = res.Point;
            
            return hresult.s_Ok;
        }

Этот код почти полностью позаимствован из примера от Нанософт о котором я упоминал в первой главе, я его не на 100% понимаю, но если вкратце мы вызываем команду для ввода первой точки стены (jig.GetPoint), затем помещаем её в чертеж (DbEntity.AddToCurrentDocument()), после чего исключаем объект из привязок, чтобы он нам не мешал вводить вторую точку (_pnt2).

Если все нормально, то объект размещается в чертеже, если нет (например не завершён ввод), то объект удаляется.

И последнее делаем ручки для изменения размера стенки.

        // Create a grip for the base point of the object
        public override bool GetGripPoints(GripPointsInfo info)
        {
            info.AppendGrip(new McSmartGrip<WalllPseudo3D>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset; }));
            info.AppendGrip(new McSmartGrip<WalllPseudo3D>(_pnt2, (obj, g, offset) => { obj.TryModify(); obj._pnt2 += offset; }));
            return true;
        }
    }
    // TODO: There are many shortcomings in this code. 
    // Including failures when working with copying, moving objects and saving files, you can improve it if you want.
}

Как я понимаю, код из примера который я упоминал выше, в части ручек устарел (для NC 8.X) и лучше ориентироваться на код из этого примера.

Ну и естественно я предупреждаю, вас что мой код далек от идеала поэтому адекватным, вменяемым правкам буду рад.

Теперь рассмотрим дверь. Начало — аналогичное.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Forms;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;

namespace nanodoor2
{
    //change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
    // Be careful GUID for door and wall classes must be different! 
    // Otherwise there will be problems with saving and moving


    [CustomEntity(typeof(DoorPseudo3D), "8b0986c0-4163-42a4-b005-187111b499d9", "DoorPseudo3D", "DoorPseudo3D Sample Entity")]
        [Serializable]
        public class DoorPseudo3D : McCustomBase
        {
            // First and second vertices of the box
            private Point3d _pnt1 = new Point3d(0, 0, 0);
            private double _h = 2085;
            private Vector3d _vecStraightDirection = new Vector3d(1, 0, 0);
            private Vector3d _vecDirectionClosed =  new Vector3d(1, 0, 0);
            public enum status { closed , middle, open   };
            private  status _dStatus = status.closed;

        [CommandMethod("DrawDoor", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
        public void DrawDoor() {
            DoorPseudo3D door = new DoorPseudo3D();
            door.PlaceObject();
        }

Разве что добавилось поле, которое будет отвечать за то открыта или закрыта наша дверь, а также появлюсь два вектора _vecStraightDirection — отвечает за текущий поворот двери, _vecDirectionClosed — хранит данные о повороте двери в закрытом состоянии. Это нам все пригодится позже.

А вот непосредственно в геометрии двери, как и следовало ожидать есть маленькие изменения.


public override void OnDraw(GeometryBuilder dc)
        {
            dc.Clear();

            // Define the basic points for drawing
            Point3d pnt1 = new Point3d(0, 0, 0);
            Point3d pnt2 = new Point3d(pnt1.X + 984, pnt1.Y, 0);
            Point3d pnt3 = new Point3d(pnt2.X + 0, pnt1.Y+50, 0);
            Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0);
            // Set the color to ByObject value
            dc.Color = McDbEntity.ByObject;
            Vector3d hvec = new Vector3d(0, 0, _h);

            // Draw the upper and lower sides
            dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, pnt1 });
            dc.DrawPolyline(new Point3d[] { pnt1.Add(hvec),
            pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});

            // Draw the edges
            dc.DrawLine(pnt1, pnt1.Add(hvec));
            dc.DrawLine(pnt2, pnt2.Add(hvec));
            dc.DrawLine(pnt3, pnt3.Add(hvec));
            dc.DrawLine(pnt4, pnt4.Add(hvec));

            // Drawing a Door Handle
            dc.DrawLine(pnt2.Add(new Vector3d( - 190, -0, _h*0.45)), 
                pnt2.Add(new Vector3d(-100, 0, _h * 0.45)));

            dc.DrawLine(pnt3.Add(new Vector3d(-190, 0, _h * 0.45)),
                pnt3.Add(new Vector3d(-100, 0, _h * 0.45)));

            // Create contours for the front and rear sides and hatch them
            // In this demo, we hatch only two sides, you can tailor the others yourself

            List<Polyline3d> c1 = new List<Polyline3d>();
            c1.Add(new Polyline3d(
               new List<Point3d>() { pnt1, pnt1.Add(hvec), pnt2.Add(hvec), pnt2, pnt1, }));
            List<Polyline3d> c2 = new List<Polyline3d>();
            c2.Add(new Polyline3d(
               new List<Point3d>() { pnt4, pnt4.Add(hvec), pnt3.Add(hvec), pnt3, pnt4, }));
            dc.DrawGeometry(new Hatch(c1, "JIS_WOOD", 0, 170, false, HatchStyle.Normal, PatternType.PreDefined, 500), 1);
            dc.DrawGeometry(new Hatch(c2, "JIS_WOOD", 0, 170, false, HatchStyle.Normal, PatternType.PreDefined, 500), 1);
        }

Во-первых, обратите внимание, что мы строим дверь по одной точке, то есть размер двери по ширине и высоте у нас жестко закреплен (ну чтобы она от стены отличалась). Также добавилась секция «// Drawing a Door Handle», там 2 линии которые обозначают условную ручку, ну и еще тип штриховки мы заменили на JIS_WOOD

А вот метод PlaceObject у нас упростился, за счет того, что не нужна вторая ручка.

   public override hresult PlaceObject(PlaceFlags lInsertType)
        {
            InputJig jig = new InputJig();

            // Get the first box point from the jig
            InputResult res = jig.GetPoint("Select first point:");
            if (res.Result != InputResult.ResultCode.Normal)
                return hresult.e_Fail;

            _pnt1 = res.Point;

            // Add the object to the database
            DbEntity.AddToCurrentDocument();
                        
            return hresult.s_Ok;
        }

Дальше идет полная новинка по отношению к классу стенки. За которую я в начале статьи поблагодарил Александра.

Ниже мы переопределим метод, который каким-то мистическим образом отвечает за создании матрицы трансформации (перемещения и поворота) нашей двери.


 	  /// <summary>
        /// Method for changing the object's SC (the graph is built at the origin of coordinates).
        /// </ summary>
        /// <param name = "tfm"> The matrix for changing the position of the object. </ param>
        /// <returns> True - if the matrix is passed, False - if not. </ returns>

        public override bool GetECS(out Matrix3d tfm)
          {
           // Create a matrix that transforms the object.
           // The object is drawn in coordinates(0.0), then it is transformed with the help of this matrix.
           tfm = Matrix3d.Displacement(this._pnt1.GetAsVector()) * Matrix3d.Rotation
                (-this._vecStraightDirection.GetAngleTo(Vector3d.XAxis, Vector3d.ZAxis), Vector3d.ZAxis, Point3d.Origin);
              return true;
             
          }

Я не полностью понимаю работу этого метода, но похоже при каждом перемещении он преобразует нашу дверь в зависимости от базовой точки и вектора поворота, это в конечном счете позволяет нам открывать или закрывать дверь на чертеже.

Дальше мы переопределяем событие которое похоже наступает при трансформации объекта.

         public override void OnTransform(Matrix3d tfm)
        {
            // To be able to cancel(Undo)
            McUndoPoint undo = new McUndoPoint();
            undo.Start();

            // Get the coordinates of the base point and the rotation vector
            this.TryModify();
            this._pnt1 = this._pnt1.TransformBy(tfm);
            this.TryModify();
            this._vecStraightDirection = this._vecStraightDirection.TransformBy(tfm);

            // We move the door only when it is closed if not - undo
            if (_dStatus == status.closed) _vecDirectionClosed = _vecStraightDirection;
            else
            {
                MessageBox.Show("Please transform only closed door");
                undo.Undo();
            }           

            undo.Stop();
        }

Для начала скажу, что чтобы не застревать с разработкой, некоторые моменты я упрощал те решения, которые мне давались с трудом. Одно из таких упрощений — это ограничение на манипуляции с дверью.
Двигать, поворочать, копировать и как-либо еще изменять дверь можно только в закрытом состоянии (оно установлено по умолчанию).
Для того, чтобы оно так и работало мы создаем объект undo и отмечаем точки старта для фиксации изменений.
После чего если все нормально передаем точке _pnt1 и вектору ._vecStraightDirection их состояние после трансформации.
Затем идет проверка условия, если дверь была закрыта, то изменения применяются и дополнительно заносятся в вектор который хранит данные о положении закрытой двери.
Если дверь была открыта (или приоткрыта) мы выдаем сообщение об ошибке и отменяем все изменения.

Поле высоты двери – аналогично стене.


        //Define the custom properties of the object
        [DisplayName("Height")]
        [Description("Height of door")]
        [Category("Door options")]
        public double HDoor
        {
            get
            {
                return _h;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                _h = value;

            }
        }

А вот следующее поле – новенькое

   [DisplayName("Door status")]
        [Description("Door may be: closed, middle, open")]
        [Category("Door options")]
        public status Stat
        {
            get
            {
                return _dStatus;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                // Change the rotation vector for each of the door states
                switch (value)
                {
                    case status.closed:
                        _vecStraightDirection = _vecDirectionClosed;
                    break;
                    case status.middle:
                        _vecStraightDirection = _vecDirectionClosed.Add(_vecDirectionClosed.GetPerpendicularVector().Negate() * 0.575) ;
                    break;
                    case status.open:
                        _vecStraightDirection = _vecDirectionClosed.GetPerpendicularVector()*-1;
                    break;

                    default:
                        _vecStraightDirection = _vecDirectionClosed;
                    break;
                }

                _dStatus = value;

            }
        }

Именно оно у нас и отвечает за состояние двери, в окне свойств появляется выпадающий список со значениями: closed, middle, open (один в один, как определение перечисления вначале класса).
При выборе каждого из значений изменяется в конечном счете вектор отвечающий за поврот двери.

При закрытой он выставляется в заранее сохранённое состояние _vecDirectionClosed;

При полуоткрытом состоянии получается результирующий вектор, который поворачивает нашу дверь на угол примерно 30 градусов, чтобы было похоже на обозначение по ГОСТ.

При открытом состоянии мы просто берем перпендикуляр к нашему вектору закрытого состояния с отрицательным значением (чтобы дверь открывалась по умолчанию вниз).

Дефолтный случай думаю не нужен вовсе, но я оставил.

Ну и последнее это ручка для манипулирования.


        // Create a grip for the base point of the object
        public override bool GetGripPoints(GripPointsInfo info)
        {
            info.AppendGrip(new McSmartGrip<DoorPseudo3D>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset;  }));
            return true;
           
    }
    // TODO: There are many shortcomings in this code. 
    // Including failures when working with copying, moving objects and saving files, you can improve it if you want.
}

За нее можно перетаскивать и все. Ручки у меня время от времени у обоих объектов скачут, куда попало, но отлаживать это у меня уже нет сил (я думал, что закончу статью быстрее, а убил уже три полных дня).

Итак, жмем F5 и с помощью команд DRAWWALL и DRAWDOOR вставляем наши двери и стены.
В результате получим то что на рисунке. На нём я демонстрирую вам работу библиотеки с 4-х разных ракурсов. Овцы к сожалению плоские, да и чертил я их от руки. Ну и двери со стенками чуть-чуть отличаются от тех, что в последней версии .dwg файла на GitHub, просто внес пару правок, а переснимать снимки экрана было лень.

Если будете загружать вашу библиотеку вручную командой NETLOAD помните, что она должна быть загружена до открытия файла с нашими объектами, или они распознаются как proxy объекты.

Для тех, кто новичок в работе с Нанокад, напомню, что получить трехмерный вид на ваши объекты удобно сделав так: вид-> орбита-> зависимая орбита, а вернуть двухмерный вид назад, можно так: вид-> виды и проекции-> вид в плане-> текущая ПСК.

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 4

3. Пробуем адаптировать код под бесплатный NanoCAD 5.1.

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 5

В прошлой статье, у меня почему-то не заработала команда рисующая лицо, а вот в этот раз удалось адаптировать код и наш объект с небольшими ограничениями запускается и в бесплатной версии NaniCAD 5.1.

Для начала кратко расскажу, как настроить среду, отличий почти никаких.

Поэтому я опять лишь кратко упомяну порядок действий для сборки под Nanocad 5.1:

  1. Создать новый проект выбрать платформу .NET Framework 3.5, в качестве шаблона выбрать библиотеку классов C#.
  2. Для версии Нанокада x32 (а 5.1 только такая) из папки SDKinclude добавить в проект ссылку на: mapimgd. Не забудьте свойство копировать локально установить в False.
  3. Также добавим ссылки на сборки от Микрософт: System.Windows.Forms.dll, System.Drawing.dll.
  4. В свойствах проекта, на вкладке «Отладка», в качестве действия при запуске выберем «открывать во внешней программе» и укажем путь к исполняемому файлу NC 5.1 (у меня — C:Program Files (x86)NanosoftnanoCAD 5.1nCad.exe)
  5. Я еще до кучи в разделе «Сборка» установил конечную платформу – x86.
  6. Создадим два класса DoorPseudo3D_nc51.cs и WalllPseudo3D_nc51.cs для двери и стены соответственно.
  7. Перейдем по C:ProgramDataNanosoftnanoCAD 5.1DataRW (у вас может отличаться) и найдем или создадим файл load.config следующего содержания
<root>
    <list>
		<module path="C:Users...binDebug nanodoor2_51.dll"/>
    </list>
</root>

У вас пути к файлам будут свои.

Код кардинально различаться не будет поэтому я спрячу под спойлер оба класса и поясню, только различия.

Итак, стена:

Полный код для стены

//Use Microsoft .NET Framework 3.5 and old version of MultiCad.NET (for NC 5.1)
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 5.1 
//Link mapimgd from Nanocad SDK
//Link System.Windows.Forms and System.Drawing
//The commands: draws a pseudo 3D wall.
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.

//More detailed - https://habrahabr.ru/post/342680/

using System;
using System.ComponentModel;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;

namespace nanowall2
{
    //change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
    // Be careful GUID for door and wall classes must be different! 
    // Otherwise there will be problems with saving and moving
    [CustomEntity(typeof(WalllPseudo3D_nc51), "b4edac1f-7978-483f-91b1-10503d20735a", "WalllPseudo3D_nc51", "WalllPseudo3D_nc51 Sample Entity")]
        [Serializable]
        public class WalllPseudo3D_nc51 : McCustomBase
        {
            // First and second vertices of the box
            private Point3d _pnt1 = new Point3d(100, 100, 0);
            private Point3d _pnt2 = new Point3d(500, 100, 0);
            private double _h = 2085;

            private double _scale = 1000;

        [CommandMethod("DrawWall", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
        public void DrawWall() {
           
            WalllPseudo3D_nc51 wall = new WalllPseudo3D_nc51();
            wall.PlaceObject();
      
        }

        public override void OnDraw(GeometryBuilder dc)
        {

            dc.Clear();
            // Define the basic points for drawing
            Point3d pnt1 = _pnt1;
            Point3d pnt2 = new Point3d(_pnt2.X, pnt1.Y, 0);
            Point3d pnt3 = new Point3d(pnt2.X, pnt1.Y+(150 * _scale), 0);
            Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0);
            // Set the color to ByObject value
            dc.Color = McDbEntity.ByObject;
            Vector3d hvec = new Vector3d(0, 0, _h * _scale);

            // Draw the upper and lower sidestes

            dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, pnt1 });
            dc.DrawPolyline(new Point3d[] { _pnt1.Add(hvec),
            pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});

            // Draw the edges
            dc.DrawLine(pnt1, pnt1.Add(hvec));
            dc.DrawLine(pnt2, pnt2.Add(hvec));
            dc.DrawLine(pnt3, pnt3.Add(hvec));
            dc.DrawLine(pnt4, pnt4.Add(hvec));


        }

        //Define the custom properties of the object
        [DisplayName("WScale")]
        [Description("Wall Scale")]
        [Category("Wall options")]
        public double WScale
        {
            get
            {
                return _scale;
            }
            set
            {
                if (!TryModify())
                    return;
                _scale = value;
            }
        }

        [DisplayName("Height")]
        [Description("Height of wall")]
        [Category("Wall options")]
        public double HWall
        {
            get
            {
                return _h;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                _h = value;

            }
        }

        public override hresult PlaceObject(PlaceFlags lInsertType)
        {
            InputJig jig = new InputJig();

            // Get the first box point from the jig
            InputResult res = jig.GetPoint("Select first point:");
            if (res.Result != InputResult.ResultCode.Normal)
                return hresult.e_Fail;
            _pnt1 = res.Point;

            // Add the object to the database
            this.DbEntity.AddToCurrentDocument();
            
            //Exclude the object from snap points
            jig.ExcludeObject(ID);

            // Monitoring mouse moving and interactive entity redrawing 
            jig.MouseMove = (s, a) => { TryModify(); _pnt2 = a.Point; this.DbEntity.Update(); };

            // Get the second box point from the jig
            res = jig.GetPoint("Select second point:");
            if (res.Result != InputResult.ResultCode.Normal)
            {
                this.DbEntity.Erase();
                return hresult.e_Fail;
            }
            _pnt2 = res.Point;
            
            return hresult.s_Ok;
        }

        // Create a grip for the base point of the object
        public override bool GetGripPoints(GripPointsInfo info)
        {
            info.AppendGrip(new McSmartGrip<WalllPseudo3D_nc51>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset; }));
            info.AppendGrip(new McSmartGrip<WalllPseudo3D_nc51>(_pnt2, (obj, g, offset) => { obj.TryModify(); obj._pnt2 += offset; }));
            return true;
        }

    }

    // TODO: There are many shortcomings in this code. 
    // Including failures when working with copying, moving objects and saving files, you can improve it if you want.

}

В чем разница, во-первых, когда я пытался вставлять объекты в пустой чертеж, то Нанокад мне его так масштабировал, что было не видно ни стенку ни дверь и я добавил поле с масштабом.

Для стены оно изменяет толщину стены, а для двери — толщину и длину.

Соответственно нужные координаты теперь домножаются на масштаб, для которого есть открытое свойство.

И второе отличие в старой версии MultiCAD.NET API — нет класса для работы со штриховкой, могу предположить, что её можно реализовать через API для обычного .NET, но я не стал.

Теперь дверь:

Полный код для двери

//Use Microsoft .NET Framework 3.5 and old version of MultiCad.NET (for NC 5.1)
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 5.1 
//Link mapimgd from Nanocad SDK
//Link System.Windows.Forms and System.Drawing
//The commands: draws a pseudo 3D door.
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.

//More detailed - https://habrahabr.ru/post/342680/

// P.S. A big thanks to Alexander Vologodsky for help in developing a method for pivoting object.

using System;
using System.ComponentModel;
using System.Windows.Forms;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;

namespace nanodoor2
{
    //change "8b0986c0-4163-42a4-b005-187111b499d7" for your Guid from Assembly.
    // Be careful GUID for door and wall classes must be different! 
    // Otherwise there will be problems with saving and moving

    [CustomEntity(typeof(DoorPseudo3D_nc51), "b4edac1f-7978-483f-91b1-10503d20735b", "DoorPseudo3D_nc51", "DoorPseudo3D_nc51 Sample Entity")]
        [Serializable]
        public class DoorPseudo3D_nc51 : McCustomBase
        {
            // First and second vertices of the box
            private Point3d _pnt1 = new Point3d(0, 0, 0);
            private double _scale = 1000;
            private double _h = 2085;
            private Vector3d _vecStraightDirection = new Vector3d(1, 0, 0);
            private Vector3d _vecDirectionClosed =  new Vector3d(1, 0, 0);
            public enum status { closed , middle, open   };
            private status _dStatus = status.closed;

        [CommandMethod("DrawDoor", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
        public void DrawDoor() {
            DoorPseudo3D_nc51 door = new DoorPseudo3D_nc51();
            door.PlaceObject();

        }

        public override void OnDraw(GeometryBuilder dc)
        {
            dc.Clear();

            // Define the basic points for drawing
            Point3d pnt1 = new Point3d(0, 0, 0);
            Point3d pnt2 = new Point3d(pnt1.X + (984 * _scale), pnt1.Y, 0);
            Point3d pnt3 = new Point3d(pnt2.X + 0, pnt1.Y+(50 * _scale), 0);
            Point3d pnt4 = new Point3d(pnt1.X , pnt3.Y, 0) ;
            // Set the color to ByObject value
            dc.Color = McDbEntity.ByObject;
            Vector3d hvec = new Vector3d(0, 0, _h * _scale) ;

            // Draw the upper and lower sides
            dc.DrawPolyline(new Point3d[] { pnt1, pnt2, pnt3, pnt4, pnt1 });
            dc.DrawPolyline(new Point3d[] { pnt1.Add(hvec),
            pnt2.Add(hvec), pnt3.Add(hvec), pnt4.Add(hvec), pnt1.Add(hvec)});

            // Draw the edges
            dc.DrawLine(pnt1, pnt1.Add(hvec));
            dc.DrawLine(pnt2, pnt2.Add(hvec));
            dc.DrawLine(pnt3, pnt3.Add(hvec));
            dc.DrawLine(pnt4, pnt4.Add(hvec));

            // Drawing a Door Handle
            dc.DrawLine(pnt2.Add(new Vector3d( -190 * _scale, -0, _h*0.45 * _scale)), 
                pnt2.Add(new Vector3d(-100 * _scale, 0, _h * 0.45 * _scale)));

            dc.DrawLine(pnt3.Add(new Vector3d(-190 * _scale, 0, _h * 0.45 * _scale)),
                pnt3.Add(new Vector3d(-100 * _scale, 0, _h * 0.45 * _scale)));

        }


        public override hresult PlaceObject(PlaceFlags lInsertType)
        {
            InputJig jig = new InputJig();

            // Get the first box point from the jig
            InputResult res = jig.GetPoint("Select first point:");
            if (res.Result != InputResult.ResultCode.Normal)
                return hresult.e_Fail;
            _pnt1 = res.Point;

            // Add the object to the database
            DbEntity.AddToCurrentDocument();
          
            return hresult.s_Ok;
        }

        /// <summary>
        /// Method for changing the object's SC (the graph is built at the origin of coordinates).
        /// </ summary>
        /// <param name = "tfm"> The matrix for changing the position of the object. </ param>
        /// <returns> True - if the matrix is passed, False - if not. </ returns>
        public override bool GetECS(out Matrix3d tfm)
          {
            // Create a matrix that transforms the object.
            // The object is drawn in coordinates(0.0), then it is transformed with the help of this matrix.
            tfm = Matrix3d.Displacement(this._pnt1.GetAsVector()) * Matrix3d.Rotation
                (-this._vecStraightDirection.GetAngleTo(Vector3d.XAxis, Vector3d.ZAxis), Vector3d.ZAxis, Point3d.Origin);
              return true;
             
          }
          
         public override void OnTransform(Matrix3d tfm)
        {
            // To be able to cancel(Undo)
            McUndoPoint undo = new McUndoPoint();
            undo.Start();
            // Get the coordinates of the base point and the rotation vector
            this.TryModify();
            this._pnt1 = this._pnt1.TransformBy(tfm);
            this.TryModify();
            this._vecStraightDirection = this._vecStraightDirection.TransformBy(tfm);
            // We move the door only when it is closed if not - undo
            if (_dStatus == status.closed) _vecDirectionClosed = _vecStraightDirection;
            else
            {
                MessageBox.Show("Please transform only closed door (when its status = 0)");
                undo.Undo();
            }

            undo.Stop();
        }

        //Define the custom properties of the object
        [DisplayName("Height")]
        [Description("Height of door")]
        [Category("Door options")]
        public double HDoor
        {
            get
            {
                return _h;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                _h = value;

            }
        }

        [DisplayName("DScale")]
        [Description("Door Scale")]
        [Category("Door options")]
        public double DScale
        {
            get
            {
                return _scale;
            }
            set
            {
                if (!TryModify())
                    return;
                _scale = value;
            }
        }

        [DisplayName("Door status")]
        [Description("0-closed, 1-midle, 2-open")]
        [Category("Door options")]
        public status Stat
        {
            get
            {
                return _dStatus;
            }
            set
            {
                //Save Undo state and set the object status to "Changed"
                if (!TryModify())
                    return;

                // Change the rotation vector for each of the door states
                switch (value)
                    {
                    case status.closed:
                        _vecStraightDirection = _vecDirectionClosed;
                    break;
                    case status.middle:
                        _vecStraightDirection = _vecDirectionClosed.Add(_vecDirectionClosed.GetPerpendicularVector().Negate() * 0.575) ;
                    break;
                    case status.open:
                        _vecStraightDirection = _vecDirectionClosed.GetPerpendicularVector()*-1;
                        break;

                    default:
                        
                    break;
                }

                _dStatus = value;

            }
        }

        // Create a grip for the base point of the object
        public override bool GetGripPoints(GripPointsInfo info)
        {
            info.AppendGrip(new McSmartGrip<DoorPseudo3D_nc51>(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset;  }));
            return true;
        }
     
    }

    // TODO: There are many shortcomings in this code. 
    // Including failures when working with copying, moving objects and saving files, you can improve it if you want.

}

Опять почти все тоже самое, единственное, в версии 5.1 похоже поле по-другому обрабатывает перечисления и в окошке свойств объекта вместо слов closed/open, мы увидим значения перечисления: 0, 1, 2 это не очень наглядно, поэтому мы немножко изменили предупреждение об ошибке. Также у двери нет штриховки и есть лишнее свойство для масштаба (его кстати при желании можно реализовать и в классах для NC 8.5).

Получится примерно так:

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 6

4. МультиКукиш (Заключение)

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 7

Как вы помните из прошлой статьи, я и Нанософт никак не связаны, а значит могу себе позволить небольшой элемент критики. Разработчики заявляют о поддержке MultiCAD.NET API в AutoCAD и ZWCAD через определенную прослойку, есть даже статья про это.

Но похоже, это неприоритетное направление разработки. В прошлой статье я писал, что не смог протестировать эту функцию потому, что у меня на компьютере установлен AutoCAD 2017, а последняя размещенная на сайте разработчиков прослойка — «MultiCAD_AC_ZC_Enabler_2209_RU.zip» (которой уже 1.5 года), не поддерживает ничего старше AutoCAD 2016. Ставить ради такого удовольствия еще одну версию Автокада мне не захотелось.

В этот раз я решил попробовать другой вариант, скачал пробную версию ZWCAD+ 2015, опять-таки последнюю версию, которую поддерживает данная прослойка. Не знаю, может быть я «рукожоп», но ни эта библиотека, ни библиотека из прошлой статьи у меня так и не «взлетела» в ZWCAD. Поэтому если у кого-то получится запустить, и он поделится скриншотом буду признателен.

Ну а в остальном, надо сказать, что чем больше возишься с этим API тем больше втягиваешься и даже начинает понемногу нравится, заметно что API улучшается, часть членов классов API уже имеет вменяемое описание на русском, да и само по себе API становится более самодостаточным.

Думаю, что когда выйдет NanoCAD 9 (или как его там назовут) с поддержкой DWG 2018, то станет еще лучше (особенно если как обещали на форуме разработчики, за ним следом выйдет новый бесплатный Нанокад).

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

P.S. Изначально у статьи в заголовке должна была быть другая картинка, но я решил, что она «не айс» и в итоге заменил её на политкорректных Кермита и Гровера (кстати часть картинок ведут на те видео, что я смог найти). А вот исходную картинку я решил спрятать в конце статьи под…

спойлером

«Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API - 8

Автор: Роман

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js