Что происходит с размером STEP-файла?

/ Просмотров: 407

Удивительная история приключилась на днях с конвертацией данных.

Модель дизельного двигателя Branco BDA.

При чтении и последующей перезаписи произвольно взятой модели STEP-транслятором библиотеки OpenCascade, размер файла увеличился с 96 до 196 мегабайт. Изменение размера как таковое логично, поскольку данный файл не был произведен библиотекой OpenCascade (по всей видимости ядром являлся Parasolid), а в качестве конвертера выступали инструменты STEP Tools, но... Сто мегабайт сверху!

Выяснилось, что оригинальный STEP содержит ~1,285 тыс. объектов, тогда как в перезаписаном файле объектов стало ~2,248 тыс. Возникает вопрос: что это за дополнительный миллион (!) непрошенных сущностей?

Количество объектов в оригинальном файле.
Количество объектов в заново сохраненном файле.
Заметим, что перезапись того же файла в SolidWorks 2017 дала на выходе ~218 мегабайт и ~1,903 объектов модели STEP, то есть наблюдаемый эффект не специфичен для OpenCascade.

Начнем с того, как узнать количество объектов в файле STEP. Для этого можно заглянуть непосредственно в файл (благо, он читабелен) и найти в нем самый старший идентификатор. Другой способ состоит в том, чтобы узнать количество объектов программно. Сделать это можно после чтения файла и перед его трансляцией в топологическую модель OpenCascade. Вот пример:

STEPControl_Reader reader;
  
// Read file.
if ( reader.ReadFile( filename.ToCString() ) == IFSelect_RetDone )
{
  // Get STEP model.
  Handle(StepData_StepModel) stepModel = reader.StepModel();
  Interface_EntityIterator entIt = stepModel->Entities();
  
  // Print number of entities.
  std::cout << "Read " << entIt.NbEntities() " << entities from STEP file." << std::endl;
}

Обратите внимание на структуру данных StepData_StepModel. Она содержит объектную модель файла STEP как граф взаимосвязей между различными сущностями. В этой модели нет реальной геометрии CAD, а есть только ее описание, зеркалирующее состав оригинального файла. На следующем этапе содержимое StepData_StepModel будет интерпретироваться транслятором для воссоздания геометрической модели с ассоциированными метаданными.

// Transfer all roots into one shape or into several shapes.
try
{
  reader.TransferRoots();
}
catch ( Standard_Failure )
{
  std::cout << "Warning: exception occurred during translation." << std::endl;
}
if ( reader.NbShapes() <= 0 )
{
  std::cout << "Error: transferring STEP to BREP failed." << std::endl;
  return false;
}
  
TopoDS_Shape result = reader.OneShape();

После трансляции можно проверить состав CAD-модели "result" уже в терминах ее реально воссозданных граничных элементов.

Количество топологических объектов в оригинальном файле.

Существенной разницы между двумя файлами эта проверка не выявляет.

Количество топологических объектов в заново сохраненном файле.

Используя программный инструментарий, нетрудно собрать полную информацию о количестве и составе объектов STEP-модели еще до этапа трансляции. Ниже выведен дамп итоговой модели (той, которую «разнесло»):

