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

29/09/2014

Плоскости, проекции и выбранные точки

В статье обсудим вопрос и пример кода, касающийся новшеств Revit API, в частности при работе с плоскостями, проекциями и выбранными точками.

Вопрос: В коде, приведенном ниже, я задаю рабочую плоскость на текущем виде и предлагаю пользователю выбрать две точки, для вычисления площади прямоугольника. Код работает для восточного и западного фасада, но на северном и южном фасадах, площадь равна 0. Есть какие-нибудь идеи, почему так может быть? Если же я задаю рабочую плоскость вручную в пользовательском интерфейсе на северном и южном фасадах и выполняю код, в котором вычисляю только площадь без создания рабочей плоскости, то площадь вычисляется нормально.

Также помимо этого, код основывается на координатах XYZ, и не позволяет вычислить правильную площадь, скажем для стены, повернутой на 45 градусов. Как можно сделать мой код более универсальным?

Код - C#: [Выделить]
  1.   public void SetWorkPlaneAndPickPointsForArea(
  2.     UIDocument uidoc )
  3.   {
  4.     Document doc = uidoc.Document
  5.  
  6.     double differenceX;
  7.     double differenceY;
  8.     double differenceZ;
  9.     double area;
  10.  
  11.     Transaction t = new Transaction( doc,
  12.       "Создание рабочей плоскости" );
  13.  
  14.     t.Start();
  15.  
  16.     Plane plane = new Plane(
  17.       doc.ActiveView.ViewDirection,
  18.       doc.ActiveView.Origin );
  19.  
  20.     SketchPlane sp = doc.Create.NewSketchPlane(
  21.       plane );
  22.  
  23.     doc.ActiveView.SketchPlane = sp;
  24.     doc.ActiveView.ShowActiveWorkPlane();
  25.  
  26.     t.Commit();
  27.  
  28.     XYZ pt1 = uidoc.Selection.PickPoint();
  29.     XYZ pt2 = uidoc.Selection.PickPoint();
  30.  
  31.     double pt1x = pt1.X;
  32.     double pt1y = pt1.Y;
  33.     double pt1z = pt1.Z;
  34.  
  35.     double pt2x = pt2.X;
  36.     double pt2y = pt2.Y;
  37.     double pt2z = pt2.Z;
  38.  
  39.     bool b;
  40.     int caseSwitch = 0;
  41.  
  42.     if( b = ( pt1z == pt2z ) )
  43.     { caseSwitch = 1; }
  44.  
  45.     if( b = ( pt1y == pt2y ) )
  46.     { caseSwitch = 2; }
  47.  
  48.     if( b = ( pt1x == pt2x ) )
  49.     { caseSwitch = 3; }
  50.  
  51.     switch( caseSwitch )
  52.     {
  53.       case 1:
  54.         differenceX = pt2x - pt1x;
  55.         differenceY = pt1y - pt2y;
  56.         area = differenceX * differenceY;
  57.         break;
  58.  
  59.       case 2:
  60.         differenceX = pt2x - pt1x;
  61.         differenceZ = pt1z - pt2z;
  62.         area = differenceX * differenceZ;
  63.         break;
  64.  
  65.       default:
  66.         differenceY = pt2y - pt1y;
  67.         differenceZ = pt1z - pt2z;
  68.         area = differenceY * differenceZ;
  69.         break;
  70.     }
  71.  
  72.     area = Math.Round( area, 2 );
  73.  
  74.     if( area < 0 )
  75.     { area = area * ( -1 ); }
  76.  
  77.     TaskDialog.Show( "Площадь", area.ToString() );
  78.   }

Ответ: Ваш вопрос и код затрагивает кучу проблем и советов. Многие из общего характера.

Использование отладчика

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

Возможно проблема сразу станет ясной.

Поместить транзакцию в блок ‘using’

