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

27/07/2016

Поиск проемов в стене

Вопрос: Я пытаюсь получить с помощью API проемы в стене. Меня в частности интересует координаты прямоугольного проема.

 

Я попытался использовать метод FindInserts(), но он не возвращает проемы.

Есть ли решение этой проблемы?

Ответ: Интересный вопрос, на который существует несколько ответов.

Можно определить элементы на стене (включая проемы) путем временного удаления этой стены. Такой способ даст вам список всех зависимых объектов, которые удаляются вместе со стеной.

Можно проанализировать геометрию стены до и после удаления вставленных на стену элементов и определить разницу.

Можно также исследовать геометрию стены на виде Фасад и найти проемы таким образом.

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

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

Подобный способ демонстрируется в проектах FindReferencesByDirection и FindColumns в примерах, поставляемых вместе с Revit SDK.

Попробуйте, расскажите что в итоге получится.

Ответ: Отлично. Метод с лучом работает превосходно. Я использовал 2D вид, но перед этим, на виде «Фасад» я нашел высоту.

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

 

Псевдо-код моего решения:

Код - C#: [Выделить]
  1.   WallOpening2D – простой класс с двумя координатами и другой базовой информацией.
  2.  
  3.   List<WallOpening2D> GetWallOpenings (
  4.     Wall wall,
  5.     ViewPlan view )
  6.   {
  7.     var rayStart = new XYZ(
  8.       wallOrigin.X - wallDirection.X,
  9.       wallOrigin.Y - wallDirection.Y,
  10.       GetElevation(view));
  11.  
  12.     pointList = (from reference in
  13.       new ReferenceIntersector(view)
  14.         .Find(rayStart, wallDirection)
  15.         .where(IsSurface)
  16.         .where(ref => ref.Proximity
  17.           < wallLength + "step outside"))
  18.         select reference.GetReference.GlobalPoint)
  19.  
  20.     if(!pointList.First().IsAlmostEqualTo(wallOrigin) //Проверяем, что первая точка не является границей стены
  21.       pointList.Insert(0, wallOrigin);
  22.     else
  23.       pointList.remove(pointList(First));
  24.  
  25.     if(!IsEven(pointList.Count)  //если не четное количество, то стена заканчивается проемом
  26.       pointList.Add(wallEndPoint);
  27.  
  28.     var wallOpenings = new List();
  29.     for(i = 0; i < pointList.Count -1; i += 2)
  30.       wallOpenings.Add(new WallOpening2D(pointList[i], pointList[i+1]);
  31.  
  32.     return wallOpenings;
  33.   }

Ответ: Рад что мой совет помог. И спасибо за псевдо-код. Однако я попробвал его реализовать и столкнулся с одной проблемой:

error CS1502: The best overloaded method match for 'Autodesk.Revit.DB.ReferenceIntersector.ReferenceIntersector(Autodesk.Revit.DB.View3D)' has some invalid arguments

error CS1503: Argument 1: cannot convert from 'Autodesk.Revit.DB.ViewPlan' to 'Autodesk.Revit.DB.View3D'

На текущий момент моя реализация такова:

Код - C#: [Выделить]
  1. List GetWallOpenings(
  2.   Wall wall,
  3.   ViewPlan view )
  4. {
  5.   Document doc = wall.Document;
  6.   Level level = doc.GetElement( view.LevelId ) as Level;
  7.   double elevation = level.Elevation;
  8.   Curve c = (wall.Location as LocationCurve).Curve;
  9.   XYZ wallOrigin = c.GetEndPoint(0);
  10.   XYZ wallEndPoint = c.GetEndPoint(1);
  11.   XYZ wallDirection =wallEndPoint - wallOrigin;
  12.   double wallLength = wallDirection.GetLength();
  13.   wallDirection = wallDirection.Normalize();
  14.   UV offset = new UV( wallDirection.X, wallDirection.Y );
  15.   double step_outside = offset.GetLength();
  16.  
  17.   XYZ rayStart = new XYZ( wallOrigin.X - offset.U,
  18.     wallOrigin.Y - offset.V, elevation );
  19.  
  20.   ReferenceIntersector intersector = new ReferenceIntersector( view ); // *** тут ошибка ***
  21.  
  22.   IList refs = intersector.Find( rayStart, wallDirection );
  23.   List pointList = new List( refs
  24.     .Where( r => IsSurface(r.GetReference()))
  25.     .Where( r => r.Proximity < wallLength + step_outside)
  26.     .Select( r => r.GetReference().GlobalPoint) );
  27.  
  28.  
  29.   // Проверяем, если перввая точка не является границей стены
  30.   // Если так, то стена начнается с проема
  31.  
  32.   if(!pointList.First().IsAlmostEqualTo(wallOrigin))
  33.   {
  34.     pointList.Insert(0, wallOrigin);
  35.   }
  36.   else
  37.   {
  38.     pointList.Remove(pointList.First());
  39.   }
  40.  
  41.   // Если количество товек нечетное, то стена
  42.   // заканчивается проемом
  43.  
  44.   if(!IsEven(pointList.Count))
  45.   {
  46.     pointList.Add(wallEndPoint);
  47.   }
  48.  
  49.   int n = pointList.Count;
  50.   var wallOpenings = new List( n / 2 );
  51.   for( int i = 0; i < n; i += 2 )
  52.   {
  53.     wallOpenings.Add( new WallOpening2d {
  54.       Start = pointList[i],
  55.       End = pointList[i + 1] } );
  56.   }
  57.   return wallOpenings;
  58. }

Очевидно, нужно передать 3D вид, а не план.

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

Начальная точка появляется дважды. А конечная граница стены не появляется вовсе. Точки не отсортированы по порядку появления:

 

Нужно конечно подчстить код и добавить защиту от дурака для более надежного результата.

Рабочую команду можно найти в примерах The Building Coder.  Команда называется CmdWallOpenings.

Ответ: Немного корректировки в мой псевдо-код:

  • Вы правы насчет 3D вида. Я использую 2D вид чтобы получить высоту. Для ReferenceIntersector я использую 3D вид по умолчанию. Нужнно только удостовериться, что он не имеет области подрезки.

Код - C#: [Выделить]
  1.   var default3DView
  2.     = new FilteredElementCollector(doc)
  3.       .OfClass(typeof (View3D))
  4.       .ToElements()
  5.       .Cast()
  6.       .FirstOrDefault( v
  7.         => v != null
  8.         && !v.IsTemplate
  9.         && v.Name.Equals("{3D}"));

Конечно, можно и создать 3D вид, если нужно.

  • Я забыл уточнить, что ReferenceIntersector должен возвратить только поверхности, принадлежащие данной стене

Код - C#: [Выделить]
  1. var referenceIntersector
  2.     = new ReferenceIntersector( wall.Id,
  3.       FindReferenceTarget.Face, default3DView);

  • Начальная точка появляется дважды. А конечная граница стены не появляется вовсе. Точки не отсортированы по порядку появления

Точки будут отсортированы, если доабвить идентификатор стены в конструктор ReferenceIntersector. В вашем случае точки отсортированы по элементам, с которыми идет пересечение.

Чтобы получить конечную точку, важно поставить смещение снаружи стены, как в этом выражении:

Код - C#: [Выделить]
  1.   .Where( r => r.Proximity < wallLength + step_outside)

Начальная точка появляется дважды, возможно из-за этого условия - !pointList.First().IsAlmostEqualTo(wallOrigin) в коде

Код - C#: [Выделить]
  1.   if(!pointList.First().IsAlmostEqualTo(wallOrigin))
  2.   {
  3.     pointList.Insert(0, wallOrigin);
  4.   }
будет всегда возвращать true и поэтому начальная точка будт вставлена дважды.

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

Измененный код:

Код - C#: [Выделить]
  1.   Curve c = (wall.Location as LocationCurve).Curve;
  2.  
  3.   var wallStartPoint = new XYZ( c.GetEndPoint(0).X,
  4.     c.GetEndPoint(0).Y, elevation);
  5.  
  6.   var rayStart = new XYZ(
  7.     wallStartPoint.X - wallLine.Direction.X,
  8.     wallStartPoint.Y - wallLine.Direction.Y,
  9.     wallStartPoint.Z);
  10.  
  11.   ...
  12.  
  13.   if (!pointList.First().IsAlmostEqualTo(wallStartPoint))
  14.     pointList.Insert(0, wallStartPoint);

Мой код теперь работает достаточно неплохо. Спасибо за предложение использовать луч J

Ответ: Большое спасибо, что ответили на мои вопросы.

Если вы заметили, то я тоже нашел и поправил все эти проблемы в репозитории, хотя и немного по своему.

1)      Я просто использовал текущий 3D вид.

2)      Ага, тоже заметил и добавил это

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