StepVisual_PresentationStyleAssignment : 371
StepGeom_Vector : 27132
StepBasic_ProductContext : 1
StepShape_EdgeLoop : 25420
StepGeom_GeomRepContextAndGlobUnitAssCtxAndGlobUncertaintyAssCtx : 1
StepGeom_Ellipse : 1633
StepGeom_Axis1Placement : 1
StepShape_ManifoldSolidBrep : 368
StepBasic_ApplicationContext : 1
StepGeom_Line : 26095
StepShape_FaceBound : 25420
StepGeom_BSplineSurfaceWithKnots : 621
StepVisual_ColourRgb : 6
StepGeom_SurfaceOfRevolution : 1
StepBasic_ApplicationProtocolDefinition : 1
StepShape_ClosedShell : 385
StepGeom_Circle : 14922
StepGeom_SphericalSurface : 543
StepShape_AdvancedFace : 22414
StepGeom_Plane : 9677
StepGeom_BSplineSurfaceWithKnotsAndRationalBSplineSurface : 2691
StepVisual_SurfaceSideStyle : 371
StepGeom_BSplineCurveWithKnots : 15503
StepBasic_ProductRelatedProductCategory : 1
StepBasic_ProductDefinitionContext : 1
StepBasic_UncertaintyMeasureWithUnit : 1
StepShape_ShapeDefinitionRepresentation : 1
StepGeom_Direction : 96373
StepGeom_Axis2Placement3d : 34620
StepRepr_ProductDefinitionShape : 1
StepGeom_SurfaceOfLinearExtrusion : 1037
StepGeom_CartesianPoint : 1723968
StepGeom_ConicalSurface : 1065
StepShape_EdgeCurve : 57458
StepBasic_ProductDefinitionFormation : 1
StepShape_VertexPoint : 36637
StepShape_BrepWithVoids : 3
StepGeom_BSplineCurveWithKnotsAndRationalBSplineCurve : 343
StepBasic_SiUnitAndSolidAngleUnit : 1
StepBasic_ProductDefinition : 1
StepVisual_FillAreaStyleColour : 371
StepBasic_SiUnitAndPlaneAngleUnit : 1
StepVisual_StyledItem : 371
StepVisual_MechanicalDesignGeometricPresentationRepresentation : 1
StepGeom_CylindricalSurface : 5429
StepShape_OrientedEdge : 114916
StepVisual_SurfaceStyleFillArea : 371
StepVisual_PresentationLayerAssignment : 1
StepVisual_SurfaceStyleUsage : 371
StepShape_OrientedClosedShell : 14
StepBasic_SiUnitAndLengthUnit : 1
StepVisual_FillAreaStyle : 371
StepShape_ShapeRepresentation : 1
StepBasic_Product : 1
StepGeom_ToroidalSurface : 1350

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

  1. Исчезли объекты типа StepShape_VertexLoop.
  2. Все объекты StepShape_FaceOuterBound были преобразованы в StepShape_FaceBound.
  3. Количество нерациональных B-кривых (StepGeom_BSplineCurveWithKnots) возросло с 11743 до 15503.
  4. Количество точек в пространстве моделирования (StepGeom_CartesianPoint) возросло с 781,352 до 1,723,968!
  5. И некоторые другие изменения.
Точки (StepGeom_CartesianPoint), прочитанные из оригинального STEP-файла.

В данном случае особенно подозрительным выглядит возросшее количество точек (StepGeom_CartesianPoint). Это может быть связано с тем, что какие-то точки, например, дублируются, либо, что менее очевидно, могла увеличиться сложность B-кривых и B-поверхностей. Например, вставка нового узла влечет образование дополнительной контрольной точки в B-кривой. В формате STEP это отразится возникновением новой сущности StepGeom_CartesianPoint вместе со ссылкой на нее из соответствующего определения B-кривой. Но пока это только догадки.

Начнем с проверки «в лоб». Для анализа пространственных точек на совпадение с контролируемой точностью можно использовать класс NCollection_CellFilter библиотеки OpenCascade. Для этого сначала реализуется собственно код проверки, например, вот так (обратите внимание на наследование от NCollection_CellFilter_InspectorXYZ):

//! Auxiliary class to search for coincident spatial points.
class InspectXYZ : public NCollection_CellFilter_InspectorXYZ
{
public:
  
  typedef gp_XYZ Target;
  
  //! Constructor accepting resolution distance and point.
  InspectXYZ(const double tol, const gp_XYZ& P) : m_fTol(tol), m_bFound(false), m_P(P) {}
  
  //! eturn true/false depending on whether the node was found or not.
  bool IsFound() const { return m_bFound; }
  
  //! Implementation of inspection method.
  NCollection_CellFilter_Action Inspect(const gp_XYZ& Target)
  {
    m_bFound = ( (m_P - Target).SquareModulus() <= Square(m_fTol) );
    return CellFilter_Keep;
  }
  
private:
  
  gp_XYZ m_P;      //!< Source point.
  bool   m_bFound; //!< Whether two points are coincident or not.
  double m_fTol;   //!< Resolution to check for coincidence.
  
};

Класс InspectXYZ описывает ячейку (cell) в пространстве с центром в некоторой точке "P" и зазором "tol". Метод Inspect() анализирует принятую извне точку на попадание в данную ячейку. Если расстояние между центром ячейки и данной точкой не превосходит значения зазора, то такие точки полагаются одинаковыми. На следующем этапе мы используем класс InspectXYZ для фильтрации точек:

...
  
// Prepare iterator by STEP entities.
Interface_EntityIterator entIt = stepModel->Entities();
  
// Cell filter for Cartesian points.
const double conf = 15.0;
//
NCollection_CellFilter<InspectXYZ> NodeFilter(conf);
  