Самый простой и безопасный способ работы с транзакциями в Revit API – заключить каждую транзакцию и группу транзакций в блок ‘using’. В этом случае транзакция автоматически будет уничтожена и выполнен откат транзакции, в случае отсутствия явного подтверждения транзакции.

Команда «Только для чтения»

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

Создание плоскости необходимо производить в транзакции. А раз в этом нет необходимости, то всю команду можно сделать read-only.

Никогда не сравнивать числа с плавающей запятой с помощью оператора “==”

Вы используете явный оператор сравнения «==» для сравнения выбранных координат.

Я бы рекомендовал никогда так не делать, так как они могу различаться на ничтожно малую величину. В этом случае оператор сравнения вернет «ложь», хотя они координаты практически идентичны c допустимой погрешностью. Нужно использовать неточной сравнение для чисел с плавающей запятой. Взгляните на эту тему – тестирование сравнение чисел с плавающей запятой (на англ.)

Это кстати может быть одной из причин, почему ваш код не работает так как надо.

Используйте класс XYZ вместо отдельных переменных X, Y, Z

Ваш код может стать короче и легче для чтения и понимания, если бы вы использовали класс XYZ, вместо того, чтобы обращаться к 3D координатам X, Y, Z по отдельности.

Например, для вычисления вектора v между двумя заданными точками p1 и p2, можно просто воспользоваться строкой:

v=p2 – p1;

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

Метод выбора точки выбрасывает исключение при отмене

Методы Revit API, которые позволяют пользователю выбирать точки или объекты в модели, выбрасывают исключение, если пользователь отменил операцию выбора.

К сожалению, это совершенно противоречит правилу, что исключения должны возникать лишь действительно в исключительных ситуациях.

Тем не менее, я всегда добавляю конструкцию try-catch при выборе точки или объекта с перехватом исключения Autodesk.Revit.Exceptions.OperationCanceledException. Оно возникает в случае отмены операции и в этом случае имеет смысл возвратить Result.Cancelled или Result.Failed в зависимости от ситуации.

Серьезные предложения по улучшению кода

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

Более правильным решением будет проекция двух выбранных точек на рабочую плоскость (так как они выбираются на плане, то они по-любому должны находится на ней) и определить их UV координаты на этой плоскости.

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

Вы должны использовать что то типа метода Face.Project, который проецирует XYZ точку на поверхность и возвращает объект типа IntersectionResult, из которого можно получить двумерные координаты точки.

К сожалению, Revit API не содержит такого метода для класса Plane, поэтому нам нужно создать его самостоятельно.

В качестве альтернативного варианта, но немного излишнего и сложного, можно предложить использовать функциональность AutoCAD ARX в Revit.

Там есть метод Geometry.AcGe.Helper.orthoProjectIntoPlane, который делает как раз то, что нам необходимо.

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

Реализация методом расширения

Первым делом я реализовал три вспомогательных метода расширения для класса Plane:

  • SignedDistanceTo – вычисляет расстояние от рабочей плоскости до заданной точки

  • ProjectOnto – проецирует заданную трехмерную точку на рабочую плоскость

  • ProjetInto - проецирует заданную трехмерную точку на рабочую плоскость и возвращает двумерные координаты в координатной системе плоскости

Для реализации методов расширения, нужно создать статический класс со статическим методом, принимающем в качестве параметра указатель на класс, который вы расширяете с ключевым словом ‘this’. В моем случае, я добавил класс JtPlaneExtensionMethods  в файл Util.cs в примеры The Building Coder.

Математическое определение плоскости и определение плоскости в Revit

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

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

Следовательно, в трёхмерную рабочую плоскость уже встроена двумерная система координат.

Расстояние от трёхмерной точки до плоскости

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

Что же такое скалярное произведение?

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

 

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

Вот моя реализация этого алгоритма:

Код - C#: [Выделить]
  1.   /// <summary>
  2.   ///  Вычисляет расстояние между точкой и плоскостью
  3.   /// </summary>
  4.   public static double SignedDistanceTo(
  5.     this Plane plane,
  6.     XYZ p )
  7.   {
  8.     Debug.Assert(
  9.       Util.IsEqual( plane.Normal.GetLength(), 1 ),
  10.       "ожидался нормализованный вектор плоскости" );
  11.  
  12.     XYZ v = p - plane.Origin;
  13.  
  14.     return plane.Normal.DotProduct( v );
  15.   }

Проекция трехмерной точки на поверхность

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

Код - C#: [Выделить]
  1.   /// <summary>
  2.   /// Проецирует заданную точку на поверхность
  3.   /// </summary>
  4.   public static XYZ ProjectOnto(
  5.     this Plane plane,
  6.     XYZ p )
  7.   {
  8.     double d = plane.SignedDistanceTo( p );
  9.  
  10.     XYZ q = p + d * plane.Normal;
  11.  
  12.     Debug.Assert(
  13.       Util.IsZero( plane.SignedDistanceTo( q ) ),
  14.       "Ожидалась точка на поверхности (нулевое расстояние до поверхности)" );
  15.  
  16.     return q;
  17.   }

Проекция трехмерной точки внутрь поверхности

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

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

Ее можно вычислить с помощью скалярного произведения вектора между исходной точкой поверхности и проецированной точкой с векторами X и Y соответственно. Эти два вектора определяют направление координатных осей U и V на поверхности плоскости:

Код - C#: [Выделить]
  1.   /// <summary>
  2.   /// Проецирует точку на поверхность ,
  3.   /// возвращая UV координаты в системе координат плоскости
  4.   /// </summary>
  5.   public static UV ProjectInto(
  6.     this Plane plane,
  7.     XYZ p )
  8.   {
  9.     XYZ q = plane.ProjectOnto( p );
  10.     XYZ o = plane.Origin;
  11.     XYZ d = q - o;
  12.     double u = d.DotProduct( plane.XVec );
  13.     double v = d.DotProduct( plane.YVec );
  14.     return new UV( u, v );
  15.   }

Выбранные точки для вычисления площади

С помощью этих вспомогательных методов можно реализовать ваш метод SetWorkPlaneAndPickPointsForArea например вот так (я переименовал его в PickPointsForArea, так как создавать поверхность больше не нужно):

Код - C#: [Выделить]
  1.   public void PickPointsForArea(
  2.     UIDocument uidoc )
  3.   {
  4.     Document doc = uidoc.Document;
  5.     View view = doc.ActiveView;
  6.  
  7.     XYZ p1, p2;
  8.  
  9.     try
  10.     {
  11.       p1 = uidoc.Selection.PickPoint(
  12.         "Выберите первую точку" );
  13.  
  14.       p2 = uidoc.Selection.PickPoint(
  15.         "Выберите вторую точку" );
  16.     }
  17.     catch( Autodesk.Revit.Exceptions.OperationCanceledException )
  18.     {
  19.       return;
  20.     }
  21.  
  22.     Plane plane = view.SketchPlane.GetPlane();
  23.  
  24.     UV q1 = plane.ProjectInto( p1 );
  25.     UV q2 = plane.ProjectInto( p2 );
  26.     UV d = q2 - q1;
  27.  
  28.     double area = d.U * d.V;
  29.  
  30.     area = Math.Round( area, 2 );
  31.  
  32.     if( area < 0 )
  33.     {
  34.       area = area * ( -1 );
  35.     }
  36.  
  37.     TaskDialog.Show( "Площадь", area.ToString() );
  38.   }

Обновленные примеры

Приведенный выше код я добавил в примеры The Building Coder.

Как всегда, вы их можете скачать на GitHub. Версия примеров, обсуждаемая в статье – 2015.0.111.2

Если же вы не хотите качать целиком все примеры, то вот прямые ссылки на классы Util и CmdPickPoint3d.

Источник: http://thebuildingcoder.typepad.com/blog/2014/09/planes-projections-and-picking-points.html

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

Опубликовано 29.09.2014