ADN Open CIS
Сообщество программистов Autodesk в СНГ

27/10/2013

Автоматическое размещение экземпляров семейств в проекте

Мой коллега Эммануэль Ди Гиакомо (Emmanuel Di Giacomo) недавно спросил меня о приложении, которое автоматически размещает экземпляры семейства в проекте исходя из координат, указанных в текстовом файле.

Похожий add-in уже был опубликован для Revit 2011 на сайте Revit Today.

Интерфейс программы довольно простой:

 

Однако эта простая формочка позволят достичь требуемого результата при помощи всего нескольких строк кода.

  1. Загрузка формы. При загрузки формы необходимо получить все семейства, у которых свойство FamilyPlacementType равно OnLevelBased.
  2. При выборе семейства из списка необходимо получить все типоразмеры выбранного семейства.
  3. Пользователь должен выбрать текстовый файл с координатами
  4. Нужен вспомогательный метод, который будет парсить строку для извлечения значений координат.
  5. Парсинг выбранного текстового файла.
  6. Главное действие внешней команды – запустить транзакцию и разместить экземпляры выбранного типоразмера по точкам из файла
  7. Заключение и ссылки на загрузку.

Загрузка формы – вывести список семейств

Когда форма изначально загружается, мы извлекаем все семейства из документа с помощью FilteredElementCollector.

Так как add-in поддерживает только самый простой способ размещения семейств и существует только один единственный перегруженный метод NewFamilyInstance, принимающий  точку, типоразмер и StructuralType в качестве параметров, мы исключим все семейства, кроме тех, у которых тип размещения равен FamilyPlacementType.OneLevelBased:

Код - C#: [Выделить]
  1.  private void PlaceInstancesForm_Load(
  2.     object sender,
  3.     EventArgs e )
  4.   {
  5.     List<Family> families = new List<Family>(
  6.       new FilteredElementCollector( _doc )
  7.         .OfClass( typeof( Family ) )
  8.         .Cast<Family>()
  9.         .Where<Family>( f =>
  10.           f.FamilyPlacementType ==
  11.             FamilyPlacementType.OneLevelBased ) );
  12.  
  13.     cmbFamily.DataSource = families;
  14.     cmbFamily.DisplayMember = "Name";
  15.   }

Обратите внимание, как легко мы можем заполнить раскрывающийся список с помощью свойства DataSource и отобразить пользователю наименование семейства.

Выбор семейства – вывести список типоразмеров

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

Получить типоразмеры довольно просто. Достаточно лишь взять значение свойства Family.Symbols.

Однако, свойство имеет тип FamilySymbolSet и мы не можем так сразу его передать в свойство DataSource. Сначала необходимо сконвертировать его в тип List<FamilySymbol>:

Код - C#: [Выделить]
  1.  private void cmbFamily_SelectedIndexChanged(
  2.     object sender,
  3.     EventArgs e )
  4.   {
  5.     ComboBox cb = sender as ComboBox;
  6.  
  7.     Debug.Assert( null != cb,
  8.       "expected a combo box" );
  9.  
  10.     Debug.Assert( cb == cmbFamily,
  11.       "what combo box are you, then?" );
  12.  
  13.     Family f = cb.SelectedItem as Family;
  14.  
  15.     FamilySymbolSet symbols = f.Symbols;
  16.  
  17.     // I have to convert the FamilySymbolSet to a
  18.     // List, or the DataSource assignment will throw
  19.     // an exception saying "Complex DataBinding
  20.     // accepts as a data source either an IList or
  21.     // an IListSource.
  22.  
  23.     List<FamilySymbol> symbols2
  24.       = new List<FamilySymbol>(
  25.         symbols.Cast<FamilySymbol>() );
  26.  
  27.     cmbType.DataSource = symbols2;
  28.     cmbType.DisplayMember = "Name";
  29.   }

Выбор файла с координатами

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

Класс Util содержит метод FileSelectTxt для выбора текстового файла:

Код - C#: [Выделить]
  1.  /// <summary>
  2.   /// Select a specified file in the given folder.
  3.   /// </summary>
  4.   /// <param name="folder">Initial folder.</param>
  5.   /// <param name="filename">Selected filename on
  6.   /// success.</param>
  7.   /// <returns>Return true if a file was successfully
  8.   /// selected.</returns>
  9.   static bool FileSelect(
  10.     string folder,
  11.     string title,
  12.     string filter,
  13.     out string filename )
  14.   {
  15.     OpenFileDialog dlg = new OpenFileDialog();
  16.     dlg.Title = title;
  17.     dlg.CheckFileExists = true;
  18.     dlg.CheckPathExists = true;
  19.     dlg.InitialDirectory = folder;
  20.     dlg.Filter = filter;
  21.     bool rc = ( DialogResult.OK == dlg.ShowDialog() );
  22.     filename = dlg.FileName;
  23.     return rc;
  24.   }
  25.  
  26.   /// <summary>
  27.   /// Select a text file in the given folder.
  28.   /// </summary>
  29.   /// <param name="folder">Initial folder.</param>
  30.   /// <param name="filename">Selected filename on
  31.   /// success.</param>
  32.   /// <returns>Return true if a file was successfully
  33.   /// selected.</returns>
  34.   static public bool FileSelectTxt(
  35.     string folder,
  36.     out string filename )
  37.   {
  38.     return FileSelect( folder,
  39.       "Select XYZ coordinate text file or Cancel to Exit",
  40.       "XYZ coordinate text Files (*.txt)|*.txt",
  41.       out filename );
  42.   }

Этот вспомогательный метод вызывается при нажатии на кнопку Browse. Путь к файлу и папка, в которой находится этот файл, сохраняются при успешном выборе файла:

Код - C#: [Выделить]
  1.  private void btnBrowseXyz_Click(
  2.     object sender,
  3.     EventArgs e )
  4.   {
  5.     string filename;
  6.  
  7.     if( Util.FileSelectTxt( _txt_folder_name,
  8.       out filename ) )
  9.     {
  10.       txtFilename.Text = filename;
  11.  
  12.       _txt_folder_name = Path.GetDirectoryName(
  13.         filename );
  14.     }
  15.   }[[$/cshapr]]
  16.  
  17. Парсинг строки в XYZ точку
  18. Я использую регулярные выражения, чтобы найти и извлечь числа из строк текстового файла:
  19.  
  20. Код - C#: [Выделить]
  21.   /// <summary>
  22.   /// A regular expression to match a
  23.   /// real number with optional leading sign.
  24.   /// </summary>
  25.   const string _one_real_number_regex
  26.     = @"[-+]?[0-9]*\.?[0-9]+";
  27. Сначала я попытался использовать более сложное регулярное выражение, чтобы получить все три координаты XYZ за один раз:
  28.   /// <summary>
  29.   /// A regular expression to match three occurrences
  30.   /// of a real number with optional leading sign.
  31.   /// We gave up using this, because the greedy .*
  32.   /// gobbles the +- sign away from the Y and Z
  33.   /// coordinates.
  34.   /// </summary>
  35.   const string _xyz_real_number_regex
  36.     = @"(?<X>" + _one_real_number_regex + ")"
  37.     + @".*(?<Y>" + _one_real_number_regex + ")"
  38.     + @".*(?<Z>" + _one_real_number_regex + ")";

Но, это не всегда работало правильно. Так же такое выражение предполагает, что строка всегда содержит все три координаты.

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

Код - C#: [Выделить]
  1.  /// <summary>
  2.   /// Static regular expression for
  3.   /// parsing real numbers.
  4.   /// </summary>
  5.   static Regex _regex = new Regex(
  6.     //_xyz_real_number_regex
  7.     _one_real_number_regex );
  8.  
  9.   /// <summary>
  10.   /// Read three real numbers from the given string
  11.   /// and return true on success. Parse the string s
  12.   /// for exactly two or three real numbers
  13.   /// representing the XY or XYZ placement
  14.   /// coordinates. Z defaults to 0.
  15.   /// </summary>
  16.   static bool GetThreeRealNumbers(
  17.     string s,
  18.     ref double[] xyz )
  19.   {
  20.     int i = 0; // index in string
  21.     int n = 0; // count real numbers found
  22.  
  23.     Match m = _regex.Match( s, i );
  24.  
  25.     // Pure debugging support
  26.  
  27.     foreach( Group g in m.Groups )
  28.     {
  29.       Debug.Print( g.ToString() );
  30.     }
  31.  
  32.     // Read all the real numbers we can get
  33.     // and stop if we find too many
  34.  
  35.     while( 4 > n && m.Success )
  36.     {
  37.       if( 3 > n )
  38.       {
  39.         xyz[n] = double.Parse( m.ToString() );
  40.         i = m.Index + m.Length;
  41.         m = _regex.Match( s, i );
  42.  
  43.         foreach( Group g in m.Groups )
  44.         {
  45.           Debug.Print( g.ToString() );
  46.         }
  47.       }
  48.       ++n;
  49.     }
  50.  
  51.     // Add the default Z coordinate in case of need
  52.  
  53.     if( 2 == n )
  54.     {
  55.       xyz[n++] = 0.0;
  56.     }
  57.  
  58.     // Return success if we found 2 or 3 real numbers
  59.  
  60.     return 3 == n;
  61.   }

