«Истина в вине» или пробуем программировать NanoCAD под Linux (MultiCAD.NET API)
Практически со времени выхода первой «дееспособной» версии NanoCAD, среди сообщества пользователей остро встал вопрос о необходимости реализации данной САПР под Linux
Наверное, вы подумали, что эта статья родилась, потому что разработчики наконец-то «сделали это!». Спешу вас успокоить — все осталось на своих местах. О версии NanoCAD под Linux лично мне по-прежнему ничего не известно. Поэтому мы будем пытаться использовать Wine.
Так что эта короткая заметка будет не столько об использовании NanoCAD в Linux, а о программировании для Nanocad в системе отличной от Windows, а если еще точнее, то том, как я вооружился Linux Mint, MonoDevelop и попытался собрать библиотеку (.dll) для бесплатной версии NanoCAD с применением MultiCAD.NET API.
Если вам интересно, что же из этого получилось, милости прошу под кат!
Часть I: …рукам покоя не дает
Кажется, мое новое хобби, тоже в итоге выльется в мини-цикл статей и поскольку мы будем ссылаться на прошлые наработки, приведу под спойлером ссылки на все предыдущие статьи.
- «Лицо без шрама» или первые шаги в Multicad.NET API 7 (для Nanocad 8.1)
- «Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API
- «Я слежу за тобой» или как из CADa сделать SCADA (MultiCAD.NET API)
В самом начале на всякий случай уточню, что я не программист, что меня при всем желании нельзя назвать «уверенным пользователем Linux», а также что с разработчиками NanoCAD (ЗАО «Нанософт») я никак не связан, так что все, что будет ниже это мое частное и местами наивно-непрофессиональное мнение.
Наверное, будет уместно поделиться исходными данными, с которыми я подошел к решению задачи:
- ОС — Linux Mint 18.3 «Sylvia» — Cinnamon (64-bit)
- Wine — PlayOnLinux (Wine 2.2)
- IDE — MonoDevelp 7.1 (Mono 5.2.0)
- Руки — «из попы» (и большое любопытство)
Поскольку людей, программирующих для NanoCAD и так немного, а среди них настолько экстремальных и неудержимых людей — готовых программировать для NanoCAD под Linix явно еще меньше, то всё что будет ниже по тексту думаю не имеет особой практической ценности и можно рассматривать только в целях удовлетворения технического любопытства.
Если после предыдущего абзаца вы еще продолжили это читать, значит у вас выдержка как у Чака Норриса, а это достойно уважения.
Пришло наконец время узнать, что нас ждет в этой статье.
Содержание:
- Часть I: …рукам покоя не дает
- Часть II: пытаемся запустить с помощью Wine
- Часть III: собираем проект в MonoDevelop
- Часть IV: рождение пингвина
Часть II: пытаемся запустить с помощью Wine
Надо отметить, что разработчики хоть напрямую и не тратят свои ресурсы на адаптацию под Linux, все же иногда переживают за нас. Толчком, к созданию этой статьи послужил их пост на Хабре на который я случайно наткнулся — «Репост совета: запуск nanoCAD free 3.5/3.7 под Linux с помощью Wine».
Конечно, версия 3.7 уже совсем устарела, и я решил попробовать, близкую к ней более свежую версию NanoCAD 5.1.
Если вдруг вы совсем не знакомы, с Нанокадом, то можно почитать самую первую статью цикла. Если кратко, то NanoCAD 5.1 хоть и не программа с открытым исходным кодом, но всё же он абсолютно бесплатен для коммерческого использования, лично я думаю, что если бы разработчики сделали вменяемую адаптацию для Linux, хотя бы с помощью Wine, то благодаря привычному интерфейсу и схожести с AutoCAD, в функциональности двухмерного «электронного кульмана» он мог бы вполне составить конкуренцию, тем же FreeCAD и QCAD (LibreCAD).
Но пока, мы будем довольствоваться тем, что есть, а есть у нас советы с форума как запустить NanoCAD 5.1 под Wine 1.4.
К сожалению, я наткнулся на этот материал уже после того, как окончательно потерял энтузиазм, так что, если у кого-то нормально запустится после выполнения этой инструкции, поделитесь в комментариях.
А я расскажу, вам, что получилось у меня.
- Начал я с установки Wine из дистрибутива Mint, а там была версия 1.6, в итоге получилось, то о чем говорилось в двух вышеописанных советах из статей. Некорректные цвета иконок и проблемы с некоторыми кнопками. Собственно, как оказалось на этом можно было и остановится, ибо сильно лучшего результата я не добился, но я в итоге решил поставить PlayOnLinux и продолжить «оживлять Франкенштейна».
- Для начала я попробовал версию 1.4.1, в которой всё должно было бы работать, и я подозреваю, что оно действительно бы работало нормально, но есть одна проблемы — я никак не смог установить .NET Framework 3.5 (да и .NET Framework 3 тоже). Перепробовал кучу всего, но как правило в процессе установки в разных местах всегда вылетала какая-нибудь ошибка.
NET Framework 2 это крайняя версия, которую удалось поставить (4-я версия не в счет), с этой версией фреймворка NanoCAD, даже начинает загружаться и если повезет загрузит большую, часть. Не будут работать только библиотеки, в первую очередь связанные с API (что нам не подходит).
На снимке экрана видно, что можно чертить и что все цвета отображаются корректно, но к сожалению команда Netload — не работает, а значит нашу библиотеку в будущем не загрузить, поэтому идем дальше.
- Дальше я перепробовал кучу версий Wine под PlayOnLinux: 1.3.Х, 1.5.Х, 2.1, даже WINE@Etersoft пробовал, везде были проблемы с установкой. NET Framework 3.5.
- В итоге я остановился на последней доступной мне в PlayOnLinux версии Wine — 2.2, под неё, каким-то чудом. NET Framework встал, NanoCAD запустился полностью, правда с цветами все же проблемы. Скриншоты того, как оно выглядит будут чуть позже.
- Другой требуемый пререквизит Microsoft Visual C++ 2008 SP1 Redistributable Package (x86), почти всегда устанавливается без проблем (можно прямо тот, что идет вместе с программой). Также не забудьте в процессе установки NanoCAD установить пакет разработчика (SDK)
Иногда при установке барахлит мастер регистрации. У меня бывали случаи, когда напрямую on-line регистрация сбоила, тогда можно взять файл лицензии с уже установленной версии например под полноценной Windows (лежит в папке ProgramData\Nanosoft\RegWizard\Licenses) и скормить его мастеру лицензий.
Ещё я пытался поставить NanoCAD 8.5 под Wine, он в отличии от NC 5.1, требует уже NET Framework 4, а с ним проблем меньше, но зато там я застрял на установке пререквизита LocalDB (видимо серверные компоненты от MS). Если кто-то сможет запустить, поделитесь в комментариях пожалуйста.
Часть III: собираем проект в MonoDevelop
Получив, более-менее работоспособную версию NanoCAD для тестирования нашего кода, сразу же попробуем проверить, удастся ли нам пересобрать и запустить ранее написанную на C# и собранную библиотеку с псевдотрёхмерной дверью в Linux.
Для сборки проекта я выбрал MonoDevelop (для пользователей Windows она может быть знакома под названием Xamarin Studio).
Я раньше с ней в чистом виде не сталкивался (только в составе Unity) и был приятно удивлен, тому, что похоже, все что нам нужно в MonoDevelop есть.
Открываем уже созданный ранее в MS Visual Studio проект и пробуем пересобрать.
Полный код класса, чтобы вам не надо было ничего искать в других статьях, я спрячу под спойлером.
// Version 1.1
//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.dll from Nanocad SDK
//Link System.Windows.Forms and System.Drawing
//upd: for version 1.1 also link .NET API: hostdbmg.dll, hostmgd.dll
//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.
// V 1.0. More detailed - https://habrahabr.ru/post/342680/
// V 1.1. More detailed - https://habrahabr.ru/post/343772/
// 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;
//added in V 1.1. for monitoring
using System.Security.Permissions;
using System.IO;
using Multicad.AplicationServices;
using HostMgd.ApplicationServices;
using HostMgd.EditorInput;
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;
//added in V 1.1. (monitor fileds)
public enum Mon { off, on};
private Mon _monitor = Mon.off;
private string _monFilePath = @"E:\test.txt";
// if it is serialized, you may not be able to copy the object in the CAD editor
[NonSerialized]
private FileSystemWatcher _watcher;
[NonSerialized]
private FileSystemEventHandler _watchHandler;
[CommandMethod("DrawDoor", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawDoor() {
DoorPseudo3D_nc51 door = new DoorPseudo3D_nc51();
door.PlaceObject();
this.TryModify();
// this.Monitor = false;
}
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;
Stat = Status.closed;
// Add the object to the database
DbEntity.AddToCurrentDocument();
// added in v.1.
return hresult.s_Ok;
}
///
/// Method for changing the object's SC (the graph is built at the origin of coordinates).
/// summary>
/// The matrix for changing the position of the object. param>
/// 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(_pnt1, (obj, g, offset) => { obj.TryModify(); obj._pnt1 += offset; }));
return true;
}
//Define the monitoring custom properties , added v. 1.1:
// added in v. 1.1
[DisplayName("Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public Mon Monitor
{
get
{
return _monitor;
}
set
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monitor = value;
if (_monitor==Mon.on)
{
StartMonitoring();
}
else StopMonitoring();
}
}
// added in v. 1.1
[DisplayName("File path for Monitoring")]
[Description("Monitoring of file for door")]
[Category("Monitoring")]
public string MonitoringFilPath
{
get
{
return _monFilePath;
}
set
{
// Get the command line editor
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
//for hot change filename
if (Monitor==Mon.on)
{
StopMonitoring();
if (!TryModify())
return;
_monFilePath = value;
StartMonitoring();
ed.WriteMessage("Monitored file is changed");
}
else
{
//Save Undo state and set the object status to "Changed"
if (!TryModify())
return;
_monFilePath = value;
}
}
}
//Define the methods, added v. 1.1:
// added in v. 1.1
[PermissionSet(SecurityAction.Demand, Name = "FullTrust")]
public void StartMonitoring()
{
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
_watcher = new FileSystemWatcher();
if (File.Exists(_monFilePath))
{
_watcher.Path = Path.GetDirectoryName(_monFilePath);
_watcher.Filter = Path.GetFileName(_monFilePath);
_watchHandler = new FileSystemEventHandler(OnChanged);
_watcher.Changed += _watchHandler;
_watcher.EnableRaisingEvents = true;
}
else ed.WriteMessage("File: " + _monFilePath + " " + "not Exists");
}
// added in v. 1.1
public void StopMonitoring()
{
if (_watcher != null & _watchHandler != null)
{
_watcher.Changed -= _watchHandler;
_watcher.EnableRaisingEvents = false;
}
}
// added in v. 1.1
private void OnChanged(object source, FileSystemEventArgs e)
{
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
ed.WriteMessage("File: " + e.FullPath + " " + e.ChangeType);
//read new value from file
try
{
if (File.Exists(_monFilePath))
{
int mStatus = -1;
ed.WriteMessage("File exists ");
using (StreamReader sr = new StreamReader(new FileStream(_monFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
if (sr.BaseStream.CanRead)
{
if (int.TryParse(sr.ReadLine(), out mStatus))
{
if (Enum.IsDefined(typeof(Status), mStatus))
{
if (!TryModify()) return;
Stat = (Status)mStatus; // преобразование
if (!TryModify()) return;
DbEntity.Update();
McContext.ExecuteCommand("REGENALL");
ed.WriteMessage("Door state is changed");
}
else ed.WriteMessage("Incorrect data in the file. Should be in diapason: 0, 1, 2 ");
}
}
else ed.WriteMessage("Can't read file ");
}
}
else ed.WriteMessage("File not exists ");
_watcher.EnableRaisingEvents = false; // disable tracking
}
finally
{
_watcher.EnableRaisingEvents = true; // reconnect tracking
}
}
}
// 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.
}
Как видите сборка выполнена успешно. Открываем NanoCAD, открываем ранее созданный .dwg файл с дверью и овцами (выложен на GitHub).
Как видите всё открывается. И даже намеренно включённое в код предупреждение об ошибке сделанное на базе Windows Forms открывается корректно (хоть и выглядит криво).
Раз всё работает, давайте попробуем, собрать, что-то новенькое и совсем уж примитивное, чисто в учебных целях. Но для начала подготовим себе инструменты.
В одной из статей я кратко описывал, процесс создания проекта для NanoCAD 5.1 в MS Visual Studio, в этой я предлагаю вам такую же краткую инструкцию, только для MonoDevelop под Linux:
- Открываем IDE и в меню File выбираем пункт создать новое решение (проект).
- В новом окне выбираем тип — библиотека классов C#
- Даем ему какое-нибудь название.
- Подключим библиотеки MultiCAD.NET, искать их надо в папке Mint, в которой организован виртуальный диск нашей системы Wine. SDK как правило ставится в одну папку с NanoCAD, подключаемые библиотеки лежат в папке include. Для этого проекта нам хватит только mapimgd.dll.
- Обязательно снимите галочку с опции «копировать локально».
- Затем кликнув ПКМ по названию проекта в инспекторе объектов выберете пункт options и установите в качестве итоговой платформы .NET Framework 3.5, как на картинке.
- После чего можно писать код и собирать код командой Build (F8). Дальше найдем нашу библиотеку в папке debug проекта и копируем на виртуальный диск «C» или в любое другое место куда имеет доступ Wine, важно чтобы NanoCAD смог вашу .dll увидеть через свой диалог открытия файлов.
Часть IV: рождение пингвина
Рас уж мы с Вами так удачно все настроили, давайте уже что-нибудь напишем. Я решил предложить вам совсем уж простенький код, чтобы точно запустился без проблем.
Под спойлером прячется код, который рисует нам мое «высокохудожественное видение» пингвина. Класс содержит только команду «Dping», которая чертит простые геометрические объекты и выводит текст.
using System.Collections.Generic;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.DatabaseServices.StandardObjects;
namespace nano
{
class penguin
{
[CommandMethod("DPing", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawFace()
{
DbCircle body = new DbCircle();
body.Center = new Point3d(200, 200, 0);
body.Radius = 105;
body.DbEntity.AddToCurrentDocument();
DbCircle eye1 = new DbCircle();
eye1.Center = new Point3d(150, 255, 0);
eye1.Radius = 8;
eye1.DbEntity.AddToCurrentDocument();
DbCircle pupil1 = new DbCircle();
pupil1.Center = new Point3d(148, 254, 0);
pupil1.Radius = 2;
pupil1.DbEntity.AddToCurrentDocument();
DbCircle eye2 = new DbCircle();
eye2.Center = new Point3d(250, 260, 0);
eye2.Radius = 8;
eye2.DbEntity.AddToCurrentDocument();
DbCircle pupil2 = new DbCircle();
pupil2.Center = new Point3d(252, 258, 0);
pupil2.Radius = 2;
pupil2.DbEntity.AddToCurrentDocument();
DbLine hand1 = new DbLine();
hand1.StartPoint = new Point3d(102, 239, 0);
hand1.EndPoint = new Point3d(72, 183, 0);
hand1.DbEntity.AddToCurrentDocument();
DbLine hand2 = new DbLine();
hand2.StartPoint = new Point3d(298, 236, 0);
hand2.EndPoint = new Point3d(325, 192, 0);
hand2.DbEntity.AddToCurrentDocument();
DbPolyline nose = new DbPolyline();
List nosePoints = new List() {
new Point3d(171, 222, 0), new Point3d(198, 177, 0), new Point3d(231, 222, 0) };
nose.Polyline = new Polyline3d(nosePoints);
nose.Polyline.SetClosed(false);
nose.DbEntity.Transform(McDocumentsManager.GetActiveDoc().UCS); //change coordinates from UCS to WCS for BD
nose.DbEntity.AddToCurrentDocument();
DbText spech = new DbText();
spech.Text = new TextGeom("Hello Habr!", new Point3d(310, 55, 0), Vector3d.XAxis, "Standard", 25);
spech.DbEntity.AddToCurrentDocument();
}
}
}
Думаю, что код без особых проблем (ну может с небольшими корректировками подключаемых пространств имен) также скомпилируется и для NC 8.5 если вы подключите библиотеки от его SDK, но протестировать в Linux мы его не сможем, потому, что как я говорил раньше NC 8.5 у меня совсем «не взлетел» под Wine.
Кстати наша скомпилированная библиотека, потом без проблем откроется и в версии NC 5.1 работающей непосредственно под управлением Windows (что в принципе очевидно).
Итак, перекидываем нашу библиотеку на диск «C:», открываем NanoCAD, вводим команду Netload выбираем нашу библиотеку (у меня ping.dll) и наслаждаемся зрелищем.
Как видите под Wine 2.2, по неизвестным причинам, в настройках NanoCAD барахлят опции связанные с назначением цвета (вместо значения цвета — крестик), решить эту задачу я не смог. Но в остальном файлы вроде открывается, диалог печати работает, простые объекты чертятся, а больше я и не тестировал.
Ну и чтобы убедится, что у нас с вами всё совсем «кроссплатформенно», запустим нашу библиотеку с пингвином в NC 5.1 под Windows.
Подведем итоги: похоже, что на первый взгляд непосредственно программировать на C# для NC 5.1 с помощью MultiCAD.NET API в Linux — вполне возможно, а вот тестировать свой код уже не так комфортно.
Я во всех вопросах связанных с Linux почти ничего не понимаю, но мне кажется, что технически возможно создать контейнер, упаковать в него правильную версию Wine, установить туда неактивированный NC 5.1, предоставить каким-то образом контейнеру с Wine доступ к внешним дискам, и будет всем счастье.
К сожалению, самостоятельно я это в обозримом будущем точно не смогу провернуть, так что придется ждать и надеется на опытных неравнодушных пользователей или разработчиков NanoCad.
Но если вдруг однажды у меня самого, что-нибудь получится я обязательно с вами этим поделюсь. Всем успехов и хорошей недели!