Все записи автора Андрей Бушман

Нарезка областей (regions) на прямоугольные куски заданного размера

Задача стояла следующая: необходимо для указанных областей (regions) создать их копию, нарезанную либо на прямоугольники заданного размера, либо на заданное количество строк и столбцов. При этом исходные области (regions), подлежащие разрезке могут быть абсолютно любой, произвольной формы: например, созданными на основании сплайнов и содержащими в себе отверстия.
AutoCAD вообще очень медленно работает с объектами Region, примерно так же медленно, как и с телами (объектами Solid3d). Причём AutoCAD 2014 и 2015 работают с областями и телами даже в два раза медленней, чем более старые версии. Это обусловлено тем, что в обозначенных версиях AutoCAD компанией Autodesk был переписан код по работе с Region и Solid3d. но при этом не было произведено надлежащего тестирования обновлённого функционала.

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

В процессе тестирования производилась разрезка различных областей, в т.ч. и созданных при помощи сплайнов (на скрине выделены коричневым цветом):



Код определяет ряд пользовательских команд:

1. Команда AcadBoundaryRegion создаёт жёлтую рамку вокруг регионов. Эта рамка показывает визуальные границы BoundaryBox для этих примитивов/ Габариты берутся именно те, которые AutoCAD API показывает программистам в свойствах примитивов:


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

2. Команда BoundaryRegion показывает корректные визуальные границы наших областей.


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

3. Команда MeshRegionThroughCellSize нарезает копии наших исходных областей на ячейки, заданного размера:



Можно задавать произвольное значение высоты и ширины ячеек.

4. Команда MeshRegionThroughRowsAndColumnsCount режет копии наших областей на указанное количество строк и столбцов:



Используя команду MeshRegionThroughRowsAndColumnsCount можно нарезать области произвольным образом, например так:



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

Исходный код программы разбит на несколько файлов. Далее приведено их полное содержимое.

Файл PluginEnvironment.cs