4)      Мой код тоже теперь работает хорошо. Я обновил его на GitHub. Если у вас будут еще обновления, дайте мне знать.

Дополнение 1: Дополнение по поводу 3D вида по умолчанию. При совместной работе нет 3D вида по умолчанию. И если пользователь созадет новый 3D вид по умолчанию, то создается вид c именем {3d – имя пользователя}. Все маленькими буквами.

Дополнение 2: Вместо использования одного луча, может быть использовать несколько, типа как расческа.

Ответ: Полностью согласен. Нужно задать параметр – минимальный размер проема, затем пройтись «расческой» по всей стене. Получить все точки, отсортировать их, исключить дубликаты и готово.

Реализация команды CmdWallOpenings

Как я сказал, я реализовал и протестировал алгоритм в новой команде CmdWallOpenings. Я почистил код, сделал поправки, как было описано выше. Они в релизе 2016.0.125.1.

Также я провел тестирование на 3 различных стенах.

 

Каждая из них имеет 4 проема. Запустив команду CmdWallOpenings для каждой из стен, мы получим вот такой результат:

Код - XML: [Выделить]
  1.  4 openings found:
  2.   ((-0.42,18.27,0.1)-(2.59,18.27,0.1))
  3.   ((5.49,18.27,0.1)-(8.49,18.27,0.1))
  4.   ((9.43,18.27,0.1)-(12.43,18.27,0.1))
  5.   ((13.36,18.27,0.1)-(16.37,18.27,0.1))
  6.   4 openings found:
  7.   ((0.78,4.77,0)-(2.73,5.68,0.1))
  8.   ((4.47,6.5,0.1)-(7.19,7.76,0.1))
  9.   ((8.93,8.58,0.1)-(11.66,9.84,0.1))
  10.   ((13.1,10.52,0.1)-(15.82,11.79,0.1))
  11.   4 openings found:
  12.   ((2.23,-3.24,0.1)-(5.12,-2.41,0.1))
  13.   ((6.96,-1.88,0.1)-(9.85,-1.06,0.1))
  14.   ((12.32,-0.35,0.1)-(15.21,0.48,0.1))
  15.   ((15.79,0.65,0.1)-(17.24,1.06,0))

 

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