// Iterate all entities.
Handle(asiAlgo_BaseCloud<double>) pts = new asiAlgo_BaseCloud<double>;
//
for ( ; entIt.More(); entIt.Next() )
{
  const Handle(Standard_Transient)& ent     = entIt.Value();
  const Handle(Standard_Type)&      entType = ent->DynamicType();
  
  if ( ent->IsKind( STANDARD_TYPE(StepGeom_CartesianPoint) ) )
  {
    Handle(StepGeom_CartesianPoint)
      cpEnt = Handle(StepGeom_CartesianPoint)::DownCast(ent);
  
    Handle(TColStd_HArray1OfReal) coords = cpEnt->Coordinates();
    //
    if ( !coords.IsNull() && coords->Size() == 3 )
    {
      gp_XYZ xyz( coords->Value( coords->Lower() ),
                  coords->Value( coords->Lower() + 1 ),
                  coords->Value( coords->Lower() + 2) );
  
      InspectXYZ Inspect(conf, xyz);
      gp_XYZ XYZ_min = Inspect.Shift( xyz, -Precision::Confusion() );
      gp_XYZ XYZ_max = Inspect.Shift( xyz,  Precision::Confusion() );
  
      // Coincidence test.
      NodeFilter.Inspect(XYZ_min, XYZ_max, Inspect);
      const bool isFound = Inspect.IsFound();
      //
      if ( !isFound )
      {
        pts->AddElement( xyz.X(), xyz.Y(), xyz.Z() );
        
        NodeFilter.Add(xyz, xyz);
      }
    }
  }
}

Здесь asiAlgo_BaseCloud представляет собой облако точек, в которое добавляются только те тройки координат, что выдержали проверку фильтром. Используя эту технику, можно эффективно отсеивать геометрически близкие точки для решения разнообразных задач (например, аппроксимации). Управляя значением переменной "conf", мы контролируем степень разреженности облака. Поскольку в библиотеке OpenCascade совпадающими считаются точки, находящиеся друг от друга на расстоянии не более Precision::Confusion(), то будем использовать именно это значение (равное 1.e-7) для фильтрации. В результате получаем 669,750 точек, то есть более миллиона точек были отфильтрованы как совпадающие с другими. Итак, по всей видимости мы имеем дело с некоторой избыточностью объектов в модели STEP. Постараемся выяснить, а откуда данные точки вообще появились?

В оригинальном файле все B-кривые типа StepGeom_BSplineCurveWithKnots содержали 386,783 контрольных точек. В результирующем файле контрольных точек стало 1,326,619, то есть на миллион больше.

Количество контрольных точек B-кривых до и после трансляции оригинального файла.

После трансляции CAD-модели, OpenCascade выполняет серию дополнительных действий для преобразования восстановленной геометрии в корректное состояние. Этот процесс необходим для преодоления различий, существующих между геометрическими ядрами. Хотя формат STEP сам по себе является «общим знаменателем» САПР, критерии, предъявляемые CAD-системами к геометрической модели, не вполне совпадают. Системы «разговаривают на разных языках». Технически, трансляция модели в OpenCascade венчается вызовом метода ProcessShape() класса XSAlgo_AlgoContainer.

Количество контрольных точек B-кривых после трансляции оригинального файла с выключенным постпроцессингом.

Класс XSAlgo_AlgoContainer выполняет, в частности, процедуры «лечения» модели, которые и приводят к повышению сложности геометрии. Отключать эти процедуры нельзя, так как в противном случае результирующая модель окажется непригодной к дальнейшей работе. Следует, однако, разобраться, что же не так с исходной моделью. Или «болеет» сам транслятор? Выясняется, что увеличение сложности ребер связано с работой класса ShapeFix_Face и его двух режимов:

  1. FixWireMode
  2. FixMissingSeamMode

Эти режимы позволяют вставить недостающие шовные ребра и соответствующим образом преобразовать контуры грани.

Одна из «больных» граней до обработки в ShapeFix_Face.

Согласно критериям корректности моделей в OpenCascade, любая грань должна иметь замкнутый контур в своем параметрическом пространстве. Шовное ребро — это ребро, проходящее через период несущей поверхности. В пространстве моделирования ему отвечает единственная кривая, тогда как в 2D оно имеет две сопряженные параметрические кривые. В исходном STEP-файле может не быть как самих шовных ребер (в силу особенностей геометрического ядра), так и параметрических кривых вообще. Восстановление этих недостающих объектов есть прямая обязанность пост-процессора OpenCascade.

Та же грань после обработки в ShapeFix_Face.

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

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

Понятно, что копирование B-кривой означает, в частности, копирование ее контрольных точек. Именно поэтому мы и наблюдаем появление большого числа геометрически идентичных точек, которые, будучи записанными как отдельные объекты в STEP, дают заметное увеличение размера файла. В завершении отметим, что по результатам данного анализа был подготовлен отчет в официальный багтрекер. Давайте вместе делать ядро лучше.