/* PluginEnvironment.cs
 * © Андрей Бушман, 2014 
 * Централизованное хранение общей информации, используемой в коде различных
 * методов. Т.о. при необходимости достаточно будет поменять её в одном месте
 * и все изменения автоматически подхватятся.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Bushman.CAD {
  public static class PluginEnvironment {
    /// <summary>
    /// Наименование группы команд AutoCAD, определённых в составе данной 
    /// сборки.
    /// </summary>
    public const String DefaultCommandGroup = "Bushman";
  }
}

Файл ExtensionApplication.cs

/* ExtensionApplication.cs
 * © Андрей Бушман, 2014 
 * Информация, отправляемая в консоль ActiveDocument при загрузке данной 
 * сборки. Если в текущий момент ActiveDocument равен null, то информация будет
 * выведена в консоль первого открытого в дальнейшем документа.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;

#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Wn = Autodesk.AutoCAD.Windows;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif

namespace Bushman.CAD {

  public sealed class ExtensionApplication : Rt.IExtensionApplication {
    #region IExtensionApplication Members

    public void Initialize() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null) {
        cad.DocumentManager.DocumentActivated += DocMng_DocActivated;
        return;
      }
      Db.Database db = doc.Database;
      Ed.Editor ed = doc.Editor;
      WriteAboutInfo(ed);
    }

    public void Terminate() {
    }
    #endregion

    internal static void WriteAboutInfo(Ed.Editor ed) {
      try {
        String title = GetAssemblyAttribute<AssemblyTitleAttribute>(
          a => a.Title);
        String copyright = GetAssemblyAttribute<AssemblyCopyrightAttribute>(
          a => a.Copyright);
        // String version = GetAssemblyAttribute<AssemblyVersionAttribute>(
        //  a => a.Version);
        String description = GetAssemblyAttribute<AssemblyDescriptionAttribute>(
          a => a.Description);

        ed.WriteMessage("n{0}n{1}. {2}n{3}n",
         Assembly.GetExecutingAssembly().Location, title, copyright,
         description);
      }
      catch(Exception ex) {
        ed.WriteMessage("{0}n", ex.Message);
      }
    }

    void DocMng_DocActivated(object sender, Ap.DocumentCollectionEventArgs e) {
      cad.DocumentManager.DocumentActivated -= DocMng_DocActivated;
      WriteAboutInfo(e.Document.Editor);
    }

    internal static String GetAssemblyAttribute<T>(Func<T, String> value)
      where T : Attribute {
      T attribute = (T)Attribute.GetCustomAttribute(
        Assembly.GetExecutingAssembly(), typeof(T));
      return value.Invoke(attribute);
    }
  }
}

Файл About.cs

/* About.cs
 * © Андрей Бушман, 2014 
 * Команда, предоставляющая пользователю общую информацию о библиотеке.
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using BUc = Bushman.CAD;

#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Wn = Autodesk.AutoCAD.Windows;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
#endif

[assembly: Rt.CommandClass(typeof(Bushman.CAD.Commands.About))]

namespace Bushman.CAD.Commands {
  public class About {
    [Rt.CommandMethod(BUc.PluginEnvironment.DefaultCommandGroup, "About",
      Rt.CommandFlags.Modal)]
    public void AboutCommand() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null || doc.IsDisposed)
        return;
      BUc.ExtensionApplication.WriteAboutInfo(doc.Editor);
    }
  }
}

Файл RegionTools.cs

/* RegionTools.cs
 * © Андрей Бушман, 2014 
 * Набор методов, предназначенных для создания разрезанной на части копии 
 * исходной области (region).
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Wn = Autodesk.AutoCAD.Windows;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
#endif

namespace Bushman.CAD.Extensions {

  /// <summary>
  /// Статический класс, предоставляющий дополнительный набор методов для 
  /// работы с объектами Region.
  /// </summary>
  public static class RegionTools {

    static Double dx = 10.0;
    static Double dy = 10.0;

    /// <summary>
    /// Предельно допустимое значение ширины ячейки. Свойство влияет на работу
    /// команды MeshRegionThroughCellSize.
    /// </summary>
    public static Double CellWidth {
      get { return dx; }
      set {
        if(dx <= 0)
          throw new ArgumentException(value.ToString());
        dx = value;
      }
    }

    /// <summary>
    /// Предельно допустимое значение высоты ячейки. Свойство влияет на работу
    /// команды MeshRegionThroughCellSize.
    /// </summary>
    public static Double CellHeight {
      get { return dy; }
      set {
        if(dy <= 0)
          throw new ArgumentException(value.ToString());
        dy = value;
      }
    }

    static Int32 rowsCount = 1;
    static Int32 columnsCount = 1;

    /// <summary>
    /// Количество строк в нарезаемой сетке. Свойство влияет на работу
    /// команды MeshRegionThroughRowsAndColumnsCount.
    /// </summary>
    public static Int32 RowsCount {
      get { return rowsCount; }
      set {
        if(rowsCount <= 0)
          throw new ArgumentException(value.ToString());
        rowsCount = value;
      }
    }

    /// <summary>
    /// Количество колонок в нарезаемой сетке. Свойство влияет на работу
    /// команды MeshRegionThroughRowsAndColumnsCount.
    /// </summary>
    public static Int32 ColumnsCount {
      get { return columnsCount; }
      set {
        if(columnsCount <= 0)
          throw new ArgumentException(value.ToString());
        columnsCount = value;
      }
    }

    // Предельная абсолютная погрешность точности измерения визуальных границ
    // примитива.
    static Double delta = 0.1;

    /// <summary>
    /// Предельная абсолютная погрешность точности измерения визуальных границ
    /// "GeometricExtents" региона. Чем меньше погрешность, тем точнее граница,
    /// но тем дольше она высчитывается. Свойство влияет на работу команды
    /// BoundaryRegion.
    /// </summary>
    public static Double Delta {
      get { return delta; }
      set {
        if(delta <= 0)
          throw new ArgumentException(value.ToString());
        delta = value;
      }
    }

    /// <summary>
    /// Настройка базы данных чертежа: установка нужных единиц измерения, 
    /// системы кооринат и т.п.
    /// </summary>
    /// <param name="db">Целевая база данных</param>
    internal static void SetDatabaseDefaultSettings(Db.Database db) {
      if(db == null)
        throw new ArgumentNullException("db");
      if(db.IsDisposed)
        throw new ArgumentException("db.IsDisposed == true");

      // Устанавливаем текущей метрическую систему измерения и миллиметры в
      // качестве используемых единиц измерения
      db.Measurement = Db.MeasurementValue.Metric;
      db.Insunits = Db.UnitsValue.Millimeters;
      // Устанавливаем текущей мировую систему координат
      db.WorldUcsBaseOrigin(Db.OrthographicView.TopView);
    }

    /// <summary>
    /// Создать прямоугольную область (Region), представляющую собой "ячейку"
    /// нарезаемой сетки.
    /// </summary>
    /// <param name="topLeftCorner">Координата верхнего левого угла "ячейки".
    /// </param>
    /// <param name="dx">Длина "ячейки".</param>
    /// <param name="dy">Высота "ячейки".</param>
    /// <returns>Возвращается новый объект Region. Программист должен либо 
    /// добавить полученный объект в Database, либо по завершению работы с ним
    /// не забыть его уничтожить, вызвав метод Dispose (дабы избежать утечки 
    /// памяти).</returns>
    public static Db.Region GetRegionCell(Gm.Point2d topLeftCorner, Double dx,
      Double dy) {

      #region Проверка входных данных
      if(dx <= 0)
        throw new ArgumentException("dx <= 0");
      if(dy <= 0)
        throw new ArgumentException("dy <= 0");
      #endregion

      Db.Region regCell = null;
      using(Db.Polyline pline = new Db.Polyline()) {
        pline.SetDatabaseDefaults();
        pline.AddVertexAt(0, new Gm.Point2d(topLeftCorner.X, topLeftCorner.Y),
          0, 0, 0);
        pline.AddVertexAt(1, new Gm.Point2d(topLeftCorner.X + dx,
          topLeftCorner.Y), 0, 0, 0);
        pline.AddVertexAt(2, new Gm.Point2d(topLeftCorner.X + dx,
          topLeftCorner.Y - dy), 0, 0, 0);
        pline.AddVertexAt(3, new Gm.Point2d(topLeftCorner.X, topLeftCorner.Y -
          dy), 0, 0, 0);
        pline.Closed = true;

        regCell = Db.Region.CreateFromCurves(new Db.DBObjectCollection() { 
          pline }).Cast<Db.Region>().First();
      }
      return regCell;
    }

    /// <summary>
    /// Получить массив областей (region) путём разреки копии указанной 
    /// области на отдельные прямоугольные (по возможности) части.
    /// </summary>
    /// <param name="region">Область, копия которой будет создана и затем 
    /// разрезана на части.</param>
    /// <param name="delta">Предельная абсолютная погрешность точности 
    /// измерения визуальных границ региона. Чем меньше погрешность, тем точнее
    /// граница, но тем дольше она высчитывается.</param>
    /// <param name="dx">Максимальная длина получаемых сегментов.</param>
    /// <param name="dy">Максимальная высота получаемых сегментов.</param>
    /// <returns>Возвращается массив объектов Region. Программист должен либо 
    /// добавить их в Database, либо по завершению своей работы с ними не 
    /// забыть уничтожить, вызвав для каждого элемента его метод Dispose (дабы
    /// избежать утечки памяти).</returns>
    public static Db.Region[] CloneAndMesh(this Db.Region region, Double delta,
      Double dx, Double dy) {

      #region Проверка входных данных
      if(region == null)
        throw new ArgumentNullException("region");
      if(region.IsDisposed)
        throw new ArgumentException("region.IsDisposed == true");
      if(dx < 0)
        throw new ArgumentException("dx < 0");
      if(dy < 0)
        throw new ArgumentException("dy < 0");
      #endregion

      List<Db.Region> result = new List<Db.Region>();

      Gm.Point2d minPoint = Gm.Point2d.Origin;
      Gm.Point2d maxPoint = Gm.Point2d.Origin;

      region.GetVisualBoundary(delta, ref minPoint, ref maxPoint);

      #region Если исходный регион не превышает ячейку, то резать нечего
      Double length = maxPoint.X - minPoint.X;
      Double height = maxPoint.Y - minPoint.Y;

      // Если исходный регион меньше ячейки нарезаемой сетки, то возвращаем
      // пустой массив, т.к. разрезать нечего
      if(dx >= length && dy >= height)
        return result.ToArray();
      #endregion

      for(Double offsetY = height; offsetY > -dy; offsetY -= dy) {

        Db.Region regionClone = region.Clone() as Db.Region;
        Gm.Point2d topLeftCorner = new Gm.Point2d(minPoint.X,
          minPoint.Y + offsetY);

        Db.Region regionRow = GetRegionCell(topLeftCorner, length, dy);

        regionClone.BooleanOperation(Db.BooleanOperationType.BoolIntersect,
          regionRow);

        // Если значения обоих свойств regionClone.IsNull и regionRow.IsNull
        // равны true, значит пересечение отсутствует

        // Если логическая операция выполнена успешно, то её результат 
        // сохраняем, т.к. его ещё предстоит резать вертикально
        if(!regionClone.IsNull && regionRow.IsNull) {

          // Теперь полученную горионтальную часть режем вертикально
          if(length > dx) {
            Db.Region row = regionClone;
            Double rowLength = maxPoint.X - minPoint.X;

            for(Double offsetX = rowLength - dx; offsetX > -dx;
              offsetX -= dx) {
              Db.Region rowClone = row.Clone() as Db.Region;
              topLeftCorner = new Gm.Point2d(minPoint.X + offsetX,
                minPoint.Y + offsetY);

              Db.Region regionCell = GetRegionCell(topLeftCorner, dx, dy);
              regionCell.LayerId = region.LayerId;

              rowClone.BooleanOperation(
                Db.BooleanOperationType.BoolIntersect, regionCell);

              // Если логическая операция выполнена успешно, то её результат 
              // сохраняем, т.к. его ещё предстоит резать вертикально
              if(!rowClone.IsNull && regionCell.IsNull) {
                result.AddRange(ExplodeIfUnitedRegions(rowClone));

                if(!regionCell.IsDisposed)
                  regionCell.Dispose();
              }
              // Предотвращаем утечку памяти
              else {
                if(!regionCell.IsDisposed)
                  regionCell.Dispose();
                if(!rowClone.IsDisposed)
                  rowClone.Dispose();
              }
            }
            row.Dispose();
          }
          else {
            result.AddRange(ExplodeIfUnitedRegions(regionClone));
          }

          if(!regionRow.IsDisposed)
            regionRow.Dispose();
        }
        // Предотвращаем утечку памяти
        else {
          if(!regionRow.IsDisposed)
            regionRow.Dispose();
          if(!regionClone.IsDisposed)
            regionClone.Dispose();
        }
      }
      return result.ToArray();
    }

    /// <summary>
    /// Метод получает на входе объект Region и проверяет, состоит ли он из
    /// нескольких объединённых областей. Если состоит, то эти области 
    /// извлекаются и записываются в массив объектов Region, после чего объект
    /// исходной области, переданной методу в качестве параметра, уничтожается.
    /// 
    /// Если исходная область не состоит из нескольких объединённых частей, то
    /// объект этой области добавляется в массив объектов Region.
    /// </summary>
    /// <param name="region">Исходная область, подлежащая обработке.</param>
    /// <returns>Возвращается массив областей. Как минимум, в массиве будет 
    /// присутствовать хотя бы один объект.</returns>
    public static Db.Region[] ExplodeIfUnitedRegions(Db.Region region) {

      if(region == null)
        throw new ArgumentNullException("region");
      if(region.IsDisposed)
        throw new ArgumentException("region.IsDisposed == true");

      List<Db.Region> result = new List<Db.Region>();
      using(Br.Brep brep = new Br.Brep(region)) {
        if(brep.Complexes != null && brep.Complexes.Count() > 1) {
          using(Db.DBObjectCollection explodeResult =
            new Db.DBObjectCollection()) {
            region.Explode(explodeResult);
            foreach(Db.DBObject item in explodeResult) {
              Db.Region n = (Db.Region)item;
              n.LayerId = region.LayerId;
              result.Add(n);
            }
          }

          if(!region.IsDisposed)
            region.Dispose();
        }
        else
          result.Add(region);
      }
      return result.ToArray();
    }

    // Код метода GetVisualBoundary был написан Александром Ривилисом здесь:
    // http://adn-cis.org/forum/index.php?topic=495.msg3043#msg3043

    /// <summary>
    /// Получить координаты границ левого нижнего и правого верхнего углов для
    /// визуального "GeometricExtents" региона.
    /// </summary>
    /// <param name="region">Регион, для которого следует получить координаты 
    /// границ визуального "GeometricExtents".</param>
    /// <param name="delta">Предельная арифметическая погрешность вычислений.
    /// </param>
    /// <param name="minPoint">Ссылка на переменную Point2d, в которой следует
    /// сохранить координаты левого нижнего угла визуального 
    /// "GeometricExtents".</param>
    /// <param name="maxPoint">Ссылка на переменную Point2d, в которой следует
    /// сохранить координаты правого верхнего угла визуального 
    /// "GeometricExtents".</param>
    public static void GetVisualBoundary(this Db.Region region, Double delta,
  ref Gm.Point2d minPoint, ref Gm.Point2d maxPoint) {
      using(Gm.BoundBlock3d boundBlk = new Gm.BoundBlock3d()) {
        using(Br.Brep brep = new Br.Brep(region)) {
          foreach(Br.Edge edge in brep.Edges) {
            using(Gm.Curve3d curve = edge.Curve) {
              Gm.ExternalCurve3d curve3d = curve as Gm.ExternalCurve3d;
              // Делать точный расчет нужно только если образующая - сплайн
              // в противном случае достаточно получить BoundBlock
              if(curve3d != null && curve3d.IsNurbCurve) {
                using(Gm.NurbCurve3d nurbCurve = curve3d.NativeCurve
                  as Gm.NurbCurve3d) {
                  Gm.Interval interval = nurbCurve.GetInterval();
                  for(double par = interval.LowerBound;
                    par <= interval.UpperBound; par += (delta * 2.0)) {
                    Gm.Point3d p = nurbCurve.EvaluatePoint(par);
                    if(!boundBlk.IsBox)
                      boundBlk.Set(p, p);
                    else
                      boundBlk.Extend(p);
                  }
                }
              }
              else {
                if(!boundBlk.IsBox) {
                  boundBlk.Set(edge.BoundBlock.GetMinimumPoint(),
                    edge.BoundBlock.GetMaximumPoint());
                }
                else {
                  boundBlk.Extend(edge.BoundBlock.GetMinimumPoint());
                  boundBlk.Extend(edge.BoundBlock.GetMaximumPoint());
                }
              }
            }
          }
        }
        // Возвращаем вычисленный результат
        minPoint = new Gm.Point2d(boundBlk.GetMinimumPoint().X,
          boundBlk.GetMinimumPoint().Y);
        maxPoint = new Gm.Point2d(boundBlk.GetMaximumPoint().X,
          boundBlk.GetMaximumPoint().Y);
      }
    }
  }
}

Файл RegionCommands.cs

/* RegionCommands.cs
 * © Андрей Бушман, 2014 
 * Дополнительные команды CAD для работы с объектами областей (region).
 */
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using BUc = Bushman.CAD;
using BUx = Bushman.CAD.Extensions;
// для возможности использования методов расширений
using Bushman.CAD.Extensions;

#if AUTOCAD
using cad = Autodesk.AutoCAD.ApplicationServices.Application;
using Ap = Autodesk.AutoCAD.ApplicationServices;
using Db = Autodesk.AutoCAD.DatabaseServices;
using Ed = Autodesk.AutoCAD.EditorInput;
using Rt = Autodesk.AutoCAD.Runtime;
using Gm = Autodesk.AutoCAD.Geometry;
using Wn = Autodesk.AutoCAD.Windows;
using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
using Br = Autodesk.AutoCAD.BoundaryRepresentation;
#endif

[assembly: Rt.CommandClass(typeof(Bushman.CAD.Commands.RegionCommands))]

namespace Bushman.CAD.Commands {

  public sealed class RegionCommands {

    /// <summary>
    /// Команда CAD, при помощи которой формируется "нарезка" копий указанных
    /// областей (Regions) на прямоугольные (по возможности) части, не 
    /// превышающие размеров dx и dy, заданных пользователем.
    /// </summary>
    [Rt.CommandMethod(Bushman.CAD.PluginEnvironment.DefaultCommandGroup,
      "MeshRegionThroughCellSize", Rt.CommandFlags.Modal)]
    public void MeshRegionThroughCellSize() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null)
        return;

      Db.Database db = doc.Database;
      BUx.RegionTools.SetDatabaseDefaultSettings(db);
      Ed.Editor ed = doc.Editor;

      Db.TypedValue[] tv = new Db.TypedValue[1];
      tv[0] = new Db.TypedValue((Int32)Db.DxfCode.Start, "REGION");
      Ed.SelectionFilter filter = new Ed.SelectionFilter(tv);
      Ed.PromptSelectionOptions pso = new Ed.PromptSelectionOptions();
      pso.MessageForAdding = "Выберите области (Regions) для добавления " +
        "их в набор";
      pso.MessageForRemoval =
        "Выберите области (Regions), подлежащие удалению из набора";
      pso.SingleOnly = false;
      pso.RejectObjectsFromNonCurrentSpace = true;
      pso.AllowDuplicates = false;

      Ed.PromptSelectionResult psr = ed.GetSelection(pso, filter);

      if(psr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Ничего не выбрано. Выполнение команды прерваноn");
        return;
      }
      else {
        ed.WriteMessage("nВсего выбрано областей (Regions): {0}n",
          psr.Value.Count);
      }
      Db.ObjectId[] ids = psr.Value.GetObjectIds();
      Ed.PromptDoubleOptions pdo = new Ed.PromptDoubleOptions("dx");
      pdo.AllowNegative = false;
      pdo.AllowZero = false;
      pdo.AllowNone = false;
      pdo.DefaultValue = RegionTools.CellWidth;
      pdo.UseDefaultValue = true;

      Ed.PromptDoubleResult pdr = ed.GetDouble(pdo);
      if(pdr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.CellWidth = pdr.Value;

      ed.WriteMessage(Environment.NewLine);

      pdo.Message = "dy";
      pdo.DefaultValue = RegionTools.CellHeight;
      pdr = ed.GetDouble(pdo);
      if(pdr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.CellHeight = pdr.Value;

      pdo = new Ed.PromptDoubleOptions(
        "Предельная абсолютная погрешность");
      pdo.AllowNegative = false;
      pdo.AllowZero = false;
      pdo.AllowNone = false;
      pdo.DefaultValue = RegionTools.Delta;
      pdo.UseDefaultValue = true;

      pdr = ed.GetDouble(pdo);

      if(pdr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.Delta = pdr.Value;

      ed.WriteMessage(Environment.NewLine);

      using(Db.Transaction tr = db.TransactionManager.StartTransaction()) {

        Db.BlockTable bt = (Db.BlockTable)tr.GetObject(db.BlockTableId,
         Db.OpenMode.ForRead);
        Db.BlockTableRecord btr = (Db.BlockTableRecord)tr.GetObject(bt[
         Db.BlockTableRecord.ModelSpace], Db.OpenMode.ForWrite);

        DateTime start = DateTime.Now;
        Int32 count = 0;
        foreach(Db.ObjectId id in ids) {
          Db.Region region = tr.GetObject(id, Db.OpenMode.ForRead)
            as Db.Region;
          Db.Region[] regions = region.CloneAndMesh(RegionTools.Delta,
            RegionTools.CellWidth, RegionTools.CellHeight);
          count += regions.Length;
          foreach(Db.Region item in regions) {
            btr.AppendEntity(item);
            tr.AddNewlyCreatedDBObject(item, true);
          }
        }
        tr.Commit();

        DateTime end = DateTime.Now;
        ed.WriteMessage(
          "nСоздано областей (region): {0}nЗатраченное время: {1}n",
         count, end - start);
      }
    }

    /// <summary>
    /// Команда CAD, при помощи которой формируется "нарезка" копий указанных
    /// областей (Regions) на прямоугольные (по возможности) части согласно 
    /// указанному количеству строк и столбцов.
    /// </summary>
    [Rt.CommandMethod(Bushman.CAD.PluginEnvironment.DefaultCommandGroup,
      "MeshRegionThroughRowsAndColumnsCount", Rt.CommandFlags.Modal)]
    public void MeshRegionThroughRowsAndColumnsCount() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null)
        return;

      Db.Database db = doc.Database;
      BUx.RegionTools.SetDatabaseDefaultSettings(db);
      Ed.Editor ed = doc.Editor;

      Db.TypedValue[] tv = new Db.TypedValue[1];
      tv[0] = new Db.TypedValue((Int32)Db.DxfCode.Start, "REGION");
      Ed.SelectionFilter filter = new Ed.SelectionFilter(tv);
      Ed.PromptSelectionOptions pso = new Ed.PromptSelectionOptions();
      pso.MessageForAdding = "Выберите области (Regions) для добавления " +
        "их в набор";
      pso.MessageForRemoval =
        "Выберите области (Regions), подлежащие удалению из набора";
      pso.SingleOnly = false;
      pso.RejectObjectsFromNonCurrentSpace = true;
      pso.AllowDuplicates = false;

      Ed.PromptSelectionResult psr = ed.GetSelection(pso, filter);

      if(psr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Ничего не выбрано. Выполнение команды прерваноn");
        return;
      }
      else {
        ed.WriteMessage("nВсего выбрано областей (Regions): {0}n",
          psr.Value.Count);
      }
      Db.ObjectId[] ids = psr.Value.GetObjectIds();
      Ed.PromptIntegerOptions pio = new Ed.PromptIntegerOptions(
        "Количество столбцов");
      pio.AllowNegative = false;
      pio.AllowZero = false;
      pio.AllowNone = false;
      pio.DefaultValue = RegionTools.ColumnsCount;
      pio.UseDefaultValue = true;

      Ed.PromptIntegerResult pir = ed.GetInteger(pio);
      if(pir.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.ColumnsCount = pir.Value;

      ed.WriteMessage(Environment.NewLine);

      pio.Message = "Количество строк";
      pio.DefaultValue = RegionTools.RowsCount;
      pir = ed.GetInteger(pio);
      if(pir.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.RowsCount = pir.Value;

      if(RegionTools.ColumnsCount != 1 || RegionTools.RowsCount != 1) {
        Ed.PromptDoubleOptions pdo = new Ed.PromptDoubleOptions(
  "Предельная абсолютная погрешность");
        pdo.AllowNegative = false;
        pdo.AllowZero = false;
        pdo.AllowNone = false;
        pdo.DefaultValue = RegionTools.Delta;
        pdo.UseDefaultValue = true;

        Ed.PromptDoubleResult pdr = ed.GetDouble(pdo);

        if(pdr.Status != Ed.PromptStatus.OK) {
          ed.WriteMessage("Выполнение команды прерваноn");
          return;
        }
        RegionTools.Delta = pdr.Value;

        ed.WriteMessage(Environment.NewLine);

        using(Db.Transaction tr = db.TransactionManager.StartTransaction()) {

          Db.BlockTable bt = (Db.BlockTable)tr.GetObject(db.BlockTableId,
           Db.OpenMode.ForRead);
          Db.BlockTableRecord btr = (Db.BlockTableRecord)tr.GetObject(bt[
           Db.BlockTableRecord.ModelSpace], Db.OpenMode.ForWrite);

          DateTime start = DateTime.Now;
          Int32 count = 0;
          foreach(Db.ObjectId id in ids) {
            Db.Region region = tr.GetObject(id, Db.OpenMode.ForRead)
              as Db.Region;

            Gm.Point2d minPoint = Gm.Point2d.Origin;
            Gm.Point2d maxPoint = Gm.Point2d.Origin;

            region.GetVisualBoundary(RegionTools.Delta, ref minPoint,
              ref maxPoint);

            Double width = (maxPoint.X - minPoint.X) / RegionTools.ColumnsCount;
            Double height = (maxPoint.Y - minPoint.Y) / RegionTools.RowsCount;

            Db.Region[] regions = region.CloneAndMesh(RegionTools.Delta, width,
              height);
            count += regions.Length;
            foreach(Db.Region item in regions) {
              btr.AppendEntity(item);
              tr.AddNewlyCreatedDBObject(item, true);
            }
          }
          tr.Commit();

          DateTime end = DateTime.Now;
          ed.WriteMessage(
            "nСоздано областей (region): {0}nЗатраченное время: {1}n",
           count, end - start);
        }
      }
      else {
        ed.WriteMessage("nЕсли количество строк и столбцов одновременно " +
          "равны 1, то создавать нечего.n");
      }
    }

    /// <summary>
    /// Получение значения визуальных границ регионов (аналог реализации от
    /// AutoCAD по умолчанию). В случаях, когда контуры областей выполнены
    /// сплайнами, команда BoundaryRegion покажет более точные границы, чем 
    /// команда AcadBoundaryRegion. Назначение AcadBoundaryRegion в том, чтобы
    /// визуально показать программисту границы контейнера области. Это может
    /// помочь понять, почему в некоторых случаях программный код даёт не те 
    /// результаты, которые ожидались на выходе.
    /// </summary>
    [Rt.CommandMethod(Bushman.CAD.PluginEnvironment.DefaultCommandGroup,
      "AcadBoundaryRegion", Rt.CommandFlags.Modal)]
    public void AcadBoundaryRegion() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null)
        return;

      Db.Database db = doc.Database;
      BUx.RegionTools.SetDatabaseDefaultSettings(db);
      Ed.Editor ed = doc.Editor;

      Db.TypedValue[] tv = new Db.TypedValue[1];
      tv[0] = new Db.TypedValue((Int32)Db.DxfCode.Start, "REGION");
      Ed.SelectionFilter filter = new Ed.SelectionFilter(tv);
      Ed.PromptSelectionOptions pso = new Ed.PromptSelectionOptions();
      pso.MessageForAdding = "Выберите области (Regions) для добавления " +
        "их в набор";
      pso.MessageForRemoval =
        "Выберите области (Regions), подлежащие удалению из набора";
      pso.SingleOnly = false;
      pso.RejectObjectsFromNonCurrentSpace = true;
      pso.AllowDuplicates = false;

      Ed.PromptSelectionResult psr = ed.GetSelection(pso, filter);

      if(psr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Ничего не выбрано. Выполнение команды прерваноn");
        return;
      }
      else {
        ed.WriteMessage("nВсего выбрано областей (Regions): {0}n",
          psr.Value.Count);
      }
      Db.ObjectId[] ids = psr.Value.GetObjectIds();

      ed.WriteMessage(Environment.NewLine);

      using(Db.Transaction tr = db.TransactionManager.StartTransaction()) {

        Db.BlockTable bt = (Db.BlockTable)tr.GetObject(db.BlockTableId,
         Db.OpenMode.ForRead);
        Db.BlockTableRecord btr = (Db.BlockTableRecord)tr.GetObject(bt[
         Db.BlockTableRecord.ModelSpace], Db.OpenMode.ForWrite);

        foreach(Db.ObjectId id in ids) {
          Db.Region region = tr.GetObject(id, Db.OpenMode.ForRead)
            as Db.Region;

          using(Br.Brep brep = new Br.Brep(region)) {
            Gm.Point3d origin = new Gm.Point3d(0, 0, 0);
            Gm.Vector3d normal = new Gm.Vector3d(0, 0, 1);
            Gm.Plane plane = new Gm.Plane(origin, normal);

            Gm.BoundBlock3d bb = brep.BoundBlock;

            Gm.Point2d minPoint = Gm.Point2d.Origin;
            Gm.Point2d maxPoint = Gm.Point2d.Origin;

            Double minX = Double.MaxValue;
            Double minY = Double.MaxValue;

            Double maxX = Double.MinValue;
            Double maxY = Double.MinValue;

            if(brep.Edges != null) {
              foreach(Br.Edge edge in brep.Edges) {

                Gm.Point3d min = bb.GetMinimumPoint();
                Gm.Point3d max = bb.GetMaximumPoint();

                if(min.X < minX)
                  minX = min.X;

                if(min.Y < minY)
                  minY = min.Y;

                if(max.X > maxX)
                  maxX = max.X;

                if(max.Y > maxY)
                  maxY = max.Y;
              }
              minPoint = new Gm.Point2d(minX, minY);
              maxPoint = new Gm.Point2d(maxX, maxY);
            }
            else {
              minPoint = bb.GetMinimumPoint().Convert2d(plane);
              maxPoint = bb.GetMaximumPoint().Convert2d(plane);
            }


            Db.Polyline pline = new Db.Polyline(4);
            pline.SetDatabaseDefaults();

            Gm.Point2d[] points = new Gm.Point2d[]{
              new Gm.Point2d(minPoint.X,maxPoint.Y),
              new Gm.Point2d(maxPoint.X,maxPoint.Y),
              new Gm.Point2d(maxPoint.X,minPoint.Y),
              new Gm.Point2d(minPoint.X, minPoint.Y)};

            for(Int32 i = 0; i < points.Length; i++)
              pline.AddVertexAt(i, points[i], 0, 0, 0);

            pline.Closed = true;
            pline.ColorIndex = 50;

            btr.AppendEntity(pline);
            tr.AddNewlyCreatedDBObject(pline, true);
          }
        }
        tr.Commit();
      }
    }