Код - C#: [Выделить]
  1.   /// <summary>
  2.   /// Простой класс с двумя координатами
  3.   /// и другой базовой информации
  4.   /// </summary>
  5.   class WallOpening2d
  6.   {
  7.     //public ElementId Id { get; set; }
  8.     public XYZ Start { get; set; }
  9.     public XYZ End { get; set; }
  10.     override public string ToString()
  11.     {
  12.       return "("
  13.         //+ Id.ToString() + "@"
  14.         + Util.PointString( Start ) + "-"
  15.         + Util.PointString( End ) + ")";
  16.     }
  17.   } [$/code]]
  18. Я применил смещение для поднятия луча отночительно уровня пола, тем самым избежав пересечения с нижней гранью стены.
  19. Это же смещение было применено для удлинения луча за границы стены.
  20.  
  21. Код - C#: [Выделить]
  22.   /// <summary>
  23.   /// Немного поднимемся от стены
  24.   /// </summary>
  25.   const double _offset = 0.1; // футы

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

Код - C#: [Выделить]
  1.   /// <summary>
  2.   /// Является ли число четным
  3.   /// </summary>
  4.   static bool IsEven( int i )
  5.   {
  6.     return 0 == i % 2;
  7.   }
  8.  
  9.   /// <summary>
  10.   /// Является ли ссылка поверхностью
  11.   /// </summary>
  12.   static bool IsSurface( Reference r )
  13.   {
  14.     return ElementReferenceType.REFERENCE_TYPE_SURFACE
  15.       == r.ElementReferenceType;
  16.   }

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

Метод сравнения для поиска дубликатов:

Код - C#: [Выделить]
  1.   class XyzEqualityComparer : IEqualityComparer<XYZ>
  2.   {
  3.     public bool Equals( XYZ a, XYZ b )
  4.     {
  5.       return Util.IsEqual( a, b );
  6.     }
  7.  
  8.     public int GetHashCode( XYZ a )
  9.     {
  10.       return Util.PointString( a ).GetHashCode();
  11.     }
  12.   }

С этими вспомогательным методами, главный метод GetWallOpenings выглядит вот так:

Код - C#: [Выделить]
  1. /// <summary>
  2. /// Извлекаем все проемы в стене
  3. /// включая те, что находятся в начале и в конце стены
  4. /// </summary>
  5. List<WallOpening2d> GetWallOpenings(
  6.   Wall wall,
  7.   View3D view )
  8. {
  9.   Document doc = wall.Document;
  10.   Level level = doc.GetElement( wall.LevelId ) as Level;
  11.   double elevation = level.Elevation;
  12.   Curve c = ( wall.Location as LocationCurve ).Curve;
  13.   XYZ wallOrigin = c.GetEndPoint( 0 );
  14.   XYZ wallEndPoint = c.GetEndPoint( 1 );
  15.   XYZ wallDirection = wallEndPoint - wallOrigin;
  16.   double wallLength = wallDirection.GetLength();
  17.   wallDirection = wallDirection.Normalize();
  18.   UV offsetOut = _offset * new UV( wallDirection.X, wallDirection.Y );
  19.  
  20.   XYZ rayStart = new XYZ( wallOrigin.X - offsetOut.U,
  21.     wallOrigin.Y - offsetOut.V, elevation + _offset );
  22.  
  23.   ReferenceIntersector intersector
  24.     = new ReferenceIntersector( wall.Id,
  25.       FindReferenceTarget.Face, view );
  26.  
  27.   IList<ReferenceWithContext> refs
  28.     = intersector.Find( rayStart, wallDirection );
  29.  
  30.   // Извлекаем точки пересечения:
  31.   // - только поверхности
  32.   // - по все длине стены плюс смещение
  33.   // - сортируем по мере появления
  34.   // - исключая дубликаты.
  35.  
  36.   List<XYZ> pointList = new List<XYZ>( refs
  37.     .Where<ReferenceWithContext>( r => IsSurface(
  38.       r.GetReference() ) )
  39.     .Where<ReferenceWithContext>( r => r.Proximity
  40.       < wallLength + _offset + _offset )
  41.     .OrderBy<ReferenceWithContext, double>(
  42.       r => r.Proximity )
  43.     .Select<ReferenceWithContext, XYZ>( r
  44.       => r.GetReference().GlobalPoint )
  45.     .Distinct<XYZ>( new XyzEqualityComparer() ) );
  46.  
  47.   // Проверяем если первая точка на границе стены
  48.   // Если так, что стена не начинается с проема
  49.   // и ее можно удалить
  50.  
  51.   XYZ q = wallOrigin + _offset * XYZ.BasisZ;
  52.  
  53.   bool wallHasFaceAtStart = Util.IsEqual(
  54.     pointList[0], q );
  55.  
  56.   if( wallHasFaceAtStart )
  57.   {
  58.     pointList.RemoveAll( p
  59.       => Util.IsEqual( p, q ) );
  60.   }
  61.   else
  62.   {
  63.     pointList.Insert( 0, wallOrigin );
  64.   }
  65.  
  66.   // Проверяем если последняя точка на границе стены
  67.   // Если так, что стена не заканчивается проемом
  68.   // и ее можно удалить
  69.  
  70.   q = wallEndPoint + _offset * XYZ.BasisZ;
  71.  
  72.   bool wallHasFaceAtEnd = Util.IsEqual(
  73.     pointList.Last(), q );
  74.  
  75.   if( wallHasFaceAtEnd )
  76.   {
  77.     pointList.RemoveAll( p
  78.       => Util.IsEqual( p, q ) );
  79.   }
  80.   else
  81.   {
  82.     pointList.Add( wallEndPoint );
  83.   }
  84.  
  85.   int n = pointList.Count;
  86.  
  87.   Debug.Assert( IsEven( n ),
  88.     "expected an even number of opening sides" );
  89.  
  90.   var wallOpenings = new List<WallOpening2d>(
  91.     n / 2 );
  92.  
  93.   for( int i = 0; i < n; i += 2 )
  94.   {
  95.     wallOpenings.Add( new WallOpening2d
  96.     {
  97.       Start = pointList[i],
  98.       End = pointList[i + 1]
  99.     } );
  100.   }
  101.   return wallOpenings;
  102. }

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

  • Проверяем, что мы находимся в валидном контексте
  • Выбираем стену или используем ту, которую выделили перед выполнением команды
  • Извлекаем все проемы
  • Выводим результат

Код - C#: [Выделить]
  1. public Result Execute(
  2.   ExternalCommandData commandData,
  3.   ref string message,
  4.   ElementSet elements )
  5. {
  6.   UIApplication uiapp = commandData.Application;
  7.   UIDocument uidoc = uiapp.ActiveUIDocument;
  8.   Document doc = uidoc.Document;
  9.  
  10.   if( null == doc )
  11.   {
  12.     message = "Please run this command in a valid document.";
  13.     return Result.Failed;
  14.   }
  15.  
  16.   View3D view = doc.ActiveView as View3D;
  17.  
  18.   if( null == view )
  19.   {
  20.     message = "Please run this command in a 3D view.";
  21.     return Result.Failed;
  22.   }
  23.  
  24.   Element e = Util.SelectSingleElementOfType(
  25.     uidoc, typeof( Wall ), "wall", true );
  26.  
  27.   List<WallOpening2d> openings = GetWallOpenings(
  28.     e as Wall, view );
  29.  
  30.   int n = openings.Count;
  31.  
  32.   string msg = string.Format(
  33.     "{0} opening{1} found{2}",
  34.     n, Util.PluralSuffix( n ),
  35.     Util.DotOrColon( n ) );
  36.  
  37.   Util.InfoMsg2( msg, string.Join(
  38.     "\r\n", openings ) );
  39.  
  40.   return Result.Succeeded;
  41. }

Источник: http://thebuildingcoder.typepad.com/blog/2015/12/retrieving-wall-openings-and-sorting-points.html

Автор перевода: Виктор Чекалин

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

Опубликовано 27.07.2016
Отредактировано 28.07.2016 в 08:53:37