Парсинг всего текстового файла

Выбрав файл, мы обрабатываем каждую его строку.

Для каждой строки мы вызываем метод GetThreeRealNumbers.

Если строка содержит значения координат, то мы добавляем в список ревитовский объект типа XYZ.

Считаем, что строки, начинающиеся с символа # являются комментариями и пропускаем их.

Каждая строка, которая не является комментарием, обрабатывается для получения координат. Любые другие строки, не содержащие координат – игнорируются:

Код - C#: [Выделить]
  1.  private void btnOk_Click(
  2.     object sender,
  3.     EventArgs e )
  4.   {
  5.     StreamReader reader = File.OpenText(
  6.       txtFilename.Text );
  7.  
  8.     string read = reader.ReadToEnd();
  9.  
  10.     string[] lines = read.Split( '\n' );
  11.  
  12.     string s;
  13.     double[] xyz = new double[3] { 0, 0, 0 };
  14.  
  15.     foreach( string line in lines )
  16.     {
  17.       s = line.Trim();
  18.  
  19.       if( s.StartsWith( "#" ) )
  20.       {
  21.         continue;
  22.       }
  23.  
  24.       // Parse string s for exactly two or three
  25.       // real numbers representing the XY or XYZ
  26.       // placement coordinates. Z defaults to 0.
  27.  
  28.       if( GetThreeRealNumbers( s, ref xyz ) )
  29.       {
  30.         XYZ p = new XYZ( xyz[0], xyz[1], xyz[2] );
  31.  
  32.         if( null == _pts )
  33.         {
  34.           _pts = new List<XYZ>( 1 );
  35.         }
  36.         _pts.Add( p );
  37.       }
  38.     }
  39.   }

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

Запуск внешней команды – размещение семейств выбранного типоразмера

В внешней команде выполняются следующие действия:

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

Пользователю отображается форма

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

Код - C#: [Выделить]
  1.  public Result Execute(
  2.     ExternalCommandData commandData,
  3.     ref string message,
  4.     ElementSet elements )
  5.   {
  6.     IWin32Window revit_window
  7.       = new JtWindowHandle(
  8.         ComponentManager.ApplicationWindow );
  9.  
  10.     UIApplication uiapp = commandData.Application;
  11.     UIDocument uidoc = uiapp.ActiveUIDocument;
  12.     Document doc = uidoc.Document;
  13.  
  14.     PlaceInstancesForm f
  15.       = new PlaceInstancesForm( doc );
  16.  
  17.     if( DialogResult.OK == f.ShowDialog(
  18.       revit_window ) )
  19.     {
  20.       using( Transaction t = new Transaction(
  21.         doc ) )
  22.       {
  23.         t.Start( "Place Instances" );
  24.  
  25.         Autodesk.Revit.Creation.Document
  26.           creation_doc = doc.Create;
  27.  
  28.         StructuralType st
  29.           = StructuralType.NonStructural;
  30.  
  31.         foreach( XYZ p in f.Points )
  32.         {
  33.           creation_doc.NewFamilyInstance(
  34.             p, f.Type, st );
  35.         }
  36.  
  37.         t.Commit();
  38.       }
  39.     }
  40.     return Result.Succeeded;
  41.   }

Заключение и ссылки

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

Я был особенно счастлив, узнав что можно вот так просто заполнить список с помощью свойства DataSource присвоив ему значение результата FilteredElementCollector.

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

Увидеть своими глазами эту надстройку можно скачав архив с полным исходным кодом и проектом для Visual Studio.

Исходный код можно получить также на GitHub из репозитория, а также скачать последнюю версию в виде архива.

Будьте внимательны, надстройка поддерживает только простой вид семейств, для размещения которых нужна одна точка.

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

Большое спасибо Эммануэлю за совет и тестирование надстройки.

Источник: http://thebuildingcoder.typepad.com/blog/2013/10/text-file-driven-automatic-placement-of-family-instances.html

Обсуждение: http://adn-cis.org/forum/index.php?topic=290

Опубликовано 27.10.2013
Отредактировано 27.10.2013 в 00:52:47