На мой взгляд один из лучших способов научится чему-то это поделится знаниями с другими людьми.
В этот раз мне понадобилось понять, как создаются пользовательские объекты в NanoCAD с помощью MultiCAD.NET API. В блоге компании Нанософт есть статья от 2013 года, в которой объясняются базовые вопросы создания пользовательских примитивов. Но согласитесь было бы не интересно, просто воспроизвести эту статью, поэтому мы ее немного дополним.
В нашем случае мы создадим псевдотрехмерную дверь, которая к тому же будет уметь открываться и закрываться. А чтобы нашей маленькой дверке не было одиноко, мы создадим ей подружку — такую же стенку.
Под понятием «псевдо-3D» в данном случае я имею ввиду, что наши объекты не будут обладать свойствами модели твёрдого тела, то есть это будет просто набор связанных геометрических примитивов в трёхмерной системе координат. Может это не совсем корректный термин, но я пока лучше ничего не подобрал.
К сожалению, программистом при всем желании меня сейчас назвать нельзя, поэтому данная статья будет в стиле от новичка-новичкам и наверняка весь код, и приемы, которые вы в ней встретите можно будет улучшить.
Так или иначе если вы интересуетесь: проектированием, САПР, NanoCAD, разработкой под .NET и в частности на C#, а также овцами и Улицей Сезам, то возможно эта статья как раз для вас.
Вам тоже интересно причем тут овцы и Улица Сезам? Тогда милости прошу под кат.
Не буду нагнетать драматическую паузу, овцы – это некоторая метафора поясняющая бессмысленность, того чем мы сегодня займемся (картинка чуть позже).
Ну, а Улица Сезам, здесь просто потому, что я недавно про нее вспомнил и меня разбила жуткая ностальгия по куклам «Маппетам», так что они помогут нам выдержать единую стилистику повествования.
Наверное, глупо было так быстро раскрыть всю интригу? Но я надеюсь, что вы все же продолжите читать статью.
Содержание:
Часть I: С новым CADом! (Введение).
Часть II: Пишем код под NanoCAD 8.5
Часть III: Пробуем адаптировать код под бесплатный NanoCAD 5.1.
Часть IV: МультиКукиш (Заключение)
1. С новым CADом! (Введение).
Начать хотелось бы с того, что на портале для разработчиков 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
Да, да, граф фон Знак всё верно сосчитал! Забегая вперед именно столько овец, стен и дверей мы получим в конце. Теперь у графа Знака будет новое задание — считать просмотры и голоса за статью. Я уже прям слышу это: «один, один просмотр, два – два просмотра, три…»
Как обычно, полный код классов и пример dwg фалов вы найдете на GitHub.
А сейчас мы начнем разбирать его по частям. Я не стал прилагать готовые сборки, думаю вы сможете собрать проект самостоятельно, в прошлой статье , я подробно с картинками рассказывал, как создать и настроить проект под MS Visual Studio 2015, для NanoCAD 8.1, так вот с того момента ничего сильно не поменялось.
Поэтому в этот раз я лишь кратко упомяну порядок действий для сборки под Nanocad 8.5:
- Создать новый проект выбрать платформу .NET Framework 4, в качестве шаблона выбрать библиотеку классов C#.
- Для версии Нанокада x64 (а у меня такая) из папки SDKinclude-x64 добавить в проект ссылки на: mapibasetypes.dll, mapimgd.dll, imapimgd.dll. Не забудьте для всех трех библиотек свойство копировать локально установить в False.
- Также добавим ссылки на сборки от Микрософт: System.Windows.Forms.dll, System.Drawing.dll.
- В свойствах проекта, на вкладке «Отладка», в качестве действия при запуске выберем «открывать во внешней программе» и укажем путь к исполняемому файлу NC 8.5 (у меня — C:Program FilesNanosoftnanoCAD x64 Plus 8.5nCad.exe)
- Создадим два класса DoorPseudo3D.cs и WalllPseudo3D.cs для двери и стены соответственно.
- Перейдем по адресу 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 объекты.
Для тех, кто новичок в работе с Нанокад, напомню, что получить трехмерный вид на ваши объекты удобно сделав так: вид-> орбита-> зависимая орбита, а вернуть двухмерный вид назад, можно так: вид-> виды и проекции-> вид в плане-> текущая ПСК.
3. Пробуем адаптировать код под бесплатный NanoCAD 5.1.
В прошлой статье, у меня почему-то не заработала команда рисующая лицо, а вот в этот раз удалось адаптировать код и наш объект с небольшими ограничениями запускается и в бесплатной версии NaniCAD 5.1.
Для начала кратко расскажу, как настроить среду, отличий почти никаких.
Поэтому я опять лишь кратко упомяну порядок действий для сборки под Nanocad 5.1:
- Создать новый проект выбрать платформу .NET Framework 3.5, в качестве шаблона выбрать библиотеку классов C#.
- Для версии Нанокада x32 (а 5.1 только такая) из папки SDKinclude добавить в проект ссылку на: mapimgd. Не забудьте свойство копировать локально установить в False.
- Также добавим ссылки на сборки от Микрософт: System.Windows.Forms.dll, System.Drawing.dll.
- В свойствах проекта, на вкладке «Отладка», в качестве действия при запуске выберем «открывать во внешней программе» и укажем путь к исполняемому файлу NC 5.1 (у меня — C:Program Files (x86)NanosoftnanoCAD 5.1nCad.exe)
- Я еще до кучи в разделе «Сборка» установил конечную платформу – x86.
- Создадим два класса DoorPseudo3D_nc51.cs и WalllPseudo3D_nc51.cs для двери и стены соответственно.
- Перейдем по 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).
Получится примерно так:
4. МультиКукиш (Заключение)
Как вы помните из прошлой статьи, я и Нанософт никак не связаны, а значит могу себе позволить небольшой элемент критики. Разработчики заявляют о поддержке 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. Изначально у статьи в заголовке должна была быть другая картинка, но я решил, что она «не айс» и в итоге заменил её на политкорректных Кермита и Гровера (кстати часть картинок ведут на те видео, что я смог найти). А вот исходную картинку я решил спрятать в конце статьи под…
Автор: Роман