среда, 25 декабря 2013 г.

AutoCAD, фильтры выбора, LINQ и методы расширения

Возможности SelectionFilter, предоставляемого AutoCAD .NET API достаточно ограничены. Далеко не любое условие выборки можно сформировать с его помощью. Например, недавно некоторым нашим сотрудникам потребовалась команда, которая бы в текущем пространстве (Model\Layout) выбирала любые полилинии, имеющие указанное в запросе количество вершин. Такую выборку с помощью фильтров AutoCAD сделать не удастся.

Дело в том, что в отличие от класса Poliline, имеющего свойство NumberOfVertices, классы Polyline2d и Polyline3d подобного свойства не имеют, а доступ к их вершинам осуществляется посредством интерфейса IEnumerable

Т. о. при помощи SelectionFilter мы можем, под наше условие, создать фильтр для Poliline но, к сожалению, никак не для Polyline2d или Polyline3d.

Сам по себе код, выполняющий обозначенную выше задачу является небольшим:

   1:  // SelectionCommands.cs
   2:  // Выбор всех полилиний с указанным количеством вершин. © Andrey Bushman, 2013
   3:   
   4:  //Microsoft
   5:  using System;
   6:  using System.Collections.Generic;
   7:  using System.Linq;
   8:   
   9:  //Autodesk
  10:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  11:  using App = Autodesk.AutoCAD.ApplicationServices;
  12:  using Db = Autodesk.AutoCAD.DatabaseServices;
  13:  using Ed = Autodesk.AutoCAD.EditorInput;
  14:  using Rtm = Autodesk.AutoCAD.Runtime;
  15:   
  16:  // Bushman
  17:  using Bushman.CAD.DatabaseServices;
  18:  using System.Collections;
  19:   
  20:  [assembly: Rtm.ExtensionApplication(typeof(Bushman.CAD.Commands.SelectionCommands))]
  21:  [assembly: Rtm.CommandClass(typeof(Bushman.CAD.Commands.SelectionCommands))]
  22:   
  23:  namespace Bushman.CAD.Commands {
  24:   
  25:      public sealed class SelectionCommands : Rtm.IExtensionApplication {
  26:   
  27:          Int32 numberOfVertices = 0;
  28:   
  29:          #region Commands
  30:   
  31:          /// <summary>
  32:          /// Команда для выбора всех полилиний (2D и 3D) с указанным количеством вершин 
  33:          /// в текущем пространстве (Model\Layout).
  34:          /// </summary>
  35:          [Rtm.CommandMethod("PlineSel", Rtm.CommandFlags.Modal)]
  36:          public void PolylineSelectionViaVertexCount() {
  37:              App.Document doc = cad.DocumentManager.MdiActiveDocument;
  38:              Db.Database db = doc.Database;
  39:              Ed.Editor ed = doc.Editor;
  40:   
  41:              Ed.PromptIntegerOptions intOpt = new Ed.PromptIntegerOptions(
  42:                  "\nКоличество вершин в полилиниях, подлежащих выборке");
  43:              intOpt.AllowNegative = false;
  44:              intOpt.AllowNone = false;
  45:              intOpt.AllowZero = false;
  46:   
  47:              Ed.PromptIntegerResult intRes = ed.GetInteger(intOpt);
  48:   
  49:              if (intRes.Status != Ed.PromptStatus.OK) {
  50:                  ed.WriteMessage("\nНе было выбрано ни одного примитива.\n");
  51:                  return;
  52:              }
  53:   
  54:              numberOfVertices = intRes.Value;
  55:   
  56:              // Получаем идентификаторы объектов, соответствующих фильтру PlinesWithSomeVertexFilter:
  57:              Db.ObjectId[] selectionIds = db.GetData<Db.ObjectId>(PlinesWithSomeVertexFilter, (t, x) => x);
  58:   
  59:              ed.WriteMessage("\nКоличество выбранных примитивов: {0}\n", selectionIds.Length);
  60:   
  61:              // Результаты выборки подсвечиваю с отображением "ручек":
  62:              ed.SetImpliedSelection(selectionIds);
  63:          }
  64:   
  65:          #endregion
  66:   
  67:          /// <summary>
  68:          /// Фильтр для выборки любых полилиний с количеством вершин, указанном в переменной
  69:          /// numberOfVertices. Выбираются только полилинии текущего пространства (Model\Layout).
  70:          /// </summary>
  71:          /// <param name="tr">Объект транзакции, с помощью которого можно получить объект
  72:          /// на основе идентификатора, переданного во втором параметре</param>
  73:          /// <param name="id">Идентификатор объекта, подлежащего проверке.</param>
  74:          /// <returns>true - объект соответствует критериям фильтрации, false - не соответствует.
  75:          /// </returns>
  76:          Boolean PlinesWithSomeVertexFilter(Db.Transaction tr, Db.ObjectId id) {
  77:   
  78:              // Если один из аргументов недопустимый - возвращаем false
  79:              if (tr == null || tr.IsDisposed || id == Db.ObjectId.Null || !id.IsValid || id.IsErased)
  80:                  return false;
  81:   
  82:              Db.Database db = id.Database;
  83:              Db.DBObject item = tr.GetObject(id, Db.OpenMode.ForRead);
  84:   
  85:              // Проверяю, является ли объект одним из трёх видов полилиний
  86:              Boolean result = item is Db.Polyline || item is Db.Polyline2d || item is Db.Polyline3d;
  87:              if (!result) return false;
  88:   
  89:              // Проверяю, расположена ли полилиния в текущем пространстве (Model\Layout)
  90:              Db.ObjectId ownerId = db.CurrentSpaceId;
  91:              // По достижении этой строки кода, мы уже знаем, что имеем дело с полилинией.
  92:              // Соответственно можем спокойно преобразовывать её в Entity.
  93:              Db.Entity entity = item as Db.Entity;
  94:   
  95:              if (entity.BlockId != ownerId) return false;
  96:   
  97:              // Проверяю количество вершин
  98:              Int32 _numberOfVertices = 0;
  99:   
 100:              if (item is Db.Polyline) {
 101:                  Db.Polyline pline = item as Db.Polyline;
 102:                  _numberOfVertices = pline.NumberOfVertices;
 103:              }
 104:              else {
 105:                  IEnumerable pline = item as IEnumerable;
 106:                  foreach (var vertex in pline) {
 107:                      ++_numberOfVertices;
 108:                  }
 109:              }
 110:   
 111:              return numberOfVertices == _numberOfVertices;
 112:          }
 113:   
 114:          #region IExtensionApplication Members
 115:   
 116:          public void Initialize() {
 117:              Ed.Editor ed = cad.DocumentManager.MdiActiveDocument.Editor;
 118:              ed.WriteMessage("\nPlineSel. © Andrey Bushman, 2013\n");
 119:          }
 120:   
 121:          public void Terminate() {
 122:              // throw new NotImplementedException();
 123:          }
 124:   
 125:          #endregion
 126:      }
 127:  }