    /// <summary>
    /// Получение значения визуальных границ регионов. В случаях, когда контуры
    /// областей выполнены сплайнами, данная команда покажет более точные 
    /// границы, чем команда AcadBoundaryRegion. Данная команда предназначена
    /// для демонстрирования работы статического метода 
    /// RegionTools.GetVisualBoundary(...).
    /// </summary>
    [Rt.CommandMethod(Bushman.CAD.PluginEnvironment.DefaultCommandGroup,
  "BoundaryRegion", Rt.CommandFlags.Modal)]
    public void BoundaryRegion() {
      Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
      if(doc == null)
        return;

      Db.Database db = doc.Database;
      BUx.RegionTools.SetDatabaseDefaultSettings(db);
      Ed.Editor ed = doc.Editor;

      Db.TypedValue[] tv = new Db.TypedValue[1];
      tv[0] = new Db.TypedValue((Int32)Db.DxfCode.Start, "REGION");
      Ed.SelectionFilter filter = new Ed.SelectionFilter(tv);
      Ed.PromptSelectionOptions pso = new Ed.PromptSelectionOptions();
      pso.MessageForAdding = "Выберите области (Regions) для добавления " +
        "их в набор";
      pso.MessageForRemoval =
        "Выберите области (Regions), подлежащие удалению из набора";
      pso.SingleOnly = false;
      pso.RejectObjectsFromNonCurrentSpace = true;
      pso.AllowDuplicates = false;

      Ed.PromptSelectionResult psr = ed.GetSelection(pso, filter);

      if(psr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Ничего не выбрано. Выполнение команды прерваноn");
        return;
      }
      else {
        ed.WriteMessage("nВсего выбрано областей (Regions): {0}n",
          psr.Value.Count);
      }
      Db.ObjectId[] ids = psr.Value.GetObjectIds();

      ed.WriteMessage(Environment.NewLine);

      Ed.PromptDoubleOptions pdo = new Ed.PromptDoubleOptions(
        "Предельная абсолютная погрешность");
      pdo.AllowNegative = false;
      pdo.AllowZero = false;
      pdo.AllowNone = false;
      pdo.DefaultValue = RegionTools.Delta;
      pdo.UseDefaultValue = true;

      Ed.PromptDoubleResult pdr = ed.GetDouble(pdo);
      if(pdr.Status != Ed.PromptStatus.OK) {
        ed.WriteMessage("Выполнение команды прерваноn");
        return;
      }
      RegionTools.Delta = pdr.Value;

      using(Db.Transaction tr = db.TransactionManager.StartTransaction()) {

        Db.BlockTable bt = (Db.BlockTable)tr.GetObject(db.BlockTableId,
         Db.OpenMode.ForRead);
        Db.BlockTableRecord btr = (Db.BlockTableRecord)tr.GetObject(bt[
         Db.BlockTableRecord.ModelSpace], Db.OpenMode.ForWrite);

        Int32 count = 0;
        DateTime start = DateTime.Now;

        foreach(Db.ObjectId id in ids) {
          Db.Region region = tr.GetObject(id, Db.OpenMode.ForRead)
            as Db.Region;
          using(Br.Brep brep = new Br.Brep(region)) {
            Gm.Point3d origin = new Gm.Point3d(0, 0, 0);
            Gm.Vector3d normal = new Gm.Vector3d(0, 0, 1);
            Gm.Plane plane = new Gm.Plane(origin, normal);

            Gm.Point2d minPoint = Gm.Point2d.Origin;
            Gm.Point2d maxPoint = Gm.Point2d.Origin;

            region.GetVisualBoundary(RegionTools.Delta, ref minPoint,
              ref maxPoint);

            Db.Polyline pline = new Db.Polyline(4);
            pline.SetDatabaseDefaults();

            Gm.Point2d[] points = new Gm.Point2d[]{
              new Gm.Point2d(minPoint.X,maxPoint.Y),
              new Gm.Point2d(maxPoint.X,maxPoint.Y),
              new Gm.Point2d(maxPoint.X,minPoint.Y),
              new Gm.Point2d(minPoint.X, minPoint.Y)};

            for(Int32 i = 0; i < points.Length; i++)
              pline.AddVertexAt(i, points[i], 0, 0, 0);

            pline.Closed = true;
            pline.ColorIndex = 80;

            btr.AppendEntity(pline);
            tr.AddNewlyCreatedDBObject(pline, true);
            ++count;
          }
        }
        tr.Commit();
        DateTime end = DateTime.Now;
        ed.WriteMessage("nСоздано границ: {0}nЗатраченное время: {1}n",
         count, end - start);
      }
    }
  }
}

Выполнение логических операций с областями (regions)

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

Комментировать тут особо не чего, поэтому сразу код:

   1:  /* RegionBooleanOperationsSample.cs
   2:   * © Andrey Bushman, 2014 
   3:   */
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:   
   8:  #if AUTOCAD
   9:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  10:  using Ap = Autodesk.AutoCAD.ApplicationServices;
  11:  using Db = Autodesk.AutoCAD.DatabaseServices;
  12:  using Ed = Autodesk.AutoCAD.EditorInput;
  13:  using Rt = Autodesk.AutoCAD.Runtime;
  14:  using Gm = Autodesk.AutoCAD.Geometry;
  15:  using Wn = Autodesk.AutoCAD.Windows;
  16:  using Hs = Autodesk.AutoCAD.DatabaseServices.HostApplicationServices;
  17:  using Us = Autodesk.AutoCAD.DatabaseServices.SymbolUtilityServices;
  18:  #endif
  19:   
  20:  [assembly: Rt.CommandClass(typeof(Bushman.CAD.Experiences
  21:    .RegionBooleanOperationsSample))]
  22:   
  23:  namespace Bushman.CAD.Experiences {
  24:    public class RegionBooleanOperationsSample {
  25:      const String cmdGroup = "Bushman";
  26:      [Rt.CommandMethod(cmdGroup, "CreateRegions", Rt.CommandFlags.Modal)]
  27:      public void CreateRegions() {
  28:        Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
  29:        if(doc == null || doc.IsDisposed)
  30:          return;
  31:        using(doc.LockDocument()) {
  32:          Ed.Editor ed = doc.Editor;
  33:          Db.Database db = doc.Database;
  34:   
  35:          Gm.Point2d[] points_1 = new Gm.Point2d[]{
  36:            new Gm.Point2d(0,0),
  37:            new Gm.Point2d(0,70),
  38:            new Gm.Point2d(10,70),
  39:            new Gm.Point2d(10,10),
  40:            new Gm.Point2d(40,10),
  41:            new Gm.Point2d(40,70),
  42:            new Gm.Point2d(50,70),
  43:            new Gm.Point2d(50,10),
  44:            new Gm.Point2d(80,10),
  45:            new Gm.Point2d(80,70),
  46:            new Gm.Point2d(90,70),
  47:            new Gm.Point2d(90,0)
  48:          };
  49:          Db.Region region_1 = CreateRegion(points_1, 10);
  50:   
  51:          Gm.Point2d[] points_2 = new Gm.Point2d[]{          
  52:            new Gm.Point2d(0,70),
  53:            new Gm.Point2d(90,70),
  54:            new Gm.Point2d(90,60),
  55:            new Gm.Point2d(0,60)
  56:          };
  57:          Db.Region region_2 = CreateRegion(points_2, 50);
  58:   
  59:          using(Db.Transaction tr = db.TransactionManager.StartTransaction()) {
  60:            Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForRead)
  61:              as Db.BlockTable;
  62:            Db.BlockTableRecord ms = tr.GetObject(bt[Db.BlockTableRecord
  63:              .ModelSpace], Db.OpenMode.ForWrite) as Db.BlockTableRecord;
  64:            ms.AppendEntity(region_1);
  65:            tr.AddNewlyCreatedDBObject(region_1, true);
  66:            ms.AppendEntity(region_2);
  67:            tr.AddNewlyCreatedDBObject(region_2, true);
  68:   
  69:            Db.DBText text = new Db.DBText();
  70:            text.SetDatabaseDefaults();
  71:            text.Height = 5;
  72:            text.TextString = "Base regions";
  73:            text.Position = new Gm.Point3d(0, 80, 0);
  74:            ms.AppendEntity(text);
  75:            tr.AddNewlyCreatedDBObject(text, true);
  76:   
  77:            Int32 dx = 120;
  78:            Int32 x = dx;
  79:   
  80:            foreach(String name in Enum.GetNames(typeof(Db.BooleanOperationType)
  81:              )) {
  82:              Db.Region region_1Clone = region_1.Clone() as Db.Region;
  83:              Db.Region region_2Clone = region_2.Clone() as Db.Region;
  84:              Db.DBText textClone = text.Clone() as Db.DBText;
  85:              textClone.TextString = String.Format("The {0} operation", name);
  86:   
  87:              Gm.Point3d acPt3d = new Gm.Point3d(0, 0, 0);
  88:              Gm.Vector3d acVec3d = acPt3d.GetVectorTo(new Gm.Point3d(x, 0,
  89:                0));
  90:   
  91:              region_1Clone.TransformBy(Gm.Matrix3d.Displacement(acVec3d));
  92:              region_2Clone.TransformBy(Gm.Matrix3d.Displacement(acVec3d));
  93:              textClone.TransformBy(Gm.Matrix3d.Displacement(acVec3d));
  94:   
  95:              region_2Clone.BooleanOperation((Db.BooleanOperationType)Enum
  96:                .Parse(typeof(Db.BooleanOperationType), name), region_1Clone);
  97:   
  98:              if(!region_2Clone.IsNull && region_1Clone.IsNull)
  99:                region_2Clone.ColorIndex = 80;
 100:   
 101:              ms.AppendEntity(region_1Clone);
 102:              tr.AddNewlyCreatedDBObject(region_1Clone, true);
 103:              ms.AppendEntity(region_2Clone);
 104:              tr.AddNewlyCreatedDBObject(region_2Clone, true);
 105:              ms.AppendEntity(textClone);
 106:              tr.AddNewlyCreatedDBObject(textClone, true);
 107:   
 108:              x += dx;
 109:            }
 110:            tr.Commit();
 111:          }
 112:        }
 113:        doc.SendStringToExecute("_ZOOM _E ", true, false, false);
 114:      }
 115:   
 116:      internal Db.Region CreateRegion(Gm.Point2d[] points, Int32 colorIndex) {
 117:        if(points == null)
 118:          throw new ArgumentNullException("points");
 119:        if(points.Length == 0)
 120:          throw new ArgumentException("points.Length == 0");
 121:        using(Db.Polyline pline = new Db.Polyline(12)) {
 122:          pline.SetDatabaseDefaults();
 123:          for(int i = 0; i < points.Length; i++)
 124:            pline.AddVertexAt(i, points[i], 0, 0, 0);
 125:          pline.Closed = true;
 126:          Db.Region region = Db.Region.CreateFromCurves(
 127:            new Db.DBObjectCollection { pline }).Cast<Db.Region>().First();
 128:          region.SetDatabaseDefaults();
 129:          region.ColorIndex = colorIndex;
 130:          return region;
 131:        }
 132:      }
 133:    }
 134:  }

Результат выглядит следующим образом:



InternalsVisibleTo и Intellysience

Порой наши библиотеки могут содержать некоторый функционал, который мы активно используем в составе сборки, но который мы бы не хотели делать общедоступным. Такие методы и классы, как правило, объявляются с модификатором доступа internal. Если программист желает разрешить доступ к этому функционалу из внешних сборок, то в целевой сборке он помечает их как дружественные, при помощи атрибута сборки InternalsVisibleTo. Однако может случиться так, что в дружественной сборке Intellysience откажется показывать то, что в целевой было объявлено с модификатором internal. При этом компиляция проектов по прежнему будет проходить благополучно.

Например, программист может писать тесты для своего плагина AutoCAD, разместив код этих тестов в отдельном проекте. В этом случае, дабы не объявлять весь тестируемый функционал как public, он может быть объявлен как internal, после чего в коде плагина добавляется соответствующий атрибут сборки:

   1:  #if DEBUG
   2:  // Объявляем сборку acad_plugin_tests дружественной
   3:  [assembly: InternalsVisibleTo("acad_plugin_tests")] 
   4:  #endif

Т.о. проект acad_plugin_tests содержащий тесты, объявляется дружественным для отладочного варианта нашей сборки, что даёт нам возможность добраться до функционала, подлежащего тестированию. При компиляции Release версии сборка acad_plugin_tests уже не будет объявлена дружественной.

Однако, если вы вдруг обнаружите, что в процессе написания кода в дружественной сборке не отображаются элементы, помеченные в целевой как internal:


то для решения этой проблемы следует удалить *.suo файлы, находящиеся в каталоге нашего решения, рядом с *.sln файлом:


Как видим, теперь в коде дружественной сборки Intellysience отображает в т.ч. и internal элементы, дополнительно помечая их "сердечком".

Git & Unicode

Начиная с версии V1.7.10 Git for Windows поддерживает кодировку Unicode.

Имена файлов и каталогов, выполненные кирилицей, в Git for Windows могут отображаться, например так:


Чтобы это исправить, следует в консоли Git установить используемым шрифт TrueType, например Lucida Console или Consolas:


И произвести ряд изменений в настройках Git, посредством запуска команд:

git config --global core.quotepath off
git config --global --unset i18n.logoutputencoding
git config --global --unset i18n.commitencoding
git config --global --unset svn.pathnameencoding

Результат выглядит следующим образом:



Источник информации здесь.

Англоязычный вариант книги Pro Git здесь (pdf формат).
Русский перевод книги Pro Git здесь (pdf формат).

P.S. Если вам потребовалось в Git for Windows для операции git commit назначить иной текстовый редактор, путь к exe файлу которого содержит пробелы - в этом случае, при указании полного пути к exe файлу, следует внутри кавычек одного типа, дополнительно указывать кавычки другого типа. Т.е. либо одинарные кавычки упаковывать в двойные, либо наоборот - двойные кавычки упаковывать в одинарные. В общем, в подобных случаях следует использовать любой из следующих вариантов:

git config --global core.editor "'C:Program FilesNotepad++notepad++.exe'"
git config --global core.editor '"C:Program FilesNotepad++notepad++.exe"'


О совместимости версий .NET плагинов, AutoCAD, .NET Framework и MS Visual Studio

Маленькая шпаргалка на тему совместимости версий .NET плагинов, AutoCAD, .NET Framework и MS Visual Studio. Вообще, Visual Studio можно использовать любую: главное, чтобы она поддерживала возможность писать код под нужную нам версию .NET Framework. Разные версии AutoCAD могут использовать разные версии .NET Framework и плагины, использующие эти платформы. Кроме того, некоторые управляемые плагины можно использовать в разных версиях AutoCAD без необходимости их перекомпиляции.

[R17.2] AutoCAD 2009: .NET 3.0, 3.5; Visual Studio 2005 и более новые.
[R18.0] AutoCAD 2010: .NET 3.5; Visual Studio 2005 и более новые.
[R18.1] AutoCAD 2011: .NET 3.5, 4.0, 4.5, 4.5.1 (см. примечание 1); Visual Studio 2005 и более новые.
[R18.2] AutoCAD 2012: .NET 4.0, 4.5, 4.5.1 (см. примечание 2); Visual Studio 2010 и более новые.
[R19.0] AutoCAD 2013: .NET 4.0, 4.5, 4.5.1 (см. примечание 2); Visual Studio 2010 и более новые.
[R19.1] AutoCAD 2014: .NET 4.0, 4.5, 4.5.1 (см. примечание 2); Visual Studio 2010 и более новые.
[R20.0] AutoCAD 2015: .NET 4.5, 4.5.1 (см. примечание 2); Visual Studio 2012 и более новые.

Особым образом хочется выделить AutoCAD 2010 и 2011. Официально компанией Autodesk заявлено, что эти версии требуют для своей работы Microsoft Visual Studio 2008 with Service Pack 1 и Microsoft .NET Framework 3.5 with Service Pack 1. Однако практика показывает, что если у программиста под рукой окажется только Visual Studio 2005 и AutoCAD 2011, то написать плагин он сможет. Но в виду того, что в Visual Studio 2005 нет возможности указать целевую версию платформы .NET, этот плагин будет компилироваться с использованием .NET 2.0. Соответственно, при написании плагина AutoCAD, программист будет ограничен рамками платформы .NET 2.0. Например, он не сможет использовать технологии LINQ и WPF.

Для примера, в Visual Studio 2005 автор написал и скомпилировал плагин для AutoCAD 2011, который успешно запустился в обозначенной версии AutoCAD:

   1:  using System;
   2:  using cad = Autodesk.AutoCAD.ApplicationServices
   3:  .Application;
   4:  using Rt = Autodesk.AutoCAD.Runtime;
   5:   
   6:  namespace acad_2011{
   7:      public class Class1 {
   8:          [Rt.CommandMethod("test", Rt.CommandFlags.Modal)]
   9:          public void Test(){
  10:              cad.DocumentManager.MdiActiveDocument
  11:                  .Editor.WriteMessage(
  12:                  "MS VS 2005: Hello, AutoCAD 2011!n");
  13:          }
  14:      }
  15:  }

В подкаталоге inс установленного ObjectARX SDK, управляемые сборки появились лишь начиная с AutoCAD 2011. .NET плагины, использующие сборки AutoCAD только из этого каталога, можно без опасений компилировать как AnyCPU. В иных случаях нужно быть осторожным. Начиная с AutoCAD 2013 в обозначенном подкаталоге появился файл AcCoreMgd.dll. В новый файл было вынесено часть функционала из файла AcDbMgd.dll и часть из файла AcMgd.dll.

Небольшая переведённая мною на русский язык цитата, автором которой является Scott McFarlane [CP2654]:
В последние несколько лет компания Autodesk работала над разделением бизнес логики AutoCAD от логики взаимодействия с GUI. Такое разделение позволяет иметь единую кодовую базу, не зависящую от конкретной операционной системы (Windows, MacOS). Это разделение было названо "большим разделением" (The Big Split). Указанная работа была завершена в AutoCAD 2013 и теперь мы имеем трёх уровневую архитектуру, как показано ниже:




Важно понимать, что каждый уровень зависит от ниже расположенных уровней и не зависит от уровней расположенных выше. Два нижних уровня представляют собой механизм ядра AutoCAD (AutoCAD Core Engine). В виду высокой скорости своего запуска, помимо использования в пакетной обработке чертежей, приложение AcCoreConsole так же хорошо подходит и для проведения автоматического тестирования кода.

Теперь программный код рекомендуется писать разделяя его зависящую от AutoCAD API часть от той части, которая не зависит от AutoCAD API. Затем, часть кода, зависящую от AutiCAD API в свою очередь рекомендуется делить на две части: на ту, которая зависит лишь от базового уровня (библиотеки acdbmgd.dll и accoremgd.dll) и на ту, которая зависит от UI (acmgd.dll).

Начиная с AutoCAD 2013 SP1 был внедрён механизм безопасности загрузки в AutoCAD сторонних программных библиотек (подробнее здесь). Начиная с AutoCAD 2014, на вкладке Files диалогового окна Options присутствует ветка Trusted Locations.

Если вы написали управляемый плагин под одну версию AutoCAD, то сможете использовать его и для другой версии AutoCAD, в случае выполнения следующих условий:
1. Обе версии AutoCAD могут использовать ту версию .NET Framework, которая применялась при написании плагина (см. примечание 1).
2. К вашему плагину AutoCAD подключены библиотеки, присутствующие в обоих версиях AutoCAD.
3. Ваш плагин использует только ту часть API, которая присутствует в обоих рассматриваемых версиях AutoCAD.
4. Если управляемый плагин скомпилирован с опцией x86 или x64 (например, по причине использования Interop в вашем коде), то и целевые версии AutoCAD должны иметь ту же самую разрядность, что и плагин.

ВНИМАНИЕ!
Даже если выполнены все обозначенные выше условия, всё равно следует обязательно произвести тестирование (желательно с использованием Gallio) вашего плагина на полную его совместимость с иными, интересующими вас версиями AutoCAD (т.е. загрузить плагин и проверить его работоспособность).

Примечание 1:
В AutoCAD 2011 конфигурационный файл acad.exe.config содержит закомментированные XML элементы, указывающие целевую версию .NET Framework:

   1:  <!--<startup useLegacyV2RuntimeActivationPolicy="true">
   2:  <supportedRuntime version="v4.0"/>
   3:  </startup>-->

В этом состоянии AutoCAD 2011 использует .NET 3.5, но если указанные выше XML элементы раскомментировать, то AutoCAD 2011 будет использовать наиболее новую версию .NET из тех которые приложение сможет найти и использовать. Применительно к AutoCAD 2011 это будут версии .NET 4.0, 4.5 и 4.5.1.

Примечание 2:
В отличие от AutoCAD 2011, в конфигурационных файлах AutoCAD 2012-2015 обозначенные в "Примечании 1" XML элементы по умолчанию раскомментированы.

Примечание 3:
Если вместо acad.exe используется accoreconsole.exe, то его конфигурационный файл, соответственно, следует искать под именем accoreconsole.exe.config.

Баг BlockTableRecord.HasAttributeDefinitions.

На форуме adn-cis.org недавно появилось сообщение о том, что BlockTableRecord.HasAttributeDefinitions возвращает неверное значение для тех записей, из которых определения атрибутов на самом деле были ранее удалены. Наличие данного бага было подтверждено и заявка отправлена в ADN. Соответственно, придётся писать свою, корректную реализацию данного функционала.

По обозначенной выше ссылке проблема обозначалась следующим образом (отредактированная цитата):
1. Создаем определение блока c одним произвольным объектом (например с полилинией).
2. Добавляем определение атрибута в наше 
определение блока.
3. Видим, что свойство BlockTableRecord.HasAttributeDefinitions равно true, как и полагается.
4. Итерацией проходим по объектам нашей BlockTableRecord и видим, что их два: полилиния и определение атрибута.
5. Теперь удаляем ранее добавленное определение атрибута.
6.Итерацией проходим по объектам нашей BlockTableRecord и видим лишь один объект (как и полагается): полилинию.
7. Теперь смотрим свойство BlockTableRecord.HasAttributeDefinitions и видим, что оно равно true вместо ожидаемого false.
Свою версию обозначенного выше функционала реализуем в виде метода расширения, которому присвоим имя HasAttDefs:

   1:  // BlockTableRecordExtentions.cs
   2:  // © Andrey Bushman, 2014
   3:  // Extention methods for the BlockTableRecord class
   4:  using System;
   5:  using System.Collections.Generic;
   6:  using System.Linq;
   7:  using System.Text;
   8:   
   9:  /// В целях переносимости кода и отсутствия в псевдонимах наименований
  10:  /// конкретного САПР, целесообразней формировать псевдонимы в 
  11:  /// нейтральной форме, например: cad, вместо acad, Ap вместо AcAp, Db 
  12:  /// вместо AcDb и т.д. Построенная таким способом система наименований
  13:  /// будет более удобной программисту, портирующему ваш код под другую 
  14:  /// САПР. Ниже приведён вариант определений таких нейтральных псевдонимов
  15:  /// под некоторый набор различных САПР.
  16:  #if AUTOCAD
  17:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  18:  using Ap = Autodesk.AutoCAD.ApplicationServices;
  19:  using Db = Autodesk.AutoCAD.DatabaseServices;
  20:  using Ed = Autodesk.AutoCAD.EditorInput;
  21:  using Gm = Autodesk.AutoCAD.Geometry;
  22:  using Rt = Autodesk.AutoCAD.Runtime;
  23:  #elif BRICSCAD
  24:  using cad = Bricscad.ApplicationServices.Application;
  25:  using Ap = Bricscad.ApplicationServices;
  26:  using Db = Teigha.DatabaseServices;
  27:  using Ed = Bricscad.EditorInput;
  28:  using Gm = Teigha.Geometry;
  29:  using Rt = Bricscad.Runtime;
  30:  #elif NANOCAD
  31:  using cad = HostMgd.ApplicationServices.Application;
  32:  using Ap = HostMgd.ApplicationServices;
  33:  using Db = Teigha.DatabaseServices;
  34:  using Ed = HostMgd.EditorInput;
  35:  using Gm = Teigha.Geometry;
  36:  using Rt = Teigha.Runtime; 
  37:  #endif
  38:   
  39:  namespace Bushman.CAD.Extentions {
  40:      /// <summary>
  41:      /// Методы расширения для экземпляров класса BlockTableRecord
  42:      /// </summary>
  43:      public static class BlockTableRecordExtentions {
  44:          /// <summary>
  45:          /// Данный метод проверяет наличие экземпляров <c>Db.AttributeDefinition</c> в
  46:          /// составе объекта <c>BlockTableRecord</c> и представляет собой замену методу
  47:          /// <c>BlockTableRecord.HasAttributeDefinitions</c>, который реализован 
  48:          /// неверно - в виду этого и возникла необходимость написать корректный вариант
  49:          /// реализации. Информация о некорректной работе 
  50:          /// <c>BlockTableRecord.HasAttributeDefinitions</c> была подтверждена и
  51:          /// отправлена в ADN. Подробности на странице 
  52:          /// http://adn-cis.org/forum/index.php?topic=625.0
  53:          /// </summary>
  54:          /// <param name="btr">экземпляр <c>BlockTableRecord</c>, подлежащий проверке.</param>
  55:          /// <returns>true - в составе указанного объекта <c>BlockTableRecord</c>
  56:          /// содержатся элементы <c>Db.AttributeDefinition</c>, иначе - false.</returns>
  57:          public static Boolean HasAttDefs(this Db.BlockTableRecord btr) {
  58:              String name = Rt.RXClass.GetClass(typeof(Db.AttributeDefinition)).Name;
  59:              return btr.Cast<Db.ObjectId>().Any(n => !n.IsNull && n.IsValid
  60:                  && !n.IsErased && !n.IsEffectivelyErased && String.Equals(
  61:                  n.ObjectClass.Name, name, StringComparison.InvariantCulture));
  62:          }
  63:   
  64:  #if DEBUG
  65:          /// <summary>
  66:          /// Команда, демонструрующая некорректную работу 
  67:          /// <c>BlockTableRecord.HasAttributeDefinitions</c>
  68:          /// и корректность работы метода <c>HasAttDefs</c>.
  69:          /// За основу взят код 
  70:          /// http://adn-cis.org/forum/index.php?topic=625.msg2168#msg2168
  71:          /// </summary>
  72:          [Rt.CommandMethod("TestBlock")]
  73:          public static void TestBlock() {
  74:              Ap.Document doc = cad.DocumentManager.MdiActiveDocument;
  75:              Ed.Editor ed = doc.Editor;
  76:              Ed.PromptResult res = ed.GetString("nType name of block: ");
  77:              if (res.Status != Ed.PromptStatus.OK) return;
  78:              using (Db.Transaction tr = doc.TransactionManager.StartTransaction()) {
  79:                  Db.BlockTable bt = tr.GetObject(doc.Database.BlockTableId,
  80:                      Db.OpenMode.ForRead) as Db.BlockTable;
  81:                  if (bt != null) {
  82:                      if (bt.Has(res.StringResult)) {
  83:                          Db.BlockTableRecord btr = tr.GetObject(bt[res.StringResult],
  84:                              Db.OpenMode.ForRead) as Db.BlockTableRecord;
  85:                          if (btr != null) {
  86:                              ed.WriteMessage("nAutodesk: Block {0} has{1} attribute definitions.n",
  87:                                  res.StringResult, btr.HasAttributeDefinitions ? "" : " not");
  88:                              // Мною добавлена эта строка кода:
  89:                              ed.WriteMessage("nBushman: Block {0} has{1} attribute definitions.n",
  90:                                  res.StringResult, btr.HasAttDefs() ? "" : " not");
  91:                          }
  92:                      }
  93:                      else {
  94:                          ed.WriteMessage("nBlock {0} not found", res.StringResult);
  95:                      }
  96:                  }
  97:                  tr.Commit();
  98:              }
  99:          }
 100:  #endif
 101:      }
 102:  }

Последовательно выполняем все шаги, демонстрирующие появление проблемы и запускаем команду TestBlock:


Как видим - наша реализация работает корректно.

tlbimp.exe и целевая версия .NET Framework

ObjectARX SDK содержит некоторый набор TLB файлов. Для некоторых из них приходится генерировать управляемую обёртку "вручную" (причина указана ниже), посредством утилиты tlbimp.exe (если нужная обёртка изначально отсутствует). Обозначенная утилита содержит некоторый набор ключей для конфигурирования конечного результата. Однако одной, очень важной настройки, всё же не хватает...

На моём рабочем компьютере установлены MS Visual Studio 2005-2012 и .NET Framework 2.0 - 4.5. В первый раз я столкнулся с необходимостью прибегать к "ручному" использованию утилиты tlbimp.exe, когда в MS Visual Studio 2012 был создан проект плагина AutoCAD 2010 (целевая платформа .NET 3.5, разрядность x64), но при этом мне не далось подключить ссылки на acax18ENU.tlb и axdb18enu.tlb. (на самом деле именно эти tlb файлы нет никакой нужды подключать, т.к. вместо них нужно подключать библиотеки Interop, но в данном случае на их примере я показываю проблему, которая может возникнуть при использовании утилиты tlbimp.exe). "За кулисами", при подключении TLB файлов IDE обычно для них автоматически генерирует управляемые обёртки, но в данном случае этого не произошло:


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

Та же самая ситуация для AutoCAD 2010 наблюдалась и в MS Visual Studio 2005-2010. В виду этого приходится генерировать DLL обёртку при помощи "ручного" запуска утилиты tlbimp.exe. Однако и тут не всё так красиво, как хотелось бы: к сожалению, в утилите tlbimp.exe нет возможности указать целевую версию платформы .NET, для которой следует генерировать обёртку.
    Я пытался сгенерировать обёртку для нужных мне TLB файлов из консолей разных версий MS Visual Studio следующим образом:

    tlbimp "C:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"C:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64

    tlbimp "C:SDKAutodeskAutoCADObjectARX 2010inc-x64axdb18enu.tlb" /out:"C:SDKAutodeskAutoCADObjectARX 2010inc-x64axdb18enu.Interop.dll" /namespace:AXDBLib /machine:x64

    Примечание:
    При подключении этих оболочек к проекту, необходимо их свойство Copy Local устанавливать в TRUE, поскольку они сгенерированы нами и будут отсутствовать в каталоге AutoCAD.

    Если использовать консоль VS 2010 или VS 2012, то обёртка будет создана для .NET 4.0. Однако в AutoCAD 2010 использует .NET 3.5, поэтому данный результат мне не подходит...

    Получить нужный результат мне удалось лишь воспользовавшись консолью VS 2008.

    На моей машинке, в составе установленных SDK

    утилиту tlbimp.exe мне удалось найти в каталогах:
    • C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools
    • C:Program Files (x86)Microsoft SDKsWindowsv8.0AbinNETFX 4.0 Tools
    • C:Program Files (x86)Microsoft SDKsWindowsv7.0ABin
    Кроме того, утилита была найдена и в каталоге
    • C:Program Files (x86)Microsoft Visual Studio 8SDKv2.0Bin
    В обозначенных выше первых трёх каталогах, я пробовал создавать конфигурационный файл tlbimp.exe.config следующего содержимого:

    <?xml version="1.0" encoding="utf-8"?>
       <startup>
       <supportedRuntime version="v2.0.50727"/>
    </startup>


    Затем из cmd.exe, перейдя в каталог указанной утилиты, запустил такую команду:

    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>tlbimp
     "D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64
    Не удалось запустить приложение, поскольку его параллельная конфигурация неправильна. Дополнительные сведения содержатся в журнале событий приложений или используйте программу командной строки sxstrace.exe для получения дополнительных сведений.


    Обозначенный выше результат наблюдался для версий v8.1A и v8.0A. Для версии v7.0A в перенастройке не было необходимости, т.к. на выходе и так получается библиотека для .Net 2.0.
    Если удалить созданный мною конфигурационный файл, то для v8.1A и v8.0A операция так же проходит успешно, но на выходе, конечно же, получаем библиотеку для .Net 4.0:

    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>
    C:Program Files (x86)Microsoft SDKsWindowsv8.1AbinNETFX 4.5.1 Tools>tlbimp
     "D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.tlb" /out:"D:SDKAutodeskAutoCADObjectARX 2010inc-x64acax18ENU.Interop.dll" /namespace:AutoCAD /machine:x64
    Microsoft (R) .NET Framework Type Library to Assembly Converter 4.0.30319.33440
    Copyright (C) Microsoft Corporation.  All rights reserved.

    TlbImp : warning TI3019 : Interface 'IAcadShadowDisplay' is marked as [dual], but does not derive from IDispatch. It will be converted as an IUnknown-derived interface.
    TlbImp : Type library imported to D:SDKAutodeskAutoCADObjectARX 2010inc-x64
    acax18ENU.Interop.dll


    Вывод:
    Если необходимо сгенерировать управляемые DLL обёртки на основе файлов TLB, то для разных целевых версий .NET следует использовать утилиту tlbimp.exe из разных Windows SDK:
    • SDK v7.0A - для .NET 2.0, 3.0, 3.5 SP1
    • SDK v8.1A или v8.0A - для .NET 4.0, 4.5, 4.5.1

    Об управляемых "заглушках" из ObjectARX SDK и целевой платформе (x86x64)

    Как известно, управляемые "заглушки" находятся в ObjectARX SDK в подкаталогах inc, inc-win32 и inc-x64. В идеальном варианте, хотелось бы всегда компилировать код как AnyCPU, однако не во всех случаях это возможно и порой приходится создавать сборки отдельно для x86 и отдельно для x64.

    В ObjectARX SDK 2009 каталог inc не содержит управляемых "заглушек" - они там появились лишь начиная с версии 2011. До этого же их версии находились по подкаталогам inc-win32 и inc-x64. Изначально, базовый набор "заглушек", подключаемых в управляемом плагине AutoCAD состоял из файлов AcDbMgd.dll и AcMgd.dll. Позднее, начиная с версии 2013, к этому перечню добавился файл AcCoreMgd.dll.

    Согласно заявлению сотрудников Autodesk - в том случае, если в управляемом плагине подключается "заглушка" не из каталога inc, то такой плагин следует компилировать не как AnyCPU, но отдельно под x86 и отдельно под x64при этом переподключая "заглушки" из соответствующего каталога - для каждой платформы свои "заглушки". Т.е. если "заглушки" подключаются из inc-win32, то и компилировать такой код следует только для x86. А ежели нужно компилировать для x64, то в подключенных ссылках нужно переуказывать заглушки из каталога inc-x64, вместо заглушек из inc-win32. В том же случае, если в плагине используются лишь заглушки каталога inc, то его можно компилировать как AnyCPU.

    Порой нарушение этого правила может и сойти с рук: например проект, в котором подключены "заглушки" из inc-x64, может быть откомпилирован как x86, после чего успешно работать без каких либо проблем. Или же, как вариант: разработчиком могут быть подключены заглушки не из каталога inc, с последующей компиляцией сборки как AnyCPU. Однако компания Autodesk (в лице Stephen Preston) предупреждает, что в ряде случаев (какие именно - не уточнялось), при подобном подходе могут возникать проблемы.

    Разработка приложений: виртуальные машинки и удалённая отладка

    Для того, чтобы на компьютер "с нуля" установить всё необходимое для работы программное обеспечение, нередко требуется много времени. При этом не имеет значения, физическая это машинка или же виртуальная.

    Описание проблемы 
    Для того, чтобы настроить машинку на разработку и тестирование плагинов AutoCAD версий 2009, 2010, 2011, 2012, 2013, 2014, нужно не только строго в обозначенном порядке установить их, но так же установить и соответствующие для них версии SDK. Так же необходимо установить и MS Visual Studio. В том же случае, если нужно писать плагины не только на .NET но и на C++, ситуация ещё более осложняется, поскольку и IDE придётся ставить так же несколько: для неуправляемого кода C++ каждой версии AutoCAD соответствует своя версия MS Visual Studio.
    Помимо этого, порой приходится устанавливать немалый набор дополнительного ПО: препочитаемый браузер, пакет MS Office, архиватор, программу работающую с виртуальными образами, Adobe Reader, антивирус и т.д. и т.п.



    Конечно же, гораздо удобней всё это один раз установить на виртуальную машинку и затем сделать её рабочую копию. Т.о. если текущая копия придёт в негодность, то достаточно быстро и просто можно создать новую на основе ранее созданной заготовки. Однако тут есть одно "НО"... Но что, если потребуется разрабатывать ПО не для AutoCAD, а к примеру, для Revit? Нужно ли устанавливать Revit на эту же виртуальную машинку (возможно даже несколько версий Revit), или же по аналогии создавать новую? А что, если дополнительно потребуется разрабатывать и тестировать код для nanoCAD, BricsCAD, а так же писать что-то своё, используя библиотеки ODA?

    Не стоит забывать и о том, что разрабатываемый нами код, по хорошему, следовало бы тестировать не только на Windows 7 x64, но и на Windows 7 x86, а так же, возможно, что и на Windows XP x86. Это означает, что придётся создавать такие машинки и устанавливать на них все программы, необходимые для тестирования нашего кода.

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

    Общая идея

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

    На машинку, предназначенную для разработки, устанавливаются только набор нужных IDE, SDK, а так же необходимый разработчику, дополнительный набор инструментов и программных библиотек. При этом сами же целевые приложения, для которых ведётся разработка, на эту машину не устанавливаются. Это означает, что на машинку разработчика не устанавливаются ни AutoCAD, ни Revit, ни nanoCAD, ни BricsCAD и т.д.

    Для тестирования и отладки же создаются и настраиваются отдельные виртуальные машинки, работающие под разными, интересующими нас операционными системами. Затем на эти машинки устанавливается набор нужных  программ, например, AutoCAD 2009 - 2014. Можно, при желании, на эту же машинку устанавливать и Revit, nanoCAD, BricsCAD и т.д., однако я всё же предпочитаю этого не делать и создавать для них отдельные виртуальные машинки, по аналогии, как это было показано для AutoCAD.



    Т.о. получается, что каждая такая машинка ориенирована на тестирование и отладку различных версий конкретного приложения. Сама же по себе отладка и тестирование выполняется удалённо, посредством установленной на клиентской машине нужной версии Remote Debugger.



    Да, безусловно, на создание таких виртуальных машинок придётся один раз потратить некоторое время. Однако, всё не так страшно, как может показаться на первый взгляд. Ниже приводится пример того, как я делал это для себя.

    Примечание
    В первый раз, обозначенная выше идея была мною реализована ещё пару-тройку лет назад, когда мой компьютер работал под управлением операционной системы Windows 7 x64, а для работы с виртуальными машинками использовался VMware. Результат был так же успешен. Около года назад (может чуть больше), я полностью перешёл дома на Linux, а вместо VMware решил использовать VirtualBox.

    Пример реализации

    Итак, мой домашний компьютер работает под управлением операционной системы Linux. Дополнительно установлен VirtualBox (для работы с виртуальными машинками).

    Собирая свой домашний физический компьютер (несколько лет назад), я заранее ориентировался на стиль разработки, который описывается мною в этой заметке: активное использование виртуальных машинок и выполнение на них удалённой отладки. Этим и обусловлены конечные параметры:
    • процессор Core i7
    • объём оперативной памяти - 24 Gb
    • пять жёстких дисков по 1Tb каждый, из них три внешних (для виртуальных машинок целиком отведён один из дисков, а для резервного копирования - ещё один)
    • видеокарта NVIDIA GeForce GTX 570

    Примечание
    К счастью, на сегодняшний день, цены на подобные составляющие являются вполне приемлемыми. 

    В результате, одновременная работа, к примеру, четырёх виртуальных машин не вызывает никаких проблем: всё работает на удивление шустро. Более того, они работают ощутимо быстрее, чем мой физический компьютер на работе. Обычно, при разработке под AutoCAD, у меня одновременно запущено сразу несколько виртуальных машинок одна та, что с IDE, а дополнительные - это машинки win7x64_acads_(work), win7x86_acads_(work) и winXPx86_acads_(work) с установленным софтом, под который в данный момент я пишу код. По мере необходимости, в настройках проекта я указываю ту или иную машинку, на которой желаю в данный момент выполнять удалённую отладку.

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

    Итак, поехали...

    Под виртуальные машинки я зарезервировал отдельный жёсткий диск, создав на нём каталог vm. В этом каталоге разместил два подкаталога:
    • templates
    • work
    Каталог templates предназначен для хранения готовых, настроенных машинок-шаблонов, на основании которых следует создавать рабочие копии (т.е. те машинки, которые непосредственно используются при разработке и тестировании). Каталог work содержит непосредственно рабочие машинки.

    Каталог templates содержит подкаталоги:
    • base
    • develop
    • hosts
    В каталоге base созданы базовые виртуальные машинки, с установленными операционными системами, платформами .NET Framework и всеми обновлениями. Более из софта на них не установлено ничего. Итак созданы:
    • win7x64
    • win7x86
    • winXPx86
    Создав эти три машинки, я сделал их копии в подкаталоге hosts и на каждую последовательно установил AutoCAD 2009 - 2014 со всеми пакетами обновлений:
    • win7x64_acads
    • win7x86_acads
    • winXPx86_acads

      Примечание:
      Далее по тексту, словосочетания "на базе", "на основе" означают, что делается копия исходной машинки и все операции выполняются с этой копией. Если иное не оговорено отдельно, то создание копий выполняется средствами VirtualBox. При этом включена опция генерации для копии нового mac-адреса. Однако будут указаны случаи, когда следует копирование выполнять обычным способом (Ctrl + C, Ctrl +V) - эти случаи будут обозначены отдельно.

      Затем, на базе машинки win7x64, в подкаталоге develop был создан шаблон для разработки:
      • win7x64_develop
      На эту машинку последовательно установлены MS Visual Studio 2005 - 2013 со всеми пакетами обновлений. Никаких дополнительных программ, а так же пакетов SDK не ставилось.


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

      В каталоге work, на основе машинки win7x64_develop была создана более детализированная рабочая машинка, предназначенная для непосредственного использования (т.е. это уже не шаблон):
      • win7_dev
      Деталлизирована она в следующем:
      • Установлен антивирус Kaspersky.
      • Установлен архиватор 7zip.
      • Создан и подключен дополнительный виртуальный диск D.
      • На диске D созданы подкаталоги: sdk, src, test, public.
      • В подкаталог D:sdkAutodeskAutoCAD установил все ObjectARX SDK по подкаталогам AutoCAD 2009, ... 2014.
      • Последовательно установил все Wizards.
      • В каталоге D:src создал подкаталоги vs2005, ... vs2013 для отдельного хранения проектов каждой IDE.
      • В настройках каждой IDE прописал путь, указанный выше, для хранения её проектов.
      • Каталог public сделал доступным для чтениязаписи всем компьютерам в домашней группе.
      Каталог D:test предназначен для тестирования, при котором нет необходимости подключаться к удалённым машинкам. Хранение проектов и SDK на отдельном виртуальном диске позволяет отключать его, в случае необходимости и подключать к другому компьютеру.



      Далее, в каталоге work были созданы так же рабочие копии машинок win7x64_acads, win7x86_acads и winXPx86_acads:
      • win7x64_acads_(work)
      • win7x86_acads_(work)
      • winXPx86_acads_(work)
      На эти машинки дополнительно установлен антивирус Kaspersky. Копии этих виртуальных машин были сделаны не средствами VirtualBox, а обычным копированием (Ctrl + C, Ctrl + V) - иначе у AutoCAD может слететь лицензия (в частности это происходит с AutoCAD 2009). Вообще, после копирования машинок средствами VirtualBox приходится на полученной копии выполнять повторную активацию Windows, но на мой взгляд - это не проблема, а если для кого-то проблема, то Ctrl + C, Ctrl + V в помощь.

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

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

      В результате, как это ни странно, мои виртуальные машинки работают ощутимо быстрее, чем мой физический компьютер на работе. Работать достаточно комфортно. Отладочная сборка при компиляции размещается в "расшаренном" (shared) каталоге удалённой машинки, на которой планируется выполнение отладки:

      Меняя в настройках проекта имя целевой удалённой машины мы, тем самым, быстро меняем условия нашего тестирования (версия ОС, её разрядность, целевое приложение, например AutoCAD или nanoCAD и т.п.):





      Если нужно, то указываем логин и пароль для удалённого подключения:


      А вот, собственно, и отладка:


      Маленькое пояснение: в заголовке отладочной машинки мы видим имя win7x64_acads(work), а в настройках проекта и в строке монитора удалённой отладки видим имя win7x64. Это различие объясняется просто: в каталоге work я создал дополнительную копию на базе шаблона win7x64_acads, воспользовавшись обычным Ctrl + C, Ctrl + V. Переименовывать машинку и диск не стал (поленился) - вместо этого подключив её в VirtualBox, переименовал ярлык на win7x64_acads(work). Т.о. в заголовке окна виртуальной машинки мы видим имя переименованного ярлыка. А настоящее имя тестовой машинки указано в настройках проекта и в мониторе удалённой отладки. Конечно, лучше было бы их переименовать в соответствии с ярлыком. Надеюсь, что в отличие от меня вы именно так и сделаете, не поленившись :).


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

      AutoCAD .NET API & IDisposable

      В данной заметке речь пойдёт об использовании IDisposable в AutoCAD .NET API. Если не освобождать ресурсы, то будет происходить утечка памяти, которая может привести к серьёзным проблемам. Дабы этого не происходило, нужно понимать, для каких объектов и когда следует вызывать Dispose(), а так же для каких объектов этого делать категорически нельзя.

      Для начала несколько ссылок, откуда черпалась информация:
        1. Cleaning up after yourself: how and when to dispose of AutoCAD objects in .NET
        2. Calling Dispose() on AutoCAD objects
        3. Examples of calling Dispose() on AutoCAD objects
        В AutoCAD класс DBObject реализовывает интерфейс IDisposable. Соответственно, указанный интерфейс реализуется и всеми классами, унаследованными от DBObject. Этот же интерфейс реализован и классами Document и Database, однако для Document  вызывать Dispose() никогда не следует (это делает сам AutoCAD), а так же нельзя вызывать Dispose() для объекта, хранящегося в свойстве Document.Database.

        "Вручную" вызывать Dispose() следует в следующих случаях:
        1. Для временных объектов, которые никогда не будут добавлены в базу данных чертежа. Например, это могут быть нерезидентные объекты, предоставленные через Autodesk.AutoCAD.Geometry.
        2. Для временных объектов, которые потенциально могут быть добавлены в базу данных чертежа, но тем не менее не были в неё добавлены.
        3. Если экземпляр класса Database был вами создан способом "new Database(...)" без открытия документа. При работе с созданным подобным образом объектом Database, необходимо не забывать предварительно инициализировать им свойство HostApplicationServices.WorkingDatabase, иначе можно получить исключение eWrongDatabase. По завершению работы с объектом, следует возвращать свойству HostApplicationServices.WorkingDatabase предыдущее значение.
        Внимание!
        Некоторые ресурсы, используемые экземплярами различных классов, могут быть разделяемыми, т.е. одним и тем же ресурсом пользуется сразу несколько разных объектов. К сожалению, в AutoCAD .NET API подобные ситуации могут привести к Fatal Error, если будет произведено освобождение такого общего ресурса в то время, как им продолжают пользоваться другие объекты. 

        Примечание 1
        Скорее всего, возникновение обозначенной выше проблемы с освобождением совместно используемых ресурсов обусловлено плохо написанным кодом разработчиков Autodesk: по-нормальному, для каждого совместно используемого ресурса должен вестись свой счётчик ссылок. В коде метода Dispose() значение счётчика должно сначала декрементироваться (т.е. уменьшаться на 1) и затем проверяться на равенство 0. Если равенство не выполняется, значит ресурс используется кем-то ещё - в этом случае общий ресурс не освобождается. Но если текущее значение счётчика равно 0, то ресурс можно смело освобождать. 

        Учитывая возможные проблемы при освобождении совместно используемых ресурсов, в подобных ситуациях компания Autodesk рекомендует пользоваться "методом научного тыка"  (см. вторую из обозначенных выше ссылок): если вдруг, добавив очередной вызов Dispose() вы обнаруживаете возникновение Fatal Error, то попробуйте закомментировать этот вызов. Если ошибка исчезла, значит в данном случае вызов Dispose() выполнять не следует (логично). Всех мест в API, где наблюдается проблема освобождения совместно используемых ресурсов, не знает даже Autodesk. Поэтому данные места вы порой будете определять экспериментально, методом проб и ошибок (к сожалению).

        Ресурсы всех объектов, полученных при помощи объекта Transaction, посредством метода GetObject(), а так же ресурсы новых объектов, добавленных в базу данных чертежа посредством метода AddNewlyCreatedDBObject(), будут освобождены автоматически в коде метода Dispose() этого объекта транзакции. Т.е. для них не нужно вызывать Dispose() персонально.

        Настоятельно не рекомендую инициализировать объект Transaction следующим образом:

           1:  // db - экземпляр Database
           2:  Transaction tr = db.TransactionManager.StartTransaction();
           3:  ...
           4:  tr.Commit();
           5:  tr.Dispose();

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

        Дабы избежать обозначенной выше опасности, следует инициализацию объекта Transaction выполнять так:

           1:  // db - экземпляр Database
           2:  using(Transaction tr = db.TransactionManager.StartTransaction()){
           3:      ...
           4:      tr.Commit();
           5:  }

        При таком способе, Dispose() гарантированно будет вызван при выходе из блока using, как для объекта транзакции, так и для всех объектов, работающих в её контексте, даже если произойдёт необработанное исключение

        Примечание 2
        Использовать блок using следует и для инициализации объектов, созданных вне контекста транзакции, поскольку для них метод Dispose() вызывать необходимо вручную.