Автоматическое размещение экземпляров семейств в проекте
Мой коллега Эммануэль Ди Гиакомо (Emmanuel Di Giacomo) недавно спросил меня о приложении, которое автоматически размещает экземпляры семейства в проекте исходя из координат, указанных в текстовом файле.
Похожий add-in уже был опубликован для Revit 2011 на сайте Revit Today.
Интерфейс программы довольно простой:
Однако эта простая формочка позволят достичь требуемого результата при помощи всего нескольких строк кода.
- Загрузка формы. При загрузки формы необходимо получить все семейства, у которых свойство FamilyPlacementType равно OnLevelBased.
- При выборе семейства из списка необходимо получить все типоразмеры выбранного семейства.
- Пользователь должен выбрать текстовый файл с координатами
- Нужен вспомогательный метод, который будет парсить строку для извлечения значений координат.
- Парсинг выбранного текстового файла.
- Главное действие внешней команды – запустить транзакцию и разместить экземпляры выбранного типоразмера по точкам из файла
- Заключение и ссылки на загрузку.
Загрузка формы – вывести список семейств
Когда форма изначально загружается, мы извлекаем все семейства из документа с помощью FilteredElementCollector.
Так как add-in поддерживает только самый простой способ размещения семейств и существует только один единственный перегруженный метод NewFamilyInstance, принимающий точку, типоразмер и StructuralType в качестве параметров, мы исключим все семейства, кроме тех, у которых тип размещения равен FamilyPlacementType.OneLevelBased:
- private void PlaceInstancesForm_Load(
- object sender,
- EventArgs e )
- {
- List<Family> families = new List<Family>(
- new FilteredElementCollector( _doc )
- .OfClass( typeof( Family ) )
- .Cast<Family>()
- .Where<Family>( f =>
- f.FamilyPlacementType ==
- FamilyPlacementType.OneLevelBased ) );
- cmbFamily.DataSource = families;
- cmbFamily.DisplayMember = "Name";
- }
Обратите внимание, как легко мы можем заполнить раскрывающийся список с помощью свойства DataSource и отобразить пользователю наименование семейства.
Выбор семейства – вывести список типоразмеров
Каждый раз, когда пользователь выбирает семейства в списке, в обработчике события SelectedIndexChanged мы обновляем список типоразмеров.
Получить типоразмеры довольно просто. Достаточно лишь взять значение свойства Family.Symbols.
Однако, свойство имеет тип FamilySymbolSet и мы не можем так сразу его передать в свойство DataSource. Сначала необходимо сконвертировать его в тип List<FamilySymbol>:
- private void cmbFamily_SelectedIndexChanged(
- object sender,
- EventArgs e )
- {
- ComboBox cb = sender as ComboBox;
- Debug.Assert( null != cb,
- "expected a combo box" );
- Debug.Assert( cb == cmbFamily,
- "what combo box are you, then?" );
- Family f = cb.SelectedItem as Family;
- FamilySymbolSet symbols = f.Symbols;
- // I have to convert the FamilySymbolSet to a
- // List, or the DataSource assignment will throw
- // an exception saying "Complex DataBinding
- // accepts as a data source either an IList or
- // an IListSource.
- List<FamilySymbol> symbols2
- = new List<FamilySymbol>(
- symbols.Cast<FamilySymbol>() );
- cmbType.DataSource = symbols2;
- cmbType.DisplayMember = "Name";
- }
Выбор файла с координатами
Хм... на самом деле совершенно нечего сказать по поводу этого шага. Хотя, я могу упомянуть о том, какие методы тут используются.
Класс Util содержит метод FileSelectTxt для выбора текстового файла:
- /// <summary>
- /// Select a specified file in the given folder.
- /// </summary>
- /// <param name="folder">Initial folder.</param>
- /// <param name="filename">Selected filename on
- /// success.</param>
- /// <returns>Return true if a file was successfully
- /// selected.</returns>
- static bool FileSelect(
- string folder,
- string title,
- string filter,
- out string filename )
- {
- OpenFileDialog dlg = new OpenFileDialog();
- dlg.Title = title;
- dlg.CheckFileExists = true;
- dlg.CheckPathExists = true;
- dlg.InitialDirectory = folder;
- dlg.Filter = filter;
- bool rc = ( DialogResult.OK == dlg.ShowDialog() );
- filename = dlg.FileName;
- return rc;
- }
- /// <summary>
- /// Select a text file in the given folder.
- /// </summary>
- /// <param name="folder">Initial folder.</param>
- /// <param name="filename">Selected filename on
- /// success.</param>
- /// <returns>Return true if a file was successfully
- /// selected.</returns>
- static public bool FileSelectTxt(
- string folder,
- out string filename )
- {
- return FileSelect( folder,
- "Select XYZ coordinate text file or Cancel to Exit",
- "XYZ coordinate text Files (*.txt)|*.txt",
- out filename );
- }
Этот вспомогательный метод вызывается при нажатии на кнопку Browse. Путь к файлу и папка, в которой находится этот файл, сохраняются при успешном выборе файла:
- private void btnBrowseXyz_Click(
- object sender,
- EventArgs e )
- {
- string filename;
- if( Util.FileSelectTxt( _txt_folder_name,
- out filename ) )
- {
- txtFilename.Text = filename;
- _txt_folder_name = Path.GetDirectoryName(
- filename );
- }
- }[[$/cshapr]]
- Парсинг строки в XYZ точку
- Я использую регулярные выражения, чтобы найти и извлечь числа из строк текстового файла:
- Код - C#: [Выделить]
- /// <summary>
- /// A regular expression to match a
- /// real number with optional leading sign.
- /// </summary>
- const string _one_real_number_regex
- = @"[-+]?[0-9]*\.?[0-9]+";
- Сначала я попытался использовать более сложное регулярное выражение, чтобы получить все три координаты XYZ за один раз:
- /// <summary>
- /// A regular expression to match three occurrences
- /// of a real number with optional leading sign.
- /// We gave up using this, because the greedy .*
- /// gobbles the +- sign away from the Y and Z
- /// coordinates.
- /// </summary>
- const string _xyz_real_number_regex
- = @"(?<X>" + _one_real_number_regex + ")"
- + @".*(?<Y>" + _one_real_number_regex + ")"
- + @".*(?<Z>" + _one_real_number_regex + ")";
Но, это не всегда работало правильно. Так же такое выражение предполагает, что строка всегда содержит все три координаты.
Парсинг только одной координаты за один проход, позволяет мне более легко контролировать ситуацию, когда строка содержит только одну или две координаты, предполагая, что значение отсутствующей координаты равно 0.
- /// <summary>
- /// Static regular expression for
- /// parsing real numbers.
- /// </summary>
- static Regex _regex = new Regex(
- //_xyz_real_number_regex
- _one_real_number_regex );
- /// <summary>
- /// Read three real numbers from the given string
- /// and return true on success. Parse the string s
- /// for exactly two or three real numbers
- /// representing the XY or XYZ placement
- /// coordinates. Z defaults to 0.
- /// </summary>
- static bool GetThreeRealNumbers(
- string s,
- ref double[] xyz )
- {
- int i = 0; // index in string
- int n = 0; // count real numbers found
- Match m = _regex.Match( s, i );
- // Pure debugging support
- foreach( Group g in m.Groups )
- {
- Debug.Print( g.ToString() );
- }
- // Read all the real numbers we can get
- // and stop if we find too many
- while( 4 > n && m.Success )
- {
- if( 3 > n )
- {
- xyz[n] = double.Parse( m.ToString() );
- i = m.Index + m.Length;
- m = _regex.Match( s, i );
- foreach( Group g in m.Groups )
- {
- Debug.Print( g.ToString() );
- }
- }
- ++n;
- }
- // Add the default Z coordinate in case of need
- if( 2 == n )
- {
- xyz[n++] = 0.0;
- }
- // Return success if we found 2 or 3 real numbers
- return 3 == n;
- }
Парсинг всего текстового файла
Выбрав файл, мы обрабатываем каждую его строку.
Для каждой строки мы вызываем метод GetThreeRealNumbers.
Если строка содержит значения координат, то мы добавляем в список ревитовский объект типа XYZ.
Считаем, что строки, начинающиеся с символа # являются комментариями и пропускаем их.
Каждая строка, которая не является комментарием, обрабатывается для получения координат. Любые другие строки, не содержащие координат – игнорируются:
- private void btnOk_Click(
- object sender,
- EventArgs e )
- {
- StreamReader reader = File.OpenText(
- txtFilename.Text );
- string read = reader.ReadToEnd();
- string[] lines = read.Split( '\n' );
- string s;
- double[] xyz = new double[3] { 0, 0, 0 };
- foreach( string line in lines )
- {
- s = line.Trim();
- if( s.StartsWith( "#" ) )
- {
- continue;
- }
- // Parse string s for exactly two or three
- // real numbers representing the XY or XYZ
- // placement coordinates. Z defaults to 0.
- if( GetThreeRealNumbers( s, ref xyz ) )
- {
- XYZ p = new XYZ( xyz[0], xyz[1], xyz[2] );
- if( null == _pts )
- {
- _pts = new List<XYZ>( 1 );
- }
- _pts.Add( p );
- }
- }
- }
Как вы видите, здесь нет никакой обработки ошибок, если парсинг строки выполнить не удалось. Если необходимо, то вы можете отобразить пользователю предупреждение.
Запуск внешней команды – размещение семейств выбранного типоразмера
В внешней команде выполняются следующие действия:
Определяется указатель на главное окно Revit, чтобы указать нашей форме собственника.
Пользователю отображается форма
При нажатии на кнопку ОК на форме, создается новая транзакция, с которой идет размещение семейств выбранного типоразмера в точках, считанных из текстового файла.
- public Result Execute(
- ExternalCommandData commandData,
- ref string message,
- ElementSet elements )
- {
- IWin32Window revit_window
- = new JtWindowHandle(
- ComponentManager.ApplicationWindow );
- UIApplication uiapp = commandData.Application;
- UIDocument uidoc = uiapp.ActiveUIDocument;
- Document doc = uidoc.Document;
- PlaceInstancesForm f
- = new PlaceInstancesForm( doc );
- if( DialogResult.OK == f.ShowDialog(
- revit_window ) )
- {
- using( Transaction t = new Transaction(
- doc ) )
- {
- t.Start( "Place Instances" );
- Autodesk.Revit.Creation.Document
- creation_doc = doc.Create;
- StructuralType st
- = StructuralType.NonStructural;
- foreach( XYZ p in f.Points )
- {
- creation_doc.NewFamilyInstance(
- p, f.Type, st );
- }
- t.Commit();
- }
- }
- return Result.Succeeded;
- }
Заключение и ссылки
На мой взгляд, данная надстройка обладает довольно полезной функциональностью и в то же время довольно проста в реализации.
Я был особенно счастлив, узнав что можно вот так просто заполнить список с помощью свойства DataSource присвоив ему значение результата FilteredElementCollector.
Хотя, это необольшая заслуга LINQ, который помог нам присвоить значние DataSource. В первой версии не было проверки на тип размещения семейства и отображались абсолютно все семейства документа.
Увидеть своими глазами эту надстройку можно скачав архив с полным исходным кодом и проектом для Visual Studio.
Исходный код можно получить также на GitHub из репозитория, а также скачать последнюю версию в виде архива.
Будьте внимательны, надстройка поддерживает только простой вид семейств, для размещения которых нужна одна точка.
Поддержку семейств, для размещения которых нужно указать линию, поверхность, стену и т.п. нужно реализовать читателю самостоятельно в качестве упражнения.
Большое спасибо Эммануэлю за совет и тестирование надстройки.
Обсуждение: http://adn-cis.org/forum/index.php?topic=290
Опубликовано 27.10.2013Отредактировано 27.10.2013 в 00:52:47