Если вы внимательно читали выше приведённый код, то наверняка обратили внимание на строки 57 и 90. Обозначенные в этих строках методы отсутствуют в AutoCAD .NET API и добавлены мною в виде методов расширения. В конце этой заметки я привожу исходный код классов и перечислений, задействованных мною в решении обозначенной в этой теме задачи. На самом деле упомянутые вспомогательные классы и перечисления не писались специально под эту задачу, но предоставляют общий полезный функционал, которым я нередко пользуюсь в различных проектах. Например, методы класса DatabaseExtensionMethods позволяют посредством LINQ достаточно просто и быстро получать или изменять информацию, хранящуюся в Database.

   1:  // DBObjectStatus.cs
   2:  // © Andrey Bushman, 2011
   3:  namespace Bushman.CAD.DatabaseServices {
   4:      /// <summary>
   5:      /// Перечисление, с помощью которого в методах IDBSearcher
   6:      /// можно указывать, идентификаторы (DBObjectId) каких объектов 
   7:      /// (DBObject) следует выбирать из базы данных (Database)
   8:      /// </summary>    
   9:      public enum DBObjectStatus {
  10:          /// <summary>
  11:          /// Выбирать только такие ObjectId, значения свойства 
  12:          /// IsErased которых равно false
  13:          /// </summary>
  14:          NotErased,
  15:          /// <summary>
  16:          /// Выбирать только такие ObjectId, значения свойства 
  17:          /// IsErased которых равно true
  18:          /// </summary>
  19:          Erased,
  20:          /// <summary>
  21:          /// Выбирать все ObjectId, не зависимо от значения
  22:          /// свойства IsErased
  23:          /// </summary>
  24:          Any
  25:      }
  26:  }


   1:  // SpaceEnum.cs
   2:  // © Andrey Bushman, 2011
   3:  // За основу взят код с сайта https://sites.google.com/site/bushmansnetlaboratory/sendbox/stati/database
   4:   
   5:  namespace Bushman.CAD.DatabaseServices {
   6:      /// <summary>
   7:      /// This enum indicates the current space in the current Database.
   8:      /// </summary>
   9:      public enum SpaceEnum {
  10:          /// <summary>
  11:          /// The Model space.
  12:          /// </summary>
  13:          Model,
  14:          /// <summary>
  15:          /// The Layout space.
  16:          /// </summary>
  17:          Layout,
  18:          /// <summary>
  19:          /// The Model space through the Layout's viewport.
  20:          /// </summary>
  21:          Viewport
  22:      }
  23:  }


   1:  // LayoutManagerExtensionMethods.cs
   2:  // © Andrey Bushman, 2011
   3:   
   4:  //Microsoft
   5:  using System;
   6:   
   7:  //Autodesk
   8:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
   9:  using App = Autodesk.AutoCAD.ApplicationServices;
  10:  using Db = Autodesk.AutoCAD.DatabaseServices;
  11:  using Ed = Autodesk.AutoCAD.EditorInput;
  12:   
  13:  namespace Bushman.CAD.DatabaseServices {
  14:   
  15:      /// <summary>
  16:      /// Методы расширения для класса Autodesk.AutoCAD.DatabaseServices.LayoutManager.
  17:      /// </summary>
  18:      public static class LayoutManagerExtensionMethods {
  19:          /// <summary>
  20:          /// This is Extension Method for the <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  21:          /// class. It gets the current space in the current Database.
  22:          /// </summary>
  23:          /// <param name="mng">Target <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  24:          /// instance.</param>
  25:          /// <returns>Returns the SpaceEnum value.</returns>        
  26:          public static SpaceEnum GetCurrentSpaceEnum(this Db.LayoutManager mng) {
  27:              Db.Database db = cad.DocumentManager.MdiActiveDocument.Database;
  28:              Int16 tilemode = (Int16)cad.GetSystemVariable("TILEMODE");
  29:   
  30:              if (tilemode == 1)
  31:                  return SpaceEnum.Model;
  32:   
  33:              Int16 cvport = (Int16)cad.GetSystemVariable("CVPORT");
  34:              if (cvport == 1)
  35:                  return SpaceEnum.Layout;
  36:              else
  37:                  return SpaceEnum.Viewport;
  38:          }
  39:   
  40:          /// <summary>
  41:          /// This is Extension Method for the <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  42:          /// class. It gets the name of the current space in the current Database.
  43:          /// </summary>
  44:          /// <param name="mng">Target <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  45:          /// instance.</param>
  46:          /// <returns>Returns the name of current space.</returns>
  47:          public static String GetCurrentSpaceName(this Db.LayoutManager mng) {
  48:              SpaceEnum space = GetCurrentSpaceEnum(mng);
  49:              Db.Database db = cad.DocumentManager.MdiActiveDocument.Database;
  50:              String modelSpaceLocalizedName = String.Empty;
  51:              using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
  52:                  Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForRead)
  53:                      as Db.BlockTable;
  54:                  Db.BlockTableRecord btr = tr.GetObject(bt[Db.BlockTableRecord.ModelSpace],
  55:                      Db.OpenMode.ForRead)
  56:                      as Db.BlockTableRecord;
  57:                  modelSpaceLocalizedName = (tr.GetObject(btr.LayoutId, Db.OpenMode.ForRead)
  58:                      as Db.Layout).LayoutName;
  59:              }
  60:              String result = space == SpaceEnum.Viewport ?
  61:                  "Model" as String : mng.CurrentLayout;
  62:              return result;
  63:          }
  64:   
  65:          /// <summary>
  66:          /// This is Extension Method for the <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  67:          /// class. It gets the localized name of the Model tab.
  68:          /// </summary>
  69:          /// <param name="mng">Target <c>Autodesk.AutoCAD.DatabaseServices.LayoutManager</c> 
  70:          /// instance.</param>
  71:          /// <returns>Returns the name of current space.</returns>
  72:          public static String GetModelTabLocalizedName(this Db.LayoutManager mng) {
  73:              Db.Database db = cad.DocumentManager.MdiActiveDocument.Database;
  74:              String modelTabLocalizedName = String.Empty;
  75:              using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
  76:                  Db.BlockTable bt = tr.GetObject(db.BlockTableId, Db.OpenMode.ForRead)
  77:                      as Db.BlockTable;
  78:                  Db.BlockTableRecord btr = tr.GetObject(bt[Db.BlockTableRecord.ModelSpace],
  79:                      Db.OpenMode.ForRead)
  80:                      as Db.BlockTableRecord;
  81:                  modelTabLocalizedName = (tr.GetObject(btr.LayoutId, Db.OpenMode.ForRead)
  82:                      as Db.Layout).LayoutName;
  83:              }
  84:              return modelTabLocalizedName;
  85:          }
  86:      }
  87:  }

   1:  // DatabaseExtensionMethods.cs
   2:  // © Andrey Bushman, 2011
   3:  // За основу взят код с сайта https://sites.google.com/site/bushmansnetlaboratory/sendbox/stati/database
   4:   
   5:  // Microsoft
   6:  using System;
   7:  using System.Collections.Generic;
   8:  using System.Collections;
   9:  using System.Linq;
  10:  using System.Text;
  11:  using System.ComponentModel;
  12:   
  13:  // Autodesk
  14:  using cad = Autodesk.AutoCAD.ApplicationServices.Application;
  15:  using App = Autodesk.AutoCAD.ApplicationServices;
  16:  using Db = Autodesk.AutoCAD.DatabaseServices;
  17:  using Ed = Autodesk.AutoCAD.EditorInput;
  18:  using Rtm = Autodesk.AutoCAD.Runtime;
  19:   
  20:  namespace Bushman.CAD.DatabaseServices {
  21:      /// <summary>    
  22:      /// Методы расширения для класса Autodesk.AutoCAD.DatabaseServices.Database.
  23:      /// </summary>
  24:      public static class DatabaseExtensionMethods {
  25:   
  26:          /// <summary>
  27:          /// Проверка корректности хэндла, а так же полученного на его основе ObjectId.
  28:          /// </summary>
  29:          /// <param name="h">Проверяемый хэндл</param>
  30:          /// <param name="id">ссылка на ObjectId, в котором следует сохранить результат 
  31:          /// в случае успешной проверки</param>
  32:          /// <returns>true - проверка прошла успешно, иначе - false</returns>
  33:          public static bool TryGetValidObjectId(this Db.Database database, Db.Handle h,
  34:              ref Db.ObjectId id) {
  35:              Boolean result = database.TryGetObjectId(h, out id);
  36:              if (result && id.IsValid)
  37:                  return result;
  38:              else {
  39:                  id = Db.ObjectId.Null;
  40:                  return false;
  41:              }
  42:          }
  43:   
  44:          /// <summary>
  45:          /// Получить идентификаторы всех объектов, унаследованных от DBObject, имеющихся в базе 
  46:          /// данных чертежа
  47:          /// </summary>
  48:          /// <param name="status">Какие именно объекты (удалённые/не удалённые/все) следует 
  49:          /// выбирать</param>
  50:          /// <returns>Возвращается массив ObjectId[]</returns>
  51:          public static Db.ObjectId[] GetAll(this Db.Database database,
  52:              DBObjectStatus status) {
  53:              bool? a = null;
  54:              switch (status) {
  55:                  case DBObjectStatus.Any:
  56:                      a = null;
  57:                      break;
  58:                  case DBObjectStatus.Erased:
  59:                      a = true;
  60:                      break;
  61:                  case DBObjectStatus.NotErased:
  62:                      a = false;
  63:                      break;
  64:              }
  65:              return GetData<Db.ObjectId>(database, (n, x) => !a.HasValue ?
  66:                  true : x.IsErased == (bool)a,
  67:                  (n, m) => m);
  68:          }
  69:   
  70:          /// <summary>
  71:          /// Выбрать данные из базы данных
  72:          /// </summary>
  73:          /// <typeparam name="R">Тип объектов, возвращаемых в массиве</typeparam>
  74:          /// <param name="requirement">Условие выборки объектов</param>
  75:          /// <param name="result">Правило построения результирующего объекта</param>
  76:          /// <returns>Возвращается массив объектов R</returns>
  77:          public static R[] GetData<R>(this Db.Database database, Func<Db.Transaction,
  78:              Db.ObjectId, Boolean> requirement, Func<Db.Transaction, Db.ObjectId, R> result) {
  79:              List<R> primitives = new List<R>();
  80:              using (Db.Transaction tr = database.TransactionManager.StartTransaction()) {
  81:                  for (long i = database.BlockTableId.Handle.Value; i < database.Handseed.Value;
  82:                      i++) {
  83:                      Db.ObjectId id = Db.ObjectId.Null;
  84:                      Db.Handle h = new Db.Handle(i);
  85:                      if (TryGetValidObjectId(database, h, ref id) && requirement(tr, id))
  86:                          primitives.Add(result(tr, id));
  87:                  }
  88:              }
  89:              return primitives.ToArray();
  90:          }
  91:   
  92:          /// <summary>
  93:          /// Получить словарь идентификаторов объектов, соответствующих заданному условию. 
  94:          /// Результирующая выборка будет представлена в виде словаря и сгруппирована в списки 
  95:          /// по именам классов (берётся из ObjectId.ObjectClass.Name) которые, в свою очередь, 
  96:          /// выступают в роли ключей. Т.о. можно быстро выбирать все объекты нужного типа, 
  97:          /// соответствующие заданному условию для дальнейшей их обработки.
  98:          /// </summary>
  99:          /// <param name="requirement">Условие выборки объектов</param>
 100:          /// <returns>Возвращается словарь, у которого в качестве ключа используется имя класса  
 101:          /// (берётся из ObjectId.ObjectClass.Name), а в качестве значения - список, содержащий 
 102:          /// в себе идентификаторы объектов этого класса</returns>
 103:          public static Dictionary<String, List<Db.ObjectId>> GetByTypes(this Db.Database database,
 104:              Func<Db.Transaction, Db.ObjectId, Boolean> requirement) {
 105:              Dictionary<string, List<Db.ObjectId>> dict = new Dictionary<String, List<Db.ObjectId>>();
 106:              using (Db.Transaction t = database.TransactionManager.StartTransaction()) {
 107:                  for (Int64 i = database.BlockTableId.Handle.Value; i < database.Handseed.Value;
 108:                      i++) {
 109:                      Db.ObjectId id = Db.ObjectId.Null;
 110:                      Db.Handle h = new Db.Handle(i);
 111:                      if (TryGetValidObjectId(database, h, ref id) && requirement(t, id)) {
 112:                          string type = id.ObjectClass.Name;
 113:                          if (!dict.Keys.Contains(type))
 114:                              dict.Add(type, new List<Db.ObjectId>());
 115:                          dict[type].Add(id);
 116:                      }
 117:                  }
 118:              }
 119:              return dict;
 120:          }
 121:   
 122:          /// <summary>
 123:          /// Сгруппировать идентификаторы (ObjectId) объектов по именам их классов (имена берутся из 
 124:          /// ObjectId.ObjectClass.Name).
 125:          /// </summary>
 126:          /// <param name="ids">Исходный массив данных</param>
 127:          /// <returns>Возвращается словарь, у которого в качестве ключа используется имя класса 
 128:          /// (берётся из ObjectId.ObjectClass.Name), а в качестве значения - список, содержащий 
 129:          /// в себе идентификаторы объектов этого класса</returns>
 130:          public static Dictionary<String, List<Db.ObjectId>> GroupByTypes(this Db.Database database,
 131:              Db.ObjectId[] ids) {
 132:              Dictionary<String, List<Db.ObjectId>> dict = new Dictionary<String, List<Db.ObjectId>>();
 133:              foreach (Db.ObjectId id in ids) {
 134:                  string type = id.ObjectClass.Name;
 135:                  if (!dict.Keys.Contains(type))
 136:                      dict.Add(type, new List<Db.ObjectId>());
 137:                  dict[type].Add(id);
 138:              }
 139:              return dict;
 140:          }
 141:   
 142:          /// <summary>
 143:          /// Выполнить указанное действие над обозначенным набором примитивов.
 144:          /// </summary>
 145:          /// <param name="ids">Идентификаторы объектов, подлежащих модификации.</param>
 146:          /// <param name="action">Действие, которое необходимо выполнить над каждым из указанных 
 147:          /// объектов.</param>
 148:          public static void Action(this Db.Database database, Db.ObjectId[] ids,
 149:              Action<Db.Transaction, Db.ObjectId> action) {
 150:              using (Db.Transaction tr = database.TransactionManager.StartTransaction()) {
 151:                  foreach (Db.ObjectId item in ids)
 152:                      action(tr, item);
 153:                  tr.Commit();
 154:              }
 155:          }
 156:      }
 157:  }


Комментариев нет: