diff --git a/Detectors/Base/CMakeLists.txt b/Detectors/Base/CMakeLists.txt index 8008736b9299a..76e4ed9f741fd 100644 --- a/Detectors/Base/CMakeLists.txt +++ b/Detectors/Base/CMakeLists.txt @@ -37,7 +37,6 @@ o2_add_library(DetectorsBase src/GlobalParams.cxx src/O2Tessellated.cxx src/TGeoGeometryUtils.cxx - src/CADGeometryUtils.cxx PUBLIC_LINK_LIBRARIES FairRoot::Base O2::CommonUtils O2::DetectorsCommonDataFormats @@ -90,6 +89,13 @@ o2_add_test( PUBLIC_LINK_LIBRARIES O2::DetectorsBase LABELS detectorsbase) +o2_add_test( + O2Tessellated + SOURCES test/testO2Tessellated.cxx + COMPONENT_NAME DetectorsBase + PUBLIC_LINK_LIBRARIES O2::DetectorsBase + LABELS detectorsbase) + if(BUILD_SIMULATION) if (NOT APPLE) o2_add_test( diff --git a/Detectors/Base/include/DetectorsBase/O2Tessellated.h b/Detectors/Base/include/DetectorsBase/O2Tessellated.h index 7a1a3945c80d8..c068194539609 100644 --- a/Detectors/Base/include/DetectorsBase/O2Tessellated.h +++ b/Detectors/Base/include/DetectorsBase/O2Tessellated.h @@ -88,6 +88,10 @@ class O2Tessellated : public TGeoBBox const TBuffer3D& GetBuffer3D(int reqSections, Bool_t localFrame) const override; void GetMeshNumbers(int& nvert, int& nsegs, int& npols) const override; int GetNmeshVertices() const override { return fNvert; } + + /// Fill \a array with \a npoints points on this solid's boundary: every vertex, then deterministic R2 samples on facet interiors. + Bool_t GetPointsOnSegments(Int_t npoints, Double_t* array) const override; + void InspectShape() const override {} TBuffer3D* MakeBuffer3D() const override; void Print(Option_t* option = "") const override; diff --git a/Detectors/Base/src/O2Tessellated.cxx b/Detectors/Base/src/O2Tessellated.cxx index 5c52625841868..d50b922cd8d25 100644 --- a/Detectors/Base/src/O2Tessellated.cxx +++ b/Detectors/Base/src/O2Tessellated.cxx @@ -484,6 +484,67 @@ void O2Tessellated::GetMeshNumbers(int& nvert, int& nsegs, int& npols) const npols = GetNfacets(); } +//////////////////////////////////////////////////////////////////////////////// +/// Fill array with npoints points on the solid's boundary. See the header. + +Bool_t O2Tessellated::GetPointsOnSegments(Int_t npoints, Double_t* array) const +{ + if (array == nullptr || npoints <= 0 || fVertices.empty()) { + return kFALSE; + } + const int vertexCount = static_cast(fVertices.size()); + if (npoints < vertexCount) { + // Hand the caller back to SetPoints(), which gives it every vertex -- more points than asked + // for, all of them exactly on the shape. + return kFALSE; + } + for (int vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { + fVertices[vertexIndex].CopyTo(&array[3 * vertexIndex]); + } + + const int extraCount = npoints - vertexCount; + const int facetCount = static_cast(fFacets.size()); + if (extraCount == 0) { + return kTRUE; + } + if (facetCount == 0) { + for (int extraIndex = 0; extraIndex < extraCount; ++extraIndex) { + fVertices[extraIndex % vertexCount].CopyTo(&array[3 * (vertexCount + extraIndex)]); + } + return kTRUE; + } + + // The same deterministic R2 low-discrepancy pair O2BVHSurfaceSolid::GetPointsOnSegments uses: + // what a shape hands out must depend on the shape and on nothing else. + constexpr double kAlpha1 = 0.7548776662466927; + constexpr double kAlpha2 = 0.5698402909980532; + for (int extraIndex = 0; extraIndex < extraCount; ++extraIndex) { + const int facetIndex = + static_cast((static_cast(extraIndex) * facetCount) / extraCount) % facetCount; + const TGeoFacet& facet = fFacets[facetIndex]; + const int facetVertices = facet.GetNvert(); + double first = std::fmod(0.5 + kAlpha1 * (extraIndex + 1), 1.); + double second = std::fmod(0.5 + kAlpha2 * (extraIndex + 1), 1.); + if (first + second > 1.) { + first = 1. - first; + second = 1. - second; + } + // A quad facet is two triangles sharing vertex 0; pick one by the parity of the sample index + // so both halves are covered. + const int cornerB = (facetVertices > 3 && (extraIndex & 1)) ? 2 : 1; + const int cornerC = (facetVertices > 3 && (extraIndex & 1)) ? 3 : ((facetVertices > 2) ? 2 : 1); + const Vertex_t& vertexA = fVertices[facet[0]]; + const Vertex_t& vertexB = fVertices[facet[cornerB]]; + const Vertex_t& vertexC = fVertices[facet[cornerC]]; + const double weightA = 1. - first - second; + double* slot = &array[3 * (vertexCount + extraIndex)]; + slot[0] = weightA * vertexA.x() + first * vertexB.x() + second * vertexC.x(); + slot[1] = weightA * vertexA.y() + first * vertexB.y() + second * vertexC.y(); + slot[2] = weightA * vertexA.z() + first * vertexB.z() + second * vertexC.z(); + } + return kTRUE; +} + //////////////////////////////////////////////////////////////////////////////// /// Creates a TBuffer3D describing *this* shape. /// Coordinates are in local reference frame. @@ -901,6 +962,27 @@ inline Vec3f triangleNormal(const Vec3f& a, const Vec3f& b, const Vec3f return normalize(cross(e1, e2)); } +/// Outward pad of every BVH leaf box, so a facet lies strictly inside the box that stands for it. +constexpr float kFacetBoxPad = 0.001f; + +/// Lowering the ray bound cannot drop a nearer facet while |origin| + |box| + distance stays below +/// this: the float rounding of ray, box and traversal then stays well inside kFacetBoxPad. +constexpr double kMaxPruneScale = kFacetBoxPad * (1 << 24) / 8.; + +/// The largest hit distance that may be used as a ray bound for this origin and root box. +template +double pruneLimit(const BBox& bbox, const double* point) +{ + double origin = 0.; + double box = 0.; + for (int index = 0; index < 3; ++index) { + origin = std::max(origin, std::abs(point[index])); + box = std::max({box, std::abs(static_cast(bbox.min[index])), + std::abs(static_cast(bbox.max[index]))}); + } + return kMaxPruneScale - origin - box; +} + } // end anonymous namespace //////////////////////////////////////////////////////////////////////////////// @@ -960,6 +1042,10 @@ Double_t O2Tessellated::DistFromOutside(const Double_t* point, const Double_t* d static constexpr bool use_robust_traversal = true; + // the ray object is ours and mutable: bvh2 re-reads tmax at every box test, so lowering it on a + // hit prunes the rest of the traversal + const double prune_limit = pruneLimit(topnode_bbox, point); + Vertex_t dir_v{dir[0], dir[1], dir[2]}; // Traverse the BVH and apply concrete object intersection in BVH leafs bvh::v2::GrowingStack stack; @@ -979,6 +1065,9 @@ Double_t O2Tessellated::DistFromOutside(const Double_t* point, const Double_t* d if (thisdist < local_step) { local_step = thisdist; + if (local_step <= prune_limit) { + ray.tmax = truncate_roundup(local_step); + } } } return false; // go on after this @@ -1023,6 +1112,10 @@ Double_t O2Tessellated::DistFromInside(const Double_t* point, const Double_t* di static constexpr bool use_robust_traversal = true; + // as in DistFromOutside: lowering the ray's own tmax on a hit prunes the rest of the traversal + const auto rootbox = mybvh->get_root().get_bbox(); + const double prune_limit = pruneLimit(rootbox, point); + Vertex_t dir_v{dir[0], dir[1], dir[2]}; // Traverse the BVH and apply concrete object intersection in BVH leafs bvh::v2::GrowingStack stack; @@ -1045,6 +1138,9 @@ Double_t O2Tessellated::DistFromInside(const Double_t* point, const Double_t* di rayTriangle(Vertex_t{point[0], point[1], point[2]}, dir_v, v0, v1, v2, 0.); if (t < local_step) { local_step = t; + if (local_step <= prune_limit) { + ray.tmax = truncate_roundup(local_step); + } } } return false; // go on after this @@ -1095,12 +1191,12 @@ void O2Tessellated::BuildBVH() const auto& v2 = fVertices[facet[1]]; const auto& v3 = fVertices[facet[2]]; BBox bbox; - bbox.min[0] = std::min(std::min(v1[0], v2[0]), v3[0]) - 0.001f; - bbox.min[1] = std::min(std::min(v1[1], v2[1]), v3[1]) - 0.001f; - bbox.min[2] = std::min(std::min(v1[2], v2[2]), v3[2]) - 0.001f; - bbox.max[0] = std::max(std::max(v1[0], v2[0]), v3[0]) + 0.001f; - bbox.max[1] = std::max(std::max(v1[1], v2[1]), v3[1]) + 0.001f; - bbox.max[2] = std::max(std::max(v1[2], v2[2]), v3[2]) + 0.001f; + bbox.min[0] = std::min(std::min(v1[0], v2[0]), v3[0]) - kFacetBoxPad; + bbox.min[1] = std::min(std::min(v1[1], v2[1]), v3[1]) - kFacetBoxPad; + bbox.min[2] = std::min(std::min(v1[2], v2[2]), v3[2]) - kFacetBoxPad; + bbox.max[0] = std::max(std::max(v1[0], v2[0]), v3[0]) + kFacetBoxPad; + bbox.max[1] = std::max(std::max(v1[1], v2[1]), v3[1]) + kFacetBoxPad; + bbox.max[2] = std::max(std::max(v1[2], v2[2]), v3[2]) + kFacetBoxPad; return bbox; }; diff --git a/Detectors/Base/src/TGeoGeometryUtils.cxx b/Detectors/Base/src/TGeoGeometryUtils.cxx index ed388c3168fd9..e0b818623f3bd 100644 --- a/Detectors/Base/src/TGeoGeometryUtils.cxx +++ b/Detectors/Base/src/TGeoGeometryUtils.cxx @@ -136,7 +136,8 @@ TGeoTessellated* MakeTessellated(const TBuffer3D& buf) } } // end anonymous namespace -///< Transform any (primitive) TGeoShape to a TGeoTessellated +///< Transform any (primitive) TGeoShape to a TGeoTessellated. +/// Display and export only: TGeoTessellated does not navigate (it is tracked as its bounding box); use O2Tessellated for transport. TGeoTessellated* TGeoGeometryUtils::TGeoShapeToTGeoTessellated(TGeoShape const* shape) { auto& buf = shape->GetBuffer3D(TBuffer3D::kRawSizes | TBuffer3D::kRaw | TBuffer3D::kCore, false); diff --git a/Detectors/Base/test/testO2Tessellated.cxx b/Detectors/Base/test/testO2Tessellated.cxx new file mode 100644 index 0000000000000..1b5838791b270 --- /dev/null +++ b/Detectors/Base/test/testO2Tessellated.cxx @@ -0,0 +1,180 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-09 + +#define BOOST_TEST_MODULE Test O2Tessellated class +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include "DetectorsBase/O2Tessellated.h" + +#include "TGeoShape.h" + +#include +#include +#include + +namespace +{ +using o2::base::O2Tessellated; +using Vertex_t = O2Tessellated::Vertex_t; + +/// A small deterministic generator, so a failing ray is reproducible from its seed alone. +class Rng +{ + public: + explicit Rng(unsigned long long seed) : mState(seed) {} + double uniform(double low, double high) + { + mState = mState * 6364136223846793005ULL + 1442695040888963407ULL; + const double unit = static_cast((mState >> 11) & ((1ULL << 53) - 1)) / static_cast(1ULL << 53); + return low + unit * (high - low); + } + + private: + unsigned long long mState; +}; + +/// Add the twelve outward-wound triangles of an axis-aligned box. +void addBox(O2Tessellated& shape, double cx, double cy, double cz, double hx, double hy, double hz) +{ + const double x0 = cx - hx, x1 = cx + hx; + const double y0 = cy - hy, y1 = cy + hy; + const double z0 = cz - hz, z1 = cz + hz; + const Vertex_t corner[8] = {{x0, y0, z0}, {x1, y0, z0}, {x1, y1, z0}, {x0, y1, z0}, {x0, y0, z1}, {x1, y0, z1}, {x1, y1, z1}, {x0, y1, z1}}; + // each quad is wound counter-clockwise seen from outside, so the facet normal points outward + const int quad[6][4] = {{0, 3, 2, 1}, {4, 5, 6, 7}, {0, 1, 5, 4}, {2, 3, 7, 6}, {1, 2, 6, 5}, {0, 4, 7, 3}}; + for (const auto& face : quad) { + shape.AddFacet(corner[face[0]], corner[face[1]], corner[face[2]]); + shape.AddFacet(corner[face[0]], corner[face[2]], corner[face[3]]); + } +} + +/// The Moeller-Trumbore distance used by O2Tessellated's leaf test, repeated here as the oracle. +double rayTriangleReference(const double* origin, const double* dir, const Vertex_t& v0, const Vertex_t& v1, + const Vertex_t& v2) +{ + constexpr double EPS = 1.e-8; + const double infinity = std::numeric_limits::infinity(); + const double e1[3] = {v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]}; + const double e2[3] = {v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]}; + const double p[3] = {dir[1] * e2[2] - dir[2] * e2[1], dir[2] * e2[0] - dir[0] * e2[2], + dir[0] * e2[1] - dir[1] * e2[0]}; + const double det = e1[0] * p[0] + e1[1] * p[1] + e1[2] * p[2]; + if (std::abs(det) <= EPS) { + return infinity; + } + const double tvec[3] = {origin[0] - v0[0], origin[1] - v0[1], origin[2] - v0[2]}; + const double invDet = 1.0 / det; + const double u = (tvec[0] * p[0] + tvec[1] * p[1] + tvec[2] * p[2]) * invDet; + if (u < 0.0 || u > 1.0) { + return infinity; + } + const double q[3] = {tvec[1] * e1[2] - tvec[2] * e1[1], tvec[2] * e1[0] - tvec[0] * e1[2], + tvec[0] * e1[1] - tvec[1] * e1[0]}; + const double v = (dir[0] * q[0] + dir[1] * q[1] + dir[2] * q[2]) * invDet; + if (v < 0.0 || u + v > 1.0) { + return infinity; + } + const double t = e2[0] * q[0] + e2[1] * q[1] + e2[2] * q[2]; + return (t * invDet > 0.) ? t * invDet : infinity; +} + +/// The unpruned answer: the nearest facet over every facet of the mesh, entering or exiting. +double bruteForce(const O2Tessellated& shape, const double* origin, const double* dir, bool entering) +{ + double best = TGeoShape::Big(); + for (int facet = 0; facet < shape.GetNfacets(); ++facet) { + const auto& description = shape.GetFacet(facet); + const Vertex_t& v0 = shape.GetVertex(description[0]); + const Vertex_t& v1 = shape.GetVertex(description[1]); + const Vertex_t& v2 = shape.GetVertex(description[2]); + const double e1[3] = {v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]}; + const double e2[3] = {v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]}; + const double normal[3] = {e1[1] * e2[2] - e1[2] * e2[1], e1[2] * e2[0] - e1[0] * e2[2], + e1[0] * e2[1] - e1[1] * e2[0]}; + const double along = normal[0] * dir[0] + normal[1] * dir[1] + normal[2] * dir[2]; + // the same facing filter the shape applies: entering facets face the ray, exiting ones face away + if (entering ? (along > 0.) : (along <= 0.)) { + continue; + } + best = std::min(best, rayTriangleReference(origin, dir, v0, v1, v2)); + } + return best; +} + +/// Eight boxes in a row, so every axial ray meets sixteen facets and the BVH has many leaves. +void buildRow(O2Tessellated& shape) +{ + for (int index = 0; index < 8; ++index) { + addBox(shape, -21. + 6. * index, 0., 0., 2., 3., 4.); + } + shape.CloseShape(true, false, false); +} +} // namespace + +BOOST_AUTO_TEST_CASE(PrunedRayQueriesEqualTheBruteForceMinimum) +{ + O2Tessellated shape("row"); + buildRow(shape); + BOOST_CHECK_EQUAL(shape.GetNfacets(), 96); + + Rng rng(20260912); + int outsideHits = 0; + int insideHits = 0; + for (int trial = 0; trial < 4000; ++trial) { + // origins inside the row and well outside it, so both directions are exercised + const double origin[3] = {rng.uniform(-40., 40.), rng.uniform(-12., 12.), rng.uniform(-12., 12.)}; + double dir[3] = {rng.uniform(-1., 1.), rng.uniform(-1., 1.), rng.uniform(-1., 1.)}; + const double norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + if (norm < 1.e-6) { + continue; + } + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + + const double outside = shape.DistFromOutside(origin, dir, 1, TGeoShape::Big(), nullptr); + const double inside = shape.DistFromInside(origin, dir, 1, TGeoShape::Big(), nullptr); + const double outsideReference = bruteForce(shape, origin, dir, true); + const double insideReference = bruteForce(shape, origin, dir, false); + + BOOST_CHECK_EQUAL(outside, outsideReference); + BOOST_CHECK_EQUAL(inside, insideReference); + outsideHits += outsideReference < TGeoShape::Big() ? 1 : 0; + insideHits += insideReference < TGeoShape::Big() ? 1 : 0; + } + // the case is only meaningful if the rays really hit the mesh; this sampling gives about 450 + // entering and 3000 exiting hits + BOOST_CHECK_GT(outsideHits, 200); + BOOST_CHECK_GT(insideHits, 200); +} + +BOOST_AUTO_TEST_CASE(APrunedRayFindsTheNearestOfManyFacetsAlongIt) +{ + O2Tessellated shape("row"); + buildRow(shape); + + // straight down the row: eight boxes, so sixteen entering and sixteen exiting facets are in line + const double origin[3] = {-40., 0., 0.}; + const double dir[3] = {1., 0., 0.}; + BOOST_CHECK_EQUAL(shape.DistFromOutside(origin, dir, 1, TGeoShape::Big(), nullptr), 17.); + BOOST_CHECK_EQUAL(shape.DistFromOutside(origin, dir, 1, TGeoShape::Big(), nullptr), + bruteForce(shape, origin, dir, true)); + + // from inside the first box, the exit is its own far face and not a later box's + const double inner[3] = {-21., 0., 0.}; + BOOST_CHECK_EQUAL(shape.DistFromInside(inner, dir, 1, TGeoShape::Big(), nullptr), 2.); + BOOST_CHECK_EQUAL(shape.DistFromInside(inner, dir, 1, TGeoShape::Big(), nullptr), + bruteForce(shape, inner, dir, false)); +} diff --git a/Detectors/CADSupport/CMakeLists.txt b/Detectors/CADSupport/CMakeLists.txt new file mode 100644 index 0000000000000..d03238b7a8761 --- /dev/null +++ b/Detectors/CADSupport/CMakeLists.txt @@ -0,0 +1,92 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +# Detectors/Base/src provides the bvh2 headers (bvh2_third_party.h, bvh2_extra_kernels.h) shared with O2Tessellated. +o2_add_library(CADSupport + SOURCES src/O2BVHSurfaceSolid.cxx + src/O2BVHAssembly.cxx + src/O2SurfaceSolidIO.cxx + src/O2OverlapCheck.cxx + src/O2SolidHarness.cxx + src/CADGeometryUtils.cxx + src/O2FlatCSG.cxx + PRIVATE_INCLUDE_DIRECTORIES ${CMAKE_SOURCE_DIR}/Detectors/Base/src + PUBLIC_LINK_LIBRARIES O2::DetectorsBase ROOT::Geom) + +o2_target_root_dictionary(CADSupport + HEADERS include/CADSupport/O2BVHSurfaceSolid.h + include/CADSupport/O2BVHAssembly.h + include/CADSupport/O2FlatCSG.h + LINKDEF src/CADSupportLinkDef.h) + +o2_add_test( + BVHSurfaceSolid + SOURCES test/testBVHSurfaceSolid.cxx + COMPONENT_NAME CADSupport + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO + LABELS cadsupport) + +o2_add_test( + BVHAssembly + SOURCES test/testBVHAssembly.cxx + COMPONENT_NAME CADSupport + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO + LABELS cadsupport) + +o2_add_test( + FlatCSG + SOURCES test/testFlatCSG.cxx + COMPONENT_NAME CADSupport + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO + LABELS cadsupport) + +o2_add_executable( + solid-harness + COMPONENT_NAME CADSupport + SOURCES test/runSolidHarness.cxx + IS_BENCHMARK + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO + nlohmann_json::nlohmann_json) + +# X-ray / geantino transport benchmark. +o2_add_executable( + xray + COMPONENT_NAME CADSupport + SOURCES test/runXRayBenchmark.cxx + IS_BENCHMARK + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::RIO + nlohmann_json::nlohmann_json) + +# Overlap census of a placed geometry. +o2_add_executable( + overlap + COMPONENT_NAME CADSupport + SOURCES test/runOverlapCensus.cxx + IS_BENCHMARK + PUBLIC_LINK_LIBRARIES O2::CADSupport ROOT::Geom ROOT::GeomPainter ROOT::RIO + nlohmann_json::nlohmann_json) + +install(PROGRAMS tools/o2-cad-to-tgeo + tools/o2-tgeo-to-cad + tools/compat/O2_CADtoTGeo.py + tools/compat/O2_TGeoToCAD.py + DESTINATION ${CMAKE_INSTALL_BINDIR}) + +install(DIRECTORY tools/ + DESTINATION ${CMAKE_INSTALL_DATADIR}/CADSupport/tools + USE_SOURCE_PERMISSIONS + PATTERN "__pycache__" EXCLUDE + PATTERN "o2-cad-to-tgeo" EXCLUDE + PATTERN "o2-tgeo-to-cad" EXCLUDE + PATTERN "compat" EXCLUDE) + +install(DIRECTORY examples/ + DESTINATION ${CMAKE_INSTALL_DATADIR}/CADSupport/examples) diff --git a/Detectors/CADSupport/README.md b/Detectors/CADSupport/README.md new file mode 100644 index 0000000000000..c40af97139c97 --- /dev/null +++ b/Detectors/CADSupport/README.md @@ -0,0 +1,271 @@ +# CAD support: STEP to TGeo and back + +`Detectors/CADSupport` converts CAD geometry exported as STEP into ROOT TGeo geometry for +simulation. It also exports TGeo geometry back to STEP. + +The converter writes one ROOT macro, `geom.C`, together with its binary payloads. The macro can be +loaded in ROOT on its own, or injected into `o2-sim` as a passive module or as a sensitive external +detector. Injection is data-driven: a JSON file tells `o2-sim` which macro to load, where to anchor +it and, for detectors, which volumes produce hits. Nothing is recompiled. + +The tutorial `doc/tutorial/index.html` walks through the whole route on the shipped `ExcavatorArm.step` +model. This file is the option reference. + +## Software setup + +The converter needs pythonOCC, which is a separate aliBuild package: + +```bash +aliBuild build pythonOCC --defaults o2 --no-system SWIG +alienv enter O2sim/latest,pythonOCC/latest +o2-cad-to-tgeo --help +o2-cad-to-tgeo --self-test +``` + +The installed wrappers `o2-cad-to-tgeo` and `o2-tgeo-to-cad` run +`$O2_ROOT/share/CADSupport/tools/O2_CADtoTGeo.py` and `O2_TGeoToCAD.py`. The example models are +installed in `$O2_ROOT/share/CADSupport/examples/`. The Geant4 NIST material table is +`$O2_ROOT/share/CADSupport/tools/g4_nist_database/G4_NIST_DB.json`. The legacy names +`O2_CADtoTGeo.py` and `O2_TGeoToCAD.py` are installed alongside them and work the same way. + +Outside the ALICE stack, a conda environment with `pythonocc-core` also works. There, run the +script from the source tree: + +```bash +conda create -n occ -c conda-forge python=3.10 pythonocc-core -y +conda activate occ +python3 $O2_SRC/Detectors/CADSupport/tools/O2_CADtoTGeo.py --help +``` + +## Convert a STEP file + +```bash +mkdir -p cad_out/excavator +o2-cad-to-tgeo $O2_ROOT/share/CADSupport/examples/ExcavatorArm.step \ + --output-folder cad_out/excavator -o geom.C --step-unit auto \ + --csg auto --exact-surfaces auto --mesh --mesh-prec 0.05 +``` + +Each leaf solid is carried by the first representation that accepts it: + +| representation | flag | shape class | payload | +| --- | --- | --- | --- | +| native ROOT CSG, or a flat CSG solid | `--csg auto\|required` | `TGeoBBox`, `TGeoTube`, ..., `TGeoCompositeShape`, `O2FlatCSG` | `shape_*.root`, `flatcsg_*.bin` | +| exact trimmed surfaces | `--exact-surfaces auto\|required` | `O2BVHSurfaceSolid` | `surfaces_*.bin` | +| triangle mesh | `--mesh` | `O2Tessellated` (`--mesh-solid o2`, default) | `facets_*.bin` | + +`off` is the default for `--csg` and `--exact-surfaces`. `auto` uses a tier where it is accepted +and falls through elsewhere. `required` stops with a report if any leaf cannot use it. Without +`--mesh`, the fallback tier emits bounding boxes. + +`--mesh-prec` sets both the linear and the angular deflection of the OCCT mesher; the default is +0.1. `--mesh-solid tgeo` emits ROOT's `TGeoTessellated`, which does not implement navigation; +use it only for a macro that must load outside O2. + +The output folder holds: + +- `geom.C`; +- the payloads above; +- `csg_report.json` (with `--csg`); +- `brep_*.brep` (with `--dump-brep`); +- `surface_report.json` (with `--surface-report PATH`). + +The macro loads its payloads relative to its own location, so move the folder as a whole. + +`geom.C` exports `get_builder_hook_unchecked()`, which `o2-sim` calls, and +`build_and_export(const char* out_root = "geom.root", bool check = true, bool checkOverlaps = false)` +for standalone use: + +```bash +(cd cad_out/excavator && root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root");') # build and export +(cd cad_out/excavator && root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root", true, true);') # also CheckOverlaps +``` + +Other conversion options: + +| option | meaning | +| --- | --- | +| `--step-unit auto\|mm\|cm\|m\|in\|ft` | STEP length unit; `auto` reads the file's declaration | +| `--recognize-surfaces exact\|off` | recover exact planes, spheres, cylinders and cones stored as NURBS (default `exact`) | +| `--surface-report PATH` | per-face classification and exact-conversion eligibility, as JSON | +| `--csg-report PATH` | where to write `csg_report.json` | +| `--max-cells N`, `--max-splits N`, `--decompose-timeout S` | raise the CSG decomposition budgets (defaults 64, 256, 60 s) | +| `--print-tree` | print the assembly tree and exit | +| `--in-field [IFIELD,FIELDM]` | take field tracking parameters from the live field (seed `2,10`) | + +## Convert part of a model + +`--include-name RE` and `--exclude-name RE` select CAD labels by regular expression. Both may be +repeated, and a matching assembly includes its whole subtree. Matching is case-insensitive unless +`--name-filter-case-sensitive` is given. + +`--clip-box XMIN YMIN ZMIN XMAX YMAX ZMAX` keeps only the geometry inside an axis-aligned box. The +box is given in STEP file units, in the assembly's world frame, with each minimum below its +maximum. + +- Solids fully outside the box are dropped. +- Solids fully inside are kept. +- Solids that straddle the boundary are intersected with the box. +- Assemblies left with no children are removed. + +`--clip-deduplicate intact` (the default) reuses shared definitions for subtrees fully inside the +box. `none` makes one volume per surviving occurrence. + +## Materials + +A bill-of-materials CSV assigns materials and, where masses and CAD volumes are both available, +effective densities. Material names are matched against the Geant4 NIST table: + +```bash +o2-cad-to-tgeo $O2_ROOT/share/CADSupport/examples/ExcavatorArm.step \ + --output-folder cad_out/excavator -o geom.C --csg auto --exact-surfaces auto --mesh \ + --materials-csv $O2_ROOT/share/CADSupport/examples/ExcavatorArm_MATERIALS.csv \ + --bom-mass-unit kg \ + --g4-nist-json $O2_ROOT/share/CADSupport/tools/g4_nist_database/G4_NIST_DB.json +``` + +Rows are read when the first two columns are `CAD,Mechanical/Part`. Their layout is +`CAD,Mechanical/Part,,,,,,...`. + +An ambiguous or missing match falls back to a simple material and leaves a comment in `geom.C`. The +matching is tuned by `--mat-min-score`, `--mat-ambiguity-delta`, `--mat-w-token`, +`--mat-w-density`, `--mat-max-log-density-diff` and `--mat-compound-penalty`. + +Geometry that came out of TGeo with `o2-tgeo-to-cad` should instead use `--media-json`. That +rebuilds the original media verbatim and takes precedence over the BOM. + +Without `--in-field`, a CAD medium has all tracking parameters zero, including `ifield`. + +## Passive geometry in `o2-sim` + +`externalGeometry.json`: + +```json +{ + "externalModules": [ + { + "name": "EXCV", + "title": "Excavator support structure from CAD", + "macro": "cad_out/excavator/geom.C", + "anchor": "barrel", + "placement": { "translation": [21.01, -13.22, -19.66], "rotation_deg": [0.0, 0.0, 0.0] } + } + ] +} +``` + +`detectorlist.json`: + +```json +{ "EXTCAD": ["EXCV"] } +``` + +```bash +o2-sim -n 1 -g boxgen --detectorList EXTCAD:detectorlist.json --extGeomFile externalGeometry.json +``` + +A module is added only when its `name` is in the active module list. `anchor` must be an existing +volume; `barrel` sits at (0, −30, 0) in the cave. `placement` is given in cm and degrees in the +anchor's frame. Several modules, each from its own `geom.C`, can be listed together: the loader compiles +each macro into its own namespace, so their identical function names do not collide. + +## Sensitive external detectors + +Use an `externalDetectors` array. It takes the same fields as a module, plus `detID` and at least +one of `sensitiveVolumes` or `sensitiveMedia`: + +```json +{ + "externalDetectors": [ + { + "name": "EXCV", + "title": "Excavator as a sensitive detector", + "macro": "cad_out/excavator/geom.C", + "anchor": "barrel", + "detID": "TST", + "sensitiveVolumes": ["Bucket"], + "placement": { "translation": [21.01, -13.22, -19.66] } + } + ] +} +``` + +- `sensitiveVolumes` and `sensitiveMedia` match **substrings** of TGeo volume and medium names. + `"Bucket"` above selects five volumes. +- `detID` is an existing detector identity that no active built-in detector uses. The default is + `ITS`. It decides the hit file, for example `o2sim_HitsTST.root`. The branch keeps the module + name, here `EXCVHit`. +- Without `sensitiveMacro`, the built-in action records one entrance/exit hit per charged track in + `o2::ext::Hit`. +- A custom action is a macro, named by `sensitiveMacro` and `sensitiveFunction`, that returns an + `o2::ext::ExternalDetector::SensitiveFcn`. It is compiled at run time and can use + `TVirtualMC::GetMC()`, `currentSensorID()`, `currentTrackID()` and `addHit()`. See + `Detectors/External/macro/sensitiveActionExample.macro`. + +In parallel mode, the hit merger reads the same `--extGeomFile` and persists the external hits. + +`run/SimExamples/External_Sensitive_Detectors` defines two detectors, `ACYL` and `BDISK`, from +hand-written macros. It needs no CAD input; run `./run.sh` there. + +## TGeo to STEP + +```bash +o2-tgeo-to-cad geometry.root out.step [--top VOLUME] [--include-name RE] [--carve-mothers] \ + [--media-json out_media.json] [--report report.json] +``` + +`o2-tgeo-to-cad --help` lists the remaining options. Converting the resulting STEP back with +`--media-json` closes the round trip. + +## Checks and validation tools + +`validation/` is not installed. Run its scripts from `$O2_SRC/Detectors/CADSupport/validation/`. + +- `root -l -b -q "$O2_SRC/Detectors/CADSupport/test/checkSurfaceSidecars.macro(\"cad_out/excavator\")"` + loads every `surfaces_*.bin` in a folder and reports closure, orientation and capacity. +- `--surface-report PATH` shows which faces are exact, recognised or unsupported. +- `validation/makeTestPartDB.py` builds a database of parts held both as surfaces and as meshes. + `o2-bench-cadsupport-solid-harness` validates and times them. See + `doc/reference/SolidNavigationHarness.md`. +- `validation/runOracleGate.py` is the acceptance gate: it converts models, samples each part and + scores it against the OpenCascade oracle. `compareGateRuns.py` compares two gate reports. +- The oracles answer from OpenCascade: `occtOracle.py` per solid, `xrayOracle.py` as crossing lists + for the X-ray benchmark (`runXRayBench.py`), and `assemblyOracle.py` volume by volume along a ray + through an assembly. `checkKnownSource.py` scores a part against the `TGeoShape` it came from. +- `validation/overlapCensus.py` sorts every pair of placed solids in a STEP assembly into + disjoint, touching or interpenetrating. +- `validation/roundTripReport.py` reports what the TGeo → STEP → TGeo round trip made of each part; + `exportSourceShapes.py` exports the source shapes it compares against. +- `validation/renderTGeo.py` raytraces a TGeo geometry through the navigator into a PNG, coloured + by representation with `--csg-report`. +- `validation/closure/` runs the same events through a TGeo module and through its STEP round trip + and compares the hits (`run_closure.sh`). +- `validation/demo/` converts ExcavatorArm into exact and tessellated geometry and compares `o2-sim` runs + over both (`convert_all.sh`, then `run_all.sh`). + +Tests and benchmarks built with the module: + +| binary | what | +| --- | --- | +| `o2-test-cadsupport-BVHSurfaceSolid` | unit tests of `O2BVHSurfaceSolid` and the sidecar reader | +| `o2-test-cadsupport-BVHAssembly` | unit tests of `O2BVHAssembly` | +| `o2-test-cadsupport-FlatCSG` | unit tests of `O2FlatCSG` | +| `o2-bench-cadsupport-solid-harness` | per-part validation and timing | +| `o2-bench-cadsupport-xray` | X-ray transport benchmark over a part database | +| `o2-bench-cadsupport-overlap` | overlap census of a placed geometry | + +## Reference documents + +`doc/reference/`: + +- `BVHSurfaceSolid.md`: the exact-surface solid and its sidecar format. +- `Design_FlatCSGSolid.md`: the flat CSG solid and its sidecar format. +- `CSG_Pipeline.md`: CSG recognition and acceptance. +- `TolerancePolicy.md`: every tolerance, with its value and reason. +- `SolidNavigationHarness.md`: the validation harness. +- `Roadmap.md`: deferred work. + +Beside them in `doc/`: + +- `known-issues.md`: open defects and limitations. +- `ideas.md`: proposals that are not yet decided. diff --git a/Detectors/CADSupport/doc/ideas.md b/Detectors/CADSupport/doc/ideas.md new file mode 100644 index 0000000000000..eca6e690a7b32 --- /dev/null +++ b/Detectors/CADSupport/doc/ideas.md @@ -0,0 +1,33 @@ +# Ideas + +Proposals for `Detectors/CADSupport` that are not yet decided. Work that has been decided on and +deferred is in `reference/Roadmap.md`; open defects are in `known-issues.md`. + +## Performance + +- Give the hot entry points hidden visibility and inline them, to undo the indirect calls the + library boundary adds. That is the standard remedy for the 4–5 % in `known-issues.md`. +- Time a flat-CSG part through `o2-bench-cadsupport-solid-harness`, so the pruning gain on + `DistFromInside` has a number of its own. +- Report `O2FlatCSG::GetUnprunedRetryCount()` from a benchmark run, so it is visible how often the + flat-CSG safety net falls back to an unpruned traversal. + +## Reach + +- Teach `tgeo2vecgeom` and VGM about the CAD solids. A converted geometry navigates under TGeo only, + so it cannot use the VecGeom or the native Geant4 navigator. +- Ship the browser viewer for the per-part reports, which lives outside this module today. +- Support free-form surfaces that no exact representation covers, instead of falling back to a mesh. + +## Testing + +- Split `test/testBVHSurfaceSolid.cxx` along its own section banners; it is larger than the code it + tests. +- Add a unit test for the axis fallback in `O2OverlapCheck`'s `containmentFlips`, which only the + overlap census exercises today. +- Give `O2FlatCSG`'s flip-containment test an assertion independent of the sampler's own rule, for + example that each sampled point lies within tolerance of a halfspace. +- Use the edge-graze fixture for the direction-sensitive `Contains` overload, which a convex box + cannot exercise. +- Move the `RepBench*` cases out of `test/testBVHSurfaceSolid.cxx` into their own test target; they + exercise `RepresentationBench.h`, not the solid. diff --git a/Detectors/CADSupport/doc/known-issues.md b/Detectors/CADSupport/doc/known-issues.md new file mode 100644 index 0000000000000..2c532b33cc6fc --- /dev/null +++ b/Detectors/CADSupport/doc/known-issues.md @@ -0,0 +1,34 @@ +# Known issues + +Open defects and limitations of `Detectors/CADSupport`. Work that has been decided on and deferred +is in `reference/Roadmap.md`; proposals that are not yet decided are in `ideas.md`. + +## Performance + +- `O2BVHSurfaceSolid` answers 4–5 % slower per query than the same code did before it moved into + `libO2CADSupport`. About half of that arrives with the library boundary itself; the remainder is + unattributed. No algorithm and no answer changed: this is measured on one part in four + representations, with every per-kernel checksum identical. +- `o2-bench-cadsupport-xray` exits with status 1 when a run has lost crossings. It predates this + module. +- `o2-bench-cadsupport-overlap --self-test` crashes. It predates this module. + +## Correctness and robustness + +- `O2BVHAssembly` builds its BVH and its bounding box lazily inside const queries, through + `EnsureBuilt`, so two threads navigating a shape read from a file can race. + `O2BVHSurfaceSolid` fills its caches in `CloseShape` and does not have this problem. + `O2BVHAssembly` has no production caller today. +- `Detectors/Base`'s `O2Tessellated` switches its ray pruning off when the ray origin plus the root + box exceeds `kMaxPruneScale` (about 2097 cm), and says nothing when it does. +- `O2OverlapCheck`'s containment-flip filter applies to `O2FlatCSG` samples only. Exact shapes keep + the safety-band filter, because a probe along a concave edge slides along the neighbouring face. +- `Detectors/Base`'s `testMatBudLUT` fails in a development build because it looks for the TPC + plugin in `lib` while the library is installed in `lib64`. It fails the same way on a clean `dev`. + +## Documentation and tooling + +- `validation/closure/roundtrip_module.sh` calls a Python interpreter through `$SW`, unlike the rest + of the suite, which resolves its interpreter through `cadsupport.occ_env`. +- `cadsupport.occ_env` picks the first architecture holding pythonOCC when `O2_ROOT`'s own + architecture has none. diff --git a/Detectors/CADSupport/doc/reference/BVHSurfaceSolid.md b/Detectors/CADSupport/doc/reference/BVHSurfaceSolid.md new file mode 100644 index 0000000000000..8e76431225f93 --- /dev/null +++ b/Detectors/CADSupport/doc/reference/BVHSurfaceSolid.md @@ -0,0 +1,397 @@ +# `O2BVHSurfaceSolid` — the exact-surface solid + +`o2::cad::O2BVHSurfaceSolid` is a `TGeoBBox`-derived shape in `libO2CADSupport`. It represents a +CAD solid by its exact boundary: a set of analytic surface patches (plane, cylinder, cone, sphere, +torus), each trimmed to its face. A BVH over boxes that cover the patches accelerates every +navigation query. It is the exact alternative to a tessellated mesh for parts whose faces are all +analytic. + +The converter `O2_CADtoTGeo.py` produces it with `--exact-surfaces auto|required`. Each exact +volume gets a sidecar `surfaces__.bin`, which the generated `geom.C` loads with +`o2::cad::LoadSurfaceSolid` (`CADSupport/O2SurfaceSolidIO.h`). + +Tolerances are listed with their values and reasons in [`TolerancePolicy.md`](TolerancePolicy.md). +The harness that validates and times the solid is described in +[`SolidNavigationHarness.md`](SolidNavigationHarness.md). + +## 1. Representation + +### 1.1 Surfaces + +The surface classes are private (`src/BoundedSurface.h`, namespace `o2::cad::surface`). All derive +from the abstract `BoundedSurface`. + +| class | carrier | parametric domain (u, v) | trim | +| --- | --- | --- | --- | +| `PlanarBoundedSurface` | plane, axes need not be orthonormal | (u, v) along axisU, axisV, cm | line-segment polygon | +| `CurvedPlanarBoundedSurface` | plane, orthonormal axes | (u, v), cm | line / arc / B-spline wires | +| `CylindricalBoundedSurface` | cylinder | (phi [rad], h [cm]) | rectangle or wire | +| `ConicalBoundedSurface` | cone, linear radius law r(h) | (phi [rad], h [cm]) | rectangle or wire | +| `SphericalBoundedSurface` | sphere | (phi [rad], theta [rad]) | rectangle or wire | +| `TorusBoundedSurface` | torus | (phiRing [rad], phiTube [rad]) | rectangle or wire | + +`phi` is measured from `referenceAxisU` projected perpendicular to the axis. `theta` is measured +from the +polar-axis pole. `phiTube` is measured around the tube from the outer equator towards +the +axis pole. A cone may have zero radius at one end (apex cone). + +A quadric or torus patch is trimmed either by a scalar parametric rectangle (phi sweep times +height, theta or tube range) or by a general wire in its (u, v) domain. With a wire, the wire is +authoritative for containment and the scalar parameters only fix the frame and a conservative +window. A wire trim may not wrap more than one full turn in any periodic angle. + +The `innerWall` flag reverses the outward normal of a quadric or torus: it then points towards the +axis, the centre or the tube spine. It marks a hole wall. + +### 1.2 Trim curves and wires + +A trim curve (`Curve2D`) is one of three kinds: + +- a line segment; +- a circular arc: centre, radius, start angle and signed sweep (a full circle is a sweep of ±2π); +- a clamped B-spline, optionally rational: degree, poles, weights, flat knot vector. + +B-splines are evaluated by de Boor. Their enclosed area is integrated by Gauss-Legendre per knot +span, which is exact for non-rational curves. Point-in-wire winding and point-to-curve distance use +one cached flattened polyline per curve. The flattener subdivides until each chord is within +`kBSplineFlatness` of the curve, judged at t = 1/4, 1/2 and 3/4 of the interval, and it never +declares an interval flat while it still contains an interior knot. + +A wire (`CurveWire` for curves, `SurfaceWire` for polygons) is one closed loop. A face has one +outer wire and any number of inner wires (holes). Wires are normalised to outer counter-clockwise +and inner clockwise; a re-orientation is logged. A wire is rejected when it is non-finite, open, +of zero area or self-touching. Consecutive curve endpoints must meet within the wire-join band, +measured as a 3D length through the surface's first fundamental form. + +### 1.3 Public construction API + +```cpp +bool AddPlanarSurface(origin, axisU, axisV, outerWire, innerWires = {}); +bool AddCurvedPlanarSurface(origin, axisU, axisV, outerWire, innerWires = {}); +bool AddCylindricalSurface(centerPoint, axis, referenceAxisU, radius, heightMin, heightMax, + phiStart = 0, phiSweep = 2pi, innerWall = false); +bool AddConicalSurface(centerPoint, axis, referenceAxisU, radiusAtMin, radiusAtMax, + heightMin, heightMax, phiStart = 0, phiSweep = 2pi, innerWall = false); +bool AddSphericalSurface(center, polarAxis, referenceAxisU, radius, thetaMin = 0, thetaMax = pi, + phiStart = 0, phiSweep = 2pi, innerWall = false); +bool AddToroidalSurface(centerPoint, axis, referenceAxisU, majorRadius, minorRadius, + phiStart = 0, phiSweep = 2pi, tubeStart = 0, tubeSweep = 2pi, + innerWall = false); +``` + +Each quadric and the torus has a second overload that appends `outerTrim` and `innerTrims` as +vectors of `PlanarBoundaryCurve`, the public mirror of `Curve2D` (`makeLine`, `makeArc`, +`makeBSpline`). `AddCurvedPlanarSurface` requires orthonormal axes; its outward normal is +axisU × axisV. + +`SetSurfaceBoundaryEdges(surfaceIndex, edgeIds, edgeFlags)` attaches the source-edge identity of a +face (section 4.2). `SetModelTolerance(cm)` records the source model's declared tolerance; zero +means "not stated". + +### 1.4 `CloseShape(check = true)` + +`CloseShape` computes the bounding box, the display mesh, the safety anchors, the BVH and the +closure diagnostics, in that order. With `check` set, closure defects are reported as `Error` +messages that state the consequence for navigation. A solid with no surfaces stays undefined and +reports `NavigationReliability::Undetermined`. + +### 1.5 The BVH + +The BVH is a `bvh::v2` float BVH over **cover boxes**, with one cover box per leaf. Each surface +supplies its cover boxes through `appendCoverBoxes`. Their union must contain both the trimmed +patch (every ray hit and on-surface point) and every point at which `distanceSqToPatch` can be +realised, so that one BVH serves the ray and the nearest-patch traversals. + +- Planes use one box. +- Cylinders and cones split their sweep into chunks of at most `kCoverChunkAngle` (π/4), each + bounded exactly. +- Spheres and tori cover the full surface of revolution, because their distance kernels project + onto the whole surface and ignore the trim. + +Every box is widened by `kBVHBoxTolerance` and rounded outward to float. Because a surface can own +several leaves, each traversal hands each surface on only once, using an epoch-stamped +`thread_local` marker. + +## 2. Queries + +All queries fall back to their loop version before `CloseShape` has built the BVH. + +### 2.1 `Contains` + +1. A point outside the bounding box (plus `kTolerance`) is outside. +2. A point within `kTolerance` of any patch is inside. Candidate patches come from a BVH + point-in-box traversal. +3. Otherwise the answer is the parity of the crossings along a fixed skew direction + (1, √2, √3), normalised. Hits within `kIntersectionTolerance` of each other form a cluster. + A cluster whose hits all enter, or all exit, is one crossing. A cluster that mixes entering and + exiting hits is a graze and counts as none. + +On a `Reliable` solid (section 4) one parity shot is the answer, unless a counted hit carried +`onTrimBoundary`. That flag means the hit lay inside its patch's on-boundary band, where the trim +test resolves the tie as "inside the trim". The solid then re-shoots. + +On any other solid, and on a re-shoot, `Contains` takes a majority vote over five golden-spiral +directions and stops once three agree. Shots that rest on a trim tie-break are counted apart and +decide only when the other shots are tied. + +### 2.2 `DistFromOutside` and `DistFromInside` + +Both call one template, `nearestCrossing`. Entering and exiting are decided by the +sign of normal · direction. The traversal is `bvh->intersect` with a leaf +lambda. As candidates are found, the ray's `tmax` shrinks to the best candidate plus a cluster +margin, rounded up by `kBVHBoxTolerance` and one float ulp. This prunes nodes beyond the best hit +without losing the hits that decide whether that candidate is a crossing or a graze. + +If the nearest candidate turns out to be a graze, the query is repeated without pruning. Hits are +accepted from `-kRayTolerance` so that a crossing at the origin is not lost. `stepmax` bounds the +traversal and the result. `DistFromOutside` first rejects a point whose gap to the bounding box +exceeds `stepmax + kBVHBoxTolerance` on any axis. + +Both follow ROOT's `iact` contract. For `iact` below 3, and with a `safe` pointer, they first +compute `Safety` into `safe`. They then return `TGeoShape::Big()` without tracing the ray for +`iact` 0, and for `iact` 1 when `stepmax` is below the safety. `iact` 3 computes no safety. + +### 2.3 `Safety` and `ComputeNormal` + +`Safety` is the exact distance to the nearest patch, found by an ordered BVH descent with a +running best. Nodes are pruned on their box distance, scaled down by (1 − 1e-12) so the bound +stays a lower bound. The running best is seeded from 24 display vertices (the safety anchors), +which lie on patches and so give an upper bound. The result is rounded down by one ulp. The `in` +argument is not used. + +Per patch, `distanceSqToPatch` is exact for planes and for untrimmed quadrics. For wire-trimmed +patches, and for sphere or torus points whose projection falls outside the trim, it is a +conservative lower bound. `Safety` is therefore always a valid underestimate. + +`ComputeNormal` uses the same traversal to find the nearest patch. It returns that +patch's outward normal, flipped to point along `dir`. + +### 2.4 `Capacity` + +`Capacity` is the absolute value of the sum of each patch's divergence-theorem contribution, +(1/3)∫X·n dA over the trimmed patch. `GetSurfaceCapacityContributions` returns the terms. + +- Polygons, curved planes without B-spline trims, and untrimmed quadrics and tori have closed forms. +- Wire-trimmed quadrics and tori integrate by Green's theorem around the trim wire, with + 20-point Gauss-Legendre per piece and pieces no wider than π/4 in u. `capacityIsExact()` is false + for them, but the result is accurate to rounding on a closed solid. +- A curved plane with a B-spline trim reports `capacityIsExact()` false. + +On an open solid, `Capacity` measures the closure defect as well as the volume. + +### 2.5 Visualisation and sampling + +Each surface emits its own display triangulation, with `kArcSamples` (24) chords per full turn. +`GetBuffer3D`, `SetPoints` and `SetSegsAndPols` use it. Navigation never depends on it; only the +safety seed reads display vertices, and only as an upper bound. + +`GetPointsOnSegments`, used by `TGeoManager::CheckOverlaps`, projects each sample back onto its +exact patch to within `kSurfacePointTolerance` (1e-11 cm). It returns `kFALSE` when fewer points +than display vertices are requested, so that ROOT falls back to `SetPoints`. + +### 2.6 Loop twins and diagnostic hooks + +`Contains_Loop`, `DistFromOutside_Loop`, `DistFromInside_Loop`, `Safety_Loop` and +`ComputeNormal_Loop` visit every surface without the BVH. They share the per-hit logic with the +accelerated queries, so the two must agree bit for bit. They serve both as the oracle and as the +performance baseline. + +Diagnostic hooks: + +- `ContainsAlongDirection` is parity along one explicit direction, without the re-shoot policy. +- `DescribeContainsCrossings` returns the crossing list of the BVH and of the loop. +- `CountBVHRayCandidates`, `HasBVH` and `GetBVHRootBounds` inspect the BVH. +- `SetRayTMaxPruning`, `ResetRayCandidateCounter` / `GetRayCandidateCount` and + `ResetSafetyCandidateCounter` / `GetSafetyCandidateCount` price the pruning. +- `SetSafetyBoundUnsoundForTest` is a negative control for the tests only. + +The measurement switches are process-wide and must not be flipped while queries run. Scratch +buffers and traversal stacks are `thread_local`, so queries allocate nothing after warm-up. The +B-spline polylines are built with their wire, so a query only reads the shared shape and is safe +to call from several threads. + +## 3. Persistence + +The solid persists the sequence of `Add*Surface` calls as `BVHSurfaceRecord`s (with their curves as +`BVHSurfaceCurveRecord`s), the source-edge identities, and the model tolerance. The custom +`Streamer` reads the records, replays them through `Add*Surface` and calls `CloseShape`, so the +closure diagnostics of a read-back solid are recomputed. A solid with no records reads back +undefined and not navigable. The class version is 3. + +## 4. Closure and navigation reliability + +Parity containment is defined only on a closed, consistently oriented 2-manifold. `CloseShape` +decides which case applies and reports it as `NavigationReliability`: + +| state | meaning | consequence | +| --- | --- | --- | +| `Undetermined` | `CloseShape` has not run, or the solid is empty | no answer is trusted | +| `Reliable` | closed and consistently oriented | single-shot parity | +| `ReversedFaces` | a shared boundary is traversed the same way by both faces | distance queries may return the wrong side | +| `OpenSurfaceSet` | a trim loop has no neighbouring face | wrong answers in the shadow of each gap | +| `NonManifold` | a trim loop runs along two or more other faces | parity is not well defined | + +The states are ordered by severity; the worst one present is reported. `IsNavigable()` is true +only for `Reliable`. A solid that is not navigable still answers every query. + +### 4.1 Rim matching (the default) + +Each face emits one 3D polyline per trim loop (a rim). Each chord midpoint of a rim is matched +against the chords of every other face. A chord counts as matched when another face's chord lies +within the rim-match tolerance plus the sampling sagitta of both chords. The rim-match tolerance is +the model tolerance, or `kRimMatchTolerance` when none is stated. The non-manifold test uses the +tolerance alone, without the sagitta. + +Reversed duplicate edges inside one face (a seam) cancel before chaining. Rims are chained by +matching endpoints. + +`GetRimReports()` returns one record per rim, with its face, loop, chord count, length, unmatched +length and state. `GetMaxRimIsolation()` is the largest distance from any chord to the nearest +chord of another face. It measures how isolated the loneliest chord is, not the width of a seam, +and it does not change with the tolerance. `GetRimChordResolution()` is the sampling floor below +which rim distances mean nothing. + +The per-chord counters (`GetBoundaryEdgeCount` and its siblings) remain as diagnostics only. + +### 4.2 Edge identity (sidecar version 3) + +When every face states its source edges, closure is decided by counting instead of by proximity. +Every edge must be used exactly twice, in opposite senses. Degenerate edges (a cone apex or sphere +pole) are excluded from the count. + +`GetMaxSharedEdgeDeviation()` then reports, as a measurement only, the largest symmetric Hausdorff +distance between the two faces' realisations of one shared edge. Each realisation is sampled at 33 +points. Only anchored edges can be measured. If any face states no edges, the rim verdict of 4.1 +applies in full. + +## 5. Surface sidecar format (`surfaces__.bin`) + +The format is written by `write_surfaces_bin` in `O2_CADtoTGeo.py` and read by +`o2::cad::LoadSurfaceSolid`. Integers are little-endian `uint32` (plus one `uint8` flag), +geometry values are little-endian `float64`, lengths are in cm and angles in radians. The converter +writes version 3. The reader accepts versions 1 to 3. + +``` +header: + char[4] magic = "O2SS" + uint32 version = 3 + uint32 nSurfaces + uint32 reserved = 0 + float64 modelTolerance # version >= 2; cm; 0 = not stated + uint32 nModelEdges # version 3; size of the solid's edge table; 0 = not stated +per surface (nSurfaces times): + uint32 surfaceType 1=plane 2=cylinder 3=cone 4=sphere 5=torus + uint32 flags bit 0: innerWall + uint32 nParams + float64 params[nParams] per-type layout below + uint32 nWires + per wire (nWires times): + uint32 wireRole 0=outer 1=inner + uint32 nEdges + per edge (nEdges times): + uint32 curveType 0=line 1=arc 2=bspline + uint32 nCurveParams + float64 curveParams[nCurveParams] + line: u0 v0 u1 v1 + arc: cu cv radius phiStart phiSweep (signed sweep; full circle = ±2π) + bspline: degree nPoles poles[2*nPoles] weights[nPoles] knots[nPoles+degree+1] + (clamped flat knot vector; weights all 1 = non-rational) + uint32 nBoundaryEdges # version 3; 0 = this face states no identity + per boundary edge (nBoundaryEdges times): + uint32 edgeId index into the solid's edge table + uint8 edgeFlags bit 0 reversed the face runs against the edge's direction + bit 1 degenerate cone apex / sphere pole: a point, no partner + bit 2 anchored entry i is trim curve i of this face +``` + +The version differences: + +- A version-1 file is a version-2 file without `modelTolerance`. The reader substitutes + 1e-6 cm and warns. +- A version-2 file is a version-3 file without `nModelEdges` and without the per-face edge block. + +Per-type `params`, in the order of the `Add*Surface` arguments: + +| type | n | layout | +| --- | --- | --- | +| plane | 9 | origin xyz, axisU xyz, axisV xyz | +| cylinder | 14 | centerPoint xyz, axis xyz, referenceAxisU xyz, radius, heightMin, heightMax, phiStart, phiSweep | +| cone | 15 | centerPoint xyz, axis xyz, referenceAxisU xyz, radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart, phiSweep | +| sphere | 14 | center xyz, polarAxis xyz, referenceAxisU xyz, radius, thetaMin, thetaMax, phiStart, phiSweep | +| torus | 15 | centerPoint xyz, axis xyz, referenceAxisU xyz, majorRadius, minorRadius, phiStart, phiSweep, tubeStart, tubeSweep | + +The reader rejects a record whose `nParams` differs from this table. + +Rules for the wire block: + +- **Planes** always carry a wire block, with exactly one outer wire. A loop made only of lines loads + through `AddPlanarSurface`. Any arc or B-spline edge routes the face through + `AddCurvedPlanarSurface`. +- **Quadrics and tori** carry no wire block when the trim is the scalar rectangle in `params`. + Otherwise the block holds one outer wire and optional holes in the patch's (u, v) domain + (section 1.1). The converter writes such a block only when the trim does not fill the (u, v) + rectangle. +- **Curved pcurves on quadrics** (circles, ellipses, Béziers, B-splines) are written as B-splines + whose poles have been pushed through the affine (u, v) → (phi, h or theta) map. This is exact, + because a B-spline is closed under affine maps. +- **Wire closure.** Consecutive edge endpoints must meet within the join band as a 3D length. The + band is the declared model tolerance, or 1e-6 cm when that is smaller or not stated. The reader + and the kernel apply the same rule. + +Rules for the edge block: + +- The boundary-edge list is written wire by wire in the file's wire order, which is + `BRepTools_WireExplorer` order — the same order as the trim curves. The reader permutes it into + the kernel's order (outer wire first). +- A face without a wire block still lists its edges, unanchored. +- Closure is decided by identity only if every surface of the solid states its edges. +- The reader refuses an `edgeId` outside the edge table when `nModelEdges` is stated. +- Each boundary-edge entry is packed without padding, 5 bytes (`uint32` + `uint8`): the writer emits + it with `struct.pack("` writes the +per-face classification without changing the output. `--dump-brep` writes the OCCT BREP of each +exact leaf, in cm, for the OCCT oracle. + +- **Stored analytic faces.** Plane, cylinder, cone, sphere and torus faces extract directly. Quadric + and torus pcurves are converted to lines or to affine-mapped B-splines. `inner_wall` follows + `TopAbs_REVERSED`. +- **Recognised faces.** A B-spline, Bézier, extrusion or revolution face is tested against plane, + sphere, cylinder and cone models. It is accepted only at a relative gap below 1e-9 + (`--recognize-surfaces exact`, the default). Its trim is rebuilt by sampling the 3D boundary + edges in the recognised frame. Every edge must then be iso-parametric: a rim or a generator. +- **Trim curves.** A B-spline trim edge that is exactly a line (collinear poles) or a circle + (relative residual below 1e-9) is stored as that curve. +- **Planar faces** accept line, circle, ellipse, B-spline and Bézier edges. Ellipses and Béziers are + converted to B-splines. +- **Model tolerance and edge table.** The model tolerance is the largest BRep tolerance of the + shape, in cm. Edge ids come from one edge table per solid. + +## 7. Known limits + +- **Free-form surfaces** (genuine B-spline or NURBS carriers) are not supported. Such parts ship as + CSG, if the CSG path accepts them, or as a mesh. +- **Recognised quadrics with non-iso trims** are refused: a face whose boundary is a slanted or + curved cut in the recognised frame falls back. The surface recogniser has no torus model. +- **The trim tie-break is one-sided.** A hit inside a patch's on-boundary band counts as inside the + trim, so a B-spline-trimmed patch can overhang its true seam by up to about `kBSplineFlatness`. + `Contains` detects this and re-shoots. `DistFromOutside`, `DistFromInside` and `ComputeNormal` + use such hits without a check. +- **Rim sampling is fixed at `kArcSamples` per turn.** Rim distances below the chord sagitta, + r(1 − cos(π/24)), cannot be resolved. The per-chord sagitta band also underestimates the + disagreement between two independently flattened polylines of one curve. +- **Two faces of one shared edge carry independent trims.** Edge identity makes the closure + verdict structural, but the geometry of the two trims is still per face. +- **`Safety` can be loose** for wire-trimmed patches and for trimmed spheres and tori. +- **Per-candidate cost** is dominated by the trim test (winding and closest point on the curve + polylines), not by the root solve. Each candidate patch also costs a virtual call. diff --git a/Detectors/CADSupport/doc/reference/CSG_Pipeline.md b/Detectors/CADSupport/doc/reference/CSG_Pipeline.md new file mode 100644 index 0000000000000..e40845fc4a29d --- /dev/null +++ b/Detectors/CADSupport/doc/reference/CSG_Pipeline.md @@ -0,0 +1,138 @@ +# The CSG pipeline + +The CSG pipeline converts a CAD leaf solid into a native ROOT CSG shape, or into `O2FlatCSG`, when +the solid can be described exactly by combining analytic carriers. It is enabled with +`O2_CADtoTGeo.py --csg auto|required`, and the code lives in `tools/cadsupport/`. + +## 1. Why CSG + +A B-rep describes a solid by its faces and by where each face stops. The "where it stops" needs +trim curves, and the intersection curve of two quadrics cannot be represented exactly in either +face's chart. A CSG description needs only the carriers and a sign for each: a point is inside +when its signs match. The intersection curve is implied by two sign tests and never represented, +so the two faces have nothing to disagree about. + +## 2. The cascade + +With `--csg auto` the converter tries three representations per leaf solid, in order: + + CSG -> exact surfaces (O2BVHSurfaceSolid) -> tessellated (O2Tessellated) + +With `--csg required`, the run stops with a report if any leaf is not CSG. `geom.C` builds the +first representation that is accepted. The other representations are still written if they were +requested, so that the validation can score every representation of a part side by side. + +The converter prints a cascade table and writes `csg_report.json`, which records each part's choice +and the evidence for it. `tools/cadsupport/decline_catalogue.py` joins that report with +`surface_report.json` into one table of the reasons a part declined each tier. + +## 3. Recognition + +`tools/cadsupport/recognise.py` proposes a description from the part's carriers. Its matchers form +a ladder from specific to general: + +1. **Elliptic or toroidal laterals:** a part with an extruded-ellipse face goes to the `TGeoEltu` + template, and one with a toroidal face to the `TGeoTorus` template. +2. **Box or prism:** a `TGeoBBox` or, for any other all-planar part, a stack of planar sections + read as `TGeoTrd1`, `TGeoTrd2`, `TGeoArb8`, `TGeoXtru` or `TGeoPgon`. +3. **Sphere:** `TGeoSphere`. +4. **One axis:** a tube, tube segment or cone (`TGeoTube`, `TGeoTubeSeg`, `TGeoCone`), and failing + that a revolved profile with any number of z sections, as a `TGeoPcon`. +5. **Two axes:** two cylinder clusters on non-parallel axes (a barrel and a lug), as + `TGeoTube ∪ TGeoTube`. A part with more than two axis clusters does not enter this rung. +6. **Single cell:** one intersection of halfspaces. +7. **Union of cells:** the decomposition of section 4, emitted as a `TGeoCompositeShape` of at most + `_PART_MAX_LEAVES` (64) leaves. +8. **Flat cells:** the same decomposition, emitted as `O2FlatCSG` + ([`Design_FlatCSGSolid.md`](Design_FlatCSGSolid.md)). + +A decline anywhere in rungs 1 to 5 passes the part to rung 6. Each of rungs 6 to 8 runs only when +the one before it has declined. + +All thresholds are relative to the part's bounding-box diagonal (`REL_TOL`, `ANG_TOL` = 1e-6). +Every unhandled structure returns a reason, not a guess. Extents come from +`BRepTools.UVBounds` of the trimmed face, not from the carrier. + +**Tier 0** (`tools/cadsupport/tier0.py`) lets a face stored as a B-spline take part as the plane, +cylinder, cone, sphere or torus it exactly is. Recognition then treats it like a natively analytic +face. + +## 4. Decomposition + +`tools/cadsupport/decompose.py` splits a part into cells: + + start from the part's connected solids; + while a piece has a trusted concave (or mixed) edge: + extend the carrier of one of the edge's faces to a full surface; + split the piece with BRepAlgoAPI_Splitter; + a piece with no trusted concave edge is one cell. + +Connected solids come first, because a part with no concave edges can still be several disjoint +pieces. The split pieces must sum to the part's volume within `VOLUME_REL_TOL` (1e-6), or the part +declines. + +The budgets apply to the whole working set: + +| budget | default | override | +| --- | --- | --- | +| `PART_MAX_CELLS` | 64 | `--max-cells` | +| `MAX_SPLITS` | 256 | `--max-splits` | +| `TIMEOUT_S` | 60 s | `--decompose-timeout` | + +## 5. Acceptance + +A proposal is shipped only if it passes the acceptance tests. The recogniser can therefore be +greedy, because the acceptance is exact. + +1. **Symmetric difference** (`tools/cadsupport/accept.py`): OCCT's + volume(candidate − original) + volume(original − candidate) must not exceed + `_BAND_FACTOR` (1.0) × model tolerance × area(original). The candidate is an OCCT realisation of + the description. +2. **False-accept guard** (`accept.contains_disagreements`): `BRepAlgoAPI_Cut` can report success + with no solid in either direction. A sampled containment comparison catches that case. +3. **Oracle gate** (`validation/runOracleGate.py`): scores the ROOT realisation of the same + description against the OCCT oracle. +4. **Known source** (`validation/checkKnownSource.py`): scores a shape against the `TGeoShape` it + was exported from, when the model came from TGeo. + +`tools/cadsupport/primitives.py` realises one description twice, with `build_occ()` and +`build_root()`. The symmetric difference and the gate therefore test the same description through +two independent builders. + +## 6. Outputs + +| file | content | +| --- | --- | +| `shape__.root` | the accepted `TGeoShape` under key `"shape"`, in cm | +| `csg__.json` | the description and its evidence | +| `flatcsg__.bin` | an `O2FlatCSG` part (format in `Design_FlatCSGSolid.md` 7.1) | +| `csg_report.json` | the per-part cascade decision | + +Writing a `.root` file needs PyROOT. When ROOT cannot be imported, only the JSON is written, and +`python3 -m cadsupport.emit --from-json ` produces the `.root` files afterwards. A part whose +`.root` file does not exist is not dispatched to CSG in `geom.C`. + +`validation/csgCensus.py` measures, per solid, which representation could apply: face types, +whether the solid is quadric-only, concave-edge counts, tier-1 template matches and Tier-0 +candidates. It uses OCCT's `ShapeAnalysis_CanonicalRecognition` only as a cross-check. Its topology +helpers are in `tools/cadsupport/census.py`. `python3 -m cadsupport.emit --self-test` runs the +package self-tests. + +## 7. Limits + +- **Free-form surfaces cannot be CSG.** A solid with a genuine B-spline face goes to the surface or + mesh tier. +- **Tangential carriers** are where splitting is least robust. A failed split declines the part to + the next tier. +- **Boundary gaps.** A convex splitter piece need not be a cell of the carrier arrangement, and such + parts decline. +- **Budgets.** Very deep booleans exceed the decomposition budgets unless they are raised. +- **Tolerance.** Acceptance is against OCCT's tolerant model, so "equal" means equal to the model + tolerance. + +## 8. Relation to `O2BVHSurfaceSolid` + +The two exact representations complement each other. CSG copes with deep boolean structure but +scales poorly with face count. The surface solid scales with face count through its BVH but has +to represent every seam as a trim curve. The cascade takes the seams the surface solid cannot +represent exactly and leaves it the many-face parts and arbitrary trims. diff --git a/Detectors/CADSupport/doc/reference/Design_FlatCSGSolid.md b/Detectors/CADSupport/doc/reference/Design_FlatCSGSolid.md new file mode 100644 index 0000000000000..7b3e4d4d7ff3d --- /dev/null +++ b/Detectors/CADSupport/doc/reference/Design_FlatCSGSolid.md @@ -0,0 +1,294 @@ +# `O2FlatCSG` — the flat-DNF halfspace solid + +`o2::cad::O2FlatCSG` is a `TGeoBBox`-derived shape that stores a solid as a union of cells, where +each cell is an intersection of signed implicit halfspaces. The depth is always two. It carries +parts that the CSG decomposition splits into cells but that are too deep to ship as a +`TGeoCompositeShape`. A BVH over sub-cell boxes keeps the cost proportional to the halfspaces that +are undecided where the query lands. + +The section numbers are referenced from `test/testFlatCSG.cxx` and `test/runXRayBenchmark.cxx`; +keep them stable. + +## 1. Why it exists + +A union of many cells shipped as a `TGeoCompositeShape` is a deep binary tree of `TGeoBoolNode`s. +Each query recurses into both children of every node, so its cost grows with the whole tree. The +tree path also has to bound each halfspace into a padded native primitive (`_cell_leaf`, sized by +`_CELL_MARGIN`), which equals the cell only near the part. + +`O2FlatCSG` stores the halfspaces themselves. There is no padding and no margin, and the cost +follows the locally undecided halfspaces. + +## 2. Scope + +The class covers the C++ shape, its sidecar IO, the flat emitter in the converter, and the `_Loop` +twins and their tests. Geant4 is out of scope (section 11). + +## 3. Representation + +### 3.1 Halfspaces + +A halfspace is `FlatCSGHalfspace { int kind; double sign; double c[11]; }`. The material side is +sign · f(x) ≤ 0, with sign = ±1. + +A quadric (`kQuadric`) stores f(x) = xᵀAx + 2bᵀx + c as ten doubles +(a00, a01, a02, a11, a12, a22, b0, b1, b2, c). Plane, sphere, cylinder, cone and elliptic cylinder +are all this one block: + +| carrier | A | 2b | c | +| --- | --- | --- | --- | +| plane, unit outward n through p | 0 | n | −n·p | +| sphere, centre p, radius r | I | −2p | \|p\|² − r² | +| cylinder, axis (p, d), radius r | I − ddᵀ | −2Ap | pᵀAp − r² | +| cone, axis (p, d), ref. radius r, k = tan α | I − (1+k²)ddᵀ | −2Ap − 2rk·d | pᵀAp + 2rk(p·d) − r² | +| elliptic cylinder, axes x̂, ŷ, semi-axes a, b | x̂x̂ᵀ/a² + ŷŷᵀ/b² | −2Ap | pᵀAp − 1 | + +A torus (`kTorus`) stores its canonical form (px, py, pz, dx, dy, dz, R, r) in the first eight +slots: centre, unit axis, major and minor radius. Inside means +sign · (√((ρ − R)² + z²) − r) ≤ 0, with ρ the distance from the axis and z the coordinate along it. +The canonical form gives both the quartic for rays and the exact signed distance for the range +bound. No reference direction is stored, because a phi limit is a plane of the same cell. + +A cell is `FlatCSGCell { int first; int count; double volume; }`, a range of the halfspace array. + +**The plane row must use a unit normal, b = n/2.** Any positive rescaling describes the same +halfspace, but only a power-of-two rescaling keeps the accelerated distances bit-identical to +their `_Loop` twins. A cell box face often lies on one of the cell's own axis-aligned planes. The +BVH then reaches that parameter as a slab bound, and the interval clipping reaches it as a +quadratic root. Both give the same double only when the scale is a power of two. The test +`the_accelerated_distances_track_their_twins_when_a_plane_is_rescaled` measures the cost of a ×3 +rescale. + +### 3.2 Fidelity + +The shipped solid is the intersection of the halfspaces the faces carry, everywhere. The tree +path, by contrast, is exact only inside its padded window. + +### 3.3 Sign convention + +The material side combines the carrier's orientation (a plane's normal is already flipped for +`TopAbs_REVERSED`) with the `side` field from `census.halfspace_side`. An inverted halfspace still +yields a solid, so the error would be silent. The self-test of `cadsupport.emit` therefore checks +every flat cell against `_cell_leaf`'s padded-primitive conjunction on a sample of points. + +## 4. The sub-cell BVH + +### 4.1 Why boxes, not cells + +The AABB of a long diagonal, curved or L-shaped cell is mostly empty. The BVH primitives are +therefore sub-boxes of cells, each with its own list of still-active halfspaces. + +### 4.2 The build + +For each cell, `CloseShape` starts from the cell box given by `SetCellBBox` and splits it +recursively at the median of the longest axis. At each box, every halfspace still active in the +parent is classified with a rigorous range bound of sign · f over the box: + +- **Quadric:** with centre m, half-extents h and g = Am + b, + |Q(x) − Q(m)| ≤ 2Σ|gᵢ|hᵢ + Σ|Aᵢⱼ|hᵢhⱼ. +- **Torus:** the signed distance is 1-Lipschitz, so f ∈ [f(m) − ‖h‖, f(m) + ‖h‖]. + +Both bounds are padded by `kPadFactor` times the magnitude accumulated when evaluating f(m). + +For each box and halfspace: + +- if min(sign · f) > 0, the box is outside the cell and is dropped; +- if max(sign · f) ≤ 0, the halfspace holds everywhere in the box and leaves its active list; +- otherwise the halfspace stays active. + +A box whose active list is empty lies wholly inside its cell. + +Splitting stops, and the box is kept, when any of these holds: + +- the active list is empty; +- the depth budget (`fSplitDepth`, 4) is spent; +- the longest side is no larger than `fMinBoxFraction` (0.05) times the part diagonal; +- the box is still far from cubic once the cubify budget (`kMaxCubifySplits`, 10 per + root-to-leaf path) is spent. + +A split is charged to the cubify budget, not the depth budget, while the box is far from cubic +(longest > 2 · max(shortest, minSize)). Halving the longest extent of a box with ratio ≤ 2 keeps +the ratio ≤ 2. A box that starts near-cubic therefore never draws on the cubify budget, and its +tree is the same as under a depth-only rule. Flooring `shortest` at `minSize` stops a flat cell +from spending the cubify budget on an axis that is never split. + +All surviving boxes of all cells go into one `bvh::v2::Bvh`. An over-wide range bound loses +pruning, never correctness. + +**The cell box is a correctness obligation on the converter.** No box is ever built outside it, +so material outside the declared box is invisible to the accelerated queries but visible to the +twins. The converter supplies the bounding box of the CAD piece the cell came from, widened by +`_FLAT_BOX_MARGIN`. It refuses the part (`_flat_box_holds_cell`) if an outward probe finds the +cell extending past that box. `emit.crosscheck_contains` then compares `Contains` against +`Contains_Loop` on the shipped shape. + +`CloseShape` refuses, and builds nothing, when a cell box is unset, inverted or non-finite. Debug +builds also sample a 5×5 grid on each box face, offset outward by 1e-6 of the diagonal, and require +every sample to be outside the cell. + +### 4.3 What the boxes buy + +- Tight boxes on long, diagonal and curved cells. +- Short active lists: a query evaluates only the few halfspaces undecided in its box. +- A rigorous `Safety` without any point-to-quadric distance formula (section 5.4). + +### 4.4 The correctness invariant + +An active list describes the cell only inside its own box. Every ray query clips the ray to a +box's slab interval before it runs the interval clipping over that box's list. Gathering active +lists across boxes and clipping once is wrong. The `_Loop` twins exist mainly to catch a violation +of this rule. + +## 5. Queries + +All accelerated queries fall back to their twin when `IsClosed()` is false. + +### 5.1 `Contains` + +`Contains` finds the boxes containing the point. A box with an empty active list answers inside at +once. Otherwise the point is inside if every active halfspace of the box satisfies +sign · f(p) ≤ 0. Cells are disjoint, so the first box that says yes decides. + +### 5.2 Distances + +Within one box, along the ray clipped to the box, the query collects the roots of every active +halfspace. A quadric gives at most two roots of αt² + 2βt + γ, with α = dᵀAd, β = dᵀ(Ao + b) and +γ = Q(o). When α ≈ 0 the equation is solved as linear. A torus gives at most four roots. The roots +are sorted, and the midpoint of each sub-interval is classified. The result is the cell's occupancy +as a set of intervals. No convexity is assumed, which is required because complemented halfspaces +make cells non-convex. + +The traversal visits the boxes the ray meets, rejoins each cell's pieces across boxes, and then +applies the tolerance rule: an interval counts only if its exit clears `TGeoShape::Tolerance()`. +This makes the result independent of the order in which boxes are visited, and equal to the twin. +`DistFromOutside` also keeps a running bound on the nearest entry and skips every box the ray +enters beyond it. Each box it keeps is still clipped to [0, step], so the answer does not change. + +- `DistFromOutside` is the first entry at t > 0. +- `DistFromInside` is the far end of the interval of the union across cells that contains t = 0. + +Both follow ROOT's `iact`/`step` contract: for `iact` below 3 they compute `Safety` first, and +`iact` 0, or `iact` 1 with `step` below the safety, returns without tracing. Scratch buffers and +traversal stacks are `thread_local`. + +### 5.3 `Capacity` + +`Capacity` is the sum of the per-cell volumes, which the converter takes from OCCT `GProp` on each +source piece. The cells are disjoint, so no inclusion–exclusion is needed. The value is inherited +from OCCT rather than computed from the shipped solid. + +### 5.4 `Safety` + +- **Outside:** the distance to the nearest box is a lower bound, because every point of the solid + lies in some box. +- **Inside:** in a box with an empty active list, the distance to that box's faces is a bound. In + an undecided box the answer is 0. + +### 5.5 The rest of the `TGeoShape` contract + +`ComputeBBox` is the union of the retained boxes, which is tighter than the union of the cell +boxes. `ComputeNormal` is the gradient of the active halfspace closest to equality: 2·sign·(Ax + b) +for a quadric, or the signed-distance gradient for a torus. It is normalised and oriented along +`dir`. Drawing follows `O2Tessellated`. + +`GetPointsOnSegments` fires deterministic rays from the boxes that carry boundary and keeps a +crossing only where `Contains` changes within `kFlipProbe` either side, so a face shared by two +cells never yields a point. The overlap check (`O2OverlapCheck`) applies the same flip test to +every `O2FlatCSG` point it samples, because `Safety` is 0 inside an undecided box and so cannot +show that a point is on the boundary. + +## 6. The `_Loop` twins + +`Contains_Loop`, `DistFromOutside_Loop` and `DistFromInside_Loop` walk all cells and all +halfspaces, without the BVH and without active lists. They define the answer, and the tests require +bit identity with the accelerated queries. `Safety_Loop` walks all boxes without the BVH. It must +equal `Safety` and must also be a sound bound. + +## 7. Persistence + +The generated `geom.C` constructs the shape and fills it with `LoadFlatCSG(file, solid)` from +`flatcsg__.bin`, then calls `CloseShape()`. The BVH and the sub-cell boxes are never +stored; they are rebuilt on load. + +The class also has an automatic ROOT streamer. A `#pragma read` rule in `CADSupportLinkDef.h` calls +`CloseShape()` on every object read, and reports an error if the build is refused. A shape read +from a file is therefore closed; only a shape built by hand needs an explicit `CloseShape()`. + +### 7.1 Flat-CSG sidecar format (`flatcsg__.bin`) + +The format is read and written by `o2::cad::LoadFlatCSG` / `WriteFlatCSG`. The production writer is +`tools/cadsupport/flat.py`; the two writers must agree byte for byte. Integers are little-endian +`int32`/`uint32`, geometry values are little-endian `float64`, and lengths are in cm. + +``` +magic char[8] "O2FLTCSG" +version uint32 1 +nHalfspaces uint32 +nCells uint32 +halfspaces nHalfspaces * { int32 kind; float64 sign; float64 c[11] } +cells nCells * { int32 first; int32 count; float64 volume; + float64 lo[3]; float64 hi[3] } +``` + +- `kind` is 0 for a quadric and 1 for a torus. `sign` is ±1. The layout of `c` is given in 3.1; + unused slots are written but ignored. +- `first` and `count` give the cell's halfspace range. The loader rejects first < 0, count ≤ 0 and + first + count > nHalfspaces. It also rejects an unknown `kind`, a non-finite coefficient and a + torus with a zero axis. +- `volume` is the OCCT volume of the source piece. +- `lo` and `hi` are the cell box passed to `SetCellBBox`: an outer bound owed by the converter. + +Records are packed without padding: 100 bytes per halfspace and 64 bytes per cell. They are read +field by field, because the natural C++ struct pads to 104 bytes. The loader checks the remaining +file length against nHalfspaces·100 + nCells·64 before reading any record, so a truncated or +overlong file is refused. `WriteFlatCSG` refuses a shape that is not closed, because its unset cell +boxes would be written as zeros. + +## 8. The converter side + +The decomposition (`tools/cadsupport/decompose.py`) splits a part into cells at trusted concave +edges. Its budget covers the whole working set of cells, pending pieces and unresolved pieces: +`PART_MAX_CELLS` = 64, `MAX_SPLITS` = 256 and `TIMEOUT_S` = 60 s by default. The converter can raise +all three with `--max-cells`, `--max-splits` and `--decompose-timeout`. + +The flat emitter (`tools/cadsupport/flat.py`) maps each carrier from `_halfspace_carriers` directly +to a quadric or torus block. The flat path has its own budgets, `_PART_MAX_FLAT_CELLS` = 256 and +`_PART_MAX_FLAT_HALFSPACES` = 1024. The tree path keeps `_PART_MAX_LEAVES` = 64. + +Ordering rules: + +- The flat path runs last, after every whole-part matcher, the single-cell reading and the + union-of-cells tree have declined. No part that another tier accepts changes representation. +- A one-piece decomposition keeps the whole-part guards: an all-planar body belongs to the prism + templates and a one-carrier body to the tier-1 templates. The flat path does not overrule them. +- A single cell is admissible in the class and in `primitives.flat_cells`. + +## 9. Open measurements + +- The crossover between flat and composite emission, in cells and in halfspaces. +- The split parameters (depth, minimum box size, cubify budget) against leaf list length, box + count, memory and query cost. +- `Safety` quality against the true distance, since a sound but weak bound costs transport steps. + +## 10. Acceptance + +A flat part ships only if it passes the same tests as any CSG part: the OCCT symmetric difference +within tolerance, the oracle gate, and `checkKnownSource.py` against the source `TGeoShape` when +one exists. The false-accept guard `accept.contains_disagreements` also runs, because +`BRepAlgoAPI_Cut` can report success with no solid in either direction. + +## 11. Risks and limits + +1. **The sign convention** is the likeliest silent error. It is mitigated by the check against + `_cell_leaf`. +2. **The clip-inside-the-box rule** is the likeliest acceleration error. It is mitigated by the + twins. +3. **The range bound is conservative.** Nearly tangent halfspaces stay undecided for many levels, + which costs boxes but never correctness. +4. **`Capacity` comes from OCCT**, not from the shipped solid. +5. **Geant4 has no direct equivalent.** A pure union of cells with all-interior halfspaces maps to + `G4MultiUnion`. The general case, with complemented halfspaces, needs a `G4VSolid` subclass + mirroring this class. +6. **Boundary-gap declines stay declined.** These are parts whose convex pieces are not cells of + the carrier arrangement. Splitting at every carrier crossing would fix them. This class makes + the resulting larger cell counts affordable. diff --git a/Detectors/CADSupport/doc/reference/Roadmap.md b/Detectors/CADSupport/doc/reference/Roadmap.md new file mode 100644 index 0000000000000..0b44637a1b859 --- /dev/null +++ b/Detectors/CADSupport/doc/reference/Roadmap.md @@ -0,0 +1,97 @@ +# Roadmap — deferred work + +This is the list of work that has been decided on but deferred. Each item states what is missing +and why it waits. Nothing here is scheduled. + +Open defects and limitations are in `../known-issues.md`; proposals that are not yet decided are in +`../ideas.md`. + +## Performance + +- **An approximate `Safety`.** `O2BVHSurfaceSolid::Safety` is always exact. Stopping the BVH descent + early, with a guaranteed underestimate, would make it cheaper. It waits because a looser safety + costs extra transport steps, which only a transport-level measurement can price. +- **Safety caching.** A per-thread cache of recent safety answers would combine with the above. + It waits on that measurement too. +- **Per-candidate trim cost.** Once the BVH has pruned, `Contains` spends most of its time in the + trim test, winding and `Curve2D::closestPoint` on B-spline polylines. The options are an exact + Bézier-clipping point-in-trim, or cover boxes subdivided in the trim domain so each candidate + carries a shorter wire. It waits because the cover boxes already cut the candidate count; the + per-candidate cost is next. +- **Optional Embree BVH engine.** It waits on Embree entering the software stack. The BVH should + sit behind an interface thin enough to swap the engine, and Embree must stay optional because + the SIMD situation differs on aarch64. +- **Ahead-of-time specialised kernels.** A converted geometry is static, so the converter could + emit per-part code with constants folded and no virtual dispatch. It waits until exact + shared-edge trims give the inner loops a small closed form. Templates over patch archetypes are + the cheaper first step. +- **A device (GPU) port of `O2FlatCSG`.** The data are already PODs in four arrays and the BVH + traversal uses an explicit stack. The remaining work is fixed-size scratch arrays instead of + `thread_local` vectors, a device BVH traversal, and a float or mixed-precision + `EvalHalfspace`. It waits on a decision about which kernel mix a device port should optimise. +- **A device port of `O2BVHSurfaceSolid`.** It needs a rewrite of the representation: the + virtual `BoundedSurface` hierarchy flattened to a tagged POD, pooled trim curves, and a + fixed-capacity hit buffer. It waits behind the `O2FlatCSG` port. + +## Coverage + +- **Free-form surfaces.** Genuine B-spline carriers are the largest remaining coverage gap. The + work is an iterative ray/surface intersector: Bézier clipping, or a BVH of Bézier sub-patches + with Newton refinement. Its benefit over a fine mesh is small, so it must be weighed against the + tessellated fallback before it is started. +- **Non-iso trims on recognised quadrics.** A NURBS face recognised as a quadric is refused when a + boundary edge is a slanted or curved cut in the recognised frame. Fixing this needs a numeric + re-fit of that edge in (phi, h). +- **Torus recognition** in the surface recogniser. The CSG path's Tier 0 already recognises tori. +- **Exact shared-edge trims.** Both faces of a shared edge should derive their trims from one object + per `TopoDS_Edge`. That removes the one-sided trim sliver and makes the rim band exact. + Sidecar v3 already carries the edge identity for the closure verdict. +- **Exact arrangement cells for trims.** This is a research-grade route to parts that neither + tier represents today. +- **The default decomposition cell budget.** `--max-cells` raises it per run. Before moving the + default, the decomposition time at higher budgets has to be measured over the deep-boolean parts. +- **Boundary-gap declines.** Splitting at every carrier crossing instead of at trusted concave edges + would convert them. It is a change to `decompose.py` with its own risk. +- **Direct `TGeoShape` → `O2FlatCSG` emitter.** It would give one flat device representation for a + whole geometry. Primitives map by template. A `TGeoCompositeShape` maps by pushing complements + down into DNF, which blows up unless redundant bounding halfspaces of subtracted tools are dropped + and empty cells are pruned with `HalfspaceRange`. The conjecture that drilled holes collapse to a + single halfspace is unverified. +- **Pcon, Pgon and Xtru round-trip bench.** Pick specimens from the Run 3 geometry, export them + with `O2_TGeoToCAD.py`, convert them back, and score the result against the source `TGeoShape`. + +## Meshing + +- **Separate linear and angular precision.** `--mesh-prec` sets both the linear and the angular + deflection, and in practice the angular one dominates. It waits on a per-volume precision + scheme, which the next item needs anyway. +- **Precision by physics relevance.** Linear deflection would be set per volume from the distance + to the interaction point or from |η|. Two cautions apply. Mesh validity is not monotone in + precision, so each volume has to be validated on its own. For far-field volumes, the acceptance + criterion should be the capacity error rather than the chordal deviation. +- **Mesh healing.** A mesh can be invalid, not just inaccurate, and chordal accuracy does not + detect that. + +## Navigation and assemblies + +- **Assembly-level transport under `TGeoNavigator`.** `assemblyOracle.py` exists; the navigator + side and a leak counter do not. It waits for the Geant integration test, which exercises the + whole geometry. +- **A face-adjacency lookup for CAD-native geometry.** Adjacent CAD parts share faces explicitly, + which could replace the per-step search among siblings with a lookup. It is unmeasured. +- **Carving with an assembly daughter.** `--carve-mothers` cannot subtract an assembly daughter. + The fix is to fuse its placed leaves into the cutter. Some mothers also fail to carve when their + daughters consume them completely. +- **A `TGeoOCCTSolid`.** OCCT itself as a shape, either as the fallback of last resort or as an + in-process oracle. It waits on checks of OCCT's thread safety and of the memory cost of resident + B-reps. +- **Overlap repair at the STEP level.** It is mechanically possible with `BRepAlgoAPI_Cut`, but + which part yields is a modelling decision per assembly. It has low priority because + `TGeoNavigator` tolerates overlaps. +- **Parity on non-manifold input.** Such parts are reported `NonManifold` and answered by vote. + The open decision is whether to reject them at `CloseShape`. + +## Tools + +- **Live event display.** Run o2-sim in service mode with warm workers and tap MCStepLogger or the + O2HitMerger channel. The batch replay comes first. diff --git a/Detectors/CADSupport/doc/reference/SolidNavigationHarness.md b/Detectors/CADSupport/doc/reference/SolidNavigationHarness.md new file mode 100644 index 0000000000000..d54496d59db9e --- /dev/null +++ b/Detectors/CADSupport/doc/reference/SolidNavigationHarness.md @@ -0,0 +1,141 @@ +# Solid navigation harness + +`o2-bench-cadsupport-solid-harness` validates and times the navigation of the CAD-derived shapes +part by part. Every part is held in more than one representation, and all of them are scored +against the same sample set. + +- **surface:** `surfaces_.bin`, loaded as `O2BVHSurfaceSolid`. +- **mesh:** `facets_.bin`, loaded as `O2Tessellated`. This is also the default sampling + reference. +- **shape:** `shape_.root`, any `TGeoShape`, as written by the CSG emitter. + +The reusable core is `CADSupport/O2SolidHarness.h` (namespace `o2::cad::harness`). It is typed on +`TGeoShape*`, so the unit tests use the same code against ROOT primitives. The front end is +`test/runSolidHarness.cxx`. + +## 1. Build a part database + +`validation/makeTestPartDB.py` runs the converter on each model and pairs the resulting sidecars +by `_`. A part enters the database only when both `surfaces_*.bin` and `facets_*.bin` +exist. It writes `/manifest.json`. + +```bash +python3 $O2_SRC/Detectors/CADSupport/validation/makeTestPartDB.py \ + --models ExcavatorArm.step as1-oc-214.stp --output +``` + +| option | default | meaning | +| --- | --- | --- | +| `--models F...` | `ExcavatorArm.step as1-oc-214.stp` | CAD files; relative names resolve against `Detectors/CADSupport/examples/` | +| `--output DIR` | `validation/test_part_db` | database directory | +| `--skip-existing` | off | reuse a model's converted directory and only re-index it | +| `--force` | off | regenerate a model's directory even if it exists | +| `--csg off\|auto\|required` | `auto` | converter CSG mode; `auto` records the per-part choice in `csg_report.json` | +| `--include-name RE` | none | passed to the converter; may be repeated | +| `--mesh-prec P` | converter default (0.1) | meshing precision passed to the converter | + +Each manifest entry holds `id`, `model`, `volume`, `lid`, `surfaces`, `facets`, `nTriangles` and +`bbox`. It also holds `shape` when a `shape_*.root` exists, and `shipped`, the representation chosen by +the converter's cascade. + +## 2. Run the harness + +```bash +o2-bench-cadsupport-solid-harness --db [options] +o2-bench-cadsupport-solid-harness --surfaces --facets [--shape ] [options] +``` + +| option | default | meaning | +| --- | --- | --- | +| `--db DIR` | — | database built by `makeTestPartDB.py` (reads `manifest.json`) | +| `--surfaces F`, `--facets F` | — | ad-hoc mode: one part given by its two sidecars | +| `--shape F` | derived | a `shape_*.root` to score as well; in `--db` mode it is taken from the manifest or derived from the `surfaces_*.bin` name | +| `--parts S` | all | only parts whose id contains the substring `S` | +| `--points N` | 5000 | point samples per part | +| `--rays N` | 5000 | ray samples per part | +| `--seed N` | 1 | sampling seed | +| `--only LIST` | `contains,distout,distin,safety` | kernels to run | +| `--loop-crosscheck` | off | also run the surface solid's `_Loop` twins and require exact agreement | +| `--pruning-ab` | off | re-run the distance kernels with ray `tmax` pruning off and report candidate counts and ns/call both ways | +| `--rims` | off | list every trim loop, not only the unmatched ones | +| `--edge-identity` | off | print the sidecar-v3 edge-identity counts and the maximum shared-edge deviation | +| `--json F` | none | write the full report as JSON | +| `--warmup N` | 1 | untimed passes before timing | +| `--repeat N` | 3 | timed passes | +| `--dump-samples D` | none | write each part's sample set to `D/samples_.json` | +| `--load-samples D` | none | read sample sets from `D` instead of generating them; `--points`, `--rays` and `--seed` are then ignored | +| `--ref-answers D` | none | validate against `D/answers_.json` from the OCCT oracle instead of the mesh | +| `-h`, `--help` | | print usage | + +A `shape_*.root` file holds one `TGeoShape` under the key `"shape"`, in cm. It may also hold an +optional `TGeoHMatrix` under `"placement"`, which takes the shape from its own frame into the part's +frame. Points and rays are transformed into the shape's frame before it is queried. + +## 3. What is measured + +**Sampling** is deterministic, given the seed and the bounding box. It draws: + +- bulk points in the inflated box; +- points within a band of the reference surface; +- points the reference calls inside; +- rays from outside points, half of them aimed at random interior points so that + `DistFromOutside` hit rates stay meaningful; +- rays from inside points. + +**Validation** compares each representation with the reference. Every disagreement is sorted into +one of four bins: + +- within the reference's own band (for the mesh, the chord sagitta); +- a missed surface (a wall missed or tunnelled through; this is never excused); +- unexplained; +- no verdict (the oracle declined). + +The worst offenders are printed with their point and direction, so each one can be reproduced. +`Safety` is checked only against its contract, 0 ≤ safety ≤ true distance, and never compared +between two shapes. + +**Timing** runs each kernel over the same sample order for every representation. A checksum of the +results stops the compiler from removing the calls. The output is ns/call and the ratio between +representations. The run also reports primitive counts, `CloseShape` time and BVH candidate counts. + +**Reliability** of each surface solid is printed per part as a `navigation:` line, and in the JSON +under `navigation`: the reliability state, whether the part is navigable, and the rim and edge +counts. Unnavigable parts are listed again at the end. An accuracy figure for a part that is not +navigable describes an incomplete solid. + +## 4. Rules for reading the output + +- **The mesh is a reference, not the truth.** It is inscribed, so on curved parts the exact solid + exits later along inside rays and enters later from outside. Mismatches within the band are + expected. +- **Compare against `O2Tessellated`, never `TGeoTessellated`.** The ROOT class does not implement + navigation and falls back to its bounding box. +- **The `_Loop` cross-check is the correctness guard that does not involve the mesh.** The BVH and + loop paths minimise over the same hits, so any difference is a traversal bug. +- **Seeds are fixed.** A number that cannot be reproduced exactly is not a measurement. +- **Look at the per-part numbers.** The spread between parts is wide, so a median alone hides it. + +## 5. OCCT oracle round trip + +The OCCT oracle gives exact answers from the part's BREP, which the converter writes with +`--dump-brep`. + +```bash +o2-bench-cadsupport-solid-harness --db --dump-samples /tmp/o +python3 $O2_SRC/Detectors/CADSupport/validation/occtOracle.py \ + --brep .brep --samples /tmp/o/samples_.json --out /tmp/o/answers_.json +o2-bench-cadsupport-solid-harness --db --ref-answers /tmp/o +``` + +With `--ref-answers`, the tolerance band is the model's declared tolerance. The oracle's own +classification of each ray origin decides which entry point is asked. A disagreement outside the +tolerance is a defect. `validation/runOracleGate.py` automates the conversion, sampling, oracle and +scoring for one model or for the fixture set. + +## 6. Profiling + +`--only` with a single kernel and one part is the entry point for `perf`: + +```bash +perf record -g o2-bench-cadsupport-solid-harness --db --parts --only distout --rays 200000 +``` diff --git a/Detectors/CADSupport/doc/reference/TolerancePolicy.md b/Detectors/CADSupport/doc/reference/TolerancePolicy.md new file mode 100644 index 0000000000000..e7a43fbd1ff64 --- /dev/null +++ b/Detectors/CADSupport/doc/reference/TolerancePolicy.md @@ -0,0 +1,192 @@ +# Tolerance policy + +This document is the register of the numerical tolerances used by the CAD support code. For each +constant it gives the value and the reason. It also states the rules the constants follow, and the +known limits of the scheme. + +## 1. Rules + +1. **Compare like with like.** A tolerance is compared only against a quantity of the same + dimension. Parametric separations on a quadric mix radians and centimetres, so they are first + converted to a 3D length through the surface's first fundamental form (section 2). +2. **Normalise algebraic guards.** Where a guard asks whether a discriminant, resolvent or + derivative is zero, the problem is normalised to be dimensionless first, and the threshold is a + multiple of the machine epsilon. Where an exact structural condition can decide instead, it is + used and no constant exists. +3. **Prefer the model's own tolerance.** When the source model declares a tolerance (sidecar + version 2 or later), it replaces the fallback constants, but never goes below the extractor + floor. +4. **Lower bounds stay lower bounds.** Every guard on a safety or pruning bound errs towards a + smaller distance. A too-small safety costs a step; a too-large one lets the navigator cross a + wall. + +## 2. The parametric metric + +`BoundedSurface::parametricMetric(uv, gUU, gUV, gVV)` gives the first fundamental form at `uv`. A +displacement (du, dv) then spans the length sqrt(gUU·du² + 2·gUV·du·dv + gVV·dv²). + +| surface | gUU | gUV | gVV | +| --- | --- | --- | --- | +| plane, curved plane | axisU·axisU | axisU·axisV | axisV·axisV | +| cylinder | r² | 0 | 1 | +| cone | r(h)² | 0 | 1 + k², with k = dr/dh | +| sphere | (R sin θ)² | 0 | R² | +| torus | (R + r cos φ_tube)² | 0 | r² | + +The form varies over the domain, so it is evaluated at the point of interest. gUU vanishes at a +sphere pole and at a cone apex; code that divides by it must handle zero. The wire-join checks in +the kernel and in the sidecar reader both go through this metric. + +## 3. Kernel constants (`src/BoundedSurface.h`) + +| constant | value | reason | +| --- | --- | --- | +| `kTolerance` | 1e-9 cm | Generic length tolerance. It sets the on-surface test and the floor of the on-boundary band for exact curves. | +| `kAreaTolerance` | 1e-18 | Parametric area below which a wire is degenerate. | +| `kRayTolerance` | 1e-9 cm | Minimum positive ray parameter for parity hits. | +| `kIntersectionTolerance` | 1e-7, relative | Two hits are one cluster if \|t1 − t2\| ≤ 1e-7·max(1, \|t1\|, \|t2\|). It is absolute below 1 cm. | +| `kClosureQuantum` | 1e-7 cm | Vertex lattice for the per-chord half-edge counters. These counters are diagnostic only and decide no verdict. | +| `kWireJoinTolerance` | 1e-6 cm | Wire-join band, as a 3D length: the extractor's endpoint precision. `wireJoinToleranceFor(t)` returns max(t, 1e-6) for a declared model tolerance t. | +| `kBSplineFlatness` | 1e-5, parametric | Chord flatness of the B-spline polyline. It is also the on-boundary band floor for B-spline trims, because the polyline is the boundary as far as winding is concerned. | +| `kRimMatchTolerance` | 1e-6 cm | Rim-matching tolerance when the model states none. Same origin as `kWireJoinTolerance`. | +| `kBVHBoxTolerance` | 1e-3 cm | Widening of every BVH cover box before outward float rounding. It must dominate every navigation length tolerance, so that a hit or on-surface point is never pruned. | +| `kQuarticEpsilon` | 32·DBL_EPSILON | Zero test for the normalised quartic solver (section 7). It is a running-error allowance for sums of three or four products of coefficients bounded by 1, not a fitted value. | +| `kArcSamples` | 24 per turn | Chord count for display meshes and rims. It must be divisible by 4 so that quarter-turn-rotated frames sample one shared circle at the same points. | +| `angularTolerance(r)` | kTolerance / max(r, kTolerance) | Angle corresponding to a `kTolerance` arc length at radius r. | +| `kCoverChunkAngle` | π/4 | Widest angular span of one cover box. A chunk's box stays within 1 − cos(π/8) (about 8%) of its arc, and a full sweep costs eight boxes. | +| `kContourQuadratureOrder` | 20 | Gauss-Legendre order of the Green's-theorem capacity integral for wire-trimmed quadrics. | +| `kContourMaxSpanU` | π/4 | Widest u-span of one contour quadrature piece. | +| `kSharedEdgeSamples` | 33 | Samples per edge in the shared-edge deviation measurement. | +| `kMaxSmoothTurn` | 0.52 rad (about 30°) | Rim vertices turning by more than this are corners and are left out of the sampling-noise estimate. A rim sampled at 24 per turn turns by 15° per vertex. | + +## 4. Solid constants (`O2BVHSurfaceSolid`) + +| constant | value | reason | +| --- | --- | --- | +| `kSurfacePointTolerance` | 1e-11 cm | Distance to its patch within which `GetPointsOnSegments` accepts a projected point. | +| `kDistanceRayTolerance` | −kRayTolerance | Distance queries accept hits from just behind the origin, so that a crossing at the origin is not lost. | +| box-distance guard | ×(1 − 1e-12) | Scales the squared point-to-box distance down, three orders above its rounding error, so it stays a lower bound. | +| anchor seed | d·(1 + 1e-12) + 1e-10 cm | Inflates the upper bound taken from the safety anchors. This stays far below `kBVHBoxTolerance`, so the winning patch is still visited. | +| safety anchors | 24 | Display vertices used to seed the nearest-patch traversal. | +| re-shoot directions | 5, majority 3 | Golden-spiral directions for the containment vote. Three directions were too few; thirteen gained little over five. | +| float ray bound | + FLT_EPSILON·\|t\| | `truncateRoundUp`: a float `tmax` is never shorter than the double bound it stands for. | + +## 5. O2Tessellated pruning constants (`Detectors/Base/src/O2Tessellated.cxx`) + +`O2Tessellated` stays in `Detectors/Base` (it is also used by `Steer/O2MCApplication`), but its BVH +ray queries follow the same pruning idea as the solid and flat-CSG traversals: lowering the ray's +own `tmax` on a hit prunes the rest of the traversal, and the constants below state how far that may +go without dropping a nearer facet. + +| constant | value | reason | +| --- | --- | --- | +| `kFacetBoxPad` | 0.001 cm | Outward pad of every BVH leaf box, so the facet it stands for lies strictly inside it. | +| `kMaxPruneScale` | `kFacetBoxPad · 2²⁴ / 8` | Largest sum of \|origin\| and \|box\| below which lowering the ray bound on a hit cannot drop a nearer facet: float rounding of ray, box and traversal then stays well inside `kFacetBoxPad`. `pruneLimit()` subtracts both from this to get the per-query cutoff. | + +## 6. IO, assembly, overlap-check, harness and flat-CSG constants + +| constant | where | value | reason | +| --- | --- | --- | --- | +| `kSidecarV1FallbackTolerance` | `O2SurfaceSolidIO.cxx` | 1e-6 cm | Model tolerance assumed for a version-1 sidecar. It is the extractor precision; the reader warns when it uses it. | +| `kBoxTolerance` | `O2BVHAssembly.cxx` | 1e-3 cm | Daughter box widening. Same value and reason as `kBVHBoxTolerance`. | +| `kSafetyBoundShare` | `O2BVHAssembly.cxx` | 1/3 | Share of a node's squared box distance that bounds a daughter's `Safety`. `TGeoBBox::Safety` returns the largest per-axis gap, which is at least the Euclidean distance over √3. | +| box-distance guard | `O2BVHAssembly.cxx` | ×(1 − 1e-12) | Same purpose as the solid's copy (section 4): scales the squared point-to-box distance down, three orders above its rounding error, so it stays a lower bound for `Safety`. | +| `kMaxRootsPerHalfspace` | `O2FlatCSG.cxx` | 4 | A quartic has at most four real roots. | +| `kMaxCubifySplits` | `O2FlatCSG.cxx` | 10 | Per-path ceiling on splits that only equalise aspect ratio. | +| `fSplitDepth` | `O2FlatCSG.h` | 4 | Sub-cell subdivision depth cap, chosen for query cost on the shipped parts. | +| `fMinBoxFraction` | `O2FlatCSG.h` | 0.05 | Minimum box size as a fraction of the part diagonal. | +| `kPadFactor` | `O2FlatCSG.cxx` | 64·DBL_EPSILON | Pads a halfspace range bound by the magnitude accumulated when evaluating it, so the bound survives cancellation. | +| debug bbox probe | `O2FlatCSG.cxx` | 1e-6 · diagonal | Outward offset of the 5×5 face samples that check a cell box contains its cell (debug builds). | +| `kFlipProbe` | `O2FlatCSG.cxx` | 1e-6 cm | Offset either side of a sampled boundary point. `GetPointsOnSegments` keeps the point only if `Contains` differs across it. | +| linear-solve cutoff | `O2FlatCSG.cxx` | \|α\| ≤ 1e-14·(\|β\|+\|γ\|) | A ray whose quadric coefficient α is this small relative to β and γ is solved as hitting a plane instead of a quadratic; the discarded root would lie beyond about 1e6 cm, outside any ALICE geometry. | +| `depthTolerance` | `O2OverlapCheck.h` | 1e-6 cm | A containment shallower than this is a shared boundary, not an overlap. | +| `residualTolerance` | `O2OverlapCheck.h` | 1e-6 cm | A sampled boundary point farther than this from its own solid's boundary is not evidence about anything and is discarded. | +| default boundary band | `O2SolidHarness.cxx` | 1e-3 · bounding-box diagonal | Fallback used when the harness config leaves `boundaryBand` unset, sizing the near-boundary sample band from the part's own extent. | + +## 7. The quartic solver + +`solveQuarticReal` (ray and torus) first substitutes x = s·y. Here s is the Cauchy root bound +max(|b|, |c|^½, |d|^⅓, |e|^¼) of the monic quartic, rounded up to a power of two. Every +coefficient then lies in [−1, 1], and the branch guards compare against `kQuarticEpsilon`. + +Scaling by a power of two is exact in binary floating point, so the normalisation changes no +rounding and no answer; only the guards change. An unrounded Cauchy bound does not have this +property. + +Two guards use structural conditions instead of a constant: + +- The Newton polishing step is taken if it is finite and no longer than 2, the root bound in + normalised units. +- `solveDepressedCubic` branches on P ≥ 0 (Cardano) versus P < 0 (trigonometric). No threshold is + needed, and P = Q = 0 returns 0 through Cardano. + +## 8. Bands built from the constants + +- **On-boundary band of a trim.** `CurveWire::boundaryBand` is the larger of `kTolerance` + (converted to parametric units through the metric's largest scale) and the wire's + `representationTolerance()`. That is `kBSplineFlatness` if any curve is a B-spline, else 0. + Winding and distance use the same polyline. A point inside the band is classified `Boundary` and + resolved as inside the trim. The hit is flagged `onTrimBoundary`, and `Contains` re-shoots when + a counted crossing carries the flag. +- **Wire join.** The 3D gap between consecutive endpoints must not exceed + `wireJoinToleranceFor(modelTolerance)`. The reader and the kernel apply the same rule. +- **Rim matching.** A chord is matched when another face's chord lies within + `rimEpsilon + own sagitta + partner sagitta`. `rimEpsilon` is the model tolerance, or + `kRimMatchTolerance`. The non-manifold test uses `rimEpsilon` alone, because at a corner a third + face legitimately comes within a chord length. +- **Rim sampling floor.** The sagitta of a rim chord is estimated from the turn angle, + (chord/2)·tan(turn/4), not from the vertex offset. A box corner would otherwise read as + sampling noise. + +## 9. Converter tolerances (Python) + +| constant | where | value | reason | +| --- | --- | --- | --- | +| `_RECOGNIZE_TOL_EXACT` | `O2_CADtoTGeo.py` | 1e-9, relative to the sample box diagonal | A recognised plane, sphere, cylinder or cone must lie on the stored surface at machine precision. | +| `_CANONICAL_CURVE_TOL` | `O2_CADtoTGeo.py` | 1e-9, relative to the curve extent | A B-spline trim edge becomes a line or circle only at machine precision. | +| `_EXTRACT_TOL` | `O2_CADtoTGeo.py` | 1e-7 | Degeneracy floor for extracted sweeps, heights, radii and areas; a record below it is not emitted. | +| `REL_TOL`, `ANG_TOL` | `cadsupport/recognise.py` | 1e-6, relative to the part diagonal | CSG template matching. CAD faces meant to coincide agree to about 1e-7 relative. | +| `REL_TOL` | `cadsupport/tier0.py` | 1e-6 | Same value as the recogniser, so that carriers and faces share one notion of "the same". | +| `TEMPLATE_REL_TOL`, `TEMPLATE_ANG_TOL` | `validation/csgCensus.py` | 1e-6 | Same, for the census. | +| `VOLUME_REL_TOL` | `cadsupport/decompose.py` | 1e-6 | The split pieces must sum to the part's volume. A breach declines the part. | +| `TANGENTIAL_SIN` | `cadsupport/census.py` | 1e-6 | Below this \|n1 × n2\| two face normals across an edge are parallel enough that the dihedral has no reliable sign; `edge_dihedral` classifies the edge `tangential` instead of convex or concave. | +| `NEAR_TANGENTIAL_SIN` | `cadsupport/census.py` | 1e-3 | Below this a `concave`/`mixed` verdict is a blend seam, not a reliable split witness: `decompose.py`'s `first_trusted_concave_edge` refuses one below it rather than cut there. | +| `_BAND_FACTOR` | `cadsupport/accept.py` | 1.0 | CSG acceptance: dV_sym ≤ factor · modelTolerance · area(original). | +| `_CELL_MARGIN` | `cadsupport/recognise.py` | 0.25 of the part diagonal | Padding of a halfspace bounded into a native primitive for tree emission. | +| `_FLAT_BOX_MARGIN` | `cadsupport/recognise.py` | 1e-3 of the part diagonal | Widening of a flat-CSG cell box, three orders above the 1e-6 agreement of the piece. | +| `_FLAT_BOX_PROBE_GRID` | `cadsupport/recognise.py` | 3 | Per-face grid of the outward probe that checks a flat cell box holds its cell. | +| `_FLAT_BOX_PROBE_OFFSETS` | `cadsupport/recognise.py` | 1e-6, 0.25, 1 and 4 box diagonals | Distances outside the box at which that probe samples. | +| `_IDENTITY_EPS` | `cadsupport/primitives.py` | 1e-12 | Frames closer than this are the same; the identity fast path needs an exact rotation. | +| `_CONE_DEGENERATE_EPS` | `cadsupport/primitives.py` | 1e-12, relative | Below this relative difference a cone's two radii are the same radius, and OCCT wants a cylinder rather than a cone. | + +## 10. Exporter tolerances (`O2_TGeoToCAD.py`) + +| constant | value | reason | +| --- | --- | --- | +| `BOOLEAN_VOLUME_TOL` | 1e-4, relative | Slack on the boolean volume invariant: a composite's exported volume must match the source TGeo volume within this fraction. | +| `_ORTHO_TOL` | 1e-6 | Band within which a hand-written rotation matrix is snapped to the nearest exact rotation and reported; outside it the matrix is refused as rigid and baked instead. | +| `_ISOMETRY_TOL` | 1e-6, relative | Relative volume band a baked isometry must preserve. | +| `EPS` | 1e-12 | Below this a tube's `rmin` is treated as zero: whether the export takes the hollow or the solid path, and whether an inner ring is even built. | + +## 11. Validation tolerances (Python) + +| constant | where | value | reason | +| --- | --- | --- | --- | +| `CAPACITY_TOLERANCE` | `validation/checkKnownSource.py` | 1e-9, relative | Capacity is compared as a relative deviation, reported as a flag rather than a failure. | +| `PROFILE_TOLERANCE` | `validation/checkKnownSource.py` | 1e-6 | The recogniser's `REL_TOL`, relative to the bounding-box diagonal. | +| `DEFAULT_SKIN_CM` | `validation/checkKnownSource.py` | 1e-9 cm | Not itself compared against anything: it is the default of `--skin`, the band within which a sampled point is too close to either shape's boundary to be scored and is counted instead. | +| `_RAY_EPS` | `validation/occtOracle.py`, `validation/xrayOracle.py`, `validation/assemblyOracle.py` | 1e-9 | Ray-intersector and classifier tolerance passed to OCCT for every oracle ray query. | + +## 12. Known limits + +- **`kBSplineFlatness` is an absolute parametric value.** On a small part the trim sliver it + permits is larger relative to the part. +- **`sameIntersection` is absolute below 1 cm.** +- **Rims use a fixed 24 chords per turn**, so rim distances below r(1 − cos(π/24)) cannot be + resolved. Sampling by a target sagitta in cm would remove this limit. +- **The sagitta band bounds each polyline against its own curve**, not against the other face's. + It underestimates the polyline-to-polyline disagreement, and tightening `kBSplineFlatness` + shrinks the band faster than it shrinks the disagreement. Deriving both faces' trims from one + shared edge object is the fix. +- **`boundaryBand` resolves `Boundary` as inside**, so the overhang is one-sided. Only `Contains` + checks for it. diff --git a/Detectors/CADSupport/doc/tutorial/index.html b/Detectors/CADSupport/doc/tutorial/index.html new file mode 100644 index 0000000000000..d4f938f6ec8f8 --- /dev/null +++ b/Detectors/CADSupport/doc/tutorial/index.html @@ -0,0 +1,1436 @@ + + + + + + +CAD to Simulation + + + + + + +
+ + + +
+ + +
+ Introduction +

Simulating ALICE geometries that come from CAD

+ +

+ Detectors are designed in CAD, but Geant transports particles through ROOT's TGeo geometry. + This guide is about crossing that gap automatically — taking an engineering model as it comes + out of the design office and turning it into something particles can be simulated through, all + the way to hits you can plot. +

+ +

+ The usual way of crossing that gap is to read the drawings and write the geometry again by hand, + in C++, volume by volume. That works, and most of ALICE was built this way, but it is slow, it is + easy to get subtly wrong, and every time the engineers move a bracket the translation has to be + redone. For a detector that is still being designed — which is exactly the situation during an + upgrade study — the hand-written geometry is out of date almost as soon as it is written. +

+ +

+ So instead we convert the CAD file directly. You export the assembly as STEP, run one converter + over it, and you get a ROOT macro that builds the geometry. From there a small JSON file tells + o2-sim to load that macro and place it in the ALICE world. Nothing is recompiled at + any point, so the loop from a new CAD revision to a new simulation takes minutes rather than + weeks. +

+ +

+ Getting the geometry in is only half of it, though. A shape that particles fly through is a + passive obstacle; to do physics you want it to record something. The second half of this + guide is therefore about the external-detector mechanism, which lets you declare parts of your + imported geometry sensitive and have them write hits — again with no detector class and no + rebuild. That is usually enough to answer the first questions an upgrade study asks: does this + thing get hit, how often, and where. +

+ +

What you will be able to do by the end

+
    +
  1. Install the converter and check that it works.
  2. +
  3. Convert a STEP assembly and look at the result.
  4. +
  5. Understand and control how faithfully each part is represented.
  6. +
  7. Attach materials, and know what the magnetic field and physics cuts will and will not do.
  8. +
  9. Place the geometry inside ALICE as passive material.
  10. +
  11. Make parts of it sensitive, run a simulation, and count hits.
  12. +
  13. Know where the system's limits are, so you do not discover them in your results.
  14. +
+ +

+ We assume you can run o2-sim, and nothing more. No CAD experience is needed, and no + knowledge of OpenCascade, which does the heavy lifting underneath but never has to be addressed + directly. +

+
+ + +
+ Start +

Install the software

+ +

+ The converter is a Python script, but it leans on OpenCascade — the CAD kernel that reads STEP + files — through its Python bindings, pythonOCC. That is the one piece you have to + provide yourself. +

+ +
+ pythonOCC is not part of O2sim +

+ It is a separate aliBuild package, and it is not pulled in when you build or + load O2sim. If you have never built it, that is genuinely step one — no amount of + loading O2sim will conjure it up. +

+
+ +

+ So we build it first. This pulls in OpenCascade itself as a dependency, and takes a while the + first time: +

+ +
+
bash
+
cd ~/alisw
+aliBuild build pythonOCC --defaults o2 --no-system SWIG
+
+ +

+ The --no-system SWIG is worth keeping even when aliBuild tells you the system SWIG + will do. The recipe asks for SWIG 4.2.1 and several distributions ship 4.2.0, which is close + enough to be picked up and not close enough to build. Forcing aliBuild to build its own costs a + few minutes once and saves a confusing failure later. +

+ +

+ With that in place, everything happens in a single shell. We load pythonOCC together + with O2sim, because the converter needs ROOT as well as OpenCascade — and the same + environment then runs o2-sim afterwards, so there is no need to switch shells + between converting and simulating: +

+ +
+
bash
+
alienv enter O2sim/latest,pythonOCC/latest
+
+ +

+ Two quick checks confirm the environment is sound. The first proves the CAD bindings import at + all; the second runs the converter's own self-test, which builds its test cases in memory and + needs no input file: +

+ +
+
bash
+
python3 -c "import OCC.Core.Bnd; print('OCC import OK')"
+o2-cad-to-tgeo --self-test
+
+ +
+
output
+
OCC import OK
+...
+20/20 in-field media checks passed
+
+ +

+ o2-cad-to-tgeo is also installed as O2_CADtoTGeo.py, and + o2-tgeo-to-cad as O2_TGeoToCAD.py, for the older command names. +

+ +
+ If the import fails with “No module named 'OCC'” +

+ Some pythonOCC installations carry a modulefile that puts the OCC + package directory itself on PYTHONPATH, rather than the site-packages + directory containing it — so Python looks inside the package and never finds it. The cure is to + drop the trailing /OCC from the prepend-path PYTHONPATH line in + $PYTHONOCC_ROOT/etc/modulefiles/pythonOCC. A recipe fix is on its way to alidist. +

+
+
+ + +
+ Start +

Convert your first model

+ +

+ Rather than start on your own detector, it is worth converting something small and known-good + first, so that anything odd later is clearly your model and not your installation. A toy + excavator arm is committed to the repository for exactly this purpose: +

+ +
+
the example model
+
$O2_ROOT/share/CADSupport/examples/ExcavatorArm.step   # 13 leaf solids, ~500 kB
+
+ +

+ It converts in seconds and is varied enough to be interesting: the hydraulic rams and pivot pins + are plain cylinders, the boom and stick are machined bodies full of concave features, and the + bucket has a torus in it. Run the converter over it, asking for all three representations at once + — we come back to what those are in the next section: +

+ +
+
bash
+
mkdir -p cad_out/excavator
+o2-cad-to-tgeo \
+    $O2_ROOT/share/CADSupport/examples/ExcavatorArm.step \
+    --output-folder cad_out/excavator \
+    -o geom.C \
+    --step-unit auto \
+    --csg auto --exact-surfaces auto --mesh --mesh-prec 0.05
+
+ +

+ That takes about thirteen seconds. Along the way the converter prints three lines worth reading + on every run, because each one catches a different common mistake: +

+ +
+
output
+
Detected STEP length unit: mm (scale to cm = 0.1)
+Placement check: 13 leaf placement(s), all at distinct world transforms.
+Emitting 13/13 logical volumes as exact O2BVHSurfaceSolid
+
+ +

+ The unit line bites hardest. TGeo works in centimetres and most CAD systems export millimetres, so + a silent unit error gives you a detector ten times too big and a simulation that still looks + almost plausible. --step-unit auto reads the declaration in the file; pass + --step-unit mm explicitly when the file declares something you do not believe. The + placement line then tells you whether two leaves landed on the same world transform, which almost + always means a duplicated part in the CAD model rather than a real coincidence. +

+ +

+ Finally the converter prints what it decided for each part, ending in a one-line summary: +

+ +
+
output
+
=== REPRESENTATION CASCADE (per leaf solid) ===
+  volume                carried by  evidence
+  BasePin               csg         TGeoTube(rmin=0, rmax=1, dz=5) [tier1-tube], dV_sym=0 cm^3
+  Base                  surface     declined CSG: 7 axis clusters: beyond the recogniser's scope ...
+  BoomCylinderOuter     csg         TGeoTube(0.6,1,7.991) u TGeoTube(0.7,1.5,1.5), dV_sym=0 cm^3
+  ...
+  tiers: CSG 7, exact surfaces 6, tessellated 0  (of 13 leaf solids)
+
+ +

+ Seven parts came out as ordinary ROOT shapes, six as exact surface solids, and none had to fall + back to an approximate mesh. The dV_sym=0 is the reassuring part: it is the + symmetric-difference volume between what was emitted and the original CAD solid, so zero means the + conversion is exact rather than merely close. +

+ +

Look at what you made

+ +

+ Numbers in a terminal are no substitute for seeing the thing. The macro can build the geometry and + write it out as an ordinary ROOT file: +

+ +
+
bash
+
cd cad_out/excavator
+root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root");'
+
+ +
+ A shaded render of the converted excavator arm: bucket, stick, boom and hydraulic rams, seen from above and to the side. +
+ The converted model, drawn by casting one ray per pixel through the TGeo navigator — so this is + the geometry as the transport sees it, not a separate preview mesh. +
+
+ +

+ The simplest interactive way to inspect the result is ROOT's own web display, which renders the + geometry with JSROOT in your browser and lets you rotate it, hide volumes and click through the + tree: +

+ +
+
bash
+
root --web geom.root
+
+ +

+ If you are on a remote machine where opening a browser is awkward, export the geometry as a JSROOT + document instead and open that file locally. It is a self-contained 32 kB for this model, and can + be dragged straight onto root.cern/js: +

+ +
+
bash
+
root -l -b -q -e 'TGeoManager::Import("geom.root");' \
+             -e 'TBufferJSON::ExportToFile("excavator.json.gz", gGeoManager);'
+
+ +

+ Spend a minute here. Turning the model around is the fastest way to notice that a subassembly is + missing, that something sits at the wrong scale, or that the part you care about was quietly + filtered out. +

+
+ + +
+ Converting +

How a part is represented

+ +

+ You have just run a conversion where every part came out exact, which is a good outcome but not an + automatic one. It is worth understanding what the converter was choosing between, because on a + real detector those choices decide both how faithful your simulation is and how fast it runs. +

+ +

+ The difficulty is that CAD and TGeo describe solids in different languages. CAD describes a body + by its boundary surfaces — this face is a piece of a cylinder, trimmed by these curves. TGeo + describes a body by combining primitives — a tube minus a box, say. Neither language is a superset + of the other, so there is no single translation that always works. The converter therefore carries + three different answers and picks the best available one for each leaf solid + independently. +

+ +
+ + + + + + + + + my.step + CAD assembly + + + + O2_CADtoTGeo + per leaf solid + + + + + + + 1  CSG primitives + TGeoTube, booleans — exact + + + 2  Exact surfaces + O2BVHSurfaceSolid — exact + + + 3  Triangle mesh + O2Tessellated — fallback + + + + + + + geom.C + + binary payloads + + +
+ +

+ The three are complementary rather than competing, and all of them end up in the same + geom.C. Nothing is ever lost along the way: a part that resists exact description + still ships as a mesh, so a conversion always produces a complete geometry. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
TierWhat it isExactCoversFlag
CSGNative ROOT shapes — TGeoTube, TGeoBBox, + TGeoCone and booleans of themYesMechanical parts that really are primitives. Fastest to navigate and smallest on disk, + so it is tried first.--csg auto
SurfacesThe part's real trimmed boundary faces carried into TGeo as + O2BVHSurfaceSolid, with a bounding-volume hierarchy for ray queriesYesAnything whose faces are planes, cylinders, cones, spheres or tori, however + complicatedly trimmed.--exact-surfaces auto
MeshA triangle mesh as O2TessellatedNoEverything else, as the fallback. Genuinely free-form surfaces end up here.--mesh
+
+ +

+ The difference is easiest to see rather than describe. Below, the same model is converted twice: + once to triangles alone at a coarse tolerance, and once with the full cascade, coloured by which + tier carried each part. +

+ +
+
+
+ Tessellated only + The excavator arm converted to triangles only, showing faceted, polygonal silhouettes on the cylindrical rams. +
+
+ The cascade, by tier + The same model with the full cascade: hydraulic rams and pins in green for CSG, machined bodies in blue for exact surfaces. +
+
+
    +
  • CSG primitives
  • +
  • Exact surfaces
  • +
  • Triangle mesh
  • +
+
+ On the left the cylinders have visibly polygonal silhouettes and flat shading bands — that is + the approximation you are accepting. On the right the rams and pivot pins were recognised as + unions of tubes and the machined bodies carried as their exact trimmed surfaces, so the curves + are curves. Both images are cast through the TGeo navigator with the same camera. +
+
+ +

+ In practice one asks for all three and lets the converter decide, which is what the + auto values in the earlier command did. Each of --csg and + --exact-surfaces accepts three settings, and the third is more useful than it looks: +

+ +
    +
  • off — never use this tier. This is the default for both, so a bare conversion + gives you meshes only, which is the left-hand picture above.
  • +
  • auto — use it wherever it is accepted, and fall through quietly elsewhere.
  • +
  • required — stop with a report if any part cannot be represented this way. Use it + when you want to know your geometry is exact rather than hope so.
  • +
+ +

+ One thing to trust here: a part is only accepted as CSG when OpenCascade's symmetric-difference + volume against the original solid falls inside the model's own tolerance. The recogniser is never + allowed to be approximately right, which is why dV_sym=0 keeps appearing in the + evidence column. +

+ +

Mesh precision, and one way to fill a disk

+ +

+ When a part does fall through to the mesh tier, --mesh-prec sets both the linear + deflection (in model units) and the angular deflection (in radians) of the mesher: lower is finer + and slower. For a desk-scale part 0.05 is a reasonable + default. For anything metre-scale you should be careful, because the cost grows quickly with size + — the default 0.1 applied to a two-metre sphere has produced a 22.9 GB + output directory. The right move for large models is to leave --mesh off entirely and + let the two exact tiers carry them. +

+ +
+ --mesh-solid tgeo does not navigate +

+ The mesh tier defaults to --mesh-solid o2, which emits + o2::base::O2Tessellated and needs the O2 environment to load. The alternative, + --mesh-solid tgeo, emits ROOT's own TGeoTessellated, which implements + none of Contains, DistFromInside, DistFromOutside or + Safety. Every such volume is then transported as its filled bounding box, + silently and with no warning. Only reach for it when the macro must load outside O2 and will + never have a particle sent through it. +

+
+
+ + +
+ Converting +

Convert only part of a model

+ +

+ Real engineering assemblies contain far more than you want to simulate — the mounting frame, the + trolley it sits on, sometimes the building. Converting all of it wastes time and fills your + geometry with volumes no particle will ever reach, so the converter offers two independent ways of + cutting a model down. They combine freely. +

+ +

Selecting by name

+ +

+ The first is by name. --include-name and --exclude-name take regular + expressions matched against the part name stored in the CAD file, case-insensitively, and either + may be repeated. Matching an assembly takes its whole subtree along with it, which is usually what + you want: +

+ +
+
bash
+
--include-name 'Bucket' --exclude-name '^SOLID\b'
+
+ +

Selecting by region

+ +

+ The second is geometric. --clip-box restricts the conversion to an axis-aligned box, + given as xmin ymin zmin xmax ymax zmax in the assembly's global frame. Note that these + are STEP file units, before the conversion to centimetres — so if your file is in + millimetres, so is your clip box: +

+ +
+
bash
+
--clip-box -50 -50 -20 50 50 20
+
+ +

+ Every solid is then classified against that box before any meshing happens. Solids fully outside + are dropped; solids fully inside are kept unchanged; and solids straddling the boundary are cut + against it with a boolean intersection, so only the part inside survives. Assemblies left with no + surviving children disappear from the output tree altogether. +

+ +

+ By default, subtrees that end up entirely inside the box keep their shared logical definitions, + which keeps the output compact when a part is repeated many times. If you need one distinct volume + per surviving occurrence instead — say because you want to name them individually later — pass + --clip-deduplicate none. +

+
+ + +
+ Converting +

Give it materials

+ +

+ So far the geometry has shape but no substance. Without material information every volume is + assigned a dummy medium called Default, which is fine while you are checking that + things are in the right place and quite wrong the moment you want physics out of it. +

+ +

+ The normal route is the bill of materials that the CAD system can export alongside + the geometry. We hand that to the converter as a CSV and it matches each part's material name + against a Geant4 NIST database. The rows it looks for are mechanical part rows in this shape: +

+ +
+
detector_bom.csv
+
Type,...,Part Number,Version,Name,Mass (kg),Material
+CAD,Mechanical/Part,Base,AA.01,Base,,Stainless Steel
+CAD,Mechanical/Part,BasePin,AA.01,BasePin,,Stainless Steel
+
+ +

Adding both files to the conversion is all that is required:

+ +
+
bash
+
o2-cad-to-tgeo my.step \
+    --output-folder cad_out/mydet -o geom.C \
+    --csg auto --exact-surfaces auto --mesh --mesh-prec 0.05 \
+    --materials-csv detector_bom.csv \
+    --bom-mass-unit kg \
+    --g4-nist-json $O2_ROOT/share/CADSupport/tools/g4_nist_database/G4_NIST_DB.json
+
+ +
+
output
+
Loaded Geant4 NIST DB with 309 materials from: .../G4_NIST_DB.json
+Loaded 13 BOM entries from: detector_bom.csv
+
+ +

+ Matching uses a combined score of name similarity and density plausibility, which handles the fact + that engineers write “Stainless Steel” where Geant4 says G4_STAINLESS-STEEL. A + confident match becomes a real TGeoMixture carrying its element composition, radiation + length and interaction length. An ambiguous or missing one falls back to a simple material and + leaves a comment in geom.C naming the part — so unresolved materials stay visible and + greppable rather than silently wrong. The scoring thresholds are adjustable + (--mat-min-score, --mat-ambiguity-delta and a few others), but the + defaults are usually right, and it is better to fix an ambiguous name in the BOM than to loosen the + matcher. +

+ +

+ One nice consequence of feeding in the BOM: where both a part mass and a CAD volume are available, + the converter derives an effective density from them. That is how a perforated bracket or a + partly-filled cable tray ends up with an honest average density instead of the density of solid + metal. +

+ +
+ If your model came from TGeo in the first place +

+ Geometry exported out of ALICE with O2_TGeoToCAD.py and coming back should use + --media-json instead. That rebuilds the original media verbatim, field by field, + rather than guessing them from names, and takes precedence over the BOM for every part it names. +

+
+
+ + +
+ Converting +

Field and cuts

+ +

+ There is one place where the converter cannot give you everything, and it is worth being explicit + about rather than discovering later. A CAD file describes a part. It cannot describe how + you want that part simulated — how the magnetic field should be integrated through it, how long a + step may be, which secondaries are worth producing. Those are simulation choices, and no CAD format + has anywhere to record them. +

+ +

Magnetic field

+ +

+ For the field there is a clean answer. Pass --in-field when the module sits inside the + magnet, and the emitted macro will ask the live field for its integration method + and maximum field strength at the moment the geometry is built — which is exactly what a + hand-written O2 detector does from its own createMaterials(). Nothing is baked into the + file: +

+ +
+
geom.C · emitted
+
int   cad_ifield = 2;
+float cad_fieldm = 10;
+cadFieldTrackingParams(cad_ifield, cad_fieldm);   // queries the loaded field
+
+med_Stainless_Steel->SetParam(1, cad_ifield);     // ifield, from the live field
+med_Stainless_Steel->SetParam(2, cad_fieldm);     // fieldm, from the live field
+
+ +

+ The 2,10 you see there is only a seed, used if no field happens to be loaded, and + --in-field 1,5.5 overrides it. To confirm that the query really happened, + check fieldm rather than ifield: ifield = 2 + is also the seed value and therefore proves nothing, whereas a fieldm the seed could + not have produced — ALICE reports 15 — proves the live field answered. +

+ +

Step control and physics cuts

+ +
+ These silently default to nothing +

+ Without --in-field, a CAD-authored medium is built through ROOT's three-argument + TGeoMedium constructor, which zeroes every parameter — including + ifield, meaning no field tracking at all. Step control + (tmaxfd stemax deemax epsil stmin) stays at the transport default in every case, and + special physics cuts are never applied, because there is no simcuts.dat for a module + with no detector directory to hold one. None of this is loud: the simulation runs and the numbers + look plausible. So set --in-field deliberately, and treat cuts as a known open item + until your study grows into a real detector, which is where they come back. +

+
+
+ + +
+ Converting +

The geom.C file

+ +

+ Everything the converter does ends up in one ROOT macro, and it is the artefact worth caring about. + It exports two functions: get_builder_hook_unchecked(), which is what + o2-sim calls when it loads your geometry, and build_and_export(), which + you already used to look at the model on its own. +

+ +

+ Alongside it, the output folder holds the binary payloads the macro reads — + facets_*.bin for meshed parts and surfaces_*.bin for exact ones — plus + csg_report.json, which records what each part became and why. +

+ +
+ The macro and its binaries travel together +

+ geom.C loads those .bin files relative to its own location. + Move or copy the macro without the rest of its folder and it will build an empty geometry without + complaining. Always move the directory. +

+
+ +

+ build_and_export() runs CheckOverlaps only when asked, because on large + models it is slow: +

+ +
+
bash
+
root -l -b -q -e '.L geom.C' -e 'build_and_export("geom.root", true, true);'
+
+ +
+
output
+
Info in <TGeoManager::CloseGeometry>: 14 nodes/ 14 volume UID's in geom
+Info in <TGeoNodeMatrix::CheckOverlaps>: Checking overlaps for Assembly and daughters within 0.1
+Info in <TGeoNodeMatrix::CheckOverlaps>: Number of illegal overlaps/extrusions : 0
+
+ +

+ Finally, a structural point that shapes how you organise your work: each converted directory holds + exactly one geom.C, and each geom.C describes one thing you hook into the + simulation. If your study involves three CAD subsystems, you run the converter three times into + three folders. They coexist without trouble, because the loader compiles each macro into its own + namespace at run time, so the identical function names inside them never collide. +

+
+ + +
+ Simulating +

Add passive geometry

+ +

+ With a macro in hand we can put the geometry into ALICE. The mechanism is deliberately data-driven: + two small JSON files, no code and no rebuild. We start with the simpler case — passive material such + as supports, cooling or cabling, which should scatter particles but does not record anything. That + goes into an externalModules array: +

+ +
+
externalGeometry.json
+
{
+  "externalModules": [
+    {
+      "name":  "EXCV",
+      "title": "Excavator support structure from CAD",
+      "macro": "cad_out/excavator/geom.C",
+      "anchor": "barrel",
+      "placement": {
+        "translation": [21.01, -13.22, -19.66],
+        "rotation_deg": [0.0, 0.0, 0.0]
+      }
+    }
+  ]
+}
+
+ +
+
name
a short tag for the module. It must also appear in the module list below, or the module is silently skipped.
+
macro
the path to the geom.C you produced.
+
anchor
a volume that already exists in the ALICE geometry. barrel is the usual choice, and it sits at cave coordinates (0, -30, 0).
+
placement
translation and rotation within the anchor's frame, in centimetres and degrees.
+
+ +

+ The second file is the module list, which is what actually switches the module on. The split exists + so that you can describe several modules in one geometry file and enable them individually: +

+ +
+
detectorlist.json
+
{ "EXTCAD": ["EXCV"] }
+
+ +

Then run the simulation, pointing at both:

+ +
+
bash
+
o2-sim-serial -n 1 -g boxgen \
+    --detectorList EXTCAD:detectorlist.json \
+    --extGeomFile externalGeometry.json
+
+ +
+
output
+
Configured external module 'EXCV' from macro 'cad_out/excavator/geom.C' anchored to volume 'barrel'
+Activating EXCV module
+Setting special cuts for passive module EXCV
+
+ +

+ Those three lines mean your CAD geometry is in the simulation and particles are being transported + through it. You can list as many modules in the same array as you like. +

+
+ + +
+ Simulating +

Make it produce hits

+ +

+ Passive geometry answers questions about material budget. To ask whether your detector is actually + hit, and how often, some of its volumes need to be sensitive. This is the fastest route from a CAD + file to plottable hits, and it still needs no detector class and no rebuild — we simply change the + array name to externalDetectors and say which volumes should record: +

+ +
+
externalGeometry.json
+
{
+  "externalDetectors": [
+    {
+      "name":  "EXCV",
+      "title": "Excavator as a sensitive detector",
+      "macro": "cad_out/excavator/geom.C",
+      "anchor": "barrel",
+      "detID": "TST",
+      "sensitiveVolumes": ["Bucket"],
+      "placement": { "translation": [21.01, -13.22, -19.66] }
+    }
+  ]
+}
+
+ +

Choosing the sensitive volumes

+ +

+ There are two ways of selecting them, and you may use either or both as long as at least one is + non-empty. sensitiveVolumes matches against TGeo volume names, and + sensitiveMedia matches against medium names — the latter being a convenient way to make + every silicon part in an assembly sensitive at once, however the parts happen to be named. +

+ +
+ Both match substrings, not whole names +

+ This catches people out. On the excavator model, + "sensitiveVolumes": ["Bucket"] selects five volumes rather than one + — Bucket, BucketLink1, BucketLink2, + BucketCylinderInner and BucketCylinderOuter. The startup log prints + every volume it registered, so read it and tighten the string if that was not what you meant. +

+
+ +

Choosing a DetID

+ +

+ The detID field ties your detector to an existing O2 detector identity, which is what + determines where the hits are filed. Pick a slot no active built-in detector is using: +

+
    +
  • TST is the general-purpose test slot, and the right default for a quick study.
  • +
  • An upgrade study normally borrows the slot it stands in for — TRK for an ALICE 3 + tracker, for instance — because it is semantically honest and keeps downstream tooling happy.
  • +
+

+ The hit branch keeps your module name rather than the borrowed one, so the configuration + above produces a branch called EXCVHit. +

+ +

Running it

+ +
+
bash
+
o2-sim-serial -n 3 -g boxgen --seed 42 \
+    --detectorList EXTCAD:detectorlist.json \
+    --extGeomFile externalGeometry.json \
+    --configKeyValues 'BoxGun.number=500;BoxGun.pdg=211;BoxGun.eta[0]=-1;BoxGun.eta[1]=1;BoxGun.prange[0]=2.0;BoxGun.prange[1]=5.0'
+
+ +
+
output
+
External detector EXCV: 5 sensitive volume(s) selected
+External detector EXCV: registered sensitive volume 'Bucket' (MC volID 8, sensor 0)
+CREATING BRANCH EXCVHit
+External detector EXCV EndOfEvent: 681 sensitive step(s) -> 94 hit(s)
+External detector EXCV EndOfEvent: 402 sensitive step(s) -> 59 hit(s)
+External detector EXCV EndOfEvent: 927 sensitive step(s) -> 124 hit(s)
+
+ +

The hits land in o2sim.root, one entry per event:

+ +
+
bash
+
root -l -b -q -e 'TFile f("o2sim.root"); TTree *t=(TTree*)f.Get("o2sim");
+                  t->Draw("EXCVHit@.size()");'
+
+ +
+ Zero hits is usually aim, not breakage +

+ The most common first result is 0 sensitive step(s), and the instinct is to suspect + the conversion. Check where the particles are going first. The run above produces nothing at all + at the default multiplicity of 10, simply because the excavator is a 40 cm object sitting 40 cm + off-axis and is a small target. Raise the multiplicity or aim the gun. To rule out the geometry + independently, shoot a ray through it in ROOT with + gGeoManager->FindNextBoundaryAndStep() and print the volume names you cross — if + they appear, navigation is fine and the problem is aim. +

+
+ +

Custom sensitive actions

+ +

+ With no further configuration, every sensitive volume records a charged-track entrance and exit hit + in the generic o2::ext::Hit format: position in and out, momentum, energy loss, PDG code + and track length. That is enough for occupancy, acceptance and material studies, which covers most + first questions. +

+ +

+ When you need something else — a different hit definition, a cut applied at scoring time, extra + quantities — you can point at a macro returning a + o2::ext::ExternalDetector::SensitiveFcn. It is compiled at run time and can query + TVirtualMC::GetMC() and call helpers such as currentSensorID(), + currentTrackID() and addHit(): +

+ +
+
externalGeometry.json · fragment
+
"sensitiveMedia": ["Silicon"],
+"sensitiveMacro": "sensitive_action.macro",
+"sensitiveFunction": "sensitiveAction()"
+
+ +
+ A worked example that needs no CAD file +

+ run/SimExamples/External_Sensitive_Detectors defines two artificial detectors + entirely from data — one using the built-in action, one with a custom action compiled at run time + — from hand-written macros that mimic converter output. Running ./run.sh in that + directory shows both hit branches appearing. +

+
+
+ + +
+ Simulating +

Grow it into a real detector

+ +
+ Not yet exercised end to end +

+ Everything above this section has been run, with its output pasted from a real terminal. This + route follows from how ExternalDetector and the built-in detectors are written, but no + detector has yet been built this way. Treat it as a design rather than a recipe, and expect to + debug it. +

+
+ +

+ The external-detector route deliberately trades flexibility for speed: you get one generic hit type + and a borrowed DetID, and in exchange you get results the same afternoon. Once a study + turns into a real subdetector you will want your own hit class, your own digitisation and a + DetID of your own — and none of that requires giving up the CAD import. The generated + geometry simply becomes one step inside an ordinary O2 detector. +

+ +

Three changes to a normal detector implementation are involved:

+ +
    +
  1. + Build the geometry from the macro instead of by hand. Copy geom.C + into your detector's simulation directory and call its builder hook from + ConstructGeometry(), in place of the new TGeoTube(...) code you would + otherwise write. Keep the .bin payloads beside it and install them with the detector's + data files, since the macro resolves them relative to itself. +
  2. +
  3. + Register your own sensitive volumes. Call AddSensitiveVolume() for + the volumes the macro created, using the names the converter derived from the CAD part names. + Print them once from geom.root and pin them down in code, because a rename in CAD + would otherwise quietly unregister a sensor. +
  4. +
  5. + Write your own hits. Implement ProcessHits() with your own hit class + and your own DetID, exactly as any hand-written detector does. Nothing about the + geometry's CAD origin constrains this. +
  6. +
+ +

+ Two things come back the moment you take this step, both of which the external-detector route cannot + offer: initFieldTrackingParams() called from your own createMaterials(), + and SetSpecialPhysicsCuts() reading a real simcuts.dat from your detector's + data directory. That closes the gap described under Field and cuts. +

+ +

+ The payoff is that re-running the converter after a CAD change regenerates only the geometry. Your + detector code stays untouched, which is the whole point of importing rather than transcribing. +

+
+ + +
+ Reference +

Check your geometry

+ +

+ Before trusting any physics that came out of a conversion, it is worth spending a few minutes on + four checks. They are ordered cheapest first, and in practice the first two catch most problems. +

+ +

1 · Read the cascade table

+ +

+ The converter already told you what it decided for every part, and wrote the same information to + csg_report.json. A part that declined CSG says which test it failed and by how much, + which is often enough to see that a model is nearly-but-not-quite a primitive. A large tessellated + count on a model you expected to be analytic is the signal to look at + --recognize-surfaces and the surface report below. +

+ +

2 · Look for overlaps

+ +

+ Run build_and_export("geom.root", true, true) to get CheckOverlaps; zero + illegal overlaps is what you want to see. A non-zero count is worth taking seriously, but do not assume it is + the conversion's fault: engineering assemblies are drawn for manufacture, not for particle transport, + and slightly interpenetrating parts are common in perfectly good CAD models. +

+ +

3 · Confirm the exact solids really load

+ +

+ Successfully extracting a solid's surfaces does not guarantee the result is a usable, watertight + body. This macro loads every surfaces_*.bin in a directory the same way the transport + does, and reports closure, orientation consistency and enclosed volume: +

+ +
+
bash
+
# $O2_SRC is your AliceO2 source directory
+root -l -b -q "$O2_SRC/Detectors/CADSupport/test/checkSurfaceSidecars.macro(\"cad_out/excavator\")"
+
+ +
+
output
+
OK    surfaces_Bucket_0_1_1_6.bin                           surfaces=   97  closed=1  orient=1  capacity=58.3121
+OK    surfaces_Base_0_1_1_3.bin                             surfaces=   44  closed=1  orient=1  capacity=241.281
+...
+
+SUMMARY cad_out/excavator
+  sidecars found            : 13
+  loaded                    : 13
+  rejected by the reader    : 0
+  loaded but not IsClosed() : 0
+  orientation inconsistent  : 0
+
+ +

+ closed=1 means the solid is a watertight manifold, which is precisely what navigation + requires. Any non-zero number on the last three summary lines identifies a part that will not + transport correctly. +

+ +

4 · Find out what the geometry really is

+ +

+ A subtlety worth knowing: the surface type stored in a STEP file describes the exporter, not + the geometry. CAD kernels routinely write an exact cylinder as a rational B-spline, which is an exact + representation rather than an approximation — but dispatching on the stored type would throw that + exactness away. The converter therefore classifies faces by their actual shape, and its surface report + shows the effect: +

+ +
+
bash
+
# a per-face classification, written alongside a normal conversion
+--surface-report cad_out/mydet/surface_report.json
+
+
+ + +
+ Reference +

Limits and pain points

+ +

+ Finally, the honest list. These are the things known to catch people today, roughly in order of how + often they do it. None is a reason not to use the system, but all of them are cheaper to read about + here than to rediscover in a result. +

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
WhatWhy it happensWhat to do
One geom.C per hooked thingThe macro exports a single builder hook, and that hook is what the JSON refers to.Run the converter once per subsystem, into its own folder. They coexist happily in one JSON.
Media, cuts and field default to zeroA CAD file carries a material, never a medium, and the emitter uses a three-argument + TGeoMedium which zeroes every parameter.Pass --in-field. Accept transport defaults for step control, and treat + production cuts as unset until you write a real detector.
The anchor volume must already existPlacement is expressed inside the frame of an existing O2 volume.Use barrel unless you have a reason not to, and remember it sits at cave + (0, -30, 0).
Free-form surfaces stay tessellatedGenuine B-spline surfaces are not supported by the exact tier at all.Check the surface report. Recognition already recovers quadrics written as NURBS, which is + the large majority of them.
Illegal overlaps in the CAD modelEngineering assemblies are not drawn as legal transport worlds, and parts routinely + interpenetrate.Read CheckOverlaps, then fix in CAD or clip the offending region.
Degenerate facets at coarse precisionO2Tessellated drops triangles that collapse to a line.Treat it as a mesh-quality signal: lower --mesh-prec, or move the part onto an + exact tier.
A surprisingly huge output directoryMeshing a metre-scale curved part at a fine chord tolerance.Convert large models without --mesh, and never use the default + --mesh-prec on something metre-sized.
o2-sim complains about a missing externalModules arrayCosmetic. The message is emitted even when your JSON correctly contains only + externalDetectors.Ignore it.
+
+ +

One rule that is not a preference

+ +

+ Run --csg auto conversions strictly serially. Parallel runs race each + other and silently lose shapes, which produces a geometry that looks complete and is not — the worst + possible failure mode, and the hardest to notice afterwards. +

+ +
+

+ Deeper material lives in Detectors/CADSupport: README.md for the + complete option reference, and doc/reference/ for the exact-surface solid, its file + format and the CSG pipeline. +

+
+
+ +
+
+ + + + diff --git a/Detectors/CADSupport/examples/ExcavatorArm.step b/Detectors/CADSupport/examples/ExcavatorArm.step new file mode 100644 index 0000000000000..ac2e3e1ca6530 --- /dev/null +++ b/Detectors/CADSupport/examples/ExcavatorArm.step @@ -0,0 +1,11687 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('FreeCAD Model'),'2;1'); +FILE_NAME('Open CASCADE Shape Model','2026-03-02T16:10:30',('Author'),( + ''),'Open CASCADE STEP processor 7.8','FreeCAD','Unknown'); +FILE_SCHEMA(( +'AP242_MANAGED_MODEL_BASED_3D_ENGINEERING_MIM_LF. {1 0 10303 442 1 1 4 +}')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('international standard', + 'ap242_managed_model_based_3d_engineering',2013,#2); +#2 = APPLICATION_CONTEXT('Managed model based 3d engineering'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('Assembly','Assembly','',(#8)); +#8 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = SHAPE_REPRESENTATION('',(#11,#15,#19,#23,#27,#31,#35,#39,#43,#47, + #51,#55,#59,#63),#67); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.,0.,0.)); +#13 = DIRECTION('',(0.,0.,1.)); +#14 = DIRECTION('',(1.,0.,-0.)); +#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18); +#16 = CARTESIAN_POINT('',(-206.5170288085,40.255699157715, + 364.26800537109)); +#17 = DIRECTION('',(0.,0.,1.)); +#18 = DIRECTION('',(1.,0.,0.)); +#19 = AXIS2_PLACEMENT_3D('',#20,#21,#22); +#20 = CARTESIAN_POINT('',(-206.5170288085,99.415699157715, + 364.26800537109)); +#21 = DIRECTION('',(-0.,0.,1.)); +#22 = DIRECTION('',(0.999989267829,-4.632950059267E-03,0.)); +#23 = AXIS2_PLACEMENT_3D('',#24,#25,#26); +#24 = CARTESIAN_POINT('',(-202.9316877521,230.96951641122, + 229.24670342923)); +#25 = DIRECTION('',(2.026526400826E-03,0.437411287808,0.899259283238)); +#26 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 5.170579729327E-16)); +#27 = AXIS2_PLACEMENT_3D('',#28,#29,#30); +#28 = CARTESIAN_POINT('',(-205.4688667447,390.40757205825, + 208.42086768647)); +#29 = DIRECTION('',(1.831142555716E-03,0.395239076644,0.918576463453)); +#30 = DIRECTION('',(0.999989267829,-4.632950059323E-03,1.7311000422E-15) + ); +#31 = AXIS2_PLACEMENT_3D('',#32,#33,#34); +#32 = CARTESIAN_POINT('',(-210.0753214669,432.19406070204, + 196.61955425438)); +#33 = DIRECTION('',(2.084393336159E-03,0.449901453589,0.893075773584)); +#34 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 1.453390187557E-15)); +#35 = AXIS2_PLACEMENT_3D('',#36,#37,#38); +#36 = CARTESIAN_POINT('',(-215.534833234,-1.246729639524E+03, + 276.4771593578)); +#37 = DIRECTION('',(3.392454376808E-03,0.732237111337,-0.681041337978)); +#38 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 1.861887793204E-15)); +#39 = AXIS2_PLACEMENT_3D('',#40,#41,#42); +#40 = CARTESIAN_POINT('',(-212.0889443908,-2.43210353169,-20.38628636038 + )); +#41 = DIRECTION('',(3.944089081718E-03,0.851303532863,0.524658688192)); +#42 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 1.465569108225E-15)); +#43 = AXIS2_PLACEMENT_3D('',#44,#45,#46); +#44 = CARTESIAN_POINT('',(-209.106385692,136.25419380278,268.96319574495 + )); +#45 = DIRECTION('',(1.845049405543E-03,0.398240771114,0.917279065506)); +#46 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + -5.25430455893E-16)); +#47 = AXIS2_PLACEMENT_3D('',#48,#49,#50); +#48 = CARTESIAN_POINT('',(-208.726583851,108.58235762312,238.80529805216 + )); +#49 = DIRECTION('',(1.845049405543E-03,0.398240771114,0.917279065506)); +#50 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 4.213523094089E-16)); +#51 = AXIS2_PLACEMENT_3D('',#52,#53,#54); +#52 = CARTESIAN_POINT('',(-203.7089608032,310.95216213757, + 236.71125566035)); +#53 = DIRECTION('',(2.678462640767E-03,0.57812708118,0.815942341004)); +#54 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 1.780223579257E-15)); +#55 = AXIS2_PLACEMENT_3D('',#56,#57,#58); +#56 = CARTESIAN_POINT('',(-203.0270581761,347.62324786744, + 221.92180548249)); +#57 = DIRECTION('',(2.678462640768E-03,0.57812708118,0.815942341004)); +#58 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 7.474778267112E-16)); +#59 = AXIS2_PLACEMENT_3D('',#60,#61,#62); +#60 = CARTESIAN_POINT('',(-205.3550855986,395.97204492723, + 110.55505260248)); +#61 = DIRECTION('',(2.365955428257E-03,0.510674625482,0.859770800355)); +#62 = DIRECTION('',(0.999989267829,-4.632950059324E-03, + 2.660188556882E-15)); +#63 = AXIS2_PLACEMENT_3D('',#64,#65,#66); +#64 = CARTESIAN_POINT('',(-205.4034412703,417.47990622491, + 46.504500967597)); +#65 = DIRECTION('',(2.435112819188E-03,0.525601755677,0.850727256326)); +#66 = DIRECTION('',(0.999989267829,-4.632950059323E-03, + 1.930322711132E-15)); +#67 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#71)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#68,#69,#70)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#68 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#69 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#70 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#71 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-05),#68, + 'distance_accuracy_value','confusion accuracy'); +#72 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7)); +#73 = SHAPE_DEFINITION_REPRESENTATION(#74,#80); +#74 = PRODUCT_DEFINITION_SHAPE('','',#75); +#75 = PRODUCT_DEFINITION('design','',#76,#79); +#76 = PRODUCT_DEFINITION_FORMATION('','',#77); +#77 = PRODUCT('BasePin','BasePin','',(#78)); +#78 = PRODUCT_CONTEXT('',#2,'mechanical'); +#79 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#80 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#81),#134); +#81 = MANIFOLD_SOLID_BREP('',#82); +#82 = CLOSED_SHELL('',(#83,#116,#125)); +#83 = ADVANCED_FACE('',(#84),#111,.T.); +#84 = FACE_BOUND('',#85,.F.); +#85 = EDGE_LOOP('',(#86,#96,#103,#104)); +#86 = ORIENTED_EDGE('',*,*,#87,.T.); +#87 = EDGE_CURVE('',#88,#90,#92,.T.); +#88 = VERTEX_POINT('',#89); +#89 = CARTESIAN_POINT('',(0.,69.16,0.)); +#90 = VERTEX_POINT('',#91); +#91 = CARTESIAN_POINT('',(-1.6E-14,69.16,100.)); +#92 = LINE('',#93,#94); +#93 = CARTESIAN_POINT('',(2.45E-15,69.16,0.)); +#94 = VECTOR('',#95,1.); +#95 = DIRECTION('',(0.,0.,1.)); +#96 = ORIENTED_EDGE('',*,*,#97,.T.); +#97 = EDGE_CURVE('',#90,#90,#98,.T.); +#98 = CIRCLE('',#99,10.); +#99 = AXIS2_PLACEMENT_3D('',#100,#101,#102); +#100 = CARTESIAN_POINT('',(0.,59.16,100.)); +#101 = DIRECTION('',(0.,-0.,1.)); +#102 = DIRECTION('',(0.,1.,0.)); +#103 = ORIENTED_EDGE('',*,*,#87,.F.); +#104 = ORIENTED_EDGE('',*,*,#105,.F.); +#105 = EDGE_CURVE('',#88,#88,#106,.T.); +#106 = CIRCLE('',#107,10.); +#107 = AXIS2_PLACEMENT_3D('',#108,#109,#110); +#108 = CARTESIAN_POINT('',(0.,59.16,0.)); +#109 = DIRECTION('',(0.,-0.,1.)); +#110 = DIRECTION('',(0.,1.,0.)); +#111 = CYLINDRICAL_SURFACE('',#112,10.); +#112 = AXIS2_PLACEMENT_3D('',#113,#114,#115); +#113 = CARTESIAN_POINT('',(0.,59.16,0.)); +#114 = DIRECTION('',(0.,0.,-1.)); +#115 = DIRECTION('',(0.,1.,0.)); +#116 = ADVANCED_FACE('',(#117),#120,.F.); +#117 = FACE_BOUND('',#118,.T.); +#118 = EDGE_LOOP('',(#119)); +#119 = ORIENTED_EDGE('',*,*,#105,.F.); +#120 = PLANE('',#121); +#121 = AXIS2_PLACEMENT_3D('',#122,#123,#124); +#122 = CARTESIAN_POINT('',(0.,59.16,0.)); +#123 = DIRECTION('',(0.,0.,1.)); +#124 = DIRECTION('',(0.,1.,0.)); +#125 = ADVANCED_FACE('',(#126),#129,.T.); +#126 = FACE_BOUND('',#127,.F.); +#127 = EDGE_LOOP('',(#128)); +#128 = ORIENTED_EDGE('',*,*,#97,.F.); +#129 = PLANE('',#130); +#130 = AXIS2_PLACEMENT_3D('',#131,#132,#133); +#131 = CARTESIAN_POINT('',(0.,59.16,100.)); +#132 = DIRECTION('',(0.,0.,1.)); +#133 = DIRECTION('',(0.,1.,0.)); +#134 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#138)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#135,#136,#137)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#135 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#136 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#137 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#138 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#135, + 'distance_accuracy_value','confusion accuracy'); +#139 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#140,#142); +#140 = ( REPRESENTATION_RELATIONSHIP('','',#80,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#141) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#141 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); +#142 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#143 + ); +#143 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','BasePin001','',#5,#75,$); +#144 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#77)); +#145 = SHAPE_DEFINITION_REPRESENTATION(#146,#152); +#146 = PRODUCT_DEFINITION_SHAPE('','',#147); +#147 = PRODUCT_DEFINITION('design','',#148,#151); +#148 = PRODUCT_DEFINITION_FORMATION('','',#149); +#149 = PRODUCT('Base','Base','',(#150)); +#150 = PRODUCT_CONTEXT('',#2,'mechanical'); +#151 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#152 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#153),#1573); +#153 = MANIFOLD_SOLID_BREP('',#154); +#154 = CLOSED_SHELL('',(#155,#224,#255,#280,#305,#329,#353,#483,#508, + #525,#563,#626,#687,#711,#781,#841,#865,#918,#995,#1019,#1080,#1136, + #1154,#1179,#1206,#1223,#1273,#1290,#1331,#1343,#1360,#1377,#1389, + #1406,#1418,#1440,#1452,#1464,#1481,#1498,#1515,#1527,#1539,#1556)); +#155 = ADVANCED_FACE('',(#156,#208),#219,.T.); +#156 = FACE_BOUND('',#157,.T.); +#157 = EDGE_LOOP('',(#158,#168,#177,#185,#193,#201)); +#158 = ORIENTED_EDGE('',*,*,#159,.T.); +#159 = EDGE_CURVE('',#160,#162,#164,.T.); +#160 = VERTEX_POINT('',#161); +#161 = CARTESIAN_POINT('',(-30.,-38.82620606324,106.)); +#162 = VERTEX_POINT('',#163); +#163 = CARTESIAN_POINT('',(-30.,-38.77075908679,106.)); +#164 = LINE('',#165,#166); +#165 = CARTESIAN_POINT('',(-30.,-59.8787016455,106.)); +#166 = VECTOR('',#167,1.); +#167 = DIRECTION('',(0.,1.,0.)); +#168 = ORIENTED_EDGE('',*,*,#169,.T.); +#169 = EDGE_CURVE('',#162,#170,#172,.T.); +#170 = VERTEX_POINT('',#171); +#171 = CARTESIAN_POINT('',(-30.,-25.34781506248,98.348872481061)); +#172 = CIRCLE('',#173,15.6); +#173 = AXIS2_PLACEMENT_3D('',#174,#175,#176); +#174 = CARTESIAN_POINT('',(-30.,-38.77075908679,90.4)); +#175 = DIRECTION('',(-1.,-0.,-6.7E-16)); +#176 = DIRECTION('',(6.7E-16,0.,-1.)); +#177 = ORIENTED_EDGE('',*,*,#178,.F.); +#178 = EDGE_CURVE('',#179,#170,#181,.T.); +#179 = VERTEX_POINT('',#180); +#180 = CARTESIAN_POINT('',(-30.,-14.48187929913,80.)); +#181 = LINE('',#182,#183); +#182 = CARTESIAN_POINT('',(-30.,-14.48187929913,80.)); +#183 = VECTOR('',#184,1.); +#184 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#185 = ORIENTED_EDGE('',*,*,#186,.F.); +#186 = EDGE_CURVE('',#187,#179,#189,.T.); +#187 = VERTEX_POINT('',#188); +#188 = CARTESIAN_POINT('',(-30.,-44.48187929913,80.)); +#189 = LINE('',#190,#191); +#190 = CARTESIAN_POINT('',(-30.,-44.48187929913,80.)); +#191 = VECTOR('',#192,1.); +#192 = DIRECTION('',(0.,1.,0.)); +#193 = ORIENTED_EDGE('',*,*,#194,.T.); +#194 = EDGE_CURVE('',#187,#195,#197,.T.); +#195 = VERTEX_POINT('',#196); +#196 = CARTESIAN_POINT('',(-30.,-49.1515476204,87.885482706876)); +#197 = LINE('',#198,#199); +#198 = CARTESIAN_POINT('',(-30.,-44.48187929913,80.)); +#199 = VECTOR('',#200,1.); +#200 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#201 = ORIENTED_EDGE('',*,*,#202,.T.); +#202 = EDGE_CURVE('',#195,#160,#203,.T.); +#203 = CIRCLE('',#204,12.); +#204 = AXIS2_PLACEMENT_3D('',#205,#206,#207); +#205 = CARTESIAN_POINT('',(-30.,-38.82620606324,94.)); +#206 = DIRECTION('',(-1.,0.,-6.7E-16)); +#207 = DIRECTION('',(-6.7E-16,0.,1.)); +#208 = FACE_BOUND('',#209,.T.); +#209 = EDGE_LOOP('',(#210)); +#210 = ORIENTED_EDGE('',*,*,#211,.T.); +#211 = EDGE_CURVE('',#212,#212,#214,.T.); +#212 = VERTEX_POINT('',#213); +#213 = CARTESIAN_POINT('',(-30.,-45.04444206723,94.459258343213)); +#214 = CIRCLE('',#215,7.); +#215 = AXIS2_PLACEMENT_3D('',#216,#217,#218); +#216 = CARTESIAN_POINT('',(-30.,-38.04444206723,94.459258343213)); +#217 = DIRECTION('',(1.,0.,1.19E-15)); +#218 = DIRECTION('',(0.,-1.,0.)); +#219 = PLANE('',#220); +#220 = AXIS2_PLACEMENT_3D('',#221,#222,#223); +#221 = CARTESIAN_POINT('',(-30.,-44.48187929913,80.)); +#222 = DIRECTION('',(-1.,0.,-1.08E-15)); +#223 = DIRECTION('',(0.,1.,0.)); +#224 = ADVANCED_FACE('',(#225),#250,.F.); +#225 = FACE_BOUND('',#226,.F.); +#226 = EDGE_LOOP('',(#227,#228,#236,#244)); +#227 = ORIENTED_EDGE('',*,*,#159,.T.); +#228 = ORIENTED_EDGE('',*,*,#229,.T.); +#229 = EDGE_CURVE('',#162,#230,#232,.T.); +#230 = VERTEX_POINT('',#231); +#231 = CARTESIAN_POINT('',(-15.,-38.77075908679,106.)); +#232 = LINE('',#233,#234); +#233 = CARTESIAN_POINT('',(-30.,-38.77075908679,106.)); +#234 = VECTOR('',#235,1.); +#235 = DIRECTION('',(1.,0.,6.7E-16)); +#236 = ORIENTED_EDGE('',*,*,#237,.T.); +#237 = EDGE_CURVE('',#230,#238,#240,.T.); +#238 = VERTEX_POINT('',#239); +#239 = CARTESIAN_POINT('',(-15.,-38.82620606324,106.)); +#240 = LINE('',#241,#242); +#241 = CARTESIAN_POINT('',(-15.,-29.8787016455,106.)); +#242 = VECTOR('',#243,1.); +#243 = DIRECTION('',(0.,-1.,0.)); +#244 = ORIENTED_EDGE('',*,*,#245,.T.); +#245 = EDGE_CURVE('',#238,#160,#246,.T.); +#246 = LINE('',#247,#248); +#247 = CARTESIAN_POINT('',(-15.,-38.82620606324,106.)); +#248 = VECTOR('',#249,1.); +#249 = DIRECTION('',(-1.,0.,-6.7E-16)); +#250 = PLANE('',#251); +#251 = AXIS2_PLACEMENT_3D('',#252,#253,#254); +#252 = CARTESIAN_POINT('',(-22.5,-44.8787016455,106.)); +#253 = DIRECTION('',(2.2E-16,-0.,-1.)); +#254 = DIRECTION('',(-1.,0.,-2.2E-16)); +#255 = ADVANCED_FACE('',(#256),#275,.T.); +#256 = FACE_BOUND('',#257,.F.); +#257 = EDGE_LOOP('',(#258,#259,#267,#274)); +#258 = ORIENTED_EDGE('',*,*,#169,.T.); +#259 = ORIENTED_EDGE('',*,*,#260,.T.); +#260 = EDGE_CURVE('',#170,#261,#263,.T.); +#261 = VERTEX_POINT('',#262); +#262 = CARTESIAN_POINT('',(-15.,-25.34781506248,98.348872481061)); +#263 = LINE('',#264,#265); +#264 = CARTESIAN_POINT('',(-30.,-25.34781506248,98.348872481061)); +#265 = VECTOR('',#266,1.); +#266 = DIRECTION('',(1.,0.,6.7E-16)); +#267 = ORIENTED_EDGE('',*,*,#268,.F.); +#268 = EDGE_CURVE('',#230,#261,#269,.T.); +#269 = CIRCLE('',#270,15.6); +#270 = AXIS2_PLACEMENT_3D('',#271,#272,#273); +#271 = CARTESIAN_POINT('',(-15.,-38.77075908679,90.4)); +#272 = DIRECTION('',(-1.,-0.,-6.7E-16)); +#273 = DIRECTION('',(6.7E-16,0.,-1.)); +#274 = ORIENTED_EDGE('',*,*,#229,.F.); +#275 = CYLINDRICAL_SURFACE('',#276,15.6); +#276 = AXIS2_PLACEMENT_3D('',#277,#278,#279); +#277 = CARTESIAN_POINT('',(-30.,-38.77075908679,90.4)); +#278 = DIRECTION('',(1.,0.,6.7E-16)); +#279 = DIRECTION('',(-6.7E-16,0.,1.)); +#280 = ADVANCED_FACE('',(#281),#300,.T.); +#281 = FACE_BOUND('',#282,.T.); +#282 = EDGE_LOOP('',(#283,#292,#293,#294)); +#283 = ORIENTED_EDGE('',*,*,#284,.T.); +#284 = EDGE_CURVE('',#285,#238,#287,.T.); +#285 = VERTEX_POINT('',#286); +#286 = CARTESIAN_POINT('',(-15.,-49.1515476204,87.885482706876)); +#287 = CIRCLE('',#288,12.); +#288 = AXIS2_PLACEMENT_3D('',#289,#290,#291); +#289 = CARTESIAN_POINT('',(-15.,-38.82620606324,94.)); +#290 = DIRECTION('',(-1.,0.,-6.7E-16)); +#291 = DIRECTION('',(-6.7E-16,0.,1.)); +#292 = ORIENTED_EDGE('',*,*,#245,.T.); +#293 = ORIENTED_EDGE('',*,*,#202,.F.); +#294 = ORIENTED_EDGE('',*,*,#295,.F.); +#295 = EDGE_CURVE('',#285,#195,#296,.T.); +#296 = LINE('',#297,#298); +#297 = CARTESIAN_POINT('',(-15.,-49.1515476204,87.885482706876)); +#298 = VECTOR('',#299,1.); +#299 = DIRECTION('',(-1.,0.,-6.7E-16)); +#300 = CYLINDRICAL_SURFACE('',#301,12.); +#301 = AXIS2_PLACEMENT_3D('',#302,#303,#304); +#302 = CARTESIAN_POINT('',(-15.,-38.82620606324,94.)); +#303 = DIRECTION('',(-1.,0.,-6.7E-16)); +#304 = DIRECTION('',(3.413938821994E-16,-0.860445129764,-0.50954310776) + ); +#305 = ADVANCED_FACE('',(#306),#324,.T.); +#306 = FACE_BOUND('',#307,.T.); +#307 = EDGE_LOOP('',(#308,#309,#310,#318)); +#308 = ORIENTED_EDGE('',*,*,#178,.T.); +#309 = ORIENTED_EDGE('',*,*,#260,.T.); +#310 = ORIENTED_EDGE('',*,*,#311,.F.); +#311 = EDGE_CURVE('',#312,#261,#314,.T.); +#312 = VERTEX_POINT('',#313); +#313 = CARTESIAN_POINT('',(-15.,-14.48187929913,80.)); +#314 = LINE('',#315,#316); +#315 = CARTESIAN_POINT('',(-15.,-14.48187929913,80.)); +#316 = VECTOR('',#317,1.); +#317 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#318 = ORIENTED_EDGE('',*,*,#319,.F.); +#319 = EDGE_CURVE('',#179,#312,#320,.T.); +#320 = LINE('',#321,#322); +#321 = CARTESIAN_POINT('',(-30.,-14.48187929913,80.)); +#322 = VECTOR('',#323,1.); +#323 = DIRECTION('',(1.,0.,2.2E-16)); +#324 = PLANE('',#325); +#325 = AXIS2_PLACEMENT_3D('',#326,#327,#328); +#326 = CARTESIAN_POINT('',(-30.,-14.48187929913,80.)); +#327 = DIRECTION('',(-2.8E-16,0.860445129764,0.50954310776)); +#328 = DIRECTION('',(1.,-4.598339533307E-18,5.572769301199E-16)); +#329 = ADVANCED_FACE('',(#330),#348,.T.); +#330 = FACE_BOUND('',#331,.T.); +#331 = EDGE_LOOP('',(#332,#340,#341,#342)); +#332 = ORIENTED_EDGE('',*,*,#333,.T.); +#333 = EDGE_CURVE('',#334,#285,#336,.T.); +#334 = VERTEX_POINT('',#335); +#335 = CARTESIAN_POINT('',(-15.,-44.48187929913,80.)); +#336 = LINE('',#337,#338); +#337 = CARTESIAN_POINT('',(-15.,-44.48187929913,80.)); +#338 = VECTOR('',#339,1.); +#339 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#340 = ORIENTED_EDGE('',*,*,#295,.T.); +#341 = ORIENTED_EDGE('',*,*,#194,.F.); +#342 = ORIENTED_EDGE('',*,*,#343,.F.); +#343 = EDGE_CURVE('',#334,#187,#344,.T.); +#344 = LINE('',#345,#346); +#345 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#346 = VECTOR('',#347,1.); +#347 = DIRECTION('',(-1.,0.,-2.2E-16)); +#348 = PLANE('',#349); +#349 = AXIS2_PLACEMENT_3D('',#350,#351,#352); +#350 = CARTESIAN_POINT('',(-15.,-44.48187929913,80.)); +#351 = DIRECTION('',(2.8E-16,-0.860445129764,-0.50954310776)); +#352 = DIRECTION('',(-1.,4.598339533307E-18,-5.572769301199E-16)); +#353 = ADVANCED_FACE('',(#354,#467),#478,.F.); +#354 = FACE_BOUND('',#355,.F.); +#355 = EDGE_LOOP('',(#356,#366,#375,#383,#391,#399,#407,#415,#423,#431, + #437,#443,#444,#445,#453,#461)); +#356 = ORIENTED_EDGE('',*,*,#357,.F.); +#357 = EDGE_CURVE('',#358,#360,#362,.T.); +#358 = VERTEX_POINT('',#359); +#359 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,80.)); +#360 = VERTEX_POINT('',#361); +#361 = CARTESIAN_POINT('',(-44.,-34.48187929913,80.)); +#362 = LINE('',#363,#364); +#363 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,80.)); +#364 = VECTOR('',#365,1.); +#365 = DIRECTION('',(-0.428144965607,-0.903710068786,-1.E-16)); +#366 = ORIENTED_EDGE('',*,*,#367,.T.); +#367 = EDGE_CURVE('',#358,#368,#370,.T.); +#368 = VERTEX_POINT('',#369); +#369 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,80.)); +#370 = CIRCLE('',#371,25.); +#371 = AXIS2_PLACEMENT_3D('',#372,#373,#374); +#372 = CARTESIAN_POINT('',(0.,0.,80.)); +#373 = DIRECTION('',(2.2E-16,0.,-1.)); +#374 = DIRECTION('',(0.,1.,0.)); +#375 = ORIENTED_EDGE('',*,*,#376,.F.); +#376 = EDGE_CURVE('',#377,#368,#379,.T.); +#377 = VERTEX_POINT('',#378); +#378 = CARTESIAN_POINT('',(44.,-34.48187929913,80.)); +#379 = LINE('',#380,#381); +#380 = CARTESIAN_POINT('',(44.,-34.48187929913,80.)); +#381 = VECTOR('',#382,1.); +#382 = DIRECTION('',(-0.428144965607,0.903710068786,-1.E-16)); +#383 = ORIENTED_EDGE('',*,*,#384,.T.); +#384 = EDGE_CURVE('',#377,#385,#387,.T.); +#385 = VERTEX_POINT('',#386); +#386 = CARTESIAN_POINT('',(44.,-59.48187929913,80.)); +#387 = LINE('',#388,#389); +#388 = CARTESIAN_POINT('',(44.,-34.48187929913,80.)); +#389 = VECTOR('',#390,1.); +#390 = DIRECTION('',(0.,-1.,0.)); +#391 = ORIENTED_EDGE('',*,*,#392,.F.); +#392 = EDGE_CURVE('',#393,#385,#395,.T.); +#393 = VERTEX_POINT('',#394); +#394 = CARTESIAN_POINT('',(30.,-59.48187929913,80.)); +#395 = LINE('',#396,#397); +#396 = CARTESIAN_POINT('',(-44.,-59.48187929913,80.)); +#397 = VECTOR('',#398,1.); +#398 = DIRECTION('',(1.,0.,1.55E-15)); +#399 = ORIENTED_EDGE('',*,*,#400,.T.); +#400 = EDGE_CURVE('',#393,#401,#403,.T.); +#401 = VERTEX_POINT('',#402); +#402 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#403 = LINE('',#404,#405); +#404 = CARTESIAN_POINT('',(30.,-94.48187929913,80.)); +#405 = VECTOR('',#406,1.); +#406 = DIRECTION('',(0.,1.,0.)); +#407 = ORIENTED_EDGE('',*,*,#408,.F.); +#408 = EDGE_CURVE('',#409,#401,#411,.T.); +#409 = VERTEX_POINT('',#410); +#410 = CARTESIAN_POINT('',(30.,-14.48187929913,80.)); +#411 = LINE('',#412,#413); +#412 = CARTESIAN_POINT('',(30.,-14.48187929913,80.)); +#413 = VECTOR('',#414,1.); +#414 = DIRECTION('',(0.,-1.,0.)); +#415 = ORIENTED_EDGE('',*,*,#416,.F.); +#416 = EDGE_CURVE('',#417,#409,#419,.T.); +#417 = VERTEX_POINT('',#418); +#418 = CARTESIAN_POINT('',(15.,-14.48187929913,80.)); +#419 = LINE('',#420,#421); +#420 = CARTESIAN_POINT('',(15.,-14.48187929913,80.)); +#421 = VECTOR('',#422,1.); +#422 = DIRECTION('',(1.,0.,2.2E-16)); +#423 = ORIENTED_EDGE('',*,*,#424,.F.); +#424 = EDGE_CURVE('',#425,#417,#427,.T.); +#425 = VERTEX_POINT('',#426); +#426 = CARTESIAN_POINT('',(15.,-44.48187929913,80.)); +#427 = LINE('',#428,#429); +#428 = CARTESIAN_POINT('',(15.,-44.48187929913,80.)); +#429 = VECTOR('',#430,1.); +#430 = DIRECTION('',(0.,1.,0.)); +#431 = ORIENTED_EDGE('',*,*,#432,.T.); +#432 = EDGE_CURVE('',#425,#334,#433,.T.); +#433 = LINE('',#434,#435); +#434 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#435 = VECTOR('',#436,1.); +#436 = DIRECTION('',(-1.,0.,-2.2E-16)); +#437 = ORIENTED_EDGE('',*,*,#438,.F.); +#438 = EDGE_CURVE('',#312,#334,#439,.T.); +#439 = LINE('',#440,#441); +#440 = CARTESIAN_POINT('',(-15.,-14.48187929913,80.)); +#441 = VECTOR('',#442,1.); +#442 = DIRECTION('',(0.,-1.,0.)); +#443 = ORIENTED_EDGE('',*,*,#319,.F.); +#444 = ORIENTED_EDGE('',*,*,#186,.F.); +#445 = ORIENTED_EDGE('',*,*,#446,.T.); +#446 = EDGE_CURVE('',#187,#447,#449,.T.); +#447 = VERTEX_POINT('',#448); +#448 = CARTESIAN_POINT('',(-30.,-59.48187929913,80.)); +#449 = LINE('',#450,#451); +#450 = CARTESIAN_POINT('',(-30.,-44.48187929913,80.)); +#451 = VECTOR('',#452,1.); +#452 = DIRECTION('',(0.,-1.,0.)); +#453 = ORIENTED_EDGE('',*,*,#454,.F.); +#454 = EDGE_CURVE('',#455,#447,#457,.T.); +#455 = VERTEX_POINT('',#456); +#456 = CARTESIAN_POINT('',(-44.,-59.48187929913,80.)); +#457 = LINE('',#458,#459); +#458 = CARTESIAN_POINT('',(-44.,-59.48187929913,80.)); +#459 = VECTOR('',#460,1.); +#460 = DIRECTION('',(1.,0.,1.55E-15)); +#461 = ORIENTED_EDGE('',*,*,#462,.T.); +#462 = EDGE_CURVE('',#455,#360,#463,.T.); +#463 = LINE('',#464,#465); +#464 = CARTESIAN_POINT('',(-44.,-94.48187929913,80.)); +#465 = VECTOR('',#466,1.); +#466 = DIRECTION('',(0.,1.,0.)); +#467 = FACE_BOUND('',#468,.F.); +#468 = EDGE_LOOP('',(#469)); +#469 = ORIENTED_EDGE('',*,*,#470,.F.); +#470 = EDGE_CURVE('',#471,#471,#473,.T.); +#471 = VERTEX_POINT('',#472); +#472 = CARTESIAN_POINT('',(-9.8E-14,10.,80.)); +#473 = CIRCLE('',#474,10.); +#474 = AXIS2_PLACEMENT_3D('',#475,#476,#477); +#475 = CARTESIAN_POINT('',(0.,0.,80.)); +#476 = DIRECTION('',(2.2E-16,0.,-1.)); +#477 = DIRECTION('',(0.,1.,0.)); +#478 = PLANE('',#479); +#479 = AXIS2_PLACEMENT_3D('',#480,#481,#482); +#480 = CARTESIAN_POINT('',(0.,-43.48690893667,80.)); +#481 = DIRECTION('',(4.4E-16,0.,-1.)); +#482 = DIRECTION('',(-1.,0.,-4.4E-16)); +#483 = ADVANCED_FACE('',(#484),#503,.F.); +#484 = FACE_BOUND('',#485,.F.); +#485 = EDGE_LOOP('',(#486,#495,#501,#502)); +#486 = ORIENTED_EDGE('',*,*,#487,.F.); +#487 = EDGE_CURVE('',#488,#488,#490,.T.); +#488 = VERTEX_POINT('',#489); +#489 = CARTESIAN_POINT('',(-15.,-45.04444206723,94.459258343213)); +#490 = CIRCLE('',#491,7.); +#491 = AXIS2_PLACEMENT_3D('',#492,#493,#494); +#492 = CARTESIAN_POINT('',(-15.,-38.04444206723,94.459258343213)); +#493 = DIRECTION('',(1.,0.,1.19E-15)); +#494 = DIRECTION('',(0.,-1.,0.)); +#495 = ORIENTED_EDGE('',*,*,#496,.T.); +#496 = EDGE_CURVE('',#488,#212,#497,.T.); +#497 = LINE('',#498,#499); +#498 = CARTESIAN_POINT('',(30.,-45.04444206723,94.459258343214)); +#499 = VECTOR('',#500,1.); +#500 = DIRECTION('',(-1.,0.,-1.19E-15)); +#501 = ORIENTED_EDGE('',*,*,#211,.T.); +#502 = ORIENTED_EDGE('',*,*,#496,.F.); +#503 = CYLINDRICAL_SURFACE('',#504,7.); +#504 = AXIS2_PLACEMENT_3D('',#505,#506,#507); +#505 = CARTESIAN_POINT('',(30.,-38.04444206723,94.459258343214)); +#506 = DIRECTION('',(1.,0.,1.19E-15)); +#507 = DIRECTION('',(0.,-1.,0.)); +#508 = ADVANCED_FACE('',(#509,#517),#520,.T.); +#509 = FACE_BOUND('',#510,.T.); +#510 = EDGE_LOOP('',(#511,#512,#513,#514,#515,#516)); +#511 = ORIENTED_EDGE('',*,*,#237,.T.); +#512 = ORIENTED_EDGE('',*,*,#284,.F.); +#513 = ORIENTED_EDGE('',*,*,#333,.F.); +#514 = ORIENTED_EDGE('',*,*,#438,.F.); +#515 = ORIENTED_EDGE('',*,*,#311,.T.); +#516 = ORIENTED_EDGE('',*,*,#268,.F.); +#517 = FACE_BOUND('',#518,.T.); +#518 = EDGE_LOOP('',(#519)); +#519 = ORIENTED_EDGE('',*,*,#487,.F.); +#520 = PLANE('',#521); +#521 = AXIS2_PLACEMENT_3D('',#522,#523,#524); +#522 = CARTESIAN_POINT('',(-15.,-14.48187929913,80.)); +#523 = DIRECTION('',(1.,0.,1.08E-15)); +#524 = DIRECTION('',(0.,-1.,0.)); +#525 = ADVANCED_FACE('',(#526),#558,.T.); +#526 = FACE_BOUND('',#527,.T.); +#527 = EDGE_LOOP('',(#528,#538,#544,#550,#551,#552)); +#528 = ORIENTED_EDGE('',*,*,#529,.T.); +#529 = EDGE_CURVE('',#530,#532,#534,.T.); +#530 = VERTEX_POINT('',#531); +#531 = CARTESIAN_POINT('',(-30.,-44.48187929913,66.)); +#532 = VERTEX_POINT('',#533); +#533 = CARTESIAN_POINT('',(30.,-44.48187929913,66.)); +#534 = LINE('',#535,#536); +#535 = CARTESIAN_POINT('',(-30.,-44.48187929913,66.)); +#536 = VECTOR('',#537,1.); +#537 = DIRECTION('',(1.,0.,2.2E-16)); +#538 = ORIENTED_EDGE('',*,*,#539,.T.); +#539 = EDGE_CURVE('',#532,#401,#540,.T.); +#540 = LINE('',#541,#542); +#541 = CARTESIAN_POINT('',(30.,-44.48187929913,1.998E-14)); +#542 = VECTOR('',#543,1.); +#543 = DIRECTION('',(-6.6E-16,0.,1.)); +#544 = ORIENTED_EDGE('',*,*,#545,.T.); +#545 = EDGE_CURVE('',#401,#425,#546,.T.); +#546 = LINE('',#547,#548); +#547 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#548 = VECTOR('',#549,1.); +#549 = DIRECTION('',(-1.,0.,-2.2E-16)); +#550 = ORIENTED_EDGE('',*,*,#432,.T.); +#551 = ORIENTED_EDGE('',*,*,#343,.T.); +#552 = ORIENTED_EDGE('',*,*,#553,.F.); +#553 = EDGE_CURVE('',#530,#187,#554,.T.); +#554 = LINE('',#555,#556); +#555 = CARTESIAN_POINT('',(-30.,-44.48187929913,-1.998E-14)); +#556 = VECTOR('',#557,1.); +#557 = DIRECTION('',(-6.6E-16,0.,1.)); +#558 = PLANE('',#559); +#559 = AXIS2_PLACEMENT_3D('',#560,#561,#562); +#560 = CARTESIAN_POINT('',(30.,-44.48187929913,1.998E-14)); +#561 = DIRECTION('',(0.,-1.,0.)); +#562 = DIRECTION('',(-1.,-0.,-6.6E-16)); +#563 = ADVANCED_FACE('',(#564),#621,.F.); +#564 = FACE_BOUND('',#565,.F.); +#565 = EDGE_LOOP('',(#566,#576,#584,#592,#600,#608,#614,#615)); +#566 = ORIENTED_EDGE('',*,*,#567,.F.); +#567 = EDGE_CURVE('',#568,#570,#572,.T.); +#568 = VERTEX_POINT('',#569); +#569 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,-1.8E-14)); +#570 = VERTEX_POINT('',#571); +#571 = CARTESIAN_POINT('',(-44.,-34.48187929913,-3.4E-14)); +#572 = LINE('',#573,#574); +#573 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,-5.02E-15)); +#574 = VECTOR('',#575,1.); +#575 = DIRECTION('',(-0.428144965607,-0.903710068786,-1.E-16)); +#576 = ORIENTED_EDGE('',*,*,#577,.T.); +#577 = EDGE_CURVE('',#568,#578,#580,.T.); +#578 = VERTEX_POINT('',#579); +#579 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,14.)); +#580 = LINE('',#581,#582); +#581 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,-1.505E-14)); +#582 = VECTOR('',#583,1.); +#583 = DIRECTION('',(-6.6E-16,0.,1.)); +#584 = ORIENTED_EDGE('',*,*,#585,.T.); +#585 = EDGE_CURVE('',#578,#586,#588,.T.); +#586 = VERTEX_POINT('',#587); +#587 = CARTESIAN_POINT('',(-30.,-4.931278499541,14.)); +#588 = LINE('',#589,#590); +#589 = CARTESIAN_POINT('',(-28.44857031316,-1.656587117801,14.)); +#590 = VECTOR('',#591,1.); +#591 = DIRECTION('',(-0.428144965607,-0.903710068786,-2.E-16)); +#592 = ORIENTED_EDGE('',*,*,#593,.T.); +#593 = EDGE_CURVE('',#586,#594,#596,.T.); +#594 = VERTEX_POINT('',#595); +#595 = CARTESIAN_POINT('',(-30.,-4.93127849954,66.)); +#596 = LINE('',#597,#598); +#597 = CARTESIAN_POINT('',(-30.,-4.931278499541,7.)); +#598 = VECTOR('',#599,1.); +#599 = DIRECTION('',(-4.4E-16,1.41E-15,1.)); +#600 = ORIENTED_EDGE('',*,*,#601,.T.); +#601 = EDGE_CURVE('',#594,#602,#604,.T.); +#602 = VERTEX_POINT('',#603); +#603 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,66.)); +#604 = LINE('',#605,#606); +#605 = CARTESIAN_POINT('',(-33.9478136604,-13.26415460737,66.)); +#606 = VECTOR('',#607,1.); +#607 = DIRECTION('',(0.428144965607,0.903710068786,2.E-16)); +#608 = ORIENTED_EDGE('',*,*,#609,.T.); +#609 = EDGE_CURVE('',#602,#358,#610,.T.); +#610 = LINE('',#611,#612); +#611 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,-1.505E-14)); +#612 = VECTOR('',#613,1.); +#613 = DIRECTION('',(-6.6E-16,0.,1.)); +#614 = ORIENTED_EDGE('',*,*,#357,.T.); +#615 = ORIENTED_EDGE('',*,*,#616,.F.); +#616 = EDGE_CURVE('',#570,#360,#617,.T.); +#617 = LINE('',#618,#619); +#618 = CARTESIAN_POINT('',(-44.,-34.48187929913,-2.931E-14)); +#619 = VECTOR('',#620,1.); +#620 = DIRECTION('',(-6.6E-16,0.,1.)); +#621 = PLANE('',#622); +#622 = AXIS2_PLACEMENT_3D('',#623,#624,#625); +#623 = CARTESIAN_POINT('',(-22.59275171965,10.703624140174,-1.505E-14)); +#624 = DIRECTION('',(0.903710068786,-0.428144965607,6.E-16)); +#625 = DIRECTION('',(-0.428144965607,-0.903710068786,-2.9E-16)); +#626 = ADVANCED_FACE('',(#627,#671),#682,.T.); +#627 = FACE_BOUND('',#628,.T.); +#628 = EDGE_LOOP('',(#629,#637,#646,#654,#663,#669,#670)); +#629 = ORIENTED_EDGE('',*,*,#630,.F.); +#630 = EDGE_CURVE('',#631,#570,#633,.T.); +#631 = VERTEX_POINT('',#632); +#632 = CARTESIAN_POINT('',(-44.,-78.78187929913,-4.5E-14)); +#633 = LINE('',#634,#635); +#634 = CARTESIAN_POINT('',(-44.,-94.48187929913,-9.77E-15)); +#635 = VECTOR('',#636,1.); +#636 = DIRECTION('',(0.,1.,0.)); +#637 = ORIENTED_EDGE('',*,*,#638,.T.); +#638 = EDGE_CURVE('',#631,#639,#641,.T.); +#639 = VERTEX_POINT('',#640); +#640 = CARTESIAN_POINT('',(-44.,-94.48187929913,15.7)); +#641 = CIRCLE('',#642,15.7); +#642 = AXIS2_PLACEMENT_3D('',#643,#644,#645); +#643 = CARTESIAN_POINT('',(-44.,-78.78187929913,15.7)); +#644 = DIRECTION('',(-1.,0.,-5.6E-16)); +#645 = DIRECTION('',(-5.6E-16,0.,1.)); +#646 = ORIENTED_EDGE('',*,*,#647,.T.); +#647 = EDGE_CURVE('',#639,#648,#650,.T.); +#648 = VERTEX_POINT('',#649); +#649 = CARTESIAN_POINT('',(-44.,-94.48187929913,15.755518480805)); +#650 = LINE('',#651,#652); +#651 = CARTESIAN_POINT('',(-44.,-94.48187929913,-2.931E-14)); +#652 = VECTOR('',#653,1.); +#653 = DIRECTION('',(-6.6E-16,0.,1.)); +#654 = ORIENTED_EDGE('',*,*,#655,.T.); +#655 = EDGE_CURVE('',#648,#656,#658,.T.); +#656 = VERTEX_POINT('',#657); +#657 = CARTESIAN_POINT('',(-44.,-92.34320804323,23.666293581533)); +#658 = CIRCLE('',#659,15.7); +#659 = AXIS2_PLACEMENT_3D('',#660,#661,#662); +#660 = CARTESIAN_POINT('',(-44.,-78.78187929913,15.755518480805)); +#661 = DIRECTION('',(-1.,-0.,-1.89E-15)); +#662 = DIRECTION('',(1.89E-15,0.,-1.)); +#663 = ORIENTED_EDGE('',*,*,#664,.T.); +#664 = EDGE_CURVE('',#656,#455,#665,.T.); +#665 = LINE('',#666,#667); +#666 = CARTESIAN_POINT('',(-44.,-94.48187929913,20.)); +#667 = VECTOR('',#668,1.); +#668 = DIRECTION('',(-1.053778900898E-15,0.503871025524,0.863778900898) + ); +#669 = ORIENTED_EDGE('',*,*,#462,.T.); +#670 = ORIENTED_EDGE('',*,*,#616,.F.); +#671 = FACE_BOUND('',#672,.T.); +#672 = EDGE_LOOP('',(#673)); +#673 = ORIENTED_EDGE('',*,*,#674,.F.); +#674 = EDGE_CURVE('',#675,#675,#677,.T.); +#675 = VERTEX_POINT('',#676); +#676 = CARTESIAN_POINT('',(-44.,-73.,15.)); +#677 = CIRCLE('',#678,7.); +#678 = AXIS2_PLACEMENT_3D('',#679,#680,#681); +#679 = CARTESIAN_POINT('',(-44.,-80.,15.)); +#680 = DIRECTION('',(-1.,0.,-1.22E-15)); +#681 = DIRECTION('',(0.,1.,0.)); +#682 = PLANE('',#683); +#683 = AXIS2_PLACEMENT_3D('',#684,#685,#686); +#684 = CARTESIAN_POINT('',(-44.,-94.48187929913,-2.931E-14)); +#685 = DIRECTION('',(-1.,0.,-6.6E-16)); +#686 = DIRECTION('',(0.,1.,0.)); +#687 = ADVANCED_FACE('',(#688),#706,.T.); +#688 = FACE_BOUND('',#689,.T.); +#689 = EDGE_LOOP('',(#690,#691,#699,#705)); +#690 = ORIENTED_EDGE('',*,*,#664,.F.); +#691 = ORIENTED_EDGE('',*,*,#692,.T.); +#692 = EDGE_CURVE('',#656,#693,#695,.T.); +#693 = VERTEX_POINT('',#694); +#694 = CARTESIAN_POINT('',(-30.,-92.34320804323,23.666293581533)); +#695 = LINE('',#696,#697); +#696 = CARTESIAN_POINT('',(-44.,-92.34320804323,23.666293581533)); +#697 = VECTOR('',#698,1.); +#698 = DIRECTION('',(1.,0.,1.89E-15)); +#699 = ORIENTED_EDGE('',*,*,#700,.F.); +#700 = EDGE_CURVE('',#447,#693,#701,.T.); +#701 = LINE('',#702,#703); +#702 = CARTESIAN_POINT('',(-30.,-92.48706064628,23.419689119171)); +#703 = VECTOR('',#704,1.); +#704 = DIRECTION('',(1.34E-15,-0.503871025524,-0.863778900898)); +#705 = ORIENTED_EDGE('',*,*,#454,.F.); +#706 = PLANE('',#707); +#707 = AXIS2_PLACEMENT_3D('',#708,#709,#710); +#708 = CARTESIAN_POINT('',(-44.,-94.48187929913,20.)); +#709 = DIRECTION('',(-7.8E-16,-0.863778900898,0.503871025524)); +#710 = DIRECTION('',(-1.34E-15,0.503871025524,0.863778900898)); +#711 = ADVANCED_FACE('',(#712,#765),#776,.T.); +#712 = FACE_BOUND('',#713,.T.); +#713 = EDGE_LOOP('',(#714,#724,#732,#738,#739,#740,#741,#750,#758)); +#714 = ORIENTED_EDGE('',*,*,#715,.F.); +#715 = EDGE_CURVE('',#716,#718,#720,.T.); +#716 = VERTEX_POINT('',#717); +#717 = CARTESIAN_POINT('',(-30.,-44.48187929913,-2.3E-14)); +#718 = VERTEX_POINT('',#719); +#719 = CARTESIAN_POINT('',(-30.,-78.78187929913,-3.3E-14)); +#720 = LINE('',#721,#722); +#721 = CARTESIAN_POINT('',(-30.,-44.48187929913,-6.66E-15)); +#722 = VECTOR('',#723,1.); +#723 = DIRECTION('',(0.,-1.,0.)); +#724 = ORIENTED_EDGE('',*,*,#725,.T.); +#725 = EDGE_CURVE('',#716,#726,#728,.T.); +#726 = VERTEX_POINT('',#727); +#727 = CARTESIAN_POINT('',(-30.,-44.48187929913,14.)); +#728 = LINE('',#729,#730); +#729 = CARTESIAN_POINT('',(-30.,-44.48187929913,-1.998E-14)); +#730 = VECTOR('',#731,1.); +#731 = DIRECTION('',(-6.6E-16,0.,1.)); +#732 = ORIENTED_EDGE('',*,*,#733,.T.); +#733 = EDGE_CURVE('',#726,#530,#734,.T.); +#734 = LINE('',#735,#736); +#735 = CARTESIAN_POINT('',(-30.,-44.48187929913,14.)); +#736 = VECTOR('',#737,1.); +#737 = DIRECTION('',(-2.2E-16,-0.,1.)); +#738 = ORIENTED_EDGE('',*,*,#553,.T.); +#739 = ORIENTED_EDGE('',*,*,#446,.T.); +#740 = ORIENTED_EDGE('',*,*,#700,.T.); +#741 = ORIENTED_EDGE('',*,*,#742,.F.); +#742 = EDGE_CURVE('',#743,#693,#745,.T.); +#743 = VERTEX_POINT('',#744); +#744 = CARTESIAN_POINT('',(-30.,-94.48187929913,15.755518480805)); +#745 = CIRCLE('',#746,15.7); +#746 = AXIS2_PLACEMENT_3D('',#747,#748,#749); +#747 = CARTESIAN_POINT('',(-30.,-78.78187929913,15.755518480805)); +#748 = DIRECTION('',(-1.,-0.,-1.89E-15)); +#749 = DIRECTION('',(1.89E-15,0.,-1.)); +#750 = ORIENTED_EDGE('',*,*,#751,.F.); +#751 = EDGE_CURVE('',#752,#743,#754,.T.); +#752 = VERTEX_POINT('',#753); +#753 = CARTESIAN_POINT('',(-30.,-94.48187929913,15.7)); +#754 = LINE('',#755,#756); +#755 = CARTESIAN_POINT('',(-30.,-94.48187929913,-1.998E-14)); +#756 = VECTOR('',#757,1.); +#757 = DIRECTION('',(-6.6E-16,0.,1.)); +#758 = ORIENTED_EDGE('',*,*,#759,.F.); +#759 = EDGE_CURVE('',#718,#752,#760,.T.); +#760 = CIRCLE('',#761,15.7); +#761 = AXIS2_PLACEMENT_3D('',#762,#763,#764); +#762 = CARTESIAN_POINT('',(-30.,-78.78187929913,15.7)); +#763 = DIRECTION('',(-1.,0.,-5.6E-16)); +#764 = DIRECTION('',(-5.6E-16,0.,1.)); +#765 = FACE_BOUND('',#766,.T.); +#766 = EDGE_LOOP('',(#767)); +#767 = ORIENTED_EDGE('',*,*,#768,.T.); +#768 = EDGE_CURVE('',#769,#769,#771,.T.); +#769 = VERTEX_POINT('',#770); +#770 = CARTESIAN_POINT('',(-30.,-73.,15.)); +#771 = CIRCLE('',#772,7.); +#772 = AXIS2_PLACEMENT_3D('',#773,#774,#775); +#773 = CARTESIAN_POINT('',(-30.,-80.,15.)); +#774 = DIRECTION('',(-1.,0.,-7.7E-16)); +#775 = DIRECTION('',(0.,1.,0.)); +#776 = PLANE('',#777); +#777 = AXIS2_PLACEMENT_3D('',#778,#779,#780); +#778 = CARTESIAN_POINT('',(-30.,-44.48187929913,-1.998E-14)); +#779 = DIRECTION('',(1.,0.,6.6E-16)); +#780 = DIRECTION('',(0.,-1.,0.)); +#781 = ADVANCED_FACE('',(#782,#825),#836,.T.); +#782 = FACE_BOUND('',#783,.T.); +#783 = EDGE_LOOP('',(#784,#794,#803,#809,#810,#818)); +#784 = ORIENTED_EDGE('',*,*,#785,.T.); +#785 = EDGE_CURVE('',#786,#788,#790,.T.); +#786 = VERTEX_POINT('',#787); +#787 = CARTESIAN_POINT('',(15.,-38.82620606324,106.)); +#788 = VERTEX_POINT('',#789); +#789 = CARTESIAN_POINT('',(15.,-38.77075908679,106.)); +#790 = LINE('',#791,#792); +#791 = CARTESIAN_POINT('',(15.,-59.8787016455,106.)); +#792 = VECTOR('',#793,1.); +#793 = DIRECTION('',(0.,1.,0.)); +#794 = ORIENTED_EDGE('',*,*,#795,.F.); +#795 = EDGE_CURVE('',#796,#788,#798,.T.); +#796 = VERTEX_POINT('',#797); +#797 = CARTESIAN_POINT('',(15.,-25.34781506248,98.348872481061)); +#798 = CIRCLE('',#799,15.6); +#799 = AXIS2_PLACEMENT_3D('',#800,#801,#802); +#800 = CARTESIAN_POINT('',(15.,-38.77075908679,90.4)); +#801 = DIRECTION('',(1.,-0.,6.7E-16)); +#802 = DIRECTION('',(6.7E-16,0.,-1.)); +#803 = ORIENTED_EDGE('',*,*,#804,.F.); +#804 = EDGE_CURVE('',#417,#796,#805,.T.); +#805 = LINE('',#806,#807); +#806 = CARTESIAN_POINT('',(15.,-14.48187929913,80.)); +#807 = VECTOR('',#808,1.); +#808 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#809 = ORIENTED_EDGE('',*,*,#424,.F.); +#810 = ORIENTED_EDGE('',*,*,#811,.T.); +#811 = EDGE_CURVE('',#425,#812,#814,.T.); +#812 = VERTEX_POINT('',#813); +#813 = CARTESIAN_POINT('',(15.,-49.1515476204,87.885482706876)); +#814 = LINE('',#815,#816); +#815 = CARTESIAN_POINT('',(15.,-44.48187929913,80.)); +#816 = VECTOR('',#817,1.); +#817 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#818 = ORIENTED_EDGE('',*,*,#819,.T.); +#819 = EDGE_CURVE('',#812,#786,#820,.T.); +#820 = CIRCLE('',#821,12.); +#821 = AXIS2_PLACEMENT_3D('',#822,#823,#824); +#822 = CARTESIAN_POINT('',(15.,-38.82620606324,94.)); +#823 = DIRECTION('',(-1.,0.,-6.7E-16)); +#824 = DIRECTION('',(-6.7E-16,0.,1.)); +#825 = FACE_BOUND('',#826,.T.); +#826 = EDGE_LOOP('',(#827)); +#827 = ORIENTED_EDGE('',*,*,#828,.T.); +#828 = EDGE_CURVE('',#829,#829,#831,.T.); +#829 = VERTEX_POINT('',#830); +#830 = CARTESIAN_POINT('',(15.,-45.04444206723,94.459258343214)); +#831 = CIRCLE('',#832,7.); +#832 = AXIS2_PLACEMENT_3D('',#833,#834,#835); +#833 = CARTESIAN_POINT('',(15.,-38.04444206723,94.459258343214)); +#834 = DIRECTION('',(1.,0.,1.19E-15)); +#835 = DIRECTION('',(0.,-1.,0.)); +#836 = PLANE('',#837); +#837 = AXIS2_PLACEMENT_3D('',#838,#839,#840); +#838 = CARTESIAN_POINT('',(15.,-44.48187929913,80.)); +#839 = DIRECTION('',(-1.,0.,-1.08E-15)); +#840 = DIRECTION('',(0.,1.,0.)); +#841 = ADVANCED_FACE('',(#842),#860,.T.); +#842 = FACE_BOUND('',#843,.T.); +#843 = EDGE_LOOP('',(#844,#845,#853,#859)); +#844 = ORIENTED_EDGE('',*,*,#804,.T.); +#845 = ORIENTED_EDGE('',*,*,#846,.T.); +#846 = EDGE_CURVE('',#796,#847,#849,.T.); +#847 = VERTEX_POINT('',#848); +#848 = CARTESIAN_POINT('',(30.,-25.34781506248,98.348872481061)); +#849 = LINE('',#850,#851); +#850 = CARTESIAN_POINT('',(15.,-25.34781506248,98.348872481061)); +#851 = VECTOR('',#852,1.); +#852 = DIRECTION('',(1.,0.,6.7E-16)); +#853 = ORIENTED_EDGE('',*,*,#854,.F.); +#854 = EDGE_CURVE('',#409,#847,#855,.T.); +#855 = LINE('',#856,#857); +#856 = CARTESIAN_POINT('',(30.,-14.48187929913,80.)); +#857 = VECTOR('',#858,1.); +#858 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#859 = ORIENTED_EDGE('',*,*,#416,.F.); +#860 = PLANE('',#861); +#861 = AXIS2_PLACEMENT_3D('',#862,#863,#864); +#862 = CARTESIAN_POINT('',(15.,-14.48187929913,80.)); +#863 = DIRECTION('',(-2.8E-16,0.860445129764,0.50954310776)); +#864 = DIRECTION('',(1.,-4.598339533307E-18,5.572769301199E-16)); +#865 = ADVANCED_FACE('',(#866,#902),#913,.T.); +#866 = FACE_BOUND('',#867,.T.); +#867 = EDGE_LOOP('',(#868,#878,#887,#893,#894,#895)); +#868 = ORIENTED_EDGE('',*,*,#869,.T.); +#869 = EDGE_CURVE('',#870,#872,#874,.T.); +#870 = VERTEX_POINT('',#871); +#871 = CARTESIAN_POINT('',(30.,-38.77075908679,106.)); +#872 = VERTEX_POINT('',#873); +#873 = CARTESIAN_POINT('',(30.,-38.82620606324,106.)); +#874 = LINE('',#875,#876); +#875 = CARTESIAN_POINT('',(30.,-29.8787016455,106.)); +#876 = VECTOR('',#877,1.); +#877 = DIRECTION('',(0.,-1.,0.)); +#878 = ORIENTED_EDGE('',*,*,#879,.F.); +#879 = EDGE_CURVE('',#880,#872,#882,.T.); +#880 = VERTEX_POINT('',#881); +#881 = CARTESIAN_POINT('',(30.,-49.1515476204,87.885482706876)); +#882 = CIRCLE('',#883,12.); +#883 = AXIS2_PLACEMENT_3D('',#884,#885,#886); +#884 = CARTESIAN_POINT('',(30.,-38.82620606324,94.)); +#885 = DIRECTION('',(-1.,0.,-6.7E-16)); +#886 = DIRECTION('',(-6.7E-16,0.,1.)); +#887 = ORIENTED_EDGE('',*,*,#888,.F.); +#888 = EDGE_CURVE('',#401,#880,#889,.T.); +#889 = LINE('',#890,#891); +#890 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#891 = VECTOR('',#892,1.); +#892 = DIRECTION('',(-9.3E-16,-0.50954310776,0.860445129764)); +#893 = ORIENTED_EDGE('',*,*,#408,.F.); +#894 = ORIENTED_EDGE('',*,*,#854,.T.); +#895 = ORIENTED_EDGE('',*,*,#896,.T.); +#896 = EDGE_CURVE('',#847,#870,#897,.T.); +#897 = CIRCLE('',#898,15.6); +#898 = AXIS2_PLACEMENT_3D('',#899,#900,#901); +#899 = CARTESIAN_POINT('',(30.,-38.77075908679,90.4)); +#900 = DIRECTION('',(1.,-0.,6.7E-16)); +#901 = DIRECTION('',(6.7E-16,0.,-1.)); +#902 = FACE_BOUND('',#903,.T.); +#903 = EDGE_LOOP('',(#904)); +#904 = ORIENTED_EDGE('',*,*,#905,.F.); +#905 = EDGE_CURVE('',#906,#906,#908,.T.); +#906 = VERTEX_POINT('',#907); +#907 = CARTESIAN_POINT('',(30.,-45.04444206723,94.459258343214)); +#908 = CIRCLE('',#909,7.); +#909 = AXIS2_PLACEMENT_3D('',#910,#911,#912); +#910 = CARTESIAN_POINT('',(30.,-38.04444206723,94.459258343214)); +#911 = DIRECTION('',(1.,0.,1.22E-15)); +#912 = DIRECTION('',(0.,-1.,0.)); +#913 = PLANE('',#914); +#914 = AXIS2_PLACEMENT_3D('',#915,#916,#917); +#915 = CARTESIAN_POINT('',(30.,-14.48187929913,80.)); +#916 = DIRECTION('',(1.,0.,1.08E-15)); +#917 = DIRECTION('',(0.,-1.,0.)); +#918 = ADVANCED_FACE('',(#919,#979),#990,.T.); +#919 = FACE_BOUND('',#920,.T.); +#920 = EDGE_LOOP('',(#921,#931,#940,#948,#957,#963,#964,#965,#973)); +#921 = ORIENTED_EDGE('',*,*,#922,.F.); +#922 = EDGE_CURVE('',#923,#925,#927,.T.); +#923 = VERTEX_POINT('',#924); +#924 = CARTESIAN_POINT('',(30.,-78.78187929913,3.E-14)); +#925 = VERTEX_POINT('',#926); +#926 = CARTESIAN_POINT('',(30.,-44.48187929913,2.3E-14)); +#927 = LINE('',#928,#929); +#928 = CARTESIAN_POINT('',(30.,-94.48187929913,6.66E-15)); +#929 = VECTOR('',#930,1.); +#930 = DIRECTION('',(0.,1.,0.)); +#931 = ORIENTED_EDGE('',*,*,#932,.T.); +#932 = EDGE_CURVE('',#923,#933,#935,.T.); +#933 = VERTEX_POINT('',#934); +#934 = CARTESIAN_POINT('',(30.,-94.48187929913,15.7)); +#935 = CIRCLE('',#936,15.7); +#936 = AXIS2_PLACEMENT_3D('',#937,#938,#939); +#937 = CARTESIAN_POINT('',(30.,-78.78187929913,15.7)); +#938 = DIRECTION('',(-1.,0.,-5.6E-16)); +#939 = DIRECTION('',(-5.6E-16,0.,1.)); +#940 = ORIENTED_EDGE('',*,*,#941,.T.); +#941 = EDGE_CURVE('',#933,#942,#944,.T.); +#942 = VERTEX_POINT('',#943); +#943 = CARTESIAN_POINT('',(30.,-94.48187929913,15.755518480805)); +#944 = LINE('',#945,#946); +#945 = CARTESIAN_POINT('',(30.,-94.48187929913,1.998E-14)); +#946 = VECTOR('',#947,1.); +#947 = DIRECTION('',(-6.6E-16,0.,1.)); +#948 = ORIENTED_EDGE('',*,*,#949,.T.); +#949 = EDGE_CURVE('',#942,#950,#952,.T.); +#950 = VERTEX_POINT('',#951); +#951 = CARTESIAN_POINT('',(30.,-92.34320804323,23.666293581534)); +#952 = CIRCLE('',#953,15.7); +#953 = AXIS2_PLACEMENT_3D('',#954,#955,#956); +#954 = CARTESIAN_POINT('',(30.,-78.78187929913,15.755518480805)); +#955 = DIRECTION('',(-1.,-0.,-1.89E-15)); +#956 = DIRECTION('',(1.89E-15,0.,-1.)); +#957 = ORIENTED_EDGE('',*,*,#958,.T.); +#958 = EDGE_CURVE('',#950,#393,#959,.T.); +#959 = LINE('',#960,#961); +#960 = CARTESIAN_POINT('',(30.,-98.83421090535,12.538860103627)); +#961 = VECTOR('',#962,1.); +#962 = DIRECTION('',(-1.34E-15,0.503871025524,0.863778900898)); +#963 = ORIENTED_EDGE('',*,*,#400,.T.); +#964 = ORIENTED_EDGE('',*,*,#539,.F.); +#965 = ORIENTED_EDGE('',*,*,#966,.T.); +#966 = EDGE_CURVE('',#532,#967,#969,.T.); +#967 = VERTEX_POINT('',#968); +#968 = CARTESIAN_POINT('',(30.,-44.48187929913,14.)); +#969 = LINE('',#970,#971); +#970 = CARTESIAN_POINT('',(30.,-44.48187929913,66.)); +#971 = VECTOR('',#972,1.); +#972 = DIRECTION('',(2.2E-16,0.,-1.)); +#973 = ORIENTED_EDGE('',*,*,#974,.F.); +#974 = EDGE_CURVE('',#925,#967,#975,.T.); +#975 = LINE('',#976,#977); +#976 = CARTESIAN_POINT('',(30.,-44.48187929913,1.998E-14)); +#977 = VECTOR('',#978,1.); +#978 = DIRECTION('',(-6.6E-16,0.,1.)); +#979 = FACE_BOUND('',#980,.T.); +#980 = EDGE_LOOP('',(#981)); +#981 = ORIENTED_EDGE('',*,*,#982,.F.); +#982 = EDGE_CURVE('',#983,#983,#985,.T.); +#983 = VERTEX_POINT('',#984); +#984 = CARTESIAN_POINT('',(30.,-73.,15.)); +#985 = CIRCLE('',#986,7.); +#986 = AXIS2_PLACEMENT_3D('',#987,#988,#989); +#987 = CARTESIAN_POINT('',(30.,-80.,15.)); +#988 = DIRECTION('',(-1.,0.,-7.7E-16)); +#989 = DIRECTION('',(0.,1.,0.)); +#990 = PLANE('',#991); +#991 = AXIS2_PLACEMENT_3D('',#992,#993,#994); +#992 = CARTESIAN_POINT('',(30.,-94.48187929913,1.998E-14)); +#993 = DIRECTION('',(-1.,0.,-6.6E-16)); +#994 = DIRECTION('',(0.,1.,0.)); +#995 = ADVANCED_FACE('',(#996),#1014,.T.); +#996 = FACE_BOUND('',#997,.T.); +#997 = EDGE_LOOP('',(#998,#999,#1007,#1013)); +#998 = ORIENTED_EDGE('',*,*,#958,.F.); +#999 = ORIENTED_EDGE('',*,*,#1000,.T.); +#1000 = EDGE_CURVE('',#950,#1001,#1003,.T.); +#1001 = VERTEX_POINT('',#1002); +#1002 = CARTESIAN_POINT('',(44.,-92.34320804323,23.666293581534)); +#1003 = LINE('',#1004,#1005); +#1004 = CARTESIAN_POINT('',(30.,-92.34320804323,23.666293581534)); +#1005 = VECTOR('',#1006,1.); +#1006 = DIRECTION('',(1.,0.,1.89E-15)); +#1007 = ORIENTED_EDGE('',*,*,#1008,.F.); +#1008 = EDGE_CURVE('',#385,#1001,#1009,.T.); +#1009 = LINE('',#1010,#1011); +#1010 = CARTESIAN_POINT('',(44.,-91.21763059447,25.59585492228)); +#1011 = VECTOR('',#1012,1.); +#1012 = DIRECTION('',(1.34E-15,-0.503871025524,-0.863778900898)); +#1013 = ORIENTED_EDGE('',*,*,#392,.F.); +#1014 = PLANE('',#1015); +#1015 = AXIS2_PLACEMENT_3D('',#1016,#1017,#1018); +#1016 = CARTESIAN_POINT('',(-44.,-94.48187929913,20.)); +#1017 = DIRECTION('',(-7.8E-16,-0.863778900898,0.503871025524)); +#1018 = DIRECTION('',(-1.34E-15,0.503871025524,0.863778900898)); +#1019 = ADVANCED_FACE('',(#1020,#1064),#1075,.T.); +#1020 = FACE_BOUND('',#1021,.T.); +#1021 = EDGE_LOOP('',(#1022,#1032,#1038,#1039,#1040,#1049,#1057)); +#1022 = ORIENTED_EDGE('',*,*,#1023,.F.); +#1023 = EDGE_CURVE('',#1024,#1026,#1028,.T.); +#1024 = VERTEX_POINT('',#1025); +#1025 = CARTESIAN_POINT('',(44.,-34.48187929913,3.4E-14)); +#1026 = VERTEX_POINT('',#1027); +#1027 = CARTESIAN_POINT('',(44.,-78.78187929913,4.2E-14)); +#1028 = LINE('',#1029,#1030); +#1029 = CARTESIAN_POINT('',(44.,-34.48187929913,9.77E-15)); +#1030 = VECTOR('',#1031,1.); +#1031 = DIRECTION('',(0.,-1.,0.)); +#1032 = ORIENTED_EDGE('',*,*,#1033,.T.); +#1033 = EDGE_CURVE('',#1024,#377,#1034,.T.); +#1034 = LINE('',#1035,#1036); +#1035 = CARTESIAN_POINT('',(44.,-34.48187929913,2.931E-14)); +#1036 = VECTOR('',#1037,1.); +#1037 = DIRECTION('',(-6.6E-16,0.,1.)); +#1038 = ORIENTED_EDGE('',*,*,#384,.T.); +#1039 = ORIENTED_EDGE('',*,*,#1008,.T.); +#1040 = ORIENTED_EDGE('',*,*,#1041,.F.); +#1041 = EDGE_CURVE('',#1042,#1001,#1044,.T.); +#1042 = VERTEX_POINT('',#1043); +#1043 = CARTESIAN_POINT('',(44.,-94.48187929913,15.755518480805)); +#1044 = CIRCLE('',#1045,15.7); +#1045 = AXIS2_PLACEMENT_3D('',#1046,#1047,#1048); +#1046 = CARTESIAN_POINT('',(44.,-78.78187929913,15.755518480805)); +#1047 = DIRECTION('',(-1.,-0.,-1.89E-15)); +#1048 = DIRECTION('',(1.89E-15,0.,-1.)); +#1049 = ORIENTED_EDGE('',*,*,#1050,.F.); +#1050 = EDGE_CURVE('',#1051,#1042,#1053,.T.); +#1051 = VERTEX_POINT('',#1052); +#1052 = CARTESIAN_POINT('',(44.,-94.48187929913,15.7)); +#1053 = LINE('',#1054,#1055); +#1054 = CARTESIAN_POINT('',(44.,-94.48187929913,2.931E-14)); +#1055 = VECTOR('',#1056,1.); +#1056 = DIRECTION('',(-6.6E-16,0.,1.)); +#1057 = ORIENTED_EDGE('',*,*,#1058,.F.); +#1058 = EDGE_CURVE('',#1026,#1051,#1059,.T.); +#1059 = CIRCLE('',#1060,15.7); +#1060 = AXIS2_PLACEMENT_3D('',#1061,#1062,#1063); +#1061 = CARTESIAN_POINT('',(44.,-78.78187929913,15.7)); +#1062 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1063 = DIRECTION('',(-5.6E-16,0.,1.)); +#1064 = FACE_BOUND('',#1065,.T.); +#1065 = EDGE_LOOP('',(#1066)); +#1066 = ORIENTED_EDGE('',*,*,#1067,.T.); +#1067 = EDGE_CURVE('',#1068,#1068,#1070,.T.); +#1068 = VERTEX_POINT('',#1069); +#1069 = CARTESIAN_POINT('',(44.,-73.,15.)); +#1070 = CIRCLE('',#1071,7.); +#1071 = AXIS2_PLACEMENT_3D('',#1072,#1073,#1074); +#1072 = CARTESIAN_POINT('',(44.,-80.,15.)); +#1073 = DIRECTION('',(-1.,0.,-7.7E-16)); +#1074 = DIRECTION('',(0.,1.,0.)); +#1075 = PLANE('',#1076); +#1076 = AXIS2_PLACEMENT_3D('',#1077,#1078,#1079); +#1077 = CARTESIAN_POINT('',(44.,-34.48187929913,2.931E-14)); +#1078 = DIRECTION('',(1.,0.,6.6E-16)); +#1079 = DIRECTION('',(0.,-1.,0.)); +#1080 = ADVANCED_FACE('',(#1081),#1131,.F.); +#1081 = FACE_BOUND('',#1082,.F.); +#1082 = EDGE_LOOP('',(#1083,#1091,#1092,#1093,#1101,#1109,#1117,#1125)); +#1083 = ORIENTED_EDGE('',*,*,#1084,.F.); +#1084 = EDGE_CURVE('',#1024,#1085,#1087,.T.); +#1085 = VERTEX_POINT('',#1086); +#1086 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,1.8E-14)); +#1087 = LINE('',#1088,#1089); +#1088 = CARTESIAN_POINT('',(44.,-34.48187929913,9.77E-15)); +#1089 = VECTOR('',#1090,1.); +#1090 = DIRECTION('',(-0.428144965607,0.903710068786,-1.E-16)); +#1091 = ORIENTED_EDGE('',*,*,#1033,.T.); +#1092 = ORIENTED_EDGE('',*,*,#376,.T.); +#1093 = ORIENTED_EDGE('',*,*,#1094,.F.); +#1094 = EDGE_CURVE('',#1095,#368,#1097,.T.); +#1095 = VERTEX_POINT('',#1096); +#1096 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,66.)); +#1097 = LINE('',#1098,#1099); +#1098 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,1.505E-14)); +#1099 = VECTOR('',#1100,1.); +#1100 = DIRECTION('',(-6.6E-16,0.,1.)); +#1101 = ORIENTED_EDGE('',*,*,#1102,.T.); +#1102 = EDGE_CURVE('',#1095,#1103,#1105,.T.); +#1103 = VERTEX_POINT('',#1104); +#1104 = CARTESIAN_POINT('',(30.,-4.931278499541,66.)); +#1105 = LINE('',#1106,#1107); +#1106 = CARTESIAN_POINT('',(39.152194453337,-24.24933883745,66.)); +#1107 = VECTOR('',#1108,1.); +#1108 = DIRECTION('',(0.428144965607,-0.903710068786,2.E-16)); +#1109 = ORIENTED_EDGE('',*,*,#1110,.T.); +#1110 = EDGE_CURVE('',#1103,#1111,#1113,.T.); +#1111 = VERTEX_POINT('',#1112); +#1112 = CARTESIAN_POINT('',(30.,-4.931278499541,14.)); +#1113 = LINE('',#1114,#1115); +#1114 = CARTESIAN_POINT('',(30.,-4.931278499541,33.)); +#1115 = VECTOR('',#1116,1.); +#1116 = DIRECTION('',(4.4E-16,1.41E-15,-1.)); +#1117 = ORIENTED_EDGE('',*,*,#1118,.T.); +#1118 = EDGE_CURVE('',#1111,#1119,#1121,.T.); +#1119 = VERTEX_POINT('',#1120); +#1120 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,14.)); +#1121 = LINE('',#1122,#1123); +#1122 = CARTESIAN_POINT('',(44.651437800573,-35.85690632702,14.)); +#1123 = VECTOR('',#1124,1.); +#1124 = DIRECTION('',(-0.428144965607,0.903710068786,-2.E-16)); +#1125 = ORIENTED_EDGE('',*,*,#1126,.F.); +#1126 = EDGE_CURVE('',#1085,#1119,#1127,.T.); +#1127 = LINE('',#1128,#1129); +#1128 = CARTESIAN_POINT('',(22.592751719653,10.703624140173,1.505E-14)); +#1129 = VECTOR('',#1130,1.); +#1130 = DIRECTION('',(-6.6E-16,0.,1.)); +#1131 = PLANE('',#1132); +#1132 = AXIS2_PLACEMENT_3D('',#1133,#1134,#1135); +#1133 = CARTESIAN_POINT('',(44.,-34.48187929913,2.931E-14)); +#1134 = DIRECTION('',(-0.903710068786,-0.428144965607,-6.E-16)); +#1135 = DIRECTION('',(-0.428144965607,0.903710068786,-2.9E-16)); +#1136 = ADVANCED_FACE('',(#1137),#1149,.T.); +#1137 = FACE_BOUND('',#1138,.T.); +#1138 = EDGE_LOOP('',(#1139,#1146,#1147,#1148)); +#1139 = ORIENTED_EDGE('',*,*,#1140,.F.); +#1140 = EDGE_CURVE('',#602,#1095,#1141,.T.); +#1141 = CIRCLE('',#1142,25.); +#1142 = AXIS2_PLACEMENT_3D('',#1143,#1144,#1145); +#1143 = CARTESIAN_POINT('',(-7.328E-14,0.,66.)); +#1144 = DIRECTION('',(1.11E-15,0.,-1.)); +#1145 = DIRECTION('',(0.,1.,0.)); +#1146 = ORIENTED_EDGE('',*,*,#609,.T.); +#1147 = ORIENTED_EDGE('',*,*,#367,.T.); +#1148 = ORIENTED_EDGE('',*,*,#1094,.F.); +#1149 = CYLINDRICAL_SURFACE('',#1150,25.); +#1150 = AXIS2_PLACEMENT_3D('',#1151,#1152,#1153); +#1151 = CARTESIAN_POINT('',(0.,0.,0.)); +#1152 = DIRECTION('',(6.6E-16,0.,-1.)); +#1153 = DIRECTION('',(0.,1.,0.)); +#1154 = ADVANCED_FACE('',(#1155),#1174,.F.); +#1155 = FACE_BOUND('',#1156,.F.); +#1156 = EDGE_LOOP('',(#1157,#1166,#1172,#1173)); +#1157 = ORIENTED_EDGE('',*,*,#1158,.F.); +#1158 = EDGE_CURVE('',#1159,#1159,#1161,.T.); +#1159 = VERTEX_POINT('',#1160); +#1160 = CARTESIAN_POINT('',(-1.1E-13,10.,66.)); +#1161 = CIRCLE('',#1162,10.); +#1162 = AXIS2_PLACEMENT_3D('',#1163,#1164,#1165); +#1163 = CARTESIAN_POINT('',(-7.328E-14,0.,66.)); +#1164 = DIRECTION('',(1.11E-15,0.,-1.)); +#1165 = DIRECTION('',(0.,1.,0.)); +#1166 = ORIENTED_EDGE('',*,*,#1167,.T.); +#1167 = EDGE_CURVE('',#1159,#471,#1168,.T.); +#1168 = LINE('',#1169,#1170); +#1169 = CARTESIAN_POINT('',(0.,10.,0.)); +#1170 = VECTOR('',#1171,1.); +#1171 = DIRECTION('',(-6.6E-16,0.,1.)); +#1172 = ORIENTED_EDGE('',*,*,#470,.T.); +#1173 = ORIENTED_EDGE('',*,*,#1167,.F.); +#1174 = CYLINDRICAL_SURFACE('',#1175,10.); +#1175 = AXIS2_PLACEMENT_3D('',#1176,#1177,#1178); +#1176 = CARTESIAN_POINT('',(0.,0.,0.)); +#1177 = DIRECTION('',(6.6E-16,0.,-1.)); +#1178 = DIRECTION('',(0.,1.,0.)); +#1179 = ADVANCED_FACE('',(#1180,#1198),#1201,.T.); +#1180 = FACE_BOUND('',#1181,.T.); +#1181 = EDGE_LOOP('',(#1182,#1183,#1189,#1190,#1191,#1192)); +#1182 = ORIENTED_EDGE('',*,*,#529,.F.); +#1183 = ORIENTED_EDGE('',*,*,#1184,.T.); +#1184 = EDGE_CURVE('',#530,#594,#1185,.T.); +#1185 = LINE('',#1186,#1187); +#1186 = CARTESIAN_POINT('',(-30.,-44.48187929913,66.)); +#1187 = VECTOR('',#1188,1.); +#1188 = DIRECTION('',(2.2E-16,1.,0.)); +#1189 = ORIENTED_EDGE('',*,*,#601,.T.); +#1190 = ORIENTED_EDGE('',*,*,#1140,.T.); +#1191 = ORIENTED_EDGE('',*,*,#1102,.T.); +#1192 = ORIENTED_EDGE('',*,*,#1193,.F.); +#1193 = EDGE_CURVE('',#532,#1103,#1194,.T.); +#1194 = LINE('',#1195,#1196); +#1195 = CARTESIAN_POINT('',(30.,-44.48187929913,66.)); +#1196 = VECTOR('',#1197,1.); +#1197 = DIRECTION('',(2.2E-16,1.,0.)); +#1198 = FACE_BOUND('',#1199,.T.); +#1199 = EDGE_LOOP('',(#1200)); +#1200 = ORIENTED_EDGE('',*,*,#1158,.F.); +#1201 = PLANE('',#1202); +#1202 = AXIS2_PLACEMENT_3D('',#1203,#1204,#1205); +#1203 = CARTESIAN_POINT('',(-30.,-44.48187929913,66.)); +#1204 = DIRECTION('',(4.4E-16,0.,-1.)); +#1205 = DIRECTION('',(1.,-2.2E-16,4.4E-16)); +#1206 = ADVANCED_FACE('',(#1207),#1218,.T.); +#1207 = FACE_BOUND('',#1208,.T.); +#1208 = EDGE_LOOP('',(#1209,#1210,#1216,#1217)); +#1209 = ORIENTED_EDGE('',*,*,#888,.T.); +#1210 = ORIENTED_EDGE('',*,*,#1211,.T.); +#1211 = EDGE_CURVE('',#880,#812,#1212,.T.); +#1212 = LINE('',#1213,#1214); +#1213 = CARTESIAN_POINT('',(30.,-49.1515476204,87.885482706876)); +#1214 = VECTOR('',#1215,1.); +#1215 = DIRECTION('',(-1.,0.,-6.7E-16)); +#1216 = ORIENTED_EDGE('',*,*,#811,.F.); +#1217 = ORIENTED_EDGE('',*,*,#545,.F.); +#1218 = PLANE('',#1219); +#1219 = AXIS2_PLACEMENT_3D('',#1220,#1221,#1222); +#1220 = CARTESIAN_POINT('',(30.,-44.48187929913,80.)); +#1221 = DIRECTION('',(2.8E-16,-0.860445129764,-0.50954310776)); +#1222 = DIRECTION('',(-1.,4.598339533307E-18,-5.572769301199E-16)); +#1223 = ADVANCED_FACE('',(#1224,#1257),#1268,.T.); +#1224 = FACE_BOUND('',#1225,.T.); +#1225 = EDGE_LOOP('',(#1226,#1227,#1233,#1234,#1240,#1241,#1247,#1248, + #1249,#1256)); +#1226 = ORIENTED_EDGE('',*,*,#1023,.T.); +#1227 = ORIENTED_EDGE('',*,*,#1228,.T.); +#1228 = EDGE_CURVE('',#1026,#923,#1229,.T.); +#1229 = LINE('',#1230,#1231); +#1230 = CARTESIAN_POINT('',(44.,-78.78187929913,3.251E-14)); +#1231 = VECTOR('',#1232,1.); +#1232 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1233 = ORIENTED_EDGE('',*,*,#922,.T.); +#1234 = ORIENTED_EDGE('',*,*,#1235,.T.); +#1235 = EDGE_CURVE('',#925,#716,#1236,.T.); +#1236 = LINE('',#1237,#1238); +#1237 = CARTESIAN_POINT('',(30.,-44.48187929913,6.66E-15)); +#1238 = VECTOR('',#1239,1.); +#1239 = DIRECTION('',(-1.,0.,-2.2E-16)); +#1240 = ORIENTED_EDGE('',*,*,#715,.T.); +#1241 = ORIENTED_EDGE('',*,*,#1242,.T.); +#1242 = EDGE_CURVE('',#718,#631,#1243,.T.); +#1243 = LINE('',#1244,#1245); +#1244 = CARTESIAN_POINT('',(-30.,-78.78187929913,-2.656E-14)); +#1245 = VECTOR('',#1246,1.); +#1246 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1247 = ORIENTED_EDGE('',*,*,#630,.T.); +#1248 = ORIENTED_EDGE('',*,*,#567,.F.); +#1249 = ORIENTED_EDGE('',*,*,#1250,.T.); +#1250 = EDGE_CURVE('',#568,#1085,#1251,.T.); +#1251 = CIRCLE('',#1252,25.); +#1252 = AXIS2_PLACEMENT_3D('',#1253,#1254,#1255); +#1253 = CARTESIAN_POINT('',(0.,0.,0.)); +#1254 = DIRECTION('',(2.2E-16,0.,-1.)); +#1255 = DIRECTION('',(0.,1.,0.)); +#1256 = ORIENTED_EDGE('',*,*,#1084,.F.); +#1257 = FACE_BOUND('',#1258,.T.); +#1258 = EDGE_LOOP('',(#1259)); +#1259 = ORIENTED_EDGE('',*,*,#1260,.F.); +#1260 = EDGE_CURVE('',#1261,#1261,#1263,.T.); +#1261 = VERTEX_POINT('',#1262); +#1262 = CARTESIAN_POINT('',(0.,10.,0.)); +#1263 = CIRCLE('',#1264,10.); +#1264 = AXIS2_PLACEMENT_3D('',#1265,#1266,#1267); +#1265 = CARTESIAN_POINT('',(0.,0.,0.)); +#1266 = DIRECTION('',(2.2E-16,0.,-1.)); +#1267 = DIRECTION('',(0.,1.,0.)); +#1268 = PLANE('',#1269); +#1269 = AXIS2_PLACEMENT_3D('',#1270,#1271,#1272); +#1270 = CARTESIAN_POINT('',(-4.71E-15,-43.48690893667,0.)); +#1271 = DIRECTION('',(4.4E-16,0.,-1.)); +#1272 = DIRECTION('',(-1.,0.,-4.4E-16)); +#1273 = ADVANCED_FACE('',(#1274),#1285,.T.); +#1274 = FACE_BOUND('',#1275,.T.); +#1275 = EDGE_LOOP('',(#1276,#1277,#1283,#1284)); +#1276 = ORIENTED_EDGE('',*,*,#733,.F.); +#1277 = ORIENTED_EDGE('',*,*,#1278,.T.); +#1278 = EDGE_CURVE('',#726,#586,#1279,.T.); +#1279 = LINE('',#1280,#1281); +#1280 = CARTESIAN_POINT('',(-30.,-44.48187929913,14.)); +#1281 = VECTOR('',#1282,1.); +#1282 = DIRECTION('',(2.2E-16,1.,0.)); +#1283 = ORIENTED_EDGE('',*,*,#593,.T.); +#1284 = ORIENTED_EDGE('',*,*,#1184,.F.); +#1285 = PLANE('',#1286); +#1286 = AXIS2_PLACEMENT_3D('',#1287,#1288,#1289); +#1287 = CARTESIAN_POINT('',(-30.,-44.48187929913,14.)); +#1288 = DIRECTION('',(1.,-2.2E-16,4.4E-16)); +#1289 = DIRECTION('',(-4.4E-16,-3.483422479331E-48,1.)); +#1290 = ADVANCED_FACE('',(#1291,#1315),#1326,.T.); +#1291 = FACE_BOUND('',#1292,.T.); +#1292 = EDGE_LOOP('',(#1293,#1299,#1305,#1306,#1313,#1314)); +#1293 = ORIENTED_EDGE('',*,*,#1294,.F.); +#1294 = EDGE_CURVE('',#967,#726,#1295,.T.); +#1295 = LINE('',#1296,#1297); +#1296 = CARTESIAN_POINT('',(30.,-44.48187929913,14.)); +#1297 = VECTOR('',#1298,1.); +#1298 = DIRECTION('',(-1.,0.,-2.2E-16)); +#1299 = ORIENTED_EDGE('',*,*,#1300,.T.); +#1300 = EDGE_CURVE('',#967,#1111,#1301,.T.); +#1301 = LINE('',#1302,#1303); +#1302 = CARTESIAN_POINT('',(30.,-44.48187929913,14.)); +#1303 = VECTOR('',#1304,1.); +#1304 = DIRECTION('',(2.2E-16,1.,0.)); +#1305 = ORIENTED_EDGE('',*,*,#1118,.T.); +#1306 = ORIENTED_EDGE('',*,*,#1307,.F.); +#1307 = EDGE_CURVE('',#578,#1119,#1308,.T.); +#1308 = CIRCLE('',#1309,25.); +#1309 = AXIS2_PLACEMENT_3D('',#1310,#1311,#1312); +#1310 = CARTESIAN_POINT('',(-1.554E-14,0.,14.)); +#1311 = DIRECTION('',(1.11E-15,0.,-1.)); +#1312 = DIRECTION('',(0.,1.,0.)); +#1313 = ORIENTED_EDGE('',*,*,#585,.T.); +#1314 = ORIENTED_EDGE('',*,*,#1278,.F.); +#1315 = FACE_BOUND('',#1316,.T.); +#1316 = EDGE_LOOP('',(#1317)); +#1317 = ORIENTED_EDGE('',*,*,#1318,.T.); +#1318 = EDGE_CURVE('',#1319,#1319,#1321,.T.); +#1319 = VERTEX_POINT('',#1320); +#1320 = CARTESIAN_POINT('',(-2.3E-14,10.,14.)); +#1321 = CIRCLE('',#1322,10.); +#1322 = AXIS2_PLACEMENT_3D('',#1323,#1324,#1325); +#1323 = CARTESIAN_POINT('',(-1.554E-14,0.,14.)); +#1324 = DIRECTION('',(1.11E-15,0.,-1.)); +#1325 = DIRECTION('',(0.,1.,0.)); +#1326 = PLANE('',#1327); +#1327 = AXIS2_PLACEMENT_3D('',#1328,#1329,#1330); +#1328 = CARTESIAN_POINT('',(30.,-44.48187929913,14.)); +#1329 = DIRECTION('',(-4.4E-16,0.,1.)); +#1330 = DIRECTION('',(-1.,2.2E-16,-4.4E-16)); +#1331 = ADVANCED_FACE('',(#1332),#1338,.T.); +#1332 = FACE_BOUND('',#1333,.T.); +#1333 = EDGE_LOOP('',(#1334,#1335,#1336,#1337)); +#1334 = ORIENTED_EDGE('',*,*,#1250,.F.); +#1335 = ORIENTED_EDGE('',*,*,#577,.T.); +#1336 = ORIENTED_EDGE('',*,*,#1307,.T.); +#1337 = ORIENTED_EDGE('',*,*,#1126,.F.); +#1338 = CYLINDRICAL_SURFACE('',#1339,25.); +#1339 = AXIS2_PLACEMENT_3D('',#1340,#1341,#1342); +#1340 = CARTESIAN_POINT('',(0.,0.,0.)); +#1341 = DIRECTION('',(6.6E-16,0.,-1.)); +#1342 = DIRECTION('',(0.,1.,0.)); +#1343 = ADVANCED_FACE('',(#1344),#1355,.T.); +#1344 = FACE_BOUND('',#1345,.T.); +#1345 = EDGE_LOOP('',(#1346,#1347,#1353,#1354)); +#1346 = ORIENTED_EDGE('',*,*,#759,.T.); +#1347 = ORIENTED_EDGE('',*,*,#1348,.T.); +#1348 = EDGE_CURVE('',#752,#639,#1349,.T.); +#1349 = LINE('',#1350,#1351); +#1350 = CARTESIAN_POINT('',(-30.,-94.48187929913,15.7)); +#1351 = VECTOR('',#1352,1.); +#1352 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1353 = ORIENTED_EDGE('',*,*,#638,.F.); +#1354 = ORIENTED_EDGE('',*,*,#1242,.F.); +#1355 = CYLINDRICAL_SURFACE('',#1356,15.7); +#1356 = AXIS2_PLACEMENT_3D('',#1357,#1358,#1359); +#1357 = CARTESIAN_POINT('',(-30.,-78.78187929913,15.7)); +#1358 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1359 = DIRECTION('',(5.6E-16,0.,-1.)); +#1360 = ADVANCED_FACE('',(#1361),#1372,.T.); +#1361 = FACE_BOUND('',#1362,.T.); +#1362 = EDGE_LOOP('',(#1363,#1364,#1365,#1366)); +#1363 = ORIENTED_EDGE('',*,*,#647,.F.); +#1364 = ORIENTED_EDGE('',*,*,#1348,.F.); +#1365 = ORIENTED_EDGE('',*,*,#751,.T.); +#1366 = ORIENTED_EDGE('',*,*,#1367,.F.); +#1367 = EDGE_CURVE('',#648,#743,#1368,.T.); +#1368 = LINE('',#1369,#1370); +#1369 = CARTESIAN_POINT('',(-44.,-94.48187929913,15.755518480805)); +#1370 = VECTOR('',#1371,1.); +#1371 = DIRECTION('',(1.,0.,1.89E-15)); +#1372 = PLANE('',#1373); +#1373 = AXIS2_PLACEMENT_3D('',#1374,#1375,#1376); +#1374 = CARTESIAN_POINT('',(-30.,-94.48187929913,-1.998E-14)); +#1375 = DIRECTION('',(0.,-1.,0.)); +#1376 = DIRECTION('',(-1.,-0.,-6.6E-16)); +#1377 = ADVANCED_FACE('',(#1378),#1384,.T.); +#1378 = FACE_BOUND('',#1379,.F.); +#1379 = EDGE_LOOP('',(#1380,#1381,#1382,#1383)); +#1380 = ORIENTED_EDGE('',*,*,#655,.T.); +#1381 = ORIENTED_EDGE('',*,*,#692,.T.); +#1382 = ORIENTED_EDGE('',*,*,#742,.F.); +#1383 = ORIENTED_EDGE('',*,*,#1367,.F.); +#1384 = CYLINDRICAL_SURFACE('',#1385,15.7); +#1385 = AXIS2_PLACEMENT_3D('',#1386,#1387,#1388); +#1386 = CARTESIAN_POINT('',(-44.,-78.78187929913,15.755518480805)); +#1387 = DIRECTION('',(1.,0.,1.89E-15)); +#1388 = DIRECTION('',(0.,-1.,0.)); +#1389 = ADVANCED_FACE('',(#1390),#1401,.F.); +#1390 = FACE_BOUND('',#1391,.F.); +#1391 = EDGE_LOOP('',(#1392,#1393,#1399,#1400)); +#1392 = ORIENTED_EDGE('',*,*,#674,.F.); +#1393 = ORIENTED_EDGE('',*,*,#1394,.T.); +#1394 = EDGE_CURVE('',#675,#769,#1395,.T.); +#1395 = LINE('',#1396,#1397); +#1396 = CARTESIAN_POINT('',(-44.,-73.,15.)); +#1397 = VECTOR('',#1398,1.); +#1398 = DIRECTION('',(1.,0.,7.7E-16)); +#1399 = ORIENTED_EDGE('',*,*,#768,.T.); +#1400 = ORIENTED_EDGE('',*,*,#1394,.F.); +#1401 = CYLINDRICAL_SURFACE('',#1402,7.); +#1402 = AXIS2_PLACEMENT_3D('',#1403,#1404,#1405); +#1403 = CARTESIAN_POINT('',(-44.,-80.,15.)); +#1404 = DIRECTION('',(-1.,0.,-7.7E-16)); +#1405 = DIRECTION('',(0.,1.,0.)); +#1406 = ADVANCED_FACE('',(#1407),#1413,.T.); +#1407 = FACE_BOUND('',#1408,.T.); +#1408 = EDGE_LOOP('',(#1409,#1410,#1411,#1412)); +#1409 = ORIENTED_EDGE('',*,*,#1235,.F.); +#1410 = ORIENTED_EDGE('',*,*,#974,.T.); +#1411 = ORIENTED_EDGE('',*,*,#1294,.T.); +#1412 = ORIENTED_EDGE('',*,*,#725,.F.); +#1413 = PLANE('',#1414); +#1414 = AXIS2_PLACEMENT_3D('',#1415,#1416,#1417); +#1415 = CARTESIAN_POINT('',(30.,-44.48187929913,1.998E-14)); +#1416 = DIRECTION('',(0.,-1.,0.)); +#1417 = DIRECTION('',(-1.,-0.,-6.6E-16)); +#1418 = ADVANCED_FACE('',(#1419),#1435,.F.); +#1419 = FACE_BOUND('',#1420,.F.); +#1420 = EDGE_LOOP('',(#1421,#1422,#1428,#1429)); +#1421 = ORIENTED_EDGE('',*,*,#785,.T.); +#1422 = ORIENTED_EDGE('',*,*,#1423,.T.); +#1423 = EDGE_CURVE('',#788,#870,#1424,.T.); +#1424 = LINE('',#1425,#1426); +#1425 = CARTESIAN_POINT('',(15.,-38.77075908679,106.)); +#1426 = VECTOR('',#1427,1.); +#1427 = DIRECTION('',(1.,0.,6.7E-16)); +#1428 = ORIENTED_EDGE('',*,*,#869,.T.); +#1429 = ORIENTED_EDGE('',*,*,#1430,.T.); +#1430 = EDGE_CURVE('',#872,#786,#1431,.T.); +#1431 = LINE('',#1432,#1433); +#1432 = CARTESIAN_POINT('',(30.,-38.82620606324,106.)); +#1433 = VECTOR('',#1434,1.); +#1434 = DIRECTION('',(-1.,0.,-6.7E-16)); +#1435 = PLANE('',#1436); +#1436 = AXIS2_PLACEMENT_3D('',#1437,#1438,#1439); +#1437 = CARTESIAN_POINT('',(22.5,-44.8787016455,106.)); +#1438 = DIRECTION('',(2.2E-16,-0.,-1.)); +#1439 = DIRECTION('',(-1.,0.,-2.2E-16)); +#1440 = ADVANCED_FACE('',(#1441),#1447,.T.); +#1441 = FACE_BOUND('',#1442,.T.); +#1442 = EDGE_LOOP('',(#1443,#1444,#1445,#1446)); +#1443 = ORIENTED_EDGE('',*,*,#795,.T.); +#1444 = ORIENTED_EDGE('',*,*,#1423,.T.); +#1445 = ORIENTED_EDGE('',*,*,#896,.F.); +#1446 = ORIENTED_EDGE('',*,*,#846,.F.); +#1447 = CYLINDRICAL_SURFACE('',#1448,15.6); +#1448 = AXIS2_PLACEMENT_3D('',#1449,#1450,#1451); +#1449 = CARTESIAN_POINT('',(15.,-38.77075908679,90.4)); +#1450 = DIRECTION('',(1.,0.,6.7E-16)); +#1451 = DIRECTION('',(-3.413938821994E-16,0.860445129764,0.50954310776) + ); +#1452 = ADVANCED_FACE('',(#1453),#1459,.T.); +#1453 = FACE_BOUND('',#1454,.T.); +#1454 = EDGE_LOOP('',(#1455,#1456,#1457,#1458)); +#1455 = ORIENTED_EDGE('',*,*,#879,.T.); +#1456 = ORIENTED_EDGE('',*,*,#1430,.T.); +#1457 = ORIENTED_EDGE('',*,*,#819,.F.); +#1458 = ORIENTED_EDGE('',*,*,#1211,.F.); +#1459 = CYLINDRICAL_SURFACE('',#1460,12.); +#1460 = AXIS2_PLACEMENT_3D('',#1461,#1462,#1463); +#1461 = CARTESIAN_POINT('',(30.,-38.82620606324,94.)); +#1462 = DIRECTION('',(-1.,0.,-6.7E-16)); +#1463 = DIRECTION('',(3.413938821994E-16,-0.860445129764,-0.50954310776) + ); +#1464 = ADVANCED_FACE('',(#1465),#1476,.F.); +#1465 = FACE_BOUND('',#1466,.F.); +#1466 = EDGE_LOOP('',(#1467,#1468,#1474,#1475)); +#1467 = ORIENTED_EDGE('',*,*,#905,.F.); +#1468 = ORIENTED_EDGE('',*,*,#1469,.T.); +#1469 = EDGE_CURVE('',#906,#829,#1470,.T.); +#1470 = LINE('',#1471,#1472); +#1471 = CARTESIAN_POINT('',(30.,-45.04444206723,94.459258343214)); +#1472 = VECTOR('',#1473,1.); +#1473 = DIRECTION('',(-1.,0.,-1.19E-15)); +#1474 = ORIENTED_EDGE('',*,*,#828,.T.); +#1475 = ORIENTED_EDGE('',*,*,#1469,.F.); +#1476 = CYLINDRICAL_SURFACE('',#1477,7.); +#1477 = AXIS2_PLACEMENT_3D('',#1478,#1479,#1480); +#1478 = CARTESIAN_POINT('',(30.,-38.04444206723,94.459258343214)); +#1479 = DIRECTION('',(1.,0.,1.19E-15)); +#1480 = DIRECTION('',(0.,-1.,0.)); +#1481 = ADVANCED_FACE('',(#1482),#1493,.T.); +#1482 = FACE_BOUND('',#1483,.T.); +#1483 = EDGE_LOOP('',(#1484,#1485,#1491,#1492)); +#1484 = ORIENTED_EDGE('',*,*,#1058,.T.); +#1485 = ORIENTED_EDGE('',*,*,#1486,.T.); +#1486 = EDGE_CURVE('',#1051,#933,#1487,.T.); +#1487 = LINE('',#1488,#1489); +#1488 = CARTESIAN_POINT('',(44.,-94.48187929913,15.7)); +#1489 = VECTOR('',#1490,1.); +#1490 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1491 = ORIENTED_EDGE('',*,*,#932,.F.); +#1492 = ORIENTED_EDGE('',*,*,#1228,.F.); +#1493 = CYLINDRICAL_SURFACE('',#1494,15.7); +#1494 = AXIS2_PLACEMENT_3D('',#1495,#1496,#1497); +#1495 = CARTESIAN_POINT('',(44.,-78.78187929913,15.7)); +#1496 = DIRECTION('',(-1.,0.,-5.6E-16)); +#1497 = DIRECTION('',(5.6E-16,0.,-1.)); +#1498 = ADVANCED_FACE('',(#1499),#1510,.T.); +#1499 = FACE_BOUND('',#1500,.T.); +#1500 = EDGE_LOOP('',(#1501,#1502,#1503,#1504)); +#1501 = ORIENTED_EDGE('',*,*,#941,.F.); +#1502 = ORIENTED_EDGE('',*,*,#1486,.F.); +#1503 = ORIENTED_EDGE('',*,*,#1050,.T.); +#1504 = ORIENTED_EDGE('',*,*,#1505,.F.); +#1505 = EDGE_CURVE('',#942,#1042,#1506,.T.); +#1506 = LINE('',#1507,#1508); +#1507 = CARTESIAN_POINT('',(30.,-94.48187929913,15.755518480805)); +#1508 = VECTOR('',#1509,1.); +#1509 = DIRECTION('',(1.,0.,1.89E-15)); +#1510 = PLANE('',#1511); +#1511 = AXIS2_PLACEMENT_3D('',#1512,#1513,#1514); +#1512 = CARTESIAN_POINT('',(44.,-94.48187929913,2.931E-14)); +#1513 = DIRECTION('',(0.,-1.,0.)); +#1514 = DIRECTION('',(-1.,-0.,-6.6E-16)); +#1515 = ADVANCED_FACE('',(#1516),#1522,.T.); +#1516 = FACE_BOUND('',#1517,.T.); +#1517 = EDGE_LOOP('',(#1518,#1519,#1520,#1521)); +#1518 = ORIENTED_EDGE('',*,*,#966,.F.); +#1519 = ORIENTED_EDGE('',*,*,#1193,.T.); +#1520 = ORIENTED_EDGE('',*,*,#1110,.T.); +#1521 = ORIENTED_EDGE('',*,*,#1300,.F.); +#1522 = PLANE('',#1523); +#1523 = AXIS2_PLACEMENT_3D('',#1524,#1525,#1526); +#1524 = CARTESIAN_POINT('',(30.,-44.48187929913,66.)); +#1525 = DIRECTION('',(-1.,2.2E-16,-4.4E-16)); +#1526 = DIRECTION('',(4.4E-16,3.483422479331E-48,-1.)); +#1527 = ADVANCED_FACE('',(#1528),#1534,.T.); +#1528 = FACE_BOUND('',#1529,.F.); +#1529 = EDGE_LOOP('',(#1530,#1531,#1532,#1533)); +#1530 = ORIENTED_EDGE('',*,*,#949,.T.); +#1531 = ORIENTED_EDGE('',*,*,#1000,.T.); +#1532 = ORIENTED_EDGE('',*,*,#1041,.F.); +#1533 = ORIENTED_EDGE('',*,*,#1505,.F.); +#1534 = CYLINDRICAL_SURFACE('',#1535,15.7); +#1535 = AXIS2_PLACEMENT_3D('',#1536,#1537,#1538); +#1536 = CARTESIAN_POINT('',(30.,-78.78187929913,15.755518480805)); +#1537 = DIRECTION('',(1.,0.,1.89E-15)); +#1538 = DIRECTION('',(0.,-1.,0.)); +#1539 = ADVANCED_FACE('',(#1540),#1551,.F.); +#1540 = FACE_BOUND('',#1541,.F.); +#1541 = EDGE_LOOP('',(#1542,#1543,#1549,#1550)); +#1542 = ORIENTED_EDGE('',*,*,#982,.F.); +#1543 = ORIENTED_EDGE('',*,*,#1544,.T.); +#1544 = EDGE_CURVE('',#983,#1068,#1545,.T.); +#1545 = LINE('',#1546,#1547); +#1546 = CARTESIAN_POINT('',(-44.,-73.,15.)); +#1547 = VECTOR('',#1548,1.); +#1548 = DIRECTION('',(1.,0.,7.7E-16)); +#1549 = ORIENTED_EDGE('',*,*,#1067,.T.); +#1550 = ORIENTED_EDGE('',*,*,#1544,.F.); +#1551 = CYLINDRICAL_SURFACE('',#1552,7.); +#1552 = AXIS2_PLACEMENT_3D('',#1553,#1554,#1555); +#1553 = CARTESIAN_POINT('',(-44.,-80.,15.)); +#1554 = DIRECTION('',(-1.,0.,-7.7E-16)); +#1555 = DIRECTION('',(0.,1.,0.)); +#1556 = ADVANCED_FACE('',(#1557),#1568,.F.); +#1557 = FACE_BOUND('',#1558,.F.); +#1558 = EDGE_LOOP('',(#1559,#1560,#1566,#1567)); +#1559 = ORIENTED_EDGE('',*,*,#1260,.F.); +#1560 = ORIENTED_EDGE('',*,*,#1561,.T.); +#1561 = EDGE_CURVE('',#1261,#1319,#1562,.T.); +#1562 = LINE('',#1563,#1564); +#1563 = CARTESIAN_POINT('',(0.,10.,0.)); +#1564 = VECTOR('',#1565,1.); +#1565 = DIRECTION('',(-6.6E-16,0.,1.)); +#1566 = ORIENTED_EDGE('',*,*,#1318,.T.); +#1567 = ORIENTED_EDGE('',*,*,#1561,.F.); +#1568 = CYLINDRICAL_SURFACE('',#1569,10.); +#1569 = AXIS2_PLACEMENT_3D('',#1570,#1571,#1572); +#1570 = CARTESIAN_POINT('',(0.,0.,0.)); +#1571 = DIRECTION('',(6.6E-16,0.,-1.)); +#1572 = DIRECTION('',(0.,1.,0.)); +#1573 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1577)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1574,#1575,#1576)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1574 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1575 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1576 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1577 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#1574, + 'distance_accuracy_value','confusion accuracy'); +#1578 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1579,#1581); +#1579 = ( REPRESENTATION_RELATIONSHIP('','',#152,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1580) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1580 = ITEM_DEFINED_TRANSFORMATION('','',#11,#19); +#1581 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1582); +#1582 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('2','Base001','',#5,#147,$); +#1583 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#149)); +#1584 = SHAPE_DEFINITION_REPRESENTATION(#1585,#1591); +#1585 = PRODUCT_DEFINITION_SHAPE('','',#1586); +#1586 = PRODUCT_DEFINITION('design','',#1587,#1590); +#1587 = PRODUCT_DEFINITION_FORMATION('','',#1588); +#1588 = PRODUCT('Boom','Boom','',(#1589)); +#1589 = PRODUCT_CONTEXT('',#2,'mechanical'); +#1590 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#1591 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1592),#2623); +#1592 = MANIFOLD_SOLID_BREP('',#1593); +#1593 = CLOSED_SHELL('',(#1594,#1738,#1769,#1794,#1850,#1874,#1899,#1924 + ,#1980,#2004,#2028,#2052,#2070,#2095,#2120,#2145,#2170,#2203,#2220, + #2329,#2353,#2458,#2475,#2492,#2509,#2526,#2543,#2560,#2572,#2589, + #2606)); +#1594 = ADVANCED_FACE('',(#1595,#1689,#1700,#1711,#1722),#1733,.F.); +#1595 = FACE_BOUND('',#1596,.F.); +#1596 = EDGE_LOOP('',(#1597,#1607,#1616,#1624,#1633,#1641,#1649,#1658, + #1666,#1674,#1683)); +#1597 = ORIENTED_EDGE('',*,*,#1598,.T.); +#1598 = EDGE_CURVE('',#1599,#1601,#1603,.T.); +#1599 = VERTEX_POINT('',#1600); +#1600 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#1601 = VERTEX_POINT('',#1602); +#1602 = CARTESIAN_POINT('',(-32.9758203125,-386.5793121341, + 259.26646994902)); +#1603 = LINE('',#1604,#1605); +#1604 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#1605 = VECTOR('',#1606,1.); +#1606 = DIRECTION('',(0.,5.353436527229E-02,0.9985660077)); +#1607 = ORIENTED_EDGE('',*,*,#1608,.T.); +#1608 = EDGE_CURVE('',#1601,#1609,#1611,.T.); +#1609 = VERTEX_POINT('',#1610); +#1610 = CARTESIAN_POINT('',(-32.9758203125,-393.8790359656, + 282.75753122336)); +#1611 = CIRCLE('',#1612,35.4); +#1612 = AXIS2_PLACEMENT_3D('',#1613,#1614,#1615); +#1613 = CARTESIAN_POINT('',(-32.9758203125,-421.9285488067, + 261.16158647966)); +#1614 = DIRECTION('',(1.,-0.,0.)); +#1615 = DIRECTION('',(0.,0.,-1.)); +#1616 = ORIENTED_EDGE('',*,*,#1617,.T.); +#1617 = EDGE_CURVE('',#1609,#1618,#1620,.T.); +#1618 = VERTEX_POINT('',#1619); +#1619 = CARTESIAN_POINT('',(-32.9758203125,-393.9252624137, + 282.81757163213)); +#1620 = LINE('',#1621,#1622); +#1621 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 272.36437946689)); +#1622 = VECTOR('',#1623,1.); +#1623 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#1624 = ORIENTED_EDGE('',*,*,#1625,.T.); +#1625 = EDGE_CURVE('',#1618,#1626,#1628,.T.); +#1626 = VERTEX_POINT('',#1627); +#1627 = CARTESIAN_POINT('',(-32.9758203125,-420.9796295631, + 296.60763662465)); +#1628 = CIRCLE('',#1629,35.4); +#1629 = AXIS2_PLACEMENT_3D('',#1630,#1631,#1632); +#1630 = CARTESIAN_POINT('',(-32.9758203125,-421.9747752548, + 261.22162688843)); +#1631 = DIRECTION('',(1.,-0.,0.)); +#1632 = DIRECTION('',(0.,0.,-1.)); +#1633 = ORIENTED_EDGE('',*,*,#1634,.T.); +#1634 = EDGE_CURVE('',#1626,#1635,#1637,.T.); +#1635 = VERTEX_POINT('',#1636); +#1636 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#1637 = LINE('',#1638,#1639); +#1638 = CARTESIAN_POINT('',(-32.9758203125,-404.178765007,296.1351530611 + )); +#1639 = VECTOR('',#1640,1.); +#1640 = DIRECTION('',(0.,-0.999604794809,2.811146021781E-02)); +#1641 = ORIENTED_EDGE('',*,*,#1642,.T.); +#1642 = EDGE_CURVE('',#1635,#1643,#1645,.T.); +#1643 = VERTEX_POINT('',#1644); +#1644 = CARTESIAN_POINT('',(-32.9758203125,-586.4198346814, + 346.16230087024)); +#1645 = LINE('',#1646,#1647); +#1646 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#1647 = VECTOR('',#1648,1.); +#1648 = DIRECTION('',(0.,-0.951580786438,0.307398775016)); +#1649 = ORIENTED_EDGE('',*,*,#1650,.T.); +#1650 = EDGE_CURVE('',#1643,#1651,#1653,.T.); +#1651 = VERTEX_POINT('',#1652); +#1652 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#1653 = CIRCLE('',#1654,14.5); +#1654 = AXIS2_PLACEMENT_3D('',#1655,#1656,#1657); +#1655 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#1656 = DIRECTION('',(1.,0.,0.)); +#1657 = DIRECTION('',(0.,1.,0.)); +#1658 = ORIENTED_EDGE('',*,*,#1659,.T.); +#1659 = EDGE_CURVE('',#1651,#1660,#1662,.T.); +#1660 = VERTEX_POINT('',#1661); +#1661 = CARTESIAN_POINT('',(-32.9758203125,-492.2665759806, + 193.13000434422)); +#1662 = LINE('',#1663,#1664); +#1663 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#1664 = VECTOR('',#1665,1.); +#1665 = DIRECTION('',(0.,0.645226007981,-0.763991752982)); +#1666 = ORIENTED_EDGE('',*,*,#1667,.T.); +#1667 = EDGE_CURVE('',#1660,#1668,#1670,.T.); +#1668 = VERTEX_POINT('',#1669); +#1669 = CARTESIAN_POINT('',(-32.9758203125,-264.2959849519, + 30.558759574336)); +#1670 = LINE('',#1671,#1672); +#1671 = CARTESIAN_POINT('',(-32.9758203125,-492.2665759806, + 193.13000434422)); +#1672 = VECTOR('',#1673,1.); +#1673 = DIRECTION('',(0.,0.814180682245,-0.580611588464)); +#1674 = ORIENTED_EDGE('',*,*,#1675,.T.); +#1675 = EDGE_CURVE('',#1668,#1676,#1678,.T.); +#1676 = VERTEX_POINT('',#1677); +#1677 = CARTESIAN_POINT('',(-32.9758203125,-244.387909682, + 51.210176042702)); +#1678 = CIRCLE('',#1679,14.5); +#1679 = AXIS2_PLACEMENT_3D('',#1680,#1681,#1682); +#1680 = CARTESIAN_POINT('',(-32.9758203125,-255.8771169192, + 42.364379466893)); +#1681 = DIRECTION('',(1.,0.,0.)); +#1682 = DIRECTION('',(0.,1.,0.)); +#1683 = ORIENTED_EDGE('',*,*,#1684,.T.); +#1684 = EDGE_CURVE('',#1676,#1599,#1685,.T.); +#1685 = LINE('',#1686,#1687); +#1686 = CARTESIAN_POINT('',(-32.9758203125,-244.387909682, + 51.210176042702)); +#1687 = VECTOR('',#1688,1.); +#1688 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#1689 = FACE_BOUND('',#1690,.F.); +#1690 = EDGE_LOOP('',(#1691)); +#1691 = ORIENTED_EDGE('',*,*,#1692,.F.); +#1692 = EDGE_CURVE('',#1693,#1693,#1695,.T.); +#1693 = VERTEX_POINT('',#1694); +#1694 = CARTESIAN_POINT('',(-32.9758203125,-378.8771169192, + 162.36437946689)); +#1695 = CIRCLE('',#1696,7.); +#1696 = AXIS2_PLACEMENT_3D('',#1697,#1698,#1699); +#1697 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 162.36437946689)); +#1698 = DIRECTION('',(1.,0.,0.)); +#1699 = DIRECTION('',(0.,1.,0.)); +#1700 = FACE_BOUND('',#1701,.F.); +#1701 = EDGE_LOOP('',(#1702)); +#1702 = ORIENTED_EDGE('',*,*,#1703,.F.); +#1703 = EDGE_CURVE('',#1704,#1704,#1706,.T.); +#1704 = VERTEX_POINT('',#1705); +#1705 = CARTESIAN_POINT('',(-32.9758203125,-248.8771169192, + 42.364379466893)); +#1706 = CIRCLE('',#1707,7.); +#1707 = AXIS2_PLACEMENT_3D('',#1708,#1709,#1710); +#1708 = CARTESIAN_POINT('',(-32.9758203125,-255.8771169192, + 42.364379466893)); +#1709 = DIRECTION('',(1.,0.,0.)); +#1710 = DIRECTION('',(0.,1.,0.)); +#1711 = FACE_BOUND('',#1712,.F.); +#1712 = EDGE_LOOP('',(#1713)); +#1713 = ORIENTED_EDGE('',*,*,#1714,.F.); +#1714 = EDGE_CURVE('',#1715,#1715,#1717,.T.); +#1715 = VERTEX_POINT('',#1716); +#1716 = CARTESIAN_POINT('',(-32.9758203125,-583.8771169192, + 332.36437946689)); +#1717 = CIRCLE('',#1718,7.); +#1718 = AXIS2_PLACEMENT_3D('',#1719,#1720,#1721); +#1719 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#1720 = DIRECTION('',(1.,0.,0.)); +#1721 = DIRECTION('',(0.,1.,0.)); +#1722 = FACE_BOUND('',#1723,.F.); +#1723 = EDGE_LOOP('',(#1724)); +#1724 = ORIENTED_EDGE('',*,*,#1725,.F.); +#1725 = EDGE_CURVE('',#1726,#1726,#1728,.T.); +#1726 = VERTEX_POINT('',#1727); +#1727 = CARTESIAN_POINT('',(-32.9758203125,-400.3771169192, + 273.86437946689)); +#1728 = CIRCLE('',#1729,7.); +#1729 = AXIS2_PLACEMENT_3D('',#1730,#1731,#1732); +#1730 = CARTESIAN_POINT('',(-32.9758203125,-407.3771169192, + 273.86437946689)); +#1731 = DIRECTION('',(1.,0.,0.)); +#1732 = DIRECTION('',(0.,1.,0.)); +#1733 = PLANE('',#1734); +#1734 = AXIS2_PLACEMENT_3D('',#1735,#1736,#1737); +#1735 = CARTESIAN_POINT('',(-32.9758203125,-417.774827926, + 197.43115187421)); +#1736 = DIRECTION('',(1.,0.,0.)); +#1737 = DIRECTION('',(0.,1.,0.)); +#1738 = ADVANCED_FACE('',(#1739),#1764,.F.); +#1739 = FACE_BOUND('',#1740,.F.); +#1740 = EDGE_LOOP('',(#1741,#1742,#1750,#1758)); +#1741 = ORIENTED_EDGE('',*,*,#1598,.F.); +#1742 = ORIENTED_EDGE('',*,*,#1743,.T.); +#1743 = EDGE_CURVE('',#1599,#1744,#1746,.T.); +#1744 = VERTEX_POINT('',#1745); +#1745 = CARTESIAN_POINT('',(-17.9758203125,-387.7508197037, + 237.41456919737)); +#1746 = LINE('',#1747,#1748); +#1747 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#1748 = VECTOR('',#1749,1.); +#1749 = DIRECTION('',(1.,0.,0.)); +#1750 = ORIENTED_EDGE('',*,*,#1751,.T.); +#1751 = EDGE_CURVE('',#1744,#1752,#1754,.T.); +#1752 = VERTEX_POINT('',#1753); +#1753 = CARTESIAN_POINT('',(-17.9758203125,-386.5793121341, + 259.26646994902)); +#1754 = LINE('',#1755,#1756); +#1755 = CARTESIAN_POINT('',(-17.9758203125,-391.0968069716, + 175.00252544001)); +#1756 = VECTOR('',#1757,1.); +#1757 = DIRECTION('',(-6.5E-16,5.353436527229E-02,0.9985660077)); +#1758 = ORIENTED_EDGE('',*,*,#1759,.F.); +#1759 = EDGE_CURVE('',#1601,#1752,#1760,.T.); +#1760 = LINE('',#1761,#1762); +#1761 = CARTESIAN_POINT('',(-32.9758203125,-386.5793121341, + 259.26646994902)); +#1762 = VECTOR('',#1763,1.); +#1763 = DIRECTION('',(1.,0.,0.)); +#1764 = PLANE('',#1765); +#1765 = AXIS2_PLACEMENT_3D('',#1766,#1767,#1768); +#1766 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#1767 = DIRECTION('',(0.,-0.9985660077,5.353436527229E-02)); +#1768 = DIRECTION('',(0.,5.353436527229E-02,0.9985660077)); +#1769 = ADVANCED_FACE('',(#1770),#1789,.T.); +#1770 = FACE_BOUND('',#1771,.T.); +#1771 = EDGE_LOOP('',(#1772,#1773,#1781,#1788)); +#1772 = ORIENTED_EDGE('',*,*,#1608,.T.); +#1773 = ORIENTED_EDGE('',*,*,#1774,.T.); +#1774 = EDGE_CURVE('',#1609,#1775,#1777,.T.); +#1775 = VERTEX_POINT('',#1776); +#1776 = CARTESIAN_POINT('',(-17.9758203125,-393.8790359656, + 282.75753122336)); +#1777 = LINE('',#1778,#1779); +#1778 = CARTESIAN_POINT('',(-32.9758203125,-393.8790359656, + 282.75753122336)); +#1779 = VECTOR('',#1780,1.); +#1780 = DIRECTION('',(1.,0.,0.)); +#1781 = ORIENTED_EDGE('',*,*,#1782,.F.); +#1782 = EDGE_CURVE('',#1752,#1775,#1783,.T.); +#1783 = CIRCLE('',#1784,35.4); +#1784 = AXIS2_PLACEMENT_3D('',#1785,#1786,#1787); +#1785 = CARTESIAN_POINT('',(-17.9758203125,-421.9285488067, + 261.16158647966)); +#1786 = DIRECTION('',(1.,-0.,0.)); +#1787 = DIRECTION('',(0.,0.,-1.)); +#1788 = ORIENTED_EDGE('',*,*,#1759,.F.); +#1789 = CYLINDRICAL_SURFACE('',#1790,35.4); +#1790 = AXIS2_PLACEMENT_3D('',#1791,#1792,#1793); +#1791 = CARTESIAN_POINT('',(-32.9758203125,-421.9285488067, + 261.16158647966)); +#1792 = DIRECTION('',(1.,0.,0.)); +#1793 = DIRECTION('',(0.,0.9985660077,-5.353436527229E-02)); +#1794 = ADVANCED_FACE('',(#1795),#1845,.F.); +#1795 = FACE_BOUND('',#1796,.F.); +#1796 = EDGE_LOOP('',(#1797,#1798,#1806,#1814,#1822,#1830,#1838,#1844)); +#1797 = ORIENTED_EDGE('',*,*,#1684,.F.); +#1798 = ORIENTED_EDGE('',*,*,#1799,.T.); +#1799 = EDGE_CURVE('',#1676,#1800,#1802,.T.); +#1800 = VERTEX_POINT('',#1801); +#1801 = CARTESIAN_POINT('',(27.0241796875,-244.387909682,51.210176042702 + )); +#1802 = LINE('',#1803,#1804); +#1803 = CARTESIAN_POINT('',(-32.9758203125,-244.387909682, + 51.210176042702)); +#1804 = VECTOR('',#1805,1.); +#1805 = DIRECTION('',(1.,0.,0.)); +#1806 = ORIENTED_EDGE('',*,*,#1807,.T.); +#1807 = EDGE_CURVE('',#1800,#1808,#1810,.T.); +#1808 = VERTEX_POINT('',#1809); +#1809 = CARTESIAN_POINT('',(27.0241796875,-387.7508197037, + 237.41456919737)); +#1810 = LINE('',#1811,#1812); +#1811 = CARTESIAN_POINT('',(27.0241796875,-244.387909682,51.210176042702 + )); +#1812 = VECTOR('',#1813,1.); +#1813 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#1814 = ORIENTED_EDGE('',*,*,#1815,.F.); +#1815 = EDGE_CURVE('',#1816,#1808,#1818,.T.); +#1816 = VERTEX_POINT('',#1817); +#1817 = CARTESIAN_POINT('',(12.0241796875,-387.7508197037, + 237.41456919737)); +#1818 = LINE('',#1819,#1820); +#1819 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#1820 = VECTOR('',#1821,1.); +#1821 = DIRECTION('',(1.,0.,0.)); +#1822 = ORIENTED_EDGE('',*,*,#1823,.T.); +#1823 = EDGE_CURVE('',#1816,#1824,#1826,.T.); +#1824 = VERTEX_POINT('',#1825); +#1825 = CARTESIAN_POINT('',(12.0241796875,-291.3794363258, + 112.24429358958)); +#1826 = LINE('',#1827,#1828); +#1827 = CARTESIAN_POINT('',(12.0241796875,-366.0274798358, + 209.19959144066)); +#1828 = VECTOR('',#1829,1.); +#1829 = DIRECTION('',(4.E-16,0.610054936263,-0.792359119807)); +#1830 = ORIENTED_EDGE('',*,*,#1831,.T.); +#1831 = EDGE_CURVE('',#1824,#1832,#1834,.T.); +#1832 = VERTEX_POINT('',#1833); +#1833 = CARTESIAN_POINT('',(-17.9758203125,-291.3794363258, + 112.24429358958)); +#1834 = LINE('',#1835,#1836); +#1835 = CARTESIAN_POINT('',(-17.9758203125,-291.3794363258, + 112.24429358958)); +#1836 = VECTOR('',#1837,1.); +#1837 = DIRECTION('',(-1.,6.8E-16,-8.8E-16)); +#1838 = ORIENTED_EDGE('',*,*,#1839,.T.); +#1839 = EDGE_CURVE('',#1832,#1744,#1840,.T.); +#1840 = LINE('',#1841,#1842); +#1841 = CARTESIAN_POINT('',(-17.9758203125,-261.9849121297, + 74.065733045679)); +#1842 = VECTOR('',#1843,1.); +#1843 = DIRECTION('',(-4.E-16,-0.610054936263,0.792359119807)); +#1844 = ORIENTED_EDGE('',*,*,#1743,.F.); +#1845 = PLANE('',#1846); +#1846 = AXIS2_PLACEMENT_3D('',#1847,#1848,#1849); +#1847 = CARTESIAN_POINT('',(-32.9758203125,-244.387909682, + 51.210176042702)); +#1848 = DIRECTION('',(0.,-0.792359119807,-0.610054936263)); +#1849 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#1850 = ADVANCED_FACE('',(#1851),#1869,.F.); +#1851 = FACE_BOUND('',#1852,.F.); +#1852 = EDGE_LOOP('',(#1853,#1854,#1855,#1863)); +#1853 = ORIENTED_EDGE('',*,*,#1617,.F.); +#1854 = ORIENTED_EDGE('',*,*,#1774,.T.); +#1855 = ORIENTED_EDGE('',*,*,#1856,.T.); +#1856 = EDGE_CURVE('',#1775,#1857,#1859,.T.); +#1857 = VERTEX_POINT('',#1858); +#1858 = CARTESIAN_POINT('',(-17.9758203125,-393.9252624137, + 282.81757163213)); +#1859 = LINE('',#1860,#1861); +#1860 = CARTESIAN_POINT('',(-17.9758203125,-323.694264607, + 191.59927587307)); +#1861 = VECTOR('',#1862,1.); +#1862 = DIRECTION('',(-4.E-16,-0.610054936263,0.792359119807)); +#1863 = ORIENTED_EDGE('',*,*,#1864,.F.); +#1864 = EDGE_CURVE('',#1618,#1857,#1865,.T.); +#1865 = LINE('',#1866,#1867); +#1866 = CARTESIAN_POINT('',(-32.9758203125,-393.9252624137, + 282.81757163213)); +#1867 = VECTOR('',#1868,1.); +#1868 = DIRECTION('',(1.,0.,0.)); +#1869 = PLANE('',#1870); +#1870 = AXIS2_PLACEMENT_3D('',#1871,#1872,#1873); +#1871 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 272.36437946689)); +#1872 = DIRECTION('',(0.,-0.792359119807,-0.610054936263)); +#1873 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#1874 = ADVANCED_FACE('',(#1875),#1894,.T.); +#1875 = FACE_BOUND('',#1876,.F.); +#1876 = EDGE_LOOP('',(#1877,#1885,#1892,#1893)); +#1877 = ORIENTED_EDGE('',*,*,#1878,.T.); +#1878 = EDGE_CURVE('',#1668,#1879,#1881,.T.); +#1879 = VERTEX_POINT('',#1880); +#1880 = CARTESIAN_POINT('',(27.0241796875,-264.2959849519, + 30.558759574336)); +#1881 = LINE('',#1882,#1883); +#1882 = CARTESIAN_POINT('',(-32.9758203125,-264.2959849519, + 30.558759574336)); +#1883 = VECTOR('',#1884,1.); +#1884 = DIRECTION('',(1.,0.,0.)); +#1885 = ORIENTED_EDGE('',*,*,#1886,.T.); +#1886 = EDGE_CURVE('',#1879,#1800,#1887,.T.); +#1887 = CIRCLE('',#1888,14.5); +#1888 = AXIS2_PLACEMENT_3D('',#1889,#1890,#1891); +#1889 = CARTESIAN_POINT('',(27.0241796875,-255.8771169192, + 42.364379466893)); +#1890 = DIRECTION('',(1.,0.,0.)); +#1891 = DIRECTION('',(0.,1.,0.)); +#1892 = ORIENTED_EDGE('',*,*,#1799,.F.); +#1893 = ORIENTED_EDGE('',*,*,#1675,.F.); +#1894 = CYLINDRICAL_SURFACE('',#1895,14.5); +#1895 = AXIS2_PLACEMENT_3D('',#1896,#1897,#1898); +#1896 = CARTESIAN_POINT('',(-32.9758203125,-255.8771169192, + 42.364379466893)); +#1897 = DIRECTION('',(-1.,-0.,-0.)); +#1898 = DIRECTION('',(0.,1.,0.)); +#1899 = ADVANCED_FACE('',(#1900),#1919,.T.); +#1900 = FACE_BOUND('',#1901,.T.); +#1901 = EDGE_LOOP('',(#1902,#1903,#1911,#1918)); +#1902 = ORIENTED_EDGE('',*,*,#1625,.T.); +#1903 = ORIENTED_EDGE('',*,*,#1904,.T.); +#1904 = EDGE_CURVE('',#1626,#1905,#1907,.T.); +#1905 = VERTEX_POINT('',#1906); +#1906 = CARTESIAN_POINT('',(-17.9758203125,-420.9796295631, + 296.60763662465)); +#1907 = LINE('',#1908,#1909); +#1908 = CARTESIAN_POINT('',(-32.9758203125,-420.9796295631, + 296.60763662465)); +#1909 = VECTOR('',#1910,1.); +#1910 = DIRECTION('',(1.,0.,0.)); +#1911 = ORIENTED_EDGE('',*,*,#1912,.F.); +#1912 = EDGE_CURVE('',#1857,#1905,#1913,.T.); +#1913 = CIRCLE('',#1914,35.4); +#1914 = AXIS2_PLACEMENT_3D('',#1915,#1916,#1917); +#1915 = CARTESIAN_POINT('',(-17.9758203125,-421.9747752548, + 261.22162688843)); +#1916 = DIRECTION('',(1.,-0.,0.)); +#1917 = DIRECTION('',(0.,0.,-1.)); +#1918 = ORIENTED_EDGE('',*,*,#1864,.F.); +#1919 = CYLINDRICAL_SURFACE('',#1920,35.4); +#1920 = AXIS2_PLACEMENT_3D('',#1921,#1922,#1923); +#1921 = CARTESIAN_POINT('',(-32.9758203125,-421.9747752548, + 261.22162688843)); +#1922 = DIRECTION('',(1.,0.,0.)); +#1923 = DIRECTION('',(0.,0.792359119807,0.610054936263)); +#1924 = ADVANCED_FACE('',(#1925),#1975,.F.); +#1925 = FACE_BOUND('',#1926,.F.); +#1926 = EDGE_LOOP('',(#1927,#1928,#1936,#1944,#1952,#1960,#1968,#1974)); +#1927 = ORIENTED_EDGE('',*,*,#1667,.F.); +#1928 = ORIENTED_EDGE('',*,*,#1929,.T.); +#1929 = EDGE_CURVE('',#1660,#1930,#1932,.T.); +#1930 = VERTEX_POINT('',#1931); +#1931 = CARTESIAN_POINT('',(-17.9758203125,-492.2665759806, + 193.13000434422)); +#1932 = LINE('',#1933,#1934); +#1933 = CARTESIAN_POINT('',(-32.9758203125,-492.2665759806, + 193.13000434422)); +#1934 = VECTOR('',#1935,1.); +#1935 = DIRECTION('',(1.,0.,0.)); +#1936 = ORIENTED_EDGE('',*,*,#1937,.T.); +#1937 = EDGE_CURVE('',#1930,#1938,#1940,.T.); +#1938 = VERTEX_POINT('',#1939); +#1939 = CARTESIAN_POINT('',(-17.9758203125,-433.8853429257, + 151.49695998485)); +#1940 = LINE('',#1941,#1942); +#1941 = CARTESIAN_POINT('',(-17.9758203125,-397.3070248444, + 125.41209230415)); +#1942 = VECTOR('',#1943,1.); +#1943 = DIRECTION('',(2.3E-16,0.814180682245,-0.580611588464)); +#1944 = ORIENTED_EDGE('',*,*,#1945,.T.); +#1945 = EDGE_CURVE('',#1938,#1946,#1948,.T.); +#1946 = VERTEX_POINT('',#1947); +#1947 = CARTESIAN_POINT('',(12.0241796875,-433.8853429257, + 151.49695998485)); +#1948 = LINE('',#1949,#1950); +#1949 = CARTESIAN_POINT('',(-17.9758203125,-433.8853429257, + 151.49695998485)); +#1950 = VECTOR('',#1951,1.); +#1951 = DIRECTION('',(1.,-1.58E-15,1.13E-15)); +#1952 = ORIENTED_EDGE('',*,*,#1953,.T.); +#1953 = EDGE_CURVE('',#1946,#1954,#1956,.T.); +#1954 = VERTEX_POINT('',#1955); +#1955 = CARTESIAN_POINT('',(12.0241796875,-492.2665759806, + 193.13000434422)); +#1956 = LINE('',#1957,#1958); +#1957 = CARTESIAN_POINT('',(12.0241796875,-560.6041657279, + 241.86316321204)); +#1958 = VECTOR('',#1959,1.); +#1959 = DIRECTION('',(-2.3E-16,-0.814180682245,0.580611588464)); +#1960 = ORIENTED_EDGE('',*,*,#1961,.T.); +#1961 = EDGE_CURVE('',#1954,#1962,#1964,.T.); +#1962 = VERTEX_POINT('',#1963); +#1963 = CARTESIAN_POINT('',(27.0241796875,-492.2665759806, + 193.13000434422)); +#1964 = LINE('',#1965,#1966); +#1965 = CARTESIAN_POINT('',(-32.9758203125,-492.2665759806, + 193.13000434422)); +#1966 = VECTOR('',#1967,1.); +#1967 = DIRECTION('',(1.,0.,0.)); +#1968 = ORIENTED_EDGE('',*,*,#1969,.T.); +#1969 = EDGE_CURVE('',#1962,#1879,#1970,.T.); +#1970 = LINE('',#1971,#1972); +#1971 = CARTESIAN_POINT('',(27.0241796875,-492.2665759806, + 193.13000434422)); +#1972 = VECTOR('',#1973,1.); +#1973 = DIRECTION('',(0.,0.814180682245,-0.580611588464)); +#1974 = ORIENTED_EDGE('',*,*,#1878,.F.); +#1975 = PLANE('',#1976); +#1976 = AXIS2_PLACEMENT_3D('',#1977,#1978,#1979); +#1977 = CARTESIAN_POINT('',(-32.9758203125,-492.2665759806, + 193.13000434422)); +#1978 = DIRECTION('',(0.,0.580611588464,0.814180682245)); +#1979 = DIRECTION('',(0.,0.814180682245,-0.580611588464)); +#1980 = ADVANCED_FACE('',(#1981),#1999,.F.); +#1981 = FACE_BOUND('',#1982,.F.); +#1982 = EDGE_LOOP('',(#1983,#1984,#1985,#1993)); +#1983 = ORIENTED_EDGE('',*,*,#1634,.F.); +#1984 = ORIENTED_EDGE('',*,*,#1904,.T.); +#1985 = ORIENTED_EDGE('',*,*,#1986,.T.); +#1986 = EDGE_CURVE('',#1905,#1987,#1989,.T.); +#1987 = VERTEX_POINT('',#1988); +#1988 = CARTESIAN_POINT('',(-17.9758203125,-434.1669088513, + 296.97849686764)); +#1989 = LINE('',#1990,#1991); +#1990 = CARTESIAN_POINT('',(-17.9758203125,-333.4853581433, + 294.14707246662)); +#1991 = VECTOR('',#1992,1.); +#1992 = DIRECTION('',(1.6E-16,-0.999604794809,2.811146021781E-02)); +#1993 = ORIENTED_EDGE('',*,*,#1994,.F.); +#1994 = EDGE_CURVE('',#1635,#1987,#1995,.T.); +#1995 = LINE('',#1996,#1997); +#1996 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#1997 = VECTOR('',#1998,1.); +#1998 = DIRECTION('',(1.,0.,0.)); +#1999 = PLANE('',#2000); +#2000 = AXIS2_PLACEMENT_3D('',#2001,#2002,#2003); +#2001 = CARTESIAN_POINT('',(-32.9758203125,-404.178765007,296.1351530611 + )); +#2002 = DIRECTION('',(0.,-2.811146021781E-02,-0.999604794809)); +#2003 = DIRECTION('',(0.,-0.999604794809,2.811146021781E-02)); +#2004 = ADVANCED_FACE('',(#2005),#2023,.F.); +#2005 = FACE_BOUND('',#2006,.F.); +#2006 = EDGE_LOOP('',(#2007,#2008,#2016,#2022)); +#2007 = ORIENTED_EDGE('',*,*,#1659,.F.); +#2008 = ORIENTED_EDGE('',*,*,#2009,.T.); +#2009 = EDGE_CURVE('',#1651,#2010,#2012,.T.); +#2010 = VERTEX_POINT('',#2011); +#2011 = CARTESIAN_POINT('',(-17.9758203125,-601.9549973374, + 323.00860235116)); +#2012 = LINE('',#2013,#2014); +#2013 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#2014 = VECTOR('',#2015,1.); +#2015 = DIRECTION('',(1.,0.,0.)); +#2016 = ORIENTED_EDGE('',*,*,#2017,.T.); +#2017 = EDGE_CURVE('',#2010,#1930,#2018,.T.); +#2018 = LINE('',#2019,#2020); +#2019 = CARTESIAN_POINT('',(-17.9758203125,-478.9133677818, + 177.31889193778)); +#2020 = VECTOR('',#2021,1.); +#2021 = DIRECTION('',(3.8E-16,0.645226007981,-0.763991752982)); +#2022 = ORIENTED_EDGE('',*,*,#1929,.F.); +#2023 = PLANE('',#2024); +#2024 = AXIS2_PLACEMENT_3D('',#2025,#2026,#2027); +#2025 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#2026 = DIRECTION('',(0.,0.763991752982,0.645226007981)); +#2027 = DIRECTION('',(0.,0.645226007981,-0.763991752982)); +#2028 = ADVANCED_FACE('',(#2029),#2047,.F.); +#2029 = FACE_BOUND('',#2030,.F.); +#2030 = EDGE_LOOP('',(#2031,#2032,#2033,#2041)); +#2031 = ORIENTED_EDGE('',*,*,#1642,.F.); +#2032 = ORIENTED_EDGE('',*,*,#1994,.T.); +#2033 = ORIENTED_EDGE('',*,*,#2034,.T.); +#2034 = EDGE_CURVE('',#1987,#2035,#2037,.T.); +#2035 = VERTEX_POINT('',#2036); +#2036 = CARTESIAN_POINT('',(-17.9758203125,-586.4198346814, + 346.16230087024)); +#2037 = LINE('',#2038,#2039); +#2038 = CARTESIAN_POINT('',(-17.9758203125,-330.989745342, + 263.64813319864)); +#2039 = VECTOR('',#2040,1.); +#2040 = DIRECTION('',(-3.E-17,-0.951580786438,0.307398775016)); +#2041 = ORIENTED_EDGE('',*,*,#2042,.F.); +#2042 = EDGE_CURVE('',#1643,#2035,#2043,.T.); +#2043 = LINE('',#2044,#2045); +#2044 = CARTESIAN_POINT('',(-32.9758203125,-586.4198346814, + 346.16230087024)); +#2045 = VECTOR('',#2046,1.); +#2046 = DIRECTION('',(1.,0.,0.)); +#2047 = PLANE('',#2048); +#2048 = AXIS2_PLACEMENT_3D('',#2049,#2050,#2051); +#2049 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#2050 = DIRECTION('',(0.,-0.307398775016,-0.951580786438)); +#2051 = DIRECTION('',(0.,-0.951580786438,0.307398775016)); +#2052 = ADVANCED_FACE('',(#2053),#2065,.T.); +#2053 = FACE_BOUND('',#2054,.F.); +#2054 = EDGE_LOOP('',(#2055,#2056,#2057,#2064)); +#2055 = ORIENTED_EDGE('',*,*,#1650,.F.); +#2056 = ORIENTED_EDGE('',*,*,#2042,.T.); +#2057 = ORIENTED_EDGE('',*,*,#2058,.F.); +#2058 = EDGE_CURVE('',#2010,#2035,#2059,.T.); +#2059 = CIRCLE('',#2060,14.5); +#2060 = AXIS2_PLACEMENT_3D('',#2061,#2062,#2063); +#2061 = CARTESIAN_POINT('',(-17.9758203125,-590.8771169192, + 332.36437946689)); +#2062 = DIRECTION('',(-1.,0.,0.)); +#2063 = DIRECTION('',(0.,1.,0.)); +#2064 = ORIENTED_EDGE('',*,*,#2009,.F.); +#2065 = CYLINDRICAL_SURFACE('',#2066,14.5); +#2066 = AXIS2_PLACEMENT_3D('',#2067,#2068,#2069); +#2067 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#2068 = DIRECTION('',(-1.,-0.,-0.)); +#2069 = DIRECTION('',(0.,1.,0.)); +#2070 = ADVANCED_FACE('',(#2071),#2090,.F.); +#2071 = FACE_BOUND('',#2072,.T.); +#2072 = EDGE_LOOP('',(#2073,#2074,#2082,#2089)); +#2073 = ORIENTED_EDGE('',*,*,#1692,.F.); +#2074 = ORIENTED_EDGE('',*,*,#2075,.T.); +#2075 = EDGE_CURVE('',#1693,#2076,#2078,.T.); +#2076 = VERTEX_POINT('',#2077); +#2077 = CARTESIAN_POINT('',(-17.9758203125,-378.8771169192, + 162.36437946689)); +#2078 = LINE('',#2079,#2080); +#2079 = CARTESIAN_POINT('',(-32.9758203125,-378.8771169192, + 162.36437946689)); +#2080 = VECTOR('',#2081,1.); +#2081 = DIRECTION('',(1.,0.,0.)); +#2082 = ORIENTED_EDGE('',*,*,#2083,.F.); +#2083 = EDGE_CURVE('',#2076,#2076,#2084,.T.); +#2084 = CIRCLE('',#2085,7.); +#2085 = AXIS2_PLACEMENT_3D('',#2086,#2087,#2088); +#2086 = CARTESIAN_POINT('',(-17.9758203125,-385.8771169192, + 162.36437946689)); +#2087 = DIRECTION('',(-1.,0.,0.)); +#2088 = DIRECTION('',(0.,1.,0.)); +#2089 = ORIENTED_EDGE('',*,*,#2075,.F.); +#2090 = CYLINDRICAL_SURFACE('',#2091,7.); +#2091 = AXIS2_PLACEMENT_3D('',#2092,#2093,#2094); +#2092 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 162.36437946689)); +#2093 = DIRECTION('',(-1.,-0.,-0.)); +#2094 = DIRECTION('',(0.,1.,0.)); +#2095 = ADVANCED_FACE('',(#2096),#2115,.F.); +#2096 = FACE_BOUND('',#2097,.T.); +#2097 = EDGE_LOOP('',(#2098,#2106,#2113,#2114)); +#2098 = ORIENTED_EDGE('',*,*,#2099,.T.); +#2099 = EDGE_CURVE('',#1704,#2100,#2102,.T.); +#2100 = VERTEX_POINT('',#2101); +#2101 = CARTESIAN_POINT('',(27.0241796875,-248.8771169192, + 42.364379466893)); +#2102 = LINE('',#2103,#2104); +#2103 = CARTESIAN_POINT('',(-32.9758203125,-248.8771169192, + 42.364379466893)); +#2104 = VECTOR('',#2105,1.); +#2105 = DIRECTION('',(1.,0.,0.)); +#2106 = ORIENTED_EDGE('',*,*,#2107,.T.); +#2107 = EDGE_CURVE('',#2100,#2100,#2108,.T.); +#2108 = CIRCLE('',#2109,7.); +#2109 = AXIS2_PLACEMENT_3D('',#2110,#2111,#2112); +#2110 = CARTESIAN_POINT('',(27.0241796875,-255.8771169192, + 42.364379466893)); +#2111 = DIRECTION('',(1.,0.,0.)); +#2112 = DIRECTION('',(0.,1.,0.)); +#2113 = ORIENTED_EDGE('',*,*,#2099,.F.); +#2114 = ORIENTED_EDGE('',*,*,#1703,.F.); +#2115 = CYLINDRICAL_SURFACE('',#2116,7.); +#2116 = AXIS2_PLACEMENT_3D('',#2117,#2118,#2119); +#2117 = CARTESIAN_POINT('',(-32.9758203125,-255.8771169192, + 42.364379466893)); +#2118 = DIRECTION('',(-1.,-0.,-0.)); +#2119 = DIRECTION('',(0.,1.,0.)); +#2120 = ADVANCED_FACE('',(#2121),#2140,.F.); +#2121 = FACE_BOUND('',#2122,.T.); +#2122 = EDGE_LOOP('',(#2123,#2124,#2132,#2139)); +#2123 = ORIENTED_EDGE('',*,*,#1714,.F.); +#2124 = ORIENTED_EDGE('',*,*,#2125,.T.); +#2125 = EDGE_CURVE('',#1715,#2126,#2128,.T.); +#2126 = VERTEX_POINT('',#2127); +#2127 = CARTESIAN_POINT('',(-17.9758203125,-583.8771169192, + 332.36437946689)); +#2128 = LINE('',#2129,#2130); +#2129 = CARTESIAN_POINT('',(-32.9758203125,-583.8771169192, + 332.36437946689)); +#2130 = VECTOR('',#2131,1.); +#2131 = DIRECTION('',(1.,0.,0.)); +#2132 = ORIENTED_EDGE('',*,*,#2133,.F.); +#2133 = EDGE_CURVE('',#2126,#2126,#2134,.T.); +#2134 = CIRCLE('',#2135,7.); +#2135 = AXIS2_PLACEMENT_3D('',#2136,#2137,#2138); +#2136 = CARTESIAN_POINT('',(-17.9758203125,-590.8771169192, + 332.36437946689)); +#2137 = DIRECTION('',(-1.,0.,0.)); +#2138 = DIRECTION('',(0.,1.,0.)); +#2139 = ORIENTED_EDGE('',*,*,#2125,.F.); +#2140 = CYLINDRICAL_SURFACE('',#2141,7.); +#2141 = AXIS2_PLACEMENT_3D('',#2142,#2143,#2144); +#2142 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#2143 = DIRECTION('',(-1.,-0.,-0.)); +#2144 = DIRECTION('',(0.,1.,0.)); +#2145 = ADVANCED_FACE('',(#2146),#2165,.F.); +#2146 = FACE_BOUND('',#2147,.T.); +#2147 = EDGE_LOOP('',(#2148,#2149,#2157,#2164)); +#2148 = ORIENTED_EDGE('',*,*,#1725,.F.); +#2149 = ORIENTED_EDGE('',*,*,#2150,.T.); +#2150 = EDGE_CURVE('',#1726,#2151,#2153,.T.); +#2151 = VERTEX_POINT('',#2152); +#2152 = CARTESIAN_POINT('',(-17.9758203125,-400.3771169192, + 273.86437946689)); +#2153 = LINE('',#2154,#2155); +#2154 = CARTESIAN_POINT('',(-32.9758203125,-400.3771169192, + 273.86437946689)); +#2155 = VECTOR('',#2156,1.); +#2156 = DIRECTION('',(1.,0.,0.)); +#2157 = ORIENTED_EDGE('',*,*,#2158,.F.); +#2158 = EDGE_CURVE('',#2151,#2151,#2159,.T.); +#2159 = CIRCLE('',#2160,7.); +#2160 = AXIS2_PLACEMENT_3D('',#2161,#2162,#2163); +#2161 = CARTESIAN_POINT('',(-17.9758203125,-407.3771169192, + 273.86437946689)); +#2162 = DIRECTION('',(-1.,0.,0.)); +#2163 = DIRECTION('',(0.,1.,0.)); +#2164 = ORIENTED_EDGE('',*,*,#2150,.F.); +#2165 = CYLINDRICAL_SURFACE('',#2166,7.); +#2166 = AXIS2_PLACEMENT_3D('',#2167,#2168,#2169); +#2167 = CARTESIAN_POINT('',(-32.9758203125,-407.3771169192, + 273.86437946689)); +#2168 = DIRECTION('',(-1.,-0.,-0.)); +#2169 = DIRECTION('',(0.,1.,0.)); +#2170 = ADVANCED_FACE('',(#2171,#2189,#2192,#2195),#2198,.T.); +#2171 = FACE_BOUND('',#2172,.T.); +#2172 = EDGE_LOOP('',(#2173,#2174,#2175,#2176,#2177,#2178,#2184,#2185, + #2186,#2187,#2188)); +#2173 = ORIENTED_EDGE('',*,*,#1986,.T.); +#2174 = ORIENTED_EDGE('',*,*,#2034,.T.); +#2175 = ORIENTED_EDGE('',*,*,#2058,.F.); +#2176 = ORIENTED_EDGE('',*,*,#2017,.T.); +#2177 = ORIENTED_EDGE('',*,*,#1937,.T.); +#2178 = ORIENTED_EDGE('',*,*,#2179,.F.); +#2179 = EDGE_CURVE('',#1832,#1938,#2180,.T.); +#2180 = LINE('',#2181,#2182); +#2181 = CARTESIAN_POINT('',(-17.9758203125,-268.0324671715, + 105.81346687215)); +#2182 = VECTOR('',#2183,1.); +#2183 = DIRECTION('',(0.,-0.964095404234,0.265556117487)); +#2184 = ORIENTED_EDGE('',*,*,#1839,.T.); +#2185 = ORIENTED_EDGE('',*,*,#1751,.T.); +#2186 = ORIENTED_EDGE('',*,*,#1782,.T.); +#2187 = ORIENTED_EDGE('',*,*,#1856,.T.); +#2188 = ORIENTED_EDGE('',*,*,#1912,.T.); +#2189 = FACE_BOUND('',#2190,.T.); +#2190 = EDGE_LOOP('',(#2191)); +#2191 = ORIENTED_EDGE('',*,*,#2158,.T.); +#2192 = FACE_BOUND('',#2193,.T.); +#2193 = EDGE_LOOP('',(#2194)); +#2194 = ORIENTED_EDGE('',*,*,#2133,.T.); +#2195 = FACE_BOUND('',#2196,.T.); +#2196 = EDGE_LOOP('',(#2197)); +#2197 = ORIENTED_EDGE('',*,*,#2083,.T.); +#2198 = PLANE('',#2199); +#2199 = AXIS2_PLACEMENT_3D('',#2200,#2201,#2202); +#2200 = CARTESIAN_POINT('',(-17.9758203125,-268.0324671715, + 105.81346687215)); +#2201 = DIRECTION('',(1.,1.779225987162E-16,6.459439208369E-16)); +#2202 = DIRECTION('',(0.,-0.964095404234,0.265556117487)); +#2203 = ADVANCED_FACE('',(#2204),#2215,.T.); +#2204 = FACE_BOUND('',#2205,.T.); +#2205 = EDGE_LOOP('',(#2206,#2207,#2213,#2214)); +#2206 = ORIENTED_EDGE('',*,*,#1945,.T.); +#2207 = ORIENTED_EDGE('',*,*,#2208,.T.); +#2208 = EDGE_CURVE('',#1946,#1824,#2209,.T.); +#2209 = LINE('',#2210,#2211); +#2210 = CARTESIAN_POINT('',(12.0241796875,-679.8271933454, + 219.24063207179)); +#2211 = VECTOR('',#2212,1.); +#2212 = DIRECTION('',(0.,0.964095404234,-0.265556117487)); +#2213 = ORIENTED_EDGE('',*,*,#1831,.T.); +#2214 = ORIENTED_EDGE('',*,*,#2179,.T.); +#2215 = PLANE('',#2216); +#2216 = AXIS2_PLACEMENT_3D('',#2217,#2218,#2219); +#2217 = CARTESIAN_POINT('',(-2.9758203125,-473.9298302584, + 162.52704947197)); +#2218 = DIRECTION('',(-4.4E-16,0.265556117487,0.964095404234)); +#2219 = DIRECTION('',(1.,1.168446916942E-16,4.24201977863E-16)); +#2220 = ADVANCED_FACE('',(#2221,#2291,#2302,#2313),#2324,.T.); +#2221 = FACE_BOUND('',#2222,.T.); +#2222 = EDGE_LOOP('',(#2223,#2231,#2232,#2233,#2234,#2242,#2251,#2259, + #2267,#2276,#2284)); +#2223 = ORIENTED_EDGE('',*,*,#2224,.T.); +#2224 = EDGE_CURVE('',#2225,#1816,#2227,.T.); +#2225 = VERTEX_POINT('',#2226); +#2226 = CARTESIAN_POINT('',(12.0241796875,-386.5793121341, + 259.26646994902)); +#2227 = LINE('',#2228,#2229); +#2228 = CARTESIAN_POINT('',(12.0241796875,-388.6551221782, + 220.54679263784)); +#2229 = VECTOR('',#2230,1.); +#2230 = DIRECTION('',(6.5E-16,-5.353436527229E-02,-0.9985660077)); +#2231 = ORIENTED_EDGE('',*,*,#1823,.T.); +#2232 = ORIENTED_EDGE('',*,*,#2208,.F.); +#2233 = ORIENTED_EDGE('',*,*,#1953,.T.); +#2234 = ORIENTED_EDGE('',*,*,#2235,.T.); +#2235 = EDGE_CURVE('',#1954,#2236,#2238,.T.); +#2236 = VERTEX_POINT('',#2237); +#2237 = CARTESIAN_POINT('',(12.0241796875,-601.9549973374, + 323.00860235116)); +#2238 = LINE('',#2239,#2240); +#2239 = CARTESIAN_POINT('',(12.0241796875,-592.5886684039,311.9182278585 + )); +#2240 = VECTOR('',#2241,1.); +#2241 = DIRECTION('',(-3.8E-16,-0.645226007981,0.763991752982)); +#2242 = ORIENTED_EDGE('',*,*,#2243,.T.); +#2243 = EDGE_CURVE('',#2236,#2244,#2246,.T.); +#2244 = VERTEX_POINT('',#2245); +#2245 = CARTESIAN_POINT('',(12.0241796875,-586.4198346814, + 346.16230087024)); +#2246 = CIRCLE('',#2247,14.5); +#2247 = AXIS2_PLACEMENT_3D('',#2248,#2249,#2250); +#2248 = CARTESIAN_POINT('',(12.0241796875,-590.8771169192, + 332.36437946689)); +#2249 = DIRECTION('',(-1.,0.,0.)); +#2250 = DIRECTION('',(0.,1.,0.)); +#2251 = ORIENTED_EDGE('',*,*,#2252,.T.); +#2252 = EDGE_CURVE('',#2244,#2253,#2255,.T.); +#2253 = VERTEX_POINT('',#2254); +#2254 = CARTESIAN_POINT('',(12.0241796875,-434.1669088513, + 296.97849686764)); +#2255 = LINE('',#2256,#2257); +#2256 = CARTESIAN_POINT('',(12.0241796875,-534.0206020457, + 329.23524627479)); +#2257 = VECTOR('',#2258,1.); +#2258 = DIRECTION('',(3.E-17,0.951580786438,-0.307398775016)); +#2259 = ORIENTED_EDGE('',*,*,#2260,.T.); +#2260 = EDGE_CURVE('',#2253,#2261,#2263,.T.); +#2261 = VERTEX_POINT('',#2262); +#2262 = CARTESIAN_POINT('',(12.0241796875,-420.9796295631, + 296.60763662465)); +#2263 = LINE('',#2264,#2265); +#2264 = CARTESIAN_POINT('',(12.0241796875,-540.81368152,299.97767866709) + ); +#2265 = VECTOR('',#2266,1.); +#2266 = DIRECTION('',(-1.6E-16,0.999604794809,-2.811146021781E-02)); +#2267 = ORIENTED_EDGE('',*,*,#2268,.F.); +#2268 = EDGE_CURVE('',#2269,#2261,#2271,.T.); +#2269 = VERTEX_POINT('',#2270); +#2270 = CARTESIAN_POINT('',(12.0241796875,-393.9252624137, + 282.81757163213)); +#2271 = CIRCLE('',#2272,35.4); +#2272 = AXIS2_PLACEMENT_3D('',#2273,#2274,#2275); +#2273 = CARTESIAN_POINT('',(12.0241796875,-421.9747752548, + 261.22162688843)); +#2274 = DIRECTION('',(1.,-0.,0.)); +#2275 = DIRECTION('',(0.,0.,-1.)); +#2276 = ORIENTED_EDGE('',*,*,#2277,.T.); +#2277 = EDGE_CURVE('',#2269,#2278,#2280,.T.); +#2278 = VERTEX_POINT('',#2279); +#2279 = CARTESIAN_POINT('',(12.0241796875,-393.8790359656, + 282.75753122336)); +#2280 = LINE('',#2281,#2282); +#2281 = CARTESIAN_POINT('',(12.0241796875,-427.7368323131, + 326.73313426806)); +#2282 = VECTOR('',#2283,1.); +#2283 = DIRECTION('',(4.E-16,0.610054936263,-0.792359119807)); +#2284 = ORIENTED_EDGE('',*,*,#2285,.F.); +#2285 = EDGE_CURVE('',#2225,#2278,#2286,.T.); +#2286 = CIRCLE('',#2287,35.4); +#2287 = AXIS2_PLACEMENT_3D('',#2288,#2289,#2290); +#2288 = CARTESIAN_POINT('',(12.0241796875,-421.9285488067, + 261.16158647966)); +#2289 = DIRECTION('',(1.,-0.,0.)); +#2290 = DIRECTION('',(0.,0.,-1.)); +#2291 = FACE_BOUND('',#2292,.T.); +#2292 = EDGE_LOOP('',(#2293)); +#2293 = ORIENTED_EDGE('',*,*,#2294,.F.); +#2294 = EDGE_CURVE('',#2295,#2295,#2297,.T.); +#2295 = VERTEX_POINT('',#2296); +#2296 = CARTESIAN_POINT('',(12.0241796875,-583.8771169192, + 332.36437946689)); +#2297 = CIRCLE('',#2298,7.); +#2298 = AXIS2_PLACEMENT_3D('',#2299,#2300,#2301); +#2299 = CARTESIAN_POINT('',(12.0241796875,-590.8771169192, + 332.36437946689)); +#2300 = DIRECTION('',(-1.,0.,0.)); +#2301 = DIRECTION('',(0.,1.,0.)); +#2302 = FACE_BOUND('',#2303,.T.); +#2303 = EDGE_LOOP('',(#2304)); +#2304 = ORIENTED_EDGE('',*,*,#2305,.F.); +#2305 = EDGE_CURVE('',#2306,#2306,#2308,.T.); +#2306 = VERTEX_POINT('',#2307); +#2307 = CARTESIAN_POINT('',(12.0241796875,-400.3771169192, + 273.86437946689)); +#2308 = CIRCLE('',#2309,7.); +#2309 = AXIS2_PLACEMENT_3D('',#2310,#2311,#2312); +#2310 = CARTESIAN_POINT('',(12.0241796875,-407.3771169192, + 273.86437946689)); +#2311 = DIRECTION('',(-1.,0.,0.)); +#2312 = DIRECTION('',(0.,1.,0.)); +#2313 = FACE_BOUND('',#2314,.T.); +#2314 = EDGE_LOOP('',(#2315)); +#2315 = ORIENTED_EDGE('',*,*,#2316,.F.); +#2316 = EDGE_CURVE('',#2317,#2317,#2319,.T.); +#2317 = VERTEX_POINT('',#2318); +#2318 = CARTESIAN_POINT('',(12.0241796875,-378.8771169192, + 162.36437946689)); +#2319 = CIRCLE('',#2320,7.); +#2320 = AXIS2_PLACEMENT_3D('',#2321,#2322,#2323); +#2321 = CARTESIAN_POINT('',(12.0241796875,-385.8771169192, + 162.36437946689)); +#2322 = DIRECTION('',(-1.,0.,0.)); +#2323 = DIRECTION('',(0.,1.,0.)); +#2324 = PLANE('',#2325); +#2325 = AXIS2_PLACEMENT_3D('',#2326,#2327,#2328); +#2326 = CARTESIAN_POINT('',(12.0241796875,-679.8271933454, + 219.24063207179)); +#2327 = DIRECTION('',(-1.,-1.779225987162E-16,-6.459439208369E-16)); +#2328 = DIRECTION('',(0.,0.964095404234,-0.265556117487)); +#2329 = ADVANCED_FACE('',(#2330),#2348,.F.); +#2330 = FACE_BOUND('',#2331,.F.); +#2331 = EDGE_LOOP('',(#2332,#2333,#2334,#2342)); +#2332 = ORIENTED_EDGE('',*,*,#2224,.T.); +#2333 = ORIENTED_EDGE('',*,*,#1815,.T.); +#2334 = ORIENTED_EDGE('',*,*,#2335,.T.); +#2335 = EDGE_CURVE('',#1808,#2336,#2338,.T.); +#2336 = VERTEX_POINT('',#2337); +#2337 = CARTESIAN_POINT('',(27.0241796875,-386.5793121341, + 259.26646994902)); +#2338 = LINE('',#2339,#2340); +#2339 = CARTESIAN_POINT('',(27.0241796875,-387.7508197037, + 237.41456919737)); +#2340 = VECTOR('',#2341,1.); +#2341 = DIRECTION('',(0.,5.353436527229E-02,0.9985660077)); +#2342 = ORIENTED_EDGE('',*,*,#2343,.F.); +#2343 = EDGE_CURVE('',#2225,#2336,#2344,.T.); +#2344 = LINE('',#2345,#2346); +#2345 = CARTESIAN_POINT('',(12.0241796875,-386.5793121341, + 259.26646994902)); +#2346 = VECTOR('',#2347,1.); +#2347 = DIRECTION('',(1.,0.,0.)); +#2348 = PLANE('',#2349); +#2349 = AXIS2_PLACEMENT_3D('',#2350,#2351,#2352); +#2350 = CARTESIAN_POINT('',(-32.9758203125,-387.7508197037, + 237.41456919737)); +#2351 = DIRECTION('',(0.,-0.9985660077,5.353436527229E-02)); +#2352 = DIRECTION('',(0.,5.353436527229E-02,0.9985660077)); +#2353 = ADVANCED_FACE('',(#2354,#2417,#2428,#2431,#2442),#2453,.T.); +#2354 = FACE_BOUND('',#2355,.T.); +#2355 = EDGE_LOOP('',(#2356,#2357,#2366,#2374,#2383,#2391,#2399,#2408, + #2414,#2415,#2416)); +#2356 = ORIENTED_EDGE('',*,*,#2335,.T.); +#2357 = ORIENTED_EDGE('',*,*,#2358,.T.); +#2358 = EDGE_CURVE('',#2336,#2359,#2361,.T.); +#2359 = VERTEX_POINT('',#2360); +#2360 = CARTESIAN_POINT('',(27.0241796875,-393.8790359656, + 282.75753122336)); +#2361 = CIRCLE('',#2362,35.4); +#2362 = AXIS2_PLACEMENT_3D('',#2363,#2364,#2365); +#2363 = CARTESIAN_POINT('',(27.0241796875,-421.9285488067, + 261.16158647966)); +#2364 = DIRECTION('',(1.,-0.,0.)); +#2365 = DIRECTION('',(0.,0.,-1.)); +#2366 = ORIENTED_EDGE('',*,*,#2367,.T.); +#2367 = EDGE_CURVE('',#2359,#2368,#2370,.T.); +#2368 = VERTEX_POINT('',#2369); +#2369 = CARTESIAN_POINT('',(27.0241796875,-393.9252624137, + 282.81757163213)); +#2370 = LINE('',#2371,#2372); +#2371 = CARTESIAN_POINT('',(27.0241796875,-385.8771169192, + 272.36437946689)); +#2372 = VECTOR('',#2373,1.); +#2373 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#2374 = ORIENTED_EDGE('',*,*,#2375,.T.); +#2375 = EDGE_CURVE('',#2368,#2376,#2378,.T.); +#2376 = VERTEX_POINT('',#2377); +#2377 = CARTESIAN_POINT('',(27.0241796875,-420.9796295631, + 296.60763662465)); +#2378 = CIRCLE('',#2379,35.4); +#2379 = AXIS2_PLACEMENT_3D('',#2380,#2381,#2382); +#2380 = CARTESIAN_POINT('',(27.0241796875,-421.9747752548, + 261.22162688843)); +#2381 = DIRECTION('',(1.,-0.,0.)); +#2382 = DIRECTION('',(0.,0.,-1.)); +#2383 = ORIENTED_EDGE('',*,*,#2384,.T.); +#2384 = EDGE_CURVE('',#2376,#2385,#2387,.T.); +#2385 = VERTEX_POINT('',#2386); +#2386 = CARTESIAN_POINT('',(27.0241796875,-434.1669088513, + 296.97849686764)); +#2387 = LINE('',#2388,#2389); +#2388 = CARTESIAN_POINT('',(27.0241796875,-404.178765007,296.1351530611) + ); +#2389 = VECTOR('',#2390,1.); +#2390 = DIRECTION('',(0.,-0.999604794809,2.811146021781E-02)); +#2391 = ORIENTED_EDGE('',*,*,#2392,.T.); +#2392 = EDGE_CURVE('',#2385,#2393,#2395,.T.); +#2393 = VERTEX_POINT('',#2394); +#2394 = CARTESIAN_POINT('',(27.0241796875,-586.4198346814, + 346.16230087024)); +#2395 = LINE('',#2396,#2397); +#2396 = CARTESIAN_POINT('',(27.0241796875,-434.1669088513, + 296.97849686764)); +#2397 = VECTOR('',#2398,1.); +#2398 = DIRECTION('',(0.,-0.951580786438,0.307398775016)); +#2399 = ORIENTED_EDGE('',*,*,#2400,.T.); +#2400 = EDGE_CURVE('',#2393,#2401,#2403,.T.); +#2401 = VERTEX_POINT('',#2402); +#2402 = CARTESIAN_POINT('',(27.0241796875,-601.9549973374, + 323.00860235116)); +#2403 = CIRCLE('',#2404,14.5); +#2404 = AXIS2_PLACEMENT_3D('',#2405,#2406,#2407); +#2405 = CARTESIAN_POINT('',(27.0241796875,-590.8771169192, + 332.36437946689)); +#2406 = DIRECTION('',(1.,0.,0.)); +#2407 = DIRECTION('',(0.,1.,0.)); +#2408 = ORIENTED_EDGE('',*,*,#2409,.T.); +#2409 = EDGE_CURVE('',#2401,#1962,#2410,.T.); +#2410 = LINE('',#2411,#2412); +#2411 = CARTESIAN_POINT('',(27.0241796875,-601.9549973374, + 323.00860235116)); +#2412 = VECTOR('',#2413,1.); +#2413 = DIRECTION('',(0.,0.645226007981,-0.763991752982)); +#2414 = ORIENTED_EDGE('',*,*,#1969,.T.); +#2415 = ORIENTED_EDGE('',*,*,#1886,.T.); +#2416 = ORIENTED_EDGE('',*,*,#1807,.T.); +#2417 = FACE_BOUND('',#2418,.T.); +#2418 = EDGE_LOOP('',(#2419)); +#2419 = ORIENTED_EDGE('',*,*,#2420,.F.); +#2420 = EDGE_CURVE('',#2421,#2421,#2423,.T.); +#2421 = VERTEX_POINT('',#2422); +#2422 = CARTESIAN_POINT('',(27.0241796875,-378.8771169192, + 162.36437946689)); +#2423 = CIRCLE('',#2424,7.); +#2424 = AXIS2_PLACEMENT_3D('',#2425,#2426,#2427); +#2425 = CARTESIAN_POINT('',(27.0241796875,-385.8771169192, + 162.36437946689)); +#2426 = DIRECTION('',(1.,0.,0.)); +#2427 = DIRECTION('',(0.,1.,0.)); +#2428 = FACE_BOUND('',#2429,.T.); +#2429 = EDGE_LOOP('',(#2430)); +#2430 = ORIENTED_EDGE('',*,*,#2107,.F.); +#2431 = FACE_BOUND('',#2432,.T.); +#2432 = EDGE_LOOP('',(#2433)); +#2433 = ORIENTED_EDGE('',*,*,#2434,.F.); +#2434 = EDGE_CURVE('',#2435,#2435,#2437,.T.); +#2435 = VERTEX_POINT('',#2436); +#2436 = CARTESIAN_POINT('',(27.0241796875,-583.8771169192, + 332.36437946689)); +#2437 = CIRCLE('',#2438,7.); +#2438 = AXIS2_PLACEMENT_3D('',#2439,#2440,#2441); +#2439 = CARTESIAN_POINT('',(27.0241796875,-590.8771169192, + 332.36437946689)); +#2440 = DIRECTION('',(1.,0.,0.)); +#2441 = DIRECTION('',(0.,1.,0.)); +#2442 = FACE_BOUND('',#2443,.T.); +#2443 = EDGE_LOOP('',(#2444)); +#2444 = ORIENTED_EDGE('',*,*,#2445,.F.); +#2445 = EDGE_CURVE('',#2446,#2446,#2448,.T.); +#2446 = VERTEX_POINT('',#2447); +#2447 = CARTESIAN_POINT('',(27.0241796875,-400.3771169192, + 273.86437946689)); +#2448 = CIRCLE('',#2449,7.); +#2449 = AXIS2_PLACEMENT_3D('',#2450,#2451,#2452); +#2450 = CARTESIAN_POINT('',(27.0241796875,-407.3771169192, + 273.86437946689)); +#2451 = DIRECTION('',(1.,0.,0.)); +#2452 = DIRECTION('',(0.,1.,0.)); +#2453 = PLANE('',#2454); +#2454 = AXIS2_PLACEMENT_3D('',#2455,#2456,#2457); +#2455 = CARTESIAN_POINT('',(27.0241796875,-417.774827926,197.43115187421 + )); +#2456 = DIRECTION('',(1.,0.,0.)); +#2457 = DIRECTION('',(0.,1.,0.)); +#2458 = ADVANCED_FACE('',(#2459),#2470,.F.); +#2459 = FACE_BOUND('',#2460,.F.); +#2460 = EDGE_LOOP('',(#2461,#2462,#2468,#2469)); +#2461 = ORIENTED_EDGE('',*,*,#2235,.T.); +#2462 = ORIENTED_EDGE('',*,*,#2463,.T.); +#2463 = EDGE_CURVE('',#2236,#2401,#2464,.T.); +#2464 = LINE('',#2465,#2466); +#2465 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#2466 = VECTOR('',#2467,1.); +#2467 = DIRECTION('',(1.,0.,0.)); +#2468 = ORIENTED_EDGE('',*,*,#2409,.T.); +#2469 = ORIENTED_EDGE('',*,*,#1961,.F.); +#2470 = PLANE('',#2471); +#2471 = AXIS2_PLACEMENT_3D('',#2472,#2473,#2474); +#2472 = CARTESIAN_POINT('',(-32.9758203125,-601.9549973374, + 323.00860235116)); +#2473 = DIRECTION('',(0.,0.763991752982,0.645226007981)); +#2474 = DIRECTION('',(0.,0.645226007981,-0.763991752982)); +#2475 = ADVANCED_FACE('',(#2476),#2487,.T.); +#2476 = FACE_BOUND('',#2477,.T.); +#2477 = EDGE_LOOP('',(#2478,#2479,#2485,#2486)); +#2478 = ORIENTED_EDGE('',*,*,#2285,.T.); +#2479 = ORIENTED_EDGE('',*,*,#2480,.T.); +#2480 = EDGE_CURVE('',#2278,#2359,#2481,.T.); +#2481 = LINE('',#2482,#2483); +#2482 = CARTESIAN_POINT('',(12.0241796875,-393.8790359656, + 282.75753122336)); +#2483 = VECTOR('',#2484,1.); +#2484 = DIRECTION('',(1.,0.,0.)); +#2485 = ORIENTED_EDGE('',*,*,#2358,.F.); +#2486 = ORIENTED_EDGE('',*,*,#2343,.F.); +#2487 = CYLINDRICAL_SURFACE('',#2488,35.4); +#2488 = AXIS2_PLACEMENT_3D('',#2489,#2490,#2491); +#2489 = CARTESIAN_POINT('',(12.0241796875,-421.9285488067, + 261.16158647966)); +#2490 = DIRECTION('',(1.,0.,0.)); +#2491 = DIRECTION('',(0.,0.9985660077,-5.353436527229E-02)); +#2492 = ADVANCED_FACE('',(#2493),#2504,.F.); +#2493 = FACE_BOUND('',#2494,.F.); +#2494 = EDGE_LOOP('',(#2495,#2496,#2497,#2498)); +#2495 = ORIENTED_EDGE('',*,*,#2277,.T.); +#2496 = ORIENTED_EDGE('',*,*,#2480,.T.); +#2497 = ORIENTED_EDGE('',*,*,#2367,.T.); +#2498 = ORIENTED_EDGE('',*,*,#2499,.F.); +#2499 = EDGE_CURVE('',#2269,#2368,#2500,.T.); +#2500 = LINE('',#2501,#2502); +#2501 = CARTESIAN_POINT('',(12.0241796875,-393.9252624137, + 282.81757163213)); +#2502 = VECTOR('',#2503,1.); +#2503 = DIRECTION('',(1.,0.,0.)); +#2504 = PLANE('',#2505); +#2505 = AXIS2_PLACEMENT_3D('',#2506,#2507,#2508); +#2506 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 272.36437946689)); +#2507 = DIRECTION('',(0.,-0.792359119807,-0.610054936263)); +#2508 = DIRECTION('',(0.,-0.610054936263,0.792359119807)); +#2509 = ADVANCED_FACE('',(#2510),#2521,.T.); +#2510 = FACE_BOUND('',#2511,.T.); +#2511 = EDGE_LOOP('',(#2512,#2513,#2519,#2520)); +#2512 = ORIENTED_EDGE('',*,*,#2268,.T.); +#2513 = ORIENTED_EDGE('',*,*,#2514,.T.); +#2514 = EDGE_CURVE('',#2261,#2376,#2515,.T.); +#2515 = LINE('',#2516,#2517); +#2516 = CARTESIAN_POINT('',(12.0241796875,-420.9796295631, + 296.60763662465)); +#2517 = VECTOR('',#2518,1.); +#2518 = DIRECTION('',(1.,0.,0.)); +#2519 = ORIENTED_EDGE('',*,*,#2375,.F.); +#2520 = ORIENTED_EDGE('',*,*,#2499,.F.); +#2521 = CYLINDRICAL_SURFACE('',#2522,35.4); +#2522 = AXIS2_PLACEMENT_3D('',#2523,#2524,#2525); +#2523 = CARTESIAN_POINT('',(12.0241796875,-421.9747752548, + 261.22162688843)); +#2524 = DIRECTION('',(1.,0.,0.)); +#2525 = DIRECTION('',(0.,0.792359119807,0.610054936263)); +#2526 = ADVANCED_FACE('',(#2527),#2538,.F.); +#2527 = FACE_BOUND('',#2528,.F.); +#2528 = EDGE_LOOP('',(#2529,#2530,#2531,#2532)); +#2529 = ORIENTED_EDGE('',*,*,#2260,.T.); +#2530 = ORIENTED_EDGE('',*,*,#2514,.T.); +#2531 = ORIENTED_EDGE('',*,*,#2384,.T.); +#2532 = ORIENTED_EDGE('',*,*,#2533,.F.); +#2533 = EDGE_CURVE('',#2253,#2385,#2534,.T.); +#2534 = LINE('',#2535,#2536); +#2535 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#2536 = VECTOR('',#2537,1.); +#2537 = DIRECTION('',(1.,0.,0.)); +#2538 = PLANE('',#2539); +#2539 = AXIS2_PLACEMENT_3D('',#2540,#2541,#2542); +#2540 = CARTESIAN_POINT('',(-32.9758203125,-404.178765007,296.1351530611 + )); +#2541 = DIRECTION('',(0.,-2.811146021781E-02,-0.999604794809)); +#2542 = DIRECTION('',(0.,-0.999604794809,2.811146021781E-02)); +#2543 = ADVANCED_FACE('',(#2544),#2555,.F.); +#2544 = FACE_BOUND('',#2545,.F.); +#2545 = EDGE_LOOP('',(#2546,#2547,#2548,#2549)); +#2546 = ORIENTED_EDGE('',*,*,#2252,.T.); +#2547 = ORIENTED_EDGE('',*,*,#2533,.T.); +#2548 = ORIENTED_EDGE('',*,*,#2392,.T.); +#2549 = ORIENTED_EDGE('',*,*,#2550,.F.); +#2550 = EDGE_CURVE('',#2244,#2393,#2551,.T.); +#2551 = LINE('',#2552,#2553); +#2552 = CARTESIAN_POINT('',(-32.9758203125,-586.4198346814, + 346.16230087024)); +#2553 = VECTOR('',#2554,1.); +#2554 = DIRECTION('',(1.,0.,0.)); +#2555 = PLANE('',#2556); +#2556 = AXIS2_PLACEMENT_3D('',#2557,#2558,#2559); +#2557 = CARTESIAN_POINT('',(-32.9758203125,-434.1669088513, + 296.97849686764)); +#2558 = DIRECTION('',(0.,-0.307398775016,-0.951580786438)); +#2559 = DIRECTION('',(0.,-0.951580786438,0.307398775016)); +#2560 = ADVANCED_FACE('',(#2561),#2567,.T.); +#2561 = FACE_BOUND('',#2562,.F.); +#2562 = EDGE_LOOP('',(#2563,#2564,#2565,#2566)); +#2563 = ORIENTED_EDGE('',*,*,#2243,.T.); +#2564 = ORIENTED_EDGE('',*,*,#2550,.T.); +#2565 = ORIENTED_EDGE('',*,*,#2400,.T.); +#2566 = ORIENTED_EDGE('',*,*,#2463,.F.); +#2567 = CYLINDRICAL_SURFACE('',#2568,14.5); +#2568 = AXIS2_PLACEMENT_3D('',#2569,#2570,#2571); +#2569 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#2570 = DIRECTION('',(-1.,-0.,-0.)); +#2571 = DIRECTION('',(0.,1.,0.)); +#2572 = ADVANCED_FACE('',(#2573),#2584,.F.); +#2573 = FACE_BOUND('',#2574,.T.); +#2574 = EDGE_LOOP('',(#2575,#2576,#2582,#2583)); +#2575 = ORIENTED_EDGE('',*,*,#2294,.T.); +#2576 = ORIENTED_EDGE('',*,*,#2577,.T.); +#2577 = EDGE_CURVE('',#2295,#2435,#2578,.T.); +#2578 = LINE('',#2579,#2580); +#2579 = CARTESIAN_POINT('',(-32.9758203125,-583.8771169192, + 332.36437946689)); +#2580 = VECTOR('',#2581,1.); +#2581 = DIRECTION('',(1.,0.,0.)); +#2582 = ORIENTED_EDGE('',*,*,#2434,.T.); +#2583 = ORIENTED_EDGE('',*,*,#2577,.F.); +#2584 = CYLINDRICAL_SURFACE('',#2585,7.); +#2585 = AXIS2_PLACEMENT_3D('',#2586,#2587,#2588); +#2586 = CARTESIAN_POINT('',(-32.9758203125,-590.8771169192, + 332.36437946689)); +#2587 = DIRECTION('',(-1.,-0.,-0.)); +#2588 = DIRECTION('',(0.,1.,0.)); +#2589 = ADVANCED_FACE('',(#2590),#2601,.F.); +#2590 = FACE_BOUND('',#2591,.T.); +#2591 = EDGE_LOOP('',(#2592,#2593,#2599,#2600)); +#2592 = ORIENTED_EDGE('',*,*,#2305,.T.); +#2593 = ORIENTED_EDGE('',*,*,#2594,.T.); +#2594 = EDGE_CURVE('',#2306,#2446,#2595,.T.); +#2595 = LINE('',#2596,#2597); +#2596 = CARTESIAN_POINT('',(-32.9758203125,-400.3771169192, + 273.86437946689)); +#2597 = VECTOR('',#2598,1.); +#2598 = DIRECTION('',(1.,0.,0.)); +#2599 = ORIENTED_EDGE('',*,*,#2445,.T.); +#2600 = ORIENTED_EDGE('',*,*,#2594,.F.); +#2601 = CYLINDRICAL_SURFACE('',#2602,7.); +#2602 = AXIS2_PLACEMENT_3D('',#2603,#2604,#2605); +#2603 = CARTESIAN_POINT('',(-32.9758203125,-407.3771169192, + 273.86437946689)); +#2604 = DIRECTION('',(-1.,-0.,-0.)); +#2605 = DIRECTION('',(0.,1.,0.)); +#2606 = ADVANCED_FACE('',(#2607),#2618,.F.); +#2607 = FACE_BOUND('',#2608,.T.); +#2608 = EDGE_LOOP('',(#2609,#2610,#2616,#2617)); +#2609 = ORIENTED_EDGE('',*,*,#2316,.T.); +#2610 = ORIENTED_EDGE('',*,*,#2611,.T.); +#2611 = EDGE_CURVE('',#2317,#2421,#2612,.T.); +#2612 = LINE('',#2613,#2614); +#2613 = CARTESIAN_POINT('',(-32.9758203125,-378.8771169192, + 162.36437946689)); +#2614 = VECTOR('',#2615,1.); +#2615 = DIRECTION('',(1.,0.,0.)); +#2616 = ORIENTED_EDGE('',*,*,#2420,.T.); +#2617 = ORIENTED_EDGE('',*,*,#2611,.F.); +#2618 = CYLINDRICAL_SURFACE('',#2619,7.); +#2619 = AXIS2_PLACEMENT_3D('',#2620,#2621,#2622); +#2620 = CARTESIAN_POINT('',(-32.9758203125,-385.8771169192, + 162.36437946689)); +#2621 = DIRECTION('',(-1.,-0.,-0.)); +#2622 = DIRECTION('',(0.,1.,0.)); +#2623 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#2627)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#2624,#2625,#2626)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#2624 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#2625 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#2626 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#2627 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#2624, + 'distance_accuracy_value','confusion accuracy'); +#2628 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#2629,#2631); +#2629 = ( REPRESENTATION_RELATIONSHIP('','',#1591,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#2630) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#2630 = ITEM_DEFINED_TRANSFORMATION('','',#11,#23); +#2631 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #2632); +#2632 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('3','Boom001','',#5,#1586,$); +#2633 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#1588)); +#2634 = SHAPE_DEFINITION_REPRESENTATION(#2635,#2641); +#2635 = PRODUCT_DEFINITION_SHAPE('','',#2636); +#2636 = PRODUCT_DEFINITION('design','',#2637,#2640); +#2637 = PRODUCT_DEFINITION_FORMATION('','',#2638); +#2638 = PRODUCT('Stick','Stick','',(#2639)); +#2639 = PRODUCT_CONTEXT('',#2,'mechanical'); +#2640 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#2641 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#2642),#3438); +#2642 = MANIFOLD_SOLID_BREP('',#2643); +#2643 = CLOSED_SHELL('',(#2644,#2686,#2798,#2822,#2901,#2950,#2967,#3016 + ,#3040,#3058,#3083,#3100,#3125,#3150,#3167,#3191,#3215,#3232,#3291, + #3316,#3375,#3392,#3404,#3421)); +#2644 = ADVANCED_FACE('',(#2645),#2681,.T.); +#2645 = FACE_BOUND('',#2646,.T.); +#2646 = EDGE_LOOP('',(#2647,#2658,#2666,#2675)); +#2647 = ORIENTED_EDGE('',*,*,#2648,.F.); +#2648 = EDGE_CURVE('',#2649,#2651,#2653,.T.); +#2649 = VERTEX_POINT('',#2650); +#2650 = CARTESIAN_POINT('',(15.3,-667.2440478981,311.4960657763)); +#2651 = VERTEX_POINT('',#2652); +#2652 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#2653 = CIRCLE('',#2654,13.); +#2654 = AXIS2_PLACEMENT_3D('',#2655,#2656,#2657); +#2655 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#2656 = DIRECTION('',(1.,-0.,0.)); +#2657 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2658 = ORIENTED_EDGE('',*,*,#2659,.T.); +#2659 = EDGE_CURVE('',#2649,#2660,#2662,.T.); +#2660 = VERTEX_POINT('',#2661); +#2661 = CARTESIAN_POINT('',(7.8,-667.2440478981,311.4960657763)); +#2662 = LINE('',#2663,#2664); +#2663 = CARTESIAN_POINT('',(15.3,-667.2440478981,311.4960657763)); +#2664 = VECTOR('',#2665,1.); +#2665 = DIRECTION('',(-1.,-0.,0.)); +#2666 = ORIENTED_EDGE('',*,*,#2667,.T.); +#2667 = EDGE_CURVE('',#2660,#2668,#2670,.T.); +#2668 = VERTEX_POINT('',#2669); +#2669 = CARTESIAN_POINT('',(7.8,-663.5337124387,336.09854297958)); +#2670 = CIRCLE('',#2671,13.); +#2671 = AXIS2_PLACEMENT_3D('',#2672,#2673,#2674); +#2672 = CARTESIAN_POINT('',(7.8,-669.12,324.36)); +#2673 = DIRECTION('',(1.,-0.,0.)); +#2674 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2675 = ORIENTED_EDGE('',*,*,#2676,.F.); +#2676 = EDGE_CURVE('',#2651,#2668,#2677,.T.); +#2677 = LINE('',#2678,#2679); +#2678 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#2679 = VECTOR('',#2680,1.); +#2680 = DIRECTION('',(-1.,-0.,0.)); +#2681 = CYLINDRICAL_SURFACE('',#2682,13.); +#2682 = AXIS2_PLACEMENT_3D('',#2683,#2684,#2685); +#2683 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#2684 = DIRECTION('',(1.,0.,0.)); +#2685 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2686 = ADVANCED_FACE('',(#2687,#2738,#2749,#2760,#2771,#2782),#2793,.T. + ); +#2687 = FACE_BOUND('',#2688,.T.); +#2688 = EDGE_LOOP('',(#2689,#2690,#2698,#2707,#2715,#2723,#2732)); +#2689 = ORIENTED_EDGE('',*,*,#2648,.T.); +#2690 = ORIENTED_EDGE('',*,*,#2691,.T.); +#2691 = EDGE_CURVE('',#2651,#2692,#2694,.T.); +#2692 = VERTEX_POINT('',#2693); +#2693 = CARTESIAN_POINT('',(15.3,-728.949823077,367.22959610918)); +#2694 = LINE('',#2695,#2696); +#2695 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#2696 = VECTOR('',#2697,1.); +#2697 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#2698 = ORIENTED_EDGE('',*,*,#2699,.T.); +#2699 = EDGE_CURVE('',#2692,#2700,#2702,.T.); +#2700 = VERTEX_POINT('',#2701); +#2701 = CARTESIAN_POINT('',(15.3,-739.8792861334,366.91417681032)); +#2702 = CIRCLE('',#2703,12.); +#2703 = AXIS2_PLACEMENT_3D('',#2704,#2705,#2706); +#2704 = CARTESIAN_POINT('',(15.3,-734.1063962105,356.39401797418)); +#2705 = DIRECTION('',(1.,-0.,0.)); +#2706 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2707 = ORIENTED_EDGE('',*,*,#2708,.T.); +#2708 = EDGE_CURVE('',#2700,#2709,#2711,.T.); +#2709 = VERTEX_POINT('',#2710); +#2710 = CARTESIAN_POINT('',(15.3,-836.9944650817,313.62265843604)); +#2711 = LINE('',#2712,#2713); +#2712 = CARTESIAN_POINT('',(15.3,-739.8792861334,366.91417681032)); +#2713 = VECTOR('',#2714,1.); +#2714 = DIRECTION('',(0.,-0.876679903011,-0.481074160246)); +#2715 = ORIENTED_EDGE('',*,*,#2716,.T.); +#2716 = EDGE_CURVE('',#2709,#2717,#2719,.T.); +#2717 = VERTEX_POINT('',#2718); +#2718 = CARTESIAN_POINT('',(15.3,-1.033136025154E+03,285.01926498908)); +#2719 = LINE('',#2720,#2721); +#2720 = CARTESIAN_POINT('',(15.3,-836.9944650817,313.62265843604)); +#2721 = VECTOR('',#2722,1.); +#2722 = DIRECTION('',(0.,-0.989533401823,-0.144304007834)); +#2723 = ORIENTED_EDGE('',*,*,#2724,.T.); +#2724 = EDGE_CURVE('',#2717,#2725,#2727,.T.); +#2725 = VERTEX_POINT('',#2726); +#2726 = CARTESIAN_POINT('',(15.3,-1.029297538545E+03,258.6976765006)); +#2727 = CIRCLE('',#2728,13.3); +#2728 = AXIS2_PLACEMENT_3D('',#2729,#2730,#2731); +#2729 = CARTESIAN_POINT('',(15.3,-1.03121678185E+03,271.85847074484)); +#2730 = DIRECTION('',(1.,-0.,0.)); +#2731 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2732 = ORIENTED_EDGE('',*,*,#2733,.T.); +#2733 = EDGE_CURVE('',#2725,#2649,#2734,.T.); +#2734 = LINE('',#2735,#2736); +#2735 = CARTESIAN_POINT('',(15.3,-1.029297538545E+03,258.6976765006)); +#2736 = VECTOR('',#2737,1.); +#2737 = DIRECTION('',(0.,0.989533401823,0.144304007834)); +#2738 = FACE_BOUND('',#2739,.T.); +#2739 = EDGE_LOOP('',(#2740)); +#2740 = ORIENTED_EDGE('',*,*,#2741,.F.); +#2741 = EDGE_CURVE('',#2742,#2742,#2744,.T.); +#2742 = VERTEX_POINT('',#2743); +#2743 = CARTESIAN_POINT('',(15.3,-724.4679952434,310.66403626855)); +#2744 = CIRCLE('',#2745,7.); +#2745 = AXIS2_PLACEMENT_3D('',#2746,#2747,#2748); +#2746 = CARTESIAN_POINT('',(15.3,-729.5033738458,315.52664486176)); +#2747 = DIRECTION('',(1.,-0.,0.)); +#2748 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2749 = FACE_BOUND('',#2750,.T.); +#2750 = EDGE_LOOP('',(#2751)); +#2751 = ORIENTED_EDGE('',*,*,#2752,.F.); +#2752 = EDGE_CURVE('',#2753,#2753,#2755,.T.); +#2753 = VERTEX_POINT('',#2754); +#2754 = CARTESIAN_POINT('',(15.3,-664.0846213976,319.49739140678)); +#2755 = CIRCLE('',#2756,7.); +#2756 = AXIS2_PLACEMENT_3D('',#2757,#2758,#2759); +#2757 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#2758 = DIRECTION('',(1.,-0.,0.)); +#2759 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2760 = FACE_BOUND('',#2761,.T.); +#2761 = EDGE_LOOP('',(#2762)); +#2762 = ORIENTED_EDGE('',*,*,#2763,.F.); +#2763 = EDGE_CURVE('',#2764,#2764,#2766,.T.); +#2764 = VERTEX_POINT('',#2765); +#2765 = CARTESIAN_POINT('',(15.3,-1.003167429232E+03,273.40889575605)); +#2766 = CIRCLE('',#2767,4.); +#2767 = AXIS2_PLACEMENT_3D('',#2768,#2769,#2770); +#2768 = CARTESIAN_POINT('',(15.3,-1.006044788433E+03,276.18752923788)); +#2769 = DIRECTION('',(1.,-0.,0.)); +#2770 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2771 = FACE_BOUND('',#2772,.T.); +#2772 = EDGE_LOOP('',(#2773)); +#2773 = ORIENTED_EDGE('',*,*,#2774,.F.); +#2774 = EDGE_CURVE('',#2775,#2775,#2777,.T.); +#2775 = VERTEX_POINT('',#2776); +#2776 = CARTESIAN_POINT('',(15.3,-1.028339422648E+03,269.079837263)); +#2777 = CIRCLE('',#2778,4.); +#2778 = AXIS2_PLACEMENT_3D('',#2779,#2780,#2781); +#2779 = CARTESIAN_POINT('',(15.3,-1.03121678185E+03,271.85847074484)); +#2780 = DIRECTION('',(1.,-0.,0.)); +#2781 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2782 = FACE_BOUND('',#2783,.T.); +#2783 = EDGE_LOOP('',(#2784)); +#2784 = ORIENTED_EDGE('',*,*,#2785,.F.); +#2785 = EDGE_CURVE('',#2786,#2786,#2788,.T.); +#2786 = VERTEX_POINT('',#2787); +#2787 = CARTESIAN_POINT('',(15.3,-731.2174119609,344.84612769041)); +#2788 = CIRCLE('',#2789,4.); +#2789 = AXIS2_PLACEMENT_3D('',#2790,#2791,#2792); +#2790 = CARTESIAN_POINT('',(15.3,-734.0947711623,347.62476117225)); +#2791 = DIRECTION('',(1.,-0.,0.)); +#2792 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2793 = PLANE('',#2794); +#2794 = AXIS2_PLACEMENT_3D('',#2795,#2796,#2797); +#2795 = CARTESIAN_POINT('',(15.3,-848.0532044301,303.55900266487)); +#2796 = DIRECTION('',(1.,0.,0.)); +#2797 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2798 = ADVANCED_FACE('',(#2799),#2817,.T.); +#2799 = FACE_BOUND('',#2800,.T.); +#2800 = EDGE_LOOP('',(#2801,#2802,#2803,#2811)); +#2801 = ORIENTED_EDGE('',*,*,#2691,.F.); +#2802 = ORIENTED_EDGE('',*,*,#2676,.T.); +#2803 = ORIENTED_EDGE('',*,*,#2804,.T.); +#2804 = EDGE_CURVE('',#2668,#2805,#2807,.T.); +#2805 = VERTEX_POINT('',#2806); +#2806 = CARTESIAN_POINT('',(7.8,-728.949823077,367.22959610918)); +#2807 = LINE('',#2808,#2809); +#2808 = CARTESIAN_POINT('',(7.8,-685.4527469697,346.52965271499)); +#2809 = VECTOR('',#2810,1.); +#2810 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#2811 = ORIENTED_EDGE('',*,*,#2812,.F.); +#2812 = EDGE_CURVE('',#2692,#2805,#2813,.T.); +#2813 = LINE('',#2814,#2815); +#2814 = CARTESIAN_POINT('',(15.3,-728.949823077,367.22959610918)); +#2815 = VECTOR('',#2816,1.); +#2816 = DIRECTION('',(-1.,-0.,0.)); +#2817 = PLANE('',#2818); +#2818 = AXIS2_PLACEMENT_3D('',#2819,#2820,#2821); +#2819 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#2820 = DIRECTION('',(0.,0.429714427785,0.902964844583)); +#2821 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#2822 = ADVANCED_FACE('',(#2823,#2874,#2885),#2896,.T.); +#2823 = FACE_BOUND('',#2824,.T.); +#2824 = EDGE_LOOP('',(#2825,#2833,#2841,#2849,#2857,#2865,#2872,#2873)); +#2825 = ORIENTED_EDGE('',*,*,#2826,.F.); +#2826 = EDGE_CURVE('',#2827,#2660,#2829,.T.); +#2827 = VERTEX_POINT('',#2828); +#2828 = CARTESIAN_POINT('',(7.8,-685.7421956493,308.7984743124)); +#2829 = LINE('',#2830,#2831); +#2830 = CARTESIAN_POINT('',(7.8,-873.2946214218,281.44763737437)); +#2831 = VECTOR('',#2832,1.); +#2832 = DIRECTION('',(0.,0.989533401823,0.144304007834)); +#2833 = ORIENTED_EDGE('',*,*,#2834,.T.); +#2834 = EDGE_CURVE('',#2827,#2835,#2837,.T.); +#2835 = VERTEX_POINT('',#2836); +#2836 = CARTESIAN_POINT('',(7.8,-730.7447479487,333.18117350803)); +#2837 = LINE('',#2838,#2839); +#2838 = CARTESIAN_POINT('',(7.8,-621.1228565201,273.78726210993)); +#2839 = VECTOR('',#2840,1.); +#2840 = DIRECTION('',(0.,-0.879240277016,0.476378562987)); +#2841 = ORIENTED_EDGE('',*,*,#2842,.T.); +#2842 = EDGE_CURVE('',#2835,#2843,#2845,.T.); +#2843 = VERTEX_POINT('',#2844); +#2844 = CARTESIAN_POINT('',(7.8,-836.9726439828,312.51952940509)); +#2845 = LINE('',#2846,#2847); +#2846 = CARTESIAN_POINT('',(7.8,-730.7447479487,333.18117350803)); +#2847 = VECTOR('',#2848,1.); +#2848 = DIRECTION('',(0.,-0.981604619541,-0.190925039994)); +#2849 = ORIENTED_EDGE('',*,*,#2850,.T.); +#2850 = EDGE_CURVE('',#2843,#2851,#2853,.T.); +#2851 = VERTEX_POINT('',#2852); +#2852 = CARTESIAN_POINT('',(7.8,-826.9342157935,319.14317505971)); +#2853 = LINE('',#2854,#2855); +#2854 = CARTESIAN_POINT('',(7.8,-836.9726439828,312.51952940509)); +#2855 = VECTOR('',#2856,1.); +#2856 = DIRECTION('',(0.,0.834675033285,0.550742761015)); +#2857 = ORIENTED_EDGE('',*,*,#2858,.F.); +#2858 = EDGE_CURVE('',#2859,#2851,#2861,.T.); +#2859 = VERTEX_POINT('',#2860); +#2860 = CARTESIAN_POINT('',(7.8,-739.8792861334,366.91417681032)); +#2861 = LINE('',#2862,#2863); +#2862 = CARTESIAN_POINT('',(7.8,-740.6417055372,366.49580258597)); +#2863 = VECTOR('',#2864,1.); +#2864 = DIRECTION('',(0.,-0.876679903011,-0.481074160246)); +#2865 = ORIENTED_EDGE('',*,*,#2866,.F.); +#2866 = EDGE_CURVE('',#2805,#2859,#2867,.T.); +#2867 = CIRCLE('',#2868,12.); +#2868 = AXIS2_PLACEMENT_3D('',#2869,#2870,#2871); +#2869 = CARTESIAN_POINT('',(7.8,-734.1063962105,356.39401797418)); +#2870 = DIRECTION('',(1.,-0.,0.)); +#2871 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2872 = ORIENTED_EDGE('',*,*,#2804,.F.); +#2873 = ORIENTED_EDGE('',*,*,#2667,.F.); +#2874 = FACE_BOUND('',#2875,.T.); +#2875 = EDGE_LOOP('',(#2876)); +#2876 = ORIENTED_EDGE('',*,*,#2877,.T.); +#2877 = EDGE_CURVE('',#2878,#2878,#2880,.T.); +#2878 = VERTEX_POINT('',#2879); +#2879 = CARTESIAN_POINT('',(7.8,-731.2174119609,344.84612769041)); +#2880 = CIRCLE('',#2881,4.); +#2881 = AXIS2_PLACEMENT_3D('',#2882,#2883,#2884); +#2882 = CARTESIAN_POINT('',(7.8,-734.0947711623,347.62476117225)); +#2883 = DIRECTION('',(1.,-0.,0.)); +#2884 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2885 = FACE_BOUND('',#2886,.T.); +#2886 = EDGE_LOOP('',(#2887)); +#2887 = ORIENTED_EDGE('',*,*,#2888,.T.); +#2888 = EDGE_CURVE('',#2889,#2889,#2891,.T.); +#2889 = VERTEX_POINT('',#2890); +#2890 = CARTESIAN_POINT('',(7.8,-664.0846213976,319.49739140678)); +#2891 = CIRCLE('',#2892,7.); +#2892 = AXIS2_PLACEMENT_3D('',#2893,#2894,#2895); +#2893 = CARTESIAN_POINT('',(7.8,-669.12,324.36)); +#2894 = DIRECTION('',(1.,-0.,0.)); +#2895 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2896 = PLANE('',#2897); +#2897 = AXIS2_PLACEMENT_3D('',#2898,#2899,#2900); +#2898 = CARTESIAN_POINT('',(7.8,-720.8545719082,328.62918929917)); +#2899 = DIRECTION('',(-1.,-0.,0.)); +#2900 = DIRECTION('',(0.,-0.719339800339,0.694658370459)); +#2901 = ADVANCED_FACE('',(#2902),#2945,.T.); +#2902 = FACE_BOUND('',#2903,.T.); +#2903 = EDGE_LOOP('',(#2904,#2905,#2913,#2921,#2929,#2937,#2943,#2944)); +#2904 = ORIENTED_EDGE('',*,*,#2733,.F.); +#2905 = ORIENTED_EDGE('',*,*,#2906,.T.); +#2906 = EDGE_CURVE('',#2725,#2907,#2909,.T.); +#2907 = VERTEX_POINT('',#2908); +#2908 = CARTESIAN_POINT('',(-14.7,-1.029297538545E+03,258.6976765006)); +#2909 = LINE('',#2910,#2911); +#2910 = CARTESIAN_POINT('',(15.3,-1.029297538545E+03,258.6976765006)); +#2911 = VECTOR('',#2912,1.); +#2912 = DIRECTION('',(-1.,-0.,0.)); +#2913 = ORIENTED_EDGE('',*,*,#2914,.T.); +#2914 = EDGE_CURVE('',#2907,#2915,#2917,.T.); +#2915 = VERTEX_POINT('',#2916); +#2916 = CARTESIAN_POINT('',(-14.7,-667.2440478981,311.4960657763)); +#2917 = LINE('',#2918,#2919); +#2918 = CARTESIAN_POINT('',(-14.7,-1.029297538545E+03,258.6976765006)); +#2919 = VECTOR('',#2920,1.); +#2920 = DIRECTION('',(0.,0.989533401823,0.144304007834)); +#2921 = ORIENTED_EDGE('',*,*,#2922,.F.); +#2922 = EDGE_CURVE('',#2923,#2915,#2925,.T.); +#2923 = VERTEX_POINT('',#2924); +#2924 = CARTESIAN_POINT('',(-7.2,-667.2440478981,311.4960657763)); +#2925 = LINE('',#2926,#2927); +#2926 = CARTESIAN_POINT('',(15.3,-667.2440478981,311.4960657763)); +#2927 = VECTOR('',#2928,1.); +#2928 = DIRECTION('',(-1.,-0.,0.)); +#2929 = ORIENTED_EDGE('',*,*,#2930,.F.); +#2930 = EDGE_CURVE('',#2931,#2923,#2933,.T.); +#2931 = VERTEX_POINT('',#2932); +#2932 = CARTESIAN_POINT('',(-7.2,-685.7421956493,308.7984743124)); +#2933 = LINE('',#2934,#2935); +#2934 = CARTESIAN_POINT('',(-7.2,-873.2946214218,281.44763737437)); +#2935 = VECTOR('',#2936,1.); +#2936 = DIRECTION('',(0.,0.989533401823,0.144304007834)); +#2937 = ORIENTED_EDGE('',*,*,#2938,.T.); +#2938 = EDGE_CURVE('',#2931,#2827,#2939,.T.); +#2939 = LINE('',#2940,#2941); +#2940 = CARTESIAN_POINT('',(11.55,-685.7421956493,308.7984743124)); +#2941 = VECTOR('',#2942,1.); +#2942 = DIRECTION('',(1.,0.,0.)); +#2943 = ORIENTED_EDGE('',*,*,#2826,.T.); +#2944 = ORIENTED_EDGE('',*,*,#2659,.F.); +#2945 = PLANE('',#2946); +#2946 = AXIS2_PLACEMENT_3D('',#2947,#2948,#2949); +#2947 = CARTESIAN_POINT('',(15.3,-1.029297538545E+03,258.6976765006)); +#2948 = DIRECTION('',(0.,0.144304007834,-0.989533401823)); +#2949 = DIRECTION('',(0.,0.989533401823,0.144304007834)); +#2950 = ADVANCED_FACE('',(#2951),#2962,.T.); +#2951 = FACE_BOUND('',#2952,.T.); +#2952 = EDGE_LOOP('',(#2953,#2954,#2955,#2956)); +#2953 = ORIENTED_EDGE('',*,*,#2699,.F.); +#2954 = ORIENTED_EDGE('',*,*,#2812,.T.); +#2955 = ORIENTED_EDGE('',*,*,#2866,.T.); +#2956 = ORIENTED_EDGE('',*,*,#2957,.F.); +#2957 = EDGE_CURVE('',#2700,#2859,#2958,.T.); +#2958 = LINE('',#2959,#2960); +#2959 = CARTESIAN_POINT('',(15.3,-739.8792861334,366.91417681032)); +#2960 = VECTOR('',#2961,1.); +#2961 = DIRECTION('',(-1.,-0.,0.)); +#2962 = CYLINDRICAL_SURFACE('',#2963,12.); +#2963 = AXIS2_PLACEMENT_3D('',#2964,#2965,#2966); +#2964 = CARTESIAN_POINT('',(15.3,-734.1063962105,356.39401797418)); +#2965 = DIRECTION('',(1.,0.,0.)); +#2966 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#2967 = ADVANCED_FACE('',(#2968),#3011,.T.); +#2968 = FACE_BOUND('',#2969,.T.); +#2969 = EDGE_LOOP('',(#2970,#2971,#2972,#2973,#2981,#2989,#2997,#3005)); +#2970 = ORIENTED_EDGE('',*,*,#2708,.F.); +#2971 = ORIENTED_EDGE('',*,*,#2957,.T.); +#2972 = ORIENTED_EDGE('',*,*,#2858,.T.); +#2973 = ORIENTED_EDGE('',*,*,#2974,.T.); +#2974 = EDGE_CURVE('',#2851,#2975,#2977,.T.); +#2975 = VERTEX_POINT('',#2976); +#2976 = CARTESIAN_POINT('',(-7.2,-826.9342157935,319.14317505971)); +#2977 = LINE('',#2978,#2979); +#2978 = CARTESIAN_POINT('',(11.55,-826.9342157935,319.14317505971)); +#2979 = VECTOR('',#2980,1.); +#2980 = DIRECTION('',(-1.,0.,0.)); +#2981 = ORIENTED_EDGE('',*,*,#2982,.F.); +#2982 = EDGE_CURVE('',#2983,#2975,#2985,.T.); +#2983 = VERTEX_POINT('',#2984); +#2984 = CARTESIAN_POINT('',(-7.2,-739.8792861334,366.91417681032)); +#2985 = LINE('',#2986,#2987); +#2986 = CARTESIAN_POINT('',(-7.2,-740.6417055372,366.49580258597)); +#2987 = VECTOR('',#2988,1.); +#2988 = DIRECTION('',(0.,-0.876679903011,-0.481074160246)); +#2989 = ORIENTED_EDGE('',*,*,#2990,.T.); +#2990 = EDGE_CURVE('',#2983,#2991,#2993,.T.); +#2991 = VERTEX_POINT('',#2992); +#2992 = CARTESIAN_POINT('',(-14.7,-739.8792861334,366.91417681032)); +#2993 = LINE('',#2994,#2995); +#2994 = CARTESIAN_POINT('',(15.3,-739.8792861334,366.91417681032)); +#2995 = VECTOR('',#2996,1.); +#2996 = DIRECTION('',(-1.,-0.,0.)); +#2997 = ORIENTED_EDGE('',*,*,#2998,.T.); +#2998 = EDGE_CURVE('',#2991,#2999,#3001,.T.); +#2999 = VERTEX_POINT('',#3000); +#3000 = CARTESIAN_POINT('',(-14.7,-836.9944650817,313.62265843604)); +#3001 = LINE('',#3002,#3003); +#3002 = CARTESIAN_POINT('',(-14.7,-739.8792861334,366.91417681032)); +#3003 = VECTOR('',#3004,1.); +#3004 = DIRECTION('',(0.,-0.876679903011,-0.481074160246)); +#3005 = ORIENTED_EDGE('',*,*,#3006,.F.); +#3006 = EDGE_CURVE('',#2709,#2999,#3007,.T.); +#3007 = LINE('',#3008,#3009); +#3008 = CARTESIAN_POINT('',(15.3,-836.9944650817,313.62265843604)); +#3009 = VECTOR('',#3010,1.); +#3010 = DIRECTION('',(-1.,-0.,0.)); +#3011 = PLANE('',#3012); +#3012 = AXIS2_PLACEMENT_3D('',#3013,#3014,#3015); +#3013 = CARTESIAN_POINT('',(15.3,-739.8792861334,366.91417681032)); +#3014 = DIRECTION('',(0.,-0.481074160246,0.876679903011)); +#3015 = DIRECTION('',(0.,-0.876679903011,-0.481074160246)); +#3016 = ADVANCED_FACE('',(#3017),#3035,.T.); +#3017 = FACE_BOUND('',#3018,.T.); +#3018 = EDGE_LOOP('',(#3019,#3020,#3028,#3034)); +#3019 = ORIENTED_EDGE('',*,*,#3006,.T.); +#3020 = ORIENTED_EDGE('',*,*,#3021,.T.); +#3021 = EDGE_CURVE('',#2999,#3022,#3024,.T.); +#3022 = VERTEX_POINT('',#3023); +#3023 = CARTESIAN_POINT('',(-14.7,-1.033136025154E+03,285.01926498908)); +#3024 = LINE('',#3025,#3026); +#3025 = CARTESIAN_POINT('',(-14.7,-836.9944650817,313.62265843604)); +#3026 = VECTOR('',#3027,1.); +#3027 = DIRECTION('',(0.,-0.989533401823,-0.144304007834)); +#3028 = ORIENTED_EDGE('',*,*,#3029,.F.); +#3029 = EDGE_CURVE('',#2717,#3022,#3030,.T.); +#3030 = LINE('',#3031,#3032); +#3031 = CARTESIAN_POINT('',(15.3,-1.033136025154E+03,285.01926498908)); +#3032 = VECTOR('',#3033,1.); +#3033 = DIRECTION('',(-1.,-0.,0.)); +#3034 = ORIENTED_EDGE('',*,*,#2716,.F.); +#3035 = PLANE('',#3036); +#3036 = AXIS2_PLACEMENT_3D('',#3037,#3038,#3039); +#3037 = CARTESIAN_POINT('',(15.3,-836.9944650817,313.62265843604)); +#3038 = DIRECTION('',(0.,-0.144304007834,0.989533401823)); +#3039 = DIRECTION('',(0.,-0.989533401823,-0.144304007834)); +#3040 = ADVANCED_FACE('',(#3041),#3053,.T.); +#3041 = FACE_BOUND('',#3042,.T.); +#3042 = EDGE_LOOP('',(#3043,#3044,#3051,#3052)); +#3043 = ORIENTED_EDGE('',*,*,#3029,.T.); +#3044 = ORIENTED_EDGE('',*,*,#3045,.T.); +#3045 = EDGE_CURVE('',#3022,#2907,#3046,.T.); +#3046 = CIRCLE('',#3047,13.3); +#3047 = AXIS2_PLACEMENT_3D('',#3048,#3049,#3050); +#3048 = CARTESIAN_POINT('',(-14.7,-1.03121678185E+03,271.85847074484)); +#3049 = DIRECTION('',(1.,-0.,0.)); +#3050 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3051 = ORIENTED_EDGE('',*,*,#2906,.F.); +#3052 = ORIENTED_EDGE('',*,*,#2724,.F.); +#3053 = CYLINDRICAL_SURFACE('',#3054,13.3); +#3054 = AXIS2_PLACEMENT_3D('',#3055,#3056,#3057); +#3055 = CARTESIAN_POINT('',(15.3,-1.03121678185E+03,271.85847074484)); +#3056 = DIRECTION('',(1.,0.,0.)); +#3057 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3058 = ADVANCED_FACE('',(#3059),#3078,.F.); +#3059 = FACE_BOUND('',#3060,.F.); +#3060 = EDGE_LOOP('',(#3061,#3069,#3076,#3077)); +#3061 = ORIENTED_EDGE('',*,*,#3062,.T.); +#3062 = EDGE_CURVE('',#2742,#3063,#3065,.T.); +#3063 = VERTEX_POINT('',#3064); +#3064 = CARTESIAN_POINT('',(-14.7,-724.4679952434,310.66403626855)); +#3065 = LINE('',#3066,#3067); +#3066 = CARTESIAN_POINT('',(15.3,-724.4679952434,310.66403626855)); +#3067 = VECTOR('',#3068,1.); +#3068 = DIRECTION('',(-1.,-0.,0.)); +#3069 = ORIENTED_EDGE('',*,*,#3070,.T.); +#3070 = EDGE_CURVE('',#3063,#3063,#3071,.T.); +#3071 = CIRCLE('',#3072,7.); +#3072 = AXIS2_PLACEMENT_3D('',#3073,#3074,#3075); +#3073 = CARTESIAN_POINT('',(-14.7,-729.5033738458,315.52664486176)); +#3074 = DIRECTION('',(1.,-0.,0.)); +#3075 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3076 = ORIENTED_EDGE('',*,*,#3062,.F.); +#3077 = ORIENTED_EDGE('',*,*,#2741,.F.); +#3078 = CYLINDRICAL_SURFACE('',#3079,7.); +#3079 = AXIS2_PLACEMENT_3D('',#3080,#3081,#3082); +#3080 = CARTESIAN_POINT('',(15.3,-729.5033738458,315.52664486176)); +#3081 = DIRECTION('',(1.,0.,0.)); +#3082 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3083 = ADVANCED_FACE('',(#3084),#3095,.F.); +#3084 = FACE_BOUND('',#3085,.F.); +#3085 = EDGE_LOOP('',(#3086,#3087,#3093,#3094)); +#3086 = ORIENTED_EDGE('',*,*,#2752,.F.); +#3087 = ORIENTED_EDGE('',*,*,#3088,.T.); +#3088 = EDGE_CURVE('',#2753,#2889,#3089,.T.); +#3089 = LINE('',#3090,#3091); +#3090 = CARTESIAN_POINT('',(15.3,-664.0846213976,319.49739140678)); +#3091 = VECTOR('',#3092,1.); +#3092 = DIRECTION('',(-1.,-0.,0.)); +#3093 = ORIENTED_EDGE('',*,*,#2888,.T.); +#3094 = ORIENTED_EDGE('',*,*,#3088,.F.); +#3095 = CYLINDRICAL_SURFACE('',#3096,7.); +#3096 = AXIS2_PLACEMENT_3D('',#3097,#3098,#3099); +#3097 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#3098 = DIRECTION('',(1.,0.,0.)); +#3099 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3100 = ADVANCED_FACE('',(#3101),#3120,.F.); +#3101 = FACE_BOUND('',#3102,.F.); +#3102 = EDGE_LOOP('',(#3103,#3111,#3118,#3119)); +#3103 = ORIENTED_EDGE('',*,*,#3104,.T.); +#3104 = EDGE_CURVE('',#2764,#3105,#3107,.T.); +#3105 = VERTEX_POINT('',#3106); +#3106 = CARTESIAN_POINT('',(-14.7,-1.003167429232E+03,273.40889575605)); +#3107 = LINE('',#3108,#3109); +#3108 = CARTESIAN_POINT('',(15.3,-1.003167429232E+03,273.40889575605)); +#3109 = VECTOR('',#3110,1.); +#3110 = DIRECTION('',(-1.,-0.,0.)); +#3111 = ORIENTED_EDGE('',*,*,#3112,.T.); +#3112 = EDGE_CURVE('',#3105,#3105,#3113,.T.); +#3113 = CIRCLE('',#3114,4.); +#3114 = AXIS2_PLACEMENT_3D('',#3115,#3116,#3117); +#3115 = CARTESIAN_POINT('',(-14.7,-1.006044788433E+03,276.18752923788)); +#3116 = DIRECTION('',(1.,-0.,0.)); +#3117 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3118 = ORIENTED_EDGE('',*,*,#3104,.F.); +#3119 = ORIENTED_EDGE('',*,*,#2763,.F.); +#3120 = CYLINDRICAL_SURFACE('',#3121,4.); +#3121 = AXIS2_PLACEMENT_3D('',#3122,#3123,#3124); +#3122 = CARTESIAN_POINT('',(15.3,-1.006044788433E+03,276.18752923788)); +#3123 = DIRECTION('',(1.,0.,0.)); +#3124 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3125 = ADVANCED_FACE('',(#3126),#3145,.F.); +#3126 = FACE_BOUND('',#3127,.F.); +#3127 = EDGE_LOOP('',(#3128,#3136,#3143,#3144)); +#3128 = ORIENTED_EDGE('',*,*,#3129,.T.); +#3129 = EDGE_CURVE('',#2775,#3130,#3132,.T.); +#3130 = VERTEX_POINT('',#3131); +#3131 = CARTESIAN_POINT('',(-14.7,-1.028339422648E+03,269.079837263)); +#3132 = LINE('',#3133,#3134); +#3133 = CARTESIAN_POINT('',(15.3,-1.028339422648E+03,269.079837263)); +#3134 = VECTOR('',#3135,1.); +#3135 = DIRECTION('',(-1.,-0.,0.)); +#3136 = ORIENTED_EDGE('',*,*,#3137,.T.); +#3137 = EDGE_CURVE('',#3130,#3130,#3138,.T.); +#3138 = CIRCLE('',#3139,4.); +#3139 = AXIS2_PLACEMENT_3D('',#3140,#3141,#3142); +#3140 = CARTESIAN_POINT('',(-14.7,-1.03121678185E+03,271.85847074484)); +#3141 = DIRECTION('',(1.,-0.,0.)); +#3142 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3143 = ORIENTED_EDGE('',*,*,#3129,.F.); +#3144 = ORIENTED_EDGE('',*,*,#2774,.F.); +#3145 = CYLINDRICAL_SURFACE('',#3146,4.); +#3146 = AXIS2_PLACEMENT_3D('',#3147,#3148,#3149); +#3147 = CARTESIAN_POINT('',(15.3,-1.03121678185E+03,271.85847074484)); +#3148 = DIRECTION('',(1.,0.,0.)); +#3149 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3150 = ADVANCED_FACE('',(#3151),#3162,.F.); +#3151 = FACE_BOUND('',#3152,.F.); +#3152 = EDGE_LOOP('',(#3153,#3154,#3160,#3161)); +#3153 = ORIENTED_EDGE('',*,*,#2785,.F.); +#3154 = ORIENTED_EDGE('',*,*,#3155,.T.); +#3155 = EDGE_CURVE('',#2786,#2878,#3156,.T.); +#3156 = LINE('',#3157,#3158); +#3157 = CARTESIAN_POINT('',(15.3,-731.2174119609,344.84612769041)); +#3158 = VECTOR('',#3159,1.); +#3159 = DIRECTION('',(-1.,-0.,0.)); +#3160 = ORIENTED_EDGE('',*,*,#2877,.T.); +#3161 = ORIENTED_EDGE('',*,*,#3155,.F.); +#3162 = CYLINDRICAL_SURFACE('',#3163,4.); +#3163 = AXIS2_PLACEMENT_3D('',#3164,#3165,#3166); +#3164 = CARTESIAN_POINT('',(15.3,-734.0947711623,347.62476117225)); +#3165 = DIRECTION('',(1.,0.,0.)); +#3166 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3167 = ADVANCED_FACE('',(#3168),#3186,.T.); +#3168 = FACE_BOUND('',#3169,.T.); +#3169 = EDGE_LOOP('',(#3170,#3171,#3179,#3185)); +#3170 = ORIENTED_EDGE('',*,*,#2850,.F.); +#3171 = ORIENTED_EDGE('',*,*,#3172,.T.); +#3172 = EDGE_CURVE('',#2843,#3173,#3175,.T.); +#3173 = VERTEX_POINT('',#3174); +#3174 = CARTESIAN_POINT('',(-7.2,-836.9726439828,312.51952940509)); +#3175 = LINE('',#3176,#3177); +#3176 = CARTESIAN_POINT('',(7.8,-836.9726439828,312.51952940509)); +#3177 = VECTOR('',#3178,1.); +#3178 = DIRECTION('',(-1.,-0.,0.)); +#3179 = ORIENTED_EDGE('',*,*,#3180,.T.); +#3180 = EDGE_CURVE('',#3173,#2975,#3181,.T.); +#3181 = LINE('',#3182,#3183); +#3182 = CARTESIAN_POINT('',(-7.2,-836.9726439828,312.51952940509)); +#3183 = VECTOR('',#3184,1.); +#3184 = DIRECTION('',(0.,0.834675033285,0.550742761015)); +#3185 = ORIENTED_EDGE('',*,*,#2974,.F.); +#3186 = PLANE('',#3187); +#3187 = AXIS2_PLACEMENT_3D('',#3188,#3189,#3190); +#3188 = CARTESIAN_POINT('',(7.8,-836.9726439828,312.51952940509)); +#3189 = DIRECTION('',(0.,0.550742761015,-0.834675033285)); +#3190 = DIRECTION('',(0.,0.834675033285,0.550742761015)); +#3191 = ADVANCED_FACE('',(#3192),#3210,.T.); +#3192 = FACE_BOUND('',#3193,.T.); +#3193 = EDGE_LOOP('',(#3194,#3202,#3208,#3209)); +#3194 = ORIENTED_EDGE('',*,*,#3195,.T.); +#3195 = EDGE_CURVE('',#2835,#3196,#3198,.T.); +#3196 = VERTEX_POINT('',#3197); +#3197 = CARTESIAN_POINT('',(-7.2,-730.7447479487,333.18117350803)); +#3198 = LINE('',#3199,#3200); +#3199 = CARTESIAN_POINT('',(7.8,-730.7447479487,333.18117350803)); +#3200 = VECTOR('',#3201,1.); +#3201 = DIRECTION('',(-1.,-0.,0.)); +#3202 = ORIENTED_EDGE('',*,*,#3203,.T.); +#3203 = EDGE_CURVE('',#3196,#3173,#3204,.T.); +#3204 = LINE('',#3205,#3206); +#3205 = CARTESIAN_POINT('',(-7.2,-730.7447479487,333.18117350803)); +#3206 = VECTOR('',#3207,1.); +#3207 = DIRECTION('',(0.,-0.981604619541,-0.190925039994)); +#3208 = ORIENTED_EDGE('',*,*,#3172,.F.); +#3209 = ORIENTED_EDGE('',*,*,#2842,.F.); +#3210 = PLANE('',#3211); +#3211 = AXIS2_PLACEMENT_3D('',#3212,#3213,#3214); +#3212 = CARTESIAN_POINT('',(7.8,-730.7447479487,333.18117350803)); +#3213 = DIRECTION('',(0.,-0.190925039994,0.981604619541)); +#3214 = DIRECTION('',(0.,-0.981604619541,-0.190925039994)); +#3215 = ADVANCED_FACE('',(#3216),#3227,.T.); +#3216 = FACE_BOUND('',#3217,.T.); +#3217 = EDGE_LOOP('',(#3218,#3219,#3220,#3226)); +#3218 = ORIENTED_EDGE('',*,*,#2834,.F.); +#3219 = ORIENTED_EDGE('',*,*,#2938,.F.); +#3220 = ORIENTED_EDGE('',*,*,#3221,.T.); +#3221 = EDGE_CURVE('',#2931,#3196,#3222,.T.); +#3222 = LINE('',#3223,#3224); +#3223 = CARTESIAN_POINT('',(-7.2,-621.1228565201,273.78726210993)); +#3224 = VECTOR('',#3225,1.); +#3225 = DIRECTION('',(0.,-0.879240277016,0.476378562987)); +#3226 = ORIENTED_EDGE('',*,*,#3195,.F.); +#3227 = PLANE('',#3228); +#3228 = AXIS2_PLACEMENT_3D('',#3229,#3230,#3231); +#3229 = CARTESIAN_POINT('',(7.8,-621.1228565201,273.78726210993)); +#3230 = DIRECTION('',(0.,0.476378562987,0.879240277016)); +#3231 = DIRECTION('',(0.,-0.879240277016,0.476378562987)); +#3232 = ADVANCED_FACE('',(#3233,#3264,#3275),#3286,.F.); +#3233 = FACE_BOUND('',#3234,.F.); +#3234 = EDGE_LOOP('',(#3235,#3236,#3237,#3238,#3239,#3240,#3249,#3257)); +#3235 = ORIENTED_EDGE('',*,*,#2930,.F.); +#3236 = ORIENTED_EDGE('',*,*,#3221,.T.); +#3237 = ORIENTED_EDGE('',*,*,#3203,.T.); +#3238 = ORIENTED_EDGE('',*,*,#3180,.T.); +#3239 = ORIENTED_EDGE('',*,*,#2982,.F.); +#3240 = ORIENTED_EDGE('',*,*,#3241,.F.); +#3241 = EDGE_CURVE('',#3242,#2983,#3244,.T.); +#3242 = VERTEX_POINT('',#3243); +#3243 = CARTESIAN_POINT('',(-7.2,-728.949823077,367.22959610918)); +#3244 = CIRCLE('',#3245,12.); +#3245 = AXIS2_PLACEMENT_3D('',#3246,#3247,#3248); +#3246 = CARTESIAN_POINT('',(-7.2,-734.1063962105,356.39401797418)); +#3247 = DIRECTION('',(1.,-0.,0.)); +#3248 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3249 = ORIENTED_EDGE('',*,*,#3250,.F.); +#3250 = EDGE_CURVE('',#3251,#3242,#3253,.T.); +#3251 = VERTEX_POINT('',#3252); +#3252 = CARTESIAN_POINT('',(-7.2,-663.5337124387,336.09854297958)); +#3253 = LINE('',#3254,#3255); +#3254 = CARTESIAN_POINT('',(-7.2,-685.4527469697,346.52965271499)); +#3255 = VECTOR('',#3256,1.); +#3256 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#3257 = ORIENTED_EDGE('',*,*,#3258,.F.); +#3258 = EDGE_CURVE('',#2923,#3251,#3259,.T.); +#3259 = CIRCLE('',#3260,13.); +#3260 = AXIS2_PLACEMENT_3D('',#3261,#3262,#3263); +#3261 = CARTESIAN_POINT('',(-7.2,-669.12,324.36)); +#3262 = DIRECTION('',(1.,-0.,0.)); +#3263 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3264 = FACE_BOUND('',#3265,.F.); +#3265 = EDGE_LOOP('',(#3266)); +#3266 = ORIENTED_EDGE('',*,*,#3267,.T.); +#3267 = EDGE_CURVE('',#3268,#3268,#3270,.T.); +#3268 = VERTEX_POINT('',#3269); +#3269 = CARTESIAN_POINT('',(-7.2,-731.2174119609,344.84612769041)); +#3270 = CIRCLE('',#3271,4.); +#3271 = AXIS2_PLACEMENT_3D('',#3272,#3273,#3274); +#3272 = CARTESIAN_POINT('',(-7.2,-734.0947711623,347.62476117225)); +#3273 = DIRECTION('',(1.,-0.,0.)); +#3274 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3275 = FACE_BOUND('',#3276,.F.); +#3276 = EDGE_LOOP('',(#3277)); +#3277 = ORIENTED_EDGE('',*,*,#3278,.T.); +#3278 = EDGE_CURVE('',#3279,#3279,#3281,.T.); +#3279 = VERTEX_POINT('',#3280); +#3280 = CARTESIAN_POINT('',(-7.2,-664.0846213976,319.49739140678)); +#3281 = CIRCLE('',#3282,7.); +#3282 = AXIS2_PLACEMENT_3D('',#3283,#3284,#3285); +#3283 = CARTESIAN_POINT('',(-7.2,-669.12,324.36)); +#3284 = DIRECTION('',(1.,-0.,0.)); +#3285 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3286 = PLANE('',#3287); +#3287 = AXIS2_PLACEMENT_3D('',#3288,#3289,#3290); +#3288 = CARTESIAN_POINT('',(-7.2,-720.8545719082,328.62918929917)); +#3289 = DIRECTION('',(-1.,-0.,0.)); +#3290 = DIRECTION('',(0.,-0.719339800339,0.694658370459)); +#3291 = ADVANCED_FACE('',(#3292),#3311,.T.); +#3292 = FACE_BOUND('',#3293,.T.); +#3293 = EDGE_LOOP('',(#3294,#3295,#3296,#3305)); +#3294 = ORIENTED_EDGE('',*,*,#3258,.F.); +#3295 = ORIENTED_EDGE('',*,*,#2922,.T.); +#3296 = ORIENTED_EDGE('',*,*,#3297,.T.); +#3297 = EDGE_CURVE('',#2915,#3298,#3300,.T.); +#3298 = VERTEX_POINT('',#3299); +#3299 = CARTESIAN_POINT('',(-14.7,-663.5337124387,336.09854297958)); +#3300 = CIRCLE('',#3301,13.); +#3301 = AXIS2_PLACEMENT_3D('',#3302,#3303,#3304); +#3302 = CARTESIAN_POINT('',(-14.7,-669.12,324.36)); +#3303 = DIRECTION('',(1.,-0.,0.)); +#3304 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3305 = ORIENTED_EDGE('',*,*,#3306,.F.); +#3306 = EDGE_CURVE('',#3251,#3298,#3307,.T.); +#3307 = LINE('',#3308,#3309); +#3308 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#3309 = VECTOR('',#3310,1.); +#3310 = DIRECTION('',(-1.,-0.,0.)); +#3311 = CYLINDRICAL_SURFACE('',#3312,13.); +#3312 = AXIS2_PLACEMENT_3D('',#3313,#3314,#3315); +#3313 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#3314 = DIRECTION('',(1.,0.,0.)); +#3315 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3316 = ADVANCED_FACE('',(#3317,#3339,#3342,#3353,#3356,#3359),#3370,.F. + ); +#3317 = FACE_BOUND('',#3318,.F.); +#3318 = EDGE_LOOP('',(#3319,#3320,#3328,#3335,#3336,#3337,#3338)); +#3319 = ORIENTED_EDGE('',*,*,#3297,.T.); +#3320 = ORIENTED_EDGE('',*,*,#3321,.T.); +#3321 = EDGE_CURVE('',#3298,#3322,#3324,.T.); +#3322 = VERTEX_POINT('',#3323); +#3323 = CARTESIAN_POINT('',(-14.7,-728.949823077,367.22959610918)); +#3324 = LINE('',#3325,#3326); +#3325 = CARTESIAN_POINT('',(-14.7,-663.5337124387,336.09854297958)); +#3326 = VECTOR('',#3327,1.); +#3327 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#3328 = ORIENTED_EDGE('',*,*,#3329,.T.); +#3329 = EDGE_CURVE('',#3322,#2991,#3330,.T.); +#3330 = CIRCLE('',#3331,12.); +#3331 = AXIS2_PLACEMENT_3D('',#3332,#3333,#3334); +#3332 = CARTESIAN_POINT('',(-14.7,-734.1063962105,356.39401797418)); +#3333 = DIRECTION('',(1.,-0.,0.)); +#3334 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3335 = ORIENTED_EDGE('',*,*,#2998,.T.); +#3336 = ORIENTED_EDGE('',*,*,#3021,.T.); +#3337 = ORIENTED_EDGE('',*,*,#3045,.T.); +#3338 = ORIENTED_EDGE('',*,*,#2914,.T.); +#3339 = FACE_BOUND('',#3340,.F.); +#3340 = EDGE_LOOP('',(#3341)); +#3341 = ORIENTED_EDGE('',*,*,#3070,.F.); +#3342 = FACE_BOUND('',#3343,.F.); +#3343 = EDGE_LOOP('',(#3344)); +#3344 = ORIENTED_EDGE('',*,*,#3345,.F.); +#3345 = EDGE_CURVE('',#3346,#3346,#3348,.T.); +#3346 = VERTEX_POINT('',#3347); +#3347 = CARTESIAN_POINT('',(-14.7,-664.0846213976,319.49739140678)); +#3348 = CIRCLE('',#3349,7.); +#3349 = AXIS2_PLACEMENT_3D('',#3350,#3351,#3352); +#3350 = CARTESIAN_POINT('',(-14.7,-669.12,324.36)); +#3351 = DIRECTION('',(1.,-0.,0.)); +#3352 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3353 = FACE_BOUND('',#3354,.F.); +#3354 = EDGE_LOOP('',(#3355)); +#3355 = ORIENTED_EDGE('',*,*,#3112,.F.); +#3356 = FACE_BOUND('',#3357,.F.); +#3357 = EDGE_LOOP('',(#3358)); +#3358 = ORIENTED_EDGE('',*,*,#3137,.F.); +#3359 = FACE_BOUND('',#3360,.F.); +#3360 = EDGE_LOOP('',(#3361)); +#3361 = ORIENTED_EDGE('',*,*,#3362,.F.); +#3362 = EDGE_CURVE('',#3363,#3363,#3365,.T.); +#3363 = VERTEX_POINT('',#3364); +#3364 = CARTESIAN_POINT('',(-14.7,-731.2174119609,344.84612769041)); +#3365 = CIRCLE('',#3366,4.); +#3366 = AXIS2_PLACEMENT_3D('',#3367,#3368,#3369); +#3367 = CARTESIAN_POINT('',(-14.7,-734.0947711623,347.62476117225)); +#3368 = DIRECTION('',(1.,-0.,0.)); +#3369 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3370 = PLANE('',#3371); +#3371 = AXIS2_PLACEMENT_3D('',#3372,#3373,#3374); +#3372 = CARTESIAN_POINT('',(-14.7,-848.0532044301,303.55900266487)); +#3373 = DIRECTION('',(1.,0.,0.)); +#3374 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3375 = ADVANCED_FACE('',(#3376),#3387,.T.); +#3376 = FACE_BOUND('',#3377,.T.); +#3377 = EDGE_LOOP('',(#3378,#3379,#3385,#3386)); +#3378 = ORIENTED_EDGE('',*,*,#3241,.F.); +#3379 = ORIENTED_EDGE('',*,*,#3380,.T.); +#3380 = EDGE_CURVE('',#3242,#3322,#3381,.T.); +#3381 = LINE('',#3382,#3383); +#3382 = CARTESIAN_POINT('',(15.3,-728.949823077,367.22959610918)); +#3383 = VECTOR('',#3384,1.); +#3384 = DIRECTION('',(-1.,-0.,0.)); +#3385 = ORIENTED_EDGE('',*,*,#3329,.T.); +#3386 = ORIENTED_EDGE('',*,*,#2990,.F.); +#3387 = CYLINDRICAL_SURFACE('',#3388,12.); +#3388 = AXIS2_PLACEMENT_3D('',#3389,#3390,#3391); +#3389 = CARTESIAN_POINT('',(15.3,-734.1063962105,356.39401797418)); +#3390 = DIRECTION('',(1.,0.,0.)); +#3391 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3392 = ADVANCED_FACE('',(#3393),#3399,.T.); +#3393 = FACE_BOUND('',#3394,.T.); +#3394 = EDGE_LOOP('',(#3395,#3396,#3397,#3398)); +#3395 = ORIENTED_EDGE('',*,*,#3250,.F.); +#3396 = ORIENTED_EDGE('',*,*,#3306,.T.); +#3397 = ORIENTED_EDGE('',*,*,#3321,.T.); +#3398 = ORIENTED_EDGE('',*,*,#3380,.F.); +#3399 = PLANE('',#3400); +#3400 = AXIS2_PLACEMENT_3D('',#3401,#3402,#3403); +#3401 = CARTESIAN_POINT('',(15.3,-663.5337124387,336.09854297958)); +#3402 = DIRECTION('',(0.,0.429714427785,0.902964844583)); +#3403 = DIRECTION('',(0.,-0.902964844583,0.429714427785)); +#3404 = ADVANCED_FACE('',(#3405),#3416,.F.); +#3405 = FACE_BOUND('',#3406,.F.); +#3406 = EDGE_LOOP('',(#3407,#3408,#3414,#3415)); +#3407 = ORIENTED_EDGE('',*,*,#3267,.F.); +#3408 = ORIENTED_EDGE('',*,*,#3409,.T.); +#3409 = EDGE_CURVE('',#3268,#3363,#3410,.T.); +#3410 = LINE('',#3411,#3412); +#3411 = CARTESIAN_POINT('',(15.3,-731.2174119609,344.84612769041)); +#3412 = VECTOR('',#3413,1.); +#3413 = DIRECTION('',(-1.,-0.,0.)); +#3414 = ORIENTED_EDGE('',*,*,#3362,.T.); +#3415 = ORIENTED_EDGE('',*,*,#3409,.F.); +#3416 = CYLINDRICAL_SURFACE('',#3417,4.); +#3417 = AXIS2_PLACEMENT_3D('',#3418,#3419,#3420); +#3418 = CARTESIAN_POINT('',(15.3,-734.0947711623,347.62476117225)); +#3419 = DIRECTION('',(1.,0.,0.)); +#3420 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3421 = ADVANCED_FACE('',(#3422),#3433,.F.); +#3422 = FACE_BOUND('',#3423,.F.); +#3423 = EDGE_LOOP('',(#3424,#3425,#3431,#3432)); +#3424 = ORIENTED_EDGE('',*,*,#3278,.F.); +#3425 = ORIENTED_EDGE('',*,*,#3426,.T.); +#3426 = EDGE_CURVE('',#3279,#3346,#3427,.T.); +#3427 = LINE('',#3428,#3429); +#3428 = CARTESIAN_POINT('',(15.3,-664.0846213976,319.49739140678)); +#3429 = VECTOR('',#3430,1.); +#3430 = DIRECTION('',(-1.,-0.,0.)); +#3431 = ORIENTED_EDGE('',*,*,#3345,.T.); +#3432 = ORIENTED_EDGE('',*,*,#3426,.F.); +#3433 = CYLINDRICAL_SURFACE('',#3434,7.); +#3434 = AXIS2_PLACEMENT_3D('',#3435,#3436,#3437); +#3435 = CARTESIAN_POINT('',(15.3,-669.12,324.36)); +#3436 = DIRECTION('',(1.,0.,0.)); +#3437 = DIRECTION('',(0.,0.719339800339,-0.694658370459)); +#3438 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3442)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#3439,#3440,#3441)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#3439 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#3440 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#3441 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#3442 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#3439, + 'distance_accuracy_value','confusion accuracy'); +#3443 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3444,#3446); +#3444 = ( REPRESENTATION_RELATIONSHIP('','',#2641,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3445) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#3445 = ITEM_DEFINED_TRANSFORMATION('','',#11,#27); +#3446 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #3447); +#3447 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('4','Stick001','',#5,#2636,$); +#3448 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#2638)); +#3449 = SHAPE_DEFINITION_REPRESENTATION(#3450,#3456); +#3450 = PRODUCT_DEFINITION_SHAPE('','',#3451); +#3451 = PRODUCT_DEFINITION('design','',#3452,#3455); +#3452 = PRODUCT_DEFINITION_FORMATION('','',#3453); +#3453 = PRODUCT('Bucket','Bucket','',(#3454)); +#3454 = PRODUCT_CONTEXT('',#2,'mechanical'); +#3455 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#3456 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3457),#6330); +#3457 = MANIFOLD_SOLID_BREP('',#3458); +#3458 = CLOSED_SHELL('',(#3459,#3499,#3547,#3571,#3602,#3676,#3732,#3757 + ,#3854,#3871,#3902,#3933,#3980,#4164,#4189,#4230,#4255,#4279,#4361, + #4378,#4402,#4426,#4452,#4476,#4498,#4529,#4551,#4582,#4604,#4635, + #4657,#4688,#4753,#4778,#4866,#4888,#4912,#4936,#4960,#4977,#4991, + #5024,#5048,#5074,#5107,#5131,#5157,#5190,#5214,#5240,#5273,#5297, + #5323,#5348,#5381,#5399,#5432,#5457,#5474,#5491,#5563,#5580,#5597, + #5609,#5626,#5643,#5655,#5672,#5689,#5706,#5723,#5810,#5834,#5859, + #5893,#5917,#5935,#5959,#6026,#6038,#6050,#6062,#6074,#6092,#6116, + #6133,#6151,#6175,#6193,#6210,#6227,#6244,#6256,#6273,#6290,#6301, + #6319)); +#3459 = ADVANCED_FACE('',(#3460),#3494,.T.); +#3460 = FACE_BOUND('',#3461,.T.); +#3461 = EDGE_LOOP('',(#3462,#3472,#3480,#3488)); +#3462 = ORIENTED_EDGE('',*,*,#3463,.F.); +#3463 = EDGE_CURVE('',#3464,#3466,#3468,.T.); +#3464 = VERTEX_POINT('',#3465); +#3465 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3466 = VERTEX_POINT('',#3467); +#3467 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#3468 = LINE('',#3469,#3470); +#3469 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3470 = VECTOR('',#3471,1.); +#3471 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#3472 = ORIENTED_EDGE('',*,*,#3473,.T.); +#3473 = EDGE_CURVE('',#3464,#3474,#3476,.T.); +#3474 = VERTEX_POINT('',#3475); +#3475 = CARTESIAN_POINT('',(-13.9,-1.10154E+03,200.94)); +#3476 = LINE('',#3477,#3478); +#3477 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3478 = VECTOR('',#3479,1.); +#3479 = DIRECTION('',(1.,0.,0.)); +#3480 = ORIENTED_EDGE('',*,*,#3481,.F.); +#3481 = EDGE_CURVE('',#3482,#3474,#3484,.T.); +#3482 = VERTEX_POINT('',#3483); +#3483 = CARTESIAN_POINT('',(-13.9,-1.11034E+03,229.64)); +#3484 = LINE('',#3485,#3486); +#3485 = CARTESIAN_POINT('',(-13.9,-1.11034E+03,229.64)); +#3486 = VECTOR('',#3487,1.); +#3487 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#3488 = ORIENTED_EDGE('',*,*,#3489,.F.); +#3489 = EDGE_CURVE('',#3466,#3482,#3490,.T.); +#3490 = LINE('',#3491,#3492); +#3491 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#3492 = VECTOR('',#3493,1.); +#3493 = DIRECTION('',(1.,0.,0.)); +#3494 = PLANE('',#3495); +#3495 = AXIS2_PLACEMENT_3D('',#3496,#3497,#3498); +#3496 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3497 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#3498 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#3499 = ADVANCED_FACE('',(#3500),#3542,.T.); +#3500 = FACE_BOUND('',#3501,.T.); +#3501 = EDGE_LOOP('',(#3502,#3503,#3511,#3519,#3528,#3536)); +#3502 = ORIENTED_EDGE('',*,*,#3463,.T.); +#3503 = ORIENTED_EDGE('',*,*,#3504,.T.); +#3504 = EDGE_CURVE('',#3466,#3505,#3507,.T.); +#3505 = VERTEX_POINT('',#3506); +#3506 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3507 = LINE('',#3508,#3509); +#3508 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#3509 = VECTOR('',#3510,1.); +#3510 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#3511 = ORIENTED_EDGE('',*,*,#3512,.T.); +#3512 = EDGE_CURVE('',#3505,#3513,#3515,.T.); +#3513 = VERTEX_POINT('',#3514); +#3514 = CARTESIAN_POINT('',(-44.9,-1.181180495325E+03,225.94254351617)); +#3515 = LINE('',#3516,#3517); +#3516 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3517 = VECTOR('',#3518,1.); +#3518 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#3519 = ORIENTED_EDGE('',*,*,#3520,.T.); +#3520 = EDGE_CURVE('',#3513,#3521,#3523,.T.); +#3521 = VERTEX_POINT('',#3522); +#3522 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#3523 = CIRCLE('',#3524,10.); +#3524 = AXIS2_PLACEMENT_3D('',#3525,#3526,#3527); +#3525 = CARTESIAN_POINT('',(-44.9,-1.17704E+03,216.84)); +#3526 = DIRECTION('',(1.,0.,0.)); +#3527 = DIRECTION('',(0.,1.,0.)); +#3528 = ORIENTED_EDGE('',*,*,#3529,.T.); +#3529 = EDGE_CURVE('',#3521,#3530,#3532,.T.); +#3530 = VERTEX_POINT('',#3531); +#3531 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3532 = LINE('',#3533,#3534); +#3533 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#3534 = VECTOR('',#3535,1.); +#3535 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#3536 = ORIENTED_EDGE('',*,*,#3537,.T.); +#3537 = EDGE_CURVE('',#3530,#3464,#3538,.T.); +#3538 = LINE('',#3539,#3540); +#3539 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3540 = VECTOR('',#3541,1.); +#3541 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#3542 = PLANE('',#3543); +#3543 = AXIS2_PLACEMENT_3D('',#3544,#3545,#3546); +#3544 = CARTESIAN_POINT('',(-44.9,-1.147896717874E+03,193.1785020231)); +#3545 = DIRECTION('',(1.,0.,0.)); +#3546 = DIRECTION('',(0.,1.,0.)); +#3547 = ADVANCED_FACE('',(#3548),#3566,.T.); +#3548 = FACE_BOUND('',#3549,.T.); +#3549 = EDGE_LOOP('',(#3550,#3551,#3552,#3560)); +#3550 = ORIENTED_EDGE('',*,*,#3504,.F.); +#3551 = ORIENTED_EDGE('',*,*,#3489,.T.); +#3552 = ORIENTED_EDGE('',*,*,#3553,.F.); +#3553 = EDGE_CURVE('',#3554,#3482,#3556,.T.); +#3554 = VERTEX_POINT('',#3555); +#3555 = CARTESIAN_POINT('',(-13.9,-1.16184E+03,234.74)); +#3556 = LINE('',#3557,#3558); +#3557 = CARTESIAN_POINT('',(-13.9,-1.16184E+03,234.74)); +#3558 = VECTOR('',#3559,1.); +#3559 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#3560 = ORIENTED_EDGE('',*,*,#3561,.F.); +#3561 = EDGE_CURVE('',#3505,#3554,#3562,.T.); +#3562 = LINE('',#3563,#3564); +#3563 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3564 = VECTOR('',#3565,1.); +#3565 = DIRECTION('',(1.,0.,0.)); +#3566 = PLANE('',#3567); +#3567 = AXIS2_PLACEMENT_3D('',#3568,#3569,#3570); +#3568 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#3569 = DIRECTION('',(0.,-9.85470909115E-02,-0.995132388616)); +#3570 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#3571 = ADVANCED_FACE('',(#3572),#3597,.T.); +#3572 = FACE_BOUND('',#3573,.T.); +#3573 = EDGE_LOOP('',(#3574,#3582,#3583,#3591)); +#3574 = ORIENTED_EDGE('',*,*,#3575,.T.); +#3575 = EDGE_CURVE('',#3576,#3482,#3578,.T.); +#3576 = VERTEX_POINT('',#3577); +#3577 = CARTESIAN_POINT('',(-9.9,-1.11034E+03,229.64)); +#3578 = LINE('',#3579,#3580); +#3579 = CARTESIAN_POINT('',(-9.9,-1.11034E+03,229.64)); +#3580 = VECTOR('',#3581,1.); +#3581 = DIRECTION('',(-1.,-0.,-0.)); +#3582 = ORIENTED_EDGE('',*,*,#3481,.T.); +#3583 = ORIENTED_EDGE('',*,*,#3584,.F.); +#3584 = EDGE_CURVE('',#3585,#3474,#3587,.T.); +#3585 = VERTEX_POINT('',#3586); +#3586 = CARTESIAN_POINT('',(-9.9,-1.10154E+03,200.94)); +#3587 = LINE('',#3588,#3589); +#3588 = CARTESIAN_POINT('',(-9.9,-1.10154E+03,200.94)); +#3589 = VECTOR('',#3590,1.); +#3590 = DIRECTION('',(-1.,-0.,-0.)); +#3591 = ORIENTED_EDGE('',*,*,#3592,.F.); +#3592 = EDGE_CURVE('',#3576,#3585,#3593,.T.); +#3593 = LINE('',#3594,#3595); +#3594 = CARTESIAN_POINT('',(-9.9,-1.11034E+03,229.64)); +#3595 = VECTOR('',#3596,1.); +#3596 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#3597 = PLANE('',#3598); +#3598 = AXIS2_PLACEMENT_3D('',#3599,#3600,#3601); +#3599 = CARTESIAN_POINT('',(-9.9,-1.11034E+03,229.64)); +#3600 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#3601 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#3602 = ADVANCED_FACE('',(#3603),#3671,.F.); +#3603 = FACE_BOUND('',#3604,.F.); +#3604 = EDGE_LOOP('',(#3605,#3613,#3614,#3615,#3623,#3631,#3640,#3648, + #3657,#3665)); +#3605 = ORIENTED_EDGE('',*,*,#3606,.F.); +#3606 = EDGE_CURVE('',#3530,#3607,#3609,.T.); +#3607 = VERTEX_POINT('',#3608); +#3608 = CARTESIAN_POINT('',(-39.9,-1.16334E+03,134.64)); +#3609 = LINE('',#3610,#3611); +#3610 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3611 = VECTOR('',#3612,1.); +#3612 = DIRECTION('',(1.,0.,0.)); +#3613 = ORIENTED_EDGE('',*,*,#3537,.T.); +#3614 = ORIENTED_EDGE('',*,*,#3473,.T.); +#3615 = ORIENTED_EDGE('',*,*,#3616,.T.); +#3616 = EDGE_CURVE('',#3474,#3617,#3619,.T.); +#3617 = VERTEX_POINT('',#3618); +#3618 = CARTESIAN_POINT('',(-13.9,-1.100359419922E+03,202.206544647)); +#3619 = LINE('',#3620,#3621); +#3620 = CARTESIAN_POINT('',(-13.9,-1.128827677663E+03,171.66535551682)); +#3621 = VECTOR('',#3622,1.); +#3622 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#3623 = ORIENTED_EDGE('',*,*,#3624,.T.); +#3624 = EDGE_CURVE('',#3617,#3625,#3627,.T.); +#3625 = VERTEX_POINT('',#3626); +#3626 = CARTESIAN_POINT('',(-44.9,-1.100359419922E+03,202.206544647)); +#3627 = LINE('',#3628,#3629); +#3628 = CARTESIAN_POINT('',(-44.9,-1.100359419922E+03,202.206544647)); +#3629 = VECTOR('',#3630,1.); +#3630 = DIRECTION('',(-1.,0.,0.)); +#3631 = ORIENTED_EDGE('',*,*,#3632,.T.); +#3632 = EDGE_CURVE('',#3625,#3633,#3635,.T.); +#3633 = VERTEX_POINT('',#3634); +#3634 = CARTESIAN_POINT('',(-46.4,-1.10154E+03,200.94)); +#3635 = ELLIPSE('',#3636,1.731445830491,1.5); +#3636 = AXIS2_PLACEMENT_3D('',#3637,#3638,#3639); +#3637 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3638 = DIRECTION('',(-7.337186423333E-33,-0.731495392293,0.681846383766 + )); +#3639 = DIRECTION('',(-1.1E-16,0.681846383766,0.731495392293)); +#3640 = ORIENTED_EDGE('',*,*,#3641,.F.); +#3641 = EDGE_CURVE('',#3642,#3633,#3644,.T.); +#3642 = VERTEX_POINT('',#3643); +#3643 = CARTESIAN_POINT('',(-46.4,-1.16334E+03,134.64)); +#3644 = LINE('',#3645,#3646); +#3645 = CARTESIAN_POINT('',(-46.4,-1.16334E+03,134.64)); +#3646 = VECTOR('',#3647,1.); +#3647 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#3648 = ORIENTED_EDGE('',*,*,#3649,.T.); +#3649 = EDGE_CURVE('',#3642,#3650,#3652,.T.); +#3650 = VERTEX_POINT('',#3651); +#3651 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#3652 = ELLIPSE('',#3653,1.743718619647,1.5); +#3653 = AXIS2_PLACEMENT_3D('',#3654,#3655,#3656); +#3654 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3655 = DIRECTION('',(-1.392343868323E-32,-0.731495392293,0.681846383766 + )); +#3656 = DIRECTION('',(9.8E-16,-0.681846383766,-0.731495392293)); +#3657 = ORIENTED_EDGE('',*,*,#3658,.T.); +#3658 = EDGE_CURVE('',#3650,#3659,#3661,.T.); +#3659 = VERTEX_POINT('',#3660); +#3660 = CARTESIAN_POINT('',(-39.9,-1.164528948235E+03,133.36447786427)); +#3661 = LINE('',#3662,#3663); +#3662 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#3663 = VECTOR('',#3664,1.); +#3664 = DIRECTION('',(1.,0.,0.)); +#3665 = ORIENTED_EDGE('',*,*,#3666,.F.); +#3666 = EDGE_CURVE('',#3607,#3659,#3667,.T.); +#3667 = LINE('',#3668,#3669); +#3668 = CARTESIAN_POINT('',(-39.9,-1.163919588191E+03,134.01820878496)); +#3669 = VECTOR('',#3670,1.); +#3670 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#3671 = PLANE('',#3672); +#3672 = AXIS2_PLACEMENT_3D('',#3673,#3674,#3675); +#3673 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3674 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#3675 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#3676 = ADVANCED_FACE('',(#3677),#3727,.T.); +#3677 = FACE_BOUND('',#3678,.T.); +#3678 = EDGE_LOOP('',(#3679,#3680,#3688,#3696,#3704,#3712,#3720,#3726)); +#3679 = ORIENTED_EDGE('',*,*,#3561,.T.); +#3680 = ORIENTED_EDGE('',*,*,#3681,.F.); +#3681 = EDGE_CURVE('',#3682,#3554,#3684,.T.); +#3682 = VERTEX_POINT('',#3683); +#3683 = CARTESIAN_POINT('',(-9.9,-1.16184E+03,234.74)); +#3684 = LINE('',#3685,#3686); +#3685 = CARTESIAN_POINT('',(-9.9,-1.16184E+03,234.74)); +#3686 = VECTOR('',#3687,1.); +#3687 = DIRECTION('',(-1.,-0.,-0.)); +#3688 = ORIENTED_EDGE('',*,*,#3689,.T.); +#3689 = EDGE_CURVE('',#3682,#3690,#3692,.T.); +#3690 = VERTEX_POINT('',#3691); +#3691 = CARTESIAN_POINT('',(20.1,-1.16184E+03,234.74)); +#3692 = LINE('',#3693,#3694); +#3693 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3694 = VECTOR('',#3695,1.); +#3695 = DIRECTION('',(1.,0.,0.)); +#3696 = ORIENTED_EDGE('',*,*,#3697,.T.); +#3697 = EDGE_CURVE('',#3690,#3698,#3700,.T.); +#3698 = VERTEX_POINT('',#3699); +#3699 = CARTESIAN_POINT('',(24.1,-1.16184E+03,234.74)); +#3700 = LINE('',#3701,#3702); +#3701 = CARTESIAN_POINT('',(20.1,-1.16184E+03,234.74)); +#3702 = VECTOR('',#3703,1.); +#3703 = DIRECTION('',(1.,0.,0.)); +#3704 = ORIENTED_EDGE('',*,*,#3705,.T.); +#3705 = EDGE_CURVE('',#3698,#3706,#3708,.T.); +#3706 = VERTEX_POINT('',#3707); +#3707 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#3708 = LINE('',#3709,#3710); +#3709 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3710 = VECTOR('',#3711,1.); +#3711 = DIRECTION('',(1.,0.,0.)); +#3712 = ORIENTED_EDGE('',*,*,#3713,.T.); +#3713 = EDGE_CURVE('',#3706,#3714,#3716,.T.); +#3714 = VERTEX_POINT('',#3715); +#3715 = CARTESIAN_POINT('',(55.1,-1.181180495325E+03,225.94254351617)); +#3716 = LINE('',#3717,#3718); +#3717 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#3718 = VECTOR('',#3719,1.); +#3719 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#3720 = ORIENTED_EDGE('',*,*,#3721,.F.); +#3721 = EDGE_CURVE('',#3513,#3714,#3722,.T.); +#3722 = LINE('',#3723,#3724); +#3723 = CARTESIAN_POINT('',(-44.9,-1.181180495325E+03,225.94254351617)); +#3724 = VECTOR('',#3725,1.); +#3725 = DIRECTION('',(1.,0.,0.)); +#3726 = ORIENTED_EDGE('',*,*,#3512,.F.); +#3727 = PLANE('',#3728); +#3728 = AXIS2_PLACEMENT_3D('',#3729,#3730,#3731); +#3729 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#3730 = DIRECTION('',(0.,0.414049532497,-0.910254351618)); +#3731 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#3732 = ADVANCED_FACE('',(#3733),#3752,.F.); +#3733 = FACE_BOUND('',#3734,.T.); +#3734 = EDGE_LOOP('',(#3735,#3736,#3745,#3751)); +#3735 = ORIENTED_EDGE('',*,*,#3721,.T.); +#3736 = ORIENTED_EDGE('',*,*,#3737,.T.); +#3737 = EDGE_CURVE('',#3714,#3738,#3740,.T.); +#3738 = VERTEX_POINT('',#3739); +#3739 = CARTESIAN_POINT('',(55.1,-1.186635384657E+03,214.02422421171)); +#3740 = CIRCLE('',#3741,10.); +#3741 = AXIS2_PLACEMENT_3D('',#3742,#3743,#3744); +#3742 = CARTESIAN_POINT('',(55.1,-1.17704E+03,216.84)); +#3743 = DIRECTION('',(1.,0.,0.)); +#3744 = DIRECTION('',(0.,1.,0.)); +#3745 = ORIENTED_EDGE('',*,*,#3746,.F.); +#3746 = EDGE_CURVE('',#3521,#3738,#3747,.T.); +#3747 = LINE('',#3748,#3749); +#3748 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#3749 = VECTOR('',#3750,1.); +#3750 = DIRECTION('',(1.,0.,0.)); +#3751 = ORIENTED_EDGE('',*,*,#3520,.F.); +#3752 = CYLINDRICAL_SURFACE('',#3753,10.); +#3753 = AXIS2_PLACEMENT_3D('',#3754,#3755,#3756); +#3754 = CARTESIAN_POINT('',(-44.9,-1.17704E+03,216.84)); +#3755 = DIRECTION('',(-1.,-0.,-0.)); +#3756 = DIRECTION('',(0.,1.,0.)); +#3757 = ADVANCED_FACE('',(#3758),#3849,.T.); +#3758 = FACE_BOUND('',#3759,.T.); +#3759 = EDGE_LOOP('',(#3760,#3761,#3769,#3777,#3785,#3793,#3801,#3809, + #3817,#3825,#3833,#3841,#3847,#3848)); +#3760 = ORIENTED_EDGE('',*,*,#3746,.T.); +#3761 = ORIENTED_EDGE('',*,*,#3762,.T.); +#3762 = EDGE_CURVE('',#3738,#3763,#3765,.T.); +#3763 = VERTEX_POINT('',#3764); +#3764 = CARTESIAN_POINT('',(55.1,-1.16334E+03,134.64)); +#3765 = LINE('',#3766,#3767); +#3766 = CARTESIAN_POINT('',(55.1,-1.186635384657E+03,214.02422421171)); +#3767 = VECTOR('',#3768,1.); +#3768 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#3769 = ORIENTED_EDGE('',*,*,#3770,.F.); +#3770 = EDGE_CURVE('',#3771,#3763,#3773,.T.); +#3771 = VERTEX_POINT('',#3772); +#3772 = CARTESIAN_POINT('',(50.1,-1.16334E+03,134.64)); +#3773 = LINE('',#3774,#3775); +#3774 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3775 = VECTOR('',#3776,1.); +#3776 = DIRECTION('',(1.,0.,0.)); +#3777 = ORIENTED_EDGE('',*,*,#3778,.F.); +#3778 = EDGE_CURVE('',#3779,#3771,#3781,.T.); +#3779 = VERTEX_POINT('',#3780); +#3780 = CARTESIAN_POINT('',(40.1,-1.16334E+03,134.64)); +#3781 = LINE('',#3782,#3783); +#3782 = CARTESIAN_POINT('',(40.1,-1.16334E+03,134.64)); +#3783 = VECTOR('',#3784,1.); +#3784 = DIRECTION('',(1.,0.,0.)); +#3785 = ORIENTED_EDGE('',*,*,#3786,.F.); +#3786 = EDGE_CURVE('',#3787,#3779,#3789,.T.); +#3787 = VERTEX_POINT('',#3788); +#3788 = CARTESIAN_POINT('',(30.1,-1.16334E+03,134.64)); +#3789 = LINE('',#3790,#3791); +#3790 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3791 = VECTOR('',#3792,1.); +#3792 = DIRECTION('',(1.,0.,0.)); +#3793 = ORIENTED_EDGE('',*,*,#3794,.F.); +#3794 = EDGE_CURVE('',#3795,#3787,#3797,.T.); +#3795 = VERTEX_POINT('',#3796); +#3796 = CARTESIAN_POINT('',(20.1,-1.16334E+03,134.64)); +#3797 = LINE('',#3798,#3799); +#3798 = CARTESIAN_POINT('',(20.1,-1.16334E+03,134.64)); +#3799 = VECTOR('',#3800,1.); +#3800 = DIRECTION('',(1.,0.,0.)); +#3801 = ORIENTED_EDGE('',*,*,#3802,.F.); +#3802 = EDGE_CURVE('',#3803,#3795,#3805,.T.); +#3803 = VERTEX_POINT('',#3804); +#3804 = CARTESIAN_POINT('',(10.1,-1.16334E+03,134.64)); +#3805 = LINE('',#3806,#3807); +#3806 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3807 = VECTOR('',#3808,1.); +#3808 = DIRECTION('',(1.,0.,0.)); +#3809 = ORIENTED_EDGE('',*,*,#3810,.F.); +#3810 = EDGE_CURVE('',#3811,#3803,#3813,.T.); +#3811 = VERTEX_POINT('',#3812); +#3812 = CARTESIAN_POINT('',(0.1,-1.16334E+03,134.64)); +#3813 = LINE('',#3814,#3815); +#3814 = CARTESIAN_POINT('',(0.1,-1.16334E+03,134.64)); +#3815 = VECTOR('',#3816,1.); +#3816 = DIRECTION('',(1.,0.,0.)); +#3817 = ORIENTED_EDGE('',*,*,#3818,.F.); +#3818 = EDGE_CURVE('',#3819,#3811,#3821,.T.); +#3819 = VERTEX_POINT('',#3820); +#3820 = CARTESIAN_POINT('',(-9.9,-1.16334E+03,134.64)); +#3821 = LINE('',#3822,#3823); +#3822 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3823 = VECTOR('',#3824,1.); +#3824 = DIRECTION('',(1.,0.,0.)); +#3825 = ORIENTED_EDGE('',*,*,#3826,.F.); +#3826 = EDGE_CURVE('',#3827,#3819,#3829,.T.); +#3827 = VERTEX_POINT('',#3828); +#3828 = CARTESIAN_POINT('',(-19.9,-1.16334E+03,134.64)); +#3829 = LINE('',#3830,#3831); +#3830 = CARTESIAN_POINT('',(-19.9,-1.16334E+03,134.64)); +#3831 = VECTOR('',#3832,1.); +#3832 = DIRECTION('',(1.,0.,0.)); +#3833 = ORIENTED_EDGE('',*,*,#3834,.F.); +#3834 = EDGE_CURVE('',#3835,#3827,#3837,.T.); +#3835 = VERTEX_POINT('',#3836); +#3836 = CARTESIAN_POINT('',(-29.9,-1.16334E+03,134.64)); +#3837 = LINE('',#3838,#3839); +#3838 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#3839 = VECTOR('',#3840,1.); +#3840 = DIRECTION('',(1.,0.,0.)); +#3841 = ORIENTED_EDGE('',*,*,#3842,.F.); +#3842 = EDGE_CURVE('',#3607,#3835,#3843,.T.); +#3843 = LINE('',#3844,#3845); +#3844 = CARTESIAN_POINT('',(-39.9,-1.16334E+03,134.64)); +#3845 = VECTOR('',#3846,1.); +#3846 = DIRECTION('',(1.,0.,0.)); +#3847 = ORIENTED_EDGE('',*,*,#3606,.F.); +#3848 = ORIENTED_EDGE('',*,*,#3529,.F.); +#3849 = PLANE('',#3850); +#3850 = AXIS2_PLACEMENT_3D('',#3851,#3852,#3853); +#3851 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#3852 = DIRECTION('',(0.,0.95953846567,0.281577578828)); +#3853 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#3854 = ADVANCED_FACE('',(#3855),#3866,.T.); +#3855 = FACE_BOUND('',#3856,.T.); +#3856 = EDGE_LOOP('',(#3857,#3858,#3859,#3860)); +#3857 = ORIENTED_EDGE('',*,*,#3681,.T.); +#3858 = ORIENTED_EDGE('',*,*,#3553,.T.); +#3859 = ORIENTED_EDGE('',*,*,#3575,.F.); +#3860 = ORIENTED_EDGE('',*,*,#3861,.F.); +#3861 = EDGE_CURVE('',#3682,#3576,#3862,.T.); +#3862 = LINE('',#3863,#3864); +#3863 = CARTESIAN_POINT('',(-9.9,-1.16184E+03,234.74)); +#3864 = VECTOR('',#3865,1.); +#3865 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#3866 = PLANE('',#3867); +#3867 = AXIS2_PLACEMENT_3D('',#3868,#3869,#3870); +#3868 = CARTESIAN_POINT('',(-9.9,-1.16184E+03,234.74)); +#3869 = DIRECTION('',(0.,-9.85470909115E-02,-0.995132388616)); +#3870 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#3871 = ADVANCED_FACE('',(#3872),#3897,.T.); +#3872 = FACE_BOUND('',#3873,.T.); +#3873 = EDGE_LOOP('',(#3874,#3875,#3883,#3891)); +#3874 = ORIENTED_EDGE('',*,*,#3584,.T.); +#3875 = ORIENTED_EDGE('',*,*,#3876,.T.); +#3876 = EDGE_CURVE('',#3474,#3877,#3879,.T.); +#3877 = VERTEX_POINT('',#3878); +#3878 = CARTESIAN_POINT('',(-13.9,-1.092601363636E+03,194.76822716242)); +#3879 = LINE('',#3880,#3881); +#3880 = CARTESIAN_POINT('',(-13.9,-1.10154E+03,200.94)); +#3881 = VECTOR('',#3882,1.); +#3882 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#3883 = ORIENTED_EDGE('',*,*,#3884,.F.); +#3884 = EDGE_CURVE('',#3885,#3877,#3887,.T.); +#3885 = VERTEX_POINT('',#3886); +#3886 = CARTESIAN_POINT('',(-9.9,-1.092601363636E+03,194.76822716242)); +#3887 = LINE('',#3888,#3889); +#3888 = CARTESIAN_POINT('',(-9.9,-1.092601363636E+03,194.76822716242)); +#3889 = VECTOR('',#3890,1.); +#3890 = DIRECTION('',(-1.,-0.,-0.)); +#3891 = ORIENTED_EDGE('',*,*,#3892,.F.); +#3892 = EDGE_CURVE('',#3585,#3885,#3893,.T.); +#3893 = LINE('',#3894,#3895); +#3894 = CARTESIAN_POINT('',(-9.9,-1.10154E+03,200.94)); +#3895 = VECTOR('',#3896,1.); +#3896 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#3897 = PLANE('',#3898); +#3898 = AXIS2_PLACEMENT_3D('',#3899,#3900,#3901); +#3899 = CARTESIAN_POINT('',(-9.9,-1.10154E+03,200.94)); +#3900 = DIRECTION('',(0.,-0.568181818182,-0.822903045011)); +#3901 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#3902 = ADVANCED_FACE('',(#3903),#3928,.T.); +#3903 = FACE_BOUND('',#3904,.T.); +#3904 = EDGE_LOOP('',(#3905,#3906,#3914,#3922)); +#3905 = ORIENTED_EDGE('',*,*,#3592,.T.); +#3906 = ORIENTED_EDGE('',*,*,#3907,.T.); +#3907 = EDGE_CURVE('',#3585,#3908,#3910,.T.); +#3908 = VERTEX_POINT('',#3909); +#3909 = CARTESIAN_POINT('',(20.1,-1.10154E+03,200.94)); +#3910 = LINE('',#3911,#3912); +#3911 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3912 = VECTOR('',#3913,1.); +#3913 = DIRECTION('',(1.,0.,0.)); +#3914 = ORIENTED_EDGE('',*,*,#3915,.F.); +#3915 = EDGE_CURVE('',#3916,#3908,#3918,.T.); +#3916 = VERTEX_POINT('',#3917); +#3917 = CARTESIAN_POINT('',(20.1,-1.11034E+03,229.64)); +#3918 = LINE('',#3919,#3920); +#3919 = CARTESIAN_POINT('',(20.1,-1.11034E+03,229.64)); +#3920 = VECTOR('',#3921,1.); +#3921 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#3922 = ORIENTED_EDGE('',*,*,#3923,.F.); +#3923 = EDGE_CURVE('',#3576,#3916,#3924,.T.); +#3924 = LINE('',#3925,#3926); +#3925 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#3926 = VECTOR('',#3927,1.); +#3927 = DIRECTION('',(1.,0.,0.)); +#3928 = PLANE('',#3929); +#3929 = AXIS2_PLACEMENT_3D('',#3930,#3931,#3932); +#3930 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#3931 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#3932 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#3933 = ADVANCED_FACE('',(#3934),#3975,.T.); +#3934 = FACE_BOUND('',#3935,.T.); +#3935 = EDGE_LOOP('',(#3936,#3937,#3945,#3953,#3961,#3969)); +#3936 = ORIENTED_EDGE('',*,*,#3666,.F.); +#3937 = ORIENTED_EDGE('',*,*,#3938,.T.); +#3938 = EDGE_CURVE('',#3607,#3939,#3941,.T.); +#3939 = VERTEX_POINT('',#3940); +#3940 = CARTESIAN_POINT('',(-39.9,-1.160255084405E+03,137.94954479362)); +#3941 = LINE('',#3942,#3943); +#3942 = CARTESIAN_POINT('',(-39.9,-1.16334E+03,134.64)); +#3943 = VECTOR('',#3944,1.); +#3944 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#3945 = ORIENTED_EDGE('',*,*,#3946,.T.); +#3946 = EDGE_CURVE('',#3939,#3947,#3949,.T.); +#3947 = VERTEX_POINT('',#3948); +#3948 = CARTESIAN_POINT('',(-39.9,-1.163662554812E+03,123.86376214781)); +#3949 = LINE('',#3950,#3951); +#3950 = CARTESIAN_POINT('',(-39.9,-1.160255084405E+03,137.94954479362)); +#3951 = VECTOR('',#3952,1.); +#3952 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#3953 = ORIENTED_EDGE('',*,*,#3954,.T.); +#3954 = EDGE_CURVE('',#3947,#3955,#3957,.T.); +#3955 = VERTEX_POINT('',#3956); +#3956 = CARTESIAN_POINT('',(-39.9,-1.167855516655E+03,138.15219926067)); +#3957 = LINE('',#3958,#3959); +#3958 = CARTESIAN_POINT('',(-39.9,-1.163662554812E+03,123.86376214781)); +#3959 = VECTOR('',#3960,1.); +#3960 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#3961 = ORIENTED_EDGE('',*,*,#3962,.T.); +#3962 = EDGE_CURVE('',#3955,#3963,#3965,.T.); +#3963 = VERTEX_POINT('',#3964); +#3964 = CARTESIAN_POINT('',(-39.9,-1.166086266675E+03,138.67138814011)); +#3965 = LINE('',#3966,#3967); +#3966 = CARTESIAN_POINT('',(-39.9,-1.167855516655E+03,138.15219926067)); +#3967 = VECTOR('',#3968,1.); +#3968 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#3969 = ORIENTED_EDGE('',*,*,#3970,.F.); +#3970 = EDGE_CURVE('',#3659,#3963,#3971,.T.); +#3971 = LINE('',#3972,#3973); +#3972 = CARTESIAN_POINT('',(-39.9,-1.176199170909E+03,173.13336715207)); +#3973 = VECTOR('',#3974,1.); +#3974 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#3975 = PLANE('',#3976); +#3976 = AXIS2_PLACEMENT_3D('',#3977,#3978,#3979); +#3977 = CARTESIAN_POINT('',(-39.9,-1.163860253502E+03,132.80086049584)); +#3978 = DIRECTION('',(-1.,-0.,-0.)); +#3979 = DIRECTION('',(0.,-1.,0.)); +#3980 = ADVANCED_FACE('',(#3981),#4159,.F.); +#3981 = FACE_BOUND('',#3982,.F.); +#3982 = EDGE_LOOP('',(#3983,#3991,#3999,#4007,#4015,#4023,#4031,#4039, + #4047,#4055,#4063,#4071,#4079,#4087,#4095,#4103,#4111,#4119,#4127, + #4135,#4143,#4151,#4157,#4158)); +#3983 = ORIENTED_EDGE('',*,*,#3984,.F.); +#3984 = EDGE_CURVE('',#3985,#3650,#3987,.T.); +#3985 = VERTEX_POINT('',#3986); +#3986 = CARTESIAN_POINT('',(-44.9,-1.188074692355E+03,213.60185784347)); +#3987 = LINE('',#3988,#3989); +#3988 = CARTESIAN_POINT('',(-44.9,-1.188074692355E+03,213.60185784347)); +#3989 = VECTOR('',#3990,1.); +#3990 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#3991 = ORIENTED_EDGE('',*,*,#3992,.T.); +#3992 = EDGE_CURVE('',#3985,#3993,#3995,.T.); +#3993 = VERTEX_POINT('',#3994); +#3994 = CARTESIAN_POINT('',(55.1,-1.188074692355E+03,213.60185784347)); +#3995 = LINE('',#3996,#3997); +#3996 = CARTESIAN_POINT('',(-44.9,-1.188074692355E+03,213.60185784347)); +#3997 = VECTOR('',#3998,1.); +#3998 = DIRECTION('',(1.,0.,0.)); +#3999 = ORIENTED_EDGE('',*,*,#4000,.T.); +#4000 = EDGE_CURVE('',#3993,#4001,#4003,.T.); +#4001 = VERTEX_POINT('',#4002); +#4002 = CARTESIAN_POINT('',(55.1,-1.164528948235E+03,133.36447786427)); +#4003 = LINE('',#4004,#4005); +#4004 = CARTESIAN_POINT('',(55.1,-1.188074692355E+03,213.60185784347)); +#4005 = VECTOR('',#4006,1.); +#4006 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#4007 = ORIENTED_EDGE('',*,*,#4008,.F.); +#4008 = EDGE_CURVE('',#4009,#4001,#4011,.T.); +#4009 = VERTEX_POINT('',#4010); +#4010 = CARTESIAN_POINT('',(50.1,-1.164528948235E+03,133.36447786427)); +#4011 = LINE('',#4012,#4013); +#4012 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#4013 = VECTOR('',#4014,1.); +#4014 = DIRECTION('',(1.,0.,0.)); +#4015 = ORIENTED_EDGE('',*,*,#4016,.T.); +#4016 = EDGE_CURVE('',#4009,#4017,#4019,.T.); +#4017 = VERTEX_POINT('',#4018); +#4018 = CARTESIAN_POINT('',(50.1,-1.166086266675E+03,138.67138814011)); +#4019 = LINE('',#4020,#4021); +#4020 = CARTESIAN_POINT('',(50.1,-1.176199170909E+03,173.13336715207)); +#4021 = VECTOR('',#4022,1.); +#4022 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4023 = ORIENTED_EDGE('',*,*,#4024,.F.); +#4024 = EDGE_CURVE('',#4025,#4017,#4027,.T.); +#4025 = VERTEX_POINT('',#4026); +#4026 = CARTESIAN_POINT('',(40.1,-1.166086266675E+03,138.67138814011)); +#4027 = LINE('',#4028,#4029); +#4028 = CARTESIAN_POINT('',(-2.4,-1.166086266675E+03,138.67138814011)); +#4029 = VECTOR('',#4030,1.); +#4030 = DIRECTION('',(1.,0.,0.)); +#4031 = ORIENTED_EDGE('',*,*,#4032,.F.); +#4032 = EDGE_CURVE('',#4033,#4025,#4035,.T.); +#4033 = VERTEX_POINT('',#4034); +#4034 = CARTESIAN_POINT('',(40.1,-1.164528948235E+03,133.36447786427)); +#4035 = LINE('',#4036,#4037); +#4036 = CARTESIAN_POINT('',(40.1,-1.176199170909E+03,173.13336715207)); +#4037 = VECTOR('',#4038,1.); +#4038 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4039 = ORIENTED_EDGE('',*,*,#4040,.F.); +#4040 = EDGE_CURVE('',#4041,#4033,#4043,.T.); +#4041 = VERTEX_POINT('',#4042); +#4042 = CARTESIAN_POINT('',(30.1,-1.164528948235E+03,133.36447786427)); +#4043 = LINE('',#4044,#4045); +#4044 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#4045 = VECTOR('',#4046,1.); +#4046 = DIRECTION('',(1.,0.,0.)); +#4047 = ORIENTED_EDGE('',*,*,#4048,.T.); +#4048 = EDGE_CURVE('',#4041,#4049,#4051,.T.); +#4049 = VERTEX_POINT('',#4050); +#4050 = CARTESIAN_POINT('',(30.1,-1.166086266675E+03,138.67138814011)); +#4051 = LINE('',#4052,#4053); +#4052 = CARTESIAN_POINT('',(30.1,-1.176199170909E+03,173.13336715207)); +#4053 = VECTOR('',#4054,1.); +#4054 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4055 = ORIENTED_EDGE('',*,*,#4056,.F.); +#4056 = EDGE_CURVE('',#4057,#4049,#4059,.T.); +#4057 = VERTEX_POINT('',#4058); +#4058 = CARTESIAN_POINT('',(20.1,-1.166086266675E+03,138.67138814011)); +#4059 = LINE('',#4060,#4061); +#4060 = CARTESIAN_POINT('',(-12.4,-1.166086266675E+03,138.67138814011)); +#4061 = VECTOR('',#4062,1.); +#4062 = DIRECTION('',(1.,0.,0.)); +#4063 = ORIENTED_EDGE('',*,*,#4064,.F.); +#4064 = EDGE_CURVE('',#4065,#4057,#4067,.T.); +#4065 = VERTEX_POINT('',#4066); +#4066 = CARTESIAN_POINT('',(20.1,-1.164528948235E+03,133.36447786427)); +#4067 = LINE('',#4068,#4069); +#4068 = CARTESIAN_POINT('',(20.1,-1.176199170909E+03,173.13336715207)); +#4069 = VECTOR('',#4070,1.); +#4070 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4071 = ORIENTED_EDGE('',*,*,#4072,.F.); +#4072 = EDGE_CURVE('',#4073,#4065,#4075,.T.); +#4073 = VERTEX_POINT('',#4074); +#4074 = CARTESIAN_POINT('',(10.1,-1.164528948235E+03,133.36447786427)); +#4075 = LINE('',#4076,#4077); +#4076 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#4077 = VECTOR('',#4078,1.); +#4078 = DIRECTION('',(1.,0.,0.)); +#4079 = ORIENTED_EDGE('',*,*,#4080,.T.); +#4080 = EDGE_CURVE('',#4073,#4081,#4083,.T.); +#4081 = VERTEX_POINT('',#4082); +#4082 = CARTESIAN_POINT('',(10.1,-1.166086266675E+03,138.67138814011)); +#4083 = LINE('',#4084,#4085); +#4084 = CARTESIAN_POINT('',(10.1,-1.176199170909E+03,173.13336715207)); +#4085 = VECTOR('',#4086,1.); +#4086 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4087 = ORIENTED_EDGE('',*,*,#4088,.F.); +#4088 = EDGE_CURVE('',#4089,#4081,#4091,.T.); +#4089 = VERTEX_POINT('',#4090); +#4090 = CARTESIAN_POINT('',(0.1,-1.166086266675E+03,138.67138814011)); +#4091 = LINE('',#4092,#4093); +#4092 = CARTESIAN_POINT('',(-22.4,-1.166086266675E+03,138.67138814011)); +#4093 = VECTOR('',#4094,1.); +#4094 = DIRECTION('',(1.,0.,0.)); +#4095 = ORIENTED_EDGE('',*,*,#4096,.F.); +#4096 = EDGE_CURVE('',#4097,#4089,#4099,.T.); +#4097 = VERTEX_POINT('',#4098); +#4098 = CARTESIAN_POINT('',(0.1,-1.164528948235E+03,133.36447786427)); +#4099 = LINE('',#4100,#4101); +#4100 = CARTESIAN_POINT('',(0.1,-1.176199170909E+03,173.13336715207)); +#4101 = VECTOR('',#4102,1.); +#4102 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4103 = ORIENTED_EDGE('',*,*,#4104,.F.); +#4104 = EDGE_CURVE('',#4105,#4097,#4107,.T.); +#4105 = VERTEX_POINT('',#4106); +#4106 = CARTESIAN_POINT('',(-9.9,-1.164528948235E+03,133.36447786427)); +#4107 = LINE('',#4108,#4109); +#4108 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#4109 = VECTOR('',#4110,1.); +#4110 = DIRECTION('',(1.,0.,0.)); +#4111 = ORIENTED_EDGE('',*,*,#4112,.T.); +#4112 = EDGE_CURVE('',#4105,#4113,#4115,.T.); +#4113 = VERTEX_POINT('',#4114); +#4114 = CARTESIAN_POINT('',(-9.9,-1.166086266675E+03,138.67138814011)); +#4115 = LINE('',#4116,#4117); +#4116 = CARTESIAN_POINT('',(-9.9,-1.176199170909E+03,173.13336715207)); +#4117 = VECTOR('',#4118,1.); +#4118 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4119 = ORIENTED_EDGE('',*,*,#4120,.F.); +#4120 = EDGE_CURVE('',#4121,#4113,#4123,.T.); +#4121 = VERTEX_POINT('',#4122); +#4122 = CARTESIAN_POINT('',(-19.9,-1.166086266675E+03,138.67138814011)); +#4123 = LINE('',#4124,#4125); +#4124 = CARTESIAN_POINT('',(-32.4,-1.166086266675E+03,138.67138814011)); +#4125 = VECTOR('',#4126,1.); +#4126 = DIRECTION('',(1.,0.,0.)); +#4127 = ORIENTED_EDGE('',*,*,#4128,.F.); +#4128 = EDGE_CURVE('',#4129,#4121,#4131,.T.); +#4129 = VERTEX_POINT('',#4130); +#4130 = CARTESIAN_POINT('',(-19.9,-1.164528948235E+03,133.36447786427)); +#4131 = LINE('',#4132,#4133); +#4132 = CARTESIAN_POINT('',(-19.9,-1.176199170909E+03,173.13336715207)); +#4133 = VECTOR('',#4134,1.); +#4134 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4135 = ORIENTED_EDGE('',*,*,#4136,.F.); +#4136 = EDGE_CURVE('',#4137,#4129,#4139,.T.); +#4137 = VERTEX_POINT('',#4138); +#4138 = CARTESIAN_POINT('',(-29.9,-1.164528948235E+03,133.36447786427)); +#4139 = LINE('',#4140,#4141); +#4140 = CARTESIAN_POINT('',(-44.9,-1.164528948235E+03,133.36447786427)); +#4141 = VECTOR('',#4142,1.); +#4142 = DIRECTION('',(1.,0.,0.)); +#4143 = ORIENTED_EDGE('',*,*,#4144,.T.); +#4144 = EDGE_CURVE('',#4137,#4145,#4147,.T.); +#4145 = VERTEX_POINT('',#4146); +#4146 = CARTESIAN_POINT('',(-29.9,-1.166086266675E+03,138.67138814011)); +#4147 = LINE('',#4148,#4149); +#4148 = CARTESIAN_POINT('',(-29.9,-1.176199170909E+03,173.13336715207)); +#4149 = VECTOR('',#4150,1.); +#4150 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#4151 = ORIENTED_EDGE('',*,*,#4152,.F.); +#4152 = EDGE_CURVE('',#3963,#4145,#4153,.T.); +#4153 = LINE('',#4154,#4155); +#4154 = CARTESIAN_POINT('',(-42.4,-1.166086266675E+03,138.67138814011)); +#4155 = VECTOR('',#4156,1.); +#4156 = DIRECTION('',(1.,0.,0.)); +#4157 = ORIENTED_EDGE('',*,*,#3970,.F.); +#4158 = ORIENTED_EDGE('',*,*,#3658,.F.); +#4159 = PLANE('',#4160); +#4160 = AXIS2_PLACEMENT_3D('',#4161,#4162,#4163); +#4161 = CARTESIAN_POINT('',(-44.9,-1.188074692355E+03,213.60185784347)); +#4162 = DIRECTION('',(0.,0.95953846567,0.281577578828)); +#4163 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#4164 = ADVANCED_FACE('',(#4165),#4184,.T.); +#4165 = FACE_BOUND('',#4166,.T.); +#4166 = EDGE_LOOP('',(#4167,#4175,#4176,#4177)); +#4167 = ORIENTED_EDGE('',*,*,#4168,.T.); +#4168 = EDGE_CURVE('',#4169,#3642,#4171,.T.); +#4169 = VERTEX_POINT('',#4170); +#4170 = CARTESIAN_POINT('',(-46.4,-1.186635384657E+03,214.02422421171)); +#4171 = LINE('',#4172,#4173); +#4172 = CARTESIAN_POINT('',(-46.4,-1.186635384657E+03,214.02422421171)); +#4173 = VECTOR('',#4174,1.); +#4174 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#4175 = ORIENTED_EDGE('',*,*,#3649,.T.); +#4176 = ORIENTED_EDGE('',*,*,#3984,.F.); +#4177 = ORIENTED_EDGE('',*,*,#4178,.T.); +#4178 = EDGE_CURVE('',#3985,#4169,#4179,.T.); +#4179 = CIRCLE('',#4180,1.5); +#4180 = AXIS2_PLACEMENT_3D('',#4181,#4182,#4183); +#4181 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#4182 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#4183 = DIRECTION('',(0.,-0.95953846567,-0.281577578828)); +#4184 = CYLINDRICAL_SURFACE('',#4185,1.5); +#4185 = AXIS2_PLACEMENT_3D('',#4186,#4187,#4188); +#4186 = CARTESIAN_POINT('',(-44.9,-1.186635384657E+03,214.02422421171)); +#4187 = DIRECTION('',(5.E-16,0.281577578828,-0.95953846567)); +#4188 = DIRECTION('',(1.,-1.79530598053E-16,4.684004080656E-16)); +#4189 = ADVANCED_FACE('',(#4190),#4225,.F.); +#4190 = FACE_BOUND('',#4191,.F.); +#4191 = EDGE_LOOP('',(#4192,#4202,#4210,#4217,#4218,#4219)); +#4192 = ORIENTED_EDGE('',*,*,#4193,.T.); +#4193 = EDGE_CURVE('',#4194,#4196,#4198,.T.); +#4194 = VERTEX_POINT('',#4195); +#4195 = CARTESIAN_POINT('',(-46.4,-1.11034E+03,229.64)); +#4196 = VERTEX_POINT('',#4197); +#4197 = CARTESIAN_POINT('',(-46.4,-1.16184E+03,234.74)); +#4198 = LINE('',#4199,#4200); +#4199 = CARTESIAN_POINT('',(-46.4,-1.11034E+03,229.64)); +#4200 = VECTOR('',#4201,1.); +#4201 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4202 = ORIENTED_EDGE('',*,*,#4203,.T.); +#4203 = EDGE_CURVE('',#4196,#4204,#4206,.T.); +#4204 = VERTEX_POINT('',#4205); +#4205 = CARTESIAN_POINT('',(-46.4,-1.181180495325E+03,225.94254351617)); +#4206 = LINE('',#4207,#4208); +#4207 = CARTESIAN_POINT('',(-46.4,-1.16184E+03,234.74)); +#4208 = VECTOR('',#4209,1.); +#4209 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#4210 = ORIENTED_EDGE('',*,*,#4211,.T.); +#4211 = EDGE_CURVE('',#4204,#4169,#4212,.T.); +#4212 = CIRCLE('',#4213,10.); +#4213 = AXIS2_PLACEMENT_3D('',#4214,#4215,#4216); +#4214 = CARTESIAN_POINT('',(-46.4,-1.17704E+03,216.84)); +#4215 = DIRECTION('',(1.,0.,0.)); +#4216 = DIRECTION('',(0.,1.,0.)); +#4217 = ORIENTED_EDGE('',*,*,#4168,.T.); +#4218 = ORIENTED_EDGE('',*,*,#3641,.T.); +#4219 = ORIENTED_EDGE('',*,*,#4220,.T.); +#4220 = EDGE_CURVE('',#3633,#4194,#4221,.T.); +#4221 = LINE('',#4222,#4223); +#4222 = CARTESIAN_POINT('',(-46.4,-1.10154E+03,200.94)); +#4223 = VECTOR('',#4224,1.); +#4224 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4225 = PLANE('',#4226); +#4226 = AXIS2_PLACEMENT_3D('',#4227,#4228,#4229); +#4227 = CARTESIAN_POINT('',(-46.4,-1.147896717874E+03,193.1785020231)); +#4228 = DIRECTION('',(1.,0.,0.)); +#4229 = DIRECTION('',(0.,1.,0.)); +#4230 = ADVANCED_FACE('',(#4231),#4250,.T.); +#4231 = FACE_BOUND('',#4232,.T.); +#4232 = EDGE_LOOP('',(#4233,#4234,#4243,#4249)); +#4233 = ORIENTED_EDGE('',*,*,#4220,.T.); +#4234 = ORIENTED_EDGE('',*,*,#4235,.F.); +#4235 = EDGE_CURVE('',#4236,#4194,#4238,.T.); +#4236 = VERTEX_POINT('',#4237); +#4237 = CARTESIAN_POINT('',(-44.9,-1.108905900014E+03,230.07972403761)); +#4238 = CIRCLE('',#4239,1.5); +#4239 = AXIS2_PLACEMENT_3D('',#4240,#4241,#4242); +#4240 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#4241 = DIRECTION('',(-6.E-17,-0.29314935841,0.956066657542)); +#4242 = DIRECTION('',(1.,2.945186501321E-17,7.178766751374E-17)); +#4243 = ORIENTED_EDGE('',*,*,#4244,.F.); +#4244 = EDGE_CURVE('',#3625,#4236,#4245,.T.); +#4245 = LINE('',#4246,#4247); +#4246 = CARTESIAN_POINT('',(-44.9,-1.100105900014E+03,201.37972403761)); +#4247 = VECTOR('',#4248,1.); +#4248 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4249 = ORIENTED_EDGE('',*,*,#3632,.T.); +#4250 = CYLINDRICAL_SURFACE('',#4251,1.5); +#4251 = AXIS2_PLACEMENT_3D('',#4252,#4253,#4254); +#4252 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#4253 = DIRECTION('',(-6.E-17,-0.29314935841,0.956066657542)); +#4254 = DIRECTION('',(1.,2.031123047657E-17,6.898496424118E-17)); +#4255 = ADVANCED_FACE('',(#4256),#4274,.F.); +#4256 = FACE_BOUND('',#4257,.F.); +#4257 = EDGE_LOOP('',(#4258,#4259,#4267,#4273)); +#4258 = ORIENTED_EDGE('',*,*,#3624,.F.); +#4259 = ORIENTED_EDGE('',*,*,#4260,.T.); +#4260 = EDGE_CURVE('',#3617,#4261,#4263,.T.); +#4261 = VERTEX_POINT('',#4262); +#4262 = CARTESIAN_POINT('',(-13.9,-1.108905900014E+03,230.07972403761)); +#4263 = LINE('',#4264,#4265); +#4264 = CARTESIAN_POINT('',(-13.9,-1.104270189936E+03,214.96098776003)); +#4265 = VECTOR('',#4266,1.); +#4266 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4267 = ORIENTED_EDGE('',*,*,#4268,.F.); +#4268 = EDGE_CURVE('',#4236,#4261,#4269,.T.); +#4269 = LINE('',#4270,#4271); +#4270 = CARTESIAN_POINT('',(-44.9,-1.108905900014E+03,230.07972403761)); +#4271 = VECTOR('',#4272,1.); +#4272 = DIRECTION('',(1.,0.,0.)); +#4273 = ORIENTED_EDGE('',*,*,#4244,.F.); +#4274 = PLANE('',#4275); +#4275 = AXIS2_PLACEMENT_3D('',#4276,#4277,#4278); +#4276 = CARTESIAN_POINT('',(-44.9,-1.100105900014E+03,201.37972403761)); +#4277 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#4278 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4279 = ADVANCED_FACE('',(#4280,#4334,#4345),#4356,.F.); +#4280 = FACE_BOUND('',#4281,.F.); +#4281 = EDGE_LOOP('',(#4282,#4283,#4284,#4293,#4301,#4310,#4318,#4326, + #4333)); +#4282 = ORIENTED_EDGE('',*,*,#3616,.F.); +#4283 = ORIENTED_EDGE('',*,*,#3876,.T.); +#4284 = ORIENTED_EDGE('',*,*,#4285,.T.); +#4285 = EDGE_CURVE('',#3877,#4286,#4288,.T.); +#4286 = VERTEX_POINT('',#4287); +#4287 = CARTESIAN_POINT('',(-13.9,-1.081120341577E+03,202.97138678066)); +#4288 = CIRCLE('',#4289,7.5); +#4289 = AXIS2_PLACEMENT_3D('',#4290,#4291,#4292); +#4290 = CARTESIAN_POINT('',(-13.9,-1.08834E+03,200.94)); +#4291 = DIRECTION('',(1.,0.,0.)); +#4292 = DIRECTION('',(0.,1.,0.)); +#4293 = ORIENTED_EDGE('',*,*,#4294,.F.); +#4294 = EDGE_CURVE('',#4295,#4286,#4297,.T.); +#4295 = VERTEX_POINT('',#4296); +#4296 = CARTESIAN_POINT('',(-13.9,-1.092220341577E+03,242.42138678066)); +#4297 = LINE('',#4298,#4299); +#4298 = CARTESIAN_POINT('',(-13.9,-1.092220341577E+03,242.42138678066)); +#4299 = VECTOR('',#4300,1.); +#4300 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#4301 = ORIENTED_EDGE('',*,*,#4302,.T.); +#4302 = EDGE_CURVE('',#4295,#4303,#4305,.T.); +#4303 = VERTEX_POINT('',#4304); +#4304 = CARTESIAN_POINT('',(-13.9,-1.099423892855E+03,247.88998270397)); +#4305 = CIRCLE('',#4306,7.5); +#4306 = AXIS2_PLACEMENT_3D('',#4307,#4308,#4309); +#4307 = CARTESIAN_POINT('',(-13.9,-1.09944E+03,240.39)); +#4308 = DIRECTION('',(1.,0.,0.)); +#4309 = DIRECTION('',(0.,1.,0.)); +#4310 = ORIENTED_EDGE('',*,*,#4311,.T.); +#4311 = EDGE_CURVE('',#4303,#4312,#4314,.T.); +#4312 = VERTEX_POINT('',#4313); +#4313 = CARTESIAN_POINT('',(-13.9,-1.156973095081E+03,235.76537178991)); +#4314 = LINE('',#4315,#4316); +#4315 = CARTESIAN_POINT('',(-13.9,-1.099423892855E+03,247.88998270397)); +#4316 = VECTOR('',#4317,1.); +#4317 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#4318 = ORIENTED_EDGE('',*,*,#4319,.F.); +#4319 = EDGE_CURVE('',#4320,#4312,#4322,.T.); +#4320 = VERTEX_POINT('',#4321); +#4321 = CARTESIAN_POINT('',(-13.9,-1.110192179364E+03,231.13269858292)); +#4322 = LINE('',#4323,#4324); +#4323 = CARTESIAN_POINT('',(-13.9,-1.111788015875E+03,231.29073287826)); +#4324 = VECTOR('',#4325,1.); +#4325 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4326 = ORIENTED_EDGE('',*,*,#4327,.F.); +#4327 = EDGE_CURVE('',#4261,#4320,#4328,.T.); +#4328 = CIRCLE('',#4329,1.5); +#4329 = AXIS2_PLACEMENT_3D('',#4330,#4331,#4332); +#4330 = CARTESIAN_POINT('',(-13.9,-1.11034E+03,229.64)); +#4331 = DIRECTION('',(1.,-0.,0.)); +#4332 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#4333 = ORIENTED_EDGE('',*,*,#4260,.F.); +#4334 = FACE_BOUND('',#4335,.F.); +#4335 = EDGE_LOOP('',(#4336)); +#4336 = ORIENTED_EDGE('',*,*,#4337,.F.); +#4337 = EDGE_CURVE('',#4338,#4338,#4340,.T.); +#4338 = VERTEX_POINT('',#4339); +#4339 = CARTESIAN_POINT('',(-13.9,-1.08434E+03,200.94)); +#4340 = CIRCLE('',#4341,4.); +#4341 = AXIS2_PLACEMENT_3D('',#4342,#4343,#4344); +#4342 = CARTESIAN_POINT('',(-13.9,-1.08834E+03,200.94)); +#4343 = DIRECTION('',(1.,0.,0.)); +#4344 = DIRECTION('',(0.,1.,0.)); +#4345 = FACE_BOUND('',#4346,.F.); +#4346 = EDGE_LOOP('',(#4347)); +#4347 = ORIENTED_EDGE('',*,*,#4348,.F.); +#4348 = EDGE_CURVE('',#4349,#4349,#4351,.T.); +#4349 = VERTEX_POINT('',#4350); +#4350 = CARTESIAN_POINT('',(-13.9,-1.09544E+03,240.39)); +#4351 = CIRCLE('',#4352,4.); +#4352 = AXIS2_PLACEMENT_3D('',#4353,#4354,#4355); +#4353 = CARTESIAN_POINT('',(-13.9,-1.09944E+03,240.39)); +#4354 = DIRECTION('',(1.,0.,0.)); +#4355 = DIRECTION('',(0.,1.,0.)); +#4356 = PLANE('',#4357); +#4357 = AXIS2_PLACEMENT_3D('',#4358,#4359,#4360); +#4358 = CARTESIAN_POINT('',(-13.9,-1.113835686113E+03,226.88613249116)); +#4359 = DIRECTION('',(1.,0.,0.)); +#4360 = DIRECTION('',(0.,1.,0.)); +#4361 = ADVANCED_FACE('',(#4362),#4373,.T.); +#4362 = FACE_BOUND('',#4363,.T.); +#4363 = EDGE_LOOP('',(#4364,#4365,#4366,#4372)); +#4364 = ORIENTED_EDGE('',*,*,#3861,.T.); +#4365 = ORIENTED_EDGE('',*,*,#3923,.T.); +#4366 = ORIENTED_EDGE('',*,*,#4367,.F.); +#4367 = EDGE_CURVE('',#3690,#3916,#4368,.T.); +#4368 = LINE('',#4369,#4370); +#4369 = CARTESIAN_POINT('',(20.1,-1.16184E+03,234.74)); +#4370 = VECTOR('',#4371,1.); +#4371 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#4372 = ORIENTED_EDGE('',*,*,#3689,.F.); +#4373 = PLANE('',#4374); +#4374 = AXIS2_PLACEMENT_3D('',#4375,#4376,#4377); +#4375 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#4376 = DIRECTION('',(0.,-9.85470909115E-02,-0.995132388616)); +#4377 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4378 = ADVANCED_FACE('',(#4379),#4397,.F.); +#4379 = FACE_BOUND('',#4380,.F.); +#4380 = EDGE_LOOP('',(#4381,#4382,#4390,#4396)); +#4381 = ORIENTED_EDGE('',*,*,#3697,.T.); +#4382 = ORIENTED_EDGE('',*,*,#4383,.T.); +#4383 = EDGE_CURVE('',#3698,#4384,#4386,.T.); +#4384 = VERTEX_POINT('',#4385); +#4385 = CARTESIAN_POINT('',(24.1,-1.11034E+03,229.64)); +#4386 = LINE('',#4387,#4388); +#4387 = CARTESIAN_POINT('',(24.1,-1.16184E+03,234.74)); +#4388 = VECTOR('',#4389,1.); +#4389 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#4390 = ORIENTED_EDGE('',*,*,#4391,.F.); +#4391 = EDGE_CURVE('',#3916,#4384,#4392,.T.); +#4392 = LINE('',#4393,#4394); +#4393 = CARTESIAN_POINT('',(20.1,-1.11034E+03,229.64)); +#4394 = VECTOR('',#4395,1.); +#4395 = DIRECTION('',(1.,0.,0.)); +#4396 = ORIENTED_EDGE('',*,*,#4367,.F.); +#4397 = PLANE('',#4398); +#4398 = AXIS2_PLACEMENT_3D('',#4399,#4400,#4401); +#4399 = CARTESIAN_POINT('',(20.1,-1.16184E+03,234.74)); +#4400 = DIRECTION('',(0.,9.85470909115E-02,0.995132388616)); +#4401 = DIRECTION('',(0.,0.995132388616,-9.85470909115E-02)); +#4402 = ADVANCED_FACE('',(#4403),#4421,.T.); +#4403 = FACE_BOUND('',#4404,.T.); +#4404 = EDGE_LOOP('',(#4405,#4406,#4414,#4420)); +#4405 = ORIENTED_EDGE('',*,*,#4383,.T.); +#4406 = ORIENTED_EDGE('',*,*,#4407,.T.); +#4407 = EDGE_CURVE('',#4384,#4408,#4410,.T.); +#4408 = VERTEX_POINT('',#4409); +#4409 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#4410 = LINE('',#4411,#4412); +#4411 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#4412 = VECTOR('',#4413,1.); +#4413 = DIRECTION('',(1.,0.,0.)); +#4414 = ORIENTED_EDGE('',*,*,#4415,.T.); +#4415 = EDGE_CURVE('',#4408,#3706,#4416,.T.); +#4416 = LINE('',#4417,#4418); +#4417 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#4418 = VECTOR('',#4419,1.); +#4419 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4420 = ORIENTED_EDGE('',*,*,#3705,.F.); +#4421 = PLANE('',#4422); +#4422 = AXIS2_PLACEMENT_3D('',#4423,#4424,#4425); +#4423 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#4424 = DIRECTION('',(0.,-9.85470909115E-02,-0.995132388616)); +#4425 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4426 = ADVANCED_FACE('',(#4427),#4447,.F.); +#4427 = FACE_BOUND('',#4428,.F.); +#4428 = EDGE_LOOP('',(#4429,#4437,#4438,#4439,#4440,#4441)); +#4429 = ORIENTED_EDGE('',*,*,#4430,.T.); +#4430 = EDGE_CURVE('',#4431,#4408,#4433,.T.); +#4431 = VERTEX_POINT('',#4432); +#4432 = CARTESIAN_POINT('',(55.1,-1.10154E+03,200.94)); +#4433 = LINE('',#4434,#4435); +#4434 = CARTESIAN_POINT('',(55.1,-1.10154E+03,200.94)); +#4435 = VECTOR('',#4436,1.); +#4436 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4437 = ORIENTED_EDGE('',*,*,#4415,.T.); +#4438 = ORIENTED_EDGE('',*,*,#3713,.T.); +#4439 = ORIENTED_EDGE('',*,*,#3737,.T.); +#4440 = ORIENTED_EDGE('',*,*,#3762,.T.); +#4441 = ORIENTED_EDGE('',*,*,#4442,.T.); +#4442 = EDGE_CURVE('',#3763,#4431,#4443,.T.); +#4443 = LINE('',#4444,#4445); +#4444 = CARTESIAN_POINT('',(55.1,-1.16334E+03,134.64)); +#4445 = VECTOR('',#4446,1.); +#4446 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4447 = PLANE('',#4448); +#4448 = AXIS2_PLACEMENT_3D('',#4449,#4450,#4451); +#4449 = CARTESIAN_POINT('',(55.1,-1.147896717874E+03,193.1785020231)); +#4450 = DIRECTION('',(1.,0.,0.)); +#4451 = DIRECTION('',(0.,1.,0.)); +#4452 = ADVANCED_FACE('',(#4453),#4471,.T.); +#4453 = FACE_BOUND('',#4454,.T.); +#4454 = EDGE_LOOP('',(#4455,#4456,#4464,#4470)); +#4455 = ORIENTED_EDGE('',*,*,#3842,.T.); +#4456 = ORIENTED_EDGE('',*,*,#4457,.T.); +#4457 = EDGE_CURVE('',#3835,#4458,#4460,.T.); +#4458 = VERTEX_POINT('',#4459); +#4459 = CARTESIAN_POINT('',(-29.9,-1.160255084405E+03,137.94954479362)); +#4460 = LINE('',#4461,#4462); +#4461 = CARTESIAN_POINT('',(-29.9,-1.16334E+03,134.64)); +#4462 = VECTOR('',#4463,1.); +#4463 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4464 = ORIENTED_EDGE('',*,*,#4465,.F.); +#4465 = EDGE_CURVE('',#3939,#4458,#4466,.T.); +#4466 = LINE('',#4467,#4468); +#4467 = CARTESIAN_POINT('',(-39.9,-1.160255084405E+03,137.94954479362)); +#4468 = VECTOR('',#4469,1.); +#4469 = DIRECTION('',(1.,0.,0.)); +#4470 = ORIENTED_EDGE('',*,*,#3938,.F.); +#4471 = PLANE('',#4472); +#4472 = AXIS2_PLACEMENT_3D('',#4473,#4474,#4475); +#4473 = CARTESIAN_POINT('',(-39.9,-1.16334E+03,134.64)); +#4474 = DIRECTION('',(0.,-0.731495332935,0.681846447446)); +#4475 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4476 = ADVANCED_FACE('',(#4477),#4493,.F.); +#4477 = FACE_BOUND('',#4478,.F.); +#4478 = EDGE_LOOP('',(#4479,#4480,#4486,#4487)); +#4479 = ORIENTED_EDGE('',*,*,#3834,.F.); +#4480 = ORIENTED_EDGE('',*,*,#4481,.T.); +#4481 = EDGE_CURVE('',#3835,#4137,#4482,.T.); +#4482 = LINE('',#4483,#4484); +#4483 = CARTESIAN_POINT('',(-29.9,-1.163919588191E+03,134.01820878496)); +#4484 = VECTOR('',#4485,1.); +#4485 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4486 = ORIENTED_EDGE('',*,*,#4136,.T.); +#4487 = ORIENTED_EDGE('',*,*,#4488,.F.); +#4488 = EDGE_CURVE('',#3827,#4129,#4489,.T.); +#4489 = LINE('',#4490,#4491); +#4490 = CARTESIAN_POINT('',(-19.9,-1.163919588191E+03,134.01820878496)); +#4491 = VECTOR('',#4492,1.); +#4492 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4493 = PLANE('',#4494); +#4494 = AXIS2_PLACEMENT_3D('',#4495,#4496,#4497); +#4495 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4496 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4497 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4498 = ADVANCED_FACE('',(#4499),#4524,.T.); +#4499 = FACE_BOUND('',#4500,.T.); +#4500 = EDGE_LOOP('',(#4501,#4502,#4510,#4518)); +#4501 = ORIENTED_EDGE('',*,*,#3826,.T.); +#4502 = ORIENTED_EDGE('',*,*,#4503,.T.); +#4503 = EDGE_CURVE('',#3819,#4504,#4506,.T.); +#4504 = VERTEX_POINT('',#4505); +#4505 = CARTESIAN_POINT('',(-9.9,-1.160255084405E+03,137.94954479362)); +#4506 = LINE('',#4507,#4508); +#4507 = CARTESIAN_POINT('',(-9.9,-1.16334E+03,134.64)); +#4508 = VECTOR('',#4509,1.); +#4509 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4510 = ORIENTED_EDGE('',*,*,#4511,.F.); +#4511 = EDGE_CURVE('',#4512,#4504,#4514,.T.); +#4512 = VERTEX_POINT('',#4513); +#4513 = CARTESIAN_POINT('',(-19.9,-1.160255084405E+03,137.94954479362)); +#4514 = LINE('',#4515,#4516); +#4515 = CARTESIAN_POINT('',(-19.9,-1.160255084405E+03,137.94954479362)); +#4516 = VECTOR('',#4517,1.); +#4517 = DIRECTION('',(1.,0.,0.)); +#4518 = ORIENTED_EDGE('',*,*,#4519,.F.); +#4519 = EDGE_CURVE('',#3827,#4512,#4520,.T.); +#4520 = LINE('',#4521,#4522); +#4521 = CARTESIAN_POINT('',(-19.9,-1.16334E+03,134.64)); +#4522 = VECTOR('',#4523,1.); +#4523 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4524 = PLANE('',#4525); +#4525 = AXIS2_PLACEMENT_3D('',#4526,#4527,#4528); +#4526 = CARTESIAN_POINT('',(-19.9,-1.16334E+03,134.64)); +#4527 = DIRECTION('',(0.,-0.731495332935,0.681846447446)); +#4528 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4529 = ADVANCED_FACE('',(#4530),#4546,.F.); +#4530 = FACE_BOUND('',#4531,.F.); +#4531 = EDGE_LOOP('',(#4532,#4533,#4539,#4540)); +#4532 = ORIENTED_EDGE('',*,*,#3818,.F.); +#4533 = ORIENTED_EDGE('',*,*,#4534,.T.); +#4534 = EDGE_CURVE('',#3819,#4105,#4535,.T.); +#4535 = LINE('',#4536,#4537); +#4536 = CARTESIAN_POINT('',(-9.9,-1.163919588191E+03,134.01820878496)); +#4537 = VECTOR('',#4538,1.); +#4538 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4539 = ORIENTED_EDGE('',*,*,#4104,.T.); +#4540 = ORIENTED_EDGE('',*,*,#4541,.F.); +#4541 = EDGE_CURVE('',#3811,#4097,#4542,.T.); +#4542 = LINE('',#4543,#4544); +#4543 = CARTESIAN_POINT('',(0.1,-1.163919588191E+03,134.01820878496)); +#4544 = VECTOR('',#4545,1.); +#4545 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4546 = PLANE('',#4547); +#4547 = AXIS2_PLACEMENT_3D('',#4548,#4549,#4550); +#4548 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4549 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4550 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4551 = ADVANCED_FACE('',(#4552),#4577,.T.); +#4552 = FACE_BOUND('',#4553,.T.); +#4553 = EDGE_LOOP('',(#4554,#4555,#4563,#4571)); +#4554 = ORIENTED_EDGE('',*,*,#3810,.T.); +#4555 = ORIENTED_EDGE('',*,*,#4556,.T.); +#4556 = EDGE_CURVE('',#3803,#4557,#4559,.T.); +#4557 = VERTEX_POINT('',#4558); +#4558 = CARTESIAN_POINT('',(10.1,-1.160255084405E+03,137.94954479362)); +#4559 = LINE('',#4560,#4561); +#4560 = CARTESIAN_POINT('',(10.1,-1.16334E+03,134.64)); +#4561 = VECTOR('',#4562,1.); +#4562 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4563 = ORIENTED_EDGE('',*,*,#4564,.F.); +#4564 = EDGE_CURVE('',#4565,#4557,#4567,.T.); +#4565 = VERTEX_POINT('',#4566); +#4566 = CARTESIAN_POINT('',(0.1,-1.160255084405E+03,137.94954479362)); +#4567 = LINE('',#4568,#4569); +#4568 = CARTESIAN_POINT('',(0.1,-1.160255084405E+03,137.94954479362)); +#4569 = VECTOR('',#4570,1.); +#4570 = DIRECTION('',(1.,0.,0.)); +#4571 = ORIENTED_EDGE('',*,*,#4572,.F.); +#4572 = EDGE_CURVE('',#3811,#4565,#4573,.T.); +#4573 = LINE('',#4574,#4575); +#4574 = CARTESIAN_POINT('',(0.1,-1.16334E+03,134.64)); +#4575 = VECTOR('',#4576,1.); +#4576 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4577 = PLANE('',#4578); +#4578 = AXIS2_PLACEMENT_3D('',#4579,#4580,#4581); +#4579 = CARTESIAN_POINT('',(0.1,-1.16334E+03,134.64)); +#4580 = DIRECTION('',(0.,-0.731495332935,0.681846447446)); +#4581 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4582 = ADVANCED_FACE('',(#4583),#4599,.F.); +#4583 = FACE_BOUND('',#4584,.F.); +#4584 = EDGE_LOOP('',(#4585,#4586,#4592,#4593)); +#4585 = ORIENTED_EDGE('',*,*,#3802,.F.); +#4586 = ORIENTED_EDGE('',*,*,#4587,.T.); +#4587 = EDGE_CURVE('',#3803,#4073,#4588,.T.); +#4588 = LINE('',#4589,#4590); +#4589 = CARTESIAN_POINT('',(10.1,-1.163919588191E+03,134.01820878496)); +#4590 = VECTOR('',#4591,1.); +#4591 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4592 = ORIENTED_EDGE('',*,*,#4072,.T.); +#4593 = ORIENTED_EDGE('',*,*,#4594,.F.); +#4594 = EDGE_CURVE('',#3795,#4065,#4595,.T.); +#4595 = LINE('',#4596,#4597); +#4596 = CARTESIAN_POINT('',(20.1,-1.163919588191E+03,134.01820878496)); +#4597 = VECTOR('',#4598,1.); +#4598 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4599 = PLANE('',#4600); +#4600 = AXIS2_PLACEMENT_3D('',#4601,#4602,#4603); +#4601 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4602 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4603 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4604 = ADVANCED_FACE('',(#4605),#4630,.T.); +#4605 = FACE_BOUND('',#4606,.T.); +#4606 = EDGE_LOOP('',(#4607,#4608,#4616,#4624)); +#4607 = ORIENTED_EDGE('',*,*,#3794,.T.); +#4608 = ORIENTED_EDGE('',*,*,#4609,.T.); +#4609 = EDGE_CURVE('',#3787,#4610,#4612,.T.); +#4610 = VERTEX_POINT('',#4611); +#4611 = CARTESIAN_POINT('',(30.1,-1.160255084405E+03,137.94954479362)); +#4612 = LINE('',#4613,#4614); +#4613 = CARTESIAN_POINT('',(30.1,-1.16334E+03,134.64)); +#4614 = VECTOR('',#4615,1.); +#4615 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4616 = ORIENTED_EDGE('',*,*,#4617,.F.); +#4617 = EDGE_CURVE('',#4618,#4610,#4620,.T.); +#4618 = VERTEX_POINT('',#4619); +#4619 = CARTESIAN_POINT('',(20.1,-1.160255084405E+03,137.94954479362)); +#4620 = LINE('',#4621,#4622); +#4621 = CARTESIAN_POINT('',(20.1,-1.160255084405E+03,137.94954479362)); +#4622 = VECTOR('',#4623,1.); +#4623 = DIRECTION('',(1.,0.,0.)); +#4624 = ORIENTED_EDGE('',*,*,#4625,.F.); +#4625 = EDGE_CURVE('',#3795,#4618,#4626,.T.); +#4626 = LINE('',#4627,#4628); +#4627 = CARTESIAN_POINT('',(20.1,-1.16334E+03,134.64)); +#4628 = VECTOR('',#4629,1.); +#4629 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4630 = PLANE('',#4631); +#4631 = AXIS2_PLACEMENT_3D('',#4632,#4633,#4634); +#4632 = CARTESIAN_POINT('',(20.1,-1.16334E+03,134.64)); +#4633 = DIRECTION('',(0.,-0.731495332935,0.681846447446)); +#4634 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4635 = ADVANCED_FACE('',(#4636),#4652,.F.); +#4636 = FACE_BOUND('',#4637,.F.); +#4637 = EDGE_LOOP('',(#4638,#4639,#4645,#4646)); +#4638 = ORIENTED_EDGE('',*,*,#3786,.F.); +#4639 = ORIENTED_EDGE('',*,*,#4640,.T.); +#4640 = EDGE_CURVE('',#3787,#4041,#4641,.T.); +#4641 = LINE('',#4642,#4643); +#4642 = CARTESIAN_POINT('',(30.1,-1.163919588191E+03,134.01820878496)); +#4643 = VECTOR('',#4644,1.); +#4644 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4645 = ORIENTED_EDGE('',*,*,#4040,.T.); +#4646 = ORIENTED_EDGE('',*,*,#4647,.F.); +#4647 = EDGE_CURVE('',#3779,#4033,#4648,.T.); +#4648 = LINE('',#4649,#4650); +#4649 = CARTESIAN_POINT('',(40.1,-1.163919588191E+03,134.01820878496)); +#4650 = VECTOR('',#4651,1.); +#4651 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4652 = PLANE('',#4653); +#4653 = AXIS2_PLACEMENT_3D('',#4654,#4655,#4656); +#4654 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4655 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4656 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4657 = ADVANCED_FACE('',(#4658),#4683,.T.); +#4658 = FACE_BOUND('',#4659,.T.); +#4659 = EDGE_LOOP('',(#4660,#4661,#4669,#4677)); +#4660 = ORIENTED_EDGE('',*,*,#3778,.T.); +#4661 = ORIENTED_EDGE('',*,*,#4662,.T.); +#4662 = EDGE_CURVE('',#3771,#4663,#4665,.T.); +#4663 = VERTEX_POINT('',#4664); +#4664 = CARTESIAN_POINT('',(50.1,-1.160255084405E+03,137.94954479362)); +#4665 = LINE('',#4666,#4667); +#4666 = CARTESIAN_POINT('',(50.1,-1.16334E+03,134.64)); +#4667 = VECTOR('',#4668,1.); +#4668 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4669 = ORIENTED_EDGE('',*,*,#4670,.F.); +#4670 = EDGE_CURVE('',#4671,#4663,#4673,.T.); +#4671 = VERTEX_POINT('',#4672); +#4672 = CARTESIAN_POINT('',(40.1,-1.160255084405E+03,137.94954479362)); +#4673 = LINE('',#4674,#4675); +#4674 = CARTESIAN_POINT('',(40.1,-1.160255084405E+03,137.94954479362)); +#4675 = VECTOR('',#4676,1.); +#4676 = DIRECTION('',(1.,0.,0.)); +#4677 = ORIENTED_EDGE('',*,*,#4678,.F.); +#4678 = EDGE_CURVE('',#3779,#4671,#4679,.T.); +#4679 = LINE('',#4680,#4681); +#4680 = CARTESIAN_POINT('',(40.1,-1.16334E+03,134.64)); +#4681 = VECTOR('',#4682,1.); +#4682 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4683 = PLANE('',#4684); +#4684 = AXIS2_PLACEMENT_3D('',#4685,#4686,#4687); +#4685 = CARTESIAN_POINT('',(40.1,-1.16334E+03,134.64)); +#4686 = DIRECTION('',(0.,-0.731495332935,0.681846447446)); +#4687 = DIRECTION('',(0.,0.681846447446,0.731495332935)); +#4688 = ADVANCED_FACE('',(#4689),#4748,.F.); +#4689 = FACE_BOUND('',#4690,.F.); +#4690 = EDGE_LOOP('',(#4691,#4692,#4698,#4699,#4708,#4716,#4725,#4733, + #4741,#4747)); +#4691 = ORIENTED_EDGE('',*,*,#3770,.F.); +#4692 = ORIENTED_EDGE('',*,*,#4693,.T.); +#4693 = EDGE_CURVE('',#3771,#4009,#4694,.T.); +#4694 = LINE('',#4695,#4696); +#4695 = CARTESIAN_POINT('',(50.1,-1.163919588191E+03,134.01820878496)); +#4696 = VECTOR('',#4697,1.); +#4697 = DIRECTION('',(0.,-0.681846383766,-0.731495392293)); +#4698 = ORIENTED_EDGE('',*,*,#4008,.T.); +#4699 = ORIENTED_EDGE('',*,*,#4700,.T.); +#4700 = EDGE_CURVE('',#4001,#4701,#4703,.T.); +#4701 = VERTEX_POINT('',#4702); +#4702 = CARTESIAN_POINT('',(56.6,-1.16334E+03,134.64)); +#4703 = ELLIPSE('',#4704,1.743718619647,1.5); +#4704 = AXIS2_PLACEMENT_3D('',#4705,#4706,#4707); +#4705 = CARTESIAN_POINT('',(55.1,-1.16334E+03,134.64)); +#4706 = DIRECTION('',(1.392343868323E-32,-0.731495392293,0.681846383766) + ); +#4707 = DIRECTION('',(-9.8E-16,-0.681846383766,-0.731495392293)); +#4708 = ORIENTED_EDGE('',*,*,#4709,.T.); +#4709 = EDGE_CURVE('',#4701,#4710,#4712,.T.); +#4710 = VERTEX_POINT('',#4711); +#4711 = CARTESIAN_POINT('',(56.6,-1.10154E+03,200.94)); +#4712 = LINE('',#4713,#4714); +#4713 = CARTESIAN_POINT('',(56.6,-1.16334E+03,134.64)); +#4714 = VECTOR('',#4715,1.); +#4715 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4716 = ORIENTED_EDGE('',*,*,#4717,.T.); +#4717 = EDGE_CURVE('',#4710,#4718,#4720,.T.); +#4718 = VERTEX_POINT('',#4719); +#4719 = CARTESIAN_POINT('',(55.1,-1.100359419922E+03,202.206544647)); +#4720 = ELLIPSE('',#4721,1.731445830491,1.5); +#4721 = AXIS2_PLACEMENT_3D('',#4722,#4723,#4724); +#4722 = CARTESIAN_POINT('',(55.1,-1.10154E+03,200.94)); +#4723 = DIRECTION('',(7.337186423333E-33,-0.731495392293,0.681846383766) + ); +#4724 = DIRECTION('',(1.1E-16,0.681846383766,0.731495392293)); +#4725 = ORIENTED_EDGE('',*,*,#4726,.T.); +#4726 = EDGE_CURVE('',#4718,#4727,#4729,.T.); +#4727 = VERTEX_POINT('',#4728); +#4728 = CARTESIAN_POINT('',(24.1,-1.100359419922E+03,202.206544647)); +#4729 = LINE('',#4730,#4731); +#4730 = CARTESIAN_POINT('',(-44.9,-1.100359419922E+03,202.206544647)); +#4731 = VECTOR('',#4732,1.); +#4732 = DIRECTION('',(-1.,0.,0.)); +#4733 = ORIENTED_EDGE('',*,*,#4734,.F.); +#4734 = EDGE_CURVE('',#4735,#4727,#4737,.T.); +#4735 = VERTEX_POINT('',#4736); +#4736 = CARTESIAN_POINT('',(24.1,-1.10154E+03,200.94)); +#4737 = LINE('',#4738,#4739); +#4738 = CARTESIAN_POINT('',(24.1,-1.128827677663E+03,171.66535551682)); +#4739 = VECTOR('',#4740,1.); +#4740 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4741 = ORIENTED_EDGE('',*,*,#4742,.T.); +#4742 = EDGE_CURVE('',#4735,#4431,#4743,.T.); +#4743 = LINE('',#4744,#4745); +#4744 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#4745 = VECTOR('',#4746,1.); +#4746 = DIRECTION('',(1.,0.,0.)); +#4747 = ORIENTED_EDGE('',*,*,#4442,.F.); +#4748 = PLANE('',#4749); +#4749 = AXIS2_PLACEMENT_3D('',#4750,#4751,#4752); +#4750 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4751 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4752 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4753 = ADVANCED_FACE('',(#4754),#4773,.T.); +#4754 = FACE_BOUND('',#4755,.T.); +#4755 = EDGE_LOOP('',(#4756,#4757,#4758,#4766)); +#4756 = ORIENTED_EDGE('',*,*,#3884,.T.); +#4757 = ORIENTED_EDGE('',*,*,#4285,.T.); +#4758 = ORIENTED_EDGE('',*,*,#4759,.F.); +#4759 = EDGE_CURVE('',#4760,#4286,#4762,.T.); +#4760 = VERTEX_POINT('',#4761); +#4761 = CARTESIAN_POINT('',(-9.9,-1.081120341577E+03,202.97138678066)); +#4762 = LINE('',#4763,#4764); +#4763 = CARTESIAN_POINT('',(-9.9,-1.081120341577E+03,202.97138678066)); +#4764 = VECTOR('',#4765,1.); +#4765 = DIRECTION('',(-1.,-0.,-0.)); +#4766 = ORIENTED_EDGE('',*,*,#4767,.F.); +#4767 = EDGE_CURVE('',#3885,#4760,#4768,.T.); +#4768 = CIRCLE('',#4769,7.5); +#4769 = AXIS2_PLACEMENT_3D('',#4770,#4771,#4772); +#4770 = CARTESIAN_POINT('',(-9.9,-1.08834E+03,200.94)); +#4771 = DIRECTION('',(1.,0.,0.)); +#4772 = DIRECTION('',(0.,1.,0.)); +#4773 = CYLINDRICAL_SURFACE('',#4774,7.5); +#4774 = AXIS2_PLACEMENT_3D('',#4775,#4776,#4777); +#4775 = CARTESIAN_POINT('',(-9.9,-1.08834E+03,200.94)); +#4776 = DIRECTION('',(1.,0.,0.)); +#4777 = DIRECTION('',(0.,1.,0.)); +#4778 = ADVANCED_FACE('',(#4779,#4839,#4850),#4861,.T.); +#4779 = FACE_BOUND('',#4780,.T.); +#4780 = EDGE_LOOP('',(#4781,#4789,#4790,#4791,#4799,#4808,#4816,#4824, + #4833)); +#4781 = ORIENTED_EDGE('',*,*,#4782,.F.); +#4782 = EDGE_CURVE('',#3585,#4783,#4785,.T.); +#4783 = VERTEX_POINT('',#4784); +#4784 = CARTESIAN_POINT('',(-9.9,-1.100359419922E+03,202.206544647)); +#4785 = LINE('',#4786,#4787); +#4786 = CARTESIAN_POINT('',(-9.9,-1.128827677663E+03,171.66535551682)); +#4787 = VECTOR('',#4788,1.); +#4788 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4789 = ORIENTED_EDGE('',*,*,#3892,.T.); +#4790 = ORIENTED_EDGE('',*,*,#4767,.T.); +#4791 = ORIENTED_EDGE('',*,*,#4792,.F.); +#4792 = EDGE_CURVE('',#4793,#4760,#4795,.T.); +#4793 = VERTEX_POINT('',#4794); +#4794 = CARTESIAN_POINT('',(-9.9,-1.092220341577E+03,242.42138678066)); +#4795 = LINE('',#4796,#4797); +#4796 = CARTESIAN_POINT('',(-9.9,-1.092220341577E+03,242.42138678066)); +#4797 = VECTOR('',#4798,1.); +#4798 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#4799 = ORIENTED_EDGE('',*,*,#4800,.T.); +#4800 = EDGE_CURVE('',#4793,#4801,#4803,.T.); +#4801 = VERTEX_POINT('',#4802); +#4802 = CARTESIAN_POINT('',(-9.9,-1.099423892855E+03,247.88998270397)); +#4803 = CIRCLE('',#4804,7.5); +#4804 = AXIS2_PLACEMENT_3D('',#4805,#4806,#4807); +#4805 = CARTESIAN_POINT('',(-9.9,-1.09944E+03,240.39)); +#4806 = DIRECTION('',(1.,0.,0.)); +#4807 = DIRECTION('',(0.,1.,0.)); +#4808 = ORIENTED_EDGE('',*,*,#4809,.T.); +#4809 = EDGE_CURVE('',#4801,#4810,#4812,.T.); +#4810 = VERTEX_POINT('',#4811); +#4811 = CARTESIAN_POINT('',(-9.9,-1.156973095081E+03,235.76537178991)); +#4812 = LINE('',#4813,#4814); +#4813 = CARTESIAN_POINT('',(-9.9,-1.099423892855E+03,247.88998270397)); +#4814 = VECTOR('',#4815,1.); +#4815 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#4816 = ORIENTED_EDGE('',*,*,#4817,.F.); +#4817 = EDGE_CURVE('',#4818,#4810,#4820,.T.); +#4818 = VERTEX_POINT('',#4819); +#4819 = CARTESIAN_POINT('',(-9.9,-1.110192179364E+03,231.13269858292)); +#4820 = LINE('',#4821,#4822); +#4821 = CARTESIAN_POINT('',(-9.9,-1.111788015875E+03,231.29073287826)); +#4822 = VECTOR('',#4823,1.); +#4823 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#4824 = ORIENTED_EDGE('',*,*,#4825,.F.); +#4825 = EDGE_CURVE('',#4826,#4818,#4828,.T.); +#4826 = VERTEX_POINT('',#4827); +#4827 = CARTESIAN_POINT('',(-9.9,-1.108905900014E+03,230.07972403761)); +#4828 = CIRCLE('',#4829,1.5); +#4829 = AXIS2_PLACEMENT_3D('',#4830,#4831,#4832); +#4830 = CARTESIAN_POINT('',(-9.9,-1.11034E+03,229.64)); +#4831 = DIRECTION('',(1.,-0.,0.)); +#4832 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#4833 = ORIENTED_EDGE('',*,*,#4834,.F.); +#4834 = EDGE_CURVE('',#4783,#4826,#4835,.T.); +#4835 = LINE('',#4836,#4837); +#4836 = CARTESIAN_POINT('',(-9.9,-1.104270189936E+03,214.96098776003)); +#4837 = VECTOR('',#4838,1.); +#4838 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#4839 = FACE_BOUND('',#4840,.T.); +#4840 = EDGE_LOOP('',(#4841)); +#4841 = ORIENTED_EDGE('',*,*,#4842,.F.); +#4842 = EDGE_CURVE('',#4843,#4843,#4845,.T.); +#4843 = VERTEX_POINT('',#4844); +#4844 = CARTESIAN_POINT('',(-9.9,-1.08434E+03,200.94)); +#4845 = CIRCLE('',#4846,4.); +#4846 = AXIS2_PLACEMENT_3D('',#4847,#4848,#4849); +#4847 = CARTESIAN_POINT('',(-9.9,-1.08834E+03,200.94)); +#4848 = DIRECTION('',(1.,0.,0.)); +#4849 = DIRECTION('',(0.,1.,0.)); +#4850 = FACE_BOUND('',#4851,.T.); +#4851 = EDGE_LOOP('',(#4852)); +#4852 = ORIENTED_EDGE('',*,*,#4853,.F.); +#4853 = EDGE_CURVE('',#4854,#4854,#4856,.T.); +#4854 = VERTEX_POINT('',#4855); +#4855 = CARTESIAN_POINT('',(-9.9,-1.09544E+03,240.39)); +#4856 = CIRCLE('',#4857,4.); +#4857 = AXIS2_PLACEMENT_3D('',#4858,#4859,#4860); +#4858 = CARTESIAN_POINT('',(-9.9,-1.09944E+03,240.39)); +#4859 = DIRECTION('',(1.,0.,0.)); +#4860 = DIRECTION('',(0.,1.,0.)); +#4861 = PLANE('',#4862); +#4862 = AXIS2_PLACEMENT_3D('',#4863,#4864,#4865); +#4863 = CARTESIAN_POINT('',(-9.9,-1.113835686113E+03,226.88613249116)); +#4864 = DIRECTION('',(1.,0.,0.)); +#4865 = DIRECTION('',(0.,1.,0.)); +#4866 = ADVANCED_FACE('',(#4867),#4883,.F.); +#4867 = FACE_BOUND('',#4868,.F.); +#4868 = EDGE_LOOP('',(#4869,#4870,#4876,#4882)); +#4869 = ORIENTED_EDGE('',*,*,#4391,.T.); +#4870 = ORIENTED_EDGE('',*,*,#4871,.T.); +#4871 = EDGE_CURVE('',#4384,#4735,#4872,.T.); +#4872 = LINE('',#4873,#4874); +#4873 = CARTESIAN_POINT('',(24.1,-1.11034E+03,229.64)); +#4874 = VECTOR('',#4875,1.); +#4875 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#4876 = ORIENTED_EDGE('',*,*,#4877,.F.); +#4877 = EDGE_CURVE('',#3908,#4735,#4878,.T.); +#4878 = LINE('',#4879,#4880); +#4879 = CARTESIAN_POINT('',(20.1,-1.10154E+03,200.94)); +#4880 = VECTOR('',#4881,1.); +#4881 = DIRECTION('',(1.,0.,0.)); +#4882 = ORIENTED_EDGE('',*,*,#3915,.F.); +#4883 = PLANE('',#4884); +#4884 = AXIS2_PLACEMENT_3D('',#4885,#4886,#4887); +#4885 = CARTESIAN_POINT('',(20.1,-1.11034E+03,229.64)); +#4886 = DIRECTION('',(0.,0.956066657542,0.29314935841)); +#4887 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#4888 = ADVANCED_FACE('',(#4889),#4907,.F.); +#4889 = FACE_BOUND('',#4890,.F.); +#4890 = EDGE_LOOP('',(#4891,#4892,#4893,#4901)); +#4891 = ORIENTED_EDGE('',*,*,#4782,.F.); +#4892 = ORIENTED_EDGE('',*,*,#3907,.T.); +#4893 = ORIENTED_EDGE('',*,*,#4894,.T.); +#4894 = EDGE_CURVE('',#3908,#4895,#4897,.T.); +#4895 = VERTEX_POINT('',#4896); +#4896 = CARTESIAN_POINT('',(20.1,-1.100359419922E+03,202.206544647)); +#4897 = LINE('',#4898,#4899); +#4898 = CARTESIAN_POINT('',(20.1,-1.128827677663E+03,171.66535551682)); +#4899 = VECTOR('',#4900,1.); +#4900 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4901 = ORIENTED_EDGE('',*,*,#4902,.T.); +#4902 = EDGE_CURVE('',#4895,#4783,#4903,.T.); +#4903 = LINE('',#4904,#4905); +#4904 = CARTESIAN_POINT('',(-44.9,-1.100359419922E+03,202.206544647)); +#4905 = VECTOR('',#4906,1.); +#4906 = DIRECTION('',(-1.,0.,0.)); +#4907 = PLANE('',#4908); +#4908 = AXIS2_PLACEMENT_3D('',#4909,#4910,#4911); +#4909 = CARTESIAN_POINT('',(-44.9,-1.16334E+03,134.64)); +#4910 = DIRECTION('',(0.,-0.731495392293,0.681846383766)); +#4911 = DIRECTION('',(0.,0.681846383766,0.731495392293)); +#4912 = ADVANCED_FACE('',(#4913),#4931,.T.); +#4913 = FACE_BOUND('',#4914,.T.); +#4914 = EDGE_LOOP('',(#4915,#4916,#4924,#4930)); +#4915 = ORIENTED_EDGE('',*,*,#3962,.F.); +#4916 = ORIENTED_EDGE('',*,*,#4917,.T.); +#4917 = EDGE_CURVE('',#3955,#4918,#4920,.T.); +#4918 = VERTEX_POINT('',#4919); +#4919 = CARTESIAN_POINT('',(-29.9,-1.167855516655E+03,138.15219926067)); +#4920 = LINE('',#4921,#4922); +#4921 = CARTESIAN_POINT('',(-39.9,-1.167855516655E+03,138.15219926067)); +#4922 = VECTOR('',#4923,1.); +#4923 = DIRECTION('',(1.,0.,0.)); +#4924 = ORIENTED_EDGE('',*,*,#4925,.T.); +#4925 = EDGE_CURVE('',#4918,#4145,#4926,.T.); +#4926 = LINE('',#4927,#4928); +#4927 = CARTESIAN_POINT('',(-29.9,-1.167855516655E+03,138.15219926067)); +#4928 = VECTOR('',#4929,1.); +#4929 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#4930 = ORIENTED_EDGE('',*,*,#4152,.F.); +#4931 = PLANE('',#4932); +#4932 = AXIS2_PLACEMENT_3D('',#4933,#4934,#4935); +#4933 = CARTESIAN_POINT('',(-39.9,-1.167855516655E+03,138.15219926067)); +#4934 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#4935 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#4936 = ADVANCED_FACE('',(#4937),#4955,.T.); +#4937 = FACE_BOUND('',#4938,.T.); +#4938 = EDGE_LOOP('',(#4939,#4947,#4953,#4954)); +#4939 = ORIENTED_EDGE('',*,*,#4940,.T.); +#4940 = EDGE_CURVE('',#3947,#4941,#4943,.T.); +#4941 = VERTEX_POINT('',#4942); +#4942 = CARTESIAN_POINT('',(-29.9,-1.163662554812E+03,123.86376214781)); +#4943 = LINE('',#4944,#4945); +#4944 = CARTESIAN_POINT('',(-39.9,-1.163662554812E+03,123.86376214781)); +#4945 = VECTOR('',#4946,1.); +#4946 = DIRECTION('',(1.,0.,0.)); +#4947 = ORIENTED_EDGE('',*,*,#4948,.T.); +#4948 = EDGE_CURVE('',#4941,#4918,#4949,.T.); +#4949 = LINE('',#4950,#4951); +#4950 = CARTESIAN_POINT('',(-29.9,-1.163662554812E+03,123.86376214781)); +#4951 = VECTOR('',#4952,1.); +#4952 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#4953 = ORIENTED_EDGE('',*,*,#4917,.F.); +#4954 = ORIENTED_EDGE('',*,*,#3954,.F.); +#4955 = PLANE('',#4956); +#4956 = AXIS2_PLACEMENT_3D('',#4957,#4958,#4959); +#4957 = CARTESIAN_POINT('',(-39.9,-1.163662554812E+03,123.86376214781)); +#4958 = DIRECTION('',(0.,-0.959538377832,-0.281577878158)); +#4959 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#4960 = ADVANCED_FACE('',(#4961),#4972,.T.); +#4961 = FACE_BOUND('',#4962,.T.); +#4962 = EDGE_LOOP('',(#4963,#4964,#4970,#4971)); +#4963 = ORIENTED_EDGE('',*,*,#4465,.T.); +#4964 = ORIENTED_EDGE('',*,*,#4965,.T.); +#4965 = EDGE_CURVE('',#4458,#4941,#4966,.T.); +#4966 = LINE('',#4967,#4968); +#4967 = CARTESIAN_POINT('',(-29.9,-1.160255084405E+03,137.94954479362)); +#4968 = VECTOR('',#4969,1.); +#4969 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#4970 = ORIENTED_EDGE('',*,*,#4940,.F.); +#4971 = ORIENTED_EDGE('',*,*,#3946,.F.); +#4972 = PLANE('',#4973); +#4973 = AXIS2_PLACEMENT_3D('',#4974,#4975,#4976); +#4974 = CARTESIAN_POINT('',(-39.9,-1.160255084405E+03,137.94954479362)); +#4975 = DIRECTION('',(0.,0.971964770456,-0.235126529752)); +#4976 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#4977 = ADVANCED_FACE('',(#4978),#4986,.F.); +#4978 = FACE_BOUND('',#4979,.F.); +#4979 = EDGE_LOOP('',(#4980,#4981,#4982,#4983,#4984,#4985)); +#4980 = ORIENTED_EDGE('',*,*,#4481,.F.); +#4981 = ORIENTED_EDGE('',*,*,#4457,.T.); +#4982 = ORIENTED_EDGE('',*,*,#4965,.T.); +#4983 = ORIENTED_EDGE('',*,*,#4948,.T.); +#4984 = ORIENTED_EDGE('',*,*,#4925,.T.); +#4985 = ORIENTED_EDGE('',*,*,#4144,.F.); +#4986 = PLANE('',#4987); +#4987 = AXIS2_PLACEMENT_3D('',#4988,#4989,#4990); +#4988 = CARTESIAN_POINT('',(-29.9,-1.163860253502E+03,132.80086049584)); +#4989 = DIRECTION('',(-1.,-0.,-0.)); +#4990 = DIRECTION('',(0.,-1.,0.)); +#4991 = ADVANCED_FACE('',(#4992),#5019,.T.); +#4992 = FACE_BOUND('',#4993,.T.); +#4993 = EDGE_LOOP('',(#4994,#4995,#4996,#5004,#5012,#5018)); +#4994 = ORIENTED_EDGE('',*,*,#4488,.F.); +#4995 = ORIENTED_EDGE('',*,*,#4519,.T.); +#4996 = ORIENTED_EDGE('',*,*,#4997,.T.); +#4997 = EDGE_CURVE('',#4512,#4998,#5000,.T.); +#4998 = VERTEX_POINT('',#4999); +#4999 = CARTESIAN_POINT('',(-19.9,-1.163662554812E+03,123.86376214781)); +#5000 = LINE('',#5001,#5002); +#5001 = CARTESIAN_POINT('',(-19.9,-1.160255084405E+03,137.94954479362)); +#5002 = VECTOR('',#5003,1.); +#5003 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5004 = ORIENTED_EDGE('',*,*,#5005,.T.); +#5005 = EDGE_CURVE('',#4998,#5006,#5008,.T.); +#5006 = VERTEX_POINT('',#5007); +#5007 = CARTESIAN_POINT('',(-19.9,-1.167855516655E+03,138.15219926067)); +#5008 = LINE('',#5009,#5010); +#5009 = CARTESIAN_POINT('',(-19.9,-1.163662554812E+03,123.86376214781)); +#5010 = VECTOR('',#5011,1.); +#5011 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5012 = ORIENTED_EDGE('',*,*,#5013,.T.); +#5013 = EDGE_CURVE('',#5006,#4121,#5014,.T.); +#5014 = LINE('',#5015,#5016); +#5015 = CARTESIAN_POINT('',(-19.9,-1.167855516655E+03,138.15219926067)); +#5016 = VECTOR('',#5017,1.); +#5017 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5018 = ORIENTED_EDGE('',*,*,#4128,.F.); +#5019 = PLANE('',#5020); +#5020 = AXIS2_PLACEMENT_3D('',#5021,#5022,#5023); +#5021 = CARTESIAN_POINT('',(-19.9,-1.163860253502E+03,132.80086049584)); +#5022 = DIRECTION('',(-1.,-0.,-0.)); +#5023 = DIRECTION('',(0.,-1.,0.)); +#5024 = ADVANCED_FACE('',(#5025),#5043,.T.); +#5025 = FACE_BOUND('',#5026,.T.); +#5026 = EDGE_LOOP('',(#5027,#5028,#5036,#5042)); +#5027 = ORIENTED_EDGE('',*,*,#5013,.F.); +#5028 = ORIENTED_EDGE('',*,*,#5029,.T.); +#5029 = EDGE_CURVE('',#5006,#5030,#5032,.T.); +#5030 = VERTEX_POINT('',#5031); +#5031 = CARTESIAN_POINT('',(-9.9,-1.167855516655E+03,138.15219926067)); +#5032 = LINE('',#5033,#5034); +#5033 = CARTESIAN_POINT('',(-19.9,-1.167855516655E+03,138.15219926067)); +#5034 = VECTOR('',#5035,1.); +#5035 = DIRECTION('',(1.,0.,0.)); +#5036 = ORIENTED_EDGE('',*,*,#5037,.T.); +#5037 = EDGE_CURVE('',#5030,#4113,#5038,.T.); +#5038 = LINE('',#5039,#5040); +#5039 = CARTESIAN_POINT('',(-9.9,-1.167855516655E+03,138.15219926067)); +#5040 = VECTOR('',#5041,1.); +#5041 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5042 = ORIENTED_EDGE('',*,*,#4120,.F.); +#5043 = PLANE('',#5044); +#5044 = AXIS2_PLACEMENT_3D('',#5045,#5046,#5047); +#5045 = CARTESIAN_POINT('',(-19.9,-1.167855516655E+03,138.15219926067)); +#5046 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5047 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5048 = ADVANCED_FACE('',(#5049),#5069,.F.); +#5049 = FACE_BOUND('',#5050,.F.); +#5050 = EDGE_LOOP('',(#5051,#5052,#5053,#5061,#5067,#5068)); +#5051 = ORIENTED_EDGE('',*,*,#4534,.F.); +#5052 = ORIENTED_EDGE('',*,*,#4503,.T.); +#5053 = ORIENTED_EDGE('',*,*,#5054,.T.); +#5054 = EDGE_CURVE('',#4504,#5055,#5057,.T.); +#5055 = VERTEX_POINT('',#5056); +#5056 = CARTESIAN_POINT('',(-9.9,-1.163662554812E+03,123.86376214781)); +#5057 = LINE('',#5058,#5059); +#5058 = CARTESIAN_POINT('',(-9.9,-1.160255084405E+03,137.94954479362)); +#5059 = VECTOR('',#5060,1.); +#5060 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5061 = ORIENTED_EDGE('',*,*,#5062,.T.); +#5062 = EDGE_CURVE('',#5055,#5030,#5063,.T.); +#5063 = LINE('',#5064,#5065); +#5064 = CARTESIAN_POINT('',(-9.9,-1.163662554812E+03,123.86376214781)); +#5065 = VECTOR('',#5066,1.); +#5066 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5067 = ORIENTED_EDGE('',*,*,#5037,.T.); +#5068 = ORIENTED_EDGE('',*,*,#4112,.F.); +#5069 = PLANE('',#5070); +#5070 = AXIS2_PLACEMENT_3D('',#5071,#5072,#5073); +#5071 = CARTESIAN_POINT('',(-9.9,-1.163860253502E+03,132.80086049584)); +#5072 = DIRECTION('',(-1.,-0.,-0.)); +#5073 = DIRECTION('',(0.,-1.,0.)); +#5074 = ADVANCED_FACE('',(#5075),#5102,.T.); +#5075 = FACE_BOUND('',#5076,.T.); +#5076 = EDGE_LOOP('',(#5077,#5078,#5079,#5087,#5095,#5101)); +#5077 = ORIENTED_EDGE('',*,*,#4541,.F.); +#5078 = ORIENTED_EDGE('',*,*,#4572,.T.); +#5079 = ORIENTED_EDGE('',*,*,#5080,.T.); +#5080 = EDGE_CURVE('',#4565,#5081,#5083,.T.); +#5081 = VERTEX_POINT('',#5082); +#5082 = CARTESIAN_POINT('',(0.1,-1.163662554812E+03,123.86376214781)); +#5083 = LINE('',#5084,#5085); +#5084 = CARTESIAN_POINT('',(0.1,-1.160255084405E+03,137.94954479362)); +#5085 = VECTOR('',#5086,1.); +#5086 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5087 = ORIENTED_EDGE('',*,*,#5088,.T.); +#5088 = EDGE_CURVE('',#5081,#5089,#5091,.T.); +#5089 = VERTEX_POINT('',#5090); +#5090 = CARTESIAN_POINT('',(0.1,-1.167855516655E+03,138.15219926067)); +#5091 = LINE('',#5092,#5093); +#5092 = CARTESIAN_POINT('',(0.1,-1.163662554812E+03,123.86376214781)); +#5093 = VECTOR('',#5094,1.); +#5094 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5095 = ORIENTED_EDGE('',*,*,#5096,.T.); +#5096 = EDGE_CURVE('',#5089,#4089,#5097,.T.); +#5097 = LINE('',#5098,#5099); +#5098 = CARTESIAN_POINT('',(0.1,-1.167855516655E+03,138.15219926067)); +#5099 = VECTOR('',#5100,1.); +#5100 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5101 = ORIENTED_EDGE('',*,*,#4096,.F.); +#5102 = PLANE('',#5103); +#5103 = AXIS2_PLACEMENT_3D('',#5104,#5105,#5106); +#5104 = CARTESIAN_POINT('',(0.1,-1.163860253502E+03,132.80086049584)); +#5105 = DIRECTION('',(-1.,-0.,-0.)); +#5106 = DIRECTION('',(0.,-1.,0.)); +#5107 = ADVANCED_FACE('',(#5108),#5126,.T.); +#5108 = FACE_BOUND('',#5109,.T.); +#5109 = EDGE_LOOP('',(#5110,#5111,#5119,#5125)); +#5110 = ORIENTED_EDGE('',*,*,#5096,.F.); +#5111 = ORIENTED_EDGE('',*,*,#5112,.T.); +#5112 = EDGE_CURVE('',#5089,#5113,#5115,.T.); +#5113 = VERTEX_POINT('',#5114); +#5114 = CARTESIAN_POINT('',(10.1,-1.167855516655E+03,138.15219926067)); +#5115 = LINE('',#5116,#5117); +#5116 = CARTESIAN_POINT('',(0.1,-1.167855516655E+03,138.15219926067)); +#5117 = VECTOR('',#5118,1.); +#5118 = DIRECTION('',(1.,0.,0.)); +#5119 = ORIENTED_EDGE('',*,*,#5120,.T.); +#5120 = EDGE_CURVE('',#5113,#4081,#5121,.T.); +#5121 = LINE('',#5122,#5123); +#5122 = CARTESIAN_POINT('',(10.1,-1.167855516655E+03,138.15219926067)); +#5123 = VECTOR('',#5124,1.); +#5124 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5125 = ORIENTED_EDGE('',*,*,#4088,.F.); +#5126 = PLANE('',#5127); +#5127 = AXIS2_PLACEMENT_3D('',#5128,#5129,#5130); +#5128 = CARTESIAN_POINT('',(0.1,-1.167855516655E+03,138.15219926067)); +#5129 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5130 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5131 = ADVANCED_FACE('',(#5132),#5152,.F.); +#5132 = FACE_BOUND('',#5133,.F.); +#5133 = EDGE_LOOP('',(#5134,#5135,#5136,#5144,#5150,#5151)); +#5134 = ORIENTED_EDGE('',*,*,#4587,.F.); +#5135 = ORIENTED_EDGE('',*,*,#4556,.T.); +#5136 = ORIENTED_EDGE('',*,*,#5137,.T.); +#5137 = EDGE_CURVE('',#4557,#5138,#5140,.T.); +#5138 = VERTEX_POINT('',#5139); +#5139 = CARTESIAN_POINT('',(10.1,-1.163662554812E+03,123.86376214781)); +#5140 = LINE('',#5141,#5142); +#5141 = CARTESIAN_POINT('',(10.1,-1.160255084405E+03,137.94954479362)); +#5142 = VECTOR('',#5143,1.); +#5143 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5144 = ORIENTED_EDGE('',*,*,#5145,.T.); +#5145 = EDGE_CURVE('',#5138,#5113,#5146,.T.); +#5146 = LINE('',#5147,#5148); +#5147 = CARTESIAN_POINT('',(10.1,-1.163662554812E+03,123.86376214781)); +#5148 = VECTOR('',#5149,1.); +#5149 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5150 = ORIENTED_EDGE('',*,*,#5120,.T.); +#5151 = ORIENTED_EDGE('',*,*,#4080,.F.); +#5152 = PLANE('',#5153); +#5153 = AXIS2_PLACEMENT_3D('',#5154,#5155,#5156); +#5154 = CARTESIAN_POINT('',(10.1,-1.163860253502E+03,132.80086049584)); +#5155 = DIRECTION('',(-1.,-0.,-0.)); +#5156 = DIRECTION('',(0.,-1.,0.)); +#5157 = ADVANCED_FACE('',(#5158),#5185,.T.); +#5158 = FACE_BOUND('',#5159,.T.); +#5159 = EDGE_LOOP('',(#5160,#5161,#5162,#5170,#5178,#5184)); +#5160 = ORIENTED_EDGE('',*,*,#4594,.F.); +#5161 = ORIENTED_EDGE('',*,*,#4625,.T.); +#5162 = ORIENTED_EDGE('',*,*,#5163,.T.); +#5163 = EDGE_CURVE('',#4618,#5164,#5166,.T.); +#5164 = VERTEX_POINT('',#5165); +#5165 = CARTESIAN_POINT('',(20.1,-1.163662554812E+03,123.86376214781)); +#5166 = LINE('',#5167,#5168); +#5167 = CARTESIAN_POINT('',(20.1,-1.160255084405E+03,137.94954479362)); +#5168 = VECTOR('',#5169,1.); +#5169 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5170 = ORIENTED_EDGE('',*,*,#5171,.T.); +#5171 = EDGE_CURVE('',#5164,#5172,#5174,.T.); +#5172 = VERTEX_POINT('',#5173); +#5173 = CARTESIAN_POINT('',(20.1,-1.167855516655E+03,138.15219926067)); +#5174 = LINE('',#5175,#5176); +#5175 = CARTESIAN_POINT('',(20.1,-1.163662554812E+03,123.86376214781)); +#5176 = VECTOR('',#5177,1.); +#5177 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5178 = ORIENTED_EDGE('',*,*,#5179,.T.); +#5179 = EDGE_CURVE('',#5172,#4057,#5180,.T.); +#5180 = LINE('',#5181,#5182); +#5181 = CARTESIAN_POINT('',(20.1,-1.167855516655E+03,138.15219926067)); +#5182 = VECTOR('',#5183,1.); +#5183 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5184 = ORIENTED_EDGE('',*,*,#4064,.F.); +#5185 = PLANE('',#5186); +#5186 = AXIS2_PLACEMENT_3D('',#5187,#5188,#5189); +#5187 = CARTESIAN_POINT('',(20.1,-1.163860253502E+03,132.80086049584)); +#5188 = DIRECTION('',(-1.,-0.,-0.)); +#5189 = DIRECTION('',(0.,-1.,0.)); +#5190 = ADVANCED_FACE('',(#5191),#5209,.T.); +#5191 = FACE_BOUND('',#5192,.T.); +#5192 = EDGE_LOOP('',(#5193,#5194,#5202,#5208)); +#5193 = ORIENTED_EDGE('',*,*,#5179,.F.); +#5194 = ORIENTED_EDGE('',*,*,#5195,.T.); +#5195 = EDGE_CURVE('',#5172,#5196,#5198,.T.); +#5196 = VERTEX_POINT('',#5197); +#5197 = CARTESIAN_POINT('',(30.1,-1.167855516655E+03,138.15219926067)); +#5198 = LINE('',#5199,#5200); +#5199 = CARTESIAN_POINT('',(20.1,-1.167855516655E+03,138.15219926067)); +#5200 = VECTOR('',#5201,1.); +#5201 = DIRECTION('',(1.,0.,0.)); +#5202 = ORIENTED_EDGE('',*,*,#5203,.T.); +#5203 = EDGE_CURVE('',#5196,#4049,#5204,.T.); +#5204 = LINE('',#5205,#5206); +#5205 = CARTESIAN_POINT('',(30.1,-1.167855516655E+03,138.15219926067)); +#5206 = VECTOR('',#5207,1.); +#5207 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5208 = ORIENTED_EDGE('',*,*,#4056,.F.); +#5209 = PLANE('',#5210); +#5210 = AXIS2_PLACEMENT_3D('',#5211,#5212,#5213); +#5211 = CARTESIAN_POINT('',(20.1,-1.167855516655E+03,138.15219926067)); +#5212 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5213 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5214 = ADVANCED_FACE('',(#5215),#5235,.F.); +#5215 = FACE_BOUND('',#5216,.F.); +#5216 = EDGE_LOOP('',(#5217,#5218,#5219,#5227,#5233,#5234)); +#5217 = ORIENTED_EDGE('',*,*,#4640,.F.); +#5218 = ORIENTED_EDGE('',*,*,#4609,.T.); +#5219 = ORIENTED_EDGE('',*,*,#5220,.T.); +#5220 = EDGE_CURVE('',#4610,#5221,#5223,.T.); +#5221 = VERTEX_POINT('',#5222); +#5222 = CARTESIAN_POINT('',(30.1,-1.163662554812E+03,123.86376214781)); +#5223 = LINE('',#5224,#5225); +#5224 = CARTESIAN_POINT('',(30.1,-1.160255084405E+03,137.94954479362)); +#5225 = VECTOR('',#5226,1.); +#5226 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5227 = ORIENTED_EDGE('',*,*,#5228,.T.); +#5228 = EDGE_CURVE('',#5221,#5196,#5229,.T.); +#5229 = LINE('',#5230,#5231); +#5230 = CARTESIAN_POINT('',(30.1,-1.163662554812E+03,123.86376214781)); +#5231 = VECTOR('',#5232,1.); +#5232 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5233 = ORIENTED_EDGE('',*,*,#5203,.T.); +#5234 = ORIENTED_EDGE('',*,*,#4048,.F.); +#5235 = PLANE('',#5236); +#5236 = AXIS2_PLACEMENT_3D('',#5237,#5238,#5239); +#5237 = CARTESIAN_POINT('',(30.1,-1.163860253502E+03,132.80086049584)); +#5238 = DIRECTION('',(-1.,-0.,-0.)); +#5239 = DIRECTION('',(0.,-1.,0.)); +#5240 = ADVANCED_FACE('',(#5241),#5268,.T.); +#5241 = FACE_BOUND('',#5242,.T.); +#5242 = EDGE_LOOP('',(#5243,#5244,#5245,#5253,#5261,#5267)); +#5243 = ORIENTED_EDGE('',*,*,#4647,.F.); +#5244 = ORIENTED_EDGE('',*,*,#4678,.T.); +#5245 = ORIENTED_EDGE('',*,*,#5246,.T.); +#5246 = EDGE_CURVE('',#4671,#5247,#5249,.T.); +#5247 = VERTEX_POINT('',#5248); +#5248 = CARTESIAN_POINT('',(40.1,-1.163662554812E+03,123.86376214781)); +#5249 = LINE('',#5250,#5251); +#5250 = CARTESIAN_POINT('',(40.1,-1.160255084405E+03,137.94954479362)); +#5251 = VECTOR('',#5252,1.); +#5252 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5253 = ORIENTED_EDGE('',*,*,#5254,.T.); +#5254 = EDGE_CURVE('',#5247,#5255,#5257,.T.); +#5255 = VERTEX_POINT('',#5256); +#5256 = CARTESIAN_POINT('',(40.1,-1.167855516655E+03,138.15219926067)); +#5257 = LINE('',#5258,#5259); +#5258 = CARTESIAN_POINT('',(40.1,-1.163662554812E+03,123.86376214781)); +#5259 = VECTOR('',#5260,1.); +#5260 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5261 = ORIENTED_EDGE('',*,*,#5262,.T.); +#5262 = EDGE_CURVE('',#5255,#4025,#5263,.T.); +#5263 = LINE('',#5264,#5265); +#5264 = CARTESIAN_POINT('',(40.1,-1.167855516655E+03,138.15219926067)); +#5265 = VECTOR('',#5266,1.); +#5266 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5267 = ORIENTED_EDGE('',*,*,#4032,.F.); +#5268 = PLANE('',#5269); +#5269 = AXIS2_PLACEMENT_3D('',#5270,#5271,#5272); +#5270 = CARTESIAN_POINT('',(40.1,-1.163860253502E+03,132.80086049584)); +#5271 = DIRECTION('',(-1.,-0.,-0.)); +#5272 = DIRECTION('',(0.,-1.,0.)); +#5273 = ADVANCED_FACE('',(#5274),#5292,.T.); +#5274 = FACE_BOUND('',#5275,.T.); +#5275 = EDGE_LOOP('',(#5276,#5277,#5285,#5291)); +#5276 = ORIENTED_EDGE('',*,*,#5262,.F.); +#5277 = ORIENTED_EDGE('',*,*,#5278,.T.); +#5278 = EDGE_CURVE('',#5255,#5279,#5281,.T.); +#5279 = VERTEX_POINT('',#5280); +#5280 = CARTESIAN_POINT('',(50.1,-1.167855516655E+03,138.15219926067)); +#5281 = LINE('',#5282,#5283); +#5282 = CARTESIAN_POINT('',(40.1,-1.167855516655E+03,138.15219926067)); +#5283 = VECTOR('',#5284,1.); +#5284 = DIRECTION('',(1.,0.,0.)); +#5285 = ORIENTED_EDGE('',*,*,#5286,.T.); +#5286 = EDGE_CURVE('',#5279,#4017,#5287,.T.); +#5287 = LINE('',#5288,#5289); +#5288 = CARTESIAN_POINT('',(50.1,-1.167855516655E+03,138.15219926067)); +#5289 = VECTOR('',#5290,1.); +#5290 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5291 = ORIENTED_EDGE('',*,*,#4024,.F.); +#5292 = PLANE('',#5293); +#5293 = AXIS2_PLACEMENT_3D('',#5294,#5295,#5296); +#5294 = CARTESIAN_POINT('',(40.1,-1.167855516655E+03,138.15219926067)); +#5295 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5296 = DIRECTION('',(0.,0.959538377832,0.281577878158)); +#5297 = ADVANCED_FACE('',(#5298),#5318,.F.); +#5298 = FACE_BOUND('',#5299,.F.); +#5299 = EDGE_LOOP('',(#5300,#5301,#5302,#5310,#5316,#5317)); +#5300 = ORIENTED_EDGE('',*,*,#4693,.F.); +#5301 = ORIENTED_EDGE('',*,*,#4662,.T.); +#5302 = ORIENTED_EDGE('',*,*,#5303,.T.); +#5303 = EDGE_CURVE('',#4663,#5304,#5306,.T.); +#5304 = VERTEX_POINT('',#5305); +#5305 = CARTESIAN_POINT('',(50.1,-1.163662554812E+03,123.86376214781)); +#5306 = LINE('',#5307,#5308); +#5307 = CARTESIAN_POINT('',(50.1,-1.160255084405E+03,137.94954479362)); +#5308 = VECTOR('',#5309,1.); +#5309 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5310 = ORIENTED_EDGE('',*,*,#5311,.T.); +#5311 = EDGE_CURVE('',#5304,#5279,#5312,.T.); +#5312 = LINE('',#5313,#5314); +#5313 = CARTESIAN_POINT('',(50.1,-1.163662554812E+03,123.86376214781)); +#5314 = VECTOR('',#5315,1.); +#5315 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#5316 = ORIENTED_EDGE('',*,*,#5286,.T.); +#5317 = ORIENTED_EDGE('',*,*,#4016,.F.); +#5318 = PLANE('',#5319); +#5319 = AXIS2_PLACEMENT_3D('',#5320,#5321,#5322); +#5320 = CARTESIAN_POINT('',(50.1,-1.163860253502E+03,132.80086049584)); +#5321 = DIRECTION('',(-1.,-0.,-0.)); +#5322 = DIRECTION('',(0.,-1.,0.)); +#5323 = ADVANCED_FACE('',(#5324),#5343,.T.); +#5324 = FACE_BOUND('',#5325,.F.); +#5325 = EDGE_LOOP('',(#5326,#5334,#5335,#5336)); +#5326 = ORIENTED_EDGE('',*,*,#5327,.T.); +#5327 = EDGE_CURVE('',#5328,#4701,#5330,.T.); +#5328 = VERTEX_POINT('',#5329); +#5329 = CARTESIAN_POINT('',(56.6,-1.186635384657E+03,214.02422421171)); +#5330 = LINE('',#5331,#5332); +#5331 = CARTESIAN_POINT('',(56.6,-1.186635384657E+03,214.02422421171)); +#5332 = VECTOR('',#5333,1.); +#5333 = DIRECTION('',(0.,0.281577578828,-0.95953846567)); +#5334 = ORIENTED_EDGE('',*,*,#4700,.F.); +#5335 = ORIENTED_EDGE('',*,*,#4000,.F.); +#5336 = ORIENTED_EDGE('',*,*,#5337,.T.); +#5337 = EDGE_CURVE('',#3993,#5328,#5338,.T.); +#5338 = CIRCLE('',#5339,1.5); +#5339 = AXIS2_PLACEMENT_3D('',#5340,#5341,#5342); +#5340 = CARTESIAN_POINT('',(55.1,-1.186635384657E+03,214.02422421171)); +#5341 = DIRECTION('',(0.,-0.281577578828,0.95953846567)); +#5342 = DIRECTION('',(-0.,-0.95953846567,-0.281577578828)); +#5343 = CYLINDRICAL_SURFACE('',#5344,1.5); +#5344 = AXIS2_PLACEMENT_3D('',#5345,#5346,#5347); +#5345 = CARTESIAN_POINT('',(55.1,-1.186635384657E+03,214.02422421171)); +#5346 = DIRECTION('',(-5.E-16,0.281577578828,-0.95953846567)); +#5347 = DIRECTION('',(-1.,-1.79530598053E-16,4.684004080656E-16)); +#5348 = ADVANCED_FACE('',(#5349),#5376,.T.); +#5349 = FACE_BOUND('',#5350,.F.); +#5350 = EDGE_LOOP('',(#5351,#5361,#5368,#5369)); +#5351 = ORIENTED_EDGE('',*,*,#5352,.T.); +#5352 = EDGE_CURVE('',#5353,#5355,#5357,.T.); +#5353 = VERTEX_POINT('',#5354); +#5354 = CARTESIAN_POINT('',(-44.9,-1.181801569624E+03,227.3079250436)); +#5355 = VERTEX_POINT('',#5356); +#5356 = CARTESIAN_POINT('',(55.1,-1.181801569624E+03,227.3079250436)); +#5357 = LINE('',#5358,#5359); +#5358 = CARTESIAN_POINT('',(-44.9,-1.181801569624E+03,227.3079250436)); +#5359 = VECTOR('',#5360,1.); +#5360 = DIRECTION('',(1.,0.,0.)); +#5361 = ORIENTED_EDGE('',*,*,#5362,.T.); +#5362 = EDGE_CURVE('',#5355,#3993,#5363,.T.); +#5363 = CIRCLE('',#5364,11.5); +#5364 = AXIS2_PLACEMENT_3D('',#5365,#5366,#5367); +#5365 = CARTESIAN_POINT('',(55.1,-1.17704E+03,216.84)); +#5366 = DIRECTION('',(1.,0.,0.)); +#5367 = DIRECTION('',(0.,1.,0.)); +#5368 = ORIENTED_EDGE('',*,*,#3992,.F.); +#5369 = ORIENTED_EDGE('',*,*,#5370,.F.); +#5370 = EDGE_CURVE('',#5353,#3985,#5371,.T.); +#5371 = CIRCLE('',#5372,11.5); +#5372 = AXIS2_PLACEMENT_3D('',#5373,#5374,#5375); +#5373 = CARTESIAN_POINT('',(-44.9,-1.17704E+03,216.84)); +#5374 = DIRECTION('',(1.,0.,0.)); +#5375 = DIRECTION('',(0.,1.,0.)); +#5376 = CYLINDRICAL_SURFACE('',#5377,11.5); +#5377 = AXIS2_PLACEMENT_3D('',#5378,#5379,#5380); +#5378 = CARTESIAN_POINT('',(-44.9,-1.17704E+03,216.84)); +#5379 = DIRECTION('',(-1.,-0.,-0.)); +#5380 = DIRECTION('',(0.,1.,0.)); +#5381 = ADVANCED_FACE('',(#5382),#5394,.T.); +#5382 = FACE_BOUND('',#5383,.T.); +#5383 = EDGE_LOOP('',(#5384,#5385,#5392,#5393)); +#5384 = ORIENTED_EDGE('',*,*,#5370,.F.); +#5385 = ORIENTED_EDGE('',*,*,#5386,.T.); +#5386 = EDGE_CURVE('',#5353,#4204,#5387,.T.); +#5387 = CIRCLE('',#5388,1.5); +#5388 = AXIS2_PLACEMENT_3D('',#5389,#5390,#5391); +#5389 = CARTESIAN_POINT('',(-44.9,-1.181180495325E+03,225.94254351617)); +#5390 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#5391 = DIRECTION('',(1.,0.,0.)); +#5392 = ORIENTED_EDGE('',*,*,#4211,.T.); +#5393 = ORIENTED_EDGE('',*,*,#4178,.F.); +#5394 = TOROIDAL_SURFACE('',#5395,10.,1.5); +#5395 = AXIS2_PLACEMENT_3D('',#5396,#5397,#5398); +#5396 = CARTESIAN_POINT('',(-44.9,-1.17704E+03,216.84)); +#5397 = DIRECTION('',(-1.,-0.,-0.)); +#5398 = DIRECTION('',(0.,1.,0.)); +#5399 = ADVANCED_FACE('',(#5400),#5427,.T.); +#5400 = FACE_BOUND('',#5401,.T.); +#5401 = EDGE_LOOP('',(#5402,#5412,#5419,#5420)); +#5402 = ORIENTED_EDGE('',*,*,#5403,.F.); +#5403 = EDGE_CURVE('',#5404,#5406,#5408,.T.); +#5404 = VERTEX_POINT('',#5405); +#5405 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5406 = VERTEX_POINT('',#5407); +#5407 = CARTESIAN_POINT('',(-44.9,-1.161692179364E+03,236.23269858292)); +#5408 = LINE('',#5409,#5410); +#5409 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5410 = VECTOR('',#5411,1.); +#5411 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5412 = ORIENTED_EDGE('',*,*,#5413,.T.); +#5413 = EDGE_CURVE('',#5404,#4194,#5414,.T.); +#5414 = CIRCLE('',#5415,1.5); +#5415 = AXIS2_PLACEMENT_3D('',#5416,#5417,#5418); +#5416 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5417 = DIRECTION('',(-8.3E-16,-0.995132388616,9.85470909115E-02)); +#5418 = DIRECTION('',(1.,-8.241165962386E-16,1.004076629285E-16)); +#5419 = ORIENTED_EDGE('',*,*,#4193,.T.); +#5420 = ORIENTED_EDGE('',*,*,#5421,.F.); +#5421 = EDGE_CURVE('',#5406,#4196,#5422,.T.); +#5422 = CIRCLE('',#5423,1.5); +#5423 = AXIS2_PLACEMENT_3D('',#5424,#5425,#5426); +#5424 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#5425 = DIRECTION('',(-8.3E-16,-0.995132388616,9.85470909115E-02)); +#5426 = DIRECTION('',(1.,-8.241165962386E-16,1.004076629285E-16)); +#5427 = CYLINDRICAL_SURFACE('',#5428,1.5); +#5428 = AXIS2_PLACEMENT_3D('',#5429,#5430,#5431); +#5429 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5430 = DIRECTION('',(-8.3E-16,-0.995132388616,9.85470909115E-02)); +#5431 = DIRECTION('',(1.,-8.241165962386E-16,1.004076629285E-16)); +#5432 = ADVANCED_FACE('',(#5433),#5452,.T.); +#5433 = FACE_BOUND('',#5434,.T.); +#5434 = EDGE_LOOP('',(#5435,#5443,#5450,#5451)); +#5435 = ORIENTED_EDGE('',*,*,#5436,.F.); +#5436 = EDGE_CURVE('',#5437,#5353,#5439,.T.); +#5437 = VERTEX_POINT('',#5438); +#5438 = CARTESIAN_POINT('',(-44.9,-1.162461074299E+03,236.10538152742)); +#5439 = LINE('',#5440,#5441); +#5440 = CARTESIAN_POINT('',(-44.9,-1.162461074299E+03,236.10538152742)); +#5441 = VECTOR('',#5442,1.); +#5442 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#5443 = ORIENTED_EDGE('',*,*,#5444,.T.); +#5444 = EDGE_CURVE('',#5437,#4196,#5445,.T.); +#5445 = CIRCLE('',#5446,1.5); +#5446 = AXIS2_PLACEMENT_3D('',#5447,#5448,#5449); +#5447 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#5448 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#5449 = DIRECTION('',(1.,0.,0.)); +#5450 = ORIENTED_EDGE('',*,*,#4203,.T.); +#5451 = ORIENTED_EDGE('',*,*,#5386,.F.); +#5452 = CYLINDRICAL_SURFACE('',#5453,1.5); +#5453 = AXIS2_PLACEMENT_3D('',#5454,#5455,#5456); +#5454 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#5455 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#5456 = DIRECTION('',(1.,0.,0.)); +#5457 = ADVANCED_FACE('',(#5458),#5469,.T.); +#5458 = FACE_BOUND('',#5459,.T.); +#5459 = EDGE_LOOP('',(#5460,#5467,#5468)); +#5460 = ORIENTED_EDGE('',*,*,#5461,.F.); +#5461 = EDGE_CURVE('',#4236,#5404,#5462,.T.); +#5462 = CIRCLE('',#5463,1.5); +#5463 = AXIS2_PLACEMENT_3D('',#5464,#5465,#5466); +#5464 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5465 = DIRECTION('',(1.,-0.,0.)); +#5466 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#5467 = ORIENTED_EDGE('',*,*,#4235,.T.); +#5468 = ORIENTED_EDGE('',*,*,#5413,.F.); +#5469 = SPHERICAL_SURFACE('',#5470,1.5); +#5470 = AXIS2_PLACEMENT_3D('',#5471,#5472,#5473); +#5471 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5472 = DIRECTION('',(-0.609242933207,0.232462644377,-0.758145215184)); +#5473 = DIRECTION('',(0.584952990716,-0.51376326958,-0.627596447953)); +#5474 = ADVANCED_FACE('',(#5475),#5486,.T.); +#5475 = FACE_BOUND('',#5476,.T.); +#5476 = EDGE_LOOP('',(#5477,#5478,#5479,#5485)); +#5477 = ORIENTED_EDGE('',*,*,#4268,.F.); +#5478 = ORIENTED_EDGE('',*,*,#5461,.T.); +#5479 = ORIENTED_EDGE('',*,*,#5480,.T.); +#5480 = EDGE_CURVE('',#5404,#4320,#5481,.T.); +#5481 = LINE('',#5482,#5483); +#5482 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5483 = VECTOR('',#5484,1.); +#5484 = DIRECTION('',(1.,0.,0.)); +#5485 = ORIENTED_EDGE('',*,*,#4327,.F.); +#5486 = CYLINDRICAL_SURFACE('',#5487,1.5); +#5487 = AXIS2_PLACEMENT_3D('',#5488,#5489,#5490); +#5488 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5489 = DIRECTION('',(1.,0.,0.)); +#5490 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#5491 = ADVANCED_FACE('',(#5492),#5558,.F.); +#5492 = FACE_BOUND('',#5493,.F.); +#5493 = EDGE_LOOP('',(#5494,#5495,#5496,#5497,#5503,#5504,#5512,#5520, + #5528,#5536,#5544,#5552)); +#5494 = ORIENTED_EDGE('',*,*,#5403,.F.); +#5495 = ORIENTED_EDGE('',*,*,#5480,.T.); +#5496 = ORIENTED_EDGE('',*,*,#4319,.T.); +#5497 = ORIENTED_EDGE('',*,*,#5498,.F.); +#5498 = EDGE_CURVE('',#4810,#4312,#5499,.T.); +#5499 = LINE('',#5500,#5501); +#5500 = CARTESIAN_POINT('',(-27.4,-1.156973095081E+03,235.76537178991)); +#5501 = VECTOR('',#5502,1.); +#5502 = DIRECTION('',(-1.,0.,0.)); +#5503 = ORIENTED_EDGE('',*,*,#4817,.F.); +#5504 = ORIENTED_EDGE('',*,*,#5505,.T.); +#5505 = EDGE_CURVE('',#4818,#5506,#5508,.T.); +#5506 = VERTEX_POINT('',#5507); +#5507 = CARTESIAN_POINT('',(20.1,-1.110192179364E+03,231.13269858292)); +#5508 = LINE('',#5509,#5510); +#5509 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5510 = VECTOR('',#5511,1.); +#5511 = DIRECTION('',(1.,0.,0.)); +#5512 = ORIENTED_EDGE('',*,*,#5513,.T.); +#5513 = EDGE_CURVE('',#5506,#5514,#5516,.T.); +#5514 = VERTEX_POINT('',#5515); +#5515 = CARTESIAN_POINT('',(20.1,-1.156973095081E+03,235.76537178991)); +#5516 = LINE('',#5517,#5518); +#5517 = CARTESIAN_POINT('',(20.1,-1.111788015875E+03,231.29073287826)); +#5518 = VECTOR('',#5519,1.); +#5519 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5520 = ORIENTED_EDGE('',*,*,#5521,.T.); +#5521 = EDGE_CURVE('',#5514,#5522,#5524,.T.); +#5522 = VERTEX_POINT('',#5523); +#5523 = CARTESIAN_POINT('',(24.1,-1.156973095081E+03,235.76537178991)); +#5524 = LINE('',#5525,#5526); +#5525 = CARTESIAN_POINT('',(-12.4,-1.156973095081E+03,235.76537178991)); +#5526 = VECTOR('',#5527,1.); +#5527 = DIRECTION('',(1.,0.,0.)); +#5528 = ORIENTED_EDGE('',*,*,#5529,.F.); +#5529 = EDGE_CURVE('',#5530,#5522,#5532,.T.); +#5530 = VERTEX_POINT('',#5531); +#5531 = CARTESIAN_POINT('',(24.1,-1.110192179364E+03,231.13269858292)); +#5532 = LINE('',#5533,#5534); +#5533 = CARTESIAN_POINT('',(24.1,-1.111788015875E+03,231.29073287826)); +#5534 = VECTOR('',#5535,1.); +#5535 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5536 = ORIENTED_EDGE('',*,*,#5537,.T.); +#5537 = EDGE_CURVE('',#5530,#5538,#5540,.T.); +#5538 = VERTEX_POINT('',#5539); +#5539 = CARTESIAN_POINT('',(55.1,-1.110192179364E+03,231.13269858292)); +#5540 = LINE('',#5541,#5542); +#5541 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5542 = VECTOR('',#5543,1.); +#5543 = DIRECTION('',(1.,0.,0.)); +#5544 = ORIENTED_EDGE('',*,*,#5545,.T.); +#5545 = EDGE_CURVE('',#5538,#5546,#5548,.T.); +#5546 = VERTEX_POINT('',#5547); +#5547 = CARTESIAN_POINT('',(55.1,-1.161692179364E+03,236.23269858292)); +#5548 = LINE('',#5549,#5550); +#5549 = CARTESIAN_POINT('',(55.1,-1.110192179364E+03,231.13269858292)); +#5550 = VECTOR('',#5551,1.); +#5551 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5552 = ORIENTED_EDGE('',*,*,#5553,.F.); +#5553 = EDGE_CURVE('',#5406,#5546,#5554,.T.); +#5554 = LINE('',#5555,#5556); +#5555 = CARTESIAN_POINT('',(-44.9,-1.161692179364E+03,236.23269858292)); +#5556 = VECTOR('',#5557,1.); +#5557 = DIRECTION('',(1.,0.,0.)); +#5558 = PLANE('',#5559); +#5559 = AXIS2_PLACEMENT_3D('',#5560,#5561,#5562); +#5560 = CARTESIAN_POINT('',(-44.9,-1.110192179364E+03,231.13269858292)); +#5561 = DIRECTION('',(0.,-9.85470909115E-02,-0.995132388616)); +#5562 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5563 = ADVANCED_FACE('',(#5564),#5575,.T.); +#5564 = FACE_BOUND('',#5565,.T.); +#5565 = EDGE_LOOP('',(#5566,#5567,#5573,#5574)); +#5566 = ORIENTED_EDGE('',*,*,#4809,.F.); +#5567 = ORIENTED_EDGE('',*,*,#5568,.T.); +#5568 = EDGE_CURVE('',#4801,#4303,#5569,.T.); +#5569 = LINE('',#5570,#5571); +#5570 = CARTESIAN_POINT('',(-9.9,-1.099423892855E+03,247.88998270397)); +#5571 = VECTOR('',#5572,1.); +#5572 = DIRECTION('',(-1.,-0.,-0.)); +#5573 = ORIENTED_EDGE('',*,*,#4311,.T.); +#5574 = ORIENTED_EDGE('',*,*,#5498,.F.); +#5575 = PLANE('',#5576); +#5576 = AXIS2_PLACEMENT_3D('',#5577,#5578,#5579); +#5577 = CARTESIAN_POINT('',(-9.9,-1.099423892855E+03,247.88998270397)); +#5578 = DIRECTION('',(0.,-0.206156840008,0.978518961144)); +#5579 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#5580 = ADVANCED_FACE('',(#5581),#5592,.T.); +#5581 = FACE_BOUND('',#5582,.T.); +#5582 = EDGE_LOOP('',(#5583,#5589,#5590,#5591)); +#5583 = ORIENTED_EDGE('',*,*,#5584,.T.); +#5584 = EDGE_CURVE('',#4793,#4295,#5585,.T.); +#5585 = LINE('',#5586,#5587); +#5586 = CARTESIAN_POINT('',(-9.9,-1.092220341577E+03,242.42138678066)); +#5587 = VECTOR('',#5588,1.); +#5588 = DIRECTION('',(-1.,-0.,-0.)); +#5589 = ORIENTED_EDGE('',*,*,#4302,.T.); +#5590 = ORIENTED_EDGE('',*,*,#5568,.F.); +#5591 = ORIENTED_EDGE('',*,*,#4800,.F.); +#5592 = CYLINDRICAL_SURFACE('',#5593,7.5); +#5593 = AXIS2_PLACEMENT_3D('',#5594,#5595,#5596); +#5594 = CARTESIAN_POINT('',(-9.9,-1.09944E+03,240.39)); +#5595 = DIRECTION('',(1.,0.,0.)); +#5596 = DIRECTION('',(0.,1.,0.)); +#5597 = ADVANCED_FACE('',(#5598),#5604,.F.); +#5598 = FACE_BOUND('',#5599,.F.); +#5599 = EDGE_LOOP('',(#5600,#5601,#5602,#5603)); +#5600 = ORIENTED_EDGE('',*,*,#5584,.T.); +#5601 = ORIENTED_EDGE('',*,*,#4294,.T.); +#5602 = ORIENTED_EDGE('',*,*,#4759,.F.); +#5603 = ORIENTED_EDGE('',*,*,#4792,.F.); +#5604 = PLANE('',#5605); +#5605 = AXIS2_PLACEMENT_3D('',#5606,#5607,#5608); +#5606 = CARTESIAN_POINT('',(-9.9,-1.092220341577E+03,242.42138678066)); +#5607 = DIRECTION('',(0.,-0.96262112309,-0.270851570755)); +#5608 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#5609 = ADVANCED_FACE('',(#5610),#5621,.F.); +#5610 = FACE_BOUND('',#5611,.F.); +#5611 = EDGE_LOOP('',(#5612,#5618,#5619,#5620)); +#5612 = ORIENTED_EDGE('',*,*,#5613,.T.); +#5613 = EDGE_CURVE('',#4843,#4338,#5614,.T.); +#5614 = LINE('',#5615,#5616); +#5615 = CARTESIAN_POINT('',(-9.9,-1.08434E+03,200.94)); +#5616 = VECTOR('',#5617,1.); +#5617 = DIRECTION('',(-1.,-0.,-0.)); +#5618 = ORIENTED_EDGE('',*,*,#4337,.T.); +#5619 = ORIENTED_EDGE('',*,*,#5613,.F.); +#5620 = ORIENTED_EDGE('',*,*,#4842,.F.); +#5621 = CYLINDRICAL_SURFACE('',#5622,4.); +#5622 = AXIS2_PLACEMENT_3D('',#5623,#5624,#5625); +#5623 = CARTESIAN_POINT('',(-9.9,-1.08834E+03,200.94)); +#5624 = DIRECTION('',(1.,0.,0.)); +#5625 = DIRECTION('',(0.,1.,0.)); +#5626 = ADVANCED_FACE('',(#5627),#5638,.F.); +#5627 = FACE_BOUND('',#5628,.F.); +#5628 = EDGE_LOOP('',(#5629,#5635,#5636,#5637)); +#5629 = ORIENTED_EDGE('',*,*,#5630,.T.); +#5630 = EDGE_CURVE('',#4854,#4349,#5631,.T.); +#5631 = LINE('',#5632,#5633); +#5632 = CARTESIAN_POINT('',(-9.9,-1.09544E+03,240.39)); +#5633 = VECTOR('',#5634,1.); +#5634 = DIRECTION('',(-1.,-0.,-0.)); +#5635 = ORIENTED_EDGE('',*,*,#4348,.T.); +#5636 = ORIENTED_EDGE('',*,*,#5630,.F.); +#5637 = ORIENTED_EDGE('',*,*,#4853,.F.); +#5638 = CYLINDRICAL_SURFACE('',#5639,4.); +#5639 = AXIS2_PLACEMENT_3D('',#5640,#5641,#5642); +#5640 = CARTESIAN_POINT('',(-9.9,-1.09944E+03,240.39)); +#5641 = DIRECTION('',(1.,0.,0.)); +#5642 = DIRECTION('',(0.,1.,0.)); +#5643 = ADVANCED_FACE('',(#5644),#5650,.T.); +#5644 = FACE_BOUND('',#5645,.T.); +#5645 = EDGE_LOOP('',(#5646,#5647,#5648,#5649)); +#5646 = ORIENTED_EDGE('',*,*,#4871,.T.); +#5647 = ORIENTED_EDGE('',*,*,#4742,.T.); +#5648 = ORIENTED_EDGE('',*,*,#4430,.T.); +#5649 = ORIENTED_EDGE('',*,*,#4407,.F.); +#5650 = PLANE('',#5651); +#5651 = AXIS2_PLACEMENT_3D('',#5652,#5653,#5654); +#5652 = CARTESIAN_POINT('',(-44.9,-1.10154E+03,200.94)); +#5653 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#5654 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5655 = ADVANCED_FACE('',(#5656),#5667,.T.); +#5656 = FACE_BOUND('',#5657,.T.); +#5657 = EDGE_LOOP('',(#5658,#5659,#5660,#5666)); +#5658 = ORIENTED_EDGE('',*,*,#4511,.T.); +#5659 = ORIENTED_EDGE('',*,*,#5054,.T.); +#5660 = ORIENTED_EDGE('',*,*,#5661,.F.); +#5661 = EDGE_CURVE('',#4998,#5055,#5662,.T.); +#5662 = LINE('',#5663,#5664); +#5663 = CARTESIAN_POINT('',(-19.9,-1.163662554812E+03,123.86376214781)); +#5664 = VECTOR('',#5665,1.); +#5665 = DIRECTION('',(1.,0.,0.)); +#5666 = ORIENTED_EDGE('',*,*,#4997,.F.); +#5667 = PLANE('',#5668); +#5668 = AXIS2_PLACEMENT_3D('',#5669,#5670,#5671); +#5669 = CARTESIAN_POINT('',(-19.9,-1.160255084405E+03,137.94954479362)); +#5670 = DIRECTION('',(0.,0.971964770456,-0.235126529752)); +#5671 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5672 = ADVANCED_FACE('',(#5673),#5684,.T.); +#5673 = FACE_BOUND('',#5674,.T.); +#5674 = EDGE_LOOP('',(#5675,#5676,#5677,#5683)); +#5675 = ORIENTED_EDGE('',*,*,#4564,.T.); +#5676 = ORIENTED_EDGE('',*,*,#5137,.T.); +#5677 = ORIENTED_EDGE('',*,*,#5678,.F.); +#5678 = EDGE_CURVE('',#5081,#5138,#5679,.T.); +#5679 = LINE('',#5680,#5681); +#5680 = CARTESIAN_POINT('',(0.1,-1.163662554812E+03,123.86376214781)); +#5681 = VECTOR('',#5682,1.); +#5682 = DIRECTION('',(1.,0.,0.)); +#5683 = ORIENTED_EDGE('',*,*,#5080,.F.); +#5684 = PLANE('',#5685); +#5685 = AXIS2_PLACEMENT_3D('',#5686,#5687,#5688); +#5686 = CARTESIAN_POINT('',(0.1,-1.160255084405E+03,137.94954479362)); +#5687 = DIRECTION('',(0.,0.971964770456,-0.235126529752)); +#5688 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5689 = ADVANCED_FACE('',(#5690),#5701,.T.); +#5690 = FACE_BOUND('',#5691,.T.); +#5691 = EDGE_LOOP('',(#5692,#5693,#5694,#5700)); +#5692 = ORIENTED_EDGE('',*,*,#4617,.T.); +#5693 = ORIENTED_EDGE('',*,*,#5220,.T.); +#5694 = ORIENTED_EDGE('',*,*,#5695,.F.); +#5695 = EDGE_CURVE('',#5164,#5221,#5696,.T.); +#5696 = LINE('',#5697,#5698); +#5697 = CARTESIAN_POINT('',(20.1,-1.163662554812E+03,123.86376214781)); +#5698 = VECTOR('',#5699,1.); +#5699 = DIRECTION('',(1.,0.,0.)); +#5700 = ORIENTED_EDGE('',*,*,#5163,.F.); +#5701 = PLANE('',#5702); +#5702 = AXIS2_PLACEMENT_3D('',#5703,#5704,#5705); +#5703 = CARTESIAN_POINT('',(20.1,-1.160255084405E+03,137.94954479362)); +#5704 = DIRECTION('',(0.,0.971964770456,-0.235126529752)); +#5705 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5706 = ADVANCED_FACE('',(#5707),#5718,.T.); +#5707 = FACE_BOUND('',#5708,.T.); +#5708 = EDGE_LOOP('',(#5709,#5710,#5711,#5717)); +#5709 = ORIENTED_EDGE('',*,*,#4670,.T.); +#5710 = ORIENTED_EDGE('',*,*,#5303,.T.); +#5711 = ORIENTED_EDGE('',*,*,#5712,.F.); +#5712 = EDGE_CURVE('',#5247,#5304,#5713,.T.); +#5713 = LINE('',#5714,#5715); +#5714 = CARTESIAN_POINT('',(40.1,-1.163662554812E+03,123.86376214781)); +#5715 = VECTOR('',#5716,1.); +#5716 = DIRECTION('',(1.,0.,0.)); +#5717 = ORIENTED_EDGE('',*,*,#5246,.F.); +#5718 = PLANE('',#5719); +#5719 = AXIS2_PLACEMENT_3D('',#5720,#5721,#5722); +#5720 = CARTESIAN_POINT('',(40.1,-1.160255084405E+03,137.94954479362)); +#5721 = DIRECTION('',(0.,0.971964770456,-0.235126529752)); +#5722 = DIRECTION('',(0.,-0.235126529752,-0.971964770456)); +#5723 = ADVANCED_FACE('',(#5724,#5783,#5794),#5805,.T.); +#5724 = FACE_BOUND('',#5725,.T.); +#5725 = EDGE_LOOP('',(#5726,#5727,#5735,#5744,#5752,#5761,#5767,#5768, + #5777)); +#5726 = ORIENTED_EDGE('',*,*,#4734,.F.); +#5727 = ORIENTED_EDGE('',*,*,#5728,.T.); +#5728 = EDGE_CURVE('',#4735,#5729,#5731,.T.); +#5729 = VERTEX_POINT('',#5730); +#5730 = CARTESIAN_POINT('',(24.1,-1.092601363636E+03,194.76822716242)); +#5731 = LINE('',#5732,#5733); +#5732 = CARTESIAN_POINT('',(24.1,-1.10154E+03,200.94)); +#5733 = VECTOR('',#5734,1.); +#5734 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#5735 = ORIENTED_EDGE('',*,*,#5736,.T.); +#5736 = EDGE_CURVE('',#5729,#5737,#5739,.T.); +#5737 = VERTEX_POINT('',#5738); +#5738 = CARTESIAN_POINT('',(24.1,-1.081120341577E+03,202.97138678066)); +#5739 = CIRCLE('',#5740,7.5); +#5740 = AXIS2_PLACEMENT_3D('',#5741,#5742,#5743); +#5741 = CARTESIAN_POINT('',(24.1,-1.08834E+03,200.94)); +#5742 = DIRECTION('',(1.,0.,0.)); +#5743 = DIRECTION('',(0.,1.,0.)); +#5744 = ORIENTED_EDGE('',*,*,#5745,.F.); +#5745 = EDGE_CURVE('',#5746,#5737,#5748,.T.); +#5746 = VERTEX_POINT('',#5747); +#5747 = CARTESIAN_POINT('',(24.1,-1.092220341577E+03,242.42138678066)); +#5748 = LINE('',#5749,#5750); +#5749 = CARTESIAN_POINT('',(24.1,-1.092220341577E+03,242.42138678066)); +#5750 = VECTOR('',#5751,1.); +#5751 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#5752 = ORIENTED_EDGE('',*,*,#5753,.T.); +#5753 = EDGE_CURVE('',#5746,#5754,#5756,.T.); +#5754 = VERTEX_POINT('',#5755); +#5755 = CARTESIAN_POINT('',(24.1,-1.099423892855E+03,247.88998270397)); +#5756 = CIRCLE('',#5757,7.5); +#5757 = AXIS2_PLACEMENT_3D('',#5758,#5759,#5760); +#5758 = CARTESIAN_POINT('',(24.1,-1.09944E+03,240.39)); +#5759 = DIRECTION('',(1.,0.,0.)); +#5760 = DIRECTION('',(0.,1.,0.)); +#5761 = ORIENTED_EDGE('',*,*,#5762,.T.); +#5762 = EDGE_CURVE('',#5754,#5522,#5763,.T.); +#5763 = LINE('',#5764,#5765); +#5764 = CARTESIAN_POINT('',(24.1,-1.099423892855E+03,247.88998270397)); +#5765 = VECTOR('',#5766,1.); +#5766 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#5767 = ORIENTED_EDGE('',*,*,#5529,.F.); +#5768 = ORIENTED_EDGE('',*,*,#5769,.F.); +#5769 = EDGE_CURVE('',#5770,#5530,#5772,.T.); +#5770 = VERTEX_POINT('',#5771); +#5771 = CARTESIAN_POINT('',(24.1,-1.108905900014E+03,230.07972403761)); +#5772 = CIRCLE('',#5773,1.5); +#5773 = AXIS2_PLACEMENT_3D('',#5774,#5775,#5776); +#5774 = CARTESIAN_POINT('',(24.1,-1.11034E+03,229.64)); +#5775 = DIRECTION('',(1.,-0.,0.)); +#5776 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#5777 = ORIENTED_EDGE('',*,*,#5778,.F.); +#5778 = EDGE_CURVE('',#4727,#5770,#5779,.T.); +#5779 = LINE('',#5780,#5781); +#5780 = CARTESIAN_POINT('',(24.1,-1.104270189936E+03,214.96098776003)); +#5781 = VECTOR('',#5782,1.); +#5782 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5783 = FACE_BOUND('',#5784,.T.); +#5784 = EDGE_LOOP('',(#5785)); +#5785 = ORIENTED_EDGE('',*,*,#5786,.F.); +#5786 = EDGE_CURVE('',#5787,#5787,#5789,.T.); +#5787 = VERTEX_POINT('',#5788); +#5788 = CARTESIAN_POINT('',(24.1,-1.08434E+03,200.94)); +#5789 = CIRCLE('',#5790,4.); +#5790 = AXIS2_PLACEMENT_3D('',#5791,#5792,#5793); +#5791 = CARTESIAN_POINT('',(24.1,-1.08834E+03,200.94)); +#5792 = DIRECTION('',(1.,0.,0.)); +#5793 = DIRECTION('',(0.,1.,0.)); +#5794 = FACE_BOUND('',#5795,.T.); +#5795 = EDGE_LOOP('',(#5796)); +#5796 = ORIENTED_EDGE('',*,*,#5797,.F.); +#5797 = EDGE_CURVE('',#5798,#5798,#5800,.T.); +#5798 = VERTEX_POINT('',#5799); +#5799 = CARTESIAN_POINT('',(24.1,-1.09544E+03,240.39)); +#5800 = CIRCLE('',#5801,4.); +#5801 = AXIS2_PLACEMENT_3D('',#5802,#5803,#5804); +#5802 = CARTESIAN_POINT('',(24.1,-1.09944E+03,240.39)); +#5803 = DIRECTION('',(1.,0.,0.)); +#5804 = DIRECTION('',(0.,1.,0.)); +#5805 = PLANE('',#5806); +#5806 = AXIS2_PLACEMENT_3D('',#5807,#5808,#5809); +#5807 = CARTESIAN_POINT('',(24.1,-1.113835686113E+03,226.88613249116)); +#5808 = DIRECTION('',(1.,0.,0.)); +#5809 = DIRECTION('',(0.,1.,0.)); +#5810 = ADVANCED_FACE('',(#5811),#5829,.F.); +#5811 = FACE_BOUND('',#5812,.F.); +#5812 = EDGE_LOOP('',(#5813,#5814,#5822,#5828)); +#5813 = ORIENTED_EDGE('',*,*,#4726,.F.); +#5814 = ORIENTED_EDGE('',*,*,#5815,.T.); +#5815 = EDGE_CURVE('',#4718,#5816,#5818,.T.); +#5816 = VERTEX_POINT('',#5817); +#5817 = CARTESIAN_POINT('',(55.1,-1.108905900014E+03,230.07972403761)); +#5818 = LINE('',#5819,#5820); +#5819 = CARTESIAN_POINT('',(55.1,-1.100105900014E+03,201.37972403761)); +#5820 = VECTOR('',#5821,1.); +#5821 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5822 = ORIENTED_EDGE('',*,*,#5823,.F.); +#5823 = EDGE_CURVE('',#5770,#5816,#5824,.T.); +#5824 = LINE('',#5825,#5826); +#5825 = CARTESIAN_POINT('',(-44.9,-1.108905900014E+03,230.07972403761)); +#5826 = VECTOR('',#5827,1.); +#5827 = DIRECTION('',(1.,0.,0.)); +#5828 = ORIENTED_EDGE('',*,*,#5778,.F.); +#5829 = PLANE('',#5830); +#5830 = AXIS2_PLACEMENT_3D('',#5831,#5832,#5833); +#5831 = CARTESIAN_POINT('',(-44.9,-1.100105900014E+03,201.37972403761)); +#5832 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#5833 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5834 = ADVANCED_FACE('',(#5835),#5854,.T.); +#5835 = FACE_BOUND('',#5836,.F.); +#5836 = EDGE_LOOP('',(#5837,#5845,#5852,#5853)); +#5837 = ORIENTED_EDGE('',*,*,#5838,.T.); +#5838 = EDGE_CURVE('',#4710,#5839,#5841,.T.); +#5839 = VERTEX_POINT('',#5840); +#5840 = CARTESIAN_POINT('',(56.6,-1.11034E+03,229.64)); +#5841 = LINE('',#5842,#5843); +#5842 = CARTESIAN_POINT('',(56.6,-1.10154E+03,200.94)); +#5843 = VECTOR('',#5844,1.); +#5844 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5845 = ORIENTED_EDGE('',*,*,#5846,.F.); +#5846 = EDGE_CURVE('',#5816,#5839,#5847,.T.); +#5847 = CIRCLE('',#5848,1.5); +#5848 = AXIS2_PLACEMENT_3D('',#5849,#5850,#5851); +#5849 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#5850 = DIRECTION('',(-6.E-17,0.29314935841,-0.956066657542)); +#5851 = DIRECTION('',(-1.,2.945186501321E-17,7.178766751374E-17)); +#5852 = ORIENTED_EDGE('',*,*,#5815,.F.); +#5853 = ORIENTED_EDGE('',*,*,#4717,.F.); +#5854 = CYLINDRICAL_SURFACE('',#5855,1.5); +#5855 = AXIS2_PLACEMENT_3D('',#5856,#5857,#5858); +#5856 = CARTESIAN_POINT('',(55.1,-1.10154E+03,200.94)); +#5857 = DIRECTION('',(6.E-17,-0.29314935841,0.956066657542)); +#5858 = DIRECTION('',(-1.,2.031123047657E-17,6.898496424118E-17)); +#5859 = ADVANCED_FACE('',(#5860),#5888,.T.); +#5860 = FACE_BOUND('',#5861,.T.); +#5861 = EDGE_LOOP('',(#5862,#5870,#5878,#5885,#5886,#5887)); +#5862 = ORIENTED_EDGE('',*,*,#5863,.T.); +#5863 = EDGE_CURVE('',#5839,#5864,#5866,.T.); +#5864 = VERTEX_POINT('',#5865); +#5865 = CARTESIAN_POINT('',(56.6,-1.16184E+03,234.74)); +#5866 = LINE('',#5867,#5868); +#5867 = CARTESIAN_POINT('',(56.6,-1.11034E+03,229.64)); +#5868 = VECTOR('',#5869,1.); +#5869 = DIRECTION('',(0.,-0.995132388616,9.85470909115E-02)); +#5870 = ORIENTED_EDGE('',*,*,#5871,.T.); +#5871 = EDGE_CURVE('',#5864,#5872,#5874,.T.); +#5872 = VERTEX_POINT('',#5873); +#5873 = CARTESIAN_POINT('',(56.6,-1.181180495325E+03,225.94254351617)); +#5874 = LINE('',#5875,#5876); +#5875 = CARTESIAN_POINT('',(56.6,-1.16184E+03,234.74)); +#5876 = VECTOR('',#5877,1.); +#5877 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#5878 = ORIENTED_EDGE('',*,*,#5879,.T.); +#5879 = EDGE_CURVE('',#5872,#5328,#5880,.T.); +#5880 = CIRCLE('',#5881,10.); +#5881 = AXIS2_PLACEMENT_3D('',#5882,#5883,#5884); +#5882 = CARTESIAN_POINT('',(56.6,-1.17704E+03,216.84)); +#5883 = DIRECTION('',(1.,0.,0.)); +#5884 = DIRECTION('',(0.,1.,0.)); +#5885 = ORIENTED_EDGE('',*,*,#5327,.T.); +#5886 = ORIENTED_EDGE('',*,*,#4709,.T.); +#5887 = ORIENTED_EDGE('',*,*,#5838,.T.); +#5888 = PLANE('',#5889); +#5889 = AXIS2_PLACEMENT_3D('',#5890,#5891,#5892); +#5890 = CARTESIAN_POINT('',(56.6,-1.147896717874E+03,193.1785020231)); +#5891 = DIRECTION('',(1.,0.,0.)); +#5892 = DIRECTION('',(0.,1.,0.)); +#5893 = ADVANCED_FACE('',(#5894),#5912,.F.); +#5894 = FACE_BOUND('',#5895,.F.); +#5895 = EDGE_LOOP('',(#5896,#5897,#5905,#5911)); +#5896 = ORIENTED_EDGE('',*,*,#4902,.F.); +#5897 = ORIENTED_EDGE('',*,*,#5898,.T.); +#5898 = EDGE_CURVE('',#4895,#5899,#5901,.T.); +#5899 = VERTEX_POINT('',#5900); +#5900 = CARTESIAN_POINT('',(20.1,-1.108905900014E+03,230.07972403761)); +#5901 = LINE('',#5902,#5903); +#5902 = CARTESIAN_POINT('',(20.1,-1.104270189936E+03,214.96098776003)); +#5903 = VECTOR('',#5904,1.); +#5904 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5905 = ORIENTED_EDGE('',*,*,#5906,.F.); +#5906 = EDGE_CURVE('',#4826,#5899,#5907,.T.); +#5907 = LINE('',#5908,#5909); +#5908 = CARTESIAN_POINT('',(-44.9,-1.108905900014E+03,230.07972403761)); +#5909 = VECTOR('',#5910,1.); +#5910 = DIRECTION('',(1.,0.,0.)); +#5911 = ORIENTED_EDGE('',*,*,#4834,.F.); +#5912 = PLANE('',#5913); +#5913 = AXIS2_PLACEMENT_3D('',#5914,#5915,#5916); +#5914 = CARTESIAN_POINT('',(-44.9,-1.100105900014E+03,201.37972403761)); +#5915 = DIRECTION('',(0.,-0.956066657542,-0.29314935841)); +#5916 = DIRECTION('',(0.,-0.29314935841,0.956066657542)); +#5917 = ADVANCED_FACE('',(#5918),#5930,.T.); +#5918 = FACE_BOUND('',#5919,.T.); +#5919 = EDGE_LOOP('',(#5920,#5921,#5922,#5923)); +#5920 = ORIENTED_EDGE('',*,*,#5906,.F.); +#5921 = ORIENTED_EDGE('',*,*,#4825,.T.); +#5922 = ORIENTED_EDGE('',*,*,#5505,.T.); +#5923 = ORIENTED_EDGE('',*,*,#5924,.F.); +#5924 = EDGE_CURVE('',#5899,#5506,#5925,.T.); +#5925 = CIRCLE('',#5926,1.5); +#5926 = AXIS2_PLACEMENT_3D('',#5927,#5928,#5929); +#5927 = CARTESIAN_POINT('',(20.1,-1.11034E+03,229.64)); +#5928 = DIRECTION('',(1.,-0.,0.)); +#5929 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#5930 = CYLINDRICAL_SURFACE('',#5931,1.5); +#5931 = AXIS2_PLACEMENT_3D('',#5932,#5933,#5934); +#5932 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#5933 = DIRECTION('',(1.,0.,0.)); +#5934 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#5935 = ADVANCED_FACE('',(#5936),#5954,.F.); +#5936 = FACE_BOUND('',#5937,.F.); +#5937 = EDGE_LOOP('',(#5938,#5939,#5940,#5948)); +#5938 = ORIENTED_EDGE('',*,*,#4877,.T.); +#5939 = ORIENTED_EDGE('',*,*,#5728,.T.); +#5940 = ORIENTED_EDGE('',*,*,#5941,.F.); +#5941 = EDGE_CURVE('',#5942,#5729,#5944,.T.); +#5942 = VERTEX_POINT('',#5943); +#5943 = CARTESIAN_POINT('',(20.1,-1.092601363636E+03,194.76822716242)); +#5944 = LINE('',#5945,#5946); +#5945 = CARTESIAN_POINT('',(20.1,-1.092601363636E+03,194.76822716242)); +#5946 = VECTOR('',#5947,1.); +#5947 = DIRECTION('',(1.,0.,0.)); +#5948 = ORIENTED_EDGE('',*,*,#5949,.F.); +#5949 = EDGE_CURVE('',#3908,#5942,#5950,.T.); +#5950 = LINE('',#5951,#5952); +#5951 = CARTESIAN_POINT('',(20.1,-1.10154E+03,200.94)); +#5952 = VECTOR('',#5953,1.); +#5953 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#5954 = PLANE('',#5955); +#5955 = AXIS2_PLACEMENT_3D('',#5956,#5957,#5958); +#5956 = CARTESIAN_POINT('',(20.1,-1.10154E+03,200.94)); +#5957 = DIRECTION('',(0.,0.568181818182,0.822903045011)); +#5958 = DIRECTION('',(0.,0.822903045011,-0.568181818182)); +#5959 = ADVANCED_FACE('',(#5960,#5999,#6010),#6021,.F.); +#5960 = FACE_BOUND('',#5961,.F.); +#5961 = EDGE_LOOP('',(#5962,#5963,#5964,#5973,#5981,#5990,#5996,#5997, + #5998)); +#5962 = ORIENTED_EDGE('',*,*,#4894,.F.); +#5963 = ORIENTED_EDGE('',*,*,#5949,.T.); +#5964 = ORIENTED_EDGE('',*,*,#5965,.T.); +#5965 = EDGE_CURVE('',#5942,#5966,#5968,.T.); +#5966 = VERTEX_POINT('',#5967); +#5967 = CARTESIAN_POINT('',(20.1,-1.081120341577E+03,202.97138678066)); +#5968 = CIRCLE('',#5969,7.5); +#5969 = AXIS2_PLACEMENT_3D('',#5970,#5971,#5972); +#5970 = CARTESIAN_POINT('',(20.1,-1.08834E+03,200.94)); +#5971 = DIRECTION('',(1.,0.,0.)); +#5972 = DIRECTION('',(0.,1.,0.)); +#5973 = ORIENTED_EDGE('',*,*,#5974,.F.); +#5974 = EDGE_CURVE('',#5975,#5966,#5977,.T.); +#5975 = VERTEX_POINT('',#5976); +#5976 = CARTESIAN_POINT('',(20.1,-1.092220341577E+03,242.42138678066)); +#5977 = LINE('',#5978,#5979); +#5978 = CARTESIAN_POINT('',(20.1,-1.092220341577E+03,242.42138678066)); +#5979 = VECTOR('',#5980,1.); +#5980 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#5981 = ORIENTED_EDGE('',*,*,#5982,.T.); +#5982 = EDGE_CURVE('',#5975,#5983,#5985,.T.); +#5983 = VERTEX_POINT('',#5984); +#5984 = CARTESIAN_POINT('',(20.1,-1.099423892855E+03,247.88998270397)); +#5985 = CIRCLE('',#5986,7.5); +#5986 = AXIS2_PLACEMENT_3D('',#5987,#5988,#5989); +#5987 = CARTESIAN_POINT('',(20.1,-1.09944E+03,240.39)); +#5988 = DIRECTION('',(1.,0.,0.)); +#5989 = DIRECTION('',(0.,1.,0.)); +#5990 = ORIENTED_EDGE('',*,*,#5991,.T.); +#5991 = EDGE_CURVE('',#5983,#5514,#5992,.T.); +#5992 = LINE('',#5993,#5994); +#5993 = CARTESIAN_POINT('',(20.1,-1.099423892855E+03,247.88998270397)); +#5994 = VECTOR('',#5995,1.); +#5995 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#5996 = ORIENTED_EDGE('',*,*,#5513,.F.); +#5997 = ORIENTED_EDGE('',*,*,#5924,.F.); +#5998 = ORIENTED_EDGE('',*,*,#5898,.F.); +#5999 = FACE_BOUND('',#6000,.F.); +#6000 = EDGE_LOOP('',(#6001)); +#6001 = ORIENTED_EDGE('',*,*,#6002,.F.); +#6002 = EDGE_CURVE('',#6003,#6003,#6005,.T.); +#6003 = VERTEX_POINT('',#6004); +#6004 = CARTESIAN_POINT('',(20.1,-1.08434E+03,200.94)); +#6005 = CIRCLE('',#6006,4.); +#6006 = AXIS2_PLACEMENT_3D('',#6007,#6008,#6009); +#6007 = CARTESIAN_POINT('',(20.1,-1.08834E+03,200.94)); +#6008 = DIRECTION('',(1.,0.,0.)); +#6009 = DIRECTION('',(0.,1.,0.)); +#6010 = FACE_BOUND('',#6011,.F.); +#6011 = EDGE_LOOP('',(#6012)); +#6012 = ORIENTED_EDGE('',*,*,#6013,.F.); +#6013 = EDGE_CURVE('',#6014,#6014,#6016,.T.); +#6014 = VERTEX_POINT('',#6015); +#6015 = CARTESIAN_POINT('',(20.1,-1.09544E+03,240.39)); +#6016 = CIRCLE('',#6017,4.); +#6017 = AXIS2_PLACEMENT_3D('',#6018,#6019,#6020); +#6018 = CARTESIAN_POINT('',(20.1,-1.09944E+03,240.39)); +#6019 = DIRECTION('',(1.,0.,0.)); +#6020 = DIRECTION('',(0.,1.,0.)); +#6021 = PLANE('',#6022); +#6022 = AXIS2_PLACEMENT_3D('',#6023,#6024,#6025); +#6023 = CARTESIAN_POINT('',(20.1,-1.113835686113E+03,226.88613249116)); +#6024 = DIRECTION('',(1.,0.,0.)); +#6025 = DIRECTION('',(0.,1.,0.)); +#6026 = ADVANCED_FACE('',(#6027),#6033,.T.); +#6027 = FACE_BOUND('',#6028,.T.); +#6028 = EDGE_LOOP('',(#6029,#6030,#6031,#6032)); +#6029 = ORIENTED_EDGE('',*,*,#5661,.T.); +#6030 = ORIENTED_EDGE('',*,*,#5062,.T.); +#6031 = ORIENTED_EDGE('',*,*,#5029,.F.); +#6032 = ORIENTED_EDGE('',*,*,#5005,.F.); +#6033 = PLANE('',#6034); +#6034 = AXIS2_PLACEMENT_3D('',#6035,#6036,#6037); +#6035 = CARTESIAN_POINT('',(-19.9,-1.163662554812E+03,123.86376214781)); +#6036 = DIRECTION('',(0.,-0.959538377832,-0.281577878158)); +#6037 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#6038 = ADVANCED_FACE('',(#6039),#6045,.T.); +#6039 = FACE_BOUND('',#6040,.T.); +#6040 = EDGE_LOOP('',(#6041,#6042,#6043,#6044)); +#6041 = ORIENTED_EDGE('',*,*,#5678,.T.); +#6042 = ORIENTED_EDGE('',*,*,#5145,.T.); +#6043 = ORIENTED_EDGE('',*,*,#5112,.F.); +#6044 = ORIENTED_EDGE('',*,*,#5088,.F.); +#6045 = PLANE('',#6046); +#6046 = AXIS2_PLACEMENT_3D('',#6047,#6048,#6049); +#6047 = CARTESIAN_POINT('',(0.1,-1.163662554812E+03,123.86376214781)); +#6048 = DIRECTION('',(0.,-0.959538377832,-0.281577878158)); +#6049 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#6050 = ADVANCED_FACE('',(#6051),#6057,.T.); +#6051 = FACE_BOUND('',#6052,.T.); +#6052 = EDGE_LOOP('',(#6053,#6054,#6055,#6056)); +#6053 = ORIENTED_EDGE('',*,*,#5695,.T.); +#6054 = ORIENTED_EDGE('',*,*,#5228,.T.); +#6055 = ORIENTED_EDGE('',*,*,#5195,.F.); +#6056 = ORIENTED_EDGE('',*,*,#5171,.F.); +#6057 = PLANE('',#6058); +#6058 = AXIS2_PLACEMENT_3D('',#6059,#6060,#6061); +#6059 = CARTESIAN_POINT('',(20.1,-1.163662554812E+03,123.86376214781)); +#6060 = DIRECTION('',(0.,-0.959538377832,-0.281577878158)); +#6061 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#6062 = ADVANCED_FACE('',(#6063),#6069,.T.); +#6063 = FACE_BOUND('',#6064,.T.); +#6064 = EDGE_LOOP('',(#6065,#6066,#6067,#6068)); +#6065 = ORIENTED_EDGE('',*,*,#5712,.T.); +#6066 = ORIENTED_EDGE('',*,*,#5311,.T.); +#6067 = ORIENTED_EDGE('',*,*,#5278,.F.); +#6068 = ORIENTED_EDGE('',*,*,#5254,.F.); +#6069 = PLANE('',#6070); +#6070 = AXIS2_PLACEMENT_3D('',#6071,#6072,#6073); +#6071 = CARTESIAN_POINT('',(40.1,-1.163662554812E+03,123.86376214781)); +#6072 = DIRECTION('',(0.,-0.959538377832,-0.281577878158)); +#6073 = DIRECTION('',(0.,-0.281577878158,0.959538377832)); +#6074 = ADVANCED_FACE('',(#6075),#6087,.T.); +#6075 = FACE_BOUND('',#6076,.F.); +#6076 = EDGE_LOOP('',(#6077,#6078,#6085,#6086)); +#6077 = ORIENTED_EDGE('',*,*,#5362,.F.); +#6078 = ORIENTED_EDGE('',*,*,#6079,.T.); +#6079 = EDGE_CURVE('',#5355,#5872,#6080,.T.); +#6080 = CIRCLE('',#6081,1.5); +#6081 = AXIS2_PLACEMENT_3D('',#6082,#6083,#6084); +#6082 = CARTESIAN_POINT('',(55.1,-1.181180495325E+03,225.94254351617)); +#6083 = DIRECTION('',(0.,0.910254351618,0.414049532497)); +#6084 = DIRECTION('',(-1.,0.,0.)); +#6085 = ORIENTED_EDGE('',*,*,#5879,.T.); +#6086 = ORIENTED_EDGE('',*,*,#5337,.F.); +#6087 = TOROIDAL_SURFACE('',#6088,10.,1.5); +#6088 = AXIS2_PLACEMENT_3D('',#6089,#6090,#6091); +#6089 = CARTESIAN_POINT('',(55.1,-1.17704E+03,216.84)); +#6090 = DIRECTION('',(1.,0.,0.)); +#6091 = DIRECTION('',(0.,1.,0.)); +#6092 = ADVANCED_FACE('',(#6093),#6111,.F.); +#6093 = FACE_BOUND('',#6094,.F.); +#6094 = EDGE_LOOP('',(#6095,#6103,#6109,#6110)); +#6095 = ORIENTED_EDGE('',*,*,#6096,.T.); +#6096 = EDGE_CURVE('',#5437,#6097,#6099,.T.); +#6097 = VERTEX_POINT('',#6098); +#6098 = CARTESIAN_POINT('',(55.1,-1.162461074299E+03,236.10538152742)); +#6099 = LINE('',#6100,#6101); +#6100 = CARTESIAN_POINT('',(-44.9,-1.162461074299E+03,236.10538152742)); +#6101 = VECTOR('',#6102,1.); +#6102 = DIRECTION('',(1.,0.,0.)); +#6103 = ORIENTED_EDGE('',*,*,#6104,.T.); +#6104 = EDGE_CURVE('',#6097,#5355,#6105,.T.); +#6105 = LINE('',#6106,#6107); +#6106 = CARTESIAN_POINT('',(55.1,-1.162461074299E+03,236.10538152742)); +#6107 = VECTOR('',#6108,1.); +#6108 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#6109 = ORIENTED_EDGE('',*,*,#5352,.F.); +#6110 = ORIENTED_EDGE('',*,*,#5436,.F.); +#6111 = PLANE('',#6112); +#6112 = AXIS2_PLACEMENT_3D('',#6113,#6114,#6115); +#6113 = CARTESIAN_POINT('',(-44.9,-1.162461074299E+03,236.10538152742)); +#6114 = DIRECTION('',(0.,0.414049532497,-0.910254351618)); +#6115 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#6116 = ADVANCED_FACE('',(#6117),#6128,.T.); +#6117 = FACE_BOUND('',#6118,.T.); +#6118 = EDGE_LOOP('',(#6119,#6126,#6127)); +#6119 = ORIENTED_EDGE('',*,*,#6120,.F.); +#6120 = EDGE_CURVE('',#5406,#5437,#6121,.T.); +#6121 = CIRCLE('',#6122,1.5); +#6122 = AXIS2_PLACEMENT_3D('',#6123,#6124,#6125); +#6123 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#6124 = DIRECTION('',(1.,-0.,0.)); +#6125 = DIRECTION('',(0.,0.995132388616,-9.854709091149E-02)); +#6126 = ORIENTED_EDGE('',*,*,#5421,.T.); +#6127 = ORIENTED_EDGE('',*,*,#5444,.F.); +#6128 = SPHERICAL_SURFACE('',#6129,1.5); +#6129 = AXIS2_PLACEMENT_3D('',#6130,#6131,#6132); +#6130 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#6131 = DIRECTION('',(0.303356449152,0.867360573188,0.394538338868)); +#6132 = DIRECTION('',(0.632235929056,0.126567461927,-0.764367979177)); +#6133 = ADVANCED_FACE('',(#6134),#6146,.T.); +#6134 = FACE_BOUND('',#6135,.T.); +#6135 = EDGE_LOOP('',(#6136,#6137,#6138,#6139)); +#6136 = ORIENTED_EDGE('',*,*,#5553,.F.); +#6137 = ORIENTED_EDGE('',*,*,#6120,.T.); +#6138 = ORIENTED_EDGE('',*,*,#6096,.T.); +#6139 = ORIENTED_EDGE('',*,*,#6140,.F.); +#6140 = EDGE_CURVE('',#5546,#6097,#6141,.T.); +#6141 = CIRCLE('',#6142,1.5); +#6142 = AXIS2_PLACEMENT_3D('',#6143,#6144,#6145); +#6143 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#6144 = DIRECTION('',(1.,-0.,0.)); +#6145 = DIRECTION('',(0.,0.995132388616,-9.854709091149E-02)); +#6146 = CYLINDRICAL_SURFACE('',#6147,1.5); +#6147 = AXIS2_PLACEMENT_3D('',#6148,#6149,#6150); +#6148 = CARTESIAN_POINT('',(-44.9,-1.16184E+03,234.74)); +#6149 = DIRECTION('',(1.,0.,0.)); +#6150 = DIRECTION('',(0.,0.995132388616,-9.854709091149E-02)); +#6151 = ADVANCED_FACE('',(#6152),#6170,.T.); +#6152 = FACE_BOUND('',#6153,.F.); +#6153 = EDGE_LOOP('',(#6154,#6155,#6162,#6163)); +#6154 = ORIENTED_EDGE('',*,*,#5545,.F.); +#6155 = ORIENTED_EDGE('',*,*,#6156,.T.); +#6156 = EDGE_CURVE('',#5538,#5839,#6157,.T.); +#6157 = CIRCLE('',#6158,1.5); +#6158 = AXIS2_PLACEMENT_3D('',#6159,#6160,#6161); +#6159 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#6160 = DIRECTION('',(-8.3E-16,0.995132388616,-9.85470909115E-02)); +#6161 = DIRECTION('',(-1.,-8.241165962386E-16,1.004076629285E-16)); +#6162 = ORIENTED_EDGE('',*,*,#5863,.T.); +#6163 = ORIENTED_EDGE('',*,*,#6164,.F.); +#6164 = EDGE_CURVE('',#5546,#5864,#6165,.T.); +#6165 = CIRCLE('',#6166,1.5); +#6166 = AXIS2_PLACEMENT_3D('',#6167,#6168,#6169); +#6167 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#6168 = DIRECTION('',(-8.3E-16,0.995132388616,-9.85470909115E-02)); +#6169 = DIRECTION('',(-1.,-8.241165962386E-16,1.004076629285E-16)); +#6170 = CYLINDRICAL_SURFACE('',#6171,1.5); +#6171 = AXIS2_PLACEMENT_3D('',#6172,#6173,#6174); +#6172 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#6173 = DIRECTION('',(8.3E-16,-0.995132388616,9.85470909115E-02)); +#6174 = DIRECTION('',(-1.,-8.241165962386E-16,1.004076629285E-16)); +#6175 = ADVANCED_FACE('',(#6176),#6188,.T.); +#6176 = FACE_BOUND('',#6177,.T.); +#6177 = EDGE_LOOP('',(#6178,#6179,#6180,#6181)); +#6178 = ORIENTED_EDGE('',*,*,#5823,.F.); +#6179 = ORIENTED_EDGE('',*,*,#5769,.T.); +#6180 = ORIENTED_EDGE('',*,*,#5537,.T.); +#6181 = ORIENTED_EDGE('',*,*,#6182,.F.); +#6182 = EDGE_CURVE('',#5816,#5538,#6183,.T.); +#6183 = CIRCLE('',#6184,1.5); +#6184 = AXIS2_PLACEMENT_3D('',#6185,#6186,#6187); +#6185 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#6186 = DIRECTION('',(1.,-0.,0.)); +#6187 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#6188 = CYLINDRICAL_SURFACE('',#6189,1.5); +#6189 = AXIS2_PLACEMENT_3D('',#6190,#6191,#6192); +#6190 = CARTESIAN_POINT('',(-44.9,-1.11034E+03,229.64)); +#6191 = DIRECTION('',(1.,0.,0.)); +#6192 = DIRECTION('',(0.,0.29314935841,-0.956066657542)); +#6193 = ADVANCED_FACE('',(#6194),#6205,.F.); +#6194 = FACE_BOUND('',#6195,.F.); +#6195 = EDGE_LOOP('',(#6196,#6197,#6203,#6204)); +#6196 = ORIENTED_EDGE('',*,*,#5991,.F.); +#6197 = ORIENTED_EDGE('',*,*,#6198,.T.); +#6198 = EDGE_CURVE('',#5983,#5754,#6199,.T.); +#6199 = LINE('',#6200,#6201); +#6200 = CARTESIAN_POINT('',(20.1,-1.099423892855E+03,247.88998270397)); +#6201 = VECTOR('',#6202,1.); +#6202 = DIRECTION('',(1.,0.,0.)); +#6203 = ORIENTED_EDGE('',*,*,#5762,.T.); +#6204 = ORIENTED_EDGE('',*,*,#5521,.F.); +#6205 = PLANE('',#6206); +#6206 = AXIS2_PLACEMENT_3D('',#6207,#6208,#6209); +#6207 = CARTESIAN_POINT('',(20.1,-1.099423892855E+03,247.88998270397)); +#6208 = DIRECTION('',(0.,0.206156840008,-0.978518961144)); +#6209 = DIRECTION('',(0.,-0.978518961144,-0.206156840008)); +#6210 = ADVANCED_FACE('',(#6211),#6222,.T.); +#6211 = FACE_BOUND('',#6212,.F.); +#6212 = EDGE_LOOP('',(#6213,#6219,#6220,#6221)); +#6213 = ORIENTED_EDGE('',*,*,#6214,.T.); +#6214 = EDGE_CURVE('',#5975,#5746,#6215,.T.); +#6215 = LINE('',#6216,#6217); +#6216 = CARTESIAN_POINT('',(20.1,-1.092220341577E+03,242.42138678066)); +#6217 = VECTOR('',#6218,1.); +#6218 = DIRECTION('',(1.,0.,0.)); +#6219 = ORIENTED_EDGE('',*,*,#5753,.T.); +#6220 = ORIENTED_EDGE('',*,*,#6198,.F.); +#6221 = ORIENTED_EDGE('',*,*,#5982,.F.); +#6222 = CYLINDRICAL_SURFACE('',#6223,7.5); +#6223 = AXIS2_PLACEMENT_3D('',#6224,#6225,#6226); +#6224 = CARTESIAN_POINT('',(20.1,-1.09944E+03,240.39)); +#6225 = DIRECTION('',(-1.,-0.,-0.)); +#6226 = DIRECTION('',(0.,1.,0.)); +#6227 = ADVANCED_FACE('',(#6228),#6239,.T.); +#6228 = FACE_BOUND('',#6229,.T.); +#6229 = EDGE_LOOP('',(#6230,#6231,#6232,#6238)); +#6230 = ORIENTED_EDGE('',*,*,#6214,.T.); +#6231 = ORIENTED_EDGE('',*,*,#5745,.T.); +#6232 = ORIENTED_EDGE('',*,*,#6233,.F.); +#6233 = EDGE_CURVE('',#5966,#5737,#6234,.T.); +#6234 = LINE('',#6235,#6236); +#6235 = CARTESIAN_POINT('',(20.1,-1.081120341577E+03,202.97138678066)); +#6236 = VECTOR('',#6237,1.); +#6237 = DIRECTION('',(1.,0.,0.)); +#6238 = ORIENTED_EDGE('',*,*,#5974,.F.); +#6239 = PLANE('',#6240); +#6240 = AXIS2_PLACEMENT_3D('',#6241,#6242,#6243); +#6241 = CARTESIAN_POINT('',(20.1,-1.092220341577E+03,242.42138678066)); +#6242 = DIRECTION('',(0.,0.96262112309,0.270851570755)); +#6243 = DIRECTION('',(0.,0.270851570755,-0.96262112309)); +#6244 = ADVANCED_FACE('',(#6245),#6251,.T.); +#6245 = FACE_BOUND('',#6246,.F.); +#6246 = EDGE_LOOP('',(#6247,#6248,#6249,#6250)); +#6247 = ORIENTED_EDGE('',*,*,#5941,.T.); +#6248 = ORIENTED_EDGE('',*,*,#5736,.T.); +#6249 = ORIENTED_EDGE('',*,*,#6233,.F.); +#6250 = ORIENTED_EDGE('',*,*,#5965,.F.); +#6251 = CYLINDRICAL_SURFACE('',#6252,7.5); +#6252 = AXIS2_PLACEMENT_3D('',#6253,#6254,#6255); +#6253 = CARTESIAN_POINT('',(20.1,-1.08834E+03,200.94)); +#6254 = DIRECTION('',(-1.,-0.,-0.)); +#6255 = DIRECTION('',(0.,1.,0.)); +#6256 = ADVANCED_FACE('',(#6257),#6268,.F.); +#6257 = FACE_BOUND('',#6258,.T.); +#6258 = EDGE_LOOP('',(#6259,#6265,#6266,#6267)); +#6259 = ORIENTED_EDGE('',*,*,#6260,.T.); +#6260 = EDGE_CURVE('',#6003,#5787,#6261,.T.); +#6261 = LINE('',#6262,#6263); +#6262 = CARTESIAN_POINT('',(20.1,-1.08434E+03,200.94)); +#6263 = VECTOR('',#6264,1.); +#6264 = DIRECTION('',(1.,0.,0.)); +#6265 = ORIENTED_EDGE('',*,*,#5786,.T.); +#6266 = ORIENTED_EDGE('',*,*,#6260,.F.); +#6267 = ORIENTED_EDGE('',*,*,#6002,.F.); +#6268 = CYLINDRICAL_SURFACE('',#6269,4.); +#6269 = AXIS2_PLACEMENT_3D('',#6270,#6271,#6272); +#6270 = CARTESIAN_POINT('',(20.1,-1.08834E+03,200.94)); +#6271 = DIRECTION('',(-1.,-0.,-0.)); +#6272 = DIRECTION('',(0.,1.,0.)); +#6273 = ADVANCED_FACE('',(#6274),#6285,.F.); +#6274 = FACE_BOUND('',#6275,.T.); +#6275 = EDGE_LOOP('',(#6276,#6282,#6283,#6284)); +#6276 = ORIENTED_EDGE('',*,*,#6277,.T.); +#6277 = EDGE_CURVE('',#6014,#5798,#6278,.T.); +#6278 = LINE('',#6279,#6280); +#6279 = CARTESIAN_POINT('',(20.1,-1.09544E+03,240.39)); +#6280 = VECTOR('',#6281,1.); +#6281 = DIRECTION('',(1.,0.,0.)); +#6282 = ORIENTED_EDGE('',*,*,#5797,.T.); +#6283 = ORIENTED_EDGE('',*,*,#6277,.F.); +#6284 = ORIENTED_EDGE('',*,*,#6013,.F.); +#6285 = CYLINDRICAL_SURFACE('',#6286,4.); +#6286 = AXIS2_PLACEMENT_3D('',#6287,#6288,#6289); +#6287 = CARTESIAN_POINT('',(20.1,-1.09944E+03,240.39)); +#6288 = DIRECTION('',(-1.,-0.,-0.)); +#6289 = DIRECTION('',(0.,1.,0.)); +#6290 = ADVANCED_FACE('',(#6291),#6296,.T.); +#6291 = FACE_BOUND('',#6292,.T.); +#6292 = EDGE_LOOP('',(#6293,#6294,#6295)); +#6293 = ORIENTED_EDGE('',*,*,#6182,.T.); +#6294 = ORIENTED_EDGE('',*,*,#6156,.T.); +#6295 = ORIENTED_EDGE('',*,*,#5846,.F.); +#6296 = SPHERICAL_SURFACE('',#6297,1.5); +#6297 = AXIS2_PLACEMENT_3D('',#6298,#6299,#6300); +#6298 = CARTESIAN_POINT('',(55.1,-1.11034E+03,229.64)); +#6299 = DIRECTION('',(-0.609242933207,-0.232462644377,0.758145215184)); +#6300 = DIRECTION('',(-0.584952990716,-0.51376326958,-0.627596447953)); +#6301 = ADVANCED_FACE('',(#6302),#6314,.T.); +#6302 = FACE_BOUND('',#6303,.F.); +#6303 = EDGE_LOOP('',(#6304,#6305,#6312,#6313)); +#6304 = ORIENTED_EDGE('',*,*,#6104,.F.); +#6305 = ORIENTED_EDGE('',*,*,#6306,.T.); +#6306 = EDGE_CURVE('',#6097,#5864,#6307,.T.); +#6307 = CIRCLE('',#6308,1.5); +#6308 = AXIS2_PLACEMENT_3D('',#6309,#6310,#6311); +#6309 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#6310 = DIRECTION('',(0.,0.910254351618,0.414049532497)); +#6311 = DIRECTION('',(-1.,0.,0.)); +#6312 = ORIENTED_EDGE('',*,*,#5871,.T.); +#6313 = ORIENTED_EDGE('',*,*,#6079,.F.); +#6314 = CYLINDRICAL_SURFACE('',#6315,1.5); +#6315 = AXIS2_PLACEMENT_3D('',#6316,#6317,#6318); +#6316 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#6317 = DIRECTION('',(0.,-0.910254351618,-0.414049532497)); +#6318 = DIRECTION('',(-1.,0.,0.)); +#6319 = ADVANCED_FACE('',(#6320),#6325,.T.); +#6320 = FACE_BOUND('',#6321,.T.); +#6321 = EDGE_LOOP('',(#6322,#6323,#6324)); +#6322 = ORIENTED_EDGE('',*,*,#6140,.T.); +#6323 = ORIENTED_EDGE('',*,*,#6306,.T.); +#6324 = ORIENTED_EDGE('',*,*,#6164,.F.); +#6325 = SPHERICAL_SURFACE('',#6326,1.5); +#6326 = AXIS2_PLACEMENT_3D('',#6327,#6328,#6329); +#6327 = CARTESIAN_POINT('',(55.1,-1.16184E+03,234.74)); +#6328 = DIRECTION('',(0.303356449152,-0.867360573188,-0.394538338868)); +#6329 = DIRECTION('',(-0.632235929056,0.126567461927,-0.764367979177)); +#6330 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6334)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#6331,#6332,#6333)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#6331 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6332 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#6333 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#6334 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-05),#6331, + 'distance_accuracy_value','confusion accuracy'); +#6335 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6336,#6338); +#6336 = ( REPRESENTATION_RELATIONSHIP('','',#3456,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6337) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#6337 = ITEM_DEFINED_TRANSFORMATION('','',#11,#31); +#6338 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #6339); +#6339 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('5','Bucket001','',#5,#3451,$); +#6340 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#3453)); +#6341 = SHAPE_DEFINITION_REPRESENTATION(#6342,#6348); +#6342 = PRODUCT_DEFINITION_SHAPE('','',#6343); +#6343 = PRODUCT_DEFINITION('design','',#6344,#6347); +#6344 = PRODUCT_DEFINITION_FORMATION('','',#6345); +#6345 = PRODUCT('BucketLink2','BucketLink2','',(#6346)); +#6346 = PRODUCT_CONTEXT('',#2,'mechanical'); +#6347 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#6348 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#6349),#7050); +#6349 = MANIFOLD_SOLID_BREP('',#6350); +#6350 = CLOSED_SHELL('',(#6351,#6413,#6445,#6477,#6519,#6563,#6625,#6657 + ,#6689,#6731,#6775,#6784,#6804,#6829,#6841,#6885,#6905,#6925,#6941, + #6950,#6994,#7014,#7034)); +#6351 = ADVANCED_FACE('',(#6352),#6408,.F.); +#6352 = FACE_BOUND('',#6353,.F.); +#6353 = EDGE_LOOP('',(#6354,#6365,#6374,#6383,#6392,#6401)); +#6354 = ORIENTED_EDGE('',*,*,#6355,.F.); +#6355 = EDGE_CURVE('',#6356,#6358,#6360,.T.); +#6356 = VERTEX_POINT('',#6357); +#6357 = CARTESIAN_POINT('',(19.781083518149,-990.3913577687, + 197.54633551117)); +#6358 = VERTEX_POINT('',#6359); +#6359 = CARTESIAN_POINT('',(19.781083518149,-1.042471360483E+03, + 183.26634541013)); +#6360 = CIRCLE('',#6361,41.999999999996); +#6361 = AXIS2_PLACEMENT_3D('',#6362,#6363,#6364); +#6362 = CARTESIAN_POINT('',(19.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6363 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6364 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6365 = ORIENTED_EDGE('',*,*,#6366,.T.); +#6366 = EDGE_CURVE('',#6356,#6367,#6369,.T.); +#6367 = VERTEX_POINT('',#6368); +#6368 = CARTESIAN_POINT('',(19.781083518149,-987.0517250749, + 212.81592034301)); +#6369 = CIRCLE('',#6370,8.); +#6370 = AXIS2_PLACEMENT_3D('',#6371,#6372,#6373); +#6371 = CARTESIAN_POINT('',(19.781083518149,-987.0517265955, + 204.81592034301)); +#6372 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6373 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6374 = ORIENTED_EDGE('',*,*,#6375,.T.); +#6375 = EDGE_CURVE('',#6367,#6376,#6378,.T.); +#6376 = VERTEX_POINT('',#6377); +#6377 = CARTESIAN_POINT('',(19.781083518149,-983.7120954224, + 212.08550517484)); +#6378 = CIRCLE('',#6379,8.); +#6379 = AXIS2_PLACEMENT_3D('',#6380,#6381,#6382); +#6380 = CARTESIAN_POINT('',(19.781083518149,-987.0517265955, + 204.81592034301)); +#6381 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6382 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6383 = ORIENTED_EDGE('',*,*,#6384,.T.); +#6384 = EDGE_CURVE('',#6376,#6385,#6387,.T.); +#6385 = VERTEX_POINT('',#6386); +#6386 = CARTESIAN_POINT('',(19.781083518149,-1.055632099171E+03, + 192.36551884484)); +#6387 = CIRCLE('',#6388,58.); +#6388 = AXIS2_PLACEMENT_3D('',#6389,#6390,#6391); +#6389 = CARTESIAN_POINT('',(19.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6390 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6391 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6392 = ORIENTED_EDGE('',*,*,#6393,.T.); +#6393 = EDGE_CURVE('',#6385,#6394,#6396,.T.); +#6394 = VERTEX_POINT('',#6395); +#6395 = CARTESIAN_POINT('',(19.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6396 = CIRCLE('',#6397,8.); +#6397 = AXIS2_PLACEMENT_3D('',#6398,#6399,#6400); +#6398 = CARTESIAN_POINT('',(19.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6399 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6400 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6401 = ORIENTED_EDGE('',*,*,#6402,.T.); +#6402 = EDGE_CURVE('',#6394,#6358,#6403,.T.); +#6403 = CIRCLE('',#6404,8.); +#6404 = AXIS2_PLACEMENT_3D('',#6405,#6406,#6407); +#6405 = CARTESIAN_POINT('',(19.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6406 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6407 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6408 = PLANE('',#6409); +#6409 = AXIS2_PLACEMENT_3D('',#6410,#6411,#6412); +#6410 = CARTESIAN_POINT('',(19.781083518149,-1.019431905347E+03, + 201.34951718971)); +#6411 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6412 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6413 = ADVANCED_FACE('',(#6414),#6440,.F.); +#6414 = FACE_BOUND('',#6415,.T.); +#6415 = EDGE_LOOP('',(#6416,#6424,#6433,#6439)); +#6416 = ORIENTED_EDGE('',*,*,#6417,.T.); +#6417 = EDGE_CURVE('',#6356,#6418,#6420,.T.); +#6418 = VERTEX_POINT('',#6419); +#6419 = CARTESIAN_POINT('',(24.781083518149,-990.3913577687, + 197.54633551117)); +#6420 = LINE('',#6421,#6422); +#6421 = CARTESIAN_POINT('',(19.781083518149,-990.3913577687, + 197.54633551117)); +#6422 = VECTOR('',#6423,1.); +#6423 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6424 = ORIENTED_EDGE('',*,*,#6425,.T.); +#6425 = EDGE_CURVE('',#6418,#6426,#6428,.T.); +#6426 = VERTEX_POINT('',#6427); +#6427 = CARTESIAN_POINT('',(24.781083518149,-1.042471360483E+03, + 183.26634541013)); +#6428 = CIRCLE('',#6429,41.999999999996); +#6429 = AXIS2_PLACEMENT_3D('',#6430,#6431,#6432); +#6430 = CARTESIAN_POINT('',(24.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6431 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6432 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6433 = ORIENTED_EDGE('',*,*,#6434,.F.); +#6434 = EDGE_CURVE('',#6358,#6426,#6435,.T.); +#6435 = LINE('',#6436,#6437); +#6436 = CARTESIAN_POINT('',(19.781083518149,-1.042471360483E+03, + 183.26634541013)); +#6437 = VECTOR('',#6438,1.); +#6438 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6439 = ORIENTED_EDGE('',*,*,#6355,.F.); +#6440 = CYLINDRICAL_SURFACE('',#6441,41.999999999996); +#6441 = AXIS2_PLACEMENT_3D('',#6442,#6443,#6444); +#6442 = CARTESIAN_POINT('',(19.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6443 = DIRECTION('',(-1.,4.254871108656E-15,5.771262587054E-15)); +#6444 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6445 = ADVANCED_FACE('',(#6446),#6472,.T.); +#6446 = FACE_BOUND('',#6447,.F.); +#6447 = EDGE_LOOP('',(#6448,#6456,#6465,#6471)); +#6448 = ORIENTED_EDGE('',*,*,#6449,.T.); +#6449 = EDGE_CURVE('',#6376,#6450,#6452,.T.); +#6450 = VERTEX_POINT('',#6451); +#6451 = CARTESIAN_POINT('',(24.781083518149,-983.7120954224, + 212.08550517484)); +#6452 = LINE('',#6453,#6454); +#6453 = CARTESIAN_POINT('',(19.781083518149,-983.7120954224, + 212.08550517484)); +#6454 = VECTOR('',#6455,1.); +#6455 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6456 = ORIENTED_EDGE('',*,*,#6457,.T.); +#6457 = EDGE_CURVE('',#6450,#6458,#6460,.T.); +#6458 = VERTEX_POINT('',#6459); +#6459 = CARTESIAN_POINT('',(24.781083518149,-1.055632099171E+03, + 192.36551884484)); +#6460 = CIRCLE('',#6461,58.); +#6461 = AXIS2_PLACEMENT_3D('',#6462,#6463,#6464); +#6462 = CARTESIAN_POINT('',(24.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6463 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6464 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6465 = ORIENTED_EDGE('',*,*,#6466,.F.); +#6466 = EDGE_CURVE('',#6385,#6458,#6467,.T.); +#6467 = LINE('',#6468,#6469); +#6468 = CARTESIAN_POINT('',(19.781083518149,-1.055632099171E+03, + 192.36551884484)); +#6469 = VECTOR('',#6470,1.); +#6470 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6471 = ORIENTED_EDGE('',*,*,#6384,.F.); +#6472 = CYLINDRICAL_SURFACE('',#6473,58.); +#6473 = AXIS2_PLACEMENT_3D('',#6474,#6475,#6476); +#6474 = CARTESIAN_POINT('',(19.781083518149,-1.007924421428E+03, + 159.38101514403)); +#6475 = DIRECTION('',(-1.,4.254871108656E-15,5.771262587054E-15)); +#6476 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6477 = ADVANCED_FACE('',(#6478),#6514,.T.); +#6478 = FACE_BOUND('',#6479,.T.); +#6479 = EDGE_LOOP('',(#6480,#6481,#6490,#6497,#6498,#6507)); +#6480 = ORIENTED_EDGE('',*,*,#6425,.F.); +#6481 = ORIENTED_EDGE('',*,*,#6482,.F.); +#6482 = EDGE_CURVE('',#6483,#6418,#6485,.T.); +#6483 = VERTEX_POINT('',#6484); +#6484 = CARTESIAN_POINT('',(24.781083518149,-987.0517250749, + 212.81592034301)); +#6485 = CIRCLE('',#6486,8.); +#6486 = AXIS2_PLACEMENT_3D('',#6487,#6488,#6489); +#6487 = CARTESIAN_POINT('',(24.781083518149,-987.0517265955, + 204.81592034301)); +#6488 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6489 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6490 = ORIENTED_EDGE('',*,*,#6491,.F.); +#6491 = EDGE_CURVE('',#6450,#6483,#6492,.T.); +#6492 = CIRCLE('',#6493,8.); +#6493 = AXIS2_PLACEMENT_3D('',#6494,#6495,#6496); +#6494 = CARTESIAN_POINT('',(24.781083518149,-987.0517265955, + 204.81592034301)); +#6495 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6496 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6497 = ORIENTED_EDGE('',*,*,#6457,.T.); +#6498 = ORIENTED_EDGE('',*,*,#6499,.F.); +#6499 = EDGE_CURVE('',#6500,#6458,#6502,.T.); +#6500 = VERTEX_POINT('',#6501); +#6501 = CARTESIAN_POINT('',(24.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6502 = CIRCLE('',#6503,8.); +#6503 = AXIS2_PLACEMENT_3D('',#6504,#6505,#6506); +#6504 = CARTESIAN_POINT('',(24.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6505 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6506 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6507 = ORIENTED_EDGE('',*,*,#6508,.F.); +#6508 = EDGE_CURVE('',#6426,#6500,#6509,.T.); +#6509 = CIRCLE('',#6510,8.); +#6510 = AXIS2_PLACEMENT_3D('',#6511,#6512,#6513); +#6511 = CARTESIAN_POINT('',(24.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6512 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6513 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6514 = PLANE('',#6515); +#6515 = AXIS2_PLACEMENT_3D('',#6516,#6517,#6518); +#6516 = CARTESIAN_POINT('',(24.781083518149,-1.019431905347E+03, + 201.34951718971)); +#6517 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6518 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6519 = ADVANCED_FACE('',(#6520),#6558,.T.); +#6520 = FACE_BOUND('',#6521,.F.); +#6521 = EDGE_LOOP('',(#6522,#6523,#6524,#6525,#6532,#6539,#6540,#6541, + #6542,#6543,#6550,#6557)); +#6522 = ORIENTED_EDGE('',*,*,#6508,.F.); +#6523 = ORIENTED_EDGE('',*,*,#6434,.F.); +#6524 = ORIENTED_EDGE('',*,*,#6402,.F.); +#6525 = ORIENTED_EDGE('',*,*,#6526,.F.); +#6526 = EDGE_CURVE('',#6527,#6394,#6529,.T.); +#6527 = VERTEX_POINT('',#6528); +#6528 = CARTESIAN_POINT('',(17.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6529 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6530,#6531),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,0.),.PIECEWISE_BEZIER_KNOTS.); +#6530 = CARTESIAN_POINT('',(17.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6531 = CARTESIAN_POINT('',(19.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6532 = ORIENTED_EDGE('',*,*,#6533,.T.); +#6533 = EDGE_CURVE('',#6527,#6527,#6534,.T.); +#6534 = CIRCLE('',#6535,8.); +#6535 = AXIS2_PLACEMENT_3D('',#6536,#6537,#6538); +#6536 = CARTESIAN_POINT('',(17.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6537 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6538 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6539 = ORIENTED_EDGE('',*,*,#6526,.T.); +#6540 = ORIENTED_EDGE('',*,*,#6393,.F.); +#6541 = ORIENTED_EDGE('',*,*,#6466,.T.); +#6542 = ORIENTED_EDGE('',*,*,#6499,.F.); +#6543 = ORIENTED_EDGE('',*,*,#6544,.T.); +#6544 = EDGE_CURVE('',#6500,#6545,#6547,.T.); +#6545 = VERTEX_POINT('',#6546); +#6546 = CARTESIAN_POINT('',(26.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6547 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6548,#6549),.UNSPECIFIED.,.F., + .F.,(2,2),(5.,7.),.PIECEWISE_BEZIER_KNOTS.); +#6548 = CARTESIAN_POINT('',(24.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6549 = CARTESIAN_POINT('',(26.781083518149,-1.049051728306E+03, + 195.81593212749)); +#6550 = ORIENTED_EDGE('',*,*,#6551,.T.); +#6551 = EDGE_CURVE('',#6545,#6545,#6552,.T.); +#6552 = CIRCLE('',#6553,8.); +#6553 = AXIS2_PLACEMENT_3D('',#6554,#6555,#6556); +#6554 = CARTESIAN_POINT('',(26.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6555 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6556 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6557 = ORIENTED_EDGE('',*,*,#6544,.F.); +#6558 = CYLINDRICAL_SURFACE('',#6559,8.); +#6559 = AXIS2_PLACEMENT_3D('',#6560,#6561,#6562); +#6560 = CARTESIAN_POINT('',(19.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6561 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6562 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6563 = ADVANCED_FACE('',(#6564),#6620,.F.); +#6564 = FACE_BOUND('',#6565,.F.); +#6565 = EDGE_LOOP('',(#6566,#6577,#6586,#6595,#6604,#6613)); +#6566 = ORIENTED_EDGE('',*,*,#6567,.F.); +#6567 = EDGE_CURVE('',#6568,#6570,#6572,.T.); +#6568 = VERTEX_POINT('',#6569); +#6569 = CARTESIAN_POINT('',(-19.21891648184,-990.3913763734, + 197.54635820058)); +#6570 = VERTEX_POINT('',#6571); +#6571 = CARTESIAN_POINT('',(-19.21891648184,-1.042471372992E+03, + 183.26634586719)); +#6572 = CIRCLE('',#6573,41.999999999996); +#6573 = AXIS2_PLACEMENT_3D('',#6574,#6575,#6576); +#6574 = CARTESIAN_POINT('',(-19.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6575 = DIRECTION('',(1.,0.,0.)); +#6576 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6577 = ORIENTED_EDGE('',*,*,#6578,.T.); +#6578 = EDGE_CURVE('',#6568,#6579,#6581,.T.); +#6579 = VERTEX_POINT('',#6580); +#6580 = CARTESIAN_POINT('',(-19.21891648184,-987.051750198, + 212.81594445807)); +#6581 = CIRCLE('',#6582,8.); +#6582 = AXIS2_PLACEMENT_3D('',#6583,#6584,#6585); +#6583 = CARTESIAN_POINT('',(-19.21891648184,-987.0517483035, + 204.81594445807)); +#6584 = DIRECTION('',(-1.,0.,0.)); +#6585 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6586 = ORIENTED_EDGE('',*,*,#6587,.T.); +#6587 = EDGE_CURVE('',#6579,#6588,#6590,.T.); +#6588 = VERTEX_POINT('',#6589); +#6589 = CARTESIAN_POINT('',(-19.21891648184,-983.7121202336, + 212.08553071555)); +#6590 = CIRCLE('',#6591,8.); +#6591 = AXIS2_PLACEMENT_3D('',#6592,#6593,#6594); +#6592 = CARTESIAN_POINT('',(-19.21891648184,-987.0517483035, + 204.81594445807)); +#6593 = DIRECTION('',(-1.,0.,0.)); +#6594 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6595 = ORIENTED_EDGE('',*,*,#6596,.T.); +#6596 = EDGE_CURVE('',#6588,#6597,#6599,.T.); +#6597 = VERTEX_POINT('',#6598); +#6598 = CARTESIAN_POINT('',(-19.21891648184,-1.055632115564E+03, + 192.36551368373)); +#6599 = CIRCLE('',#6600,58.); +#6600 = AXIS2_PLACEMENT_3D('',#6601,#6602,#6603); +#6601 = CARTESIAN_POINT('',(-19.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6602 = DIRECTION('',(1.,0.,0.)); +#6603 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6604 = ORIENTED_EDGE('',*,*,#6605,.T.); +#6605 = EDGE_CURVE('',#6597,#6606,#6608,.T.); +#6606 = VERTEX_POINT('',#6607); +#6607 = CARTESIAN_POINT('',(-19.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6608 = CIRCLE('',#6609,8.); +#6609 = AXIS2_PLACEMENT_3D('',#6610,#6611,#6612); +#6610 = CARTESIAN_POINT('',(-19.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6611 = DIRECTION('',(-1.,0.,0.)); +#6612 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6613 = ORIENTED_EDGE('',*,*,#6614,.T.); +#6614 = EDGE_CURVE('',#6606,#6570,#6615,.T.); +#6615 = CIRCLE('',#6616,8.); +#6616 = AXIS2_PLACEMENT_3D('',#6617,#6618,#6619); +#6617 = CARTESIAN_POINT('',(-19.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6618 = DIRECTION('',(-1.,0.,0.)); +#6619 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6620 = PLANE('',#6621); +#6621 = AXIS2_PLACEMENT_3D('',#6622,#6623,#6624); +#6622 = CARTESIAN_POINT('',(-19.21891648184,-1.019431925576E+03, + 201.34952748205)); +#6623 = DIRECTION('',(1.,0.,0.)); +#6624 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6625 = ADVANCED_FACE('',(#6626),#6652,.F.); +#6626 = FACE_BOUND('',#6627,.T.); +#6627 = EDGE_LOOP('',(#6628,#6636,#6645,#6651)); +#6628 = ORIENTED_EDGE('',*,*,#6629,.T.); +#6629 = EDGE_CURVE('',#6568,#6630,#6632,.T.); +#6630 = VERTEX_POINT('',#6631); +#6631 = CARTESIAN_POINT('',(-14.21891648184,-990.3913763734, + 197.54635820058)); +#6632 = LINE('',#6633,#6634); +#6633 = CARTESIAN_POINT('',(-19.21891648184,-990.3913763734, + 197.54635820058)); +#6634 = VECTOR('',#6635,1.); +#6635 = DIRECTION('',(1.,0.,0.)); +#6636 = ORIENTED_EDGE('',*,*,#6637,.T.); +#6637 = EDGE_CURVE('',#6630,#6638,#6640,.T.); +#6638 = VERTEX_POINT('',#6639); +#6639 = CARTESIAN_POINT('',(-14.21891648184,-1.042471372992E+03, + 183.26634586719)); +#6640 = CIRCLE('',#6641,41.999999999996); +#6641 = AXIS2_PLACEMENT_3D('',#6642,#6643,#6644); +#6642 = CARTESIAN_POINT('',(-14.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6643 = DIRECTION('',(1.,0.,0.)); +#6644 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6645 = ORIENTED_EDGE('',*,*,#6646,.F.); +#6646 = EDGE_CURVE('',#6570,#6638,#6647,.T.); +#6647 = LINE('',#6648,#6649); +#6648 = CARTESIAN_POINT('',(-19.21891648184,-1.042471372992E+03, + 183.26634586719)); +#6649 = VECTOR('',#6650,1.); +#6650 = DIRECTION('',(1.,0.,0.)); +#6651 = ORIENTED_EDGE('',*,*,#6567,.F.); +#6652 = CYLINDRICAL_SURFACE('',#6653,41.999999999996); +#6653 = AXIS2_PLACEMENT_3D('',#6654,#6655,#6656); +#6654 = CARTESIAN_POINT('',(-19.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6655 = DIRECTION('',(-1.,0.,0.)); +#6656 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6657 = ADVANCED_FACE('',(#6658),#6684,.T.); +#6658 = FACE_BOUND('',#6659,.F.); +#6659 = EDGE_LOOP('',(#6660,#6668,#6677,#6683)); +#6660 = ORIENTED_EDGE('',*,*,#6661,.T.); +#6661 = EDGE_CURVE('',#6588,#6662,#6664,.T.); +#6662 = VERTEX_POINT('',#6663); +#6663 = CARTESIAN_POINT('',(-14.21891648184,-983.7121202336, + 212.08553071555)); +#6664 = LINE('',#6665,#6666); +#6665 = CARTESIAN_POINT('',(-19.21891648184,-983.7121202336, + 212.08553071555)); +#6666 = VECTOR('',#6667,1.); +#6667 = DIRECTION('',(1.,0.,0.)); +#6668 = ORIENTED_EDGE('',*,*,#6669,.T.); +#6669 = EDGE_CURVE('',#6662,#6670,#6672,.T.); +#6670 = VERTEX_POINT('',#6671); +#6671 = CARTESIAN_POINT('',(-14.21891648184,-1.055632115564E+03, + 192.36551368373)); +#6672 = CIRCLE('',#6673,58.); +#6673 = AXIS2_PLACEMENT_3D('',#6674,#6675,#6676); +#6674 = CARTESIAN_POINT('',(-14.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6675 = DIRECTION('',(1.,0.,0.)); +#6676 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6677 = ORIENTED_EDGE('',*,*,#6678,.F.); +#6678 = EDGE_CURVE('',#6597,#6670,#6679,.T.); +#6679 = LINE('',#6680,#6681); +#6680 = CARTESIAN_POINT('',(-19.21891648184,-1.055632115564E+03, + 192.36551368373)); +#6681 = VECTOR('',#6682,1.); +#6682 = DIRECTION('',(1.,0.,0.)); +#6683 = ORIENTED_EDGE('',*,*,#6596,.F.); +#6684 = CYLINDRICAL_SURFACE('',#6685,58.); +#6685 = AXIS2_PLACEMENT_3D('',#6686,#6687,#6688); +#6686 = CARTESIAN_POINT('',(-19.21891648184,-1.00792442374E+03, + 159.38103034879)); +#6687 = DIRECTION('',(-1.,0.,0.)); +#6688 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6689 = ADVANCED_FACE('',(#6690),#6726,.T.); +#6690 = FACE_BOUND('',#6691,.T.); +#6691 = EDGE_LOOP('',(#6692,#6693,#6702,#6709,#6710,#6719)); +#6692 = ORIENTED_EDGE('',*,*,#6637,.F.); +#6693 = ORIENTED_EDGE('',*,*,#6694,.F.); +#6694 = EDGE_CURVE('',#6695,#6630,#6697,.T.); +#6695 = VERTEX_POINT('',#6696); +#6696 = CARTESIAN_POINT('',(-14.21891648184,-987.051750198, + 212.81594445807)); +#6697 = CIRCLE('',#6698,8.); +#6698 = AXIS2_PLACEMENT_3D('',#6699,#6700,#6701); +#6699 = CARTESIAN_POINT('',(-14.21891648184,-987.0517483035, + 204.81594445807)); +#6700 = DIRECTION('',(1.,0.,0.)); +#6701 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6702 = ORIENTED_EDGE('',*,*,#6703,.F.); +#6703 = EDGE_CURVE('',#6662,#6695,#6704,.T.); +#6704 = CIRCLE('',#6705,8.); +#6705 = AXIS2_PLACEMENT_3D('',#6706,#6707,#6708); +#6706 = CARTESIAN_POINT('',(-14.21891648184,-987.0517483035, + 204.81594445807)); +#6707 = DIRECTION('',(1.,0.,0.)); +#6708 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6709 = ORIENTED_EDGE('',*,*,#6669,.T.); +#6710 = ORIENTED_EDGE('',*,*,#6711,.F.); +#6711 = EDGE_CURVE('',#6712,#6670,#6714,.T.); +#6712 = VERTEX_POINT('',#6713); +#6713 = CARTESIAN_POINT('',(-14.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6714 = CIRCLE('',#6715,8.); +#6715 = AXIS2_PLACEMENT_3D('',#6716,#6717,#6718); +#6716 = CARTESIAN_POINT('',(-14.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6717 = DIRECTION('',(1.,0.,0.)); +#6718 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6719 = ORIENTED_EDGE('',*,*,#6720,.F.); +#6720 = EDGE_CURVE('',#6638,#6712,#6721,.T.); +#6721 = CIRCLE('',#6722,8.); +#6722 = AXIS2_PLACEMENT_3D('',#6723,#6724,#6725); +#6723 = CARTESIAN_POINT('',(-14.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6724 = DIRECTION('',(1.,0.,0.)); +#6725 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6726 = PLANE('',#6727); +#6727 = AXIS2_PLACEMENT_3D('',#6728,#6729,#6730); +#6728 = CARTESIAN_POINT('',(-14.21891648184,-1.019431925576E+03, + 201.34952748205)); +#6729 = DIRECTION('',(1.,0.,0.)); +#6730 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6731 = ADVANCED_FACE('',(#6732),#6770,.T.); +#6732 = FACE_BOUND('',#6733,.F.); +#6733 = EDGE_LOOP('',(#6734,#6735,#6736,#6737,#6744,#6751,#6752,#6753, + #6754,#6755,#6762,#6769)); +#6734 = ORIENTED_EDGE('',*,*,#6720,.F.); +#6735 = ORIENTED_EDGE('',*,*,#6646,.F.); +#6736 = ORIENTED_EDGE('',*,*,#6614,.F.); +#6737 = ORIENTED_EDGE('',*,*,#6738,.F.); +#6738 = EDGE_CURVE('',#6739,#6606,#6741,.T.); +#6739 = VERTEX_POINT('',#6740); +#6740 = CARTESIAN_POINT('',(-21.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6741 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6742,#6743),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,0.),.PIECEWISE_BEZIER_KNOTS.); +#6742 = CARTESIAN_POINT('',(-21.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6743 = CARTESIAN_POINT('',(-19.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6744 = ORIENTED_EDGE('',*,*,#6745,.T.); +#6745 = EDGE_CURVE('',#6739,#6739,#6746,.T.); +#6746 = CIRCLE('',#6747,8.); +#6747 = AXIS2_PLACEMENT_3D('',#6748,#6749,#6750); +#6748 = CARTESIAN_POINT('',(-21.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6749 = DIRECTION('',(-1.,0.,0.)); +#6750 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6751 = ORIENTED_EDGE('',*,*,#6738,.T.); +#6752 = ORIENTED_EDGE('',*,*,#6605,.F.); +#6753 = ORIENTED_EDGE('',*,*,#6678,.T.); +#6754 = ORIENTED_EDGE('',*,*,#6711,.F.); +#6755 = ORIENTED_EDGE('',*,*,#6756,.T.); +#6756 = EDGE_CURVE('',#6712,#6757,#6759,.T.); +#6757 = VERTEX_POINT('',#6758); +#6758 = CARTESIAN_POINT('',(-12.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6759 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6760,#6761),.UNSPECIFIED.,.F., + .F.,(2,2),(5.,7.),.PIECEWISE_BEZIER_KNOTS.); +#6760 = CARTESIAN_POINT('',(-14.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6761 = CARTESIAN_POINT('',(-12.21891648184,-1.049051746172E+03, + 195.81592977546)); +#6762 = ORIENTED_EDGE('',*,*,#6763,.T.); +#6763 = EDGE_CURVE('',#6757,#6757,#6764,.T.); +#6764 = CIRCLE('',#6765,8.); +#6765 = AXIS2_PLACEMENT_3D('',#6766,#6767,#6768); +#6766 = CARTESIAN_POINT('',(-12.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6767 = DIRECTION('',(1.,0.,0.)); +#6768 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6769 = ORIENTED_EDGE('',*,*,#6756,.F.); +#6770 = CYLINDRICAL_SURFACE('',#6771,8.); +#6771 = AXIS2_PLACEMENT_3D('',#6772,#6773,#6774); +#6772 = CARTESIAN_POINT('',(-19.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6773 = DIRECTION('',(1.,0.,0.)); +#6774 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6775 = ADVANCED_FACE('',(#6776),#6779,.T.); +#6776 = FACE_BOUND('',#6777,.T.); +#6777 = EDGE_LOOP('',(#6778)); +#6778 = ORIENTED_EDGE('',*,*,#6745,.T.); +#6779 = PLANE('',#6780); +#6780 = AXIS2_PLACEMENT_3D('',#6781,#6782,#6783); +#6781 = CARTESIAN_POINT('',(-21.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6782 = DIRECTION('',(-1.,0.,0.)); +#6783 = DIRECTION('',(0.,2.368162443614E-07,-1.)); +#6784 = ADVANCED_FACE('',(#6785,#6788),#6799,.T.); +#6785 = FACE_BOUND('',#6786,.T.); +#6786 = EDGE_LOOP('',(#6787)); +#6787 = ORIENTED_EDGE('',*,*,#6763,.T.); +#6788 = FACE_BOUND('',#6789,.T.); +#6789 = EDGE_LOOP('',(#6790)); +#6790 = ORIENTED_EDGE('',*,*,#6791,.T.); +#6791 = EDGE_CURVE('',#6792,#6792,#6794,.T.); +#6792 = VERTEX_POINT('',#6793); +#6793 = CARTESIAN_POINT('',(-12.21891648184,-1.044951744278E+03, + 187.81592977546)); +#6794 = CIRCLE('',#6795,4.1); +#6795 = AXIS2_PLACEMENT_3D('',#6796,#6797,#6798); +#6796 = CARTESIAN_POINT('',(-12.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6797 = DIRECTION('',(-1.,0.,0.)); +#6798 = DIRECTION('',(0.,1.,0.)); +#6799 = PLANE('',#6800); +#6800 = AXIS2_PLACEMENT_3D('',#6801,#6802,#6803); +#6801 = CARTESIAN_POINT('',(-12.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6802 = DIRECTION('',(1.,0.,0.)); +#6803 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6804 = ADVANCED_FACE('',(#6805),#6824,.T.); +#6805 = FACE_BOUND('',#6806,.F.); +#6806 = EDGE_LOOP('',(#6807,#6808,#6816,#6823)); +#6807 = ORIENTED_EDGE('',*,*,#6791,.T.); +#6808 = ORIENTED_EDGE('',*,*,#6809,.T.); +#6809 = EDGE_CURVE('',#6792,#6810,#6812,.T.); +#6810 = VERTEX_POINT('',#6811); +#6811 = CARTESIAN_POINT('',(17.781083518149,-1.044951744278E+03, + 187.81592977546)); +#6812 = LINE('',#6813,#6814); +#6813 = CARTESIAN_POINT('',(-21.21891648184,-1.044951744278E+03, + 187.81592977546)); +#6814 = VECTOR('',#6815,1.); +#6815 = DIRECTION('',(1.,0.,0.)); +#6816 = ORIENTED_EDGE('',*,*,#6817,.F.); +#6817 = EDGE_CURVE('',#6810,#6810,#6818,.T.); +#6818 = CIRCLE('',#6819,4.1); +#6819 = AXIS2_PLACEMENT_3D('',#6820,#6821,#6822); +#6820 = CARTESIAN_POINT('',(17.781083518149,-1.049051744278E+03, + 187.81592977546)); +#6821 = DIRECTION('',(-1.,0.,0.)); +#6822 = DIRECTION('',(0.,1.,0.)); +#6823 = ORIENTED_EDGE('',*,*,#6809,.F.); +#6824 = CYLINDRICAL_SURFACE('',#6825,4.1); +#6825 = AXIS2_PLACEMENT_3D('',#6826,#6827,#6828); +#6826 = CARTESIAN_POINT('',(-21.21891648184,-1.049051744278E+03, + 187.81592977546)); +#6827 = DIRECTION('',(-1.,-0.,-0.)); +#6828 = DIRECTION('',(0.,1.,0.)); +#6829 = ADVANCED_FACE('',(#6830,#6833),#6836,.T.); +#6830 = FACE_BOUND('',#6831,.T.); +#6831 = EDGE_LOOP('',(#6832)); +#6832 = ORIENTED_EDGE('',*,*,#6533,.T.); +#6833 = FACE_BOUND('',#6834,.T.); +#6834 = EDGE_LOOP('',(#6835)); +#6835 = ORIENTED_EDGE('',*,*,#6817,.F.); +#6836 = PLANE('',#6837); +#6837 = AXIS2_PLACEMENT_3D('',#6838,#6839,#6840); +#6838 = CARTESIAN_POINT('',(17.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6839 = DIRECTION('',(-1.,4.254871108656E-15,5.771262587054E-15)); +#6840 = DIRECTION('',(-5.771263395787E-15,-1.900722892334E-07,-1.)); +#6841 = ADVANCED_FACE('',(#6842),#6880,.T.); +#6842 = FACE_BOUND('',#6843,.F.); +#6843 = EDGE_LOOP('',(#6844,#6845,#6846,#6847,#6854,#6861,#6862,#6863, + #6864,#6865,#6872,#6879)); +#6844 = ORIENTED_EDGE('',*,*,#6703,.F.); +#6845 = ORIENTED_EDGE('',*,*,#6661,.F.); +#6846 = ORIENTED_EDGE('',*,*,#6587,.F.); +#6847 = ORIENTED_EDGE('',*,*,#6848,.F.); +#6848 = EDGE_CURVE('',#6849,#6579,#6851,.T.); +#6849 = VERTEX_POINT('',#6850); +#6850 = CARTESIAN_POINT('',(-21.21891648184,-987.051750198, + 212.81594445807)); +#6851 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6852,#6853),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,0.),.PIECEWISE_BEZIER_KNOTS.); +#6852 = CARTESIAN_POINT('',(-21.21891648184,-987.051750198, + 212.81594445807)); +#6853 = CARTESIAN_POINT('',(-19.21891648184,-987.051750198, + 212.81594445807)); +#6854 = ORIENTED_EDGE('',*,*,#6855,.T.); +#6855 = EDGE_CURVE('',#6849,#6849,#6856,.T.); +#6856 = CIRCLE('',#6857,8.); +#6857 = AXIS2_PLACEMENT_3D('',#6858,#6859,#6860); +#6858 = CARTESIAN_POINT('',(-21.21891648184,-987.0517483035, + 204.81594445807)); +#6859 = DIRECTION('',(-1.,0.,0.)); +#6860 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6861 = ORIENTED_EDGE('',*,*,#6848,.T.); +#6862 = ORIENTED_EDGE('',*,*,#6578,.F.); +#6863 = ORIENTED_EDGE('',*,*,#6629,.T.); +#6864 = ORIENTED_EDGE('',*,*,#6694,.F.); +#6865 = ORIENTED_EDGE('',*,*,#6866,.T.); +#6866 = EDGE_CURVE('',#6695,#6867,#6869,.T.); +#6867 = VERTEX_POINT('',#6868); +#6868 = CARTESIAN_POINT('',(-12.21891648184,-987.051750198, + 212.81594445807)); +#6869 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6870,#6871),.UNSPECIFIED.,.F., + .F.,(2,2),(5.,7.),.PIECEWISE_BEZIER_KNOTS.); +#6870 = CARTESIAN_POINT('',(-14.21891648184,-987.051750198, + 212.81594445807)); +#6871 = CARTESIAN_POINT('',(-12.21891648184,-987.051750198, + 212.81594445807)); +#6872 = ORIENTED_EDGE('',*,*,#6873,.T.); +#6873 = EDGE_CURVE('',#6867,#6867,#6874,.T.); +#6874 = CIRCLE('',#6875,8.); +#6875 = AXIS2_PLACEMENT_3D('',#6876,#6877,#6878); +#6876 = CARTESIAN_POINT('',(-12.21891648184,-987.0517483035, + 204.81594445807)); +#6877 = DIRECTION('',(1.,0.,0.)); +#6878 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6879 = ORIENTED_EDGE('',*,*,#6866,.F.); +#6880 = CYLINDRICAL_SURFACE('',#6881,8.); +#6881 = AXIS2_PLACEMENT_3D('',#6882,#6883,#6884); +#6882 = CARTESIAN_POINT('',(-19.21891648184,-987.0517483035, + 204.81594445807)); +#6883 = DIRECTION('',(1.,0.,0.)); +#6884 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6885 = ADVANCED_FACE('',(#6886,#6889),#6900,.T.); +#6886 = FACE_BOUND('',#6887,.T.); +#6887 = EDGE_LOOP('',(#6888)); +#6888 = ORIENTED_EDGE('',*,*,#6855,.T.); +#6889 = FACE_BOUND('',#6890,.T.); +#6890 = EDGE_LOOP('',(#6891)); +#6891 = ORIENTED_EDGE('',*,*,#6892,.F.); +#6892 = EDGE_CURVE('',#6893,#6893,#6895,.T.); +#6893 = VERTEX_POINT('',#6894); +#6894 = CARTESIAN_POINT('',(-21.21891648184,-987.0517492508, + 208.81594445807)); +#6895 = CIRCLE('',#6896,4.); +#6896 = AXIS2_PLACEMENT_3D('',#6897,#6898,#6899); +#6897 = CARTESIAN_POINT('',(-21.21891648184,-987.0517483035, + 204.81594445807)); +#6898 = DIRECTION('',(-1.,0.,0.)); +#6899 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6900 = PLANE('',#6901); +#6901 = AXIS2_PLACEMENT_3D('',#6902,#6903,#6904); +#6902 = CARTESIAN_POINT('',(-21.21891648184,-987.0517483035, + 204.81594445807)); +#6903 = DIRECTION('',(-1.,0.,0.)); +#6904 = DIRECTION('',(0.,2.368162443614E-07,-1.)); +#6905 = ADVANCED_FACE('',(#6906,#6909),#6920,.T.); +#6906 = FACE_BOUND('',#6907,.T.); +#6907 = EDGE_LOOP('',(#6908)); +#6908 = ORIENTED_EDGE('',*,*,#6873,.T.); +#6909 = FACE_BOUND('',#6910,.T.); +#6910 = EDGE_LOOP('',(#6911)); +#6911 = ORIENTED_EDGE('',*,*,#6912,.F.); +#6912 = EDGE_CURVE('',#6913,#6913,#6915,.T.); +#6913 = VERTEX_POINT('',#6914); +#6914 = CARTESIAN_POINT('',(-12.21891648184,-987.0517492508, + 208.81594445807)); +#6915 = CIRCLE('',#6916,4.); +#6916 = AXIS2_PLACEMENT_3D('',#6917,#6918,#6919); +#6917 = CARTESIAN_POINT('',(-12.21891648184,-987.0517483035, + 204.81594445807)); +#6918 = DIRECTION('',(1.,0.,0.)); +#6919 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6920 = PLANE('',#6921); +#6921 = AXIS2_PLACEMENT_3D('',#6922,#6923,#6924); +#6922 = CARTESIAN_POINT('',(-12.21891648184,-987.0517483035, + 204.81594445807)); +#6923 = DIRECTION('',(1.,0.,0.)); +#6924 = DIRECTION('',(0.,-2.368162443614E-07,1.)); +#6925 = ADVANCED_FACE('',(#6926),#6936,.F.); +#6926 = FACE_BOUND('',#6927,.T.); +#6927 = EDGE_LOOP('',(#6928,#6929,#6934,#6935)); +#6928 = ORIENTED_EDGE('',*,*,#6912,.T.); +#6929 = ORIENTED_EDGE('',*,*,#6930,.F.); +#6930 = EDGE_CURVE('',#6893,#6913,#6931,.T.); +#6931 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6932,#6933),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,7.),.PIECEWISE_BEZIER_KNOTS.); +#6932 = CARTESIAN_POINT('',(-21.21891648184,-987.0517492508, + 208.81594445807)); +#6933 = CARTESIAN_POINT('',(-12.21891648184,-987.0517492508, + 208.81594445807)); +#6934 = ORIENTED_EDGE('',*,*,#6892,.T.); +#6935 = ORIENTED_EDGE('',*,*,#6930,.T.); +#6936 = CYLINDRICAL_SURFACE('',#6937,4.); +#6937 = AXIS2_PLACEMENT_3D('',#6938,#6939,#6940); +#6938 = CARTESIAN_POINT('',(-19.21891648184,-987.0517483035, + 204.81594445807)); +#6939 = DIRECTION('',(1.,0.,0.)); +#6940 = DIRECTION('',(0.,-2.3681624434E-07,1.)); +#6941 = ADVANCED_FACE('',(#6942),#6945,.T.); +#6942 = FACE_BOUND('',#6943,.T.); +#6943 = EDGE_LOOP('',(#6944)); +#6944 = ORIENTED_EDGE('',*,*,#6551,.T.); +#6945 = PLANE('',#6946); +#6946 = AXIS2_PLACEMENT_3D('',#6947,#6948,#6949); +#6947 = CARTESIAN_POINT('',(26.781083518149,-1.049051729827E+03, + 187.81593212749)); +#6948 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#6949 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#6950 = ADVANCED_FACE('',(#6951),#6989,.T.); +#6951 = FACE_BOUND('',#6952,.F.); +#6952 = EDGE_LOOP('',(#6953,#6954,#6955,#6956,#6963,#6970,#6971,#6972, + #6973,#6974,#6981,#6988)); +#6953 = ORIENTED_EDGE('',*,*,#6491,.F.); +#6954 = ORIENTED_EDGE('',*,*,#6449,.F.); +#6955 = ORIENTED_EDGE('',*,*,#6375,.F.); +#6956 = ORIENTED_EDGE('',*,*,#6957,.F.); +#6957 = EDGE_CURVE('',#6958,#6367,#6960,.T.); +#6958 = VERTEX_POINT('',#6959); +#6959 = CARTESIAN_POINT('',(17.781083518149,-987.0517250749, + 212.81592034301)); +#6960 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6961,#6962),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,0.),.PIECEWISE_BEZIER_KNOTS.); +#6961 = CARTESIAN_POINT('',(17.781083518149,-987.0517250749, + 212.81592034301)); +#6962 = CARTESIAN_POINT('',(19.781083518149,-987.0517250749, + 212.81592034301)); +#6963 = ORIENTED_EDGE('',*,*,#6964,.T.); +#6964 = EDGE_CURVE('',#6958,#6958,#6965,.T.); +#6965 = CIRCLE('',#6966,8.); +#6966 = AXIS2_PLACEMENT_3D('',#6967,#6968,#6969); +#6967 = CARTESIAN_POINT('',(17.781083518149,-987.0517265955, + 204.81592034301)); +#6968 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#6969 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6970 = ORIENTED_EDGE('',*,*,#6957,.T.); +#6971 = ORIENTED_EDGE('',*,*,#6366,.F.); +#6972 = ORIENTED_EDGE('',*,*,#6417,.T.); +#6973 = ORIENTED_EDGE('',*,*,#6482,.F.); +#6974 = ORIENTED_EDGE('',*,*,#6975,.T.); +#6975 = EDGE_CURVE('',#6483,#6976,#6978,.T.); +#6976 = VERTEX_POINT('',#6977); +#6977 = CARTESIAN_POINT('',(26.781083518149,-987.0517250749, + 212.81592034301)); +#6978 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6979,#6980),.UNSPECIFIED.,.F., + .F.,(2,2),(5.,7.),.PIECEWISE_BEZIER_KNOTS.); +#6979 = CARTESIAN_POINT('',(24.781083518149,-987.0517250749, + 212.81592034301)); +#6980 = CARTESIAN_POINT('',(26.781083518149,-987.0517250749, + 212.81592034301)); +#6981 = ORIENTED_EDGE('',*,*,#6982,.T.); +#6982 = EDGE_CURVE('',#6976,#6976,#6983,.T.); +#6983 = CIRCLE('',#6984,8.); +#6984 = AXIS2_PLACEMENT_3D('',#6985,#6986,#6987); +#6985 = CARTESIAN_POINT('',(26.781083518149,-987.0517265955, + 204.81592034301)); +#6986 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6987 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6988 = ORIENTED_EDGE('',*,*,#6975,.F.); +#6989 = CYLINDRICAL_SURFACE('',#6990,8.); +#6990 = AXIS2_PLACEMENT_3D('',#6991,#6992,#6993); +#6991 = CARTESIAN_POINT('',(19.781083518149,-987.0517265955, + 204.81592034301)); +#6992 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#6993 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#6994 = ADVANCED_FACE('',(#6995,#6998),#7009,.T.); +#6995 = FACE_BOUND('',#6996,.T.); +#6996 = EDGE_LOOP('',(#6997)); +#6997 = ORIENTED_EDGE('',*,*,#6964,.T.); +#6998 = FACE_BOUND('',#6999,.T.); +#6999 = EDGE_LOOP('',(#7000)); +#7000 = ORIENTED_EDGE('',*,*,#7001,.F.); +#7001 = EDGE_CURVE('',#7002,#7002,#7004,.T.); +#7002 = VERTEX_POINT('',#7003); +#7003 = CARTESIAN_POINT('',(17.781083518149,-987.0517258352, + 208.81592034301)); +#7004 = CIRCLE('',#7005,4.); +#7005 = AXIS2_PLACEMENT_3D('',#7006,#7007,#7008); +#7006 = CARTESIAN_POINT('',(17.781083518149,-987.0517265955, + 204.81592034301)); +#7007 = DIRECTION('',(-1.,4.25E-15,5.77E-15)); +#7008 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#7009 = PLANE('',#7010); +#7010 = AXIS2_PLACEMENT_3D('',#7011,#7012,#7013); +#7011 = CARTESIAN_POINT('',(17.781083518149,-987.0517265955, + 204.81592034301)); +#7012 = DIRECTION('',(-1.,4.254871108656E-15,5.771262587054E-15)); +#7013 = DIRECTION('',(-5.771263395787E-15,-1.900722892334E-07,-1.)); +#7014 = ADVANCED_FACE('',(#7015,#7018),#7029,.T.); +#7015 = FACE_BOUND('',#7016,.T.); +#7016 = EDGE_LOOP('',(#7017)); +#7017 = ORIENTED_EDGE('',*,*,#6982,.T.); +#7018 = FACE_BOUND('',#7019,.T.); +#7019 = EDGE_LOOP('',(#7020)); +#7020 = ORIENTED_EDGE('',*,*,#7021,.F.); +#7021 = EDGE_CURVE('',#7022,#7022,#7024,.T.); +#7022 = VERTEX_POINT('',#7023); +#7023 = CARTESIAN_POINT('',(26.781083518149,-987.0517258352, + 208.81592034301)); +#7024 = CIRCLE('',#7025,4.); +#7025 = AXIS2_PLACEMENT_3D('',#7026,#7027,#7028); +#7026 = CARTESIAN_POINT('',(26.781083518149,-987.0517265955, + 204.81592034301)); +#7027 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#7028 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#7029 = PLANE('',#7030); +#7030 = AXIS2_PLACEMENT_3D('',#7031,#7032,#7033); +#7031 = CARTESIAN_POINT('',(26.781083518149,-987.0517265955, + 204.81592034301)); +#7032 = DIRECTION('',(1.,-4.254871108656E-15,-5.771262587054E-15)); +#7033 = DIRECTION('',(5.771263395787E-15,1.900722892334E-07,1.)); +#7034 = ADVANCED_FACE('',(#7035),#7045,.F.); +#7035 = FACE_BOUND('',#7036,.T.); +#7036 = EDGE_LOOP('',(#7037,#7038,#7043,#7044)); +#7037 = ORIENTED_EDGE('',*,*,#7021,.T.); +#7038 = ORIENTED_EDGE('',*,*,#7039,.F.); +#7039 = EDGE_CURVE('',#7002,#7022,#7040,.T.); +#7040 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#7041,#7042),.UNSPECIFIED.,.F., + .F.,(2,2),(-2.,7.),.PIECEWISE_BEZIER_KNOTS.); +#7041 = CARTESIAN_POINT('',(17.781083518149,-987.0517258352, + 208.81592034301)); +#7042 = CARTESIAN_POINT('',(26.781083518149,-987.0517258352, + 208.81592034301)); +#7043 = ORIENTED_EDGE('',*,*,#7001,.T.); +#7044 = ORIENTED_EDGE('',*,*,#7039,.T.); +#7045 = CYLINDRICAL_SURFACE('',#7046,4.); +#7046 = AXIS2_PLACEMENT_3D('',#7047,#7048,#7049); +#7047 = CARTESIAN_POINT('',(19.781083518149,-987.0517265955, + 204.81592034301)); +#7048 = DIRECTION('',(1.,-4.25E-15,-5.77E-15)); +#7049 = DIRECTION('',(5.770000807807E-15,1.9007228924E-07,1.)); +#7050 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#7054)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#7051,#7052,#7053)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#7051 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#7052 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#7053 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#7054 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(2.E-05),#7051, + 'distance_accuracy_value','confusion accuracy'); +#7055 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#7056,#7058); +#7056 = ( REPRESENTATION_RELATIONSHIP('','',#6348,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#7057) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#7057 = ITEM_DEFINED_TRANSFORMATION('','',#11,#35); +#7058 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #7059); +#7059 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('6','BucketLink003','',#5,#6343,$ + ); +#7060 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#6345)); +#7061 = SHAPE_DEFINITION_REPRESENTATION(#7062,#7068); +#7062 = PRODUCT_DEFINITION_SHAPE('','',#7063); +#7063 = PRODUCT_DEFINITION('design','',#7064,#7067); +#7064 = PRODUCT_DEFINITION_FORMATION('','',#7065); +#7065 = PRODUCT('BucketLink1','BucketLink1','',(#7066)); +#7066 = PRODUCT_CONTEXT('',#2,'mechanical'); +#7067 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#7068 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#7069),#7733); +#7069 = MANIFOLD_SOLID_BREP('',#7070); +#7070 = CLOSED_SHELL('',(#7071,#7222,#7255,#7279,#7316,#7360,#7384,#7410 + ,#7430,#7447,#7464,#7484,#7525,#7538,#7563,#7590,#7615,#7642,#7659, + #7679,#7696,#7721)); +#7071 = ADVANCED_FACE('',(#7072),#7217,.T.); +#7072 = FACE_BOUND('',#7073,.F.); +#7073 = EDGE_LOOP('',(#7074,#7084,#7091,#7092,#7101,#7109,#7118,#7126, + #7135,#7143,#7152,#7160,#7167,#7168,#7177,#7185,#7192,#7193,#7202, + #7210)); +#7074 = ORIENTED_EDGE('',*,*,#7075,.F.); +#7075 = EDGE_CURVE('',#7076,#7078,#7080,.T.); +#7076 = VERTEX_POINT('',#7077); +#7077 = CARTESIAN_POINT('',(-9.9,-1.01098E+03,112.2)); +#7078 = VERTEX_POINT('',#7079); +#7079 = CARTESIAN_POINT('',(-7.9,-1.01098E+03,112.2)); +#7080 = LINE('',#7081,#7082); +#7081 = CARTESIAN_POINT('',(-9.9,-1.01098E+03,112.2)); +#7082 = VECTOR('',#7083,1.); +#7083 = DIRECTION('',(1.,0.,0.)); +#7084 = ORIENTED_EDGE('',*,*,#7085,.F.); +#7085 = EDGE_CURVE('',#7076,#7076,#7086,.T.); +#7086 = CIRCLE('',#7087,8.); +#7087 = AXIS2_PLACEMENT_3D('',#7088,#7089,#7090); +#7088 = CARTESIAN_POINT('',(-9.9,-1.01898E+03,112.2)); +#7089 = DIRECTION('',(1.,0.,0.)); +#7090 = DIRECTION('',(0.,1.,0.)); +#7091 = ORIENTED_EDGE('',*,*,#7075,.T.); +#7092 = ORIENTED_EDGE('',*,*,#7093,.T.); +#7093 = EDGE_CURVE('',#7078,#7094,#7096,.T.); +#7094 = VERTEX_POINT('',#7095); +#7095 = CARTESIAN_POINT('',(-7.9,-1.026559086896E+03,114.76075024664)); +#7096 = CIRCLE('',#7097,8.); +#7097 = AXIS2_PLACEMENT_3D('',#7098,#7099,#7100); +#7098 = CARTESIAN_POINT('',(-7.9,-1.01898E+03,112.2)); +#7099 = DIRECTION('',(1.,0.,0.)); +#7100 = DIRECTION('',(0.,1.,0.)); +#7101 = ORIENTED_EDGE('',*,*,#7102,.T.); +#7102 = EDGE_CURVE('',#7094,#7103,#7105,.T.); +#7103 = VERTEX_POINT('',#7104); +#7104 = CARTESIAN_POINT('',(-2.9,-1.026559086896E+03,114.76075024664)); +#7105 = LINE('',#7106,#7107); +#7106 = CARTESIAN_POINT('',(-7.9,-1.026559086896E+03,114.76075024664)); +#7107 = VECTOR('',#7108,1.); +#7108 = DIRECTION('',(1.,0.,0.)); +#7109 = ORIENTED_EDGE('',*,*,#7110,.F.); +#7110 = EDGE_CURVE('',#7111,#7103,#7113,.T.); +#7111 = VERTEX_POINT('',#7112); +#7112 = CARTESIAN_POINT('',(-2.9,-1.01098E+03,112.2)); +#7113 = CIRCLE('',#7114,8.); +#7114 = AXIS2_PLACEMENT_3D('',#7115,#7116,#7117); +#7115 = CARTESIAN_POINT('',(-2.9,-1.01898E+03,112.2)); +#7116 = DIRECTION('',(1.,0.,0.)); +#7117 = DIRECTION('',(0.,1.,0.)); +#7118 = ORIENTED_EDGE('',*,*,#7119,.T.); +#7119 = EDGE_CURVE('',#7111,#7120,#7122,.T.); +#7120 = VERTEX_POINT('',#7121); +#7121 = CARTESIAN_POINT('',(13.1,-1.01098E+03,112.2)); +#7122 = LINE('',#7123,#7124); +#7123 = CARTESIAN_POINT('',(-9.9,-1.01098E+03,112.2)); +#7124 = VECTOR('',#7125,1.); +#7125 = DIRECTION('',(1.,0.,0.)); +#7126 = ORIENTED_EDGE('',*,*,#7127,.T.); +#7127 = EDGE_CURVE('',#7120,#7128,#7130,.T.); +#7128 = VERTEX_POINT('',#7129); +#7129 = CARTESIAN_POINT('',(13.1,-1.026559086896E+03,114.76075024664)); +#7130 = CIRCLE('',#7131,8.); +#7131 = AXIS2_PLACEMENT_3D('',#7132,#7133,#7134); +#7132 = CARTESIAN_POINT('',(13.1,-1.01898E+03,112.2)); +#7133 = DIRECTION('',(1.,0.,0.)); +#7134 = DIRECTION('',(0.,1.,0.)); +#7135 = ORIENTED_EDGE('',*,*,#7136,.T.); +#7136 = EDGE_CURVE('',#7128,#7137,#7139,.T.); +#7137 = VERTEX_POINT('',#7138); +#7138 = CARTESIAN_POINT('',(18.1,-1.026559086896E+03,114.76075024664)); +#7139 = LINE('',#7140,#7141); +#7140 = CARTESIAN_POINT('',(13.1,-1.026559086896E+03,114.76075024664)); +#7141 = VECTOR('',#7142,1.); +#7142 = DIRECTION('',(1.,0.,0.)); +#7143 = ORIENTED_EDGE('',*,*,#7144,.F.); +#7144 = EDGE_CURVE('',#7145,#7137,#7147,.T.); +#7145 = VERTEX_POINT('',#7146); +#7146 = CARTESIAN_POINT('',(18.1,-1.01098E+03,112.2)); +#7147 = CIRCLE('',#7148,8.); +#7148 = AXIS2_PLACEMENT_3D('',#7149,#7150,#7151); +#7149 = CARTESIAN_POINT('',(18.1,-1.01898E+03,112.2)); +#7150 = DIRECTION('',(1.,0.,0.)); +#7151 = DIRECTION('',(0.,1.,0.)); +#7152 = ORIENTED_EDGE('',*,*,#7153,.T.); +#7153 = EDGE_CURVE('',#7145,#7154,#7156,.T.); +#7154 = VERTEX_POINT('',#7155); +#7155 = CARTESIAN_POINT('',(20.1,-1.01098E+03,112.2)); +#7156 = LINE('',#7157,#7158); +#7157 = CARTESIAN_POINT('',(-9.9,-1.01098E+03,112.2)); +#7158 = VECTOR('',#7159,1.); +#7159 = DIRECTION('',(1.,0.,0.)); +#7160 = ORIENTED_EDGE('',*,*,#7161,.T.); +#7161 = EDGE_CURVE('',#7154,#7154,#7162,.T.); +#7162 = CIRCLE('',#7163,8.); +#7163 = AXIS2_PLACEMENT_3D('',#7164,#7165,#7166); +#7164 = CARTESIAN_POINT('',(20.1,-1.01898E+03,112.2)); +#7165 = DIRECTION('',(1.,0.,0.)); +#7166 = DIRECTION('',(0.,1.,0.)); +#7167 = ORIENTED_EDGE('',*,*,#7153,.F.); +#7168 = ORIENTED_EDGE('',*,*,#7169,.F.); +#7169 = EDGE_CURVE('',#7170,#7145,#7172,.T.); +#7170 = VERTEX_POINT('',#7171); +#7171 = CARTESIAN_POINT('',(18.1,-1.011400913104E+03,109.63924975335)); +#7172 = CIRCLE('',#7173,8.); +#7173 = AXIS2_PLACEMENT_3D('',#7174,#7175,#7176); +#7174 = CARTESIAN_POINT('',(18.1,-1.01898E+03,112.2)); +#7175 = DIRECTION('',(1.,0.,0.)); +#7176 = DIRECTION('',(0.,1.,0.)); +#7177 = ORIENTED_EDGE('',*,*,#7178,.F.); +#7178 = EDGE_CURVE('',#7179,#7170,#7181,.T.); +#7179 = VERTEX_POINT('',#7180); +#7180 = CARTESIAN_POINT('',(13.1,-1.011400913104E+03,109.63924975335)); +#7181 = LINE('',#7182,#7183); +#7182 = CARTESIAN_POINT('',(13.1,-1.011400913104E+03,109.63924975335)); +#7183 = VECTOR('',#7184,1.); +#7184 = DIRECTION('',(1.,0.,0.)); +#7185 = ORIENTED_EDGE('',*,*,#7186,.T.); +#7186 = EDGE_CURVE('',#7179,#7120,#7187,.T.); +#7187 = CIRCLE('',#7188,8.); +#7188 = AXIS2_PLACEMENT_3D('',#7189,#7190,#7191); +#7189 = CARTESIAN_POINT('',(13.1,-1.01898E+03,112.2)); +#7190 = DIRECTION('',(1.,0.,0.)); +#7191 = DIRECTION('',(0.,1.,0.)); +#7192 = ORIENTED_EDGE('',*,*,#7119,.F.); +#7193 = ORIENTED_EDGE('',*,*,#7194,.F.); +#7194 = EDGE_CURVE('',#7195,#7111,#7197,.T.); +#7195 = VERTEX_POINT('',#7196); +#7196 = CARTESIAN_POINT('',(-2.9,-1.011400913104E+03,109.63924975335)); +#7197 = CIRCLE('',#7198,8.); +#7198 = AXIS2_PLACEMENT_3D('',#7199,#7200,#7201); +#7199 = CARTESIAN_POINT('',(-2.9,-1.01898E+03,112.2)); +#7200 = DIRECTION('',(1.,0.,0.)); +#7201 = DIRECTION('',(0.,1.,0.)); +#7202 = ORIENTED_EDGE('',*,*,#7203,.F.); +#7203 = EDGE_CURVE('',#7204,#7195,#7206,.T.); +#7204 = VERTEX_POINT('',#7205); +#7205 = CARTESIAN_POINT('',(-7.9,-1.011400913104E+03,109.63924975335)); +#7206 = LINE('',#7207,#7208); +#7207 = CARTESIAN_POINT('',(-7.9,-1.011400913104E+03,109.63924975335)); +#7208 = VECTOR('',#7209,1.); +#7209 = DIRECTION('',(1.,0.,0.)); +#7210 = ORIENTED_EDGE('',*,*,#7211,.T.); +#7211 = EDGE_CURVE('',#7204,#7078,#7212,.T.); +#7212 = CIRCLE('',#7213,8.); +#7213 = AXIS2_PLACEMENT_3D('',#7214,#7215,#7216); +#7214 = CARTESIAN_POINT('',(-7.9,-1.01898E+03,112.2)); +#7215 = DIRECTION('',(1.,0.,0.)); +#7216 = DIRECTION('',(0.,1.,0.)); +#7217 = CYLINDRICAL_SURFACE('',#7218,8.); +#7218 = AXIS2_PLACEMENT_3D('',#7219,#7220,#7221); +#7219 = CARTESIAN_POINT('',(-9.9,-1.01898E+03,112.2)); +#7220 = DIRECTION('',(-1.,-0.,-0.)); +#7221 = DIRECTION('',(0.,1.,0.)); +#7222 = ADVANCED_FACE('',(#7223),#7250,.F.); +#7223 = FACE_BOUND('',#7224,.F.); +#7224 = EDGE_LOOP('',(#7225,#7226,#7234,#7243,#7249)); +#7225 = ORIENTED_EDGE('',*,*,#7211,.F.); +#7226 = ORIENTED_EDGE('',*,*,#7227,.T.); +#7227 = EDGE_CURVE('',#7204,#7228,#7230,.T.); +#7228 = VERTEX_POINT('',#7229); +#7229 = CARTESIAN_POINT('',(-7.9,-996.0364116243,155.11377112823)); +#7230 = LINE('',#7231,#7232); +#7231 = CARTESIAN_POINT('',(-7.9,-1.011400913104E+03,109.63924975335)); +#7232 = VECTOR('',#7233,1.); +#7233 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7234 = ORIENTED_EDGE('',*,*,#7235,.T.); +#7235 = EDGE_CURVE('',#7228,#7236,#7238,.T.); +#7236 = VERTEX_POINT('',#7237); +#7237 = CARTESIAN_POINT('',(-7.9,-1.011194585416E+03,160.23527162153)); +#7238 = CIRCLE('',#7239,8.); +#7239 = AXIS2_PLACEMENT_3D('',#7240,#7241,#7242); +#7240 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7241 = DIRECTION('',(-1.,0.,0.)); +#7242 = DIRECTION('',(0.,1.,0.)); +#7243 = ORIENTED_EDGE('',*,*,#7244,.F.); +#7244 = EDGE_CURVE('',#7094,#7236,#7245,.T.); +#7245 = LINE('',#7246,#7247); +#7246 = CARTESIAN_POINT('',(-7.9,-1.026559086896E+03,114.76075024664)); +#7247 = VECTOR('',#7248,1.); +#7248 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7249 = ORIENTED_EDGE('',*,*,#7093,.F.); +#7250 = PLANE('',#7251); +#7251 = AXIS2_PLACEMENT_3D('',#7252,#7253,#7254); +#7252 = CARTESIAN_POINT('',(-7.9,-1.010804932645E+03,136.39585664061)); +#7253 = DIRECTION('',(1.,0.,0.)); +#7254 = DIRECTION('',(0.,1.,0.)); +#7255 = ADVANCED_FACE('',(#7256),#7274,.F.); +#7256 = FACE_BOUND('',#7257,.F.); +#7257 = EDGE_LOOP('',(#7258,#7259,#7267,#7273)); +#7258 = ORIENTED_EDGE('',*,*,#7203,.T.); +#7259 = ORIENTED_EDGE('',*,*,#7260,.T.); +#7260 = EDGE_CURVE('',#7195,#7261,#7263,.T.); +#7261 = VERTEX_POINT('',#7262); +#7262 = CARTESIAN_POINT('',(-2.9,-996.0364116243,155.11377112823)); +#7263 = LINE('',#7264,#7265); +#7264 = CARTESIAN_POINT('',(-2.9,-1.011400913104E+03,109.63924975335)); +#7265 = VECTOR('',#7266,1.); +#7266 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7267 = ORIENTED_EDGE('',*,*,#7268,.F.); +#7268 = EDGE_CURVE('',#7228,#7261,#7269,.T.); +#7269 = LINE('',#7270,#7271); +#7270 = CARTESIAN_POINT('',(-7.9,-996.0364116243,155.11377112823)); +#7271 = VECTOR('',#7272,1.); +#7272 = DIRECTION('',(1.,0.,0.)); +#7273 = ORIENTED_EDGE('',*,*,#7227,.F.); +#7274 = PLANE('',#7275); +#7275 = AXIS2_PLACEMENT_3D('',#7276,#7277,#7278); +#7276 = CARTESIAN_POINT('',(-7.9,-1.011400913104E+03,109.63924975335)); +#7277 = DIRECTION('',(0.,-0.947385861977,0.320093780831)); +#7278 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7279 = ADVANCED_FACE('',(#7280,#7300),#7311,.T.); +#7280 = FACE_BOUND('',#7281,.T.); +#7281 = EDGE_LOOP('',(#7282,#7283,#7292,#7298,#7299)); +#7282 = ORIENTED_EDGE('',*,*,#7260,.T.); +#7283 = ORIENTED_EDGE('',*,*,#7284,.T.); +#7284 = EDGE_CURVE('',#7261,#7285,#7287,.T.); +#7285 = VERTEX_POINT('',#7286); +#7286 = CARTESIAN_POINT('',(-2.9,-1.011194585416E+03,160.23527162153)); +#7287 = CIRCLE('',#7288,8.); +#7288 = AXIS2_PLACEMENT_3D('',#7289,#7290,#7291); +#7289 = CARTESIAN_POINT('',(-2.9,-1.00361549852E+03,157.67452137488)); +#7290 = DIRECTION('',(1.,0.,0.)); +#7291 = DIRECTION('',(0.,1.,0.)); +#7292 = ORIENTED_EDGE('',*,*,#7293,.F.); +#7293 = EDGE_CURVE('',#7103,#7285,#7294,.T.); +#7294 = LINE('',#7295,#7296); +#7295 = CARTESIAN_POINT('',(-2.9,-1.026559086896E+03,114.76075024664)); +#7296 = VECTOR('',#7297,1.); +#7297 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7298 = ORIENTED_EDGE('',*,*,#7110,.F.); +#7299 = ORIENTED_EDGE('',*,*,#7194,.F.); +#7300 = FACE_BOUND('',#7301,.T.); +#7301 = EDGE_LOOP('',(#7302)); +#7302 = ORIENTED_EDGE('',*,*,#7303,.F.); +#7303 = EDGE_CURVE('',#7304,#7304,#7306,.T.); +#7304 = VERTEX_POINT('',#7305); +#7305 = CARTESIAN_POINT('',(-2.9,-999.6154985201,157.67452137488)); +#7306 = CIRCLE('',#7307,4.); +#7307 = AXIS2_PLACEMENT_3D('',#7308,#7309,#7310); +#7308 = CARTESIAN_POINT('',(-2.9,-1.00361549852E+03,157.67452137488)); +#7309 = DIRECTION('',(1.,0.,0.)); +#7310 = DIRECTION('',(0.,1.,0.)); +#7311 = PLANE('',#7312); +#7312 = AXIS2_PLACEMENT_3D('',#7313,#7314,#7315); +#7313 = CARTESIAN_POINT('',(-2.9,-1.010804932645E+03,136.39585664061)); +#7314 = DIRECTION('',(1.,0.,0.)); +#7315 = DIRECTION('',(0.,1.,0.)); +#7316 = ADVANCED_FACE('',(#7317,#7344),#7355,.F.); +#7317 = FACE_BOUND('',#7318,.F.); +#7318 = EDGE_LOOP('',(#7319,#7327,#7336,#7342,#7343)); +#7319 = ORIENTED_EDGE('',*,*,#7320,.T.); +#7320 = EDGE_CURVE('',#7179,#7321,#7323,.T.); +#7321 = VERTEX_POINT('',#7322); +#7322 = CARTESIAN_POINT('',(13.1,-996.0364116243,155.11377112823)); +#7323 = LINE('',#7324,#7325); +#7324 = CARTESIAN_POINT('',(13.1,-1.011400913104E+03,109.63924975335)); +#7325 = VECTOR('',#7326,1.); +#7326 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7327 = ORIENTED_EDGE('',*,*,#7328,.T.); +#7328 = EDGE_CURVE('',#7321,#7329,#7331,.T.); +#7329 = VERTEX_POINT('',#7330); +#7330 = CARTESIAN_POINT('',(13.1,-1.011194585416E+03,160.23527162153)); +#7331 = CIRCLE('',#7332,8.); +#7332 = AXIS2_PLACEMENT_3D('',#7333,#7334,#7335); +#7333 = CARTESIAN_POINT('',(13.1,-1.00361549852E+03,157.67452137488)); +#7334 = DIRECTION('',(1.,0.,0.)); +#7335 = DIRECTION('',(0.,1.,0.)); +#7336 = ORIENTED_EDGE('',*,*,#7337,.F.); +#7337 = EDGE_CURVE('',#7128,#7329,#7338,.T.); +#7338 = LINE('',#7339,#7340); +#7339 = CARTESIAN_POINT('',(13.1,-1.026559086896E+03,114.76075024664)); +#7340 = VECTOR('',#7341,1.); +#7341 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7342 = ORIENTED_EDGE('',*,*,#7127,.F.); +#7343 = ORIENTED_EDGE('',*,*,#7186,.F.); +#7344 = FACE_BOUND('',#7345,.F.); +#7345 = EDGE_LOOP('',(#7346)); +#7346 = ORIENTED_EDGE('',*,*,#7347,.F.); +#7347 = EDGE_CURVE('',#7348,#7348,#7350,.T.); +#7348 = VERTEX_POINT('',#7349); +#7349 = CARTESIAN_POINT('',(13.1,-999.6154985201,157.67452137488)); +#7350 = CIRCLE('',#7351,4.); +#7351 = AXIS2_PLACEMENT_3D('',#7352,#7353,#7354); +#7352 = CARTESIAN_POINT('',(13.1,-1.00361549852E+03,157.67452137488)); +#7353 = DIRECTION('',(1.,0.,0.)); +#7354 = DIRECTION('',(0.,1.,0.)); +#7355 = PLANE('',#7356); +#7356 = AXIS2_PLACEMENT_3D('',#7357,#7358,#7359); +#7357 = CARTESIAN_POINT('',(13.1,-1.010804932645E+03,136.39585664061)); +#7358 = DIRECTION('',(1.,0.,0.)); +#7359 = DIRECTION('',(0.,1.,0.)); +#7360 = ADVANCED_FACE('',(#7361),#7379,.F.); +#7361 = FACE_BOUND('',#7362,.F.); +#7362 = EDGE_LOOP('',(#7363,#7364,#7372,#7378)); +#7363 = ORIENTED_EDGE('',*,*,#7178,.T.); +#7364 = ORIENTED_EDGE('',*,*,#7365,.T.); +#7365 = EDGE_CURVE('',#7170,#7366,#7368,.T.); +#7366 = VERTEX_POINT('',#7367); +#7367 = CARTESIAN_POINT('',(18.1,-996.0364116243,155.11377112823)); +#7368 = LINE('',#7369,#7370); +#7369 = CARTESIAN_POINT('',(18.1,-1.011400913104E+03,109.63924975335)); +#7370 = VECTOR('',#7371,1.); +#7371 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7372 = ORIENTED_EDGE('',*,*,#7373,.F.); +#7373 = EDGE_CURVE('',#7321,#7366,#7374,.T.); +#7374 = LINE('',#7375,#7376); +#7375 = CARTESIAN_POINT('',(13.1,-996.0364116243,155.11377112823)); +#7376 = VECTOR('',#7377,1.); +#7377 = DIRECTION('',(1.,0.,0.)); +#7378 = ORIENTED_EDGE('',*,*,#7320,.F.); +#7379 = PLANE('',#7380); +#7380 = AXIS2_PLACEMENT_3D('',#7381,#7382,#7383); +#7381 = CARTESIAN_POINT('',(13.1,-1.011400913104E+03,109.63924975335)); +#7382 = DIRECTION('',(0.,-0.947385861977,0.320093780831)); +#7383 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7384 = ADVANCED_FACE('',(#7385),#7405,.T.); +#7385 = FACE_BOUND('',#7386,.T.); +#7386 = EDGE_LOOP('',(#7387,#7388,#7389,#7398,#7404)); +#7387 = ORIENTED_EDGE('',*,*,#7169,.F.); +#7388 = ORIENTED_EDGE('',*,*,#7365,.T.); +#7389 = ORIENTED_EDGE('',*,*,#7390,.F.); +#7390 = EDGE_CURVE('',#7391,#7366,#7393,.T.); +#7391 = VERTEX_POINT('',#7392); +#7392 = CARTESIAN_POINT('',(18.1,-1.011194585416E+03,160.23527162153)); +#7393 = CIRCLE('',#7394,8.); +#7394 = AXIS2_PLACEMENT_3D('',#7395,#7396,#7397); +#7395 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7396 = DIRECTION('',(1.,0.,0.)); +#7397 = DIRECTION('',(0.,1.,0.)); +#7398 = ORIENTED_EDGE('',*,*,#7399,.F.); +#7399 = EDGE_CURVE('',#7137,#7391,#7400,.T.); +#7400 = LINE('',#7401,#7402); +#7401 = CARTESIAN_POINT('',(18.1,-1.026559086896E+03,114.76075024664)); +#7402 = VECTOR('',#7403,1.); +#7403 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7404 = ORIENTED_EDGE('',*,*,#7144,.F.); +#7405 = PLANE('',#7406); +#7406 = AXIS2_PLACEMENT_3D('',#7407,#7408,#7409); +#7407 = CARTESIAN_POINT('',(18.1,-1.010804932645E+03,136.39585664061)); +#7408 = DIRECTION('',(1.,0.,0.)); +#7409 = DIRECTION('',(0.,1.,0.)); +#7410 = ADVANCED_FACE('',(#7411,#7414),#7425,.T.); +#7411 = FACE_BOUND('',#7412,.T.); +#7412 = EDGE_LOOP('',(#7413)); +#7413 = ORIENTED_EDGE('',*,*,#7161,.T.); +#7414 = FACE_BOUND('',#7415,.T.); +#7415 = EDGE_LOOP('',(#7416)); +#7416 = ORIENTED_EDGE('',*,*,#7417,.F.); +#7417 = EDGE_CURVE('',#7418,#7418,#7420,.T.); +#7418 = VERTEX_POINT('',#7419); +#7419 = CARTESIAN_POINT('',(20.1,-1.01498E+03,112.2)); +#7420 = CIRCLE('',#7421,4.); +#7421 = AXIS2_PLACEMENT_3D('',#7422,#7423,#7424); +#7422 = CARTESIAN_POINT('',(20.1,-1.01898E+03,112.2)); +#7423 = DIRECTION('',(1.,0.,0.)); +#7424 = DIRECTION('',(0.,1.,0.)); +#7425 = PLANE('',#7426); +#7426 = AXIS2_PLACEMENT_3D('',#7427,#7428,#7429); +#7427 = CARTESIAN_POINT('',(20.1,-1.01898E+03,112.2)); +#7428 = DIRECTION('',(1.,0.,0.)); +#7429 = DIRECTION('',(0.,1.,0.)); +#7430 = ADVANCED_FACE('',(#7431),#7442,.T.); +#7431 = FACE_BOUND('',#7432,.T.); +#7432 = EDGE_LOOP('',(#7433,#7434,#7435,#7441)); +#7433 = ORIENTED_EDGE('',*,*,#7136,.T.); +#7434 = ORIENTED_EDGE('',*,*,#7399,.T.); +#7435 = ORIENTED_EDGE('',*,*,#7436,.F.); +#7436 = EDGE_CURVE('',#7329,#7391,#7437,.T.); +#7437 = LINE('',#7438,#7439); +#7438 = CARTESIAN_POINT('',(13.1,-1.011194585416E+03,160.23527162153)); +#7439 = VECTOR('',#7440,1.); +#7440 = DIRECTION('',(1.,0.,0.)); +#7441 = ORIENTED_EDGE('',*,*,#7337,.F.); +#7442 = PLANE('',#7443); +#7443 = AXIS2_PLACEMENT_3D('',#7444,#7445,#7446); +#7444 = CARTESIAN_POINT('',(13.1,-1.026559086896E+03,114.76075024664)); +#7445 = DIRECTION('',(0.,-0.947385861977,0.320093780831)); +#7446 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7447 = ADVANCED_FACE('',(#7448),#7459,.T.); +#7448 = FACE_BOUND('',#7449,.T.); +#7449 = EDGE_LOOP('',(#7450,#7451,#7452,#7458)); +#7450 = ORIENTED_EDGE('',*,*,#7102,.T.); +#7451 = ORIENTED_EDGE('',*,*,#7293,.T.); +#7452 = ORIENTED_EDGE('',*,*,#7453,.F.); +#7453 = EDGE_CURVE('',#7236,#7285,#7454,.T.); +#7454 = LINE('',#7455,#7456); +#7455 = CARTESIAN_POINT('',(-7.9,-1.011194585416E+03,160.23527162153)); +#7456 = VECTOR('',#7457,1.); +#7457 = DIRECTION('',(1.,0.,0.)); +#7458 = ORIENTED_EDGE('',*,*,#7244,.F.); +#7459 = PLANE('',#7460); +#7460 = AXIS2_PLACEMENT_3D('',#7461,#7462,#7463); +#7461 = CARTESIAN_POINT('',(-7.9,-1.026559086896E+03,114.76075024664)); +#7462 = DIRECTION('',(0.,-0.947385861977,0.320093780831)); +#7463 = DIRECTION('',(0.,0.320093780831,0.947385861977)); +#7464 = ADVANCED_FACE('',(#7465,#7468),#7479,.F.); +#7465 = FACE_BOUND('',#7466,.F.); +#7466 = EDGE_LOOP('',(#7467)); +#7467 = ORIENTED_EDGE('',*,*,#7085,.T.); +#7468 = FACE_BOUND('',#7469,.F.); +#7469 = EDGE_LOOP('',(#7470)); +#7470 = ORIENTED_EDGE('',*,*,#7471,.F.); +#7471 = EDGE_CURVE('',#7472,#7472,#7474,.T.); +#7472 = VERTEX_POINT('',#7473); +#7473 = CARTESIAN_POINT('',(-9.9,-1.01498E+03,112.2)); +#7474 = CIRCLE('',#7475,4.); +#7475 = AXIS2_PLACEMENT_3D('',#7476,#7477,#7478); +#7476 = CARTESIAN_POINT('',(-9.9,-1.01898E+03,112.2)); +#7477 = DIRECTION('',(1.,0.,0.)); +#7478 = DIRECTION('',(0.,1.,0.)); +#7479 = PLANE('',#7480); +#7480 = AXIS2_PLACEMENT_3D('',#7481,#7482,#7483); +#7481 = CARTESIAN_POINT('',(-9.9,-1.01898E+03,112.2)); +#7482 = DIRECTION('',(1.,0.,0.)); +#7483 = DIRECTION('',(0.,1.,0.)); +#7484 = ADVANCED_FACE('',(#7485),#7520,.T.); +#7485 = FACE_BOUND('',#7486,.F.); +#7486 = EDGE_LOOP('',(#7487,#7496,#7504,#7511,#7512,#7519)); +#7487 = ORIENTED_EDGE('',*,*,#7488,.T.); +#7488 = EDGE_CURVE('',#7228,#7489,#7491,.T.); +#7489 = VERTEX_POINT('',#7490); +#7490 = CARTESIAN_POINT('',(-7.9,-995.6154985201,157.67452137488)); +#7491 = CIRCLE('',#7492,8.); +#7492 = AXIS2_PLACEMENT_3D('',#7493,#7494,#7495); +#7493 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7494 = DIRECTION('',(1.,0.,0.)); +#7495 = DIRECTION('',(0.,1.,0.)); +#7496 = ORIENTED_EDGE('',*,*,#7497,.T.); +#7497 = EDGE_CURVE('',#7489,#7498,#7500,.T.); +#7498 = VERTEX_POINT('',#7499); +#7499 = CARTESIAN_POINT('',(-9.9,-995.6154985201,157.67452137488)); +#7500 = LINE('',#7501,#7502); +#7501 = CARTESIAN_POINT('',(-7.9,-995.6154985201,157.67452137488)); +#7502 = VECTOR('',#7503,1.); +#7503 = DIRECTION('',(-1.,0.,0.)); +#7504 = ORIENTED_EDGE('',*,*,#7505,.T.); +#7505 = EDGE_CURVE('',#7498,#7498,#7506,.T.); +#7506 = CIRCLE('',#7507,8.); +#7507 = AXIS2_PLACEMENT_3D('',#7508,#7509,#7510); +#7508 = CARTESIAN_POINT('',(-9.9,-1.00361549852E+03,157.67452137488)); +#7509 = DIRECTION('',(-1.,0.,0.)); +#7510 = DIRECTION('',(0.,1.,0.)); +#7511 = ORIENTED_EDGE('',*,*,#7497,.F.); +#7512 = ORIENTED_EDGE('',*,*,#7513,.T.); +#7513 = EDGE_CURVE('',#7489,#7236,#7514,.T.); +#7514 = CIRCLE('',#7515,8.); +#7515 = AXIS2_PLACEMENT_3D('',#7516,#7517,#7518); +#7516 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7517 = DIRECTION('',(1.,0.,0.)); +#7518 = DIRECTION('',(0.,1.,0.)); +#7519 = ORIENTED_EDGE('',*,*,#7235,.F.); +#7520 = CYLINDRICAL_SURFACE('',#7521,8.); +#7521 = AXIS2_PLACEMENT_3D('',#7522,#7523,#7524); +#7522 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7523 = DIRECTION('',(1.,0.,0.)); +#7524 = DIRECTION('',(0.,1.,0.)); +#7525 = ADVANCED_FACE('',(#7526),#7533,.T.); +#7526 = FACE_BOUND('',#7527,.F.); +#7527 = EDGE_LOOP('',(#7528,#7529,#7530,#7531,#7532)); +#7528 = ORIENTED_EDGE('',*,*,#7268,.T.); +#7529 = ORIENTED_EDGE('',*,*,#7284,.T.); +#7530 = ORIENTED_EDGE('',*,*,#7453,.F.); +#7531 = ORIENTED_EDGE('',*,*,#7513,.F.); +#7532 = ORIENTED_EDGE('',*,*,#7488,.F.); +#7533 = CYLINDRICAL_SURFACE('',#7534,8.); +#7534 = AXIS2_PLACEMENT_3D('',#7535,#7536,#7537); +#7535 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7536 = DIRECTION('',(-1.,-0.,-0.)); +#7537 = DIRECTION('',(0.,1.,0.)); +#7538 = ADVANCED_FACE('',(#7539),#7558,.F.); +#7539 = FACE_BOUND('',#7540,.T.); +#7540 = EDGE_LOOP('',(#7541,#7549,#7550,#7551)); +#7541 = ORIENTED_EDGE('',*,*,#7542,.T.); +#7542 = EDGE_CURVE('',#7543,#7304,#7545,.T.); +#7543 = VERTEX_POINT('',#7544); +#7544 = CARTESIAN_POINT('',(-7.9,-999.6154985201,157.67452137488)); +#7545 = LINE('',#7546,#7547); +#7546 = CARTESIAN_POINT('',(-7.9,-999.6154985201,157.67452137488)); +#7547 = VECTOR('',#7548,1.); +#7548 = DIRECTION('',(1.,0.,0.)); +#7549 = ORIENTED_EDGE('',*,*,#7303,.T.); +#7550 = ORIENTED_EDGE('',*,*,#7542,.F.); +#7551 = ORIENTED_EDGE('',*,*,#7552,.F.); +#7552 = EDGE_CURVE('',#7543,#7543,#7553,.T.); +#7553 = CIRCLE('',#7554,4.); +#7554 = AXIS2_PLACEMENT_3D('',#7555,#7556,#7557); +#7555 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7556 = DIRECTION('',(1.,0.,0.)); +#7557 = DIRECTION('',(0.,1.,0.)); +#7558 = CYLINDRICAL_SURFACE('',#7559,4.); +#7559 = AXIS2_PLACEMENT_3D('',#7560,#7561,#7562); +#7560 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7561 = DIRECTION('',(-1.,-0.,-0.)); +#7562 = DIRECTION('',(0.,1.,0.)); +#7563 = ADVANCED_FACE('',(#7564),#7585,.T.); +#7564 = FACE_BOUND('',#7565,.F.); +#7565 = EDGE_LOOP('',(#7566,#7567,#7576,#7583,#7584)); +#7566 = ORIENTED_EDGE('',*,*,#7373,.T.); +#7567 = ORIENTED_EDGE('',*,*,#7568,.T.); +#7568 = EDGE_CURVE('',#7366,#7569,#7571,.T.); +#7569 = VERTEX_POINT('',#7570); +#7570 = CARTESIAN_POINT('',(18.1,-995.6154985201,157.67452137488)); +#7571 = CIRCLE('',#7572,8.); +#7572 = AXIS2_PLACEMENT_3D('',#7573,#7574,#7575); +#7573 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7574 = DIRECTION('',(1.,0.,0.)); +#7575 = DIRECTION('',(0.,1.,0.)); +#7576 = ORIENTED_EDGE('',*,*,#7577,.T.); +#7577 = EDGE_CURVE('',#7569,#7391,#7578,.T.); +#7578 = CIRCLE('',#7579,8.); +#7579 = AXIS2_PLACEMENT_3D('',#7580,#7581,#7582); +#7580 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7581 = DIRECTION('',(1.,0.,0.)); +#7582 = DIRECTION('',(0.,1.,0.)); +#7583 = ORIENTED_EDGE('',*,*,#7436,.F.); +#7584 = ORIENTED_EDGE('',*,*,#7328,.F.); +#7585 = CYLINDRICAL_SURFACE('',#7586,8.); +#7586 = AXIS2_PLACEMENT_3D('',#7587,#7588,#7589); +#7587 = CARTESIAN_POINT('',(13.1,-1.00361549852E+03,157.67452137488)); +#7588 = DIRECTION('',(-1.,-0.,-0.)); +#7589 = DIRECTION('',(0.,1.,0.)); +#7590 = ADVANCED_FACE('',(#7591),#7610,.F.); +#7591 = FACE_BOUND('',#7592,.T.); +#7592 = EDGE_LOOP('',(#7593,#7601,#7608,#7609)); +#7593 = ORIENTED_EDGE('',*,*,#7594,.T.); +#7594 = EDGE_CURVE('',#7348,#7595,#7597,.T.); +#7595 = VERTEX_POINT('',#7596); +#7596 = CARTESIAN_POINT('',(18.1,-999.6154985201,157.67452137488)); +#7597 = LINE('',#7598,#7599); +#7598 = CARTESIAN_POINT('',(13.1,-999.6154985201,157.67452137488)); +#7599 = VECTOR('',#7600,1.); +#7600 = DIRECTION('',(1.,0.,0.)); +#7601 = ORIENTED_EDGE('',*,*,#7602,.T.); +#7602 = EDGE_CURVE('',#7595,#7595,#7603,.T.); +#7603 = CIRCLE('',#7604,4.); +#7604 = AXIS2_PLACEMENT_3D('',#7605,#7606,#7607); +#7605 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7606 = DIRECTION('',(1.,0.,0.)); +#7607 = DIRECTION('',(0.,1.,0.)); +#7608 = ORIENTED_EDGE('',*,*,#7594,.F.); +#7609 = ORIENTED_EDGE('',*,*,#7347,.F.); +#7610 = CYLINDRICAL_SURFACE('',#7611,4.); +#7611 = AXIS2_PLACEMENT_3D('',#7612,#7613,#7614); +#7612 = CARTESIAN_POINT('',(13.1,-1.00361549852E+03,157.67452137488)); +#7613 = DIRECTION('',(-1.,-0.,-0.)); +#7614 = DIRECTION('',(0.,1.,0.)); +#7615 = ADVANCED_FACE('',(#7616),#7637,.T.); +#7616 = FACE_BOUND('',#7617,.F.); +#7617 = EDGE_LOOP('',(#7618,#7619,#7627,#7634,#7635,#7636)); +#7618 = ORIENTED_EDGE('',*,*,#7577,.F.); +#7619 = ORIENTED_EDGE('',*,*,#7620,.T.); +#7620 = EDGE_CURVE('',#7569,#7621,#7623,.T.); +#7621 = VERTEX_POINT('',#7622); +#7622 = CARTESIAN_POINT('',(20.1,-995.6154985201,157.67452137488)); +#7623 = LINE('',#7624,#7625); +#7624 = CARTESIAN_POINT('',(18.1,-995.6154985201,157.67452137488)); +#7625 = VECTOR('',#7626,1.); +#7626 = DIRECTION('',(1.,0.,0.)); +#7627 = ORIENTED_EDGE('',*,*,#7628,.T.); +#7628 = EDGE_CURVE('',#7621,#7621,#7629,.T.); +#7629 = CIRCLE('',#7630,8.); +#7630 = AXIS2_PLACEMENT_3D('',#7631,#7632,#7633); +#7631 = CARTESIAN_POINT('',(20.1,-1.00361549852E+03,157.67452137488)); +#7632 = DIRECTION('',(1.,0.,0.)); +#7633 = DIRECTION('',(0.,1.,0.)); +#7634 = ORIENTED_EDGE('',*,*,#7620,.F.); +#7635 = ORIENTED_EDGE('',*,*,#7568,.F.); +#7636 = ORIENTED_EDGE('',*,*,#7390,.F.); +#7637 = CYLINDRICAL_SURFACE('',#7638,8.); +#7638 = AXIS2_PLACEMENT_3D('',#7639,#7640,#7641); +#7639 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7640 = DIRECTION('',(-1.,-0.,-0.)); +#7641 = DIRECTION('',(0.,1.,0.)); +#7642 = ADVANCED_FACE('',(#7643),#7654,.F.); +#7643 = FACE_BOUND('',#7644,.T.); +#7644 = EDGE_LOOP('',(#7645,#7651,#7652,#7653)); +#7645 = ORIENTED_EDGE('',*,*,#7646,.T.); +#7646 = EDGE_CURVE('',#7472,#7418,#7647,.T.); +#7647 = LINE('',#7648,#7649); +#7648 = CARTESIAN_POINT('',(-9.9,-1.01498E+03,112.2)); +#7649 = VECTOR('',#7650,1.); +#7650 = DIRECTION('',(1.,0.,0.)); +#7651 = ORIENTED_EDGE('',*,*,#7417,.T.); +#7652 = ORIENTED_EDGE('',*,*,#7646,.F.); +#7653 = ORIENTED_EDGE('',*,*,#7471,.F.); +#7654 = CYLINDRICAL_SURFACE('',#7655,4.); +#7655 = AXIS2_PLACEMENT_3D('',#7656,#7657,#7658); +#7656 = CARTESIAN_POINT('',(-9.9,-1.01898E+03,112.2)); +#7657 = DIRECTION('',(-1.,-0.,-0.)); +#7658 = DIRECTION('',(0.,1.,0.)); +#7659 = ADVANCED_FACE('',(#7660,#7663),#7674,.T.); +#7660 = FACE_BOUND('',#7661,.T.); +#7661 = EDGE_LOOP('',(#7662)); +#7662 = ORIENTED_EDGE('',*,*,#7505,.T.); +#7663 = FACE_BOUND('',#7664,.T.); +#7664 = EDGE_LOOP('',(#7665)); +#7665 = ORIENTED_EDGE('',*,*,#7666,.F.); +#7666 = EDGE_CURVE('',#7667,#7667,#7669,.T.); +#7667 = VERTEX_POINT('',#7668); +#7668 = CARTESIAN_POINT('',(-9.9,-999.6154985201,157.67452137488)); +#7669 = CIRCLE('',#7670,4.); +#7670 = AXIS2_PLACEMENT_3D('',#7671,#7672,#7673); +#7671 = CARTESIAN_POINT('',(-9.9,-1.00361549852E+03,157.67452137488)); +#7672 = DIRECTION('',(-1.,0.,0.)); +#7673 = DIRECTION('',(0.,1.,0.)); +#7674 = PLANE('',#7675); +#7675 = AXIS2_PLACEMENT_3D('',#7676,#7677,#7678); +#7676 = CARTESIAN_POINT('',(-9.9,-1.00361549852E+03,157.67452137488)); +#7677 = DIRECTION('',(-1.,-0.,-0.)); +#7678 = DIRECTION('',(0.,-1.,0.)); +#7679 = ADVANCED_FACE('',(#7680),#7691,.F.); +#7680 = FACE_BOUND('',#7681,.T.); +#7681 = EDGE_LOOP('',(#7682,#7688,#7689,#7690)); +#7682 = ORIENTED_EDGE('',*,*,#7683,.T.); +#7683 = EDGE_CURVE('',#7543,#7667,#7684,.T.); +#7684 = LINE('',#7685,#7686); +#7685 = CARTESIAN_POINT('',(-7.9,-999.6154985201,157.67452137488)); +#7686 = VECTOR('',#7687,1.); +#7687 = DIRECTION('',(-1.,0.,0.)); +#7688 = ORIENTED_EDGE('',*,*,#7666,.T.); +#7689 = ORIENTED_EDGE('',*,*,#7683,.F.); +#7690 = ORIENTED_EDGE('',*,*,#7552,.T.); +#7691 = CYLINDRICAL_SURFACE('',#7692,4.); +#7692 = AXIS2_PLACEMENT_3D('',#7693,#7694,#7695); +#7693 = CARTESIAN_POINT('',(-7.9,-1.00361549852E+03,157.67452137488)); +#7694 = DIRECTION('',(1.,0.,0.)); +#7695 = DIRECTION('',(0.,1.,0.)); +#7696 = ADVANCED_FACE('',(#7697),#7716,.F.); +#7697 = FACE_BOUND('',#7698,.T.); +#7698 = EDGE_LOOP('',(#7699,#7707,#7714,#7715)); +#7699 = ORIENTED_EDGE('',*,*,#7700,.T.); +#7700 = EDGE_CURVE('',#7595,#7701,#7703,.T.); +#7701 = VERTEX_POINT('',#7702); +#7702 = CARTESIAN_POINT('',(20.1,-999.6154985201,157.67452137488)); +#7703 = LINE('',#7704,#7705); +#7704 = CARTESIAN_POINT('',(18.1,-999.6154985201,157.67452137488)); +#7705 = VECTOR('',#7706,1.); +#7706 = DIRECTION('',(1.,0.,0.)); +#7707 = ORIENTED_EDGE('',*,*,#7708,.T.); +#7708 = EDGE_CURVE('',#7701,#7701,#7709,.T.); +#7709 = CIRCLE('',#7710,4.); +#7710 = AXIS2_PLACEMENT_3D('',#7711,#7712,#7713); +#7711 = CARTESIAN_POINT('',(20.1,-1.00361549852E+03,157.67452137488)); +#7712 = DIRECTION('',(1.,0.,0.)); +#7713 = DIRECTION('',(0.,1.,0.)); +#7714 = ORIENTED_EDGE('',*,*,#7700,.F.); +#7715 = ORIENTED_EDGE('',*,*,#7602,.F.); +#7716 = CYLINDRICAL_SURFACE('',#7717,4.); +#7717 = AXIS2_PLACEMENT_3D('',#7718,#7719,#7720); +#7718 = CARTESIAN_POINT('',(18.1,-1.00361549852E+03,157.67452137488)); +#7719 = DIRECTION('',(-1.,-0.,-0.)); +#7720 = DIRECTION('',(0.,1.,0.)); +#7721 = ADVANCED_FACE('',(#7722,#7725),#7728,.T.); +#7722 = FACE_BOUND('',#7723,.T.); +#7723 = EDGE_LOOP('',(#7724)); +#7724 = ORIENTED_EDGE('',*,*,#7628,.T.); +#7725 = FACE_BOUND('',#7726,.T.); +#7726 = EDGE_LOOP('',(#7727)); +#7727 = ORIENTED_EDGE('',*,*,#7708,.F.); +#7728 = PLANE('',#7729); +#7729 = AXIS2_PLACEMENT_3D('',#7730,#7731,#7732); +#7730 = CARTESIAN_POINT('',(20.1,-1.00361549852E+03,157.67452137488)); +#7731 = DIRECTION('',(1.,0.,0.)); +#7732 = DIRECTION('',(0.,1.,0.)); +#7733 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#7737)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#7734,#7735,#7736)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#7734 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#7735 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#7736 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#7737 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#7734, + 'distance_accuracy_value','confusion accuracy'); +#7738 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#7739,#7741); +#7739 = ( REPRESENTATION_RELATIONSHIP('','',#7068,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#7740) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#7740 = ITEM_DEFINED_TRANSFORMATION('','',#11,#39); +#7741 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #7742); +#7742 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('7','BucketLink004','',#5,#7063,$ + ); +#7743 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7065)); +#7744 = SHAPE_DEFINITION_REPRESENTATION(#7745,#7751); +#7745 = PRODUCT_DEFINITION_SHAPE('','',#7746); +#7746 = PRODUCT_DEFINITION('design','',#7747,#7750); +#7747 = PRODUCT_DEFINITION_FORMATION('','',#7748); +#7748 = PRODUCT('BoomCylinderOuter','BoomCylinderOuter','',(#7749)); +#7749 = PRODUCT_CONTEXT('',#2,'mechanical'); +#7750 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#7751 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#7752),#8157); +#7752 = MANIFOLD_SOLID_BREP('',#7753); +#7753 = CLOSED_SHELL('',(#7754,#7910,#7946,#7966,#7986,#8006,#8131,#8148 + )); +#7754 = ADVANCED_FACE('',(#7755),#7905,.T.); +#7755 = FACE_BOUND('',#7756,.T.); +#7756 = EDGE_LOOP('',(#7757,#7767,#7774,#7775)); +#7757 = ORIENTED_EDGE('',*,*,#7758,.T.); +#7758 = EDGE_CURVE('',#7759,#7761,#7763,.T.); +#7759 = VERTEX_POINT('',#7760); +#7760 = CARTESIAN_POINT('',(-7.24,-154.8563850798,154.85638507985)); +#7761 = VERTEX_POINT('',#7762); +#7762 = CARTESIAN_POINT('',(-7.24,-265.1650429449,265.16504294495)); +#7763 = LINE('',#7764,#7765); +#7764 = CARTESIAN_POINT('',(-7.24,-150.6137443927,150.61374439273)); +#7765 = VECTOR('',#7766,1.); +#7766 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#7767 = ORIENTED_EDGE('',*,*,#7768,.F.); +#7768 = EDGE_CURVE('',#7761,#7761,#7769,.T.); +#7769 = CIRCLE('',#7770,10.); +#7770 = AXIS2_PLACEMENT_3D('',#7771,#7772,#7773); +#7771 = CARTESIAN_POINT('',(2.76,-265.1650429449,265.16504294495)); +#7772 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#7773 = DIRECTION('',(-1.,7.071067811865E-16,-7.071067811865E-16)); +#7774 = ORIENTED_EDGE('',*,*,#7758,.F.); +#7775 = ORIENTED_EDGE('',*,*,#7776,.T.); +#7776 = EDGE_CURVE('',#7759,#7759,#7777,.T.); +#7777 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#7778,#7779,#7780,#7781,#7782, + #7783,#7784,#7785,#7786,#7787,#7788,#7789,#7790,#7791,#7792,#7793, + #7794,#7795,#7796,#7797,#7798,#7799,#7800,#7801,#7802,#7803,#7804, + #7805,#7806,#7807,#7808,#7809,#7810,#7811,#7812,#7813,#7814,#7815, + #7816,#7817,#7818,#7819,#7820,#7821,#7822,#7823,#7824,#7825,#7826, + #7827,#7828,#7829,#7830,#7831,#7832,#7833,#7834,#7835,#7836,#7837, + #7838,#7839,#7840,#7841,#7842,#7843,#7844,#7845,#7846,#7847,#7848, + #7849,#7850,#7851,#7852,#7853,#7854,#7855,#7856,#7857,#7858,#7859, + #7860,#7861,#7862,#7863,#7864,#7865,#7866,#7867,#7868,#7869,#7870, + #7871,#7872,#7873,#7874,#7875,#7876,#7877,#7878,#7879,#7880,#7881, + #7882,#7883,#7884,#7885,#7886,#7887,#7888,#7889,#7890,#7891,#7892, + #7893,#7894,#7895,#7896,#7897,#7898,#7899,#7900,#7901,#7902,#7903, + #7904),.UNSPECIFIED.,.T.,.F.,(7,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,7),(0.,3.210916218005E-02,5.243126183046E-02, + 7.723174846044E-02,9.587022561839E-02,0.145139357103,0.199517786171, + 0.216908427465,0.250272900865,0.300505292142,0.355434892156, + 0.424678676781,0.468226147483,0.522375272218,0.553020238165, + 0.595697609962,0.645543741553,0.699817295534,0.750438244475, + 0.784108881455,0.801413668305,0.855182392021,0.922710310687, + 0.947795194011,0.967518182086,1.),.UNSPECIFIED.); +#7778 = CARTESIAN_POINT('',(-7.24,-154.8563850798,154.85638507985)); +#7779 = CARTESIAN_POINT('',(-7.24,-155.1584516385,154.55431852112)); +#7780 = CARTESIAN_POINT('',(-7.218101325523,-155.4495965558, + 154.24252732202)); +#7781 = CARTESIAN_POINT('',(-7.174467112218,-155.7276274787, + 153.92335773216)); +#7782 = CARTESIAN_POINT('',(-7.109995279279,-155.9908080824, + 153.59958073105)); +#7783 = CARTESIAN_POINT('',(-7.02631480777,-156.2378917232, + 153.27434687277)); +#7784 = CARTESIAN_POINT('',(-6.862235136215,-156.6137977419, + 152.74650236127)); +#7785 = CARTESIAN_POINT('',(-6.791832635852,-156.7529092046, + 152.54246295134)); +#7786 = CARTESIAN_POINT('',(-6.715061180348,-156.8855321276, + 152.33938917302)); +#7787 = CARTESIAN_POINT('',(-6.632365637944,-157.0117894127, + 152.13769034139)); +#7788 = CARTESIAN_POINT('',(-6.54420078635,-157.1318250248, + 151.93776754511)); +#7789 = CARTESIAN_POINT('',(-6.337334743357,-157.3848949537, + 151.49867524395)); +#7790 = CARTESIAN_POINT('',(-6.216157993544,-157.5149987587, + 151.2605064101)); +#7791 = CARTESIAN_POINT('',(-6.088182779122,-157.6365967019, + 151.02587527692)); +#7792 = CARTESIAN_POINT('',(-5.954022597898,-157.7501625006, + 150.79510387984)); +#7793 = CARTESIAN_POINT('',(-5.814225585097,-157.8561585833, + 150.56846514788)); +#7794 = CARTESIAN_POINT('',(-5.56033975899,-158.0293440758, + 150.17912863184)); +#7795 = CARTESIAN_POINT('',(-5.448475415094,-158.0996443935, + 150.01450753674)); +#7796 = CARTESIAN_POINT('',(-5.333867186799,-158.1661362824, + 149.85238717626)); +#7797 = CARTESIAN_POINT('',(-5.216677246299,-158.2290116519, + 149.69282520699)); +#7798 = CARTESIAN_POINT('',(-5.097044976897,-158.2884538851, + 149.53586881551)); +#7799 = CARTESIAN_POINT('',(-4.652702001383,-158.4931550507, + 148.9736393357)); +#7800 = CARTESIAN_POINT('',(-4.313928067907,-158.6189681044, + 148.58401417445)); +#7801 = CARTESIAN_POINT('',(-3.961078672429,-158.7251889028, + 148.21363879669)); +#7802 = CARTESIAN_POINT('',(-3.595631064369,-158.8145245193, + 147.86320919695)); +#7803 = CARTESIAN_POINT('',(-3.218406478362,-158.8893373329, + 147.53342651311)); +#7804 = CARTESIAN_POINT('',(-2.400789923729,-159.0205406488, + 146.88514813423)); +#7805 = CARTESIAN_POINT('',(-1.957682967465,-159.0742521755, + 146.571194073)); +#7806 = CARTESIAN_POINT('',(-1.501862900004,-159.1154465492, + 146.28555600891)); +#7807 = CARTESIAN_POINT('',(-1.0344161852,-159.1465430165, + 146.03049623001)); +#7808 = CARTESIAN_POINT('',(-0.556114452639,-159.1697630471, + 145.80840714824)); +#7809 = CARTESIAN_POINT('',(8.881457512087E-02,-159.1923811433, + 145.56217113392)); +#7810 = CARTESIAN_POINT('',(0.246080706081,-159.1972373237, + 145.50616346901)); +#7811 = CARTESIAN_POINT('',(0.404267577578,-159.2015268922, + 145.45391181984)); +#7812 = CARTESIAN_POINT('',(0.563309060802,-159.2053044334, + 145.40550533468)); +#7813 = CARTESIAN_POINT('',(0.723143022355,-159.2086198236, + 145.36102533237)); +#7814 = CARTESIAN_POINT('',(1.191766438661,-159.2170748106, + 145.24288401411)); +#7815 = CARTESIAN_POINT('',(1.502622316469,-159.2210945937, + 145.17992181685)); +#7816 = CARTESIAN_POINT('',(1.815537926141,-159.2238775175, + 145.13228165071)); +#7817 = CARTESIAN_POINT('',(2.129833964509,-159.2256556973, + 145.10037337943)); +#7818 = CARTESIAN_POINT('',(2.444866457959,-159.2265453243, + 145.08440970061)); +#7819 = CARTESIAN_POINT('',(3.234454105682,-159.2265453243, + 145.08440970061)); +#7820 = CARTESIAN_POINT('',(3.708212192034,-159.2245287705, + 145.12059466936)); +#7821 = CARTESIAN_POINT('',(4.179465436197,-159.220506847, + 145.19276798136)); +#7822 = CARTESIAN_POINT('',(4.646325257509,-159.2138777566, + 145.29991353356)); +#7823 = CARTESIAN_POINT('',(5.106667607934,-159.2034554189, + 145.4401606141)); +#7824 = CARTESIAN_POINT('',(6.051510978708,-159.1709410228, + 145.79707036859)); +#7825 = CARTESIAN_POINT('',(6.534540415784,-159.147838727, + 146.01976313642)); +#7826 = CARTESIAN_POINT('',(7.00655788807,-159.1167879287, + 146.27610391704)); +#7827 = CARTESIAN_POINT('',(7.466766569644,-159.0755110294, + 146.56362995988)); +#7828 = CARTESIAN_POINT('',(7.914044444957,-159.021526069, + 146.88000833193)); +#7829 = CARTESIAN_POINT('',(8.89258189884,-158.8647183284, + 147.65540893112)); +#7830 = CARTESIAN_POINT('',(9.415208360679,-158.752858328, + 148.13000510797)); +#7831 = CARTESIAN_POINT('',(9.914445556095,-158.6108310898, + 148.64436179911)); +#7832 = CARTESIAN_POINT('',(10.387734749005,-158.4317552449, + 149.19723387336)); +#7833 = CARTESIAN_POINT('',(10.829478305261,-158.2071252721, + 149.7867774179)); +#7834 = CARTESIAN_POINT('',(11.481858631487,-157.7506878672, + 150.80033814771)); +#7835 = CARTESIAN_POINT('',(11.718195869506,-157.5519387615, + 151.20579289169)); +#7836 = CARTESIAN_POINT('',(11.935665378702,-157.3284290732, + 151.62340495189)); +#7837 = CARTESIAN_POINT('',(12.130680745927,-157.0779227684, + 152.05083055018)); +#7838 = CARTESIAN_POINT('',(12.299326204847,-156.7982779277, + 152.48531478364)); +#7839 = CARTESIAN_POINT('',(12.608766295118,-156.1012377371, + 153.46875285075)); +#7840 = CARTESIAN_POINT('',(12.733555023791,-155.6651483898, + 154.02226886077)); +#7841 = CARTESIAN_POINT('',(12.798427597479,-155.1838972713, + 154.56538336514)); +#7842 = CARTESIAN_POINT('',(12.79786856397,-154.666356132, + 155.08217400687)); +#7843 = CARTESIAN_POINT('',(12.733203640874,-154.1261566149, + 155.56098591736)); +#7844 = CARTESIAN_POINT('',(12.545273206328,-153.2713412264, + 156.24007812057)); +#7845 = CARTESIAN_POINT('',(12.459735495392,-152.9610798676, + 156.47065398794)); +#7846 = CARTESIAN_POINT('',(12.357954869789,-152.6511342131, + 156.6863123578)); +#7847 = CARTESIAN_POINT('',(12.241440217236,-152.3431829121, + 156.88717486817)); +#7848 = CARTESIAN_POINT('',(12.111854098477,-152.0389161056, + 157.07352826367)); +#7849 = CARTESIAN_POINT('',(11.774921074524,-151.3237446126, + 157.48571585699)); +#7850 = CARTESIAN_POINT('',(11.556663583794,-150.9171193493, + 157.69875895037)); +#7851 = CARTESIAN_POINT('',(11.319734839346,-150.5218896115, + 157.88754156829)); +#7852 = CARTESIAN_POINT('',(11.066893933684,-150.1392877412, + 158.05456935537)); +#7853 = CARTESIAN_POINT('',(10.800278636131,-149.7701407786, + 158.20215667445)); +#7854 = CARTESIAN_POINT('',(10.195802402372,-149.0002724539, + 158.48453031323)); +#7855 = CARTESIAN_POINT('',(9.853180130849,-148.6042595638, + 158.61315006745)); +#7856 = CARTESIAN_POINT('',(9.496038222047,-148.2280012658, + 158.72152113655)); +#7857 = CARTESIAN_POINT('',(9.125930773556,-147.8722203667, + 158.81247923831)); +#7858 = CARTESIAN_POINT('',(8.743717561131,-147.537637607, + 158.88848506097)); +#7859 = CARTESIAN_POINT('',(7.9207903095,-146.8851484401,159.02054058695 + )); +#7860 = CARTESIAN_POINT('',(7.47768371795,-146.5711946007, + 159.07425208641)); +#7861 = CARTESIAN_POINT('',(7.021863894681,-146.2855566084, + 159.11544646876)); +#7862 = CARTESIAN_POINT('',(6.554417200623,-146.030496733, + 159.14654296216)); +#7863 = CARTESIAN_POINT('',(6.076115166355,-145.8084074207, + 159.16976302214)); +#7864 = CARTESIAN_POINT('',(5.131692583506,-145.4478237865, + 159.20288455938)); +#7865 = CARTESIAN_POINT('',(4.666622370822,-145.304596749, + 159.21358759553)); +#7866 = CARTESIAN_POINT('',(4.194837033017,-145.195136395, + 159.22037486901)); +#7867 = CARTESIAN_POINT('',(3.718524986532,-145.1213880289, + 159.22448455557)); +#7868 = CARTESIAN_POINT('',(3.239627285276,-145.0844097006, + 159.22654532434)); +#7869 = CARTESIAN_POINT('',(2.440974842516,-145.0844097006, + 159.22654532434)); +#7870 = CARTESIAN_POINT('',(2.122056910947,-145.1007700867, + 159.22563358946)); +#7871 = CARTESIAN_POINT('',(1.803903087376,-145.1334703586, + 159.22381127372)); +#7872 = CARTESIAN_POINT('',(1.487179404248,-145.1822874457, + 159.22095631936)); +#7873 = CARTESIAN_POINT('',(1.172589338607,-145.2467918973, + 159.21682492785)); +#7874 = CARTESIAN_POINT('',(0.700712010039,-145.3672098198, + 159.20816020673)); +#7875 = CARTESIAN_POINT('',(0.541261058068,-145.4120668428, + 159.20479777161)); +#7876 = CARTESIAN_POINT('',(0.382606155333,-145.4608283695, + 159.2009703925)); +#7877 = CARTESIAN_POINT('',(0.224808571544,-145.5134131797, + 159.19662810181)); +#7878 = CARTESIAN_POINT('',(6.793353690848E-02,-145.5697324083, + 159.19171630327)); +#7879 = CARTESIAN_POINT('',(-0.572303867149,-145.8159846826, + 159.16896616911)); +#7880 = CARTESIAN_POINT('',(-1.046447355973,-146.0371547589, + 159.14572117905)); +#7881 = CARTESIAN_POINT('',(-1.509887399942,-146.2907131034, + 159.11468310676)); +#7882 = CARTESIAN_POINT('',(-1.961871911175,-146.5743270714, + 159.07368206571)); +#7883 = CARTESIAN_POINT('',(-2.401344394524,-146.885792868, + 159.02035627369)); +#7884 = CARTESIAN_POINT('',(-3.361381357501,-147.646533536, + 158.86651318216)); +#7885 = CARTESIAN_POINT('',(-3.87378493442,-148.1105723037, + 158.75743349721)); +#7886 = CARTESIAN_POINT('',(-4.363753295196,-148.6128186092, + 158.6195184036)); +#7887 = CARTESIAN_POINT('',(-4.828907567106,-149.1520539554, + 158.44632616183)); +#7888 = CARTESIAN_POINT('',(-5.264102660793,-149.7265546106, + 158.22993765465)); +#7889 = CARTESIAN_POINT('',(-5.807718485195,-150.5578774954, + 157.86112529205)); +#7890 = CARTESIAN_POINT('',(-5.949681648059,-150.7875343531, + 157.75392844242)); +#7891 = CARTESIAN_POINT('',(-6.085863839295,-151.0214470599, + 157.63896165329)); +#7892 = CARTESIAN_POINT('',(-6.215695029562,-151.2593322763, + 157.51574290318)); +#7893 = CARTESIAN_POINT('',(-6.338536014707,-151.5008547818, + 157.38377814877)); +#7894 = CARTESIAN_POINT('',(-6.544207710108,-151.9380810439, + 157.13153187396)); +#7895 = CARTESIAN_POINT('',(-6.629992385865,-152.1325741674, + 157.01476476285)); +#7896 = CARTESIAN_POINT('',(-6.710603613425,-152.3287505143, + 156.89211055345)); +#7897 = CARTESIAN_POINT('',(-6.785623429715,-152.5262433094, + 156.76343495264)); +#7898 = CARTESIAN_POINT('',(-6.854642160531,-152.7246780084, + 156.62862231504)); +#7899 = CARTESIAN_POINT('',(-7.02037650005,-153.2514018028, + 156.25529158243)); +#7900 = CARTESIAN_POINT('',(-7.10633661341,-153.5813211679, + 156.00562909157)); +#7901 = CARTESIAN_POINT('',(-7.172608046851,-153.9098561691, + 155.73937630233)); +#7902 = CARTESIAN_POINT('',(-7.217477832968,-154.2337263828, + 155.45780965978)); +#7903 = CARTESIAN_POINT('',(-7.24,-154.5500485395,155.1627216202)); +#7904 = CARTESIAN_POINT('',(-7.24,-154.8563850798,154.85638507985)); +#7905 = CYLINDRICAL_SURFACE('',#7906,10.); +#7906 = AXIS2_PLACEMENT_3D('',#7907,#7908,#7909); +#7907 = CARTESIAN_POINT('',(2.76,-150.6137443927,150.61374439273)); +#7908 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#7909 = DIRECTION('',(-1.,7.071067811865E-16,-7.071067811865E-16)); +#7910 = ADVANCED_FACE('',(#7911,#7938),#7941,.T.); +#7911 = FACE_BOUND('',#7912,.T.); +#7912 = EDGE_LOOP('',(#7913,#7923,#7930,#7931)); +#7913 = ORIENTED_EDGE('',*,*,#7914,.T.); +#7914 = EDGE_CURVE('',#7915,#7917,#7919,.T.); +#7915 = VERTEX_POINT('',#7916); +#7916 = CARTESIAN_POINT('',(-12.24,-133.6431816442,154.85638507985)); +#7917 = VERTEX_POINT('',#7918); +#7918 = CARTESIAN_POINT('',(17.76,-133.6431816442,154.85638507985)); +#7919 = LINE('',#7920,#7921); +#7920 = CARTESIAN_POINT('',(-12.24,-133.6431816442,154.85638507985)); +#7921 = VECTOR('',#7922,1.); +#7922 = DIRECTION('',(1.,0.,0.)); +#7923 = ORIENTED_EDGE('',*,*,#7924,.F.); +#7924 = EDGE_CURVE('',#7917,#7917,#7925,.T.); +#7925 = CIRCLE('',#7926,15.); +#7926 = AXIS2_PLACEMENT_3D('',#7927,#7928,#7929); +#7927 = CARTESIAN_POINT('',(17.76,-144.249783362,144.24978336205)); +#7928 = DIRECTION('',(1.,0.,-0.)); +#7929 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#7930 = ORIENTED_EDGE('',*,*,#7914,.F.); +#7931 = ORIENTED_EDGE('',*,*,#7932,.T.); +#7932 = EDGE_CURVE('',#7915,#7915,#7933,.T.); +#7933 = CIRCLE('',#7934,15.); +#7934 = AXIS2_PLACEMENT_3D('',#7935,#7936,#7937); +#7935 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#7936 = DIRECTION('',(1.,0.,-0.)); +#7937 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#7938 = FACE_BOUND('',#7939,.T.); +#7939 = EDGE_LOOP('',(#7940)); +#7940 = ORIENTED_EDGE('',*,*,#7776,.F.); +#7941 = CYLINDRICAL_SURFACE('',#7942,15.); +#7942 = AXIS2_PLACEMENT_3D('',#7943,#7944,#7945); +#7943 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#7944 = DIRECTION('',(1.,0.,0.)); +#7945 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#7946 = ADVANCED_FACE('',(#7947,#7950),#7961,.T.); +#7947 = FACE_BOUND('',#7948,.T.); +#7948 = EDGE_LOOP('',(#7949)); +#7949 = ORIENTED_EDGE('',*,*,#7768,.T.); +#7950 = FACE_BOUND('',#7951,.T.); +#7951 = EDGE_LOOP('',(#7952)); +#7952 = ORIENTED_EDGE('',*,*,#7953,.F.); +#7953 = EDGE_CURVE('',#7954,#7954,#7956,.T.); +#7954 = VERTEX_POINT('',#7955); +#7955 = CARTESIAN_POINT('',(-3.24,-265.1650429449,265.16504294495)); +#7956 = CIRCLE('',#7957,6.); +#7957 = AXIS2_PLACEMENT_3D('',#7958,#7959,#7960); +#7958 = CARTESIAN_POINT('',(2.76,-265.1650429449,265.16504294495)); +#7959 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#7960 = DIRECTION('',(-1.,7.071067811865E-16,-7.071067811865E-16)); +#7961 = PLANE('',#7962); +#7962 = AXIS2_PLACEMENT_3D('',#7963,#7964,#7965); +#7963 = CARTESIAN_POINT('',(2.76,-265.1650429449,265.16504294495)); +#7964 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#7965 = DIRECTION('',(-1.,7.071067811865E-16,-7.071067811865E-16)); +#7966 = ADVANCED_FACE('',(#7967,#7970),#7981,.F.); +#7967 = FACE_BOUND('',#7968,.F.); +#7968 = EDGE_LOOP('',(#7969)); +#7969 = ORIENTED_EDGE('',*,*,#7932,.T.); +#7970 = FACE_BOUND('',#7971,.F.); +#7971 = EDGE_LOOP('',(#7972)); +#7972 = ORIENTED_EDGE('',*,*,#7973,.F.); +#7973 = EDGE_CURVE('',#7974,#7974,#7976,.T.); +#7974 = VERTEX_POINT('',#7975); +#7975 = CARTESIAN_POINT('',(-12.24,-139.3000358937,149.19953083036)); +#7976 = CIRCLE('',#7977,7.); +#7977 = AXIS2_PLACEMENT_3D('',#7978,#7979,#7980); +#7978 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#7979 = DIRECTION('',(1.,0.,-0.)); +#7980 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#7981 = PLANE('',#7982); +#7982 = AXIS2_PLACEMENT_3D('',#7983,#7984,#7985); +#7983 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#7984 = DIRECTION('',(1.,0.,0.)); +#7985 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#7986 = ADVANCED_FACE('',(#7987,#7990),#8001,.T.); +#7987 = FACE_BOUND('',#7988,.T.); +#7988 = EDGE_LOOP('',(#7989)); +#7989 = ORIENTED_EDGE('',*,*,#7924,.T.); +#7990 = FACE_BOUND('',#7991,.T.); +#7991 = EDGE_LOOP('',(#7992)); +#7992 = ORIENTED_EDGE('',*,*,#7993,.F.); +#7993 = EDGE_CURVE('',#7994,#7994,#7996,.T.); +#7994 = VERTEX_POINT('',#7995); +#7995 = CARTESIAN_POINT('',(17.76,-139.3000358937,149.19953083036)); +#7996 = CIRCLE('',#7997,7.); +#7997 = AXIS2_PLACEMENT_3D('',#7998,#7999,#8000); +#7998 = CARTESIAN_POINT('',(17.76,-144.249783362,144.24978336205)); +#7999 = DIRECTION('',(1.,0.,-0.)); +#8000 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#8001 = PLANE('',#8002); +#8002 = AXIS2_PLACEMENT_3D('',#8003,#8004,#8005); +#8003 = CARTESIAN_POINT('',(17.76,-144.249783362,144.24978336205)); +#8004 = DIRECTION('',(1.,0.,0.)); +#8005 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#8006 = ADVANCED_FACE('',(#8007),#8126,.F.); +#8007 = FACE_BOUND('',#8008,.F.); +#8008 = EDGE_LOOP('',(#8009,#8017,#8018,#8019)); +#8009 = ORIENTED_EDGE('',*,*,#8010,.T.); +#8010 = EDGE_CURVE('',#8011,#7954,#8013,.T.); +#8011 = VERTEX_POINT('',#8012); +#8012 = CARTESIAN_POINT('',(-3.24,-154.8563850798,154.85638507985)); +#8013 = LINE('',#8014,#8015); +#8014 = CARTESIAN_POINT('',(-3.24,-150.6137443927,150.61374439273)); +#8015 = VECTOR('',#8016,1.); +#8016 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#8017 = ORIENTED_EDGE('',*,*,#7953,.F.); +#8018 = ORIENTED_EDGE('',*,*,#8010,.F.); +#8019 = ORIENTED_EDGE('',*,*,#8020,.T.); +#8020 = EDGE_CURVE('',#8011,#8011,#8021,.T.); +#8021 = B_SPLINE_CURVE_WITH_KNOTS('',7,(#8022,#8023,#8024,#8025,#8026, + #8027,#8028,#8029,#8030,#8031,#8032,#8033,#8034,#8035,#8036,#8037, + #8038,#8039,#8040,#8041,#8042,#8043,#8044,#8045,#8046,#8047,#8048, + #8049,#8050,#8051,#8052,#8053,#8054,#8055,#8056,#8057,#8058,#8059, + #8060,#8061,#8062,#8063,#8064,#8065,#8066,#8067,#8068,#8069,#8070, + #8071,#8072,#8073,#8074,#8075,#8076,#8077,#8078,#8079,#8080,#8081, + #8082,#8083,#8084,#8085,#8086,#8087,#8088,#8089,#8090,#8091,#8092, + #8093,#8094,#8095,#8096,#8097,#8098,#8099,#8100,#8101,#8102,#8103, + #8104,#8105,#8106,#8107,#8108,#8109,#8110,#8111,#8112,#8113,#8114, + #8115,#8116,#8117,#8118,#8119,#8120,#8121,#8122,#8123,#8124,#8125), + .UNSPECIFIED.,.T.,.F.,(8,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,8),(0., + 3.932438313537E-02,9.320077792228E-02,0.11822750652,0.223849643943, + 0.25176715844,0.381497408892,0.437609437474,0.473550531953, + 0.526997113201,0.562910442032,0.593681373672,0.725248401701, + 0.77678615013,0.881704355506,0.906310979456,0.937686413657,1.), + .UNSPECIFIED.); +#8022 = CARTESIAN_POINT('',(-3.24,-154.8563850798,154.85638507985)); +#8023 = CARTESIAN_POINT('',(-3.24,-155.0407225496,154.67204761005)); +#8024 = CARTESIAN_POINT('',(-3.226785405172,-155.221026502, + 154.48426837887)); +#8025 = CARTESIAN_POINT('',(-3.200432747103,-155.3962529544, + 154.29413452859)); +#8026 = CARTESIAN_POINT('',(-3.161292785722,-155.5654877284, + 154.10284607829)); +#8027 = CARTESIAN_POINT('',(-3.109987966935,-155.7279609233, + 153.91169617272)); +#8028 = CARTESIAN_POINT('',(-3.047415790589,-155.8830502945, + 153.72204089502)); +#8029 = CARTESIAN_POINT('',(-2.875213360703,-156.2319768325, + 153.27935750741)); +#8030 = CARTESIAN_POINT('',(-2.756739510503,-156.4189116852, + 153.02885345938)); +#8031 = CARTESIAN_POINT('',(-2.621534795304,-156.5915416209, + 152.78513076429)); +#8032 = CARTESIAN_POINT('',(-2.471645220969,-156.7504713272, + 152.54942256808)); +#8033 = CARTESIAN_POINT('',(-2.308992026136,-156.896391718, + 152.32279625887)); +#8034 = CARTESIAN_POINT('',(-2.135394126152,-157.0300371644, + 152.10614646514)); +#8035 = CARTESIAN_POINT('',(-1.867667038872,-157.2088811734, + 151.80452718615)); +#8036 = CARTESIAN_POINT('',(-1.780678290364,-157.2631753648, + 151.71107405608)); +#8037 = CARTESIAN_POINT('',(-1.691748088338,-157.3151289392, + 151.61987384228)); +#8038 = CARTESIAN_POINT('',(-1.600994956943,-157.3648300004, + 151.53095977044)); +#8039 = CARTESIAN_POINT('',(-1.508527127537,-157.4123632865, + 151.44436075032)); +#8040 = CARTESIAN_POINT('',(-1.41444253869,-157.4578101702, + 151.36010137581)); +#8041 = CARTESIAN_POINT('',(-0.915303318023,-157.6845752955, + 150.93255566802)); +#8042 = CARTESIAN_POINT('',(-0.483766560227,-157.8324817736, + 150.62828087041)); +#8043 = CARTESIAN_POINT('',(-3.133411814367E-02,-157.9502522383, + 150.3678400126)); +#8044 = CARTESIAN_POINT('',(0.436841569115,-158.0421530813, + 150.15284006281)); +#8045 = CARTESIAN_POINT('',(0.916855028234,-158.1113554773, + 149.98441603295)); +#8046 = CARTESIAN_POINT('',(1.405557124803,-158.1599297966, + 149.86350844203)); +#8047 = CARTESIAN_POINT('',(2.030964251323,-158.1963577083, + 149.77187435959)); +#8048 = CARTESIAN_POINT('',(2.162047539194,-158.2025880151, + 149.7561125515)); +#8049 = CARTESIAN_POINT('',(2.293395904543,-158.2074508222, + 149.74375857349)); +#8050 = CARTESIAN_POINT('',(2.424937343999,-158.2109591159, + 149.73482184433)); +#8051 = CARTESIAN_POINT('',(2.556599782703,-158.2131211333, + 149.72930838903)); +#8052 = CARTESIAN_POINT('',(2.688311074315,-158.2139403631, + 149.72722083886)); +#8053 = CARTESIAN_POINT('',(3.431941289515,-158.2109767607, + 149.73477410651)); +#8054 = CARTESIAN_POINT('',(4.041202606723,-158.1795238709, + 149.81492824796)); +#8055 = CARTESIAN_POINT('',(4.642459306002,-158.1192964072, + 149.96800083427)); +#8056 = CARTESIAN_POINT('',(5.230184108417,-158.0277923067, + 150.19273008847)); +#8057 = CARTESIAN_POINT('',(5.797495043033,-157.9001099098, + 150.48717903992)); +#8058 = CARTESIAN_POINT('',(6.335415921454,-157.7289449356, + 150.84836850961)); +#8059 = CARTESIAN_POINT('',(7.045098558926,-157.4079631992, + 151.45430748552)); +#8060 = CARTESIAN_POINT('',(7.251854246797,-157.300955479, + 151.64919120688)); +#8061 = CARTESIAN_POINT('',(7.450011567074,-157.1830018321, + 151.8557249246)); +#8062 = CARTESIAN_POINT('',(7.63827999318,-157.0531658904, + 152.07353683614)); +#8063 = CARTESIAN_POINT('',(7.815125416178,-156.9104436928, + 152.30211677695)); +#8064 = CARTESIAN_POINT('',(7.978735405714,-156.753794069, + 152.54077826668)); +#8065 = CARTESIAN_POINT('',(8.221933889369,-156.4722636722, + 152.94738421472)); +#8066 = CARTESIAN_POINT('',(8.310652302704,-156.3561260073, + 153.11002405604)); +#8067 = CARTESIAN_POINT('',(8.392358351137,-156.2337164533, + 153.27598827785)); +#8068 = CARTESIAN_POINT('',(8.466329260682,-156.1050192423, + 153.44468861775)); +#8069 = CARTESIAN_POINT('',(8.53189259015,-155.9700650903, + 153.61551736476)); +#8070 = CARTESIAN_POINT('',(8.588422640423,-155.8289388836, + 153.78785537809)); +#8071 = CARTESIAN_POINT('',(8.705101151213,-155.4629641599, + 154.2186757532)); +#8072 = CARTESIAN_POINT('',(8.7535773444,-155.2308944484,154.47814089048 + )); +#8073 = CARTESIAN_POINT('',(8.778395171353,-154.9875959196, + 154.73563840165)); +#8074 = CARTESIAN_POINT('',(8.778395171353,-154.7356384016, + 154.98759591967)); +#8075 = CARTESIAN_POINT('',(8.7535773444,-154.4781408904,155.23089444848 + )); +#8076 = CARTESIAN_POINT('',(8.705101151213,-154.2186757532, + 155.46296415997)); +#8077 = CARTESIAN_POINT('',(8.588458883166,-153.7879891997, + 155.82882520433)); +#8078 = CARTESIAN_POINT('',(8.531977932575,-153.6157762534, + 155.96985331442)); +#8079 = CARTESIAN_POINT('',(8.466474481871,-153.4450652952, + 156.10472199123)); +#8080 = CARTESIAN_POINT('',(8.392573246029,-153.276477434, + 156.23334347505)); +#8081 = CARTESIAN_POINT('',(8.310946847673,-153.1106227874, + 156.35568430378)); +#8082 = CARTESIAN_POINT('',(8.222319479665,-152.9480924093, + 156.47175766498)); +#8083 = CARTESIAN_POINT('',(8.046202985951,-152.6535238584, + 156.67574335116)); +#8084 = CARTESIAN_POINT('',(7.96028831579,-152.5203180452, + 156.76540084172)); +#8085 = CARTESIAN_POINT('',(7.870079463211,-152.389994841,156.8507452837 + )); +#8086 = CARTESIAN_POINT('',(7.775906990805,-152.2627002492, + 156.93193489838)); +#8087 = CARTESIAN_POINT('',(7.678079460016,-152.1385636614, + 157.0091281379)); +#8088 = CARTESIAN_POINT('',(7.576883773408,-152.0176973051, + 157.08248275888)); +#8089 = CARTESIAN_POINT('',(7.026638290049,-151.3977949723, + 157.45005149902)); +#8090 = CARTESIAN_POINT('',(6.522330682477,-150.9550607424, + 157.68172062504)); +#8091 = CARTESIAN_POINT('',(5.978232290641,-150.578629714, + 157.85941403438)); +#8092 = CARTESIAN_POINT('',(5.40602146311,-150.2717462788, + 157.99330983218)); +#8093 = CARTESIAN_POINT('',(4.813726826989,-150.0363174716, + 158.09058326907)); +#8094 = CARTESIAN_POINT('',(4.206817266348,-149.8739223908, + 158.15582040774)); +#8095 = CARTESIAN_POINT('',(3.34802928181,-149.752545007,158.20402879302 + )); +#8096 = CARTESIAN_POINT('',(3.105193488747,-149.7299802404, + 158.21287779235)); +#8097 = CARTESIAN_POINT('',(2.861705695202,-149.7190936137, + 158.21711445832)); +#8098 = CARTESIAN_POINT('',(2.618019555977,-149.7199238057, + 158.21679128558)); +#8099 = CARTESIAN_POINT('',(2.374585347198,-149.7324688719, + 158.21190563634)); +#8100 = CARTESIAN_POINT('',(2.131857203094,-149.7566862559, + 158.20239979656)); +#8101 = CARTESIAN_POINT('',(1.398549897952,-149.8653861378, + 158.15917440293)); +#8102 = CARTESIAN_POINT('',(0.912733498721,-149.9861479029, + 158.11063746205)); +#8103 = CARTESIAN_POINT('',(0.435539275405,-150.1538780441, + 158.04168572283)); +#8104 = CARTESIAN_POINT('',(-2.993262397984E-02,-150.3676490771, + 157.95027855546)); +#8105 = CARTESIAN_POINT('',(-0.479841251982,-150.6263429888, + 157.83329901026)); +#8106 = CARTESIAN_POINT('',(-0.909127883146,-150.9283813591, + 157.68656003483)); +#8107 = CARTESIAN_POINT('',(-1.405031336128,-151.3517920706, + 157.46226829794)); +#8108 = CARTESIAN_POINT('',(-1.497765098259,-151.434517098, + 157.41771733581)); +#8109 = CARTESIAN_POINT('',(-1.588939722515,-151.5195100867, + 157.37115126045)); +#8110 = CARTESIAN_POINT('',(-1.678462450368,-151.606747832, + 157.32249270612)); +#8111 = CARTESIAN_POINT('',(-1.766230853057,-151.6962030681, + 157.27166116543)); +#8112 = CARTESIAN_POINT('',(-1.852132831592,-151.7878444681, + 157.21857298929)); +#8113 = CARTESIAN_POINT('',(-2.043043474594,-152.0012292489, + 157.09246161403)); +#8114 = CARTESIAN_POINT('',(-2.146830471182,-152.1243436295, + 157.01795694998)); +#8115 = CARTESIAN_POINT('',(-2.247127272725,-152.25088116, + 156.93946081833)); +#8116 = CARTESIAN_POINT('',(-2.343630165704,-152.3807239842, + 156.85680471618)); +#8117 = CARTESIAN_POINT('',(-2.436011687656,-152.5137356037, + 156.76981923093)); +#8118 = CARTESIAN_POINT('',(-2.523920257963,-152.6497614652, + 156.67833505696)); +#8119 = CARTESIAN_POINT('',(-2.771941302073,-153.0644302998, + 156.39122198816)); +#8120 = CARTESIAN_POINT('',(-2.919280229898,-153.3539567836, + 156.18011035898)); +#8121 = CARTESIAN_POINT('',(-3.043476996018,-153.6529916568, + 155.94917099097)); +#8122 = CARTESIAN_POINT('',(-3.140244264679,-153.9570238161, + 155.69931507773)); +#8123 = CARTESIAN_POINT('',(-3.206434758338,-154.261602345, + 155.43218085068)); +#8124 = CARTESIAN_POINT('',(-3.24,-154.5625998348,155.15017032481)); +#8125 = CARTESIAN_POINT('',(-3.24,-154.8563850798,154.85638507985)); +#8126 = CYLINDRICAL_SURFACE('',#8127,6.); +#8127 = AXIS2_PLACEMENT_3D('',#8128,#8129,#8130); +#8128 = CARTESIAN_POINT('',(2.76,-150.6137443927,150.61374439273)); +#8129 = DIRECTION('',(-1.E-15,-0.707106781187,0.707106781187)); +#8130 = DIRECTION('',(-1.,7.071067811865E-16,-7.071067811865E-16)); +#8131 = ADVANCED_FACE('',(#8132),#8143,.F.); +#8132 = FACE_BOUND('',#8133,.F.); +#8133 = EDGE_LOOP('',(#8134,#8135,#8141,#8142)); +#8134 = ORIENTED_EDGE('',*,*,#7993,.F.); +#8135 = ORIENTED_EDGE('',*,*,#8136,.F.); +#8136 = EDGE_CURVE('',#7974,#7994,#8137,.T.); +#8137 = LINE('',#8138,#8139); +#8138 = CARTESIAN_POINT('',(-12.24,-139.3000358937,149.19953083036)); +#8139 = VECTOR('',#8140,1.); +#8140 = DIRECTION('',(1.,0.,0.)); +#8141 = ORIENTED_EDGE('',*,*,#7973,.T.); +#8142 = ORIENTED_EDGE('',*,*,#8136,.T.); +#8143 = CYLINDRICAL_SURFACE('',#8144,7.); +#8144 = AXIS2_PLACEMENT_3D('',#8145,#8146,#8147); +#8145 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#8146 = DIRECTION('',(1.,0.,0.)); +#8147 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#8148 = ADVANCED_FACE('',(#8149),#8152,.T.); +#8149 = FACE_BOUND('',#8150,.T.); +#8150 = EDGE_LOOP('',(#8151)); +#8151 = ORIENTED_EDGE('',*,*,#8020,.T.); +#8152 = CYLINDRICAL_SURFACE('',#8153,15.); +#8153 = AXIS2_PLACEMENT_3D('',#8154,#8155,#8156); +#8154 = CARTESIAN_POINT('',(-12.24,-144.249783362,144.24978336205)); +#8155 = DIRECTION('',(1.,0.,0.)); +#8156 = DIRECTION('',(0.,0.707106781187,0.707106781187)); +#8157 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#8161)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#8158,#8159,#8160)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#8158 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#8159 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#8160 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#8161 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#8158, + 'distance_accuracy_value','confusion accuracy'); +#8162 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#8163,#8165); +#8163 = ( REPRESENTATION_RELATIONSHIP('','',#7751,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#8164) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#8164 = ITEM_DEFINED_TRANSFORMATION('','',#11,#43); +#8165 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #8166); +#8166 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('8','BoomCylinderOuter001','',#5, + #7746,$); +#8167 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#7748)); +#8168 = SHAPE_DEFINITION_REPRESENTATION(#8169,#8175); +#8169 = PRODUCT_DEFINITION_SHAPE('','',#8170); +#8170 = PRODUCT_DEFINITION('design','',#8171,#8174); +#8171 = PRODUCT_DEFINITION_FORMATION('','',#8172); +#8172 = PRODUCT('BoomCylinderInner','BoomCylinderInner','',(#8173)); +#8173 = PRODUCT_CONTEXT('',#2,'mechanical'); +#8174 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#8175 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#8176),#8431); +#8176 = MANIFOLD_SOLID_BREP('',#8177); +#8177 = CLOSED_SHELL('',(#8178,#8329,#8338,#8374,#8394,#8414)); +#8178 = ADVANCED_FACE('',(#8179),#8324,.T.); +#8179 = FACE_BOUND('',#8180,.F.); +#8180 = EDGE_LOOP('',(#8181,#8190,#8198,#8323)); +#8181 = ORIENTED_EDGE('',*,*,#8182,.F.); +#8182 = EDGE_CURVE('',#8183,#8183,#8185,.T.); +#8183 = VERTEX_POINT('',#8184); +#8184 = CARTESIAN_POINT('',(-3.748,-155.1438990476,207.19713613006)); +#8185 = CIRCLE('',#8186,6.); +#8186 = AXIS2_PLACEMENT_3D('',#8187,#8188,#8189); +#8187 = CARTESIAN_POINT('',(2.252,-155.1438990476,207.19713613006)); +#8188 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8189 = DIRECTION('',(-1.,0.,0.)); +#8190 = ORIENTED_EDGE('',*,*,#8191,.T.); +#8191 = EDGE_CURVE('',#8183,#8192,#8194,.T.); +#8192 = VERTEX_POINT('',#8193); +#8193 = CARTESIAN_POINT('',(-3.748,-270.6144364153,322.66767349782)); +#8194 = LINE('',#8195,#8196); +#8195 = CARTESIAN_POINT('',(-3.748,-155.1438990476,207.19713613006)); +#8196 = VECTOR('',#8197,1.); +#8197 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8198 = ORIENTED_EDGE('',*,*,#8199,.T.); +#8199 = EDGE_CURVE('',#8192,#8192,#8200,.T.); +#8200 = B_SPLINE_CURVE_WITH_KNOTS('',7,(#8201,#8202,#8203,#8204,#8205, + #8206,#8207,#8208,#8209,#8210,#8211,#8212,#8213,#8214,#8215,#8216, + #8217,#8218,#8219,#8220,#8221,#8222,#8223,#8224,#8225,#8226,#8227, + #8228,#8229,#8230,#8231,#8232,#8233,#8234,#8235,#8236,#8237,#8238, + #8239,#8240,#8241,#8242,#8243,#8244,#8245,#8246,#8247,#8248,#8249, + #8250,#8251,#8252,#8253,#8254,#8255,#8256,#8257,#8258,#8259,#8260, + #8261,#8262,#8263,#8264,#8265,#8266,#8267,#8268,#8269,#8270,#8271, + #8272,#8273,#8274,#8275,#8276,#8277,#8278,#8279,#8280,#8281,#8282, + #8283,#8284,#8285,#8286,#8287,#8288,#8289,#8290,#8291,#8292,#8293, + #8294,#8295,#8296,#8297,#8298,#8299,#8300,#8301,#8302,#8303,#8304, + #8305,#8306,#8307,#8308,#8309,#8310,#8311,#8312,#8313,#8314,#8315, + #8316,#8317,#8318,#8319,#8320,#8321,#8322),.UNSPECIFIED.,.T.,.F.,(8, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,8),(0.,5.792747495377E-02, + 8.552057468052E-02,0.106135747546,0.168298002248,0.250002115854, + 0.292820771135,0.331595242887,0.415772630736,0.442340942148, + 0.500146776733,0.558040256615,0.58561715796,0.606220225351, + 0.668345977641,0.750002113624,0.792795625282,0.831547339874, + 0.915675348571,0.942228074793,1.),.UNSPECIFIED.); +#8201 = CARTESIAN_POINT('',(-3.748,-270.6144364153,322.66767349782)); +#8202 = CARTESIAN_POINT('',(-3.748,-270.8891342562,322.39297565693)); +#8203 = CARTESIAN_POINT('',(-3.718654577067,-271.172113804, + 322.13074648508)); +#8204 = CARTESIAN_POINT('',(-3.660770361929,-271.4601995603, + 321.88359089367)); +#8205 = CARTESIAN_POINT('',(-3.576088389467,-271.749722254, + 321.65346822978)); +#8206 = CARTESIAN_POINT('',(-3.467281115515,-272.0366391408, + 321.44164453008)); +#8207 = CARTESIAN_POINT('',(-3.338002086341,-272.3167266989, + 321.24872787572)); +#8208 = CARTESIAN_POINT('',(-3.123912559578,-272.7140375385, + 320.99193200045)); +#8209 = CARTESIAN_POINT('',(-3.05097260886,-272.8402862836, + 320.9130300487)); +#8210 = CARTESIAN_POINT('',(-2.974440490066,-272.9644489444, + 320.83793314759)); +#8211 = CARTESIAN_POINT('',(-2.894571152664,-273.0863947976, + 320.76649337463)); +#8212 = CARTESIAN_POINT('',(-2.811602694693,-273.2060057684, + 320.69856333396)); +#8213 = CARTESIAN_POINT('',(-2.725756643329,-273.323176787, + 320.6339967601)); +#8214 = CARTESIAN_POINT('',(-2.571104952178,-273.5234647394, + 320.52681548257)); +#8215 = CARTESIAN_POINT('',(-2.503470474323,-273.6077124319, + 320.48277202273)); +#8216 = CARTESIAN_POINT('',(-2.434411851588,-273.6905302947, + 320.4404540836)); +#8217 = CARTESIAN_POINT('',(-2.364000042073,-273.7718922216, + 320.39979880996)); +#8218 = CARTESIAN_POINT('',(-2.292299912269,-273.8517749281, + 320.36074514979)); +#8219 = CARTESIAN_POINT('',(-2.219370237056,-273.930157952, + 320.32323385435)); +#8220 = CARTESIAN_POINT('',(-1.921805498299,-274.238801737, + 320.17857483207)); +#8221 = CARTESIAN_POINT('',(-1.687528956899,-274.4569050968, + 320.08338637281)); +#8222 = CARTESIAN_POINT('',(-1.443970508042,-274.6606979857, + 320.0001512505)); +#8223 = CARTESIAN_POINT('',(-1.19232299336,-274.8496645223, + 319.92754341196)); +#8224 = CARTESIAN_POINT('',(-0.933496345988,-275.0233535509, + 319.8644036027)); +#8225 = CARTESIAN_POINT('',(-0.668178272969,-275.1813235022, + 319.80974136904)); +#8226 = CARTESIAN_POINT('',(-4.032864911363E-02,-275.5094170741, + 319.70095607428)); +#8227 = CARTESIAN_POINT('',(0.326876235853,-275.6679236612, + 319.65234739872)); +#8228 = CARTESIAN_POINT('',(0.702321125704,-275.7967673239, + 319.61530972461)); +#8229 = CARTESIAN_POINT('',(1.083875450641,-275.8945528502, + 319.58846719898)); +#8230 = CARTESIAN_POINT('',(1.469605359089,-275.9603112712, + 319.57084222589)); +#8231 = CARTESIAN_POINT('',(1.857698709709,-275.9934807105, + 319.56195442408)); +#8232 = CARTESIAN_POINT('',(2.450085802317,-275.9940988866, + 319.56178878412)); +#8233 = CARTESIAN_POINT('',(2.653762506903,-275.985312736, + 319.56414322582)); +#8234 = CARTESIAN_POINT('',(2.8571192128,-275.96753105,319.56890672967) + ); +#8235 = CARTESIAN_POINT('',(3.059852443321,-275.9407984339, + 319.57611468301)); +#8236 = CARTESIAN_POINT('',(3.261652337176,-275.9052010731, + 319.5858374942)); +#8237 = CARTESIAN_POINT('',(3.46220248066,-275.8608668922, + 319.59817788948)); +#8238 = CARTESIAN_POINT('',(3.841363775643,-275.7600610017, + 319.62693326293)); +#8239 = CARTESIAN_POINT('',(4.020247910246,-275.7051338054, + 319.64285241046)); +#8240 = CARTESIAN_POINT('',(4.197662891561,-275.6432919952, + 319.66112850715)); +#8241 = CARTESIAN_POINT('',(4.373427087713,-275.5746563378, + 319.68188821088)); +#8242 = CARTESIAN_POINT('',(4.547346350891,-275.4993604283, + 319.70527998025)); +#8243 = CARTESIAN_POINT('',(4.719213881198,-275.4175506237, + 319.73147207208)); +#8244 = CARTESIAN_POINT('',(5.256994755377,-275.1379850525, + 319.8239954928)); +#8245 = CARTESIAN_POINT('',(5.61393980241,-274.9169113216, + 319.90132336563)); +#8246 = CARTESIAN_POINT('',(5.958610199681,-274.6673424571, + 319.99466479936)); +#8247 = CARTESIAN_POINT('',(6.28931313462,-274.3902413847, + 320.10665695368)); +#8248 = CARTESIAN_POINT('',(6.60347388771,-274.0866068473, + 320.24052244603)); +#8249 = CARTESIAN_POINT('',(6.897347318116,-273.7578257368, + 320.40010025888)); +#8250 = CARTESIAN_POINT('',(7.25028047809,-273.2950286208, + 320.64955104637)); +#8251 = CARTESIAN_POINT('',(7.332434982098,-273.1816648947, + 320.71241186469)); +#8252 = CARTESIAN_POINT('',(7.411886549335,-273.0660494102, + 320.77841834555)); +#8253 = CARTESIAN_POINT('',(7.48843397071,-272.9482789555, + 320.84770079201)); +#8254 = CARTESIAN_POINT('',(7.56186202984,-272.8284615853, + 320.92039025579)); +#8255 = CARTESIAN_POINT('',(7.631941261787,-272.7067162906, + 320.99661801296)); +#8256 = CARTESIAN_POINT('',(7.843085182527,-272.3143734208, + 321.25035048757)); +#8257 = CARTESIAN_POINT('',(7.972040447204,-272.0346355094, + 321.44312423545)); +#8258 = CARTESIAN_POINT('',(8.080567548116,-271.7480928483, + 321.65476169182)); +#8259 = CARTESIAN_POINT('',(8.16502268504,-271.4589633936, + 321.88464866218)); +#8260 = CARTESIAN_POINT('',(8.222743362748,-271.1712840864, + 322.13151341436)); +#8261 = CARTESIAN_POINT('',(8.252,-270.8887183895,322.39339152369)); +#8262 = CARTESIAN_POINT('',(8.252,-270.3397385745,322.94237133871)); +#8263 = CARTESIAN_POINT('',(8.222654577066,-270.0775094026, + 323.22535088646)); +#8264 = CARTESIAN_POINT('',(8.164770361928,-269.8303538112, + 323.51343664275)); +#8265 = CARTESIAN_POINT('',(8.080088389465,-269.6002311473, + 323.80295933653)); +#8266 = CARTESIAN_POINT('',(7.971281115514,-269.3884074476, + 324.08987622324)); +#8267 = CARTESIAN_POINT('',(7.842002086341,-269.1954907932, + 324.36996378142)); +#8268 = CARTESIAN_POINT('',(7.627912572633,-268.9386949336, + 324.76727459671)); +#8269 = CARTESIAN_POINT('',(7.554972612348,-268.8597929676, + 324.89352336272)); +#8270 = CARTESIAN_POINT('',(7.478440480404,-268.7846960544, + 325.01768604388)); +#8271 = CARTESIAN_POINT('',(7.398571140467,-268.7132562828, + 325.13963189687)); +#8272 = CARTESIAN_POINT('',(7.31560269454,-268.6453262534, + 325.25924284886)); +#8273 = CARTESIAN_POINT('',(7.229756658137,-268.5807596879, + 325.37641385034)); +#8274 = CARTESIAN_POINT('',(7.075104952177,-268.4735784001, + 325.57670182193)); +#8275 = CARTESIAN_POINT('',(7.007470474323,-268.4295349402, + 325.6609495144)); +#8276 = CARTESIAN_POINT('',(6.938411851588,-268.3872170011, + 325.7437673772)); +#8277 = CARTESIAN_POINT('',(6.868000042073,-268.3465617275, + 325.82512930405)); +#8278 = CARTESIAN_POINT('',(6.796299912269,-268.3075080673, + 325.9050120106)); +#8279 = CARTESIAN_POINT('',(6.723370237055,-268.2699967719, + 325.98339503449)); +#8280 = CARTESIAN_POINT('',(6.425805498299,-268.1253377496, + 326.2920388195)); +#8281 = CARTESIAN_POINT('',(6.191528956899,-268.0301492903, + 326.51014217924)); +#8282 = CARTESIAN_POINT('',(5.947970508042,-267.946914168, + 326.71393506815)); +#8283 = CARTESIAN_POINT('',(5.696322993359,-267.8743063295, + 326.90290160477)); +#8284 = CARTESIAN_POINT('',(5.437496345987,-267.8111665202, + 327.07659063336)); +#8285 = CARTESIAN_POINT('',(5.172178272969,-267.7565042866, + 327.23456058468)); +#8286 = CARTESIAN_POINT('',(4.544328649113,-267.6477189918, + 327.56265415657)); +#8287 = CARTESIAN_POINT('',(4.177123764148,-267.5991103162, + 327.72116074364)); +#8288 = CARTESIAN_POINT('',(3.801678874294,-267.5620726421, + 327.85000440638)); +#8289 = CARTESIAN_POINT('',(3.420124549357,-267.5352301165, + 327.94778993265)); +#8290 = CARTESIAN_POINT('',(3.034394640912,-267.5176051434, + 328.01354835364)); +#8291 = CARTESIAN_POINT('',(2.64630129029,-267.5087173416, + 328.04671779301)); +#8292 = CARTESIAN_POINT('',(2.053914197683,-267.5085517016, + 328.04733596913)); +#8293 = CARTESIAN_POINT('',(1.850237493096,-267.5109061433, + 328.03854981844)); +#8294 = CARTESIAN_POINT('',(1.646880787199,-267.5156696472, + 328.02076813249)); +#8295 = CARTESIAN_POINT('',(1.444147556678,-267.5228776005, + 327.99403551636)); +#8296 = CARTESIAN_POINT('',(1.242347662824,-267.5326004117, + 327.95843815563)); +#8297 = CARTESIAN_POINT('',(1.041797519339,-267.544940807, + 327.91410397471)); +#8298 = CARTESIAN_POINT('',(0.662636170099,-267.5736961846, + 327.81329806976)); +#8299 = CARTESIAN_POINT('',(0.483752084914,-267.58961533,327.7583708823) + ); +#8300 = CARTESIAN_POINT('',(0.306337151595,-267.6078914208, + 327.69652909096)); +#8301 = CARTESIAN_POINT('',(0.130572953635,-267.628651122, + 327.62789343998)); +#8302 = CARTESIAN_POINT('',(-4.334635777872E-02,-267.6520428972, + 327.5525975118)); +#8303 = CARTESIAN_POINT('',(-0.215213932556,-267.6782349984, + 327.47078767945)); +#8304 = CARTESIAN_POINT('',(-0.752994755377,-267.7707584103, + 327.19122213499)); +#8305 = CARTESIAN_POINT('',(-1.109939802413,-267.8480862831, + 326.97014840412)); +#8306 = CARTESIAN_POINT('',(-1.454610199675,-267.9414277169, + 326.72057953963)); +#8307 = CARTESIAN_POINT('',(-1.785313134628,-268.0534198712, + 326.44347846715)); +#8308 = CARTESIAN_POINT('',(-2.099473887708,-268.1872853635, + 326.13984392982)); +#8309 = CARTESIAN_POINT('',(-2.393347318117,-268.3468631764, + 325.81106281928)); +#8310 = CARTESIAN_POINT('',(-2.746280478092,-268.5963139639, + 325.34826570329)); +#8311 = CARTESIAN_POINT('',(-2.828434982099,-268.6591747822, + 325.23490197717)); +#8312 = CARTESIAN_POINT('',(-2.907886549335,-268.7251812631, + 325.11928649267)); +#8313 = CARTESIAN_POINT('',(-2.984433970709,-268.7944637095, + 325.00151603803)); +#8314 = CARTESIAN_POINT('',(-3.057862029841,-268.8671531733, + 324.8816986678)); +#8315 = CARTESIAN_POINT('',(-3.127941261789,-268.9433809305, + 324.75995337309)); +#8316 = CARTESIAN_POINT('',(-3.33908518253,-269.1971134051, + 324.36761050327)); +#8317 = CARTESIAN_POINT('',(-3.468040447204,-269.389887153, + 324.08787259189)); +#8318 = CARTESIAN_POINT('',(-3.576567548116,-269.6015246093, + 323.80132993083)); +#8319 = CARTESIAN_POINT('',(-3.661022685041,-269.8314115797, + 323.51220047605)); +#8320 = CARTESIAN_POINT('',(-3.71874336275,-270.0782763319, + 323.22452116891)); +#8321 = CARTESIAN_POINT('',(-3.748,-270.3401544412,322.94195547195)); +#8322 = CARTESIAN_POINT('',(-3.748,-270.6144364153,322.66767349782)); +#8323 = ORIENTED_EDGE('',*,*,#8191,.F.); +#8324 = CYLINDRICAL_SURFACE('',#8325,6.); +#8325 = AXIS2_PLACEMENT_3D('',#8326,#8327,#8328); +#8326 = CARTESIAN_POINT('',(2.252,-155.1438990476,207.19713613006)); +#8327 = DIRECTION('',(0.,0.707106781187,-0.707106781187)); +#8328 = DIRECTION('',(-1.,0.,0.)); +#8329 = ADVANCED_FACE('',(#8330),#8333,.F.); +#8330 = FACE_BOUND('',#8331,.T.); +#8331 = EDGE_LOOP('',(#8332)); +#8332 = ORIENTED_EDGE('',*,*,#8182,.F.); +#8333 = PLANE('',#8334); +#8334 = AXIS2_PLACEMENT_3D('',#8335,#8336,#8337); +#8335 = CARTESIAN_POINT('',(2.252,-155.1438990476,207.19713613006)); +#8336 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8337 = DIRECTION('',(-1.,0.,0.)); +#8338 = ADVANCED_FACE('',(#8339,#8366),#8369,.T.); +#8339 = FACE_BOUND('',#8340,.T.); +#8340 = EDGE_LOOP('',(#8341,#8351,#8358,#8359)); +#8341 = ORIENTED_EDGE('',*,*,#8342,.T.); +#8342 = EDGE_CURVE('',#8343,#8345,#8347,.T.); +#8343 = VERTEX_POINT('',#8344); +#8344 = CARTESIAN_POINT('',(-5.248,-287.5849991638,339.6382362463)); +#8345 = VERTEX_POINT('',#8346); +#8346 = CARTESIAN_POINT('',(9.752,-287.5849991638,339.6382362463)); +#8347 = LINE('',#8348,#8349); +#8348 = CARTESIAN_POINT('',(-5.248,-287.5849991638,339.6382362463)); +#8349 = VECTOR('',#8350,1.); +#8350 = DIRECTION('',(1.,0.,0.)); +#8351 = ORIENTED_EDGE('',*,*,#8352,.F.); +#8352 = EDGE_CURVE('',#8345,#8345,#8353,.T.); +#8353 = CIRCLE('',#8354,12.); +#8354 = AXIS2_PLACEMENT_3D('',#8355,#8356,#8357); +#8355 = CARTESIAN_POINT('',(9.752,-279.0997177896,331.15295487206)); +#8356 = DIRECTION('',(1.,0.,0.)); +#8357 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8358 = ORIENTED_EDGE('',*,*,#8342,.F.); +#8359 = ORIENTED_EDGE('',*,*,#8360,.T.); +#8360 = EDGE_CURVE('',#8343,#8343,#8361,.T.); +#8361 = CIRCLE('',#8362,12.); +#8362 = AXIS2_PLACEMENT_3D('',#8363,#8364,#8365); +#8363 = CARTESIAN_POINT('',(-5.248,-279.0997177896,331.15295487206)); +#8364 = DIRECTION('',(1.,0.,0.)); +#8365 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8366 = FACE_BOUND('',#8367,.T.); +#8367 = EDGE_LOOP('',(#8368)); +#8368 = ORIENTED_EDGE('',*,*,#8199,.T.); +#8369 = CYLINDRICAL_SURFACE('',#8370,12.); +#8370 = AXIS2_PLACEMENT_3D('',#8371,#8372,#8373); +#8371 = CARTESIAN_POINT('',(-5.248,-279.0997177896,331.15295487206)); +#8372 = DIRECTION('',(1.,0.,0.)); +#8373 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8374 = ADVANCED_FACE('',(#8375,#8378),#8389,.F.); +#8375 = FACE_BOUND('',#8376,.F.); +#8376 = EDGE_LOOP('',(#8377)); +#8377 = ORIENTED_EDGE('',*,*,#8360,.T.); +#8378 = FACE_BOUND('',#8379,.F.); +#8379 = EDGE_LOOP('',(#8380)); +#8380 = ORIENTED_EDGE('',*,*,#8381,.F.); +#8381 = EDGE_CURVE('',#8382,#8382,#8384,.T.); +#8382 = VERTEX_POINT('',#8383); +#8383 = CARTESIAN_POINT('',(-5.248,-284.0494652579,336.10270234037)); +#8384 = CIRCLE('',#8385,7.); +#8385 = AXIS2_PLACEMENT_3D('',#8386,#8387,#8388); +#8386 = CARTESIAN_POINT('',(-5.248,-279.0997177896,331.15295487206)); +#8387 = DIRECTION('',(1.,0.,0.)); +#8388 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8389 = PLANE('',#8390); +#8390 = AXIS2_PLACEMENT_3D('',#8391,#8392,#8393); +#8391 = CARTESIAN_POINT('',(-5.248,-279.0997177896,331.15295487206)); +#8392 = DIRECTION('',(1.,0.,0.)); +#8393 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8394 = ADVANCED_FACE('',(#8395,#8398),#8409,.T.); +#8395 = FACE_BOUND('',#8396,.T.); +#8396 = EDGE_LOOP('',(#8397)); +#8397 = ORIENTED_EDGE('',*,*,#8352,.T.); +#8398 = FACE_BOUND('',#8399,.T.); +#8399 = EDGE_LOOP('',(#8400)); +#8400 = ORIENTED_EDGE('',*,*,#8401,.F.); +#8401 = EDGE_CURVE('',#8402,#8402,#8404,.T.); +#8402 = VERTEX_POINT('',#8403); +#8403 = CARTESIAN_POINT('',(9.752,-284.0494652579,336.10270234037)); +#8404 = CIRCLE('',#8405,7.); +#8405 = AXIS2_PLACEMENT_3D('',#8406,#8407,#8408); +#8406 = CARTESIAN_POINT('',(9.752,-279.0997177896,331.15295487206)); +#8407 = DIRECTION('',(1.,0.,0.)); +#8408 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8409 = PLANE('',#8410); +#8410 = AXIS2_PLACEMENT_3D('',#8411,#8412,#8413); +#8411 = CARTESIAN_POINT('',(9.752,-279.0997177896,331.15295487206)); +#8412 = DIRECTION('',(1.,0.,0.)); +#8413 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8414 = ADVANCED_FACE('',(#8415),#8426,.F.); +#8415 = FACE_BOUND('',#8416,.F.); +#8416 = EDGE_LOOP('',(#8417,#8418,#8424,#8425)); +#8417 = ORIENTED_EDGE('',*,*,#8401,.F.); +#8418 = ORIENTED_EDGE('',*,*,#8419,.F.); +#8419 = EDGE_CURVE('',#8382,#8402,#8420,.T.); +#8420 = LINE('',#8421,#8422); +#8421 = CARTESIAN_POINT('',(-5.248,-284.0494652579,336.10270234037)); +#8422 = VECTOR('',#8423,1.); +#8423 = DIRECTION('',(1.,0.,0.)); +#8424 = ORIENTED_EDGE('',*,*,#8381,.T.); +#8425 = ORIENTED_EDGE('',*,*,#8419,.T.); +#8426 = CYLINDRICAL_SURFACE('',#8427,7.); +#8427 = AXIS2_PLACEMENT_3D('',#8428,#8429,#8430); +#8428 = CARTESIAN_POINT('',(-5.248,-279.0997177896,331.15295487206)); +#8429 = DIRECTION('',(1.,0.,0.)); +#8430 = DIRECTION('',(0.,-0.707106781187,0.707106781187)); +#8431 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#8435)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#8432,#8433,#8434)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#8432 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#8433 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#8434 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#8435 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#8432, + 'distance_accuracy_value','confusion accuracy'); +#8436 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#8437,#8439); +#8437 = ( REPRESENTATION_RELATIONSHIP('','',#8175,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#8438) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#8438 = ITEM_DEFINED_TRANSFORMATION('','',#11,#47); +#8439 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #8440); +#8440 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('9','BoomCylinderInner001','',#5, + #8170,$); +#8441 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#8172)); +#8442 = SHAPE_DEFINITION_REPRESENTATION(#8443,#8449); +#8443 = PRODUCT_DEFINITION_SHAPE('','',#8444); +#8444 = PRODUCT_DEFINITION('design','',#8445,#8448); +#8445 = PRODUCT_DEFINITION_FORMATION('','',#8446); +#8446 = PRODUCT('StickCylinderInner','StickCylinderInner','',(#8447)); +#8447 = PRODUCT_CONTEXT('',#2,'mechanical'); +#8448 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#8449 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#8450),#8705); +#8450 = MANIFOLD_SOLID_BREP('',#8451); +#8451 = CLOSED_SHELL('',(#8452,#8603,#8612,#8648,#8668,#8688)); +#8452 = ADVANCED_FACE('',(#8453),#8598,.T.); +#8453 = FACE_BOUND('',#8454,.F.); +#8454 = EDGE_LOOP('',(#8455,#8464,#8472,#8597)); +#8455 = ORIENTED_EDGE('',*,*,#8456,.F.); +#8456 = EDGE_CURVE('',#8457,#8457,#8459,.T.); +#8457 = VERTEX_POINT('',#8458); +#8458 = CARTESIAN_POINT('',(-7.828,-510.602403336,83.221350256195)); +#8459 = CIRCLE('',#8460,6.); +#8460 = AXIS2_PLACEMENT_3D('',#8461,#8462,#8463); +#8461 = CARTESIAN_POINT('',(-1.828000000001,-510.602403336, + 83.221350256195)); +#8462 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8463 = DIRECTION('',(-1.,0.,0.)); +#8464 = ORIENTED_EDGE('',*,*,#8465,.T.); +#8465 = EDGE_CURVE('',#8457,#8466,#8468,.T.); +#8466 = VERTEX_POINT('',#8467); +#8467 = CARTESIAN_POINT('',(-7.828,-631.9579533364,192.49037827459)); +#8468 = LINE('',#8469,#8470); +#8469 = CARTESIAN_POINT('',(-7.828000000001,-510.602403336, + 83.221350256195)); +#8470 = VECTOR('',#8471,1.); +#8471 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8472 = ORIENTED_EDGE('',*,*,#8473,.T.); +#8473 = EDGE_CURVE('',#8466,#8466,#8474,.T.); +#8474 = B_SPLINE_CURVE_WITH_KNOTS('',7,(#8475,#8476,#8477,#8478,#8479, + #8480,#8481,#8482,#8483,#8484,#8485,#8486,#8487,#8488,#8489,#8490, + #8491,#8492,#8493,#8494,#8495,#8496,#8497,#8498,#8499,#8500,#8501, + #8502,#8503,#8504,#8505,#8506,#8507,#8508,#8509,#8510,#8511,#8512, + #8513,#8514,#8515,#8516,#8517,#8518,#8519,#8520,#8521,#8522,#8523, + #8524,#8525,#8526,#8527,#8528,#8529,#8530,#8531,#8532,#8533,#8534, + #8535,#8536,#8537,#8538,#8539,#8540,#8541,#8542,#8543,#8544,#8545, + #8546,#8547,#8548,#8549,#8550,#8551,#8552,#8553,#8554,#8555,#8556, + #8557,#8558,#8559,#8560,#8561,#8562,#8563,#8564,#8565,#8566,#8567, + #8568,#8569,#8570,#8571,#8572,#8573,#8574,#8575,#8576,#8577,#8578, + #8579,#8580,#8581,#8582,#8583,#8584,#8585,#8586,#8587,#8588,#8589, + #8590,#8591,#8592,#8593,#8594,#8595,#8596),.UNSPECIFIED.,.T.,.F.,(8, + 6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,8),(0.,5.792747495377E-02, + 8.552057468052E-02,0.106135747546,0.168298002248,0.250002115854, + 0.292820771135,0.331595242887,0.415772630736,0.442340942148, + 0.500146776733,0.558040256615,0.58561715796,0.606220225351, + 0.668345977641,0.750002113624,0.792795625282,0.831547339874, + 0.915675348571,0.942228074793,1.),.UNSPECIFIED.); +#8475 = CARTESIAN_POINT('',(-7.828,-631.9579533364,192.49037827459)); +#8476 = CARTESIAN_POINT('',(-7.828,-632.2178981393,192.20168032337)); +#8477 = CARTESIAN_POINT('',(-7.798654577067,-632.4867658589, + 191.92500052225)); +#8478 = CARTESIAN_POINT('',(-7.74077036193,-632.7615216795, + 191.66310640545)); +#8479 = CARTESIAN_POINT('',(-7.656088389467,-633.0386039028, + 191.41814666964)); +#8480 = CARTESIAN_POINT('',(-7.547281115515,-633.314041584, + 191.19159719734)); +#8481 = CARTESIAN_POINT('',(-7.418002086341,-633.5836488144, + 190.98428627835)); +#8482 = CARTESIAN_POINT('',(-7.203912559578,-633.9669754955, + 190.7070486902)); +#8483 = CARTESIAN_POINT('',(-7.13097260886,-634.088921812, + 190.62164752203)); +#8484 = CARTESIAN_POINT('',(-7.054440490066,-634.2089840441, + 190.54015536702)); +#8485 = CARTESIAN_POINT('',(-6.974571152664,-634.3270239059, + 190.46243134695)); +#8486 = CARTESIAN_POINT('',(-6.891602694693,-634.4429157703, + 190.38833444752)); +#8487 = CARTESIAN_POINT('',(-6.805756643329,-634.5565470568, + 190.31772410259)); +#8488 = CARTESIAN_POINT('',(-6.651104952178,-634.7509510869, + 190.20020745176)); +#8489 = CARTESIAN_POINT('',(-6.583470474324,-634.8327782643, + 190.15181516841)); +#8490 = CARTESIAN_POINT('',(-6.514411851589,-634.9132678782, + 190.1052208725)); +#8491 = CARTESIAN_POINT('',(-6.444000042074,-634.9923905688, + 190.06036316126)); +#8492 = CARTESIAN_POINT('',(-6.37229991227,-635.0701198882, + 190.01718228494)); +#8493 = CARTESIAN_POINT('',(-6.299370237056,-635.1464323014, + 189.97562014691)); +#8494 = CARTESIAN_POINT('',(-6.001805498299,-635.4470822325, + 189.81500620717)); +#8495 = CARTESIAN_POINT('',(-5.7675289569,-635.6599049102, + 189.70853355249)); +#8496 = CARTESIAN_POINT('',(-5.523970508043,-635.8590623183, + 189.61474680531)); +#8497 = CARTESIAN_POINT('',(-5.272322993361,-636.0439698822, + 189.5323487289)); +#8498 = CARTESIAN_POINT('',(-5.013496345988,-636.2141163937, + 189.46020526915)); +#8499 = CARTESIAN_POINT('',(-4.748178272969,-636.3690090524, + 189.39735043972)); +#8500 = CARTESIAN_POINT('',(-4.120328649114,-636.690959601, + 189.27154314061)); +#8501 = CARTESIAN_POINT('',(-3.753123764147,-636.8467049788, + 189.21470548775)); +#8502 = CARTESIAN_POINT('',(-3.377678874296,-636.9734336637, + 189.17097541618)); +#8503 = CARTESIAN_POINT('',(-2.996124549359,-637.0696803491, + 189.13905197828)); +#8504 = CARTESIAN_POINT('',(-2.610394640912,-637.1344262306, + 189.11800962975)); +#8505 = CARTESIAN_POINT('',(-2.222301290291,-637.1670850608, + 189.10739805404)); +#8506 = CARTESIAN_POINT('',(-1.629914197683,-637.1676937208, + 189.10720028825)); +#8507 = CARTESIAN_POINT('',(-1.426237493097,-637.1590428332, + 189.11001133486)); +#8508 = CARTESIAN_POINT('',(-1.2228807872,-637.141534819,189.11569893203 + )); +#8509 = CARTESIAN_POINT('',(-1.020147556679,-637.1152160741, + 189.12429608415)); +#8510 = CARTESIAN_POINT('',(-0.818347662825,-637.0801763509, + 189.13586859248)); +#8511 = CARTESIAN_POINT('',(-0.61779751934,-637.0365487748, + 189.15051234743)); +#8512 = CARTESIAN_POINT('',(-0.238636224357,-636.9373859752, + 189.18450408532)); +#8513 = CARTESIAN_POINT('',(-5.975208975457E-02,-636.8833671986, + 189.20327608355)); +#8514 = CARTESIAN_POINT('',(0.117662891561,-636.8225666375, + 189.22476368376)); +#8515 = CARTESIAN_POINT('',(0.293427087713,-636.7551115218, + 189.2490870498)); +#8516 = CARTESIAN_POINT('',(0.467346350891,-636.6811430333, + 189.27638744499)); +#8517 = CARTESIAN_POINT('',(0.639213881198,-636.6008161343, + 189.30682523582)); +#8518 = CARTESIAN_POINT('',(1.176994755376,-636.3264759998, + 189.41385318791)); +#8519 = CARTESIAN_POINT('',(1.533939802409,-636.1097522709, + 189.50264519068)); +#8520 = CARTESIAN_POINT('',(1.878610199681,-635.8654105451, + 189.60892012839)); +#8521 = CARTESIAN_POINT('',(2.20931313462,-635.5945504465, + 189.73526115096)); +#8522 = CARTESIAN_POINT('',(2.52347388771,-635.2983380083, + 189.88483418917)); +#8523 = CARTESIAN_POINT('',(2.817347318116,-634.9783591383, + 190.06140037998)); +#8524 = CARTESIAN_POINT('',(3.17028047809,-634.5292515151, + 190.33473023345)); +#8525 = CARTESIAN_POINT('',(3.252434982098,-634.4193330311, + 190.40343790221)); +#8526 = CARTESIAN_POINT('',(3.331886549335,-634.3073305059, + 190.47540477043)); +#8527 = CARTESIAN_POINT('',(3.408433970709,-634.1933474146, + 190.55075589706)); +#8528 = CARTESIAN_POINT('',(3.481862029839,-634.0774985226, + 190.6296164991)); +#8529 = CARTESIAN_POINT('',(3.551941261787,-633.9599095281, + 190.7121114452)); +#8530 = CARTESIAN_POINT('',(3.763085182526,-633.5813836822, + 190.98602982753)); +#8531 = CARTESIAN_POINT('',(3.892040447204,-633.3121181404, + 191.19317973678)); +#8532 = CARTESIAN_POINT('',(4.000567548116,-633.0370444247, + 191.41952363555)); +#8533 = CARTESIAN_POINT('',(4.08502268504,-632.7603425662, + 191.66422742029)); +#8534 = CARTESIAN_POINT('',(4.142743362748,-632.4859774165, + 191.92580982454)); +#8535 = CARTESIAN_POINT('',(4.172,-632.2175046072,192.20211738498)); +#8536 = CARTESIAN_POINT('',(4.172,-631.6980085336,192.77907622582)); +#8537 = CARTESIAN_POINT('',(4.142654577066,-631.4509487429, + 193.0753919744)); +#8538 = CARTESIAN_POINT('',(4.084770361928,-631.2192091132, + 193.37601804339)); +#8539 = CARTESIAN_POINT('',(4.000088389465,-631.0045542714, + 193.67718764604)); +#8540 = CARTESIAN_POINT('',(3.891281115514,-630.8080369384, + 193.97479731901)); +#8541 = CARTESIAN_POINT('',(3.762002086341,-630.6300433198, + 194.26459750451)); +#8542 = CARTESIAN_POINT('',(3.547912572633,-630.3943930314,194.674803476 + )); +#8543 = CARTESIAN_POINT('',(3.474972612348,-630.3222065477, + 194.80500863231)); +#8544 = CARTESIAN_POINT('',(3.398440480404,-630.2537107248, + 194.9329314216)); +#8545 = CARTESIAN_POINT('',(3.318571140467,-630.1887510118, + 195.05844902079)); +#8546 = CARTESIAN_POINT('',(3.23560269454,-630.1271740316, + 195.18145123318)); +#8547 = CARTESIAN_POINT('',(3.149756658136,-630.0688282088, + 195.30184080882)); +#8548 = CARTESIAN_POINT('',(2.995104952177,-629.9722760718, + 195.50746372789)); +#8549 = CARTESIAN_POINT('',(2.927470474322,-629.9327021555, + 195.59390101841)); +#8550 = CARTESIAN_POINT('',(2.858411851587,-629.8947765637, + 195.67882013203)); +#8551 = CARTESIAN_POINT('',(2.788000042072,-629.8584351609, + 195.7621982878)); +#8552 = CARTESIAN_POINT('',(2.716299912268,-629.8236157603, + 195.84401542853)); +#8553 = CARTESIAN_POINT('',(2.643370237055,-629.7902581233, + 195.92425422072)); +#8554 = CARTESIAN_POINT('',(2.345805498299,-629.6619505188, + 196.24004588841)); +#8555 = CARTESIAN_POINT('',(2.111528956899,-629.5783071599, + 196.46283212412)); +#8556 = CARTESIAN_POINT('',(1.867970508041,-629.5058518041, + 196.67070191168)); +#8557 = CARTESIAN_POINT('',(1.616322993359,-629.4432332165, + 196.86320947689)); +#8558 = CARTESIAN_POINT('',(1.357496345987,-629.3892701196, + 197.03996495299)); +#8559 = CARTESIAN_POINT('',(1.092178272969,-629.342950307, + 197.20057921226)); +#8560 = CARTESIAN_POINT('',(0.464328649113,-629.2514851896, + 197.53391652574)); +#8561 = CARTESIAN_POINT('',(9.712376414734E-02,-629.2112387243, + 197.69474986656)); +#8562 = CARTESIAN_POINT('',(-0.278321125706,-629.1809949653, + 197.82535535562)); +#8563 = CARTESIAN_POINT('',(-0.659875450643,-629.1593069255, + 197.92441169948)); +#8564 = CARTESIAN_POINT('',(-1.045605359088,-629.1451476367, + 197.99100242065)); +#8565 = CARTESIAN_POINT('',(-1.43369870971,-629.1380079696, + 198.02459155407)); +#8566 = CARTESIAN_POINT('',(-2.026085802318,-629.1378749095, + 198.02521755192)); +#8567 = CARTESIAN_POINT('',(-2.229762506904,-629.1397662929, + 198.01632022039)); +#8568 = CARTESIAN_POINT('',(-2.433119212801,-629.143592647, + 197.9983136011)); +#8569 = CARTESIAN_POINT('',(-2.635852443322,-629.1493916451, + 197.97124038595)); +#8570 = CARTESIAN_POINT('',(-2.837652337177,-629.1572381096, + 197.93518295755)); +#8571 = CARTESIAN_POINT('',(-3.038202480661,-629.167241321, + 197.89026368869)); +#8572 = CARTESIAN_POINT('',(-3.417363829901,-629.1906815169, + 197.78809099455)); +#8573 = CARTESIAN_POINT('',(-3.596247915087,-629.2037041788, + 197.7324059392)); +#8574 = CARTESIAN_POINT('',(-3.773662848405,-629.2187186735, + 197.6696924032)); +#8575 = CARTESIAN_POINT('',(-3.949427046366,-629.2358578119, + 197.60006433617)); +#8576 = CARTESIAN_POINT('',(-4.123346357779,-629.2552768451, + 197.52364736753)); +#8577 = CARTESIAN_POINT('',(-4.295213932556,-629.2771514552, + 197.44057886404)); +#8578 = CARTESIAN_POINT('',(-4.832994755378,-629.3549167368, + 197.15655415321)); +#8579 = CARTESIAN_POINT('',(-5.189939802413,-629.4205685294, + 196.93173636803)); +#8580 = CARTESIAN_POINT('',(-5.534610199675,-629.5007206167, + 196.67762441581)); +#8581 = CARTESIAN_POINT('',(-5.865313134628,-629.5980569401, + 196.39504188423)); +#8582 = CARTESIAN_POINT('',(-6.179473887709,-629.7158479706, + 196.08481748893)); +#8583 = CARTESIAN_POINT('',(-6.473347318117,-629.8580000138, + 195.74813530404)); +#8584 = CARTESIAN_POINT('',(-6.826280478092,-630.082888008, + 195.27291718992)); +#8585 = CARTESIAN_POINT('',(-6.908434982099,-630.1397296788, + 195.15641894381)); +#8586 = CARTESIAN_POINT('',(-6.987886549335,-630.1995948531, + 195.03750739401)); +#8587 = CARTESIAN_POINT('',(-7.064433970709,-630.262618721, + 194.9162723766)); +#8588 = CARTESIAN_POINT('',(-7.137862029841,-630.3289378098, + 194.79281493933)); +#8589 = CARTESIAN_POINT('',(-7.20794126179,-630.398689443, + 194.66724703974)); +#8590 = CARTESIAN_POINT('',(-7.41908518253,-630.6315405468, + 194.2621625305)); +#8591 = CARTESIAN_POINT('',(-7.548040447204,-630.8094097539, + 193.97271899178)); +#8592 = CARTESIAN_POINT('',(-7.656567548116,-631.0057606843, + 193.67549277881)); +#8593 = CARTESIAN_POINT('',(-7.741022685041,-631.2202007361, + 193.37472821148)); +#8594 = CARTESIAN_POINT('',(-7.79874336275,-631.4516711971, + 193.07452325597)); +#8595 = CARTESIAN_POINT('',(-7.828,-631.6984020656,192.7786391642)); +#8596 = CARTESIAN_POINT('',(-7.828,-631.9579533364,192.49037827459)); +#8597 = ORIENTED_EDGE('',*,*,#8465,.F.); +#8598 = CYLINDRICAL_SURFACE('',#8599,6.); +#8599 = AXIS2_PLACEMENT_3D('',#8600,#8601,#8602); +#8600 = CARTESIAN_POINT('',(-1.828000000001,-510.602403336, + 83.221350256195)); +#8601 = DIRECTION('',(0.,0.743144825477,-0.669130606359)); +#8602 = DIRECTION('',(-1.,0.,0.)); +#8603 = ADVANCED_FACE('',(#8604),#8607,.F.); +#8604 = FACE_BOUND('',#8605,.T.); +#8605 = EDGE_LOOP('',(#8606)); +#8606 = ORIENTED_EDGE('',*,*,#8456,.F.); +#8607 = PLANE('',#8608); +#8608 = AXIS2_PLACEMENT_3D('',#8609,#8610,#8611); +#8609 = CARTESIAN_POINT('',(-1.828000000001,-510.602403336, + 83.221350256195)); +#8610 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8611 = DIRECTION('',(-1.,0.,0.)); +#8612 = ADVANCED_FACE('',(#8613,#8640),#8643,.T.); +#8613 = FACE_BOUND('',#8614,.T.); +#8614 = EDGE_LOOP('',(#8615,#8625,#8632,#8633)); +#8615 = ORIENTED_EDGE('',*,*,#8616,.T.); +#8616 = EDGE_CURVE('',#8617,#8619,#8621,.T.); +#8617 = VERTEX_POINT('',#8618); +#8618 = CARTESIAN_POINT('',(-9.328,-649.7934291479,208.5495128272)); +#8619 = VERTEX_POINT('',#8620); +#8620 = CARTESIAN_POINT('',(5.672,-649.7934291479,208.5495128272)); +#8621 = LINE('',#8622,#8623); +#8622 = CARTESIAN_POINT('',(-9.328,-649.7934291479,208.5495128272)); +#8623 = VECTOR('',#8624,1.); +#8624 = DIRECTION('',(1.,0.,0.)); +#8625 = ORIENTED_EDGE('',*,*,#8626,.F.); +#8626 = EDGE_CURVE('',#8619,#8619,#8627,.T.); +#8627 = CIRCLE('',#8628,12.); +#8628 = AXIS2_PLACEMENT_3D('',#8629,#8630,#8631); +#8629 = CARTESIAN_POINT('',(5.672,-640.8756912422,200.5199455509)); +#8630 = DIRECTION('',(1.,0.,0.)); +#8631 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8632 = ORIENTED_EDGE('',*,*,#8616,.F.); +#8633 = ORIENTED_EDGE('',*,*,#8634,.T.); +#8634 = EDGE_CURVE('',#8617,#8617,#8635,.T.); +#8635 = CIRCLE('',#8636,12.); +#8636 = AXIS2_PLACEMENT_3D('',#8637,#8638,#8639); +#8637 = CARTESIAN_POINT('',(-9.328,-640.8756912422,200.5199455509)); +#8638 = DIRECTION('',(1.,0.,0.)); +#8639 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8640 = FACE_BOUND('',#8641,.T.); +#8641 = EDGE_LOOP('',(#8642)); +#8642 = ORIENTED_EDGE('',*,*,#8473,.T.); +#8643 = CYLINDRICAL_SURFACE('',#8644,12.); +#8644 = AXIS2_PLACEMENT_3D('',#8645,#8646,#8647); +#8645 = CARTESIAN_POINT('',(-9.328,-640.8756912422,200.5199455509)); +#8646 = DIRECTION('',(1.,0.,0.)); +#8647 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8648 = ADVANCED_FACE('',(#8649,#8652),#8663,.F.); +#8649 = FACE_BOUND('',#8650,.F.); +#8650 = EDGE_LOOP('',(#8651)); +#8651 = ORIENTED_EDGE('',*,*,#8634,.T.); +#8652 = FACE_BOUND('',#8653,.F.); +#8653 = EDGE_LOOP('',(#8654)); +#8654 = ORIENTED_EDGE('',*,*,#8655,.F.); +#8655 = EDGE_CURVE('',#8656,#8656,#8658,.T.); +#8656 = VERTEX_POINT('',#8657); +#8657 = CARTESIAN_POINT('',(-9.328,-646.0777050205,205.20385979541)); +#8658 = CIRCLE('',#8659,7.); +#8659 = AXIS2_PLACEMENT_3D('',#8660,#8661,#8662); +#8660 = CARTESIAN_POINT('',(-9.328,-640.8756912422,200.5199455509)); +#8661 = DIRECTION('',(1.,0.,0.)); +#8662 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8663 = PLANE('',#8664); +#8664 = AXIS2_PLACEMENT_3D('',#8665,#8666,#8667); +#8665 = CARTESIAN_POINT('',(-9.328,-640.8756912422,200.5199455509)); +#8666 = DIRECTION('',(1.,0.,0.)); +#8667 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8668 = ADVANCED_FACE('',(#8669,#8672),#8683,.T.); +#8669 = FACE_BOUND('',#8670,.T.); +#8670 = EDGE_LOOP('',(#8671)); +#8671 = ORIENTED_EDGE('',*,*,#8626,.T.); +#8672 = FACE_BOUND('',#8673,.T.); +#8673 = EDGE_LOOP('',(#8674)); +#8674 = ORIENTED_EDGE('',*,*,#8675,.F.); +#8675 = EDGE_CURVE('',#8676,#8676,#8678,.T.); +#8676 = VERTEX_POINT('',#8677); +#8677 = CARTESIAN_POINT('',(5.672,-646.0777050205,205.20385979541)); +#8678 = CIRCLE('',#8679,7.); +#8679 = AXIS2_PLACEMENT_3D('',#8680,#8681,#8682); +#8680 = CARTESIAN_POINT('',(5.672,-640.8756912422,200.5199455509)); +#8681 = DIRECTION('',(1.,0.,0.)); +#8682 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8683 = PLANE('',#8684); +#8684 = AXIS2_PLACEMENT_3D('',#8685,#8686,#8687); +#8685 = CARTESIAN_POINT('',(5.672,-640.8756912422,200.5199455509)); +#8686 = DIRECTION('',(1.,0.,0.)); +#8687 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8688 = ADVANCED_FACE('',(#8689),#8700,.F.); +#8689 = FACE_BOUND('',#8690,.F.); +#8690 = EDGE_LOOP('',(#8691,#8692,#8698,#8699)); +#8691 = ORIENTED_EDGE('',*,*,#8675,.F.); +#8692 = ORIENTED_EDGE('',*,*,#8693,.F.); +#8693 = EDGE_CURVE('',#8656,#8676,#8694,.T.); +#8694 = LINE('',#8695,#8696); +#8695 = CARTESIAN_POINT('',(-9.328,-646.0777050205,205.20385979541)); +#8696 = VECTOR('',#8697,1.); +#8697 = DIRECTION('',(1.,0.,0.)); +#8698 = ORIENTED_EDGE('',*,*,#8655,.T.); +#8699 = ORIENTED_EDGE('',*,*,#8693,.T.); +#8700 = CYLINDRICAL_SURFACE('',#8701,7.); +#8701 = AXIS2_PLACEMENT_3D('',#8702,#8703,#8704); +#8702 = CARTESIAN_POINT('',(-9.328,-640.8756912422,200.5199455509)); +#8703 = DIRECTION('',(1.,0.,0.)); +#8704 = DIRECTION('',(0.,-0.743144825477,0.669130606359)); +#8705 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#8709)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#8706,#8707,#8708)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#8706 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#8707 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#8708 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#8709 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#8706, + 'distance_accuracy_value','confusion accuracy'); +#8710 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#8711,#8713); +#8711 = ( REPRESENTATION_RELATIONSHIP('','',#8449,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#8712) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#8712 = ITEM_DEFINED_TRANSFORMATION('','',#11,#51); +#8713 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #8714); +#8714 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('10','StickCylinderInner001','', + #5,#8444,$); +#8715 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#8446)); +#8716 = SHAPE_DEFINITION_REPRESENTATION(#8717,#8723); +#8717 = PRODUCT_DEFINITION_SHAPE('','',#8718); +#8718 = PRODUCT_DEFINITION('design','',#8719,#8722); +#8719 = PRODUCT_DEFINITION_FORMATION('','',#8720); +#8720 = PRODUCT('StickCylinderOuter','StickCylinderOuter','',(#8721)); +#8721 = PRODUCT_CONTEXT('',#2,'mechanical'); +#8722 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#8723 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#8724),#9129); +#8724 = MANIFOLD_SOLID_BREP('',#8725); +#8725 = CLOSED_SHELL('',(#8726,#8882,#8918,#8938,#8958,#8978,#9103,#9120 + )); +#8726 = ADVANCED_FACE('',(#8727),#8877,.T.); +#8727 = FACE_BOUND('',#8728,.T.); +#8728 = EDGE_LOOP('',(#8729,#8739,#8746,#8747)); +#8729 = ORIENTED_EDGE('',*,*,#8730,.T.); +#8730 = EDGE_CURVE('',#8731,#8733,#8735,.T.); +#8731 = VERTEX_POINT('',#8732); +#8732 = CARTESIAN_POINT('',(-12.34,-517.7436636433,45.874169676785)); +#8733 = VERTEX_POINT('',#8734); +#8734 = CARTESIAN_POINT('',(-12.34,-633.6742564177,150.25854426876)); +#8735 = LINE('',#8736,#8737); +#8736 = CARTESIAN_POINT('',(-12.34,-513.2847946904,41.859386038632)); +#8737 = VECTOR('',#8738,1.); +#8738 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8739 = ORIENTED_EDGE('',*,*,#8740,.F.); +#8740 = EDGE_CURVE('',#8733,#8733,#8741,.T.); +#8741 = CIRCLE('',#8742,10.); +#8742 = AXIS2_PLACEMENT_3D('',#8743,#8744,#8745); +#8743 = CARTESIAN_POINT('',(-2.34,-633.6742564177,150.25854426876)); +#8744 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8745 = DIRECTION('',(-1.,7.431448254774E-16,-6.691306063589E-16)); +#8746 = ORIENTED_EDGE('',*,*,#8730,.F.); +#8747 = ORIENTED_EDGE('',*,*,#8748,.T.); +#8748 = EDGE_CURVE('',#8731,#8731,#8749,.T.); +#8749 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#8750,#8751,#8752,#8753,#8754, + #8755,#8756,#8757,#8758,#8759,#8760,#8761,#8762,#8763,#8764,#8765, + #8766,#8767,#8768,#8769,#8770,#8771,#8772,#8773,#8774,#8775,#8776, + #8777,#8778,#8779,#8780,#8781,#8782,#8783,#8784,#8785,#8786,#8787, + #8788,#8789,#8790,#8791,#8792,#8793,#8794,#8795,#8796,#8797,#8798, + #8799,#8800,#8801,#8802,#8803,#8804,#8805,#8806,#8807,#8808,#8809, + #8810,#8811,#8812,#8813,#8814,#8815,#8816,#8817,#8818,#8819,#8820, + #8821,#8822,#8823,#8824,#8825,#8826,#8827,#8828,#8829,#8830,#8831, + #8832,#8833,#8834,#8835,#8836,#8837,#8838,#8839,#8840,#8841,#8842, + #8843,#8844,#8845,#8846,#8847,#8848,#8849,#8850,#8851,#8852,#8853, + #8854,#8855,#8856,#8857,#8858,#8859,#8860,#8861,#8862,#8863,#8864, + #8865,#8866,#8867,#8868,#8869,#8870,#8871,#8872,#8873,#8874,#8875, + #8876),.UNSPECIFIED.,.T.,.F.,(7,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5, + 5,5,5,5,5,5,7),(0.,3.210916218005E-02,5.243126183046E-02, + 7.723174846044E-02,9.587022561839E-02,0.145139357103,0.199517786171, + 0.216908427465,0.250272900865,0.300505292142,0.355434892156, + 0.424678676781,0.468226147483,0.522375272218,0.553020238165, + 0.595697609962,0.645543741553,0.699817295534,0.750438244475, + 0.784108881455,0.801413668305,0.855182392021,0.922710310687, + 0.947795194011,0.967518182086,1.),.UNSPECIFIED.); +#8750 = CARTESIAN_POINT('',(-12.34,-517.7436636433,45.874169676785)); +#8751 = CARTESIAN_POINT('',(-12.34,-518.0295072881,45.556708147574)); +#8752 = CARTESIAN_POINT('',(-12.31810132552,-518.3039353108, + 45.23010689983)); +#8753 = CARTESIAN_POINT('',(-12.27446711221,-518.5648811563, + 44.896823706585)); +#8754 = CARTESIAN_POINT('',(-12.20999527927,-518.8107559011, + 44.559716622044)); +#8755 = CARTESIAN_POINT('',(-12.12631480777,-519.0404794974, + 44.22199712685)); +#8756 = CARTESIAN_POINT('',(-11.96223513621,-519.3882451027, + 43.675202606956)); +#8757 = CARTESIAN_POINT('',(-11.89183263585,-519.5164873204, + 43.464162294521)); +#8758 = CARTESIAN_POINT('',(-11.81506118034,-519.6383004279, + 43.254425874261)); +#8759 = CARTESIAN_POINT('',(-11.73236563794,-519.7538285806, + 43.046395668124)); +#8760 = CARTESIAN_POINT('',(-11.64420078635,-519.8632365373, + 42.840464680547)); +#8761 = CARTESIAN_POINT('',(-11.43733474335,-520.0929793272, + 42.388729483405)); +#8762 = CARTESIAN_POINT('',(-11.31615799354,-520.2104400357, + 42.144077944616)); +#8763 = CARTESIAN_POINT('',(-11.18818277912,-520.3195916886, + 41.903404420613)); +#8764 = CARTESIAN_POINT('',(-11.05402259789,-520.4209242075, + 41.667005713037)); +#8765 = CARTESIAN_POINT('',(-10.91422558509,-520.5149136715, + 41.435130175241)); +#8766 = CARTESIAN_POINT('',(-10.66033975899,-520.6674855204, + 41.037263403006)); +#8767 = CARTESIAN_POINT('',(-10.54847541509,-520.7290738916, + 40.869188681041)); +#8768 = CARTESIAN_POINT('',(-10.43386718679,-520.7869899316, + 40.703810584293)); +#8769 = CARTESIAN_POINT('',(-10.31667724629,-520.8414283042, + 40.541176646579)); +#8770 = CARTESIAN_POINT('',(-10.19704497689,-520.8925746111, + 40.381324392257)); +#8771 = CARTESIAN_POINT('',(-9.752702001383,-521.0675704234, + 39.809152197164)); +#8772 = CARTESIAN_POINT('',(-9.413928067907,-521.1728196493, + 39.413476457185)); +#8773 = CARTESIAN_POINT('',(-9.061078672429,-521.2595109262, + 39.038049498945)); +#8774 = CARTESIAN_POINT('',(-8.695631064369,-521.3303840432, + 38.683424685884)); +#8775 = CARTESIAN_POINT('',(-8.318406478362,-521.3878348363, + 38.350178557605)); +#8776 = CARTESIAN_POINT('',(-7.500789923729,-521.4849300738, + 37.69592197071)); +#8777 = CARTESIAN_POINT('',(-7.057682967465,-521.5221369047, + 37.379587128506)); +#8778 = CARTESIAN_POINT('',(-6.601862900004,-521.5483256817, + 37.09218457451)); +#8779 = CARTESIAN_POINT('',(-6.1344161852,-521.566030735,36.835846882824 + )); +#8780 = CARTESIAN_POINT('',(-5.656114452639,-521.5775956989, + 36.612846923914)); +#8781 = CARTESIAN_POINT('',(-5.011185424879,-521.5872958005, + 36.365764627801)); +#8782 = CARTESIAN_POINT('',(-4.853919293919,-521.5892141109, + 36.309579566606)); +#8783 = CARTESIAN_POINT('',(-4.695732422422,-521.5907631607, + 36.257175027836)); +#8784 = CARTESIAN_POINT('',(-4.536690939198,-521.5920021252, + 36.208637180852)); +#8785 = CARTESIAN_POINT('',(-4.376856977645,-521.5929850683, + 36.164044622726)); +#8786 = CARTESIAN_POINT('',(-3.908233561339,-521.5952454293, + 36.045622713198)); +#8787 = CARTESIAN_POINT('',(-3.597377683531,-521.5959645166, + 35.982536424254)); +#8788 = CARTESIAN_POINT('',(-3.284462073859,-521.5962503328, + 35.934815900328)); +#8789 = CARTESIAN_POINT('',(-2.970166035491,-521.5963561258, + 35.902858295482)); +#8790 = CARTESIAN_POINT('',(-2.655133542041,-521.5964090592, + 35.886869934848)); +#8791 = CARTESIAN_POINT('',(-1.865545894318,-521.5964090592, + 35.886869934848)); +#8792 = CARTESIAN_POINT('',(-1.391787807966,-521.5962890439, + 35.923110851635)); +#8793 = CARTESIAN_POINT('',(-0.920534563803,-521.5960498916, + 35.995395743826)); +#8794 = CARTESIAN_POINT('',(-0.453674742491,-521.5950374511, + 36.102741396558)); +#8795 = CARTESIAN_POINT('',(6.66760793402E-03,-521.5919693619, + 36.243341736356)); +#8796 = CARTESIAN_POINT('',(0.951510978708,-521.578178739, + 36.601464030441)); +#8797 = CARTESIAN_POINT('',(1.434540415784,-521.566762943, + 36.825060686316)); +#8798 = CARTESIAN_POINT('',(1.90655788807,-521.5491705386, + 37.082675234031)); +#8799 = CARTESIAN_POINT('',(2.366766569644,-521.5229981583, + 37.37196749841)); +#8800 = CARTESIAN_POINT('',(2.814044444957,-521.4856451471, + 37.690737639421)); +#8801 = CARTESIAN_POINT('',(3.79258189884,-521.3696336378, + 38.473282262094)); +#8802 = CARTESIAN_POINT('',(4.315208360679,-521.2827653824, + 38.953082321471)); +#8803 = CARTESIAN_POINT('',(4.814445556095,-521.1678521369, + 39.474167235959)); +#8804 = CARTESIAN_POINT('',(5.287734749005,-521.0179567979, + 40.035653723828)); +#8805 = CARTESIAN_POINT('',(5.729478305261,-520.8244889978, + 40.636145543863)); +#8806 = CARTESIAN_POINT('',(6.381858631487,-520.4217227945, + 41.672205311968)); +#8807 = CARTESIAN_POINT('',(6.618195869506,-520.2444659293, + 42.087506118809)); +#8808 = CARTESIAN_POINT('',(6.835665378702,-520.0431186798, + 42.516243449461)); +#8809 = CARTESIAN_POINT('',(7.030680745927,-519.8153254126, + 42.956193762832)); +#8810 = CARTESIAN_POINT('',(7.199326204847,-519.5588029632, + 43.404718030898)); +#8811 = CARTESIAN_POINT('',(7.508766295118,-518.9141872136, + 44.42328859523)); +#8812 = CARTESIAN_POINT('',(7.633555023791,-518.5076643013, + 44.998869183791)); +#8813 = CARTESIAN_POINT('',(7.698427597479,-518.0554971377, + 45.566426106086)); +#8814 = CARTESIAN_POINT('',(7.69786856397,-517.5657120029, + 46.109594514624)); +#8815 = CARTESIAN_POINT('',(7.633203640874,-517.0513118897, + 46.616022088323)); +#8816 = CARTESIAN_POINT('',(7.445273206328,-516.2332089358, + 47.338921200036)); +#8817 = CARTESIAN_POINT('',(7.359735495392,-515.9354401879, + 47.585418896096)); +#8818 = CARTESIAN_POINT('',(7.257954869789,-515.6372059902, + 47.817003015861)); +#8819 = CARTESIAN_POINT('',(7.141440217236,-515.3401890573, + 48.033707176953)); +#8820 = CARTESIAN_POINT('',(7.011854098477,-515.0460922211, + 48.235729275872)); +#8821 = CARTESIAN_POINT('',(6.674921074524,-514.3534730776, + 48.684781164395)); +#8822 = CARTESIAN_POINT('',(6.456663583794,-513.9585548941,48.9188134116 + )); +#8823 = CARTESIAN_POINT('',(6.219734839346,-513.5737469236, + 49.128022035775)); +#8824 = CARTESIAN_POINT('',(5.966893933684,-513.200410955, + 49.314844751817)); +#8825 = CARTESIAN_POINT('',(5.700278636131,-512.8394940189, + 49.481549466885)); +#8826 = CARTESIAN_POINT('',(5.095802402372,-512.0854590664, + 49.803827917357)); +#8827 = CARTESIAN_POINT('',(4.753180130849,-511.696720336, + 49.952997115964)); +#8828 = CARTESIAN_POINT('',(4.396038222047,-511.3266493904,50.0809115041 + )); +#8829 = CARTESIAN_POINT('',(4.025930773556,-510.9761164559, + 50.190365084509)); +#8830 = CARTESIAN_POINT('',(3.643717561131,-510.6459700677, + 50.283777452506)); +#8831 = CARTESIAN_POINT('',(2.8207903095,-510.0012863667,50.449800645462 + )); +#8832 = CARTESIAN_POINT('',(2.37768371795,-509.6905738329, + 50.519869609583)); +#8833 = CARTESIAN_POINT('',(1.921863894681,-509.4074832449, + 50.575956673929)); +#8834 = CARTESIAN_POINT('',(1.454417200623,-509.1544003849, + 50.620359353148)); +#8835 = CARTESIAN_POINT('',(0.976115166355,-508.9338306823, + 50.655170847373)); +#8836 = CARTESIAN_POINT('',(3.169258350633E-02,-508.5754746628, + 50.707118481995)); +#8837 = CARTESIAN_POINT('',(-0.433377629178,-508.4330040666, + 50.725302773974)); +#8838 = CARTESIAN_POINT('',(-0.905162966983,-508.3240489427, + 50.737809458022)); +#8839 = CARTESIAN_POINT('',(-1.381475013467,-508.2506167305, + 50.745773203668)); +#8840 = CARTESIAN_POINT('',(-1.860372714724,-508.213796932, + 50.749766444396)); +#8841 = CARTESIAN_POINT('',(-2.659025157484,-508.213796932, + 50.749766444396)); +#8842 = CARTESIAN_POINT('',(-2.977943089053,-508.2300871803, + 50.747999722563)); +#8843 = CARTESIAN_POINT('',(-3.296096912624,-508.2626472649, + 50.744468504249)); +#8844 = CARTESIAN_POINT('',(-3.612820595752,-508.3112480332, + 50.739062573572)); +#8845 = CARTESIAN_POINT('',(-3.927410661393,-508.3754478633, + 50.731560941836)); +#8846 = CARTESIAN_POINT('',(-4.399287989961,-508.4952472807, + 50.716605908293)); +#8847 = CARTESIAN_POINT('',(-4.558738941932,-508.5398668526, + 50.710900446072)); +#8848 = CARTESIAN_POINT('',(-4.717393844667,-508.5883612437, + 50.704526331128)); +#8849 = CARTESIAN_POINT('',(-4.875191428456,-508.6406467304, + 50.69743791507)); +#8850 = CARTESIAN_POINT('',(-5.032066463091,-508.6966317117, + 50.689585327295)); +#8851 = CARTESIAN_POINT('',(-5.672303867149,-508.9413558558, + 50.653978523153)); +#8852 = CARTESIAN_POINT('',(-6.146447355973,-509.1610062774, + 50.619190242109)); +#8853 = CARTESIAN_POINT('',(-6.609887399942,-509.4125927219, + 50.574924487993)); +#8854 = CARTESIAN_POINT('',(-7.061871911175,-509.693672178, + 50.519136429224)); +#8855 = CARTESIAN_POINT('',(-7.501344394524,-510.0019202653, + 50.449582858044)); +#8856 = CARTESIAN_POINT('',(-8.461381357501,-510.7535668393, + 50.25613651281)); +#8857 = CARTESIAN_POINT('',(-8.97378493442,-511.2112608683, + 50.122920405147)); +#8858 = CARTESIAN_POINT('',(-9.463753295196,-511.7056009444, + 49.958908778713)); +#8859 = CARTESIAN_POINT('',(-9.928907567106,-512.2350331057, + 49.7577324934)); +#8860 = CARTESIAN_POINT('',(-10.36410266079,-512.7974215284, + 49.511573497999)); +#8861 = CARTESIAN_POINT('',(-10.90771848519,-513.6083029663, + 49.099758501805)); +#8862 = CARTESIAN_POINT('',(-11.04968164805,-513.8320348377, + 48.980689250474)); +#8863 = CARTESIAN_POINT('',(-11.18586383929,-514.0596100784, + 48.85363797414)); +#8864 = CARTESIAN_POINT('',(-11.31569502956,-514.2907205103, + 48.718138140773)); +#8865 = CARTESIAN_POINT('',(-11.4385360147,-514.525005516, + 48.573713928189)); +#8866 = CARTESIAN_POINT('',(-11.6442077101,-514.9484310247, + 48.298930693614)); +#8867 = CARTESIAN_POINT('',(-11.72999238586,-515.1365464837, + 48.172144624164)); +#8868 = CARTESIAN_POINT('',(-11.81060361342,-515.3260347524, + 48.039391431397)); +#8869 = CARTESIAN_POINT('',(-11.88562342971,-515.5165225299, + 47.900556201738)); +#8870 = CARTESIAN_POINT('',(-11.95464216053,-515.7076297327, + 47.755543050448)); +#8871 = CARTESIAN_POINT('',(-12.12037650005,-516.2140930496, + 47.355157361174)); +#8872 = CARTESIAN_POINT('',(-12.20633661341,-516.5304939464, + 47.088570378632)); +#8873 = CARTESIAN_POINT('',(-12.27260804685,-516.8446441075, + 46.805488286138)); +#8874 = CARTESIAN_POINT('',(-12.31747783296,-517.1533344089, + 46.507357463555)); +#8875 = CARTESIAN_POINT('',(-12.34,-517.4537793423,46.196118809328)); +#8876 = CARTESIAN_POINT('',(-12.34,-517.7436636433,45.874169676785)); +#8877 = CYLINDRICAL_SURFACE('',#8878,10.); +#8878 = AXIS2_PLACEMENT_3D('',#8879,#8880,#8881); +#8879 = CARTESIAN_POINT('',(-2.34,-513.2847946904,41.859386038632)); +#8880 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8881 = DIRECTION('',(-1.,7.431448254774E-16,-6.691306063589E-16)); +#8882 = ADVANCED_FACE('',(#8883,#8910),#8913,.T.); +#8883 = FACE_BOUND('',#8884,.T.); +#8884 = EDGE_LOOP('',(#8885,#8895,#8902,#8903)); +#8885 = ORIENTED_EDGE('',*,*,#8886,.T.); +#8886 = EDGE_CURVE('',#8887,#8889,#8891,.T.); +#8887 = VERTEX_POINT('',#8888); +#8888 = CARTESIAN_POINT('',(-17.34,-496.5595321657,46.984382963563)); +#8889 = VERTEX_POINT('',#8890); +#8890 = CARTESIAN_POINT('',(12.66,-496.5595321657,46.984382963563)); +#8891 = LINE('',#8892,#8893); +#8892 = CARTESIAN_POINT('',(-17.34,-496.5595321657,46.984382963563)); +#8893 = VECTOR('',#8894,1.); +#8894 = DIRECTION('',(1.,0.,0.)); +#8895 = ORIENTED_EDGE('',*,*,#8896,.F.); +#8896 = EDGE_CURVE('',#8889,#8889,#8897,.T.); +#8897 = CIRCLE('',#8898,15.); +#8898 = AXIS2_PLACEMENT_3D('',#8899,#8900,#8901); +#8899 = CARTESIAN_POINT('',(12.66,-506.5964912611,35.837210581402)); +#8900 = DIRECTION('',(1.,0.,-0.)); +#8901 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8902 = ORIENTED_EDGE('',*,*,#8886,.F.); +#8903 = ORIENTED_EDGE('',*,*,#8904,.T.); +#8904 = EDGE_CURVE('',#8887,#8887,#8905,.T.); +#8905 = CIRCLE('',#8906,15.); +#8906 = AXIS2_PLACEMENT_3D('',#8907,#8908,#8909); +#8907 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#8908 = DIRECTION('',(1.,0.,-0.)); +#8909 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8910 = FACE_BOUND('',#8911,.T.); +#8911 = EDGE_LOOP('',(#8912)); +#8912 = ORIENTED_EDGE('',*,*,#8748,.F.); +#8913 = CYLINDRICAL_SURFACE('',#8914,15.); +#8914 = AXIS2_PLACEMENT_3D('',#8915,#8916,#8917); +#8915 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#8916 = DIRECTION('',(1.,0.,0.)); +#8917 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8918 = ADVANCED_FACE('',(#8919,#8922),#8933,.T.); +#8919 = FACE_BOUND('',#8920,.T.); +#8920 = EDGE_LOOP('',(#8921)); +#8921 = ORIENTED_EDGE('',*,*,#8740,.T.); +#8922 = FACE_BOUND('',#8923,.T.); +#8923 = EDGE_LOOP('',(#8924)); +#8924 = ORIENTED_EDGE('',*,*,#8925,.F.); +#8925 = EDGE_CURVE('',#8926,#8926,#8928,.T.); +#8926 = VERTEX_POINT('',#8927); +#8927 = CARTESIAN_POINT('',(-8.34,-633.6742564177,150.25854426876)); +#8928 = CIRCLE('',#8929,6.); +#8929 = AXIS2_PLACEMENT_3D('',#8930,#8931,#8932); +#8930 = CARTESIAN_POINT('',(-2.34,-633.6742564177,150.25854426876)); +#8931 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8932 = DIRECTION('',(-1.,7.431448254774E-16,-6.691306063589E-16)); +#8933 = PLANE('',#8934); +#8934 = AXIS2_PLACEMENT_3D('',#8935,#8936,#8937); +#8935 = CARTESIAN_POINT('',(-2.34,-633.6742564177,150.25854426876)); +#8936 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8937 = DIRECTION('',(-1.,7.431448254774E-16,-6.691306063589E-16)); +#8938 = ADVANCED_FACE('',(#8939,#8942),#8953,.F.); +#8939 = FACE_BOUND('',#8940,.F.); +#8940 = EDGE_LOOP('',(#8941)); +#8941 = ORIENTED_EDGE('',*,*,#8904,.T.); +#8942 = FACE_BOUND('',#8943,.F.); +#8943 = EDGE_LOOP('',(#8944)); +#8944 = ORIENTED_EDGE('',*,*,#8945,.F.); +#8945 = EDGE_CURVE('',#8946,#8946,#8948,.T.); +#8946 = VERTEX_POINT('',#8947); +#8947 = CARTESIAN_POINT('',(-17.34,-501.9125770166,41.039224359744)); +#8948 = CIRCLE('',#8949,7.); +#8949 = AXIS2_PLACEMENT_3D('',#8950,#8951,#8952); +#8950 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#8951 = DIRECTION('',(1.,0.,-0.)); +#8952 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8953 = PLANE('',#8954); +#8954 = AXIS2_PLACEMENT_3D('',#8955,#8956,#8957); +#8955 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#8956 = DIRECTION('',(1.,0.,0.)); +#8957 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8958 = ADVANCED_FACE('',(#8959,#8962),#8973,.T.); +#8959 = FACE_BOUND('',#8960,.T.); +#8960 = EDGE_LOOP('',(#8961)); +#8961 = ORIENTED_EDGE('',*,*,#8896,.T.); +#8962 = FACE_BOUND('',#8963,.T.); +#8963 = EDGE_LOOP('',(#8964)); +#8964 = ORIENTED_EDGE('',*,*,#8965,.F.); +#8965 = EDGE_CURVE('',#8966,#8966,#8968,.T.); +#8966 = VERTEX_POINT('',#8967); +#8967 = CARTESIAN_POINT('',(12.66,-501.9125770166,41.039224359744)); +#8968 = CIRCLE('',#8969,7.); +#8969 = AXIS2_PLACEMENT_3D('',#8970,#8971,#8972); +#8970 = CARTESIAN_POINT('',(12.66,-506.5964912611,35.837210581402)); +#8971 = DIRECTION('',(1.,0.,-0.)); +#8972 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8973 = PLANE('',#8974); +#8974 = AXIS2_PLACEMENT_3D('',#8975,#8976,#8977); +#8975 = CARTESIAN_POINT('',(12.66,-506.5964912611,35.837210581402)); +#8976 = DIRECTION('',(1.,0.,0.)); +#8977 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#8978 = ADVANCED_FACE('',(#8979),#9098,.F.); +#8979 = FACE_BOUND('',#8980,.F.); +#8980 = EDGE_LOOP('',(#8981,#8989,#8990,#8991)); +#8981 = ORIENTED_EDGE('',*,*,#8982,.T.); +#8982 = EDGE_CURVE('',#8983,#8926,#8985,.T.); +#8983 = VERTEX_POINT('',#8984); +#8984 = CARTESIAN_POINT('',(-8.34,-517.7436636433,45.874169676785)); +#8985 = LINE('',#8986,#8987); +#8986 = CARTESIAN_POINT('',(-8.34,-513.2847946904,41.859386038632)); +#8987 = VECTOR('',#8988,1.); +#8988 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#8989 = ORIENTED_EDGE('',*,*,#8925,.F.); +#8990 = ORIENTED_EDGE('',*,*,#8982,.F.); +#8991 = ORIENTED_EDGE('',*,*,#8992,.T.); +#8992 = EDGE_CURVE('',#8983,#8983,#8993,.T.); +#8993 = B_SPLINE_CURVE_WITH_KNOTS('',7,(#8994,#8995,#8996,#8997,#8998, + #8999,#9000,#9001,#9002,#9003,#9004,#9005,#9006,#9007,#9008,#9009, + #9010,#9011,#9012,#9013,#9014,#9015,#9016,#9017,#9018,#9019,#9020, + #9021,#9022,#9023,#9024,#9025,#9026,#9027,#9028,#9029,#9030,#9031, + #9032,#9033,#9034,#9035,#9036,#9037,#9038,#9039,#9040,#9041,#9042, + #9043,#9044,#9045,#9046,#9047,#9048,#9049,#9050,#9051,#9052,#9053, + #9054,#9055,#9056,#9057,#9058,#9059,#9060,#9061,#9062,#9063,#9064, + #9065,#9066,#9067,#9068,#9069,#9070,#9071,#9072,#9073,#9074,#9075, + #9076,#9077,#9078,#9079,#9080,#9081,#9082,#9083,#9084,#9085,#9086, + #9087,#9088,#9089,#9090,#9091,#9092,#9093,#9094,#9095,#9096,#9097), + .UNSPECIFIED.,.T.,.F.,(8,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,8),(0., + 3.932438313537E-02,9.320077792228E-02,0.11822750652,0.223849643943, + 0.25176715844,0.381497408892,0.437609437474,0.473550531953, + 0.526997113201,0.562910442032,0.593681373672,0.725248401701, + 0.77678615013,0.881704355506,0.906310979456,0.937686413657,1.), + .UNSPECIFIED.); +#8994 = CARTESIAN_POINT('',(-8.34,-517.7436636433,45.874169676785)); +#8995 = CARTESIAN_POINT('',(-8.34,-517.9181010072,45.68043735733)); +#8996 = CARTESIAN_POINT('',(-8.326785405172,-518.0883302537, + 45.483479091295)); +#8997 = CARTESIAN_POINT('',(-8.300432747103,-518.2533657274, + 45.284435168899)); +#8998 = CARTESIAN_POINT('',(-8.261292785722,-518.4123573071, + 45.084551809049)); +#8999 = CARTESIAN_POINT('',(-8.209987966935,-518.564603825, + 44.885160677763)); +#9000 = CARTESIAN_POINT('',(-8.147415790589,-518.7095548613, + 44.68764856549)); +#9001 = CARTESIAN_POINT('',(-7.975213360703,-519.0348349492, + 44.227310456052)); +#9002 = CARTESIAN_POINT('',(-7.856739510503,-519.2084032453, + 43.967366300833)); +#9003 = CARTESIAN_POINT('',(-7.721534795304,-519.3680411374, + 43.714942866473)); +#9004 = CARTESIAN_POINT('',(-7.571645220969,-519.5144170223, + 43.471239961998)); +#9005 = CARTESIAN_POINT('',(-7.408992026136,-519.6482767295, + 43.237287353081)); +#9006 = CARTESIAN_POINT('',(-7.235394126152,-519.7704004454, + 43.013940008126)); +#9007 = CARTESIAN_POINT('',(-6.967667038872,-519.9332138215, + 42.703374115644)); +#9008 = CARTESIAN_POINT('',(-6.880678290364,-519.9825426457, + 42.607207521418)); +#9009 = CARTESIAN_POINT('',(-6.791748088338,-520.0296519691, + 42.513413254347)); +#9010 = CARTESIAN_POINT('',(-6.700994956943,-520.0746315138, + 42.422019883584)); +#9011 = CARTESIAN_POINT('',(-6.608527127537,-520.1175674146, + 42.333051844432)); +#9012 = CARTESIAN_POINT('',(-6.51444253869,-520.15854222,42.246529438348 + )); +#9013 = CARTESIAN_POINT('',(-6.015303318023,-520.3626205582, + 41.807701697412)); +#9014 = CARTESIAN_POINT('',(-5.583766560227,-520.494399823, + 41.496103070875)); +#9015 = CARTESIAN_POINT('',(-5.131334118144,-520.5983784661, + 41.229855508325)); +#9016 = CARTESIAN_POINT('',(-4.663158430885,-520.6789011342, + 41.010340489993)); +#9017 = CARTESIAN_POINT('',(-4.183144971766,-520.7391940581, + 40.83852550585)); +#9018 = CARTESIAN_POINT('',(-3.694442875197,-520.7813739936, + 40.715241431127)); +#9019 = CARTESIAN_POINT('',(-3.069035748677,-520.8129562247, + 40.621826440423)); +#9020 = CARTESIAN_POINT('',(-2.937952460806,-520.8183530838, + 40.605760164283)); +#9021 = CARTESIAN_POINT('',(-2.806604095457,-520.8225626694, + 40.593168617305)); +#9022 = CARTESIAN_POINT('',(-2.675062656001,-520.8255984428, + 40.584060525716)); +#9023 = CARTESIAN_POINT('',(-2.543400217297,-520.8274689453, + 40.578441475167)); +#9024 = CARTESIAN_POINT('',(-2.411688925685,-520.8281777985, + 40.576313910734)); +#9025 = CARTESIAN_POINT('',(-1.668058710485,-520.825613565, + 40.584011929862)); +#9026 = CARTESIAN_POINT('',(-1.058797393277,-520.7983987239, + 40.665702339915)); +#9027 = CARTESIAN_POINT('',(-0.457540693998,-520.7462650001, + 40.821717207466)); +#9028 = CARTESIAN_POINT('',(0.130184108417,-520.6666477232, + 41.050927432633)); +#9029 = CARTESIAN_POINT('',(0.697495043033,-520.554550578, + 41.351655232371)); +#9030 = CARTESIAN_POINT('',(1.235415921454,-520.4025233757, + 41.721307787043)); +#9031 = CARTESIAN_POINT('',(1.945098558926,-520.1136939294, + 42.343215230751)); +#9032 = CARTESIAN_POINT('',(2.151854246797,-520.0170322855, + 42.543432222111)); +#9033 = CARTESIAN_POINT('',(2.350011567074,-519.9100494295, + 42.755856109456)); +#9034 = CARTESIAN_POINT('',(2.53827999318,-519.7917908182, + 42.980164605499)); +#9035 = CARTESIAN_POINT('',(2.715125416178,-519.6612271662, + 43.215900788133)); +#9036 = CARTESIAN_POINT('',(2.878735405714,-519.5172828025, + 43.46243360844)); +#9037 = CARTESIAN_POINT('',(3.121933889369,-519.2574183444, + 43.883216479687)); +#9038 = CARTESIAN_POINT('',(3.210652302704,-519.1499517538, + 44.051711604509)); +#9039 = CARTESIAN_POINT('',(3.292358351137,-519.0363958541, + 44.22385479918)); +#9040 = CARTESIAN_POINT('',(3.366329260682,-518.9167041118, + 44.399059432729)); +#9041 = CARTESIAN_POINT('',(3.43189259015,-518.7908753955, + 44.576717019474)); +#9042 = CARTESIAN_POINT('',(3.488422640423,-518.6589620721, + 44.756204824532)); +#9043 = CARTESIAN_POINT('',(3.605101151213,-518.3160363004, + 45.205588412406)); +#9044 = CARTESIAN_POINT('',(3.6535773444,-518.0978639885,45.476843552005 + )); +#9045 = CARTESIAN_POINT('',(3.678395171353,-517.8683752703, + 45.74672143294)); +#9046 = CARTESIAN_POINT('',(3.678395171353,-517.629949489, + 46.011520089576)); +#9047 = CARTESIAN_POINT('',(3.6535773444,-517.3855381303,46.267961564682 + )); +#9048 = CARTESIAN_POINT('',(3.605101151213,-517.1385741713, + 46.513292588773)); +#9049 = CARTESIAN_POINT('',(3.488458883167,-516.7276255464, + 46.901192625901)); +#9050 = CARTESIAN_POINT('',(3.431977932575,-516.5630294529, + 47.051040391092)); +#9051 = CARTESIAN_POINT('',(3.366474481871,-516.3996109293, + 47.194658556312)); +#9052 = CARTESIAN_POINT('',(3.292573246029,-516.2379856403, + 47.331926975785)); +#9053 = CARTESIAN_POINT('',(3.210946847673,-516.078761116, + 47.462780302178)); +#9054 = CARTESIAN_POINT('',(3.122319479665,-515.9225282905, + 47.587200771633)); +#9055 = CARTESIAN_POINT('',(2.946202985951,-515.6390392214, + 47.806323429308)); +#9056 = CARTESIAN_POINT('',(2.86028831579,-515.5107082727, + 47.902829501003)); +#9057 = CARTESIAN_POINT('',(2.770079463211,-515.385030255, + 47.994877570903)); +#9058 = CARTESIAN_POINT('',(2.675906990805,-515.2621592521, + 48.082618002221)); +#9059 = CARTESIAN_POINT('',(2.578079460016,-515.1422327712, + 48.166202258121)); +#9060 = CARTESIAN_POINT('',(2.476883773408,-515.0253711422, + 48.245782005477)); +#9061 = CARTESIAN_POINT('',(1.926638290049,-514.4255554255, + 48.645290186802)); +#9062 = CARTESIAN_POINT('',(1.422330682477,-513.9955525728, + 48.899812737613)); +#9063 = CARTESIAN_POINT('',(0.878232290641,-513.6289371845, + 49.096963502143)); +#9064 = CARTESIAN_POINT('',(0.30602146311,-513.329481887,49.246736838445 + )); +#9065 = CARTESIAN_POINT('',(-0.286273173011,-513.0994666251, + 49.356198357223)); +#9066 = CARTESIAN_POINT('',(-0.893182733652,-512.9407083491, + 49.429845192503)); +#9067 = CARTESIAN_POINT('',(-1.75197071819,-512.8220203408, + 49.484339911309)); +#9068 = CARTESIAN_POINT('',(-1.994806511253,-512.7999496192, + 49.494357732034)); +#9069 = CARTESIAN_POINT('',(-2.238294304798,-512.7892996422, + 49.499158353823)); +#9070 = CARTESIAN_POINT('',(-2.481980444023,-512.790111783, + 49.498792175086)); +#9071 = CARTESIAN_POINT('',(-2.725414652802,-512.8023839615, + 49.493256663417)); +#9072 = CARTESIAN_POINT('',(-2.968142796906,-512.8260706591, + 49.482496411113)); +#9073 = CARTESIAN_POINT('',(-3.701450102048,-512.9323593293, + 49.433641344121)); +#9074 = CARTESIAN_POINT('',(-4.187266501279,-513.0504153674, + 49.378850738982)); +#9075 = CARTESIAN_POINT('',(-4.664460724595,-513.2143069851, + 49.301215178388)); +#9076 = CARTESIAN_POINT('',(-5.12993262398,-513.4230011708, + 49.19874536994)); +#9077 = CARTESIAN_POINT('',(-5.579841251982,-513.6752183151, + 49.068387147892)); +#9078 = CARTESIAN_POINT('',(-6.009127883146,-513.9691630278, + 48.906041806201)); +#9079 = CARTESIAN_POINT('',(-6.505031336128,-514.380254947, + 48.659897848868)); +#9080 = CARTESIAN_POINT('',(-6.597765098259,-514.4605349854, + 48.611078448873)); +#9081 = CARTESIAN_POINT('',(-6.688939722515,-514.5429744141, + 48.560128001359)); +#9082 = CARTESIAN_POINT('',(-6.778462450368,-514.6275460112, + 48.506970461071)); +#9083 = CARTESIAN_POINT('',(-6.866230853057,-514.7142183347, + 48.451526857918)); +#9084 = CARTESIAN_POINT('',(-6.952132831592,-514.8029557229, + 48.393715296973)); +#9085 = CARTESIAN_POINT('',(-7.143043474594,-515.0094479078, + 48.25660905642)); +#9086 = CARTESIAN_POINT('',(-7.246830471182,-515.1284942916, + 48.175763189592)); +#9087 = CARTESIAN_POINT('',(-7.347127272725,-515.2507502367, + 48.090752171496)); +#9088 = CARTESIAN_POINT('',(-7.443630165704,-515.3760892297, + 48.001413898294)); +#9089 = CARTESIAN_POINT('',(-7.536011687656,-515.5043660929, + 47.907586333338)); +#9090 = CARTESIAN_POINT('',(-7.623920257963,-515.635417624, + 47.809108491713)); +#9091 = CARTESIAN_POINT('',(-7.871941302073,-516.0344918322, + 47.500686811414)); +#9092 = CARTESIAN_POINT('',(-8.019280229898,-516.3125728011, + 47.274711857992)); +#9093 = CARTESIAN_POINT('',(-8.143476996018,-516.5991114248, + 47.028438708318)); +#9094 = CARTESIAN_POINT('',(-8.240244264679,-516.8896504703, + 46.763013400144)); +#9095 = CARTESIAN_POINT('',(-8.306434758338,-517.1798308598, + 46.480304862705)); +#9096 = CARTESIAN_POINT('',(-8.34,-517.4656565525,46.182927830999)); +#9097 = CARTESIAN_POINT('',(-8.34,-517.7436636433,45.874169676785)); +#9098 = CYLINDRICAL_SURFACE('',#9099,6.); +#9099 = AXIS2_PLACEMENT_3D('',#9100,#9101,#9102); +#9100 = CARTESIAN_POINT('',(-2.34,-513.2847946904,41.859386038632)); +#9101 = DIRECTION('',(-1.E-15,-0.743144825477,0.669130606359)); +#9102 = DIRECTION('',(-1.,7.431448254774E-16,-6.691306063589E-16)); +#9103 = ADVANCED_FACE('',(#9104),#9115,.F.); +#9104 = FACE_BOUND('',#9105,.F.); +#9105 = EDGE_LOOP('',(#9106,#9107,#9113,#9114)); +#9106 = ORIENTED_EDGE('',*,*,#8965,.F.); +#9107 = ORIENTED_EDGE('',*,*,#9108,.F.); +#9108 = EDGE_CURVE('',#8946,#8966,#9109,.T.); +#9109 = LINE('',#9110,#9111); +#9110 = CARTESIAN_POINT('',(-17.34,-501.9125770166,41.039224359744)); +#9111 = VECTOR('',#9112,1.); +#9112 = DIRECTION('',(1.,0.,0.)); +#9113 = ORIENTED_EDGE('',*,*,#8945,.T.); +#9114 = ORIENTED_EDGE('',*,*,#9108,.T.); +#9115 = CYLINDRICAL_SURFACE('',#9116,7.); +#9116 = AXIS2_PLACEMENT_3D('',#9117,#9118,#9119); +#9117 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#9118 = DIRECTION('',(1.,0.,0.)); +#9119 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#9120 = ADVANCED_FACE('',(#9121),#9124,.T.); +#9121 = FACE_BOUND('',#9122,.T.); +#9122 = EDGE_LOOP('',(#9123)); +#9123 = ORIENTED_EDGE('',*,*,#8992,.T.); +#9124 = CYLINDRICAL_SURFACE('',#9125,15.); +#9125 = AXIS2_PLACEMENT_3D('',#9126,#9127,#9128); +#9126 = CARTESIAN_POINT('',(-17.34,-506.5964912611,35.837210581402)); +#9127 = DIRECTION('',(1.,0.,0.)); +#9128 = DIRECTION('',(0.,0.669130606359,0.743144825477)); +#9129 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#9133)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#9130,#9131,#9132)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#9130 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#9131 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#9132 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#9133 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#9130, + 'distance_accuracy_value','confusion accuracy'); +#9134 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#9135,#9137); +#9135 = ( REPRESENTATION_RELATIONSHIP('','',#8723,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#9136) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#9136 = ITEM_DEFINED_TRANSFORMATION('','',#11,#55); +#9137 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #9138); +#9138 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('11','StickCylinderOuter001','', + #5,#8718,$); +#9139 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#8720)); +#9140 = SHAPE_DEFINITION_REPRESENTATION(#9141,#9147); +#9141 = PRODUCT_DEFINITION_SHAPE('','',#9142); +#9142 = PRODUCT_DEFINITION('design','',#9143,#9146); +#9143 = PRODUCT_DEFINITION_FORMATION('','',#9144); +#9144 = PRODUCT('BucketCylinderInner','BucketCylinderInner','',(#9145)); +#9145 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9146 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#9147 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#9148),#9397); +#9148 = MANIFOLD_SOLID_BREP('',#9149); +#9149 = CLOSED_SHELL('',(#9150,#9295,#9304,#9340,#9360,#9380)); +#9150 = ADVANCED_FACE('',(#9151),#9290,.T.); +#9151 = FACE_BOUND('',#9152,.F.); +#9152 = EDGE_LOOP('',(#9153,#9162,#9170,#9289)); +#9153 = ORIENTED_EDGE('',*,*,#9154,.F.); +#9154 = EDGE_CURVE('',#9155,#9155,#9157,.T.); +#9155 = VERTEX_POINT('',#9156); +#9156 = CARTESIAN_POINT('',(-3.288,-918.4247569684,316.72477280592)); +#9157 = CIRCLE('',#9158,3.5); +#9158 = AXIS2_PLACEMENT_3D('',#9159,#9160,#9161); +#9159 = CARTESIAN_POINT('',(0.212,-918.4247569684,316.72477280592)); +#9160 = DIRECTION('',(-0.,-0.987688340595,-0.15643446504)); +#9161 = DIRECTION('',(-1.,0.,0.)); +#9162 = ORIENTED_EDGE('',*,*,#9163,.T.); +#9163 = EDGE_CURVE('',#9155,#9164,#9166,.T.); +#9164 = VERTEX_POINT('',#9165); +#9165 = CARTESIAN_POINT('',(-3.288,-1.082677328009E+03,290.70972126973) + ); +#9166 = LINE('',#9167,#9168); +#9167 = CARTESIAN_POINT('',(-3.288,-918.4247569684,316.72477280592)); +#9168 = VECTOR('',#9169,1.); +#9169 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9170 = ORIENTED_EDGE('',*,*,#9171,.T.); +#9171 = EDGE_CURVE('',#9164,#9164,#9172,.T.); +#9172 = B_SPLINE_CURVE_WITH_KNOTS('',7,(#9173,#9174,#9175,#9176,#9177, + #9178,#9179,#9180,#9181,#9182,#9183,#9184,#9185,#9186,#9187,#9188, + #9189,#9190,#9191,#9192,#9193,#9194,#9195,#9196,#9197,#9198,#9199, + #9200,#9201,#9202,#9203,#9204,#9205,#9206,#9207,#9208,#9209,#9210, + #9211,#9212,#9213,#9214,#9215,#9216,#9217,#9218,#9219,#9220,#9221, + #9222,#9223,#9224,#9225,#9226,#9227,#9228,#9229,#9230,#9231,#9232, + #9233,#9234,#9235,#9236,#9237,#9238,#9239,#9240,#9241,#9242,#9243, + #9244,#9245,#9246,#9247,#9248,#9249,#9250,#9251,#9252,#9253,#9254, + #9255,#9256,#9257,#9258,#9259,#9260,#9261,#9262,#9263,#9264,#9265, + #9266,#9267,#9268,#9269,#9270,#9271,#9272,#9273,#9274,#9275,#9276, + #9277,#9278,#9279,#9280,#9281,#9282,#9283,#9284,#9285,#9286,#9287, + #9288),.UNSPECIFIED.,.T.,.F.,(8,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6,6, + 8),(0.,3.926552750955E-02,6.537901140178E-02,9.70199891188E-02, + 0.12238714492,0.247776261025,0.378742388374,0.40404212655, + 0.435402177988,0.472799344234,0.526898883198,0.564187723589, + 0.595973797033,0.621335038835,0.747755382924,0.8787592429, + 0.904066270179,0.935435356756,0.96067046288,1.),.UNSPECIFIED.); +#9173 = CARTESIAN_POINT('',(-3.288,-1.082677328009E+03,290.70972126973) + ); +#9174 = CARTESIAN_POINT('',(-3.288,-1.082653884147E+03,290.5617025469)); +#9175 = CARTESIAN_POINT('',(-3.280513600073,-1.082633348019E+03, + 290.41343174636)); +#9176 = CARTESIAN_POINT('',(-3.265577903507,-1.082615833986E+03, + 290.26572285975)); +#9177 = CARTESIAN_POINT('',(-3.243375147748,-1.082601366704E+03, + 290.11939808428)); +#9178 = CARTESIAN_POINT('',(-3.214231399211,-1.08258988083E+03, + 289.97527464973)); +#9179 = CARTESIAN_POINT('',(-3.178618189528,-1.082581228467E+03, + 289.83415057398)); +#9180 = CARTESIAN_POINT('',(-3.109583449405,-1.082571181375E+03, + 289.60543745554)); +#9181 = CARTESIAN_POINT('',(-3.079400057681,-1.082568323034E+03, + 289.51567272502)); +#9182 = CARTESIAN_POINT('',(-3.046741146595,-1.082566553591E+03, + 289.42758983446)); +#9183 = CARTESIAN_POINT('',(-3.011743814904,-1.082565803219E+03, + 289.34127722306)); +#9184 = CARTESIAN_POINT('',(-2.974548355303,-1.082565999023E+03, + 289.25681693841)); +#9185 = CARTESIAN_POINT('',(-2.935298578141,-1.082567066066E+03, + 289.17428448497)); +#9186 = CARTESIAN_POINT('',(-2.844274024812,-1.082571184925E+03, + 288.99616568212)); +#9187 = CARTESIAN_POINT('',(-2.791596923085,-1.082574609504E+03, + 288.90149485982)); +#9188 = CARTESIAN_POINT('',(-2.736321998586,-1.082579070027E+03, + 288.80976430578)); +#9189 = CARTESIAN_POINT('',(-2.678647460585,-1.082584439183E+03, + 288.72099366292)); +#9190 = CARTESIAN_POINT('',(-2.618758785525,-1.082590595151E+03, + 288.63519427379)); +#9191 = CARTESIAN_POINT('',(-2.556828943557,-1.082597422309E+03, + 288.55236933748)); +#9192 = CARTESIAN_POINT('',(-2.441860715049,-1.082610736344E+03, + 288.40849262182)); +#9193 = CARTESIAN_POINT('',(-2.389485917614,-1.082617023195E+03, + 288.34636978442)); +#9194 = CARTESIAN_POINT('',(-2.335968958258,-1.082623617549E+03, + 288.28613505752)); +#9195 = CARTESIAN_POINT('',(-2.28137883839,-1.082630467393E+03, + 288.22777692504)); +#9196 = CARTESIAN_POINT('',(-2.225778835333,-1.082637523648E+03, + 288.17128285153)); +#9197 = CARTESIAN_POINT('',(-2.169226502323,-1.082644740168E+03, + 288.1166392822)); +#9198 = CARTESIAN_POINT('',(-1.827785975681,-1.082688323385E+03, + 287.80280501172)); +#9199 = CARTESIAN_POINT('',(-1.521028156374,-1.082727530416E+03, + 287.58593062088)); +#9200 = CARTESIAN_POINT('',(-1.198237892869,-1.082764405455E+03, + 287.41256459338)); +#9201 = CARTESIAN_POINT('',(-0.864249067258,-1.082795044362E+03, + 287.28189906634)); +#9202 = CARTESIAN_POINT('',(-0.522790310249,-1.082816837862E+03, + 287.19332100601)); +#9203 = CARTESIAN_POINT('',(-0.17701232623,-1.082828375778E+03, + 287.14663077428)); +#9204 = CARTESIAN_POINT('',(0.5326593169,-1.082830588444E+03, + 287.13767885073)); +#9205 = CARTESIAN_POINT('',(0.894428458042,-1.082820305431E+03, + 287.17929282784)); +#9206 = CARTESIAN_POINT('',(1.251996464656,-1.082798634023E+03, + 287.26656187644)); +#9207 = CARTESIAN_POINT('',(1.601803046584,-1.082767045397E+03, + 287.39969102017)); +#9208 = CARTESIAN_POINT('',(1.939640857739,-1.082728440773E+03, + 287.57936799762)); +#9209 = CARTESIAN_POINT('',(2.260017601128,-1.082687263134E+03, + 287.80650329578)); +#9210 = CARTESIAN_POINT('',(2.612188399908,-1.08264232191E+03, + 288.13497151469)); +#9211 = CARTESIAN_POINT('',(2.668297611715,-1.082635172683E+03, + 288.18997916908)); +#9212 = CARTESIAN_POINT('',(2.72344670653,-1.082628193637E+03, + 288.24682940484)); +#9213 = CARTESIAN_POINT('',(2.777577689298,-1.082621431035E+03, + 288.30553560237)); +#9214 = CARTESIAN_POINT('',(2.830626782576,-1.082614934056E+03, + 288.36610997173)); +#9215 = CARTESIAN_POINT('',(2.882524426533,-1.082608754801E+03, + 288.42856355267)); +#9216 = CARTESIAN_POINT('',(2.996003855347,-1.082595750885E+03, + 288.57266155323)); +#9217 = CARTESIAN_POINT('',(3.056940580158,-1.082589124734E+03, + 288.6553357719)); +#9218 = CARTESIAN_POINT('',(3.115845954624,-1.082583176732E+03, + 288.74093138144)); +#9219 = CARTESIAN_POINT('',(3.172548910922,-1.082578020337E+03, + 288.82944293331)); +#9220 = CARTESIAN_POINT('',(3.226866584228,-1.082573774847E+03, + 288.9208568772)); +#9221 = CARTESIAN_POINT('',(3.278604084787,-1.082570564673E+03, + 289.01515141868)); +#9222 = CARTESIAN_POINT('',(3.385927841752,-1.082566078681E+03, + 289.22814268411)); +#9223 = CARTESIAN_POINT('',(3.440397983105,-1.082565291657E+03, + 289.34816279464)); +#9224 = CARTESIAN_POINT('',(3.490482919497,-1.082566387005E+03, + 289.47209085034)); +#9225 = CARTESIAN_POINT('',(3.535735563226,-1.082569584412E+03, + 289.59963161515)); +#9226 = CARTESIAN_POINT('',(3.575741240083,-1.08257508668E+03, + 289.73046205472)); +#9227 = CARTESIAN_POINT('',(3.61011541602,-1.082583072608E+03, + 289.86423291591)); +#9228 = CARTESIAN_POINT('',(3.679565228594,-1.082609049052E+03, + 290.19779888581)); +#9229 = CARTESIAN_POINT('',(3.708073154425,-1.082629905662E+03, + 290.40027972799)); +#9230 = CARTESIAN_POINT('',(3.722709207967,-1.08265659598E+03, + 290.60559705956)); +#9231 = CARTESIAN_POINT('',(3.722827359679,-1.082689123995E+03, + 290.81126796328)); +#9232 = CARTESIAN_POINT('',(3.708424107243,-1.082727102047E+03, + 291.01483128171)); +#9233 = CARTESIAN_POINT('',(3.680138530397,-1.082769762871E+03, + 291.21392522589)); +#9234 = CARTESIAN_POINT('',(3.611117429153,-1.082847942314E+03, + 291.53898761542)); +#9235 = CARTESIAN_POINT('',(3.576991926901,-1.082881549303E+03, + 291.66840604114)); +#9236 = CARTESIAN_POINT('',(3.537255306857,-1.082916610066E+03, + 291.79429510249)); +#9237 = CARTESIAN_POINT('',(3.492288711576,-1.082952848148E+03, + 291.91637383772)); +#9238 = CARTESIAN_POINT('',(3.442504711018,-1.082989971863E+03, + 292.03439352579)); +#9239 = CARTESIAN_POINT('',(3.388349563832,-1.083027681547E+03, + 292.14813702478)); +#9240 = CARTESIAN_POINT('',(3.280827041235,-1.08309806507E+03, + 292.35057243159)); +#9241 = CARTESIAN_POINT('',(3.228476316959,-1.083130691397E+03, + 292.44057203607)); +#9242 = CARTESIAN_POINT('',(3.173469410196,-1.083163418287E+03, + 292.52742816799)); +#9243 = CARTESIAN_POINT('',(3.116009504758,-1.083196115348E+03, + 292.61115906215)); +#9244 = CARTESIAN_POINT('',(3.056287112496,-1.083228659991E+03, + 292.69178956285)); +#9245 = CARTESIAN_POINT('',(2.994480321925,-1.083260938129E+03, + 292.76935074273)); +#9246 = CARTESIAN_POINT('',(2.879910386421,-1.083318302373E+03, + 292.90334400332)); +#9247 = CARTESIAN_POINT('',(2.827835612368,-1.083343527786E+03, + 292.96088823245)); +#9248 = CARTESIAN_POINT('',(2.774606413843,-1.083368471573E+03, + 293.01653884943)); +#9249 = CARTESIAN_POINT('',(2.720292663573,-1.083393087365E+03, + 293.07032268093)); +#9250 = CARTESIAN_POINT('',(2.664958417849,-1.083417331951E+03, + 293.12226673987)); +#9251 = CARTESIAN_POINT('',(2.608661916526,-1.083441165273E+03, + 293.17239822546)); +#9252 = CARTESIAN_POINT('',(2.266294295502,-1.08358112043E+03, + 293.46174043472)); +#9253 = CARTESIAN_POINT('',(1.957740299679,-1.083686874408E+03, + 293.65904012813)); +#9254 = CARTESIAN_POINT('',(1.632760101819,-1.083776822215E+03, + 293.81496272544)); +#9255 = CARTESIAN_POINT('',(1.296325724204,-1.083847394485E+03, + 293.93155444913)); +#9256 = CARTESIAN_POINT('',(0.952255731628,-1.083896232863E+03, + 294.01025565296)); +#9257 = CARTESIAN_POINT('',(0.603766287349,-1.083922019589E+03, + 294.0517149907)); +#9258 = CARTESIAN_POINT('',(-0.1086593169,-1.083926909157E+03, + 294.05957540988)); +#9259 = CARTESIAN_POINT('',(-0.470428458043,-1.083904270004E+03, + 294.02317579156)); +#9260 = CARTESIAN_POINT('',(-0.827996464654,-1.083856691651E+03, + 293.94687482766)); +#9261 = CARTESIAN_POINT('',(-1.177803046586,-1.083785509915E+03, + 293.83002291019)); +#9262 = CARTESIAN_POINT('',(-1.515640857739,-1.083693271496E+03, + 293.67106943492)); +#9263 = CARTESIAN_POINT('',(-1.836017601129,-1.083583920567E+03, + 293.46777551968)); +#9264 = CARTESIAN_POINT('',(-2.188188399909,-1.083439676661E+03, + 293.16927128177)); +#9265 = CARTESIAN_POINT('',(-2.244297611715,-1.083415879042E+03, + 293.11916512635)); +#9266 = CARTESIAN_POINT('',(-2.29944670653,-1.083391673886E+03, + 293.06725398272)); +#9267 = CARTESIAN_POINT('',(-2.353577689298,-1.083367101057E+03, + 293.01351083004)); +#9268 = CARTESIAN_POINT('',(-2.406626782576,-1.083342203553E+03, + 292.95790885822)); +#9269 = CARTESIAN_POINT('',(-2.458524426533,-1.083317027514E+03, + 292.90042146793)); +#9270 = CARTESIAN_POINT('',(-2.572003855348,-1.083260131324E+03, + 292.76739455657)); +#9271 = CARTESIAN_POINT('',(-2.632940580159,-1.083228281741E+03, + 292.69081429562)); +#9272 = CARTESIAN_POINT('',(-2.691845954625,-1.083196174357E+03, + 292.6112460671)); +#9273 = CARTESIAN_POINT('',(-2.748548910923,-1.083163918761E+03, + 292.52865999246)); +#9274 = CARTESIAN_POINT('',(-2.802866584228,-1.083131632597E+03, + 292.44303209407)); +#9275 = CARTESIAN_POINT('',(-2.854604084787,-1.083099440925E+03, + 292.3543446542)); +#9276 = CARTESIAN_POINT('',(-2.942932627876,-1.08304176079E+03, + 292.18877107132)); +#9277 = CARTESIAN_POINT('',(-2.980512622093,-1.083016188872E+03, + 292.11295807316)); +#9278 = CARTESIAN_POINT('',(-3.016160484239,-1.082990844758E+03, + 292.03518748115)); +#9279 = CARTESIAN_POINT('',(-3.049746047691,-1.082965814209E+03, + 291.95550509915)); +#9280 = CARTESIAN_POINT('',(-3.08114247767,-1.082941182926E+03, + 291.8739627788)); +#9281 = CARTESIAN_POINT('',(-3.110226001301,-1.082917035677E+03, + 291.79061856508)); +#9282 = CARTESIAN_POINT('',(-3.178409755203,-1.082856705016E+03, + 291.57293487182)); +#9283 = CARTESIAN_POINT('',(-3.21408931529,-1.082821281124E+03, + 291.4359294921)); +#9284 = CARTESIAN_POINT('',(-3.243289017482,-1.082787626644E+03, + 291.29518375587)); +#9285 = CARTESIAN_POINT('',(-3.265534860806,-1.082756130704E+03, + 291.15141447883)); +#9286 = CARTESIAN_POINT('',(-3.280499404224,-1.082727109364E+03, + 291.00538228472)); +#9287 = CARTESIAN_POINT('',(-3.288,-1.082700794089E+03,290.85788026741) + ); +#9288 = CARTESIAN_POINT('',(-3.288,-1.082677328009E+03,290.70972126973) + ); +#9289 = ORIENTED_EDGE('',*,*,#9163,.F.); +#9290 = CYLINDRICAL_SURFACE('',#9291,3.5); +#9291 = AXIS2_PLACEMENT_3D('',#9292,#9293,#9294); +#9292 = CARTESIAN_POINT('',(0.212,-918.4247569684,316.72477280592)); +#9293 = DIRECTION('',(0.,0.987688340595,0.15643446504)); +#9294 = DIRECTION('',(-1.,0.,0.)); +#9295 = ADVANCED_FACE('',(#9296),#9299,.F.); +#9296 = FACE_BOUND('',#9297,.T.); +#9297 = EDGE_LOOP('',(#9298)); +#9298 = ORIENTED_EDGE('',*,*,#9154,.F.); +#9299 = PLANE('',#9300); +#9300 = AXIS2_PLACEMENT_3D('',#9301,#9302,#9303); +#9301 = CARTESIAN_POINT('',(0.212,-918.4247569684,316.72477280592)); +#9302 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9303 = DIRECTION('',(-1.,0.,0.)); +#9304 = ADVANCED_FACE('',(#9305,#9332),#9335,.T.); +#9305 = FACE_BOUND('',#9306,.T.); +#9306 = EDGE_LOOP('',(#9307,#9317,#9324,#9325)); +#9307 = ORIENTED_EDGE('',*,*,#9308,.T.); +#9308 = EDGE_CURVE('',#9309,#9311,#9313,.T.); +#9309 = VERTEX_POINT('',#9310); +#9310 = CARTESIAN_POINT('',(-7.288,-1.10045571814E+03,287.89390089901)); +#9311 = VERTEX_POINT('',#9312); +#9312 = CARTESIAN_POINT('',(7.712,-1.10045571814E+03,287.89390089901)); +#9313 = LINE('',#9314,#9315); +#9314 = CARTESIAN_POINT('',(-7.288,-1.10045571814E+03,287.89390089901)); +#9315 = VECTOR('',#9316,1.); +#9316 = DIRECTION('',(1.,0.,0.)); +#9317 = ORIENTED_EDGE('',*,*,#9318,.F.); +#9318 = EDGE_CURVE('',#9311,#9311,#9319,.T.); +#9319 = CIRCLE('',#9320,9.); +#9320 = AXIS2_PLACEMENT_3D('',#9321,#9322,#9323); +#9321 = CARTESIAN_POINT('',(7.712,-1.091566523075E+03,289.30181108437)); +#9322 = DIRECTION('',(1.,0.,0.)); +#9323 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9324 = ORIENTED_EDGE('',*,*,#9308,.F.); +#9325 = ORIENTED_EDGE('',*,*,#9326,.T.); +#9326 = EDGE_CURVE('',#9309,#9309,#9327,.T.); +#9327 = CIRCLE('',#9328,9.); +#9328 = AXIS2_PLACEMENT_3D('',#9329,#9330,#9331); +#9329 = CARTESIAN_POINT('',(-7.288,-1.091566523075E+03,289.30181108437) + ); +#9330 = DIRECTION('',(1.,0.,0.)); +#9331 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9332 = FACE_BOUND('',#9333,.T.); +#9333 = EDGE_LOOP('',(#9334)); +#9334 = ORIENTED_EDGE('',*,*,#9171,.T.); +#9335 = CYLINDRICAL_SURFACE('',#9336,9.); +#9336 = AXIS2_PLACEMENT_3D('',#9337,#9338,#9339); +#9337 = CARTESIAN_POINT('',(-7.288,-1.091566523075E+03,289.30181108437) + ); +#9338 = DIRECTION('',(1.,0.,0.)); +#9339 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9340 = ADVANCED_FACE('',(#9341,#9344),#9355,.F.); +#9341 = FACE_BOUND('',#9342,.F.); +#9342 = EDGE_LOOP('',(#9343)); +#9343 = ORIENTED_EDGE('',*,*,#9326,.T.); +#9344 = FACE_BOUND('',#9345,.F.); +#9345 = EDGE_LOOP('',(#9346)); +#9346 = ORIENTED_EDGE('',*,*,#9347,.F.); +#9347 = EDGE_CURVE('',#9348,#9348,#9350,.T.); +#9348 = VERTEX_POINT('',#9349); +#9349 = CARTESIAN_POINT('',(-7.288,-1.096504964778E+03,288.51963875917) + ); +#9350 = CIRCLE('',#9351,5.); +#9351 = AXIS2_PLACEMENT_3D('',#9352,#9353,#9354); +#9352 = CARTESIAN_POINT('',(-7.288,-1.091566523075E+03,289.30181108437) + ); +#9353 = DIRECTION('',(1.,0.,0.)); +#9354 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9355 = PLANE('',#9356); +#9356 = AXIS2_PLACEMENT_3D('',#9357,#9358,#9359); +#9357 = CARTESIAN_POINT('',(-7.288,-1.091566523075E+03,289.30181108437) + ); +#9358 = DIRECTION('',(1.,0.,0.)); +#9359 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9360 = ADVANCED_FACE('',(#9361,#9364),#9375,.T.); +#9361 = FACE_BOUND('',#9362,.T.); +#9362 = EDGE_LOOP('',(#9363)); +#9363 = ORIENTED_EDGE('',*,*,#9318,.T.); +#9364 = FACE_BOUND('',#9365,.T.); +#9365 = EDGE_LOOP('',(#9366)); +#9366 = ORIENTED_EDGE('',*,*,#9367,.F.); +#9367 = EDGE_CURVE('',#9368,#9368,#9370,.T.); +#9368 = VERTEX_POINT('',#9369); +#9369 = CARTESIAN_POINT('',(7.712,-1.096504964778E+03,288.51963875917)); +#9370 = CIRCLE('',#9371,5.); +#9371 = AXIS2_PLACEMENT_3D('',#9372,#9373,#9374); +#9372 = CARTESIAN_POINT('',(7.712,-1.091566523075E+03,289.30181108437)); +#9373 = DIRECTION('',(1.,0.,0.)); +#9374 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9375 = PLANE('',#9376); +#9376 = AXIS2_PLACEMENT_3D('',#9377,#9378,#9379); +#9377 = CARTESIAN_POINT('',(7.712,-1.091566523075E+03,289.30181108437)); +#9378 = DIRECTION('',(1.,0.,0.)); +#9379 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9380 = ADVANCED_FACE('',(#9381),#9392,.F.); +#9381 = FACE_BOUND('',#9382,.F.); +#9382 = EDGE_LOOP('',(#9383,#9384,#9390,#9391)); +#9383 = ORIENTED_EDGE('',*,*,#9367,.F.); +#9384 = ORIENTED_EDGE('',*,*,#9385,.F.); +#9385 = EDGE_CURVE('',#9348,#9368,#9386,.T.); +#9386 = LINE('',#9387,#9388); +#9387 = CARTESIAN_POINT('',(-7.288,-1.096504964778E+03,288.51963875917) + ); +#9388 = VECTOR('',#9389,1.); +#9389 = DIRECTION('',(1.,0.,0.)); +#9390 = ORIENTED_EDGE('',*,*,#9347,.T.); +#9391 = ORIENTED_EDGE('',*,*,#9385,.T.); +#9392 = CYLINDRICAL_SURFACE('',#9393,5.); +#9393 = AXIS2_PLACEMENT_3D('',#9394,#9395,#9396); +#9394 = CARTESIAN_POINT('',(-7.288,-1.091566523075E+03,289.30181108437) + ); +#9395 = DIRECTION('',(1.,0.,0.)); +#9396 = DIRECTION('',(0.,-0.987688340595,-0.15643446504)); +#9397 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#9401)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#9398,#9399,#9400)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#9398 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#9399 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#9400 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#9401 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#9398, + 'distance_accuracy_value','confusion accuracy'); +#9402 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#9403,#9405); +#9403 = ( REPRESENTATION_RELATIONSHIP('','',#9147,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#9404) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#9404 = ITEM_DEFINED_TRANSFORMATION('','',#11,#59); +#9405 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #9406); +#9406 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('12','BucketCylinderInner001','', + #5,#9142,$); +#9407 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#9144)); +#9408 = SHAPE_DEFINITION_REPRESENTATION(#9409,#9415); +#9409 = PRODUCT_DEFINITION_SHAPE('','',#9410); +#9410 = PRODUCT_DEFINITION('design','',#9411,#9414); +#9411 = PRODUCT_DEFINITION_FORMATION('','',#9412); +#9412 = PRODUCT('BucketCylinderOuter','BucketCylinderOuter','',(#9413)); +#9413 = PRODUCT_CONTEXT('',#2,'mechanical'); +#9414 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#9415 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#9416),#9840); +#9416 = MANIFOLD_SOLID_BREP('',#9417); +#9417 = CLOSED_SHELL('',(#9418,#9531,#9581,#9591,#9601,#9621,#9641,#9661 + ,#9814,#9831)); +#9418 = ADVANCED_FACE('',(#9419),#9526,.T.); +#9419 = FACE_BOUND('',#9420,.T.); +#9420 = EDGE_LOOP('',(#9421,#9431,#9438,#9439,#9456,#9465,#9502,#9511)); +#9421 = ORIENTED_EDGE('',*,*,#9422,.T.); +#9422 = EDGE_CURVE('',#9423,#9425,#9427,.T.); +#9423 = VERTEX_POINT('',#9424); +#9424 = CARTESIAN_POINT('',(-5.640000000002,-895.100951382, + 358.05789796717)); +#9425 = VERTEX_POINT('',#9426); +#9426 = CARTESIAN_POINT('',(-5.640000000002,-1.053654999617E+03, + 330.10054136279)); +#9427 = LINE('',#9428,#9429); +#9428 = CARTESIAN_POINT('',(-5.640000000001,-894.116143629, + 358.23154614483)); +#9429 = VECTOR('',#9430,1.); +#9430 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9431 = ORIENTED_EDGE('',*,*,#9432,.F.); +#9432 = EDGE_CURVE('',#9425,#9425,#9433,.T.); +#9433 = CIRCLE('',#9434,6.); +#9434 = AXIS2_PLACEMENT_3D('',#9435,#9436,#9437); +#9435 = CARTESIAN_POINT('',(0.359999999998,-1.053654999617E+03, + 330.10054136279)); +#9436 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9437 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9438 = ORIENTED_EDGE('',*,*,#9422,.F.); +#9439 = ORIENTED_EDGE('',*,*,#9440,.T.); +#9440 = EDGE_CURVE('',#9423,#9441,#9443,.T.); +#9441 = VERTEX_POINT('',#9442); +#9442 = CARTESIAN_POINT('',(-3.76310562562,-893.3592287708, + 353.93886867064)); +#9443 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#9444,#9445,#9446,#9447,#9448, + #9449,#9450,#9451,#9452,#9453,#9454,#9455),.UNSPECIFIED.,.F.,.F.,(7, + 5,7),(0.,0.442659268195,1.),.UNSPECIFIED.); +#9444 = CARTESIAN_POINT('',(-5.640000000001,-895.100951382, + 358.05789796717)); +#9445 = CARTESIAN_POINT('',(-5.640000000001,-895.0298271751, + 357.65453254541)); +#9446 = CARTESIAN_POINT('',(-5.606447149803,-894.9395267269, + 357.2583485994)); +#9447 = CARTESIAN_POINT('',(-5.540493428399,-894.8316970181, + 356.87469695618)); +#9448 = CARTESIAN_POINT('',(-5.444890988537,-894.7094266143, + 356.50850224254)); +#9449 = CARTESIAN_POINT('',(-5.323996917427,-894.5771079529, + 356.16403607483)); +#9450 = CARTESIAN_POINT('',(-5.007824203702,-894.2670809554, + 355.44288916563)); +#9451 = CARTESIAN_POINT('',(-4.799892876981,-894.0850267328, + 355.07757049338)); +#9452 = CARTESIAN_POINT('',(-4.567255725683,-893.8993350071, + 354.74700218863)); +#9453 = CARTESIAN_POINT('',(-4.314894693102,-893.7143819953, + 354.44868153828)); +#9454 = CARTESIAN_POINT('',(-4.046182245274,-893.5334390628, + 354.18004489418)); +#9455 = CARTESIAN_POINT('',(-3.763104315927,-893.3592279648, + 353.9388675548)); +#9456 = ORIENTED_EDGE('',*,*,#9457,.T.); +#9457 = EDGE_CURVE('',#9441,#9458,#9460,.T.); +#9458 = VERTEX_POINT('',#9459); +#9459 = CARTESIAN_POINT('',(4.483105625615,-893.3592287708, + 353.93886867064)); +#9460 = CIRCLE('',#9461,6.); +#9461 = AXIS2_PLACEMENT_3D('',#9462,#9463,#9464); +#9462 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9463 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9464 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9465 = ORIENTED_EDGE('',*,*,#9466,.T.); +#9466 = EDGE_CURVE('',#9458,#9467,#9469,.T.); +#9467 = VERTEX_POINT('',#9468); +#9468 = CARTESIAN_POINT('',(4.483105625615,-894.8730584872, + 362.52422361903)); +#9469 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#9470,#9471,#9472,#9473,#9474, + #9475,#9476,#9477,#9478,#9479,#9480,#9481,#9482,#9483,#9484,#9485, + #9486,#9487,#9488,#9489,#9490,#9491,#9492,#9493,#9494,#9495,#9496, + #9497,#9498,#9499,#9500,#9501),.UNSPECIFIED.,.F.,.F.,(7,5,5,5,5,5,7) + ,(0.,0.278882302633,0.361330253104,0.500039604488,0.593910553392, + 0.722889486355,1.),.UNSPECIFIED.); +#9470 = CARTESIAN_POINT('',(4.483104315924,-893.3592279648, + 353.9388675548)); +#9471 = CARTESIAN_POINT('',(4.76668846292,-893.5337505979, + 354.18047618257)); +#9472 = CARTESIAN_POINT('',(5.035855333057,-893.7150289361, + 354.4496420971)); +#9473 = CARTESIAN_POINT('',(5.288609847446,-893.9003274612, + 354.74860306075)); +#9474 = CARTESIAN_POINT('',(5.521557633062,-894.0863558874, + 355.07993710029)); +#9475 = CARTESIAN_POINT('',(5.729682678424,-894.2687089379, + 355.44615798254)); +#9476 = CARTESIAN_POINT('',(5.958031639906,-894.4928724716, + 355.9682545412)); +#9477 = CARTESIAN_POINT('',(6.007326199113,-894.5431826918, + 356.09054399308)); +#9478 = CARTESIAN_POINT('',(6.053584703186,-894.5924612947, + 356.21592344263)); +#9479 = CARTESIAN_POINT('',(6.096589723376,-894.6405184758, + 356.3443148144)); +#9480 = CARTESIAN_POINT('',(6.136131659396,-894.6871688374, + 356.47562964497)); +#9481 = CARTESIAN_POINT('',(6.232365359415,-894.8080487754, + 356.83544262277)); +#9482 = CARTESIAN_POINT('',(6.282454495431,-894.879509993, + 357.06951151383)); +#9483 = CARTESIAN_POINT('',(6.320949729218,-894.9453678713, + 357.31046891567)); +#9484 = CARTESIAN_POINT('',(6.346959280459,-895.004628824, + 357.55668471167)); +#9485 = CARTESIAN_POINT('',(6.359999999999,-895.0566103693, + 357.80642758813)); +#9486 = CARTESIAN_POINT('',(6.359999999999,-895.1309589691,358.22807945) + ); +#9487 = CARTESIAN_POINT('',(6.354027434298,-895.1574233812, + 358.39880270428)); +#9488 = CARTESIAN_POINT('',(6.342093490106,-895.1802432227, + 358.56945712619)); +#9489 = CARTESIAN_POINT('',(6.324305630703,-895.1993933926, + 358.73942460874)); +#9490 = CARTESIAN_POINT('',(6.300869201406,-895.2149225,358.9080808988) + ); +#9491 = CARTESIAN_POINT('',(6.23254501124,-895.2434764834, + 359.30386525654)); +#9492 = CARTESIAN_POINT('',(6.182957780587,-895.2533737196, + 359.52900925705)); +#9493 = CARTESIAN_POINT('',(6.124018944618,-895.257040522, + 359.74944950143)); +#9494 = CARTESIAN_POINT('',(6.056501172392,-895.2549721609, + 359.96444590821)); +#9495 = CARTESIAN_POINT('',(5.981268540667,-895.2477418922, + 360.17330184343)); +#9496 = CARTESIAN_POINT('',(5.723149721926,-895.2107165876, + 360.80950661897)); +#9497 = CARTESIAN_POINT('',(5.515707994392,-895.1645207676, + 361.21267974608)); +#9498 = CARTESIAN_POINT('',(5.283852131494,-895.1032262779, + 361.58472916432)); +#9499 = CARTESIAN_POINT('',(5.032481726662,-895.031725336, + 361.92650747114)); +#9500 = CARTESIAN_POINT('',(4.76491422019,-894.9539116797, + 362.23929075547)); +#9501 = CARTESIAN_POINT('',(4.483104315924,-894.8730581114, + 362.52422494324)); +#9502 = ORIENTED_EDGE('',*,*,#9503,.T.); +#9503 = EDGE_CURVE('',#9467,#9504,#9506,.T.); +#9504 = VERTEX_POINT('',#9505); +#9505 = CARTESIAN_POINT('',(-3.76310562562,-894.8730584872, + 362.52422361903)); +#9506 = CIRCLE('',#9507,6.); +#9507 = AXIS2_PLACEMENT_3D('',#9508,#9509,#9510); +#9508 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9509 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9510 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9511 = ORIENTED_EDGE('',*,*,#9512,.T.); +#9512 = EDGE_CURVE('',#9504,#9423,#9513,.T.); +#9513 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#9514,#9515,#9516,#9517,#9518, + #9519,#9520,#9521,#9522,#9523,#9524,#9525),.UNSPECIFIED.,.F.,.F.,(7, + 5,7),(0.,0.553778497516,1.),.UNSPECIFIED.); +#9514 = CARTESIAN_POINT('',(-3.763104315927,-894.8730581114, + 362.52422494324)); +#9515 = CARTESIAN_POINT('',(-4.044355585595,-894.9537514028, + 362.23985558337)); +#9516 = CARTESIAN_POINT('',(-4.311416101113,-895.0314154485, + 361.92775225989)); +#9517 = CARTESIAN_POINT('',(-4.562348112978,-895.1027983788, + 361.58677304432)); +#9518 = CARTESIAN_POINT('',(-4.793858436223,-895.1640317202, + 361.21564668485)); +#9519 = CARTESIAN_POINT('',(-5.001082543013,-895.210256019, + 360.81352218644)); +#9520 = CARTESIAN_POINT('',(-5.31907833768,-895.256164718, + 360.03167200008)); +#9521 = CARTESIAN_POINT('',(-5.441781889222,-895.2632916618, + 359.66026787242)); +#9522 = CARTESIAN_POINT('',(-5.538882312559,-895.2533355177, + 359.27149428823)); +#9523 = CARTESIAN_POINT('',(-5.605898737449,-895.2235436596, + 358.87098362458)); +#9524 = CARTESIAN_POINT('',(-5.640000000001,-895.1726545127, + 358.46454662857)); +#9525 = CARTESIAN_POINT('',(-5.640000000001,-895.100951382, + 358.05789796717)); +#9526 = CYLINDRICAL_SURFACE('',#9527,6.); +#9527 = AXIS2_PLACEMENT_3D('',#9528,#9529,#9530); +#9528 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9529 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9530 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9531 = ADVANCED_FACE('',(#9532,#9559),#9576,.T.); +#9532 = FACE_BOUND('',#9533,.T.); +#9533 = EDGE_LOOP('',(#9534,#9544,#9551,#9552)); +#9534 = ORIENTED_EDGE('',*,*,#9535,.T.); +#9535 = EDGE_CURVE('',#9536,#9538,#9540,.T.); +#9536 = VERTEX_POINT('',#9537); +#9537 = CARTESIAN_POINT('',(-7.140000000002,-886.9893556286, + 369.64245727396)); +#9538 = VERTEX_POINT('',#9539); +#9539 = CARTESIAN_POINT('',(7.859999999998,-886.9893556286, + 369.64245727396)); +#9540 = LINE('',#9541,#9542); +#9541 = CARTESIAN_POINT('',(-7.140000000001,-886.9893556286, + 369.64245727396)); +#9542 = VECTOR('',#9543,1.); +#9543 = DIRECTION('',(1.,0.,0.)); +#9544 = ORIENTED_EDGE('',*,*,#9545,.F.); +#9545 = EDGE_CURVE('',#9538,#9538,#9546,.T.); +#9546 = CIRCLE('',#9547,10.); +#9547 = AXIS2_PLACEMENT_3D('',#9548,#9549,#9550); +#9548 = CARTESIAN_POINT('',(7.859999999999,-885.2528738519, + 359.79437974384)); +#9549 = DIRECTION('',(1.,0.,0.)); +#9550 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9551 = ORIENTED_EDGE('',*,*,#9535,.F.); +#9552 = ORIENTED_EDGE('',*,*,#9553,.T.); +#9553 = EDGE_CURVE('',#9536,#9536,#9554,.T.); +#9554 = CIRCLE('',#9555,10.); +#9555 = AXIS2_PLACEMENT_3D('',#9556,#9557,#9558); +#9556 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9557 = DIRECTION('',(1.,0.,0.)); +#9558 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9559 = FACE_BOUND('',#9560,.T.); +#9560 = EDGE_LOOP('',(#9561,#9562,#9568,#9569,#9570)); +#9561 = ORIENTED_EDGE('',*,*,#9466,.F.); +#9562 = ORIENTED_EDGE('',*,*,#9563,.F.); +#9563 = EDGE_CURVE('',#9441,#9458,#9564,.T.); +#9564 = LINE('',#9565,#9566); +#9565 = CARTESIAN_POINT('',(-7.140000000001,-893.3592287708, + 353.93886867064)); +#9566 = VECTOR('',#9567,1.); +#9567 = DIRECTION('',(1.,0.,0.)); +#9568 = ORIENTED_EDGE('',*,*,#9440,.F.); +#9569 = ORIENTED_EDGE('',*,*,#9512,.F.); +#9570 = ORIENTED_EDGE('',*,*,#9571,.T.); +#9571 = EDGE_CURVE('',#9504,#9467,#9572,.T.); +#9572 = LINE('',#9573,#9574); +#9573 = CARTESIAN_POINT('',(-7.140000000001,-894.8730584872, + 362.52422361903)); +#9574 = VECTOR('',#9575,1.); +#9575 = DIRECTION('',(1.,0.,0.)); +#9576 = CYLINDRICAL_SURFACE('',#9577,10.); +#9577 = AXIS2_PLACEMENT_3D('',#9578,#9579,#9580); +#9578 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9579 = DIRECTION('',(1.,0.,0.)); +#9580 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9581 = ADVANCED_FACE('',(#9582),#9586,.F.); +#9582 = FACE_BOUND('',#9583,.F.); +#9583 = EDGE_LOOP('',(#9584,#9585)); +#9584 = ORIENTED_EDGE('',*,*,#9503,.T.); +#9585 = ORIENTED_EDGE('',*,*,#9571,.T.); +#9586 = PLANE('',#9587); +#9587 = AXIS2_PLACEMENT_3D('',#9588,#9589,#9590); +#9588 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9589 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9590 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9591 = ADVANCED_FACE('',(#9592),#9596,.F.); +#9592 = FACE_BOUND('',#9593,.F.); +#9593 = EDGE_LOOP('',(#9594,#9595)); +#9594 = ORIENTED_EDGE('',*,*,#9563,.F.); +#9595 = ORIENTED_EDGE('',*,*,#9457,.T.); +#9596 = PLANE('',#9597); +#9597 = AXIS2_PLACEMENT_3D('',#9598,#9599,#9600); +#9598 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9599 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9600 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9601 = ADVANCED_FACE('',(#9602,#9605),#9616,.T.); +#9602 = FACE_BOUND('',#9603,.T.); +#9603 = EDGE_LOOP('',(#9604)); +#9604 = ORIENTED_EDGE('',*,*,#9432,.T.); +#9605 = FACE_BOUND('',#9606,.T.); +#9606 = EDGE_LOOP('',(#9607)); +#9607 = ORIENTED_EDGE('',*,*,#9608,.F.); +#9608 = EDGE_CURVE('',#9609,#9609,#9611,.T.); +#9609 = VERTEX_POINT('',#9610); +#9610 = CARTESIAN_POINT('',(-3.140000000002,-1.053654999617E+03, + 330.10054136279)); +#9611 = CIRCLE('',#9612,3.5); +#9612 = AXIS2_PLACEMENT_3D('',#9613,#9614,#9615); +#9613 = CARTESIAN_POINT('',(0.359999999998,-1.053654999617E+03, + 330.10054136279)); +#9614 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9615 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9616 = PLANE('',#9617); +#9617 = AXIS2_PLACEMENT_3D('',#9618,#9619,#9620); +#9618 = CARTESIAN_POINT('',(0.359999999998,-1.053654999617E+03, + 330.10054136279)); +#9619 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9620 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9621 = ADVANCED_FACE('',(#9622,#9625),#9636,.F.); +#9622 = FACE_BOUND('',#9623,.F.); +#9623 = EDGE_LOOP('',(#9624)); +#9624 = ORIENTED_EDGE('',*,*,#9553,.T.); +#9625 = FACE_BOUND('',#9626,.F.); +#9626 = EDGE_LOOP('',(#9627)); +#9627 = ORIENTED_EDGE('',*,*,#9628,.F.); +#9628 = EDGE_CURVE('',#9629,#9629,#9631,.T.); +#9629 = VERTEX_POINT('',#9630); +#9630 = CARTESIAN_POINT('',(-7.140000000002,-885.9474665626, + 363.73361075588)); +#9631 = CIRCLE('',#9632,4.); +#9632 = AXIS2_PLACEMENT_3D('',#9633,#9634,#9635); +#9633 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9634 = DIRECTION('',(1.,0.,0.)); +#9635 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9636 = PLANE('',#9637); +#9637 = AXIS2_PLACEMENT_3D('',#9638,#9639,#9640); +#9638 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9639 = DIRECTION('',(1.,0.,0.)); +#9640 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9641 = ADVANCED_FACE('',(#9642,#9645),#9656,.T.); +#9642 = FACE_BOUND('',#9643,.T.); +#9643 = EDGE_LOOP('',(#9644)); +#9644 = ORIENTED_EDGE('',*,*,#9545,.T.); +#9645 = FACE_BOUND('',#9646,.T.); +#9646 = EDGE_LOOP('',(#9647)); +#9647 = ORIENTED_EDGE('',*,*,#9648,.F.); +#9648 = EDGE_CURVE('',#9649,#9649,#9651,.T.); +#9649 = VERTEX_POINT('',#9650); +#9650 = CARTESIAN_POINT('',(7.859999999998,-885.9474665626, + 363.73361075588)); +#9651 = CIRCLE('',#9652,4.); +#9652 = AXIS2_PLACEMENT_3D('',#9653,#9654,#9655); +#9653 = CARTESIAN_POINT('',(7.859999999999,-885.2528738519, + 359.79437974384)); +#9654 = DIRECTION('',(1.,0.,0.)); +#9655 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9656 = PLANE('',#9657); +#9657 = AXIS2_PLACEMENT_3D('',#9658,#9659,#9660); +#9658 = CARTESIAN_POINT('',(7.859999999999,-885.2528738519, + 359.79437974384)); +#9659 = DIRECTION('',(1.,0.,0.)); +#9660 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9661 = ADVANCED_FACE('',(#9662),#9809,.F.); +#9662 = FACE_BOUND('',#9663,.F.); +#9663 = EDGE_LOOP('',(#9664,#9672,#9673,#9674)); +#9664 = ORIENTED_EDGE('',*,*,#9665,.T.); +#9665 = EDGE_CURVE('',#9666,#9609,#9668,.T.); +#9666 = VERTEX_POINT('',#9667); +#9667 = CARTESIAN_POINT('',(-3.140000000002,-895.100951382, + 358.05789796717)); +#9668 = LINE('',#9669,#9670); +#9669 = CARTESIAN_POINT('',(-3.140000000001,-894.116143629, + 358.23154614483)); +#9670 = VECTOR('',#9671,1.); +#9671 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9672 = ORIENTED_EDGE('',*,*,#9608,.F.); +#9673 = ORIENTED_EDGE('',*,*,#9665,.F.); +#9674 = ORIENTED_EDGE('',*,*,#9675,.T.); +#9675 = EDGE_CURVE('',#9666,#9666,#9676,.T.); +#9676 = B_SPLINE_CURVE_WITH_KNOTS('',6,(#9677,#9678,#9679,#9680,#9681, + #9682,#9683,#9684,#9685,#9686,#9687,#9688,#9689,#9690,#9691,#9692, + #9693,#9694,#9695,#9696,#9697,#9698,#9699,#9700,#9701,#9702,#9703, + #9704,#9705,#9706,#9707,#9708,#9709,#9710,#9711,#9712,#9713,#9714, + #9715,#9716,#9717,#9718,#9719,#9720,#9721,#9722,#9723,#9724,#9725, + #9726,#9727,#9728,#9729,#9730,#9731,#9732,#9733,#9734,#9735,#9736, + #9737,#9738,#9739,#9740,#9741,#9742,#9743,#9744,#9745,#9746,#9747, + #9748,#9749,#9750,#9751,#9752,#9753,#9754,#9755,#9756,#9757,#9758, + #9759,#9760,#9761,#9762,#9763,#9764,#9765,#9766,#9767,#9768,#9769, + #9770,#9771,#9772,#9773,#9774,#9775,#9776,#9777,#9778,#9779,#9780, + #9781,#9782,#9783,#9784,#9785,#9786,#9787,#9788,#9789,#9790,#9791, + #9792,#9793,#9794,#9795,#9796,#9797,#9798,#9799,#9800,#9801,#9802, + #9803,#9804,#9805,#9806,#9807,#9808),.UNSPECIFIED.,.T.,.F.,(7,5,5,5, + 5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,5,7),(0., + 4.189936623363E-02,6.776443385695E-02,0.101382567774,0.129137813334, + 0.153083641105,0.173506673096,0.190452965582,0.249187386775, + 0.325728115164,0.369961394772,0.39840585919,0.432464508491, + 0.471645901775,0.527224944835,0.567717753025,0.601986301897, + 0.629116633326,0.672605275077,0.749971521159,0.826648444801, + 0.847280179852,0.870638730063,0.898411333693,0.932467587623, + 0.958694807973,1.),.UNSPECIFIED.); +#9677 = CARTESIAN_POINT('',(-3.140000000001,-895.100951382, + 358.05789796717)); +#9678 = CARTESIAN_POINT('',(-3.140000000001,-895.0685862228, + 357.87434602812)); +#9679 = CARTESIAN_POINT('',(-3.128089518694,-895.0321810733, + 357.69188844325)); +#9680 = CARTESIAN_POINT('',(-3.104358897324,-894.9920581524, + 357.51217135014)); +#9681 = CARTESIAN_POINT('',(-3.069307856407,-894.9487700181, + 357.33678092978)); +#9682 = CARTESIAN_POINT('',(-3.023839466298,-894.9030799622, + 357.16718403757)); +#9683 = CARTESIAN_POINT('',(-2.935596457345,-894.8267312137, + 356.90436807603)); +#9684 = CARTESIAN_POINT('',(-2.898411064901,-894.7969780171, + 356.80664234109)); +#9685 = CARTESIAN_POINT('',(-2.857947968336,-894.7667580027, + 356.71161341945)); +#9686 = CARTESIAN_POINT('',(-2.814438340207,-894.7362182853, + 356.61937352858)); +#9687 = CARTESIAN_POINT('',(-2.768119225071,-894.7055052524, + 356.5299996129)); +#9688 = CARTESIAN_POINT('',(-2.655696265398,-894.6348022427, + 356.33119613822)); +#9689 = CARTESIAN_POINT('',(-2.587812234258,-894.5947837313, + 356.22376455743)); +#9690 = CARTESIAN_POINT('',(-2.516016102646,-894.5549576152, + 356.12122644934)); +#9691 = CARTESIAN_POINT('',(-2.440704499938,-894.5155549901, + 356.02353105664)); +#9692 = CARTESIAN_POINT('',(-2.362237732819,-894.4767839481, + 355.93061123186)); +#9693 = CARTESIAN_POINT('',(-2.213821512076,-894.4074898871, + 355.76954522386)); +#9694 = CARTESIAN_POINT('',(-2.144759595123,-894.3767014238, + 355.69989008376)); +#9695 = CARTESIAN_POINT('',(-2.073920112428,-894.3465575183, + 355.63335892389)); +#9696 = CARTESIAN_POINT('',(-2.001450953427,-894.3171447438, + 355.56989163122)); +#9697 = CARTESIAN_POINT('',(-1.927482689079,-894.2885404876, + 355.50942869064)); +#9698 = CARTESIAN_POINT('',(-1.787116835298,-894.2368910293, + 355.40228797823)); +#9699 = CARTESIAN_POINT('',(-1.72107501505,-894.2136222974, + 355.35485822227)); +#9700 = CARTESIAN_POINT('',(-1.654076682917,-894.1910510632, + 355.30958524465)); +#9701 = CARTESIAN_POINT('',(-1.586189953351,-894.1692183001, + 355.26643519857)); +#9702 = CARTESIAN_POINT('',(-1.517477483461,-894.148161648, + 355.22537706273)); +#9703 = CARTESIAN_POINT('',(-1.388737176876,-894.1106477066, + 355.15312489297)); +#9704 = CARTESIAN_POINT('',(-1.328919307733,-894.0939696375, + 355.12136844262)); +#9705 = CARTESIAN_POINT('',(-1.268578680398,-894.0779026208, + 355.09109595233)); +#9706 = CARTESIAN_POINT('',(-1.207749521832,-894.0624666247, + 355.0622916832)); +#9707 = CARTESIAN_POINT('',(-1.14646447114,-894.0476801708, + 355.03494149545)); +#9708 = CARTESIAN_POINT('',(-1.033549945894,-894.0218442039, + 354.98753479115)); +#9709 = CARTESIAN_POINT('',(-0.98205293311,-894.0105870616, + 354.96702924455)); +#9710 = CARTESIAN_POINT('',(-0.93028138143,-893.9997986854, + 354.94750905408)); +#9711 = CARTESIAN_POINT('',(-0.878252717719,-893.9894881886, + 354.92896774189)); +#9712 = CARTESIAN_POINT('',(-0.825983955491,-893.9796640192, + 354.91139950686)); +#9713 = CARTESIAN_POINT('',(-0.591557944846,-893.9379967609, + 354.83726404542)); +#9714 = CARTESIAN_POINT('',(-0.406905002843,-893.9115889166, + 354.7913453635)); +#9715 = CARTESIAN_POINT('',(-0.220270901119,-893.891446672, + 354.75685162533)); +#9716 = CARTESIAN_POINT('',(-3.232320085882E-02,-893.8777936942, + 354.73365114041)); +#9717 = CARTESIAN_POINT('',(0.156308804861,-893.8707442367, + 354.72167723186)); +#9718 = CARTESIAN_POINT('',(0.590900387597,-893.8697356046, + 354.71996403366)); +#9719 = CARTESIAN_POINT('',(0.836765334354,-893.8803933427, + 354.7380665647)); +#9720 = CARTESIAN_POINT('',(1.081251233278,-893.9022725568,354.775208659 + )); +#9721 = CARTESIAN_POINT('',(1.322998640486,-893.935072426, + 354.83157807787)); +#9722 = CARTESIAN_POINT('',(1.560505078804,-893.9781804498, + 354.90755843849)); +#9723 = CARTESIAN_POINT('',(1.92577571713,-894.0609797268, + 355.05926857088)); +#9724 = CARTESIAN_POINT('',(2.057558348189,-894.094435378, + 355.12157173412)); +#9725 = CARTESIAN_POINT('',(2.187065484811,-894.1308563313, + 355.19075309056)); +#9726 = CARTESIAN_POINT('',(2.313956728931,-894.1700425265, + 355.26697709753)); +#9727 = CARTESIAN_POINT('',(2.437828485571,-894.2117550563, + 355.35043503347)); +#9728 = CARTESIAN_POINT('',(2.635626628556,-894.283978974, + 355.49980677858)); +#9729 = CARTESIAN_POINT('',(2.711601348218,-894.3131751451, + 355.56135199621)); +#9730 = CARTESIAN_POINT('',(2.786015903804,-894.3432275375,355.626044012 + )); +#9731 = CARTESIAN_POINT('',(2.858730865657,-894.3740536497, + 355.69394652366)); +#9732 = CARTESIAN_POINT('',(2.92958833172,-894.4055611299, + 355.76512444647)); +#9733 = CARTESIAN_POINT('',(3.080819260199,-894.4760664858, + 355.92887133991)); +#9734 = CARTESIAN_POINT('',(3.16033081871,-894.5153247285, + 356.02291133808)); +#9735 = CARTESIAN_POINT('',(3.236609440878,-894.5552326474, + 356.12184897302)); +#9736 = CARTESIAN_POINT('',(3.30928086531,-894.5955740639, + 356.22575386391)); +#9737 = CARTESIAN_POINT('',(3.377932684384,-894.6361086237, + 356.33467837366)); +#9738 = CARTESIAN_POINT('',(3.515947498936,-894.7231266024, + 356.57977671715)); +#9739 = CARTESIAN_POINT('',(3.583970719966,-894.7696551761, + 356.71777468398)); +#9740 = CARTESIAN_POINT('',(3.645227396884,-894.8156119888, + 356.86231724601)); +#9741 = CARTESIAN_POINT('',(3.698851174356,-894.8604391555, + 357.0129852953)); +#9742 = CARTESIAN_POINT('',(3.744057002803,-894.9035989358, + 357.16926954162)); +#9743 = CARTESIAN_POINT('',(3.831307348176,-895.0027690517, + 357.55936814971)); +#9744 = CARTESIAN_POINT('',(3.864087164051,-895.0565701356, + 357.79814666944)); +#9745 = CARTESIAN_POINT('',(3.875970787462,-895.1040472489, + 358.0428992322)); +#9746 = CARTESIAN_POINT('',(3.866124260543,-895.1438849842, + 358.28917643507)); +#9747 = CARTESIAN_POINT('',(3.835289455885,-895.1757451804, + 358.53234663131)); +#9748 = CARTESIAN_POINT('',(3.749882618572,-895.2177698805, + 358.93961264748)); +#9749 = CARTESIAN_POINT('',(3.704049773746,-895.2314639703, + 359.1071390531)); +#9750 = CARTESIAN_POINT('',(3.649156204293,-895.2414068647,359.26970836) + ); +#9751 = CARTESIAN_POINT('',(3.58606439488,-895.2479599032, + 359.42657323551)); +#9752 = CARTESIAN_POINT('',(3.515734565562,-895.2515434482, + 359.57707469924)); +#9753 = CARTESIAN_POINT('',(3.374493234079,-895.25349793,359.84215718517 + )); +#9754 = CARTESIAN_POINT('',(3.305240883068,-895.2525866938, + 359.95887710721)); +#9755 = CARTESIAN_POINT('',(3.231935919419,-895.25013047,360.07075310338 + )); +#9756 = CARTESIAN_POINT('',(3.15499667288,-895.2463770539, + 360.17775340381)); +#9757 = CARTESIAN_POINT('',(3.074802165497,-895.2415568677, + 360.27987104098)); +#9758 = CARTESIAN_POINT('',(2.925895794868,-895.2313854967, + 360.45411731617)); +#9759 = CARTESIAN_POINT('',(2.858258374914,-895.2263532311, + 360.5280772909)); +#9760 = CARTESIAN_POINT('',(2.788933644,-895.2208858406,360.59902603962) + ); +#9761 = CARTESIAN_POINT('',(2.718058744931,-895.2150780007, + 360.66698882726)); +#9762 = CARTESIAN_POINT('',(2.645755132174,-895.2090163461, + 360.73199315951)); +#9763 = CARTESIAN_POINT('',(2.45410866807,-895.1927820539, + 360.89357307956)); +#9764 = CARTESIAN_POINT('',(2.332683769215,-895.1823340802, + 360.98555720858)); +#9765 = CARTESIAN_POINT('',(2.20830272863,-895.1717611716, + 361.07012613647)); +#9766 = CARTESIAN_POINT('',(2.081352998909,-895.1613421183, + 361.14737443326)); +#9767 = CARTESIAN_POINT('',(1.95216168612,-895.1513127497, + 361.21738581818)); +#9768 = CARTESIAN_POINT('',(1.587654066521,-895.1250708191, + 361.3920372487)); +#9769 = CARTESIAN_POINT('',(1.348020880999,-895.1101248286, + 361.48118725368)); +#9770 = CARTESIAN_POINT('',(1.103946094761,-895.0981655925, + 361.54789603149)); +#9771 = CARTESIAN_POINT('',(0.856986201158,-895.089941683, + 361.59232620624)); +#9772 = CARTESIAN_POINT('',(0.608542757006,-895.0858261433, + 361.61455809645)); +#9773 = CARTESIAN_POINT('',(0.11367172422,-895.0858261433, + 361.61455809645)); +#9774 = CARTESIAN_POINT('',(-0.132561418577,-895.0898686603, + 361.592720609)); +#9775 = CARTESIAN_POINT('',(-0.377350203079,-895.0979469133, + 361.54907753237)); +#9776 = CARTESIAN_POINT('',(-0.619328694438,-895.1097007997, + 361.48355188259)); +#9777 = CARTESIAN_POINT('',(-0.856983092479,-895.1244078826, + 361.39598770564)); +#9778 = CARTESIAN_POINT('',(-1.150821197592,-895.1454319852, + 361.25663363341)); +#9779 = CARTESIAN_POINT('',(-1.212687193972,-895.1500247837, + 361.22547285293)); +#9780 = CARTESIAN_POINT('',(-1.274086552332,-895.1547317727, + 361.19269149381)); +#9781 = CARTESIAN_POINT('',(-1.334985592599,-895.1595299641, + 361.15828235259)); +#9782 = CARTESIAN_POINT('',(-1.395348967336,-895.164394384, + 361.12223717679)); +#9783 = CARTESIAN_POINT('',(-1.522832656739,-895.1748498614, + 361.04187474638)); +#9784 = CARTESIAN_POINT('',(-1.589790998178,-895.1804518862, + 360.99709403848)); +#9785 = CARTESIAN_POINT('',(-1.655961403947,-895.1860651168, + 360.95019077138)); +#9786 = CARTESIAN_POINT('',(-1.721285644466,-895.1916468135, + 360.90114980895)); +#9787 = CARTESIAN_POINT('',(-1.785700542698,-895.1971505263, + 360.84995464865)); +#9788 = CARTESIAN_POINT('',(-1.924563144921,-895.2089174835, + 360.73313541656)); +#9789 = CARTESIAN_POINT('',(-1.998604602692,-895.2151274359, + 360.66661445992)); +#9790 = CARTESIAN_POINT('',(-2.071149236627,-895.221071791, + 360.59699252518)); +#9791 = CARTESIAN_POINT('',(-2.142066651901,-895.2266575472, + 360.52424010485)); +#9792 = CARTESIAN_POINT('',(-2.211209169695,-895.2317828628, + 360.44833021032)); +#9793 = CARTESIAN_POINT('',(-2.360819324852,-895.2419216528, + 360.27225172394)); +#9794 = CARTESIAN_POINT('',(-2.440330909873,-895.2466487607, + 360.1704558774)); +#9795 = CARTESIAN_POINT('',(-2.516609547056,-895.2503112734, + 360.06383556868)); +#9796 = CARTESIAN_POINT('',(-2.589280972972,-895.252682237, + 359.95239931277)); +#9797 = CARTESIAN_POINT('',(-2.657932762868,-895.2535178834, + 359.8361801421)); +#9798 = CARTESIAN_POINT('',(-2.771539845128,-895.2518234937, + 359.62209575444)); +#9799 = CARTESIAN_POINT('',(-2.818321999938,-895.2500229124, + 359.52613886972)); +#9800 = CARTESIAN_POINT('',(-2.862204434082,-895.2470414464, + 359.42747995312)); +#9801 = CARTESIAN_POINT('',(-2.902939375707,-895.2427633382, + 359.32624927421)); +#9802 = CARTESIAN_POINT('',(-2.940285802933,-895.2370779102, + 359.2225925623)); +#9803 = CARTESIAN_POINT('',(-3.027118399151,-895.2185515688, + 358.94985396929)); +#9804 = CARTESIAN_POINT('',(-3.071329199072,-895.2034571227, + 358.77713986727)); +#9805 = CARTESIAN_POINT('',(-3.105386379106,-895.1842365562, + 358.59999823077)); +#9806 = CARTESIAN_POINT('',(-3.128434235794,-895.1606902051, + 358.42000515945)); +#9807 = CARTESIAN_POINT('',(-3.140000000001,-895.1328447441, + 358.23877421192)); +#9808 = CARTESIAN_POINT('',(-3.140000000001,-895.100951382, + 358.05789796717)); +#9809 = CYLINDRICAL_SURFACE('',#9810,3.5); +#9810 = AXIS2_PLACEMENT_3D('',#9811,#9812,#9813); +#9811 = CARTESIAN_POINT('',(0.359999999999,-894.116143629, + 358.23154614483)); +#9812 = DIRECTION('',(-1.E-15,-0.984807753012,-0.173648177667)); +#9813 = DIRECTION('',(-1.,9.848077530122E-16,1.736481776669E-16)); +#9814 = ADVANCED_FACE('',(#9815),#9826,.F.); +#9815 = FACE_BOUND('',#9816,.F.); +#9816 = EDGE_LOOP('',(#9817,#9818,#9824,#9825)); +#9817 = ORIENTED_EDGE('',*,*,#9648,.F.); +#9818 = ORIENTED_EDGE('',*,*,#9819,.F.); +#9819 = EDGE_CURVE('',#9629,#9649,#9820,.T.); +#9820 = LINE('',#9821,#9822); +#9821 = CARTESIAN_POINT('',(-7.140000000001,-885.9474665626, + 363.73361075589)); +#9822 = VECTOR('',#9823,1.); +#9823 = DIRECTION('',(1.,0.,0.)); +#9824 = ORIENTED_EDGE('',*,*,#9628,.T.); +#9825 = ORIENTED_EDGE('',*,*,#9819,.T.); +#9826 = CYLINDRICAL_SURFACE('',#9827,4.); +#9827 = AXIS2_PLACEMENT_3D('',#9828,#9829,#9830); +#9828 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9829 = DIRECTION('',(1.,0.,0.)); +#9830 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9831 = ADVANCED_FACE('',(#9832),#9835,.T.); +#9832 = FACE_BOUND('',#9833,.T.); +#9833 = EDGE_LOOP('',(#9834)); +#9834 = ORIENTED_EDGE('',*,*,#9675,.T.); +#9835 = CYLINDRICAL_SURFACE('',#9836,10.); +#9836 = AXIS2_PLACEMENT_3D('',#9837,#9838,#9839); +#9837 = CARTESIAN_POINT('',(-7.140000000001,-885.2528738519, + 359.79437974384)); +#9838 = DIRECTION('',(1.,0.,0.)); +#9839 = DIRECTION('',(0.,-0.173648177667,0.984807753012)); +#9840 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#9844)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#9841,#9842,#9843)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#9841 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#9842 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#9843 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#9844 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-07),#9841, + 'distance_accuracy_value','confusion accuracy'); +#9845 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#9846,#9848); +#9846 = ( REPRESENTATION_RELATIONSHIP('','',#9415,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#9847) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#9847 = ITEM_DEFINED_TRANSFORMATION('','',#11,#63); +#9848 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #9849); +#9849 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('13','BucketCylinderOuter001','', + #5,#9410,$); +#9850 = PRODUCT_RELATED_PRODUCT_CATEGORY('part',$,(#9412)); +#9851 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9852),#3438); +#9852 = STYLED_ITEM('color',(#9853),#2642); +#9853 = PRESENTATION_STYLE_ASSIGNMENT((#9854,#9860)); +#9854 = SURFACE_STYLE_USAGE(.BOTH.,#9855); +#9855 = SURFACE_SIDE_STYLE('',(#9856)); +#9856 = SURFACE_STYLE_FILL_AREA(#9857); +#9857 = FILL_AREA_STYLE('',(#9858)); +#9858 = FILL_AREA_STYLE_COLOUR('',#9859); +#9859 = COLOUR_RGB('',0.541176494856,0.890196087049,0.631372563332); +#9860 = CURVE_STYLE('',#9861,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9861 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9862 = COLOUR_RGB('',9.803921802644E-02,9.803921802644E-02, + 9.803921802644E-02); +#9863 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9864),#9840); +#9864 = STYLED_ITEM('color',(#9865),#9416); +#9865 = PRESENTATION_STYLE_ASSIGNMENT((#9866,#9872)); +#9866 = SURFACE_STYLE_USAGE(.BOTH.,#9867); +#9867 = SURFACE_SIDE_STYLE('',(#9868)); +#9868 = SURFACE_STYLE_FILL_AREA(#9869); +#9869 = FILL_AREA_STYLE('',(#9870)); +#9870 = FILL_AREA_STYLE_COLOUR('',#9871); +#9871 = COLOUR_RGB('',0.800000010877,0.800000010877,0.800000010877); +#9872 = CURVE_STYLE('',#9873,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9873 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9874 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9875),#7733); +#9875 = STYLED_ITEM('color',(#9876),#7069); +#9876 = PRESENTATION_STYLE_ASSIGNMENT((#9877,#9882)); +#9877 = SURFACE_STYLE_USAGE(.BOTH.,#9878); +#9878 = SURFACE_SIDE_STYLE('',(#9879)); +#9879 = SURFACE_STYLE_FILL_AREA(#9880); +#9880 = FILL_AREA_STYLE('',(#9881)); +#9881 = FILL_AREA_STYLE_COLOUR('',#9871); +#9882 = CURVE_STYLE('',#9883,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9883 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9884 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9885),#6330); +#9885 = STYLED_ITEM('color',(#9886),#3457); +#9886 = PRESENTATION_STYLE_ASSIGNMENT((#9887,#9893)); +#9887 = SURFACE_STYLE_USAGE(.BOTH.,#9888); +#9888 = SURFACE_SIDE_STYLE('',(#9889)); +#9889 = SURFACE_STYLE_FILL_AREA(#9890); +#9890 = FILL_AREA_STYLE('',(#9891)); +#9891 = FILL_AREA_STYLE_COLOUR('',#9892); +#9892 = COLOUR_RGB('',0.301960791261,0.301960791261,0.301960791261); +#9893 = CURVE_STYLE('',#9894,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9894 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9895 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9896),#1573); +#9896 = STYLED_ITEM('color',(#9897),#153); +#9897 = PRESENTATION_STYLE_ASSIGNMENT((#9898,#9904)); +#9898 = SURFACE_STYLE_USAGE(.BOTH.,#9899); +#9899 = SURFACE_SIDE_STYLE('',(#9900)); +#9900 = SURFACE_STYLE_FILL_AREA(#9901); +#9901 = FILL_AREA_STYLE('',(#9902)); +#9902 = FILL_AREA_STYLE_COLOUR('',#9903); +#9903 = COLOUR_RGB('',7.450980588415E-02,0.615686309239, + 7.450980588415E-02); +#9904 = CURVE_STYLE('',#9905,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9905 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9906 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9907),#8157); +#9907 = STYLED_ITEM('color',(#9908),#7752); +#9908 = PRESENTATION_STYLE_ASSIGNMENT((#9909,#9914)); +#9909 = SURFACE_STYLE_USAGE(.BOTH.,#9910); +#9910 = SURFACE_SIDE_STYLE('',(#9911)); +#9911 = SURFACE_STYLE_FILL_AREA(#9912); +#9912 = FILL_AREA_STYLE('',(#9913)); +#9913 = FILL_AREA_STYLE_COLOUR('',#9871); +#9914 = CURVE_STYLE('',#9915,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9915 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9916 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9917),#8431); +#9917 = STYLED_ITEM('color',(#9918),#8176); +#9918 = PRESENTATION_STYLE_ASSIGNMENT((#9919,#9924)); +#9919 = SURFACE_STYLE_USAGE(.BOTH.,#9920); +#9920 = SURFACE_SIDE_STYLE('',(#9921)); +#9921 = SURFACE_STYLE_FILL_AREA(#9922); +#9922 = FILL_AREA_STYLE('',(#9923)); +#9923 = FILL_AREA_STYLE_COLOUR('',#9871); +#9924 = CURVE_STYLE('',#9925,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9925 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9926 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9927),#7050); +#9927 = STYLED_ITEM('color',(#9928),#6349); +#9928 = PRESENTATION_STYLE_ASSIGNMENT((#9929,#9934)); +#9929 = SURFACE_STYLE_USAGE(.BOTH.,#9930); +#9930 = SURFACE_SIDE_STYLE('',(#9931)); +#9931 = SURFACE_STYLE_FILL_AREA(#9932); +#9932 = FILL_AREA_STYLE('',(#9933)); +#9933 = FILL_AREA_STYLE_COLOUR('',#9871); +#9934 = CURVE_STYLE('',#9935,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9935 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9936 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9937),#134); +#9937 = STYLED_ITEM('color',(#9938),#81); +#9938 = PRESENTATION_STYLE_ASSIGNMENT((#9939,#9944)); +#9939 = SURFACE_STYLE_USAGE(.BOTH.,#9940); +#9940 = SURFACE_SIDE_STYLE('',(#9941)); +#9941 = SURFACE_STYLE_FILL_AREA(#9942); +#9942 = FILL_AREA_STYLE('',(#9943)); +#9943 = FILL_AREA_STYLE_COLOUR('',#9871); +#9944 = CURVE_STYLE('',#9945,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9945 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9946 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9947),#9129); +#9947 = STYLED_ITEM('color',(#9948),#8724); +#9948 = PRESENTATION_STYLE_ASSIGNMENT((#9949,#9954)); +#9949 = SURFACE_STYLE_USAGE(.BOTH.,#9950); +#9950 = SURFACE_SIDE_STYLE('',(#9951)); +#9951 = SURFACE_STYLE_FILL_AREA(#9952); +#9952 = FILL_AREA_STYLE('',(#9953)); +#9953 = FILL_AREA_STYLE_COLOUR('',#9871); +#9954 = CURVE_STYLE('',#9955,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9955 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9956 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9957),#8705); +#9957 = STYLED_ITEM('color',(#9958),#8450); +#9958 = PRESENTATION_STYLE_ASSIGNMENT((#9959,#9964)); +#9959 = SURFACE_STYLE_USAGE(.BOTH.,#9960); +#9960 = SURFACE_SIDE_STYLE('',(#9961)); +#9961 = SURFACE_STYLE_FILL_AREA(#9962); +#9962 = FILL_AREA_STYLE('',(#9963)); +#9963 = FILL_AREA_STYLE_COLOUR('',#9871); +#9964 = CURVE_STYLE('',#9965,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9965 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9966 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9967),#2623); +#9967 = STYLED_ITEM('color',(#9968),#1592); +#9968 = PRESENTATION_STYLE_ASSIGNMENT((#9969,#9974)); +#9969 = SURFACE_STYLE_USAGE(.BOTH.,#9970); +#9970 = SURFACE_SIDE_STYLE('',(#9971)); +#9971 = SURFACE_STYLE_FILL_AREA(#9972); +#9972 = FILL_AREA_STYLE('',(#9973)); +#9973 = FILL_AREA_STYLE_COLOUR('',#9859); +#9974 = CURVE_STYLE('',#9975,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9975 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +#9976 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #9977),#9397); +#9977 = STYLED_ITEM('color',(#9978),#9148); +#9978 = PRESENTATION_STYLE_ASSIGNMENT((#9979,#9984)); +#9979 = SURFACE_STYLE_USAGE(.BOTH.,#9980); +#9980 = SURFACE_SIDE_STYLE('',(#9981)); +#9981 = SURFACE_STYLE_FILL_AREA(#9982); +#9982 = FILL_AREA_STYLE('',(#9983)); +#9983 = FILL_AREA_STYLE_COLOUR('',#9871); +#9984 = CURVE_STYLE('',#9985,POSITIVE_LENGTH_MEASURE(0.1),#9862); +#9985 = DRAUGHTING_PRE_DEFINED_CURVE_FONT('continuous'); +ENDSEC; +END-ISO-10303-21; diff --git a/Detectors/CADSupport/examples/ExcavatorArm_MATERIALS.csv b/Detectors/CADSupport/examples/ExcavatorArm_MATERIALS.csv new file mode 100644 index 0000000000000..dd1e24148083a --- /dev/null +++ b/Detectors/CADSupport/examples/ExcavatorArm_MATERIALS.csv @@ -0,0 +1,14 @@ +Type,"[CAD/Document] Type","[Part/CAD/Document] Part Number","[Part/CAD/Document] Version","[Part/CAD/Document] Name",[CAD] Mass (kg),[CAD] Material +CAD,Mechanical/Part,Base,AA.01,Base,,Stainless Steel +CAD,Mechanical/Part,BasePin,AA.01,BasePin,,Stainless Steel +CAD,Mechanical/Part,Boom,AA.01,Boom,,Stainless Steel +CAD,Mechanical/Part,BoomCylinderInner,AA.01,BoomCylinderInner,,Stainless Steel +CAD,Mechanical/Part,BoomCylinderOuter,AA.01,BoomCylinderOuter,,Stainless Steel +CAD,Mechanical/Part,Stick,AA.01,Stick,,Stainless Steel +CAD,Mechanical/Part,StickCylinderInner,AA.01,StickCylinderInner,,Stainless Steel +CAD,Mechanical/Part,StickCylinderOuter,AA.01,StickCylinderOuter,,Stainless Steel +CAD,Mechanical/Part,Bucket,AA.01,Bucket,,Stainless Steel +CAD,Mechanical/Part,BucketCylinderInner,AA.01,BucketCylinderInner,,Stainless Steel +CAD,Mechanical/Part,BucketCylinderOuter,AA.01,BucketCylinderOuter,,Stainless Steel +CAD,Mechanical/Part,BucketLink1,AA.01,BucketLink1,,Stainless Steel +CAD,Mechanical/Part,BucketLink2,AA.01,BucketLink2,,Stainless Steel diff --git a/Detectors/CADSupport/examples/IRIS_MATERIALS.csv b/Detectors/CADSupport/examples/IRIS_MATERIALS.csv new file mode 100644 index 0000000000000..cc6266717fd44 --- /dev/null +++ b/Detectors/CADSupport/examples/IRIS_MATERIALS.csv @@ -0,0 +1,51 @@ +11,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +#REF!,,Bill Of Material Report,,,Part present in various sub assemblies,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,Part Number,"ST2487728, Rev: 1.01",,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,Name,IRIS 3 SECTORS ASSY. WIITH BEAM PIPE,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,CERN Drawing Reference,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,Date,03-03-2026,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,Extracted by,Pascal Jean Secouet (psecouet),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +Parts and CAD Documents,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +Type,"[CAD/Document] Type, Type","[Part/CAD/Document] Part Number, Document Number, Document Number","[Part/CAD/Document] Version, Version, Version","[Part/CAD/Document] Name, Definition, Title",[CAD] Mass (kg),[CAD] Material,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST0923290_01,AA.00,UHV GATE VALVE SERIES 108 DN63 CF,9.568453,Stainless Steel,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487458_01,AA.03,IRIS BELLOWS-3 SECTORS DECENTRE,0.21233,St. Steel EN 1.4306 (304L),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487195_01,AA.04,TRANSITION FOIL-3 SECTORS,0.0532305,Cu Be C17410 (TH02),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487462_01,AA.09,ACTUATOR LINK-2ND VACUUM-3 sectors-DECENTRE,4.71115,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1829909_01,AA.01,MD100HSMSL1X000Z,7.23448,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1782525_01,AA.04,BEAM PIPE C SIDE-DOUBLE VACUUM-VERSION 072023,1.51881,St. Steel EN 1.4306 (304L),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487461_01,AA.04,SMALL ROTATIVE RING-2ND VACUUM-3 sectors-DECENTRE,0.0105409,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487721_01,AA.02,CENTRAL PIPE-IRIS-2ND VACUUM-3 sectors-DECENTRE,0.313133,Carbon Fiber,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487459_01,AA.04,ROTATIVE RING-2ND VACUUM-3 sectors-DECENTRE,0.0155589,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487736_01,AA.07,EXTERNAL BP SECONDARY VACUUM-3 SECTORS-DECENTRE,7.23049,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1873216_01,AA.00,UHV GATE VALVE SERIES 108 DN100CF-CUSTOM,10.803705,Stainless Steel,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1A38526_01,AA.04,IRIS BASE-3 SECTORS-SYM,0.0721655,Alu EN AW-6082 (T6),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1A38495_01,AA.04,IRIS BASE-3 sectors,0.0891183,Alu EN AW-6082 (T6),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487455_01,AA.09,VACUUM VESSEL-SECONDARY VACUUM-3 sectors,7.27557,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2500409_01,AA.02,MICROCHANNEL-3 sectors,0.0896362,Carbon Fiber,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1A38494_01,AA.05,IRIS 1/3 SECTOR-CENTRAL PIPE,0.0608399,Alu EN AW-5083 (O-H111),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2487457_01,AA.03,EXTERNAL RF CONTACT FOR IRIS 3 SECTORS DECENTRE,0.0439901,Cu Be C17410 (TH02),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1A38476_01,AA.05,IRIS 1/3 SECTOR-END CAP 1,0.0176523,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2500394_01,AA.03,ITS4-CHIPS-3sectors,2.5503,Copper,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST1A38486_01,AA.06,IRIS 1/3 SECTOR-END CAP 2,0.0176523,Alu EN AW-5083 (H116),,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, +CAD,Mechanical/Part,ST2513437_01,AA.03,SILICON SENSORS IRIS TRACKER-B0-B1-B2,0.0204394,Silicium - Silicon,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, diff --git a/Detectors/CADSupport/examples/as1-oc-214.stp b/Detectors/CADSupport/examples/as1-oc-214.stp new file mode 100644 index 0000000000000..02c3ef244b24a --- /dev/null +++ b/Detectors/CADSupport/examples/as1-oc-214.stp @@ -0,0 +1,8378 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('CAx-IF test model AS1: Geometric Validation Properties'),'2;1'); +FILE_NAME('as1-oc-214.stp','2014-12-12T10:28:33',('abv'),( + 'Open CASCADE'),'Open CASCADE STEP processor 6.8','Open CASCADE 6.8 DRAW' + ,'Unknown'); +FILE_SCHEMA(('AUTOMOTIVE_DESIGN_CC2 { 1 2 10303 214 -1 1 5 4 }')); +ENDSEC; +DATA; +#1 = APPLICATION_PROTOCOL_DEFINITION('committee draft', + 'automotive_design',1997,#2); +#2 = APPLICATION_CONTEXT( + 'core data for automotive mechanical design processes'); +#3 = SHAPE_DEFINITION_REPRESENTATION(#4,#10); +#4 = PRODUCT_DEFINITION_SHAPE('','',#5); +#5 = PRODUCT_DEFINITION('design','',#6,#9); +#6 = PRODUCT_DEFINITION_FORMATION('','',#7); +#7 = PRODUCT('as1','as1','',(#8)); +#8 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#9 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#10 = SHAPE_REPRESENTATION('',(#11,#15,#19,#23,#27),#31); +#11 = AXIS2_PLACEMENT_3D('',#12,#13,#14); +#12 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#13 = DIRECTION('',(0.E+000,0.E+000,1.)); +#14 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#15 = AXIS2_PLACEMENT_3D('',#16,#17,#18); +#16 = CARTESIAN_POINT('',(-10.,75.,60.)); +#17 = DIRECTION('',(1.,-0.E+000,0.E+000)); +#18 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#19 = AXIS2_PLACEMENT_3D('',#20,#21,#22); +#20 = CARTESIAN_POINT('',(5.,125.,20.)); +#21 = DIRECTION('',(0.E+000,0.E+000,1.)); +#22 = DIRECTION('',(1.,0.E+000,0.E+000)); +#23 = AXIS2_PLACEMENT_3D('',#24,#25,#26); +#24 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#25 = DIRECTION('',(0.E+000,0.E+000,1.)); +#26 = DIRECTION('',(1.,0.E+000,0.E+000)); +#27 = AXIS2_PLACEMENT_3D('',#28,#29,#30); +#28 = CARTESIAN_POINT('',(175.,25.,20.)); +#29 = DIRECTION('',(0.E+000,0.E+000,1.)); +#30 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#31 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#35)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#32,#33,#34)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#32 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#33 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#34 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#35 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#32, + 'distance_accuracy_value','confusion accuracy'); +#36 = PRODUCT_TYPE('part',$,(#7)); +#37 = SHAPE_DEFINITION_REPRESENTATION(#38,#44); +#38 = PRODUCT_DEFINITION_SHAPE('','',#39); +#39 = PRODUCT_DEFINITION('design','',#40,#43); +#40 = PRODUCT_DEFINITION_FORMATION('','',#41); +#41 = PRODUCT('rod-assembly','rod-assembly','',(#42)); +#42 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#43 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#44 = SHAPE_REPRESENTATION('',(#11,#45,#49,#53),#57); +#45 = AXIS2_PLACEMENT_3D('',#46,#47,#48); +#46 = CARTESIAN_POINT('',(-10.,-7.5,185.)); +#47 = DIRECTION('',(0.E+000,0.E+000,1.)); +#48 = DIRECTION('',(1.,0.E+000,0.E+000)); +#49 = AXIS2_PLACEMENT_3D('',#50,#51,#52); +#50 = CARTESIAN_POINT('',(-10.,-7.5,12.)); +#51 = DIRECTION('',(0.E+000,0.E+000,1.)); +#52 = DIRECTION('',(1.,0.E+000,0.E+000)); +#53 = AXIS2_PLACEMENT_3D('',#54,#55,#56); +#54 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#55 = DIRECTION('',(0.E+000,0.E+000,1.)); +#56 = DIRECTION('',(1.,0.E+000,0.E+000)); +#57 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#61)) GLOBAL_UNIT_ASSIGNED_CONTEXT( +(#58,#59,#60)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#58 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#59 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#60 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#61 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#58, + 'distance_accuracy_value','confusion accuracy'); +#62 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#63),#735); +#63 = MANIFOLD_SOLID_BREP('',#64); +#64 = CLOSED_SHELL('',(#65,#423,#499,#548,#597,#624,#695,#724)); +#65 = ADVANCED_FACE('',(#66,#185),#80,.T.); +#66 = FACE_BOUND('',#67,.T.); +#67 = EDGE_LOOP('',(#68,#103,#131,#159)); +#68 = ORIENTED_EDGE('',*,*,#69,.F.); +#69 = EDGE_CURVE('',#70,#72,#74,.T.); +#70 = VERTEX_POINT('',#71); +#71 = CARTESIAN_POINT('',(20.,0.E+000,3.)); +#72 = VERTEX_POINT('',#73); +#73 = CARTESIAN_POINT('',(0.E+000,0.E+000,3.)); +#74 = SURFACE_CURVE('',#75,(#79,#91),.PCURVE_S1.); +#75 = LINE('',#76,#77); +#76 = CARTESIAN_POINT('',(10.,0.E+000,3.)); +#77 = VECTOR('',#78,1.); +#78 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#79 = PCURVE('',#80,#85); +#80 = PLANE('',#81); +#81 = AXIS2_PLACEMENT_3D('',#82,#83,#84); +#82 = CARTESIAN_POINT('',(10.,7.5,3.)); +#83 = DIRECTION('',(0.E+000,0.E+000,1.)); +#84 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#85 = DEFINITIONAL_REPRESENTATION('',(#86),#90); +#86 = LINE('',#87,#88); +#87 = CARTESIAN_POINT('',(0.E+000,-7.5)); +#88 = VECTOR('',#89,1.); +#89 = DIRECTION('',(-1.,0.E+000)); +#90 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#91 = PCURVE('',#92,#97); +#92 = PLANE('',#93); +#93 = AXIS2_PLACEMENT_3D('',#94,#95,#96); +#94 = CARTESIAN_POINT('',(10.,0.E+000,0.E+000)); +#95 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#96 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#97 = DEFINITIONAL_REPRESENTATION('',(#98),#102); +#98 = LINE('',#99,#100); +#99 = CARTESIAN_POINT('',(-3.,0.E+000)); +#100 = VECTOR('',#101,1.); +#101 = DIRECTION('',(0.E+000,-1.)); +#102 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#103 = ORIENTED_EDGE('',*,*,#104,.F.); +#104 = EDGE_CURVE('',#105,#70,#107,.T.); +#105 = VERTEX_POINT('',#106); +#106 = CARTESIAN_POINT('',(20.,15.,3.)); +#107 = SURFACE_CURVE('',#108,(#112,#119),.PCURVE_S1.); +#108 = LINE('',#109,#110); +#109 = CARTESIAN_POINT('',(20.,7.5,3.)); +#110 = VECTOR('',#111,1.); +#111 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#112 = PCURVE('',#80,#113); +#113 = DEFINITIONAL_REPRESENTATION('',(#114),#118); +#114 = LINE('',#115,#116); +#115 = CARTESIAN_POINT('',(10.,0.E+000)); +#116 = VECTOR('',#117,1.); +#117 = DIRECTION('',(0.E+000,-1.)); +#118 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#119 = PCURVE('',#120,#125); +#120 = PLANE('',#121); +#121 = AXIS2_PLACEMENT_3D('',#122,#123,#124); +#122 = CARTESIAN_POINT('',(20.,7.5,0.E+000)); +#123 = DIRECTION('',(1.,0.E+000,0.E+000)); +#124 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#125 = DEFINITIONAL_REPRESENTATION('',(#126),#130); +#126 = LINE('',#127,#128); +#127 = CARTESIAN_POINT('',(-3.,0.E+000)); +#128 = VECTOR('',#129,1.); +#129 = DIRECTION('',(0.E+000,-1.)); +#130 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#131 = ORIENTED_EDGE('',*,*,#132,.F.); +#132 = EDGE_CURVE('',#133,#105,#135,.T.); +#133 = VERTEX_POINT('',#134); +#134 = CARTESIAN_POINT('',(0.E+000,15.,3.)); +#135 = SURFACE_CURVE('',#136,(#140,#147),.PCURVE_S1.); +#136 = LINE('',#137,#138); +#137 = CARTESIAN_POINT('',(10.,15.,3.)); +#138 = VECTOR('',#139,1.); +#139 = DIRECTION('',(1.,0.E+000,0.E+000)); +#140 = PCURVE('',#80,#141); +#141 = DEFINITIONAL_REPRESENTATION('',(#142),#146); +#142 = LINE('',#143,#144); +#143 = CARTESIAN_POINT('',(0.E+000,7.5)); +#144 = VECTOR('',#145,1.); +#145 = DIRECTION('',(1.,0.E+000)); +#146 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#147 = PCURVE('',#148,#153); +#148 = PLANE('',#149); +#149 = AXIS2_PLACEMENT_3D('',#150,#151,#152); +#150 = CARTESIAN_POINT('',(10.,15.,0.E+000)); +#151 = DIRECTION('',(0.E+000,1.,0.E+000)); +#152 = DIRECTION('',(0.E+000,-0.E+000,1.)); +#153 = DEFINITIONAL_REPRESENTATION('',(#154),#158); +#154 = LINE('',#155,#156); +#155 = CARTESIAN_POINT('',(3.,0.E+000)); +#156 = VECTOR('',#157,1.); +#157 = DIRECTION('',(0.E+000,1.)); +#158 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#159 = ORIENTED_EDGE('',*,*,#160,.F.); +#160 = EDGE_CURVE('',#72,#133,#161,.T.); +#161 = SURFACE_CURVE('',#162,(#166,#173),.PCURVE_S1.); +#162 = LINE('',#163,#164); +#163 = CARTESIAN_POINT('',(0.E+000,7.5,3.)); +#164 = VECTOR('',#165,1.); +#165 = DIRECTION('',(0.E+000,1.,0.E+000)); +#166 = PCURVE('',#80,#167); +#167 = DEFINITIONAL_REPRESENTATION('',(#168),#172); +#168 = LINE('',#169,#170); +#169 = CARTESIAN_POINT('',(-10.,0.E+000)); +#170 = VECTOR('',#171,1.); +#171 = DIRECTION('',(0.E+000,1.)); +#172 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#173 = PCURVE('',#174,#179); +#174 = PLANE('',#175); +#175 = AXIS2_PLACEMENT_3D('',#176,#177,#178); +#176 = CARTESIAN_POINT('',(0.E+000,7.5,0.E+000)); +#177 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#178 = DIRECTION('',(0.E+000,0.E+000,1.)); +#179 = DEFINITIONAL_REPRESENTATION('',(#180),#184); +#180 = LINE('',#181,#182); +#181 = CARTESIAN_POINT('',(3.,0.E+000)); +#182 = VECTOR('',#183,1.); +#183 = DIRECTION('',(0.E+000,1.)); +#184 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#185 = FACE_BOUND('',#186,.T.); +#186 = EDGE_LOOP('',(#187,#307)); +#187 = ORIENTED_EDGE('',*,*,#188,.T.); +#188 = EDGE_CURVE('',#189,#191,#193,.T.); +#189 = VERTEX_POINT('',#190); +#190 = CARTESIAN_POINT('',(5.,7.5,3.)); +#191 = VERTEX_POINT('',#192); +#192 = CARTESIAN_POINT('',(15.,7.5,3.)); +#193 = SURFACE_CURVE('',#194,(#219,#247),.PCURVE_S1.); +#194 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#195,#196,#197,#198,#199,#200, + #201,#202,#203,#204,#205,#206,#207,#208,#209,#210,#211,#212,#213, + #214,#215,#216,#217,#218),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164414,7.85828164644,10.7238180516,13.583658994, + 16.4911855022,20.3877608702,22.3658107336),.UNSPECIFIED.); +#195 = CARTESIAN_POINT('',(5.,7.5,3.)); +#196 = CARTESIAN_POINT('',(5.,7.96719825234,3.)); +#197 = CARTESIAN_POINT('',(5.05456967986,8.46798546394,3.)); +#198 = CARTESIAN_POINT('',(5.17958225879,8.9911230353,3.)); +#199 = CARTESIAN_POINT('',(5.57268612552,9.98006143429,3.)); +#200 = CARTESIAN_POINT('',(6.25801463611,10.8809047397,3.)); +#201 = CARTESIAN_POINT('',(6.64523619345,11.2686263331,3.)); +#202 = CARTESIAN_POINT('',(7.43250862613,11.8620880289,3.)); +#203 = CARTESIAN_POINT('',(8.35481073757,12.2518403653,3.)); +#204 = CARTESIAN_POINT('',(8.77677855674,12.3779193361,3.)); +#205 = CARTESIAN_POINT('',(9.64371296306,12.5354809914,3.)); +#206 = CARTESIAN_POINT('',(10.5264003018,12.501400762,3.)); +#207 = CARTESIAN_POINT('',(10.9630506746,12.435748566,3.)); +#208 = CARTESIAN_POINT('',(11.8186421203,12.2088457881,3.)); +#209 = CARTESIAN_POINT('',(12.5957546194,11.8071306708,3.)); +#210 = CARTESIAN_POINT('',(12.9603131848,11.5642190824,3.)); +#211 = CARTESIAN_POINT('',(13.7355490363,10.916301294,3.)); +#212 = CARTESIAN_POINT('',(14.3095225983,10.1246556547,3.)); +#213 = CARTESIAN_POINT('',(14.5637500219,9.64244819984,3.)); +#214 = CARTESIAN_POINT('',(14.8362924347,8.90481893489,3.)); +#215 = CARTESIAN_POINT('',(14.96121877,8.18885510165,3.)); +#216 = CARTESIAN_POINT('',(14.9876332288,7.95243137655,3.)); +#217 = CARTESIAN_POINT('',(15.,7.72240966553,3.)); +#218 = CARTESIAN_POINT('',(15.,7.5,3.)); +#219 = PCURVE('',#80,#220); +#220 = DEFINITIONAL_REPRESENTATION('',(#221),#246); +#221 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#222,#223,#224,#225,#226,#227, + #228,#229,#230,#231,#232,#233,#234,#235,#236,#237,#238,#239,#240, + #241,#242,#243,#244,#245),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164414,7.85828164644,10.7238180516,13.583658994, + 16.4911855022,20.3877608702,22.3658107336),.UNSPECIFIED.); +#222 = CARTESIAN_POINT('',(-5.,0.E+000)); +#223 = CARTESIAN_POINT('',(-5.,0.46719825234)); +#224 = CARTESIAN_POINT('',(-4.94543032014,0.96798546394)); +#225 = CARTESIAN_POINT('',(-4.82041774121,1.4911230353)); +#226 = CARTESIAN_POINT('',(-4.42731387448,2.48006143429)); +#227 = CARTESIAN_POINT('',(-3.74198536389,3.3809047397)); +#228 = CARTESIAN_POINT('',(-3.35476380655,3.7686263331)); +#229 = CARTESIAN_POINT('',(-2.56749137387,4.3620880289)); +#230 = CARTESIAN_POINT('',(-1.64518926243,4.7518403653)); +#231 = CARTESIAN_POINT('',(-1.22322144326,4.8779193361)); +#232 = CARTESIAN_POINT('',(-0.35628703694,5.0354809914)); +#233 = CARTESIAN_POINT('',(0.5264003018,5.001400762)); +#234 = CARTESIAN_POINT('',(0.9630506746,4.935748566)); +#235 = CARTESIAN_POINT('',(1.8186421203,4.7088457881)); +#236 = CARTESIAN_POINT('',(2.5957546194,4.3071306708)); +#237 = CARTESIAN_POINT('',(2.9603131848,4.0642190824)); +#238 = CARTESIAN_POINT('',(3.7355490363,3.416301294)); +#239 = CARTESIAN_POINT('',(4.3095225983,2.6246556547)); +#240 = CARTESIAN_POINT('',(4.5637500219,2.14244819984)); +#241 = CARTESIAN_POINT('',(4.8362924347,1.40481893489)); +#242 = CARTESIAN_POINT('',(4.96121877,0.68885510165)); +#243 = CARTESIAN_POINT('',(4.9876332288,0.45243137655)); +#244 = CARTESIAN_POINT('',(5.,0.22240966553)); +#245 = CARTESIAN_POINT('',(5.,0.E+000)); +#246 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#247 = PCURVE('',#248,#257); +#248 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#249,#250,#251,#252) + ,(#253,#254,#255,#256 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,3.00099800399),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#249 = CARTESIAN_POINT('',(5.,7.5,3.)); +#250 = CARTESIAN_POINT('',(5.,17.5,3.)); +#251 = CARTESIAN_POINT('',(15.,17.5,3.)); +#252 = CARTESIAN_POINT('',(15.,7.5,3.)); +#253 = CARTESIAN_POINT('',(5.,7.5,0.E+000)); +#254 = CARTESIAN_POINT('',(5.,17.5,0.E+000)); +#255 = CARTESIAN_POINT('',(15.,17.5,0.E+000)); +#256 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#257 = DEFINITIONAL_REPRESENTATION('',(#258),#306); +#258 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#259,#260,#261,#262,#263,#264, + #265,#266,#267,#268,#269,#270,#271,#272,#273,#274,#275,#276,#277, + #278,#279,#280,#281,#282,#283,#284,#285,#286,#287,#288,#289,#290, + #291,#292,#293,#294,#295,#296,#297,#298,#299,#300,#301,#302,#303, + #304,#305),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000, + 0.508313880309,1.016627760618,1.524941640927,2.033255521236, + 2.541569401545,3.049883281855,3.558197162164,4.066511042473, + 4.574824922782,5.083138803091,5.5914526834,6.099766563709, + 6.608080444018,7.116394324327,7.624708204636,8.133022084945, + 8.641335965255,9.149649845564,9.657963725873,10.166277606182, + 10.674591486491,11.1829053668,11.691219247109,12.199533127418, + 12.707847007727,13.216160888036,13.724474768345,14.232788648655, + 14.741102528964,15.249416409273,15.757730289582,16.266044169891, + 16.7743580502,17.282671930509,17.790985810818,18.299299691127, + 18.807613571436,19.315927451745,19.824241332055,20.332555212364, + 20.840869092673,21.349182972982,21.857496853291,22.3658107336), + .QUASI_UNIFORM_KNOTS.); +#259 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#260 = CARTESIAN_POINT('',(9.9800399E-004,0.285786133984)); +#261 = CARTESIAN_POINT('',(9.9800399E-004,0.851023724305)); +#262 = CARTESIAN_POINT('',(9.9800399E-004,1.679658949906)); +#263 = CARTESIAN_POINT('',(9.980039899999E-004,2.488775842698)); +#264 = CARTESIAN_POINT('',(9.980039900006E-004,3.278357390721)); +#265 = CARTESIAN_POINT('',(9.980039900005E-004,4.048590090071)); +#266 = CARTESIAN_POINT('',(9.9800399E-004,4.799873550567)); +#267 = CARTESIAN_POINT('',(9.980039899996E-004,5.532780975437)); +#268 = CARTESIAN_POINT('',(9.980039900015E-004,6.248020910349)); +#269 = CARTESIAN_POINT('',(9.980039899997E-004,6.946360574083)); +#270 = CARTESIAN_POINT('',(9.9800399E-004,7.628688634712)); +#271 = CARTESIAN_POINT('',(9.980039900006E-004,8.296073973071)); +#272 = CARTESIAN_POINT('',(9.980039900004E-004,8.949683944662)); +#273 = CARTESIAN_POINT('',(9.980039900006E-004,9.590744782664)); +#274 = CARTESIAN_POINT('',(9.980039899999E-004,10.220499188568)); +#275 = CARTESIAN_POINT('',(9.980039900001E-004,10.840182523672)); +#276 = CARTESIAN_POINT('',(9.9800399E-004,11.450961995018)); +#277 = CARTESIAN_POINT('',(9.980039900003E-004,12.054057835882)); +#278 = CARTESIAN_POINT('',(9.980039899991E-004,12.650784954516)); +#279 = CARTESIAN_POINT('',(9.98003990001E-004,13.242437006153)); +#280 = CARTESIAN_POINT('',(9.980039899998E-004,13.830311318193)); +#281 = CARTESIAN_POINT('',(9.980039900001E-004,14.415700441563)); +#282 = CARTESIAN_POINT('',(9.980039900002E-004,14.999897614205)); +#283 = CARTESIAN_POINT('',(9.980039899993E-004,15.584089012766)); +#284 = CARTESIAN_POINT('',(9.980039900001E-004,16.169496122547)); +#285 = CARTESIAN_POINT('',(9.980039900007E-004,16.757374012694)); +#286 = CARTESIAN_POINT('',(9.980039900001E-004,17.349001918787)); +#287 = CARTESIAN_POINT('',(9.980039899992E-004,17.945677527815)); +#288 = CARTESIAN_POINT('',(9.980039900009E-004,18.54871222184)); +#289 = CARTESIAN_POINT('',(9.980039900002E-004,19.159406297875)); +#290 = CARTESIAN_POINT('',(9.980039900015E-004,19.779034542658)); +#291 = CARTESIAN_POINT('',(9.980039899996E-004,20.408844113292)); +#292 = CARTESIAN_POINT('',(9.980039900003E-004,21.050050717178)); +#293 = CARTESIAN_POINT('',(9.980039899995E-004,21.703821241748)); +#294 = CARTESIAN_POINT('',(9.980039899995E-004,22.371286808828)); +#295 = CARTESIAN_POINT('',(9.980039900003E-004,23.053580533636)); +#296 = CARTESIAN_POINT('',(9.980039899995E-004,23.751780889668)); +#297 = CARTESIAN_POINT('',(9.980039899993E-004,24.466876468307)); +#298 = CARTESIAN_POINT('',(9.980039900012E-004,25.199732652869)); +#299 = CARTESIAN_POINT('',(9.980039899991E-004,25.951064418362)); +#300 = CARTESIAN_POINT('',(9.980039900002E-004,26.721413686029)); +#301 = CARTESIAN_POINT('',(9.980039900004E-004,27.511129454125)); +#302 = CARTESIAN_POINT('',(9.980039899986E-004,28.320321954363)); +#303 = CARTESIAN_POINT('',(9.980039900003E-004,29.148977248214)); +#304 = CARTESIAN_POINT('',(9.980039900005E-004,29.714213803107)); +#305 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#306 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#307 = ORIENTED_EDGE('',*,*,#308,.T.); +#308 = EDGE_CURVE('',#191,#189,#309,.T.); +#309 = SURFACE_CURVE('',#310,(#335,#363),.PCURVE_S1.); +#310 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#311,#312,#313,#314,#315,#316, + #317,#318,#319,#320,#321,#322,#323,#324,#325,#326,#327,#328,#329, + #330,#331,#332,#333,#334),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164517,7.85828164968,10.7238180555,13.5836589972, + 16.4911855043,20.3877608712,22.3658107337),.UNSPECIFIED.); +#311 = CARTESIAN_POINT('',(15.,7.5,3.)); +#312 = CARTESIAN_POINT('',(15.,7.03280174754,3.)); +#313 = CARTESIAN_POINT('',(14.9454303202,6.53201453581,3.)); +#314 = CARTESIAN_POINT('',(14.8204177413,6.00887696498,3.)); +#315 = CARTESIAN_POINT('',(14.4273138745,5.01993856555,3.)); +#316 = CARTESIAN_POINT('',(13.7419853635,4.11909525976,3.)); +#317 = CARTESIAN_POINT('',(13.3547638071,3.73137366727,3.)); +#318 = CARTESIAN_POINT('',(12.5674913741,3.13791197119,3.)); +#319 = CARTESIAN_POINT('',(11.6451892622,2.74815963462,3.)); +#320 = CARTESIAN_POINT('',(11.2232214435,2.62208066399,3.)); +#321 = CARTESIAN_POINT('',(10.3562870372,2.46451900862,3.)); +#322 = CARTESIAN_POINT('',(9.47359969847,2.49859923799,3.)); +#323 = CARTESIAN_POINT('',(9.03694932519,2.56425143411,3.)); +#324 = CARTESIAN_POINT('',(8.18135787977,2.79115421194,3.)); +#325 = CARTESIAN_POINT('',(7.40424538089,3.19286932902,3.)); +#326 = CARTESIAN_POINT('',(7.03968681504,3.43578091778,3.)); +#327 = CARTESIAN_POINT('',(6.26445096378,4.08369870599,3.)); +#328 = CARTESIAN_POINT('',(5.69047740185,4.87534434499,3.)); +#329 = CARTESIAN_POINT('',(5.43624997802,5.35755180047,3.)); +#330 = CARTESIAN_POINT('',(5.1637075653,6.09518106513,3.)); +#331 = CARTESIAN_POINT('',(5.03878123004,6.81114489813,3.)); +#332 = CARTESIAN_POINT('',(5.01236677119,7.04756862366,3.)); +#333 = CARTESIAN_POINT('',(5.,7.27759033457,3.)); +#334 = CARTESIAN_POINT('',(5.,7.5,3.)); +#335 = PCURVE('',#80,#336); +#336 = DEFINITIONAL_REPRESENTATION('',(#337),#362); +#337 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#338,#339,#340,#341,#342,#343, + #344,#345,#346,#347,#348,#349,#350,#351,#352,#353,#354,#355,#356, + #357,#358,#359,#360,#361),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164517,7.85828164968,10.7238180555,13.5836589972, + 16.4911855043,20.3877608712,22.3658107337),.UNSPECIFIED.); +#338 = CARTESIAN_POINT('',(5.,0.E+000)); +#339 = CARTESIAN_POINT('',(5.,-0.46719825246)); +#340 = CARTESIAN_POINT('',(4.9454303202,-0.96798546419)); +#341 = CARTESIAN_POINT('',(4.8204177413,-1.49112303502)); +#342 = CARTESIAN_POINT('',(4.4273138745,-2.48006143445)); +#343 = CARTESIAN_POINT('',(3.7419853635,-3.38090474024)); +#344 = CARTESIAN_POINT('',(3.3547638071,-3.76862633273)); +#345 = CARTESIAN_POINT('',(2.5674913741,-4.36208802881)); +#346 = CARTESIAN_POINT('',(1.6451892622,-4.75184036538)); +#347 = CARTESIAN_POINT('',(1.2232214435,-4.87791933601)); +#348 = CARTESIAN_POINT('',(0.3562870372,-5.03548099138)); +#349 = CARTESIAN_POINT('',(-0.52640030153,-5.00140076201)); +#350 = CARTESIAN_POINT('',(-0.96305067481,-4.93574856589)); +#351 = CARTESIAN_POINT('',(-1.81864212023,-4.70884578806)); +#352 = CARTESIAN_POINT('',(-2.59575461911,-4.30713067098)); +#353 = CARTESIAN_POINT('',(-2.96031318496,-4.06421908222)); +#354 = CARTESIAN_POINT('',(-3.73554903622,-3.41630129401)); +#355 = CARTESIAN_POINT('',(-4.30952259815,-2.62465565501)); +#356 = CARTESIAN_POINT('',(-4.56375002198,-2.14244819953)); +#357 = CARTESIAN_POINT('',(-4.8362924347,-1.40481893487)); +#358 = CARTESIAN_POINT('',(-4.96121876996,-0.68885510187)); +#359 = CARTESIAN_POINT('',(-4.98763322881,-0.45243137634)); +#360 = CARTESIAN_POINT('',(-5.,-0.22240966543)); +#361 = CARTESIAN_POINT('',(-5.,0.E+000)); +#362 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#363 = PCURVE('',#364,#373); +#364 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#365,#366,#367,#368) + ,(#369,#370,#371,#372 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,3.00099800399),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#365 = CARTESIAN_POINT('',(15.,7.5,3.)); +#366 = CARTESIAN_POINT('',(15.,-2.5,3.)); +#367 = CARTESIAN_POINT('',(5.,-2.5,3.)); +#368 = CARTESIAN_POINT('',(5.,7.5,3.)); +#369 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#370 = CARTESIAN_POINT('',(15.,-2.5,0.E+000)); +#371 = CARTESIAN_POINT('',(5.,-2.5,0.E+000)); +#372 = CARTESIAN_POINT('',(5.,7.5,0.E+000)); +#373 = DEFINITIONAL_REPRESENTATION('',(#374),#422); +#374 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#375,#376,#377,#378,#379,#380, + #381,#382,#383,#384,#385,#386,#387,#388,#389,#390,#391,#392,#393, + #394,#395,#396,#397,#398,#399,#400,#401,#402,#403,#404,#405,#406, + #407,#408,#409,#410,#411,#412,#413,#414,#415,#416,#417,#418,#419, + #420,#421),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000, + 0.508313880311,1.016627760623,1.524941640934,2.033255521245, + 2.541569401557,3.049883281868,3.55819716218,4.066511042491, + 4.574824922802,5.083138803114,5.591452683425,6.099766563736, + 6.608080444048,7.116394324359,7.62470820467,8.133022084982, + 8.641335965293,9.149649845605,9.657963725916,10.166277606227, + 10.674591486539,11.18290536685,11.691219247161,12.199533127473, + 12.707847007784,13.216160888095,13.724474768407,14.232788648718, + 14.74110252903,15.249416409341,15.757730289652,16.266044169964, + 16.774358050275,17.282671930586,17.790985810898,18.299299691209, + 18.80761357152,19.315927451832,19.824241332143,20.332555212455, + 20.840869092766,21.349182973077,21.857496853389,22.3658107337), + .QUASI_UNIFORM_KNOTS.); +#375 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#376 = CARTESIAN_POINT('',(9.980039900002E-004,0.285786133999)); +#377 = CARTESIAN_POINT('',(9.980039900001E-004,0.851023724304)); +#378 = CARTESIAN_POINT('',(9.980039899993E-004,1.679658949757)); +#379 = CARTESIAN_POINT('',(9.980039900002E-004,2.48877584225)); +#380 = CARTESIAN_POINT('',(9.980039899999E-004,3.278357389909)); +#381 = CARTESIAN_POINT('',(9.9800399E-004,4.048590088927)); +#382 = CARTESIAN_POINT('',(9.9800399E-004,4.799873549198)); +#383 = CARTESIAN_POINT('',(9.9800399E-004,5.532780973984)); +#384 = CARTESIAN_POINT('',(9.9800399E-004,6.248020908926)); +#385 = CARTESIAN_POINT('',(9.9800399E-004,6.946360572727)); +#386 = CARTESIAN_POINT('',(9.980039899998E-004,7.628688633133)); +#387 = CARTESIAN_POINT('',(9.980039900008E-004,8.296073970944)); +#388 = CARTESIAN_POINT('',(9.980039899996E-004,8.949683941827)); +#389 = CARTESIAN_POINT('',(9.980039900005E-004,9.590744779194)); +#390 = CARTESIAN_POINT('',(9.980039900008E-004,10.220499184724)); +#391 = CARTESIAN_POINT('',(9.980039899988E-004,10.840182519777)); +#392 = CARTESIAN_POINT('',(9.98003990001E-004,11.450961991235)); +#393 = CARTESIAN_POINT('',(9.980039899995E-004,12.054057832055)); +#394 = CARTESIAN_POINT('',(9.980039900008E-004,12.650784950465)); +#395 = CARTESIAN_POINT('',(9.980039899998E-004,13.242437001825)); +#396 = CARTESIAN_POINT('',(9.980039899997E-004,13.830311313687)); +#397 = CARTESIAN_POINT('',(9.980039900009E-004,14.415700437053)); +#398 = CARTESIAN_POINT('',(9.980039899989E-004,14.999897609704)); +#399 = CARTESIAN_POINT('',(9.980039900004E-004,15.584089008431)); +#400 = CARTESIAN_POINT('',(9.980039899991E-004,16.169496118509)); +#401 = CARTESIAN_POINT('',(9.980039900002E-004,16.757374008936)); +#402 = CARTESIAN_POINT('',(9.980039899996E-004,17.349001915149)); +#403 = CARTESIAN_POINT('',(9.980039900008E-004,17.945677524114)); +#404 = CARTESIAN_POINT('',(9.980039899991E-004,18.548712218151)); +#405 = CARTESIAN_POINT('',(9.980039899994E-004,19.159406294427)); +#406 = CARTESIAN_POINT('',(9.9800399E-004,19.779034539582)); +#407 = CARTESIAN_POINT('',(9.980039899999E-004,20.40884411053)); +#408 = CARTESIAN_POINT('',(9.980039899998E-004,21.050050714504)); +#409 = CARTESIAN_POINT('',(9.980039900001E-004,21.703821239013)); +#410 = CARTESIAN_POINT('',(9.98003989999E-004,22.371286806128)); +#411 = CARTESIAN_POINT('',(9.980039900005E-004,23.053580531118)); +#412 = CARTESIAN_POINT('',(9.980039899983E-004,23.751780887468)); +#413 = CARTESIAN_POINT('',(9.980039900003E-004,24.46687646648)); +#414 = CARTESIAN_POINT('',(9.980039899998E-004,25.199732651355)); +#415 = CARTESIAN_POINT('',(9.980039899995E-004,25.951064417007)); +#416 = CARTESIAN_POINT('',(9.980039899985E-004,26.721413684648)); +#417 = CARTESIAN_POINT('',(9.980039900002E-004,27.511129452701)); +#418 = CARTESIAN_POINT('',(9.980039899999E-004,28.320321953565)); +#419 = CARTESIAN_POINT('',(9.980039899992E-004,29.148977248108)); +#420 = CARTESIAN_POINT('',(9.980039899995E-004,29.714213803178)); +#421 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#422 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#423 = ADVANCED_FACE('',(#424),#92,.T.); +#424 = FACE_BOUND('',#425,.T.); +#425 = EDGE_LOOP('',(#426,#449,#450,#473)); +#426 = ORIENTED_EDGE('',*,*,#427,.T.); +#427 = EDGE_CURVE('',#428,#70,#430,.T.); +#428 = VERTEX_POINT('',#429); +#429 = CARTESIAN_POINT('',(20.,0.E+000,0.E+000)); +#430 = SURFACE_CURVE('',#431,(#435,#442),.PCURVE_S1.); +#431 = LINE('',#432,#433); +#432 = CARTESIAN_POINT('',(20.,0.E+000,1.5)); +#433 = VECTOR('',#434,1.); +#434 = DIRECTION('',(0.E+000,0.E+000,1.)); +#435 = PCURVE('',#92,#436); +#436 = DEFINITIONAL_REPRESENTATION('',(#437),#441); +#437 = LINE('',#438,#439); +#438 = CARTESIAN_POINT('',(-1.5,10.)); +#439 = VECTOR('',#440,1.); +#440 = DIRECTION('',(-1.,0.E+000)); +#441 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#442 = PCURVE('',#120,#443); +#443 = DEFINITIONAL_REPRESENTATION('',(#444),#448); +#444 = LINE('',#445,#446); +#445 = CARTESIAN_POINT('',(-1.5,-7.5)); +#446 = VECTOR('',#447,1.); +#447 = DIRECTION('',(-1.,0.E+000)); +#448 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#449 = ORIENTED_EDGE('',*,*,#69,.T.); +#450 = ORIENTED_EDGE('',*,*,#451,.F.); +#451 = EDGE_CURVE('',#452,#72,#454,.T.); +#452 = VERTEX_POINT('',#453); +#453 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#454 = SURFACE_CURVE('',#455,(#459,#466),.PCURVE_S1.); +#455 = LINE('',#456,#457); +#456 = CARTESIAN_POINT('',(0.E+000,0.E+000,1.5)); +#457 = VECTOR('',#458,1.); +#458 = DIRECTION('',(0.E+000,0.E+000,1.)); +#459 = PCURVE('',#92,#460); +#460 = DEFINITIONAL_REPRESENTATION('',(#461),#465); +#461 = LINE('',#462,#463); +#462 = CARTESIAN_POINT('',(-1.5,-10.)); +#463 = VECTOR('',#464,1.); +#464 = DIRECTION('',(-1.,0.E+000)); +#465 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#466 = PCURVE('',#174,#467); +#467 = DEFINITIONAL_REPRESENTATION('',(#468),#472); +#468 = LINE('',#469,#470); +#469 = CARTESIAN_POINT('',(1.5,-7.5)); +#470 = VECTOR('',#471,1.); +#471 = DIRECTION('',(1.,0.E+000)); +#472 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#473 = ORIENTED_EDGE('',*,*,#474,.T.); +#474 = EDGE_CURVE('',#452,#428,#475,.T.); +#475 = SURFACE_CURVE('',#476,(#480,#487),.PCURVE_S1.); +#476 = LINE('',#477,#478); +#477 = CARTESIAN_POINT('',(10.,0.E+000,0.E+000)); +#478 = VECTOR('',#479,1.); +#479 = DIRECTION('',(1.,0.E+000,0.E+000)); +#480 = PCURVE('',#92,#481); +#481 = DEFINITIONAL_REPRESENTATION('',(#482),#486); +#482 = LINE('',#483,#484); +#483 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#484 = VECTOR('',#485,1.); +#485 = DIRECTION('',(0.E+000,1.)); +#486 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#487 = PCURVE('',#488,#493); +#488 = PLANE('',#489); +#489 = AXIS2_PLACEMENT_3D('',#490,#491,#492); +#490 = CARTESIAN_POINT('',(10.,7.5,0.E+000)); +#491 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#492 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#493 = DEFINITIONAL_REPRESENTATION('',(#494),#498); +#494 = LINE('',#495,#496); +#495 = CARTESIAN_POINT('',(0.E+000,-7.5)); +#496 = VECTOR('',#497,1.); +#497 = DIRECTION('',(-1.,0.E+000)); +#498 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#499 = ADVANCED_FACE('',(#500),#120,.T.); +#500 = FACE_BOUND('',#501,.T.); +#501 = EDGE_LOOP('',(#502,#525,#546,#547)); +#502 = ORIENTED_EDGE('',*,*,#503,.T.); +#503 = EDGE_CURVE('',#428,#504,#506,.T.); +#504 = VERTEX_POINT('',#505); +#505 = CARTESIAN_POINT('',(20.,15.,0.E+000)); +#506 = SURFACE_CURVE('',#507,(#511,#518),.PCURVE_S1.); +#507 = LINE('',#508,#509); +#508 = CARTESIAN_POINT('',(20.,7.5,0.E+000)); +#509 = VECTOR('',#510,1.); +#510 = DIRECTION('',(0.E+000,1.,0.E+000)); +#511 = PCURVE('',#120,#512); +#512 = DEFINITIONAL_REPRESENTATION('',(#513),#517); +#513 = LINE('',#514,#515); +#514 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#515 = VECTOR('',#516,1.); +#516 = DIRECTION('',(0.E+000,1.)); +#517 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#518 = PCURVE('',#488,#519); +#519 = DEFINITIONAL_REPRESENTATION('',(#520),#524); +#520 = LINE('',#521,#522); +#521 = CARTESIAN_POINT('',(-10.,0.E+000)); +#522 = VECTOR('',#523,1.); +#523 = DIRECTION('',(0.E+000,1.)); +#524 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#525 = ORIENTED_EDGE('',*,*,#526,.T.); +#526 = EDGE_CURVE('',#504,#105,#527,.T.); +#527 = SURFACE_CURVE('',#528,(#532,#539),.PCURVE_S1.); +#528 = LINE('',#529,#530); +#529 = CARTESIAN_POINT('',(20.,15.,1.5)); +#530 = VECTOR('',#531,1.); +#531 = DIRECTION('',(0.E+000,0.E+000,1.)); +#532 = PCURVE('',#120,#533); +#533 = DEFINITIONAL_REPRESENTATION('',(#534),#538); +#534 = LINE('',#535,#536); +#535 = CARTESIAN_POINT('',(-1.5,7.5)); +#536 = VECTOR('',#537,1.); +#537 = DIRECTION('',(-1.,0.E+000)); +#538 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#539 = PCURVE('',#148,#540); +#540 = DEFINITIONAL_REPRESENTATION('',(#541),#545); +#541 = LINE('',#542,#543); +#542 = CARTESIAN_POINT('',(1.5,10.)); +#543 = VECTOR('',#544,1.); +#544 = DIRECTION('',(1.,0.E+000)); +#545 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#546 = ORIENTED_EDGE('',*,*,#104,.T.); +#547 = ORIENTED_EDGE('',*,*,#427,.F.); +#548 = ADVANCED_FACE('',(#549),#148,.T.); +#549 = FACE_BOUND('',#550,.T.); +#550 = EDGE_LOOP('',(#551,#574,#575,#576)); +#551 = ORIENTED_EDGE('',*,*,#552,.T.); +#552 = EDGE_CURVE('',#553,#133,#555,.T.); +#553 = VERTEX_POINT('',#554); +#554 = CARTESIAN_POINT('',(0.E+000,15.,0.E+000)); +#555 = SURFACE_CURVE('',#556,(#560,#567),.PCURVE_S1.); +#556 = LINE('',#557,#558); +#557 = CARTESIAN_POINT('',(0.E+000,15.,1.5)); +#558 = VECTOR('',#559,1.); +#559 = DIRECTION('',(0.E+000,0.E+000,1.)); +#560 = PCURVE('',#148,#561); +#561 = DEFINITIONAL_REPRESENTATION('',(#562),#566); +#562 = LINE('',#563,#564); +#563 = CARTESIAN_POINT('',(1.5,-10.)); +#564 = VECTOR('',#565,1.); +#565 = DIRECTION('',(1.,0.E+000)); +#566 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#567 = PCURVE('',#174,#568); +#568 = DEFINITIONAL_REPRESENTATION('',(#569),#573); +#569 = LINE('',#570,#571); +#570 = CARTESIAN_POINT('',(1.5,7.5)); +#571 = VECTOR('',#572,1.); +#572 = DIRECTION('',(1.,0.E+000)); +#573 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#574 = ORIENTED_EDGE('',*,*,#132,.T.); +#575 = ORIENTED_EDGE('',*,*,#526,.F.); +#576 = ORIENTED_EDGE('',*,*,#577,.T.); +#577 = EDGE_CURVE('',#504,#553,#578,.T.); +#578 = SURFACE_CURVE('',#579,(#583,#590),.PCURVE_S1.); +#579 = LINE('',#580,#581); +#580 = CARTESIAN_POINT('',(10.,15.,0.E+000)); +#581 = VECTOR('',#582,1.); +#582 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#583 = PCURVE('',#148,#584); +#584 = DEFINITIONAL_REPRESENTATION('',(#585),#589); +#585 = LINE('',#586,#587); +#586 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#587 = VECTOR('',#588,1.); +#588 = DIRECTION('',(0.E+000,-1.)); +#589 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#590 = PCURVE('',#488,#591); +#591 = DEFINITIONAL_REPRESENTATION('',(#592),#596); +#592 = LINE('',#593,#594); +#593 = CARTESIAN_POINT('',(0.E+000,7.5)); +#594 = VECTOR('',#595,1.); +#595 = DIRECTION('',(1.,0.E+000)); +#596 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#597 = ADVANCED_FACE('',(#598),#174,.T.); +#598 = FACE_BOUND('',#599,.T.); +#599 = EDGE_LOOP('',(#600,#601,#602,#603)); +#600 = ORIENTED_EDGE('',*,*,#451,.T.); +#601 = ORIENTED_EDGE('',*,*,#160,.T.); +#602 = ORIENTED_EDGE('',*,*,#552,.F.); +#603 = ORIENTED_EDGE('',*,*,#604,.T.); +#604 = EDGE_CURVE('',#553,#452,#605,.T.); +#605 = SURFACE_CURVE('',#606,(#610,#617),.PCURVE_S1.); +#606 = LINE('',#607,#608); +#607 = CARTESIAN_POINT('',(0.E+000,7.5,0.E+000)); +#608 = VECTOR('',#609,1.); +#609 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#610 = PCURVE('',#174,#611); +#611 = DEFINITIONAL_REPRESENTATION('',(#612),#616); +#612 = LINE('',#613,#614); +#613 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#614 = VECTOR('',#615,1.); +#615 = DIRECTION('',(0.E+000,-1.)); +#616 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#617 = PCURVE('',#488,#618); +#618 = DEFINITIONAL_REPRESENTATION('',(#619),#623); +#619 = LINE('',#620,#621); +#620 = CARTESIAN_POINT('',(10.,0.E+000)); +#621 = VECTOR('',#622,1.); +#622 = DIRECTION('',(0.E+000,-1.)); +#623 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#624 = ADVANCED_FACE('',(#625),#248,.T.); +#625 = FACE_BOUND('',#626,.T.); +#626 = EDGE_LOOP('',(#627,#654,#674,#675)); +#627 = ORIENTED_EDGE('',*,*,#628,.T.); +#628 = EDGE_CURVE('',#629,#631,#633,.T.); +#629 = VERTEX_POINT('',#630); +#630 = CARTESIAN_POINT('',(5.,7.5,2.22044604925E-016)); +#631 = VERTEX_POINT('',#632); +#632 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#633 = SURFACE_CURVE('',#634,(#639,#646),.PCURVE_S1.); +#634 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#635,#636,#637,#638), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#635 = CARTESIAN_POINT('',(5.,7.5,0.E+000)); +#636 = CARTESIAN_POINT('',(5.,17.5,0.E+000)); +#637 = CARTESIAN_POINT('',(15.,17.5,0.E+000)); +#638 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#639 = PCURVE('',#248,#640); +#640 = DEFINITIONAL_REPRESENTATION('',(#641),#645); +#641 = LINE('',#642,#643); +#642 = CARTESIAN_POINT('',(3.00099800399,0.E+000)); +#643 = VECTOR('',#644,1.); +#644 = DIRECTION('',(0.E+000,1.)); +#645 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#646 = PCURVE('',#488,#647); +#647 = DEFINITIONAL_REPRESENTATION('',(#648),#653); +#648 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#649,#650,#651,#652), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#649 = CARTESIAN_POINT('',(5.,0.E+000)); +#650 = CARTESIAN_POINT('',(5.,10.)); +#651 = CARTESIAN_POINT('',(-5.,10.)); +#652 = CARTESIAN_POINT('',(-5.,0.E+000)); +#653 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#654 = ORIENTED_EDGE('',*,*,#655,.F.); +#655 = EDGE_CURVE('',#191,#631,#656,.T.); +#656 = SURFACE_CURVE('',#657,(#660,#667),.PCURVE_S1.); +#657 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#658,#659),.UNSPECIFIED.,.F.,.F., + (2,2),(9.9800399E-004,3.00099800399),.PIECEWISE_BEZIER_KNOTS.); +#658 = CARTESIAN_POINT('',(15.,7.5,3.)); +#659 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#660 = PCURVE('',#248,#661); +#661 = DEFINITIONAL_REPRESENTATION('',(#662),#666); +#662 = LINE('',#663,#664); +#663 = CARTESIAN_POINT('',(0.E+000,30.)); +#664 = VECTOR('',#665,1.); +#665 = DIRECTION('',(1.,0.E+000)); +#666 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#667 = PCURVE('',#364,#668); +#668 = DEFINITIONAL_REPRESENTATION('',(#669),#673); +#669 = LINE('',#670,#671); +#670 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#671 = VECTOR('',#672,1.); +#672 = DIRECTION('',(1.,0.E+000)); +#673 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#674 = ORIENTED_EDGE('',*,*,#188,.F.); +#675 = ORIENTED_EDGE('',*,*,#676,.T.); +#676 = EDGE_CURVE('',#189,#629,#677,.T.); +#677 = SURFACE_CURVE('',#678,(#681,#688),.PCURVE_S1.); +#678 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#679,#680),.UNSPECIFIED.,.F.,.F., + (2,2),(9.9800399E-004,3.00099800399),.PIECEWISE_BEZIER_KNOTS.); +#679 = CARTESIAN_POINT('',(5.,7.5,3.)); +#680 = CARTESIAN_POINT('',(5.,7.5,0.E+000)); +#681 = PCURVE('',#248,#682); +#682 = DEFINITIONAL_REPRESENTATION('',(#683),#687); +#683 = LINE('',#684,#685); +#684 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#685 = VECTOR('',#686,1.); +#686 = DIRECTION('',(1.,0.E+000)); +#687 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#688 = PCURVE('',#364,#689); +#689 = DEFINITIONAL_REPRESENTATION('',(#690),#694); +#690 = LINE('',#691,#692); +#691 = CARTESIAN_POINT('',(0.E+000,30.)); +#692 = VECTOR('',#693,1.); +#693 = DIRECTION('',(1.,0.E+000)); +#694 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#695 = ADVANCED_FACE('',(#696),#364,.T.); +#696 = FACE_BOUND('',#697,.T.); +#697 = EDGE_LOOP('',(#698,#721,#722,#723)); +#698 = ORIENTED_EDGE('',*,*,#699,.T.); +#699 = EDGE_CURVE('',#631,#629,#700,.T.); +#700 = SURFACE_CURVE('',#701,(#706,#713),.PCURVE_S1.); +#701 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#702,#703,#704,#705), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#702 = CARTESIAN_POINT('',(15.,7.5,0.E+000)); +#703 = CARTESIAN_POINT('',(15.,-2.5,0.E+000)); +#704 = CARTESIAN_POINT('',(5.,-2.5,0.E+000)); +#705 = CARTESIAN_POINT('',(5.,7.5,0.E+000)); +#706 = PCURVE('',#364,#707); +#707 = DEFINITIONAL_REPRESENTATION('',(#708),#712); +#708 = LINE('',#709,#710); +#709 = CARTESIAN_POINT('',(3.00099800399,0.E+000)); +#710 = VECTOR('',#711,1.); +#711 = DIRECTION('',(0.E+000,1.)); +#712 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#713 = PCURVE('',#488,#714); +#714 = DEFINITIONAL_REPRESENTATION('',(#715),#720); +#715 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#716,#717,#718,#719), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#716 = CARTESIAN_POINT('',(-5.,0.E+000)); +#717 = CARTESIAN_POINT('',(-5.,-10.)); +#718 = CARTESIAN_POINT('',(5.,-10.)); +#719 = CARTESIAN_POINT('',(5.,0.E+000)); +#720 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#721 = ORIENTED_EDGE('',*,*,#676,.F.); +#722 = ORIENTED_EDGE('',*,*,#308,.F.); +#723 = ORIENTED_EDGE('',*,*,#655,.T.); +#724 = ADVANCED_FACE('',(#725,#731),#488,.T.); +#725 = FACE_BOUND('',#726,.T.); +#726 = EDGE_LOOP('',(#727,#728,#729,#730)); +#727 = ORIENTED_EDGE('',*,*,#503,.F.); +#728 = ORIENTED_EDGE('',*,*,#474,.F.); +#729 = ORIENTED_EDGE('',*,*,#604,.F.); +#730 = ORIENTED_EDGE('',*,*,#577,.F.); +#731 = FACE_BOUND('',#732,.T.); +#732 = EDGE_LOOP('',(#733,#734)); +#733 = ORIENTED_EDGE('',*,*,#699,.F.); +#734 = ORIENTED_EDGE('',*,*,#628,.F.); +#735 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#739)) GLOBAL_UNIT_ASSIGNED_CONTEXT +((#736,#737,#738)) REPRESENTATION_CONTEXT('Context #1', + '3D Context with UNIT and UNCERTAINTY') ); +#736 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#737 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#738 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#739 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#736, + 'distance_accuracy_value','confusion accuracy'); +#740 = SHAPE_DEFINITION_REPRESENTATION(#741,#62); +#741 = PRODUCT_DEFINITION_SHAPE('','',#742); +#742 = PRODUCT_DEFINITION('design','',#743,#746); +#743 = PRODUCT_DEFINITION_FORMATION('','',#744); +#744 = PRODUCT('nut','nut','',(#745)); +#745 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#746 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#747 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#748,#750); +#748 = ( REPRESENTATION_RELATIONSHIP('','',#62,#44) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#749) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#749 = ITEM_DEFINED_TRANSFORMATION('','',#11,#45); +#750 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#751 + ); +#751 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('1','nut_1','',#39,#742,$); +#752 = PRODUCT_TYPE('part',$,(#744)); +#753 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#754,#756); +#754 = ( REPRESENTATION_RELATIONSHIP('','',#62,#44) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#755) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#755 = ITEM_DEFINED_TRANSFORMATION('','',#11,#49); +#756 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item',#757 + ); +#757 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('2','nut_2','',#39,#742,$); +#758 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#759),#1115); +#759 = MANIFOLD_SOLID_BREP('',#760); +#760 = CLOSED_SHELL('',(#761,#1005,#1081,#1110)); +#761 = ADVANCED_FACE('',(#762),#797,.T.); +#762 = FACE_BOUND('',#763,.T.); +#763 = EDGE_LOOP('',(#764,#889)); +#764 = ORIENTED_EDGE('',*,*,#765,.F.); +#765 = EDGE_CURVE('',#766,#768,#770,.T.); +#766 = VERTEX_POINT('',#767); +#767 = CARTESIAN_POINT('',(5.,2.22044604925E-016,200.)); +#768 = VERTEX_POINT('',#769); +#769 = CARTESIAN_POINT('',(-5.,-2.22044604925E-016,200.)); +#770 = SURFACE_CURVE('',#771,(#796,#829),.PCURVE_S1.); +#771 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#772,#773,#774,#775,#776,#777, + #778,#779,#780,#781,#782,#783,#784,#785,#786,#787,#788,#789,#790, + #791,#792,#793,#794,#795),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164387,7.85828164661,10.7238180516,13.5836589935, + 16.4911855015,20.38776087,22.3658107353),.UNSPECIFIED.); +#772 = CARTESIAN_POINT('',(5.,-2.22044604925E-016,200.)); +#773 = CARTESIAN_POINT('',(5.,-0.467198252312,200.)); +#774 = CARTESIAN_POINT('',(4.94543032016,-0.967985463874,200.)); +#775 = CARTESIAN_POINT('',(4.82041774119,-1.49112303535,200.)); +#776 = CARTESIAN_POINT('',(4.42731387443,-2.48006143438,200.)); +#777 = CARTESIAN_POINT('',(3.74198536382,-3.38090473983,200.)); +#778 = CARTESIAN_POINT('',(3.35476380665,-3.76862633308,200.)); +#779 = CARTESIAN_POINT('',(2.56749137395,-4.36208802884,200.)); +#780 = CARTESIAN_POINT('',(1.64518926245,-4.75184036526,200.)); +#781 = CARTESIAN_POINT('',(1.22322144323,-4.87791933608,200.)); +#782 = CARTESIAN_POINT('',(0.356287037014,-5.03548099138,200.)); +#783 = CARTESIAN_POINT('',(-0.52640030158,-5.00140076198,200.)); +#784 = CARTESIAN_POINT('',(-0.963050674765,-4.93574856594,200.)); +#785 = CARTESIAN_POINT('',(-1.81864212033,-4.70884578804,200.)); +#786 = CARTESIAN_POINT('',(-2.59575461931,-4.30713067084,200.)); +#787 = CARTESIAN_POINT('',(-2.9603131848,-4.06421908239,200.)); +#788 = CARTESIAN_POINT('',(-3.73554903634,-3.41630129394,200.)); +#789 = CARTESIAN_POINT('',(-4.3095225984,-2.62465565461,200.)); +#790 = CARTESIAN_POINT('',(-4.56375002186,-2.14244819995,200.)); +#791 = CARTESIAN_POINT('',(-4.8362924348,-1.40481893471,200.)); +#792 = CARTESIAN_POINT('',(-4.96121877006,-0.68885510118,200.)); +#793 = CARTESIAN_POINT('',(-4.98763322877,-0.452431376999,200.)); +#794 = CARTESIAN_POINT('',(-5.,-0.222409665749,200.)); +#795 = CARTESIAN_POINT('',(-5.,4.4408920985E-016,200.)); +#796 = PCURVE('',#797,#802); +#797 = PLANE('',#798); +#798 = AXIS2_PLACEMENT_3D('',#799,#800,#801); +#799 = CARTESIAN_POINT('',(0.E+000,0.E+000,200.)); +#800 = DIRECTION('',(0.E+000,0.E+000,1.)); +#801 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#802 = DEFINITIONAL_REPRESENTATION('',(#803),#828); +#803 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#804,#805,#806,#807,#808,#809, + #810,#811,#812,#813,#814,#815,#816,#817,#818,#819,#820,#821,#822, + #823,#824,#825,#826,#827),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164387,7.85828164661,10.7238180516,13.5836589935, + 16.4911855015,20.38776087,22.3658107353),.UNSPECIFIED.); +#804 = CARTESIAN_POINT('',(5.,-2.22044604925E-016)); +#805 = CARTESIAN_POINT('',(5.,-0.467198252312)); +#806 = CARTESIAN_POINT('',(4.94543032016,-0.967985463874)); +#807 = CARTESIAN_POINT('',(4.82041774119,-1.49112303535)); +#808 = CARTESIAN_POINT('',(4.42731387443,-2.48006143438)); +#809 = CARTESIAN_POINT('',(3.74198536382,-3.38090473983)); +#810 = CARTESIAN_POINT('',(3.35476380665,-3.76862633308)); +#811 = CARTESIAN_POINT('',(2.56749137395,-4.36208802884)); +#812 = CARTESIAN_POINT('',(1.64518926245,-4.75184036526)); +#813 = CARTESIAN_POINT('',(1.22322144323,-4.87791933608)); +#814 = CARTESIAN_POINT('',(0.356287037014,-5.03548099138)); +#815 = CARTESIAN_POINT('',(-0.52640030158,-5.00140076198)); +#816 = CARTESIAN_POINT('',(-0.963050674765,-4.93574856594)); +#817 = CARTESIAN_POINT('',(-1.81864212033,-4.70884578804)); +#818 = CARTESIAN_POINT('',(-2.59575461931,-4.30713067084)); +#819 = CARTESIAN_POINT('',(-2.9603131848,-4.06421908239)); +#820 = CARTESIAN_POINT('',(-3.73554903634,-3.41630129394)); +#821 = CARTESIAN_POINT('',(-4.3095225984,-2.62465565461)); +#822 = CARTESIAN_POINT('',(-4.56375002186,-2.14244819995)); +#823 = CARTESIAN_POINT('',(-4.8362924348,-1.40481893471)); +#824 = CARTESIAN_POINT('',(-4.96121877006,-0.68885510118)); +#825 = CARTESIAN_POINT('',(-4.98763322877,-0.452431376999)); +#826 = CARTESIAN_POINT('',(-5.,-0.222409665749)); +#827 = CARTESIAN_POINT('',(-5.,4.4408920985E-016)); +#828 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#829 = PCURVE('',#830,#839); +#830 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#831,#832,#833,#834) + ,(#835,#836,#837,#838 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,200.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#831 = CARTESIAN_POINT('',(-5.,0.E+000,200.)); +#832 = CARTESIAN_POINT('',(-5.,-10.,200.)); +#833 = CARTESIAN_POINT('',(5.,-10.,200.)); +#834 = CARTESIAN_POINT('',(5.,0.E+000,200.)); +#835 = CARTESIAN_POINT('',(-5.,0.E+000,0.E+000)); +#836 = CARTESIAN_POINT('',(-5.,-10.,0.E+000)); +#837 = CARTESIAN_POINT('',(5.,-10.,0.E+000)); +#838 = CARTESIAN_POINT('',(5.,0.E+000,0.E+000)); +#839 = DEFINITIONAL_REPRESENTATION('',(#840),#888); +#840 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#841,#842,#843,#844,#845,#846, + #847,#848,#849,#850,#851,#852,#853,#854,#855,#856,#857,#858,#859, + #860,#861,#862,#863,#864,#865,#866,#867,#868,#869,#870,#871,#872, + #873,#874,#875,#876,#877,#878,#879,#880,#881,#882,#883,#884,#885, + #886,#887),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000, + 0.508313880348,1.016627760695,1.524941641043,2.033255521391, + 2.541569401739,3.049883282086,3.558197162434,4.066511042782, + 4.57482492313,5.083138803477,5.591452683825,6.099766564173, + 6.60808044452,7.116394324868,7.624708205216,8.133022085564, + 8.641335965911,9.149649846259,9.657963726607,10.166277606955, + 10.674591487302,11.18290536765,11.691219247998,12.199533128345, + 12.707847008693,13.216160889041,13.724474769389,14.232788649736, + 14.741102530084,15.249416410432,15.75773029078,16.266044171127, + 16.774358051475,17.282671931823,17.79098581217,18.299299692518, + 18.807613572866,19.315927453214,19.824241333561,20.332555213909, + 20.840869094257,21.349182974605,21.857496854952,22.3658107353), + .QUASI_UNIFORM_KNOTS.); +#841 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#842 = CARTESIAN_POINT('',(9.980039899826E-004,29.714213865995)); +#843 = CARTESIAN_POINT('',(9.980039899667E-004,29.148976275626)); +#844 = CARTESIAN_POINT('',(9.98003989972E-004,28.320341049933)); +#845 = CARTESIAN_POINT('',(9.980039899747E-004,27.511224157016)); +#846 = CARTESIAN_POINT('',(9.980039899587E-004,26.721642608853)); +#847 = CARTESIAN_POINT('',(9.980039900196E-004,25.951409909365)); +#848 = CARTESIAN_POINT('',(9.980039899628E-004,25.200126448755)); +#849 = CARTESIAN_POINT('',(9.980039899586E-004,24.467219023802)); +#850 = CARTESIAN_POINT('',(9.98003990032E-004,23.751979088838)); +#851 = CARTESIAN_POINT('',(9.980039899132E-004,23.053639425058)); +#852 = CARTESIAN_POINT('',(9.98003989974E-004,22.371311364439)); +#853 = CARTESIAN_POINT('',(9.9800399002E-004,21.703926026155)); +#854 = CARTESIAN_POINT('',(9.980039899456E-004,21.050316054675)); +#855 = CARTESIAN_POINT('',(9.980039900268E-004,20.409255216776)); +#856 = CARTESIAN_POINT('',(9.980039899471E-004,19.779500810931)); +#857 = CARTESIAN_POINT('',(9.98003990014E-004,19.159817475822)); +#858 = CARTESIAN_POINT('',(9.98003989997E-004,18.549038004437)); +#859 = CARTESIAN_POINT('',(9.980039899983E-004,17.945942163512)); +#860 = CARTESIAN_POINT('',(9.980039900102E-004,17.349215044793)); +#861 = CARTESIAN_POINT('',(9.980039899614E-004,16.757562993069)); +#862 = CARTESIAN_POINT('',(9.980039899745E-004,16.169688680961)); +#863 = CARTESIAN_POINT('',(9.980039899711E-004,15.584299557553)); +#864 = CARTESIAN_POINT('',(9.980039899716E-004,15.000102384886)); +#865 = CARTESIAN_POINT('',(9.980039899734E-004,14.415910986161)); +#866 = CARTESIAN_POINT('',(9.980039899657E-004,13.830503876104)); +#867 = CARTESIAN_POINT('',(9.980039899949E-004,13.242625985685)); +#868 = CARTESIAN_POINT('',(9.980039900566E-004,12.650998079437)); +#869 = CARTESIAN_POINT('',(9.980039899516E-004,12.054322470375)); +#870 = CARTESIAN_POINT('',(9.980039899689E-004,11.451287776291)); +#871 = CARTESIAN_POINT('',(9.980039900049E-004,10.840593700147)); +#872 = CARTESIAN_POINT('',(9.980039900144E-004,10.220965455217)); +#873 = CARTESIAN_POINT('',(9.980039899406E-004,9.59115588443)); +#874 = CARTESIAN_POINT('',(9.98003990056E-004,8.94994928042)); +#875 = CARTESIAN_POINT('',(9.980039900095E-004,8.296178755736)); +#876 = CARTESIAN_POINT('',(9.980039899101E-004,7.628713188564)); +#877 = CARTESIAN_POINT('',(9.980039900137E-004,6.946419463728)); +#878 = CARTESIAN_POINT('',(9.980039900403E-004,6.248219107721)); +#879 = CARTESIAN_POINT('',(9.980039900015E-004,5.533123529128)); +#880 = CARTESIAN_POINT('',(9.980039899607E-004,4.800267344587)); +#881 = CARTESIAN_POINT('',(9.980039899929E-004,4.048935579056)); +#882 = CARTESIAN_POINT('',(9.980039899063E-004,3.278586311278)); +#883 = CARTESIAN_POINT('',(9.980039900514E-004,2.488870543065)); +#884 = CARTESIAN_POINT('',(9.980039899004E-004,1.679678044096)); +#885 = CARTESIAN_POINT('',(9.980039900201E-004,0.851022751652)); +#886 = CARTESIAN_POINT('',(9.980039900301E-004,0.285786197076)); +#887 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#888 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#889 = ORIENTED_EDGE('',*,*,#890,.F.); +#890 = EDGE_CURVE('',#768,#766,#891,.T.); +#891 = SURFACE_CURVE('',#892,(#917,#945),.PCURVE_S1.); +#892 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#893,#894,#895,#896,#897,#898, + #899,#900,#901,#902,#903,#904,#905,#906,#907,#908,#909,#910,#911, + #912,#913,#914,#915,#916),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164387,7.85828164661,10.7238180516,13.5836589935, + 16.4911855015,20.38776087,22.3658107353),.UNSPECIFIED.); +#893 = CARTESIAN_POINT('',(-5.,2.22044604925E-016,200.)); +#894 = CARTESIAN_POINT('',(-5.,0.467198252312,200.)); +#895 = CARTESIAN_POINT('',(-4.94543032016,0.967985463874,200.)); +#896 = CARTESIAN_POINT('',(-4.82041774119,1.49112303535,200.)); +#897 = CARTESIAN_POINT('',(-4.42731387443,2.48006143438,200.)); +#898 = CARTESIAN_POINT('',(-3.74198536382,3.38090473983,200.)); +#899 = CARTESIAN_POINT('',(-3.35476380665,3.76862633308,200.)); +#900 = CARTESIAN_POINT('',(-2.56749137395,4.36208802884,200.)); +#901 = CARTESIAN_POINT('',(-1.64518926245,4.75184036526,200.)); +#902 = CARTESIAN_POINT('',(-1.22322144323,4.87791933608,200.)); +#903 = CARTESIAN_POINT('',(-0.356287037014,5.03548099138,200.)); +#904 = CARTESIAN_POINT('',(0.52640030158,5.00140076198,200.)); +#905 = CARTESIAN_POINT('',(0.963050674765,4.93574856594,200.)); +#906 = CARTESIAN_POINT('',(1.81864212033,4.70884578804,200.)); +#907 = CARTESIAN_POINT('',(2.59575461931,4.30713067084,200.)); +#908 = CARTESIAN_POINT('',(2.9603131848,4.06421908239,200.)); +#909 = CARTESIAN_POINT('',(3.73554903634,3.41630129394,200.)); +#910 = CARTESIAN_POINT('',(4.3095225984,2.62465565461,200.)); +#911 = CARTESIAN_POINT('',(4.56375002186,2.14244819995,200.)); +#912 = CARTESIAN_POINT('',(4.8362924348,1.40481893471,200.)); +#913 = CARTESIAN_POINT('',(4.96121877006,0.68885510118,200.)); +#914 = CARTESIAN_POINT('',(4.98763322877,0.452431376999,200.)); +#915 = CARTESIAN_POINT('',(5.,0.222409665749,200.)); +#916 = CARTESIAN_POINT('',(5.,-4.4408920985E-016,200.)); +#917 = PCURVE('',#797,#918); +#918 = DEFINITIONAL_REPRESENTATION('',(#919),#944); +#919 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#920,#921,#922,#923,#924,#925, + #926,#927,#928,#929,#930,#931,#932,#933,#934,#935,#936,#937,#938, + #939,#940,#941,#942,#943),.UNSPECIFIED.,.F.,.F.,(6,3,3,3,3,3,3,6),( + 0.E+000,4.15513164387,7.85828164661,10.7238180516,13.5836589935, + 16.4911855015,20.38776087,22.3658107353),.UNSPECIFIED.); +#920 = CARTESIAN_POINT('',(-5.,2.22044604925E-016)); +#921 = CARTESIAN_POINT('',(-5.,0.467198252312)); +#922 = CARTESIAN_POINT('',(-4.94543032016,0.967985463874)); +#923 = CARTESIAN_POINT('',(-4.82041774119,1.49112303535)); +#924 = CARTESIAN_POINT('',(-4.42731387443,2.48006143438)); +#925 = CARTESIAN_POINT('',(-3.74198536382,3.38090473983)); +#926 = CARTESIAN_POINT('',(-3.35476380665,3.76862633308)); +#927 = CARTESIAN_POINT('',(-2.56749137395,4.36208802884)); +#928 = CARTESIAN_POINT('',(-1.64518926245,4.75184036526)); +#929 = CARTESIAN_POINT('',(-1.22322144323,4.87791933608)); +#930 = CARTESIAN_POINT('',(-0.356287037014,5.03548099138)); +#931 = CARTESIAN_POINT('',(0.52640030158,5.00140076198)); +#932 = CARTESIAN_POINT('',(0.963050674765,4.93574856594)); +#933 = CARTESIAN_POINT('',(1.81864212033,4.70884578804)); +#934 = CARTESIAN_POINT('',(2.59575461931,4.30713067084)); +#935 = CARTESIAN_POINT('',(2.9603131848,4.06421908239)); +#936 = CARTESIAN_POINT('',(3.73554903634,3.41630129394)); +#937 = CARTESIAN_POINT('',(4.3095225984,2.62465565461)); +#938 = CARTESIAN_POINT('',(4.56375002186,2.14244819995)); +#939 = CARTESIAN_POINT('',(4.8362924348,1.40481893471)); +#940 = CARTESIAN_POINT('',(4.96121877006,0.68885510118)); +#941 = CARTESIAN_POINT('',(4.98763322877,0.452431376999)); +#942 = CARTESIAN_POINT('',(5.,0.222409665749)); +#943 = CARTESIAN_POINT('',(5.,-4.4408920985E-016)); +#944 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#945 = PCURVE('',#946,#955); +#946 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#947,#948,#949,#950) + ,(#951,#952,#953,#954 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,200.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#947 = CARTESIAN_POINT('',(5.,0.E+000,200.)); +#948 = CARTESIAN_POINT('',(5.,10.,200.)); +#949 = CARTESIAN_POINT('',(-5.,10.,200.)); +#950 = CARTESIAN_POINT('',(-5.,0.E+000,200.)); +#951 = CARTESIAN_POINT('',(5.,0.E+000,0.E+000)); +#952 = CARTESIAN_POINT('',(5.,10.,0.E+000)); +#953 = CARTESIAN_POINT('',(-5.,10.,0.E+000)); +#954 = CARTESIAN_POINT('',(-5.,0.E+000,0.E+000)); +#955 = DEFINITIONAL_REPRESENTATION('',(#956),#1004); +#956 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#957,#958,#959,#960,#961,#962, + #963,#964,#965,#966,#967,#968,#969,#970,#971,#972,#973,#974,#975, + #976,#977,#978,#979,#980,#981,#982,#983,#984,#985,#986,#987,#988, + #989,#990,#991,#992,#993,#994,#995,#996,#997,#998,#999,#1000,#1001, + #1002,#1003),.UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000, + 0.508313880348,1.016627760695,1.524941641043,2.033255521391, + 2.541569401739,3.049883282086,3.558197162434,4.066511042782, + 4.57482492313,5.083138803477,5.591452683825,6.099766564173, + 6.60808044452,7.116394324868,7.624708205216,8.133022085564, + 8.641335965911,9.149649846259,9.657963726607,10.166277606955, + 10.674591487302,11.18290536765,11.691219247998,12.199533128345, + 12.707847008693,13.216160889041,13.724474769389,14.232788649736, + 14.741102530084,15.249416410432,15.75773029078,16.266044171127, + 16.774358051475,17.282671931823,17.79098581217,18.299299692518, + 18.807613572866,19.315927453214,19.824241333561,20.332555213909, + 20.840869094257,21.349182974605,21.857496854952,22.3658107353), + .QUASI_UNIFORM_KNOTS.); +#957 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#958 = CARTESIAN_POINT('',(9.980039899826E-004,29.714213865995)); +#959 = CARTESIAN_POINT('',(9.980039899667E-004,29.148976275626)); +#960 = CARTESIAN_POINT('',(9.98003989972E-004,28.320341049933)); +#961 = CARTESIAN_POINT('',(9.980039899747E-004,27.511224157016)); +#962 = CARTESIAN_POINT('',(9.980039899587E-004,26.721642608853)); +#963 = CARTESIAN_POINT('',(9.980039900196E-004,25.951409909365)); +#964 = CARTESIAN_POINT('',(9.980039899628E-004,25.200126448755)); +#965 = CARTESIAN_POINT('',(9.980039899586E-004,24.467219023802)); +#966 = CARTESIAN_POINT('',(9.98003990032E-004,23.751979088838)); +#967 = CARTESIAN_POINT('',(9.980039899132E-004,23.053639425058)); +#968 = CARTESIAN_POINT('',(9.98003989974E-004,22.371311364439)); +#969 = CARTESIAN_POINT('',(9.9800399002E-004,21.703926026155)); +#970 = CARTESIAN_POINT('',(9.980039899456E-004,21.050316054675)); +#971 = CARTESIAN_POINT('',(9.980039900268E-004,20.409255216776)); +#972 = CARTESIAN_POINT('',(9.980039899471E-004,19.779500810931)); +#973 = CARTESIAN_POINT('',(9.98003990014E-004,19.159817475822)); +#974 = CARTESIAN_POINT('',(9.98003989997E-004,18.549038004437)); +#975 = CARTESIAN_POINT('',(9.980039899983E-004,17.945942163512)); +#976 = CARTESIAN_POINT('',(9.980039900102E-004,17.349215044793)); +#977 = CARTESIAN_POINT('',(9.980039899614E-004,16.757562993069)); +#978 = CARTESIAN_POINT('',(9.980039899745E-004,16.169688680961)); +#979 = CARTESIAN_POINT('',(9.980039899711E-004,15.584299557553)); +#980 = CARTESIAN_POINT('',(9.980039899716E-004,15.000102384886)); +#981 = CARTESIAN_POINT('',(9.980039899734E-004,14.415910986161)); +#982 = CARTESIAN_POINT('',(9.980039899657E-004,13.830503876104)); +#983 = CARTESIAN_POINT('',(9.980039899949E-004,13.242625985685)); +#984 = CARTESIAN_POINT('',(9.980039900566E-004,12.650998079437)); +#985 = CARTESIAN_POINT('',(9.980039899516E-004,12.054322470375)); +#986 = CARTESIAN_POINT('',(9.980039899689E-004,11.451287776291)); +#987 = CARTESIAN_POINT('',(9.980039900049E-004,10.840593700147)); +#988 = CARTESIAN_POINT('',(9.980039900144E-004,10.220965455217)); +#989 = CARTESIAN_POINT('',(9.980039899406E-004,9.59115588443)); +#990 = CARTESIAN_POINT('',(9.98003990056E-004,8.94994928042)); +#991 = CARTESIAN_POINT('',(9.980039900095E-004,8.296178755736)); +#992 = CARTESIAN_POINT('',(9.980039899101E-004,7.628713188564)); +#993 = CARTESIAN_POINT('',(9.980039900137E-004,6.946419463728)); +#994 = CARTESIAN_POINT('',(9.980039900403E-004,6.248219107721)); +#995 = CARTESIAN_POINT('',(9.980039900015E-004,5.533123529128)); +#996 = CARTESIAN_POINT('',(9.980039899607E-004,4.800267344587)); +#997 = CARTESIAN_POINT('',(9.980039899929E-004,4.048935579056)); +#998 = CARTESIAN_POINT('',(9.980039899063E-004,3.278586311278)); +#999 = CARTESIAN_POINT('',(9.980039900514E-004,2.488870543065)); +#1000 = CARTESIAN_POINT('',(9.980039899004E-004,1.679678044096)); +#1001 = CARTESIAN_POINT('',(9.980039900201E-004,0.851022751652)); +#1002 = CARTESIAN_POINT('',(9.980039900301E-004,0.285786197076)); +#1003 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#1004 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1005 = ADVANCED_FACE('',(#1006),#830,.T.); +#1006 = FACE_BOUND('',#1007,.T.); +#1007 = EDGE_LOOP('',(#1008,#1009,#1031,#1061)); +#1008 = ORIENTED_EDGE('',*,*,#765,.T.); +#1009 = ORIENTED_EDGE('',*,*,#1010,.T.); +#1010 = EDGE_CURVE('',#768,#1011,#1013,.T.); +#1011 = VERTEX_POINT('',#1012); +#1012 = CARTESIAN_POINT('',(-5.,0.E+000,0.E+000)); +#1013 = SURFACE_CURVE('',#1014,(#1017,#1024),.PCURVE_S1.); +#1014 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1015,#1016),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,200.000998004),.PIECEWISE_BEZIER_KNOTS.); +#1015 = CARTESIAN_POINT('',(-5.,-5.55111512307E-016,200.)); +#1016 = CARTESIAN_POINT('',(-5.,-5.55111512307E-016,0.E+000)); +#1017 = PCURVE('',#830,#1018); +#1018 = DEFINITIONAL_REPRESENTATION('',(#1019),#1023); +#1019 = LINE('',#1020,#1021); +#1020 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1021 = VECTOR('',#1022,1.); +#1022 = DIRECTION('',(1.,0.E+000)); +#1023 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1024 = PCURVE('',#946,#1025); +#1025 = DEFINITIONAL_REPRESENTATION('',(#1026),#1030); +#1026 = LINE('',#1027,#1028); +#1027 = CARTESIAN_POINT('',(0.E+000,30.)); +#1028 = VECTOR('',#1029,1.); +#1029 = DIRECTION('',(1.,0.E+000)); +#1030 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1031 = ORIENTED_EDGE('',*,*,#1032,.T.); +#1032 = EDGE_CURVE('',#1011,#1033,#1035,.T.); +#1033 = VERTEX_POINT('',#1034); +#1034 = CARTESIAN_POINT('',(5.,0.E+000,0.E+000)); +#1035 = SURFACE_CURVE('',#1036,(#1041,#1048),.PCURVE_S1.); +#1036 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1037,#1038,#1039,#1040), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1037 = CARTESIAN_POINT('',(-5.,0.E+000,0.E+000)); +#1038 = CARTESIAN_POINT('',(-5.,-10.,0.E+000)); +#1039 = CARTESIAN_POINT('',(5.,-10.,0.E+000)); +#1040 = CARTESIAN_POINT('',(5.,0.E+000,0.E+000)); +#1041 = PCURVE('',#830,#1042); +#1042 = DEFINITIONAL_REPRESENTATION('',(#1043),#1047); +#1043 = LINE('',#1044,#1045); +#1044 = CARTESIAN_POINT('',(200.000998004,0.E+000)); +#1045 = VECTOR('',#1046,1.); +#1046 = DIRECTION('',(0.E+000,1.)); +#1047 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1048 = PCURVE('',#1049,#1054); +#1049 = PLANE('',#1050); +#1050 = AXIS2_PLACEMENT_3D('',#1051,#1052,#1053); +#1051 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#1052 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1053 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#1054 = DEFINITIONAL_REPRESENTATION('',(#1055),#1060); +#1055 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1056,#1057,#1058,#1059), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1056 = CARTESIAN_POINT('',(5.,0.E+000)); +#1057 = CARTESIAN_POINT('',(5.,-10.)); +#1058 = CARTESIAN_POINT('',(-5.,-10.)); +#1059 = CARTESIAN_POINT('',(-5.,0.E+000)); +#1060 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1061 = ORIENTED_EDGE('',*,*,#1062,.F.); +#1062 = EDGE_CURVE('',#766,#1033,#1063,.T.); +#1063 = SURFACE_CURVE('',#1064,(#1067,#1074),.PCURVE_S1.); +#1064 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1065,#1066),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,200.000998004),.PIECEWISE_BEZIER_KNOTS.); +#1065 = CARTESIAN_POINT('',(5.,-5.55111512307E-016,200.)); +#1066 = CARTESIAN_POINT('',(5.,-5.55111512307E-016,0.E+000)); +#1067 = PCURVE('',#830,#1068); +#1068 = DEFINITIONAL_REPRESENTATION('',(#1069),#1073); +#1069 = LINE('',#1070,#1071); +#1070 = CARTESIAN_POINT('',(0.E+000,30.)); +#1071 = VECTOR('',#1072,1.); +#1072 = DIRECTION('',(1.,0.E+000)); +#1073 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1074 = PCURVE('',#946,#1075); +#1075 = DEFINITIONAL_REPRESENTATION('',(#1076),#1080); +#1076 = LINE('',#1077,#1078); +#1077 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1078 = VECTOR('',#1079,1.); +#1079 = DIRECTION('',(1.,0.E+000)); +#1080 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1081 = ADVANCED_FACE('',(#1082),#946,.T.); +#1082 = FACE_BOUND('',#1083,.T.); +#1083 = EDGE_LOOP('',(#1084,#1085,#1086,#1109)); +#1084 = ORIENTED_EDGE('',*,*,#890,.T.); +#1085 = ORIENTED_EDGE('',*,*,#1062,.T.); +#1086 = ORIENTED_EDGE('',*,*,#1087,.T.); +#1087 = EDGE_CURVE('',#1033,#1011,#1088,.T.); +#1088 = SURFACE_CURVE('',#1089,(#1094,#1101),.PCURVE_S1.); +#1089 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1090,#1091,#1092,#1093), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1090 = CARTESIAN_POINT('',(5.,0.E+000,0.E+000)); +#1091 = CARTESIAN_POINT('',(5.,10.,0.E+000)); +#1092 = CARTESIAN_POINT('',(-5.,10.,0.E+000)); +#1093 = CARTESIAN_POINT('',(-5.,0.E+000,0.E+000)); +#1094 = PCURVE('',#946,#1095); +#1095 = DEFINITIONAL_REPRESENTATION('',(#1096),#1100); +#1096 = LINE('',#1097,#1098); +#1097 = CARTESIAN_POINT('',(200.000998004,0.E+000)); +#1098 = VECTOR('',#1099,1.); +#1099 = DIRECTION('',(0.E+000,1.)); +#1100 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1101 = PCURVE('',#1049,#1102); +#1102 = DEFINITIONAL_REPRESENTATION('',(#1103),#1108); +#1103 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1104,#1105,#1106,#1107), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1104 = CARTESIAN_POINT('',(-5.,0.E+000)); +#1105 = CARTESIAN_POINT('',(-5.,10.)); +#1106 = CARTESIAN_POINT('',(5.,10.)); +#1107 = CARTESIAN_POINT('',(5.,0.E+000)); +#1108 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1109 = ORIENTED_EDGE('',*,*,#1010,.F.); +#1110 = ADVANCED_FACE('',(#1111),#1049,.T.); +#1111 = FACE_BOUND('',#1112,.T.); +#1112 = EDGE_LOOP('',(#1113,#1114)); +#1113 = ORIENTED_EDGE('',*,*,#1032,.F.); +#1114 = ORIENTED_EDGE('',*,*,#1087,.F.); +#1115 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1119)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1116,#1117,#1118)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1116 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1117 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1118 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1119 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-005),#1116, + 'distance_accuracy_value','confusion accuracy'); +#1120 = SHAPE_DEFINITION_REPRESENTATION(#1121,#758); +#1121 = PRODUCT_DEFINITION_SHAPE('','',#1122); +#1122 = PRODUCT_DEFINITION('design','',#1123,#1126); +#1123 = PRODUCT_DEFINITION_FORMATION('','',#1124); +#1124 = PRODUCT('rod','rod','',(#1125)); +#1125 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#1126 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#1127 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1128,#1130); +#1128 = ( REPRESENTATION_RELATIONSHIP('','',#758,#44) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1129) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1129 = ITEM_DEFINED_TRANSFORMATION('','',#11,#53); +#1130 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1131); +#1131 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('3','rod_1','',#39,#1122,$); +#1132 = PRODUCT_TYPE('part',$,(#1124)); +#1133 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1134,#1136); +#1134 = ( REPRESENTATION_RELATIONSHIP('','',#44,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1135) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1135 = ITEM_DEFINED_TRANSFORMATION('','',#11,#15); +#1136 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1137); +#1137 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('4','rod-assembly_1','',#5,#39,$ + ); +#1138 = PRODUCT_TYPE('part',$,(#41)); +#1139 = SHAPE_DEFINITION_REPRESENTATION(#1140,#1146); +#1140 = PRODUCT_DEFINITION_SHAPE('','',#1141); +#1141 = PRODUCT_DEFINITION('design','',#1142,#1145); +#1142 = PRODUCT_DEFINITION_FORMATION('','',#1143); +#1143 = PRODUCT('l-bracket-assembly','l-bracket-assembly','',(#1144)); +#1144 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#1145 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#1146 = SHAPE_REPRESENTATION('',(#11,#1147,#1151,#1155,#1159),#1163); +#1147 = AXIS2_PLACEMENT_3D('',#1148,#1149,#1150); +#1148 = CARTESIAN_POINT('',(27.5,-40.,0.E+000)); +#1149 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1150 = DIRECTION('',(1.,0.E+000,0.E+000)); +#1151 = AXIS2_PLACEMENT_3D('',#1152,#1153,#1154); +#1152 = CARTESIAN_POINT('',(50.,-52.99038106,0.E+000)); +#1153 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1154 = DIRECTION('',(1.,0.E+000,0.E+000)); +#1155 = AXIS2_PLACEMENT_3D('',#1156,#1157,#1158); +#1156 = CARTESIAN_POINT('',(50.,-27.00961894,0.E+000)); +#1157 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1158 = DIRECTION('',(1.,0.E+000,0.E+000)); +#1159 = AXIS2_PLACEMENT_3D('',#1160,#1161,#1162); +#1160 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#1161 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#1162 = DIRECTION('',(1.,0.E+000,0.E+000)); +#1163 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1167)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1164,#1165,#1166)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1164 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1165 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1166 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1167 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#1164, + 'distance_accuracy_value','confusion accuracy'); +#1168 = SHAPE_DEFINITION_REPRESENTATION(#1169,#1175); +#1169 = PRODUCT_DEFINITION_SHAPE('','',#1170); +#1170 = PRODUCT_DEFINITION('design','',#1171,#1174); +#1171 = PRODUCT_DEFINITION_FORMATION('','',#1172); +#1172 = PRODUCT('nut-bolt-assembly','nut-bolt-assembly','',(#1173)); +#1173 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#1174 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#1175 = SHAPE_REPRESENTATION('',(#11,#1176,#1180),#1184); +#1176 = AXIS2_PLACEMENT_3D('',#1177,#1178,#1179); +#1177 = CARTESIAN_POINT('',(-7.5,-10.,13.)); +#1178 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1179 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#1180 = AXIS2_PLACEMENT_3D('',#1181,#1182,#1183); +#1181 = CARTESIAN_POINT('',(2.5,-17.5,-20.)); +#1182 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1183 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#1184 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1188)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1185,#1186,#1187)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1185 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1186 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1187 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1188 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#1185, + 'distance_accuracy_value','confusion accuracy'); +#1189 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1190),#1894); +#1190 = MANIFOLD_SOLID_BREP('',#1191); +#1191 = CLOSED_SHELL('',(#1192,#1674,#1750,#1779,#1855,#1884,#1889)); +#1192 = ADVANCED_FACE('',(#1193,#1436),#1228,.T.); +#1193 = FACE_BOUND('',#1194,.T.); +#1194 = EDGE_LOOP('',(#1195,#1320)); +#1195 = ORIENTED_EDGE('',*,*,#1196,.F.); +#1196 = EDGE_CURVE('',#1197,#1199,#1201,.T.); +#1197 = VERTEX_POINT('',#1198); +#1198 = CARTESIAN_POINT('',(7.5,0.E+000,3.)); +#1199 = VERTEX_POINT('',#1200); +#1200 = CARTESIAN_POINT('',(-7.5,0.E+000,3.)); +#1201 = SURFACE_CURVE('',#1202,(#1227,#1260),.PCURVE_S1.); +#1202 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1203,#1204,#1205,#1206,#1207, + #1208,#1209,#1210,#1211,#1212,#1213,#1214,#1215,#1216,#1217,#1218, + #1219,#1220,#1221,#1222,#1223,#1224,#1225,#1226),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,5.20225778542,9.84158873828, + 14.2673349509,18.6433186512,23.0548848731,27.6530164185, + 33.5425690087),.UNSPECIFIED.); +#1203 = CARTESIAN_POINT('',(7.5,6.66133814775E-016,3.)); +#1204 = CARTESIAN_POINT('',(7.5,-0.585054612929,3.)); +#1205 = CARTESIAN_POINT('',(7.44295106424,-1.20521801478,3.)); +#1206 = CARTESIAN_POINT('',(7.31515940691,-1.85033890984,3.)); +#1207 = CARTESIAN_POINT('',(6.9174836202,-3.08527233291,3.)); +#1208 = CARTESIAN_POINT('',(6.21610886075,-4.27235963842,3.)); +#1209 = CARTESIAN_POINT('',(5.81621499215,-4.80660561995,3.)); +#1210 = CARTESIAN_POINT('',(4.90603051399,-5.77088806315,3.)); +#1211 = CARTESIAN_POINT('',(3.775988505,-6.53134212728,3.)); +#1212 = CARTESIAN_POINT('',(3.1790299248,-6.8428729705,3.)); +#1213 = CARTESIAN_POINT('',(1.92404155108,-7.32665470362,3.)); +#1214 = CARTESIAN_POINT('',(0.582116172098,-7.52278240149,3.)); +#1215 = CARTESIAN_POINT('',(-9.46313364034E-002,-7.54474978799,3.)); +#1216 = CARTESIAN_POINT('',(-1.44588275644,-7.43589277948,3.)); +#1217 = CARTESIAN_POINT('',(-2.73149765405,-7.03353365966,3.)); +#1218 = CARTESIAN_POINT('',(-3.34804882139,-6.76091512264,3.)); +#1219 = CARTESIAN_POINT('',(-4.52434338626,-6.07498368569,3.)); +#1220 = CARTESIAN_POINT('',(-5.49752166125,-5.16815745669,3.)); +#1221 = CARTESIAN_POINT('',(-5.93188641726,-4.6595782538,3.)); +#1222 = CARTESIAN_POINT('',(-6.76982690894,-3.42768019481,3.)); +#1223 = CARTESIAN_POINT('',(-7.26056394836,-2.1079334227,3.)); +#1224 = CARTESIAN_POINT('',(-7.42688130669,-1.36969623529,3.)); +#1225 = CARTESIAN_POINT('',(-7.5,-0.662348936385,3.)); +#1226 = CARTESIAN_POINT('',(-7.5,-6.66133814775E-016,3.)); +#1227 = PCURVE('',#1228,#1233); +#1228 = PLANE('',#1229); +#1229 = AXIS2_PLACEMENT_3D('',#1230,#1231,#1232); +#1230 = CARTESIAN_POINT('',(0.E+000,0.E+000,3.)); +#1231 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1232 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#1233 = DEFINITIONAL_REPRESENTATION('',(#1234),#1259); +#1234 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1235,#1236,#1237,#1238,#1239, + #1240,#1241,#1242,#1243,#1244,#1245,#1246,#1247,#1248,#1249,#1250, + #1251,#1252,#1253,#1254,#1255,#1256,#1257,#1258),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,5.20225778542,9.84158873828, + 14.2673349509,18.6433186512,23.0548848731,27.6530164185, + 33.5425690087),.UNSPECIFIED.); +#1235 = CARTESIAN_POINT('',(7.5,6.66133814775E-016)); +#1236 = CARTESIAN_POINT('',(7.5,-0.585054612929)); +#1237 = CARTESIAN_POINT('',(7.44295106424,-1.20521801478)); +#1238 = CARTESIAN_POINT('',(7.31515940691,-1.85033890984)); +#1239 = CARTESIAN_POINT('',(6.9174836202,-3.08527233291)); +#1240 = CARTESIAN_POINT('',(6.21610886075,-4.27235963842)); +#1241 = CARTESIAN_POINT('',(5.81621499215,-4.80660561995)); +#1242 = CARTESIAN_POINT('',(4.90603051399,-5.77088806315)); +#1243 = CARTESIAN_POINT('',(3.775988505,-6.53134212728)); +#1244 = CARTESIAN_POINT('',(3.1790299248,-6.8428729705)); +#1245 = CARTESIAN_POINT('',(1.92404155108,-7.32665470362)); +#1246 = CARTESIAN_POINT('',(0.582116172098,-7.52278240149)); +#1247 = CARTESIAN_POINT('',(-9.46313364034E-002,-7.54474978799)); +#1248 = CARTESIAN_POINT('',(-1.44588275644,-7.43589277948)); +#1249 = CARTESIAN_POINT('',(-2.73149765405,-7.03353365966)); +#1250 = CARTESIAN_POINT('',(-3.34804882139,-6.76091512264)); +#1251 = CARTESIAN_POINT('',(-4.52434338626,-6.07498368569)); +#1252 = CARTESIAN_POINT('',(-5.49752166125,-5.16815745669)); +#1253 = CARTESIAN_POINT('',(-5.93188641726,-4.6595782538)); +#1254 = CARTESIAN_POINT('',(-6.76982690894,-3.42768019481)); +#1255 = CARTESIAN_POINT('',(-7.26056394836,-2.1079334227)); +#1256 = CARTESIAN_POINT('',(-7.42688130669,-1.36969623529)); +#1257 = CARTESIAN_POINT('',(-7.5,-0.662348936385)); +#1258 = CARTESIAN_POINT('',(-7.5,-6.66133814775E-016)); +#1259 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1260 = PCURVE('',#1261,#1270); +#1261 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#1262,#1263,#1264,#1265) + ,(#1266,#1267,#1268,#1269 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,3.00099800399),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#1262 = CARTESIAN_POINT('',(-7.5,0.E+000,3.)); +#1263 = CARTESIAN_POINT('',(-7.5,-15.,3.)); +#1264 = CARTESIAN_POINT('',(7.5,-15.,3.)); +#1265 = CARTESIAN_POINT('',(7.5,0.E+000,3.)); +#1266 = CARTESIAN_POINT('',(-7.5,0.E+000,0.E+000)); +#1267 = CARTESIAN_POINT('',(-7.5,-15.,0.E+000)); +#1268 = CARTESIAN_POINT('',(7.5,-15.,0.E+000)); +#1269 = CARTESIAN_POINT('',(7.5,0.E+000,0.E+000)); +#1270 = DEFINITIONAL_REPRESENTATION('',(#1271),#1319); +#1271 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#1272,#1273,#1274,#1275,#1276, + #1277,#1278,#1279,#1280,#1281,#1282,#1283,#1284,#1285,#1286,#1287, + #1288,#1289,#1290,#1291,#1292,#1293,#1294,#1295,#1296,#1297,#1298, + #1299,#1300,#1301,#1302,#1303,#1304,#1305,#1306,#1307,#1308,#1309, + #1310,#1311,#1312,#1313,#1314,#1315,#1316,#1317,#1318), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.762331113834, + 1.524662227668,2.286993341502,3.049324455336,3.81165556917, + 4.573986683005,5.336317796839,6.098648910673,6.860980024507, + 7.623311138341,8.385642252175,9.147973366009,9.910304479843, + 10.672635593677,11.434966707511,12.197297821345,12.95962893518, + 13.721960049014,14.484291162848,15.246622276682,16.008953390516, + 16.77128450435,17.533615618184,18.295946732018,19.058277845852, + 19.820608959686,20.58294007352,21.345271187355,22.107602301189, + 22.869933415023,23.632264528857,24.394595642691,25.156926756525, + 25.919257870359,26.681588984193,27.443920098027,28.206251211861, + 28.968582325695,29.73091343953,30.493244553364,31.255575667198, + 32.017906781032,32.780237894866,33.5425690087), + .QUASI_UNIFORM_KNOTS.); +#1272 = CARTESIAN_POINT('',(9.9800399E-004,45.)); +#1273 = CARTESIAN_POINT('',(9.980039900001E-004,44.571302812759)); +#1274 = CARTESIAN_POINT('',(9.980039900001E-004,43.723451988301)); +#1275 = CARTESIAN_POINT('',(9.980039899997E-004,42.480603180286)); +#1276 = CARTESIAN_POINT('',(9.980039899987E-004,41.267127064423)); +#1277 = CARTESIAN_POINT('',(9.980039900005E-004,40.082949207123)); +#1278 = CARTESIAN_POINT('',(9.980039899997E-004,38.92770430726)); +#1279 = CARTESIAN_POINT('',(9.980039899987E-004,37.800756852125)); +#1280 = CARTESIAN_POINT('',(9.980039900008E-004,36.701299976325)); +#1281 = CARTESIAN_POINT('',(9.980039899991E-004,35.628440627625)); +#1282 = CARTESIAN_POINT('',(9.980039900013E-004,34.580978071595)); +#1283 = CARTESIAN_POINT('',(9.980039899994E-004,33.557472237094)); +#1284 = CARTESIAN_POINT('',(9.980039899998E-004,32.556310364454)); +#1285 = CARTESIAN_POINT('',(9.980039900001E-004,31.575759692059)); +#1286 = CARTESIAN_POINT('',(9.980039900011E-004,30.614017309608)); +#1287 = CARTESIAN_POINT('',(9.980039899995E-004,29.6692735353)); +#1288 = CARTESIAN_POINT('',(9.980039899997E-004,28.739730155524)); +#1289 = CARTESIAN_POINT('',(9.980039900007E-004,27.82355261073)); +#1290 = CARTESIAN_POINT('',(9.980039899995E-004,26.918879220695)); +#1291 = CARTESIAN_POINT('',(9.980039900007E-004,26.023811406403)); +#1292 = CARTESIAN_POINT('',(9.980039899997E-004,25.136388793607)); +#1293 = CARTESIAN_POINT('',(9.980039900002E-004,24.254616243117)); +#1294 = CARTESIAN_POINT('',(9.980039899993E-004,23.376593359876)); +#1295 = CARTESIAN_POINT('',(9.980039899997E-004,22.500427783925)); +#1296 = CARTESIAN_POINT('',(9.980039899991E-004,21.624247365846)); +#1297 = CARTESIAN_POINT('',(9.980039900012E-004,20.74618278857)); +#1298 = CARTESIAN_POINT('',(9.980039899988E-004,19.864397566237)); +#1299 = CARTESIAN_POINT('',(9.980039900012E-004,18.976941798027)); +#1300 = CARTESIAN_POINT('',(9.980039899995E-004,18.081820706376)); +#1301 = CARTESIAN_POINT('',(9.980039900011E-004,17.17711381209)); +#1302 = CARTESIAN_POINT('',(9.980039899992E-004,16.260927030417)); +#1303 = CARTESIAN_POINT('',(9.980039900002E-004,15.331390617179)); +#1304 = CARTESIAN_POINT('',(9.980039900008E-004,14.386646151192)); +#1305 = CARTESIAN_POINT('',(9.9800399E-004,13.424926609852)); +#1306 = CARTESIAN_POINT('',(9.980039900002E-004,12.444427651184)); +#1307 = CARTESIAN_POINT('',(9.980039900002E-004,11.443331536935)); +#1308 = CARTESIAN_POINT('',(9.980039900002E-004,10.419877046088)); +#1309 = CARTESIAN_POINT('',(9.980039900002E-004,9.372427008604)); +#1310 = CARTESIAN_POINT('',(9.980039900004E-004,8.299579036962)); +#1311 = CARTESIAN_POINT('',(9.980039899994E-004,7.200183660574)); +#1312 = CARTESIAN_POINT('',(9.980039900006E-004,6.07319337542)); +#1313 = CARTESIAN_POINT('',(9.980039899995E-004,4.917761146069)); +#1314 = CARTESIAN_POINT('',(9.980039900001E-004,3.733303759495)); +#1315 = CARTESIAN_POINT('',(9.980039899989E-004,2.519557037946)); +#1316 = CARTESIAN_POINT('',(9.980039900006E-004,1.276559770167)); +#1317 = CARTESIAN_POINT('',(9.980039900006E-004,0.428685598944)); +#1318 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#1319 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1320 = ORIENTED_EDGE('',*,*,#1321,.F.); +#1321 = EDGE_CURVE('',#1199,#1197,#1322,.T.); +#1322 = SURFACE_CURVE('',#1323,(#1348,#1376),.PCURVE_S1.); +#1323 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1324,#1325,#1326,#1327,#1328, + #1329,#1330,#1331,#1332,#1333,#1334,#1335,#1336,#1337,#1338,#1339, + #1340,#1341,#1342,#1343,#1344,#1345,#1346,#1347),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,5.20225778542,9.84158873828, + 14.2673349509,18.6433186512,23.0548848731,27.6530164185, + 33.5425690087),.UNSPECIFIED.); +#1324 = CARTESIAN_POINT('',(-7.5,-6.66133814775E-016,3.)); +#1325 = CARTESIAN_POINT('',(-7.5,0.585054612929,3.)); +#1326 = CARTESIAN_POINT('',(-7.44295106424,1.20521801478,3.)); +#1327 = CARTESIAN_POINT('',(-7.31515940691,1.85033890984,3.)); +#1328 = CARTESIAN_POINT('',(-6.9174836202,3.08527233291,3.)); +#1329 = CARTESIAN_POINT('',(-6.21610886075,4.27235963842,3.)); +#1330 = CARTESIAN_POINT('',(-5.81621499215,4.80660561995,3.)); +#1331 = CARTESIAN_POINT('',(-4.90603051399,5.77088806315,3.)); +#1332 = CARTESIAN_POINT('',(-3.775988505,6.53134212728,3.)); +#1333 = CARTESIAN_POINT('',(-3.1790299248,6.8428729705,3.)); +#1334 = CARTESIAN_POINT('',(-1.92404155108,7.32665470362,3.)); +#1335 = CARTESIAN_POINT('',(-0.582116172098,7.52278240149,3.)); +#1336 = CARTESIAN_POINT('',(9.46313364034E-002,7.54474978799,3.)); +#1337 = CARTESIAN_POINT('',(1.44588275644,7.43589277948,3.)); +#1338 = CARTESIAN_POINT('',(2.73149765405,7.03353365966,3.)); +#1339 = CARTESIAN_POINT('',(3.34804882139,6.76091512264,3.)); +#1340 = CARTESIAN_POINT('',(4.52434338626,6.07498368569,3.)); +#1341 = CARTESIAN_POINT('',(5.49752166125,5.16815745669,3.)); +#1342 = CARTESIAN_POINT('',(5.93188641726,4.6595782538,3.)); +#1343 = CARTESIAN_POINT('',(6.76982690894,3.42768019481,3.)); +#1344 = CARTESIAN_POINT('',(7.26056394836,2.1079334227,3.)); +#1345 = CARTESIAN_POINT('',(7.42688130669,1.36969623529,3.)); +#1346 = CARTESIAN_POINT('',(7.5,0.662348936385,3.)); +#1347 = CARTESIAN_POINT('',(7.5,6.66133814775E-016,3.)); +#1348 = PCURVE('',#1228,#1349); +#1349 = DEFINITIONAL_REPRESENTATION('',(#1350),#1375); +#1350 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1351,#1352,#1353,#1354,#1355, + #1356,#1357,#1358,#1359,#1360,#1361,#1362,#1363,#1364,#1365,#1366, + #1367,#1368,#1369,#1370,#1371,#1372,#1373,#1374),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,5.20225778542,9.84158873828, + 14.2673349509,18.6433186512,23.0548848731,27.6530164185, + 33.5425690087),.UNSPECIFIED.); +#1351 = CARTESIAN_POINT('',(-7.5,-6.66133814775E-016)); +#1352 = CARTESIAN_POINT('',(-7.5,0.585054612929)); +#1353 = CARTESIAN_POINT('',(-7.44295106424,1.20521801478)); +#1354 = CARTESIAN_POINT('',(-7.31515940691,1.85033890984)); +#1355 = CARTESIAN_POINT('',(-6.9174836202,3.08527233291)); +#1356 = CARTESIAN_POINT('',(-6.21610886075,4.27235963842)); +#1357 = CARTESIAN_POINT('',(-5.81621499215,4.80660561995)); +#1358 = CARTESIAN_POINT('',(-4.90603051399,5.77088806315)); +#1359 = CARTESIAN_POINT('',(-3.775988505,6.53134212728)); +#1360 = CARTESIAN_POINT('',(-3.1790299248,6.8428729705)); +#1361 = CARTESIAN_POINT('',(-1.92404155108,7.32665470362)); +#1362 = CARTESIAN_POINT('',(-0.582116172098,7.52278240149)); +#1363 = CARTESIAN_POINT('',(9.46313364034E-002,7.54474978799)); +#1364 = CARTESIAN_POINT('',(1.44588275644,7.43589277948)); +#1365 = CARTESIAN_POINT('',(2.73149765405,7.03353365966)); +#1366 = CARTESIAN_POINT('',(3.34804882139,6.76091512264)); +#1367 = CARTESIAN_POINT('',(4.52434338626,6.07498368569)); +#1368 = CARTESIAN_POINT('',(5.49752166125,5.16815745669)); +#1369 = CARTESIAN_POINT('',(5.93188641726,4.6595782538)); +#1370 = CARTESIAN_POINT('',(6.76982690894,3.42768019481)); +#1371 = CARTESIAN_POINT('',(7.26056394836,2.1079334227)); +#1372 = CARTESIAN_POINT('',(7.42688130669,1.36969623529)); +#1373 = CARTESIAN_POINT('',(7.5,0.662348936385)); +#1374 = CARTESIAN_POINT('',(7.5,6.66133814775E-016)); +#1375 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1376 = PCURVE('',#1377,#1386); +#1377 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#1378,#1379,#1380,#1381) + ,(#1382,#1383,#1384,#1385 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,3.00099800399),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#1378 = CARTESIAN_POINT('',(7.5,0.E+000,3.)); +#1379 = CARTESIAN_POINT('',(7.5,15.,3.)); +#1380 = CARTESIAN_POINT('',(-7.5,15.,3.)); +#1381 = CARTESIAN_POINT('',(-7.5,0.E+000,3.)); +#1382 = CARTESIAN_POINT('',(7.5,0.E+000,0.E+000)); +#1383 = CARTESIAN_POINT('',(7.5,15.,0.E+000)); +#1384 = CARTESIAN_POINT('',(-7.5,15.,0.E+000)); +#1385 = CARTESIAN_POINT('',(-7.5,0.E+000,0.E+000)); +#1386 = DEFINITIONAL_REPRESENTATION('',(#1387),#1435); +#1387 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#1388,#1389,#1390,#1391,#1392, + #1393,#1394,#1395,#1396,#1397,#1398,#1399,#1400,#1401,#1402,#1403, + #1404,#1405,#1406,#1407,#1408,#1409,#1410,#1411,#1412,#1413,#1414, + #1415,#1416,#1417,#1418,#1419,#1420,#1421,#1422,#1423,#1424,#1425, + #1426,#1427,#1428,#1429,#1430,#1431,#1432,#1433,#1434), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.762331113834, + 1.524662227668,2.286993341502,3.049324455336,3.81165556917, + 4.573986683005,5.336317796839,6.098648910673,6.860980024507, + 7.623311138341,8.385642252175,9.147973366009,9.910304479843, + 10.672635593677,11.434966707511,12.197297821345,12.95962893518, + 13.721960049014,14.484291162848,15.246622276682,16.008953390516, + 16.77128450435,17.533615618184,18.295946732018,19.058277845852, + 19.820608959686,20.58294007352,21.345271187355,22.107602301189, + 22.869933415023,23.632264528857,24.394595642691,25.156926756525, + 25.919257870359,26.681588984193,27.443920098027,28.206251211861, + 28.968582325695,29.73091343953,30.493244553364,31.255575667198, + 32.017906781032,32.780237894866,33.5425690087), + .QUASI_UNIFORM_KNOTS.); +#1388 = CARTESIAN_POINT('',(9.9800399E-004,45.)); +#1389 = CARTESIAN_POINT('',(9.980039900001E-004,44.571302812759)); +#1390 = CARTESIAN_POINT('',(9.980039900001E-004,43.723451988301)); +#1391 = CARTESIAN_POINT('',(9.980039899997E-004,42.480603180286)); +#1392 = CARTESIAN_POINT('',(9.980039899987E-004,41.267127064423)); +#1393 = CARTESIAN_POINT('',(9.980039900005E-004,40.082949207123)); +#1394 = CARTESIAN_POINT('',(9.980039899997E-004,38.92770430726)); +#1395 = CARTESIAN_POINT('',(9.980039899987E-004,37.800756852125)); +#1396 = CARTESIAN_POINT('',(9.980039900008E-004,36.701299976325)); +#1397 = CARTESIAN_POINT('',(9.980039899991E-004,35.628440627625)); +#1398 = CARTESIAN_POINT('',(9.980039900013E-004,34.580978071595)); +#1399 = CARTESIAN_POINT('',(9.980039899994E-004,33.557472237094)); +#1400 = CARTESIAN_POINT('',(9.980039899998E-004,32.556310364454)); +#1401 = CARTESIAN_POINT('',(9.980039900001E-004,31.575759692059)); +#1402 = CARTESIAN_POINT('',(9.980039900011E-004,30.614017309608)); +#1403 = CARTESIAN_POINT('',(9.980039899995E-004,29.6692735353)); +#1404 = CARTESIAN_POINT('',(9.980039899997E-004,28.739730155524)); +#1405 = CARTESIAN_POINT('',(9.980039900007E-004,27.82355261073)); +#1406 = CARTESIAN_POINT('',(9.980039899995E-004,26.918879220695)); +#1407 = CARTESIAN_POINT('',(9.980039900007E-004,26.023811406403)); +#1408 = CARTESIAN_POINT('',(9.980039899997E-004,25.136388793607)); +#1409 = CARTESIAN_POINT('',(9.980039900002E-004,24.254616243117)); +#1410 = CARTESIAN_POINT('',(9.980039899993E-004,23.376593359876)); +#1411 = CARTESIAN_POINT('',(9.980039899997E-004,22.500427783925)); +#1412 = CARTESIAN_POINT('',(9.980039899991E-004,21.624247365846)); +#1413 = CARTESIAN_POINT('',(9.980039900012E-004,20.74618278857)); +#1414 = CARTESIAN_POINT('',(9.980039899988E-004,19.864397566237)); +#1415 = CARTESIAN_POINT('',(9.980039900012E-004,18.976941798027)); +#1416 = CARTESIAN_POINT('',(9.980039899995E-004,18.081820706376)); +#1417 = CARTESIAN_POINT('',(9.980039900011E-004,17.17711381209)); +#1418 = CARTESIAN_POINT('',(9.980039899992E-004,16.260927030417)); +#1419 = CARTESIAN_POINT('',(9.980039900002E-004,15.331390617179)); +#1420 = CARTESIAN_POINT('',(9.980039900008E-004,14.386646151192)); +#1421 = CARTESIAN_POINT('',(9.9800399E-004,13.424926609852)); +#1422 = CARTESIAN_POINT('',(9.980039900002E-004,12.444427651184)); +#1423 = CARTESIAN_POINT('',(9.980039900002E-004,11.443331536935)); +#1424 = CARTESIAN_POINT('',(9.980039900002E-004,10.419877046088)); +#1425 = CARTESIAN_POINT('',(9.980039900002E-004,9.372427008604)); +#1426 = CARTESIAN_POINT('',(9.980039900004E-004,8.299579036962)); +#1427 = CARTESIAN_POINT('',(9.980039899994E-004,7.200183660574)); +#1428 = CARTESIAN_POINT('',(9.980039900006E-004,6.07319337542)); +#1429 = CARTESIAN_POINT('',(9.980039899995E-004,4.917761146069)); +#1430 = CARTESIAN_POINT('',(9.980039900001E-004,3.733303759495)); +#1431 = CARTESIAN_POINT('',(9.980039899989E-004,2.519557037946)); +#1432 = CARTESIAN_POINT('',(9.980039900006E-004,1.276559770167)); +#1433 = CARTESIAN_POINT('',(9.980039900006E-004,0.428685598944)); +#1434 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#1435 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1436 = FACE_BOUND('',#1437,.T.); +#1437 = EDGE_LOOP('',(#1438,#1558)); +#1438 = ORIENTED_EDGE('',*,*,#1439,.F.); +#1439 = EDGE_CURVE('',#1440,#1442,#1444,.T.); +#1440 = VERTEX_POINT('',#1441); +#1441 = CARTESIAN_POINT('',(-5.,2.22044604925E-016,3.)); +#1442 = VERTEX_POINT('',#1443); +#1443 = CARTESIAN_POINT('',(5.,-2.22044604925E-016,3.)); +#1444 = SURFACE_CURVE('',#1445,(#1470,#1498),.PCURVE_S1.); +#1445 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1446,#1447,#1448,#1449,#1450, + #1451,#1452,#1453,#1454,#1455,#1456,#1457,#1458,#1459,#1460,#1461, + #1462,#1463,#1464,#1465,#1466,#1467,#1468,#1469),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164387,7.85828164661, + 10.7238180516,13.5836589935,16.4911855015,20.38776087,22.3658107353) + ,.UNSPECIFIED.); +#1446 = CARTESIAN_POINT('',(-5.,-2.22044604925E-016,3.)); +#1447 = CARTESIAN_POINT('',(-5.,-0.467198252312,3.)); +#1448 = CARTESIAN_POINT('',(-4.94543032016,-0.967985463874,3.)); +#1449 = CARTESIAN_POINT('',(-4.82041774119,-1.49112303535,3.)); +#1450 = CARTESIAN_POINT('',(-4.42731387443,-2.48006143438,3.)); +#1451 = CARTESIAN_POINT('',(-3.74198536382,-3.38090473983,3.)); +#1452 = CARTESIAN_POINT('',(-3.35476380665,-3.76862633308,3.)); +#1453 = CARTESIAN_POINT('',(-2.56749137395,-4.36208802884,3.)); +#1454 = CARTESIAN_POINT('',(-1.64518926245,-4.75184036526,3.)); +#1455 = CARTESIAN_POINT('',(-1.22322144323,-4.87791933608,3.)); +#1456 = CARTESIAN_POINT('',(-0.356287037014,-5.03548099138,3.)); +#1457 = CARTESIAN_POINT('',(0.52640030158,-5.00140076198,3.)); +#1458 = CARTESIAN_POINT('',(0.963050674765,-4.93574856594,3.)); +#1459 = CARTESIAN_POINT('',(1.81864212033,-4.70884578804,3.)); +#1460 = CARTESIAN_POINT('',(2.59575461931,-4.30713067084,3.)); +#1461 = CARTESIAN_POINT('',(2.9603131848,-4.06421908239,3.)); +#1462 = CARTESIAN_POINT('',(3.73554903634,-3.41630129394,3.)); +#1463 = CARTESIAN_POINT('',(4.3095225984,-2.62465565461,3.)); +#1464 = CARTESIAN_POINT('',(4.56375002186,-2.14244819995,3.)); +#1465 = CARTESIAN_POINT('',(4.8362924348,-1.40481893471,3.)); +#1466 = CARTESIAN_POINT('',(4.96121877006,-0.68885510118,3.)); +#1467 = CARTESIAN_POINT('',(4.98763322877,-0.452431376999,3.)); +#1468 = CARTESIAN_POINT('',(5.,-0.222409665749,3.)); +#1469 = CARTESIAN_POINT('',(5.,4.4408920985E-016,3.)); +#1470 = PCURVE('',#1228,#1471); +#1471 = DEFINITIONAL_REPRESENTATION('',(#1472),#1497); +#1472 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1473,#1474,#1475,#1476,#1477, + #1478,#1479,#1480,#1481,#1482,#1483,#1484,#1485,#1486,#1487,#1488, + #1489,#1490,#1491,#1492,#1493,#1494,#1495,#1496),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164387,7.85828164661, + 10.7238180516,13.5836589935,16.4911855015,20.38776087,22.3658107353) + ,.UNSPECIFIED.); +#1473 = CARTESIAN_POINT('',(-5.,-2.22044604925E-016)); +#1474 = CARTESIAN_POINT('',(-5.,-0.467198252312)); +#1475 = CARTESIAN_POINT('',(-4.94543032016,-0.967985463874)); +#1476 = CARTESIAN_POINT('',(-4.82041774119,-1.49112303535)); +#1477 = CARTESIAN_POINT('',(-4.42731387443,-2.48006143438)); +#1478 = CARTESIAN_POINT('',(-3.74198536382,-3.38090473983)); +#1479 = CARTESIAN_POINT('',(-3.35476380665,-3.76862633308)); +#1480 = CARTESIAN_POINT('',(-2.56749137395,-4.36208802884)); +#1481 = CARTESIAN_POINT('',(-1.64518926245,-4.75184036526)); +#1482 = CARTESIAN_POINT('',(-1.22322144323,-4.87791933608)); +#1483 = CARTESIAN_POINT('',(-0.356287037014,-5.03548099138)); +#1484 = CARTESIAN_POINT('',(0.52640030158,-5.00140076198)); +#1485 = CARTESIAN_POINT('',(0.963050674765,-4.93574856594)); +#1486 = CARTESIAN_POINT('',(1.81864212033,-4.70884578804)); +#1487 = CARTESIAN_POINT('',(2.59575461931,-4.30713067084)); +#1488 = CARTESIAN_POINT('',(2.9603131848,-4.06421908239)); +#1489 = CARTESIAN_POINT('',(3.73554903634,-3.41630129394)); +#1490 = CARTESIAN_POINT('',(4.3095225984,-2.62465565461)); +#1491 = CARTESIAN_POINT('',(4.56375002186,-2.14244819995)); +#1492 = CARTESIAN_POINT('',(4.8362924348,-1.40481893471)); +#1493 = CARTESIAN_POINT('',(4.96121877006,-0.68885510118)); +#1494 = CARTESIAN_POINT('',(4.98763322877,-0.452431376999)); +#1495 = CARTESIAN_POINT('',(5.,-0.222409665749)); +#1496 = CARTESIAN_POINT('',(5.,4.4408920985E-016)); +#1497 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1498 = PCURVE('',#1499,#1508); +#1499 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#1500,#1501,#1502,#1503) + ,(#1504,#1505,#1506,#1507 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,34.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#1500 = CARTESIAN_POINT('',(-5.,0.E+000,37.)); +#1501 = CARTESIAN_POINT('',(-5.,-10.,37.)); +#1502 = CARTESIAN_POINT('',(5.,-10.,37.)); +#1503 = CARTESIAN_POINT('',(5.,0.E+000,37.)); +#1504 = CARTESIAN_POINT('',(-5.,0.E+000,3.)); +#1505 = CARTESIAN_POINT('',(-5.,-10.,3.)); +#1506 = CARTESIAN_POINT('',(5.,-10.,3.)); +#1507 = CARTESIAN_POINT('',(5.,0.E+000,3.)); +#1508 = DEFINITIONAL_REPRESENTATION('',(#1509),#1557); +#1509 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#1510,#1511,#1512,#1513,#1514, + #1515,#1516,#1517,#1518,#1519,#1520,#1521,#1522,#1523,#1524,#1525, + #1526,#1527,#1528,#1529,#1530,#1531,#1532,#1533,#1534,#1535,#1536, + #1537,#1538,#1539,#1540,#1541,#1542,#1543,#1544,#1545,#1546,#1547, + #1548,#1549,#1550,#1551,#1552,#1553,#1554,#1555,#1556), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880348, + 1.016627760695,1.524941641043,2.033255521391,2.541569401739, + 3.049883282086,3.558197162434,4.066511042782,4.57482492313, + 5.083138803477,5.591452683825,6.099766564173,6.60808044452, + 7.116394324868,7.624708205216,8.133022085564,8.641335965911, + 9.149649846259,9.657963726607,10.166277606955,10.674591487302, + 11.18290536765,11.691219247998,12.199533128345,12.707847008693, + 13.216160889041,13.724474769389,14.232788649736,14.741102530084, + 15.249416410432,15.75773029078,16.266044171127,16.774358051475, + 17.282671931823,17.79098581217,18.299299692518,18.807613572866, + 19.315927453214,19.824241333561,20.332555213909,20.840869094257, + 21.349182974605,21.857496854952,22.3658107353), + .QUASI_UNIFORM_KNOTS.); +#1510 = CARTESIAN_POINT('',(34.000998004,0.E+000)); +#1511 = CARTESIAN_POINT('',(34.000998004,0.285786134005)); +#1512 = CARTESIAN_POINT('',(34.000998004,0.851023724374)); +#1513 = CARTESIAN_POINT('',(34.000998004,1.679658950067)); +#1514 = CARTESIAN_POINT('',(34.000998004,2.488775842984)); +#1515 = CARTESIAN_POINT('',(34.000998004,3.278357391147)); +#1516 = CARTESIAN_POINT('',(34.000998004,4.048590090635)); +#1517 = CARTESIAN_POINT('',(34.000998004,4.799873551245)); +#1518 = CARTESIAN_POINT('',(34.000998004,5.532780976198)); +#1519 = CARTESIAN_POINT('',(34.000998004,6.248020911162)); +#1520 = CARTESIAN_POINT('',(34.000998004,6.946360574942)); +#1521 = CARTESIAN_POINT('',(34.000998004,7.628688635561)); +#1522 = CARTESIAN_POINT('',(34.000998004,8.296073973845)); +#1523 = CARTESIAN_POINT('',(34.000998004,8.949683945325)); +#1524 = CARTESIAN_POINT('',(34.000998004,9.590744783224)); +#1525 = CARTESIAN_POINT('',(34.000998004,10.220499189069)); +#1526 = CARTESIAN_POINT('',(34.000998004,10.840182524178)); +#1527 = CARTESIAN_POINT('',(34.000998004,11.450961995563)); +#1528 = CARTESIAN_POINT('',(34.000998004,12.054057836488)); +#1529 = CARTESIAN_POINT('',(34.000998004,12.650784955207)); +#1530 = CARTESIAN_POINT('',(34.000998004,13.242437006931)); +#1531 = CARTESIAN_POINT('',(34.000998004,13.830311319039)); +#1532 = CARTESIAN_POINT('',(34.000998004,14.415700442447)); +#1533 = CARTESIAN_POINT('',(34.000998004,14.999897615114)); +#1534 = CARTESIAN_POINT('',(34.000998004,15.584089013839)); +#1535 = CARTESIAN_POINT('',(34.000998004,16.169496123896)); +#1536 = CARTESIAN_POINT('',(34.000998004,16.757374014315)); +#1537 = CARTESIAN_POINT('',(34.000998004,17.349001920563)); +#1538 = CARTESIAN_POINT('',(34.000998004,17.945677529625)); +#1539 = CARTESIAN_POINT('',(34.000998004,18.548712223709)); +#1540 = CARTESIAN_POINT('',(34.000998004,19.159406299853)); +#1541 = CARTESIAN_POINT('',(34.000998004,19.779034544783)); +#1542 = CARTESIAN_POINT('',(34.000998004,20.40884411557)); +#1543 = CARTESIAN_POINT('',(34.000998004,21.05005071958)); +#1544 = CARTESIAN_POINT('',(34.000998004,21.703821244264)); +#1545 = CARTESIAN_POINT('',(34.000998004,22.371286811436)); +#1546 = CARTESIAN_POINT('',(34.000998004,23.053580536272)); +#1547 = CARTESIAN_POINT('',(34.000998004,23.751780892279)); +#1548 = CARTESIAN_POINT('',(34.000998004,24.466876470872)); +#1549 = CARTESIAN_POINT('',(34.000998004,25.199732655413)); +#1550 = CARTESIAN_POINT('',(34.000998004,25.951064420944)); +#1551 = CARTESIAN_POINT('',(34.000998004,26.721413688722)); +#1552 = CARTESIAN_POINT('',(34.000998004,27.511129456935)); +#1553 = CARTESIAN_POINT('',(34.000998004,28.320321955904)); +#1554 = CARTESIAN_POINT('',(34.000998004,29.148977248348)); +#1555 = CARTESIAN_POINT('',(34.000998004,29.714213802924)); +#1556 = CARTESIAN_POINT('',(34.000998004,30.)); +#1557 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1558 = ORIENTED_EDGE('',*,*,#1559,.F.); +#1559 = EDGE_CURVE('',#1442,#1440,#1560,.T.); +#1560 = SURFACE_CURVE('',#1561,(#1586,#1614),.PCURVE_S1.); +#1561 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1562,#1563,#1564,#1565,#1566, + #1567,#1568,#1569,#1570,#1571,#1572,#1573,#1574,#1575,#1576,#1577, + #1578,#1579,#1580,#1581,#1582,#1583,#1584,#1585),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164387,7.85828164661, + 10.7238180516,13.5836589935,16.4911855015,20.38776087,22.3658107353) + ,.UNSPECIFIED.); +#1562 = CARTESIAN_POINT('',(5.,2.22044604925E-016,3.)); +#1563 = CARTESIAN_POINT('',(5.,0.467198252312,3.)); +#1564 = CARTESIAN_POINT('',(4.94543032016,0.967985463874,3.)); +#1565 = CARTESIAN_POINT('',(4.82041774119,1.49112303535,3.)); +#1566 = CARTESIAN_POINT('',(4.42731387443,2.48006143438,3.)); +#1567 = CARTESIAN_POINT('',(3.74198536382,3.38090473983,3.)); +#1568 = CARTESIAN_POINT('',(3.35476380665,3.76862633308,3.)); +#1569 = CARTESIAN_POINT('',(2.56749137395,4.36208802884,3.)); +#1570 = CARTESIAN_POINT('',(1.64518926245,4.75184036526,3.)); +#1571 = CARTESIAN_POINT('',(1.22322144323,4.87791933608,3.)); +#1572 = CARTESIAN_POINT('',(0.356287037014,5.03548099138,3.)); +#1573 = CARTESIAN_POINT('',(-0.52640030158,5.00140076198,3.)); +#1574 = CARTESIAN_POINT('',(-0.963050674765,4.93574856594,3.)); +#1575 = CARTESIAN_POINT('',(-1.81864212033,4.70884578804,3.)); +#1576 = CARTESIAN_POINT('',(-2.59575461931,4.30713067084,3.)); +#1577 = CARTESIAN_POINT('',(-2.9603131848,4.06421908239,3.)); +#1578 = CARTESIAN_POINT('',(-3.73554903634,3.41630129394,3.)); +#1579 = CARTESIAN_POINT('',(-4.3095225984,2.62465565461,3.)); +#1580 = CARTESIAN_POINT('',(-4.56375002186,2.14244819995,3.)); +#1581 = CARTESIAN_POINT('',(-4.8362924348,1.40481893471,3.)); +#1582 = CARTESIAN_POINT('',(-4.96121877006,0.68885510118,3.)); +#1583 = CARTESIAN_POINT('',(-4.98763322877,0.452431376999,3.)); +#1584 = CARTESIAN_POINT('',(-5.,0.222409665749,3.)); +#1585 = CARTESIAN_POINT('',(-5.,-4.4408920985E-016,3.)); +#1586 = PCURVE('',#1228,#1587); +#1587 = DEFINITIONAL_REPRESENTATION('',(#1588),#1613); +#1588 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#1589,#1590,#1591,#1592,#1593, + #1594,#1595,#1596,#1597,#1598,#1599,#1600,#1601,#1602,#1603,#1604, + #1605,#1606,#1607,#1608,#1609,#1610,#1611,#1612),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164387,7.85828164661, + 10.7238180516,13.5836589935,16.4911855015,20.38776087,22.3658107353) + ,.UNSPECIFIED.); +#1589 = CARTESIAN_POINT('',(5.,2.22044604925E-016)); +#1590 = CARTESIAN_POINT('',(5.,0.467198252312)); +#1591 = CARTESIAN_POINT('',(4.94543032016,0.967985463874)); +#1592 = CARTESIAN_POINT('',(4.82041774119,1.49112303535)); +#1593 = CARTESIAN_POINT('',(4.42731387443,2.48006143438)); +#1594 = CARTESIAN_POINT('',(3.74198536382,3.38090473983)); +#1595 = CARTESIAN_POINT('',(3.35476380665,3.76862633308)); +#1596 = CARTESIAN_POINT('',(2.56749137395,4.36208802884)); +#1597 = CARTESIAN_POINT('',(1.64518926245,4.75184036526)); +#1598 = CARTESIAN_POINT('',(1.22322144323,4.87791933608)); +#1599 = CARTESIAN_POINT('',(0.356287037014,5.03548099138)); +#1600 = CARTESIAN_POINT('',(-0.52640030158,5.00140076198)); +#1601 = CARTESIAN_POINT('',(-0.963050674765,4.93574856594)); +#1602 = CARTESIAN_POINT('',(-1.81864212033,4.70884578804)); +#1603 = CARTESIAN_POINT('',(-2.59575461931,4.30713067084)); +#1604 = CARTESIAN_POINT('',(-2.9603131848,4.06421908239)); +#1605 = CARTESIAN_POINT('',(-3.73554903634,3.41630129394)); +#1606 = CARTESIAN_POINT('',(-4.3095225984,2.62465565461)); +#1607 = CARTESIAN_POINT('',(-4.56375002186,2.14244819995)); +#1608 = CARTESIAN_POINT('',(-4.8362924348,1.40481893471)); +#1609 = CARTESIAN_POINT('',(-4.96121877006,0.68885510118)); +#1610 = CARTESIAN_POINT('',(-4.98763322877,0.452431376999)); +#1611 = CARTESIAN_POINT('',(-5.,0.222409665749)); +#1612 = CARTESIAN_POINT('',(-5.,-4.4408920985E-016)); +#1613 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1614 = PCURVE('',#1615,#1624); +#1615 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#1616,#1617,#1618,#1619) + ,(#1620,#1621,#1622,#1623 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,34.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#1616 = CARTESIAN_POINT('',(5.,0.E+000,37.)); +#1617 = CARTESIAN_POINT('',(5.,10.,37.)); +#1618 = CARTESIAN_POINT('',(-5.,10.,37.)); +#1619 = CARTESIAN_POINT('',(-5.,0.E+000,37.)); +#1620 = CARTESIAN_POINT('',(5.,0.E+000,3.)); +#1621 = CARTESIAN_POINT('',(5.,10.,3.)); +#1622 = CARTESIAN_POINT('',(-5.,10.,3.)); +#1623 = CARTESIAN_POINT('',(-5.,0.E+000,3.)); +#1624 = DEFINITIONAL_REPRESENTATION('',(#1625),#1673); +#1625 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#1626,#1627,#1628,#1629,#1630, + #1631,#1632,#1633,#1634,#1635,#1636,#1637,#1638,#1639,#1640,#1641, + #1642,#1643,#1644,#1645,#1646,#1647,#1648,#1649,#1650,#1651,#1652, + #1653,#1654,#1655,#1656,#1657,#1658,#1659,#1660,#1661,#1662,#1663, + #1664,#1665,#1666,#1667,#1668,#1669,#1670,#1671,#1672), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880348, + 1.016627760695,1.524941641043,2.033255521391,2.541569401739, + 3.049883282086,3.558197162434,4.066511042782,4.57482492313, + 5.083138803477,5.591452683825,6.099766564173,6.60808044452, + 7.116394324868,7.624708205216,8.133022085564,8.641335965911, + 9.149649846259,9.657963726607,10.166277606955,10.674591487302, + 11.18290536765,11.691219247998,12.199533128345,12.707847008693, + 13.216160889041,13.724474769389,14.232788649736,14.741102530084, + 15.249416410432,15.75773029078,16.266044171127,16.774358051475, + 17.282671931823,17.79098581217,18.299299692518,18.807613572866, + 19.315927453214,19.824241333561,20.332555213909,20.840869094257, + 21.349182974605,21.857496854952,22.3658107353), + .QUASI_UNIFORM_KNOTS.); +#1626 = CARTESIAN_POINT('',(34.000998004,0.E+000)); +#1627 = CARTESIAN_POINT('',(34.000998004,0.285786134005)); +#1628 = CARTESIAN_POINT('',(34.000998004,0.851023724374)); +#1629 = CARTESIAN_POINT('',(34.000998004,1.679658950067)); +#1630 = CARTESIAN_POINT('',(34.000998004,2.488775842984)); +#1631 = CARTESIAN_POINT('',(34.000998004,3.278357391147)); +#1632 = CARTESIAN_POINT('',(34.000998004,4.048590090635)); +#1633 = CARTESIAN_POINT('',(34.000998004,4.799873551245)); +#1634 = CARTESIAN_POINT('',(34.000998004,5.532780976198)); +#1635 = CARTESIAN_POINT('',(34.000998004,6.248020911162)); +#1636 = CARTESIAN_POINT('',(34.000998004,6.946360574942)); +#1637 = CARTESIAN_POINT('',(34.000998004,7.628688635561)); +#1638 = CARTESIAN_POINT('',(34.000998004,8.296073973845)); +#1639 = CARTESIAN_POINT('',(34.000998004,8.949683945325)); +#1640 = CARTESIAN_POINT('',(34.000998004,9.590744783224)); +#1641 = CARTESIAN_POINT('',(34.000998004,10.220499189069)); +#1642 = CARTESIAN_POINT('',(34.000998004,10.840182524178)); +#1643 = CARTESIAN_POINT('',(34.000998004,11.450961995563)); +#1644 = CARTESIAN_POINT('',(34.000998004,12.054057836488)); +#1645 = CARTESIAN_POINT('',(34.000998004,12.650784955207)); +#1646 = CARTESIAN_POINT('',(34.000998004,13.242437006931)); +#1647 = CARTESIAN_POINT('',(34.000998004,13.830311319039)); +#1648 = CARTESIAN_POINT('',(34.000998004,14.415700442447)); +#1649 = CARTESIAN_POINT('',(34.000998004,14.999897615114)); +#1650 = CARTESIAN_POINT('',(34.000998004,15.584089013839)); +#1651 = CARTESIAN_POINT('',(34.000998004,16.169496123896)); +#1652 = CARTESIAN_POINT('',(34.000998004,16.757374014315)); +#1653 = CARTESIAN_POINT('',(34.000998004,17.349001920563)); +#1654 = CARTESIAN_POINT('',(34.000998004,17.945677529625)); +#1655 = CARTESIAN_POINT('',(34.000998004,18.548712223709)); +#1656 = CARTESIAN_POINT('',(34.000998004,19.159406299853)); +#1657 = CARTESIAN_POINT('',(34.000998004,19.779034544783)); +#1658 = CARTESIAN_POINT('',(34.000998004,20.40884411557)); +#1659 = CARTESIAN_POINT('',(34.000998004,21.05005071958)); +#1660 = CARTESIAN_POINT('',(34.000998004,21.703821244264)); +#1661 = CARTESIAN_POINT('',(34.000998004,22.371286811436)); +#1662 = CARTESIAN_POINT('',(34.000998004,23.053580536272)); +#1663 = CARTESIAN_POINT('',(34.000998004,23.751780892279)); +#1664 = CARTESIAN_POINT('',(34.000998004,24.466876470872)); +#1665 = CARTESIAN_POINT('',(34.000998004,25.199732655413)); +#1666 = CARTESIAN_POINT('',(34.000998004,25.951064420944)); +#1667 = CARTESIAN_POINT('',(34.000998004,26.721413688722)); +#1668 = CARTESIAN_POINT('',(34.000998004,27.511129456935)); +#1669 = CARTESIAN_POINT('',(34.000998004,28.320321955904)); +#1670 = CARTESIAN_POINT('',(34.000998004,29.148977248348)); +#1671 = CARTESIAN_POINT('',(34.000998004,29.714213802924)); +#1672 = CARTESIAN_POINT('',(34.000998004,30.)); +#1673 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1674 = ADVANCED_FACE('',(#1675),#1261,.T.); +#1675 = FACE_BOUND('',#1676,.T.); +#1676 = EDGE_LOOP('',(#1677,#1678,#1700,#1730)); +#1677 = ORIENTED_EDGE('',*,*,#1196,.T.); +#1678 = ORIENTED_EDGE('',*,*,#1679,.T.); +#1679 = EDGE_CURVE('',#1199,#1680,#1682,.T.); +#1680 = VERTEX_POINT('',#1681); +#1681 = CARTESIAN_POINT('',(-7.5,0.E+000,-2.22044604925E-016)); +#1682 = SURFACE_CURVE('',#1683,(#1686,#1693),.PCURVE_S1.); +#1683 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1684,#1685),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,3.00099800399),.PIECEWISE_BEZIER_KNOTS.); +#1684 = CARTESIAN_POINT('',(-7.5,8.32667268461E-016,3.)); +#1685 = CARTESIAN_POINT('',(-7.5,8.32667268461E-016,0.E+000)); +#1686 = PCURVE('',#1261,#1687); +#1687 = DEFINITIONAL_REPRESENTATION('',(#1688),#1692); +#1688 = LINE('',#1689,#1690); +#1689 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1690 = VECTOR('',#1691,1.); +#1691 = DIRECTION('',(1.,0.E+000)); +#1692 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1693 = PCURVE('',#1377,#1694); +#1694 = DEFINITIONAL_REPRESENTATION('',(#1695),#1699); +#1695 = LINE('',#1696,#1697); +#1696 = CARTESIAN_POINT('',(0.E+000,45.)); +#1697 = VECTOR('',#1698,1.); +#1698 = DIRECTION('',(1.,0.E+000)); +#1699 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1700 = ORIENTED_EDGE('',*,*,#1701,.T.); +#1701 = EDGE_CURVE('',#1680,#1702,#1704,.T.); +#1702 = VERTEX_POINT('',#1703); +#1703 = CARTESIAN_POINT('',(7.5,0.E+000,2.22044604925E-016)); +#1704 = SURFACE_CURVE('',#1705,(#1710,#1717),.PCURVE_S1.); +#1705 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1706,#1707,#1708,#1709), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1706 = CARTESIAN_POINT('',(-7.5,0.E+000,0.E+000)); +#1707 = CARTESIAN_POINT('',(-7.5,-15.,0.E+000)); +#1708 = CARTESIAN_POINT('',(7.5,-15.,0.E+000)); +#1709 = CARTESIAN_POINT('',(7.5,0.E+000,0.E+000)); +#1710 = PCURVE('',#1261,#1711); +#1711 = DEFINITIONAL_REPRESENTATION('',(#1712),#1716); +#1712 = LINE('',#1713,#1714); +#1713 = CARTESIAN_POINT('',(3.00099800399,0.E+000)); +#1714 = VECTOR('',#1715,1.); +#1715 = DIRECTION('',(0.E+000,1.)); +#1716 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1717 = PCURVE('',#1718,#1723); +#1718 = PLANE('',#1719); +#1719 = AXIS2_PLACEMENT_3D('',#1720,#1721,#1722); +#1720 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#1721 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1722 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#1723 = DEFINITIONAL_REPRESENTATION('',(#1724),#1729); +#1724 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1725,#1726,#1727,#1728), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1725 = CARTESIAN_POINT('',(7.5,0.E+000)); +#1726 = CARTESIAN_POINT('',(7.5,-15.)); +#1727 = CARTESIAN_POINT('',(-7.5,-15.)); +#1728 = CARTESIAN_POINT('',(-7.5,0.E+000)); +#1729 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1730 = ORIENTED_EDGE('',*,*,#1731,.F.); +#1731 = EDGE_CURVE('',#1197,#1702,#1732,.T.); +#1732 = SURFACE_CURVE('',#1733,(#1736,#1743),.PCURVE_S1.); +#1733 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1734,#1735),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,3.00099800399),.PIECEWISE_BEZIER_KNOTS.); +#1734 = CARTESIAN_POINT('',(7.5,8.32667268461E-016,3.)); +#1735 = CARTESIAN_POINT('',(7.5,8.32667268461E-016,0.E+000)); +#1736 = PCURVE('',#1261,#1737); +#1737 = DEFINITIONAL_REPRESENTATION('',(#1738),#1742); +#1738 = LINE('',#1739,#1740); +#1739 = CARTESIAN_POINT('',(0.E+000,45.)); +#1740 = VECTOR('',#1741,1.); +#1741 = DIRECTION('',(1.,0.E+000)); +#1742 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1743 = PCURVE('',#1377,#1744); +#1744 = DEFINITIONAL_REPRESENTATION('',(#1745),#1749); +#1745 = LINE('',#1746,#1747); +#1746 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1747 = VECTOR('',#1748,1.); +#1748 = DIRECTION('',(1.,0.E+000)); +#1749 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1750 = ADVANCED_FACE('',(#1751),#1377,.T.); +#1751 = FACE_BOUND('',#1752,.T.); +#1752 = EDGE_LOOP('',(#1753,#1754,#1755,#1778)); +#1753 = ORIENTED_EDGE('',*,*,#1321,.T.); +#1754 = ORIENTED_EDGE('',*,*,#1731,.T.); +#1755 = ORIENTED_EDGE('',*,*,#1756,.T.); +#1756 = EDGE_CURVE('',#1702,#1680,#1757,.T.); +#1757 = SURFACE_CURVE('',#1758,(#1763,#1770),.PCURVE_S1.); +#1758 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1759,#1760,#1761,#1762), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1759 = CARTESIAN_POINT('',(7.5,0.E+000,0.E+000)); +#1760 = CARTESIAN_POINT('',(7.5,15.,0.E+000)); +#1761 = CARTESIAN_POINT('',(-7.5,15.,0.E+000)); +#1762 = CARTESIAN_POINT('',(-7.5,0.E+000,0.E+000)); +#1763 = PCURVE('',#1377,#1764); +#1764 = DEFINITIONAL_REPRESENTATION('',(#1765),#1769); +#1765 = LINE('',#1766,#1767); +#1766 = CARTESIAN_POINT('',(3.00099800399,0.E+000)); +#1767 = VECTOR('',#1768,1.); +#1768 = DIRECTION('',(0.E+000,1.)); +#1769 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1770 = PCURVE('',#1718,#1771); +#1771 = DEFINITIONAL_REPRESENTATION('',(#1772),#1777); +#1772 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1773,#1774,#1775,#1776), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,45.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1773 = CARTESIAN_POINT('',(-7.5,0.E+000)); +#1774 = CARTESIAN_POINT('',(-7.5,15.)); +#1775 = CARTESIAN_POINT('',(7.5,15.)); +#1776 = CARTESIAN_POINT('',(7.5,0.E+000)); +#1777 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1778 = ORIENTED_EDGE('',*,*,#1679,.F.); +#1779 = ADVANCED_FACE('',(#1780),#1499,.T.); +#1780 = FACE_BOUND('',#1781,.T.); +#1781 = EDGE_LOOP('',(#1782,#1783,#1805,#1835)); +#1782 = ORIENTED_EDGE('',*,*,#1439,.T.); +#1783 = ORIENTED_EDGE('',*,*,#1784,.F.); +#1784 = EDGE_CURVE('',#1785,#1442,#1787,.T.); +#1785 = VERTEX_POINT('',#1786); +#1786 = CARTESIAN_POINT('',(5.,4.4408920985E-016,37.)); +#1787 = SURFACE_CURVE('',#1788,(#1791,#1798),.PCURVE_S1.); +#1788 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1789,#1790),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,34.000998004),.PIECEWISE_BEZIER_KNOTS.); +#1789 = CARTESIAN_POINT('',(5.,-5.55111512307E-016,37.)); +#1790 = CARTESIAN_POINT('',(5.,-5.55111512307E-016,3.)); +#1791 = PCURVE('',#1499,#1792); +#1792 = DEFINITIONAL_REPRESENTATION('',(#1793),#1797); +#1793 = LINE('',#1794,#1795); +#1794 = CARTESIAN_POINT('',(0.E+000,30.)); +#1795 = VECTOR('',#1796,1.); +#1796 = DIRECTION('',(1.,0.E+000)); +#1797 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1798 = PCURVE('',#1615,#1799); +#1799 = DEFINITIONAL_REPRESENTATION('',(#1800),#1804); +#1800 = LINE('',#1801,#1802); +#1801 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1802 = VECTOR('',#1803,1.); +#1803 = DIRECTION('',(1.,0.E+000)); +#1804 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1805 = ORIENTED_EDGE('',*,*,#1806,.F.); +#1806 = EDGE_CURVE('',#1807,#1785,#1809,.T.); +#1807 = VERTEX_POINT('',#1808); +#1808 = CARTESIAN_POINT('',(-5.,4.4408920985E-016,37.)); +#1809 = SURFACE_CURVE('',#1810,(#1815,#1822),.PCURVE_S1.); +#1810 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1811,#1812,#1813,#1814), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1811 = CARTESIAN_POINT('',(-5.,0.E+000,37.)); +#1812 = CARTESIAN_POINT('',(-5.,-10.,37.)); +#1813 = CARTESIAN_POINT('',(5.,-10.,37.)); +#1814 = CARTESIAN_POINT('',(5.,0.E+000,37.)); +#1815 = PCURVE('',#1499,#1816); +#1816 = DEFINITIONAL_REPRESENTATION('',(#1817),#1821); +#1817 = LINE('',#1818,#1819); +#1818 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#1819 = VECTOR('',#1820,1.); +#1820 = DIRECTION('',(0.E+000,1.)); +#1821 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1822 = PCURVE('',#1823,#1828); +#1823 = PLANE('',#1824); +#1824 = AXIS2_PLACEMENT_3D('',#1825,#1826,#1827); +#1825 = CARTESIAN_POINT('',(0.E+000,0.E+000,37.)); +#1826 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1827 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#1828 = DEFINITIONAL_REPRESENTATION('',(#1829),#1834); +#1829 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1830,#1831,#1832,#1833), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1830 = CARTESIAN_POINT('',(-5.,0.E+000)); +#1831 = CARTESIAN_POINT('',(-5.,-10.)); +#1832 = CARTESIAN_POINT('',(5.,-10.)); +#1833 = CARTESIAN_POINT('',(5.,0.E+000)); +#1834 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1835 = ORIENTED_EDGE('',*,*,#1836,.T.); +#1836 = EDGE_CURVE('',#1807,#1440,#1837,.T.); +#1837 = SURFACE_CURVE('',#1838,(#1841,#1848),.PCURVE_S1.); +#1838 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#1839,#1840),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,34.000998004),.PIECEWISE_BEZIER_KNOTS.); +#1839 = CARTESIAN_POINT('',(-5.,-5.55111512307E-016,37.)); +#1840 = CARTESIAN_POINT('',(-5.,-5.55111512307E-016,3.)); +#1841 = PCURVE('',#1499,#1842); +#1842 = DEFINITIONAL_REPRESENTATION('',(#1843),#1847); +#1843 = LINE('',#1844,#1845); +#1844 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#1845 = VECTOR('',#1846,1.); +#1846 = DIRECTION('',(1.,0.E+000)); +#1847 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1848 = PCURVE('',#1615,#1849); +#1849 = DEFINITIONAL_REPRESENTATION('',(#1850),#1854); +#1850 = LINE('',#1851,#1852); +#1851 = CARTESIAN_POINT('',(0.E+000,30.)); +#1852 = VECTOR('',#1853,1.); +#1853 = DIRECTION('',(1.,0.E+000)); +#1854 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1855 = ADVANCED_FACE('',(#1856),#1615,.T.); +#1856 = FACE_BOUND('',#1857,.T.); +#1857 = EDGE_LOOP('',(#1858,#1859,#1860,#1883)); +#1858 = ORIENTED_EDGE('',*,*,#1559,.T.); +#1859 = ORIENTED_EDGE('',*,*,#1836,.F.); +#1860 = ORIENTED_EDGE('',*,*,#1861,.F.); +#1861 = EDGE_CURVE('',#1785,#1807,#1862,.T.); +#1862 = SURFACE_CURVE('',#1863,(#1868,#1875),.PCURVE_S1.); +#1863 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1864,#1865,#1866,#1867), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1864 = CARTESIAN_POINT('',(5.,0.E+000,37.)); +#1865 = CARTESIAN_POINT('',(5.,10.,37.)); +#1866 = CARTESIAN_POINT('',(-5.,10.,37.)); +#1867 = CARTESIAN_POINT('',(-5.,0.E+000,37.)); +#1868 = PCURVE('',#1615,#1869); +#1869 = DEFINITIONAL_REPRESENTATION('',(#1870),#1874); +#1870 = LINE('',#1871,#1872); +#1871 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#1872 = VECTOR('',#1873,1.); +#1873 = DIRECTION('',(0.E+000,1.)); +#1874 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1875 = PCURVE('',#1823,#1876); +#1876 = DEFINITIONAL_REPRESENTATION('',(#1877),#1882); +#1877 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#1878,#1879,#1880,#1881), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#1878 = CARTESIAN_POINT('',(5.,0.E+000)); +#1879 = CARTESIAN_POINT('',(5.,10.)); +#1880 = CARTESIAN_POINT('',(-5.,10.)); +#1881 = CARTESIAN_POINT('',(-5.,0.E+000)); +#1882 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1883 = ORIENTED_EDGE('',*,*,#1784,.T.); +#1884 = ADVANCED_FACE('',(#1885),#1718,.T.); +#1885 = FACE_BOUND('',#1886,.T.); +#1886 = EDGE_LOOP('',(#1887,#1888)); +#1887 = ORIENTED_EDGE('',*,*,#1701,.F.); +#1888 = ORIENTED_EDGE('',*,*,#1756,.F.); +#1889 = ADVANCED_FACE('',(#1890),#1823,.T.); +#1890 = FACE_BOUND('',#1891,.T.); +#1891 = EDGE_LOOP('',(#1892,#1893)); +#1892 = ORIENTED_EDGE('',*,*,#1806,.T.); +#1893 = ORIENTED_EDGE('',*,*,#1861,.T.); +#1894 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#1898)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#1895,#1896,#1897)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#1895 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#1896 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#1897 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#1898 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-005),#1895, + 'distance_accuracy_value','confusion accuracy'); +#1899 = SHAPE_DEFINITION_REPRESENTATION(#1900,#1189); +#1900 = PRODUCT_DEFINITION_SHAPE('','',#1901); +#1901 = PRODUCT_DEFINITION('design','',#1902,#1905); +#1902 = PRODUCT_DEFINITION_FORMATION('','',#1903); +#1903 = PRODUCT('bolt','bolt','',(#1904)); +#1904 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#1905 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#1906 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1907,#1909); +#1907 = ( REPRESENTATION_RELATIONSHIP('','',#1189,#1175) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1908) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1908 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1176); +#1909 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1910); +#1910 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('5','bolt_1','',#1170,#1901,$); +#1911 = PRODUCT_TYPE('part',$,(#1903)); +#1912 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1913,#1915); +#1913 = ( REPRESENTATION_RELATIONSHIP('','',#62,#1175) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1914) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1914 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1180); +#1915 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1916); +#1916 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('6','nut_3','',#1170,#742,$); +#1917 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1918,#1920); +#1918 = ( REPRESENTATION_RELATIONSHIP('','',#1175,#1146) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1919) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1919 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1147); +#1920 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1921); +#1921 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('7','nut-bolt-assembly_1','', + #1141,#1170,$); +#1922 = PRODUCT_TYPE('part',$,(#1172)); +#1923 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1924,#1926); +#1924 = ( REPRESENTATION_RELATIONSHIP('','',#1175,#1146) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1925) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1925 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1151); +#1926 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1927); +#1927 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('8','nut-bolt-assembly_2','', + #1141,#1170,$); +#1928 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#1929,#1931); +#1929 = ( REPRESENTATION_RELATIONSHIP('','',#1175,#1146) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#1930) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#1930 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1155); +#1931 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #1932); +#1932 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('9','nut-bolt-assembly_3','', + #1141,#1170,$); +#1933 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#1934),#3788); +#1934 = MANIFOLD_SOLID_BREP('',#1935); +#1935 = CLOSED_SHELL('',(#1936,#2294,#3084,#3189,#3238,#3311,#3382,#3411 + ,#3438,#3509,#3538,#3609,#3638,#3709,#3738,#3777)); +#1936 = ADVANCED_FACE('',(#1937,#2056),#1951,.T.); +#1937 = FACE_BOUND('',#1938,.T.); +#1938 = EDGE_LOOP('',(#1939,#1974,#2002,#2030)); +#1939 = ORIENTED_EDGE('',*,*,#1940,.F.); +#1940 = EDGE_CURVE('',#1941,#1943,#1945,.T.); +#1941 = VERTEX_POINT('',#1942); +#1942 = CARTESIAN_POINT('',(0.E+000,0.E+000,100.)); +#1943 = VERTEX_POINT('',#1944); +#1944 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#1945 = SURFACE_CURVE('',#1946,(#1950,#1962),.PCURVE_S1.); +#1946 = LINE('',#1947,#1948); +#1947 = CARTESIAN_POINT('',(0.E+000,0.E+000,50.)); +#1948 = VECTOR('',#1949,1.); +#1949 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1950 = PCURVE('',#1951,#1956); +#1951 = PLANE('',#1952); +#1952 = AXIS2_PLACEMENT_3D('',#1953,#1954,#1955); +#1953 = CARTESIAN_POINT('',(0.E+000,60.,100.)); +#1954 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#1955 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1956 = DEFINITIONAL_REPRESENTATION('',(#1957),#1961); +#1957 = LINE('',#1958,#1959); +#1958 = CARTESIAN_POINT('',(-50.,-60.)); +#1959 = VECTOR('',#1960,1.); +#1960 = DIRECTION('',(-1.,0.E+000)); +#1961 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1962 = PCURVE('',#1963,#1968); +#1963 = PLANE('',#1964); +#1964 = AXIS2_PLACEMENT_3D('',#1965,#1966,#1967); +#1965 = CARTESIAN_POINT('',(0.E+000,0.E+000,100.)); +#1966 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#1967 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#1968 = DEFINITIONAL_REPRESENTATION('',(#1969),#1973); +#1969 = LINE('',#1970,#1971); +#1970 = CARTESIAN_POINT('',(50.,0.E+000)); +#1971 = VECTOR('',#1972,1.); +#1972 = DIRECTION('',(1.,0.E+000)); +#1973 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1974 = ORIENTED_EDGE('',*,*,#1975,.F.); +#1975 = EDGE_CURVE('',#1976,#1941,#1978,.T.); +#1976 = VERTEX_POINT('',#1977); +#1977 = CARTESIAN_POINT('',(0.E+000,60.,100.)); +#1978 = SURFACE_CURVE('',#1979,(#1983,#1990),.PCURVE_S1.); +#1979 = LINE('',#1980,#1981); +#1980 = CARTESIAN_POINT('',(0.E+000,30.,100.)); +#1981 = VECTOR('',#1982,1.); +#1982 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#1983 = PCURVE('',#1951,#1984); +#1984 = DEFINITIONAL_REPRESENTATION('',(#1985),#1989); +#1985 = LINE('',#1986,#1987); +#1986 = CARTESIAN_POINT('',(0.E+000,-30.)); +#1987 = VECTOR('',#1988,1.); +#1988 = DIRECTION('',(0.E+000,-1.)); +#1989 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#1990 = PCURVE('',#1991,#1996); +#1991 = PLANE('',#1992); +#1992 = AXIS2_PLACEMENT_3D('',#1993,#1994,#1995); +#1993 = CARTESIAN_POINT('',(0.E+000,0.E+000,100.)); +#1994 = DIRECTION('',(0.E+000,0.E+000,1.)); +#1995 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#1996 = DEFINITIONAL_REPRESENTATION('',(#1997),#2001); +#1997 = LINE('',#1998,#1999); +#1998 = CARTESIAN_POINT('',(0.E+000,30.)); +#1999 = VECTOR('',#2000,1.); +#2000 = DIRECTION('',(0.E+000,-1.)); +#2001 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2002 = ORIENTED_EDGE('',*,*,#2003,.T.); +#2003 = EDGE_CURVE('',#1976,#2004,#2006,.T.); +#2004 = VERTEX_POINT('',#2005); +#2005 = CARTESIAN_POINT('',(0.E+000,60.,0.E+000)); +#2006 = SURFACE_CURVE('',#2007,(#2011,#2018),.PCURVE_S1.); +#2007 = LINE('',#2008,#2009); +#2008 = CARTESIAN_POINT('',(0.E+000,60.,50.)); +#2009 = VECTOR('',#2010,1.); +#2010 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#2011 = PCURVE('',#1951,#2012); +#2012 = DEFINITIONAL_REPRESENTATION('',(#2013),#2017); +#2013 = LINE('',#2014,#2015); +#2014 = CARTESIAN_POINT('',(-50.,0.E+000)); +#2015 = VECTOR('',#2016,1.); +#2016 = DIRECTION('',(-1.,0.E+000)); +#2017 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2018 = PCURVE('',#2019,#2024); +#2019 = PLANE('',#2020); +#2020 = AXIS2_PLACEMENT_3D('',#2021,#2022,#2023); +#2021 = CARTESIAN_POINT('',(10.,60.,100.)); +#2022 = DIRECTION('',(0.E+000,1.,0.E+000)); +#2023 = DIRECTION('',(0.E+000,-0.E+000,1.)); +#2024 = DEFINITIONAL_REPRESENTATION('',(#2025),#2029); +#2025 = LINE('',#2026,#2027); +#2026 = CARTESIAN_POINT('',(-50.,-10.)); +#2027 = VECTOR('',#2028,1.); +#2028 = DIRECTION('',(-1.,0.E+000)); +#2029 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2030 = ORIENTED_EDGE('',*,*,#2031,.T.); +#2031 = EDGE_CURVE('',#2004,#1943,#2032,.T.); +#2032 = SURFACE_CURVE('',#2033,(#2037,#2044),.PCURVE_S1.); +#2033 = LINE('',#2034,#2035); +#2034 = CARTESIAN_POINT('',(0.E+000,30.,0.E+000)); +#2035 = VECTOR('',#2036,1.); +#2036 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#2037 = PCURVE('',#1951,#2038); +#2038 = DEFINITIONAL_REPRESENTATION('',(#2039),#2043); +#2039 = LINE('',#2040,#2041); +#2040 = CARTESIAN_POINT('',(-100.,-30.)); +#2041 = VECTOR('',#2042,1.); +#2042 = DIRECTION('',(0.E+000,-1.)); +#2043 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2044 = PCURVE('',#2045,#2050); +#2045 = PLANE('',#2046); +#2046 = AXIS2_PLACEMENT_3D('',#2047,#2048,#2049); +#2047 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#2048 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#2049 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#2050 = DEFINITIONAL_REPRESENTATION('',(#2051),#2055); +#2051 = LINE('',#2052,#2053); +#2052 = CARTESIAN_POINT('',(0.E+000,30.)); +#2053 = VECTOR('',#2054,1.); +#2054 = DIRECTION('',(0.E+000,-1.)); +#2055 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2056 = FACE_BOUND('',#2057,.T.); +#2057 = EDGE_LOOP('',(#2058,#2178)); +#2058 = ORIENTED_EDGE('',*,*,#2059,.T.); +#2059 = EDGE_CURVE('',#2060,#2062,#2064,.T.); +#2060 = VERTEX_POINT('',#2061); +#2061 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#2062 = VERTEX_POINT('',#2063); +#2063 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#2064 = SURFACE_CURVE('',#2065,(#2090,#2118),.PCURVE_S1.); +#2065 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2066,#2067,#2068,#2069,#2070, + #2071,#2072,#2073,#2074,#2075,#2076,#2077,#2078,#2079,#2080,#2081, + #2082,#2083,#2084,#2085,#2086,#2087,#2088,#2089),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164424,7.85828164686, + 10.7238180515,13.5836589937,16.4911855013,20.3877608685, + 22.3658107304),.UNSPECIFIED.); +#2066 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#2067 = CARTESIAN_POINT('',(0.E+000,40.4671982524,45.)); +#2068 = CARTESIAN_POINT('',(0.E+000,40.967985464,45.0545696798)); +#2069 = CARTESIAN_POINT('',(0.E+000,41.4911230353,45.1795822588)); +#2070 = CARTESIAN_POINT('',(0.E+000,42.4800614343,45.5726861255)); +#2071 = CARTESIAN_POINT('',(0.E+000,43.3809047398,46.2580146362)); +#2072 = CARTESIAN_POINT('',(0.E+000,43.7686263331,46.6452361934)); +#2073 = CARTESIAN_POINT('',(0.E+000,44.3620880288,47.432508626)); +#2074 = CARTESIAN_POINT('',(0.E+000,44.7518403652,48.3548107374)); +#2075 = CARTESIAN_POINT('',(0.E+000,44.8779193361,48.7767785569)); +#2076 = CARTESIAN_POINT('',(0.E+000,45.0354809914,49.6437129631)); +#2077 = CARTESIAN_POINT('',(0.E+000,45.001400762,50.5264003017)); +#2078 = CARTESIAN_POINT('',(0.E+000,44.935748566,50.9630506747)); +#2079 = CARTESIAN_POINT('',(0.E+000,44.7088457881,51.8186421202)); +#2080 = CARTESIAN_POINT('',(0.E+000,44.3071306709,52.5957546192)); +#2081 = CARTESIAN_POINT('',(0.E+000,44.0642190823,52.9603131849)); +#2082 = CARTESIAN_POINT('',(0.E+000,43.416301294,53.7355490362)); +#2083 = CARTESIAN_POINT('',(0.E+000,42.624655655,54.3095225982)); +#2084 = CARTESIAN_POINT('',(0.E+000,42.1424481996,54.563750022)); +#2085 = CARTESIAN_POINT('',(0.E+000,41.404818935,54.8362924347)); +#2086 = CARTESIAN_POINT('',(0.E+000,40.688855102,54.9612187699)); +#2087 = CARTESIAN_POINT('',(0.E+000,40.4524313762,54.9876332288)); +#2088 = CARTESIAN_POINT('',(0.E+000,40.2224096654,55.)); +#2089 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#2090 = PCURVE('',#1951,#2091); +#2091 = DEFINITIONAL_REPRESENTATION('',(#2092),#2117); +#2092 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2093,#2094,#2095,#2096,#2097, + #2098,#2099,#2100,#2101,#2102,#2103,#2104,#2105,#2106,#2107,#2108, + #2109,#2110,#2111,#2112,#2113,#2114,#2115,#2116),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164424,7.85828164686, + 10.7238180515,13.5836589937,16.4911855013,20.3877608685, + 22.3658107304),.UNSPECIFIED.); +#2093 = CARTESIAN_POINT('',(-55.,-20.)); +#2094 = CARTESIAN_POINT('',(-55.,-19.5328017476)); +#2095 = CARTESIAN_POINT('',(-54.9454303202,-19.032014536)); +#2096 = CARTESIAN_POINT('',(-54.8204177412,-18.5088769647)); +#2097 = CARTESIAN_POINT('',(-54.4273138745,-17.5199385657)); +#2098 = CARTESIAN_POINT('',(-53.7419853638,-16.6190952602)); +#2099 = CARTESIAN_POINT('',(-53.3547638066,-16.2313736669)); +#2100 = CARTESIAN_POINT('',(-52.567491374,-15.6379119712)); +#2101 = CARTESIAN_POINT('',(-51.6451892626,-15.2481596348)); +#2102 = CARTESIAN_POINT('',(-51.2232214431,-15.1220806639)); +#2103 = CARTESIAN_POINT('',(-50.3562870369,-14.9645190086)); +#2104 = CARTESIAN_POINT('',(-49.4735996983,-14.998599238)); +#2105 = CARTESIAN_POINT('',(-49.0369493253,-15.064251434)); +#2106 = CARTESIAN_POINT('',(-48.1813578798,-15.2911542119)); +#2107 = CARTESIAN_POINT('',(-47.4042453808,-15.6928693291)); +#2108 = CARTESIAN_POINT('',(-47.0396868151,-15.9357809177)); +#2109 = CARTESIAN_POINT('',(-46.2644509638,-16.583698706)); +#2110 = CARTESIAN_POINT('',(-45.6904774018,-17.375344345)); +#2111 = CARTESIAN_POINT('',(-45.436249978,-17.8575518004)); +#2112 = CARTESIAN_POINT('',(-45.1637075653,-18.595181065)); +#2113 = CARTESIAN_POINT('',(-45.0387812301,-19.311144898)); +#2114 = CARTESIAN_POINT('',(-45.0123667712,-19.5475686238)); +#2115 = CARTESIAN_POINT('',(-45.,-19.7775903346)); +#2116 = CARTESIAN_POINT('',(-45.,-20.)); +#2117 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2118 = PCURVE('',#2119,#2128); +#2119 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2120,#2121,#2122,#2123) + ,(#2124,#2125,#2126,#2127 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2120 = CARTESIAN_POINT('',(10.,40.,55.)); +#2121 = CARTESIAN_POINT('',(10.,50.,55.)); +#2122 = CARTESIAN_POINT('',(10.,50.,45.)); +#2123 = CARTESIAN_POINT('',(10.,40.,45.)); +#2124 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#2125 = CARTESIAN_POINT('',(0.E+000,50.,55.)); +#2126 = CARTESIAN_POINT('',(0.E+000,50.,45.)); +#2127 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#2128 = DEFINITIONAL_REPRESENTATION('',(#2129),#2177); +#2129 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2130,#2131,#2132,#2133,#2134, + #2135,#2136,#2137,#2138,#2139,#2140,#2141,#2142,#2143,#2144,#2145, + #2146,#2147,#2148,#2149,#2150,#2151,#2152,#2153,#2154,#2155,#2156, + #2157,#2158,#2159,#2160,#2161,#2162,#2163,#2164,#2165,#2166,#2167, + #2168,#2169,#2170,#2171,#2172,#2173,#2174,#2175,#2176), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880236, + 1.016627760473,1.524941640709,2.033255520945,2.541569401182, + 3.049883281418,3.558197161655,4.066511041891,4.574824922127, + 5.083138802364,5.5914526826,6.099766562836,6.608080443073, + 7.116394323309,7.624708203545,8.133022083782,8.641335964018, + 9.149649844255,9.657963724491,10.166277604727,10.674591484964, + 11.1829053652,11.691219245436,12.199533125673,12.707847005909, + 13.216160886145,13.724474766382,14.232788646618,14.741102526855, + 15.249416407091,15.757730287327,16.266044167564,16.7743580478, + 17.282671928036,17.790985808273,18.299299688509,18.807613568745, + 19.315927448982,19.824241329218,20.332555209455,20.840869089691, + 21.349182969927,21.857496850164,22.3658107304),.UNSPECIFIED.); +#2130 = CARTESIAN_POINT('',(10.000998004,30.)); +#2131 = CARTESIAN_POINT('',(10.000998004,29.714213866026)); +#2132 = CARTESIAN_POINT('',(10.000998004,29.148976275749)); +#2133 = CARTESIAN_POINT('',(10.000998004,28.320341050263)); +#2134 = CARTESIAN_POINT('',(10.000998004,27.511224157616)); +#2135 = CARTESIAN_POINT('',(10.000998004,26.721642609747)); +#2136 = CARTESIAN_POINT('',(10.000998004,25.951409910544)); +#2137 = CARTESIAN_POINT('',(10.000998004,25.200126450178)); +#2138 = CARTESIAN_POINT('',(10.000998004,24.467219025419)); +#2139 = CARTESIAN_POINT('',(10.000998004,23.751979090598)); +#2140 = CARTESIAN_POINT('',(10.000998004,23.053639426926)); +#2141 = CARTESIAN_POINT('',(10.000998004,22.371311366386)); +#2142 = CARTESIAN_POINT('',(10.000998004,21.703926028164)); +#2143 = CARTESIAN_POINT('',(10.000998004,21.050316056745)); +#2144 = CARTESIAN_POINT('',(10.000998004,20.40925521892)); +#2145 = CARTESIAN_POINT('',(10.000998004,19.779500813173)); +#2146 = CARTESIAN_POINT('',(10.000998004,19.15981747818)); +#2147 = CARTESIAN_POINT('',(10.000998004,18.549038006927)); +#2148 = CARTESIAN_POINT('',(10.000998004,17.94594216606)); +#2149 = CARTESIAN_POINT('',(10.000998004,17.349215047295)); +#2150 = CARTESIAN_POINT('',(10.000998004,16.757562995502)); +#2151 = CARTESIAN_POINT('',(10.000998004,16.169688683392)); +#2152 = CARTESIAN_POINT('',(10.000998004,15.584299560095)); +#2153 = CARTESIAN_POINT('',(10.000998004,15.000102387554)); +#2154 = CARTESIAN_POINT('',(10.000998004,14.415910989025)); +#2155 = CARTESIAN_POINT('',(10.000998004,13.830503879233)); +#2156 = CARTESIAN_POINT('',(10.000998004,13.242625989092)); +#2157 = CARTESIAN_POINT('',(10.000998004,12.650998083074)); +#2158 = CARTESIAN_POINT('',(10.000998004,12.054322474192)); +#2159 = CARTESIAN_POINT('',(10.000998004,11.451287780254)); +#2160 = CARTESIAN_POINT('',(10.000998004,10.840593704162)); +#2161 = CARTESIAN_POINT('',(10.000998004,10.220965459246)); +#2162 = CARTESIAN_POINT('',(10.000998004,9.591155888523)); +#2163 = CARTESIAN_POINT('',(10.000998004,8.949949284694)); +#2164 = CARTESIAN_POINT('',(10.000998004,8.296178760285)); +#2165 = CARTESIAN_POINT('',(10.000998004,7.628713193302)); +#2166 = CARTESIAN_POINT('',(10.000998004,6.94641946847)); +#2167 = CARTESIAN_POINT('',(10.000998004,6.248219112305)); +#2168 = CARTESIAN_POINT('',(10.000998004,5.533123533488)); +#2169 = CARTESIAN_POINT('',(10.000998004,4.800267348802)); +#2170 = CARTESIAN_POINT('',(10.000998004,4.048935583317)); +#2171 = CARTESIAN_POINT('',(10.000998004,3.278586315814)); +#2172 = CARTESIAN_POINT('',(10.000998004,2.488870547876)); +#2173 = CARTESIAN_POINT('',(10.000998004,1.679678046715)); +#2174 = CARTESIAN_POINT('',(10.000998004,0.851022751886)); +#2175 = CARTESIAN_POINT('',(10.000998004,0.285786196767)); +#2176 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2177 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2178 = ORIENTED_EDGE('',*,*,#2179,.T.); +#2179 = EDGE_CURVE('',#2062,#2060,#2180,.T.); +#2180 = SURFACE_CURVE('',#2181,(#2206,#2234),.PCURVE_S1.); +#2181 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2182,#2183,#2184,#2185,#2186, + #2187,#2188,#2189,#2190,#2191,#2192,#2193,#2194,#2195,#2196,#2197, + #2198,#2199,#2200,#2201,#2202,#2203,#2204,#2205),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164422,7.85828164677, + 10.7238180514,13.5836589927,16.4911854995,20.3877608665, + 22.3658107284),.UNSPECIFIED.); +#2182 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#2183 = CARTESIAN_POINT('',(0.E+000,39.5328017476,55.)); +#2184 = CARTESIAN_POINT('',(0.E+000,39.032014536,54.9454303202)); +#2185 = CARTESIAN_POINT('',(0.E+000,38.5088769647,54.8204177412)); +#2186 = CARTESIAN_POINT('',(0.E+000,37.5199385657,54.4273138745)); +#2187 = CARTESIAN_POINT('',(0.E+000,36.6190952602,53.7419853638)); +#2188 = CARTESIAN_POINT('',(0.E+000,36.2313736669,53.3547638066)); +#2189 = CARTESIAN_POINT('',(0.E+000,35.6379119712,52.567491374)); +#2190 = CARTESIAN_POINT('',(0.E+000,35.2481596348,51.6451892626)); +#2191 = CARTESIAN_POINT('',(0.E+000,35.1220806639,51.2232214431)); +#2192 = CARTESIAN_POINT('',(0.E+000,34.9645190086,50.356287037)); +#2193 = CARTESIAN_POINT('',(0.E+000,34.998599238,49.4735996986)); +#2194 = CARTESIAN_POINT('',(0.E+000,35.0642514341,49.036949325)); +#2195 = CARTESIAN_POINT('',(0.E+000,35.291154212,48.1813578798)); +#2196 = CARTESIAN_POINT('',(0.E+000,35.692869329,47.404245381)); +#2197 = CARTESIAN_POINT('',(0.E+000,35.9357809179,47.0396868149)); +#2198 = CARTESIAN_POINT('',(0.E+000,36.583698706,46.2644509637)); +#2199 = CARTESIAN_POINT('',(0.E+000,37.375344345,45.6904774019)); +#2200 = CARTESIAN_POINT('',(0.E+000,37.8575518004,45.436249978)); +#2201 = CARTESIAN_POINT('',(0.E+000,38.595181065,45.1637075653)); +#2202 = CARTESIAN_POINT('',(0.E+000,39.311144898,45.0387812301)); +#2203 = CARTESIAN_POINT('',(0.E+000,39.5475686238,45.0123667712)); +#2204 = CARTESIAN_POINT('',(0.E+000,39.7775903347,45.)); +#2205 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#2206 = PCURVE('',#1951,#2207); +#2207 = DEFINITIONAL_REPRESENTATION('',(#2208),#2233); +#2208 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2209,#2210,#2211,#2212,#2213, + #2214,#2215,#2216,#2217,#2218,#2219,#2220,#2221,#2222,#2223,#2224, + #2225,#2226,#2227,#2228,#2229,#2230,#2231,#2232),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164422,7.85828164677, + 10.7238180514,13.5836589927,16.4911854995,20.3877608665, + 22.3658107284),.UNSPECIFIED.); +#2209 = CARTESIAN_POINT('',(-45.,-20.)); +#2210 = CARTESIAN_POINT('',(-45.,-20.4671982524)); +#2211 = CARTESIAN_POINT('',(-45.0545696798,-20.967985464)); +#2212 = CARTESIAN_POINT('',(-45.1795822588,-21.4911230353)); +#2213 = CARTESIAN_POINT('',(-45.5726861255,-22.4800614343)); +#2214 = CARTESIAN_POINT('',(-46.2580146362,-23.3809047398)); +#2215 = CARTESIAN_POINT('',(-46.6452361934,-23.7686263331)); +#2216 = CARTESIAN_POINT('',(-47.432508626,-24.3620880288)); +#2217 = CARTESIAN_POINT('',(-48.3548107374,-24.7518403652)); +#2218 = CARTESIAN_POINT('',(-48.7767785569,-24.8779193361)); +#2219 = CARTESIAN_POINT('',(-49.643712963,-25.0354809914)); +#2220 = CARTESIAN_POINT('',(-50.5264003014,-25.001400762)); +#2221 = CARTESIAN_POINT('',(-50.963050675,-24.9357485659)); +#2222 = CARTESIAN_POINT('',(-51.8186421202,-24.708845788)); +#2223 = CARTESIAN_POINT('',(-52.595754619,-24.307130671)); +#2224 = CARTESIAN_POINT('',(-52.9603131851,-24.0642190821)); +#2225 = CARTESIAN_POINT('',(-53.7355490363,-23.416301294)); +#2226 = CARTESIAN_POINT('',(-54.3095225981,-22.624655655)); +#2227 = CARTESIAN_POINT('',(-54.563750022,-22.1424481996)); +#2228 = CARTESIAN_POINT('',(-54.8362924347,-21.404818935)); +#2229 = CARTESIAN_POINT('',(-54.9612187699,-20.688855102)); +#2230 = CARTESIAN_POINT('',(-54.9876332288,-20.4524313762)); +#2231 = CARTESIAN_POINT('',(-55.,-20.2224096653)); +#2232 = CARTESIAN_POINT('',(-55.,-20.)); +#2233 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2234 = PCURVE('',#2235,#2244); +#2235 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2236,#2237,#2238,#2239) + ,(#2240,#2241,#2242,#2243 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2236 = CARTESIAN_POINT('',(10.,40.,45.)); +#2237 = CARTESIAN_POINT('',(10.,30.,45.)); +#2238 = CARTESIAN_POINT('',(10.,30.,55.)); +#2239 = CARTESIAN_POINT('',(10.,40.,55.)); +#2240 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#2241 = CARTESIAN_POINT('',(0.E+000,30.,45.)); +#2242 = CARTESIAN_POINT('',(0.E+000,30.,55.)); +#2243 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#2244 = DEFINITIONAL_REPRESENTATION('',(#2245),#2293); +#2245 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2246,#2247,#2248,#2249,#2250, + #2251,#2252,#2253,#2254,#2255,#2256,#2257,#2258,#2259,#2260,#2261, + #2262,#2263,#2264,#2265,#2266,#2267,#2268,#2269,#2270,#2271,#2272, + #2273,#2274,#2275,#2276,#2277,#2278,#2279,#2280,#2281,#2282,#2283, + #2284,#2285,#2286,#2287,#2288,#2289,#2290,#2291,#2292), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880191, + 1.016627760382,1.524941640573,2.033255520764,2.541569400955, + 3.049883281145,3.558197161336,4.066511041527,4.574824921718, + 5.083138801909,5.5914526821,6.099766562291,6.608080442482, + 7.116394322673,7.624708202864,8.133022083055,8.641335963245, + 9.149649843436,9.657963723627,10.166277603818,10.674591484009, + 11.1829053642,11.691219244391,12.199533124582,12.707847004773, + 13.216160884964,13.724474765155,14.232788645345,14.741102525536, + 15.249416405727,15.757730285918,16.266044166109,16.7743580463, + 17.282671926491,17.790985806682,18.299299686873,18.807613567064, + 19.315927447255,19.824241327445,20.332555207636,20.840869087827, + 21.349182968018,21.857496848209,22.3658107284), + .QUASI_UNIFORM_KNOTS.); +#2246 = CARTESIAN_POINT('',(10.000998004,30.)); +#2247 = CARTESIAN_POINT('',(10.000998004,29.71421386605)); +#2248 = CARTESIAN_POINT('',(10.000998004,29.14897627582)); +#2249 = CARTESIAN_POINT('',(10.000998004,28.320341050402)); +#2250 = CARTESIAN_POINT('',(10.000998004,27.511224157819)); +#2251 = CARTESIAN_POINT('',(10.000998004,26.72164261001)); +#2252 = CARTESIAN_POINT('',(10.000998004,25.951409910862)); +#2253 = CARTESIAN_POINT('',(10.000998004,25.200126450549)); +#2254 = CARTESIAN_POINT('',(10.000998004,24.467219025838)); +#2255 = CARTESIAN_POINT('',(10.000998004,23.751979091062)); +#2256 = CARTESIAN_POINT('',(10.000998004,23.053639427433)); +#2257 = CARTESIAN_POINT('',(10.000998004,22.371311366934)); +#2258 = CARTESIAN_POINT('',(10.000998004,21.70392602875)); +#2259 = CARTESIAN_POINT('',(10.000998004,21.050316057367)); +#2260 = CARTESIAN_POINT('',(10.000998004,20.409255219579)); +#2261 = CARTESIAN_POINT('',(10.000998004,19.779500813868)); +#2262 = CARTESIAN_POINT('',(10.000998004,19.159817478911)); +#2263 = CARTESIAN_POINT('',(10.000998004,18.549038007695)); +#2264 = CARTESIAN_POINT('',(10.000998004,17.945942166867)); +#2265 = CARTESIAN_POINT('',(10.000998004,17.349215048139)); +#2266 = CARTESIAN_POINT('',(10.000998004,16.757562996382)); +#2267 = CARTESIAN_POINT('',(10.000998004,16.169688684309)); +#2268 = CARTESIAN_POINT('',(10.000998004,15.584299561055)); +#2269 = CARTESIAN_POINT('',(10.000998004,15.000102388583)); +#2270 = CARTESIAN_POINT('',(10.000998004,14.415910989914)); +#2271 = CARTESIAN_POINT('',(10.000998004,13.830503879808)); +#2272 = CARTESIAN_POINT('',(10.000998004,13.242625989363)); +#2273 = CARTESIAN_POINT('',(10.000998004,12.650998083229)); +#2274 = CARTESIAN_POINT('',(10.000998004,12.054322474433)); +#2275 = CARTESIAN_POINT('',(10.000998004,11.4512877805)); +#2276 = CARTESIAN_POINT('',(10.000998004,10.84059370422)); +#2277 = CARTESIAN_POINT('',(10.000998004,10.220965459014)); +#2278 = CARTESIAN_POINT('',(10.000998004,9.591155888064)); +#2279 = CARTESIAN_POINT('',(10.000998004,8.949949284218)); +#2280 = CARTESIAN_POINT('',(10.000998004,8.296178759904)); +#2281 = CARTESIAN_POINT('',(10.000998004,7.62871319297)); +#2282 = CARTESIAN_POINT('',(10.000998004,6.946419468164)); +#2283 = CARTESIAN_POINT('',(10.000998004,6.248219112002)); +#2284 = CARTESIAN_POINT('',(10.000998004,5.533123533185)); +#2285 = CARTESIAN_POINT('',(10.000998004,4.800267348507)); +#2286 = CARTESIAN_POINT('',(10.000998004,4.048935583046)); +#2287 = CARTESIAN_POINT('',(10.000998004,3.278586315578)); +#2288 = CARTESIAN_POINT('',(10.000998004,2.488870547681)); +#2289 = CARTESIAN_POINT('',(10.000998004,1.67967804655)); +#2290 = CARTESIAN_POINT('',(10.000998004,0.851022751666)); +#2291 = CARTESIAN_POINT('',(10.000998004,0.28578619665)); +#2292 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2293 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2294 = ADVANCED_FACE('',(#2295,#2370,#2608,#2846),#1963,.T.); +#2295 = FACE_BOUND('',#2296,.T.); +#2296 = EDGE_LOOP('',(#2297,#2327,#2348,#2349)); +#2297 = ORIENTED_EDGE('',*,*,#2298,.F.); +#2298 = EDGE_CURVE('',#2299,#2301,#2303,.T.); +#2299 = VERTEX_POINT('',#2300); +#2300 = CARTESIAN_POINT('',(50.,0.E+000,100.)); +#2301 = VERTEX_POINT('',#2302); +#2302 = CARTESIAN_POINT('',(50.,0.E+000,0.E+000)); +#2303 = SURFACE_CURVE('',#2304,(#2308,#2315),.PCURVE_S1.); +#2304 = LINE('',#2305,#2306); +#2305 = CARTESIAN_POINT('',(50.,0.E+000,50.)); +#2306 = VECTOR('',#2307,1.); +#2307 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#2308 = PCURVE('',#1963,#2309); +#2309 = DEFINITIONAL_REPRESENTATION('',(#2310),#2314); +#2310 = LINE('',#2311,#2312); +#2311 = CARTESIAN_POINT('',(50.,50.)); +#2312 = VECTOR('',#2313,1.); +#2313 = DIRECTION('',(1.,0.E+000)); +#2314 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2315 = PCURVE('',#2316,#2321); +#2316 = PLANE('',#2317); +#2317 = AXIS2_PLACEMENT_3D('',#2318,#2319,#2320); +#2318 = CARTESIAN_POINT('',(50.,0.E+000,100.)); +#2319 = DIRECTION('',(1.,0.E+000,0.E+000)); +#2320 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#2321 = DEFINITIONAL_REPRESENTATION('',(#2322),#2326); +#2322 = LINE('',#2323,#2324); +#2323 = CARTESIAN_POINT('',(50.,0.E+000)); +#2324 = VECTOR('',#2325,1.); +#2325 = DIRECTION('',(1.,0.E+000)); +#2326 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2327 = ORIENTED_EDGE('',*,*,#2328,.F.); +#2328 = EDGE_CURVE('',#1941,#2299,#2329,.T.); +#2329 = SURFACE_CURVE('',#2330,(#2334,#2341),.PCURVE_S1.); +#2330 = LINE('',#2331,#2332); +#2331 = CARTESIAN_POINT('',(25.,0.E+000,100.)); +#2332 = VECTOR('',#2333,1.); +#2333 = DIRECTION('',(1.,0.E+000,0.E+000)); +#2334 = PCURVE('',#1963,#2335); +#2335 = DEFINITIONAL_REPRESENTATION('',(#2336),#2340); +#2336 = LINE('',#2337,#2338); +#2337 = CARTESIAN_POINT('',(0.E+000,25.)); +#2338 = VECTOR('',#2339,1.); +#2339 = DIRECTION('',(0.E+000,1.)); +#2340 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2341 = PCURVE('',#1991,#2342); +#2342 = DEFINITIONAL_REPRESENTATION('',(#2343),#2347); +#2343 = LINE('',#2344,#2345); +#2344 = CARTESIAN_POINT('',(25.,0.E+000)); +#2345 = VECTOR('',#2346,1.); +#2346 = DIRECTION('',(1.,0.E+000)); +#2347 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2348 = ORIENTED_EDGE('',*,*,#1940,.T.); +#2349 = ORIENTED_EDGE('',*,*,#2350,.T.); +#2350 = EDGE_CURVE('',#1943,#2301,#2351,.T.); +#2351 = SURFACE_CURVE('',#2352,(#2356,#2363),.PCURVE_S1.); +#2352 = LINE('',#2353,#2354); +#2353 = CARTESIAN_POINT('',(25.,0.E+000,0.E+000)); +#2354 = VECTOR('',#2355,1.); +#2355 = DIRECTION('',(1.,0.E+000,0.E+000)); +#2356 = PCURVE('',#1963,#2357); +#2357 = DEFINITIONAL_REPRESENTATION('',(#2358),#2362); +#2358 = LINE('',#2359,#2360); +#2359 = CARTESIAN_POINT('',(100.,25.)); +#2360 = VECTOR('',#2361,1.); +#2361 = DIRECTION('',(0.E+000,1.)); +#2362 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2363 = PCURVE('',#2045,#2364); +#2364 = DEFINITIONAL_REPRESENTATION('',(#2365),#2369); +#2365 = LINE('',#2366,#2367); +#2366 = CARTESIAN_POINT('',(-25.,0.E+000)); +#2367 = VECTOR('',#2368,1.); +#2368 = DIRECTION('',(-1.,0.E+000)); +#2369 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2370 = FACE_BOUND('',#2371,.T.); +#2371 = EDGE_LOOP('',(#2372,#2492)); +#2372 = ORIENTED_EDGE('',*,*,#2373,.T.); +#2373 = EDGE_CURVE('',#2374,#2376,#2378,.T.); +#2374 = VERTEX_POINT('',#2375); +#2375 = CARTESIAN_POINT('',(42.5,0.E+000,42.0096189398)); +#2376 = VERTEX_POINT('',#2377); +#2377 = CARTESIAN_POINT('',(42.5,0.E+000,32.0096189398)); +#2378 = SURFACE_CURVE('',#2379,(#2404,#2432),.PCURVE_S1.); +#2379 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2380,#2381,#2382,#2383,#2384, + #2385,#2386,#2387,#2388,#2389,#2390,#2391,#2392,#2393,#2394,#2395, + #2396,#2397,#2398,#2399,#2400,#2401,#2402,#2403),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165632,7.85828166598, + 10.7238180637,13.5836590149,16.4911855364,20.3877609237, + 22.3658108252),.UNSPECIFIED.); +#2380 = CARTESIAN_POINT('',(42.5,0.E+000,42.0096189398)); +#2381 = CARTESIAN_POINT('',(42.9671982537,0.E+000,42.0096189398)); +#2382 = CARTESIAN_POINT('',(43.4679854668,0.E+000,41.9550492597)); +#2383 = CARTESIAN_POINT('',(43.9911230323,0.E+000,41.8300366822)); +#2384 = CARTESIAN_POINT('',(44.9800614342,0.E+000,41.4369328146)); +#2385 = CARTESIAN_POINT('',(45.8809047407,0.E+000,40.7516043032)); +#2386 = CARTESIAN_POINT('',(46.2686263317,0.E+000,40.364382748)); +#2387 = CARTESIAN_POINT('',(46.8620880278,0.E+000,39.5771103155)); +#2388 = CARTESIAN_POINT('',(47.2518403645,0.E+000,38.6548082046)); +#2389 = CARTESIAN_POINT('',(47.3779193365,0.E+000,38.2328403825)); +#2390 = CARTESIAN_POINT('',(47.5354809915,0.E+000,37.3659059762)); +#2391 = CARTESIAN_POINT('',(47.501400762,0.E+000,36.4832186373)); +#2392 = CARTESIAN_POINT('',(47.4357485667,0.E+000,36.04656827)); +#2393 = CARTESIAN_POINT('',(47.2088457881,0.E+000,35.1909768206)); +#2394 = CARTESIAN_POINT('',(46.807130669,0.E+000,34.4138643184)); +#2395 = CARTESIAN_POINT('',(46.564219085,0.E+000,34.0493057582)); +#2396 = CARTESIAN_POINT('',(45.916301294,0.E+000,33.2740699026)); +#2397 = CARTESIAN_POINT('',(45.1246556495,0.E+000,32.7000963378)); +#2398 = CARTESIAN_POINT('',(44.6424482051,0.E+000,32.4458689217)); +#2399 = CARTESIAN_POINT('',(43.9048189333,0.E+000,32.1733265057)); +#2400 = CARTESIAN_POINT('',(43.1888550914,0.E+000,32.04840017)); +#2401 = CARTESIAN_POINT('',(42.9524313854,0.E+000,32.0219857115)); +#2402 = CARTESIAN_POINT('',(42.7224096698,0.E+000,32.0096189398)); +#2403 = CARTESIAN_POINT('',(42.5,0.E+000,32.0096189398)); +#2404 = PCURVE('',#1963,#2405); +#2405 = DEFINITIONAL_REPRESENTATION('',(#2406),#2431); +#2406 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2407,#2408,#2409,#2410,#2411, + #2412,#2413,#2414,#2415,#2416,#2417,#2418,#2419,#2420,#2421,#2422, + #2423,#2424,#2425,#2426,#2427,#2428,#2429,#2430),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165632,7.85828166598, + 10.7238180637,13.5836590149,16.4911855364,20.3877609237, + 22.3658108252),.UNSPECIFIED.); +#2407 = CARTESIAN_POINT('',(57.9903810602,42.5)); +#2408 = CARTESIAN_POINT('',(57.9903810602,42.9671982537)); +#2409 = CARTESIAN_POINT('',(58.0449507403,43.4679854668)); +#2410 = CARTESIAN_POINT('',(58.1699633178,43.9911230323)); +#2411 = CARTESIAN_POINT('',(58.5630671854,44.9800614342)); +#2412 = CARTESIAN_POINT('',(59.2483956968,45.8809047407)); +#2413 = CARTESIAN_POINT('',(59.635617252,46.2686263317)); +#2414 = CARTESIAN_POINT('',(60.4228896845,46.8620880278)); +#2415 = CARTESIAN_POINT('',(61.3451917954,47.2518403645)); +#2416 = CARTESIAN_POINT('',(61.7671596175,47.3779193365)); +#2417 = CARTESIAN_POINT('',(62.6340940238,47.5354809915)); +#2418 = CARTESIAN_POINT('',(63.5167813627,47.501400762)); +#2419 = CARTESIAN_POINT('',(63.95343173,47.4357485667)); +#2420 = CARTESIAN_POINT('',(64.8090231794,47.2088457881)); +#2421 = CARTESIAN_POINT('',(65.5861356816,46.807130669)); +#2422 = CARTESIAN_POINT('',(65.9506942418,46.564219085)); +#2423 = CARTESIAN_POINT('',(66.7259300974,45.916301294)); +#2424 = CARTESIAN_POINT('',(67.2999036622,45.1246556495)); +#2425 = CARTESIAN_POINT('',(67.5541310783,44.6424482051)); +#2426 = CARTESIAN_POINT('',(67.8266734943,43.9048189333)); +#2427 = CARTESIAN_POINT('',(67.95159983,43.1888550914)); +#2428 = CARTESIAN_POINT('',(67.9780142885,42.9524313854)); +#2429 = CARTESIAN_POINT('',(67.9903810602,42.7224096698)); +#2430 = CARTESIAN_POINT('',(67.9903810602,42.5)); +#2431 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2432 = PCURVE('',#2433,#2442); +#2433 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2434,#2435,#2436,#2437) + ,(#2438,#2439,#2440,#2441 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2434 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#2435 = CARTESIAN_POINT('',(52.5,10.,32.00961894)); +#2436 = CARTESIAN_POINT('',(52.5,10.,42.00961894)); +#2437 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#2438 = CARTESIAN_POINT('',(42.5,0.E+000,32.00961894)); +#2439 = CARTESIAN_POINT('',(52.5,0.E+000,32.00961894)); +#2440 = CARTESIAN_POINT('',(52.5,0.E+000,42.00961894)); +#2441 = CARTESIAN_POINT('',(42.5,0.E+000,42.00961894)); +#2442 = DEFINITIONAL_REPRESENTATION('',(#2443),#2491); +#2443 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2444,#2445,#2446,#2447,#2448, + #2449,#2450,#2451,#2452,#2453,#2454,#2455,#2456,#2457,#2458,#2459, + #2460,#2461,#2462,#2463,#2464,#2465,#2466,#2467,#2468,#2469,#2470, + #2471,#2472,#2473,#2474,#2475,#2476,#2477,#2478,#2479,#2480,#2481, + #2482,#2483,#2484,#2485,#2486,#2487,#2488,#2489,#2490), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313882391, + 1.016627764782,1.524941647173,2.033255529564,2.541569411955, + 3.049883294345,3.558197176736,4.066511059127,4.574824941518, + 5.083138823909,5.5914527063,6.099766588691,6.608080471082, + 7.116394353473,7.624708235864,8.133022118255,8.641336000645, + 9.149649883036,9.657963765427,10.166277647818,10.674591530209, + 11.1829054126,11.691219294991,12.199533177382,12.707847059773, + 13.216160942164,13.724474824555,14.232788706945,14.741102589336, + 15.249416471727,15.757730354118,16.266044236509,16.7743581189, + 17.282672001291,17.790985883682,18.299299766073,18.807613648464, + 19.315927530855,19.824241413245,20.332555295636,20.840869178027, + 21.349183060418,21.857496942809,22.3658108252), + .QUASI_UNIFORM_KNOTS.); +#2444 = CARTESIAN_POINT('',(10.000998004,30.)); +#2445 = CARTESIAN_POINT('',(10.000998004,29.71421386473)); +#2446 = CARTESIAN_POINT('',(10.000998004,29.148976272343)); +#2447 = CARTESIAN_POINT('',(10.000998004,28.320341045137)); +#2448 = CARTESIAN_POINT('',(10.000998004,27.511224152571)); +#2449 = CARTESIAN_POINT('',(10.000998004,26.721642605677)); +#2450 = CARTESIAN_POINT('',(10.000998004,25.951409907321)); +#2451 = CARTESIAN_POINT('',(10.000998004,25.200126446802)); +#2452 = CARTESIAN_POINT('',(10.000998004,24.467219020533)); +#2453 = CARTESIAN_POINT('',(10.000998004,23.751979083143)); +#2454 = CARTESIAN_POINT('',(10.000998004,23.053639417136)); +#2455 = CARTESIAN_POINT('',(10.000998004,22.371311355221)); +#2456 = CARTESIAN_POINT('',(10.000998004,21.703926016379)); +#2457 = CARTESIAN_POINT('',(10.000998004,21.050316044609)); +#2458 = CARTESIAN_POINT('',(10.000998004,20.409255206124)); +#2459 = CARTESIAN_POINT('',(10.000998004,19.779500799029)); +#2460 = CARTESIAN_POINT('',(10.000998004,19.159817461882)); +#2461 = CARTESIAN_POINT('',(10.000998004,18.549037988407)); +#2462 = CARTESIAN_POINT('',(10.000998004,17.945942144676)); +#2463 = CARTESIAN_POINT('',(10.000998004,17.349215021909)); +#2464 = CARTESIAN_POINT('',(10.000998004,16.757562965883)); +#2465 = CARTESIAN_POINT('',(10.000998004,16.169688650255)); +#2466 = CARTESIAN_POINT('',(10.000998004,15.584299524584)); +#2467 = CARTESIAN_POINT('',(10.000998004,15.000102349713)); +#2468 = CARTESIAN_POINT('',(10.000998004,14.41591095074)); +#2469 = CARTESIAN_POINT('',(10.000998004,13.830503841967)); +#2470 = CARTESIAN_POINT('',(10.000998004,13.24262595249)); +#2471 = CARTESIAN_POINT('',(10.000998004,12.650998045143)); +#2472 = CARTESIAN_POINT('',(10.000998004,12.054322432743)); +#2473 = CARTESIAN_POINT('',(10.000998004,11.451287736281)); +#2474 = CARTESIAN_POINT('',(10.000998004,10.840593660521)); +#2475 = CARTESIAN_POINT('',(10.000998004,10.220965417485)); +#2476 = CARTESIAN_POINT('',(10.000998004,9.591155847716)); +#2477 = CARTESIAN_POINT('',(10.000998004,8.949949241796)); +#2478 = CARTESIAN_POINT('',(10.000998004,8.296178712958)); +#2479 = CARTESIAN_POINT('',(10.000998004,7.628713143093)); +#2480 = CARTESIAN_POINT('',(10.000998004,6.946419418445)); +#2481 = CARTESIAN_POINT('',(10.000998004,6.248219065189)); +#2482 = CARTESIAN_POINT('',(10.000998004,5.533123490298)); +#2483 = CARTESIAN_POINT('',(10.000998004,4.8002673082)); +#2484 = CARTESIAN_POINT('',(10.000998004,4.048935541973)); +#2485 = CARTESIAN_POINT('',(10.000998004,3.278586269626)); +#2486 = CARTESIAN_POINT('',(10.000998004,2.488870495423)); +#2487 = CARTESIAN_POINT('',(10.000998004,1.679678017969)); +#2488 = CARTESIAN_POINT('',(10.000998004,0.851022750739)); +#2489 = CARTESIAN_POINT('',(10.000998004,0.285786201188)); +#2490 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2491 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2492 = ORIENTED_EDGE('',*,*,#2493,.T.); +#2493 = EDGE_CURVE('',#2376,#2374,#2494,.T.); +#2494 = SURFACE_CURVE('',#2495,(#2520,#2548),.PCURVE_S1.); +#2495 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2496,#2497,#2498,#2499,#2500, + #2501,#2502,#2503,#2504,#2505,#2506,#2507,#2508,#2509,#2510,#2511, + #2512,#2513,#2514,#2515,#2516,#2517,#2518,#2519),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513162148,7.85828163111, + 10.7238180489,13.583658992,16.4911855021,20.3877608676,22.3658107326 + ),.UNSPECIFIED.); +#2496 = CARTESIAN_POINT('',(42.5,0.E+000,32.0096189398)); +#2497 = CARTESIAN_POINT('',(42.0328017497,0.E+000,32.0096189398)); +#2498 = CARTESIAN_POINT('',(41.5320145405,0.E+000,32.0641886193)); +#2499 = CARTESIAN_POINT('',(41.0088769576,0.E+000,32.1892012003)); +#2500 = CARTESIAN_POINT('',(40.0199385585,0.E+000,32.5823050688)); +#2501 = CARTESIAN_POINT('',(39.1190952597,0.E+000,33.2676335757)); +#2502 = CARTESIAN_POINT('',(38.7313736684,0.E+000,33.6548551346)); +#2503 = CARTESIAN_POINT('',(38.1379119704,0.E+000,34.4421275707)); +#2504 = CARTESIAN_POINT('',(37.7481596331,0.E+000,35.3644296843)); +#2505 = CARTESIAN_POINT('',(37.6220806643,0.E+000,35.7863974929)); +#2506 = CARTESIAN_POINT('',(37.4645190086,0.E+000,36.6533319007)); +#2507 = CARTESIAN_POINT('',(37.4985992382,0.E+000,37.5360192423)); +#2508 = CARTESIAN_POINT('',(37.5642514339,0.E+000,37.972669614)); +#2509 = CARTESIAN_POINT('',(37.7911542119,0.E+000,38.8282610603)); +#2510 = CARTESIAN_POINT('',(38.1928693296,0.E+000,39.6053735599)); +#2511 = CARTESIAN_POINT('',(38.4357809169,0.E+000,39.9699321238)); +#2512 = CARTESIAN_POINT('',(39.0836987058,0.E+000,40.7451679759)); +#2513 = CARTESIAN_POINT('',(39.8753443446,0.E+000,41.3191415378)); +#2514 = CARTESIAN_POINT('',(40.3575518005,0.E+000,41.5733689617)); +#2515 = CARTESIAN_POINT('',(41.0951810662,0.E+000,41.8459113747)); +#2516 = CARTESIAN_POINT('',(41.8111448982,0.E+000,41.9708377099)); +#2517 = CARTESIAN_POINT('',(42.0475686226,0.E+000,41.9972521686)); +#2518 = CARTESIAN_POINT('',(42.2775903341,0.E+000,42.0096189398)); +#2519 = CARTESIAN_POINT('',(42.5,0.E+000,42.0096189398)); +#2520 = PCURVE('',#1963,#2521); +#2521 = DEFINITIONAL_REPRESENTATION('',(#2522),#2547); +#2522 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2523,#2524,#2525,#2526,#2527, + #2528,#2529,#2530,#2531,#2532,#2533,#2534,#2535,#2536,#2537,#2538, + #2539,#2540,#2541,#2542,#2543,#2544,#2545,#2546),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513162148,7.85828163111, + 10.7238180489,13.583658992,16.4911855021,20.3877608676,22.3658107326 + ),.UNSPECIFIED.); +#2523 = CARTESIAN_POINT('',(67.9903810602,42.5)); +#2524 = CARTESIAN_POINT('',(67.9903810602,42.0328017497)); +#2525 = CARTESIAN_POINT('',(67.9358113807,41.5320145405)); +#2526 = CARTESIAN_POINT('',(67.8107987997,41.0088769576)); +#2527 = CARTESIAN_POINT('',(67.4176949312,40.0199385585)); +#2528 = CARTESIAN_POINT('',(66.7323664243,39.1190952597)); +#2529 = CARTESIAN_POINT('',(66.3451448654,38.7313736684)); +#2530 = CARTESIAN_POINT('',(65.5578724293,38.1379119704)); +#2531 = CARTESIAN_POINT('',(64.6355703157,37.7481596331)); +#2532 = CARTESIAN_POINT('',(64.2136025071,37.6220806643)); +#2533 = CARTESIAN_POINT('',(63.3466680993,37.4645190086)); +#2534 = CARTESIAN_POINT('',(62.4639807577,37.4985992382)); +#2535 = CARTESIAN_POINT('',(62.027330386,37.5642514339)); +#2536 = CARTESIAN_POINT('',(61.1717389397,37.7911542119)); +#2537 = CARTESIAN_POINT('',(60.3946264401,38.1928693296)); +#2538 = CARTESIAN_POINT('',(60.0300678762,38.4357809169)); +#2539 = CARTESIAN_POINT('',(59.2548320241,39.0836987058)); +#2540 = CARTESIAN_POINT('',(58.6808584622,39.8753443446)); +#2541 = CARTESIAN_POINT('',(58.4266310383,40.3575518005)); +#2542 = CARTESIAN_POINT('',(58.1540886253,41.0951810662)); +#2543 = CARTESIAN_POINT('',(58.0291622901,41.8111448982)); +#2544 = CARTESIAN_POINT('',(58.0027478314,42.0475686226)); +#2545 = CARTESIAN_POINT('',(57.9903810602,42.2775903341)); +#2546 = CARTESIAN_POINT('',(57.9903810602,42.5)); +#2547 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2548 = PCURVE('',#2549,#2558); +#2549 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2550,#2551,#2552,#2553) + ,(#2554,#2555,#2556,#2557 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2550 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#2551 = CARTESIAN_POINT('',(32.5,10.,42.00961894)); +#2552 = CARTESIAN_POINT('',(32.5,10.,32.00961894)); +#2553 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#2554 = CARTESIAN_POINT('',(42.5,0.E+000,42.00961894)); +#2555 = CARTESIAN_POINT('',(32.5,0.E+000,42.00961894)); +#2556 = CARTESIAN_POINT('',(32.5,0.E+000,32.00961894)); +#2557 = CARTESIAN_POINT('',(42.5,0.E+000,32.00961894)); +#2558 = DEFINITIONAL_REPRESENTATION('',(#2559),#2607); +#2559 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2560,#2561,#2562,#2563,#2564, + #2565,#2566,#2567,#2568,#2569,#2570,#2571,#2572,#2573,#2574,#2575, + #2576,#2577,#2578,#2579,#2580,#2581,#2582,#2583,#2584,#2585,#2586, + #2587,#2588,#2589,#2590,#2591,#2592,#2593,#2594,#2595,#2596,#2597, + #2598,#2599,#2600,#2601,#2602,#2603,#2604,#2605,#2606), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880286, + 1.016627760573,1.524941640859,2.033255521145,2.541569401432, + 3.049883281718,3.558197162005,4.066511042291,4.574824922577, + 5.083138802864,5.59145268315,6.099766563436,6.608080443723, + 7.116394324009,7.624708204295,8.133022084582,8.641335964868, + 9.149649845155,9.657963725441,10.166277605727,10.674591486014, + 11.1829053663,11.691219246586,12.199533126873,12.707847007159, + 13.216160887445,13.724474767732,14.232788648018,14.741102528305, + 15.249416408591,15.757730288877,16.266044169164,16.77435804945, + 17.282671929736,17.790985810023,18.299299690309,18.807613570595, + 19.315927450882,19.824241331168,20.332555211455,20.840869091741, + 21.349182972027,21.857496852314,22.3658107326),.UNSPECIFIED.); +#2560 = CARTESIAN_POINT('',(10.000998004,30.)); +#2561 = CARTESIAN_POINT('',(10.000998004,29.714213865937)); +#2562 = CARTESIAN_POINT('',(10.000998004,29.148976274665)); +#2563 = CARTESIAN_POINT('',(10.000998004,28.320341045234)); +#2564 = CARTESIAN_POINT('',(10.000998004,27.511224145495)); +#2565 = CARTESIAN_POINT('',(10.000998004,26.721642589108)); +#2566 = CARTESIAN_POINT('',(10.000998004,25.951409881938)); +#2567 = CARTESIAN_POINT('',(10.000998004,25.200126415948)); +#2568 = CARTESIAN_POINT('',(10.000998004,24.467218988867)); +#2569 = CARTESIAN_POINT('',(10.000998004,23.751979054917)); +#2570 = CARTESIAN_POINT('',(10.000998004,23.053639393732)); +#2571 = CARTESIAN_POINT('',(10.000998004,22.371311336103)); +#2572 = CARTESIAN_POINT('',(10.000998004,21.7039260005)); +#2573 = CARTESIAN_POINT('',(10.000998004,21.050316030914)); +#2574 = CARTESIAN_POINT('',(10.000998004,20.409255194012)); +#2575 = CARTESIAN_POINT('',(10.000998004,19.779500788414)); +#2576 = CARTESIAN_POINT('',(10.000998004,19.159817453332)); +#2577 = CARTESIAN_POINT('',(10.000998004,18.549037981764)); +#2578 = CARTESIAN_POINT('',(10.000998004,17.945942143431)); +#2579 = CARTESIAN_POINT('',(10.000998004,17.349215031035)); +#2580 = CARTESIAN_POINT('',(10.000998004,16.757562986474)); +#2581 = CARTESIAN_POINT('',(10.000998004,16.16968867911)); +#2582 = CARTESIAN_POINT('',(10.000998004,15.584299556328)); +#2583 = CARTESIAN_POINT('',(10.000998004,15.000102383364)); +#2584 = CARTESIAN_POINT('',(10.000998004,14.415910984911)); +#2585 = CARTESIAN_POINT('',(10.000998004,13.830503875548)); +#2586 = CARTESIAN_POINT('',(10.000998004,13.242625985881)); +#2587 = CARTESIAN_POINT('',(10.000998004,12.650998079982)); +#2588 = CARTESIAN_POINT('',(10.000998004,12.05432247075)); +#2589 = CARTESIAN_POINT('',(10.000998004,11.451287776763)); +#2590 = CARTESIAN_POINT('',(10.000998004,10.840593701457)); +#2591 = CARTESIAN_POINT('',(10.000998004,10.220965457727)); +#2592 = CARTESIAN_POINT('',(10.000998004,9.59115588787)); +#2593 = CARTESIAN_POINT('',(10.000998004,8.949949283992)); +#2594 = CARTESIAN_POINT('',(10.000998004,8.296178759194)); +#2595 = CARTESIAN_POINT('',(10.000998004,7.628713192038)); +#2596 = CARTESIAN_POINT('',(10.000998004,6.94641946689)); +#2597 = CARTESIAN_POINT('',(10.000998004,6.248219110218)); +#2598 = CARTESIAN_POINT('',(10.000998004,5.533123530703)); +#2599 = CARTESIAN_POINT('',(10.000998004,4.800267345232)); +#2600 = CARTESIAN_POINT('',(10.000998004,4.048935579088)); +#2601 = CARTESIAN_POINT('',(10.000998004,3.278586311318)); +#2602 = CARTESIAN_POINT('',(10.000998004,2.488870543964)); +#2603 = CARTESIAN_POINT('',(10.000998004,1.679678045349)); +#2604 = CARTESIAN_POINT('',(10.000998004,0.8510227524)); +#2605 = CARTESIAN_POINT('',(10.000998004,0.285786197317)); +#2606 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2607 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2608 = FACE_BOUND('',#2609,.T.); +#2609 = EDGE_LOOP('',(#2610,#2730)); +#2610 = ORIENTED_EDGE('',*,*,#2611,.T.); +#2611 = EDGE_CURVE('',#2612,#2614,#2616,.T.); +#2612 = VERTEX_POINT('',#2613); +#2613 = CARTESIAN_POINT('',(42.5,0.E+000,67.9903810602)); +#2614 = VERTEX_POINT('',#2615); +#2615 = CARTESIAN_POINT('',(42.5,0.E+000,57.9903810602)); +#2616 = SURFACE_CURVE('',#2617,(#2642,#2670),.PCURVE_S1.); +#2617 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2618,#2619,#2620,#2621,#2622, + #2623,#2624,#2625,#2626,#2627,#2628,#2629,#2630,#2631,#2632,#2633, + #2634,#2635,#2636,#2637,#2638,#2639,#2640,#2641),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513162009,7.85828162953, + 10.7238180471,13.5836589903,16.4911855013,20.3877608671, + 22.3658107334),.UNSPECIFIED.); +#2618 = CARTESIAN_POINT('',(42.5,0.E+000,67.9903810602)); +#2619 = CARTESIAN_POINT('',(42.9671982501,0.E+000,67.9903810602)); +#2620 = CARTESIAN_POINT('',(43.467985459,0.E+000,67.9358113808)); +#2621 = CARTESIAN_POINT('',(43.9911230428,0.E+000,67.8107987996)); +#2622 = CARTESIAN_POINT('',(44.9800614416,0.E+000,67.417694931)); +#2623 = CARTESIAN_POINT('',(45.8809047403,0.E+000,66.7323664244)); +#2624 = CARTESIAN_POINT('',(46.2686263317,0.E+000,66.3451448654)); +#2625 = CARTESIAN_POINT('',(46.8620880296,0.E+000,65.5578724293)); +#2626 = CARTESIAN_POINT('',(47.2518403668,0.E+000,64.6355703158)); +#2627 = CARTESIAN_POINT('',(47.3779193357,0.E+000,64.213602507)); +#2628 = CARTESIAN_POINT('',(47.5354809914,0.E+000,63.3466680992)); +#2629 = CARTESIAN_POINT('',(47.5014007618,0.E+000,62.4639807577)); +#2630 = CARTESIAN_POINT('',(47.4357485661,0.E+000,62.027330386)); +#2631 = CARTESIAN_POINT('',(47.2088457881,0.E+000,61.1717389395)); +#2632 = CARTESIAN_POINT('',(46.8071306702,0.E+000,60.3946264398)); +#2633 = CARTESIAN_POINT('',(46.5642190833,0.E+000,60.0300678764)); +#2634 = CARTESIAN_POINT('',(45.9163012943,0.E+000,59.2548320242)); +#2635 = CARTESIAN_POINT('',(45.1246556554,0.E+000,58.6808584622)); +#2636 = CARTESIAN_POINT('',(44.6424481995,0.E+000,58.4266310383)); +#2637 = CARTESIAN_POINT('',(43.9048189337,0.E+000,58.1540886252)); +#2638 = CARTESIAN_POINT('',(43.1888551014,0.E+000,58.0291622901)); +#2639 = CARTESIAN_POINT('',(42.9524313776,0.E+000,58.0027478314)); +#2640 = CARTESIAN_POINT('',(42.7224096661,0.E+000,57.9903810602)); +#2641 = CARTESIAN_POINT('',(42.5,0.E+000,57.9903810602)); +#2642 = PCURVE('',#1963,#2643); +#2643 = DEFINITIONAL_REPRESENTATION('',(#2644),#2669); +#2644 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2645,#2646,#2647,#2648,#2649, + #2650,#2651,#2652,#2653,#2654,#2655,#2656,#2657,#2658,#2659,#2660, + #2661,#2662,#2663,#2664,#2665,#2666,#2667,#2668),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513162009,7.85828162953, + 10.7238180471,13.5836589903,16.4911855013,20.3877608671, + 22.3658107334),.UNSPECIFIED.); +#2645 = CARTESIAN_POINT('',(32.0096189398,42.5)); +#2646 = CARTESIAN_POINT('',(32.0096189398,42.9671982501)); +#2647 = CARTESIAN_POINT('',(32.0641886192,43.467985459)); +#2648 = CARTESIAN_POINT('',(32.1892012004,43.9911230428)); +#2649 = CARTESIAN_POINT('',(32.582305069,44.9800614416)); +#2650 = CARTESIAN_POINT('',(33.2676335756,45.8809047403)); +#2651 = CARTESIAN_POINT('',(33.6548551346,46.2686263317)); +#2652 = CARTESIAN_POINT('',(34.4421275707,46.8620880296)); +#2653 = CARTESIAN_POINT('',(35.3644296842,47.2518403668)); +#2654 = CARTESIAN_POINT('',(35.786397493,47.3779193357)); +#2655 = CARTESIAN_POINT('',(36.6533319008,47.5354809914)); +#2656 = CARTESIAN_POINT('',(37.5360192423,47.5014007618)); +#2657 = CARTESIAN_POINT('',(37.972669614,47.4357485661)); +#2658 = CARTESIAN_POINT('',(38.8282610605,47.2088457881)); +#2659 = CARTESIAN_POINT('',(39.6053735602,46.8071306702)); +#2660 = CARTESIAN_POINT('',(39.9699321236,46.5642190833)); +#2661 = CARTESIAN_POINT('',(40.7451679758,45.9163012943)); +#2662 = CARTESIAN_POINT('',(41.3191415378,45.1246556554)); +#2663 = CARTESIAN_POINT('',(41.5733689617,44.6424481995)); +#2664 = CARTESIAN_POINT('',(41.8459113748,43.9048189337)); +#2665 = CARTESIAN_POINT('',(41.9708377099,43.1888551014)); +#2666 = CARTESIAN_POINT('',(41.9972521686,42.9524313776)); +#2667 = CARTESIAN_POINT('',(42.0096189398,42.7224096661)); +#2668 = CARTESIAN_POINT('',(42.0096189398,42.5)); +#2669 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2670 = PCURVE('',#2671,#2680); +#2671 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2672,#2673,#2674,#2675) + ,(#2676,#2677,#2678,#2679 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2672 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#2673 = CARTESIAN_POINT('',(52.5,10.,57.99038106)); +#2674 = CARTESIAN_POINT('',(52.5,10.,67.99038106)); +#2675 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#2676 = CARTESIAN_POINT('',(42.5,0.E+000,57.99038106)); +#2677 = CARTESIAN_POINT('',(52.5,0.E+000,57.99038106)); +#2678 = CARTESIAN_POINT('',(52.5,0.E+000,67.99038106)); +#2679 = CARTESIAN_POINT('',(42.5,0.E+000,67.99038106)); +#2680 = DEFINITIONAL_REPRESENTATION('',(#2681),#2729); +#2681 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2682,#2683,#2684,#2685,#2686, + #2687,#2688,#2689,#2690,#2691,#2692,#2693,#2694,#2695,#2696,#2697, + #2698,#2699,#2700,#2701,#2702,#2703,#2704,#2705,#2706,#2707,#2708, + #2709,#2710,#2711,#2712,#2713,#2714,#2715,#2716,#2717,#2718,#2719, + #2720,#2721,#2722,#2723,#2724,#2725,#2726,#2727,#2728), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880305, + 1.016627760609,1.524941640914,2.033255521218,2.541569401523, + 3.049883281827,3.558197162132,4.066511042436,4.574824922741, + 5.083138803045,5.59145268335,6.099766563655,6.608080443959, + 7.116394324264,7.624708204568,8.133022084873,8.641335965177, + 9.149649845482,9.657963725786,10.166277606091,10.674591486395, + 11.1829053667,11.691219247005,12.199533127309,12.707847007614, + 13.216160887918,13.724474768223,14.232788648527,14.741102528832, + 15.249416409136,15.757730289441,16.266044169745,16.77435805005, + 17.282671930355,17.790985810659,18.299299690964,18.807613571268, + 19.315927451573,19.824241331877,20.332555212182,20.840869092486, + 21.349182972791,21.857496853095,22.3658107334), + .QUASI_UNIFORM_KNOTS.); +#2682 = CARTESIAN_POINT('',(10.000998004,30.)); +#2683 = CARTESIAN_POINT('',(10.000998004,29.714213865971)); +#2684 = CARTESIAN_POINT('',(10.000998004,29.148976274717)); +#2685 = CARTESIAN_POINT('',(10.000998004,28.320341045137)); +#2686 = CARTESIAN_POINT('',(10.000998004,27.511224144985)); +#2687 = CARTESIAN_POINT('',(10.000998004,26.721642588047)); +#2688 = CARTESIAN_POINT('',(10.000998004,25.951409880337)); +#2689 = CARTESIAN_POINT('',(10.000998004,25.200126413953)); +#2690 = CARTESIAN_POINT('',(10.000998004,24.467218986691)); +#2691 = CARTESIAN_POINT('',(10.000998004,23.751979052749)); +#2692 = CARTESIAN_POINT('',(10.000998004,23.053639391603)); +#2693 = CARTESIAN_POINT('',(10.000998004,22.37131133398)); +#2694 = CARTESIAN_POINT('',(10.000998004,21.703925998355)); +#2695 = CARTESIAN_POINT('',(10.000998004,21.050316028729)); +#2696 = CARTESIAN_POINT('',(10.000998004,20.409255191791)); +#2697 = CARTESIAN_POINT('',(10.000998004,19.779500786179)); +#2698 = CARTESIAN_POINT('',(10.000998004,19.159817451111)); +#2699 = CARTESIAN_POINT('',(10.000998004,18.549037979584)); +#2700 = CARTESIAN_POINT('',(10.000998004,17.945942141233)); +#2701 = CARTESIAN_POINT('',(10.000998004,17.349215028728)); +#2702 = CARTESIAN_POINT('',(10.000998004,16.757562984029)); +#2703 = CARTESIAN_POINT('',(10.000998004,16.16968867657)); +#2704 = CARTESIAN_POINT('',(10.000998004,15.584299553772)); +#2705 = CARTESIAN_POINT('',(10.000998004,15.000102380823)); +#2706 = CARTESIAN_POINT('',(10.000998004,14.415910982381)); +#2707 = CARTESIAN_POINT('',(10.000998004,13.830503873011)); +#2708 = CARTESIAN_POINT('',(10.000998004,13.242625983313)); +#2709 = CARTESIAN_POINT('',(10.000998004,12.650998077366)); +#2710 = CARTESIAN_POINT('',(10.000998004,12.054322468057)); +#2711 = CARTESIAN_POINT('',(10.000998004,11.451287774064)); +#2712 = CARTESIAN_POINT('',(10.000998004,10.840593698998)); +#2713 = CARTESIAN_POINT('',(10.000998004,10.220965455649)); +#2714 = CARTESIAN_POINT('',(10.000998004,9.591155886117)); +#2715 = CARTESIAN_POINT('',(10.000998004,8.949949282326)); +#2716 = CARTESIAN_POINT('',(10.000998004,8.296178757472)); +#2717 = CARTESIAN_POINT('',(10.000998004,7.628713190284)); +#2718 = CARTESIAN_POINT('',(10.000998004,6.946419465101)); +#2719 = CARTESIAN_POINT('',(10.000998004,6.248219108403)); +#2720 = CARTESIAN_POINT('',(10.000998004,5.533123528866)); +#2721 = CARTESIAN_POINT('',(10.000998004,4.800267343376)); +#2722 = CARTESIAN_POINT('',(10.000998004,4.048935577202)); +#2723 = CARTESIAN_POINT('',(10.000998004,3.278586309373)); +#2724 = CARTESIAN_POINT('',(10.000998004,2.488870541882)); +#2725 = CARTESIAN_POINT('',(10.000998004,1.679678044077)); +#2726 = CARTESIAN_POINT('',(10.000998004,0.851022752257)); +#2727 = CARTESIAN_POINT('',(10.000998004,0.285786197451)); +#2728 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2729 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2730 = ORIENTED_EDGE('',*,*,#2731,.T.); +#2731 = EDGE_CURVE('',#2614,#2612,#2732,.T.); +#2732 = SURFACE_CURVE('',#2733,(#2758,#2786),.PCURVE_S1.); +#2733 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2734,#2735,#2736,#2737,#2738, + #2739,#2740,#2741,#2742,#2743,#2744,#2745,#2746,#2747,#2748,#2749, + #2750,#2751,#2752,#2753,#2754,#2755,#2756,#2757),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165736,7.85828166679, + 10.7238180644,13.5836590156,16.491185538,20.3877609254,22.3658108266 + ),.UNSPECIFIED.); +#2734 = CARTESIAN_POINT('',(42.5,0.E+000,57.9903810602)); +#2735 = CARTESIAN_POINT('',(42.0328017462,0.E+000,57.9903810602)); +#2736 = CARTESIAN_POINT('',(41.5320145329,0.E+000,58.0449507403)); +#2737 = CARTESIAN_POINT('',(41.0088769679,0.E+000,58.1699633177)); +#2738 = CARTESIAN_POINT('',(40.019938566,0.E+000,58.5630671853)); +#2739 = CARTESIAN_POINT('',(39.1190952593,0.E+000,59.2483956968)); +#2740 = CARTESIAN_POINT('',(38.7313736682,0.E+000,59.635617252)); +#2741 = CARTESIAN_POINT('',(38.1379119722,0.E+000,60.4228896845)); +#2742 = CARTESIAN_POINT('',(37.7481596355,0.E+000,61.3451917954)); +#2743 = CARTESIAN_POINT('',(37.6220806636,0.E+000,61.7671596175)); +#2744 = CARTESIAN_POINT('',(37.4645190085,0.E+000,62.6340940238)); +#2745 = CARTESIAN_POINT('',(37.498599238,0.E+000,63.5167813627)); +#2746 = CARTESIAN_POINT('',(37.5642514333,0.E+000,63.95343173)); +#2747 = CARTESIAN_POINT('',(37.7911542119,0.E+000,64.8090231795)); +#2748 = CARTESIAN_POINT('',(38.1928693311,0.E+000,65.5861356819)); +#2749 = CARTESIAN_POINT('',(38.4357809149,0.E+000,65.9506942416)); +#2750 = CARTESIAN_POINT('',(39.0836987059,0.E+000,66.7259300973)); +#2751 = CARTESIAN_POINT('',(39.8753443505,0.E+000,67.2999036622)); +#2752 = CARTESIAN_POINT('',(40.3575517948,0.E+000,67.5541310783)); +#2753 = CARTESIAN_POINT('',(41.0951810667,0.E+000,67.8266734943)); +#2754 = CARTESIAN_POINT('',(41.8111449084,0.E+000,67.95159983)); +#2755 = CARTESIAN_POINT('',(42.0475686146,0.E+000,67.9780142885)); +#2756 = CARTESIAN_POINT('',(42.2775903302,0.E+000,67.9903810602)); +#2757 = CARTESIAN_POINT('',(42.5,0.E+000,67.9903810602)); +#2758 = PCURVE('',#1963,#2759); +#2759 = DEFINITIONAL_REPRESENTATION('',(#2760),#2785); +#2760 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2761,#2762,#2763,#2764,#2765, + #2766,#2767,#2768,#2769,#2770,#2771,#2772,#2773,#2774,#2775,#2776, + #2777,#2778,#2779,#2780,#2781,#2782,#2783,#2784),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165736,7.85828166679, + 10.7238180644,13.5836590156,16.491185538,20.3877609254,22.3658108266 + ),.UNSPECIFIED.); +#2761 = CARTESIAN_POINT('',(42.0096189398,42.5)); +#2762 = CARTESIAN_POINT('',(42.0096189398,42.0328017462)); +#2763 = CARTESIAN_POINT('',(41.9550492597,41.5320145329)); +#2764 = CARTESIAN_POINT('',(41.8300366823,41.0088769679)); +#2765 = CARTESIAN_POINT('',(41.4369328147,40.019938566)); +#2766 = CARTESIAN_POINT('',(40.7516043032,39.1190952593)); +#2767 = CARTESIAN_POINT('',(40.364382748,38.7313736682)); +#2768 = CARTESIAN_POINT('',(39.5771103155,38.1379119722)); +#2769 = CARTESIAN_POINT('',(38.6548082046,37.7481596355)); +#2770 = CARTESIAN_POINT('',(38.2328403825,37.6220806636)); +#2771 = CARTESIAN_POINT('',(37.3659059762,37.4645190085)); +#2772 = CARTESIAN_POINT('',(36.4832186373,37.498599238)); +#2773 = CARTESIAN_POINT('',(36.04656827,37.5642514333)); +#2774 = CARTESIAN_POINT('',(35.1909768205,37.7911542119)); +#2775 = CARTESIAN_POINT('',(34.4138643181,38.1928693311)); +#2776 = CARTESIAN_POINT('',(34.0493057584,38.4357809149)); +#2777 = CARTESIAN_POINT('',(33.2740699027,39.0836987059)); +#2778 = CARTESIAN_POINT('',(32.7000963378,39.8753443505)); +#2779 = CARTESIAN_POINT('',(32.4458689217,40.3575517948)); +#2780 = CARTESIAN_POINT('',(32.1733265057,41.0951810667)); +#2781 = CARTESIAN_POINT('',(32.04840017,41.8111449084)); +#2782 = CARTESIAN_POINT('',(32.0219857115,42.0475686146)); +#2783 = CARTESIAN_POINT('',(32.0096189398,42.2775903302)); +#2784 = CARTESIAN_POINT('',(32.0096189398,42.5)); +#2785 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2786 = PCURVE('',#2787,#2796); +#2787 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2788,#2789,#2790,#2791) + ,(#2792,#2793,#2794,#2795 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2788 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#2789 = CARTESIAN_POINT('',(32.5,10.,67.99038106)); +#2790 = CARTESIAN_POINT('',(32.5,10.,57.99038106)); +#2791 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#2792 = CARTESIAN_POINT('',(42.5,0.E+000,67.99038106)); +#2793 = CARTESIAN_POINT('',(32.5,0.E+000,67.99038106)); +#2794 = CARTESIAN_POINT('',(32.5,0.E+000,57.99038106)); +#2795 = CARTESIAN_POINT('',(42.5,0.E+000,57.99038106)); +#2796 = DEFINITIONAL_REPRESENTATION('',(#2797),#2845); +#2797 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2798,#2799,#2800,#2801,#2802, + #2803,#2804,#2805,#2806,#2807,#2808,#2809,#2810,#2811,#2812,#2813, + #2814,#2815,#2816,#2817,#2818,#2819,#2820,#2821,#2822,#2823,#2824, + #2825,#2826,#2827,#2828,#2829,#2830,#2831,#2832,#2833,#2834,#2835, + #2836,#2837,#2838,#2839,#2840,#2841,#2842,#2843,#2844), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313882423, + 1.016627764845,1.524941647268,2.033255529691,2.541569412114, + 3.049883294536,3.558197176959,4.066511059382,4.574824941805, + 5.083138824227,5.59145270665,6.099766589073,6.608080471495, + 7.116394353918,7.624708236341,8.133022118764,8.641336001186, + 9.149649883609,9.657963766032,10.166277648455,10.674591530877, + 11.1829054133,11.691219295723,12.199533178145,12.707847060568, + 13.216160942991,13.724474825414,14.232788707836,14.741102590259, + 15.249416472682,15.757730355105,16.266044237527,16.77435811995, + 17.282672002373,17.790985884795,18.299299767218,18.807613649641, + 19.315927532064,19.824241414486,20.332555296909,20.840869179332, + 21.349183061755,21.857496944177,22.3658108266),.UNSPECIFIED.); +#2798 = CARTESIAN_POINT('',(10.000998004,30.)); +#2799 = CARTESIAN_POINT('',(10.000998004,29.714213864711)); +#2800 = CARTESIAN_POINT('',(10.000998004,29.148976272306)); +#2801 = CARTESIAN_POINT('',(10.000998004,28.320341045158)); +#2802 = CARTESIAN_POINT('',(10.000998004,27.511224152804)); +#2803 = CARTESIAN_POINT('',(10.000998004,26.721642606208)); +#2804 = CARTESIAN_POINT('',(10.000998004,25.951409908151)); +#2805 = CARTESIAN_POINT('',(10.000998004,25.200126447846)); +#2806 = CARTESIAN_POINT('',(10.000998004,24.46721902166)); +#2807 = CARTESIAN_POINT('',(10.000998004,23.751979084226)); +#2808 = CARTESIAN_POINT('',(10.000998004,23.053639418127)); +#2809 = CARTESIAN_POINT('',(10.000998004,22.371311356097)); +#2810 = CARTESIAN_POINT('',(10.000998004,21.70392601713)); +#2811 = CARTESIAN_POINT('',(10.000998004,21.050316045245)); +#2812 = CARTESIAN_POINT('',(10.000998004,20.409255206665)); +#2813 = CARTESIAN_POINT('',(10.000998004,19.779500799499)); +#2814 = CARTESIAN_POINT('',(10.000998004,19.159817462296)); +#2815 = CARTESIAN_POINT('',(10.000998004,18.54903798876)); +#2816 = CARTESIAN_POINT('',(10.000998004,17.945942144969)); +#2817 = CARTESIAN_POINT('',(10.000998004,17.349215022149)); +#2818 = CARTESIAN_POINT('',(10.000998004,16.757562966067)); +#2819 = CARTESIAN_POINT('',(10.000998004,16.169688650378)); +#2820 = CARTESIAN_POINT('',(10.000998004,15.584299524649)); +#2821 = CARTESIAN_POINT('',(10.000998004,15.000102349728)); +#2822 = CARTESIAN_POINT('',(10.000998004,14.415910950711)); +#2823 = CARTESIAN_POINT('',(10.000998004,13.830503841901)); +#2824 = CARTESIAN_POINT('',(10.000998004,13.242625952391)); +#2825 = CARTESIAN_POINT('',(10.000998004,12.650998045019)); +#2826 = CARTESIAN_POINT('',(10.000998004,12.054322432562)); +#2827 = CARTESIAN_POINT('',(10.000998004,11.451287736086)); +#2828 = CARTESIAN_POINT('',(10.000998004,10.840593660536)); +#2829 = CARTESIAN_POINT('',(10.000998004,10.220965417836)); +#2830 = CARTESIAN_POINT('',(10.000998004,9.59115584836)); +#2831 = CARTESIAN_POINT('',(10.000998004,8.949949242541)); +#2832 = CARTESIAN_POINT('',(10.000998004,8.296178713686)); +#2833 = CARTESIAN_POINT('',(10.000998004,7.628713143824)); +#2834 = CARTESIAN_POINT('',(10.000998004,6.946419419182)); +#2835 = CARTESIAN_POINT('',(10.000998004,6.248219065942)); +#2836 = CARTESIAN_POINT('',(10.000998004,5.53312349106)); +#2837 = CARTESIAN_POINT('',(10.000998004,4.800267308953)); +#2838 = CARTESIAN_POINT('',(10.000998004,4.048935542703)); +#2839 = CARTESIAN_POINT('',(10.000998004,3.278586270346)); +#2840 = CARTESIAN_POINT('',(10.000998004,2.488870496242)); +#2841 = CARTESIAN_POINT('',(10.000998004,1.679678018568)); +#2842 = CARTESIAN_POINT('',(10.000998004,0.851022750959)); +#2843 = CARTESIAN_POINT('',(10.000998004,0.285786201224)); +#2844 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2845 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2846 = FACE_BOUND('',#2847,.T.); +#2847 = EDGE_LOOP('',(#2848,#2968)); +#2848 = ORIENTED_EDGE('',*,*,#2849,.T.); +#2849 = EDGE_CURVE('',#2850,#2852,#2854,.T.); +#2850 = VERTEX_POINT('',#2851); +#2851 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#2852 = VERTEX_POINT('',#2853); +#2853 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#2854 = SURFACE_CURVE('',#2855,(#2880,#2908),.PCURVE_S1.); +#2855 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2856,#2857,#2858,#2859,#2860, + #2861,#2862,#2863,#2864,#2865,#2866,#2867,#2868,#2869,#2870,#2871, + #2872,#2873,#2874,#2875,#2876,#2877,#2878,#2879),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164501,7.85828164811, + 10.7238180535,13.5836589949,16.4911855021,20.3877608686, + 22.3658107291),.UNSPECIFIED.); +#2856 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#2857 = CARTESIAN_POINT('',(20.4671982525,0.E+000,55.)); +#2858 = CARTESIAN_POINT('',(20.9679854642,0.E+000,54.9454303202)); +#2859 = CARTESIAN_POINT('',(21.4911230351,0.E+000,54.8204177413)); +#2860 = CARTESIAN_POINT('',(22.4800614343,0.E+000,54.4273138745)); +#2861 = CARTESIAN_POINT('',(23.38090474,0.E+000,53.7419853637)); +#2862 = CARTESIAN_POINT('',(23.768626333,0.E+000,53.3547638067)); +#2863 = CARTESIAN_POINT('',(24.3620880288,0.E+000,52.5674913739)); +#2864 = CARTESIAN_POINT('',(24.7518403653,0.E+000,51.6451892624)); +#2865 = CARTESIAN_POINT('',(24.8779193361,0.E+000,51.2232214433)); +#2866 = CARTESIAN_POINT('',(25.0354809914,0.E+000,50.3562870372)); +#2867 = CARTESIAN_POINT('',(25.001400762,0.E+000,49.4735996986)); +#2868 = CARTESIAN_POINT('',(24.9357485659,0.E+000,49.0369493251)); +#2869 = CARTESIAN_POINT('',(24.708845788,0.E+000,48.1813578797)); +#2870 = CARTESIAN_POINT('',(24.307130671,0.E+000,47.4042453809)); +#2871 = CARTESIAN_POINT('',(24.0642190822,0.E+000,47.039686815)); +#2872 = CARTESIAN_POINT('',(23.416301294,0.E+000,46.2644509638)); +#2873 = CARTESIAN_POINT('',(22.6246556551,0.E+000,45.6904774019)); +#2874 = CARTESIAN_POINT('',(22.1424481995,0.E+000,45.436249978)); +#2875 = CARTESIAN_POINT('',(21.4048189351,0.E+000,45.1637075654)); +#2876 = CARTESIAN_POINT('',(20.6888551023,0.E+000,45.0387812301)); +#2877 = CARTESIAN_POINT('',(20.4524313759,0.E+000,45.0123667712)); +#2878 = CARTESIAN_POINT('',(20.2224096652,0.E+000,45.)); +#2879 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#2880 = PCURVE('',#1963,#2881); +#2881 = DEFINITIONAL_REPRESENTATION('',(#2882),#2907); +#2882 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2883,#2884,#2885,#2886,#2887, + #2888,#2889,#2890,#2891,#2892,#2893,#2894,#2895,#2896,#2897,#2898, + #2899,#2900,#2901,#2902,#2903,#2904,#2905,#2906),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164501,7.85828164811, + 10.7238180535,13.5836589949,16.4911855021,20.3877608686, + 22.3658107291),.UNSPECIFIED.); +#2883 = CARTESIAN_POINT('',(45.,20.)); +#2884 = CARTESIAN_POINT('',(45.,20.4671982525)); +#2885 = CARTESIAN_POINT('',(45.0545696798,20.9679854642)); +#2886 = CARTESIAN_POINT('',(45.1795822587,21.4911230351)); +#2887 = CARTESIAN_POINT('',(45.5726861255,22.4800614343)); +#2888 = CARTESIAN_POINT('',(46.2580146363,23.38090474)); +#2889 = CARTESIAN_POINT('',(46.6452361933,23.768626333)); +#2890 = CARTESIAN_POINT('',(47.4325086261,24.3620880288)); +#2891 = CARTESIAN_POINT('',(48.3548107376,24.7518403653)); +#2892 = CARTESIAN_POINT('',(48.7767785567,24.8779193361)); +#2893 = CARTESIAN_POINT('',(49.6437129628,25.0354809914)); +#2894 = CARTESIAN_POINT('',(50.5264003014,25.001400762)); +#2895 = CARTESIAN_POINT('',(50.9630506749,24.9357485659)); +#2896 = CARTESIAN_POINT('',(51.8186421203,24.708845788)); +#2897 = CARTESIAN_POINT('',(52.5957546191,24.307130671)); +#2898 = CARTESIAN_POINT('',(52.960313185,24.0642190822)); +#2899 = CARTESIAN_POINT('',(53.7355490362,23.416301294)); +#2900 = CARTESIAN_POINT('',(54.3095225981,22.6246556551)); +#2901 = CARTESIAN_POINT('',(54.563750022,22.1424481995)); +#2902 = CARTESIAN_POINT('',(54.8362924346,21.4048189351)); +#2903 = CARTESIAN_POINT('',(54.9612187699,20.6888551023)); +#2904 = CARTESIAN_POINT('',(54.9876332288,20.4524313759)); +#2905 = CARTESIAN_POINT('',(55.,20.2224096652)); +#2906 = CARTESIAN_POINT('',(55.,20.)); +#2907 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2908 = PCURVE('',#2909,#2918); +#2909 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#2910,#2911,#2912,#2913) + ,(#2914,#2915,#2916,#2917 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#2910 = CARTESIAN_POINT('',(20.,10.,45.)); +#2911 = CARTESIAN_POINT('',(30.,10.,45.)); +#2912 = CARTESIAN_POINT('',(30.,10.,55.)); +#2913 = CARTESIAN_POINT('',(20.,10.,55.)); +#2914 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#2915 = CARTESIAN_POINT('',(30.,0.E+000,45.)); +#2916 = CARTESIAN_POINT('',(30.,0.E+000,55.)); +#2917 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#2918 = DEFINITIONAL_REPRESENTATION('',(#2919),#2967); +#2919 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#2920,#2921,#2922,#2923,#2924, + #2925,#2926,#2927,#2928,#2929,#2930,#2931,#2932,#2933,#2934,#2935, + #2936,#2937,#2938,#2939,#2940,#2941,#2942,#2943,#2944,#2945,#2946, + #2947,#2948,#2949,#2950,#2951,#2952,#2953,#2954,#2955,#2956,#2957, + #2958,#2959,#2960,#2961,#2962,#2963,#2964,#2965,#2966), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880207, + 1.016627760414,1.52494164062,2.033255520827,2.541569401034, + 3.049883281241,3.558197161448,4.066511041655,4.574824921861, + 5.083138802068,5.591452682275,6.099766562482,6.608080442689, + 7.116394322895,7.624708203102,8.133022083309,8.641335963516, + 9.149649843723,9.65796372393,10.166277604136,10.674591484343, + 11.18290536455,11.691219244757,12.199533124964,12.70784700517, + 13.216160885377,13.724474765584,14.232788645791,14.741102525998, + 15.249416406205,15.757730286411,16.266044166618,16.774358046825, + 17.282671927032,17.790985807239,18.299299687445,18.807613567652, + 19.315927447859,19.824241328066,20.332555208273,20.84086908848, + 21.349182968686,21.857496848893,22.3658107291), + .QUASI_UNIFORM_KNOTS.); +#2920 = CARTESIAN_POINT('',(10.000998004,30.)); +#2921 = CARTESIAN_POINT('',(10.000998004,29.714213866027)); +#2922 = CARTESIAN_POINT('',(10.000998004,29.148976275785)); +#2923 = CARTESIAN_POINT('',(10.000998004,28.320341050449)); +#2924 = CARTESIAN_POINT('',(10.000998004,27.511224158067)); +#2925 = CARTESIAN_POINT('',(10.000998004,26.721642610512)); +#2926 = CARTESIAN_POINT('',(10.000998004,25.951409911596)); +#2927 = CARTESIAN_POINT('',(10.000998004,25.20012645143)); +#2928 = CARTESIAN_POINT('',(10.000998004,24.467219026753)); +#2929 = CARTESIAN_POINT('',(10.000998004,23.751979091918)); +#2930 = CARTESIAN_POINT('',(10.000998004,23.053639428221)); +#2931 = CARTESIAN_POINT('',(10.000998004,22.371311367746)); +#2932 = CARTESIAN_POINT('',(10.000998004,21.70392602968)); +#2933 = CARTESIAN_POINT('',(10.000998004,21.050316058458)); +#2934 = CARTESIAN_POINT('',(10.000998004,20.409255220807)); +#2935 = CARTESIAN_POINT('',(10.000998004,19.779500815162)); +#2936 = CARTESIAN_POINT('',(10.000998004,19.159817480195)); +#2937 = CARTESIAN_POINT('',(10.000998004,18.549038008934)); +#2938 = CARTESIAN_POINT('',(10.000998004,17.94594216819)); +#2939 = CARTESIAN_POINT('',(10.000998004,17.349215049709)); +#2940 = CARTESIAN_POINT('',(10.000998004,16.757562998245)); +#2941 = CARTESIAN_POINT('',(10.000998004,16.169688686379)); +#2942 = CARTESIAN_POINT('',(10.000998004,15.584299563168)); +#2943 = CARTESIAN_POINT('',(10.000998004,15.000102390674)); +#2944 = CARTESIAN_POINT('',(10.000998004,14.415910992005)); +#2945 = CARTESIAN_POINT('',(10.000998004,13.830503881918)); +#2946 = CARTESIAN_POINT('',(10.000998004,13.242625991484)); +#2947 = CARTESIAN_POINT('',(10.000998004,12.650998085332)); +#2948 = CARTESIAN_POINT('',(10.000998004,12.054322476506)); +#2949 = CARTESIAN_POINT('',(10.000998004,11.451287782624)); +#2950 = CARTESIAN_POINT('',(10.000998004,10.840593706492)); +#2951 = CARTESIAN_POINT('',(10.000998004,10.220965461471)); +#2952 = CARTESIAN_POINT('',(10.000998004,9.59115589066)); +#2953 = CARTESIAN_POINT('',(10.000998004,8.949949286842)); +#2954 = CARTESIAN_POINT('',(10.000998004,8.296178762516)); +#2955 = CARTESIAN_POINT('',(10.000998004,7.628713195579)); +#2956 = CARTESIAN_POINT('',(10.000998004,6.946419470734)); +#2957 = CARTESIAN_POINT('',(10.000998004,6.248219114497)); +#2958 = CARTESIAN_POINT('',(10.000998004,5.533123535579)); +#2959 = CARTESIAN_POINT('',(10.000998004,4.800267350802)); +#2960 = CARTESIAN_POINT('',(10.000998004,4.04893558527)); +#2961 = CARTESIAN_POINT('',(10.000998004,3.278586317777)); +#2962 = CARTESIAN_POINT('',(10.000998004,2.488870549857)); +#2963 = CARTESIAN_POINT('',(10.000998004,1.679678047808)); +#2964 = CARTESIAN_POINT('',(10.000998004,0.851022751946)); +#2965 = CARTESIAN_POINT('',(10.000998004,0.285786196615)); +#2966 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#2967 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#2968 = ORIENTED_EDGE('',*,*,#2969,.T.); +#2969 = EDGE_CURVE('',#2852,#2850,#2970,.T.); +#2970 = SURFACE_CURVE('',#2971,(#2996,#3024),.PCURVE_S1.); +#2971 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2972,#2973,#2974,#2975,#2976, + #2977,#2978,#2979,#2980,#2981,#2982,#2983,#2984,#2985,#2986,#2987, + #2988,#2989,#2990,#2991,#2992,#2993,#2994,#2995),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164518,7.85828164919, + 10.7238180549,13.583658997,16.491185504,20.3877608712,22.3658107361) + ,.UNSPECIFIED.); +#2972 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#2973 = CARTESIAN_POINT('',(19.5328017475,0.E+000,45.)); +#2974 = CARTESIAN_POINT('',(19.0320145358,0.E+000,45.0545696798)); +#2975 = CARTESIAN_POINT('',(18.508876965,0.E+000,45.1795822587)); +#2976 = CARTESIAN_POINT('',(17.5199385656,0.E+000,45.5726861255)); +#2977 = CARTESIAN_POINT('',(16.6190952599,0.E+000,46.2580146364)); +#2978 = CARTESIAN_POINT('',(16.2313736672,0.E+000,46.645236193)); +#2979 = CARTESIAN_POINT('',(15.6379119712,0.E+000,47.432508626)); +#2980 = CARTESIAN_POINT('',(15.2481596346,0.E+000,48.3548107377)); +#2981 = CARTESIAN_POINT('',(15.122080664,0.E+000,48.7767785566)); +#2982 = CARTESIAN_POINT('',(14.9645190086,0.E+000,49.6437129629)); +#2983 = CARTESIAN_POINT('',(14.998599238,0.E+000,50.5264003017)); +#2984 = CARTESIAN_POINT('',(15.0642514341,0.E+000,50.9630506747)); +#2985 = CARTESIAN_POINT('',(15.2911542119,0.E+000,51.8186421202)); +#2986 = CARTESIAN_POINT('',(15.692869329,0.E+000,52.5957546191)); +#2987 = CARTESIAN_POINT('',(15.9357809178,0.E+000,52.960313185)); +#2988 = CARTESIAN_POINT('',(16.583698706,0.E+000,53.7355490363)); +#2989 = CARTESIAN_POINT('',(17.3753443451,0.E+000,54.3095225982)); +#2990 = CARTESIAN_POINT('',(17.8575518004,0.E+000,54.5637500219)); +#2991 = CARTESIAN_POINT('',(18.5951810654,0.E+000,54.8362924348)); +#2992 = CARTESIAN_POINT('',(19.3111448987,0.E+000,54.96121877)); +#2993 = CARTESIAN_POINT('',(19.5475686231,0.E+000,54.9876332288)); +#2994 = CARTESIAN_POINT('',(19.7775903343,0.E+000,55.)); +#2995 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#2996 = PCURVE('',#1963,#2997); +#2997 = DEFINITIONAL_REPRESENTATION('',(#2998),#3023); +#2998 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#2999,#3000,#3001,#3002,#3003, + #3004,#3005,#3006,#3007,#3008,#3009,#3010,#3011,#3012,#3013,#3014, + #3015,#3016,#3017,#3018,#3019,#3020,#3021,#3022),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164518,7.85828164919, + 10.7238180549,13.583658997,16.491185504,20.3877608712,22.3658107361) + ,.UNSPECIFIED.); +#2999 = CARTESIAN_POINT('',(55.,20.)); +#3000 = CARTESIAN_POINT('',(55.,19.5328017475)); +#3001 = CARTESIAN_POINT('',(54.9454303202,19.0320145358)); +#3002 = CARTESIAN_POINT('',(54.8204177413,18.508876965)); +#3003 = CARTESIAN_POINT('',(54.4273138745,17.5199385656)); +#3004 = CARTESIAN_POINT('',(53.7419853636,16.6190952599)); +#3005 = CARTESIAN_POINT('',(53.354763807,16.2313736672)); +#3006 = CARTESIAN_POINT('',(52.567491374,15.6379119712)); +#3007 = CARTESIAN_POINT('',(51.6451892623,15.2481596346)); +#3008 = CARTESIAN_POINT('',(51.2232214434,15.122080664)); +#3009 = CARTESIAN_POINT('',(50.3562870371,14.9645190086)); +#3010 = CARTESIAN_POINT('',(49.4735996983,14.998599238)); +#3011 = CARTESIAN_POINT('',(49.0369493253,15.0642514341)); +#3012 = CARTESIAN_POINT('',(48.1813578798,15.2911542119)); +#3013 = CARTESIAN_POINT('',(47.4042453809,15.692869329)); +#3014 = CARTESIAN_POINT('',(47.039686815,15.9357809178)); +#3015 = CARTESIAN_POINT('',(46.2644509637,16.583698706)); +#3016 = CARTESIAN_POINT('',(45.6904774018,17.3753443451)); +#3017 = CARTESIAN_POINT('',(45.4362499781,17.8575518004)); +#3018 = CARTESIAN_POINT('',(45.1637075652,18.5951810654)); +#3019 = CARTESIAN_POINT('',(45.03878123,19.3111448987)); +#3020 = CARTESIAN_POINT('',(45.0123667712,19.5475686231)); +#3021 = CARTESIAN_POINT('',(45.,19.7775903343)); +#3022 = CARTESIAN_POINT('',(45.,20.)); +#3023 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3024 = PCURVE('',#3025,#3034); +#3025 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#3026,#3027,#3028,#3029) + ,(#3030,#3031,#3032,#3033 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,10.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#3026 = CARTESIAN_POINT('',(20.,10.,55.)); +#3027 = CARTESIAN_POINT('',(10.,10.,55.)); +#3028 = CARTESIAN_POINT('',(10.,10.,45.)); +#3029 = CARTESIAN_POINT('',(20.,10.,45.)); +#3030 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#3031 = CARTESIAN_POINT('',(10.,0.E+000,55.)); +#3032 = CARTESIAN_POINT('',(10.,0.E+000,45.)); +#3033 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#3034 = DEFINITIONAL_REPRESENTATION('',(#3035),#3083); +#3035 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#3036,#3037,#3038,#3039,#3040, + #3041,#3042,#3043,#3044,#3045,#3046,#3047,#3048,#3049,#3050,#3051, + #3052,#3053,#3054,#3055,#3056,#3057,#3058,#3059,#3060,#3061,#3062, + #3063,#3064,#3065,#3066,#3067,#3068,#3069,#3070,#3071,#3072,#3073, + #3074,#3075,#3076,#3077,#3078,#3079,#3080,#3081,#3082), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880366, + 1.016627760732,1.524941641098,2.033255521464,2.54156940183, + 3.049883282195,3.558197162561,4.066511042927,4.574824923293, + 5.083138803659,5.591452684025,6.099766564391,6.608080444757, + 7.116394325123,7.624708205489,8.133022085855,8.64133596622, + 9.149649846586,9.657963726952,10.166277607318,10.674591487684, + 11.18290536805,11.691219248416,12.199533128782,12.707847009148, + 13.216160889514,13.72447476988,14.232788650245,14.741102530611, + 15.249416410977,15.757730291343,16.266044171709,16.774358052075, + 17.282671932441,17.790985812807,18.299299693173,18.807613573539, + 19.315927453905,19.82424133427,20.332555214636,20.840869095002, + 21.349182975368,21.857496855734,22.3658107361),.UNSPECIFIED.); +#3036 = CARTESIAN_POINT('',(10.000998004,30.)); +#3037 = CARTESIAN_POINT('',(10.000998004,29.714213865947)); +#3038 = CARTESIAN_POINT('',(10.000998004,29.148976275557)); +#3039 = CARTESIAN_POINT('',(10.000998004,28.320341050023)); +#3040 = CARTESIAN_POINT('',(10.000998004,27.51122415747)); +#3041 = CARTESIAN_POINT('',(10.000998004,26.721642609755)); +#3042 = CARTESIAN_POINT('',(10.000998004,25.951409910678)); +#3043 = CARTESIAN_POINT('',(10.000998004,25.200126450341)); +#3044 = CARTESIAN_POINT('',(10.000998004,24.467219025485)); +#3045 = CARTESIAN_POINT('',(10.000998004,23.751979090475)); +#3046 = CARTESIAN_POINT('',(10.000998004,23.053639426626)); +#3047 = CARTESIAN_POINT('',(10.000998004,22.371311366117)); +#3048 = CARTESIAN_POINT('',(10.000998004,21.70392602813)); +#3049 = CARTESIAN_POINT('',(10.000998004,21.05031605703)); +#3050 = CARTESIAN_POINT('',(10.000998004,20.409255219457)); +#3051 = CARTESIAN_POINT('',(10.000998004,19.779500813775)); +#3052 = CARTESIAN_POINT('',(10.000998004,19.159817478642)); +#3053 = CARTESIAN_POINT('',(10.000998004,18.549038007162)); +#3054 = CARTESIAN_POINT('',(10.000998004,17.94594216629)); +#3055 = CARTESIAN_POINT('',(10.000998004,17.349215047768)); +#3056 = CARTESIAN_POINT('',(10.000998004,16.75756299627)); +#3057 = CARTESIAN_POINT('',(10.000998004,16.169688684297)); +#3058 = CARTESIAN_POINT('',(10.000998004,15.584299560881)); +#3059 = CARTESIAN_POINT('',(10.000998004,15.000102388171)); +#3060 = CARTESIAN_POINT('',(10.000998004,14.415910989471)); +#3061 = CARTESIAN_POINT('',(10.000998004,13.83050387949)); +#3062 = CARTESIAN_POINT('',(10.000998004,13.242625989149)); +#3063 = CARTESIAN_POINT('',(10.000998004,12.650998082929)); +#3064 = CARTESIAN_POINT('',(10.000998004,12.054322473875)); +#3065 = CARTESIAN_POINT('',(10.000998004,11.451287779753)); +#3066 = CARTESIAN_POINT('',(10.000998004,10.840593703357)); +#3067 = CARTESIAN_POINT('',(10.000998004,10.220965458061)); +#3068 = CARTESIAN_POINT('',(10.000998004,9.591155886969)); +#3069 = CARTESIAN_POINT('',(10.000998004,8.949949282871)); +#3070 = CARTESIAN_POINT('',(10.000998004,8.296178758235)); +#3071 = CARTESIAN_POINT('',(10.000998004,7.628713191002)); +#3072 = CARTESIAN_POINT('',(10.000998004,6.946419465936)); +#3073 = CARTESIAN_POINT('',(10.000998004,6.248219109545)); +#3074 = CARTESIAN_POINT('',(10.000998004,5.533123530503)); +#3075 = CARTESIAN_POINT('',(10.000998004,4.800267345576)); +#3076 = CARTESIAN_POINT('',(10.000998004,4.048935579824)); +#3077 = CARTESIAN_POINT('',(10.000998004,3.278586312029)); +#3078 = CARTESIAN_POINT('',(10.000998004,2.488870543834)); +#3079 = CARTESIAN_POINT('',(10.000998004,1.67967804453)); +#3080 = CARTESIAN_POINT('',(10.000998004,0.851022751719)); +#3081 = CARTESIAN_POINT('',(10.000998004,0.285786197044)); +#3082 = CARTESIAN_POINT('',(10.000998004,0.E+000)); +#3083 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3084 = ADVANCED_FACE('',(#3085),#1991,.T.); +#3085 = FACE_BOUND('',#3086,.T.); +#3086 = EDGE_LOOP('',(#3087,#3088,#3089,#3112,#3140,#3168)); +#3087 = ORIENTED_EDGE('',*,*,#1975,.T.); +#3088 = ORIENTED_EDGE('',*,*,#2328,.T.); +#3089 = ORIENTED_EDGE('',*,*,#3090,.T.); +#3090 = EDGE_CURVE('',#2299,#3091,#3093,.T.); +#3091 = VERTEX_POINT('',#3092); +#3092 = CARTESIAN_POINT('',(50.,10.,100.)); +#3093 = SURFACE_CURVE('',#3094,(#3098,#3105),.PCURVE_S1.); +#3094 = LINE('',#3095,#3096); +#3095 = CARTESIAN_POINT('',(50.,5.,100.)); +#3096 = VECTOR('',#3097,1.); +#3097 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3098 = PCURVE('',#1991,#3099); +#3099 = DEFINITIONAL_REPRESENTATION('',(#3100),#3104); +#3100 = LINE('',#3101,#3102); +#3101 = CARTESIAN_POINT('',(50.,5.)); +#3102 = VECTOR('',#3103,1.); +#3103 = DIRECTION('',(0.E+000,1.)); +#3104 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3105 = PCURVE('',#2316,#3106); +#3106 = DEFINITIONAL_REPRESENTATION('',(#3107),#3111); +#3107 = LINE('',#3108,#3109); +#3108 = CARTESIAN_POINT('',(0.E+000,5.)); +#3109 = VECTOR('',#3110,1.); +#3110 = DIRECTION('',(0.E+000,1.)); +#3111 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3112 = ORIENTED_EDGE('',*,*,#3113,.T.); +#3113 = EDGE_CURVE('',#3091,#3114,#3116,.T.); +#3114 = VERTEX_POINT('',#3115); +#3115 = CARTESIAN_POINT('',(10.,10.,100.)); +#3116 = SURFACE_CURVE('',#3117,(#3121,#3128),.PCURVE_S1.); +#3117 = LINE('',#3118,#3119); +#3118 = CARTESIAN_POINT('',(30.,10.,100.)); +#3119 = VECTOR('',#3120,1.); +#3120 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3121 = PCURVE('',#1991,#3122); +#3122 = DEFINITIONAL_REPRESENTATION('',(#3123),#3127); +#3123 = LINE('',#3124,#3125); +#3124 = CARTESIAN_POINT('',(30.,10.)); +#3125 = VECTOR('',#3126,1.); +#3126 = DIRECTION('',(-1.,0.E+000)); +#3127 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3128 = PCURVE('',#3129,#3134); +#3129 = PLANE('',#3130); +#3130 = AXIS2_PLACEMENT_3D('',#3131,#3132,#3133); +#3131 = CARTESIAN_POINT('',(50.,10.,100.)); +#3132 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3133 = DIRECTION('',(0.E+000,-0.E+000,1.)); +#3134 = DEFINITIONAL_REPRESENTATION('',(#3135),#3139); +#3135 = LINE('',#3136,#3137); +#3136 = CARTESIAN_POINT('',(0.E+000,-20.)); +#3137 = VECTOR('',#3138,1.); +#3138 = DIRECTION('',(0.E+000,-1.)); +#3139 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3140 = ORIENTED_EDGE('',*,*,#3141,.T.); +#3141 = EDGE_CURVE('',#3114,#3142,#3144,.T.); +#3142 = VERTEX_POINT('',#3143); +#3143 = CARTESIAN_POINT('',(10.,60.,100.)); +#3144 = SURFACE_CURVE('',#3145,(#3149,#3156),.PCURVE_S1.); +#3145 = LINE('',#3146,#3147); +#3146 = CARTESIAN_POINT('',(10.,35.,100.)); +#3147 = VECTOR('',#3148,1.); +#3148 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3149 = PCURVE('',#1991,#3150); +#3150 = DEFINITIONAL_REPRESENTATION('',(#3151),#3155); +#3151 = LINE('',#3152,#3153); +#3152 = CARTESIAN_POINT('',(10.,35.)); +#3153 = VECTOR('',#3154,1.); +#3154 = DIRECTION('',(0.E+000,1.)); +#3155 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3156 = PCURVE('',#3157,#3162); +#3157 = PLANE('',#3158); +#3158 = AXIS2_PLACEMENT_3D('',#3159,#3160,#3161); +#3159 = CARTESIAN_POINT('',(10.,10.,100.)); +#3160 = DIRECTION('',(1.,0.E+000,0.E+000)); +#3161 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3162 = DEFINITIONAL_REPRESENTATION('',(#3163),#3167); +#3163 = LINE('',#3164,#3165); +#3164 = CARTESIAN_POINT('',(0.E+000,25.)); +#3165 = VECTOR('',#3166,1.); +#3166 = DIRECTION('',(0.E+000,1.)); +#3167 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3168 = ORIENTED_EDGE('',*,*,#3169,.T.); +#3169 = EDGE_CURVE('',#3142,#1976,#3170,.T.); +#3170 = SURFACE_CURVE('',#3171,(#3175,#3182),.PCURVE_S1.); +#3171 = LINE('',#3172,#3173); +#3172 = CARTESIAN_POINT('',(5.,60.,100.)); +#3173 = VECTOR('',#3174,1.); +#3174 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3175 = PCURVE('',#1991,#3176); +#3176 = DEFINITIONAL_REPRESENTATION('',(#3177),#3181); +#3177 = LINE('',#3178,#3179); +#3178 = CARTESIAN_POINT('',(5.,60.)); +#3179 = VECTOR('',#3180,1.); +#3180 = DIRECTION('',(-1.,0.E+000)); +#3181 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3182 = PCURVE('',#2019,#3183); +#3183 = DEFINITIONAL_REPRESENTATION('',(#3184),#3188); +#3184 = LINE('',#3185,#3186); +#3185 = CARTESIAN_POINT('',(0.E+000,-5.)); +#3186 = VECTOR('',#3187,1.); +#3187 = DIRECTION('',(0.E+000,-1.)); +#3188 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3189 = ADVANCED_FACE('',(#3190),#2019,.T.); +#3190 = FACE_BOUND('',#3191,.T.); +#3191 = EDGE_LOOP('',(#3192,#3193,#3216,#3237)); +#3192 = ORIENTED_EDGE('',*,*,#3169,.F.); +#3193 = ORIENTED_EDGE('',*,*,#3194,.T.); +#3194 = EDGE_CURVE('',#3142,#3195,#3197,.T.); +#3195 = VERTEX_POINT('',#3196); +#3196 = CARTESIAN_POINT('',(10.,60.,0.E+000)); +#3197 = SURFACE_CURVE('',#3198,(#3202,#3209),.PCURVE_S1.); +#3198 = LINE('',#3199,#3200); +#3199 = CARTESIAN_POINT('',(10.,60.,50.)); +#3200 = VECTOR('',#3201,1.); +#3201 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3202 = PCURVE('',#2019,#3203); +#3203 = DEFINITIONAL_REPRESENTATION('',(#3204),#3208); +#3204 = LINE('',#3205,#3206); +#3205 = CARTESIAN_POINT('',(-50.,0.E+000)); +#3206 = VECTOR('',#3207,1.); +#3207 = DIRECTION('',(-1.,0.E+000)); +#3208 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3209 = PCURVE('',#3157,#3210); +#3210 = DEFINITIONAL_REPRESENTATION('',(#3211),#3215); +#3211 = LINE('',#3212,#3213); +#3212 = CARTESIAN_POINT('',(50.,50.)); +#3213 = VECTOR('',#3214,1.); +#3214 = DIRECTION('',(1.,0.E+000)); +#3215 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3216 = ORIENTED_EDGE('',*,*,#3217,.T.); +#3217 = EDGE_CURVE('',#3195,#2004,#3218,.T.); +#3218 = SURFACE_CURVE('',#3219,(#3223,#3230),.PCURVE_S1.); +#3219 = LINE('',#3220,#3221); +#3220 = CARTESIAN_POINT('',(5.,60.,0.E+000)); +#3221 = VECTOR('',#3222,1.); +#3222 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3223 = PCURVE('',#2019,#3224); +#3224 = DEFINITIONAL_REPRESENTATION('',(#3225),#3229); +#3225 = LINE('',#3226,#3227); +#3226 = CARTESIAN_POINT('',(-100.,-5.)); +#3227 = VECTOR('',#3228,1.); +#3228 = DIRECTION('',(0.E+000,-1.)); +#3229 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3230 = PCURVE('',#2045,#3231); +#3231 = DEFINITIONAL_REPRESENTATION('',(#3232),#3236); +#3232 = LINE('',#3233,#3234); +#3233 = CARTESIAN_POINT('',(-5.,60.)); +#3234 = VECTOR('',#3235,1.); +#3235 = DIRECTION('',(1.,0.E+000)); +#3236 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3237 = ORIENTED_EDGE('',*,*,#2003,.F.); +#3238 = ADVANCED_FACE('',(#3239),#2045,.T.); +#3239 = FACE_BOUND('',#3240,.T.); +#3240 = EDGE_LOOP('',(#3241,#3242,#3243,#3266,#3289,#3310)); +#3241 = ORIENTED_EDGE('',*,*,#2031,.F.); +#3242 = ORIENTED_EDGE('',*,*,#3217,.F.); +#3243 = ORIENTED_EDGE('',*,*,#3244,.F.); +#3244 = EDGE_CURVE('',#3245,#3195,#3247,.T.); +#3245 = VERTEX_POINT('',#3246); +#3246 = CARTESIAN_POINT('',(10.,10.,0.E+000)); +#3247 = SURFACE_CURVE('',#3248,(#3252,#3259),.PCURVE_S1.); +#3248 = LINE('',#3249,#3250); +#3249 = CARTESIAN_POINT('',(10.,35.,0.E+000)); +#3250 = VECTOR('',#3251,1.); +#3251 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3252 = PCURVE('',#2045,#3253); +#3253 = DEFINITIONAL_REPRESENTATION('',(#3254),#3258); +#3254 = LINE('',#3255,#3256); +#3255 = CARTESIAN_POINT('',(-10.,35.)); +#3256 = VECTOR('',#3257,1.); +#3257 = DIRECTION('',(0.E+000,1.)); +#3258 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3259 = PCURVE('',#3157,#3260); +#3260 = DEFINITIONAL_REPRESENTATION('',(#3261),#3265); +#3261 = LINE('',#3262,#3263); +#3262 = CARTESIAN_POINT('',(100.,25.)); +#3263 = VECTOR('',#3264,1.); +#3264 = DIRECTION('',(0.E+000,1.)); +#3265 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3266 = ORIENTED_EDGE('',*,*,#3267,.F.); +#3267 = EDGE_CURVE('',#3268,#3245,#3270,.T.); +#3268 = VERTEX_POINT('',#3269); +#3269 = CARTESIAN_POINT('',(50.,10.,0.E+000)); +#3270 = SURFACE_CURVE('',#3271,(#3275,#3282),.PCURVE_S1.); +#3271 = LINE('',#3272,#3273); +#3272 = CARTESIAN_POINT('',(30.,10.,0.E+000)); +#3273 = VECTOR('',#3274,1.); +#3274 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3275 = PCURVE('',#2045,#3276); +#3276 = DEFINITIONAL_REPRESENTATION('',(#3277),#3281); +#3277 = LINE('',#3278,#3279); +#3278 = CARTESIAN_POINT('',(-30.,10.)); +#3279 = VECTOR('',#3280,1.); +#3280 = DIRECTION('',(1.,0.E+000)); +#3281 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3282 = PCURVE('',#3129,#3283); +#3283 = DEFINITIONAL_REPRESENTATION('',(#3284),#3288); +#3284 = LINE('',#3285,#3286); +#3285 = CARTESIAN_POINT('',(-100.,-20.)); +#3286 = VECTOR('',#3287,1.); +#3287 = DIRECTION('',(0.E+000,-1.)); +#3288 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3289 = ORIENTED_EDGE('',*,*,#3290,.F.); +#3290 = EDGE_CURVE('',#2301,#3268,#3291,.T.); +#3291 = SURFACE_CURVE('',#3292,(#3296,#3303),.PCURVE_S1.); +#3292 = LINE('',#3293,#3294); +#3293 = CARTESIAN_POINT('',(50.,5.,0.E+000)); +#3294 = VECTOR('',#3295,1.); +#3295 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3296 = PCURVE('',#2045,#3297); +#3297 = DEFINITIONAL_REPRESENTATION('',(#3298),#3302); +#3298 = LINE('',#3299,#3300); +#3299 = CARTESIAN_POINT('',(-50.,5.)); +#3300 = VECTOR('',#3301,1.); +#3301 = DIRECTION('',(0.E+000,1.)); +#3302 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3303 = PCURVE('',#2316,#3304); +#3304 = DEFINITIONAL_REPRESENTATION('',(#3305),#3309); +#3305 = LINE('',#3306,#3307); +#3306 = CARTESIAN_POINT('',(100.,5.)); +#3307 = VECTOR('',#3308,1.); +#3308 = DIRECTION('',(0.E+000,1.)); +#3309 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3310 = ORIENTED_EDGE('',*,*,#2350,.F.); +#3311 = ADVANCED_FACE('',(#3312),#2119,.T.); +#3312 = FACE_BOUND('',#3313,.T.); +#3313 = EDGE_LOOP('',(#3314,#3341,#3361,#3362)); +#3314 = ORIENTED_EDGE('',*,*,#3315,.F.); +#3315 = EDGE_CURVE('',#3316,#3318,#3320,.T.); +#3316 = VERTEX_POINT('',#3317); +#3317 = CARTESIAN_POINT('',(10.,40.,55.)); +#3318 = VERTEX_POINT('',#3319); +#3319 = CARTESIAN_POINT('',(10.,40.,45.)); +#3320 = SURFACE_CURVE('',#3321,(#3326,#3333),.PCURVE_S1.); +#3321 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3322,#3323,#3324,#3325), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3322 = CARTESIAN_POINT('',(10.,40.,55.)); +#3323 = CARTESIAN_POINT('',(10.,50.,55.)); +#3324 = CARTESIAN_POINT('',(10.,50.,45.)); +#3325 = CARTESIAN_POINT('',(10.,40.,45.)); +#3326 = PCURVE('',#2119,#3327); +#3327 = DEFINITIONAL_REPRESENTATION('',(#3328),#3332); +#3328 = LINE('',#3329,#3330); +#3329 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3330 = VECTOR('',#3331,1.); +#3331 = DIRECTION('',(0.E+000,1.)); +#3332 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3333 = PCURVE('',#3157,#3334); +#3334 = DEFINITIONAL_REPRESENTATION('',(#3335),#3340); +#3335 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3336,#3337,#3338,#3339), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3336 = CARTESIAN_POINT('',(45.,30.)); +#3337 = CARTESIAN_POINT('',(45.,40.)); +#3338 = CARTESIAN_POINT('',(55.,40.)); +#3339 = CARTESIAN_POINT('',(55.,30.)); +#3340 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3341 = ORIENTED_EDGE('',*,*,#3342,.T.); +#3342 = EDGE_CURVE('',#3316,#2062,#3343,.T.); +#3343 = SURFACE_CURVE('',#3344,(#3347,#3354),.PCURVE_S1.); +#3344 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3345,#3346),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3345 = CARTESIAN_POINT('',(10.,40.,55.)); +#3346 = CARTESIAN_POINT('',(0.E+000,40.,55.)); +#3347 = PCURVE('',#2119,#3348); +#3348 = DEFINITIONAL_REPRESENTATION('',(#3349),#3353); +#3349 = LINE('',#3350,#3351); +#3350 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3351 = VECTOR('',#3352,1.); +#3352 = DIRECTION('',(1.,0.E+000)); +#3353 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3354 = PCURVE('',#2235,#3355); +#3355 = DEFINITIONAL_REPRESENTATION('',(#3356),#3360); +#3356 = LINE('',#3357,#3358); +#3357 = CARTESIAN_POINT('',(0.E+000,30.)); +#3358 = VECTOR('',#3359,1.); +#3359 = DIRECTION('',(1.,0.E+000)); +#3360 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3361 = ORIENTED_EDGE('',*,*,#2059,.F.); +#3362 = ORIENTED_EDGE('',*,*,#3363,.F.); +#3363 = EDGE_CURVE('',#3318,#2060,#3364,.T.); +#3364 = SURFACE_CURVE('',#3365,(#3368,#3375),.PCURVE_S1.); +#3365 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3366,#3367),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3366 = CARTESIAN_POINT('',(10.,40.,45.)); +#3367 = CARTESIAN_POINT('',(0.E+000,40.,45.)); +#3368 = PCURVE('',#2119,#3369); +#3369 = DEFINITIONAL_REPRESENTATION('',(#3370),#3374); +#3370 = LINE('',#3371,#3372); +#3371 = CARTESIAN_POINT('',(0.E+000,30.)); +#3372 = VECTOR('',#3373,1.); +#3373 = DIRECTION('',(1.,0.E+000)); +#3374 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3375 = PCURVE('',#2235,#3376); +#3376 = DEFINITIONAL_REPRESENTATION('',(#3377),#3381); +#3377 = LINE('',#3378,#3379); +#3378 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3379 = VECTOR('',#3380,1.); +#3380 = DIRECTION('',(1.,0.E+000)); +#3381 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3382 = ADVANCED_FACE('',(#3383),#2235,.T.); +#3383 = FACE_BOUND('',#3384,.T.); +#3384 = EDGE_LOOP('',(#3385,#3408,#3409,#3410)); +#3385 = ORIENTED_EDGE('',*,*,#3386,.F.); +#3386 = EDGE_CURVE('',#3318,#3316,#3387,.T.); +#3387 = SURFACE_CURVE('',#3388,(#3393,#3400),.PCURVE_S1.); +#3388 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3389,#3390,#3391,#3392), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3389 = CARTESIAN_POINT('',(10.,40.,45.)); +#3390 = CARTESIAN_POINT('',(10.,30.,45.)); +#3391 = CARTESIAN_POINT('',(10.,30.,55.)); +#3392 = CARTESIAN_POINT('',(10.,40.,55.)); +#3393 = PCURVE('',#2235,#3394); +#3394 = DEFINITIONAL_REPRESENTATION('',(#3395),#3399); +#3395 = LINE('',#3396,#3397); +#3396 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3397 = VECTOR('',#3398,1.); +#3398 = DIRECTION('',(0.E+000,1.)); +#3399 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3400 = PCURVE('',#3157,#3401); +#3401 = DEFINITIONAL_REPRESENTATION('',(#3402),#3407); +#3402 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3403,#3404,#3405,#3406), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3403 = CARTESIAN_POINT('',(55.,30.)); +#3404 = CARTESIAN_POINT('',(55.,20.)); +#3405 = CARTESIAN_POINT('',(45.,20.)); +#3406 = CARTESIAN_POINT('',(45.,30.)); +#3407 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3408 = ORIENTED_EDGE('',*,*,#3363,.T.); +#3409 = ORIENTED_EDGE('',*,*,#2179,.F.); +#3410 = ORIENTED_EDGE('',*,*,#3342,.F.); +#3411 = ADVANCED_FACE('',(#3412),#2316,.T.); +#3412 = FACE_BOUND('',#3413,.T.); +#3413 = EDGE_LOOP('',(#3414,#3435,#3436,#3437)); +#3414 = ORIENTED_EDGE('',*,*,#3415,.F.); +#3415 = EDGE_CURVE('',#3091,#3268,#3416,.T.); +#3416 = SURFACE_CURVE('',#3417,(#3421,#3428),.PCURVE_S1.); +#3417 = LINE('',#3418,#3419); +#3418 = CARTESIAN_POINT('',(50.,10.,50.)); +#3419 = VECTOR('',#3420,1.); +#3420 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3421 = PCURVE('',#2316,#3422); +#3422 = DEFINITIONAL_REPRESENTATION('',(#3423),#3427); +#3423 = LINE('',#3424,#3425); +#3424 = CARTESIAN_POINT('',(50.,10.)); +#3425 = VECTOR('',#3426,1.); +#3426 = DIRECTION('',(1.,0.E+000)); +#3427 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3428 = PCURVE('',#3129,#3429); +#3429 = DEFINITIONAL_REPRESENTATION('',(#3430),#3434); +#3430 = LINE('',#3431,#3432); +#3431 = CARTESIAN_POINT('',(-50.,0.E+000)); +#3432 = VECTOR('',#3433,1.); +#3433 = DIRECTION('',(-1.,0.E+000)); +#3434 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3435 = ORIENTED_EDGE('',*,*,#3090,.F.); +#3436 = ORIENTED_EDGE('',*,*,#2298,.T.); +#3437 = ORIENTED_EDGE('',*,*,#3290,.T.); +#3438 = ADVANCED_FACE('',(#3439),#2433,.T.); +#3439 = FACE_BOUND('',#3440,.T.); +#3440 = EDGE_LOOP('',(#3441,#3468,#3488,#3489)); +#3441 = ORIENTED_EDGE('',*,*,#3442,.F.); +#3442 = EDGE_CURVE('',#3443,#3445,#3447,.T.); +#3443 = VERTEX_POINT('',#3444); +#3444 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#3445 = VERTEX_POINT('',#3446); +#3446 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#3447 = SURFACE_CURVE('',#3448,(#3453,#3460),.PCURVE_S1.); +#3448 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3449,#3450,#3451,#3452), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3449 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#3450 = CARTESIAN_POINT('',(52.5,10.,32.00961894)); +#3451 = CARTESIAN_POINT('',(52.5,10.,42.00961894)); +#3452 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#3453 = PCURVE('',#2433,#3454); +#3454 = DEFINITIONAL_REPRESENTATION('',(#3455),#3459); +#3455 = LINE('',#3456,#3457); +#3456 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3457 = VECTOR('',#3458,1.); +#3458 = DIRECTION('',(0.E+000,1.)); +#3459 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3460 = PCURVE('',#3129,#3461); +#3461 = DEFINITIONAL_REPRESENTATION('',(#3462),#3467); +#3462 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3463,#3464,#3465,#3466), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3463 = CARTESIAN_POINT('',(-67.99038106,-7.5)); +#3464 = CARTESIAN_POINT('',(-67.99038106,2.5)); +#3465 = CARTESIAN_POINT('',(-57.99038106,2.5)); +#3466 = CARTESIAN_POINT('',(-57.99038106,-7.5)); +#3467 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3468 = ORIENTED_EDGE('',*,*,#3469,.T.); +#3469 = EDGE_CURVE('',#3443,#2376,#3470,.T.); +#3470 = SURFACE_CURVE('',#3471,(#3474,#3481),.PCURVE_S1.); +#3471 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3472,#3473),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3472 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#3473 = CARTESIAN_POINT('',(42.5,0.E+000,32.00961894)); +#3474 = PCURVE('',#2433,#3475); +#3475 = DEFINITIONAL_REPRESENTATION('',(#3476),#3480); +#3476 = LINE('',#3477,#3478); +#3477 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3478 = VECTOR('',#3479,1.); +#3479 = DIRECTION('',(1.,0.E+000)); +#3480 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3481 = PCURVE('',#2549,#3482); +#3482 = DEFINITIONAL_REPRESENTATION('',(#3483),#3487); +#3483 = LINE('',#3484,#3485); +#3484 = CARTESIAN_POINT('',(0.E+000,30.)); +#3485 = VECTOR('',#3486,1.); +#3486 = DIRECTION('',(1.,0.E+000)); +#3487 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3488 = ORIENTED_EDGE('',*,*,#2373,.F.); +#3489 = ORIENTED_EDGE('',*,*,#3490,.F.); +#3490 = EDGE_CURVE('',#3445,#2374,#3491,.T.); +#3491 = SURFACE_CURVE('',#3492,(#3495,#3502),.PCURVE_S1.); +#3492 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3493,#3494),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3493 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#3494 = CARTESIAN_POINT('',(42.5,0.E+000,42.00961894)); +#3495 = PCURVE('',#2433,#3496); +#3496 = DEFINITIONAL_REPRESENTATION('',(#3497),#3501); +#3497 = LINE('',#3498,#3499); +#3498 = CARTESIAN_POINT('',(0.E+000,30.)); +#3499 = VECTOR('',#3500,1.); +#3500 = DIRECTION('',(1.,0.E+000)); +#3501 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3502 = PCURVE('',#2549,#3503); +#3503 = DEFINITIONAL_REPRESENTATION('',(#3504),#3508); +#3504 = LINE('',#3505,#3506); +#3505 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3506 = VECTOR('',#3507,1.); +#3507 = DIRECTION('',(1.,0.E+000)); +#3508 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3509 = ADVANCED_FACE('',(#3510),#2549,.T.); +#3510 = FACE_BOUND('',#3511,.T.); +#3511 = EDGE_LOOP('',(#3512,#3535,#3536,#3537)); +#3512 = ORIENTED_EDGE('',*,*,#3513,.F.); +#3513 = EDGE_CURVE('',#3445,#3443,#3514,.T.); +#3514 = SURFACE_CURVE('',#3515,(#3520,#3527),.PCURVE_S1.); +#3515 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3516,#3517,#3518,#3519), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3516 = CARTESIAN_POINT('',(42.5,10.,42.00961894)); +#3517 = CARTESIAN_POINT('',(32.5,10.,42.00961894)); +#3518 = CARTESIAN_POINT('',(32.5,10.,32.00961894)); +#3519 = CARTESIAN_POINT('',(42.5,10.,32.00961894)); +#3520 = PCURVE('',#2549,#3521); +#3521 = DEFINITIONAL_REPRESENTATION('',(#3522),#3526); +#3522 = LINE('',#3523,#3524); +#3523 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3524 = VECTOR('',#3525,1.); +#3525 = DIRECTION('',(0.E+000,1.)); +#3526 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3527 = PCURVE('',#3129,#3528); +#3528 = DEFINITIONAL_REPRESENTATION('',(#3529),#3534); +#3529 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3530,#3531,#3532,#3533), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3530 = CARTESIAN_POINT('',(-57.99038106,-7.5)); +#3531 = CARTESIAN_POINT('',(-57.99038106,-17.5)); +#3532 = CARTESIAN_POINT('',(-67.99038106,-17.5)); +#3533 = CARTESIAN_POINT('',(-67.99038106,-7.5)); +#3534 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3535 = ORIENTED_EDGE('',*,*,#3490,.T.); +#3536 = ORIENTED_EDGE('',*,*,#2493,.F.); +#3537 = ORIENTED_EDGE('',*,*,#3469,.F.); +#3538 = ADVANCED_FACE('',(#3539),#2671,.T.); +#3539 = FACE_BOUND('',#3540,.T.); +#3540 = EDGE_LOOP('',(#3541,#3568,#3588,#3589)); +#3541 = ORIENTED_EDGE('',*,*,#3542,.F.); +#3542 = EDGE_CURVE('',#3543,#3545,#3547,.T.); +#3543 = VERTEX_POINT('',#3544); +#3544 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#3545 = VERTEX_POINT('',#3546); +#3546 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#3547 = SURFACE_CURVE('',#3548,(#3553,#3560),.PCURVE_S1.); +#3548 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3549,#3550,#3551,#3552), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3549 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#3550 = CARTESIAN_POINT('',(52.5,10.,57.99038106)); +#3551 = CARTESIAN_POINT('',(52.5,10.,67.99038106)); +#3552 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#3553 = PCURVE('',#2671,#3554); +#3554 = DEFINITIONAL_REPRESENTATION('',(#3555),#3559); +#3555 = LINE('',#3556,#3557); +#3556 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3557 = VECTOR('',#3558,1.); +#3558 = DIRECTION('',(0.E+000,1.)); +#3559 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3560 = PCURVE('',#3129,#3561); +#3561 = DEFINITIONAL_REPRESENTATION('',(#3562),#3567); +#3562 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3563,#3564,#3565,#3566), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3563 = CARTESIAN_POINT('',(-42.00961894,-7.5)); +#3564 = CARTESIAN_POINT('',(-42.00961894,2.5)); +#3565 = CARTESIAN_POINT('',(-32.00961894,2.5)); +#3566 = CARTESIAN_POINT('',(-32.00961894,-7.5)); +#3567 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3568 = ORIENTED_EDGE('',*,*,#3569,.T.); +#3569 = EDGE_CURVE('',#3543,#2614,#3570,.T.); +#3570 = SURFACE_CURVE('',#3571,(#3574,#3581),.PCURVE_S1.); +#3571 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3572,#3573),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3572 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#3573 = CARTESIAN_POINT('',(42.5,0.E+000,57.99038106)); +#3574 = PCURVE('',#2671,#3575); +#3575 = DEFINITIONAL_REPRESENTATION('',(#3576),#3580); +#3576 = LINE('',#3577,#3578); +#3577 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3578 = VECTOR('',#3579,1.); +#3579 = DIRECTION('',(1.,0.E+000)); +#3580 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3581 = PCURVE('',#2787,#3582); +#3582 = DEFINITIONAL_REPRESENTATION('',(#3583),#3587); +#3583 = LINE('',#3584,#3585); +#3584 = CARTESIAN_POINT('',(0.E+000,30.)); +#3585 = VECTOR('',#3586,1.); +#3586 = DIRECTION('',(1.,0.E+000)); +#3587 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3588 = ORIENTED_EDGE('',*,*,#2611,.F.); +#3589 = ORIENTED_EDGE('',*,*,#3590,.F.); +#3590 = EDGE_CURVE('',#3545,#2612,#3591,.T.); +#3591 = SURFACE_CURVE('',#3592,(#3595,#3602),.PCURVE_S1.); +#3592 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3593,#3594),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3593 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#3594 = CARTESIAN_POINT('',(42.5,0.E+000,67.99038106)); +#3595 = PCURVE('',#2671,#3596); +#3596 = DEFINITIONAL_REPRESENTATION('',(#3597),#3601); +#3597 = LINE('',#3598,#3599); +#3598 = CARTESIAN_POINT('',(0.E+000,30.)); +#3599 = VECTOR('',#3600,1.); +#3600 = DIRECTION('',(1.,0.E+000)); +#3601 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3602 = PCURVE('',#2787,#3603); +#3603 = DEFINITIONAL_REPRESENTATION('',(#3604),#3608); +#3604 = LINE('',#3605,#3606); +#3605 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3606 = VECTOR('',#3607,1.); +#3607 = DIRECTION('',(1.,0.E+000)); +#3608 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3609 = ADVANCED_FACE('',(#3610),#2787,.T.); +#3610 = FACE_BOUND('',#3611,.T.); +#3611 = EDGE_LOOP('',(#3612,#3635,#3636,#3637)); +#3612 = ORIENTED_EDGE('',*,*,#3613,.F.); +#3613 = EDGE_CURVE('',#3545,#3543,#3614,.T.); +#3614 = SURFACE_CURVE('',#3615,(#3620,#3627),.PCURVE_S1.); +#3615 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3616,#3617,#3618,#3619), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3616 = CARTESIAN_POINT('',(42.5,10.,67.99038106)); +#3617 = CARTESIAN_POINT('',(32.5,10.,67.99038106)); +#3618 = CARTESIAN_POINT('',(32.5,10.,57.99038106)); +#3619 = CARTESIAN_POINT('',(42.5,10.,57.99038106)); +#3620 = PCURVE('',#2787,#3621); +#3621 = DEFINITIONAL_REPRESENTATION('',(#3622),#3626); +#3622 = LINE('',#3623,#3624); +#3623 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3624 = VECTOR('',#3625,1.); +#3625 = DIRECTION('',(0.E+000,1.)); +#3626 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3627 = PCURVE('',#3129,#3628); +#3628 = DEFINITIONAL_REPRESENTATION('',(#3629),#3634); +#3629 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3630,#3631,#3632,#3633), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3630 = CARTESIAN_POINT('',(-32.00961894,-7.5)); +#3631 = CARTESIAN_POINT('',(-32.00961894,-17.5)); +#3632 = CARTESIAN_POINT('',(-42.00961894,-17.5)); +#3633 = CARTESIAN_POINT('',(-42.00961894,-7.5)); +#3634 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3635 = ORIENTED_EDGE('',*,*,#3590,.T.); +#3636 = ORIENTED_EDGE('',*,*,#2731,.F.); +#3637 = ORIENTED_EDGE('',*,*,#3569,.F.); +#3638 = ADVANCED_FACE('',(#3639),#2909,.T.); +#3639 = FACE_BOUND('',#3640,.T.); +#3640 = EDGE_LOOP('',(#3641,#3668,#3688,#3689)); +#3641 = ORIENTED_EDGE('',*,*,#3642,.F.); +#3642 = EDGE_CURVE('',#3643,#3645,#3647,.T.); +#3643 = VERTEX_POINT('',#3644); +#3644 = CARTESIAN_POINT('',(20.,10.,45.)); +#3645 = VERTEX_POINT('',#3646); +#3646 = CARTESIAN_POINT('',(20.,10.,55.)); +#3647 = SURFACE_CURVE('',#3648,(#3653,#3660),.PCURVE_S1.); +#3648 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3649,#3650,#3651,#3652), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3649 = CARTESIAN_POINT('',(20.,10.,45.)); +#3650 = CARTESIAN_POINT('',(30.,10.,45.)); +#3651 = CARTESIAN_POINT('',(30.,10.,55.)); +#3652 = CARTESIAN_POINT('',(20.,10.,55.)); +#3653 = PCURVE('',#2909,#3654); +#3654 = DEFINITIONAL_REPRESENTATION('',(#3655),#3659); +#3655 = LINE('',#3656,#3657); +#3656 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3657 = VECTOR('',#3658,1.); +#3658 = DIRECTION('',(0.E+000,1.)); +#3659 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3660 = PCURVE('',#3129,#3661); +#3661 = DEFINITIONAL_REPRESENTATION('',(#3662),#3667); +#3662 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3663,#3664,#3665,#3666), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3663 = CARTESIAN_POINT('',(-55.,-30.)); +#3664 = CARTESIAN_POINT('',(-55.,-20.)); +#3665 = CARTESIAN_POINT('',(-45.,-20.)); +#3666 = CARTESIAN_POINT('',(-45.,-30.)); +#3667 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3668 = ORIENTED_EDGE('',*,*,#3669,.T.); +#3669 = EDGE_CURVE('',#3643,#2852,#3670,.T.); +#3670 = SURFACE_CURVE('',#3671,(#3674,#3681),.PCURVE_S1.); +#3671 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3672,#3673),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3672 = CARTESIAN_POINT('',(20.,10.,45.)); +#3673 = CARTESIAN_POINT('',(20.,0.E+000,45.)); +#3674 = PCURVE('',#2909,#3675); +#3675 = DEFINITIONAL_REPRESENTATION('',(#3676),#3680); +#3676 = LINE('',#3677,#3678); +#3677 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3678 = VECTOR('',#3679,1.); +#3679 = DIRECTION('',(1.,0.E+000)); +#3680 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3681 = PCURVE('',#3025,#3682); +#3682 = DEFINITIONAL_REPRESENTATION('',(#3683),#3687); +#3683 = LINE('',#3684,#3685); +#3684 = CARTESIAN_POINT('',(0.E+000,30.)); +#3685 = VECTOR('',#3686,1.); +#3686 = DIRECTION('',(1.,0.E+000)); +#3687 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3688 = ORIENTED_EDGE('',*,*,#2849,.F.); +#3689 = ORIENTED_EDGE('',*,*,#3690,.F.); +#3690 = EDGE_CURVE('',#3645,#2850,#3691,.T.); +#3691 = SURFACE_CURVE('',#3692,(#3695,#3702),.PCURVE_S1.); +#3692 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#3693,#3694),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,10.000998004),.PIECEWISE_BEZIER_KNOTS.); +#3693 = CARTESIAN_POINT('',(20.,10.,55.)); +#3694 = CARTESIAN_POINT('',(20.,0.E+000,55.)); +#3695 = PCURVE('',#2909,#3696); +#3696 = DEFINITIONAL_REPRESENTATION('',(#3697),#3701); +#3697 = LINE('',#3698,#3699); +#3698 = CARTESIAN_POINT('',(0.E+000,30.)); +#3699 = VECTOR('',#3700,1.); +#3700 = DIRECTION('',(1.,0.E+000)); +#3701 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3702 = PCURVE('',#3025,#3703); +#3703 = DEFINITIONAL_REPRESENTATION('',(#3704),#3708); +#3704 = LINE('',#3705,#3706); +#3705 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#3706 = VECTOR('',#3707,1.); +#3707 = DIRECTION('',(1.,0.E+000)); +#3708 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3709 = ADVANCED_FACE('',(#3710),#3025,.T.); +#3710 = FACE_BOUND('',#3711,.T.); +#3711 = EDGE_LOOP('',(#3712,#3735,#3736,#3737)); +#3712 = ORIENTED_EDGE('',*,*,#3713,.F.); +#3713 = EDGE_CURVE('',#3645,#3643,#3714,.T.); +#3714 = SURFACE_CURVE('',#3715,(#3720,#3727),.PCURVE_S1.); +#3715 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3716,#3717,#3718,#3719), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3716 = CARTESIAN_POINT('',(20.,10.,55.)); +#3717 = CARTESIAN_POINT('',(10.,10.,55.)); +#3718 = CARTESIAN_POINT('',(10.,10.,45.)); +#3719 = CARTESIAN_POINT('',(20.,10.,45.)); +#3720 = PCURVE('',#3025,#3721); +#3721 = DEFINITIONAL_REPRESENTATION('',(#3722),#3726); +#3722 = LINE('',#3723,#3724); +#3723 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#3724 = VECTOR('',#3725,1.); +#3725 = DIRECTION('',(0.E+000,1.)); +#3726 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3727 = PCURVE('',#3129,#3728); +#3728 = DEFINITIONAL_REPRESENTATION('',(#3729),#3734); +#3729 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#3730,#3731,#3732,#3733), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#3730 = CARTESIAN_POINT('',(-45.,-30.)); +#3731 = CARTESIAN_POINT('',(-45.,-40.)); +#3732 = CARTESIAN_POINT('',(-55.,-40.)); +#3733 = CARTESIAN_POINT('',(-55.,-30.)); +#3734 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3735 = ORIENTED_EDGE('',*,*,#3690,.T.); +#3736 = ORIENTED_EDGE('',*,*,#2969,.F.); +#3737 = ORIENTED_EDGE('',*,*,#3669,.F.); +#3738 = ADVANCED_FACE('',(#3739,#3765,#3769,#3773),#3129,.T.); +#3739 = FACE_BOUND('',#3740,.T.); +#3740 = EDGE_LOOP('',(#3741,#3762,#3763,#3764)); +#3741 = ORIENTED_EDGE('',*,*,#3742,.F.); +#3742 = EDGE_CURVE('',#3114,#3245,#3743,.T.); +#3743 = SURFACE_CURVE('',#3744,(#3748,#3755),.PCURVE_S1.); +#3744 = LINE('',#3745,#3746); +#3745 = CARTESIAN_POINT('',(10.,10.,50.)); +#3746 = VECTOR('',#3747,1.); +#3747 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3748 = PCURVE('',#3129,#3749); +#3749 = DEFINITIONAL_REPRESENTATION('',(#3750),#3754); +#3750 = LINE('',#3751,#3752); +#3751 = CARTESIAN_POINT('',(-50.,-40.)); +#3752 = VECTOR('',#3753,1.); +#3753 = DIRECTION('',(-1.,0.E+000)); +#3754 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3755 = PCURVE('',#3157,#3756); +#3756 = DEFINITIONAL_REPRESENTATION('',(#3757),#3761); +#3757 = LINE('',#3758,#3759); +#3758 = CARTESIAN_POINT('',(50.,0.E+000)); +#3759 = VECTOR('',#3760,1.); +#3760 = DIRECTION('',(1.,0.E+000)); +#3761 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3762 = ORIENTED_EDGE('',*,*,#3113,.F.); +#3763 = ORIENTED_EDGE('',*,*,#3415,.T.); +#3764 = ORIENTED_EDGE('',*,*,#3267,.T.); +#3765 = FACE_BOUND('',#3766,.T.); +#3766 = EDGE_LOOP('',(#3767,#3768)); +#3767 = ORIENTED_EDGE('',*,*,#3442,.T.); +#3768 = ORIENTED_EDGE('',*,*,#3513,.T.); +#3769 = FACE_BOUND('',#3770,.T.); +#3770 = EDGE_LOOP('',(#3771,#3772)); +#3771 = ORIENTED_EDGE('',*,*,#3542,.T.); +#3772 = ORIENTED_EDGE('',*,*,#3613,.T.); +#3773 = FACE_BOUND('',#3774,.T.); +#3774 = EDGE_LOOP('',(#3775,#3776)); +#3775 = ORIENTED_EDGE('',*,*,#3642,.T.); +#3776 = ORIENTED_EDGE('',*,*,#3713,.T.); +#3777 = ADVANCED_FACE('',(#3778,#3784),#3157,.T.); +#3778 = FACE_BOUND('',#3779,.T.); +#3779 = EDGE_LOOP('',(#3780,#3781,#3782,#3783)); +#3780 = ORIENTED_EDGE('',*,*,#3194,.F.); +#3781 = ORIENTED_EDGE('',*,*,#3141,.F.); +#3782 = ORIENTED_EDGE('',*,*,#3742,.T.); +#3783 = ORIENTED_EDGE('',*,*,#3244,.T.); +#3784 = FACE_BOUND('',#3785,.T.); +#3785 = EDGE_LOOP('',(#3786,#3787)); +#3786 = ORIENTED_EDGE('',*,*,#3315,.T.); +#3787 = ORIENTED_EDGE('',*,*,#3386,.T.); +#3788 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#3792)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#3789,#3790,#3791)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#3789 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#3790 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#3791 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#3792 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(5.E-006),#3789, + 'distance_accuracy_value','confusion accuracy'); +#3793 = SHAPE_DEFINITION_REPRESENTATION(#3794,#1933); +#3794 = PRODUCT_DEFINITION_SHAPE('','',#3795); +#3795 = PRODUCT_DEFINITION('design','',#3796,#3799); +#3796 = PRODUCT_DEFINITION_FORMATION('','',#3797); +#3797 = PRODUCT('l-bracket','l-bracket','',(#3798)); +#3798 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#3799 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#3800 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3801,#3803); +#3801 = ( REPRESENTATION_RELATIONSHIP('','',#1933,#1146) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3802) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#3802 = ITEM_DEFINED_TRANSFORMATION('','',#11,#1159); +#3803 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #3804); +#3804 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('10','l-bracket_1','',#1141,#3795 + ,$); +#3805 = PRODUCT_TYPE('part',$,(#3797)); +#3806 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#3807,#3809); +#3807 = ( REPRESENTATION_RELATIONSHIP('','',#1146,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#3808) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#3808 = ITEM_DEFINED_TRANSFORMATION('','',#11,#19); +#3809 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #3810); +#3810 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('11','l-bracket-assembly_1','',#5 + ,#1141,$); +#3811 = PRODUCT_TYPE('part',$,(#1143)); +#3812 = ADVANCED_BREP_SHAPE_REPRESENTATION('',(#11,#3813),#6195); +#3813 = MANIFOLD_SOLID_BREP('',#3814); +#3814 = CLOSED_SHELL('',(#3815,#5363,#5439,#5488,#5537,#5564,#5635,#5664 + ,#5735,#5764,#5835,#5864,#5935,#5964,#6035,#6064,#6135,#6164)); +#3815 = ADVANCED_FACE('',(#3816,#3935,#4173,#4411,#4649,#4887,#5125), + #3830,.T.); +#3816 = FACE_BOUND('',#3817,.T.); +#3817 = EDGE_LOOP('',(#3818,#3853,#3881,#3909)); +#3818 = ORIENTED_EDGE('',*,*,#3819,.F.); +#3819 = EDGE_CURVE('',#3820,#3822,#3824,.T.); +#3820 = VERTEX_POINT('',#3821); +#3821 = CARTESIAN_POINT('',(180.,0.E+000,20.)); +#3822 = VERTEX_POINT('',#3823); +#3823 = CARTESIAN_POINT('',(0.E+000,0.E+000,20.)); +#3824 = SURFACE_CURVE('',#3825,(#3829,#3841),.PCURVE_S1.); +#3825 = LINE('',#3826,#3827); +#3826 = CARTESIAN_POINT('',(90.,0.E+000,20.)); +#3827 = VECTOR('',#3828,1.); +#3828 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3829 = PCURVE('',#3830,#3835); +#3830 = PLANE('',#3831); +#3831 = AXIS2_PLACEMENT_3D('',#3832,#3833,#3834); +#3832 = CARTESIAN_POINT('',(90.,75.,20.)); +#3833 = DIRECTION('',(0.E+000,0.E+000,1.)); +#3834 = DIRECTION('',(1.,0.E+000,-0.E+000)); +#3835 = DEFINITIONAL_REPRESENTATION('',(#3836),#3840); +#3836 = LINE('',#3837,#3838); +#3837 = CARTESIAN_POINT('',(0.E+000,-75.)); +#3838 = VECTOR('',#3839,1.); +#3839 = DIRECTION('',(-1.,0.E+000)); +#3840 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3841 = PCURVE('',#3842,#3847); +#3842 = PLANE('',#3843); +#3843 = AXIS2_PLACEMENT_3D('',#3844,#3845,#3846); +#3844 = CARTESIAN_POINT('',(90.,0.E+000,0.E+000)); +#3845 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#3846 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3847 = DEFINITIONAL_REPRESENTATION('',(#3848),#3852); +#3848 = LINE('',#3849,#3850); +#3849 = CARTESIAN_POINT('',(-20.,0.E+000)); +#3850 = VECTOR('',#3851,1.); +#3851 = DIRECTION('',(0.E+000,-1.)); +#3852 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3853 = ORIENTED_EDGE('',*,*,#3854,.F.); +#3854 = EDGE_CURVE('',#3855,#3820,#3857,.T.); +#3855 = VERTEX_POINT('',#3856); +#3856 = CARTESIAN_POINT('',(180.,150.,20.)); +#3857 = SURFACE_CURVE('',#3858,(#3862,#3869),.PCURVE_S1.); +#3858 = LINE('',#3859,#3860); +#3859 = CARTESIAN_POINT('',(180.,75.,20.)); +#3860 = VECTOR('',#3861,1.); +#3861 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#3862 = PCURVE('',#3830,#3863); +#3863 = DEFINITIONAL_REPRESENTATION('',(#3864),#3868); +#3864 = LINE('',#3865,#3866); +#3865 = CARTESIAN_POINT('',(90.,0.E+000)); +#3866 = VECTOR('',#3867,1.); +#3867 = DIRECTION('',(0.E+000,-1.)); +#3868 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3869 = PCURVE('',#3870,#3875); +#3870 = PLANE('',#3871); +#3871 = AXIS2_PLACEMENT_3D('',#3872,#3873,#3874); +#3872 = CARTESIAN_POINT('',(180.,75.,0.E+000)); +#3873 = DIRECTION('',(1.,0.E+000,0.E+000)); +#3874 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#3875 = DEFINITIONAL_REPRESENTATION('',(#3876),#3880); +#3876 = LINE('',#3877,#3878); +#3877 = CARTESIAN_POINT('',(-20.,0.E+000)); +#3878 = VECTOR('',#3879,1.); +#3879 = DIRECTION('',(0.E+000,-1.)); +#3880 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3881 = ORIENTED_EDGE('',*,*,#3882,.F.); +#3882 = EDGE_CURVE('',#3883,#3855,#3885,.T.); +#3883 = VERTEX_POINT('',#3884); +#3884 = CARTESIAN_POINT('',(0.E+000,150.,20.)); +#3885 = SURFACE_CURVE('',#3886,(#3890,#3897),.PCURVE_S1.); +#3886 = LINE('',#3887,#3888); +#3887 = CARTESIAN_POINT('',(90.,150.,20.)); +#3888 = VECTOR('',#3889,1.); +#3889 = DIRECTION('',(1.,0.E+000,0.E+000)); +#3890 = PCURVE('',#3830,#3891); +#3891 = DEFINITIONAL_REPRESENTATION('',(#3892),#3896); +#3892 = LINE('',#3893,#3894); +#3893 = CARTESIAN_POINT('',(0.E+000,75.)); +#3894 = VECTOR('',#3895,1.); +#3895 = DIRECTION('',(1.,0.E+000)); +#3896 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3897 = PCURVE('',#3898,#3903); +#3898 = PLANE('',#3899); +#3899 = AXIS2_PLACEMENT_3D('',#3900,#3901,#3902); +#3900 = CARTESIAN_POINT('',(90.,150.,0.E+000)); +#3901 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3902 = DIRECTION('',(0.E+000,-0.E+000,1.)); +#3903 = DEFINITIONAL_REPRESENTATION('',(#3904),#3908); +#3904 = LINE('',#3905,#3906); +#3905 = CARTESIAN_POINT('',(20.,0.E+000)); +#3906 = VECTOR('',#3907,1.); +#3907 = DIRECTION('',(0.E+000,1.)); +#3908 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3909 = ORIENTED_EDGE('',*,*,#3910,.F.); +#3910 = EDGE_CURVE('',#3822,#3883,#3911,.T.); +#3911 = SURFACE_CURVE('',#3912,(#3916,#3923),.PCURVE_S1.); +#3912 = LINE('',#3913,#3914); +#3913 = CARTESIAN_POINT('',(0.E+000,75.,20.)); +#3914 = VECTOR('',#3915,1.); +#3915 = DIRECTION('',(0.E+000,1.,0.E+000)); +#3916 = PCURVE('',#3830,#3917); +#3917 = DEFINITIONAL_REPRESENTATION('',(#3918),#3922); +#3918 = LINE('',#3919,#3920); +#3919 = CARTESIAN_POINT('',(-90.,0.E+000)); +#3920 = VECTOR('',#3921,1.); +#3921 = DIRECTION('',(0.E+000,1.)); +#3922 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3923 = PCURVE('',#3924,#3929); +#3924 = PLANE('',#3925); +#3925 = AXIS2_PLACEMENT_3D('',#3926,#3927,#3928); +#3926 = CARTESIAN_POINT('',(0.E+000,75.,0.E+000)); +#3927 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#3928 = DIRECTION('',(0.E+000,0.E+000,1.)); +#3929 = DEFINITIONAL_REPRESENTATION('',(#3930),#3934); +#3930 = LINE('',#3931,#3932); +#3931 = CARTESIAN_POINT('',(20.,0.E+000)); +#3932 = VECTOR('',#3933,1.); +#3933 = DIRECTION('',(0.E+000,1.)); +#3934 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3935 = FACE_BOUND('',#3936,.T.); +#3936 = EDGE_LOOP('',(#3937,#4057)); +#3937 = ORIENTED_EDGE('',*,*,#3938,.T.); +#3938 = EDGE_CURVE('',#3939,#3941,#3943,.T.); +#3939 = VERTEX_POINT('',#3940); +#3940 = CARTESIAN_POINT('',(42.5,87.9903810602,20.)); +#3941 = VERTEX_POINT('',#3942); +#3942 = CARTESIAN_POINT('',(52.5,87.9903810602,20.)); +#3943 = SURFACE_CURVE('',#3944,(#3969,#3997),.PCURVE_S1.); +#3944 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#3945,#3946,#3947,#3948,#3949, + #3950,#3951,#3952,#3953,#3954,#3955,#3956,#3957,#3958,#3959,#3960, + #3961,#3962,#3963,#3964,#3965,#3966,#3967,#3968),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165568,7.85828166216, + 10.723818054,13.5836589983,16.4911855042,20.3877608737,22.3658107415 + ),.UNSPECIFIED.); +#3945 = CARTESIAN_POINT('',(42.5,87.9903810602,20.)); +#3946 = CARTESIAN_POINT('',(42.5,88.4575793138,20.)); +#3947 = CARTESIAN_POINT('',(42.5545696802,88.9583665269,20.)); +#3948 = CARTESIAN_POINT('',(42.6795822577,89.4815040925,20.)); +#3949 = CARTESIAN_POINT('',(43.0726861246,90.4704424936,20.)); +#3950 = CARTESIAN_POINT('',(43.7580146369,91.3712858011,20.)); +#3951 = CARTESIAN_POINT('',(44.1452361926,91.7590073924,20.)); +#3952 = CARTESIAN_POINT('',(44.9325086237,92.3524690876,20.)); +#3953 = CARTESIAN_POINT('',(45.8548107341,92.742221424,20.)); +#3954 = CARTESIAN_POINT('',(46.2767785587,92.8683003968,20.)); +#3955 = CARTESIAN_POINT('',(47.1437129636,93.0258620516,20.)); +#3956 = CARTESIAN_POINT('',(48.0264003005,92.9917818222,20.)); +#3957 = CARTESIAN_POINT('',(48.4630506736,92.9261296265,20.)); +#3958 = CARTESIAN_POINT('',(49.3186421197,92.6992268484,20.)); +#3959 = CARTESIAN_POINT('',(50.0957546192,92.2975117311,20.)); +#3960 = CARTESIAN_POINT('',(50.4603131853,92.0546001422,20.)); +#3961 = CARTESIAN_POINT('',(51.2355490366,91.4066823538,20.)); +#3962 = CARTESIAN_POINT('',(51.8095225986,90.6150367145,20.)); +#3963 = CARTESIAN_POINT('',(52.0637500218,90.13282926,20.)); +#3964 = CARTESIAN_POINT('',(52.336292435,89.3951999942,20.)); +#3965 = CARTESIAN_POINT('',(52.4612187701,88.6792361613,20.)); +#3966 = CARTESIAN_POINT('',(52.4876332288,88.4428124377,20.)); +#3967 = CARTESIAN_POINT('',(52.5,88.2127907262,20.)); +#3968 = CARTESIAN_POINT('',(52.5,87.9903810602,20.)); +#3969 = PCURVE('',#3830,#3970); +#3970 = DEFINITIONAL_REPRESENTATION('',(#3971),#3996); +#3971 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#3972,#3973,#3974,#3975,#3976, + #3977,#3978,#3979,#3980,#3981,#3982,#3983,#3984,#3985,#3986,#3987, + #3988,#3989,#3990,#3991,#3992,#3993,#3994,#3995),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165568,7.85828166216, + 10.723818054,13.5836589983,16.4911855042,20.3877608737,22.3658107415 + ),.UNSPECIFIED.); +#3972 = CARTESIAN_POINT('',(-47.5,12.9903810602)); +#3973 = CARTESIAN_POINT('',(-47.5,13.4575793138)); +#3974 = CARTESIAN_POINT('',(-47.4454303198,13.9583665269)); +#3975 = CARTESIAN_POINT('',(-47.3204177423,14.4815040925)); +#3976 = CARTESIAN_POINT('',(-46.9273138754,15.4704424936)); +#3977 = CARTESIAN_POINT('',(-46.2419853631,16.3712858011)); +#3978 = CARTESIAN_POINT('',(-45.8547638074,16.7590073924)); +#3979 = CARTESIAN_POINT('',(-45.0674913763,17.3524690876)); +#3980 = CARTESIAN_POINT('',(-44.1451892659,17.742221424)); +#3981 = CARTESIAN_POINT('',(-43.7232214413,17.8683003968)); +#3982 = CARTESIAN_POINT('',(-42.8562870364,18.0258620516)); +#3983 = CARTESIAN_POINT('',(-41.9735996995,17.9917818222)); +#3984 = CARTESIAN_POINT('',(-41.5369493264,17.9261296265)); +#3985 = CARTESIAN_POINT('',(-40.6813578803,17.6992268484)); +#3986 = CARTESIAN_POINT('',(-39.9042453808,17.2975117311)); +#3987 = CARTESIAN_POINT('',(-39.5396868147,17.0546001422)); +#3988 = CARTESIAN_POINT('',(-38.7644509634,16.4066823538)); +#3989 = CARTESIAN_POINT('',(-38.1904774014,15.6150367145)); +#3990 = CARTESIAN_POINT('',(-37.9362499782,15.13282926)); +#3991 = CARTESIAN_POINT('',(-37.663707565,14.3951999942)); +#3992 = CARTESIAN_POINT('',(-37.5387812299,13.6792361613)); +#3993 = CARTESIAN_POINT('',(-37.5123667712,13.4428124377)); +#3994 = CARTESIAN_POINT('',(-37.5,13.2127907262)); +#3995 = CARTESIAN_POINT('',(-37.5,12.9903810602)); +#3996 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#3997 = PCURVE('',#3998,#4007); +#3998 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#3999,#4000,#4001,#4002) + ,(#4003,#4004,#4005,#4006 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#3999 = CARTESIAN_POINT('',(42.5,87.99038106,20.)); +#4000 = CARTESIAN_POINT('',(42.5,97.99038106,20.)); +#4001 = CARTESIAN_POINT('',(52.5,97.99038106,20.)); +#4002 = CARTESIAN_POINT('',(52.5,87.99038106,20.)); +#4003 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#4004 = CARTESIAN_POINT('',(42.5,97.99038106,0.E+000)); +#4005 = CARTESIAN_POINT('',(52.5,97.99038106,0.E+000)); +#4006 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#4007 = DEFINITIONAL_REPRESENTATION('',(#4008),#4056); +#4008 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4009,#4010,#4011,#4012,#4013, + #4014,#4015,#4016,#4017,#4018,#4019,#4020,#4021,#4022,#4023,#4024, + #4025,#4026,#4027,#4028,#4029,#4030,#4031,#4032,#4033,#4034,#4035, + #4036,#4037,#4038,#4039,#4040,#4041,#4042,#4043,#4044,#4045,#4046, + #4047,#4048,#4049,#4050,#4051,#4052,#4053,#4054,#4055), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880489, + 1.016627760977,1.524941641466,2.033255521955,2.541569402443, + 3.049883282932,3.55819716342,4.066511043909,4.574824924398, + 5.083138804886,5.591452685375,6.099766565864,6.608080446352, + 7.116394326841,7.62470820733,8.133022087818,8.641335968307, + 9.149649848795,9.657963729284,10.166277609773,10.674591490261, + 11.18290537075,11.691219251239,12.199533131727,12.707847012216, + 13.216160892705,13.724474773193,14.232788653682,14.74110253417, + 15.249416414659,15.757730295148,16.266044175636,16.774358056125, + 17.282671936614,17.790985817102,18.299299697591,18.80761357808, + 19.315927458568,19.824241339057,20.332555219545,20.840869100034, + 21.349182980523,21.857496861011,22.3658107415), + .QUASI_UNIFORM_KNOTS.); +#4009 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4010 = CARTESIAN_POINT('',(9.980039899968E-004,0.285786134526)); +#4011 = CARTESIAN_POINT('',(9.980039899955E-004,0.851023725123)); +#4012 = CARTESIAN_POINT('',(9.980039899993E-004,1.679658949222)); +#4013 = CARTESIAN_POINT('',(9.980039900076E-004,2.488775839043)); +#4014 = CARTESIAN_POINT('',(9.980039899919E-004,3.278357383281)); +#4015 = CARTESIAN_POINT('',(9.980039900039E-004,4.048590079098)); +#4016 = CARTESIAN_POINT('',(9.980039899934E-004,4.799873537182)); +#4017 = CARTESIAN_POINT('',(9.980039900023E-004,5.532780961181)); +#4018 = CARTESIAN_POINT('',(9.980039899986E-004,6.248020896562)); +#4019 = CARTESIAN_POINT('',(9.980039900048E-004,6.946360561026)); +#4020 = CARTESIAN_POINT('',(9.980039900052E-004,7.62868862173)); +#4021 = CARTESIAN_POINT('',(9.980039899975E-004,8.296073959471)); +#4022 = CARTESIAN_POINT('',(9.980039900069E-004,8.949683930066)); +#4023 = CARTESIAN_POINT('',(9.980039899987E-004,9.590744767173)); +#4024 = CARTESIAN_POINT('',(9.98003990001E-004,10.22049917264)); +#4025 = CARTESIAN_POINT('',(9.980039900004E-004,10.840182508009)); +#4026 = CARTESIAN_POINT('',(9.980039900006E-004,11.450961979695)); +#4027 = CARTESIAN_POINT('',(9.980039900006E-004,12.054057822467)); +#4028 = CARTESIAN_POINT('',(9.980039900008E-004,12.650784945233)); +#4029 = CARTESIAN_POINT('',(9.980039900005E-004,13.242437001407)); +#4030 = CARTESIAN_POINT('',(9.980039900018E-004,13.830311316457)); +#4031 = CARTESIAN_POINT('',(9.980039899971E-004,14.41570044039)); +#4032 = CARTESIAN_POINT('',(9.980039900148E-004,14.99989761317)); +#4033 = CARTESIAN_POINT('',(9.980039899915E-004,15.584089011939)); +#4034 = CARTESIAN_POINT('',(9.980039900035E-004,16.169496121936)); +#4035 = CARTESIAN_POINT('',(9.980039900002E-004,16.757374012386)); +#4036 = CARTESIAN_POINT('',(9.980039900016E-004,17.349001918912)); +#4037 = CARTESIAN_POINT('',(9.980039899997E-004,17.945677528451)); +#4038 = CARTESIAN_POINT('',(9.980039900061E-004,18.548712223074)); +#4039 = CARTESIAN_POINT('',(9.98003990004E-004,19.159406300008)); +#4040 = CARTESIAN_POINT('',(9.980039900063E-004,19.779034545809)); +#4041 = CARTESIAN_POINT('',(9.980039899995E-004,20.408844117306)); +#4042 = CARTESIAN_POINT('',(9.980039900034E-004,21.050050721665)); +#4043 = CARTESIAN_POINT('',(9.980039899948E-004,21.703821246548)); +#4044 = CARTESIAN_POINT('',(9.980039900043E-004,22.371286813948)); +#4045 = CARTESIAN_POINT('',(9.980039899967E-004,23.053580538936)); +#4046 = CARTESIAN_POINT('',(9.980039899966E-004,23.751780895042)); +#4047 = CARTESIAN_POINT('',(9.98003990005E-004,24.466876473707)); +#4048 = CARTESIAN_POINT('',(9.980039899931E-004,25.199732658311)); +#4049 = CARTESIAN_POINT('',(9.980039900112E-004,25.951064423859)); +#4050 = CARTESIAN_POINT('',(9.980039899935E-004,26.721413691496)); +#4051 = CARTESIAN_POINT('',(9.980039900041E-004,27.511129459065)); +#4052 = CARTESIAN_POINT('',(9.980039900012E-004,28.320321956614)); +#4053 = CARTESIAN_POINT('',(9.980039900025E-004,29.148977247728)); +#4054 = CARTESIAN_POINT('',(9.980039900012E-004,29.714213802412)); +#4055 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4056 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4057 = ORIENTED_EDGE('',*,*,#4058,.T.); +#4058 = EDGE_CURVE('',#3941,#3939,#4059,.T.); +#4059 = SURFACE_CURVE('',#4060,(#4085,#4113),.PCURVE_S1.); +#4060 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4061,#4062,#4063,#4064,#4065, + #4066,#4067,#4068,#4069,#4070,#4071,#4072,#4073,#4074,#4075,#4076, + #4077,#4078,#4079,#4080,#4081,#4082,#4083,#4084),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163241,7.85828165153, + 10.7238180696,13.583659015,16.4911855247,20.3877608942,22.3658107307 + ),.UNSPECIFIED.); +#4061 = CARTESIAN_POINT('',(52.5,87.9903810602,20.)); +#4062 = CARTESIAN_POINT('',(52.5,87.5231828091,20.)); +#4063 = CARTESIAN_POINT('',(52.4454303204,87.0223955989,20.)); +#4064 = CARTESIAN_POINT('',(52.3204177402,86.4992580219,20.)); +#4065 = CARTESIAN_POINT('',(51.9273138725,85.5103196223,20.)); +#4066 = CARTESIAN_POINT('',(51.2419853611,84.6094763168,20.)); +#4067 = CARTESIAN_POINT('',(50.8547638088,84.2217547299,20.)); +#4068 = CARTESIAN_POINT('',(50.0674913726,83.6282930311,20.)); +#4069 = CARTESIAN_POINT('',(49.1451892572,83.2385406935,20.)); +#4070 = CARTESIAN_POINT('',(48.723221447,83.1124617246,20.)); +#4071 = CARTESIAN_POINT('',(47.8562870386,82.9549000687,20.)); +#4072 = CARTESIAN_POINT('',(46.9735996974,82.9889802983,20.)); +#4073 = CARTESIAN_POINT('',(46.5369493258,83.0546324941,20.)); +#4074 = CARTESIAN_POINT('',(45.6813578799,83.2815352719,20.)); +#4075 = CARTESIAN_POINT('',(44.9042453807,83.6832503895,20.)); +#4076 = CARTESIAN_POINT('',(44.5396868156,83.9261619774,20.)); +#4077 = CARTESIAN_POINT('',(43.7644509637,84.5740797661,20.)); +#4078 = CARTESIAN_POINT('',(43.1904774015,85.3657254057,20.)); +#4079 = CARTESIAN_POINT('',(42.9362499782,85.8479328615,20.)); +#4080 = CARTESIAN_POINT('',(42.6637075666,86.5855621231,20.)); +#4081 = CARTESIAN_POINT('',(42.5387812311,87.3015259544,20.)); +#4082 = CARTESIAN_POINT('',(42.5123667709,87.5379496899,20.)); +#4083 = CARTESIAN_POINT('',(42.5,87.7679713976,20.)); +#4084 = CARTESIAN_POINT('',(42.5,87.9903810602,20.)); +#4085 = PCURVE('',#3830,#4086); +#4086 = DEFINITIONAL_REPRESENTATION('',(#4087),#4112); +#4087 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4088,#4089,#4090,#4091,#4092, + #4093,#4094,#4095,#4096,#4097,#4098,#4099,#4100,#4101,#4102,#4103, + #4104,#4105,#4106,#4107,#4108,#4109,#4110,#4111),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163241,7.85828165153, + 10.7238180696,13.583659015,16.4911855247,20.3877608942,22.3658107307 + ),.UNSPECIFIED.); +#4088 = CARTESIAN_POINT('',(-37.5,12.9903810602)); +#4089 = CARTESIAN_POINT('',(-37.5,12.5231828091)); +#4090 = CARTESIAN_POINT('',(-37.5545696796,12.0223955989)); +#4091 = CARTESIAN_POINT('',(-37.6795822598,11.4992580219)); +#4092 = CARTESIAN_POINT('',(-38.0726861275,10.5103196223)); +#4093 = CARTESIAN_POINT('',(-38.7580146389,9.6094763168)); +#4094 = CARTESIAN_POINT('',(-39.1452361912,9.2217547299)); +#4095 = CARTESIAN_POINT('',(-39.9325086274,8.6282930311)); +#4096 = CARTESIAN_POINT('',(-40.8548107428,8.2385406935)); +#4097 = CARTESIAN_POINT('',(-41.276778553,8.1124617246)); +#4098 = CARTESIAN_POINT('',(-42.1437129614,7.9549000687)); +#4099 = CARTESIAN_POINT('',(-43.0264003026,7.9889802983)); +#4100 = CARTESIAN_POINT('',(-43.4630506742,8.0546324941)); +#4101 = CARTESIAN_POINT('',(-44.3186421201,8.2815352719)); +#4102 = CARTESIAN_POINT('',(-45.0957546193,8.6832503895)); +#4103 = CARTESIAN_POINT('',(-45.4603131844,8.9261619774)); +#4104 = CARTESIAN_POINT('',(-46.2355490363,9.5740797661)); +#4105 = CARTESIAN_POINT('',(-46.8095225985,10.3657254057)); +#4106 = CARTESIAN_POINT('',(-47.0637500218,10.8479328615)); +#4107 = CARTESIAN_POINT('',(-47.3362924334,11.5855621231)); +#4108 = CARTESIAN_POINT('',(-47.4612187689,12.3015259544)); +#4109 = CARTESIAN_POINT('',(-47.4876332291,12.5379496899)); +#4110 = CARTESIAN_POINT('',(-47.5,12.7679713976)); +#4111 = CARTESIAN_POINT('',(-47.5,12.9903810602)); +#4112 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4113 = PCURVE('',#4114,#4123); +#4114 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4115,#4116,#4117,#4118) + ,(#4119,#4120,#4121,#4122 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4115 = CARTESIAN_POINT('',(52.5,87.99038106,20.)); +#4116 = CARTESIAN_POINT('',(52.5,77.99038106,20.)); +#4117 = CARTESIAN_POINT('',(42.5,77.99038106,20.)); +#4118 = CARTESIAN_POINT('',(42.5,87.99038106,20.)); +#4119 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#4120 = CARTESIAN_POINT('',(52.5,77.99038106,0.E+000)); +#4121 = CARTESIAN_POINT('',(42.5,77.99038106,0.E+000)); +#4122 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#4123 = DEFINITIONAL_REPRESENTATION('',(#4124),#4172); +#4124 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4125,#4126,#4127,#4128,#4129, + #4130,#4131,#4132,#4133,#4134,#4135,#4136,#4137,#4138,#4139,#4140, + #4141,#4142,#4143,#4144,#4145,#4146,#4147,#4148,#4149,#4150,#4151, + #4152,#4153,#4154,#4155,#4156,#4157,#4158,#4159,#4160,#4161,#4162, + #4163,#4164,#4165,#4166,#4167,#4168,#4169,#4170,#4171), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880243, + 1.016627760486,1.52494164073,2.033255520973,2.541569401216, + 3.049883281459,3.558197161702,4.066511041945,4.574824922189, + 5.083138802432,5.591452682675,6.099766562918,6.608080443161, + 7.116394323405,7.624708203648,8.133022083891,8.641335964134, + 9.149649844377,9.65796372462,10.166277604864,10.674591485107, + 11.18290536535,11.691219245593,12.199533125836,12.70784700608, + 13.216160886323,13.724474766566,14.232788646809,14.741102527052, + 15.249416407295,15.757730287539,16.266044167782,16.774358048025, + 17.282671928268,17.790985808511,18.299299688755,18.807613568998, + 19.315927449241,19.824241329484,20.332555209727,20.84086908997, + 21.349182970214,21.857496850457,22.3658107307),.UNSPECIFIED.); +#4125 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4126 = CARTESIAN_POINT('',(9.9800399E-004,0.285786133536)); +#4127 = CARTESIAN_POINT('',(9.980039900001E-004,0.851023723772)); +#4128 = CARTESIAN_POINT('',(9.980039899996E-004,1.679658951148)); +#4129 = CARTESIAN_POINT('',(9.980039900017E-004,2.488775847134)); +#4130 = CARTESIAN_POINT('',(9.980039899938E-004,3.278357399113)); +#4131 = CARTESIAN_POINT('',(9.980039900018E-004,4.048590102139)); +#4132 = CARTESIAN_POINT('',(9.980039899992E-004,4.799873565183)); +#4133 = CARTESIAN_POINT('',(9.980039900017E-004,5.532780991083)); +#4134 = CARTESIAN_POINT('',(9.980039899942E-004,6.248020925664)); +#4135 = CARTESIAN_POINT('',(9.980039900006E-004,6.946360588908)); +#4136 = CARTESIAN_POINT('',(9.980039900041E-004,7.628688647214)); +#4137 = CARTESIAN_POINT('',(9.980039900052E-004,8.296073981228)); +#4138 = CARTESIAN_POINT('',(9.980039899972E-004,8.949683947635)); +#4139 = CARTESIAN_POINT('',(9.980039900068E-004,9.5907447811)); +#4140 = CARTESIAN_POINT('',(9.98003989998E-004,10.220499184278)); +#4141 = CARTESIAN_POINT('',(9.980039900025E-004,10.840182518657)); +#4142 = CARTESIAN_POINT('',(9.980039899935E-004,11.450961990405)); +#4143 = CARTESIAN_POINT('',(9.980039900036E-004,12.054057829209)); +#4144 = CARTESIAN_POINT('',(9.980039899937E-004,12.650784942582)); +#4145 = CARTESIAN_POINT('',(9.980039900023E-004,13.242436988192)); +#4146 = CARTESIAN_POINT('',(9.980039899994E-004,13.830311296248)); +#4147 = CARTESIAN_POINT('',(9.980039900026E-004,14.415700419084)); +#4148 = CARTESIAN_POINT('',(9.980039899926E-004,14.999897591734)); +#4149 = CARTESIAN_POINT('',(9.980039900082E-004,15.58408898968)); +#4150 = CARTESIAN_POINT('',(9.980039899988E-004,16.169496098413)); +#4151 = CARTESIAN_POINT('',(9.980039899995E-004,16.757373987383)); +#4152 = CARTESIAN_POINT('',(9.980039900064E-004,17.349001892551)); +#4153 = CARTESIAN_POINT('',(9.980039899995E-004,17.945677500953)); +#4154 = CARTESIAN_POINT('',(9.980039899991E-004,18.548712194227)); +#4155 = CARTESIAN_POINT('',(9.980039900079E-004,19.159406269329)); +#4156 = CARTESIAN_POINT('',(9.980039899944E-004,19.779034513082)); +#4157 = CARTESIAN_POINT('',(9.980039899971E-004,20.408844082753)); +#4158 = CARTESIAN_POINT('',(9.980039900001E-004,21.050050685885)); +#4159 = CARTESIAN_POINT('',(9.980039900071E-004,21.703821209766)); +#4160 = CARTESIAN_POINT('',(9.980039899977E-004,22.371286776084)); +#4161 = CARTESIAN_POINT('',(9.980039900073E-004,23.053580500174)); +#4162 = CARTESIAN_POINT('',(9.980039899996E-004,23.751780855547)); +#4163 = CARTESIAN_POINT('',(9.980039899996E-004,24.466876433587)); +#4164 = CARTESIAN_POINT('',(9.980039900076E-004,25.199732617576)); +#4165 = CARTESIAN_POINT('',(9.980039899972E-004,25.95106438254)); +#4166 = CARTESIAN_POINT('',(9.980039900096E-004,26.721413649762)); +#4167 = CARTESIAN_POINT('',(9.98003989992E-004,27.511129418022)); +#4168 = CARTESIAN_POINT('',(9.980039900078E-004,28.320321934731)); +#4169 = CARTESIAN_POINT('',(9.98003990005E-004,29.148977246309)); +#4170 = CARTESIAN_POINT('',(9.980039900021E-004,29.714213805265)); +#4171 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4172 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4173 = FACE_BOUND('',#4174,.T.); +#4174 = EDGE_LOOP('',(#4175,#4295)); +#4175 = ORIENTED_EDGE('',*,*,#4176,.T.); +#4176 = EDGE_CURVE('',#4177,#4179,#4181,.T.); +#4177 = VERTEX_POINT('',#4178); +#4178 = CARTESIAN_POINT('',(42.5,62.0096189398,20.)); +#4179 = VERTEX_POINT('',#4180); +#4180 = CARTESIAN_POINT('',(52.5,62.0096189398,20.)); +#4181 = SURFACE_CURVE('',#4182,(#4207,#4235),.PCURVE_S1.); +#4182 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4183,#4184,#4185,#4186,#4187, + #4188,#4189,#4190,#4191,#4192,#4193,#4194,#4195,#4196,#4197,#4198, + #4199,#4200,#4201,#4202,#4203,#4204,#4205,#4206),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163339,7.85828165276, + 10.7238180712,13.5836590167,16.4911855274,20.3877608974, + 22.3658107333),.UNSPECIFIED.); +#4183 = CARTESIAN_POINT('',(42.5,62.0096189398,20.)); +#4184 = CARTESIAN_POINT('',(42.5,62.476817191,20.)); +#4185 = CARTESIAN_POINT('',(42.5545696796,62.9776044013,20.)); +#4186 = CARTESIAN_POINT('',(42.6795822598,63.5007419778,20.)); +#4187 = CARTESIAN_POINT('',(43.0726861274,64.4896803776,20.)); +#4188 = CARTESIAN_POINT('',(43.758014639,65.3905236833,20.)); +#4189 = CARTESIAN_POINT('',(44.1452361911,65.7782452701,20.)); +#4190 = CARTESIAN_POINT('',(44.9325086274,66.3717069689,20.)); +#4191 = CARTESIAN_POINT('',(45.8548107429,66.7614593066,20.)); +#4192 = CARTESIAN_POINT('',(46.2767785529,66.8875382754,20.)); +#4193 = CARTESIAN_POINT('',(47.1437129614,67.0450999313,20.)); +#4194 = CARTESIAN_POINT('',(48.0264003027,67.0110197017,20.)); +#4195 = CARTESIAN_POINT('',(48.4630506741,66.9453675059,20.)); +#4196 = CARTESIAN_POINT('',(49.3186421203,66.718464728,20.)); +#4197 = CARTESIAN_POINT('',(50.0957546196,66.3167496104,20.)); +#4198 = CARTESIAN_POINT('',(50.4603131842,66.0738380227,20.)); +#4199 = CARTESIAN_POINT('',(51.2355490363,65.4259202339,20.)); +#4200 = CARTESIAN_POINT('',(51.8095225986,64.6342745942,20.)); +#4201 = CARTESIAN_POINT('',(52.0637500217,64.1520671386,20.)); +#4202 = CARTESIAN_POINT('',(52.3362924333,63.4144378771,20.)); +#4203 = CARTESIAN_POINT('',(52.4612187689,62.6984740458,20.)); +#4204 = CARTESIAN_POINT('',(52.4876332292,62.46205031,20.)); +#4205 = CARTESIAN_POINT('',(52.5,62.2320286023,20.)); +#4206 = CARTESIAN_POINT('',(52.5,62.0096189398,20.)); +#4207 = PCURVE('',#3830,#4208); +#4208 = DEFINITIONAL_REPRESENTATION('',(#4209),#4234); +#4209 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4210,#4211,#4212,#4213,#4214, + #4215,#4216,#4217,#4218,#4219,#4220,#4221,#4222,#4223,#4224,#4225, + #4226,#4227,#4228,#4229,#4230,#4231,#4232,#4233),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163339,7.85828165276, + 10.7238180712,13.5836590167,16.4911855274,20.3877608974, + 22.3658107333),.UNSPECIFIED.); +#4210 = CARTESIAN_POINT('',(-47.5,-12.9903810602)); +#4211 = CARTESIAN_POINT('',(-47.5,-12.523182809)); +#4212 = CARTESIAN_POINT('',(-47.4454303204,-12.0223955987)); +#4213 = CARTESIAN_POINT('',(-47.3204177402,-11.4992580222)); +#4214 = CARTESIAN_POINT('',(-46.9273138726,-10.5103196224)); +#4215 = CARTESIAN_POINT('',(-46.241985361,-9.6094763167)); +#4216 = CARTESIAN_POINT('',(-45.8547638089,-9.2217547299)); +#4217 = CARTESIAN_POINT('',(-45.0674913726,-8.6282930311)); +#4218 = CARTESIAN_POINT('',(-44.1451892571,-8.2385406934)); +#4219 = CARTESIAN_POINT('',(-43.7232214471,-8.1124617246)); +#4220 = CARTESIAN_POINT('',(-42.8562870386,-7.9549000687)); +#4221 = CARTESIAN_POINT('',(-41.9735996973,-7.9889802983)); +#4222 = CARTESIAN_POINT('',(-41.5369493259,-8.0546324941)); +#4223 = CARTESIAN_POINT('',(-40.6813578797,-8.281535272)); +#4224 = CARTESIAN_POINT('',(-39.9042453804,-8.6832503896)); +#4225 = CARTESIAN_POINT('',(-39.5396868158,-8.9261619773)); +#4226 = CARTESIAN_POINT('',(-38.7644509637,-9.5740797661)); +#4227 = CARTESIAN_POINT('',(-38.1904774014,-10.3657254058)); +#4228 = CARTESIAN_POINT('',(-37.9362499783,-10.8479328614)); +#4229 = CARTESIAN_POINT('',(-37.6637075667,-11.5855621229)); +#4230 = CARTESIAN_POINT('',(-37.5387812311,-12.3015259542)); +#4231 = CARTESIAN_POINT('',(-37.5123667708,-12.53794969)); +#4232 = CARTESIAN_POINT('',(-37.5,-12.7679713977)); +#4233 = CARTESIAN_POINT('',(-37.5,-12.9903810602)); +#4234 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4235 = PCURVE('',#4236,#4245); +#4236 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4237,#4238,#4239,#4240) + ,(#4241,#4242,#4243,#4244 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4237 = CARTESIAN_POINT('',(42.5,62.00961894,20.)); +#4238 = CARTESIAN_POINT('',(42.5,72.00961894,20.)); +#4239 = CARTESIAN_POINT('',(52.5,72.00961894,20.)); +#4240 = CARTESIAN_POINT('',(52.5,62.00961894,20.)); +#4241 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#4242 = CARTESIAN_POINT('',(42.5,72.00961894,0.E+000)); +#4243 = CARTESIAN_POINT('',(52.5,72.00961894,0.E+000)); +#4244 = CARTESIAN_POINT('',(52.5,62.00961894,0.E+000)); +#4245 = DEFINITIONAL_REPRESENTATION('',(#4246),#4294); +#4246 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4247,#4248,#4249,#4250,#4251, + #4252,#4253,#4254,#4255,#4256,#4257,#4258,#4259,#4260,#4261,#4262, + #4263,#4264,#4265,#4266,#4267,#4268,#4269,#4270,#4271,#4272,#4273, + #4274,#4275,#4276,#4277,#4278,#4279,#4280,#4281,#4282,#4283,#4284, + #4285,#4286,#4287,#4288,#4289,#4290,#4291,#4292,#4293), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880302, + 1.016627760605,1.524941640907,2.033255521209,2.541569401511, + 3.049883281814,3.558197162116,4.066511042418,4.57482492272, + 5.083138803023,5.591452683325,6.099766563627,6.60808044393, + 7.116394324232,7.624708204534,8.133022084836,8.641335965139, + 9.149649845441,9.657963725743,10.166277606045,10.674591486348, + 11.18290536665,11.691219246952,12.199533127255,12.707847007557, + 13.216160887859,13.724474768161,14.232788648464,14.741102528766, + 15.249416409068,15.75773028937,16.266044169673,16.774358049975, + 17.282671930277,17.79098581058,18.299299690882,18.807613571184, + 19.315927451486,19.824241331789,20.332555212091,20.840869092393, + 21.349182972695,21.857496852998,22.3658107333), + .QUASI_UNIFORM_KNOTS.); +#4247 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4248 = CARTESIAN_POINT('',(9.9800399E-004,0.285786133572)); +#4249 = CARTESIAN_POINT('',(9.980039899999E-004,0.851023723838)); +#4250 = CARTESIAN_POINT('',(9.980039900003E-004,1.679658951146)); +#4251 = CARTESIAN_POINT('',(9.980039899988E-004,2.488775846929)); +#4252 = CARTESIAN_POINT('',(9.980039900044E-004,3.278357398652)); +#4253 = CARTESIAN_POINT('',(9.980039900049E-004,4.048590101454)); +#4254 = CARTESIAN_POINT('',(9.980039899975E-004,4.799873564371)); +#4255 = CARTESIAN_POINT('',(9.980039900053E-004,5.532780990271)); +#4256 = CARTESIAN_POINT('',(9.980039900029E-004,6.248020924957)); +#4257 = CARTESIAN_POINT('',(9.980039900049E-004,6.946360588335)); +#4258 = CARTESIAN_POINT('',(9.980039899992E-004,7.628688646726)); +#4259 = CARTESIAN_POINT('',(9.980039899988E-004,8.296073980765)); +#4260 = CARTESIAN_POINT('',(9.980039900062E-004,8.949683947154)); +#4261 = CARTESIAN_POINT('',(9.980039899985E-004,9.590744780597)); +#4262 = CARTESIAN_POINT('',(9.980039900007E-004,10.220499183786)); +#4263 = CARTESIAN_POINT('',(9.980039899996E-004,10.840182518226)); +#4264 = CARTESIAN_POINT('',(9.980039900019E-004,11.450961990074)); +#4265 = CARTESIAN_POINT('',(9.980039899941E-004,12.054057828913)); +#4266 = CARTESIAN_POINT('',(9.980039900019E-004,12.650784942234)); +#4267 = CARTESIAN_POINT('',(9.980039899998E-004,13.242436987774)); +#4268 = CARTESIAN_POINT('',(9.980039900004E-004,13.830311295816)); +#4269 = CARTESIAN_POINT('',(9.980039900003E-004,14.41570041873)); +#4270 = CARTESIAN_POINT('',(9.980039900004E-004,14.999897591469)); +#4271 = CARTESIAN_POINT('',(9.980039900004E-004,15.584088989436)); +#4272 = CARTESIAN_POINT('',(9.980039900003E-004,16.169496098151)); +#4273 = CARTESIAN_POINT('',(9.980039900009E-004,16.757373987128)); +#4274 = CARTESIAN_POINT('',(9.980039899987E-004,17.34900189237)); +#4275 = CARTESIAN_POINT('',(9.980039900071E-004,17.945677500902)); +#4276 = CARTESIAN_POINT('',(9.980039899972E-004,18.548712194178)); +#4277 = CARTESIAN_POINT('',(9.980039900071E-004,19.159406269051)); +#4278 = CARTESIAN_POINT('',(9.980039899987E-004,19.779034512466)); +#4279 = CARTESIAN_POINT('',(9.980039900014E-004,20.408844081875)); +#4280 = CARTESIAN_POINT('',(9.980039899993E-004,21.050050684956)); +#4281 = CARTESIAN_POINT('',(9.98003990005E-004,21.703821208895)); +#4282 = CARTESIAN_POINT('',(9.980039900056E-004,22.371286775205)); +#4283 = CARTESIAN_POINT('',(9.980039899977E-004,23.05358049922)); +#4284 = CARTESIAN_POINT('',(9.980039900076E-004,23.75178085446)); +#4285 = CARTESIAN_POINT('',(9.980039899975E-004,24.466876432345)); +#4286 = CARTESIAN_POINT('',(9.980039900069E-004,25.199732616206)); +#4287 = CARTESIAN_POINT('',(9.980039900007E-004,25.951064381106)); +#4288 = CARTESIAN_POINT('',(9.980039899949E-004,26.721413648344)); +#4289 = CARTESIAN_POINT('',(9.980039900031E-004,27.511129416666)); +#4290 = CARTESIAN_POINT('',(9.980039899977E-004,28.320321933917)); +#4291 = CARTESIAN_POINT('',(9.98003990011E-004,29.148977246151)); +#4292 = CARTESIAN_POINT('',(9.980039900076E-004,29.714213805302)); +#4293 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4294 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4295 = ORIENTED_EDGE('',*,*,#4296,.T.); +#4296 = EDGE_CURVE('',#4179,#4177,#4297,.T.); +#4297 = SURFACE_CURVE('',#4298,(#4323,#4351),.PCURVE_S1.); +#4298 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4299,#4300,#4301,#4302,#4303, + #4304,#4305,#4306,#4307,#4308,#4309,#4310,#4311,#4312,#4313,#4314, + #4315,#4316,#4317,#4318,#4319,#4320,#4321,#4322),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165514,7.85828166212, + 10.7238180543,13.5836589987,16.4911855045,20.3877608737, + 22.3658107409),.UNSPECIFIED.); +#4299 = CARTESIAN_POINT('',(52.5,62.0096189398,20.)); +#4300 = CARTESIAN_POINT('',(52.5,61.5424206863,20.)); +#4301 = CARTESIAN_POINT('',(52.4454303198,61.0416334732,20.)); +#4302 = CARTESIAN_POINT('',(52.3204177422,60.5184959073,20.)); +#4303 = CARTESIAN_POINT('',(51.9273138753,59.5295575063,20.)); +#4304 = CARTESIAN_POINT('',(51.241985363,58.6287141988,20.)); +#4305 = CARTESIAN_POINT('',(50.8547638076,58.2409926076,20.)); +#4306 = CARTESIAN_POINT('',(50.0674913763,57.6475309124,20.)); +#4307 = CARTESIAN_POINT('',(49.1451892658,57.257778576,20.)); +#4308 = CARTESIAN_POINT('',(48.7232214414,57.1316996033,20.)); +#4309 = CARTESIAN_POINT('',(47.8562870364,56.9741379484,20.)); +#4310 = CARTESIAN_POINT('',(46.9735996995,57.0082181778,20.)); +#4311 = CARTESIAN_POINT('',(46.5369493264,57.0738703735,20.)); +#4312 = CARTESIAN_POINT('',(45.6813578803,57.3007731516,20.)); +#4313 = CARTESIAN_POINT('',(44.9042453808,57.7024882688,20.)); +#4314 = CARTESIAN_POINT('',(44.5396868147,57.9453998579,20.)); +#4315 = CARTESIAN_POINT('',(43.7644509634,58.5933176462,20.)); +#4316 = CARTESIAN_POINT('',(43.1904774014,59.3849632855,20.)); +#4317 = CARTESIAN_POINT('',(42.9362499782,59.8671707401,20.)); +#4318 = CARTESIAN_POINT('',(42.6637075651,60.6048000057,20.)); +#4319 = CARTESIAN_POINT('',(42.5387812299,61.3207638385,20.)); +#4320 = CARTESIAN_POINT('',(42.5123667712,61.5571875624,20.)); +#4321 = CARTESIAN_POINT('',(42.5,61.7872092739,20.)); +#4322 = CARTESIAN_POINT('',(42.5,62.0096189398,20.)); +#4323 = PCURVE('',#3830,#4324); +#4324 = DEFINITIONAL_REPRESENTATION('',(#4325),#4350); +#4325 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4326,#4327,#4328,#4329,#4330, + #4331,#4332,#4333,#4334,#4335,#4336,#4337,#4338,#4339,#4340,#4341, + #4342,#4343,#4344,#4345,#4346,#4347,#4348,#4349),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513165514,7.85828166212, + 10.7238180543,13.5836589987,16.4911855045,20.3877608737, + 22.3658107409),.UNSPECIFIED.); +#4326 = CARTESIAN_POINT('',(-37.5,-12.9903810602)); +#4327 = CARTESIAN_POINT('',(-37.5,-13.4575793137)); +#4328 = CARTESIAN_POINT('',(-37.5545696802,-13.9583665268)); +#4329 = CARTESIAN_POINT('',(-37.6795822578,-14.4815040927)); +#4330 = CARTESIAN_POINT('',(-38.0726861247,-15.4704424937)); +#4331 = CARTESIAN_POINT('',(-38.758014637,-16.3712858012)); +#4332 = CARTESIAN_POINT('',(-39.1452361924,-16.7590073924)); +#4333 = CARTESIAN_POINT('',(-39.9325086237,-17.3524690876)); +#4334 = CARTESIAN_POINT('',(-40.8548107342,-17.742221424)); +#4335 = CARTESIAN_POINT('',(-41.2767785586,-17.8683003967)); +#4336 = CARTESIAN_POINT('',(-42.1437129636,-18.0258620516)); +#4337 = CARTESIAN_POINT('',(-43.0264003005,-17.9917818222)); +#4338 = CARTESIAN_POINT('',(-43.4630506736,-17.9261296265)); +#4339 = CARTESIAN_POINT('',(-44.3186421197,-17.6992268484)); +#4340 = CARTESIAN_POINT('',(-45.0957546192,-17.2975117312)); +#4341 = CARTESIAN_POINT('',(-45.4603131853,-17.0546001421)); +#4342 = CARTESIAN_POINT('',(-46.2355490366,-16.4066823538)); +#4343 = CARTESIAN_POINT('',(-46.8095225986,-15.6150367145)); +#4344 = CARTESIAN_POINT('',(-47.0637500218,-15.1328292599)); +#4345 = CARTESIAN_POINT('',(-47.3362924349,-14.3951999943)); +#4346 = CARTESIAN_POINT('',(-47.4612187701,-13.6792361615)); +#4347 = CARTESIAN_POINT('',(-47.4876332288,-13.4428124376)); +#4348 = CARTESIAN_POINT('',(-47.5,-13.2127907261)); +#4349 = CARTESIAN_POINT('',(-47.5,-12.9903810602)); +#4350 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4351 = PCURVE('',#4352,#4361); +#4352 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4353,#4354,#4355,#4356) + ,(#4357,#4358,#4359,#4360 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4353 = CARTESIAN_POINT('',(52.5,62.00961894,20.)); +#4354 = CARTESIAN_POINT('',(52.5,52.00961894,20.)); +#4355 = CARTESIAN_POINT('',(42.5,52.00961894,20.)); +#4356 = CARTESIAN_POINT('',(42.5,62.00961894,20.)); +#4357 = CARTESIAN_POINT('',(52.5,62.00961894,0.E+000)); +#4358 = CARTESIAN_POINT('',(52.5,52.00961894,0.E+000)); +#4359 = CARTESIAN_POINT('',(42.5,52.00961894,0.E+000)); +#4360 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#4361 = DEFINITIONAL_REPRESENTATION('',(#4362),#4410); +#4362 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4363,#4364,#4365,#4366,#4367, + #4368,#4369,#4370,#4371,#4372,#4373,#4374,#4375,#4376,#4377,#4378, + #4379,#4380,#4381,#4382,#4383,#4384,#4385,#4386,#4387,#4388,#4389, + #4390,#4391,#4392,#4393,#4394,#4395,#4396,#4397,#4398,#4399,#4400, + #4401,#4402,#4403,#4404,#4405,#4406,#4407,#4408,#4409), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880475, + 1.01662776095,1.524941641425,2.0332555219,2.541569402375, + 3.04988328285,3.558197163325,4.0665110438,4.574824924275, + 5.08313880475,5.591452685225,6.0997665657,6.608080446175, + 7.11639432665,7.624708207125,8.1330220876,8.641335968075, + 9.14964984855,9.657963729025,10.1662776095,10.674591489975, + 11.18290537045,11.691219250925,12.1995331314,12.707847011875, + 13.21616089235,13.724474772825,14.2327886533,14.741102533775, + 15.24941641425,15.757730294725,16.2660441752,16.774358055675, + 17.28267193615,17.790985816625,18.2992996971,18.807613577575, + 19.31592745805,19.824241338525,20.332555219,20.840869099475, + 21.34918297995,21.857496860425,22.3658107409), + .QUASI_UNIFORM_KNOTS.); +#4363 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4364 = CARTESIAN_POINT('',(9.980039899982E-004,0.28578613449)); +#4365 = CARTESIAN_POINT('',(9.980039899992E-004,0.851023725067)); +#4366 = CARTESIAN_POINT('',(9.980039900055E-004,1.679658949251)); +#4367 = CARTESIAN_POINT('',(9.980039900001E-004,2.488775839249)); +#4368 = CARTESIAN_POINT('',(9.980039899943E-004,3.27835738369)); +#4369 = CARTESIAN_POINT('',(9.980039900016E-004,4.048590079679)); +#4370 = CARTESIAN_POINT('',(9.980039899995E-004,4.79987353786)); +#4371 = CARTESIAN_POINT('',(9.980039900006E-004,5.532780961866)); +#4372 = CARTESIAN_POINT('',(9.980039899984E-004,6.248020897187)); +#4373 = CARTESIAN_POINT('',(9.980039900062E-004,6.946360561602)); +#4374 = CARTESIAN_POINT('',(9.980039899985E-004,7.628688622213)); +#4375 = CARTESIAN_POINT('',(9.980039900005E-004,8.296073959795)); +#4376 = CARTESIAN_POINT('',(9.980039900005E-004,8.949683930198)); +#4377 = CARTESIAN_POINT('',(9.980039899986E-004,9.590744767127)); +#4378 = CARTESIAN_POINT('',(9.980039900063E-004,10.220499172478)); +#4379 = CARTESIAN_POINT('',(9.980039899989E-004,10.840182507808)); +#4380 = CARTESIAN_POINT('',(9.980039899995E-004,11.450961979492)); +#4381 = CARTESIAN_POINT('',(9.980039900047E-004,12.054057822195)); +#4382 = CARTESIAN_POINT('',(9.980039900049E-004,12.650784944821)); +#4383 = CARTESIAN_POINT('',(9.980039899992E-004,13.242437000851)); +#4384 = CARTESIAN_POINT('',(9.980039900006E-004,13.830311315814)); +#4385 = CARTESIAN_POINT('',(9.980039900007E-004,14.415700439734)); +#4386 = CARTESIAN_POINT('',(9.980039899992E-004,14.999897612483)); +#4387 = CARTESIAN_POINT('',(9.980039900055E-004,15.584089011206)); +#4388 = CARTESIAN_POINT('',(9.980039900034E-004,16.169496121161)); +#4389 = CARTESIAN_POINT('',(9.980039900056E-004,16.757374011576)); +#4390 = CARTESIAN_POINT('',(9.980039899988E-004,17.349001918072)); +#4391 = CARTESIAN_POINT('',(9.980039900027E-004,17.945677527575)); +#4392 = CARTESIAN_POINT('',(9.980039899943E-004,18.548712222154)); +#4393 = CARTESIAN_POINT('',(9.980039900028E-004,19.159406299081)); +#4394 = CARTESIAN_POINT('',(9.980039899987E-004,19.779034544911)); +#4395 = CARTESIAN_POINT('',(9.980039900069E-004,20.408844116443)); +#4396 = CARTESIAN_POINT('',(9.980039899995E-004,21.050050720802)); +#4397 = CARTESIAN_POINT('',(9.9800399E-004,21.703821245659)); +#4398 = CARTESIAN_POINT('',(9.980039900057E-004,22.371286813055)); +#4399 = CARTESIAN_POINT('',(9.980039900038E-004,23.053580538057)); +#4400 = CARTESIAN_POINT('',(9.980039900059E-004,23.751780894188)); +#4401 = CARTESIAN_POINT('',(9.980039899997E-004,24.466876472869)); +#4402 = CARTESIAN_POINT('',(9.980039900014E-004,25.199732657463)); +#4403 = CARTESIAN_POINT('',(9.98003990001E-004,25.951064422964)); +#4404 = CARTESIAN_POINT('',(9.980039900011E-004,26.721413690527)); +#4405 = CARTESIAN_POINT('',(9.980039900011E-004,27.511129458051)); +#4406 = CARTESIAN_POINT('',(9.980039900011E-004,28.320321956023)); +#4407 = CARTESIAN_POINT('',(9.980039900014E-004,29.148977247686)); +#4408 = CARTESIAN_POINT('',(9.980039900007E-004,29.71421380249)); +#4409 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4410 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4411 = FACE_BOUND('',#4412,.T.); +#4412 = EDGE_LOOP('',(#4413,#4533)); +#4413 = ORIENTED_EDGE('',*,*,#4414,.T.); +#4414 = EDGE_CURVE('',#4415,#4417,#4419,.T.); +#4415 = VERTEX_POINT('',#4416); +#4416 = CARTESIAN_POINT('',(127.5,62.0096189398,20.)); +#4417 = VERTEX_POINT('',#4418); +#4418 = CARTESIAN_POINT('',(137.5,62.0096189398,20.)); +#4419 = SURFACE_CURVE('',#4420,(#4445,#4473),.PCURVE_S1.); +#4420 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4421,#4422,#4423,#4424,#4425, + #4426,#4427,#4428,#4429,#4430,#4431,#4432,#4433,#4434,#4435,#4436, + #4437,#4438,#4439,#4440,#4441,#4442,#4443,#4444),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163359,7.85828165183, + 10.7238180689,13.5836590139,16.4911855248,20.3877608811, + 22.3658107236),.UNSPECIFIED.); +#4421 = CARTESIAN_POINT('',(127.5,62.0096189398,20.)); +#4422 = CARTESIAN_POINT('',(127.5,62.476817191,20.)); +#4423 = CARTESIAN_POINT('',(127.55456968,62.9776044013,20.)); +#4424 = CARTESIAN_POINT('',(127.679582259,63.5007419779,20.)); +#4425 = CARTESIAN_POINT('',(128.072686127,64.4896803774,20.)); +#4426 = CARTESIAN_POINT('',(128.758014639,65.390523683,20.)); +#4427 = CARTESIAN_POINT('',(129.145236192,65.7782452702,20.)); +#4428 = CARTESIAN_POINT('',(129.932508627,66.3717069689,20.)); +#4429 = CARTESIAN_POINT('',(130.854810743,66.7614593064,20.)); +#4430 = CARTESIAN_POINT('',(131.276778553,66.8875382755,20.)); +#4431 = CARTESIAN_POINT('',(132.143712962,67.0450999313,20.)); +#4432 = CARTESIAN_POINT('',(133.026400303,67.0110197017,20.)); +#4433 = CARTESIAN_POINT('',(133.463050674,66.9453675059,20.)); +#4434 = CARTESIAN_POINT('',(134.31864212,66.718464728,20.)); +#4435 = CARTESIAN_POINT('',(135.09575462,66.3167496104,20.)); +#4436 = CARTESIAN_POINT('',(135.460313186,66.0738380209,20.)); +#4437 = CARTESIAN_POINT('',(136.235549037,65.4259202334,20.)); +#4438 = CARTESIAN_POINT('',(136.809522598,64.6342745955,20.)); +#4439 = CARTESIAN_POINT('',(137.063750023,64.1520671352,20.)); +#4440 = CARTESIAN_POINT('',(137.336292434,63.4144378745,20.)); +#4441 = CARTESIAN_POINT('',(137.461218769,62.6984740442,20.)); +#4442 = CARTESIAN_POINT('',(137.487633229,62.4620503115,20.)); +#4443 = CARTESIAN_POINT('',(137.5,62.232028603,20.)); +#4444 = CARTESIAN_POINT('',(137.5,62.0096189398,20.)); +#4445 = PCURVE('',#3830,#4446); +#4446 = DEFINITIONAL_REPRESENTATION('',(#4447),#4472); +#4447 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4448,#4449,#4450,#4451,#4452, + #4453,#4454,#4455,#4456,#4457,#4458,#4459,#4460,#4461,#4462,#4463, + #4464,#4465,#4466,#4467,#4468,#4469,#4470,#4471),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163359,7.85828165183, + 10.7238180689,13.5836590139,16.4911855248,20.3877608811, + 22.3658107236),.UNSPECIFIED.); +#4448 = CARTESIAN_POINT('',(37.5,-12.9903810602)); +#4449 = CARTESIAN_POINT('',(37.5,-12.523182809)); +#4450 = CARTESIAN_POINT('',(37.55456968,-12.0223955987)); +#4451 = CARTESIAN_POINT('',(37.679582259,-11.4992580221)); +#4452 = CARTESIAN_POINT('',(38.072686127,-10.5103196226)); +#4453 = CARTESIAN_POINT('',(38.758014639,-9.609476317)); +#4454 = CARTESIAN_POINT('',(39.145236192,-9.2217547298)); +#4455 = CARTESIAN_POINT('',(39.932508627,-8.6282930311)); +#4456 = CARTESIAN_POINT('',(40.854810743,-8.2385406936)); +#4457 = CARTESIAN_POINT('',(41.276778553,-8.1124617245)); +#4458 = CARTESIAN_POINT('',(42.143712962,-7.9549000687)); +#4459 = CARTESIAN_POINT('',(43.026400303,-7.9889802983)); +#4460 = CARTESIAN_POINT('',(43.463050674,-8.0546324941)); +#4461 = CARTESIAN_POINT('',(44.31864212,-8.281535272)); +#4462 = CARTESIAN_POINT('',(45.09575462,-8.6832503896)); +#4463 = CARTESIAN_POINT('',(45.460313186,-8.9261619791)); +#4464 = CARTESIAN_POINT('',(46.235549037,-9.5740797666)); +#4465 = CARTESIAN_POINT('',(46.809522598,-10.3657254045)); +#4466 = CARTESIAN_POINT('',(47.063750023,-10.8479328648)); +#4467 = CARTESIAN_POINT('',(47.336292434,-11.5855621255)); +#4468 = CARTESIAN_POINT('',(47.461218769,-12.3015259558)); +#4469 = CARTESIAN_POINT('',(47.487633229,-12.5379496885)); +#4470 = CARTESIAN_POINT('',(47.5,-12.767971397)); +#4471 = CARTESIAN_POINT('',(47.5,-12.9903810602)); +#4472 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4473 = PCURVE('',#4474,#4483); +#4474 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4475,#4476,#4477,#4478) + ,(#4479,#4480,#4481,#4482 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4475 = CARTESIAN_POINT('',(127.5,62.00961894,20.)); +#4476 = CARTESIAN_POINT('',(127.5,72.00961894,20.)); +#4477 = CARTESIAN_POINT('',(137.5,72.00961894,20.)); +#4478 = CARTESIAN_POINT('',(137.5,62.00961894,20.)); +#4479 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#4480 = CARTESIAN_POINT('',(127.5,72.00961894,0.E+000)); +#4481 = CARTESIAN_POINT('',(137.5,72.00961894,0.E+000)); +#4482 = CARTESIAN_POINT('',(137.5,62.00961894,0.E+000)); +#4483 = DEFINITIONAL_REPRESENTATION('',(#4484),#4532); +#4484 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4485,#4486,#4487,#4488,#4489, + #4490,#4491,#4492,#4493,#4494,#4495,#4496,#4497,#4498,#4499,#4500, + #4501,#4502,#4503,#4504,#4505,#4506,#4507,#4508,#4509,#4510,#4511, + #4512,#4513,#4514,#4515,#4516,#4517,#4518,#4519,#4520,#4521,#4522, + #4523,#4524,#4525,#4526,#4527,#4528,#4529,#4530,#4531), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880082, + 1.016627760164,1.524941640245,2.033255520327,2.541569400409, + 3.049883280491,3.558197160573,4.066511040655,4.574824920736, + 5.083138800818,5.5914526809,6.099766560982,6.608080441064, + 7.116394321145,7.624708201227,8.133022081309,8.641335961391, + 9.149649841473,9.657963721555,10.166277601636,10.674591481718, + 11.1829053618,11.691219241882,12.199533121964,12.707847002045, + 13.216160882127,13.724474762209,14.232788642291,14.741102522373, + 15.249416402455,15.757730282536,16.266044162618,16.7743580427, + 17.282671922782,17.790985802864,18.299299682945,18.807613563027, + 19.315927443109,19.824241323191,20.332555203273,20.840869083355, + 21.349182963436,21.857496843518,22.3658107236),.UNSPECIFIED.); +#4485 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4486 = CARTESIAN_POINT('',(9.980039900008E-004,0.28578613343)); +#4487 = CARTESIAN_POINT('',(9.980039900006E-004,0.851023723438)); +#4488 = CARTESIAN_POINT('',(9.980039899967E-004,1.679658950391)); +#4489 = CARTESIAN_POINT('',(9.980039900128E-004,2.48877584581)); +#4490 = CARTESIAN_POINT('',(9.980039899952E-004,3.278357397118)); +#4491 = CARTESIAN_POINT('',(9.98003990007E-004,4.048590099459)); +#4492 = CARTESIAN_POINT('',(9.980039899987E-004,4.799873561925)); +#4493 = CARTESIAN_POINT('',(9.980039899991E-004,5.532780987459)); +#4494 = CARTESIAN_POINT('',(9.980039900061E-004,6.248020921925)); +#4495 = CARTESIAN_POINT('',(9.980039899991E-004,6.946360585173)); +#4496 = CARTESIAN_POINT('',(9.980039899989E-004,7.628688643638)); +#4497 = CARTESIAN_POINT('',(9.980039900068E-004,8.296073977935)); +#4498 = CARTESIAN_POINT('',(9.980039899971E-004,8.949683944617)); +#4499 = CARTESIAN_POINT('',(9.980039900068E-004,9.590744778216)); +#4500 = CARTESIAN_POINT('',(9.980039899991E-004,10.220499181319)); +#4501 = CARTESIAN_POINT('',(9.980039899992E-004,10.840182515446)); +#4502 = CARTESIAN_POINT('',(9.980039900065E-004,11.450961987093)); +#4503 = CARTESIAN_POINT('',(9.980039899989E-004,12.054057826011)); +#4504 = CARTESIAN_POINT('',(9.980039900008E-004,12.650784939501)); +#4505 = CARTESIAN_POINT('',(9.98003990001E-004,13.242436985231)); +#4506 = CARTESIAN_POINT('',(9.980039899985E-004,13.830311293435)); +#4507 = CARTESIAN_POINT('',(9.980039900084E-004,14.415700416456)); +#4508 = CARTESIAN_POINT('',(9.980039899928E-004,14.999897589126)); +#4509 = CARTESIAN_POINT('',(9.980039900027E-004,15.584088986808)); +#4510 = CARTESIAN_POINT('',(9.980039900001E-004,16.169496095104)); +#4511 = CARTESIAN_POINT('',(9.980039900009E-004,16.757373983641)); +#4512 = CARTESIAN_POINT('',(9.980039900007E-004,17.349001888558)); +#4513 = CARTESIAN_POINT('',(9.980039900008E-004,17.945677496913)); +#4514 = CARTESIAN_POINT('',(9.980039900008E-004,18.548712190339)); +#4515 = CARTESIAN_POINT('',(9.980039900007E-004,19.159406265994)); +#4516 = CARTESIAN_POINT('',(9.980039900012E-004,19.779034510528)); +#4517 = CARTESIAN_POINT('',(9.980039899996E-004,20.408844080842)); +#4518 = CARTESIAN_POINT('',(9.980039900059E-004,21.050050684138)); +#4519 = CARTESIAN_POINT('',(9.980039900039E-004,21.703821207655)); +#4520 = CARTESIAN_POINT('',(9.980039900059E-004,22.371286774169)); +#4521 = CARTESIAN_POINT('',(9.9800399E-004,23.053580499765)); +#4522 = CARTESIAN_POINT('',(9.980039900002E-004,23.751780857846)); +#4523 = CARTESIAN_POINT('',(9.980039900055E-004,24.466876439143)); +#4524 = CARTESIAN_POINT('',(9.980039900057E-004,25.199732625989)); +#4525 = CARTESIAN_POINT('',(9.9800399E-004,25.951064392591)); +#4526 = CARTESIAN_POINT('',(9.980039900014E-004,26.721413659959)); +#4527 = CARTESIAN_POINT('',(9.980039900015E-004,27.511129428057)); +#4528 = CARTESIAN_POINT('',(9.980039899999E-004,28.320321940437)); +#4529 = CARTESIAN_POINT('',(9.980039900065E-004,29.148977247336)); +#4530 = CARTESIAN_POINT('',(9.980039900044E-004,29.714213804951)); +#4531 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4532 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4533 = ORIENTED_EDGE('',*,*,#4534,.T.); +#4534 = EDGE_CURVE('',#4417,#4415,#4535,.T.); +#4535 = SURFACE_CURVE('',#4536,(#4561,#4589),.PCURVE_S1.); +#4536 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4537,#4538,#4539,#4540,#4541, + #4542,#4543,#4544,#4545,#4546,#4547,#4548,#4549,#4550,#4551,#4552, + #4553,#4554,#4555,#4556,#4557,#4558,#4559,#4560),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164798,7.85828164866, + 10.7238180467,13.583658987,16.4911854966,20.3877608643,22.3658107618 + ),.UNSPECIFIED.); +#4537 = CARTESIAN_POINT('',(137.5,62.0096189398,20.)); +#4538 = CARTESIAN_POINT('',(137.5,61.5424206871,20.)); +#4539 = CARTESIAN_POINT('',(137.44543032,61.0416334749,20.)); +#4540 = CARTESIAN_POINT('',(137.320417741,60.5184959056,20.)); +#4541 = CARTESIAN_POINT('',(136.927313875,59.5295575063,20.)); +#4542 = CARTESIAN_POINT('',(136.241985364,58.6287142002,20.)); +#4543 = CARTESIAN_POINT('',(135.854763806,58.2409926065,20.)); +#4544 = CARTESIAN_POINT('',(135.067491375,57.6475309115,20.)); +#4545 = CARTESIAN_POINT('',(134.145189264,57.2577785753,20.)); +#4546 = CARTESIAN_POINT('',(133.723221441,57.1316996035,20.)); +#4547 = CARTESIAN_POINT('',(132.856287036,56.9741379484,20.)); +#4548 = CARTESIAN_POINT('',(131.973599699,57.0082181778,20.)); +#4549 = CARTESIAN_POINT('',(131.536949325,57.0738703738,20.)); +#4550 = CARTESIAN_POINT('',(130.681357879,57.300773152,20.)); +#4551 = CARTESIAN_POINT('',(129.90424538,57.7024882694,20.)); +#4552 = CARTESIAN_POINT('',(129.539686814,57.945399859,20.)); +#4553 = CARTESIAN_POINT('',(128.764450962,58.5933176475,20.)); +#4554 = CARTESIAN_POINT('',(128.1904774,59.3849632871,20.)); +#4555 = CARTESIAN_POINT('',(127.93624998,59.8671707404,20.)); +#4556 = CARTESIAN_POINT('',(127.663707566,60.6048000098,20.)); +#4557 = CARTESIAN_POINT('',(127.53878123,61.3207638459,20.)); +#4558 = CARTESIAN_POINT('',(127.512366772,61.5571875554,20.)); +#4559 = CARTESIAN_POINT('',(127.5,61.7872092705,20.)); +#4560 = CARTESIAN_POINT('',(127.5,62.0096189398,20.)); +#4561 = PCURVE('',#3830,#4562); +#4562 = DEFINITIONAL_REPRESENTATION('',(#4563),#4588); +#4563 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4564,#4565,#4566,#4567,#4568, + #4569,#4570,#4571,#4572,#4573,#4574,#4575,#4576,#4577,#4578,#4579, + #4580,#4581,#4582,#4583,#4584,#4585,#4586,#4587),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164798,7.85828164866, + 10.7238180467,13.583658987,16.4911854966,20.3877608643,22.3658107618 + ),.UNSPECIFIED.); +#4564 = CARTESIAN_POINT('',(47.5,-12.9903810602)); +#4565 = CARTESIAN_POINT('',(47.5,-13.4575793129)); +#4566 = CARTESIAN_POINT('',(47.44543032,-13.9583665251)); +#4567 = CARTESIAN_POINT('',(47.320417741,-14.4815040944)); +#4568 = CARTESIAN_POINT('',(46.927313875,-15.4704424937)); +#4569 = CARTESIAN_POINT('',(46.241985364,-16.3712857998)); +#4570 = CARTESIAN_POINT('',(45.854763806,-16.7590073935)); +#4571 = CARTESIAN_POINT('',(45.067491375,-17.3524690885)); +#4572 = CARTESIAN_POINT('',(44.145189264,-17.7422214247)); +#4573 = CARTESIAN_POINT('',(43.723221441,-17.8683003965)); +#4574 = CARTESIAN_POINT('',(42.856287036,-18.0258620516)); +#4575 = CARTESIAN_POINT('',(41.973599699,-17.9917818222)); +#4576 = CARTESIAN_POINT('',(41.536949325,-17.9261296262)); +#4577 = CARTESIAN_POINT('',(40.681357879,-17.699226848)); +#4578 = CARTESIAN_POINT('',(39.90424538,-17.2975117306)); +#4579 = CARTESIAN_POINT('',(39.539686814,-17.054600141)); +#4580 = CARTESIAN_POINT('',(38.764450962,-16.4066823525)); +#4581 = CARTESIAN_POINT('',(38.1904774,-15.6150367129)); +#4582 = CARTESIAN_POINT('',(37.93624998,-15.1328292596)); +#4583 = CARTESIAN_POINT('',(37.663707566,-14.3951999902)); +#4584 = CARTESIAN_POINT('',(37.53878123,-13.6792361541)); +#4585 = CARTESIAN_POINT('',(37.512366772,-13.4428124446)); +#4586 = CARTESIAN_POINT('',(37.5,-13.2127907295)); +#4587 = CARTESIAN_POINT('',(37.5,-12.9903810602)); +#4588 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4589 = PCURVE('',#4590,#4599); +#4590 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4591,#4592,#4593,#4594) + ,(#4595,#4596,#4597,#4598 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4591 = CARTESIAN_POINT('',(137.5,62.00961894,20.)); +#4592 = CARTESIAN_POINT('',(137.5,52.00961894,20.)); +#4593 = CARTESIAN_POINT('',(127.5,52.00961894,20.)); +#4594 = CARTESIAN_POINT('',(127.5,62.00961894,20.)); +#4595 = CARTESIAN_POINT('',(137.5,62.00961894,0.E+000)); +#4596 = CARTESIAN_POINT('',(137.5,52.00961894,0.E+000)); +#4597 = CARTESIAN_POINT('',(127.5,52.00961894,0.E+000)); +#4598 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#4599 = DEFINITIONAL_REPRESENTATION('',(#4600),#4648); +#4600 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4601,#4602,#4603,#4604,#4605, + #4606,#4607,#4608,#4609,#4610,#4611,#4612,#4613,#4614,#4615,#4616, + #4617,#4618,#4619,#4620,#4621,#4622,#4623,#4624,#4625,#4626,#4627, + #4628,#4629,#4630,#4631,#4632,#4633,#4634,#4635,#4636,#4637,#4638, + #4639,#4640,#4641,#4642,#4643,#4644,#4645,#4646,#4647), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.50831388095, + 1.0166277619,1.52494164285,2.0332555238,2.54156940475,3.0498832857, + 3.55819716665,4.0665110476,4.57482492855,5.0831388095,5.59145269045, + 6.0997665714,6.60808045235,7.1163943333,7.62470821425,8.1330220952, + 8.64133597615,9.1496498571,9.65796373805,10.166277619,10.67459149995 + ,11.1829053809,11.69121926185,12.1995331428,12.70784702375, + 13.2161609047,13.72447478565,14.2327886666,14.74110254755, + 15.2494164285,15.75773030945,16.2660441904,16.77435807135, + 17.2826719523,17.79098583325,18.2992997142,18.80761359515, + 19.3159274761,19.82424135705,20.332555238,20.84086911895, + 21.3491829999,21.85749688085,22.3658107618),.QUASI_UNIFORM_KNOTS.); +#4601 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4602 = CARTESIAN_POINT('',(9.9800399E-004,0.285786134697)); +#4603 = CARTESIAN_POINT('',(9.980039900001E-004,0.851023725931)); +#4604 = CARTESIAN_POINT('',(9.980039899994E-004,1.679658951849)); +#4605 = CARTESIAN_POINT('',(9.98003990002E-004,2.488775844557)); +#4606 = CARTESIAN_POINT('',(9.980039899921E-004,3.278357392157)); +#4607 = CARTESIAN_POINT('',(9.980039900078E-004,4.048590091131)); +#4608 = CARTESIAN_POINT('',(9.980039899977E-004,4.799873551566)); +#4609 = CARTESIAN_POINT('',(9.98003990001E-004,5.532780976828)); +#4610 = CARTESIAN_POINT('',(9.980039899981E-004,6.248020912541)); +#4611 = CARTESIAN_POINT('',(9.980039900065E-004,6.946360577195)); +#4612 = CARTESIAN_POINT('',(9.98003989997E-004,7.628688638873)); +#4613 = CARTESIAN_POINT('',(9.980039900052E-004,8.296073978386)); +#4614 = CARTESIAN_POINT('',(9.980039900031E-004,8.949683951118)); +#4615 = CARTESIAN_POINT('',(9.980039900033E-004,9.590744790129)); +#4616 = CARTESIAN_POINT('',(9.980039900048E-004,10.220499196831)); +#4617 = CARTESIAN_POINT('',(9.980039899985E-004,10.840182532611)); +#4618 = CARTESIAN_POINT('',(9.980039900009E-004,11.450962004594)); +#4619 = CARTESIAN_POINT('',(9.980039899978E-004,12.054057847271)); +#4620 = CARTESIAN_POINT('',(9.980039900077E-004,12.650784969126)); +#4621 = CARTESIAN_POINT('',(9.980039899927E-004,13.242437024218)); +#4622 = CARTESIAN_POINT('',(9.980039900003E-004,13.830311338687)); +#4623 = CARTESIAN_POINT('',(9.980039900061E-004,14.415700462863)); +#4624 = CARTESIAN_POINT('',(9.980039899967E-004,14.999897636024)); +#4625 = CARTESIAN_POINT('',(9.98003990007E-004,15.584089035661)); +#4626 = CARTESIAN_POINT('',(9.980039899966E-004,16.169496146983)); +#4627 = CARTESIAN_POINT('',(9.980039900067E-004,16.757374038821)); +#4628 = CARTESIAN_POINT('',(9.980039899981E-004,17.349001946392)); +#4629 = CARTESIAN_POINT('',(9.980039900009E-004,17.945677556594)); +#4630 = CARTESIAN_POINT('',(9.980039899983E-004,18.548712251895)); +#4631 = CARTESIAN_POINT('',(9.980039900062E-004,19.159406329557)); +#4632 = CARTESIAN_POINT('',(9.980039899983E-004,19.779034576268)); +#4633 = CARTESIAN_POINT('',(9.980039900008E-004,20.40884414892)); +#4634 = CARTESIAN_POINT('',(9.980039899987E-004,21.050050754629)); +#4635 = CARTESIAN_POINT('',(9.980039900046E-004,21.703821280962)); +#4636 = CARTESIAN_POINT('',(9.980039900046E-004,22.371286849544)); +#4637 = CARTESIAN_POINT('',(9.980039899989E-004,23.053580575252)); +#4638 = CARTESIAN_POINT('',(9.980039900003E-004,23.751780931797)); +#4639 = CARTESIAN_POINT('',(9.980039900005E-004,24.466876510942)); +#4640 = CARTESIAN_POINT('',(9.980039899984E-004,25.199732696417)); +#4641 = CARTESIAN_POINT('',(9.980039900064E-004,25.951064463423)); +#4642 = CARTESIAN_POINT('',(9.98003989998E-004,26.721413733117)); +#4643 = CARTESIAN_POINT('',(9.980039900021E-004,27.511129502751)); +#4644 = CARTESIAN_POINT('',(9.98003989994E-004,28.320321980919)); +#4645 = CARTESIAN_POINT('',(9.980039900011E-004,29.148977250316)); +#4646 = CARTESIAN_POINT('',(9.980039900018E-004,29.714213799825)); +#4647 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4648 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4649 = FACE_BOUND('',#4650,.T.); +#4650 = EDGE_LOOP('',(#4651,#4771)); +#4651 = ORIENTED_EDGE('',*,*,#4652,.T.); +#4652 = EDGE_CURVE('',#4653,#4655,#4657,.T.); +#4653 = VERTEX_POINT('',#4654); +#4654 = CARTESIAN_POINT('',(127.5,87.9903810602,20.)); +#4655 = VERTEX_POINT('',#4656); +#4656 = CARTESIAN_POINT('',(137.5,87.9903810602,20.)); +#4657 = SURFACE_CURVE('',#4658,(#4683,#4711),.PCURVE_S1.); +#4658 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4659,#4660,#4661,#4662,#4663, + #4664,#4665,#4666,#4667,#4668,#4669,#4670,#4671,#4672,#4673,#4674, + #4675,#4676,#4677,#4678,#4679,#4680,#4681,#4682),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164903,7.85828164914, + 10.7238180511,13.5836589913,16.4911854971,20.3877608695, + 22.3658107102),.UNSPECIFIED.); +#4659 = CARTESIAN_POINT('',(127.5,87.9903810602,20.)); +#4660 = CARTESIAN_POINT('',(127.5,88.457579313,20.)); +#4661 = CARTESIAN_POINT('',(127.55456968,88.9583665253,20.)); +#4662 = CARTESIAN_POINT('',(127.679582259,89.4815040941,20.)); +#4663 = CARTESIAN_POINT('',(128.072686125,90.4704424936,20.)); +#4664 = CARTESIAN_POINT('',(128.758014636,91.3712857996,20.)); +#4665 = CARTESIAN_POINT('',(129.145236194,91.7590073937,20.)); +#4666 = CARTESIAN_POINT('',(129.932508626,92.3524690889,20.)); +#4667 = CARTESIAN_POINT('',(130.854810737,92.7422214252,20.)); +#4668 = CARTESIAN_POINT('',(131.276778557,92.8683003963,20.)); +#4669 = CARTESIAN_POINT('',(132.143712963,93.0258620515,20.)); +#4670 = CARTESIAN_POINT('',(133.026400301,92.9917818222,20.)); +#4671 = CARTESIAN_POINT('',(133.463050675,92.9261296262,20.)); +#4672 = CARTESIAN_POINT('',(134.31864212,92.6992268482,20.)); +#4673 = CARTESIAN_POINT('',(135.095754619,92.2975117311,20.)); +#4674 = CARTESIAN_POINT('',(135.460313185,92.0546001422,20.)); +#4675 = CARTESIAN_POINT('',(136.235549037,91.4066823535,20.)); +#4676 = CARTESIAN_POINT('',(136.809522599,90.6150367138,20.)); +#4677 = CARTESIAN_POINT('',(137.063750022,90.1328292589,20.)); +#4678 = CARTESIAN_POINT('',(137.336292433,89.3951999961,20.)); +#4679 = CARTESIAN_POINT('',(137.461218769,88.679236166,20.)); +#4680 = CARTESIAN_POINT('',(137.487633229,88.4428124314,20.)); +#4681 = CARTESIAN_POINT('',(137.5,88.2127907231,20.)); +#4682 = CARTESIAN_POINT('',(137.5,87.9903810602,20.)); +#4683 = PCURVE('',#3830,#4684); +#4684 = DEFINITIONAL_REPRESENTATION('',(#4685),#4710); +#4685 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4686,#4687,#4688,#4689,#4690, + #4691,#4692,#4693,#4694,#4695,#4696,#4697,#4698,#4699,#4700,#4701, + #4702,#4703,#4704,#4705,#4706,#4707,#4708,#4709),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164903,7.85828164914, + 10.7238180511,13.5836589913,16.4911854971,20.3877608695, + 22.3658107102),.UNSPECIFIED.); +#4686 = CARTESIAN_POINT('',(37.5,12.9903810602)); +#4687 = CARTESIAN_POINT('',(37.5,13.457579313)); +#4688 = CARTESIAN_POINT('',(37.55456968,13.9583665253)); +#4689 = CARTESIAN_POINT('',(37.679582259,14.4815040941)); +#4690 = CARTESIAN_POINT('',(38.072686125,15.4704424936)); +#4691 = CARTESIAN_POINT('',(38.758014636,16.3712857996)); +#4692 = CARTESIAN_POINT('',(39.145236194,16.7590073937)); +#4693 = CARTESIAN_POINT('',(39.932508626,17.3524690889)); +#4694 = CARTESIAN_POINT('',(40.854810737,17.7422214252)); +#4695 = CARTESIAN_POINT('',(41.276778557,17.8683003963)); +#4696 = CARTESIAN_POINT('',(42.143712963,18.0258620515)); +#4697 = CARTESIAN_POINT('',(43.026400301,17.9917818222)); +#4698 = CARTESIAN_POINT('',(43.463050675,17.9261296262)); +#4699 = CARTESIAN_POINT('',(44.31864212,17.6992268482)); +#4700 = CARTESIAN_POINT('',(45.095754619,17.2975117311)); +#4701 = CARTESIAN_POINT('',(45.460313185,17.0546001422)); +#4702 = CARTESIAN_POINT('',(46.235549037,16.4066823535)); +#4703 = CARTESIAN_POINT('',(46.809522599,15.6150367138)); +#4704 = CARTESIAN_POINT('',(47.063750022,15.1328292589)); +#4705 = CARTESIAN_POINT('',(47.336292433,14.3951999961)); +#4706 = CARTESIAN_POINT('',(47.461218769,13.679236166)); +#4707 = CARTESIAN_POINT('',(47.487633229,13.4428124314)); +#4708 = CARTESIAN_POINT('',(47.5,13.2127907231)); +#4709 = CARTESIAN_POINT('',(47.5,12.9903810602)); +#4710 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4711 = PCURVE('',#4712,#4721); +#4712 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4713,#4714,#4715,#4716) + ,(#4717,#4718,#4719,#4720 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4713 = CARTESIAN_POINT('',(127.5,87.99038106,20.)); +#4714 = CARTESIAN_POINT('',(127.5,97.99038106,20.)); +#4715 = CARTESIAN_POINT('',(137.5,97.99038106,20.)); +#4716 = CARTESIAN_POINT('',(137.5,87.99038106,20.)); +#4717 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#4718 = CARTESIAN_POINT('',(127.5,97.99038106,0.E+000)); +#4719 = CARTESIAN_POINT('',(137.5,97.99038106,0.E+000)); +#4720 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#4721 = DEFINITIONAL_REPRESENTATION('',(#4722),#4770); +#4722 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4723,#4724,#4725,#4726,#4727, + #4728,#4729,#4730,#4731,#4732,#4733,#4734,#4735,#4736,#4737,#4738, + #4739,#4740,#4741,#4742,#4743,#4744,#4745,#4746,#4747,#4748,#4749, + #4750,#4751,#4752,#4753,#4754,#4755,#4756,#4757,#4758,#4759,#4760, + #4761,#4762,#4763,#4764,#4765,#4766,#4767,#4768,#4769), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313879777, + 1.016627759555,1.524941639332,2.033255519109,2.541569398886, + 3.049883278664,3.558197158441,4.066511038218,4.574824917995, + 5.083138797773,5.59145267755,6.099766557327,6.608080437105, + 7.116394316882,7.624708196659,8.133022076436,8.641335956214, + 9.149649835991,9.657963715768,10.166277595545,10.674591475323, + 11.1829053551,11.691219234877,12.199533114655,12.707846994432, + 13.216160874209,13.724474753986,14.232788633764,14.741102513541, + 15.249416393318,15.757730273095,16.266044152873,16.77435803265, + 17.282671912427,17.790985792205,18.299299671982,18.807613551759, + 19.315927431536,19.824241311314,20.332555191091,20.840869070868, + 21.349182950645,21.857496830423,22.3658107102),.UNSPECIFIED.); +#4723 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4724 = CARTESIAN_POINT('',(9.980039899968E-004,0.285786134035)); +#4725 = CARTESIAN_POINT('',(9.980039899954E-004,0.851023723936)); +#4726 = CARTESIAN_POINT('',(9.980039899998E-004,1.679658947836)); +#4727 = CARTESIAN_POINT('',(9.980039900059E-004,2.488775838494)); +#4728 = CARTESIAN_POINT('',(9.980039899985E-004,3.278357384096)); +#4729 = CARTESIAN_POINT('',(9.980039900007E-004,4.048590081208)); +#4730 = CARTESIAN_POINT('',(9.980039899996E-004,4.799873539974)); +#4731 = CARTESIAN_POINT('',(9.980039900019E-004,5.532780963769)); +#4732 = CARTESIAN_POINT('',(9.98003989994E-004,6.248020898176)); +#4733 = CARTESIAN_POINT('',(9.980039900024E-004,6.946360561552)); +#4734 = CARTESIAN_POINT('',(9.980039899982E-004,7.628688622063)); +#4735 = CARTESIAN_POINT('',(9.980039900066E-004,8.296073960544)); +#4736 = CARTESIAN_POINT('',(9.980039899987E-004,8.949683932339)); +#4737 = CARTESIAN_POINT('',(9.980039900008E-004,9.590744770448)); +#4738 = CARTESIAN_POINT('',(9.980039900007E-004,10.220499176237)); +#4739 = CARTESIAN_POINT('',(9.980039899992E-004,10.840182511)); +#4740 = CARTESIAN_POINT('',(9.980039900055E-004,11.450961981931)); +#4741 = CARTESIAN_POINT('',(9.980039900035E-004,12.054057822704)); +#4742 = CARTESIAN_POINT('',(9.980039900056E-004,12.650784941612)); +#4743 = CARTESIAN_POINT('',(9.980039899994E-004,13.242436993607)); +#4744 = CARTESIAN_POINT('',(9.98003990001E-004,13.830311305766)); +#4745 = CARTESIAN_POINT('',(9.980039900008E-004,14.415700428839)); +#4746 = CARTESIAN_POINT('',(9.980039900004E-004,14.999897601012)); +#4747 = CARTESIAN_POINT('',(9.980039900025E-004,15.584088999461)); +#4748 = CARTESIAN_POINT('',(9.980039899946E-004,16.169496109395)); +#4749 = CARTESIAN_POINT('',(9.980039900031E-004,16.757373999623)); +#4750 = CARTESIAN_POINT('',(9.980039899984E-004,17.349001905418)); +#4751 = CARTESIAN_POINT('',(9.98003990009E-004,17.945677513811)); +#4752 = CARTESIAN_POINT('',(9.980039899931E-004,18.548712207394)); +#4753 = CARTESIAN_POINT('',(9.980039900036E-004,19.159406283254)); +#4754 = CARTESIAN_POINT('',(9.98003989999E-004,19.779034527962)); +#4755 = CARTESIAN_POINT('',(9.98003990007E-004,20.40884409835)); +#4756 = CARTESIAN_POINT('',(9.980039900012E-004,21.050050701577)); +#4757 = CARTESIAN_POINT('',(9.980039899956E-004,21.703821225222)); +#4758 = CARTESIAN_POINT('',(9.980039900029E-004,22.371286791223)); +#4759 = CARTESIAN_POINT('',(9.98003990001E-004,23.053580514668)); +#4760 = CARTESIAN_POINT('',(9.980039900015E-004,23.751780869104)); +#4761 = CARTESIAN_POINT('',(9.980039900016E-004,24.466876446008)); +#4762 = CARTESIAN_POINT('',(9.980039900011E-004,25.199732628809)); +#4763 = CARTESIAN_POINT('',(9.980039900032E-004,25.951064392582)); +#4764 = CARTESIAN_POINT('',(9.980039899956E-004,26.721413658498)); +#4765 = CARTESIAN_POINT('',(9.98003990003E-004,27.511129424557)); +#4766 = CARTESIAN_POINT('',(9.980039900026E-004,28.320321937829)); +#4767 = CARTESIAN_POINT('',(9.980039899974E-004,29.148977246312)); +#4768 = CARTESIAN_POINT('',(9.980039899976E-004,29.71421380479)); +#4769 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4770 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4771 = ORIENTED_EDGE('',*,*,#4772,.T.); +#4772 = EDGE_CURVE('',#4655,#4653,#4773,.T.); +#4773 = SURFACE_CURVE('',#4774,(#4799,#4827),.PCURVE_S1.); +#4774 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4775,#4776,#4777,#4778,#4779, + #4780,#4781,#4782,#4783,#4784,#4785,#4786,#4787,#4788,#4789,#4790, + #4791,#4792,#4793,#4794,#4795,#4796,#4797,#4798),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163251,7.85828164883, + 10.7238180658,13.583659012,16.491185527,20.3877608853,22.3658107303) + ,.UNSPECIFIED.); +#4775 = CARTESIAN_POINT('',(137.5,87.9903810602,20.)); +#4776 = CARTESIAN_POINT('',(137.5,87.5231828091,20.)); +#4777 = CARTESIAN_POINT('',(137.44543032,87.022395599,20.)); +#4778 = CARTESIAN_POINT('',(137.320417741,86.4992580219,20.)); +#4779 = CARTESIAN_POINT('',(136.927313873,85.5103196227,20.)); +#4780 = CARTESIAN_POINT('',(136.241985361,84.6094763174,20.)); +#4781 = CARTESIAN_POINT('',(135.854763808,84.2217547294,20.)); +#4782 = CARTESIAN_POINT('',(135.067491372,83.628293031,20.)); +#4783 = CARTESIAN_POINT('',(134.145189257,83.2385406936,20.)); +#4784 = CARTESIAN_POINT('',(133.723221447,83.1124617245,20.)); +#4785 = CARTESIAN_POINT('',(132.856287038,82.9549000687,20.)); +#4786 = CARTESIAN_POINT('',(131.973599697,82.9889802983,20.)); +#4787 = CARTESIAN_POINT('',(131.536949326,83.0546324941,20.)); +#4788 = CARTESIAN_POINT('',(130.681357879,83.2815352721,20.)); +#4789 = CARTESIAN_POINT('',(129.904245379,83.6832503902,20.)); +#4790 = CARTESIAN_POINT('',(129.539686815,83.9261619783,20.)); +#4791 = CARTESIAN_POINT('',(128.764450964,84.5740797663,20.)); +#4792 = CARTESIAN_POINT('',(128.190477401,85.365725405,20.)); +#4793 = CARTESIAN_POINT('',(127.936249977,85.8479328643,20.)); +#4794 = CARTESIAN_POINT('',(127.663707566,86.5855621255,20.)); +#4795 = CARTESIAN_POINT('',(127.538781231,87.3015259565,20.)); +#4796 = CARTESIAN_POINT('',(127.512366771,87.5379496879,20.)); +#4797 = CARTESIAN_POINT('',(127.5,87.7679713967,20.)); +#4798 = CARTESIAN_POINT('',(127.5,87.9903810602,20.)); +#4799 = PCURVE('',#3830,#4800); +#4800 = DEFINITIONAL_REPRESENTATION('',(#4801),#4826); +#4801 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4802,#4803,#4804,#4805,#4806, + #4807,#4808,#4809,#4810,#4811,#4812,#4813,#4814,#4815,#4816,#4817, + #4818,#4819,#4820,#4821,#4822,#4823,#4824,#4825),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513163251,7.85828164883, + 10.7238180658,13.583659012,16.491185527,20.3877608853,22.3658107303) + ,.UNSPECIFIED.); +#4802 = CARTESIAN_POINT('',(47.5,12.9903810602)); +#4803 = CARTESIAN_POINT('',(47.5,12.5231828091)); +#4804 = CARTESIAN_POINT('',(47.44543032,12.022395599)); +#4805 = CARTESIAN_POINT('',(47.320417741,11.4992580219)); +#4806 = CARTESIAN_POINT('',(46.927313873,10.5103196227)); +#4807 = CARTESIAN_POINT('',(46.241985361,9.6094763174)); +#4808 = CARTESIAN_POINT('',(45.854763808,9.2217547294)); +#4809 = CARTESIAN_POINT('',(45.067491372,8.628293031)); +#4810 = CARTESIAN_POINT('',(44.145189257,8.2385406936)); +#4811 = CARTESIAN_POINT('',(43.723221447,8.1124617245)); +#4812 = CARTESIAN_POINT('',(42.856287038,7.9549000687)); +#4813 = CARTESIAN_POINT('',(41.973599697,7.9889802983)); +#4814 = CARTESIAN_POINT('',(41.536949326,8.0546324941)); +#4815 = CARTESIAN_POINT('',(40.681357879,8.2815352721)); +#4816 = CARTESIAN_POINT('',(39.904245379,8.6832503902)); +#4817 = CARTESIAN_POINT('',(39.539686815,8.9261619783)); +#4818 = CARTESIAN_POINT('',(38.764450964,9.5740797663)); +#4819 = CARTESIAN_POINT('',(38.190477401,10.365725405)); +#4820 = CARTESIAN_POINT('',(37.936249977,10.8479328643)); +#4821 = CARTESIAN_POINT('',(37.663707566,11.5855621255)); +#4822 = CARTESIAN_POINT('',(37.538781231,12.3015259565)); +#4823 = CARTESIAN_POINT('',(37.512366771,12.5379496879)); +#4824 = CARTESIAN_POINT('',(37.5,12.7679713967)); +#4825 = CARTESIAN_POINT('',(37.5,12.9903810602)); +#4826 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4827 = PCURVE('',#4828,#4837); +#4828 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4829,#4830,#4831,#4832) + ,(#4833,#4834,#4835,#4836 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4829 = CARTESIAN_POINT('',(137.5,87.99038106,20.)); +#4830 = CARTESIAN_POINT('',(137.5,77.99038106,20.)); +#4831 = CARTESIAN_POINT('',(127.5,77.99038106,20.)); +#4832 = CARTESIAN_POINT('',(127.5,87.99038106,20.)); +#4833 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#4834 = CARTESIAN_POINT('',(137.5,77.99038106,0.E+000)); +#4835 = CARTESIAN_POINT('',(127.5,77.99038106,0.E+000)); +#4836 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#4837 = DEFINITIONAL_REPRESENTATION('',(#4838),#4886); +#4838 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4839,#4840,#4841,#4842,#4843, + #4844,#4845,#4846,#4847,#4848,#4849,#4850,#4851,#4852,#4853,#4854, + #4855,#4856,#4857,#4858,#4859,#4860,#4861,#4862,#4863,#4864,#4865, + #4866,#4867,#4868,#4869,#4870,#4871,#4872,#4873,#4874,#4875,#4876, + #4877,#4878,#4879,#4880,#4881,#4882,#4883,#4884,#4885), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880234, + 1.016627760468,1.524941640702,2.033255520936,2.54156940117, + 3.049883281405,3.558197161639,4.066511041873,4.574824922107, + 5.083138802341,5.591452682575,6.099766562809,6.608080443043, + 7.116394323277,7.624708203511,8.133022083745,8.64133596398, + 9.149649844214,9.657963724448,10.166277604682,10.674591484916, + 11.18290536515,11.691219245384,12.199533125618,12.707847005852, + 13.216160886086,13.72447476632,14.232788646555,14.741102526789, + 15.249416407023,15.757730287257,16.266044167491,16.774358047725, + 17.282671927959,17.790985808193,18.299299688427,18.807613568661, + 19.315927448895,19.82424132913,20.332555209364,20.840869089598, + 21.349182969832,21.857496850066,22.3658107303), + .QUASI_UNIFORM_KNOTS.); +#4839 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4840 = CARTESIAN_POINT('',(9.980039900018E-004,0.285786133517)); +#4841 = CARTESIAN_POINT('',(9.980039900009E-004,0.851023723715)); +#4842 = CARTESIAN_POINT('',(9.98003989994E-004,1.679658951021)); +#4843 = CARTESIAN_POINT('',(9.980039900015E-004,2.488775846926)); +#4844 = CARTESIAN_POINT('',(9.980039899995E-004,3.278357398785)); +#4845 = CARTESIAN_POINT('',(9.980039899999E-004,4.048590101665)); +#4846 = CARTESIAN_POINT('',(9.980039900002E-004,4.799873564588)); +#4847 = CARTESIAN_POINT('',(9.980039899985E-004,5.532780990465)); +#4848 = CARTESIAN_POINT('',(9.980039900048E-004,6.248020925181)); +#4849 = CARTESIAN_POINT('',(9.980039900026E-004,6.946360588667)); +#4850 = CARTESIAN_POINT('',(9.980039900051E-004,7.628688647535)); +#4851 = CARTESIAN_POINT('',(9.980039899972E-004,8.296073982405)); +#4852 = CARTESIAN_POINT('',(9.98003990005E-004,8.949683949752)); +#4853 = CARTESIAN_POINT('',(9.980039900029E-004,9.590744784014)); +#4854 = CARTESIAN_POINT('',(9.980039900036E-004,10.220499187688)); +#4855 = CARTESIAN_POINT('',(9.980039900029E-004,10.840182522235)); +#4856 = CARTESIAN_POINT('',(9.980039900052E-004,11.450961994024)); +#4857 = CARTESIAN_POINT('',(9.980039899965E-004,12.054057832898)); +#4858 = CARTESIAN_POINT('',(9.980039900077E-004,12.650784946353)); +#4859 = CARTESIAN_POINT('',(9.980039899928E-004,13.242436992094)); +#4860 = CARTESIAN_POINT('',(9.980039899987E-004,13.830311300344)); +#4861 = CARTESIAN_POINT('',(9.980039900115E-004,14.415700423411)); +#4862 = CARTESIAN_POINT('',(9.980039899968E-004,14.999897596134)); +#4863 = CARTESIAN_POINT('',(9.980039900002E-004,15.584088993867)); +#4864 = CARTESIAN_POINT('',(9.980039900013E-004,16.169496102237)); +#4865 = CARTESIAN_POINT('',(9.980039899938E-004,16.757373990909)); +#4866 = CARTESIAN_POINT('',(9.980039900015E-004,17.349001895997)); +#4867 = CARTESIAN_POINT('',(9.980039899995E-004,17.945677504543)); +#4868 = CARTESIAN_POINT('',(9.980039899999E-004,18.548712197726)); +#4869 = CARTESIAN_POINT('',(9.980039900003E-004,19.159406272192)); +#4870 = CARTESIAN_POINT('',(9.980039899982E-004,19.779034515055)); +#4871 = CARTESIAN_POINT('',(9.980039900061E-004,20.408844084014)); +#4872 = CARTESIAN_POINT('',(9.980039899981E-004,21.050050686982)); +#4873 = CARTESIAN_POINT('',(9.980039900008E-004,21.703821211017)); +#4874 = CARTESIAN_POINT('',(9.980039899981E-004,22.371286777985)); +#4875 = CARTESIAN_POINT('',(9.980039900061E-004,23.053580503695)); +#4876 = CARTESIAN_POINT('',(9.980039899981E-004,23.751780861504)); +#4877 = CARTESIAN_POINT('',(9.980039900007E-004,24.466876442315)); +#4878 = CARTESIAN_POINT('',(9.980039899984E-004,25.199732628727)); +#4879 = CARTESIAN_POINT('',(9.98003990005E-004,25.951064395187)); +#4880 = CARTESIAN_POINT('',(9.980039900023E-004,26.721413662813)); +#4881 = CARTESIAN_POINT('',(9.980039900068E-004,27.511129431342)); +#4882 = CARTESIAN_POINT('',(9.980039899914E-004,28.320321942215)); +#4883 = CARTESIAN_POINT('',(9.980039900061E-004,29.148977247338)); +#4884 = CARTESIAN_POINT('',(9.980039900057E-004,29.714213804631)); +#4885 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#4886 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4887 = FACE_BOUND('',#4888,.T.); +#4888 = EDGE_LOOP('',(#4889,#5009)); +#4889 = ORIENTED_EDGE('',*,*,#4890,.T.); +#4890 = EDGE_CURVE('',#4891,#4893,#4895,.T.); +#4891 = VERTEX_POINT('',#4892); +#4892 = CARTESIAN_POINT('',(20.,75.,20.)); +#4893 = VERTEX_POINT('',#4894); +#4894 = CARTESIAN_POINT('',(30.,75.,20.)); +#4895 = SURFACE_CURVE('',#4896,(#4921,#4949),.PCURVE_S1.); +#4896 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4897,#4898,#4899,#4900,#4901, + #4902,#4903,#4904,#4905,#4906,#4907,#4908,#4909,#4910,#4911,#4912, + #4913,#4914,#4915,#4916,#4917,#4918,#4919,#4920),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164484,7.85828164824, + 10.7238180433,13.5836589908,16.4911854976,20.3877608637,22.365810724 + ),.UNSPECIFIED.); +#4897 = CARTESIAN_POINT('',(20.,75.,20.)); +#4898 = CARTESIAN_POINT('',(20.,75.4671982525,20.)); +#4899 = CARTESIAN_POINT('',(20.0545696798,75.9679854641,20.)); +#4900 = CARTESIAN_POINT('',(20.1795822587,76.4911230351,20.)); +#4901 = CARTESIAN_POINT('',(20.5726861255,77.4800614343,20.)); +#4902 = CARTESIAN_POINT('',(21.2580146363,78.38090474,20.)); +#4903 = CARTESIAN_POINT('',(21.6452361932,78.7686263329,20.)); +#4904 = CARTESIAN_POINT('',(22.4325086248,79.3620880279,20.)); +#4905 = CARTESIAN_POINT('',(23.3548107347,79.7518403641,20.)); +#4906 = CARTESIAN_POINT('',(23.7767785579,79.8779193366,20.)); +#4907 = CARTESIAN_POINT('',(24.6437129635,80.0354809915,20.)); +#4908 = CARTESIAN_POINT('',(25.5264003015,80.0014007619,20.)); +#4909 = CARTESIAN_POINT('',(25.9630506731,79.9357485663,20.)); +#4910 = CARTESIAN_POINT('',(26.8186421194,79.7088457885,20.)); +#4911 = CARTESIAN_POINT('',(27.595754619,79.307130671,20.)); +#4912 = CARTESIAN_POINT('',(27.9603131851,79.0642190821,20.)); +#4913 = CARTESIAN_POINT('',(28.7355490362,78.416301294,20.)); +#4914 = CARTESIAN_POINT('',(29.309522598,77.6246556552,20.)); +#4915 = CARTESIAN_POINT('',(29.5637500221,77.1424481994,20.)); +#4916 = CARTESIAN_POINT('',(29.8362924346,76.4048189351,20.)); +#4917 = CARTESIAN_POINT('',(29.9612187699,75.6888551024,20.)); +#4918 = CARTESIAN_POINT('',(29.9876332288,75.4524313758,20.)); +#4919 = CARTESIAN_POINT('',(30.,75.2224096652,20.)); +#4920 = CARTESIAN_POINT('',(30.,75.,20.)); +#4921 = PCURVE('',#3830,#4922); +#4922 = DEFINITIONAL_REPRESENTATION('',(#4923),#4948); +#4923 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#4924,#4925,#4926,#4927,#4928, + #4929,#4930,#4931,#4932,#4933,#4934,#4935,#4936,#4937,#4938,#4939, + #4940,#4941,#4942,#4943,#4944,#4945,#4946,#4947),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164484,7.85828164824, + 10.7238180433,13.5836589908,16.4911854976,20.3877608637,22.365810724 + ),.UNSPECIFIED.); +#4924 = CARTESIAN_POINT('',(-70.,0.E+000)); +#4925 = CARTESIAN_POINT('',(-70.,0.4671982525)); +#4926 = CARTESIAN_POINT('',(-69.9454303202,0.9679854641)); +#4927 = CARTESIAN_POINT('',(-69.8204177413,1.4911230351)); +#4928 = CARTESIAN_POINT('',(-69.4273138745,2.4800614343)); +#4929 = CARTESIAN_POINT('',(-68.7419853637,3.38090474)); +#4930 = CARTESIAN_POINT('',(-68.3547638068,3.7686263329)); +#4931 = CARTESIAN_POINT('',(-67.5674913752,4.3620880279)); +#4932 = CARTESIAN_POINT('',(-66.6451892653,4.7518403641)); +#4933 = CARTESIAN_POINT('',(-66.2232214421,4.8779193366)); +#4934 = CARTESIAN_POINT('',(-65.3562870365,5.0354809915)); +#4935 = CARTESIAN_POINT('',(-64.4735996985,5.0014007619)); +#4936 = CARTESIAN_POINT('',(-64.0369493269,4.9357485663)); +#4937 = CARTESIAN_POINT('',(-63.1813578806,4.7088457885)); +#4938 = CARTESIAN_POINT('',(-62.404245381,4.307130671)); +#4939 = CARTESIAN_POINT('',(-62.0396868149,4.0642190821)); +#4940 = CARTESIAN_POINT('',(-61.2644509638,3.416301294)); +#4941 = CARTESIAN_POINT('',(-60.690477402,2.6246556552)); +#4942 = CARTESIAN_POINT('',(-60.4362499779,2.1424481994)); +#4943 = CARTESIAN_POINT('',(-60.1637075654,1.4048189351)); +#4944 = CARTESIAN_POINT('',(-60.0387812301,0.6888551024)); +#4945 = CARTESIAN_POINT('',(-60.0123667712,0.4524313758)); +#4946 = CARTESIAN_POINT('',(-60.,0.2224096652)); +#4947 = CARTESIAN_POINT('',(-60.,0.E+000)); +#4948 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#4949 = PCURVE('',#4950,#4959); +#4950 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#4951,#4952,#4953,#4954) + ,(#4955,#4956,#4957,#4958 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#4951 = CARTESIAN_POINT('',(20.,75.,20.)); +#4952 = CARTESIAN_POINT('',(20.,85.,20.)); +#4953 = CARTESIAN_POINT('',(30.,85.,20.)); +#4954 = CARTESIAN_POINT('',(30.,75.,20.)); +#4955 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#4956 = CARTESIAN_POINT('',(20.,85.,0.E+000)); +#4957 = CARTESIAN_POINT('',(30.,85.,0.E+000)); +#4958 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#4959 = DEFINITIONAL_REPRESENTATION('',(#4960),#5008); +#4960 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#4961,#4962,#4963,#4964,#4965, + #4966,#4967,#4968,#4969,#4970,#4971,#4972,#4973,#4974,#4975,#4976, + #4977,#4978,#4979,#4980,#4981,#4982,#4983,#4984,#4985,#4986,#4987, + #4988,#4989,#4990,#4991,#4992,#4993,#4994,#4995,#4996,#4997,#4998, + #4999,#5000,#5001,#5002,#5003,#5004,#5005,#5006,#5007), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880091, + 1.016627760182,1.524941640273,2.033255520364,2.541569400455, + 3.049883280545,3.558197160636,4.066511040727,4.574824920818, + 5.083138800909,5.591452681,6.099766561091,6.608080441182, + 7.116394321273,7.624708201364,8.133022081455,8.641335961545, + 9.149649841636,9.657963721727,10.166277601818,10.674591481909, + 11.182905362,11.691219242091,12.199533122182,12.707847002273, + 13.216160882364,13.724474762455,14.232788642545,14.741102522636, + 15.249416402727,15.757730282818,16.266044162909,16.774358043, + 17.282671923091,17.790985803182,18.299299683273,18.807613563364, + 19.315927443455,19.824241323545,20.332555203636,20.840869083727, + 21.349182963818,21.857496843909,22.365810724), + .QUASI_UNIFORM_KNOTS.); +#4961 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#4962 = CARTESIAN_POINT('',(9.980039900004E-004,0.285786133916)); +#4963 = CARTESIAN_POINT('',(9.980039900034E-004,0.85102372403)); +#4964 = CARTESIAN_POINT('',(9.980039900089E-004,1.679658949168)); +#4965 = CARTESIAN_POINT('',(9.980039900038E-004,2.488775841388)); +#4966 = CARTESIAN_POINT('',(9.980039899976E-004,3.278357388811)); +#4967 = CARTESIAN_POINT('',(9.980039900065E-004,4.048590087612)); +#4968 = CARTESIAN_POINT('',(9.980039899984E-004,4.799873547661)); +#4969 = CARTESIAN_POINT('',(9.980039900006E-004,5.532780972207)); +#4970 = CARTESIAN_POINT('',(9.980039900001E-004,6.248020906897)); +#4971 = CARTESIAN_POINT('',(9.980039900003E-004,6.946360570455)); +#4972 = CARTESIAN_POINT('',(9.980039900002E-004,7.628688630766)); +#4973 = CARTESIAN_POINT('',(9.980039900008E-004,8.296073968645)); +#4974 = CARTESIAN_POINT('',(9.980039899987E-004,8.949683939675)); +#4975 = CARTESIAN_POINT('',(9.980039900069E-004,9.590744777157)); +#4976 = CARTESIAN_POINT('',(9.980039899976E-004,10.220499182656)); +#4977 = CARTESIAN_POINT('',(9.980039900055E-004,10.840182517569)); +#4978 = CARTESIAN_POINT('',(9.980039900049E-004,11.450961988571)); +#4979 = CARTESIAN_POINT('',(9.980039899999E-004,12.054057830263)); +#4980 = CARTESIAN_POINT('',(9.980039899994E-004,12.650784951439)); +#4981 = CARTESIAN_POINT('',(9.980039900069E-004,13.242437005955)); +#4982 = CARTESIAN_POINT('',(9.980039899991E-004,13.830311319745)); +#4983 = CARTESIAN_POINT('',(9.980039900015E-004,14.41570044305)); +#4984 = CARTESIAN_POINT('',(9.9800399E-004,14.999897615392)); +#4985 = CARTESIAN_POINT('',(9.980039900039E-004,15.584089013162)); +#4986 = CARTESIAN_POINT('',(9.980039900116E-004,16.169496121671)); +#4987 = CARTESIAN_POINT('',(9.980039899987E-004,16.757374010561)); +#4988 = CARTESIAN_POINT('',(9.980039900002E-004,17.349001915896)); +#4989 = CARTESIAN_POINT('',(9.980039900074E-004,17.945677524637)); +#4990 = CARTESIAN_POINT('',(9.980039899987E-004,18.548712218422)); +#4991 = CARTESIAN_POINT('',(9.98003990005E-004,19.159406294572)); +#4992 = CARTESIAN_POINT('',(9.980039899888E-004,19.779034539644)); +#4993 = CARTESIAN_POINT('',(9.980039900052E-004,20.408844110465)); +#4994 = CARTESIAN_POINT('',(9.980039899989E-004,21.050050714187)); +#4995 = CARTESIAN_POINT('',(9.98003990008E-004,21.703821238354)); +#4996 = CARTESIAN_POINT('',(9.980039899994E-004,22.371286805171)); +#4997 = CARTESIAN_POINT('',(9.980039900037E-004,23.053580529958)); +#4998 = CARTESIAN_POINT('',(9.980039899955E-004,23.751780886188)); +#4999 = CARTESIAN_POINT('',(9.980039900031E-004,24.466876465107)); +#5000 = CARTESIAN_POINT('',(9.980039900025E-004,25.199732649846)); +#5001 = CARTESIAN_POINT('',(9.980039899974E-004,25.951064415264)); +#5002 = CARTESIAN_POINT('',(9.980039899973E-004,26.721413682561)); +#5003 = CARTESIAN_POINT('',(9.980039900032E-004,27.511129450274)); +#5004 = CARTESIAN_POINT('',(9.980039900015E-004,28.320321952371)); +#5005 = CARTESIAN_POINT('',(9.980039900025E-004,29.148977248229)); +#5006 = CARTESIAN_POINT('',(9.980039900013E-004,29.714213803472)); +#5007 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#5008 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5009 = ORIENTED_EDGE('',*,*,#5010,.T.); +#5010 = EDGE_CURVE('',#4893,#4891,#5011,.T.); +#5011 = SURFACE_CURVE('',#5012,(#5037,#5065),.PCURVE_S1.); +#5012 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5013,#5014,#5015,#5016,#5017, + #5018,#5019,#5020,#5021,#5022,#5023,#5024,#5025,#5026,#5027,#5028, + #5029,#5030,#5031,#5032,#5033,#5034,#5035,#5036),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164535,7.85828164977, + 10.7238180444,13.583658992,16.4911854986,20.3877608659,22.3658107305 + ),.UNSPECIFIED.); +#5013 = CARTESIAN_POINT('',(30.,75.,20.)); +#5014 = CARTESIAN_POINT('',(30.,74.5328017475,20.)); +#5015 = CARTESIAN_POINT('',(29.9454303202,74.0320145358,20.)); +#5016 = CARTESIAN_POINT('',(29.8204177413,73.5088769651,20.)); +#5017 = CARTESIAN_POINT('',(29.4273138745,72.5199385656,20.)); +#5018 = CARTESIAN_POINT('',(28.7419853635,71.6190952598,20.)); +#5019 = CARTESIAN_POINT('',(28.354763807,71.2313736673,20.)); +#5020 = CARTESIAN_POINT('',(27.5674913754,70.6379119722,20.)); +#5021 = CARTESIAN_POINT('',(26.6451892654,70.2481596359,20.)); +#5022 = CARTESIAN_POINT('',(26.2232214419,70.1220806634,20.)); +#5023 = CARTESIAN_POINT('',(25.3562870364,69.9645190085,20.)); +#5024 = CARTESIAN_POINT('',(24.4735996985,69.9985992381,20.)); +#5025 = CARTESIAN_POINT('',(24.0369493269,70.0642514337,20.)); +#5026 = CARTESIAN_POINT('',(23.1813578806,70.2911542115,20.)); +#5027 = CARTESIAN_POINT('',(22.4042453811,70.6928693289,20.)); +#5028 = CARTESIAN_POINT('',(22.0396868149,70.9357809179,20.)); +#5029 = CARTESIAN_POINT('',(21.2644509637,71.5836987061,20.)); +#5030 = CARTESIAN_POINT('',(20.6904774017,72.3753443451,20.)); +#5031 = CARTESIAN_POINT('',(20.4362499781,72.8575518004,20.)); +#5032 = CARTESIAN_POINT('',(20.1637075652,73.5951810654,20.)); +#5033 = CARTESIAN_POINT('',(20.03878123,74.3111448986,20.)); +#5034 = CARTESIAN_POINT('',(20.0123667712,74.5475686232,20.)); +#5035 = CARTESIAN_POINT('',(20.,74.7775903343,20.)); +#5036 = CARTESIAN_POINT('',(20.,75.,20.)); +#5037 = PCURVE('',#3830,#5038); +#5038 = DEFINITIONAL_REPRESENTATION('',(#5039),#5064); +#5039 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5040,#5041,#5042,#5043,#5044, + #5045,#5046,#5047,#5048,#5049,#5050,#5051,#5052,#5053,#5054,#5055, + #5056,#5057,#5058,#5059,#5060,#5061,#5062,#5063),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164535,7.85828164977, + 10.7238180444,13.583658992,16.4911854986,20.3877608659,22.3658107305 + ),.UNSPECIFIED.); +#5040 = CARTESIAN_POINT('',(-60.,0.E+000)); +#5041 = CARTESIAN_POINT('',(-60.,-0.4671982525)); +#5042 = CARTESIAN_POINT('',(-60.0545696798,-0.9679854642)); +#5043 = CARTESIAN_POINT('',(-60.1795822587,-1.4911230349)); +#5044 = CARTESIAN_POINT('',(-60.5726861255,-2.4800614344)); +#5045 = CARTESIAN_POINT('',(-61.2580146365,-3.3809047402)); +#5046 = CARTESIAN_POINT('',(-61.645236193,-3.7686263327)); +#5047 = CARTESIAN_POINT('',(-62.4325086246,-4.3620880278)); +#5048 = CARTESIAN_POINT('',(-63.3548107346,-4.7518403641)); +#5049 = CARTESIAN_POINT('',(-63.7767785581,-4.8779193366)); +#5050 = CARTESIAN_POINT('',(-64.6437129636,-5.0354809915)); +#5051 = CARTESIAN_POINT('',(-65.5264003015,-5.0014007619)); +#5052 = CARTESIAN_POINT('',(-65.9630506731,-4.9357485663)); +#5053 = CARTESIAN_POINT('',(-66.8186421194,-4.7088457885)); +#5054 = CARTESIAN_POINT('',(-67.5957546189,-4.3071306711)); +#5055 = CARTESIAN_POINT('',(-67.9603131851,-4.0642190821)); +#5056 = CARTESIAN_POINT('',(-68.7355490363,-3.4163012939)); +#5057 = CARTESIAN_POINT('',(-69.3095225983,-2.6246556549)); +#5058 = CARTESIAN_POINT('',(-69.5637500219,-2.1424481996)); +#5059 = CARTESIAN_POINT('',(-69.8362924348,-1.4048189346)); +#5060 = CARTESIAN_POINT('',(-69.96121877,-0.6888551014)); +#5061 = CARTESIAN_POINT('',(-69.9876332288,-0.4524313768)); +#5062 = CARTESIAN_POINT('',(-70.,-0.2224096657)); +#5063 = CARTESIAN_POINT('',(-70.,0.E+000)); +#5064 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5065 = PCURVE('',#5066,#5075); +#5066 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#5067,#5068,#5069,#5070) + ,(#5071,#5072,#5073,#5074 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#5067 = CARTESIAN_POINT('',(30.,75.,20.)); +#5068 = CARTESIAN_POINT('',(30.,65.,20.)); +#5069 = CARTESIAN_POINT('',(20.,65.,20.)); +#5070 = CARTESIAN_POINT('',(20.,75.,20.)); +#5071 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#5072 = CARTESIAN_POINT('',(30.,65.,0.E+000)); +#5073 = CARTESIAN_POINT('',(20.,65.,0.E+000)); +#5074 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#5075 = DEFINITIONAL_REPRESENTATION('',(#5076),#5124); +#5076 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#5077,#5078,#5079,#5080,#5081, + #5082,#5083,#5084,#5085,#5086,#5087,#5088,#5089,#5090,#5091,#5092, + #5093,#5094,#5095,#5096,#5097,#5098,#5099,#5100,#5101,#5102,#5103, + #5104,#5105,#5106,#5107,#5108,#5109,#5110,#5111,#5112,#5113,#5114, + #5115,#5116,#5117,#5118,#5119,#5120,#5121,#5122,#5123), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313880239, + 1.016627760477,1.524941640716,2.033255520955,2.541569401193, + 3.049883281432,3.55819716167,4.066511041909,4.574824922148, + 5.083138802386,5.591452682625,6.099766562864,6.608080443102, + 7.116394323341,7.62470820358,8.133022083818,8.641335964057, + 9.149649844295,9.657963724534,10.166277604773,10.674591485011, + 11.18290536525,11.691219245489,12.199533125727,12.707847005966, + 13.216160886205,13.724474766443,14.232788646682,14.74110252692, + 15.249416407159,15.757730287398,16.266044167636,16.774358047875, + 17.282671928114,17.790985808352,18.299299688591,18.80761356883, + 19.315927449068,19.824241329307,20.332555209545,20.840869089784, + 21.349182970023,21.857496850261,22.3658107305), + .QUASI_UNIFORM_KNOTS.); +#5077 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#5078 = CARTESIAN_POINT('',(9.980039900022E-004,0.285786133971)); +#5079 = CARTESIAN_POINT('',(9.980039900043E-004,0.851023724195)); +#5080 = CARTESIAN_POINT('',(9.980039900031E-004,1.679658949476)); +#5081 = CARTESIAN_POINT('',(9.980039900048E-004,2.488775841771)); +#5082 = CARTESIAN_POINT('',(9.98003989999E-004,3.278357389236)); +#5083 = CARTESIAN_POINT('',(9.980039899992E-004,4.048590088088)); +#5084 = CARTESIAN_POINT('',(9.980039900044E-004,4.799873548236)); +#5085 = CARTESIAN_POINT('',(9.980039900046E-004,5.53278097294)); +#5086 = CARTESIAN_POINT('',(9.980039899987E-004,6.248020907828)); +#5087 = CARTESIAN_POINT('',(9.980039900008E-004,6.946360571572)); +#5088 = CARTESIAN_POINT('',(9.980039899982E-004,7.628688631929)); +#5089 = CARTESIAN_POINT('',(9.980039900067E-004,8.296073969708)); +#5090 = CARTESIAN_POINT('',(9.980039899968E-004,8.949683940569)); +#5091 = CARTESIAN_POINT('',(9.980039900066E-004,9.590744777915)); +#5092 = CARTESIAN_POINT('',(9.980039899988E-004,10.220499183388)); +#5093 = CARTESIAN_POINT('',(9.980039899989E-004,10.840182518419)); +#5094 = CARTESIAN_POINT('',(9.980039900062E-004,11.450961989592)); +#5095 = CARTESIAN_POINT('',(9.980039899985E-004,12.054057831516)); +#5096 = CARTESIAN_POINT('',(9.980039900007E-004,12.650784953019)); +#5097 = CARTESIAN_POINT('',(9.980039899996E-004,13.242437007878)); +#5098 = CARTESIAN_POINT('',(9.980039900019E-004,13.830311321935)); +#5099 = CARTESIAN_POINT('',(9.98003989994E-004,14.415700445387)); +#5100 = CARTESIAN_POINT('',(9.98003990002E-004,14.99989761786)); +#5101 = CARTESIAN_POINT('',(9.980039899994E-004,15.584089015774)); +#5102 = CARTESIAN_POINT('',(9.980039900019E-004,16.169496124441)); +#5103 = CARTESIAN_POINT('',(9.980039899944E-004,16.757374013504)); +#5104 = CARTESIAN_POINT('',(9.980039900008E-004,17.349001919024)); +#5105 = CARTESIAN_POINT('',(9.980039900043E-004,17.945677527939)); +#5106 = CARTESIAN_POINT('',(9.980039900054E-004,18.548712221896)); +#5107 = CARTESIAN_POINT('',(9.980039899975E-004,19.159406298292)); +#5108 = CARTESIAN_POINT('',(9.980039900068E-004,19.779034543671)); +#5109 = CARTESIAN_POINT('',(9.980039899986E-004,20.408844114822)); +#5110 = CARTESIAN_POINT('',(9.980039900009E-004,21.050050718852)); +#5111 = CARTESIAN_POINT('',(9.980039900003E-004,21.703821243354)); +#5112 = CARTESIAN_POINT('',(9.980039900004E-004,22.371286810459)); +#5113 = CARTESIAN_POINT('',(9.980039900006E-004,23.053580535369)); +#5114 = CARTESIAN_POINT('',(9.9800399E-004,23.751780891585)); +#5115 = CARTESIAN_POINT('',(9.980039900024E-004,24.466876470443)); +#5116 = CARTESIAN_POINT('',(9.980039899936E-004,25.199732655186)); +#5117 = CARTESIAN_POINT('',(9.980039900052E-004,25.951064420751)); +#5118 = CARTESIAN_POINT('',(9.980039900104E-004,26.721413688344)); +#5119 = CARTESIAN_POINT('',(9.980039899995E-004,27.511129456288)); +#5120 = CARTESIAN_POINT('',(9.980039899954E-004,28.320321955614)); +#5121 = CARTESIAN_POINT('',(9.980039900015E-004,29.148977248431)); +#5122 = CARTESIAN_POINT('',(9.980039900022E-004,29.714213803035)); +#5123 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#5124 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5125 = FACE_BOUND('',#5126,.T.); +#5126 = EDGE_LOOP('',(#5127,#5247)); +#5127 = ORIENTED_EDGE('',*,*,#5128,.T.); +#5128 = EDGE_CURVE('',#5129,#5131,#5133,.T.); +#5129 = VERTEX_POINT('',#5130); +#5130 = CARTESIAN_POINT('',(150.,75.,20.)); +#5131 = VERTEX_POINT('',#5132); +#5132 = CARTESIAN_POINT('',(160.,75.,20.)); +#5133 = SURFACE_CURVE('',#5134,(#5159,#5187),.PCURVE_S1.); +#5134 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5135,#5136,#5137,#5138,#5139, + #5140,#5141,#5142,#5143,#5144,#5145,#5146,#5147,#5148,#5149,#5150, + #5151,#5152,#5153,#5154,#5155,#5156,#5157,#5158),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164503,7.85828164656, + 10.7238180501,13.5836589945,16.4911855081,20.3877608582, + 22.3658107087),.UNSPECIFIED.); +#5135 = CARTESIAN_POINT('',(150.,75.,20.)); +#5136 = CARTESIAN_POINT('',(150.,75.4671982525,20.)); +#5137 = CARTESIAN_POINT('',(150.05456968,75.9679854642,20.)); +#5138 = CARTESIAN_POINT('',(150.179582259,76.491123035,20.)); +#5139 = CARTESIAN_POINT('',(150.572686125,77.4800614342,20.)); +#5140 = CARTESIAN_POINT('',(151.258014636,78.3809047395,20.)); +#5141 = CARTESIAN_POINT('',(151.645236194,78.7686263332,20.)); +#5142 = CARTESIAN_POINT('',(152.432508626,79.3620880288,20.)); +#5143 = CARTESIAN_POINT('',(153.354810737,79.7518403651,20.)); +#5144 = CARTESIAN_POINT('',(153.776778557,79.8779193362,20.)); +#5145 = CARTESIAN_POINT('',(154.643712964,80.0354809914,20.)); +#5146 = CARTESIAN_POINT('',(155.526400302,80.0014007619,20.)); +#5147 = CARTESIAN_POINT('',(155.963050674,79.9357485661,20.)); +#5148 = CARTESIAN_POINT('',(156.818642121,79.708845788,20.)); +#5149 = CARTESIAN_POINT('',(157.595754621,79.30713067,20.)); +#5150 = CARTESIAN_POINT('',(157.960313185,79.0642190816,20.)); +#5151 = CARTESIAN_POINT('',(158.735549036,78.4163012945,20.)); +#5152 = CARTESIAN_POINT('',(159.309522597,77.624655657,20.)); +#5153 = CARTESIAN_POINT('',(159.563750023,77.1424481935,20.)); +#5154 = CARTESIAN_POINT('',(159.836292434,76.4048189324,20.)); +#5155 = CARTESIAN_POINT('',(159.961218769,75.688855103,20.)); +#5156 = CARTESIAN_POINT('',(159.987633229,75.4524313736,20.)); +#5157 = CARTESIAN_POINT('',(160.,75.2224096641,20.)); +#5158 = CARTESIAN_POINT('',(160.,75.,20.)); +#5159 = PCURVE('',#3830,#5160); +#5160 = DEFINITIONAL_REPRESENTATION('',(#5161),#5186); +#5161 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5162,#5163,#5164,#5165,#5166, + #5167,#5168,#5169,#5170,#5171,#5172,#5173,#5174,#5175,#5176,#5177, + #5178,#5179,#5180,#5181,#5182,#5183,#5184,#5185),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164503,7.85828164656, + 10.7238180501,13.5836589945,16.4911855081,20.3877608582, + 22.3658107087),.UNSPECIFIED.); +#5162 = CARTESIAN_POINT('',(60.,0.E+000)); +#5163 = CARTESIAN_POINT('',(60.,0.4671982525)); +#5164 = CARTESIAN_POINT('',(60.05456968,0.9679854642)); +#5165 = CARTESIAN_POINT('',(60.179582259,1.491123035)); +#5166 = CARTESIAN_POINT('',(60.572686125,2.4800614342)); +#5167 = CARTESIAN_POINT('',(61.258014636,3.3809047395)); +#5168 = CARTESIAN_POINT('',(61.645236194,3.7686263332)); +#5169 = CARTESIAN_POINT('',(62.432508626,4.3620880288)); +#5170 = CARTESIAN_POINT('',(63.354810737,4.7518403651)); +#5171 = CARTESIAN_POINT('',(63.776778557,4.8779193362)); +#5172 = CARTESIAN_POINT('',(64.643712964,5.0354809914)); +#5173 = CARTESIAN_POINT('',(65.526400302,5.0014007619)); +#5174 = CARTESIAN_POINT('',(65.963050674,4.9357485661)); +#5175 = CARTESIAN_POINT('',(66.818642121,4.708845788)); +#5176 = CARTESIAN_POINT('',(67.595754621,4.30713067)); +#5177 = CARTESIAN_POINT('',(67.960313185,4.0642190816)); +#5178 = CARTESIAN_POINT('',(68.735549036,3.4163012945)); +#5179 = CARTESIAN_POINT('',(69.309522597,2.624655657)); +#5180 = CARTESIAN_POINT('',(69.563750023,2.1424481935)); +#5181 = CARTESIAN_POINT('',(69.836292434,1.4048189324)); +#5182 = CARTESIAN_POINT('',(69.961218769,0.688855103)); +#5183 = CARTESIAN_POINT('',(69.987633229,0.4524313736)); +#5184 = CARTESIAN_POINT('',(70.,0.2224096641)); +#5185 = CARTESIAN_POINT('',(70.,0.E+000)); +#5186 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5187 = PCURVE('',#5188,#5197); +#5188 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#5189,#5190,#5191,#5192) + ,(#5193,#5194,#5195,#5196 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#5189 = CARTESIAN_POINT('',(150.,75.,20.)); +#5190 = CARTESIAN_POINT('',(150.,85.,20.)); +#5191 = CARTESIAN_POINT('',(160.,85.,20.)); +#5192 = CARTESIAN_POINT('',(160.,75.,20.)); +#5193 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#5194 = CARTESIAN_POINT('',(150.,85.,0.E+000)); +#5195 = CARTESIAN_POINT('',(160.,85.,0.E+000)); +#5196 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#5197 = DEFINITIONAL_REPRESENTATION('',(#5198),#5246); +#5198 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#5199,#5200,#5201,#5202,#5203, + #5204,#5205,#5206,#5207,#5208,#5209,#5210,#5211,#5212,#5213,#5214, + #5215,#5216,#5217,#5218,#5219,#5220,#5221,#5222,#5223,#5224,#5225, + #5226,#5227,#5228,#5229,#5230,#5231,#5232,#5233,#5234,#5235,#5236, + #5237,#5238,#5239,#5240,#5241,#5242,#5243,#5244,#5245), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.508313879743, + 1.016627759486,1.52494163923,2.033255518973,2.541569398716, + 3.049883278459,3.558197158202,4.066511037945,4.574824917689, + 5.083138797432,5.591452677175,6.099766556918,6.608080436661, + 7.116394316405,7.624708196148,8.133022075891,8.641335955634, + 9.149649835377,9.65796371512,10.166277594864,10.674591474607, + 11.18290535435,11.691219234093,12.199533113836,12.70784699358, + 13.216160873323,13.724474753066,14.232788632809,14.741102512552, + 15.249416392295,15.757730272039,16.266044151782,16.774358031525, + 17.282671911268,17.790985791011,18.299299670755,18.807613550498, + 19.315927430241,19.824241309984,20.332555189727,20.84086906947, + 21.349182949214,21.857496828957,22.3658107087), + .QUASI_UNIFORM_KNOTS.); +#5199 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#5200 = CARTESIAN_POINT('',(9.980039900036E-004,0.285786133711)); +#5201 = CARTESIAN_POINT('',(9.980039900051E-004,0.85102372344)); +#5202 = CARTESIAN_POINT('',(9.980039899982E-004,1.679658948044)); +#5203 = CARTESIAN_POINT('',(9.980039900024E-004,2.488775839735)); +#5204 = CARTESIAN_POINT('',(9.980039899926E-004,3.278357386635)); +#5205 = CARTESIAN_POINT('',(9.980039900063E-004,4.048590084919)); +#5206 = CARTESIAN_POINT('',(9.980039900041E-004,4.799873544464)); +#5207 = CARTESIAN_POINT('',(9.980039899994E-004,5.532780968517)); +#5208 = CARTESIAN_POINT('',(9.980039899993E-004,6.248020902737)); +#5209 = CARTESIAN_POINT('',(9.980039900045E-004,6.94636056585)); +#5210 = CARTESIAN_POINT('',(9.980039900052E-004,7.628688626046)); +#5211 = CARTESIAN_POINT('',(9.980039899973E-004,8.296073964156)); +#5212 = CARTESIAN_POINT('',(9.980039900067E-004,8.94968393557)); +#5213 = CARTESIAN_POINT('',(9.980039899985E-004,9.590744773333)); +#5214 = CARTESIAN_POINT('',(9.980039900006E-004,10.220499178825)); +#5215 = CARTESIAN_POINT('',(9.980039900006E-004,10.840182513329)); +#5216 = CARTESIAN_POINT('',(9.980039899987E-004,11.450961983983)); +#5217 = CARTESIAN_POINT('',(9.980039900065E-004,12.054057824443)); +#5218 = CARTESIAN_POINT('',(9.980039899987E-004,12.650784943057)); +#5219 = CARTESIAN_POINT('',(9.980039900009E-004,13.24243699482)); +#5220 = CARTESIAN_POINT('',(9.9800399E-004,13.830311306814)); +#5221 = CARTESIAN_POINT('',(9.980039900017E-004,14.415700429728)); +#5222 = CARTESIAN_POINT('',(9.980039899959E-004,14.999897601529)); +#5223 = CARTESIAN_POINT('',(9.980039899963E-004,15.584088998856)); +#5224 = CARTESIAN_POINT('',(9.980039900007E-004,16.169496107211)); +#5225 = CARTESIAN_POINT('',(9.980039900043E-004,16.757373996043)); +#5226 = CARTESIAN_POINT('',(9.980039900068E-004,17.349001901135)); +#5227 = CARTESIAN_POINT('',(9.980039899934E-004,17.945677509452)); +#5228 = CARTESIAN_POINT('',(9.980039900022E-004,18.548712202426)); +#5229 = CARTESIAN_POINT('',(9.980039900019E-004,19.159406276733)); +#5230 = CARTESIAN_POINT('',(9.980039899946E-004,19.779034519507)); +#5231 = CARTESIAN_POINT('',(9.980039900029E-004,20.408844088363)); +#5232 = CARTESIAN_POINT('',(9.980039899986E-004,21.050050691109)); +#5233 = CARTESIAN_POINT('',(9.980039900075E-004,21.703821214481)); +#5234 = CARTESIAN_POINT('',(9.980039899975E-004,22.371286781119)); +#5235 = CARTESIAN_POINT('',(9.980039900075E-004,23.053580507691)); +#5236 = CARTESIAN_POINT('',(9.98003989999E-004,23.751780867479)); +#5237 = CARTESIAN_POINT('',(9.980039900017E-004,24.466876450808)); +#5238 = CARTESIAN_POINT('',(9.980039899996E-004,25.199732639387)); +#5239 = CARTESIAN_POINT('',(9.980039900057E-004,25.951064406832)); +#5240 = CARTESIAN_POINT('',(9.980039900049E-004,26.721413673865)); +#5241 = CARTESIAN_POINT('',(9.980039900022E-004,27.511129440899)); +#5242 = CARTESIAN_POINT('',(9.980039899927E-004,28.320321947345)); +#5243 = CARTESIAN_POINT('',(9.98003990012E-004,29.148977247975)); +#5244 = CARTESIAN_POINT('',(9.980039900095E-004,29.714213804197)); +#5245 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#5246 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5247 = ORIENTED_EDGE('',*,*,#5248,.T.); +#5248 = EDGE_CURVE('',#5131,#5129,#5249,.T.); +#5249 = SURFACE_CURVE('',#5250,(#5275,#5303),.PCURVE_S1.); +#5250 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5251,#5252,#5253,#5254,#5255, + #5256,#5257,#5258,#5259,#5260,#5261,#5262,#5263,#5264,#5265,#5266, + #5267,#5268,#5269,#5270,#5271,#5272,#5273,#5274),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164529,7.85828164788, + 10.7238180522,13.5836589941,16.4911855053,20.3877608633, + 22.3658107059),.UNSPECIFIED.); +#5251 = CARTESIAN_POINT('',(160.,75.,20.)); +#5252 = CARTESIAN_POINT('',(160.,74.5328017475,20.)); +#5253 = CARTESIAN_POINT('',(159.94543032,74.0320145358,20.)); +#5254 = CARTESIAN_POINT('',(159.820417741,73.5088769651,20.)); +#5255 = CARTESIAN_POINT('',(159.427313875,72.5199385658,20.)); +#5256 = CARTESIAN_POINT('',(158.741985364,71.6190952602,20.)); +#5257 = CARTESIAN_POINT('',(158.354763806,71.2313736669,20.)); +#5258 = CARTESIAN_POINT('',(157.567491374,70.6379119712,20.)); +#5259 = CARTESIAN_POINT('',(156.645189263,70.2481596349,20.)); +#5260 = CARTESIAN_POINT('',(156.223221443,70.1220806638,20.)); +#5261 = CARTESIAN_POINT('',(155.356287037,69.9645190086,20.)); +#5262 = CARTESIAN_POINT('',(154.473599699,69.9985992381,20.)); +#5263 = CARTESIAN_POINT('',(154.036949325,70.064251434,20.)); +#5264 = CARTESIAN_POINT('',(153.181357879,70.2911542121,20.)); +#5265 = CARTESIAN_POINT('',(152.40424538,70.6928693296,20.)); +#5266 = CARTESIAN_POINT('',(152.039686814,70.9357809188,20.)); +#5267 = CARTESIAN_POINT('',(151.264450963,71.5836987066,20.)); +#5268 = CARTESIAN_POINT('',(150.690477401,72.3753443448,20.)); +#5269 = CARTESIAN_POINT('',(150.436249977,72.8575518045,20.)); +#5270 = CARTESIAN_POINT('',(150.163707566,73.5951810656,20.)); +#5271 = CARTESIAN_POINT('',(150.038781231,74.3111448952,20.)); +#5272 = CARTESIAN_POINT('',(150.012366771,74.5475686283,20.)); +#5273 = CARTESIAN_POINT('',(150.,74.7775903368,20.)); +#5274 = CARTESIAN_POINT('',(150.,75.,20.)); +#5275 = PCURVE('',#3830,#5276); +#5276 = DEFINITIONAL_REPRESENTATION('',(#5277),#5302); +#5277 = B_SPLINE_CURVE_WITH_KNOTS('',5,(#5278,#5279,#5280,#5281,#5282, + #5283,#5284,#5285,#5286,#5287,#5288,#5289,#5290,#5291,#5292,#5293, + #5294,#5295,#5296,#5297,#5298,#5299,#5300,#5301),.UNSPECIFIED.,.F., + .F.,(6,3,3,3,3,3,3,6),(0.E+000,4.15513164529,7.85828164788, + 10.7238180522,13.5836589941,16.4911855053,20.3877608633, + 22.3658107059),.UNSPECIFIED.); +#5278 = CARTESIAN_POINT('',(70.,0.E+000)); +#5279 = CARTESIAN_POINT('',(70.,-0.4671982525)); +#5280 = CARTESIAN_POINT('',(69.94543032,-0.9679854642)); +#5281 = CARTESIAN_POINT('',(69.820417741,-1.4911230349)); +#5282 = CARTESIAN_POINT('',(69.427313875,-2.4800614342)); +#5283 = CARTESIAN_POINT('',(68.741985364,-3.3809047398)); +#5284 = CARTESIAN_POINT('',(68.354763806,-3.7686263331)); +#5285 = CARTESIAN_POINT('',(67.567491374,-4.3620880288)); +#5286 = CARTESIAN_POINT('',(66.645189263,-4.7518403651)); +#5287 = CARTESIAN_POINT('',(66.223221443,-4.8779193362)); +#5288 = CARTESIAN_POINT('',(65.356287037,-5.0354809914)); +#5289 = CARTESIAN_POINT('',(64.473599699,-5.0014007619)); +#5290 = CARTESIAN_POINT('',(64.036949325,-4.935748566)); +#5291 = CARTESIAN_POINT('',(63.181357879,-4.7088457879)); +#5292 = CARTESIAN_POINT('',(62.40424538,-4.3071306704)); +#5293 = CARTESIAN_POINT('',(62.039686814,-4.0642190812)); +#5294 = CARTESIAN_POINT('',(61.264450963,-3.4163012934)); +#5295 = CARTESIAN_POINT('',(60.690477401,-2.6246556552)); +#5296 = CARTESIAN_POINT('',(60.436249977,-2.1424481955)); +#5297 = CARTESIAN_POINT('',(60.163707566,-1.4048189344)); +#5298 = CARTESIAN_POINT('',(60.038781231,-0.6888551048)); +#5299 = CARTESIAN_POINT('',(60.012366771,-0.4524313717)); +#5300 = CARTESIAN_POINT('',(60.,-0.2224096632)); +#5301 = CARTESIAN_POINT('',(60.,0.E+000)); +#5302 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5303 = PCURVE('',#5304,#5313); +#5304 = ( BOUNDED_SURFACE() B_SPLINE_SURFACE(1,3,( + (#5305,#5306,#5307,#5308) + ,(#5309,#5310,#5311,#5312 +)),.UNSPECIFIED.,.F.,.F.,.F.) B_SPLINE_SURFACE_WITH_KNOTS((2,2),(4,4),( + 9.9800399E-004,20.000998004),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_SURFACE(( + (1.,0.33333333333,0.33333333333,1.) +,(1.,0.33333333333,0.33333333333,1. + ))) REPRESENTATION_ITEM('') SURFACE() ); +#5305 = CARTESIAN_POINT('',(160.,75.,20.)); +#5306 = CARTESIAN_POINT('',(160.,65.,20.)); +#5307 = CARTESIAN_POINT('',(150.,65.,20.)); +#5308 = CARTESIAN_POINT('',(150.,75.,20.)); +#5309 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#5310 = CARTESIAN_POINT('',(160.,65.,0.E+000)); +#5311 = CARTESIAN_POINT('',(150.,65.,0.E+000)); +#5312 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#5313 = DEFINITIONAL_REPRESENTATION('',(#5314),#5362); +#5314 = B_SPLINE_CURVE_WITH_KNOTS('',3,(#5315,#5316,#5317,#5318,#5319, + #5320,#5321,#5322,#5323,#5324,#5325,#5326,#5327,#5328,#5329,#5330, + #5331,#5332,#5333,#5334,#5335,#5336,#5337,#5338,#5339,#5340,#5341, + #5342,#5343,#5344,#5345,#5346,#5347,#5348,#5349,#5350,#5351,#5352, + #5353,#5354,#5355,#5356,#5357,#5358,#5359,#5360,#5361), + .UNSPECIFIED.,.F.,.F.,(4,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1 + ,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,4),(0.E+000,0.50831387968, + 1.016627759359,1.524941639039,2.033255518718,2.541569398398, + 3.049883278077,3.558197157757,4.066511037436,4.574824917116, + 5.083138796795,5.591452676475,6.099766556155,6.608080435834, + 7.116394315514,7.624708195193,8.133022074873,8.641335954552, + 9.149649834232,9.657963713911,10.166277593591,10.67459147327, + 11.18290535295,11.69121923263,12.199533112309,12.707846991989, + 13.216160871668,13.724474751348,14.232788631027,14.741102510707, + 15.249416390386,15.757730270066,16.266044149745,16.774358029425, + 17.282671909105,17.790985788784,18.299299668464,18.807613548143, + 19.315927427823,19.824241307502,20.332555187182,20.840869066861, + 21.349182946541,21.85749682622,22.3658107059),.UNSPECIFIED.); +#5315 = CARTESIAN_POINT('',(9.9800399E-004,0.E+000)); +#5316 = CARTESIAN_POINT('',(9.980039899982E-004,0.285786133659)); +#5317 = CARTESIAN_POINT('',(9.980039899991E-004,0.85102372328)); +#5318 = CARTESIAN_POINT('',(9.980039900059E-004,1.679658947713)); +#5319 = CARTESIAN_POINT('',(9.980039899988E-004,2.488775839219)); +#5320 = CARTESIAN_POINT('',(9.980039899993E-004,3.278357385934)); +#5321 = CARTESIAN_POINT('',(9.980039900046E-004,4.048590084048)); +#5322 = CARTESIAN_POINT('',(9.980039900041E-004,4.799873543449)); +#5323 = CARTESIAN_POINT('',(9.980039900009E-004,5.53278096739)); +#5324 = CARTESIAN_POINT('',(9.98003989993E-004,6.248020901528)); +#5325 = CARTESIAN_POINT('',(9.980039900066E-004,6.946360564584)); +#5326 = CARTESIAN_POINT('',(9.980039900029E-004,7.628688624637)); +#5327 = CARTESIAN_POINT('',(9.98003990004E-004,8.296073962504)); +#5328 = CARTESIAN_POINT('',(9.980039900034E-004,8.949683933624)); +#5329 = CARTESIAN_POINT('',(9.980039900051E-004,9.590744771096)); +#5330 = CARTESIAN_POINT('',(9.98003989999E-004,10.220499176339)); +#5331 = CARTESIAN_POINT('',(9.980039900006E-004,10.840182510642)); +#5332 = CARTESIAN_POINT('',(9.980039900003E-004,11.450961981105)); +#5333 = CARTESIAN_POINT('',(9.980039900003E-004,12.054057821357)); +#5334 = CARTESIAN_POINT('',(9.980039900008E-004,12.65078493973)); +#5335 = CARTESIAN_POINT('',(9.980039899988E-004,13.242436991189)); +#5336 = CARTESIAN_POINT('',(9.980039900065E-004,13.830311302823)); +#5337 = CARTESIAN_POINT('',(9.980039899992E-004,14.415700425386)); +#5338 = CARTESIAN_POINT('',(9.980039899994E-004,14.999897597052)); +#5339 = CARTESIAN_POINT('',(9.980039900061E-004,15.584088995026)); +#5340 = CARTESIAN_POINT('',(9.980039900006E-004,16.169496104535)); +#5341 = CARTESIAN_POINT('',(9.98003989995E-004,16.757373994389)); +#5342 = CARTESIAN_POINT('',(9.980039900016E-004,17.349001899823)); +#5343 = CARTESIAN_POINT('',(9.980039900023E-004,17.945677507762)); +#5344 = CARTESIAN_POINT('',(9.98003989993E-004,18.548712200658)); +#5345 = CARTESIAN_POINT('',(9.980039900086E-004,19.159406275716)); +#5346 = CARTESIAN_POINT('',(9.980039899984E-004,19.779034519667)); +#5347 = CARTESIAN_POINT('',(9.980039900025E-004,20.408844089475)); +#5348 = CARTESIAN_POINT('',(9.980039899962E-004,21.050050692392)); +#5349 = CARTESIAN_POINT('',(9.980039899963E-004,21.703821215721)); +#5350 = CARTESIAN_POINT('',(9.980039900025E-004,22.371286781966)); +#5351 = CARTESIAN_POINT('',(9.98003989999E-004,23.05358050698)); +#5352 = CARTESIAN_POINT('',(9.980039900072E-004,23.751780864192)); +#5353 = CARTESIAN_POINT('',(9.980039899992E-004,24.466876444464)); +#5354 = CARTESIAN_POINT('',(9.980039900019E-004,25.199732630295)); +#5355 = CARTESIAN_POINT('',(9.980039899993E-004,25.951064395972)); +#5356 = CARTESIAN_POINT('',(9.980039900073E-004,26.721413662433)); +#5357 = CARTESIAN_POINT('',(9.980039899995E-004,27.511129429173)); +#5358 = CARTESIAN_POINT('',(9.980039900016E-004,28.320321940868)); +#5359 = CARTESIAN_POINT('',(9.980039900014E-004,29.148977247292)); +#5360 = CARTESIAN_POINT('',(9.980039900008E-004,29.714213804884)); +#5361 = CARTESIAN_POINT('',(9.9800399E-004,30.)); +#5362 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5363 = ADVANCED_FACE('',(#5364),#3842,.T.); +#5364 = FACE_BOUND('',#5365,.T.); +#5365 = EDGE_LOOP('',(#5366,#5389,#5390,#5413)); +#5366 = ORIENTED_EDGE('',*,*,#5367,.T.); +#5367 = EDGE_CURVE('',#5368,#3820,#5370,.T.); +#5368 = VERTEX_POINT('',#5369); +#5369 = CARTESIAN_POINT('',(180.,0.E+000,0.E+000)); +#5370 = SURFACE_CURVE('',#5371,(#5375,#5382),.PCURVE_S1.); +#5371 = LINE('',#5372,#5373); +#5372 = CARTESIAN_POINT('',(180.,0.E+000,10.)); +#5373 = VECTOR('',#5374,1.); +#5374 = DIRECTION('',(0.E+000,0.E+000,1.)); +#5375 = PCURVE('',#3842,#5376); +#5376 = DEFINITIONAL_REPRESENTATION('',(#5377),#5381); +#5377 = LINE('',#5378,#5379); +#5378 = CARTESIAN_POINT('',(-10.,90.)); +#5379 = VECTOR('',#5380,1.); +#5380 = DIRECTION('',(-1.,0.E+000)); +#5381 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5382 = PCURVE('',#3870,#5383); +#5383 = DEFINITIONAL_REPRESENTATION('',(#5384),#5388); +#5384 = LINE('',#5385,#5386); +#5385 = CARTESIAN_POINT('',(-10.,-75.)); +#5386 = VECTOR('',#5387,1.); +#5387 = DIRECTION('',(-1.,0.E+000)); +#5388 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5389 = ORIENTED_EDGE('',*,*,#3819,.T.); +#5390 = ORIENTED_EDGE('',*,*,#5391,.F.); +#5391 = EDGE_CURVE('',#5392,#3822,#5394,.T.); +#5392 = VERTEX_POINT('',#5393); +#5393 = CARTESIAN_POINT('',(0.E+000,0.E+000,0.E+000)); +#5394 = SURFACE_CURVE('',#5395,(#5399,#5406),.PCURVE_S1.); +#5395 = LINE('',#5396,#5397); +#5396 = CARTESIAN_POINT('',(0.E+000,0.E+000,10.)); +#5397 = VECTOR('',#5398,1.); +#5398 = DIRECTION('',(0.E+000,0.E+000,1.)); +#5399 = PCURVE('',#3842,#5400); +#5400 = DEFINITIONAL_REPRESENTATION('',(#5401),#5405); +#5401 = LINE('',#5402,#5403); +#5402 = CARTESIAN_POINT('',(-10.,-90.)); +#5403 = VECTOR('',#5404,1.); +#5404 = DIRECTION('',(-1.,0.E+000)); +#5405 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5406 = PCURVE('',#3924,#5407); +#5407 = DEFINITIONAL_REPRESENTATION('',(#5408),#5412); +#5408 = LINE('',#5409,#5410); +#5409 = CARTESIAN_POINT('',(10.,-75.)); +#5410 = VECTOR('',#5411,1.); +#5411 = DIRECTION('',(1.,0.E+000)); +#5412 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5413 = ORIENTED_EDGE('',*,*,#5414,.T.); +#5414 = EDGE_CURVE('',#5392,#5368,#5415,.T.); +#5415 = SURFACE_CURVE('',#5416,(#5420,#5427),.PCURVE_S1.); +#5416 = LINE('',#5417,#5418); +#5417 = CARTESIAN_POINT('',(90.,0.E+000,0.E+000)); +#5418 = VECTOR('',#5419,1.); +#5419 = DIRECTION('',(1.,0.E+000,0.E+000)); +#5420 = PCURVE('',#3842,#5421); +#5421 = DEFINITIONAL_REPRESENTATION('',(#5422),#5426); +#5422 = LINE('',#5423,#5424); +#5423 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5424 = VECTOR('',#5425,1.); +#5425 = DIRECTION('',(0.E+000,1.)); +#5426 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5427 = PCURVE('',#5428,#5433); +#5428 = PLANE('',#5429); +#5429 = AXIS2_PLACEMENT_3D('',#5430,#5431,#5432); +#5430 = CARTESIAN_POINT('',(90.,75.,0.E+000)); +#5431 = DIRECTION('',(0.E+000,0.E+000,-1.)); +#5432 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#5433 = DEFINITIONAL_REPRESENTATION('',(#5434),#5438); +#5434 = LINE('',#5435,#5436); +#5435 = CARTESIAN_POINT('',(0.E+000,-75.)); +#5436 = VECTOR('',#5437,1.); +#5437 = DIRECTION('',(-1.,0.E+000)); +#5438 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5439 = ADVANCED_FACE('',(#5440),#3870,.T.); +#5440 = FACE_BOUND('',#5441,.T.); +#5441 = EDGE_LOOP('',(#5442,#5465,#5486,#5487)); +#5442 = ORIENTED_EDGE('',*,*,#5443,.T.); +#5443 = EDGE_CURVE('',#5368,#5444,#5446,.T.); +#5444 = VERTEX_POINT('',#5445); +#5445 = CARTESIAN_POINT('',(180.,150.,0.E+000)); +#5446 = SURFACE_CURVE('',#5447,(#5451,#5458),.PCURVE_S1.); +#5447 = LINE('',#5448,#5449); +#5448 = CARTESIAN_POINT('',(180.,75.,0.E+000)); +#5449 = VECTOR('',#5450,1.); +#5450 = DIRECTION('',(0.E+000,1.,0.E+000)); +#5451 = PCURVE('',#3870,#5452); +#5452 = DEFINITIONAL_REPRESENTATION('',(#5453),#5457); +#5453 = LINE('',#5454,#5455); +#5454 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5455 = VECTOR('',#5456,1.); +#5456 = DIRECTION('',(0.E+000,1.)); +#5457 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5458 = PCURVE('',#5428,#5459); +#5459 = DEFINITIONAL_REPRESENTATION('',(#5460),#5464); +#5460 = LINE('',#5461,#5462); +#5461 = CARTESIAN_POINT('',(-90.,0.E+000)); +#5462 = VECTOR('',#5463,1.); +#5463 = DIRECTION('',(0.E+000,1.)); +#5464 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5465 = ORIENTED_EDGE('',*,*,#5466,.T.); +#5466 = EDGE_CURVE('',#5444,#3855,#5467,.T.); +#5467 = SURFACE_CURVE('',#5468,(#5472,#5479),.PCURVE_S1.); +#5468 = LINE('',#5469,#5470); +#5469 = CARTESIAN_POINT('',(180.,150.,10.)); +#5470 = VECTOR('',#5471,1.); +#5471 = DIRECTION('',(0.E+000,0.E+000,1.)); +#5472 = PCURVE('',#3870,#5473); +#5473 = DEFINITIONAL_REPRESENTATION('',(#5474),#5478); +#5474 = LINE('',#5475,#5476); +#5475 = CARTESIAN_POINT('',(-10.,75.)); +#5476 = VECTOR('',#5477,1.); +#5477 = DIRECTION('',(-1.,0.E+000)); +#5478 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5479 = PCURVE('',#3898,#5480); +#5480 = DEFINITIONAL_REPRESENTATION('',(#5481),#5485); +#5481 = LINE('',#5482,#5483); +#5482 = CARTESIAN_POINT('',(10.,90.)); +#5483 = VECTOR('',#5484,1.); +#5484 = DIRECTION('',(1.,0.E+000)); +#5485 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5486 = ORIENTED_EDGE('',*,*,#3854,.T.); +#5487 = ORIENTED_EDGE('',*,*,#5367,.F.); +#5488 = ADVANCED_FACE('',(#5489),#3898,.T.); +#5489 = FACE_BOUND('',#5490,.T.); +#5490 = EDGE_LOOP('',(#5491,#5514,#5515,#5516)); +#5491 = ORIENTED_EDGE('',*,*,#5492,.T.); +#5492 = EDGE_CURVE('',#5493,#3883,#5495,.T.); +#5493 = VERTEX_POINT('',#5494); +#5494 = CARTESIAN_POINT('',(0.E+000,150.,0.E+000)); +#5495 = SURFACE_CURVE('',#5496,(#5500,#5507),.PCURVE_S1.); +#5496 = LINE('',#5497,#5498); +#5497 = CARTESIAN_POINT('',(0.E+000,150.,10.)); +#5498 = VECTOR('',#5499,1.); +#5499 = DIRECTION('',(0.E+000,0.E+000,1.)); +#5500 = PCURVE('',#3898,#5501); +#5501 = DEFINITIONAL_REPRESENTATION('',(#5502),#5506); +#5502 = LINE('',#5503,#5504); +#5503 = CARTESIAN_POINT('',(10.,-90.)); +#5504 = VECTOR('',#5505,1.); +#5505 = DIRECTION('',(1.,0.E+000)); +#5506 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5507 = PCURVE('',#3924,#5508); +#5508 = DEFINITIONAL_REPRESENTATION('',(#5509),#5513); +#5509 = LINE('',#5510,#5511); +#5510 = CARTESIAN_POINT('',(10.,75.)); +#5511 = VECTOR('',#5512,1.); +#5512 = DIRECTION('',(1.,0.E+000)); +#5513 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5514 = ORIENTED_EDGE('',*,*,#3882,.T.); +#5515 = ORIENTED_EDGE('',*,*,#5466,.F.); +#5516 = ORIENTED_EDGE('',*,*,#5517,.T.); +#5517 = EDGE_CURVE('',#5444,#5493,#5518,.T.); +#5518 = SURFACE_CURVE('',#5519,(#5523,#5530),.PCURVE_S1.); +#5519 = LINE('',#5520,#5521); +#5520 = CARTESIAN_POINT('',(90.,150.,0.E+000)); +#5521 = VECTOR('',#5522,1.); +#5522 = DIRECTION('',(-1.,0.E+000,0.E+000)); +#5523 = PCURVE('',#3898,#5524); +#5524 = DEFINITIONAL_REPRESENTATION('',(#5525),#5529); +#5525 = LINE('',#5526,#5527); +#5526 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5527 = VECTOR('',#5528,1.); +#5528 = DIRECTION('',(0.E+000,-1.)); +#5529 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5530 = PCURVE('',#5428,#5531); +#5531 = DEFINITIONAL_REPRESENTATION('',(#5532),#5536); +#5532 = LINE('',#5533,#5534); +#5533 = CARTESIAN_POINT('',(0.E+000,75.)); +#5534 = VECTOR('',#5535,1.); +#5535 = DIRECTION('',(1.,0.E+000)); +#5536 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5537 = ADVANCED_FACE('',(#5538),#3924,.T.); +#5538 = FACE_BOUND('',#5539,.T.); +#5539 = EDGE_LOOP('',(#5540,#5541,#5542,#5543)); +#5540 = ORIENTED_EDGE('',*,*,#5391,.T.); +#5541 = ORIENTED_EDGE('',*,*,#3910,.T.); +#5542 = ORIENTED_EDGE('',*,*,#5492,.F.); +#5543 = ORIENTED_EDGE('',*,*,#5544,.T.); +#5544 = EDGE_CURVE('',#5493,#5392,#5545,.T.); +#5545 = SURFACE_CURVE('',#5546,(#5550,#5557),.PCURVE_S1.); +#5546 = LINE('',#5547,#5548); +#5547 = CARTESIAN_POINT('',(0.E+000,75.,0.E+000)); +#5548 = VECTOR('',#5549,1.); +#5549 = DIRECTION('',(0.E+000,-1.,0.E+000)); +#5550 = PCURVE('',#3924,#5551); +#5551 = DEFINITIONAL_REPRESENTATION('',(#5552),#5556); +#5552 = LINE('',#5553,#5554); +#5553 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5554 = VECTOR('',#5555,1.); +#5555 = DIRECTION('',(0.E+000,-1.)); +#5556 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5557 = PCURVE('',#5428,#5558); +#5558 = DEFINITIONAL_REPRESENTATION('',(#5559),#5563); +#5559 = LINE('',#5560,#5561); +#5560 = CARTESIAN_POINT('',(90.,0.E+000)); +#5561 = VECTOR('',#5562,1.); +#5562 = DIRECTION('',(0.E+000,-1.)); +#5563 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5564 = ADVANCED_FACE('',(#5565),#3998,.T.); +#5565 = FACE_BOUND('',#5566,.T.); +#5566 = EDGE_LOOP('',(#5567,#5594,#5614,#5615)); +#5567 = ORIENTED_EDGE('',*,*,#5568,.T.); +#5568 = EDGE_CURVE('',#5569,#5571,#5573,.T.); +#5569 = VERTEX_POINT('',#5570); +#5570 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#5571 = VERTEX_POINT('',#5572); +#5572 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#5573 = SURFACE_CURVE('',#5574,(#5579,#5586),.PCURVE_S1.); +#5574 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5575,#5576,#5577,#5578), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5575 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#5576 = CARTESIAN_POINT('',(42.5,97.99038106,0.E+000)); +#5577 = CARTESIAN_POINT('',(52.5,97.99038106,0.E+000)); +#5578 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#5579 = PCURVE('',#3998,#5580); +#5580 = DEFINITIONAL_REPRESENTATION('',(#5581),#5585); +#5581 = LINE('',#5582,#5583); +#5582 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5583 = VECTOR('',#5584,1.); +#5584 = DIRECTION('',(0.E+000,1.)); +#5585 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5586 = PCURVE('',#5428,#5587); +#5587 = DEFINITIONAL_REPRESENTATION('',(#5588),#5593); +#5588 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5589,#5590,#5591,#5592), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5589 = CARTESIAN_POINT('',(47.5,12.99038106)); +#5590 = CARTESIAN_POINT('',(47.5,22.99038106)); +#5591 = CARTESIAN_POINT('',(37.5,22.99038106)); +#5592 = CARTESIAN_POINT('',(37.5,12.99038106)); +#5593 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5594 = ORIENTED_EDGE('',*,*,#5595,.F.); +#5595 = EDGE_CURVE('',#3941,#5571,#5596,.T.); +#5596 = SURFACE_CURVE('',#5597,(#5600,#5607),.PCURVE_S1.); +#5597 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5598,#5599),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5598 = CARTESIAN_POINT('',(52.5,87.99038106,20.)); +#5599 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#5600 = PCURVE('',#3998,#5601); +#5601 = DEFINITIONAL_REPRESENTATION('',(#5602),#5606); +#5602 = LINE('',#5603,#5604); +#5603 = CARTESIAN_POINT('',(0.E+000,30.)); +#5604 = VECTOR('',#5605,1.); +#5605 = DIRECTION('',(1.,0.E+000)); +#5606 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5607 = PCURVE('',#4114,#5608); +#5608 = DEFINITIONAL_REPRESENTATION('',(#5609),#5613); +#5609 = LINE('',#5610,#5611); +#5610 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5611 = VECTOR('',#5612,1.); +#5612 = DIRECTION('',(1.,0.E+000)); +#5613 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5614 = ORIENTED_EDGE('',*,*,#3938,.F.); +#5615 = ORIENTED_EDGE('',*,*,#5616,.T.); +#5616 = EDGE_CURVE('',#3939,#5569,#5617,.T.); +#5617 = SURFACE_CURVE('',#5618,(#5621,#5628),.PCURVE_S1.); +#5618 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5619,#5620),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5619 = CARTESIAN_POINT('',(42.5,87.99038106,20.)); +#5620 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#5621 = PCURVE('',#3998,#5622); +#5622 = DEFINITIONAL_REPRESENTATION('',(#5623),#5627); +#5623 = LINE('',#5624,#5625); +#5624 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5625 = VECTOR('',#5626,1.); +#5626 = DIRECTION('',(1.,0.E+000)); +#5627 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5628 = PCURVE('',#4114,#5629); +#5629 = DEFINITIONAL_REPRESENTATION('',(#5630),#5634); +#5630 = LINE('',#5631,#5632); +#5631 = CARTESIAN_POINT('',(0.E+000,30.)); +#5632 = VECTOR('',#5633,1.); +#5633 = DIRECTION('',(1.,0.E+000)); +#5634 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5635 = ADVANCED_FACE('',(#5636),#4114,.T.); +#5636 = FACE_BOUND('',#5637,.T.); +#5637 = EDGE_LOOP('',(#5638,#5661,#5662,#5663)); +#5638 = ORIENTED_EDGE('',*,*,#5639,.T.); +#5639 = EDGE_CURVE('',#5571,#5569,#5640,.T.); +#5640 = SURFACE_CURVE('',#5641,(#5646,#5653),.PCURVE_S1.); +#5641 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5642,#5643,#5644,#5645), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5642 = CARTESIAN_POINT('',(52.5,87.99038106,0.E+000)); +#5643 = CARTESIAN_POINT('',(52.5,77.99038106,0.E+000)); +#5644 = CARTESIAN_POINT('',(42.5,77.99038106,0.E+000)); +#5645 = CARTESIAN_POINT('',(42.5,87.99038106,0.E+000)); +#5646 = PCURVE('',#4114,#5647); +#5647 = DEFINITIONAL_REPRESENTATION('',(#5648),#5652); +#5648 = LINE('',#5649,#5650); +#5649 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5650 = VECTOR('',#5651,1.); +#5651 = DIRECTION('',(0.E+000,1.)); +#5652 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5653 = PCURVE('',#5428,#5654); +#5654 = DEFINITIONAL_REPRESENTATION('',(#5655),#5660); +#5655 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5656,#5657,#5658,#5659), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5656 = CARTESIAN_POINT('',(37.5,12.99038106)); +#5657 = CARTESIAN_POINT('',(37.5,2.99038106)); +#5658 = CARTESIAN_POINT('',(47.5,2.99038106)); +#5659 = CARTESIAN_POINT('',(47.5,12.99038106)); +#5660 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5661 = ORIENTED_EDGE('',*,*,#5616,.F.); +#5662 = ORIENTED_EDGE('',*,*,#4058,.F.); +#5663 = ORIENTED_EDGE('',*,*,#5595,.T.); +#5664 = ADVANCED_FACE('',(#5665),#4236,.T.); +#5665 = FACE_BOUND('',#5666,.T.); +#5666 = EDGE_LOOP('',(#5667,#5694,#5714,#5715)); +#5667 = ORIENTED_EDGE('',*,*,#5668,.T.); +#5668 = EDGE_CURVE('',#5669,#5671,#5673,.T.); +#5669 = VERTEX_POINT('',#5670); +#5670 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#5671 = VERTEX_POINT('',#5672); +#5672 = CARTESIAN_POINT('',(52.5,62.00961894,-1.7763568394E-015)); +#5673 = SURFACE_CURVE('',#5674,(#5679,#5686),.PCURVE_S1.); +#5674 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5675,#5676,#5677,#5678), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5675 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#5676 = CARTESIAN_POINT('',(42.5,72.00961894,0.E+000)); +#5677 = CARTESIAN_POINT('',(52.5,72.00961894,0.E+000)); +#5678 = CARTESIAN_POINT('',(52.5,62.00961894,0.E+000)); +#5679 = PCURVE('',#4236,#5680); +#5680 = DEFINITIONAL_REPRESENTATION('',(#5681),#5685); +#5681 = LINE('',#5682,#5683); +#5682 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5683 = VECTOR('',#5684,1.); +#5684 = DIRECTION('',(0.E+000,1.)); +#5685 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5686 = PCURVE('',#5428,#5687); +#5687 = DEFINITIONAL_REPRESENTATION('',(#5688),#5693); +#5688 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5689,#5690,#5691,#5692), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5689 = CARTESIAN_POINT('',(47.5,-12.99038106)); +#5690 = CARTESIAN_POINT('',(47.5,-2.99038106)); +#5691 = CARTESIAN_POINT('',(37.5,-2.99038106)); +#5692 = CARTESIAN_POINT('',(37.5,-12.99038106)); +#5693 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5694 = ORIENTED_EDGE('',*,*,#5695,.F.); +#5695 = EDGE_CURVE('',#4179,#5671,#5696,.T.); +#5696 = SURFACE_CURVE('',#5697,(#5700,#5707),.PCURVE_S1.); +#5697 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5698,#5699),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5698 = CARTESIAN_POINT('',(52.5,62.00961894,20.)); +#5699 = CARTESIAN_POINT('',(52.5,62.00961894,0.E+000)); +#5700 = PCURVE('',#4236,#5701); +#5701 = DEFINITIONAL_REPRESENTATION('',(#5702),#5706); +#5702 = LINE('',#5703,#5704); +#5703 = CARTESIAN_POINT('',(0.E+000,30.)); +#5704 = VECTOR('',#5705,1.); +#5705 = DIRECTION('',(1.,0.E+000)); +#5706 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5707 = PCURVE('',#4352,#5708); +#5708 = DEFINITIONAL_REPRESENTATION('',(#5709),#5713); +#5709 = LINE('',#5710,#5711); +#5710 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5711 = VECTOR('',#5712,1.); +#5712 = DIRECTION('',(1.,0.E+000)); +#5713 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5714 = ORIENTED_EDGE('',*,*,#4176,.F.); +#5715 = ORIENTED_EDGE('',*,*,#5716,.T.); +#5716 = EDGE_CURVE('',#4177,#5669,#5717,.T.); +#5717 = SURFACE_CURVE('',#5718,(#5721,#5728),.PCURVE_S1.); +#5718 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5719,#5720),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5719 = CARTESIAN_POINT('',(42.5,62.00961894,20.)); +#5720 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#5721 = PCURVE('',#4236,#5722); +#5722 = DEFINITIONAL_REPRESENTATION('',(#5723),#5727); +#5723 = LINE('',#5724,#5725); +#5724 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5725 = VECTOR('',#5726,1.); +#5726 = DIRECTION('',(1.,0.E+000)); +#5727 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5728 = PCURVE('',#4352,#5729); +#5729 = DEFINITIONAL_REPRESENTATION('',(#5730),#5734); +#5730 = LINE('',#5731,#5732); +#5731 = CARTESIAN_POINT('',(0.E+000,30.)); +#5732 = VECTOR('',#5733,1.); +#5733 = DIRECTION('',(1.,0.E+000)); +#5734 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5735 = ADVANCED_FACE('',(#5736),#4352,.T.); +#5736 = FACE_BOUND('',#5737,.T.); +#5737 = EDGE_LOOP('',(#5738,#5761,#5762,#5763)); +#5738 = ORIENTED_EDGE('',*,*,#5739,.T.); +#5739 = EDGE_CURVE('',#5671,#5669,#5740,.T.); +#5740 = SURFACE_CURVE('',#5741,(#5746,#5753),.PCURVE_S1.); +#5741 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5742,#5743,#5744,#5745), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5742 = CARTESIAN_POINT('',(52.5,62.00961894,0.E+000)); +#5743 = CARTESIAN_POINT('',(52.5,52.00961894,0.E+000)); +#5744 = CARTESIAN_POINT('',(42.5,52.00961894,0.E+000)); +#5745 = CARTESIAN_POINT('',(42.5,62.00961894,0.E+000)); +#5746 = PCURVE('',#4352,#5747); +#5747 = DEFINITIONAL_REPRESENTATION('',(#5748),#5752); +#5748 = LINE('',#5749,#5750); +#5749 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5750 = VECTOR('',#5751,1.); +#5751 = DIRECTION('',(0.E+000,1.)); +#5752 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5753 = PCURVE('',#5428,#5754); +#5754 = DEFINITIONAL_REPRESENTATION('',(#5755),#5760); +#5755 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5756,#5757,#5758,#5759), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5756 = CARTESIAN_POINT('',(37.5,-12.99038106)); +#5757 = CARTESIAN_POINT('',(37.5,-22.99038106)); +#5758 = CARTESIAN_POINT('',(47.5,-22.99038106)); +#5759 = CARTESIAN_POINT('',(47.5,-12.99038106)); +#5760 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5761 = ORIENTED_EDGE('',*,*,#5716,.F.); +#5762 = ORIENTED_EDGE('',*,*,#4296,.F.); +#5763 = ORIENTED_EDGE('',*,*,#5695,.T.); +#5764 = ADVANCED_FACE('',(#5765),#4474,.T.); +#5765 = FACE_BOUND('',#5766,.T.); +#5766 = EDGE_LOOP('',(#5767,#5794,#5814,#5815)); +#5767 = ORIENTED_EDGE('',*,*,#5768,.T.); +#5768 = EDGE_CURVE('',#5769,#5771,#5773,.T.); +#5769 = VERTEX_POINT('',#5770); +#5770 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#5771 = VERTEX_POINT('',#5772); +#5772 = CARTESIAN_POINT('',(137.5,62.00961894,-1.7763568394E-015)); +#5773 = SURFACE_CURVE('',#5774,(#5779,#5786),.PCURVE_S1.); +#5774 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5775,#5776,#5777,#5778), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5775 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#5776 = CARTESIAN_POINT('',(127.5,72.00961894,0.E+000)); +#5777 = CARTESIAN_POINT('',(137.5,72.00961894,0.E+000)); +#5778 = CARTESIAN_POINT('',(137.5,62.00961894,0.E+000)); +#5779 = PCURVE('',#4474,#5780); +#5780 = DEFINITIONAL_REPRESENTATION('',(#5781),#5785); +#5781 = LINE('',#5782,#5783); +#5782 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5783 = VECTOR('',#5784,1.); +#5784 = DIRECTION('',(0.E+000,1.)); +#5785 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5786 = PCURVE('',#5428,#5787); +#5787 = DEFINITIONAL_REPRESENTATION('',(#5788),#5793); +#5788 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5789,#5790,#5791,#5792), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5789 = CARTESIAN_POINT('',(-37.5,-12.99038106)); +#5790 = CARTESIAN_POINT('',(-37.5,-2.99038106)); +#5791 = CARTESIAN_POINT('',(-47.5,-2.99038106)); +#5792 = CARTESIAN_POINT('',(-47.5,-12.99038106)); +#5793 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5794 = ORIENTED_EDGE('',*,*,#5795,.F.); +#5795 = EDGE_CURVE('',#4417,#5771,#5796,.T.); +#5796 = SURFACE_CURVE('',#5797,(#5800,#5807),.PCURVE_S1.); +#5797 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5798,#5799),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5798 = CARTESIAN_POINT('',(137.5,62.00961894,20.)); +#5799 = CARTESIAN_POINT('',(137.5,62.00961894,0.E+000)); +#5800 = PCURVE('',#4474,#5801); +#5801 = DEFINITIONAL_REPRESENTATION('',(#5802),#5806); +#5802 = LINE('',#5803,#5804); +#5803 = CARTESIAN_POINT('',(0.E+000,30.)); +#5804 = VECTOR('',#5805,1.); +#5805 = DIRECTION('',(1.,0.E+000)); +#5806 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5807 = PCURVE('',#4590,#5808); +#5808 = DEFINITIONAL_REPRESENTATION('',(#5809),#5813); +#5809 = LINE('',#5810,#5811); +#5810 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5811 = VECTOR('',#5812,1.); +#5812 = DIRECTION('',(1.,0.E+000)); +#5813 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5814 = ORIENTED_EDGE('',*,*,#4414,.F.); +#5815 = ORIENTED_EDGE('',*,*,#5816,.T.); +#5816 = EDGE_CURVE('',#4415,#5769,#5817,.T.); +#5817 = SURFACE_CURVE('',#5818,(#5821,#5828),.PCURVE_S1.); +#5818 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5819,#5820),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5819 = CARTESIAN_POINT('',(127.5,62.00961894,20.)); +#5820 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#5821 = PCURVE('',#4474,#5822); +#5822 = DEFINITIONAL_REPRESENTATION('',(#5823),#5827); +#5823 = LINE('',#5824,#5825); +#5824 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5825 = VECTOR('',#5826,1.); +#5826 = DIRECTION('',(1.,0.E+000)); +#5827 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5828 = PCURVE('',#4590,#5829); +#5829 = DEFINITIONAL_REPRESENTATION('',(#5830),#5834); +#5830 = LINE('',#5831,#5832); +#5831 = CARTESIAN_POINT('',(0.E+000,30.)); +#5832 = VECTOR('',#5833,1.); +#5833 = DIRECTION('',(1.,0.E+000)); +#5834 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5835 = ADVANCED_FACE('',(#5836),#4590,.T.); +#5836 = FACE_BOUND('',#5837,.T.); +#5837 = EDGE_LOOP('',(#5838,#5861,#5862,#5863)); +#5838 = ORIENTED_EDGE('',*,*,#5839,.T.); +#5839 = EDGE_CURVE('',#5771,#5769,#5840,.T.); +#5840 = SURFACE_CURVE('',#5841,(#5846,#5853),.PCURVE_S1.); +#5841 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5842,#5843,#5844,#5845), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5842 = CARTESIAN_POINT('',(137.5,62.00961894,0.E+000)); +#5843 = CARTESIAN_POINT('',(137.5,52.00961894,0.E+000)); +#5844 = CARTESIAN_POINT('',(127.5,52.00961894,0.E+000)); +#5845 = CARTESIAN_POINT('',(127.5,62.00961894,0.E+000)); +#5846 = PCURVE('',#4590,#5847); +#5847 = DEFINITIONAL_REPRESENTATION('',(#5848),#5852); +#5848 = LINE('',#5849,#5850); +#5849 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5850 = VECTOR('',#5851,1.); +#5851 = DIRECTION('',(0.E+000,1.)); +#5852 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5853 = PCURVE('',#5428,#5854); +#5854 = DEFINITIONAL_REPRESENTATION('',(#5855),#5860); +#5855 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5856,#5857,#5858,#5859), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5856 = CARTESIAN_POINT('',(-47.5,-12.99038106)); +#5857 = CARTESIAN_POINT('',(-47.5,-22.99038106)); +#5858 = CARTESIAN_POINT('',(-37.5,-22.99038106)); +#5859 = CARTESIAN_POINT('',(-37.5,-12.99038106)); +#5860 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5861 = ORIENTED_EDGE('',*,*,#5816,.F.); +#5862 = ORIENTED_EDGE('',*,*,#4534,.F.); +#5863 = ORIENTED_EDGE('',*,*,#5795,.T.); +#5864 = ADVANCED_FACE('',(#5865),#4712,.T.); +#5865 = FACE_BOUND('',#5866,.T.); +#5866 = EDGE_LOOP('',(#5867,#5894,#5914,#5915)); +#5867 = ORIENTED_EDGE('',*,*,#5868,.T.); +#5868 = EDGE_CURVE('',#5869,#5871,#5873,.T.); +#5869 = VERTEX_POINT('',#5870); +#5870 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#5871 = VERTEX_POINT('',#5872); +#5872 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#5873 = SURFACE_CURVE('',#5874,(#5879,#5886),.PCURVE_S1.); +#5874 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5875,#5876,#5877,#5878), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5875 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#5876 = CARTESIAN_POINT('',(127.5,97.99038106,0.E+000)); +#5877 = CARTESIAN_POINT('',(137.5,97.99038106,0.E+000)); +#5878 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#5879 = PCURVE('',#4712,#5880); +#5880 = DEFINITIONAL_REPRESENTATION('',(#5881),#5885); +#5881 = LINE('',#5882,#5883); +#5882 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5883 = VECTOR('',#5884,1.); +#5884 = DIRECTION('',(0.E+000,1.)); +#5885 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5886 = PCURVE('',#5428,#5887); +#5887 = DEFINITIONAL_REPRESENTATION('',(#5888),#5893); +#5888 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5889,#5890,#5891,#5892), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5889 = CARTESIAN_POINT('',(-37.5,12.99038106)); +#5890 = CARTESIAN_POINT('',(-37.5,22.99038106)); +#5891 = CARTESIAN_POINT('',(-47.5,22.99038106)); +#5892 = CARTESIAN_POINT('',(-47.5,12.99038106)); +#5893 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5894 = ORIENTED_EDGE('',*,*,#5895,.F.); +#5895 = EDGE_CURVE('',#4655,#5871,#5896,.T.); +#5896 = SURFACE_CURVE('',#5897,(#5900,#5907),.PCURVE_S1.); +#5897 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5898,#5899),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5898 = CARTESIAN_POINT('',(137.5,87.99038106,20.)); +#5899 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#5900 = PCURVE('',#4712,#5901); +#5901 = DEFINITIONAL_REPRESENTATION('',(#5902),#5906); +#5902 = LINE('',#5903,#5904); +#5903 = CARTESIAN_POINT('',(0.E+000,30.)); +#5904 = VECTOR('',#5905,1.); +#5905 = DIRECTION('',(1.,0.E+000)); +#5906 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5907 = PCURVE('',#4828,#5908); +#5908 = DEFINITIONAL_REPRESENTATION('',(#5909),#5913); +#5909 = LINE('',#5910,#5911); +#5910 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5911 = VECTOR('',#5912,1.); +#5912 = DIRECTION('',(1.,0.E+000)); +#5913 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5914 = ORIENTED_EDGE('',*,*,#4652,.F.); +#5915 = ORIENTED_EDGE('',*,*,#5916,.T.); +#5916 = EDGE_CURVE('',#4653,#5869,#5917,.T.); +#5917 = SURFACE_CURVE('',#5918,(#5921,#5928),.PCURVE_S1.); +#5918 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5919,#5920),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5919 = CARTESIAN_POINT('',(127.5,87.99038106,20.)); +#5920 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#5921 = PCURVE('',#4712,#5922); +#5922 = DEFINITIONAL_REPRESENTATION('',(#5923),#5927); +#5923 = LINE('',#5924,#5925); +#5924 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#5925 = VECTOR('',#5926,1.); +#5926 = DIRECTION('',(1.,0.E+000)); +#5927 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5928 = PCURVE('',#4828,#5929); +#5929 = DEFINITIONAL_REPRESENTATION('',(#5930),#5934); +#5930 = LINE('',#5931,#5932); +#5931 = CARTESIAN_POINT('',(0.E+000,30.)); +#5932 = VECTOR('',#5933,1.); +#5933 = DIRECTION('',(1.,0.E+000)); +#5934 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5935 = ADVANCED_FACE('',(#5936),#4828,.T.); +#5936 = FACE_BOUND('',#5937,.T.); +#5937 = EDGE_LOOP('',(#5938,#5961,#5962,#5963)); +#5938 = ORIENTED_EDGE('',*,*,#5939,.T.); +#5939 = EDGE_CURVE('',#5871,#5869,#5940,.T.); +#5940 = SURFACE_CURVE('',#5941,(#5946,#5953),.PCURVE_S1.); +#5941 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5942,#5943,#5944,#5945), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5942 = CARTESIAN_POINT('',(137.5,87.99038106,0.E+000)); +#5943 = CARTESIAN_POINT('',(137.5,77.99038106,0.E+000)); +#5944 = CARTESIAN_POINT('',(127.5,77.99038106,0.E+000)); +#5945 = CARTESIAN_POINT('',(127.5,87.99038106,0.E+000)); +#5946 = PCURVE('',#4828,#5947); +#5947 = DEFINITIONAL_REPRESENTATION('',(#5948),#5952); +#5948 = LINE('',#5949,#5950); +#5949 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5950 = VECTOR('',#5951,1.); +#5951 = DIRECTION('',(0.E+000,1.)); +#5952 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5953 = PCURVE('',#5428,#5954); +#5954 = DEFINITIONAL_REPRESENTATION('',(#5955),#5960); +#5955 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5956,#5957,#5958,#5959), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5956 = CARTESIAN_POINT('',(-47.5,12.99038106)); +#5957 = CARTESIAN_POINT('',(-47.5,2.99038106)); +#5958 = CARTESIAN_POINT('',(-37.5,2.99038106)); +#5959 = CARTESIAN_POINT('',(-37.5,12.99038106)); +#5960 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5961 = ORIENTED_EDGE('',*,*,#5916,.F.); +#5962 = ORIENTED_EDGE('',*,*,#4772,.F.); +#5963 = ORIENTED_EDGE('',*,*,#5895,.T.); +#5964 = ADVANCED_FACE('',(#5965),#4950,.T.); +#5965 = FACE_BOUND('',#5966,.T.); +#5966 = EDGE_LOOP('',(#5967,#5994,#6014,#6015)); +#5967 = ORIENTED_EDGE('',*,*,#5968,.T.); +#5968 = EDGE_CURVE('',#5969,#5971,#5973,.T.); +#5969 = VERTEX_POINT('',#5970); +#5970 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#5971 = VERTEX_POINT('',#5972); +#5972 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#5973 = SURFACE_CURVE('',#5974,(#5979,#5986),.PCURVE_S1.); +#5974 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5975,#5976,#5977,#5978), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5975 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#5976 = CARTESIAN_POINT('',(20.,85.,0.E+000)); +#5977 = CARTESIAN_POINT('',(30.,85.,0.E+000)); +#5978 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#5979 = PCURVE('',#4950,#5980); +#5980 = DEFINITIONAL_REPRESENTATION('',(#5981),#5985); +#5981 = LINE('',#5982,#5983); +#5982 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#5983 = VECTOR('',#5984,1.); +#5984 = DIRECTION('',(0.E+000,1.)); +#5985 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5986 = PCURVE('',#5428,#5987); +#5987 = DEFINITIONAL_REPRESENTATION('',(#5988),#5993); +#5988 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#5989,#5990,#5991,#5992), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#5989 = CARTESIAN_POINT('',(70.,0.E+000)); +#5990 = CARTESIAN_POINT('',(70.,10.)); +#5991 = CARTESIAN_POINT('',(60.,10.)); +#5992 = CARTESIAN_POINT('',(60.,0.E+000)); +#5993 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#5994 = ORIENTED_EDGE('',*,*,#5995,.F.); +#5995 = EDGE_CURVE('',#4893,#5971,#5996,.T.); +#5996 = SURFACE_CURVE('',#5997,(#6000,#6007),.PCURVE_S1.); +#5997 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#5998,#5999),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#5998 = CARTESIAN_POINT('',(30.,75.,20.)); +#5999 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#6000 = PCURVE('',#4950,#6001); +#6001 = DEFINITIONAL_REPRESENTATION('',(#6002),#6006); +#6002 = LINE('',#6003,#6004); +#6003 = CARTESIAN_POINT('',(0.E+000,30.)); +#6004 = VECTOR('',#6005,1.); +#6005 = DIRECTION('',(1.,0.E+000)); +#6006 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6007 = PCURVE('',#5066,#6008); +#6008 = DEFINITIONAL_REPRESENTATION('',(#6009),#6013); +#6009 = LINE('',#6010,#6011); +#6010 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#6011 = VECTOR('',#6012,1.); +#6012 = DIRECTION('',(1.,0.E+000)); +#6013 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6014 = ORIENTED_EDGE('',*,*,#4890,.F.); +#6015 = ORIENTED_EDGE('',*,*,#6016,.T.); +#6016 = EDGE_CURVE('',#4891,#5969,#6017,.T.); +#6017 = SURFACE_CURVE('',#6018,(#6021,#6028),.PCURVE_S1.); +#6018 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6019,#6020),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#6019 = CARTESIAN_POINT('',(20.,75.,20.)); +#6020 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#6021 = PCURVE('',#4950,#6022); +#6022 = DEFINITIONAL_REPRESENTATION('',(#6023),#6027); +#6023 = LINE('',#6024,#6025); +#6024 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#6025 = VECTOR('',#6026,1.); +#6026 = DIRECTION('',(1.,0.E+000)); +#6027 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6028 = PCURVE('',#5066,#6029); +#6029 = DEFINITIONAL_REPRESENTATION('',(#6030),#6034); +#6030 = LINE('',#6031,#6032); +#6031 = CARTESIAN_POINT('',(0.E+000,30.)); +#6032 = VECTOR('',#6033,1.); +#6033 = DIRECTION('',(1.,0.E+000)); +#6034 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6035 = ADVANCED_FACE('',(#6036),#5066,.T.); +#6036 = FACE_BOUND('',#6037,.T.); +#6037 = EDGE_LOOP('',(#6038,#6061,#6062,#6063)); +#6038 = ORIENTED_EDGE('',*,*,#6039,.T.); +#6039 = EDGE_CURVE('',#5971,#5969,#6040,.T.); +#6040 = SURFACE_CURVE('',#6041,(#6046,#6053),.PCURVE_S1.); +#6041 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6042,#6043,#6044,#6045), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6042 = CARTESIAN_POINT('',(30.,75.,0.E+000)); +#6043 = CARTESIAN_POINT('',(30.,65.,0.E+000)); +#6044 = CARTESIAN_POINT('',(20.,65.,0.E+000)); +#6045 = CARTESIAN_POINT('',(20.,75.,0.E+000)); +#6046 = PCURVE('',#5066,#6047); +#6047 = DEFINITIONAL_REPRESENTATION('',(#6048),#6052); +#6048 = LINE('',#6049,#6050); +#6049 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#6050 = VECTOR('',#6051,1.); +#6051 = DIRECTION('',(0.E+000,1.)); +#6052 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6053 = PCURVE('',#5428,#6054); +#6054 = DEFINITIONAL_REPRESENTATION('',(#6055),#6060); +#6055 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6056,#6057,#6058,#6059), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6056 = CARTESIAN_POINT('',(60.,0.E+000)); +#6057 = CARTESIAN_POINT('',(60.,-10.)); +#6058 = CARTESIAN_POINT('',(70.,-10.)); +#6059 = CARTESIAN_POINT('',(70.,0.E+000)); +#6060 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6061 = ORIENTED_EDGE('',*,*,#6016,.F.); +#6062 = ORIENTED_EDGE('',*,*,#5010,.F.); +#6063 = ORIENTED_EDGE('',*,*,#5995,.T.); +#6064 = ADVANCED_FACE('',(#6065),#5188,.T.); +#6065 = FACE_BOUND('',#6066,.T.); +#6066 = EDGE_LOOP('',(#6067,#6094,#6114,#6115)); +#6067 = ORIENTED_EDGE('',*,*,#6068,.T.); +#6068 = EDGE_CURVE('',#6069,#6071,#6073,.T.); +#6069 = VERTEX_POINT('',#6070); +#6070 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#6071 = VERTEX_POINT('',#6072); +#6072 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#6073 = SURFACE_CURVE('',#6074,(#6079,#6086),.PCURVE_S1.); +#6074 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6075,#6076,#6077,#6078), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6075 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#6076 = CARTESIAN_POINT('',(150.,85.,0.E+000)); +#6077 = CARTESIAN_POINT('',(160.,85.,0.E+000)); +#6078 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#6079 = PCURVE('',#5188,#6080); +#6080 = DEFINITIONAL_REPRESENTATION('',(#6081),#6085); +#6081 = LINE('',#6082,#6083); +#6082 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#6083 = VECTOR('',#6084,1.); +#6084 = DIRECTION('',(0.E+000,1.)); +#6085 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6086 = PCURVE('',#5428,#6087); +#6087 = DEFINITIONAL_REPRESENTATION('',(#6088),#6093); +#6088 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6089,#6090,#6091,#6092), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6089 = CARTESIAN_POINT('',(-60.,0.E+000)); +#6090 = CARTESIAN_POINT('',(-60.,10.)); +#6091 = CARTESIAN_POINT('',(-70.,10.)); +#6092 = CARTESIAN_POINT('',(-70.,0.E+000)); +#6093 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6094 = ORIENTED_EDGE('',*,*,#6095,.F.); +#6095 = EDGE_CURVE('',#5131,#6071,#6096,.T.); +#6096 = SURFACE_CURVE('',#6097,(#6100,#6107),.PCURVE_S1.); +#6097 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6098,#6099),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#6098 = CARTESIAN_POINT('',(160.,75.,20.)); +#6099 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#6100 = PCURVE('',#5188,#6101); +#6101 = DEFINITIONAL_REPRESENTATION('',(#6102),#6106); +#6102 = LINE('',#6103,#6104); +#6103 = CARTESIAN_POINT('',(0.E+000,30.)); +#6104 = VECTOR('',#6105,1.); +#6105 = DIRECTION('',(1.,0.E+000)); +#6106 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6107 = PCURVE('',#5304,#6108); +#6108 = DEFINITIONAL_REPRESENTATION('',(#6109),#6113); +#6109 = LINE('',#6110,#6111); +#6110 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#6111 = VECTOR('',#6112,1.); +#6112 = DIRECTION('',(1.,0.E+000)); +#6113 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6114 = ORIENTED_EDGE('',*,*,#5128,.F.); +#6115 = ORIENTED_EDGE('',*,*,#6116,.T.); +#6116 = EDGE_CURVE('',#5129,#6069,#6117,.T.); +#6117 = SURFACE_CURVE('',#6118,(#6121,#6128),.PCURVE_S1.); +#6118 = B_SPLINE_CURVE_WITH_KNOTS('',1,(#6119,#6120),.UNSPECIFIED.,.F., + .F.,(2,2),(9.9800399E-004,20.000998004),.PIECEWISE_BEZIER_KNOTS.); +#6119 = CARTESIAN_POINT('',(150.,75.,20.)); +#6120 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#6121 = PCURVE('',#5188,#6122); +#6122 = DEFINITIONAL_REPRESENTATION('',(#6123),#6127); +#6123 = LINE('',#6124,#6125); +#6124 = CARTESIAN_POINT('',(0.E+000,0.E+000)); +#6125 = VECTOR('',#6126,1.); +#6126 = DIRECTION('',(1.,0.E+000)); +#6127 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6128 = PCURVE('',#5304,#6129); +#6129 = DEFINITIONAL_REPRESENTATION('',(#6130),#6134); +#6130 = LINE('',#6131,#6132); +#6131 = CARTESIAN_POINT('',(0.E+000,30.)); +#6132 = VECTOR('',#6133,1.); +#6133 = DIRECTION('',(1.,0.E+000)); +#6134 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6135 = ADVANCED_FACE('',(#6136),#5304,.T.); +#6136 = FACE_BOUND('',#6137,.T.); +#6137 = EDGE_LOOP('',(#6138,#6161,#6162,#6163)); +#6138 = ORIENTED_EDGE('',*,*,#6139,.T.); +#6139 = EDGE_CURVE('',#6071,#6069,#6140,.T.); +#6140 = SURFACE_CURVE('',#6141,(#6146,#6153),.PCURVE_S1.); +#6141 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6142,#6143,#6144,#6145), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6142 = CARTESIAN_POINT('',(160.,75.,0.E+000)); +#6143 = CARTESIAN_POINT('',(160.,65.,0.E+000)); +#6144 = CARTESIAN_POINT('',(150.,65.,0.E+000)); +#6145 = CARTESIAN_POINT('',(150.,75.,0.E+000)); +#6146 = PCURVE('',#5304,#6147); +#6147 = DEFINITIONAL_REPRESENTATION('',(#6148),#6152); +#6148 = LINE('',#6149,#6150); +#6149 = CARTESIAN_POINT('',(20.000998004,0.E+000)); +#6150 = VECTOR('',#6151,1.); +#6151 = DIRECTION('',(0.E+000,1.)); +#6152 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6153 = PCURVE('',#5428,#6154); +#6154 = DEFINITIONAL_REPRESENTATION('',(#6155),#6160); +#6155 = ( BOUNDED_CURVE() B_SPLINE_CURVE(3,(#6156,#6157,#6158,#6159), +.UNSPECIFIED.,.F.,.F.) B_SPLINE_CURVE_WITH_KNOTS((4,4),(0.E+000,30.), +.PIECEWISE_BEZIER_KNOTS.) CURVE() GEOMETRIC_REPRESENTATION_ITEM() +RATIONAL_B_SPLINE_CURVE((1.,0.33333333333,0.33333333333,1.)) +REPRESENTATION_ITEM('') ); +#6156 = CARTESIAN_POINT('',(-70.,0.E+000)); +#6157 = CARTESIAN_POINT('',(-70.,-10.)); +#6158 = CARTESIAN_POINT('',(-60.,-10.)); +#6159 = CARTESIAN_POINT('',(-60.,0.E+000)); +#6160 = ( GEOMETRIC_REPRESENTATION_CONTEXT(2) +PARAMETRIC_REPRESENTATION_CONTEXT() REPRESENTATION_CONTEXT('2D SPACE','' + ) ); +#6161 = ORIENTED_EDGE('',*,*,#6116,.F.); +#6162 = ORIENTED_EDGE('',*,*,#5248,.F.); +#6163 = ORIENTED_EDGE('',*,*,#6095,.T.); +#6164 = ADVANCED_FACE('',(#6165,#6171,#6175,#6179,#6183,#6187,#6191), + #5428,.T.); +#6165 = FACE_BOUND('',#6166,.T.); +#6166 = EDGE_LOOP('',(#6167,#6168,#6169,#6170)); +#6167 = ORIENTED_EDGE('',*,*,#5443,.F.); +#6168 = ORIENTED_EDGE('',*,*,#5414,.F.); +#6169 = ORIENTED_EDGE('',*,*,#5544,.F.); +#6170 = ORIENTED_EDGE('',*,*,#5517,.F.); +#6171 = FACE_BOUND('',#6172,.T.); +#6172 = EDGE_LOOP('',(#6173,#6174)); +#6173 = ORIENTED_EDGE('',*,*,#5639,.F.); +#6174 = ORIENTED_EDGE('',*,*,#5568,.F.); +#6175 = FACE_BOUND('',#6176,.T.); +#6176 = EDGE_LOOP('',(#6177,#6178)); +#6177 = ORIENTED_EDGE('',*,*,#5739,.F.); +#6178 = ORIENTED_EDGE('',*,*,#5668,.F.); +#6179 = FACE_BOUND('',#6180,.T.); +#6180 = EDGE_LOOP('',(#6181,#6182)); +#6181 = ORIENTED_EDGE('',*,*,#5839,.F.); +#6182 = ORIENTED_EDGE('',*,*,#5768,.F.); +#6183 = FACE_BOUND('',#6184,.T.); +#6184 = EDGE_LOOP('',(#6185,#6186)); +#6185 = ORIENTED_EDGE('',*,*,#5939,.F.); +#6186 = ORIENTED_EDGE('',*,*,#5868,.F.); +#6187 = FACE_BOUND('',#6188,.T.); +#6188 = EDGE_LOOP('',(#6189,#6190)); +#6189 = ORIENTED_EDGE('',*,*,#6039,.F.); +#6190 = ORIENTED_EDGE('',*,*,#5968,.F.); +#6191 = FACE_BOUND('',#6192,.T.); +#6192 = EDGE_LOOP('',(#6193,#6194)); +#6193 = ORIENTED_EDGE('',*,*,#6139,.F.); +#6194 = ORIENTED_EDGE('',*,*,#6068,.F.); +#6195 = ( GEOMETRIC_REPRESENTATION_CONTEXT(3) +GLOBAL_UNCERTAINTY_ASSIGNED_CONTEXT((#6199)) +GLOBAL_UNIT_ASSIGNED_CONTEXT((#6196,#6197,#6198)) REPRESENTATION_CONTEXT +('Context #1','3D Context with UNIT and UNCERTAINTY') ); +#6196 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6197 = ( NAMED_UNIT(*) PLANE_ANGLE_UNIT() SI_UNIT($,.RADIAN.) ); +#6198 = ( NAMED_UNIT(*) SI_UNIT($,.STERADIAN.) SOLID_ANGLE_UNIT() ); +#6199 = UNCERTAINTY_MEASURE_WITH_UNIT(LENGTH_MEASURE(1.E-005),#6196, + 'distance_accuracy_value','confusion accuracy'); +#6200 = SHAPE_DEFINITION_REPRESENTATION(#6201,#3812); +#6201 = PRODUCT_DEFINITION_SHAPE('','',#6202); +#6202 = PRODUCT_DEFINITION('design','',#6203,#6206); +#6203 = PRODUCT_DEFINITION_FORMATION('','',#6204); +#6204 = PRODUCT('plate','plate','',(#6205)); +#6205 = MECHANICAL_CONTEXT('',#2,'mechanical'); +#6206 = PRODUCT_DEFINITION_CONTEXT('part definition',#2,'design'); +#6207 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6208,#6210); +#6208 = ( REPRESENTATION_RELATIONSHIP('','',#3812,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6209) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#6209 = ITEM_DEFINED_TRANSFORMATION('','',#11,#23); +#6210 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #6211); +#6211 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('12','plate_1','',#5,#6202,$); +#6212 = PRODUCT_TYPE('part',$,(#6204)); +#6213 = CONTEXT_DEPENDENT_SHAPE_REPRESENTATION(#6214,#6216); +#6214 = ( REPRESENTATION_RELATIONSHIP('','',#1146,#10) +REPRESENTATION_RELATIONSHIP_WITH_TRANSFORMATION(#6215) +SHAPE_REPRESENTATION_RELATIONSHIP() ); +#6215 = ITEM_DEFINED_TRANSFORMATION('','',#11,#27); +#6216 = PRODUCT_DEFINITION_SHAPE('Placement','Placement of an item', + #6217); +#6217 = NEXT_ASSEMBLY_USAGE_OCCURRENCE('13','l-bracket-assembly_2','',#5 + ,#1141,$); +#6218 = PRESENTATION_LAYER_ASSIGNMENT('256','visible',(#63,#759,#1190, + #1934,#3813)); +#6219 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #6220),#1115); +#6220 = STYLED_ITEM('color',(#6221),#759); +#6221 = PRESENTATION_STYLE_ASSIGNMENT((#6222)); +#6222 = SURFACE_STYLE_USAGE(.BOTH.,#6223); +#6223 = SURFACE_SIDE_STYLE('',(#6224)); +#6224 = SURFACE_STYLE_FILL_AREA(#6225); +#6225 = FILL_AREA_STYLE('',(#6226)); +#6226 = FILL_AREA_STYLE_COLOUR('',#6227); +#6227 = COLOUR_RGB('',1.,0.5,0.E+000); +#6228 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #6229),#1894); +#6229 = STYLED_ITEM('color',(#6230),#1190); +#6230 = PRESENTATION_STYLE_ASSIGNMENT((#6231)); +#6231 = SURFACE_STYLE_USAGE(.BOTH.,#6232); +#6232 = SURFACE_SIDE_STYLE('',(#6233)); +#6233 = SURFACE_STYLE_FILL_AREA(#6234); +#6234 = FILL_AREA_STYLE('',(#6235)); +#6235 = FILL_AREA_STYLE_COLOUR('',#6236); +#6236 = DRAUGHTING_PRE_DEFINED_COLOUR('blue'); +#6237 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #6238),#6195); +#6238 = STYLED_ITEM('color',(#6239),#3813); +#6239 = PRESENTATION_STYLE_ASSIGNMENT((#6240)); +#6240 = SURFACE_STYLE_USAGE(.BOTH.,#6241); +#6241 = SURFACE_SIDE_STYLE('',(#6242)); +#6242 = SURFACE_STYLE_FILL_AREA(#6243); +#6243 = FILL_AREA_STYLE('',(#6244)); +#6244 = FILL_AREA_STYLE_COLOUR('',#6245); +#6245 = COLOUR_RGB('',0.800000011921,1.,0.E+000); +#6246 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #6247),#735); +#6247 = STYLED_ITEM('color',(#6248),#63); +#6248 = PRESENTATION_STYLE_ASSIGNMENT((#6249)); +#6249 = SURFACE_STYLE_USAGE(.BOTH.,#6250); +#6250 = SURFACE_SIDE_STYLE('',(#6251)); +#6251 = SURFACE_STYLE_FILL_AREA(#6252); +#6252 = FILL_AREA_STYLE('',(#6253)); +#6253 = FILL_AREA_STYLE_COLOUR('',#6254); +#6254 = DRAUGHTING_PRE_DEFINED_COLOUR('red'); +#6255 = MECHANICAL_DESIGN_GEOMETRIC_PRESENTATION_REPRESENTATION('',( + #6256),#3788); +#6256 = STYLED_ITEM('color',(#6257),#1934); +#6257 = PRESENTATION_STYLE_ASSIGNMENT((#6258)); +#6258 = SURFACE_STYLE_USAGE(.BOTH.,#6259); +#6259 = SURFACE_SIDE_STYLE('',(#6260)); +#6260 = SURFACE_STYLE_FILL_AREA(#6261); +#6261 = FILL_AREA_STYLE('',(#6262)); +#6262 = FILL_AREA_STYLE_COLOUR('',#6263); +#6263 = DRAUGHTING_PRE_DEFINED_COLOUR('green'); +#6264 = SHAPE_DEFINITION_REPRESENTATION(#6265,#6267); +#6265 = PROPERTY_DEFINITION('shape with specific properties', + 'properties for subshape',#6266); +#6266 = SHAPE_ASPECT('','',#741,.F.); +#6267 = SHAPE_REPRESENTATION('',(#63),#735); +#6268 = PROPERTY_DEFINITION_REPRESENTATION(#6269,#6270); +#6269 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#6266); +#6270 = REPRESENTATION('surface area',(#6271),#735); +#6271 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 748.23793178072),#6272); +#6272 = DERIVED_UNIT((#6273)); +#6273 = DERIVED_UNIT_ELEMENT(#6274,2.); +#6274 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6275 = PROPERTY_DEFINITION_REPRESENTATION(#6276,#6277); +#6276 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #6266); +#6277 = REPRESENTATION('volume',(#6278),#735); +#6278 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 664.86671363901),#6279); +#6279 = DERIVED_UNIT((#6280)); +#6280 = DERIVED_UNIT_ELEMENT(#6281,3.); +#6281 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6282 = PROPERTY_DEFINITION_REPRESENTATION(#6283,#6284); +#6283 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #6266); +#6284 = REPRESENTATION('centroid',(#6285),#735); +#6285 = CARTESIAN_POINT('centre point',(9.999999999999,7.5, + 1.499113884522)); +#6286 = SHAPE_DEFINITION_REPRESENTATION(#6287,#6289); +#6287 = PROPERTY_DEFINITION('shape with specific properties', + 'properties for subshape',#6288); +#6288 = SHAPE_ASPECT('','',#1121,.F.); +#6289 = SHAPE_REPRESENTATION('',(#759),#1115); +#6290 = PROPERTY_DEFINITION_REPRESENTATION(#6291,#6292); +#6291 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#6288); +#6292 = REPRESENTATION('surface area',(#6293),#1115); +#6293 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 6.440717023158E+003),#6294); +#6294 = DERIVED_UNIT((#6295)); +#6295 = DERIVED_UNIT_ELEMENT(#6296,2.); +#6296 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6297 = PROPERTY_DEFINITION_REPRESENTATION(#6298,#6299); +#6298 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #6288); +#6299 = REPRESENTATION('volume',(#6300),#1115); +#6300 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 1.567555242406E+004),#6301); +#6301 = DERIVED_UNIT((#6302)); +#6302 = DERIVED_UNIT_ELEMENT(#6303,3.); +#6303 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6304 = PROPERTY_DEFINITION_REPRESENTATION(#6305,#6306); +#6305 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #6288); +#6306 = REPRESENTATION('centroid',(#6307),#1115); +#6307 = CARTESIAN_POINT('centre point',(-2.719684958609E-018, + -1.305448780132E-016,100.16703963797)); +#6308 = PROPERTY_DEFINITION_REPRESENTATION(#6309,#6310); +#6309 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#38); +#6310 = REPRESENTATION('surface area',(#6311),#57); +#6311 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 7.93719288672E+003),#6312); +#6312 = DERIVED_UNIT((#6313)); +#6313 = DERIVED_UNIT_ELEMENT(#6314,2.); +#6314 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6315 = PROPERTY_DEFINITION_REPRESENTATION(#6316,#6317); +#6316 = PROPERTY_DEFINITION('geometric_validation_property','volume',#38 + ); +#6317 = REPRESENTATION('volume',(#6318),#57); +#6318 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 1.700528585134E+004),#6319); +#6319 = DERIVED_UNIT((#6320)); +#6320 = DERIVED_UNIT_ELEMENT(#6321,3.); +#6321 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6322 = PROPERTY_DEFINITION_REPRESENTATION(#6323,#6324); +#6323 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #38); +#6324 = REPRESENTATION('centroid',(#6325),#57); +#6325 = CARTESIAN_POINT('centre point',(-5.564956655149E-014, + -6.43133781133E-015,100.15390863331)); +#6326 = SHAPE_DEFINITION_REPRESENTATION(#6327,#6329); +#6327 = PROPERTY_DEFINITION('shape with specific properties', + 'properties for subshape',#6328); +#6328 = SHAPE_ASPECT('','',#1900,.F.); +#6329 = SHAPE_REPRESENTATION('',(#1190),#1894); +#6330 = PROPERTY_DEFINITION_REPRESENTATION(#6331,#6332); +#6331 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#6328); +#6332 = REPRESENTATION('surface area',(#6333),#1894); +#6333 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 1.559572215389E+003),#6334); +#6334 = DERIVED_UNIT((#6335)); +#6335 = DERIVED_UNIT_ELEMENT(#6336,2.); +#6336 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6337 = PROPERTY_DEFINITION_REPRESENTATION(#6338,#6339); +#6338 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #6328); +#6339 = REPRESENTATION('volume',(#6340),#1894); +#6340 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 3.182973610564E+003),#6341); +#6341 = DERIVED_UNIT((#6342)); +#6342 = DERIVED_UNIT_ELEMENT(#6343,3.); +#6343 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6344 = PROPERTY_DEFINITION_REPRESENTATION(#6345,#6346); +#6345 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #6328); +#6346 = REPRESENTATION('centroid',(#6347),#1894); +#6347 = CARTESIAN_POINT('centre point',(-2.957828873471E-017, + -2.402942149057E-016,16.934159973894)); +#6348 = PROPERTY_DEFINITION_REPRESENTATION(#6349,#6350); +#6349 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#1169); +#6350 = REPRESENTATION('surface area',(#6351),#1184); +#6351 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 2.30781014717E+003),#6352); +#6352 = DERIVED_UNIT((#6353)); +#6353 = DERIVED_UNIT_ELEMENT(#6354,2.); +#6354 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6355 = PROPERTY_DEFINITION_REPRESENTATION(#6356,#6357); +#6356 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #1169); +#6357 = REPRESENTATION('volume',(#6358),#1184); +#6358 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 3.847840324203E+003),#6359); +#6359 = DERIVED_UNIT((#6360)); +#6360 = DERIVED_UNIT_ELEMENT(#6361,3.); +#6361 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6362 = PROPERTY_DEFINITION_REPRESENTATION(#6363,#6364); +#6363 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #1169); +#6364 = REPRESENTATION('centroid',(#6365),#1184); +#6365 = CARTESIAN_POINT('centre point',(-7.5,-10.,-6.969200983347)); +#6366 = SHAPE_DEFINITION_REPRESENTATION(#6367,#6369); +#6367 = PROPERTY_DEFINITION('shape with specific properties', + 'properties for subshape',#6368); +#6368 = SHAPE_ASPECT('','',#3794,.F.); +#6369 = SHAPE_REPRESENTATION('',(#1934),#3788); +#6370 = PROPERTY_DEFINITION_REPRESENTATION(#6371,#6372); +#6371 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#6368); +#6372 = REPRESENTATION('surface area',(#6373),#3788); +#6373 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 2.463250770748E+004),#6374); +#6374 = DERIVED_UNIT((#6375)); +#6375 = DERIVED_UNIT_ELEMENT(#6376,2.); +#6376 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6377 = PROPERTY_DEFINITION_REPRESENTATION(#6378,#6379); +#6378 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #6368); +#6379 = REPRESENTATION('volume',(#6380),#3788); +#6380 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 9.684518992424E+004),#6381); +#6381 = DERIVED_UNIT((#6382)); +#6382 = DERIVED_UNIT_ELEMENT(#6383,3.); +#6383 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6384 = PROPERTY_DEFINITION_REPRESENTATION(#6385,#6386); +#6385 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #6368); +#6386 = REPRESENTATION('centroid',(#6387),#3788); +#6387 = CARTESIAN_POINT('centre point',(14.59311007429,20.202683779389, + 50.)); +#6388 = PROPERTY_DEFINITION_REPRESENTATION(#6389,#6390); +#6389 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#1140); +#6390 = REPRESENTATION('surface area',(#6391),#1163); +#6391 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 3.155593814899E+004),#6392); +#6392 = DERIVED_UNIT((#6393)); +#6393 = DERIVED_UNIT_ELEMENT(#6394,2.); +#6394 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6395 = PROPERTY_DEFINITION_REPRESENTATION(#6396,#6397); +#6396 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #1140); +#6397 = REPRESENTATION('volume',(#6398),#1163); +#6398 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 1.083887108968E+005),#6399); +#6399 = DERIVED_UNIT((#6400)); +#6400 = DERIVED_UNIT_ELEMENT(#6401,3.); +#6401 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6402 = PROPERTY_DEFINITION_REPRESENTATION(#6403,#6404); +#6403 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #1140); +#6404 = REPRESENTATION('centroid',(#6405),#1163); +#6405 = CARTESIAN_POINT('centre point',(16.766467058555,-50., + 17.308847151676)); +#6406 = SHAPE_DEFINITION_REPRESENTATION(#6407,#6409); +#6407 = PROPERTY_DEFINITION('shape with specific properties', + 'properties for subshape',#6408); +#6408 = SHAPE_ASPECT('','',#6201,.F.); +#6409 = SHAPE_REPRESENTATION('',(#3813),#6195); +#6410 = PROPERTY_DEFINITION_REPRESENTATION(#6411,#6412); +#6411 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#6408); +#6412 = REPRESENTATION('surface area',(#6413),#6195); +#6413 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 7.003461677988E+004),#6414); +#6414 = DERIVED_UNIT((#6415)); +#6415 = DERIVED_UNIT_ELEMENT(#6416,2.); +#6416 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6417 = PROPERTY_DEFINITION_REPRESENTATION(#6418,#6419); +#6418 = PROPERTY_DEFINITION('geometric_validation_property','volume', + #6408); +#6419 = REPRESENTATION('volume',(#6420),#6195); +#6420 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 5.305946685456E+005),#6421); +#6421 = DERIVED_UNIT((#6422)); +#6422 = DERIVED_UNIT_ELEMENT(#6423,3.); +#6423 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6424 = PROPERTY_DEFINITION_REPRESENTATION(#6425,#6426); +#6425 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #6408); +#6426 = REPRESENTATION('centroid',(#6427),#6195); +#6427 = CARTESIAN_POINT('centre point',(90.000000000003,75., + 9.999703905212)); +#6428 = PROPERTY_DEFINITION_REPRESENTATION(#6429,#6430); +#6429 = PROPERTY_DEFINITION('geometric_validation_property', + 'surface area',#4); +#6430 = REPRESENTATION('surface area',(#6431),#31); +#6431 = MEASURE_REPRESENTATION_ITEM('surface area measure',AREA_MEASURE( + 1.410836859646E+005),#6432); +#6432 = DERIVED_UNIT((#6433)); +#6433 = DERIVED_UNIT_ELEMENT(#6434,2.); +#6434 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6435 = PROPERTY_DEFINITION_REPRESENTATION(#6436,#6437); +#6436 = PROPERTY_DEFINITION('geometric_validation_property','volume',#4 + ); +#6437 = REPRESENTATION('volume',(#6438),#31); +#6438 = MEASURE_REPRESENTATION_ITEM('volume measure',VOLUME_MEASURE( + 7.643773761907E+005),#6439); +#6439 = DERIVED_UNIT((#6440)); +#6440 = DERIVED_UNIT_ELEMENT(#6441,3.); +#6441 = ( LENGTH_UNIT() NAMED_UNIT(*) SI_UNIT(.MILLI.,.METRE.) ); +#6442 = PROPERTY_DEFINITION_REPRESENTATION(#6443,#6444); +#6443 = PROPERTY_DEFINITION('geometric_validation_property','centroid', + #4); +#6444 = REPRESENTATION('centroid',(#6445),#31); +#6445 = CARTESIAN_POINT('centre point',(90.003424042086,75., + 18.856945371263)); +ENDSEC; +END-ISO-10303-21; diff --git a/Detectors/CADSupport/examples/oTOF_MATERIALS.csv b/Detectors/CADSupport/examples/oTOF_MATERIALS.csv new file mode 100644 index 0000000000000..b767eb48a7a13 --- /dev/null +++ b/Detectors/CADSupport/examples/oTOF_MATERIALS.csv @@ -0,0 +1,211 @@ +#,"oTOF material assignment, derived from AliceO2" +#,"Detectors/Upgrades/ALICE3/IOTOF/simulation/src/{Detector,Layer}.cxx" +#,Every solid body of the AliceO2 model is silicon (sensor and chip are +#,"medSi); AIR$ appears only as the layer envelope, which has no CAD body." +#,columns: CAD | Mechanical/Part | part number | revision | name | mass | material +CAD,Mechanical/Part,Component1,,Component1,,Silicon +CAD,Mechanical/Part,Plate 1,,Plate 1,,Silicon +CAD,Mechanical/Part,Plate 2,,Plate 2,,Silicon +CAD,Mechanical/Part,Component1_1,,Component1_1,,Silicon +CAD,Mechanical/Part,Plate 1_1,,Plate 1_1,,Silicon +CAD,Mechanical/Part,Plate 2_1,,Plate 2_1,,Silicon +CAD,Mechanical/Part,Component1_2,,Component1_2,,Silicon +CAD,Mechanical/Part,Plate 1_2,,Plate 1_2,,Silicon +CAD,Mechanical/Part,Plate 2_2,,Plate 2_2,,Silicon +CAD,Mechanical/Part,Component1_3,,Component1_3,,Silicon +CAD,Mechanical/Part,Plate 1_3,,Plate 1_3,,Silicon +CAD,Mechanical/Part,Plate 2_3,,Plate 2_3,,Silicon +CAD,Mechanical/Part,Component1_4,,Component1_4,,Silicon +CAD,Mechanical/Part,Plate 1_4,,Plate 1_4,,Silicon +CAD,Mechanical/Part,Plate 2_4,,Plate 2_4,,Silicon +CAD,Mechanical/Part,Component1_5,,Component1_5,,Silicon +CAD,Mechanical/Part,Plate 1_5,,Plate 1_5,,Silicon +CAD,Mechanical/Part,Plate 2_5,,Plate 2_5,,Silicon +CAD,Mechanical/Part,Component1_6,,Component1_6,,Silicon +CAD,Mechanical/Part,Plate 1_6,,Plate 1_6,,Silicon +CAD,Mechanical/Part,Plate 2_6,,Plate 2_6,,Silicon +CAD,Mechanical/Part,Component1_7,,Component1_7,,Silicon +CAD,Mechanical/Part,Plate 1_7,,Plate 1_7,,Silicon +CAD,Mechanical/Part,Plate 2_7,,Plate 2_7,,Silicon +CAD,Mechanical/Part,Component1_8,,Component1_8,,Silicon +CAD,Mechanical/Part,Plate 1_8,,Plate 1_8,,Silicon +CAD,Mechanical/Part,Plate 2_8,,Plate 2_8,,Silicon +CAD,Mechanical/Part,Component1_9,,Component1_9,,Silicon +CAD,Mechanical/Part,Plate 1_9,,Plate 1_9,,Silicon +CAD,Mechanical/Part,Plate 2_9,,Plate 2_9,,Silicon +CAD,Mechanical/Part,Component1_10,,Component1_10,,Silicon +CAD,Mechanical/Part,Plate 1_10,,Plate 1_10,,Silicon +CAD,Mechanical/Part,Plate 2_10,,Plate 2_10,,Silicon +CAD,Mechanical/Part,Component1_11,,Component1_11,,Silicon +CAD,Mechanical/Part,Plate 1_11,,Plate 1_11,,Silicon +CAD,Mechanical/Part,Plate 2_11,,Plate 2_11,,Silicon +CAD,Mechanical/Part,Component1_12,,Component1_12,,Silicon +CAD,Mechanical/Part,Plate 1_12,,Plate 1_12,,Silicon +CAD,Mechanical/Part,Plate 2_12,,Plate 2_12,,Silicon +CAD,Mechanical/Part,Component1_13,,Component1_13,,Silicon +CAD,Mechanical/Part,Plate 1_13,,Plate 1_13,,Silicon +CAD,Mechanical/Part,Plate 2_13,,Plate 2_13,,Silicon +CAD,Mechanical/Part,Component1_14,,Component1_14,,Silicon +CAD,Mechanical/Part,Plate 1_14,,Plate 1_14,,Silicon +CAD,Mechanical/Part,Plate 2_14,,Plate 2_14,,Silicon +CAD,Mechanical/Part,Component1_15,,Component1_15,,Silicon +CAD,Mechanical/Part,Plate 1_15,,Plate 1_15,,Silicon +CAD,Mechanical/Part,Plate 2_15,,Plate 2_15,,Silicon +CAD,Mechanical/Part,Component1_16,,Component1_16,,Silicon +CAD,Mechanical/Part,Plate 1_16,,Plate 1_16,,Silicon +CAD,Mechanical/Part,Plate 2_16,,Plate 2_16,,Silicon +CAD,Mechanical/Part,Component1_17,,Component1_17,,Silicon +CAD,Mechanical/Part,Plate 1_17,,Plate 1_17,,Silicon +CAD,Mechanical/Part,Plate 2_17,,Plate 2_17,,Silicon +CAD,Mechanical/Part,Component1_18,,Component1_18,,Silicon +CAD,Mechanical/Part,Plate 1_18,,Plate 1_18,,Silicon +CAD,Mechanical/Part,Plate 2_18,,Plate 2_18,,Silicon +CAD,Mechanical/Part,Component1_19,,Component1_19,,Silicon +CAD,Mechanical/Part,Plate 1_19,,Plate 1_19,,Silicon +CAD,Mechanical/Part,Plate 2_19,,Plate 2_19,,Silicon +CAD,Mechanical/Part,Component1_20,,Component1_20,,Silicon +CAD,Mechanical/Part,Plate 1_20,,Plate 1_20,,Silicon +CAD,Mechanical/Part,Plate 2_20,,Plate 2_20,,Silicon +CAD,Mechanical/Part,Component1_21,,Component1_21,,Silicon +CAD,Mechanical/Part,Plate 1_21,,Plate 1_21,,Silicon +CAD,Mechanical/Part,Plate 2_21,,Plate 2_21,,Silicon +CAD,Mechanical/Part,Component1_22,,Component1_22,,Silicon +CAD,Mechanical/Part,Plate 1_22,,Plate 1_22,,Silicon +CAD,Mechanical/Part,Plate 2_22,,Plate 2_22,,Silicon +CAD,Mechanical/Part,Component1_23,,Component1_23,,Silicon +CAD,Mechanical/Part,Plate 1_23,,Plate 1_23,,Silicon +CAD,Mechanical/Part,Plate 2_23,,Plate 2_23,,Silicon +CAD,Mechanical/Part,Component1_24,,Component1_24,,Silicon +CAD,Mechanical/Part,Plate 1_24,,Plate 1_24,,Silicon +CAD,Mechanical/Part,Plate 2_24,,Plate 2_24,,Silicon +CAD,Mechanical/Part,Component1_25,,Component1_25,,Silicon +CAD,Mechanical/Part,Plate 1_25,,Plate 1_25,,Silicon +CAD,Mechanical/Part,Plate 2_25,,Plate 2_25,,Silicon +CAD,Mechanical/Part,Component1_26,,Component1_26,,Silicon +CAD,Mechanical/Part,Plate 1_26,,Plate 1_26,,Silicon +CAD,Mechanical/Part,Plate 2_26,,Plate 2_26,,Silicon +CAD,Mechanical/Part,Component1_27,,Component1_27,,Silicon +CAD,Mechanical/Part,Plate 1_27,,Plate 1_27,,Silicon +CAD,Mechanical/Part,Plate 2_27,,Plate 2_27,,Silicon +CAD,Mechanical/Part,Component1_28,,Component1_28,,Silicon +CAD,Mechanical/Part,Plate 1_28,,Plate 1_28,,Silicon +CAD,Mechanical/Part,Plate 2_28,,Plate 2_28,,Silicon +CAD,Mechanical/Part,Component1_29,,Component1_29,,Silicon +CAD,Mechanical/Part,Plate 1_29,,Plate 1_29,,Silicon +CAD,Mechanical/Part,Plate 2_29,,Plate 2_29,,Silicon +CAD,Mechanical/Part,Component1_30,,Component1_30,,Silicon +CAD,Mechanical/Part,Plate 1_30,,Plate 1_30,,Silicon +CAD,Mechanical/Part,Plate 2_30,,Plate 2_30,,Silicon +CAD,Mechanical/Part,Component1_31,,Component1_31,,Silicon +CAD,Mechanical/Part,Plate 1_31,,Plate 1_31,,Silicon +CAD,Mechanical/Part,Plate 2_31,,Plate 2_31,,Silicon +CAD,Mechanical/Part,Component1_32,,Component1_32,,Silicon +CAD,Mechanical/Part,Plate 1_32,,Plate 1_32,,Silicon +CAD,Mechanical/Part,Plate 2_32,,Plate 2_32,,Silicon +CAD,Mechanical/Part,Component1_33,,Component1_33,,Silicon +CAD,Mechanical/Part,Plate 1_33,,Plate 1_33,,Silicon +CAD,Mechanical/Part,Plate 2_33,,Plate 2_33,,Silicon +CAD,Mechanical/Part,Component1_34,,Component1_34,,Silicon +CAD,Mechanical/Part,Plate 1_34,,Plate 1_34,,Silicon +CAD,Mechanical/Part,Plate 2_34,,Plate 2_34,,Silicon +CAD,Mechanical/Part,Component1_35,,Component1_35,,Silicon +CAD,Mechanical/Part,Plate 1_35,,Plate 1_35,,Silicon +CAD,Mechanical/Part,Plate 2_35,,Plate 2_35,,Silicon +CAD,Mechanical/Part,Component1_36,,Component1_36,,Silicon +CAD,Mechanical/Part,Plate 1_36,,Plate 1_36,,Silicon +CAD,Mechanical/Part,Plate 2_36,,Plate 2_36,,Silicon +CAD,Mechanical/Part,Component1_37,,Component1_37,,Silicon +CAD,Mechanical/Part,Plate 1_37,,Plate 1_37,,Silicon +CAD,Mechanical/Part,Plate 2_37,,Plate 2_37,,Silicon +CAD,Mechanical/Part,Component1_38,,Component1_38,,Silicon +CAD,Mechanical/Part,Plate 1_38,,Plate 1_38,,Silicon +CAD,Mechanical/Part,Plate 2_38,,Plate 2_38,,Silicon +CAD,Mechanical/Part,Component1_39,,Component1_39,,Silicon +CAD,Mechanical/Part,Plate 1_39,,Plate 1_39,,Silicon +CAD,Mechanical/Part,Plate 2_39,,Plate 2_39,,Silicon +CAD,Mechanical/Part,Component1_40,,Component1_40,,Silicon +CAD,Mechanical/Part,Plate 1_40,,Plate 1_40,,Silicon +CAD,Mechanical/Part,Plate 2_40,,Plate 2_40,,Silicon +CAD,Mechanical/Part,Component1_41,,Component1_41,,Silicon +CAD,Mechanical/Part,Plate 1_41,,Plate 1_41,,Silicon +CAD,Mechanical/Part,Plate 2_41,,Plate 2_41,,Silicon +CAD,Mechanical/Part,Component1_42,,Component1_42,,Silicon +CAD,Mechanical/Part,Plate 1_42,,Plate 1_42,,Silicon +CAD,Mechanical/Part,Plate 2_42,,Plate 2_42,,Silicon +CAD,Mechanical/Part,Component1_43,,Component1_43,,Silicon +CAD,Mechanical/Part,Plate 1_43,,Plate 1_43,,Silicon +CAD,Mechanical/Part,Plate 2_43,,Plate 2_43,,Silicon +CAD,Mechanical/Part,Component1_44,,Component1_44,,Silicon +CAD,Mechanical/Part,Plate 1_44,,Plate 1_44,,Silicon +CAD,Mechanical/Part,Plate 2_44,,Plate 2_44,,Silicon +CAD,Mechanical/Part,Component1_45,,Component1_45,,Silicon +CAD,Mechanical/Part,Plate 1_45,,Plate 1_45,,Silicon +CAD,Mechanical/Part,Plate 2_45,,Plate 2_45,,Silicon +CAD,Mechanical/Part,Component1_46,,Component1_46,,Silicon +CAD,Mechanical/Part,Plate 1_46,,Plate 1_46,,Silicon +CAD,Mechanical/Part,Plate 2_46,,Plate 2_46,,Silicon +CAD,Mechanical/Part,Component1_47,,Component1_47,,Silicon +CAD,Mechanical/Part,Plate 1_47,,Plate 1_47,,Silicon +CAD,Mechanical/Part,Plate 2_47,,Plate 2_47,,Silicon +CAD,Mechanical/Part,Component1_48,,Component1_48,,Silicon +CAD,Mechanical/Part,Plate 1_48,,Plate 1_48,,Silicon +CAD,Mechanical/Part,Plate 2_48,,Plate 2_48,,Silicon +CAD,Mechanical/Part,Component1_49,,Component1_49,,Silicon +CAD,Mechanical/Part,Plate 1_49,,Plate 1_49,,Silicon +CAD,Mechanical/Part,Plate 2_49,,Plate 2_49,,Silicon +CAD,Mechanical/Part,Component1_50,,Component1_50,,Silicon +CAD,Mechanical/Part,Plate 1_50,,Plate 1_50,,Silicon +CAD,Mechanical/Part,Plate 2_50,,Plate 2_50,,Silicon +CAD,Mechanical/Part,Component1_51,,Component1_51,,Silicon +CAD,Mechanical/Part,Plate 1_51,,Plate 1_51,,Silicon +CAD,Mechanical/Part,Plate 2_51,,Plate 2_51,,Silicon +CAD,Mechanical/Part,Component1_52,,Component1_52,,Silicon +CAD,Mechanical/Part,Plate 1_52,,Plate 1_52,,Silicon +CAD,Mechanical/Part,Plate 2_52,,Plate 2_52,,Silicon +CAD,Mechanical/Part,Component1_53,,Component1_53,,Silicon +CAD,Mechanical/Part,Plate 1_53,,Plate 1_53,,Silicon +CAD,Mechanical/Part,Plate 2_53,,Plate 2_53,,Silicon +CAD,Mechanical/Part,Component1_54,,Component1_54,,Silicon +CAD,Mechanical/Part,Plate 1_54,,Plate 1_54,,Silicon +CAD,Mechanical/Part,Plate 2_54,,Plate 2_54,,Silicon +CAD,Mechanical/Part,Component1_55,,Component1_55,,Silicon +CAD,Mechanical/Part,Plate 1_55,,Plate 1_55,,Silicon +CAD,Mechanical/Part,Plate 2_55,,Plate 2_55,,Silicon +CAD,Mechanical/Part,Component1_56,,Component1_56,,Silicon +CAD,Mechanical/Part,Plate 1_56,,Plate 1_56,,Silicon +CAD,Mechanical/Part,Plate 2_56,,Plate 2_56,,Silicon +CAD,Mechanical/Part,Component1_57,,Component1_57,,Silicon +CAD,Mechanical/Part,Plate 1_57,,Plate 1_57,,Silicon +CAD,Mechanical/Part,Plate 2_57,,Plate 2_57,,Silicon +CAD,Mechanical/Part,Component1_58,,Component1_58,,Silicon +CAD,Mechanical/Part,Plate 1_58,,Plate 1_58,,Silicon +CAD,Mechanical/Part,Plate 2_58,,Plate 2_58,,Silicon +CAD,Mechanical/Part,Component1_59,,Component1_59,,Silicon +CAD,Mechanical/Part,Plate 1_59,,Plate 1_59,,Silicon +CAD,Mechanical/Part,Plate 2_59,,Plate 2_59,,Silicon +CAD,Mechanical/Part,Component1_60,,Component1_60,,Silicon +CAD,Mechanical/Part,Plate 1_60,,Plate 1_60,,Silicon +CAD,Mechanical/Part,Plate 2_60,,Plate 2_60,,Silicon +CAD,Mechanical/Part,Component1_61,,Component1_61,,Silicon +CAD,Mechanical/Part,Plate 1_61,,Plate 1_61,,Silicon +CAD,Mechanical/Part,Plate 2_61,,Plate 2_61,,Silicon +CAD,Mechanical/Part,Component1_62,,Component1_62,,Silicon +CAD,Mechanical/Part,Plate 1_62,,Plate 1_62,,Silicon +CAD,Mechanical/Part,Plate 2_62,,Plate 2_62,,Silicon +CAD,Mechanical/Part,Component1_63,,Component1_63,,Silicon +CAD,Mechanical/Part,Plate 1_63,,Plate 1_63,,Silicon +CAD,Mechanical/Part,Plate 2_63,,Plate 2_63,,Silicon +CAD,Mechanical/Part,Component1_64,,Component1_64,,Silicon +CAD,Mechanical/Part,Plate 1_64,,Plate 1_64,,Silicon +CAD,Mechanical/Part,Plate 2_64,,Plate 2_64,,Silicon +CAD,Mechanical/Part,Component1_65,,Component1_65,,Silicon +CAD,Mechanical/Part,Plate 1_65,,Plate 1_65,,Silicon +CAD,Mechanical/Part,Plate 2_65,,Plate 2_65,,Silicon +CAD,Mechanical/Part,Component1_66,,Component1_66,,Silicon +CAD,Mechanical/Part,Plate 1_66,,Plate 1_66,,Silicon +CAD,Mechanical/Part,Plate 2_66,,Plate 2_66,,Silicon +CAD,Mechanical/Part,Component1_67,,Component1_67,,Silicon +CAD,Mechanical/Part,oTOF v2,,oTOF v2,,Silicon +CAD,Mechanical/Part,Plate 1_67,,Plate 1_67,,Silicon +CAD,Mechanical/Part,Plate 2_67,,Plate 2_67,,Silicon +CAD,Mechanical/Part,Module,,Module,,Silicon diff --git a/Detectors/Base/include/DetectorsBase/CADGeometryUtils.h b/Detectors/CADSupport/include/CADSupport/CADGeometryUtils.h similarity index 87% rename from Detectors/Base/include/DetectorsBase/CADGeometryUtils.h rename to Detectors/CADSupport/include/CADSupport/CADGeometryUtils.h index 12d626d0eb572..eb87fff016056 100644 --- a/Detectors/Base/include/DetectorsBase/CADGeometryUtils.h +++ b/Detectors/CADSupport/include/CADSupport/CADGeometryUtils.h @@ -8,6 +8,8 @@ // In applying this license CERN does not waive the privileges and immunities // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 /// \file CADGeometryUtils.h /// \brief Helpers to inject CAD-derived (TGeo) geometry into O2 simulation @@ -15,16 +17,16 @@ /// These utilities are shared between purely passive external modules /// (o2::passive::ExternalModule) and sensitive external detectors /// (o2::ext::ExternalDetector). They deal with the geometry produced by -/// scripts/geometry/O2_CADtoTGeo.py, which is emitted as a ROOT macro. +/// Detectors/CADSupport/tools/O2_CADtoTGeo.py, which is emitted as a ROOT macro. -#ifndef ALICEO2_BASE_CADGEOMETRYUTILS_H -#define ALICEO2_BASE_CADGEOMETRYUTILS_H +#ifndef ALICEO2_CADSUPPORT_CADGEOMETRYUTILS_H +#define ALICEO2_CADSUPPORT_CADGEOMETRYUTILS_H #include class TGeoVolume; -namespace o2::base +namespace o2::cad { /// JIT-compile a CAD-derived ROOT geometry macro (as produced by O2_CADtoTGeo.py) @@ -45,6 +47,6 @@ TGeoVolume* buildCADVolumeFromMacro(const std::string& macroFile, const std::str /// handling (so that e.g. tracking cuts apply consistently). void remapCADMedia(TGeoVolume* top, const char* modulename); -} // namespace o2::base +} // namespace o2::cad #endif diff --git a/Detectors/CADSupport/include/CADSupport/O2BVHAssembly.h b/Detectors/CADSupport/include/CADSupport/O2BVHAssembly.h new file mode 100644 index 0000000000000..81ae19eb79365 --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2BVHAssembly.h @@ -0,0 +1,77 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#ifndef ALICEO2_CADSUPPORT_O2BVHASSEMBLY_ +#define ALICEO2_CADSUPPORT_O2BVHASSEMBLY_ + +#include "TGeoShapeAssembly.h" + +class TGeoVolumeAssembly; + +namespace o2 +{ +namespace cad +{ + +/// A BVH-accelerated drop-in for ROOT's `TGeoShapeAssembly`: a BVH over the daughter boxes answers Contains, DistFromOutside and Safety. +/// Install it with MakeBVHAssembly(volume) after `TGeoManager::CloseGeometry()`, or construct it on the volume and call `SetShape`. +/// Each query has a `_Loop` twin over all daughters in index order; the lowest-indexed daughter wins ties, as in ROOT. +/// Unlike ROOT, DistFromOutside also answers from outside the bounding box of a voxelized assembly. +class O2BVHAssembly : public TGeoShapeAssembly +{ + public: + O2BVHAssembly(); + /// Build over the daughters \a volume has *now*; later daughters trigger a lazy rebuild. + explicit O2BVHAssembly(TGeoVolumeAssembly* volume); + ~O2BVHAssembly() override; + + O2BVHAssembly(const O2BVHAssembly&) = delete; + O2BVHAssembly& operator=(const O2BVHAssembly&) = delete; + + /// (Re)build the acceleration structure from the volume's current daughter list. + void BuildBVH(); + /// Number of daughter placements the current BVH covers, -1 if it was never built. + int GetNbuilt() const { return fNbuilt; } + /// Bytes held by the BVH nodes and the primitive-index permutation. + size_t GetBVHMemory() const; + + // ---- the accelerated part of the TGeoShapeAssembly contract ---------------------------- + Bool_t Contains(const Double_t* point) const override; + Double_t DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact = 1, + Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override; + Double_t Safety(const Double_t* point, Bool_t in = kTRUE) const override; + + // ---- the reference twins: same answer, all daughters, index order ---------------------- + Bool_t Contains_Loop(const Double_t* point) const; + Double_t DistFromOutside_Loop(const Double_t* point, const Double_t* dir, + Double_t step = TGeoShape::Big()) const; + Double_t Safety_Loop(const Double_t* point, Bool_t in = kTRUE) const; + + /// Replace \a volume's shape by an O2BVHAssembly and return it. + static O2BVHAssembly* MakeBVHAssembly(TGeoVolumeAssembly* volume); + + private: + /// Rebuild if the daughter count changed since the last build; not thread-safe while the geometry is being assembled. + void EnsureBuilt() const; + + void* fBVH = nullptr; //! bvh::v2::Bvh over the daughter placement boxes + int fNbuilt = -1; //! daughter count the BVH was built for; -1 = never built + int fTreeDepth = 0; //! node levels of the BVH, which size the traversal stacks + + ClassDefOverride(O2BVHAssembly, 1) // BVH-accelerated assembly shape +}; + +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/include/CADSupport/O2BVHSurfaceSolid.h b/Detectors/CADSupport/include/CADSupport/O2BVHSurfaceSolid.h new file mode 100644 index 0000000000000..4506d3de21650 --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2BVHSurfaceSolid.h @@ -0,0 +1,432 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +#ifndef ALICEO2_CADSUPPORT_O2BVHSURFACESOLID_ +#define ALICEO2_CADSUPPORT_O2BVHSURFACESOLID_ + +#include "TGeoBBox.h" + +#include +#include +#include + +class TBuffer3D; + +namespace o2 +{ +namespace cad +{ + +/// One boundary curve of a BVHSurfaceRecord in the flat form ROOT streams: a segment, an arc or a B-spline. +struct BVHSurfaceCurveRecord { + int kind = 0; ///< PlanarBoundaryCurve::Kind: 0 = Line, 1 = Arc, 2 = BSpline + double lineStart[2] = {0., 0.}; + double lineEnd[2] = {0., 0.}; + double center[2] = {0., 0.}; + double radius = 0.; + double startAngle = 0.; + double endAngle = 0.; + int degree = 0; ///< B-spline degree + std::vector poles; ///< B-spline control points, flattened (u, v) pairs + std::vector weights; ///< B-spline weights (empty => non-rational) + std::vector knots; ///< B-spline clamped flat knot vector +}; + +/// The persistent record of one successful Add*Surface call; reading a solid back replays the records. +struct BVHSurfaceRecord { + enum Kind { PlanarPolygon = 0, + CurvedPlanar = 1, + Cylindrical = 2, + Spherical = 3, + Conical = 4, + Toroidal = 5 }; + + int kind = PlanarPolygon; + double origin[3] = {0., 0., 0.}; ///< origin / centerPoint / center + double axisA[3] = {0., 0., 0.}; ///< axisU / axis / polarAxis + double axisB[3] = {0., 0., 0.}; ///< axisV / referenceAxisU + + /// The remaining scalar arguments in Add*Surface declaration order (see expectedScalarCount); + /// the count is checked against the kind on replay. + std::vector scalars; + + bool innerWall = false; + bool trimmed = false; ///< the wire-trim overload was used (quadrics only) + + /// The wires, outer first: PlanarPolygon stores (u, v) pairs in polygonPoints, the others curves; wireSizes counts per wire. + std::vector polygonPoints; + std::vector curves; + std::vector wireSizes; + + /// Sidecar v3 boundary edge identities in curve order: an edge-table index and a BoundaryEdgeFlag byte; empty when not stated. + std::vector boundaryEdgeIds; + std::vector boundaryEdgeFlags; + + /// How many entries \a scalars must hold for \a kind, or -1 for an unknown kind. + static int expectedScalarCount(int recordKind); +}; + +class O2BVHSurfaceSolid : public TGeoBBox +{ + public: + using Point2D = std::array; + using Point3D = std::array; + + O2BVHSurfaceSolid(); + explicit O2BVHSurfaceSolid(const char* name); + ~O2BVHSurfaceSolid() override; + + O2BVHSurfaceSolid(const O2BVHSurfaceSolid&) = delete; + O2BVHSurfaceSolid& operator=(const O2BVHSurfaceSolid&) = delete; + + bool AddPlanarSurface(const Point3D& origin, const Point3D& axisU, const Point3D& axisV, + const std::vector& outerWire, + const std::vector>& innerWires = {}); + + /// One boundary curve in the surface's local (u, v) frame: a line segment, a circular arc or a clamped (rational) B-spline. + struct PlanarBoundaryCurve { + enum Kind { Line, + Arc, + BSpline }; + Kind kind = Line; + Point2D lineStart{0., 0.}; + Point2D lineEnd{0., 0.}; + Point2D center{0., 0.}; + double radius = 0.; + double startAngle = 0.; + double endAngle = 0.; + int degree = 0; ///< B-spline degree + std::vector poles; ///< B-spline control points + std::vector weights; ///< B-spline weights (empty ⇒ non-rational) + std::vector knots; ///< B-spline clamped flat knot vector + + static PlanarBoundaryCurve makeLine(const Point2D& start, const Point2D& end) + { + PlanarBoundaryCurve curve; + curve.kind = Line; + curve.lineStart = start; + curve.lineEnd = end; + return curve; + } + static PlanarBoundaryCurve makeArc(const Point2D& c, double r, double start, double end) + { + PlanarBoundaryCurve curve; + curve.kind = Arc; + curve.center = c; + curve.radius = r; + curve.startAngle = start; + curve.endAngle = end; + return curve; + } + static PlanarBoundaryCurve makeBSpline(int splineDegree, std::vector splinePoles, + std::vector splineWeights, std::vector splineKnots) + { + PlanarBoundaryCurve curve; + curve.kind = BSpline; + curve.degree = splineDegree; + curve.poles = std::move(splinePoles); + curve.weights = std::move(splineWeights); + curve.knots = std::move(splineKnots); + return curve; + } + }; + + /// Add an exact planar surface bounded by line/arc wires; axisU and axisV are orthonormal and axisU x axisV points out. + bool AddCurvedPlanarSurface(const Point3D& origin, const Point3D& axisU, const Point3D& axisV, + const std::vector& outerWire, + const std::vector>& innerWires = {}); + + /// Add a cylindrical wall of \a radius around \a axis over a height range and a phi sweep; innerWall points the normal to the axis. + bool AddCylindricalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double radius, double heightMin, double heightMax, double phiStart = 0., + double phiSweep = 6.283185307179586, bool innerWall = false); + + /// As AddCylindricalSurface, trimmed by line/arc wires in the (phi[rad], h[cm]) domain, which decide containment. + bool AddCylindricalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double radius, double heightMin, double heightMax, double phiStart, double phiSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims = {}); + + /// Add a spherical surface of \a radius trimmed to a theta range and a phi sweep; the defaults give a full sphere. + bool AddSphericalSurface(const Point3D& center, const Point3D& polarAxis, const Point3D& referenceAxisU, + double radius, double thetaMin = 0., double thetaMax = 3.141592653589793, + double phiStart = 0., double phiSweep = 6.283185307179586, bool innerWall = false); + + /// As AddSphericalSurface, trimmed by line/arc wires in the (phi[rad], theta[rad]) domain. + bool AddSphericalSurface(const Point3D& center, const Point3D& polarAxis, const Point3D& referenceAxisU, + double radius, double thetaMin, double thetaMax, double phiStart, double phiSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims = {}); + + /// Add a conical wall whose radius runs linearly from \a radiusAtMin to \a radiusAtMax; one radius may be zero. + bool AddConicalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double radiusAtMin, double radiusAtMax, double heightMin, double heightMax, + double phiStart = 0., double phiSweep = 6.283185307179586, bool innerWall = false); + + /// As AddConicalSurface, trimmed by line/arc wires in the (phi[rad], h[cm]) domain, which decide containment. + bool AddConicalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double radiusAtMin, double radiusAtMax, double heightMin, double heightMax, double phiStart, + double phiSweep, bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims = {}); + + /// Add a toroidal surface trimmed to a phiRing x phiTube rectangle; the defaults give a full torus, innerWall points the normal to the tube spine. + bool AddToroidalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double majorRadius, double minorRadius, double phiStart = 0., + double phiSweep = 6.283185307179586, double tubeStart = 0., + double tubeSweep = 6.283185307179586, bool innerWall = false); + + /// As AddToroidalSurface, trimmed by wires in the (phiRing, phiTube) domain; the trim may not wrap more than a turn in either angle. + bool AddToroidalSurface(const Point3D& centerPoint, const Point3D& axis, const Point3D& referenceAxisU, + double majorRadius, double minorRadius, double phiStart, double phiSweep, double tubeStart, + double tubeSweep, bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims = {}); + + /// \name Boundary edge identity (sidecar v3): when every face states its edges, CloseShape decides closure by counting them + /// @{ + enum BoundaryEdgeFlag : unsigned char { + kEdgeReversed = 1u << 0, ///< the face runs against the edge's own direction + kEdgeDegenerate = 1u << 1, ///< cone apex / sphere pole: a point, so it has no second face + kEdgeAnchored = 1u << 2 ///< entry i is trim curve i of this face, so it can be measured + }; + + /// Attach surface \a surfaceIndex's edge identities in trim-curve order; false on a bad index or mismatched lengths. + bool SetSurfaceBoundaryEdges(int surfaceIndex, const std::vector& edgeIds, + const std::vector& edgeFlags); + /// @} + + /// Finalize the shape: bounding box, display mesh, BVH and closure diagnostics, reported when \a check is set. + void CloseShape(bool check = true); + + int GetNsurfaces() const; + bool IsDefined() const; + + /// \name The source model's own tolerance, in cm, from the sidecar; zero means not stated + /// @{ + void SetModelTolerance(double toleranceCm); + double GetModelTolerance() const { return fModelTolerance; } + /// @} + + /// Whether the BVH acceleration structure has been built (after CloseShape). + bool HasBVH() const; + /// Fill the BVH root-node bounding box; returns false when no BVH has been built. + bool GetBVHRootBounds(Point3D& lower, Point3D& upper) const; + /// Test hook: distinct surfaces whose cover boxes the ray traverses; -1 without a BVH. + int CountBVHRayCandidates(const Point3D& point, const Point3D& direction) const; + + /// Ray tmax tightening in the distance queries, on by default; it never changes an answer. Process-wide, not thread safe. + static void SetRayTMaxPruning(bool enable); + static bool GetRayTMaxPruning(); + + /// Per-thread count of surfaces handed to the BVH leaf callback by DistFrom* since the last reset. + static void ResetRayCandidateCounter(); + static long long GetRayCandidateCount(); + + /// Per-thread count of surfaces handed to distanceSqToPatch by Safety and ComputeNormal since the last reset. + static void ResetSafetyCandidateCounter(); + static long long GetSafetyCandidateCount(); + + /// Test-only sabotage: prune on the distance to the box centre, which bounds nothing, so the twins must disagree. + static void SetSafetyBoundUnsoundForTest(bool enable); + static bool GetSafetyBoundUnsoundForTest(); + + /// One crossing of the containment parity ray, as seen by Contains(). + struct ContainsCrossing { + double distance = 0.; ///< ray parameter of the hit + double normalAlignment = 0.; ///< dot(hit normal, test direction): < 0 enters, > 0 exits + /// The hit lay in its patch's on-boundary band, so a tie-break kept it; Contains() re-shoots on these. + bool onTrimBoundary = false; + }; + + /// Diagnostic: the parity ray's crossings at \a point from the BVH and from the loop, sorted by distance. + void DescribeContainsCrossings(const Point3D& point, std::vector& bvhCrossings, + std::vector& loopCrossings) const; + + /// As above for an explicit shooting \a direction: the crossing list behind ContainsAlongDirection(). + void DescribeContainsCrossings(const Point3D& point, const Point3D& direction, + std::vector& bvhCrossings, + std::vector& loopCrossings) const; + + /// Whether the closed shape forms a closed 2-manifold (every boundary edge shared by two faces). + /// Meaningful only after CloseShape(); detects e.g. missing faces. + bool IsClosed() const; + /// Whether all shared boundary edges are traversed in opposite directions after CloseShape(); + /// detects e.g. reversed faces (inconsistent outward normals). + bool IsOrientationConsistent() const; + + /// How far navigation can be trusted: parity containment is defined only on a closed, consistently oriented 2-manifold. + /// Ordered by severity; CloseShape reports the worst defect. + enum class NavigationReliability { + Undetermined = 0, ///< CloseShape() has not run yet: no diagnostics exist + Reliable, ///< closed, consistently oriented 2-manifold: parity is well defined + ReversedFaces, ///< closed, but some rim's partner traverses the shared curve the same + ///< way: at least one face's outward normal points inward + OpenSurfaceSet, ///< some rim has no other face within the match band (missing faces / + ///< trim gaps): parity is undefined in the shadow of every gap along + ///< the parity test direction. GetRimReports() names the loops + NonManifold ///< some rim has two or more other faces within tolerance (coincident + ///< or duplicated faces): parity depends on the order hits are + ///< clustered in + }; + + /// The reliability state derived from the last CloseShape(); Undetermined before it has run. + NavigationReliability GetNavigationReliability() const; + /// Shorthand for GetNavigationReliability() == NavigationReliability::Reliable. False means the + /// navigation answers of this solid are not to be trusted anywhere, not just near the defect. + bool IsNavigable() const; + /// Short stable identifier of a reliability state ("reliable", "open-surface-set", ...), for + /// logs and machine-readable reports. + static const char* GetNavigationReliabilityName(NavigationReliability reliability); + + /// Per-chord closure counts: diagnostics only; GetNavigationReliability() reads the rim counts below. + int GetBoundaryEdgeCount() const; + int GetNonManifoldEdgeCount() const; + int GetReversedEdgeCount() const; + + /// \name The rim-based closure measurement, in cm and per rim; GetNavigationReliability() decides on it + /// @{ + /// Largest distance from any face's trim boundary to the nearest trim boundary of another face, in cm. + double GetMaxRimIsolation() const; + /// \name Closure by edge identity (sidecar v3); when available these decide closure and reliability + /// @{ + /// Whether the edge identities were complete enough to decide closure by counting. + bool HasEdgeIdentity() const; + /// Distinct source edges and their incidence: shared, boundary, non-manifold, reversed and degenerate. + int GetSourceEdgeCount() const; + int GetSharedSourceEdgeCount() const; + int GetBoundarySourceEdgeCount() const; + int GetNonManifoldSourceEdgeCount() const; + int GetReversedSourceEdgeCount() const; + int GetDegenerateSourceEdgeCount() const; + /// Largest Hausdorff distance between the two faces' realisations of one shared edge, in cm; a measurement only. + double GetMaxSharedEdgeDeviation() const; + /// How many shared edges that maximum is over, and how many could not contribute because one of + /// the two faces carries a parametric-rectangle trim with no per-edge curve to sample. + int GetMeasuredSharedEdgeCount() const; + int GetUnmeasuredSharedEdgeCount() const; + /// @} + /// Largest distance a rim polyline sits from the smooth rim it samples, in cm. + double GetRimChordResolution() const; + /// The declared rim match tolerance in cm, the model's own or a fallback: the floor of each chord's match band. + double GetRimMatchTolerance() const; + /// Summed trim-boundary length, and the part with no other face within the match band, in cm. + double GetTotalRimLength() const; + double GetUnmatchedRimLength() const; + /// Rim counts: total, and split by the same four states as the edge counters above. + int GetRimCount() const; + int GetMatchedRimCount() const; + int GetBoundaryRimCount() const; + int GetNonManifoldRimCount() const; + int GetReversedRimCount() const; + + /// One trim loop of one face as the closure measurement saw it, naming the rim and its worst chord. + struct RimReport { + int surface = -1; ///< index into GetSurfaceRecords() of the face owning this rim + int rimOnSurface = -1; ///< which trim loop of that face, in the order the face emits them + bool closed = false; ///< the rim polyline returns to its own first point + int chords = 0; + int unmatchedChords = 0; ///< of them, how many found no other face within the tolerance + double length = 0.; ///< the rim's length in cm + double unmatchedLength = 0.; ///< how much of it has no other face within the tolerance, in cm + /// Largest distance from a chord midpoint of this rim to another face's chord, where, and which face (-1 if none). + double maxIsolation = 0.; + std::array maxIsolationPoint{{0., 0., 0.}}; + int maxIsolationFace = -1; + /// What this rim alone implies about the solid, on the same scale GetNavigationReliability() + /// reports: Reliable means matched. That call returns exactly the worst state present here. + NavigationReliability state = NavigationReliability::Undetermined; + }; + /// Every rim of the last CloseShape(), in the order the faces were visited; empty before it has + /// run. GetRimCount() is its size. + const std::vector& GetRimReports() const; + + /// Each face's divergence-theorem contribution to Capacity(), in record order. + void GetSurfaceCapacityContributions(std::vector& contributions) const; + /// @} + + void ComputeBBox() override; + + int DistancetoPrimitive(int, int) override { return 99999; } + const TBuffer3D& GetBuffer3D(int reqSections, Bool_t localFrame) const override; + void GetMeshNumbers(int& nvert, int& nsegs, int& npols) const override; + int GetNmeshVertices() const override; + + /// Fill \a array with \a npoints points on the solid's exact boundary; kFALSE below GetNmeshVertices() so ROOT uses SetPoints(). + Bool_t GetPointsOnSegments(Int_t npoints, Double_t* array) const override; + + /// The tolerance GetPointsOnSegments() holds its points to, in cm. A point further than this + /// from its own patch is replaced by an exact display-mesh vertex rather than emitted. + static constexpr double kSurfacePointTolerance = 1.e-11; + + void InspectShape() const override {} + TBuffer3D* MakeBuffer3D() const override; + void Print(Option_t* option = "") const override; + void SavePrimitive(std::ostream&, Option_t*) override {} + void SetPoints(double* points) const override; + void SetPoints(Float_t* points) const override; + void SetSegsAndPols(TBuffer3D& buff) const override; + void Sizeof3D() const override {} + + Double_t DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact = 1, + Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override; + Double_t DistFromInside(const Double_t* point, const Double_t* dir, Int_t iact = 1, + Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override; + bool Contains(const Double_t* point) const override; + /// Trivial non-BVH Contains looping over all surfaces; kept for debugging and + /// cross-validation of the BVH-accelerated path (see O2Tessellated::Contains_Loop). + bool Contains_Loop(const Double_t* point) const; + /// Diagnostic: the parity answer for one explicit \a direction, bypassing Contains()'s re-shoot policy. + bool ContainsAlongDirection(const Double_t* point, const Double_t* direction) const; + /// Non-BVH DistFrom* over all surfaces: the oracles the BVH paths must match exactly. + Double_t DistFromOutside_Loop(const Double_t* point, const Double_t* dir, + Double_t stepmax = TGeoShape::Big()) const; + Double_t DistFromInside_Loop(const Double_t* point, const Double_t* dir, + Double_t stepmax = TGeoShape::Big()) const; + Double_t Safety(const Double_t* point, Bool_t in = kTRUE) const override; + void ComputeNormal(const Double_t* point, const Double_t* dir, Double_t* norm) const override; + /// Non-BVH Safety/ComputeNormal over all surfaces: the oracles the BVH traversal must match bit for bit. + Double_t Safety_Loop(const Double_t* point, Bool_t in = kTRUE) const; + void ComputeNormal_Loop(const Double_t* point, const Double_t* dir, Double_t* norm) const; + Double_t Capacity() const override; + + /// The Add*Surface calls this solid was built from, in order. + const std::vector& GetSurfaceRecords() const { return fRecords; } + + private: + /// Containment shared by Contains() and Contains_Loop(): one parity shot if Reliable, else a vote; \a useBVH picks the path. + bool containsByParity(const Double_t* point, bool useBVH) const; + + /// The normal shared by ComputeNormal() and ComputeNormal_Loop(); \a useLoop picks the all-surfaces scan. + void computeNormalFrom(const Double_t* point, const Double_t* dir, Double_t* norm, bool useLoop) const; + + /// Replay fRecords through Add*Surface and CloseShape(); false, leaving the solid undefined, when a record fails. + bool RebuildFromRecords(); + + /// Walk \a point onto patch \a surfaceIndex along its normal to kSurfacePointTolerance; false if it does not get there. + bool ProjectOntoPatch(int surfaceIndex, double* point) const; + + struct Impl; + Impl* fImpl = nullptr; //! private bounded-surface implementation + + /// The persistent state: everything else is rebuilt from it. See BVHSurfaceRecord. + std::vector fRecords; + + /// The source model's declared tolerance in cm; 0 when unknown. See SetModelTolerance. + double fModelTolerance = 0.; + + ClassDefOverride(O2BVHSurfaceSolid, 3) // BVH surface-bounded shape class +}; + +} // namespace cad +} // namespace o2 + +#endif \ No newline at end of file diff --git a/Detectors/CADSupport/include/CADSupport/O2FlatCSG.h b/Detectors/CADSupport/include/CADSupport/O2FlatCSG.h new file mode 100644 index 0000000000000..02c10222b3ec6 --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2FlatCSG.h @@ -0,0 +1,218 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#ifndef ALICEO2_CADSUPPORT_O2FLATCSG_ +#define ALICEO2_CADSUPPORT_O2FLATCSG_ + +#include "TGeoBBox.h" + +#include + +namespace o2 +{ +namespace cad +{ + +/// One signed implicit halfspace, the region `sign * f(x) <= 0`: kQuadric stores `x^T A x + 2 b^T x + c` as +/// (a00, a01, a02, a11, a12, a22, b0, b1, b2, c); kTorus stores (px, py, pz, dx, dy, dz, R, r) in the first eight. +struct FlatCSGHalfspace { + enum Kind : int { kQuadric = 0, + kTorus = 1 }; + int kind = kQuadric; + double sign = 1.; + double c[11] = {}; +}; + +/// One DNF cell: `[first, first + count)` of the halfspace array, intersected; `volume` is its own volume. +struct FlatCSGCell { + int first = 0; + int count = 0; + double volume = 0.; +}; + +/// One box of the sub-cell subdivision; `nActive == 0` means it is wholly inside its cell. +/// An active list describes its cell only inside its box, so every ray query clips to the box first. +struct FlatCSGBox { + double min[3] = {}; + double max[3] = {}; + int cell = -1; + int firstActive = 0; + int nActive = 0; +}; + +/// A solid stored as a union of intersection cells over signed implicit halfspaces, the flat DNF of a decomposed part. +/// Every accelerated query has a bit-identical `_Loop` twin over all cells and halfspaces. +class O2FlatCSG : public TGeoBBox +{ + public: + O2FlatCSG(); + explicit O2FlatCSG(const char* name); + ~O2FlatCSG() override; + + // The shape owns a raw `bvh::v2::Bvh` behind `fBVH`, so a compiler-written copy would hand two + // shapes the same BVH and then free it twice; same treatment as O2BVHAssembly. + O2FlatCSG(const O2FlatCSG&) = delete; + O2FlatCSG& operator=(const O2FlatCSG&) = delete; + + // ---- building ------------------------------------------------------------------------- + /// Append a quadric halfspace; returns its index. `sign` is +1 or -1, inside is `sign*Q <= 0`. + int AddQuadric(double sign, const double coeff[10]); + /// Append a torus halfspace, inside `sign * (sqrt((rho - major)^2 + z^2) - minor) <= 0` about unit \a axis; returns its index. + int AddTorus(double sign, const double* centre, const double* axis, double major, double minor); + /// Append a cell over `[first, first + count)` of the halfspace array; returns its index. + int AddCell(int first, int count, double volume); + + int GetNhalfspaces() const { return static_cast(fHalfspaces.size()); } + int GetNcells() const { return static_cast(fCells.size()); } + const FlatCSGHalfspace& GetHalfspace(int index) const { return fHalfspaces[index]; } + const FlatCSGCell& GetCell(int index) const { return fCells[index]; } + + /// The AABB of cell \a cell. The halfspaces alone do not bound a cell -- an intersection of + /// halfspaces can be unbounded -- so the converter supplies the box the decomposition measured. + void SetCellBBox(int cell, const double* lo, const double* hi); + /// The AABB `SetCellBBox` recorded for cell \a cell, for the sidecar writer. Reads back zeros + /// for a cell whose box was never set. + void GetCellBBox(int cell, double* lo, double* hi) const; + + /// Build the sub-cell boxes and their BVH. Call once, after the last AddCell. + void CloseShape(); + bool IsClosed() const { return fClosed; } + + /// Bytes held by the BVH nodes and the primitive-index permutation. + size_t GetBVHMemory() const; + + int GetNboxes() const { return static_cast(fBoxes.size()); } + const FlatCSGBox& GetBox(int index) const { return fBoxes[index]; } + /// For the tests: the box structure is the thing being proved sound, so it has to be readable. + int GetActive(int index) const { return fActive[index]; } + /// True when every halfspace of cell `index` contains `point`. + bool CellContains(int index, const double* point) const; + + /// Subdivision depth cap. See `fSplitDepth` for where the default comes from. + void SetSplitDepth(int depth) { fSplitDepth = depth; } + /// Stop splitting a box narrower than this fraction of the part's bounding-box diagonal. + /// See `fMinBoxFraction` for where the default comes from. + void SetMinBoxFraction(double fraction) { fMinBoxFraction = fraction; } + + /// `sign * f(point)`; the halfspace contains the point when this is `<= 0`. + static double EvalHalfspace(const FlatCSGHalfspace& halfspace, const double* point); + + /// A rigorous enclosure `[rangeLo, rangeHi]` of `sign * f` over the box `[lo, hi]`, padded outward. + /// Requires `lo[i] <= hi[i]` and finite bounds, which CloseShape enforces; it does not check them. + static void HalfspaceRange(const FlatCSGHalfspace& halfspace, const double* lo, const double* hi, + double& rangeLo, double& rangeHi); + + /// Real roots of `sign * f(origin + t*dir) = 0`, unsorted, at most four; returns the count. + static int HalfspaceRoots(const FlatCSGHalfspace& halfspace, const double* origin, + const double* dir, double* roots); + + /// The occupancy of cell \a cell along the ray within `[tlo, thi]`, as `[enter, exit]` pairs in \a out; a null \a active uses every halfspace. + /// Returns the pair count, or a negative value when \a maxOut is too small. + int CellIntervals(int cell, const int* active, int nActive, const double* origin, + const double* dir, double tlo, double thi, double* out, int maxOut) const; + + // ---- the TGeoShape contract; the accelerated queries use their `_Loop` twin until CloseShape succeeds ---- + Bool_t Contains(const Double_t* point) const override; + + Double_t DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact = 1, + Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override; + Double_t DistFromInside(const Double_t* point, const Double_t* dir, Int_t iact = 1, + Double_t step = TGeoShape::Big(), Double_t* safe = nullptr) const override; + + /// A lower bound on the distance to the boundary from the box structure: outside the nearest box, inside the faces of a solid box, else 0. + Double_t Safety(const Double_t* point, Bool_t in = kTRUE) const override; + + /// Per-thread count of DistFromInside queries whose pruned traversal had to be redone unpruned. + static void ResetUnprunedRetryCounter(); + static long long GetUnprunedRetryCount(); + + /// The union of the retained sub-cell boxes, tighter than the union of the cell AABBs. + void ComputeBBox() override; + + /// The sum of the cells' own volumes. The cells of a decomposition are disjoint by construction + /// (`decompose`'s volume guard checks it), so there is no inclusion-exclusion to do. + Double_t Capacity() const override; + + /// The normal of the halfspace nearest to equality at `point`, oriented along `dir`. + void ComputeNormal(const Double_t* point, const Double_t* dir, Double_t* norm) const override; + /// Points on the solid's own boundary, for the overlap checkers; kFALSE if fewer than \a npoints were found. + Bool_t GetPointsOnSegments(Int_t npoints, Double_t* array) const override; + + // ---- the reference twins --------------------------------------------------------------- + Bool_t Contains_Loop(const Double_t* point) const; + + Double_t DistFromOutside_Loop(const Double_t* point, const Double_t* dir, + Double_t step = TGeoShape::Big()) const; + Double_t DistFromInside_Loop(const Double_t* point, const Double_t* dir, + Double_t step = TGeoShape::Big()) const; + /// `Safety`'s twin over all boxes: it must equal `Safety` and be a sound bound. + Double_t Safety_Loop(const Double_t* point, Bool_t in = kTRUE) const; + + protected: + /// Grow the per-cell bounding-box storage to the cell count. + void EnsureCellBBoxStorage(); + + /// The accelerated DistFromOutside/DistFromInside bodies; each clips the ray to a box before using its active list. + Double_t DistFromOutsideBVH(const Double_t* point, const Double_t* dir, Double_t step) const; + Double_t DistFromInsideBVH(const Double_t* point, const Double_t* dir, Double_t step) const; + + /// What the running bound prunes against: nothing, the nearest entry so far (DistFromOutside) or + /// the far end of the interval holding t = 0 so far (DistFromInside). + enum class RayBound { kNone, + kEntry, + kExit }; + + /// Each box's own occupancy pieces along the ray within `[0, step]`: `[enter, exit]` in \a pairs and its cell in \a cells, unmerged. + /// False when a `CellIntervals` call overflowed. \a smallestPruned reports the nearest entry the + /// exit bound skipped, Big if it skipped nothing or if the bound is not `kExit`. + bool GatherRayPieces(const Double_t* point, const Double_t* dir, Double_t step, + std::vector& pairs, std::vector& cells, RayBound bound, + double& smallestPruned) const; + + /// Recursively split `[lo, hi]` for `cell`, dropping the halfspaces the range bound decides and the boxes it proves outside. + /// A split of a far-from-cubic box draws on `cubifyBudget`, any other on `depth`. + void SplitBox(int cell, const double* lo, const double* hi, const std::vector& active, + int depth, double minSize, int cubifyBudget); + + std::vector fHalfspaces; ///< the flat halfspace array + std::vector fCells; ///< the DNF's cells, indexing into it + + /// The sub-cell boxes, rebuilt by `CloseShape`; not streamed. + std::vector fBoxes; //! + /// The boxes' active-halfspace lists, concatenated. Derived alongside `fBoxes`; not streamed + /// for the same reason. + std::vector fActive; //! + std::vector fCellLo; ///< each cell's AABB low corner, 3 doubles per cell + std::vector fCellHi; ///< each cell's AABB high corner, 3 doubles per cell + /// Whether `SetCellBBox` was ever called for a given cell; `CloseShape` refuses to build a + /// solid missing one rather than silently drop that cell -- see `CloseShape`'s implementation. + std::vector fCellBBoxSet; + /// Set by a successful `CloseShape`; not streamed. The `#pragma read` rule closes every shape ROOT reads back. + bool fClosed = false; //! + /// Subdivision depth cap, and the minimum box size as a fraction of the part's bounding-box + /// diagonal, chosen for query cost on the shipped parts. + int fSplitDepth = 4; + double fMinBoxFraction = 0.05; + + /// The BVH over `fBoxes`, rebuilt by `CloseShape`; not streamed. + void* fBVH = nullptr; //! bvh::v2::Bvh over the sub-cell boxes + + // Scratch buffers are thread_local statics in the .cxx, never members: shapes are shared by all navigator threads. + + ClassDefOverride(O2FlatCSG, 1) // flat-DNF halfspace shape class +}; + +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/include/CADSupport/O2OverlapCheck.h b/Detectors/CADSupport/include/CADSupport/O2OverlapCheck.h new file mode 100644 index 0000000000000..58fbd25dec65c --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2OverlapCheck.h @@ -0,0 +1,134 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#ifndef ALICEO2_CADSUPPORT_O2OVERLAPCHECK_ +#define ALICEO2_CADSUPPORT_O2OVERLAPCHECK_ + +#include +#include +#include + +class TGeoShape; +class TGeoMatrix; +class TGeoVolume; + +namespace o2 +{ +namespace cad +{ + +/// Whether two placed solids may legally coexist: disjoint and touching are legal, interpenetrating and contained are not. +enum class OverlapVerdict { + Disjoint, ///< no sampled boundary point of either solid lies inside the other + Touching, ///< boundary points coincide, but none is deeper than the depth tolerance + Interpenetrating, ///< a boundary point of one solid lies strictly inside the other: illegal + Contained ///< every sampled boundary point of the smaller solid is inside the other +}; + +const char* OverlapVerdictName(OverlapVerdict verdict); + +struct OverlapOptions { + /// Boundary points sampled per solid. Coverage, not accuracy: every individual answer is exact, + /// so this bounds the *false negatives* and nothing else. + int pointsPerSolid = 20000; + /// A containment shallower than this is a shared boundary, not an overlap. In cm. + double depthTolerance = 1.e-6; + /// A sampled point further than this from the boundary of the solid it was sampled from is not + /// evidence about anything and is discarded (and counted). In cm. + double residualTolerance = 1.e-6; + /// Bounding-box inflation before the pairwise rejection, in cm; it decides which disjoint pairs get a separation. + double padCm = 0.1; + /// Monte-Carlo samples for the shared volume of an illegal pair; 0, the default, disables the estimate. + int volumeSamples = 0; + /// Also test every daughter against the mother it sits in (ROOT's "extrusion" case). Silently a + /// no-op when the mother is an assembly, which has no shape to be extruded from. + bool checkExtrusion = true; +}; + +/// One pair of placed solids, and everything measured about it. +struct OverlapPair { + std::string nameA; + std::string nameB; + OverlapVerdict verdict = OverlapVerdict::Disjoint; + + /// The largest depth of a sampled boundary point of one solid inside the other: the verdict's evidence. + /// A lower bound on the penetration depth, and when positive a proof that the interiors share volume. + double depthCm = 0.; + std::array deepestPoint{{0., 0., 0.}}; ///< in the master frame + std::string deepestPointFrom; ///< which solid's boundary the deepest point came from + + int pointsAInsideB = 0; ///< sampled points of A found inside B at any depth + int pointsBInsideA = 0; + int deepPointsAInsideB = 0; ///< ... of which deeper than depthTolerance + int deepPointsBInsideA = 0; + int sampledA = 0; ///< accepted (on-boundary) sample counts actually used + int sampledB = 0; + + /// Smallest sampled distance from a boundary point of one solid to the other, in cm; meaningful only when Disjoint. + double separationCm = -1.; + + double sharedVolumeCm3 = -1.; ///< Monte-Carlo estimate; < 0 when not measured + double sharedVolumeErrCm3 = 0.; ///< its 1-sigma statistical error + int sharedVolumeHits = 0; +}; + +/// One solid's sampling report; a shape with a poor display mesh shows here as reduced coverage. +struct OverlapSolidReport { + std::string name; + std::string shapeClass; + int requested = 0; + int accepted = 0; + int rejected = 0; + double worstResidualCm = 0.; ///< the largest own-boundary distance among the *accepted* points + bool usedPointsOnSegments = false; +}; + +struct OverlapCensus { + std::vector solids; + std::vector pairs; ///< only the pairs that survived the bounding-box rejection + std::vector extrusions; + + int nSolids = 0; + int nPairsTotal = 0; ///< N (N - 1) / 2 + int nPairsTested = 0; ///< after the bounding-box rejection + int nDisjoint = 0; + int nTouching = 0; + int nInterpenetrating = 0; + int nContained = 0; + int nExtruding = 0; + int nPointsRejected = 0; + double worstResidualCm = 0.; + double elapsedSeconds = 0.; + + /// The one-line answer: nInterpenetrating + nContained + nExtruding. + int illegalCount() const { return nInterpenetrating + nContained + nExtruding; } +}; + +/// Sample \a npoints points on \a shape's own boundary into \a points, keeping those within \a residualTolerance where Contains flips. +/// Returns the number kept; \a rejected and \a worstResidual report the filter. +int SampleBoundaryPoints(const TGeoShape* shape, int npoints, double residualTolerance, + std::vector& points, int& rejected, double& worstResidual, + bool* usedPointsOnSegments = nullptr); + +/// Test one placed pair. \a matA / \a matB take each shape's local frame to the common frame. +OverlapPair CheckPairOverlap(const TGeoShape* shapeA, const TGeoMatrix* matA, const std::string& nameA, + const TGeoShape* shapeB, const TGeoMatrix* matB, const std::string& nameB, + const OverlapOptions& options = OverlapOptions()); + +/// Census every pair of \a volume's immediate daughters, and optionally each daughter against \a volume. +OverlapCensus CheckWorldOverlaps(const TGeoVolume* volume, const OverlapOptions& options = OverlapOptions()); + +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/include/CADSupport/O2SolidHarness.h b/Detectors/CADSupport/include/CADSupport/O2SolidHarness.h new file mode 100644 index 0000000000000..79bdbd380283f --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2SolidHarness.h @@ -0,0 +1,224 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +/// \file O2SolidHarness.h +/// \brief Validation and timing harness for TGeoShape navigation, typed on plain `TGeoShape*`. + +#ifndef ALICEO2_CADSUPPORT_O2SOLIDHARNESS_ +#define ALICEO2_CADSUPPORT_O2SOLIDHARNESS_ + +#include "TGeoShape.h" + +class TGeoMatrix; +class TGeoHMatrix; + +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace cad +{ +namespace harness +{ + +using Point3D = std::array; + +struct Ray { + Point3D origin{}; + Point3D dir{}; // unit vector by convention (TGeo contract); not renormalized by the harness +}; + +/// Parameters of `generateSamples`; the counts are targets, and a category may come back short. +struct SampleConfig { + int nBulk = 2000; ///< uniform points over the inflated bbox + int nBoundary = 2000; ///< points within `boundaryBand` of the reference surface + int nInside = 1000; ///< points accepted by the reference Contains() + int nOutsideRays = 4000; ///< rays from outside origins, for DistFromOutside + int nInsideRays = 2000; ///< rays from inside origins, for DistFromInside + double bboxInflate = 0.15; ///< fractional bbox half-extent padding for bulk/outside sampling + double boundaryBand = -1.; ///< absolute distance (cm); <0 auto-picks 1e-3 * bbox diagonal + double aimedRayFraction = 0.5; ///< fraction of rays aimed at a random interior bbox point rather + ///< than an isotropic direction (keeps DistFromOutside hit rates + ///< non-degenerate) + int maxRejectionAttempts = 200; ///< attempts per accepted sample before giving up on that category + uint64_t seed = 1; ///< every SampleSet is fully determined by this and the bbox +}; + +struct SampleSet { + Point3D bboxMin{}; + Point3D bboxMax{}; + std::vector bulkPoints; + std::vector boundaryPoints; + std::vector insidePoints; + std::vector outsideRays; + std::vector insideRays; +}; + +/// A deterministic sample set from `cfg.seed` and the bbox; \a reference, the trusted mesh, classifies the points. +SampleSet generateSamples(const TGeoShape* reference, const Point3D& bboxMin, const Point3D& bboxMax, + const SampleConfig& cfg = {}); + +// ---- Validation ---------------------------------------------------------------------------------- + +/// One worst-case disagreement, with enough state (point/direction/values) to reproduce it +/// directly outside the harness. +struct Offender { + Point3D point{}; + Point3D dir{}; // zero for point-only queries (Contains, Safety) + double candidateValue = 0.; + double referenceValue = 0.; + double deviation = 0.; + double referenceSafety = 0.; // point queries: reference distance to its own surface + double incidenceCosine = 1.; // ray queries: |cos| between ray and surface normal at the hit, + // i.e. how much surface uncertainty this ray amplifies +}; + +struct ValidationResult { + size_t nSamples = 0; + size_t nAgree = 0; + size_t nMismatchWithinBand = 0; // explainable by the reference's own imprecision (see below) + size_t nMismatchMissedSurface = 0; // one side found no crossing where the other did + size_t nMismatchUnexplained = 0; + size_t nNoVerdict = 0; // oracle mode only: the reference declined to answer + size_t nRelabelled = 0; // ray queries, oracle mode: origins whose category the oracle + // contradicted, so the other TGeo entry point was asked + double worstDeviation = 0.; + std::vector worstOffenders; // bounded by opt.maxOffenders, worst-first +}; + +/// `nMismatchMissedSurface` counts a candidate that misses a wall the reference hits, or tunnels +/// to a farther one; such a mismatch is never explained away as mesh chording. +struct ValidationOptions { + double distanceTolerance = 1.e-6; ///< absolute agreement tolerance for distances (cm) + double meshBand = 1.e-2; ///< the reference's positional uncertainty (cm): chord sagitta or model tolerance + /// Floor of the incidence cosine that scales the distance allowance, so a tangent ray cannot excuse an unbounded error. + double minIncidenceCosine = 1.e-2; + double stepmax = TGeoShape::Big(); + size_t maxOffenders = 10; +}; + +ValidationResult validateContains(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& points, const ValidationOptions& opt = {}); + +ValidationResult validateDistFromOutside(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& rays, const ValidationOptions& opt = {}); + +ValidationResult validateDistFromInside(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& rays, const ValidationOptions& opt = {}); + +/// Check one shape's Safety() lower-bound contract against its own DistFrom* along six probe directions; never compares two shapes. +ValidationResult validateSafety(const TGeoShape* shape, const std::vector& points, + const ValidationOptions& opt = {}); + +// ---- Validation against the OpenCascade oracle: a disagreement beyond the model tolerance is a defect ---- + +/// `oracleState`: 1 inside, 0 outside, -1 declined; `oracleBoundaryDistance` may cover only a prefix of \a points. +ValidationResult validateContainsAgainstOracle(const TGeoShape* candidate, + const std::vector& points, + const std::vector& oracleState, + const std::vector& oracleBoundaryDistance, + const ValidationOptions& opt = {}); + +/// `oracleDistance`: the nearest positive crossing, or >= Big() for a miss. `oracleOriginState` (1, 0, -1), when present, +/// decides per ray which entry point is asked, and a -1 origin abstains; otherwise `wantInside` decides. +ValidationResult validateDistanceAgainstOracle(const TGeoShape* candidate, + const std::vector& rays, + const std::vector& oracleDistance, + bool wantInside, const ValidationOptions& opt = {}, + const std::vector& oracleOriginState = {}); + +/// Safety's contract against the oracle's exact distance: `0 <= safety <= trueDistance`. +ValidationResult validateSafetyAgainstOracle(const TGeoShape* candidate, + const std::vector& points, + const std::vector& oracleBoundaryDistance, + const ValidationOptions& opt = {}); + +// ---- Timing -------------------------------------------------------------------------------------- + +struct TimingResult { + size_t nCalls = 0; + double nsPerCall = 0.; + uint64_t checksum = 0; ///< accumulated from the results so the optimizer cannot elide the calls +}; + +namespace detail +{ +/// Checksum mixer the timing loops accumulate results through, so the optimizer cannot elide the +/// measured calls. Exposed only because `timeRayKernel` below is a template. +uint64_t mixDouble(uint64_t acc, double value); +} // namespace detail + +/// Time a per-ray kernel `kernel(origin, dir)` exactly like the `timeDistFrom*` functions, e.g. a `_Loop` twin. +template +TimingResult timeRayKernel(const std::vector& rays, int warmupRepeats, int timedRepeats, RayKernel&& kernel) +{ + for (int warmup = 0; warmup < warmupRepeats; ++warmup) { + for (const auto& ray : rays) { + volatile double sink = kernel(ray.origin, ray.dir); + (void)sink; + } + } + uint64_t checksum = 0; + const auto start = std::chrono::steady_clock::now(); + for (int repeat = 0; repeat < timedRepeats; ++repeat) { + for (const auto& ray : rays) { + checksum = detail::mixDouble(checksum, kernel(ray.origin, ray.dir)); + } + } + const auto stop = std::chrono::steady_clock::now(); + TimingResult result; + result.nCalls = rays.size() * static_cast(timedRepeats); + const double nanoseconds = std::chrono::duration(stop - start).count(); + result.nsPerCall = result.nCalls > 0 ? nanoseconds / static_cast(result.nCalls) : 0.; + result.checksum = checksum; + return result; +} + +TimingResult timeContains(const TGeoShape* shape, const std::vector& points, int warmupRepeats, + int timedRepeats); +TimingResult timeDistFromOutside(const TGeoShape* shape, const std::vector& rays, int warmupRepeats, + int timedRepeats, double stepmax = TGeoShape::Big()); +TimingResult timeDistFromInside(const TGeoShape* shape, const std::vector& rays, int warmupRepeats, + int timedRepeats); +TimingResult timeSafety(const TGeoShape* shape, const std::vector& points, int warmupRepeats, + int timedRepeats); + +// ---- The `shape_.root` sidecar ------------------------------------------------------------- +// +// * one file per part, `shape__.root`, next to the part's other sidecars; +// * one TGeoShape-derived object under the key "shape" (the first such key is the fallback); +// * lengths in centimetres; +// * an optional TGeoHMatrix under "placement" takes the shape's frame to the part's (`local -> part`); +// no key means the identity; +// * a TGeoCompositeShape is written whole and needs no TGeoManager. + +/// Read the single TGeoShape of a `shape_.root` sidecar; nullptr on failure, with the reason in `*error`. The caller owns it. +TGeoShape* loadShapeFromRootFile(const std::string& path, std::string* error = nullptr); + +/// Read the shape's placement, or nullptr when there is none, meaning the identity. The caller owns it. +TGeoHMatrix* loadShapePlacementFromRootFile(const std::string& path); + +/// Write a shape sidecar, with \a placement under "placement" unless it is null or the identity. +bool saveShapeToRootFile(const std::string& path, const TGeoShape& shape, std::string* error = nullptr); +bool saveShapeToRootFile(const std::string& path, const TGeoShape& shape, + const TGeoMatrix* placement, std::string* error); + +} // namespace harness +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/include/CADSupport/O2SurfaceSolidIO.h b/Detectors/CADSupport/include/CADSupport/O2SurfaceSolidIO.h new file mode 100644 index 0000000000000..3e9cf992665ae --- /dev/null +++ b/Detectors/CADSupport/include/CADSupport/O2SurfaceSolidIO.h @@ -0,0 +1,49 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +#ifndef ALICEO2_CADSUPPORT_O2SURFACESOLIDIO_ +#define ALICEO2_CADSUPPORT_O2SURFACESOLIDIO_ + +#include + +namespace o2 +{ +namespace base +{ +class O2Tessellated; +} +namespace cad +{ + +class O2BVHSurfaceSolid; +class O2FlatCSG; + +/// Load an exact-surface sidecar (surfaces_*.bin, versions 1-3) into \a solid through its Add*Surface methods; call CloseShape() after. +/// False on an I/O or format error, when the solid may be partly filled and should be discarded. +bool LoadSurfaceSolid(const std::string& file, O2BVHSurfaceSolid& solid); + +/// Load a facet sidecar (facets_*.bin: a uint32 triangle count, then nine float32 per triangle) into \a solid; call CloseShape() after. +/// False on an I/O or format error; degenerate facets are skipped and counted in a warning. +bool LoadFacetSolid(const std::string& file, o2::base::O2Tessellated& solid); + +/// Load a flat-CSG sidecar (flatcsg_*.bin, version 1) into \a solid; call CloseShape() after. False on an I/O or format error. +bool LoadFlatCSG(const std::string& file, O2FlatCSG& solid); + +/// Write \a solid in the same format. Used by the converter's tests and by the round-trip case; +/// the production writer is Detectors/CADSupport/tools/cadsupport/flat.py, and the two must agree byte for byte. +bool WriteFlatCSG(const std::string& file, const O2FlatCSG& solid); + +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/src/BoundedSurface.h b/Detectors/CADSupport/src/BoundedSurface.h new file mode 100644 index 0000000000000..20c21716c0483 --- /dev/null +++ b/Detectors/CADSupport/src/BoundedSurface.h @@ -0,0 +1,5229 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +/// \file BoundedSurface.h +/// \brief Private analytic bounded surfaces, trim wires and closure checks behind O2BVHSurfaceSolid. + +#ifndef ALICEO2_CADSUPPORT_BOUNDEDSURFACE_H_ +#define ALICEO2_CADSUPPORT_BOUNDEDSURFACE_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace o2::cad::surface +{ + +/// \name Numerical conventions: the tolerances shared by all bounded-surface code +/// @{ +inline constexpr double kTolerance = 1.e-9; ///< generic length tolerance +inline constexpr double kToleranceSq = kTolerance * kTolerance; +inline constexpr double kAreaTolerance = 1.e-18; ///< degenerate (zero) parametric area +inline constexpr double kRayTolerance = 1.e-9; ///< minimum positive ray parameter t +inline constexpr double kIntersectionTolerance = 1.e-7; ///< clustering of near-equal intersections +inline constexpr double kClosureQuantum = 1.e-7; ///< vertex quantization for closure matching +/// Wire-closure tolerance, a 3D length in cm through the surface metric: the CAD extractor's endpoint precision. +inline constexpr double kWireJoinTolerance = 1.e-6; +/// The wire-join band for a model with a declared tolerance: that tolerance when looser than kWireJoinTolerance, else the floor. +inline constexpr double wireJoinToleranceFor(double modelTolerance) +{ + return modelTolerance > kWireJoinTolerance ? modelTolerance : kWireJoinTolerance; +} +/// Chord flatness of the adaptive B-spline sampler, in the curve's parametric units; a B-spline trim is this polyline. +inline constexpr double kBSplineFlatness = 1.e-5; +inline constexpr double kBSplineFlatnessSq = kBSplineFlatness * kBSplineFlatness; +/// Rim-matching distance in cm when the model states no tolerance: the extractor precision, as kWireJoinTolerance. +inline constexpr double kRimMatchTolerance = 1.e-6; + +/// Widening of the BVH leaf boxes before the outward float rounding; it dominates every navigation length tolerance. +inline constexpr double kBVHBoxTolerance = 1.e-3; +/// Zero threshold of solveQuarticReal's branch tests, in machine epsilons relative to the normalised terms: dimensionless. +inline constexpr double kQuarticEpsilon = 32. * 2.220446049250313e-16; +/// @} + +/// A 2D point/vector in a surface's parametric (u, v) domain. +struct Vec2 { + double uCoord = 0.; + double vCoord = 0.; +}; + +/// A 3D point/vector in the solid's local frame. +struct Vec3 { + double xCoord = 0.; + double yCoord = 0.; + double zCoord = 0.; +}; + +inline Vec3 operator+(const Vec3& firstVector, const Vec3& secondVector) +{ + return {firstVector.xCoord + secondVector.xCoord, firstVector.yCoord + secondVector.yCoord, + firstVector.zCoord + secondVector.zCoord}; +} + +inline Vec3 operator-(const Vec3& firstVector, const Vec3& secondVector) +{ + return {firstVector.xCoord - secondVector.xCoord, firstVector.yCoord - secondVector.yCoord, + firstVector.zCoord - secondVector.zCoord}; +} + +inline Vec3 operator*(const Vec3& vector, double scale) +{ + return {vector.xCoord * scale, vector.yCoord * scale, vector.zCoord * scale}; +} + +inline Vec3 operator*(double scale, const Vec3& vector) +{ + return vector * scale; +} + +inline Vec2 operator-(const Vec2& firstPoint, const Vec2& secondPoint) +{ + return {firstPoint.uCoord - secondPoint.uCoord, firstPoint.vCoord - secondPoint.vCoord}; +} + +/// The 3D length squared of parametric displacement \a delta under the first fundamental form (\a gUU, \a gUV, \a gVV). +inline double parametricLengthSq(double gUU, double gUV, double gVV, const Vec2& delta) +{ + return gUU * delta.uCoord * delta.uCoord + 2. * gUV * delta.uCoord * delta.vCoord + + gVV * delta.vCoord * delta.vCoord; +} + +/// How a wire converts a parametric separation into a 3D length: the owning surface's first fundamental form, or the identity. +struct ParametricMetric { + using Evaluate = void (*)(const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV); + + Evaluate evaluate = nullptr; + const void* context = nullptr; + + /// The 3D length squared spanned by the parametric displacement \a delta starting at \a uv. + double lengthSq(const Vec2& uv, const Vec2& delta) const + { + if (evaluate == nullptr) { + return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord; + } + double gUU = 1.; + double gUV = 0.; + double gVV = 1.; + evaluate(context, uv, gUU, gUV, gVV); + return parametricLengthSq(gUU, gUV, gVV, delta); + } + + /// The 3D distance squared between two nearby parametric points, with the form evaluated at \a from. + double distanceSq(const Vec2& from, const Vec2& to) const { return lengthSq(from, to - from); } + + /// The largest 3D length a unit parametric displacement spans at \a uv: the square root of the larger eigenvalue. + double maxScale(const Vec2& uv) const + { + if (evaluate == nullptr) { + return 1.; + } + double gUU = 1.; + double gUV = 0.; + double gVV = 1.; + evaluate(context, uv, gUU, gUV, gVV); + const double trace = gUU + gVV; + const double determinant = gUU * gVV - gUV * gUV; + // the eigenvalues of a symmetric 2x2 form, guarded against a slightly negative discriminant + const double discriminant = std::max(0., trace * trace - 4. * determinant); + return std::sqrt(std::max(0., 0.5 * (trace + std::sqrt(discriminant)))); + } +}; + +/// A ParametricMetric that defers to \a surface, which must outlive it. Every use here is a +/// surface building its own wires inside initialize(), so that holds by construction. +template +inline ParametricMetric parametricMetricOf(const Surface& surface) +{ + return {[](const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV) { + static_cast(context)->parametricMetric(uv, gUU, gUV, gVV); + }, + &surface}; +} + +inline double dot(const Vec3& firstVector, const Vec3& secondVector) +{ + return firstVector.xCoord * secondVector.xCoord + firstVector.yCoord * secondVector.yCoord + + firstVector.zCoord * secondVector.zCoord; +} + +inline Vec3 cross(const Vec3& firstVector, const Vec3& secondVector) +{ + return {firstVector.yCoord * secondVector.zCoord - firstVector.zCoord * secondVector.yCoord, + firstVector.zCoord * secondVector.xCoord - firstVector.xCoord * secondVector.zCoord, + firstVector.xCoord * secondVector.yCoord - firstVector.yCoord * secondVector.xCoord}; +} + +inline double normSq(const Vec3& vector) +{ + return dot(vector, vector); +} + +inline double norm(const Vec3& vector) +{ + return std::sqrt(normSq(vector)); +} + +inline Vec3 normalized(const Vec3& vector) +{ + const double vectorNorm = norm(vector); + if (vectorNorm <= kTolerance) { + return {}; + } + return vector * (1. / vectorNorm); +} + +inline double component(const Vec3& vector, int dimension) +{ + if (dimension == 0) { + return vector.xCoord; + } + if (dimension == 1) { + return vector.yCoord; + } + return vector.zCoord; +} + +inline void assignComponent(Vec3& vector, int dimension, double value) +{ + if (dimension == 0) { + vector.xCoord = value; + } else if (dimension == 1) { + vector.yCoord = value; + } else { + vector.zCoord = value; + } +} + +inline bool finite(const Vec2& point) +{ + return std::isfinite(point.uCoord) && std::isfinite(point.vCoord); +} + +inline bool finite(const Vec3& point) +{ + return std::isfinite(point.xCoord) && std::isfinite(point.yCoord) && std::isfinite(point.zCoord); +} + +inline double distanceSq(const Vec2& firstPoint, const Vec2& secondPoint) +{ + const double deltaU = firstPoint.uCoord - secondPoint.uCoord; + const double deltaV = firstPoint.vCoord - secondPoint.vCoord; + return deltaU * deltaU + deltaV * deltaV; +} + +inline double distanceSq(const Vec3& firstPoint, const Vec3& secondPoint) +{ + return normSq(firstPoint - secondPoint); +} + +inline double cross2D(const Vec2& firstVector, const Vec2& secondVector) +{ + return firstVector.uCoord * secondVector.vCoord - firstVector.vCoord * secondVector.uCoord; +} + +inline double pointSegmentDistanceSq(const Vec2& point, const Vec2& segmentStart, const Vec2& segmentEnd) +{ + const Vec2 segmentVector = segmentEnd - segmentStart; + const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord; + if (segmentLengthSq <= kToleranceSq) { + return distanceSq(point, segmentStart); + } + const double pointProjection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord + + (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) / + segmentLengthSq; + const double clampedProjection = std::max(0., std::min(1., pointProjection)); + const Vec2 closestPoint{segmentStart.uCoord + clampedProjection * segmentVector.uCoord, + segmentStart.vCoord + clampedProjection * segmentVector.vCoord}; + return distanceSq(point, closestPoint); +} + +inline double pointSegmentDistanceSq(const Vec3& point, const Vec3& segmentStart, const Vec3& segmentEnd) +{ + const Vec3 segmentVector = segmentEnd - segmentStart; + const double segmentLengthSq = normSq(segmentVector); + if (segmentLengthSq <= kToleranceSq) { + return distanceSq(point, segmentStart); + } + const double pointProjection = dot(point - segmentStart, segmentVector) / segmentLengthSq; + const double clampedProjection = std::max(0., std::min(1., pointProjection)); + const Vec3 closestPoint = segmentStart + segmentVector * clampedProjection; + return distanceSq(point, closestPoint); +} + +/// \name First fundamental forms by surface family, shared by the surfaces and the sidecar reader +/// @{ + +/// Plane: the frame axes carry the domain's units and need be neither unit nor orthogonal, which +/// makes this the only family with a cross term. +inline void planeParametricMetric(const Vec3& axisU, const Vec3& axisV, double& gUU, double& gUV, double& gVV) +{ + gUU = dot(axisU, axisU); + gUV = dot(axisU, axisV); + gVV = dot(axisV, axisV); +} + +/// Cylinder, (u, v) = (phi[rad], h[cm]). +inline void cylinderParametricMetric(double radius, double& gUU, double& gUV, double& gVV) +{ + gUU = radius * radius; + gUV = 0.; + gVV = 1.; +} + +/// Cone, (u, v) = (phi[rad], h[cm]). \a radiusAtHeight is r(v), which reaches zero at an apex; +/// a step in h also walks along the slope, hence gVV > 1. +inline void coneParametricMetric(double radiusAtHeight, double slope, double& gUU, double& gUV, double& gVV) +{ + gUU = radiusAtHeight * radiusAtHeight; + gUV = 0.; + gVV = 1. + slope * slope; +} + +/// Sphere, (u, v) = (phi[rad], theta[rad]). The azimuthal scale is the radius of the parallel at +/// \a theta, so it vanishes at either pole. +inline void sphereParametricMetric(double radius, double theta, double& gUU, double& gUV, double& gVV) +{ + const double parallelRadius = radius * std::sin(theta); + gUU = parallelRadius * parallelRadius; + gUV = 0.; + gVV = radius * radius; +} + +/// Torus, (u, v) = (phiRing[rad], phiTube[rad]). The ring scale runs from R - r to R + r. +inline void torusParametricMetric(double majorRadius, double minorRadius, double phiTube, double& gUU, double& gUV, + double& gVV) +{ + const double ringRadius = majorRadius + minorRadius * std::cos(phiTube); + gUU = ringRadius * ringRadius; + gUV = 0.; + gVV = minorRadius * minorRadius; +} +/// @} + +inline bool sameIntersection(double firstDistance, double secondDistance) +{ + return std::abs(firstDistance - secondDistance) <= + kIntersectionTolerance * std::max(1., std::max(std::abs(firstDistance), std::abs(secondDistance))); +} + +/// One ray/surface intersection: the ray parameter and the outward normal; a quadric patch can give several per ray. +struct RayHit { + double distance = 0.; + Vec3 normal; + /// The hit lies within the trim's on-boundary band, so its inside/outside side is a tie-break, not data. + bool onTrimBoundary = false; +}; + +/// One straight line segment of a polygon wire, in a surface's parametric (u, v) domain. +struct SurfaceEdge { + Vec2 start; + Vec2 end; + + Vec2 direction() const { return end - start; } + + double lengthSq() const + { + const Vec2 delta = end - start; + return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord; + } + + bool degenerate() const { return lengthSq() <= kToleranceSq; } + + /// Squared distance from a parametric point to this edge. + double distanceSq(const Vec2& point) const { return pointSegmentDistanceSq(point, start, end); } + + /// Closest point on this edge to \a point. Returns the projected point and its clamped + /// parameter \a parameter in [0, 1] (0 at start, 1 at end). Degenerate edges return start. + Vec2 closestPoint(const Vec2& point, double& parameter) const + { + const Vec2 segmentVector = end - start; + const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord + + segmentVector.vCoord * segmentVector.vCoord; + if (segmentLengthSq <= kToleranceSq) { + parameter = 0.; + return start; + } + const double projection = ((point.uCoord - start.uCoord) * segmentVector.uCoord + + (point.vCoord - start.vCoord) * segmentVector.vCoord) / + segmentLengthSq; + parameter = std::max(0., std::min(1., projection)); + return {start.uCoord + parameter * segmentVector.uCoord, start.vCoord + parameter * segmentVector.vCoord}; + } + + /// Accumulate the edge endpoints into a parametric axis-aligned bounding box. + void extendBounds(Vec2& lower, Vec2& upper) const + { + lower.uCoord = std::min({lower.uCoord, start.uCoord, end.uCoord}); + lower.vCoord = std::min({lower.vCoord, start.vCoord, end.vCoord}); + upper.uCoord = std::max({upper.uCoord, start.uCoord, end.uCoord}); + upper.vCoord = std::max({upper.vCoord, start.vCoord, end.vCoord}); + } +}; + +/// Classification of a parametric point against a closed wire. +enum class WireClassification { Outside, + Boundary, + Inside }; + +/// The role a wire plays for a bounded surface. Outer wires bound the material, inner wires +/// (holes) subtract from it. The role fixes the expected winding relative to the surface normal. +enum class WireRole { Outer, + Inner }; + +/// Outcome of wire construction / validation. Valid and Reversed are both usable results; +/// Reversed additionally signals that the orientation had to be normalized (a logged repair). +enum class WireStatus { + Valid, ///< well-formed and already correctly oriented + Reversed, ///< well-formed but re-oriented to match its role (simple, logged repair) + NonFinite, ///< a vertex/edge contained a non-finite coordinate + Open, ///< an explicit edge list did not form a closed loop + TooFewVertices, ///< fewer than three distinct vertices after cleanup + DegenerateVertex, ///< a non-adjacent vertex coincided (self-touching / pinched loop) + ZeroArea ///< the loop encloses no area +}; + +/// Human-readable description of a wire status, for logging. +inline const char* wireStatusMessage(WireStatus status) +{ + switch (status) { + case WireStatus::Valid: + return "valid"; + case WireStatus::Reversed: + return "orientation normalized to match wire role"; + case WireStatus::NonFinite: + return "wire contains a non-finite vertex"; + case WireStatus::Open: + return "wire edges do not form a closed loop"; + case WireStatus::TooFewVertices: + return "wire needs at least three distinct vertices"; + case WireStatus::DegenerateVertex: + return "wire has a coincident (pinched) vertex"; + case WireStatus::ZeroArea: + return "wire has zero area"; + } + return "unknown wire status"; +} + +/// kTolerance as a parametric separation at \a uv: the floor of every trim's on-boundary band. +inline double trimLengthFloor(const ParametricMetric& metric, const Vec2& uv) +{ + const double scale = metric.maxScale(uv); + return scale > kTolerance ? kTolerance / scale : 0.; +} + +/// One closed, oriented polygon loop in a surface's parametric domain: outer loops wind counter-clockwise, holes clockwise. +struct SurfaceWire { + std::vector vertices; + WireRole role = WireRole::Outer; + + /// For each stored segment its input segment, or -1 once a vertex was dropped; sidecar v3 edge identities key on it. + std::vector sourceEdge; + + int edgeCount() const { return static_cast(vertices.size()); } + + /// The stored segment that came from input segment \a inputIndex, or -1 if there is none. + int storedIndexOfSource(int inputIndex) const + { + for (size_t index = 0; index < sourceEdge.size(); ++index) { + if (sourceEdge[index] == inputIndex) { + return static_cast(index); + } + } + return -1; + } + + SurfaceEdge edge(int index) const + { + const int count = edgeCount(); + return {vertices[index % count], vertices[(index + 1) % count]}; + } + + /// Build and validate the wire from an implicitly closed vertex ring; \a metric turns separations into 3D lengths. + bool initialize(const std::vector& inputVertices, WireRole wireRole, WireStatus& status, + const ParametricMetric& metric = {}) + { + role = wireRole; + vertices.clear(); + vertices.reserve(inputVertices.size()); + bool droppedAVertex = false; + + for (const auto& vertex : inputVertices) { + if (!finite(vertex)) { + status = WireStatus::NonFinite; + return false; + } + if (vertices.empty() || metric.distanceSq(vertices.back(), vertex) > kToleranceSq) { + vertices.push_back(vertex); + } else { + droppedAVertex = true; + } + } + + // drop an explicit closing duplicate (first == last) + if (vertices.size() > 1 && metric.distanceSq(vertices.front(), vertices.back()) <= kToleranceSq) { + vertices.pop_back(); + droppedAVertex = true; + } + + if (vertices.size() < 3) { + status = WireStatus::TooFewVertices; + return false; + } + + // reject self-touching loops (non-adjacent coincident vertices) + for (size_t firstIndex = 0; firstIndex < vertices.size(); ++firstIndex) { + for (size_t secondIndex = firstIndex + 1; secondIndex < vertices.size(); ++secondIndex) { + if (metric.distanceSq(vertices[firstIndex], vertices[secondIndex]) <= kToleranceSq) { + status = WireStatus::DegenerateVertex; + return false; + } + } + } + + const double area = signedArea(); + if (std::abs(area) <= kAreaTolerance) { + status = WireStatus::ZeroArea; + return false; + } + + // segment i is input segment i unless a vertex was dropped; then it is unknown + const int storedCount = static_cast(vertices.size()); + sourceEdge.assign(static_cast(storedCount), -1); + if (!droppedAVertex) { + for (int index = 0; index < storedCount; ++index) { + sourceEdge[static_cast(index)] = index; + } + } + + // outer wires must wind CCW (positive area), inner wires CW (negative area) + const bool wantPositiveArea = (role == WireRole::Outer); + if ((area > 0.) != wantPositiveArea) { + std::reverse(vertices.begin(), vertices.end()); + // reversing the ring maps old vertex k to new index n-1-k, so new segment j spans old + // vertices n-1-j and n-2-j, i.e. it is old segment n-2-j traversed backwards + std::vector reversedSource(static_cast(storedCount), -1); + for (int index = 0; index < storedCount; ++index) { + reversedSource[static_cast(index)] = + sourceEdge[static_cast((storedCount - 2 - index % storedCount + 2 * storedCount) % storedCount)]; + } + sourceEdge.swap(reversedSource); + status = WireStatus::Reversed; + return true; + } + + status = WireStatus::Valid; + return true; + } + + /// Build and validate the wire from an ordered edge list, joining within \a joinTolerance through \a metric, as CurveWire does. + bool initializeFromEdges(const std::vector& edges, WireRole wireRole, WireStatus& status, + const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance) + { + if (edges.size() < 3) { + status = WireStatus::TooFewVertices; + return false; + } + for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) { + if (!finite(edges[edgeIndex].start) || !finite(edges[edgeIndex].end)) { + status = WireStatus::NonFinite; + return false; + } + const Vec2& nextStart = edges[(edgeIndex + 1) % edges.size()].start; + if (metric.distanceSq(edges[edgeIndex].end, nextStart) > joinTolerance * joinTolerance) { + status = WireStatus::Open; + return false; + } + } + + std::vector ringVertices; + ringVertices.reserve(edges.size()); + for (const auto& singleEdge : edges) { + ringVertices.push_back(singleEdge.start); + } + return initialize(ringVertices, wireRole, status, metric); + } + + double signedArea() const + { + double area = 0.; + for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) { + const auto& currentVertex = vertices[vertexIndex]; + const auto& nextVertex = vertices[(vertexIndex + 1) % vertices.size()]; + area += currentVertex.uCoord * nextVertex.vCoord - nextVertex.uCoord * currentVertex.vCoord; + } + return 0.5 * area; + } + + /// Accumulate this wire's vertices into a parametric axis-aligned bounding box. This is + /// independent of any concrete surface so cylinders, spheres and cones can reuse it. + void parametricBounds(Vec2& lower, Vec2& upper) const + { + for (const auto& vertex : vertices) { + lower.uCoord = std::min(lower.uCoord, vertex.uCoord); + lower.vCoord = std::min(lower.vCoord, vertex.vCoord); + upper.uCoord = std::max(upper.uCoord, vertex.uCoord); + upper.vCoord = std::max(upper.vCoord, vertex.vCoord); + } + } + + /// The de-duplicated vertex ring, closed back to its first vertex. + std::vector sampledBoundary() const + { + std::vector samples; + if (vertices.empty()) { + return samples; + } + samples.reserve(vertices.size() + 1); + samples.insert(samples.end(), vertices.begin(), vertices.end()); + samples.push_back(vertices.front()); + return samples; + } + + /// Classify against the polygon with an on-boundary half-width of \a band, in parametric units. + WireClassification classify(const Vec2& point, double band) const + { + const double bandSq = band * band; + bool inside = false; + for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) { + const auto& segmentStart = vertices[vertexIndex]; + const auto& segmentEnd = vertices[(vertexIndex + 1) % vertices.size()]; + if (pointSegmentDistanceSq(point, segmentStart, segmentEnd) <= bandSq) { + return WireClassification::Boundary; + } + const bool crossesScanline = (segmentStart.vCoord > point.vCoord) != (segmentEnd.vCoord > point.vCoord); + if (crossesScanline) { + const double intersectionU = segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) * + (segmentEnd.uCoord - segmentStart.uCoord) / + (segmentEnd.vCoord - segmentStart.vCoord); + if (point.uCoord < intersectionU) { + inside = !inside; + } + } + } + return inside ? WireClassification::Inside : WireClassification::Outside; + } + + /// \a metric sizes the band only: a polygon is exact, so its band is the length floor. + WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const + { + return classify(point, trimLengthFloor(metric, point)); + } +}; + +inline bool pointInTriangle(const Vec2& point, const Vec2& firstVertex, const Vec2& secondVertex, + const Vec2& thirdVertex) +{ + const double firstCross = cross2D(secondVertex - firstVertex, point - firstVertex); + const double secondCross = cross2D(thirdVertex - secondVertex, point - secondVertex); + const double thirdCross = cross2D(firstVertex - thirdVertex, point - thirdVertex); + return firstCross >= -kTolerance && secondCross >= -kTolerance && thirdCross >= -kTolerance; +} + +/// Ear-clipping triangulation of a simple (non-self-intersecting) parametric wire. +inline std::vector> triangulateSimpleWire(const SurfaceWire& wire) +{ + std::vector remainingIndices; + remainingIndices.reserve(wire.vertices.size()); + if (wire.signedArea() >= 0.) { + for (size_t vertexIndex = 0; vertexIndex < wire.vertices.size(); ++vertexIndex) { + remainingIndices.push_back(static_cast(vertexIndex)); + } + } else { + for (size_t reverseIndex = wire.vertices.size(); reverseIndex > 0; --reverseIndex) { + remainingIndices.push_back(static_cast(reverseIndex - 1)); + } + } + + std::vector> triangles; + size_t guardCounter = 0; + while (remainingIndices.size() > 3 && guardCounter++ < wire.vertices.size() * wire.vertices.size()) { + bool clippedEar = false; + for (size_t indexPosition = 0; indexPosition < remainingIndices.size(); ++indexPosition) { + const int previousIndex = remainingIndices[(indexPosition + remainingIndices.size() - 1) % remainingIndices.size()]; + const int currentIndex = remainingIndices[indexPosition]; + const int nextIndex = remainingIndices[(indexPosition + 1) % remainingIndices.size()]; + + const auto& previousVertex = wire.vertices[previousIndex]; + const auto& currentVertex = wire.vertices[currentIndex]; + const auto& nextVertex = wire.vertices[nextIndex]; + if (cross2D(currentVertex - previousVertex, nextVertex - currentVertex) <= kTolerance) { + continue; + } + + bool containsOtherVertex = false; + for (int candidateIndex : remainingIndices) { + if (candidateIndex == previousIndex || candidateIndex == currentIndex || candidateIndex == nextIndex) { + continue; + } + if (pointInTriangle(wire.vertices[candidateIndex], previousVertex, currentVertex, nextVertex)) { + containsOtherVertex = true; + break; + } + } + if (containsOtherVertex) { + continue; + } + + triangles.push_back({previousIndex, currentIndex, nextIndex}); + remainingIndices.erase(remainingIndices.begin() + indexPosition); + clippedEar = true; + break; + } + + if (!clippedEar) { + break; + } + } + + if (remainingIndices.size() == 3) { + triangles.push_back({remainingIndices[0], remainingIndices[1], remainingIndices[2]}); + } + return triangles; +} + +/// \name Angular constants for parametric arc curves +/// @{ +inline constexpr double kPi = 3.14159265358979323846; +inline constexpr double kTwoPi = 2. * kPi; +inline constexpr double kHalfPi = 0.5 * kPi; +/// Chords per full-circle arc for display and rims, shared by all surfaces so shared rims match; divisible by 4. +inline constexpr int kArcSamples = 24; +/// @} + +/// Angular tolerance equivalent to a kTolerance arc length at the given radius. +inline double angularTolerance(double radius) +{ + return kTolerance / std::max(radius, kTolerance); +} + +/// Widest angular span of one cover box: pi/4, eight boxes per full turn. +inline constexpr double kCoverChunkAngle = kPi / 4.; + +/// The number of kCoverChunkAngle chunks covering an angular span: at least one, and never more +/// than a full turn takes, since a sweep may overshoot 2pi by a rounding hair. +inline int coverChunkCount(double span) +{ + constexpr int fullTurnChunks = static_cast(kTwoPi / kCoverChunkAngle); // eight + return std::max(1, std::min(fullTurnChunks, static_cast(std::ceil(span / kCoverChunkAngle)))); +} + +/// Exact range of a cos(t) + b sin(t) over [t0, t1], at most a turn: the endpoint values, widened to the amplitude at a crest. +inline void sinusoidRange(double a, double b, double t0, double t1, double& minimum, double& maximum) +{ + const double atStart = a * std::cos(t0) + b * std::sin(t0); + const double atEnd = a * std::cos(t1) + b * std::sin(t1); + minimum = std::min(atStart, atEnd); + maximum = std::max(atStart, atEnd); + const double amplitude = std::hypot(a, b); + const double crest = std::atan2(b, a); + // shifted into [t0, t0 + 2pi), where a span of at most a full turn makes "<= t1" exactly the + // test for falling inside the interval + const double crestInRange = crest - kTwoPi * std::floor((crest - t0) / kTwoPi); + if (crestInRange <= t1) { + maximum = amplitude; + } + const double trough = crest + kPi; + const double troughInRange = trough - kTwoPi * std::floor((trough - t0) / kTwoPi); + if (troughInRange <= t1) { + minimum = -amplitude; + } +} + +/// One end of sinusoidRange, for the doubly swept covers of the sphere and the torus. +/// @{ +inline double sinusoidMinimum(double a, double b, double t0, double t1) +{ + double minimum = 0.; + double maximum = 0.; + sinusoidRange(a, b, t0, t1, minimum, maximum); + return minimum; +} + +inline double sinusoidMaximum(double a, double b, double t0, double t1) +{ + double minimum = 0.; + double maximum = 0.; + sinusoidRange(a, b, t0, t1, minimum, maximum); + return maximum; +} +/// @} + +/// True if \a angle lies within the angular range [start, start + sweep] (sweep in (0, 2pi]), +/// allowing \a tolerance on both ends and treating a >= 2pi sweep as the full circle. +inline bool angleInSweepRange(double angle, double start, double sweep, double tolerance) +{ + if (sweep >= kTwoPi - kTolerance) { + return true; + } + double delta = angle - start; + delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi) + return delta <= sweep + tolerance || delta >= kTwoPi - tolerance; +} + +/// The \a n-point Gauss-Legendre nodes and weights on [-1, 1], by Newton iteration on P_n. +inline void gaussLegendre(int n, std::vector& nodes, std::vector& weights) +{ + nodes.assign(std::max(n, 1), 0.); + weights.assign(std::max(n, 1), 0.); + if (n < 1) { + return; + } + for (int i = 0; i < n; ++i) { + double root = std::cos(kPi * (i + 0.75) / (n + 0.5)); // asymptotic initial guess + double derivative = 1.; + for (int iteration = 0; iteration < 100; ++iteration) { + double previous = 1.; + double current = root; + for (int degreeIndex = 2; degreeIndex <= n; ++degreeIndex) { + const double next = ((2 * degreeIndex - 1) * root * current - (degreeIndex - 1) * previous) / degreeIndex; + previous = current; + current = next; + } + derivative = n * (root * current - previous) / (root * root - 1.); + const double delta = current / derivative; + root -= delta; + if (std::abs(delta) < 1.e-15) { + break; + } + } + nodes[i] = root; + weights[i] = 2. / ((1. - root * root) * derivative * derivative); + } +} + +/// Fill \a roots with the real roots of w^3 + P w + Q = 0 and return their count: Cardano, or the trigonometric form for three. +/// The branch is chosen by the sign of P, not by a tolerance, so every input is covered. +inline int solveDepressedCubic(double coeffP, double coeffQ, std::array& roots) +{ + const double discriminant = coeffQ * coeffQ / 4. + coeffP * coeffP * coeffP / 27.; + if (!(coeffP < 0.) || discriminant > 0.) { + const double sqrtDiscriminant = std::sqrt(std::max(0., discriminant)); + roots[0] = std::cbrt(-0.5 * coeffQ + sqrtDiscriminant) + std::cbrt(-0.5 * coeffQ - sqrtDiscriminant); + return 1; + } + // three real roots: coeffP < 0 here, so the trigonometric form is well defined + const double magnitude = 2. * std::sqrt(-coeffP / 3.); + const double cosineArgument = std::max(-1., std::min(1., 3. * coeffQ / (coeffP * magnitude))); + const double baseAngle = std::acos(cosineArgument); + for (int branch = 0; branch < 3; ++branch) { + roots[branch] = magnitude * std::cos((baseAngle - kTwoPi * branch) / 3.); + } + return 3; +} + +/// Which of solveQuarticReal's branches produced its roots, for the tests. +enum class QuarticBranch { + NotAQuartic, ///< the leading coefficient vanishes; no roots are produced + Biquadratic, ///< the depressed quartic's odd term is zero, so y^4 + p y^2 + r = 0 is solved directly + Resolvent ///< Ferrari's general branch, through the resolvent cubic +}; + +/// The real roots of a quartic: at most four, held inline. +struct QuarticRoots { + std::array value{}; + int count = 0; + void push_back(double root) + { + assert(count < 4 && "QuarticRoots holds at most four roots"); + value[count++] = root; + } + double* begin() { return value.data(); } + double* end() { return value.data() + count; } + const double* begin() const { return value.data(); } + const double* end() const { return value.data() + count; } + size_t size() const { return static_cast(count); } + bool empty() const { return count == 0; } + double operator[](size_t index) const { return value[index]; } +}; + +/// Real roots of a4 x^4 + a3 x^3 + a2 x^2 + a1 x + a0 = 0 (a4 != 0) by Ferrari's method and Newton polishing; a tangential root is a near-equal pair. +/// The root variable is first rescaled by a power of two, exactly, so all branch tests are dimensionless; \a takenBranch reports the branch. +inline QuarticRoots solveQuarticReal(double a4, double a3, double a2, double a1, double a0, + QuarticBranch* takenBranch = nullptr) +{ + const auto note = [takenBranch](QuarticBranch branch) { + if (takenBranch) { + *takenBranch = branch; + } + }; + note(QuarticBranch::NotAQuartic); + QuarticRoots roots; + // A genuine quartic needs only a non-zero leading coefficient. There is no scale to compare it + // against -- the normalisation below handles any coefficient ratio -- so the test is exact. + if (!(std::abs(a4) > 0.)) { + return roots; // the torus caller guarantees a4 = |dir|^4 > 0 + } + // monic x^4 + b x^3 + c x^2 + d x + e + double coeffB = a3 / a4, coeffC = a2 / a4, coeffD = a1 / a4, coeffE = a0 / a4; + if (!std::isfinite(coeffB) || !std::isfinite(coeffC) || !std::isfinite(coeffD) || !std::isfinite(coeffE)) { + return roots; // a4 is denormal-small next to the rest, or an input was not finite + } + // Cauchy root bound rounded up to a power of two, so x = scale * y is exact; x^4 = 0 keeps scale = 1 + const double rootBound = std::max({std::abs(coeffB), std::sqrt(std::abs(coeffC)), + std::cbrt(std::abs(coeffD)), std::sqrt(std::sqrt(std::abs(coeffE)))}); + int boundExponent = 0; + std::frexp(rootBound, &boundExponent); + const double scale = std::ldexp(1., boundExponent); + coeffB /= scale; + coeffC /= scale * scale; + coeffD /= scale * scale * scale; + coeffE /= scale * scale * scale * scale; + + // depress with y = z - b/4: z^4 + p z^2 + q z + r + const double termP = coeffC - 3. * coeffB * coeffB / 8.; + const double termQ = coeffD - coeffB * coeffC / 2. + coeffB * coeffB * coeffB / 8.; + const double termR = + coeffE - coeffB * coeffD / 4. + coeffB * coeffB * coeffC / 16. - 3. * coeffB * coeffB * coeffB * coeffB / 256.; + const double shift = -coeffB / 4.; + + auto addQuadraticRoots = [&](double quadB, double quadC) { + const double discriminant = quadB * quadB - 4. * quadC; + if (discriminant < 0.) { + return; // complex pair + } + const double sqrtDiscriminant = std::sqrt(discriminant); + roots.push_back(shift + 0.5 * (-quadB - sqrtDiscriminant)); + roots.push_back(shift + 0.5 * (-quadB + sqrtDiscriminant)); + }; + + auto addBiquadraticRoots = [&]() { + // biquadratic z^4 + p z^2 + r = 0 + const double discriminant = termP * termP - 4. * termR; + if (discriminant < 0.) { + return; + } + const double sqrtDiscriminant = std::sqrt(discriminant); + for (const double zSquared : {0.5 * (-termP + sqrtDiscriminant), 0.5 * (-termP - sqrtDiscriminant)}) { + if (zSquared >= 0.) { + const double z = std::sqrt(zSquared); + roots.push_back(shift + z); + roots.push_back(shift - z); + } + } + }; + + // q is zero to the precision of its terms, which normalisation bounds by 1: kQuarticEpsilon over the whole quartic, not over q's terms + bool biquadratic = std::abs(termQ) <= kQuarticEpsilon; + if (!biquadratic) { + note(QuarticBranch::Resolvent); + // resolvent cubic m^3 + p m^2 + (p^2/4 - r) m - q^2/8 = 0; its largest real root is > 0 + const double cubicA2 = termP; + const double cubicA1 = termP * termP / 4. - termR; + const double cubicA0 = -termQ * termQ / 8.; + const double cubicP = cubicA1 - cubicA2 * cubicA2 / 3.; + const double cubicQ = 2. * cubicA2 * cubicA2 * cubicA2 / 27. - cubicA2 * cubicA1 / 3. + cubicA0; + std::array cubicRoots; + const int cubicCount = solveDepressedCubic(cubicP, cubicQ, cubicRoots); + double resolvent = 0.; + for (int index = 0; index < cubicCount; ++index) { + resolvent = std::max(resolvent, cubicRoots[index] - cubicA2 / 3.); + } + // a resolvent below the resolution of its cubic is noise; then the biquadratic branch is the better-conditioned answer + const double resolventScale = std::max({std::abs(cubicA2), std::sqrt(std::abs(cubicA1)), + std::cbrt(std::abs(cubicA0))}); + if (resolvent > kQuarticEpsilon * resolventScale) { + const double sqrtTwoResolvent = std::sqrt(2. * resolvent); + const double linearTerm = sqrtTwoResolvent * termQ / (4. * resolvent); + addQuadraticRoots(-sqrtTwoResolvent, termP / 2. + resolvent + linearTerm); + addQuadraticRoots(sqrtTwoResolvent, termP / 2. + resolvent - linearTerm); + } else { + biquadratic = true; + } + } + if (biquadratic) { + note(QuarticBranch::Biquadratic); + addBiquadraticRoots(); + } + + // Newton polish against the monic quartic; a step longer than the Cauchy bound 2, or non-finite, is rejected + auto quartic = [&](double x) { return (((x + coeffB) * x + coeffC) * x + coeffD) * x + coeffE; }; + auto quarticDerivative = [&](double x) { return ((4. * x + 3. * coeffB) * x + 2. * coeffC) * x + coeffD; }; + for (double& root : roots) { + for (int iteration = 0; iteration < 2; ++iteration) { + const double step = quartic(root) / quarticDerivative(root); + if (std::isfinite(step) && std::abs(step) <= 2.) { + root -= step; + } + } + } + for (double& root : roots) { + root *= scale; // exact: scale is a power of two + } + return roots; +} + +/// Kind of a 2D trimmed boundary curve. +enum class CurveKind { Line, ///< straight line segment + Arc, ///< circular arc + BSpline ///< clamped (rational) B-spline curve +}; + +/// One trimmed boundary curve in a surface's (u, v) domain: a line segment, a circular arc or a clamped (rational) B-spline. +struct Curve2D { + CurveKind kind = CurveKind::Line; + Vec2 lineStart; ///< line: start point (unused for arcs) + Vec2 lineEnd; ///< line: end point (unused for arcs) + Vec2 center; ///< arc: circle centre (unused for lines) + double radius = 0.; ///< arc: circle radius + double startAngle = 0.; ///< arc: start angle [rad] + double endAngle = 0.; ///< arc: end angle [rad] (sweep = endAngle - startAngle) + + /// \name B-spline data (kind == BSpline): poles, optional weights and a clamped knot vector; the curve parameter runs on [0, 1]. @{ + int degree = 0; + std::vector poles; + std::vector weights; + std::vector knots; + /// The flattened on-curve polyline, both ends included; CurveWire::initialize fills it and reversing clears it. + mutable std::vector bsplineCache; + /// @} + + /// \name Loop-canonical endpoints: the seam vertices the curve's neighbours agree on, substituted at the polyline's ends + /// @{ + Vec2 canonicalStart; + Vec2 canonicalEnd; + bool hasCanonicalEndpoints = false; + + void setCanonicalEndpoints(const Vec2& start, const Vec2& end) + { + canonicalStart = start; + canonicalEnd = end; + hasCanonicalEndpoints = true; + bsplineCache.clear(); // the polyline carries them, so it has to be rebuilt + } + + /// Where this curve begins and ends as far as the loop is concerned: the canonical seam vertex + /// when a wire has fixed one, and the curve's own endpoint when it stands alone. + Vec2 loopStart() const { return hasCanonicalEndpoints ? canonicalStart : startPoint(); } + Vec2 loopEnd() const { return hasCanonicalEndpoints ? canonicalEnd : endPoint(); } + /// @} + + static Curve2D makeLine(const Vec2& start, const Vec2& end) + { + Curve2D curve; + curve.kind = CurveKind::Line; + curve.lineStart = start; + curve.lineEnd = end; + return curve; + } + + static Curve2D makeArc(const Vec2& arcCenter, double arcRadius, double arcStartAngle, double arcEndAngle) + { + Curve2D curve; + curve.kind = CurveKind::Arc; + curve.center = arcCenter; + curve.radius = arcRadius; + curve.startAngle = arcStartAngle; + curve.endAngle = arcEndAngle; + return curve; + } + + /// Full circle as one arc curve (counter-clockwise unless \a clockwise is set). + static Curve2D makeCircle(const Vec2& arcCenter, double arcRadius, bool clockwise = false) + { + return makeArc(arcCenter, arcRadius, 0., clockwise ? -kTwoPi : kTwoPi); + } + + /// Clamped (rational) B-spline curve of degree \a splineDegree. \a splineWeights may be empty + /// for a non-rational curve; \a splineKnots must be the clamped flat knot vector. + static Curve2D makeBSpline(int splineDegree, std::vector splinePoles, + std::vector splineWeights, std::vector splineKnots) + { + Curve2D curve; + curve.kind = CurveKind::BSpline; + curve.degree = splineDegree; + curve.poles = std::move(splinePoles); + curve.weights = std::move(splineWeights); + curve.knots = std::move(splineKnots); + return curve; + } + + bool isArc() const { return kind == CurveKind::Arc; } + bool isBSpline() const { return kind == CurveKind::BSpline; } + + double sweep() const { return endAngle - startAngle; } + + /// \name B-spline evaluation helpers (kind == BSpline) + /// @{ + double bsplineT0() const { return knots[degree]; } + double bsplineT1() const { return knots[poles.size()]; } + + /// True when the knot vector is clamped, so the curve interpolates its first and last pole. + bool bsplineIsClamped() const + { + const size_t lastKnot = knots.size() - 1; + for (int offset = 1; offset <= degree; ++offset) { + if (std::abs(knots[offset] - knots[0]) > kTolerance || + std::abs(knots[lastKnot - offset] - knots[lastKnot]) > kTolerance) { + return false; + } + } + return true; + } + + /// True if the curve carries non-unit weights (a rational B-spline). + bool bsplineRational() const + { + for (double weight : weights) { + if (std::abs(weight - 1.) > kTolerance) { + return true; + } + } + return false; + } + + /// Knot span index of parameter \a knotValue for the clamped knot vector. + int bsplineSpan(double knotValue) const + { + const int lastPole = static_cast(poles.size()) - 1; + if (knotValue >= knots[lastPole + 1]) { + return lastPole; + } + if (knotValue <= knots[degree]) { + return degree; + } + int low = degree; + int high = lastPole + 1; + int mid = (low + high) / 2; + while (knotValue < knots[mid] || knotValue >= knots[mid + 1]) { + if (knotValue < knots[mid]) { + high = mid; + } else { + low = mid; + } + mid = (low + high) / 2; + } + return mid; + } + + /// Non-zero degree-p basis functions and first derivatives at \a knotValue in \a span (The NURBS Book, DersBasisFuns). + void bsplineBasis(int span, double knotValue, std::vector& basis, + std::vector& basisDeriv) const + { + const int p = degree; + std::vector> ndu(p + 1, std::vector(p + 1, 0.)); + std::vector left(p + 1, 0.); + std::vector right(p + 1, 0.); + ndu[0][0] = 1.; + for (int j = 1; j <= p; ++j) { + left[j] = knotValue - knots[span + 1 - j]; + right[j] = knots[span + j] - knotValue; + double saved = 0.; + for (int r = 0; r < j; ++r) { + ndu[j][r] = right[r + 1] + left[j - r]; + const double temp = ndu[r][j - 1] / ndu[j][r]; + ndu[r][j] = saved + right[r + 1] * temp; + saved = left[j - r] * temp; + } + ndu[j][j] = saved; + } + basis.assign(p + 1, 0.); + basisDeriv.assign(p + 1, 0.); + for (int j = 0; j <= p; ++j) { + basis[j] = ndu[j][p]; + } + // first derivative (specialization of DersBasisFuns for the k = 1 term) + for (int r = 0; r <= p; ++r) { + double d = 0.; + const int pk = p - 1; + if (r >= 1) { + d += (1. / ndu[pk + 1][r - 1]) * ndu[r - 1][pk]; + } + if (r <= pk) { + d += (-1. / ndu[pk + 1][r]) * ndu[r][pk]; + } + basisDeriv[r] = d * p; + } + } + + /// Evaluate the (rational) B-spline point \a pointOut and its knot-parameter derivative + /// \a derivativeOut at knot parameter \a knotValue. + void bsplineEval(double knotValue, Vec2& pointOut, Vec2& derivativeOut) const + { + const int p = degree; + const int span = bsplineSpan(knotValue); + std::vector basis; + std::vector basisDeriv; + bsplineBasis(span, knotValue, basis, basisDeriv); + Vec2 weightedSum{0., 0.}; + Vec2 weightedDeriv{0., 0.}; + double weightTotal = 0.; + double weightDeriv = 0.; + for (int j = 0; j <= p; ++j) { + const int idx = span - p + j; + const double weight = weights.empty() ? 1. : weights[idx]; + weightedSum.uCoord += basis[j] * weight * poles[idx].uCoord; + weightedSum.vCoord += basis[j] * weight * poles[idx].vCoord; + weightTotal += basis[j] * weight; + weightedDeriv.uCoord += basisDeriv[j] * weight * poles[idx].uCoord; + weightedDeriv.vCoord += basisDeriv[j] * weight * poles[idx].vCoord; + weightDeriv += basisDeriv[j] * weight; + } + const double invWeight = (std::abs(weightTotal) > kTolerance) ? 1. / weightTotal : 0.; + pointOut = {weightedSum.uCoord * invWeight, weightedSum.vCoord * invWeight}; + derivativeOut = {(weightedDeriv.uCoord * weightTotal - weightedSum.uCoord * weightDeriv) * invWeight * invWeight, + (weightedDeriv.vCoord * weightTotal - weightedSum.vCoord * weightDeriv) * invWeight * invWeight}; + } + + /// B-spline point at curve parameter \a parameter in [0, 1]. + Vec2 bsplinePointAt(double parameter) const + { + const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0()); + Vec2 point; + Vec2 derivative; + bsplineEval(knotValue, point, derivative); + return point; + } + + /// Adaptively sample the B-spline into an on-curve polyline, subdividing until each chord is flat to sqrt(\a flatnessSq). + void bsplineSampleInto(std::vector& samples, double flatnessSq = kBSplineFlatnessSq, + int maxDepth = 16) const + { + const double t0 = bsplineT0(); + const double t1 = bsplineT1(); + Vec2 startPointValue; + Vec2 endPointValue; + Vec2 unusedDerivative; + bsplineEval(t0, startPointValue, unusedDerivative); + bsplineEval(t1, endPointValue, unusedDerivative); + samples.push_back(startPointValue); + bsplineSampleRecursive(t0, t1, startPointValue, endPointValue, flatnessSq, maxDepth, samples); + } + + /// Whether a knot lies strictly inside (\a lowT, \a highT); such an interval is never called flat. + bool spansInteriorKnot(double lowT, double highT) const + { + // a clamped knot vector repeats its ends degree+1 times, so the interior knots are the + // entries [degree + 1, poles.size()); a single-span (Bezier) curve has none + const size_t firstInterior = static_cast(degree) + 1; + const size_t endInterior = std::min(poles.size(), knots.size()); + if (firstInterior >= endInterior) { + return false; + } + const auto begin = knots.begin() + static_cast(firstInterior); + const auto end = knots.begin() + static_cast(endInterior); + const auto firstAbove = std::upper_bound(begin, end, lowT); + return firstAbove != end && *firstAbove < highT; + } + + /// Append the interior knots in (\a from, \a to), in the curve's [0, 1] parameter; none for a line or an arc. + void appendInteriorKnots(double from, double to, std::vector& breakpoints) const + { + if (kind != CurveKind::BSpline) { + return; + } + const double t0 = bsplineT0(); + const double span = bsplineT1() - t0; + if (!(span > 0.)) { + return; + } + const size_t firstInterior = static_cast(degree) + 1; + const size_t endInterior = std::min(poles.size(), knots.size()); + for (size_t index = firstInterior; index < endInterior; ++index) { + const double parameter = (knots[index] - t0) / span; + if (parameter > from && parameter < to) { + breakpoints.push_back(parameter); + } + } + } + + /// An upper bound on how far u travels along the curve between \a from and \a to. + double uVariation(double from, double to) const + { + if (kind == CurveKind::Line) { + return std::abs(lineEnd.uCoord - lineStart.uCoord) * std::abs(to - from); + } + if (kind == CurveKind::BSpline) { + // within one knot span the curve lies in the hull of its degree + 1 poles, so their u spread bounds the travel + const double knotStart = bsplineT0(); + const double knotSpan = bsplineT1() - knotStart; + const double knotMid = knotStart + 0.5 * (from + to) * knotSpan; + size_t spanIndex = static_cast(degree); + while (spanIndex + 1 < poles.size() && spanIndex + 1 < knots.size() && knots[spanIndex + 1] <= knotMid) { + ++spanIndex; + } + const size_t firstPole = spanIndex - static_cast(degree); + double lowU = std::numeric_limits::infinity(); + double highU = -std::numeric_limits::infinity(); + for (size_t index = firstPole; index <= spanIndex && index < poles.size(); ++index) { + lowU = std::min(lowU, poles[index].uCoord); + highU = std::max(highU, poles[index].uCoord); + } + return (highU >= lowU) ? (highU - lowU) : 0.; + } + // arc: u(angle) = center.u + radius cos(angle), so u turns exactly at angle = 0 and pi (mod + // 2 pi). Sum the monotone runs between those turning points and the interval's own ends. + const double angleFrom = startAngle + from * sweep(); + const double angleTo = startAngle + to * sweep(); + const double low = std::min(angleFrom, angleTo); + const double high = std::max(angleFrom, angleTo); + double variation = 0.; + double previous = low; + const double firstTurn = std::ceil(low / kPi) * kPi; + for (double turn = firstTurn; turn < high; turn += kPi) { + variation += std::abs(radius * (std::cos(turn) - std::cos(previous))); + previous = turn; + } + return variation + std::abs(radius * (std::cos(high) - std::cos(previous))); + } + + void bsplineSampleRecursive(double t0, double t1, const Vec2& p0, const Vec2& p1, double flatnessSq, + int depth, std::vector& samples) const + { + const double tMid = 0.5 * (t0 + t1); + Vec2 midPoint; + Vec2 unusedDerivative; + bsplineEval(tMid, midPoint, unusedDerivative); + // a degenerate (closed) chord must not end the recursion: test the distance to its single point instead + const bool degenerateChord = surface::distanceSq(p0, p1) <= flatnessSq; + const auto deviationSq = [&](const Vec2& point) { + return degenerateChord ? surface::distanceSq(point, p0) : pointSegmentDistanceSq(point, p0, p1); + }; + // Three interior probes: a single midpoint probe is blind to curves symmetric about their parameter midpoint. + double flatness = deviationSq(midPoint); + for (const double fraction : {0.25, 0.75}) { + Vec2 probePoint; + bsplineEval(t0 + (t1 - t0) * fraction, probePoint, unusedDerivative); + flatness = std::max(flatness, deviationSq(probePoint)); + } + if (depth <= 0 || (flatness <= flatnessSq && !spansInteriorKnot(t0, t1))) { + samples.push_back(p1); + return; + } + bsplineSampleRecursive(t0, tMid, p0, midPoint, flatnessSq, depth - 1, samples); + bsplineSampleRecursive(tMid, t1, midPoint, p1, flatnessSq, depth - 1, samples); + } + + /// The flattened polyline in \a bsplineCache, computed here if the wire has not filled it. + const std::vector& bsplineSamples() const + { + if (bsplineCache.empty()) { + bsplineSampleInto(bsplineCache); + // one canonical polyline, with the seam vertices substituted at its ends + if (hasCanonicalEndpoints && bsplineCache.size() >= 2) { + bsplineCache.front() = canonicalStart; + bsplineCache.back() = canonicalEnd; + } + } + return bsplineCache; + } + /// @} + + /// Basic structural validity (finite data, positive radius for arcs, well-formed clamped knot + /// vector for B-splines). + bool valid() const + { + if (kind == CurveKind::Line) { + return finite(lineStart) && finite(lineEnd); + } + if (kind == CurveKind::Arc) { + return finite(center) && std::isfinite(radius) && radius > kTolerance && std::isfinite(startAngle) && + std::isfinite(endAngle); + } + // B-spline + const int nPoles = static_cast(poles.size()); + if (degree < 1 || nPoles < degree + 1) { + return false; + } + if (static_cast(knots.size()) != nPoles + degree + 1) { + return false; + } + if (!weights.empty() && static_cast(weights.size()) != nPoles) { + return false; + } + for (const auto& pole : poles) { + if (!finite(pole)) { + return false; + } + } + for (double weight : weights) { + if (!std::isfinite(weight) || weight <= kTolerance) { + return false; + } + } + for (size_t index = 1; index < knots.size(); ++index) { + if (!std::isfinite(knots[index]) || knots[index] < knots[index - 1] - kTolerance) { + return false; + } + } + return bsplineT1() - bsplineT0() > kTolerance; + } + + Vec2 pointAtAngle(double angle) const + { + return {center.uCoord + radius * std::cos(angle), center.vCoord + radius * std::sin(angle)}; + } + + /// Point at curve parameter \a parameter in [0, 1] (0 at the start, 1 at the end). + Vec2 pointAt(double parameter) const + { + if (kind == CurveKind::Line) { + return {lineStart.uCoord + parameter * (lineEnd.uCoord - lineStart.uCoord), + lineStart.vCoord + parameter * (lineEnd.vCoord - lineStart.vCoord)}; + } + if (kind == CurveKind::BSpline) { + return bsplinePointAt(parameter); + } + return pointAtAngle(startAngle + parameter * sweep()); + } + + Vec2 startPoint() const + { + if (kind == CurveKind::Line) { + return lineStart; + } + if (kind == CurveKind::BSpline) { + // a clamped knot vector interpolates its first pole exactly; anything else has to be evaluated + return bsplineIsClamped() ? poles.front() : bsplinePointAt(0.); + } + return pointAtAngle(startAngle); + } + Vec2 endPoint() const + { + if (kind == CurveKind::Line) { + return lineEnd; + } + if (kind == CurveKind::BSpline) { + return bsplineIsClamped() ? poles.back() : bsplinePointAt(1.); + } + return pointAtAngle(endAngle); + } + + /// dC/dt at \a parameter in [0, 1], unnormalised; tangentAt() is it normalised. + Vec2 derivativeAt(double parameter) const + { + if (kind == CurveKind::Line) { + return {lineEnd.uCoord - lineStart.uCoord, lineEnd.vCoord - lineStart.vCoord}; + } + if (kind == CurveKind::BSpline) { + const double span = bsplineT1() - bsplineT0(); + Vec2 point; + Vec2 derivative; + bsplineEval(bsplineT0() + parameter * span, point, derivative); + return {derivative.uCoord * span, derivative.vCoord * span}; + } + const double angle = startAngle + parameter * sweep(); + return {-radius * std::sin(angle) * sweep(), radius * std::cos(angle) * sweep()}; + } + + /// Unit tangent at parameter \a parameter, pointing in the direction of increasing parameter. + Vec2 tangentAt(double parameter) const + { + if (kind == CurveKind::Line) { + const Vec2 delta{lineEnd.uCoord - lineStart.uCoord, lineEnd.vCoord - lineStart.vCoord}; + const double length = std::sqrt(delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord); + if (length <= kTolerance) { + return {0., 0.}; + } + return {delta.uCoord / length, delta.vCoord / length}; + } + if (kind == CurveKind::BSpline) { + // dC/dt scaled by the positive constant dt/ds, so the normalized direction is unchanged + const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0()); + Vec2 point; + Vec2 derivative; + bsplineEval(knotValue, point, derivative); + const double length = std::sqrt(derivative.uCoord * derivative.uCoord + derivative.vCoord * derivative.vCoord); + if (length <= kTolerance) { + return {0., 0.}; + } + return {derivative.uCoord / length, derivative.vCoord / length}; + } + const double angle = startAngle + parameter * sweep(); + const double direction = sweep() >= 0. ? 1. : -1.; + return {-direction * std::sin(angle), direction * std::cos(angle)}; + } + + /// True if \a angle lies within the arc's angular sweep (accounting for direction and wrap). + bool angleInSweep(double angle) const + { + const double totalSweep = sweep(); + const double magnitude = std::abs(totalSweep); + if (magnitude >= kTwoPi - kTolerance) { + return true; // full circle + } + double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle); + delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi) + return delta <= magnitude + kTolerance; + } + + /// Map an angle known to lie within the sweep to a clamped parameter in [0, 1]. + double angleParameter(double angle) const + { + const double totalSweep = sweep(); + if (std::abs(totalSweep) <= kTolerance) { + return 0.; + } + double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle); + delta -= kTwoPi * std::floor(delta / kTwoPi); + return std::max(0., std::min(1., delta / std::abs(totalSweep))); + } + + /// Accumulate this curve's exact extent into a parametric axis-aligned bounding box. + void extendBounds(Vec2& lower, Vec2& upper) const + { + auto include = [&](const Vec2& point) { + lower.uCoord = std::min(lower.uCoord, point.uCoord); + lower.vCoord = std::min(lower.vCoord, point.vCoord); + upper.uCoord = std::max(upper.uCoord, point.uCoord); + upper.vCoord = std::max(upper.vCoord, point.vCoord); + }; + if (kind == CurveKind::BSpline) { + // the control-point convex hull contains the curve, so its box is a conservative (exact + // upper bound) parametric AABB — consistent with the BVH's conservative-box philosophy + for (const auto& pole : poles) { + include(pole); + } + return; + } + includeAnalyticExtremes(include); + } + + /// As extendBounds, measured on the curve: a B-spline contributes its sampled polyline, not its pole hull. + void extendTightBounds(Vec2& lower, Vec2& upper) const + { + auto include = [&](const Vec2& point) { + lower.uCoord = std::min(lower.uCoord, point.uCoord); + lower.vCoord = std::min(lower.vCoord, point.vCoord); + upper.uCoord = std::max(upper.uCoord, point.uCoord); + upper.vCoord = std::max(upper.vCoord, point.vCoord); + }; + if (kind == CurveKind::BSpline) { + for (const auto& sample : bsplineSamples()) { + include(sample); + } + return; + } + includeAnalyticExtremes(include); + } + + /// Endpoints plus an arc's axis-extreme points inside the sweep: the exact extent of a line or an arc. + template + void includeAnalyticExtremes(const Include& include) const + { + include(startPoint()); + include(endPoint()); + if (kind == CurveKind::Arc) { + // include the axis-extreme points (angles 0, pi/2, pi, 3pi/2) that fall within the sweep + const double cardinalAngles[4] = {0., kHalfPi, kPi, 3. * kHalfPi}; + for (double cardinal : cardinalAngles) { + if (angleInSweep(cardinal)) { + include(pointAtAngle(cardinal)); + } + } + } + } + + /// Closest point on the curve to \a point, returning the clamped parameter in \a parameter. + Vec2 closestPoint(const Vec2& point, double& parameter) const + { + if (kind == CurveKind::BSpline) { + // distance to the cached polyline, accurate to the sampling flatness + const auto& polyline = bsplineSamples(); + if (polyline.size() < 2) { + parameter = 0.; + return startPoint(); + } + const int segments = static_cast(polyline.size()) - 1; + double bestDistanceSq = std::numeric_limits::infinity(); + Vec2 bestPoint = polyline.front(); + double bestParameter = 0.; + for (int index = 0; index < segments; ++index) { + const Vec2 segmentStart = polyline[index]; + const Vec2 segmentVector = polyline[index + 1] - segmentStart; + const double segmentLengthSq = + segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord; + double projection = 0.; + if (segmentLengthSq > kToleranceSq) { + projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord + + (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) / + segmentLengthSq; + projection = std::max(0., std::min(1., projection)); + } + const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord, + segmentStart.vCoord + projection * segmentVector.vCoord}; + const double candidateDistanceSq = surface::distanceSq(point, candidate); + if (candidateDistanceSq < bestDistanceSq) { + bestDistanceSq = candidateDistanceSq; + bestPoint = candidate; + bestParameter = (index + projection) / segments; + } + } + parameter = bestParameter; + return bestPoint; + } + if (kind == CurveKind::Line) { + const Vec2 segment{lineEnd.uCoord - lineStart.uCoord, lineEnd.vCoord - lineStart.vCoord}; + const double lengthSq = segment.uCoord * segment.uCoord + segment.vCoord * segment.vCoord; + if (lengthSq <= kToleranceSq) { + parameter = 0.; + return lineStart; + } + const double projection = ((point.uCoord - lineStart.uCoord) * segment.uCoord + + (point.vCoord - lineStart.vCoord) * segment.vCoord) / + lengthSq; + parameter = std::max(0., std::min(1., projection)); + return {lineStart.uCoord + parameter * segment.uCoord, lineStart.vCoord + parameter * segment.vCoord}; + } + // arc: project radially onto the circle, then clamp the angle to the sweep + const double deltaU = point.uCoord - center.uCoord; + const double deltaV = point.vCoord - center.vCoord; + if (deltaU * deltaU + deltaV * deltaV <= kToleranceSq) { + parameter = 0.; // point at the centre: every arc point is equidistant + return startPoint(); + } + const double angle = std::atan2(deltaV, deltaU); + if (angleInSweep(angle)) { + parameter = angleParameter(angle); + return pointAtAngle(angle); + } + const Vec2 startCandidate = startPoint(); + const Vec2 endCandidate = endPoint(); + if (surface::distanceSq(point, startCandidate) <= surface::distanceSq(point, endCandidate)) { + parameter = 0.; + return startCandidate; + } + parameter = 1.; + return endCandidate; + } + + /// Squared distance from \a point to the curve. + double distanceSq(const Vec2& point) const + { + double parameter = 0.; + return surface::distanceSq(point, closestPoint(point, parameter)); + } + + /// Exact contribution of this directed curve to the enclosed signed area, + /// i.e. (1/2) * integral of (u dv - v du) along the curve (Green's theorem). + double signedAreaContribution() const + { + if (kind == CurveKind::Line) { + return 0.5 * (lineStart.uCoord * lineEnd.vCoord - lineEnd.uCoord * lineStart.vCoord); + } + if (kind == CurveKind::BSpline) { + // Green's area per knot span by Gauss-Legendre: exact for a non-rational span, approximate for a rational one + const int p = degree; + const int order = bsplineRational() ? std::max(2 * p + 2, 8) : (p + 1); + std::vector nodes; + std::vector nodeWeights; + gaussLegendre(order, nodes, nodeWeights); + double area = 0.; + const int lastSpan = static_cast(poles.size()) - 1; + for (int spanIndex = p; spanIndex <= lastSpan; ++spanIndex) { + const double spanLow = knots[spanIndex]; + const double spanHigh = knots[spanIndex + 1]; + const double halfSpan = 0.5 * (spanHigh - spanLow); + if (halfSpan <= kTolerance) { + continue; + } + const double spanMid = 0.5 * (spanLow + spanHigh); + for (int nodeIndex = 0; nodeIndex < order; ++nodeIndex) { + const double knotValue = spanMid + halfSpan * nodes[nodeIndex]; + Vec2 point; + Vec2 derivative; + bsplineEval(knotValue, point, derivative); + area += 0.5 * (point.uCoord * derivative.vCoord - point.vCoord * derivative.uCoord) * + nodeWeights[nodeIndex] * halfSpan; + } + } + return area; + } + return 0.5 * (radius * center.uCoord * (std::sin(endAngle) - std::sin(startAngle)) - + radius * center.vCoord * (std::cos(endAngle) - std::cos(startAngle)) + + radius * radius * (endAngle - startAngle)); + } + + /// How far this curve's representation can sit from the curve, in parametric units: kBSplineFlatness for a B-spline, else 0. + double representationTolerance() const { return kind == CurveKind::BSpline ? kBSplineFlatness : 0.; } + + /// B-spline only: true if \a point is within sqrt(\a bandSq) of the polyline, else adds its rightward crossings. + /// One walk of the polyline with the arithmetic of closestPoint and rightwardCrossings. + bool bsplineBandOrCrossings(const Vec2& point, double bandSq, int& crossings) const + { + const auto& polyline = bsplineSamples(); + if (polyline.size() < 2) { + return surface::distanceSq(point, startPoint()) <= bandSq; + } + int found = 0; + for (size_t index = 0; index + 1 < polyline.size(); ++index) { + const Vec2 segmentStart = polyline[index]; + const Vec2 segmentEnd = polyline[index + 1]; + const Vec2 segmentVector = segmentEnd - segmentStart; + const double segmentLengthSq = + segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord; + double projection = 0.; + if (segmentLengthSq > kToleranceSq) { + projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord + + (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) / + segmentLengthSq; + projection = std::max(0., std::min(1., projection)); + } + const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord, + segmentStart.vCoord + projection * segmentVector.vCoord}; + if (surface::distanceSq(point, candidate) <= bandSq) { + return true; + } + const bool firstAbove = segmentStart.vCoord > point.vCoord; + const bool secondAbove = segmentEnd.vCoord > point.vCoord; + if (firstAbove != secondAbove) { + const double intersectU = + segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) * (segmentEnd.uCoord - segmentStart.uCoord) / + (segmentEnd.vCoord - segmentStart.vCoord); + if (point.uCoord < intersectU) { + ++found; + } + } + } + crossings += found; + return false; + } + + /// Rightward crossings of a horizontal ray from \a point, with the caller's canonical endpoints so that seams stay consistent. + int rightwardCrossings(const Vec2& point, const Vec2& canonicalStart, const Vec2& canonicalEnd) const + { + auto segmentCrossing = [&](const Vec2& first, const Vec2& second, double exactIntersectU) { + const bool firstAbove = first.vCoord > point.vCoord; + const bool secondAbove = second.vCoord > point.vCoord; + if (firstAbove == secondAbove) { + return false; + } + return point.uCoord < exactIntersectU; + }; + + if (kind == CurveKind::Line) { + const bool firstAbove = canonicalStart.vCoord > point.vCoord; + const bool secondAbove = canonicalEnd.vCoord > point.vCoord; + if (firstAbove == secondAbove) { + return 0; + } + const double intersectU = canonicalStart.uCoord + (point.vCoord - canonicalStart.vCoord) * + (canonicalEnd.uCoord - canonicalStart.uCoord) / + (canonicalEnd.vCoord - canonicalStart.vCoord); + return (point.uCoord < intersectU) ? 1 : 0; + } + + if (kind == CurveKind::BSpline) { + // the lines' half-open segment-crossing rule over the polyline, whose ends are the canonical seam vertices + const auto& polyline = bsplineSamples(); + if (polyline.size() < 2) { + return 0; + } + int crossings = 0; + for (size_t index = 0; index + 1 < polyline.size(); ++index) { + // No substitution here: the polyline already ends on the loop-canonical vertices (see + // setCanonicalEndpoints), so this is the same boundary closestPoint measures against. + const Vec2 first = polyline[index]; + const Vec2 second = polyline[index + 1]; + const bool firstAbove = first.vCoord > point.vCoord; + const bool secondAbove = second.vCoord > point.vCoord; + if (firstAbove == secondAbove) { + continue; + } + const double intersectU = + first.uCoord + (point.vCoord - first.vCoord) * (second.uCoord - first.uCoord) / + (second.vCoord - first.vCoord); + if (point.uCoord < intersectU) { + ++crossings; + } + } + return crossings; + } + + // split the arc into v-monotonic sub-arcs at its extreme angles, where the crossing u is exact + const double totalSweep = sweep(); + if (std::abs(totalSweep) <= kTolerance || radius <= kTolerance) { + return 0; + } + std::array breakParameters{}; + int breakCount = 0; + breakParameters[breakCount++] = 0.; + const double lowAngle = std::min(startAngle, endAngle); + const double highAngle = std::max(startAngle, endAngle); + const int firstK = static_cast(std::floor((lowAngle - kHalfPi) / kPi)) - 1; + const int lastK = static_cast(std::ceil((highAngle - kHalfPi) / kPi)) + 1; + for (int k = firstK; k <= lastK && breakCount < 7; ++k) { + const double extremeAngle = kHalfPi + k * kPi; + if (extremeAngle <= lowAngle + kTolerance || extremeAngle >= highAngle - kTolerance) { + continue; + } + const double extremeParameter = (extremeAngle - startAngle) / totalSweep; + if (extremeParameter > kTolerance && extremeParameter < 1. - kTolerance) { + breakParameters[breakCount++] = extremeParameter; + } + } + breakParameters[breakCount++] = 1.; + std::sort(breakParameters.begin(), breakParameters.begin() + breakCount); + + double ratio = (point.vCoord - center.vCoord) / radius; + ratio = std::max(-1., std::min(1., ratio)); + const double cosMagnitude = std::sqrt(std::max(0., 1. - ratio * ratio)); + + int crossings = 0; + for (int index = 0; index + 1 < breakCount; ++index) { + const Vec2 subStart = (index == 0) ? canonicalStart : pointAt(breakParameters[index]); + const Vec2 subEnd = (index + 2 == breakCount) ? canonicalEnd : pointAt(breakParameters[index + 1]); + const double midAngle = startAngle + 0.5 * (breakParameters[index] + breakParameters[index + 1]) * totalSweep; + const double cosSign = std::cos(midAngle) >= 0. ? 1. : -1.; + const double intersectU = center.uCoord + cosSign * radius * cosMagnitude; + if (segmentCrossing(subStart, subEnd, intersectU)) { + ++crossings; + } + } + return crossings; + } + + /// Reverse the curve's direction in place (start <-> end), keeping the same geometric image. + void reverseInPlace() + { + if (hasCanonicalEndpoints) { + std::swap(canonicalStart, canonicalEnd); + bsplineCache.clear(); + } + if (kind == CurveKind::Line) { + std::swap(lineStart, lineEnd); + } else if (kind == CurveKind::Arc) { + std::swap(startAngle, endAngle); + } else { + // B-spline: reverse the poles/weights and complement the knot vector about its span so the + // parametrization runs the other way (knots stay non-decreasing and clamped). + std::reverse(poles.begin(), poles.end()); + if (!weights.empty()) { + std::reverse(weights.begin(), weights.end()); + } + const double knotSum = knots.front() + knots.back(); + std::vector reversedKnots(knots.size()); + for (size_t index = 0; index < knots.size(); ++index) { + reversedKnots[index] = knotSum - knots[knots.size() - 1 - index]; + } + knots = std::move(reversedKnots); + bsplineCache.clear(); // geometry order changed; recompute lazily + } + } +}; + +/// One closed, oriented boundary loop of Curve2D segments: outer loops wind counter-clockwise, holes clockwise. +struct CurveWire { + std::vector curves; + WireRole role = WireRole::Outer; + /// The largest representationTolerance() over the curves, fixed when the curves are set. + double mRepresentationTolerance = 0.; + + /// For each stored curve its input index; reverse() is the only reordering, and sidecar v3 edge identities key on it. + std::vector sourceCurve; + + /// The stored curve that came from input curve \a inputIndex, or -1 if there is none. + int storedIndexOfSource(int inputIndex) const + { + for (size_t index = 0; index < sourceCurve.size(); ++index) { + if (sourceCurve[index] == inputIndex) { + return static_cast(index); + } + } + return -1; + } + + /// Build and validate the wire from an ordered closed list of curves, joining within \a joinTolerance through \a metric. + bool initialize(const std::vector& inputCurves, WireRole wireRole, WireStatus& status, + const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance) + { + role = wireRole; + curves = inputCurves; + mRepresentationTolerance = 0.; + for (const auto& curve : curves) { + mRepresentationTolerance = std::max(mRepresentationTolerance, curve.representationTolerance()); + } + sourceCurve.resize(curves.size()); + for (size_t index = 0; index < curves.size(); ++index) { + sourceCurve[index] = static_cast(index); + } + + if (curves.empty()) { + status = WireStatus::TooFewVertices; + return false; + } + for (size_t index = 0; index < curves.size(); ++index) { + if (!curves[index].valid()) { + status = WireStatus::NonFinite; + return false; + } + const Vec2 currentEnd = curves[index].endPoint(); + const Vec2 nextStart = curves[(index + 1) % curves.size()].startPoint(); + if (metric.distanceSq(currentEnd, nextStart) > joinTolerance * joinTolerance) { + status = WireStatus::Open; + return false; + } + } + + // one vertex value per seam, given to both curves that meet there + for (size_t index = 0; index < curves.size(); ++index) { + curves[index].setCanonicalEndpoints(curves[index].startPoint(), + curves[(index + 1) % curves.size()].startPoint()); + } + + const double area = signedArea(); + if (std::abs(area) <= kAreaTolerance) { + status = WireStatus::ZeroArea; + return false; + } + + const bool wantPositiveArea = (role == WireRole::Outer); + if ((area > 0.) != wantPositiveArea) { + reverse(); + status = WireStatus::Reversed; + fillBSplineCaches(); + return true; + } + status = WireStatus::Valid; + fillBSplineCaches(); + return true; + } + + /// Fill every B-spline's polyline cache now, so that const navigation queries only read it. + void fillBSplineCaches() const + { + for (const auto& curve : curves) { + if (curve.kind == CurveKind::BSpline) { + curve.bsplineSamples(); + } + } + } + + /// The widest gap between the loop's representation and its boundary, in parametric units; 0 for lines and arcs. + double representationTolerance() const { return mRepresentationTolerance; } + + /// Reverse the loop orientation in place (order and per-curve direction). + void reverse() + { + std::reverse(curves.begin(), curves.end()); + std::reverse(sourceCurve.begin(), sourceCurve.end()); + for (auto& curve : curves) { + curve.reverseInPlace(); + } + } + + /// True if any curve of the loop is a B-spline (whose trimmed-face capacity is only numerically + /// integrated, so the owning surface must report capacityIsExact() == false). + bool hasBSpline() const + { + for (const auto& curve : curves) { + if (curve.kind == CurveKind::BSpline) { + return true; + } + } + return false; + } + + /// Exact signed area enclosed by the loop (positive when counter-clockwise). + double signedArea() const + { + double area = 0.; + for (const auto& curve : curves) { + area += curve.signedAreaContribution(); + } + return area; + } + + /// Add the loop's conservative extent, a B-spline's pole hull included, to a parametric bounding box. + void parametricBounds(Vec2& lower, Vec2& upper) const + { + for (const auto& curve : curves) { + curve.extendBounds(lower, upper); + } + } + + /// Add the loop's extent measured on the curves to a parametric bounding box; use it to reject a wire as too big. + void tightParametricBounds(Vec2& lower, Vec2& upper) const + { + for (const auto& curve : curves) { + curve.extendTightBounds(lower, upper); + } + } + + /// Half-width of the on-boundary band in parametric units: the larger of the length floor and the representation tolerance. + /// A degenerate metric (a pole, an apex) leaves only the representation term. + double boundaryBand(double lengthFloor) const { return std::max(lengthFloor, mRepresentationTolerance); } + + /// Classify a point against the loop with band floor \a lengthFloor: Boundary within the band, else the crossing parity. + WireClassification classify(const Vec2& point, double lengthFloor) const + { + const double band = boundaryBand(lengthFloor); + const double bandSq = band * band; + // Each curve's polyline already ends on the loop-canonical seam vertices, so the half-open + // crossing convention stays consistent across seams without any substitution here. + int crossings = 0; + for (const auto& curve : curves) { + if (curve.kind == CurveKind::BSpline) { + if (curve.bsplineBandOrCrossings(point, bandSq, crossings)) { + return WireClassification::Boundary; + } + } else if (curve.distanceSq(point) <= bandSq) { + return WireClassification::Boundary; + } else { + crossings += curve.rightwardCrossings(point, curve.loopStart(), curve.loopEnd()); + } + } + return (crossings % 2 == 1) ? WireClassification::Inside : WireClassification::Outside; + } + + /// \a metric only sizes the on-boundary band; the winding count is topological. + WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const + { + return classify(point, trimLengthFloor(metric, point)); + } + + /// Ordered, closed boundary polyline; arcs are sampled into \a segmentsPerArc chords. This is a + /// mesh-independent hook for visualization and tessellated fallback of curved boundaries. + std::vector sampledBoundary(int segmentsPerArc = kArcSamples) const + { + std::vector samples; + if (curves.empty()) { + return samples; + } + for (const auto& curve : curves) { + if (curve.kind == CurveKind::Line) { + samples.push_back(curve.startPoint()); + } else if (curve.kind == CurveKind::BSpline) { + // adaptively flatten and append every sample except the closing one (the next curve's + // start reproduces it) + std::vector curveSamples; + curve.bsplineSampleInto(curveSamples); + for (size_t index = 0; index + 1 < curveSamples.size(); ++index) { + samples.push_back(curveSamples[index]); + } + } else { + // chords scale with the arc's sweep, so a rim shared with a quadric wall samples identical vertices + const int arcSteps = + std::max(1, static_cast(std::lround(segmentsPerArc * std::abs(curve.sweep()) / kTwoPi))); + for (int step = 0; step < arcSteps; ++step) { + samples.push_back(curve.pointAt(static_cast(step) / arcSteps)); + } + } + } + samples.push_back(samples.front()); + return samples; + } +}; + +/// \name Curve-wire trim helpers for quadric parametric domains (u = phi, v = height or theta) +/// @{ + +/// Shift \a angle by whole turns to lie as close as possible to the window [uMin, uMax]. +inline double unwrapAngleInto(double angle, double uMin, double uMax) +{ + const double windowCenter = 0.5 * (uMin + uMax); + return angle - kTwoPi * std::round((angle - windowCenter) / kTwoPi); +} + +/// Whether a parametric point is in a curve-wire trim (outer loop minus holes); \a boundary reports an on-boundary hit. +inline bool curveTrimContains(const CurveWire& outerWire, const std::vector& innerWires, + const Vec2& point, bool* boundary = nullptr, + const ParametricMetric& metric = {}) +{ + if (boundary != nullptr) { + *boundary = false; + } + const double lengthFloor = trimLengthFloor(metric, point); + const auto outerClassification = outerWire.classify(point, lengthFloor); + if (outerClassification == WireClassification::Outside) { + return false; + } + if (outerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + for (const auto& innerWire : innerWires) { + const auto innerClassification = innerWire.classify(point, lengthFloor); + if (innerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + if (innerClassification == WireClassification::Inside) { + return false; + } + } + return true; +} + +/// Gauss-Legendre nodes per contour sub-interval, and the widest u span one sub-interval covers. +inline constexpr int kContourQuadratureOrder = 20; +inline constexpr double kContourMaxSpanU = 0.25 * kPi; + +/// Integrate F(u, v) dv along one directed curve of a trim wire, from \a from to \a to in the +/// curve's own [0, 1] parameter. +template +double contourIntegralAlongCurve(const Curve2D& curve, const Antiderivative& antiderivative, double from, + double to) +{ + static thread_local std::vector nodes; + static thread_local std::vector weights; + if (static_cast(nodes.size()) != kContourQuadratureOrder) { + gaussLegendre(kContourQuadratureOrder, nodes, weights); + } + // split at the interior knots, then into pieces whose u travel is at most kContourMaxSpanU + static thread_local std::vector breakpoints; + breakpoints.clear(); + breakpoints.push_back(from); + curve.appendInteriorKnots(std::min(from, to), std::max(from, to), breakpoints); + std::sort(breakpoints.begin() + 1, breakpoints.end(), + [forward = (to >= from)](double first, double second) { return forward ? first < second : first > second; }); + breakpoints.push_back(to); + + double total = 0.; + for (size_t segment = 0; segment + 1 < breakpoints.size(); ++segment) { + const double segmentFrom = breakpoints[segment]; + const double segmentTo = breakpoints[segment + 1]; + if (segmentFrom == segmentTo) { + continue; + } + const double travelU = curve.uVariation(std::min(segmentFrom, segmentTo), std::max(segmentFrom, segmentTo)); + const int pieces = std::max(1, static_cast(std::ceil(travelU / kContourMaxSpanU))); + for (int piece = 0; piece < pieces; ++piece) { + const double low = segmentFrom + (segmentTo - segmentFrom) * piece / pieces; + const double high = segmentFrom + (segmentTo - segmentFrom) * (piece + 1) / pieces; + const double half = 0.5 * (high - low); + const double mid = 0.5 * (high + low); + for (int nodeIndex = 0; nodeIndex < kContourQuadratureOrder; ++nodeIndex) { + const double parameter = mid + half * nodes[nodeIndex]; + const Vec2 point = curve.pointAt(parameter); + const Vec2 derivative = curve.derivativeAt(parameter); + total += weights[nodeIndex] * half * antiderivative(point.uCoord, point.vCoord) * derivative.vCoord; + } + } + } + return total; +} + +/// Green's theorem over a wire-trimmed patch: the double integral of f is the contour integral of F dv, F the u-antiderivative of f; seams are bridged. +template +double integrateOverCurveTrimByParts(const CurveWire& outerWire, const std::vector& innerWires, + const Antiderivative& antiderivative) +{ + const auto loopIntegral = [&antiderivative](const CurveWire& wire) { + double total = 0.; + for (size_t index = 0; index < wire.curves.size(); ++index) { + const auto& curve = wire.curves[index]; + total += contourIntegralAlongCurve(curve, antiderivative, 0., 1.); + // seam bridge: a straight run from this curve's end to the next curve's start + const Vec2 seamFrom = curve.endPoint(); + const Vec2 seamTo = wire.curves[(index + 1) % wire.curves.size()].startPoint(); + const double deltaV = seamTo.vCoord - seamFrom.vCoord; + if (deltaV != 0.) { + const Curve2D bridge = Curve2D::makeLine(seamFrom, seamTo); + total += contourIntegralAlongCurve(bridge, antiderivative, 0., 1.); + } + } + return total; + }; + + double total = loopIntegral(outerWire); + for (const auto& innerWire : innerWires) { + total += loopIntegral(innerWire); + } + return total; +} + +/// Midpoint-rule integral of \a integrand over the trimmed region; kept as the independent check of the contour form. +template +double integrateOverCurveTrim(const CurveWire& outerWire, const std::vector& innerWires, + const Integrand& integrand, int samplesPerAxis = 128) +{ + Vec2 lower{std::numeric_limits::infinity(), std::numeric_limits::infinity()}; + Vec2 upper{-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + outerWire.parametricBounds(lower, upper); + if (!finite(lower) || !finite(upper) || samplesPerAxis < 1) { + return 0.; + } + const double stepU = (upper.uCoord - lower.uCoord) / samplesPerAxis; + const double stepV = (upper.vCoord - lower.vCoord) / samplesPerAxis; + const double cellArea = stepU * stepV; + double sum = 0.; + for (int indexU = 0; indexU < samplesPerAxis; ++indexU) { + const double uCoord = lower.uCoord + (indexU + 0.5) * stepU; + for (int indexV = 0; indexV < samplesPerAxis; ++indexV) { + const double vCoord = lower.vCoord + (indexV + 0.5) * stepV; + if (curveTrimContains(outerWire, innerWires, {uCoord, vCoord})) { + sum += integrand(uCoord, vCoord) * cellArea; + } + } + } + return sum; +} + +/// Build validated outer and inner trim wires and the outer loop's parametric bounds; rejects a trim wider than a turn in u. +inline bool buildCurveTrim(const std::vector& outerTrim, + const std::vector>& innerTrims, CurveWire& outerWire, + std::vector& innerWires, Vec2& lower, Vec2& upper, + std::string& errorMessage, const ParametricMetric& metric = {}, + double joinTolerance = kWireJoinTolerance) +{ + WireStatus status = WireStatus::Valid; + if (!outerWire.initialize(outerTrim, WireRole::Outer, status, metric, joinTolerance)) { + errorMessage = std::string("quadric outer trim wire invalid: ") + wireStatusMessage(status); + return false; + } + innerWires.clear(); + innerWires.reserve(innerTrims.size()); + for (const auto& innerLoop : innerTrims) { + CurveWire innerWire; + WireStatus innerStatus = WireStatus::Valid; + if (!innerWire.initialize(innerLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) { + errorMessage = std::string("quadric inner trim wire invalid: ") + wireStatusMessage(innerStatus); + return false; + } + innerWires.push_back(std::move(innerWire)); + } + lower = {std::numeric_limits::infinity(), std::numeric_limits::infinity()}; + upper = {-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + outerWire.parametricBounds(lower, upper); + if (!finite(lower) || !finite(upper)) { + errorMessage = "quadric trim wire has non-finite parametric bounds"; + return false; + } + if (upper.uCoord - lower.uCoord > kTwoPi + kTolerance) { + // the pole hull can overshoot the curve; re-measure on the curves before refusing + Vec2 tightLower{std::numeric_limits::infinity(), std::numeric_limits::infinity()}; + Vec2 tightUpper{-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + outerWire.tightParametricBounds(tightLower, tightUpper); + if (!finite(tightLower) || !finite(tightUpper) || tightUpper.uCoord - tightLower.uCoord > kTwoPi + kTolerance) { + errorMessage = "quadric trim wire spans more than a full turn in phi"; + return false; + } + // the wire is admissible; keep the tight box, since the conservative one is not a valid + // parametric window for a periodic coordinate once it exceeds a full turn + lower = tightLower; + upper = tightUpper; + } + return true; +} + +/// Sub-sample a curve-wire loop so its u span is chorded at \a segmentsPerTurn per turn, matching neighbouring rims. +inline std::vector sampleCurveWireByU(const CurveWire& wire, int segmentsPerTurn = kArcSamples) +{ + std::vector samples; + for (const auto& curve : wire.curves) { + if (curve.kind == CurveKind::BSpline) { + // adaptively flatten in the parameter domain; append every sample except the closing one + std::vector curveSamples; + curve.bsplineSampleInto(curveSamples); + for (size_t index = 0; index + 1 < curveSamples.size(); ++index) { + samples.push_back(curveSamples[index]); + } + continue; + } + Vec2 lower{std::numeric_limits::infinity(), std::numeric_limits::infinity()}; + Vec2 upper{-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + curve.extendBounds(lower, upper); + const double uSpan = upper.uCoord - lower.uCoord; + int steps = std::max(1, static_cast(std::lround(segmentsPerTurn * uSpan / kTwoPi))); + if (curve.kind == CurveKind::Arc) { + steps = std::max(steps, static_cast(std::lround(segmentsPerTurn * std::abs(curve.sweep()) / kTwoPi))); + steps = std::max(steps, 1); + } + for (int step = 0; step < steps; ++step) { + samples.push_back(curve.pointAt(static_cast(step) / steps)); + } + } + return samples; +} + +/// Append the display triangulation of a wire-trimmed quadric patch: the sampled outer loop, ear-clipped; holes are omitted. +template +void appendCurveTrimMesh(const CurveWire& outerWire, const MapUV& mapUV, std::vector& vertices, + std::vector>& triangles) +{ + SurfaceWire sampledWire; + sampledWire.vertices = sampleCurveWireByU(outerWire); + if (sampledWire.vertices.size() < 3) { + return; + } + const int firstVertexIndex = static_cast(vertices.size()); + for (const auto& sample : sampledWire.vertices) { + vertices.push_back(mapUV(sample.uCoord, sample.vCoord)); + } + for (const auto& triangle : triangulateSimpleWire(sampledWire)) { + triangles.push_back( + {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]}); + } +} + +/// Append the directed 3D boundary edges of a wire-trimmed quadric patch; a negative \a orientationSign reverses them. +template +void appendCurveTrimEdges(const CurveWire& outerWire, const std::vector& innerWires, + const MapUV& mapUV, double orientationSign, + std::vector>& edges) +{ + auto appendLoop = [&](const CurveWire& wire) { + const auto samples = sampleCurveWireByU(wire); + const size_t sampleCount = samples.size(); + for (size_t sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) { + const Vec2& current = samples[sampleIndex]; + const Vec2& next = samples[(sampleIndex + 1) % sampleCount]; + const Vec3 edgeStart = mapUV(current.uCoord, current.vCoord); + const Vec3 edgeEnd = mapUV(next.uCoord, next.vCoord); + if (orientationSign >= 0.) { + edges.emplace_back(edgeStart, edgeEnd); + } else { + edges.emplace_back(edgeEnd, edgeStart); + } + } + }; + appendLoop(outerWire); + for (const auto& innerWire : innerWires) { + appendLoop(innerWire); + } +} + +/// Samples per trim curve when measuring a shared edge's deviation; it never enters a verdict. +inline constexpr int kSharedEdgeSamples = 33; + +/// Sample input curve \a index of a curve-wire trim into 3D through \a mapUV; false when out of range or not traceable. +template +bool sampleTrimCurveOfCurveWires(const CurveWire& outerWire, const std::vector& innerWires, + size_t index, const MapUV& mapUV, std::vector& samples) +{ + const CurveWire* wire = nullptr; + size_t local = index; + if (local < outerWire.curves.size()) { + wire = &outerWire; + } else { + local -= outerWire.curves.size(); + for (const auto& innerWire : innerWires) { + if (local < innerWire.curves.size()) { + wire = &innerWire; + break; + } + local -= innerWire.curves.size(); + } + } + if (wire == nullptr) { + return false; + } + const int stored = wire->storedIndexOfSource(static_cast(local)); + if (stored < 0) { + return false; + } + const Curve2D& curve = wire->curves[static_cast(stored)]; + samples.clear(); + samples.reserve(kSharedEdgeSamples); + for (int step = 0; step < kSharedEdgeSamples; ++step) { + const Vec2 uv = curve.pointAt(static_cast(step) / (kSharedEdgeSamples - 1)); + samples.push_back(mapUV(uv.uCoord, uv.vCoord)); + } + return true; +} + +/// The same for a polygon (vertex-ring) trim, whose curves are all straight segments. +template +bool sampleTrimCurveOfSurfaceWires(const SurfaceWire& outerWire, const std::vector& innerWires, + size_t index, const MapUV& mapUV, std::vector& samples) +{ + const SurfaceWire* wire = nullptr; + size_t local = index; + if (local < outerWire.vertices.size()) { + wire = &outerWire; + } else { + local -= outerWire.vertices.size(); + for (const auto& innerWire : innerWires) { + if (local < innerWire.vertices.size()) { + wire = &innerWire; + break; + } + local -= innerWire.vertices.size(); + } + } + if (wire == nullptr) { + return false; + } + const int stored = wire->storedIndexOfSource(static_cast(local)); + if (stored < 0) { + return false; + } + const SurfaceEdge segment = wire->edge(stored); + samples.clear(); + samples.push_back(mapUV(segment.start.uCoord, segment.start.vCoord)); + samples.push_back(mapUV(segment.end.uCoord, segment.end.vCoord)); + return true; +} +/// @} + +/// One trim loop of one face as an ordered 3D polyline, compared with other faces' rims as a curve. +struct SurfaceRim { + int surfaceIndex = -1; ///< index of the owning face in the solid's surface list + bool closed = false; ///< the polyline returns to its own first point + std::vector points; ///< consecutive samples; a closed rim does not repeat the first point +}; + +/// Chain a face's directed chords into rims by matching endpoints within kTolerance, appending them to \a rims. +inline void assembleRims(const std::vector>& edges, std::vector& rims) +{ + if (edges.empty()) { + return; + } + auto quantize = [](double value) { return static_cast(std::llround(value / kTolerance)); }; + using VertexKey = std::tuple; + auto keyOf = [&](const Vec3& point) { + return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)}; + }; + + // cancel reversed duplicate chords (a self-closing seam) before chaining, keyed by their shared midpoint + std::vector consumed(edges.size(), false); + std::map> edgesByMidpoint; + for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) { + const Vec3 midpoint = (edges[edgeIndex].first + edges[edgeIndex].second) * 0.5; + const auto [xKey, yKey, zKey] = keyOf(midpoint); + bool cancelled = false; + for (int64_t dx = -1; dx <= 1 && !cancelled; ++dx) { + for (int64_t dy = -1; dy <= 1 && !cancelled; ++dy) { + for (int64_t dz = -1; dz <= 1 && !cancelled; ++dz) { + const auto found = edgesByMidpoint.find(VertexKey{xKey + dx, yKey + dy, zKey + dz}); + if (found == edgesByMidpoint.end()) { + continue; + } + for (const size_t candidate : found->second) { + if (consumed[candidate] || + distanceSq(edges[candidate].first, edges[edgeIndex].second) > kToleranceSq || + distanceSq(edges[candidate].second, edges[edgeIndex].first) > kToleranceSq) { + continue; + } + consumed[candidate] = true; + consumed[edgeIndex] = true; + cancelled = true; + break; + } + } + } + } + if (!cancelled) { + edgesByMidpoint[keyOf(midpoint)].push_back(edgeIndex); + } + } + + std::map> edgesByStart; + for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) { + if (!consumed[edgeIndex]) { + edgesByStart[keyOf(edges[edgeIndex].first)].push_back(edgeIndex); + } + } + + // A vertex can land either side of a lattice boundary, so probe the 27 neighbouring cells and + // accept the first unused chord whose start really is within kTolerance. + auto findSuccessor = [&](const Vec3& point) -> long long { + const auto [xKey, yKey, zKey] = keyOf(point); + for (int64_t dx = -1; dx <= 1; ++dx) { + for (int64_t dy = -1; dy <= 1; ++dy) { + for (int64_t dz = -1; dz <= 1; ++dz) { + const auto found = edgesByStart.find(VertexKey{xKey + dx, yKey + dy, zKey + dz}); + if (found == edgesByStart.end()) { + continue; + } + for (const size_t candidate : found->second) { + if (!consumed[candidate] && distanceSq(edges[candidate].first, point) <= kToleranceSq) { + return static_cast(candidate); + } + } + } + } + } + return -1; + }; + + for (size_t seed = 0; seed < edges.size(); ++seed) { + if (consumed[seed]) { + continue; + } + consumed[seed] = true; + SurfaceRim rim; + rim.points.push_back(edges[seed].first); + rim.points.push_back(edges[seed].second); + while (true) { + if (distanceSq(rim.points.back(), rim.points.front()) <= kToleranceSq) { + rim.closed = true; + rim.points.pop_back(); // a closed rim does not repeat its first point + break; + } + const long long next = findSuccessor(rim.points.back()); + if (next < 0) { + break; // an open chain: the face's boundary is not a set of closed loops + } + consumed[static_cast(next)] = true; + rim.points.push_back(edges[static_cast(next)].second); + } + if (rim.points.size() >= 2) { + rims.push_back(std::move(rim)); + } + } +} + +/// Abstract analytic surface patch: one support surface plus its trim, with the kernels the navigation needs. +class BoundedSurface +{ + public: + virtual ~BoundedSurface() = default; + + /// \name Boundary edge identity (sidecar v3): the source edges bounding this face; empty means not stated + /// @{ + struct BoundaryEdgeRef { + uint32_t edgeId = 0; ///< index into the model's edge table; identity, not a coordinate + bool reversed = false; ///< this face runs against the edge's own direction + bool degenerate = false; ///< a cone apex / sphere pole: one point, no length, no partner + /// Whether trim curve \a i exists to sample for edge \a i; false for a parametric-rectangle trim. + bool anchored = false; + }; + + void setBoundaryEdges(std::vector refs) { mBoundaryEdges = std::move(refs); } + const std::vector& boundaryEdges() const { return mBoundaryEdges; } + + /// Sample trim curve \a index into 3D, in construction order; false when this face has no such curve. + virtual bool sampleTrimCurve(size_t index, std::vector& samples) const + { + (void)index; + (void)samples; + return false; + } + /// @} + + /// Accumulate a conservative axis-aligned bounding box of the trimmed patch. + virtual void conservativeBounds(Vec3& lower, Vec3& upper) const = 0; + + /// One axis-aligned cover box of the sub-patch BVH, as a (lower corner, upper corner) pair. + using CoverBox = std::pair; + + /// Append cover boxes whose union holds the trimmed patch and every point that can realise distanceSqToPatch. + /// Spheres and tori realise on their whole surface, so they cover it all; the default is conservativeBounds(). + virtual void appendCoverBoxes(std::vector& boxes) const + { + // conservativeBounds only accumulates, so the corners start beyond any geometry + constexpr double kBig = std::numeric_limits::max(); + CoverBox box{Vec3{kBig, kBig, kBig}, Vec3{-kBig, -kBig, -kBig}}; + conservativeBounds(box.first, box.second); + boxes.push_back(box); + } + + /// True if the 3D point lies on the trimmed patch within tolerance. + virtual bool containsPointOnSurface(const Vec3& point) const = 0; + + /// Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward normal; no tangential grazes. + virtual void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const = 0; + + /// Squared distance from a 3D point to the trimmed patch (used for Safety). + virtual double distanceSqToPatch(const Vec3& point) const = 0; + + /// Outward-oriented normal at (or nearest to) the given point. + virtual Vec3 normalAt(const Vec3& point) const = 0; + + /// The first fundamental form at \a uv, turning parametric displacements into 3D lengths; it varies over the domain and gUU vanishes at poles. + virtual void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const = 0; + + /// The 3D length squared spanned by a parametric displacement \a delta starting at \a uv. + double parametricLengthSqAt(const Vec2& uv, const Vec2& delta) const + { + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + parametricMetric(uv, gUU, gUV, gVV); + return parametricLengthSq(gUU, gUV, gVV, delta); + } + + /// Signed divergence-theorem contribution to the enclosed volume. + virtual double capacityContribution() const = 0; + + /// Whether capacityContribution() is analytically exact for this surface. + virtual bool capacityIsExact() const = 0; + + /// Append this patch's visualization triangulation (navigation must never depend on it). + virtual void appendDisplayMesh(std::vector& vertices, + std::vector>& triangles) const = 0; + + /// Append the 3D directed boundary edges of the patch, for solid-closure validation. + virtual void appendDirectedEdges(std::vector>& edges) const = 0; + + /// Append the trim boundary as rims, one polyline per loop; the default chains appendDirectedEdges(). + virtual void appendRims(std::vector& rims) const + { + std::vector> edges; + appendDirectedEdges(edges); + assembleRims(edges, rims); + } + + protected: + std::vector mBoundaryEdges; +}; + +/// A bounded planar surface: an infinite plane frame trimmed by one outer wire and optional +/// inner (hole) wires expressed in the plane's local 2D coordinates. +class PlanarBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV, + const std::vector& outerWireVertices, + const std::vector>& innerWireVertices, std::string& errorMessage) + { + if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) { + errorMessage = "surface frame contains a non-finite value"; + return false; + } + + mOrigin = surfaceOrigin; + mAxisU = surfaceAxisU; + mAxisV = surfaceAxisV; + const Vec3 normalVector = cross(mAxisU, mAxisV); + mAreaScale = norm(normalVector); + if (mAreaScale <= kTolerance) { + errorMessage = "surface frame axes are degenerate"; + return false; + } + mNormal = normalVector * (1. / mAreaScale); + + mMetricUU = dot(mAxisU, mAxisU); + mMetricUV = dot(mAxisU, mAxisV); + mMetricVV = dot(mAxisV, mAxisV); + const double metricDet = mMetricUU * mMetricVV - mMetricUV * mMetricUV; + if (std::abs(metricDet) <= kToleranceSq) { + errorMessage = "surface frame metric is singular"; + return false; + } + mInverseMetricDet = 1. / metricDet; + + WireStatus outerStatus = WireStatus::Valid; + const ParametricMetric metric = parametricMetricOf(*this); + mTrimBand = trimLengthFloor(metric, Vec2{0., 0.}); + if (!mOuterWire.initialize(outerWireVertices, WireRole::Outer, outerStatus, metric)) { + errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus); + return false; + } + mOuterReoriented = (outerStatus == WireStatus::Reversed); + + mInnerWires.clear(); + mInnerWires.reserve(innerWireVertices.size()); + mInnerReoriented = false; + for (const auto& innerWireInput : innerWireVertices) { + SurfaceWire innerWire; + WireStatus innerStatus = WireStatus::Valid; + if (!innerWire.initialize(innerWireInput, WireRole::Inner, innerStatus, metric)) { + errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus); + return false; + } + mInnerReoriented = mInnerReoriented || (innerStatus == WireStatus::Reversed); + mInnerWires.emplace_back(std::move(innerWire)); + } + + const auto ringOf = [this](const SurfaceWire& wire) { + std::vector ring; + ring.reserve(wire.vertices.size()); + for (const auto& vertex : wire.vertices) { + ring.push_back(toGlobal(vertex)); + } + return ring; + }; + mOuterRing = ringOf(mOuterWire); + mInnerRings.clear(); + for (const auto& innerWire : mInnerWires) { + mInnerRings.push_back(ringOf(innerWire)); + } + return true; + } + + /// True if either the outer or any inner wire had to be re-oriented during initialization. + bool wasReoriented() const { return mOuterReoriented || mInnerReoriented; } + + Vec3 toGlobal(const Vec2& point) const + { + return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord; + } + + Vec2 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mOrigin; + const double projectionU = dot(relativePoint, mAxisU); + const double projectionV = dot(relativePoint, mAxisV); + return {(projectionU * mMetricVV - projectionV * mMetricUV) * mInverseMetricDet, + (projectionV * mMetricUU - projectionU * mMetricUV) * mInverseMetricDet}; + } + + double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); } + + bool containsLocal(const Vec2& point, bool* boundary = nullptr) const + { + if (boundary != nullptr) { + *boundary = false; + } + + const auto outerClassification = mOuterWire.classify(point, mTrimBand); + if (outerClassification == WireClassification::Outside) { + return false; + } + if (outerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + + for (const auto& innerWire : mInnerWires) { + const auto innerClassification = innerWire.classify(point, mTrimBand); + if (innerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + if (innerClassification == WireClassification::Inside) { + return false; + } + } + return true; + } + + bool containsPointOnSurface(const Vec3& point) const override + { + if (std::abs(planeDistance(point)) > kTolerance) { + return false; + } + return containsLocal(toLocal(point)); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const double denominator = dot(mNormal, rayDirection); + if (std::abs(denominator) <= kTolerance) { + return; + } + const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator; + if (candidateDistance < minDistance || candidateDistance > maxDistance) { + return; + } + const Vec3 candidatePoint = rayOrigin + rayDirection * candidateDistance; + bool onTrimBoundary = false; + if (!containsLocal(toLocal(candidatePoint), &onTrimBoundary)) { + return; + } + hits.push_back({candidateDistance, mNormal, onTrimBoundary}); + } + + double distanceSqToEdges(const Vec3& point, const std::vector& ring) const + { + double bestDistanceSq = std::numeric_limits::infinity(); + for (size_t vertexIndex = 0; vertexIndex < ring.size(); ++vertexIndex) { + bestDistanceSq = + std::min(bestDistanceSq, pointSegmentDistanceSq(point, ring[vertexIndex], ring[(vertexIndex + 1) % ring.size()])); + } + return bestDistanceSq; + } + + double distanceSqToPatch(const Vec3& point) const override + { + const Vec2 projectedPoint = toLocal(point); + if (containsLocal(projectedPoint)) { + const double signedPlaneDistance = planeDistance(point); + return signedPlaneDistance * signedPlaneDistance; + } + + double bestDistanceSq = distanceSqToEdges(point, mOuterRing); + for (const auto& innerRing : mInnerRings) { + bestDistanceSq = std::min(bestDistanceSq, distanceSqToEdges(point, innerRing)); + } + return bestDistanceSq; + } + + Vec3 normalAt(const Vec3&) const override { return mNormal; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + auto extendPoint = [&](const Vec2& surfacePoint) { + const Vec3 globalPoint = toGlobal(surfacePoint); + lower.xCoord = std::min(lower.xCoord, globalPoint.xCoord); + lower.yCoord = std::min(lower.yCoord, globalPoint.yCoord); + lower.zCoord = std::min(lower.zCoord, globalPoint.zCoord); + upper.xCoord = std::max(upper.xCoord, globalPoint.xCoord); + upper.yCoord = std::max(upper.yCoord, globalPoint.yCoord); + upper.zCoord = std::max(upper.zCoord, globalPoint.zCoord); + }; + + for (const auto& vertex : mOuterWire.vertices) { + extendPoint(vertex); + } + for (const auto& innerWire : mInnerWires) { + for (const auto& vertex : innerWire.vertices) { + extendPoint(vertex); + } + } + } + + double area() const + { + double parametricArea = std::abs(mOuterWire.signedArea()); + for (const auto& innerWire : mInnerWires) { + parametricArea -= std::abs(innerWire.signedArea()); + } + return std::max(0., parametricArea) * mAreaScale; + } + + /// Constant over the plane, with a cross term: the frame axes need be neither unit-length nor orthogonal. + void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override + { + planeParametricMetric(mAxisU, mAxisV, gUU, gUV, gVV); + } + + double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; } + + bool capacityIsExact() const override { return true; } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + const int firstVertexIndex = static_cast(vertices.size()); + for (const auto& vertex : mOuterWire.vertices) { + vertices.push_back(toGlobal(vertex)); + } + + const auto localTriangles = triangulateSimpleWire(mOuterWire); + for (const auto& triangle : localTriangles) { + triangles.push_back( + {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]}); + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + auto appendWire = [&](const SurfaceWire& wire) { + for (size_t vertexIndex = 0; vertexIndex < wire.vertices.size(); ++vertexIndex) { + const Vec3 edgeStart = toGlobal(wire.vertices[vertexIndex]); + const Vec3 edgeEnd = toGlobal(wire.vertices[(vertexIndex + 1) % wire.vertices.size()]); + edges.emplace_back(edgeStart, edgeEnd); + } + }; + appendWire(mOuterWire); + for (const auto& innerWire : mInnerWires) { + appendWire(innerWire); + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + return sampleTrimCurveOfSurfaceWires( + mOuterWire, mInnerWires, index, + [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples); + } + + private: + Vec3 mOrigin; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mNormal; + double mMetricUU = 0.; + double mMetricUV = 0.; + double mMetricVV = 0.; + double mInverseMetricDet = 0.; + double mAreaScale = 0.; + bool mOuterReoriented = false; + bool mInnerReoriented = false; + double mTrimBand = 0.; ///< the wires' on-boundary band; the plane's metric is constant + SurfaceWire mOuterWire; + std::vector mInnerWires; + std::vector mOuterRing; ///< the outer wire's vertices in 3D + std::vector> mInnerRings; ///< the inner wires' vertices in 3D +}; + +/// A plane trimmed by curved (line/arc/B-spline) loops in an orthonormal frame: exact caps, disks and annuli. +class CurvedPlanarBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV, + const std::vector& outerCurves, + const std::vector>& innerCurves, std::string& errorMessage, + double joinTolerance = kWireJoinTolerance) + { + if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) { + errorMessage = "surface frame contains a non-finite value"; + return false; + } + if (std::abs(norm(surfaceAxisU) - 1.) > kTolerance || std::abs(norm(surfaceAxisV) - 1.) > kTolerance || + std::abs(dot(surfaceAxisU, surfaceAxisV)) > kTolerance) { + errorMessage = "curved planar surface requires orthonormal frame axes"; + return false; + } + + mOrigin = surfaceOrigin; + mAxisU = surfaceAxisU; + mAxisV = surfaceAxisV; + mNormal = cross(mAxisU, mAxisV); + + WireStatus outerStatus = WireStatus::Valid; + const ParametricMetric metric = parametricMetricOf(*this); + mTrimFloor = trimLengthFloor(metric, Vec2{0., 0.}); + if (!mOuterWire.initialize(outerCurves, WireRole::Outer, outerStatus, metric, joinTolerance)) { + errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus); + return false; + } + mReoriented = (outerStatus == WireStatus::Reversed); + + mInnerWires.clear(); + mInnerWires.reserve(innerCurves.size()); + for (const auto& innerCurveLoop : innerCurves) { + CurveWire innerWire; + WireStatus innerStatus = WireStatus::Valid; + if (!innerWire.initialize(innerCurveLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) { + errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus); + return false; + } + mReoriented = mReoriented || (innerStatus == WireStatus::Reversed); + mInnerWires.emplace_back(std::move(innerWire)); + } + + // A B-spline boundary makes the area (hence the capacity contribution) a numeric quadrature, + // so flag the capacity as inexact (matching the wire-trimmed-quadric policy). + mCapacityExact = !mOuterWire.hasBSpline(); + for (const auto& innerWire : mInnerWires) { + mCapacityExact = mCapacityExact && !innerWire.hasBSpline(); + } + return true; + } + + /// True if the outer or any inner wire had to be re-oriented during initialization. + bool wasReoriented() const { return mReoriented; } + + Vec3 toGlobal(const Vec2& point) const { return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord; } + + Vec2 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mOrigin; + return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV)}; + } + + double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); } + + bool containsLocal(const Vec2& point, bool* boundary = nullptr) const + { + if (boundary != nullptr) { + *boundary = false; + } + + const auto outerClassification = mOuterWire.classify(point, mTrimFloor); + if (outerClassification == WireClassification::Outside) { + return false; + } + if (outerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + + for (const auto& innerWire : mInnerWires) { + const auto innerClassification = innerWire.classify(point, mTrimFloor); + if (innerClassification == WireClassification::Boundary) { + if (boundary != nullptr) { + *boundary = true; + } + return true; + } + if (innerClassification == WireClassification::Inside) { + return false; + } + } + return true; + } + + bool containsPointOnSurface(const Vec3& point) const override + { + if (std::abs(planeDistance(point)) > kTolerance) { + return false; + } + return containsLocal(toLocal(point)); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const double denominator = dot(mNormal, rayDirection); + if (std::abs(denominator) <= kTolerance) { + return; + } + const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator; + if (candidateDistance < minDistance || candidateDistance > maxDistance) { + return; + } + bool onTrimBoundary = false; + if (!containsLocal(toLocal(rayOrigin + rayDirection * candidateDistance), &onTrimBoundary)) { + return; + } + hits.push_back({candidateDistance, mNormal, onTrimBoundary}); + } + + double distanceSqToPatch(const Vec3& point) const override + { + const Vec2 projectedPoint = toLocal(point); + const double signedPlaneDistance = planeDistance(point); + if (containsLocal(projectedPoint)) { + return signedPlaneDistance * signedPlaneDistance; + } + + // exact for an orthonormal frame: split into in-plane distance to the trim curves plus the + // out-of-plane plane distance + double bestCurveDistanceSq = std::numeric_limits::infinity(); + for (const auto& curve : mOuterWire.curves) { + bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint)); + } + for (const auto& innerWire : mInnerWires) { + for (const auto& curve : innerWire.curves) { + bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint)); + } + } + return bestCurveDistanceSq + signedPlaneDistance * signedPlaneDistance; + } + + Vec3 normalAt(const Vec3&) const override { return mNormal; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + Vec2 parametricLower{std::numeric_limits::infinity(), std::numeric_limits::infinity()}; + Vec2 parametricUpper{-std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; + mOuterWire.parametricBounds(parametricLower, parametricUpper); + + // the affine image of the parametric AABB contains the patch; its corners bound the 3D AABB + for (const double cornerU : {parametricLower.uCoord, parametricUpper.uCoord}) { + for (const double cornerV : {parametricLower.vCoord, parametricUpper.vCoord}) { + const Vec3 globalCorner = toGlobal({cornerU, cornerV}); + lower.xCoord = std::min(lower.xCoord, globalCorner.xCoord); + lower.yCoord = std::min(lower.yCoord, globalCorner.yCoord); + lower.zCoord = std::min(lower.zCoord, globalCorner.zCoord); + upper.xCoord = std::max(upper.xCoord, globalCorner.xCoord); + upper.yCoord = std::max(upper.yCoord, globalCorner.yCoord); + upper.zCoord = std::max(upper.zCoord, globalCorner.zCoord); + } + } + } + + double area() const + { + double parametricArea = std::abs(mOuterWire.signedArea()); + for (const auto& innerWire : mInnerWires) { + parametricArea -= std::abs(innerWire.signedArea()); + } + return std::max(0., parametricArea); + } + + /// The identity form: initialize() rejects a frame whose axes are not orthonormal, so (u, v) + /// here are already lengths in centimetres. (The polygon-wire plane is the general case.) + void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override + { + gUU = 1.; + gUV = 0.; + gVV = 1.; + } + + double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; } + + bool capacityIsExact() const override { return mCapacityExact; } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + // triangulate the sampled outer boundary; holes are ignored in the display mesh (as for the + // polygonal planar surface, visualization never influences navigation) + auto samples = mOuterWire.sampledBoundary(); + if (samples.size() < 4) { + return; + } + samples.pop_back(); // drop the closing duplicate + + SurfaceWire sampledWire; + sampledWire.vertices = std::move(samples); + + const int firstVertexIndex = static_cast(vertices.size()); + for (const auto& vertex : sampledWire.vertices) { + vertices.push_back(toGlobal(vertex)); + } + for (const auto& triangle : triangulateSimpleWire(sampledWire)) { + triangles.push_back( + {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]}); + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + auto appendWire = [&](const CurveWire& wire) { + const auto samples = wire.sampledBoundary(); + for (size_t sampleIndex = 0; sampleIndex + 1 < samples.size(); ++sampleIndex) { + edges.emplace_back(toGlobal(samples[sampleIndex]), toGlobal(samples[sampleIndex + 1])); + } + }; + appendWire(mOuterWire); + for (const auto& innerWire : mInnerWires) { + appendWire(innerWire); + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + return sampleTrimCurveOfCurveWires( + mOuterWire, mInnerWires, index, + [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples); + } + + private: + Vec3 mOrigin; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mNormal; + bool mReoriented = false; + bool mCapacityExact = true; + double mTrimFloor = 0.; ///< the wires' band floor; the plane's metric is constant + CurveWire mOuterWire; + std::vector mInnerWires; +}; + +/// Cover boxes of a band of revolution between two rim circles: the phi window in chunks, each the box of its two rim arcs. +inline void appendArcBandCoverBoxes(const Vec3& center, const Vec3& axisU, const Vec3& axisV, const Vec3& axisW, + double phiStart, double phiSweep, double heightMin, double heightMax, + double radiusAtMin, double radiusAtMax, + std::vector& boxes) +{ + const int chunks = coverChunkCount(phiSweep); + for (int chunk = 0; chunk < chunks; ++chunk) { + const double phiLow = phiStart + phiSweep * chunk / chunks; + const double phiHigh = phiStart + phiSweep * (chunk + 1) / chunks; + double lower[3]; + double upper[3]; + for (int dimension = 0; dimension < 3; ++dimension) { + double radialLow = 0.; + double radialHigh = 0.; + sinusoidRange(component(axisU, dimension), component(axisV, dimension), phiLow, phiHigh, radialLow, radialHigh); + const double centerAtMin = component(center, dimension) + heightMin * component(axisW, dimension); + const double centerAtMax = component(center, dimension) + heightMax * component(axisW, dimension); + lower[dimension] = std::min(centerAtMin + radiusAtMin * radialLow, centerAtMax + radiusAtMax * radialLow); + upper[dimension] = std::max(centerAtMin + radiusAtMin * radialHigh, centerAtMax + radiusAtMax * radialHigh); + } + boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}}); + } +} + +/// A cylinder of given radius around an axis, trimmed to a (phi, h) rectangle or by curve wires; innerWall points the normal to the axis. +class CylindricalBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius, + double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, + std::string& errorMessage) + { + if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radius) || + !std::isfinite(heightMin) || !std::isfinite(heightMax) || !std::isfinite(phiStart) || + !std::isfinite(phiSweep)) { + errorMessage = "cylindrical surface parameter is non-finite"; + return false; + } + if (radius <= kTolerance) { + errorMessage = "cylindrical surface needs a positive radius"; + return false; + } + if (heightMax - heightMin <= kTolerance) { + errorMessage = "cylindrical surface needs a positive height range"; + return false; + } + if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) { + errorMessage = "cylindrical surface needs an angular sweep in (0, 2pi]"; + return false; + } + if (!makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) { + return false; + } + + mCenter = centerPoint; + mRadius = radius; + mPhiTolerance = angularTolerance(mRadius); + mHeightMin = heightMin; + mHeightMax = heightMax; + mPhiStart = phiStart; + mPhiSweep = std::min(phiSweep, kTwoPi); + mNormalSign = innerWall ? -1. : 1.; + return true; + } + + /// Wire-trimmed overload: the wires in the (phi[rad], h[cm]) domain decide containment; the window tightens to their bounds. + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius, + double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, + const std::vector& outerTrim, const std::vector>& innerTrims, + std::string& errorMessage, double joinTolerance = kWireJoinTolerance) + { + if (!initialize(centerPoint, axis, referenceAxisU, radius, heightMin, heightMax, phiStart, phiSweep, innerWall, + errorMessage)) { + return false; + } + Vec2 lower, upper; + if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage, + parametricMetricOf(*this), joinTolerance)) { + return false; + } + mPhiStart = lower.uCoord; + mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord); + mHeightMin = lower.vCoord; + mHeightMax = upper.vCoord; + mHasWireTrim = true; + return true; + } + + bool hasWireTrim() const { return mHasWireTrim; } + + /// True if the (phi, h) point lies in the trim wire (phi unwrapped into the wire window). + bool pointInTrim(double phi, double height, bool* boundary = nullptr) const + { + const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep); + return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this)); + } + + /// Build an orthonormal frame (U, V, W) with W along \a axis and U the projection of + /// \a referenceAxisU perpendicular to W. Shared by all axis-symmetric quadric surfaces. + static bool makeFrame(const Vec3& axis, const Vec3& referenceAxisU, Vec3& axisU, Vec3& axisV, Vec3& axisW, + std::string& errorMessage) + { + if (norm(axis) <= kTolerance) { + errorMessage = "surface axis is degenerate"; + return false; + } + axisW = normalized(axis); + const Vec3 projectedU = referenceAxisU - axisW * dot(referenceAxisU, axisW); + if (norm(projectedU) <= kTolerance) { + errorMessage = "surface reference axis is parallel to the main axis"; + return false; + } + axisU = normalized(projectedU); + axisV = cross(axisW, axisU); // gives axisU x axisV = axisW + return true; + } + + bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; } + + Vec3 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mCenter; + return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)}; + } + + bool heightInRange(double height) const + { + return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance; + } + + bool phiInSweep(double phi) const + { + return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance); + } + + Vec3 pointAt(double phi, double height) const + { + return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * mRadius; + } + + bool containsPointOnSurface(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (std::abs(radialDistance - mRadius) > kTolerance) { + return false; + } + if (radialDistance <= kTolerance) { + return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined on the axis + } + const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord); + if (mHasWireTrim) { + return pointInTrim(phi, localPoint.zCoord); + } + return heightInRange(localPoint.zCoord) && phiInSweep(phi); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const Vec3 localOrigin = toLocal(rayOrigin); + const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)}; + + const double quadraticA = localDirection.xCoord * localDirection.xCoord + + localDirection.yCoord * localDirection.yCoord; + if (quadraticA <= kToleranceSq) { + return; // ray parallel to the axis: no transversal crossing of the lateral surface + } + const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord + + localOrigin.yCoord * localDirection.yCoord); + const double quadraticC = localOrigin.xCoord * localOrigin.xCoord + + localOrigin.yCoord * localOrigin.yCoord - mRadius * mRadius; + const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC; + if (discriminant <= 0.) { + return; + } + const double sqrtDiscriminant = std::sqrt(discriminant); + const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA); + const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA); + if (sameIntersection(firstRoot, secondRoot)) { + return; // tangential graze: report neither hit so crossing parity stays even + } + + for (const double candidate : {firstRoot, secondRoot}) { + if (candidate < minDistance || candidate > maxDistance) { + continue; + } + const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord; + const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord; + const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord; + const double hitPhi = std::atan2(hitV, hitU); + bool onTrimBoundary = false; + if (mHasWireTrim) { + if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) { + continue; + } + } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) { + continue; + } + const double radialDistance = std::hypot(hitU, hitV); + const Vec3 hitNormal = (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance)) * mNormalSign; + hits.push_back({candidate, hitNormal, onTrimBoundary}); + } + } + + /// Distance to the patch: exact for the parametric rectangle, a lower bound for a wire trim. + double distanceSqToPatch(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) { + return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord}, Vec2{mRadius, mHeightMin}, + Vec2{mRadius, mHeightMax}); + } + const double distanceToStartSeam = + pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax)); + const double endPhi = mPhiStart + mPhiSweep; + const double distanceToEndSeam = + pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax)); + return std::min(distanceToStartSeam, distanceToEndSeam); + } + + Vec3 normalAt(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (radialDistance <= kTolerance) { + return mAxisU * mNormalSign; // ill-defined on the axis; return a stable direction + } + return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance)) * + mNormalSign; + } + + /// (u, v) = (phi[rad], h[cm]): X_phi has length r and X_h is the unit axis. + void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override + { + cylinderParametricMetric(mRadius, gUU, gUV, gVV); + } + + /// Divergence-theorem contribution over the (phi, h) rectangle; a wire trim uses the contour form, F = (s r / 3)(a sin phi - b cos phi + r phi). + double capacityContribution() const override + { + if (mHasWireTrim) { + const double centreU = dot(mCenter, mAxisU); + const double centreV = dot(mCenter, mAxisV); + const double factor = mNormalSign * mRadius / 3.; + return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double) { + return factor * (centreU * std::sin(phi) - centreV * std::cos(phi) + mRadius * phi); + }); + } + const double endPhi = mPhiStart + mPhiSweep; + const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) - + dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart)); + const double height = mHeightMax - mHeightMin; + return mNormalSign * mRadius * height * (phiFactor + mRadius * mPhiSweep) / 3.; + } + + bool capacityIsExact() const override { return !mHasWireTrim; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + // conservative: the AABB of the two full rim circles (partial sweeps get a larger box) + for (const double height : {mHeightMin, mHeightMax}) { + const Vec3 rimCenter = mCenter + mAxisW * height; + for (int dimension = 0; dimension < 3; ++dimension) { + const double radialExtent = mRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension)); + const double centerValue = component(rimCenter, dimension); + if (dimension == 0) { + lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent); + upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent); + } else if (dimension == 1) { + lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent); + upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent); + } else { + lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent); + upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent); + } + } + } + } + + /// Cover boxes: the sweep window in angular chunks, which holds every point that realises distanceSqToPatch. + void appendCoverBoxes(std::vector& boxes) const override + { + appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax, mRadius, + mRadius, boxes); + } + + /// Number of chord segments used for rim sampling, consistent with CurveWire::sampledBoundary + /// so shared circular boundaries close against curved planar caps. + int rimSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * mPhiSweep / kTwoPi))); + } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + if (mHasWireTrim) { + appendCurveTrimMesh(mTrimOuter, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles); + return; + } + const int segments = rimSegments(); + const int firstVertexIndex = static_cast(vertices.size()); + for (int step = 0; step <= segments; ++step) { + const double phi = mPhiStart + mPhiSweep * step / segments; + vertices.push_back(pointAt(phi, mHeightMin)); + vertices.push_back(pointAt(phi, mHeightMax)); + } + for (int step = 0; step < segments; ++step) { + const int base = firstVertexIndex + 2 * step; + triangles.push_back({base, base + 2, base + 3}); + triangles.push_back({base, base + 3, base + 1}); + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + if (mHasWireTrim) { + // the (phi, h) -> 3D map is orientation-consistent with the outward normal, so a CCW trim + // loop yields a CCW 3D loop for an outer wall; the sign is just mNormalSign + appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges); + return; + } + // boundary counter-clockwise seen along the outward normal, so rims shared with caps cancel + const int segments = rimSegments(); + auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) { + if (mNormalSign > 0.) { + edges.emplace_back(edgeStart, edgeEnd); + } else { + edges.emplace_back(edgeEnd, edgeStart); + } + }; + for (int step = 0; step < segments; ++step) { + const double phi = mPhiStart + mPhiSweep * step / segments; + const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments; + emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin)); + emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax)); + } + if (!fullSweep()) { + const double endPhi = mPhiStart + mPhiSweep; + emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax)); + emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin)); + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + if (!mHasWireTrim) { + return false; // a parametric-rectangle trim carries no per-edge curve to sample + } + return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples); + } + + private: + Vec3 mCenter; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mAxisW; + double mRadius = 0.; + double mHeightMin = 0.; + double mHeightMax = 0.; + double mPhiStart = 0.; + double mPhiSweep = kTwoPi; + double mPhiTolerance = 0.; ///< angularTolerance of the radius + double mNormalSign = 1.; + bool mHasWireTrim = false; + CurveWire mTrimOuter; + std::vector mTrimInner; +}; + +/// A sphere of given radius trimmed to a (theta, phi) rectangle or by curve wires; innerWall points the normal to the centre. +class SphericalBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius, + double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall, + std::string& errorMessage) + { + if (!finite(center) || !finite(polarAxis) || !finite(referenceAxisU) || !std::isfinite(radius) || + !std::isfinite(thetaMin) || !std::isfinite(thetaMax) || !std::isfinite(phiStart) || + !std::isfinite(phiSweep)) { + errorMessage = "spherical surface parameter is non-finite"; + return false; + } + if (radius <= kTolerance) { + errorMessage = "spherical surface needs a positive radius"; + return false; + } + if (thetaMin < -kTolerance || thetaMax > kPi + kTolerance || thetaMax - thetaMin <= kTolerance) { + errorMessage = "spherical surface needs a polar range within [0, pi]"; + return false; + } + if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) { + errorMessage = "spherical surface needs an angular sweep in (0, 2pi]"; + return false; + } + if (!CylindricalBoundedSurface::makeFrame(polarAxis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) { + return false; + } + + mCenter = center; + mRadius = radius; + mThetaMin = std::max(0., thetaMin); + mThetaMax = std::min(kPi, thetaMax); + mPhiStart = phiStart; + mPhiSweep = std::min(phiSweep, kTwoPi); + mNormalSign = innerWall ? -1. : 1.; + return true; + } + + /// Wire-trimmed overload: the wires in the (phi[rad], theta[rad]) domain decide containment; the window tightens to their bounds. + bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius, + double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall, + const std::vector& outerTrim, const std::vector>& innerTrims, + std::string& errorMessage, double joinTolerance = kWireJoinTolerance) + { + if (!initialize(center, polarAxis, referenceAxisU, radius, thetaMin, thetaMax, phiStart, phiSweep, innerWall, + errorMessage)) { + return false; + } + Vec2 lower, upper; + if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage, + parametricMetricOf(*this), joinTolerance)) { + return false; + } + mPhiStart = lower.uCoord; + mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord); + mThetaMin = std::max(0., lower.vCoord); + mThetaMax = std::min(kPi, upper.vCoord); + mHasWireTrim = true; + return true; + } + + bool hasWireTrim() const { return mHasWireTrim; } + + /// True if the (phi, theta) point lies in the trim wire (phi unwrapped into the wire window). + bool pointInTrim(double phi, double theta, bool* boundary = nullptr) const + { + const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep); + return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, theta}, boundary, parametricMetricOf(*this)); + } + + bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; } + + Vec3 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mCenter; + return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)}; + } + + bool directionInTrim(const Vec3& localPoint, bool* boundary = nullptr) const + { + if (boundary != nullptr) { + *boundary = false; + } + const double pointRadius = norm(localPoint); + if (pointRadius <= kTolerance) { + return true; // the center is angle-degenerate; every patch point is equidistant + } + const double thetaTolerance = angularTolerance(mRadius); + const double theta = std::acos(std::max(-1., std::min(1., localPoint.zCoord / pointRadius))); + const double transverseDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (mHasWireTrim) { + if (transverseDistance <= kTolerance) { + // on the polar axis phi is degenerate; accept by the wire's theta (v) range + return theta >= mThetaMin - thetaTolerance && theta <= mThetaMax + thetaTolerance; + } + return pointInTrim(std::atan2(localPoint.yCoord, localPoint.xCoord), theta, boundary); + } + if (theta < mThetaMin - thetaTolerance || theta > mThetaMax + thetaTolerance) { + return false; + } + if (transverseDistance <= kTolerance) { + return true; // on the polar axis phi is degenerate + } + return angleInSweepRange(std::atan2(localPoint.yCoord, localPoint.xCoord), mPhiStart, mPhiSweep, + thetaTolerance); + } + + Vec3 pointAt(double theta, double phi) const + { + const double sinTheta = std::sin(theta); + return mCenter + (mAxisU * (sinTheta * std::cos(phi)) + mAxisV * (sinTheta * std::sin(phi)) + + mAxisW * std::cos(theta)) * + mRadius; + } + + bool containsPointOnSurface(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + if (std::abs(norm(localPoint) - mRadius) > kTolerance) { + return false; + } + return directionInTrim(localPoint); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const Vec3 relativeOrigin = rayOrigin - mCenter; + const double quadraticA = normSq(rayDirection); + if (quadraticA <= kToleranceSq) { + return; + } + const double quadraticB = 2. * dot(relativeOrigin, rayDirection); + const double quadraticC = normSq(relativeOrigin) - mRadius * mRadius; + const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC; + if (discriminant <= 0.) { + return; + } + const double sqrtDiscriminant = std::sqrt(discriminant); + const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA); + const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA); + if (sameIntersection(firstRoot, secondRoot)) { + return; // tangential graze + } + + for (const double candidate : {firstRoot, secondRoot}) { + if (candidate < minDistance || candidate > maxDistance) { + continue; + } + const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate); + bool onTrimBoundary = false; + if (!directionInTrim(localHit, &onTrimBoundary)) { + continue; + } + hits.push_back({candidate, + (mAxisU * localHit.xCoord + mAxisV * localHit.yCoord + mAxisW * localHit.zCoord) * + (mNormalSign / mRadius), + onTrimBoundary}); + } + } + + /// Distance to the patch: exact inside the trim, else the full-sphere distance, a lower bound. + double distanceSqToPatch(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialOffset = norm(localPoint) - mRadius; + return radialOffset * radialOffset; + } + + Vec3 normalAt(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double pointRadius = norm(localPoint); + if (pointRadius <= kTolerance) { + return mAxisW * mNormalSign; // ill-defined at the center; return a stable direction + } + return (mAxisU * localPoint.xCoord + mAxisV * localPoint.yCoord + mAxisW * localPoint.zCoord) * + (mNormalSign / pointRadius); + } + + /// (u, v) = (phi[rad], theta[rad]); gUU vanishes at either pole. + void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override + { + sphereParametricMetric(mRadius, uv.vCoord, gUU, gUV, gVV); + } + + /// Divergence-theorem contribution over the (theta, phi) rectangle; a wire trim uses the contour form in (phi, theta). + double capacityContribution() const override + { + if (mHasWireTrim) { + const double centreU = dot(mCenter, mAxisU); + const double centreV = dot(mCenter, mAxisV); + const double centreW = dot(mCenter, mAxisW); + const double factor = mNormalSign * mRadius * mRadius / 3.; + return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double theta) { + const double sinTheta = std::sin(theta); + return factor * sinTheta * + (sinTheta * (centreU * std::sin(phi) - centreV * std::cos(phi)) + + (centreW * std::cos(theta) + mRadius) * phi); + }); + } + const double endPhi = mPhiStart + mPhiSweep; + const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) - + dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart)); + const double thetaIntegralSinSq = + 0.5 * ((mThetaMax - std::sin(mThetaMax) * std::cos(mThetaMax)) - + (mThetaMin - std::sin(mThetaMin) * std::cos(mThetaMin))); + const double thetaIntegralSinCos = + 0.5 * (std::sin(mThetaMax) * std::sin(mThetaMax) - std::sin(mThetaMin) * std::sin(mThetaMin)); + const double thetaIntegralSin = std::cos(mThetaMin) - std::cos(mThetaMax); + return mNormalSign * mRadius * mRadius * + (phiFactor * thetaIntegralSinSq + dot(mCenter, mAxisW) * mPhiSweep * thetaIntegralSinCos + + mRadius * mPhiSweep * thetaIntegralSin) / + 3.; + } + + bool capacityIsExact() const override { return !mHasWireTrim; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + lower.xCoord = std::min(lower.xCoord, mCenter.xCoord - mRadius); + lower.yCoord = std::min(lower.yCoord, mCenter.yCoord - mRadius); + lower.zCoord = std::min(lower.zCoord, mCenter.zCoord - mRadius); + upper.xCoord = std::max(upper.xCoord, mCenter.xCoord + mRadius); + upper.yCoord = std::max(upper.yCoord, mCenter.yCoord + mRadius); + upper.zCoord = std::max(upper.zCoord, mCenter.zCoord + mRadius); + } + + /// Cover boxes: the whole sphere in (theta, phi) chunks, since distanceSqToPatch ignores the trim. + void appendCoverBoxes(std::vector& boxes) const override + { + const int thetaChunks = coverChunkCount(kPi); + const int phiChunks = coverChunkCount(kTwoPi); + for (int thetaChunk = 0; thetaChunk < thetaChunks; ++thetaChunk) { + const double thetaLow = kPi * thetaChunk / thetaChunks; + const double thetaHigh = kPi * (thetaChunk + 1) / thetaChunks; + for (int phiChunk = 0; phiChunk < phiChunks; ++phiChunk) { + const double phiLow = kTwoPi * phiChunk / phiChunks; + const double phiHigh = kTwoPi * (phiChunk + 1) / phiChunks; + double lower[3]; + double upper[3]; + for (int dimension = 0; dimension < 3; ++dimension) { + double inPlaneLow = 0.; + double inPlaneHigh = 0.; + sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), phiLow, phiHigh, inPlaneLow, + inPlaneHigh); + // sin(theta) >= 0 on [0, pi], so the chunk extremes are the theta sinusoid at s's own extremes + const double axisComponent = component(mAxisW, dimension); + const double high = sinusoidMaximum(axisComponent, inPlaneHigh, thetaLow, thetaHigh); + const double low = sinusoidMinimum(axisComponent, inPlaneLow, thetaLow, thetaHigh); + lower[dimension] = component(mCenter, dimension) + mRadius * low; + upper[dimension] = component(mCenter, dimension) + mRadius * high; + } + boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}}); + } + } + } + + int phiSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * mPhiSweep / kTwoPi))); + } + + int thetaSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * (mThetaMax - mThetaMin) / kTwoPi))); + } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + if (mHasWireTrim) { + appendCurveTrimMesh(mTrimOuter, [this](double phi, double theta) { return pointAt(theta, phi); }, vertices, triangles); + return; + } + const int phiSteps = phiSegments(); + const int thetaSteps = thetaSegments(); + const int firstVertexIndex = static_cast(vertices.size()); + for (int thetaStep = 0; thetaStep <= thetaSteps; ++thetaStep) { + const double theta = mThetaMin + (mThetaMax - mThetaMin) * thetaStep / thetaSteps; + for (int phiStep = 0; phiStep <= phiSteps; ++phiStep) { + vertices.push_back(pointAt(theta, mPhiStart + mPhiSweep * phiStep / phiSteps)); + } + } + const int rowLength = phiSteps + 1; + for (int thetaStep = 0; thetaStep < thetaSteps; ++thetaStep) { + for (int phiStep = 0; phiStep < phiSteps; ++phiStep) { + const int base = firstVertexIndex + thetaStep * rowLength + phiStep; + triangles.push_back({base, base + 1, base + rowLength + 1}); + triangles.push_back({base, base + rowLength + 1, base + rowLength}); + } + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + if (mHasWireTrim) { + // the (phi, theta) -> 3D map is orientation-*reversed* relative to the outward normal + // (X_phi x X_theta points inward), so the sign is -mNormalSign + appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double theta) { return pointAt(theta, phi); }, -mNormalSign, edges); + return; + } + // boundary of the (theta, phi) rectangle, traversed counter-clockwise for an outer wall; + // pole rims are degenerate points and full-sweep phi seams cancel, so both are skipped + auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) { + if (mNormalSign > 0.) { + edges.emplace_back(edgeStart, edgeEnd); + } else { + edges.emplace_back(edgeEnd, edgeStart); + } + }; + const double thetaTolerance = angularTolerance(mRadius); + const int phiSteps = phiSegments(); + const double endPhi = mPhiStart + mPhiSweep; + if (mThetaMin > thetaTolerance) { + for (int step = 0; step < phiSteps; ++step) { + const double phi = mPhiStart + mPhiSweep * step / phiSteps; + const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps; + emitEdge(pointAt(mThetaMin, nextPhi), pointAt(mThetaMin, phi)); // -phi at the small-theta rim + } + } + if (mThetaMax < kPi - thetaTolerance) { + for (int step = 0; step < phiSteps; ++step) { + const double phi = mPhiStart + mPhiSweep * step / phiSteps; + const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps; + emitEdge(pointAt(mThetaMax, phi), pointAt(mThetaMax, nextPhi)); // +phi at the large-theta rim + } + } + if (!fullSweep()) { + const int thetaSteps = thetaSegments(); + for (int step = 0; step < thetaSteps; ++step) { + const double theta = mThetaMin + (mThetaMax - mThetaMin) * step / thetaSteps; + const double nextTheta = mThetaMin + (mThetaMax - mThetaMin) * (step + 1) / thetaSteps; + emitEdge(pointAt(theta, mPhiStart), pointAt(nextTheta, mPhiStart)); // +theta at phiStart + emitEdge(pointAt(nextTheta, endPhi), pointAt(theta, endPhi)); // -theta at phiEnd + } + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + if (!mHasWireTrim) { + return false; // a parametric-rectangle trim carries no per-edge curve to sample + } + return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double theta) { return pointAt(theta, phi); }, samples); + } + + private: + Vec3 mCenter; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mAxisW; + double mRadius = 0.; + double mThetaMin = 0.; + double mThetaMax = kPi; + double mPhiStart = 0.; + double mPhiSweep = kTwoPi; + double mNormalSign = 1.; + bool mHasWireTrim = false; + CurveWire mTrimOuter; + std::vector mTrimInner; +}; + +/// A cone whose radius varies linearly with height, trimmed as the cylinder; one radius may be zero (an apex) and slope 0 is a cylinder. +class ConicalBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin, + double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep, + bool innerWall, std::string& errorMessage) + { + if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radiusAtMin) || + !std::isfinite(radiusAtMax) || !std::isfinite(heightMin) || !std::isfinite(heightMax) || + !std::isfinite(phiStart) || !std::isfinite(phiSweep)) { + errorMessage = "conical surface parameter is non-finite"; + return false; + } + if (radiusAtMin < -kTolerance || radiusAtMax < -kTolerance || + std::max(radiusAtMin, radiusAtMax) <= kTolerance) { + errorMessage = "conical surface needs non-negative radii, at least one positive"; + return false; + } + if (heightMax - heightMin <= kTolerance) { + errorMessage = "conical surface needs a positive height range"; + return false; + } + if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) { + errorMessage = "conical surface needs an angular sweep in (0, 2pi]"; + return false; + } + if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) { + return false; + } + + mCenter = centerPoint; + mHeightMin = heightMin; + mHeightMax = heightMax; + mSlope = (radiusAtMax - radiusAtMin) / (heightMax - heightMin); + mRadius0 = radiusAtMin - mSlope * heightMin; // radius at h = 0 of the linear law + mPhiStart = phiStart; + mPhiSweep = std::min(phiSweep, kTwoPi); + mNormalSign = innerWall ? -1. : 1.; + mPhiTolerance = angularTolerance(meanRadius()); + return true; + } + + /// Wire-trimmed overload: the scalar radii pin r(h); the wires in the (phi[rad], h[cm]) domain decide containment. + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin, + double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims, std::string& errorMessage, + double joinTolerance = kWireJoinTolerance) + { + if (!initialize(centerPoint, axis, referenceAxisU, radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart, + phiSweep, innerWall, errorMessage)) { + return false; + } + Vec2 lower, upper; + if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage, + parametricMetricOf(*this), joinTolerance)) { + return false; + } + mPhiStart = lower.uCoord; + mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord); + mHeightMin = lower.vCoord; + mHeightMax = upper.vCoord; + mPhiTolerance = angularTolerance(meanRadius()); + mHasWireTrim = true; + return true; + } + + bool hasWireTrim() const { return mHasWireTrim; } + + /// True if the (phi, h) point lies in the trim wire (phi unwrapped into the wire window). + bool pointInTrim(double phi, double height, bool* boundary = nullptr) const + { + const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep); + return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this)); + } + + bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; } + + double radiusAt(double height) const { return mRadius0 + mSlope * height; } + + double meanRadius() const { return 0.5 * (radiusAt(mHeightMin) + radiusAt(mHeightMax)); } + + Vec3 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mCenter; + return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)}; + } + + bool heightInRange(double height) const + { + return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance; + } + + bool phiInSweep(double phi) const + { + return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance); + } + + Vec3 pointAt(double phi, double height) const + { + return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * radiusAt(height); + } + + bool containsPointOnSurface(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double surfaceRadius = radiusAt(localPoint.zCoord); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + // |rho - r(h)| overestimates the true surface distance by sqrt(1 + slope^2) + if (std::abs(radialDistance - surfaceRadius) > kTolerance * std::sqrt(1. + mSlope * mSlope)) { + return false; + } + if (radialDistance <= kTolerance) { + return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined near the apex + } + const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord); + if (mHasWireTrim) { + return pointInTrim(phi, localPoint.zCoord); + } + return heightInRange(localPoint.zCoord) && phiInSweep(phi); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const Vec3 localOrigin = toLocal(rayOrigin); + const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)}; + + // (ox + t dx)^2 + (oy + t dy)^2 = (radius0 + slope * (oz + t dz))^2 + const double surfaceRadiusAtOrigin = mRadius0 + mSlope * localOrigin.zCoord; + const double quadraticA = localDirection.xCoord * localDirection.xCoord + + localDirection.yCoord * localDirection.yCoord - + mSlope * mSlope * localDirection.zCoord * localDirection.zCoord; + const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord + + localOrigin.yCoord * localDirection.yCoord - + mSlope * localDirection.zCoord * surfaceRadiusAtOrigin); + const double quadraticC = localOrigin.xCoord * localOrigin.xCoord + + localOrigin.yCoord * localOrigin.yCoord - + surfaceRadiusAtOrigin * surfaceRadiusAtOrigin; + + std::array candidates{}; + int candidateCount = 0; + if (std::abs(quadraticA) <= kToleranceSq) { + if (std::abs(quadraticB) <= kToleranceSq) { + return; // ray runs along the cone surface or its asymptote: no transversal crossing + } + candidates[candidateCount++] = -quadraticC / quadraticB; + } else { + const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC; + if (discriminant <= 0.) { + return; + } + const double sqrtDiscriminant = std::sqrt(discriminant); + const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA); + const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA); + if (sameIntersection(firstRoot, secondRoot)) { + return; // tangential graze (this also covers rays through the exact apex) + } + candidates[candidateCount++] = std::min(firstRoot, secondRoot); + candidates[candidateCount++] = std::max(firstRoot, secondRoot); + } + + for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) { + const double candidate = candidates[candidateIndex]; + if (candidate < minDistance || candidate > maxDistance) { + continue; + } + const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord; + const double hitSurfaceRadius = radiusAt(hitHeight); + if (hitSurfaceRadius < -kTolerance) { + continue; // mirror nappe of the infinite cone + } + const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord; + const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord; + const double radialDistance = std::hypot(hitU, hitV); + if (radialDistance <= kTolerance) { + continue; // apex hit: the normal is undefined there + } + const double hitPhi = std::atan2(hitV, hitU); + bool onTrimBoundary = false; + if (mHasWireTrim) { + if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) { + continue; + } + } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) { + continue; + } + const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope); + const Vec3 hitNormal = + (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance) - mAxisW * mSlope) * normalScale; + hits.push_back({candidate, hitNormal, onTrimBoundary}); + } + } + + /// Distance to the patch: exact for the parametric rectangle, a lower bound for a wire trim. + double distanceSqToPatch(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) { + return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord}, + Vec2{radiusAt(mHeightMin), mHeightMin}, Vec2{radiusAt(mHeightMax), mHeightMax}); + } + const double endPhi = mPhiStart + mPhiSweep; + const double distanceToStartSeam = + pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax)); + const double distanceToEndSeam = + pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax)); + return std::min(distanceToStartSeam, distanceToEndSeam); + } + + Vec3 normalAt(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord); + const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope); + if (radialDistance <= kTolerance) { + return (mAxisU - mAxisW * mSlope) * normalScale; // ill-defined on the axis; stable fallback + } + return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance) - + mAxisW * mSlope) * + normalScale; + } + + /// (u, v) = (phi[rad], h[cm]): the azimuthal scale is the local radius, and a step in h spans sqrt(1 + slope^2). + void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override + { + coneParametricMetric(radiusAt(uv.vCoord), mSlope, gUU, gUV, gVV); + } + + /// Divergence-theorem contribution over the (phi, h) rectangle; a wire trim uses the contour form, as for the cylinder. + double capacityContribution() const override + { + if (mHasWireTrim) { + const double centreU = dot(mCenter, mAxisU); + const double centreV = dot(mCenter, mAxisV); + const double centreW = dot(mCenter, mAxisW); + return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double height) { + const double localRadius = radiusAt(height); + return mNormalSign / 3. * localRadius * + (centreU * std::sin(phi) - centreV * std::cos(phi) + + (localRadius - mSlope * (centreW + height)) * phi); + }); + } + const double endPhi = mPhiStart + mPhiSweep; + const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) - + dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart)); + const double radiusIntegral = mRadius0 * (mHeightMax - mHeightMin) + + 0.5 * mSlope * (mHeightMax * mHeightMax - mHeightMin * mHeightMin); + return mNormalSign * radiusIntegral * + (phiFactor + (mRadius0 - mSlope * dot(mCenter, mAxisW)) * mPhiSweep) / 3.; + } + + bool capacityIsExact() const override { return !mHasWireTrim; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + for (const double height : {mHeightMin, mHeightMax}) { + const Vec3 rimCenter = mCenter + mAxisW * height; + const double rimRadius = std::max(0., radiusAt(height)); + for (int dimension = 0; dimension < 3; ++dimension) { + const double radialExtent = + rimRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension)); + const double centerValue = component(rimCenter, dimension); + if (dimension == 0) { + lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent); + upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent); + } else if (dimension == 1) { + lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent); + upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent); + } else { + lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent); + upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent); + } + } + } + } + + /// Cover boxes: as for the cylinder, with the rim radii from the linear radius law. + void appendCoverBoxes(std::vector& boxes) const override + { + appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax, + std::max(0., radiusAt(mHeightMin)), std::max(0., radiusAt(mHeightMax)), boxes); + } + + int rimSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * mPhiSweep / kTwoPi))); + } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + if (mHasWireTrim) { + appendCurveTrimMesh(mTrimOuter, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles); + return; + } + const int segments = rimSegments(); + const int firstVertexIndex = static_cast(vertices.size()); + for (int step = 0; step <= segments; ++step) { + const double phi = mPhiStart + mPhiSweep * step / segments; + vertices.push_back(pointAt(phi, mHeightMin)); + vertices.push_back(pointAt(phi, mHeightMax)); + } + for (int step = 0; step < segments; ++step) { + const int base = firstVertexIndex + 2 * step; + // skip triangles that collapse at an apex rim + if (radiusAt(mHeightMin) > kTolerance) { + triangles.push_back({base, base + 2, base + 3}); + } + if (radiusAt(mHeightMax) > kTolerance) { + triangles.push_back({base, base + 3, base + 1}); + } + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + if (mHasWireTrim) { + // the (phi, h) -> 3D map is orientation-consistent with the outward normal (as for the + // cylinder), so the sign is just mNormalSign + appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges); + return; + } + // same boundary orientation as the cylinder; an apex rim degenerates to a point and is + // skipped so an apex cone closes against just one cap + const int segments = rimSegments(); + auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) { + if (mNormalSign > 0.) { + edges.emplace_back(edgeStart, edgeEnd); + } else { + edges.emplace_back(edgeEnd, edgeStart); + } + }; + for (int step = 0; step < segments; ++step) { + const double phi = mPhiStart + mPhiSweep * step / segments; + const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments; + if (radiusAt(mHeightMin) > kTolerance) { + emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin)); + } + if (radiusAt(mHeightMax) > kTolerance) { + emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax)); + } + } + if (!fullSweep()) { + const double endPhi = mPhiStart + mPhiSweep; + emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax)); + emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin)); + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + if (!mHasWireTrim) { + return false; // a parametric-rectangle trim carries no per-edge curve to sample + } + return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples); + } + + private: + Vec3 mCenter; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mAxisW; + double mRadius0 = 0.; + double mSlope = 0.; + double mHeightMin = 0.; + double mHeightMax = 0.; + double mPhiStart = 0.; + double mPhiSweep = kTwoPi; + double mPhiTolerance = 0.; ///< angularTolerance of the mean radius of the final window + double mNormalSign = 1.; + bool mHasWireTrim = false; + CurveWire mTrimOuter; + std::vector mTrimInner; +}; + +/// A torus of major radius R and minor radius r, trimmed to a (phiRing, phiTube) rectangle or by curve wires; +/// X(u, v) = centre + (U cos u + V sin u)(R + r cos v) + W r sin v, orientation-consistent with the outward normal. +class TorusBoundedSurface final : public BoundedSurface +{ + public: + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius, + double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep, + bool innerWall, std::string& errorMessage) + { + if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(majorRadius) || + !std::isfinite(minorRadius) || !std::isfinite(phiStart) || !std::isfinite(phiSweep) || + !std::isfinite(tubeStart) || !std::isfinite(tubeSweep)) { + errorMessage = "toroidal surface parameter is non-finite"; + return false; + } + if (majorRadius <= kTolerance || minorRadius <= kTolerance) { + errorMessage = "toroidal surface needs positive major and minor radii"; + return false; + } + if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) { + errorMessage = "toroidal surface needs a ring sweep in (0, 2pi]"; + return false; + } + if (tubeSweep <= kTolerance || tubeSweep > kTwoPi + kTolerance) { + errorMessage = "toroidal surface needs a tube sweep in (0, 2pi]"; + return false; + } + if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) { + return false; + } + + mCenter = centerPoint; + mMajorRadius = majorRadius; + mMinorRadius = minorRadius; + mRingTolerance = angularTolerance(mMajorRadius); + mTubeTolerance = angularTolerance(mMinorRadius); + mPhiStart = phiStart; + mPhiSweep = std::min(phiSweep, kTwoPi); + mTubeStart = tubeStart; + mTubeSweep = std::min(tubeSweep, kTwoPi); + mNormalSign = innerWall ? -1. : 1.; + return true; + } + + /// Wire-trimmed overload: the wires in the (phiRing, phiTube) domain decide containment; a trim wrapping a full turn is refused. + bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius, + double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims, std::string& errorMessage, + double joinTolerance = kWireJoinTolerance) + { + if (!initialize(centerPoint, axis, referenceAxisU, majorRadius, minorRadius, phiStart, phiSweep, tubeStart, + tubeSweep, innerWall, errorMessage)) { + return false; + } + Vec2 lower, upper; + if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage, + parametricMetricOf(*this), joinTolerance)) { + return false; + } + if (upper.vCoord - lower.vCoord > kTwoPi + kTolerance) { + errorMessage = "toroidal trim wire spans more than a full turn in the tube angle"; + return false; + } + mPhiStart = lower.uCoord; + mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord); + mTubeStart = lower.vCoord; + mTubeSweep = std::min(kTwoPi, upper.vCoord - lower.vCoord); + mHasWireTrim = true; + return true; + } + + bool hasWireTrim() const { return mHasWireTrim; } + + /// Whether (phiRing, phiTube) lies in the trim wire, both angles unwrapped into their windows. + bool pointInTrim(double phiRing, double phiTube, bool* boundary = nullptr) const + { + const double uCoord = unwrapAngleInto(phiRing, mPhiStart, mPhiStart + mPhiSweep); + const double vCoord = unwrapAngleInto(phiTube, mTubeStart, mTubeStart + mTubeSweep); + return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, vCoord}, boundary, parametricMetricOf(*this)); + } + + bool fullRingSweep() const { return mPhiSweep >= kTwoPi - kTolerance; } + bool fullTubeSweep() const { return mTubeSweep >= kTwoPi - kTolerance; } + + Vec3 toLocal(const Vec3& point) const + { + const Vec3 relativePoint = point - mCenter; + return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)}; + } + + bool ringInSweep(double phiRing) const + { + return angleInSweepRange(phiRing, mPhiStart, mPhiSweep, mRingTolerance); + } + + bool tubeInSweep(double phiTube) const + { + return angleInSweepRange(phiTube, mTubeStart, mTubeSweep, mTubeTolerance); + } + + Vec3 pointAt(double phiRing, double phiTube) const + { + const double ringRadius = mMajorRadius + mMinorRadius * std::cos(phiTube); + return mCenter + (mAxisU * std::cos(phiRing) + mAxisV * std::sin(phiRing)) * ringRadius + + mAxisW * (mMinorRadius * std::sin(phiTube)); + } + + /// Unit outward normal (pointing away from the tube spine) from a local surface point. + Vec3 localNormal(const Vec3& localPoint) const + { + const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord); + if (rho <= kTolerance) { + return mAxisW * (localPoint.zCoord >= 0. ? mNormalSign : -mNormalSign); + } + const double radialFactor = (rho - mMajorRadius) / rho; + Vec3 normal{radialFactor * localPoint.xCoord, radialFactor * localPoint.yCoord, localPoint.zCoord}; + const double length = norm(normal); + if (length <= kTolerance) { + return mAxisU * mNormalSign; + } + return (mAxisU * normal.xCoord + mAxisV * normal.yCoord + mAxisW * normal.zCoord) * (mNormalSign / length); + } + + bool containsPointOnSurface(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord); + const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius; + if (std::abs(meridianDistance) > kTolerance) { + return false; + } + const double phiTube = std::atan2(localPoint.zCoord, rho - mMajorRadius); + if (rho <= kTolerance) { + return false; // on the axis phiRing is undefined (only reachable on a horn/spindle torus) + } + const double phiRing = std::atan2(localPoint.yCoord, localPoint.xCoord); + if (mHasWireTrim) { + return pointInTrim(phiRing, phiTube); + } + return ringInSweep(phiRing) && tubeInSweep(phiTube); + } + + void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance, + double maxDistance, std::vector& hits) const override + { + const Vec3 localOrigin = toLocal(rayOrigin); + const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)}; + + // Torus implicit form (local): (|X|^2 + R^2 - r^2)^2 = 4 R^2 (x^2 + y^2). Substituting the ray + // X = O + t D gives a quartic in t whose leading coefficient is |D|^4 > 0. + const double dirDotDir = normSq(localDirection); + if (dirDotDir <= kToleranceSq) { + return; // degenerate direction + } + const double originDotDir = dot(localOrigin, localDirection); + const double originDotOrigin = normSq(localOrigin); + const double constantK = mMajorRadius * mMajorRadius - mMinorRadius * mMinorRadius; + const double transverseE = localDirection.xCoord * localDirection.xCoord + + localDirection.yCoord * localDirection.yCoord; + const double transverseF = localOrigin.xCoord * localDirection.xCoord + + localOrigin.yCoord * localDirection.yCoord; + const double transverseG = localOrigin.xCoord * localOrigin.xCoord + + localOrigin.yCoord * localOrigin.yCoord; + const double fourRSquared = 4. * mMajorRadius * mMajorRadius; + + const double coeff4 = dirDotDir * dirDotDir; + const double coeff3 = 4. * dirDotDir * originDotDir; + const double coeff2 = + 4. * originDotDir * originDotDir + 2. * dirDotDir * (originDotOrigin + constantK) - fourRSquared * transverseE; + const double coeff1 = 4. * originDotDir * (originDotOrigin + constantK) - 2. * fourRSquared * transverseF; + const double coeff0 = (originDotOrigin + constantK) * (originDotOrigin + constantK) - fourRSquared * transverseG; + + QuarticRoots candidates = solveQuarticReal(coeff4, coeff3, coeff2, coeff1, coeff0); + if (candidates.empty()) { + return; + } + std::sort(candidates.begin(), candidates.end()); + + // an even-sized cluster of near-equal roots is a tangency and is dropped; an odd one is one crossing at its mean + size_t rootIndex = 0; + while (rootIndex < candidates.size()) { + size_t clusterEnd = rootIndex + 1; + double clusterSum = candidates[rootIndex]; + while (clusterEnd < candidates.size() && sameIntersection(candidates[clusterEnd], candidates[clusterEnd - 1])) { + clusterSum += candidates[clusterEnd]; + ++clusterEnd; + } + const size_t clusterSize = clusterEnd - rootIndex; + rootIndex = clusterEnd; + if ((clusterSize & 1u) == 0u) { + continue; // tangential graze + } + const double candidate = clusterSum / static_cast(clusterSize); + if (candidate < minDistance || candidate > maxDistance) { + continue; + } + const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate); + const double rho = std::hypot(localHit.xCoord, localHit.yCoord); + if (rho <= kTolerance) { + continue; + } + const double phiTube = std::atan2(localHit.zCoord, rho - mMajorRadius); + const double phiRing = std::atan2(localHit.yCoord, localHit.xCoord); + bool onTrimBoundary = false; + if (mHasWireTrim) { + if (!pointInTrim(phiRing, phiTube, &onTrimBoundary)) { + continue; + } + } else if (!ringInSweep(phiRing) || !tubeInSweep(phiTube)) { + continue; + } + hits.push_back({candidate, localNormal(localHit), onTrimBoundary}); + } + } + + /// Distance to the patch: exact for the full torus by the meridian distance, a lower bound for a trimmed patch. + double distanceSqToPatch(const Vec3& point) const override + { + const Vec3 localPoint = toLocal(point); + const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord); + const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius; + return meridianDistance * meridianDistance; + } + + Vec3 normalAt(const Vec3& point) const override { return localNormal(toLocal(point)); } + + /// (u, v) = (phiRing[rad], phiTube[rad]): the tube scale is r, the ring scale the distance from the axis. + void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override + { + torusParametricMetric(mMajorRadius, mMinorRadius, uv.vCoord, gUU, gUV, gVV); + } + + /// Divergence-theorem contribution over the (phiRing, phiTube) rectangle; a wire trim uses the contour form. + double capacityContribution() const override + { + if (mHasWireTrim) { + const double centreU = dot(mCenter, mAxisU); + const double centreV = dot(mCenter, mAxisV); + const double centreW = dot(mCenter, mAxisW); + return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phiRing, double phiTube) { + const double cosTube = std::cos(phiTube); + const double sinTube = std::sin(phiTube); + const double rho = mMajorRadius + mMinorRadius * cosTube; + return mNormalSign * mMinorRadius * rho / 3. * + (cosTube * (centreU * std::sin(phiRing) - centreV * std::cos(phiRing)) + + (centreW * sinTube + rho * cosTube + mMinorRadius * sinTube * sinTube) * phiRing); + }); + } + // Closed form over u in [u0, u1] (ring) and v in [v0, v1] (tube). + const double majorR = mMajorRadius; + const double minorR = mMinorRadius; + const double u0 = mPhiStart, u1 = mPhiStart + mPhiSweep; + const double v0 = mTubeStart, v1 = mTubeStart + mTubeSweep; + const double centerU = dot(mCenter, mAxisU); + const double centerV = dot(mCenter, mAxisV); + const double centerW = dot(mCenter, mAxisW); + const double deltaU = u1 - u0; + const double deltaV = v1 - v0; + const double sinIntegralU = std::sin(u1) - std::sin(u0); // integral cos u du + const double cosIntegralU = std::cos(u0) - std::cos(u1); // integral sin u du + const double sinIntegralV = std::sin(v1) - std::sin(v0); // integral cos v dv + const double sinFromCosV = std::cos(v0) - std::cos(v1); // integral sin v dv + const double cosSquaredV = 0.5 * deltaV + 0.25 * (std::sin(2. * v1) - std::sin(2. * v0)); // integral cos^2 v dv + const double sinCosV = 0.25 * (std::cos(2. * v0) - std::cos(2. * v1)); // integral sin v cos v dv + + // centre-independent part, integrated over v then multiplied by the ring span + const double centerlessV = + minorR * ((majorR * majorR + minorR * minorR) * sinIntegralV + majorR * minorR * deltaV + + majorR * minorR * cosSquaredV); + // W component of the centre offset + const double centerWpart = minorR * (majorR * sinFromCosV + minorR * sinCosV); + // U/V components of the centre offset (ring-angle dependent) + const double centerUVpart = + (centerU * sinIntegralU + centerV * cosIntegralU) * minorR * (majorR * sinIntegralV + minorR * cosSquaredV); + + const double total = deltaU * centerlessV + deltaU * centerW * centerWpart + centerUVpart; + return mNormalSign * total / 3.; + } + + bool capacityIsExact() const override { return !mHasWireTrim; } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + // conservative: the AABB of the full torus (partial sweeps get a larger box) + const double outerRadius = mMajorRadius + mMinorRadius; + for (int dimension = 0; dimension < 3; ++dimension) { + const double radialExtent = outerRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension)) + + mMinorRadius * std::abs(component(mAxisW, dimension)); + const double centerValue = component(mCenter, dimension); + if (dimension == 0) { + lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent); + upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent); + } else if (dimension == 1) { + lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent); + upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent); + } else { + lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent); + upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent); + } + } + } + + /// Cover boxes: the full torus in angular chunks, since the meridian projection ignores the trim; a spindle torus uses one box. + void appendCoverBoxes(std::vector& boxes) const override + { + if (mMajorRadius < mMinorRadius) { + BoundedSurface::appendCoverBoxes(boxes); + return; + } + const int ringChunks = coverChunkCount(kTwoPi); + const int tubeChunks = coverChunkCount(kTwoPi); + for (int ringChunk = 0; ringChunk < ringChunks; ++ringChunk) { + const double ringLow = kTwoPi * ringChunk / ringChunks; + const double ringHigh = kTwoPi * (ringChunk + 1) / ringChunks; + for (int tubeChunk = 0; tubeChunk < tubeChunks; ++tubeChunk) { + const double tubeLow = kTwoPi * tubeChunk / tubeChunks; + const double tubeHigh = kTwoPi * (tubeChunk + 1) / tubeChunks; + double lower[3]; + double upper[3]; + for (int dimension = 0; dimension < 3; ++dimension) { + double inPlaneLow = 0.; + double inPlaneHigh = 0.; + sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), ringLow, ringHigh, inPlaneLow, + inPlaneHigh); + // the coordinate is p(u) (R + r cos v) + w r sin v; with R + r cos v >= 0 it is + // monotone in p, so each extreme is a v sinusoid taken at p's own extreme + const double axisComponent = component(mAxisW, dimension); + const double high = sinusoidMaximum(inPlaneHigh, axisComponent, tubeLow, tubeHigh); + const double low = sinusoidMinimum(inPlaneLow, axisComponent, tubeLow, tubeHigh); + lower[dimension] = component(mCenter, dimension) + inPlaneLow * mMajorRadius + mMinorRadius * low; + upper[dimension] = component(mCenter, dimension) + inPlaneHigh * mMajorRadius + mMinorRadius * high; + } + boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}}); + } + } + } + + int ringSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * mPhiSweep / kTwoPi))); + } + + int tubeSegments() const + { + return std::max(1, static_cast(std::lround(kArcSamples * mTubeSweep / kTwoPi))); + } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + if (mHasWireTrim) { + appendCurveTrimMesh(mTrimOuter, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, vertices, triangles); + return; + } + const int ringSteps = ringSegments(); + const int tubeSteps = tubeSegments(); + const int firstVertexIndex = static_cast(vertices.size()); + for (int ringStep = 0; ringStep <= ringSteps; ++ringStep) { + const double phiRing = mPhiStart + mPhiSweep * ringStep / ringSteps; + for (int tubeStep = 0; tubeStep <= tubeSteps; ++tubeStep) { + vertices.push_back(pointAt(phiRing, mTubeStart + mTubeSweep * tubeStep / tubeSteps)); + } + } + const int rowLength = tubeSteps + 1; + for (int ringStep = 0; ringStep < ringSteps; ++ringStep) { + for (int tubeStep = 0; tubeStep < tubeSteps; ++tubeStep) { + const int base = firstVertexIndex + ringStep * rowLength + tubeStep; + triangles.push_back({base, base + rowLength, base + rowLength + 1}); + triangles.push_back({base, base + rowLength + 1, base + 1}); + } + } + } + + void appendDirectedEdges(std::vector>& edges) const override + { + if (mHasWireTrim) { + // the (phiRing, phiTube) -> 3D map is orientation-consistent with the outward normal, so + // the sign is just mNormalSign (as for the cylinder and cone) + appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, mNormalSign, edges); + return; + } + // boundary of the (phiRing, phiTube) rectangle traversed counter-clockwise as seen along the + // outward normal; a full sweep in either angle has no seam there, so it is skipped + auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) { + if (mNormalSign > 0.) { + edges.emplace_back(edgeStart, edgeEnd); + } else { + edges.emplace_back(edgeEnd, edgeStart); + } + }; + const int ringSteps = ringSegments(); + const int tubeSteps = tubeSegments(); + const double endRing = mPhiStart + mPhiSweep; + const double endTube = mTubeStart + mTubeSweep; + if (!fullTubeSweep()) { + for (int step = 0; step < ringSteps; ++step) { + const double phiRing = mPhiStart + mPhiSweep * step / ringSteps; + const double nextRing = mPhiStart + mPhiSweep * (step + 1) / ringSteps; + emitEdge(pointAt(phiRing, mTubeStart), pointAt(nextRing, mTubeStart)); // +phiRing at tubeStart + emitEdge(pointAt(nextRing, endTube), pointAt(phiRing, endTube)); // -phiRing at tubeEnd + } + } + if (!fullRingSweep()) { + for (int step = 0; step < tubeSteps; ++step) { + const double phiTube = mTubeStart + mTubeSweep * step / tubeSteps; + const double nextTube = mTubeStart + mTubeSweep * (step + 1) / tubeSteps; + emitEdge(pointAt(endRing, phiTube), pointAt(endRing, nextTube)); // +phiTube at ringEnd + emitEdge(pointAt(mPhiStart, nextTube), pointAt(mPhiStart, phiTube)); // -phiTube at ringStart + } + } + } + + bool sampleTrimCurve(size_t index, std::vector& samples) const override + { + if (!mHasWireTrim) { + return false; // a parametric-rectangle trim carries no per-edge curve to sample + } + return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, samples); + } + + private: + Vec3 mCenter; + Vec3 mAxisU; + Vec3 mAxisV; + Vec3 mAxisW; + double mMajorRadius = 0.; + double mMinorRadius = 0.; + double mPhiStart = 0.; + double mPhiSweep = kTwoPi; + double mTubeStart = 0.; + double mTubeSweep = kTwoPi; + double mRingTolerance = 0.; ///< angularTolerance of the major radius + double mTubeTolerance = 0.; ///< angularTolerance of the minor radius + double mNormalSign = 1.; + bool mHasWireTrim = false; + CurveWire mTrimOuter; + std::vector mTrimInner; +}; + +/// How one rim came out of the closure measurement. The four states are exhaustive, and they are +/// exactly the four the ClosureReport rim counters tally. +enum class RimState { + Matched = 0, ///< every chord has another face within its match band, traversed the other way + Reversed, ///< matched, but the partner traverses the shared curve the same way + Boundary, ///< some chord has no other face within its match band + NonManifold ///< some chord has two or more other faces within the declared tolerance +}; + +/// One trim loop of one face as measureRimClosure saw it, naming the rim and its worst chord. +struct RimRecord { + int surfaceIndex = -1; ///< the owning face's index in the solid's surface list + int rimIndexOnSurface = -1; ///< which trim loop of that face, in the order the face emits them + bool closed = false; ///< the polyline returns to its own first point + int chords = 0; + int unmatchedChords = 0; ///< of them, how many found no other face within their match band + double length = 0.; ///< summed chord length, cm + double unmatchedLength = 0.; ///< how much of it has no other face within the match band, cm + /// Largest distance from a chord midpoint of this rim to another face's chord, and where: how alone the loneliest chord is. + double maxIsolation = 0.; + Vec3 maxIsolationPoint{}; + int maxIsolationFace = -1; ///< the face owning the nearest chord there, or -1 if there was none + RimState state = RimState::Matched; +}; + +/// Whether a set of bounded surfaces forms a closed, consistently oriented 2-manifold, by half-edges, rims and edge identities. +struct ClosureReport { + bool closed = true; ///< every boundary edge is shared by exactly two faces + bool orientationConsistent = true; ///< shared edges are traversed in opposite directions + int boundaryEdges = 0; ///< edges present on only one face (e.g. a missing face) + int nonManifoldEdges = 0; ///< edges shared by more than two faces + int reversedEdges = 0; ///< edges shared by two faces in the same direction + double signedVolume = 0.; ///< divergence-theorem volume; positive if normals point out + + /// \name Rim-based measurement: the boundary as curves in cm, counted per rim; the verdict when there are no edge identities + /// @{ + /// Largest distance in cm from any rim chord to the nearest chord of another face; not a seam width. + double maxRimIsolation = 0.; + double totalRimLength = 0.; ///< summed length in cm of every face's trim boundary + double unmatchedRimLength = 0.; ///< how much of it has no other face within the match band, cm + double rimEpsilon = 0.; ///< the declared matching tolerance, in cm + double rimChordResolution = 0.; ///< the largest amount by which any rim polyline can sit off the + ///< smooth rim it samples, in cm; the per-chord value of this is + ///< what widens the match band + int rims = 0; ///< total number of trim loops over all faces + int matchedRims = 0; ///< every chord has another face within the match band, + ///< traversed the opposite way + int reversedRims = 0; ///< matched, but the partner traverses the shared curve the + ///< same way (one face's outward normal points inward) + int nonManifoldRims = 0; ///< some chord has two or more other faces within rimEpsilon + int boundaryRims = 0; ///< some chord has no other face within the match band + /// One entry per rim, in the order the faces were visited: the detail behind the counters above. + /// The counters say how many rims are open; these say which, and where. + std::vector rimRecords; + /// @} + + /// \name Closure by edge identity (sidecar v3): an edge is shared when it appears exactly twice, once each way + /// @{ + /// True when every surface carried a boundary edge list. + bool edgeIdentityAvailable = false; + int edgeIncidences = 0; ///< distinct edge identifiers seen over all faces + int edgeSharedCount = 0; ///< appearing exactly twice, opposite sense: a properly shared edge + int edgeBoundaryCount = 0; ///< appearing once: a face is missing on the other side + int edgeNonManifoldCount = 0; ///< appearing three or more times + int edgeReversedCount = 0; ///< appearing exactly twice, but with the same sense + int edgeDegenerateCount = 0; ///< flagged degenerate (cone apex, sphere pole): excluded from the + ///< counts above, because a point has no second face to meet + + /// Largest Hausdorff distance between two faces' realisations of a shared edge, in cm; a measurement, not a verdict. + double maxSharedEdgeDeviation = 0.; + uint32_t maxSharedEdgeDeviationEdge = 0; ///< which edge that was + Vec3 maxSharedEdgeDeviationPoint{}; ///< and where on it + int maxSharedEdgeDeviationFaces[2] = {-1, -1}; ///< between which two faces + int sharedEdgesMeasured = 0; ///< shared edges both of whose faces could be sampled + int sharedEdgesUnmeasured = 0; ///< the rest: a parametric-rectangle face names its edges but + ///< carries no curve for them, so there is nothing to compare + /// @} +}; + +/// Measure the Hausdorff distance between the two faces of each shared edge into \a report; it decides nothing. +inline void measureSharedEdgeDeviation(const std::vector>& surfaces, + ClosureReport& report) +{ + // edgeId -> the (surface, slot) pairs claiming it + std::map>> claims; + for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) { + if (surfaces[surfaceIndex] == nullptr) { + continue; + } + const auto& refs = surfaces[surfaceIndex]->boundaryEdges(); + for (size_t slot = 0; slot < refs.size(); ++slot) { + if (refs[slot].degenerate) { + continue; // a point has no partner and no length to disagree over + } + // unanchored claims are collected too, so that an edge whose other side is a + // parametric-rectangle face is counted as *unmeasured* rather than silently dropped + claims[refs[slot].edgeId].emplace_back(static_cast(surfaceIndex), slot); + } + } + + std::vector first; + std::vector second; + for (const auto& [edgeId, holders] : claims) { + if (holders.size() != 2) { + continue; + } + const auto& [firstSurface, firstSlot] = holders[0]; + const auto& [secondSurface, secondSlot] = holders[1]; + if (!surfaces[static_cast(firstSurface)]->sampleTrimCurve(firstSlot, first) || + !surfaces[static_cast(secondSurface)]->sampleTrimCurve(secondSlot, second) || first.size() < 2 || + second.size() < 2) { + ++report.sharedEdgesUnmeasured; + continue; + } + ++report.sharedEdgesMeasured; + auto worstAgainst = [](const std::vector& probes, const std::vector& polyline, Vec3& where) { + double worst = 0.; + for (const Vec3& probe : probes) { + double nearest = std::numeric_limits::infinity(); + for (size_t segment = 0; segment + 1 < polyline.size(); ++segment) { + nearest = std::min(nearest, pointSegmentDistanceSq(probe, polyline[segment], polyline[segment + 1])); + } + if (nearest > worst) { + worst = nearest; + where = probe; + } + } + return std::sqrt(worst); + }; + Vec3 forwardPoint{}; + Vec3 backwardPoint{}; + const double forwardWorst = worstAgainst(first, second, forwardPoint); + const double backwardWorst = worstAgainst(second, first, backwardPoint); + const double deviation = std::max(forwardWorst, backwardWorst); + if (deviation > report.maxSharedEdgeDeviation) { + report.maxSharedEdgeDeviation = deviation; + report.maxSharedEdgeDeviationEdge = edgeId; + report.maxSharedEdgeDeviationPoint = forwardWorst >= backwardWorst ? forwardPoint : backwardPoint; + report.maxSharedEdgeDeviationFaces[0] = firstSurface; + report.maxSharedEdgeDeviationFaces[1] = secondSurface; + } + } +} + +/// Measure the face-to-face gaps of \a surfaces as curves into \a report, probing chord midpoints against other faces' chords. +inline void measureRimClosure(const std::vector>& surfaces, double epsilon, + ClosureReport& report) +{ + report.rimEpsilon = epsilon; + + std::vector rims; + std::vector rimIndexOnSurface; + for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) { + if (surfaces[surfaceIndex] == nullptr) { + continue; + } + const size_t firstNewRim = rims.size(); + surfaces[surfaceIndex]->appendRims(rims); + for (size_t rimIndex = firstNewRim; rimIndex < rims.size(); ++rimIndex) { + rims[rimIndex].surfaceIndex = static_cast(surfaceIndex); + rimIndexOnSurface.push_back(static_cast(rimIndex - firstNewRim)); + } + } + report.rims = static_cast(rims.size()); + if (rims.empty()) { + return; + } + + // Flatten to chords with each chord's sagitta: two polylines of one curve differ by it, so it widens the match band. + // The sagitta is estimated per chord from the turn angle at smooth vertices; a corner has none. + constexpr double kMaxSmoothTurn = 0.52; // ~30 degrees; a rim sampled at kArcSamples turns by 15 + struct Chord { + Vec3 start; + Vec3 end; + int surfaceIndex; + double resolution; ///< how far this chord can sit from the smooth rim it samples, in cm + }; + std::vector chords; + std::vector> chordRange(rims.size()); // [first, last) chord of each rim + for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) { + const SurfaceRim& rim = rims[rimIndex]; + chordRange[rimIndex].first = chords.size(); + const size_t pointCount = rim.points.size(); + std::vector vertexSagitta(pointCount, 0.); + const size_t interiorCount = rim.closed ? pointCount : (pointCount >= 2 ? pointCount - 2 : 0); + for (size_t offset = 0; offset < interiorCount; ++offset) { + const size_t middle = rim.closed ? offset : offset + 1; + const Vec3 incoming = rim.points[middle] - rim.points[(middle + pointCount - 1) % pointCount]; + const Vec3 outgoing = rim.points[(middle + 1) % pointCount] - rim.points[middle]; + const double incomingLength = norm(incoming); + const double outgoingLength = norm(outgoing); + if (incomingLength <= kTolerance || outgoingLength <= kTolerance) { + continue; + } + const double turn = std::acos(std::clamp(dot(incoming, outgoing) / (incomingLength * outgoingLength), -1., 1.)); + if (turn > kMaxSmoothTurn) { + continue; // a corner of the trim, not a sample of a smooth run + } + vertexSagitta[middle] = 0.25 * (incomingLength + outgoingLength) * std::tan(0.25 * turn); + report.rimChordResolution = std::max(report.rimChordResolution, vertexSagitta[middle]); + } + const size_t chordCount = rim.closed ? pointCount : pointCount - 1; + for (size_t pointIndex = 0; pointIndex < chordCount; ++pointIndex) { + const size_t nextIndex = (pointIndex + 1) % pointCount; + chords.push_back({rim.points[pointIndex], rim.points[nextIndex], rim.surfaceIndex, + std::max(vertexSagitta[pointIndex], vertexSagitta[nextIndex])}); + } + chordRange[rimIndex].second = chords.size(); + } + if (chords.empty()) { + return; + } + + Vec3 lower{chords.front().start}; + Vec3 upper{chords.front().start}; + auto grow = [&](const Vec3& point) { + lower = {std::min(lower.xCoord, point.xCoord), std::min(lower.yCoord, point.yCoord), + std::min(lower.zCoord, point.zCoord)}; + upper = {std::max(upper.xCoord, point.xCoord), std::max(upper.yCoord, point.yCoord), + std::max(upper.zCoord, point.zCoord)}; + }; + for (const Chord& chord : chords) { + grow(chord.start); + grow(chord.end); + } + const int gridDimension = + std::clamp(static_cast(std::cbrt(static_cast(chords.size()))), 1, 32); + const Vec3 extent = upper - lower; + const double cellSize = + std::max({extent.xCoord, extent.yCoord, extent.zCoord, kTolerance}) / gridDimension; + auto cellOf = [&](double coordinate, double origin) { + return std::clamp(static_cast((coordinate - origin) / cellSize), 0, gridDimension - 1); + }; + auto cellIndex = [&](int xCell, int yCell, int zCell) { + return (xCell * gridDimension + yCell) * gridDimension + zCell; + }; + std::vector> cells(static_cast(gridDimension) * gridDimension * gridDimension); + for (size_t chordIndex = 0; chordIndex < chords.size(); ++chordIndex) { + const Chord& chord = chords[chordIndex]; + const int xLow = cellOf(std::min(chord.start.xCoord, chord.end.xCoord), lower.xCoord); + const int xHigh = cellOf(std::max(chord.start.xCoord, chord.end.xCoord), lower.xCoord); + const int yLow = cellOf(std::min(chord.start.yCoord, chord.end.yCoord), lower.yCoord); + const int yHigh = cellOf(std::max(chord.start.yCoord, chord.end.yCoord), lower.yCoord); + const int zLow = cellOf(std::min(chord.start.zCoord, chord.end.zCoord), lower.zCoord); + const int zHigh = cellOf(std::max(chord.start.zCoord, chord.end.zCoord), lower.zCoord); + for (int xCell = xLow; xCell <= xHigh; ++xCell) { + for (int yCell = yLow; yCell <= yHigh; ++yCell) { + for (int zCell = zLow; zCell <= zHigh; ++zCell) { + cells[cellIndex(xCell, yCell, zCell)].push_back(static_cast(chordIndex)); + } + } + } + } + + struct Match { + double distance = std::numeric_limits::infinity(); + int chordIndex = -1; + /// Another face's chord lies within this chord's match band. + bool withinBand = false; + /// The distinct faces found within the declared tolerance alone. Room for three is enough: + /// only none, one and "more than one" are distinguished, and only the last is used. + std::array coincidentFaces{-1, -1, -1}; + int coincidentFaceCount = 0; + }; + // Two bands: shared-edge matching uses the sampling-aware band, non-manifold detection the declared tolerance alone. + const double maxBand = epsilon + 2. * report.rimChordResolution; + auto nearestOtherFace = [&](const Vec3& probe, int ownSurfaceIndex, double probeResolution) { + Match match; + auto consider = [&](int chordIndex) { + const Chord& chord = chords[static_cast(chordIndex)]; + if (chord.surfaceIndex == ownSurfaceIndex) { + return; + } + const double distance = std::sqrt(pointSegmentDistanceSq(probe, chord.start, chord.end)); + if (distance < match.distance) { + match.distance = distance; + match.chordIndex = chordIndex; + } + if (distance <= epsilon + probeResolution + chord.resolution) { + match.withinBand = true; + } + if (distance <= epsilon && match.coincidentFaceCount < static_cast(match.coincidentFaces.size())) { + for (int seen = 0; seen < match.coincidentFaceCount; ++seen) { + if (match.coincidentFaces[static_cast(seen)] == chord.surfaceIndex) { + return; + } + } + match.coincidentFaces[static_cast(match.coincidentFaceCount++)] = chord.surfaceIndex; + } + }; + const int xCentre = cellOf(probe.xCoord, lower.xCoord); + const int yCentre = cellOf(probe.yCoord, lower.yCoord); + const int zCentre = cellOf(probe.zCoord, lower.zCoord); + for (int shell = 0; shell < gridDimension; ++shell) { + // stop once the nearest hit is closer than this shell's inner distance and the shells reach the match band + const double shellReach = (shell - 1) * cellSize; + if (shell > 0 && shellReach > std::max(match.distance, maxBand)) { + break; + } + for (int xCell = xCentre - shell; xCell <= xCentre + shell; ++xCell) { + if (xCell < 0 || xCell >= gridDimension) { + continue; + } + for (int yCell = yCentre - shell; yCell <= yCentre + shell; ++yCell) { + if (yCell < 0 || yCell >= gridDimension) { + continue; + } + for (int zCell = zCentre - shell; zCell <= zCentre + shell; ++zCell) { + if (zCell < 0 || zCell >= gridDimension) { + continue; + } + const bool onShell = std::abs(xCell - xCentre) == shell || std::abs(yCell - yCentre) == shell || + std::abs(zCell - zCentre) == shell; + if (!onShell) { + continue; // interior of the shell: visited on an earlier pass + } + for (const int chordIndex : cells[cellIndex(xCell, yCell, zCell)]) { + consider(chordIndex); + } + } + } + } + } + return match; + }; + + report.rimRecords.reserve(rims.size()); + for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) { + bool hasUnmatched = false; + bool hasNonManifold = false; + int sameDirectionVotes = 0; + int oppositeDirectionVotes = 0; + RimRecord record; + record.surfaceIndex = rims[rimIndex].surfaceIndex; + record.rimIndexOnSurface = rimIndexOnSurface[rimIndex]; + record.closed = rims[rimIndex].closed; + record.chords = static_cast(chordRange[rimIndex].second - chordRange[rimIndex].first); + for (size_t chordIndex = chordRange[rimIndex].first; chordIndex < chordRange[rimIndex].second; ++chordIndex) { + const Chord& chord = chords[chordIndex]; + const Vec3 along = chord.end - chord.start; + const double chordLength = norm(along); + report.totalRimLength += chordLength; + record.length += chordLength; + const Vec3 probe = chord.start + along * 0.5; + const Match match = nearestOtherFace(probe, chord.surfaceIndex, chord.resolution); + if (std::isfinite(match.distance)) { + report.maxRimIsolation = std::max(report.maxRimIsolation, match.distance); + if (match.distance > record.maxIsolation || record.maxIsolationFace < 0) { + record.maxIsolation = match.distance; + record.maxIsolationPoint = probe; + record.maxIsolationFace = chords[static_cast(match.chordIndex)].surfaceIndex; + } + } + if (match.coincidentFaceCount > 1) { + hasNonManifold = true; + } + if (!match.withinBand) { + hasUnmatched = true; + ++record.unmatchedChords; + report.unmatchedRimLength += chordLength; + record.unmatchedLength += chordLength; + continue; + } + const Chord& partner = chords[static_cast(match.chordIndex)]; + if (dot(along, partner.end - partner.start) < 0.) { + ++oppositeDirectionVotes; + } else { + ++sameDirectionVotes; + } + } + if (hasNonManifold) { + ++report.nonManifoldRims; + record.state = RimState::NonManifold; + } else if (hasUnmatched) { + ++report.boundaryRims; + record.state = RimState::Boundary; + } else if (sameDirectionVotes > oppositeDirectionVotes) { + ++report.reversedRims; + record.state = RimState::Reversed; + } else { + ++report.matchedRims; + record.state = RimState::Matched; + } + report.rimRecords.push_back(record); + } +} + +/// Decide closure by counting edge identities when every surface states them: twice opposite is shared, once is open, +/// three or more is non-manifold, twice same-sense is reversed; degenerate edges are excluded. +inline void applyEdgeIdentityClosure(const std::vector>& surfaces, + ClosureReport& report) +{ + size_t surfacesPresent = 0; + size_t surfacesStatingEdges = 0; + for (const auto& surface : surfaces) { + if (surface == nullptr) { + continue; + } + ++surfacesPresent; + if (!surface->boundaryEdges().empty()) { + ++surfacesStatingEdges; + } + } + if (surfacesPresent == 0 || surfacesStatingEdges != surfacesPresent) { + return; // no edge identity, or only some of it: leave the geometric verdict alone + } + report.edgeIdentityAvailable = true; + + struct Incidence { + int forward = 0; + int reversed = 0; + int degenerate = 0; + }; + std::map incidences; + // which faces own each edge, so a defect can be attributed back to a rim + std::map> owners; + for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) { + if (surfaces[surfaceIndex] == nullptr) { + continue; + } + for (const auto& ref : surfaces[surfaceIndex]->boundaryEdges()) { + Incidence& incidence = incidences[ref.edgeId]; + if (ref.degenerate) { + ++incidence.degenerate; + } else if (ref.reversed) { + ++incidence.reversed; + } else { + ++incidence.forward; + } + owners[ref.edgeId].push_back(static_cast(surfaceIndex)); + } + } + + // per face, the worst identity defect any of its edges carries + std::vector faceState(surfaces.size(), RimState::Matched); + auto worsen = [](RimState& state, RimState candidate) { + // the enum is not ordered by severity, so spell the precedence out + auto rank = [](RimState value) { + switch (value) { + case RimState::Matched: + return 0; + case RimState::Reversed: + return 1; + case RimState::Boundary: + return 2; + case RimState::NonManifold: + return 3; + } + return 0; + }; + if (rank(candidate) > rank(state)) { + state = candidate; + } + }; + + for (const auto& [edgeId, incidence] : incidences) { + ++report.edgeIncidences; + if (incidence.degenerate > 0 && incidence.forward + incidence.reversed == 0) { + ++report.edgeDegenerateCount; + continue; + } + const int total = incidence.forward + incidence.reversed; + RimState state = RimState::Matched; + if (total == 1) { + ++report.edgeBoundaryCount; + state = RimState::Boundary; + } else if (total == 2) { + if (incidence.forward == 1 && incidence.reversed == 1) { + ++report.edgeSharedCount; + } else { + ++report.edgeReversedCount; + state = RimState::Reversed; + } + } else { + ++report.edgeNonManifoldCount; + state = RimState::NonManifold; + } + if (state != RimState::Matched) { + for (const int owner : owners[edgeId]) { + worsen(faceState[static_cast(owner)], state); + } + } + } + + report.closed = (report.edgeBoundaryCount == 0) && (report.edgeNonManifoldCount == 0); + report.orientationConsistent = (report.edgeReversedCount == 0); + + report.matchedRims = 0; + report.boundaryRims = 0; + report.nonManifoldRims = 0; + report.reversedRims = 0; + for (RimRecord& record : report.rimRecords) { + const RimState state = record.surfaceIndex >= 0 && record.surfaceIndex < static_cast(faceState.size()) + ? faceState[static_cast(record.surfaceIndex)] + : RimState::Matched; + record.state = state; + switch (state) { + case RimState::NonManifold: + ++report.nonManifoldRims; + break; + case RimState::Boundary: + ++report.boundaryRims; + break; + case RimState::Reversed: + ++report.reversedRims; + break; + case RimState::Matched: + ++report.matchedRims; + break; + } + } + + measureSharedEdgeDeviation(surfaces, report); +} + +/// Validate closure and orientation of \a surfaces by half-edges, measure the rims, and count edge identities when present. +inline ClosureReport validateClosure(const std::vector>& surfaces, + double modelTolerance = 0.) +{ + ClosureReport report; + + auto quantize = [](double value) { return static_cast(std::llround(value / kClosureQuantum)); }; + using VertexKey = std::tuple; + auto keyOf = [&](const Vec3& point) { + return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)}; + }; + + std::vector> directedEdges; + for (const auto& surface : surfaces) { + if (surface != nullptr) { + surface->appendDirectedEdges(directedEdges); + report.signedVolume += surface->capacityContribution(); + } + } + + // For each undirected edge, count occurrences in the forward and reverse directions. + std::map, std::pair> edgeCounts; + for (const auto& directedEdge : directedEdges) { + const VertexKey startKey = keyOf(directedEdge.first); + const VertexKey endKey = keyOf(directedEdge.second); + if (startKey == endKey) { + continue; // degenerate edge, already flagged at wire level + } + const bool forward = startKey < endKey; + const auto orderedKey = forward ? std::make_pair(startKey, endKey) : std::make_pair(endKey, startKey); + auto& counts = edgeCounts[orderedKey]; + if (forward) { + ++counts.first; + } else { + ++counts.second; + } + } + + for (const auto& [edgeKey, counts] : edgeCounts) { + const int total = counts.first + counts.second; + if (total == 1) { + ++report.boundaryEdges; // missing neighbouring face + } else if (total == 2) { + if (counts.first != 1 || counts.second != 1) { + ++report.reversedEdges; // both faces traverse the edge the same way + } + } else { + ++report.nonManifoldEdges; + } + } + + measureRimClosure(surfaces, modelTolerance > 0. ? modelTolerance : kRimMatchTolerance, report); + + // the verdict is the rim measurement's; the chord counters only describe how faces differ + report.closed = (report.boundaryRims == 0) && (report.nonManifoldRims == 0); + report.orientationConsistent = (report.reversedRims == 0); + + // ... unless the surfaces state their edge identities, which then decide by counting + applyEdgeIdentityClosure(surfaces, report); + return report; +} + +} // namespace o2::cad::surface + +#endif diff --git a/Detectors/Base/src/CADGeometryUtils.cxx b/Detectors/CADSupport/src/CADGeometryUtils.cxx similarity index 78% rename from Detectors/Base/src/CADGeometryUtils.cxx rename to Detectors/CADSupport/src/CADGeometryUtils.cxx index 84c1890d844b5..adb1ee2f08e30 100644 --- a/Detectors/Base/src/CADGeometryUtils.cxx +++ b/Detectors/CADSupport/src/CADGeometryUtils.cxx @@ -8,8 +8,10 @@ // In applying this license CERN does not waive the privileges and immunities // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 -#include "DetectorsBase/CADGeometryUtils.h" +#include "CADSupport/CADGeometryUtils.h" #include "DetectorsBase/MaterialManager.h" #include #include @@ -29,8 +31,9 @@ #include #include #include +#include -namespace o2::base +namespace o2::cad { TGeoVolume* buildCADVolumeFromMacro(const std::string& macroFile, const std::string& instanceTag) @@ -44,13 +47,7 @@ TGeoVolume* buildCADVolumeFromMacro(const std::string& macroFile, const std::str return nullptr; } - // We JIT the macro into a *unique* namespace per call. This is essential when several - // external geometries are present at the same time: every macro produced by - // O2_CADtoTGeo.py exports identically named symbols (build(), get_builder_hook_unchecked(), - // LoadFacets(), ...). Loading them all into the single global Cling scope would collide - // (the first definition wins and subsequent macros are silently ignored). By wrapping each - // macro body in its own namespace we keep the symbols separate. The preprocessor #include - // lines must stay at global scope, so we hoist them out of the namespace. + // JIT each macro into its own namespace, since every converter macro defines the same symbols; includes stay global. std::ifstream macroStream(expandedHookFileName, std::ios::in); if (!macroStream.is_open()) { LOG(error) << "Cannot open external geometry macro " << expandedHookFileName; @@ -106,7 +103,10 @@ void remapCADMedia(TGeoVolume* top, const char* modulename) { std::unordered_map medium_ptr_mapping; std::unordered_set volumes_already_treated; + // a material may back several media (the `_NF` twins), so materials are deduplicated apart from media + std::unordered_map material_index; int counter = 1; + int matcounter = 1; // The transformer function auto transform_media = [&](TGeoVolume* vol_) { @@ -139,7 +139,30 @@ void remapCADMedia(TGeoVolume* top, const char* modulename) auto curr_mat = medium->GetMaterial(); auto& matmgr = o2::base::MaterialManager::Instance(); - matmgr.Material(modulename, counter, curr_mat->GetName(), curr_mat->GetA(), curr_mat->GetZ(), curr_mat->GetDensity(), curr_mat->GetRadLen(), curr_mat->GetIntLen()); + // Register the material once, however many media wear it. + const std::string matname(curr_mat->GetName()); + auto itmat = material_index.find(matname); + int imat; + if (itmat != material_index.end()) { + imat = itmat->second; + } else { + imat = matcounter++; + // A TGeoMixture goes through Mixture() so Geant keeps its element composition. + if (auto* mix = dynamic_cast(curr_mat)) { + const Int_t nel = mix->GetNelements(); + std::vector a(nel), z(nel), w(nel); + for (Int_t i = 0; i < nel; ++i) { + a[i] = mix->GetAmixt()[i]; + z[i] = mix->GetZmixt()[i]; + w[i] = mix->GetWmixt()[i]; + } + matmgr.Mixture(modulename, imat, curr_mat->GetName(), a.data(), z.data(), + curr_mat->GetDensity(), nel, w.data()); + } else { + matmgr.Material(modulename, imat, curr_mat->GetName(), curr_mat->GetA(), curr_mat->GetZ(), curr_mat->GetDensity(), curr_mat->GetRadLen(), curr_mat->GetIntLen()); + } + material_index[matname] = imat; + } // TGeo medium params are stored in a flat array with the following convention // fParams[0] = isvol; // fParams[1] = ifield; @@ -158,7 +181,7 @@ void remapCADMedia(TGeoVolume* top, const char* modulename) const auto epsil = medium->GetParam(6); const auto stmin = medium->GetParam(7); - matmgr.Medium(modulename, counter, medium->GetName(), counter, isvol, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, stmin); + matmgr.Medium(modulename, counter, medium->GetName(), imat, isvol, isxfld, sxmgmx, tmaxfd, stemax, deemax, epsil, stmin); // there will be new Material and Medium objects; fetch them auto new_med = matmgr.getTGeoMedium(modulename, counter); @@ -199,4 +222,4 @@ void remapCADMedia(TGeoVolume* top, const char* modulename) visit_volume(top); } -} // namespace o2::base +} // namespace o2::cad diff --git a/Detectors/CADSupport/src/CADSupportLinkDef.h b/Detectors/CADSupport/src/CADSupportLinkDef.h new file mode 100644 index 0000000000000..60985e2490f05 --- /dev/null +++ b/Detectors/CADSupport/src/CADSupportLinkDef.h @@ -0,0 +1,34 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-09 + +#ifdef __CLING__ + +#pragma link off all globals; +#pragma link off all classes; +#pragma link off all functions; + +#pragma link C++ class o2::cad::BVHSurfaceCurveRecord + ; +#pragma link C++ class o2::cad::BVHSurfaceRecord + ; +#pragma link C++ class std::vector < o2::cad::BVHSurfaceCurveRecord> + ; +#pragma link C++ class std::vector < o2::cad::BVHSurfaceRecord> + ; +#pragma link C++ class o2::cad::O2BVHSurfaceSolid - ; +#pragma link C++ class o2::cad::O2BVHAssembly + ; +#pragma link C++ class o2::cad::FlatCSGHalfspace + ; +#pragma link C++ class o2::cad::FlatCSGCell + ; +#pragma link C++ class std::vector < o2::cad::FlatCSGHalfspace> + ; +#pragma link C++ class std::vector < o2::cad::FlatCSGCell> + ; +#pragma link C++ class o2::cad::O2FlatCSG + ; +// Close every O2FlatCSG read from a file, so that any reader gets the accelerated shape. +#pragma read sourceClass = "o2::cad::O2FlatCSG" targetClass = "o2::cad::O2FlatCSG" version = "[1-]" source = "" target = "" code = "{ newObj->CloseShape(); if (!newObj->IsClosed()) { newObj->Error(\"Streamer\", \"Shape %s was read from a file and CloseShape() refused it, so it has no sub-cell boxes and every query falls back to its _Loop twin. See the Error above: a cell bounding box is missing, inverted or non-finite.\", newObj->GetName()); } }"; + +#endif diff --git a/Detectors/CADSupport/src/O2BVHAssembly.cxx b/Detectors/CADSupport/src/O2BVHAssembly.cxx new file mode 100644 index 0000000000000..fdca04c017b61 --- /dev/null +++ b/Detectors/CADSupport/src/O2BVHAssembly.cxx @@ -0,0 +1,489 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#include "CADSupport/O2BVHAssembly.h" + +#include "TGeoBBox.h" +#include "TGeoManager.h" +#include "TGeoMatrix.h" +#include "TGeoNode.h" +#include "TGeoVolume.h" + +// the same third-party BVH2 entry point O2Tessellated and O2BVHSurfaceSolid use +#include "bvh2_third_party.h" +#include "bvh2_extra_kernels.h" + +#include +#include +#include +#include + +using namespace o2::cad; +ClassImp(O2BVHAssembly); + +namespace +{ +// float BVH types, following the O2Tessellated::BuildBVH pattern +using BVHScalar = float; +using BVHBBox = bvh::v2::BBox; +using BVHVec3 = bvh::v2::Vec; +using BVHNode = bvh::v2::Node; +using BVH = bvh::v2::Bvh; +using BVHRay = bvh::v2::Ray; + +/// Widening of every daughter box before the outward float rounding; the value O2BVHSurfaceSolid uses. +constexpr double kBoxTolerance = 1.e-3; + +/// Round a double outward into float, away from the interval the box encloses. +inline float roundOutward(double value, bool up) +{ + return std::nextafterf(static_cast(value), + up ? std::numeric_limits::infinity() : -std::numeric_limits::infinity()); +} + +/// A float ray bound that is never *shorter* than the double distance it stands for. +inline float truncateRoundUp(double value) +{ + const float rounded = static_cast(value); + return rounded < value ? std::nextafterf(rounded, std::numeric_limits::infinity()) : rounded; +} + +/// Squared distance from \a point to a node box, in double and shrunk by a relative guard; scale it by kSafetyBoundShare. +inline double boxDistanceSq(const BVHBBox& box, const double* point) +{ + double distanceSq = 0.; + for (int dimension = 0; dimension < 3; ++dimension) { + const double lower = static_cast(box.min[dimension]); + const double upper = static_cast(box.max[dimension]); + const double coordinate = point[dimension]; + if (coordinate < lower) { + const double gap = lower - coordinate; + distanceSq += gap * gap; + } else if (coordinate > upper) { + const double gap = coordinate - upper; + distanceSq += gap * gap; + } + } + return distanceSq * (1. - 1.e-12); +} + +/// Share of a node's squared box distance that bounds a box daughter's axis-max Safety from below: max d_i >= |d| / sqrt(3). +/// A sharp daughter (a thin tube segment, an Arb8) can answer less, so the result stays a sound safety but may differ from Safety_Loop. +constexpr double kSafetyBoundShare = 1. / 3.; + +inline bool boxContains(const BVHBBox& box, const double* point) +{ + return point[0] >= static_cast(box.min[0]) && point[0] <= static_cast(box.max[0]) && + point[1] >= static_cast(box.min[1]) && point[1] <= static_cast(box.max[1]) && + point[2] >= static_cast(box.min[2]) && point[2] <= static_cast(box.max[2]); +} + +/// One entry of the nearest-daughter traversal stack. +struct SafetyEntry { + double distanceSq; + size_t node; +}; + +/// Capacity of the call-stack traversal stack; a local one, because assembly queries nest. +constexpr unsigned kSmallStackCapacity = 64; + +/// Run \a traverse with a fixed-size stack when a tree of \a treeDepth levels fits it, else a growing one. +template +auto withTraversalStack(int treeDepth, Traverse&& traverse) +{ + if (treeDepth + 2 <= static_cast(kSmallStackCapacity)) { + bvh::v2::SmallStack stack; + return traverse(stack); + } + bvh::v2::GrowingStack stack; + return traverse(stack); +} +} // namespace + +O2BVHAssembly::O2BVHAssembly() : TGeoShapeAssembly() {} + +O2BVHAssembly::O2BVHAssembly(TGeoVolumeAssembly* volume) : TGeoShapeAssembly(volume) +{ + if (volume != nullptr) { + BuildBVH(); + } +} + +O2BVHAssembly::~O2BVHAssembly() +{ + delete static_cast(fBVH); + fBVH = nullptr; +} + +size_t O2BVHAssembly::GetBVHMemory() const +{ + const auto* bvh = static_cast(fBVH); + if (bvh == nullptr) { + return 0; + } + return bvh->nodes.size() * sizeof(BVHNode) + bvh->prim_ids.size() * sizeof(size_t); +} + +//////////////////////////////////////////////////////////////////////////////// +/// BuildBVH -- one primitive per daughter: its box in the assembly frame, widened and rounded outward in float. + +void O2BVHAssembly::BuildBVH() +{ + delete static_cast(fBVH); + fBVH = nullptr; + fNbuilt = -1; + fTreeDepth = 0; + if (fVolume == nullptr) { + return; + } + ComputeBBox(); + const int nDaughters = fVolume->GetNdaughters(); + fNbuilt = nDaughters; + if (nDaughters == 0) { + return; + } + + std::vector boxes; + std::vector centers; + boxes.reserve(nDaughters); + centers.reserve(nDaughters); + + double corners[24]; + double master[3]; + for (int index = 0; index < nDaughters; ++index) { + TGeoNode* node = fVolume->GetNode(index); + TGeoShape* shape = node->GetVolume()->GetShape(); + // an assembly daughter, or one whose box was never computed, has to produce it first -- + // the same guard TGeoShapeAssembly::RecomputeBoxLast uses + if (node->GetVolume()->IsAssembly() || TGeoShape::IsSameWithinTolerance(((TGeoBBox*)shape)->GetDX(), 0.)) { + shape->ComputeBBox(); + } + ((TGeoBBox*)shape)->SetBoxPoints(corners); + double lower[3] = {TGeoShape::Big(), TGeoShape::Big(), TGeoShape::Big()}; + double upper[3] = {-TGeoShape::Big(), -TGeoShape::Big(), -TGeoShape::Big()}; + for (int corner = 0; corner < 8; ++corner) { + node->LocalToMaster(&corners[3 * corner], master); + for (int dimension = 0; dimension < 3; ++dimension) { + lower[dimension] = std::min(lower[dimension], master[dimension]); + upper[dimension] = std::max(upper[dimension], master[dimension]); + } + } + BVHBBox box; + for (int dimension = 0; dimension < 3; ++dimension) { + box.min[dimension] = roundOutward(lower[dimension] - kBoxTolerance, false); + box.max[dimension] = roundOutward(upper[dimension] + kBoxTolerance, true); + } + boxes.push_back(box); + centers.emplace_back(box.get_center()); + } + + typename bvh::v2::DefaultBuilder::Config config; + config.quality = bvh::v2::DefaultBuilder::Quality::High; + // One daughter per leaf: bvh2 enters a leaf without a box test, and a daughter query costs far more than one. + config.max_leaf_size = 1; + auto* built = new BVH(bvh::v2::DefaultBuilder::build(boxes, centers, config)); + fBVH = static_cast(built); + + // tree depth, which decides whether a traversal fits the fixed-size stack + std::vector> pending{{0, 1}}; + while (!pending.empty()) { + const auto [index, level] = pending.back(); + pending.pop_back(); + fTreeDepth = std::max(fTreeDepth, level); + const auto& node = built->nodes[index]; + if (!node.is_leaf()) { + const size_t firstChild = node.index.first_id(); + for (size_t child : {firstChild, firstChild + 1}) { + if (child < built->nodes.size()) { + pending.push_back({child, level + 1}); + } + } + } + } +} + +void O2BVHAssembly::EnsureBuilt() const +{ + const int nDaughters = fVolume != nullptr ? fVolume->GetNdaughters() : 0; + if (fNbuilt == nDaughters && (fBVH != nullptr || nDaughters == 0)) { + return; + } + const_cast(this)->BuildBVH(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Contains + +Bool_t O2BVHAssembly::Contains(const Double_t* point) const +{ + EnsureBuilt(); + if (!fBBoxOK) { + const_cast(this)->ComputeBBox(); + } + if (!TGeoBBox::Contains(point)) { + return kFALSE; + } + const auto* bvh = static_cast(fBVH); + if (bvh == nullptr) { + return kFALSE; + } + + int best = -1; + withTraversalStack(fTreeDepth, [&](auto& stack) { + double local[3]; + stack.push(0); // the bvh2 root node + while (!stack.is_empty()) { + const auto& node = bvh->nodes[stack.pop()]; + if (!boxContains(node.get_bbox(), point)) { + continue; + } + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const int daughter = static_cast(bvh->prim_ids[primitive]); + // the loop twin takes the lowest-indexed daughter that contains the point, so a candidate + // that cannot beat the standing answer need not be resolved at all + if (best >= 0 && daughter > best) { + continue; + } + TGeoNode* geoNode = fVolume->GetNode(daughter); + geoNode->MasterToLocal(point, local); + if (geoNode->GetVolume()->GetShape()->Contains(local)) { + best = daughter; + } + } + } else { + const auto firstChild = node.index.first_id(); + for (size_t child : {firstChild, firstChild + 1}) { + if (child < bvh->nodes.size()) { + stack.push(child); + } + } + } + } + }); + + if (best < 0) { + return kFALSE; + } + // this is how the daughter identity reaches TGeoNavigator, and through it the hit + fVolume->SetCurrentNodeIndex(best); + fVolume->SetNextNodeIndex(best); + return kTRUE; +} + +Bool_t O2BVHAssembly::Contains_Loop(const Double_t* point) const +{ + if (!fBBoxOK) { + const_cast(this)->ComputeBBox(); + } + if (!TGeoBBox::Contains(point)) { + return kFALSE; + } + double local[3]; + const int nDaughters = fVolume != nullptr ? fVolume->GetNdaughters() : 0; + for (int index = 0; index < nDaughters; ++index) { + TGeoNode* geoNode = fVolume->GetNode(index); + geoNode->MasterToLocal(point, local); + if (geoNode->GetVolume()->GetShape()->Contains(local)) { + fVolume->SetCurrentNodeIndex(index); + fVolume->SetNextNodeIndex(index); + return kTRUE; + } + } + return kFALSE; +} + +//////////////////////////////////////////////////////////////////////////////// +/// DistFromOutside -- daughters are queried with the fixed query bound, so the answer is visit-order independent. + +Double_t O2BVHAssembly::DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact, Double_t step, + Double_t* safe) const +{ + EnsureBuilt(); + if (!fBBoxOK) { + const_cast(this)->ComputeBBox(); + } + if (iact < 3 && safe != nullptr) { + *safe = Safety(point, kFALSE); + if (iact == 0) { + return TGeoShape::Big(); + } + if (iact == 1 && step <= *safe) { + return TGeoShape::Big(); + } + } + const auto* bvh = static_cast(fBVH); + if (bvh == nullptr) { + return TGeoShape::Big(); + } + + double best = TGeoShape::Big(); + int bestIndex = -1; + BVHRay ray(BVHVec3(static_cast(point[0]), static_cast(point[1]), static_cast(point[2])), + BVHVec3(static_cast(dir[0]), static_cast(dir[1]), static_cast(dir[2])), 0.f, + truncateRoundUp(step + kBoxTolerance)); + static constexpr bool useRobustTraversal = true; + auto* volume = fVolume; + withTraversalStack(fTreeDepth, [&](auto& stack) { + bvh->intersect( + ray, bvh->get_root().index, stack, [&](size_t beginPrimitive, size_t endPrimitive) { + double local[3]; + double localDir[3]; + for (size_t primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const int daughter = static_cast(bvh->prim_ids[primitive]); + TGeoNode* geoNode = volume->GetNode(daughter); + geoNode->MasterToLocal(point, local); + geoNode->MasterToLocalVect(dir, localDir); + const double distance = geoNode->GetVolume()->GetShape()->DistFromOutside(local, localDir, 3, step); + if (distance < best) { + best = distance; + bestIndex = daughter; + } else if (distance == best && daughter < bestIndex) { + bestIndex = daughter; + } + } + // A daughter whose box the ray only meets beyond best + kBoxTolerance cannot cross nearer, + // and cannot tie either: its true crossing is at least its box entry distance. + if (bestIndex >= 0) { + ray.tmax = std::min(ray.tmax, truncateRoundUp(best + kBoxTolerance)); + } + return false; // keep traversing + }); + }); + + if (bestIndex < 0 || best >= step) { + return TGeoShape::Big(); + } + volume->SetNextNodeIndex(bestIndex); + return best; +} + +Double_t O2BVHAssembly::DistFromOutside_Loop(const Double_t* point, const Double_t* dir, Double_t step) const +{ + if (!fBBoxOK) { + const_cast(this)->ComputeBBox(); + } + double best = TGeoShape::Big(); + int bestIndex = -1; + double local[3]; + double localDir[3]; + const int nDaughters = fVolume != nullptr ? fVolume->GetNdaughters() : 0; + for (int index = 0; index < nDaughters; ++index) { + TGeoNode* geoNode = fVolume->GetNode(index); + geoNode->MasterToLocal(point, local); + geoNode->MasterToLocalVect(dir, localDir); + const double distance = geoNode->GetVolume()->GetShape()->DistFromOutside(local, localDir, 3, step); + if (distance < best) { + best = distance; + bestIndex = index; + } + } + if (bestIndex < 0 || best >= step) { + return TGeoShape::Big(); + } + fVolume->SetNextNodeIndex(bestIndex); + return best; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Safety -- from inside as ROOT; from outside a nearest-daughter descent of the BVH. + +Double_t O2BVHAssembly::Safety(const Double_t* point, Bool_t in) const +{ + if (in) { + return TGeoShapeAssembly::Safety(point, in); + } + EnsureBuilt(); + if (!fBBoxOK) { + const_cast(this)->ComputeBBox(); + } + const auto* bvh = static_cast(fBVH); + if (bvh == nullptr) { + return TGeoShape::Big(); + } + + return withTraversalStack(fTreeDepth, [&](auto& stack) { + double best = TGeoShape::Big(); + stack.push({boxDistanceSq(bvh->nodes[0].get_bbox(), point), size_t(0)}); + while (!stack.is_empty()) { + const SafetyEntry entry = stack.pop(); + if (entry.distanceSq * kSafetyBoundShare >= best * best) { + continue; + } + const auto& node = bvh->nodes[entry.node]; + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const int daughter = static_cast(bvh->prim_ids[primitive]); + const double safety = fVolume->GetNode(daughter)->Safety(point, kFALSE); + if (safety <= 0.) { + return 0.; + } + best = std::min(best, safety); + } + } else { + const auto firstChild = node.index.first_id(); + const size_t children[2] = {firstChild, firstChild + 1}; + double distancesSq[2] = {TGeoShape::Big(), TGeoShape::Big()}; + for (int side = 0; side < 2; ++side) { + if (children[side] < bvh->nodes.size()) { + distancesSq[side] = boxDistanceSq(bvh->nodes[children[side]].get_bbox(), point); + } + } + // push the farther child first so the nearer one is popped, and prunes, first + const bool leftIsFarther = distancesSq[0] >= distancesSq[1]; + const int order[2] = {leftIsFarther ? 0 : 1, leftIsFarther ? 1 : 0}; + for (int side = 0; side < 2; ++side) { + const int which = order[side]; + if (children[which] < bvh->nodes.size() && distancesSq[which] * kSafetyBoundShare < best * best) { + stack.push({distancesSq[which], children[which]}); + } + } + } + } + return best; + }); +} + +Double_t O2BVHAssembly::Safety_Loop(const Double_t* point, Bool_t in) const +{ + if (in) { + return TGeoShapeAssembly::Safety(point, in); + } + double best = TGeoShape::Big(); + const int nDaughters = fVolume != nullptr ? fVolume->GetNdaughters() : 0; + for (int index = 0; index < nDaughters; ++index) { + const double safety = fVolume->GetNode(index)->Safety(point, kFALSE); + if (safety <= 0.) { + return 0.; + } + best = std::min(best, safety); + } + return best; +} + +//////////////////////////////////////////////////////////////////////////////// +/// MakeBVHAssembly + +O2BVHAssembly* O2BVHAssembly::MakeBVHAssembly(TGeoVolumeAssembly* volume) +{ + if (volume == nullptr) { + return nullptr; + } + auto* shape = new O2BVHAssembly(volume); + volume->SetShape(shape); + return shape; +} diff --git a/Detectors/CADSupport/src/O2BVHSurfaceSolid.cxx b/Detectors/CADSupport/src/O2BVHSurfaceSolid.cxx new file mode 100644 index 0000000000000..944e6ef5b019f --- /dev/null +++ b/Detectors/CADSupport/src/O2BVHSurfaceSolid.cxx @@ -0,0 +1,2241 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +#include "CADSupport/O2BVHSurfaceSolid.h" + +#include "BoundedSurface.h" + +// the third-party BVH headers plus extra kernels, shared with O2Tessellated +#include "bvh2_third_party.h" +#include "bvh2_extra_kernels.h" + +#include "TBuffer.h" +#include "TBuffer3D.h" +#include "TBuffer3DTypes.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace o2::cad; +using namespace o2::cad::surface; +ClassImp(O2BVHSurfaceSolid); + +namespace +{ +// float BVH types following the O2Tessellated::BuildBVH pattern +using BVHScalar = float; +using BVHBBox = bvh::v2::BBox; +using BVHVec3 = bvh::v2::Vec; +using BVHNode = bvh::v2::Node; +using BVH = bvh::v2::Bvh; +using BVHRay = bvh::v2::Ray; + +Vec2 makeVec2(const O2BVHSurfaceSolid::Point2D& point) +{ + return {point[0], point[1]}; +} + +Vec3 makeVec3(const O2BVHSurfaceSolid::Point3D& point) +{ + return {point[0], point[1], point[2]}; +} + +Vec3 makeVec3(const Double_t* point) +{ + return {point[0], point[1], point[2]}; +} + +// The arbitrary skew test direction used for parity-based containment: probes all normals and +// avoids evident symmetries (same as O2Tessellated), normalized so hit distances are lengths. +const Vec3 kContainsTestDirection = normalized({1., 1.41421356237, 1.73205080757}); + +/// The re-shoot vote's directions: five golden-angle spiral directions, well separated and off every axis and symmetry plane. +const std::array& reshootDirections() +{ + static const std::array directions = [] { + std::array spiral{}; + for (int index = 0; index < 5; ++index) { + const double cosTheta = 1. - 2. * (index + 0.5) / 5.; + const double sinTheta = std::sqrt(1. - cosTheta * cosTheta); + const double phi = 2.399963229728653 * index; // golden angle + spiral[index] = normalized({sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta}); + } + return spiral; + }(); + return directions; +} + +// Ray tmax tightening in the BVH distance queries; see O2BVHSurfaceSolid::SetRayTMaxPruning. +bool gRayTMaxPruning = true; +// Per-thread diagnostic counter of leaf surface patches visited by the BVH distance queries. +thread_local long long gRayCandidateCount = 0; +// ... and by the nearest-patch queries behind Safety and ComputeNormal; see +// O2BVHSurfaceSolid::ResetSafetyCandidateCounter. +thread_local long long gSafetyCandidateCount = 0; +// Deliberately unsound node bound for the nearest-patch traversal; see +// O2BVHSurfaceSolid::SetSafetyBoundUnsoundForTest. Never true outside a test. +bool gSafetyBoundUnsound = false; + +// Per-thread backing store of SurfaceVisitMarker, one stamp per surface index plus the epoch the +// live marker stamps with; see the class below. +thread_local std::vector gSurfaceVisitStamps; +thread_local unsigned long long gSurfaceVisitEpoch = 0; + +/// Per-query dedup of the surfaces a traversal hands on, epoch-stamped over a thread_local array. +/// Traversals never nest, so one stamp array per thread is enough. +class SurfaceVisitMarker +{ + public: + explicit SurfaceVisitMarker(size_t surfaceCount) : mStamps(gSurfaceVisitStamps), mEpoch(++gSurfaceVisitEpoch) + { + if (mStamps.size() < surfaceCount) { + mStamps.resize(surfaceCount, 0); + } + } + + /// True exactly once per surface index and marker lifetime. + bool firstVisit(size_t index) + { + if (mStamps[index] == mEpoch) { + return false; + } + mStamps[index] = mEpoch; + return true; + } + + private: + /// bound once per query rather than looked up per visit, which is the hot path + std::vector& mStamps; + unsigned long long mEpoch; +}; + +/// Squared distance from \a point to a node box, shrunk by (1 - 1e-12) so it never exceeds the distance to a patch inside. +/// \a unsoundBound is gSafetyBoundUnsound, read once per query by the caller. +inline double boxDistanceSq(const BVHBBox& box, const Vec3& point, bool unsoundBound) +{ + const double coordinates[3] = {point.xCoord, point.yCoord, point.zCoord}; + double distanceSq = 0.; + for (int dimension = 0; dimension < 3; ++dimension) { + const double lower = static_cast(box.min[dimension]); + const double upper = static_cast(box.max[dimension]); + const double value = coordinates[dimension]; + if (value < lower) { + distanceSq += (lower - value) * (lower - value); + } else if (value > upper) { + distanceSq += (value - upper) * (value - upper); + } + } + if (unsoundBound) { + // The negative control: the distance to the box centre bounds nothing. + double centreDistanceSq = 0.; + for (int dimension = 0; dimension < 3; ++dimension) { + const double centre = + 0.5 * (static_cast(box.min[dimension]) + static_cast(box.max[dimension])); + const double gap = coordinates[dimension] - centre; + centreDistanceSq += gap * gap; + } + return centreDistanceSq; + } + return distanceSq * (1. - 1.e-12); +} + +/// Convert a double ray bound to float, rounding up so the float bound is never below the double one. +inline BVHScalar truncateRoundUp(double bound) +{ + const double clamped = std::min(bound, static_cast(std::numeric_limits::max())); + const double biased = clamped + std::numeric_limits::epsilon() * std::abs(clamped); + return static_cast(biased); +} + +/// Lower ray parameter of the distance queries: just behind the origin, so a point on a face sees its t = 0 crossing. +constexpr double kDistanceRayTolerance = -kRayTolerance; + +/// Which side of the surface a hit is on for a ray along \a rayDirection; Tangential within kTolerance of tangency. +enum class CrossingSense { Entering, + Exiting, + Tangential }; + +/// Twice the half-width of the window sameIntersection() treats as one intersection at \a distance. +inline double clusterMargin(double distance) +{ + return 2. * kIntersectionTolerance * std::max(1., std::abs(distance)); +} + +inline CrossingSense crossingSense(const RayHit& hit, const Vec3& rayDirection) +{ + const double alignment = dot(hit.normal, rayDirection); + if (alignment < -kTolerance) { + return CrossingSense::Entering; + } + if (alignment > kTolerance) { + return CrossingSense::Exiting; + } + return CrossingSense::Tangential; +} + +/// Sort \a hits and visit their clusters in increasing distance; a cluster with both senses is a graze and reports Tangential. +template +void forEachCrossingCluster(std::vector& hits, const Vec3& rayDirection, ClusterVisitor&& visitor) +{ + std::sort(hits.begin(), hits.end(), + [](const RayHit& firstHit, const RayHit& secondHit) { return firstHit.distance < secondHit.distance; }); + + size_t hitIndex = 0; + while (hitIndex < hits.size()) { + bool entering = false; + bool exiting = false; + size_t clusterEnd = hitIndex; + // Compared against the cluster's first member, not its predecessor: chaining would merge thin features at large t. + while (clusterEnd < hits.size() && + (clusterEnd == hitIndex || sameIntersection(hits[clusterEnd].distance, hits[hitIndex].distance))) { + switch (crossingSense(hits[clusterEnd], rayDirection)) { + case CrossingSense::Entering: + entering = true; + break; + case CrossingSense::Exiting: + exiting = true; + break; + case CrossingSense::Tangential: + break; + } + ++clusterEnd; + } + // both, or neither: nothing was crossed + const CrossingSense sense = entering == exiting ? CrossingSense::Tangential + : (entering ? CrossingSense::Entering : CrossingSense::Exiting); + if (!visitor(hitIndex, clusterEnd, sense)) { + return; + } + hitIndex = clusterEnd; + } +} + +/// Distance to the nearest genuine entering or exiting crossing in \a hits, or Big; \a grazedFirst reports a graze on the way. +template +double nearestCrossingInHits(std::vector& hits, const Vec3& rayDirection, bool& grazedFirst) +{ + constexpr CrossingSense wanted = wantEntering ? CrossingSense::Entering : CrossingSense::Exiting; + double distance = TGeoShape::Big(); + grazedFirst = false; + forEachCrossingCluster(hits, rayDirection, [&](size_t firstIndex, size_t, CrossingSense sense) { + if (sense == CrossingSense::Tangential) { + grazedFirst = true; + return true; + } + if (sense != wanted) { + return true; + } + // clusters come in increasing distance, so the first match is the answer; a crossing is never negative + distance = std::max(0., hits[firstIndex].distance); + return false; + }); + return distance; +} + +/// @name Persistent surface records: translation between the Add*Surface arguments and BVHSurfaceRecord +/// @{ + +void fillPoint3(double (&target)[3], const O2BVHSurfaceSolid::Point3D& source) +{ + target[0] = source[0]; + target[1] = source[1]; + target[2] = source[2]; +} + +O2BVHSurfaceSolid::Point3D makePoint3D(const double (&source)[3]) +{ + return {source[0], source[1], source[2]}; +} + +/// The frame-and-scalars part of a record, shared by all six surface families. +BVHSurfaceRecord makeRecord(int kind, const O2BVHSurfaceSolid::Point3D& origin, + const O2BVHSurfaceSolid::Point3D& axisA, const O2BVHSurfaceSolid::Point3D& axisB, + std::vector scalars, bool innerWall, bool trimmed) +{ + BVHSurfaceRecord record; + record.kind = kind; + fillPoint3(record.origin, origin); + fillPoint3(record.axisA, axisA); + fillPoint3(record.axisB, axisB); + record.scalars = std::move(scalars); + record.innerWall = innerWall; + record.trimmed = trimmed; + return record; +} + +BVHSurfaceCurveRecord makeCurveRecord(const O2BVHSurfaceSolid::PlanarBoundaryCurve& curve) +{ + BVHSurfaceCurveRecord record; + record.kind = static_cast(curve.kind); + record.lineStart[0] = curve.lineStart[0]; + record.lineStart[1] = curve.lineStart[1]; + record.lineEnd[0] = curve.lineEnd[0]; + record.lineEnd[1] = curve.lineEnd[1]; + record.center[0] = curve.center[0]; + record.center[1] = curve.center[1]; + record.radius = curve.radius; + record.startAngle = curve.startAngle; + record.endAngle = curve.endAngle; + record.degree = curve.degree; + record.poles.reserve(2 * curve.poles.size()); + for (const auto& pole : curve.poles) { + record.poles.push_back(pole[0]); + record.poles.push_back(pole[1]); + } + record.weights = curve.weights; + record.knots = curve.knots; + return record; +} + +O2BVHSurfaceSolid::PlanarBoundaryCurve makeBoundaryCurve(const BVHSurfaceCurveRecord& record) +{ + O2BVHSurfaceSolid::PlanarBoundaryCurve curve; + curve.kind = static_cast(record.kind); + curve.lineStart = {record.lineStart[0], record.lineStart[1]}; + curve.lineEnd = {record.lineEnd[0], record.lineEnd[1]}; + curve.center = {record.center[0], record.center[1]}; + curve.radius = record.radius; + curve.startAngle = record.startAngle; + curve.endAngle = record.endAngle; + curve.degree = record.degree; + curve.poles.reserve(record.poles.size() / 2); + for (size_t index = 0; index + 1 < record.poles.size(); index += 2) { + curve.poles.push_back({record.poles[index], record.poles[index + 1]}); + } + curve.weights = record.weights; + curve.knots = record.knots; + return curve; +} + +/// Store an outer wire plus its holes as one flat curve list with per-wire sizes. +void storeCurveWires(BVHSurfaceRecord& record, const std::vector& outerWire, + const std::vector>& innerWires) +{ + record.wireSizes.push_back(static_cast(outerWire.size())); + for (const auto& curve : outerWire) { + record.curves.push_back(makeCurveRecord(curve)); + } + for (const auto& innerWire : innerWires) { + record.wireSizes.push_back(static_cast(innerWire.size())); + for (const auto& curve : innerWire) { + record.curves.push_back(makeCurveRecord(curve)); + } + } +} + +/// The inverse of storeCurveWires. Returns false when the per-wire sizes do not add up to the +/// stored curve count, i.e. when the record is truncated or corrupt. +bool loadCurveWires(const BVHSurfaceRecord& record, std::vector& outerWire, + std::vector>& innerWires) +{ + size_t consumed = 0; + for (size_t wireIndex = 0; wireIndex < record.wireSizes.size(); ++wireIndex) { + const int wireSize = record.wireSizes[wireIndex]; + if (wireSize < 0 || consumed + static_cast(wireSize) > record.curves.size()) { + return false; + } + auto& wire = wireIndex == 0 ? outerWire : innerWires.emplace_back(); + for (int curveIndex = 0; curveIndex < wireSize; ++curveIndex) { + wire.push_back(makeBoundaryCurve(record.curves[consumed + curveIndex])); + } + consumed += static_cast(wireSize); + } + return consumed == record.curves.size(); +} + +/// storeCurveWires/loadCurveWires for the polygon-vertex flavour of a planar surface. +void storePolygonWires(BVHSurfaceRecord& record, const std::vector& outerWire, + const std::vector>& innerWires) +{ + const auto append = [&record](const std::vector& wire) { + record.wireSizes.push_back(static_cast(wire.size())); + for (const auto& vertex : wire) { + record.polygonPoints.push_back(vertex[0]); + record.polygonPoints.push_back(vertex[1]); + } + }; + append(outerWire); + for (const auto& innerWire : innerWires) { + append(innerWire); + } +} + +bool loadPolygonWires(const BVHSurfaceRecord& record, std::vector& outerWire, + std::vector>& innerWires) +{ + size_t consumed = 0; + for (size_t wireIndex = 0; wireIndex < record.wireSizes.size(); ++wireIndex) { + const int wireSize = record.wireSizes[wireIndex]; + if (wireSize < 0 || 2 * (consumed + static_cast(wireSize)) > record.polygonPoints.size()) { + return false; + } + auto& wire = wireIndex == 0 ? outerWire : innerWires.emplace_back(); + for (int vertexIndex = 0; vertexIndex < wireSize; ++vertexIndex) { + const size_t offset = 2 * (consumed + vertexIndex); + wire.push_back({record.polygonPoints[offset], record.polygonPoints[offset + 1]}); + } + consumed += static_cast(wireSize); + } + return 2 * consumed == record.polygonPoints.size(); +} +/// @} + +// Ray parity of a full intersection list (sorts in place); a mixed-sense cluster is a graze and counts even. +bool oddCrossingParity(std::vector& hits, const Vec3& rayDirection) +{ + int crossings = 0; + forEachCrossingCluster(hits, rayDirection, [&](size_t, size_t, CrossingSense sense) { + if (sense != CrossingSense::Tangential) { + ++crossings; + } + return true; + }); + return (crossings & 1) != 0; +} + +/// A rim's state on the solid's scale; the solid reports the worst over its rims. +O2BVHSurfaceSolid::NavigationReliability rimStateToReliability(RimState state) +{ + using Reliability = O2BVHSurfaceSolid::NavigationReliability; + switch (state) { + case RimState::Matched: + return Reliability::Reliable; + case RimState::Reversed: + return Reliability::ReversedFaces; + case RimState::Boundary: + return Reliability::OpenSurfaceSet; + case RimState::NonManifold: + return Reliability::NonManifold; + } + return Reliability::Undetermined; +} +} // namespace + +struct O2BVHSurfaceSolid::Impl { + std::vector> surfaces; + std::vector displayVertices; + std::vector> displayTriangles; + /// The surface each display triangle came from, parallel to displayTriangles; see GetPointsOnSegments. + std::vector displayTriangleSurface; + ClosureReport closure; + /// closure.rimRecords in the public form, built once by CloseShape so the accessor can hand out + /// a reference. The two are the same data; only the state enum and the Vec3 differ in type. + std::vector rimReports; + bool defined = false; + std::unique_ptr bvh; //!< acceleration structure over the sub-patch cover boxes (built in CloseShape) + /// The surface of each BVH leaf primitive, in leaf order. + std::vector leafSurface; + /// GetNavigationReliability() is Reliable; set by CloseShape. + bool reliable = false; + /// A few on-patch display vertices, seeding the nearest-patch traversal's upper bound; see anchorSeedDistanceSq. + std::vector safetyAnchors; + + /// Build the BVH over the surfaces' cover boxes, widened by kBVHBoxTolerance and rounded outward to float. + void buildBVH() + { + bvh.reset(); + leafSurface.clear(); + if (surfaces.empty()) { + return; + } + + std::vector primitiveBoxes; + std::vector primitiveCenters; + std::vector coverBoxes; + std::vector coverSurface; + for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) { + coverBoxes.clear(); + surfaces[surfaceIndex]->appendCoverBoxes(coverBoxes); + for (const auto& coverBox : coverBoxes) { + BVHBBox primitiveBox; + for (int dimension = 0; dimension < 3; ++dimension) { + primitiveBox.min[dimension] = std::nextafterf( + static_cast(component(coverBox.first, dimension) - kBVHBoxTolerance), + -std::numeric_limits::infinity()); + primitiveBox.max[dimension] = std::nextafterf( + static_cast(component(coverBox.second, dimension) + kBVHBoxTolerance), + std::numeric_limits::infinity()); + } + primitiveBoxes.push_back(primitiveBox); + primitiveCenters.emplace_back(primitiveBox.get_center()); + coverSurface.push_back(static_cast(surfaceIndex)); + } + } + + typename bvh::v2::DefaultBuilder::Config config; + config.quality = bvh::v2::DefaultBuilder::Quality::High; + // One cover box per leaf: bvh2 enters a leaf without a box test, and a patch intersection costs far more than one. + config.max_leaf_size = 1; + bvh = std::make_unique(bvh::v2::DefaultBuilder::build(primitiveBoxes, primitiveCenters, config)); + leafSurface.resize(bvh->prim_ids.size()); + for (size_t leaf = 0; leaf < bvh->prim_ids.size(); ++leaf) { + leafSurface[leaf] = coverSurface[bvh->prim_ids[leaf]]; + } + } + + /// The surface a BVH leaf primitive belongs to. + size_t surfaceOfPrimitive(size_t primitive) const + { + return static_cast(leafSurface[primitive]); + } + + /// Subsample the display vertices, which lie on their patches, as safety anchors. + void collectSafetyAnchors() + { + constexpr size_t kAnchorCount = 24; + safetyAnchors.clear(); + if (displayVertices.empty()) { + return; + } + const size_t stride = std::max(1, displayVertices.size() / kAnchorCount); + for (size_t index = 0; index < displayVertices.size() && safetyAnchors.size() < kAnchorCount; index += stride) { + safetyAnchors.push_back(displayVertices[index]); + } + } + + /// The squared distance to the nearest safety anchor, inflated by a hair: an upper bound on the exact answer. + /// It prunes only nodes that cannot win, so the value and index stay the loop's; infinity without anchors. + double anchorSeedDistanceSq(const Vec3& point) const + { + double bestDistanceSq = std::numeric_limits::infinity(); + for (const auto& anchor : safetyAnchors) { + bestDistanceSq = std::min(bestDistanceSq, normSq(point - anchor)); + } + if (!std::isfinite(bestDistanceSq)) { + return bestDistanceSq; + } + // the relative term dominates the roundings, the absolute one the anchors' on-patch tolerance; both far below kBVHBoxTolerance + const double inflated = std::sqrt(bestDistanceSq) * (1. + 1.e-12) + 1.e-10; + return inflated * inflated; + } + + /// Visit every surface one of whose cover-box leaves is traversed by the (unbounded) ray, + /// each exactly once however many of its boxes the ray crosses. + template + void visitRayCandidates(const Vec3& rayOrigin, const Vec3& rayDirection, SurfaceVisitor&& visitor) const + { + BVHRay ray(BVHVec3(rayOrigin.xCoord, rayOrigin.yCoord, rayOrigin.zCoord), + BVHVec3(rayDirection.xCoord, rayDirection.yCoord, rayDirection.zCoord), 0.f, + std::numeric_limits::max()); + static constexpr bool useRobustTraversal = true; + static thread_local bvh::v2::GrowingStack stack; + stack.clear(); + SurfaceVisitMarker marker(surfaces.size()); + bvh->intersect(ray, bvh->get_root().index, stack, + [&](size_t beginPrimitive, size_t endPrimitive) { + for (size_t primitive = beginPrimitive; primitive < endPrimitive; + ++primitive) { + const size_t surfaceIndex = surfaceOfPrimitive(primitive); + if (marker.firstVisit(surfaceIndex)) { + visitor(*surfaces[surfaceIndex]); + } + } + return false; // keep traversing + }); + } + + /// Distance to the nearest entering (\a wantEntering) or exiting crossing within \a stepmax, else Big. + /// The ray bound shrinks to the best candidate, rounded up past kBVHBoxTolerance, so no nearer hit is cut. + template + double nearestCrossing(const Vec3& rayOrigin, const Vec3& rayDirection, double stepmax) const + { + static thread_local std::vector collectedHits; + constexpr CrossingSense wanted = wantEntering ? CrossingSense::Entering : CrossingSense::Exiting; + + // Hits are classified with their neighbours, since a graze crosses nothing. If pruning stopped at a candidate + // that turns out to be a graze, redo the query without pruning: both passes must return the same number. + long long candidates = 0; + for (int attempt = 0; attempt < 2; ++attempt) { + const bool pruning = gRayTMaxPruning && attempt == 0; + collectedHits.clear(); + + double bestCandidate = TGeoShape::Big(); + BVHRay ray(BVHVec3(rayOrigin.xCoord, rayOrigin.yCoord, rayOrigin.zCoord), + BVHVec3(rayDirection.xCoord, rayDirection.yCoord, rayDirection.zCoord), 0.f, + truncateRoundUp(stepmax)); + static constexpr bool useRobustTraversal = true; + + static thread_local bvh::v2::GrowingStack stack; + stack.clear(); + SurfaceVisitMarker marker(surfaces.size()); + // ray is captured by reference on purpose: bvh2 takes it as const Ray&, but the object + // itself is ours and mutable, and the traversal reads tmax afresh at every node test. + bvh->intersect( + ray, bvh->get_root().index, stack, [&](size_t beginPrimitive, size_t endPrimitive) { + for (size_t primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const size_t surfaceIndex = surfaceOfPrimitive(primitive); + if (!marker.firstVisit(surfaceIndex)) { + continue; + } + const BoundedSurface& surface = *surfaces[surfaceIndex]; + ++candidates; + // the per-surface bound keeps a margin past the candidate, so its cluster partners are never cut + const double bound = + pruning ? std::min(stepmax, bestCandidate + clusterMargin(bestCandidate)) : stepmax; + const size_t firstNewHit = collectedHits.size(); + surface.appendIntersections(rayOrigin, rayDirection, kDistanceRayTolerance, bound, collectedHits); + for (size_t hitIndex = firstNewHit; hitIndex < collectedHits.size(); ++hitIndex) { + const RayHit& hit = collectedHits[hitIndex]; + if (crossingSense(hit, rayDirection) == wanted && hit.distance < bestCandidate) { + bestCandidate = hit.distance; + } + } + } + if (pruning && bestCandidate < stepmax) { + ray.tmax = std::min(ray.tmax, truncateRoundUp(bestCandidate + kBVHBoxTolerance)); + } + return false; // keep traversing; the shrunk tmax does the pruning + }); + + bool grazedFirst = false; + const double distance = nearestCrossingInHits(collectedHits, rayDirection, grazedFirst); + if (!pruning || !grazedFirst) { + gRayCandidateCount += candidates; + return distance; + } + } + gRayCandidateCount += candidates; + return TGeoShape::Big(); // unreachable: the second attempt never prunes + } + + /// Same query without the BVH: visit every surface. Oracle and baseline for nearestCrossing. + template + double nearestCrossingLoop(const Vec3& rayOrigin, const Vec3& rayDirection, double stepmax) const + { + static thread_local std::vector collectedLoopHits; + + // No pruning at all here: this is the oracle the accelerated query is checked against, so it + // trades the shrinking upper bound for having every hit in hand and needing no retry. + collectedLoopHits.clear(); + for (const auto& surface : surfaces) { + surface->appendIntersections(rayOrigin, rayDirection, kDistanceRayTolerance, stepmax, collectedLoopHits); + } + bool grazedFirst = false; + return nearestCrossingInHits(collectedLoopHits, rayDirection, grazedFirst); + } + + /// Parity of the ray's crossings with the surface set, through the BVH or the loop; \a ambiguous reports a trim-band tie-break. + bool parityAlong(const Vec3& point, const Vec3& direction, bool useBVH, bool* ambiguous = nullptr) const + { + // reused across calls so containment allocates nothing on the hot path; the capacity is paid + // once per thread. Distinct from the distance queries' buffers, which are their own. + static thread_local std::vector parityHits; + parityHits.clear(); + if (useBVH) { + visitRayCandidates(point, direction, [&](const BoundedSurface& surface) { + surface.appendIntersections(point, direction, kRayTolerance, TGeoShape::Big(), parityHits); + }); + } else { + for (const auto& surface : surfaces) { + surface->appendIntersections(point, direction, kRayTolerance, TGeoShape::Big(), parityHits); + } + } + if (ambiguous != nullptr) { + *ambiguous = std::any_of(parityHits.begin(), parityHits.end(), + [](const RayHit& hit) { return hit.onTrimBoundary; }); + } + return oddCrossingParity(parityHits, direction); + } + + /// Containment by majority vote over reshootDirections() for a solid that is not a closed 2-manifold; stops at a majority. + /// \a allTiedOnBoundary reports that no direction's parity rested on the geometry alone. + bool containsByVote(const Vec3& point, bool useBVH, bool* allTiedOnBoundary = nullptr) const + { + constexpr int kMajority = 3; // of the five directions + int inside = 0; // shots whose parity rests on no trim-boundary tie-break + int outside = 0; + int insideOnBoundary = 0; // and shots that do, counted apart + int outsideOnBoundary = 0; + for (const auto& direction : reshootDirections()) { + bool ambiguous = false; + const bool answer = parityAlong(point, direction, useBVH, &ambiguous); + if (ambiguous) { + answer ? ++insideOnBoundary : ++outsideOnBoundary; + } else { + answer ? ++inside : ++outside; + } + if (inside >= kMajority || outside >= kMajority) { + break; + } + } + if (allTiedOnBoundary != nullptr) { + *allTiedOnBoundary = (inside == outside); + } + // Decide among the shots that rest on the geometry unless they tie; a genuine tie counts all five. + if (inside != outside) { + return inside > outside; + } + return (inside + insideOnBoundary) > (outside + outsideOnBoundary); + } + + /// Visit every surface whose widened leaf box holds the point, until the visitor returns true. + template + bool visitPointCandidates(const Vec3& point, SurfaceVisitor&& visitor) const + { + const BVHVec3 testPoint(point.xCoord, point.yCoord, point.zCoord); + SurfaceVisitMarker marker(surfaces.size()); + static thread_local std::vector nodeStack; + nodeStack.clear(); + nodeStack.push_back(0); // start from the root node + while (!nodeStack.empty()) { + const auto& node = bvh->nodes[nodeStack.back()]; + nodeStack.pop_back(); + if (!bvh::v2::extra::contains(node.get_bbox(), testPoint)) { + continue; + } + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const size_t surfaceIndex = surfaceOfPrimitive(primitive); + if (marker.firstVisit(surfaceIndex) && visitor(*surfaces[surfaceIndex])) { + return true; + } + } + } else { + const auto firstChild = node.index.first_id(); + for (size_t child : {firstChild, firstChild + 1}) { + if (child < bvh->nodes.size()) { + nodeStack.push_back(child); + } + } + } + } + return false; + } + + /// The brute-force nearest patch and its index; the lowest index wins an exact tie, which ComputeNormal relies on. + double nearestPatchDistanceSqLoop(const Vec3& point, size_t* closestIndex) const + { + double bestDistanceSq = std::numeric_limits::infinity(); + size_t bestIndex = surfaces.size(); + for (size_t index = 0; index < surfaces.size(); ++index) { + const double patchDistanceSq = surfaces[index]->distanceSqToPatch(point); + if (patchDistanceSq < bestDistanceSq) { + bestDistanceSq = patchDistanceSq; + bestIndex = index; + } + } + if (closestIndex != nullptr) { + *closestIndex = bestIndex; + } + return bestDistanceSq; + } + + /// Same answer as nearestPatchDistanceSqLoop through the BVH: an ordered descent with a running best. + /// Box and patch distances both err downward, so Safety can only be too small; \a TrackIndex keeps ties for ComputeNormal. + template + double nearestPatchDistanceSq(const Vec3& point, size_t* closestIndex) const + { + if (bvh == nullptr) { + return nearestPatchDistanceSqLoop(point, closestIndex); + } + + struct StackEntry { + size_t node; + double lowerBoundSq; + }; + // reused across calls so the hot path allocates nothing; capacity is paid once per thread + static thread_local std::vector nodeStack; + nodeStack.clear(); + + // Seed the running best with the anchor distance; it never displaces the true winner (see anchorSeedDistanceSq). + double bestDistanceSq = anchorSeedDistanceSq(point); + size_t bestIndex = surfaces.size(); + SurfaceVisitMarker marker(surfaces.size()); + const bool unsound = gSafetyBoundUnsound; + long long candidates = 0; + + auto pruned = [](double lowerBoundSq, double bestSoFarSq) { + return TrackIndex ? lowerBoundSq > bestSoFarSq : lowerBoundSq >= bestSoFarSq; + }; + + nodeStack.push_back({0, boxDistanceSq(bvh->nodes[0].get_bbox(), point, unsound)}); + while (!nodeStack.empty()) { + const StackEntry entry = nodeStack.back(); + nodeStack.pop_back(); + if (pruned(entry.lowerBoundSq, bestDistanceSq)) { + continue; + } + const auto& node = bvh->nodes[entry.node]; + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + const size_t surfaceIndex = surfaceOfPrimitive(primitive); + if (!marker.firstVisit(surfaceIndex)) { + continue; + } + ++candidates; + const double patchDistanceSq = surfaces[surfaceIndex]->distanceSqToPatch(point); + if (patchDistanceSq < bestDistanceSq) { + bestDistanceSq = patchDistanceSq; + bestIndex = surfaceIndex; + } else if (TrackIndex && patchDistanceSq == bestDistanceSq && surfaceIndex < bestIndex) { + bestIndex = surfaceIndex; + } + } + continue; + } + const size_t firstChild = node.index.first_id(); + const size_t secondChild = firstChild + 1; + if (secondChild >= bvh->nodes.size()) { + if (firstChild < bvh->nodes.size()) { + nodeStack.push_back({firstChild, boxDistanceSq(bvh->nodes[firstChild].get_bbox(), point, unsound)}); + } + continue; + } + double nearBound = boxDistanceSq(bvh->nodes[firstChild].get_bbox(), point, unsound); + double farBound = boxDistanceSq(bvh->nodes[secondChild].get_bbox(), point, unsound); + size_t nearChild = firstChild; + size_t farChild = secondChild; + if (farBound < nearBound) { + std::swap(nearBound, farBound); + std::swap(nearChild, farChild); + } + // farther child first: the stack is LIFO, so the nearer one is popped -- and tightens the + // best -- before the farther one is re-tested + if (!pruned(farBound, bestDistanceSq)) { + nodeStack.push_back({farChild, farBound}); + } + if (!pruned(nearBound, bestDistanceSq)) { + nodeStack.push_back({nearChild, nearBound}); + } + } + + gSafetyCandidateCount += candidates; + if (closestIndex != nullptr) { + *closestIndex = bestIndex; + } + return bestDistanceSq; + } + + /// True, after reporting it for \a method of \a owner, if the shape is defined and takes no more surfaces. + bool refuseIfDefined(const O2BVHSurfaceSolid& owner, const char* method) const + { + if (!defined) { + return false; + } + owner.Error(method, "Shape %s already fully defined. Not adding", owner.GetName()); + return true; + } + + /// Append a built surface, and to \a records the record that rebuilds it. + bool commit(std::unique_ptr surface, BVHSurfaceRecord record, + std::vector& records) + { + records.push_back(std::move(record)); + surfaces.emplace_back(std::move(surface)); + return true; + } +}; + +int BVHSurfaceRecord::expectedScalarCount(int recordKind) +{ + switch (recordKind) { + case PlanarPolygon: + case CurvedPlanar: + return 0; + case Cylindrical: // radius, heightMin, heightMax, phiStart, phiSweep + case Spherical: // radius, thetaMin, thetaMax, phiStart, phiSweep + return 5; + case Conical: // radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart, phiSweep + case Toroidal: // majorRadius, minorRadius, phiStart, phiSweep, tubeStart, tubeSweep + return 6; + default: + return -1; + } +} + +O2BVHSurfaceSolid::O2BVHSurfaceSolid() : TGeoBBox(), fImpl(new Impl) +{ +} + +O2BVHSurfaceSolid::O2BVHSurfaceSolid(const char* name) : TGeoBBox(name, 0., 0., 0.), fImpl(new Impl) +{ +} + +O2BVHSurfaceSolid::~O2BVHSurfaceSolid() +{ + delete fImpl; +} + +bool O2BVHSurfaceSolid::AddPlanarSurface(const Point3D& origin, const Point3D& axisU, const Point3D& axisV, + const std::vector& outerWire, + const std::vector>& innerWires) +{ + if (fImpl->refuseIfDefined(*this, "AddPlanarSurface")) { + return false; + } + + std::vector convertedOuterWire; + convertedOuterWire.reserve(outerWire.size()); + for (const auto& vertex : outerWire) { + convertedOuterWire.push_back(makeVec2(vertex)); + } + + std::vector> convertedInnerWires; + convertedInnerWires.reserve(innerWires.size()); + for (const auto& innerWire : innerWires) { + auto& convertedInnerWire = convertedInnerWires.emplace_back(); + convertedInnerWire.reserve(innerWire.size()); + for (const auto& vertex : innerWire) { + convertedInnerWire.push_back(makeVec2(vertex)); + } + } + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(origin), makeVec3(axisU), makeVec3(axisV), convertedOuterWire, convertedInnerWires, + errorMessage)) { + Error("AddPlanarSurface", "%s", errorMessage.c_str()); + return false; + } + if (surface->wasReoriented()) { + Warning("AddPlanarSurface", "Shape %s: planar surface %d had a wire re-oriented to match its role", GetName(), + static_cast(fImpl->surfaces.size())); + } + + auto record = makeRecord(BVHSurfaceRecord::PlanarPolygon, origin, axisU, axisV, {}, false, false); + storePolygonWires(record, outerWire, innerWires); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +namespace +{ +/// Translate a public PlanarBoundaryCurve wire into the internal Curve2D loop. +std::vector makeCurveWire(const std::vector& wire) +{ + std::vector curves; + curves.reserve(wire.size()); + for (const auto& c : wire) { + if (c.kind == O2BVHSurfaceSolid::PlanarBoundaryCurve::Arc) { + curves.push_back(Curve2D::makeArc({c.center[0], c.center[1]}, c.radius, c.startAngle, c.endAngle)); + } else if (c.kind == O2BVHSurfaceSolid::PlanarBoundaryCurve::BSpline) { + std::vector poles; + poles.reserve(c.poles.size()); + for (const auto& pole : c.poles) { + poles.push_back({pole[0], pole[1]}); + } + curves.push_back(Curve2D::makeBSpline(c.degree, std::move(poles), c.weights, c.knots)); + } else { + curves.push_back(Curve2D::makeLine({c.lineStart[0], c.lineStart[1]}, {c.lineEnd[0], c.lineEnd[1]})); + } + } + return curves; +} + +/// Translate public PlanarBoundaryCurve wires into internal Curve2D loops. +std::vector> makeCurveWires( + const std::vector>& wires) +{ + std::vector> loops; + loops.reserve(wires.size()); + for (const auto& wire : wires) { + loops.push_back(makeCurveWire(wire)); + } + return loops; +} +} // namespace + +bool O2BVHSurfaceSolid::AddCurvedPlanarSurface(const Point3D& origin, const Point3D& axisU, const Point3D& axisV, + const std::vector& outerWire, + const std::vector>& innerWires) +{ + if (fImpl->refuseIfDefined(*this, "AddCurvedPlanarSurface")) { + return false; + } + + const std::vector outerCurves = makeCurveWire(outerWire); + const std::vector> innerCurves = makeCurveWires(innerWires); + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(origin), makeVec3(axisU), makeVec3(axisV), outerCurves, innerCurves, + errorMessage, wireJoinToleranceFor(fModelTolerance))) { + Error("AddCurvedPlanarSurface", "%s", errorMessage.c_str()); + return false; + } + + auto record = makeRecord(BVHSurfaceRecord::CurvedPlanar, origin, axisU, axisV, {}, false, false); + storeCurveWires(record, outerWire, innerWires); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +bool O2BVHSurfaceSolid::AddCylindricalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double radius, double heightMin, + double heightMax, double phiStart, double phiSweep, bool innerWall) +{ + if (fImpl->refuseIfDefined(*this, "AddCylindricalSurface")) { + return false; + } + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), radius, heightMin, + heightMax, phiStart, phiSweep, innerWall, errorMessage)) { + Error("AddCylindricalSurface", "%s", errorMessage.c_str()); + return false; + } + + return fImpl->commit(std::move(surface), + makeRecord(BVHSurfaceRecord::Cylindrical, centerPoint, axis, referenceAxisU, + {radius, heightMin, heightMax, phiStart, phiSweep}, innerWall, false), + fRecords); +} + +bool O2BVHSurfaceSolid::AddCylindricalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double radius, double heightMin, + double heightMax, double phiStart, double phiSweep, bool innerWall, + const std::vector& outerTrim, + const std::vector>& innerTrims) +{ + if (fImpl->refuseIfDefined(*this, "AddCylindricalSurface")) { + return false; + } + + const std::vector outerCurves = makeCurveWire(outerTrim); + const std::vector> innerCurves = makeCurveWires(innerTrims); + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), radius, heightMin, + heightMax, phiStart, phiSweep, innerWall, outerCurves, innerCurves, errorMessage, + wireJoinToleranceFor(fModelTolerance))) { + Error("AddCylindricalSurface", "%s", errorMessage.c_str()); + return false; + } + + auto record = makeRecord(BVHSurfaceRecord::Cylindrical, centerPoint, axis, referenceAxisU, + {radius, heightMin, heightMax, phiStart, phiSweep}, innerWall, true); + storeCurveWires(record, outerTrim, innerTrims); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +bool O2BVHSurfaceSolid::AddSphericalSurface(const Point3D& center, const Point3D& polarAxis, + const Point3D& referenceAxisU, double radius, double thetaMin, + double thetaMax, double phiStart, double phiSweep, bool innerWall) +{ + if (fImpl->refuseIfDefined(*this, "AddSphericalSurface")) { + return false; + } + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(center), makeVec3(polarAxis), makeVec3(referenceAxisU), radius, thetaMin, + thetaMax, phiStart, phiSweep, innerWall, errorMessage)) { + Error("AddSphericalSurface", "%s", errorMessage.c_str()); + return false; + } + + return fImpl->commit(std::move(surface), + makeRecord(BVHSurfaceRecord::Spherical, center, polarAxis, referenceAxisU, + {radius, thetaMin, thetaMax, phiStart, phiSweep}, innerWall, false), + fRecords); +} + +bool O2BVHSurfaceSolid::AddSphericalSurface(const Point3D& center, const Point3D& polarAxis, + const Point3D& referenceAxisU, double radius, double thetaMin, + double thetaMax, double phiStart, double phiSweep, bool innerWall, + const std::vector& outerTrim, + const std::vector>& innerTrims) +{ + if (fImpl->refuseIfDefined(*this, "AddSphericalSurface")) { + return false; + } + + const std::vector outerCurves = makeCurveWire(outerTrim); + const std::vector> innerCurves = makeCurveWires(innerTrims); + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(center), makeVec3(polarAxis), makeVec3(referenceAxisU), radius, thetaMin, + thetaMax, phiStart, phiSweep, innerWall, outerCurves, innerCurves, errorMessage, + wireJoinToleranceFor(fModelTolerance))) { + Error("AddSphericalSurface", "%s", errorMessage.c_str()); + return false; + } + + auto record = makeRecord(BVHSurfaceRecord::Spherical, center, polarAxis, referenceAxisU, + {radius, thetaMin, thetaMax, phiStart, phiSweep}, innerWall, true); + storeCurveWires(record, outerTrim, innerTrims); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +bool O2BVHSurfaceSolid::AddConicalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double radiusAtMin, double radiusAtMax, + double heightMin, double heightMax, double phiStart, double phiSweep, + bool innerWall) +{ + if (fImpl->refuseIfDefined(*this, "AddConicalSurface")) { + return false; + } + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), radiusAtMin, + radiusAtMax, heightMin, heightMax, phiStart, phiSweep, innerWall, errorMessage)) { + Error("AddConicalSurface", "%s", errorMessage.c_str()); + return false; + } + + return fImpl->commit(std::move(surface), + makeRecord(BVHSurfaceRecord::Conical, centerPoint, axis, referenceAxisU, + {radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart, phiSweep}, innerWall, + false), + fRecords); +} + +bool O2BVHSurfaceSolid::AddConicalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double radiusAtMin, double radiusAtMax, + double heightMin, double heightMax, double phiStart, double phiSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims) +{ + if (fImpl->refuseIfDefined(*this, "AddConicalSurface")) { + return false; + } + + const std::vector outerCurves = makeCurveWire(outerTrim); + const std::vector> innerCurves = makeCurveWires(innerTrims); + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), radiusAtMin, + radiusAtMax, heightMin, heightMax, phiStart, phiSweep, innerWall, outerCurves, + innerCurves, errorMessage, wireJoinToleranceFor(fModelTolerance))) { + Error("AddConicalSurface", "%s", errorMessage.c_str()); + return false; + } + + auto record = makeRecord(BVHSurfaceRecord::Conical, centerPoint, axis, referenceAxisU, + {radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart, phiSweep}, innerWall, true); + storeCurveWires(record, outerTrim, innerTrims); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +bool O2BVHSurfaceSolid::AddToroidalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double majorRadius, double minorRadius, + double phiStart, double phiSweep, double tubeStart, double tubeSweep, + bool innerWall) +{ + if (fImpl->refuseIfDefined(*this, "AddToroidalSurface")) { + return false; + } + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), majorRadius, minorRadius, + phiStart, phiSweep, tubeStart, tubeSweep, innerWall, errorMessage)) { + Error("AddToroidalSurface", "%s", errorMessage.c_str()); + return false; + } + + return fImpl->commit(std::move(surface), + makeRecord(BVHSurfaceRecord::Toroidal, centerPoint, axis, referenceAxisU, + {majorRadius, minorRadius, phiStart, phiSweep, tubeStart, tubeSweep}, innerWall, + false), + fRecords); +} + +bool O2BVHSurfaceSolid::AddToroidalSurface(const Point3D& centerPoint, const Point3D& axis, + const Point3D& referenceAxisU, double majorRadius, double minorRadius, + double phiStart, double phiSweep, double tubeStart, double tubeSweep, + bool innerWall, const std::vector& outerTrim, + const std::vector>& innerTrims) +{ + if (fImpl->refuseIfDefined(*this, "AddToroidalSurface")) { + return false; + } + + const std::vector outerCurves = makeCurveWire(outerTrim); + const std::vector> innerCurves = makeCurveWires(innerTrims); + + auto surface = std::make_unique(); + std::string errorMessage; + if (!surface->initialize(makeVec3(centerPoint), makeVec3(axis), makeVec3(referenceAxisU), majorRadius, minorRadius, + phiStart, phiSweep, tubeStart, tubeSweep, innerWall, outerCurves, innerCurves, + errorMessage, wireJoinToleranceFor(fModelTolerance))) { + Error("AddToroidalSurface", "%s", errorMessage.c_str()); + return false; + } + + auto record = makeRecord(BVHSurfaceRecord::Toroidal, centerPoint, axis, referenceAxisU, + {majorRadius, minorRadius, phiStart, phiSweep, tubeStart, tubeSweep}, innerWall, true); + storeCurveWires(record, outerTrim, innerTrims); + return fImpl->commit(std::move(surface), std::move(record), fRecords); +} + +void O2BVHSurfaceSolid::CloseShape(bool check) +{ + // An empty surface set is unknown, not closed: leave it undefined (Undetermined) and keep the streamed bounding box. + if (fImpl->surfaces.empty()) { + Error("CloseShape", "Shape %s has no bounded surfaces; it stays undefined and reports itself not navigable", + GetName()); + return; + } + + ComputeBBox(); + + // the display mesh feeds the safety anchors, so it is assembled before the BVH machinery + fImpl->displayVertices.clear(); + fImpl->displayTriangles.clear(); + fImpl->displayTriangleSurface.clear(); + for (size_t surfaceIndex = 0; surfaceIndex < fImpl->surfaces.size(); ++surfaceIndex) { + fImpl->surfaces[surfaceIndex]->appendDisplayMesh(fImpl->displayVertices, fImpl->displayTriangles); + // resize() only writes the entries it adds, so each surface stamps exactly its own triangles. + fImpl->displayTriangleSurface.resize(fImpl->displayTriangles.size(), static_cast(surfaceIndex)); + } + fImpl->collectSafetyAnchors(); + fImpl->buildBVH(); + + fImpl->closure = validateClosure(fImpl->surfaces, fModelTolerance); + fImpl->rimReports.clear(); + fImpl->rimReports.reserve(fImpl->closure.rimRecords.size()); + for (const RimRecord& record : fImpl->closure.rimRecords) { + RimReport report; + report.surface = record.surfaceIndex; + report.rimOnSurface = record.rimIndexOnSurface; + report.closed = record.closed; + report.chords = record.chords; + report.unmatchedChords = record.unmatchedChords; + report.length = record.length; + report.unmatchedLength = record.unmatchedLength; + report.maxIsolation = record.maxIsolation; + report.maxIsolationPoint = {record.maxIsolationPoint.xCoord, record.maxIsolationPoint.yCoord, + record.maxIsolationPoint.zCoord}; + report.maxIsolationFace = record.maxIsolationFace; + report.state = rimStateToReliability(record.state); + fImpl->rimReports.push_back(report); + } + fImpl->defined = true; + fImpl->reliable = GetNavigationReliability() == NavigationReliability::Reliable; + + if (check) { + const auto& closure = fImpl->closure; + // State the consequence, not only the counts. + if (closure.edgeIdentityAvailable && closure.boundaryRims > 0) { + // counted by edge identity, so it says a face is missing + Error("CloseShape", + "Shape %s is NOT a closed surface: %d of its %d source edge(s) have only one face and %d more than two, " + "leaving %d of %d trim loop(s) open; navigation is unreliable, see GetRimReports().", + GetName(), closure.edgeBoundaryCount, closure.edgeIncidences, closure.edgeNonManifoldCount, + closure.boundaryRims, closure.rims); + } else if (closure.boundaryRims > 0) { + Error("CloseShape", + "Shape %s is NOT a closed surface: %d of %d trim loop(s) have no neighbouring face within %g cm, leaving " + "%g cm of %g cm of boundary open (loneliest chord %g cm); navigation is unreliable, see GetRimReports().", + GetName(), closure.boundaryRims, closure.rims, closure.rimEpsilon, closure.unmatchedRimLength, + closure.totalRimLength, closure.maxRimIsolation); + } + if (closure.nonManifoldRims > 0) { + Error("CloseShape", + "Shape %s is NOT a 2-manifold: %d of %d trim loop(s) run along two or more other faces; navigation is " + "unreliable, see GetRimReports().", + GetName(), closure.nonManifoldRims, closure.rims); + } + if (!closure.orientationConsistent) { + Error("CloseShape", + "Shape %s has %d inconsistently oriented (reversed) trim loop(s); navigation is unreliable, see " + "GetRimReports().", + GetName(), closure.reversedRims); + } + if (closure.closed && closure.signedVolume < 0.) { + Warning("CloseShape", + "Shape %s has inward-pointing surface normals (signed volume %g); navigation expects outward normals", + GetName(), closure.signedVolume); + } + } +} + +int O2BVHSurfaceSolid::GetNsurfaces() const +{ + return static_cast(fImpl->surfaces.size()); +} + +bool O2BVHSurfaceSolid::IsDefined() const +{ + return fImpl->defined; +} + +void O2BVHSurfaceSolid::SetModelTolerance(double toleranceCm) +{ + if (!(toleranceCm >= 0.) || !std::isfinite(toleranceCm)) { + Error("SetModelTolerance", "Shape %s: ignoring a non-finite or negative model tolerance %g; it stays %g", + GetName(), toleranceCm, fModelTolerance); + return; + } + fModelTolerance = toleranceCm; +} + +bool O2BVHSurfaceSolid::HasBVH() const +{ + return fImpl->bvh != nullptr; +} + +bool O2BVHSurfaceSolid::GetBVHRootBounds(Point3D& lower, Point3D& upper) const +{ + if (!HasBVH()) { + return false; + } + const auto rootBox = fImpl->bvh->get_root().get_bbox(); + for (int dimension = 0; dimension < 3; ++dimension) { + lower[dimension] = rootBox.min[dimension]; + upper[dimension] = rootBox.max[dimension]; + } + return true; +} + +int O2BVHSurfaceSolid::CountBVHRayCandidates(const Point3D& point, const Point3D& direction) const +{ + if (!HasBVH()) { + return -1; + } + int candidates = 0; + fImpl->visitRayCandidates(makeVec3(point), makeVec3(direction), [&](const BoundedSurface&) { ++candidates; }); + return candidates; +} + +bool O2BVHSurfaceSolid::IsClosed() const +{ + return fImpl->defined && fImpl->closure.closed; +} + +bool O2BVHSurfaceSolid::IsOrientationConsistent() const +{ + return fImpl->defined && fImpl->closure.orientationConsistent; +} + +O2BVHSurfaceSolid::NavigationReliability O2BVHSurfaceSolid::GetNavigationReliability() const +{ + if (!fImpl->defined) { + return NavigationReliability::Undetermined; + } + // the worst defect wins; the enum is ordered by severity + const auto& closure = fImpl->closure; + // With edge identities their counts are the verdict, read directly so that faces without rims still report. + if (closure.edgeIdentityAvailable) { + if (closure.edgeNonManifoldCount > 0) { + return NavigationReliability::NonManifold; + } + if (closure.edgeBoundaryCount > 0) { + return NavigationReliability::OpenSurfaceSet; + } + if (closure.edgeReversedCount > 0) { + return NavigationReliability::ReversedFaces; + } + return NavigationReliability::Reliable; + } + if (closure.nonManifoldRims > 0) { + return NavigationReliability::NonManifold; + } + if (closure.boundaryRims > 0) { + return NavigationReliability::OpenSurfaceSet; + } + if (closure.reversedRims > 0) { + return NavigationReliability::ReversedFaces; + } + return NavigationReliability::Reliable; +} + +bool O2BVHSurfaceSolid::IsNavigable() const +{ + return GetNavigationReliability() == NavigationReliability::Reliable; +} + +const char* O2BVHSurfaceSolid::GetNavigationReliabilityName(NavigationReliability reliability) +{ + switch (reliability) { + case NavigationReliability::Undetermined: + return "undetermined"; + case NavigationReliability::Reliable: + return "reliable"; + case NavigationReliability::ReversedFaces: + return "reversed-faces"; + case NavigationReliability::OpenSurfaceSet: + return "open-surface-set"; + case NavigationReliability::NonManifold: + return "non-manifold"; + } + return "unknown"; +} + +int O2BVHSurfaceSolid::GetBoundaryEdgeCount() const +{ + return fImpl->closure.boundaryEdges; +} + +int O2BVHSurfaceSolid::GetNonManifoldEdgeCount() const +{ + return fImpl->closure.nonManifoldEdges; +} + +int O2BVHSurfaceSolid::GetReversedEdgeCount() const +{ + return fImpl->closure.reversedEdges; +} + +double O2BVHSurfaceSolid::GetMaxRimIsolation() const +{ + return fImpl->closure.maxRimIsolation; +} + +bool O2BVHSurfaceSolid::SetSurfaceBoundaryEdges(int surfaceIndex, const std::vector& edgeIds, + const std::vector& edgeFlags) +{ + if (surfaceIndex < 0 || surfaceIndex >= static_cast(fImpl->surfaces.size()) || + surfaceIndex >= static_cast(fRecords.size())) { + Error("SetSurfaceBoundaryEdges", "Shape %s: surface index %d is out of range (%d surface(s))", GetName(), + surfaceIndex, GetNsurfaces()); + return false; + } + if (edgeIds.size() != edgeFlags.size()) { + Error("SetSurfaceBoundaryEdges", "Shape %s: surface %d was given %d edge id(s) and %d flag(s)", GetName(), + surfaceIndex, static_cast(edgeIds.size()), static_cast(edgeFlags.size())); + return false; + } + std::vector refs; + refs.reserve(edgeIds.size()); + for (size_t index = 0; index < edgeIds.size(); ++index) { + BoundedSurface::BoundaryEdgeRef ref; + ref.edgeId = edgeIds[index]; + ref.reversed = (edgeFlags[index] & kEdgeReversed) != 0; + ref.degenerate = (edgeFlags[index] & kEdgeDegenerate) != 0; + ref.anchored = (edgeFlags[index] & kEdgeAnchored) != 0; + refs.push_back(ref); + } + fImpl->surfaces[static_cast(surfaceIndex)]->setBoundaryEdges(std::move(refs)); + fRecords[static_cast(surfaceIndex)].boundaryEdgeIds = edgeIds; + fRecords[static_cast(surfaceIndex)].boundaryEdgeFlags = edgeFlags; + return true; +} + +bool O2BVHSurfaceSolid::HasEdgeIdentity() const +{ + return fImpl->closure.edgeIdentityAvailable; +} + +int O2BVHSurfaceSolid::GetSourceEdgeCount() const +{ + return fImpl->closure.edgeIncidences; +} + +int O2BVHSurfaceSolid::GetSharedSourceEdgeCount() const +{ + return fImpl->closure.edgeSharedCount; +} + +int O2BVHSurfaceSolid::GetBoundarySourceEdgeCount() const +{ + return fImpl->closure.edgeBoundaryCount; +} + +int O2BVHSurfaceSolid::GetNonManifoldSourceEdgeCount() const +{ + return fImpl->closure.edgeNonManifoldCount; +} + +int O2BVHSurfaceSolid::GetReversedSourceEdgeCount() const +{ + return fImpl->closure.edgeReversedCount; +} + +int O2BVHSurfaceSolid::GetDegenerateSourceEdgeCount() const +{ + return fImpl->closure.edgeDegenerateCount; +} + +double O2BVHSurfaceSolid::GetMaxSharedEdgeDeviation() const +{ + return fImpl->closure.maxSharedEdgeDeviation; +} + +int O2BVHSurfaceSolid::GetMeasuredSharedEdgeCount() const +{ + return fImpl->closure.sharedEdgesMeasured; +} + +int O2BVHSurfaceSolid::GetUnmeasuredSharedEdgeCount() const +{ + return fImpl->closure.sharedEdgesUnmeasured; +} + +double O2BVHSurfaceSolid::GetRimChordResolution() const +{ + return fImpl->closure.rimChordResolution; +} + +double O2BVHSurfaceSolid::GetRimMatchTolerance() const +{ + return fImpl->closure.rimEpsilon; +} + +double O2BVHSurfaceSolid::GetTotalRimLength() const +{ + return fImpl->closure.totalRimLength; +} + +double O2BVHSurfaceSolid::GetUnmatchedRimLength() const +{ + return fImpl->closure.unmatchedRimLength; +} + +int O2BVHSurfaceSolid::GetRimCount() const +{ + return fImpl->closure.rims; +} + +int O2BVHSurfaceSolid::GetMatchedRimCount() const +{ + return fImpl->closure.matchedRims; +} + +int O2BVHSurfaceSolid::GetBoundaryRimCount() const +{ + return fImpl->closure.boundaryRims; +} + +int O2BVHSurfaceSolid::GetNonManifoldRimCount() const +{ + return fImpl->closure.nonManifoldRims; +} + +int O2BVHSurfaceSolid::GetReversedRimCount() const +{ + return fImpl->closure.reversedRims; +} + +const std::vector& O2BVHSurfaceSolid::GetRimReports() const +{ + return fImpl->rimReports; +} + +void O2BVHSurfaceSolid::GetSurfaceCapacityContributions(std::vector& contributions) const +{ + contributions.clear(); + contributions.reserve(fImpl->surfaces.size()); + for (const auto& surface : fImpl->surfaces) { + contributions.push_back(surface == nullptr ? 0. : surface->capacityContribution()); + } +} + +void O2BVHSurfaceSolid::ComputeBBox() +{ + if (fImpl->surfaces.empty()) { + fDX = fDY = fDZ = 0.; + fOrigin[0] = fOrigin[1] = fOrigin[2] = 0.; + return; + } + + Vec3 lowerCorner{TGeoShape::Big(), TGeoShape::Big(), TGeoShape::Big()}; + Vec3 upperCorner{-TGeoShape::Big(), -TGeoShape::Big(), -TGeoShape::Big()}; + for (const auto& surface : fImpl->surfaces) { + surface->conservativeBounds(lowerCorner, upperCorner); + } + + for (int dimension = 0; dimension < 3; ++dimension) { + const double lowerValue = component(lowerCorner, dimension) - kTolerance; + const double upperValue = component(upperCorner, dimension) + kTolerance; + fOrigin[dimension] = 0.5 * (lowerValue + upperValue); + const double halfLength = 0.5 * (upperValue - lowerValue); + if (dimension == 0) { + fDX = halfLength; + } else if (dimension == 1) { + fDY = halfLength; + } else { + fDZ = halfLength; + } + } +} + +void O2BVHSurfaceSolid::GetMeshNumbers(int& nvert, int& nsegs, int& npols) const +{ + nvert = GetNmeshVertices(); + npols = static_cast(fImpl->displayTriangles.size()); + nsegs = 3 * npols; +} + +int O2BVHSurfaceSolid::GetNmeshVertices() const +{ + return static_cast(fImpl->displayVertices.size()); +} + +namespace +{ +/// The deterministic R2 low-discrepancy pair in [0,1)^2, so a shape's sample points depend on the shape alone. +void r2Pair(long long index, double& firstCoordinate, double& secondCoordinate) +{ + constexpr double kAlpha1 = 0.7548776662466927; // 1 / plastic number + constexpr double kAlpha2 = 0.5698402909980532; // 1 / plastic number^2 + const double shifted = static_cast(index + 1); + firstCoordinate = std::fmod(0.5 + kAlpha1 * shifted, 1.); + secondCoordinate = std::fmod(0.5 + kAlpha2 * shifted, 1.); +} +} // namespace + +/// Newton on the patch distance along the patch normal; returns whether the point reached the surface. +bool O2BVHSurfaceSolid::ProjectOntoPatch(int surfaceIndex, double* point) const +{ + if (surfaceIndex < 0 || static_cast(surfaceIndex) >= fImpl->surfaces.size()) { + return false; + } + const BoundedSurface& surface = *fImpl->surfaces[surfaceIndex]; + constexpr double kToleranceSquared = kSurfacePointTolerance * kSurfacePointTolerance; + + Vec3 current = makeVec3(point); + double currentDistanceSq = surface.distanceSqToPatch(current); + for (int iteration = 0; iteration < 8 && currentDistanceSq > kToleranceSquared; ++iteration) { + const double distance = std::sqrt(currentDistanceSq); + const Vec3 normal = surface.normalAt(current); + const Vec3 inward{current.xCoord - distance * normal.xCoord, current.yCoord - distance * normal.yCoord, + current.zCoord - distance * normal.zCoord}; + const Vec3 outward{current.xCoord + distance * normal.xCoord, current.yCoord + distance * normal.yCoord, + current.zCoord + distance * normal.zCoord}; + const double inwardDistanceSq = surface.distanceSqToPatch(inward); + const double outwardDistanceSq = surface.distanceSqToPatch(outward); + const double bestDistanceSq = std::min(inwardDistanceSq, outwardDistanceSq); + // not converging: the nearest patch point lies on the trim wire + if (!(bestDistanceSq < currentDistanceSq)) { + return false; + } + current = (inwardDistanceSq < outwardDistanceSq) ? inward : outward; + currentDistanceSq = bestDistanceSq; + } + + if (currentDistanceSq > kToleranceSquared) { + return false; + } + point[0] = current.xCoord; + point[1] = current.yCoord; + point[2] = current.zCoord; + return true; +} + +Bool_t O2BVHSurfaceSolid::GetPointsOnSegments(Int_t npoints, Double_t* array) const +{ + if (array == nullptr || npoints <= 0 || fImpl->displayVertices.empty()) { + return kFALSE; + } + const int vertexCount = static_cast(fImpl->displayVertices.size()); + // Below the mesh size, decline so ROOT uses SetPoints(), whose vertices all lie on patches. + if (npoints < vertexCount) { + return kFALSE; + } + + auto writeVertex = [&](int slot, const Vec3& vertex) { + array[3 * slot + 0] = vertex.xCoord; + array[3 * slot + 1] = vertex.yCoord; + array[3 * slot + 2] = vertex.zCoord; + }; + + for (int vertexIndex = 0; vertexIndex < vertexCount; ++vertexIndex) { + writeVertex(vertexIndex, fImpl->displayVertices[vertexIndex]); + } + + const int extraCount = npoints - vertexCount; + const int triangleCount = static_cast(fImpl->displayTriangles.size()); + if (extraCount == 0) { + return kTRUE; + } + if (triangleCount == 0 || fImpl->displayTriangleSurface.size() != fImpl->displayTriangles.size()) { + // No triangles (or a mesh built before the provenance existed): repeat vertices rather than + // leave the tail of the buffer uninitialised, which the caller would read as coordinates. + for (int extraIndex = 0; extraIndex < extraCount; ++extraIndex) { + writeVertex(vertexCount + extraIndex, fImpl->displayVertices[extraIndex % vertexCount]); + } + return kTRUE; + } + + for (int extraIndex = 0; extraIndex < extraCount; ++extraIndex) { + // Stride over the triangles rather than walking them in order, so a request that cannot cover + // every triangle still spreads over the whole solid instead of over its first few faces. + const int triangleIndex = + static_cast((static_cast(extraIndex) * triangleCount) / extraCount) % triangleCount; + const auto& triangle = fImpl->displayTriangles[triangleIndex]; + const Vec3& cornerA = fImpl->displayVertices[triangle[0]]; + const Vec3& cornerB = fImpl->displayVertices[triangle[1]]; + const Vec3& cornerC = fImpl->displayVertices[triangle[2]]; + + double firstCoordinate = 0.; + double secondCoordinate = 0.; + r2Pair(extraIndex, firstCoordinate, secondCoordinate); + if (firstCoordinate + secondCoordinate > 1.) { + firstCoordinate = 1. - firstCoordinate; + secondCoordinate = 1. - secondCoordinate; + } + const double weightA = 1. - firstCoordinate - secondCoordinate; + double candidate[3] = {weightA * cornerA.xCoord + firstCoordinate * cornerB.xCoord + secondCoordinate * cornerC.xCoord, + weightA * cornerA.yCoord + firstCoordinate * cornerB.yCoord + secondCoordinate * cornerC.yCoord, + weightA * cornerA.zCoord + firstCoordinate * cornerB.zCoord + secondCoordinate * cornerC.zCoord}; + + if (!ProjectOntoPatch(fImpl->displayTriangleSurface[triangleIndex], candidate)) { + // fall back to a vertex of the sampled triangle, which is on the patch + candidate[0] = cornerA.xCoord; + candidate[1] = cornerA.yCoord; + candidate[2] = cornerA.zCoord; + } + array[3 * (vertexCount + extraIndex) + 0] = candidate[0]; + array[3 * (vertexCount + extraIndex) + 1] = candidate[1]; + array[3 * (vertexCount + extraIndex) + 2] = candidate[2]; + } + return kTRUE; +} + +TBuffer3D* O2BVHSurfaceSolid::MakeBuffer3D() const +{ + int nvert = 0; + int nsegs = 0; + int npols = 0; + GetMeshNumbers(nvert, nsegs, npols); + auto buff = new TBuffer3D(TBuffer3DTypes::kGeneric, nvert, 3 * nvert, nsegs, 3 * nsegs, npols, 6 * npols); + if (buff != nullptr) { + SetPoints(buff->fPnts); + SetSegsAndPols(*buff); + } + return buff; +} + +void O2BVHSurfaceSolid::Print(Option_t*) const +{ + std::cout << "=== BVH surface solid " << GetName() << " having " << GetNsurfaces() << " bounded surfaces\n"; + const auto reliability = GetNavigationReliability(); + std::cout << " navigation: " << GetNavigationReliabilityName(reliability); + if (reliability != NavigationReliability::Reliable && reliability != NavigationReliability::Undetermined) { + std::cout << " (UNRELIABLE; boundary=" << GetBoundaryEdgeCount() << " non-manifold=" << GetNonManifoldEdgeCount() + << " reversed=" << GetReversedEdgeCount() << ")"; + } + std::cout << "\n model tolerance: "; + if (fModelTolerance > 0.) { + std::cout << fModelTolerance << " cm (from the source model)"; + } else { + std::cout << "not stated"; + } + // the identity counts first: when present they are the verdict + if (HasEdgeIdentity()) { + std::cout << "\n edge identity: " << GetSourceEdgeCount() << " source edge(s), shared=" << GetSharedSourceEdgeCount() + << " boundary=" << GetBoundarySourceEdgeCount() << " non-manifold=" << GetNonManifoldSourceEdgeCount() + << " reversed=" << GetReversedSourceEdgeCount() << " degenerate=" << GetDegenerateSourceEdgeCount() + << "\n shared edge deviation: max " << GetMaxSharedEdgeDeviation() << " cm over " + << GetMeasuredSharedEdgeCount() << " measured edge(s)"; + if (GetUnmeasuredSharedEdgeCount() > 0) { + std::cout << " (" << GetUnmeasuredSharedEdgeCount() << " not measurable: parametric-rectangle trim)"; + } + } + // The isolation, and the resolution that widened the band it was judged in, always together: the + // number is how alone the loneliest chord is, not how far apart two faces are. + if (GetRimCount() > 0) { + std::cout << "\n rim isolation: max " << GetMaxRimIsolation() << " cm (chord resolution " + << GetRimChordResolution() << " cm, declared tolerance " << GetRimMatchTolerance() << " cm)" + << "\n rims: " << GetRimCount() << " (matched=" << GetMatchedRimCount() + << " boundary=" << GetBoundaryRimCount() << " non-manifold=" << GetNonManifoldRimCount() + << " reversed=" << GetReversedRimCount() << "), open " << GetUnmatchedRimLength() << " of " + << GetTotalRimLength() << " cm"; + } + std::cout << "\n"; +} + +void O2BVHSurfaceSolid::SetPoints(double* points) const +{ + int coordinateIndex = 0; + for (const auto& vertex : fImpl->displayVertices) { + points[coordinateIndex++] = vertex.xCoord; + points[coordinateIndex++] = vertex.yCoord; + points[coordinateIndex++] = vertex.zCoord; + } +} + +void O2BVHSurfaceSolid::SetPoints(Float_t* points) const +{ + int coordinateIndex = 0; + for (const auto& vertex : fImpl->displayVertices) { + points[coordinateIndex++] = vertex.xCoord; + points[coordinateIndex++] = vertex.yCoord; + points[coordinateIndex++] = vertex.zCoord; + } +} + +void O2BVHSurfaceSolid::SetSegsAndPols(TBuffer3D& buff) const +{ + const int color = GetBasicColor(); + int* segs = buff.fSegs; + int* pols = buff.fPols; + int segmentDataIndex = 0; + int polygonDataIndex = 0; + int segmentIndex = 0; + for (const auto& triangle : fImpl->displayTriangles) { + pols[polygonDataIndex++] = color; + pols[polygonDataIndex++] = 3; + for (int triangleEdge = 0; triangleEdge < 3; ++triangleEdge) { + const int nextTriangleEdge = (triangleEdge + 1) % 3; + segs[segmentDataIndex++] = color; + segs[segmentDataIndex++] = triangle[triangleEdge]; + segs[segmentDataIndex++] = triangle[nextTriangleEdge]; + pols[polygonDataIndex + 2 - triangleEdge] = segmentIndex++; + } + polygonDataIndex += 3; + } +} + +const TBuffer3D& O2BVHSurfaceSolid::GetBuffer3D(int reqSections, Bool_t localFrame) const +{ + static TBuffer3D buffer(TBuffer3DTypes::kGeneric); + + FillBuffer3D(buffer, reqSections, localFrame); + + int nvert = 0; + int nsegs = 0; + int npols = 0; + GetMeshNumbers(nvert, nsegs, npols); + + if (reqSections & TBuffer3D::kRawSizes) { + if (buffer.SetRawSizes(nvert, 3 * nvert, nsegs, 3 * nsegs, npols, 6 * npols)) { + buffer.SetSectionsValid(TBuffer3D::kRawSizes); + } + } + if ((reqSections & TBuffer3D::kRaw) && buffer.SectionsValid(TBuffer3D::kRawSizes)) { + SetPoints(buffer.fPnts); + if (!buffer.fLocalFrame) { + TransformPoints(buffer.fPnts, buffer.NbPnts()); + } + SetSegsAndPols(buffer); + buffer.SetSectionsValid(TBuffer3D::kRaw); + } + + return buffer; +} + +bool O2BVHSurfaceSolid::Contains(const Double_t* point) const +{ + if (fImpl->surfaces.empty()) { + return false; + } + + if (fImpl->bvh == nullptr) { + // Before CloseShape there is no BVH and no bounding box, so this fallback must come before the box check. + return Contains_Loop(point); + } + + const Vec3 testPoint = makeVec3(point); + if (std::abs(testPoint.xCoord - fOrigin[0]) > fDX + kTolerance || + std::abs(testPoint.yCoord - fOrigin[1]) > fDY + kTolerance || + std::abs(testPoint.zCoord - fOrigin[2]) > fDZ + kTolerance) { + return false; + } + + // boundary policy: a point within tolerance of any surface patch counts as inside + if (fImpl->visitPointCandidates( + testPoint, [&](const BoundedSurface& surface) { return surface.containsPointOnSurface(testPoint); })) { + return true; + } + + return containsByParity(point, true); +} + +bool O2BVHSurfaceSolid::ContainsAlongDirection(const Double_t* point, const Double_t* direction) const +{ + if (fImpl->surfaces.empty()) { + return false; + } + const Vec3 testPoint = makeVec3(point); + for (const auto& surface : fImpl->surfaces) { + if (surface->containsPointOnSurface(testPoint)) { + return true; + } + } + return fImpl->parityAlong(testPoint, normalized(makeVec3(direction)), fImpl->bvh != nullptr); +} + +bool O2BVHSurfaceSolid::Contains_Loop(const Double_t* point) const +{ + if (fImpl->surfaces.empty()) { + return false; + } + + const Vec3 testPoint = makeVec3(point); + for (const auto& surface : fImpl->surfaces) { + if (surface->containsPointOnSurface(testPoint)) { + return true; + } + } + + return containsByParity(point, false); +} + +bool O2BVHSurfaceSolid::containsByParity(const Double_t* point, bool useBVH) const +{ + // Reliable solid: one parity shot, unless it rests on a trim-band tie-break; otherwise a 5-direction vote. + const Vec3 testPoint = makeVec3(point); + if (fImpl->reliable) { + bool ambiguous = false; + const bool answer = fImpl->parityAlong(testPoint, kContainsTestDirection, useBVH, &ambiguous); + if (!ambiguous) { + return answer; + } + // This shot crossed a patch within its own trim accuracy, so its parity rests on a tie-break + // rather than on the geometry. Re-aim: the sliver belongs to the ray, not to the point. + return fImpl->containsByVote(testPoint, useBVH); + } + return fImpl->containsByVote(testPoint, useBVH); +} + +void O2BVHSurfaceSolid::DescribeContainsCrossings(const Point3D& point, + std::vector& bvhCrossings, + std::vector& loopCrossings) const +{ + const Point3D direction{kContainsTestDirection.xCoord, kContainsTestDirection.yCoord, + kContainsTestDirection.zCoord}; + DescribeContainsCrossings(point, direction, bvhCrossings, loopCrossings); +} + +void O2BVHSurfaceSolid::DescribeContainsCrossings(const Point3D& point, const Point3D& direction, + std::vector& bvhCrossings, + std::vector& loopCrossings) const +{ + bvhCrossings.clear(); + loopCrossings.clear(); + if (fImpl->surfaces.empty()) { + return; + } + const Vec3 testPoint = makeVec3(point.data()); + const Vec3 testDirection = normalized(makeVec3(direction.data())); + + auto collect = [&](std::vector& hits, std::vector& out) { + std::sort(hits.begin(), hits.end(), + [](const RayHit& first, const RayHit& second) { return first.distance < second.distance; }); + out.reserve(hits.size()); + for (const auto& hit : hits) { + out.push_back({hit.distance, dot(hit.normal, testDirection), hit.onTrimBoundary}); + } + }; + + std::vector loopHits; + for (const auto& surface : fImpl->surfaces) { + surface->appendIntersections(testPoint, testDirection, kRayTolerance, TGeoShape::Big(), loopHits); + } + collect(loopHits, loopCrossings); + + if (fImpl->bvh != nullptr) { + std::vector bvhHits; + fImpl->visitRayCandidates(testPoint, testDirection, [&](const BoundedSurface& surface) { + surface.appendIntersections(testPoint, testDirection, kRayTolerance, TGeoShape::Big(), bvhHits); + }); + collect(bvhHits, bvhCrossings); + } +} + +Double_t O2BVHSurfaceSolid::DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact, Double_t stepmax, + Double_t* safe) const +{ + if (iact < 3 && safe != nullptr) { + *safe = Safety(point, kFALSE); + if (iact == 0) { + return TGeoShape::Big(); + } + if (iact == 1 && stepmax < *safe) { + return TGeoShape::Big(); + } + } + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + if (fImpl->bvh == nullptr) { + // before CloseShape there is no acceleration structure yet; stay usable via the plain loop + return DistFromOutside_Loop(point, dir, stepmax); + } + + // cheap reject: a per-axis gap to the bounding box beyond stepmax means no reachable crossing + const Double_t halfLengths[3] = {fDX, fDY, fDZ}; + for (int dimension = 0; dimension < 3; ++dimension) { + const Double_t lower = fOrigin[dimension] - halfLengths[dimension]; + const Double_t upper = fOrigin[dimension] + halfLengths[dimension]; + if (lower - point[dimension] > stepmax + kBVHBoxTolerance || + point[dimension] - upper > stepmax + kBVHBoxTolerance) { + return TGeoShape::Big(); + } + } + + return fImpl->nearestCrossing(makeVec3(point), makeVec3(dir), stepmax); +} + +Double_t O2BVHSurfaceSolid::DistFromInside(const Double_t* point, const Double_t* dir, Int_t iact, Double_t stepmax, + Double_t* safe) const +{ + if (iact < 3 && safe != nullptr) { + *safe = Safety(point, kTRUE); + if (iact == 0) { + return TGeoShape::Big(); + } + if (iact == 1 && stepmax < *safe) { + return TGeoShape::Big(); + } + } + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + if (fImpl->bvh == nullptr) { + return DistFromInside_Loop(point, dir, stepmax); + } + // no bounding-box reject here: the point is inside by contract, so the box is always reachable + return fImpl->nearestCrossing(makeVec3(point), makeVec3(dir), stepmax); +} + +Double_t O2BVHSurfaceSolid::DistFromOutside_Loop(const Double_t* point, const Double_t* dir, Double_t stepmax) const +{ + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + return fImpl->nearestCrossingLoop(makeVec3(point), makeVec3(dir), stepmax); +} + +Double_t O2BVHSurfaceSolid::DistFromInside_Loop(const Double_t* point, const Double_t* dir, Double_t stepmax) const +{ + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + return fImpl->nearestCrossingLoop(makeVec3(point), makeVec3(dir), stepmax); +} + +void O2BVHSurfaceSolid::SetRayTMaxPruning(bool enable) +{ + gRayTMaxPruning = enable; +} + +bool O2BVHSurfaceSolid::GetRayTMaxPruning() +{ + return gRayTMaxPruning; +} + +void O2BVHSurfaceSolid::ResetRayCandidateCounter() +{ + gRayCandidateCount = 0; +} + +long long O2BVHSurfaceSolid::GetRayCandidateCount() +{ + return gRayCandidateCount; +} + +void O2BVHSurfaceSolid::ResetSafetyCandidateCounter() +{ + gSafetyCandidateCount = 0; +} + +long long O2BVHSurfaceSolid::GetSafetyCandidateCount() +{ + return gSafetyCandidateCount; +} + +void O2BVHSurfaceSolid::SetSafetyBoundUnsoundForTest(bool enable) +{ + gSafetyBoundUnsound = enable; +} + +bool O2BVHSurfaceSolid::GetSafetyBoundUnsoundForTest() +{ + return gSafetyBoundUnsound; +} + +/// The distance to the nearest patch, rounded down by one ulp so that Safety is never too large. +Double_t O2BVHSurfaceSolid::Safety(const Double_t* point, Bool_t) const +{ + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + const double bestDistanceSq = fImpl->nearestPatchDistanceSq(makeVec3(point), nullptr); + return std::nextafter(std::sqrt(bestDistanceSq), 0.); +} + +Double_t O2BVHSurfaceSolid::Safety_Loop(const Double_t* point, Bool_t) const +{ + if (fImpl->surfaces.empty()) { + return TGeoShape::Big(); + } + const double bestDistanceSq = fImpl->nearestPatchDistanceSqLoop(makeVec3(point), nullptr); + return std::nextafter(std::sqrt(bestDistanceSq), 0.); +} + +void O2BVHSurfaceSolid::ComputeNormal(const Double_t* point, const Double_t* dir, Double_t* norm) const +{ + computeNormalFrom(point, dir, norm, false); +} + +void O2BVHSurfaceSolid::ComputeNormal_Loop(const Double_t* point, const Double_t* dir, Double_t* norm) const +{ + computeNormalFrom(point, dir, norm, true); +} + +void O2BVHSurfaceSolid::computeNormalFrom(const Double_t* point, const Double_t* dir, Double_t* norm, + bool useLoop) const +{ + if (fImpl->surfaces.empty()) { + norm[0] = 1.; + norm[1] = 0.; + norm[2] = 0.; + return; + } + + const Vec3 testPoint = makeVec3(point); + size_t closestIndex = fImpl->surfaces.size(); + if (useLoop) { + fImpl->nearestPatchDistanceSqLoop(testPoint, &closestIndex); + } else { + fImpl->nearestPatchDistanceSq(testPoint, &closestIndex); + } + + if (closestIndex >= fImpl->surfaces.size()) { + norm[0] = 1.; + norm[1] = 0.; + norm[2] = 0.; + return; + } + + Vec3 normal = fImpl->surfaces[closestIndex]->normalAt(testPoint); + if (dir != nullptr) { + const Vec3 direction = makeVec3(dir); + if (dot(normal, direction) < 0.) { + normal = normal * -1.; + } + } + norm[0] = normal.xCoord; + norm[1] = normal.yCoord; + norm[2] = normal.zCoord; +} + +Double_t O2BVHSurfaceSolid::Capacity() const +{ + double capacity = 0.; + for (const auto& surface : fImpl->surfaces) { + capacity += surface->capacityContribution(); + } + return std::abs(capacity); +} + +bool O2BVHSurfaceSolid::RebuildFromRecords() +{ + // Add*Surface refuses to run on a defined shape and re-appends to fRecords as it replays, so + // take the records aside and start from a fresh implementation. + std::vector records; + records.swap(fRecords); + delete fImpl; + fImpl = new Impl; + // a solid missing a face is a different solid, so a failed record discards the whole shape + const auto discard = [this]() { + fRecords.clear(); + delete fImpl; + fImpl = new Impl; + return false; + }; + + if (records.empty()) { + Error("RebuildFromRecords", "Shape %s carries no surface records, so it stays undefined and not navigable.", + GetName()); + return false; + } + + for (size_t recordIndex = 0; recordIndex < records.size(); ++recordIndex) { + const auto& record = records[recordIndex]; + const int expectedScalars = BVHSurfaceRecord::expectedScalarCount(record.kind); + if (expectedScalars < 0 || record.scalars.size() != static_cast(expectedScalars)) { + Error("RebuildFromRecords", "Shape %s: surface record %d has kind %d with %d scalar(s), expected %d", GetName(), + static_cast(recordIndex), record.kind, static_cast(record.scalars.size()), expectedScalars); + return discard(); + } + + const Point3D origin = makePoint3D(record.origin); + const Point3D axisA = makePoint3D(record.axisA); + const Point3D axisB = makePoint3D(record.axisB); + const auto& s = record.scalars; + + std::vector outerWire; + std::vector> innerWires; + std::vector outerPolygon; + std::vector> innerPolygons; + const bool wiresLoaded = record.kind == BVHSurfaceRecord::PlanarPolygon + ? loadPolygonWires(record, outerPolygon, innerPolygons) + : loadCurveWires(record, outerWire, innerWires); + + bool added = false; + if (!wiresLoaded) { + Error("RebuildFromRecords", "Shape %s: surface record %d has inconsistent wire sizes", GetName(), + static_cast(recordIndex)); + } else { + switch (record.kind) { + case BVHSurfaceRecord::PlanarPolygon: + added = AddPlanarSurface(origin, axisA, axisB, outerPolygon, innerPolygons); + break; + case BVHSurfaceRecord::CurvedPlanar: + added = AddCurvedPlanarSurface(origin, axisA, axisB, outerWire, innerWires); + break; + case BVHSurfaceRecord::Cylindrical: + added = record.trimmed ? AddCylindricalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], + record.innerWall, outerWire, innerWires) + : AddCylindricalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], + record.innerWall); + break; + case BVHSurfaceRecord::Spherical: + added = record.trimmed ? AddSphericalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], + record.innerWall, outerWire, innerWires) + : AddSphericalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], + record.innerWall); + break; + case BVHSurfaceRecord::Conical: + added = record.trimmed ? AddConicalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], s[5], + record.innerWall, outerWire, innerWires) + : AddConicalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], s[5], + record.innerWall); + break; + case BVHSurfaceRecord::Toroidal: + added = record.trimmed ? AddToroidalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], s[5], + record.innerWall, outerWire, innerWires) + : AddToroidalSurface(origin, axisA, axisB, s[0], s[1], s[2], s[3], s[4], s[5], + record.innerWall); + break; + default: + break; + } + } + + if (!added) { + // a solid missing a face is a different solid: discard it rather than return a partial shape + Error("RebuildFromRecords", + "Shape %s: surface record %d (kind %d) did not rebuild, so the shape is discarded and stays undefined.", + GetName(), static_cast(recordIndex), record.kind); + return discard(); + } + + // the edge identities are part of the record: replay them, or the read-back closure verdict could differ + if (!record.boundaryEdgeIds.empty()) { + SetSurfaceBoundaryEdges(static_cast(recordIndex), record.boundaryEdgeIds, record.boundaryEdgeFlags); + } + } + + // check == false: replaying a solid must not re-emit the closure diagnostics that were already + // reported when it was first built. The report itself is recomputed, not trusted. + CloseShape(false); + return true; +} + +void O2BVHSurfaceSolid::Streamer(TBuffer& buffer) +{ + if (buffer.IsReading()) { + buffer.ReadClassBuffer(O2BVHSurfaceSolid::Class(), this); + RebuildFromRecords(); + } else { + buffer.WriteClassBuffer(O2BVHSurfaceSolid::Class(), this); + } +} \ No newline at end of file diff --git a/Detectors/CADSupport/src/O2FlatCSG.cxx b/Detectors/CADSupport/src/O2FlatCSG.cxx new file mode 100644 index 0000000000000..414d8e211bf50 --- /dev/null +++ b/Detectors/CADSupport/src/O2FlatCSG.cxx @@ -0,0 +1,1438 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#include "CADSupport/O2FlatCSG.h" + +#include "BoundedSurface.h" + +// the same third-party BVH2 entry point O2Tessellated, O2BVHSurfaceSolid and O2BVHAssembly use +#include "bvh2_third_party.h" +#include "bvh2_extra_kernels.h" + +#include "TGeoShape.h" + +#include +#include +#include +#include +#include +#include +#include + +ClassImp(o2::cad::O2FlatCSG); + +namespace o2 +{ +namespace cad +{ + +namespace +{ +/// The most roots one cell can contribute to one ray: four per torus halfspace. +constexpr int kMaxRootsPerHalfspace = 4; + +/// Per-path cap on SplitBox's aspect-ratio-equalising splits; bounds recursion on pathological cells. +constexpr int kMaxCubifySplits = 10; + +/// An upper bound on the `[enter, exit]` pairs one cell produces along a ray. +int maxPairsForCell(int halfspaceCount) +{ + return 2 + kMaxRootsPerHalfspace * halfspaceCount; +} + +// float BVH types: the BVH only nominates boxes, and roundOutward makes each node box a superset of its boxes. +using BVHScalar = float; +using BVHBBox = bvh::v2::BBox; +using BVHVec3 = bvh::v2::Vec; +using BVHNode = bvh::v2::Node; +using BVH = bvh::v2::Bvh; + +/// Per-thread count of DistFromInside queries redone without pruning. +thread_local long long gUnprunedRetryCount = 0; + +/// Round a double outward into float, away from the interval the box encloses. +inline float roundOutward(double value, bool up) +{ + return std::nextafterf(static_cast(value), up ? std::numeric_limits::infinity() + : -std::numeric_limits::infinity()); +} + +/// Clip [tlo, thi] to the box's slab; false when nothing survives. +/// Divides by dir (no reciprocal) so a box face on the cell's own plane gives HalfspaceRoots' t exactly. +bool slabWindow(const double* boxMin, const double* boxMax, const double* origin, const double* dir, + double& tlo, double& thi) +{ + for (int index = 0; index < 3; ++index) { + if (std::abs(dir[index]) < 1.e-300) { + // parallel to this pair of faces: the ray is either inside the slab for every t or outside + // it for every t + if (origin[index] < boxMin[index] || origin[index] > boxMax[index]) { + return false; + } + continue; + } + double low = (boxMin[index] - origin[index]) / dir[index]; + double high = (boxMax[index] - origin[index]) / dir[index]; + if (low > high) { + std::swap(low, high); + } + tlo = std::max(tlo, low); + thi = std::min(thi, high); + if (tlo > thi) { + return false; + } + } + return true; +} + +/// The same clip against a BVH node's (float, outward-rounded) box. +inline bool nodeWindow(const BVHBBox& box, const double* origin, const double* dir, double& tlo, + double& thi) +{ + const double lo[3] = {box.min[0], box.min[1], box.min[2]}; + const double hi[3] = {box.max[0], box.max[1], box.max[2]}; + return slabWindow(lo, hi, origin, dir, tlo, thi); +} + +/// Whether \a point is in the box's own double bounds, closed on every face. +inline bool boxHoldsPoint(const FlatCSGBox& box, const double* point) +{ + return point[0] >= box.min[0] && point[0] <= box.max[0] && point[1] >= box.min[1] && + point[1] <= box.max[1] && point[2] >= box.min[2] && point[2] <= box.max[2]; +} + +/// Unnormalised gradient of `sign * f` at \a point: `2(Ax + b)` for a quadric, the gradient of the signed distance for a torus. +void halfspaceGradient(const FlatCSGHalfspace& halfspace, const double* point, double grad[3]) +{ + if (halfspace.kind == FlatCSGHalfspace::kTorus) { + const double* c = halfspace.c; + const double axis[3] = {c[3], c[4], c[5]}; + const double major = c[6]; + const double offset[3] = {point[0] - c[0], point[1] - c[1], point[2] - c[2]}; + const double along = offset[0] * axis[0] + offset[1] * axis[1] + offset[2] * axis[2]; + double radial[3]; + for (int index = 0; index < 3; ++index) { + radial[index] = offset[index] - along * axis[index]; + } + const double rho = std::sqrt(radial[0] * radial[0] + radial[1] * radial[1] + radial[2] * radial[2]); + const double u = rho - major; + const double s = std::hypot(u, along); + if (s < 1.e-300 || rho < 1.e-300) { + // degenerate: on the revolution axis or the kissing point; leave it zero for the caller's fallback + grad[0] = grad[1] = grad[2] = 0.; + return; + } + const double du = u / s; + const double dv = along / s; + for (int index = 0; index < 3; ++index) { + grad[index] = halfspace.sign * (du * (radial[index] / rho) + dv * axis[index]); + } + return; + } + const double* c = halfspace.c; + const double a[3][3] = {{c[0], c[1], c[2]}, {c[1], c[3], c[4]}, {c[2], c[4], c[5]}}; + const double b[3] = {c[6], c[7], c[8]}; + for (int row = 0; row < 3; ++row) { + double value = b[row]; + for (int column = 0; column < 3; ++column) { + value += a[row][column] * point[column]; + } + grad[row] = halfspace.sign * 2. * value; + } +} + +/// Hand every leaf box whose node box the ray meets within `[0, cap]` to \a visit. +/// \a tmax is re-read at every node test, so the visitor may lower it; with \a nearFirst the nearer child +/// is visited first, and a non-null \a culled collects the nearest entry the lowered bound skipped. +template +void traverseRay(const BVH& bvh, const double* origin, const double* dir, double cap, const double& tmax, + bool nearFirst, double* culled, Visit&& visit) +{ + struct Entry { + size_t node; + double tlo; ///< where the ray enters the node box + }; + // thread_local rather than a member or a fresh vector per call: TGeo shares one shape object + // across every navigator under TGeoManager::SetMaxThreads, and this is not re-entered + thread_local std::vector stack; + stack.clear(); + const auto entersWithin = [&](size_t index, double& tlo) { + tlo = 0.; + double thi = cap; + return nodeWindow(bvh.nodes[index].get_bbox(), origin, dir, tlo, thi); + }; + // a skipped node's own entry is a lower bound on every piece under it + const auto skip = [&](double tlo) { + if (culled != nullptr && tlo < *culled) { + *culled = tlo; + } + }; + double rootTlo = 0.; + if (entersWithin(0, rootTlo)) { + stack.push_back({0, rootTlo}); // the bvh2 root node + } + while (!stack.empty()) { + const Entry entry = stack.back(); + stack.pop_back(); + if (entry.tlo > tmax) { + skip(entry.tlo); // the visitor lowered tmax past this node + continue; + } + const auto& node = bvh.nodes[entry.node]; + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + visit(static_cast(bvh.prim_ids[primitive])); + } + } else { + const auto firstChild = node.index.first_id(); + Entry children[2]; + int count = 0; + for (size_t child : {firstChild, firstChild + 1}) { + double tlo = 0.; + if (child < bvh.nodes.size() && entersWithin(child, tlo)) { + if (tlo > tmax) { + skip(tlo); + } else { + children[count++] = {child, tlo}; + } + } + } + // LIFO: the farther child is pushed first + if (nearFirst && count == 2 && children[0].tlo < children[1].tlo) { + std::swap(children[0], children[1]); + } + for (int index = 0; index < count; ++index) { + stack.push_back(children[index]); + } + } + } +} +/// Hand every leaf box whose node box holds \a point to \a visit, in traversal order, until \a visit +/// returns true; returns whether it did. +template +bool traversePoint(const BVH& bvh, const double* point, Visit&& visit) +{ + const BVHVec3 query(static_cast(point[0]), static_cast(point[1]), + static_cast(point[2])); + thread_local std::vector stack; + stack.clear(); + stack.push_back(0); // the bvh2 root node + while (!stack.empty()) { + const size_t current = stack.back(); + stack.pop_back(); + const auto& node = bvh.nodes[current]; + if (!bvh::v2::extra::contains(node.get_bbox(), query)) { + continue; + } + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + if (visit(static_cast(bvh.prim_ids[primitive]))) { + return true; + } + } + } else { + const auto firstChild = node.index.first_id(); + for (size_t child : {firstChild, firstChild + 1}) { + if (child < bvh.nodes.size()) { + stack.push_back(child); + } + } + } + } + return false; +} + +/// Squared distance from \a point to the box's own double bounds; 0 inside. +inline double boxDistanceSquared(const FlatCSGBox& box, const double* point) +{ + double squared = 0.; + for (int index = 0; index < 3; ++index) { + const double value = point[index]; + if (value < box.min[index]) { + squared += (box.min[index] - value) * (box.min[index] - value); + } else if (value > box.max[index]) { + squared += (value - box.max[index]) * (value - box.max[index]); + } + } + return squared; +} + +/// Distance from \a point, inside the box, to the box's nearest face. +inline double distanceToFaces(const FlatCSGBox& box, const double* point) +{ + double toFace = TGeoShape::Big(); + for (int index = 0; index < 3; ++index) { + toFace = std::min(toFace, std::min(point[index] - box.min[index], box.max[index] - point[index])); + } + return toFace; +} +} // namespace + +O2FlatCSG::O2FlatCSG() : TGeoBBox(0., 0., 0.) {} + +O2FlatCSG::O2FlatCSG(const char* name) : TGeoBBox(name, 0., 0., 0.) {} + +O2FlatCSG::~O2FlatCSG() +{ + delete static_cast(fBVH); + fBVH = nullptr; +} + +size_t O2FlatCSG::GetBVHMemory() const +{ + const auto* bvh = static_cast(fBVH); + if (bvh == nullptr) { + return 0; + } + return bvh->nodes.size() * sizeof(BVHNode) + bvh->prim_ids.size() * sizeof(size_t); +} + +int O2FlatCSG::AddQuadric(double sign, const double coeff[10]) +{ + FlatCSGHalfspace halfspace; + halfspace.kind = FlatCSGHalfspace::kQuadric; + halfspace.sign = sign < 0. ? -1. : 1.; + for (int index = 0; index < 10; ++index) { + halfspace.c[index] = coeff[index]; + } + fHalfspaces.push_back(halfspace); + return static_cast(fHalfspaces.size()) - 1; +} + +int O2FlatCSG::AddTorus(double sign, const double* centre, const double* axis, double major, + double minor) +{ + FlatCSGHalfspace halfspace; + halfspace.kind = FlatCSGHalfspace::kTorus; + halfspace.sign = sign < 0. ? -1. : 1.; + // normalise the axis once here; a zero axis is a caller bug and asserts + const double axisNorm = std::sqrt(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]); + assert(axisNorm > 0. && "O2FlatCSG::AddTorus: axis must not be the zero vector"); + for (int index = 0; index < 3; ++index) { + halfspace.c[index] = centre[index]; + halfspace.c[3 + index] = axis[index] / axisNorm; + } + halfspace.c[6] = major; + halfspace.c[7] = minor; + fHalfspaces.push_back(halfspace); + return static_cast(fHalfspaces.size()) - 1; +} + +int O2FlatCSG::AddCell(int first, int count, double volume) +{ + FlatCSGCell cell; + cell.first = first; + cell.count = count; + cell.volume = volume; + fCells.push_back(cell); + return static_cast(fCells.size()) - 1; +} + +void O2FlatCSG::EnsureCellBBoxStorage() +{ + if (static_cast(fCellBBoxSet.size()) < GetNcells()) { + fCellLo.resize(3 * GetNcells(), 0.); + fCellHi.resize(3 * GetNcells(), 0.); + fCellBBoxSet.resize(GetNcells(), false); + } +} + +void O2FlatCSG::SetCellBBox(int cell, const double* lo, const double* hi) +{ + if (cell < 0 || cell >= GetNcells()) { + // a cell index before its AddCell would write past the end of fCellLo/fCellHi + Error("SetCellBBox", "Shape %s: cell %d is out of range (%d cell(s) so far); ignoring", + GetName(), cell, GetNcells()); + return; + } + EnsureCellBBoxStorage(); + for (int index = 0; index < 3; ++index) { + fCellLo[3 * cell + index] = lo[index]; + fCellHi[3 * cell + index] = hi[index]; + } + fCellBBoxSet[cell] = true; +} + +void O2FlatCSG::GetCellBBox(int cell, double* lo, double* hi) const +{ + const bool set = cell >= 0 && cell < GetNcells() && static_cast(cell) < fCellBBoxSet.size() && + fCellBBoxSet[cell]; + for (int index = 0; index < 3; ++index) { + lo[index] = set ? fCellLo[3 * cell + index] : 0.; + hi[index] = set ? fCellHi[3 * cell + index] : 0.; + } +} + +double O2FlatCSG::EvalHalfspace(const FlatCSGHalfspace& halfspace, const double* point) +{ + if (halfspace.kind == FlatCSGHalfspace::kTorus) { + const double* c = halfspace.c; + const double offset[3] = {point[0] - c[0], point[1] - c[1], point[2] - c[2]}; + const double along = offset[0] * c[3] + offset[1] * c[4] + offset[2] * c[5]; + const double radial[3] = {offset[0] - along * c[3], offset[1] - along * c[4], + offset[2] - along * c[5]}; + const double rho = std::sqrt(radial[0] * radial[0] + radial[1] * radial[1] + + radial[2] * radial[2]); + // the exact signed distance, which is 1-Lipschitz + return halfspace.sign * (std::hypot(rho - c[6], along) - c[7]); + } + const double* c = halfspace.c; + const double x = point[0]; + const double y = point[1]; + const double z = point[2]; + const double quadratic = c[0] * x * x + c[3] * y * y + c[5] * z * z + + 2. * (c[1] * x * y + c[2] * x * z + c[4] * y * z); + const double linear = 2. * (c[6] * x + c[7] * y + c[8] * z); + return halfspace.sign * (quadratic + linear + c[9]); +} + +void O2FlatCSG::HalfspaceRange(const FlatCSGHalfspace& halfspace, const double* lo, + const double* hi, double& rangeLo, double& rangeHi) +{ + // preconditions (see the header): non-negative half-extents and finite bounds + assert(std::isfinite(lo[0]) && std::isfinite(lo[1]) && std::isfinite(lo[2]) && + std::isfinite(hi[0]) && std::isfinite(hi[1]) && std::isfinite(hi[2]) && + lo[0] <= hi[0] && lo[1] <= hi[1] && lo[2] <= hi[2] && + "O2FlatCSG::HalfspaceRange: lo/hi must be finite and lo[i] <= hi[i] on every axis"); + + double centre[3]; + double half[3]; + for (int index = 0; index < 3; ++index) { + centre[index] = 0.5 * (lo[index] + hi[index]); + half[index] = 0.5 * (hi[index] - lo[index]); + } + const double middle = EvalHalfspace(halfspace, centre); + + // Pad by 64 eps times the summed term magnitudes, not |middle|, which cancels on a straddling box. + constexpr double kPadFactor = 64. * std::numeric_limits::epsilon(); + + double halfWidth; + double mag; + if (halfspace.kind == FlatCSGHalfspace::kTorus) { + // the torus's signed distance is 1-Lipschitz, so over the box it deviates by at most |h| + halfWidth = std::sqrt(half[0] * half[0] + half[1] * half[1] + half[2] * half[2]); + const double* c = halfspace.c; + const double offset[3] = {centre[0] - c[0], centre[1] - c[1], centre[2] - c[2]}; + const double along = offset[0] * c[3] + offset[1] * c[4] + offset[2] * c[5]; + const double radial[3] = {offset[0] - along * c[3], offset[1] - along * c[4], + offset[2] - along * c[5]}; + const double rho = std::sqrt(radial[0] * radial[0] + radial[1] * radial[1] + + radial[2] * radial[2]); + // mag needs no term for the centre's scale: near the core circle offset is exact by Sterbenz's lemma + mag = rho + std::abs(c[6]) + std::abs(along) + std::abs(c[7]); + } else { + const double* c = halfspace.c; + const double a[3][3] = {{c[0], c[1], c[2]}, {c[1], c[3], c[4]}, {c[2], c[4], c[5]}}; + const double b[3] = {c[6], c[7], c[8]}; + double slack = 0.; + mag = std::abs(c[9]); + for (int row = 0; row < 3; ++row) { + double gradient = b[row]; + mag += 2. * std::abs(b[row] * centre[row]); + for (int column = 0; column < 3; ++column) { + gradient += a[row][column] * centre[column]; + // sum |A_ij| h_i h_j over-estimates the cross-term deviation only for non-negative half-extents + slack += std::abs(a[row][column]) * half[row] * half[column]; + mag += std::abs(a[row][column] * centre[row] * centre[column]); + } + slack += 2. * std::abs(gradient) * half[row]; + } + // |sign| == 1, so the unsigned slack bounds the signed deviation too + halfWidth = slack; + } + // widen by the pad: the drop tests treat the bound as exact and nActive == 0 is trusted + halfWidth += kPadFactor * mag; + rangeLo = middle - halfWidth; + rangeHi = middle + halfWidth; +} + +bool O2FlatCSG::CellContains(int index, const double* point) const +{ + const FlatCSGCell& cell = fCells[index]; + for (int offset = 0; offset < cell.count; ++offset) { + if (EvalHalfspace(fHalfspaces[cell.first + offset], point) > 0.) { + return false; + } + } + return true; +} + +void O2FlatCSG::SplitBox(int cell, const double* lo, const double* hi, + const std::vector& active, int depth, double minSize, + int cubifyBudget) +{ + std::vector stillActive; + stillActive.reserve(active.size()); + for (int halfspace : active) { + double rangeLo = 0.; + double rangeHi = 0.; + HalfspaceRange(fHalfspaces[halfspace], lo, hi, rangeLo, rangeHi); + if (rangeLo > 0.) { + return; // the box is wholly outside this halfspace, hence wholly outside the cell + } + if (rangeHi > 0.) { + stillActive.push_back(halfspace); // undecided; it stays + } + // rangeHi <= 0: the halfspace holds everywhere in the box, so it is dropped + } + + double longest = 0.; + double shortest = TGeoShape::Big(); + int axis = 0; + for (int index = 0; index < 3; ++index) { + const double extent = hi[index] - lo[index]; + if (extent > longest) { + longest = extent; + axis = index; + } + shortest = std::min(shortest, extent); + } + // a split out of a far-from-cubic box draws on cubifyBudget, not on depth + // `shortest` is floored at minSize so a flat cell does not burn the whole cubifyBudget + const bool farFromCubic = longest > 2. * std::max(shortest, minSize); + const bool keep = stillActive.empty() || depth <= 0 || longest <= minSize || + (farFromCubic && cubifyBudget <= 0); + if (keep) { + FlatCSGBox box; + for (int index = 0; index < 3; ++index) { + box.min[index] = lo[index]; + box.max[index] = hi[index]; + } + box.cell = cell; + box.firstActive = static_cast(fActive.size()); + box.nActive = static_cast(stillActive.size()); + fActive.insert(fActive.end(), stillActive.begin(), stillActive.end()); + fBoxes.push_back(box); + return; + } + + const int childDepth = farFromCubic ? depth : depth - 1; + const int childCubifyBudget = farFromCubic ? cubifyBudget - 1 : cubifyBudget; + const double middle = 0.5 * (lo[axis] + hi[axis]); + double childLo[3] = {lo[0], lo[1], lo[2]}; + double childHi[3] = {hi[0], hi[1], hi[2]}; + childHi[axis] = middle; + SplitBox(cell, childLo, childHi, stillActive, childDepth, minSize, childCubifyBudget); + childHi[axis] = hi[axis]; + childLo[axis] = middle; + SplitBox(cell, childLo, childHi, stillActive, childDepth, minSize, childCubifyBudget); +} + +void O2FlatCSG::CloseShape() +{ + fBoxes.clear(); + fActive.clear(); + fClosed = false; + // dropped before the validation below can return: a BVH left over from an earlier CloseShape + // would describe boxes that no longer exist, and the queries key off `fBVH != nullptr` + delete static_cast(fBVH); + fBVH = nullptr; + + EnsureCellBBoxStorage(); + // Refuse the whole shape when a cell's bbox is missing, inverted or non-finite: a cell without a box would vanish. + bool anyProblem = false; + for (int cell = 0; cell < GetNcells(); ++cell) { + if (!fCellBBoxSet[cell]) { + Error("CloseShape", + "Shape %s cell %d has no bounding box (SetCellBBox was never called for it); it would " + "silently vanish from the solid. Not building any boxes -- IsClosed() stays false.", + GetName(), cell); + anyProblem = true; + continue; + } + for (int index = 0; index < 3; ++index) { + const double loValue = fCellLo[3 * cell + index]; + const double hiValue = fCellHi[3 * cell + index]; + if (!std::isfinite(loValue) || !std::isfinite(hiValue)) { + Error("CloseShape", + "Shape %s cell %d has a non-finite bounding box on axis %d (lo %g, hi %g). Not " + "building any boxes -- IsClosed() stays false.", + GetName(), cell, index, loValue, hiValue); + anyProblem = true; + continue; + } + if (hiValue < loValue) { + Error("CloseShape", + "Shape %s cell %d has an inverted bounding box on axis %d (lo %g > hi %g); " + "SetCellBBox's arguments look swapped. Not building any boxes -- IsClosed() stays " + "false.", + GetName(), cell, index, loValue, hiValue); + anyProblem = true; + } + } + } + if (anyProblem) { + return; + } + + double partLo[3] = {TGeoShape::Big(), TGeoShape::Big(), TGeoShape::Big()}; + double partHi[3] = {-TGeoShape::Big(), -TGeoShape::Big(), -TGeoShape::Big()}; + for (int cell = 0; cell < GetNcells(); ++cell) { + for (int index = 0; index < 3; ++index) { + partLo[index] = std::min(partLo[index], fCellLo[3 * cell + index]); + partHi[index] = std::max(partHi[index], fCellHi[3 * cell + index]); + } + } + const double diagonal = std::sqrt((partHi[0] - partLo[0]) * (partHi[0] - partLo[0]) + + (partHi[1] - partLo[1]) * (partHi[1] - partLo[1]) + + (partHi[2] - partLo[2]) * (partHi[2] - partLo[2])); + const double minSize = fMinBoxFraction * diagonal; + +#ifndef NDEBUG + // A cell must lie inside its bbox; one that spills out makes the accelerated queries and the twins disagree. + { + const double reach = 1.e-6 * (diagonal > 0. ? diagonal : 1.); + for (int cell = 0; cell < GetNcells(); ++cell) { + const double* cellLo = &fCellLo[3 * cell]; + const double* cellHi = &fCellHi[3 * cell]; + for (int axis = 0; axis < 3; ++axis) { + const int first = (axis + 1) % 3; + const int second = (axis + 2) % 3; + for (int side = 0; side < 2; ++side) { + for (int step1 = 0; step1 <= 4; ++step1) { + for (int step2 = 0; step2 <= 4; ++step2) { + double probe[3]; + probe[axis] = side == 0 ? cellLo[axis] - reach : cellHi[axis] + reach; + probe[first] = cellLo[first] + 0.25 * step1 * (cellHi[first] - cellLo[first]); + probe[second] = cellLo[second] + 0.25 * step2 * (cellHi[second] - cellLo[second]); + assert(!CellContains(cell, probe) && + "O2FlatCSG::CloseShape: a cell reaches past the bounding box SetCellBBox was " + "given, so this shape and its own _Loop twins answer differently out there. " + "The converter's box is the CAD piece's own bbox, so the cell is larger than " + "the part: close the cell's halfspaces or refuse the part -- do NOT widen " + "the box, which would ship the phantom material"); + } + } + } + } + } + } +#endif + + for (int cell = 0; cell < GetNcells(); ++cell) { + std::vector active; + active.reserve(fCells[cell].count); + for (int offset = 0; offset < fCells[cell].count; ++offset) { + active.push_back(fCells[cell].first + offset); + } + SplitBox(cell, &fCellLo[3 * cell], &fCellHi[3 * cell], active, fSplitDepth, minSize, + kMaxCubifySplits); + } + + if (!fBoxes.empty()) { + std::vector boxes; + std::vector centers; + boxes.reserve(fBoxes.size()); + centers.reserve(fBoxes.size()); + for (const auto& box : fBoxes) { + BVHBBox bounds; + for (int index = 0; index < 3; ++index) { + // outward, so a float node box is a superset of the double box it stands for and the + // traversal can only ever nominate too many candidates -- never drop one + bounds.min[index] = roundOutward(box.min[index], false); + bounds.max[index] = roundOutward(box.max[index], true); + } + boxes.push_back(bounds); + centers.emplace_back(bounds.get_center()); + } + typename bvh::v2::DefaultBuilder::Config config; + config.quality = bvh::v2::DefaultBuilder::Quality::High; + // One box per leaf: bvh2 enters a leaf without a box test, and each box is visited at most once per traversal. + config.max_leaf_size = 1; + fBVH = static_cast( + new BVH(bvh::v2::DefaultBuilder::build(boxes, centers, config))); + } + + fClosed = true; + ComputeBBox(); +} + +Bool_t O2FlatCSG::Contains_Loop(const Double_t* point) const +{ + for (int index = 0; index < GetNcells(); ++index) { + if (CellContains(index, point)) { + return kTRUE; + } + } + return kFALSE; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Contains -- inside its box a box's active list is the cell, so the point must first be in the box's own bounds. + +Bool_t O2FlatCSG::GetPointsOnSegments(Int_t npoints, Double_t* array) const +{ + if (array == nullptr || npoints <= 0 || !fClosed) { + return kFALSE; + } + // the boxes that carry boundary: those with a non-empty active list + std::vector boundaryBoxes; + for (int index = 0; index < static_cast(fBoxes.size()); ++index) { + if (fBoxes[index].nActive > 0) { + boundaryBoxes.push_back(index); + } + } + if (boundaryBoxes.empty()) { + return kFALSE; + } + // the R2 low-discrepancy pair O2Tessellated uses, mapped to directions on the unit sphere + constexpr double kAlpha1 = 0.7548776662466927; + constexpr double kAlpha2 = 0.5698402909980532; + constexpr double kFlipProbe = 1.e-6; ///< cm either side of a point at which Contains must change + const double zAxis[3] = {0., 0., 1.}; + std::vector pairs; + int produced = 0; + const long long maxAttempts = 64LL * npoints; + for (long long attempt = 0; attempt < maxAttempts && produced < npoints; ++attempt) { + const FlatCSGBox& box = fBoxes[boundaryBoxes[attempt % static_cast(boundaryBoxes.size())]]; + const double u = std::fmod(0.5 + kAlpha1 * static_cast(attempt + 1), 1.); + const double v = std::fmod(0.5 + kAlpha2 * static_cast(attempt + 1), 1.); + const double cosTheta = 1. - 2. * u; + const double sinTheta = std::sqrt(std::max(0., 1. - cosTheta * cosTheta)); + const double phi = o2::cad::surface::kTwoPi * v; + const double dir[3] = {sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta}; + const double centre[3] = {0.5 * (box.min[0] + box.max[0]), 0.5 * (box.min[1] + box.max[1]), + 0.5 * (box.min[2] + box.max[2])}; + double tlo = 0.; + double thi = TGeoShape::Big(); + if (!slabWindow(box.min, box.max, centre, dir, tlo, thi)) { + continue; + } + const int capacity = maxPairsForCell(box.nActive); + pairs.resize(2 * static_cast(capacity)); + const int found = CellIntervals(box.cell, fActive.data() + box.firstActive, box.nActive, centre, dir, tlo, thi, + pairs.data(), capacity); + // the first crossing of the cell's surface inside the box; a window end is a box face, not surface + double crossing = -1.; + for (int pair = 0; pair < found && crossing < 0.; ++pair) { + if (pairs[2 * pair] > tlo) { + crossing = pairs[2 * pair]; + } else if (pairs[2 * pair + 1] < thi) { + crossing = pairs[2 * pair + 1]; + } + } + if (crossing < 0.) { + continue; + } + double* slot = &array[3 * static_cast(produced)]; + for (int axis = 0; axis < 3; ++axis) { + slot[axis] = centre[axis] + crossing * dir[axis]; + } + // a face between two cells is not boundary of the union: keep only points where containment flips + double normal[3] = {0., 0., 0.}; + ComputeNormal(slot, zAxis, normal); + double below[3]; + double above[3]; + for (int axis = 0; axis < 3; ++axis) { + below[axis] = slot[axis] - kFlipProbe * normal[axis]; + above[axis] = slot[axis] + kFlipProbe * normal[axis]; + } + if (Contains(below) != Contains(above)) { + ++produced; + } + } + return produced == npoints ? kTRUE : kFALSE; +} + +Bool_t O2FlatCSG::Contains(const Double_t* point) const +{ + if (!fClosed || fBVH == nullptr) { + // no boxes to walk: answer from the twin rather than report no material + return Contains_Loop(point); + } + const bool inside = traversePoint(*static_cast(fBVH), point, [&](int index) { + const FlatCSGBox& box = fBoxes[index]; + if (!boxHoldsPoint(box, point)) { + return false; + } + if (box.nActive == 0) { + return true; // wholly inside its cell: nothing left to test + } + bool inCell = true; + for (int slot = 0; slot < box.nActive && inCell; ++slot) { + inCell = EvalHalfspace(fHalfspaces[fActive[box.firstActive + slot]], point) <= 0.; + } + return inCell; + }); + return inside ? kTRUE : kFALSE; +} + +int O2FlatCSG::HalfspaceRoots(const FlatCSGHalfspace& halfspace, const double* origin, + const double* dir, double* roots) +{ + if (halfspace.kind == FlatCSGHalfspace::kTorus) { + // the quartic derivation below takes the leading coefficient a4 = |dir|^4 to be exactly 1; + // a non-unit direction silently returns wrong roots instead of failing, so catch it here + assert(std::abs(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2] - 1.) < 1.e-9 && + "O2FlatCSG::HalfspaceRoots: torus branch requires a unit direction"); + const double* c = halfspace.c; + const double axis[3] = {c[3], c[4], c[5]}; + const double major = c[6]; + const double minor = c[7]; + const double offset[3] = {origin[0] - c[0], origin[1] - c[1], origin[2] - c[2]}; + // components along the axis, and the perpendicular parts + const double pz = offset[0] * axis[0] + offset[1] * axis[1] + offset[2] * axis[2]; + const double dz = dir[0] * axis[0] + dir[1] * axis[1] + dir[2] * axis[2]; + double pPerp[3]; + double dPerp[3]; + for (int index = 0; index < 3; ++index) { + pPerp[index] = offset[index] - pz * axis[index]; + dPerp[index] = dir[index] - dz * axis[index]; + } + const double pp = pPerp[0] * pPerp[0] + pPerp[1] * pPerp[1] + pPerp[2] * pPerp[2]; + const double dd = dPerp[0] * dPerp[0] + dPerp[1] * dPerp[1] + dPerp[2] * dPerp[2]; + const double pd = pPerp[0] * dPerp[0] + pPerp[1] * dPerp[1] + pPerp[2] * dPerp[2]; + // (|X|^2 + R^2 - r^2)^2 - 4 R^2 (X_perp . X_perp) = 0 with X = P + tD, |D| = 1 + const double e = pp + pz * pz + major * major - minor * minor; + const double f = pd + pz * dz; + const double a4 = 1.; + const double a3 = 4. * f; + const double a2 = 2. * e + 4. * f * f - 4. * major * major * dd; + const double a1 = 4. * e * f - 8. * major * major * pd; + const double a0 = e * e - 4. * major * major * pp; + // solveQuarticReal is scale-normalised, so the torus needs no degeneracy guard + const auto found = o2::cad::surface::solveQuarticReal(a4, a3, a2, a1, a0); + int count = 0; + for (double value : found) { + if (count < kMaxRootsPerHalfspace) { + roots[count++] = value; + } + } + return count; + } + const double* c = halfspace.c; + // A d + const double ad[3] = {c[0] * dir[0] + c[1] * dir[1] + c[2] * dir[2], + c[1] * dir[0] + c[3] * dir[1] + c[4] * dir[2], + c[2] * dir[0] + c[4] * dir[1] + c[5] * dir[2]}; + // A o + b + const double aob[3] = {c[0] * origin[0] + c[1] * origin[1] + c[2] * origin[2] + c[6], + c[1] * origin[0] + c[3] * origin[1] + c[4] * origin[2] + c[7], + c[2] * origin[0] + c[4] * origin[1] + c[5] * origin[2] + c[8]}; + const double alpha = dir[0] * ad[0] + dir[1] * ad[1] + dir[2] * ad[2]; + const double beta = dir[0] * aob[0] + dir[1] * aob[1] + dir[2] * aob[2]; + const double gamma = EvalHalfspace(halfspace, origin) * halfspace.sign; // sign*sign==1: the unsigned Q(o) + + // a plane has alpha exactly 0 and an axis-parallel ray nearly so: both are linear equations + // 1e-14 is cm-dependent: a root it discards lies at |t| >= ~1e6 cm, outside any ALICE geometry + const double reference = std::abs(beta) + std::abs(gamma) + 1.e-300; + if (std::abs(alpha) <= 1.e-14 * reference) { + if (std::abs(beta) <= 1.e-300) { + return 0; + } + roots[0] = -0.5 * gamma / beta; + return 1; + } + const double disc = beta * beta - alpha * gamma; + if (disc < 0.) { + return 0; + } + const double root = std::sqrt(disc); + // the numerically stable pair, so a grazing ray does not lose the near root to cancellation + const double q = -(beta + (beta >= 0. ? root : -root)); + if (q == 0.) { + // q == 0 only when beta == gamma == 0: one double root at t = 0, without the 0/0 of the general formula + roots[0] = 0.; + return 1; + } + roots[0] = q / alpha; + roots[1] = gamma / q; + return 2; +} + +int O2FlatCSG::CellIntervals(int cell, const int* active, int nActive, const double* origin, + const double* dir, double tlo, double thi, double* out, + int maxOut) const +{ + const FlatCSGCell& description = fCells[cell]; + const int count = nActive < 0 ? description.count : nActive; + if (thi <= tlo) { + return 0; + } + + // every root of every active halfspace in the window; thread_local, sized from the cell's halfspace count + thread_local std::vector breakBuffer; + const std::size_t needed = 2 + static_cast(kMaxRootsPerHalfspace) * static_cast(count); + if (breakBuffer.size() < needed) { + breakBuffer.resize(needed); + } + double* breaks = breakBuffer.data(); + int nBreaks = 0; + breaks[nBreaks++] = tlo; + breaks[nBreaks++] = thi; + for (int slot = 0; slot < count; ++slot) { + const int index = active != nullptr ? active[slot] : description.first + slot; + double roots[kMaxRootsPerHalfspace]; + const int found = HalfspaceRoots(fHalfspaces[index], origin, dir, roots); + for (int root = 0; root < found; ++root) { + if (roots[root] > tlo && roots[root] < thi) { + breaks[nBreaks++] = roots[root]; + } + } + } + std::sort(breaks, breaks + nBreaks); + + // classify the midpoint of each sub-interval and merge the runs that are inside + int pairs = 0; + bool open = false; + bool overflow = false; + for (int index = 0; index + 1 < nBreaks; ++index) { + const double lo = breaks[index]; + const double hi = breaks[index + 1]; + if (hi <= lo) { + continue; + } + const double middle = 0.5 * (lo + hi); + double probe[3] = {origin[0] + middle * dir[0], origin[1] + middle * dir[1], + origin[2] + middle * dir[2]}; + bool inside = true; + for (int slot = 0; slot < count && inside; ++slot) { + const int halfspace = active != nullptr ? active[slot] : description.first + slot; + inside = EvalHalfspace(fHalfspaces[halfspace], probe) <= 0.; + } + if (inside) { + if (open) { + out[2 * (pairs - 1) + 1] = hi; + } else if (pairs < maxOut) { + out[2 * pairs] = lo; + out[2 * pairs + 1] = hi; + ++pairs; + open = true; + } else { + // maxOut was too small for this cell along this ray: fail loudly (a negative count) + // rather than hand the caller a silently truncated list that reads as a valid answer + overflow = true; + open = false; + } + } else { + open = false; + } + } + return overflow ? -1 : pairs; +} + +namespace +{ +/// Merge `[enter, exit]` pairs in place, joining ones that touch within \a glue. +int mergeIntervals(double* pairs, int count, double glue) +{ + if (count < 2) { + return count; + } + // sort by entry + for (int outer = 1; outer < count; ++outer) { + const double lo = pairs[2 * outer]; + const double hi = pairs[2 * outer + 1]; + int inner = outer - 1; + while (inner >= 0 && pairs[2 * inner] > lo) { + pairs[2 * (inner + 1)] = pairs[2 * inner]; + pairs[2 * (inner + 1) + 1] = pairs[2 * inner + 1]; + --inner; + } + pairs[2 * (inner + 1)] = lo; + pairs[2 * (inner + 1) + 1] = hi; + } + int kept = 1; + for (int index = 1; index < count; ++index) { + if (pairs[2 * index] <= pairs[2 * (kept - 1) + 1] + glue) { + pairs[2 * (kept - 1) + 1] = std::max(pairs[2 * (kept - 1) + 1], pairs[2 * index + 1]); + } else { + pairs[2 * kept] = pairs[2 * index]; + pairs[2 * kept + 1] = pairs[2 * index + 1]; + ++kept; + } + } + return kept; +} +} // namespace + +Double_t O2FlatCSG::DistFromOutside_Loop(const Double_t* point, const Double_t* dir, + Double_t step) const +{ + // thread_local: see the comment on the scratch-buffer members it replaced in the header + thread_local std::vector pairBuffer; + double best = TGeoShape::Big(); + for (int cell = 0; cell < GetNcells(); ++cell) { + // sized from this cell's own halfspace count, so a busy cell's intervals are never truncated + const int capacity = maxPairsForCell(fCells[cell].count); + if (static_cast(pairBuffer.size()) < 2 * capacity) { + pairBuffer.resize(2 * capacity); + } + const int found = CellIntervals(cell, nullptr, -1, point, dir, 0., step, + pairBuffer.data(), capacity); + // capacity is provably sufficient (maxPairsForCell), so CellIntervals cannot overflow here; + // a negative found would mean that bound itself is wrong, which is a bug, not live data + for (int pair = 0; pair < found; ++pair) { + // a point exactly on the boundary is already inside; only a real entry counts + if (pairBuffer[2 * pair + 1] > TGeoShape::Tolerance() && pairBuffer[2 * pair] < best) { + best = std::max(pairBuffer[2 * pair], 0.); + } + } + } + return best; +} + +Double_t O2FlatCSG::DistFromInside_Loop(const Double_t* point, const Double_t* dir, + Double_t step) const +{ + // the union's occupancy; the buffer fits every cell's worst case at once + thread_local std::vector pairBuffer; + int totalCapacity = 0; + for (int cell = 0; cell < GetNcells(); ++cell) { + totalCapacity += maxPairsForCell(fCells[cell].count); + } + if (static_cast(pairBuffer.size()) < 2 * totalCapacity) { + pairBuffer.resize(2 * totalCapacity); + } + int count = 0; + for (int cell = 0; cell < GetNcells(); ++cell) { + // not expected to overflow, but a negative count must never reach the pointer arithmetic + const int found = CellIntervals(cell, nullptr, -1, point, dir, 0., step, + pairBuffer.data() + 2 * count, totalCapacity - count); + if (found < 0) { + Error("DistFromInside_Loop", + "CellIntervals overflowed for cell %d: the maxPairsForCell bound no longer holds", + cell); + return TGeoShape::Big(); + } + count += found; + } + count = mergeIntervals(pairBuffer.data(), count, TGeoShape::Tolerance()); + for (int pair = 0; pair < count; ++pair) { + if (pairBuffer[2 * pair] <= TGeoShape::Tolerance()) { + return pairBuffer[2 * pair + 1]; + } + } + return 0.; +} + +//////////////////////////////////////////////////////////////////////////////// +/// GatherRayPieces -- each box's window is its own slab intersected with `[0, step]`, never pooled across boxes. + +bool O2FlatCSG::GatherRayPieces(const Double_t* point, const Double_t* dir, Double_t step, + std::vector& pairs, std::vector& cells, RayBound bound, + double& smallestPruned) const +{ + pairs.clear(); + cells.clear(); + smallestPruned = TGeoShape::Big(); + const BVH& bvh = *static_cast(fBVH); + + // one box's intervals; thread_local for the reason the header's scratch-buffer comment gives + thread_local std::vector boxPairs; + bool overflowed = false; + // the running bound: with kEntry an upper bound on DistFromOutside's answer, with kExit the far + // end of the interval holding t = 0; a box entered past it cannot change the answer + double limit = step; + double reach = -1.; // kExit's chain end, negative until a piece holds t = 0 + // only kExit can prune a box that later turns out to matter, so only it needs the record + double* culled = bound == RayBound::kExit ? &smallestPruned : nullptr; + traverseRay(bvh, point, dir, step, limit, bound != RayBound::kNone, culled, [&](int index) { + const FlatCSGBox& box = fBoxes[index]; + double tlo = 0.; + double thi = step; + if (!slabWindow(box.min, box.max, point, dir, tlo, thi) || thi <= tlo) { + return; + } + if (tlo > limit) { + if (culled != nullptr && tlo < smallestPruned) { + smallestPruned = tlo; + } + return; + } + // sized from THIS box's active-list length, which is the count CellIntervals will walk, so + // the bound it is asked to respect is the one it was given + const int capacity = maxPairsForCell(box.nActive); + if (static_cast(boxPairs.size()) < 2 * capacity) { + boxPairs.resize(2 * capacity); + } + // nActive == 0 means the box is wholly inside its cell; CellIntervals then has no halfspace + // to break on and returns the whole window, which is exactly the right answer + const int* active = box.nActive > 0 ? fActive.data() + box.firstActive : nullptr; + const int found = CellIntervals(box.cell, active, box.nActive, point, dir, tlo, thi, + boxPairs.data(), capacity); + if (found < 0) { + overflowed = true; + return; + } + for (int pair = 0; pair < found; ++pair) { + const double enter = boxPairs[2 * pair]; + const double exit = boxPairs[2 * pair + 1]; + pairs.push_back(enter); + pairs.push_back(exit); + cells.push_back(box.cell); + if (bound == RayBound::kEntry && exit > TGeoShape::Tolerance()) { + limit = std::min(limit, std::max({enter, 0., TGeoShape::Tolerance()})); + } else if (bound == RayBound::kExit && + (reach < 0. ? enter <= TGeoShape::Tolerance() : enter <= reach + TGeoShape::Tolerance())) { + // the chain of pieces holding t = 0, joined with DistFromInside's own merge glue + reach = std::max(reach, exit); + limit = std::min(step, reach + TGeoShape::Tolerance()); + } + } + }); + return !overflowed; +} + +//////////////////////////////////////////////////////////////////////////////// +/// DistFromOutsideBVH -- the pieces are rejoined per cell, never across cells, as the twin's per-cell intervals. + +Double_t O2FlatCSG::DistFromOutsideBVH(const Double_t* point, const Double_t* dir, + Double_t step) const +{ + thread_local std::vector pairs; + thread_local std::vector cells; + double smallestPruned = TGeoShape::Big(); + if (!GatherRayPieces(point, dir, step, pairs, cells, RayBound::kEntry, smallestPruned)) { + Error("DistFromOutside", + "Shape %s: CellIntervals overflowed a per-box buffer sized from that box's own active " + "list; the maxPairsForCell bound no longer holds. Answering from the loop twin.", + GetName()); + return DistFromOutside_Loop(point, dir, step); + } + + // sort the pieces by (cell, entry) through a permutation, so the run merge below sees each + // cell's pieces contiguously and in order + const int count = static_cast(cells.size()); + thread_local std::vector order; + order.resize(count); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](int left, int right) { + if (cells[left] != cells[right]) { + return cells[left] < cells[right]; + } + return pairs[2 * left] < pairs[2 * right]; + }); + + double best = TGeoShape::Big(); + int index = 0; + while (index < count) { + const int cell = cells[order[index]]; + const double enter = pairs[2 * order[index]]; + double exit = pairs[2 * order[index] + 1]; + ++index; + // join what is only one interval of this cell, cut into pieces by the boxes that tile it + while (index < count && cells[order[index]] == cell && pairs[2 * order[index]] <= exit) { + exit = std::max(exit, pairs[2 * order[index] + 1]); + ++index; + } + // DistFromOutside_Loop's rule, unchanged: a point exactly on the boundary is already inside, + // so only an interval that really extends past the tolerance counts as an entry + if (exit > TGeoShape::Tolerance() && enter < best) { + best = std::max(enter, 0.); + } + } + return best; +} + +//////////////////////////////////////////////////////////////////////////////// +/// DistFromInsideBVH -- the far end of the union's interval containing t = 0, merged across cells with the twin's glue. + +Double_t O2FlatCSG::DistFromInsideBVH(const Double_t* point, const Double_t* dir, + Double_t step) const +{ + thread_local std::vector pairs; + thread_local std::vector cells; + for (int attempt = 0; attempt < 2; ++attempt) { + // the bound grows as pieces merge, so a box skipped against an earlier, smaller one might have + // mattered after all; the second attempt does not prune and is the definition of the answer + const RayBound bound = attempt == 0 ? RayBound::kExit : RayBound::kNone; + double smallestPruned = TGeoShape::Big(); + if (!GatherRayPieces(point, dir, step, pairs, cells, bound, smallestPruned)) { + Error("DistFromInside", + "Shape %s: CellIntervals overflowed a per-box buffer sized from that box's own active " + "list; the maxPairsForCell bound no longer holds. Answering from the loop twin.", + GetName()); + return DistFromInside_Loop(point, dir, step); + } + const int count = mergeIntervals(pairs.data(), static_cast(cells.size()), + TGeoShape::Tolerance()); + double answer = 0.; + for (int pair = 0; pair < count; ++pair) { + if (pairs[2 * pair] <= TGeoShape::Tolerance()) { + answer = pairs[2 * pair + 1]; + break; + } + } + if (attempt == 1 || smallestPruned > answer + TGeoShape::Tolerance()) { + return answer; + } + ++gUnprunedRetryCount; + } + return 0.; // unreachable: the second attempt never prunes +} + +void O2FlatCSG::ResetUnprunedRetryCounter() +{ + gUnprunedRetryCount = 0; +} + +long long O2FlatCSG::GetUnprunedRetryCount() +{ + return gUnprunedRetryCount; +} + +Double_t O2FlatCSG::DistFromOutside(const Double_t* point, const Double_t* dir, Int_t iact, + Double_t step, Double_t* safe) const +{ + if (iact < 3 && safe != nullptr) { + *safe = Safety(point, kFALSE); + if (iact == 0) { + return TGeoShape::Big(); + } + if (iact == 1 && step < *safe) { + return TGeoShape::Big(); + } + } + if (!fClosed || fBVH == nullptr) { + // no boxes to walk: see the note on Contains. The twin is the definition of the answer, and + // an empty box array in the accelerated path would silently report empty space. + return DistFromOutside_Loop(point, dir, step); + } + return DistFromOutsideBVH(point, dir, step); +} + +Double_t O2FlatCSG::DistFromInside(const Double_t* point, const Double_t* dir, Int_t iact, + Double_t step, Double_t* safe) const +{ + if (iact < 3 && safe != nullptr) { + *safe = Safety(point, kTRUE); + if (iact == 0) { + return TGeoShape::Big(); + } + if (iact == 1 && step < *safe) { + return TGeoShape::Big(); + } + } + if (!fClosed || fBVH == nullptr) { + return DistFromInside_Loop(point, dir, step); + } + return DistFromInsideBVH(point, dir, step); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Safety_Loop -- outside the distance to the nearest box; inside the distance to the faces of a wholly-inside box, else 0. + +Double_t O2FlatCSG::Safety_Loop(const Double_t* point, Bool_t in) const +{ + if (!in) { + double best = TGeoShape::Big(); + for (const auto& box : fBoxes) { + best = std::min(best, boxDistanceSquared(box, point)); + } + return best >= TGeoShape::Big() ? 0. : std::sqrt(best); + } + + double best = 0.; + for (const auto& box : fBoxes) { + if (boxHoldsPoint(box, point) && box.nActive == 0) { + best = std::max(best, distanceToFaces(box, point)); + } + } + return std::max(best, 0.); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Safety -- Safety_Loop's computation through the BVH; the pruning never drops the nearest box. + +Double_t O2FlatCSG::Safety(const Double_t* point, Bool_t in) const +{ + if (!fClosed || fBVH == nullptr) { + // no boxes to walk: see the note on Contains -- the twin is the definition of the answer. + return Safety_Loop(point, in); + } + const BVH& bvh = *static_cast(fBVH); + + if (!in) { + // node boxes are read back as double and measured against the double point: a float query could prune the nearest box + using DVec3 = bvh::v2::Vec; + using DBBox = bvh::v2::BBox; + const DVec3 dpoint(point[0], point[1], point[2]); + const auto nodeDistanceSquared = [&bvh, &dpoint](size_t index) { + const auto& fbox = bvh.nodes[index].get_bbox(); + const DBBox dbox(DVec3(static_cast(fbox.min[0]), static_cast(fbox.min[1]), + static_cast(fbox.min[2])), + DVec3(static_cast(fbox.max[0]), static_cast(fbox.max[1]), + static_cast(fbox.max[2]))); + return bvh::v2::extra::SafetySqToNode(dbox, dpoint); + }; + struct NodeEntry { + size_t node; + double squared; ///< the node box's squared distance, computed once when pushed + }; + thread_local std::vector nearStack; + nearStack.clear(); + nearStack.push_back({0, nodeDistanceSquared(0)}); // the bvh2 root node + double best = TGeoShape::Big(); + while (!nearStack.empty()) { + const NodeEntry entry = nearStack.back(); + nearStack.pop_back(); + const auto& node = bvh.nodes[entry.node]; + if (entry.squared >= best) { + continue; // this subtree cannot hold anything nearer than what is already found + } + if (node.is_leaf()) { + const auto beginPrimitive = node.index.first_id(); + const auto endPrimitive = beginPrimitive + node.index.prim_count(); + for (auto primitive = beginPrimitive; primitive < endPrimitive; ++primitive) { + best = std::min(best, boxDistanceSquared(fBoxes[bvh.prim_ids[primitive]], point)); + } + } else { + // nearer child first, pruning on the way in; the same min in another order + const auto firstChild = node.index.first_id(); + size_t children[2] = {firstChild, firstChild + 1}; + double childSquared[2] = {TGeoShape::Big(), TGeoShape::Big()}; + for (int index = 0; index < 2; ++index) { + if (children[index] < bvh.nodes.size()) { + childSquared[index] = nodeDistanceSquared(children[index]); + } + } + const int nearer = childSquared[0] <= childSquared[1] ? 0 : 1; + const int farther = 1 - nearer; + // LIFO, so the farther child is pushed first and popped last. + if (children[farther] < bvh.nodes.size() && childSquared[farther] < best) { + nearStack.push_back({children[farther], childSquared[farther]}); + } + if (children[nearer] < bvh.nodes.size() && childSquared[nearer] < best) { + nearStack.push_back({children[nearer], childSquared[nearer]}); + } + } + } + return best >= TGeoShape::Big() ? 0. : std::sqrt(best); + } + + double best = 0.; + traversePoint(bvh, point, [&](int index) { + const FlatCSGBox& box = fBoxes[index]; + if (boxHoldsPoint(box, point) && box.nActive == 0) { + best = std::max(best, distanceToFaces(box, point)); + } + return false; + }); + return std::max(best, 0.); +} + +Double_t O2FlatCSG::Capacity() const +{ + // the cells of a decomposition are disjoint by construction, so their own volumes just sum + return std::accumulate(fCells.begin(), fCells.end(), 0., + [](double sum, const FlatCSGCell& cell) { return sum + cell.volume; }); +} + +//////////////////////////////////////////////////////////////////////////////// +/// ComputeNormal -- the halfspace with the smallest first-order distance |f| / |grad f| among the active list of the box +/// holding \a point, which HalfspaceRange's 64-eps pad makes every halfspace that can be at equality there. + +void O2FlatCSG::ComputeNormal(const Double_t* point, const Double_t* dir, Double_t* norm) const +{ + norm[0] = norm[1] = norm[2] = 0.; + if (fHalfspaces.empty()) { + return; + } + + // the candidates: the active list of the box that holds the point; for a box wholly inside its + // cell, that cell's halfspace run; and when no box holds the point, every halfspace + const int* activeList = nullptr; + int rangeFirst = 0; + int nCandidates = GetNhalfspaces(); + if (fClosed && fBVH != nullptr) { + traversePoint(*static_cast(fBVH), point, [&](int index) { + const FlatCSGBox& box = fBoxes[index]; + if (!boxHoldsPoint(box, point)) { + return false; + } + if (box.nActive > 0) { + activeList = fActive.data() + box.firstActive; + nCandidates = box.nActive; + } else { + rangeFirst = fCells[box.cell].first; + nCandidates = fCells[box.cell].count; + } + return true; // cells are disjoint; the first box that holds the point is the answer + }); + } + const auto indexAt = [&](int slot) { return activeList != nullptr ? activeList[slot] : rangeFirst + slot; }; + + int best = -1; + double bestValue = std::numeric_limits::infinity(); + double bestGrad[3] = {0., 0., 0.}; + for (int slot = 0; slot < nCandidates; ++slot) { + const int candidate = indexAt(slot); + const FlatCSGHalfspace& halfspace = fHalfspaces[candidate]; + const double f = EvalHalfspace(halfspace, point); + double grad[3]; + halfspaceGradient(halfspace, point, grad); + const double gradLength = std::sqrt(grad[0] * grad[0] + grad[1] * grad[1] + grad[2] * grad[2]); + if (gradLength < 1.e-300) { + continue; // degenerate gradient (see halfspaceGradient); this halfspace cannot win + } + const double value = std::abs(f) / gradLength; // the first-order distance to this surface + if (value < bestValue) { + bestValue = value; + best = candidate; + bestGrad[0] = grad[0] / gradLength; + bestGrad[1] = grad[1] / gradLength; + bestGrad[2] = grad[2] / gradLength; + } + } + + if (best < 0) { + // every candidate's gradient was degenerate (a torus axis or core circle): fall back to the travel direction + const double dirLength = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + if (dirLength > 1.e-300) { + for (int index = 0; index < 3; ++index) { + norm[index] = dir[index] / dirLength; + } + } + return; + } + + for (int index = 0; index < 3; ++index) { + norm[index] = bestGrad[index]; + } + const double dot = norm[0] * dir[0] + norm[1] * dir[1] + norm[2] * dir[2]; + if (dot < 0.) { + for (int index = 0; index < 3; ++index) { + norm[index] = -norm[index]; + } + } +} + +void O2FlatCSG::ComputeBBox() +{ + // the union of the retained sub-cell boxes, tighter than the union of the cell AABBs + if (fBoxes.empty()) { + return; + } + double lo[3] = {TGeoShape::Big(), TGeoShape::Big(), TGeoShape::Big()}; + double hi[3] = {-TGeoShape::Big(), -TGeoShape::Big(), -TGeoShape::Big()}; + for (const FlatCSGBox& box : fBoxes) { + for (int index = 0; index < 3; ++index) { + lo[index] = std::min(lo[index], box.min[index]); + hi[index] = std::max(hi[index], box.max[index]); + } + } + for (int index = 0; index < 3; ++index) { + fOrigin[index] = 0.5 * (lo[index] + hi[index]); + } + fDX = 0.5 * (hi[0] - lo[0]); + fDY = 0.5 * (hi[1] - lo[1]); + fDZ = 0.5 * (hi[2] - lo[2]); +} + +} // namespace cad +} // namespace o2 diff --git a/Detectors/CADSupport/src/O2OverlapCheck.cxx b/Detectors/CADSupport/src/O2OverlapCheck.cxx new file mode 100644 index 0000000000000..1d04e9a0cad55 --- /dev/null +++ b/Detectors/CADSupport/src/O2OverlapCheck.cxx @@ -0,0 +1,483 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +/// \file O2OverlapCheck.cxx +/// \brief An overlap census that asks the shapes: every sampled point is verified to lie on its solid's boundary, +/// and the depth, not containment, separates touching from interpenetrating pairs. + +#include "CADSupport/O2OverlapCheck.h" +#include "CADSupport/O2FlatCSG.h" + +#include "TGeoShape.h" +#include "TGeoBBox.h" +#include "TGeoMatrix.h" +#include "TGeoVolume.h" +#include "TGeoNode.h" + +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace cad +{ + +const char* OverlapVerdictName(OverlapVerdict verdict) +{ + switch (verdict) { + case OverlapVerdict::Disjoint: + return "disjoint"; + case OverlapVerdict::Touching: + return "touching"; + case OverlapVerdict::Interpenetrating: + return "INTERPENETRATING"; + case OverlapVerdict::Contained: + return "CONTAINED"; + } + return "unknown"; +} + +namespace +{ + +/// The master-frame axis-aligned box of a shape's local bounding box under \a matrix, inflated by +/// \a pad. Conservative for a rotation because it takes the box of the eight transformed corners. +struct MasterBox { + double lower[3] = {0., 0., 0.}; + double upper[3] = {0., 0., 0.}; + bool valid = false; +}; + +MasterBox masterBox(const TGeoShape* shape, const TGeoMatrix* matrix, double pad) +{ + MasterBox box; + const auto* boundingBox = dynamic_cast(shape); + if (boundingBox == nullptr) { + return box; + } + const double* origin = boundingBox->GetOrigin(); + const double halfLengths[3] = {boundingBox->GetDX(), boundingBox->GetDY(), boundingBox->GetDZ()}; + for (int dimension = 0; dimension < 3; ++dimension) { + box.lower[dimension] = std::numeric_limits::max(); + box.upper[dimension] = -std::numeric_limits::max(); + } + for (int corner = 0; corner < 8; ++corner) { + const double local[3] = {origin[0] + ((corner & 1) ? halfLengths[0] : -halfLengths[0]), + origin[1] + ((corner & 2) ? halfLengths[1] : -halfLengths[1]), + origin[2] + ((corner & 4) ? halfLengths[2] : -halfLengths[2])}; + double master[3] = {0., 0., 0.}; + matrix->LocalToMaster(local, master); + for (int dimension = 0; dimension < 3; ++dimension) { + box.lower[dimension] = std::min(box.lower[dimension], master[dimension]); + box.upper[dimension] = std::max(box.upper[dimension], master[dimension]); + } + } + for (int dimension = 0; dimension < 3; ++dimension) { + box.lower[dimension] -= pad; + box.upper[dimension] += pad; + } + box.valid = true; + return box; +} + +bool boxesOverlap(const MasterBox& first, const MasterBox& second) +{ + if (!first.valid || !second.valid) { + return true; // no box means no rejection; test the pair + } + for (int dimension = 0; dimension < 3; ++dimension) { + if (first.upper[dimension] < second.lower[dimension] || second.upper[dimension] < first.lower[dimension]) { + return false; + } + } + return true; +} + +/// Radical-inverse (Halton) coordinate; deterministic, so two runs differ only if the geometry does. +inline double halton(unsigned int index, unsigned int base) +{ + double result = 0.; + double fraction = 1.; + while (index > 0) { + fraction /= base; + result += fraction * (index % base); + index /= base; + } + return result; +} + +/// Whether Contains() changes across \a point, probed \a eps either side along the shape's normal, +/// then along each axis when the normal probe does not flip. +bool containmentFlips(const TGeoShape* shape, const double* point, double eps) +{ + const auto flipsAlong = [&](const double* direction) { + double below[3]; + double above[3]; + for (int axis = 0; axis < 3; ++axis) { + below[axis] = point[axis] - eps * direction[axis]; + above[axis] = point[axis] + eps * direction[axis]; + } + return shape->Contains(below) != shape->Contains(above); + }; + const double zAxis[3] = {0., 0., 1.}; + double normal[3] = {0., 0., 0.}; + shape->ComputeNormal(point, zAxis, normal); + const double length = std::sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); + if (std::isfinite(length) && length > 0.5 && flipsAlong(normal)) { + return true; + } + for (int axis = 0; axis < 3; ++axis) { + double direction[3] = {0., 0., 0.}; + direction[axis] = 1.; + if (flipsAlong(direction)) { + return true; + } + } + return false; +} + +} // namespace + +int SampleBoundaryPoints(const TGeoShape* shape, int npoints, double residualTolerance, + std::vector& points, int& rejected, double& worstResidual, + bool* usedPointsOnSegments) +{ + points.clear(); + rejected = 0; + worstResidual = 0.; + if (usedPointsOnSegments != nullptr) { + *usedPointsOnSegments = false; + } + if (shape == nullptr || npoints <= 0) { + return 0; + } + + int meshVertices = 0; + int meshSegments = 0; + int meshPolygons = 0; + shape->GetMeshNumbers(meshVertices, meshSegments, meshPolygons); + + // TGeoChecker::MakeCheckOverlap's choice: a shape that declines to sample still has display vertices + const int capacity = std::max(npoints, meshVertices); + std::vector raw(3 * static_cast(std::max(capacity, 1)), 0.); + int rawCount = 0; + if (shape->GetPointsOnSegments(npoints, raw.data())) { + rawCount = npoints; + if (usedPointsOnSegments != nullptr) { + *usedPointsOnSegments = true; + } + } else { + if (meshVertices <= 0) { + return 0; + } + shape->SetPoints(raw.data()); + rawCount = meshVertices; + } + + // O2FlatCSG returns Safety 0 inside undecided boxes, so only its points must also flip containment + const bool flatCSG = dynamic_cast(shape) != nullptr; + points.reserve(3 * static_cast(rawCount)); + for (int index = 0; index < rawCount; ++index) { + const double* candidate = &raw[3 * static_cast(index)]; + // Safety() is a lower bound on the distance to the boundary, so a large value is a proof that + // the point is *not* on it. That is the direction this filter needs. + const double residual = shape->Safety(candidate, shape->Contains(candidate)); + if (!(residual <= residualTolerance) || (flatCSG && !containmentFlips(shape, candidate, residualTolerance))) { + rejected++; + continue; + } + worstResidual = std::max(worstResidual, residual); + points.push_back(candidate[0]); + points.push_back(candidate[1]); + points.push_back(candidate[2]); + } + return static_cast(points.size() / 3); +} + +namespace +{ + +/// One direction of the pair test: every accepted boundary point of \a points (in \a matFrom's +/// local frame) against \a target. +struct DirectionResult { + int contained = 0; + int deep = 0; + double maxDepth = 0.; + double deepestMaster[3] = {0., 0., 0.}; + double minSeparation = std::numeric_limits::max(); +}; + +DirectionResult probeDirection(const std::vector& points, const TGeoMatrix* matFrom, + const TGeoShape* target, const TGeoMatrix* matTo, double depthTolerance) +{ + DirectionResult result; + const size_t count = points.size() / 3; + for (size_t index = 0; index < count; ++index) { + double master[3] = {0., 0., 0.}; + double local[3] = {0., 0., 0.}; + matFrom->LocalToMaster(&points[3 * index], master); + matTo->MasterToLocal(master, local); + if (target->Contains(local)) { + result.contained++; + const double depth = target->Safety(local, kTRUE); + if (depth > depthTolerance) { + result.deep++; + } + if (depth > result.maxDepth) { + result.maxDepth = depth; + std::memcpy(result.deepestMaster, master, 3 * sizeof(double)); + } + } else { + result.minSeparation = std::min(result.minSeparation, target->Safety(local, kFALSE)); + } + } + return result; +} + +/// Probe a sampled pair both ways and set its counts, depth, deepest point and verdict. +OverlapPair assemblePair(const std::string& nameA, const std::vector& pointsA, const TGeoShape* shapeA, + const TGeoMatrix* matA, const std::string& nameB, const std::vector& pointsB, + const TGeoShape* shapeB, const TGeoMatrix* matB, const OverlapOptions& options) +{ + OverlapPair pair; + pair.nameA = nameA; + pair.nameB = nameB; + pair.sampledA = static_cast(pointsA.size() / 3); + pair.sampledB = static_cast(pointsB.size() / 3); + + const DirectionResult aInB = probeDirection(pointsA, matA, shapeB, matB, options.depthTolerance); + const DirectionResult bInA = probeDirection(pointsB, matB, shapeA, matA, options.depthTolerance); + + pair.pointsAInsideB = aInB.contained; + pair.pointsBInsideA = bInA.contained; + pair.deepPointsAInsideB = aInB.deep; + pair.deepPointsBInsideA = bInA.deep; + + if (aInB.maxDepth >= bInA.maxDepth) { + pair.depthCm = aInB.maxDepth; + std::copy(aInB.deepestMaster, aInB.deepestMaster + 3, pair.deepestPoint.begin()); + pair.deepestPointFrom = nameA; + } else { + pair.depthCm = bInA.maxDepth; + std::copy(bInA.deepestMaster, bInA.deepestMaster + 3, pair.deepestPoint.begin()); + pair.deepestPointFrom = nameB; + } + + // Containment: every boundary point of one solid is inside the other, and none of them is merely + // on its boundary. Legal only as a declared mother/daughter, which a flat conversion never emits. + const bool allAInside = pair.sampledA > 0 && aInB.contained == pair.sampledA && aInB.deep == pair.sampledA; + const bool allBInside = pair.sampledB > 0 && bInA.contained == pair.sampledB && bInA.deep == pair.sampledB; + + if (allAInside || allBInside) { + pair.verdict = OverlapVerdict::Contained; + } else if (aInB.deep > 0 || bInA.deep > 0) { + pair.verdict = OverlapVerdict::Interpenetrating; + } else if (aInB.contained > 0 || bInA.contained > 0) { + pair.verdict = OverlapVerdict::Touching; + } else { + pair.verdict = OverlapVerdict::Disjoint; + const double separation = std::min(aInB.minSeparation, bInA.minSeparation); + if (separation < std::numeric_limits::max()) { + pair.separationCm = separation; + } + } + return pair; +} + +/// Monte-Carlo estimate of the volume two placed solids share, into \a pair's shared-volume fields. +void estimateSharedVolume(const TGeoShape* shapeA, const TGeoMatrix* matA, const TGeoShape* shapeB, + const TGeoMatrix* matB, int samples, OverlapPair& pair) +{ + const MasterBox boxA = masterBox(shapeA, matA, 0.); + const MasterBox boxB = masterBox(shapeB, matB, 0.); + if (!boxA.valid || !boxB.valid) { + return; + } + double lower[3]; + double upper[3]; + double boxVolume = 1.; + for (int dimension = 0; dimension < 3; ++dimension) { + lower[dimension] = std::max(boxA.lower[dimension], boxB.lower[dimension]); + upper[dimension] = std::min(boxA.upper[dimension], boxB.upper[dimension]); + boxVolume *= std::max(0., upper[dimension] - lower[dimension]); + } + if (!(boxVolume > 0.)) { + return; + } + int hits = 0; + for (int sample = 0; sample < samples; ++sample) { + const double master[3] = {lower[0] + (upper[0] - lower[0]) * halton(sample + 1, 2), + lower[1] + (upper[1] - lower[1]) * halton(sample + 1, 3), + lower[2] + (upper[2] - lower[2]) * halton(sample + 1, 5)}; + double local[3]; + matA->MasterToLocal(master, local); + if (!shapeA->Contains(local)) { + continue; + } + matB->MasterToLocal(master, local); + if (shapeB->Contains(local)) { + hits++; + } + } + const double fraction = double(hits) / samples; + pair.sharedVolumeHits = hits; + pair.sharedVolumeCm3 = fraction * boxVolume; + pair.sharedVolumeErrCm3 = std::sqrt(std::max(1., double(hits))) / samples * boxVolume; +} + +} // namespace + +OverlapPair CheckPairOverlap(const TGeoShape* shapeA, const TGeoMatrix* matA, const std::string& nameA, + const TGeoShape* shapeB, const TGeoMatrix* matB, const std::string& nameB, + const OverlapOptions& options) +{ + OverlapPair pair; + pair.nameA = nameA; + pair.nameB = nameB; + if (shapeA == nullptr || shapeB == nullptr || matA == nullptr || matB == nullptr) { + return pair; + } + + int rejectedA = 0; + int rejectedB = 0; + double residualA = 0.; + double residualB = 0.; + std::vector pointsA; + std::vector pointsB; + SampleBoundaryPoints(shapeA, options.pointsPerSolid, options.residualTolerance, pointsA, rejectedA, residualA); + SampleBoundaryPoints(shapeB, options.pointsPerSolid, options.residualTolerance, pointsB, rejectedB, residualB); + pair = assemblePair(nameA, pointsA, shapeA, matA, nameB, pointsB, shapeB, matB, options); + if (options.volumeSamples > 0 && + (pair.verdict == OverlapVerdict::Interpenetrating || pair.verdict == OverlapVerdict::Contained)) { + estimateSharedVolume(shapeA, matA, shapeB, matB, options.volumeSamples, pair); + } + return pair; +} + +OverlapCensus CheckWorldOverlaps(const TGeoVolume* volume, const OverlapOptions& options) +{ + const auto startTime = std::chrono::steady_clock::now(); + OverlapCensus census; + if (volume == nullptr) { + return census; + } + const int daughters = volume->GetNdaughters(); + census.nSolids = daughters; + census.nPairsTotal = daughters * (daughters - 1) / 2; + + std::vector shapes(daughters, nullptr); + std::vector matrices(daughters, nullptr); + std::vector names(daughters); + std::vector boxes(daughters); + std::vector> points(daughters); + + for (int index = 0; index < daughters; ++index) { + TGeoNode* node = volume->GetNode(index); + shapes[index] = node->GetVolume()->GetShape(); + matrices[index] = node->GetMatrix(); + names[index] = node->GetVolume()->GetName(); + boxes[index] = masterBox(shapes[index], matrices[index], options.padCm); + + OverlapSolidReport report; + report.name = names[index]; + report.shapeClass = shapes[index] != nullptr ? shapes[index]->ClassName() : "none"; + report.requested = options.pointsPerSolid; + bool usedSegments = false; + report.accepted = SampleBoundaryPoints(shapes[index], options.pointsPerSolid, options.residualTolerance, + points[index], report.rejected, report.worstResidualCm, &usedSegments); + report.usedPointsOnSegments = usedSegments; + census.nPointsRejected += report.rejected; + census.worstResidualCm = std::max(census.worstResidualCm, report.worstResidualCm); + census.solids.push_back(report); + } + + for (int first = 0; first < daughters; ++first) { + for (int second = first + 1; second < daughters; ++second) { + if (!boxesOverlap(boxes[first], boxes[second])) { + continue; + } + census.nPairsTested++; + // Reuse the point sets: sampling is the expensive part and it does not depend on the partner. + OverlapPair pair = assemblePair(names[first], points[first], shapes[first], matrices[first], names[second], + points[second], shapes[second], matrices[second], options); + switch (pair.verdict) { + case OverlapVerdict::Disjoint: + census.nDisjoint++; + break; + case OverlapVerdict::Touching: + census.nTouching++; + break; + case OverlapVerdict::Interpenetrating: + census.nInterpenetrating++; + break; + case OverlapVerdict::Contained: + census.nContained++; + break; + } + if (options.volumeSamples > 0 && (pair.verdict == OverlapVerdict::Interpenetrating || + pair.verdict == OverlapVerdict::Contained)) { + estimateSharedVolume(shapes[first], matrices[first], shapes[second], matrices[second], options.volumeSamples, + pair); + } + census.pairs.push_back(pair); + } + } + + // extrusion: a daughter's boundary point outside its mother + if (options.checkExtrusion && volume->GetShape() != nullptr && !volume->IsAssembly()) { + TGeoIdentity identity; + for (int index = 0; index < daughters; ++index) { + OverlapPair pair; + pair.nameA = names[index]; + pair.nameB = volume->GetName(); + pair.sampledA = static_cast(points[index].size() / 3); + const TGeoShape* mother = volume->GetShape(); + double worst = 0.; + int outside = 0; + double worstMaster[3] = {0., 0., 0.}; + for (size_t point = 0; point < points[index].size() / 3; ++point) { + double master[3] = {0., 0., 0.}; + matrices[index]->LocalToMaster(&points[index][3 * point], master); + if (!mother->Contains(master)) { + const double depth = mother->Safety(master, kFALSE); + if (depth > options.depthTolerance) { + outside++; + if (depth > worst) { + worst = depth; + std::memcpy(worstMaster, master, 3 * sizeof(double)); + } + } + } + } + if (outside > 0) { + pair.verdict = OverlapVerdict::Interpenetrating; + pair.depthCm = worst; + pair.deepPointsAInsideB = outside; + pair.deepestPointFrom = names[index]; + std::copy(worstMaster, worstMaster + 3, pair.deepestPoint.begin()); + census.extrusions.push_back(pair); + census.nExtruding++; + } + } + } + + census.elapsedSeconds = + std::chrono::duration(std::chrono::steady_clock::now() - startTime).count(); + return census; +} + +} // namespace cad +} // namespace o2 diff --git a/Detectors/CADSupport/src/O2SolidHarness.cxx b/Detectors/CADSupport/src/O2SolidHarness.cxx new file mode 100644 index 0000000000000..25f8045b4ab8a --- /dev/null +++ b/Detectors/CADSupport/src/O2SolidHarness.cxx @@ -0,0 +1,674 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +#include "CADSupport/O2SolidHarness.h" +#include "CADSupport/O2FlatCSG.h" + +#include "TClass.h" +#include "TFile.h" +#include "TGeoMatrix.h" +#include "TKey.h" + +#include +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace cad +{ +namespace harness +{ + +namespace +{ + +// iact = 3, the convention O2Tessellated documents, for every shape. +constexpr Int_t kIact = 3; + +Point3D add(const Point3D& a, const Point3D& b) { return {a[0] + b[0], a[1] + b[1], a[2] + b[2]}; } +Point3D sub(const Point3D& a, const Point3D& b) { return {a[0] - b[0], a[1] - b[1], a[2] - b[2]}; } +Point3D scale(const Point3D& a, double s) { return {a[0] * s, a[1] * s, a[2] * s}; } +double normSq(const Point3D& a) { return a[0] * a[0] + a[1] * a[1] + a[2] * a[2]; } + +Point3D sampleUniform(std::mt19937_64& rng, const Point3D& lo, const Point3D& hi) +{ + std::uniform_real_distribution ux(lo[0], hi[0]); + std::uniform_real_distribution uy(lo[1], hi[1]); + std::uniform_real_distribution uz(lo[2], hi[2]); + return {ux(rng), uy(rng), uz(rng)}; +} + +Point3D isotropicDir(std::mt19937_64& rng) +{ + std::uniform_real_distribution uCos(-1., 1.); + std::uniform_real_distribution uPhi(0., 2. * M_PI); + const double cosTheta = uCos(rng); + const double sinTheta = std::sqrt(std::max(0., 1. - cosTheta * cosTheta)); + const double phi = uPhi(rng); + return {sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta}; +} + +bool isBig(double d) { return d >= 0.9 * TGeoShape::Big(); } + +} // namespace + +namespace detail +{ +uint64_t mixDouble(uint64_t acc, double value) +{ + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + bits += 0x9e3779b97f4a7c15ULL + (acc << 6) + (acc >> 2); + return acc ^ bits; +} +} // namespace detail + +SampleSet generateSamples(const TGeoShape* reference, const Point3D& bboxMin, const Point3D& bboxMax, + const SampleConfig& cfg) +{ + SampleSet out; + out.bboxMin = bboxMin; + out.bboxMax = bboxMax; + + const Point3D center = scale(add(bboxMin, bboxMax), 0.5); + const Point3D halfExtent = scale(sub(bboxMax, bboxMin), 0.5); + const Point3D inflatedLo = sub(center, scale(halfExtent, 1. + cfg.bboxInflate)); + const Point3D inflatedHi = add(center, scale(halfExtent, 1. + cfg.bboxInflate)); + + double band = cfg.boundaryBand; + if (band < 0.) { + const double diag = std::sqrt(normSq(sub(bboxMax, bboxMin))); + band = 1.e-3 * diag; + } + + std::mt19937_64 rng(cfg.seed); + + out.bulkPoints.reserve(cfg.nBulk); + for (int i = 0; i < cfg.nBulk; ++i) { + out.bulkPoints.push_back(sampleUniform(rng, inflatedLo, inflatedHi)); + } + + out.boundaryPoints.reserve(cfg.nBoundary); + { + const long long budget = static_cast(cfg.nBoundary) * cfg.maxRejectionAttempts; + long long attempts = 0; + while (static_cast(out.boundaryPoints.size()) < cfg.nBoundary && attempts < budget) { + ++attempts; + const Point3D p = sampleUniform(rng, bboxMin, bboxMax); + const bool in = reference->Contains(p.data()); + const double s = reference->Safety(p.data(), in); + if (s < band) { + out.boundaryPoints.push_back(p); + } + } + } + + out.insidePoints.reserve(cfg.nInside); + { + const long long budget = static_cast(cfg.nInside) * cfg.maxRejectionAttempts; + long long attempts = 0; + while (static_cast(out.insidePoints.size()) < cfg.nInside && attempts < budget) { + ++attempts; + const Point3D p = sampleUniform(rng, bboxMin, bboxMax); + if (reference->Contains(p.data())) { + out.insidePoints.push_back(p); + } + } + } + + out.outsideRays.reserve(cfg.nOutsideRays); + { + std::uniform_real_distribution u01(0., 1.); + const long long budget = static_cast(cfg.nOutsideRays) * cfg.maxRejectionAttempts; + long long attempts = 0; + while (static_cast(out.outsideRays.size()) < cfg.nOutsideRays && attempts < budget) { + ++attempts; + const Point3D origin = sampleUniform(rng, inflatedLo, inflatedHi); + if (reference->Contains(origin.data())) { + continue; + } + Point3D dir; + if (u01(rng) < cfg.aimedRayFraction) { + Point3D target = sampleUniform(rng, bboxMin, bboxMax); + Point3D delta = sub(target, origin); + double len = std::sqrt(normSq(delta)); + if (len < 1.e-12) { + dir = isotropicDir(rng); + } else { + dir = scale(delta, 1. / len); + } + } else { + dir = isotropicDir(rng); + } + out.outsideRays.push_back({origin, dir}); + } + } + + out.insideRays.reserve(cfg.nInsideRays); + { + const long long budget = static_cast(cfg.nInsideRays) * cfg.maxRejectionAttempts; + long long attempts = 0; + while (static_cast(out.insideRays.size()) < cfg.nInsideRays && attempts < budget) { + ++attempts; + const Point3D origin = sampleUniform(rng, bboxMin, bboxMax); + if (!reference->Contains(origin.data())) { + continue; + } + out.insideRays.push_back({origin, isotropicDir(rng)}); + } + } + + return out; +} + +// ---- Validation ---------------------------------------------------------------------------------- + +namespace +{ + +enum class MismatchClass { WithinBand, + MissedSurface, + Unexplained }; + +void recordOffender(ValidationResult& result, const ValidationOptions& opt, Offender&& off, + MismatchClass mismatchClass) +{ + switch (mismatchClass) { + case MismatchClass::WithinBand: + ++result.nMismatchWithinBand; + break; + case MismatchClass::MissedSurface: + ++result.nMismatchMissedSurface; + break; + case MismatchClass::Unexplained: + ++result.nMismatchUnexplained; + break; + } + result.worstDeviation = std::max(result.worstDeviation, std::fabs(off.deviation)); + result.worstOffenders.push_back(std::move(off)); + std::sort(result.worstOffenders.begin(), result.worstOffenders.end(), + [](const Offender& a, const Offender& b) { return std::fabs(a.deviation) > std::fabs(b.deviation); }); + if (result.worstOffenders.size() > opt.maxOffenders) { + result.worstOffenders.resize(opt.maxOffenders); + } +} + +/// How far a crossing may move when the reference surface is uncertain by `opt.meshBand`: d / |cos(incidence)|, floored. +double allowedCrossingShift(const TGeoShape* normalSource, const Point3D& probePoint, + const Point3D& dir, const ValidationOptions& opt, double& cosIncidence) +{ + cosIncidence = 1.; + if (normalSource != nullptr) { + double normal[3] = {0., 0., 0.}; + normalSource->ComputeNormal(probePoint.data(), dir.data(), normal); + const double normalNorm = + std::sqrt(normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2]); + if (normalNorm > 0.) { + const double dotProduct = + (normal[0] * dir[0] + normal[1] * dir[1] + normal[2] * dir[2]) / normalNorm; + cosIncidence = std::fabs(dotProduct); + } + } + const double effectiveCosine = std::max(cosIncidence, opt.minIncidenceCosine); + return std::max(opt.distanceTolerance, opt.meshBand / effectiveCosine); +} + +/// Shared classification for both distance queries. `dc`/`dr` are the candidate and reference +/// distances; `reference` (may be null) is used only to measure the incidence angle. +MismatchClass classifyDistanceMismatch(const TGeoShape* reference, const Ray& ray, double dc, + double dr, bool dcBig, bool drBig, + const ValidationOptions& opt, double& cosIncidence) +{ + cosIncidence = 1.; + // One side found a crossing where the other found none. No amount of surface uncertainty + // explains a missing wall, so this can never be counted as "within band". + if (dcBig != drBig) { + return MismatchClass::MissedSurface; + } + const Point3D probePoint = add(ray.origin, scale(ray.dir, dr)); + const double allowed = allowedCrossingShift(reference, probePoint, ray.dir, opt, cosIncidence); + return std::fabs(dc - dr) <= allowed ? MismatchClass::WithinBand : MismatchClass::Unexplained; +} + +} // namespace + +ValidationResult validateContains(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& points, const ValidationOptions& opt) +{ + ValidationResult result; + result.nSamples = points.size(); + for (const auto& p : points) { + const bool bc = candidate->Contains(p.data()); + const bool br = reference->Contains(p.data()); + if (bc == br) { + ++result.nAgree; + continue; + } + const double refSafety = reference->Safety(p.data(), br); + Offender off; + off.point = p; + off.candidateValue = bc ? 1. : 0.; + off.referenceValue = br ? 1. : 0.; + off.deviation = refSafety; // rank Contains mismatches by how deep into the "unambiguous" region they are + off.referenceSafety = refSafety; + // A point closer to the reference surface than the reference's own positional uncertainty + // genuinely has no defined reference answer; further out, the reference is authoritative. + recordOffender(result, opt, std::move(off), + refSafety < opt.meshBand ? MismatchClass::WithinBand : MismatchClass::Unexplained); + } + return result; +} + +namespace +{ +/// The distance validation of DistFromInside (\a inside) or DistFromOutside. +ValidationResult validateDistance(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& rays, const ValidationOptions& opt, bool inside) +{ + const auto distance = [&](const TGeoShape* shape, const Ray& r) { + return inside ? shape->DistFromInside(r.origin.data(), r.dir.data(), kIact, opt.stepmax) + : shape->DistFromOutside(r.origin.data(), r.dir.data(), kIact, opt.stepmax); + }; + ValidationResult result; + result.nSamples = rays.size(); + for (const auto& r : rays) { + const double dc = distance(candidate, r); + const double dr = distance(reference, r); + const bool dcBig = isBig(dc); + const bool drBig = isBig(dr); + if (dcBig && drBig) { + ++result.nAgree; + continue; + } + if (!dcBig && !drBig && std::fabs(dc - dr) <= opt.distanceTolerance) { + ++result.nAgree; + continue; + } + double cosIncidence = 1.; + const MismatchClass mismatchClass = + classifyDistanceMismatch(reference, r, dc, dr, dcBig, drBig, opt, cosIncidence); + Offender off; + off.point = r.origin; + off.dir = r.dir; + off.candidateValue = dcBig ? opt.stepmax : dc; + off.referenceValue = drBig ? opt.stepmax : dr; + off.deviation = off.candidateValue - off.referenceValue; + off.incidenceCosine = cosIncidence; + recordOffender(result, opt, std::move(off), mismatchClass); + } + return result; +} +} // namespace + +ValidationResult validateDistFromOutside(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& rays, const ValidationOptions& opt) +{ + return validateDistance(candidate, reference, rays, opt, false); +} + +ValidationResult validateDistFromInside(const TGeoShape* candidate, const TGeoShape* reference, + const std::vector& rays, const ValidationOptions& opt) +{ + return validateDistance(candidate, reference, rays, opt, true); +} + +ValidationResult validateSafety(const TGeoShape* shape, const std::vector& points, + const ValidationOptions& opt) +{ + static const std::array kProbeDirs = { + Point3D{1., 0., 0.}, Point3D{-1., 0., 0.}, Point3D{0., 1., 0.}, + Point3D{0., -1., 0.}, Point3D{0., 0., 1.}, Point3D{0., 0., -1.}}; + + ValidationResult result; + result.nSamples = points.size(); + for (const auto& p : points) { + const bool in = shape->Contains(p.data()); + const double s = shape->Safety(p.data(), in); + + double minProbed = TGeoShape::Big(); + for (const auto& d : kProbeDirs) { + const double dist = in ? shape->DistFromInside(p.data(), d.data(), kIact, opt.stepmax) + : shape->DistFromOutside(p.data(), d.data(), kIact, opt.stepmax); + minProbed = std::min(minProbed, isBig(dist) ? opt.stepmax : dist); + } + + const bool violatesLowerBound = s < -opt.distanceTolerance; + const bool violatesUpperBound = s > minProbed + opt.distanceTolerance; + if (!violatesLowerBound && !violatesUpperBound) { + ++result.nAgree; + continue; + } + Offender off; + off.point = p; + off.candidateValue = s; + off.referenceValue = minProbed; + off.deviation = s - minProbed; + off.referenceSafety = s; + recordOffender(result, opt, std::move(off), MismatchClass::Unexplained); + } + return result; +} + +// ---- Validation against an external oracle --------------------------------------------------------- + +namespace +{ +/// The oracle's exact boundary distance covers a capped prefix; beyond it the value is negative, meaning unknown. +constexpr double kUnknownDistance = -1.; + +double oracleDistanceAt(const std::vector& distances, size_t index) +{ + return index < distances.size() ? distances[index] : kUnknownDistance; +} +} // namespace + +ValidationResult validateContainsAgainstOracle(const TGeoShape* candidate, + const std::vector& points, + const std::vector& oracleState, + const std::vector& oracleBoundaryDistance, + const ValidationOptions& opt) +{ + ValidationResult result; + result.nSamples = points.size(); + for (size_t index = 0; index < points.size(); ++index) { + const int state = index < oracleState.size() ? oracleState[index] : -1; + const double boundaryDistance = oracleDistanceAt(oracleBoundaryDistance, index); + // the oracle abstains on the boundary or within the model tolerance of it + if (state < 0 || (boundaryDistance >= 0. && boundaryDistance < opt.meshBand)) { + ++result.nNoVerdict; + continue; + } + const bool candidateInside = candidate->Contains(points[index].data()); + if (candidateInside == (state == 1)) { + ++result.nAgree; + continue; + } + Offender off; + off.point = points[index]; + off.candidateValue = candidateInside ? 1. : 0.; + off.referenceValue = state == 1 ? 1. : 0.; + // Rank by how far into unambiguous territory the disagreement sits: a wrong answer 1 cm from + // any surface is a different animal from one 1 um away. + off.deviation = boundaryDistance >= 0. ? boundaryDistance : 0.; + off.referenceSafety = boundaryDistance; + recordOffender(result, opt, std::move(off), MismatchClass::Unexplained); + } + return result; +} + +ValidationResult validateDistanceAgainstOracle(const TGeoShape* candidate, + const std::vector& rays, + const std::vector& oracleDistance, + bool wantInside, const ValidationOptions& opt, + const std::vector& oracleOriginState) +{ + ValidationResult result; + result.nSamples = rays.size(); + for (size_t index = 0; index < rays.size(); ++index) { + if (index >= oracleDistance.size()) { + ++result.nNoVerdict; + continue; + } + // the oracle's own origin classification decides which entry point is defined here + bool askInside = wantInside; + if (index < oracleOriginState.size()) { + const int state = oracleOriginState[index]; + if (state < 0) { + ++result.nNoVerdict; // origin ON the boundary: neither entry point is defined + continue; + } + askInside = state == 1; + if (askInside != wantInside) { + ++result.nRelabelled; + } + } + const auto& ray = rays[index]; + const double dc = askInside + ? candidate->DistFromInside(ray.origin.data(), ray.dir.data(), kIact, opt.stepmax) + : candidate->DistFromOutside(ray.origin.data(), ray.dir.data(), kIact, opt.stepmax); + const double dr = oracleDistance[index]; + const bool dcBig = isBig(dc); + const bool drBig = isBig(dr); + if (dcBig && drBig) { + ++result.nAgree; + continue; + } + if (!dcBig && !drBig && std::fabs(dc - dr) <= opt.distanceTolerance) { + ++result.nAgree; + continue; + } + // no reference shape to take a normal from: the strict perpendicular allowance applies + double cosIncidence = 1.; + const MismatchClass mismatchClass = + classifyDistanceMismatch(nullptr, ray, dc, dr, dcBig, drBig, opt, cosIncidence); + Offender off; + off.point = ray.origin; + off.dir = ray.dir; + off.candidateValue = dcBig ? opt.stepmax : dc; + off.referenceValue = drBig ? opt.stepmax : dr; + off.deviation = off.candidateValue - off.referenceValue; + off.incidenceCosine = cosIncidence; + recordOffender(result, opt, std::move(off), mismatchClass); + } + return result; +} + +ValidationResult validateSafetyAgainstOracle(const TGeoShape* candidate, + const std::vector& points, + const std::vector& oracleBoundaryDistance, + const ValidationOptions& opt) +{ + ValidationResult result; + result.nSamples = points.size(); + for (size_t index = 0; index < points.size(); ++index) { + const double trueDistance = oracleDistanceAt(oracleBoundaryDistance, index); + if (trueDistance < 0.) { + ++result.nNoVerdict; + continue; + } + const bool inside = candidate->Contains(points[index].data()); + const double safety = candidate->Safety(points[index].data(), inside); + // Safety must be a non-negative lower bound on the true distance + const bool violatesLowerBound = safety < -opt.distanceTolerance; + const bool violatesUpperBound = safety > trueDistance + opt.distanceTolerance; + if (!violatesLowerBound && !violatesUpperBound) { + ++result.nAgree; + continue; + } + Offender off; + off.point = points[index]; + off.candidateValue = safety; + off.referenceValue = trueDistance; + off.deviation = safety - trueDistance; + off.referenceSafety = trueDistance; + recordOffender(result, opt, std::move(off), MismatchClass::Unexplained); + } + return result; +} + +// ---- Timing -------------------------------------------------------------------------------------- + +namespace +{ +/// timeRayKernel's methodology for a per-point kernel: `kernel(point)` returns a double to mix. +template +TimingResult timePointKernel(const std::vector& points, int warmupRepeats, int timedRepeats, + PointKernel&& kernel) +{ + for (int warmup = 0; warmup < warmupRepeats; ++warmup) { + for (const auto& point : points) { + volatile double sink = kernel(point); + (void)sink; + } + } + uint64_t checksum = 0; + const auto start = std::chrono::steady_clock::now(); + for (int repeat = 0; repeat < timedRepeats; ++repeat) { + for (const auto& point : points) { + checksum = detail::mixDouble(checksum, kernel(point)); + } + } + const auto stop = std::chrono::steady_clock::now(); + TimingResult result; + result.nCalls = points.size() * static_cast(timedRepeats); + const double nanoseconds = std::chrono::duration(stop - start).count(); + result.nsPerCall = result.nCalls > 0 ? nanoseconds / static_cast(result.nCalls) : 0.; + result.checksum = checksum; + return result; +} +} // namespace + +TimingResult timeContains(const TGeoShape* shape, const std::vector& points, int warmupRepeats, + int timedRepeats) +{ + return timePointKernel(points, warmupRepeats, timedRepeats, + [&](const Point3D& p) { return shape->Contains(p.data()) ? 1. : 0.; }); +} + +TimingResult timeDistFromOutside(const TGeoShape* shape, const std::vector& rays, int warmupRepeats, + int timedRepeats, double stepmax) +{ + return timeRayKernel(rays, warmupRepeats, timedRepeats, [&](const Point3D& origin, const Point3D& dir) { + return shape->DistFromOutside(origin.data(), dir.data(), kIact, stepmax); + }); +} + +TimingResult timeDistFromInside(const TGeoShape* shape, const std::vector& rays, int warmupRepeats, + int timedRepeats) +{ + return timeRayKernel(rays, warmupRepeats, timedRepeats, [&](const Point3D& origin, const Point3D& dir) { + return shape->DistFromInside(origin.data(), dir.data(), kIact, TGeoShape::Big()); + }); +} + +TimingResult timeSafety(const TGeoShape* shape, const std::vector& points, int warmupRepeats, + int timedRepeats) +{ + return timePointKernel(points, warmupRepeats, timedRepeats, + [&](const Point3D& p) { return shape->Safety(p.data(), shape->Contains(p.data())); }); +} + +// ---- The `shape_.root` sidecar ------------------------------------------------------------- + +namespace +{ +/// The key an emitter is required to write. Kept here rather than duplicated at both call sites +/// so reader and writer cannot disagree about it. +constexpr const char* kShapeKeyName = "shape"; +/// The optional companion key: the shape's rigid placement, `local -> part`. Absent means +/// identity. +constexpr const char* kPlacementKeyName = "placement"; +} // namespace + +TGeoShape* loadShapeFromRootFile(const std::string& path, std::string* error) +{ + const auto fail = [error](const std::string& why) -> TGeoShape* { + if (error != nullptr) { + *error = why; + } + return nullptr; + }; + std::unique_ptr file(TFile::Open(path.c_str(), "READ")); + if (!file || file->IsZombie()) { + return fail(path + ": cannot be opened as a ROOT file"); + } + TObject* object = file->Get(kShapeKeyName); + if (object == nullptr) { + // fall back to the first TGeoShape-derived key; emitters must write "shape" + TIter next(file->GetListOfKeys()); + while (auto* key = static_cast(next())) { + TClass* cl = TClass::GetClass(key->GetClassName()); + if (cl != nullptr && cl->InheritsFrom(TGeoShape::Class())) { + object = key->ReadObj(); + break; + } + } + } + if (object == nullptr) { + return fail(path + ": holds no object inheriting from TGeoShape (expected key \"" + + kShapeKeyName + "\")"); + } + auto* shape = dynamic_cast(object); + if (shape == nullptr) { + const std::string className = object->ClassName(); + delete object; + return fail(path + ": key \"" + kShapeKeyName + "\" holds a " + className + + ", which does not inherit from TGeoShape"); + } + // An O2FlatCSG read from a file was closed by the `#pragma read` rule in CADSupportLinkDef.h; + // one that is still open refused, which means a broken file. + if (auto* flat = dynamic_cast(shape); flat != nullptr && !flat->IsClosed()) { + delete shape; + return fail(path + + ": the O2FlatCSG it holds refused to close, so its sub-cell boxes could " + "not be rebuilt (see the Error above)"); + } + // The object was read out of a TDirectory but is not a TDirectory-owned type (TGeoShape is not + // a histogram/tree), so we own it and it stays valid past the file's destruction. + return shape; +} + +TGeoHMatrix* loadShapePlacementFromRootFile(const std::string& path) +{ + std::unique_ptr file(TFile::Open(path.c_str(), "READ")); + if (!file || file->IsZombie()) { + return nullptr; + } + auto* stored = file->Get(kPlacementKeyName); + if (stored == nullptr) { + return nullptr; + } + // copied out rather than detached from the file + auto* placement = new TGeoHMatrix(*stored); + return placement; +} + +bool saveShapeToRootFile(const std::string& path, const TGeoShape& shape, std::string* error) +{ + return saveShapeToRootFile(path, shape, nullptr, error); +} + +bool saveShapeToRootFile(const std::string& path, const TGeoShape& shape, + const TGeoMatrix* placement, std::string* error) +{ + std::unique_ptr file(TFile::Open(path.c_str(), "RECREATE")); + if (!file || file->IsZombie()) { + if (error != nullptr) { + *error = path + ": cannot be opened for writing"; + } + return false; + } + const int written = file->WriteTObject(&shape, kShapeKeyName); + // an identity placement is not written: no key means the identity + if (placement != nullptr && !placement->IsIdentity()) { + TGeoHMatrix stored(*placement); + stored.SetName(kPlacementKeyName); + file->WriteTObject(&stored, kPlacementKeyName); + } + file->Close(); + if (written <= 0) { + if (error != nullptr) { + *error = path + ": WriteTObject wrote 0 bytes"; + } + return false; + } + return true; +} + +} // namespace harness +} // namespace cad +} // namespace o2 diff --git a/Detectors/CADSupport/src/O2SurfaceSolidIO.cxx b/Detectors/CADSupport/src/O2SurfaceSolidIO.cxx new file mode 100644 index 0000000000000..36283411a439a --- /dev/null +++ b/Detectors/CADSupport/src/O2SurfaceSolidIO.cxx @@ -0,0 +1,864 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +/// \file O2SurfaceSolidIO.cxx +/// \brief Readers of the surface, facet and flat-CSG sidecars, in sync with the writers in O2_CADtoTGeo.py and cadsupport/flat.py. + +#include "CADSupport/O2SurfaceSolidIO.h" +#include "CADSupport/O2BVHSurfaceSolid.h" +#include "DetectorsBase/O2Tessellated.h" +#include "CADSupport/O2FlatCSG.h" + +#include "BoundedSurface.h" + +#include + +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace cad +{ + +using o2::base::O2Tessellated; + +namespace +{ + +/// Sidecar versions this reader understands: v2 adds a float64 model tolerance (cm) to the header, +/// v3 a uint32 edge-table size and each face's boundary edge identities after its wires. +constexpr uint32_t kSidecarVersionMin = 1; +constexpr uint32_t kSidecarVersionMax = 3; + +/// A version-1 sidecar's model tolerance, in cm: the extractor precision, as a fallback. +constexpr double kSidecarV1FallbackTolerance = 1.e-6; + +constexpr uint32_t kFlagInnerWall = 1u << 0; + +/// The flat-CSG sidecar version, and its packed record sizes: 100-byte halfspaces and 64-byte cells. +constexpr uint32_t kFlatCSGVersion = 1; +constexpr uint64_t kFlatCSGHalfspaceBytes = 100; +constexpr uint64_t kFlatCSGCellBytes = 64; + +enum SurfaceType : uint32_t { + kPlane = 1, + kCylinder = 2, + kCone = 3, + kSphere = 4, + kTorus = 5, +}; + +enum CurveType : uint32_t { + kLineSegment = 0, + kCircularArc = 1, + kBSpline2D = 2, +}; + +/// Parse a B-spline edge record [degree, nPoles, poles, weights, knots] into \a curve; false when malformed. +bool parseBSplineEdge(const std::vector& params, O2BVHSurfaceSolid::PlanarBoundaryCurve& curve) +{ + if (params.size() < 2) { + return false; + } + const int degree = static_cast(std::lround(params[0])); + const int nPoles = static_cast(std::lround(params[1])); + if (degree < 1 || nPoles < degree + 1) { + return false; + } + const size_t nKnots = static_cast(nPoles) + degree + 1; + const size_t expected = 2 + 2 * static_cast(nPoles) + static_cast(nPoles) + nKnots; + if (params.size() < expected) { + return false; + } + std::vector poles(nPoles); + size_t offset = 2; + for (int i = 0; i < nPoles; ++i) { + poles[i] = {params[offset], params[offset + 1]}; + offset += 2; + } + std::vector weights(nPoles); + for (int i = 0; i < nPoles; ++i) { + weights[i] = params[offset++]; + } + std::vector knots(nKnots); + for (size_t i = 0; i < nKnots; ++i) { + knots[i] = params[offset++]; + } + curve = O2BVHSurfaceSolid::PlanarBoundaryCurve::makeBSpline(degree, std::move(poles), std::move(weights), + std::move(knots)); + return true; +} + +struct SidecarEdge { + uint32_t curveType = 0; + std::vector params; +}; + +struct SidecarWire { + uint32_t role = 0; // 0 = outer, 1 = inner + std::vector edges; +}; + +/// Packed records: always read field by field. +template +bool readValue(std::ifstream& in, T& value) +{ + in.read(reinterpret_cast(&value), sizeof(value)); + return static_cast(in); +} + +/// A single field, written on its own; the counterpart of readValue. +template +void writeValue(std::ofstream& out, const T& value) +{ + out.write(reinterpret_cast(&value), sizeof(value)); +} + +/// Bytes left to read, 0 once the stream is bad; every count read from the file is checked against it. +uint64_t bytesRemaining(std::ifstream& in, std::streamoff fileSize) +{ + if (!in) { + return 0; + } + const std::streamoff here = in.tellg(); + return here < 0 || here > fileSize ? 0 : static_cast(fileSize - here); +} + +bool readDoubles(std::ifstream& in, std::vector& values, uint32_t n, std::streamoff fileSize) +{ + if (static_cast(n) * sizeof(double) > bytesRemaining(in, fileSize)) { + return false; + } + values.resize(n); + in.read(reinterpret_cast(values.data()), static_cast(n) * sizeof(double)); + return static_cast(in); +} + +O2BVHSurfaceSolid::Point3D point3(const std::vector& p, size_t offset) +{ + return {p[offset], p[offset + 1], p[offset + 2]}; +} + +/// Start/end (u, v) endpoints of a sidecar edge; a B-spline edge's parsed curve goes to \a bspline. +bool edgeEndpoints(const SidecarEdge& edge, O2BVHSurfaceSolid::Point2D& start, O2BVHSurfaceSolid::Point2D& end, + O2BVHSurfaceSolid::PlanarBoundaryCurve& bspline) +{ + if (edge.curveType == kLineSegment && edge.params.size() >= 4) { + start = {edge.params[0], edge.params[1]}; + end = {edge.params[2], edge.params[3]}; + return true; + } + if (edge.curveType == kCircularArc && edge.params.size() >= 5) { + const double cu = edge.params[0], cv = edge.params[1], r = edge.params[2]; + const double a0 = edge.params[3], a1 = edge.params[3] + edge.params[4]; + start = {cu + r * std::cos(a0), cv + r * std::sin(a0)}; + end = {cu + r * std::cos(a1), cv + r * std::sin(a1)}; + return true; + } + if (edge.curveType == kBSpline2D) { + if (!parseBSplineEdge(edge.params, bspline)) { + return false; + } + // Evaluate the curve rather than read its first and last poles, which lie off the curve for an + // unclamped or periodic knot vector. + std::vector poles; + poles.reserve(bspline.poles.size()); + for (const auto& pole : bspline.poles) { + poles.push_back({pole[0], pole[1]}); + } + const surface::Curve2D evaluated = + surface::Curve2D::makeBSpline(bspline.degree, std::move(poles), bspline.weights, bspline.knots); + const surface::Vec2 first = evaluated.startPoint(); + const surface::Vec2 last = evaluated.endPoint(); + start = {first.uCoord, first.vCoord}; + end = {last.uCoord, last.vCoord}; + return true; + } + return false; +} + +/// The first fundamental form of a sidecar record's surface, from its own parameters, for the join check. +struct RecordMetric { + uint32_t surfaceType = 0; + const double* params = nullptr; + + static void evaluate(const void* context, const surface::Vec2& uv, double& gUU, double& gUV, double& gVV) + { + const auto& record = *static_cast(context); + const double* p = record.params; + switch (record.surfaceType) { + case kPlane: + surface::planeParametricMetric({p[3], p[4], p[5]}, {p[6], p[7], p[8]}, gUU, gUV, gVV); + return; + case kCylinder: + surface::cylinderParametricMetric(p[9], gUU, gUV, gVV); + return; + case kCone: { + // r(h) = radiusAtMin + slope * (h - heightMin), with slope from the two radii/heights + const double slope = (p[10] - p[9]) / (p[12] - p[11]); + surface::coneParametricMetric(p[9] + slope * (uv.vCoord - p[11]), slope, gUU, gUV, gVV); + return; + } + case kSphere: + surface::sphereParametricMetric(p[9], uv.vCoord, gUU, gUV, gVV); + return; + case kTorus: + surface::torusParametricMetric(p[9], p[10], uv.vCoord, gUU, gUV, gVV); + return; + default: + // an unknown type is rejected further down; the identity keeps this total meanwhile + gUU = 1.; + gUV = 0.; + gVV = 1.; + return; + } + } + + surface::ParametricMetric metric() const { return {&evaluate, this}; } +}; + +/// Convert a sidecar wire into a PlanarBoundaryCurve loop; joins are judged as 3D gaps in cm against the kernel's band. +/// \a anyArc is set by a curved edge; \a toleranceOrigin names the band for the diagnostic. +bool wireToCurves(const std::string& file, size_t surfaceIndex, const SidecarWire& wire, + std::vector& curves, bool& anyArc, + const surface::ParametricMetric& metric, double joinTolerance, const char* toleranceOrigin) +{ + using Curve = O2BVHSurfaceSolid::PlanarBoundaryCurve; + curves.clear(); + curves.reserve(wire.edges.size()); + // every edge's endpoints, and a B-spline edge's parsed curve, computed once + const size_t nEdges = wire.edges.size(); + std::vector starts(nEdges); + std::vector ends(nEdges); + std::vector bsplines(nEdges); + for (size_t e = 0; e < nEdges; ++e) { + if (!edgeEndpoints(wire.edges[e], starts[e], ends[e], bsplines[e])) { + ::Error("LoadSurfaceSolid", "%s: surface %zu: unsupported or malformed wire edge %zu", file.c_str(), + surfaceIndex, e); + return false; + } + } + for (size_t e = 0; e < nEdges; ++e) { + const auto& edge = wire.edges[e]; + const auto& end = ends[e]; + const auto& nextStart = starts[(e + 1) % nEdges]; + const double joinGapSq = metric.distanceSq({end[0], end[1]}, {nextStart[0], nextStart[1]}); + if (joinGapSq > joinTolerance * joinTolerance) { + ::Error("LoadSurfaceSolid", + "%s: surface %zu: wire edge %zu end does not join the next edge start (gap %.3g cm, tolerance %.3g cm, " + "%s)", + file.c_str(), surfaceIndex, e, std::sqrt(joinGapSq), joinTolerance, toleranceOrigin); + return false; + } + if (edge.curveType == kCircularArc) { + anyArc = true; + curves.push_back(Curve::makeArc({edge.params[0], edge.params[1]}, edge.params[2], edge.params[3], + edge.params[3] + edge.params[4])); + } else if (edge.curveType == kBSpline2D) { + anyArc = true; // a bspline is a curved edge, so route the plane through AddCurvedPlanarSurface + curves.push_back(std::move(bsplines[e])); + } else { + curves.push_back(Curve::makeLine(starts[e], end)); + } + } + return true; +} + +/// The two error texts of a trim block, as printf formats taking the file and the surface index. +struct TrimWording { + const char* moreThanOneOuter; + const char* noOuter; +}; +constexpr TrimWording kPlaneWording{"%s: plane surface %zu has more than one outer wire", + "%s: plane surface %zu has no outer wire"}; +constexpr TrimWording kQuadricWording{"%s: quadric surface %zu has more than one outer trim wire", + "%s: quadric surface %zu trim block has no outer wire"}; + +/// Collect a wire block into one outer and several inner PlanarBoundaryCurve loops in the (u, v) domain; \a anyArc is set by a curved edge. +bool collectTrim(const std::string& file, size_t surfaceIndex, const TrimWording& wording, + const std::vector& wires, std::vector& outer, + std::vector>& inners, bool& anyArc, + const surface::ParametricMetric& metric, double joinTolerance, const char* toleranceOrigin) +{ + bool haveOuter = false; + for (const auto& wire : wires) { + std::vector curves; + if (!wireToCurves(file, surfaceIndex, wire, curves, anyArc, metric, joinTolerance, toleranceOrigin)) { + return false; + } + if (wire.role == 0) { + if (haveOuter) { + ::Error("LoadSurfaceSolid", wording.moreThanOneOuter, file.c_str(), surfaceIndex); + return false; + } + outer = std::move(curves); + haveOuter = true; + } else { + inners.push_back(std::move(curves)); + } + } + if (!haveOuter) { + ::Error("LoadSurfaceSolid", wording.noOuter, file.c_str(), surfaceIndex); + return false; + } + return true; +} + +/// Permute a face's edge identities from the sidecar's wire order into the kernel's: the outer wire first, then the inner wires. +void reorderEdgeRefsToKernelOrder(const std::vector& wires, std::vector& edgeIds, + std::vector& edgeFlags) +{ + size_t totalEdges = 0; + for (const auto& wire : wires) { + totalEdges += wire.edges.size(); + } + if (wires.empty() || totalEdges != edgeIds.size()) { + return; + } + // kernel offset of each sidecar wire: the outer wire first, then the inner wires in file order + std::vector kernelOffset(wires.size(), 0); + size_t running = 0; + for (size_t w = 0; w < wires.size(); ++w) { + if (wires[w].role == 0) { + kernelOffset[w] = 0; + running = wires[w].edges.size(); + break; + } + } + for (size_t w = 0; w < wires.size(); ++w) { + if (wires[w].role != 0) { + kernelOffset[w] = running; + running += wires[w].edges.size(); + } + } + + std::vector permutedIds(edgeIds.size()); + std::vector permutedFlags(edgeFlags.size()); + size_t sidecarOffset = 0; + for (size_t w = 0; w < wires.size(); ++w) { + for (size_t e = 0; e < wires[w].edges.size(); ++e) { + permutedIds[kernelOffset[w] + e] = edgeIds[sidecarOffset + e]; + permutedFlags[kernelOffset[w] + e] = edgeFlags[sidecarOffset + e]; + } + sidecarOffset += wires[w].edges.size(); + } + edgeIds.swap(permutedIds); + edgeFlags.swap(permutedFlags); +} + +} // namespace + +bool LoadSurfaceSolid(const std::string& file, O2BVHSurfaceSolid& solid) +{ + std::ifstream in(file, std::ios::binary); + if (!in) { + ::Error("LoadSurfaceSolid", "Cannot open surface sidecar file %s", file.c_str()); + return false; + } + + in.seekg(0, std::ios::end); + const std::streamoff fileSize = in.tellg(); + in.seekg(0, std::ios::beg); + + char magic[4]; + in.read(magic, sizeof(magic)); + if (!in || std::memcmp(magic, "O2SS", 4) != 0) { + ::Error("LoadSurfaceSolid", "%s is not a surface sidecar file (bad magic)", file.c_str()); + return false; + } + + uint32_t version = 0, nSurfaces = 0, reserved = 0; + if (!readValue(in, version) || !readValue(in, nSurfaces) || !readValue(in, reserved)) { + ::Error("LoadSurfaceSolid", "%s: truncated header", file.c_str()); + return false; + } + if (version < kSidecarVersionMin || version > kSidecarVersionMax) { + ::Error("LoadSurfaceSolid", "%s: unsupported sidecar version %u (reader supports %u..%u)", file.c_str(), version, + kSidecarVersionMin, kSidecarVersionMax); + return false; + } + + uint32_t nModelEdges = 0; + if (version >= 2) { + double modelTolerance = 0.; + if (!readValue(in, modelTolerance)) { + ::Error("LoadSurfaceSolid", "%s: truncated version-2 header (no model tolerance)", file.c_str()); + return false; + } + solid.SetModelTolerance(modelTolerance); + if (version >= 3 && !readValue(in, nModelEdges)) { + ::Error("LoadSurfaceSolid", "%s: truncated version-3 header (no edge table size)", file.c_str()); + return false; + } + } else { + ::Warning("LoadSurfaceSolid", + "%s is a version-1 sidecar and states no model tolerance; assuming %g cm (the extractor's precision). " + "Re-run the converter to record the model's own value.", + file.c_str(), kSidecarV1FallbackTolerance); + solid.SetModelTolerance(kSidecarV1FallbackTolerance); + } + + // the wire-join band, from the header: the band the kernel's Add*Surface applies to the same wires + const double joinTolerance = surface::wireJoinToleranceFor(solid.GetModelTolerance()); + const char* toleranceOrigin = joinTolerance > surface::kWireJoinTolerance + ? "declared by the model" + : "the extractor-precision fallback"; + + for (size_t s = 0; s < nSurfaces; ++s) { + uint32_t surfaceType = 0, flags = 0, nParams = 0; + if (!readValue(in, surfaceType) || !readValue(in, flags) || !readValue(in, nParams)) { + ::Error("LoadSurfaceSolid", "%s: truncated surface record %zu", file.c_str(), s); + return false; + } + std::vector p; + if (!readDoubles(in, p, nParams, fileSize)) { + ::Error("LoadSurfaceSolid", "%s: truncated parameters of surface %zu", file.c_str(), s); + return false; + } + + // The wire block is self-describing; read it unconditionally. + uint32_t nWires = 0; + if (!readValue(in, nWires)) { + ::Error("LoadSurfaceSolid", "%s: truncated wire count of surface %zu", file.c_str(), s); + return false; + } + // 8 bytes of header per wire is the floor, so a count beyond that cannot be honest + if (static_cast(nWires) * 8u > bytesRemaining(in, fileSize)) { + ::Error("LoadSurfaceSolid", "%s: surface %zu claims %u wires, more than the file holds", file.c_str(), s, nWires); + return false; + } + std::vector wires(nWires); + for (auto& wire : wires) { + uint32_t nEdges = 0; + if (!readValue(in, wire.role) || !readValue(in, nEdges)) { + ::Error("LoadSurfaceSolid", "%s: truncated wire header in surface %zu", file.c_str(), s); + return false; + } + if (static_cast(nEdges) * 8u > bytesRemaining(in, fileSize)) { + ::Error("LoadSurfaceSolid", "%s: surface %zu claims %u wire edges, more than the file holds", file.c_str(), s, + nEdges); + return false; + } + wire.edges.resize(nEdges); + for (auto& edge : wire.edges) { + uint32_t nCurveParams = 0; + if (!readValue(in, edge.curveType) || !readValue(in, nCurveParams) || + !readDoubles(in, edge.params, nCurveParams, fileSize)) { + ::Error("LoadSurfaceSolid", "%s: truncated edge record in surface %zu", file.c_str(), s); + return false; + } + } + } + + // Version 3: the face's boundary edge identities, in the sidecar's own wire order. + std::vector edgeIds; + std::vector edgeFlags; + if (version >= 3) { + uint32_t nEdgeRefs = 0; + if (!readValue(in, nEdgeRefs)) { + ::Error("LoadSurfaceSolid", "%s: truncated edge identity count of surface %zu", file.c_str(), s); + return false; + } + if (static_cast(nEdgeRefs) * 5u > bytesRemaining(in, fileSize)) { + ::Error("LoadSurfaceSolid", "%s: surface %zu claims %u edge identities, more than the file holds", + file.c_str(), s, nEdgeRefs); + return false; + } + edgeIds.resize(nEdgeRefs); + edgeFlags.resize(nEdgeRefs); + for (uint32_t e = 0; e < nEdgeRefs; ++e) { + uint32_t edgeId = 0; + uint8_t edgeFlag = 0; + if (!readValue(in, edgeId) || !readValue(in, edgeFlag)) { + ::Error("LoadSurfaceSolid", "%s: truncated edge identity %u of surface %zu", file.c_str(), e, s); + return false; + } + if (nModelEdges > 0 && edgeId >= nModelEdges) { + ::Error("LoadSurfaceSolid", "%s: surface %zu edge identity %u is %u, outside the model's %u edge(s)", + file.c_str(), s, e, edgeId, nModelEdges); + return false; + } + edgeIds[e] = edgeId; + edgeFlags[e] = edgeFlag; + } + } + + const bool innerWall = (flags & kFlagInnerWall) != 0; + const RecordMetric recordMetric{surfaceType, p.data()}; + bool added = false; + + // one quadric: check the parameter count, then add the surface untrimmed or with its trim block + const auto addQuadric = [&](const char* name, uint32_t expectedParams, const auto& addUntrimmed, + const auto& addTrimmed) { + if (nParams != expectedParams) { + ::Error("LoadSurfaceSolid", "%s: %s surface %zu has %u parameters, expected %u", file.c_str(), name, s, + nParams, expectedParams); + return false; + } + if (wires.empty()) { + added = addUntrimmed(); + return true; + } + std::vector outer; + std::vector> inners; + bool anyArc = false; // quadric domains accept both line and arc trim edges + if (!collectTrim(file, s, kQuadricWording, wires, outer, inners, anyArc, recordMetric.metric(), joinTolerance, + toleranceOrigin)) { + return false; + } + added = addTrimmed(outer, inners); + return true; + }; + + switch (surfaceType) { + case kPlane: { + if (nParams != 9) { + ::Error("LoadSurfaceSolid", "%s: plane surface %zu has %u parameters, expected 9", file.c_str(), s, nParams); + return false; + } + // Read every wire as a general line/arc loop. A pure line-segment loop keeps the + // polygon path (AddPlanarSurface, general-metric); any arc routes to the curved path. + std::vector outer; + std::vector> inners; + bool anyArc = false; + if (!collectTrim(file, s, kPlaneWording, wires, outer, inners, anyArc, recordMetric.metric(), joinTolerance, + toleranceOrigin)) { + return false; + } + if (anyArc) { + added = solid.AddCurvedPlanarSurface(point3(p, 0), point3(p, 3), point3(p, 6), outer, inners); + } else { + const auto toPolygon = [](const std::vector& curves) { + std::vector polygon; + polygon.reserve(curves.size()); + for (const auto& c : curves) { + polygon.push_back(c.lineStart); + } + return polygon; + }; + std::vector> innerPolys; + innerPolys.reserve(inners.size()); + for (const auto& inner : inners) { + innerPolys.push_back(toPolygon(inner)); + } + added = solid.AddPlanarSurface(point3(p, 0), point3(p, 3), point3(p, 6), toPolygon(outer), innerPolys); + } + break; + } + case kCylinder: + if (!addQuadric( + "cylinder", 14, + [&] { + return solid.AddCylindricalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], innerWall); + }, + [&](const auto& outer, const auto& inners) { + return solid.AddCylindricalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], innerWall, outer, inners); + })) { + return false; + } + break; + case kCone: + if (!addQuadric( + "cone", 15, + [&] { + return solid.AddConicalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], p[14], innerWall); + }, + [&](const auto& outer, const auto& inners) { + return solid.AddConicalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], p[14], innerWall, outer, inners); + })) { + return false; + } + break; + case kSphere: + if (!addQuadric( + "sphere", 14, + [&] { + return solid.AddSphericalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], innerWall); + }, + [&](const auto& outer, const auto& inners) { + return solid.AddSphericalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], innerWall, outer, inners); + })) { + return false; + } + break; + case kTorus: + if (!addQuadric( + "torus", 15, + [&] { + return solid.AddToroidalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], p[14], innerWall); + }, + [&](const auto& outer, const auto& inners) { + return solid.AddToroidalSurface(point3(p, 0), point3(p, 3), point3(p, 6), p[9], p[10], p[11], p[12], + p[13], p[14], innerWall, outer, inners); + })) { + return false; + } + break; + default: + ::Error("LoadSurfaceSolid", "%s: surface %zu has unknown surface type %u", file.c_str(), s, surfaceType); + return false; + } + + if (!added) { + ::Error("LoadSurfaceSolid", "%s: surface %zu was rejected by O2BVHSurfaceSolid", file.c_str(), s); + return false; + } + if (!edgeIds.empty()) { + reorderEdgeRefsToKernelOrder(wires, edgeIds, edgeFlags); + solid.SetSurfaceBoundaryEdges(static_cast(s), edgeIds, edgeFlags); + } + } + + return true; +} + +bool LoadFacetSolid(const std::string& file, O2Tessellated& solid) +{ + std::ifstream in(file, std::ios::binary); + if (!in) { + ::Error("LoadFacetSolid", "Cannot open facet sidecar file %s", file.c_str()); + return false; + } + + in.seekg(0, std::ios::end); + const std::streamoff fileSize = in.tellg(); + in.seekg(0, std::ios::beg); + + uint32_t nTriangles = 0; + if (!readValue(in, nTriangles)) { + ::Error("LoadFacetSolid", "%s: truncated header", file.c_str()); + return false; + } + + // one record is nine float32; the count is checked against the file before one block read + const uint64_t recordsBytes = static_cast(nTriangles) * 9u * sizeof(float); + const uint64_t remaining = bytesRemaining(in, fileSize); + if (recordsBytes > remaining) { + ::Error("LoadFacetSolid", "%s: truncated: %u facet record(s) need %llu byte(s), found %llu", file.c_str(), + nTriangles, static_cast(recordsBytes), static_cast(remaining)); + return false; + } + std::vector records(9 * static_cast(nTriangles)); + in.read(reinterpret_cast(records.data()), static_cast(recordsBytes)); + if (!in) { + ::Error("LoadFacetSolid", "%s: truncated facet records", file.c_str()); + return false; + } + + uint32_t nDegenerate = 0; + for (uint32_t i = 0; i < nTriangles; ++i) { + const float* v = &records[9 * static_cast(i)]; + const O2Tessellated::Vertex_t p0(v[0], v[1], v[2]); + const O2Tessellated::Vertex_t p1(v[3], v[4], v[5]); + const O2Tessellated::Vertex_t p2(v[6], v[7], v[8]); + if (!solid.AddFacet(p0, p1, p2)) { + // a degenerate facet is a mesh property, not a format error: count it and carry on + ++nDegenerate; + continue; + } + } + if (nDegenerate > 0) { + ::Warning("LoadFacetSolid", "%s: skipped %u degenerate facet(s) of %u", file.c_str(), nDegenerate, nTriangles); + } + + return true; +} + +bool LoadFlatCSG(const std::string& file, O2FlatCSG& solid) +{ + std::ifstream in(file, std::ios::binary); + if (!in) { + ::Error("LoadFlatCSG", "Cannot open flat-CSG sidecar file %s", file.c_str()); + return false; + } + + in.seekg(0, std::ios::end); + const std::streamoff fileSize = in.tellg(); + in.seekg(0, std::ios::beg); + + char magic[8]; + in.read(magic, sizeof(magic)); + if (!in || std::memcmp(magic, "O2FLTCSG", sizeof(magic)) != 0) { + ::Error("LoadFlatCSG", "%s is not a flat-CSG sidecar file (bad magic)", file.c_str()); + return false; + } + + uint32_t version = 0, nHalfspaces = 0, nCells = 0; + if (!readValue(in, version) || !readValue(in, nHalfspaces) || !readValue(in, nCells)) { + ::Error("LoadFlatCSG", "%s: truncated header", file.c_str()); + return false; + } + if (version != kFlatCSGVersion) { + ::Error("LoadFlatCSG", "%s: unsupported sidecar version %u (reader supports %u)", file.c_str(), version, + kFlatCSGVersion); + return false; + } + + // refuse a file whose length does not match its header before reading a record + const uint64_t expected = + static_cast(nHalfspaces) * kFlatCSGHalfspaceBytes + static_cast(nCells) * kFlatCSGCellBytes; + const uint64_t remaining = bytesRemaining(in, fileSize); + if (remaining != expected) { + ::Error("LoadFlatCSG", + "%s: file length does not match its header (%u halfspace(s) + %u cell(s) implies %llu more byte(s), " + "found %llu)", + file.c_str(), nHalfspaces, nCells, static_cast(expected), + static_cast(remaining)); + return false; + } + + // Every field below is read on its own -- see readValue's comment on why a struct-based read of + // the 100-byte halfspace record would be wrong for every record after the first. + for (uint32_t h = 0; h < nHalfspaces; ++h) { + int32_t kind = 0; + double sign = 0.; + if (!readValue(in, kind) || !readValue(in, sign)) { + ::Error("LoadFlatCSG", "%s: truncated halfspace record %u", file.c_str(), h); + return false; + } + double c[11]; + bool ok = true; + for (int i = 0; i < 11 && ok; ++i) { + ok = readValue(in, c[i]); + } + if (!ok) { + ::Error("LoadFlatCSG", "%s: truncated halfspace record %u", file.c_str(), h); + return false; + } + bool finite = std::isfinite(sign); + for (double value : c) { + finite = finite && std::isfinite(value); + } + if (!finite) { + ::Error("LoadFlatCSG", "%s: halfspace %u has a non-finite coefficient", file.c_str(), h); + return false; + } + if (kind == FlatCSGHalfspace::kQuadric) { + solid.AddQuadric(sign, c); + } else if (kind == FlatCSGHalfspace::kTorus) { + const double centre[3] = {c[0], c[1], c[2]}; + const double axis[3] = {c[3], c[4], c[5]}; + if (!(axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2] > 0.)) { + ::Error("LoadFlatCSG", "%s: torus halfspace %u has a zero axis", file.c_str(), h); + return false; + } + solid.AddTorus(sign, centre, axis, c[6], c[7]); + } else { + ::Error("LoadFlatCSG", "%s: halfspace %u has unknown kind %d", file.c_str(), h, kind); + return false; + } + } + + for (uint32_t cellIdx = 0; cellIdx < nCells; ++cellIdx) { + int32_t first = 0, count = 0; + double volume = 0.; + if (!readValue(in, first) || !readValue(in, count) || !readValue(in, volume)) { + ::Error("LoadFlatCSG", "%s: truncated cell record %u", file.c_str(), cellIdx); + return false; + } + double lo[3], hi[3]; + bool ok = true; + for (int i = 0; i < 3 && ok; ++i) { + ok = readValue(in, lo[i]); + } + for (int i = 0; i < 3 && ok; ++i) { + ok = readValue(in, hi[i]); + } + if (!ok) { + ::Error("LoadFlatCSG", "%s: truncated cell record %u", file.c_str(), cellIdx); + return false; + } + if (first < 0 || count <= 0 || static_cast(first) + count > static_cast(nHalfspaces)) { + ::Error("LoadFlatCSG", "%s: cell %u has an invalid range (first=%d, count=%d) into %u halfspace(s)", + file.c_str(), cellIdx, first, count, nHalfspaces); + return false; + } + solid.AddCell(first, count, volume); + solid.SetCellBBox(static_cast(cellIdx), lo, hi); + } + + return true; +} + +bool WriteFlatCSG(const std::string& file, const O2FlatCSG& solid) +{ + // refuse an unclosed shape: its unset cell boxes would read back as zeros and pass validation on reload + if (!solid.IsClosed()) { + ::Error("WriteFlatCSG", + "%s: shape %s is not closed (CloseShape() was never called, or refused); refusing to " + "write a sidecar that may encode a degenerate cell box", + file.c_str(), solid.GetName()); + return false; + } + + std::ofstream out(file, std::ios::binary); + if (!out) { + ::Error("WriteFlatCSG", "Cannot open %s for writing", file.c_str()); + return false; + } + + out.write("O2FLTCSG", 8); + const uint32_t version = kFlatCSGVersion; + const uint32_t nHalfspaces = static_cast(solid.GetNhalfspaces()); + const uint32_t nCells = static_cast(solid.GetNcells()); + writeValue(out, version); + writeValue(out, nHalfspaces); + writeValue(out, nCells); + + // field by field, byte-identical to the loader and to cadsupport/flat.py's writer + for (uint32_t h = 0; h < nHalfspaces; ++h) { + const FlatCSGHalfspace& halfspace = solid.GetHalfspace(static_cast(h)); + const int32_t kind = halfspace.kind; + writeValue(out, kind); + writeValue(out, halfspace.sign); + for (double value : halfspace.c) { + writeValue(out, value); + } + } + for (uint32_t cellIdx = 0; cellIdx < nCells; ++cellIdx) { + const FlatCSGCell& cell = solid.GetCell(static_cast(cellIdx)); + const int32_t first = cell.first; + const int32_t count = cell.count; + writeValue(out, first); + writeValue(out, count); + writeValue(out, cell.volume); + double lo[3], hi[3]; + solid.GetCellBBox(static_cast(cellIdx), lo, hi); + for (double value : lo) { + writeValue(out, value); + } + for (double value : hi) { + writeValue(out, value); + } + } + + if (!out) { + ::Error("WriteFlatCSG", "%s: write failed", file.c_str()); + return false; + } + return true; +} + +} // namespace cad +} // namespace o2 diff --git a/Detectors/CADSupport/test/RepresentationBench.h b/Detectors/CADSupport/test/RepresentationBench.h new file mode 100644 index 0000000000000..f60034809cb29 --- /dev/null +++ b/Detectors/CADSupport/test/RepresentationBench.h @@ -0,0 +1,576 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +/// \file RepresentationBench.h +/// \brief Per-call cost, memory and the synthetic boolean ladder: the measuring parts of the +/// representation comparison. +/// +/// Header-only, and deliberately NOT in CADSupport, for the same reason `XRayTransport.h` is +/// not: an instrument must not change the thing it measures. Nothing in the gate path or in +/// `libO2CADSupport` is rebuilt differently because this file exists. +/// +/// It is a header rather than code inside runXRayBenchmark.cxx so that the unit tests exercise +/// THE SAME timing loop, THE SAME memory probe and THE SAME ladder the benchmark reports from. A +/// test written against a second implementation of the same idea tests neither. +/// +/// THREE THINGS THIS FILE IS CAREFUL ABOUT, each bought with a known way of getting it wrong: +/// +/// 1. **One wall clock is not a measurement.** Every kernel is timed over several complete +/// passes and reported as the MEDIAN with the min/max spread beside it, never as a single +/// elapsed time. A single pass on a shared interactive machine is a sample of the machine's +/// mood as much as of the kernel. +/// 2. **The same questions, from the same sample sets, for every representation.** The point and +/// ray sets are built ONCE per part from a reference representation's own classification and +/// handed unchanged to all three. Letting each representation partition its own inside/outside +/// set would compare three different questions and call the answer a speed ratio. +/// 3. **Two memory numbers, because they answer different questions.** A STRUCTURAL count (exact, +/// derived from the shape's own counters and element sizes) and a MEASURED resident/heap delta +/// (noisy, allocator-dependent, but the only one that sees what the shape actually asked the +/// allocator for). Where they disagree the structural one is the exact statement and the +/// measured one is the honest one; both are printed. + +#ifndef ALICEO2_BASE_REPRESENTATIONBENCH_H_ +#define ALICEO2_BASE_REPRESENTATIONBENCH_H_ + +#include "CADSupport/O2SolidHarness.h" + +#include "TGeoBBox.h" +#include "TGeoBoolNode.h" +#include "TGeoCompositeShape.h" +#include "TGeoMatrix.h" +#include "TGeoShape.h" +#include "TGeoTube.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __linux__ +#include +#include +#endif + +namespace o2 +{ +namespace cad +{ +namespace bench +{ + +using o2::cad::harness::Point3D; +using o2::cad::harness::Ray; + +// ------------------------------------------------------------------------------------------ +// 1. Timing: several passes, a robust statistic, and the spread +// ------------------------------------------------------------------------------------------ + +/// The result of timing one kernel over several complete passes of the same sample set. +/// +/// `median` is the reported number and `min`/`max` are the honest error bar. The minimum is +/// singled out in the printouts as well, because on a machine with other tenants it is the +/// closest thing to the kernel's own cost: noise can only ever make a pass slower. +struct TimingStat { + long long callsPerPass = 0; + int passes = 0; + double medianNsPerCall = 0.; + double minNsPerCall = 0.; + double maxNsPerCall = 0.; + /// (max - min) / median, as a fraction. A number that is quoted with every timing rather than + /// hidden, because it is what says whether two representations that differ by 10 % differ. + double spread = 0.; + uint64_t checksum = 0; ///< accumulated from the results so the optimizer cannot elide the calls + /// Fraction of calls that returned a finite (< TGeoShape::Big()) distance. A distance kernel + /// that never hits anything is fast for a reason that has nothing to do with its speed, so this + /// travels with every ray timing. Meaningless (and left at -1) for point queries. + double hitFraction = -1.; +}; + +namespace detail +{ +/// The checksum mixer, identical in spirit to O2SolidHarness's: the timed loop must not be +/// removable by the optimizer, and a `volatile` sink costs a store per call. +inline uint64_t mix(uint64_t acc, double value) +{ + uint64_t bits = 0; + std::memcpy(&bits, &value, sizeof(bits)); + acc ^= bits + 0x9e3779b97f4a7c15ULL + (acc << 6) + (acc >> 2); + return acc; +} +} // namespace detail + +/// Time `pass()` -- one complete sweep over the sample set, returning a checksum -- over +/// `warmupPasses` untimed and `passes` timed repetitions, and report the median ns/call. +/// +/// The warmup is not decoration: the first pass over a freshly loaded shape pays for the page +/// faults of its own data and for the branch predictor's ignorance, and on the mesh +/// representation that alone was measured at more than 2x the steady-state cost. Every number +/// this function returns is therefore a WARM-CACHE number, and the caller is expected to say so. +template +TimingStat timePasses(long long callsPerPass, int warmupPasses, int passes, Pass&& pass) +{ + TimingStat stat; + stat.callsPerPass = callsPerPass; + if (callsPerPass <= 0 || passes <= 0) { + return stat; + } + for (int i = 0; i < warmupPasses; ++i) { + stat.checksum = detail::mix(stat.checksum, static_cast(pass())); + } + std::vector perPass; + perPass.reserve(passes); + for (int i = 0; i < passes; ++i) { + const auto t0 = std::chrono::steady_clock::now(); + const uint64_t sum = pass(); + const auto t1 = std::chrono::steady_clock::now(); + stat.checksum = detail::mix(stat.checksum, static_cast(sum)); + perPass.push_back(std::chrono::duration(t1 - t0).count() / + static_cast(callsPerPass)); + } + std::sort(perPass.begin(), perPass.end()); + stat.passes = passes; + stat.minNsPerCall = perPass.front(); + stat.maxNsPerCall = perPass.back(); + stat.medianNsPerCall = perPass[perPass.size() / 2]; + stat.spread = stat.medianNsPerCall > 0. + ? (stat.maxNsPerCall - stat.minNsPerCall) / stat.medianNsPerCall + : 0.; + return stat; +} + +// ------------------------------------------------------------------------------------------ +// 2. Memory: one exact number and one measured number +// ------------------------------------------------------------------------------------------ + +/// A point-in-time reading of what this process is holding. +/// +/// `residentBytes` comes from /proc/self/statm and is what the operating system sees: it includes +/// the allocator's unreturned arenas and every page the process has ever touched, so it is a +/// generous upper bound and it never goes down when a vector is freed. `heapInUseBytes` comes +/// from mallinfo2 and is what glibc believes is currently handed out to the program -- much +/// closer to the structural number and much less noisy, but blind to anything allocated outside +/// malloc. Both are reported; neither is the truth on its own. +/// +/// `uordblks` ALONE IS NOT THE HEAP. glibc services any request over M_MMAP_THRESHOLD (128 kB by +/// default) with its own mmap and books it in `hblkhd`, not in `uordblks` -- so a 64 MB +/// allocation moved this counter by exactly zero until `hblkhd` was added. That was caught by +/// this file's own negative control (`control 11` in the benchmark self-test) and it is the +/// reason the control exists: a memory column that cannot see 64 MB is not a memory column. +struct MemorySnapshot { + long long residentBytes = 0; + long long heapInUseBytes = 0; +}; + +inline MemorySnapshot readMemory() +{ + MemorySnapshot out; +#ifdef __linux__ + std::ifstream statm("/proc/self/statm"); + if (statm) { + long long totalPages = 0; + long long residentPages = 0; + statm >> totalPages >> residentPages; + out.residentBytes = residentPages * static_cast(::sysconf(_SC_PAGESIZE)); + } +#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 33)) + const struct mallinfo2 info = ::mallinfo2(); + out.heapInUseBytes = static_cast(info.uordblks) + static_cast(info.hblkhd); +#endif +#endif + return out; +} + +inline MemorySnapshot operator-(const MemorySnapshot& a, const MemorySnapshot& b) +{ + return {a.residentBytes - b.residentBytes, a.heapInUseBytes - b.heapInUseBytes}; +} + +/// The exact structural size of a representation, derived from its own counters. +/// +/// This is the number that is a property of the geometry rather than of the allocator, and it is +/// the one to quote when asking "what would N of these cost". `formula` records how it was +/// arrived at, so a reader can check it rather than trust it. +struct StructuralMemory { + long long primitives = 0; ///< triangles / analytic patches / boolean leaves + long long bytes = 0; ///< the arithmetic below, exact for the arrays it counts + long long sidecarBytes = 0; ///< the file the representation was loaded from, on disk + std::string formula; +}; + +/// Bytes a `.bin`/`.root` sidecar occupies on disk. Exact, and the one memory number that needs +/// no assumption about anybody's allocator. +inline long long fileBytes(const std::string& path) +{ + std::ifstream in(path, std::ios::binary | std::ios::ate); + return in ? static_cast(in.tellg()) : 0; +} + +// ------------------------------------------------------------------------------------------ +// 3. The sample sets -- built once per part, handed unchanged to every representation +// ------------------------------------------------------------------------------------------ + +/// The four kernels take two kinds of input and the split between inside and outside has to be +/// made by SOMETHING. It is made once, by a designated reference representation, and recorded -- +/// `partitionedBy` travels with every table this produces. Letting each representation classify +/// its own points would mean `DistFromInside` is timed on a different set for each of them, and +/// a ratio between those is not a speed comparison. +struct QuerySamples { + std::string partitionedBy; + std::vector points; ///< all query points, mixed inside/outside, in bbox order + std::vector pointIsInside; ///< the reference's Contains() for each, as a fixed label + std::vector outsideRays; ///< origin outside per the reference, aimed into the bbox + std::vector insideRays; ///< origin inside per the reference, isotropic direction + long long insidePoints = 0; +}; + +namespace detail +{ +/// A tiny explicit LCG. Not for statistical quality -- for the property that matters here, which +/// is that the sample set is a pure function of the seed and the bounding box and is therefore +/// reproducible across representations, across runs and across machines. +struct Lcg { + uint64_t state = 88172645463325252ULL; + double next() + { + state = state * 6364136223846793005ULL + 1442695040888963407ULL; + return static_cast((state >> 11) & ((1ULL << 53) - 1)) / static_cast(1ULL << 53); + } +}; +} // namespace detail + +/// Build the shared sample sets from `reference`'s own classification. +/// +/// `nPoints` points are drawn uniformly over the bounding box inflated by `inflate`, so the set +/// contains both interior and exterior points in whatever ratio the part's own fill factor gives +/// -- which is the realistic mixture a navigator sees, and is a per-part property that is +/// reported rather than forced. Rays are drawn until the requested counts are met or the attempt +/// budget runs out; outside rays are AIMED at a random point of the bounding box, because an +/// isotropically-directed ray from outside misses a thin part almost always and would time the +/// miss path exclusively. +inline QuerySamples buildQuerySamples(const TGeoShape* reference, const std::string& referenceName, + const Point3D& bboxMin, const Point3D& bboxMax, int nPoints, + int nRays, uint64_t seed = 20260802ULL, double inflate = 0.12) +{ + QuerySamples out; + out.partitionedBy = referenceName; + detail::Lcg rng{seed}; + Point3D lo{}; + Point3D hi{}; + Point3D centre{}; + for (int k = 0; k < 3; ++k) { + const double half = 0.5 * (bboxMax[k] - bboxMin[k]); + centre[k] = 0.5 * (bboxMax[k] + bboxMin[k]); + lo[k] = centre[k] - half * (1. + inflate); + hi[k] = centre[k] + half * (1. + inflate); + } + auto drawPoint = [&]() { + Point3D p{}; + for (int k = 0; k < 3; ++k) { + p[k] = lo[k] + (hi[k] - lo[k]) * rng.next(); + } + return p; + }; + out.points.reserve(nPoints); + out.pointIsInside.reserve(nPoints); + for (int i = 0; i < nPoints; ++i) { + const Point3D p = drawPoint(); + const bool in = reference->Contains(p.data()); + out.points.push_back(p); + out.pointIsInside.push_back(in ? 1 : 0); + out.insidePoints += in ? 1 : 0; + } + const int budget = 400 * std::max(1, nRays); + int attempts = 0; + while (static_cast(out.outsideRays.size()) < nRays && attempts < budget) { + ++attempts; + const Point3D p = drawPoint(); + if (reference->Contains(p.data())) { + continue; + } + // Aim at a random point of the (uninflated) bounding box: a thin part is missed by an + // isotropic direction almost always, and a DistFromOutside timing dominated by misses prices + // the early-out rather than the kernel. + Point3D target{}; + for (int k = 0; k < 3; ++k) { + target[k] = bboxMin[k] + (bboxMax[k] - bboxMin[k]) * rng.next(); + } + Point3D d{target[0] - p[0], target[1] - p[1], target[2] - p[2]}; + const double norm = std::sqrt(d[0] * d[0] + d[1] * d[1] + d[2] * d[2]); + if (!(norm > 0.)) { + continue; + } + for (int k = 0; k < 3; ++k) { + d[k] /= norm; + } + out.outsideRays.push_back({p, d}); + } + attempts = 0; + while (static_cast(out.insideRays.size()) < nRays && attempts < budget) { + ++attempts; + const Point3D p = drawPoint(); + if (!reference->Contains(p.data())) { + continue; + } + const double z = 2. * rng.next() - 1.; + const double phi = 2. * 3.14159265358979323846 * rng.next(); + const double r = std::sqrt(std::max(0., 1. - z * z)); + out.insideRays.push_back({p, {r * std::cos(phi), r * std::sin(phi), z}}); + } + return out; +} + +// ------------------------------------------------------------------------------------------ +// 4. The four kernel passes +// ------------------------------------------------------------------------------------------ +// +// Each is a closure over the shared sample set that performs exactly `callsPerPass` virtual calls +// and returns a checksum. They exist as named functions rather than as lambdas at the call site +// so that the unit tests time the same loop bodies the benchmark does. + +inline TimingStat timeContainsPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes) +{ + return timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& p : s.points) { + acc = detail::mix(acc, shape->Contains(p.data()) ? 1. : 0.); + } + return acc; + }); +} + +/// Safety is asked with the FIXED label from the reference partition, not with each shape's own +/// Contains(). Two reasons, and the second is the one that matters: `Safety(p, in)` takes +/// different branches for in/out on every implementation here, so a shape that disagreed about a +/// point would be timed on a different branch; and asking each shape's own Contains() first would +/// price two kernels and call it one. +inline TimingStat timeSafetyPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes) +{ + return timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (size_t i = 0; i < s.points.size(); ++i) { + acc = detail::mix(acc, shape->Safety(s.points[i].data(), s.pointIsInside[i] ? kTRUE : kFALSE)); + } + return acc; + }); +} + +inline TimingStat timeDistOutPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes) +{ + long long hits = 0; + long long calls = 0; + TimingStat stat = timePasses(static_cast(s.outsideRays.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& ray : s.outsideRays) { + const double d = shape->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr); + hits += (d < TGeoShape::Big()) ? 1 : 0; + ++calls; + acc = detail::mix(acc, d); + } + return acc; + }); + stat.hitFraction = calls > 0 ? static_cast(hits) / static_cast(calls) : -1.; + return stat; +} + +inline TimingStat timeDistInPass(const TGeoShape* shape, const QuerySamples& s, int warmup, int passes) +{ + long long hits = 0; + long long calls = 0; + TimingStat stat = timePasses(static_cast(s.insideRays.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& ray : s.insideRays) { + const double d = shape->DistFromInside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr); + hits += (d < TGeoShape::Big()) ? 1 : 0; + ++calls; + acc = detail::mix(acc, d); + } + return acc; + }); + stat.hitFraction = calls > 0 ? static_cast(hits) / static_cast(calls) : -1.; + return stat; +} + +// ------------------------------------------------------------------------------------------ +// 5. The synthetic boolean ladder +// ------------------------------------------------------------------------------------------ +// +// Every genuine boolean in the corpus today is a 2-leaf union of two TGeoTubes, so the corpus +// cannot say how a composite scales with leaf count +// and no amount of running it harder will make it. This builds the missing fixture: unions of +// 2, 4, 8, ... TGeoTubes, in the two tree shapes an emitter can plausibly produce. +// +// The two shapes are the point of the experiment. +// * CHAIN -- (((t0 + t1) + t2) + t3) ... : depth K-1, the natural output of a fold over a +// list of leaves. Every query descends the whole spine. +// * BALANCED-- a complete binary tree of depth ceil(log2 K). This is what a BVH over primitives +// would give you for free, minus the bounding-box rejection. +// If the two scale the same way, tree shape is not where the cost is and a BVH-over-primitives +// CSG solid has nothing to win from restructuring alone. If they separate, the gap IS the prize. + +enum class LadderShape { Chain, + Balanced }; + +/// One rung of the ladder: `leaves` overlapping tubes on a line, unioned in the requested shape. +/// +/// Overlapping rather than disjoint, deliberately: the corpus's booleans are two COAXIAL tubes +/// with shared interior, and a union of disjoint bodies is an easier question (a point is inside +/// at most one leaf, so a short-circuiting evaluator stops early on every interior query). The +/// pitch of 0.8 against a radius of 0.5 gives every leaf a genuine overlap with its neighbour. +/// +/// Returns a shape owned by the current gGeoManager, like every other TGeoShape. +inline TGeoShape* buildBooleanLadder(int leaves, LadderShape shape, const std::string& tag) +{ + if (leaves < 1) { + return nullptr; + } + const double rMin = 0.2; + const double rMax = 0.5; + const double dz = 1.0; + const double pitch = 0.8; + auto leafName = [&](int i) { return tag + "_leaf" + std::to_string(i); }; + std::vector nodes; + std::vector offsets; + for (int i = 0; i < leaves; ++i) { + auto* tube = new TGeoTube(leafName(i).c_str(), rMin, rMax, dz); + nodes.push_back(tube); + auto* m = new TGeoTranslation((i - 0.5 * (leaves - 1)) * pitch, 0., 0.); + m->SetName((leafName(i) + "_m").c_str()); + m->RegisterYourself(); + offsets.push_back(m); + } + if (leaves == 1) { + return nodes.front(); + } + int serial = 0; + auto join = [&](TGeoShape* a, TGeoMatrix* ma, TGeoShape* b, TGeoMatrix* mb) -> TGeoShape* { + auto* node = new TGeoUnion(a, b, ma, mb); + auto* composite = new TGeoCompositeShape((tag + "_u" + std::to_string(serial++)).c_str(), node); + return composite; + }; + if (shape == LadderShape::Chain) { + TGeoShape* acc = nodes[0]; + TGeoMatrix* accMatrix = offsets[0]; + for (int i = 1; i < leaves; ++i) { + acc = join(acc, accMatrix, nodes[i], offsets[i]); + accMatrix = nullptr; // the accumulated composite is already in the common frame + } + return acc; + } + std::vector level = nodes; + std::vector levelMatrix = offsets; + while (level.size() > 1) { + std::vector next; + std::vector nextMatrix; + for (size_t i = 0; i < level.size(); i += 2) { + if (i + 1 < level.size()) { + next.push_back(join(level[i], levelMatrix[i], level[i + 1], levelMatrix[i + 1])); + nextMatrix.push_back(nullptr); + } else { + next.push_back(level[i]); + nextMatrix.push_back(levelMatrix[i]); + } + } + level.swap(next); + levelMatrix.swap(nextMatrix); + } + return level.front(); +} + +/// Walk a shape's boolean tree and count leaves, internal nodes and depth. +/// +/// The structural memory number for a composite, and the control the ladder's own self-test +/// checks: a fixture that claims 32 leaves and holds 16 is not a scaling experiment. +struct BooleanTreeStats { + long long leaves = 0; + long long nodes = 0; ///< TGeoCompositeShape / TGeoBoolNode pairs + int depth = 0; +}; + +inline BooleanTreeStats booleanTreeStats(const TGeoShape* shape) +{ + BooleanTreeStats out; + const auto* composite = dynamic_cast(shape); + if (composite == nullptr || composite->GetBoolNode() == nullptr) { + out.leaves = 1; + out.depth = 1; + return out; + } + const TGeoBoolNode* node = composite->GetBoolNode(); + const BooleanTreeStats left = booleanTreeStats(node->GetLeftShape()); + const BooleanTreeStats right = booleanTreeStats(node->GetRightShape()); + out.leaves = left.leaves + right.leaves; + out.nodes = left.nodes + right.nodes + 1; + out.depth = 1 + std::max(left.depth, right.depth); + return out; +} + +// ------------------------------------------------------------------------------------------ +// 6. The negative control for the timing harness itself +// ------------------------------------------------------------------------------------------ + +/// A TGeoBBox that is deliberately slower than a TGeoBBox, by a controllable amount. +/// +/// The timing harness's negative control: every kernel timed here is also timed against this, +/// and the ratio must exceed a stated factor. +/// +/// The burn loop is a data dependency on the point, so it cannot be hoisted or constant-folded, +/// and its result is folded into the returned value so it cannot be dropped. +class BallastShape : public TGeoBBox +{ + public: + BallastShape(const char* name, double dx, double dy, double dz, int burn) + : TGeoBBox(name, dx, dy, dz), mBurn(burn) {} + + double ballast(const double* point) const + { + double acc = 1.; + for (int i = 0; i < mBurn; ++i) { + acc = std::sqrt(acc * acc + point[i % 3] * point[i % 3] + 1.); + } + return acc; + } + + bool Contains(const double* point) const override + { + return TGeoBBox::Contains(point) && ballast(point) > 0.; + } + double Safety(const double* point, bool in = kTRUE) const override + { + return TGeoBBox::Safety(point, in) + 0. * ballast(point); + } + double DistFromOutside(const double* point, const double* dir, int iact = 1, + double step = TGeoShape::Big(), double* safe = nullptr) const override + { + return TGeoBBox::DistFromOutside(point, dir, iact, step, safe) + 0. * ballast(point); + } + double DistFromInside(const double* point, const double* dir, int iact = 1, + double step = TGeoShape::Big(), double* safe = nullptr) const override + { + return TGeoBBox::DistFromInside(point, dir, iact, step, safe) + 0. * ballast(point); + } + + private: + int mBurn = 0; +}; + +} // namespace bench +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/test/XRayTransport.h b/Detectors/CADSupport/test/XRayTransport.h new file mode 100644 index 0000000000000..b195f1adac9a7 --- /dev/null +++ b/Detectors/CADSupport/test/XRayTransport.h @@ -0,0 +1,573 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +/// \file XRayTransport.h +/// \brief The X-ray transport benchmark's algorithms: stepping, auditing and comparing ordered +/// crossing lists. +/// +/// Header-only, and deliberately NOT in CADSupport: this is a measuring instrument, and the +/// project's rule is that an instrument must not change the thing it measures. Putting it here +/// means `o2-bench-cadsupport-solid-harness` and the oracle gate are untouched -- not one +/// object file of the existing path is rebuilt differently. +/// +/// It is a header rather than code inside runXRayBenchmark.cxx for one reason: the unit tests in +/// testBVHSurfaceSolid.cxx must exercise THE SAME stepping loop and THE SAME comparator that the +/// benchmark runs. A test against a second implementation of the same idea tests neither. + +#ifndef ALICEO2_BASE_XRAYTRANSPORT_H_ +#define ALICEO2_BASE_XRAYTRANSPORT_H_ + +#include "CADSupport/O2SolidHarness.h" + +#include "TGeoShape.h" + +#include +#include +#include +#include +#include + +namespace o2 +{ +namespace cad +{ +namespace xray +{ + +using o2::cad::harness::Point3D; + +/// A single boundary crossing along a ray: the distance from the ray origin and whether the ray +/// is entering (+1) or leaving (-1) the solid there. +struct Crossing { + double t = 0.; + int kind = 0; +}; + +/// Everything a transport loop can do wrong that a single-shot distance query cannot express. +/// Every counter here is a *count of events*, never a rate, so two runs can be added. +struct Robustness { + long long rays = 0; + long long raysWithCrossings = 0; + long long crossings = 0; + long long steps = 0; + long long zeroLengthSteps = 0; ///< a step at or below `zeroStep` (default 1e-9 cm) + long long nonAdvancingSteps = 0; ///< the accumulated distance did not increase + long long unstickPushes = 0; ///< a stalled step that had to be nudged to continue + long long iterationCapHits = 0; ///< the loop hit `maxIter` without leaving the window + long long unterminated = 0; ///< the ray ended INSIDE the solid: entered and never left + long long oddCrossingLists = 0; ///< odd number of crossings (the same event, counted as the + ///< brief names it; equal to `unterminated` by construction + ///< in mode (a) and an independent number in mode (b)) + long long nonAlternating = 0; ///< two consecutive crossings of the same kind + long long duplicateCrossings = 0; ///< two crossings closer together than the match tolerance + /// A parity mismatch whose midpoint is within the match tolerance of the boundary: excused, + /// counted, and never folded into `parityMismatchIntervals`. Same principle as the sample + /// gate's `nNoVerdict`. + long long parityMismatchNearBoundary = 0; + long long parityMismatchIntervals = 0; ///< Contains() at an interval midpoint contradicts the + ///< in/out state the crossing list implies. This is the + ///< one check that is INDEPENDENT of the stepping: the + ///< list alternates by construction in both modes, so + ///< without it "non-alternating" could never fire. + long long originInside = 0; ///< a raster ray whose origin was not outside the solid + long long boundaryWithoutTransition = 0; ///< mode (b): a boundary was crossed but the volume did + ///< not change (a re-entry into the same volume) + /// mode (b): the ray origin was not inside the navigator's world at all, so the transport never + /// started; counted apart so a misconfigured world is not mistaken for a geometry defect. + long long originOutsideWorld = 0; + double insideLength = 0.; ///< summed inside-segment length, cm (the chord integral) + double seconds = 0.; +}; + +struct StepConfig { + /// Distance the point is advanced *past* a found crossing before the next query. This is the + /// crux of a transport loop: land exactly on a face and the next query re-finds the same + /// crossing at zero. Default 1e-9 cm = the kernel's own kRayTolerance, so the recorded crossing + /// distances carry a known bias of at most (k-1) * push over a k-crossing ray, i.e. below 1e-8 + /// cm -- two orders under the 1e-6 cm comparison band. + double push = 1.e-9; + /// A step at or below this is a stall, not progress. + double zeroStep = 1.e-9; + /// What a stalled loop is nudged by to continue, mirroring what a navigator has to do. Every + /// use is counted (`unstickPushes`): it is a repair, and a repair that is not counted is a lie. + double unstickPush = 1.e-6; + int maxIter = 512; + /// Crossing-list match tolerance, cm. Set from the model's own declared tolerance where the + /// oracle supplies one, floored here. + double matchTolerance = 1.e-6; +}; + +// ------------------------------------------------------------------------------------------ +// Mode (a): the direct shape-API stepping loop +// ------------------------------------------------------------------------------------------ +// +// Contains() to establish the starting state, then alternating DistFromOutside/DistFromInside, +// advancing the point along the ray, until the accumulated distance leaves the raster window. +// `stepmax` is deliberately NOT used to bound the query: its semantics differ between shape +// implementations (some return the crossing, some return stepmax, some return Big), and this loop +// must be a measurement of the crossing list rather than of that convention. The window is +// enforced on the returned crossing distance instead. + +/// The stepping loop, parameterised on the three kernels it calls. +/// +/// Templated so the SAME loop can be driven by `O2BVHSurfaceSolid`'s BVH entry points and by its +/// non-BVH `_Loop` twins. That turns the project's existing single-query "BVH == _Loop" guard into +/// a transport-level one: every query after the first starts from a point the previous query put +/// on a boundary, so a traversal-order difference that is invisible on an isolated query can still +/// send the two down different sequences of states. +template +std::vector stepCrossingsWithKernels(const Point3D& origin, const Point3D& dir, + double tMax, const StepConfig& cfg, + Robustness& stats, ContainsFn contains, + DistOutFn distFromOutside, DistInFn distFromInside) +{ + std::vector crossings; + double point[3] = {origin[0], origin[1], origin[2]}; + bool inside = contains(point); + if (inside) { + ++stats.originInside; + } + double t = 0.; + int iter = 0; + for (; iter < cfg.maxIter; ++iter) { + const double step = inside ? distFromInside(point, dir.data()) : distFromOutside(point, dir.data()); + ++stats.steps; + if (!(step < TGeoShape::Big())) { + break; // no further crossing along this ray + } + const double tCross = t + step; + if (tCross > tMax) { + break; // beyond the raster window: not this ray's business + } + if (step <= cfg.zeroStep) { + ++stats.zeroLengthSteps; + } + crossings.push_back({tCross, inside ? -1 : +1}); + inside = !inside; + double advance = step + cfg.push; + if (!(advance > 0.)) { + ++stats.nonAdvancingSteps; + advance = cfg.unstickPush; + ++stats.unstickPushes; + } else if (step <= cfg.zeroStep) { + advance = step + cfg.unstickPush; + ++stats.unstickPushes; + } + t += advance; + if (t > tMax) { + break; + } + for (int k = 0; k < 3; ++k) { + point[k] = origin[k] + t * dir[k]; + } + } + if (iter >= cfg.maxIter) { + ++stats.iterationCapHits; + } + if (inside) { + ++stats.unterminated; + } + return crossings; +} + +/// Mode (a): the same loop driven by the ordinary TGeoShape virtuals. +inline std::vector stepWithShapeApi(const TGeoShape* shape, const Point3D& origin, + const Point3D& dir, double tMax, + const StepConfig& cfg, Robustness& stats) +{ + return stepCrossingsWithKernels( + origin, dir, tMax, cfg, stats, [shape](const double* p) { return shape->Contains(p); }, + [shape](const double* p, const double* d) { + return shape->DistFromOutside(p, d, 3, TGeoShape::Big(), nullptr); + }, + [shape](const double* p, const double* d) { + return shape->DistFromInside(p, d, 3, TGeoShape::Big(), nullptr); + }); +} + +/// Book the per-ray consistency properties of one crossing list. Split out because both modes and +/// the oracle's own answer go through it, so a defect in one cannot be excused by a different +/// bookkeeping in another. +inline void auditCrossingList(const std::vector& crossings, const TGeoShape* shape, + const Point3D& origin, const Point3D& dir, double tMax, + const StepConfig& cfg, Robustness& stats) +{ + ++stats.rays; + stats.crossings += static_cast(crossings.size()); + if (!crossings.empty()) { + ++stats.raysWithCrossings; + } + if (crossings.size() % 2 != 0) { + ++stats.oddCrossingLists; + } + for (size_t i = 1; i < crossings.size(); ++i) { + if (crossings[i].kind == crossings[i - 1].kind) { + ++stats.nonAlternating; + } + if (std::fabs(crossings[i].t - crossings[i - 1].t) <= cfg.matchTolerance) { + ++stats.duplicateCrossings; + } + } + // The inside-segment length: the chord integral's contribution from this ray. + for (size_t i = 0; i + 1 < crossings.size(); i += 2) { + if (crossings[i].kind == +1 && crossings[i + 1].kind == -1) { + stats.insideLength += crossings[i + 1].t - crossings[i].t; + } + } + // The independent check. Both stepping modes produce an alternating list *by construction*, so + // `nonAlternating` above can never fire on them; asking the shape's own Contains() at the + // midpoint of every interval is the only way this instrument can contradict itself. + if (shape != nullptr) { + std::vector edges; + edges.push_back(0.); + for (const auto& c : crossings) { + edges.push_back(c.t); + } + edges.push_back(tMax); + bool expectInside = false; + for (size_t i = 0; i + 1 < edges.size(); ++i) { + const double mid = 0.5 * (edges[i] + edges[i + 1]); + if (edges[i + 1] - edges[i] > 8. * cfg.matchTolerance) { + double p[3]; + for (int k = 0; k < 3; ++k) { + p[k] = origin[k] + mid * dir[k]; + } + const bool actuallyInside = shape->Contains(p); + if (actuallyInside != expectInside) { + // Classify before counting. A midpoint within the match tolerance of the boundary has no + // defined answer on either side, exactly as the sample gate's `nNoVerdict` points do; + // counting it as a contradiction would manufacture defects out of near-tangency. + // Safety() is only paid for on a mismatch, which is rare. + // Safety() must be asked with the state the shape ITSELF reports; asking it with the + // state the crossing list expects makes a plain outside point look like a boundary + // point (TGeoBBox::Safety(p, in=true) goes negative there) and silently excuses every + // real contradiction. That mistake made this counter read 0 on a deliberately + // truncated list. + if (shape->Safety(p, actuallyInside ? kTRUE : kFALSE) <= cfg.matchTolerance) { + ++stats.parityMismatchNearBoundary; + } else { + ++stats.parityMismatchIntervals; + } + } + } + expectInside = !expectInside; + } + } +} + +/// How two crossing lists differ, with LOST and DISPLACED kept apart. +/// +/// That separation is the whole localising value of comparing lists rather than aggregates. A +/// crossing the candidate never found is a wall a track walks through; a crossing it found half a +/// millimetre late is a step length that is slightly wrong. Both are defects, they have completely +/// different consequences for transport, and a single "disagreements" count merges them. +struct ListComparison { + long long rays = 0; + long long raysIdentical = 0; ///< the whole ordered list matched, position and sense + long long raysStructural = 0; ///< the lists have different lengths or senses + long long matched = 0; + long long displaced = 0; ///< same position in both lists, more than `tolerance` apart + long long missing = 0; ///< in the reference, absent from the candidate + long long extra = 0; ///< in the candidate, absent from the reference + long long kindMismatch = 0; + double worstDeltaT = 0.; ///< max |dt| over positionally matched crossings, cm + Point3D worstOrigin{}; + Point3D worstDir{}; + std::string worstReason; +}; + +inline void compareLists(const std::vector& candidate, const std::vector& reference, + const Point3D& origin, const Point3D& dir, double tolerance, ListComparison& out) +{ + ++out.rays; + bool sameShape = candidate.size() == reference.size(); + for (size_t i = 0; sameShape && i < candidate.size(); ++i) { + sameShape = candidate[i].kind == reference[i].kind; + } + if (sameShape) { + // Same number of crossings in the same order with the same senses: every difference is a + // position, so report the positions and never manufacture a missing/extra pair out of one + // displaced crossing. + bool identical = true; + for (size_t i = 0; i < candidate.size(); ++i) { + const double delta = std::fabs(candidate[i].t - reference[i].t); + ++out.matched; + if (delta > tolerance) { + ++out.displaced; + identical = false; + } + if (delta > out.worstDeltaT) { + out.worstDeltaT = delta; + out.worstOrigin = origin; + out.worstDir = dir; + out.worstReason = delta > tolerance ? "displaced crossing" : "deltaT"; + } + } + out.raysIdentical += identical; + return; + } + + // Structurally different: walk both lists and attribute each unpaired crossing to the side it + // came from. This is the branch that names a LOST wall. + ++out.raysStructural; + size_t i = 0; + size_t j = 0; + while (i < candidate.size() && j < reference.size()) { + const double delta = candidate[i].t - reference[j].t; + if (std::fabs(delta) <= tolerance) { + ++out.matched; + if (candidate[i].kind != reference[j].kind) { + ++out.kindMismatch; + } + if (std::fabs(delta) > out.worstDeltaT) { + out.worstDeltaT = std::fabs(delta); + } + ++i; + ++j; + } else if (delta < 0.) { + ++out.extra; + ++i; + } else { + ++out.missing; + ++j; + } + } + out.extra += static_cast(candidate.size() - i); + out.missing += static_cast(reference.size() - j); + if (out.worstReason.empty() || out.worstReason == "deltaT" || + out.worstReason == "displaced crossing") { + out.worstOrigin = origin; + out.worstDir = dir; + out.worstReason = reference.size() > candidate.size() ? "MISSING crossing" : "EXTRA crossing"; + } +} + +struct RayDef { + Point3D origin{}; + Point3D dir{}; + double tMax = 0.; + int beam = 0; ///< index into Raster::beams +}; + +/// One parallel beam: a direction and the orthonormal frame the lattice is laid out in. +/// +/// Beams are directions, not axes, because a tilted beam produces generic ray/surface +/// configurations that an axis-aligned beam misses. +struct Beam { + Point3D dir{}; + Point3D u{}; + Point3D v{}; + std::string label; +}; + +struct Raster { + int n = 0; + std::vector beams; + std::vector cellArea; ///< per beam, cm^2 + /// Fractional excess of the raster window's cross-section over the part's own bounding box, + /// per beam. Not decoration: at finite N a window wider than the silhouette biases the chord + /// integral UPWARD by about this much, because the cells straddling the silhouette are counted + /// whole. It is reported next to every volume so the systematic is never invisible. + std::vector windowExcess; + std::vector rays; + double transverseMargin = 0.; + Point3D windowMin{}; ///< the part bbox plus the margin, in world coordinates (the world box) + Point3D windowMax{}; +}; + +inline double dot3(const Point3D& a, const Point3D& b) +{ + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +} + +inline Point3D normalize3(const Point3D& a) +{ + const double norm = std::sqrt(dot3(a, a)); + return {a[0] / norm, a[1] / norm, a[2] / norm}; +} + +inline Point3D cross3(const Point3D& a, const Point3D& b) +{ + return {a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]}; +} + +/// The beams for `axesSpec` (a subset of x, y, z), each tilted by `tiltDegrees`. +/// +/// At tilt 0 the frame is exactly the two remaining coordinate axes, so an axis-aligned box's +/// chord integral stays EXACT (every cell centre is inside its own bounding box; see buildRaster). +/// A non-zero tilt rotates the beam by `tilt` about one transverse axis and by 0.618 * tilt about +/// the other -- an irrational-looking ratio on purpose, so no beam lands on a symmetry plane of a +/// part that was drawn on a coordinate grid. +/// `count` beams spread over the sphere by the Fibonacci spiral, deterministic and seed-free. +/// +/// This exists because of a measurement, not for completeness. A parallel-beam raster is +/// DIRECTION-POOR: three axes (or three tilted axes) are three directions, however many rays are +/// fired. The known torus quartic defect fires on a configuration that depends on the ray +/// DIRECTION, so it is invisible to a 3-beam raster of 27648 rays and visible to a fan of many +/// directions. Impact-parameter density and direction density are different resolutions and a +/// benchmark that only has the first will report a clean sheet on a defect it cannot see. +inline std::vector buildFanBeams(int count) +{ + std::vector beams; + const double golden = 3.14159265358979323846 * (3. - std::sqrt(5.)); + for (int i = 0; i < count; ++i) { + // Only the upper hemisphere is needed: a beam and its reverse sample the same lines. + const double z = (count == 1) ? 1. : 1. - static_cast(i) / static_cast(count); + const double radius = std::sqrt(std::max(0., 1. - z * z)); + const double theta = golden * i; + Beam beam; + beam.dir = normalize3({radius * std::cos(theta), radius * std::sin(theta), z}); + // A transverse frame: Gram-Schmidt off whichever axis the beam is least aligned with. + int least = 0; + for (int k = 1; k < 3; ++k) { + if (std::fabs(beam.dir[k]) < std::fabs(beam.dir[least])) { + least = k; + } + } + Point3D seed{}; + seed[least] = 1.; + const double projection = dot3(seed, beam.dir); + beam.u = normalize3({seed[0] - projection * beam.dir[0], seed[1] - projection * beam.dir[1], + seed[2] - projection * beam.dir[2]}); + beam.v = cross3(beam.dir, beam.u); + beam.label = "f" + std::to_string(i); + beams.push_back(std::move(beam)); + } + return beams; +} + +inline std::vector buildBeams(const std::string& axesSpec, double tiltDegrees) +{ + std::vector beams; + const double t = std::tan(tiltDegrees * 3.14159265358979323846 / 180.); + for (const char c : axesSpec) { + int axis = -1; + if (c == 'x' || c == 'X') { + axis = 0; + } else if (c == 'y' || c == 'Y') { + axis = 1; + } else if (c == 'z' || c == 'Z') { + axis = 2; + } else { + continue; + } + const int iu = (axis + 1) % 3; + const int iv = (axis + 2) % 3; + Point3D w{}; + Point3D u{}; + Point3D v{}; + w[axis] = 1.; + u[iu] = 1.; + v[iv] = 1.; + Beam beam; + if (t == 0.) { + beam.dir = w; + beam.u = u; + beam.v = v; + beam.label = std::string(1, "xyz"[axis]); + } else { + Point3D dir{w[0] + t * u[0] + 0.618 * t * v[0], w[1] + t * u[1] + 0.618 * t * v[1], + w[2] + t * u[2] + 0.618 * t * v[2]}; + beam.dir = normalize3(dir); + // Gram-Schmidt the transverse frame off the original in-plane axis. + Point3D uu{u[0] - dot3(u, beam.dir) * beam.dir[0], u[1] - dot3(u, beam.dir) * beam.dir[1], + u[2] - dot3(u, beam.dir) * beam.dir[2]}; + beam.u = normalize3(uu); + beam.v = cross3(beam.dir, beam.u); + beam.label = std::string(1, "xyz"[axis]) + "+t"; + } + beams.push_back(std::move(beam)); + } + return beams; +} + +/// The transverse window and the longitudinal start are deliberately DECOUPLED. +/// +/// Transverse: the window is the bounding box's own extent IN THE BEAM'S FRAME plus +/// `transverseMargin`, which should be as small as the bounding box's reliability allows, because +/// the window excess is a first-order systematic on the volume. +/// +/// Longitudinal: the ray must start strictly OUTSIDE the solid, or Contains() at the origin is a +/// coin toss on the face and the whole transport starts in the wrong state. That margin is +/// therefore generous and costs nothing -- it is along the ray, not across it. +inline Raster buildRaster(const Point3D& bboxMin, const Point3D& bboxMax, int n, + const std::vector& beams, double transverseMargin) +{ + Raster raster; + raster.n = n; + raster.beams = beams; + raster.transverseMargin = transverseMargin; + for (int k = 0; k < 3; ++k) { + raster.windowMin[k] = bboxMin[k] - transverseMargin; + raster.windowMax[k] = bboxMax[k] + transverseMargin; + } + for (const auto& beam : beams) { + // Project the eight bounding-box corners into the beam frame; the window is their extent. + double lo[3] = {1.e300, 1.e300, 1.e300}; + double hi[3] = {-1.e300, -1.e300, -1.e300}; + for (int corner = 0; corner < 8; ++corner) { + const Point3D p{(corner & 1) ? bboxMax[0] : bboxMin[0], (corner & 2) ? bboxMax[1] : bboxMin[1], + (corner & 4) ? bboxMax[2] : bboxMin[2]}; + const double coordinate[3] = {dot3(p, beam.u), dot3(p, beam.v), dot3(p, beam.dir)}; + for (int k = 0; k < 3; ++k) { + lo[k] = std::min(lo[k], coordinate[k]); + hi[k] = std::max(hi[k], coordinate[k]); + } + } + const double uLo = lo[0] - transverseMargin; + const double vLo = lo[1] - transverseMargin; + const double du = (hi[0] - lo[0] + 2. * transverseMargin) / n; + const double dv = (hi[1] - lo[1] + 2. * transverseMargin) / n; + raster.cellArea.push_back(du * dv); + const double bboxArea = (hi[0] - lo[0]) * (hi[1] - lo[1]); + raster.windowExcess.push_back(bboxArea > 0. ? (du * dv * n * n) / bboxArea - 1. : 0.); + const double extent = hi[2] - lo[2]; + const double lead = 0.05 * extent + 1.e-3; + const double wStart = lo[2] - lead; + const int index = static_cast(raster.cellArea.size()) - 1; + for (int i = 0; i < n; ++i) { + for (int j = 0; j < n; ++j) { + const double uu = uLo + (i + 0.5) * du; + const double vv = vLo + (j + 0.5) * dv; + RayDef ray; + ray.beam = index; + for (int k = 0; k < 3; ++k) { + ray.origin[k] = uu * beam.u[k] + vv * beam.v[k] + wStart * beam.dir[k]; + ray.dir[k] = beam.dir[k]; + } + ray.tMax = extent + 2. * lead; + raster.rays.push_back(ray); + } + } + } + return raster; +} + +/// Each beam is an independent estimate of the same volume; the reported number is their mean and +/// the per-beam spread is the honest error bar. +inline double chordVolume(const Raster& raster, const std::vector& insideLengthPerBeam) +{ + double sum = 0.; + size_t used = 0; + for (size_t i = 0; i < raster.beams.size() && i < insideLengthPerBeam.size(); ++i) { + sum += insideLengthPerBeam[i] * raster.cellArea[i]; + ++used; + } + return used > 0 ? sum / static_cast(used) : 0.; +} + +} // namespace xray +} // namespace cad +} // namespace o2 + +#endif diff --git a/Detectors/CADSupport/test/checkSurfaceSidecars.macro b/Detectors/CADSupport/test/checkSurfaceSidecars.macro new file mode 100644 index 0000000000000..9411425231cc8 --- /dev/null +++ b/Detectors/CADSupport/test/checkSurfaceSidecars.macro @@ -0,0 +1,100 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +/// \file checkSurfaceSidecars.macro +/// \brief Load every surfaces_*.bin sidecar in a directory and report its health. +/// +/// Companion to `O2_CADtoTGeo.py --exact-surfaces auto`, which writes one +/// `surfaces__.bin` sidecar per exactly-converted leaf solid. Extraction succeeding +/// does NOT imply the sidecar loads -- e.g. `LoadSurfaceSolid` can still reject one on a wire +/// edge-join tolerance. Run this after any conversion sweep to get the honest +/// "extracted vs. usable" number. +/// +/// Reports per sidecar: surface count, `IsClosed()`, `IsOrientationConsistent()` and `Capacity()`. +/// Note `LoadSurfaceSolid` does NOT call `CloseShape()`; this macro does, which is what populates +/// the closure diagnostics (and emits any CloseShape warnings). +/// +/// Usage: +/// root -l -b -q 'checkSurfaceSidecars.macro("/path/to/conversion/output")' +/// +/// IMPORTANT: `alienv O2/latest` resolves libO2CADSupport from the *installed* prefix. After an +/// incremental `ninja` build, point the loader at the build output first, or this silently checks +/// the old code: +/// export LD_LIBRARY_PATH=/O2-latest/O2/stage/lib64:$LD_LIBRARY_PATH + +R__ADD_INCLUDE_PATH($O2_ROOT / include) +R__LOAD_LIBRARY(libO2CADSupport) + +#include "CADSupport/O2BVHSurfaceSolid.h" +#include +#include +#include +#include +#include + +// O2SurfaceSolidIO.h is not part of the ROOT dictionary, so textual inclusion fails in interpreted +// mode. Declare the one entry point we need instead (works interpreted and compiled). +namespace o2 +{ +namespace cad +{ +bool LoadSurfaceSolid(const std::string& file, O2BVHSurfaceSolid& solid); +} +} // namespace o2 + +void checkSurfaceSidecars(const char* dir) +{ + void* handle = gSystem->OpenDirectory(dir); + if (handle == nullptr) { + printf("cannot open directory %s\n", dir); + return; + } + std::vector files; + const char* entry = nullptr; + while ((entry = gSystem->GetDirEntry(handle)) != nullptr) { + std::string name(entry); + if (name.rfind("surfaces_", 0) == 0 && name.size() > 4 && + name.substr(name.size() - 4) == ".bin") { + files.push_back(name); + } + } + gSystem->FreeDirectory(handle); + std::sort(files.begin(), files.end()); + + int nOk = 0, nBad = 0, nNotClosed = 0, nBadOrientation = 0; + for (const auto& file : files) { + o2::cad::O2BVHSurfaceSolid solid(file.c_str()); + const std::string path = std::string(dir) + "/" + file; + if (!o2::cad::LoadSurfaceSolid(path, solid)) { + printf("FAIL %-52s LoadSurfaceSolid rejected the sidecar\n", file.c_str()); + ++nBad; + continue; + } + solid.CloseShape(); + const bool closed = solid.IsClosed(); + const bool oriented = solid.IsOrientationConsistent(); + nNotClosed += closed ? 0 : 1; + nBadOrientation += oriented ? 0 : 1; + printf("OK %-52s surfaces=%5d closed=%d orient=%d capacity=%.6g\n", + file.c_str(), solid.GetNsurfaces(), static_cast(closed), + static_cast(oriented), solid.Capacity()); + ++nOk; + } + + printf("\nSUMMARY %s\n", dir); + printf(" sidecars found : %d\n", static_cast(files.size())); + printf(" loaded : %d\n", nOk); + printf(" rejected by the reader : %d\n", nBad); + printf(" loaded but not IsClosed() : %d\n", nNotClosed); + printf(" orientation inconsistent : %d\n", nBadOrientation); +} diff --git a/Detectors/CADSupport/test/runOverlapCensus.cxx b/Detectors/CADSupport/test/runOverlapCensus.cxx new file mode 100644 index 0000000000000..ba06b846bf6a2 --- /dev/null +++ b/Detectors/CADSupport/test/runOverlapCensus.cxx @@ -0,0 +1,560 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +/// \file runOverlapCensus.cxx +/// \brief "Is this assembly legal?", as a routine run rather than a bespoke investigation. +/// +/// Built as `o2-bench-cadsupport-overlap`. +/// +/// Takes any TGeo geometry file and answers, pair by pair and by name, whether the placed solids +/// compose into a world TGeo and Geant4 will accept -- separating the pairs that *share a face* +/// (legal, and the normal state of an assembly) from the pairs that *interpenetrate* (illegal, and +/// silently wrong transport). `--inject` translates one node first, which is the positive control: +/// a check that cannot fail has not passed. + +#include "CADSupport/O2OverlapCheck.h" +#include "CADSupport/O2BVHSurfaceSolid.h" +#include "DetectorsBase/O2Tessellated.h" + +#include "TGeoBBox.h" +#include "TGeoManager.h" +#include "TGeoMatrix.h" +#include "TGeoNode.h" +#include "TGeoTube.h" +#include "TGeoVolume.h" +#include "TFile.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace o2::cad; + +namespace +{ + +struct Options { + std::string geometry; + std::string topVolume; + std::string json; + std::vector injections; + OverlapOptions check; + bool rootCheck = false; + int rootNmesh = 0; + double rootOvlp = 0.001; + bool selfTest = false; + bool listPairs = false; +}; + +void usage(const char* argv0) +{ + std::cout + << "usage: " << argv0 << " --geometry [options]\n" + << " or: " << argv0 << " --self-test\n\n" + << " --geometry PATH ROOT file holding a TGeoManager (as written by geom.C's\n" + << " build_and_export, or by any other producer)\n" + << " --top NAME volume whose daughters are censused (default: the top volume)\n" + << " --points N boundary points sampled per solid (default 20000). Coverage only:\n" + << " every individual answer is exact, so this bounds false NEGATIVES\n" + << " --tol CM depth below which a containment is a shared face, not an overlap\n" + << " (default 1e-6)\n" + << " --residual CM a sampled point further than this from its own solid's boundary is\n" + << " discarded rather than used as evidence (default 1e-6)\n" + << " --pad CM bounding-box inflation for the pairwise rejection (default 0.1).\n" + << " Scopes which DISJOINT pairs get measured; never hides an overlap\n" + << " --volume-samples N Monte-Carlo estimate of the shared volume of each illegal pair\n" + << " --inject NAME:DX,DY,DZ translate a node by (dx,dy,dz) cm before the census. The\n" + << " positive control; may be repeated\n" + << " --root-check [N] also run TGeoManager::CheckOverlaps for comparison, optionally\n" + << " after SetNmeshPoints(N)\n" + << " --root-ovlp CM the tolerance handed to CheckOverlaps (default 0.001)\n" + << " --list-pairs print every tested pair, not only the illegal ones\n" + << " --json PATH write the census as JSON\n" + << " --self-test analytic controls, no geometry file needed; exits non-zero on any\n" + << " failure\n\n" + << "Exit code is the number of illegal pairs, capped at 250; 251 on a usage or load error.\n"; +} + +bool parseInjection(const std::string& spec, std::string& name, double shift[3]) +{ + const auto colon = spec.rfind(':'); + if (colon == std::string::npos) { + return false; + } + name = spec.substr(0, colon); + return std::sscanf(spec.c_str() + colon + 1, "%lf,%lf,%lf", &shift[0], &shift[1], &shift[2]) == 3; +} + +/// Translate a placed node by \a shift cm in the mother frame. Used only by --inject. +bool injectShift(TGeoVolume* mother, const std::string& nodeName, const double shift[3]) +{ + for (int index = 0; index < mother->GetNdaughters(); ++index) { + TGeoNode* node = mother->GetNode(index); + if (nodeName != node->GetVolume()->GetName() && nodeName != node->GetName()) { + continue; + } + auto* nodeWithMatrix = dynamic_cast(node); + if (nodeWithMatrix == nullptr) { + return false; + } + auto* replacement = new TGeoHMatrix(*node->GetMatrix()); + const double* translation = replacement->GetTranslation(); + replacement->SetDx(translation[0] + shift[0]); + replacement->SetDy(translation[1] + shift[1]); + replacement->SetDz(translation[2] + shift[2]); + replacement->RegisterYourself(); + nodeWithMatrix->SetMatrix(replacement); + return true; + } + return false; +} + +void printCensus(const OverlapCensus& census, bool listPairs) +{ + std::printf("\n%d placed solids -> %d pairs; %d survive the bounding-box rejection (%.2f %%)\n", census.nSolids, + census.nPairsTotal, census.nPairsTested, + census.nPairsTotal > 0 ? 100. * census.nPairsTested / census.nPairsTotal : 0.); + std::printf("disjoint %d | touching %d | INTERPENETRATING %d | contained %d | extruding %d (%.1f s)\n", + census.nDisjoint, census.nTouching, census.nInterpenetrating, census.nContained, census.nExtruding, + census.elapsedSeconds); + std::printf("points rejected as not on their own solid: %d; worst accepted residual %.3e cm\n", + census.nPointsRejected, census.worstResidualCm); + + std::printf("\n%-28s %-10s %10s %10s %8s %8s %6s\n", "solid", "shape", "requested", "accepted", "rejected", + "residual", "onSeg"); + for (const auto& solid : census.solids) { + std::printf("%-28s %-10s %10d %10d %8d %8.1e %6s\n", solid.name.c_str(), + solid.shapeClass.size() > 10 ? solid.shapeClass.substr(solid.shapeClass.size() - 10).c_str() + : solid.shapeClass.c_str(), + solid.requested, solid.accepted, solid.rejected, solid.worstResidualCm, + solid.usedPointsOnSegments ? "yes" : "no"); + } + + std::printf("\n%-46s %-17s %13s %8s %8s %13s\n", "pair", "verdict", "depth(cm)", "AinB", "BinA", "sep/vol"); + for (const auto& pair : census.pairs) { + const bool illegal = + pair.verdict == OverlapVerdict::Interpenetrating || pair.verdict == OverlapVerdict::Contained; + if (!listPairs && !illegal) { + continue; + } + char label[128]; + std::snprintf(label, sizeof(label), "%s | %s", pair.nameA.c_str(), pair.nameB.c_str()); + char trailer[64] = ""; + if (pair.sharedVolumeCm3 >= 0.) { + std::snprintf(trailer, sizeof(trailer), "V=%.4e", pair.sharedVolumeCm3); + } else if (pair.separationCm >= 0.) { + std::snprintf(trailer, sizeof(trailer), "gap=%.4e", pair.separationCm); + } + std::printf("%-46s %-17s %13.6e %8d %8d %13s\n", label, OverlapVerdictName(pair.verdict), pair.depthCm, + pair.deepPointsAInsideB, pair.deepPointsBInsideA, trailer); + } + for (const auto& pair : census.extrusions) { + std::printf("%-46s %-17s %13.6e %8d %8s %13s\n", (pair.nameA + " extrudes " + pair.nameB).c_str(), "EXTRUSION", + pair.depthCm, pair.deepPointsAInsideB, "-", ""); + } +} + +nlohmann::json censusToJson(const OverlapCensus& census) +{ + nlohmann::json out; + out["nSolids"] = census.nSolids; + out["nPairsTotal"] = census.nPairsTotal; + out["nPairsTested"] = census.nPairsTested; + out["nDisjoint"] = census.nDisjoint; + out["nTouching"] = census.nTouching; + out["nInterpenetrating"] = census.nInterpenetrating; + out["nContained"] = census.nContained; + out["nExtruding"] = census.nExtruding; + out["illegal"] = census.illegalCount(); + out["nPointsRejected"] = census.nPointsRejected; + out["worstResidualCm"] = census.worstResidualCm; + out["elapsedSeconds"] = census.elapsedSeconds; + for (const auto& solid : census.solids) { + out["solids"].push_back({{"name", solid.name}, + {"shape", solid.shapeClass}, + {"requested", solid.requested}, + {"accepted", solid.accepted}, + {"rejected", solid.rejected}, + {"worstResidualCm", solid.worstResidualCm}, + {"usedPointsOnSegments", solid.usedPointsOnSegments}}); + } + auto pairJson = [](const OverlapPair& pair) { + return nlohmann::json{{"a", pair.nameA}, + {"b", pair.nameB}, + {"verdict", OverlapVerdictName(pair.verdict)}, + {"depthCm", pair.depthCm}, + {"deepestPoint", pair.deepestPoint}, + {"deepestPointFrom", pair.deepestPointFrom}, + {"pointsAInsideB", pair.pointsAInsideB}, + {"pointsBInsideA", pair.pointsBInsideA}, + {"deepPointsAInsideB", pair.deepPointsAInsideB}, + {"deepPointsBInsideA", pair.deepPointsBInsideA}, + {"sampledA", pair.sampledA}, + {"sampledB", pair.sampledB}, + {"separationCm", pair.separationCm}, + {"sharedVolumeCm3", pair.sharedVolumeCm3}, + {"sharedVolumeErrCm3", pair.sharedVolumeErrCm3}, + {"sharedVolumeHits", pair.sharedVolumeHits}}; + }; + for (const auto& pair : census.pairs) { + out["pairs"].push_back(pairJson(pair)); + } + for (const auto& pair : census.extrusions) { + out["extrusions"].push_back(pairJson(pair)); + } + return out; +} + +// --------------------------------------------------------------------------------------------- +// Self-test: the three populations, built from arithmetic, with the controls that make them mean +// something. No geometry file, no build directory, no model. +// --------------------------------------------------------------------------------------------- + +int gChecks = 0; +int gFailures = 0; + +void check(bool condition, const std::string& what) +{ + gChecks++; + if (!condition) { + gFailures++; + std::printf(" FAIL %s\n", what.c_str()); + } else { + std::printf(" ok %s\n", what.c_str()); + } +} + +TGeoVolume* makeWorld(const char* name) +{ + auto* manager = new TGeoManager(name, name); + auto* material = new TGeoMaterial("vac", 0., 0., 0.); + auto* medium = new TGeoMedium("vac", 1, material); + auto* world = manager->MakeBox("world", medium, 100., 100., 100.); + manager->SetTopVolume(world); + return world; +} + +/// A unit cube as an O2BVHSurfaceSolid, so the controls run on the representation this branch +/// ships rather than only on ROOT's primitives. +O2BVHSurfaceSolid* makeSurfaceBox(const char* name, double halfX, double halfY, double halfZ) +{ + auto* solid = new O2BVHSurfaceSolid(name); + const double faces[6][3] = {{1., 0., 0.}, {-1., 0., 0.}, {0., 1., 0.}, {0., -1., 0.}, {0., 0., 1.}, {0., 0., -1.}}; + const double half[3] = {halfX, halfY, halfZ}; + for (const auto& normal : faces) { + const int axis = (normal[0] != 0.) ? 0 : ((normal[1] != 0.) ? 1 : 2); + const int axisU = (axis + 1) % 3; + const int axisV = (axis + 2) % 3; + O2BVHSurfaceSolid::Point3D origin{0., 0., 0.}; + origin[axis] = normal[axis] * half[axis]; + O2BVHSurfaceSolid::Point3D directionU{0., 0., 0.}; + O2BVHSurfaceSolid::Point3D directionV{0., 0., 0.}; + directionU[axisU] = 1.; + directionV[axisV] = 1.; + // Wind the quad so its normal points out of the box. + const double sign = normal[axis]; + std::vector wire; + const double extentU = half[axisU]; + const double extentV = half[axisV]; + if (sign > 0) { + wire = {{-extentU, -extentV}, {extentU, -extentV}, {extentU, extentV}, {-extentU, extentV}}; + } else { + wire = {{-extentU, -extentV}, {-extentU, extentV}, {extentU, extentV}, {extentU, -extentV}}; + } + solid->AddPlanarSurface(origin, directionU, directionV, wire, {}); + } + solid->CloseShape(false); + return solid; +} + +int selfTest() +{ + std::printf("== o2-bench-cadsupport-overlap self-test ==\n"); + + OverlapOptions options; + options.pointsPerSolid = 4000; + options.checkExtrusion = false; + + // --- 1. Two boxes sharing a face exactly: TOUCHING, and it must not be called an overlap. --- + { + TGeoVolume* world = makeWorld("touch"); + auto* left = new TGeoVolume("left", makeSurfaceBox("leftBox", 1., 1., 1.), world->GetMedium()); + auto* right = new TGeoVolume("right", makeSurfaceBox("rightBox", 1., 1., 1.), world->GetMedium()); + world->AddNode(left, 1, new TGeoTranslation(-1., 0., 0.)); + world->AddNode(right, 1, new TGeoTranslation(1., 0., 0.)); + gGeoManager->CloseGeometry(); + const OverlapCensus census = CheckWorldOverlaps(world, options); + check(census.nPairsTested == 1, "touching: the pair survives the box rejection"); + check(census.nTouching == 1 && census.nInterpenetrating == 0, + "touching: a shared face is TOUCHING, not an overlap"); + check(!census.pairs.empty() && census.pairs[0].pointsAInsideB + census.pairs[0].pointsBInsideA > 0, + "touching: the check was capable of firing (points ARE found inside)"); + check(!census.pairs.empty() && census.pairs[0].depthCm <= options.depthTolerance, + "touching: the depth is at the tolerance, i.e. zero"); + delete gGeoManager; + } + + // --- 2. The same two boxes moved 0.2 cm into each other: INTERPENETRATING, at that depth. --- + { + TGeoVolume* world = makeWorld("overlap"); + auto* left = new TGeoVolume("left", makeSurfaceBox("leftBox", 1., 1., 1.), world->GetMedium()); + auto* right = new TGeoVolume("right", makeSurfaceBox("rightBox", 1., 1., 1.), world->GetMedium()); + world->AddNode(left, 1, new TGeoTranslation(-1., 0., 0.)); + world->AddNode(right, 1, new TGeoTranslation(0.8, 0., 0.)); + gGeoManager->CloseGeometry(); + OverlapOptions withVolume = options; + withVolume.volumeSamples = 200000; + const OverlapCensus census = CheckWorldOverlaps(world, withVolume); + check(census.nInterpenetrating == 1, "injected 0.2 cm: INTERPENETRATING"); + const double depth = census.pairs.empty() ? 0. : census.pairs[0].depthCm; + check(std::abs(depth - 0.2) < 1e-9, + "injected 0.2 cm: the depth is the injected displacement (" + std::to_string(depth) + ")"); + const double volume = census.pairs.empty() ? -1. : census.pairs[0].sharedVolumeCm3; + check(std::abs(volume - 0.8) < 0.02, + "injected 0.2 cm: shared volume 0.2 x 2 x 2 = 0.8 cm3 (" + std::to_string(volume) + ")"); + delete gGeoManager; + } + + // --- 3. The negative control: the same two boxes 0.2 cm APART must not fire. --- + { + TGeoVolume* world = makeWorld("gap"); + auto* left = new TGeoVolume("left", makeSurfaceBox("leftBox", 1., 1., 1.), world->GetMedium()); + auto* right = new TGeoVolume("right", makeSurfaceBox("rightBox", 1., 1., 1.), world->GetMedium()); + world->AddNode(left, 1, new TGeoTranslation(-1., 0., 0.)); + world->AddNode(right, 1, new TGeoTranslation(1.2, 0., 0.)); + gGeoManager->CloseGeometry(); + const OverlapCensus census = CheckWorldOverlaps(world, options); + check(census.nDisjoint == 1 && census.illegalCount() == 0, "0.2 cm gap: disjoint, nothing flagged"); + const double separation = census.pairs.empty() ? -1. : census.pairs[0].separationCm; + check(std::abs(separation - 0.2) < 1e-9, + "0.2 cm gap: the separation is recovered (" + std::to_string(separation) + ")"); + delete gGeoManager; + } + + // --- 4. A tenth of a micron: the tolerance is a decision, and it is measured, not assumed. --- + { + TGeoVolume* world = makeWorld("thin"); + auto* left = new TGeoVolume("left", makeSurfaceBox("leftBox", 1., 1., 1.), world->GetMedium()); + auto* right = new TGeoVolume("right", makeSurfaceBox("rightBox", 1., 1., 1.), world->GetMedium()); + world->AddNode(left, 1, new TGeoTranslation(-1., 0., 0.)); + world->AddNode(right, 1, new TGeoTranslation(1. - 1e-5, 0., 0.)); + gGeoManager->CloseGeometry(); + const OverlapCensus loose = CheckWorldOverlaps(world, options); + check(loose.nInterpenetrating == 1, "1e-5 cm interpenetration is resolved at the default 1e-6 tolerance"); + OverlapOptions coarse = options; + coarse.depthTolerance = 1e-4; + const OverlapCensus blunted = CheckWorldOverlaps(world, coarse); + check(blunted.nInterpenetrating == 0 && blunted.nTouching == 1, + "CONTROL: at a 1e-4 tolerance the same 1e-5 interpenetration reads as touching"); + delete gGeoManager; + } + + // --- 5. Containment, which is legal only as a declared mother/daughter. --- + { + TGeoVolume* world = makeWorld("nested"); + auto* outer = new TGeoVolume("outer", makeSurfaceBox("outerBox", 3., 3., 3.), world->GetMedium()); + auto* inner = new TGeoVolume("inner", makeSurfaceBox("innerBox", 1., 1., 1.), world->GetMedium()); + world->AddNode(outer, 1, new TGeoTranslation(0., 0., 0.)); + world->AddNode(inner, 1, new TGeoTranslation(0., 0., 0.)); + gGeoManager->CloseGeometry(); + const OverlapCensus census = CheckWorldOverlaps(world, options); + check(census.nContained == 1, "a solid wholly inside another is CONTAINED, not merely overlapping"); + delete gGeoManager; + } + + // --- 6. A curved contact: a press fit exact in the model must not read as an overlap. This is + // the case ROOT's checker got wrong, and the sagitta of its 24-gon is 8.6e-3 cm. --- + { + TGeoVolume* world = makeWorld("press"); + auto* pin = new TGeoVolume("pin", new TGeoTube("pinTube", 0., 1., 5.), world->GetMedium()); + auto* sleeve = new TGeoVolume("sleeve", new TGeoTube("sleeveTube", 1., 2., 5.), world->GetMedium()); + world->AddNode(pin, 1, new TGeoTranslation(0., 0., 0.)); + world->AddNode(sleeve, 1, new TGeoTranslation(0., 0., 0.)); + gGeoManager->CloseGeometry(); + const OverlapCensus census = CheckWorldOverlaps(world, options); + check(census.nInterpenetrating == 0 && census.nTouching == 1, + "an exact press fit on a cylinder is TOUCHING, not an 8.6e-3 cm overlap"); + const double depth = census.pairs.empty() ? -1. : census.pairs[0].depthCm; + check(depth < 1e-6, "press fit: depth " + std::to_string(depth) + " is below the 24-gon sagitta 8.6e-3 by 4 decades"); + delete gGeoManager; + } + + // --- 7. The residual filter: a point that is not on its own solid is not evidence. --- + { + auto* box = new TGeoBBox("residualBox", 1., 1., 1.); + std::vector points; + int rejected = 0; + double worst = 0.; + const int accepted = SampleBoundaryPoints(box, 4000, 1e-6, points, rejected, worst); + check(accepted > 0 && rejected == 0, "TGeoBBox: every sampled point is on the box"); + check(worst < 1e-9, "TGeoBBox: worst accepted residual " + std::to_string(worst) + " is at round-off"); + } + + // --- 8. The sampling contract on the shape this branch ships. --- + { + auto* solid = makeSurfaceBox("contractBox", 1., 2., 3.); + const int meshVertices = solid->GetNmeshVertices(); + std::vector buffer(3 * (meshVertices + 5000), -1.2345e33); + check(solid->GetPointsOnSegments(meshVertices + 5000, buffer.data()), + "GetPointsOnSegments fills the buffer when asked for more than the mesh"); + int unfilled = 0; + double worst = 0.; + for (int index = 0; index < meshVertices + 5000; ++index) { + if (buffer[3 * index] == -1.2345e33) { + unfilled++; + continue; + } + worst = std::max(worst, solid->Safety(&buffer[3 * index], solid->Contains(&buffer[3 * index]))); + } + check(unfilled == 0, "GetPointsOnSegments leaves no slot unwritten"); + check(worst < O2BVHSurfaceSolid::kSurfacePointTolerance, + "every generated point is on the solid (worst " + std::to_string(worst) + ")"); + check(!solid->GetPointsOnSegments(meshVertices - 1, buffer.data()), + "below the mesh size it declines, so ROOT falls back to the full exact vertex set"); + delete solid; + } + + std::printf("\n%d checks, %d failures\n", gChecks, gFailures); + return gFailures == 0 ? 0 : 1; +} + +} // namespace + +int main(int argc, char** argv) +{ + Options options; + for (int index = 1; index < argc; ++index) { + const std::string argument = argv[index]; + auto next = [&](const char* what) -> std::string { + if (index + 1 >= argc) { + std::cerr << "error: " << what << " needs a value\n"; + std::exit(251); + } + return argv[++index]; + }; + if (argument == "-h" || argument == "--help") { + usage(argv[0]); + return 0; + } else if (argument == "--geometry") { + options.geometry = next("--geometry"); + } else if (argument == "--top") { + options.topVolume = next("--top"); + } else if (argument == "--points") { + options.check.pointsPerSolid = std::atoi(next("--points").c_str()); + } else if (argument == "--tol") { + options.check.depthTolerance = std::atof(next("--tol").c_str()); + } else if (argument == "--residual") { + options.check.residualTolerance = std::atof(next("--residual").c_str()); + } else if (argument == "--pad") { + options.check.padCm = std::atof(next("--pad").c_str()); + } else if (argument == "--volume-samples") { + options.check.volumeSamples = std::atoi(next("--volume-samples").c_str()); + } else if (argument == "--inject") { + options.injections.push_back(next("--inject")); + } else if (argument == "--root-check") { + options.rootCheck = true; + if (index + 1 < argc && argv[index + 1][0] != '-') { + options.rootNmesh = std::atoi(argv[++index]); + } + } else if (argument == "--root-ovlp") { + options.rootOvlp = std::atof(next("--root-ovlp").c_str()); + } else if (argument == "--list-pairs") { + options.listPairs = true; + } else if (argument == "--json") { + options.json = next("--json"); + } else if (argument == "--self-test") { + options.selfTest = true; + } else { + std::cerr << "error: unknown argument " << argument << "\n"; + usage(argv[0]); + return 251; + } + } + + if (options.selfTest) { + return selfTest(); + } + if (options.geometry.empty()) { + usage(argv[0]); + return 251; + } + + TGeoManager::Import(options.geometry.c_str()); + if (gGeoManager == nullptr) { + std::cerr << "error: no TGeoManager in " << options.geometry << "\n"; + return 251; + } + TGeoVolume* top = options.topVolume.empty() ? gGeoManager->GetTopVolume() + : gGeoManager->GetVolume(options.topVolume.c_str()); + if (top == nullptr) { + std::cerr << "error: no such volume: " << options.topVolume << "\n"; + return 251; + } + + for (const auto& specification : options.injections) { + std::string name; + double shift[3] = {0., 0., 0.}; + if (!parseInjection(specification, name, shift)) { + std::cerr << "error: cannot parse --inject " << specification << " (expected NAME:DX,DY,DZ)\n"; + return 251; + } + if (!injectShift(top, name, shift)) { + std::cerr << "error: --inject names no daughter of " << top->GetName() << ": " << name << "\n"; + return 251; + } + std::printf("# injected: %s by (%g, %g, %g) cm\n", name.c_str(), shift[0], shift[1], shift[2]); + } + + std::printf("# geometry %s, top volume %s, %d points per solid, depth tolerance %g cm, pad %g cm\n", + options.geometry.c_str(), top->GetName(), options.check.pointsPerSolid, options.check.depthTolerance, + options.check.padCm); + + const OverlapCensus census = CheckWorldOverlaps(top, options.check); + printCensus(census, options.listPairs); + + if (options.rootCheck) { + std::printf("\n== TGeoManager::CheckOverlaps, for comparison (nmesh %s, ovlp %g) ==\n", + options.rootNmesh > 0 ? std::to_string(options.rootNmesh).c_str() : "default", options.rootOvlp); + gGeoManager->GetGeomPainter(); + if (options.rootNmesh > 0) { + gGeoManager->SetNmeshPoints(options.rootNmesh); + } + gGeoManager->CheckOverlaps(options.rootOvlp); + gGeoManager->PrintOverlaps(); + } + + if (!options.json.empty()) { + nlohmann::json out = censusToJson(census); + out["geometry"] = options.geometry; + out["top"] = top->GetName(); + out["options"] = {{"pointsPerSolid", options.check.pointsPerSolid}, + {"depthToleranceCm", options.check.depthTolerance}, + {"residualToleranceCm", options.check.residualTolerance}, + {"padCm", options.check.padCm}, + {"volumeSamples", options.check.volumeSamples}}; + out["injections"] = options.injections; + std::ofstream stream(options.json); + stream << out.dump(1) << "\n"; + std::printf("\nwrote %s\n", options.json.c_str()); + } + + return std::min(census.illegalCount(), 250); +} diff --git a/Detectors/CADSupport/test/runSolidHarness.cxx b/Detectors/CADSupport/test/runSolidHarness.cxx new file mode 100644 index 0000000000000..1dfbed27fbe3e --- /dev/null +++ b/Detectors/CADSupport/test/runSolidHarness.cxx @@ -0,0 +1,1391 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +/// \file runSolidHarness.cxx +/// \brief Front-end for the O2SolidHarness validation / performance comparison harness. +/// +/// Built as o2-bench-cadsupport-solid-harness (see Detectors/CADSupport/CMakeLists.txt). Loads +/// paired surfaces_*.bin / facets_*.bin parts from a test-part database (see +/// Detectors/CADSupport/validation/makeTestPartDB.py) and, for each, validates and times +/// O2BVHSurfaceSolid (candidate) against O2Tessellated (reference). Usage and reading rules are in +/// Detectors/CADSupport/doc/reference/SolidNavigationHarness.md. + +#include "CADSupport/O2SolidHarness.h" +#include "CADSupport/O2BVHSurfaceSolid.h" +#include "DetectorsBase/O2Tessellated.h" +#include "CADSupport/O2SurfaceSolidIO.h" + +#include "TGeoBBox.h" +#include "TGeoCompositeShape.h" +#include "TGeoMatrix.h" +#include "TGeoScaledShape.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; +using namespace o2::cad; +using namespace o2::cad::harness; +using o2::base::O2Tessellated; + +namespace +{ + +struct Options { + std::string db; + std::string explicitSurfaces; + std::string explicitFacets; + std::string partsPattern; + int points = 5000; + int rays = 5000; + uint64_t seed = 1; + std::set only = {"contains", "distout", "distin", "safety"}; + bool loopCrosscheck = false; + bool pruningAb = false; + bool allRims = false; ///< print every rim, not only the ones that are not cleanly matched + std::string jsonOut; + int warmup = 1; + int repeat = 3; + std::string dumpSamples; ///< directory to write per-part sample sets into, for the OCCT oracle + std::string refAnswers; ///< directory holding the oracle's answers for those sample sets + std::string loadSamples; ///< directory to read per-part sample sets from, instead of generating + bool edgeIdentity = false; ///< report the sidecar-v3 edge-identity block + std::string explicitShape; ///< ad-hoc mode: the shape_.root sidecar to score alongside +}; + +struct Part { + std::string id; + std::string model; + std::string surfaces; + std::string facets; + /// The `shape__.root` sidecar, when the part has one. Optional by construction: it is + /// the future CSG emitter's output and no part has one today. + std::string shape; +}; + +/// `surfaces_.bin` -> `shape_.root` in the same directory. +/// +/// Derived rather than only read from the manifest so that a shape sidecar dropped next to the +/// other artifacts is picked up by a `--skip-convert` re-score, which is the loop anyone +/// developing an emitter will actually run. `makeTestPartDB.py` records the same path under the +/// manifest's `"shape"` key when it indexes the database, and that entry wins when present. +std::string deriveShapeSidecarPath(const std::string& surfacesPath) +{ + const auto slash = surfacesPath.find_last_of('/'); + const std::string dir = slash == std::string::npos ? std::string() : surfacesPath.substr(0, slash + 1); + std::string base = slash == std::string::npos ? surfacesPath : surfacesPath.substr(slash + 1); + const std::string prefix = "surfaces_"; + const std::string suffix = ".bin"; + if (base.rfind(prefix, 0) != 0 || base.size() <= prefix.size() + suffix.size() || + base.compare(base.size() - suffix.size(), suffix.size(), suffix) != 0) { + return {}; + } + const std::string stem = base.substr(prefix.size(), base.size() - prefix.size() - suffix.size()); + return dir + "shape_" + stem + ".root"; +} + +bool fileExists(const std::string& path) +{ + if (path.empty()) { + return false; + } + std::ifstream probe(path); + return static_cast(probe); +} + +void printUsage(const char* argv0) +{ + std::cout << "Usage: " << argv0 << " --db [--parts ] [--points N] [--rays N] [--seed N]\n" + " [--only contains,distout,distin,safety] [--loop-crosscheck]\n" + " [--pruning-ab] [--json ] [--warmup N] [--repeat N]\n" + " or: " + << argv0 << " --surfaces --facets [--shape ] [options as above]\n\n" + " Every representation a part has is scored side by side against the same oracle answers:\n" + " surface surfaces_.bin -> O2BVHSurfaceSolid (the historical candidate)\n" + " mesh facets_.bin -> O2Tessellated (also the sampling reference)\n" + " shape shape_.root -> any TGeoShape (the CSG emitter's hand-over)\n" + " The `shape` sidecar is one ROOT file holding one TGeoShape-derived object under the key\n" + " \"shape\", in cm, plus an OPTIONAL TGeoHMatrix under the key \"placement\" taking it from\n" + " its own frame into the part's; absent means identity, and points and rays are transformed\n" + " into the shape's frame before it is asked. See CADSupport/O2SolidHarness.h.\n\n" + " --loop-crosscheck also run the surface solid's non-BVH _Loop twins and require exact\n" + " agreement; this is the correctness guard that does not involve the mesh\n" + " --pruning-ab re-run the distance kernels with ray tmax pruning disabled, reporting\n" + " the BVH candidate counts and ns/call both ways (prices the optimization)\n" + " --rims list every trim loop, not only the ones that are not cleanly matched;\n" + " the same records go into --json unconditionally\n" + " --dump-samples D write each part's sample set to D/samples_.json\n" + " --load-samples D read each part's sample set from D/samples_.json instead of\n" + " generating it. The generator derives its points from the *mesh*, so two\n" + " runs on differently-tessellated shapes cannot be compared point by\n" + " point; loading a frozen (and, for a transformed shape, transformed) set\n" + " removes the mesh from the comparison entirely. --points/--rays/--seed\n" + " are then ignored and the file's counts are used.\n" + " --edge-identity report the sidecar-v3 edge-identity block (source-edge counts and the\n" + " max shared-edge deviation) on stdout; it is always in --json\n" + " --ref-answers D validate against D/answers_.json instead of the mesh; those are\n" + " produced by Detectors/CADSupport/validation/occtOracle.py from the part's .brep, so a\n" + " disagreement outside the model tolerance is a defect, not chording\n\n" + "OCCT oracle round trip:\n" + " " + << argv0 << " --db --dump-samples /tmp/o\n" + " occtOracle.py --brep .brep --samples /tmp/o/samples_.json \\\n" + " --out /tmp/o/answers_.json\n" + " " + << argv0 << " --db --ref-answers /tmp/o\n\n" + "perf record entry point (single kernel, one part):\n" + " perf record -g " + << argv0 << " --db --parts ExcavatorArm --only distout --rays 200000\n"; +} + +std::set splitCsv(const std::string& s) +{ + std::set out; + std::stringstream ss(s); + std::string tok; + while (std::getline(ss, tok, ',')) { + if (!tok.empty()) { + out.insert(tok); + } + } + return out; +} + +bool parseArgs(int argc, char** argv, Options& opt) +{ + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + auto next = [&](const char* flag) -> std::string { + if (i + 1 >= argc) { + throw std::runtime_error(std::string("missing value for ") + flag); + } + return argv[++i]; + }; + if (a == "--db") { + opt.db = next("--db"); + } else if (a == "--surfaces") { + opt.explicitSurfaces = next("--surfaces"); + } else if (a == "--facets") { + opt.explicitFacets = next("--facets"); + } else if (a == "--shape") { + opt.explicitShape = next("--shape"); + } else if (a == "--parts") { + opt.partsPattern = next("--parts"); + } else if (a == "--points") { + opt.points = std::stoi(next("--points")); + } else if (a == "--rays") { + opt.rays = std::stoi(next("--rays")); + } else if (a == "--seed") { + opt.seed = std::stoull(next("--seed")); + } else if (a == "--only") { + opt.only = splitCsv(next("--only")); + } else if (a == "--loop-crosscheck") { + opt.loopCrosscheck = true; + } else if (a == "--pruning-ab") { + opt.pruningAb = true; + } else if (a == "--rims") { + opt.allRims = true; + } else if (a == "--json") { + opt.jsonOut = next("--json"); + } else if (a == "--warmup") { + opt.warmup = std::stoi(next("--warmup")); + } else if (a == "--repeat") { + opt.repeat = std::stoi(next("--repeat")); + } else if (a == "--dump-samples") { + opt.dumpSamples = next("--dump-samples"); + } else if (a == "--ref-answers") { + opt.refAnswers = next("--ref-answers"); + } else if (a == "--load-samples") { + opt.loadSamples = next("--load-samples"); + } else if (a == "--edge-identity") { + opt.edgeIdentity = true; + } else if (a == "-h" || a == "--help") { + printUsage(argv[0]); + return false; + } else { + throw std::runtime_error("unrecognized option: " + a); + } + } + if (opt.db.empty() && (opt.explicitSurfaces.empty() || opt.explicitFacets.empty())) { + throw std::runtime_error("either --db or both --surfaces/--facets are required"); + } + return true; +} + +std::vector collectParts(const Options& opt) +{ + std::vector parts; + if (!opt.explicitSurfaces.empty()) { + Part part{"adhoc", "adhoc", opt.explicitSurfaces, opt.explicitFacets, opt.explicitShape}; + if (part.shape.empty()) { + part.shape = deriveShapeSidecarPath(part.surfaces); + } + parts.push_back(std::move(part)); + return parts; + } + const std::string manifestPath = opt.db + "/manifest.json"; + std::ifstream in(manifestPath); + if (!in) { + throw std::runtime_error("cannot open " + manifestPath); + } + json manifest; + in >> manifest; + for (const auto& p : manifest.at("parts")) { + Part part; + part.id = p.at("id").get(); + part.model = p.at("model").get(); + part.surfaces = p.at("surfaces").get(); + part.facets = p.at("facets").get(); + part.shape = p.value("shape", std::string()); + if (part.shape.empty()) { + part.shape = deriveShapeSidecarPath(part.surfaces); + } + if (!opt.partsPattern.empty()) { + const bool idMatch = part.id.find(opt.partsPattern) != std::string::npos; + const bool modelMatch = part.model.find(opt.partsPattern) != std::string::npos; + if (!idMatch && !modelMatch) { + continue; + } + } + parts.push_back(std::move(part)); + } + return parts; +} + +json validationToJson(const ValidationResult& r) +{ + json j; + j["nSamples"] = r.nSamples; + j["nAgree"] = r.nAgree; + j["nMismatchWithinBand"] = r.nMismatchWithinBand; + j["nMismatchMissedSurface"] = r.nMismatchMissedSurface; + j["nMismatchUnexplained"] = r.nMismatchUnexplained; + j["nNoVerdict"] = r.nNoVerdict; + j["nRelabelled"] = r.nRelabelled; + j["worstDeviation"] = r.worstDeviation; + json offenders = json::array(); + for (const auto& o : r.worstOffenders) { + offenders.push_back({{"point", {o.point[0], o.point[1], o.point[2]}}, + {"dir", {o.dir[0], o.dir[1], o.dir[2]}}, + {"candidateValue", o.candidateValue}, + {"referenceValue", o.referenceValue}, + {"deviation", o.deviation}, + {"referenceSafety", o.referenceSafety}, + {"incidenceCosine", o.incidenceCosine}}); + } + j["worstOffenders"] = offenders; + return j; +} + +// The sample/answer JSON contract shared with Detectors/CADSupport/validation/occtOracle.py. Bump on both sides +// together; the oracle refuses a version it does not speak rather than guessing. +constexpr int kOracleFormatVersion = 1; + +/// Part ids carry '/' and other path-hostile characters; the oracle round trip pairs files by +/// this sanitized form on both sides. +std::string sanitizePartId(const std::string& id) +{ + std::string out; + out.reserve(id.size()); + for (const char c : id) { + out.push_back((std::isalnum(static_cast(c)) || c == '-' || c == '.') ? c : '_'); + } + return out; +} + +json pointsToJson(const std::vector& points) +{ + json array = json::array(); + for (const auto& p : points) { + array.push_back({p[0], p[1], p[2]}); + } + return array; +} + +json raysToJson(const std::vector& rays) +{ + json array = json::array(); + for (const auto& r : rays) { + array.push_back({{"o", {r.origin[0], r.origin[1], r.origin[2]}}, + {"d", {r.dir[0], r.dir[1], r.dir[2]}}}); + } + return array; +} + +/// Serialize a sample set so an external oracle can answer exactly the same queries. The samples +/// come from a seeded mt19937_64 inside the harness, so nothing outside can regenerate them; +/// dumping is the only way to ask another implementation about the same points. +void writeSamples(const std::string& dir, const std::string& partId, const SampleSet& samples) +{ + json doc; + doc["version"] = kOracleFormatVersion; + doc["part"] = partId; + doc["bboxMin"] = {samples.bboxMin[0], samples.bboxMin[1], samples.bboxMin[2]}; + doc["bboxMax"] = {samples.bboxMax[0], samples.bboxMax[1], samples.bboxMax[2]}; + doc["points"] = {{"bulk", pointsToJson(samples.bulkPoints)}, + {"boundary", pointsToJson(samples.boundaryPoints)}, + {"inside", pointsToJson(samples.insidePoints)}}; + doc["rays"] = {{"outside", raysToJson(samples.outsideRays)}, + {"inside", raysToJson(samples.insideRays)}}; + const std::string path = dir + "/samples_" + sanitizePartId(partId) + ".json"; + std::ofstream out(path); + if (!out) { + throw std::runtime_error("cannot write " + path); + } + out << doc.dump(1); + std::printf(" wrote samples: %s\n", path.c_str()); +} + +std::vector pointsFromJson(const json& array) +{ + std::vector points; + points.reserve(array.size()); + for (const auto& p : array) { + points.push_back(Point3D{p.at(0).get(), p.at(1).get(), p.at(2).get()}); + } + return points; +} + +std::vector raysFromJson(const json& array) +{ + std::vector rays; + rays.reserve(array.size()); + for (const auto& r : array) { + const auto& o = r.at("o"); + const auto& d = r.at("d"); + rays.push_back(Ray{Point3D{o.at(0).get(), o.at(1).get(), o.at(2).get()}, + Point3D{d.at(0).get(), d.at(1).get(), d.at(2).get()}}); + } + return rays; +} + +/// Read back a sample set written by writeSamples(), so that two runs on differently tessellated or +/// transformed shapes ask exactly the same questions. +SampleSet readSamples(const std::string& dir, const std::string& partId) +{ + const std::string path = dir + "/samples_" + sanitizePartId(partId) + ".json"; + std::ifstream in(path); + if (!in) { + throw std::runtime_error("cannot read " + path); + } + json doc; + in >> doc; + const int version = doc.value("version", -1); + if (version != kOracleFormatVersion) { + throw std::runtime_error(path + ": sample format version " + std::to_string(version) + + ", this harness speaks " + std::to_string(kOracleFormatVersion)); + } + SampleSet samples; + for (int i = 0; i < 3; ++i) { + samples.bboxMin[i] = doc.at("bboxMin").at(i).get(); + samples.bboxMax[i] = doc.at("bboxMax").at(i).get(); + } + samples.bulkPoints = pointsFromJson(doc.at("points").at("bulk")); + samples.boundaryPoints = pointsFromJson(doc.at("points").at("boundary")); + samples.insidePoints = pointsFromJson(doc.at("points").at("inside")); + samples.outsideRays = raysFromJson(doc.at("rays").at("outside")); + samples.insideRays = raysFromJson(doc.at("rays").at("inside")); + std::printf(" loaded samples: %s (bulk=%zu boundary=%zu inside=%zu outRays=%zu inRays=%zu)\n", + path.c_str(), samples.bulkPoints.size(), samples.boundaryPoints.size(), + samples.insidePoints.size(), samples.outsideRays.size(), samples.insideRays.size()); + return samples; +} + +/// Oracle answers for one part, or `has == false` when no answer file exists for it. +struct OracleAnswers { + bool has = false; + double tolerance = 0.; + double capacity = 0.; + bool valid = false; + /// The BREP's own bounding box, in the frame the oracle answered in. Every candidate must live + /// in that same frame; this is what makes that checkable instead of assumed. + bool hasBbox = false; + Point3D bboxMin{}; + Point3D bboxMax{}; + std::map> containsState; + /// Per ray category, the oracle's classification of each ray *origin* (1/0/-1). This is what + /// makes the distance columns soundly categorised rather than categorised by the reference mesh. + std::map> originContains; + std::map> boundaryDistance; + std::map> distOutside; + std::map> distInside; +}; + +template +std::map> readColumns(const json& parent, const char* key) +{ + std::map> columns; + if (!parent.contains(key)) { + return columns; + } + for (const auto& [category, values] : parent.at(key).items()) { + columns[category] = values.template get>(); + } + return columns; +} + +OracleAnswers loadOracleAnswers(const std::string& dir, const std::string& partId) +{ + OracleAnswers answers; + const std::string path = dir + "/answers_" + sanitizePartId(partId) + ".json"; + std::ifstream in(path); + if (!in) { + std::printf(" oracle: no answers file %s, skipping oracle validation\n", path.c_str()); + return answers; + } + json doc; + in >> doc; + const int version = doc.value("version", -1); + if (version != kOracleFormatVersion) { + throw std::runtime_error(path + ": answer format version " + std::to_string(version) + + ", this harness speaks " + std::to_string(kOracleFormatVersion)); + } + answers.has = true; + answers.tolerance = doc.value("tolerance", 0.); + answers.capacity = doc.value("capacity", 0.); + answers.valid = doc.value("valid", false); + if (doc.contains("bboxMin") && doc.contains("bboxMax")) { + answers.hasBbox = true; + for (int i = 0; i < 3; ++i) { + answers.bboxMin[i] = doc.at("bboxMin").at(i).get(); + answers.bboxMax[i] = doc.at("bboxMax").at(i).get(); + } + } + answers.containsState = readColumns(doc, "contains"); + answers.originContains = readColumns(doc, "originContains"); + answers.boundaryDistance = readColumns(doc, "safetyUpperBound"); + answers.distOutside = readColumns(doc, "distFromOutside"); + answers.distInside = readColumns(doc, "distFromInside"); + return answers; +} + +/// Concatenate the oracle's per-category columns in the same order the harness concatenates its +/// point categories, so index i of the merged column belongs to point i of `allPoints`. +/// +/// Each column is padded to its category's *point count* before the next one is appended. That +/// padding is not cosmetic: the oracle answers `contains` for every point but caps the expensive +/// boundary-distance query, so its columns have different lengths per category. Concatenating +/// them raw would shift every later category's answers onto the wrong points -- a silent, +/// systematic mis-scoring rather than an error. +template +std::vector mergeCategories(const std::map>& columns, + const std::array& categorySizes, T missing) +{ + static constexpr std::array kOrder = {"bulk", "boundary", "inside"}; + std::vector merged; + for (size_t categoryIndex = 0; categoryIndex < kOrder.size(); ++categoryIndex) { + const size_t expected = categorySizes[categoryIndex]; + const auto it = columns.find(kOrder[categoryIndex]); + const size_t available = it == columns.end() ? 0 : std::min(expected, it->second.size()); + for (size_t i = 0; i < available; ++i) { + merged.push_back(it->second[i]); + } + merged.insert(merged.end(), expected - available, missing); + } + return merged; +} + +json timingToJson(const TimingResult& t) +{ + return {{"nCalls", t.nCalls}, {"nsPerCall", t.nsPerCall}, {"checksum", t.checksum}}; +} + +void printValidation(const std::string& name, const ValidationResult& r) +{ + // Scored = everything the reference was willing to answer. Reporting the percentage against + // nSamples would let a reference that abstains on half the points look like agreement. + const size_t scored = r.nSamples - r.nNoVerdict; + const double agreePct = scored ? 100. * static_cast(r.nAgree) / static_cast(scored) : 0.; + std::printf( + " %-10s scored=%-7zu agree=%6.2f%% mismatch(band=%zu,missed=%zu,unexplained=%zu)" + " noVerdict=%zu worstDev=%.6g\n", + name.c_str(), scored, agreePct, r.nMismatchWithinBand, r.nMismatchMissedSurface, + r.nMismatchUnexplained, r.nNoVerdict, r.worstDeviation); + if (r.nRelabelled > 0) { + // Not a candidate result: it says how many rays the sample generator had put in the wrong + // category, which is a statement about the reference mesh. Printed so an improvement in these + // columns is never mistaken for a kernel improvement. + std::printf(" %-10s relabelled=%zu ray(s) by the oracle's own origin classification\n", + name.c_str(), r.nRelabelled); + } + if (r.nMismatchUnexplained > 0 || r.nMismatchMissedSurface > 0) { + const size_t nShow = std::min(3, r.worstOffenders.size()); + for (size_t i = 0; i < nShow; ++i) { + const auto& o = r.worstOffenders[i]; + std::printf(" offender[%zu]: point=(%.6g,%.6g,%.6g) dir=(%.6g,%.6g,%.6g) cand=%.6g ref=%.6g dev=%.6g refSafety=%.6g\n", + i, o.point[0], o.point[1], o.point[2], o.dir[0], o.dir[1], o.dir[2], o.candidateValue, + o.referenceValue, o.deviation, o.referenceSafety); + } + } +} + +void printTiming(const std::string& name, const TimingResult& candidate, const TimingResult& reference) +{ + const double ratio = reference.nsPerCall > 0. ? candidate.nsPerCall / reference.nsPerCall : 0.; + std::printf(" %-10s candidate=%9.1f ns/call reference=%9.1f ns/call ratio(cand/ref)=%.2fx\n", name.c_str(), + candidate.nsPerCall, reference.nsPerCall, ratio); +} + +// What the BVH traversal buys over the all-surfaces loop on the *same* shape: unlike the +// candidate/reference ratio this compares like with like, so it prices the acceleration structure +// alone rather than analytic patches against triangles. +void printLoopSpeedup(const std::string& name, const TimingResult& bvh, const TimingResult& loop) +{ + const double speedup = bvh.nsPerCall > 0. ? loop.nsPerCall / bvh.nsPerCall : 0.; + std::printf(" %-10s BVH=%9.1f ns/call _Loop=%9.1f ns/call speedup(loop/bvh)=%.2fx\n", name.c_str(), + bvh.nsPerCall, loop.nsPerCall, speedup); +} + +double toSeconds(std::chrono::steady_clock::time_point t0, std::chrono::steady_clock::time_point t1) +{ + return std::chrono::duration(t1 - t0).count(); +} + +// ------------------------------------------------------------------------------------------ +// Representations: the same part, scored several ways against one set of oracle answers +// ------------------------------------------------------------------------------------------ +// +// The four scored queries are TGeoShape virtuals, so the scoring loop below has no business +// knowing what it is scoring. Everything that is specific to O2BVHSurfaceSolid -- closure, rims, +// NavigationReliability, the _Loop twins, the BVH candidate counters -- hangs off `surfaceSolid`, +// which is null for every other representation, and is reported only where it means something. +// A TGeoCompositeShape has no rims and no closure; reporting "reliable" or "not navigable" for it +// would be a category error, so those keys are simply absent from its entry and a +// `closureApplicable: false` says why. + +struct Representation { + std::string name; ///< "surface" | "mesh" | "shape" + std::string source; ///< the file it was loaded from + const TGeoShape* shape = nullptr; + const O2BVHSurfaceSolid* surfaceSolid = nullptr; ///< non-null only for "surface" + int primitives = 0; ///< patches / triangles / -1 when not countable + const char* primitiveKind = ""; + /// The shape's own frame, expressed in the part frame; null means the two are the same. + /// + /// Only the `shape` representation can have one, and only since a placed primitive stopped + /// being written as a degenerate TGeoCompositeShape. **Points and rays are transformed into the + /// shape's frame** rather than the shape being wrapped in something that carries the matrix. + /// The reason is that this is the only arrangement under which the object the gate scores is + /// the object the converter emitted: `shapeClass` is the real class, `Capacity()` is the real + /// analytic capacity, and nothing between the sample and the shape can absorb an error. A + /// wrapper (or a one-node TGeoVolume) would reintroduce exactly the indirection this change + /// removed, and its own bounding box would be the inflated corner hull again. + const TGeoMatrix* placement = nullptr; +}; + +/// A point of the part frame, expressed in the shape's own frame. +Point3D toLocal(const TGeoMatrix* placement, const Point3D& p) +{ + if (placement == nullptr) { + return p; + } + Point3D out{}; + placement->MasterToLocal(p.data(), out.data()); + return out; +} + +/// A direction of the part frame, expressed in the shape's own frame. A rigid transform preserves +/// lengths, so every distance the oracle states along a ray is unchanged by this -- which is why +/// the oracle's answers can be compared against the transformed query without touching them. +Ray toLocal(const TGeoMatrix* placement, const Ray& r) +{ + if (placement == nullptr) { + return r; + } + Ray out{}; + placement->MasterToLocal(r.origin.data(), out.origin.data()); + placement->MasterToLocalVect(r.dir.data(), out.dir.data()); + return out; +} + +/// Transformed copies of a sample vector. Returns an EMPTY vector when there is no placement, so +/// that the overwhelmingly common unplaced case selects the caller's own vector by reference and +/// copies nothing. +template +std::vector toLocal(const TGeoMatrix* placement, const std::vector& in) +{ + if (placement == nullptr) { + return {}; + } + std::vector out; + out.reserve(in.size()); + for (const auto& item : in) { + out.push_back(toLocal(placement, item)); + } + return out; +} + +/// How the shape computes Capacity(), and therefore whether comparing it against the OCCT volume +/// is a measurement or noise. +/// +/// `TGeoCompositeShape::Capacity()` throws 10000 accepted Monte-Carlo points into the bounding +/// box (TGeoCompositeShape.cxx:282), so its relative error is ~1e-2 -- four orders of magnitude +/// above the 1e-6 gate band. It is reported, and explicitly marked not comparable, rather than +/// silently producing a failure that means nothing. Every other ROOT shape in this version +/// computes Capacity in closed form (checked: TGeoCompositeShape is the only Capacity() in +/// geom/geom/src that touches gRandom). +struct CapacityKind { + const char* method = "root-analytic"; + bool comparable = true; +}; + +bool usesMonteCarloCapacity(const TGeoShape* shape) +{ + if (shape == nullptr) { + return false; + } + if (shape->InheritsFrom(TGeoCompositeShape::Class())) { + return true; + } + // TGeoScaledShape::Capacity() forwards to the shape it wraps, so a scaled composite is just as + // sampled as a bare one. + if (const auto* scaled = dynamic_cast(shape)) { + return usesMonteCarloCapacity(scaled->GetShape()); + } + return false; +} + +CapacityKind capacityKindOf(const Representation& rep) +{ + if (rep.surfaceSolid != nullptr) { + // Divergence theorem in closed form over the analytic faces. + return {"exact-divergence", true}; + } + if (dynamic_cast(rep.shape) != nullptr) { + // Exact for the mesh (signed tetrahedra over its own triangles), deterministic, and therefore + // a real measurement -- of the chording deficit, not of a bug. + return {"mesh-divergence", true}; + } + if (usesMonteCarloCapacity(rep.shape)) { + return {"root-montecarlo", false}; + } + return {"root-analytic", true}; +} + +/// Max deviation, in cm, between a shape's own bounding box and the oracle's, over all six faces. +/// +/// This is the frame check. A TGeoShape answers in its own local frame and the oracle answers in +/// the .brep's; if an emitter writes a shape in the assembly frame instead of the part frame, +/// every column below fills with plausible-looking nonsense and nothing else would notice. +/// Returns -1 when the shape does not derive from TGeoBBox (nothing in ROOT's shape library that +/// matters here fails that) or when the answer file predates the bbox fields. +/// +/// With a placement, the shape's box has to be carried into the part frame before it can be +/// compared, and the only frame-independent way to do that is to transform the eight corners and +/// take their axis-aligned hull. For a rotated body that hull is strictly larger than the body, so +/// the number becomes conservative -- exactly as it already was for a TGeoCompositeShape, whose +/// TGeoBoolNode::ComputeBBox does the same thing internally. It is a *frame* check, not a +/// tightness measurement, and it still moves by the size of a frame error. +double bboxDeviationFromOracle(const TGeoShape* shape, const OracleAnswers& oracle, + const TGeoMatrix* placement = nullptr) +{ + if (!oracle.hasBbox) { + return -1.; + } + const auto* box = dynamic_cast(shape); + if (box == nullptr) { + return -1.; + } + const double half[3] = {box->GetDX(), box->GetDY(), box->GetDZ()}; + double lo[3]; + double hi[3]; + for (int i = 0; i < 3; ++i) { + lo[i] = box->GetOrigin()[i] - half[i]; + hi[i] = box->GetOrigin()[i] + half[i]; + } + if (placement != nullptr) { + double outLo[3] = {1.e300, 1.e300, 1.e300}; + double outHi[3] = {-1.e300, -1.e300, -1.e300}; + for (int corner = 0; corner < 8; ++corner) { + const double local[3] = {(corner & 1) ? hi[0] : lo[0], (corner & 2) ? hi[1] : lo[1], + (corner & 4) ? hi[2] : lo[2]}; + double master[3]; + placement->LocalToMaster(local, master); + for (int i = 0; i < 3; ++i) { + outLo[i] = std::min(outLo[i], master[i]); + outHi[i] = std::max(outHi[i], master[i]); + } + } + std::copy(std::begin(outLo), std::end(outLo), std::begin(lo)); + std::copy(std::begin(outHi), std::end(outHi), std::begin(hi)); + } + double worst = 0.; + for (int i = 0; i < 3; ++i) { + worst = std::max(worst, std::fabs(lo[i] - oracle.bboxMin[i])); + worst = std::max(worst, std::fabs(hi[i] - oracle.bboxMax[i])); + } + return worst; +} + +/// Everything the gate reads out of one (candidate, oracle answers) pair. Deliberately typed on +/// TGeoShape*: this is the whole point of the abstraction. +/// +/// The returned object is exactly the historical `partJson["oracle"]` block, so the surface +/// representation's columns are produced by the same code that produced them before and the +/// existing path stays inert. +json scoreAgainstOracle(const TGeoShape* candidate, const OracleAnswers& oracle, + const ValidationOptions& oracleOpt, const std::vector& allPointsIn, + const std::vector& containsState, + const std::vector& boundaryDistance, const SampleSet& samplesIn, + const std::set& only, const std::string& label, + const std::string& capacityLabel, const TGeoMatrix* placement = nullptr) +{ + // The samples are stated in the part frame -- the frame the oracle answered in. A shape that + // carries a placement answers in its own, so the *questions* move and the answers do not: a + // rigid transform preserves both the inside/outside relation and every distance along a ray. + const std::vector localPoints = toLocal(placement, allPointsIn); + const std::vector localOutsideRays = toLocal(placement, samplesIn.outsideRays); + const std::vector localInsideRays = toLocal(placement, samplesIn.insideRays); + const std::vector& allPoints = placement != nullptr ? localPoints : allPointsIn; + const std::vector& outsideRays = + placement != nullptr ? localOutsideRays : samplesIn.outsideRays; + const std::vector& insideRays = placement != nullptr ? localInsideRays : samplesIn.insideRays; + json oracleJson; + oracleJson["tolerance"] = oracle.tolerance; + oracleJson["capacity"] = oracle.capacity; + oracleJson["valid"] = oracle.valid; + const double capacity = candidate->Capacity(); + oracleJson["capacityCandidate"] = capacity; + oracleJson["capacityRelativeDeviation"] = + oracle.capacity != 0. ? (capacity - oracle.capacity) / oracle.capacity : 0.; + std::printf(" %s: capacity candidate=%.6g reference=%.6g relDev=%.3g\n", capacityLabel.c_str(), + capacity, oracle.capacity, oracleJson["capacityRelativeDeviation"].get()); + + if (only.count("contains")) { + auto v = validateContainsAgainstOracle(candidate, allPoints, containsState, boundaryDistance, + oracleOpt); + printValidation(label + ":contains", v); + oracleJson["contains"] = validationToJson(v); + } + const auto originStateFor = [&oracle](const char* category) { + const auto it = oracle.originContains.find(category); + return it == oracle.originContains.end() ? std::vector{} : it->second; + }; + if (only.count("distout")) { + const auto it = oracle.distOutside.find("outside"); + if (it != oracle.distOutside.end()) { + auto v = validateDistanceAgainstOracle(candidate, outsideRays, it->second, + /*wantInside=*/false, oracleOpt, + originStateFor("outside")); + printValidation(label + ":distout", v); + oracleJson["distout"] = validationToJson(v); + } + } + if (only.count("distin")) { + const auto it = oracle.distInside.find("inside"); + if (it != oracle.distInside.end()) { + auto v = validateDistanceAgainstOracle(candidate, insideRays, it->second, + /*wantInside=*/true, oracleOpt, originStateFor("inside")); + printValidation(label + ":distin", v); + oracleJson["distin"] = validationToJson(v); + } + } + if (only.count("safety")) { + auto v = validateSafetyAgainstOracle(candidate, allPoints, boundaryDistance, oracleOpt); + printValidation(label + ":safety", v); + oracleJson["safety"] = validationToJson(v); + } + return oracleJson; +} + +/// Disagreements outside tolerance, summed over the four columns. This is the invariant the +/// project defends and it is a *different* number from the gate total, so it is computed once +/// here and reported next to every representation rather than reconstructed by each consumer. +size_t countDisagreements(const json& oracleJson) +{ + size_t bad = 0; + for (const char* key : {"contains", "distout", "distin", "safety"}) { + if (!oracleJson.contains(key)) { + continue; + } + const auto& column = oracleJson.at(key); + bad += column.value("nMismatchUnexplained", size_t{0}); + bad += column.value("nMismatchMissedSurface", size_t{0}); + } + return bad; +} + +} // namespace + +int main(int argc, char** argv) +{ + Options opt; + try { + if (!parseArgs(argc, argv, opt)) { + return 0; + } + } catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; + printUsage(argv[0]); + return 1; + } + + std::vector parts; + try { + parts = collectParts(opt); + } catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; + return 1; + } + if (parts.empty()) { + std::cerr << "no parts matched (pattern='" << opt.partsPattern << "')\n"; + return 1; + } + + json jsonReport = json::array(); + std::vector unreliableParts; + + for (const auto& part : parts) { + std::printf("=== %s (%s) ===\n", part.id.c_str(), part.model.c_str()); + + O2BVHSurfaceSolid surf(part.id.c_str()); + if (!LoadSurfaceSolid(part.surfaces, surf)) { + std::cerr << " skip: LoadSurfaceSolid failed for " << part.surfaces << "\n"; + continue; + } + auto t0 = std::chrono::steady_clock::now(); + surf.CloseShape(true); + auto t1 = std::chrono::steady_clock::now(); + const double surfCloseSeconds = toSeconds(t0, t1); + + O2Tessellated mesh(part.id.c_str()); + if (!LoadFacetSolid(part.facets, mesh)) { + std::cerr << " skip: LoadFacetSolid failed for " << part.facets << "\n"; + continue; + } + t0 = std::chrono::steady_clock::now(); + mesh.CloseShape(); + t1 = std::chrono::steady_clock::now(); + const double meshCloseSeconds = toSeconds(t0, t1); + + const TGeoShape* candidate = &surf; + const TGeoShape* reference = &mesh; + + // Every representation this part has, in the order they are reported. `surface` first so the + // historical candidate keeps its place; `mesh` second because it is also the sampling + // reference; `shape` last because it is optional and does not exist yet for any converted + // part -- it is the slot the CSG emitter writes into. + std::vector representations; + representations.push_back({"surface", part.surfaces, &surf, &surf, surf.GetNsurfaces(), "patches"}); + representations.push_back({"mesh", part.facets, &mesh, nullptr, mesh.GetNfacets(), "triangles"}); + std::unique_ptr rootShape; + std::unique_ptr rootShapePlacement; + if (fileExists(part.shape)) { + std::string shapeError; + rootShape.reset(loadShapeFromRootFile(part.shape, &shapeError)); + if (rootShape) { + rootShapePlacement.reset(loadShapePlacementFromRootFile(part.shape)); + std::printf(" shape sidecar: %s -> %s \"%s\"%s\n", part.shape.c_str(), + rootShape->ClassName(), rootShape->GetName(), + rootShapePlacement ? " (placed: queries are transformed into its own frame)" + : ""); + representations.push_back({"shape", part.shape, rootShape.get(), nullptr, -1, + rootShape->ClassName(), rootShapePlacement.get()}); + } else { + std::printf(" shape sidecar: *** %s\n", shapeError.c_str()); + } + } + + const Point3D bboxMin{mesh.GetOrigin()[0] - mesh.GetDX(), mesh.GetOrigin()[1] - mesh.GetDY(), + mesh.GetOrigin()[2] - mesh.GetDZ()}; + const Point3D bboxMax{mesh.GetOrigin()[0] + mesh.GetDX(), mesh.GetOrigin()[1] + mesh.GetDY(), + mesh.GetOrigin()[2] + mesh.GetDZ()}; + + std::printf(" surfaces=%d triangles=%d closeShape: surface=%.4fs mesh=%.4fs\n", surf.GetNsurfaces(), + mesh.GetNfacets(), surfCloseSeconds, meshCloseSeconds); + + // Label every measurement with whether its subject is a closed manifold at all. + const auto reliability = surf.GetNavigationReliability(); + const char* reliabilityName = O2BVHSurfaceSolid::GetNavigationReliabilityName(reliability); + const bool navigable = surf.IsNavigable(); + std::printf(" navigation: %s%s (boundary=%d non-manifold=%d reversed=%d)\n", reliabilityName, + navigable ? "" : " *** UNRELIABLE: results below are not a measurement of accuracy ***", + surf.GetBoundaryEdgeCount(), surf.GetNonManifoldEdgeCount(), surf.GetReversedEdgeCount()); + // The same boundary measured as curves, in cm. The isolation is how alone the loneliest chord + // is, *not* a seam width; the chord resolution is next to it because it is what widens the + // band each chord is matched in, over the declared tolerance. + std::printf( + " rim isolation: max %.3g cm (chord resolution %.3g cm, declared tolerance %.3g cm); rims %d " + "(matched=%d boundary=%d non-manifold=%d reversed=%d), open %.3g of %.3g cm\n", + surf.GetMaxRimIsolation(), surf.GetRimChordResolution(), surf.GetRimMatchTolerance(), surf.GetRimCount(), + surf.GetMatchedRimCount(), surf.GetBoundaryRimCount(), surf.GetNonManifoldRimCount(), + surf.GetReversedRimCount(), surf.GetUnmatchedRimLength(), surf.GetTotalRimLength()); + // Sidecar v3: closure decided by edge *identity* rather than by proximity. The + // deviation is a measured cm number and deliberately not a verdict -- it says how far the two + // faces that provably share an edge actually are, which is the first defensible answer this + // project has had to that question. Always in --json; on stdout only when asked, because a + // 19-part run is already dense. + if (opt.edgeIdentity) { + if (surf.HasEdgeIdentity()) { + std::printf( + " edge identity: %d source edge(s) (shared=%d boundary=%d non-manifold=%d " + "reversed=%d degenerate=%d), max shared-edge deviation %.4g cm\n", + surf.GetSourceEdgeCount(), surf.GetSharedSourceEdgeCount(), + surf.GetBoundarySourceEdgeCount(), surf.GetNonManifoldSourceEdgeCount(), + surf.GetReversedSourceEdgeCount(), surf.GetDegenerateSourceEdgeCount(), + surf.GetMaxSharedEdgeDeviation()); + } else { + std::printf(" edge identity: absent (sidecar predates v3); closure fell back to proximity\n"); + } + } + // Name the offending rims; the line above gives only their count and length. + json rimsJson = json::array(); + for (const auto& rim : surf.GetRimReports()) { + const char* stateName = O2BVHSurfaceSolid::GetNavigationReliabilityName(rim.state); + const bool clean = rim.state == O2BVHSurfaceSolid::NavigationReliability::Reliable; + if (opt.allRims || !clean) { + std::printf( + " rim face=%d loop=%d %s %s: %d chords, %.4g cm (%d chords / %.4g cm unmatched); " + "loneliest chord %.3g cm from face %d at (%.4g, %.4g, %.4g)\n", + rim.surface, rim.rimOnSurface, rim.closed ? "closed" : "OPEN-CHAIN", stateName, rim.chords, + rim.length, rim.unmatchedChords, rim.unmatchedLength, rim.maxIsolation, rim.maxIsolationFace, + rim.maxIsolationPoint[0], rim.maxIsolationPoint[1], rim.maxIsolationPoint[2]); + } + rimsJson.push_back({{"face", rim.surface}, + {"loop", rim.rimOnSurface}, + {"state", stateName}, + {"closed", rim.closed}, + {"chords", rim.chords}, + {"unmatchedChords", rim.unmatchedChords}, + {"length", rim.length}, + {"unmatchedLength", rim.unmatchedLength}, + {"maxIsolation", rim.maxIsolation}, + {"maxIsolationFace", rim.maxIsolationFace}, + {"maxIsolationPoint", rim.maxIsolationPoint}}); + } + if (!navigable) { + unreliableParts.push_back(part.id + " (" + reliabilityName + ")"); + } + + SampleConfig cfg; + cfg.nBulk = opt.points; + cfg.nBoundary = opt.points; + cfg.nInside = std::max(1, opt.points / 2); + cfg.nOutsideRays = opt.rays; + cfg.nInsideRays = std::max(1, opt.rays / 2); + cfg.seed = opt.seed; + const SampleSet samples = opt.loadSamples.empty() ? generateSamples(reference, bboxMin, bboxMax, cfg) + : readSamples(opt.loadSamples, part.id); + + long long candidatesSampled = 0; + const size_t nProbe = std::min(200, samples.outsideRays.size()); + for (size_t i = 0; i < nProbe; ++i) { + const auto& r = samples.outsideRays[i]; + const int n = surf.CountBVHRayCandidates(r.origin, r.dir); + if (n > 0) { + candidatesSampled += n; + } + } + std::printf(" BVH ray candidates: sum=%lld over %zu probe rays\n", candidatesSampled, nProbe); + + json partJson; + partJson["id"] = part.id; + partJson["model"] = part.model; + partJson["nSurfaces"] = surf.GetNsurfaces(); + partJson["nTriangles"] = mesh.GetNfacets(); + partJson["closeShapeSecondsSurface"] = surfCloseSeconds; + partJson["closeShapeSecondsMesh"] = meshCloseSeconds; + partJson["bvhRayCandidatesSampled"] = candidatesSampled; + partJson["bvhRayCandidatesProbeRays"] = nProbe; + partJson["navigation"] = {{"reliability", reliabilityName}, + {"navigable", navigable}, + {"boundaryEdges", surf.GetBoundaryEdgeCount()}, + {"nonManifoldEdges", surf.GetNonManifoldEdgeCount()}, + {"reversedEdges", surf.GetReversedEdgeCount()}, + {"maxRimIsolation", surf.GetMaxRimIsolation()}, + {"rimChordResolution", surf.GetRimChordResolution()}, + {"rimMatchTolerance", surf.GetRimMatchTolerance()}, + {"totalRimLength", surf.GetTotalRimLength()}, + {"unmatchedRimLength", surf.GetUnmatchedRimLength()}, + {"rims", surf.GetRimCount()}, + {"matchedRims", surf.GetMatchedRimCount()}, + {"boundaryRims", surf.GetBoundaryRimCount()}, + {"nonManifoldRims", surf.GetNonManifoldRimCount()}, + {"reversedRims", surf.GetReversedRimCount()}, + {"hasEdgeIdentity", surf.HasEdgeIdentity()}, + {"sourceEdges", surf.GetSourceEdgeCount()}, + {"sharedSourceEdges", surf.GetSharedSourceEdgeCount()}, + {"boundarySourceEdges", surf.GetBoundarySourceEdgeCount()}, + {"nonManifoldSourceEdges", surf.GetNonManifoldSourceEdgeCount()}, + {"reversedSourceEdges", surf.GetReversedSourceEdgeCount()}, + {"degenerateSourceEdges", surf.GetDegenerateSourceEdgeCount()}, + {"maxSharedEdgeDeviation", surf.GetMaxSharedEdgeDeviation()}, + {"rimDetail", rimsJson}}; + + std::vector allPoints = samples.bulkPoints; + allPoints.insert(allPoints.end(), samples.boundaryPoints.begin(), samples.boundaryPoints.end()); + allPoints.insert(allPoints.end(), samples.insidePoints.begin(), samples.insidePoints.end()); + const std::array categorySizes{samples.bulkPoints.size(), samples.boundaryPoints.size(), + samples.insidePoints.size()}; + + if (!opt.dumpSamples.empty()) { + writeSamples(opt.dumpSamples, part.id, samples); + } + + // Ground-truth validation, when the oracle has answered this part. Kept separate from the + // mesh comparison below rather than replacing it: the mesh columns stay comparable with every + // measurement recorded so far, while these columns are the ones a gate can be written against. + if (!opt.refAnswers.empty()) { + const OracleAnswers oracle = loadOracleAnswers(opt.refAnswers, part.id); + if (oracle.has) { + ValidationOptions oracleOpt; + // The band is now the model's own declared tolerance instead of a guessed mesh sagitta. + // A floor keeps a perfectly-toleranced synthetic fixture from demanding bit equality. + oracleOpt.meshBand = std::max(oracle.tolerance, oracleOpt.distanceTolerance); + const auto boundaryDistance = + mergeCategories(oracle.boundaryDistance, categorySizes, -1.); + const auto containsState = mergeCategories(oracle.containsState, categorySizes, -1); + + std::printf(" oracle: %s tolerance=%.3g capacity=%.6g cm^3 (band=%.3g)\n", + oracle.valid ? "valid" : "*** NOT BRepCheck-VALID ***", oracle.tolerance, + oracle.capacity, oracleOpt.meshBand); + + // The historical block, unchanged in content: the exact-surface representation's columns + // under `oracle`, printed with the same "O:" labels. Everything written here before this + // refactor is still written here, by the same code, so the existing path is inert. + json oracleJson = scoreAgainstOracle(candidate, oracle, oracleOpt, allPoints, containsState, + boundaryDistance, samples, opt.only, "O", "oracle"); + partJson["oracle"] = oracleJson; + + // New: the same four columns for every other representation the part has, against the + // same answers. This is what makes a CSG-emitted or tessellated part scoreable at all, + // and it is the shape the tiered coverage scorecard needs -- parallel columns, not + // alternatives behind a flag. + json representationsJson = json::array(); + for (const auto& rep : representations) { + const bool isSurface = rep.surfaceSolid != nullptr; + json repJson; + repJson["name"] = rep.name; + repJson["source"] = rep.source; + repJson["shapeClass"] = rep.shape->ClassName(); + if (rep.primitives >= 0) { + repJson["primitives"] = rep.primitives; + repJson["primitiveKind"] = rep.primitiveKind; + } + const auto capacityKind = capacityKindOf(rep); + repJson["capacityMethod"] = capacityKind.method; + repJson["capacityComparable"] = capacityKind.comparable; + // The frame check, per representation: a candidate whose box does not sit where the + // oracle's box sits is not being asked the same questions the oracle answered. + repJson["bboxDeviationFromOracle"] = + bboxDeviationFromOracle(rep.shape, oracle, rep.placement); + // The placement, mirrored into the scorecard as a 3x4 row-major [R | t] so that a + // Python consumer never has to open the .root file, and null when there is none. + if (rep.placement != nullptr) { + const double* rot = rep.placement->GetRotationMatrix(); + const double* tr = rep.placement->GetTranslation(); + repJson["placement"] = {{rot[0], rot[1], rot[2], tr[0]}, + {rot[3], rot[4], rot[5], tr[1]}, + {rot[6], rot[7], rot[8], tr[2]}}; + } else { + repJson["placement"] = nullptr; + } + + // Closure / rims / NavigationReliability are O2BVHSurfaceSolid concepts. A + // TGeoCompositeShape has neither, and a triangle mesh has a different notion entirely, + // so those keys exist only where the question has an answer. `closureApplicable` + // records the decision explicitly instead of leaving a reader to infer it from an + // absent field. + repJson["closureApplicable"] = isSurface; + if (isSurface) { + repJson["reliability"] = reliabilityName; + repJson["navigable"] = navigable; + } else if (rep.name == "mesh") { + // O2Tessellated's own, differently-named watertightness statement. Deliberately not + // called `navigable`: it is a property of the triangle soup, decided by half-edge + // counting over chords, and it is not the same claim. + repJson["meshClosedBody"] = mesh.IsClosedBody(); + } + + if (isSurface) { + // Already computed above; scoring the same shape twice would only cost time and + // invite the two copies to drift. + repJson["oracle"] = oracleJson; + } else { + std::printf(" --- representation '%s' (%s) against the same oracle answers ---\n", + rep.name.c_str(), rep.shape->ClassName()); + repJson["oracle"] = scoreAgainstOracle(rep.shape, oracle, oracleOpt, allPoints, + containsState, boundaryDistance, samples, + opt.only, "R:" + rep.name, + "oracle[" + rep.name + "]", rep.placement); + } + repJson["disagreements"] = countDisagreements(repJson["oracle"]); + representationsJson.push_back(std::move(repJson)); + } + partJson["representations"] = std::move(representationsJson); + } + } + + if (opt.only.count("contains")) { + auto v = validateContains(candidate, reference, allPoints); + printValidation("contains", v); + auto tc = timeContains(candidate, allPoints, opt.warmup, opt.repeat); + auto tr = timeContains(reference, allPoints, opt.warmup, opt.repeat); + printTiming("contains", tc, tr); + partJson["contains"] = {{"validation", validationToJson(v)}, + {"timingCandidate", timingToJson(tc)}, + {"timingReference", timingToJson(tr)}}; + } + if (opt.only.count("distout")) { + auto v = validateDistFromOutside(candidate, reference, samples.outsideRays); + printValidation("distout", v); + auto tc = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat); + auto tr = timeDistFromOutside(reference, samples.outsideRays, opt.warmup, opt.repeat); + printTiming("distout", tc, tr); + // the all-surfaces baseline: what the BVH traversal buys over visiting every patch + auto tl = timeRayKernel(samples.outsideRays, opt.warmup, opt.repeat, + [&surf](const Point3D& o, const Point3D& d) { + return surf.DistFromOutside_Loop(o.data(), d.data()); + }); + printLoopSpeedup("distout", tc, tl); + partJson["distout"] = {{"validation", validationToJson(v)}, + {"timingCandidate", timingToJson(tc)}, + {"timingReference", timingToJson(tr)}, + {"timingCandidateLoop", timingToJson(tl)}}; + } + if (opt.only.count("distin")) { + auto v = validateDistFromInside(candidate, reference, samples.insideRays); + printValidation("distin", v); + auto tc = timeDistFromInside(candidate, samples.insideRays, opt.warmup, opt.repeat); + auto tr = timeDistFromInside(reference, samples.insideRays, opt.warmup, opt.repeat); + printTiming("distin", tc, tr); + auto tl = timeRayKernel(samples.insideRays, opt.warmup, opt.repeat, + [&surf](const Point3D& o, const Point3D& d) { + return surf.DistFromInside_Loop(o.data(), d.data()); + }); + printLoopSpeedup("distin", tc, tl); + partJson["distin"] = {{"validation", validationToJson(v)}, + {"timingCandidate", timingToJson(tc)}, + {"timingReference", timingToJson(tr)}, + {"timingCandidateLoop", timingToJson(tl)}}; + } + if (opt.only.count("safety")) { + // Never compared against each other (see ground rules): each shape's Safety() is checked + // against its own DistFrom{Inside,Outside} contract independently. + auto vc = validateSafety(candidate, allPoints); + auto vr = validateSafety(reference, allPoints); + printValidation("safety(cand)", vc); + printValidation("safety(ref)", vr); + auto tc = timeSafety(candidate, allPoints, opt.warmup, opt.repeat); + auto tr = timeSafety(reference, allPoints, opt.warmup, opt.repeat); + printTiming("safety", tc, tr); + partJson["safety"] = {{"validationCandidate", validationToJson(vc)}, + {"validationReference", validationToJson(vr)}, + {"timingCandidate", timingToJson(tc)}, + {"timingReference", timingToJson(tr)}}; + } + + if (opt.loopCrosscheck) { + // Independent of the tessellated reference entirely: separates + // BVH/traversal bugs from surface-kernel bugs. + // The distance twins must agree *exactly*, not within a tolerance: both take a + // minimum over the same hits from the same kernels and differ only in which surfaces the + // BVH lets them skip, so any difference at all is a traversal or pruning bug. + size_t containsAgree = 0; + size_t crossingDumps = 0; + constexpr size_t kMaxCrossingDumps = 3; + std::vector bvhCrossings; + std::vector loopCrossings; + for (const auto& p : allPoints) { + if (surf.Contains(p.data()) == surf.Contains_Loop(p.data())) { + ++containsAgree; + continue; + } + // A parity disagreement between two paths over the same kernels means the two hit lists + // differ. Print them: the difference is the diagnosis, and guessing at it has already + // cost this project one three-item plan built on a wrong premise. + if (crossingDumps++ >= kMaxCrossingDumps) { + continue; + } + surf.DescribeContainsCrossings(p, bvhCrossings, loopCrossings); + std::printf(" BVH!=Loop at (%.9g,%.9g,%.9g): BVH=%d (%zu crossings) Loop=%d (%zu crossings)\n", + p[0], p[1], p[2], static_cast(surf.Contains(p.data())), bvhCrossings.size(), + static_cast(surf.Contains_Loop(p.data())), loopCrossings.size()); + const size_t nShow = std::max(bvhCrossings.size(), loopCrossings.size()); + for (size_t i = 0; i < nShow; ++i) { + const char* bvhKind = i < bvhCrossings.size() + ? (bvhCrossings[i].normalAlignment < 0. ? "ENTER" : "EXIT ") + : "-----"; + const char* loopKind = i < loopCrossings.size() + ? (loopCrossings[i].normalAlignment < 0. ? "ENTER" : "EXIT ") + : "-----"; + const double bvhT = i < bvhCrossings.size() ? bvhCrossings[i].distance : -1.; + const double loopT = i < loopCrossings.size() ? loopCrossings[i].distance : -1.; + std::printf(" [%2zu] BVH %s t=%-18.12g Loop %s t=%-18.12g%s\n", i, bvhKind, bvhT, + loopKind, loopT, + (i < bvhCrossings.size() && i < loopCrossings.size() && + std::fabs(bvhT - loopT) > 1.e-12) + ? " <-- differs" + : ""); + } + } + std::printf(" loop-crosscheck contains: BVH==Loop for %zu/%zu points\n", containsAgree, allPoints.size()); + partJson["loopCrosscheckContains"] = {{"agree", containsAgree}, {"total", allPoints.size()}}; + + size_t outAgree = 0; + double worstOutDeviation = 0.; + for (const auto& r : samples.outsideRays) { + const double bvh = surf.DistFromOutside(r.origin.data(), r.dir.data(), 3); + const double loop = surf.DistFromOutside_Loop(r.origin.data(), r.dir.data()); + if (bvh == loop) { + ++outAgree; + } else { + worstOutDeviation = std::max(worstOutDeviation, std::fabs(bvh - loop)); + } + } + std::printf(" loop-crosscheck distout : BVH==Loop for %zu/%zu rays (worstDev=%.6g)\n", outAgree, + samples.outsideRays.size(), worstOutDeviation); + partJson["loopCrosscheckDistOutside"] = { + {"agree", outAgree}, {"total", samples.outsideRays.size()}, {"worstDeviation", worstOutDeviation}}; + + size_t inAgree = 0; + double worstInDeviation = 0.; + for (const auto& r : samples.insideRays) { + const double bvh = surf.DistFromInside(r.origin.data(), r.dir.data(), 3); + const double loop = surf.DistFromInside_Loop(r.origin.data(), r.dir.data()); + if (bvh == loop) { + ++inAgree; + } else { + worstInDeviation = std::max(worstInDeviation, std::fabs(bvh - loop)); + } + } + std::printf(" loop-crosscheck distin : BVH==Loop for %zu/%zu rays (worstDev=%.6g)\n", inAgree, + samples.insideRays.size(), worstInDeviation); + partJson["loopCrosscheckDistInside"] = { + {"agree", inAgree}, {"total", samples.insideRays.size()}, {"worstDeviation", worstInDeviation}}; + } + + if (opt.pruningAb) { + // Prices the ray tmax tightening: the same rays run with it on and off, reporting both the + // surface patches the traversal actually handed to the leaf callback and the wall time. The + // answers must be bit-identical -- the switch is a cost knob, never a semantic one, and a + // mismatch here is a bug in the tightening rather than a measurement. + json pruningJson; + size_t identical = 0; + std::vector prunedValues; + prunedValues.reserve(samples.outsideRays.size()); + + O2BVHSurfaceSolid::SetRayTMaxPruning(true); + O2BVHSurfaceSolid::ResetRayCandidateCounter(); + for (const auto& r : samples.outsideRays) { + prunedValues.push_back(surf.DistFromOutside(r.origin.data(), r.dir.data(), 3)); + } + const long long prunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount(); + auto tPruned = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat); + + O2BVHSurfaceSolid::SetRayTMaxPruning(false); + O2BVHSurfaceSolid::ResetRayCandidateCounter(); + for (size_t i = 0; i < samples.outsideRays.size(); ++i) { + const auto& r = samples.outsideRays[i]; + if (surf.DistFromOutside(r.origin.data(), r.dir.data(), 3) == prunedValues[i]) { + ++identical; + } + } + const long long unprunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount(); + auto tUnpruned = timeDistFromOutside(candidate, samples.outsideRays, opt.warmup, opt.repeat); + O2BVHSurfaceSolid::SetRayTMaxPruning(true); + + const double candidateRatio = + unprunedCandidates > 0 ? static_cast(prunedCandidates) / static_cast(unprunedCandidates) : 0.; + const double speedup = tPruned.nsPerCall > 0. ? tUnpruned.nsPerCall / tPruned.nsPerCall : 0.; + std::printf(" tmax-pruning A/B (distout, %zu rays): identical=%zu/%zu\n", samples.outsideRays.size(), identical, + samples.outsideRays.size()); + std::printf(" candidates: pruned=%lld unpruned=%lld (%.1f%% of the work)\n", prunedCandidates, + unprunedCandidates, 100. * candidateRatio); + std::printf(" time : pruned=%9.1f ns/call unpruned=%9.1f ns/call speedup=%.2fx\n", + tPruned.nsPerCall, tUnpruned.nsPerCall, speedup); + + pruningJson["identical"] = identical; + pruningJson["total"] = samples.outsideRays.size(); + pruningJson["candidatesPruned"] = prunedCandidates; + pruningJson["candidatesUnpruned"] = unprunedCandidates; + pruningJson["timingPruned"] = timingToJson(tPruned); + pruningJson["timingUnpruned"] = timingToJson(tUnpruned); + partJson["tmaxPruningAB"] = std::move(pruningJson); + } + + jsonReport.push_back(std::move(partJson)); + } + + // Repeated at the end because per-part lines scroll away in a 19-part run, and because the whole + // point of item 4 is that no future reader can attribute an "unexplained" column to mesh + // chording without first seeing whether the subject was a closed manifold at all. + if (!unreliableParts.empty()) { + std::printf( + "\n*** %zu of %zu part(s) are NOT navigable; their accuracy columns above measure an\n" + "*** undefined answer, not the exact solid's error.\n", + unreliableParts.size(), parts.size()); + for (const auto& id : unreliableParts) { + std::printf("*** %s\n", id.c_str()); + } + } else { + std::printf("\nAll %zu part(s) closed consistently oriented manifolds: navigation results are meaningful.\n", + parts.size()); + } + + // The tiered scorecard, in its most compact form: how many disagreements outside tolerance each + // representation of each part has. Printed here because the per-part blocks scroll away, and + // because "which representation would have accepted this part" is the question the converter's + // dispatch policy will be written against. + bool anyRepresentations = false; + for (const auto& partJson : jsonReport) { + anyRepresentations = anyRepresentations || partJson.contains("representations"); + } + if (anyRepresentations) { + std::printf("\n=== REPRESENTATION SCORECARD (disagreements outside tolerance, all four columns) ===\n"); + for (const auto& partJson : jsonReport) { + if (!partJson.contains("representations")) { + continue; + } + std::printf(" %-46s", partJson.at("id").get().c_str()); + for (const auto& rep : partJson.at("representations")) { + const double capacityDeviation = + rep.at("oracle").value("capacityRelativeDeviation", 0.); + const bool capacityComparable = rep.value("capacityComparable", false); + char capacityText[32]; + if (capacityComparable) { + std::snprintf(capacityText, sizeof(capacityText), "%.2g", std::fabs(capacityDeviation)); + } else { + std::snprintf(capacityText, sizeof(capacityText), "n/a"); + } + std::printf(" %s=%zu (cap %s)", rep.at("name").get().c_str(), + rep.value("disagreements", size_t{0}), capacityText); + } + std::printf("\n"); + } + } + + if (!opt.jsonOut.empty()) { + std::ofstream out(opt.jsonOut); + out << jsonReport.dump(1); + std::printf("\nWrote %s\n", opt.jsonOut.c_str()); + } + + return 0; +} diff --git a/Detectors/CADSupport/test/runXRayBenchmark.cxx b/Detectors/CADSupport/test/runXRayBenchmark.cxx new file mode 100644 index 0000000000000..a201334e5a632 --- /dev/null +++ b/Detectors/CADSupport/test/runXRayBenchmark.cxx @@ -0,0 +1,2225 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +/// \file runXRayBenchmark.cxx +/// \brief X-ray / geantino transport benchmark: ordered crossing lists, by stepping. +/// +/// Built as `o2-bench-cadsupport-xray`. +/// +/// WHY THIS EXISTS, in one paragraph. Everything the oracle gate measures is a *single-shot* +/// query: from a sampled point, how far to the surface. A transport loop is different in kind -- +/// step, land *on* the boundary, step again from there -- and that is where geometry navigation +/// actually fails: zero-length steps, ping-ponging on a face, a particle that enters and never +/// exits, a crossing found twice, a step that overshoots into the next volume. None of those can +/// be expressed as a disagreement on `distout` from an interior sample, so the existing gate is +/// structurally blind to all of them. This benchmark shoots a structured parallel-beam raster +/// through a part and produces, per ray, the ORDERED CROSSING LIST -- the sequence of entry/exit +/// distances -- by stepping, two independent ways, and compares the lists (not aggregates) +/// against OpenCascade. +/// +/// TWO STEPPING MODES, and the reason both exist: +/// (a) `shape` -- a direct shape-API loop: Contains() to establish the starting state, then +/// alternating DistFromOutside()/DistFromInside(), advancing the point, until +/// the ray leaves the raster window. Depends on nothing but the shape. +/// (b) `nav` -- the real TGeoNavigator: the part placed in a TGeoVolume inside a minimal +/// world, transported with FindNextBoundaryAndStep(). The production path. +/// If (a) and (b) disagree, that isolates *the shape* from *the navigator* immediately; with only +/// (b) one cannot tell which of the two lied. Both are always reported. +/// +/// THREE-STAGE ROUND TRIP, mirroring the oracle gate: +/// 1. `--dump-rays D` writes D/xrays_.json: the raster window and every ray. +/// 2. `xrayOracle.py` answers exactly those rays from the part's .brep, in OpenCascade, +/// into D/crossings_.json. +/// 3. `--ref-crossings D` steps both modes over the same rays and scores the lists. +/// The rays are written and read rather than regenerated on both sides for the same reason the +/// sample sets are: a comparison is only evidence if both sides answered the same question. +/// +/// NOT REQUIRED: a tessellated mesh. The raster is structured and deterministic, so unlike +/// `generateSamples()` nothing here rejection-samples through `O2Tessellated`. That is what makes +/// this instrument runnable on a model whose meshing does not fit in memory. + +#include "RepresentationBench.h" +#include "XRayTransport.h" + +#include "CADSupport/O2SolidHarness.h" +#include "CADSupport/O2BVHSurfaceSolid.h" +#include "DetectorsBase/O2Tessellated.h" +#include "CADSupport/O2FlatCSG.h" +#include "CADSupport/O2SurfaceSolidIO.h" + +#include "TGeoBBox.h" +#include "TGeoManager.h" +#include "TGeoMaterial.h" +#include "TGeoMatrix.h" +#include "TGeoMedium.h" +#include "TGeoNavigator.h" +#include "TGeoNode.h" +#include "TGeoSphere.h" +#include "TGeoTube.h" +#include "TGeoVolume.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using json = nlohmann::json; +using namespace o2::cad; +using namespace o2::cad::harness; +using namespace o2::cad::xray; +using namespace o2::cad::bench; +using o2::base::O2Tessellated; + +namespace +{ + +// The xrays_/crossings_ JSON contract shared with Detectors/CADSupport/validation/xrayOracle.py. Bump on both +// sides together; the oracle refuses a version it does not speak rather than guessing. +constexpr int kXRayFormatVersion = 2; + +json comparisonToJson(const ListComparison& c) +{ + return json{{"rays", c.rays}, + {"raysIdentical", c.raysIdentical}, + {"raysStructural", c.raysStructural}, + {"matched", c.matched}, + {"displacedCrossings", c.displaced}, + {"missingCrossings", c.missing}, + {"extraCrossings", c.extra}, + {"kindMismatch", c.kindMismatch}, + {"worstDeltaT", c.worstDeltaT}, + {"worstOrigin", {c.worstOrigin[0], c.worstOrigin[1], c.worstOrigin[2]}}, + {"worstDir", {c.worstDir[0], c.worstDir[1], c.worstDir[2]}}, + {"worstReason", c.worstReason}}; +} + +json robustnessToJson(const Robustness& r) +{ + return json{{"rays", r.rays}, + {"raysWithCrossings", r.raysWithCrossings}, + {"crossings", r.crossings}, + {"steps", r.steps}, + {"zeroLengthSteps", r.zeroLengthSteps}, + {"nonAdvancingSteps", r.nonAdvancingSteps}, + {"unstickPushes", r.unstickPushes}, + {"iterationCapHits", r.iterationCapHits}, + {"unterminated", r.unterminated}, + {"oddCrossingLists", r.oddCrossingLists}, + {"nonAlternating", r.nonAlternating}, + {"duplicateCrossings", r.duplicateCrossings}, + {"parityMismatchIntervals", r.parityMismatchIntervals}, + {"parityMismatchNearBoundary", r.parityMismatchNearBoundary}, + {"originInside", r.originInside}, + {"boundaryWithoutTransition", r.boundaryWithoutTransition}, + {"originOutsideWorld", r.originOutsideWorld}, + {"insideLengthCm", r.insideLength}, + {"seconds", r.seconds}}; +} + +// ------------------------------------------------------------------------------------------ +// Mode (b): the real TGeoNavigator +// ------------------------------------------------------------------------------------------ + +/// One part in a minimal world, transported with FindNextBoundaryAndStep(). +/// +/// The crossing distance is taken as (projection of the point *before* the step onto the ray) + +/// GetStep(), rather than by accumulating GetStep(): the navigator moves the point a little past +/// each boundary, and reprojecting absorbs that push instead of letting it accumulate. +class NavigatorTransport +{ + public: + /// Builds the world INSIDE the caller's manager, deliberately. + /// + /// Constructing a second TGeoManager here is what the first version did, and it segfaulted: + /// `TGeoManager`'s constructor DELETES the existing `gGeoManager`, and that manager owns the + /// shape being handed in (TGeoShape registers itself in `gGeoManager`'s shape list on + /// construction). The world therefore has to be built in the manager the shape already belongs + /// to, and everything created here is freed with it. + /// `placement` is the shape's own frame expressed in the part frame, or null when they are the + /// same. Mode (b) carries it on the NODE rather than transforming the rays, which is the whole + /// point of having a navigator: the rays stay in the part frame, ROOT performs the transform it + /// would perform in production, and mode (a) -- which transforms the rays by hand -- becomes an + /// independent check of it rather than a restatement. + NavigatorTransport(TGeoManager* manager, TGeoShape* shape, const Point3D& bboxMin, + const Point3D& bboxMax, const TGeoMatrix* placement = nullptr) + { + mManager = manager; + auto* material = new TGeoMaterial("Vacuum", 0., 0., 0.); + auto* medium = new TGeoMedium("Vacuum", 1, material); + double half[3]; + double centre[3]; + for (int k = 0; k < 3; ++k) { + centre[k] = 0.5 * (bboxMax[k] + bboxMin[k]); + half[k] = 0.5 * (bboxMax[k] - bboxMin[k]) + 0.05 * (bboxMax[k] - bboxMin[k]) + 0.1; + } + auto* worldBox = new TGeoBBox("xrayWorld", half[0], half[1], half[2], centre); + mWorld = new TGeoVolume("TOP", worldBox, medium); + mPart = new TGeoVolume("PART", shape, medium); + // Identity unless the shape carries a placement, in which case this is where it is applied. + mWorld->AddNode(mPart, 1, placement != nullptr ? new TGeoHMatrix(*placement) : nullptr); + mManager->SetTopVolume(mWorld); + mManager->CloseGeometry(); + mManager->SetNsegments(80); + mNavigator = mManager->GetCurrentNavigator(); + } + + /// Owns nothing: the manager handed in outlives this object and frees the world with itself. + ~NavigatorTransport() = default; + + NavigatorTransport(const NavigatorTransport&) = delete; + NavigatorTransport& operator=(const NavigatorTransport&) = delete; + + bool valid() const { return mNavigator != nullptr; } + + std::vector transport(const Point3D& origin, const Point3D& dir, double tMax, + const StepConfig& cfg, Robustness& stats) + { + std::vector crossings; + mNavigator->InitTrack(origin.data(), dir.data()); + if (mNavigator->IsOutside()) { + // The world is built to contain every ray of the raster, so this cannot fire on a correct + // configuration -- and it gets its own counter precisely so that a wrong one is never + // mistaken for a geometry defect. + ++stats.originOutsideWorld; + return crossings; + } + bool inPart = (mNavigator->GetCurrentVolume() == mPart); + if (inPart) { + ++stats.originInside; + } + int iter = 0; + for (; iter < cfg.maxIter; ++iter) { + const double* before = mNavigator->GetCurrentPoint(); + double tBefore = 0.; + for (int k = 0; k < 3; ++k) { + tBefore += (before[k] - origin[k]) * dir[k]; + } + mNavigator->FindNextBoundaryAndStep(TGeoShape::Big(), kFALSE); + const double step = mNavigator->GetStep(); + ++stats.steps; + const double tCross = tBefore + step; + if (step <= cfg.zeroStep) { + ++stats.zeroLengthSteps; + } + if (!(tCross > tBefore)) { + ++stats.nonAdvancingSteps; + } + if (mNavigator->IsOutside() || tCross > tMax || !(step < TGeoShape::Big())) { + break; + } + const bool nowIn = (mNavigator->GetCurrentVolume() == mPart); + if (nowIn != inPart) { + crossings.push_back({tCross, nowIn ? +1 : -1}); + inPart = nowIn; + } else { + ++stats.boundaryWithoutTransition; + } + } + if (iter >= cfg.maxIter) { + ++stats.iterationCapHits; + } + if (inPart) { + ++stats.unterminated; + } + return crossings; + } + + private: + TGeoManager* mManager = nullptr; + TGeoVolume* mWorld = nullptr; + TGeoVolume* mPart = nullptr; + TGeoNavigator* mNavigator = nullptr; +}; + +// ------------------------------------------------------------------------------------------ +// Reading a crossing list, and comparing two of them +// ------------------------------------------------------------------------------------------ + +// ------------------------------------------------------------------------------------------ +// The raster +// ------------------------------------------------------------------------------------------ +// +// A structured parallel-beam raster, not Monte Carlo. Cell centres of an N x N lattice over the +// raster window, one beam per axis. Structured wins for two independent reasons: the chord +// integral converges far better than random sampling (boundary cells are the whole error budget +// and their count grows as N rather than N^2), and a lattice deliberately produces the grazing, +// edge-on and vertex-on rays a random direction essentially never generates -- which is where a +// transport loop stalls. + +// ------------------------------------------------------------------------------------------ +// Options, part collection, IO +// ------------------------------------------------------------------------------------------ + +struct Options { + std::string db; + std::string explicitSurfaces; + std::string explicitFacets; + std::string explicitShape; + std::string explicitFlatCSG; + /// `O2FlatCSG::SetSplitDepth` / `SetMinBoxFraction` for every flat subject, or < 0 / < 0 to + /// leave the class defaults alone. These exist so the split knobs can be swept from outside + /// the class, which is how their defaults were chosen (Design_FlatCSGSolid.md section 9). + int flatSplitDepth = -1; + double flatMinBoxFraction = -1.; + std::string partsPattern; + int raster = 48; + std::string axesSpec = "xyz"; + /// Transverse padding of the raster window over the part's bounding box, cm. Kept absolute and + /// small: it is a first-order systematic on the chord volume (see buildRaster). It exists only + /// to cover the fact that a tessellated bounding box is INSCRIBED -- measured at 1e-4 to 1e-3 cm + /// on these corpora -- so a zero margin would clip the true solid's silhouette. + double margin = 1.e-3; + /// Rotate every beam off its coordinate axis by this many degrees. At 0 the beams are exactly + /// axis-aligned, which keeps a box's chord integral exact but samples a very special family of + /// ray/surface configurations; a non-zero tilt makes them generic. + double tiltDegrees = 0.; + /// When > 0, replace the axis beams by this many Fibonacci-spiral directions. A parallel-beam + /// raster is direction-poor and a direction-dependent defect is invisible to it; see + /// buildFanBeams. + int fanBeams = 0; + std::string dumpRays; + std::string refCrossings; + std::string jsonOut; + /// `flatcsg` is deliberately NOT here: a flat part's `shape_*.root` already holds the same + /// `O2FlatCSG`, so a database run would score one solid twice. Naming `--flatcsg ` + /// adds it, and `--representations` can ask for it by name. + std::set representations = {"surface", "mesh", "shape"}; + bool skipNavigator = false; + bool selfTest = false; + /// The representation cost/memory comparison: per-call ns for + /// the four navigation kernels plus transport, and two memory numbers, per representation, from + /// ONE shared sample set per part. + bool perf = false; + int perfPoints = 4096; + int perfRays = 4096; + int perfPasses = 9; + int perfWarmup = 2; + /// Comma-separated leaf counts for the synthetic boolean ladder. Needs no database and no model. + std::string ladderSpec; + StepConfig step; +}; + +struct Part { + std::string id; + std::string model; + std::string surfaces; + std::string facets; + std::string shape; + std::string flatcsg; +}; + +/// The file one named representation of \a part reads. One place, because four call sites +/// used to spell the same three-way conditional and a fourth representation would have made +/// each of them a place to forget it. +const std::string& sourceFor(const Part& part, const std::string& name) +{ + if (name == "surface") { + return part.surfaces; + } + if (name == "mesh") { + return part.facets; + } + if (name == "flatcsg") { + return part.flatcsg; + } + return part.shape; +} + +/// Every representation name, in the order the tables print them. +const std::array& allRepresentations() +{ + static const std::array names{"surface", "mesh", "shape", "flatcsg"}; + return names; +} + +std::string deriveSidecarPath(const std::string& surfacesPath, const char* prefixOut, + const char* suffixOut) +{ + const auto slash = surfacesPath.find_last_of('/'); + const std::string dir = slash == std::string::npos ? std::string() : surfacesPath.substr(0, slash + 1); + std::string base = slash == std::string::npos ? surfacesPath : surfacesPath.substr(slash + 1); + const std::string prefix = "surfaces_"; + const std::string suffix = ".bin"; + if (base.rfind(prefix, 0) != 0 || base.size() <= prefix.size() + suffix.size() || + base.compare(base.size() - suffix.size(), suffix.size(), suffix) != 0) { + return {}; + } + const std::string stem = base.substr(prefix.size(), base.size() - prefix.size() - suffix.size()); + return dir + prefixOut + stem + suffixOut; +} + +bool fileExists(const std::string& path) +{ + if (path.empty()) { + return false; + } + std::ifstream probe(path); + return static_cast(probe); +} + +/// Must match sanitizePartId() in runSolidHarness.cxx and sanitize_part_id() in the gate scripts. +std::string sanitizePartId(const std::string& id) +{ + std::string out; + out.reserve(id.size()); + for (const char c : id) { + out.push_back((std::isalnum(static_cast(c)) || c == '-' || c == '.') ? c : '_'); + } + return out; +} + +void printUsage(const char* argv0) +{ + std::cout << "X-ray / geantino transport benchmark -- ordered crossing lists, by stepping.\n\n" + "Usage: " + << argv0 << " --db [--parts ] [--raster N] [--axes xyz]\n" + " [--dump-rays D] [--ref-crossings D] [--json out.json]\n" + " or: " + << argv0 << " --surfaces [--facets ] [--shape ] [--flatcsg ]\n" + " [options as above]\n" + " or: " + << argv0 << " --self-test\n\n" + " --raster N N x N parallel rays per beam axis (default 48). Structured, not random:\n" + " the chord integral converges as the boundary-cell count (~N) rather than\n" + " as sqrt of the sample count, and a lattice generates the edge-on and\n" + " vertex-on rays that stall a transport loop.\n" + " --axes xyz which beam axes to fire (subset of x,y,z; default all three)\n" + " --beams N fire N Fibonacci-spiral directions instead of the axis beams. A parallel\n" + " beam is DIRECTION-POOR: three axes are three directions however many rays\n" + " are fired, and a direction-dependent defect (the torus quartic) is\n" + " invisible to them. Use this whenever hunting one.\n" + " --tilt DEG rotate every beam off its axis by DEG (default 0). An axis-aligned beam\n" + " is a special family of configurations; a tilted one is generic. The known\n" + " torus quartic defect is invisible at tilt 0 and visible at tilt 12.\n" + " --dump-rays D write D/xrays_.json (the raster window and every ray) and exit\n" + " --ref-crossings D read D/crossings_.json (Detectors/CADSupport/validation/xrayOracle.py) and score\n" + " the crossing LISTS against it, per representation, per mode\n" + " --flatcsg an o2::cad::O2FlatCSG sidecar (flatcsg_*.bin) as its own subject.\n" + " NOT in the default set -- a flat part's shape_*.root already holds\n" + " the same solid -- so name it here, or in --representations.\n" + " This is how the flat halfspace solid is scored against the SAME part\n" + " emitted as a plain TGeoCompositeShape through --shape: two subjects,\n" + " one raster, one sample set (Design_FlatCSGSolid.md section 9).\n" + " --flat-split-depth N override O2FlatCSG::SetSplitDepth on every flat subject\n" + " --flat-min-box-fraction X override O2FlatCSG::SetMinBoxFraction likewise. The two\n" + " knobs are swept from here rather than from a test, so the defaults in\n" + " the header rest on the same instrument that reports the query cost.\n" + " --representations surface,mesh,shape,flatcsg which to run (default: all present)\n" + " --no-navigator skip mode (b); mode (a) depends on nothing but the shape\n" + " --perf the representation cost/memory comparison: per-call ns for Contains,\n" + " Safety, DistFromOutside and DistFromInside, plus transport ns/ray and\n" + " ns/crossing, plus structural and measured memory -- for every\n" + " representation, from ONE shared sample set per part. Warm cache; the\n" + " reported number is the median over --perf-passes complete passes and the\n" + " min/max spread is printed with it.\n" + " --perf-points N query points per part (default 4096)\n" + " --perf-rays N rays per distance kernel (default 4096)\n" + " --perf-passes N timed passes (default 9); --perf-warmup N untimed first (default 2)\n" + " --ladder 2,4,8 the synthetic boolean ladder: unions of K TGeoTubes as a left-deep CHAIN\n" + " and as a BALANCED tree, timed with the same kernels. Needs no database:\n" + " every genuine boolean in the corpus is a 2-leaf union, so the corpus\n" + " cannot answer how a composite scales with leaf count and this fixture is\n" + " what does.\n" + " --push X distance advanced past a found crossing (cm, default 1e-9 = kRayTolerance)\n" + " --unstick-push X the nudge a stalled step is repaired with (cm, default 1e-6); every use\n" + " is counted in `unstickPushes`\n" + " --max-iter N transport iteration cap per ray (default 512)\n" + " --self-test analytic self-checks (box, tube, sphere) plus the synthetic controls that\n" + " prove the comparison can fail. Needs no database and no oracle.\n\n" + "Three-stage round trip:\n" + " " + << argv0 << " --db --dump-rays /tmp/x\n" + " xrayOracle.py --brep .brep --rays /tmp/x/xrays_.json \\\n" + " --out /tmp/x/crossings_.json\n" + " " + << argv0 << " --db --ref-crossings /tmp/x --json /tmp/x/xray.json\n"; +} + +std::set splitCsv(const std::string& s) +{ + std::set out; + std::stringstream ss(s); + std::string tok; + while (std::getline(ss, tok, ',')) { + if (!tok.empty()) { + out.insert(tok); + } + } + return out; +} + +bool parseArgs(int argc, char** argv, Options& opt) +{ + for (int i = 1; i < argc; ++i) { + const std::string a = argv[i]; + auto next = [&](const char* flag) -> std::string { + if (i + 1 >= argc) { + throw std::runtime_error(std::string("missing value for ") + flag); + } + return argv[++i]; + }; + if (a == "--db") { + opt.db = next("--db"); + } else if (a == "--surfaces") { + opt.explicitSurfaces = next("--surfaces"); + } else if (a == "--facets") { + opt.explicitFacets = next("--facets"); + } else if (a == "--shape") { + opt.explicitShape = next("--shape"); + } else if (a == "--flatcsg") { + opt.explicitFlatCSG = next("--flatcsg"); + } else if (a == "--flat-split-depth") { + opt.flatSplitDepth = std::stoi(next("--flat-split-depth")); + } else if (a == "--flat-min-box-fraction") { + opt.flatMinBoxFraction = std::stod(next("--flat-min-box-fraction")); + } else if (a == "--parts") { + opt.partsPattern = next("--parts"); + } else if (a == "--raster") { + opt.raster = std::stoi(next("--raster")); + } else if (a == "--axes") { + opt.axesSpec = next("--axes"); + } else if (a == "--beams") { + opt.fanBeams = std::stoi(next("--beams")); + } else if (a == "--tilt") { + opt.tiltDegrees = std::stod(next("--tilt")); + } else if (a == "--margin") { + opt.margin = std::stod(next("--margin")); + } else if (a == "--dump-rays") { + opt.dumpRays = next("--dump-rays"); + } else if (a == "--ref-crossings") { + opt.refCrossings = next("--ref-crossings"); + } else if (a == "--json") { + opt.jsonOut = next("--json"); + } else if (a == "--representations") { + opt.representations = splitCsv(next("--representations")); + } else if (a == "--no-navigator") { + opt.skipNavigator = true; + } else if (a == "--perf") { + opt.perf = true; + } else if (a == "--perf-points") { + opt.perfPoints = std::stoi(next("--perf-points")); + } else if (a == "--perf-rays") { + opt.perfRays = std::stoi(next("--perf-rays")); + } else if (a == "--perf-passes") { + opt.perfPasses = std::stoi(next("--perf-passes")); + } else if (a == "--perf-warmup") { + opt.perfWarmup = std::stoi(next("--perf-warmup")); + } else if (a == "--ladder") { + opt.ladderSpec = next("--ladder"); + } else if (a == "--push") { + opt.step.push = std::stod(next("--push")); + } else if (a == "--unstick-push") { + opt.step.unstickPush = std::stod(next("--unstick-push")); + } else if (a == "--zero-step") { + opt.step.zeroStep = std::stod(next("--zero-step")); + } else if (a == "--max-iter") { + opt.step.maxIter = std::stoi(next("--max-iter")); + } else if (a == "--self-test") { + opt.selfTest = true; + } else if (a == "-h" || a == "--help") { + printUsage(argv[0]); + return false; + } else { + throw std::runtime_error("unrecognized option: " + a); + } + } + if (!opt.selfTest && opt.ladderSpec.empty() && opt.db.empty() && opt.explicitSurfaces.empty() && + opt.explicitShape.empty() && opt.explicitFlatCSG.empty()) { + throw std::runtime_error( + "either --db , --surfaces/--shape/--flatcsg , " + "--ladder or --self-test is required"); + } + // Naming a sidecar means "score this", whatever the default set says. + if (!opt.explicitFlatCSG.empty()) { + opt.representations.insert("flatcsg"); + } + return true; +} + +std::vector collectParts(const Options& opt) +{ + std::vector parts; + if (!opt.explicitSurfaces.empty() || !opt.explicitShape.empty() || + !opt.explicitFlatCSG.empty()) { + Part part{"adhoc", "adhoc", opt.explicitSurfaces, opt.explicitFacets, opt.explicitShape, + opt.explicitFlatCSG}; + // The siblings are only DERIVED from a `surfaces_*.bin` stem. Naming a shape or a sidecar + // directly means "score exactly this", which is how one part is emitted two ways and the two + // scored against each other; guessing a third subject from that name would be inventing one. + if (!part.surfaces.empty()) { + if (part.facets.empty()) { + part.facets = deriveSidecarPath(part.surfaces, "facets_", ".bin"); + } + if (part.shape.empty()) { + part.shape = deriveSidecarPath(part.surfaces, "shape_", ".root"); + } + if (part.flatcsg.empty()) { + part.flatcsg = deriveSidecarPath(part.surfaces, "flatcsg_", ".bin"); + } + } + parts.push_back(std::move(part)); + return parts; + } + const std::string manifestPath = opt.db + "/manifest.json"; + std::ifstream in(manifestPath); + if (!in) { + throw std::runtime_error("cannot open " + manifestPath); + } + json manifest; + in >> manifest; + for (const auto& p : manifest.at("parts")) { + Part part; + part.id = p.at("id").get(); + part.model = p.value("model", std::string("?")); + part.surfaces = p.value("surfaces", std::string()); + part.facets = p.value("facets", std::string()); + part.shape = p.value("shape", std::string()); + if (part.shape.empty()) { + part.shape = deriveSidecarPath(part.surfaces, "shape_", ".root"); + } + part.flatcsg = p.value("flatcsg", std::string()); + if (part.flatcsg.empty()) { + part.flatcsg = deriveSidecarPath(part.surfaces, "flatcsg_", ".bin"); + } + if (!opt.partsPattern.empty()) { + const bool idMatch = part.id.find(opt.partsPattern) != std::string::npos; + const bool modelMatch = part.model.find(opt.partsPattern) != std::string::npos; + if (!idMatch && !modelMatch) { + continue; + } + } + parts.push_back(std::move(part)); + } + return parts; +} + +void writeRays(const std::string& dir, const std::string& partId, const Raster& raster, + const std::string& bboxSource) +{ + json doc; + doc["version"] = kXRayFormatVersion; + doc["part"] = partId; + doc["windowMin"] = {raster.windowMin[0], raster.windowMin[1], raster.windowMin[2]}; + doc["windowMax"] = {raster.windowMax[0], raster.windowMax[1], raster.windowMax[2]}; + doc["raster"] = raster.n; + json beams = json::array(); + for (const auto& beam : raster.beams) { + beams.push_back({{"label", beam.label}, {"dir", {beam.dir[0], beam.dir[1], beam.dir[2]}}}); + } + doc["beams"] = beams; + doc["cellArea"] = raster.cellArea; + doc["transverseMargin"] = raster.transverseMargin; + doc["windowExcess"] = raster.windowExcess; + doc["bboxSource"] = bboxSource; + json rays = json::array(); + for (const auto& r : raster.rays) { + rays.push_back({{"o", {r.origin[0], r.origin[1], r.origin[2]}}, + {"d", {r.dir[0], r.dir[1], r.dir[2]}}, + {"tmax", r.tMax}, + {"beam", r.beam}}); + } + doc["rays"] = std::move(rays); + const std::string path = dir + "/xrays_" + sanitizePartId(partId) + ".json"; + std::ofstream out(path); + if (!out) { + throw std::runtime_error("cannot write " + path); + } + out << doc.dump(); + std::printf(" wrote %s (%zu rays)\n", path.c_str(), raster.rays.size()); +} + +/// The oracle's answer for one part: the ordered crossing list per ray, plus its own chord volume. +struct OracleCrossings { + bool has = false; + double tolerance = 1.e-7; + double capacity = 0.; + double volumeChord = 0.; + bool valid = false; + std::vector> perRay; + std::vector ambiguous; + long long ambiguousRays = 0; + Raster raster; +}; + +OracleCrossings loadOracleCrossings(const std::string& dir, const std::string& partId) +{ + OracleCrossings out; + const std::string path = dir + "/crossings_" + sanitizePartId(partId) + ".json"; + std::ifstream in(path); + if (!in) { + return out; + } + json doc; + in >> doc; + if (doc.value("version", 0) != kXRayFormatVersion) { + throw std::runtime_error(path + ": unsupported format version"); + } + out.has = true; + out.tolerance = doc.value("tolerance", 1.e-7); + out.capacity = doc.value("capacity", 0.); + out.volumeChord = doc.value("volumeChord", 0.); + out.valid = doc.value("valid", false); + out.ambiguousRays = doc.value("ambiguousRays", 0); + out.raster.n = doc.value("raster", 0); + out.raster.transverseMargin = doc.value("transverseMargin", 0.); + const auto& window0 = doc.at("windowMin"); + const auto& window1 = doc.at("windowMax"); + for (int k = 0; k < 3; ++k) { + out.raster.windowMin[k] = window0[k].get(); + out.raster.windowMax[k] = window1[k].get(); + } + out.raster.cellArea = doc.at("cellArea").get>(); + out.raster.windowExcess = doc.value("windowExcess", std::vector(out.raster.cellArea.size(), 0.)); + for (const auto& b : doc.at("beams")) { + Beam beam; + beam.label = b.at("label").get(); + for (int k = 0; k < 3; ++k) { + beam.dir[k] = b.at("dir")[k].get(); + } + out.raster.beams.push_back(std::move(beam)); + } + out.raster.rays.reserve(doc.at("rays").size()); + for (const auto& r : doc.at("rays")) { + RayDef ray; + for (int k = 0; k < 3; ++k) { + ray.origin[k] = r.at("o")[k].get(); + ray.dir[k] = r.at("d")[k].get(); + } + ray.tMax = r.at("tmax").get(); + ray.beam = r.at("beam").get(); + out.raster.rays.push_back(ray); + std::vector crossings; + const auto& ts = r.at("t"); + const auto& kinds = r.at("k"); + for (size_t i = 0; i < ts.size(); ++i) { + crossings.push_back({ts[i].get(), kinds[i].get()}); + } + out.perRay.push_back(std::move(crossings)); + // A ray OCCT itself declined to classify somewhere along its length. Excluded from the + // comparison rather than scored either way -- the same treatment `nNoVerdict` gets in the + // sample gate, for the same reason: there is no ground truth to compare against there. + out.ambiguous.push_back(r.value("amb", false)); + } + return out; +} + +/// A ray of the part frame, expressed in a placed shape's own frame. +/// +/// Mode (a) speaks to the shape API directly, so it is the caller's job to put the query in the +/// shape's frame. A rigid transform preserves lengths, so every `t` in the resulting crossing list +/// is the same number it would have been in the part frame -- which is why the lists produced this +/// way are compared against the oracle's, and against mode (b)'s, without any further correction. +void toShapeFrame(const TGeoMatrix* placement, const Point3D& origin, const Point3D& dir, + Point3D& localOrigin, Point3D& localDir) +{ + if (placement == nullptr) { + localOrigin = origin; + localDir = dir; + return; + } + placement->MasterToLocal(origin.data(), localOrigin.data()); + placement->MasterToLocalVect(dir.data(), localDir.data()); +} + +/// A shape's bounding box carried into the part frame: the axis-aligned hull of the eight +/// transformed corners. Conservative for a rotated body, which is exactly what a raster window and +/// a navigator world both need. +void placedBox(const TGeoBBox& box, const TGeoMatrix* placement, Point3D& lo, Point3D& hi) +{ + const double half[3] = {box.GetDX(), box.GetDY(), box.GetDZ()}; + for (int k = 0; k < 3; ++k) { + lo[k] = box.GetOrigin()[k] - half[k]; + hi[k] = box.GetOrigin()[k] + half[k]; + } + if (placement == nullptr) { + return; + } + Point3D outLo{1.e300, 1.e300, 1.e300}; + Point3D outHi{-1.e300, -1.e300, -1.e300}; + for (int corner = 0; corner < 8; ++corner) { + const double local[3] = {(corner & 1) ? hi[0] : lo[0], (corner & 2) ? hi[1] : lo[1], + (corner & 4) ? hi[2] : lo[2]}; + double master[3]; + placement->LocalToMaster(local, master); + for (int k = 0; k < 3; ++k) { + outLo[k] = std::min(outLo[k], master[k]); + outHi[k] = std::max(outHi[k], master[k]); + } + } + lo = outLo; + hi = outHi; +} + +/// The tightest CONTAINING bounding box available for a part, and where it came from. +/// +/// The order is a measurement: the surface solid's box is conservative, while the shape's and the +/// mesh's are tight. So: shape, else mesh, else surface, and say which. +bool resolveBoundingBox(const Part& part, const Options& opt, Point3D& lo, Point3D& hi, + std::string& source) +{ + struct Candidate { + const char* name; + const std::string& path; + }; + const Candidate candidates[4] = {{"shape", part.shape}, + {"flatcsg", part.flatcsg}, + {"mesh", part.facets}, + {"surface", part.surfaces}}; + for (const auto& candidate : candidates) { + if (!opt.representations.count(candidate.name) || !fileExists(candidate.path)) { + continue; + } + auto* manager = new TGeoManager("xrayBBox", "bbox probe"); + TGeoShape* shape = nullptr; + std::unique_ptr placement; + if (std::string(candidate.name) == "surface") { + auto* solid = new O2BVHSurfaceSolid(part.id.c_str()); + if (LoadSurfaceSolid(candidate.path, *solid)) { + solid->CloseShape(true); + shape = solid; + } + } else if (std::string(candidate.name) == "mesh") { + auto* solid = new O2Tessellated(part.id.c_str()); + if (LoadFacetSolid(candidate.path, *solid)) { + solid->CloseShape(); + shape = solid; + } + } else if (std::string(candidate.name) == "flatcsg") { + auto* solid = new O2FlatCSG(part.id.c_str()); + if (LoadFlatCSG(candidate.path, *solid)) { + solid->CloseShape(); + shape = solid; + } + } else { + shape = loadShapeFromRootFile(candidate.path, nullptr); + // The window must be stated in the PART frame, so a placed shape's box is carried through + // its placement first. Skipping this would raster a rotated tube against the box of the tube + // at the origin -- a window that misses the part entirely. + placement.reset(loadShapePlacementFromRootFile(candidate.path)); + } + const auto* box = dynamic_cast(shape); + if (box != nullptr) { + placedBox(*box, placement.get(), lo, hi); + source = candidate.name; + delete manager; + gGeoManager = nullptr; + return true; + } + delete manager; + gGeoManager = nullptr; + } + return false; +} + +// ------------------------------------------------------------------------------------------ +// --perf: per-call cost and memory, per representation, from one shared sample set +// ------------------------------------------------------------------------------------------ +// +// Everything here answers one question -- "what does asking this representation a navigation +// question cost, and what does holding it cost" -- and it answers it under three constraints that +// are the whole difference between a benchmark and a stopwatch: +// +// * SAME QUESTIONS. The point and ray sets are built once per part, from a designated reference +// representation's own Contains(), and handed unchanged to all three. `partitionedBy` is +// reported so nobody has to guess which one. +// * WARM CACHE, and said so. Every kernel is warmed before it is timed and every part fits in +// cache, so these are steady-state numbers for a single resident solid. A real simulation +// holds thousands of solids and misses; the ratios here are an upper bound on how well the +// cheaper representation does there, not a prediction of it. +// * LOAD EXCLUDED FROM THE KERNEL, AND REPORTED SEPARATELY, because loading dominates the run +// on a large model. + +json timingToJson(const TimingStat& t) +{ + json out{{"callsPerPass", t.callsPerPass}, + {"passes", t.passes}, + {"nsPerCallMedian", t.medianNsPerCall}, + {"nsPerCallMin", t.minNsPerCall}, + {"nsPerCallMax", t.maxNsPerCall}, + {"spread", t.spread}, + {"checksum", t.checksum}}; + if (t.hitFraction >= 0.) { + out["hitFraction"] = t.hitFraction; + } + return out; +} + +/// A representation, loaded, with everything the cost table needs to say about it. +struct LoadedRep { + TGeoManager* manager = nullptr; + TGeoShape* shape = nullptr; + const O2BVHSurfaceSolid* surfaceSolid = nullptr; + const O2FlatCSG* flatSolid = nullptr; + std::unique_ptr placement; + StructuralMemory structural; + MemorySnapshot loadDelta; ///< across the file read + MemorySnapshot closeDelta; ///< across CloseShape(), i.e. the acceleration structure + double loadSeconds = 0.; + double closeSeconds = 0.; + bool meshClosedBody = true; + bool ok = false; +}; + +/// Load one representation into its own TGeoManager, measuring what it cost to do so. +/// +/// The split between `loadDelta` and `closeDelta` is deliberate and it is where the surface +/// solid's memory actually is: `LoadSurfaceSolid` reads the sidecar, `CloseShape` builds the BVH, +/// and lumping the two together would attribute an acceleration structure to a file format. +LoadedRep loadRepresentation(const std::string& name, const std::string& source, + const std::string& partId, int flatSplitDepth = -1, + double flatMinBoxFraction = -1.) +{ + LoadedRep rep; + rep.manager = new TGeoManager(("perf_" + name).c_str(), "representation benchmark"); + rep.structural.sidecarBytes = fileBytes(source); + const MemorySnapshot before = readMemory(); + const auto t0 = std::chrono::steady_clock::now(); + if (name == "surface") { + auto* solid = new O2BVHSurfaceSolid(partId.c_str()); + if (!LoadSurfaceSolid(source, *solid)) { + return rep; + } + const auto t1 = std::chrono::steady_clock::now(); + rep.loadSeconds = std::chrono::duration(t1 - t0).count(); + rep.loadDelta = readMemory() - before; + const MemorySnapshot beforeClose = readMemory(); + solid->CloseShape(true); + rep.closeSeconds = std::chrono::duration(std::chrono::steady_clock::now() - t1).count(); + rep.closeDelta = readMemory() - beforeClose; + rep.shape = solid; + rep.surfaceSolid = solid; + rep.structural.primitives = solid->GetNsurfaces(); + // The patch count and the sidecar are the two EXACT numbers a surface solid has. The trim + // wires are variable-length per patch and live behind a private type, so the in-memory + // arithmetic is not available from outside; the sidecar bytes bound it from below and the + // measured heap delta bounds it from above, and both are printed rather than one guessed + // number in between. + rep.structural.bytes = rep.structural.sidecarBytes; + rep.structural.formula = "patches=" + std::to_string(rep.structural.primitives) + + "; bytes = sidecar on disk (in-memory trim arrays are not " + "introspectable; see measured heap delta)"; + } else if (name == "mesh") { + auto* solid = new O2Tessellated(partId.c_str()); + if (!LoadFacetSolid(source, *solid)) { + return rep; + } + const auto t1 = std::chrono::steady_clock::now(); + rep.loadSeconds = std::chrono::duration(t1 - t0).count(); + rep.loadDelta = readMemory() - before; + const MemorySnapshot beforeClose = readMemory(); + solid->CloseShape(); + rep.closeSeconds = std::chrono::duration(std::chrono::steady_clock::now() - t1).count(); + rep.closeDelta = readMemory() - beforeClose; + rep.shape = solid; + rep.meshClosedBody = solid->IsClosedBody(); + rep.structural.primitives = solid->GetNfacets(); + // Exact, and the one representation whose in-memory size IS arithmetic: three index arrays + // per facet plus a deduplicated vertex array plus one outward normal per facet. + const long long nF = solid->GetNfacets(); + const long long nV = solid->GetNvertices(); + rep.structural.bytes = nV * static_cast(sizeof(O2Tessellated::Vertex_t)) + + nF * static_cast(sizeof(TGeoFacet)) + + nF * static_cast(sizeof(O2Tessellated::Vertex_t)); + rep.structural.formula = + std::to_string(nV) + " vertices x " + std::to_string(sizeof(O2Tessellated::Vertex_t)) + + " B + " + std::to_string(nF) + " facets x " + std::to_string(sizeof(TGeoFacet)) + + " B + " + std::to_string(nF) + " normals x " + std::to_string(sizeof(O2Tessellated::Vertex_t)) + " B"; + } else if (name == "flatcsg") { + auto* solid = new O2FlatCSG(partId.c_str()); + if (!LoadFlatCSG(source, *solid)) { + return rep; + } + if (flatSplitDepth >= 0) { + solid->SetSplitDepth(flatSplitDepth); + } + if (flatMinBoxFraction >= 0.) { + solid->SetMinBoxFraction(flatMinBoxFraction); + } + const auto t1 = std::chrono::steady_clock::now(); + rep.loadSeconds = std::chrono::duration(t1 - t0).count(); + rep.loadDelta = readMemory() - before; + const MemorySnapshot beforeClose = readMemory(); + solid->CloseShape(); + rep.closeSeconds = std::chrono::duration(std::chrono::steady_clock::now() - t1).count(); + rep.closeDelta = readMemory() - beforeClose; + if (!solid->IsClosed()) { + // A refused CloseShape leaves a shape that answers through its `_Loop` twins -- correct, and + // orders of magnitude slower. Timing it as if it were the accelerated path would be a + // measurement of the wrong thing, so the representation is dropped instead. + return rep; + } + rep.shape = solid; + rep.flatSolid = solid; + rep.structural.primitives = solid->GetNcells(); + // Exact for everything this class owns: the halfspace blocks, the cell table, the sub-cell + // boxes with their concatenated active lists, and the BVH the class reports for itself. + const long long nH = solid->GetNhalfspaces(); + const long long nC = solid->GetNcells(); + const long long nB = solid->GetNboxes(); + long long active = 0; + for (int i = 0; i < solid->GetNboxes(); ++i) { + active += solid->GetBox(i).nActive; + } + const long long bvh = static_cast(solid->GetBVHMemory()); + rep.structural.bytes = nH * static_cast(sizeof(FlatCSGHalfspace)) + + nC * static_cast(sizeof(FlatCSGCell)) + + nC * 6 * static_cast(sizeof(double)) + + nB * static_cast(sizeof(FlatCSGBox)) + + active * static_cast(sizeof(int)) + bvh; + rep.structural.formula = + std::to_string(nH) + " halfspaces x " + std::to_string(sizeof(FlatCSGHalfspace)) + " B + " + + std::to_string(nC) + " cells x " + std::to_string(sizeof(FlatCSGCell) + 6 * sizeof(double)) + + " B + " + std::to_string(nB) + " boxes x " + std::to_string(sizeof(FlatCSGBox)) + " B + " + + std::to_string(active) + " active x " + std::to_string(sizeof(int)) + " B + BVH " + + std::to_string(bvh) + " B"; + } else { + std::string error; + rep.shape = loadShapeFromRootFile(source, &error); + if (rep.shape == nullptr) { + return rep; + } + rep.placement.reset(loadShapePlacementFromRootFile(source)); + rep.loadSeconds = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + rep.loadDelta = readMemory() - before; + const BooleanTreeStats tree = booleanTreeStats(rep.shape); + rep.structural.primitives = tree.leaves; + // A composite is a handful of objects: the node count is exact and tiny, and that is the + // headline of the whole memory column. + rep.structural.bytes = tree.leaves * 200 + tree.nodes * 200; + rep.structural.formula = "leaves=" + std::to_string(tree.leaves) + + " nodes=" + std::to_string(tree.nodes) + + " depth=" + std::to_string(tree.depth) + + "; bytes ~ (leaves+nodes) x 200 B (ROOT object overhead dominates)"; + } + rep.ok = true; + return rep; +} + +/// The same sample set, expressed in a placed shape's own frame. +/// +/// A rigid transform preserves lengths, so every distance the kernels return is the number it +/// would have been in the part frame. This is the same argument mode (a) of the transport loop +/// makes, and it is why the timing of a placed primitive is comparable with everything else. +QuerySamples toShapeFrame(const QuerySamples& in, const TGeoMatrix* placement) +{ + if (placement == nullptr) { + return in; + } + QuerySamples out = in; + for (auto& p : out.points) { + Point3D q; + placement->MasterToLocal(p.data(), q.data()); + p = q; + } + auto move = [&](std::vector& rays) { + for (auto& r : rays) { + Point3D o; + Point3D d; + placement->MasterToLocal(r.origin.data(), o.data()); + placement->MasterToLocalVect(r.dir.data(), d.data()); + r.origin = o; + r.dir = d; + } + }; + move(out.outsideRays); + move(out.insideRays); + return out; +} + +/// The one part of this that is about O2BVHSurfaceSolid rather than about representations. +/// +/// An aggregate says *that*, never *where*. If the surface solid is slower than a two-leaf +/// composite, "the BVH surface solid is slow" is not a finding -- it is a restatement. These four +/// numbers localise it: how many patches the BVH hands to the leaf callback per ray query, what +/// the same query costs with the acceleration structure's tmax pruning switched off, what it +/// costs with no BVH at all (the `_Loop` twin), and therefore what one patch intersection costs. +/// Nothing here is optimised; it is measured and reported. +json localiseSurfaceSolid(const O2BVHSurfaceSolid* solid, const QuerySamples& s, int warmup, int passes) +{ + json out; + const auto* shape = static_cast(solid); + (void)shape; + + const bool pruningWas = O2BVHSurfaceSolid::GetRayTMaxPruning(); + + O2BVHSurfaceSolid::SetRayTMaxPruning(true); + O2BVHSurfaceSolid::ResetRayCandidateCounter(); + for (const auto& ray : s.outsideRays) { + volatile double sink = solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr); + (void)sink; + } + const long long prunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount(); + const TimingStat pruned = timePasses(static_cast(s.outsideRays.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& ray : s.outsideRays) { + acc ^= static_cast( + solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr) * 1.e6); + } + return acc; + }); + + O2BVHSurfaceSolid::SetRayTMaxPruning(false); + O2BVHSurfaceSolid::ResetRayCandidateCounter(); + for (const auto& ray : s.outsideRays) { + volatile double sink = solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr); + (void)sink; + } + const long long unprunedCandidates = O2BVHSurfaceSolid::GetRayCandidateCount(); + const TimingStat unpruned = timePasses(static_cast(s.outsideRays.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& ray : s.outsideRays) { + acc ^= static_cast( + solid->DistFromOutside(ray.origin.data(), ray.dir.data(), 3, TGeoShape::Big(), nullptr) * 1.e6); + } + return acc; + }); + O2BVHSurfaceSolid::SetRayTMaxPruning(pruningWas); + + const TimingStat loop = timePasses(static_cast(s.outsideRays.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& ray : s.outsideRays) { + acc ^= static_cast( + solid->DistFromOutside_Loop(ray.origin.data(), ray.dir.data()) * 1.e6); + } + return acc; + }); + const TimingStat containsLoop = timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (const auto& p : s.points) { + acc ^= solid->Contains_Loop(p.data()) ? 1u : 0u; + } + return acc; + }); + + // --- the nearest-patch queries: Safety() and ComputeNormal() ------------------------------- + // + // Same shape of measurement; the `_Loop` twins are the unaccelerated kernels, so "before" and + // "after" run in the same binary on the same sample set. + // + // The disagreement counter travels with the timing on purpose: the twins must return bit- + // identical answers, and a speed ratio quoted without it would price two different kernels. + O2BVHSurfaceSolid::ResetSafetyCandidateCounter(); + long long safetyDisagreements = 0; + long long normalDisagreements = 0; + for (size_t index = 0; index < s.points.size(); ++index) { + const bool inside = s.pointIsInside[index] != 0; + if (solid->Safety(s.points[index].data(), inside) != solid->Safety_Loop(s.points[index].data(), inside)) { + ++safetyDisagreements; + } + Point3D viaBVH{0., 0., 0.}; + Point3D viaLoop{0., 0., 0.}; + solid->ComputeNormal(s.points[index].data(), nullptr, viaBVH.data()); + solid->ComputeNormal_Loop(s.points[index].data(), nullptr, viaLoop.data()); + if (viaBVH != viaLoop) { + ++normalDisagreements; + } + } + O2BVHSurfaceSolid::ResetSafetyCandidateCounter(); + for (size_t index = 0; index < s.points.size(); ++index) { + volatile double sink = solid->Safety(s.points[index].data(), s.pointIsInside[index] != 0); + (void)sink; + } + const long long safetyCandidates = O2BVHSurfaceSolid::GetSafetyCandidateCount(); + + const TimingStat safetyBVH = timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (size_t index = 0; index < s.points.size(); ++index) { + acc ^= static_cast(solid->Safety(s.points[index].data(), s.pointIsInside[index] != 0) * 1.e6); + } + return acc; + }); + const TimingStat safetyLoop = timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + for (size_t index = 0; index < s.points.size(); ++index) { + acc ^= static_cast(solid->Safety_Loop(s.points[index].data(), s.pointIsInside[index] != 0) * 1.e6); + } + return acc; + }); + const TimingStat normalBVH = timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + Point3D normal{0., 0., 0.}; + for (const auto& p : s.points) { + solid->ComputeNormal(p.data(), nullptr, normal.data()); + acc ^= static_cast(normal[0] * 1.e6); + } + return acc; + }); + const TimingStat normalLoop = timePasses(static_cast(s.points.size()), warmup, passes, [&]() { + uint64_t acc = 0; + Point3D normal{0., 0., 0.}; + for (const auto& p : s.points) { + solid->ComputeNormal_Loop(p.data(), nullptr, normal.data()); + acc ^= static_cast(normal[0] * 1.e6); + } + return acc; + }); + + const double rays = static_cast(std::max(1, s.outsideRays.size())); + const double points = static_cast(std::max(1, s.points.size())); + out["safetyBVHNs"] = safetyBVH.medianNsPerCall; + out["safetyLoopNs"] = safetyLoop.medianNsPerCall; + out["safetySpeedup"] = safetyBVH.medianNsPerCall > 0. ? safetyLoop.medianNsPerCall / safetyBVH.medianNsPerCall : 0.; + out["normalBVHNs"] = normalBVH.medianNsPerCall; + out["normalLoopNs"] = normalLoop.medianNsPerCall; + out["bvhCandidatesPerSafetyCall"] = safetyCandidates / points; + out["loopCandidatesPerSafetyCall"] = static_cast(solid->GetNsurfaces()); + out["safetyDisagreements"] = safetyDisagreements; + out["normalDisagreements"] = normalDisagreements; + out["nearestPatchComparedPoints"] = static_cast(s.points.size()); + std::printf( + " safety: %.1f ns BVH vs %.1f ns _Loop (%.1fx) | %.2f candidates/call of %d | " + "normal %.1f ns vs %.1f ns | disagreements %lld safety / %lld normal in %zu points\n", + safetyBVH.medianNsPerCall, safetyLoop.medianNsPerCall, out["safetySpeedup"].get(), + safetyCandidates / points, solid->GetNsurfaces(), normalBVH.medianNsPerCall, + normalLoop.medianNsPerCall, safetyDisagreements, normalDisagreements, s.points.size()); + + out["patches"] = solid->GetNsurfaces(); + out["bvhCandidatesPerDistOutCall"] = prunedCandidates / rays; + out["loopCandidatesPerDistOutCall"] = unprunedCandidates / rays; + out["distOutPrunedNs"] = pruned.medianNsPerCall; + out["distOutUnprunedNs"] = unpruned.medianNsPerCall; + out["distOutLoopNs"] = loop.medianNsPerCall; + out["containsLoopNs"] = containsLoop.medianNsPerCall; + out["nsPerBVHCandidate"] = + prunedCandidates > 0 ? pruned.medianNsPerCall * rays / static_cast(prunedCandidates) : 0.; + std::printf( + " localise: %d patches | %.1f BVH candidates/distout call (unpruned %.1f) | " + "distout %.1f ns pruned, %.1f ns unpruned, %.1f ns _Loop | %.2f ns per candidate patch | " + "Contains_Loop %.1f ns\n", + solid->GetNsurfaces(), prunedCandidates / rays, unprunedCandidates / rays, + pruned.medianNsPerCall, unpruned.medianNsPerCall, loop.medianNsPerCall, + out["nsPerBVHCandidate"].get(), containsLoop.medianNsPerCall); + return out; +} + +void printTiming(const char* label, const TimingStat& t) +{ + std::printf(" %-14s %9.1f ns/call [%9.1f .. %9.1f, spread %5.1f%%]", label, + t.medianNsPerCall, t.minNsPerCall, t.maxNsPerCall, 100. * t.spread); + if (t.hitFraction >= 0.) { + std::printf(" hit %5.1f%%", 100. * t.hitFraction); + } + std::printf("\n"); +} + +/// The synthetic boolean ladder: `--ladder 2,4,8,16,32`. +/// +/// It exists because the corpus cannot answer the question it answers. Reported for both tree +/// shapes and with the leaf count verified from the built tree rather than from the request -- +/// a fixture that claims 32 leaves and holds 16 would show sublinear scaling and be wrong. +json runLadder(const Options& opt) +{ + json out = json::array(); + std::vector counts; + { + std::stringstream ss(opt.ladderSpec); + std::string tok; + while (std::getline(ss, tok, ',')) { + if (!tok.empty()) { + counts.push_back(std::stoi(tok)); + } + } + } + std::printf("=== synthetic boolean ladder: unions of K overlapping TGeoTubes ===\n"); + std::printf( + " Every genuine boolean in the corpus is a 2-leaf union of two TGeoTubes, so the\n" + " corpus cannot say how a composite scales with K. This can.\n\n"); + for (const int k : counts) { + for (const auto shapeKind : {LadderShape::Chain, LadderShape::Balanced}) { + const char* kindName = shapeKind == LadderShape::Chain ? "chain" : "balanced"; + auto* manager = new TGeoManager("ladder", "boolean ladder"); + const std::string tag = std::string("L") + kindName + std::to_string(k); + const MemorySnapshot before = readMemory(); + TGeoShape* shape = buildBooleanLadder(k, shapeKind, tag); + const MemorySnapshot after = readMemory(); + if (shape == nullptr) { + delete manager; + gGeoManager = nullptr; + continue; + } + const BooleanTreeStats tree = booleanTreeStats(shape); + const auto* box = dynamic_cast(shape); + const Point3D lo{box->GetOrigin()[0] - box->GetDX(), box->GetOrigin()[1] - box->GetDY(), + box->GetOrigin()[2] - box->GetDZ()}; + const Point3D hi{box->GetOrigin()[0] + box->GetDX(), box->GetOrigin()[1] + box->GetDY(), + box->GetOrigin()[2] + box->GetDZ()}; + const QuerySamples samples = + buildQuerySamples(shape, "self", lo, hi, opt.perfPoints, opt.perfRays); + const TimingStat contains = timeContainsPass(shape, samples, opt.perfWarmup, opt.perfPasses); + const TimingStat safety = timeSafetyPass(shape, samples, opt.perfWarmup, opt.perfPasses); + const TimingStat distOut = timeDistOutPass(shape, samples, opt.perfWarmup, opt.perfPasses); + const TimingStat distIn = timeDistInPass(shape, samples, opt.perfWarmup, opt.perfPasses); + std::printf(" --- K=%-3d %-9s (leaves=%lld nodes=%lld depth=%d, %.1f%% of points inside) ---\n", + k, kindName, tree.leaves, tree.nodes, tree.depth, + 100. * static_cast(samples.insidePoints) / + static_cast(std::max(1, samples.points.size()))); + printTiming("Contains", contains); + printTiming("Safety", safety); + printTiming("DistFromOutside", distOut); + printTiming("DistFromInside", distIn); + out.push_back({{"leavesRequested", k}, + {"treeShape", kindName}, + {"leaves", tree.leaves}, + {"nodes", tree.nodes}, + {"depth", tree.depth}, + {"buildResidentBytes", (after - before).residentBytes}, + {"buildHeapBytes", (after - before).heapInUseBytes}, + {"insideFraction", static_cast(samples.insidePoints) / + static_cast(std::max(1, samples.points.size()))}, + {"contains", timingToJson(contains)}, + {"safety", timingToJson(safety)}, + {"distFromOutside", timingToJson(distOut)}, + {"distFromInside", timingToJson(distIn)}}); + delete manager; + gGeoManager = nullptr; + } + } + return out; +} + +// ------------------------------------------------------------------------------------------ +// Self-test: analytic references, and the controls that prove the comparison can fail +// ------------------------------------------------------------------------------------------ + +int selfTest() +{ + int failures = 0; + auto check = [&](const char* name, bool ok, const std::string& detail = {}) { + std::printf(" [%s] %s%s\n", ok ? "ok " : "FAIL", name, + ok || detail.empty() ? "" : (" " + detail).c_str()); + if (!ok) { + ++failures; + } + }; + + StepConfig cfg; + Robustness stats; + + // 1. A box: exactly two crossings, at analytically known distances. + { + TGeoBBox box("selftestBox", 1., 1.5, 2.); + const Point3D origin{-5., 0., 0.}; + const Point3D dir{1., 0., 0.}; + auto crossings = stepWithShapeApi(&box, origin, dir, 10., cfg, stats); + check("box: exactly two crossings along a central ray", crossings.size() == 2, + "got " + std::to_string(crossings.size())); + if (crossings.size() == 2) { + check("box: enter at 4.0 cm", std::fabs(crossings[0].t - 4.) < 1.e-9); + check("box: exit at 6.0 cm", std::fabs(crossings[1].t - 6.) < 1.e-9); + check("box: kinds are enter then exit", crossings[0].kind == +1 && crossings[1].kind == -1); + } + } + + // 2. A hollow tube: FOUR crossings along a diameter. This is the case a single-shot `distout` + // query cannot express at all -- it reports the first of the four and stops. + { + TGeoTube tube("selftestTube", 0.5, 1.0, 2.0); + const Point3D origin{-5., 0., 0.}; + const Point3D dir{1., 0., 0.}; + auto crossings = stepWithShapeApi(&tube, origin, dir, 10., cfg, stats); + check("hollow tube: four crossings along a diameter", crossings.size() == 4, + "got " + std::to_string(crossings.size())); + if (crossings.size() == 4) { + const double expect[4] = {4.0, 4.5, 5.5, 6.0}; + bool ok = true; + for (int i = 0; i < 4; ++i) { + ok = ok && std::fabs(crossings[i].t - expect[i]) < 1.e-9; + } + check("hollow tube: crossings at 4.0 / 4.5 / 5.5 / 6.0 cm", ok); + check("hollow tube: in, out, in, out", + crossings[0].kind == +1 && crossings[1].kind == -1 && crossings[2].kind == +1 && + crossings[3].kind == -1); + } + } + + // 3a. A BOX's chord integral is EXACT, at every raster density, when the window is its own + // bounding box. That is the sharpest available self-check on the volume instrument: no + // convergence argument, no tolerance -- either the quadrature is the volume or it is not. + // It is also what fixed the raster geometry: with the window inflated by 2 % instead, this + // same box came out 5.1e-02 too large at N = 32. + { + TGeoBBox box("selftestVolBox", 1., 1.5, 2.); + const Point3D bboxMin{-1., -1.5, -2.}; + const Point3D bboxMax{1., 1.5, 2.}; + for (const int n : {7, 32}) { + Raster raster = buildRaster(bboxMin, bboxMax, n, buildBeams("xyz", 0.), 0.); + Robustness s; + std::vector byAxis(3, 0.); + for (const auto& ray : raster.rays) { + const double before = s.insideLength; + auto crossings = stepWithShapeApi(&box, ray.origin, ray.dir, ray.tMax, cfg, s); + auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, s); + byAxis[ray.beam] += s.insideLength - before; + } + const double volume = chordVolume(raster, byAxis); + check(("box 2 x 3 x 4 cm: chord integral is EXACT at N=" + std::to_string(n)).c_str(), + std::fabs(volume - 24.) < 1.e-9, "got " + std::to_string(volume)); + } + } + + // 3b. A sphere's chord integral against its closed-form volume. A curved silhouette cannot be + // exact at finite N, so this is where the ACHIEVED PRECISION of the volume instrument is + // measured -- and the measurement says the convergence is NOT monotone in N (the silhouette + // cells realign with the lattice at every density), so the honest statement is an envelope + // at a stated density, never an extrapolation. + { + TGeoSphere sphere("selftestSphere", 0., 1.); + const Point3D bboxMin{-1., -1., -1.}; + const Point3D bboxMax{1., 1., 1.}; + const double exact = 4. / 3. * 3.14159265358979323846; + double worst = 0.; + for (const int n : {24, 48, 96, 192}) { + Raster raster = buildRaster(bboxMin, bboxMax, n, buildBeams("z", 0.), 0.); + Robustness s; + for (const auto& ray : raster.rays) { + auto crossings = stepWithShapeApi(&sphere, ray.origin, ray.dir, ray.tMax, cfg, s); + auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, s); + } + const double volume = s.insideLength * raster.cellArea[0]; + const double rel = std::fabs(volume - exact) / exact; + worst = std::max(worst, rel); + std::printf( + " sphere r=1: raster %3d x %3d -> V = %.8f cm^3, exact %.8f, " + "relative %.3e\n", + n, n, volume, exact, rel); + } + // The bound is the MEASURED envelope over N = 24..192, not a convergence rate. If a future + // change makes the quadrature worse than this it is a regression; if the envelope itself has + // to be widened, that is a result to report rather than a constant to tune. + check("sphere chord integral stays inside the measured 2e-3 envelope for N = 24..192", + worst < 2.e-3, "worst rel=" + std::to_string(worst)); + } + + // 4. THE CONTROLS. A comparison that cannot fail is not a comparison. Take a correct crossing + // list and (i) perturb one distance, (ii) drop one crossing, (iii) duplicate one, and require + // the comparator to name each. + { + const std::vector truth{{4.0, +1}, {4.5, -1}, {5.5, +1}, {6.0, -1}}; + const Point3D o{-5., 0., 0.}; + const Point3D d{1., 0., 0.}; + + ListComparison clean; + compareLists(truth, truth, o, d, 1.e-6, clean); + check("control 0: identical lists compare clean", + clean.raysIdentical == 1 && clean.missing == 0 && clean.extra == 0 && + clean.matched == 4); + + auto perturbed = truth; + perturbed[2].t += 1.e-3; + ListComparison shifted; + compareLists(perturbed, truth, o, d, 1.e-6, shifted); + check("control 1: a crossing moved by 1e-3 cm is CAUGHT, and as DISPLACED not as lost", + shifted.raysIdentical == 0 && shifted.displaced == 1 && shifted.missing == 0 && + shifted.extra == 0 && std::fabs(shifted.worstDeltaT - 1.e-3) < 1.e-12, + "displaced=" + std::to_string(shifted.displaced) + " missing=" + + std::to_string(shifted.missing) + " dt=" + std::to_string(shifted.worstDeltaT)); + + auto dropped = truth; + dropped.erase(dropped.begin() + 1); + ListComparison lost; + compareLists(dropped, truth, o, d, 1.e-6, lost); + check("control 2: a dropped crossing is CAUGHT as `missing`", + lost.missing == 1 && lost.extra == 0, "missing=" + std::to_string(lost.missing)); + + auto doubled = truth; + doubled.insert(doubled.begin() + 1, {4.2, -1}); + ListComparison spurious; + compareLists(doubled, truth, o, d, 1.e-6, spurious); + check("control 3: an extra crossing is CAUGHT as `extra`", + spurious.extra == 1 && spurious.missing == 0, "extra=" + std::to_string(spurious.extra)); + + // A crossing at the right place but with the wrong sense (enter where the truth exits) is a + // different defect and must not be absorbed into `matched`. + auto flipped = truth; + flipped[1].kind = +1; + ListComparison sense; + compareLists(flipped, truth, o, d, 1.e-6, sense); + check("control 4: a crossing with the wrong sense is CAUGHT", sense.kindMismatch == 1); + } + + // 5b. THE TIMING HARNESS'S OWN NEGATIVE CONTROL. A timing harness that cannot distinguish a + // deliberately slowed shape from a fast one is not measuring what it claims. So: + // time the same kernels on a TGeoBBox and on a TGeoBBox carrying ballast, and require the + // number to MOVE, in the right direction, on all four kernels. + { + TGeoBBox fast("perfControlFast", 1., 1., 1.); + BallastShape slow("perfControlSlow", 1., 1., 1., 60); + const Point3D lo{-1., -1., -1.}; + const Point3D hi{1., 1., 1.}; + const QuerySamples samples = buildQuerySamples(&fast, "control", lo, hi, 2000, 2000); + check("control 5: the shared sample set has both inside and outside points", + samples.insidePoints > 100 && + samples.insidePoints < static_cast(samples.points.size()) - 100, + "inside=" + std::to_string(samples.insidePoints) + " of " + + std::to_string(samples.points.size())); + check("control 6: the sample partition is consistent with the reference it came from", [&] { + for (size_t i = 0; i < samples.points.size(); ++i) { + if (fast.Contains(samples.points[i].data()) != (samples.pointIsInside[i] != 0)) { + return false; + } + } + return true; + }()); + check("control 7: DistFromOutside rays actually hit (an all-miss set times the early-out)", + timeDistOutPass(&fast, samples, 1, 3).hitFraction > 0.5); + + struct Kernel { + const char* name; + TimingStat (*fn)(const TGeoShape*, const QuerySamples&, int, int); + }; + const Kernel kernels[4] = {{"Contains", &timeContainsPass}, + {"Safety", &timeSafetyPass}, + {"DistFromOutside", &timeDistOutPass}, + {"DistFromInside", &timeDistInPass}}; + for (const auto& kernel : kernels) { + const TimingStat quick = kernel.fn(&fast, samples, 2, 7); + const TimingStat heavy = kernel.fn(&slow, samples, 2, 7); + const double ratio = quick.medianNsPerCall > 0. ? heavy.medianNsPerCall / quick.medianNsPerCall : 0.; + check((std::string("control 8: ballast is VISIBLE on ") + kernel.name + + " (the timing harness can move its own number)") + .c_str(), + ratio > 2., + std::string("fast=") + std::to_string(quick.medianNsPerCall) + " ns slow=" + + std::to_string(heavy.medianNsPerCall) + " ns ratio=" + std::to_string(ratio)); + check((std::string("control 9: the ") + kernel.name + + " timing loop was not elided (non-zero checksum, positive time)") + .c_str(), + quick.checksum != 0 && quick.medianNsPerCall > 0. && quick.passes == 7); + } + } + + // 5c. THE MEMORY PROBE'S NEGATIVE CONTROL. Both memory numbers must move when memory is taken, + // and the heap number must come back when it is given up. Without this the "resident delta" + // column could be reporting allocator noise and nobody would know. + { + const MemorySnapshot before = readMemory(); + constexpr size_t kBytes = 64u << 20; + auto* block = new char[kBytes]; + for (size_t i = 0; i < kBytes; i += 4096) { + block[i] = static_cast(i); // touch every page: an untouched mmap is not resident + } + const MemorySnapshot held = readMemory(); + const MemorySnapshot delta = held - before; + check("control 10: the resident probe sees a 64 MB touched allocation", + delta.residentBytes > 32LL << 20, + "delta=" + std::to_string(delta.residentBytes >> 20) + " MB"); + check("control 11: the heap probe sees a 64 MB allocation", + delta.heapInUseBytes > 32LL << 20, + "delta=" + std::to_string(delta.heapInUseBytes >> 20) + " MB"); + delete[] block; + const MemorySnapshot released = readMemory() - before; + check("control 12: the heap probe sees it released again (the resident one need not)", + released.heapInUseBytes < 8LL << 20, + "still=" + std::to_string(released.heapInUseBytes >> 20) + " MB"); + } + + // 5d. THE STRUCTURAL MEMORY CONTROL. The exact column has to depend on the geometry, so build + // the same tree twice at different sizes and require the count -- and the derived byte + // figure -- to follow. A structural number that does not move with the structure is a + // constant with a units label. + { + auto* manager = new TGeoManager("perfControlLadder", "structural control"); + TGeoShape* small = buildBooleanLadder(4, LadderShape::Balanced, "ctlS"); + TGeoShape* big = buildBooleanLadder(32, LadderShape::Balanced, "ctlB"); + const BooleanTreeStats a = booleanTreeStats(small); + const BooleanTreeStats b = booleanTreeStats(big); + check("control 13: the ladder builds the leaf count it was asked for", + a.leaves == 4 && b.leaves == 32, + "got " + std::to_string(a.leaves) + " and " + std::to_string(b.leaves)); + check("control 14: a balanced ladder's depth is logarithmic in its leaf count", + a.depth == 3 && b.depth == 6, + "depth " + std::to_string(a.depth) + " and " + std::to_string(b.depth)); + TGeoShape* chain = buildBooleanLadder(32, LadderShape::Chain, "ctlC"); + const BooleanTreeStats c = booleanTreeStats(chain); + check( + "control 15: a chain ladder of the same leaf count is deeper, so the two tree shapes " + "really are different fixtures", + c.leaves == 32 && c.depth == 32, + "leaves=" + std::to_string(c.leaves) + " depth=" + std::to_string(c.depth)); + delete manager; + gGeoManager = nullptr; + } + + // 5. The parity audit's own control: hand it a list with a crossing removed and require the + // midpoint Contains() check to contradict it. + { + TGeoBBox box("selftestBox2", 1., 1., 1.); + const Point3D origin{-5., 0., 0.}; + const Point3D dir{1., 0., 0.}; + Robustness good; + auditCrossingList({{4.0, +1}, {6.0, -1}}, &box, origin, dir, 10., cfg, good); + check("parity audit: a correct list has no parity mismatch", good.parityMismatchIntervals == 0); + Robustness bad; + auditCrossingList({{4.0, +1}}, &box, origin, dir, 10., cfg, bad); + check("parity audit: a truncated list is CAUGHT by Contains() at the midpoints", + bad.parityMismatchIntervals > 0 && bad.oddCrossingLists == 1); + } + + std::printf("\n%s: %d failure(s)\n", failures == 0 ? "SELF-TEST PASSED" : "SELF-TEST FAILED", + failures); + return failures == 0 ? 0 : 1; +} + +} // namespace + +int main(int argc, char** argv) +{ + Options opt; + try { + if (!parseArgs(argc, argv, opt)) { + return 0; + } + } catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; + printUsage(argv[0]); + return 1; + } + + if (opt.selfTest) { + return selfTest(); + } + + if (!opt.ladderSpec.empty()) { + json ladder = runLadder(opt); + if (!opt.jsonOut.empty()) { + std::ofstream out(opt.jsonOut); + out << json{{"ladder", std::move(ladder)}}.dump(1); + std::printf("\nreport: %s\n", opt.jsonOut.c_str()); + } + return 0; + } + + std::vector beams; + std::vector parts; + try { + beams = opt.fanBeams > 0 ? buildFanBeams(opt.fanBeams) + : buildBeams(opt.axesSpec, opt.tiltDegrees); + if (beams.empty()) { + throw std::runtime_error("no beam selected (--axes)"); + } + parts = collectParts(opt); + } catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; + return 1; + } + if (parts.empty()) { + std::cerr << "no parts matched (pattern='" << opt.partsPattern << "')\n"; + return 1; + } + + json report = json::array(); + + // ---- --perf: the representation cost/memory comparison --------------------------------- + if (opt.perf) { + std::printf( + "Per-call costs are WARM-CACHE, single-threaded, median of %d passes after %d " + "warmup passes.\nEvery representation of a part answers the SAME sample set.\n\n", + opt.perfPasses, opt.perfWarmup); + for (const auto& part : parts) { + std::printf("=== %s (%s) ===\n", part.id.c_str(), part.model.c_str()); + Point3D lo{}; + Point3D hi{}; + std::string bboxSource; + if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) { + std::printf(" skip: no representation could supply a bounding box\n"); + continue; + } + const Raster raster = buildRaster(lo, hi, opt.raster, beams, opt.margin); + + // The sample set is built ONCE, from the first representation present in the order + // surface -> mesh -> shape, and every representation is then asked exactly it. The order is + // a preference for the representation whose Contains() is exact, not an accident: the + // partition is a fixed label, so it wants to come from the most trustworthy classifier + // available, and it is reported either way. + QuerySamples samples; + std::string partitionedBy; + for (const auto& candidate : allRepresentations()) { + const std::string& source = sourceFor(part, candidate); + if (!opt.representations.count(candidate) || !fileExists(source)) { + continue; + } + LoadedRep rep = loadRepresentation(candidate, source, part.id, opt.flatSplitDepth, + opt.flatMinBoxFraction); + if (rep.ok) { + // Points are drawn in the PART frame; a placed shape classifies them in its own. + QuerySamples inFrame = + buildQuerySamples(rep.shape, candidate, lo, hi, opt.perfPoints, opt.perfRays); + if (rep.placement) { + // Undo the frame so the stored set is the part frame's, as every other consumer + // expects. Drawing in the shape frame and unmapping is equivalent and simpler than + // threading the matrix through the generator. + for (auto& p : inFrame.points) { + Point3D q; + rep.placement->LocalToMaster(p.data(), q.data()); + p = q; + } + for (auto* rays : {&inFrame.outsideRays, &inFrame.insideRays}) { + for (auto& r : *rays) { + Point3D o; + Point3D d; + rep.placement->LocalToMaster(r.origin.data(), o.data()); + rep.placement->LocalToMasterVect(r.dir.data(), d.data()); + r.origin = o; + r.dir = d; + } + } + } + samples = std::move(inFrame); + partitionedBy = candidate; + } + delete rep.manager; + gGeoManager = nullptr; + if (!partitionedBy.empty()) { + break; + } + } + if (partitionedBy.empty()) { + std::printf(" skip: no representation loaded\n"); + continue; + } + samples.partitionedBy = partitionedBy; + std::printf( + " samples: %zu points (%.1f%% inside), %zu outside rays, %zu inside rays, " + "partitioned by '%s'; raster %d x %d x %zu beams = %zu rays\n", + samples.points.size(), + 100. * static_cast(samples.insidePoints) / + static_cast(std::max(1, samples.points.size())), + samples.outsideRays.size(), samples.insideRays.size(), partitionedBy.c_str(), + raster.n, raster.n, raster.beams.size(), raster.rays.size()); + + json partJson; + partJson["id"] = part.id; + partJson["model"] = part.model; + partJson["partitionedBy"] = partitionedBy; + partJson["insideFraction"] = static_cast(samples.insidePoints) / + static_cast(std::max(1, samples.points.size())); + partJson["bboxSource"] = bboxSource; + json repsJson = json::array(); + + for (const auto& candidate : allRepresentations()) { + const std::string& source = sourceFor(part, candidate); + if (!opt.representations.count(candidate) || !fileExists(source)) { + continue; + } + LoadedRep rep = loadRepresentation(candidate, source, part.id, opt.flatSplitDepth, + opt.flatMinBoxFraction); + if (!rep.ok) { + std::printf(" [skip %s] would not load from %s\n", candidate.c_str(), source.c_str()); + delete rep.manager; + gGeoManager = nullptr; + continue; + } + const QuerySamples local = toShapeFrame(samples, rep.placement.get()); + std::printf(" --- %-8s %-22s (%lld %s, load %.3f s + close %.3f s) ---\n", candidate.c_str(), + rep.shape->ClassName(), rep.structural.primitives, + candidate == "mesh" ? "triangles" + : candidate == "surface" ? "patches" + : candidate == "flatcsg" ? "cells" + : "leaves", + rep.loadSeconds, rep.closeSeconds); + + const TimingStat contains = timeContainsPass(rep.shape, local, opt.perfWarmup, opt.perfPasses); + const TimingStat safety = timeSafetyPass(rep.shape, local, opt.perfWarmup, opt.perfPasses); + const TimingStat distOut = timeDistOutPass(rep.shape, local, opt.perfWarmup, opt.perfPasses); + const TimingStat distIn = timeDistInPass(rep.shape, local, opt.perfWarmup, opt.perfPasses); + printTiming("Contains", contains); + printTiming("Safety", safety); + printTiming("DistFromOutside", distOut); + printTiming("DistFromInside", distIn); + + // Full geantino transport over the raster, timed the same way: several complete passes, + // median reported. This is the number a simulation actually pays, and it is the only one + // that composes the four kernels in the order a transport does. + Robustness statsTransport; + long long crossings = 0; + const TimingStat transport = + timePasses(static_cast(raster.rays.size()), opt.perfWarmup, opt.perfPasses, [&]() { + uint64_t acc = 0; + Robustness s; + long long found = 0; + for (const auto& ray : raster.rays) { + Point3D o; + Point3D d; + toShapeFrame(rep.placement.get(), ray.origin, ray.dir, o, d); + const auto list = stepWithShapeApi(rep.shape, o, d, ray.tMax, opt.step, s); + found += static_cast(list.size()); + acc += list.size(); + } + statsTransport = s; + crossings = found; + return acc; + }); + // `crossings` is set from the last pass; every pass sees the same rays, so it is the + // per-pass crossing count and the ns/crossing below is exact rather than averaged over a + // varying denominator. It is counted from the returned lists rather than from the + // Robustness bookkeeping, which only fills in `crossings` when the per-ray audit runs -- + // and the audit is deliberately NOT run inside a timed pass, because Contains() at every + // interval midpoint would put a fifth kernel into a transport measurement. + const double nsPerCrossing = + crossings > 0 ? transport.medianNsPerCall * static_cast(raster.rays.size()) / + static_cast(crossings) + : 0.; + std::printf( + " %-14s %9.1f ns/ray [%9.1f .. %9.1f, spread %5.1f%%] %.1f ns/crossing " + "(%lld crossings, %lld steps)\n", + "transport", transport.medianNsPerCall, transport.minNsPerCall, + transport.maxNsPerCall, 100. * transport.spread, nsPerCrossing, crossings, + statsTransport.steps); + + const MemorySnapshot total{rep.loadDelta.residentBytes + rep.closeDelta.residentBytes, + rep.loadDelta.heapInUseBytes + rep.closeDelta.heapInUseBytes}; + std::printf( + " memory: structural %lld B (%s)\n" + " sidecar on disk %lld B | measured heap +%lld B (load %lld + close " + "%lld) | resident +%lld B\n", + rep.structural.bytes, rep.structural.formula.c_str(), + rep.structural.sidecarBytes, total.heapInUseBytes, rep.loadDelta.heapInUseBytes, + rep.closeDelta.heapInUseBytes, total.residentBytes); + if (candidate == "mesh" && !rep.meshClosedBody) { + std::printf( + " *** meshClosedBody = FALSE: this mesh is INVALID, not merely " + "inaccurate. Read no accuracy column of this row as a safety statement. ***\n"); + } + + json repJson; + repJson["name"] = candidate; + repJson["source"] = source; + repJson["shapeClass"] = rep.shape->ClassName(); + repJson["primitives"] = rep.structural.primitives; + repJson["loadSeconds"] = rep.loadSeconds; + repJson["closeSeconds"] = rep.closeSeconds; + repJson["structuralBytes"] = rep.structural.bytes; + repJson["structuralFormula"] = rep.structural.formula; + repJson["sidecarBytes"] = rep.structural.sidecarBytes; + repJson["heapBytesLoad"] = rep.loadDelta.heapInUseBytes; + repJson["heapBytesClose"] = rep.closeDelta.heapInUseBytes; + repJson["heapBytesTotal"] = total.heapInUseBytes; + repJson["residentBytesTotal"] = total.residentBytes; + repJson["capacity"] = rep.shape->Capacity(); + repJson["placed"] = (rep.placement != nullptr); + repJson["contains"] = timingToJson(contains); + repJson["safety"] = timingToJson(safety); + repJson["distFromOutside"] = timingToJson(distOut); + repJson["distFromInside"] = timingToJson(distIn); + repJson["transport"] = timingToJson(transport); + repJson["transportNsPerCrossing"] = nsPerCrossing; + repJson["transportCrossings"] = crossings; + repJson["transportSteps"] = statsTransport.steps; + repJson["transportUnterminated"] = statsTransport.unterminated; + repJson["transportParityMismatch"] = statsTransport.parityMismatchIntervals; + if (candidate == "mesh") { + repJson["meshClosedBody"] = rep.meshClosedBody; + } + if (rep.surfaceSolid != nullptr) { + repJson["localise"] = localiseSurfaceSolid(rep.surfaceSolid, local, opt.perfWarmup, + opt.perfPasses); + } + if (rep.flatSolid != nullptr) { + // The two counts the crossover is regressed against (Design_FlatCSGSolid.md section 9), + // plus the box structure the split knobs move. + long long active = 0; + long long worst = 0; + for (int i = 0; i < rep.flatSolid->GetNboxes(); ++i) { + const long long n = rep.flatSolid->GetBox(i).nActive; + active += n; + worst = std::max(worst, n); + } + repJson["flatCells"] = rep.flatSolid->GetNcells(); + repJson["flatHalfspaces"] = rep.flatSolid->GetNhalfspaces(); + repJson["flatBoxes"] = rep.flatSolid->GetNboxes(); + repJson["flatActiveTotal"] = active; + repJson["flatActiveMean"] = + rep.flatSolid->GetNboxes() > 0 + ? static_cast(active) / static_cast(rep.flatSolid->GetNboxes()) + : 0.; + repJson["flatActiveMax"] = worst; + repJson["flatBVHBytes"] = static_cast(rep.flatSolid->GetBVHMemory()); + repJson["flatSplitDepth"] = opt.flatSplitDepth; + repJson["flatMinBoxFraction"] = opt.flatMinBoxFraction; + repJson["flatCloseSeconds"] = rep.closeSeconds; + } + repsJson.push_back(std::move(repJson)); + delete rep.manager; + gGeoManager = nullptr; + } + partJson["representations"] = std::move(repsJson); + report.push_back(std::move(partJson)); + std::printf("\n"); + } + if (!opt.jsonOut.empty()) { + std::ofstream out(opt.jsonOut); + out << report.dump(1); + std::printf("\nreport: %s\n", opt.jsonOut.c_str()); + } + return 0; + } + + for (const auto& part : parts) { + std::printf("=== %s (%s) ===\n", part.id.c_str(), part.model.c_str()); + json partJson; + partJson["id"] = part.id; + partJson["model"] = part.model; + + // ---- the raster window ------------------------------------------------------------- + // In scoring mode it comes from the oracle's answer file, so the two sides cannot possibly be + // asking about different rays; otherwise it is built here from the tightest containing + // bounding box the part has. + Raster raster; + OracleCrossings oracle; + std::string bboxSource = "?"; + if (!opt.refCrossings.empty()) { + try { + oracle = loadOracleCrossings(opt.refCrossings, part.id); + } catch (const std::exception& e) { + std::cerr << " error reading crossings: " << e.what() << "\n"; + continue; + } + if (!oracle.has) { + std::printf(" skip: no crossings file for this part in %s\n", opt.refCrossings.c_str()); + continue; + } + raster = oracle.raster; + opt.step.matchTolerance = std::max(oracle.tolerance, 1.e-6); + std::printf( + " oracle: %s tolerance=%.3g capacity=%.6g cm^3 chordVolume=%.6g cm^3 " + "(%lld ambiguous ray(s))\n", + oracle.valid ? "valid" : "*** NOT BRepCheck-VALID ***", oracle.tolerance, + oracle.capacity, oracle.volumeChord, oracle.ambiguousRays); + } else { + Point3D lo{}; + Point3D hi{}; + if (!resolveBoundingBox(part, opt, lo, hi, bboxSource)) { + std::printf(" skip: no representation could supply a bounding box\n"); + continue; + } + raster = buildRaster(lo, hi, opt.raster, beams, opt.margin); + std::printf( + " raster: %d x %d x %zu beam(s) = %zu rays (tilt %.3g deg); window from the " + "'%s' bounding box + %.3g cm, cross-section excess %.3g\n", + raster.n, raster.n, raster.beams.size(), raster.rays.size(), opt.tiltDegrees, + bboxSource.c_str(), raster.transverseMargin, raster.windowExcess.front()); + } + + // `--dump-rays` writes the raster and stops: the oracle answers it next, and the scoring pass + // then reads the rays back from the oracle's file. Nothing is stepped here. + if (!opt.dumpRays.empty()) { + writeRays(opt.dumpRays, part.id, raster, bboxSource); + continue; + } + + // ---- representations --------------------------------------------------------------- + struct RepSpec { + std::string name; + std::string source; + }; + std::vector specs; + if (opt.representations.count("surface") && fileExists(part.surfaces)) { + specs.push_back({"surface", part.surfaces}); + } + if (opt.representations.count("mesh") && fileExists(part.facets)) { + specs.push_back({"mesh", part.facets}); + } + if (opt.representations.count("shape") && fileExists(part.shape)) { + specs.push_back({"shape", part.shape}); + } + if (opt.representations.count("flatcsg") && fileExists(part.flatcsg)) { + specs.push_back({"flatcsg", part.flatcsg}); + } + if (specs.empty()) { + std::printf(" skip: no representation available\n"); + continue; + } + + json repsJson = json::array(); + + for (const auto& spec : specs) { + // A fresh TGeoManager per representation: it owns the shape (TGeoShape registers itself in + // gGeoManager on construction, so any other arrangement double-frees) and it carries the + // one-part world mode (b) transports through. + auto* manager = new TGeoManager(("xray_" + spec.name).c_str(), "X-ray benchmark world"); + TGeoShape* shape = nullptr; + // The shape's own frame, when it is not the part frame. Mode (a) transforms each ray into + // it; mode (b) puts it on the node. Owned here: the manager owns the shape, not the matrix. + std::unique_ptr placement; + double loadSeconds = 0.; + int primitives = -1; + const char* primitiveKind = ""; + const auto tLoad0 = std::chrono::steady_clock::now(); + if (spec.name == "surface") { + auto* solid = new O2BVHSurfaceSolid(part.id.c_str()); + if (!LoadSurfaceSolid(spec.source, *solid)) { + std::printf(" [skip %s] LoadSurfaceSolid failed for %s\n", spec.name.c_str(), + spec.source.c_str()); + delete manager; + gGeoManager = nullptr; + continue; + } + solid->CloseShape(true); + primitives = solid->GetNsurfaces(); + primitiveKind = "patches"; + shape = solid; + } else if (spec.name == "mesh") { + auto* solid = new O2Tessellated(part.id.c_str()); + if (!LoadFacetSolid(spec.source, *solid)) { + std::printf(" [skip %s] LoadFacetSolid failed for %s\n", spec.name.c_str(), + spec.source.c_str()); + delete manager; + gGeoManager = nullptr; + continue; + } + solid->CloseShape(); + primitives = solid->GetNfacets(); + primitiveKind = "triangles"; + shape = solid; + } else if (spec.name == "flatcsg") { + auto* solid = new O2FlatCSG(part.id.c_str()); + if (opt.flatSplitDepth >= 0) { + solid->SetSplitDepth(opt.flatSplitDepth); + } + if (opt.flatMinBoxFraction >= 0.) { + solid->SetMinBoxFraction(opt.flatMinBoxFraction); + } + if (!LoadFlatCSG(spec.source, *solid)) { + std::printf(" [skip %s] LoadFlatCSG failed for %s\n", spec.name.c_str(), + spec.source.c_str()); + delete manager; + gGeoManager = nullptr; + continue; + } + solid->CloseShape(); + if (!solid->IsClosed()) { + std::printf( + " [skip %s] CloseShape refused %s, so the shape would answer through its " + "_Loop twins and the row would not be the accelerated path\n", + spec.name.c_str(), spec.source.c_str()); + delete manager; + gGeoManager = nullptr; + continue; + } + primitives = solid->GetNcells(); + primitiveKind = "cells"; + shape = solid; + } else { + std::string error; + shape = loadShapeFromRootFile(spec.source, &error); + if (shape == nullptr) { + std::printf(" [skip %s] %s\n", spec.name.c_str(), error.c_str()); + delete manager; + gGeoManager = nullptr; + continue; + } + placement.reset(loadShapePlacementFromRootFile(spec.source)); + primitiveKind = shape->ClassName(); + } + loadSeconds = std::chrono::duration(std::chrono::steady_clock::now() - tLoad0).count(); + + const auto* box = dynamic_cast(shape); + + std::printf(" --- %-8s %-28s (%d %s, load %.3f s) ---\n", spec.name.c_str(), + shape->ClassName(), primitives, primitiveKind, loadSeconds); + + json repJson; + repJson["name"] = spec.name; + repJson["source"] = spec.source; + repJson["shapeClass"] = shape->ClassName(); + repJson["primitives"] = primitives; + repJson["primitiveKind"] = primitiveKind; + repJson["loadSeconds"] = loadSeconds; + repJson["capacity"] = shape->Capacity(); + repJson["placed"] = (placement != nullptr); + + // ---- mode (a): the shape API ------------------------------------------------------ + Robustness statsA; + std::vector insideByAxisA(raster.beams.size(), 0.); + std::vector> listsA(raster.rays.size()); + ListComparison vsOracleA; + { + const auto t0 = std::chrono::steady_clock::now(); + for (size_t i = 0; i < raster.rays.size(); ++i) { + const auto& ray = raster.rays[i]; + const double before = statsA.insideLength; + Point3D o; + Point3D d; + toShapeFrame(placement.get(), ray.origin, ray.dir, o, d); + listsA[i] = stepWithShapeApi(shape, o, d, ray.tMax, opt.step, statsA); + auditCrossingList(listsA[i], shape, o, d, ray.tMax, opt.step, statsA); + insideByAxisA[ray.beam] += statsA.insideLength - before; + } + statsA.seconds = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + } + repJson["modeA"] = robustnessToJson(statsA); + repJson["modeA"]["volumeChordCm3"] = chordVolume(raster, insideByAxisA); + json perAxisA = json::object(); + for (size_t b = 0; b < raster.beams.size(); ++b) { + perAxisA[raster.beams[b].label] = insideByAxisA[b] * raster.cellArea[b]; + } + repJson["modeA"]["volumeChordPerAxisCm3"] = perAxisA; + if (oracle.has) { + for (size_t i = 0; i < raster.rays.size() && i < oracle.perRay.size(); ++i) { + if (oracle.ambiguous[i]) { + continue; // OCCT declined somewhere along this ray; there is no ground truth to score + } + compareLists(listsA[i], oracle.perRay[i], raster.rays[i].origin, raster.rays[i].dir, + opt.step.matchTolerance, vsOracleA); + } + repJson["modeA"]["vsOracle"] = comparisonToJson(vsOracleA); + } + std::printf( + " (a) shape API : %lld rays, %lld crossings, %.4f s | zero=%lld stall=%lld " + "nonAdv=%lld cap=%lld unterm=%lld odd=%lld dup=%lld parity=%lld\n", + statsA.rays, statsA.crossings, statsA.seconds, statsA.zeroLengthSteps, + statsA.unstickPushes, statsA.nonAdvancingSteps, statsA.iterationCapHits, + statsA.unterminated, statsA.oddCrossingLists, statsA.duplicateCrossings, + statsA.parityMismatchIntervals); + if (oracle.has) { + std::printf( + " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld " + "displaced=%lld kind=%lld worst dt=%.3g cm\n", + vsOracleA.raysIdentical, vsOracleA.rays, vsOracleA.missing, vsOracleA.extra, + vsOracleA.displaced, vsOracleA.kindMismatch, vsOracleA.worstDeltaT); + if (!vsOracleA.worstReason.empty() && vsOracleA.worstReason != "deltaT") { + std::printf(" worst : %s at o=(%.6g, %.6g, %.6g) d=(%.4g, %.4g, %.4g)\n", + vsOracleA.worstReason.c_str(), vsOracleA.worstOrigin[0], + vsOracleA.worstOrigin[1], vsOracleA.worstOrigin[2], vsOracleA.worstDir[0], + vsOracleA.worstDir[1], vsOracleA.worstDir[2]); + } + } + std::printf(" volume : chord integral %.8g cm^3 (Capacity %.8g)\n", + repJson["modeA"]["volumeChordCm3"].get(), shape->Capacity()); + + // ---- mode (b): the real navigator ------------------------------------------------- + if (!opt.skipNavigator && box != nullptr) { + Robustness statsB; + std::vector insideByAxisB(raster.beams.size(), 0.); + ListComparison vsOracleB; + ListComparison aVsB; + // The world must contain the part AND every ray of the raster, start to finish. Deriving + // it from the axis-aligned window is not enough once the beams are tilted: a rotated + // lattice reaches outside the part's own box, and the first version of this loop reported + // 5358 lost crossings at a 27 degree tilt that were entirely its own undersized world. + Point3D wMin; + Point3D wMax; + placedBox(*box, placement.get(), wMin, wMax); + for (const auto& ray : raster.rays) { + for (int k = 0; k < 3; ++k) { + const double end = ray.origin[k] + ray.tMax * ray.dir[k]; + wMin[k] = std::min({wMin[k], ray.origin[k], end}); + wMax[k] = std::max({wMax[k], ray.origin[k], end}); + } + } + NavigatorTransport transport(manager, shape, wMin, wMax, placement.get()); + const auto t0 = std::chrono::steady_clock::now(); + for (size_t i = 0; i < raster.rays.size(); ++i) { + const auto& ray = raster.rays[i]; + const double before = statsB.insideLength; + auto listB = transport.transport(ray.origin, ray.dir, ray.tMax, opt.step, statsB); + // The shape is handed in here as well, deliberately: in mode (b) the parity audit + // compares the NAVIGATOR's crossing list against the SHAPE's own Contains(), which is a + // genuine cross-check between the two and not a tautology. + Point3D o; + Point3D d; + toShapeFrame(placement.get(), ray.origin, ray.dir, o, d); + auditCrossingList(listB, shape, o, d, ray.tMax, opt.step, statsB); + insideByAxisB[ray.beam] += statsB.insideLength - before; + if (oracle.has && i < oracle.perRay.size() && !oracle.ambiguous[i]) { + compareLists(listB, oracle.perRay[i], ray.origin, ray.dir, opt.step.matchTolerance, + vsOracleB); + } + compareLists(listB, listsA[i], ray.origin, ray.dir, opt.step.matchTolerance, aVsB); + } + statsB.seconds = std::chrono::duration(std::chrono::steady_clock::now() - t0).count(); + repJson["modeB"] = robustnessToJson(statsB); + repJson["modeB"]["volumeChordCm3"] = chordVolume(raster, insideByAxisB); + if (oracle.has) { + repJson["modeB"]["vsOracle"] = comparisonToJson(vsOracleB); + } + repJson["modeAvsB"] = comparisonToJson(aVsB); + std::printf( + " (b) navigator: %lld rays, %lld crossings, %.4f s | zero=%lld nonAdv=%lld " + "cap=%lld unterm=%lld odd=%lld dup=%lld noTransition=%lld outsideWorld=%lld\n", + statsB.rays, statsB.crossings, statsB.seconds, statsB.zeroLengthSteps, + statsB.nonAdvancingSteps, statsB.iterationCapHits, statsB.unterminated, + statsB.oddCrossingLists, statsB.duplicateCrossings, + statsB.boundaryWithoutTransition, statsB.originOutsideWorld); + if (oracle.has) { + std::printf( + " vs OCCT : %lld/%lld rays identical, LOST=%lld extra=%lld " + "displaced=%lld worst dt=%.3g cm\n", + vsOracleB.raysIdentical, vsOracleB.rays, vsOracleB.missing, vsOracleB.extra, + vsOracleB.displaced, vsOracleB.worstDeltaT); + } + std::printf( + " (a)vs(b): %lld/%lld rays identical, LOST=%lld extra=%lld " + "displaced=%lld worst dt=%.3g cm\n", + aVsB.raysIdentical, aVsB.rays, aVsB.missing, aVsB.extra, aVsB.displaced, + aVsB.worstDeltaT); + std::printf(" volume : chord integral %.8g cm^3\n", + repJson["modeB"]["volumeChordCm3"].get()); + } + + repsJson.push_back(std::move(repJson)); + delete manager; // frees the shape, the world and the navigator with it + gGeoManager = nullptr; + } + + partJson["raster"] = {{"n", raster.n}, + {"rays", raster.rays.size()}, + {"cellArea", raster.cellArea}, + {"transverseMargin", raster.transverseMargin}, + {"windowExcess", raster.windowExcess}, + {"windowMin", {raster.windowMin[0], raster.windowMin[1], raster.windowMin[2]}}, + {"windowMax", {raster.windowMax[0], raster.windowMax[1], raster.windowMax[2]}}}; + if (oracle.has) { + // Three volume numbers, and they answer three different questions. `volumeChordCm3` is + // OCCT's OWN chord integral over these same rays, so comparing a candidate against it is + // immune to the raster's own error; `capacity` is OCCT's exact volume, so + // (oracle chord - capacity) IS the raster's achieved precision, measured at this density; + // and each representation's `capacity` is the number the sample gate already scores. + partJson["oracle"] = {{"tolerance", oracle.tolerance}, + {"capacity", oracle.capacity}, + {"volumeChordCm3", oracle.volumeChord}, + {"chordVsExactRelative", + oracle.capacity != 0. + ? (oracle.volumeChord - oracle.capacity) / oracle.capacity + : 0.}, + {"ambiguousRays", oracle.ambiguousRays}, + {"valid", oracle.valid}}; + std::printf( + " raster precision: OCCT chord integral %.8g vs OCCT exact %.8g " + "-> %.3e relative (N=%d, %zu rays)\n", + oracle.volumeChord, oracle.capacity, + partJson["oracle"]["chordVsExactRelative"].get(), raster.n, + raster.rays.size()); + } + partJson["representations"] = std::move(repsJson); + report.push_back(std::move(partJson)); + } + + if (!opt.jsonOut.empty()) { + std::ofstream out(opt.jsonOut); + out << report.dump(1); + std::printf("\nreport: %s\n", opt.jsonOut.c_str()); + } + return 0; +} diff --git a/Detectors/CADSupport/test/testBVHAssembly.cxx b/Detectors/CADSupport/test/testBVHAssembly.cxx new file mode 100644 index 0000000000000..15fc44163517e --- /dev/null +++ b/Detectors/CADSupport/test/testBVHAssembly.cxx @@ -0,0 +1,707 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#define BOOST_TEST_MODULE Test O2BVHAssembly class +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include "CADSupport/O2BVHAssembly.h" + +#include "TFile.h" +#include "TGeoBBox.h" +#include "TGeoManager.h" +#include "TGeoMaterial.h" +#include "TGeoMatrix.h" +#include "TGeoMedium.h" +#include "TGeoNode.h" +#include "TGeoShapeAssembly.h" +#include "TGeoVolume.h" + +#include +#include +#include +#include +#include + +namespace +{ +using o2::cad::O2BVHAssembly; + +/// A small deterministic generator, so a failing case can be reproduced from its seed alone. +class Rng +{ + public: + explicit Rng(unsigned long long seed) : mState(seed) {} + double uniform(double low, double high) + { + mState = mState * 6364136223846793005ULL + 1442695040888963407ULL; + const double unit = static_cast((mState >> 11) & ((1ULL << 53) - 1)) / static_cast(1ULL << 53); + return low + unit * (high - low); + } + void direction(double* dir) + { + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + } + + private: + unsigned long long mState; +}; + +TGeoMedium* vacuum() +{ + auto* material = new TGeoMaterial("Vacuum", 0., 0., 0.); + return new TGeoMedium("Vacuum", 1, material); +} + +/// A fresh geometry holding one assembly of \a count^3 unit boxes on a \a pitch grid, inside a +/// world large enough that every query point can be placed by hand. +struct Grid { + TGeoManager* manager = nullptr; + TGeoVolume* world = nullptr; + TGeoVolumeAssembly* assembly = nullptr; + int count = 0; + double pitch = 0.; + double halfBox = 0.; +}; + +Grid makeGrid(const char* name, int count, double pitch, double halfBox) +{ + Grid grid; + grid.manager = new TGeoManager(name, name); + grid.count = count; + grid.pitch = pitch; + grid.halfBox = halfBox; + auto* medium = vacuum(); + const double extent = 4. * count * pitch; + grid.world = grid.manager->MakeBox("WORLD", medium, extent, extent, extent); + grid.assembly = new TGeoVolumeAssembly("GRID"); + int copy = 0; + for (int ix = 0; ix < count; ++ix) { + for (int iy = 0; iy < count; ++iy) { + for (int iz = 0; iz < count; ++iz) { + auto* box = grid.manager->MakeBox(Form("cell_%d", copy), medium, halfBox, halfBox, halfBox); + grid.assembly->AddNode(box, copy, + new TGeoTranslation(pitch * (ix - 0.5 * (count - 1)), + pitch * (iy - 0.5 * (count - 1)), + pitch * (iz - 0.5 * (count - 1)))); + ++copy; + } + } + } + grid.world->AddNode(grid.assembly, 1, new TGeoTranslation(0., 0., 0.)); + grid.manager->SetTopVolume(grid.world); + return grid; +} + +/// The extent a Grid's daughters occupy, half-width per axis. +double gridReach(const Grid& grid) +{ + return 0.5 * grid.pitch * (grid.count - 1) + grid.halfBox; +} + +/// Clear the per-thread daughter indices through which the assembly queries report their daughter. +void clearNodeIndices(TGeoVolumeAssembly* volume) +{ + volume->SetCurrentNodeIndex(-1); + volume->SetNextNodeIndex(-1); +} +} // namespace + +// --------------------------------------------------------------------------------------------- +// Construction +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(BuildsOnePrimitivePerDaughter) +{ + Grid grid = makeGrid("build_grid", 5, 3., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), 125); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), grid.assembly->GetNdaughters()); + BOOST_CHECK_GT(shape->GetBVHMemory(), 0u); +} + +BOOST_AUTO_TEST_CASE(BoundingBoxMatchesRoot) +{ + Grid grid = makeGrid("bbox_grid", 4, 3., 1.); + auto* rootShape = static_cast(grid.assembly->GetShape()); + rootShape->ComputeBBox(); + auto* shape = new O2BVHAssembly(grid.assembly); + const auto* ours = static_cast(shape); + const auto* theirs = static_cast(rootShape); + BOOST_CHECK_EQUAL(ours->GetDX(), theirs->GetDX()); + BOOST_CHECK_EQUAL(ours->GetDY(), theirs->GetDY()); + BOOST_CHECK_EQUAL(ours->GetDZ(), theirs->GetDZ()); + for (int axis = 0; axis < 3; ++axis) { + BOOST_CHECK_EQUAL(ours->GetOrigin()[axis], theirs->GetOrigin()[axis]); + } +} + +BOOST_AUTO_TEST_CASE(EmptyAssemblyAnswersNothing) +{ + auto* manager = new TGeoManager("empty_asm", "empty_asm"); + auto* medium = vacuum(); + auto* world = manager->MakeBox("WORLD", medium, 10., 10., 10.); + auto* assembly = new TGeoVolumeAssembly("EMPTY"); + world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.)); + manager->SetTopVolume(world); + auto* shape = new O2BVHAssembly(assembly); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), 0); + const double point[3] = {0., 0., 0.}; + const double direction[3] = {1., 0., 0.}; + BOOST_CHECK(!shape->Contains(point)); + BOOST_CHECK_EQUAL(shape->DistFromOutside(point, direction, 3, TGeoShape::Big()), TGeoShape::Big()); + BOOST_CHECK_EQUAL(shape->Safety(point, kFALSE), TGeoShape::Big()); +} + +// --------------------------------------------------------------------------------------------- +// BVH == Loop, bit for bit +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(ContainsMatchesLoopOnAGrid) +{ + Grid grid = makeGrid("contains_grid", 6, 3., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double reach = 1.3 * gridReach(grid); + Rng rng(20260823); + int inside = 0; + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + clearNodeIndices(grid.assembly); + const bool fromBVH = shape->Contains(point); + const int bvhNode = grid.assembly->GetCurrentNodeIndex(); + clearNodeIndices(grid.assembly); + const bool fromLoop = shape->Contains_Loop(point); + const int loopNode = grid.assembly->GetCurrentNodeIndex(); + BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); + BOOST_REQUIRE_EQUAL(bvhNode, loopNode); + inside += fromBVH ? 1 : 0; + } + // the corpus has to actually exercise both verdicts + BOOST_CHECK_GT(inside, 100); + BOOST_CHECK_LT(inside, 19900); +} + +BOOST_AUTO_TEST_CASE(DistFromOutsideMatchesLoopOnAGrid) +{ + Grid grid = makeGrid("dist_grid", 6, 3., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double start = 3. * gridReach(grid); + Rng rng(777); + int hits = 0; + for (int trial = 0; trial < 5000; ++trial) { + double direction[3]; + rng.direction(direction); + const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]}; + // aim back through a random point in the grid volume + const double target[3] = {rng.uniform(-gridReach(grid), gridReach(grid)), + rng.uniform(-gridReach(grid), gridReach(grid)), + rng.uniform(-gridReach(grid), gridReach(grid))}; + double aim[3] = {target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]}; + const double norm = std::sqrt(aim[0] * aim[0] + aim[1] * aim[1] + aim[2] * aim[2]); + for (int axis = 0; axis < 3; ++axis) { + aim[axis] /= norm; + } + clearNodeIndices(grid.assembly); + const double fromBVH = shape->DistFromOutside(origin, aim, 3, TGeoShape::Big()); + const int bvhNode = grid.assembly->GetNextNodeIndex(); + clearNodeIndices(grid.assembly); + const double fromLoop = shape->DistFromOutside_Loop(origin, aim, TGeoShape::Big()); + const int loopNode = grid.assembly->GetNextNodeIndex(); + BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); // exact: both minimise the same per-daughter numbers + BOOST_REQUIRE_EQUAL(bvhNode, loopNode); + hits += fromBVH < TGeoShape::Big() ? 1 : 0; + } + BOOST_CHECK_GT(hits, 1000); +} + +BOOST_AUTO_TEST_CASE(DistFromOutsideRespectsTheStepBound) +{ + Grid grid = makeGrid("step_grid", 5, 3., 1.); // odd count, so a cell sits on the axis + auto* shape = new O2BVHAssembly(grid.assembly); + const double origin[3] = {-50., 0., 0.}; + const double direction[3] = {1., 0., 0.}; + const double unbounded = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big()); + BOOST_REQUIRE_LT(unbounded, TGeoShape::Big()); + // a bound just short of the crossing must hide it, one just past must not + BOOST_CHECK_EQUAL(shape->DistFromOutside(origin, direction, 3, unbounded * 0.5), TGeoShape::Big()); + BOOST_CHECK_EQUAL(shape->DistFromOutside(origin, direction, 3, unbounded * 1.5), unbounded); + BOOST_CHECK_EQUAL(shape->DistFromOutside_Loop(origin, direction, unbounded * 0.5), TGeoShape::Big()); +} + +BOOST_AUTO_TEST_CASE(SafetyMatchesLoopOnAGrid) +{ + Grid grid = makeGrid("safety_grid", 6, 3., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double reach = 1.5 * gridReach(grid); + Rng rng(4242); + int positive = 0; + for (int trial = 0; trial < 4000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + const double fromBVH = shape->Safety(point, kFALSE); + const double fromLoop = shape->Safety_Loop(point, kFALSE); + BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); // exact: the traversal prunes only on a lower bound + positive += fromBVH > 0. ? 1 : 0; + } + BOOST_CHECK_GT(positive, 100); +} + +// --------------------------------------------------------------------------------------------- +// Agreement with ROOT +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(ContainsAgreesWithRootOnAClosedGeometry) +{ + Grid grid = makeGrid("root_contains", 6, 3., 1.); + grid.manager->CloseGeometry(); + BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr); // ROOT's accelerated path, not the linear one + auto* rootShape = static_cast(grid.assembly->GetShape()); + auto* shape = new O2BVHAssembly(grid.assembly); + const double reach = 1.3 * gridReach(grid); + Rng rng(31337); + for (int trial = 0; trial < 10000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + const bool fromRoot = rootShape->Contains(point); + const int rootNode = grid.assembly->GetCurrentNodeIndex(); + const bool fromBVH = shape->Contains(point); + BOOST_REQUIRE_EQUAL(fromRoot, fromBVH); + if (fromBVH) { + BOOST_REQUIRE_EQUAL(rootNode, grid.assembly->GetCurrentNodeIndex()); // the same daughter, not just a daughter + } + } +} + +/// ROOT's TGeoShapeAssembly::Safety prunes daughters on the *Euclidean* gap to their bounding +/// boxes while TGeoBBox::Safety answers the *axis-max* gap, which is smaller -- so ROOT discards +/// daughters that would have answered less and returns more than the minimum over its own +/// daughters. This class prunes on the axis-max gap and returns the minimum. The requirement is +/// therefore one-sided: never more than ROOT, and always exactly the loop. +BOOST_AUTO_TEST_CASE(SafetyIsNeverLargerThanRoot) +{ + Grid grid = makeGrid("root_safety", 5, 3., 1.); + grid.manager->CloseGeometry(); + auto* rootShape = static_cast(grid.assembly->GetShape()); + auto* shape = new O2BVHAssembly(grid.assembly); + const double reach = 1.5 * gridReach(grid); + Rng rng(99); + int rootTooLarge = 0; + for (int trial = 0; trial < 2000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + const double ours = shape->Safety(point, kFALSE); + const double theirs = rootShape->Safety(point, kFALSE); + BOOST_REQUIRE_EQUAL(ours, shape->Safety_Loop(point, kFALSE)); + BOOST_REQUIRE_LE(ours, theirs); + rootTooLarge += theirs > ours ? 1 : 0; + } + BOOST_TEST_MESSAGE("ROOT returned more than the daughter minimum on " << rootTooLarge << " of 2000 points"); +} + +/// ROOT's DistFromOutside gives up on a point outside the assembly bounding box when the volume is +/// voxelized. This pins the *direction* of the disagreement: +/// ROOT is allowed to be right or to return Big(), never to return a different finite answer. It +/// keeps passing if ROOT is fixed upstream. +BOOST_AUTO_TEST_CASE(DistFromOutsideIsNeverWorseThanRoot) +{ + Grid grid = makeGrid("root_dist", 6, 3., 1.); + grid.manager->CloseGeometry(); + BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr); + auto* rootShape = static_cast(grid.assembly->GetShape()); + auto* shape = new O2BVHAssembly(grid.assembly); + const double start = 3. * gridReach(grid); + Rng rng(2024); + int weFound = 0; + int rootGaveUp = 0; + for (int trial = 0; trial < 2000; ++trial) { + double direction[3]; + rng.direction(direction); + const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]}; + const double target[3] = {rng.uniform(-gridReach(grid), gridReach(grid)), + rng.uniform(-gridReach(grid), gridReach(grid)), + rng.uniform(-gridReach(grid), gridReach(grid))}; + double aim[3] = {target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]}; + const double norm = std::sqrt(aim[0] * aim[0] + aim[1] * aim[1] + aim[2] * aim[2]); + for (int axis = 0; axis < 3; ++axis) { + aim[axis] /= norm; + } + const double ours = shape->DistFromOutside(origin, aim, 3, TGeoShape::Big()); + const double theirs = rootShape->DistFromOutside(origin, aim, 3, TGeoShape::Big()); + BOOST_REQUIRE_EQUAL(ours, shape->DistFromOutside_Loop(origin, aim, TGeoShape::Big())); + if (theirs < TGeoShape::Big()) { + BOOST_REQUIRE_EQUAL(theirs, ours); + } else { + ++rootGaveUp; + } + weFound += ours < TGeoShape::Big() ? 1 : 0; + } + BOOST_CHECK_GT(weFound, 500); + BOOST_TEST_MESSAGE("ROOT returned Big() on " << rootGaveUp << " of 2000 rays this class answered"); +} + +// --------------------------------------------------------------------------------------------- +// Overlaps, nesting, rotations +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(OverlappingDaughtersResolveToTheLowestIndex) +{ + auto* manager = new TGeoManager("overlap_asm", "overlap_asm"); + auto* medium = vacuum(); + auto* world = manager->MakeBox("WORLD", medium, 50., 50., 50.); + auto* assembly = new TGeoVolumeAssembly("OVERLAP"); + // five boxes each shifted by half their width: every interior point sits in two of them + for (int index = 0; index < 5; ++index) { + auto* box = manager->MakeBox(Form("ov_%d", index), medium, 2., 2., 2.); + assembly->AddNode(box, index, new TGeoTranslation(2. * index, 0., 0.)); + } + world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.)); + manager->SetTopVolume(world); + auto* shape = new O2BVHAssembly(assembly); + Rng rng(5); + int overlaps = 0; + for (int trial = 0; trial < 5000; ++trial) { + const double point[3] = {rng.uniform(-4., 12.), rng.uniform(-3., 3.), rng.uniform(-3., 3.)}; + clearNodeIndices(assembly); + const bool fromBVH = shape->Contains(point); + const int bvhNode = assembly->GetCurrentNodeIndex(); + clearNodeIndices(assembly); + const bool fromLoop = shape->Contains_Loop(point); + const int loopNode = assembly->GetCurrentNodeIndex(); + BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); + BOOST_REQUIRE_EQUAL(bvhNode, loopNode); + if (fromBVH) { + int count = 0; + double local[3]; + for (int index = 0; index < assembly->GetNdaughters(); ++index) { + assembly->GetNode(index)->MasterToLocal(point, local); + count += assembly->GetNode(index)->GetVolume()->GetShape()->Contains(local) ? 1 : 0; + } + overlaps += count > 1 ? 1 : 0; + } + } + BOOST_CHECK_GT(overlaps, 100); // the corpus really does have shared points +} + +BOOST_AUTO_TEST_CASE(NestedAssembliesAgreeWithTheLoop) +{ + auto* manager = new TGeoManager("nested_asm", "nested_asm"); + auto* medium = vacuum(); + auto* world = manager->MakeBox("WORLD", medium, 100., 100., 100.); + auto* outer = new TGeoVolumeAssembly("OUTER"); + for (int block = 0; block < 6; ++block) { + auto* inner = new TGeoVolumeAssembly(Form("INNER_%d", block)); + for (int cell = 0; cell < 8; ++cell) { + auto* box = manager->MakeBox(Form("n_%d_%d", block, cell), medium, 1., 1., 1.); + inner->AddNode(box, cell, new TGeoTranslation(2.5 * cell, 0., 0.)); + } + auto* rotation = new TGeoRotation(Form("rot_%d", block), 13. * block, 7. * block, 5. * block); + outer->AddNode(inner, block, new TGeoCombiTrans(0., 6. * block, 0., rotation)); + } + world->AddNode(outer, 1, new TGeoTranslation(0., 0., 0.)); + manager->SetTopVolume(world); + auto* shape = new O2BVHAssembly(outer); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), 6); + Rng rng(606); + int inside = 0; + int hits = 0; + for (int trial = 0; trial < 4000; ++trial) { + const double point[3] = {rng.uniform(-5., 25.), rng.uniform(-5., 35.), rng.uniform(-5., 5.)}; + clearNodeIndices(outer); + const bool fromBVH = shape->Contains(point); + const int bvhNode = outer->GetCurrentNodeIndex(); + clearNodeIndices(outer); + const bool fromLoop = shape->Contains_Loop(point); + BOOST_REQUIRE_EQUAL(fromBVH, fromLoop); + BOOST_REQUIRE_EQUAL(bvhNode, outer->GetCurrentNodeIndex()); + inside += fromBVH ? 1 : 0; + + double direction[3]; + rng.direction(direction); + const double origin[3] = {point[0] - 200. * direction[0], point[1] - 200. * direction[1], + point[2] - 200. * direction[2]}; + const double distanceBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big()); + const double distanceLoop = shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big()); + BOOST_REQUIRE_EQUAL(distanceBVH, distanceLoop); + hits += distanceBVH < TGeoShape::Big() ? 1 : 0; + + BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE)); + } + BOOST_CHECK_GT(inside, 50); + BOOST_CHECK_GT(hits, 500); +} + +BOOST_AUTO_TEST_CASE(RotatedDaughtersAgreeWithTheLoop) +{ + auto* manager = new TGeoManager("rotated_asm", "rotated_asm"); + auto* medium = vacuum(); + auto* world = manager->MakeBox("WORLD", medium, 100., 100., 100.); + auto* assembly = new TGeoVolumeAssembly("ROTATED"); + for (int index = 0; index < 40; ++index) { + auto* box = manager->MakeBox(Form("r_%d", index), medium, 3., 0.5, 2.); + auto* rotation = new TGeoRotation(Form("rr_%d", index), 9. * index, 4. * index, 17. * index); + assembly->AddNode(box, index, + new TGeoCombiTrans(8. * std::cos(0.31 * index), 8. * std::sin(0.31 * index), 0.7 * index - 14., + rotation)); + } + world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.)); + manager->SetTopVolume(world); + auto* shape = new O2BVHAssembly(assembly); + Rng rng(818); + for (int trial = 0; trial < 4000; ++trial) { + const double point[3] = {rng.uniform(-15., 15.), rng.uniform(-15., 15.), rng.uniform(-20., 20.)}; + BOOST_REQUIRE_EQUAL(shape->Contains(point), shape->Contains_Loop(point)); + BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE)); + double direction[3]; + rng.direction(direction); + BOOST_REQUIRE_EQUAL(shape->DistFromOutside(point, direction, 3, TGeoShape::Big()), + shape->DistFromOutside_Loop(point, direction, TGeoShape::Big())); + } +} + +// --------------------------------------------------------------------------------------------- +// Edge cases the tolerance discipline exists for +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(PointsOnSharedFacesAgreeWithTheLoop) +{ + // touching cells: pitch equals the box width, so consecutive cells share a face exactly + Grid grid = makeGrid("faces_grid", 5, 2., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double first = -0.5 * grid.pitch * (grid.count - 1); + int checked = 0; + for (int ix = 0; ix < grid.count; ++ix) { + for (int iy = 0; iy < grid.count; ++iy) { + for (int iz = 0; iz < grid.count; ++iz) { + // the +x face of cell (ix,iy,iz), which is the -x face of its neighbour + const double point[3] = {first + grid.pitch * ix + grid.halfBox, first + grid.pitch * iy, + first + grid.pitch * iz}; + clearNodeIndices(grid.assembly); + const bool fromBVH = shape->Contains(point); + const int bvhNode = grid.assembly->GetCurrentNodeIndex(); + clearNodeIndices(grid.assembly); + BOOST_REQUIRE_EQUAL(fromBVH, shape->Contains_Loop(point)); + BOOST_REQUIRE_EQUAL(bvhNode, grid.assembly->GetCurrentNodeIndex()); + BOOST_REQUIRE_EQUAL(shape->Safety(point, kFALSE), shape->Safety_Loop(point, kFALSE)); + ++checked; + } + } + } + BOOST_CHECK_EQUAL(checked, 125); +} + +BOOST_AUTO_TEST_CASE(RaysAlongASeamAgreeWithTheLoop) +{ + Grid grid = makeGrid("seam_grid", 5, 2., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double first = -0.5 * grid.pitch * (grid.count - 1); + const double start = 4. * gridReach(grid); + int checked = 0; + for (int iy = 0; iy < grid.count; ++iy) { + for (int iz = 0; iz < grid.count; ++iz) { + for (int offset = -1; offset <= 1; ++offset) { + // a ray running exactly along the plane where two rows of cells touch + const double y = first + grid.pitch * iy + offset * grid.halfBox; + const double origin[3] = {-start, y, first + grid.pitch * iz}; + const double direction[3] = {1., 0., 0.}; + clearNodeIndices(grid.assembly); + const double fromBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big()); + const int bvhNode = grid.assembly->GetNextNodeIndex(); + clearNodeIndices(grid.assembly); + BOOST_REQUIRE_EQUAL(fromBVH, shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big())); + BOOST_REQUIRE_EQUAL(bvhNode, grid.assembly->GetNextNodeIndex()); + ++checked; + } + } + } + BOOST_CHECK_EQUAL(checked, 75); +} + +BOOST_AUTO_TEST_CASE(RaysAlongTheCoordinateAxesAgreeWithTheLoop) +{ + Grid grid = makeGrid("axis_grid", 5, 3., 1.); + auto* shape = new O2BVHAssembly(grid.assembly); + const double start = 4. * gridReach(grid); + const double first = -0.5 * grid.pitch * (grid.count - 1); + for (int axis = 0; axis < 3; ++axis) { + for (int step = 0; step < grid.count; ++step) { + double origin[3] = {0., 0., 0.}; + double direction[3] = {0., 0., 0.}; + origin[axis] = -start; + direction[axis] = 1.; + origin[(axis + 1) % 3] = first + grid.pitch * step; + const double fromBVH = shape->DistFromOutside(origin, direction, 3, TGeoShape::Big()); + BOOST_REQUIRE_EQUAL(fromBVH, shape->DistFromOutside_Loop(origin, direction, TGeoShape::Big())); + BOOST_REQUIRE_LT(fromBVH, TGeoShape::Big()); + } + } +} + +// --------------------------------------------------------------------------------------------- +// Lifecycle: lazy rebuild, shape swap, navigation, I/O +// --------------------------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(AddingADaughterRebuildsLazily) +{ + auto* manager = new TGeoManager("lazy_asm", "lazy_asm"); + auto* medium = vacuum(); + auto* world = manager->MakeBox("WORLD", medium, 50., 50., 50.); + auto* assembly = new TGeoVolumeAssembly("LAZY"); + auto* firstBox = manager->MakeBox("lz_0", medium, 1., 1., 1.); + assembly->AddNode(firstBox, 0, new TGeoTranslation(0., 0., 0.)); + world->AddNode(assembly, 1, new TGeoTranslation(0., 0., 0.)); + manager->SetTopVolume(world); + + auto* shape = new O2BVHAssembly(assembly); + assembly->SetShape(shape); + const double newPoint[3] = {10., 0., 0.}; + BOOST_CHECK(!shape->Contains(newPoint)); + + auto* secondBox = manager->MakeBox("lz_1", medium, 1., 1., 1.); + assembly->AddNode(secondBox, 1, new TGeoTranslation(10., 0., 0.)); + // AddNode invalidated the base bounding box; the BVH notices the new daughter count by itself + BOOST_CHECK(shape->Contains(newPoint)); + BOOST_CHECK_EQUAL(assembly->GetCurrentNodeIndex(), 1); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), 2); +} + +BOOST_AUTO_TEST_CASE(MakeBVHAssemblySwapsTheShapeAndKeepsTheVoxels) +{ + Grid grid = makeGrid("swap_asm", 5, 3., 1.); + grid.manager->CloseGeometry(); + BOOST_REQUIRE(grid.assembly->GetVoxels() != nullptr); + auto* shape = O2BVHAssembly::MakeBVHAssembly(grid.assembly); + BOOST_REQUIRE(shape != nullptr); + BOOST_CHECK_EQUAL(grid.assembly->GetShape(), static_cast(shape)); + BOOST_CHECK(grid.assembly->IsAssembly()); + BOOST_CHECK(shape->IsAssembly()); + BOOST_CHECK_EQUAL(shape->GetNbuilt(), 125); + // the finder stays by default: TGeoNavigator::SearchNode reads it once it is inside the + // assembly, and dropping it turns point location into a linear walk + BOOST_CHECK(grid.assembly->GetVoxels() != nullptr); + BOOST_CHECK(O2BVHAssembly::MakeBVHAssembly(nullptr) == nullptr); +} + +BOOST_AUTO_TEST_CASE(NavigationFindsTheSameLeafBeforeAndAfterTheSwap) +{ + Grid grid = makeGrid("nav_asm", 5, 3., 1.); + grid.manager->CloseGeometry(); + const double reach = 1.2 * gridReach(grid); + Rng rng(1234); + std::vector points; + std::vector paths; + for (int trial = 0; trial < 3000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + grid.manager->FindNode(point[0], point[1], point[2]); + points.insert(points.end(), {point[0], point[1], point[2]}); + paths.emplace_back(grid.manager->GetPath()); + } + O2BVHAssembly::MakeBVHAssembly(grid.assembly); + int deep = 0; + for (size_t trial = 0; trial < paths.size(); ++trial) { + grid.manager->FindNode(points[3 * trial], points[3 * trial + 1], points[3 * trial + 2]); + BOOST_REQUIRE_EQUAL(paths[trial], std::string(grid.manager->GetPath())); + deep += paths[trial].find("/cell_") != std::string::npos ? 1 : 0; + } + BOOST_CHECK_GT(deep, 100); // the corpus really does reach the leaves through the assembly +} + +BOOST_AUTO_TEST_CASE(TransportCrossesTheSameLeavesAsRoot) +{ + Grid reference = makeGrid("transport_root", 5, 3., 1.); + reference.manager->CloseGeometry(); + const double start = 3. * gridReach(reference); + Rng rng(24680); + std::vector origins; + std::vector directions; + std::vector> rootPaths; + for (int ray = 0; ray < 200; ++ray) { + double direction[3]; + rng.direction(direction); + const double origin[3] = {-start * direction[0], -start * direction[1], -start * direction[2]}; + origins.insert(origins.end(), {origin[0], origin[1], origin[2]}); + directions.insert(directions.end(), {direction[0], direction[1], direction[2]}); + reference.manager->InitTrack(origin, direction); + std::vector path; + int guard = 0; + while (!reference.manager->IsOutside() && guard++ < 500) { + reference.manager->FindNextBoundaryAndStep(1.e10); + path.emplace_back(reference.manager->GetPath()); + } + rootPaths.push_back(path); + } + + O2BVHAssembly::MakeBVHAssembly(reference.assembly); + int crossings = 0; + for (int ray = 0; ray < 200; ++ray) { + reference.manager->InitTrack(&origins[3 * ray], &directions[3 * ray]); + std::vector path; + int guard = 0; + while (!reference.manager->IsOutside() && guard++ < 500) { + reference.manager->FindNextBoundaryAndStep(1.e10); + path.emplace_back(reference.manager->GetPath()); + } + // this class only ever finds *more* than ROOT (section 4 of the stream document), so the + // requirement is that everything ROOT saw is still seen, in order + BOOST_REQUIRE_GE(path.size(), rootPaths[ray].size()); + for (const auto& step : rootPaths[ray]) { + crossings += step.find("/cell_") != std::string::npos ? 1 : 0; + } + if (path.size() == rootPaths[ray].size()) { + BOOST_REQUIRE(path == rootPaths[ray]); + } + } + BOOST_CHECK_GT(crossings, 100); +} + +BOOST_AUTO_TEST_CASE(SurvivesAGeometryRoundTrip) +{ + const std::string file = "testBVHAssembly_roundtrip.root"; + Grid grid = makeGrid("io_asm", 4, 3., 1.); + grid.manager->CloseGeometry(); + O2BVHAssembly::MakeBVHAssembly(grid.assembly); + const double reach = 1.2 * gridReach(grid); + Rng rng(1111); + std::vector points; + std::vector nodes; + auto* shape = static_cast(grid.assembly->GetShape()); + for (int trial = 0; trial < 2000; ++trial) { + const double point[3] = {rng.uniform(-reach, reach), rng.uniform(-reach, reach), rng.uniform(-reach, reach)}; + points.insert(points.end(), {point[0], point[1], point[2]}); + clearNodeIndices(grid.assembly); + shape->Contains(point); + nodes.push_back(grid.assembly->GetCurrentNodeIndex()); + } + grid.manager->Export(file.c_str()); + + auto* reloaded = TGeoManager::Import(file.c_str()); + BOOST_REQUIRE(reloaded != nullptr); + auto* reloadedAssembly = dynamic_cast(reloaded->GetTopVolume()->GetNode(0)->GetVolume()); + BOOST_REQUIRE(reloadedAssembly != nullptr); + auto* reloadedShape = dynamic_cast(reloadedAssembly->GetShape()); + BOOST_REQUIRE(reloadedShape != nullptr); // the shape survived streaming as itself + for (size_t trial = 0; trial < nodes.size(); ++trial) { + clearNodeIndices(reloadedAssembly); + reloadedShape->Contains(&points[3 * trial]); + BOOST_REQUIRE_EQUAL(nodes[trial], reloadedAssembly->GetCurrentNodeIndex()); + } + BOOST_CHECK_EQUAL(reloadedShape->GetNbuilt(), 64); // rebuilt lazily on the first query + std::error_code ignored; + std::filesystem::remove(file, ignored); +} diff --git a/Detectors/CADSupport/test/testBVHSurfaceSolid.cxx b/Detectors/CADSupport/test/testBVHSurfaceSolid.cxx new file mode 100644 index 0000000000000..589aea8cc5aeb --- /dev/null +++ b/Detectors/CADSupport/test/testBVHSurfaceSolid.cxx @@ -0,0 +1,6961 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-07 + +#define BOOST_TEST_MODULE Test O2BVHSurfaceSolid class +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include "CADSupport/O2BVHSurfaceSolid.h" +#include "CADSupport/O2SurfaceSolidIO.h" +#include "CADSupport/O2SolidHarness.h" + +#include "../src/BoundedSurface.h" + +#include "TFile.h" +#include "TGeoBBox.h" +#include "TGeoBoolNode.h" +#include "TGeoCompositeShape.h" +#include "TGeoCone.h" +#include "TGeoManager.h" +#include "TGeoMaterial.h" +#include "TGeoMatrix.h" +#include "TGeoMedium.h" +#include "TGeoNode.h" +#include "TGeoShape.h" +#include "TGeoSphere.h" +#include "TGeoTorus.h" +#include "TGeoTube.h" +#include "TGeoVolume.h" +#include "TMath.h" +#include "TNamed.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ +using SurfaceSolid = o2::cad::O2BVHSurfaceSolid; +using Point2D = SurfaceSolid::Point2D; +using Point3D = SurfaceSolid::Point3D; +namespace surf = o2::cad::surface; + +std::vector rectangleWire(double extentU, double extentV) +{ + return {{0., 0.}, {extentU, 0.}, {extentU, extentV}, {0., extentV}}; +} + +using BoundaryCurve = SurfaceSolid::PlanarBoundaryCurve; + +// A full-circle boundary wire centred at (0,0) as a single +/-2pi arc (clockwise for holes). +std::vector circleWire(double radius, bool clockwise = false) +{ + return {BoundaryCurve::makeArc({0., 0.}, radius, 0., clockwise ? -surf::kTwoPi : surf::kTwoPi)}; +} + +// A rectangular trim loop in a quadric's (u, v) parametric domain, as four line boundary curves +// (u = phi, v = height or theta). Wound counter-clockwise; the kernel reorients as needed. +std::vector paramRectWire(double uMin, double uMax, double vMin, double vMax) +{ + return {BoundaryCurve::makeLine({uMin, vMin}, {uMax, vMin}), BoundaryCurve::makeLine({uMax, vMin}, {uMax, vMax}), + BoundaryCurve::makeLine({uMax, vMax}, {uMin, vMax}), BoundaryCurve::makeLine({uMin, vMax}, {uMin, vMin})}; +} + +// Same rectangle as paramRectWire but as internal Curve2D segments, for direct kernel-level tests. +std::vector paramRectWireCurves(double uMin, double uMax, double vMin, double vMax) +{ + return {surf::Curve2D::makeLine({uMin, vMin}, {uMax, vMin}), surf::Curve2D::makeLine({uMax, vMin}, {uMax, vMax}), + surf::Curve2D::makeLine({uMax, vMax}, {uMin, vMax}), surf::Curve2D::makeLine({uMin, vMax}, {uMin, vMin})}; +} + +// Add a planar disk (or annulus when holeRadius > 0) via the general curved-planar API, +// replacing the retired AddPlanarDiskSurface convenience. +bool addDiskSurface(SurfaceSolid& solid, const Point3D& center, const Point3D& axisU, const Point3D& axisV, + double radius, double holeRadius = 0.) +{ + std::vector> inners; + if (holeRadius > 0.) { + inners.push_back(circleWire(holeRadius, true)); // clockwise hole: no reorientation needed + } + return solid.AddCurvedPlanarSurface(center, axisU, axisV, circleWire(radius), inners); +} + +// Local frame (origin + parametric axes + rectangle extents) of a box face by index +// (0:+x 1:-x 2:+y 3:-y 4:+z 5:-z), for a box centred at the origin. +struct FaceFrame { + Point3D origin; + Point3D axisU; + Point3D axisV; + double extentU; + double extentV; +}; + +FaceFrame boxFaceFrame(int faceIndex, double halfX, double halfY, double halfZ) +{ + switch (faceIndex) { + case 0: + return {{halfX, -halfY, -halfZ}, {0., 1., 0.}, {0., 0., 1.}, 2. * halfY, 2. * halfZ}; + case 1: + return {{-halfX, -halfY, -halfZ}, {0., 0., 1.}, {0., 1., 0.}, 2. * halfZ, 2. * halfY}; + case 2: + return {{-halfX, halfY, -halfZ}, {0., 0., 1.}, {1., 0., 0.}, 2. * halfZ, 2. * halfX}; + case 3: + return {{-halfX, -halfY, -halfZ}, {1., 0., 0.}, {0., 0., 1.}, 2. * halfX, 2. * halfZ}; + case 4: + return {{-halfX, -halfY, halfZ}, {1., 0., 0.}, {0., 1., 0.}, 2. * halfX, 2. * halfY}; + default: + return {{-halfX, -halfY, -halfZ}, {0., 1., 0.}, {1., 0., 0.}, 2. * halfY, 2. * halfX}; + } +} + +// Add a single box face by index of a box centred at "center". When "reversed" is set the +// face's parametric axes are swapped, which flips the outward normal inward without changing +// the covered rectangle - used to build an orientation-inconsistent fixture. +bool addBoxFace(SurfaceSolid& solid, int faceIndex, double halfX, double halfY, double halfZ, bool reversed = false, + const Point3D& center = {0., 0., 0.}) +{ + FaceFrame frame = boxFaceFrame(faceIndex, halfX, halfY, halfZ); + if (reversed) { + std::swap(frame.axisU, frame.axisV); + std::swap(frame.extentU, frame.extentV); + } + for (int dimension = 0; dimension < 3; ++dimension) { + frame.origin[dimension] += center[dimension]; + } + return solid.AddPlanarSurface(frame.origin, frame.axisU, frame.axisV, rectangleWire(frame.extentU, frame.extentV)); +} + +void addBoxSurfaces(SurfaceSolid& solid, double halfX, double halfY, double halfZ, + const Point3D& center = {0., 0., 0.}) +{ + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(solid, faceIndex, halfX, halfY, halfZ, false, center)); + } +} + +surf::SurfaceWire makeWire(const std::vector& vertices, surf::WireRole role, surf::WireStatus& status) +{ + surf::SurfaceWire wire; + wire.initialize(vertices, role, status); + return wire; +} + +void checkClose(double value, double reference, double tolerance = 1.e-9) +{ + BOOST_CHECK_SMALL(value - reference, tolerance); +} + +std::array unitDirection(double x, double y, double z) +{ + const double length = std::sqrt(x * x + y * y + z * z); + return {x / length, y / length, z / length}; +} + +// Compare Contains against a reference ROOT shape on a regular grid. The fractional offsets keep +// grid points away from exact shape boundaries, where inside/outside conventions may differ. +void compareContainsGrid(const SurfaceSolid& solid, const TGeoShape& reference, double extent, int samples) +{ + for (int stepX = 0; stepX < samples; ++stepX) { + for (int stepY = 0; stepY < samples; ++stepY) { + for (int stepZ = 0; stepZ < samples; ++stepZ) { + const double point[3] = {-extent + 2. * extent * (stepX + 0.517) / samples, + -extent + 2. * extent * (stepY + 0.263) / samples, + -extent + 2. * extent * (stepZ + 0.741) / samples}; + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(solid.Contains(point), reference.Contains(point)); + } + } + } + } +} + +// Compare the direction-appropriate distance function against a reference ROOT shape. +void compareDistance(const SurfaceSolid& solid, const TGeoShape& reference, const std::array& point, + const std::array& direction, double tolerance = 1.e-9) +{ + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ") direction = (" + << direction[0] << ", " << direction[1] << ", " << direction[2] << ")") + { + const bool inside = reference.Contains(point.data()); + BOOST_CHECK_EQUAL(solid.Contains(point.data()), inside); + if (inside) { + checkClose(solid.DistFromInside(point.data(), direction.data(), 3), + reference.DistFromInside(point.data(), direction.data(), 3), tolerance); + } else { + checkClose(solid.DistFromOutside(point.data(), direction.data(), 3), + reference.DistFromOutside(point.data(), direction.data(), 3), tolerance); + } + } +} + +/// @name Closed fixtures for the navigation sweeps +/// +/// The distance tests exercise the same solids from several directions rather than one shape per +/// case, so the fixtures are built once here. Each is closed and therefore BVH-backed. The solid +/// is neither copyable nor movable, hence the unique_ptr. +/// @{ + +std::unique_ptr makeBoxSolid(const char* name, double halfX, double halfY, double halfZ) +{ + auto solid = std::make_unique(name); + addBoxSurfaces(*solid, halfX, halfY, halfZ); + solid->CloseShape(); + return solid; +} + +// innerRadius > 0 gives a hollow tube (an inner wall plus annular caps). +std::unique_ptr makeTubeSolid(const char* name, double innerRadius, double outerRadius, + double halfHeight) +{ + auto solid = std::make_unique(name); + BOOST_REQUIRE(solid->AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, outerRadius, -halfHeight, + halfHeight)); + if (innerRadius > 0.) { + BOOST_REQUIRE(solid->AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, innerRadius, -halfHeight, + halfHeight, 0., surf::kTwoPi, true)); + } + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, outerRadius, innerRadius)); + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, outerRadius, innerRadius)); + solid->CloseShape(); + return solid; +} + +std::unique_ptr makeConeSolid(const char* name, double radiusAtBottom, double radiusAtTop, + double halfHeight) +{ + auto solid = std::make_unique(name); + BOOST_REQUIRE(solid->AddConicalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radiusAtBottom, radiusAtTop, + -halfHeight, halfHeight)); + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radiusAtTop)); + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radiusAtBottom)); + solid->CloseShape(); + return solid; +} + +std::unique_ptr makeSphereSolid(const char* name, double radius) +{ + auto solid = std::make_unique(name); + BOOST_REQUIRE(solid->AddSphericalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius)); + solid->CloseShape(); + return solid; +} + +std::unique_ptr makeTorusSolid(const char* name, double majorRadius, double minorRadius) +{ + auto solid = std::make_unique(name); + BOOST_REQUIRE(solid->AddToroidalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, majorRadius, minorRadius)); + solid->CloseShape(); + return solid; +} + +// Cylinder barrel closed by two hemispherical endcaps; a mixed-quadric solid with no ROOT +// primitive equivalent, so the loop oracle is the only reference it has. +std::unique_ptr makeCapsuleSolid(const char* name, double radius, double halfHeight) +{ + auto solid = std::make_unique(name); + BOOST_REQUIRE(solid->AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight)); + BOOST_REQUIRE(solid->AddSphericalSurface({0., 0., halfHeight}, {0., 0., 1.}, {1., 0., 0.}, radius, 0., + surf::kPi / 2.)); + BOOST_REQUIRE(solid->AddSphericalSurface({0., 0., -halfHeight}, {0., 0., -1.}, {1., 0., 0.}, radius, 0., + surf::kPi / 2.)); + solid->CloseShape(); + return solid; +} + +/// @} + +// Directions probed by the navigation sweeps: the three axes both ways, face and body diagonals, +// and a few skew directions that align with no symmetry of any fixture. +const std::vector>& probeDirections() +{ + static const std::vector> directions{ + {1., 0., 0.}, {-1., 0., 0.}, {0., 1., 0.}, {0., -1., 0.}, {0., 0., 1.}, {0., 0., -1.}, unitDirection(1., 1., 0.), unitDirection(1., 0., 1.), unitDirection(0., 1., 1.), unitDirection(1., 1., 1.), unitDirection(-1., 1., -1.), unitDirection(0.37, -0.82, 0.44), unitDirection(-0.91, 0.13, 0.39), unitDirection(0.21, 0.55, -0.81)}; + return directions; +} + +// A deterministic point grid over the cube of half-side "extent". The fractional offsets are the +// same irrational-looking shifts the Contains sweeps use, which keeps samples off exact symmetry +// planes and shape boundaries. +std::vector> probeGrid(double extent, int samples) +{ + std::vector> points; + points.reserve(static_cast(samples) * samples * samples); + for (int stepX = 0; stepX < samples; ++stepX) { + for (int stepY = 0; stepY < samples; ++stepY) { + for (int stepZ = 0; stepZ < samples; ++stepZ) { + points.push_back({-extent + 2. * extent * (stepX + 0.517) / samples, + -extent + 2. * extent * (stepY + 0.263) / samples, + -extent + 2. * extent * (stepZ + 0.741) / samples}); + } + } + } + return points; +} + +/// The BVH distance queries must return *exactly* what the all-surfaces loop returns. Both run +/// the same analytic kernels on the same patches and take a minimum over the same hit set; they +/// differ only in the order surfaces are visited and in which of them the BVH lets them skip. So +/// any difference at all -- not merely one above a tolerance -- is a traversal or pruning bug, +/// and exact comparison is the sharpest available oracle. Independent of any mesh reference. +/// +/// Ray tmax tightening is an optimization and nothing else, so both settings are checked and must +/// agree with the same loop value. +void checkDistanceAgainstLoop(const SurfaceSolid& solid, const std::array& point, + const std::array& direction, double stepmax = TGeoShape::Big()) +{ + const double loopOutside = solid.DistFromOutside_Loop(point.data(), direction.data(), stepmax); + const double loopInside = solid.DistFromInside_Loop(point.data(), direction.data(), stepmax); + for (const bool pruning : {true, false}) { + SurfaceSolid::SetRayTMaxPruning(pruning); + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ") direction = (" + << direction[0] << ", " << direction[1] << ", " << direction[2] + << ") stepmax = " << stepmax << " pruning = " << pruning) + { + BOOST_CHECK_EQUAL(solid.DistFromOutside(point.data(), direction.data(), 3, stepmax), loopOutside); + BOOST_CHECK_EQUAL(solid.DistFromInside(point.data(), direction.data(), 3, stepmax), loopInside); + } + } + SurfaceSolid::SetRayTMaxPruning(true); +} + +// Sweep every grid point against every probe direction, cross-checking BVH against the loop. +void sweepDistanceAgainstLoop(const SurfaceSolid& solid, double extent, int samples) +{ + for (const auto& point : probeGrid(extent, samples)) { + for (const auto& direction : probeDirections()) { + checkDistanceAgainstLoop(solid, point, direction); + } + } +} + +// Sweep both distance functions against a reference ROOT primitive, using each point in the role +// (inside/outside) the reference itself assigns it. Points closer than "skin" to the reference +// boundary are skipped: there the two shapes may legitimately disagree on which side the point is +// on, and the resulting distances are then answers to different questions. +void sweepDistanceAgainstReference(const SurfaceSolid& solid, const TGeoShape& reference, double extent, int samples, + double tolerance = 1.e-9, double skin = 1.e-6) +{ + for (const auto& point : probeGrid(extent, samples)) { + const bool inside = reference.Contains(point.data()); + if (reference.Safety(point.data(), inside) < skin) { + continue; + } + for (const auto& direction : probeDirections()) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ") direction = (" + << direction[0] << ", " << direction[1] << ", " << direction[2] << ")") + { + if (inside) { + checkClose(solid.DistFromInside(point.data(), direction.data(), 3), + reference.DistFromInside(point.data(), direction.data(), 3), tolerance); + } else { + checkClose(solid.DistFromOutside(point.data(), direction.data(), 3), + reference.DistFromOutside(point.data(), direction.data(), 3), tolerance); + } + } + } + } +} + +} // namespace + +BOOST_AUTO_TEST_CASE(PlanarBoxNavigationMatchesTGeoBBox) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + + SurfaceSolid solid("planarBox"); + addBoxSurfaces(solid, halfX, halfY, halfZ); + solid.CloseShape(); + + TGeoBBox reference("referenceBox", halfX, halfY, halfZ); + + BOOST_CHECK(solid.IsDefined()); + BOOST_CHECK_EQUAL(solid.GetNsurfaces(), 6); + + int meshVertices = 0; + int meshSegments = 0; + int meshPolygons = 0; + solid.GetMeshNumbers(meshVertices, meshSegments, meshPolygons); + BOOST_CHECK_EQUAL(meshVertices, 24); + BOOST_CHECK_EQUAL(meshSegments, 36); + BOOST_CHECK_EQUAL(meshPolygons, 12); + + const std::array, 5> insidePoints{{{0., 0., 0.}, {0.9, 0., 0.}, {1., 0., 0.}, {1., 2., 3.}, {-1., -2., -3.}}}; + for (const auto& point : insidePoints) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK(solid.Contains(point.data())); + BOOST_CHECK(reference.Contains(point.data())); + } + } + + const std::array, 4> outsidePoints{{{1.1, 0., 0.}, {0., 2.1, 0.}, {0., 0., -3.1}, {2., 3., 4.}}}; + for (const auto& point : outsidePoints) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK(!solid.Contains(point.data())); + BOOST_CHECK(!reference.Contains(point.data())); + } + } + + const double fromLeft[3] = {-3., 0., 0.}; + const double toRight[3] = {1., 0., 0.}; + checkClose(solid.DistFromOutside(fromLeft, toRight, 3), reference.DistFromOutside(fromLeft, toRight, 3)); + + const double fromFront[3] = {0., -5., 0.}; + const double toBack[3] = {0., 1., 0.}; + checkClose(solid.DistFromOutside(fromFront, toBack, 3), reference.DistFromOutside(fromFront, toBack, 3)); + + const double fromCenter[3] = {0., 0., 0.}; + const double alongX[3] = {1., 0., 0.}; + const double alongZ[3] = {0., 0., -1.}; + checkClose(solid.DistFromInside(fromCenter, alongX, 3), reference.DistFromInside(fromCenter, alongX, 3)); + checkClose(solid.DistFromInside(fromCenter, alongZ, 3), reference.DistFromInside(fromCenter, alongZ, 3)); + + // safeties against analytic distances (TGeo safeties may be weaker underestimates) + const double outsideSafetyPoint[3] = {2.5, 0., 0.}; + checkClose(solid.Safety(fromCenter, kTRUE), halfX); + checkClose(solid.Safety(outsideSafetyPoint, kFALSE), 2.5 - halfX); + + const double normalPoint[3] = {halfX, 0., 0.}; + double normal[3] = {0., 0., 0.}; + solid.ComputeNormal(normalPoint, alongX, normal); + checkClose(normal[0], 1.); + checkClose(normal[1], 0.); + checkClose(normal[2], 0.); + + checkClose(solid.Capacity(), 8. * halfX * halfY * halfZ); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); +} + +BOOST_AUTO_TEST_CASE(WireValidationAndOrientation) +{ + using surf::WireRole; + using surf::WireStatus; + + // outer square given clockwise (negative area) must be re-oriented to CCW + WireStatus reversedStatus = WireStatus::Valid; + auto reversedOuter = makeWire({{0., 0.}, {0., 1.}, {1., 1.}, {1., 0.}}, WireRole::Outer, reversedStatus); + BOOST_CHECK(reversedStatus == WireStatus::Reversed); + BOOST_CHECK_GT(reversedOuter.signedArea(), 0.); + + // outer square already CCW stays valid + WireStatus outerStatus = WireStatus::Valid; + auto outerWire = makeWire({{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}}, WireRole::Outer, outerStatus); + BOOST_CHECK(outerStatus == WireStatus::Valid); + BOOST_CHECK_GT(outerWire.signedArea(), 0.); + + // inner (hole) wire must end up clockwise (negative area) + WireStatus innerStatus = WireStatus::Valid; + auto innerWire = makeWire({{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}}, WireRole::Inner, innerStatus); + BOOST_CHECK(innerStatus == WireStatus::Reversed); + BOOST_CHECK_LT(innerWire.signedArea(), 0.); + + // degenerate / invalid inputs are rejected with a specific status + surf::SurfaceWire scratch; + WireStatus status = WireStatus::Valid; + BOOST_CHECK(!scratch.initialize({{0., 0.}, {1., 0.}}, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::TooFewVertices); + + BOOST_CHECK(!scratch.initialize({{0., 0.}, {1., 0.}, {2., 0.}}, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::ZeroArea); + + // self-touching (pinched) loop: a non-adjacent vertex repeats + BOOST_CHECK(!scratch.initialize({{0., 0.}, {1., 0.}, {0., 0.}, {1., 1.}}, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::DegenerateVertex); + + // explicit edge list that does not close is flagged as open + const std::vector openEdges{{{0., 0.}, {1., 0.}}, {{1., 0.}, {1., 1.}}, {{1., 1.}, {0.5, 0.5}}}; + BOOST_CHECK(!scratch.initializeFromEdges(openEdges, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::Open); + + // a closed edge list is accepted + const std::vector closedEdges{ + {{0., 0.}, {1., 0.}}, {{1., 0.}, {1., 1.}}, {{1., 1.}, {0., 1.}}, {{0., 1.}, {0., 0.}}}; + BOOST_CHECK(scratch.initializeFromEdges(closedEdges, WireRole::Outer, status)); + + // point classification: inside, outside, and on-edge + BOOST_CHECK(outerWire.classify({0.5, 0.5}) == surf::WireClassification::Inside); + BOOST_CHECK(outerWire.classify({1.5, 0.5}) == surf::WireClassification::Outside); + BOOST_CHECK(outerWire.classify({0.5, 0.}) == surf::WireClassification::Boundary); +} + +namespace o2::cad::surface +{ +/// A trivial bounded surface, a single 3D triangle, to exercise the BoundedSurface interface. +class DummyBoundedSurface final : public BoundedSurface +{ + public: + DummyBoundedSurface(const Vec3& firstVertex, const Vec3& secondVertex, const Vec3& thirdVertex) + : mVertices{firstVertex, secondVertex, thirdVertex} + { + mNormal = normalized(cross(secondVertex - firstVertex, thirdVertex - firstVertex)); + } + + void conservativeBounds(Vec3& lower, Vec3& upper) const override + { + for (const auto& vertex : mVertices) { + lower.xCoord = std::min(lower.xCoord, vertex.xCoord); + lower.yCoord = std::min(lower.yCoord, vertex.yCoord); + lower.zCoord = std::min(lower.zCoord, vertex.zCoord); + upper.xCoord = std::max(upper.xCoord, vertex.xCoord); + upper.yCoord = std::max(upper.yCoord, vertex.yCoord); + upper.zCoord = std::max(upper.zCoord, vertex.zCoord); + } + } + + bool containsPointOnSurface(const Vec3&) const override { return false; } + + void appendIntersections(const Vec3&, const Vec3&, double, double, std::vector&) const override {} + + double distanceSqToPatch(const Vec3& point) const override + { + double bestDistanceSq = std::numeric_limits::infinity(); + for (int vertexIndex = 0; vertexIndex < 3; ++vertexIndex) { + bestDistanceSq = std::min(bestDistanceSq, pointSegmentDistanceSq(point, mVertices[vertexIndex], + mVertices[(vertexIndex + 1) % 3])); + } + return bestDistanceSq; + } + + Vec3 normalAt(const Vec3&) const override { return mNormal; } + + /// A triangle carries no parametric domain, so the form is the identity. + void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override + { + gUU = 1.; + gUV = 0.; + gVV = 1.; + } + + double capacityContribution() const override { return 0.; } + + bool capacityIsExact() const override { return false; } + + void appendDisplayMesh(std::vector& vertices, std::vector>& triangles) const override + { + const int firstVertexIndex = static_cast(vertices.size()); + for (const auto& vertex : mVertices) { + vertices.push_back(vertex); + } + triangles.push_back({firstVertexIndex, firstVertexIndex + 1, firstVertexIndex + 2}); + } + + void appendDirectedEdges(std::vector>& edges) const override + { + for (int vertexIndex = 0; vertexIndex < 3; ++vertexIndex) { + edges.emplace_back(mVertices[vertexIndex], mVertices[(vertexIndex + 1) % 3]); + } + } + + private: + std::array mVertices; + Vec3 mNormal; +}; +} // namespace o2::cad::surface + +BOOST_AUTO_TEST_CASE(DummyBoundedSurfaceInterface) +{ + auto dummy = std::make_unique(surf::Vec3{0., 0., 0.}, surf::Vec3{1., 0., 0.}, + surf::Vec3{0., 1., 0.}); + + surf::Vec3 lower{surf::Vec3{1.e30, 1.e30, 1.e30}}; + surf::Vec3 upper{surf::Vec3{-1.e30, -1.e30, -1.e30}}; + dummy->conservativeBounds(lower, upper); + checkClose(lower.xCoord, 0.); + checkClose(upper.xCoord, 1.); + checkClose(upper.yCoord, 1.); + + const surf::Vec3 normal = dummy->normalAt({0., 0., 0.}); + checkClose(std::abs(normal.zCoord), 1.); + BOOST_CHECK(!dummy->capacityIsExact()); + + std::vector vertices; + std::vector> triangles; + dummy->appendDisplayMesh(vertices, triangles); + BOOST_CHECK_EQUAL(vertices.size(), 3u); + BOOST_CHECK_EQUAL(triangles.size(), 1u); + + // a single open triangle is not a closed manifold + std::vector> surfaces; + surfaces.emplace_back(std::move(dummy)); + const surf::ClosureReport report = surf::validateClosure(surfaces); + BOOST_CHECK(!report.closed); + BOOST_CHECK_EQUAL(report.boundaryEdges, 3); +} + +BOOST_AUTO_TEST_CASE(SolidClosureDetectsMissingAndReversedFaces) +{ + constexpr double halfX = 1.; + constexpr double halfY = 1.5; + constexpr double halfZ = 2.; + + // missing face: only five of the six box faces are added + SurfaceSolid missing("missingFaceBox"); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(missing, faceIndex, halfX, halfY, halfZ)); + } + missing.CloseShape(false); + BOOST_CHECK(!missing.IsClosed()); + + // reversed face: the +x face keeps its geometry but has an inward normal + SurfaceSolid reversed("reversedFaceBox"); + BOOST_REQUIRE(addBoxFace(reversed, 0, halfX, halfY, halfZ, true)); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(reversed, faceIndex, halfX, halfY, halfZ)); + } + reversed.CloseShape(false); + BOOST_CHECK(reversed.IsClosed()); + BOOST_CHECK(!reversed.IsOrientationConsistent()); +} + +// The queryable navigation-reliability state. +// A caller must be able to ask "can I trust this solid's navigation answers" and get a single +// answer, rather than having to notice a printed warning; the state must also survive being +// closed with check==false, since diagnostics and reporting are separate concerns. +BOOST_AUTO_TEST_CASE(NavigationReliabilityIsQueryable) +{ + using Reliability = SurfaceSolid::NavigationReliability; + constexpr double halfX = 1.; + constexpr double halfY = 1.5; + constexpr double halfZ = 2.; + + // before CloseShape there are no diagnostics at all + SurfaceSolid fresh("freshBox"); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(fresh, faceIndex, halfX, halfY, halfZ)); + } + BOOST_CHECK(fresh.GetNavigationReliability() == Reliability::Undetermined); + BOOST_CHECK(!fresh.IsNavigable()); + + fresh.CloseShape(false); + BOOST_CHECK(fresh.GetNavigationReliability() == Reliability::Reliable); + BOOST_CHECK(fresh.IsNavigable()); + BOOST_CHECK_EQUAL(fresh.GetBoundaryEdgeCount(), 0); + BOOST_CHECK_EQUAL(fresh.GetNonManifoldEdgeCount(), 0); + BOOST_CHECK_EQUAL(fresh.GetReversedEdgeCount(), 0); + + // a missing face leaves boundary edges: the gap case that motivates the whole state + SurfaceSolid missing("missingFaceBoxState"); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(missing, faceIndex, halfX, halfY, halfZ)); + } + missing.CloseShape(false); + BOOST_CHECK(missing.GetNavigationReliability() == Reliability::OpenSurfaceSet); + BOOST_CHECK(!missing.IsNavigable()); + BOOST_CHECK(missing.GetBoundaryEdgeCount() > 0); + + // a reversed face is closed but inconsistently oriented + SurfaceSolid reversed("reversedFaceBoxState"); + BOOST_REQUIRE(addBoxFace(reversed, 0, halfX, halfY, halfZ, true)); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(reversed, faceIndex, halfX, halfY, halfZ)); + } + reversed.CloseShape(false); + BOOST_CHECK(reversed.GetNavigationReliability() == Reliability::ReversedFaces); + BOOST_CHECK(!reversed.IsNavigable()); + BOOST_CHECK(reversed.GetReversedEdgeCount() > 0); + + // duplicated faces: every edge is now shared by four faces. Non-manifold outranks the boundary + // and orientation cases because parity is not even order-independent on such input. + SurfaceSolid duplicated("duplicatedFaceBox"); + for (int pass = 0; pass < 2; ++pass) { + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(duplicated, faceIndex, halfX, halfY, halfZ)); + } + } + duplicated.CloseShape(false); + BOOST_CHECK(duplicated.GetNavigationReliability() == Reliability::NonManifold); + BOOST_CHECK(!duplicated.IsNavigable()); + BOOST_CHECK(duplicated.GetNonManifoldEdgeCount() > 0); + + BOOST_CHECK_EQUAL(std::string(SurfaceSolid::GetNavigationReliabilityName(Reliability::Reliable)), "reliable"); + BOOST_CHECK_EQUAL(std::string(SurfaceSolid::GetNavigationReliabilityName(Reliability::OpenSurfaceSet)), + "open-surface-set"); + BOOST_CHECK_EQUAL(std::string(SurfaceSolid::GetNavigationReliabilityName(Reliability::NonManifold)), "non-manifold"); +} + +BOOST_AUTO_TEST_CASE(NumericalConventions) +{ + using surf::WireClassification; + using surf::WireRole; + using surf::WireStatus; + + // near-boundary point classification: a unit square wire, points offset from the bottom edge. + WireStatus status = WireStatus::Valid; + auto square = makeWire({{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}}, WireRole::Outer, status); + BOOST_REQUIRE(status == WireStatus::Valid); + + // within tolerance of an edge -> Boundary, on both sides + BOOST_CHECK(square.classify({0.5, 0.5 * surf::kTolerance}) == WireClassification::Boundary); + BOOST_CHECK(square.classify({0.5, -0.5 * surf::kTolerance}) == WireClassification::Boundary); + // clearly beyond tolerance -> Inside / Outside + BOOST_CHECK(square.classify({0.5, 1.e3 * surf::kTolerance}) == WireClassification::Inside); + BOOST_CHECK(square.classify({0.5, -1.e3 * surf::kTolerance}) == WireClassification::Outside); + + // near-tangent rays against a planar surface in the z = 0 plane. + surf::PlanarBoundedSurface plane; + std::string planeError; + BOOST_REQUIRE(plane.initialize({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, + {{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}}, {}, planeError)); + + const surf::Vec3 origin{0.5, 0.5, 1.}; + std::vector hits; + + // direction almost parallel to the plane (tiny z component) -> grazing miss + const surf::Vec3 grazing = surf::normalized({1., 0., 0.1 * surf::kTolerance}); + plane.appendIntersections(origin, grazing, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // steeper direction -> real intersection at the plane + const surf::Vec3 steep = surf::normalized({0., 0., -1.}); + plane.appendIntersections(origin, steep, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits[0].distance, 1.); + + // a hit rejected when it falls below the minimum ray parameter + hits.clear(); + plane.appendIntersections(origin, steep, 2., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // duplicate-intersection clustering respects kIntersectionTolerance. + BOOST_CHECK(surf::sameIntersection(1., 1. + 0.1 * surf::kIntersectionTolerance)); + BOOST_CHECK(!surf::sameIntersection(1., 1. + 1.e3 * surf::kIntersectionTolerance)); +} + +BOOST_AUTO_TEST_CASE(WireDataModel) +{ + using surf::WireClassification; + using surf::WireRole; + using surf::WireStatus; + + // outer square wire: area, orientation, parametric AABB, and boundary sampling. + WireStatus status = WireStatus::Valid; + auto square = makeWire({{0., 0.}, {2., 0.}, {2., 3.}, {0., 3.}}, WireRole::Outer, status); + BOOST_REQUIRE(status == WireStatus::Valid); + checkClose(square.signedArea(), 6.); + + surf::Vec2 lower{1.e30, 1.e30}; + surf::Vec2 upper{-1.e30, -1.e30}; + square.parametricBounds(lower, upper); + checkClose(lower.uCoord, 0.); + checkClose(lower.vCoord, 0.); + checkClose(upper.uCoord, 2.); + checkClose(upper.vCoord, 3.); + + // the sampled boundary is the closed vertex ring (first vertex repeated at the end). + const auto samples = square.sampledBoundary(); + BOOST_CHECK_EQUAL(samples.size(), square.vertices.size() + 1); + checkClose(samples.front().uCoord, samples.back().uCoord); + checkClose(samples.front().vCoord, samples.back().vCoord); + + // edge distance / projection (closest point) on the bottom edge. + const surf::SurfaceEdge bottom{{0., 0.}, {2., 0.}}; + double parameter = -1.; + const surf::Vec2 projected = bottom.closestPoint({1., 5.}, parameter); + checkClose(projected.uCoord, 1.); + checkClose(projected.vCoord, 0.); + checkClose(parameter, 0.5); + // projection is clamped to the segment endpoints. + bottom.closestPoint({-5., 1.}, parameter); + checkClose(parameter, 0.); + bottom.closestPoint({5., 1.}, parameter); + checkClose(parameter, 1.); + checkClose(std::sqrt(bottom.distanceSq({1., 4.})), 4.); + + // reversed wire: same shape, opposite winding sign, identical parametric AABB. + WireStatus reversedStatus = WireStatus::Valid; + auto reversed = makeWire({{0., 0.}, {0., 3.}, {2., 3.}, {2., 0.}}, WireRole::Outer, reversedStatus); + BOOST_CHECK(reversedStatus == WireStatus::Reversed); + checkClose(reversed.signedArea(), 6.); // normalized back to CCW (positive) + surf::Vec2 reversedLower{1.e30, 1.e30}; + surf::Vec2 reversedUpper{-1.e30, -1.e30}; + reversed.parametricBounds(reversedLower, reversedUpper); + checkClose(reversedUpper.uCoord, 2.); + checkClose(reversedUpper.vCoord, 3.); + + // open wire via an explicit non-closing edge list is rejected. + surf::SurfaceWire scratch; + const std::vector openEdges{{{0., 0.}, {2., 0.}}, {{2., 0.}, {2., 3.}}, {{2., 3.}, {1., 1.}}}; + BOOST_CHECK(!scratch.initializeFromEdges(openEdges, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::Open); + + // point-on-edge classification. + BOOST_CHECK(square.classify({1., 0.}) == WireClassification::Boundary); + BOOST_CHECK(square.classify({1., 1.5}) == WireClassification::Inside); + BOOST_CHECK(square.classify({3., 1.5}) == WireClassification::Outside); + + // square-with-hole: a planar surface with one inner (hole) wire. + surf::PlanarBoundedSurface holedFace; + std::string faceError; + const std::vector outer{{0., 0.}, {4., 0.}, {4., 4.}, {0., 4.}}; + const std::vector> holes{{{1., 1.}, {3., 1.}, {3., 3.}, {1., 3.}}}; + BOOST_REQUIRE(holedFace.initialize({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, outer, holes, faceError)); + + bool boundary = false; + BOOST_CHECK(holedFace.containsLocal({0.5, 0.5}, &boundary)); // in material, outside the hole + BOOST_CHECK(!boundary); + BOOST_CHECK(!holedFace.containsLocal({2., 2.})); // inside the hole -> not on the patch + BOOST_CHECK(holedFace.containsLocal({2., 1.}, &boundary)); // on the hole boundary -> on the patch + BOOST_CHECK(boundary); + + // the trimmed area accounts for the hole (16 - 4). + checkClose(holedFace.area(), 12.); +} + +BOOST_AUTO_TEST_CASE(TrimmedCurveBoundaries) +{ + using surf::Curve2D; + using surf::CurveWire; + using surf::WireClassification; + using surf::WireRole; + using surf::WireStatus; + + // --- line curve: endpoint, tangent, bounds, projection ------------------------------------- + const Curve2D line = Curve2D::makeLine({0., 0.}, {4., 0.}); + checkClose(line.startPoint().uCoord, 0.); + checkClose(line.endPoint().uCoord, 4.); + const surf::Vec2 lineTangent = line.tangentAt(0.5); + checkClose(lineTangent.uCoord, 1.); + checkClose(lineTangent.vCoord, 0.); + + double lineParameter = -1.; + const surf::Vec2 lineProjection = line.closestPoint({1., 5.}, lineParameter); + checkClose(lineProjection.uCoord, 1.); + checkClose(lineProjection.vCoord, 0.); + checkClose(lineParameter, 0.25); + checkClose(std::sqrt(line.distanceSq({1., 5.})), 5.); + + // --- arc curve: endpoint, tangent, exact bounds, projection -------------------------------- + // quarter circle of radius 2 centred at the origin, from angle 0 to pi/2. + const Curve2D quarter = Curve2D::makeArc({0., 0.}, 2., 0., surf::kHalfPi); + checkClose(quarter.startPoint().uCoord, 2.); + checkClose(quarter.startPoint().vCoord, 0.); + checkClose(quarter.endPoint().uCoord, 0.); + checkClose(quarter.endPoint().vCoord, 2.); + // tangent at the start of a CCW arc points in +v. + const surf::Vec2 arcTangent = quarter.tangentAt(0.); + checkClose(arcTangent.uCoord, 0.); + checkClose(arcTangent.vCoord, 1.); + + // the quarter arc's exact bounding box is [0, 2] x [0, 2] (no cardinal extreme inside). + surf::Vec2 arcLower{1.e30, 1.e30}; + surf::Vec2 arcUpper{-1.e30, -1.e30}; + quarter.extendBounds(arcLower, arcUpper); + checkClose(arcLower.uCoord, 0.); + checkClose(arcLower.vCoord, 0.); + checkClose(arcUpper.uCoord, 2.); + checkClose(arcUpper.vCoord, 2.); + + // projection of a far radial point lands on the circle (distance = |d - r|). + double arcParameter = -1.; + const surf::Vec2 arcProjection = quarter.closestPoint({5., 5.}, arcParameter); + checkClose(std::hypot(arcProjection.uCoord, arcProjection.vCoord), 2.); + checkClose(arcParameter, 0.5); + + // a full circle's exact bounding box spans the whole diameter in both axes. + const Curve2D circle = Curve2D::makeCircle({1., -1.}, 3.); + surf::Vec2 circleLower{1.e30, 1.e30}; + surf::Vec2 circleUpper{-1.e30, -1.e30}; + circle.extendBounds(circleLower, circleUpper); + checkClose(circleLower.uCoord, -2.); + checkClose(circleUpper.uCoord, 4.); + checkClose(circleLower.vCoord, -4.); + checkClose(circleUpper.vCoord, 2.); + + // --- disk: one full-circle outer wire ------------------------------------------------------ + WireStatus status = WireStatus::Valid; + CurveWire disk; + BOOST_REQUIRE(disk.initialize({Curve2D::makeCircle({0., 0.}, 2.)}, WireRole::Outer, status)); + BOOST_CHECK(status == WireStatus::Valid); + // exact area of the disk is pi * r^2. + checkClose(disk.signedArea(), surf::kPi * 4., 1.e-9); + BOOST_CHECK(disk.classify({0., 0.}) == WireClassification::Inside); + BOOST_CHECK(disk.classify({1.5, 0.}) == WireClassification::Inside); + BOOST_CHECK(disk.classify({3., 0.}) == WireClassification::Outside); + BOOST_CHECK(disk.classify({0., 3.}) == WireClassification::Outside); + BOOST_CHECK(disk.classify({2., 0.}) == WireClassification::Boundary); + BOOST_CHECK(disk.classify({0., -2.}) == WireClassification::Boundary); + + // a clockwise circle used as an outer wire is re-oriented to counter-clockwise. + WireStatus reversedStatus = WireStatus::Valid; + CurveWire reversedDisk; + BOOST_REQUIRE(reversedDisk.initialize({Curve2D::makeCircle({0., 0.}, 2., true)}, WireRole::Outer, reversedStatus)); + BOOST_CHECK(reversedStatus == WireStatus::Reversed); + checkClose(reversedDisk.signedArea(), surf::kPi * 4., 1.e-9); + + // --- annulus: outer disk (CCW) minus an inner hole wire (CW) -------------------------------- + WireStatus outerStatus = WireStatus::Valid; + WireStatus holeStatus = WireStatus::Valid; + CurveWire outerRing; + CurveWire innerRing; + BOOST_REQUIRE(outerRing.initialize({Curve2D::makeCircle({0., 0.}, 3.)}, WireRole::Outer, outerStatus)); + BOOST_REQUIRE(innerRing.initialize({Curve2D::makeCircle({0., 0.}, 1.)}, WireRole::Inner, holeStatus)); + BOOST_CHECK(holeStatus == WireStatus::Reversed); // CCW circle normalized to CW for a hole + BOOST_CHECK_LT(innerRing.signedArea(), 0.); + // net annulus area = pi * (R^2 - r^2). + checkClose(outerRing.signedArea() + innerRing.signedArea(), surf::kPi * (9. - 1.), 1.e-9); + + // a point in the material (between radii) is inside the outer ring and outside the inner hole. + const surf::Vec2 materialPoint{2., 0.}; + BOOST_CHECK(outerRing.classify(materialPoint) == WireClassification::Inside); + BOOST_CHECK(innerRing.classify(materialPoint) == WireClassification::Outside); + // a point inside the hole is inside both rings (so subtracted from the material). + const surf::Vec2 holePoint{0.2, 0.}; + BOOST_CHECK(outerRing.classify(holePoint) == WireClassification::Inside); + BOOST_CHECK(innerRing.classify(holePoint) == WireClassification::Inside); + + // --- mixed line + arc loop: a stadium / half-disk closed by a diameter --------------------- + // upper half-disk: diameter along v = 0 from (-2,0) to (2,0), closed by a CCW semicircle. + WireStatus halfStatus = WireStatus::Valid; + CurveWire halfDisk; + const std::vector halfDiskCurves{Curve2D::makeLine({-2., 0.}, {2., 0.}), + Curve2D::makeArc({0., 0.}, 2., 0., surf::kPi)}; + BOOST_REQUIRE(halfDisk.initialize(halfDiskCurves, WireRole::Outer, halfStatus)); + BOOST_CHECK(halfStatus == WireStatus::Valid); + checkClose(halfDisk.signedArea(), 0.5 * surf::kPi * 4., 1.e-9); // half of pi*r^2 + BOOST_CHECK(halfDisk.classify({0., 1.}) == WireClassification::Inside); + BOOST_CHECK(halfDisk.classify({0., -1.}) == WireClassification::Outside); + BOOST_CHECK(halfDisk.classify({0., 0.}) == WireClassification::Boundary); + + // an open curve loop is rejected. + WireStatus openStatus = WireStatus::Valid; + CurveWire openWire; + const std::vector openCurves{Curve2D::makeLine({0., 0.}, {2., 0.}), + Curve2D::makeLine({2., 0.}, {2., 2.})}; + BOOST_CHECK(!openWire.initialize(openCurves, WireRole::Outer, openStatus)); + BOOST_CHECK(openStatus == WireStatus::Open); +} + +BOOST_AUTO_TEST_CASE(BSplineTrimCurveKernels) +{ + using surf::Curve2D; + using surf::CurveWire; + using surf::Vec2; + using surf::WireClassification; + using surf::WireRole; + using surf::WireStatus; + + // --- non-rational cubic B-spline: validity, clamped endpoints, convex-hull bounds ------------ + const std::vector poles{{0., 0.}, {1., 2.}, {2., -1.}, {3., 1.}, {4., 0.}}; + const std::vector knots{0., 0., 0., 0., 0.5, 1., 1., 1., 1.}; + const Curve2D spline = Curve2D::makeBSpline(3, poles, {}, knots); + BOOST_CHECK(spline.valid()); + checkClose(spline.startPoint().uCoord, 0.); + checkClose(spline.startPoint().vCoord, 0.); + checkClose(spline.endPoint().uCoord, 4.); + checkClose(spline.endPoint().vCoord, 0.); + // extendBounds returns the (conservative) control-point convex-hull box + Vec2 lower{1.e30, 1.e30}; + Vec2 upper{-1.e30, -1.e30}; + spline.extendBounds(lower, upper); + checkClose(lower.uCoord, 0.); + checkClose(upper.uCoord, 4.); + checkClose(lower.vCoord, -1.); + checkClose(upper.vCoord, 2.); + + // --- rational quadratic B-spline: an exact NURBS quarter circle ----------------------------- + const std::vector circlePoles{{1., 0.}, {1., 1.}, {0., 1.}}; + const std::vector circleWeights{1., std::sqrt(0.5), 1.}; + const std::vector circleKnots{0., 0., 0., 1., 1., 1.}; + const Curve2D quarter = Curve2D::makeBSpline(2, circlePoles, circleWeights, circleKnots); + BOOST_CHECK(quarter.valid()); + for (int index = 0; index <= 8; ++index) { + const Vec2 point = quarter.pointAt(static_cast(index) / 8); + checkClose(std::hypot(point.uCoord, point.vCoord), 1., 1.e-9); + } + + // --- closed loop (B-spline top + three closing lines): area vs a fine-polygon reference ------ + const std::vector loop{spline, Curve2D::makeLine({4., 0.}, {4., -3.}), + Curve2D::makeLine({4., -3.}, {0., -3.}), Curve2D::makeLine({0., -3.}, {0., 0.})}; + WireStatus status = WireStatus::Valid; + CurveWire wire; + BOOST_REQUIRE(wire.initialize(loop, WireRole::Outer, status)); + double referenceArea = 0.; + const auto boundarySamples = wire.sampledBoundary(); + for (size_t k = 0; k + 1 < boundarySamples.size(); ++k) { + referenceArea += 0.5 * (boundarySamples[k].uCoord * boundarySamples[k + 1].vCoord - + boundarySamples[k + 1].uCoord * boundarySamples[k].vCoord); + } + // wire.signedArea() is the exact Gauss-Legendre value; referenceArea is a chord-polyline + // approximation of it, so compare at the sampling-accuracy level rather than machine precision + checkClose(wire.signedArea(), std::abs(referenceArea), 1.e-4); + + // classify inside / outside / boundary + BOOST_CHECK(wire.classify({2., -1.5}) == WireClassification::Inside); + BOOST_CHECK(wire.classify({2., -2.9}) == WireClassification::Inside); + BOOST_CHECK(wire.classify({-1., -1.}) == WireClassification::Outside); + BOOST_CHECK(wire.classify({2., 5.}) == WireClassification::Outside); + BOOST_CHECK(wire.classify({0., 0.}) == WireClassification::Boundary); // on the B-spline start + BOOST_CHECK(wire.classify({2., -3.}) == WireClassification::Boundary); // on the bottom line + + // --- horizontal-tangent case: a scanline tangent to a smooth apex must not flip parity ------- + // downward arch: quadratic B-spline (0,0) -> apex (1,1) -> (2,0), closed by the baseline. + const Curve2D arch = Curve2D::makeBSpline(2, {{0., 0.}, {1., 2.}, {2., 0.}}, {}, {0., 0., 0., 1., 1., 1.}); + checkClose(arch.pointAt(0.5).vCoord, 1.); // apex height + WireStatus archStatus = WireStatus::Valid; + CurveWire archRegion; + BOOST_REQUIRE(archRegion.initialize({arch, Curve2D::makeLine({2., 0.}, {0., 0.})}, WireRole::Outer, archStatus)); + BOOST_CHECK(archRegion.classify({1., 0.5}) == WireClassification::Inside); + BOOST_CHECK(archRegion.classify({1., 1.5}) == WireClassification::Outside); + // scanline v = 1 is tangent to the apex to the right of these points: a robust kernel counts an + // even number of crossings so both points classify Outside. + BOOST_CHECK(archRegion.classify({-1., 1.}) == WireClassification::Outside); + BOOST_CHECK(archRegion.classify({3., 1.}) == WireClassification::Outside); + + // --- reversal keeps the same geometric image (poles/knots complemented) ---------------------- + Curve2D reversed = spline; + reversed.reverseInPlace(); + checkClose(reversed.startPoint().uCoord, 4.); + checkClose(reversed.endPoint().uCoord, 0.); + checkClose(reversed.pointAt(0.25).uCoord, spline.pointAt(0.75).uCoord, 1.e-9); + checkClose(reversed.pointAt(0.25).vCoord, spline.pointAt(0.75).vCoord, 1.e-9); +} + +// The adaptive sampler must not be fooled by a curve that meets its own chord where it is probed. +// Both halves of the criterion get their own case, because +// each defeats the other's reproducer on its own. +BOOST_AUTO_TEST_CASE(BSplineSamplingIsNotFooledBySymmetry) +{ + using surf::Curve2D; + using surf::Vec2; + + // --- symmetry about the parameter midpoint, within a single Bezier span ---------------------- + // A cubic Bezier is (P0 + 3 P1 + 3 P2 + P3) / 8 at t = 1/2, so this S-curve passes through + // (1, 0) -- exactly on its own chord from (0, 0) to (2, 0) -- while bulging by about 0.3 either + // side of it. A single midpoint probe therefore calls it flat at the very first step and + // replaces the whole curve with a straight line. That is what happened to the tube-tube junction + // curve of six ExcavatorArm parts, whose rim vanished entirely as a result. + const Curve2D sCurve = + Curve2D::makeBSpline(3, {{0., 0.}, {0.5, 1.}, {1.5, -1.}, {2., 0.}}, {}, {0., 0., 0., 0., 1., 1., 1., 1.}); + BOOST_REQUIRE(sCurve.valid()); + checkClose(sCurve.pointAt(0.5).uCoord, 1., 1.e-12); + checkClose(sCurve.pointAt(0.5).vCoord, 0., 1.e-12); // the trap: the midpoint is on the chord + double worstOffChord = 0.; + for (int step = 0; step <= 64; ++step) { + worstOffChord = std::max(worstOffChord, std::abs(sCurve.pointAt(static_cast(step) / 64).vCoord)); + } + BOOST_CHECK(worstOffChord > 0.2); // and the curve really does leave it, by a lot + + std::vector samples; + sCurve.bsplineSampleInto(samples); + BOOST_CHECK(samples.size() > 2); // not flattened to its chord + double worstSampleError = 0.; + for (int step = 0; step <= 64; ++step) { + const Vec2 onCurve = sCurve.pointAt(static_cast(step) / 64); + double nearest = 1.e30; + for (size_t index = 0; index + 1 < samples.size(); ++index) { + nearest = std::min(nearest, surf::pointSegmentDistanceSq(onCurve, samples[index], samples[index + 1])); + } + worstSampleError = std::max(worstSampleError, std::sqrt(nearest)); + } + BOOST_CHECK(worstSampleError < 1.e-4); // and the polyline now follows it + + // --- every knot span gets sampled, however flat the curve looks ------------------------------ + // A B-spline is one polynomial piece only *within* a span, so a flatness verdict that straddles + // a knot is a verdict about a curve the test's own model does not describe. This one is exactly + // straight, so no probe anywhere can distinguish it from its chord -- and it must still be + // resolved span by span, because that is the only thing the curve's own structure guarantees. + std::vector straightPoles; + std::vector uniformKnots{0., 0., 0., 0.}; + constexpr int spanCount = 8; + for (int index = 0; index < spanCount + 3; ++index) { + straightPoles.push_back({static_cast(index), 0.}); + } + for (int index = 1; index < spanCount; ++index) { + uniformKnots.push_back(static_cast(index) / spanCount); + } + uniformKnots.insert(uniformKnots.end(), {1., 1., 1., 1.}); + const Curve2D straight = Curve2D::makeBSpline(3, straightPoles, {}, uniformKnots); + BOOST_REQUIRE(straight.valid()); + std::vector straightSamples; + straight.bsplineSampleInto(straightSamples); + BOOST_CHECK(static_cast(straightSamples.size()) >= spanCount + 1); +} + +BOOST_AUTO_TEST_CASE(CurvedPlanarDiskKernels) +{ + using surf::Curve2D; + + // annulus in the z = 0 plane: outer radius 2, hole radius 1 + surf::CurvedPlanarBoundedSurface annulus; + std::string error; + BOOST_REQUIRE(annulus.initialize({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, + {Curve2D::makeCircle({0., 0.}, 2.)}, + {{Curve2D::makeCircle({0., 0.}, 1., true)}}, error)); + BOOST_CHECK(!annulus.wasReoriented()); // outer CCW, hole CW: both already correctly oriented + + // a skewed (non-orthonormal) frame is rejected + surf::CurvedPlanarBoundedSurface skewed; + BOOST_CHECK(!skewed.initialize({0., 0., 0.}, {1., 0., 0.}, {0.5, 1., 0.}, + {Curve2D::makeCircle({0., 0.}, 2.)}, {}, error)); + + // on-surface classification: material, hole, outside, off-plane + BOOST_CHECK(annulus.containsPointOnSurface({1.5, 0., 0.})); + BOOST_CHECK(!annulus.containsPointOnSurface({0.5, 0., 0.})); + BOOST_CHECK(!annulus.containsPointOnSurface({3., 0., 0.})); + BOOST_CHECK(!annulus.containsPointOnSurface({1.5, 0., 0.5})); + + // ray intersections: one hit through the material, none through the hole + std::vector hits; + annulus.appendIntersections({1.5, 0., 1.}, {0., 0., -1.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits.front().distance, 1.); + checkClose(hits.front().normal.zCoord, 1.); + hits.clear(); + annulus.appendIntersections({0.5, 0., 1.}, {0., 0., -1.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // exact patch distances: in-hole (to the hole rim), in-material off-plane, outside the rim, + // and the combined in-plane plus out-of-plane case + checkClose(annulus.distanceSqToPatch({0., 0., 0.}), 1.); + checkClose(annulus.distanceSqToPatch({1.5, 0., 2.}), 4.); + checkClose(annulus.distanceSqToPatch({4., 0., 0.}), 4.); + checkClose(annulus.distanceSqToPatch({0.5, 0., 1.}), 1.25); + + // divergence-theorem contribution of an offset disk: (origin . normal) * area / 3 + surf::CurvedPlanarBoundedSurface offsetDisk; + BOOST_REQUIRE(offsetDisk.initialize({0., 0., 2.}, {1., 0., 0.}, {0., 1., 0.}, + {Curve2D::makeCircle({0., 0.}, 1.5)}, {}, error)); + checkClose(offsetDisk.capacityContribution(), 2. * surf::kPi * 1.5 * 1.5 / 3., 1.e-9); + BOOST_CHECK(offsetDisk.capacityIsExact()); +} + +BOOST_AUTO_TEST_CASE(CylindricalSurfaceKernels) +{ + // full lateral cylinder, radius 2, height [-3, 3], axis z + surf::CylindricalBoundedSurface cylinder; + std::string error; + BOOST_REQUIRE(cylinder.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kTwoPi, + false, error)); + + // a transversal ray crosses the lateral surface twice: both hits must be reported + std::vector hits; + cylinder.appendIntersections({-5., 0.5, 0.}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 2u); + const double chordHalf = std::sqrt(4. - 0.25); + checkClose(hits[0].distance, 5. - chordHalf); + checkClose(hits[1].distance, 5. + chordHalf); + // entering hit: outward normal opposes the ray direction; exiting hit: aligned + BOOST_CHECK_LT(hits[0].normal.xCoord, 0.); + BOOST_CHECK_GT(hits[1].normal.xCoord, 0.); + + // tangential graze reports no hits (keeps crossing parity even) + hits.clear(); + cylinder.appendIntersections({-5., 2., 0.}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // axis-parallel ray never crosses the lateral surface + hits.clear(); + cylinder.appendIntersections({0., 0., -5.}, {0., 0., 1.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // exact patch distances: radial, above the rim, and the diagonal rim case + checkClose(cylinder.distanceSqToPatch({4., 0., 0.}), 4.); + checkClose(cylinder.distanceSqToPatch({0., 0., 5.}), 8.); + checkClose(cylinder.distanceSqToPatch({3., 0., 4.}), 2.); + + // half cylinder (phi in [0, pi]): the phi trim filters hits and surface points + surf::CylindricalBoundedSurface halfCylinder; + BOOST_REQUIRE(halfCylinder.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kPi, + false, error)); + BOOST_CHECK(halfCylinder.containsPointOnSurface({0., 2., 0.})); + BOOST_CHECK(!halfCylinder.containsPointOnSurface({0., -2., 0.})); + hits.clear(); + halfCylinder.appendIntersections({0., -5., 0.}, {0., 1., 0.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits.front().distance, 7.); +} + +BOOST_AUTO_TEST_CASE(ClosedCylinderMatchesTGeoTube) +{ + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + + SurfaceSolid solid("closedCylinder"); + BOOST_REQUIRE(solid.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight)); + // cap frames: outward normal is axisU x axisV, so the bottom cap flips axisV + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoTube reference("referenceTube", 0., radius, halfHeight); + compareContainsGrid(solid, reference, 4., 9); + + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 5.}, {0., 0., -1.}); + compareDistance(solid, reference, {5., 0.5, 1.}, {-1., 0., 0.}); + compareDistance(solid, reference, {-4., -1., -2.}, unitDirection(1., 0.3, 0.5)); + compareDistance(solid, reference, {0., 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, {0., 0., 1.}); + compareDistance(solid, reference, {0., 0., 0.}, unitDirection(1., 1., 1.)); + compareDistance(solid, reference, {1., 0.5, -2.}, unitDirection(0.3, -0.4, 0.5)); + compareDistance(solid, reference, {5., 2.5, 0.}, {-1., 0., 0.}); // grazing miss + + // safeties against analytic distances (TGeo safeties may be weaker underestimates, so they + // are not compared directly) + const double center[3] = {0., 0., 0.}; + const double insidePoint[3] = {1., 0.5, 1.}; + const double radialOutside[3] = {4., 0., 0.}; + const double axialOutside[3] = {0., 0., 5.}; + const double cornerOutside[3] = {4., 0., 5.}; + checkClose(solid.Safety(center, kTRUE), radius); + checkClose(solid.Safety(insidePoint, kTRUE), radius - std::sqrt(1.25)); + checkClose(solid.Safety(radialOutside, kFALSE), 2.); + checkClose(solid.Safety(axialOutside, kFALSE), 2.); + checkClose(solid.Safety(cornerOutside, kFALSE), std::sqrt(8.)); // exact corner distance + + // normals on the lateral surface and the caps + double normal[3] = {0., 0., 0.}; + const double sidePoint[3] = {radius, 0., 1.}; + const double alongX[3] = {1., 0., 0.}; + solid.ComputeNormal(sidePoint, alongX, normal); + checkClose(normal[0], 1.); + checkClose(normal[1], 0.); + checkClose(normal[2], 0.); + const double capPoint[3] = {0.5, 0.5, halfHeight}; + const double alongZ[3] = {0., 0., 1.}; + solid.ComputeNormal(capPoint, alongZ, normal); + checkClose(normal[2], 1.); + + checkClose(solid.Capacity(), reference.Capacity(), 1.e-9); + + int meshVertices = 0; + int meshSegments = 0; + int meshPolygons = 0; + solid.GetMeshNumbers(meshVertices, meshSegments, meshPolygons); + BOOST_CHECK_GT(meshVertices, 0); + BOOST_CHECK_GT(meshPolygons, 0); +} + +BOOST_AUTO_TEST_CASE(HollowCylinderMatchesTGeoTube) +{ + constexpr double innerRadius = 1.; + constexpr double outerRadius = 2.; + constexpr double halfHeight = 3.; + + SurfaceSolid solid("hollowCylinder"); + BOOST_REQUIRE(solid.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, outerRadius, -halfHeight, + halfHeight)); + BOOST_REQUIRE(solid.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, innerRadius, -halfHeight, + halfHeight, 0., surf::kTwoPi, true)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, outerRadius, + innerRadius)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, outerRadius, + innerRadius)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoTube reference("referenceHollowTube", innerRadius, outerRadius, halfHeight); + compareContainsGrid(solid, reference, 4., 9); + + // from the hole the solid is entered through the inner wall + compareDistance(solid, reference, {0., 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 2.}, unitDirection(0.4, 0.2, -1.)); + // inside the material both walls are exit candidates + compareDistance(solid, reference, {1.5, 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {1.5, 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {-1.2, 0.8, 1.}, unitDirection(-0.2, 0.9, 0.4)); + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + + // analytic safety in the middle of the material: 0.5 to either wall + const double materialPoint[3] = {1.5, 0., 0.}; + checkClose(solid.Safety(materialPoint, kTRUE), 0.5); + + checkClose(solid.Capacity(), reference.Capacity(), 1.e-9); +} + +BOOST_AUTO_TEST_CASE(SphereMatchesTGeoSphere) +{ + constexpr double radius = 2.5; + + SurfaceSolid solid("fullSphere"); + BOOST_REQUIRE(solid.AddSphericalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius)); + solid.CloseShape(); + + // a full sphere is self-closing: no boundary edges at all + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoSphere reference("referenceSphere", 0., radius); + compareContainsGrid(solid, reference, 3.5, 9); + + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, unitDirection(1., 1., 1.)); + compareDistance(solid, reference, {1., 1., 1.}, unitDirection(-0.3, 0.5, 0.8)); + compareDistance(solid, reference, {-4., 0.5, 0.5}, {1., 0., 0.}); + compareDistance(solid, reference, {-4., 2.6, 0.}, {1., 0., 0.}); // clean miss + + // analytic safeties: |distance to center - radius| + const double insidePoint[3] = {1., 0., 0.}; + const double outsidePoint[3] = {4., 0., 0.}; + checkClose(solid.Safety(insidePoint, kTRUE), radius - 1.); + checkClose(solid.Safety(outsidePoint, kFALSE), 4. - radius); + + double normal[3] = {0., 0., 0.}; + const double surfacePoint[3] = {radius, 0., 0.}; + const double alongX[3] = {1., 0., 0.}; + solid.ComputeNormal(surfacePoint, alongX, normal); + checkClose(normal[0], 1.); + + checkClose(solid.Capacity(), reference.Capacity(), 1.e-9); + + int meshVertices = 0; + int meshSegments = 0; + int meshPolygons = 0; + solid.GetMeshNumbers(meshVertices, meshSegments, meshPolygons); + BOOST_CHECK_GT(meshVertices, 0); + BOOST_CHECK_GT(meshPolygons, 0); +} + +BOOST_AUTO_TEST_CASE(SphericalSectionKernels) +{ + // upper hemisphere shell of radius 2 (theta in [0, pi/2], full phi) + surf::SphericalBoundedSurface hemisphere; + std::string error; + BOOST_REQUIRE(hemisphere.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., 0., surf::kHalfPi, 0., + surf::kTwoPi, false, error)); + + // divergence contribution of a centred hemisphere shell: 2 pi R^3 / 3 + checkClose(hemisphere.capacityContribution(), 2. * surf::kPi * 8. / 3., 1.e-9); + + // the polar-axis ray meets the sphere twice but only the upper hit is on the patch + std::vector hits; + hemisphere.appendIntersections({0., 0., 5.}, {0., 0., -1.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits.front().distance, 3.); + checkClose(hits.front().normal.zCoord, 1.); + + // a transversal ray at z = 1 stays in the upper hemisphere: both hits reported + hits.clear(); + hemisphere.appendIntersections({-5., 0., 1.}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_CHECK_EQUAL(hits.size(), 2u); + + // the mirrored ray at z = -1 misses the trimmed patch entirely + hits.clear(); + hemisphere.appendIntersections({-5., 0., -1.}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // trim-aware surface point classification (equator lies on the trim boundary) + BOOST_CHECK(hemisphere.containsPointOnSurface({0., 0., 2.})); + BOOST_CHECK(hemisphere.containsPointOnSurface({2., 0., 0.})); + BOOST_CHECK(!hemisphere.containsPointOnSurface({0., 0., -2.})); + + // patch distance: exact radially above the pole, conservative lower bound below the equator + checkClose(hemisphere.distanceSqToPatch({0., 0., 5.}), 9.); + BOOST_CHECK_LE(hemisphere.distanceSqToPatch({0., 0., -4.}), 4. + 1.e-9); +} + +BOOST_AUTO_TEST_CASE(TruncatedConeMatchesTGeoCone) +{ + constexpr double halfHeight = 3.; + constexpr double radiusAtBottom = 2.; + constexpr double radiusAtTop = 1.; + + SurfaceSolid solid("truncatedCone"); + BOOST_REQUIRE(solid.AddConicalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radiusAtBottom, radiusAtTop, + -halfHeight, halfHeight)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radiusAtTop)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radiusAtBottom)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoCone reference("referenceCone", halfHeight, 0., radiusAtBottom, 0., radiusAtTop); + compareContainsGrid(solid, reference, 3.5, 9); + + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 5.}, {0., 0., -1.}); + compareDistance(solid, reference, {0., 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, {0., 0., 1.}); + compareDistance(solid, reference, {0., 0., 0.}, {0., 0., -1.}); + compareDistance(solid, reference, {0.5, -0.3, 1.}, unitDirection(0.6, 0.4, 0.2)); + compareDistance(solid, reference, {-4., 0.2, -2.}, unitDirection(1., 0.05, 0.3)); + + // central safety: exact distance to the lateral generator segment (2,-3)-(1,3) in (rho, z); + // TGeoCone's safety degenerates to 0 on the axis of an rmin = 0 cone, so no direct comparison + const double center[3] = {0., 0., 0.}; + checkClose(solid.Safety(center, kTRUE), 9. / std::sqrt(37.)); + + // lateral-surface normal against the ROOT cone + double normal[3] = {0., 0., 0.}; + double referenceNormal[3] = {0., 0., 0.}; + const double sidePoint[3] = {1.5, 0., 0.}; + const double alongX[3] = {1., 0., 0.}; + solid.ComputeNormal(sidePoint, alongX, normal); + reference.ComputeNormal(sidePoint, alongX, referenceNormal); + checkClose(normal[0], referenceNormal[0]); + checkClose(normal[1], referenceNormal[1]); + checkClose(normal[2], referenceNormal[2]); + + checkClose(solid.Capacity(), reference.Capacity(), 1.e-9); +} + +BOOST_AUTO_TEST_CASE(ApexConeClosesWithSingleCap) +{ + // full cone: radius 3 at z = -1.5 shrinking to the apex at z = +1.5, closed by one cap + constexpr double halfHeight = 1.5; + constexpr double baseRadius = 3.; + + SurfaceSolid solid("apexCone"); + BOOST_REQUIRE(solid.AddConicalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, baseRadius, 0., -halfHeight, + halfHeight)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, baseRadius)); + solid.CloseShape(); + + // the apex rim degenerates to a point, so one cap suffices for a closed manifold + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + // analytic containment: inside iff |z| < halfHeight and rho < r(z) = halfHeight - z + const auto analyticInside = [&](double x, double y, double z) { + return std::abs(z) < halfHeight && std::hypot(x, y) < halfHeight - z; + }; + const std::array, 7> probePoints{{{0., 0., 0.}, + {1., 0., 0.}, + {1.4, 0., 0.5}, + {0., 0., 1.4}, + {0., 0., 1.6}, + {2., 2., -1.}, + {2., 0., -1.}}}; + for (const auto& probe : probePoints) { + BOOST_TEST_CONTEXT("point = (" << probe[0] << ", " << probe[1] << ", " << probe[2] << ")") + { + BOOST_CHECK_EQUAL(solid.Contains(probe.data()), analyticInside(probe[0], probe[1], probe[2])); + } + } + + // radial exit through the slanted surface + const double insidePoint[3] = {0., 0., -1.}; + const double alongX[3] = {1., 0., 0.}; + checkClose(solid.DistFromInside(insidePoint, alongX, 3), 2.5); + + // central safety is the exact distance to the slanted line rho + z = halfHeight + const double center[3] = {0., 0., 0.}; + checkClose(solid.Safety(center, kTRUE), halfHeight / std::sqrt(2.), 1.e-9); + + // exact capacity of a full cone: pi R^2 H / 3 + checkClose(solid.Capacity(), surf::kPi * baseRadius * baseRadius * 2. * halfHeight / 3., 1.e-9); +} + +BOOST_AUTO_TEST_CASE(ToroidalSurfaceKernels) +{ + // full torus, major radius 3, minor (tube) radius 1, axis z + constexpr double majorR = 3.; + constexpr double minorR = 1.; + surf::TorusBoundedSurface torus; + std::string error; + BOOST_REQUIRE(torus.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, majorR, minorR, 0., surf::kTwoPi, 0., + surf::kTwoPi, false, error)); + + // a ray along +x through the centre crosses the donut four times: at rho = -(R+r), -(R-r), + // (R-r), (R+r), i.e. distances 6, 8, 12, 14 from the origin at x = -10 + std::vector hits; + torus.appendIntersections({-10., 0., 0.}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 4u); + std::sort(hits.begin(), hits.end(), + [](const surf::RayHit& a, const surf::RayHit& b) { return a.distance < b.distance; }); + checkClose(hits[0].distance, 6., 1.e-7); + checkClose(hits[1].distance, 8., 1.e-7); + checkClose(hits[2].distance, 12., 1.e-7); + checkClose(hits[3].distance, 14., 1.e-7); + // crossings alternate enter/exit/enter/exit along the ray + BOOST_CHECK_LT(hits[0].normal.xCoord, 0.); + BOOST_CHECK_GT(hits[1].normal.xCoord, 0.); + BOOST_CHECK_LT(hits[2].normal.xCoord, 0.); + BOOST_CHECK_GT(hits[3].normal.xCoord, 0.); + + // a z-ray tangent to the outer equator (rho = R + r) touches at a single double root: no hit + hits.clear(); + torus.appendIntersections({majorR + minorR, 0., -10.}, {0., 0., 1.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // a ray passing above the whole torus (z = 2 r) misses entirely + hits.clear(); + torus.appendIntersections({-10., 0., 2. * minorR}, {1., 0., 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + + // outward normals: +x at the outer equator, -x (towards the axis) at the inner equator + const surf::Vec3 outerNormal = torus.normalAt({majorR + minorR, 0., 0.}); + checkClose(outerNormal.xCoord, 1.); + const surf::Vec3 innerNormal = torus.normalAt({majorR - minorR, 0., 0.}); + checkClose(innerNormal.xCoord, -1.); + // top of the tube: normal points along +z + const surf::Vec3 topNormal = torus.normalAt({majorR, 0., minorR}); + checkClose(topNormal.zCoord, 1.); + + // exact meridian distances: radially outside the outer equator and inside the hole + checkClose(torus.distanceSqToPatch({majorR + minorR + 2., 0., 0.}), 4.); + checkClose(torus.distanceSqToPatch({0., 0., 0.}), (majorR - minorR) * (majorR - minorR)); + + // surface-point classification + BOOST_CHECK(torus.containsPointOnSurface({majorR + minorR, 0., 0.})); + BOOST_CHECK(torus.containsPointOnSurface({majorR, 0., minorR})); + BOOST_CHECK(!torus.containsPointOnSurface({majorR, 0., 0.})); // tube spine (interior) + BOOST_CHECK(!torus.containsPointOnSurface({majorR + 5., 0., 0.})); // off the surface + + // exact divergence-theorem capacity of a full torus: 2 pi^2 R r^2 + checkClose(torus.capacityContribution(), 2. * surf::kPi * surf::kPi * majorR * minorR * minorR, 1.e-9); + BOOST_CHECK(torus.capacityIsExact()); + + // partial tube section (a quarter-tube fillet-like patch, phiTube in [0, pi/2], full ring): + // the trim filters intersections and surface points + surf::TorusBoundedSurface quarterTube; + BOOST_REQUIRE(quarterTube.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, majorR, minorR, 0., surf::kTwoPi, 0., + surf::kHalfPi, false, error)); + BOOST_CHECK(quarterTube.containsPointOnSurface({majorR + minorR, 0., 0.})); // phiTube = 0 boundary + BOOST_CHECK(quarterTube.containsPointOnSurface({majorR, 0., minorR})); // phiTube = pi/2 boundary + BOOST_CHECK(!quarterTube.containsPointOnSurface({majorR, 0., -minorR})); // phiTube = -pi/2, off patch + hits.clear(); + quarterTube.appendIntersections({majorR, 0., -10.}, {0., 0., 1.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); // only the top (+z) tube point is on the quarter patch + checkClose(hits.front().distance, 10. + minorR, 1.e-7); +} + +BOOST_AUTO_TEST_CASE(FullTorusMatchesTGeoTorus) +{ + constexpr double majorR = 3.; + constexpr double minorR = 1.; + + SurfaceSolid solid("fullTorus"); + BOOST_REQUIRE(solid.AddToroidalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, majorR, minorR)); + solid.CloseShape(); + + // a full torus is self-closing: no boundary edges + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + // TGeoTorus(R, Rmin, Rmax): a solid torus has Rmin = 0, Rmax = tube radius + TGeoTorus reference("referenceTorus", majorR, 0., minorR); + compareContainsGrid(solid, reference, 4.5, 11); + + // analytic x-axis crossings (see the kernel test): from outside and from inside the material + const double outsidePoint[3] = {-10., 0., 0.}; + const double alongX[3] = {1., 0., 0.}; + checkClose(solid.DistFromOutside(outsidePoint, alongX, 3), 6., 1.e-7); + const double materialPoint[3] = {majorR + minorR - 0.25, 0., 0.}; // inside the tube on the +x side + BOOST_CHECK(solid.Contains(materialPoint)); + checkClose(solid.DistFromInside(materialPoint, alongX, 3), 0.25, 1.e-7); + + // a couple of oblique rays cross-checked against the ROOT torus + compareDistance(solid, reference, {-10., 0.3, 0.2}, {1., 0., 0.}, 1.e-6); + compareDistance(solid, reference, {0., 0., 5.}, {0., 0., -1.}, 1.e-6); // clean axial miss (through the hole) + + // exact capacity: 2 pi^2 R r^2 + checkClose(solid.Capacity(), reference.Capacity(), 1.e-7); + checkClose(solid.Capacity(), 2. * surf::kPi * surf::kPi * majorR * minorR * minorR, 1.e-9); + + int meshVertices = 0; + int meshSegments = 0; + int meshPolygons = 0; + solid.GetMeshNumbers(meshVertices, meshSegments, meshPolygons); + BOOST_CHECK_GT(meshVertices, 0); + BOOST_CHECK_GT(meshPolygons, 0); +} + +BOOST_AUTO_TEST_CASE(WireTrimmedTorusMatchesSection) +{ + // A partial toroidal patch (ring [0, pi/2], tube [0.2, 2.0] - a non-wrapping fillet-like arc) + // built two ways must classify points identically: with the scalar parametric rectangle and + // with an equivalent (phiRing, phiTube) line-wire trim. This exercises the wire-trim path + // (numeric capacity, conservative Safety) and the periodic-in-both-angles unwrapping. + constexpr double majorR = 4.; + constexpr double minorR = 1.5; + constexpr double tubeLow = 0.2; + constexpr double tubeHigh = 2.0; + std::string error; + + surf::TorusBoundedSurface scalarSection; + BOOST_REQUIRE(scalarSection.initialize({0.2, -0.1, 0.3}, {0., 0., 1.}, {1., 0., 0.}, majorR, minorR, 0., + surf::kHalfPi, tubeLow, tubeHigh - tubeLow, false, error)); + + surf::TorusBoundedSurface wireSection; + const auto wire = paramRectWireCurves(0., surf::kHalfPi, tubeLow, tubeHigh); + BOOST_REQUIRE(wireSection.initialize({0.2, -0.1, 0.3}, {0., 0., 1.}, {1., 0., 0.}, majorR, minorR, 0., surf::kHalfPi, + tubeLow, tubeHigh - tubeLow, false, wire, {}, error)); + BOOST_CHECK(wireSection.hasWireTrim()); + + // classification agrees across a set of on-surface probes at several ring/tube angles + for (double ring : {0.1, 0.7, 1.2, 1.7, 2.5}) { + for (double tube : {0.3, 0.8, 1.5, 1.9, 2.6}) { + const surf::Vec3 probe = scalarSection.pointAt(ring, tube); + BOOST_TEST_CONTEXT("ring = " << ring << " tube = " << tube) + { + BOOST_CHECK_EQUAL(scalarSection.containsPointOnSurface(probe), wireSection.containsPointOnSurface(probe)); + } + } + } + + // wire-trim capacity is numeric (flagged inexact) but must approximate the exact scalar value + BOOST_CHECK(scalarSection.capacityIsExact()); + BOOST_CHECK(!wireSection.capacityIsExact()); + BOOST_CHECK_SMALL(wireSection.capacityContribution() - scalarSection.capacityContribution(), + 1.e-2 * std::abs(scalarSection.capacityContribution())); +} + +BOOST_AUTO_TEST_CASE(BVHConstructionAndTraversal) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + + SurfaceSolid solid("bvhBox"); + addBoxSurfaces(solid, halfX, halfY, halfZ); + BOOST_CHECK(!solid.HasBVH()); // built only in CloseShape + solid.CloseShape(); + BOOST_REQUIRE(solid.HasBVH()); + + // the BVH root box must enclose the exact solid bounds and stay conservative: not tighter + // than the exact bounds, not looser than the documented expansion (plus float rounding) + Point3D lower{}; + Point3D upper{}; + BOOST_REQUIRE(solid.GetBVHRootBounds(lower, upper)); + const Point3D exactLower{-halfX, -halfY, -halfZ}; + const Point3D exactUpper{halfX, halfY, halfZ}; + constexpr double boxSlack = 2. * surf::kBVHBoxTolerance; + for (int dimension = 0; dimension < 3; ++dimension) { + BOOST_TEST_CONTEXT("dimension = " << dimension) + { + BOOST_CHECK(lower[dimension] <= exactLower[dimension]); + BOOST_CHECK(lower[dimension] >= exactLower[dimension] - boxSlack); + BOOST_CHECK(upper[dimension] >= exactUpper[dimension]); + BOOST_CHECK(upper[dimension] <= exactUpper[dimension] + boxSlack); + } + } + + // a ray through the box must traverse (at least) the entry and exit face leaves ... + BOOST_CHECK_GE(solid.CountBVHRayCandidates({-2., 0., 0.}, {1., 0., 0.}), 2); + // ... while a ray pointing away from the solid reaches no leaf at all + BOOST_CHECK_EQUAL(solid.CountBVHRayCandidates({0., 5., 0.}, {0., 1., 0.}), 0); + + // two disjoint boxes: BVH pruning with well-separated primitive clusters. The union of two + // closed manifolds is still a closed manifold, and parity containment handles it naturally. + constexpr double half = 1.; + constexpr double centerX = 3.; + SurfaceSolid twoBoxes("twoBoxes"); + addBoxSurfaces(twoBoxes, half, half, half, {-centerX, 0., 0.}); + addBoxSurfaces(twoBoxes, half, half, half, {centerX, 0., 0.}); + twoBoxes.CloseShape(); + BOOST_REQUIRE(twoBoxes.HasBVH()); + BOOST_CHECK_EQUAL(twoBoxes.GetNsurfaces(), 12); + BOOST_CHECK(twoBoxes.IsClosed()); + BOOST_CHECK(twoBoxes.IsOrientationConsistent()); + + const auto analyticInside = [&](const double* point) { + return (std::abs(std::abs(point[0]) - centerX) < half) && std::abs(point[1]) < half && std::abs(point[2]) < half; + }; + constexpr int samples = 9; + constexpr double extent = 5.; + for (int stepX = 0; stepX < samples; ++stepX) { + for (int stepY = 0; stepY < samples; ++stepY) { + for (int stepZ = 0; stepZ < samples; ++stepZ) { + const double point[3] = {-extent + 2. * extent * (stepX + 0.517) / samples, + -extent + 2. * extent * (stepY + 0.263) / samples, + -extent + 2. * extent * (stepZ + 0.741) / samples}; + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + const bool bvhInside = twoBoxes.Contains(point); + BOOST_CHECK_EQUAL(bvhInside, twoBoxes.Contains_Loop(point)); + BOOST_CHECK_EQUAL(bvhInside, analyticInside(point)); + } + } + } + } +} + +BOOST_AUTO_TEST_CASE(ContainsBoundaryPointsAndCapsule) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + + SurfaceSolid box("boundaryBox"); + addBoxSurfaces(box, halfX, halfY, halfZ); + box.CloseShape(); + + // boundary policy: points exactly on faces, edges and vertices count as inside, + // in the BVH-accelerated path and in the trivial loop alike + const std::array, 6> boundaryPoints{{ + {halfX, 0., 0.}, // face + {0., -halfY, 0.}, // face + {halfX, halfY, 0.}, // edge + {-halfX, 0., halfZ}, // edge + {halfX, halfY, halfZ}, // vertex + {-halfX, -halfY, -halfZ} // vertex + }}; + for (const auto& point : boundaryPoints) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK(box.Contains(point.data())); + BOOST_CHECK(box.Contains_Loop(point.data())); + } + } + + // capsule: cylinder barrel closed by two spherical endcaps - a mixed quadric fixture with no + // ROOT primitive equivalent, cross-validated against the trivial loop and the analytic shape + constexpr double radius = 1.; + constexpr double halfHeight = 1.5; + SurfaceSolid capsule("capsule"); + BOOST_REQUIRE(capsule.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight)); + BOOST_REQUIRE(capsule.AddSphericalSurface({0., 0., halfHeight}, {0., 0., 1.}, {1., 0., 0.}, radius, 0., + surf::kPi / 2.)); + BOOST_REQUIRE(capsule.AddSphericalSurface({0., 0., -halfHeight}, {0., 0., -1.}, {1., 0., 0.}, radius, 0., + surf::kPi / 2.)); + capsule.CloseShape(); + BOOST_REQUIRE(capsule.HasBVH()); + BOOST_CHECK(capsule.IsClosed()); + BOOST_CHECK(capsule.IsOrientationConsistent()); + + const auto capsuleInside = [&](const double* point) { + const double axialDistance = std::max(0., std::abs(point[2]) - halfHeight); + return std::hypot(point[0], point[1], axialDistance) < radius; + }; + constexpr int samples = 9; + constexpr double extent = 3.; + for (int stepX = 0; stepX < samples; ++stepX) { + for (int stepY = 0; stepY < samples; ++stepY) { + for (int stepZ = 0; stepZ < samples; ++stepZ) { + const double point[3] = {-extent + 2. * extent * (stepX + 0.517) / samples, + -extent + 2. * extent * (stepY + 0.263) / samples, + -extent + 2. * extent * (stepZ + 0.741) / samples}; + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + const bool bvhInside = capsule.Contains(point); + BOOST_CHECK_EQUAL(bvhInside, capsule.Contains_Loop(point)); + BOOST_CHECK_EQUAL(bvhInside, capsuleInside(point)); + } + } + } + } + + // a few characteristic capsule points, incl. points exactly on the barrel and cap surfaces + const double onBarrel[3] = {radius, 0., 0.5}; + const double onCapApex[3] = {0., 0., halfHeight + radius}; + const double aboveApex[3] = {0., 0., halfHeight + radius + 0.01}; + const double onRim[3] = {radius, 0., halfHeight}; // shared cylinder/sphere rim + BOOST_CHECK(capsule.Contains(onBarrel)); + BOOST_CHECK(capsule.Contains(onCapApex)); + BOOST_CHECK(!capsule.Contains(aboveApex)); + BOOST_CHECK(capsule.Contains(onRim)); + + // exact capacity: cylinder plus a full sphere from the two hemispheres + checkClose(capsule.Capacity(), + surf::kPi * radius * radius * 2. * halfHeight + 4. * surf::kPi * radius * radius * radius / 3., 1.e-9); +} + +BOOST_AUTO_TEST_CASE(DistanceBVHMatchesLoopOnAllFixtures) +{ + // The BVH distance queries against their all-surfaces oracle, over every fixture family and a + // dense point x direction sweep. This is the correctness guard that does not depend on any + // reference shape: it isolates traversal and pruning from the analytic kernels, which the + // per-shape cases above already validate against ROOT. + const std::array, double>, 7> fixtures{{ + {makeBoxSolid("loopBox", 1., 2., 3.), 4.}, + {makeTubeSolid("loopTube", 0., 2., 3.), 4.}, + {makeTubeSolid("loopHollowTube", 1., 2., 3.), 4.}, + {makeConeSolid("loopCone", 2., 1., 3.), 4.}, + {makeSphereSolid("loopSphere", 2.5), 3.5}, + {makeTorusSolid("loopTorus", 3., 1.), 4.5}, + {makeCapsuleSolid("loopCapsule", 1., 1.5), 3.}, + }}; + + for (const auto& [solid, extent] : fixtures) { + BOOST_TEST_CONTEXT("fixture = " << solid->GetName()) + { + BOOST_REQUIRE(solid->HasBVH()); + sweepDistanceAgainstLoop(*solid, extent, 5); + } + } +} + +BOOST_AUTO_TEST_CASE(DistanceSweepsMatchRootPrimitives) +{ + // Systematic point x direction sweeps against the ROOT primitives, for both the entering and + // the exiting query. The per-shape cases above check a handful of hand-picked rays; this walks + // a grid, so it also covers rays that miss, that graze, and that cross a hole. + constexpr int samples = 5; + + const auto box = makeBoxSolid("sweepBox", 1., 2., 3.); + TGeoBBox boxReference("sweepBoxReference", 1., 2., 3.); + sweepDistanceAgainstReference(*box, boxReference, 4., samples); + + const auto tube = makeTubeSolid("sweepTube", 0., 2., 3.); + TGeoTube tubeReference("sweepTubeReference", 0., 2., 3.); + sweepDistanceAgainstReference(*tube, tubeReference, 4., samples); + + const auto hollowTube = makeTubeSolid("sweepHollowTube", 1., 2., 3.); + TGeoTube hollowTubeReference("sweepHollowTubeReference", 1., 2., 3.); + sweepDistanceAgainstReference(*hollowTube, hollowTubeReference, 4., samples); + + const auto cone = makeConeSolid("sweepCone", 2., 1., 3.); + TGeoCone coneReference("sweepConeReference", 3., 0., 2., 0., 1.); + sweepDistanceAgainstReference(*cone, coneReference, 4., samples); + + const auto sphere = makeSphereSolid("sweepSphere", 2.5); + TGeoSphere sphereReference("sweepSphereReference", 0., 2.5); + sweepDistanceAgainstReference(*sphere, sphereReference, 3.5, samples); + + // the torus kernel solves a quartic, so it carries more rounding than the quadric shapes + const auto torus = makeTorusSolid("sweepTorus", 3., 1.); + TGeoTorus torusReference("sweepTorusReference", 3., 0., 1.); + sweepDistanceAgainstReference(*torus, torusReference, 4.5, samples, 1.e-6); +} + +BOOST_AUTO_TEST_CASE(DistanceHardCases) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + const auto box = makeBoxSolid("hardCaseBox", halfX, halfY, halfZ); + TGeoBBox reference("hardCaseBoxReference", halfX, halfY, halfZ); + + // --- rays through a shared edge and a shared vertex ----------------------------------------- + // Both are seen by more than one patch, so the same crossing is reported several times. Taking + // the minimum over entering hits is insensitive to that, but the BVH and the loop must still + // see the same set, which is what the loop cross-check asserts. + const std::array, 4> throughFeature{{ + {-5., halfY, 0.}, // straight at the x = -1 / y = +2 edge + {-5., halfY, halfZ}, // straight at the (-1, +2, +3) vertex + {0., 0., 0.}, // from the centre out through the +y/+z edge + {-5., -5., -5.}, // body diagonal through the (-1,-2,-3) vertex + }}; + const std::array, 4> throughFeatureDirection{{ + {1., 0., 0.}, + {1., 0., 0.}, + unitDirection(0., 1., 1.5), + unitDirection(1., 2., 3.), + }}; + for (size_t index = 0; index < throughFeature.size(); ++index) { + checkDistanceAgainstLoop(*box, throughFeature[index], throughFeatureDirection[index]); + } + + // --- grazing / tangent rays ------------------------------------------------------------------ + // A ray in the plane of a face never enters: every hit it can report is tangential, and a + // tangential hit is not a crossing. Both queries must agree with the loop and find nothing. + const std::array, 3> grazing{{ + {-5., halfY, 0.}, // in the plane of the y = +2 face + {halfX, -5., 0.}, // in the plane of the x = +1 face + {0., 0., halfZ}, // in the plane of the z = +3 face + }}; + const std::array, 3> grazingDirection{{ + {1., 0., 0.}, + {0., 1., 0.}, + unitDirection(1., 1., 0.), + }}; + for (size_t index = 0; index < grazing.size(); ++index) { + checkDistanceAgainstLoop(*box, grazing[index], grazingDirection[index]); + } + // a cylinder tangent ray: the double root must not be reported as two crossings + const auto tube = makeTubeSolid("hardCaseTube", 0., 2., 3.); + checkDistanceAgainstLoop(*tube, {-5., 2., 0.}, {1., 0., 0.}); + checkDistanceAgainstLoop(*tube, {-5., 2. - 1.e-7, 0.}, {1., 0., 0.}); // just inside tangency + checkDistanceAgainstLoop(*tube, {-5., 2. + 1.e-7, 0.}, {1., 0., 0.}); // just outside tangency + + // --- rays starting exactly on a surface ------------------------------------------------------- + // The on-surface convention (a crossing at t = 0 is below kRayTolerance and is not reported) is + // inherited from the analytic kernels; what matters here is that the BVH reproduces it exactly. + const std::array, 4> onSurface{{ + {halfX, 0., 0.}, // on a face + {-halfX, 0.5, -1.}, // on the opposite face + {halfX, halfY, 0.}, // on an edge + {halfX, halfY, halfZ}, // on a vertex + }}; + for (const auto& point : onSurface) { + for (const auto& direction : probeDirections()) { + checkDistanceAgainstLoop(*box, point, direction); + } + } + // just off the surface the answers must be the ordinary ones: entering after ~1e-6 from + // outside, exiting after the full traversal from inside + const std::array justOutside{halfX + 1.e-6, 0., 0.}; + const std::array justInside{halfX - 1.e-6, 0., 0.}; + const std::array inward{-1., 0., 0.}; + checkClose(box->DistFromOutside(justOutside.data(), inward.data(), 3), 1.e-6, 1.e-12); + checkClose(box->DistFromInside(justInside.data(), inward.data(), 3), 2. * halfX - 1.e-6, 1.e-12); + checkClose(box->DistFromOutside(justOutside.data(), inward.data(), 3), + reference.DistFromOutside(justOutside.data(), inward.data(), 3), 1.e-12); + + // --- stepmax ---------------------------------------------------------------------------------- + const std::array farOutside{-5., 0., 0.}; + const std::array alongX{1., 0., 0.}; + const double entryDistance = box->DistFromOutside(farOutside.data(), alongX.data(), 3); + checkClose(entryDistance, 4.); + // a hit beyond stepmax must not be reported ... + BOOST_CHECK_EQUAL(box->DistFromOutside(farOutside.data(), alongX.data(), 3, entryDistance * 0.5), + TGeoShape::Big()); + // ... including when it lies only just beyond, and the cheap bounding-box reject must agree + BOOST_CHECK_EQUAL(box->DistFromOutside(farOutside.data(), alongX.data(), 3, entryDistance - 1.e-3), + TGeoShape::Big()); + // ... while a stepmax past the hit changes nothing + checkClose(box->DistFromOutside(farOutside.data(), alongX.data(), 3, entryDistance + 1.e-3), entryDistance); + checkClose(box->DistFromOutside(farOutside.data(), alongX.data(), 3, 100.), entryDistance); + // the same for the exiting query + const std::array center{0., 0., 0.}; + const double exitDistance = box->DistFromInside(center.data(), alongX.data(), 3); + checkClose(exitDistance, halfX); + BOOST_CHECK_EQUAL(box->DistFromInside(center.data(), alongX.data(), 3, exitDistance * 0.5), TGeoShape::Big()); + checkClose(box->DistFromInside(center.data(), alongX.data(), 3, exitDistance * 2.), exitDistance); + // and the loop must honour stepmax identically, at and around the hit + for (const double stepmax : {entryDistance * 0.5, entryDistance - 1.e-9, entryDistance, entryDistance + 1.e-9, + entryDistance * 2.}) { + checkDistanceAgainstLoop(*box, farOutside, alongX, stepmax); + checkDistanceAgainstLoop(*box, center, alongX, stepmax); + } + + // --- a ray that cannot reach the solid at all -------------------------------------------------- + const std::array wayOff{-1000., 0., 0.}; + BOOST_CHECK_EQUAL(box->DistFromOutside(wayOff.data(), alongX.data(), 3, 10.), TGeoShape::Big()); + checkClose(box->DistFromOutside(wayOff.data(), alongX.data(), 3), 999.); +} + +BOOST_AUTO_TEST_CASE(RayTMaxPruningIsOptimizationOnly) +{ + // A row of well-separated boxes: a ray along the row enters the first one, after which every + // node behind it is beyond the tightened bound and must not be visited. Turning the tightening + // off must cost candidates without changing a single answer. + constexpr int boxCount = 8; + constexpr double half = 0.5; + constexpr double spacing = 4.; + + SurfaceSolid row("prunedRow"); + for (int boxIndex = 0; boxIndex < boxCount; ++boxIndex) { + addBoxSurfaces(row, half, half, half, {boxIndex * spacing, 0., 0.}); + } + row.CloseShape(); + BOOST_REQUIRE(row.HasBVH()); + BOOST_CHECK(row.IsClosed()); + BOOST_CHECK_EQUAL(row.GetNsurfaces(), 6 * boxCount); + + const std::array beforeRow{-5., 0., 0.}; + const std::array alongRow{1., 0., 0.}; + + BOOST_CHECK(SurfaceSolid::GetRayTMaxPruning()); // on by default + + SurfaceSolid::ResetRayCandidateCounter(); + const double prunedDistance = row.DistFromOutside(beforeRow.data(), alongRow.data(), 3); + const long long prunedCandidates = SurfaceSolid::GetRayCandidateCount(); + + SurfaceSolid::SetRayTMaxPruning(false); + SurfaceSolid::ResetRayCandidateCounter(); + const double unprunedDistance = row.DistFromOutside(beforeRow.data(), alongRow.data(), 3); + const long long unprunedCandidates = SurfaceSolid::GetRayCandidateCount(); + SurfaceSolid::SetRayTMaxPruning(true); + + // same answer, and it is the entry face of the first box + BOOST_CHECK_EQUAL(prunedDistance, unprunedDistance); + checkClose(prunedDistance, 5. - half); + // ... reached after strictly less work + BOOST_CHECK_GT(prunedCandidates, 0); + BOOST_CHECK_LT(prunedCandidates, unprunedCandidates); + + // the answers stay identical over a full sweep, which is the property that lets the benchmark + // treat the switch as a pure cost knob + sweepDistanceAgainstLoop(row, 1.2 * boxCount * spacing / 2., 4); + + // the counter is not touched by the _Loop variants, which visit everything by construction + SurfaceSolid::ResetRayCandidateCounter(); + row.DistFromOutside_Loop(beforeRow.data(), alongRow.data()); + BOOST_CHECK_EQUAL(SurfaceSolid::GetRayCandidateCount(), 0); +} + +BOOST_AUTO_TEST_CASE(RayTMaxPruningKeepsNearTies) +{ + // Two entering candidates a controlled hair apart, one of them behind a very loose bounding + // box: the geometry in which a mis-set tmax would do its damage. + // + // Why this shape of test. A node is culled when the ray *enters its box* beyond tmax, and a box + // is always entered no later than the patch inside it is hit. A candidate nearer than the + // current best therefore has a box entered earlier than the current best's hit, and survives + // any bound at or above that hit -- which is why the implementation's bound (the best hit, + // rounded up, plus the box inflation) can be argued safe rather than merely measured safe. The + // narrow window that is left needs a loose box visited first and a tight one entered between + // the loose patch's box and its hit, so that is what this fixture builds. + // + // Fixture: a sphere hit by a near-limb ray far behind where its bounding box starts, plus a + // small flat patch just in front of that hit, swept over several decades of separation. It is + // deliberately not a closed manifold (the patch clips into the sphere) and is closed with the + // diagnostics off: it exists to place the two candidates, not to model a solid. That is + // legitimate because the oracle is DistFromOutside_Loop, which minimises over the same hits. + // + // Scope, honestly: mutation-testing this suite showed that a bound scaled by 0.5 is caught + // loudly by the sweeps above, while one scaled by 0.999 is caught by neither them nor this + // case -- with so few primitives, both leaves are box-tested in the same inner-node visit, + // before any leaf callback has run and tightened anything. So this pins the near-tie geometry + // and the pruning-on == pruning-off == loop identity; the guarantee against a subtly tight + // bound rests on the argument above, not on this test. + constexpr double radius = 2.; + constexpr double rayOffsetY = 1.9; // near the limb: box entered at x = -2, surface at x = -0.62 + const double sphereHitX = -std::sqrt(radius * radius - rayOffsetY * rayOffsetY); + const std::array rayOrigin{-10., rayOffsetY, 0.}; + const std::array alongX{1., 0., 0.}; + const double sphereDistance = sphereHitX - rayOrigin[0]; + + // relative offsets spanning several decades below the sphere hit, so any tmax that is too + // tight by anything in that range is caught by at least one of them regardless of how the + // builder happens to lay out the tree + for (const double relativeOffset : {1.e-5, 3.e-5, 1.e-4, 3.e-4, 1.e-3, 3.e-3, 1.e-2}) { + const double patchX = sphereHitX - relativeOffset * sphereDistance; + BOOST_TEST_CONTEXT("relativeOffset = " << relativeOffset << " patchX = " << patchX) + { + SurfaceSolid solid("nearTie"); + BOOST_REQUIRE(solid.AddSphericalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius)); + // axisU x axisV = z x y = -x: the patch faces the incoming ray, so crossing it enters + BOOST_REQUIRE(solid.AddPlanarSurface({patchX, rayOffsetY - 0.1, -0.1}, {0., 0., 1.}, {0., 1., 0.}, + rectangleWire(0.2, 0.2))); + solid.CloseShape(false); + BOOST_REQUIRE(solid.HasBVH()); + + // the flat patch is the nearer entering crossing, by construction + const double expected = patchX - rayOrigin[0]; + for (const bool pruning : {true, false}) { + SurfaceSolid::SetRayTMaxPruning(pruning); + BOOST_TEST_CONTEXT("pruning = " << pruning) + { + const double distance = solid.DistFromOutside(rayOrigin.data(), alongX.data(), 3); + checkClose(distance, expected, 1.e-9); + BOOST_CHECK_EQUAL(distance, solid.DistFromOutside_Loop(rayOrigin.data(), alongX.data())); + } + } + SurfaceSolid::SetRayTMaxPruning(true); + } + } +} + +BOOST_AUTO_TEST_CASE(CurvedPlanarStadiumPrism) +{ + // A stadium (rectangle with two semicircular ends) extruded along z: the two end caps are + // planar faces with mixed line+arc wires - the general curved-planar case a disk cannot + // express. Straight sides are flat rectangles; the round ends are half-cylinders. + constexpr double halfLen = 3.; // straight half-length along x + constexpr double radius = 2.; // corner radius and half-width along y + constexpr double halfHeight = 4.; // half-height along z + + // Stadium cross-section boundary in the cap's local (u=x, v=y) frame, CCW: bottom line, + // right semicircle, top line, left semicircle. + const std::vector stadiumWire{ + BoundaryCurve::makeLine({-halfLen, -radius}, {halfLen, -radius}), + BoundaryCurve::makeArc({halfLen, 0.}, radius, -surf::kHalfPi, surf::kHalfPi), + BoundaryCurve::makeLine({halfLen, radius}, {-halfLen, radius}), + BoundaryCurve::makeArc({-halfLen, 0.}, radius, surf::kHalfPi, 3. * surf::kHalfPi)}; + + SurfaceSolid solid("stadiumPrism"); + // caps (outward +z / -z: the bottom cap flips axisV) + BOOST_REQUIRE(solid.AddCurvedPlanarSurface({0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, stadiumWire)); + BOOST_REQUIRE(solid.AddCurvedPlanarSurface({0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, stadiumWire)); + // flat side walls at y = +/- radius (outward +/- y) + BOOST_REQUIRE(solid.AddPlanarSurface({-halfLen, radius, -halfHeight}, {0., 0., 1.}, {1., 0., 0.}, + rectangleWire(2. * halfHeight, 2. * halfLen))); + BOOST_REQUIRE(solid.AddPlanarSurface({-halfLen, -radius, -halfHeight}, {1., 0., 0.}, {0., 0., 1.}, + rectangleWire(2. * halfLen, 2. * halfHeight))); + // round ends as half-cylinders (outer walls) + BOOST_REQUIRE(solid.AddCylindricalSurface({halfLen, 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight, -surf::kHalfPi, surf::kPi)); + BOOST_REQUIRE(solid.AddCylindricalSurface({-halfLen, 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight, surf::kHalfPi, surf::kPi)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + // exact capacity: (rectangle 2L x 2R + full circle pi R^2) x height 2H + checkClose(solid.Capacity(), (4. * halfLen * radius + surf::kPi * radius * radius) * 2. * halfHeight, 1.e-6); + + const auto stadiumInside = [&](const double* point) { + if (std::abs(point[2]) > halfHeight) { + return false; + } + const double ax = std::abs(point[0]); + const double dx = ax > halfLen ? ax - halfLen : 0.; + return dx * dx + point[1] * point[1] <= radius * radius; + }; + // deterministic grid spanning well beyond the solid on every axis + constexpr int samples = 21; + const double extentX = 6., extentY = 3.5, extentZ = 5.; + for (int stepX = 0; stepX < samples; ++stepX) { + for (int stepY = 0; stepY < samples; ++stepY) { + for (int stepZ = 0; stepZ < samples; ++stepZ) { + const double point[3] = {-extentX + 2. * extentX * (stepX + 0.517) / samples, + -extentY + 2. * extentY * (stepY + 0.263) / samples, + -extentZ + 2. * extentZ * (stepZ + 0.741) / samples}; + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(solid.Contains(point), stadiumInside(point)); + } + } + } + } +} + +BOOST_AUTO_TEST_CASE(WireTrimmedCylinderMatchesTube) +{ + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + + // lateral wall via the wire-trim overload: the trim is the full parametric rectangle + // phi in [0, 2pi] x h in [-hh, hh] expressed as four line edges, which must behave exactly like + // the scalar rectangle path (equivalence check). + SurfaceSolid solid("wireTrimmedCylinder"); + BOOST_REQUIRE(solid.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight, 0., surf::kTwoPi, false, + paramRectWire(0., surf::kTwoPi, -halfHeight, halfHeight))); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoTube reference("wireTrimTube", 0., radius, halfHeight); + compareContainsGrid(solid, reference, 4., 9); + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 5.}, {0., 0., -1.}); + compareDistance(solid, reference, {-4., -1., -2.}, unitDirection(1., 0.3, 0.5)); + compareDistance(solid, reference, {0., 0., 0.}, unitDirection(1., 1., 1.)); + compareDistance(solid, reference, {5., 2.5, 0.}, {-1., 0., 0.}); // grazing miss + + // capacity is numerically integrated for a wire trim; the wall integrand is constant here so it + // stays accurate, but compare with a relaxed tolerance to reflect the quadrature + checkClose(solid.Capacity(), reference.Capacity(), 1.e-6); + + double normal[3] = {0., 0., 0.}; + const double sidePoint[3] = {radius, 0., 1.}; + const double alongX[3] = {1., 0., 0.}; + solid.ComputeNormal(sidePoint, alongX, normal); + checkClose(normal[0], 1.); + checkClose(normal[1], 0.); + checkClose(normal[2], 0.); +} + +BOOST_AUTO_TEST_CASE(WireTrimmedConeMatchesCone) +{ + constexpr double halfHeight = 3.; + constexpr double radiusAtBottom = 2.; + constexpr double radiusAtTop = 1.; + + SurfaceSolid solid("wireTrimmedCone"); + BOOST_REQUIRE(solid.AddConicalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radiusAtBottom, radiusAtTop, + -halfHeight, halfHeight, 0., surf::kTwoPi, false, + paramRectWire(0., surf::kTwoPi, -halfHeight, halfHeight))); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radiusAtTop)); + BOOST_REQUIRE(addDiskSurface(solid, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radiusAtBottom)); + solid.CloseShape(); + + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoCone reference("wireTrimCone", halfHeight, 0., radiusAtBottom, 0., radiusAtTop); + compareContainsGrid(solid, reference, 3.5, 9); + compareDistance(solid, reference, {5., 0., 0.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, {1., 0., 0.}); + compareDistance(solid, reference, {-4., 0.2, -2.}, unitDirection(1., 0.05, 0.3)); + // The wire-trimmed cone's capacity used to need a 1e-3 allowance for the grid quadrature; the + // Green's-theorem contour form is exact on this rectangle-tracing wire, so it can be held to the + // same tolerance as any untrimmed patch. A regression to the grid rule fails here. + checkClose(solid.Capacity(), reference.Capacity(), 1.e-9); +} + +// Green's theorem for wire-trimmed quadrics. +// +// The integrand does not depend on the second parameter for a cylinder, and depends on the first +// only through sin/cos/identity for all four quadrics, so an antiderivative in u exists in closed +// form and the area integral collapses to a contour integral around the trim wire. +// +// The sharpest possible check: a wire that traces exactly the parametric rectangle must give the +// same number as the rectangle's own closed form, which is analytically exact. Anything the +// contour form got wrong -- a sign, an orientation, a missed seam, a wrong antiderivative -- +// shows up here immediately, and the old quadrature could only ever have agreed to ~1e-2. +BOOST_AUTO_TEST_CASE(WireTrimCapacityMatchesTheClosedForm) +{ + std::string error; + // an off-origin centre and a tilted frame, so every term of every antiderivative is exercised + // (C.U, C.V and C.W all non-zero) rather than cancelling + const surf::Vec3 centre{0.7, -1.3, 0.45}; + const surf::Vec3 axis = surf::normalized({0.3, 0.4, 1.}); + const surf::Vec3 reference{1., 0.2, 0.}; + constexpr double kRelative = 1.e-12; + + const auto compare = [&](const char* what, const surf::BoundedSurface& rectangle, + const surf::BoundedSurface& wired) { + BOOST_TEST_CONTEXT(what) + { + BOOST_CHECK(!wired.capacityIsExact()); // still reported inexact: see the note below + const double exact = rectangle.capacityContribution(); + const double contour = wired.capacityContribution(); + BOOST_CHECK_GT(std::abs(exact), 1.e-6); // a zero contribution would prove nothing + checkClose(contour, exact, kRelative * std::abs(exact)); + } + }; + + { + surf::CylindricalBoundedSurface rectangle; + surf::CylindricalBoundedSurface wired; + const double phiLow = 0.3, phiHigh = 2.4, hLow = -0.8, hHigh = 1.9; + BOOST_REQUIRE(rectangle.initialize(centre, axis, reference, 1.7, hLow, hHigh, phiLow, phiHigh - phiLow, false, + error)); + BOOST_REQUIRE(wired.initialize(centre, axis, reference, 1.7, hLow, hHigh, phiLow, phiHigh - phiLow, false, + paramRectWireCurves(phiLow, phiHigh, hLow, hHigh), {}, error)); + compare("cylinder", rectangle, wired); + } + { + surf::ConicalBoundedSurface rectangle; + surf::ConicalBoundedSurface wired; + const double phiLow = -0.4, phiHigh = 1.9, hLow = 0.2, hHigh = 2.1; + BOOST_REQUIRE(rectangle.initialize(centre, axis, reference, 1.1, 2.3, hLow, hHigh, phiLow, phiHigh - phiLow, + false, error)); + BOOST_REQUIRE(wired.initialize(centre, axis, reference, 1.1, 2.3, hLow, hHigh, phiLow, phiHigh - phiLow, false, + paramRectWireCurves(phiLow, phiHigh, hLow, hHigh), {}, error)); + compare("cone", rectangle, wired); + } + { + surf::SphericalBoundedSurface rectangle; + surf::SphericalBoundedSurface wired; + const double phiLow = 0.2, phiHigh = 2.7, thetaLow = 0.4, thetaHigh = 2.3; + BOOST_REQUIRE(rectangle.initialize(centre, axis, reference, 2.2, thetaLow, thetaHigh, phiLow, phiHigh - phiLow, + false, error)); + BOOST_REQUIRE(wired.initialize(centre, axis, reference, 2.2, thetaLow, thetaHigh, phiLow, phiHigh - phiLow, false, + paramRectWireCurves(phiLow, phiHigh, thetaLow, thetaHigh), {}, error)); + compare("sphere", rectangle, wired); + } + { + surf::TorusBoundedSurface rectangle; + surf::TorusBoundedSurface wired; + const double ringLow = 0.1, ringHigh = 2.2, tubeLow = -0.3, tubeHigh = 1.8; + BOOST_REQUIRE(rectangle.initialize(centre, axis, reference, 4., 1.4, ringLow, ringHigh - ringLow, tubeLow, + tubeHigh - tubeLow, false, error)); + BOOST_REQUIRE(wired.initialize(centre, axis, reference, 4., 1.4, ringLow, ringHigh - ringLow, tubeLow, + tubeHigh - tubeLow, false, + paramRectWireCurves(ringLow, ringHigh, tubeLow, tubeHigh), {}, error)); + compare("torus", rectangle, wired); + } + + // A trim the rectangle cannot express, checked against the integrator it replaces. The midpoint + // rule is a genuinely independent computation -- it needs nothing of the integrand but its value + // -- so agreement is evidence, but only to its own O(1/N) accuracy, which is the whole reason + // this change exists. Refining it must walk *towards* the contour answer; that direction is the + // real assertion here, not either number. + { + surf::CylindricalBoundedSurface disk; + const double radius = 1.7; + const std::vector trim{surf::Curve2D::makeCircle({1.0, 0.2}, 0.6)}; + BOOST_REQUIRE(disk.initialize(centre, axis, reference, radius, -2., 2., 0., surf::kTwoPi, false, trim, {}, + error)); + const double contour = disk.capacityContribution(); + + // the same trim, rebuilt here so the grid rule can be run over it directly + surf::CurveWire outerWire; + std::vector innerWires; + surf::Vec2 lower, upper; + BOOST_REQUIRE(surf::buildCurveTrim(trim, {}, outerWire, innerWires, lower, upper, error, + surf::parametricMetricOf(disk))); + + const auto gridRelativeError = [&](int samples) { + const double grid = surf::integrateOverCurveTrim( + outerWire, innerWires, + [&disk, radius](double phi, double height) { + const surf::Vec3 point = disk.pointAt(phi, height); + return surf::dot(point, disk.normalAt(point)) * radius / 3.; + }, + samples); + return std::abs(grid - contour) / std::abs(contour); + }; + const double at128 = gridRelativeError(128); + const double at512 = gridRelativeError(512); + const double at2048 = gridRelativeError(2048); + + // The grid rule confirms the contour value to its own accuracy -- an independent computation + // agreeing to 3e-5 is what says the antiderivative route is not just self-consistent. + BOOST_CHECK_LT(at512, 1.e-4); + BOOST_CHECK_LT(at2048, 1.e-4); + // But it cannot do better, and that is the point. At the shipped 128 it is off by 2e-3 -- + // three orders outside the gate's 1e-6 band -- and refining it sixteen-fold does not fix that, + // because the error is not monotone: the staircase re-phases and 2048 is *worse* than 512 + // (2.9e-5 against 2.4e-5 here; on ExcavatorArm/BucketLink2 the sequence 128..2048 runs 16.004, + // 17.710, 16.927, 17.244, 17.032 around a true 17.079). So no N could have been the fix. + BOOST_CHECK_GT(at128, 1.e-3); + BOOST_CHECK_GT(at2048, at512); + } +} + +BOOST_AUTO_TEST_CASE(WireTrimmedQuadricKernels) +{ + using surf::Curve2D; + using surf::Vec3; + std::string error; + + const auto onCylinder = [](double phi, double height) { + return Vec3{2. * std::cos(phi), 2. * std::sin(phi), height}; + }; + + // (1) cylinder wall with a rectangular window (hole) in (phi, h): phi in [2.0, 2.5], h in [-1, 1] + surf::CylindricalBoundedSurface windowed; + const std::vector outer{Curve2D::makeLine({0., -3.}, {surf::kTwoPi, -3.}), + Curve2D::makeLine({surf::kTwoPi, -3.}, {surf::kTwoPi, 3.}), + Curve2D::makeLine({surf::kTwoPi, 3.}, {0., 3.}), + Curve2D::makeLine({0., 3.}, {0., -3.})}; + const std::vector hole{Curve2D::makeLine({2.0, -1.}, {2.5, -1.}), Curve2D::makeLine({2.5, -1.}, {2.5, 1.}), + Curve2D::makeLine({2.5, 1.}, {2.0, 1.}), Curve2D::makeLine({2.0, 1.}, {2.0, -1.})}; + BOOST_REQUIRE(windowed.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kTwoPi, false, + outer, {hole}, error)); + BOOST_CHECK(windowed.containsPointOnSurface(onCylinder(0.5, 0.))); // material + BOOST_CHECK(windowed.containsPointOnSurface(onCylinder(2.25, 2.5))); // material above the window + BOOST_CHECK(!windowed.containsPointOnSurface(onCylinder(2.25, 0.))); // inside the window + BOOST_CHECK(windowed.containsPointOnSurface(onCylinder(2.25, 1.))); // on the window edge (boundary) + + // a radial ray into the window is filtered out; a radial ray into material registers one hit + std::vector hits; + windowed.appendIntersections({0., 0., 0.}, {std::cos(2.25), std::sin(2.25), 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + hits.clear(); + windowed.appendIntersections({0., 0., 0.}, {std::cos(0.5), std::sin(0.5), 0.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits.front().distance, 2.); + + // (2) arc trim: a parametric circle (disk in (phi, h)) centred at (pi, 0), radius 0.5 + surf::CylindricalBoundedSurface arcTrim; + const std::vector arcOuter{Curve2D::makeCircle({surf::kPi, 0.}, 0.5)}; + BOOST_REQUIRE(arcTrim.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + arcOuter, {}, error)); + BOOST_CHECK(arcTrim.containsPointOnSurface(onCylinder(surf::kPi, 0.))); // centre of the disk + BOOST_CHECK(!arcTrim.containsPointOnSurface(onCylinder(surf::kPi, 0.6))); // outside in h + BOOST_CHECK(!arcTrim.containsPointOnSurface(onCylinder(surf::kPi + 0.6, 0.))); // outside in phi + BOOST_CHECK_GT(std::abs(arcTrim.capacityContribution()), 0.); + + // (3) sphere section reproduced as a (phi, theta) rectangle wire must match the scalar section + const auto onSphere = [](double theta, double phi) { + return Vec3{2. * std::sin(theta) * std::cos(phi), 2. * std::sin(theta) * std::sin(phi), 2. * std::cos(theta)}; + }; + surf::SphericalBoundedSurface sphereWire; + BOOST_REQUIRE(sphereWire.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., surf::kHalfPi / 2., surf::kHalfPi, + 0., surf::kHalfPi, false, + paramRectWireCurves(0., surf::kHalfPi, surf::kHalfPi / 2., surf::kHalfPi), {}, + error)); + surf::SphericalBoundedSurface sphereScalar; + BOOST_REQUIRE(sphereScalar.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., surf::kHalfPi / 2., + surf::kHalfPi, 0., surf::kHalfPi, false, error)); + BOOST_CHECK(sphereWire.containsPointOnSurface(onSphere(surf::kPi / 3., surf::kPi / 4.))); // inside the section + BOOST_CHECK(!sphereWire.containsPointOnSurface(onSphere(surf::kPi / 6., surf::kPi / 4.))); // theta too small + BOOST_CHECK(!sphereWire.containsPointOnSurface(onSphere(surf::kPi / 3., 3. * surf::kPi / 4.))); // phi outside + // same story on the sphere: 1e-3 was the grid rule's allowance, not the geometry's + checkClose(sphereWire.capacityContribution(), sphereScalar.capacityContribution(), 1.e-9); + + // (4) a trim spanning more than a full turn in phi is rejected + surf::CylindricalBoundedSurface tooWide; + const std::vector wideOuter{Curve2D::makeLine({0., -1.}, {7., -1.}), Curve2D::makeLine({7., -1.}, {7., 1.}), + Curve2D::makeLine({7., 1.}, {0., 1.}), Curve2D::makeLine({0., 1.}, {0., -1.})}; + BOOST_CHECK(!tooWide.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + wideOuter, {}, error)); +} + +namespace +{ +// Estimate the first fundamental form at (u, v) by central differences of a surface's own +// parametrisation. Checking parametricMetric against this is a proof that the closed form +// describes the map the rest of the kernel actually evaluates -- restating the formula in the +// test would only prove it was copied twice. +template +void checkMetricAgainstFiniteDifference(const surf::BoundedSurface& surface, const PointAt& pointAt, + double uCoord, double vCoord, double tolerance = 1.e-6) +{ + const double step = 1.e-5; + const surf::Vec3 dU = (pointAt(uCoord + step, vCoord) - pointAt(uCoord - step, vCoord)) * (0.5 / step); + const surf::Vec3 dV = (pointAt(uCoord, vCoord + step) - pointAt(uCoord, vCoord - step)) * (0.5 / step); + + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + surface.parametricMetric({uCoord, vCoord}, gUU, gUV, gVV); + checkClose(gUU, dot(dU, dU), tolerance); + checkClose(gUV, dot(dU, dV), tolerance); + checkClose(gVV, dot(dV, dV), tolerance); +} +} // namespace + +// The first fundamental form of every surface family, against the surface's own parametrisation, +// plus the two degeneracies and the cross term the callers of it have to cope with. This is the +// conversion that makes a parametric tolerance mean a length (findings K3, K5, K12, S10). +BOOST_AUTO_TEST_CASE(ParametricMetricIsTheFirstFundamentalForm) +{ + using surf::Vec2; + using surf::Vec3; + std::string error; + + // (1) plane with deliberately non-orthonormal axes: the only family with a cross term, and the + // only one whose (u, v) are not already lengths. + const Vec3 axisU{2., 0., 0.}; + const Vec3 axisV{1., 3., 0.}; // not unit, not orthogonal to axisU + const std::vector unitSquare{{0., 0.}, {1., 0.}, {1., 1.}, {0., 1.}}; + surf::PlanarBoundedSurface plane; + BOOST_REQUIRE(plane.initialize({0.5, -1., 2.}, axisU, axisV, unitSquare, {}, error)); + checkMetricAgainstFiniteDifference(plane, [&](double u, double v) { return plane.toGlobal({u, v}); }, 0.3, 0.7); + { + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + plane.parametricMetric({0., 0.}, gUU, gUV, gVV); + checkClose(gUU, 4.); + checkClose(gUV, 2.); // dot(axisU, axisV) -- zero for every other family + checkClose(gVV, 10.); + // and it really measures 3D length: (du, dv) = (1, 0) spans |axisU| = 2 cm + checkClose(std::sqrt(plane.parametricLengthSqAt({0., 0.}, {1., 0.})), 2.); + checkClose(std::sqrt(plane.parametricLengthSqAt({0., 0.}, {0., 1.})), std::sqrt(10.)); + } + + // (2) curved planar: initialize() insists on an orthonormal frame, so (u, v) are centimetres. + surf::CurvedPlanarBoundedSurface curvedPlane; + BOOST_REQUIRE(curvedPlane.initialize({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, + {surf::Curve2D::makeCircle({0., 0.}, 1.)}, {}, error)); + checkMetricAgainstFiniteDifference( + curvedPlane, [&](double u, double v) { return curvedPlane.toGlobal({u, v}); }, 0.2, -0.4); + + // (3) cylinder, (u, v) = (phi, h). The radius factor is the whole point: the same parametric + // drift is a different distance on a small hole and on a large cylinder. + for (const double radius : {0.01, 100.}) { + surf::CylindricalBoundedSurface cylinder; + BOOST_REQUIRE(cylinder.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -1., 1., 0., surf::kTwoPi, + false, error)); + checkMetricAgainstFiniteDifference(cylinder, [&](double u, double v) { return cylinder.pointAt(u, v); }, 1.1, 0.3, 1.e-4 * radius * radius); + // a 2e-5 rad join drift is 2e-7 cm on the small cylinder and 2e-3 cm on the large one + checkClose(std::sqrt(cylinder.parametricLengthSqAt({1.1, 0.3}, {2.e-5, 0.})), 2.e-5 * radius, 1.e-12); + } + + // (4) sphere, (u, v) = (phi, theta) -- the trim domain's order, the transpose of pointAt's. + surf::SphericalBoundedSurface sphere; + BOOST_REQUIRE(sphere.initialize({1., 2., 3.}, {0., 0., 1.}, {1., 0., 0.}, 2.5, 0., surf::kPi, 0., surf::kTwoPi, + false, error)); + checkMetricAgainstFiniteDifference(sphere, [&](double u, double v) { return sphere.pointAt(v, u); }, 0.9, 1.2); + { + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + // at the pole the azimuth degenerates: a phi separation there spans no distance at all + sphere.parametricMetric({0.9, 0.}, gUU, gUV, gVV); + checkClose(gUU, 0.); + checkClose(gVV, 2.5 * 2.5); + checkClose(sphere.parametricLengthSqAt({0.9, 0.}, {1., 0.}), 0.); + sphere.parametricMetric({0.9, surf::kPi}, gUU, gUV, gVV); + checkClose(gUU, 0.); + // and on the equator it is the full radius + sphere.parametricMetric({0.9, surf::kHalfPi}, gUU, gUV, gVV); + checkClose(gUU, 2.5 * 2.5); + } + + // (5) cone, (u, v) = (phi, h): the azimuthal scale shrinks to zero at the apex, and a step in h + // walks along the slope rather than along the axis. + surf::ConicalBoundedSurface cone; + BOOST_REQUIRE(cone.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 0., 4., 0., 2., 0., surf::kTwoPi, false, + error)); + checkMetricAgainstFiniteDifference(cone, [&](double u, double v) { return cone.pointAt(u, v); }, 2.0, 1.3); + { + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + cone.parametricMetric({2.0, 0.}, gUU, gUV, gVV); // the apex, where r(h) = 0 + checkClose(gUU, 0.); + checkClose(gVV, 1. + 2. * 2.); // slope = (4 - 0) / (2 - 0) + checkClose(cone.parametricLengthSqAt({2.0, 0.}, {1., 0.}), 0.); + cone.parametricMetric({2.0, 2.}, gUU, gUV, gVV); // the wide end, r = 4 + checkClose(gUU, 16.); + } + + // (6) torus, (u, v) = (phiRing, phiTube): the ring scale runs from R - r to R + r around the + // tube, so it is the one family whose gUU varies without any degeneracy. + surf::TorusBoundedSurface torus; + BOOST_REQUIRE(torus.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 5., 1.5, 0., surf::kTwoPi, 0., + surf::kTwoPi, false, error)); + checkMetricAgainstFiniteDifference(torus, [&](double u, double v) { return torus.pointAt(u, v); }, 0.7, 2.1); + { + double gUU = 0.; + double gUV = 0.; + double gVV = 0.; + torus.parametricMetric({0.7, 0.}, gUU, gUV, gVV); // outside of the ring + checkClose(gUU, 6.5 * 6.5); + checkClose(gVV, 1.5 * 1.5); + torus.parametricMetric({0.7, surf::kPi}, gUU, gUV, gVV); // inside of the ring + checkClose(gUU, 3.5 * 3.5); + } +} + +// The join tolerance is a length, so the same parametric drift is accepted on a small cylinder +// and refused on a large one. Today's rule cannot tell them apart, which is the whole of K3 -- and +// it is the synthetic form of the measured ST1829909_01 loader rejection (six joins under 3e-5 rad +// on cylinder trims, negligible in arc length, read as three times over a 1e-5 "tolerance"). +BOOST_AUTO_TEST_CASE(WireJoinToleranceIsALength) +{ + using surf::Curve2D; + std::string error; + + // A rectangular (phi, h) trim whose last edge stops `drift` radians short of closing the loop. + const auto trimWithPhiDrift = [](double drift) { + return std::vector{Curve2D::makeLine({0.2, -1.}, {1.2, -1.}), Curve2D::makeLine({1.2, -1.}, {1.2, 1.}), + Curve2D::makeLine({1.2, 1.}, {0.2 + drift, 1.}), + Curve2D::makeLine({0.2 + drift, 1.}, {0.2 + drift, -1.})}; + }; + const auto acceptsDrift = [&](double radius, double drift) { + surf::CylindricalBoundedSurface cylinder; + return cylinder.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -1., 1., 0., surf::kTwoPi, false, + trimWithPhiDrift(drift), {}, error); + }; + + // The same 2e-5 rad drift, on two radii: 2e-7 cm of arc on the small cylinder against 2e-3 cm + // on the large one. One is a rounding error, the other is a real gap. + BOOST_CHECK(acceptsDrift(0.01, 2.e-5)); // the old rule refused this: 2e-5 > 1e-5, radius unseen + BOOST_CHECK(!acceptsDrift(100., 2.e-5)); + // and the discrimination really is the radius: give the large cylinder a drift small enough to + // span the same 2e-7 cm and it is accepted; give the small one a gap of 2e-5 cm and it is not. + BOOST_CHECK(acceptsDrift(100., 2.e-9)); + BOOST_CHECK(!acceptsDrift(0.01, 2.e-3)); + // On a cylinder of radius 1 the two rules coincide up to the change of constant -- which is the + // only configuration the old one was ever right on, and then only by accident. + BOOST_CHECK(acceptsDrift(1., 5.e-7)); + BOOST_CHECK(!acceptsDrift(1., 5.e-6)); // the old rule accepted this: 5e-6 < 1e-5 +} + +// K12: the polygon and curve wire types are fed by the same extractor with the same per-endpoint +// precision, and now judge a join by the same rule. They used to differ by four orders of +// magnitude -- 1e-9 for polygons against 1e-5 for curves -- and in incompatible units. +BOOST_AUTO_TEST_CASE(PolygonAndCurveWiresShareOneJoinRule) +{ + using surf::SurfaceEdge; + using surf::Vec2; + using surf::WireRole; + using surf::WireStatus; + + // A square whose last edge ends `gap` short of the first edge's start, in a domain where + // (u, v) are already centimetres (the default identity metric). + const auto polygonAcceptsGap = [](double gap) { + const std::vector edges{{{0., 0.}, {1., 0.}}, + {{1., 0.}, {1., 1.}}, + {{1., 1.}, {0., 1.}}, + {{0., 1.}, {gap, 0.}}}; + surf::SurfaceWire wire; + WireStatus status = WireStatus::Valid; + return wire.initializeFromEdges(edges, WireRole::Outer, status, {}); + }; + const auto curveAcceptsGap = [](double gap) { + const std::vector curves{surf::Curve2D::makeLine({0., 0.}, {1., 0.}), + surf::Curve2D::makeLine({1., 0.}, {1., 1.}), + surf::Curve2D::makeLine({1., 1.}, {0., 1.}), + surf::Curve2D::makeLine({0., 1.}, {gap, 0.})}; + surf::CurveWire wire; + WireStatus status = WireStatus::Valid; + return wire.initialize(curves, WireRole::Outer, status, {}); + }; + + // inside the 1e-6 cm tolerance. 1e-8 and 1e-7 are the discriminating cases: the polygon wire + // used to refuse them at 1e-9 while the curve wire accepted them at 1e-5. + for (const double gap : {0., 1.e-8, 1.e-7}) { + BOOST_CHECK(polygonAcceptsGap(gap)); + BOOST_CHECK(curveAcceptsGap(gap)); + } + // outside it. 1e-5 is the mirror case: the curve wire used to accept it and the polygon not. + for (const double gap : {1.e-5, 1.e-4}) { + BOOST_CHECK(!polygonAcceptsGap(gap)); + BOOST_CHECK(!curveAcceptsGap(gap)); + } +} + +namespace +{ +// Helpers writing the surface sidecar binary format documented in +// Detectors/CADSupport/doc/reference/BVHSurfaceSolid.md. Kept independent of the loader implementation so the +// test is a true round-trip through the documented byte layout. +void appendU32(std::vector& bytes, uint32_t value) +{ + const char* raw = reinterpret_cast(&value); + bytes.insert(bytes.end(), raw, raw + sizeof(value)); +} + +void appendDoubles(std::vector& bytes, std::initializer_list values) +{ + for (const double value : values) { + const char* raw = reinterpret_cast(&value); + bytes.insert(bytes.end(), raw, raw + sizeof(value)); + } +} + +// The fixed header. Version 1 is the three-uint32 form; version 2 appends the model tolerance in +// cm. The default stays at version 1 on purpose: every sidecar test below then doubles as a +// regression test that the reader still accepts the older format. +void appendSidecarHeader(std::vector& bytes, uint32_t nSurfaces, uint32_t version = 1, + double modelTolerance = 0., uint32_t nModelEdges = 0) +{ + bytes.insert(bytes.end(), {'O', '2', 'S', 'S'}); + appendU32(bytes, version); + appendU32(bytes, nSurfaces); + appendU32(bytes, 0); // reserved + if (version >= 2) { + appendDoubles(bytes, {modelTolerance}); + } + if (version >= 3) { + appendU32(bytes, nModelEdges); // size of the model's edge table + } +} + +// plane record (type 1) with a single rectangular outer wire of four line-segment edges +void appendPlaneRecord(std::vector& bytes, const FaceFrame& frame) +{ + appendU32(bytes, 1); // surfaceType plane + appendU32(bytes, 0); // flags + appendU32(bytes, 9); // nParams + appendDoubles(bytes, {frame.origin[0], frame.origin[1], frame.origin[2], frame.axisU[0], frame.axisU[1], + frame.axisU[2], frame.axisV[0], frame.axisV[1], frame.axisV[2]}); + appendU32(bytes, 1); // nWires + appendU32(bytes, 0); // wireRole outer + appendU32(bytes, 4); // nEdges + const double extentU = frame.extentU; + const double extentV = frame.extentV; + const std::array, 4> edges{{{0., 0., extentU, 0.}, + {extentU, 0., extentU, extentV}, + {extentU, extentV, 0., extentV}, + {0., extentV, 0., 0.}}}; + for (const auto& edge : edges) { + appendU32(bytes, 0); // curveType line + appendU32(bytes, 4); // nCurveParams + appendDoubles(bytes, {edge[0], edge[1], edge[2], edge[3]}); + } +} + +// plane record (type 1) for a disk/annulus cap: one full-circle outer arc wire, plus a +// clockwise inner arc wire when holeRadius > 0. Exercises the arc-wire reader path. +void appendDiskPlaneRecord(std::vector& bytes, const Point3D& center, const Point3D& axisU, + const Point3D& axisV, double radius, double holeRadius = 0.) +{ + appendU32(bytes, 1); // surfaceType plane + appendU32(bytes, 0); // flags + appendU32(bytes, 9); // nParams + appendDoubles(bytes, {center[0], center[1], center[2], axisU[0], axisU[1], axisU[2], axisV[0], axisV[1], axisV[2]}); + const uint32_t nWires = holeRadius > 0. ? 2u : 1u; + appendU32(bytes, nWires); + appendU32(bytes, 0); // outer wire role + appendU32(bytes, 1); // one edge + appendU32(bytes, 1); // curveType arc + appendU32(bytes, 5); // nCurveParams + appendDoubles(bytes, {0., 0., radius, 0., 2. * surf::kPi}); // cu cv radius phiStart phiSweep (CCW full circle) + if (holeRadius > 0.) { + appendU32(bytes, 1); // inner wire role + appendU32(bytes, 1); + appendU32(bytes, 1); // arc + appendU32(bytes, 5); + appendDoubles(bytes, {0., 0., holeRadius, 0., -2. * surf::kPi}); // clockwise hole + } +} + +std::filesystem::path writeSidecarFile(const std::string& name, const std::vector& bytes) +{ + const auto path = std::filesystem::temp_directory_path() / name; + std::ofstream out(path, std::ios::binary); + out.write(bytes.data(), static_cast(bytes.size())); + BOOST_REQUIRE(out.good()); + return path; +} +} // namespace + +BOOST_AUTO_TEST_CASE(SurfaceSidecarRoundTrip) +{ + // planar box: six plane records with polygon wires, loaded and compared against TGeoBBox + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + + std::vector boxBytes; + appendSidecarHeader(boxBytes, 6); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + appendPlaneRecord(boxBytes, boxFaceFrame(faceIndex, halfX, halfY, halfZ)); + } + const auto boxPath = writeSidecarFile("o2_sidecar_roundtrip_box.bin", boxBytes); + + SurfaceSolid box("sidecarBox"); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(boxPath.string(), box)); + std::filesystem::remove(boxPath); + BOOST_CHECK_EQUAL(box.GetNsurfaces(), 6); + box.CloseShape(); + BOOST_CHECK(box.IsClosed()); + BOOST_CHECK(box.IsOrientationConsistent()); + + TGeoBBox referenceBox("referenceBox", halfX, halfY, halfZ); + compareContainsGrid(box, referenceBox, 4., 7); + compareDistance(box, referenceBox, {5., 0.5, 0.5}, {-1., 0., 0.}); + compareDistance(box, referenceBox, {0., 0., 0.}, unitDirection(1., 1., 1.)); + checkClose(box.Capacity(), referenceBox.Capacity(), 1.e-9); + + // quadric + arc-wire caps: closed cylinder (lateral wall + two disk caps) against TGeoTube + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + + std::vector tubeBytes; + appendSidecarHeader(tubeBytes, 3); + appendU32(tubeBytes, 2); // surfaceType cylinder + appendU32(tubeBytes, 0); // flags (outer wall) + appendU32(tubeBytes, 14); // nParams + appendDoubles(tubeBytes, {0., 0., 0., 0., 0., 1., 1., 0., 0., radius, -halfHeight, halfHeight, 0., 2. * surf::kPi}); + appendU32(tubeBytes, 0); // nWires + // caps as arc-wire plane records: outward normal is axisU x axisV, so the bottom cap flips axisV + appendDiskPlaneRecord(tubeBytes, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius); + appendDiskPlaneRecord(tubeBytes, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius); + const auto tubePath = writeSidecarFile("o2_sidecar_roundtrip_tube.bin", tubeBytes); + + SurfaceSolid tube("sidecarTube"); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(tubePath.string(), tube)); + std::filesystem::remove(tubePath); + BOOST_CHECK_EQUAL(tube.GetNsurfaces(), 3); + tube.CloseShape(); + BOOST_CHECK(tube.IsClosed()); + BOOST_CHECK(tube.IsOrientationConsistent()); + + TGeoTube referenceTube("referenceTube", 0., radius, halfHeight); + compareContainsGrid(tube, referenceTube, 4., 7); + compareDistance(tube, referenceTube, {5., 0.5, 1.}, {-1., 0., 0.}); + compareDistance(tube, referenceTube, {0., 0., 0.}, unitDirection(1., 1., 1.)); + checkClose(tube.Capacity(), referenceTube.Capacity(), 1.e-9); + + // malformed input must be rejected without loading surfaces + const auto badPath = writeSidecarFile("o2_sidecar_bad_magic.bin", {'X', 'X', 'X', 'X', 0, 0, 0, 0}); + SurfaceSolid bad("sidecarBad"); + BOOST_CHECK(!o2::cad::LoadSurfaceSolid(badPath.string(), bad)); + std::filesystem::remove(badPath); + BOOST_CHECK_EQUAL(bad.GetNsurfaces(), 0); + BOOST_CHECK(!o2::cad::LoadSurfaceSolid("/nonexistent/o2_sidecar_missing.bin", bad)); + + // truncated file: valid header announcing a surface that never follows + std::vector truncatedBytes; + appendSidecarHeader(truncatedBytes, 1); + const auto truncatedPath = writeSidecarFile("o2_sidecar_truncated.bin", truncatedBytes); + SurfaceSolid truncated("sidecarTruncated"); + BOOST_CHECK(!o2::cad::LoadSurfaceSolid(truncatedPath.string(), truncated)); + std::filesystem::remove(truncatedPath); +} + +// Sidecar version 2 carries the source model's own tolerance, so the kernel stops guessing what +// epsilon two faces of an imported solid should agree to. Both versions must load: a v1 file is a +// v2 file that simply does not state one. +BOOST_AUTO_TEST_CASE(SidecarModelToleranceRoundTrip) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + const auto boxBytesWithHeader = [&](uint32_t version, double modelTolerance) { + std::vector bytes; + appendSidecarHeader(bytes, 6, version, modelTolerance); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + appendPlaneRecord(bytes, boxFaceFrame(faceIndex, halfX, halfY, halfZ)); + } + return bytes; + }; + const auto loadFrom = [](const char* name, const std::vector& bytes, SurfaceSolid& solid) { + const auto path = writeSidecarFile(name, bytes); + const bool ok = o2::cad::LoadSurfaceSolid(path.string(), solid); + std::filesystem::remove(path); + return ok; + }; + + // version 2: the written tolerance reaches the solid untouched, and survives to CloseShape + SurfaceSolid v2("sidecarV2"); + BOOST_REQUIRE(loadFrom("o2_sidecar_v2.bin", boxBytesWithHeader(2, 3.5e-5), v2)); + BOOST_CHECK_EQUAL(v2.GetNsurfaces(), 6); + checkClose(v2.GetModelTolerance(), 3.5e-5, 1.e-18); + v2.CloseShape(); + checkClose(v2.GetModelTolerance(), 3.5e-5, 1.e-18); + + // a v2 file may still state nothing, and "nothing" is zero rather than an invented number + SurfaceSolid v2Silent("sidecarV2Silent"); + BOOST_REQUIRE(loadFrom("o2_sidecar_v2_silent.bin", boxBytesWithHeader(2, 0.), v2Silent)); + BOOST_CHECK_EQUAL(v2Silent.GetModelTolerance(), 0.); + + // version 1: still loads, and gets the reader's documented fallback rather than zero + SurfaceSolid v1("sidecarV1"); + BOOST_REQUIRE(loadFrom("o2_sidecar_v1.bin", boxBytesWithHeader(1, 0.), v1)); + BOOST_CHECK_EQUAL(v1.GetNsurfaces(), 6); + checkClose(v1.GetModelTolerance(), 1.e-6, 1.e-18); + + // a solid nobody told anything keeps zero: "not stated" is not the same as "the fallback" + SurfaceSolid handBuilt("handBuilt"); + BOOST_CHECK_EQUAL(handBuilt.GetModelTolerance(), 0.); + handBuilt.SetModelTolerance(1.e-4); + checkClose(handBuilt.GetModelTolerance(), 1.e-4, 1.e-18); + handBuilt.SetModelTolerance(-1.); // refused, and the previous value stands + checkClose(handBuilt.GetModelTolerance(), 1.e-4, 1.e-18); + + // version 3 is understood now (it is a version-2 file that also states its edge identities); + // it is exercised in SidecarV3EdgeIdentityRoundTrip below. + + // an unknown version is refused rather than reinterpreted + SurfaceSolid v4("sidecarV4"); + BOOST_CHECK(!loadFrom("o2_sidecar_v4.bin", boxBytesWithHeader(4, 1.e-5), v4)); + BOOST_CHECK_EQUAL(v4.GetNsurfaces(), 0); + + // and a v2 header that stops before its tolerance is a truncated file, not a v1 one + std::vector stump; + stump.insert(stump.end(), {'O', '2', 'S', 'S'}); + appendU32(stump, 2); + appendU32(stump, 6); + appendU32(stump, 0); + SurfaceSolid stumped("sidecarV2Stump"); + BOOST_CHECK(!loadFrom("o2_sidecar_v2_stump.bin", stump, stumped)); + BOOST_CHECK_EQUAL(stumped.GetNsurfaces(), 0); +} + +BOOST_AUTO_TEST_CASE(WireTrimmedSidecarRoundTrip) +{ + // a cylinder record carrying a (line) trim wire block in its (phi, h) domain must load through + // the wire-taking Add* overload and navigate like the equivalent scalar cylinder + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + + std::vector bytes; + appendSidecarHeader(bytes, 3); + appendU32(bytes, 2); // surfaceType cylinder + appendU32(bytes, 0); // flags (outer wall) + appendU32(bytes, 14); // nParams + appendDoubles(bytes, {0., 0., 0., 0., 0., 1., 1., 0., 0., radius, -halfHeight, halfHeight, 0., 2. * surf::kPi}); + appendU32(bytes, 1); // nWires + appendU32(bytes, 0); // outer wire role + appendU32(bytes, 4); // nEdges + const std::array, 4> edges{{{0., -halfHeight, 2. * surf::kPi, -halfHeight}, + {2. * surf::kPi, -halfHeight, 2. * surf::kPi, halfHeight}, + {2. * surf::kPi, halfHeight, 0., halfHeight}, + {0., halfHeight, 0., -halfHeight}}}; + for (const auto& edge : edges) { + appendU32(bytes, 0); // curveType line + appendU32(bytes, 4); // nCurveParams + appendDoubles(bytes, {edge[0], edge[1], edge[2], edge[3]}); + } + appendDiskPlaneRecord(bytes, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius); + appendDiskPlaneRecord(bytes, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius); + const auto path = writeSidecarFile("o2_sidecar_wiretrim_cylinder.bin", bytes); + + SurfaceSolid solid("sidecarWireCylinder"); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(path.string(), solid)); + std::filesystem::remove(path); + BOOST_CHECK_EQUAL(solid.GetNsurfaces(), 3); + solid.CloseShape(); + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoTube reference("wireTrimSidecarTube", 0., radius, halfHeight); + compareContainsGrid(solid, reference, 4., 7); + compareDistance(solid, reference, {5., 0.5, 1.}, {-1., 0., 0.}); + compareDistance(solid, reference, {0., 0., 0.}, unitDirection(1., 1., 1.)); + checkClose(solid.Capacity(), reference.Capacity(), 1.e-6); +} + +BOOST_AUTO_TEST_CASE(TorusSidecarRoundTrip) +{ + // a full-torus record (surfaceType 5, 15 params, empty wire block) must load through the + // scalar AddToroidalSurface path and navigate like TGeoTorus + constexpr double majorR = 3.; + constexpr double minorR = 1.; + + std::vector bytes; + appendSidecarHeader(bytes, 1); + appendU32(bytes, 5); // surfaceType torus + appendU32(bytes, 0); // flags (outer wall) + appendU32(bytes, 15); // nParams + appendDoubles(bytes, {0., 0., 0., 0., 0., 1., 1., 0., 0., majorR, minorR, 0., 2. * surf::kPi, 0., 2. * surf::kPi}); + appendU32(bytes, 0); // nWires (full torus: scalar path) + const auto path = writeSidecarFile("o2_sidecar_roundtrip_torus.bin", bytes); + + SurfaceSolid solid("sidecarTorus"); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(path.string(), solid)); + std::filesystem::remove(path); + BOOST_CHECK_EQUAL(solid.GetNsurfaces(), 1); + solid.CloseShape(); + BOOST_CHECK(solid.IsClosed()); + BOOST_CHECK(solid.IsOrientationConsistent()); + + TGeoTorus reference("sidecarTorusRef", majorR, 0., minorR); + compareContainsGrid(solid, reference, 4.5, 9); + checkClose(solid.Capacity(), reference.Capacity(), 1.e-7); +} + +// Wire-join gaps are judged against the tolerance the sidecar itself declares (the +// version-2 model tolerance), with the extractor-precision constant as the floor -- not against +// the bare constant when the model states it cannot do better. This is the ST1829909_01 +// rejection: surface 1006's bspline->line join gaps by 5.41e-6 cm on a model that declares +// 4.7e-4 cm, and the 1e-6 constant "is a fallback, not a measurement of the model". The band +// must hold in the loader *and* in the kernel's own wire construction, or the loader would +// accept a wire that Add*Surface rejects moments later. +BOOST_AUTO_TEST_CASE(StreamY_LoaderHonoursTheDeclaredModelTolerance) +{ + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + // one seam offset purely in v (cm), so the 3D gap equals the offset on any cylinder: + // over the 1e-6 cm extractor floor, under the model tolerance the loading case declares + constexpr double joinGap = 5.e-6; + + const auto cylinderBytes = [&](uint32_t version, double modelTolerance) { + std::vector bytes; + appendSidecarHeader(bytes, 3, version, modelTolerance); + appendU32(bytes, 2); // surfaceType cylinder + appendU32(bytes, 0); // flags (outer wall) + appendU32(bytes, 14); // nParams + appendDoubles(bytes, {0., 0., 0., 0., 0., 1., 1., 0., 0., radius, -halfHeight, halfHeight, 0., 2. * surf::kPi}); + appendU32(bytes, 1); // nWires + appendU32(bytes, 0); // outer wire role + appendU32(bytes, 5); // nEdges + // The bottom edge is split in two and the second half starts joinGap off the first half's + // end -- a mid-wire join like surface 1006's bspline->line seam. Deliberately *not* at the + // phi-wrap corner: the full-turn seam pair (u = 0 vs u = 2*pi) must stay exactly coincident + // to cancel in the rim chaining, and the real rejection's face spans only half a turn. + const std::array, 5> edges{ + {{0., -halfHeight, surf::kPi, -halfHeight}, + {surf::kPi, -halfHeight + joinGap, 2. * surf::kPi, -halfHeight}, // starts joinGap off edge 0's end + {2. * surf::kPi, -halfHeight, 2. * surf::kPi, halfHeight}, + {2. * surf::kPi, halfHeight, 0., halfHeight}, + {0., halfHeight, 0., -halfHeight}}}; + for (const auto& edge : edges) { + appendU32(bytes, 0); // curveType line + appendU32(bytes, 4); // nCurveParams + appendDoubles(bytes, {edge[0], edge[1], edge[2], edge[3]}); + } + appendDiskPlaneRecord(bytes, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius); + appendDiskPlaneRecord(bytes, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius); + return bytes; + }; + const auto loadFrom = [](const char* name, const std::vector& bytes, SurfaceSolid& solid) { + const auto path = writeSidecarFile(name, bytes); + const bool ok = o2::cad::LoadSurfaceSolid(path.string(), solid); + std::filesystem::remove(path); + return ok; + }; + + // a model declaring 1e-4 cm: the 5e-6 cm seam is within the model's own statement, so it loads, + // closes, and navigates like the gap-free tube (the kernel canonicalizes each seam on accept) + SurfaceSolid declared("sidecarJoinDeclared"); + BOOST_REQUIRE(loadFrom("o2_sidecar_join_declared.bin", cylinderBytes(2, 1.e-4), declared)); + BOOST_CHECK_EQUAL(declared.GetNsurfaces(), 3); + declared.CloseShape(); + BOOST_CHECK(declared.IsClosed()); + BOOST_CHECK(declared.IsOrientationConsistent()); + TGeoTube reference("declaredToleranceTube", 0., radius, halfHeight); + compareContainsGrid(declared, reference, 4., 7); + compareDistance(declared, reference, {5., 0.5, 1.}, {-1., 0., 0.}); + + // a v1 file states nothing, so the extractor-precision floor stands and the same seam is open + SurfaceSolid silent("sidecarJoinSilent"); + BOOST_CHECK(!loadFrom("o2_sidecar_join_silent.bin", cylinderBytes(1, 0.), silent)); + BOOST_CHECK_EQUAL(silent.GetNsurfaces(), 0); + + // a declared tolerance below the gap does not save it: the model itself calls the seam open + SurfaceSolid tight("sidecarJoinTight"); + BOOST_CHECK(!loadFrom("o2_sidecar_join_tight.bin", cylinderBytes(2, 2.e-6), tight)); + BOOST_CHECK_EQUAL(tight.GetNsurfaces(), 0); +} + +namespace +{ +// Append a plane record whose rectangular outer wire has its bottom edge as a degree-3 B-spline +// with collinear poles — geometrically identical to the straight edge, so the box still closes, +// but it exercises the whole B-spline sidecar pipeline (curveType 2 reader -> kernel). +void appendBSplineEdgePlaneRecord(std::vector& bytes, const FaceFrame& frame) +{ + appendU32(bytes, 1); // surfaceType plane + appendU32(bytes, 0); // flags + appendU32(bytes, 9); // nParams + appendDoubles(bytes, {frame.origin[0], frame.origin[1], frame.origin[2], frame.axisU[0], frame.axisU[1], + frame.axisU[2], frame.axisV[0], frame.axisV[1], frame.axisV[2]}); + appendU32(bytes, 1); // nWires + appendU32(bytes, 0); // wireRole outer + appendU32(bytes, 4); // nEdges + const double extentU = frame.extentU; + const double extentV = frame.extentV; + // edge 0: collinear cubic B-spline from (0, 0) to (extentU, 0) + appendU32(bytes, 2); // curveType bspline + appendU32(bytes, 22); // nCurveParams = 2 + 2*4 + 4 + 8 + appendDoubles(bytes, {3., 4., // degree, nPoles + 0., 0., extentU / 3., 0., 2. * extentU / 3., 0., extentU, 0., // poles + 1., 1., 1., 1., // weights + 0., 0., 0., 0., 1., 1., 1., 1.}); // clamped knots + const std::array, 3> lines{ + {{extentU, 0., extentU, extentV}, {extentU, extentV, 0., extentV}, {0., extentV, 0., 0.}}}; + for (const auto& edge : lines) { + appendU32(bytes, 0); // curveType line + appendU32(bytes, 4); // nCurveParams + appendDoubles(bytes, {edge[0], edge[1], edge[2], edge[3]}); + } +} + +// A rational quadratic B-spline (NURBS) quarter circle from angle a0 to a0 + pi/2, in the (u, v) +// domain, centred at (cu, cv) with radius r. Four of these form an exact circle. +/// A full circle as ONE closed rational B-spline (the standard 9-pole degree-2 NURBS circle), +/// i.e. a wire whose single edge starts and ends at the same point. This is the shape a CAD +/// kernel writes for a tube-tube intersection curve, and it is structurally different from the +/// same circle spelled as four separate quarter arcs. +surf::Curve2D fullCircleBSpline(double cu, double cv, double r) +{ + const double w = std::sqrt(0.5); + const std::vector poles{{cu + r, cv}, {cu + r, cv + r}, {cu, cv + r}, {cu - r, cv + r}, {cu - r, cv}, {cu - r, cv - r}, {cu, cv - r}, {cu + r, cv - r}, {cu + r, cv}}; + return surf::Curve2D::makeBSpline(2, poles, {1., w, 1., w, 1., w, 1., w, 1.}, + {0., 0., 0., 1., 1., 2., 2., 3., 3., 4., 4., 4.}); +} + +surf::Curve2D quarterCircleBSpline(double cu, double cv, double r, double a0) +{ + const double a1 = a0 + surf::kHalfPi; + const double aMid = 0.5 * (a0 + a1); + const std::vector poles{{cu + r * std::cos(a0), cv + r * std::sin(a0)}, + {cu + r * std::sqrt(2.) * std::cos(aMid), cv + r * std::sqrt(2.) * std::sin(aMid)}, + {cu + r * std::cos(a1), cv + r * std::sin(a1)}}; + return surf::Curve2D::makeBSpline(2, poles, {1., std::sqrt(0.5), 1.}, {0., 0., 0., 1., 1., 1.}); +} +} // namespace + +// K5: the on-boundary band has to be as wide as the representation it measures against, and +// winding and distance have to measure against the same polyline. +BOOST_AUTO_TEST_CASE(BoundaryBandMatchesTheRepresentation) +{ + using surf::Vec2; + using surf::WireClassification; + using surf::WireRole; + using surf::WireStatus; + std::string error; + + // A loop of lines and arcs is held exactly and claims no width of its own... + surf::CurveWire exactWire; + WireStatus status = WireStatus::Valid; + BOOST_REQUIRE(exactWire.initialize({surf::Curve2D::makeCircle({0., 0.}, 1.)}, WireRole::Outer, status)); + BOOST_CHECK_EQUAL(exactWire.representationTolerance(), 0.); + + // ...while a B-spline loop is only as good as the polyline it is flattened to. + surf::CurveWire splineWire; + BOOST_REQUIRE(splineWire.initialize({fullCircleBSpline(0., 0., 1.)}, WireRole::Outer, status)); + checkClose(splineWire.representationTolerance(), surf::kBSplineFlatness, 1.e-18); + + // The Boundary state must therefore be reachable for a B-spline trim. It was not: a 1e-9 band + // around a 1e-5 polyline is noise, so a point this close to the curve used to come back Inside + // or Outside by coin flip. + const double justInsideTheBand = 0.5 * surf::kBSplineFlatness; + BOOST_CHECK(splineWire.classify({1. - justInsideTheBand, 0.}) == WireClassification::Boundary); + BOOST_CHECK(splineWire.classify({1. + justInsideTheBand, 0.}) == WireClassification::Boundary); + // and well outside the band the answer is decided again, in both directions + BOOST_CHECK(splineWire.classify({0.5, 0.}) == WireClassification::Inside); + BOOST_CHECK(splineWire.classify({2.0, 0.}) == WireClassification::Outside); + // the exact loop keeps its narrow band: the same offset is decidable there + BOOST_CHECK(exactWire.classify({1. - justInsideTheBand, 0.}) == WireClassification::Inside); + + // The band is a length, so on a surface that stretches the domain it narrows in parametric + // terms. A 100 cm cylinder resolves 1e-9 cm at 1e-11 rad, not at 1e-9 rad. + surf::CylindricalBoundedSurface bigCylinder; + BOOST_REQUIRE(bigCylinder.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 100., -1., 1., 0., surf::kTwoPi, + false, error)); + const auto bigMetric = surf::parametricMetricOf(bigCylinder); + surf::CurveWire squareWire; + BOOST_REQUIRE(squareWire.initialize({surf::Curve2D::makeLine({0., -1.}, {1., -1.}), + surf::Curve2D::makeLine({1., -1.}, {1., 1.}), + surf::Curve2D::makeLine({1., 1.}, {0., 1.}), + surf::Curve2D::makeLine({0., 1.}, {0., -1.})}, + WireRole::Outer, status, bigMetric)); + // 1e-10 rad is 1e-8 cm on this cylinder -- outside a 1e-9 cm band, so the point is decidable + BOOST_CHECK(squareWire.classify({0.5, 1. - 1.e-10}, bigMetric) == WireClassification::Inside); + // 1e-12 rad is 1e-10 cm, inside it + BOOST_CHECK(squareWire.classify({0.5, 1. - 1.e-12}, bigMetric) == WireClassification::Boundary); + + // One polyline: a wire fixes one vertex value per seam, and both the winding polyline and the + // point-to-curve distance are built from it. Give two quarter arcs endpoints that differ within + // the join tolerance and the wire must still agree with itself about where its boundary is. + const double seamDrift = 4.e-7; + surf::Curve2D first = quarterCircleBSpline(0., 0., 1., 0.); + surf::Curve2D second = quarterCircleBSpline(0., 0., 1., surf::kHalfPi); + surf::Curve2D third = quarterCircleBSpline(0., 0., 1., surf::kPi); + surf::Curve2D fourth = quarterCircleBSpline(0., 0., 1., 3. * surf::kHalfPi); + second.poles.front() = {second.poles.front().uCoord + seamDrift, second.poles.front().vCoord}; + surf::CurveWire driftedWire; + BOOST_REQUIRE(driftedWire.initialize({first, second, third, fourth}, WireRole::Outer, status)); + for (const auto& curve : driftedWire.curves) { + BOOST_CHECK(curve.hasCanonicalEndpoints); + } + // every curve now begins exactly where its predecessor ends -- there is one boundary, not two + for (size_t index = 0; index < driftedWire.curves.size(); ++index) { + const Vec2 thisEnd = driftedWire.curves[index].loopEnd(); + const Vec2 nextStart = driftedWire.curves[(index + 1) % driftedWire.curves.size()].loopStart(); + BOOST_CHECK_EQUAL(thisEnd.uCoord, nextStart.uCoord); + BOOST_CHECK_EQUAL(thisEnd.vCoord, nextStart.vCoord); + } + // and the polyline the winding walks is the one the distance measures against + for (const auto& curve : driftedWire.curves) { + const auto& polyline = curve.bsplineSamples(); + BOOST_REQUIRE(polyline.size() >= 2); + BOOST_CHECK_EQUAL(polyline.front().uCoord, curve.loopStart().uCoord); + BOOST_CHECK_EQUAL(polyline.front().vCoord, curve.loopStart().vCoord); + BOOST_CHECK_EQUAL(polyline.back().uCoord, curve.loopEnd().uCoord); + BOOST_CHECK_EQUAL(polyline.back().vCoord, curve.loopEnd().vCoord); + } + BOOST_CHECK(driftedWire.classify({0., 0.}) == WireClassification::Inside); + BOOST_CHECK(driftedWire.classify({3., 0.}) == WireClassification::Outside); +} + +// The other half of the on-boundary band check. +// +// BoundaryBandMatchesTheRepresentation above pins the *width* of the band. This pins what happens +// to a ray that lands in it. Resolving Boundary as "inside the trim" is a tie-break, not a fact, +// and it is one-sided: the patch keeps a sliver of the band's width past its true trim curve. On a +// Boolean seam that sliver lies in the solid's interior, where a crossing must not be counted, so +// a ray through it gains a spurious crossing and Contains() flips. +// +// Measured on cyl_cross_cyl (two unit cylinders fused, whose seam is transcendental in either +// face's chart, so it has to be carried as a B-spline): every one of 1440 sampled positions along +// the true seam overhangs by 1.0e-5 to 1.9e-5 cm and *none* undercuts -- the floor being the band +// itself and the excess the polyline flattening. That is the single direction-dependent point the +// section 4.2 sweep found, and it is not the root-finding defect (K6) it was filed as. +// +// The kernel cannot remove the sliver -- the data does not say where the seam is to better than +// this -- so it labels it instead, and Contains() re-aims when a shot rests on one. Hence the +// contract here: the flag is set exactly when the answer came from the tie-break, and is *not* +// set for a hit the trim decides on its own, because a flag that fired everywhere would put every +// query on the voting path. +BOOST_AUTO_TEST_CASE(TrimBoundaryHitsAreFlaggedAsAmbiguous) +{ + using surf::Curve2D; + std::string error; + + // a cylinder of radius 2 carrying a circular B-spline window of radius 0.5 in (phi, h) + const double centrePhi = surf::kPi; + const double trimRadius = 0.5; + surf::CylindricalBoundedSurface splineTrimmed; + BOOST_REQUIRE(splineTrimmed.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, + false, + {quarterCircleBSpline(centrePhi, 0., trimRadius, 0.), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kHalfPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, 3. * surf::kHalfPi)}, + {}, error)); + + // the same window held exactly, as one arc: it claims no width, so nothing is ever ambiguous + surf::CylindricalBoundedSurface arcTrimmed; + BOOST_REQUIRE(arcTrimmed.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + {Curve2D::makeCircle({centrePhi, 0.}, trimRadius)}, {}, error)); + + // a radial ray that meets the wall at azimuth phi, h = 0 + const auto hitAt = [](const surf::CylindricalBoundedSurface& surface, double phi) { + std::vector hits; + surface.appendIntersections({0., 0., 0.}, {std::cos(phi), std::sin(phi), 0.}, 0., 1.e30, hits); + return hits; + }; + + // The band on this surface is the representation's own tolerance: 1e-5 in (phi, h), since the + // length floor kTolerance / maxScale is 1e-9 / 2 and loses. + const double band = surf::kBSplineFlatness; + const double justInside = 0.5 * band; + + // 1. well inside the window the trim decides by itself -- accepted, and NOT flagged + { + const auto hits = hitAt(splineTrimmed, centrePhi); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + BOOST_CHECK(!hits.front().onTrimBoundary); + } + // 2. inside the window but within the band of its edge -- accepted, and flagged + { + const auto hits = hitAt(splineTrimmed, centrePhi + trimRadius - justInside); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + BOOST_CHECK(hits.front().onTrimBoundary); + } + // 3. OUTSIDE the window, still within the band -- accepted anyway, and flagged. This is the + // sliver: the tie-break keeps material the trim curve does not enclose. + { + const auto hits = hitAt(splineTrimmed, centrePhi + trimRadius + justInside); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + BOOST_CHECK(hits.front().onTrimBoundary); + } + // 4. beyond the band the patch really does end + { + BOOST_CHECK(hitAt(splineTrimmed, centrePhi + trimRadius + 100. * band).empty()); + } + // 5. an exactly-held trim has no sliver to label: the same offsets are decided, not flagged + { + const auto inside = hitAt(arcTrimmed, centrePhi + trimRadius - justInside); + BOOST_REQUIRE_EQUAL(inside.size(), 1u); + BOOST_CHECK(!inside.front().onTrimBoundary); + BOOST_CHECK(hitAt(arcTrimmed, centrePhi + trimRadius + justInside).empty()); + } + // 6. and an untrimmed patch never sets it, which is what keeps the fast path fast + { + surf::CylindricalBoundedSurface plain; + BOOST_REQUIRE(plain.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + error)); + const auto hits = hitAt(plain, centrePhi); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + BOOST_CHECK(!hits.front().onTrimBoundary); + } +} + +BOOST_AUTO_TEST_CASE(BSplineSidecarRoundTrip) +{ + // a closed box whose first face carries a (collinear) B-spline boundary edge round-trips through + // the sidecar reader and navigates identically to TGeoBBox + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + + std::vector bytes; + appendSidecarHeader(bytes, 6); + appendBSplineEdgePlaneRecord(bytes, boxFaceFrame(0, halfX, halfY, halfZ)); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { + appendPlaneRecord(bytes, boxFaceFrame(faceIndex, halfX, halfY, halfZ)); + } + const auto path = writeSidecarFile("o2_sidecar_bspline_box.bin", bytes); + + SurfaceSolid box("sidecarBSplineBox"); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(path.string(), box)); + std::filesystem::remove(path); + BOOST_CHECK_EQUAL(box.GetNsurfaces(), 6); + box.CloseShape(); + BOOST_CHECK(box.IsClosed()); + BOOST_CHECK(box.IsOrientationConsistent()); + + TGeoBBox reference("bsplineBoxRef", halfX, halfY, halfZ); + compareContainsGrid(box, reference, 4., 7); + compareDistance(box, reference, {5., 0.5, 0.5}, {-1., 0., 0.}); + compareDistance(box, reference, {0., 0., 0.}, unitDirection(1., 1., 1.)); + checkClose(box.Capacity(), reference.Capacity(), 1.e-6); +} + +BOOST_AUTO_TEST_CASE(BSplineWindowInCylinderWall) +{ + using surf::Curve2D; + using surf::Vec3; + std::string error; + + const auto onCylinder = [](double phi, double height) { + return Vec3{2. * std::cos(phi), 2. * std::sin(phi), height}; + }; + + // an exact circular trim in (phi, h) built from four NURBS quarter arcs must classify identically + // to the same circle expressed as one exact arc Curve2D — validating the B-spline trim path on a + // quadric against the closed-form arc path. + const double centrePhi = surf::kPi; + const double trimRadius = 0.5; + surf::CylindricalBoundedSurface bsplineDisk; + const std::vector bsplineOuter{quarterCircleBSpline(centrePhi, 0., trimRadius, 0.), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kHalfPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, 3. * surf::kHalfPi)}; + BOOST_REQUIRE(bsplineDisk.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + bsplineOuter, {}, error)); + BOOST_CHECK(!bsplineDisk.capacityIsExact()); // B-spline (wire) trim -> numeric capacity + + surf::CylindricalBoundedSurface arcDisk; + BOOST_REQUIRE(arcDisk.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + {Curve2D::makeCircle({centrePhi, 0.}, trimRadius)}, {}, error)); + + // classification agrees across a grid of the (phi, h) neighbourhood of the trim (skip a thin band + // around the boundary, where the exact-arc and sampled-B-spline classifications can legitimately + // differ by the sampling tolerance) + int compared = 0; + for (int phiStep = -12; phiStep <= 12; ++phiStep) { + const double phi = centrePhi + 0.09 * phiStep; + for (int hStep = -12; hStep <= 12; ++hStep) { + const double height = 0.09 * hStep; + // radial distance in (phi, h) from the trim centre; skip the boundary band + const double distToCentre = std::hypot(phi - centrePhi, height); + if (std::abs(distToCentre - trimRadius) < 5.e-3) { + continue; + } + const Vec3 point = onCylinder(phi, height); + BOOST_CHECK_EQUAL(bsplineDisk.containsPointOnSurface(point), arcDisk.containsPointOnSurface(point)); + ++compared; + } + } + BOOST_CHECK_GT(compared, 100); + + // a radial ray into the B-spline window (its centre) registers exactly one wall hit; a ray well + // outside the window (opposite side of the cylinder) misses the trimmed patch + std::vector hits; + bsplineDisk.appendIntersections({0., 0., 0.}, {std::cos(centrePhi), std::sin(centrePhi), 0.}, 0., 1.e30, hits); + BOOST_REQUIRE_EQUAL(hits.size(), 1u); + checkClose(hits.front().distance, 2.); + hits.clear(); + bsplineDisk.appendIntersections({0., 0., 0.}, {std::cos(0.), std::sin(0.), 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); +} + +// A B-spline wire used as an *inner hole*, and in particular one spelled as a single CLOSED +// B-spline edge. This is what a tube-tube intersection produces and what the converter emits +// constantly: where a boom tube is planted on a fat tube, the fat tube's wall keeps a +// full-rectangle outer wire and carries the intersection curve as one closed B-spline hole. +// +// It regresses a bug that silently deleted such a wire outright. bsplineSampleRecursive used to +// end the recursion when the chord p0->p1 was shorter than the flatness scale; a closed curve has +// p0 == p1 exactly, so a full circle flattened to two coincident points and every polyline-based +// query (winding, closest point, boundary band, display mesh) saw an empty curve. The wire still +// validated and still reported the correct enclosed area, because signedAreaContribution +// integrates the curve by Gauss-Legendre rather than from the polyline -- which is exactly why +// this survived: every check that could have caught it used the analytic path. +// +// Impact: on ExcavatorArm/BoomCylinderOuter_0_1_1_9 a point 0.026 cm inside such a hole was reported as +// lying on the face, and a whole face whose outer wire was one closed B-spline did not exist at +// all. `WireTrimmedQuadricKernels` covers a *line* hole and `BSplineWindowInCylinderWall` covers a +// B-spline outer wire built from four *open* quarter arcs, so neither could see it. +BOOST_AUTO_TEST_CASE(BSplineHoleInCylinderWall) +{ + using surf::Curve2D; + using surf::Vec3; + std::string error; + + const auto onCylinder = [](double phi, double height) { + return Vec3{2. * std::cos(phi), 2. * std::sin(phi), height}; + }; + + // full-sweep outer wire (what the converter writes for an untrimmed cylinder wall) ... + const std::vector outer{Curve2D::makeLine({0., -3.}, {surf::kTwoPi, -3.}), + Curve2D::makeLine({surf::kTwoPi, -3.}, {surf::kTwoPi, 3.}), + Curve2D::makeLine({surf::kTwoPi, 3.}, {0., 3.}), + Curve2D::makeLine({0., 3.}, {0., -3.})}; + // ... with a circular hole punched in it, expressed once as four NURBS quarter arcs and once as + // the equivalent exact arc. The arc form is the oracle: it is the already-trusted path. + const double centrePhi = surf::kPi; + const double trimRadius = 0.5; + const std::vector bsplineHole{quarterCircleBSpline(centrePhi, 0., trimRadius, 0.), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kHalfPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, surf::kPi), + quarterCircleBSpline(centrePhi, 0., trimRadius, 3. * surf::kHalfPi)}; + + surf::CylindricalBoundedSurface bsplineHoled; + BOOST_REQUIRE(bsplineHoled.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kTwoPi, false, + outer, {bsplineHole}, error)); + surf::CylindricalBoundedSurface arcHoled; + BOOST_REQUIRE(arcHoled.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kTwoPi, false, + outer, {{Curve2D::makeCircle({centrePhi, 0.}, trimRadius)}}, error)); + + // the defining property of a hole: its interior is NOT part of the face + BOOST_CHECK(!arcHoled.containsPointOnSurface(onCylinder(centrePhi, 0.))); // oracle + BOOST_CHECK(!bsplineHoled.containsPointOnSurface(onCylinder(centrePhi, 0.))); // the case under test + // and material well away from the hole still is + BOOST_CHECK(bsplineHoled.containsPointOnSurface(onCylinder(0.5, 0.))); + BOOST_CHECK(bsplineHoled.containsPointOnSurface(onCylinder(centrePhi, 2.5))); + + // the two spellings of the same hole must classify identically away from the boundary band + int compared = 0; + for (int phiStep = -12; phiStep <= 12; ++phiStep) { + const double phi = centrePhi + 0.09 * phiStep; + for (int hStep = -12; hStep <= 12; ++hStep) { + const double height = 0.09 * hStep; + if (std::abs(std::hypot(phi - centrePhi, height) - trimRadius) < 5.e-3) { + continue; + } + const Vec3 point = onCylinder(phi, height); + BOOST_CHECK_EQUAL(bsplineHoled.containsPointOnSurface(point), arcHoled.containsPointOnSurface(point)); + ++compared; + } + } + BOOST_CHECK_GT(compared, 100); + + // a radial ray aimed through the hole must not register a wall hit; one aimed at material must + std::vector hits; + bsplineHoled.appendIntersections({0., 0., 0.}, {std::cos(centrePhi), std::sin(centrePhi), 0.}, 0., 1.e30, hits); + BOOST_CHECK(hits.empty()); + hits.clear(); + bsplineHoled.appendIntersections({0., 0., 0.}, {std::cos(0.5), std::sin(0.5), 0.}, 0., 1.e30, hits); + BOOST_CHECK_EQUAL(hits.size(), 1u); + + // The same hole as ONE closed B-spline edge rather than four arc segments. This is what the + // converter actually emits for a tube-tube seam (`_quadric_trim_wire` writes one B-spline per + // BREP edge, and the intersection curve is a single closed edge). + surf::CylindricalBoundedSurface singleEdgeHoled; + BOOST_REQUIRE(singleEdgeHoled.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -3., 3., 0., surf::kTwoPi, + false, outer, {{fullCircleBSpline(centrePhi, 0., trimRadius)}}, error)); + BOOST_CHECK(!singleEdgeHoled.containsPointOnSurface(onCylinder(centrePhi, 0.))); + BOOST_CHECK(singleEdgeHoled.containsPointOnSurface(onCylinder(0.5, 0.))); + BOOST_CHECK(singleEdgeHoled.containsPointOnSurface(onCylinder(centrePhi, 2.5))); + for (int phiStep = -12; phiStep <= 12; ++phiStep) { + const double phi = centrePhi + 0.09 * phiStep; + for (int hStep = -12; hStep <= 12; ++hStep) { + const double height = 0.09 * hStep; + if (std::abs(std::hypot(phi - centrePhi, height) - trimRadius) < 5.e-3) { + continue; + } + const Vec3 point = onCylinder(phi, height); + BOOST_CHECK_EQUAL(singleEdgeHoled.containsPointOnSurface(point), arcHoled.containsPointOnSurface(point)); + } + } +} + +namespace +{ +// The public-API mirror of quarterCircleBSpline, for building a NURBS trim through Add*Surface. +BoundaryCurve quarterCircleBoundaryCurve(double cu, double cv, double r, double a0) +{ + const double a1 = a0 + surf::kHalfPi; + const double aMid = 0.5 * (a0 + a1); + const std::vector poles{{cu + r * std::cos(a0), cv + r * std::sin(a0)}, + {cu + r * std::sqrt(2.) * std::cos(aMid), cv + r * std::sqrt(2.) * std::sin(aMid)}, + {cu + r * std::cos(a1), cv + r * std::sin(a1)}}; + return BoundaryCurve::makeBSpline(2, poles, {1., std::sqrt(0.5), 1.}, {0., 0., 0., 1., 1., 1.}); +} + +// Assert that two solids are the *same* solid, not merely similar ones: identical closure +// diagnostics and reliability, identical bounding box and capacity, and bit-identical answers +// from all four navigation kernels over the standard probe grid and direction set. This is the +// acceptance criterion for persistence -- a solid that survives a write/read cycle must be +// indistinguishable through the public interface. +void checkSolidsIdentical(const SurfaceSolid& solid, const SurfaceSolid& other, double extent, int samples) +{ + BOOST_CHECK_EQUAL(other.GetNsurfaces(), solid.GetNsurfaces()); + BOOST_CHECK_EQUAL(other.IsDefined(), solid.IsDefined()); + BOOST_CHECK_EQUAL(other.HasBVH(), solid.HasBVH()); + BOOST_CHECK_EQUAL(other.IsClosed(), solid.IsClosed()); + BOOST_CHECK_EQUAL(other.IsOrientationConsistent(), solid.IsOrientationConsistent()); + BOOST_CHECK_EQUAL(static_cast(other.GetNavigationReliability()), + static_cast(solid.GetNavigationReliability())); + BOOST_CHECK_EQUAL(other.GetBoundaryEdgeCount(), solid.GetBoundaryEdgeCount()); + BOOST_CHECK_EQUAL(other.GetNonManifoldEdgeCount(), solid.GetNonManifoldEdgeCount()); + BOOST_CHECK_EQUAL(other.GetReversedEdgeCount(), solid.GetReversedEdgeCount()); + + BOOST_CHECK_EQUAL(other.GetDX(), solid.GetDX()); + BOOST_CHECK_EQUAL(other.GetDY(), solid.GetDY()); + BOOST_CHECK_EQUAL(other.GetDZ(), solid.GetDZ()); + for (int dimension = 0; dimension < 3; ++dimension) { + BOOST_CHECK_EQUAL(other.GetOrigin()[dimension], solid.GetOrigin()[dimension]); + } + BOOST_CHECK_EQUAL(other.Capacity(), solid.Capacity()); + + for (const auto& point : probeGrid(extent, samples)) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(other.Contains(point.data()), solid.Contains(point.data())); + BOOST_CHECK_EQUAL(other.Safety(point.data(), solid.Contains(point.data())), + solid.Safety(point.data(), solid.Contains(point.data()))); + for (const auto& direction : probeDirections()) { + BOOST_CHECK_EQUAL(other.DistFromOutside(point.data(), direction.data(), 3), + solid.DistFromOutside(point.data(), direction.data(), 3)); + BOOST_CHECK_EQUAL(other.DistFromInside(point.data(), direction.data(), 3), + solid.DistFromInside(point.data(), direction.data(), 3)); + } + } + } +} + +// Write "solid" to a ROOT file and read it back as an independent object. +std::unique_ptr writeAndReadBack(const SurfaceSolid& solid) +{ + const auto path = std::filesystem::temp_directory_path() / + (std::string("o2_bvhsurfacesolid_persist_") + solid.GetName() + ".root"); + { + TFile file(path.string().c_str(), "RECREATE"); + BOOST_REQUIRE(!file.IsZombie()); + // WriteObject takes a non-const pointer; the call does not modify the solid. + file.WriteObject(const_cast(&solid), "solid"); + } + std::unique_ptr restored; + { + TFile file(path.string().c_str(), "READ"); + BOOST_REQUIRE(!file.IsZombie()); + restored.reset(file.Get("solid")); + } + std::filesystem::remove(path); + return restored; +} +} // namespace + +// ROOT persistence round trip. The kernel objects behind the solid (BoundedSurface, the BVH, the +// display mesh) are all *derived* state; what has to survive a write/read cycle is the sequence of +// Add*Surface calls the solid was built from, after which CloseShape() reconstructs the rest. +// +// It regresses a bug where nothing at all was streamed: fImpl was transient, so +// a read-back solid came back with zero surfaces, CloseShape(false) then zeroed the streamed +// bounding box, and an *empty* ClosureReport defaults to closed/consistent -- so the husk reported +// NavigationReliability::Reliable and answered "outside" everywhere with full confidence. Any +// TGeoManager::Export/Import of a geometry containing one of these solids silently replaced it by +// an authoritatively-reliable empty point. +BOOST_AUTO_TEST_CASE(PersistenceRoundTrip) +{ + // every surface family and both trim flavours (scalar range and wire trim, the latter with + // line, arc and B-spline curves) must survive, so each record field is exercised + const auto box = makeBoxSolid("persistBox", 1., 2., 3.); + const auto tube = makeTubeSolid("persistTube", 1., 2., 3.); // inner wall + annular arc-wire caps + const auto cone = makeConeSolid("persistCone", 2., 1., 3.); + const auto sphere = makeSphereSolid("persistSphere", 2.); + const auto torus = makeTorusSolid("persistTorus", 3., 1.); + const auto capsule = makeCapsuleSolid("persistCapsule", 2., 3.); + + for (const auto* solid : {box.get(), tube.get(), cone.get(), sphere.get(), torus.get(), capsule.get()}) { + BOOST_TEST_CONTEXT("solid = " << solid->GetName()) + { + const auto restored = writeAndReadBack(*solid); + BOOST_REQUIRE(restored != nullptr); + checkSolidsIdentical(*solid, *restored, 4.5, 5); + } + } + + // a wire-trimmed cylinder whose window is a NURBS loop: the B-spline degree, poles, weights and + // knots all have to make the round trip, and the trimmed overload has to be the one replayed + SurfaceSolid trimmed("persistTrimmed"); + constexpr double radius = 2.; + constexpr double halfHeight = 3.; + const std::vector window{quarterCircleBoundaryCurve(surf::kPi, 0., 0.5, 0.), + quarterCircleBoundaryCurve(surf::kPi, 0., 0.5, surf::kHalfPi), + quarterCircleBoundaryCurve(surf::kPi, 0., 0.5, surf::kPi), + quarterCircleBoundaryCurve(surf::kPi, 0., 0.5, 3. * surf::kHalfPi)}; + BOOST_REQUIRE(trimmed.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, + halfHeight, 0., surf::kTwoPi, false, window)); + BOOST_REQUIRE(addDiskSurface(trimmed, {0., 0., halfHeight}, {1., 0., 0.}, {0., 1., 0.}, radius)); + BOOST_REQUIRE(addDiskSurface(trimmed, {0., 0., -halfHeight}, {1., 0., 0.}, {0., -1., 0.}, radius)); + trimmed.CloseShape(false); + BOOST_CHECK_EQUAL(trimmed.GetNsurfaces(), 3); + + const auto restoredTrimmed = writeAndReadBack(trimmed); + BOOST_REQUIRE(restoredTrimmed != nullptr); + checkSolidsIdentical(trimmed, *restoredTrimmed, 4.5, 5); + + // the model's own tolerance is solid-level state, not derived from the records, so it has to be + // streamed rather than recomputed on replay + SurfaceSolid toleranced("persistTolerance"); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(toleranced, faceIndex, 1., 2., 3.)); + } + toleranced.SetModelTolerance(7.25e-5); + toleranced.CloseShape(false); + const auto restoredToleranced = writeAndReadBack(toleranced); + BOOST_REQUIRE(restoredToleranced != nullptr); + checkClose(restoredToleranced->GetModelTolerance(), 7.25e-5, 1.e-18); + + // an unnavigable solid must come back unnavigable: the failure mode S1 describes is precisely a + // defective solid that acquires a clean bill of health by losing its surfaces on the way + SurfaceSolid openBox("persistOpenBox"); + for (int faceIndex = 0; faceIndex < 5; ++faceIndex) { // deliberately missing the sixth face + BOOST_REQUIRE(addBoxFace(openBox, faceIndex, 1., 2., 3.)); + } + openBox.CloseShape(false); + BOOST_REQUIRE(!openBox.IsNavigable()); + BOOST_CHECK_EQUAL(static_cast(openBox.GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::OpenSurfaceSet)); + + const auto restoredOpenBox = writeAndReadBack(openBox); + BOOST_REQUIRE(restoredOpenBox != nullptr); + BOOST_CHECK(!restoredOpenBox->IsNavigable()); + checkSolidsIdentical(openBox, *restoredOpenBox, 4.5, 5); +} + +// A solid that reaches the reader with no surface records -- a file written by an older version, +// or a solid streamed before CloseShape() -- must report Undetermined rather than manufacture a +// clean ClosureReport out of an empty surface set. "I do not know" is the only honest answer, and +// the difference matters: NavigationReliability is the flag callers are told to check. +BOOST_AUTO_TEST_CASE(EmptySolidIsNotReliable) +{ + SurfaceSolid empty("emptySolid"); + BOOST_CHECK_EQUAL(static_cast(empty.GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::Undetermined)); + BOOST_CHECK(!empty.IsNavigable()); + + // CloseShape on an empty surface set must not define the shape, with or without checking + empty.CloseShape(false); + BOOST_CHECK(!empty.IsDefined()); + BOOST_CHECK_EQUAL(static_cast(empty.GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::Undetermined)); + BOOST_CHECK(!empty.IsNavigable()); + BOOST_CHECK(!empty.IsClosed()); +} + +namespace +{ +// A golden-angle spiral of unit directions: quasi-uniform on the sphere, so no two are +// near-parallel and none aligns with a coordinate axis or a 45-degree symmetry plane. Used to +// test the invariant that containment does not depend on where the parity ray is aimed. +std::vector> spiralDirections(int count) +{ + std::vector> directions; + directions.reserve(count); + for (int index = 0; index < count; ++index) { + const double cosTheta = 1. - 2. * (index + 0.5) / count; + const double sinTheta = std::sqrt(1. - cosTheta * cosTheta); + const double phi = 2.399963229728653 * index; + directions.push_back({sinTheta * std::cos(phi), sinTheta * std::sin(phi), cosTheta}); + } + return directions; +} +} // namespace + +// Parity containment answers a topological question, so on a closed, consistently oriented +// 2-manifold it cannot depend on where the ray is aimed. That invariant is what licenses the +// single-shot fast path: Contains() casts one fixed direction and stops. +// +// It is also the sharpest available oracle for the surface set itself -- no reference shape is +// involved, only the solid disagreeing with itself. Measured over the Phase 0 corpus, every part +// the closure check calls Reliable has *zero* direction disagreements in 11k points, and every +// part with disagreements is one the closure check already rejects. +BOOST_AUTO_TEST_CASE(ContainsIsDirectionIndependentOnClosedSolids) +{ + const auto box = makeBoxSolid("dirBox", 1., 2., 3.); + const auto tube = makeTubeSolid("dirTube", 1., 2., 3.); + const auto cone = makeConeSolid("dirCone", 2., 1., 3.); + const auto sphere = makeSphereSolid("dirSphere", 2.); + const auto torus = makeTorusSolid("dirTorus", 3., 1.); + const auto capsule = makeCapsuleSolid("dirCapsule", 2., 3.); + + const auto directions = spiralDirections(13); + for (const auto* solid : {box.get(), tube.get(), cone.get(), sphere.get(), torus.get(), capsule.get()}) { + BOOST_TEST_CONTEXT("solid = " << solid->GetName()) + { + BOOST_REQUIRE(solid->IsNavigable()); + for (const auto& point : probeGrid(4.5, 7)) { + const bool reference = solid->Contains(point.data()); + for (const auto& direction : directions) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ") direction = (" + << direction[0] << ", " << direction[1] << ", " << direction[2] << ")") + { + BOOST_CHECK_EQUAL(solid->ContainsAlongDirection(point.data(), direction.data()), reference); + } + } + } + } + } +} + +// Section 4.4's re-shoot, on the defect it exists for. A gap in the surface set costs the parity +// ray exactly the crossings that fall inside the gap, so a point is misclassified over the whole +// *shadow* of the gap along the shooting direction -- centimetres of wrong answers arbitrarily far +// from any surface. Aiming the ray somewhere else escapes that shadow, which is why a majority +// over several directions recovers the right answer: measured over the 55 points where the single +// fixed direction disagrees with the OpenCascade oracle on the Phase 0 corpus, not one point is +// wrong in every direction. +// +// The fixture makes the mechanism explicit rather than statistical: the +x face of a box is split +// into two rectangles with a thin strip left out, so a ray leaving along +x from inside sees no +// crossing at all and reports "outside". +BOOST_AUTO_TEST_CASE(ContainsReshootsThroughSurfaceGaps) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + constexpr double gap = 0.05; // half-width in z of the missing strip on the +x face + + SurfaceSolid gapped("gappedBox"); + for (int faceIndex = 1; faceIndex < 6; ++faceIndex) { // every face but +x + BOOST_REQUIRE(addBoxFace(gapped, faceIndex, halfX, halfY, halfZ)); + } + // the +x face as two rectangles, leaving z in (-gap, +gap) uncovered. Frame of face 0: + // origin (halfX, -halfY, -halfZ), axisU = +y, axisV = +z. + BOOST_REQUIRE(gapped.AddPlanarSurface({halfX, -halfY, -halfZ}, {0., 1., 0.}, {0., 0., 1.}, + rectangleWire(2. * halfY, halfZ - gap))); + BOOST_REQUIRE(gapped.AddPlanarSurface({halfX, -halfY, gap}, {0., 1., 0.}, {0., 0., 1.}, + rectangleWire(2. * halfY, halfZ - gap))); + gapped.CloseShape(false); + + // the gap is what makes the solid unnavigable, and only an unnavigable solid re-shoots + BOOST_REQUIRE(!gapped.IsNavigable()); + BOOST_CHECK_EQUAL(static_cast(gapped.GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::OpenSurfaceSet)); + + // a point deep inside whose +x ray leaves straight through the gap + const std::array insidePoint{0., 0.3, 0.}; + const std::array throughGap{1., 0., 0.}; + BOOST_CHECK(!gapped.ContainsAlongDirection(insidePoint.data(), throughGap.data())); // the defect itself + BOOST_CHECK(gapped.Contains(insidePoint.data())); // the re-shoot recovers it + BOOST_CHECK(gapped.Contains_Loop(insidePoint.data())); // ... on both paths + + // the same point in the same box *without* the gap is inside from every direction, so the + // fixture isolates the gap and not some accident of the point + const auto intact = makeBoxSolid("intactBox", halfX, halfY, halfZ); + BOOST_CHECK(intact->Contains(insidePoint.data())); + BOOST_CHECK(intact->ContainsAlongDirection(insidePoint.data(), throughGap.data())); + + // and the BVH and loop parities still agree everywhere on the defective solid: the re-shoot is + // applied by one shared helper, so it can never make the two paths differ + for (const auto& point : probeGrid(4.5, 7)) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(gapped.Contains(point.data()), gapped.Contains_Loop(point.data())); + } + } +} + +namespace +{ +// An L-shaped prism, the concave fixture. Its footprint is ([0,3]x[0,1]) union ([0,1]x[1,2]) +// extruded over +// z in [0, height], so it has a *reflex* (concave) vertical edge at x = 1, y = 1 -- the one place +// where a ray can touch the boundary from inside and stay inside, which no convex fixture can +// reproduce. Built from eight planar faces with outward normals, so it is closed and consistently +// oriented. +std::unique_ptr makeLPrismSolid(const char* name, double height = 1.) +{ + // footprint, counter-clockwise; (1,1) is the reflex vertex + const std::vector footprint{{0., 0.}, {3., 0.}, {3., 1.}, {1., 1.}, {1., 2.}, {0., 2.}}; + + auto solid = std::make_unique(name); + + // bottom (outward normal -z) and top (+z); axisU x axisV fixes the normal + std::vector bottomWire; + bottomWire.reserve(footprint.size()); + for (const auto& vertex : footprint) { + bottomWire.push_back({vertex[1], vertex[0]}); // (u, v) = (y, x) so that axisU x axisV = -z + } + BOOST_REQUIRE(solid->AddPlanarSurface({0., 0., 0.}, {0., 1., 0.}, {1., 0., 0.}, bottomWire)); + BOOST_REQUIRE(solid->AddPlanarSurface({0., 0., height}, {1., 0., 0.}, {0., 1., 0.}, footprint)); + + // one vertical wall per footprint edge; axisU along the edge and axisV = +z put the normal at + // (dy, -dx, 0), which points out of a counter-clockwise footprint + for (size_t index = 0; index < footprint.size(); ++index) { + const auto& start = footprint[index]; + const auto& end = footprint[(index + 1) % footprint.size()]; + const double deltaU = end[0] - start[0]; + const double deltaV = end[1] - start[1]; + const double length = std::hypot(deltaU, deltaV); + BOOST_REQUIRE(solid->AddPlanarSurface({start[0], start[1], 0.}, {deltaU / length, deltaV / length, 0.}, + {0., 0., 1.}, rectangleWire(length, height))); + } + solid->CloseShape(); + return solid; +} +} // namespace + +// S2: the bounding-box pre-check ran *before* the documented "no BVH yet, fall back to the plain +// loop" branch. Before CloseShape() the box is still all zeros, so the pre-check rejected every +// point outside a 1e-9 cube at the origin and the fallback was unreachable -- Contains() was +// effectively disabled on any solid that had not been closed yet. +BOOST_AUTO_TEST_CASE(ContainsWorksBeforeCloseShape) +{ + SurfaceSolid box("preCloseBox"); + addBoxSurfaces(box, 1., 2., 3.); + BOOST_REQUIRE(!box.HasBVH()); // the premise: no acceleration structure and no bounding box yet + + for (const auto& point : probeGrid(4.5, 5)) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + const bool inside = std::abs(point[0]) < 1. && std::abs(point[1]) < 2. && std::abs(point[2]) < 3.; + BOOST_CHECK_EQUAL(box.Contains(point.data()), inside); + BOOST_CHECK_EQUAL(box.Contains(point.data()), box.Contains_Loop(point.data())); + } + } +} + +// S3: a point *on* a face is inside for Contains, but its t = 0 exit was below the minimum ray +// parameter and therefore invisible to DistFromInside, which then returned Big. A navigator that +// asks "how far to the wall" while standing on the wall and is told "never" tunnels straight +// through the geometry. ROOT's own primitives answer 0 here, and so must this. +BOOST_AUTO_TEST_CASE(BoundaryPointsAgreeBetweenContainsAndDistances) +{ + const auto box = makeBoxSolid("boundaryPolicyBox", 1., 2., 3.); + const std::array onFace{1., 0.5, 0.5}; // exactly on the +x face + const std::array outward{1., 0., 0.}; + const std::array inward{-1., 0., 0.}; + + BOOST_CHECK(box->Contains(onFace.data())); // documented policy: on a face counts as inside + + TGeoBBox reference("boundaryPolicyReference", 1., 2., 3.); + BOOST_CHECK_EQUAL(box->DistFromInside(onFace.data(), outward.data(), 3), 0.); + BOOST_CHECK_EQUAL(box->DistFromOutside(onFace.data(), inward.data(), 3), 0.); + checkClose(box->DistFromInside(onFace.data(), outward.data(), 3), + reference.DistFromInside(onFace.data(), outward.data(), 3)); + checkClose(box->DistFromOutside(onFace.data(), inward.data(), 3), + reference.DistFromOutside(onFace.data(), inward.data(), 3)); + + // going the other way the far wall is still the answer, so the fix is not "always return 0" + checkClose(box->DistFromInside(onFace.data(), inward.data(), 3), 2.); + + // the BVH and loop paths must agree on all of it + for (const auto& direction : {outward, inward}) { + checkDistanceAgainstLoop(*box, onFace, direction); + } +} + +// S4 / S5: a ray that only *touches* the boundary has not crossed it. Contains() knows this -- +// near-equal hits are clustered and a cluster carrying both an entering and an exiting hit +// contributes even parity -- but the distance queries classified every hit on its own, so they +// reported the touch as a crossing. The two then disagree about the same ray: DistFromOutside +// hands the navigator a step to the touch point, Contains says it is still outside once it gets +// there, and the navigator takes zero-length steps forever. +// +// Both flavours are covered. A convex edge graze (box) is the outside-facing case, and the L-prism +// reflex edge is the inside-facing one, which no convex solid can produce: there the ray leaves +// and re-enters the material at a single point and must be reported as never having left. +BOOST_AUTO_TEST_CASE(EdgeGrazesAreNotCrossings) +{ + const double invSqrt2 = 1. / std::sqrt(2.); + + // --- convex: touch the box edge x = +1, y = +2 and stay outside on both sides of the touch + const auto box = makeBoxSolid("grazeBox", 1., 2., 3.); + const std::array grazeDirection{invSqrt2, -invSqrt2, 0.}; + const std::array grazeOrigin{1. - 5. * invSqrt2, 2. + 5. * invSqrt2, 0.}; + BOOST_REQUIRE(!box->Contains(grazeOrigin.data())); + + // a point just past the touch is still outside, so nothing was entered ... + const std::array pastTouch{1. + 1.e-3 * invSqrt2, 2. - 1.e-3 * invSqrt2, 0.}; + BOOST_REQUIRE(!box->Contains(pastTouch.data())); + // ... and the distance query must say so too + BOOST_CHECK_EQUAL(box->DistFromOutside(grazeOrigin.data(), grazeDirection.data(), 3), TGeoShape::Big()); + BOOST_CHECK_EQUAL(box->DistFromOutside_Loop(grazeOrigin.data(), grazeDirection.data()), TGeoShape::Big()); + + // --- concave: the L-prism's reflex edge at x = 1, y = 1 + const auto prism = makeLPrismSolid("grazePrism"); + BOOST_REQUIRE(prism->IsNavigable()); + checkClose(prism->Capacity(), 4.); // 3x1 plus 1x1, extruded over unit height + + // a ray through the reflex edge along (1,-1): inside before the touch, inside after it, so the + // touch is not an exit. The real exit is where it leaves the long arm at y = 0. + const std::array reflexDirection{invSqrt2, -invSqrt2, 0.}; + const std::array reflexOrigin{1. - 0.5 * invSqrt2, 1. + 0.5 * invSqrt2, 0.5}; + BOOST_REQUIRE(prism->Contains(reflexOrigin.data())); + const std::array pastReflex{1. + 1.e-3 * invSqrt2, 1. - 1.e-3 * invSqrt2, 0.5}; + BOOST_REQUIRE(prism->Contains(pastReflex.data())); // still inside: the touch was not an exit + + const double touchDistance = 0.5; + const double exitDistance = 0.5 + std::sqrt(2.); // on to (2, 0, 0.5), in the middle of the y = 0 wall + const double reported = prism->DistFromInside(reflexOrigin.data(), reflexDirection.data(), 3); + BOOST_CHECK_GT(reported, touchDistance + 1.e-6); // the touch is not the answer ... + checkClose(reported, exitDistance); // ... the far wall is + BOOST_CHECK_EQUAL(prism->DistFromInside_Loop(reflexOrigin.data(), reflexDirection.data()), reported); +} + +// The direction-taking DescribeContainsCrossings dumps the crossing list behind ContainsAlongDirection. +BOOST_AUTO_TEST_CASE(DescribeContainsCrossingsTakesAnExplicitDirection) +{ + const auto box = makeBoxSolid("describeBox", 1., 2., 3.); + const SurfaceSolid::Point3D inside{0.2, 0.3, 0.4}; + const double invSqrt2 = 1. / std::sqrt(2.); + // +x leaves through x = +1 at 0.8; the unnormalised -z direction leaves through z = -3 at 3.4 + const std::vector> cases{ + {{1., 0., 0.}, 0.8}, {{invSqrt2, -invSqrt2, 0.}, 0.8 * std::sqrt(2.)}, {{0., 0., -2.}, 3.4}}; + for (const auto& [direction, exitDistance] : cases) { + BOOST_TEST_CONTEXT("direction = (" << direction[0] << ", " << direction[1] << ", " << direction[2] << ")") + { + std::vector bvhCrossings; + std::vector loopCrossings; + box->DescribeContainsCrossings(inside, direction, bvhCrossings, loopCrossings); + // from inside a convex box every ray leaves through exactly one face + BOOST_REQUIRE_EQUAL(bvhCrossings.size(), 1u); + BOOST_REQUIRE_EQUAL(loopCrossings.size(), 1u); + BOOST_CHECK_EQUAL(bvhCrossings[0].distance, loopCrossings[0].distance); + checkClose(bvhCrossings[0].distance, exitDistance); + BOOST_CHECK_GT(bvhCrossings[0].normalAlignment, 0.); // an exit + BOOST_CHECK(!bvhCrossings[0].onTrimBoundary); + BOOST_CHECK(box->ContainsAlongDirection(inside.data(), direction.data())); + } + } +} + +// The concave fixture earns its keep beyond the single grazing ray: the whole sweep battery is +// run on it, since every invariant the convex fixtures pin (BVH == loop, direction-independent +// parity, Contains consistent with the distance answers) is weaker on shapes with no reflex edge. +BOOST_AUTO_TEST_CASE(LPrismSweeps) +{ + const auto prism = makeLPrismSolid("sweepPrism"); + BOOST_REQUIRE(prism->IsNavigable()); + + sweepDistanceAgainstLoop(*prism, 3.5, 5); + + const auto directions = spiralDirections(13); + for (const auto& point : probeGrid(3.5, 7)) { + const bool inside = prism->Contains(point.data()); + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(prism->Contains_Loop(point.data()), inside); + for (const auto& direction : directions) { + BOOST_CHECK_EQUAL(prism->ContainsAlongDirection(point.data(), direction.data()), inside); + } + // the closed-form answer for the extruded L footprint + const bool expected = point[2] > 0. && point[2] < 1. && + ((point[0] > 0. && point[0] < 3. && point[1] > 0. && point[1] < 1.) || + (point[0] > 0. && point[0] < 1. && point[1] >= 1. && point[1] < 2.)); + BOOST_CHECK_EQUAL(inside, expected); + } + } +} + +// K1: the B-spline endpoint shortcut assumed a clamped knot vector. A clamped curve interpolates +// its first and last pole, so returning those is exact and free; an unclamped one -- which is what +// OCC writes for a periodic tube-tube intersection curve before SetNotPeriodic -- starts and ends +// strictly inside its control polygon, and the shortcut then returned points that are not on the +// curve at all. Downstream, the wire's edges no longer meet (so it reads as Open and the whole +// face is thrown away) or the off-curve endpoint corrupts the winding classification, since +// CurveWire::classify deliberately uses canonical shared endpoints. +BOOST_AUTO_TEST_CASE(UnclampedBSplineEndpointsAreOnTheCurve) +{ + using surf::Curve2D; + using surf::Vec2; + + // the same cubic control polygon read twice: once with a clamped knot vector, once with a + // uniform (unclamped) one. Only the clamped curve may claim its poles as endpoints. + const std::vector poles{{0., 0.}, {1., 2.}, {3., 2.}, {4., 0.}}; + const Curve2D clamped = Curve2D::makeBSpline(3, poles, {}, {0., 0., 0., 0., 1., 1., 1., 1.}); + const Curve2D uniform = Curve2D::makeBSpline(3, poles, {}, {0., 1., 2., 3., 4., 5., 6., 7.}); + + BOOST_REQUIRE(clamped.valid()); + BOOST_REQUIRE(uniform.valid()); + + // the endpoints must lie on their own curve, whatever the knot vector says + for (const auto* curve : {&clamped, &uniform}) { + const Vec2 start = curve->startPoint(); + const Vec2 end = curve->endPoint(); + const Vec2 evaluatedStart = curve->pointAt(0.); + const Vec2 evaluatedEnd = curve->pointAt(1.); + checkClose(start.uCoord, evaluatedStart.uCoord); + checkClose(start.vCoord, evaluatedStart.vCoord); + checkClose(end.uCoord, evaluatedEnd.uCoord); + checkClose(end.vCoord, evaluatedEnd.vCoord); + } + + // and the two curves really are different, so the test is not vacuous: the clamped one + // interpolates its outer poles, the uniform one does not come near them + checkClose(clamped.startPoint().uCoord, 0.); + checkClose(clamped.endPoint().uCoord, 4.); + BOOST_CHECK_GT(std::hypot(uniform.startPoint().uCoord - poles.front().uCoord, + uniform.startPoint().vCoord - poles.front().vCoord), + 0.1); + + // a wire closed on the *curve* must validate, which is what the shortcut used to prevent: with + // poles.front() as the reported start, the joining line would have missed it by that distance + const Vec2 uniformStart = uniform.startPoint(); + const Vec2 uniformEnd = uniform.endPoint(); + surf::CurveWire wire; + surf::WireStatus status = surf::WireStatus::Valid; + BOOST_CHECK(wire.initialize({uniform, Curve2D::makeLine(uniformEnd, uniformStart)}, surf::WireRole::Outer, status)); +} + +// K2: the full-turn rejection measured the *control-point hull*, not the curve. A closed trim +// curve that wraps nearly a full turn in phi has poles outside its own span (that is what makes +// the hull a conservative bound), so the check saw more than 2*pi and refused a perfectly legal +// through-hole host face -- and a refused face is a face missing from the parity solid, i.e. wrong +// containment throughout its shadow. +BOOST_AUTO_TEST_CASE(NearFullTurnTrimIsNotRejectedOnItsPoleHull) +{ + using surf::Curve2D; + using surf::Vec2; + std::string error; + + // A trim wrapping 350 degrees of a cylinder, spelled as two quadratic B-spline spans whose + // middle poles sit *outside* the span in phi -- which is exactly what makes the control-point + // hull a conservative bound and not the curve's own extent. The curve stays inside 2*pi; its + // pole hull does not. + const double sweep = 350. * surf::kPi / 180.; + const double overshoot = 0.4; + const std::vector outer{ + Curve2D::makeBSpline(2, {{0., -1.}, {-overshoot, 0.}, {0.5 * sweep, 1.}}, {}, {0., 0., 0., 1., 1., 1.}), + Curve2D::makeBSpline(2, {{0.5 * sweep, 1.}, {sweep + overshoot, 0.}, {sweep, -1.}}, {}, {0., 0., 0., 1., 1., 1.}), + Curve2D::makeLine({sweep, -1.}, {0., -1.})}; + + // the pole hull must genuinely exceed a full turn, otherwise the fixture proves nothing + Vec2 hullLower{1.e300, 1.e300}; + Vec2 hullUpper{-1.e300, -1.e300}; + surf::CurveWire hullWire; + surf::WireStatus hullStatus = surf::WireStatus::Valid; + BOOST_REQUIRE(hullWire.initialize(outer, surf::WireRole::Outer, hullStatus)); + hullWire.parametricBounds(hullLower, hullUpper); + BOOST_REQUIRE_GT(hullUpper.uCoord - hullLower.uCoord, surf::kTwoPi); + + // ... while the curve itself does not + Vec2 tightLower{1.e300, 1.e300}; + Vec2 tightUpper{-1.e300, -1.e300}; + hullWire.tightParametricBounds(tightLower, tightUpper); + BOOST_CHECK_LT(tightUpper.uCoord - tightLower.uCoord, surf::kTwoPi); + + // so the surface must be accepted + surf::CylindricalBoundedSurface surface; + BOOST_CHECK_MESSAGE(surface.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -2., 2., 0., surf::kTwoPi, + false, outer, {}, error), + "near-full-turn trim rejected: " << error); + + // a trim that really does wrap more than a full turn is still refused + surf::CylindricalBoundedSurface tooWide; + const std::vector overWrapped{Curve2D::makeLine({0., -1.}, {surf::kTwoPi + 0.5, -1.}), + Curve2D::makeLine({surf::kTwoPi + 0.5, -1.}, {surf::kTwoPi + 0.5, 1.}), + Curve2D::makeLine({surf::kTwoPi + 0.5, 1.}, {0., 1.}), + Curve2D::makeLine({0., 1.}, {0., -1.})}; + BOOST_CHECK(!tooWide.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -2., 2., 0., surf::kTwoPi, false, + overWrapped, {}, error)); +} + +// K7 claimed a face that fails to build is logged and silently omitted from the parity solid. +// Reading the code does not support that on any production path: Add*Surface returns false, and +// the sidecar loader turns that into a whole-file rejection, which the converter's generated macro +// turns into an exception. What is true is the weaker statement that the *return value* is the +// only signal, so this pins both halves -- the rejection is reported, and nothing is added behind +// the caller's back. Recorded rather than "fixed", in the same spirit as the S6 correction. +BOOST_AUTO_TEST_CASE(RejectedFacesAreNeverSilentlyAdded) +{ + SurfaceSolid solid("rejectingSolid"); + BOOST_REQUIRE(addBoxFace(solid, 0, 1., 2., 3.)); + BOOST_REQUIRE_EQUAL(solid.GetNsurfaces(), 1); + + // degenerate frame (axisU parallel to axisV), a wire with too few vertices, and a zero-radius + // cylinder: each must be refused, and none may leave a surface behind + BOOST_CHECK(!solid.AddPlanarSurface({0., 0., 0.}, {1., 0., 0.}, {1., 0., 0.}, rectangleWire(1., 1.))); + BOOST_CHECK(!solid.AddPlanarSurface({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, {{0., 0.}, {1., 0.}})); + BOOST_CHECK(!solid.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 0., -1., 1.)); + BOOST_CHECK_EQUAL(solid.GetNsurfaces(), 1); + BOOST_CHECK_EQUAL(static_cast(solid.GetSurfaceRecords().size()), 1); + + // the loader's contract on the same failure: reject the file rather than return a partial solid + std::vector bytes; + appendSidecarHeader(bytes, 1); + appendU32(bytes, 2); // cylinder + appendU32(bytes, 0); // flags + appendU32(bytes, 14); // nParams + appendDoubles(bytes, {0., 0., 0., 0., 0., 1., 1., 0., 0., 0. /* radius */, -1., 1., 0., 2. * surf::kPi}); + appendU32(bytes, 0); // nWires + const auto path = writeSidecarFile("o2_sidecar_rejected_face.bin", bytes); + SurfaceSolid loaded("loadedRejecting"); + BOOST_CHECK(!o2::cad::LoadSurfaceSolid(path.string(), loaded)); + std::filesystem::remove(path); +} + +// The rim-based closure measurement. +// +// The half-edge check asks whether two faces emitted the *same vertices* along a shared edge. On +// real CAD that question has the answer "no" for reasons that are not gaps: each face samples the +// shared curve independently, so the vertices genuinely are not the same points and no tolerance +// on vertex equality can help. The rim measurement compares the boundaries as curves instead, and +// reports the answer as a length in cm rather than as a chord count. +// +// Nothing derives a verdict from it yet -- IsNavigable() still reads the chord counters -- so +// these tests pin the measurement, not a change of behaviour. +BOOST_AUTO_TEST_CASE(RimClosureMeasuresTheGapInCentimetres) +{ + constexpr double halfX = 1.; + constexpr double halfY = 1.5; + constexpr double halfZ = 2.; + + // a closed box: one rim per face, all matched, and no gap at all + SurfaceSolid closedBox("rimClosedBox"); + addBoxSurfaces(closedBox, halfX, halfY, halfZ); + closedBox.CloseShape(false); + BOOST_REQUIRE(closedBox.IsNavigable()); + BOOST_CHECK_EQUAL(closedBox.GetRimCount(), 6); + BOOST_CHECK_EQUAL(closedBox.GetMatchedRimCount(), 6); + BOOST_CHECK_EQUAL(closedBox.GetBoundaryRimCount(), 0); + BOOST_CHECK_SMALL(closedBox.GetMaxRimIsolation(), 1.e-12); + BOOST_CHECK_SMALL(closedBox.GetUnmatchedRimLength(), 1.e-12); + // a box has no curved rim, so its polylines are exact and the measurement has no noise floor + BOOST_CHECK_SMALL(closedBox.GetRimChordResolution(), 1.e-12); + // the summed perimeter of the six faces, which is what "how much boundary" is measured in + BOOST_CHECK_CLOSE(closedBox.GetTotalRimLength(), 16. * (halfX + halfY + halfZ), 1.e-9); + + // the same box with the +z face lifted by a known delta. A box's rims are straight, so their + // sampling resolution is zero and the match band is the declared tolerance alone; the lifted + // face's rim is then alone by exactly delta, and that is what the isolation reports. + constexpr double delta = 1.e-3; + SurfaceSolid shiftedBox("rimShiftedBox"); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + const Point3D center = faceIndex == 4 ? Point3D{0., 0., delta} : Point3D{0., 0., 0.}; + BOOST_REQUIRE(addBoxFace(shiftedBox, faceIndex, halfX, halfY, halfZ, false, center)); + } + shiftedBox.CloseShape(false); + BOOST_CHECK_CLOSE(shiftedBox.GetMaxRimIsolation(), delta, 1.e-6); + // and the shift is far above the sampling noise floor, so the number means what it says + BOOST_CHECK(shiftedBox.GetMaxRimIsolation() > shiftedBox.GetRimChordResolution()); +} + +// The structural failure the rim criterion exists to fix, pinned: two faces that sample one +// shared edge at different chord counts emit different vertices, so vertex matching calls a +// perfectly closed box open -- and open by *chords*, which is how a seven-loop solid came to +// report 1418 boundary edges. Rim matching compares the curves and gets it right, and it is the +// rim answer that IsClosed()/IsNavigable() now report. +BOOST_AUTO_TEST_CASE(RimClosureSurvivesUnequalChordCounts) +{ + constexpr double halfX = 1.; + constexpr double halfY = 1.5; + constexpr double halfZ = 2.; + + SurfaceSolid resampled("rimResampledBox"); + for (int faceIndex = 0; faceIndex < 5; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(resampled, faceIndex, halfX, halfY, halfZ)); + } + // the last face again, but with every edge split in two: the same rectangle, twice the vertices + const FaceFrame frame = boxFaceFrame(5, halfX, halfY, halfZ); + const double extentU = frame.extentU; + const double extentV = frame.extentV; + BOOST_REQUIRE(resampled.AddPlanarSurface(frame.origin, frame.axisU, frame.axisV, + {{0., 0.}, + {0.5 * extentU, 0.}, + {extentU, 0.}, + {extentU, 0.5 * extentV}, + {extentU, extentV}, + {0.5 * extentU, extentV}, + {0., extentV}, + {0., 0.5 * extentV}})); + resampled.CloseShape(false); + + // the per-chord counters still see the disagreement -- they compare the vertices the two faces + // emitted, and those really are different points. That is the defect, and it is why they no + // longer decide anything. + BOOST_CHECK(resampled.GetBoundaryEdgeCount() > 0); + + // the verdict comes from the rims, which see one boundary curve per face, all matched, no gap + BOOST_CHECK(resampled.IsClosed()); + BOOST_CHECK(resampled.IsNavigable()); + BOOST_CHECK_EQUAL(resampled.GetRimCount(), 6); + BOOST_CHECK_EQUAL(resampled.GetMatchedRimCount(), 6); + BOOST_CHECK_EQUAL(resampled.GetBoundaryRimCount(), 0); + BOOST_CHECK_SMALL(resampled.GetMaxRimIsolation(), 1.e-12); + BOOST_CHECK_SMALL(resampled.GetUnmatchedRimLength(), 1.e-12); +} + +// The per-rim records name which loop is open. A rim's state is on the same scale the solid reports, so +// the solid's verdict must be exactly the worst state present -- which is a self-check, not a +// restatement: the two are accumulated independently. +BOOST_AUTO_TEST_CASE(RimReportsNameTheOffendingLoop) +{ + constexpr double halfX = 1.; + constexpr double halfY = 1.5; + constexpr double halfZ = 2.; + constexpr double delta = 1.e-3; + + SurfaceSolid shiftedBox("rimReportBox"); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + const Point3D center = faceIndex == 4 ? Point3D{0., 0., delta} : Point3D{0., 0., 0.}; + BOOST_REQUIRE(addBoxFace(shiftedBox, faceIndex, halfX, halfY, halfZ, false, center)); + } + shiftedBox.CloseShape(false); + + const auto& rims = shiftedBox.GetRimReports(); + BOOST_REQUIRE_EQUAL(static_cast(rims.size()), shiftedBox.GetRimCount()); + + int boundaryRims = 0; + auto worst = SurfaceSolid::NavigationReliability::Reliable; + double openLength = 0.; + for (const auto& rim : rims) { + BOOST_CHECK(rim.surface >= 0 && rim.surface < shiftedBox.GetNsurfaces()); + BOOST_CHECK(rim.rimOnSurface >= 0); + BOOST_CHECK(rim.closed); // a box face's boundary is one closed loop + BOOST_CHECK(rim.length > 0.); + openLength += rim.unmatchedLength; + if (rim.state == SurfaceSolid::NavigationReliability::OpenSurfaceSet) { + ++boundaryRims; + BOOST_CHECK(rim.unmatchedChords > 0); + BOOST_CHECK(rim.unmatchedLength > 0.); + } else { + BOOST_CHECK_EQUAL(rim.unmatchedChords, 0); + } + worst = std::max(worst, rim.state); + } + BOOST_CHECK_EQUAL(boundaryRims, shiftedBox.GetBoundaryRimCount()); + BOOST_CHECK(worst == shiftedBox.GetNavigationReliability()); + BOOST_CHECK_CLOSE(openLength, shiftedBox.GetUnmatchedRimLength(), 1.e-9); + + // the lifted face's own rim is the one that is alone, and it is alone by the lift + const auto lifted = std::find_if(rims.begin(), rims.end(), [](const auto& rim) { return rim.surface == 4; }); + BOOST_REQUIRE(lifted != rims.end()); + BOOST_CHECK(lifted->state == SurfaceSolid::NavigationReliability::OpenSurfaceSet); + BOOST_CHECK_CLOSE(lifted->maxIsolation, delta, 1.e-6); + BOOST_CHECK(lifted->maxIsolationFace >= 0 && lifted->maxIsolationFace != 4); + // and the worst chord is named where it is: on the +z face, which sits at halfZ + delta + BOOST_CHECK_CLOSE(lifted->maxIsolationPoint[2], halfZ + delta, 1.e-6); +} + +// Rims are counted per boundary loop and measured in centimetres, not counted per chord. A bare +// cylinder wall is the clearest case: two circular rims, sampled at kArcSamples chords each. +BOOST_AUTO_TEST_CASE(RimCountsAreLoopsAndLengthsNotChords) +{ + constexpr double radius = 1.; + constexpr double halfHeight = 2.; + SurfaceSolid openTube("rimOpenTube"); + BOOST_REQUIRE( + openTube.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, -halfHeight, halfHeight)); + openTube.CloseShape(false); + + // the chord counter reports every sample of both rims + BOOST_CHECK(openTube.GetBoundaryEdgeCount() >= 2 * surf::kArcSamples); + // the rim measurement reports two open loops, and how long they are + BOOST_CHECK_EQUAL(openTube.GetRimCount(), 2); + BOOST_CHECK_EQUAL(openTube.GetBoundaryRimCount(), 2); + BOOST_CHECK_EQUAL(openTube.GetMatchedRimCount(), 0); + // both rims are open, and the length is the sampled circumference (a chord polygon, so slightly + // under 2*pi*r); it is the *length* that is reported, not the sample count + BOOST_CHECK_CLOSE(openTube.GetUnmatchedRimLength(), openTube.GetTotalRimLength(), 1.e-9); + BOOST_CHECK_CLOSE(openTube.GetTotalRimLength(), 2. * surf::kTwoPi * radius, 1.); + + // the sagitta of a circle of this radius sampled at kArcSamples per turn, from the closed form: + // r (1 - cos(pi/kArcSamples)). The estimator must recover it, because it is the floor below + // which a rim gap is how the rims were sampled rather than how far apart the faces are. + const double exactSagitta = radius * (1. - std::cos(surf::kPi / surf::kArcSamples)); + BOOST_CHECK_CLOSE(openTube.GetRimChordResolution(), exactSagitta, 1.); + + // Two rims and no third: a full-turn patch emits its seam twice, once each way, and that pair + // bounds nothing -- it cancels in the half-edge check for the same reason. Chained naively it + // would become a two-point rim straddling the patch, reporting a gap the size of the patch. +} + +// The matching tolerance is the model's own declared one when the sidecar states it (version 2), +// and a documented constant when it does not. Before this the kernel had no way to know what +// epsilon two faces of an imported solid should agree to, and guessed. +BOOST_AUTO_TEST_CASE(RimMatchToleranceComesFromTheModel) +{ + SurfaceSolid unstated("rimToleranceUnstated"); + addBoxSurfaces(unstated, 1., 1., 1.); + unstated.CloseShape(false); + BOOST_CHECK_EQUAL(unstated.GetModelTolerance(), 0.); + BOOST_CHECK_CLOSE(unstated.GetRimMatchTolerance(), surf::kRimMatchTolerance, 1.e-9); + + SurfaceSolid stated("rimToleranceStated"); + addBoxSurfaces(stated, 1., 1., 1.); + stated.SetModelTolerance(2.5e-7); + stated.CloseShape(false); + BOOST_CHECK_CLOSE(stated.GetRimMatchTolerance(), 2.5e-7, 1.e-9); +} + +// --- Closed-loop quadrature and unclamped B-spline endpoints --- + +/// N1. contourIntegralAlongCurve sized its quadrature from the difference between a curve's +/// endpoints. A closed trim loop -- every hole -- has identical endpoints, so it reported zero +/// travel in u and was handed a single interval, and because max(1, ceil(0 / x)) is 1 the interval +/// cap could not reach it at any value. Curve2D::uVariation measures the travel instead. +BOOST_AUTO_TEST_CASE(ClosedCurveReportsItsTravelNotItsEndpointGap) +{ + // a full circle in the (u, v) chart, written as an arc: the endpoints coincide exactly + const surf::Curve2D circle = surf::Curve2D::makeArc({0., 0.}, 2., 0., 2. * surf::kPi); + BOOST_CHECK_SMALL(std::abs(circle.endPoint().uCoord - circle.startPoint().uCoord), 1.e-12); + // u = 2 cos(angle) travels from +2 down to -2 and back: total variation 8, not 0 + BOOST_CHECK_CLOSE(circle.uVariation(0., 1.), 8., 1.e-9); + + // and the same for a closed B-spline, whose poles bound the travel from above + std::vector poles{{0., 0.}, {1., 1.}, {2., 0.}, {1., -1.}, {0., 0.}}; + std::vector knots{0., 0., 0., 0.25, 0.5, 0.75, 1., 1., 1.}; + const surf::Curve2D loop = surf::Curve2D::makeBSpline(2, poles, {}, knots); + BOOST_CHECK_SMALL(std::abs(loop.endPoint().uCoord - loop.startPoint().uCoord), 1.e-9); + BOOST_CHECK_GT(loop.uVariation(0., 1.), 0.5); +} + +/// N1, the defect itself: the contour integrator spent one 20-node Gauss-Legendre rule across a +/// B-spline's whole knot domain, and Gauss-Legendre's geometric convergence needs the integrand +/// analytic on the interval it covers -- a B-spline is one polynomial only within a span. The hole +/// here is an exact circle written the way a CAD kernel writes one, a closed rational quadratic +/// over four knot spans. Verified to fail (by 8e-4 absolute) with the knot subdivision removed. +/// +/// The endpoint-based interval count is the *second* defect, and it is why this one hid: a closed +/// loop has coincident endpoints, so it reported zero travel in u, and max(1, ceil(0 / x)) is 1 at +/// every x -- a sweep of kContourMaxSpanU moved nothing while the defect sat behind it. That half +/// is pinned by ClosedCurveReportsItsTravelNotItsEndpointGap; on this corpus it is worth 4e-5 cm^3 +/// on ExcavatorArm/BoomCylinderInner against the knot subdivision's 1.1e-2, so it is a correctness fix +/// rather than the cause. +BOOST_AUTO_TEST_CASE(HoleInWireTrimIntegratesToTheAnalyticCapacity) +{ + const double radius = 1.2; + const double height = 1.5; + const double holeRadius = 0.3; + const double centreU = surf::kPi; + const double centreV = 0.5 * height; + + std::vector outer{ + o2::cad::O2BVHSurfaceSolid::PlanarBoundaryCurve::makeLine({0., 0.}, {2. * surf::kPi, 0.}), + o2::cad::O2BVHSurfaceSolid::PlanarBoundaryCurve::makeLine({2. * surf::kPi, 0.}, {2. * surf::kPi, height}), + o2::cad::O2BVHSurfaceSolid::PlanarBoundaryCurve::makeLine({2. * surf::kPi, height}, {0., height}), + o2::cad::O2BVHSurfaceSolid::PlanarBoundaryCurve::makeLine({0., height}, {0., 0.})}; + + // the textbook exact NURBS circle: nine poles on the circumscribed square's corners and edge + // midpoints, corner weights sqrt(2)/2, four double interior knots + const double corner = std::sqrt(2.) / 2.; + const std::array, 9> unit{{{1., 0.}, {1., 1.}, {0., 1.}, {-1., 1.}, {-1., 0.}, {-1., -1.}, {0., -1.}, {1., -1.}, {1., 0.}}}; + std::vector poles; + for (const auto& pole : unit) { + poles.push_back({centreU + holeRadius * pole[0], centreV + holeRadius * pole[1]}); + } + const std::vector weights{1., corner, 1., corner, 1., corner, 1., corner, 1.}; + const std::vector knots{0., 0., 0., 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1., 1., 1.}; + std::vector> holes{ + {o2::cad::O2BVHSurfaceSolid::PlanarBoundaryCurve::makeBSpline(2, poles, weights, knots)}}; + + o2::cad::O2BVHSurfaceSolid withHole("withHole"); + BOOST_REQUIRE(withHole.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, radius, 0., height, 0., + 2. * surf::kPi, false, outer, holes)); + std::vector contributions; + withHole.GetSurfaceCapacityContributions(contributions); + BOOST_REQUIRE_EQUAL(contributions.size(), 1u); + + // f = (r/3)(C.U cos phi + C.V sin phi + r) with the axis through the origin reduces to r^2/3, so + // the contribution is r^2/3 times the trimmed chart area: 2 pi h minus the hole's pi rho^2. + const double chartArea = 2. * surf::kPi * height - surf::kPi * holeRadius * holeRadius; + BOOST_CHECK_CLOSE(contributions[0], radius * radius * chartArea / 3., 1.e-6); +} + +/// N2. The loader read a B-spline edge's endpoints off its first and last poles, which are the +/// endpoints only for a clamped knot vector. On an unclamped one the kernel (since K1) evaluates +/// and the loader did not, so the two measured the same wire join between different points. +BOOST_AUTO_TEST_CASE(UnclampedBSplineEndpointsAreEvaluatedNotReadOffThePoles) +{ + // uniform (unclamped) knots: the curve starts well inside the pole polygon + std::vector poles{{0., 0.}, {1., 2.}, {2., 2.}, {3., 0.}}; + std::vector knots{0., 1., 2., 3., 4., 5., 6., 7.}; + const surf::Curve2D unclamped = surf::Curve2D::makeBSpline(3, poles, {}, knots); + const surf::Vec2 start = unclamped.startPoint(); + const surf::Vec2 end = unclamped.endPoint(); + // the whole point: neither endpoint is a pole + BOOST_CHECK_GT(std::abs(start.uCoord - poles.front().uCoord) + std::abs(start.vCoord - poles.front().vCoord), 1.e-3); + BOOST_CHECK_GT(std::abs(end.uCoord - poles.back().uCoord) + std::abs(end.vCoord - poles.back().vCoord), 1.e-3); + // and they are on the curve + BOOST_CHECK_SMALL(std::abs(unclamped.pointAt(0.).uCoord - start.uCoord), 1.e-12); + BOOST_CHECK_SMALL(std::abs(unclamped.pointAt(1.).vCoord - end.vCoord), 1.e-12); +} + +// -------------------------------------------------------------------------------------------- +// Sidecar v3 edge identity. +// -------------------------------------------------------------------------------------------- + +namespace +{ +/// The v3 edge identity of a box built by addBoxSurfaces, derived from the geometry *once, here*. +/// +/// The converter derives this from `TopExp::MapShapesAndAncestors` on the source B-rep; a unit +/// test has no B-rep, so it keys the identity on the shared 3D endpoints of the box's own trim +/// segments. That is legitimate precisely because it is not what is under test: what is under +/// test is that the kernel decides closure by *counting the identities it is given* and by +/// nothing else, so the identities have to come from somewhere the kernel cannot see. +/// +/// `perFace[f]` is face f's list of (edgeId, flags) in the order AddPlanarSurface was given its +/// four rectangle vertices, which is the order SetSurfaceBoundaryEdges expects. +std::vector, std::vector>> + boxEdgeIdentity(double halfX, double halfY, double halfZ) +{ + using Key = std::array; + auto quantize = [](double value) { return static_cast(std::llround(value * 1.e9)); }; + std::map edgeIds; + std::vector, std::vector>> perFace(6); + + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + const FaceFrame frame = boxFaceFrame(faceIndex, halfX, halfY, halfZ); + const auto corners = rectangleWire(frame.extentU, frame.extentV); + for (size_t segment = 0; segment < corners.size(); ++segment) { + const auto& startUV = corners[segment]; + const auto& endUV = corners[(segment + 1) % corners.size()]; + auto toGlobal = [&](const Point2D& uv) { + return Point3D{frame.origin[0] + frame.axisU[0] * uv[0] + frame.axisV[0] * uv[1], + frame.origin[1] + frame.axisU[1] * uv[0] + frame.axisV[1] * uv[1], + frame.origin[2] + frame.axisU[2] * uv[0] + frame.axisV[2] * uv[1]}; + }; + const Point3D start = toGlobal(startUV); + const Point3D end = toGlobal(endUV); + const Key forwardKey{quantize(start[0]), quantize(start[1]), quantize(start[2]), + quantize(end[0]), quantize(end[1]), quantize(end[2])}; + const Key backwardKey{forwardKey[3], forwardKey[4], forwardKey[5], + forwardKey[0], forwardKey[1], forwardKey[2]}; + // the lexicographically smaller ordering names the edge; running the other way is "reversed" + const bool reversed = backwardKey < forwardKey; + const Key canonical = reversed ? backwardKey : forwardKey; + const auto inserted = edgeIds.emplace(canonical, static_cast(edgeIds.size())); + perFace[faceIndex].first.push_back(inserted.first->second); + perFace[faceIndex].second.push_back( + static_cast(SurfaceSolid::kEdgeAnchored | (reversed ? SurfaceSolid::kEdgeReversed : 0u))); + } + } + BOOST_REQUIRE_EQUAL(edgeIds.size(), 12u); // a box has twelve edges; if it did not, nothing below means anything + return perFace; +} + +/// A closed box carrying its v3 edge identity, ready for CloseShape(). +std::unique_ptr makeIdentifiedBox(const char* name, double halfX, double halfY, double halfZ) +{ + auto solid = std::make_unique(name); + addBoxSurfaces(*solid, halfX, halfY, halfZ); + const auto identity = boxEdgeIdentity(halfX, halfY, halfZ); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(solid->SetSurfaceBoundaryEdges(faceIndex, identity[faceIndex].first, identity[faceIndex].second)); + } + return solid; +} +} // namespace + +/// N3, the core of it. Closure is a count of edge identities, and no tolerance, band or sampling +/// enters the verdict. The self-check is the box: 12 edges, every one of them shared by exactly +/// two faces running opposite ways, and the two faces' realisations of each edge coincide exactly. +BOOST_AUTO_TEST_CASE(EdgeIdentityDecidesClosureByCounting) +{ + const auto box = makeIdentifiedBox("identityBox", 1., 2., 3.); + box->CloseShape(false); + + BOOST_CHECK(box->HasEdgeIdentity()); + BOOST_CHECK_EQUAL(box->GetSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(box->GetSharedSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(box->GetBoundarySourceEdgeCount(), 0); + BOOST_CHECK_EQUAL(box->GetNonManifoldSourceEdgeCount(), 0); + BOOST_CHECK_EQUAL(box->GetReversedSourceEdgeCount(), 0); + BOOST_CHECK_EQUAL(box->GetDegenerateSourceEdgeCount(), 0); + BOOST_CHECK(box->IsClosed()); + BOOST_CHECK(box->IsOrientationConsistent()); + BOOST_CHECK(box->IsNavigable()); + + // the deviation is a measurement, and on a box built from one set of corners it is exactly zero + BOOST_CHECK_EQUAL(box->GetMeasuredSharedEdgeCount(), 12); + BOOST_CHECK_EQUAL(box->GetUnmeasuredSharedEdgeCount(), 0); + BOOST_CHECK_SMALL(box->GetMaxSharedEdgeDeviation(), 1.e-15); +} + +/// A missing face is a missing face however close the survivors happen to lie. Five faces of a box +/// leave four edges used once, and that is decided by counting rather than by how far apart +/// anything is -- the old criterion had to find the nearest chord and compare it against a band. +BOOST_AUTO_TEST_CASE(EdgeIdentityFindsTheMissingFace) +{ + SurfaceSolid openBox("identityOpenBox"); + for (int faceIndex = 0; faceIndex < 5; ++faceIndex) { + BOOST_REQUIRE(addBoxFace(openBox, faceIndex, 1., 2., 3.)); + } + const auto identity = boxEdgeIdentity(1., 2., 3.); + for (int faceIndex = 0; faceIndex < 5; ++faceIndex) { + BOOST_REQUIRE(openBox.SetSurfaceBoundaryEdges(faceIndex, identity[faceIndex].first, identity[faceIndex].second)); + } + openBox.CloseShape(false); + + BOOST_CHECK(openBox.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(openBox.GetSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(openBox.GetSharedSourceEdgeCount(), 8); + BOOST_CHECK_EQUAL(openBox.GetBoundarySourceEdgeCount(), 4); // the missing face's own four edges + BOOST_CHECK(!openBox.IsClosed()); + BOOST_CHECK(!openBox.IsNavigable()); + BOOST_CHECK_EQUAL(static_cast(openBox.GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::OpenSurfaceSet)); +} + +/// The sense bit carries the orientation, and it is checked. Two faces that traverse a shared edge +/// the same way disagree about which side is out, whatever their normals happen to look like. +BOOST_AUTO_TEST_CASE(EdgeIdentityFindsReversedAndDuplicatedFaces) +{ + { + const auto box = makeIdentifiedBox("identityReversed", 1., 2., 3.); + // flip one face's senses: its four edges now read "twice, same way" instead of "twice, opposite" + auto identity = boxEdgeIdentity(1., 2., 3.); + for (auto& flag : identity[0].second) { + flag = static_cast(flag ^ SurfaceSolid::kEdgeReversed); + } + BOOST_REQUIRE(box->SetSurfaceBoundaryEdges(0, identity[0].first, identity[0].second)); + box->CloseShape(false); + BOOST_CHECK_EQUAL(box->GetReversedSourceEdgeCount(), 4); + BOOST_CHECK(box->IsClosed()); // still every edge twice: closed, but inconsistently oriented + BOOST_CHECK(!box->IsOrientationConsistent()); + BOOST_CHECK_EQUAL(static_cast(box->GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::ReversedFaces)); + } + { + // a seventh face claiming edges that already have two owners is non-manifold by count + const auto box = makeIdentifiedBox("identityNonManifold", 1., 2., 3.); + BOOST_REQUIRE(addBoxFace(*box, 0, 1., 2., 3.)); + const auto identity = boxEdgeIdentity(1., 2., 3.); + BOOST_REQUIRE(box->SetSurfaceBoundaryEdges(6, identity[0].first, identity[0].second)); + box->CloseShape(false); + BOOST_CHECK_EQUAL(box->GetNonManifoldSourceEdgeCount(), 4); + BOOST_CHECK(!box->IsClosed()); + BOOST_CHECK_EQUAL(static_cast(box->GetNavigationReliability()), + static_cast(SurfaceSolid::NavigationReliability::NonManifold)); + } +} + +/// maxSharedEdgeDeviation is a measurement, and it has to measure the thing it is named after. +/// Move one face of the box bodily by a known delta while it keeps claiming the same edges: the +/// solid is still *closed* by identity (nothing about which edges exist has changed) and the +/// deviation reports the delta. That separation is the whole design -- the verdict says whether +/// the faces are meant to meet, the number says how well they do. +BOOST_AUTO_TEST_CASE(SharedEdgeDeviationMeasuresHowFarApartTheFacesAre) +{ + constexpr double delta = 3.e-4; + SurfaceSolid shifted("identityShifted"); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + // face 4 is the +z cap; nudge it along +z, which pulls its whole rim off the four side faces + const Point3D centre = faceIndex == 4 ? Point3D{0., 0., delta} : Point3D{0., 0., 0.}; + BOOST_REQUIRE(addBoxFace(shifted, faceIndex, 1., 2., 3., false, centre)); + } + const auto identity = boxEdgeIdentity(1., 2., 3.); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + BOOST_REQUIRE(shifted.SetSurfaceBoundaryEdges(faceIndex, identity[faceIndex].first, identity[faceIndex].second)); + } + shifted.CloseShape(false); + + BOOST_CHECK(shifted.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(shifted.GetSharedSourceEdgeCount(), 12); + BOOST_CHECK(shifted.IsClosed()); // by identity: the same twelve edges, each used twice + BOOST_CHECK_EQUAL(shifted.GetMeasuredSharedEdgeCount(), 12); + checkClose(shifted.GetMaxSharedEdgeDeviation(), delta, 1.e-12); +} + +/// The correspondence between edge identity i and trim curve i survives the wire reorientation +/// that CurveWire/SurfaceWire perform on load. A loop handed in with the wrong winding is +/// reversed in place, so storage index i stops being input index i; pairing the wrong two curves +/// would still produce a number, and a plausible one, which is why the mapping is recorded rather +/// than assumed. Handing the same box in with every loop wound the other way must not move the +/// deviation off zero. +BOOST_AUTO_TEST_CASE(TrimCurveIdentitySurvivesWireReorientation) +{ + SurfaceSolid flipped("identityFlippedWinding"); + const auto identity = boxEdgeIdentity(1., 2., 3.); + std::vector> ids(6); + std::vector> flags(6); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + FaceFrame frame = boxFaceFrame(faceIndex, 1., 2., 3.); + auto corners = rectangleWire(frame.extentU, frame.extentV); + // reverse the vertex ring: segment j of the new ring is segment (n-2-j) of the old one, run + // backwards, and initialize() will reverse it again to restore the winding it wants + std::reverse(corners.begin(), corners.end()); + BOOST_REQUIRE(flipped.AddPlanarSurface(frame.origin, frame.axisU, frame.axisV, corners)); + const size_t n = corners.size(); + ids[faceIndex].resize(n); + flags[faceIndex].resize(n); + for (size_t j = 0; j < n; ++j) { + const size_t source = (n - 2 - j + n) % n; + ids[faceIndex][j] = identity[faceIndex].first[source]; + flags[faceIndex][j] = identity[faceIndex].second[source]; + } + BOOST_REQUIRE(flipped.SetSurfaceBoundaryEdges(faceIndex, ids[faceIndex], flags[faceIndex])); + } + flipped.CloseShape(false); + + BOOST_CHECK(flipped.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(flipped.GetSharedSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(flipped.GetMeasuredSharedEdgeCount(), 12); + // the load-bearing assertion: had the reversal gone unrecorded, this would be a box edge long + BOOST_CHECK_SMALL(flipped.GetMaxSharedEdgeDeviation(), 1.e-12); +} + +/// Partial identity is no identity. A face that names no edges looks exactly like a face with no +/// missing neighbours, which is the failure this replaces, so the whole solid falls back on the +/// geometric rim measurement unless *every* face states its edges. +BOOST_AUTO_TEST_CASE(PartialEdgeIdentityFallsBackToTheRimMeasurement) +{ + SurfaceSolid partial("identityPartial"); + addBoxSurfaces(partial, 1., 2., 3.); + const auto identity = boxEdgeIdentity(1., 2., 3.); + for (int faceIndex = 0; faceIndex < 5; ++faceIndex) { // one face left silent + BOOST_REQUIRE(partial.SetSurfaceBoundaryEdges(faceIndex, identity[faceIndex].first, identity[faceIndex].second)); + } + partial.CloseShape(false); + BOOST_CHECK(!partial.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(partial.GetSourceEdgeCount(), 0); + BOOST_CHECK(partial.IsNavigable()); // the geometric verdict, unchanged: the box really is closed + + // and an out-of-range index or mismatched arrays are refused rather than half-applied + BOOST_CHECK(!partial.SetSurfaceBoundaryEdges(6, identity[0].first, identity[0].second)); + BOOST_CHECK(!partial.SetSurfaceBoundaryEdges(0, {1u, 2u}, {0})); +} + +/// The identity is persistent state, not derived state: a solid that loses it on the way through a +/// ROOT file would come back deciding closure by a different rule than the one that was written. +BOOST_AUTO_TEST_CASE(EdgeIdentitySurvivesPersistence) +{ + const auto box = makeIdentifiedBox("identityPersist", 1., 2., 3.); + box->CloseShape(false); + BOOST_REQUIRE(box->HasEdgeIdentity()); + + const auto restored = writeAndReadBack(*box); + BOOST_REQUIRE(restored != nullptr); + BOOST_CHECK(restored->HasEdgeIdentity()); + BOOST_CHECK_EQUAL(restored->GetSourceEdgeCount(), box->GetSourceEdgeCount()); + BOOST_CHECK_EQUAL(restored->GetSharedSourceEdgeCount(), box->GetSharedSourceEdgeCount()); + BOOST_CHECK_EQUAL(restored->GetMeasuredSharedEdgeCount(), box->GetMeasuredSharedEdgeCount()); + checkClose(restored->GetMaxSharedEdgeDeviation(), box->GetMaxSharedEdgeDeviation(), 1.e-18); + checkSolidsIdentical(*box, *restored, 4.5, 5); +} + +/// The version-3 sidecar: the edge identities reach the kernel through the file, a version-2 file +/// still loads and still gets the geometric verdict, and a file that claims v3 without carrying +/// the identities is rejected as truncated rather than parsed into whatever follows. +BOOST_AUTO_TEST_CASE(SidecarV3EdgeIdentityRoundTrip) +{ + constexpr double halfX = 1.; + constexpr double halfY = 2.; + constexpr double halfZ = 3.; + const auto identity = boxEdgeIdentity(halfX, halfY, halfZ); + + const auto boxBytes = [&](uint32_t version, bool writeIdentity) { + std::vector bytes; + appendSidecarHeader(bytes, 6, version, 1.e-7, writeIdentity ? 12u : 0u); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + appendPlaneRecord(bytes, boxFaceFrame(faceIndex, halfX, halfY, halfZ)); + if (version >= 3) { + const auto& ids = identity[faceIndex].first; + const auto& flags = identity[faceIndex].second; + appendU32(bytes, writeIdentity ? static_cast(ids.size()) : 0u); + if (writeIdentity) { + for (size_t e = 0; e < ids.size(); ++e) { + appendU32(bytes, ids[e]); + bytes.push_back(static_cast(flags[e])); + } + } + } + } + return bytes; + }; + + SurfaceSolid v3("sidecarV3Identity"); + const auto v3Path = writeSidecarFile("o2_sidecar_v3_identity.bin", boxBytes(3, true)); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(v3Path.string(), v3)); + std::filesystem::remove(v3Path); + v3.CloseShape(false); + BOOST_CHECK(v3.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(v3.GetSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(v3.GetSharedSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(v3.GetMeasuredSharedEdgeCount(), 12); + BOOST_CHECK_SMALL(v3.GetMaxSharedEdgeDeviation(), 1.e-15); + BOOST_CHECK(v3.IsNavigable()); + TGeoBBox reference("identityBoxReference", halfX, halfY, halfZ); + compareContainsGrid(v3, reference, 4., 7); + checkClose(v3.Capacity(), reference.Capacity(), 1.e-9); + + // a v3 file may legitimately state no identities per face, and then it is a v2 file in all but + // the header: same load, same geometric verdict, and nothing pretends to know the topology + SurfaceSolid v3Silent("sidecarV3Silent"); + const auto silentPath = writeSidecarFile("o2_sidecar_v3_silent.bin", boxBytes(3, false)); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(silentPath.string(), v3Silent)); + std::filesystem::remove(silentPath); + v3Silent.CloseShape(false); + BOOST_CHECK(!v3Silent.HasEdgeIdentity()); + BOOST_CHECK(v3Silent.IsNavigable()); + + // and the same six faces written as version 2 -- the compatibility statement, in one assertion + SurfaceSolid v2("sidecarV2StillLoads"); + const auto v2Path = writeSidecarFile("o2_sidecar_v2_still_loads.bin", boxBytes(2, false)); + BOOST_REQUIRE(o2::cad::LoadSurfaceSolid(v2Path.string(), v2)); + std::filesystem::remove(v2Path); + v2.CloseShape(false); + BOOST_CHECK(!v2.HasEdgeIdentity()); + BOOST_CHECK(v2.IsNavigable()); + checkClose(v2.GetModelTolerance(), 1.e-7, 1.e-18); + + // a v3 header over a v2 body: the counts it reads are the next record's bytes, and a reader that + // resize()s to them is killed rather than reporting anything. It must fail as a parse error. + std::vector mislabelled; + appendSidecarHeader(mislabelled, 6, 2, 1.e-7); + mislabelled[4] = 3; // rewrite the version word in place, leaving a version-2 body behind it + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + appendPlaneRecord(mislabelled, boxFaceFrame(faceIndex, halfX, halfY, halfZ)); + } + SurfaceSolid mislabelledSolid("sidecarV3Mislabelled"); + const auto badPath = writeSidecarFile("o2_sidecar_v3_mislabelled.bin", mislabelled); + BOOST_CHECK(!o2::cad::LoadSurfaceSolid(badPath.string(), mislabelledSolid)); + std::filesystem::remove(badPath); +} + +/// The point of the exercise, stated as a test: the verdict must not depend on how finely a +/// B-spline trim is flattened. Under the old criterion it did, and *inversely* -- tightening +/// kBSplineFlatness shrank the per-chord sagitta faster than it shrank the disagreement it was +/// standing in for, so a better-resolved solid read as more open. +/// Sampling cannot reach this criterion at all: it counts identities, and the deviation it reports +/// is evaluated on the curves rather than on their polylines. +BOOST_AUTO_TEST_CASE(EdgeIdentityVerdictIsIndependentOfChordSampling) +{ + const auto box = makeIdentifiedBox("identitySampling", 1., 2., 3.); + box->CloseShape(false); + const double deviation = box->GetMaxSharedEdgeDeviation(); + const bool navigable = box->IsNavigable(); + + // Resample one face's rim at a different chord count by splitting its wire into eight segments + // instead of four. The rim polylines the geometric measurement compares now differ in phase and + // in count -- the exact situation section 13 shows moving the old verdict -- while the edge + // identity, and every curve it names, is untouched. + SurfaceSolid resampled("identitySamplingResampled"); + const auto identity = boxEdgeIdentity(1., 2., 3.); + for (int faceIndex = 0; faceIndex < 6; ++faceIndex) { + const FaceFrame frame = boxFaceFrame(faceIndex, 1., 2., 3.); + auto corners = rectangleWire(frame.extentU, frame.extentV); + std::vector dense; + std::vector ids; + std::vector flags; + for (size_t segment = 0; segment < corners.size(); ++segment) { + const auto& a = corners[segment]; + const auto& b = corners[(segment + 1) % corners.size()]; + dense.push_back(a); + dense.push_back({0.5 * (a[0] + b[0]), 0.5 * (a[1] + b[1])}); + // both halves of one box edge carry that edge's identity: an edge split for sampling is + // still one edge, and the count has to see it as one + for (int half = 0; half < 2; ++half) { + ids.push_back(identity[faceIndex].first[segment]); + flags.push_back(identity[faceIndex].second[segment]); + } + } + BOOST_REQUIRE(resampled.AddPlanarSurface(frame.origin, frame.axisU, frame.axisV, dense)); + BOOST_REQUIRE(resampled.SetSurfaceBoundaryEdges(faceIndex, ids, flags)); + } + resampled.CloseShape(false); + + // every box edge now appears four times (twice per face, split in half), so it is *not* the + // "exactly twice" case -- which is the honest answer for this deliberately abused fixture, and + // the assertion worth making is that the count says so rather than that it says "closed" + BOOST_CHECK(resampled.HasEdgeIdentity()); + BOOST_CHECK_EQUAL(resampled.GetSourceEdgeCount(), 12); + BOOST_CHECK_EQUAL(resampled.GetNonManifoldSourceEdgeCount(), 12); + + // and the undisturbed box is unmoved by anything sampling-related + BOOST_CHECK_EQUAL(box->IsNavigable(), navigable); + checkClose(box->GetMaxSharedEdgeDeviation(), deviation, 1.e-18); +} + +// --- Sidecar v3 edge identity --- + +// --- Position and scale independence --- +// +// The kernel's length tolerances are absolute, so every number recorded on this branch is a +// statement about centimetre-scale geometry near the origin until someone runs the ladder +// somewhere else. Doing that found one defect, and these tests are what keeps it named. + +namespace +{ +/// The ray/torus quartic exactly as TorusBoundedSurface::appendIntersections builds it, for a +/// torus of the given radii on the origin with axis z. Kept here rather than reaching into the +/// surface class so the test exercises the solver on coefficients a reader can check by hand. +std::array torusRayQuartic(double majorRadius, double minorRadius, + const Point3D& origin, const Point3D& dir) +{ + const double dirDotDir = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]; + const double originDotDir = origin[0] * dir[0] + origin[1] * dir[1] + origin[2] * dir[2]; + const double originDotOrigin = + origin[0] * origin[0] + origin[1] * origin[1] + origin[2] * origin[2]; + const double constantK = majorRadius * majorRadius - minorRadius * minorRadius; + const double transverseE = dir[0] * dir[0] + dir[1] * dir[1]; + const double transverseF = origin[0] * dir[0] + origin[1] * dir[1]; + const double transverseG = origin[0] * origin[0] + origin[1] * origin[1]; + const double fourRSquared = 4. * majorRadius * majorRadius; + return {dirDotDir * dirDotDir, + 4. * dirDotDir * originDotDir, + 4. * originDotDir * originDotDir + 2. * dirDotDir * (originDotOrigin + constantK) - + fourRSquared * transverseE, + 4. * originDotDir * (originDotOrigin + constantK) - 2. * fourRSquared * transverseF, + (originDotOrigin + constantK) * (originDotOrigin + constantK) - + fourRSquared * transverseG}; +} + +/// The offending ray of the x0.1 sweep, given at unit scale; the whole configuration scales with +/// `scale` so the exact solution scales with it too. +std::vector torusRootsAtScale(double scale) +{ + const Point3D origin{2.094269422822338 * scale, 3.292530879918199 * scale, + 1.9347519602583996 * scale}; + const Point3D dir{-0.7547297076674779, -0.03154700875395883, -0.655276929704412}; + const auto c = torusRayQuartic(2.5 * scale, 0.8 * scale, origin, dir); + const auto roots = surf::solveQuarticReal(c[0], c[1], c[2], c[3], c[4]); + return {roots.begin(), roots.end()}; +} +} // namespace + +BOOST_AUTO_TEST_CASE(StreamE_TorusQuarticIsScaleCovariantWhereItWorks) +{ + // Ferrari's method is exactly scale-covariant: scaling the geometry and the ray origin by k + // scales every root by k and nothing else. This is the property the whole sweep rests on, so it + // is asserted rather than assumed. + const auto reference = torusRootsAtScale(1.); + BOOST_REQUIRE_EQUAL(reference.size(), 2u); + for (const double scale : {0.5, 2., 10.}) { + const auto roots = torusRootsAtScale(scale); + BOOST_REQUIRE_EQUAL(roots.size(), reference.size()); + for (size_t i = 0; i < roots.size(); ++i) { + checkClose(roots[i], reference[i] * scale, 1.e-12); + } + } +} + +BOOST_AUTO_TEST_CASE(StreamE_TorusQuarticKeepsEveryRootBelowTheOldResolventGuard) +{ + // The resolvent guard must be dimensionless: it used to compare a cm^2 quantity against a + // length tolerance and silently returned zero roots as the geometry shrank. These three scales + // must all keep finding both roots of a ray that genuinely crosses the torus twice. + BOOST_CHECK_EQUAL(torusRootsAtScale(0.15).size(), 2u); // was above the old guard + BOOST_CHECK_EQUAL(torusRootsAtScale(0.12).size(), 2u); // was below it: every root was lost + BOOST_CHECK_EQUAL(torusRootsAtScale(0.05).size(), 2u); + + // and the roots are exactly the unit-scale ones scaled down, since Ferrari's method is exactly + // scale-covariant -- the property StreamE_TorusQuarticIsScaleCovariantWhereItWorks asserts + // above, now that "where it works" is everywhere. + const auto reference = torusRootsAtScale(1.); + BOOST_REQUIRE_EQUAL(reference.size(), 2u); + for (const double factor : {0.15, 0.12, 0.05, 0.01}) { + const auto roots = torusRootsAtScale(factor); + BOOST_REQUIRE_EQUAL(roots.size(), reference.size()); + for (size_t i = 0; i < roots.size(); ++i) { + BOOST_CHECK_LT(std::abs(roots[i] - reference[i] * factor), 1.e-12 * reference[i] * factor); + } + } + + // And the roots really are there: the quartic changes sign across each of the two crossings the + // unit-scale solve found, scaled down. Without this the counts above could be read as the + // solver merely agreeing with itself. + const double scale = 0.12; + const Point3D origin{2.094269422822338 * scale, 3.292530879918199 * scale, + 1.9347519602583996 * scale}; + const Point3D dir{-0.7547297076674779, -0.03154700875395883, -0.655276929704412}; + const auto c = torusRayQuartic(2.5 * scale, 0.8 * scale, origin, dir); + const auto evaluate = [&c](double t) { + return (((c[0] * t + c[1]) * t + c[2]) * t + c[3]) * t + c[4]; + }; + for (const double root : reference) { + const double t = root * scale; + const double span = 0.02 * t; + BOOST_CHECK_LT(evaluate(t - span) * evaluate(t + span), 0.); + } +} + +// --- Gating any TGeoShape --- +// +// The oracle gate could score exactly one thing: an O2BVHSurfaceSolid loaded from a +// surfaces_.bin sidecar. The four scored queries are TGeoShape virtuals, so the scoring +// loop was never actually specific to that class -- only the loading was. These cases pin the +// two halves of removing that restriction: +// +// 1. the `shape_.root` sidecar convention itself (one TGeoShape-derived object under the +// key "shape"), through the same save/load pair the harness and the fixture generator use, +// so producer and consumer cannot drift; +// 2. that the oracle validators really are representation-agnostic -- with a *negative +// control*, because a validator that reports "0 disagreements" for every input would pass a +// positive-only test while being structurally incapable of failing. + +BOOST_AUTO_TEST_CASE(ShapeSidecarRoundTripsAnyTGeoShape) +{ + namespace harness = o2::cad::harness; + const auto dir = std::filesystem::temp_directory_path(); + + // A TGeoBBox that is *not* centred on the origin. The offset is the point: it is the cheapest + // way for a frame convention to be silently wrong, so it has to survive the round trip. + double origin[3] = {1.0, 1.5, 2.0}; + TGeoBBox box("shape", 1.0, 1.5, 2.0, origin); + + // A TGeoCompositeShape, which is what the CSG emitter will actually hand over: a 4 cm cube with + // an r = 0.8 cm axial through-hole. Built from a TGeoBoolNode rather than from a string + // expression, so no TGeoManager is needed on either side. + auto* cube = new TGeoBBox("cube", 2.0, 2.0, 2.0); + auto* drill = new TGeoTube("drill", 0.0, 0.8, 2.5); + TGeoCompositeShape composite("shape", new TGeoSubtraction(cube, drill, nullptr, nullptr)); + + const std::vector probes{{0.5, 0.5, 0.5}, {1.0, 1.5, 2.0}, {3.0, 1.5, 2.0}, {0.0, 0.0, 0.0}, {1.9, 0.0, 0.0}, {0.0, 0.0, 1.9}, {-1.5, -1.5, 1.0}, {0.79, 0.0, 0.0}, {0.81, 0.0, 0.0}}; + const std::vector directions{{1., 0., 0.}, {0., 1., 0.}, {0., 0., 1.}, {-1., 0., 0.}, {0.6, 0.8, 0.}}; + + for (const TGeoShape* original : {static_cast(&box), + static_cast(&composite)}) { + const std::string path = (dir / (std::string("o2_shape_sidecar_") + original->ClassName() + ".root")).string(); + std::string error; + BOOST_REQUIRE_MESSAGE(harness::saveShapeToRootFile(path, *original, &error), error); + + std::unique_ptr loaded(harness::loadShapeFromRootFile(path, &error)); + BOOST_REQUIRE_MESSAGE(loaded != nullptr, error); + BOOST_CHECK_EQUAL(std::string(loaded->ClassName()), std::string(original->ClassName())); + // TGeoCompositeShape::Capacity() is Monte-Carlo sampled, so this is a loose check by + // necessity -- which is precisely why the gate does not treat capacity as a column for + // composites. 5% is far outside the ~1% MC spread and far inside any real error. + BOOST_CHECK_CLOSE(loaded->Capacity(), original->Capacity(), 5.0); + + // The queries the gate actually scores must be bit-identical across the round trip. + for (const auto& p : probes) { + BOOST_CHECK_EQUAL(loaded->Contains(p.data()), original->Contains(p.data())); + BOOST_CHECK_EQUAL(loaded->Safety(p.data(), original->Contains(p.data())), + original->Safety(p.data(), original->Contains(p.data()))); + for (const auto& d : directions) { + BOOST_CHECK_EQUAL(loaded->DistFromOutside(p.data(), d.data(), 3), + original->DistFromOutside(p.data(), d.data(), 3)); + BOOST_CHECK_EQUAL(loaded->DistFromInside(p.data(), d.data(), 3), + original->DistFromInside(p.data(), d.data(), 3)); + } + } + std::filesystem::remove(path); + } +} + +BOOST_AUTO_TEST_CASE(ShapeSidecarRefusesWhatIsNotAShape) +{ + namespace harness = o2::cad::harness; + const auto dir = std::filesystem::temp_directory_path(); + std::string error; + + BOOST_CHECK(harness::loadShapeFromRootFile((dir / "o2_shape_absent.root").string(), &error) == nullptr); + BOOST_CHECK(!error.empty()); + + // A well-formed ROOT file whose "shape" key holds something else must be refused rather than + // silently ignored: an emitter that writes the wrong object would otherwise look like an + // emitter that wrote nothing, and the part would quietly lose its column. + const std::string path = (dir / "o2_shape_not_a_shape.root").string(); + { + TFile out(path.c_str(), "RECREATE"); + TNamed impostor("shape", "not a shape"); + out.WriteTObject(&impostor, "shape"); + out.Close(); + } + error.clear(); + BOOST_CHECK(harness::loadShapeFromRootFile(path, &error) == nullptr); + BOOST_CHECK(!error.empty()); + std::filesystem::remove(path); +} + +BOOST_AUTO_TEST_CASE(OracleValidatorsScoreAPlainRootShape) +{ + namespace harness = o2::cad::harness; + + // A ROOT primitive with no connection to O2BVHSurfaceSolid at all, and a *wrong* copy of it: + // same shape, radius 0.05 cm too large. Everything below is asserted twice, once for each, so + // no "0 disagreements" here can come from a validator that is unable to report anything else. + constexpr double kR = 1.5; + constexpr double kZ = 2.0; + constexpr double kError = 0.05; + const TGeoTube truth("truth", 0., kR, kZ); + const TGeoTube wrong("wrong", 0., kR + kError, kZ); + + // The oracle columns, built analytically from the tube's own closed form rather than from + // either shape's methods, so `contains` and `safety` are genuinely independent of what is being + // scored. (The two distance columns below are taken from `truth`, which is independent of + // `wrong` -- the case that has to be able to fail.) + const auto trueContains = [](const Point3D& p) { + return (std::hypot(p[0], p[1]) <= kR && std::fabs(p[2]) <= kZ) ? 1 : 0; + }; + const auto trueBoundaryDistance = [](const Point3D& p) { + const double r = std::hypot(p[0], p[1]); + const double dr = kR - r; + const double dz = kZ - std::fabs(p[2]); + if (dr > 0. && dz > 0.) { + return std::min(dr, dz); + } + return std::hypot(std::max(r - kR, 0.), std::max(std::fabs(p[2]) - kZ, 0.)); + }; + + std::vector points; + std::vector containsState; + std::vector boundaryDistance; + for (int ix = -6; ix <= 6; ++ix) { + for (int iy = -6; iy <= 6; ++iy) { + for (int iz = -4; iz <= 4; ++iz) { + const Point3D p{0.31 * ix, 0.29 * iy, 0.53 * iz}; + // Points nearer the wall than the wrong shape's error would be legitimately ambiguous + // for it, so they are dropped: the negative control has to fail on geometry, not on the + // band. Points in the annulus the two shapes disagree about are deliberately kept. + if (std::fabs(trueBoundaryDistance(p)) < 1.e-3) { + continue; + } + points.push_back(p); + containsState.push_back(trueContains(p)); + boundaryDistance.push_back(trueBoundaryDistance(p)); + } + } + } + BOOST_REQUIRE_GT(points.size(), 500u); + + harness::ValidationOptions opt; + opt.meshBand = 1.e-6; // a synthetic shape has no modelling tolerance to hide behind + opt.distanceTolerance = 1.e-9; + + auto containsTruth = harness::validateContainsAgainstOracle(&truth, points, containsState, + boundaryDistance, opt); + auto containsWrong = harness::validateContainsAgainstOracle(&wrong, points, containsState, + boundaryDistance, opt); + BOOST_CHECK_EQUAL(containsTruth.nMismatchUnexplained + containsTruth.nMismatchMissedSurface, 0u); + BOOST_CHECK_GT(containsWrong.nMismatchUnexplained + containsWrong.nMismatchMissedSurface, 0u); + + auto safetyTruth = harness::validateSafetyAgainstOracle(&truth, points, boundaryDistance, opt); + auto safetyWrong = harness::validateSafetyAgainstOracle(&wrong, points, boundaryDistance, opt); + BOOST_CHECK_EQUAL(safetyTruth.nMismatchUnexplained + safetyTruth.nMismatchMissedSurface, 0u); + BOOST_CHECK_GT(safetyWrong.nMismatchUnexplained + safetyWrong.nMismatchMissedSurface, 0u); + + // Rays from well outside, aimed at points spread through the tube, so the hit rate is not + // degenerate; the oracle distance is the nearest positive crossing, exactly as occtOracle.py + // defines it, and the origin classification decides which TGeo entry point is asked. + std::vector rays; + std::vector rayDistance; + std::vector originState; + for (int i = 0; i < 400; ++i) { + const double phi = 0.0173 * i; + const double z = -1.9 + 0.0095 * i; + const Point3D target{0.9 * kR * std::cos(2.1 * phi), 0.9 * kR * std::sin(2.1 * phi), z}; + const Point3D origin{5.0 * std::cos(phi), 5.0 * std::sin(phi), 3.0 - 0.01 * i}; + Point3D dir{target[0] - origin[0], target[1] - origin[1], target[2] - origin[2]}; + const double norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + for (auto& component : dir) { + component /= norm; + } + rays.push_back(harness::Ray{origin, dir}); + rayDistance.push_back(truth.DistFromOutside(origin.data(), dir.data(), 3)); + originState.push_back(trueContains(origin)); + } + + auto distTruth = harness::validateDistanceAgainstOracle(&truth, rays, rayDistance, + /*wantInside=*/false, opt, originState); + auto distWrong = harness::validateDistanceAgainstOracle(&wrong, rays, rayDistance, + /*wantInside=*/false, opt, originState); + BOOST_CHECK_EQUAL(distTruth.nMismatchUnexplained + distTruth.nMismatchMissedSurface, 0u); + BOOST_CHECK_GT(distWrong.nMismatchUnexplained + distWrong.nMismatchMissedSurface, 0u); +} +// --- The CSG emitter's two ROOT-side load-bearing claims --- +// +// The emitter itself is Python (Detectors/CADSupport/tools/cadsupport), and its own self-tests live there. What +// belongs here are the two properties of *ROOT* that the emitted file silently depends on. If a +// future ROOT changes either, every CSG part written by this project becomes wrong geometry that +// still loads, and nothing else in the suite would notice. + +namespace +{ +// Build the emitter's former `placed(primitive, M)` idiom: no TGeoShape in ROOT 6.36 can carry a +// rigid transform (TGeoBBox has fOrigin and nothing else does), and TGeoCompositeShape is the only +// shape that holds a TGeoMatrix at all -- through its TGeoBoolNode, which needs two operands. So a +// recognised tube that was not already on the z axis USED TO BE written as the union of the +// primitive with an identical copy of itself under the same matrix. +// +// It is now written as the bare primitive plus a placement instead. This helper stays, because +// the self-union is +// still exactly the same point set and is therefore the reference the new emission is measured +// against -- see PlacedPrimitiveAnswersExactlyLikeTheSelfUnionComposite. +TGeoCompositeShape* makePlacedTube(const char* name, double rmin, double rmax, double dz, + TGeoMatrix* matrixA, TGeoMatrix* matrixB) +{ + auto* left = new TGeoTube(Form("%s_l", name), rmin, rmax, dz); + auto* right = new TGeoTube(Form("%s_r", name), rmin, rmax, dz); + auto* node = new TGeoUnion(left, right, matrixA, matrixB); + return new TGeoCompositeShape(name, node); +} +} // namespace + +BOOST_AUTO_TEST_CASE(CsgSelfUnionCarriesARigidTransformExactly) +{ + // A tube on an axis that is neither a coordinate axis nor through the origin -- i.e. the + // ExcavatorArm case. Every query on the composite must equal the same query on the bare primitive + // asked in the primitive's own frame, exactly, not within a band. + constexpr double kRmin = 0.4; + constexpr double kRmax = 1.0; + constexpr double kDz = 5.0; + const TGeoTube reference("reference", kRmin, kRmax, kDz); + + auto* rotation = new TGeoRotation("csgRot", 0., 0., 0.); + rotation->RotateX(30.); + rotation->RotateZ(17.); + auto* matrixA = new TGeoCombiTrans(0.3, 5.916, 2.0, rotation); + auto* matrixB = new TGeoCombiTrans(0.3, 5.916, 2.0, rotation); + const TGeoCombiTrans placement(0.3, 5.916, 2.0, rotation); + std::unique_ptr placed( + makePlacedTube("csgPlaced", kRmin, kRmax, kDz, matrixA, matrixB)); + + std::size_t probes = 0; + std::size_t inside = 0; + std::size_t outside = 0; + for (int ix = -8; ix <= 8; ++ix) { + for (int iy = -8; iy <= 8; ++iy) { + for (int iz = -8; iz <= 8; ++iz) { + const Point3D master{0.3 + 0.37 * ix, 5.916 + 0.41 * iy, 2.0 + 0.43 * iz}; + Point3D local{}; + placement.MasterToLocal(master.data(), local.data()); + // A point on the wall is decided by floating-point luck on either side; skip a thin + // shell so the check tests geometry rather than tie-breaking. + const double r = std::hypot(local[0], local[1]); + if (std::fabs(r - kRmin) < 1.e-9 || std::fabs(r - kRmax) < 1.e-9 || + std::fabs(std::fabs(local[2]) - kDz) < 1.e-9) { + continue; + } + ++probes; + const bool wanted = reference.Contains(local.data()); + BOOST_REQUIRE_EQUAL(placed->Contains(master.data()), wanted); + BOOST_REQUIRE_CLOSE_FRACTION(placed->Safety(master.data(), wanted), + reference.Safety(local.data(), wanted), 1.e-12); + wanted ? ++inside : ++outside; + + for (const auto& dir : {Point3D{1., 0., 0.}, Point3D{0., 1., 0.}, Point3D{0., 0., 1.}, + Point3D{0.5773502691896258, 0.5773502691896258, 0.5773502691896258}}) { + Point3D localDir{}; + placement.MasterToLocalVect(dir.data(), localDir.data()); + if (wanted) { + BOOST_REQUIRE_CLOSE_FRACTION(placed->DistFromInside(master.data(), dir.data(), 3), + reference.DistFromInside(local.data(), localDir.data(), 3), + 1.e-12); + } else { + const double got = placed->DistFromOutside(master.data(), dir.data(), 3); + const double want = reference.DistFromOutside(local.data(), localDir.data(), 3); + if (want > 1.e20) { + BOOST_REQUIRE_GT(got, 1.e20); + } else { + BOOST_REQUIRE_CLOSE_FRACTION(got, want, 1.e-12); + } + } + } + } + } + } + // A check that cannot fail is not a check: both classes must actually be populated. + BOOST_CHECK_GT(probes, 2000u); + BOOST_CHECK_GT(inside, 100u); + BOOST_CHECK_GT(outside, 100u); + + // The negative half. The same comparison against a primitive 0.05 cm too wide must disagree, + // otherwise the loop above proves nothing about the transform. + // The probes are placed *in the shell the two disagree about* and then mapped out to the + // master frame, rather than being taken from a lattice that might miss a 0.05 cm shell. + const TGeoTube wrong("wrongReference", kRmin, kRmax + 0.05, kDz); + std::size_t disagreements = 0; + std::size_t shellProbes = 0; + for (int iphi = 0; iphi < 24; ++iphi) { + const double phi = 2. * M_PI * iphi / 24.; + for (int iz = -3; iz <= 3; ++iz) { + const double radius = kRmax + 0.025; + const Point3D local{radius * std::cos(phi), radius * std::sin(phi), 1.3 * iz}; + Point3D master{}; + placement.LocalToMaster(local.data(), master.data()); + ++shellProbes; + disagreements += (placed->Contains(master.data()) != wrong.Contains(local.data())) ? 1 : 0; + } + } + BOOST_CHECK_EQUAL(disagreements, shellProbes); +} + +BOOST_AUTO_TEST_CASE(CsgTwoLeafUnionRoundTripsAndMatchesTheClosedForm) +{ + // The ExcavatorArm ram, in miniature and in closed form: an eye (a tube on x) plus a rod (a solid + // cylinder on z), which is what `tier2-tube-union` emits. The union is checked against the + // membership function of the two cylinders written out by hand, which depends on neither ROOT + // shape, and then the whole composite is pushed through the shape sidecar and checked again -- + // so a streaming defect that dropped a bool node's matrix would be caught here rather than in + // a gate run three steps later. + constexpr double kEyeRmin = 0.7; + constexpr double kEyeRmax = 1.2; + constexpr double kEyeDz = 0.75; + constexpr double kRodR = 0.6; + constexpr double kRodDz = 3.5; + constexpr double kRodCentre = 3.5; // rod spans z in [0, 7] + + auto* eyeRotation = new TGeoRotation("csgEyeRot", 90., 90., 0.); // local z -> global x + auto* eyeMatrix = new TGeoCombiTrans(0., 0., 0., eyeRotation); + auto* rodMatrix = new TGeoTranslation(0., 0., kRodCentre); + auto* eye = new TGeoTube("csgEye", kEyeRmin, kEyeRmax, kEyeDz); + auto* rod = new TGeoTube("csgRod", 0., kRodR, kRodDz); + auto* node = new TGeoUnion(eye, rod, eyeMatrix, rodMatrix); + std::unique_ptr ram(new TGeoCompositeShape("csgRam", node)); + + const auto closedForm = [&](const Point3D& p) { + const double rEye = std::hypot(p[1], p[2]); + const bool inEye = rEye >= kEyeRmin && rEye <= kEyeRmax && std::fabs(p[0]) <= kEyeDz; + const double rRod = std::hypot(p[0], p[1]); + const bool inRod = rRod <= kRodR && p[2] >= 0. && p[2] <= 2. * kRodDz; + return inEye || inRod; + }; + const auto nearWall = [&](const Point3D& p) { + const double rEye = std::hypot(p[1], p[2]); + const double rRod = std::hypot(p[0], p[1]); + return std::fabs(rEye - kEyeRmin) < 1.e-9 || std::fabs(rEye - kEyeRmax) < 1.e-9 || + std::fabs(std::fabs(p[0]) - kEyeDz) < 1.e-9 || std::fabs(rRod - kRodR) < 1.e-9 || + std::fabs(p[2]) < 1.e-9 || std::fabs(p[2] - 2. * kRodDz) < 1.e-9; + }; + + const std::filesystem::path path = + std::filesystem::temp_directory_path() / "o2_csg_ram_shape.root"; + namespace harness = o2::cad::harness; + std::string error; + BOOST_REQUIRE_MESSAGE(harness::saveShapeToRootFile(path.string(), *ram, &error), error); + std::unique_ptr loaded(harness::loadShapeFromRootFile(path.string(), &error)); + BOOST_REQUIRE_MESSAGE(loaded != nullptr, error); + BOOST_CHECK_EQUAL(std::string(loaded->ClassName()), std::string("TGeoCompositeShape")); + + std::size_t inside = 0; + std::size_t outside = 0; + for (int ix = -6; ix <= 6; ++ix) { + for (int iy = -6; iy <= 6; ++iy) { + for (int iz = -4; iz <= 20; ++iz) { + const Point3D p{0.23 * ix, 0.27 * iy, 0.41 * iz}; + if (nearWall(p)) { + continue; + } + const bool wanted = closedForm(p); + BOOST_REQUIRE_EQUAL(ram->Contains(p.data()), wanted); + BOOST_REQUIRE_EQUAL(loaded->Contains(p.data()), wanted); + wanted ? ++inside : ++outside; + } + } + } + BOOST_CHECK_GT(inside, 50u); + BOOST_CHECK_GT(outside, 500u); + + // Capacity is Monte-Carlo for a composite, so it is *reported* and never gated. + // The assertion is stated as scatter rather than as accuracy, because scatter needs no exact + // volume and is the sharper statement: repeated calls returning *different* answers prove the + // method is sampled, and a spread four orders of magnitude above the gate's 1e-6 band proves + // that no capacity criterion could ever be applied to a shape written this way. If a future + // ROOT made TGeoCompositeShape::Capacity() analytic this test would fail, which is the right + // outcome: the emitter's acceptance policy would then be worth revisiting. + double minCapacity = ram->Capacity(); + double maxCapacity = minCapacity; + for (int i = 0; i < 5; ++i) { + const double sampled = ram->Capacity(); + minCapacity = std::min(minCapacity, sampled); + maxCapacity = std::max(maxCapacity, sampled); + } + BOOST_CHECK_GT(minCapacity, 0.); + const double spread = (maxCapacity - minCapacity) / (0.5 * (maxCapacity + minCapacity)); + BOOST_CHECK_GT(spread, 1.e-4); + + std::filesystem::remove(path); +} +// --- The CSG emitter's ROOT-side claims --- + +// ============================================================================================ +// X-ray / geantino transport -- ordered crossing lists +// ============================================================================================ +// +// Everything above this block, and everything the oracle gate measures, is a SINGLE-SHOT query: +// from a point, how far to the surface. A transport loop is different in kind -- step, land on +// the boundary, step again from there -- and its failure modes (a zero-length step, a particle +// that enters and never leaves, a crossing found twice, a step that overshoots) cannot be +// expressed as a disagreement on DistFromOutside from an interior sample. These cases pin the +// properties the X-ray benchmark rests on. +// +// They include XRayTransport.h, which is the SAME header the benchmark binary steps with. That +// is deliberate: a test written against a second implementation of the same idea tests neither. + +#include "XRayTransport.h" + +using namespace o2::cad::xray; +using XRayPoint = o2::cad::harness::Point3D; + +/// A box has exactly two crossings and a hollow tube has four -- and the second fact is the one +/// no single-shot query can express, because DistFromOutside reports the first of the four and +/// stops. Both distances are known in closed form, so this needs no oracle and no fixture. +BOOST_AUTO_TEST_CASE(XRayCrossingListsMatchClosedFormOnPrimitives) +{ + StepConfig cfg; + Robustness stats; + const XRayPoint origin{-5., 0., 0.}; + const XRayPoint dir{1., 0., 0.}; + + TGeoBBox box("xrayBox", 1., 1.5, 2.); + const auto boxCrossings = stepWithShapeApi(&box, origin, dir, 10., cfg, stats); + BOOST_REQUIRE_EQUAL(boxCrossings.size(), 2u); + BOOST_CHECK_SMALL(boxCrossings[0].t - 4., 1.e-12); + BOOST_CHECK_SMALL(boxCrossings[1].t - 6., 1.e-12); + BOOST_CHECK_EQUAL(boxCrossings[0].kind, +1); + BOOST_CHECK_EQUAL(boxCrossings[1].kind, -1); + + TGeoTube tube("xrayTube", 0.5, 1.0, 2.0); + const auto tubeCrossings = stepWithShapeApi(&tube, origin, dir, 10., cfg, stats); + BOOST_REQUIRE_EQUAL(tubeCrossings.size(), 4u); + const double expected[4] = {4.0, 4.5, 5.5, 6.0}; + const int senses[4] = {+1, -1, +1, -1}; + for (int i = 0; i < 4; ++i) { + BOOST_CHECK_SMALL(tubeCrossings[i].t - expected[i], 1.e-12); + BOOST_CHECK_EQUAL(tubeCrossings[i].kind, senses[i]); + } + BOOST_CHECK_EQUAL(stats.zeroLengthSteps, 0); + BOOST_CHECK_EQUAL(stats.nonAdvancingSteps, 0); + BOOST_CHECK_EQUAL(stats.unstickPushes, 0); +} + +/// The transport-level BVH == _Loop guard. +/// +/// `DistanceBVHMatchesLoopOnAllFixtures` above compares the twins one query at a time from +/// generated points. This compares whole ORDERED CROSSING LISTS produced by stepping, where every +/// query after the first starts from a point the previous query put on a boundary. That is a +/// harder condition and a different one: a traversal-order difference that is invisible on an +/// isolated query can still send the two loops down different sequences of states. +BOOST_AUTO_TEST_CASE(XRayCrossingListsAgreeBetweenBVHAndLoopOnAllFixtures) +{ + StepConfig cfg; + std::array, double>, 7> fixtures{{ + {makeBoxSolid("xrayLoopBox", 1., 2., 3.), 4.}, + {makeTubeSolid("xrayLoopTube", 0., 2., 3.), 4.}, + {makeTubeSolid("xrayLoopHollowTube", 1., 2., 3.), 4.}, + {makeConeSolid("xrayLoopCone", 2., 1., 3.), 4.}, + {makeSphereSolid("xrayLoopSphere", 2.5), 3.5}, + {makeTorusSolid("xrayLoopTorus", 3., 1.), 4.5}, + {makeCapsuleSolid("xrayLoopCapsule", 1., 1.5), 3.}, + }}; + size_t comparedRays = 0; + size_t comparedCrossings = 0; + for (const auto& [solid, extent] : fixtures) { + BOOST_TEST_CONTEXT("fixture = " << solid->GetName()) + { + BOOST_REQUIRE(solid->HasBVH()); + const XRayPoint lo{-extent, -extent, -extent}; + const XRayPoint hi{extent, extent, extent}; + // A fan rather than the three axes: a parallel beam is direction-poor, and the point of this + // case is to exercise many ray/surface configurations per fixture. + const Raster raster = buildRaster(lo, hi, 9, buildFanBeams(11), 0.); + for (const auto& ray : raster.rays) { + Robustness bvhStats; + Robustness loopStats; + const auto viaBVH = + stepWithShapeApi(solid.get(), ray.origin, ray.dir, ray.tMax, cfg, bvhStats); + // The non-BVH twins, stepped through the identical loop: only the traversal differs. + const auto viaLoop = stepCrossingsWithKernels( + ray.origin, ray.dir, ray.tMax, cfg, loopStats, + [&solid](const double* p) { return solid->Contains_Loop(p); }, + [&solid](const double* p, const double* d) { return solid->DistFromOutside_Loop(p, d); }, + [&solid](const double* p, const double* d) { return solid->DistFromInside_Loop(p, d); }); + BOOST_REQUIRE_EQUAL(viaBVH.size(), viaLoop.size()); + for (size_t i = 0; i < viaBVH.size(); ++i) { + BOOST_CHECK_EQUAL(viaBVH[i].kind, viaLoop[i].kind); + // Bit-identical is the contract: both minimise over the same hits from the same kernels. + BOOST_CHECK_EQUAL(viaBVH[i].t, viaLoop[i].t); + } + comparedRays += 1; + comparedCrossings += viaBVH.size(); + } + } + } + BOOST_CHECK_GT(comparedRays, 2000u); + BOOST_CHECK_GT(comparedCrossings, 2000u); +} + +/// The comparator's own positive AND negative controls. A comparison that cannot fail is not a +/// comparison, and the distinction this one has to preserve is LOST (a wall a track walks +/// through) against DISPLACED (a wrong step length) -- merging them was the first version's bug. +BOOST_AUTO_TEST_CASE(XRayCrossingComparatorCatchesInjectedDefects) +{ + const std::vector truth{{4.0, +1}, {4.5, -1}, {5.5, +1}, {6.0, -1}}; + const double tolerance = 1.e-6; + + ListComparison clean; + compareLists(truth, truth, {}, {}, tolerance, clean); + BOOST_CHECK_EQUAL(clean.raysIdentical, 1); + BOOST_CHECK_EQUAL(clean.matched, 4); + BOOST_CHECK_EQUAL(clean.missing, 0); + BOOST_CHECK_EQUAL(clean.extra, 0); + BOOST_CHECK_EQUAL(clean.displaced, 0); + + auto perturbed = truth; + perturbed[2].t += 1.e-3; + ListComparison displaced; + compareLists(perturbed, truth, {}, {}, tolerance, displaced); + BOOST_CHECK_EQUAL(displaced.raysIdentical, 0); + BOOST_CHECK_EQUAL(displaced.displaced, 1); + BOOST_CHECK_EQUAL(displaced.missing, 0); // a moved crossing is NOT a lost one + BOOST_CHECK_EQUAL(displaced.extra, 0); + BOOST_CHECK_SMALL(displaced.worstDeltaT - 1.e-3, 1.e-12); + + auto dropped = truth; + dropped.erase(dropped.begin() + 1); + ListComparison lost; + compareLists(dropped, truth, {}, {}, tolerance, lost); + BOOST_CHECK_EQUAL(lost.missing, 1); + BOOST_CHECK_EQUAL(lost.extra, 0); + + auto doubled = truth; + doubled.insert(doubled.begin() + 1, {4.2, -1}); + ListComparison spurious; + compareLists(doubled, truth, {}, {}, tolerance, spurious); + BOOST_CHECK_EQUAL(spurious.extra, 1); + BOOST_CHECK_EQUAL(spurious.missing, 0); + + auto flipped = truth; + flipped[1].kind = +1; + ListComparison sense; + compareLists(flipped, truth, {}, {}, tolerance, sense); + BOOST_CHECK_EQUAL(sense.kindMismatch, 1); + + // A crossing moved by LESS than the tolerance must not be reported at all, or every run would + // drown in last-digit noise. + auto nudged = truth; + nudged[0].t += 1.e-9; + ListComparison quiet; + compareLists(nudged, truth, {}, {}, tolerance, quiet); + BOOST_CHECK_EQUAL(quiet.raysIdentical, 1); + BOOST_CHECK_EQUAL(quiet.displaced, 0); +} + +/// The parity audit is the only check in the benchmark that is independent of the stepping: both +/// modes produce an alternating list by construction, so `nonAlternating` can never fire on them. +/// Asking Contains() at the midpoint of every interval is what can contradict a list. +BOOST_AUTO_TEST_CASE(XRayParityAuditContradictsATruncatedList) +{ + StepConfig cfg; + TGeoBBox box("xrayParityBox", 1., 1., 1.); + const XRayPoint origin{-5., 0., 0.}; + const XRayPoint dir{1., 0., 0.}; + + Robustness good; + auditCrossingList({{4.0, +1}, {6.0, -1}}, &box, origin, dir, 10., cfg, good); + BOOST_CHECK_EQUAL(good.parityMismatchIntervals, 0); + BOOST_CHECK_EQUAL(good.oddCrossingLists, 0); + BOOST_CHECK_SMALL(good.insideLength - 2., 1.e-12); + + Robustness truncated; + auditCrossingList({{4.0, +1}}, &box, origin, dir, 10., cfg, truncated); + BOOST_CHECK_GT(truncated.parityMismatchIntervals, 0); + BOOST_CHECK_EQUAL(truncated.oddCrossingLists, 1); + + Robustness invented; + auditCrossingList({{1.0, +1}, {2.0, -1}, {4.0, +1}, {6.0, -1}}, &box, origin, dir, 10., cfg, + invented); + BOOST_CHECK_GT(invented.parityMismatchIntervals, 0); +} + +/// The chord integral is EXACT for an axis-aligned box whose raster window is its own bounding +/// box, at every raster density. No convergence argument and no tolerance: either the quadrature +/// is the volume or it is not. This is what fixed the raster geometry -- with the window inflated +/// by 2 % instead, the same box came out 5.1e-02 too large at N = 32. +BOOST_AUTO_TEST_CASE(XRayChordIntegralIsExactForABoxAndConvergesForASphere) +{ + StepConfig cfg; + TGeoBBox box("xrayVolBox", 1., 1.5, 2.); + for (const int n : {5, 16, 41}) { + const Raster raster = buildRaster({-1., -1.5, -2.}, {1., 1.5, 2.}, n, buildBeams("xyz", 0.), 0.); + Robustness stats; + std::vector byBeam(raster.beams.size(), 0.); + for (const auto& ray : raster.rays) { + const double before = stats.insideLength; + const auto crossings = stepWithShapeApi(&box, ray.origin, ray.dir, ray.tMax, cfg, stats); + auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, stats); + byBeam[ray.beam] += stats.insideLength - before; + } + BOOST_CHECK_SMALL(chordVolume(raster, byBeam) - 24., 1.e-9); + } + + // A curved silhouette cannot be exact at finite N. The bound below is the MEASURED envelope + // over N = 24..192 (2e-3), not a convergence rate -- the convergence is NOT monotone in N, + // because the silhouette cells realign with the lattice at every density. That is the reason + // this benchmark's volume is quoted with its raster density and never extrapolated. + TGeoSphere sphere("xrayVolSphere", 0., 1.); + const double exact = 4. / 3. * 3.14159265358979323846; + for (const int n : {24, 96}) { + const Raster raster = buildRaster({-1., -1., -1.}, {1., 1., 1.}, n, buildBeams("z", 0.), 0.); + Robustness stats; + for (const auto& ray : raster.rays) { + const auto crossings = stepWithShapeApi(&sphere, ray.origin, ray.dir, ray.tMax, cfg, stats); + auditCrossingList(crossings, nullptr, ray.origin, ray.dir, ray.tMax, cfg, stats); + } + const double volume = stats.insideLength * raster.cellArea[0]; + BOOST_CHECK_LT(std::fabs(volume - exact) / exact, 2.e-3); + } +} + +/// The raster's own contract, because every number above depends on it: the rays start strictly +/// outside the solid, the lattice covers the bounding box, and a fan is direction-diverse where +/// the axis beams are not. The last property is not cosmetic -- it is why the fan finds the torus +/// quartic defect at x0.1 and the three axis beams do not. +BOOST_AUTO_TEST_CASE(XRayRasterRaysStartOutsideAndFansAreDirectionDiverse) +{ + TGeoBBox box("xrayRasterBox", 1., 1.5, 2.); + const Raster raster = buildRaster({-1., -1.5, -2.}, {1., 1.5, 2.}, 8, buildBeams("xyz", 0.), 0.); + BOOST_CHECK_EQUAL(raster.rays.size(), 3u * 8u * 8u); + for (const auto& ray : raster.rays) { + BOOST_REQUIRE(!box.Contains(ray.origin.data())); + // and the far end must be outside too, so the window really does bracket the solid + const double end[3] = {ray.origin[0] + ray.tMax * ray.dir[0], + ray.origin[1] + ray.tMax * ray.dir[1], + ray.origin[2] + ray.tMax * ray.dir[2]}; + BOOST_REQUIRE(!box.Contains(end)); + } + + const auto axes = buildBeams("xyz", 0.); + const auto fan = buildFanBeams(64); + BOOST_CHECK_EQUAL(axes.size(), 3u); + BOOST_CHECK_EQUAL(fan.size(), 64u); + for (const auto& beams : {axes, fan}) { + for (const auto& beam : beams) { + BOOST_CHECK_SMALL(dot3(beam.dir, beam.dir) - 1., 1.e-12); + BOOST_CHECK_SMALL(dot3(beam.u, beam.v), 1.e-12); + BOOST_CHECK_SMALL(dot3(beam.u, beam.dir), 1.e-12); + BOOST_CHECK_SMALL(dot3(beam.v, beam.dir), 1.e-12); + } + } + // Direction diversity, stated as a number: the axis beams are mutually orthogonal and nothing + // else, while no two fan beams are closer than a few degrees and they span the sphere. + double worstFanAlignment = -1.; + for (size_t i = 0; i < fan.size(); ++i) { + for (size_t j = i + 1; j < fan.size(); ++j) { + worstFanAlignment = std::max(worstFanAlignment, std::fabs(dot3(fan[i].dir, fan[j].dir))); + } + } + BOOST_CHECK_LT(worstFanAlignment, 0.999); +} +// --- X-ray / geantino transport benchmark --- + +// --- Dimensionally consistent guards in the quartic root solver --- +// +// solveQuarticReal used to decide all three of its branches with kTolerance -- 1e-9 *cm*, a +// length -- applied to quantities that are not lengths: +// +// |termQ| <= kTolerance selects the biquadratic branch; termQ scales as L^3 +// resolvent > kTolerance licenses Ferrari's second stage; resolvent scales as L^2 +// |derivative| > kTolerance licenses a Newton polishing step; the derivative scales as L^3 +// +// Two consequences: +// +// * the resolvent guard fails and the function returns the EMPTY root set, so a ray silently +// misses a torus it does cross; +// * the termQ guard misroutes an asymmetric quartic into the biquadratic branch -- which +// *assumes* termQ = 0 and forces the roots to be symmetric about -b/4 -- so it returns +// confidently wrong roots instead of the right ones. That is worse than a miss, because a +// miss at least leaves a visible gap. +// +// The trigger is the ratio of the ray's lever arm to the feature it hits, not the model's scale: +// the reproducer below is real, *unscaled* ALICE3 geometry, a ray 375 cm from a 0.1 cm tube. +// +// These cases pin the repair from both sides. It is not enough that the previously-failing case +// now works: the branches exist for real reasons, so the biquadratic branch must still be +// *selected* for a true biquadratic, and both branches must still *decline* a configuration that +// genuinely has no real roots. A guard that always passes would satisfy neither. + +namespace +{ +/// The relative backward error of \a x as a root of a4 x^4 + ... + a0: |p(x)| divided by the sum +/// of the magnitudes of the terms that produced it. Scale-free, so it means the same thing for a +/// torus 400 cm away and one 0.1 cm across, which is the whole point of this block. +double quarticBackwardError(const std::array& coefficients, double x) +{ + double value = 0., magnitude = 0., power = 1.; + for (int i = 0; i < 5; ++i) { + const double term = coefficients[4 - i] * power; + value += term; + magnitude += std::abs(term); + power *= x; + } + return magnitude > 0. ? std::abs(value) / magnitude : std::abs(value); +} + +/// The monic quartic with exactly these four real roots, from the elementary symmetric functions. +std::array quarticFromRoots(double r1, double r2, double r3, double r4) +{ + return {1., -(r1 + r2 + r3 + r4), + r1 * r2 + r1 * r3 + r1 * r4 + r2 * r3 + r2 * r4 + r3 * r4, + -(r1 * r2 * r3 + r1 * r2 * r4 + r1 * r3 * r4 + r2 * r3 * r4), r1 * r2 * r3 * r4}; +} + +std::vector sortedRoots(const std::array& c, surf::QuarticBranch* branch = nullptr) +{ + const auto found = surf::solveQuarticReal(c[0], c[1], c[2], c[3], c[4], branch); + std::vector roots(found.begin(), found.end()); + std::sort(roots.begin(), roots.end()); + return roots; +} + +/// Every returned root must actually be a root, to the precision of the coefficients themselves. +void checkRootsAreRoots(const std::array& c, const std::vector& roots) +{ + for (const double root : roots) { + BOOST_CHECK_LT(quarticBackwardError(c, root), 1.e-12); + } +} +} // namespace + +BOOST_AUTO_TEST_CASE(StreamM_QuarticFindsTheALICE3ProductionScaleRoots) +{ + // ALICE3 part ST2487462_01, face 47: a torus of R = 5.3 cm and + // r = 0.1 cm, hit by a ray whose origin is 375 cm away. The crossing lies on the untrimmed + // surface to 1.7e-14 cm and inside both parameter windows -- the patch is there and the trim + // admits it -- and the solver returned nothing. + // + // It is a *biquadratic* (the ray is perpendicular to the torus axis, so the true termQ is 0), + // but termQ is evaluated as d - b*c/2 + b^3/8 from terms of magnitude ~1e8 and cancels to + // -5.96e-08 rather than to 0. That is above the absolute 1e-9 test, so the quartic was routed + // into the resolvent branch, whose resolvent is 7.1e-15 and fails its own absolute test. + const std::array c{1.0, -1501.7280000044018, 845808.25396968238, -211752288.545858, + 19882619385.616932}; + + // The reference roots are Newton's method run to convergence on the *exact* binary values of + // those five coefficients in 60-digit decimal arithmetic, so they are the truth for this input + // and not another double-precision solve. + const double firstRoot = 375.3392295779947145; + const double secondRoot = 375.5247704240909448; + + // The tolerance is not arbitrary. p'(firstRoot) = -14.47, so one ulp of a0 (3.8e-06 at 1.99e10) + // moves this root by 2.6e-07 cm: the input coefficients do not determine the roots better than + // that. 1e-06 cm is a few times the conditioning limit and four orders below the 0.1 cm tube + // whose crossing this is. + surf::QuarticBranch branch = surf::QuarticBranch::NotAQuartic; + const auto roots = sortedRoots(c, &branch); + BOOST_REQUIRE_EQUAL(roots.size(), 2u); + checkClose(roots[0], firstRoot, 1.e-6); + checkClose(roots[1], secondRoot, 1.e-6); + checkRootsAreRoots(c, roots); + + // and it must get there by recognising the biquadratic, not by luck in the resolvent branch + BOOST_CHECK(branch == surf::QuarticBranch::Biquadratic); +} + +BOOST_AUTO_TEST_CASE(StreamM_QuarticIsScaleInvariantOnAnAsymmetricQuartic) +{ + // A thoroughly well-conditioned quartic with four simple real roots {1, 2, 3, 7}, uniformly + // scaled. Ferrari's method is exactly scale-covariant, so every one of these must return four + // roots at k times the reference ones -- there is no numerical excuse anywhere in this sweep. + // + // Before the repair this collapsed at k = 1e-04, where |termQ| = 5.6e-10 falls under the + // absolute 1e-09 test: the solver takes the biquadratic branch on a quartic that is not + // biquadratic and returns *two* roots, 6.5093e-04 and -9.3257e-07, instead of four. + for (const double k : {1.e6, 1.e3, 1., 1.e-1, 1.e-2, 1.e-3, 1.e-4, 1.e-5, 1.e-6, 1.e-8}) { + const auto c = quarticFromRoots(1. * k, 2. * k, 3. * k, 7. * k); + surf::QuarticBranch branch = surf::QuarticBranch::NotAQuartic; + const auto roots = sortedRoots(c, &branch); + BOOST_REQUIRE_EQUAL(roots.size(), 4u); + const double expected[4] = {1. * k, 2. * k, 3. * k, 7. * k}; + for (int i = 0; i < 4; ++i) { + BOOST_CHECK_LT(std::abs(roots[i] - expected[i]), 1.e-9 * std::abs(expected[i])); + } + checkRootsAreRoots(c, roots); + // The other half of the positive control: an asymmetric quartic must NOT be routed into the + // biquadratic branch at any scale. A termQ test that always passed would fail here. + BOOST_CHECK(branch == surf::QuarticBranch::Resolvent); + } + + // The same statement made about accuracy rather than about the branch, on the family that + // produced "two confidently wrong roots": at k = 1e-04 the shipped code returns four roots for + // {-2, -1, 1, 2.1} * k that are wrong by 1.3 % (relative backward error 1.6e-02), because the + // biquadratic branch forces them to be symmetric about -b/4 and they are not. + for (const double k : {1., 1.e-2, 1.e-4, 1.e-6}) { + const auto c = quarticFromRoots(-2. * k, -1. * k, 1. * k, 2.1 * k); + const auto roots = sortedRoots(c); + BOOST_REQUIRE_EQUAL(roots.size(), 4u); + const double expected[4] = {-2. * k, -1. * k, 1. * k, 2.1 * k}; + for (int i = 0; i < 4; ++i) { + BOOST_CHECK_LT(std::abs(roots[i] - expected[i]), 1.e-9 * std::abs(expected[i])); + } + checkRootsAreRoots(c, roots); + } +} + +BOOST_AUTO_TEST_CASE(StreamM_QuarticStillSelectsTheBiquadraticBranch) +{ + // Positive control, first direction. The biquadratic branch is not a fallback: it is the + // correct, better-conditioned answer whenever the depressed quartic really has no odd term, and + // a repair that simply widened its guard into irrelevance would be caught by the previous case + // while a repair that narrowed it away would be caught here. + { + // y^4 - 5 y^2 + 4, roots +-1, +-2; termQ is exactly zero + const std::array c{1., 0., -5., 0., 4.}; + surf::QuarticBranch branch = surf::QuarticBranch::NotAQuartic; + const auto roots = sortedRoots(c, &branch); + BOOST_CHECK(branch == surf::QuarticBranch::Biquadratic); + BOOST_REQUIRE_EQUAL(roots.size(), 4u); + const double expected[4] = {-2., -1., 1., 2.}; + for (int i = 0; i < 4; ++i) { + checkClose(roots[i], expected[i], 1.e-12); + } + } + // The same quartic shifted along x and scaled, which is what a torus at a lever arm produces: + // termQ is zero in exact arithmetic but is computed by cancelling terms of size |b|^3, so the + // criterion has to be relative to those terms rather than to a fixed length. + // The centres stop at 100. Beyond that the *depression* step -- p, q, r from b, c, d, e -- is + // the limit, not the guards: it cancels numbers of size (centre)^k to leave numbers of size + // (spread)^k, so a quartic whose roots agree to 4 significant figures has lost 8 digits before + // any branch is chosen and Ferrari's discriminants become noise. Measured on this family, with + // rounded coefficients, both before and after this change: relative root spread 2e-01 gives + // 3.9e-14, 2e-02 gives 6.2e-11, 2e-03 gives 9.3e-08, and 2e-04 returns no roots at all. It is a + // property of Ferrari's method and is not hidden behind a looser tolerance here. + for (const double centre : {0., 1., 100.}) { + for (const double k : {1., 1.e-3, 1.e3}) { + const auto c = quarticFromRoots((centre - 2.) * k, (centre - 1.) * k, (centre + 1.) * k, + (centre + 2.) * k); + surf::QuarticBranch branch = surf::QuarticBranch::NotAQuartic; + const auto roots = sortedRoots(c, &branch); + BOOST_CHECK(branch == surf::QuarticBranch::Biquadratic); + BOOST_REQUIRE_EQUAL(roots.size(), 4u); + const double expected[4] = {(centre - 2.) * k, (centre - 1.) * k, (centre + 1.) * k, + (centre + 2.) * k}; + for (int i = 0; i < 4; ++i) { + BOOST_CHECK_LT(std::abs(roots[i] - expected[i]), 1.e-9 * std::max(1.e-30, std::abs(expected[i]))); + } + checkRootsAreRoots(c, roots); + } + } +} + +BOOST_AUTO_TEST_CASE(StreamM_QuarticStillDeclinesDegenerateConfigurations) +{ + // Positive control, second direction. A guard that always passes is not a fix. Each of these + // must still produce no roots, in both branches, at every scale -- "declines" has to survive + // the repair as surely as "accepts" does. + { + // not a genuine quartic at all + surf::QuarticBranch branch = surf::QuarticBranch::Biquadratic; + const auto roots = surf::solveQuarticReal(0., 1., 2., 3., 4., &branch); + BOOST_CHECK_EQUAL(roots.size(), 0u); + BOOST_CHECK(branch == surf::QuarticBranch::NotAQuartic); + } + for (const double k : {1.e4, 1., 1.e-4, 1.e-8}) { + // (x^2 + k^2)(x^2 + 4 k^2): no real roots, termQ = 0 -> the biquadratic branch must decline + const double k2 = k * k; + const std::array biquadratic{1., 0., 5. * k2, 0., 4. * k2 * k2}; + surf::QuarticBranch branch = surf::QuarticBranch::NotAQuartic; + BOOST_CHECK_EQUAL(sortedRoots(biquadratic, &branch).size(), 0u); + BOOST_CHECK(branch == surf::QuarticBranch::Biquadratic); + + // (x^2 + k^2)((x + k)^2 + 4 k^2): no real roots, termQ != 0 -> the resolvent branch must + // decline, by finding both of Ferrari's quadratics complex rather than by refusing to run + const std::array asymmetric{1., 2. * k, 6. * k2, 2. * k2 * k, 5. * k2 * k2}; + BOOST_CHECK_EQUAL(sortedRoots(asymmetric, &branch).size(), 0u); + BOOST_CHECK(branch == surf::QuarticBranch::Resolvent); + } +} + +BOOST_AUTO_TEST_CASE(StreamM_QuarticHasNoCliffAsAQuarticApproachesBiquadratic) +{ + // The defect is a *cliff*: an absolute threshold crossed by a quantity that carries units, so + // the answer changes discontinuously with the size of the geometry. The repair has to be + // continuous instead -- as termQ is driven to zero the two branches must agree, because they + // are the two sides of one limit. + // + // {-2, -1, 1, 2 + delta} scaled by k: at delta = 0 the quartic is exactly biquadratic, and + // delta walks it away from that continuously. Every point must give four correct roots. + for (const double k : {1., 1.e-2, 1.e-4, 1.e-6}) { + for (const double delta : {1.e-1, 1.e-3, 1.e-6, 1.e-9, 1.e-12, 0.}) { + const auto c = quarticFromRoots(-2. * k, -1. * k, 1. * k, (2. + delta) * k); + const auto roots = sortedRoots(c); + BOOST_REQUIRE_EQUAL(roots.size(), 4u); + const double expected[4] = {-2. * k, -1. * k, 1. * k, (2. + delta) * k}; + for (int i = 0; i < 4; ++i) { + BOOST_CHECK_LT(std::abs(roots[i] - expected[i]), 1.e-8 * std::abs(expected[i])); + } + checkRootsAreRoots(c, roots); + } + } +} +// --- Tier 0: canonical recognition of NURBS-encoded quadrics +// +// The recognition work itself is entirely converter-side and its own controls live in +// `O2_CADtoTGeo.py --self-test` (18 checks: a NurbsConvert-ed quadric of each kind must be +// recovered, a genuine free-form patch must not, and every accepted face's MEASURED gap must be +// inside the acceptance tolerance). Nothing of that can be asserted from C++. +// +// What *can* be asserted here, and matters more than it looks, is the kernel-side contract the +// converter measures against. `_recognized_inner_wall()` decides a recognized quadric's +// `inner_wall` flag by comparing the face's own outward normal with "away from the axis", because +// on a NURBS-encoded quadric `TopoDS` orientation says nothing (on ALICE3: nine +// ALICE3 faces with an exactly antiparallel outward normal, 404 lost crossings, and every closure +// and edge-identity check blind to it because they are all sign-blind). That measurement is only +// correct if the kernel's own convention is the one it assumes. This work multiplies the number +// of faces going through that path, so the convention is pinned rather than assumed: if it were +// ever inverted, every recognized face would silently flip and no existing test would notice. +BOOST_AUTO_TEST_CASE(StreamK_InnerWallIsExactlyTheSignOfTheOutwardNormal) +{ + const Point3D centre{0.3, -0.7, 1.1}; + const Point3D axis{0., 0., 1.}; + const Point3D refU{1., 0., 0.}; + constexpr double radius = 2.5; + constexpr double phi = 0.9; + + // A point on each surface, and the direction "away from the axis / centre" there. + const double cx = centre[0] + radius * std::cos(phi); + const double cy = centre[1] + radius * std::sin(phi); + const Double_t onCylinder[3] = {cx, cy, centre[2] + 0.4}; + const Double_t awayFromAxis[3] = {std::cos(phi), std::sin(phi), 0.}; + + for (const bool innerWall : {false, true}) { + SurfaceSolid solid(innerWall ? "streamK_innerCyl" : "streamK_outerCyl"); + BOOST_REQUIRE(solid.AddCylindricalSurface(centre, axis, refU, radius, -1., 1., 0., surf::kTwoPi, innerWall)); + Double_t n[3] = {0., 0., 0.}; + solid.ComputeNormal(onCylinder, nullptr, n); + const double alignment = n[0] * awayFromAxis[0] + n[1] * awayFromAxis[1] + n[2] * awayFromAxis[2]; + // Exactly +1 or exactly -1: this is a sign, not a tolerance. + BOOST_CHECK_CLOSE(alignment, innerWall ? -1. : 1., 1.e-9); + } + + // The same convention on the cone and on the sphere. All three go through the converter's one + // `_recognized_inner_wall` measurement, and ALICE3 exercises only the cylinder branch today + // (recognized planes and spheres are untested there), so the + // other two are pinned here rather than left to the first model that uses them. + for (const bool innerWall : {false, true}) { + SurfaceSolid solid(innerWall ? "streamK_innerCone" : "streamK_outerCone"); + // r(h) = 1 + h over h in [0, 2]: half-angle 45 degrees, apex at h = -1. + BOOST_REQUIRE(solid.AddConicalSurface(centre, axis, refU, 1., 3., 0., 2., 0., surf::kTwoPi, innerWall)); + const double h = 1.0; + const double r = 2.0; + const Double_t onCone[3] = {centre[0] + r * std::cos(phi), centre[1] + r * std::sin(phi), centre[2] + h}; + Double_t n[3] = {0., 0., 0.}; + solid.ComputeNormal(onCone, nullptr, n); + // The cone's outward normal tilts out of the radial direction by the half angle, so the + // radial component is what carries the sign -- which is exactly the reasoning + // `_recognized_inner_wall` relies on, and the reason it can use the radial direction alone. + const double radial = n[0] * std::cos(phi) + n[1] * std::sin(phi); + BOOST_CHECK_GT(innerWall ? -radial : radial, 0.5); + } + + for (const bool innerWall : {false, true}) { + SurfaceSolid solid(innerWall ? "streamK_innerSph" : "streamK_outerSph"); + BOOST_REQUIRE(solid.AddSphericalSurface(centre, axis, refU, radius, 0., surf::kPi, 0., surf::kTwoPi, innerWall)); + const double theta = 1.1; + const Double_t onSphere[3] = {centre[0] + radius * std::sin(theta) * std::cos(phi), + centre[1] + radius * std::sin(theta) * std::sin(phi), + centre[2] + radius * std::cos(theta)}; + const double outward[3] = {std::sin(theta) * std::cos(phi), std::sin(theta) * std::sin(phi), std::cos(theta)}; + Double_t n[3] = {0., 0., 0.}; + solid.ComputeNormal(onSphere, nullptr, n); + const double alignment = n[0] * outward[0] + n[1] * outward[1] + n[2] * outward[2]; + BOOST_CHECK_CLOSE(alignment, innerWall ? -1. : 1., 1.e-9); + } +} +// --- Placed primitives --- +// +// A recognised primitive whose frame is not the identity used to be emitted as a degenerate +// TGeoCompositeShape -- the primitive unioned with an identical copy of itself under the same +// matrix -- because no TGeoShape in ROOT 6.36 carries a rigid transform. That is still true of +// ROOT; what changed is where the transform lives. The shape is now written in its OWN canonical +// frame and the transform travels beside it, as a TGeoHMatrix under the key "placement" in +// shape_.root. These cases pin the three things that can go wrong with that: +// +// 1. the artefact: the placement must survive the round trip, and its ABSENCE must keep meaning +// the identity, so that every file written before this convention still loads and still +// scores exactly as it did; +// 2. the equivalence: the bare primitive queried in its own frame must answer *exactly* like +// the composite it replaces, with a negative control that moves the count; +// 3. the composition order in geom.C -- `partPlacement * shapePlacement`. That one is silent +// when wrong: the geometry still builds and the shape is still the right shape, it is simply +// somewhere else. It is checked by navigating, with a transposed rotation and a reversed +// product as the controls. + +namespace +{ +/// The two matrices a placed tube is defined by in these cases: a rotation that is neither +/// symmetric nor axis-aligned, off the origin. +TGeoCombiTrans* makeStreamNPlacement() +{ + auto* rotation = new TGeoRotation("streamNRot", 0., 0., 0.); + rotation->RotateX(30.); + rotation->RotateZ(17.); + rotation->RotateY(-41.); + return new TGeoCombiTrans(0.3, 5.916, 2.0, rotation); +} +} // namespace + +BOOST_AUTO_TEST_CASE(ShapeSidecarRoundTripsAPlacement) +{ + namespace harness = o2::cad::harness; + const auto dir = std::filesystem::temp_directory_path(); + const std::string path = (dir / "o2_shape_placed.root").string(); + + const TGeoTube tube("shape", 0.4, 1.0, 5.0); + std::unique_ptr placement(makeStreamNPlacement()); + + std::string error; + BOOST_REQUIRE_MESSAGE(harness::saveShapeToRootFile(path, tube, placement.get(), &error), error); + + std::unique_ptr loaded(harness::loadShapeFromRootFile(path, &error)); + BOOST_REQUIRE_MESSAGE(loaded != nullptr, error); + BOOST_CHECK_EQUAL(std::string(loaded->ClassName()), std::string("TGeoTube")); + + std::unique_ptr back(harness::loadShapePlacementFromRootFile(path)); + BOOST_REQUIRE(back != nullptr); + for (int i = 0; i < 9; ++i) { + BOOST_CHECK_EQUAL(back->GetRotationMatrix()[i], placement->GetRotationMatrix()[i]); + } + for (int i = 0; i < 3; ++i) { + BOOST_CHECK_EQUAL(back->GetTranslation()[i], placement->GetTranslation()[i]); + } + // The point of storing it: a point of the part frame reaches the same place through the file as + // through the original matrix. + const Point3D master{0.9, 6.2, 3.1}; + Point3D viaFile{}; + Point3D viaOriginal{}; + back->MasterToLocal(master.data(), viaFile.data()); + placement->MasterToLocal(master.data(), viaOriginal.data()); + for (int i = 0; i < 3; ++i) { + BOOST_CHECK_EQUAL(viaFile[i], viaOriginal[i]); + } + std::filesystem::remove(path); +} + +BOOST_AUTO_TEST_CASE(AbsentPlacementMeansIdentity) +{ + namespace harness = o2::cad::harness; + const auto dir = std::filesystem::temp_directory_path(); + const TGeoTube tube("shape", 0.4, 1.0, 5.0); + std::string error; + + // 1. The historical two-argument overload -- the one every existing shape_*.root was written + // with -- must record no placement at all. + const std::string legacy = (dir / "o2_shape_legacy.root").string(); + BOOST_REQUIRE_MESSAGE(harness::saveShapeToRootFile(legacy, tube, &error), error); + BOOST_CHECK(harness::loadShapePlacementFromRootFile(legacy) == nullptr); + + // 2. An identity placement is deliberately NOT written, so that "no key" stays the one and only + // spelling of the identity. + const std::string identity = (dir / "o2_shape_identity.root").string(); + TGeoHMatrix unit("unit"); + BOOST_REQUIRE_MESSAGE(harness::saveShapeToRootFile(identity, tube, &unit, &error), error); + BOOST_CHECK(harness::loadShapePlacementFromRootFile(identity) == nullptr); + + // 3. A file that is not there is the same answer, and must not throw or complain: a part with + // no shape sidecar at all is the overwhelmingly common case. + BOOST_CHECK(harness::loadShapePlacementFromRootFile((dir / "o2_shape_nothing.root").string()) == + nullptr); + + std::filesystem::remove(legacy); + std::filesystem::remove(identity); +} + +BOOST_AUTO_TEST_CASE(PlacedPrimitiveAnswersExactlyLikeTheSelfUnionComposite) +{ + // The equivalence the change rests on: the bare primitive queried in its own frame answers like + // the composite it replaces, on all four scored queries, exactly. + constexpr double kRmin = 0.4; + constexpr double kRmax = 1.0; + constexpr double kDz = 5.0; + + std::unique_ptr placement(makeStreamNPlacement()); + const TGeoTube placedPrimitive("streamNTube", kRmin, kRmax, kDz); + // The old emission, built here so the two are compared rather than one being trusted. + std::unique_ptr composite( + makePlacedTube("streamNComposite", kRmin, kRmax, kDz, new TGeoCombiTrans(*placement), + new TGeoCombiTrans(*placement))); + + std::size_t probes = 0; + std::size_t inside = 0; + std::size_t disagreements = 0; + // The negative control travels with the check: the same loop against a 5% fatter tube must + // disagree, or the loop is not measuring anything. + const TGeoTube wrong("streamNWrong", kRmin, kRmax * 1.05, kDz); + std::size_t controlDisagreements = 0; + + for (int ix = -8; ix <= 8; ++ix) { + for (int iy = -8; iy <= 8; ++iy) { + for (int iz = -8; iz <= 8; ++iz) { + const Point3D master{0.3 + 0.37 * ix, 5.916 + 0.41 * iy, 2.0 + 0.43 * iz}; + Point3D local{}; + placement->MasterToLocal(master.data(), local.data()); + const double r = std::hypot(local[0], local[1]); + if (std::fabs(r - kRmin) < 1.e-9 || std::fabs(r - kRmax) < 1.e-9 || + std::fabs(r - kRmax * 1.05) < 1.e-9 || std::fabs(std::fabs(local[2]) - kDz) < 1.e-9) { + continue; + } + ++probes; + const bool wanted = composite->Contains(master.data()); + if (placedPrimitive.Contains(local.data()) != wanted) { + ++disagreements; + } + if (wrong.Contains(local.data()) != wanted) { + ++controlDisagreements; + } + if (wanted) { + ++inside; + } + BOOST_REQUIRE_CLOSE_FRACTION(placedPrimitive.Safety(local.data(), wanted), + composite->Safety(master.data(), wanted), 1.e-12); + for (const auto& dir : {Point3D{1., 0., 0.}, Point3D{0., 1., 0.}, Point3D{0., 0., 1.}, + Point3D{0.5773502691896258, 0.5773502691896258, + 0.5773502691896258}}) { + Point3D localDir{}; + placement->MasterToLocalVect(dir.data(), localDir.data()); + if (wanted) { + BOOST_REQUIRE_CLOSE_FRACTION(placedPrimitive.DistFromInside(local.data(), + localDir.data(), 3), + composite->DistFromInside(master.data(), dir.data(), 3), + 1.e-12); + } else { + const double got = placedPrimitive.DistFromOutside(local.data(), localDir.data(), 3); + const double want = composite->DistFromOutside(master.data(), dir.data(), 3); + if (want > 1.e20) { + BOOST_REQUIRE_GT(got, 1.e20); + } else { + BOOST_REQUIRE_CLOSE_FRACTION(got, want, 1.e-12); + } + } + } + } + } + } + BOOST_CHECK_EQUAL(disagreements, 0u); + BOOST_CHECK_GT(controlDisagreements, 0u); + BOOST_CHECK_GT(inside, 100u); + BOOST_CHECK_GT(probes, 3000u); +} + +BOOST_AUTO_TEST_CASE(PlacedPrimitiveRecoversTheAnalyticCapacity) +{ + // What the degenerate composite cost, stated as a measurement rather than as a claim. + constexpr double kRmin = 0.4; + constexpr double kRmax = 1.0; + constexpr double kDz = 5.0; + const double analytic = TMath::Pi() * (kRmax * kRmax - kRmin * kRmin) * 2. * kDz; + + const TGeoTube tube("streamNCapTube", kRmin, kRmax, kDz); + BOOST_CHECK_CLOSE_FRACTION(tube.Capacity(), analytic, 1.e-14); + // Deterministic: asked twice, the same bits. + BOOST_CHECK_EQUAL(tube.Capacity(), tube.Capacity()); + + std::unique_ptr placement(makeStreamNPlacement()); + std::unique_ptr composite( + makePlacedTube("streamNCapComposite", kRmin, kRmax, kDz, new TGeoCombiTrans(*placement), + new TGeoCombiTrans(*placement))); + // ... whereas TGeoCompositeShape::Capacity() throws 10000 Monte-Carlo points into the bounding + // box, so two calls on the same object return different numbers. That is the reason the gate + // marks a composite `capacityComparable=false`, and the reason a placed primitive that is no + // longer a composite gets its capacity column back. + const double first = composite->Capacity(); + const double second = composite->Capacity(); + BOOST_CHECK_NE(first, second); + BOOST_CHECK_GT(std::fabs(first - analytic) / analytic, 1.e-6); +} + +BOOST_AUTO_TEST_CASE(NodeMatrixIsPartPlacementTimesShapePlacement) +{ + // The composition geom.C emits, decided by NAVIGATION rather than by reading the code. + // + // The reference is built without ever forming the product: a point of the assembly frame is + // carried into the part frame by the part placement, then into the shape's frame by the shape + // placement, and the tube membership is evaluated there. If `partPlacement * shapePlacement` is + // the right node matrix, ROOT's navigator must reach the same verdict for every point. + constexpr double kRmin = 0.4; + constexpr double kRmax = 1.0; + constexpr double kDz = 5.0; + + std::unique_ptr shapePlacementOwned(makeStreamNPlacement()); + const TGeoHMatrix shapePlacement(*shapePlacementOwned); + auto* partRotation = new TGeoRotation("streamNPartRot", 37., 24., 61.); + const TGeoCombiTrans partPlacement(-2.0, 7.0, 1.5, partRotation); + + const auto reference = [&](const Point3D& master, bool& onWall) { + Point3D partFrame{}; + Point3D shapeFrame{}; + partPlacement.MasterToLocal(master.data(), partFrame.data()); + shapePlacement.MasterToLocal(partFrame.data(), shapeFrame.data()); + const double r = std::hypot(shapeFrame[0], shapeFrame[1]); + onWall = std::fabs(r - kRmin) < 1.e-9 || std::fabs(r - kRmax) < 1.e-9 || + std::fabs(std::fabs(shapeFrame[2]) - kDz) < 1.e-9; + return r >= kRmin && r <= kRmax && std::fabs(shapeFrame[2]) <= kDz; + }; + + // Every candidate node matrix, including the three ways of getting it wrong. `partOnly` is the + // bug this test is really for: forgetting to compose at all. + TGeoHMatrix correct(partPlacement); + correct.Multiply(&shapePlacement); + TGeoHMatrix reversed(shapePlacement); + reversed.Multiply(&partPlacement); + TGeoHMatrix transposedRotation(shapePlacement); + { + double rt[9]; + const double* r = shapePlacement.GetRotationMatrix(); + for (int i = 0; i < 3; ++i) { + for (int j = 0; j < 3; ++j) { + rt[3 * i + j] = r[3 * j + i]; + } + } + transposedRotation.SetRotation(rt); + transposedRotation.SetBit(TGeoMatrix::kGeoRotation); + } + TGeoHMatrix withTransposed(partPlacement); + withTransposed.Multiply(&transposedRotation); + const TGeoHMatrix partOnly(partPlacement); + + const std::vector> candidates{ + {"part*shape", &correct}, + {"shape*part", &reversed}, + {"part*shape^T", &withTransposed}, + {"part only", &partOnly}}; + + // The lattice is centred where the solid actually is -- the translation of the CORRECT node + // matrix -- and spans more than the tube's own extent. Guessing the centre by adding the two + // translations put every probe outside the solid, and the controls then reported zero + // disagreements while being structurally incapable of reporting anything else. + const double* centre = correct.GetTranslation(); + + std::vector disagreements(candidates.size(), 0); + size_t probes = 0; + size_t insideProbes = 0; + + for (size_t c = 0; c < candidates.size(); ++c) { + // One manager per candidate, and everything inside it allocated with new: a TGeoShape and a + // TGeoVolume register themselves with gGeoManager, which frees them. + auto* manager = new TGeoManager(("streamN_" + std::to_string(c)).c_str(), "composition order"); + auto* material = new TGeoMaterial("Vacuum", 0., 0., 0.); + auto* medium = new TGeoMedium("Vacuum", 1, material); + auto* world = new TGeoVolume("TOP", new TGeoBBox("streamNWorld", 30., 30., 30.), medium); + auto* part = new TGeoVolume("PART", new TGeoTube("streamNNodeTube", kRmin, kRmax, kDz), medium); + world->AddNode(part, 1, new TGeoHMatrix(*candidates[c].second)); + manager->SetTopVolume(world); + manager->CloseGeometry(); + + size_t localProbes = 0; + size_t localInside = 0; + for (int ix = -14; ix <= 14; ++ix) { + for (int iy = -14; iy <= 14; ++iy) { + for (int iz = -14; iz <= 14; ++iz) { + const Point3D master{centre[0] + 0.45 * ix, centre[1] + 0.47 * iy, + centre[2] + 0.43 * iz}; + bool onWall = false; + const bool wanted = reference(master, onWall); + if (onWall) { + continue; + } + ++localProbes; + if (wanted) { + ++localInside; + } + TGeoNode* node = manager->FindNode(master[0], master[1], master[2]); + const bool got = node != nullptr && std::string(node->GetVolume()->GetName()) == "PART"; + if (got != wanted) { + ++disagreements[c]; + } + } + } + } + probes = localProbes; + insideProbes = localInside; + delete manager; + gGeoManager = nullptr; + } + + // The sampling has to be capable of failing: enough points, and enough of them inside. + BOOST_CHECK_GT(probes, 5000u); + BOOST_CHECK_GT(insideProbes, 200u); + BOOST_CHECK_EQUAL(disagreements[0], 0u); // partPlacement * shapePlacement + BOOST_CHECK_GT(disagreements[1], 0u); // reversed product + BOOST_CHECK_GT(disagreements[2], 0u); // transposed shape rotation + BOOST_CHECK_GT(disagreements[3], 0u); // shape placement dropped +} + +// ============================================================================================ +// The representation cost/memory benchmark's own instruments +// ============================================================================================ +// +// These pin the MEASURING apparatus, not the geometry. A per-call cost table is only worth +// reading if the harness that produced it can be shown to move its number when the thing it +// measures moves. Each case below is that demonstration for one column of the benchmark. +// +// They include RepresentationBench.h -- the SAME header the benchmark binary times with. + +#include "RepresentationBench.h" + +using namespace o2::cad::bench; + +/// The timing harness runs the requested passes over a sample set that exercises both branches. +BOOST_AUTO_TEST_CASE(RepBenchTimingHarnessRunsTheRequestedPasses) +{ + TGeoBBox fast("repBenchFast", 1., 1., 1.); + const o2::cad::harness::Point3D lo{-1., -1., -1.}; + const o2::cad::harness::Point3D hi{1., 1., 1.}; + const QuerySamples samples = buildQuerySamples(&fast, "control", lo, hi, 1500, 1500); + + // The sample set has to be capable of exercising both branches, or three of the four kernels + // are being timed on an empty vector. + BOOST_CHECK_GT(samples.insidePoints, 100); + BOOST_CHECK_LT(samples.insidePoints, static_cast(samples.points.size()) - 100); + BOOST_CHECK_EQUAL(samples.outsideRays.size(), 1500u); + BOOST_CHECK_EQUAL(samples.insideRays.size(), 1500u); + + // And the loop must not have been optimised away: a non-zero checksum, a positive time, and + // the requested number of passes actually run. + const TimingStat stat = timeContainsPass(&fast, samples, 1, 5); + BOOST_CHECK_NE(stat.checksum, 0u); + BOOST_CHECK_GT(stat.medianNsPerCall, 0.); + BOOST_CHECK_EQUAL(stat.passes, 5); + BOOST_CHECK_LE(stat.minNsPerCall, stat.medianNsPerCall); + BOOST_CHECK_LE(stat.medianNsPerCall, stat.maxNsPerCall); +} + +/// The sample set is the whole basis of "the same questions from the same sample sets": every +/// representation of a part is handed this one object. So its labels must agree with the +/// reference that produced them, and rays must actually reach the solid -- a DistFromOutside +/// column measured on rays that all miss prices the early-out, not the kernel. +BOOST_AUTO_TEST_CASE(RepBenchSampleSetIsReproducibleAndActuallyHits) +{ + TGeoTube tube("repBenchTube", 0.3, 1., 2.); + const o2::cad::harness::Point3D lo{-1., -1., -2.}; + const o2::cad::harness::Point3D hi{1., 1., 2.}; + const QuerySamples a = buildQuerySamples(&tube, "surface", lo, hi, 2000, 2000); + const QuerySamples b = buildQuerySamples(&tube, "surface", lo, hi, 2000, 2000); + + // Same seed, same bbox, same reference -> bit-identical. Without this the cost table's + // "same sample set" claim is not checkable from outside. + BOOST_REQUIRE_EQUAL(a.points.size(), b.points.size()); + for (size_t i = 0; i < a.points.size(); ++i) { + BOOST_CHECK_EQUAL(a.points[i][0], b.points[i][0]); + BOOST_CHECK_EQUAL(a.pointIsInside[i], b.pointIsInside[i]); + BOOST_CHECK_EQUAL(a.pointIsInside[i] != 0, tube.Contains(a.points[i].data())); + } + BOOST_CHECK_GT(timeDistOutPass(&tube, a, 1, 3).hitFraction, 0.5); + BOOST_CHECK_EQUAL(timeDistInPass(&tube, a, 1, 3).hitFraction, 1.); +} + +/// Both memory columns have to move when memory moves, and the heap column has to come back when +/// it is released. The 64 MB block is deliberately over glibc's mmap threshold: `uordblks` alone +/// does not see such an allocation at all, which is exactly how this check earned its place. +/// Linux only: `readMemory()` reads /proc/self/statm and mallinfo2, both no-ops elsewhere. +#ifdef __linux__ +BOOST_AUTO_TEST_CASE(RepBenchMemoryProbeSeesAnAllocationAndItsRelease) +{ + const MemorySnapshot before = readMemory(); + constexpr size_t kBytes = 64u << 20; + auto block = std::make_unique(kBytes); + for (size_t i = 0; i < kBytes; i += 4096) { + block[i] = static_cast(i); + } + const MemorySnapshot delta = readMemory() - before; + BOOST_CHECK_GT(delta.residentBytes, 32LL << 20); + BOOST_CHECK_GT(delta.heapInUseBytes, 32LL << 20); + block.reset(); + BOOST_CHECK_LT((readMemory() - before).heapInUseBytes, 8LL << 20); +} +#endif + +/// The synthetic boolean ladder is a fixture whose whole purpose is a scaling exponent, so the +/// structure it claims has to be the structure it built -- and the two tree shapes have to be +/// genuinely different, or the "chain vs balanced" column compares a thing with itself. +BOOST_AUTO_TEST_CASE(RepBenchBooleanLadderHasTheStructureItClaims) +{ + auto* manager = new TGeoManager("repBenchLadder", "ladder"); + for (const int k : {2, 4, 8, 16, 32}) { + const BooleanTreeStats chain = + booleanTreeStats(buildBooleanLadder(k, LadderShape::Chain, "tC" + std::to_string(k))); + const BooleanTreeStats balanced = + booleanTreeStats(buildBooleanLadder(k, LadderShape::Balanced, "tB" + std::to_string(k))); + BOOST_CHECK_EQUAL(chain.leaves, k); + BOOST_CHECK_EQUAL(balanced.leaves, k); + BOOST_CHECK_EQUAL(chain.nodes, k - 1); + BOOST_CHECK_EQUAL(balanced.nodes, k - 1); + BOOST_CHECK_EQUAL(chain.depth, k); + BOOST_CHECK_EQUAL(balanced.depth, 1 + static_cast(std::lround(std::log2(k)))); + } + // A single leaf is not a composite at all: the ladder must hand back the primitive rather than + // a one-sided union, or the K=1 baseline row would be priced with boolean machinery. + TGeoShape* single = buildBooleanLadder(1, LadderShape::Balanced, "tOne"); + BOOST_CHECK(dynamic_cast(single) == nullptr); + BOOST_CHECK_EQUAL(booleanTreeStats(single).leaves, 1); + delete manager; + gGeoManager = nullptr; +} + +// --- Representation cost/memory benchmark --- + +// --- BVH-accelerated Safety and ComputeNormal --- +// +// Safety() and ComputeNormal() answer the same question -- which trimmed patch is nearest to this +// point -- and both used to answer it with a bare loop over every patch, which on ALICE3's +// 965-patch solid cost 812 us per call. They now walk the +// BVH that was already there. The oracle is the loop they replaced, kept as Safety_Loop() / +// ComputeNormal_Loop(), and the contract against it is *exact equality*, not agreement to a +// tolerance: both minimise the same distanceSqToPatch over the same patches under the same +// tie-break, so any difference at all is a traversal or pruning bug. + +namespace +{ +// The benchmark's deterministic LCG, seeded explicitly, so a failure is reproducible from the seed alone. +struct SampleStream { + explicit SampleStream(std::uint64_t seed) { lcg.state = seed | 1u; } + o2::cad::bench::detail::Lcg lcg; + double uniform() { return lcg.next(); } + double symmetric(double extent) { return (2. * uniform() - 1.) * extent; } +}; + +/// Points in every regime the two kernels have to survive, for one solid of half-extent \a extent. +/// +/// The regimes are not decoration. Pruning is trivially correct where one patch is far nearer than +/// all others and only bites where several are comparably near, which is exactly *on* the surface; +/// and a point far outside the bounding box is the case where the node bound is large and a +/// rounding error in it would prune the whole tree. So the sample is deliberately loaded towards +/// the surface and towards infinity rather than being uniform in the box. +std::vector> nearestPatchSample(const SurfaceSolid& solid, double extent, int count) +{ + SampleStream stream(0x5EAFE7Full); + std::vector> points; + points.reserve(static_cast(count)); + for (int index = 0; index < count; ++index) { + std::array point{stream.symmetric(extent), stream.symmetric(extent), stream.symmetric(extent)}; + switch (index % 5) { + case 0: // wherever it landed: inside or outside, generic + break; + case 1: { // walked onto the surface along its own normal, i.e. distance ~ 0 + std::array normal{0., 0., 0.}; + const double safety = solid.Safety_Loop(point.data(), solid.Contains(point.data())); + solid.ComputeNormal_Loop(point.data(), nullptr, normal.data()); + const double sign = solid.Contains(point.data()) ? 1. : -1.; + for (int dimension = 0; dimension < 3; ++dimension) { + point[dimension] += sign * safety * normal[dimension]; + } + break; + } + case 2: { // a hair off the surface, at the scale where several patches compete + std::array normal{0., 0., 0.}; + const double safety = solid.Safety_Loop(point.data(), solid.Contains(point.data())); + solid.ComputeNormal_Loop(point.data(), nullptr, normal.data()); + const double sign = solid.Contains(point.data()) ? 1. : -1.; + const double offset = safety - sign * 1.e-9 * std::max(1., extent); + for (int dimension = 0; dimension < 3; ++dimension) { + point[dimension] += sign * offset * normal[dimension]; + } + break; + } + case 3: // well outside the bounding box + for (auto& coordinate : point) { + coordinate *= 40.; + } + break; + case 4: // very far away, where the node bound is large and its rounding is worst + for (auto& coordinate : point) { + coordinate *= 1.e7; + } + break; + } + points.push_back(point); + } + // and the exact centre, where every face of a box is equidistant: the tie the tie-break decides + points.push_back({0., 0., 0.}); + return points; +} + +/// Compare the accelerated nearest-patch kernels against their loop twins at one point. Returns +/// the number of disagreements, so a caller can both assert zero and count. +int countNearestPatchDisagreements(const SurfaceSolid& solid, const std::array& point, + double* worstSafetyGap = nullptr) +{ + int disagreements = 0; + for (const bool inside : {true, false}) { + const double accelerated = solid.Safety(point.data(), inside); + const double reference = solid.Safety_Loop(point.data(), inside); + if (accelerated != reference) { + ++disagreements; + } + if (worstSafetyGap != nullptr) { + *worstSafetyGap = std::max(*worstSafetyGap, accelerated - reference); + } + } + // with and without a direction, since the direction flips the sign of the chosen patch's normal + // and a wrong patch can hide behind that flip + const std::array direction = unitDirection(0.37, -0.82, 0.44); + for (const double* dir : {static_cast(nullptr), direction.data()}) { + std::array accelerated{0., 0., 0.}; + std::array reference{0., 0., 0.}; + solid.ComputeNormal(point.data(), dir, accelerated.data()); + solid.ComputeNormal_Loop(point.data(), dir, reference.data()); + for (int dimension = 0; dimension < 3; ++dimension) { + if (accelerated[dimension] != reference[dimension]) { + ++disagreements; + } + } + } + return disagreements; +} + +// A solid with many patches whose boxes overlap heavily: eight boxes on a line is the easy case +// for pruning, a shell of small boxes around a sphere is not. +std::unique_ptr makeManyPatchSolid(const char* name, int ringCount) +{ + auto solid = std::make_unique(name); + for (int ring = 0; ring < ringCount; ++ring) { + const double angle = surf::kTwoPi * ring / ringCount; + addBoxSurfaces(*solid, 0.4, 0.4, 0.4, {3. * std::cos(angle), 3. * std::sin(angle), 0.}); + } + solid->CloseShape(); + return solid; +} + +// A quarter of a circle of radius r about (cu, cv) in a parametric domain, as a rational quadratic +// B-spline -- the public-API twin of the kernel-level quarterCircleBSpline above. +BoundaryCurve quarterCircleBoundary(double cu, double cv, double r, double a0) +{ + const double a1 = a0 + surf::kHalfPi; + const double aMid = 0.5 * (a0 + a1); + std::vector poles{{cu + r * std::cos(a0), cv + r * std::sin(a0)}, + {cu + r * std::sqrt(2.) * std::cos(aMid), cv + r * std::sqrt(2.) * std::sin(aMid)}, + {cu + r * std::cos(a1), cv + r * std::sin(a1)}}; + return BoundaryCurve::makeBSpline(2, std::move(poles), {1., std::sqrt(0.5), 1.}, {0., 0., 0., 1., 1., 1.}); +} + +// Four cylindrical windows cut by B-spline wires in (phi, h), plus two disks. Not a closed solid -- +// it is not meant to be navigated -- but it is the *trim* family measured at 2-6 us per +// candidate patch, whose distanceSqToPatch walks a flattened polyline. That is where pruning has +// the most to save and where a wrong bound would cost the most, so the cross-check has to cover it. +std::unique_ptr makeWireTrimmedSolid(const char* name) +{ + auto solid = std::make_unique(name); + for (int window = 0; window < 4; ++window) { + const double centrePhi = surf::kHalfPi * window + 0.3; + BOOST_REQUIRE(solid->AddCylindricalSurface( + {0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0., surf::kTwoPi, false, + {quarterCircleBoundary(centrePhi, 0., 0.5, 0.), quarterCircleBoundary(centrePhi, 0., 0.5, surf::kHalfPi), + quarterCircleBoundary(centrePhi, 0., 0.5, surf::kPi), + quarterCircleBoundary(centrePhi, 0., 0.5, 3. * surf::kHalfPi)})); + } + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., 1.}, {1., 0., 0.}, {0., 1., 0.}, 2.)); + BOOST_REQUIRE(addDiskSurface(*solid, {0., 0., -1.}, {1., 0., 0.}, {0., -1., 0.}, 2.)); + solid->CloseShape(); + return solid; +} + +struct NearestPatchFixture { + std::unique_ptr solid; + double extent; +}; + +std::vector nearestPatchFixtures() +{ + std::vector fixtures; + fixtures.push_back({makeBoxSolid("safetyBox", 1., 2., 3.), 4.}); + fixtures.push_back({makeTubeSolid("safetyTube", 0., 2., 3.), 4.}); + fixtures.push_back({makeTubeSolid("safetyHollowTube", 1., 2., 3.), 4.}); + fixtures.push_back({makeConeSolid("safetyCone", 2., 1., 3.), 4.}); + fixtures.push_back({makeSphereSolid("safetySphere", 2.5), 3.5}); + fixtures.push_back({makeTorusSolid("safetyTorus", 3., 1.), 4.5}); + fixtures.push_back({makeCapsuleSolid("safetyCapsule", 1., 1.5), 3.}); + fixtures.push_back({makeManyPatchSolid("safetyRing", 12), 4.5}); + fixtures.push_back({makeWireTrimmedSolid("safetyWireTrim"), 3.}); + return fixtures; +} +} // namespace + +/// The invariant the whole acceleration rests on, pinned at the level it is a property of. +/// +/// A node is pruned when the distance from the query point to its bounding box already exceeds the +/// best patch distance found so far. That is only sound if the box distance is a **lower** bound on +/// the distance to every patch inside it -- and the box is built from each surface's own +/// conservativeBounds(), so per surface the statement is +/// +/// distance(point, conservativeBounds) <= sqrt(distanceSqToPatch(point)) for every point. +/// +/// It holds because every distanceSqToPatch in BoundedSurface.h is realised on the patch's +/// *untrimmed* window -- the wire itself for the planar families, the full rim band for a cylinder +/// or cone, the full sphere, the full torus -- and each family's conservativeBounds() encloses +/// exactly that window. Reading the code says so; this measures it, on every surface family, from +/// points in every regime. If a future surface family returned a distance realised outside its own +/// bounds, Safety() would silently start answering too much and only this case would say so. +BOOST_AUTO_TEST_CASE(StreamS_PatchDistanceIsNeverBelowTheDistanceToItsOwnBoundingBox) +{ + using surf::Vec2; + using surf::Vec3; + std::string error; + + surf::PlanarBoundedSurface polygon; + const std::vector rectangle{{0., 0.}, {2., 0.}, {2., 3.}, {0., 3.}}; + BOOST_REQUIRE(polygon.initialize({0., 0., 0.}, {1., 0., 0.}, {0., 1., 0.}, rectangle, {}, error)); + surf::CurvedPlanarBoundedSurface disk; + BOOST_REQUIRE(disk.initialize({0., 0., 0.5}, {1., 0., 0.}, {0., 1., 0.}, + {surf::Curve2D::makeCircle({0., 0.}, 1.5)}, {}, error)); + // a B-spline trim wire on a second planar face: the trim family found to dominate the + // per-patch cost, and the one whose distanceSqToPatch walks a flattened polyline rather than a + // closed form -- so the lower-bound claim has to hold for an approximated boundary too + surf::CurvedPlanarBoundedSurface splineFace; + BOOST_REQUIRE(splineFace.initialize({0.2, -0.3, 1.1}, {1., 0., 0.}, {0., 1., 0.}, + {quarterCircleBSpline(0., 0., 1.2, 0.), + quarterCircleBSpline(0., 0., 1.2, surf::kHalfPi), + quarterCircleBSpline(0., 0., 1.2, surf::kPi), + quarterCircleBSpline(0., 0., 1.2, 3. * surf::kHalfPi)}, + {}, error)); + surf::CylindricalBoundedSurface cylinder; + BOOST_REQUIRE(cylinder.initialize({0.1, -0.2, 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., 0.3, + 1.7 * surf::kPi, false, error)); + surf::ConicalBoundedSurface cone; + BOOST_REQUIRE(cone.initialize({0., 0., 0.}, {0., 1., 0.}, {1., 0., 0.}, 2., 0.5, -1., 1., 0., 1.1 * surf::kPi, + false, error)); + surf::SphericalBoundedSurface sphere; + BOOST_REQUIRE(sphere.initialize({0.3, 0.4, -0.5}, {0., 0., 1.}, {1., 0., 0.}, 1.7, 0.2, 2.4, 0., + 1.3 * surf::kPi, false, error)); + surf::TorusBoundedSurface torus; + BOOST_REQUIRE(torus.initialize({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 3., 0.8, 0., 1.4 * surf::kPi, 0., + 1.9 * surf::kPi, false, error)); + + const std::vector surfaces{&polygon, &disk, &splineFace, + &cylinder, &cone, &sphere, + &torus}; + + SampleStream stream(0xB0B0Dull); + size_t checked = 0; + for (const auto* surface : surfaces) { + Vec3 lower{TGeoShape::Big(), TGeoShape::Big(), TGeoShape::Big()}; + Vec3 upper{-TGeoShape::Big(), -TGeoShape::Big(), -TGeoShape::Big()}; + surface->conservativeBounds(lower, upper); + const double boxLower[3] = {lower.xCoord, lower.yCoord, lower.zCoord}; + const double boxUpper[3] = {upper.xCoord, upper.yCoord, upper.zCoord}; + // the box the BVH actually stores is this one inflated outward, which only lowers the bound + for (int sample = 0; sample < 4000; ++sample) { + const double scale = (sample % 4 == 3) ? 1.e6 : ((sample % 4 == 2) ? 20. : 5.); + const Vec3 point{stream.symmetric(scale), stream.symmetric(scale), stream.symmetric(scale)}; + const double coordinates[3] = {point.xCoord, point.yCoord, point.zCoord}; + double boxDistanceSq = 0.; + for (int dimension = 0; dimension < 3; ++dimension) { + if (coordinates[dimension] < boxLower[dimension]) { + const double gap = boxLower[dimension] - coordinates[dimension]; + boxDistanceSq += gap * gap; + } else if (coordinates[dimension] > boxUpper[dimension]) { + const double gap = coordinates[dimension] - boxUpper[dimension]; + boxDistanceSq += gap * gap; + } + } + const double patchDistanceSq = surface->distanceSqToPatch(point); + // the direction of this inequality is the whole safety argument; a tolerance would hide the + // failure it exists to catch, so it is asserted with the same relative guard the traversal + // itself applies (1e-12) and nothing more + BOOST_REQUIRE_LE(boxDistanceSq * (1. - 1.e-12), patchDistanceSq); + ++checked; + } + } + BOOST_CHECK_EQUAL(checked, 7u * 4000u); +} + +/// Accelerated == brute force, exactly, for both kernels, over every fixture and every regime. +BOOST_AUTO_TEST_CASE(StreamS_SafetyAndNormalAreIdenticalToTheAllSurfacesLoop) +{ + size_t comparedPoints = 0; + double worstSafetyGap = -std::numeric_limits::infinity(); + for (const auto& fixture : nearestPatchFixtures()) { + BOOST_TEST_CONTEXT("fixture = " << fixture.solid->GetName()) + { + BOOST_REQUIRE(fixture.solid->HasBVH()); + for (const auto& point : nearestPatchSample(*fixture.solid, fixture.extent, 2000)) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_REQUIRE_EQUAL(countNearestPatchDisagreements(*fixture.solid, point, &worstSafetyGap), 0); + } + ++comparedPoints; + } + } + } + BOOST_CHECK_EQUAL(comparedPoints, 9u * 2001u); + // exact equality, so the gap is not merely non-positive but identically zero + BOOST_CHECK_EQUAL(worstSafetyGap, 0.); +} + +/// The traversal must also be right before there is anything to traverse, and on a solid whose +/// surface set is empty -- the two states where the accelerated path has to fall back rather than +/// crash or answer something else. +BOOST_AUTO_TEST_CASE(StreamS_SafetyFallsBackBeforeCloseShapeAndOnAnEmptySolid) +{ + SurfaceSolid open("safetyBeforeClose"); + addBoxSurfaces(open, 1., 2., 3.); + BOOST_REQUIRE(!open.HasBVH()); // CloseShape not called: no acceleration structure yet + const std::array probe{0.3, -1.1, 2.2}; + BOOST_CHECK_EQUAL(open.Safety(probe.data(), kTRUE), open.Safety_Loop(probe.data(), kTRUE)); + std::array viaBVH{0., 0., 0.}; + std::array viaLoop{0., 0., 0.}; + open.ComputeNormal(probe.data(), nullptr, viaBVH.data()); + open.ComputeNormal_Loop(probe.data(), nullptr, viaLoop.data()); + BOOST_CHECK_EQUAL(viaBVH[0], viaLoop[0]); + BOOST_CHECK_EQUAL(viaBVH[1], viaLoop[1]); + BOOST_CHECK_EQUAL(viaBVH[2], viaLoop[2]); + + SurfaceSolid empty("safetyEmpty"); + empty.CloseShape(); + BOOST_CHECK_EQUAL(empty.Safety(probe.data(), kTRUE), TGeoShape::Big()); + BOOST_CHECK_EQUAL(empty.Safety_Loop(probe.data(), kTRUE), TGeoShape::Big()); + empty.ComputeNormal(probe.data(), nullptr, viaBVH.data()); + empty.ComputeNormal_Loop(probe.data(), nullptr, viaLoop.data()); + BOOST_CHECK_EQUAL(viaBVH[0], viaLoop[0]); +} + +/// ComputeNormal's tie-break, isolated. At the exact centre of a box all six faces are the same +/// distance away, so which one wins is decided entirely by the loop's strict `<` -- the first, i.e. +/// the lowest-indexed, patch. A traversal that visits patches in BVH order would legitimately pick +/// a different face and return a different normal, so the accelerated path carries the index +/// tie-break explicitly and declines to prune a node whose bound merely *equals* the current best. +/// +/// This is a real configuration, not a contrived one: the centre of a box, the axis of a tube and +/// the centre of a sphere all produce exact ties, and a navigator that asks for a normal there gets +/// an answer that must not depend on how the tree happened to be built. +BOOST_AUTO_TEST_CASE(StreamS_ComputeNormalKeepsTheLowestIndexTieBreak) +{ + const auto box = makeBoxSolid("tieBreakBox", 2., 2., 2.); + const std::array centre{0., 0., 0.}; + std::array viaBVH{0., 0., 0.}; + std::array viaLoop{0., 0., 0.}; + box->ComputeNormal(centre.data(), nullptr, viaBVH.data()); + box->ComputeNormal_Loop(centre.data(), nullptr, viaLoop.data()); + // all six faces are exactly 2 away, and the loop's strict `<` keeps the first of them + BOOST_CHECK_EQUAL(box->Safety(centre.data(), kTRUE), box->Safety_Loop(centre.data(), kTRUE)); + BOOST_CHECK_EQUAL(viaBVH[0], viaLoop[0]); + BOOST_CHECK_EQUAL(viaBVH[1], viaLoop[1]); + BOOST_CHECK_EQUAL(viaBVH[2], viaLoop[2]); + // ... and it is genuinely a tie, i.e. the case has something to protect + BOOST_CHECK_EQUAL(std::abs(viaLoop[0]) + std::abs(viaLoop[1]) + std::abs(viaLoop[2]), 1.); + + // the same on the axis of a hollow tube, where the inner wall and both caps compete + const auto tube = makeTubeSolid("tieBreakTube", 1., 2., 1.); + tube->ComputeNormal(centre.data(), nullptr, viaBVH.data()); + tube->ComputeNormal_Loop(centre.data(), nullptr, viaLoop.data()); + BOOST_CHECK_EQUAL(viaBVH[0], viaLoop[0]); + BOOST_CHECK_EQUAL(viaBVH[1], viaLoop[1]); + BOOST_CHECK_EQUAL(viaBVH[2], viaLoop[2]); +} + +/// The negative control. A cross-check that cannot fail has not passed, so the pruning bound is +/// deliberately replaced by one that is not a lower bound -- the distance to the node's bounding +/// box *centre*, which is larger than the distance to the box for any box with extent -- and the +/// comparison against the loop must then break. +/// +/// It must break in a stated direction: an over-large bound prunes subtrees that hold the true +/// nearest patch, so the accelerated Safety comes out **too large**. That is the failure mode that +/// matters (a valid safety may never exceed the true distance to the boundary), and it is the one +/// the sabotage reproduces, so the healthy case is being watched by a test that has been shown to +/// see exactly the thing it is there to see. +BOOST_AUTO_TEST_CASE(StreamS_BreakingThePruningBoundIsCaught) +{ + BOOST_REQUIRE(!SurfaceSolid::GetSafetyBoundUnsoundForTest()); // sound by default + + const auto fixtures = nearestPatchFixtures(); + size_t caughtOnFixtures = 0; + size_t prunableFixtures = 0; + size_t sabotagedDisagreements = 0; + size_t safetyTooLarge = 0; + size_t safetyTooSmall = 0; + for (const auto& fixture : fixtures) { + // With the sub-patch BVH every fixture here has something to prune: a multi-surface solid has + // one leaf per cover box across its surfaces, and even a single full sphere or torus owns a + // whole grid of cover-box leaves. (Before sub-patching, single-patch solids were a single + // unprunable leaf and had to be excluded here; that blind spot is gone by construction.) + ++prunableFixtures; + const auto points = nearestPatchSample(*fixture.solid, fixture.extent, 400); + size_t disagreementsHere = 0; + SurfaceSolid::SetSafetyBoundUnsoundForTest(true); + for (const auto& point : points) { + const double sabotaged = fixture.solid->Safety(point.data(), kTRUE); + SurfaceSolid::SetSafetyBoundUnsoundForTest(false); + const double reference = fixture.solid->Safety_Loop(point.data(), kTRUE); + SurfaceSolid::SetSafetyBoundUnsoundForTest(true); + if (sabotaged != reference) { + ++disagreementsHere; + (sabotaged > reference) ? ++safetyTooLarge : ++safetyTooSmall; + } + disagreementsHere += static_cast(countNearestPatchDisagreements(*fixture.solid, point)); + } + SurfaceSolid::SetSafetyBoundUnsoundForTest(false); + sabotagedDisagreements += disagreementsHere; + if (disagreementsHere > 0) { + ++caughtOnFixtures; + BOOST_TEST_MESSAGE("sabotaged bound caught on " << fixture.solid->GetName() << ": " << disagreementsHere + << " disagreements over " << points.size() << " points"); + } else { + BOOST_TEST_MESSAGE("sabotaged bound NOT caught on " << fixture.solid->GetName() << " (" + << fixture.solid->GetNsurfaces() << " patches)"); + } + } + + // every fixture is sensitive to the sabotage, not just one lucky one + BOOST_CHECK_EQUAL(caughtOnFixtures, prunableFixtures); + BOOST_CHECK_EQUAL(prunableFixtures, fixtures.size()); + BOOST_CHECK_GT(sabotagedDisagreements, 100u); + // and it fails the dangerous way: too much safety, never too little + BOOST_CHECK_GT(safetyTooLarge, 0u); + BOOST_CHECK_EQUAL(safetyTooSmall, 0u); + + // with the sabotage off again the same sample is clean, so the disagreements above are the + // sabotage and not the fixtures + for (const auto& fixture : fixtures) { + for (const auto& point : nearestPatchSample(*fixture.solid, fixture.extent, 400)) { + BOOST_REQUIRE_EQUAL(countNearestPatchDisagreements(*fixture.solid, point), 0); + } + } + BOOST_CHECK(!SurfaceSolid::GetSafetyBoundUnsoundForTest()); +} + +/// What the acceleration actually buys, in the currency the defect was measured in: patches handed +/// to distanceSqToPatch per call. The loop's number is GetNsurfaces() by construction; the +/// traversal's is what this counts. The bounds below are deliberately far looser than the measured +/// values so the case pins the *existence* of pruning rather than becoming a performance trap. +BOOST_AUTO_TEST_CASE(StreamS_SafetyVisitsFarFewerPatchesThanTheLoop) +{ + const auto ring = makeManyPatchSolid("candidateRing", 24); // 24 boxes, 144 patches + BOOST_REQUIRE_EQUAL(ring->GetNsurfaces(), 144); + const auto points = nearestPatchSample(*ring, 5., 500); + + SurfaceSolid::ResetSafetyCandidateCounter(); + for (const auto& point : points) { + ring->Safety(point.data(), kTRUE); + } + const long long acceleratedCandidates = SurfaceSolid::GetSafetyCandidateCount(); + const double perCall = static_cast(acceleratedCandidates) / points.size(); + + BOOST_TEST_MESSAGE("Safety candidates per call: " << perCall << " of " << ring->GetNsurfaces() << " patches"); + BOOST_CHECK_GT(acceleratedCandidates, 0); + BOOST_CHECK_LT(perCall, 0.4 * ring->GetNsurfaces()); + + // the counter is not touched by the loop twin, which visits everything by construction + SurfaceSolid::ResetSafetyCandidateCounter(); + for (const auto& point : points) { + ring->Safety_Loop(point.data(), kTRUE); + std::array normal{0., 0., 0.}; + ring->ComputeNormal_Loop(point.data(), nullptr, normal.data()); + } + BOOST_CHECK_EQUAL(SurfaceSolid::GetSafetyCandidateCount(), 0); + + // ComputeNormal prunes too, only slightly less: it may not drop a node whose bound ties the + // current best, because such a node can hold an equally near patch of lower index + SurfaceSolid::ResetSafetyCandidateCounter(); + for (const auto& point : points) { + std::array normal{0., 0., 0.}; + ring->ComputeNormal(point.data(), nullptr, normal.data()); + } + const double normalPerCall = static_cast(SurfaceSolid::GetSafetyCandidateCount()) / points.size(); + BOOST_TEST_MESSAGE("ComputeNormal candidates per call: " << normalPerCall); + BOOST_CHECK_LT(normalPerCall, 0.4 * ring->GetNsurfaces()); + BOOST_CHECK_GE(normalPerCall, perCall); +} + +/// @name The sub-patch BVH +/// +/// One conservative box per surface makes every swept quadric a giant leaf: a full cylinder's box +/// is the box of its two full rim circles, a sphere's is the whole ball, and every ray through +/// that box pays an analytic patch intersection that mostly reports nothing. The sub-patch BVH +/// lets each surface contribute several tighter boxes (appendCoverBoxes) and dedups the surfaces +/// a query actually tests, so the leaf boxes hug the geometry and the answers stay bit-identical +/// to the loop twins. +/// @{ + +namespace +{ +using CoverBox = surf::BoundedSurface::CoverBox; + +// Squared distance from a point to an axis-aligned box, zero inside; double throughout, so the +// test's bound carries no float rounding of its own. +double coverBoxDistanceSq(const CoverBox& box, const surf::Vec3& point) +{ + double distanceSq = 0.; + const double coordinates[3] = {point.xCoord, point.yCoord, point.zCoord}; + const double lower[3] = {box.first.xCoord, box.first.yCoord, box.first.zCoord}; + const double upper[3] = {box.second.xCoord, box.second.yCoord, box.second.zCoord}; + for (int dimension = 0; dimension < 3; ++dimension) { + const double gap = std::max({lower[dimension] - coordinates[dimension], + coordinates[dimension] - upper[dimension], 0.}); + distanceSq += gap * gap; + } + return distanceSq; +} + +double minCoverBoxDistanceSq(const std::vector& boxes, const surf::Vec3& point) +{ + double best = std::numeric_limits::infinity(); + for (const auto& box : boxes) { + best = std::min(best, coverBoxDistanceSq(box, point)); + } + return best; +} + +bool anyCoverBoxContains(const std::vector& boxes, const surf::Vec3& point, double slack) +{ + for (const auto& box : boxes) { + if (point.xCoord >= box.first.xCoord - slack && point.xCoord <= box.second.xCoord + slack && + point.yCoord >= box.first.yCoord - slack && point.yCoord <= box.second.yCoord + slack && + point.zCoord >= box.first.zCoord - slack && point.zCoord <= box.second.zCoord + slack) { + return true; + } + } + return false; +} + +// The two properties every surface's cover boxes owe the traversal, checked against the surface's +// own kernels: (lower bound) the nearest cover box is never farther than distanceSqToPatch, which +// is what makes pruning on a box distance sound for Safety; (coverage) every point of the trimmed +// patch lies in some box, which is what makes a ray traversal that skips the other boxes complete. +void checkCoverBoxProperties(const surf::BoundedSurface& surface, const std::vector& patchPoints, + const char* label) +{ + std::vector boxes; + surface.appendCoverBoxes(boxes); + BOOST_TEST_CONTEXT("surface = " << label) + { + BOOST_REQUIRE(!boxes.empty()); + for (const auto& point : patchPoints) { + BOOST_TEST_CONTEXT("patch point = (" << point.xCoord << ", " << point.yCoord << ", " << point.zCoord << ")") + { + BOOST_CHECK(anyCoverBoxContains(boxes, point, 1.e-9)); + } + } + SampleStream stream(0xC0FEB0C5ull); + // near the patch, a few radii out, and far away, so the bound is exercised where the box and + // the patch nearly coincide and where the whole surface is a speck + constexpr double kProbeScales[3] = {1.5, 8., 300.}; + for (int index = 0; index < 400; ++index) { + const double scale = kProbeScales[index % 3]; + const surf::Vec3 point{stream.symmetric(scale), stream.symmetric(scale), stream.symmetric(scale)}; + const double patchDistanceSq = surface.distanceSqToPatch(point); + const double boxDistanceSq = minCoverBoxDistanceSq(boxes, point); + BOOST_TEST_CONTEXT("point = (" << point.xCoord << ", " << point.yCoord << ", " << point.zCoord << ")") + { + BOOST_CHECK_LE(boxDistanceSq, patchDistanceSq * (1. + 1.e-9) + 1.e-18); + } + } + } +} + +// A curved family must actually sub-patch: one conservative box would satisfy both properties +// above and tighten nothing, which is the state this whole stream exists to leave behind. +void checkEmitsSeveralCoverBoxes(const surf::BoundedSurface& surface, const char* label) +{ + std::vector boxes; + surface.appendCoverBoxes(boxes); + BOOST_TEST_CONTEXT("surface = " << label) + { + BOOST_CHECK_GT(boxes.size(), 1u); + } +} +} // namespace + +/// The cover boxes of every family, against that family's own kernels. The curved families must +/// emit more than one box -- a single conservative box passes the two properties trivially and +/// tightens nothing -- and the properties must hold on awkward frames, partial sweeps and wire +/// trims, not just on the axis-aligned full-sweep cases. +BOOST_AUTO_TEST_CASE(StreamX_CoverBoxesAreATightLowerBoundEnvelopePerFamily) +{ + using surf::Vec3; + std::string error; + + const Vec3 skewCenter{0.4, -0.2, 0.1}; + const Vec3 skewAxis{0.2, 0.3, 1.}; + const Vec3 referenceU{1., 0., 0.}; + + { + surf::CylindricalBoundedSurface cylinder; + BOOST_REQUIRE(cylinder.initialize(skewCenter, skewAxis, referenceU, 1.7, -0.8, 1.2, 0.4, 1.9, false, error)); + std::vector patchPoints; + for (int stepPhi = 0; stepPhi <= 12; ++stepPhi) { + for (int stepH = 0; stepH <= 4; ++stepH) { + patchPoints.push_back(cylinder.pointAt(0.4 + 1.9 * stepPhi / 12., -0.8 + 2. * stepH / 4.)); + } + } + checkCoverBoxProperties(cylinder, patchPoints, "partial cylinder, skew axis"); + checkEmitsSeveralCoverBoxes(cylinder, "partial cylinder, skew axis"); + } + { + surf::CylindricalBoundedSurface cylinder; + BOOST_REQUIRE(cylinder.initialize({0., 0., 0.}, {0., 0., 1.}, referenceU, 2., -1., 1., 0., surf::kTwoPi, + false, error)); + std::vector patchPoints; + for (int stepPhi = 0; stepPhi <= 24; ++stepPhi) { + patchPoints.push_back(cylinder.pointAt(surf::kTwoPi * stepPhi / 24., -1. + 2. * (stepPhi % 5) / 4.)); + } + checkCoverBoxProperties(cylinder, patchPoints, "full cylinder"); + checkEmitsSeveralCoverBoxes(cylinder, "full cylinder"); + } + { + surf::CylindricalBoundedSurface cylinder; + BOOST_REQUIRE(cylinder.initialize({0., 0., 0.}, {0., 0., 1.}, referenceU, 2., -1., 1., 0., surf::kTwoPi, false, + paramRectWireCurves(0.3, 2.1, -0.5, 0.7), {}, error)); + std::vector patchPoints; + for (int stepPhi = 0; stepPhi <= 10; ++stepPhi) { + for (int stepH = 0; stepH <= 4; ++stepH) { + const double phi = 0.3 + 1.8 * stepPhi / 10.; + const double height = -0.5 + 1.2 * stepH / 4.; + if (cylinder.pointInTrim(phi, height)) { + patchPoints.push_back(cylinder.pointAt(phi, height)); + } + } + } + BOOST_REQUIRE(!patchPoints.empty()); + checkCoverBoxProperties(cylinder, patchPoints, "wire-trimmed cylinder"); + } + { + surf::SphericalBoundedSurface sphere; + BOOST_REQUIRE(sphere.initialize(skewCenter, skewAxis, referenceU, 2.5, 0., surf::kPi, 0., surf::kTwoPi, + false, error)); + std::vector patchPoints; + for (int stepTheta = 0; stepTheta <= 8; ++stepTheta) { + for (int stepPhi = 0; stepPhi < 16; ++stepPhi) { + patchPoints.push_back(sphere.pointAt(surf::kPi * stepTheta / 8., surf::kTwoPi * stepPhi / 16.)); + } + } + checkCoverBoxProperties(sphere, patchPoints, "full sphere, skew frame"); + checkEmitsSeveralCoverBoxes(sphere, "full sphere, skew frame"); + } + { + // a polar cap: distanceSqToPatch realises on the *whole* sphere (radial projection), so the + // cover boxes must still cover the full ball surface, not merely the cap + surf::SphericalBoundedSurface cap; + BOOST_REQUIRE(cap.initialize({0., 0., 0.}, {0., 0., 1.}, referenceU, 2., 0., 0.6, 0.2, 1.1, false, error)); + std::vector patchPoints; + for (int stepTheta = 0; stepTheta <= 4; ++stepTheta) { + for (int stepPhi = 0; stepPhi <= 6; ++stepPhi) { + patchPoints.push_back(cap.pointAt(0.6 * stepTheta / 4., 0.2 + 1.1 * stepPhi / 6.)); + } + } + checkCoverBoxProperties(cap, patchPoints, "spherical cap"); + } + { + surf::ConicalBoundedSurface cone; + BOOST_REQUIRE(cone.initialize(skewCenter, skewAxis, referenceU, 2., 0.5, -0.9, 1.1, 0.7, 2.3, false, error)); + std::vector patchPoints; + for (int stepPhi = 0; stepPhi <= 10; ++stepPhi) { + for (int stepH = 0; stepH <= 4; ++stepH) { + patchPoints.push_back(cone.pointAt(0.7 + 2.3 * stepPhi / 10., -0.9 + 2. * stepH / 4.)); + } + } + checkCoverBoxProperties(cone, patchPoints, "partial cone, skew axis"); + checkEmitsSeveralCoverBoxes(cone, "partial cone, skew axis"); + } + { + surf::TorusBoundedSurface torus; + BOOST_REQUIRE(torus.initialize(skewCenter, skewAxis, referenceU, 2.4, 0.7, 0.3, 2.1, -0.4, 1.7, false, error)); + std::vector patchPoints; + for (int stepRing = 0; stepRing <= 10; ++stepRing) { + for (int stepTube = 0; stepTube <= 6; ++stepTube) { + patchPoints.push_back(torus.pointAt(0.3 + 2.1 * stepRing / 10., -0.4 + 1.7 * stepTube / 6.)); + } + } + checkCoverBoxProperties(torus, patchPoints, "partial torus, skew axis"); + checkEmitsSeveralCoverBoxes(torus, "partial torus, skew axis"); + } + { + surf::TorusBoundedSurface torus; + BOOST_REQUIRE(torus.initialize({0., 0., 0.}, {0., 0., 1.}, referenceU, 3., 1., 0., surf::kTwoPi, 0., + surf::kTwoPi, false, error)); + std::vector patchPoints; + for (int stepRing = 0; stepRing < 16; ++stepRing) { + for (int stepTube = 0; stepTube < 8; ++stepTube) { + patchPoints.push_back(torus.pointAt(surf::kTwoPi * stepRing / 16., surf::kTwoPi * stepTube / 8.)); + } + } + checkCoverBoxProperties(torus, patchPoints, "full torus"); + } + { + surf::PlanarBoundedSurface polygon; + const std::vector rectangle{{0., 0.}, {2., 0.}, {2., 3.}, {0., 3.}}; + BOOST_REQUIRE(polygon.initialize({0.2, -0.4, 0.5}, {1., 0.2, 0.}, {-0.1, 1., 0.3}, rectangle, {}, error)); + std::vector patchPoints; + patchPoints.push_back(polygon.toGlobal({0.01, 0.01})); + patchPoints.push_back(polygon.toGlobal({1.9, 2.9})); + checkCoverBoxProperties(polygon, patchPoints, "planar polygon"); + } +} + +/// Rays that cross a swept quadric's old conservative box while missing the surface itself must +/// reach no patch at all once the leaves are sub-patch boxes. Each case here is a ray the single +/// per-surface box turns into a paid analytic intersection and the sub-patch boxes reject on the +/// box test alone. +BOOST_AUTO_TEST_CASE(StreamX_RaysThroughEmptyBoxRegionsReachNoPatch) +{ + // corner of the ball box, well outside the sphere: rho = |(2.2, 2.2)| = 3.11 > 2.5 + const auto sphere = makeSphereSolid("subBoxSphere", 2.5); + BOOST_CHECK_EQUAL(sphere->CountBVHRayCandidates({2.2, 2.2, -5.}, {0., 0., 1.}), 0); + BOOST_CHECK_GE(sphere->CountBVHRayCandidates({0., 0., -5.}, {0., 0., 1.}), 1); + + // along the axis of a solid tube: the barrel patch cannot be hit, only the two caps can + const auto tube = makeTubeSolid("subBoxTube", 0., 2., 1.); + BOOST_CHECK_EQUAL(tube->CountBVHRayCandidates({0., 0., -5.}, {0., 0., 1.}), 2); + + // corner of the torus box, outside the outer equator: rho = |(3.4, 3.4)| = 4.8 > R + r = 4 + const auto torus = makeTorusSolid("subBoxTorus", 3., 1.); + BOOST_CHECK_EQUAL(torus->CountBVHRayCandidates({3.4, 3.4, -5.}, {0., 0., 1.}), 0); + BOOST_CHECK_GE(torus->CountBVHRayCandidates({3., 0., -5.}, {0., 0., 1.}), 1); + + // behind the back of a quarter cylinder: the full rim circles' box is crossed, the sweep band + // is nowhere near. Not closed (a bare patch), which the BVH does not require. + SurfaceSolid quarter("subBoxQuarterCylinder"); + BOOST_REQUIRE(quarter.AddCylindricalSurface({0., 0., 0.}, {0., 0., 1.}, {1., 0., 0.}, 2., -1., 1., + -surf::kPi / 4., surf::kHalfPi)); + quarter.CloseShape(false); + BOOST_REQUIRE(quarter.HasBVH()); + BOOST_CHECK_EQUAL(quarter.CountBVHRayCandidates({-1.9, -5., 0.}, {0., 1., 0.}), 0); + BOOST_CHECK_GE(quarter.CountBVHRayCandidates({5., 0., 0.}, {-1., 0., 0.}), 1); +} + +/// With several boxes per surface a ray can enter the same surface's leaves more than once, and a +/// duplicated appendIntersections call would flip parity and corrupt the graze clustering. So the +/// dedup is not an optimization but a correctness requirement, and the sharpest way to pin it is +/// the crossing lists themselves: same multiset, both traversals, on the curved fixtures whose +/// surfaces now own many boxes. +BOOST_AUTO_TEST_CASE(StreamX_CurvedFixturesStayIdenticalToTheLoop) +{ + struct Fixture { + std::unique_ptr solid; + double extent; + }; + std::vector fixtures; + fixtures.push_back({makeSphereSolid("subBoxSweepSphere", 2.5), 3.5}); + fixtures.push_back({makeTorusSolid("subBoxSweepTorus", 3., 1.), 4.5}); + fixtures.push_back({makeCapsuleSolid("subBoxSweepCapsule", 1., 1.5), 3.}); + fixtures.push_back({makeConeSolid("subBoxSweepCone", 2., 1., 3.), 4.}); + fixtures.push_back({makeWireTrimmedSolid("subBoxSweepWireTrim"), 3.}); + + for (const auto& fixture : fixtures) { + BOOST_TEST_CONTEXT("fixture = " << fixture.solid->GetName()) + { + sweepDistanceAgainstLoop(*fixture.solid, fixture.extent, 4); + for (const auto& point : probeGrid(fixture.extent, 4)) { + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_EQUAL(fixture.solid->Contains(point.data()), fixture.solid->Contains_Loop(point.data())); + std::vector bvhCrossings; + std::vector loopCrossings; + fixture.solid->DescribeContainsCrossings({point[0], point[1], point[2]}, bvhCrossings, loopCrossings); + BOOST_REQUIRE_EQUAL(bvhCrossings.size(), loopCrossings.size()); + for (size_t index = 0; index < bvhCrossings.size(); ++index) { + BOOST_CHECK_EQUAL(bvhCrossings[index].distance, loopCrossings[index].distance); + } + } + } + } + } +} + +/// @} diff --git a/Detectors/CADSupport/test/testFlatCSG.cxx b/Detectors/CADSupport/test/testFlatCSG.cxx new file mode 100644 index 0000000000000..a49048e38ebbe --- /dev/null +++ b/Detectors/CADSupport/test/testFlatCSG.cxx @@ -0,0 +1,1463 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +#define BOOST_TEST_MODULE Test O2FlatCSG class +#define BOOST_TEST_MAIN +#define BOOST_TEST_DYN_LINK +#include + +#include "CADSupport/O2FlatCSG.h" +#include "CADSupport/O2SurfaceSolidIO.h" + +#include "TFile.h" +#include "TGeoBBox.h" +#include "TGeoShape.h" +#include "TGeoTorus.h" +#include "TGeoTube.h" +#include "TMath.h" + +#include +#include +#include +#include +#include + +namespace +{ +using o2::cad::O2FlatCSG; + +/// A small deterministic generator, so a failing case is reproducible from its seed alone. +class Rng +{ + public: + explicit Rng(unsigned long long seed) : mState(seed) {} + double uniform(double low, double high) + { + mState = mState * 6364136223846793005ULL + 1442695040888963407ULL; + const double unit = static_cast((mState >> 11) & ((1ULL << 53) - 1)) / static_cast(1ULL << 53); + return low + unit * (high - low); + } + + private: + unsigned long long mState; +}; + +/// The quadric of the plane with outward unit normal \a n through \a p: Q(x) = n.(x - p). +void planeQuadric(const double n[3], const double p[3], double coeff[10]) +{ + for (int index = 0; index < 6; ++index) { + coeff[index] = 0.; + } + coeff[6] = 0.5 * n[0]; + coeff[7] = 0.5 * n[1]; + coeff[8] = 0.5 * n[2]; + coeff[9] = -(n[0] * p[0] + n[1] * p[1] + n[2] * p[2]); +} + +/// The quadric of the cylinder of radius \a r about the z axis: Q(x) = x^2 + y^2 - r^2. +void zCylinderQuadric(double r, double coeff[10]) +{ + const double values[10] = {1., 0., 0., 1., 0., 0., 0., 0., 0., -r * r}; + for (int index = 0; index < 10; ++index) { + coeff[index] = values[index]; + } +} + +/// The quadric of the cylinder of radius \a r about the tilted axis d = (1,1,1)/sqrt(3): +/// Q(x) = x^T (I - d d^T) x - r^2. Every plane in this file has A = 0 and every upright cylinder +/// has A diagonal, so this is the only halfspace with a genuinely nonzero off-diagonal A -- it +/// exists to exercise the half[row]*half[column] cross term in HalfspaceRange's quadric branch, +/// which a mis-indexed variant (half[column]*half[column]) can get past every other quadric here. +void tiltedCylinderQuadric(double r, double coeff[10]) +{ + const double s = 1. / std::sqrt(3.); + const double d[3] = {s, s, s}; + double a[3][3]; + for (int row = 0; row < 3; ++row) { + for (int column = 0; column < 3; ++column) { + a[row][column] = (row == column ? 1. : 0.) - d[row] * d[column]; + } + } + coeff[0] = a[0][0]; + coeff[1] = a[0][1]; + coeff[2] = a[0][2]; + coeff[3] = a[1][1]; + coeff[4] = a[1][2]; + coeff[5] = a[2][2]; + coeff[6] = 0.; + coeff[7] = 0.; + coeff[8] = 0.; + coeff[9] = -r * r; +} + +/// A box of half-extents (dx, dy, dz) centred on the origin, as one cell of six planes. +void addBoxCell(O2FlatCSG& solid, double dx, double dy, double dz) +{ + const double half[3] = {dx, dy, dz}; + const int first = solid.GetNhalfspaces(); + for (int axis = 0; axis < 3; ++axis) { + for (int sense = -1; sense <= 1; sense += 2) { + double normal[3] = {0., 0., 0.}; + double through[3] = {0., 0., 0.}; + normal[axis] = static_cast(sense); + through[axis] = sense * half[axis]; + double coeff[10]; + planeQuadric(normal, through, coeff); + solid.AddQuadric(1., coeff); + } + } + solid.AddCell(first, 6, 8. * dx * dy * dz); +} +} // namespace + +BOOST_AUTO_TEST_CASE(box_from_six_planes_contains_like_TGeoBBox) +{ + O2FlatCSG solid("box"); + addBoxCell(solid, 3., 4., 5.); + BOOST_CHECK_EQUAL(solid.GetNcells(), 1); + BOOST_CHECK_EQUAL(solid.GetNhalfspaces(), 6); + + TGeoBBox reference(3., 4., 5.); + Rng rng(20260824ULL); + int scored = 0; + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-7., 7.), rng.uniform(-8., 8.)}; + // skip the boundary shell, where the two shapes are allowed to disagree by tolerance + if (std::abs(std::abs(point[0]) - 3.) < 1.e-9 || std::abs(std::abs(point[1]) - 4.) < 1.e-9 || + std::abs(std::abs(point[2]) - 5.) < 1.e-9) { + continue; + } + BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point)); + BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point)); + ++scored; + } + BOOST_CHECK_GT(scored, 19000); +} + +BOOST_AUTO_TEST_CASE(tube_from_two_cylinders_and_two_planes_contains_like_TGeoTube) +{ + // rmin = 2, rmax = 5, dz = 7: the inner cylinder is a COMPLEMENTED halfspace, which is what + // makes this cell non-convex and is the case the whole class exists for. + O2FlatCSG solid("tube"); + double coeff[10]; + zCylinderQuadric(5., coeff); + solid.AddQuadric(1., coeff); + zCylinderQuadric(2., coeff); + solid.AddQuadric(-1., coeff); + const double up[3] = {0., 0., 1.}; + const double down[3] = {0., 0., -1.}; + const double top[3] = {0., 0., 7.}; + const double bottom[3] = {0., 0., -7.}; + planeQuadric(up, top, coeff); + solid.AddQuadric(1., coeff); + planeQuadric(down, bottom, coeff); + solid.AddQuadric(1., coeff); + solid.AddCell(0, 4, TMath::Pi() * (25. - 4.) * 14.); + + TGeoTube reference(2., 5., 7.); + Rng rng(777ULL); + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)}; + const double radius = std::hypot(point[0], point[1]); + if (std::abs(radius - 2.) < 1.e-9 || std::abs(radius - 5.) < 1.e-9 || + std::abs(std::abs(point[2]) - 7.) < 1.e-9) { + continue; + } + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point)); + } + } +} + +BOOST_AUTO_TEST_CASE(two_disjoint_cells_are_a_union) +{ + O2FlatCSG solid("two_boxes"); + addBoxCell(solid, 1., 1., 1.); + // a second box, centred at x = +10, as six planes of its own + const int first = solid.GetNhalfspaces(); + const double centre = 10.; + for (int axis = 0; axis < 3; ++axis) { + for (int sense = -1; sense <= 1; sense += 2) { + double normal[3] = {0., 0., 0.}; + double through[3] = {centre, 0., 0.}; + normal[axis] = static_cast(sense); + through[axis] += (axis == 0 ? sense * 1. : 0.); + if (axis != 0) { + through[axis] = sense * 1.; + } + double coeff[10]; + planeQuadric(normal, through, coeff); + solid.AddQuadric(1., coeff); + } + } + solid.AddCell(first, 6, 8.); + + const double inFirst[3] = {0., 0., 0.}; + const double inSecond[3] = {10., 0., 0.}; + const double between[3] = {5., 0., 0.}; + BOOST_CHECK(solid.Contains_Loop(inFirst)); + BOOST_CHECK(solid.Contains_Loop(inSecond)); + BOOST_CHECK(!solid.Contains_Loop(between)); +} + +BOOST_AUTO_TEST_CASE(box_distances_match_TGeoBBox) +{ + O2FlatCSG solid("box_dist"); + addBoxCell(solid, 3., 4., 5.); + TGeoBBox reference(3., 4., 5.); + + Rng rng(4242ULL); + for (int trial = 0; trial < 20000; ++trial) { + double point[3] = {rng.uniform(-12., 12.), rng.uniform(-12., 12.), rng.uniform(-12., 12.)}; + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + const bool inside = reference.Contains(point); + if (inside != static_cast(solid.Contains_Loop(point))) { + continue; // a boundary point; classification is tested separately + } + const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big()) + : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()); + const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr) + : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr); + if (theirs >= TGeoShape::Big()) { + BOOST_REQUIRE_GE(mine, TGeoShape::Big()); + } else { + BOOST_REQUIRE_SMALL(mine - theirs, 1.e-9); + } + } +} + +BOOST_AUTO_TEST_CASE(tube_distances_match_TGeoTube_through_the_bore) +{ + // the complemented inner cylinder makes the occupancy along a ray TWO intervals for a ray that + // crosses the bore, which is the case a convexity assumption would get wrong + O2FlatCSG solid("tube_dist"); + double coeff[10]; + zCylinderQuadric(5., coeff); + solid.AddQuadric(1., coeff); + zCylinderQuadric(2., coeff); + solid.AddQuadric(-1., coeff); + const double up[3] = {0., 0., 1.}; + const double down[3] = {0., 0., -1.}; + const double top[3] = {0., 0., 7.}; + const double bottom[3] = {0., 0., -7.}; + planeQuadric(up, top, coeff); + solid.AddQuadric(1., coeff); + planeQuadric(down, bottom, coeff); + solid.AddQuadric(1., coeff); + solid.AddCell(0, 4, 0.); + + TGeoTube reference(2., 5., 7.); + // a ray straight along +x at z = 0 enters the wall at x = -5, leaves it at x = -2, re-enters at + // x = +2 and leaves at x = +5 + const double origin[3] = {-9., 0., 0.}; + const double dir[3] = {1., 0., 0.}; + BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(origin, dir, TGeoShape::Big()) - 4., 1.e-12); + + const double inWall[3] = {-4., 0., 0.}; + BOOST_CHECK_SMALL(solid.DistFromInside_Loop(inWall, dir, TGeoShape::Big()) - 2., 1.e-12); + + const double inBore[3] = {0., 0., 0.}; + BOOST_CHECK(!solid.Contains_Loop(inBore)); + BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(inBore, dir, TGeoShape::Big()) - 2., 1.e-12); + + Rng rng(99ULL); + for (int trial = 0; trial < 20000; ++trial) { + double point[3] = {rng.uniform(-9., 9.), rng.uniform(-9., 9.), rng.uniform(-10., 10.)}; + double direction[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + direction[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + direction[index] /= norm; + } + const bool inside = reference.Contains(point); + if (inside != static_cast(solid.Contains_Loop(point))) { + continue; + } + const double mine = inside ? solid.DistFromInside_Loop(point, direction, TGeoShape::Big()) + : solid.DistFromOutside_Loop(point, direction, TGeoShape::Big()); + const double theirs = inside ? reference.DistFromInside(point, direction, 3, TGeoShape::Big(), nullptr) + : reference.DistFromOutside(point, direction, 3, TGeoShape::Big(), nullptr); + if (theirs >= TGeoShape::Big()) { + BOOST_REQUIRE_GE(mine, TGeoShape::Big()); + } else { + BOOST_REQUIRE_SMALL(mine - theirs, 1.e-8); + } + } +} + +BOOST_AUTO_TEST_CASE(a_ray_leaving_one_cell_into_a_touching_one_does_not_stop_between_them) +{ + // two unit boxes sharing the face at x = 1: the union's DistFromInside from the origin along +x + // is 3, not 1. This is why DistFromInside needs the union across cells and not one cell's exit. + O2FlatCSG solid("touching"); + addBoxCell(solid, 1., 1., 1.); + const int first = solid.GetNhalfspaces(); + const double planes[6][2][3] = {{{1., 0., 0.}, {3., 0., 0.}}, + {{-1., 0., 0.}, {1., 0., 0.}}, + {{0., 1., 0.}, {0., 1., 0.}}, + {{0., -1., 0.}, {0., -1., 0.}}, + {{0., 0., 1.}, {0., 0., 1.}}, + {{0., 0., -1.}, {0., 0., -1.}}}; + for (const auto& plane : planes) { + double coeff[10]; + planeQuadric(plane[0], plane[1], coeff); + solid.AddQuadric(1., coeff); + } + solid.AddCell(first, 6, 8.); + + const double origin[3] = {0., 0., 0.}; + const double dir[3] = {1., 0., 0.}; + BOOST_CHECK_SMALL(solid.DistFromInside_Loop(origin, dir, TGeoShape::Big()) - 3., 1.e-12); +} + +BOOST_AUTO_TEST_CASE(tangential_ray_on_a_cylinder_from_a_point_on_its_surface_has_no_nan_root) +{ + // a ray tangential to a cylinder, starting exactly on its surface, has beta == 0 and gamma == 0 + // together in HalfspaceRoots' quadratic -- the q == 0 case that used to divide 0./0. into a + // NaN second root instead of recognising the single double root at t = 0 + O2FlatCSG solid("tangent_ray"); + double coeff[10]; + zCylinderQuadric(5., coeff); + solid.AddQuadric(1., coeff); + const auto& cylinder = solid.GetHalfspace(0); + + const double origin[3] = {5., 0., 0.}; + const double dir[3] = {0., 1., 0.}; + double roots[4]; + const int found = O2FlatCSG::HalfspaceRoots(cylinder, origin, dir, roots); + + BOOST_REQUIRE_EQUAL(found, 1); + BOOST_CHECK(std::isfinite(roots[0])); + BOOST_CHECK_SMALL(roots[0], 1.e-12); + + // the twin: an independent check that the reported root really is one, by plugging it back + // into the surface equation directly rather than trusting the root-finder's own algebra + const double hit[3] = {origin[0] + roots[0] * dir[0], origin[1] + roots[0] * dir[1], + origin[2] + roots[0] * dir[2]}; + BOOST_CHECK_SMALL(O2FlatCSG::EvalHalfspace(cylinder, hit), 1.e-9); +} + +BOOST_AUTO_TEST_CASE(torus_contains_and_distances_match_TGeoTorus) +{ + // a full torus, R = 10, r = 3, about z -- one cell of one halfspace + O2FlatCSG solid("torus"); + const double centre[3] = {0., 0., 0.}; + const double axis[3] = {0., 0., 1.}; + solid.AddTorus(1., centre, axis, 10., 3.); + solid.AddCell(0, 1, 2. * TMath::Pi() * TMath::Pi() * 10. * 9.); + + TGeoTorus reference(10., 0., 3.); + Rng rng(31415ULL); + int scoredPoints = 0; + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-15., 15.), rng.uniform(-15., 15.), rng.uniform(-5., 5.)}; + const double radial = std::hypot(point[0], point[1]); + const double distance = std::hypot(radial - 10., point[2]) - 3.; + if (std::abs(distance) < 1.e-9) { + continue; + } + BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point)); + ++scoredPoints; + } + BOOST_CHECK_GT(scoredPoints, 19000); + + for (int trial = 0; trial < 20000; ++trial) { + double point[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)}; + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + const bool inside = reference.Contains(point); + if (inside != static_cast(solid.Contains_Loop(point))) { + continue; + } + const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big()) + : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()); + const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr) + : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr); + if (theirs >= TGeoShape::Big()) { + BOOST_REQUIRE_GE(mine, TGeoShape::Big()); + } else { + // the quartic is the looser of the two solvers; 1e-6 cm on a 10 cm torus + BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6); + } + } +} + +BOOST_AUTO_TEST_CASE(a_tilted_torus_is_the_same_solid_as_an_upright_one_rotated) +{ + // the frame handling is where a torus block goes wrong silently, so it gets its own case + const double axis[3] = {0., 1. / std::sqrt(2.), 1. / std::sqrt(2.)}; + const double centre[3] = {1., 2., 3.}; + O2FlatCSG solid("tilted_torus"); + solid.AddTorus(1., centre, axis, 8., 2.); + solid.AddCell(0, 1, 0.); + + Rng rng(2718ULL); + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-14., 16.), rng.uniform(-13., 17.), rng.uniform(-12., 18.)}; + // the closed-form signed distance is the reference: sqrt((rho - R)^2 + z^2) - r + const double offset[3] = {point[0] - centre[0], point[1] - centre[1], point[2] - centre[2]}; + const double along = offset[0] * axis[0] + offset[1] * axis[1] + offset[2] * axis[2]; + double radialVec[3]; + for (int index = 0; index < 3; ++index) { + radialVec[index] = offset[index] - along * axis[index]; + } + const double rho = std::sqrt(radialVec[0] * radialVec[0] + radialVec[1] * radialVec[1] + + radialVec[2] * radialVec[2]); + const double signedDistance = std::hypot(rho - 8., along) - 2.; + if (std::abs(signedDistance) < 1.e-9) { + continue; + } + BOOST_REQUIRE_EQUAL(static_cast(solid.Contains_Loop(point)), signedDistance < 0.); + } + + // Contains_Loop only exercises EvalHalfspace's frame decomposition; HalfspaceRoots has its own, + // separate one (the pz/dz/pPerp/dPerp block), and the upright case never gives it a non-z axis + // to get wrong. Compare distances against TGeoTorus by carrying a local (upright, origin- + // centred) point and direction alongside a world one related by the same rotation that carries + // the local z axis onto `axis`, so the reference and the shape describe the same solid. + const double s = 1. / std::sqrt(2.); + // rotation about the world x axis that sends local (0,0,1) to (0, s, s) == axis + auto rotateToWorld = [s](const double local[3], double world[3]) { + world[0] = local[0]; + world[1] = s * local[1] + s * local[2]; + world[2] = -s * local[1] + s * local[2]; + }; + + TGeoTorus reference(8., 0., 2.); + for (int trial = 0; trial < 20000; ++trial) { + double localPoint[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)}; + double localDir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + localDir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(localDir[0] * localDir[0] + localDir[1] * localDir[1] + localDir[2] * localDir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + localDir[index] /= norm; + } + double worldPoint[3]; + double worldDir[3]; + rotateToWorld(localPoint, worldPoint); + rotateToWorld(localDir, worldDir); + for (int index = 0; index < 3; ++index) { + worldPoint[index] += centre[index]; + } + + const bool inside = reference.Contains(localPoint); + if (inside != static_cast(solid.Contains_Loop(worldPoint))) { + continue; + } + const double mine = inside ? solid.DistFromInside_Loop(worldPoint, worldDir, TGeoShape::Big()) + : solid.DistFromOutside_Loop(worldPoint, worldDir, TGeoShape::Big()); + const double theirs = inside ? reference.DistFromInside(localPoint, localDir, 3, TGeoShape::Big(), nullptr) + : reference.DistFromOutside(localPoint, localDir, 3, TGeoShape::Big(), nullptr); + if (theirs >= TGeoShape::Big()) { + BOOST_REQUIRE_GE(mine, TGeoShape::Big()); + } else { + BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6); + } + } +} + +BOOST_AUTO_TEST_CASE(the_range_bound_encloses_the_sampled_range) +{ + // the bound must be an ENCLOSURE: over-wide is safe, under-wide is a wrong solid + O2FlatCSG solid("range"); + double coeff[10]; + zCylinderQuadric(5., coeff); + const int cylinder = solid.AddQuadric(1., coeff); + const double normal[3] = {0., 0., 1.}; + const double through[3] = {0., 0., 2.}; + planeQuadric(normal, through, coeff); + const int plane = solid.AddQuadric(-1., coeff); + const double centre[3] = {1., 0., 0.}; + const double axis[3] = {0., 0., 1.}; + const int torus = solid.AddTorus(1., centre, axis, 7., 2.); + tiltedCylinderQuadric(5., coeff); + const int tilted = solid.AddQuadric(1., coeff); + + Rng rng(555ULL); + const std::vector halfspaces = {cylinder, plane, torus, tilted}; + + auto checkBox = [&](const double* lo, const double* hi) { + for (int which : halfspaces) { + double rangeLo = 0.; + double rangeHi = 0.; + O2FlatCSG::HalfspaceRange(solid.GetHalfspace(which), lo, hi, rangeLo, rangeHi); + BOOST_REQUIRE_LE(rangeLo, rangeHi); + + auto checkPoint = [&](const double point[3]) { + const double value = O2FlatCSG::EvalHalfspace(solid.GetHalfspace(which), point); + BOOST_REQUIRE_GE(value, rangeLo - 1.e-9); + BOOST_REQUIRE_LE(value, rangeHi + 1.e-9); + }; + + // deterministic coverage of the box's extremities: a plane's bound is tight exactly AT a + // corner, so uniform interior sampling has probability zero of ever landing where a + // slightly under-wide bound would actually be caught + for (int cx : {0, 1}) { + for (int cy : {0, 1}) { + for (int cz : {0, 1}) { + const double corner[3] = {cx ? hi[0] : lo[0], cy ? hi[1] : lo[1], cz ? hi[2] : lo[2]}; + checkPoint(corner); + } + } + } + const double mid[3] = {0.5 * (lo[0] + hi[0]), 0.5 * (lo[1] + hi[1]), 0.5 * (lo[2] + hi[2])}; + for (int faceAxis = 0; faceAxis < 3; ++faceAxis) { + for (int side : {0, 1}) { + double face[3] = {mid[0], mid[1], mid[2]}; + face[faceAxis] = side ? hi[faceAxis] : lo[faceAxis]; + checkPoint(face); + } + } + for (int edgeAxis = 0; edgeAxis < 3; ++edgeAxis) { + const int other1 = (edgeAxis + 1) % 3; + const int other2 = (edgeAxis + 2) % 3; + for (int s1 : {0, 1}) { + for (int s2 : {0, 1}) { + double edge[3]; + edge[edgeAxis] = mid[edgeAxis]; + edge[other1] = s1 ? hi[other1] : lo[other1]; + edge[other2] = s2 ? hi[other2] : lo[other2]; + checkPoint(edge); + } + } + } + + // plus random interior samples, as before + for (int sample = 0; sample < 200; ++sample) { + const double point[3] = {rng.uniform(lo[0], hi[0]), rng.uniform(lo[1], hi[1]), + rng.uniform(lo[2], hi[2])}; + checkPoint(point); + } + } + }; + + for (int trial = 0; trial < 3000; ++trial) { + double lo[3]; + double hi[3]; + for (int index = 0; index < 3; ++index) { + const double a = rng.uniform(-12., 12.); + const double b = a + rng.uniform(0.01, 6.); + lo[index] = a; + hi[index] = b; + } + checkBox(lo, hi); + } + + // extreme-aspect-ratio boxes -- one axis ~0.01 wide, another ~24 -- outside the size range the + // random trials above ever draw (at most 6 wide per axis) + const double extreme[4][3][2] = { + {{-0.005, 0.005}, {-12., 12.}, {-0.5, 0.5}}, + {{-12., 12.}, {-0.005, 0.005}, {3., 27.}}, + {{2., 2.01}, {-1., 1.}, {-12., 12.}}, + {{-24., 0.}, {5., 5.01}, {-3., 3.}}, + }; + for (const auto& box : extreme) { + const double lo[3] = {box[0][0], box[1][0], box[2][0]}; + const double hi[3] = {box[0][1], box[1][1], box[2][1]}; + checkBox(lo, hi); + } +} + +BOOST_AUTO_TEST_CASE(the_boxes_cover_the_solid_and_their_active_lists_are_sound) +{ + // rmin = 2, rmax = 5, dz = 7 again, so there is a bore for the boxes to carve around + O2FlatCSG solid("boxes"); + double coeff[10]; + zCylinderQuadric(5., coeff); + solid.AddQuadric(1., coeff); + zCylinderQuadric(2., coeff); + solid.AddQuadric(-1., coeff); + const double up[3] = {0., 0., 1.}; + const double down[3] = {0., 0., -1.}; + const double top[3] = {0., 0., 7.}; + const double bottom[3] = {0., 0., -7.}; + planeQuadric(up, top, coeff); + solid.AddQuadric(1., coeff); + planeQuadric(down, bottom, coeff); + solid.AddQuadric(1., coeff); + solid.AddCell(0, 4, 0.); + const double lo[3] = {-5., -5., -7.}; + const double hi[3] = {5., 5., 7.}; + solid.SetCellBBox(0, lo, hi); + solid.CloseShape(); + + BOOST_CHECK_GT(solid.GetNboxes(), 1); + + Rng rng(8080ULL); + int insideSamples = 0; + for (int trial = 0; trial < 50000; ++trial) { + const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)}; + if (!solid.Contains_Loop(point)) { + continue; + } + ++insideSamples; + // COVERAGE: every point of the solid is in some box + bool covered = false; + for (int index = 0; index < solid.GetNboxes() && !covered; ++index) { + const auto& box = solid.GetBox(index); + covered = point[0] >= box.min[0] && point[0] <= box.max[0] && point[1] >= box.min[1] && + point[1] <= box.max[1] && point[2] >= box.min[2] && point[2] <= box.max[2]; + } + BOOST_REQUIRE(covered); + } + BOOST_CHECK_GT(insideSamples, 5000); + + // SOUNDNESS of the active lists: in every box, the active list alone decides membership + for (int index = 0; index < solid.GetNboxes(); ++index) { + const auto& box = solid.GetBox(index); + for (int sample = 0; sample < 200; ++sample) { + const double point[3] = {rng.uniform(box.min[0], box.max[0]), + rng.uniform(box.min[1], box.max[1]), + rng.uniform(box.min[2], box.max[2])}; + bool byActive = true; + for (int slot = 0; slot < box.nActive && byActive; ++slot) { + byActive = O2FlatCSG::EvalHalfspace( + solid.GetHalfspace(solid.GetActive(box.firstActive + slot)), point) <= 0.; + } + BOOST_REQUIRE_EQUAL(byActive, solid.CellContains(box.cell, point)); + } + } +} + +BOOST_AUTO_TEST_CASE(a_box_wholly_inside_a_cell_carries_no_active_halfspaces) +{ + O2FlatCSG solid("solid_boxes"); + addBoxCell(solid, 4., 4., 4.); + const double lo[3] = {-4., -4., -4.}; + const double hi[3] = {4., 4., 4.}; + solid.SetCellBBox(0, lo, hi); + // Both knobs are pinned, not defaulted: this case is about what the subdivision CAN produce, + // and the shipped defaults are chosen for query cost, which is + // a different question. Six levels on a cube is a 4 x 4 x 4 grid, whose innermost eight boxes + // touch no face. + solid.SetSplitDepth(6); + solid.SetMinBoxFraction(0.01); + solid.CloseShape(); + // a box has six planes and is convex, so subdivision must find interior boxes with an empty list + int solidBoxes = 0; + for (int index = 0; index < solid.GetNboxes(); ++index) { + if (solid.GetBox(index).nActive == 0) { + ++solidBoxes; + } + } + BOOST_CHECK_GT(solidBoxes, 0); +} + +BOOST_AUTO_TEST_CASE(a_cell_without_a_bbox_fails_loudly_instead_of_vanishing) +{ + // cell 0 gets a box; cell 1 (a second, disjoint box) never does -- CloseShape must refuse to + // build a partial, silently-wrong solid rather than just drop cell 1 + O2FlatCSG solid("missing_bbox"); + addBoxCell(solid, 1., 1., 1.); + const int first = solid.GetNhalfspaces(); + const double centre[3] = {10., 0., 0.}; + for (int axis = 0; axis < 3; ++axis) { + for (int sense = -1; sense <= 1; sense += 2) { + double normal[3] = {0., 0., 0.}; + double through[3] = {centre[0], centre[1], centre[2]}; + normal[axis] = static_cast(sense); + through[axis] += sense * 1.; + double coeff[10]; + planeQuadric(normal, through, coeff); + solid.AddQuadric(1., coeff); + } + } + solid.AddCell(first, 6, 8.); + + const double lo[3] = {-1., -1., -1.}; + const double hi[3] = {1., 1., 1.}; + solid.SetCellBBox(0, lo, hi); // cell 1's box is never set + + solid.CloseShape(); + BOOST_CHECK(!solid.IsClosed()); + BOOST_CHECK_EQUAL(solid.GetNboxes(), 0); +} + +BOOST_AUTO_TEST_CASE(an_inverted_cell_bbox_fails_loudly_instead_of_being_kept_as_solid) +{ + // a converter that swapped lo/hi arguments must not get a shape that quietly reports itself + // closed: an all-axes-inverted box never grows past SplitBox's longest = 0. initialiser, so it + // would otherwise be kept immediately with an active list computed from a negative-half-extent + // (hence invalid) range bound -- possibly nActive == 0, which downstream reads as solid material + O2FlatCSG solid("inverted_bbox"); + addBoxCell(solid, 1., 1., 1.); + const double lo[3] = {-1., -1., -1.}; + const double hi[3] = {1., 1., 1.}; + solid.SetCellBBox(0, hi, lo); // lo/hi swapped + + solid.CloseShape(); + BOOST_CHECK(!solid.IsClosed()); + BOOST_CHECK_EQUAL(solid.GetNboxes(), 0); +} + +BOOST_AUTO_TEST_CASE(a_nan_cell_bbox_fails_loudly_instead_of_defeating_the_inverted_box_check) +{ + // a NaN passes every ordinary "hi < lo" comparison silently (every comparison with NaN is + // false), so it must be its own check rather than fall through the inverted-box test above -- + // otherwise it would reach HalfspaceRange, produce a NaN range that fails both of SplitBox's + // drop tests, and get kept as a spurious box + O2FlatCSG solid("nan_bbox"); + addBoxCell(solid, 1., 1., 1.); + const double nan = std::numeric_limits::quiet_NaN(); + const double lo[3] = {-1., -1., -1.}; + const double hi[3] = {1., nan, 1.}; + solid.SetCellBBox(0, lo, hi); + + solid.CloseShape(); + BOOST_CHECK(!solid.IsClosed()); + BOOST_CHECK_EQUAL(solid.GetNboxes(), 0); +} + +namespace +{ +/// An L-shaped bracket with a bore: three cells, a complemented cylinder, a long diagonal extent. +/// Deliberately the shape a cell-level BVH would handle badly. +/// +/// The washer sits ABOVE the arm, at z in [1, 3], so its bore is a genuine hole in the union: the +/// arm spans |y| <= 1 and |z| <= 1, so a washer at |z| <= 1 would have had its own bore filled in +/// by the arm and the solid would have had no cavity anywhere. +/// +/// \a planeScale multiplies every PLANE quadric. `sign * Q <= 0` is the same halfspace for any +/// positive scale, so the solid is unchanged -- but the accelerated queries are only bit-identical +/// to their twins when the scale is a power of two; see the rescaled test below and +/// Detectors/CADSupport/doc/reference/Design_FlatCSGSolid.md section 3.1. +void buildBracket(O2FlatCSG& solid, double planeScale = 1.) +{ + double coeff[10]; + const auto scaledPlane = [&](const double* normal, const double* through) { + planeQuadric(normal, through, coeff); + for (int index = 0; index < 10; ++index) { + coeff[index] *= planeScale; + } + }; + // cell 0: the long arm, x in [-10, 10], y in [-1, 1], z in [-1, 1] + const double arm[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}}, + {{-1., 0., 0.}, {-10., 0., 0.}}, + {{0., 1., 0.}, {0., 1., 0.}}, + {{0., -1., 0.}, {0., -1., 0.}}, + {{0., 0., 1.}, {0., 0., 1.}}, + {{0., 0., -1.}, {0., 0., -1.}}}; + int first = solid.GetNhalfspaces(); + for (const auto& plane : arm) { + scaledPlane(plane[0], plane[1]); + solid.AddQuadric(1., coeff); + } + solid.AddCell(first, 6, 8. * 10. * 1. * 1.); + const double armLo[3] = {-10., -1., -1.}; + const double armHi[3] = {10., 1., 1.}; + solid.SetCellBBox(0, armLo, armHi); + + // cell 1: the upright, x in [8, 10], y in [-1, 1], z in [1, 12] + const double upright[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}}, + {{-1., 0., 0.}, {8., 0., 0.}}, + {{0., 1., 0.}, {0., 1., 0.}}, + {{0., -1., 0.}, {0., -1., 0.}}, + {{0., 0., 1.}, {0., 0., 12.}}, + {{0., 0., -1.}, {0., 0., 1.}}}; + first = solid.GetNhalfspaces(); + for (const auto& plane : upright) { + scaledPlane(plane[0], plane[1]); + solid.AddQuadric(1., coeff); + } + solid.AddCell(first, 6, 2. * 2. * 11.); + const double uprightLo[3] = {8., -1., 1.}; + const double uprightHi[3] = {10., 1., 12.}; + solid.SetCellBBox(1, uprightLo, uprightHi); + + // cell 2: a washer around z at x = -8 and z in [1, 3], with a bore -- a complemented cylinder, + // so non-convex, and clear of the arm so the bore is empty space + first = solid.GetNhalfspaces(); + const double centreShift = -8.; + // outer cylinder about the axis through (-8, 0, *): translate by completing the square + const double outer[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0., + centreShift * centreShift - 9.}; + solid.AddQuadric(1., outer); + const double inner[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0., + centreShift * centreShift - 1.}; + solid.AddQuadric(-1., inner); + const double washer[2][2][3] = {{{0., 0., 1.}, {0., 0., 3.}}, {{0., 0., -1.}, {0., 0., 1.}}}; + for (const auto& plane : washer) { + scaledPlane(plane[0], plane[1]); + solid.AddQuadric(1., coeff); + } + solid.AddCell(first, 4, TMath::Pi() * (9. - 1.) * 2.); + const double washerLo[3] = {-11., -3., 1.}; + const double washerHi[3] = {-5., 3., 3.}; + solid.SetCellBBox(2, washerLo, washerHi); +} +} // namespace + +BOOST_AUTO_TEST_CASE(the_accelerated_contains_is_bit_identical_to_its_twin) +{ + O2FlatCSG solid("bracket"); + buildBracket(solid); + solid.CloseShape(); + BOOST_CHECK_GT(solid.GetNboxes(), 3); + BOOST_CHECK_GT(solid.GetBVHMemory(), 0u); + + Rng rng(123456ULL); + for (int trial = 0; trial < 200000; ++trial) { + const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)}; + BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point)); + } +} + +BOOST_AUTO_TEST_CASE(the_sampled_boundary_points_flip_containment) +{ + O2FlatCSG solid("bracket_points"); + buildBracket(solid); + solid.CloseShape(); + constexpr int kPoints = 4000; + std::vector points(3 * kPoints, 0.); + BOOST_REQUIRE(solid.GetPointsOnSegments(kPoints, points.data())); + const double zAxis[3] = {0., 0., 1.}; + for (int index = 0; index < kPoints; ++index) { + const double* point = &points[3 * index]; + double normal[3] = {0., 0., 0.}; + solid.ComputeNormal(point, zAxis, normal); + double below[3]; + double above[3]; + for (int axis = 0; axis < 3; ++axis) { + below[axis] = point[axis] - 1.e-6 * normal[axis]; + above[axis] = point[axis] + 1.e-6 * normal[axis]; + } + BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")") + { + BOOST_CHECK_NE(solid.Contains(below), solid.Contains(above)); + } + } +} + +BOOST_AUTO_TEST_CASE(the_accelerated_distances_are_bit_identical_to_their_twins) +{ + O2FlatCSG solid("bracket_dist"); + buildBracket(solid); + solid.CloseShape(); + + Rng rng(654321ULL); + for (int trial = 0; trial < 200000; ++trial) { + double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)}; + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + if (solid.Contains_Loop(point)) { + BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr), + solid.DistFromInside_Loop(point, dir, TGeoShape::Big())); + } else { + BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr), + solid.DistFromOutside_Loop(point, dir, TGeoShape::Big())); + } + } +} + +BOOST_AUTO_TEST_CASE(a_ray_along_the_long_arm_crosses_every_cell_it_should) +{ + // the case a per-box clip gets wrong if it forgets to clip: a ray running the length of the + // bracket passes through many boxes of the same cell, and must see ONE interval, not many + O2FlatCSG solid("bracket_long"); + buildBracket(solid); + solid.CloseShape(); + const double dir[3] = {1., 0., 0.}; + + // the entry, which one box decides on its own: nothing lies before the arm along z = 0 + const double origin[3] = {-20., 0., 0.}; + BOOST_CHECK_SMALL(solid.DistFromOutside(origin, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12); + + // the exit, which sixteen boxes of cell 0 decide together: the arm is split along x into boxes + // 1.25 wide, so this is the cross-box join, and a traversal that forgot it would stop at the + // first box boundary + const double inArm[3] = {0., 0., 0.}; + // inside the arm at the origin, the exit is x = 10 (the arm and the upright touch at x = 8..10 + // only for z > 1, so along z = 0 the arm alone decides) + BOOST_CHECK_SMALL(solid.DistFromInside(inArm, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12); + + // An ENTRY that needs the per-cell merge, which is otherwise hard to reach: the twin's rule + // takes the smallest entry over the intervals whose exit clears TGeoShape::Tolerance(), so a + // box boundary crossed within the tolerance of the origin cuts the real interval into a + // sub-tolerance stub the rule would throw away, and the answer would jump from the true entry + // to the box boundary. This ray starts 1e-11 outside the arm's y = 1 face and 2e-11 before its + // x = -8.75 box boundary, so both crossings sit inside the tolerance. + const double grazing[3] = {-8.75 - 2.e-11, 1. + 1.e-11, 0.5}; + const double slant = 1. / std::sqrt(2.); + const double slantDir[3] = {slant, -slant, 0.}; + const double entered = solid.DistFromOutside(grazing, slantDir, 3, TGeoShape::Big(), nullptr); + BOOST_CHECK_EQUAL(entered, solid.DistFromOutside_Loop(grazing, slantDir, TGeoShape::Big())); + // and it really is in the regime where a per-box rule would differ: below the tolerance, and + // strictly nearer than the x = -8.75 box boundary at 2e-11 * sqrt(2) + BOOST_CHECK_GT(entered, 0.); + BOOST_CHECK_LT(entered, TGeoShape::Tolerance()); + BOOST_CHECK_LT(entered, 2.e-11 * std::sqrt(2.)); +} + +BOOST_AUTO_TEST_CASE(the_accelerated_distances_track_their_twins_when_a_plane_is_rescaled) +{ + // Bit identity between an accelerated query and its twin is a self-check discipline, not a + // physics requirement: a one-ulp difference in an exit distance is navigationally irrelevant. + // It is achievable only under the plane convention of design section 3.1, where a unit normal n + // is stored as 2b = n. There the slab bound (v - o_k) / d_k and the root -0.5*gamma/beta divide + // numerator and denominator each scaled by exactly one half, so the single IEEE division returns + // the same double for both, and a box face lying on a plane halfspace is crossed at one value. + // + // Rescaling every plane by a NON-POWER-OF-TWO -- 3 here, which is what an unnormalised carrier + // normal (3, 0, 0) would give -- describes exactly the same solid, but fl(1.5 * d_x) rounds, the + // root moves off the slab bound by an ulp, and the last bit is lost. Nothing else in the system + // notices, so this test pins the size of what is lost: the answers must still agree closely. + O2FlatCSG solid("bracket_scaled"); + buildBracket(solid, 3.); + solid.CloseShape(); + BOOST_REQUIRE(solid.IsClosed()); + + Rng rng(1357911ULL); + double worst = 0.; + for (int trial = 0; trial < 200000; ++trial) { + double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)}; + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + const bool inside = solid.Contains_Loop(point); + // Contains has no arithmetic of its own to lose, so it stays bit-identical under any scale + BOOST_REQUIRE_EQUAL(solid.Contains(point), inside); + const double accelerated = + inside ? solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr) + : solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr); + const double twin = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big()) + : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()); + const double slack = std::abs(accelerated - twin); + worst = std::max(worst, slack); + // The bound is set just above what the rescale actually costs -- the run below measures + // 1.24e-14 -- so the assertion, and not only the message under it, is what pins the size of + // the loss. A looser bound would pass on a rescale that had broken something far larger. + BOOST_REQUIRE_LE(slack, 1.e-13 * std::max(1., std::abs(twin))); + } + BOOST_TEST_MESSAGE("largest accelerated-vs-twin gap under a x3 plane rescale: " << worst); + // The aggregate is deliberately looser than the relative assertion above: it is an absolute + // bound on a maximum over a sample, and FMA contraction or a different libm moves the last + // couple of ulps. 1e-12 still pins the size of the loss a thousand times tighter than the + // 1e-9 this test used to assert, without being a cross-platform tripwire. + BOOST_CHECK_LE(worst, 1.e-12); +} + +BOOST_AUTO_TEST_CASE(a_shape_that_failed_to_close_still_answers_through_the_loop_twins) +{ + // CloseShape refuses an unset cell bbox and builds nothing, so there is no box array and no BVH. + // An accelerated query that walked the empty array would answer "no material anywhere" -- the + // silent vanishing the refusal exists to prevent -- so all three must fall back to the twins. + O2FlatCSG solid("bracket_unclosed"); + buildBracket(solid); + // a fourth cell, a box at x in [12, 14], deliberately left without a bounding box + const double extra[6][2][3] = {{{1., 0., 0.}, {14., 0., 0.}}, + {{-1., 0., 0.}, {12., 0., 0.}}, + {{0., 1., 0.}, {0., 1., 0.}}, + {{0., -1., 0.}, {0., -1., 0.}}, + {{0., 0., 1.}, {0., 0., 1.}}, + {{0., 0., -1.}, {0., 0., -1.}}}; + double coeff[10]; + const int first = solid.GetNhalfspaces(); + for (const auto& plane : extra) { + planeQuadric(plane[0], plane[1], coeff); + solid.AddQuadric(1., coeff); + } + solid.AddCell(first, 6, 2. * 2. * 2.); + + solid.CloseShape(); + BOOST_REQUIRE(!solid.IsClosed()); + BOOST_CHECK_EQUAL(solid.GetNboxes(), 0); + BOOST_CHECK_EQUAL(solid.GetBVHMemory(), 0u); + + // material inside every one of the four cells is still found, and empty space is still empty + const double inArm[3] = {0., 0., 0.}; + const double inUpright[3] = {9., 0., 6.}; + const double inWasher[3] = {-10.5, 0., 2.}; + const double inExtra[3] = {13., 0., 0.}; + // the washer's bore, which is empty space now that the washer sits above the arm + const double inBore[3] = {-8., 0., 2.}; + const double outside[3] = {0., 0., 20.}; + BOOST_CHECK(solid.Contains(inArm)); + BOOST_CHECK(solid.Contains(inUpright)); + BOOST_CHECK(solid.Contains(inWasher)); + BOOST_CHECK(solid.Contains(inExtra)); + BOOST_CHECK(!solid.Contains(inBore)); + BOOST_CHECK(!solid.Contains(outside)); + + Rng rng(24680ULL); + for (int trial = 0; trial < 20000; ++trial) { + const double point[3] = {rng.uniform(-16., 16.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)}; + BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point)); + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + const bool inside = solid.Contains_Loop(point); + if (inside) { + BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr), + solid.DistFromInside_Loop(point, dir, TGeoShape::Big())); + } else { + BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr), + solid.DistFromOutside_Loop(point, dir, TGeoShape::Big())); + } + // Safety falls back to its twin exactly like the other three accelerated queries -- this was + // asserted for Contains and the distances above but never extended to Safety + BOOST_REQUIRE_EQUAL(solid.Safety(point, inside), solid.Safety_Loop(point, inside)); + } +} + +BOOST_AUTO_TEST_CASE(safety_is_sound_and_matches_its_twin) +{ + O2FlatCSG solid("bracket_safety"); + buildBracket(solid); + solid.CloseShape(); + + Rng rng(24680ULL); + for (int trial = 0; trial < 50000; ++trial) { + double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)}; + const bool inside = solid.Contains_Loop(point); + const double safety = solid.Safety(point, inside); + BOOST_REQUIRE_GE(safety, 0.); + BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside)); + + // SOUNDNESS: no point within `safety` of `point` may have the opposite classification + for (int probe = 0; probe < 40; ++probe) { + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + const double reach = safety * rng.uniform(0., 0.999) / norm; + const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1], + point[2] + reach * dir[2]}; + BOOST_REQUIRE_EQUAL(static_cast(solid.Contains_Loop(near)), inside); + } + } +} + +BOOST_AUTO_TEST_CASE(safety_is_sound_when_the_inside_bound_is_actually_nonzero) +{ + // At the class's default split depth, buildBracket's arm is so far from cubic (20 x 2 x 2) that + // the depth cap fires before any leaf fully detaches from all six faces: EVERY box keeps + // nActive != 0, so the inside branch's `nActive == 0` selection path -- the one piece of + // `Safety` whose soundness rests on a structural invariant (design section 4.2's hard guarantee) + // rather than an exact box-distance formula -- is never taken by the test above. Its probes are + // then vacuous: with `safety == 0.`, `reach` is always `0.` too, so the "nearby" point IS the + // query point and the soundness check is trivially true. A deeper split makes solid boxes exist + // (see the fix-round measurement in the task report), which this case forces so the nActive == 0 + // path is genuinely exercised end to end, not just agreed upon by two implementations at zero. + O2FlatCSG solid("bracket_safety_deep"); + buildBracket(solid); + solid.SetSplitDepth(14); + // The size floor has to come down with the depth cap, or it stops the split first: at the + // shipped 0.05 the arm is thinner than one minimum box and no leaf ever detaches. + solid.SetMinBoxFraction(0.002); + solid.CloseShape(); + + bool sawPositiveInsideSafety = false; + Rng rng(11235813ULL); + for (int trial = 0; trial < 50000; ++trial) { + double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)}; + const bool inside = solid.Contains_Loop(point); + const double safety = solid.Safety(point, inside); + BOOST_REQUIRE_GE(safety, 0.); + BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside)); + if (inside && safety > 0.) { + sawPositiveInsideSafety = true; + } + + // the same soundness probes as above, now with genuine reach on at least some trials + for (int probe = 0; probe < 40; ++probe) { + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + const double reach = safety * rng.uniform(0., 0.999) / norm; + const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1], + point[2] + reach * dir[2]}; + BOOST_REQUIRE_EQUAL(static_cast(solid.Contains_Loop(near)), inside); + } + } + BOOST_REQUIRE(sawPositiveInsideSafety); +} + +BOOST_AUTO_TEST_CASE(capacity_is_the_sum_of_the_cell_volumes) +{ + O2FlatCSG solid("bracket_capacity"); + buildBracket(solid); + solid.CloseShape(); + const double expected = 8. * 10. * 1. * 1. + 2. * 2. * 11. + TMath::Pi() * (9. - 1.) * 2.; + BOOST_CHECK_SMALL(solid.Capacity() - expected, 1.e-12); +} + +BOOST_AUTO_TEST_CASE(the_bounding_box_is_tight_around_the_retained_boxes) +{ + // ComputeBBox is the union of the RETAINED boxes -- a subset of the union of the cell AABBs -- + // so exact GetDX/GetDY/GetDZ/GetOrigin values depend on subdivision details rather than on the + // contract. Assert the two legs that matter for navigation correctness instead: the bounding + // box holds the whole solid, and it does not overshoot past what the cells could possibly reach. + O2FlatCSG solid("bracket_bbox"); + buildBracket(solid); + solid.CloseShape(); + + // every point of the solid is inside the bounding box + Rng rng(13579ULL); + for (int trial = 0; trial < 50000; ++trial) { + const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)}; + if (solid.Contains_Loop(point)) { + BOOST_REQUIRE(solid.TGeoBBox::Contains(point)); + } + } + + // the bounding box is contained in the union of the cell AABBs, which buildBracket fixes: + // arm x in [-10, 10], y in [-1, 1], z in [-1, 1]; upright x in [8, 10], y in [-1, 1], z in + // [1, 12]; washer x in [-11, -5], y in [-3, 3], z in [1, 3] + const double cellLo[3][3] = {{-10., -1., -1.}, {8., -1., 1.}, {-11., -3., 1.}}; + const double cellHi[3][3] = {{10., 1., 1.}, {10., 1., 12.}, {-5., 3., 3.}}; + double unionLo[3] = {cellLo[0][0], cellLo[0][1], cellLo[0][2]}; + double unionHi[3] = {cellHi[0][0], cellHi[0][1], cellHi[0][2]}; + for (int cell = 1; cell < 3; ++cell) { + for (int index = 0; index < 3; ++index) { + unionLo[index] = std::min(unionLo[index], cellLo[cell][index]); + unionHi[index] = std::max(unionHi[index], cellHi[cell][index]); + } + } + const double* origin = solid.GetOrigin(); + for (int index = 0; index < 3; ++index) { + const double dHalf = index == 0 ? solid.GetDX() : (index == 1 ? solid.GetDY() : solid.GetDZ()); + BOOST_CHECK_GE(origin[index] - dHalf, unionLo[index] - 1.e-9); + BOOST_CHECK_LE(origin[index] + dHalf, unionHi[index] + 1.e-9); + } +} + +BOOST_AUTO_TEST_CASE(the_normal_on_a_face_is_the_face_normal) +{ + O2FlatCSG solid("box_normal"); + addBoxCell(solid, 3., 4., 5.); + const double lo[3] = {-3., -4., -5.}; + const double hi[3] = {3., 4., 5.}; + solid.SetCellBBox(0, lo, hi); + solid.CloseShape(); + + const double onFace[3] = {3., 1., 1.}; + const double dir[3] = {1., 0., 0.}; + double normal[3] = {0., 0., 0.}; + solid.ComputeNormal(onFace, dir, normal); + BOOST_CHECK_SMALL(normal[0] - 1., 1.e-12); + BOOST_CHECK_SMALL(normal[1], 1.e-12); + BOOST_CHECK_SMALL(normal[2], 1.e-12); +} + +BOOST_AUTO_TEST_CASE(the_normal_selection_is_scale_invariant_across_cells) +{ + // Fix round 1: |EvalHalfspace| alone is not a distance -- its gain per unit distance is 1 for a + // unit plane but ~2R for a cylinder of radius R, so a naive argmin over |f| can pick a distant + // plane over the surface the point is actually on. Two cells make the point concrete: cell 0 is + // a cylinder of radius 100 about z, cell 1 a single plane at z = 0.05. The test point sits + // 0.0005 from the cylinder wall (radially) and 0.05 from the plane -- the cylinder is the true + // nearest surface by two orders of magnitude, but |f_cylinder| ~= 0.1 > |f_plane| = 0.05, so the + // unscaled rule would have picked the plane and returned (0, 0, 1) instead of the correct + // (1, 0, 0). Deliberately not closed: with CloseShape run, the box-restriction half of the fix + // alone would make this pass trivially (the point's own box never sees the other cell's plane), + // so this exercises ComputeNormal's cross-cell fallback scan, where only the |f| / |grad f| + // fix -- not the box restriction -- can be what saves it. + O2FlatCSG solid("scale_invariance"); + double coeff[10]; + zCylinderQuadric(100., coeff); + solid.AddQuadric(1., coeff); + solid.AddCell(0, 1, 0.); + + const double planeNormal[3] = {0., 0., 1.}; + const double planeThrough[3] = {0., 0., 0.05}; + planeQuadric(planeNormal, planeThrough, coeff); + const int planeFirst = solid.GetNhalfspaces(); + solid.AddQuadric(1., coeff); + solid.AddCell(planeFirst, 1, 0.); + + BOOST_REQUIRE(!solid.IsClosed()); + + const double point[3] = {99.9995, 0., 0.}; + const double dir[3] = {1., 0., 0.}; + double normal[3] = {0., 0., 0.}; + solid.ComputeNormal(point, dir, normal); + BOOST_CHECK_SMALL(normal[0] - 1., 1.e-9); + BOOST_CHECK_SMALL(normal[1], 1.e-9); + BOOST_CHECK_SMALL(normal[2], 1.e-9); +} + +BOOST_AUTO_TEST_CASE(a_zero_extent_cell_bbox_does_not_burn_the_whole_cubify_budget) +{ + // Fix round 2: a cell bbox with a genuinely zero extent on one axis passes CloseShape's + // validation (it rejects only unset, inverted or non-finite boxes, not degenerate-but-flat + // ones). Without SplitBox's `shortest` floor at `minSize`, that axis's extent stays pinned at + // zero forever (it is never the longest, so never split), making `longest > 2 * shortest` + // permanently true and spending the ENTIRE per-path cubify ceiling on a cell a depth-only rule + // would have resolved in a handful of splits -- roughly `2^kMaxCubifySplits` leaves along every + // branch instead. A 100 x 100 x 0 slab, subdivided down to the default minSize floor, needs on + // the order of a dozen splits total once x and y are treated as the only axes that matter; this + // asserts the box count stays in that regime rather than climbing towards the ceiling. + O2FlatCSG solid("flat_cell"); + double coeff[10]; + const double planes[6][2][3] = { + {{1., 0., 0.}, {50., 0., 0.}}, {{-1., 0., 0.}, {-50., 0., 0.}}, {{0., 1., 0.}, {0., 50., 0.}}, {{0., -1., 0.}, {0., -50., 0.}}, {{0., 0., 1.}, {0., 0., 0.}}, {{0., 0., -1.}, {0., 0., 0.}}}; + for (const auto& plane : planes) { + planeQuadric(plane[0], plane[1], coeff); + solid.AddQuadric(1., coeff); + } + solid.AddCell(0, 6, 0.); + const double lo[3] = {-50., -50., 0.}; + const double hi[3] = {50., 50., 0.}; + solid.SetCellBBox(0, lo, hi); + solid.CloseShape(); + + BOOST_REQUIRE(solid.IsClosed()); + // measured 272 boxes with the guard in place; a generous margin above that, and two orders of + // magnitude below what hitting the per-path ceiling on every branch would produce + BOOST_CHECK_LT(solid.GetNboxes(), 600); +} + +BOOST_AUTO_TEST_CASE(a_sidecar_round_trip_reproduces_the_solid) +{ + O2FlatCSG original("bracket_io"); + buildBracket(original); + original.CloseShape(); + + const std::string path = "testFlatCSG_roundtrip.bin"; + BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original)); // test-only writer + + O2FlatCSG loaded("bracket_io_loaded"); + BOOST_REQUIRE(o2::cad::LoadFlatCSG(path, loaded)); + loaded.CloseShape(); + + BOOST_CHECK_EQUAL(loaded.GetNhalfspaces(), original.GetNhalfspaces()); + BOOST_CHECK_EQUAL(loaded.GetNcells(), original.GetNcells()); + BOOST_CHECK_EQUAL(loaded.GetNboxes(), original.GetNboxes()); + BOOST_CHECK_EQUAL(loaded.Capacity(), original.Capacity()); + + Rng rng(97531ULL); + for (int trial = 0; trial < 100000; ++trial) { + const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)}; + BOOST_REQUIRE_EQUAL(loaded.Contains(point), original.Contains(point)); + } + std::filesystem::remove(path); +} + +BOOST_AUTO_TEST_CASE(writing_an_unclosed_shape_is_refused) +{ + // GetCellBBox reads back zeros for a cell whose box was never set -- a finite, non-inverted box + // that would otherwise pass CloseShape's own validation on reload, silently shipping a + // degenerate point-box for that cell. WriteFlatCSG refuses before that invariant can ever reach + // a file: no CloseShape() call at all, and a cell missing a bbox (CloseShape() refused). + const std::string path = "testFlatCSG_unclosed.bin"; + + O2FlatCSG neverClosed("bracket_never_closed"); + buildBracket(neverClosed); + BOOST_REQUIRE(!neverClosed.IsClosed()); + BOOST_CHECK(!o2::cad::WriteFlatCSG(path, neverClosed)); + BOOST_CHECK(!std::filesystem::exists(path)); + + O2FlatCSG refused("bracket_refused_close"); + double coeff[10]; + const double plane[2][3] = {{1., 0., 0.}, {0., 0., 0.}}; + planeQuadric(plane[0], plane[1], coeff); + refused.AddQuadric(1., coeff); + refused.AddCell(0, 1, 0.); // no SetCellBBox for this cell -- CloseShape must refuse + refused.CloseShape(); + BOOST_REQUIRE(!refused.IsClosed()); + BOOST_CHECK(!o2::cad::WriteFlatCSG(path, refused)); + BOOST_CHECK(!std::filesystem::exists(path)); +} + +BOOST_AUTO_TEST_CASE(a_truncated_sidecar_is_refused_rather_than_half_loaded) +{ + O2FlatCSG original("bracket_trunc"); + buildBracket(original); + original.CloseShape(); + const std::string path = "testFlatCSG_truncated.bin"; + BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original)); + std::filesystem::resize_file(path, std::filesystem::file_size(path) - 17); + + O2FlatCSG loaded("bracket_trunc_loaded"); + BOOST_CHECK(!o2::cad::LoadFlatCSG(path, loaded)); + std::filesystem::remove(path); +} + +BOOST_AUTO_TEST_CASE(the_shape_survives_a_ROOT_file_without_its_sidecar) +{ + O2FlatCSG original("bracket_root"); + buildBracket(original); + original.CloseShape(); + + const std::string path = "testFlatCSG_shape.root"; + { + TFile file(path.c_str(), "RECREATE"); + file.WriteObject(&original, "shape"); + } + O2FlatCSG* restored = nullptr; + { + TFile file(path.c_str(), "READ"); + file.GetObject("shape", restored); + } + BOOST_REQUIRE(restored != nullptr); + restored->CloseShape(); // the BVH is not streamed; it is rebuilt + + Rng rng(11223ULL); + for (int trial = 0; trial < 100000; ++trial) { + const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)}; + BOOST_REQUIRE_EQUAL(restored->Contains(point), original.Contains(point)); + } + std::filesystem::remove(path); +} + +namespace +{ +/// One axis-aligned box as its own cell, with its bbox. +void addBoxAsCell(O2FlatCSG& solid, const double* lo, const double* hi) +{ + const int first = solid.GetNhalfspaces(); + double coeff[10]; + for (int axis = 0; axis < 3; ++axis) { + for (int sense = -1; sense <= 1; sense += 2) { + double normal[3] = {0., 0., 0.}; + double through[3] = {0., 0., 0.}; + normal[axis] = static_cast(sense); + through[axis] = sense > 0 ? hi[axis] : lo[axis]; + planeQuadric(normal, through, coeff); + solid.AddQuadric(1., coeff); + } + } + const int cell = solid.AddCell(first, 6, (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2])); + solid.SetCellBBox(cell, lo, hi); +} + +/// Three touching cells along x. The two tall ones sit together in the BVH, so the far one is +/// tested while the running bound is still the near one's exit, and only the short cell between +/// them then extends the union past it. +void buildStaggeredChain(O2FlatCSG& solid) +{ + const double nearLo[3] = {0., -1., -1.}; + const double nearHi[3] = {1., 20., 1.}; + const double farLo[3] = {5., -1., -1.}; + const double farHi[3] = {6., 20., 1.}; + const double middleLo[3] = {1., -1., -1.}; + const double middleHi[3] = {5., 1., 1.}; + addBoxAsCell(solid, nearLo, nearHi); + addBoxAsCell(solid, farLo, farHi); + addBoxAsCell(solid, middleLo, middleHi); + solid.CloseShape(); +} +} // namespace + +BOOST_AUTO_TEST_CASE(a_far_box_that_extends_the_union_is_recovered_by_the_unpruned_retry) +{ + O2FlatCSG solid("staggered"); + buildStaggeredChain(solid); + + // along the chain: the answer is the far cell's exit at x = 6, which the pruned traversal can + // only reach through the middle cell it sees last + const double point[3] = {0.5, 0., 0.}; + const double dir[3] = {1., 0., 0.}; + O2FlatCSG::ResetUnprunedRetryCounter(); + const double distance = solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr); + BOOST_CHECK_EQUAL(distance, solid.DistFromInside_Loop(point, dir, TGeoShape::Big())); + BOOST_CHECK_CLOSE(distance, 5.5, 1.e-9); + BOOST_CHECK_GT(O2FlatCSG::GetUnprunedRetryCount(), 0); +} + +BOOST_AUTO_TEST_CASE(the_pruned_DistFromInside_is_bit_identical_to_its_twin_on_the_staggered_chain) +{ + O2FlatCSG solid("staggered_random"); + buildStaggeredChain(solid); + + Rng rng(97531ULL); + int inside = 0; + for (int trial = 0; trial < 100000; ++trial) { + double point[3] = {rng.uniform(-1., 7.), rng.uniform(-2., 21.), rng.uniform(-2., 2.)}; + if (!solid.Contains_Loop(point)) { + continue; + } + double dir[3]; + double norm = 0.; + do { + for (int index = 0; index < 3; ++index) { + dir[index] = rng.uniform(-1., 1.); + } + norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]); + } while (norm < 1.e-3); + for (int index = 0; index < 3; ++index) { + dir[index] /= norm; + } + ++inside; + BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr), + solid.DistFromInside_Loop(point, dir, TGeoShape::Big())); + // a finite step must answer as the twin does with the same step + BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, 2., nullptr), + solid.DistFromInside_Loop(point, dir, 2.)); + } + BOOST_CHECK_GT(inside, 1000); +} diff --git a/Detectors/CADSupport/tools/O2_CADtoTGeo.py b/Detectors/CADSupport/tools/O2_CADtoTGeo.py new file mode 100644 index 0000000000000..85509b35464e4 --- /dev/null +++ b/Detectors/CADSupport/tools/O2_CADtoTGeo.py @@ -0,0 +1,4734 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-02 + +""" +O2_CADtoTGeo.py -- STEP/XCAF -> ROOT TGeo conversion. + +It writes a ROOT macro (geom.C) and, into --output-folder, one facet file per leaf logical volume, +facets__.bin; with --exact-surfaces also surfaces__.bin sidecars (and +brep_*.brep with --dump-brep), and with --csg native ROOT shapes. Materials come from a BOM CSV +(--materials-csv) or a media sidecar (--media-json). VOLNAME is the XCAF label name and LID the +label entry. The STEP length unit is detected, or set with --step-unit; TGeo uses cm. + +Facet file format (little-endian): + uint32 nTriangles + then nTriangles * 9 * float32: + ax ay az bx by bz cx cy cz +""" + +import argparse +import csv +import json +import math +import random +import re +import struct +import sys +from array import array +from collections import Counter +from dataclasses import dataclass +from pathlib import Path as _Path +from typing import Dict, List, Optional, Pattern, Tuple + +import numpy as np + +from OCC.Core.Bnd import Bnd_Box +from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common +from OCC.Core.BRepBndLib import brepbndlib +from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh +from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox +from OCC.Core.BRepTools import breptools, BRepTools_WireExplorer +from OCC.Core.BRep import BRep_Tool +from OCC.Core.Geom2dAdaptor import Geom2dAdaptor_Curve +from OCC.Core.Geom import Geom_TrimmedCurve +from OCC.Core.Geom2d import Geom2d_TrimmedCurve +from OCC.Core.GeomConvert import geomconvert +from OCC.Core.Geom2dConvert import geom2dconvert +from OCC.Core.Convert import Convert_TgtThetaOver2 +from OCC.Core.GeomAbs import ( + GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone, GeomAbs_Sphere, GeomAbs_Torus, + GeomAbs_Line, GeomAbs_Circle, GeomAbs_Ellipse, + GeomAbs_BezierCurve, GeomAbs_BSplineCurve, +) +from OCC.Core.TopExp import TopExp_Explorer, topexp +from OCC.Core.TopLoc import TopLoc_Location +from OCC.Core.TopAbs import TopAbs_REVERSED, TopAbs_WIRE, TopAbs_EDGE, TopAbs_FACE, TopAbs_SOLID +from OCC.Core.TopTools import TopTools_IndexedMapOfShape +from OCC.Core.TopoDS import topods +from OCC.Extend.TopologyUtils import TopologyExplorer + +from OCC.Core.STEPCAFControl import STEPCAFControl_Reader +from OCC.Core.TDocStd import TDocStd_Document +from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool +from OCC.Core.IFSelect import IFSelect_RetDone + +from OCC.Core.TDF import TDF_Label, TDF_LabelSequence, TDF_Tool +from OCC.Core.TCollection import TCollection_AsciiString +from OCC.Core.gp import gp_Pnt, gp_Vec, gp_Trsf +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.GProp import GProp_GProps +from cadsupport import accept # noqa: E402 +from cadsupport.analytic import (CURVE_TYPE_NAME, SURFACE_TYPE_NAME, # noqa: E402 + _analytic_surface_gap, _analytic_surface_proposals, + _sample_surface_for_recognition, _self_test_bezier_patch, + _self_test_tapered_near_circle, _v_cross, _v_dot) + + +# ------------------------------- +# STEP/XCAF loading +# ------------------------------- + +def load_step_with_xcaf(path: str): + doc = TDocStd_Document("pythonocc-doc") + reader = STEPCAFControl_Reader() + reader.SetColorMode(True) + reader.SetNameMode(True) + reader.SetLayerMode(True) + + status = reader.ReadFile(path) + if status != IFSelect_RetDone: + raise RuntimeError(f"STEP read failed for: {path}") + + reader.Transfer(doc) + shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) + return doc, shape_tool + + +def label_id(label: TDF_Label) -> str: + s = TCollection_AsciiString() + TDF_Tool.Entry(label, s) + return s.ToCString() + + +def label_name(label: TDF_Label) -> str: + # Uses the XCAF/STEP name when present; can be empty. + try: + n = label.GetLabelName() + if n: + return str(n) + except Exception: + pass + return "" + + +# ------------------------------- +# Units +# ------------------------------- + +def step_unit_scale_to_cm(step_unit: str) -> float: + step_unit = (step_unit or "auto").lower() + if step_unit == "mm": + return 0.1 + if step_unit == "cm": + return 1.0 + if step_unit == "m": + return 100.0 + if step_unit == "in": + return 2.54 + if step_unit == "ft": + return 30.48 + raise ValueError(f"Unknown --step-unit {step_unit} (use auto, mm, cm, m, in, ft)") + + +def detect_step_length_unit(step_path: str) -> str: + """ + Heuristic unit detection by scanning STEP file text for common unit tokens. + This avoids relying on OCCT APIs that can vary across pythonOCC builds. + + Returns one of: mm, cm, m, in, ft. Defaults to mm if uncertain. + """ + p = _Path(step_path) + # STEP can be huge: read only the first few MB; units are near the header. + max_bytes = 4 * 1024 * 1024 + data = p.open("rb").read(max_bytes).decode("latin-1", errors="ignore").upper() + + if ".MILLI." in data: + return "mm" + if ".CENTI." in data: + return "cm" + if ".METRE." in data or ".METER." in data: + return "m" + if "INCH" in data: + return "in" + if "FOOT" in data or "FEET" in data: + return "ft" + + # Conservative default for mechanical CAD STEP is mm + return "mm" + + +@dataclass(frozen=True) +class ClipBox: + xmin: float + ymin: float + zmin: float + xmax: float + ymax: float + zmax: float + + @classmethod + def from_values(cls, values: List[float]) -> "ClipBox": + if len(values) != 6: + raise ValueError("--clip-box expects 6 values: xmin ymin zmin xmax ymax zmax") + xmin, ymin, zmin, xmax, ymax, zmax = (float(v) for v in values) + if not (xmin < xmax and ymin < ymax and zmin < zmax): + raise ValueError("--clip-box requires xmin Tuple[float, float, float, float, float, float]: + return (self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.zmax) + + +@dataclass(frozen=True) +class NameFilter: + include: Tuple[Pattern[str], ...] + exclude: Tuple[Pattern[str], ...] + + @classmethod + def from_patterns(cls, include: List[str], exclude: List[str], case_sensitive: bool = False) -> "NameFilter": + flags = 0 if case_sensitive else re.IGNORECASE + return cls( + tuple(re.compile(pattern, flags) for pattern in include), + tuple(re.compile(pattern, flags) for pattern in exclude), + ) + + @property + def active(self) -> bool: + return bool(self.include or self.exclude) + + @property + def has_include(self) -> bool: + return bool(self.include) + + def _text(self, lid: str, name: str) -> str: + return f"{name} {lid}".strip() + + def matches_include(self, lid: str, name: str) -> bool: + text = self._text(lid, name) + return any(pattern.search(text) for pattern in self.include) + + def matches_exclude(self, lid: str, name: str) -> bool: + text = self._text(lid, name) + return any(pattern.search(text) for pattern in self.exclude) + + +# ------------------------------- +# Triangulation helpers +# ------------------------------- + +def triangulate_asbbox(shape, scale_to_cm: float = 1.0): + box = Bnd_Box() + brepbndlib.Add(shape, box) + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + + p000 = (xmin, ymin, zmin) + p001 = (xmin, ymin, zmax) + p010 = (xmin, ymax, zmin) + p011 = (xmin, ymax, zmax) + p100 = (xmax, ymin, zmin) + p101 = (xmax, ymin, zmax) + p110 = (xmax, ymax, zmin) + p111 = (xmax, ymax, zmax) + + triangles = [ + (p000, p100, p110), (p000, p110, p010), + (p001, p111, p101), (p001, p011, p111), + (p000, p101, p100), (p000, p001, p101), + (p010, p110, p111), (p010, p111, p011), + (p000, p010, p011), (p000, p011, p001), + (p100, p101, p111), (p100, p111, p110), + ] + tris = np.array([a + b + c for (a, b, c) in triangles], dtype=float) + return tris * scale_to_cm if scale_to_cm != 1.0 else tris + + +def triangulate_CAD_solid(my_solid, meshparam, scale_to_cm: float = 1.0): + lin_defl = float(meshparam.get("lin_defl", 0.1)) + ang_defl = float(meshparam.get("ang_defl", 0.1)) + + BRepMesh_IncrementalMesh(my_solid, lin_defl, False, ang_defl, True) + + chunks = [] + for face in TopologyExplorer(my_solid).faces(): + loc = TopLoc_Location() + triangulation = BRep_Tool.Triangulation(face, loc) + if triangulation is None or triangulation.NbTriangles() == 0: + continue + + trsf = loc.Transformation() + nodes = np.array([(p.X(), p.Y(), p.Z()) for p in + (triangulation.Node(i).Transformed(trsf) + for i in range(1, triangulation.NbNodes() + 1))], dtype=float) + idx = np.array([triangulation.Triangle(i).Get() + for i in range(1, triangulation.NbTriangles() + 1)], dtype=np.int64) - 1 + if face.Orientation() == TopAbs_REVERSED: + idx = idx[:, [0, 2, 1]] + chunks.append(nodes[idx].reshape(-1, 9)) + + tris = np.concatenate(chunks) if chunks else np.zeros((0, 9)) + return tris * scale_to_cm if scale_to_cm != 1.0 else tris + + +# ------------------------------- +# Volume helpers (for density) +# ------------------------------- + +def volume_cm3_of_shape(shape, scale_to_cm: float) -> float: + """Compute CAD solid volume in cm^3 (using STEP->cm scale).""" + try: + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + # volume returned in STEP length units^3 + v = float(props.Mass()) + return v * (scale_to_cm ** 3) + except Exception: + pass + + # Fallback: bounding-box volume (rough but always defined) + box = Bnd_Box() + brepbndlib.Add(shape, box) + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + dx, dy, dz = (xmax - xmin) * scale_to_cm, (ymax - ymin) * scale_to_cm, (zmax - zmin) * scale_to_cm + return max(dx, 0.0) * max(dy, 0.0) * max(dz, 0.0) + + +def _leaf_volume_cm3(lid: str, scale_to_cm: float) -> float: + """The CAD volume of a leaf before clipping, in cm^3, or 0.0 when it cannot be computed.""" + shape = def_volume_source.get(lid) + if shape is None: + return 0.0 + try: + return volume_cm3_of_shape(shape, scale_to_cm=scale_to_cm) + except Exception: + return 0.0 + + +# ------------------------------- +# Naming helpers +# ------------------------------- + +def import_csg_hook(): + """Import `cadsupport/hook.py` lazily.""" + from cadsupport import hook + return hook + + +def sanitize_cpp_name(s: str) -> str: + safe = re.sub(r"[^0-9a-zA-Z]", "_", s) + if not safe: + safe = "x" + if not (safe[0].isalpha() or safe[0] == "_"): + safe = "_" + safe + return safe + + +def sanitize_filename(s: str) -> str: + safe = re.sub(r"[^0-9a-zA-Z]", "_", s) + return safe or "x" + + +# ------------------------------- +# Binary facet IO +# ------------------------------- + +def write_facets_bin(path: _Path, triangles): + path.parent.mkdir(parents=True, exist_ok=True) + tris = np.asarray(triangles, dtype=float).reshape(-1, 9) + with open(path, "wb") as f: + f.write(struct.pack(" List[float]: + return [v.X() * scale, v.Y() * scale, v.Z() * scale] + + +def _surface_params(adaptor: BRepAdaptor_Surface, surf_type: str, scale_to_cm: float) -> dict: + """Extracts the analytic parameters (lengths in cm, angles in rad) for simple types.""" + s = scale_to_cm + try: + if surf_type == "plane": + pln = adaptor.Plane() + ax3 = pln.Position() + return { + "origin_cm": _xyz(ax3.Location(), s), + "normal": _xyz(pln.Axis().Direction()), + "axis_u": _xyz(ax3.XDirection()), + "axis_v": _xyz(ax3.YDirection()), + } + if surf_type == "cylinder": + cyl = adaptor.Cylinder() + ax3 = cyl.Position() + return { + "origin_cm": _xyz(ax3.Location(), s), + "axis": _xyz(cyl.Axis().Direction()), + "ref_axis_u": _xyz(ax3.XDirection()), + "radius_cm": cyl.Radius() * s, + } + if surf_type == "cone": + cone = adaptor.Cone() + ax3 = cone.Position() + return { + "origin_cm": _xyz(ax3.Location(), s), + "axis": _xyz(cone.Axis().Direction()), + "ref_axis_u": _xyz(ax3.XDirection()), + "ref_radius_cm": cone.RefRadius() * s, + "half_angle_rad": cone.SemiAngle(), + "apex_cm": _xyz(cone.Apex(), s), + } + if surf_type == "sphere": + sph = adaptor.Sphere() + ax3 = sph.Position() + return { + "center_cm": _xyz(ax3.Location(), s), + "polar_axis": _xyz(ax3.Direction()), + "ref_axis_u": _xyz(ax3.XDirection()), + "radius_cm": sph.Radius() * s, + } + if surf_type == "torus": + tor = adaptor.Torus() + ax3 = tor.Position() + return { + "center_cm": _xyz(ax3.Location(), s), + "axis": _xyz(ax3.Direction()), + "ref_axis_u": _xyz(ax3.XDirection()), + "major_radius_cm": tor.MajorRadius() * s, + "minor_radius_cm": tor.MinorRadius() * s, + } + except Exception as exc: + return {"error": f"parameter extraction failed: {exc}"} + return {} + + +def _edge_pcurve_is_iso(edge, face, uv_bounds) -> bool: + """True when the edge's 2D pcurve on the face is iso-parametric (u or v constant).""" + try: + curve2d, first, last = BRep_Tool.CurveOnSurface(edge, face) + except Exception: + return False + if curve2d is None: + return False + us, vs = [], [] + for i in range(5): + t = first + (last - first) * i / 4.0 + p = curve2d.Value(t) + us.append(p.X()) + vs.append(p.Y()) + umin, umax, vmin, vmax = uv_bounds + tol_u = 1e-6 * max(1.0, abs(umax - umin)) + tol_v = 1e-6 * max(1.0, abs(vmax - vmin)) + return (max(us) - min(us) <= tol_u) or (max(vs) - min(vs) <= tol_v) + + +def classify_face(face, scale_to_cm: float, recognize_surfaces: bool = True, + recognition=None, key=None) -> dict: + """Classifies a single TopoDS face: analytic type, parameters, wires and edges. + + With `recognize_surfaces` a face whose stored type has no extractor also goes to the + canonical-form recognizer (a surface-only, optimistic claim). `recognition`, when given, + receives the recognizer's result under `key`, for the extraction. + """ + adaptor = BRepAdaptor_Surface(face) + surf_type = SURFACE_TYPE_NAME.get(adaptor.GetType(), "unknown") + + try: + uv_bounds = list(breptools.UVBounds(face)) + except Exception: + uv_bounds = [float("nan")] * 4 + + record = { + "type": surf_type, + "orientation_reversed": face.Orientation() == TopAbs_REVERSED, + "uv_bounds": uv_bounds, + "params": _surface_params(adaptor, surf_type, scale_to_cm), + "wires": [], + } + + if recognize_surfaces and surf_type not in _SUPPORTED_SURFACE_TYPES and not any(math.isnan(x) for x in uv_bounds): + rec = _recognize_analytic_surface(adaptor, uv_bounds) + if recognition is not None: + recognition[key] = rec + if rec is not None: + record["recognized_type"] = rec["kind"] + record["recognized_residual"] = rec["residual"] + # The achieved gap in cm; `recognized_residual` is it relative to the patch diagonal. + record["recognized_gap_cm"] = rec["gap"] * scale_to_cm + record["recognized_gap_relative"] = rec["gap_relative"] + + try: + outer_wire = breptools.OuterWire(face) + except Exception: + outer_wire = None + + wx = TopExp_Explorer(face, TopAbs_WIRE) + while wx.More(): + wire = topods.Wire(wx.Current()) + curve_types: Dict[str, int] = {} + n_edges = 0 + n_degenerated = 0 + all_pcurves_iso = True + + ex = TopExp_Explorer(wire, TopAbs_EDGE) + while ex.More(): + edge = topods.Edge(ex.Current()) + n_edges += 1 + if BRep_Tool.Degenerated(edge): + # degenerate edges (sphere poles, cone apex) carry no 3D curve; + # their pcurves are iso lines by construction + n_degenerated += 1 + else: + try: + ctype = CURVE_TYPE_NAME.get(BRepAdaptor_Curve(edge).GetType(), "unknown") + except Exception: + ctype = "unknown" + curve_types[ctype] = curve_types.get(ctype, 0) + 1 + if not _edge_pcurve_is_iso(edge, face, uv_bounds): + all_pcurves_iso = False + ex.Next() + + record["wires"].append({ + "outer": bool(outer_wire is not None and wire.IsSame(outer_wire)), + "n_edges": n_edges, + "n_degenerated": n_degenerated, + "curve_types": curve_types, + "all_pcurves_iso": all_pcurves_iso, + }) + wx.Next() + + return record + + +def face_supported(record: dict) -> Tuple[bool, Optional[str]]: + """Evaluates one classify_face record against the current C++ support matrix.""" + surf_type = record["type"] + if surf_type not in _SUPPORTED_SURFACE_TYPES: + recognized = record.get("recognized_type") + if recognized is not None: + record["trim_kind"] = "recognized" + return True, None + return False, f"unsupported surface type '{surf_type}'" + + curve_types = set() + for w in record["wires"]: + curve_types.update(w["curve_types"].keys()) + + if surf_type == "plane": + bad = curve_types - _SUPPORTED_PLANAR_CURVES + if bad: + return False, f"plane with unsupported boundary curves: {sorted(bad)}" + record["trim_kind"] = "wires" + return True, None + + # Quadrics: only the boundary-curve type limits eligibility here. + bad = curve_types - _SUPPORTED_QUADRIC_CURVES + if bad: + record["trim_kind"] = "general" + return False, f"{surf_type} with unsupported trim curves: {sorted(bad)}" + is_rectangle = len(record["wires"]) == 1 and all(w["all_pcurves_iso"] for w in record["wires"]) + record["trim_kind"] = "parametric-rectangle" if is_rectangle else "general" + return True, None + + +def distill_reasons(reasons: List[str]) -> Optional[str]: + """Fold a per-face reason list into one brief line, most frequent first. + + "40 face(s): unsupported surface type 'bspline'; 2 face(s): ..." -- the `why_not_surface` field. + """ + if not reasons: + return None + counts: Dict[str, int] = {} + for r in reasons: + counts[r] = counts.get(r, 0) + 1 + return "; ".join(f"{n} face(s): {r}" + for r, n in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0]))) + + +def build_surface_report(step_path: str, scale_to_cm: float, recognize_surfaces: bool = True, + recognition=None) -> dict: + """Builds the JSON-serializable exact-conversion eligibility report over def_shapes. + + With `recognize_surfaces` it also tallies the recognition pre-pass; `recognition` collects each + recognition by (lid, face index) for `extract_surfaces_for_shape`. + """ + volumes = {} + n_eligible = 0 + face_type_counts: Dict[str, int] = {} + curve_type_counts: Dict[str, int] = {} + fallback_reasons: Dict[str, int] = {} + recognized_surface_counts: Dict[str, int] = {} + recognized_stored_type_counts: Dict[str, int] = {} + + recognized_max_gap_cm: Dict[str, float] = {} + n_eligible_without_recognition = 0 + n_rescued_by_recognition = 0 + + for lid, shape in def_shapes.items(): + faces = [] + for index, face in enumerate(TopologyExplorer(shape).faces()): + rec = classify_face(face, scale_to_cm, recognize_surfaces=recognize_surfaces, + recognition=recognition, key=(lid, index)) + ok, reason = face_supported(rec) + rec["supported"] = ok + if reason: + rec["reason"] = reason + fallback_reasons[reason] = fallback_reasons.get(reason, 0) + 1 + faces.append(rec) + + face_type_counts[rec["type"]] = face_type_counts.get(rec["type"], 0) + 1 + for w in rec["wires"]: + for ctype, n in w["curve_types"].items(): + curve_type_counts[ctype] = curve_type_counts.get(ctype, 0) + n + recognized_kind = rec.get("recognized_type") + if recognized_kind is not None: + recognized_surface_counts[recognized_kind] = recognized_surface_counts.get(recognized_kind, 0) + 1 + recognized_stored_type_counts[rec["type"]] = recognized_stored_type_counts.get(rec["type"], 0) + 1 + gap = rec.get("recognized_gap_cm", 0.0) + recognized_max_gap_cm[recognized_kind] = max(recognized_max_gap_cm.get(recognized_kind, 0.0), gap) + + eligible = bool(faces) and all(f["supported"] for f in faces) + # The coverage *delta* recognition is responsible for: how the same solid would score with + # the pre-pass switched off. Quoting `n_eligible` on its own does not say that. + eligible_without = bool(faces) and all( + f["supported"] and f.get("recognized_type") is None for f in faces) + if eligible: + n_eligible += 1 + if eligible_without: + n_eligible_without_recognition += 1 + elif eligible: + n_rescued_by_recognition += 1 + vol_recognized: Dict[str, int] = {} + vol_gap = 0.0 + for f in faces: + k = f.get("recognized_type") + if k is not None: + vol_recognized[k] = vol_recognized.get(k, 0) + 1 + vol_gap = max(vol_gap, f.get("recognized_gap_cm", 0.0)) + volumes[lid] = { + "name": def_names.get(lid, ""), + "n_faces": len(faces), + "eligible": eligible, + "eligible_without_recognition": eligible_without, + "recognized_counts": vol_recognized, + "recognized_max_gap_cm": vol_gap, + # Brief reason this solid cannot be a SurfaceSolid; extraction may refine it. + "why_not_surface": None if eligible else distill_reasons( + [f.get("reason") or f"unsupported {f['type']} face" + for f in faces if not f["supported"]]), + "faces": faces, + } + + return { + "report_version": 1, + "step_file": step_path, + "scale_to_cm": scale_to_cm, + "summary": { + "n_volumes": len(volumes), + "n_eligible": n_eligible, + "face_type_counts": face_type_counts, + "curve_type_counts": curve_type_counts, + "fallback_reasons": fallback_reasons, + "recognized_surface_counts": recognized_surface_counts, + "recognized_stored_type_counts": recognized_stored_type_counts, + "recognized_max_gap_cm": recognized_max_gap_cm, + "recognized_acceptance_tolerance_relative": _RECOGNIZE_TOL_EXACT, + "n_eligible_without_recognition": n_eligible_without_recognition, + "n_rescued_by_recognition": n_rescued_by_recognition, + }, + "volumes": volumes, + } + + +# ------------------------------- +# Surface sidecar binary IO +# ------------------------------- +# Versioned binary sidecar for exact surfaces (surfaces_*.bin), read by o2::cad::LoadSurfaceSolid. + +SURFACE_SIDECAR_MAGIC = b"O2SS" +# Version 2 appends a float64 model tolerance (cm) to the fixed header. +# Version 3 appends a uint32 edge-table size and, per surface, its boundary edges' (edgeId, flags). +SURFACE_SIDECAR_VERSION = 3 +SURFACE_TYPE_ENUM = {"plane": 1, "cylinder": 2, "cone": 3, "sphere": 4, "torus": 5} +CURVE_TYPE_ENUM = {"line": 0, "arc": 1, "bspline": 2} +SURFACE_FLAG_INNER_WALL = 1 << 0 + +# Per-boundary-edge flag bits, version 3. +EDGE_FLAG_REVERSED = 1 << 0 # the face traverses the edge against the edge's own direction +EDGE_FLAG_DEGENERATE = 1 << 1 # BRep_Tool.Degenerated: a cone apex / sphere pole, no 3D curve +EDGE_FLAG_ANCHORED = 1 << 2 # entry i is trim curve i of this surface, in flattened wire order + + +def build_edge_table(shape): + """Index every TopoDS_Edge of \\a shape once, and return (map, edge_id). + + `edge_id(edge)` is a 0-based id stable for the whole solid: two faces' trims share an edge by id. + """ + edge_map = TopTools_IndexedMapOfShape() + topexp.MapShapes(shape, TopAbs_EDGE, edge_map) + + def edge_id(edge) -> int: + return edge_map.FindIndex(edge) - 1 # FindIndex is 1-based; 0 means "not in the map" + + return edge_map, edge_id + + +def face_boundary_edge_refs(face, edge_id, anchored: bool, wires=None) -> List[Tuple[int, int]]: + """The face's boundary edges as ordered (edgeId, flags) pairs, in `_face_wire_edges` order. + + `anchored` says whether the record carries the wire block; `wires` is + `list(_face_wire_edges(face))` when the caller has it. + """ + refs: List[Tuple[int, int]] = [] + base_flags = EDGE_FLAG_ANCHORED if anchored else 0 + for _wire, _is_outer, edges in (_face_wire_edges(face) if wires is None else wires): + for edge, _start_vertex in edges: + flags = base_flags + if edge.Orientation() == TopAbs_REVERSED: + flags |= EDGE_FLAG_REVERSED + if BRep_Tool.Degenerated(edge): + flags |= EDGE_FLAG_DEGENERATE + refs.append((edge_id(edge), flags)) + return refs + + +def write_surfaces_bin(path: _Path, surfaces: List[dict], model_tolerance_cm: float = 0.0, + n_model_edges: int = 0): + """Write a surfaces_*.bin sidecar, version 3.""" + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, "wb") as f: + f.write(SURFACE_SIDECAR_MAGIC) + f.write(struct.pack(" 0.0: + axis_v = ydir + else: + axis_v = [-c for c in ydir] + return origin_cm, axis_u, axis_v + + +def _face_wire_edges(face): + """Yield (wire, is_outer, [edges-in-connected-order]) for every wire of the face.""" + try: + outer_wire = breptools.OuterWire(face) + except Exception: + outer_wire = None + wx = TopExp_Explorer(face, TopAbs_WIRE) + while wx.More(): + wire = topods.Wire(wx.Current()) + edges = [] + we = BRepTools_WireExplorer(wire, face) + while we.More(): + edges.append((we.Current(), we.CurrentVertex())) + we.Next() + is_outer = outer_wire is not None and wire.IsSame(outer_wire) + yield wire, is_outer, edges + wx.Next() + + +def _planar_projector(origin_cm, axis_u, axis_v, s): + """Return project(gp_Pnt) -> (u, v): the point's plane-local coordinates in cm.""" + def project(pnt) -> Tuple[float, float]: + rel = [pnt.X() * s - origin_cm[0], pnt.Y() * s - origin_cm[1], pnt.Z() * s - origin_cm[2]] + return _v_dot(rel, axis_u), _v_dot(rel, axis_v) + return project + + +def _arc_edge_params(edge, curve, project, s) -> Tuple[Optional[List[float]], Optional[str]]: + """Build [cu, cv, radius, startAngle, phiSweep] for a circular boundary edge. + + The signed sweep is recovered by sampling the 3D edge in *wire-traversal* order (the edge + is walked backwards when its orientation is REVERSED relative to the underlying curve), + projecting each sample into the plane frame and unwrapping the polar angle. This is robust + to full circles (single periodic edge -> +/-2pi), arcs wider than pi, and either winding. + """ + circ = curve.Circle() + cu, cv = project(circ.Location()) + radius = circ.Radius() * s + first, last = curve.FirstParameter(), curve.LastParameter() + reversed_edge = edge.Orientation() == TopAbs_REVERSED + angles: List[float] = [] + for tau in (0.0, 0.25, 0.5, 0.75, 1.0): + t = (1.0 - tau) if reversed_edge else tau + u, v = project(curve.Value(first + t * (last - first))) + angles.append(math.atan2(v - cv, u - cu)) + unwrapped = [angles[0]] + for a in angles[1:]: + d = a - unwrapped[-1] + d -= 2.0 * math.pi * math.floor((d + math.pi) / (2.0 * math.pi)) # wrap into (-pi, pi] + unwrapped.append(unwrapped[-1] + d) + sweep = unwrapped[-1] - unwrapped[0] + if abs(sweep) < _EXTRACT_TOL: + return None, "planar arc edge has a degenerate sweep" + return [cu, cv, radius, unwrapped[0], sweep], None + + +def _bspline_flat_params(first: float, last: float, reversed_edge: bool, pole_xform, to_bspline): + """Flat sidecar B-spline record [degree, nPoles, poles(2*nPoles), weights(nPoles), + knots(nPoles+degree+1)] for a curve segment [first, last]. + + `to_bspline(lo, hi)` trims the source curve to [lo, hi] and returns a clamped (Geom or Geom2d) + BSplineCurve; `pole_xform(pole)` maps one control point to its output (u, v). The curve is + trimmed *before* conversion so the parametrisation matches the edge; a periodic result is made + non-periodic. Poles/weights/knots are reversed when the edge runs opposite the curve.""" + lo, hi = (first, last) if first <= last else (last, first) + bs = to_bspline(lo, hi) + if bs is None: + return None + if bs.IsPeriodic(): + bs.SetNotPeriodic() + degree = bs.Degree() + nb = bs.NbPoles() + if degree < 1 or nb < degree + 1: + return None + poles = [] + weights = [] + for i in range(1, nb + 1): + u, v = pole_xform(bs.Pole(i)) + poles.append((u, v)) + weights.append(bs.Weight(i)) + flat = [] + for i in range(1, bs.NbKnots() + 1): + flat.extend([bs.Knot(i)] * bs.Multiplicity(i)) + if len(flat) != nb + degree + 1: + return None + if reversed_edge: + poles.reverse() + weights.reverse() + span = flat[0] + flat[-1] + flat = [span - k for k in reversed(flat)] + params = [float(degree), float(nb)] + for u, v in poles: + params.extend([float(u), float(v)]) + params.extend(float(w) for w in weights) + params.extend(float(k) for k in flat) + return params + + +# Relative residual below which a sampled trim curve is taken as EXACTLY a line or a circle; +# an almost-circle stays a B-spline. +_CANONICAL_CURVE_TOL = 1.e-9 + + +def _recognize_canonical_curve(samples, poles=None): + """Recognize a sampled 2D trim curve as an exact line or circle in its output domain. + + `samples` are points in the output domain, in edge direction; collinear `poles`, when given, + prove a straight segment. Returns ("line", [u0, v0, u1, v1]), ("arc", [cu, cv, r, a0, sweep]) + or (None, None). + """ + points = np.asarray(samples, dtype=float) + if len(points) < 3: + return None, None + extent = float(np.linalg.norm(points.max(axis=0) - points.min(axis=0))) + if extent < _EXTRACT_TOL: + return None, None + + # --- straight line + chord = points[-1] - points[0] + chord_length = float(np.linalg.norm(chord)) + if chord_length > _EXTRACT_TOL: + unit = chord / chord_length + def off_axis(candidates): + rel = candidates - points[0] + return float(np.abs(rel[:, 0] * unit[1] - rel[:, 1] * unit[0]).max() / extent) + straight = off_axis(points) < _CANONICAL_CURVE_TOL + if straight and poles is not None and len(poles) >= 2: + straight = off_axis(np.asarray(poles, dtype=float)) < _CANONICAL_CURVE_TOL + if straight: + # reject a curve that doubles back along its own chord: geometrically it is not the + # segment from the first point to the last one, however collinear the samples are + along = (points - points[0]) @ unit + if np.all(np.diff(along) >= -_CANONICAL_CURVE_TOL * extent): + return "line", [float(points[0][0]), float(points[0][1]), + float(points[-1][0]), float(points[-1][1])] + + # --- circle: |P - C|^2 = R^2 linearized as 2 P.C + (R^2 - |C|^2) = |P|^2, one least-squares + # solve with no initial guess. A closed loop (zero chord) lands here as well as an open arc. + matrix = np.column_stack([2.0 * points, np.ones(len(points))]) + solution, *_ = np.linalg.lstsq(matrix, np.einsum('ij,ij->i', points, points), rcond=None) + centre = solution[:2] + radius_sq = solution[2] + float(centre @ centre) + if radius_sq <= 0.0: + return None, None + radius = math.sqrt(radius_sq) + if float(np.abs(np.linalg.norm(points - centre, axis=1) - radius).max() / extent) >= _CANONICAL_CURVE_TOL: + return None, None + # sweep by accumulating signed angle steps, so a full turn and the traversal sense survive + angles = np.arctan2(points[:, 1] - centre[1], points[:, 0] - centre[0]) + steps = np.diff(angles) + steps = (steps + math.pi) % (2.0 * math.pi) - math.pi + sweep = float(steps.sum()) + if abs(sweep) < _EXTRACT_TOL: + return None, None + return "arc", [float(centre[0]), float(centre[1]), radius, float(angles[0]), sweep] + + +def _sample_curve_in_domain(curve, first, last, reversed_edge, point_map, n=64): + """Sample an OCC curve over [first, last] and map each point into the output domain, ordered + along the edge. `point_map(p)` takes the curve's own point type to an output (u, v).""" + lo, hi = (first, last) if first <= last else (last, first) + if not (math.isfinite(lo) and math.isfinite(hi)) or hi - lo <= 0.0: + return None + try: + samples = [point_map(curve.Value(float(t))) for t in np.linspace(lo, hi, n)] + except Exception: + return None + if reversed_edge: + samples.reverse() + return samples + + +def _planar_bspline_edge_params(edge, project) -> Optional[List[float]]: + """Sidecar B-spline record for a planar face's B-spline / Bezier boundary edge. + + The 3D boundary curve lies in the plane, so projecting its control poles into the plane frame + (an affine map) yields the exact 2D B-spline. Returns None on failure (caller falls back).""" + try: + curve3d, first, last = BRep_Tool.Curve(edge) + if curve3d is None: + return None + reversed_edge = edge.Orientation() == TopAbs_REVERSED + + def to_bspline(lo, hi): + trimmed = Geom_TrimmedCurve(curve3d, lo, hi) + return geomconvert.CurveToBSplineCurve(trimmed, Convert_TgtThetaOver2) + + return _bspline_flat_params(first, last, reversed_edge, project, to_bspline) + except Exception: + return None + + +def _planar_canonical_edge(edge, project, params): + """Recognize a planar face's B-spline boundary edge as an exact line or arc in the plane frame. + + `params` is the already-extracted flat B-spline record, whose poles are reused as the convex + hull evidence for straightness. Returns ("line"|"arc", canonical_params) or (None, None).""" + try: + curve3d, first, last = BRep_Tool.Curve(edge) + if curve3d is None: + return None, None + reversed_edge = edge.Orientation() == TopAbs_REVERSED + samples = _sample_curve_in_domain(curve3d, first, last, reversed_edge, project) + if not samples: + return None, None + n_poles = int(params[1]) + poles = [(params[2 + 2 * i], params[3 + 2 * i]) for i in range(n_poles)] + return _recognize_canonical_curve(samples, poles) + except Exception: + return None, None + + +def extract_planar_face(face, scale_to_cm: float, frame_override=None, + wires=None) -> Tuple[Optional[dict], Optional[str]]: + """Convert a planar TopoDS face into a sidecar 'plane' surface record with general + line/arc/B-spline boundary wires; any other boundary curve forces a fallback. + + `frame_override` (origin_cm, axis_u, axis_v) replaces the face's own plane frame, for a face + recognized as flat whose stored type is not a plane. + """ + if frame_override is not None: + origin_cm, axis_u, axis_v = frame_override + else: + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Plane: + return None, f"not a plane ({SURFACE_TYPE_NAME.get(adaptor.GetType(), 'unknown')})" + origin_cm, axis_u, axis_v = _planar_frame(face, scale_to_cm) + s = scale_to_cm + project = _planar_projector(origin_cm, axis_u, axis_v, s) + + wires_out: List[dict] = [] + for wire, is_outer, edges in (_face_wire_edges(face) if wires is None else wires): + classified = [] # (edge, curve, geom_type, projected start (u, v)) + for edge, start_vertex in edges: + if BRep_Tool.Degenerated(edge): + return None, "planar face has a degenerated boundary edge" + try: + curve = BRepAdaptor_Curve(edge) + gt = curve.GetType() + except Exception: + gt = None + if gt not in (GeomAbs_Line, GeomAbs_Circle, GeomAbs_Ellipse, + GeomAbs_BSplineCurve, GeomAbs_BezierCurve): + name = CURVE_TYPE_NAME.get(gt, "unknown") + return None, (f"planar boundary edge is a {name} curve " + "(only line/circle/ellipse/bspline supported)") + classified.append((edge, curve, gt, project(BRep_Tool.Pnt(start_vertex)))) + + n = len(classified) + if n == 0: + return None, "planar face has an empty wire" + + # Canonical-form pre-pass, before the polygon check: a straight B-spline becomes a line. + resolved = [] # per edge: ("line", None) | ("arc", params) | ("bspline", params) + for edge, curve, gt, _start_uv in classified: + if gt == GeomAbs_Line: + resolved.append(("line", None)) + elif gt == GeomAbs_Circle: + params, reason = _arc_edge_params(edge, curve, project, s) + if params is None: + return None, reason + resolved.append(("arc", params)) + else: # ellipse / B-spline / Bezier: project the 3D poles into the plane frame + # An ellipse is its exact rational quadratic B-spline; the projection is an isometry. + params = _planar_bspline_edge_params(edge, project) + if params is None: + return None, "planar B-spline boundary edge extraction failed" + canonical_kind, canonical = _planar_canonical_edge(edge, project, params) + if canonical_kind == "line": + resolved.append(("line", None)) + elif canonical_kind == "arc": + resolved.append(("arc", canonical)) + else: + resolved.append(("bspline", params)) + + n_curved = sum(1 for kind, _ in resolved if kind != "line") + if n_curved == 0 and n < 3: + return None, "planar polygon wire has fewer than 3 edges" + + seg_edges = [] + for i, (kind, params) in enumerate(resolved): + if kind == "line": + u0, v0 = classified[i][3] + u1, v1 = classified[(i + 1) % n][3] + seg_edges.append({"curve": "line", "params": [u0, v0, u1, v1]}) + else: + seg_edges.append({"curve": kind, "params": params}) + wires_out.append({"role": "outer" if is_outer else "inner", "edges": seg_edges}) + + if not wires_out: + return None, "planar face has no wires" + n_outer = sum(1 for w in wires_out if w["role"] == "outer") + if n_outer != 1: + return None, f"planar face has {n_outer} outer wires (expected exactly 1)" + + return {"type": "plane", "params": list(origin_cm) + list(axis_u) + list(axis_v), "wires": wires_out}, None + + +def _quadric_phi_range(ax3, umin: float, umax: float) -> Tuple[float, float]: + """Map an OCC angular U-range [umin, umax] to the C++ (phiStart, phiSweep). + + The C++ bounded quadrics measure phi in a right-handed frame with YDir = axis x refU. + OCC's stored YDirection equals that only for a *direct* (right-handed) gp_Ax3; otherwise + it is negated, so a point at OCC parameter u sits at C++ phi = -u and the range mirrors. + Returns a positive sweep clamped into (0, 2pi]. + """ + sweep = umax - umin + two_pi = 2.0 * math.pi + if sweep <= 0.0: + sweep += two_pi + sweep = min(sweep, two_pi) + phi_start = umin if ax3.Direct() else -umax + return phi_start, sweep + + +def _quadric_trim_wire(face, map_uv, wires=None) -> Tuple[Optional[List[dict]], Optional[str]]: + """Build general line/arc/B-spline trim wires in a quadric face's parametric (phi, v) domain. + + `map_uv(u, v)` is the affine map to the C++ (phi, height/theta) domain; a curved pcurve becomes a + B-spline whose poles it maps exactly. Returns (wires, None) with exactly one outer wire, or + (None, reason). + """ + wires_out: List[dict] = [] + for _wire, is_outer, edges in (_face_wire_edges(face) if wires is None else wires): + parsed = [] # per edge: {"kind": "line", "start": (phi, v)} or {"kind": "bspline", ...} + for edge, _start_vertex in edges: + curve2d, first, last = BRep_Tool.CurveOnSurface(edge, face) + if curve2d is None: + return None, "quadric boundary edge has no 2D pcurve" + reversed_edge = edge.Orientation() == TopAbs_REVERSED + ctype = Geom2dAdaptor_Curve(curve2d).GetType() + if ctype == GeomAbs_Line: + param = last if reversed_edge else first + p = curve2d.Value(param) + parsed.append({"kind": "line", "start": map_uv(p.X(), p.Y())}) + elif ctype in (GeomAbs_Circle, GeomAbs_Ellipse, GeomAbs_BSplineCurve, GeomAbs_BezierCurve): + def to_bspline(lo, hi, c2=curve2d): + trimmed = Geom2d_TrimmedCurve(c2, lo, hi) + return geom2dconvert.CurveToBSplineCurve(trimmed, Convert_TgtThetaOver2) + + params = _bspline_flat_params(first, last, reversed_edge, + lambda p: map_uv(p.X(), p.Y()), to_bspline) + if params is None: + return None, "quadric B-spline pcurve extraction failed" + # Pre-pass: a B-spline pcurve that is exactly a line in (phi, v) is stored as one. + samples = _sample_curve_in_domain(curve2d, first, last, reversed_edge, + lambda p: map_uv(p.X(), p.Y())) + n_poles = int(params[1]) + poles = [(params[2 + 2 * i], params[3 + 2 * i]) for i in range(n_poles)] + kind, canonical = _recognize_canonical_curve(samples, poles) if samples else (None, None) + if kind == "line": + parsed.append({"kind": "line", "start": (canonical[0], canonical[1])}) + elif kind == "arc": + parsed.append({"kind": "arc", "params": canonical, + "start": (canonical[0] + canonical[2] * math.cos(canonical[3]), + canonical[1] + canonical[2] * math.sin(canonical[3]))}) + else: + parsed.append({"kind": "bspline", "params": params, "start": (params[2], params[3])}) + else: + name = CURVE_TYPE_NAME.get(ctype, "unknown") + return None, f"quadric boundary pcurve is a {name} curve (unsupported)" + n = len(parsed) + if n == 0: + return None, "quadric trim wire has no edges" + if all(p["kind"] == "line" for p in parsed) and n < 3: + return None, "quadric line trim wire has fewer than 3 edges" + seg_edges = [] + for i, p in enumerate(parsed): + if p["kind"] == "line": + u0, v0 = p["start"] + u1, v1 = parsed[(i + 1) % n]["start"] + seg_edges.append({"curve": "line", "params": [u0, v0, u1, v1]}) + elif p["kind"] == "arc": + seg_edges.append({"curve": "arc", "params": p["params"]}) + else: + seg_edges.append({"curve": "bspline", "params": p["params"]}) + wires_out.append({"role": "outer" if is_outer else "inner", "edges": seg_edges}) + if not wires_out: + return None, "quadric face has no wires" + n_outer = sum(1 for w in wires_out if w["role"] == "outer") + if n_outer != 1: + return None, f"quadric face has {n_outer} outer trim wires (expected exactly 1)" + return wires_out, None + + +def _quadric_trim_fills_uv_box(face, uv_bounds, wires=None) -> bool: + """True when a quadric face's trim is exactly its parametric-rectangle UV box, so the scalar + parameters describe it: one line-bounded wire whose (u, v) polygon area equals the box area.""" + umin, umax, vmin, vmax = uv_bounds + box_area = abs((umax - umin) * (vmax - vmin)) + if box_area <= _EXTRACT_TOL: + return False + wires = list(_face_wire_edges(face)) if wires is None else wires + if len(wires) != 1: + return False + _wire, _is_outer, edges = wires[0] + points = [] + for edge, _start_vertex in edges: + curve2d, first, last = BRep_Tool.CurveOnSurface(edge, face) + if curve2d is None or Geom2dAdaptor_Curve(curve2d).GetType() != GeomAbs_Line: + return False + param = last if edge.Orientation() == TopAbs_REVERSED else first + p = curve2d.Value(param) + points.append((p.X(), p.Y())) + area = 0.0 + n = len(points) + for i in range(n): + u0, v0 = points[i] + u1, v1 = points[(i + 1) % n] + area += u0 * v1 - u1 * v0 + return abs(0.5 * area - box_area) <= 1e-6 * box_area + + +def extract_cylindrical_face(face, scale_to_cm: float, wires=None) -> Tuple[Optional[dict], Optional[str]]: + """Cylindrical face -> a 'cylinder' surface record; U = azimuth, V = height along the axis.""" + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Cylinder: + return None, "not a cylinder" + umin, umax, vmin, vmax = breptools.UVBounds(face) + cyl = adaptor.Cylinder() + ax3 = cyl.Position() + s = scale_to_cm + center = _xyz(ax3.Location(), s) + axis = _xyz(cyl.Axis().Direction()) + ref_u = _xyz(ax3.XDirection()) + radius = cyl.Radius() * s + height_min, height_max = vmin * s, vmax * s + if height_max - height_min <= _EXTRACT_TOL: + return None, "cylindrical face has a degenerate height range" + phi_start, phi_sweep = _quadric_phi_range(ax3, umin, umax) + inner_wall = face.Orientation() == TopAbs_REVERSED + params = list(center) + list(axis) + list(ref_u) + [radius, height_min, height_max, phi_start, phi_sweep] + record = {"type": "cylinder", "inner_wall": inner_wall, "params": params} + wires = list(_face_wire_edges(face)) if wires is None else wires + if _quadric_trim_fills_uv_box(face, (umin, umax, vmin, vmax), wires): + return record, None # trim is exactly the parametric rectangle: the scalar params suffice + phi_of_u = (lambda u: u) if ax3.Direct() else (lambda u: -u) + # affine (u, v) -> (phi[rad], h[cm]); OCC V is the height along the axis + trim, reason = _quadric_trim_wire(face, lambda u, v: (phi_of_u(u), v * s), wires) + if trim is None: + return None, reason + record["wires"] = trim + return record, None + + +def extract_conical_face(face, scale_to_cm: float, wires=None) -> Tuple[Optional[dict], Optional[str]]: + """Conical face -> a 'cone' surface record; r(v) = RefRadius + v sin(a), h(v) = v cos(a).""" + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Cone: + return None, "not a cone" + umin, umax, vmin, vmax = breptools.UVBounds(face) + cone = adaptor.Cone() + ax3 = cone.Position() + s = scale_to_cm + half = cone.SemiAngle() + ref_radius = cone.RefRadius() + cos_a, sin_a = math.cos(half), math.sin(half) + h_lo, r_lo = vmin * cos_a, ref_radius + vmin * sin_a + h_hi, r_hi = vmax * cos_a, ref_radius + vmax * sin_a + if h_lo > h_hi: + h_lo, h_hi, r_lo, r_hi = h_hi, h_lo, r_hi, r_lo + if r_lo < -_EXTRACT_TOL or r_hi < -_EXTRACT_TOL: + return None, "conical trim produces a negative radius" + r_lo, r_hi = max(0.0, r_lo), max(0.0, r_hi) + if max(r_lo, r_hi) <= _EXTRACT_TOL: + return None, "conical face has degenerate radii" + if (h_hi - h_lo) * s <= _EXTRACT_TOL: + return None, "conical face has a degenerate height range" + center = _xyz(ax3.Location(), s) + axis = _xyz(cone.Axis().Direction()) + ref_u = _xyz(ax3.XDirection()) + phi_start, phi_sweep = _quadric_phi_range(ax3, umin, umax) + inner_wall = face.Orientation() == TopAbs_REVERSED + params = (list(center) + list(axis) + list(ref_u) + + [r_lo * s, r_hi * s, h_lo * s, h_hi * s, phi_start, phi_sweep]) + record = {"type": "cone", "inner_wall": inner_wall, "params": params} + wires = list(_face_wire_edges(face)) if wires is None else wires + if _quadric_trim_fills_uv_box(face, (umin, umax, vmin, vmax), wires): + return record, None # trim is exactly the parametric rectangle: the scalar params suffice + # OCC V (ruling-line distance) maps to the C++ axial height h = v cos(alpha), in cm + phi_of_u = (lambda u: u) if ax3.Direct() else (lambda u: -u) + trim, reason = _quadric_trim_wire(face, lambda u, v: (phi_of_u(u), v * cos_a * s), wires) + if trim is None: + return None, reason + record["wires"] = trim + return record, None + + +def extract_spherical_face(face, scale_to_cm: float, wires=None) -> Tuple[Optional[dict], Optional[str]]: + """Spherical face -> a 'sphere' surface record; the C++ polar angle is theta = pi/2 - v.""" + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Sphere: + return None, "not a sphere" + umin, umax, vmin, vmax = breptools.UVBounds(face) + sph = adaptor.Sphere() + ax3 = sph.Position() + s = scale_to_cm + center = _xyz(ax3.Location(), s) + polar_axis = _xyz(ax3.Direction()) + ref_u = _xyz(ax3.XDirection()) + radius = sph.Radius() * s + theta_min = 0.5 * math.pi - vmax + theta_max = 0.5 * math.pi - vmin + if theta_max - theta_min <= _EXTRACT_TOL: + return None, "spherical face has a degenerate polar range" + phi_start, phi_sweep = _quadric_phi_range(ax3, umin, umax) + params = list(center) + list(polar_axis) + list(ref_u) + [radius, theta_min, theta_max, phi_start, phi_sweep] + inner_wall = face.Orientation() == TopAbs_REVERSED + record = {"type": "sphere", "inner_wall": inner_wall, "params": params} + wires = list(_face_wire_edges(face)) if wires is None else wires + if _quadric_trim_fills_uv_box(face, (umin, umax, vmin, vmax), wires): + return record, None # trim is exactly the parametric rectangle: the scalar params suffice + # OCC V (latitude) maps to the C++ polar angle theta = pi/2 - v (rad) + phi_of_u = (lambda u: u) if ax3.Direct() else (lambda u: -u) + trim, reason = _quadric_trim_wire(face, lambda u, v: (phi_of_u(u), 0.5 * math.pi - v), wires) + if trim is None: + return None, reason + record["wires"] = trim + return record, None + + +def extract_toroidal_face(face, scale_to_cm: float, wires=None) -> Tuple[Optional[dict], Optional[str]]: + """Toroidal face -> a 'torus' surface record. + + U is the ring phi (mirrored for a left-handed ax3) and V the tube phi. + """ + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Torus: + return None, "not a torus" + umin, umax, vmin, vmax = breptools.UVBounds(face) + tor = adaptor.Torus() + ax3 = tor.Position() + s = scale_to_cm + major_radius = tor.MajorRadius() * s + minor_radius = tor.MinorRadius() * s + if minor_radius <= _EXTRACT_TOL or major_radius <= _EXTRACT_TOL: + return None, "toroidal face has degenerate radii" + center = _xyz(ax3.Location(), s) + axis = _xyz(ax3.Direction()) + ref_u = _xyz(ax3.XDirection()) + phi_start, phi_sweep = _quadric_phi_range(ax3, umin, umax) + two_pi = 2.0 * math.pi + tube_sweep = vmax - vmin + if tube_sweep <= 0.0: + tube_sweep += two_pi + tube_sweep = min(tube_sweep, two_pi) + tube_start = vmin + inner_wall = face.Orientation() == TopAbs_REVERSED + params = (list(center) + list(axis) + list(ref_u) + + [major_radius, minor_radius, phi_start, phi_sweep, tube_start, tube_sweep]) + record = {"type": "torus", "inner_wall": inner_wall, "params": params} + wires = list(_face_wire_edges(face)) if wires is None else wires + if _quadric_trim_fills_uv_box(face, (umin, umax, vmin, vmax), wires): + return record, None # trim is exactly the parametric rectangle: the scalar params suffice + # affine (u, v) -> (phiRing[rad], phiTube[rad]); OCC V is the tube angle, unchanged by the frame + phi_of_u = (lambda u: u) if ax3.Direct() else (lambda u: -u) + trim, reason = _quadric_trim_wire(face, lambda u, v: (phi_of_u(u), v), wires) + if trim is None: + return None, reason + record["wires"] = trim + return record, None + + +# ------------------------------- +# Canonical-form recognition: recover the exact analytic model behind a stored NURBS +# ------------------------------- +# Model selection, not fitting: only a machine-precision fit is accepted, so an almost-cylinder +# stays free-form. Used for faces whose stored type has no direct extractor. + +_RECOGNIZE_TOL_EXACT = 1.e-9 + + +def _recognize_analytic_surface(adaptor, uv_bounds) -> Optional[dict]: + """The exact plane/sphere/cylinder/cone behind a face, or None; lengths in native CAD units. + + Proposals are scored by their measured gap over the sample diagonal only; the fewest-parameter + model below _RECOGNIZE_TOL_EXACT wins.""" + umin, umax, vmin, vmax = uv_bounds + P, N = _sample_surface_for_recognition(adaptor, umin, umax, vmin, vmax) + if P is None: + return None + scale = float(np.linalg.norm(P.max(axis=0) - P.min(axis=0))) + if scale < 1e-12: + return None + + def score(kind, model): + """The one criterion: the achieved gap, relative to the patch's own size.""" + try: + gap = _analytic_surface_gap(kind, model, P) + except (ValueError, FloatingPointError): + return float("inf") + return gap / scale if math.isfinite(gap) else float("inf") + + best = ("freeform", float("inf"), {}) + for kind, model in _analytic_surface_proposals(P, N): + res = score(kind, model) + if kind == "plane": + if res < _RECOGNIZE_TOL_EXACT: + # Parsimony: an exact plane wins outright. + out = {"kind": "plane", "residual": res, "P": P, "N": N} + out.update(model) + out["gap"] = res * scale + out["gap_relative"] = res + return out + continue + if res < best[1]: + best = (kind, res, model) + + kind, res, extra = best + if res >= _RECOGNIZE_TOL_EXACT: + return None + out = {"kind": kind, "residual": res, "P": P, "N": N} + out.update(extra) + out["gap"] = res * scale + out["gap_relative"] = res + return out + + +# ------------------------------- +# Self-test for the recognition path (`--self-test`) +# ------------------------------- +# Every positive control has a negative one; all are built in-process from OCC primitives. + +class _Checks: + """A self-test block's counters, and its one printed line per check.""" + + def __init__(self): + self.checks = 0 + self.failures = 0 + + def report(self, ok: bool, label: str, detail: str = ""): + self.checks += 1 + if not ok: + self.failures += 1 + print(f" [{'ok ' if ok else 'FAIL'}] {label}{(' -- ' + detail) if detail else ''}") + + +def _self_test_faces_of(shape) -> List[object]: + out = [] + explorer = TopExp_Explorer(shape, TopAbs_FACE) + while explorer.More(): + out.append(topods.Face(explorer.Current())) + explorer.Next() + return out + + +def run_recognition_self_test() -> int: + """Assert the canonical-form recognizer against models whose answer is known in closed form. + + Returns the number of failures; prints one line per check. + """ + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_NurbsConvert, BRepBuilderAPI_MakeFace + from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeCone, BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus) + from OCC.Core.gp import (gp_Ax2, gp_Ax3, gp_Cone, gp_Cylinder, gp_Dir, gp_Pln, gp_Sphere) + + tally = _Checks() + report = tally.report + accepted_gaps = [] + + def recognize(face): + adaptor = BRepAdaptor_Surface(face) + try: + uv_bounds = breptools.UVBounds(face) + except Exception: + return None + rec = _recognize_analytic_surface(adaptor, uv_bounds) + if rec is not None: + accepted_gaps.append((rec["kind"], rec["gap_relative"])) + return rec + + def nurbs(shape): + return BRepBuilderAPI_NurbsConvert(shape, True).Shape() + + def expect(face, want: Optional[str], label: str): + rec = recognize(face) + got = rec["kind"] if rec else None + detail = (f"got {got}" if rec is None or want is None else + f"got {got}, gap {rec['gap_relative']:.2e} of the patch diagonal") + if rec is not None and want is not None and got == want: + detail = f"gap {rec['gap_relative']:.2e} of the patch diagonal" + report(got == want, label, detail) + return rec + + print("Canonical-form recognition self-test") + print(" positive controls: a quadric written as NURBS must be recovered") + # BRepBuilderAPI_NurbsConvert turns each analytic face into the rational B-spline a CAD + # exporter would have written -- the exporter artefact this whole path exists for, built here. + frame = gp_Ax3(gp_Pnt(1.0, -2.0, 3.0), gp_Dir(0.3, 0.4, 0.866), gp_Dir(0.866, 0.0, -0.3)) + for label, surface, want in ( + ("cylinder", gp_Cylinder(frame, 5.0), "cylinder"), + ("cone", gp_Cone(frame, 0.4, 2.0), "cone"), + ("sphere", gp_Sphere(frame, 7.0), "sphere"), + ("plane", gp_Pln(frame), "plane")): + if label == "plane": + native = BRepBuilderAPI_MakeFace(surface, -5.0, 5.0, -3.0, 3.0).Shape() + elif label == "sphere": + native = BRepBuilderAPI_MakeFace(surface, 0.2, 2.4, -0.9, 0.9).Shape() + else: + native = BRepBuilderAPI_MakeFace(surface, 0.2, 2.4, 1.0, 9.0).Shape() + faces = _self_test_faces_of(nurbs(native)) + report(len(faces) == 1, f"NURBS-converted {label} patch is one face", f"{len(faces)} found") + if faces: + expect(faces[0], want, f"NURBS-encoded {label} is recognized as a {want}") + + print(" negative controls: a genuinely free-form surface must be declined") + expect(_self_test_bezier_patch( + lambda s, t: (10 * s - 5, 10 * t - 5, (10 * s - 5) * (10 * t - 5) / 10.0), 6, 6), + None, "free-form saddle is not recognized as any quadric") + expect(_self_test_bezier_patch( + lambda s, t: (20 * s - 10, 0.5 * t, 0.02 * (20 * s - 10) ** 2 + 0.3 * (20 * s - 10) * t), 6, 6), + None, "narrow free-form ridge is not recognized as any quadric") + for face in _self_test_faces_of( + nurbs(BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 10.0, 1.0).Shape())): + expect(face, None, "NURBS-encoded torus is declined (no torus model -- known limitation)") + + print(" the ALICE3 cone over-acceptance: a swept non-circular profile") + for bulge in (1.0e-3, 1.0e-2): + for taper in (1.0e-4, 1.0e-6, 1.0e-8): + expect(_self_test_tapered_near_circle(bulge, taper), None, + f"swept non-circular profile (bulge {bulge:.0e}, taper {taper:.0e}) is declined") + + print(" the invariant: every accepted recognition is within the declared tolerance") + worst = max(accepted_gaps, key=lambda kv: kv[1], default=("-", 0.0)) + report(all(gap < _RECOGNIZE_TOL_EXACT for _kind, gap in accepted_gaps), + "every accepted face's MEASURED gap is below the acceptance tolerance", + f"worst {worst[0]} at {worst[1]:.2e} against {_RECOGNIZE_TOL_EXACT:.0e}") + + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + + +def run_placement_self_test() -> int: + """Assert the placed-primitive emission and the COMPOSITION ORDER in `geom.C`. + + Points are classified by navigating the assembly and compared with OCCT in the part frame; + three negative controls (transposed rotation, reversed product, dropped placement) must move + the count. Returns the number of failures; needs PyROOT and pythonOCC. + """ + tally = _Checks() + report = tally.report + + print("\nPlaced-primitive emission and geom.C composition order") + try: + import ROOT + except Exception as exc: # noqa: BLE001 + print(f" [FAIL] PyROOT is not importable in this interpreter ({exc}); the placement " + "checks cannot run. Use the O2 environment.") + print("\n1 checks, 1 failure(s)") + return 1 + ROOT.gROOT.SetBatch(True) + + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier + from OCC.Core.BRepGProp import brepgprop + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder + from OCC.Core.GProp import GProp_GProps + from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON + from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf, gp_Vec + + from cadsupport import emit as csg_emit, primitives as prim # noqa: E402 + + # --- the specimen: a tube SEGMENT, rotated and translated off every coordinate axis -------- + axis = gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)) + wedge = BRepPrimAPI_MakeCylinder(axis, 2.0, 10.0, math.radians(75.0)).Shape() + bore = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1)), 1.0, 12.0).Shape() + seg = BRepAlgoAPI_Cut(wedge, bore).Shape() + spin = gp_Trsf() + spin.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 2, 3)), 0.9) + shift = gp_Trsf() + shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + placed = BRepBuilderAPI_Transform(seg, shift.Multiplied(spin), True).Shape() + + record = csg_emit.process_solid(placed, "selftest-placed-tubeseg") + if not record["accepted"]: + report(False, "a rotated, translated tube segment is recognised and accepted", + f"{record['reason']}") + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + report(True, "a rotated, translated tube segment is recognised and accepted", + f"{record['recogniser']}: {record['description']}") + + shape, placement = prim.build_root(record["candidate"], "selftest") + report(shape.ClassName() == "TGeoTubeSeg" and placement is not None, + "it emits a TGeoTubeSeg with a placement, not a TGeoCompositeShape", + f"{shape.ClassName()}, placement {'present' if placement else 'absent'}") + + # --- the win: an analytic Capacity() again, checked against OCCT's own volume ------------- + props = GProp_GProps() + brepgprop.VolumeProperties(placed, props) + occ_volume = props.Mass() + rel = abs(shape.Capacity() - occ_volume) / occ_volume + report(rel < 1.0e-12, "its Capacity() is analytic and agrees with the OCCT volume", + f"ROOT {shape.Capacity():.12g} vs OCCT {occ_volume:.12g}, rel {rel:.2e}") + # ... and the same comparison must fail on a shape that is 1% too fat. + fat = dict(record["candidate"]["leaves"][0]["params"]) + fat["rmax"] *= 1.01 + fat_cand = prim.candidate("primitive", [prim.leaf( + "TGeoTubeSeg", fat, record["candidate"]["leaves"][0]["frame"])], "selftest-negative") + fat_shape, _ = prim.build_root(fat_cand, "selftest_fat") + rel_fat = abs(fat_shape.Capacity() - occ_volume) / occ_volume + report(rel_fat > 1.0e-3, "the same capacity comparison does reject a 1% wrong radius", + f"rel {rel_fat:.2e}") + + # --- the composition order, decided by navigation ------------------------------------------ + # A deliberately non-symmetric part placement, as emit_placement_cpp writes for an AddNode. + part_rot = ROOT.TGeoRotation("selftest_partrot", 37.0, 24.0, 61.0) + ROOT.SetOwnership(part_rot, False) + part_placement = ROOT.TGeoCombiTrans(-2.0, 7.0, 1.5, part_rot) + ROOT.SetOwnership(part_placement, False) + shape_placement = prim.root_placement_matrix(placement, "selftest_shapeplace") + + def compose(order): + """The node matrix under a given composition rule.""" + if order == "part*shape": + m = ROOT.TGeoHMatrix(part_placement) + m.Multiply(shape_placement) + elif order == "shape*part": + m = ROOT.TGeoHMatrix(shape_placement) + m.Multiply(part_placement) + elif order == "part*shapeT": + t = [[placement[r][c] for r in range(3)] + [placement[c][3]] for c in range(3)] + m = ROOT.TGeoHMatrix(part_placement) + m.Multiply(prim.root_placement_matrix(t, "selftest_shapeplaceT")) + else: # the placement dropped on the floor -- the bug this test is really for + m = ROOT.TGeoHMatrix(part_placement) + return m + + # Probes in the assembly frame, with OCCT's verdict after undoing the part placement only; + # drawn over the padded part box, so about a third are inside. + classifier = BRepClass3d_SolidClassifier(placed) + tolerance = max(csg_emit.model_tolerance_cm(placed), 1.0e-9) + bnd = Bnd_Box() + brepbndlib.Add(placed, bnd) + bnd.SetGap(0.0) + bxmin, bymin, bzmin, bxmax, bymax, bzmax = bnd.Get() + bpad = 0.1 * max(bxmax - bxmin, bymax - bymin, bzmax - bzmin) + rng = random.Random(4242) + probes = [] + master = array("d", [0.0, 0.0, 0.0]) + for _ in range(3000): + part_point = (rng.uniform(bxmin - bpad, bxmax + bpad), + rng.uniform(bymin - bpad, bymax + bpad), + rng.uniform(bzmin - bpad, bzmax + bpad)) + classifier.Perform(gp_Pnt(*part_point), tolerance) + state = classifier.State() + if state == TopAbs_ON: + continue + part_placement.LocalToMaster(array("d", list(part_point)), master) + probes.append(((master[0], master[1], master[2]), state == TopAbs_IN)) + n_inside = sum(1 for _p, inside in probes if inside) + print(f" ({len(probes)} probes, {n_inside} of them inside the CAD body)") + + def _keep(obj): + """Everything below is registered with the TGeoManager, which frees it. Handing ownership + to Python as well is a double free -- the same rule cadsupport/primitives.py follows.""" + ROOT.SetOwnership(obj, False) + return obj + + _keep(shape) + _keep(fat_shape) + + def disagreements(order): + # A fresh manager per variant. Constructing one DELETES the previous geometry, which is + # why nothing created here may be owned by Python as well. + manager = _keep(ROOT.TGeoManager(f"selftest_{order}", "placement composition self-test")) + vacuum = _keep(ROOT.TGeoMaterial("Vacuum", 0., 0., 0.)) + medium = _keep(ROOT.TGeoMedium("Vacuum", 1, vacuum)) + world = _keep(ROOT.TGeoVolume("TOP", _keep(ROOT.TGeoBBox("selftestWorld", 40., 40., 40.)), + medium)) + # A fresh copy of the shape per manager, for the same reason. + local_shape, _ = prim.build_root(record["candidate"], f"selftest_{order}_shape") + part = _keep(ROOT.TGeoVolume("PART", _keep(local_shape), medium)) + world.AddNode(part, 1, _keep(compose(order))) + manager.SetTopVolume(world) + manager.CloseGeometry() + bad = 0 + for p, want in probes: + node = manager.FindNode(p[0], p[1], p[2]) + inside = node is not None and node.GetVolume().GetName() == "PART" + if inside != want: + bad += 1 + return bad, len(probes) + + bad_ok, scored = disagreements("part*shape") + report(bad_ok == 0 and scored > 500, + "geom.C's node matrix partPlacement * shapePlacement puts the solid where the CAD " + "body is", f"{bad_ok} disagreement(s) over {scored} navigated points") + for order, label in (("shape*part", "the reversed product"), + ("part*shapeT", "a transposed shape rotation"), + ("part-only", "dropping the shape placement")): + bad, _n = disagreements(order) + report(bad > 0, f"{label} does move the count", f"{bad} disagreement(s)") + + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + + +# ------------------------------- +# Self-test for the planar trim vocabulary (`--self-test`, third block) +# ------------------------------- +# An oblique plane cuts a cylinder on an ellipse, stored exactly as a rational B-spline; the +# deviation is measured both ways, and the instrument must be able to report a large one. + +def _self_test_oblique_cut_cylinder(radius: float = 1.2, height: float = 5.0, + tilt_deg: float = 60.0, lift: float = 2.5): + """The `oblique_cut_cyl` ladder fixture, built in-process: a cylinder cut by a plane inclined + to its axis. Returns the solid. Everything is already in cm (scale_to_cm = 1).""" + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCylinder + from OCC.Core.gp import gp_Ax1, gp_Dir, gp_Trsf, gp_Vec + + cyl = BRepPrimAPI_MakeCylinder(radius, height).Shape() + knife = BRepPrimAPI_MakeBox(gp_Pnt(-20.0, -20.0, 0.0), 40.0, 40.0, 40.0).Shape() + rot = gp_Trsf() + rot.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), math.radians(tilt_deg)) + move = gp_Trsf() + move.SetTranslation(gp_Vec(0.0, 0.0, lift)) + knife = BRepBuilderAPI_Transform(knife, move * rot, True).Shape() + return BRepAlgoAPI_Cut(cyl, knife).Shape() + + +def _self_test_conic_bounded_plane(conic, t0: float, t1: float): + """A planar face bounded by one conic arc from `t0` to `t1` plus the chord closing it.""" + from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakeWire) + arc = BRepBuilderAPI_MakeEdge(conic, t0, t1).Edge() + chord = BRepBuilderAPI_MakeEdge(conic.Value(t1), conic.Value(t0)).Edge() + wire = BRepBuilderAPI_MakeWire(arc, chord).Wire() + return BRepBuilderAPI_MakeFace(wire, True).Face() + + +def _self_test_rebuild_2d_curve(seg): + """Rebuild a sidecar wire segment as an OCC `Geom2d_Curve`, so the curve the *sidecar* carries + can be measured against the CAD edge instead of being argued about.""" + from OCC.Core.Geom2d import Geom2d_BSplineCurve + from OCC.Core.gp import gp_Pnt2d + from OCC.Core.TColgp import TColgp_Array1OfPnt2d + from OCC.Core.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger + + if seg["curve"] != "bspline": + return None + p = seg["params"] + degree, n_poles = int(p[0]), int(p[1]) + poles = TColgp_Array1OfPnt2d(1, n_poles) + for i in range(n_poles): + poles.SetValue(i + 1, gp_Pnt2d(p[2 + 2 * i], p[3 + 2 * i])) + weights = TColStd_Array1OfReal(1, n_poles) + for i in range(n_poles): + weights.SetValue(i + 1, p[2 + 2 * n_poles + i]) + flat = p[2 + 3 * n_poles:] + distinct = [] + for k in flat: + if not distinct or abs(k - distinct[-1][0]) > 1e-12: + distinct.append([k, 1]) + else: + distinct[-1][1] += 1 + knots = TColStd_Array1OfReal(1, len(distinct)) + mults = TColStd_Array1OfInteger(1, len(distinct)) + for i, (k, m) in enumerate(distinct): + knots.SetValue(i + 1, k) + mults.SetValue(i + 1, m) + return Geom2d_BSplineCurve(poles, weights, knots, mults, degree) + + +def _self_test_trim_deviation(face, record, scale_to_cm: float = 1.0, n: int = 257): + """Largest distance, in cm, between the CAD and the stored boundary curves, measured both ways. + + Returns (max_deviation_cm, patch_diagonal_cm) or (None, None) if a segment cannot be rebuilt.""" + from OCC.Core.Geom2dAPI import Geom2dAPI_ProjectPointOnCurve + from OCC.Core.GeomAPI import GeomAPI_ProjectPointOnCurve + from OCC.Core.gp import gp_Pnt2d + + origin_cm = record["params"][0:3] + axis_u = record["params"][3:6] + axis_v = record["params"][6:9] + project = _planar_projector(origin_cm, axis_u, axis_v, scale_to_cm) + + def unproject(u, v): + return gp_Pnt(*[origin_cm[i] + u * axis_u[i] + v * axis_v[i] for i in range(3)]) + + def distance_to(proj, endpoints, point): + """Distance from `point` to a curve, falling back to the endpoints (an upper bound).""" + best = min(point.Distance(e) for e in endpoints) + if proj.NbPoints() > 0: + best = min(best, proj.LowerDistance()) + return best + + segs = [s for w in record["wires"] for s in w["edges"]] + edges = [e for _w, _o, es in _face_wire_edges(face) for e, _v in es] + if len(segs) != len(edges): + return None, None + worst = 0.0 + points = [] + for seg, edge in zip(segs, edges): + curve3d, first, last = BRep_Tool.Curve(edge) + if curve3d is None: + return None, None + lo, hi = (first, last) if first <= last else (last, first) + cad = [curve3d.Value(float(t)) for t in np.linspace(lo, hi, n)] + points.extend([(p.X() * scale_to_cm, p.Y() * scale_to_cm, p.Z() * scale_to_cm) for p in cad]) + if seg["curve"] == "line": + u0, v0, u1, v1 = seg["params"] + for p in cad: + u, v = project(p) + du, dv = u - u0, v - v0 + lu, lv = u1 - u0, v1 - v0 + l2 = lu * lu + lv * lv + t = 0.0 if l2 <= 0.0 else min(1.0, max(0.0, (du * lu + dv * lv) / l2)) + worst = max(worst, math.hypot(du - t * lu, dv - t * lv)) + stored = [unproject(u0 + (u1 - u0) * t, v0 + (v1 - v0) * t) + for t in np.linspace(0.0, 1.0, n)] + elif seg["curve"] == "arc": + cu, cv, r, a0, sweep = seg["params"] + stored = [unproject(cu + r * math.cos(a0 + sweep * t), cv + r * math.sin(a0 + sweep * t)) + for t in np.linspace(0.0, 1.0, n)] + else: + curve2d = _self_test_rebuild_2d_curve(seg) + if curve2d is None: + return None, None + t0, t1 = curve2d.FirstParameter(), curve2d.LastParameter() + stored = [] + for t in np.linspace(t0, t1, n): + q = curve2d.Value(float(t)) + stored.append(unproject(q.X(), q.Y())) + ends2d = [curve2d.Value(t0), curve2d.Value(t1)] + for p in cad: + u, v = project(p) + here = gp_Pnt2d(u, v) + worst = max(worst, distance_to(Geom2dAPI_ProjectPointOnCurve(here, curve2d), + ends2d, here)) + # the reverse direction: every stored sample back onto the CAD 3D curve + ends3d = [curve3d.Value(float(lo)), curve3d.Value(float(hi))] + for q in stored: + here = gp_Pnt(q.X() / scale_to_cm, q.Y() / scale_to_cm, q.Z() / scale_to_cm) + worst = max(worst, scale_to_cm * + distance_to(GeomAPI_ProjectPointOnCurve(here, curve3d), ends3d, here)) + arr = np.asarray(points) + diagonal = float(np.linalg.norm(arr.max(axis=0) - arr.min(axis=0))) if len(arr) else 0.0 + return worst, diagonal + + +def run_planar_trim_self_test() -> int: + """Assert the planar face's trim-curve vocabulary: an ellipse boundary is carried EXACTLY, and + a boundary that is not a conic we can write exactly is still declined.""" + from OCC.Core.Geom import Geom_Ellipse, Geom_Hyperbola, Geom_Parabola + from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Elips, gp_Hypr, gp_Parab + + tally = _Checks() + report = tally.report + + print("\nPlanar trim vocabulary: the ellipse boundary") + + solid = _self_test_oblique_cut_cylinder() + cut_face = None + for face in _self_test_faces_of(solid): + adaptor = BRepAdaptor_Surface(face) + if adaptor.GetType() != GeomAbs_Plane: + continue + kinds = set() + for _w, _o, es in _face_wire_edges(face): + for e, _v in es: + kinds.add(CURVE_TYPE_NAME.get(BRepAdaptor_Curve(e).GetType(), "unknown")) + if "ellipse" in kinds: + cut_face = face + report(cut_face is not None, + "the oblique cut of a cylinder really does produce an ellipse-bounded planar face", + "found" if cut_face is not None else "no ellipse boundary edge -- fixture is wrong") + + if cut_face is not None: + record, reason = extract_planar_face(cut_face, 1.0) + report(record is not None, "an oblique planar cut of a cylinder is accepted", + "accepted" if record else f"declined: {reason}") + if record is not None: + segs = [s for w in record["wires"] for s in w["edges"]] + kinds = sorted({s["curve"] for s in segs}) + report(kinds == ["bspline"], + "the ellipse is stored as a B-spline segment", f"segments: {kinds}") + spreads = [] + for s in segs: + if s["curve"] != "bspline": + continue + n_poles = int(s["params"][1]) + w = s["params"][2 + 2 * n_poles: 2 + 3 * n_poles] + spreads.append(max(w) - min(w)) + rational = any(spread > 1e-12 for spread in spreads) + report(rational, + "it is a RATIONAL B-spline -- the exact conic form, not a polynomial fit", + f"weight spread {max(spreads):.3f}" if spreads else "no bspline segment") + dev, diag = _self_test_trim_deviation(cut_face, record) + report(dev is not None and dev < 1.0e-9, + "the stored trim reproduces the CAD boundary at machine precision", + f"max deviation {dev:.2e} cm = {dev / diag:.2e} patch diagonals" + if dev is not None else "could not be measured") + + # A partial ellipse arc: the ExcavatorArm/Bucket shape of the problem, not the fixture's closed one. + frame = gp_Ax2(gp_Pnt(0.3, -0.2, 1.1), gp_Dir(0.3, 0.4, 0.866), gp_Dir(0.866, 0.0, -0.3)) + ell_face = _self_test_conic_bounded_plane(Geom_Ellipse(gp_Elips(frame, 2.4, 1.2)), 0.35, 2.6) + record, reason = extract_planar_face(ell_face, 1.0) + report(record is not None, "an ellipse ARC boundary (the Bucket case) is accepted", + "accepted" if record else f"declined: {reason}") + if record is not None: + dev, diag = _self_test_trim_deviation(ell_face, record) + report(dev is not None and dev < 1.0e-9, + "the ellipse arc's stored trim reproduces the CAD boundary at machine precision", + f"max deviation {dev:.2e} cm = {dev / diag:.2e} patch diagonals" + if dev is not None else "could not be measured") + + print(" the deviation instrument must be able to return a large number") + if record is not None: + # A circular arc with the ellipse's endpoints and centre: the instrument must see it. + import copy + wrong = copy.deepcopy(record) + for w in wrong["wires"]: + for s in w["edges"]: + if s["curve"] == "bspline": + n_poles = int(s["params"][1]) + for i in range(n_poles): + s["params"][2 + 2 * i] *= 0.5 # squash the major axis: a different conic + bad_dev, bad_diag = _self_test_trim_deviation(ell_face, wrong) + report(bad_dev is not None and bad_dev > 1.0e-3, + "a deliberately wrong conic is caught by the same measurement", + f"max deviation {bad_dev:.2e} cm = {bad_dev / bad_diag:.2e} patch diagonals" + if bad_dev is not None else "could not be measured") + + print(" negative controls: a boundary that is not an exactly-writable conic is still declined") + hyp_face = _self_test_conic_bounded_plane(Geom_Hyperbola(gp_Hypr(frame, 2.0, 1.0)), 0.2, 0.9) + record, reason = extract_planar_face(hyp_face, 1.0) + report(record is None and reason is not None and "hyperbola" in reason, + "a hyperbola boundary edge is declined", reason if record is None else "ACCEPTED") + par_face = _self_test_conic_bounded_plane(Geom_Parabola(gp_Parab(frame, 1.5)), -1.4, 1.4) + record, reason = extract_planar_face(par_face, 1.0) + report(record is None and reason is not None and "parabola" in reason, + "a parabola boundary edge is declined", reason if record is None else "ACCEPTED") + + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + + +# ------------------------------- +# Self-test for coincident placements (`--self-test`, fourth block) +# ------------------------------- + +def _self_test_shape_tool(): + """A fresh, empty in-memory XCAF document and its shape tool, for pathological fixtures.""" + doc = TDocStd_Document("selftest-placements") + return doc, XCAFDoc_DocumentTool.ShapeTool(doc.Main()) + + +def _self_test_shift(dx: float = 0.0, dy: float = 0.0, dz: float = 0.0) -> gp_Trsf: + trsf = gp_Trsf() + if (dx, dy, dz) != (0.0, 0.0, 0.0): + trsf.SetTranslation(gp_Vec(dx, dy, dz)) + return trsf + + +def _self_test_leaf(shape_tool, side: float): + return shape_tool.AddShape(BRepPrimAPI_MakeBox(side, side, side).Shape(), False) + + +def _self_test_assembly(shape_tool, components): + """`components` is a sequence of (child label, gp_Trsf).""" + label = shape_tool.NewShape() + for child, trsf in components: + shape_tool.AddComponent(label, child, TopLoc_Location(trsf)) + return label + + +def _self_test_convert(shape_tool): + """Run the production traversal over an in-memory assembly and report what it placed. + + Returns (report, leaf occurrences), where the occurrences are (definition, world transform + signature) pairs -- measured by walking the emitted graph, not read back out of the rule. + """ + reset_graph() + report = expand_free_shapes(shape_tool, meshparam=None, scale_to_cm=1.0) + leaves = [occ for occ in enumerate_occurrences(placements, top_defs) + if occ[0] in logical_volumes] + return report, leaves + + +def run_duplicate_placement_self_test() -> int: + """Assert that one definition at one world transform is placed exactly ONCE, and that one + definition at two different world transforms is still placed twice (the negative control). + + Returns the number of failures; prints one line per check. + """ + tally = _Checks() + report = tally.report + + print("\nCoincident placements: one definition, one world transform, one placement") + + # --- 1. the ALICE3 shape: a root whose FIRST child contains its own siblings --------------- + _doc, st = _self_test_shape_tool() + leaves = [_self_test_leaf(st, 1.0 + i) for i in range(3)] + subs = [_self_test_assembly(st, [(leaf, _self_test_shift(dx=10.0 * i))]) + for i, leaf in enumerate(leaves)] + detector = _self_test_assembly(st, [(sub, gp_Trsf()) for sub in subs]) + # The root lists the detector AND, at the identity beside it, the detector's own three + # children -- entity for entity what CAD_noETA.stp's root does. + _self_test_assembly(st, [(detector, gp_Trsf())] + [(sub, gp_Trsf()) for sub in subs]) + st.UpdateAssemblies() + rep, occ = _self_test_convert(st) + + report(rep["declared_leaf_placements"] == 6 and rep["declared_multiplicity"] == {2: 3}, + "the fixture really does declare the ALICE3 defect: 3 solids, each declared twice at " + "the same place", + f"{rep['declared_leaf_placements']} declared, multiplicity " + f"{rep['declared_multiplicity']}") + report(len(occ) == 3, "it converts to 3 leaf placements, not 6", f"{len(occ)} placed") + report(len(set(occ)) == 3 and len(occ) == len(set(occ)), + "and no two of them share a definition and a world transform", + f"{len(set(occ))} distinct (definition, world transform) pair(s)") + report(rep["n_suppressed_by_rule"]["root-containment"] == 3, + "the root-containment rule is what fires, and it drops exactly the 3 root edges", + f"{rep['n_suppressed_by_rule']}") + + # --- 2. THE negative control: legitimate instancing at two DIFFERENT transforms ------------ + # A rule keyed on the definition alone would fail here. + _doc, st = _self_test_shape_tool() + leaf = _self_test_leaf(st, 2.0) + module = _self_test_assembly(st, [(leaf, gp_Trsf())]) + _self_test_assembly(st, [(module, _self_test_shift(dx=0.0)), + (module, _self_test_shift(dx=100.0))]) + st.UpdateAssemblies() + rep, occ = _self_test_convert(st) + report(len(occ) == 2, "one sub-assembly instanced twice at DIFFERENT transforms still gets " + "two placements", f"{len(occ)} placed") + report(len(set(sig for _lid, sig in occ)) == 2, + "and they are at two different world transforms, as the CAD says", + f"{len(set(sig for _lid, sig in occ))} distinct world transform(s)") + report(sum(rep["n_suppressed_by_rule"].values()) == 0, + "nothing is suppressed there", f"{rep['n_suppressed_by_rule']}") + + # ... and the same model with a THIRD, coincident instance bolted on must lose exactly one. + _doc, st = _self_test_shape_tool() + leaf = _self_test_leaf(st, 2.0) + module = _self_test_assembly(st, [(leaf, gp_Trsf())]) + _self_test_assembly(st, [(module, _self_test_shift(dx=0.0)), + (module, _self_test_shift(dx=100.0)), + (module, _self_test_shift(dx=100.0))]) + st.UpdateAssemblies() + rep, occ = _self_test_convert(st) + report(len(occ) == 2 and len(set(occ)) == 2 and rep["declared_leaf_placements"] == 3, + "a third instance that coincides with the second is the one that goes", + f"{rep['declared_leaf_placements']} declared -> {len(occ)} placed at " + f"{len(set(occ))} distinct transform(s)") + + # --- 3. the same definition at the same transform down two different assembly paths -------- + _doc, st = _self_test_shape_tool() + shared_leaf = _self_test_leaf(st, 3.0) + own_left, own_right = _self_test_leaf(st, 4.0), _self_test_leaf(st, 5.0) + shared = _self_test_assembly(st, [(shared_leaf, gp_Trsf())]) + at = _self_test_shift(dz=7.0) + left = _self_test_assembly(st, [(shared, at), (own_left, _self_test_shift(dx=20.0))]) + right = _self_test_assembly(st, [(shared, at), (own_right, _self_test_shift(dx=40.0))]) + _self_test_assembly(st, [(left, gp_Trsf()), (right, gp_Trsf())]) + st.UpdateAssemblies() + rep, occ = _self_test_convert(st) + report(rep["declared_leaf_placements"] == 4 and len(occ) == 3, + "the same sub-assembly at the same transform down two assembly paths is placed once", + f"{rep['declared_leaf_placements']} declared -> {len(occ)} placed") + report(len(set(occ)) == len(occ), + "and the two paths' own, distinct parts both survive", + f"{len(set(occ))} distinct (definition, world transform) pair(s)") + report(rep["n_suppressed_by_rule"]["coincident-occurrence"] == 1 + and rep["n_suppressed_by_rule"]["root-containment"] == 0, + "here it is the defensive rule that fires, not the structural one", + f"{rep['n_suppressed_by_rule']}") + + # --- 4. the invariant on a real corpus, not only on a fixture ------------------------------ + # ExcavatorArm must never move: 13 solids, 13 distinct signatures. + excavator_arm = _Path(__file__).resolve().parent.parent / "examples" / "ExcavatorArm.step" + if not excavator_arm.exists(): + report(False, "the count invariant holds on a real corpus (ExcavatorArm.step)", + f"missing corpus: {excavator_arm}") + else: + extract_graph(str(excavator_arm), meshparam=None, scale_to_cm=0.1) + occ = [o for o in enumerate_occurrences(placements, top_defs) if o[0] in logical_volumes] + report(len(occ) == 13 and len(set(occ)) == 13, + "the count invariant holds on a real corpus: ExcavatorArm.step has 13 placed solids in " + "and 13 out", f"{len(occ)} placed, {len(set(occ))} distinct") + + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + + +def run_in_field_media_self_test() -> int: + """Assert what `--in-field` writes, and that without it no SetParam is written. + + Returns the number of failures; prints one line per check. + """ + tally = _Checks() + report = tally.report + + print("\nMedium parameters under --in-field") + + mat = ResolvedMaterial( + bom_name="Silicon", nist_name="G4_Si", score=1.0, note="self-test", + rho_used_g_cm3=2.33, + elements=[{"symbol": "Si", "Z": 14, "A_g_mol": 28.0853614555, "mass_fraction": 1.0}], + radlen_cm=9.3660702922, intlen_cm=45.6603073704) + used = {"Silicon": mat} + + off, _ = emit_materials_cpp(used, in_field=None) + report("SetParam" not in off, + "without --in-field no SetParam is written (the negative control)", + "" if "SetParam" not in off else "emitter changed behaviour for existing modules") + + on, _ = emit_materials_cpp(used, in_field=(2.0, 10.0)) + report("cadFieldTrackingParams(cad_ifield, cad_fieldm);" in on, + "--in-field queries the LIVE field instead of asserting a pair") + report("med_Silicon->SetParam(1, cad_ifield);" in on, + "ifield is the queried variable, not a literal") + report("med_Silicon->SetParam(2, cad_fieldm);" in on, + "fieldm is the queried variable, not a literal") + report("int cad_ifield = 2;" in on and "float cad_fieldm = 10;" in on, + "the seed is only what applies when no field is loaded") + report("med_Default->SetParam(1, cad_ifield);" in on, + "the Default medium is not left field-free either") + for slot, key in enumerate(MEDIUM_PARAM_ORDER): + if key in ("ifield", "fieldm"): + continue + report(f"med_Silicon->SetParam({slot}, 0);" in on, + f"step control {key} stays 0 (the transport default)") + report(on.count("SetParam") == 2 * len(MEDIUM_PARAM_ORDER), + "all eight parameters are written for each of the two media", + f"found {on.count('SetParam')}") + _pre_on, _pre_off = emit_cpp_prelude(in_field=True), emit_cpp_prelude(in_field=False) + report('#include "Field/MagneticField.h"' in _pre_on and "#include \"TVirtualMC.h\"" in _pre_on, + "the prelude pulls the two headers Cling parses standalone") + report("static void cadFieldTrackingParams(int& mode, float& maxfield)" in _pre_on, + "and defines the query helper") + report("DetectorsBase/Detector.h" not in _pre_on, + "and NOT Detector.h, whose FairDetector payload segfaults a bare root -l session") + report("cadFieldTrackingParams" not in _pre_off, + "none of it appears without --in-field (the negative control)") + + # The exported driver: CheckOverlaps must be opt-in. Emitting the whole macro needs a CAD + # model, so this asserts on the emitter's own source, which is where the default lives. + import inspect as _inspect + _src = _inspect.getsource(emit_root_macro) + report("if (checkOverlaps) { gGeoManager->CheckOverlaps(); }" in _src, + "the emitted build_and_export runs CheckOverlaps only on request") + report("bool checkOverlaps=false" in _src, + "and its default is off -- it cost ~15 min on oTOF's 62 628 placements") + + custom, _ = emit_materials_cpp(used, in_field=(1.0, 5.5)) + report("int cad_ifield = 1;" in custom and "float cad_fieldm = 5.5;" in custom, + "IFIELD,FIELDM overrides seed the query") + + print(f"\n{tally.checks - tally.failures}/{tally.checks} in-field media checks passed") + return tally.failures + + +def run_bom_token_self_test() -> int: + """Assert that BOM tokenisation strips the "EN AW" alloy prefix and nothing inside a word.""" + tally = _Checks() + print("\nBOM material tokens") + for text, want in (("Tungsten", ["tungsten", "w"]), ("EN AW-6082", ["6082"])): + got = _norm_tokens(text) + tally.report(got == want, f"_norm_tokens({text!r}) == {want}", str(got)) + return tally.failures + + +def run_multibody_leaf_self_test() -> int: + """Assert that one XCAF leaf label carrying several solid bodies becomes several volumes, and + that a single-body leaf keeps its bare label entry as definition key. + + Returns the number of failures; prints one line per check. + """ + from OCC.Core.BRep import BRep_Builder + from OCC.Core.TopoDS import TopoDS_Compound + + tally = _Checks() + report = tally.report + + def compound_of(*shapes): + comp = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(comp) + for s in shapes: + builder.Add(comp, s) + return comp + + print("\nMulti-body leaf labels: one label, one body each") + + # --- 1. a leaf label holding two boxes, instanced twice ----------------------------------- + _doc, st = _self_test_shape_tool() + two_bodies = compound_of(BRepPrimAPI_MakeBox(gp_Pnt(0., 0., 0.), 1., 1., 1.).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(5., 0., 0.), 1., 1., 1.).Shape()) + part = st.AddShape(two_bodies, False) + module = _self_test_assembly(st, [(part, gp_Trsf())]) + _self_test_assembly(st, [(module, _self_test_shift(dx=0.0)), + (module, _self_test_shift(dx=100.0))]) + st.UpdateAssemblies() + rep, occ = _self_test_convert(st) + report(len(logical_volumes) == 2, + "a leaf label carrying two solid bodies becomes two logical volumes, not one", + f"{len(logical_volumes)} logical volume(s)") + report(len(occ) == 4 and len(set(occ)) == 4, + "and instancing that label twice places four bodies, all at distinct transforms", + f"{rep['declared_leaf_placements']} declared -> {len(occ)} placed, " + f"{len(set(occ))} distinct") + report(sum(rep["n_suppressed_by_rule"].values()) == 0, + "the two bodies of one label never look like a coincident duplicate", + f"{rep['n_suppressed_by_rule']}") + + # --- 2. the control: a single-body leaf keeps its bare label entry as the definition key --- + _doc, st = _self_test_shape_tool() + one_body = st.AddShape(BRepPrimAPI_MakeBox(2., 2., 2.).Shape(), False) + _self_test_assembly(st, [(one_body, gp_Trsf())]) + st.UpdateAssemblies() + _rep, occ = _self_test_convert(st) + keys = list(logical_volumes) + report(len(keys) == 1 and "#b" not in keys[0] and keys[0] == label_id(one_body), + "a single-body leaf is untouched: one volume, keyed on the bare label entry", + f"{keys}") + report(len(occ) == 1, "and it is placed exactly once", f"{len(occ)} placed") + + # --- 3. a leaf label with no geometry at all is skipped, not crashed on --------------------- + _doc, st = _self_test_shape_tool() + empty = st.AddShape(compound_of(), False) + good = st.AddShape(BRepPrimAPI_MakeBox(3., 3., 3.).Shape(), False) + _self_test_assembly(st, [(empty, gp_Trsf()), (good, _self_test_shift(dx=10.0))]) + st.UpdateAssemblies() + ok_empty = True + detail = "" + try: + _rep, occ = _self_test_convert(st) + except Exception as exc: # the old failure mode: Bnd_Box is void + ok_empty = False + occ = [] + detail = f"{type(exc).__name__}: {exc}" + report(ok_empty and len(logical_volumes) == 1 and len(occ) == 1, + "an empty leaf label is dropped with a warning and its siblings still convert", + detail or f"{len(logical_volumes)} volume(s), {len(occ)} placed") + + print(f"\n{tally.checks} checks, {tally.failures} failure(s)") + return tally.failures + + +def _recognized_inner_wall(face, rec) -> Optional[bool]: + """Decide, by measurement, which side of a RECOGNIZED quadric is outside the solid. + + On a NURBS-encoded quadric the orientation flag says nothing about the axis, so the face's own + outward normal is compared with the quadric's radial direction at every sample. Returns None + when the samples do not decide. + """ + samples = rec.get("P") + normals = rec.get("N") + if samples is None or normals is None or len(samples) == 0: + return None + sign = -1.0 if face.Orientation() == TopAbs_REVERSED else 1.0 + kind = rec["kind"] + if kind in ("cylinder", "cone"): + axis = np.asarray(rec["axis"], dtype=float) + axis = axis / np.linalg.norm(axis) + votes = 0 + for point, normal in zip(samples, normals): + outward = np.asarray(normal, dtype=float) * sign + if kind == "cylinder": + radial = point - rec["origin"] + radial = radial - np.dot(radial, axis) * axis + elif kind == "sphere": + radial = point - rec["centre"] + elif kind == "cone": + relative = point - rec["apex"] + radial = relative - np.dot(relative, axis) * axis + # The cone's outward normal tilts out of the radial direction by the half angle; only + # its sign relative to the radial direction matters here, and that tilt cannot flip it. + else: + return None + length = np.linalg.norm(radial) + if length < 1e-12: + continue # on the axis: this sample says nothing + votes += 1 if float(np.dot(outward, radial / length)) > 0.0 else -1 + if votes == 0: + return None + return votes < 0 + + +def _arbitrary_orthonormal_frame(axis): + """One arbitrary orthonormal in-plane vector for an axis with no natural reference direction + (a full/partial sphere has no preferred polar reference).""" + axis = np.asarray(axis, dtype=float) + axis = axis / np.linalg.norm(axis) + seed = np.array([1.0, 0.0, 0.0]) if abs(axis[0]) < 0.9 else np.array([0.0, 1.0, 0.0]) + e1 = seed - np.dot(seed, axis) * axis + return e1 / np.linalg.norm(e1) + + +def _recognized_quadric_wire_block(face, project): + """Build line-only trim wires in the recognized (phi, other) domain from the edges' 3D curves. + + phi is unwrapped continuously over all samples of a wire; an accepted edge is iso in phi or in + `other`, and a degenerate edge takes the incoming phi. Returns (wires, (phiStart, phiSweep, + otherLo, otherHi), None), or (None, None, reason). + """ + wires_edges = list(_face_wire_edges(face)) + if not wires_edges: + return None, None, "recognized quadric face has no wires" + + n_samples = 9 + per_wire = [] # (is_outer, [ [ (phi_raw, other), ... ] or None (degenerate) per edge ], [ other_at_degenerate_vertex or None ]) + all_other = [] + for _wire, is_outer, edges in wires_edges: + if len(edges) < 3: + return None, None, "recognized quadric trim wire has fewer than 3 edges" + edge_samples = [] + degenerate_other = [] + for edge, start_vertex in edges: + if BRep_Tool.Degenerated(edge): + _phi, other = project(BRep_Tool.Pnt(start_vertex)) + edge_samples.append(None) + degenerate_other.append(other) + all_other.append(other) + continue + degenerate_other.append(None) + try: + curve3d, first, last = BRep_Tool.Curve(edge) + except Exception: + curve3d = None + if curve3d is None: + return None, None, "recognized quadric boundary edge has no 3D curve" + reversed_edge = edge.Orientation() == TopAbs_REVERSED + samples = [] + for k in range(n_samples): + tau = k / (n_samples - 1.0) + t = (1.0 - tau) if reversed_edge else tau + phi, other = project(curve3d.Value(first + t * (last - first))) + samples.append((phi, other)) + all_other.append(other) + edge_samples.append(samples) + per_wire.append((is_outer, edge_samples, degenerate_other)) + tol_other = 1e-6 * max(1.0, max(all_other) - min(all_other)) + tol_phi = 1e-7 + + wires_out: List[dict] = [] + outer_window = None + for is_outer, edge_samples, degenerate_other in per_wire: + n = len(edge_samples) + unwrapped_edges = [] + prev_phi = None + for samples, deg_other in zip(edge_samples, degenerate_other): + if samples is None: + # degenerate point: carry the running phi through unchanged (see docstring) + if prev_phi is None: + prev_phi = 0.0 + unwrapped_edges.append([prev_phi] * n_samples) + continue + u_edge = [] + for phi_raw, _other in samples: + if prev_phi is None: + phi_u = phi_raw + else: + d = phi_raw - prev_phi + d -= 2.0 * math.pi * math.floor((d + math.pi) / (2.0 * math.pi)) + phi_u = prev_phi + d + u_edge.append(phi_u) + prev_phi = phi_u + unwrapped_edges.append(u_edge) + + starts = [] + all_phi_u, all_other_w = [], [] + for i, samples in enumerate(edge_samples): + phis_u = unwrapped_edges[i] + if samples is None: + others = [degenerate_other[i]] * n_samples + else: + others = [o for _p, o in samples] + all_phi_u.extend(phis_u) + all_other_w.extend(others) + is_iso_other = (max(others) - min(others)) <= tol_other + is_iso_phi = (max(phis_u) - min(phis_u)) <= tol_phi + if not (is_iso_other or is_iso_phi): + return None, None, "recognized quadric boundary edge is not axis-aligned in (phi, h/theta)" + starts.append((phis_u[0], others[0])) + if is_outer: + outer_window = (min(all_phi_u), max(all_phi_u) - min(all_phi_u), min(all_other_w), max(all_other_w)) + + seg_edges = [] + for i in range(n): + u0, v0 = starts[i] + u1, v1 = starts[(i + 1) % n] + seg_edges.append({"curve": "line", "params": [u0, v0, u1, v1]}) + wires_out.append({"role": "outer" if is_outer else "inner", "edges": seg_edges}) + + n_outer = sum(1 for w in wires_out if w["role"] == "outer") + if n_outer != 1: + return None, None, f"recognized quadric face has {n_outer} outer trim wires (expected exactly 1)" + return wires_out, outer_window, None + + +_NOT_RECOGNIZED_YET = object() + + +def recognize_and_extract_face(face, scale_to_cm: float, + rec=_NOT_RECOGNIZED_YET) -> Tuple[Optional[dict], Optional[str]]: + """Canonical-form pre-pass: extract a face whose stored surface has no direct extractor through + the exact plane/sphere/cylinder/cone behind it; (None, None) when it is not recognizable. + `rec` is the recognizer's result when the surface report already computed it. + """ + adaptor = BRepAdaptor_Surface(face) + try: + uv_bounds = breptools.UVBounds(face) + except Exception: + return None, None + if rec is _NOT_RECOGNIZED_YET: + rec = _recognize_analytic_surface(adaptor, uv_bounds) + if rec is None: + return None, None + kind = rec["kind"] + s = scale_to_cm + # Which side is outside is measured on the face, falling back to the orientation flag. + inner_wall = face.Orientation() == TopAbs_REVERSED + measured_inner_wall = _recognized_inner_wall(face, rec) + if measured_inner_wall is not None: + inner_wall = measured_inner_wall + + if kind == "plane": + normal = rec["normal"] + e1 = _arbitrary_orthonormal_frame(normal) + outward_sign = -1.0 if inner_wall else 1.0 + e2 = np.cross(normal, e1) * outward_sign # axisU x axisV must equal the outward normal + origin_cm = (rec["point"] * s).tolist() + record, reason = extract_planar_face(face, s, frame_override=(origin_cm, e1.tolist(), e2.tolist())) + if record is None: + return None, f"recognized as plane but {reason}" + record["recognized"] = {"kind": "plane", "residual": rec["residual"]} + return record, None + + if kind == "cylinder": + axis = rec["axis"] / np.linalg.norm(rec["axis"]) + refu = rec["refu"] - np.dot(rec["refu"], axis) * axis + refu = refu / np.linalg.norm(refu) + e2 = np.cross(axis, refu) + origin_native = rec["origin"] + + def project(pnt): + rel = np.array([pnt.X(), pnt.Y(), pnt.Z()]) - origin_native + phi = math.atan2(np.dot(rel, e2), np.dot(rel, refu)) + return phi, float(np.dot(rel, axis)) * s + + wires, window, reason = _recognized_quadric_wire_block(face, project) + if wires is None: + return None, f"recognized as cylinder but {reason}" + phi_start, phi_sweep, h_lo, h_hi = window + if phi_sweep <= 0.0 or phi_sweep > 2.0 * math.pi + 1e-9: + return None, "recognized cylinder trim wraps more than a full turn in phi" + params = ((origin_native * s).tolist() + axis.tolist() + refu.tolist() + + [rec["radius"] * s, h_lo, h_hi, phi_start, phi_sweep]) + record = {"type": "cylinder", "inner_wall": inner_wall, "params": params, "wires": wires, + "recognized": {"kind": "cylinder", "residual": rec["residual"]}} + return record, None + + if kind == "cone": + axis = rec["axis"] / np.linalg.norm(rec["axis"]) + refu = rec["refu"] - np.dot(rec["refu"], axis) * axis + refu = refu / np.linalg.norm(refu) + e2 = np.cross(axis, refu) + apex_native = rec["apex"] + tan_half = math.tan(rec["half_angle"]) + + def project(pnt): + rel = np.array([pnt.X(), pnt.Y(), pnt.Z()]) - apex_native + phi = math.atan2(np.dot(rel, e2), np.dot(rel, refu)) + return phi, float(np.dot(rel, axis)) * s + + wires, window, reason = _recognized_quadric_wire_block(face, project) + if wires is None: + return None, f"recognized as cone but {reason}" + phi_start, phi_sweep, h_lo, h_hi = window + if phi_sweep <= 0.0 or phi_sweep > 2.0 * math.pi + 1e-9: + return None, "recognized cone trim wraps more than a full turn in phi" + h_lo = max(0.0, h_lo) + h_hi = max(h_lo, h_hi) + params = ((apex_native * s).tolist() + axis.tolist() + refu.tolist() + + [h_lo * tan_half, h_hi * tan_half, h_lo, h_hi, phi_start, phi_sweep]) + record = {"type": "cone", "inner_wall": inner_wall, "params": params, "wires": wires, + "recognized": {"kind": "cone", "residual": rec["residual"]}} + return record, None + + if kind == "sphere": + centre_native = rec["centre"] + # A sphere has no natural polar axis; any orthonormal frame is a valid (self-consistent) + # (phi, theta) parametrization for this face. + polar_axis = np.array([0.0, 0.0, 1.0]) + refu = _arbitrary_orthonormal_frame(polar_axis) + e2 = np.cross(polar_axis, refu) + + def project(pnt): + rel = (np.array([pnt.X(), pnt.Y(), pnt.Z()]) - centre_native) / rec["radius"] + theta = math.acos(max(-1.0, min(1.0, float(np.dot(rel, polar_axis))))) + phi = math.atan2(float(np.dot(rel, e2)), float(np.dot(rel, refu))) + return phi, theta + + wires, window, reason = _recognized_quadric_wire_block(face, project) + if wires is None: + return None, f"recognized as sphere but {reason}" + phi_start, phi_sweep, theta_lo, theta_hi = window + if phi_sweep <= 0.0 or phi_sweep > 2.0 * math.pi + 1e-9: + return None, "recognized sphere trim wraps more than a full turn in phi" + params = ((centre_native * s).tolist() + polar_axis.tolist() + refu.tolist() + + [rec["radius"] * s, theta_lo, theta_hi, phi_start, phi_sweep]) + record = {"type": "sphere", "inner_wall": inner_wall, "params": params, "wires": wires, + "recognized": {"kind": "sphere", "residual": rec["residual"]}} + return record, None + + return None, None + + +# Face extractors dispatched by analytic surface type. +_FACE_EXTRACTORS = { + "plane": extract_planar_face, + "cylinder": extract_cylindrical_face, + "cone": extract_conical_face, + "sphere": extract_spherical_face, + "torus": extract_toroidal_face, +} + + +def extract_surfaces_for_shape(shape, scale_to_cm: float, recognize_surfaces: bool = True, + recognition=None, + lid=None) -> Tuple[Optional[List[dict]], List[str], int]: + """Attempt to extract every face of a leaf solid into exact sidecar surface records. + + Returns (surfaces, [], nModelEdges), or (None, reasons, 0) when any face is unsupported, so an + emitted sidecar describes all faces. `recognition` holds the surface report's results by + (`lid`, face index). + """ + surfaces: List[dict] = [] + reasons: List[str] = [] + n_faces = 0 + edge_map, edge_id = build_edge_table(shape) + for index, face in enumerate(TopologyExplorer(shape).faces()): + n_faces += 1 + adaptor = BRepAdaptor_Surface(face) + surf_type = SURFACE_TYPE_NAME.get(adaptor.GetType(), "unknown") + extractor = _FACE_EXTRACTORS.get(surf_type) + wires = None + if extractor is None: + record, reason = None, f"{surf_type} face extraction not implemented yet" + else: + wires = list(_face_wire_edges(face)) + record, reason = extractor(face, scale_to_cm, wires=wires) + if record is None and recognize_surfaces: + known = (recognition.get((lid, index), _NOT_RECOGNIZED_YET) if recognition is not None + else _NOT_RECOGNIZED_YET) + rec_record, rec_reason = recognize_and_extract_face(face, scale_to_cm, rec=known) + if rec_record is not None: + record, reason = rec_record, None + elif rec_reason is not None: + reason = f"{reason}; recognition attempted: {rec_reason}" + if record is None: + reasons.append(reason or f"{surf_type} face not supported") + else: + record["edge_refs"] = face_boundary_edge_refs(face, edge_id, + anchored=bool(record.get("wires")), + wires=wires) + surfaces.append(record) + if n_faces == 0: + return None, ["shape has no faces"], 0 + if reasons: + return None, reasons, 0 + return surfaces, [], edge_map.Size() + + +# ------------------------------- +# BOM / material mapping +# ------------------------------- + +@dataclass(frozen=True) +class BomEntry: + part_number: str + revision: str + name: str + mass_value: float # as in CSV + material: str + + @property + def part_number_key(self) -> str: + return (self.part_number or "").strip() + + @property + def name_key(self) -> str: + return (self.name or "").strip() + + +def _to_float(s: str) -> Optional[float]: + try: + if s is None: + return None + s = str(s).strip() + if not s: + return None + return float(s) + except Exception: + return None + + +def read_bom_csv(csv_path: str) -> List[BomEntry]: + """ + Reads a BOM CSV in the format provided by design team. + + We look for rows whose first column is 'CAD' and second is 'Mechanical/Part'. + Columns (0-based): + 0 CAD + 1 type + 2 part number + 3 revision + 4 name/description + 5 mass + 6 material + """ + entries: List[BomEntry] = [] + with open(csv_path, newline="", encoding="utf-8", errors="ignore") as f: + reader = csv.reader(f) + for row in reader: + if not row: + continue + if len(row) < 7: + continue + if row[0].strip() != "CAD": + continue + if row[1].strip() != "Mechanical/Part": + continue + + part_no = (row[2] or "").strip() + rev = (row[3] or "").strip() + name = (row[4] or "").strip() + mass = _to_float(row[5]) + mat = (row[6] or "").strip() + + if not (part_no or name): + continue + if mass is None: + mass = float("nan") + if not mat: + mat = "Default" + + entries.append(BomEntry(part_no, rev, name, float(mass), mat)) + return entries + + + +def normalize_material_name(mat: str) -> str: + """ + Normalizes a BOM material string for matching / caching. + + Note: We keep the *original* string for ROOT object names; this is only used + internally for robust matching and dictionary keys. + """ + mat = (mat or "Default").strip() + mat = re.sub(r"\s+", " ", mat) + return mat + + +def _norm_tokens(s: str) -> List[str]: + s = (s or "").lower() + # common grade/format noise + s = re.sub(r"\(.*?\)", " ", s) + s = re.sub(r"\ben[\s-]*aw\b", " ", s) + s = re.sub(r"\b(en|aw)\b", " ", s) + s = s.replace("_", " ").replace("-", " ") + s = re.sub(r"[^a-z0-9]+", " ", s) + s = re.sub(r"\s+", " ", s).strip() + if not s: + return [] + toks = s.split(" ") + + # small synonym normalization + syn = { + "alu": "al", + "aluminium": "aluminum", + "silicium": "silicon", + "inox": "stainless", + "ss": "stainless", + "cu": "copper", + "fe": "iron", + "ptfe": "teflon", + "ti": "titanium", + "be": "beryllium", + } + + # Expand common element symbols to names and vice-versa so that e.g. "G4_Si" can match "silicon". + elem_alias = { + "h": "hydrogen", "he": "helium", "c": "carbon", "n": "nitrogen", "o": "oxygen", + "al": "aluminum", "si": "silicon", "fe": "iron", "cu": "copper", "be": "beryllium", + "mg": "magnesium", "mn": "manganese", "cr": "chromium", "ni": "nickel", "zn": "zinc", + "ti": "titanium", "w": "tungsten", "pb": "lead", "sn": "tin", + } + name_to_sym = {v: k for k, v in elem_alias.items()} + + out: List[str] = [] + for t in toks: + t2 = syn.get(t, t) + out.append(t2) + if t2 in elem_alias: + out.append(elem_alias[t2]) + if t2 in name_to_sym: + out.append(name_to_sym[t2]) + + # de-dup while preserving order + seen = set() + out2: List[str] = [] + for t in out: + if t and t not in seen: + seen.add(t) + out2.append(t) + return out2 + + +def _density_score(rho_part: Optional[float], rho_ref: Optional[float]) -> float: + if rho_part is None or rho_ref is None or not (rho_part > 0.0) or not (rho_ref > 0.0): + return 0.0 + # symmetric score in log-space; 1.0 is perfect match + d = abs(math.log(rho_ref / rho_part)) + return 1.0 / (1.0 + d) + + +def _token_score(tokens_a: List[str], tokens_b: List[str]) -> float: + if not tokens_a or not tokens_b: + return 0.0 + sa = set(tokens_a) + sb = set(tokens_b) + inter = len(sa & sb) + union = len(sa | sb) + if union == 0: + return 0.0 + return inter / union + + +def load_g4_nist_db(json_path: str) -> Dict[str, dict]: + """ + Loads a JSON dump created by the 'nist_export_all' tool. + Returns a dict: nist_name -> material record. + """ + with open(json_path, "r", encoding="utf-8") as f: + data = json.load(f) + mats = data.get("materials", {}) + if not isinstance(mats, dict) or not mats: + raise RuntimeError(f"G4 NIST DB JSON seems empty or malformed: {json_path}") + return mats + +# Minimal periodic table for parsing custom alloys not present in NIST. +# Values: Z (atomic number), A (g/mol) +_ELEMENT_TABLE = { + "H": (1, 1.00794), + "C": (6, 12.0107), + "N": (7, 14.0067), + "O": (8, 15.9994), + "Al": (13, 26.9815385), + "Si": (14, 28.0855), + "Fe": (26, 55.845), + "Cu": (29, 63.546), + "Be": (4, 9.0121831), + "Mg": (12, 24.305), + "Mn": (25, 54.938044), + "Cr": (24, 51.9961), + "Ni": (28, 58.6934), + "Zn": (30, 65.38), + "Ti": (22, 47.867), + "W": (74, 183.84), + "Pb": (82, 207.2), + "Sn": (50, 118.71), +} + + +@dataclass +class ResolvedMaterial: + bom_name: str + nist_name: Optional[str] # e.g. "G4_Al" + score: float + rho_used_g_cm3: Optional[float] # density used in ROOT definition + radlen_cm: Optional[float] + intlen_cm: Optional[float] + elements: Optional[List[dict]] # list of {symbol,Z,A_g_mol,mass_fraction} + note: str # for comments in geom.C (warnings/FIXME) + +@dataclass +class MatMatchConfig: + # Minimum combined score to accept a match. + min_score: float = 0.35 + # If (best - second_best) < ambiguity_delta, treat as ambiguous/unresolved. + ambiguity_delta: float = 0.05 + # Weights for the combined score = w_token * token_score + w_density * density_score + w_token: float = 0.75 + w_density: float = 0.25 + # Optional hard filter on density proximity (in log-space). If <=0, disabled. + # Example: max_log_density_diff=0.8 means accept within exp(0.8)~2.2x in either direction. + max_log_density_diff: float = 0.0 + # Penalize compound matches (oxide/dioxide/carbide/...) when BOM doesn't mention those tokens. + compound_penalty: float = 0.25 + + +def resolve_bom_material( + bom_material: str, + rho_part_g_cm3: Optional[float], + g4db: Optional[Dict[str, dict]], + cfg: MatMatchConfig, +) -> ResolvedMaterial: + """ + Resolves an arbitrary BOM material string to a Geant4 NIST material name using: + - exact key match (BOM already uses e.g. "G4_Al") + - token overlap scoring on names + - density proximity scoring (if rho_part_g_cm3 available) + + If unresolved/ambiguous, tries to parse element symbols from the BOM string (e.g. "Cu Be") + and emits a placeholder mixture (equal mass fractions) annotated with FIXME. + """ + raw_bom_material = (bom_material or "").strip() + bom_material = normalize_material_name(bom_material) + + if not g4db: + return ResolvedMaterial( + bom_name=bom_material, + nist_name=None, + score=0.0, + rho_used_g_cm3=rho_part_g_cm3, + radlen_cm=None, + intlen_cm=None, + elements=None, + note="FIXME: No Geant4 NIST DB provided; using dummy material.", + ) + + # Trivial: BOM already provides an exact Geant4 material key + if bom_material in g4db: + rec = g4db[bom_material] + rho_ref = rec.get("density_g_cm3") + # Use NIST density for emission; CAD-derived density is used only for matching. + rho_used = rho_ref + + rad = rec.get("radlen_cm") + itl = rec.get("intlen_cm") + + return ResolvedMaterial( + bom_name=bom_material, + nist_name=bom_material, + score=1.0, + rho_used_g_cm3=rho_used, + radlen_cm=rad, + intlen_cm=itl, + elements=rec.get("elements", []), + note="Resolved by exact Geant4 NIST name from BOM.", + ) + + bom_toks = _norm_tokens(bom_material) + if not bom_toks: + return ResolvedMaterial( + bom_name=bom_material, + nist_name=None, + score=0.0, + rho_used_g_cm3=rho_part_g_cm3, + radlen_cm=None, + intlen_cm=None, + elements=None, + note="FIXME: Empty/unknown BOM material string; using dummy material.", + ) + + def _build_custom_from_elements(note_prefix: str) -> Optional[ResolvedMaterial]: + s = raw_bom_material + if not s: + return None + + symbols = set(re.findall(r"\b([A-Z][a-z]?)\b", s)) + name_to_symbol = { + "aluminum": "Al", "aluminium": "Al", "silicon": "Si", "iron": "Fe", "copper": "Cu", + "beryllium": "Be", "magnesium": "Mg", "manganese": "Mn", "chromium": "Cr", "nickel": "Ni", + "zinc": "Zn", "titanium": "Ti", "tungsten": "W", "lead": "Pb", "tin": "Sn", + } + for t in bom_toks: + if t in name_to_symbol: + symbols.add(name_to_symbol[t]) + + symbols = [sym for sym in sorted(symbols) if sym in _ELEMENT_TABLE] + if not symbols: + return None + + frac = 1.0 / float(len(symbols)) + elems: List[dict] = [] + for sym in symbols: + Z, A = _ELEMENT_TABLE[sym] + elems.append({"symbol": sym, "Z": Z, "A_g_mol": A, "mass_fraction": frac}) + + return ResolvedMaterial( + bom_name=bom_material, + nist_name=None, + score=0.0, + rho_used_g_cm3=rho_part_g_cm3, + radlen_cm=None, + intlen_cm=None, + elements=elems, + note=f"FIXME: {note_prefix} No suitable Geant4 NIST material. Emitting placeholder mixture from parsed elements {symbols} with equal mass fractions; please adjust fractions/material.", + ) + + best = (None, -1.0, 0.0, 0.0) # (nist_name, score, dens_score, token_score) + second = (None, -1.0, 0.0, 0.0) + + bom_has_compound = any(t in bom_toks for t in ( + "oxide", "dioxide", "carbide", "nitride", "fluoride", "chloride", + "sulfate", "phosphate", "glass", "dioxyde" + )) + + for nist_name, rec in g4db.items(): + nist_toks = _norm_tokens(nist_name) + ts = _token_score(bom_toks, nist_toks) + if ts <= 0.0: + continue + + ds = _density_score(rho_part_g_cm3, rec.get("density_g_cm3")) + + # Optional hard density filter + if cfg.max_log_density_diff and cfg.max_log_density_diff > 0.0 and rho_part_g_cm3 and rec.get("density_g_cm3"): + try: + if abs(math.log(float(rec.get("density_g_cm3")) / float(rho_part_g_cm3))) > cfg.max_log_density_diff: + continue + except Exception: + pass + + nist_has_compound = any(t in nist_toks for t in ( + "oxide", "dioxide", "carbide", "nitride", "fluoride", "chloride", + "sulfate", "phosphate", "glass", "dioxyde" + )) + compound_pen = cfg.compound_penalty if (nist_has_compound and not bom_has_compound) else 0.0 + + score = cfg.w_token * ts + cfg.w_density * ds - compound_pen + + if score > best[1]: + second = best + best = (nist_name, score, ds, ts) + elif score > second[1]: + second = (nist_name, score, ds, ts) + + nist_best, score_best, ds_best, ts_best = best + nist_second, score_second, _, _ = second + + if nist_best is None or score_best < cfg.min_score: + custom = _build_custom_from_elements("Could not resolve with enough confidence.") + if custom is not None: + return custom + return ResolvedMaterial( + bom_name=bom_material, + nist_name=None, + score=float(score_best if score_best > 0 else 0.0), + rho_used_g_cm3=rho_part_g_cm3, + radlen_cm=None, + intlen_cm=None, + elements=None, + note="FIXME: Could not resolve BOM material to a Geant4 NIST material with enough confidence; using dummy material.", + ) + + if score_second > 0 and (score_best - score_second) < cfg.ambiguity_delta: + custom = _build_custom_from_elements( + f"Ambiguous material match (best '{nist_best}' score={score_best:.3f}, second '{nist_second}' score={score_second:.3f})." + ) + if custom is not None: + return custom + return ResolvedMaterial( + bom_name=bom_material, + nist_name=None, + score=float(score_best), + rho_used_g_cm3=rho_part_g_cm3, + radlen_cm=None, + intlen_cm=None, + elements=None, + note=f"FIXME: Ambiguous material match (best '{nist_best}' score={score_best:.3f}, second '{nist_second}' score={score_second:.3f}); using dummy material.", + ) + + rec = g4db[nist_best] + rho_ref = rec.get("density_g_cm3") + # Use NIST density for emission; CAD-derived density is used only for matching. + rho_used = rho_ref + + rad = rec.get("radlen_cm") + itl = rec.get("intlen_cm") + + return ResolvedMaterial( + bom_name=bom_material, + nist_name=nist_best, + score=float(score_best), + rho_used_g_cm3=rho_used, + radlen_cm=rad, + intlen_cm=itl, + elements=rec.get("elements", []), + note=f"Resolved to '{nist_best}' (token={ts_best:.3f}, density={ds_best:.3f}, score={score_best:.3f}).", + ) + + +def build_volume_to_material_map( + bom_entries: List[BomEntry], + def_names: Dict[str, str], +) -> Dict[str, BomEntry]: + """ + Builds a mapping def_lid -> BomEntry by matching the XCAF display name to: + - exact part_number match + - exact description/name match + - substring match on part_number within the XCAF name + + This is heuristic; if nothing matches we keep no assignment for that volume. + """ + # lookup tables + by_part: Dict[str, BomEntry] = {} + by_name: Dict[str, BomEntry] = {} + for e in bom_entries: + if e.part_number_key: + by_part[e.part_number_key] = e + if e.name_key and e.name_key not in by_name: + by_name[e.name_key] = e + + out: Dict[str, BomEntry] = {} + for lid, disp in def_names.items(): + key = (disp or "").strip() + if not key: + continue + + # 1) exact part number + if key in by_part: + out[lid] = by_part[key] + continue + # 2) exact name/description + if key in by_name: + out[lid] = by_name[key] + continue + # 3) substring match on any part number + for pn, e in by_part.items(): + if pn and pn in key: + out[lid] = e + break + return out + + +# ------------------------------- +# C++ emission helpers +# ------------------------------- + +def trsf_to_tgeo(trsf: gp_Trsf, name: str, scale_to_cm: float) -> str: + m = trsf.GetRotation().GetMatrix() + t = trsf.TranslationPart() + return f""" + Double_t {name}_m[9] = {{ + {m.Value(1,1)}, {m.Value(1,2)}, {m.Value(1,3)}, + {m.Value(2,1)}, {m.Value(2,2)}, {m.Value(2,3)}, + {m.Value(3,1)}, {m.Value(3,2)}, {m.Value(3,3)} + }}; + TGeoRotation *{name}_rot = new TGeoRotation(); + {name}_rot->SetMatrix({name}_m); + TGeoCombiTrans *{name} = new TGeoCombiTrans({t.X()*scale_to_cm}, {t.Y()*scale_to_cm}, {t.Z()*scale_to_cm}, {name}_rot); +""" + + +def emit_cpp_prelude(exact_surfaces: bool = False, csg_shapes: bool = False, + flat_csg_shapes: bool = False, o2_tessellated: bool = False, + in_field: bool = False) -> str: + prelude = """#include +#include +#include +#include +#include +#include + +static void LoadFacets(const std::string& file, TGeoTessellated* solid, bool check=false) +{ + std::ifstream in(file, std::ios::binary); + if (!in) throw std::runtime_error("Cannot open facet file: " + file); + + uint32_t nTri = 0; + in.read(reinterpret_cast(&nTri), sizeof(nTri)); + if (!in) throw std::runtime_error("Bad facet header in: " + file); + + for (uint32_t i=0;i(v), sizeof(v)); + if (!in) throw std::runtime_error("Unexpected EOF in: " + file); + + solid->AddFacet(TGeoTessellated::Vertex_t(v[0],v[1],v[2]), + TGeoTessellated::Vertex_t(v[3],v[4],v[5]), + TGeoTessellated::Vertex_t(v[6],v[7],v[8])); + } + solid->CloseShape(check, true); +} +""" + if in_field: + # --in-field queries the live field through headers Cling parses standalone. + prelude += """#include "Field/MagneticField.h" +#include "TVirtualMC.h" + +// The live field's integration mode and maximum, exactly as +// o2::base::Detector::initFieldTrackingParams computes them. Values passed in are the fallback +// used when no field is loaded. +static void cadFieldTrackingParams(int& mode, float& maxfield) +{ + auto vmc = TVirtualMC::GetMC(); + if (!vmc) { + return; + } + if (auto* fld = dynamic_cast(vmc->GetMagField())) { + mode = fld->Integral(); + maxfield = fld->Max(); + } +} +""" + + if csg_shapes: + # TGeoHMatrix comes in through TGeoManager.h today, but the CSG loader names it directly + # and must not depend on that. + prelude += "#include \n" + prelude += import_csg_hook().CPP_LOADER + if flat_csg_shapes: + prelude += import_csg_hook().FLAT_CPP_PRELUDE + if not exact_surfaces and not o2_tessellated: + return prelude + + # The navigable solids need libO2CADSupport; headers are included, never declared by prototype. + prelude += """ +// --- navigable O2 solid support (requires the ALICE O2 environment) --- +R__ADD_INCLUDE_PATH($O2_ROOT/include) +R__LOAD_LIBRARY(libO2CADSupport) +""" + if o2_tessellated: + # O2Tessellated navigates the facets; ROOT's TGeoTessellated only navigates as its bbox. + prelude += """#include "DetectorsBase/O2Tessellated.h" +#include "CADSupport/O2SurfaceSolidIO.h" + +static void LoadFacetsO2(const std::string& file, o2::base::O2Tessellated* solid, bool check=false) +{ + if (!o2::cad::LoadFacetSolid(file, *solid)) { + throw std::runtime_error("Cannot load facet sidecar: " + file); + } + solid->CloseShape(check, true, false); +} +""" + if not exact_surfaces: + return prelude + prelude += """#include "CADSupport/O2BVHSurfaceSolid.h" +// The loader comes from its own public header, NOT from a hand-rolled prototype. +// o2::cad::loadCADGeometryHook JITs this macro inside a unique namespace and hoists +// only lines beginning with '#' to global scope, so a `namespace o2 { namespace cad {` +// block here becomes `::o2::cad` and shadows the real one -- every later +// `o2::cad::O2BVHSurfaceSolid` then fails to resolve and the whole module silently +// does not load. An #include is hoisted, so it declares the right symbol. +#include "CADSupport/O2SurfaceSolidIO.h" + +static void LoadSurfaces(const std::string& file, o2::cad::O2BVHSurfaceSolid* solid, bool check=false) +{ + if (!o2::cad::LoadSurfaceSolid(file, *solid)) { + throw std::runtime_error("Cannot load surface sidecar: " + file); + } + solid->CloseShape(check); + if (check && (!solid->IsClosed() || !solid->IsOrientationConsistent())) { + throw std::runtime_error("Surface solid not closed/orientation-consistent: " + file); + } +} +""" + return prelude + + +def emit_media_sidecar_cpp(sidecar: dict) -> Tuple[str, Dict[str, str]]: + """Emit the media of a TGeo -> STEP writer sidecar verbatim, field for field. + + Returns the C++ block and a map from medium name to its C++ variable. + """ + cpp: List[str] = [] + cpp.append(" // Media rebuilt verbatim from the TGeo -> STEP media sidecar.") + cpp.append(" // Default stays as the fallback for a part the sidecar does not name.") + cpp.append(" TGeoMaterial *mat_Default = new TGeoMaterial(\"Default\", 0., 0., 0.);") + cpp.append(" TGeoMedium *med_Default = new TGeoMedium(\"Default\", 1, mat_Default);") + cpp.append("") + + medium_var: Dict[str, str] = {"Default": "med_Default"} + order = list(sidecar.get("mediumParamOrder") or + ("isvol", "ifield", "fieldm", "tmaxfd", + "stemax", "deemax", "epsil", "stmin")) + + for name in sorted(sidecar.get("media", {})): + rec = sidecar["media"][name] + mat = rec["material"] + safe = sanitize_cpp_name(name) + mvar, medvar = f"mat_{safe}", f"med_{safe}" + + if mat.get("isMixture"): + els = mat.get("elements", []) + cpp.append(f" TGeoMixture *{mvar} = new TGeoMixture(\"{mat['name']}\", " + f"{len(els)}, {mat['density']:.17g});") + for el in els: + cpp.append(f" {mvar}->AddElement({el['A']:.17g}, {el['Z']:.17g}, " + f"{el['W']:.17g});") + else: + cpp.append(f" TGeoMaterial *{mvar} = new TGeoMaterial(\"{mat['name']}\", " + f"{mat['A']:.17g}, {mat['Z']:.17g}, {mat['density']:.17g});") + + # No SetRadLen: ROOT recomputes it from the recipe, which is carried exactly. + cpp.append(f" TGeoMedium *{medvar} = new TGeoMedium(\"{name}\", {int(rec['id'])}, " + f"{mvar});") + for i, key in enumerate(order): + cpp.append(f" {medvar}->SetParam({i}, {float(rec['params'][key]):.17g});" + f" // {key}") + cpp.append("") + medium_var[name] = medvar + + return "\n".join(cpp), medium_var + + +# The eight Geant medium parameters, in the order TGeoMedium stores them. +MEDIUM_PARAM_ORDER = ("isvol", "ifield", "fieldm", "tmaxfd", + "stemax", "deemax", "epsil", "stmin") + +# The --in-field seed: Detector::initFieldTrackingParams's own fallback; step controls stay 0. +IN_FIELD_SEED = (2, 10.0) + + +def _in_field_setparams(medvar: str) -> List[str]: + """The eight SetParam lines for one medium under `--in-field`. + + ifield and fieldm come from the live-field query; the step controls stay 0 (transport default). + """ + out: List[str] = [] + for i, key in enumerate(MEDIUM_PARAM_ORDER): + if key == "ifield": + out.append(f" {medvar}->SetParam({i}, cad_ifield); // ifield, from the live field") + elif key == "fieldm": + out.append(f" {medvar}->SetParam({i}, cad_fieldm); // fieldm, from the live field") + else: + out.append(f" {medvar}->SetParam({i}, 0); // {key} (transport default)") + return out + + +def emit_materials_cpp( + used_materials: Dict[str, ResolvedMaterial], + in_field: Optional[Tuple[float, float]] = None, + # key: BOM material string as used in CSV after normalization +) -> Tuple[str, Dict[str, str]]: + """ + Emits C++ code defining TGeoMaterial/TGeoMixture + TGeoMedium for all used materials. + + - A resolved Geant4 NIST material becomes a mixture, with RadLen/IntLen when available. + - An unresolved one becomes a dummy material with FIXME comments. + - With `in_field` the eight medium parameters are written, ifield and fieldm from the live field. + """ + cpp: List[str] = [] + cpp.append(" // Default material/medium (placeholder; can be replaced later)") + cpp.append(" TGeoMaterial *mat_Default = new TGeoMaterial(\"Default\", 0., 0., 0.);") + cpp.append(" TGeoMedium *med_Default = new TGeoMedium(\"Default\", 1, mat_Default);") + if in_field is not None: + cpp.append("") + cpp.append(" // Field tracking parameters, taken from the LIVE field: the same query") + cpp.append(" // o2::base::Detector::initFieldTrackingParams makes, so a CAD module is not") + cpp.append(" // treated differently from a hand-written detector. The seeds below are what") + cpp.append(" // that function itself falls back to when no field is loaded.") + cpp.append(f" int cad_ifield = {int(in_field[0])};") + cpp.append(f" float cad_fieldm = {float(in_field[1]):.17g};") + cpp.append(" cadFieldTrackingParams(cad_ifield, cad_fieldm);") + cpp.append("") + cpp.extend(_in_field_setparams("med_Default")) + cpp.append("") + + emitted_el: Dict[str, str] = {} + + def _emit_element(el: dict) -> str: + sym = el.get("symbol", "X") + Z = int(el.get("Z", 0)) + A = float(el.get("A_g_mol", 0.0)) + if sym in emitted_el: + return emitted_el[sym] + safe = sanitize_cpp_name(sym) + var = f"el_{safe}" + cpp.append(f" TGeoElement *{var} = new TGeoElement(\"{sym}\", \"{sym}\", {Z}, {A:.10g});") + emitted_el[sym] = var + return var + + medium_var: Dict[str, str] = {"Default": "med_Default"} + next_id = 2 + + for bom_mat in sorted(used_materials.keys(), key=lambda s: s.lower()): + rm = used_materials[bom_mat] + safe = sanitize_cpp_name(bom_mat) + base = safe + k = 2 + while f"med_{safe}" in medium_var.values(): + safe = f"{base}_{k}" + k += 1 + + rho = rm.rho_used_g_cm3 if (rm.rho_used_g_cm3 and rm.rho_used_g_cm3 > 0.0) else 0.0 + + cpp.append(f" // BOM material: {rm.bom_name}") + cpp.append(f" // {rm.note}") + + if rm.elements: + elems = rm.elements + if len(elems) == 1 and abs(float(elems[0].get('mass_fraction', 1.0)) - 1.0) < 1e-6: + el = elems[0] + A = float(el.get("A_g_mol", 0.0)) + Z = float(el.get("Z", 0)) + cpp.append(f" TGeoMaterial *mat_{safe} = new TGeoMaterial(\"{bom_mat}\", {A:.10g}, {Z:.10g}, {rho:.10g});") + else: + cpp.append(f" TGeoMixture *mat_{safe} = new TGeoMixture(\"{bom_mat}\", {len(elems)}, {rho:.10g});") + for el in elems: + elvar = _emit_element(el) + w = float(el.get("mass_fraction", 0.0)) + cpp.append(f" mat_{safe}->AddElement({elvar}, {w:.10g});") + + if rm.radlen_cm is not None and rm.intlen_cm is not None: + cpp.append(f" mat_{safe}->SetRadLen({float(rm.radlen_cm):.10g}, {float(rm.intlen_cm):.10g});") + elif rm.radlen_cm is not None: + cpp.append(f" mat_{safe}->SetRadLen({float(rm.radlen_cm):.10g});") + else: + cpp.append(" // FIXME: Unresolved material. Replace with a proper TGeoMaterial/TGeoMixture.") + cpp.append(f" TGeoMaterial *mat_{safe} = new TGeoMaterial(\"{bom_mat}\", 0., 0., {rho:.10g});") + + cpp.append(f" TGeoMedium *med_{safe} = new TGeoMedium(\"{bom_mat}\", {next_id}, mat_{safe});") + if in_field is not None: + cpp.extend(_in_field_setparams(f"med_{safe}")) + cpp.append("") + medium_var[bom_mat] = f"med_{safe}" + next_id += 1 + + return "\n".join(cpp), medium_var + + + + +def emit_tessellated_cpp(lid: str, vol_display_name: str, facet_abspath: str, ntriangles: int, medium_var: str, + solid_class: str = "o2::base::O2Tessellated") -> str: + """Emit the tessellated fallback for one leaf solid. + + ``solid_class`` defaults to O2Tessellated; ROOT's TGeoTessellated navigates as its bbox. + """ + safe = sanitize_cpp_name(lid) + shape_name = vol_display_name if vol_display_name else lid + + if ntriangles <= 0: + out = [] + out.append(f' TGeoBBox *solid_{safe} = new TGeoBBox("{shape_name}", 0.001, 0.001, 0.001);') + out.append(f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});') + return "\n".join(out) + + loader = "LoadFacetsO2" if solid_class != "TGeoTessellated" else "LoadFacets" + out = [] + out.append(f' {solid_class} *solid_{safe} = new {solid_class}("{shape_name}", {ntriangles});') + out.append(f' {loader}("{facet_abspath}", solid_{safe}, check);') + out.append(f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});') + return "\n".join(out) + + +def emit_surface_solid_cpp(lid: str, vol_display_name: str, surface_abspath: str, medium_var: str) -> str: + """Exact-surface counterpart of emit_tessellated_cpp: the volume gets an + O2BVHSurfaceSolid filled from a surface sidecar file. Requires + emit_cpp_prelude(exact_surfaces=True).""" + safe = sanitize_cpp_name(lid) + shape_name = vol_display_name if vol_display_name else lid + + out = [] + out.append(f' auto *solid_{safe} = new o2::cad::O2BVHSurfaceSolid("{shape_name}");') + out.append(f' LoadSurfaces("{surface_abspath}", solid_{safe}, check);') + out.append(f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});') + return "\n".join(out) + + +def emit_assembly_cpp(lid: str, asm_display_name: str) -> str: + safe = sanitize_cpp_name(lid) + name = asm_display_name if asm_display_name else lid + return f' TGeoVolumeAssembly *asm_{safe} = new TGeoVolumeAssembly("{name}");' + + +# ------------------------------- +# CAD clipping helpers +# ------------------------------- + +def make_clip_box_shape(clip_box: ClipBox): + return BRepPrimAPI_MakeBox( + gp_Pnt(clip_box.xmin, clip_box.ymin, clip_box.zmin), + gp_Pnt(clip_box.xmax, clip_box.ymax, clip_box.zmax), + ).Shape() + + +def _compose_trsf(parent_to_world: gp_Trsf, local_to_parent: gp_Trsf) -> gp_Trsf: + return parent_to_world.Multiplied(local_to_parent) + + +def _shape_is_empty(shape) -> bool: + if shape is None: + return True + try: + if shape.IsNull(): + return True + except Exception: + pass + try: + for _ in TopologyExplorer(shape).faces(): + return False + return True + except Exception: + return False + + +def _transformed_bbox(shape, trsf: gp_Trsf) -> Optional[Tuple[float, float, float, float, float, float]]: + box = Bnd_Box() + brepbndlib.Add(shape, box) + try: + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + except Exception: + return None + + points = [] + for x in (xmin, xmax): + for y in (ymin, ymax): + for z in (zmin, zmax): + p = gp_Pnt(x, y, z) + p.Transform(trsf) + points.append((p.X(), p.Y(), p.Z())) + + return ( + min(p[0] for p in points), + min(p[1] for p in points), + min(p[2] for p in points), + max(p[0] for p in points), + max(p[1] for p in points), + max(p[2] for p in points), + ) + + +def _bbox_outside_clip_box(bbox: Tuple[float, float, float, float, float, float], clip_box: ClipBox) -> bool: + xmin, ymin, zmin, xmax, ymax, zmax = bbox + return ( + xmax < clip_box.xmin or xmin > clip_box.xmax or + ymax < clip_box.ymin or ymin > clip_box.ymax or + zmax < clip_box.zmin or zmin > clip_box.zmax + ) + + +def _bbox_inside_clip_box(bbox: Tuple[float, float, float, float, float, float], clip_box: ClipBox) -> bool: + xmin, ymin, zmin, xmax, ymax, zmax = bbox + return ( + xmin >= clip_box.xmin and xmax <= clip_box.xmax and + ymin >= clip_box.ymin and ymax <= clip_box.ymax and + zmin >= clip_box.zmin and zmax <= clip_box.zmax + ) + + +def _classify_shape_against_clip_box(shape, clip_box: ClipBox, local_to_world: gp_Trsf) -> Optional[str]: + world_bbox = _transformed_bbox(shape, local_to_world) + if world_bbox is None: + return None + if _bbox_outside_clip_box(world_bbox, clip_box): + return "outside" + if _bbox_inside_clip_box(world_bbox, clip_box): + return "inside" + return "overlap" + + +def clip_shape_to_box(shape, clip_box: ClipBox, clip_box_shape, local_to_world: gp_Trsf, lid: str): + clip_state = _classify_shape_against_clip_box(shape, clip_box, local_to_world) + if clip_state is None: + return None + if clip_state == "outside": + return None + if clip_state == "inside": + return shape + + local_clip = BRepBuilderAPI_Transform(clip_box_shape, local_to_world.Inverted(), True).Shape() + common = BRepAlgoAPI_Common(shape, local_clip) + common.Build() + if not common.IsDone(): + raise RuntimeError(f"Failed to clip CAD shape {lid} against --clip-box") + + clipped = common.Shape() + if _shape_is_empty(clipped): + return None + return clipped + + +# ------------------------------- +# Definition graph extraction +# ------------------------------- + +logical_volumes: Dict[str, list] = {} # def_lid -> triangles +def_names: Dict[str, str] = {} # def_lid -> human display name (may be "") +def_volume_source: Dict[str, object] = {} # def_lid -> unclipped leaf shape, for the BOM volume +def_shapes: Dict[str, object] = {} # def_lid -> (possibly clipped) TopoDS shape (leaf only) +assemblies = set() # def_lid +placements = [] # (parent_def_lid, child_def_lid, gp_Trsf local) +top_defs = set() # top definition lids +visited_defs = set() # expanded defs + + +def reset_graph() -> None: + """Clear the definition graph. One place, so `extract_graph` and the self-test agree.""" + global logical_volumes, def_names, def_volume_source, def_shapes, assemblies, placements, top_defs, visited_defs + logical_volumes = {} + def_names = {} + def_volume_source = {} + def_shapes = {} + assemblies = set() + placements = [] + top_defs = set() + visited_defs = set() + + +def cpp_var_for_def(lid: str) -> str: + safe = sanitize_cpp_name(lid) + return f"asm_{safe}" if lid in assemblies else f"vol_{safe}" + + +def solid_bodies_of(shape) -> list: + """The TopoDS_Solid bodies a shape carries, each with its own location already baked in.""" + if shape is None: + return [] + try: + if shape.IsNull(): + return [] + except Exception: + return [] + out = [] + exp = TopExp_Explorer(shape, TopAbs_SOLID) + while exp.More(): + out.append(topods.Solid(exp.Current())) + exp.Next() + return out + + +def _register_leaf_shape(def_key: str, shape, meshparam, scale_to_cm: float, + clip_enabled: bool, clip_box, clip_box_shape, + world_trsf, def_lid: str) -> bool: + """Record one leaf logical volume: its unclipped shape, its shape and its triangles. + + Returns False when clipping removed the shape entirely, in which case nothing is recorded. + """ + source = shape + if clip_enabled: + shape = clip_shape_to_box(shape, clip_box, clip_box_shape, world_trsf, def_lid) + if shape is None: + return False + + def_volume_source[def_key] = source + def_shapes[def_key] = shape + + do_meshing = (meshparam is not None) and meshparam.get("do_meshing", None) is True + logical_volumes[def_key] = (triangulate_CAD_solid(shape, meshparam=meshparam, scale_to_cm=scale_to_cm) + if do_meshing else triangulate_asbbox(shape, scale_to_cm=scale_to_cm)) + return True + + +def expand_definition( + def_label: TDF_Label, + shape_tool, + meshparam=None, + scale_to_cm: float = 1.0, + clip_box: Optional[ClipBox] = None, + clip_box_shape=None, + clip_deduplicate: str = "intact", + name_filter: Optional[NameFilter] = None, + include_subtree: bool = False, + world_trsf: Optional[gp_Trsf] = None, + occ_path: str = "r1", +) -> Optional[str]: + clip_enabled = clip_box_shape is not None + if world_trsf is None: + world_trsf = gp_Trsf() + + def_lid = label_id(def_label) + nm = label_name(def_label) + + subtree_included = include_subtree + if name_filter is not None: + if name_filter.matches_exclude(def_lid, nm): + return None + if name_filter.has_include and name_filter.matches_include(def_lid, nm): + subtree_included = True + + if clip_enabled and clip_box is not None: + try: + shape_for_clip = shape_tool.GetShape(def_label) + except Exception: + shape_for_clip = None + if shape_for_clip is not None: + clip_state = _classify_shape_against_clip_box(shape_for_clip, clip_box, world_trsf) + if clip_state == "outside": + return None + if clip_state == "inside" and clip_deduplicate == "intact": + return expand_definition( + def_label, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=None, + clip_box_shape=None, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + include_subtree=subtree_included, + ) + + def_key = f"{def_lid}@{occ_path}" if clip_enabled else def_lid + if not clip_enabled and def_lid in visited_defs: + return def_lid + if not clip_enabled: + visited_defs.add(def_lid) + + if nm and def_key not in def_names: + def_names[def_key] = nm + elif def_key not in def_names: + def_names[def_key] = "" + + children = TDF_LabelSequence() + shape_tool.GetComponents(def_label, children) + has_children = children.Length() > 0 + + if has_children or shape_tool.IsAssembly(def_label): + assemblies.add(def_key) + kept_children = 0 + + for i in range(children.Length()): + child = children.Value(i + 1) + child_occ_path = f"{occ_path}_{i + 1}" + if shape_tool.IsReference(child): + referred = TDF_Label() + shape_tool.GetReferredShape(child, referred) + + loc = shape_tool.GetLocation(child) + trsf = loc.Transformation() + if clip_enabled: + child_key = expand_definition( + referred, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=clip_box, + clip_box_shape=clip_box_shape, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + include_subtree=subtree_included, + world_trsf=_compose_trsf(world_trsf, trsf), + occ_path=child_occ_path, + ) + if child_key is None: + continue + placements.append((def_key, child_key, trsf)) + else: + child_key = expand_definition( + referred, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + include_subtree=subtree_included, + ) + if child_key is None: + continue + placements.append((def_key, child_key, trsf)) + else: + trsf = gp_Trsf() + if clip_enabled: + child_key = expand_definition( + child, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=clip_box, + clip_box_shape=clip_box_shape, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + include_subtree=subtree_included, + world_trsf=world_trsf, + occ_path=child_occ_path, + ) + if child_key is None: + continue + placements.append((def_key, child_key, trsf)) + else: + child_key = expand_definition( + child, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + include_subtree=subtree_included, + ) + if child_key is None: + continue + placements.append((def_key, child_key, trsf)) + kept_children += 1 + + if (clip_enabled or (name_filter is not None and name_filter.has_include)) and kept_children == 0: + assemblies.discard(def_key) + return None + return def_key + + if shape_tool.IsSimpleShape(def_label): + if name_filter is not None and name_filter.has_include and not subtree_included: + return None + + if def_key in logical_volumes or def_key in assemblies: + return def_key + + shape = shape_tool.GetShape(def_label) + bodies = solid_bodies_of(shape) + + # A leaf label may hold several bodies; each becomes a volume the label places once. + if len(bodies) > 1: + assemblies.add(def_key) + kept_bodies = 0 + for i, body in enumerate(bodies): + body_key = f"{def_key}#b{i + 1}" + if body_key not in def_names: + def_names[body_key] = nm + if _register_leaf_shape(body_key, body, meshparam, scale_to_cm, + clip_enabled, clip_box, clip_box_shape, + world_trsf, def_lid): + placements.append((def_key, body_key, gp_Trsf())) + kept_bodies += 1 + if kept_bodies == 0: + assemblies.discard(def_key) + return None + return def_key + + if not bodies and _shape_is_empty(shape): + print(f"WARNING: CAD leaf {def_lid} ('{nm}') carries no geometry at all " + f"(empty compound); skipping it.") + return None + + if not _register_leaf_shape(def_key, shape, meshparam, scale_to_cm, + clip_enabled, clip_box, clip_box_shape, + world_trsf, def_lid): + return None + return def_key + + assemblies.add(def_key) + return def_key + + +# ------------------------------- +# Coincident placements: one definition, one world transform, ONE placement +# ------------------------------- +# +# The rule keys on the (definition, world transform) pair only: two placements of a definition at +# different transforms are instancing, and both stay. + +_PLACEMENT_SIG_DIGITS = 9 + + +def trsf_signature(trsf: gp_Trsf, ndigits: int = _PLACEMENT_SIG_DIGITS) -> tuple: + """A hashable stand-in for a world transform: the 12 matrix entries, rounded. + + Rounding can only cost a missed duplicate, which leaves geometry where the CAD put it; two + distinct placements are never within 1e-9 model units. + """ + return tuple(round(trsf.Value(r, c), ndigits) for r in range(1, 4) for c in range(1, 5)) + + +_IDENTITY_TRSF_SIG = trsf_signature(gp_Trsf()) + + +def _placement_children(placements_list) -> Dict[str, List[tuple]]: + """parent def key -> [(edge index, child def key, local transform), ...], in emission order.""" + kids: Dict[str, List[tuple]] = {} + for idx, (parent, child, trsf) in enumerate(placements_list): + kids.setdefault(parent, []).append((idx, child, trsf)) + return kids + + +def enumerate_occurrences(placements_list, tops, suppressed=frozenset(), limit=8_000_000): + """Every occurrence the geometry would contain, WITH multiplicity (independent of the dedup). + + Returns a list of (def_key, world transform signature) in depth-first order. + """ + kids = _placement_children(placements_list) + out: List[tuple] = [] + stack = [(top, gp_Trsf()) for top in sorted(tops, reverse=True)] + while stack: + key, world = stack.pop() + out.append((key, trsf_signature(world))) + if len(out) > limit: + raise RuntimeError( + f"assembly graph expands past {limit} occurrences; it is probably cyclic") + for idx, child, trsf in reversed(kids.get(key, ())): + if idx in suppressed: + continue + stack.append((child, _compose_trsf(world, trsf))) + return out + + +def _walk_distinct_occurrences(kids, tops, suppressed): + """Depth-first over DISTINCT (def_key, world signature) occurrences. + + Returns (seen, discoverer); marking at visit time keeps the deep structure, not a flat root. + """ + seen: Dict[tuple, gp_Trsf] = {} + discoverer: Dict[tuple, int] = {} + stack = [(top, gp_Trsf(), _IDENTITY_TRSF_SIG, -1) for top in sorted(tops, reverse=True)] + while stack: + key, world, sig, via = stack.pop() + if (key, sig) in seen: + continue + seen[(key, sig)] = world + if via >= 0: + discoverer[(key, sig)] = via + for idx, child, trsf in reversed(kids.get(key, ())): + if idx in suppressed: + continue + cworld = _compose_trsf(world, trsf) + stack.append((child, cworld, trsf_signature(cworld), idx)) + return seen, discoverer + + +def _occurrences_below(kids, start_def, start_world, suppressed) -> set: + """Every (def_key, world signature) placed strictly BELOW this occurrence.""" + visited = set() + stack = [(start_def, start_world, True)] + while stack: + key, world, is_start = stack.pop() + if not is_start: + sig = trsf_signature(world) + if (key, sig) in visited: + continue + visited.add((key, sig)) + for idx, child, trsf in kids.get(key, ()): + if idx in suppressed: + continue + stack.append((child, _compose_trsf(world, trsf), False)) + return visited + + +def deduplicate_placements(placements_list, tops, leaf_keys): + """Suppress the placement edges that would build one definition twice in the same place. + + Rule 1 drops a root child that a sibling root child already places at the same transform. + Rule 2 drops an edge only when EVERY one of its occurrences coincides with another edge's; a + partly coincident edge is reported and kept. + + Returns (kept placements, report dict, emitted leaf occurrences). + """ + kids = _placement_children(placements_list) + suppressed: set = set() + by_rule: Dict[str, List[int]] = {"root-containment": [], "coincident-occurrence": []} + + # --- rule 1: a root child that another root child already contains ----------------------- + for top in sorted(tops): + siblings = kids.get(top, ()) + holders: Dict[tuple, List[int]] = {} # occurrence strictly below sibling p -> [p] + for p, (_idx, child, trsf) in enumerate(siblings): + for occ in _occurrences_below(kids, child, trsf, suppressed): + holders.setdefault(occ, []).append(p) + for jp, (jdx, jchild, jtrsf) in enumerate(siblings): + owners = holders.get((jchild, trsf_signature(jtrsf)), ()) + if any(p != jp and siblings[p][0] not in suppressed for p in owners): + suppressed.add(jdx) + by_rule["root-containment"].append(jdx) + + # --- rule 2: whatever is left that is still coincident, to a fixed point ------------------ + partial: Dict[int, tuple] = {} + for _ in range(64): + seen, discoverer = _walk_distinct_occurrences(kids, tops, suppressed) + total = [0] * len(placements_list) + for (key, _sig), world in seen.items(): + for idx, _child, _trsf in kids.get(key, ()): + if idx not in suppressed: + total[idx] += 1 + kept = [0] * len(placements_list) + for idx in discoverer.values(): + kept[idx] += 1 + newly, partial = set(), {} + for idx in range(len(placements_list)): + if idx in suppressed or total[idx] == 0: + continue + if kept[idx] == 0: + newly.add(idx) + elif kept[idx] < total[idx]: + partial[idx] = (kept[idx], total[idx]) + if not newly: + break + suppressed |= newly + by_rule["coincident-occurrence"].extend(sorted(newly)) + else: # pragma: no cover - pathological + raise RuntimeError("coincident-placement de-duplication did not converge") + + declared = [occ for occ in enumerate_occurrences(placements_list, tops) if occ[0] in leaf_keys] + emitted = [occ for occ in enumerate_occurrences(placements_list, tops, suppressed) + if occ[0] in leaf_keys] + kept_placements = [p for idx, p in enumerate(placements_list) if idx not in suppressed] + report = { + "declared_leaf_placements": len(declared), + "distinct_leaf_placements": len(set(declared)), + "emitted_leaf_placements": len(emitted), + "declared_multiplicity": dict(sorted(Counter(Counter(declared).values()).items())), + "emitted_multiplicity": dict(sorted(Counter(Counter(emitted).values()).items())), + "suppressed_edges": [(placements_list[i][0], placements_list[i][1], rule) + for rule, idxs in by_rule.items() for i in sorted(idxs)], + "n_suppressed_by_rule": {rule: len(idxs) for rule, idxs in by_rule.items()}, + "partial_edges": [(placements_list[i][0], placements_list[i][1]) + v + for i, v in sorted(partial.items())], + } + return kept_placements, report, emitted + + +def report_duplicate_placements(report: dict, names: Optional[Dict[str, str]] = None) -> None: + """Say it out loud, every run. A model that declares coincident duplicates is telling us + something about the CAD, and silence here would hide the next one.""" + names = names or {} + n_sup = sum(report["n_suppressed_by_rule"].values()) + declared, distinct = report["declared_leaf_placements"], report["distinct_leaf_placements"] + if n_sup == 0 and declared == distinct: + print(f"Placement check: {declared} leaf placement(s), all at distinct world transforms.") + return + + print(f"WARNING: this CAD model DECLARES {declared - distinct} leaf solid placement(s) that " + f"coincide exactly with another placement of the same solid.") + print(f" The assembly structure in the file says so -- these are not an artefact of this " + f"traversal, which walks the STEP product structure edge for edge.") + print(f" Leaf placements: {declared} declared " + f"(multiplicity {report['declared_multiplicity']}) -> {distinct} distinct.") + print(f" Suppressed {n_sup} placement edge(s) so that no definition is built twice at the " + f"same world transform " + f"({', '.join(f'{n} by {rule}' for rule, n in report['n_suppressed_by_rule'].items())}):") + for parent, child, rule in report["suppressed_edges"]: + pn, cn = names.get(parent, "") or parent, names.get(child, "") or child + print(f" dropped {pn} -> {cn} [{rule}]") + print(f" Emitting {report['emitted_leaf_placements']} leaf placement(s) " + f"(multiplicity {report['emitted_multiplicity']}).") + for parent, child, kept, total in report["partial_edges"]: + pn, cn = names.get(parent, "") or parent, names.get(child, "") or child + print(f" WARNING: {pn} -> {cn} is coincident for {total - kept} of its {total} instances " + f"and NOT suppressed: the placement graph is keyed by definition, so dropping it " + f"would delete the {kept} instance(s) that are needed.") + + +def verify_placement_invariant(placements_list, tops, leaf_keys, occurrences=None) -> dict: + """The permanent check: leaf placements in == out, and no two share definition and transform. + + Raises rather than warns. `occurrences` is the leaf occurrence list when the caller has it. + """ + occ = (occurrences if occurrences is not None else + [o for o in enumerate_occurrences(placements_list, tops) if o[0] in leaf_keys]) + multiplicity = Counter(Counter(occ).values()) + if set(multiplicity) - {1}: + worst = Counter(occ).most_common(1)[0] + raise RuntimeError( + f"placement invariant violated: {len(occ)} leaf placements hold only " + f"{len(set(occ))} distinct (definition, world transform) pairs " + f"(multiplicity {dict(sorted(multiplicity.items()))}); e.g. {worst[0][0]} is placed " + f"{worst[1]} times at the same world matrix") + return {"leaf_placements": len(occ), "multiplicity": dict(sorted(multiplicity.items()))} + + +def extract_graph( + step_path: str, + meshparam=None, + scale_to_cm: float = 1.0, + clip_box: Optional[ClipBox] = None, + clip_deduplicate: str = "intact", + name_filter: Optional[NameFilter] = None, +): + reset_graph() + doc, shape_tool = load_step_with_xcaf(step_path) + expand_free_shapes( + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=clip_box, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + ) + return doc, shape_tool + + +def expand_free_shapes( + shape_tool, + meshparam=None, + scale_to_cm: float = 1.0, + clip_box: Optional[ClipBox] = None, + clip_deduplicate: str = "intact", + name_filter: Optional[NameFilter] = None, +): + """Expand every XCAF free shape into the definition graph, then make the placements unique.""" + global placements + clip_box_shape = make_clip_box_shape(clip_box) if clip_box is not None else None + + roots = TDF_LabelSequence() + shape_tool.GetFreeShapes(roots) + + for i in range(roots.Length()): + root = roots.Value(i + 1) + root_occ_path = f"r{i + 1}" + if shape_tool.IsReference(root): + ref = TDF_Label() + shape_tool.GetReferredShape(root, ref) + root = ref + top = expand_definition( + root, + shape_tool, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=clip_box, + clip_box_shape=clip_box_shape, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + occ_path=root_occ_path, + ) + if top is not None: + top_defs.add(top) + + placements, dup_report, emitted = deduplicate_placements(placements, top_defs, + set(logical_volumes)) + report_duplicate_placements(dup_report, def_names) + verify_placement_invariant(placements, top_defs, set(logical_volumes), emitted) + return dup_report + + +# ------------------------------- +# ROOT macro emission +# ------------------------------- + +def emit_nested_placement_cpp(body_def: str, child_def: str, trsf: gp_Trsf, copy_no: int, + scale_to_cm: float, csg_lids: Optional[set] = None) -> str: + """One `AddNode` of a daughter INTO its mother's body volume, not beside it. + + A TGeo daughter takes precedence over its mother's solid, so this restores the source nesting + with no boolean. A child at T in the assembly frame is at `P^-1 * T` in the body volume's frame. + """ + body_cpp = cpp_var_for_def(body_def) + child_cpp = cpp_var_for_def(child_def) + tr_name = f"trn_{sanitize_cpp_name(body_def)}_{sanitize_cpp_name(child_def)}_{copy_no}" + out = trsf_to_tgeo(trsf, tr_name, scale_to_cm) + node_matrix = tr_name + + hook = import_csg_hook() + if csg_lids and child_def in csg_lids: + node_matrix = f"{tr_name}_placed" + out += hook.emit_csg_composed_placement_cpp( + tr_name, hook.csg_placement_var(child_def, sanitize_cpp_name), node_matrix) + "\n" + if csg_lids and body_def in csg_lids: + inv = f"{tr_name}_inbody" + pvar = hook.csg_placement_var(body_def, sanitize_cpp_name) + out += (f" TGeoHMatrix *{inv} = new TGeoHMatrix({pvar}->Inverse());\n" + f" {inv}->Multiply({node_matrix});\n") + node_matrix = inv + return out + f" {body_cpp}->AddNode({child_cpp}, {copy_no}, {node_matrix});\n" + + +def emit_placement_cpp(parent_def: str, child_def: str, trsf: gp_Trsf, copy_no: int, scale_to_cm: float, + csg_lids: Optional[set] = None) -> str: + """One `AddNode`, with the child's own shape placement composed in when it has one. + + The node matrix is `partPlacement * shapePlacement`, emitted for every placement of the child. + """ + parent_cpp = cpp_var_for_def(parent_def) + child_cpp = cpp_var_for_def(child_def) + tr_name = f"tr_{sanitize_cpp_name(parent_def)}_{sanitize_cpp_name(child_def)}_{copy_no}" + out = trsf_to_tgeo(trsf, tr_name, scale_to_cm) + node_matrix = tr_name + if csg_lids and child_def in csg_lids: + hook = import_csg_hook() + node_matrix = f"{tr_name}_placed" + out += hook.emit_csg_composed_placement_cpp( + tr_name, hook.csg_placement_var(child_def, sanitize_cpp_name), node_matrix) + "\n" + return out + f" {parent_cpp}->AddNode({child_cpp}, {copy_no}, {node_matrix});\n" + + + +def _compute_density_g_cm3( + volume_cm3: float, + mass_value: float, + mass_unit: str, +) -> Tuple[Optional[float], str]: + """ + Computes an effective part density from (mass, CAD volume). + + Returns (rho_g_cm3 or None, comment). If rho is None, caller should fall back + to the Geant4 NIST density (if resolved) or to a dummy density. + """ + if not volume_cm3 or volume_cm3 <= 0: + return None, "no CAD volume available for density" + + if (mass_value is None) or (isinstance(mass_value, float) and math.isnan(mass_value)): + return None, "no BOM mass available for density" + + mass_g = float(mass_value) + mu = (mass_unit or "kg").lower() + if mu == "kg": + mass_g *= 1000.0 + elif mu == "g": + pass + else: + # unknown unit: assume kg + mass_g *= 1000.0 + + rho = mass_g / float(volume_cm3) + # Guard against obvious unit/volume issues + if not (0.01 < rho < 50.0): + return None, f"computed density {rho:.3g} g/cm3 rejected (unit mismatch?)" + + return rho, "density from BOM mass and CAD volume" + + +def emit_root_macro( + step_path: str, + out_folder: _Path, + meshparam=None, + step_unit: str = "auto", + clip_box: Optional[ClipBox] = None, + clip_deduplicate: str = "intact", + name_filter: Optional[NameFilter] = None, + materials_csv: Optional[str] = None, + media_json: Optional[str] = None, + in_field: Optional[Tuple[float, float]] = None, + bom_mass_unit: str = "kg", + g4_nist_json: Optional[str] = None, + mat_cfg: Optional[MatMatchConfig] = None, + surface_report: Optional[str] = None, + exact_surfaces: str = "off", + recognize_surfaces: str = "exact", + dump_brep: bool = False, + csg: str = "off", + csg_report: Optional[str] = None, + max_cells: Optional[int] = None, + max_splits: Optional[int] = None, + decompose_timeout: Optional[float] = None, + mesh_solid: str = "o2", +): + # exact_surfaces mode: + # off : tessellated output only (default; leaves generated output unchanged). + # auto : emit O2BVHSurfaceSolid for every leaf solid whose faces all extract + # exactly, tessellated fallback otherwise. + # required : like auto, but abort if any leaf solid cannot be represented exactly. + # + # dump_brep: also write brep__.brep, scaled to cm, next to each surfaces_*.bin. + if (step_unit or "auto").lower() == "auto": + detected = detect_step_length_unit(step_path) + scale_to_cm = step_unit_scale_to_cm(detected) + print(f"Detected STEP length unit: {detected} (scale to cm = {scale_to_cm})") + else: + scale_to_cm = step_unit_scale_to_cm(step_unit) + print(f"Using overridden STEP length unit: {step_unit} (scale to cm = {scale_to_cm})") + + if clip_box is not None: + print(f"Clipping CAD geometry to STEP-coordinate bounding box: {clip_box.as_tuple()}") + print(f"Clip deduplication mode: {clip_deduplicate}") + + if name_filter is not None and name_filter.active: + print(f"CAD name filters: {len(name_filter.include)} include regex(es), {len(name_filter.exclude)} exclude regex(es)") + + extract_graph( + step_path, + meshparam=meshparam, + scale_to_cm=scale_to_cm, + clip_box=clip_box, + clip_deduplicate=clip_deduplicate, + name_filter=name_filter, + ) + + out_folder = out_folder.expanduser().resolve() + out_folder.mkdir(parents=True, exist_ok=True) + + recognize_mode = (recognize_surfaces or "exact").lower() + recognize_flag = recognize_mode == "exact" + + # --- optional exact-surface eligibility report (does not modify the emitted geometry) --- + surface_report_data = None + surface_report_path = None + recognition: Dict[tuple, Optional[dict]] = {} # (lid, face index) -> recognizer result + if surface_report: + surface_report_data = build_surface_report(step_path, scale_to_cm, + recognize_surfaces=recognize_flag, + recognition=recognition) + surface_report_path = _Path(surface_report).expanduser().resolve() + surface_report_path.parent.mkdir(parents=True, exist_ok=True) + surface_report_path.write_text(json.dumps(surface_report_data, indent=1)) + summ = surface_report_data["summary"] + print(f"Surface report: {summ['n_eligible']}/{summ['n_volumes']} logical volumes eligible " + f"for exact O2BVHSurfaceSolid conversion") + print(f" face types: {summ['face_type_counts']}") + if summ["recognized_surface_counts"]: + print(f" recognized (stored type is not the geometry): {summ['recognized_surface_counts']}" + f" recovered from stored {summ['recognized_stored_type_counts']}") + if summ["fallback_reasons"]: + top = sorted(summ["fallback_reasons"].items(), key=lambda kv: -kv[1])[:5] + for reason, count in top: + print(f" fallback ({count}x): {reason}") + print(f"Wrote surface report: {surface_report_path}") + + # --- exact-surface extraction (auto/required modes) --- + exact_mode = (exact_surfaces or "off").lower() + scaled_shapes: Dict[str, object] = {} # def_lid -> the cm copy written for --dump-brep + surface_files: Dict[str, str] = {} # def_lid -> absolute path of its surfaces_*.bin + if exact_mode != "off": + brep_files: Dict[str, str] = {} # def_lid -> absolute path of brep_*.brep (--dump-brep) + failures: Dict[str, List[str]] = {} # def_lid -> unsupported-face reasons + extracted: Dict[str, int] = {} # def_lid -> number of surface records written + for lid, shape in def_shapes.items(): + surfaces, reasons, n_model_edges = extract_surfaces_for_shape( + shape, scale_to_cm, recognize_surfaces=recognize_flag, recognition=recognition, + lid=lid) + if surfaces is None: + failures[lid] = reasons + continue + extracted[lid] = len(surfaces) + disp = def_names.get(lid, "") + volname = sanitize_filename(disp) if disp else "vol" + name_suffix = f"{volname}_{sanitize_filename(lid)}" + fpath = (out_folder / f"surfaces_{name_suffix}.bin").resolve() + write_surfaces_bin(fpath, surfaces, accept.model_tolerance_cm(shape) * scale_to_cm, + n_model_edges) + surface_files[lid] = str(fpath) + if dump_brep: + bpath = (out_folder / f"brep_{name_suffix}.brep").resolve() + scaled_shapes[lid] = write_brep_cm(bpath, shape, scale_to_cm) + brep_files[lid] = str(bpath) + if dump_brep: + print(f"Wrote {len(brep_files)} reference BREP file(s) (brep_*.brep, scaled to cm)") + n_leaf = len(def_shapes) + print(f"Exact-surface extraction ({exact_mode}): {len(surface_files)}/{n_leaf} leaf solids " + f"represented exactly, {len(failures)} fall back to tessellation") + reason_counts: Dict[str, int] = {} + if failures: + # Aggregate reasons for a compact, useful report. + for reasons in failures.values(): + for r in reasons: + reason_counts[r] = reason_counts.get(r, 0) + 1 + for reason, count in sorted(reason_counts.items(), key=lambda kv: -kv[1]): + print(f" fallback ({count} face(s)): {reason}") + if exact_mode == "required": + lines = [f"--exact-surfaces required: {len(failures)}/{n_leaf} leaf solid(s) cannot be " + f"represented exactly:"] + for lid in sorted(failures): + name = def_names.get(lid, "") or lid + uniq = sorted(set(failures[lid])) + lines.append(f" {name} [{lid}]: {'; '.join(uniq)}") + raise ValueError("\n".join(lines)) + + # `eligible` is a claim about surfaces only; `emitted` is what extraction actually did. + if surface_report_data is not None: + n_emitted_rescued = 0 + for lid, vol in surface_report_data["volumes"].items(): + emitted_here = lid in extracted + vol["emitted"] = emitted_here + if not emitted_here: + vol["extraction_reasons"] = sorted(set(failures.get(lid, []))) + # The extractor's verdict supersedes the classification pass's optimistic + # one: this is the reason the sidecar was actually not written. + vol["why_not_surface"] = (distill_reasons(failures.get(lid, [])) + or vol.get("why_not_surface") + or "no sidecar was written") + else: + # The part has a sidecar; whatever the classification pass guessed, there + # is no "why not". + vol["why_not_surface"] = None + if vol["recognized_counts"]: + n_emitted_rescued += 1 + summary = surface_report_data["summary"] + summary["n_emitted"] = len(extracted) + summary["n_emitted_carrying_recognized_faces"] = n_emitted_rescued + summary["n_eligible_but_not_emitted"] = sum( + 1 for v in surface_report_data["volumes"].values() + if v["eligible"] and not v["emitted"]) + summary["extraction_fallback_reasons"] = reason_counts if failures else {} + surface_report_path.write_text(json.dumps(surface_report_data, indent=1)) + print(f" emitted {summary['n_emitted']}/{summary['n_volumes']}; " + f"{summary['n_eligible_but_not_emitted']} surface-eligible solid(s) declined at " + f"extraction; {summary['n_emitted_carrying_recognized_faces']} emitted solid(s) " + f"carry recognized faces") + + # --- CSG recognition (--csg auto|required) -- the one CSG hook ------------------------ + # Only an accepted part is emitted as a native ROOT shape; every representation is still written. + csg_mode = (csg or "off").lower() + csg_files: Dict[str, str] = {} + # flatcsg_*.bin per O2FlatCSG part, disjoint from csg_files. + flat_files: Dict[str, str] = {} + if csg_mode != "off": + hook = import_csg_hook() + # The budgets live as module constants in cadsupport/decompose.py and are read at call time by + # cadsupport/recognise.py, so setting them here is enough and nothing has to be threaded. + if any(v is not None for v in (max_cells, max_splits, decompose_timeout)): + from cadsupport import decompose as _decomp + if max_cells is not None: + print(f" cell budget raised: {_decomp.PART_MAX_CELLS} -> {max_cells}") + _decomp.PART_MAX_CELLS = max_cells + if max_splits is not None: + print(f" split budget raised: {_decomp.MAX_SPLITS} -> {max_splits}") + _decomp.MAX_SPLITS = max_splits + if decompose_timeout is not None: + print(f" decomposition timeout raised: {_decomp.TIMEOUT_S} -> " + f"{decompose_timeout} s") + _decomp.TIMEOUT_S = decompose_timeout + csg_files, flat_files, csg_records = hook.recognise_and_emit( + def_shapes, def_names, scale_to_cm, out_folder, sanitize_filename, mode=csg_mode, + scaled=scaled_shapes) + csg_report_path = _Path(csg_report) if csg_report else (out_folder / "csg_report.json") + # The lid -> sidecar mapping lets write_report compute tessellation exactness. + csg_report_data = hook.write_report(csg_records, csg_report_path, dict(surface_files), + set(logical_volumes)) + hook.print_tier_table(csg_report_data) + print(f"Wrote CSG report: {csg_report_path}") + + # --- Geant4 NIST material DB (optional but recommended) --- + g4db: Optional[Dict[str, dict]] = None + if g4_nist_json: + g4db = load_g4_nist_db(g4_nist_json) + print(f"Loaded Geant4 NIST DB with {len(g4db)} materials from: {g4_nist_json}") + else: + print("No --g4-nist-json provided: unresolved materials will fall back to dummy ROOT materials.") + mat_cfg = mat_cfg or MatMatchConfig() + + + # --- BOM: map volumes to materials (heuristic) --- + lid_to_bom: Dict[str, BomEntry] = {} + if materials_csv: + bom_entries = read_bom_csv(materials_csv) + lid_to_bom = build_volume_to_material_map(bom_entries, def_names) + print(f"Loaded {len(bom_entries)} BOM entries from: {materials_csv}") + print(f"Matched {len(lid_to_bom)} CAD logical volumes to BOM entries (by name/part-number heuristics)") + else: + print("No --materials-csv provided: emitting Default medium for all logical volumes") + + # --- media sidecar: the exact media of the geometry this STEP came from --- + media_sidecar: Optional[dict] = None + if media_json: + with open(media_json) as _fh: + media_sidecar = json.load(_fh) + print(f"Loaded media sidecar: {media_sidecar.get('nMedia')} media over " + f"{media_sidecar.get('nParts')} parts from {media_json}") + + # --- facet files --- + facet_files = {} # def_lid -> absolute path string + for lid, tris in logical_volumes.items(): + disp = def_names.get(lid, "") + volname = sanitize_filename(disp) if disp else "vol" + lidname = sanitize_filename(lid) + fname = f"facets_{volname}_{lidname}.bin" + fpath = (out_folder / fname).resolve() + write_facets_bin(fpath, tris) + facet_files[lid] = str(fpath).replace("\\", "\\\\") # C++ string literal safety + + # --- which materials do we need to emit? --- + + # --- materials: collect unique BOM material strings actually used by leaf volumes --- + # We resolve each unique BOM string to a Geant4 NIST material using string + density scoring. + used_materials: Dict[str, ResolvedMaterial] = {} + + # Precompute one representative part density per BOM material (first good value wins) + mat_to_rho: Dict[str, Optional[float]] = {} + mat_to_rho_note: Dict[str, str] = {} + + for lid in logical_volumes.keys(): + if lid not in lid_to_bom: + continue + bom = lid_to_bom[lid] + mat_name = normalize_material_name(bom.material) + + if mat_name not in mat_to_rho: + rho_part, rho_note = _compute_density_g_cm3( + _leaf_volume_cm3(lid, scale_to_cm), + bom.mass_value, + bom_mass_unit, + ) + mat_to_rho[mat_name] = rho_part + mat_to_rho_note[mat_name] = rho_note + + for mat_name in sorted(mat_to_rho.keys(), key=lambda s: s.lower()): + rho_part = mat_to_rho.get(mat_name) + rm = resolve_bom_material(mat_name, rho_part, g4db, mat_cfg) + + # Fold density provenance into the note for geom.C comments + rm.note = f"{rm.note} (density: {mat_to_rho_note.get(mat_name, 'n/a')})" + + if rm.nist_name is None: + print(f"WARNING: Unresolved/ambiguous material '{mat_name}'. See FIXME in generated geom.C.") + + used_materials[mat_name] = rm + + if media_sidecar is not None: + materials_cpp, medium_var_map = emit_media_sidecar_cpp(media_sidecar) + else: + materials_cpp, medium_var_map = emit_materials_cpp(used_materials, in_field=in_field) + + # --- emit C++ macro --- + if surface_files: + print(f"Emitting {len(surface_files)}/{len(logical_volumes)} logical volumes as exact O2BVHSurfaceSolid " + f"(macro requires the ALICE O2 environment)") + + # The tessellated fallback's shape class; "tgeo" navigates as bounding boxes. + if mesh_solid not in ("o2", "tgeo"): + raise ValueError(f"mesh_solid must be 'o2' or 'tgeo', got {mesh_solid!r}") + tess_lids = [lid for lid in logical_volumes + if lid not in flat_files and lid not in csg_files and lid not in surface_files + and len(logical_volumes[lid]) > 0] + solid_class = "o2::base::O2Tessellated" if mesh_solid == "o2" else "TGeoTessellated" + if tess_lids: + if mesh_solid == "o2": + print(f"Emitting {len(tess_lids)}/{len(logical_volumes)} logical volumes as navigable " + f"o2::base::O2Tessellated (macro requires the ALICE O2 environment)") + else: + print(f" [WARN] --mesh-solid tgeo: {len(tess_lids)}/{len(logical_volumes)} logical volume(s) " + f"are emitted as ROOT TGeoTessellated, which implements no navigation of its own and " + f"inherits Contains/DistFrom*/Safety from TGeoBBox. Every one of them will be navigated " + f"as its bounding box, filled. Use --mesh-solid o2 for a geometry meant to be traversed.") + + cpp: List[str] = [] + cpp.append(emit_cpp_prelude(exact_surfaces=bool(surface_files), csg_shapes=bool(csg_files), + flat_csg_shapes=bool(flat_files), + o2_tessellated=bool(tess_lids) and mesh_solid == "o2", + in_field=in_field is not None)) + + _media_unresolved: List[Tuple[str, str]] = [] # part named a medium the sidecar lacks + _media_unnamed: List[str] = [] # part the sidecar does not name at all + cpp.append("TGeoVolume* build(bool check=true) {") + cpp.append(' if (!gGeoManager) { throw std::runtime_error("gGeoManager is null. Call build_and_export(), or create a TGeoManager yourself before calling build() directly: new TGeoManager(\\"geom\\",\\"geom\\");"); }') + cpp.append(materials_cpp) + + for lid in logical_volumes.keys(): + ntriangles = len(logical_volumes[lid]) + + # choose medium for this volume + med = "med_Default" + if media_sidecar is not None: + # The sidecar keys on the emitted STEP part name, which is exactly the + # display name the reader recovered for this definition. + part = def_names.get(lid, "") + medname = media_sidecar.get("parts", {}).get(part) + if medname: + med = medium_var_map.get(medname, "med_Default") + if med == "med_Default": + _media_unresolved.append((part, medname)) + else: + _media_unnamed.append(part) + elif lid in lid_to_bom: + mat_name = normalize_material_name(lid_to_bom[lid].material) + med = medium_var_map.get(mat_name, "med_Default") + + # The cascade, in one place: CSG, else exact surfaces, else the tessellated fallback. + if lid in flat_files: + sidecar = str(_Path(flat_files[lid]).expanduser().resolve()).replace("\\", "\\\\") + cpp.append(import_csg_hook().emit_flat_csg_shape_cpp( + lid, def_names.get(lid, ""), sidecar, med, sanitize_cpp_name)) + elif lid in csg_files: + shape_path = str(_Path(csg_files[lid]).expanduser().resolve()).replace("\\", "\\\\") + cpp.append(import_csg_hook().emit_csg_shape_cpp( + lid, def_names.get(lid, ""), shape_path, med, sanitize_cpp_name)) + elif lid in surface_files: + sidecar = str(_Path(surface_files[lid]).expanduser().resolve()).replace("\\", "\\\\") + cpp.append(emit_surface_solid_cpp(lid, def_names.get(lid, ""), sidecar, med)) + else: + cpp.append(emit_tessellated_cpp(lid, def_names.get(lid, ""), facet_files[lid], ntriangles, med, + solid_class=solid_class)) + + if media_sidecar is not None: + nvol = len(logical_volumes) + nresolved = nvol - len(_media_unnamed) - len(_media_unresolved) + print(f"Media from sidecar: {nresolved}/{nvol} volumes carry their source medium") + if _media_unnamed: + print(f" [WARN] {len(_media_unnamed)} volume(s) are not named by the sidecar " + f"and fall back to Default (transparent): {_media_unnamed[:5]}") + if _media_unresolved: + print(f" [WARN] {len(_media_unresolved)} volume(s) name a medium the sidecar " + f"does not define: {_media_unresolved[:5]}") + + for lid in sorted(assemblies): + cpp.append(emit_assembly_cpp(lid, def_names.get(lid, ""))) + + csg_lids = set(csg_files) + + # Which emitted part is the body of which assembly, from the writer's sidecar. + # Keyed by def id here, because that is what the placement edges carry. + body_of = {} # assembly def id -> its body def id + if media_sidecar is not None: + name_to_lid = {} + for _lid, _nm in def_names.items(): + if _nm: + name_to_lid.setdefault(_nm, []).append(_lid) + # A completely carved mother must NOT be nested: carving or nesting, one per mother. + carved_complete = media_sidecar.get("carvedComplete") or {} + _skipped_carved = 0 + for bodyname, asmname in (media_sidecar.get("bodyOfAssembly") or {}).items(): + if carved_complete.get(asmname): + _skipped_carved += 1 + continue + blids, alids = name_to_lid.get(bodyname, []), name_to_lid.get(asmname, []) + if len(blids) == 1 and len(alids) == 1: + body_of[alids[0]] = blids[0] + elif blids and alids: + # An ambiguous name would nest a mother's daughters into the wrong + # body, so refuse rather than guess. + print(f" [WARN] not nesting {asmname}: {len(alids)} definition(s) " + f"of that name and {len(blids)} of {bodyname}") + + if media_sidecar is not None and (media_sidecar.get("carvedComplete") or {}): + print(f"Carving: {_skipped_carved} mother(s) were carved completely and are left " + f"flat; {len(body_of)} were not and keep their nesting") + _nested = 0 + for idx, (parent, child, trsf) in enumerate(placements, start=1): + body = body_of.get(parent) + if body is not None and child != body: + cpp.append(emit_nested_placement_cpp(body, child, trsf, idx, scale_to_cm, csg_lids)) + _nested += 1 + else: + cpp.append(emit_placement_cpp(parent, child, trsf, idx, scale_to_cm, csg_lids)) + if media_sidecar is not None: + print(f"Mother nesting: {_nested} of {len(placements)} placement(s) go inside " + f"their mother's body volume ({len(body_of)} assembly/assemblies with a body)") + + # A top-level CSG volume gets a one-node assembly to carry its shape placement. + placed_tops = sorted(lid for lid in top_defs if lid in csg_lids) + if len(top_defs) == 1 and not placed_tops: + top = next(iter(top_defs)) + cpp.append(f" return {cpp_var_for_def(top)};") + else: + hook = import_csg_hook() if placed_tops else None + cpp.append(' TGeoVolumeAssembly *asm_WORLD = new TGeoVolumeAssembly("WORLD");') + for i, node in enumerate(sorted(top_defs), start=1): + if node in csg_lids: + cpp.append(f" asm_WORLD->AddNode({cpp_var_for_def(node)}, {i}, " + f"{hook.csg_placement_var(node, sanitize_cpp_name)});") + else: + cpp.append(f" asm_WORLD->AddNode({cpp_var_for_def(node)}, {i});") + cpp.append(" return asm_WORLD;") + + cpp.append("}") + + # The build_and_export driver; CheckOverlaps runs only with checkOverlaps=true. + cpp.append('void build_and_export(const char* out_root = "geom.root", bool check=true,') + cpp.append(' bool checkOverlaps=false) {') + cpp.append(' if (!gGeoManager) { new TGeoManager("geom","geom"); }') + cpp.append(' TGeoVolume* top = build(check);') + cpp.append(' gGeoManager->SetTopVolume(top);') + cpp.append(' gGeoManager->CloseGeometry();') + cpp.append(' if (checkOverlaps) { gGeoManager->CheckOverlaps(); }') + cpp.append(' gGeoManager->Export(out_root);') + cpp.append('}') + + # exports a function to get get hold of the builder function in ALICE O2 + cpp.append('std::function get_builder_hook_checked() {') + cpp.append(' return []() { return build(true); };') + cpp.append('}') + # exports a function to get get hold of the builder function in ALICE O2 + cpp.append('std::function get_builder_hook_unchecked() {') + cpp.append(' return []() { return build(false); };') + cpp.append('}') + + return "\n".join(cpp) + + +# ------------------------------- +# Geometry Tree printing (debug) +# ------------------------------- + +def traverse_print(label, shape_tool, depth=0): + indent = " " * depth + name = label.GetLabelName() + entry = label_id(label) + print(f"{indent}- {name} =>[{entry}]") + + if shape_tool.IsReference(label): + ref_label = TDF_Label() + shape_tool.GetReferredShape(label, ref_label) + traverse_print(ref_label, shape_tool, depth + 1) + return + + children = TDF_LabelSequence() + shape_tool.GetComponents(label, children) + if children.Length() > 0 or shape_tool.IsAssembly(label): + for i in range(children.Length()): + traverse_print(children.Value(i + 1), shape_tool, depth + 1) + return + + if shape_tool.IsSimpleShape(label): + shape = shape_tool.GetShape(label) + print(f"{indent} [LogicalShape id={id(shape)}]") + + +def print_geom(step_file): + print(f"Printing GEOM hierarchy for {step_file}") + doc, shape_tool = load_step_with_xcaf(step_file) + roots = TDF_LabelSequence() + shape_tool.GetFreeShapes(roots) + for i in range(roots.Length()): + traverse_print(roots.Value(i + 1), shape_tool) + + +# ------------------------------- +# CLI +# ------------------------------- + +def main(): + ap = argparse.ArgumentParser(description="Convert STEP/XCAF to ROOT TGeo macro, facets in per-volume binary files.") + ap.add_argument("step", nargs="?", help="Input STEP file (omit with --self-test)") + ap.add_argument("-o", "--out", default="geom.C", help="Output ROOT macro file name (default: geom.C)") + ap.add_argument("--output-folder", default="./", help="Output folder for macro + facet files") + ap.add_argument("--mesh", action="store_true", help="Use full BRepMesh triangulation instead of bounding boxes") + ap.add_argument("--print-tree", action="store_true", help="Just prints the geometry tree") + ap.add_argument("--mesh-prec", type=float, default=0.1, help="meshing precision. lower --> slower") + ap.add_argument("--in-field", nargs="?", const="2,10", default=None, metavar="IFIELD,FIELDM", + help="Treat this module as sitting in the magnetic field: write the eight Geant " + "medium parameters, with ifield and fieldm taken from the live field. " + "IFIELD,FIELDM applies when no field is loaded (default 2,10); step " + "control stays at the transport default. BOM/NIST material route only.") + ap.add_argument("--step-unit", default="auto", choices=["auto", "mm", "cm", "m", "in", "ft"], help="STEP length unit override (default: auto-detect); TGeo expects cm") + ap.add_argument("--clip-box", nargs=6, type=float, metavar=("XMIN", "YMIN", "ZMIN", "XMAX", "YMAX", "ZMAX"), default=None, help="Clip CAD geometry to this axis-aligned bounding box before meshing (coordinates in STEP file units, before conversion to cm)") + ap.add_argument("--clip-deduplicate", default="intact", choices=["none", "intact"], help="When clipping, reuse original logical definitions for subtrees fully inside the clip box (default: intact); use 'none' for one volume per surviving occurrence") + ap.add_argument("--include-name", action="append", default=[], help="Only convert CAD labels whose XCAF name or label entry matches this regex; may be repeated. Matching an assembly includes its subtree.") + ap.add_argument("--exclude-name", action="append", default=[], help="Skip CAD labels/subtrees whose XCAF name or label entry matches this regex; may be repeated.") + ap.add_argument("--name-filter-case-sensitive", action="store_true", help="Make --include-name/--exclude-name matching case-sensitive (default: case-insensitive)") + ap.add_argument("--surface-report", default=None, metavar="PATH", help="Write a JSON report classifying each face by analytic surface type and each logical volume by exact O2BVHSurfaceSolid conversion eligibility. Does not change the generated geometry output.") + ap.add_argument("--mesh-solid", default="o2", choices=["o2", "tgeo"], help="Shape class for the tessellated fallback. 'o2' (default): o2::base::O2Tessellated, which navigates and needs the O2 environment. 'tgeo': ROOT's TGeoTessellated, which navigates as its bounding box; only for a macro that must load outside O2.") + ap.add_argument("--exact-surfaces", default="off", choices=["off", "auto", "required"], help="Emit exact O2BVHSurfaceSolid shapes, each with a surfaces_*.bin sidecar. 'off' (default): tessellated only. 'auto': exact where every face extracts, tessellated otherwise. 'required': fail if any leaf solid cannot be exact.") + ap.add_argument("--dump-brep", action="store_true", help="With --exact-surfaces auto|required, also write brep__.brep (the leaf solid in cm) next to each surfaces_*.bin, for the OCCT reference oracle.") + ap.add_argument("--csg", default="off", choices=["off", "auto", "required"], help="Emit leaf solids recognised as native ROOT CSG shapes as shape__.root, when OCCT's symmetric-difference volume against the CAD solid is inside the model tolerance. 'off' (default). 'auto': the per-part cascade CSG -> exact surfaces -> tessellated. 'required': fail if any leaf solid is not CSG. The evidence goes to csg__.json and csg_report.json.") + ap.add_argument("--max-cells", type=int, default=None, metavar="N", + help="Raise the decomposition's per-part cell budget (default 64), so deeper " + "boolean parts can ship as O2FlatCSG.") + ap.add_argument("--max-splits", type=int, default=None, metavar="N", + help="Raise the decomposition's split budget (default 256). A raised cell " + "budget usually needs this too, since every cell costs a split.") + ap.add_argument("--decompose-timeout", type=float, default=None, metavar="S", + help="Raise the per-part decomposition timeout in seconds (default 60).") + ap.add_argument("--csg-report", default=None, metavar="PATH", help="Where to write the per-part CSG cascade report (default: csg_report.json in the output folder).") + ap.add_argument("--recognize-surfaces", default="exact", choices=["exact", "off"], help="Recover the exact plane/sphere/cylinder/cone behind a stored bspline/bezier/revolution/extrusion face. 'exact' (default): only a fit at machine precision. 'off': keep such faces tessellated. Applies to --surface-report and --exact-surfaces.") + + # BOM / material support + ap.add_argument("--materials-csv", default=None, help="BOM CSV file providing material + mass per part (optional)") + ap.add_argument("--media-json", default=None, + help="Media sidecar written by O2_TGeoToCAD.py --media-json; rebuilds the " + "source media verbatim and takes precedence over --materials-csv.") + ap.add_argument("--bom-mass-unit", default="kg", choices=["kg", "g"], help="Unit of the BOM mass column (default: kg)") + ap.add_argument("--g4-nist-json", default=None, help="Path to Geant4 NIST DB JSON dump (from nist_export_all). Enables TGeoMixture emission + RadLen/IntLen.") + + + # Material matching scoring knobs (only used if --g4-nist-json is provided) + ap.add_argument("--mat-min-score", type=float, default=0.35, help="Minimum combined score to accept a G4 NIST material match (default: 0.35)") + ap.add_argument("--mat-ambiguity-delta", type=float, default=0.05, help="If best-second < delta, treat match as ambiguous/unresolved (default: 0.05)") + ap.add_argument("--mat-w-token", type=float, default=0.75, help="Weight for token/name similarity score (default: 0.75)") + ap.add_argument("--mat-w-density", type=float, default=0.25, help="Weight for density proximity score (default: 0.25)") + ap.add_argument("--mat-max-log-density-diff", type=float, default=0.0, help="Optional hard density filter in log-space (0 disables). Example 0.8 ~ within 2.2x (default: 0.0)") + ap.add_argument("--mat-compound-penalty", type=float, default=0.25, help="Penalty for matching to oxides/carbides/etc. when BOM doesn't mention them (default: 0.25)") + + ap.add_argument("--self-test", action="store_true", help="Run the converter self-tests (no STEP file needed) and exit non-zero on any failure.") + + args = ap.parse_args() + + if args.self_test: + sys.exit(1 if (run_recognition_self_test() + run_placement_self_test() + + run_planar_trim_self_test() + + run_duplicate_placement_self_test() + + run_multibody_leaf_self_test() + + run_in_field_media_self_test() + + run_bom_token_self_test()) else 0) + if args.step is None: + ap.error("the following arguments are required: step (or pass --self-test)") + + step_path = str(_Path(args.step).expanduser().resolve()) + if args.print_tree: + print_geom(step_path) + return + + out_folder = _Path(args.output_folder) + + clip_box = None + if args.clip_box is not None: + try: + clip_box = ClipBox.from_values(args.clip_box) + except ValueError as exc: + ap.error(str(exc)) + + in_field = None + if args.in_field is not None: + try: + parts = [float(x) for x in str(args.in_field).split(",")] + except ValueError: + parts = [] + if len(parts) != 2: + ap.error("--in-field takes IFIELD,FIELDM (e.g. --in-field 2,10) or no value at all") + in_field = (parts[0], parts[1]) + print(f"--in-field: media take ifield/fieldm from the live field at build time " + f"(seed {in_field[0]:g},{in_field[1]:g} if none is loaded); " + "step control left at the transport default") + + name_filter = None + if args.include_name or args.exclude_name: + try: + name_filter = NameFilter.from_patterns( + args.include_name, + args.exclude_name, + case_sensitive=args.name_filter_case_sensitive, + ) + except re.error as exc: + ap.error(f"Invalid CAD name filter regex: {exc}") + + meshparam = {"do_meshing": args.mesh, "lin_defl": args.mesh_prec, "ang_defl": args.mesh_prec} + + + mat_cfg = MatMatchConfig( + min_score=args.mat_min_score, + ambiguity_delta=args.mat_ambiguity_delta, + w_token=args.mat_w_token, + w_density=args.mat_w_density, + max_log_density_diff=args.mat_max_log_density_diff, + compound_penalty=args.mat_compound_penalty, + ) + + out_folder = out_folder.expanduser().resolve() + out_folder.mkdir(parents=True, exist_ok=True) + + out_macro = (out_folder / _Path(args.out).name).resolve() + code = emit_root_macro( + step_path, + out_folder, + meshparam=meshparam, + step_unit=args.step_unit, + clip_box=clip_box, + clip_deduplicate=args.clip_deduplicate, + name_filter=name_filter, + materials_csv=args.materials_csv, + media_json=args.media_json, + in_field=in_field, + bom_mass_unit=args.bom_mass_unit, + g4_nist_json=args.g4_nist_json, + mat_cfg=mat_cfg, + surface_report=args.surface_report, + exact_surfaces=args.exact_surfaces, + recognize_surfaces=args.recognize_surfaces, + dump_brep=args.dump_brep, + csg=args.csg, + csg_report=args.csg_report, + max_cells=args.max_cells, + max_splits=args.max_splits, + decompose_timeout=args.decompose_timeout, + mesh_solid=args.mesh_solid, + ) + out_macro.write_text(code) + + print(f"Wrote ROOT macro: {out_macro}") + print(f"Wrote facet files into: {out_folder}") + print("In ROOT you can do:") + print(f" root -l {out_macro}") + print(' build_and_export("geom.root");') + + +if __name__ == "__main__": + main() diff --git a/Detectors/CADSupport/tools/O2_TGeoToCAD.py b/Detectors/CADSupport/tools/O2_TGeoToCAD.py new file mode 100755 index 0000000000000..03d154fc9c38c --- /dev/null +++ b/Detectors/CADSupport/tools/O2_TGeoToCAD.py @@ -0,0 +1,2880 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +""" +O2_TGeoToCAD.py -- TGeo -> STEP (AP214) with XCAF assembly structure. + +The inverse of `O2_CADtoTGeo.py`: it reads a ROOT geometry file (an `o2-sim` +`o2sim_geometry.root`, ideal or aligned), walks the `TGeoVolume` DAG, builds an +OCCT solid for every volume whose shape it can map, and writes one STEP file with +the assembly tree preserved. The original TGeo is then an exact oracle for the +round trip TGeo -> STEP -> `O2_CADtoTGeo.py`. + +The mapping +----------- + TGeoVolume with no daughters -> one XCAF simple shape (a definition) + TGeoVolume with daughters -> one XCAF assembly label, one component + per TGeoNode referring to the daughter's + definition, carrying the node's TGeoMatrix + TGeoVolume with daughters AND its + own (non-assembly) shape -> the above, plus one extra component + `__body` holding the mother's own + solid at the identity + TGeoVolumeAssembly -> a pure XCAF assembly, no solid + +A logical volume is converted once and referenced from every node that places it. +Definitions are keyed on volume identity, never on the name, which TGeo does not +require to be unique; two volumes share a definition when they agree by value (the +same shape, and for a mother the same placed content). A name covering several +definitions is emitted as `name`, `name#2`, ... and recorded as `nameDisambiguation`. + +Mother solids are exported uncarved, so every part is the shape the TGeo author +wrote and compares directly with `TGeoShape::Capacity()`. `--carve-mothers` +subtracts the placed daughters for a CAD-facing export. + +Units: TGeo is cm, STEP is written in mm, so every length and translation is +scaled by 10. + +Usage +----- + O2_TGeoToCAD.py INPUT.root OUTPUT.step [options] + O2_TGeoToCAD.py --self-test + + --report FILE per-volume JSON report (default: .report.json) + --top VOLNAME start from this volume instead of the TGeoManager top + --include-name PAT only convert volumes whose name matches this glob + (their ancestors are still emitted as assemblies) + --no-mother-bodies omit the `__body` component of volumes with daughters + --skip-top-body omit only the top volume's own solid (the `cave` box) + --carve-mothers subtract placed daughters from each mother solid + --dedup-world expand the tree per occurrence and drop any placement of a + volume that coincides exactly with another placement of the + same volume (see "coincident placements" below) + --no-verify skip the per-definition BRepGProp capacity check + --no-step build and report, but do not write the STEP + --quiet + +Coincident placements +--------------------- +The default export reproduces a volume placed twice at the same world transform, +which `O2_CADtoTGeo.py` refuses. `--dedup-world` expands the tree per occurrence +and drops every repeated (definition, world transform); the key is the definition, +as in `O2_CADtoTGeo.py`. + +Reflections +----------- +A STEP placement is a proper rigid motion. With Z = diag(1, 1, -1) and V^ = Z*V a +volume's mirrored prototype, a reflecting placement M of V is M*V = (M*Z)*V^ with +M*Z proper; the same identity one level down pushes a reflection through an +assembly to its leaves, so every volume has at most two prototypes. Mirrored +solids use an exact `gp_Trsf`; `gp_GTrsf` is only for a genuine non-uniform scale, +which is baked. + +Report +------ +One record per definition with {name, emittedName, mirrored, shapeClass, converted, +reason, capacity_cm3, occVolume_cm3, relDev, sharedByVolumes, ...}, a summary keyed +by shape class, `nameDisambiguation` and `sharedDefinitionMaxRelDev`. A declined +volume carries a machine-readable `reason`. +""" + +import argparse +import fnmatch +import json +import math +import os +import sys +import time + +# -------------------------------------------------------------------------- +# OCCT +# -------------------------------------------------------------------------- + +from OCC.Core.gp import ( + gp_Pnt, gp_Dir, gp_Vec, gp_XYZ, gp_Ax1, gp_Ax2, gp_Trsf, gp_GTrsf, gp_Mat, + gp_Elips, gp_Pln, +) +from OCC.Core.GC import GC_MakeArcOfCircle +from OCC.Core.TopLoc import TopLoc_Location +from OCC.Core.TopoDS import TopoDS_Compound, TopoDS_Shape +from OCC.Core.TopAbs import TopAbs_SOLID, TopAbs_FACE +from OCC.Core.TopExp import TopExp_Explorer +from OCC.Core.BRep import BRep_Builder +from OCC.Core.BRepPrimAPI import ( + BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakeCone, + BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus, BRepPrimAPI_MakeRevol, + BRepPrimAPI_MakePrism, BRepPrimAPI_MakeHalfSpace, +) +from OCC.Core.BRepBuilderAPI import ( + BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeEdge, + BRepBuilderAPI_MakeWire, BRepBuilderAPI_Transform, BRepBuilderAPI_GTransform, + BRepBuilderAPI_Sewing, BRepBuilderAPI_MakeSolid, +) +from OCC.Core.BRepFill import brepfill +from OCC.Core.TopoDS import topods +from OCC.Core.BRepOffsetAPI import BRepOffsetAPI_ThruSections +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse, BRepAlgoAPI_Common +from OCC.Core.ShapeUpgrade import ShapeUpgrade_UnifySameDomain +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.GProp import GProp_GProps +from OCC.Core.TDocStd import TDocStd_Document +from OCC.Core.TDataStd import TDataStd_Name +from OCC.Core.TDF import TDF_LabelSequence, TDF_Label +from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool +from OCC.Core.STEPCAFControl import STEPCAFControl_Writer +from OCC.Core.Interface import Interface_Static +from OCC.Core.IFSelect import IFSelect_RetDone + +SCALE_TO_MM = 10.0 # TGeo cm -> STEP mm +BOOLEAN_VOLUME_TOL = 1e-4 # relative slack on the boolean volume invariant +EPS = 1e-12 + + +class ShapeDeclined(Exception): + """A TGeo shape this mapper does not (or could not) convert. The message is + the machine-readable decline reason that lands in the report.""" + + +# -------------------------------------------------------------------------- +# small OCCT helpers +# -------------------------------------------------------------------------- + +def _moved(shape, trsf): + return BRepBuilderAPI_Transform(shape, trsf, True).Shape() + + +def _rotz(deg): + t = gp_Trsf() + t.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), math.radians(deg)) + return t + + +def _translate(dx, dy, dz): + t = gp_Trsf() + t.SetTranslation(gp_Vec(float(dx), float(dy), float(dz))) + return t + + +def _ax2(z0, phi1_deg): + ph = math.radians(phi1_deg) + return gp_Ax2(gp_Pnt(0.0, 0.0, float(z0)), gp_Dir(0, 0, 1), + gp_Dir(math.cos(ph), math.sin(ph), 0.0)) + + +def solid_volume_mm3(shape): + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return abs(props.Mass()) + + +def _has_solid(shape): + if shape is None or shape.IsNull(): + return False + return TopExp_Explorer(shape, TopAbs_SOLID).More() + + +def _check(shape, what): + if shape is None or shape.IsNull(): + raise ShapeDeclined(f"{what}: OCCT returned a null shape") + if not _has_solid(shape): + raise ShapeDeclined(f"{what}: OCCT result contains no solid") + return shape + + +def _dedupe_ring(pts, tol=1e-9): + """Drop consecutive duplicates in a closed point ring, wrap included.""" + out = [] + for p in pts: + if out and abs(p[0] - out[-1][0]) < tol and abs(p[1] - out[-1][1]) < tol: + continue + out.append(p) + while len(out) > 1 and abs(out[0][0] - out[-1][0]) < tol and abs(out[0][1] - out[-1][1]) < tol: + out.pop() + return out + + +def _revolve_profile(pts_rz, phi1_deg, dphi_deg, what): + """Revolve a closed (r, z) profile in the x>=0 half of the XZ plane about +Z. + + This is the exact route for every solid of revolution: one operation, no + booleans, and rmin > 0 comes out as a real inner face rather than a cut. + """ + pts = _dedupe_ring([(float(r), float(z)) for (r, z) in pts_rz]) + if len(pts) < 3: + raise ShapeDeclined(f"{what}: degenerate r-z profile ({len(pts)} distinct points)") + if min(p[0] for p in pts) < -1e-9: + raise ShapeDeclined(f"{what}: negative radius in profile") + poly = BRepBuilderAPI_MakePolygon() + for (r, z) in pts: + poly.Add(gp_Pnt(r, 0.0, z)) + poly.Close() + if not poly.IsDone(): + raise ShapeDeclined(f"{what}: could not build the r-z profile wire") + mf = BRepBuilderAPI_MakeFace(poly.Wire()) + if not mf.IsDone(): + raise ShapeDeclined(f"{what}: r-z profile is not a valid planar face") + rev = BRepPrimAPI_MakeRevol(mf.Face(), gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + math.radians(dphi_deg)) + rev.Build() + if not rev.IsDone(): + raise ShapeDeclined(f"{what}: revolution of the r-z profile failed") + sh = rev.Shape() + if abs(phi1_deg) > 1e-12: + sh = _moved(sh, _rotz(phi1_deg)) + return _check(sh, what) + + +def _revolve_edges(elements, phi1_deg, dphi_deg, what): + """Revolve a closed (r, z) profile made of line and arc elements about +Z. + + Elements are ("line", p1, p2) or ("arc", p1, pmid, p2), each point an (r, z) + pair in the x >= 0 half of the XZ plane. This is the sphere route: OCCT's + BRepPrimAPI_MakeSphere cuts theta with *planes* (a spherical zone), while TGeo + cuts it with *cones* through the centre (a spherical cone), so the primitive + cannot be used for a theta-sectioned sphere at all. + """ + def _p(rz): + return gp_Pnt(float(rz[0]), 0.0, float(rz[1])) + + mw = BRepBuilderAPI_MakeWire() + nedges = 0 + for e in elements: + if e[0] == "line": + p1, p2 = e[1], e[2] + if math.hypot(p1[0] - p2[0], p1[1] - p2[1]) < 1e-9: + continue + mw.Add(BRepBuilderAPI_MakeEdge(_p(p1), _p(p2)).Edge()) + else: + p1, pm, p2 = e[1], e[2], e[3] + arc = GC_MakeArcOfCircle(_p(p1), _p(pm), _p(p2)) + if not arc.IsDone(): + raise ShapeDeclined(f"{what}: could not build a profile arc") + mw.Add(BRepBuilderAPI_MakeEdge(arc.Value()).Edge()) + nedges += 1 + if nedges < 2 or not mw.IsDone(): + raise ShapeDeclined(f"{what}: could not close the r-z profile wire") + mf = BRepBuilderAPI_MakeFace(mw.Wire()) + if not mf.IsDone(): + raise ShapeDeclined(f"{what}: r-z profile is not a valid planar face") + rev = BRepPrimAPI_MakeRevol(mf.Face(), gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + math.radians(dphi_deg)) + rev.Build() + if not rev.IsDone(): + raise ShapeDeclined(f"{what}: revolution of the r-z profile failed") + sh = rev.Shape() + if abs(phi1_deg) > 1e-12: + sh = _moved(sh, _rotz(phi1_deg)) + return _check(sh, what) + + +def _signed_volume(shape): + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return props.Mass() + + +def _polygon_wire(pts, what): + poly = BRepBuilderAPI_MakePolygon() + for (x, y, z) in pts: + poly.Add(gp_Pnt(float(x), float(y), float(z))) + poly.Close() + if not poly.IsDone(): + raise ShapeDeclined(f"{what}: could not build a polygon wire") + return poly.Wire() + + +def _dedupe_ring3(pts, tol=1e-9): + out = [] + for p in pts: + if out and max(abs(p[i] - out[-1][i]) for i in range(3)) < tol: + continue + out.append(p) + while len(out) > 1 and max(abs(out[0][i] - out[-1][i]) for i in range(3)) < tol: + out.pop() + return out + + +def _quad_face(b0, b1, t1, t0, what, tol=1e-7): + """One lateral patch of a prism: a planar face when the four corners are + coplanar (so the reverse converter sees a plane), else a ruled face.""" + def sub(a, b): + return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) + + def cross(a, b): + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]) + + pts = _dedupe_ring3([b0, b1, t1, t0]) + if len(pts) < 3: + return None + # Distinct points can be collinear (a TGeoPgon z-step); a zero Newell area means no face. + nrm = [0.0, 0.0, 0.0] + for i in range(len(pts)): + a, b = pts[i], pts[(i + 1) % len(pts)] + nrm[0] += (a[1] - b[1]) * (a[2] + b[2]) + nrm[1] += (a[2] - b[2]) * (a[0] + b[0]) + nrm[2] += (a[0] - b[0]) * (a[1] + b[1]) + span = max(math.sqrt(sum((pp[i] - pts[0][i]) ** 2 for i in range(3))) for pp in pts[1:]) + if math.sqrt(sum(c * c for c in nrm)) <= tol * span * span: + return None + if len(pts) == 3: + return BRepBuilderAPI_MakeFace(_polygon_wire(pts, what)).Face() + n = cross(sub(b1, b0), sub(t0, b0)) + nn = math.sqrt(sum(c * c for c in n)) + scale = max(math.sqrt(sum(c * c for c in sub(b1, b0))), + math.sqrt(sum(c * c for c in sub(t0, b0))), 1e-30) + d = sub(t1, b0) + off = abs(sum(n[i] * d[i] for i in range(3))) / nn if nn > 0 else 0.0 + if nn > 1e-24 and off <= tol * scale: + mf = BRepBuilderAPI_MakeFace(_polygon_wire(pts, what)) + if mf.IsDone(): + return mf.Face() + e1 = BRepBuilderAPI_MakeEdge(gp_Pnt(*b0), gp_Pnt(*b1)).Edge() + e2 = BRepBuilderAPI_MakeEdge(gp_Pnt(*t0), gp_Pnt(*t1)).Edge() + return brepfill.Face(e1, e2) + + +def _prism_from_rings(outer, inner=None, what="prism"): + """Build a solid from a stack of closed sections by sewing explicit faces. + + `outer` (and the optional `inner`, which makes the caps annular) is a list of + sections, each a list of (x, y, z) with the same vertex count and order. + """ + outer = [_dedupe_ring3(r) for r in outer] + if len(outer) < 2: + raise ShapeDeclined(f"{what}: fewer than two sections") + nv = len(outer[0]) + if nv < 3 or any(len(r) != nv for r in outer): + raise ShapeDeclined( + f"{what}: sections have {sorted(set(len(r) for r in outer))} distinct " + "vertices; a prism needs the same count in every section") + rings = [outer] + if inner is not None: + inner = [_dedupe_ring3(r) for r in inner] + if any(len(r) != nv for r in inner): + raise ShapeDeclined(f"{what}: inner sections do not match the outer count") + rings.append(inner) + + faces = [] + for ring in rings: + for k in range(len(ring) - 1): + lo, hi = ring[k], ring[k + 1] + for i in range(nv): + j = (i + 1) % nv + f = _quad_face(lo[i], lo[j], hi[j], hi[i], what) + if f is not None: + faces.append(f) + # caps, annular when there is an inner stack + for idx in (0, -1): + mf = BRepBuilderAPI_MakeFace(_polygon_wire(outer[idx], what)) + if inner is not None: + mf.Add(topods.Wire(_polygon_wire(inner[idx], what).Reversed())) + if not mf.IsDone(): + raise ShapeDeclined(f"{what}: could not build a cap face") + faces.append(mf.Face()) + + ext = max(abs(c) for r in outer for p in r for c in p) or 1.0 + sew = BRepBuilderAPI_Sewing(1e-7 * ext) + for f in faces: + sew.Add(f) + sew.Perform() + shell = sew.SewedShape() + if shell is None or shell.IsNull(): + raise ShapeDeclined(f"{what}: sewing produced nothing") + try: + ms = BRepBuilderAPI_MakeSolid(topods.Shell(shell)) + ms.Build() + solid = ms.Solid() + except Exception as e: + raise ShapeDeclined(f"{what}: faces did not sew into a closed shell ({e})") + if _signed_volume(solid) < 0: + solid = topods.Solid(solid.Reversed()) + return _check(solid, what) + + +def _unify(shape): + """Merge co-planar / co-cylindrical neighbouring faces (the seams a fuse chain leaves).""" + u = ShapeUpgrade_UnifySameDomain(shape) + u.Build() + return u.Shape() + + +def _run_boolean(op, a, b): + algo = op(a, b) + algo.Build() + if not algo.IsDone(): + return None + return algo.Shape() + + +def _boolean(op, a, b, what, lower=True): + """A boolean with a volume invariant, because OCCT can fail silently. + + fuse max(vA, vB) <= v <= vA + vB + cut vA - vB <= v <= vA + common 0 <= v <= min(vA, vB) + + `lower=False` drops the lower bound, for an unbounded half-space tool. A violation + is retried with the operands unified, then declined. The band is loose on + purpose: it catches a lost operand, not an accuracy error. + """ + try: + va, vb = solid_volume_mm3(a), solid_volume_mm3(b) + except Exception: + va = vb = None + + def bounds(v): + if va is None: + return True, "" + tol = BOOLEAN_VOLUME_TOL * max(va, vb, 1.0) + if op is BRepAlgoAPI_Fuse: + lo, hi = max(va, vb) - tol, va + vb + tol + elif op is BRepAlgoAPI_Cut: + lo, hi = va - vb - tol, va + tol + else: + lo, hi = -tol, min(va, vb) + tol + if not lower: + lo = -tol + return lo <= v <= hi, f"{v:.6g} outside [{lo:.6g}, {hi:.6g}] mm^3" + + sh = _run_boolean(op, a, b) + if sh is None: + raise ShapeDeclined(f"{what}: OCCT boolean did not complete") + _check(sh, what) + ok, msg = bounds(solid_volume_mm3(sh)) + if ok: + return sh + retry = _run_boolean(op, _unify(a), _unify(b)) + if retry is not None and _has_solid(retry): + ok2, msg2 = bounds(solid_volume_mm3(retry)) + if ok2: + return retry + msg = f"{msg}; after unifying the operands {msg2}" + raise ShapeDeclined( + f"{what}: OCCT's boolean returned a volume the operands cannot give " + f"({msg}); the operation failed silently") + + +# -------------------------------------------------------------------------- +# TGeoMatrix -> OCCT transform +# -------------------------------------------------------------------------- + +def tgeo_matrix_components(m): + """(3x3 row-major matrix including any TGeoScale, translation in mm).""" + if m is None: + return [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]], [0., 0., 0.] + r = m.GetRotationMatrix() + s = m.GetScale() + t = m.GetTranslation() + rot = [[float(r[3 * i + j]) for j in range(3)] for i in range(3)] + sc = [float(s[j]) for j in range(3)] + mat = [[rot[i][j] * sc[j] for j in range(3)] for i in range(3)] + tr = [float(t[i]) * SCALE_TO_MM for i in range(3)] + return mat, tr + + +def _det3(m): + return (m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) + - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0])) + + +# Hand-written rotation constants are not exactly orthogonal: a matrix inside this band is +# snapped to the nearest rotation (and reported), one outside is refused as rigid and baked. +_ORTHO_TOL = 1e-6 + +# The relative volume band a baked isometry must preserve. +_ISOMETRY_TOL = 1e-6 + +# A rotation correction below this is double-precision noise, not reported. +_ORTHO_NOISE = 1e-12 + + +def orthogonality_deviation(mat): + """max |M^T M - I| over the nine entries: 0 for an exact rotation or mirror.""" + return max(abs(sum(mat[k][i] * mat[k][j] for k in range(3)) + - (1.0 if i == j else 0.0)) + for i in range(3) for j in range(3)) + + +def _inv3(m): + d = _det3(m) + if abs(d) < 1e-30: + return None + c = [[m[(i + 1) % 3][(j + 1) % 3] * m[(i + 2) % 3][(j + 2) % 3] + - m[(i + 1) % 3][(j + 2) % 3] * m[(i + 2) % 3][(j + 1) % 3] + for j in range(3)] for i in range(3)] + return [[c[j][i] / d for j in range(3)] for i in range(3)] + + +def orthonormalise(mat): + """(the nearest orthogonal matrix, how far the input was, how far it moved). + + Polar decomposition by Newton's iteration `R <- (R + R^-T)/2`, which converges + to the orthogonal factor of `R` and preserves the sign of the determinant, so a + reflection stays a reflection. Snapping is what keeps the exactness downstream: + `gp_Trsf` and every world transform composed from it are then built from a + matrix that really is an isometry. + """ + dev = orthogonality_deviation(mat) + r = [row[:] for row in mat] + for _ in range(8): + inv = _inv3(r) + if inv is None: + return mat, dev, 0.0 + r = [[0.5 * (r[i][j] + inv[j][i]) for j in range(3)] for i in range(3)] + if orthogonality_deviation(r) < 1e-15: + break + corr = max(abs(r[i][j] - mat[i][j]) for i in range(3) for j in range(3)) + return r, dev, corr + + +def _isometry_trsf(mat, tr, proper_only): + """(gp_Trsf, orthogonality deviation, correction) or (None, deviation, 0.0). + + A `gp_Trsf` carries an improper orthogonal matrix perfectly well -- OCCT models + it as a uniform scale of -1 -- and `BRepBuilderAPI_Transform` then moves the + exact analytic carriers. Only a genuinely non-uniform scale needs a `gp_GTrsf`. + """ + dev = orthogonality_deviation(mat) + if dev > _ORTHO_TOL: + return None, dev, 0.0 + d = _det3(mat) + if abs(abs(d) - 1.0) > _ORTHO_TOL or (proper_only and d < 0.0): + return None, dev, 0.0 + mat, dev, corr = orthonormalise(mat) + t = gp_Trsf() + try: + t.SetValues(mat[0][0], mat[0][1], mat[0][2], tr[0], + mat[1][0], mat[1][1], mat[1][2], tr[1], + mat[2][0], mat[2][1], mat[2][2], tr[2]) + except Exception: + return None, dev, corr + return t, dev, corr + + +def tgeo_matrix_to_isometry(m): + """An exact gp_Trsf for any isometry of a TGeoMatrix, reflections included.""" + mat, tr = tgeo_matrix_components(m) + return _isometry_trsf(mat, tr, proper_only=False)[0] + + +def tgeo_matrix_to_gtrsf(m): + mat, tr = tgeo_matrix_components(m) + g = gp_GTrsf() + g.SetVectorialPart(gp_Mat(mat[0][0], mat[0][1], mat[0][2], + mat[1][0], mat[1][1], mat[1][2], + mat[2][0], mat[2][1], mat[2][2])) + g.SetTranslationPart(gp_XYZ(tr[0], tr[1], tr[2])) + return g + + +def apply_isometry(shape, t, what): + """Apply an exact isometry. + + An isometry cannot change a volume, so this is priced against the volume it must preserve. + """ + v0 = solid_volume_mm3(shape) + algo = BRepBuilderAPI_Transform(shape, t, True) + if not algo.IsDone(): + raise ShapeDeclined(f"{what}: BRepBuilderAPI_Transform failed") + out = _check(algo.Shape(), what) + v1 = solid_volume_mm3(out) + if v0 > 0 and abs(v1 - v0) > _ISOMETRY_TOL * v0: + raise ShapeDeclined(f"{what}: the isometry changed the volume by " + f"{abs(v1 - v0) / v0:.3e} relative") + if _signed_volume(out) < 0: + raise ShapeDeclined(f"{what}: the isometry left the solid inside out") + return out + + +def zmirror_trsf(): + """The canonical reflection z -> -z, exactly.""" + t = gp_Trsf() + t.SetMirror(gp_Ax2(gp_Pnt(0., 0., 0.), gp_Dir(0., 0., 1.))) + return t + + +def _zmirror_left(mat, tr): + """Z * M, with Z = diag(1, 1, -1).""" + return ([mat[0][:], mat[1][:], [-v for v in mat[2]]], [tr[0], tr[1], -tr[2]]) + + +def _zmirror_right(mat, tr): + """M * Z.""" + return ([[mat[i][0], mat[i][1], -mat[i][2]] for i in range(3)], list(tr)) + + +def child_location(parent_mirrored, mat, tr): + """Where a daughter goes, and whether it is the daughter's mirrored prototype. + + Write Z = diag(1, 1, -1) and let V^ = Z*V be a volume's mirrored prototype. + A reflecting placement M of V is then M*V = (M*Z)*(Z*V) = (M*Z)*V^, and M*Z is + proper -- so a reflection never needs a general transform and never needs a + solid to bake into: it becomes a rigid placement of the child's prototype. The + same identity applied to Z*M pushes the reflection through an assembly and down + to its leaves, which is why a reflected subtree can be emitted at all. + + Every volume therefore has at most two prototypes, itself and Z*itself, shared + by every reflected use of it. + + Returns (gp_Trsf or None, child_mirrored, location matrix, location + translation, orthogonality deviation, orthonormalisation correction). + """ + if parent_mirrored: + mat, tr = _zmirror_left(mat, tr) + mirrored = _det3(mat) < 0.0 + if mirrored: + mat, tr = _zmirror_right(mat, tr) + t, dev, corr = _isometry_trsf(mat, tr, proper_only=True) + return t, mirrored, mat, tr, dev, corr + + +def mirror_solid_z(shape, what="mirrored copy"): + """Reflect a solid through the z = 0 plane, exactly and carrier-preserving.""" + return apply_isometry(shape, zmirror_trsf(), what) + + +def apply_tgeo_matrix(shape, m, what): + """Move `shape` by a TGeoMatrix: an isometry through `gp_Trsf`, a non-uniform scale through `gp_GTrsf`.""" + t = tgeo_matrix_to_isometry(m) + if t is not None: + if t.IsNegative(): + return apply_isometry(shape, t, what) + return _moved(shape, t) + g = tgeo_matrix_to_gtrsf(m) + algo = BRepBuilderAPI_GTransform(shape, g, True) + if not algo.IsDone(): + raise ShapeDeclined(f"{what}: could not apply a reflecting/scaling matrix") + return _check(algo.Shape(), what) + + +# -------------------------------------------------------------------------- +# volume and shape identity +# -------------------------------------------------------------------------- + +_ROOT = None + + +def _root(): + global _ROOT + if _ROOT is None: + import ROOT + _ROOT = ROOT + return _ROOT + + +def obj_id(o): + """The address of a ROOT object; it identifies a `TGeoVolume`, whose name need not be unique.""" + return int(_root().addressof(o)) + + +def _r(v): + return round(float(v), 12) + + +def _rs(seq, n): + return tuple(_r(seq[i]) for i in range(n)) + + +def _sig_zprofile(sh): + nz = int(sh.GetNz()) + return (nz, tuple((_r(sh.GetZ(i)), _r(sh.GetRmin(i)), _r(sh.GetRmax(i))) + for i in range(nz))) + + +def shape_signature(sh): + """A value key for a `TGeoShape`: equal keys mean the same solid. + + A class not known by value, `TGeoCompositeShape` included, is keyed on its address. + Classes match exactly, so a subclass (`TGeoGtra` under `TGeoTrap`) is never taken for its base. + """ + cls = str(sh.ClassName()) + o = sh.GetOrigin() + bbox = (_r(sh.GetDX()), _r(sh.GetDY()), _r(sh.GetDZ()), + _r(o[0]), _r(o[1]), _r(o[2])) + if cls == "TGeoBBox": + return (cls, bbox) + if cls == "TGeoTube": + return (cls, bbox, _r(sh.GetRmin()), _r(sh.GetRmax()), _r(sh.GetDz())) + if cls == "TGeoTubeSeg": + return (cls, bbox, _r(sh.GetRmin()), _r(sh.GetRmax()), _r(sh.GetDz()), + _r(sh.GetPhi1()), _r(sh.GetPhi2())) + if cls == "TGeoCtub": + return (cls, bbox, _r(sh.GetRmin()), _r(sh.GetRmax()), _r(sh.GetDz()), + _r(sh.GetPhi1()), _r(sh.GetPhi2()), + _rs(sh.GetNlow(), 3), _rs(sh.GetNhigh(), 3)) + if cls == "TGeoCone": + return (cls, bbox, _r(sh.GetDz()), _r(sh.GetRmin1()), _r(sh.GetRmax1()), + _r(sh.GetRmin2()), _r(sh.GetRmax2())) + if cls == "TGeoConeSeg": + return (cls, bbox, _r(sh.GetDz()), _r(sh.GetRmin1()), _r(sh.GetRmax1()), + _r(sh.GetRmin2()), _r(sh.GetRmax2()), + _r(sh.GetPhi1()), _r(sh.GetPhi2())) + if cls == "TGeoPcon": + return (cls, bbox, _r(sh.GetPhi1()), _r(sh.GetDphi()), _sig_zprofile(sh)) + if cls == "TGeoPgon": + return (cls, bbox, _r(sh.GetPhi1()), _r(sh.GetDphi()), int(sh.GetNedges()), + _sig_zprofile(sh)) + if cls == "TGeoSphere": + return (cls, bbox, _r(sh.GetRmin()), _r(sh.GetRmax()), + _r(sh.GetTheta1()), _r(sh.GetTheta2()), + _r(sh.GetPhi1()), _r(sh.GetPhi2())) + if cls == "TGeoTorus": + return (cls, bbox, _r(sh.GetR()), _r(sh.GetRmin()), _r(sh.GetRmax()), + _r(sh.GetPhi1()), _r(sh.GetDphi())) + if cls == "TGeoEltu": + return (cls, bbox, _r(sh.GetA()), _r(sh.GetB()), _r(sh.GetDz())) + if cls == "TGeoTrd1": + return (cls, bbox, _r(sh.GetDx1()), _r(sh.GetDx2()), _r(sh.GetDy()), + _r(sh.GetDz())) + if cls == "TGeoTrd2": + return (cls, bbox, _r(sh.GetDx1()), _r(sh.GetDx2()), _r(sh.GetDy1()), + _r(sh.GetDy2()), _r(sh.GetDz())) + if cls in ("TGeoArb8", "TGeoTrap"): + return (cls, bbox, _r(sh.GetDz()), _rs(sh.GetVertices(), 16)) + if cls == "TGeoXtru": + nv, nz = int(sh.GetNvert()), int(sh.GetNz()) + return (cls, bbox, nv, tuple((_r(sh.GetX(i)), _r(sh.GetY(i))) for i in range(nv)), + nz, tuple((_r(sh.GetZ(k)), _r(sh.GetXOffset(k)), _r(sh.GetYOffset(k)), + _r(sh.GetScale(k))) for k in range(nz))) + if cls == "TGeoScaledShape": + return (cls, bbox, _rs(sh.GetScale().GetScale(), 3), + shape_signature(sh.GetShape())) + return ("byAddress", cls, obj_id(sh)) + + +# -------------------------------------------------------------------------- +# shape converters -- all output mm, centred as TGeo centres them +# -------------------------------------------------------------------------- + +def _phi_span(phi1, phi2): + d = float(phi2) - float(phi1) + while d <= 0: + d += 360.0 + return float(phi1), min(d, 360.0) + + +def conv_box(sh, s): + dx, dy, dz = sh.GetDX() * s, sh.GetDY() * s, sh.GetDZ() * s + ox, oy, oz = (sh.GetOrigin()[0] * s, sh.GetOrigin()[1] * s, sh.GetOrigin()[2] * s) + if min(dx, dy, dz) <= 0: + raise ShapeDeclined("TGeoBBox: a half-length is zero or negative") + box = BRepPrimAPI_MakeBox(2 * dx, 2 * dy, 2 * dz).Shape() + return _moved(box, _translate(ox - dx, oy - dy, oz - dz)) + + +def _tube_like(rmin, rmax, dz, phi1, dphi, what): + if rmax <= 0 or dz <= 0: + raise ShapeDeclined(f"{what}: rmax or dz is zero") + if rmin >= rmax: + raise ShapeDeclined(f"{what}: rmin >= rmax") + if rmin <= EPS: + cyl = BRepPrimAPI_MakeCylinder(_ax2(-dz, phi1), rmax, 2 * dz, math.radians(dphi)) + cyl.Build() + if not cyl.IsDone(): + raise ShapeDeclined(f"{what}: BRepPrimAPI_MakeCylinder failed") + return _check(cyl.Shape(), what) + return _revolve_profile([(rmin, -dz), (rmax, -dz), (rmax, dz), (rmin, dz)], + phi1, dphi, what) + + +def conv_tube(sh, s): + return _tube_like(sh.GetRmin() * s, sh.GetRmax() * s, sh.GetDz() * s, + 0.0, 360.0, "TGeoTube") + + +def conv_tubeseg(sh, s): + phi1, dphi = _phi_span(sh.GetPhi1(), sh.GetPhi2()) + return _tube_like(sh.GetRmin() * s, sh.GetRmax() * s, sh.GetDz() * s, + phi1, dphi, "TGeoTubeSeg") + + +def _cone_like(rmin1, rmax1, rmin2, rmax2, dz, phi1, dphi, what): + if dz <= 0: + raise ShapeDeclined(f"{what}: dz is zero") + if max(rmax1, rmax2) <= 0: + raise ShapeDeclined(f"{what}: both outer radii are zero") + if rmin1 <= EPS and rmin2 <= EPS: + cone = BRepPrimAPI_MakeCone(_ax2(-dz, phi1), rmax1, rmax2, 2 * dz, math.radians(dphi)) + cone.Build() + if not cone.IsDone(): + raise ShapeDeclined(f"{what}: BRepPrimAPI_MakeCone failed") + return _check(cone.Shape(), what) + return _revolve_profile([(rmin1, -dz), (rmax1, -dz), (rmax2, dz), (rmin2, dz)], + phi1, dphi, what) + + +def conv_cone(sh, s): + return _cone_like(sh.GetRmin1() * s, sh.GetRmax1() * s, + sh.GetRmin2() * s, sh.GetRmax2() * s, sh.GetDz() * s, + 0.0, 360.0, "TGeoCone") + + +def conv_coneseg(sh, s): + phi1, dphi = _phi_span(sh.GetPhi1(), sh.GetPhi2()) + return _cone_like(sh.GetRmin1() * s, sh.GetRmax1() * s, + sh.GetRmin2() * s, sh.GetRmax2() * s, sh.GetDz() * s, + phi1, dphi, "TGeoConeSeg") + + +def conv_pcon(sh, s): + nz = int(sh.GetNz()) + if nz < 2: + raise ShapeDeclined("TGeoPcon: fewer than two z planes") + z = [sh.GetZ(i) * s for i in range(nz)] + rmin = [sh.GetRmin(i) * s for i in range(nz)] + rmax = [sh.GetRmax(i) * s for i in range(nz)] + phi1, dphi = float(sh.GetPhi1()), float(sh.GetDphi()) + outer = [(rmax[i], z[i]) for i in range(nz)] + if all(r <= EPS for r in rmin): + inner = [(0.0, z[nz - 1]), (0.0, z[0])] + else: + inner = [(rmin[i], z[i]) for i in range(nz - 1, -1, -1)] + return _revolve_profile(outer + inner, phi1, dphi, "TGeoPcon") + + +def _pgon_ring(r_apothem, z, phi1_deg, dphi_deg, nedges, full): + """The polygon at one z plane. TGeo's rmin/rmax are inscribed-circle radii.""" + dseg = math.radians(dphi_deg) / nedges + R = r_apothem / math.cos(dseg / 2.0) + n = nedges if full else nedges + 1 + out = [] + for k in range(n): + a = math.radians(phi1_deg) + k * dseg + out.append((R * math.cos(a), R * math.sin(a), z)) + return out + + +def conv_pgon(sh, s): + nz = int(sh.GetNz()) + nedges = int(sh.GetNedges()) + if nz < 2 or nedges < 1: + raise ShapeDeclined("TGeoPgon: fewer than two z planes or no edges") + phi1, dphi = float(sh.GetPhi1()), float(sh.GetDphi()) + full = abs(dphi - 360.0) < 1e-9 + z = [sh.GetZ(i) * s for i in range(nz)] + rmin = [sh.GetRmin(i) * s for i in range(nz)] + rmax = [sh.GetRmax(i) * s for i in range(nz)] + hollow = any(r > EPS for r in rmin) + rings = [] + for i in range(nz): + outer = _pgon_ring(rmax[i], z[i], phi1, dphi, nedges, full) + if hollow: + inner = _pgon_ring(max(rmin[i], EPS), z[i], phi1, dphi, nedges, full) + ring = outer + list(reversed(inner)) + elif full: + ring = outer + else: + ring = outer + [(0.0, 0.0, z[i])] + rings.append(ring) + if hollow and full: + # A full hollow polyhedra has two disjoint rings per section: sew outer and inner stacks. + outer_rings = [_pgon_ring(rmax[i], z[i], phi1, dphi, nedges, True) for i in range(nz)] + inner_rings = [_pgon_ring(max(rmin[i], EPS), z[i], phi1, dphi, nedges, True) for i in range(nz)] + return _prism_from_rings(outer_rings, inner_rings, what="TGeoPgon") + return _prism_from_rings(rings, what="TGeoPgon") + + +def conv_sphere(sh, s): + rmin, rmax = sh.GetRmin() * s, sh.GetRmax() * s + th1, th2 = float(sh.GetTheta1()), float(sh.GetTheta2()) + phi1, dphi = _phi_span(sh.GetPhi1(), sh.GetPhi2()) + if rmax <= 0: + raise ShapeDeclined("TGeoSphere: rmax is zero") + if th2 <= th1: + raise ShapeDeclined("TGeoSphere: theta2 <= theta1") + + def rz(r, theta_deg): + a = math.radians(theta_deg) + return (r * math.sin(a), r * math.cos(a)) + + thm = 0.5 * (th1 + th2) + p1, pm, p2 = rz(rmax, th1), rz(rmax, thm), rz(rmax, th2) + elems = [("arc", p1, pm, p2)] + if rmin > EPS: + q1, qm, q2 = rz(rmin, th1), rz(rmin, thm), rz(rmin, th2) + elems += [("line", p2, q2), ("arc", q2, qm, q1), ("line", q1, p1)] + elif p1[0] < 1e-9 and p2[0] < 1e-9: + elems += [("line", p2, p1)] # both poles: close on the axis + else: + elems += [("line", p2, (0.0, 0.0)), ("line", (0.0, 0.0), p1)] + return _revolve_edges(elems, phi1, dphi, "TGeoSphere") + + +def conv_torus(sh, s): + R = sh.GetR() * s + rmin, rmax = sh.GetRmin() * s, sh.GetRmax() * s + phi1, dphi = float(sh.GetPhi1()), float(sh.GetDphi()) + if rmax <= 0 or R <= 0: + raise ShapeDeclined("TGeoTorus: R or Rmax is zero") + + def mk(r): + m = BRepPrimAPI_MakeTorus(_ax2(0.0, phi1), R, r, math.radians(dphi)) + m.Build() + if not m.IsDone(): + raise ShapeDeclined("TGeoTorus: BRepPrimAPI_MakeTorus failed") + return _check(m.Shape(), "TGeoTorus") + + outer = mk(rmax) + if rmin > EPS: + return _boolean(BRepAlgoAPI_Cut, outer, mk(rmin), "TGeoTorus(hollow)") + return outer + + +def conv_eltu(sh, s): + a, b, dz = sh.GetA() * s, sh.GetB() * s, sh.GetDz() * s + if a <= 0 or b <= 0 or dz <= 0: + raise ShapeDeclined("TGeoEltu: a semi-axis or dz is zero") + if a >= b: + ax = gp_Ax2(gp_Pnt(0, 0, -dz), gp_Dir(0, 0, 1), gp_Dir(1, 0, 0)) + maj, mnr = a, b + else: + ax = gp_Ax2(gp_Pnt(0, 0, -dz), gp_Dir(0, 0, 1), gp_Dir(0, 1, 0)) + maj, mnr = b, a + edge = BRepBuilderAPI_MakeEdge(gp_Elips(ax, maj, mnr)).Edge() + wire = BRepBuilderAPI_MakeWire(edge).Wire() + face = BRepBuilderAPI_MakeFace(wire).Face() + pr = BRepPrimAPI_MakePrism(face, gp_Vec(0, 0, 2 * dz)) + pr.Build() + if not pr.IsDone(): + raise ShapeDeclined("TGeoEltu: prism failed") + return _check(pr.Shape(), "TGeoEltu") + + +def conv_trd1(sh, s): + dx1, dx2 = sh.GetDx1() * s, sh.GetDx2() * s + dy, dz = sh.GetDy() * s, sh.GetDz() * s + return _prism_from_rings([ + [(-dx1, -dy, -dz), (dx1, -dy, -dz), (dx1, dy, -dz), (-dx1, dy, -dz)], + [(-dx2, -dy, dz), (dx2, -dy, dz), (dx2, dy, dz), (-dx2, dy, dz)], + ], what="TGeoTrd1") + + +def conv_trd2(sh, s): + dx1, dx2 = sh.GetDx1() * s, sh.GetDx2() * s + dy1, dy2 = sh.GetDy1() * s, sh.GetDy2() * s + dz = sh.GetDz() * s + return _prism_from_rings([ + [(-dx1, -dy1, -dz), (dx1, -dy1, -dz), (dx1, dy1, -dz), (-dx1, dy1, -dz)], + [(-dx2, -dy2, dz), (dx2, -dy2, dz), (dx2, dy2, dz), (-dx2, dy2, dz)], + ], what="TGeoTrd2") + + +def conv_arb8(sh, s): + """TGeoArb8 and its subclasses (Trap): eight vertices, ruled lateral faces.""" + v = sh.GetVertices() + dz = sh.GetDz() * s + bot = [(v[2 * i] * s, v[2 * i + 1] * s, -dz) for i in range(4)] + top = [(v[8 + 2 * i] * s, v[8 + 2 * i + 1] * s, dz) for i in range(4)] + return _prism_from_rings([bot, top], what=sh.ClassName()) + + +def conv_xtru(sh, s): + nv, nz = int(sh.GetNvert()), int(sh.GetNz()) + if nv < 3 or nz < 2: + raise ShapeDeclined("TGeoXtru: fewer than 3 vertices or 2 sections") + x = [sh.GetX(i) for i in range(nv)] + y = [sh.GetY(i) for i in range(nv)] + rings = [] + for k in range(nz): + z = sh.GetZ(k) * s + x0, y0, sc = sh.GetXOffset(k) * s, sh.GetYOffset(k) * s, sh.GetScale(k) + rings.append([(x0 + sc * x[i] * s, y0 + sc * y[i] * s, z) for i in range(nv)]) + return _prism_from_rings(rings, what="TGeoXtru") + + +def conv_ctub(sh, s): + """A cut tube. + + TGeo's cut planes replace the +-dz end faces, so the tube is built long enough to reach + past both planes before they cut it. + """ + rmin, rmax = sh.GetRmin() * s, sh.GetRmax() * s + dz = sh.GetDz() * s + phi1, dphi = _phi_span(sh.GetPhi1(), sh.GetPhi2()) + planes = [] + ext = 0.0 + for nvec, z0 in ((sh.GetNlow(), -dz), (sh.GetNhigh(), dz)): + n = (float(nvec[0]), float(nvec[1]), float(nvec[2])) + norm = math.sqrt(sum(c * c for c in n)) + if norm < EPS: + raise ShapeDeclined("TGeoCtub: a cut normal is null") + n = tuple(c / norm for c in n) + if abs(n[2]) < 1e-9: + raise ShapeDeclined("TGeoCtub: a cut plane is parallel to the axis") + planes.append((n, z0)) + ext = max(ext, rmax * math.hypot(n[0], n[1]) / abs(n[2])) + ext = ext * 1.5 + 1e-3 * max(rmax, dz) + base = _tube_like(rmin, rmax, dz + ext, phi1, dphi, "TGeoCtub(base tube)") + big = 4.0 * max(rmax, dz + ext) + 10.0 + for (n, z0) in planes: + pl = BRepBuilderAPI_MakeFace( + gp_Pln(gp_Pnt(0.0, 0.0, z0), gp_Dir(*n)), -big, big, -big, big) + if not pl.IsDone(): + raise ShapeDeclined("TGeoCtub: could not build a cut plane") + ref = gp_Pnt(n[0] * big, n[1] * big, z0 + n[2] * big) # outside the solid + hs = BRepPrimAPI_MakeHalfSpace(pl.Face(), ref) + hs.Build() + if not hs.IsDone(): + raise ShapeDeclined("TGeoCtub: half-space construction failed") + base = _boolean(BRepAlgoAPI_Cut, base, hs.Solid(), "TGeoCtub", lower=False) + return base + + +_BOOL_OPS = { + "TGeoUnion": (BRepAlgoAPI_Fuse, "union"), + "TGeoSubtraction": (BRepAlgoAPI_Cut, "subtraction"), + "TGeoIntersection": (BRepAlgoAPI_Common, "intersection"), +} + +# A runaway guard on the boolean-tree walk, well above any real chain. +MAX_BOOLEAN_DEPTH = 512 + + +def conv_composite(sh, s, depth=0): + if depth > MAX_BOOLEAN_DEPTH: + raise ShapeDeclined(f"TGeoCompositeShape: boolean tree deeper than {MAX_BOOLEAN_DEPTH}") + bn = sh.GetBoolNode() + if bn is None: + raise ShapeDeclined("TGeoCompositeShape: no boolean node") + op = _BOOL_OPS.get(bn.ClassName()) + if op is None: + raise ShapeDeclined(f"TGeoCompositeShape: unknown boolean node {bn.ClassName()}") + algo, opname = op + left = shape_to_occ(bn.GetLeftShape(), s, depth + 1) + right = shape_to_occ(bn.GetRightShape(), s, depth + 1) + left = apply_tgeo_matrix(left, bn.GetLeftMatrix(), "composite left operand") + right = apply_tgeo_matrix(right, bn.GetRightMatrix(), "composite right operand") + return _boolean(algo, left, right, f"TGeoCompositeShape({opname})") + + +def conv_scaled(sh, s, depth=0): + inner = shape_to_occ(sh.GetShape(), s, depth + 1) + sc = sh.GetScale().GetScale() + g = gp_GTrsf() + g.SetVectorialPart(gp_Mat(sc[0], 0, 0, 0, sc[1], 0, 0, 0, sc[2])) + algo = BRepBuilderAPI_GTransform(inner, g, True) + if not algo.IsDone(): + raise ShapeDeclined("TGeoScaledShape: could not apply the scale") + return _check(algo.Shape(), "TGeoScaledShape") + + +_DISPATCH = { + "TGeoBBox": conv_box, + "TGeoTube": conv_tube, + "TGeoTubeSeg": conv_tubeseg, + "TGeoCtub": conv_ctub, + "TGeoCone": conv_cone, + "TGeoConeSeg": conv_coneseg, + "TGeoPcon": conv_pcon, + "TGeoPgon": conv_pgon, + "TGeoSphere": conv_sphere, + "TGeoTorus": conv_torus, + "TGeoEltu": conv_eltu, + "TGeoTrd1": conv_trd1, + "TGeoTrd2": conv_trd2, + "TGeoArb8": conv_arb8, + "TGeoTrap": conv_arb8, + "TGeoXtru": conv_xtru, +} + +# Shapes we know about and deliberately do not map, with the reason. +_KNOWN_DECLINES = { + "TGeoHalfSpace": "unbounded solid: a half-space has no B-rep body of its own", + "TGeoGtra": "twisted trapezoid: the lateral twist is not a ruled loft of the " + "eight Arb8 vertices", + "TGeoParaboloid": "quadric of revolution not mapped (no OCCT primitive; would " + "need a revolved parabola profile)", + "TGeoHype": "hyperboloid of revolution not mapped", + "TGeoPara": "parallelepiped not mapped", + "TGeoTessellated": "already a mesh: STEP would carry facets, not a B-rep solid", + "TGeoShapeAssembly": "assembly shape: emitted as a pure XCAF assembly, no solid", +} + + +def shape_to_occ(sh, s=SCALE_TO_MM, depth=0): + """TGeoShape -> TopoDS_Shape in mm. Raises ShapeDeclined with a reason.""" + if sh is None: + raise ShapeDeclined("volume has no shape") + cls = sh.ClassName() + if cls == "TGeoCompositeShape": + return conv_composite(sh, s, depth) + if cls == "TGeoScaledShape": + return conv_scaled(sh, s, depth) + fn = _DISPATCH.get(cls) + if fn is None: + raise ShapeDeclined(_KNOWN_DECLINES.get(cls, f"shape class {cls} is not mapped")) + return fn(sh, s) + + +# -------------------------------------------------------------------------- +# media and materials, dumped verbatim into a sidecar keyed by emitted STEP part name +# +# The eight medium parameters are Geant's, in TGeoMedium's own order: +# 0 isvol 1 ifield 2 fieldm 3 tmaxfd 4 stemax 5 deemax 6 epsil 7 stmin +# -------------------------------------------------------------------------- + +MEDIUM_PARAM_NAMES = ("isvol", "ifield", "fieldm", "tmaxfd", + "stemax", "deemax", "epsil", "stmin") + + +def material_record(mat): + """Everything needed to rebuild one TGeoMaterial or TGeoMixture.""" + rec = { + "name": str(mat.GetName()), + "class": str(mat.ClassName()), + "Z": float(mat.GetZ()), + "A": float(mat.GetA()), + "density": float(mat.GetDensity()), + "radLen": float(mat.GetRadLen()), + "intLen": float(mat.GetIntLen()), + "isMixture": bool(mat.IsMixture()), + } + if mat.IsMixture(): + n = int(mat.GetNelements()) + zs, as_, ws = mat.GetZmixt(), mat.GetAmixt(), mat.GetWmixt() + rec["nElements"] = n + rec["elements"] = [{"Z": float(zs[i]), "A": float(as_[i]), + "W": float(ws[i])} for i in range(n)] + return rec + + +def medium_record(med): + """Everything needed to rebuild one TGeoMedium, its material included.""" + return { + "name": str(med.GetName()), + "id": int(med.GetId()), + "params": {k: float(med.GetParam(i)) + for i, k in enumerate(MEDIUM_PARAM_NAMES)}, + "material": material_record(med.GetMaterial()), + } + + +class TGeoToStep: + def __init__(self, opts): + self.opts = opts + self.doc = TDocStd_Document("O2_TGeoToCAD") + self.shape_tool = XCAFDoc_DocumentTool.ShapeTool(self.doc.Main()) + self.definitions = {} # definition id -> (label, occ solid or None) + self.records = {} # definition id -> report record + self.media = {} # medium name -> medium_record() + # Volumes emitted as pure assemblies, with daughters but no body (the experiment hall). + self.hollow = set(getattr(self.opts, 'hollow_volumes', None) or ()) + self.hollow_tag = getattr(self.opts, 'hollow_tag', None) or '' + self._intern = {} # definition key -> definition id + self._byvol = {} # (volume address, mirrored) -> definition id + self._seen_vols = set() # distinct TGeoVolume objects visited + self._sigcache = {} # TGeoShape address -> value signature + self._name_slots = {} # TGeo name -> {slot key: emitted STEP name} + self._asm_names = {} # volume address -> per-occurrence assembly name + self.nvolumes = 0 # distinct TGeoVolume objects visited + self.ncomponents = 0 + self.nbaked = 0 + self.nscaled = 0 + self.northo = 0 # placements snapped to the nearest rotation + self.ortho_worst = (0.0, 0.0, None) # (deviation, correction, where) + self.ortho_records = [] # per-placement, capped + self.scaled_records = [] # matrices refused as rigid, with the number + self.placed_world = set() # (definition id, world key), --dedup-world only + self.ndropped = 0 + self.dropped_examples = [] + self.reflected_nodes = [] # TGeo placements whose matrix reflects + self.nmirrored_components = 0 # components that place a mirrored prototype + self.share_worst = (0.0, None) # the shared-definition capacity self-check + self.t0 = time.time() + + # ------------------------------------------------------------------ + + def log(self, *a): + if not self.opts.quiet: + print(*a, file=sys.stderr, flush=True) + + + def _record(self, did, vol, emitted, **kw): + rec = self.records.setdefault(did, { + "name": str(vol.GetName()), + "emittedName": emitted, + "shapeClass": vol.GetShape().ClassName() if vol.GetShape() else None, + "ndaughters": int(vol.GetNdaughters()), + "isAssembly": bool(vol.IsAssembly()), + "converted": False, + "reason": None, + "capacity_cm3": None, + "occVolume_cm3": None, + "relDev": None, + "mirrored": False, + "sharedByVolumes": 1, + }) + rec.update(kw) + med = vol.GetMedium() + if med is not None: + name = str(med.GetName()) + rec["medium"] = name + if name not in self.media: + self.media[name] = medium_record(med) + return rec + + # ------------------------------------------------------------------ + # definition keys, name disambiguation, and the sharing self-check + # ------------------------------------------------------------------ + + def _kid(self, key): + """Intern a definition key as a small integer. + + Assembly keys quote their children, so without interning the top volume's + key would be a nested copy of the whole tree. + """ + i = self._intern.get(key) + if i is None: + i = len(self._intern) + 1 + self._intern[key] = i + return i + + def _shape_sig(self, vol): + sh = vol.GetShape() + if sh is None: + return ("noShape", obj_id(vol)) + a = obj_id(sh) + s = self._sigcache.get(a) + if s is None: + s = shape_signature(sh) + self._sigcache[a] = s + return s + + def _emit_name(self, vol, slot): + """The STEP name of a definition: `name`, then `name#2`, `name#3`, ... + + One TGeo name can cover several definitions, so the emitted names are + disambiguated in the order the definitions are created, which the + depth-first walk makes deterministic. The mapping goes into the report. + """ + base = str(vol.GetName()) + # Hall volumes are tagged per module so that two converted modules do not collide. + if base in self.hollow and self.hollow_tag: + base = f"{base}_{self.hollow_tag}" + slots = self._name_slots.setdefault(base, {}) + nm = slots.get(slot) + if nm is None: + nm = base if not slots else f"{base}#{len(slots) + 1}" + slots[slot] = nm + return nm + + def _occ_asm_name(self, vol): + """The STEP name of a per-occurrence assembly label (`--dedup-world`). + + One name per *volume*, not per occurrence, so a shared subtree keeps one + name however often it is expanded. + """ + a = obj_id(vol) + nm = self._asm_names.get(a) + if nm is None: + nm = self._emit_name(vol, ("vol", a)) + self._asm_names[a] = nm + return nm + + def _note_orthonormalised(self, where, dev, corr): + """Record a placement matrix that had to be snapped to the nearest rotation. + + The correction is not absorbed silently: it is counted, the worst is in the + report summary and the first 50 are listed with their numbers. + """ + if corr <= _ORTHO_NOISE: + return # double-precision dust, not a correction + self.northo += 1 + if len(self.ortho_records) < 50: + self.ortho_records.append({"placement": where, + "orthogonalityDeviation": dev, + "rotationCorrection": corr}) + if dev > self.ortho_worst[0]: + self.ortho_worst = (dev, corr, where) + + def _note_scaled(self, where, child, dev): + """A placement matrix refused as a rigid one: say so, with the number.""" + self.nscaled += 1 + self.scaled_records.append({"placement": where, + "volume": str(child.GetName()), + "orthogonalityDeviation": dev}) + self.log(f" [WARN] {where}: placement matrix is not an isometry " + f"(|M^T M - I| = {dev:.3e} > {_ORTHO_TOL:.0e}); baking it into a " + f"private copy of {child.GetName()}" + + (", whose daughters cannot follow" if child.GetNdaughters() else "")) + + def _note_shared(self, did, vol): + """Price a shared definition against the sharing volume's own capacity. + + A `TGeoCompositeShape` is skipped: its `Capacity()` is a Monte Carlo estimate. + """ + rec = self.records.get(did) + if rec is None: + return + rec["sharedByVolumes"] = rec.get("sharedByVolumes", 1) + 1 + occv = rec.get("occVolume_cm3") + sh = vol.GetShape() + if occv is None or sh is None or sh.ClassName() == "TGeoCompositeShape": + return + try: + cap = float(sh.Capacity()) + except Exception: + return + if not cap: + return + rel = abs(occv - cap) / abs(cap) + if rel > rec.get("shareMaxRelDev", 0.0): + rec["shareMaxRelDev"] = rel + if rel > self.share_worst[0]: + self.share_worst = (rel, str(vol.GetName())) + + # ------------------------------------------------------------------ + + def _solid_for(self, vol): + """The OCCT solid of a volume's own shape, or (None, reason).""" + sh = vol.GetShape() + if sh is None or vol.IsAssembly() or sh.ClassName() == "TGeoShapeAssembly": + return None, "pure assembly: no solid of its own, by design" + try: + occ = shape_to_occ(sh, SCALE_TO_MM) + except ShapeDeclined as e: + return None, str(e) + except Exception as e: # OCCT can throw + return None, f"{sh.ClassName()}: OCCT raised {type(e).__name__}: {e}" + return occ, None + + def _verify(self, vol, occ, rec): + sh = vol.GetShape() + try: + cap = float(sh.Capacity()) + except Exception: + cap = None + rec["capacity_cm3"] = cap + if not self.opts.verify: + return + try: + v_cm3 = solid_volume_mm3(occ) / 1000.0 + except Exception as e: + rec["occVolume_cm3"] = None + rec["verifyError"] = str(e) + return + rec["occVolume_cm3"] = v_cm3 + if cap and abs(cap) > 0: + rec["relDev"] = abs(v_cm3 - cap) / abs(cap) + + # ------------------------------------------------------------------ + + def build(self, vol, depth=0): + """Return the XCAF label for `vol`, building it (once) if needed.""" + return self.definitions[self.build_def(vol, depth)][0] + + def build_def(self, vol, depth=0, mirrored=False): + """The definition id of `vol`; `self.definitions[id]` is (label, solid). + + `mirrored` asks for the volume's Z-mirrored prototype instead of the volume + itself; see `child_location`. + """ + a = obj_id(vol) + k = (a, mirrored) + hit = self._byvol.get(k) + if hit is not None: + return hit + if a not in self._seen_vols: + self._seen_vols.add(a) + self.nvolumes += 1 + did = self._build_def(vol, depth, mirrored) + self._byvol[k] = did + return did + + def _own_solid(self, vol, wanted, mirrored): + """The volume's own OCCT solid, Z-mirrored if this is the prototype.""" + if not wanted: + return None, "excluded by --include-name" + # A hollowed volume contributes structure only, with or without daughters. + if str(vol.GetName()) in self.hollow: + return None, "hollow volume (--hollow-volume)" + occ, reason = self._solid_for(vol) + if occ is None or not mirrored: + return occ, reason + try: + return mirror_solid_z(occ, f"{vol.GetName()}: mirrored prototype"), None + except ShapeDeclined as e: + return None, str(e) + + def _build_def(self, vol, depth, mirrored=False): + name = str(vol.GetName()) + nd = int(vol.GetNdaughters()) + descend = nd > 0 + wanted = (self.opts.include_name is None + or fnmatch.fnmatch(name, self.opts.include_name)) + sig = self._shape_sig(vol) + + if not descend: + did = self._kid(("leaf", name, sig, wanted, mirrored)) + if did in self.definitions: + self._note_shared(did, vol) + return did + occ, reason = self._own_solid(vol, wanted, mirrored) + emitted = self._emit_name(vol, ("shape", sig)) if occ is not None else name + if occ is not None and mirrored: + emitted += "__mirrored" + self.nbaked += 1 + rec = self._record(did, vol, emitted, mirrored=mirrored, + converted=occ is not None, reason=reason) + if occ is None: + self.definitions[did] = (None, None) + return did + self._verify(vol, occ, rec) + lab = self.shape_tool.AddShape(occ, False) + TDataStd_Name.Set(lab, emitted) + self.definitions[did] = (lab, occ) + return did + + # A volume with daughters becomes an assembly; children first, so the key can quote them. + occ, reason = self._own_solid(vol, wanted, mirrored) + emit_body = (occ is not None and self.opts.mother_bodies + and not (depth == 0 and self.opts.skip_top_body)) + plan = [] + for i in range(nd): + node = vol.GetNode(i) + child = node.GetVolume() + mat, tr = tgeo_matrix_components(node.GetMatrix()) + t, cmir, lmat, ltr, dev, corr = child_location(mirrored, mat, tr) + if _det3(mat) < 0.0: + self.reflected_nodes.append(f"{name}/{node.GetName()}") + if cmir: + self.nmirrored_components += 1 + self._note_orthonormalised(f"{name}/{node.GetName()}", dev, corr) + if t is None: + # Not an isometry -- a genuine non-uniform scale. There is no + # prototype for that, so it stays a baked private copy. + self._bake_scaled(vol, node, child, depth, plan, dev) + continue + cdid = self.build_def(child, depth + 1, cmir) + if self.definitions[cdid][0] is None: + continue + plan.append((cdid, self._world_key(lmat, ltr), str(node.GetName()), t)) + + did = self._kid(("asm", name, sig, wanted, emit_body, mirrored, + bool(self.opts.carve_mothers), + tuple((p[0], p[1]) for p in plan))) + if did in self.definitions: + self._note_shared(did, vol) + return did + + base = self._emit_name(vol, ("vol", obj_id(vol))) + mir = "__mirrored" if mirrored else "" + emitted = base + mir + rec = self._record(did, vol, emitted, mirrored=mirrored, + converted=occ is not None, reason=reason) + if occ is not None: + self._verify(vol, occ, rec) + + asm = self.shape_tool.NewShape() + TDataStd_Name.Set(asm, emitted) + ncomp0 = self.ncomponents + placed_children = [] + for (cdid, _mk, nodename, t) in plan: + comp = self.shape_tool.AddComponent(asm, self.definitions[cdid][0], + TopLoc_Location(t)) + placed_children.append((self.definitions[cdid][1], t)) + TDataStd_Name.Set(comp, nodename) + self.ncomponents += 1 + + if emit_body: + body = occ + if self.opts.carve_mothers: + carved, complete = self._carve(occ, placed_children, name) + body = carved or occ + rec["carveComplete"] = bool(complete and carved is not None) + blab = self.shape_tool.AddShape(body, False) + TDataStd_Name.Set(blab, f"{base}__body{mir}") + comp = self.shape_tool.AddComponent(asm, blab, TopLoc_Location(gp_Trsf())) + TDataStd_Name.Set(comp, f"{base}__body{mir}") + self.ncomponents += 1 + rec["bodyComponent"] = f"{base}__body{mir}" + elif occ is not None: + rec["bodyComponent"] = None + rec["reason"] = "mother solid omitted (--no-mother-bodies/--skip-top-body)" + rec["converted"] = False + if self.ncomponents == ncomp0: + # every child declined and there is no body: an empty XCAF label reads + # back as a leaf holding an empty compound, so drop it instead. + self.shape_tool.RemoveShape(asm) + self.definitions[did] = (None, occ) + return did + self.definitions[did] = (asm, occ) + return did + + def _bake_scaled(self, vol, node, child, depth, plan, dev=None): + """A non-uniformly scaling placement: bake it, as there is no prototype.""" + self._note_scaled(f"{vol.GetName()}/{node.GetName()}", child, dev) + cdid = self.build_def(child, depth + 1, False) + csolid = self.definitions[cdid][1] + cname = self.records.get(cdid, {}).get("emittedName", str(child.GetName())) + if csolid is None: + self._record(cdid, child, cname, + reason="scaling placement of a volume with daughters " + "cannot be baked") + return + try: + baked = apply_tgeo_matrix(csolid, node.GetMatrix(), "scaling placement") + except ShapeDeclined as e: + self._record(cdid, child, cname, reason=str(e)) + return + blab = self.shape_tool.AddShape(baked, False) + TDataStd_Name.Set(blab, f"{cname}__scaled") + sdid = self._kid(("scaled", obj_id(node))) + self.definitions[sdid] = (blab, baked) + plan.append((sdid, ("scaled", obj_id(node)), str(node.GetName()), gp_Trsf())) + + # ------------------------------------------------------------------ + + @staticmethod + def _world_key(mat, tr): + return (tuple(round(mat[i][j], 9) for i in range(3) for j in range(3)) + + tuple(round(v, 6) for v in tr)) + + @staticmethod + def _compose(pmat, ptr, cmat, ctr): + """Compose a parent world transform with a child's (matrix, translation), in mm.""" + mat = [[sum(pmat[i][k] * cmat[k][j] for k in range(3)) for j in range(3)] + for i in range(3)] + tr = [sum(pmat[i][k] * ctr[k] for k in range(3)) + ptr[i] for i in range(3)] + return mat, tr + + def _shared_definition(self, vol, kind, mirrored=False): + """The shared XCAF definition of a volume's own solid (built once). + + `kind` is "leaf" for a volume without daughters and "body" for the mother + solid of one with daughters; both are keyed on the volume's *name and shape + value*, never on the name alone. `mirrored` asks for the Z-mirrored + prototype, which every reflected use of the volume shares. + """ + sig = self._shape_sig(vol) + did = self._kid((kind, str(vol.GetName()), sig, True, mirrored)) + if did in self.definitions: + self._note_shared(did, vol) + return did + occ, reason = self._own_solid(vol, True, mirrored) + # A mother body shares the slot of its own assembly label, so the two carry + # one disambiguated base name between them. + slot = ("vol", obj_id(vol)) if kind == "body" else ("shape", sig) + emitted = self._emit_name(vol, slot) if occ is not None \ + else str(vol.GetName()) + if kind == "body": + emitted = emitted + "__body" + if occ is not None and mirrored: + emitted = emitted + "__mirrored" + self.nbaked += 1 + # (body first, then the mirror suffix: `X__body__mirrored`) + rec = self._record(did, vol, emitted, mirrored=mirrored, + converted=occ is not None, reason=reason) + if occ is None: + self.definitions[did] = (None, None) + return did + self._verify(vol, occ, rec) + if kind == "body": + rec["bodyComponent"] = emitted + lab = self.shape_tool.AddShape(occ, False) + TDataStd_Name.Set(lab, emitted) + self.definitions[did] = (lab, occ) + return did + + def build_world(self, vol, depth=0, wmat=None, wtr=None, path="", + mirrored=False): + """Per-occurrence walk that drops coincident (definition, world transform) pairs. + + Assembly labels are one per occurrence and leaf solids one per (name, shape value, + mirrored). `wmat`/`wtr` are the volume's TGeo world transform (the coincidence key); + `mirrored` says whether the label is the Z-mirrored prototype. + """ + if wmat is None: + wmat = [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]] + wtr = [0., 0., 0.] + name = str(vol.GetName()) + a = obj_id(vol) + if a not in self._seen_vols: + self._seen_vols.add(a) + self.nvolumes += 1 + nd = int(vol.GetNdaughters()) + descend = nd > 0 + if not (self.opts.include_name is None + or fnmatch.fnmatch(name, self.opts.include_name)): + return None + + if not descend: + did = self._shared_definition(vol, "leaf", mirrored) + lab = self.definitions[did][0] + if lab is None: + return None + key = (did, self._world_key(wmat, wtr)) + if key in self.placed_world: + self.ndropped += 1 + if len(self.dropped_examples) < 50: + self.dropped_examples.append(path) + return None + self.placed_world.add(key) + return lab + + # Surviving children first: an occurrence with none must not leave an empty assembly label. + comps = [] + for i in range(nd): + node = vol.GetNode(i) + child = node.GetVolume() + mat, tr = tgeo_matrix_components(node.GetMatrix()) + cmat, ctr = self._compose(wmat, wtr, mat, tr) + t, cmir, _lmat, _ltr, dev, corr = child_location(mirrored, mat, tr) + if _det3(mat) < 0.0: + self.reflected_nodes.append(f"{name}/{node.GetName()}") + if cmir: + self.nmirrored_components += 1 + self._note_orthonormalised(f"{name}/{node.GetName()}", dev, corr) + if t is None: + # Not an isometry -- a genuine non-uniform scale. There is no + # prototype for that, so it stays a baked private copy. + self._bake_scaled_world(child, node, cmat, ctr, comps, dev) + continue + clab = self.build_world(child, depth + 1, cmat, ctr, + f"{path}/{node.GetName()}", cmir) + if clab is None: + continue + comps.append((clab, t, str(node.GetName()))) + + if (self.opts.mother_bodies + and not (depth == 0 and self.opts.skip_top_body)): + bdid = self._shared_definition(vol, "body", mirrored) + blab = self.definitions[bdid][0] + if blab is not None: + key = (bdid, self._world_key(wmat, wtr)) + if key in self.placed_world: + self.ndropped += 1 + else: + self.placed_world.add(key) + comps.append((blab, gp_Trsf(), + self.records[bdid]["emittedName"])) + + if not comps: + return None + asm = self.shape_tool.NewShape() + TDataStd_Name.Set(asm, self._occ_asm_name(vol) + + ("__mirrored" if mirrored else "")) + for (clab, t, cname) in comps: + comp = self.shape_tool.AddComponent(asm, clab, TopLoc_Location(t)) + TDataStd_Name.Set(comp, cname) + self.ncomponents += 1 + return asm + + def _bake_scaled_world(self, child, node, cmat, ctr, comps, dev=None): + """A non-uniformly scaling placement in the per-occurrence walk.""" + self._note_scaled(str(node.GetName()), child, dev) + cdid = self._shared_definition(child, "leaf", False) + csolid = self.definitions[cdid][1] + cname = self.records.get(cdid, {}).get("emittedName", str(child.GetName())) + if csolid is None: + self._record(cdid, child, cname, + reason="scaling placement of a volume with daughters " + "cannot be baked") + return + key = (("scaled", cdid), self._world_key(cmat, ctr)) + if key in self.placed_world: + self.ndropped += 1 + return + try: + baked = apply_tgeo_matrix(csolid, node.GetMatrix(), "scaling placement") + except ShapeDeclined as e: + self._record(cdid, child, cname, reason=str(e)) + return + self.placed_world.add(key) + blab = self.shape_tool.AddShape(baked, False) + TDataStd_Name.Set(blab, f"{cname}__scaled") + comps.append((blab, gp_Trsf(), str(node.GetName()))) + + # ------------------------------------------------------------------ + + def _carve(self, mother, placed, name): + """Subtract the placed daughters from the mother; return (body, every daughter subtracted). + + An assembly daughter (`None` in `placed`) has no solid to subtract; the reverse + converter nests exactly the mothers whose carve is incomplete. + """ + missing = sum(1 for (sh, _t) in placed if sh is None) + cutters = [_moved(sh, t) for (sh, t) in placed if sh is not None] + if not cutters: + if missing: + self.log(f" [WARN] {name}: {missing} daughter(s) are assemblies and have " + f"no solid to subtract; the mother stays uncarved") + return mother, not missing + try: + tool = cutters[0] + for c in cutters[1:]: + tool = _boolean(BRepAlgoAPI_Fuse, tool, c, "carve fuse") + carved = _boolean(BRepAlgoAPI_Cut, mother, tool, "carve cut") + except ShapeDeclined as e: + self.log(f" [WARN] {name}: carving failed ({e}); keeping the uncarved mother") + return mother, False + if missing: + # Carve every daughter or none: a nested partial carve leaves daughters outside the mother. + self.log(f" [WARN] {name}: {missing} of {len(placed)} daughter(s) are " + f"assemblies and have no solid to subtract; discarding the partial " + f"carve and keeping the mother whole, to be nested instead") + return mother, False + return carved, True + + # ------------------------------------------------------------------ + + def write(self, path): + self.shape_tool.UpdateAssemblies() + Interface_Static.SetCVal("write.step.schema", "AP214IS") + Interface_Static.SetCVal("write.step.unit", "MM") + Interface_Static.SetCVal("write.step.product.name", "O2_TGeoToCAD") + w = STEPCAFControl_Writer() + w.SetNameMode(True) + w.SetColorMode(False) + w.SetLayerMode(False) + if not w.Transfer(self.doc): + raise RuntimeError("STEPCAFControl_Writer.Transfer failed") + if w.Write(path) != IFSelect_RetDone: + raise RuntimeError(f"STEP write failed for {path}") + + def media_sidecar(self, source): + """The media sidecar: every medium used, and which emitted STEP part wears which.""" + parts = {} + for r in self.records.values(): + if not r.get("medium"): + continue + name = r.get("emittedName") + # Only a part emitted as a solid wears a medium; a mother's lives in its `__body` leaf. + is_body = bool(name) and (name.endswith("__body") + or name.endswith("__body__mirrored")) + if name and r.get("converted") and (r.get("ndaughters", 0) == 0 or is_body): + parts[name] = r["medium"] + # The mother's `__body` leaf carries its material. + if r.get("bodyComponent"): + parts[r["bodyComponent"]] = r["medium"] + # Which emitted part is the body of which assembly, so the reverse converter can nest. + bodies = {} + carved_complete = {} + for r in self.records.values(): + if r.get("bodyComponent") and r.get("emittedName"): + bodies[r["bodyComponent"]] = r["emittedName"] + if "carveComplete" in r: + carved_complete[r["emittedName"]] = bool(r["carveComplete"]) + + return { + "generator": "O2_TGeoToCAD.py", + "source": os.path.abspath(source), + "mediumParamOrder": list(MEDIUM_PARAM_NAMES), + "bodyOfAssembly": bodies, + # --carve-mothers only: True means every daughter was subtracted, so do not nest. + "carvedComplete": carved_complete, + "nBodies": len(bodies), + "nMedia": len(self.media), + "nParts": len(parts), + "media": self.media, + "parts": parts, + } + + def report(self, source, out_step): + by_class = {} + npure = 0 + for r in self.records.values(): + pure = r["isAssembly"] or r["shapeClass"] == "TGeoShapeAssembly" + c = by_class.setdefault(r["shapeClass"], {"converted": 0, "declined": 0, + "pureAssembly": 0, "reasons": {}}) + if r["converted"]: + c["converted"] += 1 + elif pure: + c["pureAssembly"] += 1 + npure += 1 + else: + c["declined"] += 1 + key = (r["reason"] or "unknown").split(":")[0] + c["reasons"][key] = c["reasons"].get(key, 0) + 1 + recs = sorted(self.records.values(), + key=lambda r: (r["name"], r.get("emittedName") or "")) + devs = [r["relDev"] for r in recs if r.get("relDev") is not None] + disambiguated = {} + for base, slots in self._name_slots.items(): + if len(slots) > 1: + disambiguated[base] = sorted(set(slots.values())) + return { + "source": os.path.abspath(source), + "output": os.path.abspath(out_step), + "scaleToMm": SCALE_TO_MM, + "generator": "O2_TGeoToCAD.py", + "wallSeconds": round(time.time() - self.t0, 2), + "volumesVisited": self.nvolumes, + "definitions": sum(1 for r in recs if r["converted"]), + "pureAssemblies": npure, + "declined": sum(1 for r in recs if not r["converted"] + and not (r["isAssembly"] or r["shapeClass"] == "TGeoShapeAssembly")), + "assemblies": sum(1 for r in recs if r["ndaughters"] > 0), + "components": self.ncomponents, + "mirroredPrototypes": self.nbaked, + "mirroredComponents": self.nmirrored_components, + "scaledPlacementsBaked": self.nscaled, + "scaledPlacements": self.scaled_records[:50], + "orthonormalisedPlacements": self.northo, + "maxOrthogonalityDeviation": self.ortho_worst[0], + "maxRotationCorrection": self.ortho_worst[1], + "worstOrthogonalityPlacement": self.ortho_worst[2], + "orthonormalisations": self.ortho_records[:50], + "coincidentPlacementsDropped": self.ndropped, + "coincidentPlacementExamples": self.dropped_examples, + "reflectedPlacements": self.reflected_nodes[:50], + "nReflectedPlacements": len(self.reflected_nodes), + "maxRelDev": max(devs) if devs else None, + "medianRelDev": sorted(devs)[len(devs) // 2] if devs else None, + "hollowVolumes": sorted(self.hollow), + "hollowTag": self.hollow_tag or None, + "nameDisambiguation": disambiguated, + "nDisambiguatedNames": len(disambiguated), + "sharedDefinitionMaxRelDev": self.share_worst[0], + "sharedDefinitionWorstVolume": self.share_worst[1], + "byShapeClass": by_class, + "volumes": recs, + } + + +# -------------------------------------------------------------------------- +# self-test +# -------------------------------------------------------------------------- + +def _cap_check(label, tgeo_shape, band, results, expect_fail=False, occ=None): + """One capacity-parity check: BRepGProp on our solid vs TGeoShape::Capacity().""" + try: + if occ is None: + occ = shape_to_occ(tgeo_shape, SCALE_TO_MM) + v = solid_volume_mm3(occ) / 1000.0 + cap = float(tgeo_shape.Capacity()) + rel = abs(v - cap) / abs(cap) if cap else float("inf") + ok = rel <= band + except Exception as e: + v, cap, rel, ok = None, None, None, False + label = f"{label} [{type(e).__name__}: {e}]" + passed = (ok != expect_fail) + results.append((label, passed, cap, v, rel)) + return passed + + +def _print_suite(title, results): + fails = [r for r in results if not r[1]] + print(f"\n--- {title}: {len(results)} checks, {len(fails)} failures") + for (label, ok, cap, v, rel) in results: + mark = "ok " if ok else "FAIL" + if rel is None: + print(f" [{mark}] {label}") + else: + print(f" [{mark}] {label:44s} TGeo {cap:14.6f} OCC {v:14.6f} rel {rel:.3e}") + return len(fails) + + +def self_test(): + import ROOT + ROOT.gROOT.SetBatch(True) + import array + + total = 0 + failures = 0 + + import random + rngc = random.Random(4242) + + def mc_volume(shape, n=200000): + dx, dy, dz = shape.GetDX(), shape.GetDY(), shape.GetDZ() + o = shape.GetOrigin() + ox, oy, oz = o[0], o[1], o[2] + vbox = 8.0 * dx * dy * dz + pt = array.array("d", [0.0, 0.0, 0.0]) + hits = 0 + for _ in range(n): + pt[0] = ox + rngc.uniform(-dx, dx) + pt[1] = oy + rngc.uniform(-dy, dy) + pt[2] = oz + rngc.uniform(-dz, dz) + if shape.Contains(pt): + hits += 1 + pf = hits / float(n) + return pf * vbox, vbox * math.sqrt(max(pf * (1.0 - pf), 1e-15) / n) + + + # ---- suite 1: primitives, analytic Capacity() ---- + band = 1e-9 + r1 = [] + _cap_check("TGeoBBox(1,2,3)", ROOT.TGeoBBox("b", 1, 2, 3), band, r1) + _cap_check("TGeoTube(0,2,5)", ROOT.TGeoTube("t0", 0, 2, 5), band, r1) + _cap_check("TGeoTube(1,2,5) rmin>0", ROOT.TGeoTube("t1", 1, 2, 5), band, r1) + _cap_check("TGeoTubeSeg(1,2,5,30,150)", ROOT.TGeoTubeSeg("ts", 1, 2, 5, 30, 150), band, r1) + _cap_check("TGeoTubeSeg(0,2,5,200,340)", ROOT.TGeoTubeSeg("ts2", 0, 2, 5, 200, 340), band, r1) + _cap_check("TGeoCone(3,0,2,0,4)", ROOT.TGeoCone("c0", 3, 0, 2, 0, 4), band, r1) + _cap_check("TGeoCone(3,1,2,0.5,4) rmin>0", ROOT.TGeoCone("c1", 3, 1, 2, 0.5, 4), band, r1) + _cap_check("TGeoConeSeg(2,.5,1,.7,1.5,30,150)", + ROOT.TGeoConeSeg("cs", 2, .5, 1, .7, 1.5, 30, 150), band, r1) + _cap_check("TGeoEltu(2,3,4)", ROOT.TGeoEltu("e", 2, 3, 4), band, r1) + _cap_check("TGeoTorus(10,0,2)", ROOT.TGeoTorus("to0", 10, 0, 2, 0, 360), band, r1) + _cap_check("TGeoTorus(10,1,2) hollow", ROOT.TGeoTorus("to1", 10, 1, 2, 0, 360), band, r1) + _cap_check("TGeoTorus(10,1,2,45,120) wedge", + ROOT.TGeoTorus("to2", 10, 1, 2, 45, 120), band, r1) + _cap_check("TGeoTrd1(1,2,3,4)", ROOT.TGeoTrd1("d1", 1, 2, 3, 4), band, r1) + _cap_check("TGeoTrd2(1,2,3,4,5)", ROOT.TGeoTrd2("d2", 1, 2, 3, 4, 5), band, r1) + _cap_check("TGeoSphere(0,2) full", ROOT.TGeoSphere("s0", 0, 2, 0, 180, 0, 360), band, r1) + _cap_check("TGeoSphere(1,2) shell", ROOT.TGeoSphere("s1", 1, 2, 0, 180, 0, 360), band, r1) + _cap_check("TGeoSphere(0,2,30,120) theta", + ROOT.TGeoSphere("s2", 0, 2, 30, 120, 0, 360), band, r1) + _cap_check("TGeoSphere(1,2,30,120,20,200)", + ROOT.TGeoSphere("s3", 1, 2, 30, 120, 20, 200), band, r1) + _cap_check("TGeoCtub(0,1,1) straight", + ROOT.TGeoCtub("ct", 0, 1, 1, 0, 360, 0, 0, -1, 0, 0, 1), band, r1) + arb8v = array.array("d", [-1, -1, -1, 1, 1, 1, 1, -1, -2, -2, -2, 2, 2, 2, 2, -2]) + _cap_check("TGeoArb8 (pyramid frustum)", ROOT.TGeoArb8("a8", 1.0, arb8v), band, r1) + _cap_check("TGeoTrap(2,0,0,1,1,1,0,1,1,1,0)", + ROOT.TGeoTrap("tp", 2, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0), band, r1) + x = ROOT.TGeoXtru(2) + x.DefinePolygon(4, array.array("d", [0, 0, 2, 2]), array.array("d", [0, 1, 1, 0])) + x.DefineSection(0, -1, 0, 0, 1) + x.DefineSection(1, 1, 0.5, 0, 2) + _cap_check("TGeoXtru (4-gon, scaled+offset)", x, band, r1) + pg = ROOT.TGeoPgon("pg", 0, 360, 6, 2) + pg.DefineSection(0, -1, 0, 1) + pg.DefineSection(1, 1, 0, 1) + _cap_check("TGeoPgon(0,360,6) solid", pg, band, r1) + pg2 = ROOT.TGeoPgon("pg2", 10, 90, 3, 2) + pg2.DefineSection(0, -1, 0.5, 1) + pg2.DefineSection(1, 1, 0.5, 1) + _cap_check("TGeoPgon(10,90,3) hollow wedge", pg2, band, r1) + pg3 = ROOT.TGeoPgon("pg3", 0, 360, 8, 3) + pg3.DefineSection(0, -2, 0.5, 1) + pg3.DefineSection(1, 0, 0.5, 2) + pg3.DefineSection(2, 2, 0.8, 2) + _cap_check("TGeoPgon(0,360,8) hollow stack", pg3, band, r1) + pc = ROOT.TGeoPcon("pc", 0, 360, 3) + pc.DefineSection(0, -1, 0, 1) + pc.DefineSection(1, 0, 0, 2) + pc.DefineSection(2, 1, 0, 2) + _cap_check("TGeoPcon(0,360) rmin=0", pc, band, r1) + pc2 = ROOT.TGeoPcon("pc2", 0, 360, 3) + pc2.DefineSection(0, -1, 0.5, 1) + pc2.DefineSection(1, 0, 0.5, 2) + pc2.DefineSection(2, 1, 0.8, 2) + _cap_check("TGeoPcon(0,360) rmin>0", pc2, band, r1) + pc3 = ROOT.TGeoPcon("pc3", 20, 150, 4) + pc3.DefineSection(0, -3, 0.5, 1) + pc3.DefineSection(1, -1, 0.5, 2) + pc3.DefineSection(2, -1, 1.2, 2) # a zero-thickness radius jump + pc3.DefineSection(3, 2, 1.2, 1.8) + _cap_check("TGeoPcon(20,150) wedge + z-jump", pc3, band, r1) + total += len(r1) + failures += _print_suite("primitives vs TGeoShape::Capacity(), band 1e-9", r1) + + # ---- suite 2: composites, against an independent Monte-Carlo of TGeo itself ---- + # Composite Capacity() is itself an MC estimate: require the OCCT volume within 4 sigma of our own MC. + r2 = [] + _keep = [ROOT.TGeoBBox("ca", 2, 2, 2), ROOT.TGeoTube("cb", 0, 1, 3)] + tr = ROOT.TGeoTranslation("shift", 3, 0, 0) + tr.RegisterYourself() + rot = ROOT.TGeoRotation("rot90", 0, 90, 0) + rot.RegisterYourself() + composites = [ + # TGeoCtub's z extent follows its cut planes, so it is scored by MC too. + ("cut tube, slanted (TGeoCtub)", + ROOT.TGeoCtub("ct2", 0, 1, 1, 0, 360, 0, -0.6, -0.8, 0, 0.6, 0.8)), + ("box - tube (subtraction)", ROOT.TGeoCompositeShape("sub", "ca - cb")), + ("box * tube (intersection)", ROOT.TGeoCompositeShape("inter", "ca * cb")), + ("box + shifted tube (union)", ROOT.TGeoCompositeShape("uni", "ca + cb:shift")), + ("(box - tube) + shifted tube (nested)", + ROOT.TGeoCompositeShape("nest", "(ca - cb) + cb:shift")), + ("box - rotated tube (rotated operand)", + ROOT.TGeoCompositeShape("rotsub", "ca - cb:rot90")), + ] + for (label, cs) in composites: + try: + occ = shape_to_occ(cs, SCALE_TO_MM) + v = solid_volume_mm3(occ) / 1000.0 + vmc, sig = mc_volume(cs) + ok = abs(v - vmc) <= 4.0 * sig + print(f" {label:40s} OCC {v:10.6f} MC {vmc:10.6f} +- {sig:.4f}" + f" ({abs(v - vmc) / sig:.2f} sigma)") + except Exception as e: + ok = False + label = f"{label} [{type(e).__name__}: {e}]" + r2.append((label, ok, None, None, None)) + # the control on the control: a 1% wrong volume must be outside 4 sigma + _, cs0 = composites[0] + vmc0, sig0 = mc_volume(cs0) + r2.append((f"a +2% wrong volume would be rejected ({0.02 * vmc0 / sig0:.1f} sigma)", + abs(1.02 * vmc0 - vmc0) > 4.0 * sig0, None, None, None)) + # A depth-40 union chain of 41 disjoint boxes must convert, with a closed-form volume. + chain = ROOT.TGeoBBox("chain0", 1, 1, 1) + ROOT.SetOwnership(chain, False) + for i in range(1, 41): + box = ROOT.TGeoBBox(f"chain{i}", 1, 1, 1) + shift = ROOT.TGeoTranslation(f"chainT{i}", 2.5 * i, 0, 0) + node = ROOT.TGeoUnion(chain, box, ROOT.nullptr, shift) + for obj in (box, shift, node): + ROOT.SetOwnership(obj, False) + chain = ROOT.TGeoCompositeShape(f"chainC{i}", node) + ROOT.SetOwnership(chain, False) + try: + v_chain = solid_volume_mm3(shape_to_occ(chain, SCALE_TO_MM)) / 1000.0 + ok_chain = abs(v_chain - 41 * 8.0) <= 1.0e-9 * 41 * 8.0 + chain_detail = f"OCC {v_chain:.9f} vs closed form {41 * 8.0}" + except Exception as e: + ok_chain, chain_detail = False, f"{type(e).__name__}: {e}" + r2.append((f"a depth-40 union chain converts exactly ({chain_detail})", ok_chain, + None, None, None)) + # ... and the guard still refuses loudly past the real bound. + try: + shape_to_occ(chain, SCALE_TO_MM, MAX_BOOLEAN_DEPTH) + guarded, guard_msg = False, "no exception" + except ShapeDeclined as e: + guarded, guard_msg = str(MAX_BOOLEAN_DEPTH) in str(e), str(e) + r2.append((f"the depth guard still refuses past {MAX_BOOLEAN_DEPTH}", guarded, + None, None, None)) + total += len(r2) + failures += _print_suite("composites vs an independent MC of TGeo (N=200k, 4 sigma)", r2) + + # ---- suite 2b: point-by-point Contains agreement, TGeo vs OCCT ---- + from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier + from OCC.Core.TopAbs import TopAbs_IN + r2b = [] + for (label, cs) in composites[:2] + [("pcon rmin>0", None)]: + if cs is None: + cs = ROOT.TGeoPcon("pcx", 0, 360, 3) + cs.DefineSection(0, -1, 0.5, 1) + cs.DefineSection(1, 0, 0.5, 2) + cs.DefineSection(2, 1, 0.8, 2) + occ = shape_to_occ(cs, SCALE_TO_MM) + clf = BRepClass3d_SolidClassifier(occ) + dx, dy, dz = cs.GetDX(), cs.GetDY(), cs.GetDZ() + o = cs.GetOrigin() + pt = array.array("d", [0.0, 0.0, 0.0]) + bad = skipped = 0 + ntot = 3000 + for _ in range(ntot): + pt[0] = o[0] + rngc.uniform(-dx, dx) + pt[1] = o[1] + rngc.uniform(-dy, dy) + pt[2] = o[2] + rngc.uniform(-dz, dz) + tin = bool(cs.Contains(pt)) + if cs.Safety(pt, tin) < 1e-6: # on the surface: not a fair question + skipped += 1 + continue + clf.Perform(gp_Pnt(pt[0] * SCALE_TO_MM, pt[1] * SCALE_TO_MM, + pt[2] * SCALE_TO_MM), 1e-7) + if (clf.State() == TopAbs_IN) != tin: + bad += 1 + print(f" {label:40s} {ntot - skipped} scored, {bad} disagreements" + f" ({skipped} within 1e-6 cm of a surface)") + r2b.append((f"Contains agrees, TGeo vs OCCT: {label}", bad == 0, None, None, None)) + total += len(r2b) + failures += _print_suite("Contains agreement, TGeo vs OCCT classifier (3000 pts each)", r2b) + + # ---- suite 3: placement transforms ---- + r3 = [] + rng = random.Random(20260822) + for k, m in enumerate([ + ROOT.TGeoTranslation("t", 1.5, -2.5, 3.5), + ROOT.TGeoRotation("r", 30, 40, 50), + ROOT.TGeoCombiTrans("ct", 1, 2, 3, ROOT.TGeoRotation("r2", 11, 22, 33)), + ]): + t = _isometry_trsf(*tgeo_matrix_components(m), proper_only=True)[0] + worst = 0.0 + for _ in range(200): + loc = [rng.uniform(-5, 5) for _ in range(3)] + mas = array.array("d", [0, 0, 0]) + m.LocalToMaster(array.array("d", loc), mas) + p = gp_Pnt(loc[0] * SCALE_TO_MM, loc[1] * SCALE_TO_MM, loc[2] * SCALE_TO_MM) + p.Transform(t) + worst = max(worst, + abs(p.X() - mas[0] * SCALE_TO_MM), + abs(p.Y() - mas[1] * SCALE_TO_MM), + abs(p.Z() - mas[2] * SCALE_TO_MM)) + ok = worst < 1e-9 + r3.append((f"{m.ClassName()} LocalToMaster vs gp_Trsf (200 pts, mm)", ok, + None, None, None)) + print(f" worst |delta| = {worst:.3e} mm") + # a reflection must be refused as a rigid placement and offered as a GTrsf + refl = ROOT.TGeoRotation("refl") + refl.ReflectZ(True) + r3.append(("reflecting TGeoRotation refused as a gp_Trsf", + _isometry_trsf(*tgeo_matrix_components(refl), proper_only=True)[0] is None, + None, None, None)) + box = shape_to_occ(ROOT.TGeoBBox("rb", 1, 2, 3), SCALE_TO_MM) + mirrored = apply_tgeo_matrix(box, refl, "reflection test") + r3.append(("reflected box keeps its volume (baked as an exact isometry)", + abs(solid_volume_mm3(mirrored) - solid_volume_mm3(box)) < 1e-6, + None, None, None)) + total += len(r3) + failures += _print_suite("placement transforms", r3) + + # ---- suite 4: negative controls ---- + r4 = [] + # each of these compares a deliberately WRONG TGeo shape against our solid for + # the RIGHT one; the band must reject it, or the band proves nothing. + good_tube = shape_to_occ(ROOT.TGeoTube("ngt", 1, 2, 5), SCALE_TO_MM) + _cap_check("wrong rmin: Capacity(0,2,5) vs solid(1,2,5) must FAIL", + ROOT.TGeoTube("ngt2", 0, 2, 5), 1e-9, r4, expect_fail=True, occ=good_tube) + good_pcon = shape_to_occ(pc2, SCALE_TO_MM) + pc2b = ROOT.TGeoPcon("pc2b", 0, 360, 3) + pc2b.DefineSection(0, -1, 0.5, 1) + pc2b.DefineSection(1, 0, 0.5, 2) + pc2b.DefineSection(2, 1, 0.9, 2) # rmin 0.8 -> 0.9 + _cap_check("wrong pcon rmin (0.8 -> 0.9) must FAIL", + pc2b, 1e-9, r4, expect_fail=True, occ=good_pcon) + good_pgon = shape_to_occ(pg, SCALE_TO_MM) + pgb = ROOT.TGeoPgon("pgb", 0, 360, 7, 2) # 6 -> 7 edges + pgb.DefineSection(0, -1, 0, 1) + pgb.DefineSection(1, 1, 0, 1) + _cap_check("wrong pgon nedges (6 -> 7) must FAIL", + pgb, 1e-9, r4, expect_fail=True, occ=good_pgon) + # and a control on the control: the band accepts the right answer + _cap_check("same pgon accepted (control on the control)", pg, 1e-9, r4, occ=good_pgon) + total += len(r4) + failures += _print_suite("negative controls (a wrong parameter must be rejected)", r4) + + # ---- suite 5: analytic surface types ---- + # The carriers must be the analytic surfaces TGeo meant, not B-splines. + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.GeomAbs import ( + GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone, GeomAbs_Sphere, GeomAbs_Torus, + GeomAbs_BezierSurface, GeomAbs_BSplineSurface, GeomAbs_SurfaceOfRevolution, + GeomAbs_SurfaceOfExtrusion, GeomAbs_OffsetSurface, GeomAbs_OtherSurface) + _SNAME = {GeomAbs_Plane: "plane", GeomAbs_Cylinder: "cylinder", GeomAbs_Cone: "cone", + GeomAbs_Sphere: "sphere", GeomAbs_Torus: "torus", + GeomAbs_BezierSurface: "bezier", GeomAbs_BSplineSurface: "bspline", + GeomAbs_SurfaceOfRevolution: "revolution", + GeomAbs_SurfaceOfExtrusion: "extrusion", + GeomAbs_OffsetSurface: "offset", GeomAbs_OtherSurface: "other"} + + def face_types(shape): + import collections as _c + c = _c.Counter() + ex = TopExp_Explorer(shape, TopAbs_FACE) + while ex.More(): + c[_SNAME.get(BRepAdaptor_Surface(topods.Face(ex.Current())).GetType(), "?")] += 1 + ex.Next() + return dict(c) + + pgf = ROOT.TGeoPgon("pgf", 0, 360, 6, 2) + pgf.DefineSection(0, -1, 0.5, 1) + pgf.DefineSection(1, 1, 0.5, 1) + vtw = array.array("d", [-1, -1, -1, 1, 1, 1, 1, -1, + -1.5, -0.5, -0.5, 1.5, 1.5, 0.5, 0.5, -1.5]) + r6 = [] + for nm, tsh, want in [ + ("TGeoBBox", ROOT.TGeoBBox("fb", 1, 2, 3), {"plane": 6}), + ("TGeoTube", ROOT.TGeoTube("ft", 1, 2, 5), {"plane": 2, "cylinder": 2}), + ("TGeoTubeSeg", ROOT.TGeoTubeSeg("fts", 1, 2, 5, 30, 150), + {"plane": 4, "cylinder": 2}), + ("TGeoCone", ROOT.TGeoCone("fc", 3, 1, 2, 0.5, 4), {"plane": 2, "cone": 2}), + ("TGeoPcon", pc2, {"plane": 2, "cylinder": 2, "cone": 2}), + ("TGeoSphere", ROOT.TGeoSphere("fs", 1, 2, 30, 120, 20, 200), + {"sphere": 2, "cone": 2, "plane": 2}), + ("TGeoTorus", ROOT.TGeoTorus("fto", 10, 1, 2, 45, 120), {"torus": 2, "plane": 2}), + ("TGeoEltu", ROOT.TGeoEltu("fe", 2, 3, 4), {"extrusion": 1, "plane": 2}), + ("TGeoTrd1", ROOT.TGeoTrd1("fd1", 1, 2, 3, 4), {"plane": 6}), + ("TGeoTrd2", ROOT.TGeoTrd2("fd2", 1, 2, 3, 4, 5), {"plane": 6}), + ("TGeoXtru", x, {"plane": 6}), + ("TGeoPgon hollow", pgf, {"plane": 14}), + ("TGeoTrap", ROOT.TGeoTrap("ftp", 2, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0), {"plane": 6}), + ("TGeoArb8 planar", ROOT.TGeoArb8("fa8", 1.0, arb8v), {"plane": 6}), + ("TGeoArb8 twisted", ROOT.TGeoArb8("fa8t", 1.0, vtw), {"plane": 2, "bspline": 4}), + ("TGeoCtub", ROOT.TGeoCtub("fct", 0, 1, 1, 0, 360, 0, -0.6, -0.8, 0, 0.6, 0.8), + {"cylinder": 1, "plane": 2}), + ]: + try: + got = face_types(shape_to_occ(tsh, SCALE_TO_MM)) + ok = got == want + if not ok: + nm = f"{nm} (got {got}, want {want})" + except Exception as e: + ok = False + nm = f"{nm} [{type(e).__name__}: {e}]" + r6.append((f"{nm}", ok, None, None, None)) + total += len(r6) + failures += _print_suite("analytic surface types of every face", r6) + + # ---- suite 6: the XCAF document, written and read back ---- + r5 = [] + import tempfile + mgr = ROOT.TGeoManager("stmgr", "self-test") + vac = mgr.MakeBox("world", ROOT.nullptr, 50, 50, 50) + mgr.SetTopVolume(vac) + inner = mgr.MakeTube("innertube", ROOT.nullptr, 1, 2, 5) + vac.AddNode(inner, 1, ROOT.TGeoTranslation(3, 0, 0)) + vac.AddNode(inner, 2, ROOT.TGeoTranslation(-3, 0, 0)) + grp = mgr.MakeVolumeAssembly("grp") + leaf = mgr.MakeBox("leafbox", ROOT.nullptr, 1, 1, 1) + grp.AddNode(leaf, 1, ROOT.TGeoTranslation(0, 4, 0)) + vac.AddNode(grp, 1, ROOT.TGeoTranslation(0, 0, 7)) + mgr.CloseGeometry() + + class _O: + pass + o = _O() + o.quiet = True + o.verify = True + o.mother_bodies = True + o.skip_top_body = False + o.carve_mothers = False + o.include_name = None + o.dedup_world = False + conv = TGeoToStep(o) + conv.build(mgr.GetTopVolume()) + tmp = os.path.join(tempfile.mkdtemp(), "selftest.step") + conv.write(tmp) + rep = conv.report("in-memory", tmp) + r5.append(("STEP file written", os.path.getsize(tmp) > 0, None, None, None)) + r5.append(("one definition per logical volume (3 shaped, 2 assemblies)", + rep["definitions"] == 3 and rep["assemblies"] == 2, None, None, None)) + r5.append(("shared tube emitted once, placed twice", + conv.ncomponents == 5, None, None, None)) + + from OCC.Core.STEPCAFControl import STEPCAFControl_Reader + d2 = TDocStd_Document("rb") + rd = STEPCAFControl_Reader() + rd.SetNameMode(True) + r5.append(("STEP reads back", rd.ReadFile(tmp) == IFSelect_RetDone, None, None, None)) + rd.Transfer(d2) + st2 = XCAFDoc_DocumentTool.ShapeTool(d2.Main()) + roots = TDF_LabelSequence() + st2.GetFreeShapes(roots) + r5.append(("exactly one free shape (the top assembly)", + roots.Length() == 1, None, None, None)) + names = [] + leaves = [] + + def walk(lb): + ch = TDF_LabelSequence() + st2.GetComponents(lb, ch) + if ch.Length() == 0: + leaves.append(lb) + names.append(lb.GetLabelName()) + return + for i in range(ch.Length()): + c = ch.Value(i + 1) + if st2.IsReference(c): + ref = TDF_Label() + st2.GetReferredShape(c, ref) + walk(ref) + else: + walk(c) + + walk(roots.Value(1)) + r5.append(("names survive the write/read (innertube present)", + "innertube" in names, None, None, None)) + r5.append(("the mother body is a named leaf (world__body)", + "world__body" in names, None, None, None)) + r5.append((f"leaf occurrences == placements ({len(leaves)} == 4)", + len(leaves) == 4, None, None, None)) + total += len(r5) + failures += _print_suite("XCAF assembly document, written and read back", r5) + + # ---- suite 7: the definition cache is keyed on identity, not on the name ---- + # Volume names need not be unique: check the keying and the value signature sharing rests on. + r7 = [] + sig_a = shape_signature(ROOT.TGeoTube("sgA", 1, 2, 5)) + sig_b = shape_signature(ROOT.TGeoTube("sgB", 1, 2, 5)) + sig_c = shape_signature(ROOT.TGeoTube("sgC", 0, 2, 5)) + r7.append(("two equal tubes have equal signatures", sig_a == sig_b, + None, None, None)) + r7.append(("a wrong rmin changes the signature (negative control)", + sig_a != sig_c, None, None, None)) + sgp1 = ROOT.TGeoPcon("sgp1", 0, 360, 3) + sgp2 = ROOT.TGeoPcon("sgp2", 0, 360, 3) + for p, rin in ((sgp1, 0.5), (sgp2, 0.9)): + p.DefineSection(0, -1, 0.5, 1) + p.DefineSection(1, 0, 0.5, 2) + p.DefineSection(2, 1, rin, 2) + r7.append(("a wrong pcon inner radius changes the signature (negative control)", + shape_signature(sgp1) != shape_signature(sgp2), None, None, None)) + _kc = [ROOT.TGeoBBox("kca", 2, 2, 2), ROOT.TGeoTube("kcb", 0, 1, 3)] + cs1 = ROOT.TGeoCompositeShape("kc1", "kca - kcb") + cs2 = ROOT.TGeoCompositeShape("kc2", "kca - kcb") + r7.append(("a composite is keyed on its address, never shared by value", + shape_signature(cs1) != shape_signature(cs2) + and shape_signature(cs1) == shape_signature(cs1), None, None, None)) + + mgr2 = ROOT.TGeoManager("stmgr2", "name-collision self-test") + w2 = mgr2.MakeBox("nworld", ROOT.nullptr, 50, 50, 50) + mgr2.SetTopVolume(w2) + dupA = mgr2.MakeTube("dup", ROOT.nullptr, 0, 2, 5) # two volumes, one name, + dupB = mgr2.MakeTube("dup", ROOT.nullptr, 0, 1, 5) # four times the volume + same1 = mgr2.MakeBox("same", ROOT.nullptr, 1, 1, 1) # two volumes, one name, + same2 = mgr2.MakeBox("same", ROOT.nullptr, 1, 1, 1) # one shape + w2.AddNode(dupA, 1, ROOT.TGeoTranslation(-10, 0, 0)) + w2.AddNode(dupB, 1, ROOT.TGeoTranslation(10, 0, 0)) + w2.AddNode(same1, 1, ROOT.TGeoTranslation(0, -10, 0)) + w2.AddNode(same2, 1, ROOT.TGeoTranslation(0, 10, 0)) + mgr2.CloseGeometry() + conv2 = TGeoToStep(o) + conv2.build(mgr2.GetTopVolume()) + rep2 = conv2.report("in-memory", "none") + emitted2 = sorted(r["emittedName"] for r in rep2["volumes"] if r["converted"]) + r7.append((f"one name, two shapes -> two definitions {emitted2}", + emitted2 == ["dup", "dup#2", "nworld", "same"], None, None, None)) + r7.append(("the disambiguation is recorded in the report", + rep2["nameDisambiguation"] == {"dup": ["dup", "dup#2"]}, + None, None, None)) + r7.append(("one name, one shape -> still one definition, placed twice", + rep2["definitions"] == 4 and conv2.ncomponents == 5, + None, None, None)) + devs2 = [r["relDev"] for r in rep2["volumes"] if r.get("relDev") is not None] + shared2 = rep2["sharedDefinitionMaxRelDev"] + print(f" every definition vs its own volume's Capacity(): worst " + f"{max(devs2):.3e}, worst over a *shared* definition {shared2:.3e}") + r7.append(("every volume gets a solid that is its own shape", + max(devs2) <= 1e-9 and shared2 <= 1e-9, None, None, None)) + capA = float(dupA.GetShape().Capacity()) + capB = float(dupB.GetShape().Capacity()) + ratio = abs(capA - capB) / capB + print(f" a name-keyed cache would have given one of them the other's solid:" + f" {capA:.6f} vs {capB:.6f} cm3, {ratio:.2f} relative") + r7.append((f"the test could have failed: the two shapes differ by {ratio:.2f}", + ratio > 1e-2, None, None, None)) + total += len(r7) + failures += _print_suite("definition cache keyed on volume identity", r7) + + # ---- suite 8: baking a reflection is an isometry, and keeps the carriers ---- + # Volume and carriers are asserted; the gp_GTrsf route is the negative control. + r8 = [] + mtube = ROOT.TGeoTube("mt", 4, 5, 10) + occ_t = shape_to_occ(mtube, SCALE_TO_MM) + v_t = solid_volume_mm3(occ_t) + f_t = face_types(occ_t) + mir_t = mirror_solid_z(occ_t, "self-test tube") + v_m = solid_volume_mm3(mir_t) + f_m = face_types(mir_t) + rel_t = abs(v_m - v_t) / v_t + g = gp_GTrsf() + g.SetVectorialPart(gp_Mat(1, 0, 0, 0, 1, 0, 0, 0, -1)) + old = BRepBuilderAPI_GTransform(occ_t, g, True).Shape() + v_o = solid_volume_mm3(old) + f_o = face_types(old) + rel_o = abs(v_o - v_t) / v_t + print(f" tube {f_t} -> gp_Trsf mirror {f_m}, rel {rel_t:.3e}") + print(f" the retired gp_GTrsf route: {f_o}, rel {rel_o:.3e}") + r8.append((f"a mirrored tube keeps its volume (rel {rel_t:.3e})", + rel_t <= 1e-12, None, None, None)) + r8.append((f"a mirrored tube keeps its analytic faces {f_m}", + f_m == f_t and sum(f_m.get(k, 0) for k in + ("bspline", "bezier", "revolution")) == 0, + None, None, None)) + r8.append((f"the retired gp_GTrsf route is wrong by {rel_o:.3e} and all " + f"B-spline (negative control)", + rel_o > 1e-3 and f_o.get("bspline", 0) == 4, None, None, None)) + mpc = ROOT.TGeoPcon("mpc", 0, 360, 3) + mpc.DefineSection(0, -1, 0.5, 1) + mpc.DefineSection(1, 0, 0.5, 2) + mpc.DefineSection(2, 1, 0.8, 2) + occ_p = shape_to_occ(mpc, SCALE_TO_MM) + mir_p = mirror_solid_z(occ_p, "self-test pcon") + cap_p = float(mpc.Capacity()) + rel_p = abs(solid_volume_mm3(mir_p) / 1000.0 - cap_p) / cap_p + r8.append((f"a mirrored Pcon matches its analytic Capacity() ({rel_p:.3e})", + rel_p <= 1e-9, None, None, None)) + r8.append(("a mirrored Pcon keeps its analytic faces", + face_types(mir_p) == face_types(occ_p), None, None, None)) + rotxz = ROOT.TGeoRotation("rotxz_st", 90., 0., 90., 90., 180., 0.) + baked = apply_tgeo_matrix(occ_t, rotxz, "self-test rotxz") + r8.append(("apply_tgeo_matrix takes a real reflecting TGeoRotation exactly", + abs(solid_volume_mm3(baked) - v_t) <= 1e-12 * v_t + and face_types(baked) == f_t, None, None, None)) + r8.append(("the mirrored solid is not inside out", + _signed_volume(mir_t) > 0, None, None, None)) + bad = gp_Trsf() + bad.SetScale(gp_Pnt(0, 0, 0), 1.01) + try: + apply_isometry(occ_t, bad, "not an isometry") + caught = False + except ShapeDeclined: + caught = True + r8.append(("the volume invariant rejects a transform that is not an isometry " + "(negative control)", caught, None, None, None)) + # A real hand-written rotation must be snapped, not refused. + sloppy = [[+0.681268213, 0.0, +0.732033940], + [0.0, 1.0, 0.0], + [-0.732033894, 0.0, +0.681268164]] # TRD BM49/B051_1, verbatim + dev0 = orthogonality_deviation(sloppy) + fixed, dev1, corr = orthonormalise(sloppy) + print(f" TRD BM49/B051_1: |M^T M - I| {dev0:.3e} -> " + f"{orthogonality_deviation(fixed):.3e}, rotation moved by {corr:.3e}") + r8.append((f"a hand-written rotation is snapped to an exact one " + f"({dev0:.2e} -> {orthogonality_deviation(fixed):.2e})", + dev0 > 1e-9 and orthogonality_deviation(fixed) < 1e-14 + and 0.0 < corr < 1e-6, None, None, None)) + exact_rot = tgeo_matrix_components(ROOT.TGeoRotation("orr", 30, 40, 50))[0] + r8.append((f"an exact rotation is left alone to the double-precision floor " + f"({orthonormalise(exact_rot)[2]:.1e})", + orthonormalise(exact_rot)[2] <= 1e-15, None, None, None)) + sloppy_refl = [[r[0], r[1], -r[2]] for r in sloppy] + r8.append(("the snap keeps a reflection a reflection", + _det3(orthonormalise(sloppy_refl)[0]) < 0, None, None, None)) + r8.append(("a genuine non-uniform scale is still refused as a placement " + "(negative control)", + _isometry_trsf([[1., 0., 0.], [0., 1., 0.], [0., 0., 2.]], + [0., 0., 0.], True)[0] is None, None, None, None)) + total += len(r8) + failures += _print_suite("mirror baking: exact isometry, analytic carriers", r8) + + # ---- suite 9: a reflected subtree is emitted, and lands where TGeo puts it -- + # TGeoManager's world matrix is the oracle for where the mirrored leaves land. + r9 = [] + mgr3 = ROOT.TGeoManager("stmgr3", "reflected-subtree self-test") + w3 = mgr3.MakeBox("rworld", ROOT.nullptr, 100, 100, 100) + mgr3.SetTopVolume(w3) + grp3 = mgr3.MakeVolumeAssembly("rgrp") # an assembly: no solid to bake + rtube = mgr3.MakeTube("rtube", ROOT.nullptr, 1, 2, 5) + rbox = mgr3.MakeBox("rbox", ROOT.nullptr, 1, 2, 3) + rflip = mgr3.MakeBox("rflip", ROOT.nullptr, 1, 1, 4) + refl3 = ROOT.TGeoRotation("reflz3") + refl3.ReflectZ(True) + grp3.AddNode(rtube, 1, ROOT.TGeoTranslation(0, 0, 7)) + grp3.AddNode(rbox, 1, ROOT.TGeoCombiTrans(3, 0, 2, + ROOT.TGeoRotation("rr3", 20, 30, 40))) + grp3.AddNode(rflip, 1, ROOT.TGeoCombiTrans(0, 4, 1, refl3)) # already mirrored + w3.AddNode(grp3, 1, ROOT.TGeoTranslation(0, 0, 20)) + w3.AddNode(grp3, 2, ROOT.TGeoCombiTrans(0, 0, -20, refl3)) + mgr3.CloseGeometry() + conv3 = TGeoToStep(o) + conv3.build(mgr3.GetTopVolume()) + tmp3 = os.path.join(tempfile.mkdtemp(), "reflected.step") + conv3.write(tmp3) + + d3 = TDocStd_Document("rb3") + rd3 = STEPCAFControl_Reader() + rd3.SetNameMode(True) + rd3.ReadFile(tmp3) + rd3.Transfer(d3) + st3 = XCAFDoc_DocumentTool.ShapeTool(d3.Main()) + roots3 = TDF_LabelSequence() + st3.GetFreeShapes(roots3) + from OCC.Core.TopLoc import TopLoc_Location as _TL + found3 = {} + + def _walk3(lab, loc): + ch = TDF_LabelSequence() + st3.GetComponents(lab, ch) + if ch.Length() == 0: + t = loc.Transformation() + mat = [[t.Value(i + 1, j + 1) for j in range(3)] for i in range(3)] + tr = [t.Value(i + 1, 4) for i in range(3)] + nm = str(lab.GetLabelName()) + if nm.endswith("__mirrored"): + mat = [[mat[i][0], mat[i][1], -mat[i][2]] for i in range(3)] + found3.setdefault(nm, []).append((mat, tr)) + return + for i in range(ch.Length()): + c = ch.Value(i + 1) + cloc = loc.Multiplied(st3.GetLocation(c)) + if st3.IsReference(c): + ref = TDF_Label() + st3.GetReferredShape(c, ref) + _walk3(ref, cloc) + else: + _walk3(c, cloc) + + for i in range(roots3.Length()): + _walk3(roots3.Value(i + 1), _TL()) + + def _tgeo_world(path): + if not mgr3.cd(path): + return None + gm = mgr3.GetCurrentMatrix() + rr = gm.GetRotationMatrix() + tt = gm.GetTranslation() + return ([[float(rr[3 * i + j]) for j in range(3)] for i in range(3)], + [float(tt[i]) * SCALE_TO_MM for i in range(3)]) + + def _worst(step_entries, want): + best = None + for (mat, tr) in step_entries: + d = max(max(abs(mat[i][j] - want[0][i][j]) for j in range(3)) + for i in range(3)) + d = max(d, max(abs(tr[i] - want[1][i]) for i in range(3))) + if best is None or d < best: + best = d + return best if best is not None else float("inf") + + nleaf3 = sum(len(v) for v in found3.values()) + r9.append((f"exactly one free shape, no orphaned subtree " + f"({roots3.Length()} root(s))", roots3.Length() == 1, + None, None, None)) + r9.append((f"every leaf occurrence is emitted ({nleaf3} == 7)", + nleaf3 == 7, None, None, None)) + for (nm, path, mirror_expected) in ( + ("rtube", "/rworld_1/rgrp_2/rtube_1", True), + ("rbox", "/rworld_1/rgrp_2/rbox_1", True), + ("rflip", "/rworld_1/rgrp_2/rflip_1", False), + ("rtube", "/rworld_1/rgrp_1/rtube_1", False)): + want = _tgeo_world(path) + key3 = nm + ("__mirrored" if mirror_expected else "") + got = found3.get(key3, []) + d = _worst(got, want) if want else float("inf") + r9.append((f"{path} lands where TGeo puts it, as `{key3}` (worst " + f"|delta| {d:.2e} mm)", bool(got) and d < 1e-9, + None, None, None)) + # Negative control: the prototype placed at M instead of M*Z must land elsewhere. + want = _tgeo_world("/rworld_1/rgrp_2/rbox_1") + wrong = [([[m[i][0], m[i][1], -m[i][2]] for i in range(3)], t) + for (m, t) in found3.get("rbox__mirrored", [])] + dwrong = _worst(wrong, want) + r9.append((f"the un-conjugated convention would be wrong by {dwrong:.2e} mm " + f"(negative control)", dwrong > 1e-6, None, None, None)) + # rflip is reflected inside rgrp, so the parities multiply: each prototype shows up once. + r9.append(("a reflection under a reflection is the plain volume again", + len(found3.get("rflip", [])) == 1 + and len(found3.get("rflip__mirrored", [])) == 1, + None, None, None)) + r9.append((f"3 mirrored solid definitions carry 4 mirrored components, rather " + f"than one bake per placement ({conv3.nbaked}, " + f"{conv3.nmirrored_components})", + conv3.nbaked == 3 and conv3.nmirrored_components == 4, + None, None, None)) + total += len(r9) + failures += _print_suite("reflected subtrees: mirrored prototypes, placed", r9) + + # ---- suite 10: the TGeoPgon z-step, and the collinear-face guard ---------- + # Two sections at one z give collinear closure points; the Newell-area guard keeps the shell valid. + r10 = [] + shift10 = 1.5 / math.sin(math.radians(10.0)) + pg10 = ROOT.TGeoPgon("st_zstep", 0.0, 20.0, 1, 4) + pg10.DefineSection(0, -3.5, 86.3 - shift10, 240.4 - shift10) + pg10.DefineSection(1, -1.5, 86.3 - shift10, 240.4 - shift10) + pg10.DefineSection(2, -1.5, 86.3 - shift10, 243.4 - shift10) + pg10.DefineSection(3, 3.5, 86.3 - shift10, 243.4 - shift10) + occ10 = shape_to_occ(pg10) + from OCC.Core.BRepCheck import BRepCheck_Analyzer as _BCA10 + r10.append(("a z-step TGeoPgon builds a VALID shell (TPC_WSEG's tpc_hole)", + _BCA10(occ10).IsValid(), None, None, None)) + v10 = solid_volume_mm3(occ10) / 1000.0 + d10 = abs(v10 - pg10.Capacity()) / pg10.Capacity() + r10.append((f"its volume matches Capacity() ({d10:.2e})", d10 < 1e-9, None, None, None)) + r10.append(("three collinear points yield no face (the guard, negative control)", + _quad_face((0, 0, 0), (1, 0, 0), (2, 0, 0), (1, 0, 0), "st") is None, + None, None, None)) + r10.append(("a genuine triangle still yields a face", + _quad_face((0, 0, 0), (1, 0, 0), (0, 1, 0), (0, 0, 0), "st") is not None, + None, None, None)) + total += len(r10) + failures += _print_suite("TGeoPgon z-step and the collinear-face guard", r10) + + # ---- the media sidecar ------------------------------------------------- + # No medium may lose a parameter and no mixture an element; Air_F/Air_NF differ only in ifield. + r11 = [] + mgr11 = ROOT.TGeoManager("mediatest", "media sidecar") + mix11 = ROOT.TGeoMixture("Air", 4, 0.00120479) + mix11.AddElement(12.0107, 6.0, 0.000124) + mix11.AddElement(14.0067, 7.0, 0.755267) + mix11.AddElement(15.9994, 8.0, 0.231781) + mix11.AddElement(39.9480, 18.0, 0.012827) + fe11 = ROOT.TGeoMaterial("Fe", 55.85, 26.0, 7.87) + med_f = ROOT.TGeoMedium("Air_F", 1, mix11, ROOT.nullptr) + med_f.SetParam(1, 1.0) # ifield: in field + med_f.SetParam(4, 0.75) # stemax + med_n = ROOT.TGeoMedium("Air_NF", 2, mix11, ROOT.nullptr) + med_n.SetParam(1, 0.0) # ifield: field free + med_n.SetParam(4, 0.75) + med_fe = ROOT.TGeoMedium("Fe", 3, fe11, ROOT.nullptr) + + top11 = mgr11.MakeBox("top", med_f, 50, 50, 50) + mgr11.SetTopVolume(top11) + a11 = mgr11.MakeBox("a", med_n, 5, 5, 5) + b11 = mgr11.MakeBox("b", med_fe, 5, 5, 5) + top11.AddNode(a11, 1, ROOT.TGeoTranslation(10, 0, 0)) + top11.AddNode(b11, 1, ROOT.TGeoTranslation(-10, 0, 0)) + mgr11.CloseGeometry() + + class _O11: + pass + o11 = _O11() + o11.quiet = True + o11.verify = False + o11.mother_bodies = True + o11.skip_top_body = False + o11.carve_mothers = False + o11.include_name = None + o11.dedup_world = False + o11.hollow_volumes = [] + o11.hollow_tag = None + conv11 = TGeoToStep(o11) + conv11.build(top11) + side11 = conv11.media_sidecar("in-memory") + + r11.append((f"every emitted part carries a medium ({side11['nParts']} parts)", + side11["nParts"] >= 3, None, None, None)) + r11.append(("a mother's own body leaf is in the table, not just its assembly " + "label (else every mother comes back transparent)", + side11["parts"].get("top__body") == "Air_F", None, None, None)) + r11.append((f"the three media are collected once each ({side11['nMedia']})", + side11["nMedia"] == 3, None, None, None)) + r11.append(("each part names its own medium", + side11["parts"].get("top__body") == "Air_F" + and side11["parts"].get("a") == "Air_NF" + and side11["parts"].get("b") == "Fe", None, None, None)) + mf, mn = side11["media"]["Air_F"], side11["media"]["Air_NF"] + r11.append(("the in-field and field-free twins differ ONLY in ifield", + mf["params"]["ifield"] == 1.0 and mn["params"]["ifield"] == 0.0 + and all(mf["params"][k] == mn["params"][k] + for k in MEDIUM_PARAM_NAMES if k != "ifield"), + None, None, None)) + r11.append(("all eight medium parameters are recorded, in Geant's order", + list(mf["params"]) == list(MEDIUM_PARAM_NAMES), None, None, None)) + r11.append(("stemax survives (the parameter a medium loses most quietly)", + abs(mf["params"]["stemax"] - 0.75) < 1e-12, None, None, None)) + r11.append(("a mixture keeps all four elements with their weights", + mf["material"]["isMixture"] and mf["material"]["nElements"] == 4 + and abs(sum(e["W"] for e in mf["material"]["elements"]) - 1.0) < 1e-6, + None, None, None)) + r11.append(("a plain material is not reported as a mixture, and keeps Z/A/rho", + not side11["media"]["Fe"]["material"]["isMixture"] + and side11["media"]["Fe"]["material"]["Z"] == 26.0 + and abs(side11["media"]["Fe"]["material"]["density"] - 7.87) < 1e-9, + None, None, None)) + r11.append(("radiation and interaction length are carried, not recomputed", + mf["material"]["radLen"] > 0.0 and mf["material"]["intLen"] > 0.0, + None, None, None)) + r11.append(("the sidecar says which part is the body of which assembly, so " + "the converter can put the mother/daughter nesting back", + side11["bodyOfAssembly"].get("top__body") == "top", + None, None, None)) + # --hollow-volume: the hall is structure the CAD run already has. + o11.hollow_volumes = ["top"] + conv11h = TGeoToStep(o11) + conv11h.build(top11) + side11h = conv11h.media_sidecar("in-memory") + r11.append(("a hollow volume emits no body of its own", + "top__body" not in side11h["parts"], None, None, None)) + r11.append(("its daughters are still emitted, with their media", + side11h["parts"].get("a") == "Air_NF" + and side11h["parts"].get("b") == "Fe", None, None, None)) + r11.append(("hollowing is opt-in: the same build without it keeps the body " + "(negative control)", + side11["parts"].get("top__body") == "Air_F", None, None, None)) + o11.hollow_tag = "MOD" + conv11t = TGeoToStep(o11) + conv11t.build(top11) + side11t = conv11t.media_sidecar("in-memory") + r11.append(("--hollow-tag renames only the hollowed volume, so two modules " + "converted from one world do not collide", + conv11t.records[[d for d in conv11t.records + if conv11t.records[d]["name"] == "top"][0]] + ["emittedName"].startswith("top_MOD") + and side11t["parts"].get("a") == "Air_NF", None, None, None)) + # A hollowed volume with NO daughters takes the leaf path, not the assembly one. + mgr11b = ROOT.TGeoManager("mediatest2", "hollow leaf") + fe11b = ROOT.TGeoMaterial("Fe2", 55.85, 26.0, 7.87) + med11b = ROOT.TGeoMedium("Fe2", 1, fe11b, ROOT.nullptr) + top11b = mgr11b.MakeBox("w", med11b, 50, 50, 50) + mgr11b.SetTopVolume(top11b) + leaf11b = mgr11b.MakeBox("hall", med11b, 5, 5, 5) + keep11b = mgr11b.MakeBox("keepme", med11b, 5, 5, 5) + top11b.AddNode(leaf11b, 1, ROOT.TGeoTranslation(10, 0, 0)) + top11b.AddNode(keep11b, 1, ROOT.TGeoTranslation(-10, 0, 0)) + mgr11b.CloseGeometry() + o11.hollow_volumes = ["hall"] + o11.hollow_tag = None + conv11b = TGeoToStep(o11) + conv11b.build(top11b) + side11b = conv11b.media_sidecar("in-memory") + r11.append(("a hollowed volume with no daughters is not emitted either", + "hall" not in side11b["parts"], None, None, None)) + r11.append(("its daughterless sibling still is (negative control)", + side11b["parts"].get("keepme") == "Fe2", None, None, None)) + + # --carve-mothers. These build their own managers, so they come last: gGeoManager follows + # the most recently created one. + mgr11c = ROOT.TGeoManager("carvetest", "all-solid daughters") + fe11c = ROOT.TGeoMaterial("Fe3", 55.85, 26.0, 7.87) + med11c = ROOT.TGeoMedium("Fe3", 1, fe11c, ROOT.nullptr) + top11c = mgr11c.MakeBox("cw", med11c, 50, 50, 50) + mgr11c.SetTopVolume(top11c) + solid11c = mgr11c.MakeBox("csolid", med11c, 5, 5, 5) + top11c.AddNode(solid11c, 1) + mgr11c.CloseGeometry() + o11.hollow_volumes = [] + o11.hollow_tag = None + o11.carve_mothers = True + conv11c = TGeoToStep(o11) + conv11c.build(top11c) + side11c = conv11c.media_sidecar("in-memory") + r11.append(("--carve-mothers reports a mother whose daughters are all solids as " + "completely carved, so the converter leaves it flat", + side11c["carvedComplete"].get("cw") is True, None, None, None)) + r11.append(("carving is opt-in: without it the sidecar makes no claim " + "(negative control)", + side11["carvedComplete"] == {}, None, None, None)) + + # An assembly daughter has no solid to subtract, so the carve is incomplete. + mgr11d = ROOT.TGeoManager("carvetest2", "assembly daughter") + fe11d = ROOT.TGeoMaterial("Fe4", 55.85, 26.0, 7.87) + med11d = ROOT.TGeoMedium("Fe4", 1, fe11d, ROOT.nullptr) + top11d = mgr11d.MakeBox("dw", med11d, 50, 50, 50) + mgr11d.SetTopVolume(top11d) + mother11d = mgr11d.MakeBox("dmother", med11d, 20, 20, 20) + asm11d = ROOT.TGeoVolumeAssembly("dasm") + inner11d = mgr11d.MakeBox("dinner", med11d, 2, 2, 2) + asm11d.AddNode(inner11d, 1) + mother11d.AddNode(asm11d, 1) + top11d.AddNode(mother11d, 1) + mgr11d.CloseGeometry() + conv11d = TGeoToStep(o11) + conv11d.build(top11d) + side11d = conv11d.media_sidecar("in-memory") + r11.append(("a mother whose daughter is an assembly reports an INCOMPLETE carve, " + "so the converter keeps nesting it", + side11d["carvedComplete"].get("dmother") is False, None, None, None)) + + # An incomplete carve must return the mother whole; asked of _carve directly. + _cbox = BRepPrimAPI_MakeBox(10.0, 10.0, 10.0).Shape() + _ccut = _moved(BRepPrimAPI_MakeBox(2.0, 2.0, 2.0).Shape(), gp_Trsf()) + _full = conv11d._carve(_cbox, [(_ccut, gp_Trsf())], "all-solid") + _part = conv11d._carve(_cbox, [(_ccut, gp_Trsf()), (None, gp_Trsf())], "mixed") + r11.append(("a carve with every daughter subtracted returns a new, smaller body", + _full[1] is True and _full[0] is not _cbox, None, None, None)) + r11.append(("a carve that cannot subtract every daughter returns the mother " + "WHOLE, so nesting stays correct", + _part[1] is False and _part[0] is _cbox, None, None, None)) + o11.carve_mothers = False + total += len(r11) + failures += _print_suite("the media/material sidecar", r11) + + print(f"\n{total} checks, {failures} failures") + sys.stdout.flush() + sys.stderr.flush() + # PyROOT double-frees loose TGeoShapes at teardown; leave first so the exit status is the verdict. + os._exit(1 if failures else 0) + + +# -------------------------------------------------------------------------- +# main +# -------------------------------------------------------------------------- + +def load_manager(path): + import ROOT + ROOT.gROOT.SetBatch(True) + geo = ROOT.TGeoManager.Import(path) + if geo is None: + raise RuntimeError(f"could not import a TGeoManager from {path}") + return geo + + +def main(argv=None): + ap = argparse.ArgumentParser(description="TGeo -> STEP (AP214) converter") + ap.add_argument("input", nargs="?", help="ROOT geometry file") + ap.add_argument("output", nargs="?", help="output .step file") + ap.add_argument("--report", default=None) + ap.add_argument("--hollow-volume", dest="hollow_volumes", action="append", + default=[], metavar="NAME", + help="emit this volume as a pure assembly: its daughters at their " + "own transforms, but no body of its own. Repeatable. Meant for " + "the experiment hall (cave, barrel, caveRB24), which o2-sim " + "builds natively whatever module list is asked for.") + ap.add_argument("--hollow-tag", default=None, metavar="TAG", + help="suffix the name of every --hollow-volume with _TAG. Two " + "modules converted from the same world would otherwise emit " + "the same hall volume names and collide when placed together.") + ap.add_argument("--media-json", default=None, + help="write the media/material sidecar here (default: " + "_media.json). The reverse converter reads it " + "with its own --media-json and rebuilds the media " + "verbatim instead of using a placeholder.") + ap.add_argument("--top", default=None) + ap.add_argument("--include-name", default=None) + ap.add_argument("--no-mother-bodies", dest="mother_bodies", action="store_false") + ap.add_argument("--skip-top-body", action="store_true") + ap.add_argument("--carve-mothers", action="store_true") + ap.add_argument("--dedup-world", action="store_true") + ap.add_argument("--no-step", dest="write_step", action="store_false", + help="build every solid and write the report, but skip the STEP " + "write (which is where OCCT gives out on very large models)") + ap.add_argument("--no-verify", dest="verify", action="store_false") + ap.add_argument("--quiet", action="store_true") + ap.add_argument("--self-test", action="store_true") + opts = ap.parse_args(argv) + + if opts.self_test: + return self_test() + if not opts.input or not opts.output: + ap.error("input and output are required (or use --self-test)") + + geo = load_manager(opts.input) + if opts.top: + vol = geo.GetVolume(opts.top) + if vol is None: + raise SystemExit(f"no volume named {opts.top}") + else: + vol = geo.GetTopVolume() + + conv = TGeoToStep(opts) + conv.log(f"walking {vol.GetName()} ...") + if opts.dedup_world: + conv.build_world(vol) + else: + conv.build(vol) + conv.log(f" {conv.nvolumes} logical volumes (by identity), " + f"{len(conv.definitions)} definitions, {conv.ncomponents} components") + if opts.write_step: + conv.log(f" writing {opts.output}") + conv.write(opts.output) + + rep = conv.report(opts.input, opts.output) + rpath = opts.report or (os.path.splitext(opts.output)[0] + "_report.json") + with open(rpath, "w") as f: + json.dump(rep, f, indent=1) + + media = conv.media_sidecar(opts.input) + mpath = opts.media_json or (os.path.splitext(opts.output)[0] + "_media.json") + with open(mpath, "w") as f: + json.dump(media, f, indent=1) + + print(f"{rep['definitions']} solids, {rep['assemblies']} volumes with daughters, " + f"{rep['pureAssemblies']} pure assemblies, {rep['components']} components, " + f"{rep['declined']} volumes declined") + if rep["maxRelDev"] is not None: + print(f"capacity check: max relative deviation {rep['maxRelDev']:.3e}, " + f"median {rep['medianRelDev']:.3e}") + if rep["coincidentPlacementsDropped"]: + print(f"{rep['coincidentPlacementsDropped']} coincident placement(s) dropped " + f"(--dedup-world); e.g. {rep['coincidentPlacementExamples'][:2]}") + if rep["nReflectedPlacements"]: + print(f"{rep['nReflectedPlacements']} reflecting placement(s); " + f"{rep['mirroredComponents']} component(s) place a mirrored prototype, " + f"drawn from {rep['mirroredPrototypes']} mirrored definition(s)") + if rep["scaledPlacementsBaked"]: + print(f"[WARN] {rep['scaledPlacementsBaked']} placement matrix/matrices are not " + f"isometries and were baked, not placed: " + f"{[r['placement'] for r in rep['scaledPlacements'][:3]]}") + if rep["orthonormalisedPlacements"]: + print(f"{rep['orthonormalisedPlacements']} placement matrix/matrices snapped to " + f"the nearest rotation; worst |M^T M - I| " + f"{rep['maxOrthogonalityDeviation']:.3e}, correction " + f"{rep['maxRotationCorrection']:.3e} ({rep['worstOrthogonalityPlacement']})") + if rep["nDisambiguatedNames"]: + ex = sorted(rep["nameDisambiguation"].items())[:3] + print(f"{rep['nDisambiguatedNames']} TGeo name(s) cover more than one definition " + f"and were disambiguated; e.g. {ex}") + if rep["sharedDefinitionMaxRelDev"] > 1e-6: + print(f" [WARN] a shared definition disagrees with a sharing volume's own " + f"capacity by {rep['sharedDefinitionMaxRelDev']:.3e} " + f"({rep['sharedDefinitionWorstVolume']})") + for cls, c in sorted(rep["byShapeClass"].items(), key=lambda kv: -(kv[1]["declined"])): + if c["declined"]: + print(f" declined {cls}: {c['declined']} ({c['reasons']})") + size = (f"{os.path.getsize(opts.output) / 1e6:.2f} MB" + if opts.write_step else "no STEP written (--no-step)") + print(f"report: {rpath} ({rep['wallSeconds']} s, {size})") + print(f"media: {mpath} ({media['nMedia']} media over {media['nParts']} parts)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/tools/cadsupport/__init__.py b/Detectors/CADSupport/tools/cadsupport/__init__.py new file mode 100644 index 0000000000000..a3b24188c52fe --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""The CSG path of the CAD -> TGeo converter: recognise a leaf solid as ROOT CSG, prove, emit.""" diff --git a/Detectors/CADSupport/tools/cadsupport/accept.py b/Detectors/CADSupport/tools/cadsupport/accept.py new file mode 100644 index 0000000000000..f7a73901eff76 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/accept.py @@ -0,0 +1,332 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Acceptance test 1 of 2: the OCCT symmetric-difference volume. + + volume(candidate - original) + volume(original - candidate) <= bandFactor * modelTolerance * area + +A zero difference is also what a failed build or an empty cut gives, so three guards refuse a false +accept: both cuts must report `IsDone()`, the original's volume and area must be positive, and the +candidate's volume must be positive and within a loose factor of the original's. +""" + +import math + +_BAND_FACTOR = 1.0 +# A candidate whose volume is off by more than this factor is a recogniser bug, not a near-miss. +_SANITY_VOLUME_RATIO = 4.0 + + +def model_tolerance_cm(shape): + """The largest tolerance over the shape's faces, edges and vertices, in the shape's own unit. + + It lives here because `recognise.py` needs it and must not import the emitter. + """ + from OCC.Core.BRep import BRep_Tool + from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_VERTEX + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + worst = 0.0 + for kind, getter in ((TopAbs_FACE, lambda s: BRep_Tool.Tolerance(topods.Face(s))), + (TopAbs_EDGE, lambda s: BRep_Tool.Tolerance(topods.Edge(s))), + (TopAbs_VERTEX, lambda s: BRep_Tool.Tolerance(topods.Vertex(s)))): + walk = TopExp_Explorer(shape, kind) + while walk.More(): + worst = max(worst, getter(walk.Current())) + walk.Next() + return worst + + +def contains_disagreements(original, cand_shape, model_tol, n_points=4000, seed=1234): + """Classify points against both solids; `(disagreements, scored, worst distance)`. + + It tells an empty cut from an equal pair, which the symmetric difference cannot. Points within + `model_tol` of either boundary are skipped; `worst` is the farthest disagreement from it. + """ + import random + from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier + from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON + from OCC.Core.gp import gp_Pnt + from cadsupport.recognise import _point_to_shape_distance + + # Distances go to the original's faces: against the solid, an interior point would read 0. + boundary = _faces_of(original) + box = _bbox(original) + if box is None: + return 0, 0, 0.0 + xmin, ymin, zmin, xmax, ymax, zmax = box + pad = 0.05 * max(xmax - xmin, ymax - ymin, zmax - zmin) + tol = max(model_tol, 1.0e-9) + original_cls = BRepClass3d_SolidClassifier(original) + candidate_cls = BRepClass3d_SolidClassifier(cand_shape) + rng = random.Random(seed) + disagreements = scored = 0 + worst = 0.0 + for _ in range(n_points): + point = (rng.uniform(xmin - pad, xmax + pad), rng.uniform(ymin - pad, ymax + pad), + rng.uniform(zmin - pad, zmax + pad)) + gp = gp_Pnt(*point) + original_cls.Perform(gp, tol) + if original_cls.State() == TopAbs_ON: + continue + candidate_cls.Perform(gp, tol) + if candidate_cls.State() == TopAbs_ON: + continue + scored += 1 + if (original_cls.State() == TopAbs_IN) != (candidate_cls.State() == TopAbs_IN): + disagreements += 1 + if disagreements <= _WORST_DISTANCE_SAMPLES: + distance = _point_to_shape_distance(point, boundary) + if distance == distance and distance != float("inf"): + worst = max(worst, distance) + return disagreements, scored, worst + + +# `worst` is a reporting number, so it is measured on the first few disagreements rather than on +# all of them; a part that disagrees hundreds of times has already declined. +_WORST_DISTANCE_SAMPLES = 24 + + +def _faces_of(shape): + """The shape's faces as one compound: its boundary, as something to measure a distance to.""" + from OCC.Core.BRep import BRep_Builder + from OCC.Core.TopAbs import TopAbs_FACE + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import TopoDS_Compound, topods + compound = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(compound) + walk = TopExp_Explorer(shape, TopAbs_FACE) + while walk.More(): + builder.Add(compound, topods.Face(walk.Current())) + walk.Next() + return compound + + +def _bbox(shape): + from OCC.Core.Bnd import Bnd_Box + from OCC.Core.BRepBndLib import brepbndlib + box = Bnd_Box() + brepbndlib.Add(shape, box) + box.SetGap(0.0) + try: + return box.Get() + except Exception: # noqa: BLE001 + return None + + +def _props(shape): + from OCC.Core.BRepGProp import brepgprop + from OCC.Core.GProp import GProp_GProps + vol = GProp_GProps() + brepgprop.VolumeProperties(shape, vol) + surf = GProp_GProps() + brepgprop.SurfaceProperties(shape, surf) + return vol.Mass(), surf.Mass() + + +def _cut_volume(a, b, what): + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + op = BRepAlgoAPI_Cut(a, b) + op.Build() + if not op.IsDone(): + raise RuntimeError(f"BRepAlgoAPI_Cut failed ({what})") + volume, _area = _props(op.Shape()) + return abs(volume) + + +def symmetric_difference(original, cand_shape, model_tolerance_cm, band_factor=_BAND_FACTOR, + original_props=None): + """Measure `original` against `cand_shape`; returns a dict, never raises on a mere mismatch. + + `original_props` is `_props(original)` when the caller already has it. + """ + v_orig, a_orig = _props(original) if original_props is None else original_props + if not (v_orig > 0.0 and a_orig > 0.0): + return {"accepted": False, + "reason": f"original has non-positive volume/area ({v_orig:.6g}/{a_orig:.6g})"} + v_cand, a_cand = _props(cand_shape) + if not v_cand > 0.0: + return {"accepted": False, "volumeOriginal": v_orig, "volumeCandidate": v_cand, + "reason": f"candidate has non-positive volume ({v_cand:.6g})"} + if not (1.0 / _SANITY_VOLUME_RATIO <= v_cand / v_orig <= _SANITY_VOLUME_RATIO): + return {"accepted": False, "volumeOriginal": v_orig, "volumeCandidate": v_cand, + "reason": f"candidate volume {v_cand:.6g} is not comparable to the original's " + f"{v_orig:.6g}"} + extra = _cut_volume(cand_shape, original, "candidate - original") + missing = _cut_volume(original, cand_shape, "original - candidate") + dv = extra + missing + band = band_factor * model_tolerance_cm * a_orig + return { + "accepted": dv <= band, + "volumeOriginal": v_orig, + "volumeCandidate": v_cand, + "areaOriginal": a_orig, + "extraVolume": extra, + "missingVolume": missing, + "symmetricDifference": dv, + "band": band, + "modelToleranceCm": model_tolerance_cm, + "relativeToVolume": dv / v_orig, + "reason": None if dv <= band else + f"symmetric difference {dv:.6g} cm^3 exceeds the band {band:.6g} cm^3 " + f"(= {model_tolerance_cm:.3g} cm x area {a_orig:.6g} cm^2); " + f"extra {extra:.6g}, missing {missing:.6g}", + } + + +# ------------------------------------------------------------------------------------------ +# self-test +# ------------------------------------------------------------------------------------------ + +def self_test(verbose=True): + """Hand-built pairs whose verdict is known, including candidates that must be rejected.""" + from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere) + from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt + from cadsupport import primitives as prim + + checks = [] + + def check(name, condition, detail=""): + checks.append((name, bool(condition), detail)) + if verbose: + print(f" [{'ok ' if condition else 'FAIL'}] {name}" + (f" {detail}" if detail else "")) + + tol = 1.0e-7 + + # 1. positive control: a shape against itself. + box = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0).Shape() + r = symmetric_difference(box, box, tol) + check("a box against itself is accepted", r["accepted"], f"dV={r['symmetricDifference']:.3g}") + check("a box against itself has zero symmetric difference", r["symmetricDifference"] == 0.0) + + # 2. positive control through the description, which is how the pipeline uses it: an OCCT box + # built independently of the description must still match. + cand = prim.candidate("primitive", [prim.leaf( + "TGeoBBox", {"dx": 1.0, "dy": 1.5, "dz": 2.0}, + prim.identity_frame((1.0, 1.5, 2.0)))], "self-test") + r = symmetric_difference(box, prim.build_occ(cand), tol) + check("an independently built TGeoBBox description matches the box", r["accepted"], + f"dV={r['symmetricDifference']:.3g} band={r['band']:.3g}") + + # 3. negative control: the same box 1 micron (1e-4 cm) too long. + long_box = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0001).Shape() + r = symmetric_difference(box, long_box, tol) + check("a box 1e-4 cm too long is rejected", not r["accepted"], + f"dV={r['symmetricDifference']:.3g} band={r['band']:.3g}") + + # 3b. how fine is the knife? A displacement of exactly one model tolerance must sit at the + # band, and ten of them must be outside it. + for factor, want_accept in ((0.5, True), (10.0, False)): + nudged = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0 + factor * tol).Shape() + r = symmetric_difference(box, nudged, tol) + check(f"a box {factor}x the model tolerance too long is " + f"{'accepted' if want_accept else 'rejected'}", + r["accepted"] == want_accept, + f"dV={r['symmetricDifference']:.3g} band={r['band']:.3g}") + + # 4. negative control: right volume, wrong shape (a volume comparison alone would pass it). + v = 2.0 * 3.0 * 4.0 + rad = (3.0 * v / (4.0 * math.pi)) ** (1.0 / 3.0) + sphere = BRepPrimAPI_MakeSphere(gp_Pnt(1.0, 1.5, 2.0), rad).Shape() + r = symmetric_difference(box, sphere, tol) + check("a sphere of equal volume is rejected", not r["accepted"], + f"dV={r['symmetricDifference']:.3g}, volumes {r['volumeOriginal']:.4f} vs " + f"{r['volumeCandidate']:.4f}") + + # 5. a tube, built two ways: OCCT cut versus the description's TGeoTube. + outer = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)), 2.0, 10.0).Shape() + inner = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1)), 1.0, 12.0).Shape() + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + op = BRepAlgoAPI_Cut(outer, inner) + op.Build() + tube = op.Shape() + cand = prim.candidate("primitive", [prim.leaf( + "TGeoTube", {"rmin": 1.0, "rmax": 2.0, "dz": 5.0}, prim.identity_frame())], "self-test") + r = symmetric_difference(tube, prim.build_occ(cand), tol) + check("a TGeoTube description matches an OCCT-cut tube", r["accepted"], + f"dV={r['symmetricDifference']:.3g} band={r['band']:.3g}") + + # 6. negative control on the tube: a solid cylinder must not pass as the tube. + solid_cand = prim.candidate("primitive", [prim.leaf( + "TGeoTube", {"rmin": 0.0, "rmax": 2.0, "dz": 5.0}, prim.identity_frame())], "self-test") + r = symmetric_difference(tube, prim.build_occ(solid_cand), tol) + check("a solid cylinder is rejected as the tube", not r["accepted"], + f"dV={r['symmetricDifference']:.3g}") + + # 7. the tube in a rotated, translated frame -- the case the whole frame machinery exists for. + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCC.Core.gp import gp_Trsf, gp_Ax1, gp_Vec + trsf = gp_Trsf() + trsf.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 1, 0)), 0.7) + shift = gp_Trsf() + shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + moved = BRepBuilderAPI_Transform(tube, shift.Multiplied(trsf), True).Shape() + zaxis = _rotated((0.0, 0.0, 1.0), (1.0, 1.0, 0.0), 0.7) + xaxis = _rotated((1.0, 0.0, 0.0), (1.0, 1.0, 0.0), 0.7) + frame = prim.frame_from_axis((3.0, -4.0, 5.0), zaxis, xaxis) + cand = prim.candidate("primitive", [prim.leaf( + "TGeoTube", {"rmin": 1.0, "rmax": 2.0, "dz": 5.0}, frame)], "self-test") + r = symmetric_difference(moved, prim.build_occ(cand), tol) + check("a rotated, translated tube matches its placed description", r["accepted"], + f"dV={r['symmetricDifference']:.3g} band={r['band']:.3g}") + + # 8. negative control on the frame: the *unrotated* description must be rejected against it. + flat = prim.candidate("primitive", [prim.leaf( + "TGeoTube", {"rmin": 1.0, "rmax": 2.0, "dz": 5.0}, + prim.identity_frame((3.0, -4.0, 5.0)))], "self-test") + r = symmetric_difference(moved, prim.build_occ(flat), tol) + check("the same tube without the rotation is rejected", not r["accepted"], + f"dV={r['symmetricDifference']:.3g}") + + # --- the containment corroboration, checked as an instrument before it is trusted --- + box = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0, 3.0, 2.0).Shape() + same, scored, worst = contains_disagreements(box, BRepPrimAPI_MakeBox( + gp_Pnt(0, 0, 0), 4.0, 3.0, 2.0).Shape(), 1.0e-7) + check("the containment corroboration reports no disagreement for an identical pair", + same == 0 and scored > 3000, f"{same} of {scored} scored") + for grow, want_worst in ((0.02, 0.02), (0.2, 0.2)): + bigger = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0 + grow, 3.0, 2.0).Shape() + n_bad, n_scored, far = contains_disagreements(box, bigger, 1.0e-7) + check(f"the containment corroboration sees a face displaced by {grow} cm", + n_bad > 0 and abs(far - want_worst) <= 0.1 * want_worst, + f"{n_bad} of {n_scored} scored, the farthest {far:.4g} cm from the boundary " + f"(the slab is {want_worst} cm thick)") + # The mirror case: a missing slab is measured to the original's faces, not to its solid. + smaller = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0 - 0.2, 3.0, 2.0).Shape() + n_bad, n_scored, far = contains_disagreements(box, smaller, 1.0e-7) + check("the containment corroboration measures a MISSING slab at its true size, not at zero", + n_bad > 0 and 0.5 * 0.2 <= far <= 1.05 * 0.2, + f"{n_bad} of {n_scored} scored, the farthest {far:.4g} cm from the boundary " + f"(the missing slab is 0.2 cm thick; measured against the solid instead of its faces " + f"this number would be 0)") + + # A sub-tolerance difference must not be reported. + hair = BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0 + 1.0e-9, 3.0, 2.0).Shape() + n_bad, n_scored, _far = contains_disagreements(box, hair, 1.0e-7) + check("the containment corroboration does not cry wolf on a sub-tolerance difference", + n_bad == 0, f"{n_bad} of {n_scored} scored") + + n_ok = sum(1 for _n, ok, _d in checks if ok) + if verbose: + print(f" {n_ok}/{len(checks)} acceptance self-checks passed") + return n_ok, len(checks) + + +def _rotated(vec, axis, angle): + """Rodrigues; used only by the self-test, to state the expected frame independently.""" + from cadsupport.primitives import _cross, _dot, _scale, _unit, _add + k = _unit(axis) + return _add(_add(_scale(vec, math.cos(angle)), _scale(_cross(k, vec), math.sin(angle))), + _scale(k, _dot(k, vec) * (1.0 - math.cos(angle)))) diff --git a/Detectors/CADSupport/tools/cadsupport/analytic.py b/Detectors/CADSupport/tools/cadsupport/analytic.py new file mode 100644 index 0000000000000..21d02b90403b9 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/analytic.py @@ -0,0 +1,212 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Analytic-surface helpers shared by `O2_CADtoTGeo.py` and the `cadsupport` package.""" + +import math +from typing import List + +import numpy as np +from OCC.Core.GeomAbs import ( + GeomAbs_Plane, GeomAbs_Cylinder, GeomAbs_Cone, GeomAbs_Sphere, GeomAbs_Torus, + GeomAbs_BezierSurface, GeomAbs_BSplineSurface, GeomAbs_SurfaceOfRevolution, + GeomAbs_SurfaceOfExtrusion, GeomAbs_OffsetSurface, GeomAbs_OtherSurface, + GeomAbs_Line, GeomAbs_Circle, GeomAbs_Ellipse, GeomAbs_Hyperbola, GeomAbs_Parabola, + GeomAbs_BezierCurve, GeomAbs_BSplineCurve, GeomAbs_OffsetCurve, GeomAbs_OtherCurve, +) +from OCC.Core.gp import gp_Pnt, gp_Vec + +# Names of the OCCT surface and curve types, shared by the converter and the package. +SURFACE_TYPE_NAME = { + GeomAbs_Plane: "plane", + GeomAbs_Cylinder: "cylinder", + GeomAbs_Cone: "cone", + GeomAbs_Sphere: "sphere", + GeomAbs_Torus: "torus", + GeomAbs_BezierSurface: "bezier", + GeomAbs_BSplineSurface: "bspline", + GeomAbs_SurfaceOfRevolution: "revolution", + GeomAbs_SurfaceOfExtrusion: "extrusion", + GeomAbs_OffsetSurface: "offset", + GeomAbs_OtherSurface: "other", +} + +CURVE_TYPE_NAME = { + GeomAbs_Line: "line", + GeomAbs_Circle: "circle", + GeomAbs_Ellipse: "ellipse", + GeomAbs_Hyperbola: "hyperbola", + GeomAbs_Parabola: "parabola", + GeomAbs_BezierCurve: "bezier", + GeomAbs_BSplineCurve: "bspline", + GeomAbs_OffsetCurve: "offset", + GeomAbs_OtherCurve: "other", +} + + +def _v_dot(a: List[float], b: List[float]) -> float: + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + + +def _v_cross(a: List[float], b: List[float]) -> List[float]: + return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]] + + +def _analytic_surface_gap(kind: str, model: dict, P) -> float: + """The largest distance, in native CAD units, from any sampled point to the candidate surface. + + One quantity for every kind, so an ill-conditioned proposal cannot pass on its own residual. + """ + if kind == "plane": + normal = np.asarray(model["normal"], dtype=float) + normal = normal / np.linalg.norm(normal) + return float(np.abs((P - np.asarray(model["point"], dtype=float)) @ normal).max()) + if kind == "sphere": + return float(np.abs(np.linalg.norm(P - model["centre"], axis=1) - model["radius"]).max()) + if kind == "cylinder": + axis = np.asarray(model["axis"], dtype=float) + axis = axis / np.linalg.norm(axis) + radial = P - model["origin"] + radial = radial - np.outer(radial @ axis, axis) + return float(np.abs(np.linalg.norm(radial, axis=1) - model["radius"]).max()) + if kind == "cone": + axis = np.asarray(model["axis"], dtype=float) + axis = axis / np.linalg.norm(axis) + rel = P - model["apex"] + h = rel @ axis + r = np.linalg.norm(rel - np.outer(h, axis), axis=1) + half = model["half_angle"] + return float(np.abs(r * math.cos(half) - h * math.sin(half)).max()) + return float("inf") + + +def _sample_surface_for_recognition(adaptor, umin: float, umax: float, vmin: float, vmax: float, n: int = 9): + """Sample an (n x n) grid over the face's actual trimmed (u, v) box (from `breptools.UVBounds`, + not the underlying surface's full natural domain). Returns (points, unit normals) in *native* + (unscaled) CAD length units, or (None, None) if unsampleable.""" + if not all(math.isfinite(x) for x in (umin, umax, vmin, vmax)): + return None, None + points, normals = [], [] + p, du, dv = gp_Pnt(), gp_Vec(), gp_Vec() + for i in range(n): + u = umin + (umax - umin) * i / (n - 1.0) + for j in range(n): + v = vmin + (vmax - vmin) * j / (n - 1.0) + try: + adaptor.D1(u, v, p, du, dv) + except Exception: + return None, None + nrm = _v_cross([du.X(), du.Y(), du.Z()], [dv.X(), dv.Y(), dv.Z()]) + length = math.sqrt(_v_dot(nrm, nrm)) + if length < 1e-14: # parametric degeneracy (pole/seam): skip this sample + continue + points.append([p.X(), p.Y(), p.Z()]) + normals.append([c / length for c in nrm]) + if len(points) < 3 * n: + return None, None + return np.array(points), np.array(normals) + + +def _analytic_surface_proposals(P, N): + """Yield `(kind, model)` for every candidate surface these samples propose, in order of + parsimony: plane (3 parameters), sphere (4), cylinder (5), cone (6). + + Nothing here decides: degenerate proposals are left in and the gap judges them. + """ + # --- plane (3): the samples lie in one plane; the frame is the sampled normal itself + yield "plane", {"normal": N[0] / np.linalg.norm(N[0]), "point": P[0]} + + # --- sphere (4): normal lines concurrent, P_i = C + r*N_i + A = np.zeros((3 * len(P), 4)) + b = np.zeros(3 * len(P)) + for i in range(len(P)): + A[3 * i:3 * i + 3, 0:3] = np.eye(3) + A[3 * i:3 * i + 3, 3] = N[i] + b[3 * i:3 * i + 3] = P[i] + sol, *_ = np.linalg.lstsq(A, b, rcond=None) + yield "sphere", {"centre": sol[:3], "radius": abs(sol[3])} + + # --- cylinder (5): normals coplanar; axis = smallest right singular vector of the normal field + _, _, Vt = np.linalg.svd(N, full_matrices=False) + axis = Vt[-1] + if np.abs(N @ axis).max() < 1e-9: + e1 = Vt[0] + e2 = np.cross(axis, e1) + x, y = P @ e1, P @ e2 + M = np.column_stack([x, y, np.ones_like(x)]) + D, E, F = np.linalg.lstsq(M, -(x ** 2 + y ** 2), rcond=None)[0] + cx, cy = -D / 2, -E / 2 + r2 = cx * cx + cy * cy - F + if r2 > 0: + origin = cx * e1 + cy * e2 # a point on the axis (axial component is free) + yield "cylinder", {"axis": axis, "refu": e1, "origin": origin, + "radius": math.sqrt(r2)} + + # --- cone (6): N_i . (P_i - A) = 0 is linear in the apex A + apex, *_ = np.linalg.lstsq(N, np.einsum('ij,ij->i', N, P), rcond=None) + d = P - apex + dn = np.linalg.norm(d, axis=1) + ok = dn > 1e-12 + if ok.sum() > 10: + u = d[ok] / dn[ok, None] + mean_dir = u.mean(axis=0) + _, _, Vt2 = np.linalg.svd(u - mean_dir, full_matrices=False) + ax2 = np.cross(Vt2[0], Vt2[1]) + n2 = np.linalg.norm(ax2) + if n2 > 1e-12: # a ruling axis exists + ax2 = ax2 / n2 + if np.dot(mean_dir, ax2) < 0.0: + ax2 = -ax2 + ref = u[0] - np.dot(u[0], ax2) * ax2 + refn = np.linalg.norm(ref) + if refn > 1e-9: + half_angle = float(np.arccos(np.clip(np.abs(u @ ax2), -1.0, 1.0)).mean()) + yield "cone", {"axis": ax2, "apex": apex, "refu": ref / refn, + "half_angle": half_angle} + + +def _self_test_bezier_patch(fn, nu: int, nv: int): + """A non-rational Bezier patch whose control net is `fn(s, t)` on a (nu x nv) grid.""" + from OCC.Core.Geom import Geom_BSplineSurface + from OCC.Core.TColgp import TColgp_Array2OfPnt + from OCC.Core.TColStd import TColStd_Array1OfReal, TColStd_Array1OfInteger + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace + + poles = TColgp_Array2OfPnt(1, nu, 1, nv) + for i in range(nu): + for j in range(nv): + x, y, z = fn(i / (nu - 1.0), j / (nv - 1.0)) + poles.SetValue(i + 1, j + 1, gp_Pnt(float(x), float(y), float(z))) + uk = TColStd_Array1OfReal(1, 2) + uk.SetValue(1, 0.0) + uk.SetValue(2, 1.0) + vk = TColStd_Array1OfReal(1, 2) + vk.SetValue(1, 0.0) + vk.SetValue(2, 1.0) + um = TColStd_Array1OfInteger(1, 2) + um.SetValue(1, nu) + um.SetValue(2, nu) + vm = TColStd_Array1OfInteger(1, 2) + vm.SetValue(1, nv) + vm.SetValue(2, nv) + surface = Geom_BSplineSurface(poles, uk, vk, um, vm, nu - 1, nv - 1) + return BRepBuilderAPI_MakeFace(surface, 1e-6).Face() + + +def _self_test_tapered_near_circle(bulge: float, taper: float): + """Negative control: a tapered non-circular profile that must never be recognised as a cone.""" + def fn(s, t): + a = 1.2 * s - 0.6 + r = 9.8 * (1.0 + bulge * math.cos(3.0 * a)) * (1.0 + taper * t) + return (r * math.cos(a), r * math.sin(a), 10.0 * t) + return _self_test_bezier_patch(fn, nu=6, nv=3) diff --git a/Detectors/CADSupport/tools/cadsupport/census.py b/Detectors/CADSupport/tools/cadsupport/census.py new file mode 100644 index 0000000000000..192c18840cfc5 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/census.py @@ -0,0 +1,320 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Topology helpers of the CSG recogniser: edge convexity, material side, volume and bounding box. + +The recognition census tool built on them is `validation/csgCensus.py`. +""" + +import math + +from cadsupport.occ_env import ensure_occ # noqa: E402 + +ensure_occ() + +from OCC.Core.BRep import BRep_Tool # noqa: E402 +from OCC.Core.BRepAdaptor import BRepAdaptor_Curve, BRepAdaptor_Surface # noqa: E402 +from OCC.Core.BRepBndLib import brepbndlib # noqa: E402 +from OCC.Core.BRepGProp import brepgprop # noqa: E402 +from OCC.Core.BRepLProp import BRepLProp_SLProps # noqa: E402 +from OCC.Core.Bnd import Bnd_Box # noqa: E402 +from OCC.Core.GProp import GProp_GProps # noqa: E402 +from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE, TopAbs_REVERSED # noqa: E402 +from OCC.Core.TopExp import TopExp_Explorer, topexp # noqa: E402 +from OCC.Core.TopTools import (TopTools_IndexedDataMapOfShapeListOfShape, # noqa: E402 + TopTools_IndexedMapOfShape, + TopTools_ListIteratorOfListOfShape) +from OCC.Core.TopoDS import topods # noqa: E402 +from OCC.Core.gp import gp_Pnt, gp_Vec # noqa: E402 +from cadsupport.analytic import SURFACE_TYPE_NAME # noqa: E402,F401 (read as census.SURFACE_TYPE_NAME) +from cadsupport.primitives import _cross, _dot, _norm, _sub # noqa: E402 + +# Below this |n1 x n2| an edge is tangential and its dihedral has no sign. +TANGENTIAL_SIN = 1.0e-6 + +# Below this a concave/mixed verdict is labelled untrustworthy (a blend seam), never changed. +NEAR_TANGENTIAL_SIN = 1.0e-3 + + +# -------------------------------------------------------------------------------------------- +# small vector helpers (gp_Dir/gp_Pnt are awkward to compare directly) +# -------------------------------------------------------------------------------------------- + +def _xyz(p): + return (p.X(), p.Y(), p.Z()) + + +def halfspace_side(face, ad, stype): + """Which side of its own carrier the material is on: `interior` or `exterior`. + + Decided geometrically from the face's own normal, not from the ORIENTATION flag alone. + """ + if stype == "plane": + return "interior" + if stype not in ("cylinder", "cone", "sphere", "torus"): + return None + if stype == "sphere": + sp = ad.Sphere() + carrier = {"kind": "sphere", "p": _xyz(sp.Location())} + elif stype == "torus": + to = ad.Torus() + ax = to.Axis() + carrier = {"kind": "torus", "p": _xyz(ax.Location()), "d": _xyz(ax.Direction()), + "r": to.MajorRadius()} + else: + ax = ad.Cylinder().Axis() if stype == "cylinder" else ad.Cone().Axis() + carrier = {"kind": stype, "p": _xyz(ax.Location()), "d": _xyz(ax.Direction())} + return halfspace_side_of(face, ad, carrier) + + +def halfspace_side_of(face, ad, carrier): + """`halfspace_side` for a carrier given as parameters, such as a canonicalised B-spline face.""" + kind = carrier["kind"] + if kind == "plane": + return "interior" + if kind not in ("cylinder", "cone", "sphere", "torus"): + return None + u = 0.5 * (ad.FirstUParameter() + ad.LastUParameter()) + v = 0.5 * (ad.FirstVParameter() + ad.LastVParameter()) + if not all(math.isfinite(x) for x in (u, v)): + return None + n = _face_normal(face, u, v, ad) + if n is None: + return None + try: + p = _xyz(ad.Value(u, v)) + except Exception: + return None + out = _outward_of_carrier(carrier, p) + if out is None or _norm(out) < 1e-30: + return None + return "interior" if _dot(n, out) > 0.0 else "exterior" + + +def _outward_of_carrier(carrier, p): + """The direction pointing out of `carrier` at `p` (radial for a cylinder or cone), or None.""" + kind = carrier["kind"] + if kind == "sphere": + return _sub(p, carrier["p"]) + loc, d = carrier["p"], carrier["d"] + rel = _sub(p, loc) + radial = _sub(rel, tuple(c * _dot(rel, d) for c in d)) + if kind != "torus": + return radial + rl = _norm(radial) + if rl < 1e-30: + return None + centre = tuple(loc[i] + radial[i] / rl * carrier["r"] for i in range(3)) + return _sub(p, centre) + + +# -------------------------------------------------------------------------------------------- +# edge convexity +# -------------------------------------------------------------------------------------------- + +class FaceEdgeOrientations: + """Per-face map from edge to the orientation(s) it occurs with, built once per face. + + Rescanning a face's wires for every edge would be quadratic in its edge count. + """ + + def __init__(self, solid): + self._emap = TopTools_IndexedMapOfShape() + topexp.MapShapes(solid, TopAbs_EDGE, self._emap) + self._fmap = TopTools_IndexedMapOfShape() + topexp.MapShapes(solid, TopAbs_FACE, self._fmap) + self._cache = {} + self._adaptors = {} + + def adaptor(self, face): + """The face's `BRepAdaptor_Surface(face, True)`, built once per face.""" + fi = self._fmap.FindIndex(face) + ad = self._adaptors.get(fi) if fi else None + if ad is None: + ad = BRepAdaptor_Surface(face, True) + if fi: + self._adaptors[fi] = ad + return ad + + def get(self, edge, face): + fi = self._fmap.FindIndex(face) + table = self._cache.get(fi) + if table is None: + table = {} + exp = TopExp_Explorer(face, TopAbs_EDGE) + while exp.More(): + e = topods.Edge(exp.Current()) + table.setdefault(self._emap.FindIndex(e), []).append(e.Orientation()) + exp.Next() + self._cache[fi] = table + return table.get(self._emap.FindIndex(edge), []) + + +def _face_normal(face, u, v, adaptor=None): + try: + ad = BRepAdaptor_Surface(face, True) if adaptor is None else adaptor + props = BRepLProp_SLProps(ad, u, v, 1, 1.0e-9) + if not props.IsNormalDefined(): + return None + n = _xyz(props.Normal()) + except Exception: + return None + if _norm(n) < 1e-30: + return None + if face.Orientation() == TopAbs_REVERSED: + n = (-n[0], -n[1], -n[2]) + return n + + +def _pcurve(edge, face): + """`(2D curve, first, last)` of an edge on a face, or None.""" + res = BRep_Tool.CurveOnSurface(edge, face) + if res is None: + return None + c2d, f, l = res[0], res[1], res[2] + if c2d is None: + return None + return c2d, f, l + + +def _uv_at(pcurve, t): + """The (u, v) of a pcurve at edge parameter `t`, clamped to its range.""" + if pcurve is None: + return None + c2d, f, l = pcurve + p = c2d.Value(min(max(t, f), l)) + return p.X(), p.Y() + + +def edge_dihedral(edge, f1, f2, orients, samples=3): + """Classify the dihedral along an edge shared by two faces. + + (n1 x n2) . t >= 0 is convex, with t the edge tangent oriented along f1's traversal and n + the *outward* normals (face orientation applied). A curved edge can change character, so it + is sampled and the verdict is `mixed` when it does. + """ + try: + curve = BRepAdaptor_Curve(edge) + first, last = curve.FirstParameter(), curve.LastParameter() + except Exception: + return "error", 0.0 + if not (math.isfinite(first) and math.isfinite(last)) or last <= first: + return "error", 0.0 + + o1 = orients.get(edge, f1) + if len(o1) != 1: + return "seam", 0.0 + sign1 = -1.0 if o1[0] == TopAbs_REVERSED else 1.0 + + verdicts = set() + max_sin = 0.0 + p, d1 = gp_Pnt(), gp_Vec() + pcurves = None + for i in range(samples): + frac = (i + 1.0) / (samples + 1.0) + t = first + frac * (last - first) + try: + curve.D1(t, p, d1) + except Exception: + continue + tangent = (d1.X() * sign1, d1.Y() * sign1, d1.Z() * sign1) + tl = _norm(tangent) + if tl < 1e-30: + continue + tangent = tuple(c / tl for c in tangent) + + if pcurves is None: + pcurves = (_pcurve(edge, f1), _pcurve(edge, f2)) + uv1 = _uv_at(pcurves[0], t) + uv2 = _uv_at(pcurves[1], t) + if uv1 is None or uv2 is None: + continue + n1 = _face_normal(f1, uv1[0], uv1[1], orients.adaptor(f1)) + n2 = _face_normal(f2, uv2[0], uv2[1], orients.adaptor(f2)) + if n1 is None or n2 is None: + continue + x = _cross(n1, n2) + s = _norm(x) + max_sin = max(max_sin, s) + if s <= TANGENTIAL_SIN: + verdicts.add("tangential") + else: + verdicts.add("convex" if _dot(x, tangent) >= 0.0 else "concave") + + if not verdicts: + return "error", max_sin + if len(verdicts) == 1: + return verdicts.pop(), max_sin + verdicts.discard("tangential") + if len(verdicts) == 1: + return verdicts.pop(), max_sin + return "mixed", max_sin + + + +def shape_list(lst): + """The shapes in a TopTools_ListOfShape, via its iterator (pythonOCC 7.9 has no __iter__).""" + out = [] + it = TopTools_ListIteratorOfListOfShape(lst) + while it.More(): + out.append(it.Value()) + it.Next() + return out + + +def edge_census(solid): + amap = TopTools_IndexedDataMapOfShapeListOfShape() + topexp.MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, amap) + orients = FaceEdgeOrientations(solid) + counts = {"edges": 0, "convex": 0, "concave": 0, "tangential": 0, "mixed": 0, + "seam": 0, "nonManifold": 0, "boundary": 0, "degenerate": 0, "error": 0, + # Concave/mixed verdicts on near-tangential edges, whose sign is noise. + "concaveNearTangential": 0, "mixedNearTangential": 0} + for i in range(1, amap.Size() + 1): + edge = topods.Edge(amap.FindKey(i)) + counts["edges"] += 1 + if BRep_Tool.Degenerated(edge): + counts["degenerate"] += 1 # a pole of a sphere/cone: no dihedral exists + continue + faces = shape_list(amap.FindFromIndex(i)) + distinct = [] + for f in faces: + if not any(f.IsSame(g) for g in distinct): + distinct.append(f) + if len(distinct) == 1: + counts["seam" if len(faces) > 1 else "boundary"] += 1 + continue + if len(distinct) != 2: + counts["nonManifold"] += 1 + continue + verdict, max_sin = edge_dihedral(edge, topods.Face(distinct[0]), + topods.Face(distinct[1]), orients) + counts[verdict] = counts.get(verdict, 0) + 1 + if verdict in ("concave", "mixed") and max_sin < NEAR_TANGENTIAL_SIN: + counts[verdict + "NearTangential"] += 1 + return counts + + +def bounding_box(shape): + box = Bnd_Box() + brepbndlib.Add(shape, box) + if box.IsVoid(): + return None + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + return [xmin, ymin, zmin, xmax, ymax, zmax] + + +def volume_of(shape): + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return props.Mass() diff --git a/Detectors/CADSupport/tools/cadsupport/decline_catalogue.py b/Detectors/CADSupport/tools/cadsupport/decline_catalogue.py new file mode 100644 index 0000000000000..7f684811eed69 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/decline_catalogue.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Per-part decline catalogue: part x {ships-as, whyNotCSG, whyNotSurface}. + +Joins csg_report.json and surface_report.json of converter output directories into +decline_reasons.json. `--source NAME=FILE` records which CAD file a run was converted from. + +Usage +----- + decline_catalogue.py --run ExcavatorArm=/path/to/converted/ExcavatorArm \ + --run ALICE3=/path/to/converted/ALICE3 \ + [--source ALICE3=/path/to/CAD_noETA.stp] \ + [--gate-db /path/to/gate/workdir/db] \ + --out decline_reasons.json [--markdown] + +`--gate-db` adds every model subdirectory of a gate database (each holds the two reports) as a +run named after the subdirectory. +""" + +import argparse +import json +import sys +from pathlib import Path + + +def load_run(name, out_dir): + out_dir = Path(out_dir) + csg_path = out_dir / "csg_report.json" + surf_path = out_dir / "surface_report.json" + if not csg_path.exists(): + raise SystemExit(f"{name}: {csg_path} does not exist (convert with --csg auto)") + if not surf_path.exists(): + raise SystemExit(f"{name}: {surf_path} does not exist (convert with --surface-report)") + csg = json.loads(csg_path.read_text()) + surf = json.loads(surf_path.read_text()) + volumes = surf.get("volumes", {}) + rows = [] + for part in csg.get("parts", []): + lid = part.get("lid") + vol = volumes.get(lid, {}) + why_not_csg = part.get("whyNotCSG") + rows.append({ + "name": part.get("volume") or lid, + "model": name, + "lid": lid, + "shipsAs": part.get("representation"), + "whyNotCSG": why_not_csg, + "whyNotSurface": vol.get("why_not_surface"), + "nFaces": vol.get("n_faces"), + }) + # A leaf solid can, in principle, appear in the surface report only (it never reached the + # CSG hook). Keep it, so the catalogue counts every part the converter saw. + seen = {r["lid"] for r in rows} + for lid, vol in volumes.items(): + if lid in seen: + continue + rows.append({ + "name": vol.get("name") or lid, + "model": name, + "lid": lid, + "shipsAs": "surface" if vol.get("emitted") else "mesh", + "whyNotCSG": "not assessed (part never reached the CSG hook)", + "whyNotSurface": vol.get("why_not_surface"), + "nFaces": vol.get("n_faces"), + }) + return rows + + +def markdown(rows): + out = ["| model | part | faces | ships as | why not CSG | why not SurfaceSolid |", + "| --- | --- | ---: | --- | --- | --- |"] + for r in rows: + why_csg = r["whyNotCSG"] or "—" + why_surf = r["whyNotSurface"] or "—" + out.append(f"| {r['model']} | `{r['name']}` | {r['nFaces'] if r['nFaces'] is not None else '?'} " + f"| **{r['shipsAs']}** | {why_csg} | {why_surf} |") + return "\n".join(out) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--run", action="append", default=[], metavar="NAME=DIR", + help="a converter output directory holding csg_report.json and " + "surface_report.json; repeatable") + ap.add_argument("--source", action="append", default=[], metavar="NAME=FILE", + help="the CAD file a run was converted from, recorded in the output's " + "sourceModel map; repeatable") + ap.add_argument("--gate-db", type=Path, + help="a gate workdir's db/ directory: every model subdirectory becomes a run") + ap.add_argument("--out", type=Path, help="write decline_reasons.json here") + ap.add_argument("--markdown", action="store_true", help="print the table as markdown") + args = ap.parse_args() + + runs = [] + for spec in args.run: + name, _, folder = spec.partition("=") + if not folder: + ap.error(f"--run wants NAME=DIR, got {spec!r}") + runs.append((name, folder)) + sources = {} + for spec in args.source: + name, _, path = spec.partition("=") + if not path: + ap.error(f"--source wants NAME=FILE, got {spec!r}") + sources[name] = path + if args.gate_db: + for sub in sorted(args.gate_db.iterdir()): + if sub.is_dir() and (sub / "csg_report.json").exists(): + runs.append((sub.name, sub)) + if not runs: + ap.error("give --run and/or --gate-db") + unknown = sorted(set(sources) - {name for name, _ in runs}) + if unknown: + ap.error(f"--source names no such run: {', '.join(unknown)}") + + rows = [] + for name, folder in runs: + rows.extend(load_run(name, folder)) + + n_csg = sum(1 for r in rows if r["shipsAs"] == "csg") + n_surface = sum(1 for r in rows if r["shipsAs"] == "surface") + n_mesh = sum(1 for r in rows if r["shipsAs"] == "mesh") + missing = [r for r in rows + if (r["shipsAs"] != "csg" and not r["whyNotCSG"]) + or (r["shipsAs"] == "mesh" and not r["whyNotSurface"])] + print(f"{len(rows)} part(s) over {len(runs)} run(s): " + f"csg {n_csg}, surface {n_surface}, mesh {n_mesh}; " + f"{len(missing)} row(s) with a missing decline reason") + for r in missing: + print(f" [missing] {r['model']}/{r['name']}: shipsAs={r['shipsAs']} " + f"whyNotCSG={r['whyNotCSG']!r} whyNotSurface={r['whyNotSurface']!r}") + + if args.out: + args.out.parent.mkdir(parents=True, exist_ok=True) + # `null` for a run nobody named a source for: an absent statement, never a guess. + source_map = {name: sources.get(name) for name, _ in runs} + args.out.write_text(json.dumps({"sourceModel": source_map, "parts": rows}, indent=1)) + print(f"Wrote {args.out}") + if args.markdown: + print(markdown(rows)) + return 1 if missing else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/tools/cadsupport/decompose.py b/Detectors/CADSupport/tools/cadsupport/decompose.py new file mode 100644 index 0000000000000..34744e23c6db4 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/decompose.py @@ -0,0 +1,319 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Split a part into single cells, so a union of them can be emitted. + + while a piece has a trusted concave (or mixed) edge: + extend the carrier of one of the edge's two faces to a full surface; + split the piece with BRepAlgoAPI_Splitter; + recurse on the pieces. + a piece with no trusted concave edge is one CSG cell. + +The loop starts from the shape's connected solids, because zero concave edges does not mean one +cell. It only reports; a split that does not conserve the volume is flagged for the caller. +""" + +import math +import time + +# The per-part cell budget; a part over it is declined naming the bound, never shipped as a tree +# that wide. +PART_MAX_CELLS = 64 + +# The wall clock and the split count, so a blow-up is a decline and never a hang. +MAX_SPLITS = 256 +TIMEOUT_S = 60.0 + +# The splitter's volume guard: the pieces must sum to the part within this relative band. +VOLUME_REL_TOL = 1.0e-6 + +# How far a cutting tool has to reach, in bounding-box diagonals of the piece being split. +TOOL_EXTENT_DIAGONALS = 4.0 + + +def _occ(): + from cadsupport.occ_env import ensure_occ + ensure_occ() + + +# ------------------------------------------------------------------------------------------ +# connectivity +# ------------------------------------------------------------------------------------------ + +def solid_components(shape): + """The shape's own `TopoDS_Solid` bodies, or the shape itself when it carries none.""" + from OCC.Core.TopAbs import TopAbs_SOLID + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + out = [] + walk = TopExp_Explorer(shape, TopAbs_SOLID) + while walk.More(): + out.append(topods.Solid(walk.Current())) + walk.Next() + return out or [shape] + + +# ------------------------------------------------------------------------------------------ +# finding the split witness: the sharpest trusted concave/mixed edge, with its two faces +# ------------------------------------------------------------------------------------------ + +def first_trusted_concave_edge(solid): + """(edge, face1, face2) of the sharpest trusted concave or mixed dihedral, or None. + + "Trusted" excludes a verdict whose |n1 x n2| stays below `NEAR_TANGENTIAL_SIN`. + """ + from cadsupport.census import NEAR_TANGENTIAL_SIN, FaceEdgeOrientations, edge_dihedral + from OCC.Core.BRep import BRep_Tool + from OCC.Core.TopAbs import TopAbs_EDGE, TopAbs_FACE + from OCC.Core.TopExp import topexp + from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape + from cadsupport.census import shape_list + from OCC.Core.TopoDS import topods + + amap = TopTools_IndexedDataMapOfShapeListOfShape() + topexp.MapShapesAndAncestors(solid, TopAbs_EDGE, TopAbs_FACE, amap) + orients = FaceEdgeOrientations(solid) + best = None + for i in range(1, amap.Size() + 1): + edge = topods.Edge(amap.FindKey(i)) + if BRep_Tool.Degenerated(edge): + continue + faces = shape_list(amap.FindFromIndex(i)) + distinct = [] + for f in faces: + if not any(f.IsSame(g) for g in distinct): + distinct.append(f) + if len(distinct) != 2: + continue + f1, f2 = topods.Face(distinct[0]), topods.Face(distinct[1]) + verdict, max_sin = edge_dihedral(edge, f1, f2, orients) + if verdict in ("concave", "mixed") and max_sin >= NEAR_TANGENTIAL_SIN: + if best is None or max_sin > best[3]: + best = (edge, f1, f2, max_sin) + return None if best is None else best[:3] + + +# ------------------------------------------------------------------------------------------ +# extending a face's carrier into a splitting tool +# ------------------------------------------------------------------------------------------ + +def carrier_tool_face(face, extent, scale=None): + """A face covering the whole carrier of `face`, big enough to cut anything within `extent`. + + A B-spline face goes through Tier 0 first; None when the carrier is none of the five families. + """ + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.GeomAbs import (GeomAbs_Cone, GeomAbs_Cylinder, GeomAbs_Plane, + GeomAbs_Sphere, GeomAbs_Torus) + ad = BRepAdaptor_Surface(face, True) + kind = ad.GetType() + try: + if kind == GeomAbs_Plane: + return _tool_from_plane(ad.Plane(), extent) + if kind == GeomAbs_Cylinder: + return _tool_from_cylinder(ad.Cylinder(), extent) + if kind == GeomAbs_Cone: + return _tool_from_cone(ad.Cone(), extent) + if kind == GeomAbs_Sphere: + return _tool_from_sphere(ad.Sphere()) + if kind == GeomAbs_Torus: + return _tool_from_torus(ad.Torus()) + except Exception: # noqa: BLE001 + return None + if scale is None: + return None + return _canonical_tool_face(face, ad, extent, scale) + + +def _canonical_tool_face(face, adaptor, extent, scale): + """The tool a Tier-0 canonicalised face extends to, or None if the face is not canonical.""" + from cadsupport import tier0 + carrier, _gap = tier0.canonicalise(face, adaptor, scale) + if carrier is None: + return None + from OCC.Core.gp import (gp_Ax3, gp_Cone, gp_Cylinder, gp_Dir, gp_Pln, gp_Pnt, gp_Sphere, + gp_Torus) + try: + if carrier["kind"] == "plane": + return _tool_from_plane( + gp_Pln(gp_Pnt(*carrier["p"]), gp_Dir(*carrier["n"])), extent) + frame = gp_Ax3(gp_Pnt(*carrier["p"]), gp_Dir(*carrier["d"]), gp_Dir(*carrier["x"])) + if carrier["kind"] == "cylinder": + return _tool_from_cylinder(gp_Cylinder(frame, carrier["r"]), extent) + if carrier["kind"] == "cone": + return _tool_from_cone(gp_Cone(frame, carrier["a"], carrier["r"]), extent) + if carrier["kind"] == "sphere": + return _tool_from_sphere(gp_Sphere( + gp_Ax3(gp_Pnt(*carrier["p"]), gp_Dir(0.0, 0.0, 1.0)), carrier["r"])) + if carrier["kind"] == "torus": + return _tool_from_torus(gp_Torus(frame, carrier["r"], carrier["rt"])) + except Exception: # noqa: BLE001 + return None + return None + + +def _face_of(surface, umin, umax, vmin, vmax): + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace + return BRepBuilderAPI_MakeFace(surface, umin, umax, vmin, vmax, 1.0e-7).Face() + + +def _tool_from_plane(pln, extent): + from OCC.Core.Geom import Geom_Plane + return _face_of(Geom_Plane(pln), -extent, extent, -extent, extent) + + +def _tool_from_cylinder(cyl, extent): + from OCC.Core.Geom import Geom_CylindricalSurface + return _face_of(Geom_CylindricalSurface(cyl), 0.0, 2.0 * math.pi, -extent, extent) + + +def _tool_from_cone(cone, extent): + from OCC.Core.Geom import Geom_ConicalSurface + return _face_of(Geom_ConicalSurface(cone), 0.0, 2.0 * math.pi, -extent, extent) + + +def _tool_from_sphere(sphere): + from OCC.Core.Geom import Geom_SphericalSurface + return _face_of(Geom_SphericalSurface(sphere), 0.0, 2.0 * math.pi, + -0.5 * math.pi, 0.5 * math.pi) + + +def _tool_from_torus(torus): + from OCC.Core.Geom import Geom_ToroidalSurface + return _face_of(Geom_ToroidalSurface(torus), 0.0, 2.0 * math.pi, 0.0, 2.0 * math.pi) + + +def split_solid(piece, tool): + """Split `piece` by `tool`; returns the list of solids, or None on failure.""" + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Splitter + from OCC.Core.TopAbs import TopAbs_SOLID + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopTools import TopTools_ListOfShape + from OCC.Core.TopoDS import topods + + splitter = BRepAlgoAPI_Splitter() + args = TopTools_ListOfShape() + args.Append(piece) + tools = TopTools_ListOfShape() + tools.Append(tool) + splitter.SetArguments(args) + splitter.SetTools(tools) + try: + splitter.Build() + except Exception: # noqa: BLE001 + return None + if not splitter.IsDone(): + return None + out = [] + walk = TopExp_Explorer(splitter.Shape(), TopAbs_SOLID) + while walk.More(): + out.append(topods.Solid(walk.Current())) + walk.Next() + return out or None + + +# ------------------------------------------------------------------------------------------ +# the loop +# ------------------------------------------------------------------------------------------ + +def bbox_diagonal(shape): + from cadsupport import census + box = census.bounding_box(shape) + if box is None: + return 1.0 + xmin, ymin, zmin, xmax, ymax, zmax = box + return math.sqrt((xmax - xmin) ** 2 + (ymax - ymin) ** 2 + (zmax - zmin) ** 2) + + +def split_into_cells(solid, max_cells=PART_MAX_CELLS, max_splits=MAX_SPLITS, + timeout_s=TIMEOUT_S, scale=None, verbose=False): + """Split at trusted concave edges until every piece is one cell; returns a report. + + `stop` names the budget that was hit, or is None; `scale` is the part's Tier-0 length, or None. + """ + _occ() + from cadsupport.census import volume_of + start = time.time() + diagonal = bbox_diagonal(solid) + extent = TOOL_EXTENT_DIAGONALS * max(diagonal, 1.0) + original_volume = volume_of(solid) + pending = list(solid_components(solid)) + n_components = len(pending) + cells, unresolved = [], [] + n_splits = n_split_failures = 0 + stop = None + + while pending: + if len(cells) + len(pending) + len(unresolved) > max_cells: + stop = f"the cell budget of {max_cells} was exceeded" + break + if n_splits >= max_splits: + stop = f"the split budget of {max_splits} was exceeded" + break + if time.time() - start > timeout_s: + stop = f"the {timeout_s:.0f} s decomposition timeout was exceeded" + break + piece = pending.pop() + witness = first_trusted_concave_edge(piece) + if witness is None: + cells.append(piece) + continue + _edge, face1, face2 = witness + parts = _split_at(piece, face1, face2, extent, scale) + if parts is None: + n_split_failures += 1 + unresolved.append(piece) + continue + n_splits += 1 + pending.extend(parts) + if verbose: + print(f" split {n_splits}: {len(parts)} piece(s), {len(cells)} cell(s) so far, " + f"{len(pending)} pending") + + piece_volume = sum(volume_of(c) for c in cells) + sum(volume_of(u) for u in unresolved) + conserved = None + if stop is None: + conserved = abs(piece_volume - original_volume) <= \ + VOLUME_REL_TOL * max(abs(original_volume), 1.0) + return { + "pieces": cells, + "unresolved": unresolved, + "pending": list(pending), + "components": n_components, + "splits": n_splits, + "splitFailures": n_split_failures, + "stop": stop, + "volumeOriginal": original_volume, + "volumePieces": piece_volume, + "volumeConserved": conserved, + "volumeDrift": (abs(piece_volume - original_volume) / max(abs(original_volume), 1e-30) + if stop is None else None), + "seconds": round(time.time() - start, 2), + "diagonal": diagonal, + } + + +def _split_at(piece, face1, face2, extent, scale): + """Cut `piece` on a witness-edge carrier, the planar one first; the pieces, or None.""" + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.GeomAbs import GeomAbs_Plane + faces = sorted((face1, face2), + key=lambda f: BRepAdaptor_Surface(f, True).GetType() != GeomAbs_Plane) + for face in faces: + tool = carrier_tool_face(face, extent, scale) + if tool is None: + continue + parts = split_solid(piece, tool) + if parts is not None and len(parts) > 1: + return parts + return None diff --git a/Detectors/CADSupport/tools/cadsupport/emit.py b/Detectors/CADSupport/tools/cadsupport/emit.py new file mode 100644 index 0000000000000..f4ce613e8cc67 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/emit.py @@ -0,0 +1,477 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Recognise CAD leaf solids as CSG, prove it, and emit `shape__.root`. + +A part converts only if the OCCT symmetric difference and the oracle gate both accept it. +`--db` walks the `brep_*.brep` files of a gate run, `O2_CADtoTGeo.py --csg` reaches the same code +through `cadsupport.hook`, and `--from-json` completes descriptions written without PyROOT. +""" + +import argparse +import json +import math +import sys +from pathlib import Path + +from cadsupport.occ_env import ensure_occ # noqa: E402 + +ensure_occ() + +from cadsupport import accept, primitives as prim, recognise # noqa: E402 + + +# ------------------------------------------------------------------------------------------ +# per-solid pipeline +# ------------------------------------------------------------------------------------------ + +# The shape-tolerance helper lives in `cadsupport/accept.py`, because `cadsupport/recognise.py` needs it too +# and must not import this module. Re-exported here under its long-standing name. +model_tolerance_cm = accept.model_tolerance_cm + + +# Retried after the acceptance test rejects a candidate, in order of increasing generality. +_RETRIES = (("a revolved profile", recognise.recognise_revolved), + ("a single cell", recognise.recognise_single_cell), + ("a union of cells", recognise.recognise_union_of_cells), + # Last, after the union of cells, as in the cascade. + ("flat cells", recognise.recognise_flat_cells)) + + +def _build_and_accept(solid, cand, tol, band_factor, cache): + """`(acceptance|None, reason|None)` for one candidate. Never raises on a bad candidate.""" + occ_shape = recognise.realised_for(cache, cand) + if occ_shape is None: + try: + occ_shape = prim.build_occ(cand) + except Exception as exc: # noqa: BLE001 + return None, f"candidate failed to build in OCCT: {exc}" + recognise.remember_realised(cache, cand, occ_shape) + if "props" not in cache: + cache["props"] = accept._props(solid) + result = accept.symmetric_difference(solid, occ_shape, tol, band_factor, + original_props=cache["props"]) + return result, (None if result.get("accepted") else result.get("reason")) + + +def process_solid(solid, name, tolerance=None, band_factor=1.0, cache=None): + """recognise -> build -> accept. Returns a record; `record['candidate']` is None if declined. + + A rejected candidate is retried with each of `_RETRIES`. `cache` is the per-solid memo they + share; `recognise.realised_for` reads the accepted candidate's OCCT shape back from it. + The memo is keyed by nothing but the solid, so it assumes the solid is not mutated while it lives. + """ + cache = {} if cache is None else cache + record = {"part": name, "recognised": False, "accepted": False, "candidate": None, + "reason": None, "acceptance": None, "recogniser": None, "description": None} + cand, reason = recognise.recognise(solid, cache=cache) + if cand is None: + record["reason"] = reason + return record + record["recognised"] = True + record["recogniser"] = cand["recogniser"] + record["description"] = prim.describe(cand) + tol = model_tolerance_cm(solid) if tolerance is None else tolerance + result, why_not = _build_and_accept(solid, cand, tol, band_factor, cache) + if result is not None: + record["acceptance"] = result + record["accepted"] = bool(result.get("accepted")) + if record["accepted"]: + record["candidate"] = cand + return record + record["reason"] = why_not + + notes = [] + for label, propose in _RETRIES: + alternative, alt_declined = propose(solid, cache=cache) + if alternative is None: + notes.append(f"as {label}: {alt_declined}") + continue + if alternative["recogniser"] == cand["recogniser"]: + # This matcher is what produced the candidate that was just refused; retrying it + # would refuse it again. + notes.append(f"as {label}: the same proposal that was just rejected") + continue + alt_result, alt_why_not = _build_and_accept(solid, alternative, tol, band_factor, cache) + if alt_result is not None and alt_result.get("accepted"): + record["retriedAfter"] = {"recogniser": cand["recogniser"], + "description": record["description"], "reason": why_not} + record["recogniser"] = alternative["recogniser"] + record["description"] = prim.describe(alternative) + record["acceptance"] = alt_result + record["accepted"] = True + record["candidate"] = alternative + record["reason"] = None + return record + notes.append(f"retried as {alternative['recogniser']} " + f"({prim.describe(alternative)}): {alt_why_not}") + record["reason"] = "; ".join([why_not] + notes) + return record + + +def write_shape_root(cand, path): + """Write the description as `shape_.root`, per the convention in O2SolidHarness.h. + + The shape is under `shape`, in cm, with an optional `placement` TGeoHMatrix from its own frame + to the part frame; no `placement` means the identity. + """ + return write_shape_object(*prim.build_root(cand, "shape"), path) + + +def write_shape_object(shape, placement, path): + """`write_shape_root`'s second half, for a caller that already built and checked the shape.""" + import ROOT + ROOT.gROOT.SetBatch(True) + out = ROOT.TFile.Open(str(path), "RECREATE") + out.WriteTObject(shape, "shape") + matrix = prim.root_placement_matrix(placement, "placement") + if matrix is not None: + out.WriteTObject(matrix, "placement") + out.Close() + return shape + + +# Points twin_parity draws by default: max(floor, per-cell * cells), so each cell gets enough. +_TWIN_PARITY_FLOOR = 4000 +_TWIN_PARITY_PER_CELL = 500 + + +def twin_parity(shape, n_points=None, seed=7771, grow=1.0): + """`Contains` against `Contains_Loop` on a shape that has twins. `None` when it has none. + + It samples the union of the declared cell boxes grown by `grow` about its centre, where a cell + reaching past its box shows up; `n_points` defaults to `max(4000, 500 * cells)`. + """ + if not (hasattr(shape, "Contains_Loop") and hasattr(shape, "GetCellBBox")): + return None + if n_points is None: + n_points = max(_TWIN_PARITY_FLOOR, _TWIN_PARITY_PER_CELL * shape.GetNcells()) + import random + from array import array + lo = [float("inf")] * 3 + hi = [float("-inf")] * 3 + cell_lo, cell_hi = array("d", [0.0] * 3), array("d", [0.0] * 3) + for cell in range(shape.GetNcells()): + shape.GetCellBBox(cell, cell_lo, cell_hi) + for axis in range(3): + lo[axis] = min(lo[axis], cell_lo[axis]) + hi[axis] = max(hi[axis], cell_hi[axis]) + if not all(math.isfinite(lo[i]) and math.isfinite(hi[i]) for i in range(3)): + return {"points": 0, "disagreements": 0, "insideAccelerated": 0, "growFactor": grow} + centre = [0.5 * (lo[i] + hi[i]) for i in range(3)] + half = [0.5 * (hi[i] - lo[i]) * (1.0 + grow) for i in range(3)] + rng = random.Random(seed) + probe = array("d", [0.0, 0.0, 0.0]) + disagreements = inside = 0 + for _ in range(n_points): + for axis in range(3): + probe[axis] = centre[axis] - half[axis] + rng.random() * 2.0 * half[axis] + accelerated = bool(shape.Contains(probe)) + inside += int(accelerated) + if accelerated != bool(shape.Contains_Loop(probe)): + disagreements += 1 + return {"points": n_points, "disagreements": disagreements, "insideAccelerated": inside, + "growFactor": grow} + + +def twin_decline_reason(parity): + """The one wording both emission paths use when the twin-parity gate refuses a part.""" + return (f"the emitted shape disagrees with its own _Loop twin about " + f"{parity['disagreements']} of {parity['points']} classified point(s): a cell " + "reaches past the bounding box declared for it, so the accelerated queries and the " + "reference ones are not describing the same solid") + + +def _occ_bbox(shape): + from OCC.Core.Bnd import Bnd_Box + from OCC.Core.BRepBndLib import brepbndlib + box = Bnd_Box() + brepbndlib.Add(shape, box) + # OCCT's box carries the shape's tolerance gap; ROOT's is tight. + box.SetGap(0.0) + return box.Get() + + +def crosscheck_bbox(cand, occ_shape=None, built=None): + """Max deviation, in cm, between the ROOT realisation's bounding box and the OCCT one. + + Exact for an unplaced primitive; for a placed or boolean shape ROOT's box is a hull, so + `crosscheck_contains` is the sharp check. + """ + occ_shape = occ_shape if occ_shape is not None else prim.build_occ(cand) + xmin, ymin, zmin, xmax, ymax, zmax = _occ_bbox(occ_shape) + shape, placement = built if built is not None else prim.build_root(cand, "bboxprobe") + origin = [shape.GetOrigin()[i] for i in range(3)] + half = [shape.GetDX(), shape.GetDY(), shape.GetDZ()] + lo_root = [origin[i] - half[i] for i in range(3)] + hi_root = [origin[i] + half[i] for i in range(3)] + if placement is not None: + lo_root, hi_root = _placed_box(placement, lo_root, hi_root) + worst = 0.0 + for i, (lo, hi) in enumerate(((xmin, xmax), (ymin, ymax), (zmin, zmax))): + worst = max(worst, abs(lo_root[i] - lo), abs(hi_root[i] - hi)) + return worst + + +def _placed_box(placement, lo, hi): + """The axis-aligned hull, in the part frame, of a local box under a rigid placement.""" + out_lo = [float("inf")] * 3 + out_hi = [float("-inf")] * 3 + for ix in (lo[0], hi[0]): + for iy in (lo[1], hi[1]): + for iz in (lo[2], hi[2]): + for i in range(3): + v = (placement[i][0] * ix + placement[i][1] * iy + placement[i][2] * iz + + placement[i][3]) + out_lo[i] = min(out_lo[i], v) + out_hi[i] = max(out_hi[i], v) + return out_lo, out_hi + + +def crosscheck_contains(cand, original, n_points=4000, seed=1234, built=None): + """Classify random points against the original CAD solid and against the emitted ROOT shape. + + Points within one model tolerance of the boundary are skipped. For an `O2FlatCSG` the same + points also count `twinDisagreements`, `Contains` against `Contains_Loop`. + """ + import random + from array import array + from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier + from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON + from OCC.Core.gp import gp_Pnt + + xmin, ymin, zmin, xmax, ymax, zmax = _occ_bbox(original) + pad = 0.05 * max(xmax - xmin, ymax - ymin, zmax - zmin) + shape, placement = built if built is not None else prim.build_root(cand, "containsprobe") + tol = max(model_tolerance_cm(original), 1.0e-9) + classifier = BRepClass3d_SolidClassifier(original) + rng = random.Random(seed) + disagreements = 0 + scored = 0 + has_twin = hasattr(shape, "Contains_Loop") + twin_disagreements = 0 if has_twin else None + for _ in range(n_points): + p = (rng.uniform(xmin - pad, xmax + pad), rng.uniform(ymin - pad, ymax + pad), + rng.uniform(zmin - pad, zmax + pad)) + classifier.Perform(gp_Pnt(*p), tol) + state = classifier.State() + if state == TopAbs_ON: + continue + scored += 1 + # The point is in the part frame; the shape answers in its own. + local = prim.placement_to_local(placement, p) + probe = array("d", list(local)) + accelerated = bool(shape.Contains(probe)) + if accelerated != (state == TopAbs_IN): + disagreements += 1 + if has_twin and accelerated != bool(shape.Contains_Loop(probe)): + twin_disagreements += 1 + return {"points": scored, "disagreements": disagreements, + "twinDisagreements": twin_disagreements} + + +# ------------------------------------------------------------------------------------------ +# driving a converter output directory +# ------------------------------------------------------------------------------------------ + +def load_brep(path): + from OCC.Core.BRep import BRep_Builder + from OCC.Core.BRepTools import breptools + from OCC.Core.TopAbs import TopAbs_SOLID + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import TopoDS_Shape, topods + shape = TopoDS_Shape() + builder = BRep_Builder() + if not breptools.Read(shape, str(path), builder): + raise RuntimeError(f"failed to read {path}") + solids = [] + exp = TopExp_Explorer(shape, TopAbs_SOLID) + while exp.More(): + solids.append(topods.Solid(exp.Current())) + exp.Next() + if len(solids) != 1: + return shape, len(solids) + return solids[0], 1 + + +def run_db(db_dir, write_root=True, band_factor=1.0, quiet=False): + db_dir = Path(db_dir) + breps = sorted(db_dir.glob("*/brep_*.brep")) or sorted(db_dir.glob("brep_*.brep")) + if not breps: + raise SystemExit(f"no brep_*.brep under {db_dir}") + records = [] + for brep in breps: + suffix = brep.name[len("brep_"):-len(".brep")] + part = f"{brep.parent.name}/{suffix}" + solid, n_solids = load_brep(brep) + record = process_solid(solid, part, band_factor=band_factor) + record["brep"] = str(brep) + record["nSolids"] = n_solids + if record["accepted"] and write_root: + target = brep.parent / f"shape_{suffix}.root" + write_shape_root(record["candidate"], target) + record["shape"] = str(target) + record["bboxRootVsOcctCm"] = crosscheck_bbox(record["candidate"]) + record["containsCrosscheck"] = crosscheck_contains(record["candidate"], solid) + json_target = brep.parent / f"csg_{suffix}.json" + json_target.write_text(json.dumps( + {"part": part, "candidate": record["candidate"], "acceptance": record["acceptance"], + "recogniser": record["recogniser"]}, indent=1)) + records.append(record) + if not quiet: + _print_record(record) + return records + + +def from_json(folder, quiet=False): + """Turn every accepted `csg_.json` in a folder into its `shape_.root`. + + Nothing is re-recognised, but `twin_parity` gates each part: a refused part gets no + `shape_.root` and no `flatcsg_.bin`. Returns `(written, refused)`. + """ + folder = Path(folder) + files = sorted(folder.glob("csg_*.json")) or sorted(folder.glob("*/csg_*.json")) + written, refused = [], [] + for path in files: + payload = json.loads(path.read_text()) + if not payload.get("candidate"): + continue + suffix = path.name[len("csg_"):-len(".json")] + target = path.parent / f"shape_{suffix}.root" + shape, placement = prim.build_root(payload["candidate"], "shape") + parity = twin_parity(shape) + if parity is not None and parity["disagreements"]: + refused.append((suffix, parity)) + if not quiet: + print(f" [REFUSED] {suffix}: {twin_decline_reason(parity)}; no shape file and " + "no sidecar written, so geom.C ships this part one tier down") + continue + if payload["candidate"].get("op") == "flatCells": + # The macro loads the flat sidecar, so a deferred part writes it here, after the gate. + from cadsupport import flat as flat_writer + blocks, cells = prim.flat_sidecar_records(payload["candidate"]) + flat_writer.write_sidecar(path.parent / f"flatcsg_{suffix}.bin", blocks, cells) + write_shape_object(shape, placement, target) + written.append(target) + if not quiet: + print(f" wrote {target} ({shape.ClassName()})") + if not quiet: + print(f"{len(written)} shape file(s) written from {len(files)} description(s)" + + (f"; {len(refused)} REFUSED by the twin-parity gate" if refused else "")) + return written, refused + + +def _print_record(record): + if record["accepted"]: + acc = record["acceptance"] + extra = "" + if record.get("containsCrosscheck") is not None: + cc = record["containsCrosscheck"] + twin = ("" if cc.get("twinDisagreements") is None + else f", twin {cc['twinDisagreements']}/{cc['points']}") + parity = record.get("twinParity") + box_twin = ("" if not parity + else f", twin(boxes x2) {parity['disagreements']}/{parity['points']}") + extra = (f", ROOT-vs-CAD Contains {cc['disagreements']}/{cc['points']}{twin}{box_twin}" + f", bbox(ROOT vs OCCT) {record['bboxRootVsOcctCm']:.2e} cm") + print(f" [CSG ] {record['part']}: {record['description']} " + f"[{record['recogniser']}] dV_sym={acc['symmetricDifference']:.3g} cm^3 " + f"(band {acc['band']:.3g}, rel {acc['relativeToVolume']:.2e}){extra}") + elif record["recognised"]: + print(f" [rej ] {record['part']}: {record['description']} rejected -- {record['reason']}") + else: + print(f" [decl] {record['part']}: {record['reason']}") + + +def summarise(records): + n_csg = sum(1 for r in records if r["accepted"]) + n_rej = sum(1 for r in records if r["recognised"] and not r["accepted"]) + n_dec = sum(1 for r in records if not r["recognised"]) + print(f"\n{n_csg}/{len(records)} part(s) accepted as CSG " + f"({n_rej} recognised but rejected by the symmetric difference, {n_dec} declined " + f"by the recogniser)") + return n_csg, n_rej, n_dec + + +# ------------------------------------------------------------------------------------------ + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--db", type=Path, help="a gate workdir's db/ directory (walks brep_*.brep)") + ap.add_argument("--brep", type=Path, help="a single .brep file, in cm") + ap.add_argument("--from-json", type=Path, dest="from_json", + help="write shape_*.root for every accepted csg_*.json in this folder " + "(needs PyROOT only; nothing is re-recognised)") + ap.add_argument("--report", type=Path, help="write the per-part record as JSON") + ap.add_argument("--no-root", action="store_true", + help="recognise and accept, but do not write shape_*.root (no PyROOT needed)") + ap.add_argument("--band-factor", type=float, default=1.0, + help="multiplier on the acceptance band (model tolerance x area); " + "default %(default)s") + ap.add_argument("--self-test", action="store_true") + ap.add_argument("--no-self-test", action="store_true", + help="skip the self-test that otherwise runs before any emission") + args = ap.parse_args() + from cadsupport.selftest_emit import self_test + + if args.self_test: + ok_a, n_a = accept.self_test() + ok_e, n_e = self_test(with_root=not args.no_root) + print(f"\n{ok_a + ok_e}/{n_a + n_e} self-checks passed") + return 0 if (ok_a == n_a and ok_e == n_e) else 1 + + if args.from_json: + _written, refused = from_json(args.from_json) + if refused: + # A refused part is a broken description, not a cosmetic warning: it would have + # shipped a solid that disagrees with its own reference implementation. + return 1 + return 0 + + if not args.db and not args.brep: + ap.error("give --db, --brep, --from-json or --self-test") + + if not args.no_self_test: + ok_a, n_a = accept.self_test(verbose=False) + ok_e, n_e = self_test(verbose=False, with_root=not args.no_root) + if ok_a != n_a or ok_e != n_e: + raise SystemExit(f"self-test failed ({ok_a}/{n_a} acceptance, {ok_e}/{n_e} " + "recognise/emit); refusing to emit") + print(f"[self-test] {ok_a + ok_e}/{n_a + n_e} checks passed") + + if args.brep: + solid, _n = load_brep(args.brep) + suffix = args.brep.name[len("brep_"):-len(".brep")] + record = process_solid(solid, suffix, band_factor=args.band_factor) + if record["accepted"] and not args.no_root: + target = args.brep.parent / f"shape_{suffix}.root" + write_shape_root(record["candidate"], target) + record["shape"] = str(target) + record["bboxRootVsOcctCm"] = crosscheck_bbox(record["candidate"]) + record["containsCrosscheck"] = crosscheck_contains(record["candidate"], solid) + _print_record(record) + records = [record] + else: + records = run_db(args.db, write_root=not args.no_root, band_factor=args.band_factor) + summarise(records) + if args.report: + args.report.write_text(json.dumps(records, indent=1)) + print(f"Wrote {args.report}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/tools/cadsupport/emit_selftest_candidates.json b/Detectors/CADSupport/tools/cadsupport/emit_selftest_candidates.json new file mode 100644 index 0000000000000..b0c791fcf6462 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/emit_selftest_candidates.json @@ -0,0 +1,3599 @@ +{ + "Arb8 (TPC_IHSTR's trapezoidal prism)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 0.6, + "vertices": [ + 0.0, + 0.0, + 3.38, + 0.0, + 2.3, + 1.08, + 0.0, + 1.08, + 0.0, + 0.0, + 3.38, + 0.0, + 2.3, + 1.08, + 0.0, + 1.08 + ] + }, + "type": "TGeoArb8" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 0.0, + "prismGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "rung2-arb8" + }, + "Arb8 (a TGeoTrap's eight corners)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "vertices": [ + -4.653488486034171, + 1.698463103929542, + -4.003443140137866, + -2.301536896070458, + 1.996556859862133, + -2.301536896070458, + 3.3465115139658295, + 1.698463103929542, + -2.996556859862133, + 2.301536896070458, + -2.346511513965829, + -1.698463103929542, + 3.653488486034171, + -1.698463103929542, + 5.003443140137866, + 2.301536896070458 + ] + }, + "type": "TGeoArb8" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 5.269540684211594e-16, + "prismGapRelative": 3.5984475572404803e-17 + }, + "op": "primitive", + "recogniser": "rung2-arb8" + }, + "Arb8 (parallelepiped)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 3.0, + "vertices": [ + -2.0, + -2.0, + 2.0, + -2.0, + 2.0, + 2.0, + -2.0, + 2.0, + -1.0, + -1.5, + 3.0, + -1.5, + 3.0, + 2.5, + -1.0, + 2.5 + ] + }, + "type": "TGeoArb8" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 3.3642012326330224e-17, + "prismGapRelative": 3.732246025365849e-18 + }, + "op": "primitive", + "recogniser": "rung2-arb8" + }, + "Arb8 (sheared in x only)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 2.0, + "vertices": [ + -2.0, + -1.0, + 2.0, + -1.0, + 2.0, + 1.0, + -2.0, + 1.0, + -2.0, + -1.0, + 4.0, + -1.0, + 4.0, + 1.0, + -2.0, + 1.0 + ] + }, + "type": "TGeoArb8" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 4.5393212517870803e-17, + "prismGapRelative": 6.065922915992254e-18 + }, + "op": "primitive", + "recogniser": "rung2-arb8" + }, + "L-shaped plate": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "scale": [ + 1.0, + 1.0 + ], + "x": [ + 0.0, + 4.0, + 4.0, + 2.0, + 2.0, + 0.0 + ], + "xoff": [ + 0.0, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 2.0, + 2.0, + 4.0, + 4.0 + ], + "yoff": [ + 0.0, + 0.0 + ], + "z": [ + 0.0, + 1.0 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 6, + "nSections": 2, + "nWires": 1, + "prismGapCm": 0.0, + "prismGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "NURBS-encoded box": { + "leaves": [ + { + "frame": { + "origin": [ + 1.0, + 1.5, + 2.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 1.0, + "dy": 1.5, + "dz": 2.0 + }, + "type": "TGeoBBox" + } + ], + "notes": { + "tier0Faces": 6, + "tier0WorstGapCm": 0.0, + "tier0WorstGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "tier1-box" + }, + "NURBS-encoded cone": { + "leaves": [ + { + "frame": { + "origin": [ + -2.87537903068208e-17, + 4.407427436385315e-17, + -2.0 + ], + "x": [ + 1.0, + -2.2037137181926578e-17, + -1.61878471725017e-34 + ], + "y": [ + -2.2037137181926578e-17, + -1.0, + -7.345712393975528e-18 + ], + "z": [ + -0.0, + 7.345712393975528e-18, + -1.0 + ] + }, + "params": { + "dz": 3.0000000000000004, + "rmax1": 0.9999999999999997, + "rmax2": 3.0000000000000013, + "rmin1": 0.0, + "rmin2": 0.0 + }, + "type": "TGeoCone" + } + ], + "notes": { + "tier0Faces": 3, + "tier0WorstGapCm": 2.220446049250313e-15, + "tier0WorstGapRelative": 1.4802973327551765e-16 + }, + "op": "primitive", + "recogniser": "tier1-cone" + }, + "NURBS-encoded cube with an axial through-hole": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 3.0, + "dy": 3.0, + "dz": 3.0 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + -2.6617596725924927e-16, + 2.2204460492503165e-16, + 0.0 + ], + "x": [ + -1.0, + 1.2908167325507775e-15, + -0.0 + ], + "y": [ + -1.2908167325507775e-15, + -1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "outside": true, + "params": { + "dz": 5.598076297955856, + "rmax": 1.5000000000000007, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "cellGapCm": 2.814441328640912e-15, + "cellGapRelative": 2.7081973409088196e-16, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 7, + "nLeaves": 2, + "nOutside": 1, + "tier0Faces": 7, + "tier0WorstGapCm": 1.3322676295501878e-15, + "tier0WorstGapRelative": 1.2819750815232067e-16 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "NURBS-encoded hollow torus wedge": { + "leaves": [ + { + "frame": { + "origin": [ + 1.8538593469225349e-16, + -1.3657331084934836e-16, + -5.5130637352258837e-17 + ], + "x": [ + 1.0, + -3.1603014293071932e-34, + 1.8904723831159486e-17 + ], + "y": [ + 0.0, + 1.0, + 1.6716993369129594e-17 + ], + "z": [ + -1.8904723831159486e-17, + -1.6716993369129594e-17, + 1.0 + ] + }, + "params": { + "dphi": 140.0, + "phi1": 1.7389054012879818e-15, + "r": 6.0, + "rmax": 1.5, + "rmin": 0.7 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "nTori": 2, + "nWedges": 2, + "tier0Faces": 4, + "tier0WorstGapCm": 2.6645352591003757e-15, + "tier0WorstGapRelative": 1.0640709402967009e-16, + "torusGapCm": 1.8343989859068613e-15, + "torusGapRelative": 7.325595137638587e-17 + }, + "op": "primitive", + "recogniser": "tier1-torus" + }, + "NURBS-encoded solid cylinder": { + "leaves": [ + { + "frame": { + "origin": [ + -2.434789753871469e-16, + 2.0314404549504875e-31, + 0.0 + ], + "x": [ + -1.0, + 8.343391669528629e-16, + -0.0 + ], + "y": [ + -8.343391669528629e-16, + -1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "rmax": 2.0000000000000004, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "tier0Faces": 3, + "tier0WorstGapCm": 1.3322676295501878e-15, + "tier0WorstGapRelative": 9.82160702636268e-17 + }, + "op": "primitive", + "recogniser": "tier1-tube" + }, + "NURBS-encoded solid torus": { + "leaves": [ + { + "frame": { + "origin": [ + -1.6799071085578556e-16, + -2.3010989955208096e-16, + 1.8685153885541143e-16 + ], + "x": [ + 1.0, + -2.566708458273968e-36, + -2.3938119370970135e-17 + ], + "y": [ + 0.0, + 1.0, + -1.0722264428953541e-19 + ], + "z": [ + 2.3938119370970135e-17, + 1.0722264428953541e-19, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 5.999999999999999, + "rmax": 1.4999999999999998, + "rmin": 0.0 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "nTori": 1, + "nWedges": 0, + "tier0Faces": 1, + "tier0WorstGapCm": 2.886579864025407e-15, + "tier0WorstGapRelative": 8.304340957381122e-17, + "torusGapCm": 2.006252412035028e-15, + "torusGapRelative": 5.771745408378733e-17 + }, + "op": "primitive", + "recogniser": "tier1-torus" + }, + "NURBS-encoded sphere": { + "leaves": [ + { + "frame": { + "origin": [ + 2.2082989928976365e-16, + 4.440892098500626e-16, + -1.3987495347505313e-17 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "rmax": 2.999999999999996, + "rmin": 0.0 + }, + "type": "TGeoSphere" + } + ], + "notes": { + "tier0Faces": 1, + "tier0WorstGapCm": 5.329070518200751e-15, + "tier0WorstGapRelative": 3.552713598612424e-16 + }, + "op": "primitive", + "recogniser": "tier1-sphere" + }, + "NURBS-encoded tube segment": { + "leaves": [ + { + "frame": { + "origin": [ + -3.55263211659603e-16, + -2.581138319064717e-16, + 0.0 + ], + "x": [ + -0.8090169943749472, + -0.5877852522924734, + 0.0 + ], + "y": [ + 0.5877852522924734, + -0.8090169943749472, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "phi1": 144.0, + "phi2": 215.99999999999997, + "rmax": 1.9999999999999998, + "rmin": 0.0 + }, + "type": "TGeoTubeSeg" + } + ], + "notes": { + "tier0Faces": 5, + "tier0WorstGapCm": 1.1102230246251565e-15, + "tier0WorstGapRelative": 1.0702067643120936e-16 + }, + "op": "primitive", + "recogniser": "tier1-tubeseg" + }, + "Pgon (TPC_Strip's thin 18-edge shell)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 18.0, + "phi1": 0.0, + "rmax": [ + 85.235, + 85.235 + ], + "rmin": [ + 85.22499999999998, + 85.22499999999998 + ], + "z": [ + -124.8, + 124.8 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 36, + "nSections": 2, + "nWires": 2, + "prismGapCm": 2.929642751054232e-14, + "prismGapRelative": 8.410887378564506e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (a 90 deg wedge closing on the axis)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 90.0, + "nedges": 3.0, + "phi1": 10.0, + "rmax": [ + 3.9999999999999996, + 3.9999999999999996 + ], + "rmin": [ + 0.0, + 0.0 + ], + "z": [ + -2.0, + 2.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 5, + "nSections": 2, + "nWires": 1, + "prismGapCm": 4.47545209131181e-16, + "prismGapRelative": 5.999587827638793e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (a wedge across phi = 0)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 19.99999999999998, + "nedges": 2.0, + "phi1": 350.0, + "rmax": [ + 4.0, + 4.0 + ], + "rmin": [ + 0.0, + 0.0 + ], + "z": [ + -2.0, + 2.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 4.440892098500626e-16, + "prismGapRelative": 7.608565229642237e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (hollow 48-edge prism)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 48.0, + "phi1": 0.0, + "rmax": [ + 2.9999999999999996, + 2.9999999999999996 + ], + "rmin": [ + 1.4999999999999998, + 1.4999999999999998 + ], + "z": [ + -5.0, + 5.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 96, + "nSections": 2, + "nWires": 2, + "prismGapCm": 9.930136612989092e-16, + "prismGapRelative": 7.564859091567714e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (hollow 8-edge prism)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 8.0, + "phi1": 0.0, + "rmax": [ + 3.0, + 3.0 + ], + "rmin": [ + 1.5, + 1.5 + ], + "z": [ + -5.0, + 5.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 16, + "nSections": 2, + "nWires": 2, + "prismGapCm": 6.280369834735101e-16, + "prismGapRelative": 4.625512005605031e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (solid hexagonal prism)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 6.0, + "phi1": 0.0, + "rmax": [ + 2.9999999999999996, + 2.9999999999999996 + ], + "rmin": [ + 0.0, + 0.0 + ], + "z": [ + -5.0, + 5.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 6, + "nSections": 2, + "nWires": 1, + "prismGapCm": 8.95090418262362e-16, + "prismGapRelative": 6.598693945752998e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (tapered eight-edge prism)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 8.0, + "phi1": 0.0, + "rmax": [ + 3.0, + 1.5 + ], + "rmin": [ + 0.0, + 0.0 + ], + "z": [ + -5.0, + 5.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 8, + "nSections": 2, + "nWires": 1, + "prismGapCm": 6.280369834735101e-16, + "prismGapRelative": 4.625512005605031e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Pgon (three hollow sections)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 6.0, + "phi1": 0.0, + "rmax": [ + 2.9999999999999996, + 2.9999999999999996, + 4.0 + ], + "rmin": [ + 1.0, + 1.0, + 2.0 + ], + "z": [ + -5.0, + 0.0, + 5.0 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 12, + "nSections": 3, + "nWires": 2, + "prismGapCm": 8.95090418262362e-16, + "prismGapRelative": 5.668611938065619e-17 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "Steinmetz solid (two cylinders intersected)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + -0.0, + 0.0, + 1.0 + ], + "y": [ + 0.0, + -1.0, + 0.0 + ], + "z": [ + 1.0, + 0.0, + 0.0 + ] + }, + "params": { + "dz": 1.8660254903869793, + "rmax": 1.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 1.8660254903869795, + "rmax": 1.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "cellGapCm": 3.8459253727671276e-16, + "cellGapRelative": 1.1102229136028649e-16, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 2, + "nLeaves": 2, + "nOutside": 0 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "Trd1 (TPC_IRB1's 0.5 % slant)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx1": 14.205637404580152, + "dx2": 14.281551908396947, + "dy": 2.06, + "dz": 0.2 + }, + "type": "TGeoTrd1" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 1.7763569307031836e-15, + "prismGapRelative": 6.154766237633496e-17 + }, + "op": "primitive", + "recogniser": "rung2-trd1" + }, + "Trd1 (slanted x faces)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx1": 3.0, + "dx2": 1.0, + "dy": 2.0, + "dz": 5.0 + }, + "type": "TGeoTrd1" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 1.0501002181931766e-17, + "prismGapRelative": 8.51743726210796e-19 + }, + "op": "primitive", + "recogniser": "rung2-trd1" + }, + "Trd1 (taper reversed)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx1": 1.0, + "dx2": 3.0, + "dy": 2.0, + "dz": 4.0 + }, + "type": "TGeoTrd1" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 3.819614671290182e-17, + "prismGapRelative": 3.54642308039489e-18 + }, + "op": "primitive", + "recogniser": "rung2-trd1" + }, + "Trd2 (both half-widths vary)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx1": 3.0, + "dx2": 1.0, + "dy1": 2.0, + "dy2": 4.0, + "dz": 5.0 + }, + "type": "TGeoTrd2" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 4.024545237684801e-17, + "prismGapRelative": 2.8457831604601527e-18 + }, + "op": "primitive", + "recogniser": "rung2-trd2" + }, + "Trd2 (isotropic taper, also a legal Xtru)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx1": 3.0, + "dx2": 1.5, + "dy1": 2.0, + "dy2": 1.0, + "dz": 5.0 + }, + "type": "TGeoTrd2" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 7.742941401211941e-17, + "prismGapRelative": 6.280354623911604e-18 + }, + "op": "primitive", + "recogniser": "rung2-trd2" + }, + "Xtru (ITS ConeARibVol0's eight-corner section)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "scale": [ + 1.0, + 1.0 + ], + "x": [ + 0.0, + 4.2, + 4.2, + 5.05, + 9.803, + 5.9, + 5.0, + 0.0 + ], + "xoff": [ + 0.0, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 0.1, + 0.1, + 1.83, + 1.83, + 2.73, + 2.73 + ], + "yoff": [ + 0.0, + 0.0 + ], + "z": [ + -0.045, + 0.045 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 8, + "nSections": 2, + "nWires": 1, + "prismGapCm": 8.881784197001252e-16, + "prismGapRelative": 8.72779598296214e-17 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "Xtru (a triangular section)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "scale": [ + 1.0, + 1.0 + ], + "x": [ + 0.0, + 0.05, + 0.0 + ], + "xoff": [ + 0.0, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 0.074 + ], + "yoff": [ + 0.0, + 0.0 + ], + "z": [ + -14.5, + 14.5 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 3, + "nSections": 2, + "nWires": 1, + "prismGapCm": 7.757919228897728e-18, + "prismGapRelative": 2.675131857785675e-19 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "Xtru (non-convex L section)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "scale": [ + 1.0, + 1.0 + ], + "x": [ + 0.0, + 3.0, + 3.0, + 1.0, + 1.0, + 0.0 + ], + "xoff": [ + 0.0, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 1.0, + 1.0, + 3.0, + 3.0 + ], + "yoff": [ + 0.0, + 0.0 + ], + "z": [ + -2.0, + 2.0 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 6, + "nSections": 2, + "nWires": 1, + "prismGapCm": 0.0, + "prismGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "Xtru (three sections, offset and scaled)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "scale": [ + 1.0, + 1.4, + 0.6000000000000001 + ], + "x": [ + 0.0, + 2.0, + 2.0, + 1.0, + 0.0 + ], + "xoff": [ + 0.0, + 0.5, + 1.0 + ], + "y": [ + 0.0, + 0.0, + 1.0, + 2.0, + 2.0 + ], + "yoff": [ + 0.0, + -0.25, + -1.1102230246251565e-16 + ], + "z": [ + -3.0, + 0.0, + 3.0 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 5, + "nSections": 3, + "nWires": 1, + "prismGapCm": 4.440892098500626e-16, + "prismGapRelative": 6.002849814483854e-17 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "a cylinder with a hexagonal collar": { + "cells": [ + { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + -6.661338147750939e-16, + 3.8050383565440793 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 6.074178328225914, + "dy": 5.61007671308816, + "dz": 3.8050383565440793 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 0.0, + -6.661338147750939e-16, + 1.1949616434559207 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + -1.0, + 0.0 + ], + "z": [ + -0.0, + -0.0, + -1.0 + ] + }, + "params": { + "dx": 6.074178328225914, + "dy": 5.61007671308816, + "dz": 3.8050383565440793 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + -1.6730267092890643e-16, + -1.305038356544081, + 2.5 + ], + "x": [ + 1.0, + -1.2819751242557095e-16, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 1.0 + ], + "z": [ + -1.2819751242557095e-16, + -1.0, + 0.0 + ] + }, + "params": { + "dx": 6.074178328225914, + "dy": 5.110076713088159, + "dz": 4.305038356544081 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 1.0038160255734397e-15, + 1.305038356544081, + 2.5 + ], + "x": [ + 1.0, + -7.691850745534258e-16, + 0.0 + ], + "y": [ + 0.0, + -0.0, + -1.0 + ], + "z": [ + 7.691850745534258e-16, + 1.0, + -0.0 + ] + }, + "params": { + "dx": 6.074178328225916, + "dy": 5.110076713088159, + "dz": 4.305038356544081 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + -1.779715422518596, + 1.0275191782720403, + 2.5 + ], + "x": [ + 0.5000000000000003, + 0.8660254037844385, + 0.0 + ], + "y": [ + 0.0, + 0.0, + -1.0 + ], + "z": [ + -0.8660254037844385, + 0.5000000000000003, + 0.0 + ] + }, + "params": { + "dx": 6.940203732010353, + "dy": 5.110076713088159, + "dz": 5.055038356544079 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 1.779715422518596, + 1.0275191782720385, + 2.5 + ], + "x": [ + 0.4999999999999996, + -0.8660254037844388, + 0.0 + ], + "y": [ + 0.0, + -0.0, + -1.0 + ], + "z": [ + 0.8660254037844388, + 0.4999999999999997, + -0.0 + ] + }, + "params": { + "dx": 6.940203732010353, + "dy": 5.110076713088159, + "dz": 5.055038356544079 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + -1.779715422518596, + -1.0275191782720403, + 2.5 + ], + "x": [ + 0.4999999999999999, + -0.8660254037844388, + 0.0 + ], + "y": [ + 0.0, + 0.0, + 1.0000000000000002 + ], + "z": [ + -0.8660254037844387, + -0.49999999999999994, + 0.0 + ] + }, + "params": { + "dx": 6.940203732010353, + "dy": 5.1100767130881595, + "dz": 5.05503835654408 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 1.7797154225185956, + -1.0275191782720412, + 2.5 + ], + "x": [ + 0.5000000000000001, + 0.8660254037844387, + 0.0 + ], + "y": [ + -0.0, + 0.0, + 1.0000000000000002 + ], + "z": [ + 0.8660254037844386, + -0.5000000000000002, + 0.0 + ] + }, + "params": { + "dx": 6.940203732010355, + "dy": 5.1100767130881595, + "dz": 5.05503835654408 + }, + "type": "TGeoBBox" + } + ], + "op": "intersection" + }, + { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + -2.5 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 2.5, + "rmax": 3.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "op": "primitive" + } + ], + "notes": { + "cellGapCm": 6.669805074684541e-10, + "cellGapRelative": 4.9170454143743324e-11, + "cellLeaves": [ + 8, + 1 + ], + "containsScored": 4000, + "marginDiagonals": 0.25, + "nCarriers": 11, + "nCells": 2, + "nComponents": 1, + "nLeaves": 9, + "nOutside": 0, + "nSplits": 1, + "volumeDriftRelative": 1.912269981636708e-16 + }, + "op": "unionOfCells", + "recogniser": "cells-union" + }, + "a torus with a cylinder through it": { + "cells": [ + { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 2.0, + "rmax": 2.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "outside": true, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 2.5, + "rmax": 0.8, + "rmin": 0.0 + }, + "type": "TGeoTorus" + } + ], + "op": "intersection" + }, + { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 2.5, + "rmax": 0.8, + "rmin": 0.0 + }, + "type": "TGeoTorus" + } + ], + "op": "primitive" + } + ], + "notes": { + "cellGapCm": 9.485749680535094e-16, + "cellGapRelative": 8.729846219708091e-17, + "cellLeaves": [ + 2, + 1 + ], + "containsScored": 4000, + "marginDiagonals": 0.25, + "nCarriers": 5, + "nCells": 2, + "nComponents": 1, + "nLeaves": 3, + "nOutside": 1, + "nSplits": 1, + "volumeDriftRelative": 0.0 + }, + "op": "unionOfCells", + "recogniser": "cells-union" + }, + "box": { + "leaves": [ + { + "frame": { + "origin": [ + 1.0, + 1.5, + 2.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 1.0, + "dy": 1.5, + "dz": 2.0 + }, + "type": "TGeoBBox" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-box" + }, + "cone": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "rmax1": 3.0, + "rmax2": 0.9999999999999996, + "rmin1": 0.0, + "rmin2": 0.0 + }, + "type": "TGeoCone" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-cone" + }, + "cube with an axial through-hole": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 2.0, + "dy": 2.0, + "dz": 2.0 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "outside": true, + "params": { + "dz": 3.732050894171417, + "rmax": 0.8, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "cellGapCm": 0.0, + "cellGapRelative": 0.0, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 7, + "nLeaves": 2, + "nOutside": 1 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "cylinder cut by an oblique plane": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 3.001646143988088 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 3.001646143988088, + "rmax": 1.2, + "rmin": 0.0 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + 0.0, + 1.5169700591347397, + 1.4134074125472813 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + -0.5000000000000001, + -0.8660254037844386 + ], + "z": [ + -0.0, + 0.8660254037844386, + -0.5000000000000001 + ] + }, + "params": { + "dx": 2.6248313188935244, + "dy": 4.007363073624073, + "dz": 1.8570309017174251 + }, + "type": "TGeoBBox" + } + ], + "notes": { + "cellGapCm": 1.8710928022974537e-15, + "cellGapRelative": 3.283007569889888e-16, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 3, + "nLeaves": 2, + "nOutside": 0 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "cylinder with a milled flat": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "rmax": 2.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + -1.6655940145615564, + 0.0, + 0.0 + ], + "x": [ + 0.0, + 1.0, + 0.0 + ], + "y": [ + -0.0, + 0.0, + -1.0 + ], + "z": [ + -1.0, + -0.0, + 0.0 + ] + }, + "params": { + "dx": 4.831188029123113, + "dy": 7.831188029123113, + "dz": 3.1655940145615564 + }, + "type": "TGeoBBox" + } + ], + "notes": { + "cellGapCm": 0.0, + "cellGapRelative": 0.0, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 4, + "nLeaves": 2, + "nOutside": 0 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "elliptic cylinder with equal semi-axes": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "a": 2.0, + "b": 2.0, + "dz": 5.0 + }, + "type": "TGeoEltu" + } + ], + "notes": { + "eltuGapCm": 0.0, + "eltuGapRelative": 0.0, + "semiAxisRatio": 1.0 + }, + "op": "primitive", + "recogniser": "tier1-eltu" + }, + "elliptic cylinder, a < b": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + -0.0, + -0.0 + ], + "y": [ + 0.0, + 1.0, + -0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "a": 1.5, + "b": 3.0, + "dz": 5.0 + }, + "type": "TGeoEltu" + } + ], + "notes": { + "eltuGapCm": 0.0, + "eltuGapRelative": 0.0, + "semiAxisRatio": 0.5 + }, + "op": "primitive", + "recogniser": "tier1-eltu" + }, + "elliptic cylinder, a > b": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "a": 3.0, + "b": 1.5, + "dz": 5.0 + }, + "type": "TGeoEltu" + } + ], + "notes": { + "eltuGapCm": 0.0, + "eltuGapRelative": 0.0, + "semiAxisRatio": 0.5 + }, + "op": "primitive", + "recogniser": "tier1-eltu" + }, + "half a bellows ply": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 5.0, + "rmax": 0.3, + "rmin": 0.0 + }, + "type": "TGeoTorus" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 2.179235673962775 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 9.79515000947524, + "dy": 9.79515000947524, + "dz": 2.179235673962775 + }, + "type": "TGeoBBox" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "outside": true, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 5.0, + "rmax": 0.28, + "rmin": 0.0 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "cellGapCm": 0.0, + "cellGapRelative": 0.0, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 3, + "nLeaves": 3, + "nOutside": 1 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "hollow 48-edge polygon (TGeoPgon)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 48.0, + "phi1": 0.0, + "rmax": [ + 2.9999999999999996, + 2.9999999999999996 + ], + "rmin": [ + 1.4999999999999998, + 1.4999999999999998 + ], + "z": [ + -5.000000000000004, + 4.9999999999999964 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 96, + "nSections": 2, + "nWires": 2, + "prismGapCm": 4.534279142523387e-15, + "prismGapRelative": 3.454250801715585e-16 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "hollow 8-edge polygon (TGeoPgon)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "nedges": 8.0, + "phi1": 0.0, + "rmax": [ + 3.0, + 3.0 + ], + "rmin": [ + 1.5, + 1.5 + ], + "z": [ + -5.000000000000002, + 4.999999999999998 + ] + }, + "type": "TGeoPgon" + } + ], + "notes": { + "nCorners": 16, + "nSections": 2, + "nWires": 2, + "prismGapCm": 1.88411095042053e-15, + "prismGapRelative": 1.3876535843775772e-16 + }, + "op": "primitive", + "recogniser": "rung2-pgon" + }, + "hollow torus wedge": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 120.0, + "phi1": 20.0, + "r": 4.0, + "rmax": 1.0, + "rmin": 0.8 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "nTori": 2, + "nWedges": 2, + "torusGapCm": 1.1102230246251565e-15, + "torusGapRelative": 1.0798827056165825e-16 + }, + "op": "primitive", + "recogniser": "tier1-torus" + }, + "placed Trd1": { + "leaves": [ + { + "frame": { + "origin": [ + 3.0, + -4.0, + 5.0 + ], + "x": [ + 0.8824210936422443, + 0.11757890635775577, + -0.4555306952060858 + ], + "y": [ + -0.11757890635775585, + -0.8824210936422444, + -0.4555306952060858 + ], + "z": [ + -0.45553069520608575, + 0.45553069520608575, + -0.7648421872844885 + ] + }, + "params": { + "dx1": 1.0000000000000009, + "dx2": 3.0000000000000004, + "dy": 2.000000000000001, + "dz": 5.000000000000002 + }, + "type": "TGeoTrd1" + } + ], + "notes": { + "nCorners": 4, + "nSections": 2, + "nWires": 1, + "prismGapCm": 3.76822190084106e-15, + "prismGapRelative": 2.2768363131657283e-16 + }, + "op": "primitive", + "recogniser": "rung2-trd1" + }, + "placed Xtru (non-convex L section)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 0.8902200771298411, + 0.23309765709167907, + -0.39137411326416294 + ], + "y": [ + 0.0, + -0.8591607928574432, + -0.511705708407254 + ], + "z": [ + -0.45553069520608575, + 0.45553069520608575, + -0.7648421872844885 + ] + }, + "params": { + "scale": [ + 1.0, + 1.0 + ], + "x": [ + -0.2186009632980075, + 0.17763444874810785, + 1.1688737122875894, + 0.9047167709235127, + 2.8871952980024758, + 2.7551168273204367 + ], + "xoff": [ + 0.0, + 0.0 + ], + "y": [ + 0.8781146293935023, + -2.0956031612249424, + -1.963524690542904, + 0.01895383653605931, + 0.2831107779001365, + 1.274350041439618 + ], + "yoff": [ + 0.0, + -1.6653345369377348e-16 + ], + "z": [ + -9.012925802865045, + -5.012925802865042 + ] + }, + "type": "TGeoXtru" + } + ], + "notes": { + "nCorners": 6, + "nSections": 2, + "nWires": 1, + "prismGapCm": 3.972054645195637e-15, + "prismGapRelative": 4.56726478491397e-16 + }, + "op": "primitive", + "recogniser": "rung2-xtru" + }, + "placed tube": { + "leaves": [ + { + "frame": { + "origin": [ + 3.0000000000000004, + -4.0, + 5.0 + ], + "x": [ + 0.8824210936422443, + 0.11757890635775577, + -0.45553069520608575 + ], + "y": [ + 0.11757890635775582, + 0.8824210936422444, + 0.4555306952060858 + ], + "z": [ + 0.45553069520608575, + -0.45553069520608575, + 0.7648421872844885 + ] + }, + "params": { + "dz": 5.000000000000001, + "rmax": 2.0, + "rmin": 1.0 + }, + "type": "TGeoTube" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-tube" + }, + "rod-and-eye (two-cluster union)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + -0.0, + 0.0, + 1.0 + ], + "y": [ + 0.0, + -1.0, + 0.0 + ], + "z": [ + 1.0, + 0.0, + 0.0 + ] + }, + "params": { + "dz": 0.75, + "rmax": 1.2, + "rmin": 0.7 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 4.519615242270663 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 3.480384757729337, + "rmax": 0.6, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "nCaps": [ + 2, + 1 + ] + }, + "op": "union", + "recogniser": "tier2-tube-union" + }, + "solid cylinder": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "rmax": 2.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-tube" + }, + "solid torus": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 4.0, + "rmax": 1.0, + "rmin": 0.0 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "nTori": 1, + "nWedges": 0, + "torusGapCm": 0.0, + "torusGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "tier1-torus" + }, + "sphere": { + "leaves": [ + { + "frame": { + "origin": [ + 1.0, + 2.0, + 3.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "rmax": 2.5, + "rmin": 0.0 + }, + "type": "TGeoSphere" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-sphere" + }, + "three disjoint boxes": { + "cells": [ + { + "leaves": [ + { + "frame": { + "origin": [ + 9.0, + 1.0, + 1.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 1.0, + "dy": 1.0, + "dz": 1.0 + }, + "type": "TGeoBBox" + } + ], + "op": "primitive" + }, + { + "leaves": [ + { + "frame": { + "origin": [ + 5.0, + 1.0, + 1.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 1.0, + "dy": 1.0, + "dz": 1.0 + }, + "type": "TGeoBBox" + } + ], + "op": "primitive" + }, + { + "leaves": [ + { + "frame": { + "origin": [ + 1.0, + 1.0, + 1.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dx": 1.0, + "dy": 1.0, + "dz": 1.0 + }, + "type": "TGeoBBox" + } + ], + "op": "primitive" + } + ], + "notes": { + "cellGapCm": 0.0, + "cellGapRelative": 0.0, + "cellLeaves": [ + 1, + 1, + 1 + ], + "containsScored": 4000, + "marginDiagonals": 0.25, + "nCarriers": 18, + "nCells": 3, + "nComponents": 3, + "nLeaves": 3, + "nOutside": 0, + "nSplits": 0, + "volumeDriftRelative": 2.9605947323337526e-16 + }, + "op": "unionOfCells", + "recogniser": "cells-union" + }, + "torus shell (a bellows ply)": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + 0.0 + ], + "y": [ + 0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dphi": 360.0, + "phi1": 0.0, + "r": 5.0, + "rmax": 0.3, + "rmin": 0.28 + }, + "type": "TGeoTorus" + } + ], + "notes": { + "nTori": 2, + "nWedges": 0, + "torusGapCm": 0.0, + "torusGapRelative": 0.0 + }, + "op": "primitive", + "recogniser": "tier1-torus" + }, + "tube": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "rmax": 2.0, + "rmin": 1.0 + }, + "type": "TGeoTube" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-tube" + }, + "tube segment": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 5.0, + "phi1": 0.0, + "phi2": 75.0, + "rmax": 2.0, + "rmin": 1.0 + }, + "type": "TGeoTubeSeg" + } + ], + "notes": {}, + "op": "primitive", + "recogniser": "tier1-tubeseg" + }, + "tube with a transverse window": { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 3.0, + "rmax": 1.5, + "rmin": 0.0 + }, + "type": "TGeoTube" + }, + { + "frame": { + "origin": [ + 0.0, + 0.0, + 0.0 + ], + "x": [ + -0.0, + 0.0, + 1.0 + ], + "y": [ + 0.0, + -1.0, + 0.0 + ], + "z": [ + 1.0, + 0.0, + 0.0 + ] + }, + "outside": true, + "params": { + "dz": 3.337117388737042, + "rmax": 0.8, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "notes": { + "cellGapCm": 6.761908106961042e-16, + "cellGapRelative": 9.201791007500116e-17, + "concaveEdgesTrusted": 0, + "marginDiagonals": 0.25, + "nCarriers": 4, + "nLeaves": 2, + "nOutside": 1 + }, + "op": "intersection", + "recogniser": "cell-intersection" + }, + "two rods sharing no edge": { + "cells": [ + { + "leaves": [ + { + "frame": { + "origin": [ + 6.0, + 0.0, + 2.5 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 2.5, + "rmax": 1.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "op": "primitive" + }, + { + "leaves": [ + { + "frame": { + "origin": [ + 0.0, + 0.0, + 2.5 + ], + "x": [ + 1.0, + 0.0, + -0.0 + ], + "y": [ + -0.0, + 1.0, + 0.0 + ], + "z": [ + 0.0, + 0.0, + 1.0 + ] + }, + "params": { + "dz": 2.5, + "rmax": 1.0, + "rmin": 0.0 + }, + "type": "TGeoTube" + } + ], + "op": "primitive" + } + ], + "notes": { + "cellGapCm": 0.0, + "cellGapRelative": 0.0, + "cellLeaves": [ + 1, + 1 + ], + "containsScored": 4000, + "marginDiagonals": 0.25, + "nCarriers": 6, + "nCells": 2, + "nComponents": 2, + "nLeaves": 2, + "nOutside": 0, + "nSplits": 0, + "volumeDriftRelative": 0.0 + }, + "op": "unionOfCells", + "recogniser": "cells-union" + } +} diff --git a/Detectors/CADSupport/tools/cadsupport/flat.py b/Detectors/CADSupport/tools/cadsupport/flat.py new file mode 100644 index 0000000000000..25391e86d81c3 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/flat.py @@ -0,0 +1,226 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""The flat-DNF emitter: a cell's carriers as signed implicit halfspaces for `O2FlatCSG`. + +The material side is `sign * Q(x) <= 0`; the sign composes the carrier's own orientation and its +`side`, and an inverted one still gives a solid, so the emitter self-test checks it. A plane is +stored with `2b = n` for a unit outward normal `n`, which keeps `O2FlatCSG`'s accelerated queries +bit-identical to their `_Loop` twins. +""" + +import math +import struct + +SIDECAR_MAGIC = b"O2FLTCSG" +SIDECAR_VERSION = 1 + +# The two `FlatCSGHalfspace::Kind` values, as the sidecar spells them. +KIND_QUADRIC = 0 +KIND_TORUS = 1 + +# One quadric block is ten doubles; the record on file carries eleven, the last unused. +QUADRIC_COEFFICIENTS = 10 +BLOCK_COEFFICIENTS = 11 + + +def _outer(u, v): + return [[u[i] * v[j] for j in range(3)] for i in range(3)] + + +def _quadric(a, b, c): + """Pack A (3x3 symmetric), b (3) and c into the ten-double block the shape stores.""" + return [a[0][0], a[0][1], a[0][2], a[1][1], a[1][2], a[2][2], b[0], b[1], b[2], c] + + +def quadric_from_carrier(carrier): + """`(sign, block)` for a plane, sphere, cylinder or cone carrier; `exterior` flips the sign.""" + from cadsupport import recognise + sign = -1.0 if carrier["side"] == "exterior" else 1.0 + kind = carrier["kind"] + + if kind == "plane": + n = carrier["n"] + p = carrier["p"] + # Q(x) = n.(x - p); the material side of an outward normal is Q <= 0. `2b = n` for a unit + # normal is the convention of design section 3.1 and is not free to vary. + return sign, _quadric([[0.0] * 3 for _ in range(3)], + [0.5 * n[0], 0.5 * n[1], 0.5 * n[2]], + -(n[0] * p[0] + n[1] * p[1] + n[2] * p[2])) + + if kind == "sphere": + p = carrier["p"] + r = carrier["r"] + identity = [[1.0 if i == j else 0.0 for j in range(3)] for i in range(3)] + return sign, _quadric(identity, [-p[0], -p[1], -p[2]], + p[0] * p[0] + p[1] * p[1] + p[2] * p[2] - r * r) + + if kind in ("cylinder", "cone"): + d = carrier["d"] + p = carrier["p"] + r = carrier["r"] + k = 0.0 if kind == "cylinder" else math.tan(carrier["a"]) + scale = 1.0 + k * k + dd = _outer(d, d) + a = [[(1.0 if i == j else 0.0) - scale * dd[i][j] for j in range(3)] for i in range(3)] + ap = [sum(a[i][j] * p[j] for j in range(3)) for i in range(3)] + pd = sum(p[i] * d[i] for i in range(3)) + b = [-ap[i] - r * k * d[i] for i in range(3)] + c = sum(p[i] * ap[i] for i in range(3)) + 2.0 * r * k * pd - r * r + return sign, _quadric(a, b, c) + + raise recognise.Declined(f"a {kind} carrier has no quadric form") + + +def torus_from_carrier(carrier): + """`(sign, centre, axis, major, minor)` for a torus carrier. + + The axis is normalised here, as `O2FlatCSG::AddTorus` does, so both evaluate the same torus. + """ + sign = -1.0 if carrier["side"] == "exterior" else 1.0 + axis = list(carrier["d"]) + length = math.sqrt(sum(v * v for v in axis)) + if length <= 0.0 or not math.isfinite(length): + raise ValueError(f"a torus carrier's axis {tuple(axis)} has no direction") + return (sign, list(carrier["p"]), [v / length for v in axis], carrier["r"], carrier["rt"]) + + +# Below this the tangent of a cone's semi-angle is a cylinder's, and the carrier has no apex. +# `recognise._cell_leaf` uses the same floor and declines there rather than build a leaf. +_APEX_SLOPE_FLOOR = 1.0e-30 + + +def cone_apex(carrier): + """The apex of a cone carrier, or None when its semi-angle is too small for one to exist.""" + k = math.tan(carrier["a"]) + if abs(k) < _APEX_SLOPE_FLOOR: + return None, k + p, d = carrier["p"], carrier["d"] + return tuple(p[i] - (carrier["r"] / k) * d[i] for i in range(3)), k + + +def cone_apex_plane(carrier): + """The plane block an INTERIOR cone carrier needs beside its quadric, or None. + + `sign*Q <= 0` is the double cone; `{rho <= r + k u} == {Q <= 0} n {r + k u >= 0}`, and the + second set is the plane through the apex with unit outward normal `-sign(k) d`. An exterior + cone gets nothing: `check_cell_box` refuses it. + """ + if carrier["kind"] != "cone" or carrier["side"] == "exterior": + return None + apex, k = cone_apex(carrier) + if apex is None: + return None + d = carrier["d"] + n = tuple(-math.copysign(1.0, k) * d[i] for i in range(3)) + return {"kind": "quadric", "sign": 1.0, + "c": _quadric([[0.0] * 3 for _ in range(3)], + [0.5 * n[0], 0.5 * n[1], 0.5 * n[2]], + -(n[0] * apex[0] + n[1] * apex[1] + n[2] * apex[2])) + [0.0]} + + +def check_cell_box(carriers, lo, hi): + """`Declined` when an EXTERIOR cone's quadric carves a mirror cone inside this cell box. + + The caller must pass the same `lo`/`hi` it hands to `O2FlatCSG::SetCellBBox`, per cell. + """ + from cadsupport import recognise + corners = [(x, y, z) for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])] + for carrier in carriers: + if carrier["kind"] != "cone" or carrier["side"] != "exterior": + continue + apex, k = cone_apex(carrier) + if apex is None: + continue + d = carrier["d"] + reach = min(k * sum((corner[i] - apex[i]) * d[i] for i in range(3)) for corner in corners) + if reach < 0.0: + raise recognise.Declined( + "an exterior cone carrier whose cell box reaches past its apex: the quadric's " + "mirror nappe would remove material that is really there") + + +def blocks_from_carriers(carriers): + """The halfspace blocks of one cell, in carrier order, plus an interior cone's apex plane. + + So not one block per carrier: size a cell's `count` with `len(...)` of the result. + """ + blocks = [] + for carrier in carriers: + if carrier["kind"] == "torus": + sign, centre, axis, major, minor = torus_from_carrier(carrier) + blocks.append({"kind": "torus", "sign": sign, + "c": centre + axis + [major, minor, 0.0, 0.0, 0.0]}) + else: + sign, block = quadric_from_carrier(carrier) + blocks.append({"kind": "quadric", "sign": sign, "c": block + [0.0]}) + apex_plane = cone_apex_plane(carrier) + if apex_plane is not None: + blocks.append(apex_plane) + return blocks + + +def eval_block(block, point): + """`sign * f(point)`, the arithmetic of `O2FlatCSG::EvalHalfspace`; `<= 0` means inside.""" + c = block["c"] + x, y, z = point + if block["kind"] == "torus": + offset = (x - c[0], y - c[1], z - c[2]) + along = offset[0] * c[3] + offset[1] * c[4] + offset[2] * c[5] + radial = tuple(offset[i] - along * c[3 + i] for i in range(3)) + rho = math.sqrt(sum(v * v for v in radial)) + return block["sign"] * (math.hypot(rho - c[6], along) - c[7]) + quadratic = (c[0] * x * x + c[3] * y * y + c[5] * z * z + + 2.0 * (c[1] * x * y + c[2] * x * z + c[4] * y * z)) + return block["sign"] * (quadratic + 2.0 * (c[6] * x + c[7] * y + c[8] * z) + c[9]) + + +def flat_contains(blocks, point): + """True when every block contains the point: one cell's membership test.""" + return all(eval_block(block, point) <= 0.0 for block in blocks) + + +def plane_scaling_error(block): + """`| |2b| - 1 |` for a plane block (the `2b = n` convention), or None for another block.""" + if block["kind"] != "quadric": + return None + c = block["c"] + if any(c[index] != 0.0 for index in range(6)): + return None + two_b = math.sqrt(4.0 * (c[6] * c[6] + c[7] * c[7] + c[8] * c[8])) + return abs(two_b - 1.0) + + +def write_sidecar(path, blocks, cells): + """Write the version-1 flat-CSG sidecar, byte-compatible with `WriteFlatCSG`. + + Field by field, never as a struct: the record packs at 100 bytes, not the 104-byte C++ layout. + """ + with open(path, "wb") as handle: + handle.write(SIDECAR_MAGIC) + handle.write(struct.pack(" BLOCK_COEFFICIENTS: + raise ValueError(f"a halfspace block carries {len(coefficients)} coefficients, " + f"more than the {BLOCK_COEFFICIENTS} the sidecar has room for") + coefficients += [0.0] * (BLOCK_COEFFICIENTS - len(coefficients)) + handle.write(struct.pack("<11d", *coefficients)) + for cell in cells: + handle.write(struct.pack(" +# Since: 2026-08 + +"""The converter's single CSG integration point, `recognise_and_emit()`. + +With `--csg auto` each part ships as CSG, else exact surfaces, else tessellated; the other +representations are still written for the gate. `shape__.root` is written only where +PyROOT imports, and `geom.C` never references a file that was not written. +""" + +import json +import sys +from pathlib import Path + +from cadsupport import emit, planar, primitives as prim, recognise # noqa: E402 + + +def have_root(): + try: + import ROOT # noqa: F401 + return True + except Exception: # noqa: BLE001 + return False + + +def scaled_to_cm(shape, scale_to_cm): + """A copy of `shape` scaled to cm, the frame and units of the sidecar, mesh, `.brep` and oracle.""" + if scale_to_cm == 1.0: + return shape + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCC.Core.gp import gp_Pnt, gp_Trsf + trsf = gp_Trsf() + trsf.SetScale(gp_Pnt(0.0, 0.0, 0.0), scale_to_cm) + return BRepBuilderAPI_Transform(shape, trsf, True).Shape() + + +def recognise_and_emit(def_shapes, def_names, scale_to_cm, out_folder, sanitize_filename, + mode="auto", band_factor=1.0, verbose=True, scaled=None): + """Recognise every leaf solid; emit what both acceptance tests admit. + + Returns `(csg_files, flat_files, records)`: lid -> `shape_*.root`, lid -> `flatcsg_*.bin` for + `O2FlatCSG` parts (a part is in exactly one map), and the per-part evidence. `scaled` maps a + lid to the cm copy the caller already made. + """ + out_folder = Path(out_folder) + root_available = have_root() + csg_files = {} + flat_files = {} + records = [] + for lid, shape in def_shapes.items(): + display = def_names.get(lid, "") + volname = sanitize_filename(display) if display else "vol" + suffix = f"{volname}_{sanitize_filename(lid)}" + solid = scaled[lid] if scaled and lid in scaled else scaled_to_cm(shape, scale_to_cm) + cache = {} + record = emit.process_solid(solid, suffix, band_factor=band_factor, cache=cache) + record["lid"] = lid + record["volume"] = display + # The placement is derived from the description alone (no ROOT needed), so the deferred + # `--from-json` path and this one cannot disagree about it. None means identity. + record["placement"] = (prim.placement_for_candidate(record["candidate"]) + if record["candidate"] else None) + (out_folder / f"csg_{suffix}.json").write_text(json.dumps( + {"part": suffix, "lid": lid, "candidate": record["candidate"], + "acceptance": record["acceptance"], "recogniser": record["recogniser"], + "placement": record["placement"]}, indent=1)) + if record["accepted"]: + is_flat = record["candidate"]["op"] == "flatCells" + if root_available: + # Built once and checked before any file is written. + built = prim.build_root(record["candidate"], "shape") + record["twinParity"] = emit.twin_parity(built[0]) + record["bboxRootVsOcctCm"] = emit.crosscheck_bbox( + record["candidate"], occ_shape=recognise.realised_for(cache, record["candidate"]), + built=built) + record["containsCrosscheck"] = emit.crosscheck_contains( + record["candidate"], solid, built=built) + # Either twin sampling refuses the part: a cell reaches past its declared box. + parity = record["twinParity"] + cross_twin = (record["containsCrosscheck"] or {}).get("twinDisagreements") + if parity is not None and parity["disagreements"]: + record["accepted"] = False + record["reason"] = emit.twin_decline_reason(parity) + elif cross_twin: + record["accepted"] = False + record["reason"] = emit.twin_decline_reason( + {"disagreements": cross_twin, + "points": record["containsCrosscheck"]["points"]}) + if not record["accepted"]: + record["shape"] = None + record["flatSidecar"] = None + else: + if is_flat: + # Written only here: a deferred part must not advertise a sidecar. + record["flatSidecar"] = write_flat_sidecar( + record["candidate"], out_folder, suffix) + target = (out_folder / f"shape_{suffix}.root").resolve() + emit.write_shape_object(built[0], built[1], target) + record["shape"] = str(target) + if is_flat: + flat_files[lid] = record["flatSidecar"] + else: + csg_files[lid] = str(target) + else: + record["shape"] = None + record["shapeDeferred"] = True + # Name the real cause: the environment, not the geometry. + record["reason"] = ("csg deferred: ROOT unavailable in this interpreter; the " + f"accepted candidate is in csg_{suffix}.json -- run " + "`python3 -m cadsupport.emit --from-json ` from the directory holding the " + "cadsupport package to complete it") + records.append(record) + if verbose: + emit._print_record(record) + if record.get("shapeDeferred"): + print(f" [WARN] {display or lid}: accepted as CSG but NOT emitted -- " + "ROOT unavailable; geom.C will dispatch this part one tier down") + + n_csg = sum(1 for r in records if r["accepted"]) + if verbose: + print(f"CSG recognition ({mode}): {n_csg}/{len(records)} leaf solid(s) accepted as native " + f"ROOT shapes ({len(csg_files) + len(flat_files)} written, of which " + f"{len(flat_files)} as flat halfspace solids)") + if n_csg and not root_available: + n_deferred = sum(1 for r in records if r.get("shapeDeferred")) + print(f" [WARN] PyROOT is not importable in this interpreter: {n_deferred} accepted " + "CSG part(s) were NOT emitted and geom.C dispatches them one tier down. " + "csg_report.json records each as 'csg deferred: ROOT unavailable'. Run " + "`python3 -m cadsupport.emit --from-json ` from the directory holding the cadsupport " + "package, under the O2 environment, then " + "reconvert (or re-run the gate), to ship them as CSG.") + if mode == "required": + failed = [r for r in records if not r["accepted"]] + if failed: + lines = [f"--csg required: {len(failed)}/{len(records)} leaf solid(s) are not CSG:"] + for r in failed: + lines.append(f" {r['volume'] or r['lid']}: {r['reason']}") + raise ValueError("\n".join(lines)) + return csg_files, flat_files, records + + +def write_flat_sidecar(cand, out_folder, suffix): + """Write `flatcsg_.bin` for a `flatCells` candidate; returns its absolute path.""" + from cadsupport import flat + target = (Path(out_folder) / f"flatcsg_{suffix}.bin").resolve() + blocks, cells = prim.flat_sidecar_records(cand) + flat.write_sidecar(target, blocks, cells) + return str(target) + + +def write_report(records, path, surface_lids, facet_lids): + """The per-part cascade report: which representation carries each part, and on what evidence. + + Each row also records `tessellationExact` (`cadsupport/planar.py`). `surface_lids` is a set of + lids or the lid -> sidecar mapping; only the mapping lets that exactness be computed. + """ + surface_paths = surface_lids if isinstance(surface_lids, dict) else {} + rows = [] + tiers = {"csg": 0, "surface": 0, "mesh": 0} + exactness = {"exact": 0, "approximate": 0, "unknown": 0} + for record in records: + lid = record["lid"] + if record["accepted"] and record.get("shape"): + tier = "csg" + why_not_csg = None + evidence = { + "recogniser": record["recogniser"], + "description": record["description"], + "symmetricDifferenceCm3": record["acceptance"]["symmetricDifference"], + "bandCm3": record["acceptance"]["band"], + "relativeToVolume": record["acceptance"]["relativeToVolume"], + "rootVsCadContains": record.get("containsCrosscheck"), + } + elif lid in surface_lids: + tier = "surface" + why_not_csg = record["reason"] + evidence = {"declinedCsgBecause": record["reason"]} + else: + tier = "mesh" + why_not_csg = record["reason"] + evidence = {"declinedCsgBecause": record["reason"]} + tiers[tier] += 1 + sidecar = surface_paths.get(lid) + if sidecar: + mesh_exact, mesh_reason, mesh_census = planar.tessellation_is_exact(sidecar) + else: + mesh_exact, mesh_reason, mesh_census = None, "no exact sidecar for this part", None + exactness["exact" if mesh_exact else + ("approximate" if mesh_exact is False else "unknown")] += 1 + # `part` is the artifact stem, which joins this row to manifest.json and gate.json. + rows.append({"lid": lid, "part": record.get("part"), "volume": record["volume"], + "representation": tier, "shapeFile": record.get("shape"), + # The flatcsg_*.bin an O2FlatCSG part ships with; null otherwise. + "flatSidecar": record.get("flatSidecar"), + # Brief decline reason; None when the part ships as CSG. + "whyNotCSG": why_not_csg, + "shapeDeferred": bool(record.get("shapeDeferred", False)), + # [R | t] from the shape frame to the part frame (3x4 row-major), or null. + "shapePlacement": record.get("placement"), + # Whether the mesh IS the exact surface solid; null without a sidecar. + "tessellationExact": mesh_exact, + "tessellationExactWhy": mesh_reason, + "surfaceCensus": mesh_census, + "evidence": evidence}) + report = {"tiers": tiers, "tessellationExactness": exactness, + "nLeafSolids": len(records), "parts": rows} + Path(path).write_text(json.dumps(report, indent=1)) + return report + + +def print_tier_table(report): + print("\n=== REPRESENTATION CASCADE (per leaf solid) ===") + print(f" {'volume':<28} {'carried by':<10} evidence") + for row in report["parts"]: + ev = row["evidence"] + if row["representation"] == "csg": + detail = (f"{ev['description']} [{ev['recogniser']}], dV_sym=" + f"{ev['symmetricDifferenceCm3']:.3g} cm^3 (band {ev['bandCm3']:.3g})") + else: + detail = f"declined CSG: {ev['declinedCsgBecause']}" + print(f" {(row['volume'] or row['lid'])[:28]:<28} {row['representation']:<10} {detail}") + exact = report.get("tessellationExactness") or {} + if exact.get("exact"): + total = sum(exact.values()) or 1 + print(f" tessellation is EXACT (every face a planar polygon) for {exact['exact']} of " + f"{total} part(s) -- {100.0 * exact['exact'] / total:.1f} %; for those the mesh is " + f"not an approximation of the part, it is the part") + tiers = report["tiers"] + print(f" tiers: CSG {tiers['csg']}, exact surfaces {tiers['surface']}, " + f"tessellated {tiers['mesh']} (of {report['nLeafSolids']} leaf solids)") + + +def csg_placement_var(lid, sanitize_cpp_name): + """The macro variable holding a CSG part's shape placement. One namer, two call sites.""" + return f"shapePlace_{sanitize_cpp_name(lid)}" + + +def emit_csg_shape_cpp(lid, vol_display_name, shape_abspath, medium_var, sanitize_cpp_name): + """geom.C branch for a CSG part: load the TGeoShape and its placement from its own file.""" + safe = sanitize_cpp_name(lid) + shape_name = vol_display_name if vol_display_name else lid + return "\n".join([ + f' TGeoShape *solid_{safe} = LoadShape("{shape_abspath}", "{shape_name}");', + f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});', + f' TGeoHMatrix *{csg_placement_var(lid, sanitize_cpp_name)} = ' + f'LoadShapePlacement("{shape_abspath}");', + ]) + + +def emit_csg_composed_placement_cpp(matrix_var, placement_var, composed_var): + """`composed = partPlacement * shapePlacement`, in that order. + + A point goes shape -> part -> parent; `TGeoHMatrix::Multiply(right)` is `this = this * right`, + so the part placement is copied and the shape placement is the right operand. + """ + return "\n".join([ + f" TGeoHMatrix *{composed_var} = new TGeoHMatrix(*{matrix_var});", + f" {composed_var}->Multiply({placement_var});", + ]) + + +def emit_flat_csg_shape_cpp(lid, vol_display_name, sidecar_abspath, medium_var, + sanitize_cpp_name): + """geom.C branch for an `O2FlatCSG` part: construct, load the sidecar, close. + + A sidecar that fails to load is fatal: a geometry that cannot be built must stop the job. + """ + safe = sanitize_cpp_name(lid) + shape_name = vol_display_name if vol_display_name else lid + return "\n".join([ + f' auto *solid_{safe} = new o2::cad::O2FlatCSG("{shape_name}");', + f' if (!o2::cad::LoadFlatCSG("{sidecar_abspath}", *solid_{safe})) {{', + f' ::Fatal("geom", "flat-CSG sidecar for {shape_name} failed to load: ' + f'{sidecar_abspath}");', + ' }', + f' solid_{safe}->CloseShape();', + f' if (!solid_{safe}->IsClosed()) {{', + f' ::Fatal("geom", "flat-CSG shape {shape_name} refused to close; see the Error above");', + ' }', + f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});', + ]) + + +FLAT_CPP_PRELUDE = r''' +// --- flat-CSG parts: o2::cad::O2FlatCSG filled from a flatcsg_*.bin sidecar --- +// Both headers are included, never declared by prototype: loadCADGeometryHook JITs this +// macro inside a unique namespace and hoists only '#' lines to global scope, so a +// `namespace o2 { namespace cad {` block here becomes `::o2::cad` and shadows +// the real one -- every later o2::cad:: name then fails to resolve and the module +// silently does not load. O2SurfaceSolidIO.h declares LoadFlatCSG and LoadSurfaceSolid both. +R__ADD_INCLUDE_PATH($O2_ROOT/include) +R__LOAD_LIBRARY(libO2CADSupport) +#include "CADSupport/O2FlatCSG.h" +#include "CADSupport/O2SurfaceSolidIO.h" +#include +''' + + +CPP_LOADER = r''' +// --- CSG parts: one ROOT-serialised TGeoShape per part, written by Detectors/CADSupport/tools/cadsupport --- +// The file holds exactly one object inheriting from TGeoShape under the key "shape", in cm; and +// optionally a TGeoHMatrix under the key "placement", the rigid transform from the shape's own +// canonical frame into the part's local frame. No "placement" key means the identity, which is +// what every file written before that change means (see O2SolidHarness.h, next to the C++ loader +// that reads the same convention). +TGeoHMatrix* LoadShapePlacement(const char* path) { + TFile* f = TFile::Open(path, "READ"); + if (!f || f->IsZombie()) { + throw std::runtime_error(std::string("cannot open CSG shape file: ") + path); + } + auto* stored = dynamic_cast(f->Get("placement")); + // Identity when the file records none. Returning a matrix rather than a null pointer keeps the + // composition below unconditional, so the placed and unplaced cases go down one code path. + auto* placement = stored ? new TGeoHMatrix(*stored) : new TGeoHMatrix("identity"); + f->Close(); + delete f; + return placement; +} + +TGeoShape* LoadShape(const char* path, const char* name) { + TFile* f = TFile::Open(path, "READ"); + if (!f || f->IsZombie()) { + throw std::runtime_error(std::string("cannot open CSG shape file: ") + path); + } + auto* shape = dynamic_cast(f->Get("shape")); + if (!shape) { + delete f; + throw std::runtime_error(std::string("no TGeoShape under key \"shape\" in ") + path); + } + // The shape registers itself with gGeoManager on construction and is owned by it; the file can + // go away. + shape->SetName(name); + f->Close(); + delete f; + return shape; +} +''' diff --git a/Detectors/CADSupport/tools/cadsupport/occ_env.py b/Detectors/CADSupport/tools/cadsupport/occ_env.py new file mode 100644 index 0000000000000..abacd8b327e63 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/occ_env.py @@ -0,0 +1,108 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Make `import OCC` work regardless of which interpreter started us. + +If OCC is not importable, `ensure_occ()` re-executes the current script, or the module started +with `python3 -m`, under the aliBuild Python that pythonOCC is built against. Call it before any +`from OCC...` import. +""" + +import os +import sys +from pathlib import Path + +UNRESOLVED = ("cannot locate the aliBuild area that holds pythonOCC: set ALIBUILD_ARCH_ROOT to " + "/, or load the O2 environment, or set ALIBUILD_WORK_DIR") + +_GUARD = "O2_CSG_OCC_REEXEC" + + +def arch_root(): + """The aliBuild / directory, or None when it cannot be found. + + ALIBUILD_ARCH_ROOT wins; otherwise the directory two levels above O2_ROOT, or the one + architecture under ALIBUILD_WORK_DIR, whichever has pythonOCC installed. Several architectures + under ALIBUILD_WORK_DIR with no O2_ROOT candidate is an error, not a guess. + """ + if os.environ.get("ALIBUILD_ARCH_ROOT"): + return Path(os.environ["ALIBUILD_ARCH_ROOT"]) + candidates = [] + o2_root = Path(os.environ.get("O2_ROOT", "")).resolve() + if os.environ.get("O2_ROOT") and len(o2_root.parents) > 1: + candidates.append(o2_root.parents[1]) + if os.environ.get("ALIBUILD_WORK_DIR"): + work = [p.parents[1] for p in Path(os.environ["ALIBUILD_WORK_DIR"]).glob("*/pythonOCC/latest")] + if len(work) > 1 and not candidates: + raise SystemExit("several aliBuild architectures hold pythonOCC (" + + ", ".join(sorted(p.name for p in work)) + + "); set ALIBUILD_ARCH_ROOT to choose one") + candidates += sorted(work) + for candidate in candidates: + if (candidate / "pythonOCC/latest").exists(): + return candidate + return None + + +def occ_python(): + """The Python 3.10 pythonOCC is built against, or None.""" + sw = arch_root() + return None if sw is None else sw / "Python/latest/bin/python3.10" + + +def occ_env_prefix(): + """The PYTHONPATH and LD_LIBRARY_PATH entries that make OCC importable, or None.""" + sw = arch_root() + if sw is None: + return None + return { + "PYTHONPATH": f"{sw}/pythonOCC/latest/lib/python3.10/site-packages:" + f"{sw}/Python-modules/latest/lib/python3.10/site-packages", + "LD_LIBRARY_PATH": f"{sw}/OCCT/latest/lib:{sw}/Python/latest/lib", + } + + +def have_occ() -> bool: + try: + import OCC # noqa: F401 + return True + except Exception: + return False + + +def ensure_occ() -> None: + """Re-exec this process under the pythonOCC interpreter if OCC is not importable. + + The pythonOCC paths are prepended to the inherited ones, so a process started from an O2 + shell can import both OCC and ROOT. + """ + if have_occ(): + return + python = occ_python() + if python is None: + raise SystemExit(f"OCC is not importable here, and {UNRESOLVED}") + if os.environ.get(_GUARD): + raise SystemExit(f"cannot import OCC even under {python}; check the pythonOCC installation") + if not python.exists(): + raise SystemExit(f"pythonOCC interpreter not found: {python}") + env = dict(os.environ) + for key, prefix in occ_env_prefix().items(): + existing = env.get(key, "") + env[key] = prefix + (":" + existing if existing else "") + env[_GUARD] = "1" + spec = getattr(sys.modules.get("__main__"), "__spec__", None) + if spec is not None and spec.name: + argv = [str(python), "-m", spec.name] # started as `python3 -m`; the working directory is kept + else: + argv = [str(python), str(Path(sys.argv[0]).resolve())] + os.execve(str(python), argv + sys.argv[1:], env) diff --git a/Detectors/CADSupport/tools/cadsupport/planar.py b/Detectors/CADSupport/tools/cadsupport/planar.py new file mode 100644 index 0000000000000..99b25d0d35660 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/planar.py @@ -0,0 +1,120 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Is a part's tessellation the same solid as its exact surfaces? + +It is exactly when every face is a planar polygon with no arc or B-spline edge, the condition under +which `LoadSurfaceSolid` builds only `PlanarPolygon` records. It measures; it does not route. +""" + +import struct + +SIDECAR_MAGIC = b"O2SS" +SIDECAR_VERSION_MIN = 1 +SIDECAR_VERSION_MAX = 3 + +# The sidecar's own surface-type numbering (not BVHSurfaceRecord::Kind, which is decided on read). +TYPE_NAME = {1: "plane", 2: "cylinder", 3: "cone", 4: "sphere", 5: "torus"} +TYPE_PLANE = 1 + +# Curve types in a wire edge record. Anything that is not a line segment makes a plane curved. +CURVE_LINE = 0 + + +class _Cursor: + def __init__(self, data): + self.data = data + self.offset = 0 + + def u32(self): + value = struct.unpack_from("': n}` for one `surfaces_*.bin`. + + Raises `ValueError` on a file it cannot read, so "not exact" and "could not tell" stay apart. + """ + with open(path, "rb") as handle: + data = handle.read() + if len(data) < 16 or data[:4] != SIDECAR_MAGIC: + raise ValueError(f"{path} is not a surface sidecar (bad magic)") + cursor = _Cursor(data) + cursor.offset = 4 + version = cursor.u32() + n_surfaces = cursor.u32() + cursor.u32() # reserved + if not SIDECAR_VERSION_MIN <= version <= SIDECAR_VERSION_MAX: + raise ValueError(f"{path}: unsupported sidecar version {version}") + if version >= 2: + cursor.f64() # model tolerance + if version >= 3: + cursor.u32() # nModelEdges + + counts = {} + for _ in range(n_surfaces): + surface_type = cursor.u32() + cursor.u32() # flags + cursor.skip_doubles(cursor.u32()) # params + straight = True + for _ in range(cursor.u32()): # wires + cursor.u32() # role + for _ in range(cursor.u32()): # edges + if cursor.u32() != CURVE_LINE: + straight = False + cursor.skip_doubles(cursor.u32()) # curve params + if version >= 3: + for _ in range(cursor.u32()): # edge identities + cursor.u32() + cursor.u8() + if surface_type == TYPE_PLANE: + key = "planarPolygon" if straight else "curvedPlanar" + else: + key = TYPE_NAME.get(surface_type, f"unknown{surface_type}") + counts[key] = counts.get(key, 0) + 1 + return counts + + +def tessellation_is_exact(path): + """`(exact, reason, census)` for one part's sidecar; `(None, why, None)` when it cannot be read.""" + try: + counts = surface_census(path) + except Exception as error: # a file we cannot read is not a verdict + return None, str(error), None + if not counts: + return None, "the sidecar carries no surfaces", counts + planar = counts.get("planarPolygon", 0) + if planar == sum(counts.values()): + return True, (f"all {planar} faces are planar polygons, so triangulating them loses " + "nothing"), counts + curved = {k: v for k, v in counts.items() if k not in ("planarPolygon",)} + if list(curved) == ["curvedPlanar"]: + return False, (f"{curved['curvedPlanar']} face(s) are flat but have a curved boundary (an " + "arc or a spline), so the face is exact and its outline is not"), counts + named = ", ".join(f"{v} {k}" for k, v in sorted(curved.items(), key=lambda kv: -kv[1])) + return False, f"{named} face(s) are not planar polygons", counts diff --git a/Detectors/CADSupport/tools/cadsupport/primitives.py b/Detectors/CADSupport/tools/cadsupport/primitives.py new file mode 100644 index 0000000000000..66edd9c7e7967 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/primitives.py @@ -0,0 +1,1236 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""The intermediate CSG description, and the two builders that realise it. + +A recognised part is a JSON-serialisable tree of placed primitives, realised twice from the same +description: `build_occ()` gives the OCCT solid the symmetric difference measures, `build_root()` +the `TGeoShape` the oracle gate scores. Each leaf carries a frame `(origin, x, y, z)` in the part +frame, in cm, with `z` as the primitive's axis. + +`build_root()` returns `(shape, placement)`: the shape in its own canonical frame and a 3x4 +row-major `[R | t]` with `part = R * canonical + t`, or None for the identity. `build_occ()` +builds the solid in the part frame. +""" + +import math + +# Frames closer than this are the same; the identity fast path needs an exact rotation. +_IDENTITY_EPS = 1.0e-12 + +# Below this relative difference a cone's two radii are the same radius, and OCCT wants a +# cylinder rather than a cone. See `_occ_frustum`. +_CONE_DEGENERATE_EPS = 1.0e-12 + + +def identity_frame(origin=(0.0, 0.0, 0.0)): + return {"origin": [float(c) for c in origin], + "x": [1.0, 0.0, 0.0], "y": [0.0, 1.0, 0.0], "z": [0.0, 0.0, 1.0]} + + +def frame_from_axis(origin, axis_z, ref_x=None): + """An orthonormal right-handed frame with `z` along `axis_z`, `x` along `ref_x` if given.""" + z = _unit(axis_z) + if ref_x is not None: + x = _sub(ref_x, _scale(z, _dot(ref_x, z))) + if _norm(x) < 1.0e-9: + x = None + else: + x = _unit(x) + else: + x = None + if x is None: + # any vector not parallel to z + seed = (1.0, 0.0, 0.0) if abs(z[0]) < 0.9 else (0.0, 1.0, 0.0) + x = _unit(_sub(seed, _scale(z, _dot(seed, z)))) + y = _cross(z, x) + return {"origin": [float(c) for c in origin], "x": list(x), "y": list(y), "z": list(z)} + + +def frame_is_identity_rotation(frame): + return (abs(frame["x"][0] - 1.0) < _IDENTITY_EPS and abs(frame["x"][1]) < _IDENTITY_EPS + and abs(frame["x"][2]) < _IDENTITY_EPS and abs(frame["y"][1] - 1.0) < _IDENTITY_EPS + and abs(frame["y"][0]) < _IDENTITY_EPS and abs(frame["y"][2]) < _IDENTITY_EPS + and abs(frame["z"][2] - 1.0) < _IDENTITY_EPS and abs(frame["z"][0]) < _IDENTITY_EPS + and abs(frame["z"][1]) < _IDENTITY_EPS) + + +def frame_is_identity(frame): + return frame_is_identity_rotation(frame) and all(abs(c) < _IDENTITY_EPS + for c in frame["origin"]) + + +# ------------------------------------------------------------------------------------------ +# tiny vector helpers +# ------------------------------------------------------------------------------------------ + +def _dot(a, b): + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2] + + +def _sub(a, b): + return (a[0] - b[0], a[1] - b[1], a[2] - b[2]) + + +def _add(a, b): + return (a[0] + b[0], a[1] + b[1], a[2] + b[2]) + + +def _scale(a, s): + return (a[0] * s, a[1] * s, a[2] * s) + + +def _cross(a, b): + return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) + + +def _norm(a): + return math.sqrt(_dot(a, a)) + + +def _unit(a): + n = _norm(a) + if n == 0.0: + raise ValueError("cannot normalise a zero vector") + return (a[0] / n, a[1] / n, a[2] / n) + + +# ------------------------------------------------------------------------------------------ +# the description +# ------------------------------------------------------------------------------------------ + +LEAF_TYPES = ("TGeoBBox", "TGeoTube", "TGeoTubeSeg", "TGeoCone", "TGeoSphere", "TGeoPcon", + "TGeoTrd1", "TGeoTrd2", "TGeoArb8", "TGeoXtru", "TGeoPgon", "TGeoTorus", "TGeoEltu") + +_REQUIRED_PARAMS = { + "TGeoBBox": ("dx", "dy", "dz"), + "TGeoTube": ("rmin", "rmax", "dz"), + "TGeoTubeSeg": ("rmin", "rmax", "dz", "phi1", "phi2"), + "TGeoCone": ("dz", "rmin1", "rmax1", "rmin2", "rmax2"), + "TGeoSphere": ("rmin", "rmax"), + "TGeoPcon": ("phi1", "dphi"), + "TGeoTorus": ("r", "rmin", "rmax", "phi1", "dphi"), + "TGeoEltu": ("a", "b", "dz"), + "TGeoTrd1": ("dx1", "dx2", "dy", "dz"), + "TGeoTrd2": ("dx1", "dx2", "dy1", "dy2", "dz"), + "TGeoArb8": ("dz",), + "TGeoXtru": (), + "TGeoPgon": ("phi1", "dphi", "nedges"), +} + +# Array parameters per leaf type, as lists; by default all of one leaf's arrays share a length. +_REQUIRED_ARRAY_PARAMS = { + "TGeoPcon": ("z", "rmin", "rmax"), + "TGeoPgon": ("z", "rmin", "rmax"), + "TGeoArb8": ("vertices",), + "TGeoXtru": ("x", "y", "z", "xoff", "yoff", "scale"), +} + +_MIN_ARRAY_LENGTH = { + "TGeoPcon": 2, + "TGeoPgon": 2, + "TGeoArb8": 16, +} + +# A TGeoXtru's polygon and section counts are independent: groups of arrays, each with a minimum. +_ARRAY_LENGTH_GROUPS = { + "TGeoXtru": ((("x", "y"), 3), (("z", "xoff", "yoff", "scale"), 2)), +} + + +class InvalidDescription(ValueError): + """The numbers do not describe a legal solid of that class, so a recogniser declines. + + A missing parameter or an unknown leaf type is a caller bug and stays a plain `ValueError`. + """ + + +def _validate_eltu(p): + for key in ("a", "b", "dz"): + if p[key] <= 0.0: + raise InvalidDescription(f"TGeoEltu: {key} = {p[key]} is not positive") + + +def _validate_torus(p): + if p["r"] <= 0.0: + raise InvalidDescription(f"TGeoTorus: the major radius {p['r']} is not positive") + if p["rmax"] <= 0.0: + raise InvalidDescription(f"TGeoTorus: rmax {p['rmax']} is not positive") + if p["rmin"] < 0.0: + raise InvalidDescription(f"TGeoTorus: rmin {p['rmin']} is negative") + if p["rmin"] >= p["rmax"]: + raise InvalidDescription(f"TGeoTorus: rmin {p['rmin']} is not below rmax {p['rmax']}") + if p["rmax"] > p["r"]: + # A tube radius above the major radius is a self-intersecting torus: refused. + raise InvalidDescription( + f"TGeoTorus: rmax {p['rmax']} exceeds the major radius {p['r']}, so this is a " + "self-intersecting torus (a fillet blend) that TGeoTorus cannot state") + if not 0.0 < p["dphi"] <= 360.0 + 1.0e-9: + raise InvalidDescription(f"TGeoTorus: dphi {p['dphi']} is not in (0, 360]") + + +def _validate_pcon(p): + if not 0.0 < p["dphi"] <= 360.0 + 1.0e-9: + raise InvalidDescription(f"TGeoPcon: dphi {p['dphi']} is not in (0, 360]") + z, rmin, rmax = p["z"], p["rmin"], p["rmax"] + for i in range(len(z)): + if rmin[i] < 0.0: + raise InvalidDescription(f"TGeoPcon: rmin[{i}] = {rmin[i]} is negative") + if rmin[i] > rmax[i]: + raise InvalidDescription( + f"TGeoPcon: rmin[{i}] = {rmin[i]} exceeds rmax[{i}] = {rmax[i]}") + for i in range(1, len(z)): + if z[i] < z[i - 1]: + raise InvalidDescription(f"TGeoPcon: z is not non-decreasing at section {i} " + f"({z[i]} < {z[i - 1]})") + if z[-1] <= z[0]: + raise InvalidDescription("TGeoPcon: the profile has no axial extent") + for i in range(2, len(z)): + if z[i] == z[i - 1] == z[i - 2]: + raise InvalidDescription(f"TGeoPcon: three sections share z = {z[i]}") + + +def _validate_pgon(p): + _validate_pcon(p) + if p["nedges"] < 1 or abs(p["nedges"] - round(p["nedges"])) > 1.0e-9: + raise InvalidDescription(f"TGeoPgon: nedges {p['nedges']} is not a positive whole number") + + +def _validate_trd1(p): + if p["dy"] <= 0.0 or p["dz"] <= 0.0: + raise InvalidDescription(f"TGeoTrd1: dy {p['dy']} and dz {p['dz']} must both be positive") + if min(p["dx1"], p["dx2"]) < 0.0 or max(p["dx1"], p["dx2"]) <= 0.0: + raise InvalidDescription(f"TGeoTrd1: dx1 {p['dx1']}, dx2 {p['dx2']} do not bound a solid") + + +def _validate_trd2(p): + if p["dz"] <= 0.0: + raise InvalidDescription(f"TGeoTrd2: dz {p['dz']} must be positive") + for a, b in (("dx1", "dx2"), ("dy1", "dy2")): + if min(p[a], p[b]) < 0.0 or max(p[a], p[b]) <= 0.0: + raise InvalidDescription(f"TGeoTrd2: {a} {p[a]}, {b} {p[b]} do not bound a solid") + + +def _validate_arb8(p): + if p["dz"] <= 0.0: + raise InvalidDescription(f"TGeoArb8: dz {p['dz']} must be positive") + if len(p["vertices"]) != 16: + raise InvalidDescription(f"TGeoArb8: needs 16 vertex coordinates, got {len(p['vertices'])}") + for half, name in ((p["vertices"][:8], "-dz"), (p["vertices"][8:], "+dz")): + corners = [(half[2 * i], half[2 * i + 1]) for i in range(4)] + if len({(round(c[0], 12), round(c[1], 12)) for c in corners}) < 3: + raise InvalidDescription( + f"TGeoArb8: the {name} face has fewer than three distinct corners") + + +def _validate_xtru(p): + z, scale = p["z"], p["scale"] + for i in range(1, len(z)): + if z[i] <= z[i - 1]: + raise InvalidDescription(f"TGeoXtru: z is not strictly increasing at section {i} " + f"({z[i]} <= {z[i - 1]})") + for i, s in enumerate(scale): + if s <= 0.0: + raise InvalidDescription(f"TGeoXtru: scale[{i}] = {s} is not positive") + corners = {(round(a, 12), round(b, 12)) for a, b in zip(p["x"], p["y"])} + if len(corners) != len(p["x"]): + raise InvalidDescription("TGeoXtru: the polygon repeats a corner") + + +_LEAF_VALIDATORS = { + "TGeoEltu": _validate_eltu, + "TGeoTorus": _validate_torus, + "TGeoPcon": _validate_pcon, + "TGeoPgon": _validate_pgon, + "TGeoTrd1": _validate_trd1, + "TGeoTrd2": _validate_trd2, + "TGeoArb8": _validate_arb8, + "TGeoXtru": _validate_xtru, +} + + +def leaf(kind, params, frame, outside=False): + """One placed primitive. `outside` marks a halfspace whose material is *outside* it. + + ROOT writes such a leaf as a `TGeoSubtraction` and OCCT as a `BRepAlgoAPI_Cut`. + """ + if kind not in LEAF_TYPES: + raise ValueError(f"unknown leaf type {kind!r}") + arrays = _REQUIRED_ARRAY_PARAMS.get(kind, ()) + missing = [k for k in _REQUIRED_PARAMS[kind] + arrays if k not in params] + if missing: + raise ValueError(f"{kind}: missing parameter(s) {missing}") + out = {k: float(params[k]) for k in _REQUIRED_PARAMS[kind]} + for k in arrays: + out[k] = [float(v) for v in params[k]] + if arrays: + groups = _ARRAY_LENGTH_GROUPS.get(kind, + ((arrays, _MIN_ARRAY_LENGTH.get(kind, 1)),)) + for names, want in groups: + lengths = {len(out[k]) for k in names} + if len(lengths) != 1: + raise InvalidDescription( + f"{kind}: array parameters {list(names)} have unequal lengths " + + ", ".join(f"{k}={len(out[k])}" for k in names)) + n = lengths.pop() + if n < want: + raise InvalidDescription( + f"{kind}: needs at least {want} of {list(names)}, got {n}") + validator = _LEAF_VALIDATORS.get(kind) + if validator is not None: + validator(out) + described = {"type": kind, "params": out, "frame": frame} + if outside: + # Only written when true, so every leaf recorded before halfspaces existed keeps its + # bytes and the frozen digests of the self-test stay meaningful. + described["outside"] = True + return described + + +def placement_from_frame(frame): + """The frame as a 3x4 row-major `[R | t]`, with `part = R * canonical + t`. + + `R`'s columns are the frame's basis vectors, as in `TGeoRotation::SetMatrix`. + """ + x, y, z, o = frame["x"], frame["y"], frame["z"], frame["origin"] + return [[x[0], y[0], z[0], o[0]], + [x[1], y[1], z[1], o[1]], + [x[2], y[2], z[2], o[2]]] + + +def placement_to_local(placement, point): + """`R^T (p - t)`: a point in the part frame expressed in the shape's own frame.""" + if placement is None: + return tuple(float(c) for c in point) + d = (point[0] - placement[0][3], point[1] - placement[1][3], point[2] - placement[2][3]) + return tuple(sum(placement[r][c] * d[r] for r in range(3)) for c in range(3)) + + +def placement_for_candidate(cand): + """The rigid transform `build_root()` hands back beside the shape, or None for identity. + + It needs no ROOT, so `csg_.json` and `--from-json` agree on the placement. + """ + if cand["op"] != "primitive": + # A genuine multi-leaf boolean is still a TGeoCompositeShape, whose TGeoBoolNode carries + # the leaves' matrices itself; the composite is already in the part frame. + return None + lf = cand["leaves"][0] + frame = lf["frame"] + if frame_is_identity(frame): + return None + if lf_is_box(lf) and frame_is_identity_rotation(frame): + # TGeoBBox carries a pure translation itself, through fOrigin. Leaving it there keeps + # every artefact written for an axis-aligned box byte-identical to before this change. + return None + return placement_from_frame(frame) + + +def candidate(op, leaves, recogniser, notes=None): + """A described solid: `primitive`, `union`, or `intersection` (of halfspaces). + + An intersection folds its leaves left to right and an `outside` leaf subtracts; the first leaf + cannot be one, since an intersection of complements is unbounded. + """ + if op not in ("primitive", "union", "intersection"): + raise ValueError(f"unknown op {op!r}") + if op == "primitive" and len(leaves) != 1: + raise ValueError("op 'primitive' takes exactly one leaf") + if op == "union" and len(leaves) < 2: + raise ValueError("op 'union' takes at least two leaves") + if op == "intersection": + if len(leaves) < 2: + raise ValueError("op 'intersection' takes at least two leaves") + if leaves[0].get("outside"): + raise ValueError("op 'intersection': the first leaf cannot be a complement") + if op != "intersection" and any(lf.get("outside") for lf in leaves): + raise ValueError(f"op {op!r} has no meaning for a complemented leaf") + return {"op": op, "leaves": leaves, "recogniser": recogniser, "notes": notes or {}} + + +CELL_OPS = ("primitive", "intersection") + + +def cell(op, leaves): + """One cell of a two-level DNF: a bare placed primitive, or an intersection of halfspaces. + + Validated as a candidate, then stripped to `{op, leaves}`. + """ + if op not in CELL_OPS: + raise ValueError(f"a cell is {' or '.join(CELL_OPS)}, not {op!r}") + described = candidate(op, leaves, "cell") + return {"op": described["op"], "leaves": described["leaves"]} + + +def union_of_cells(cells, recogniser, notes=None): + """A union of intersection-cells: `{op: "unionOfCells", cells, recogniser, notes}`. + + It has no `leaves` key, so a one-level reader fails loudly; a cell may not itself be a union. + """ + if len(cells) < 2: + raise ValueError("op 'unionOfCells' takes at least two cells; one cell is that cell") + for i, c in enumerate(cells): + if not isinstance(c, dict) or set(c) != {"op", "leaves"}: + raise ValueError(f"cell {i} is not a bare {{op, leaves}} description: " + f"{sorted(c) if isinstance(c, dict) else type(c).__name__}") + if c["op"] not in CELL_OPS: + raise ValueError(f"cell {i} has op {c['op']!r}: a DNF is two levels deep, so a cell " + f"is {' or '.join(CELL_OPS)} and never a union") + cell(c["op"], c["leaves"]) + return {"op": "unionOfCells", "cells": cells, "recogniser": recogniser, "notes": notes or {}} + + +FLAT_CELL_KEYS = ("blocks", "volume", "lo", "hi") + + +def flat_cells(cells, recogniser, notes=None): + """A union of halfspace cells for `O2FlatCSG`: `{op: "flatCells", cells, recogniser, notes}`. + + A cell is `{blocks, volume, lo, hi}`; `lo`/`hi` must be an outer bound of the cell, since + `O2FlatCSG` builds its sub-cell boxes inside it. One cell is legal. `build_occ` folds the + padded cells in `notes["occCells"]`. + """ + if not cells: + raise ValueError("op 'flatCells' takes at least one cell") + for i, c in enumerate(cells): + if not isinstance(c, dict) or set(c) != set(FLAT_CELL_KEYS): + raise ValueError(f"cell {i} is not a bare {{{', '.join(FLAT_CELL_KEYS)}}} " + f"description: " + f"{sorted(c) if isinstance(c, dict) else type(c).__name__}") + if not c["blocks"]: + raise ValueError(f"cell {i} has no halfspace block: an empty intersection is " + "everything, not a cell") + for key in ("lo", "hi"): + if len(c[key]) != 3 or not all(math.isfinite(float(v)) for v in c[key]): + raise ValueError(f"cell {i}'s {key} is not three finite numbers: {c[key]!r}") + for axis in range(3): + if float(c["lo"][axis]) > float(c["hi"][axis]): + raise ValueError(f"cell {i}'s bounding box is inverted on axis {axis}: " + f"{c['lo'][axis]} > {c['hi'][axis]}") + if not (float(c["volume"]) > 0.0): + raise ValueError(f"cell {i} has non-positive volume {c['volume']!r}") + return {"op": "flatCells", "cells": cells, "recogniser": recogniser, "notes": notes or {}} + + +def flat_occ_cells(cand): + """The padded cells `build_occ` folds for a `flatCells` description, kept under `notes`.""" + occ = (cand.get("notes") or {}).get("occCells") + if not occ: + raise ValueError("a flatCells description carries no notes['occCells']: there is nothing " + "to realise it with in OCCT") + return occ + + +def describe(cand): + """One line, for reports.""" + if cand["op"] == "flatCells": + blocks = sum(len(c["blocks"]) for c in cand["cells"]) + return (f"O2FlatCSG({len(cand['cells'])} cell(s), {blocks} halfspace(s))") + if cand["op"] == "unionOfCells": + return " u ".join(f"({describe(c)})" if len(c["leaves"]) > 1 else describe(c) + for c in cand["cells"]) + parts = [] + for lf in cand["leaves"]: + p = lf["params"] + if lf["type"] in ("TGeoTube", "TGeoTubeSeg"): + parts.append(f"{lf['type']}(rmin={p['rmin']:.4g}, rmax={p['rmax']:.4g}, " + f"dz={p['dz']:.4g})") + elif lf["type"] == "TGeoBBox": + parts.append(f"TGeoBBox({p['dx']:.4g}, {p['dy']:.4g}, {p['dz']:.4g})") + elif lf["type"] == "TGeoCone": + parts.append(f"TGeoCone(dz={p['dz']:.4g}, {p['rmin1']:.4g}/{p['rmax1']:.4g} -> " + f"{p['rmin2']:.4g}/{p['rmax2']:.4g})") + elif lf["type"] == "TGeoTrd1": + parts.append(f"TGeoTrd1(dx {p['dx1']:.4g} -> {p['dx2']:.4g}, dy={p['dy']:.4g}, " + f"dz={p['dz']:.4g})") + elif lf["type"] == "TGeoTrd2": + parts.append(f"TGeoTrd2(dx {p['dx1']:.4g} -> {p['dx2']:.4g}, " + f"dy {p['dy1']:.4g} -> {p['dy2']:.4g}, dz={p['dz']:.4g})") + elif lf["type"] == "TGeoArb8": + v = p["vertices"] + parts.append(f"TGeoArb8(dz={p['dz']:.4g}, x {min(v[0::2]):.4g}..{max(v[0::2]):.4g}, " + f"y {min(v[1::2]):.4g}..{max(v[1::2]):.4g})") + elif lf["type"] == "TGeoXtru": + parts.append(f"TGeoXtru(nvert={len(p['x'])}, nz={len(p['z'])}, " + f"z {p['z'][0]:.4g}..{p['z'][-1]:.4g}, " + f"scale {min(p['scale']):.4g}..{max(p['scale']):.4g})") + elif lf["type"] == "TGeoPgon": + parts.append(f"TGeoPgon(nedges={int(round(p['nedges']))}, nz={len(p['z'])}, " + f"phi1={p['phi1']:.4g}, dphi={p['dphi']:.4g}, " + f"z {p['z'][0]:.4g}..{p['z'][-1]:.4g}, " + f"rmin {min(p['rmin']):.4g}..{max(p['rmin']):.4g}, " + f"rmax {min(p['rmax']):.4g}..{max(p['rmax']):.4g})") + elif lf["type"] == "TGeoEltu": + parts.append(f"TGeoEltu(a={p['a']:.4g}, b={p['b']:.4g}, dz={p['dz']:.4g})") + elif lf["type"] == "TGeoTorus": + parts.append(f"TGeoTorus(r={p['r']:.4g}, rmin={p['rmin']:.4g}, " + f"rmax={p['rmax']:.4g}, phi1={p['phi1']:.4g}, dphi={p['dphi']:.4g})") + elif lf["type"] == "TGeoPcon": + parts.append(f"TGeoPcon(nz={len(p['z'])}, phi1={p['phi1']:.4g}, " + f"dphi={p['dphi']:.4g}, z {p['z'][0]:.4g}..{p['z'][-1]:.4g}, " + f"rmin {min(p['rmin']):.4g}..{max(p['rmin']):.4g}, " + f"rmax {min(p['rmax']):.4g}..{max(p['rmax']):.4g})") + else: + parts.append(f"TGeoSphere(rmin={p['rmin']:.4g}, rmax={p['rmax']:.4g})") + if cand["op"] == "union": + return " u ".join(parts) + if cand["op"] == "intersection": + out = [parts[0]] + for lf, text in zip(cand["leaves"][1:], parts[1:]): + out.append((" - " if lf.get("outside") else " ^ ") + text) + return "".join(out) + return parts[0] + + +# ------------------------------------------------------------------------------------------ +# builder 1: OCCT (the acceptance test's candidate side) +# ------------------------------------------------------------------------------------------ + +def build_occ(cand): + """Realise the description as a `TopoDS_Shape` in OCCT. Requires pythonOCC.""" + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse + if cand["op"] == "flatCells": + # The padded realisation, per `flat_cells`'s docstring: OCCT has no unbounded halfspace + # either, so the acceptance test measures `_cell_leaf`'s bounded forms of the same cells. + return _occ_balanced_union([build_occ(c) for c in flat_occ_cells(cand)]) + if cand["op"] == "unionOfCells": + return _occ_balanced_union([build_occ(c) for c in cand["cells"]]) + leaves = cand["leaves"] + out = _occ_leaf(leaves[0]) + for lf in leaves[1:]: + nxt = _occ_leaf(lf) + if cand["op"] == "union": + maker, what = BRepAlgoAPI_Fuse, "BRepAlgoAPI_Fuse" + elif lf.get("outside"): + maker, what = BRepAlgoAPI_Cut, "BRepAlgoAPI_Cut" + else: + maker, what = BRepAlgoAPI_Common, "BRepAlgoAPI_Common" + op = maker(out, nxt) + op.Build() + if not op.IsDone(): + raise RuntimeError(f"{what} failed while building the candidate") + out = op.Shape() + return out + + +def _occ_balanced_union(shapes): + """Fuse the cells pairwise, level by level, so the OCCT tree has the ROOT tree's shape.""" + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Fuse + level = list(shapes) + while len(level) > 1: + higher = [] + for i in range(0, len(level) - 1, 2): + op = BRepAlgoAPI_Fuse(level[i], level[i + 1]) + op.Build() + if not op.IsDone(): + raise RuntimeError("BRepAlgoAPI_Fuse failed while building the candidate") + higher.append(op.Shape()) + if len(level) % 2: + higher.append(level[-1]) + level = higher + return level[0] + + +def _occ_ax2(frame, along_z=0.0): + from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Pnt + o = _add(tuple(frame["origin"]), _scale(tuple(frame["z"]), along_z)) + return gp_Ax2(gp_Pnt(*o), gp_Dir(*frame["z"]), gp_Dir(*frame["x"])) + + +def _occ_cut(outer, inner): + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + op = BRepAlgoAPI_Cut(outer, inner) + op.Build() + if not op.IsDone(): + raise RuntimeError("BRepAlgoAPI_Cut failed while building the candidate") + return op.Shape() + + +def _dedupe_ring(pts, tol=1.0e-12): + """Drop consecutive duplicates in a closed (r, z) ring, the wrap included. + + The rule of `O2_TGeoToCAD._dedupe_ring`; a duplicated corner would be a zero-length edge. + """ + out = [] + for pt in pts: + if out and abs(pt[0] - out[-1][0]) < tol and abs(pt[1] - out[-1][1]) < tol: + continue + out.append(pt) + while len(out) > 1 and abs(out[0][0] - out[-1][0]) < tol and abs(out[0][1] - out[-1][1]) < tol: + out.pop() + return out + + +def pcon_profile_rz(params, tol=1.0e-12): + """The closed (r, z) profile of a `TGeoPcon`, outer chain then inner chain reversed. + + Exactly the ring `O2_TGeoToCAD.conv_pcon` revolves. + """ + z, rmin, rmax = params["z"], params["rmin"], params["rmax"] + nz = len(z) + outer = [(rmax[i], z[i]) for i in range(nz)] + if all(r <= tol for r in rmin): + inner = [(0.0, z[nz - 1]), (0.0, z[0])] + else: + inner = [(rmin[i], z[i]) for i in range(nz - 1, -1, -1)] + return _dedupe_ring(outer + inner, tol) + + +def _occ_pcon(lf): + """Revolve the (r, z) profile face: true cone/cylinder/plane faces, nothing tessellated.""" + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakePolygon + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeRevol + from OCC.Core.gp import gp_Ax1, gp_Dir, gp_Pnt + p, frame = lf["params"], lf["frame"] + pts = pcon_profile_rz(p) + if len(pts) < 3: + raise ValueError("TGeoPcon: degenerate (r, z) profile " + f"({len(pts)} distinct corner(s))") + # OCCT sweeps from the profile's own half-plane, so the profile is laid out at phi1 and the + # revolution covers dphi -- the same convention `_occ_leaf` uses for a TGeoTubeSeg. + phi1 = math.radians(p["phi1"]) + xr = _add(_scale(tuple(frame["x"]), math.cos(phi1)), + _scale(tuple(frame["y"]), math.sin(phi1))) + origin, zax = tuple(frame["origin"]), tuple(frame["z"]) + poly = BRepBuilderAPI_MakePolygon() + for (r, zz) in pts: + poly.Add(gp_Pnt(*_add(origin, _add(_scale(xr, r), _scale(zax, zz))))) + poly.Close() + if not poly.IsDone(): + raise RuntimeError("TGeoPcon: could not build the (r, z) profile wire") + face = BRepBuilderAPI_MakeFace(poly.Wire()) + if not face.IsDone(): + raise RuntimeError("TGeoPcon: the (r, z) profile is not a valid planar face") + rev = BRepPrimAPI_MakeRevol(face.Face(), gp_Ax1(gp_Pnt(*origin), gp_Dir(*zax)), + math.radians(p["dphi"])) + rev.Build() + if not rev.IsDone(): + raise RuntimeError("TGeoPcon: revolution of the (r, z) profile failed") + return rev.Shape() + + +# ------------------------------------------------------------------------------------------ +# the prism family: Trd1 / Trd2 / Arb8 / Xtru / Pgon +# ------------------------------------------------------------------------------------------ +# +# One construction, a stack of corresponding closed sections; `prism_rings` states it once. + +_PRISM_TYPES = ("TGeoTrd1", "TGeoTrd2", "TGeoArb8", "TGeoXtru", "TGeoPgon") + + +def _dedupe_ring3(pts, tol=1.0e-9): + """Drop consecutive duplicate corners of a closed 3-D ring, wrap included, as the writer does.""" + out = [] + for q in pts: + if out and max(abs(q[i] - out[-1][i]) for i in range(3)) < tol: + continue + out.append(tuple(float(c) for c in q)) + while len(out) > 1 and max(abs(out[0][i] - out[-1][i]) for i in range(3)) < tol: + out.pop() + return out + + +def _pgon_section_ring(r_apothem, z, phi1_deg, dphi_deg, nedges, full): + """One `TGeoPgon` section polygon, as `O2_TGeoToCAD._pgon_ring` builds it. + + ROOT's rmin/rmax are apothem radii, so the corners sit at `r / cos(dseg / 2)`. + """ + dseg = math.radians(dphi_deg) / nedges + radius = r_apothem / math.cos(dseg / 2.0) + n = nedges if full else nedges + 1 + return [(radius * math.cos(math.radians(phi1_deg) + k * dseg), + radius * math.sin(math.radians(phi1_deg) + k * dseg), z) for k in range(n)] + + +def pgon_rings(params): + """`(outer_stack, inner_stack|None)` for a `TGeoPgon`, as `conv_pgon` builds them.""" + z, rmin, rmax = params["z"], params["rmin"], params["rmax"] + phi1, dphi, nedges = params["phi1"], params["dphi"], int(round(params["nedges"])) + full = abs(dphi - 360.0) < 1.0e-9 + hollow = any(r > 0.0 for r in rmin) + if hollow and full: + # An annular section is two disjoint rings, which no single wire can express: the outer + # and the inner prism are separate stacks and the caps are annular. + return ([_pgon_section_ring(rmax[i], z[i], phi1, dphi, nedges, True) + for i in range(len(z))], + [_pgon_section_ring(max(rmin[i], 0.0), z[i], phi1, dphi, nedges, True) + for i in range(len(z))]) + rings = [] + for i in range(len(z)): + outer = _pgon_section_ring(rmax[i], z[i], phi1, dphi, nedges, full) + if hollow: + inner = _pgon_section_ring(max(rmin[i], 0.0), z[i], phi1, dphi, nedges, full) + rings.append(outer + list(reversed(inner))) + elif full: + rings.append(outer) + else: + rings.append(outer + [(0.0, 0.0, z[i])]) + return rings, None + + +def prism_rings(lf): + """`(outer_stack, inner_stack|None)`: the leaf's sections, in the leaf's own frame. + + Every ring is a closed polygon in corner order, and corner `i` of section `k` is joined to + corner `i` of section `k + 1`. Ring lengths agree across the stack by construction. + """ + kind, p = lf["type"], lf["params"] + if kind == "TGeoTrd1": + dx1, dx2, dy, dz = p["dx1"], p["dx2"], p["dy"], p["dz"] + return ([[(-dx1, -dy, -dz), (dx1, -dy, -dz), (dx1, dy, -dz), (-dx1, dy, -dz)], + [(-dx2, -dy, dz), (dx2, -dy, dz), (dx2, dy, dz), (-dx2, dy, dz)]], None) + if kind == "TGeoTrd2": + dx1, dx2, dy1, dy2, dz = p["dx1"], p["dx2"], p["dy1"], p["dy2"], p["dz"] + return ([[(-dx1, -dy1, -dz), (dx1, -dy1, -dz), (dx1, dy1, -dz), (-dx1, dy1, -dz)], + [(-dx2, -dy2, dz), (dx2, -dy2, dz), (dx2, dy2, dz), (-dx2, dy2, dz)]], None) + if kind == "TGeoArb8": + v, dz = p["vertices"], p["dz"] + return ([[(v[2 * i], v[2 * i + 1], -dz) for i in range(4)], + [(v[8 + 2 * i], v[8 + 2 * i + 1], dz) for i in range(4)]], None) + if kind == "TGeoXtru": + x, y, z = p["x"], p["y"], p["z"] + xoff, yoff, sc = p["xoff"], p["yoff"], p["scale"] + return ([[(xoff[k] + sc[k] * x[i], yoff[k] + sc[k] * y[i], z[k]) + for i in range(len(x))] for k in range(len(z))], None) + if kind == "TGeoPgon": + return pgon_rings(p) + raise ValueError(f"{kind} is not a prism-family leaf") + + +def _to_part(frame, q): + return _add(tuple(frame["origin"]), + _add(_scale(tuple(frame["x"]), q[0]), + _add(_scale(tuple(frame["y"]), q[1]), _scale(tuple(frame["z"]), q[2])))) + + +def prism_samples(lf): + """Every corner and every edge midpoint of a prism-family leaf, in the part frame. + + Edge midpoints are included because corners alone miss a wrong corner order. + """ + outer, inner = prism_rings(lf) + frame = lf["frame"] + out = [] + for stack in (outer, inner): + if stack is None: + continue + rings = [_dedupe_ring3(r) for r in stack] + for k, ring in enumerate(rings): + n = len(ring) + for i, q in enumerate(ring): + out.append(_to_part(frame, q)) + nxt = ring[(i + 1) % n] + out.append(_to_part(frame, _scale(_add(q, nxt), 0.5))) + if k + 1 < len(rings) and len(rings[k + 1]) == n: + up = rings[k + 1][i] + out.append(_to_part(frame, _scale(_add(q, up), 0.5))) + return out + + +def _occ_quad_face(b0, b1, t1, t0, tol=1.0e-7): + """One lateral patch: planar when its corners are coplanar, ruled when they are not. + + The rule of `O2_TGeoToCAD._quad_face`, including the Newell area test for a degenerate patch. + """ + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace + from OCC.Core.BRepFill import brepfill + from OCC.Core.gp import gp_Pnt + pts = _dedupe_ring3([b0, b1, t1, t0]) + if len(pts) < 3: + return None + nrm = [0.0, 0.0, 0.0] + for i in range(len(pts)): + a, b = pts[i], pts[(i + 1) % len(pts)] + nrm[0] += (a[1] - b[1]) * (a[2] + b[2]) + nrm[1] += (a[2] - b[2]) * (a[0] + b[0]) + nrm[2] += (a[0] - b[0]) * (a[1] + b[1]) + span = max(_norm(_sub(q, pts[0])) for q in pts[1:]) + if _norm(nrm) <= tol * span * span: + return None + if len(pts) == 3: + return BRepBuilderAPI_MakeFace(_occ_polygon_wire(pts)).Face() + n = _cross(_sub(b1, b0), _sub(t0, b0)) + nn = _norm(n) + scale = max(_norm(_sub(b1, b0)), _norm(_sub(t0, b0)), 1.0e-30) + off = abs(_dot(n, _sub(t1, b0))) / nn if nn > 0.0 else 0.0 + if nn > 1.0e-24 and off <= tol * scale: + mf = BRepBuilderAPI_MakeFace(_occ_polygon_wire(pts)) + if mf.IsDone(): + return mf.Face() + e1 = BRepBuilderAPI_MakeEdge(gp_Pnt(*b0), gp_Pnt(*b1)).Edge() + e2 = BRepBuilderAPI_MakeEdge(gp_Pnt(*t0), gp_Pnt(*t1)).Edge() + return brepfill.Face(e1, e2) + + +def _occ_polygon_wire(pts): + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakePolygon + from OCC.Core.gp import gp_Pnt + poly = BRepBuilderAPI_MakePolygon() + for q in pts: + poly.Add(gp_Pnt(float(q[0]), float(q[1]), float(q[2]))) + poly.Close() + if not poly.IsDone(): + raise RuntimeError("prism: could not build a section wire") + return poly.Wire() + + +def _occ_prism(lf): + """Sew a prism-family leaf out of explicit faces -- no tessellation, no approximation.""" + from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeFace, BRepBuilderAPI_MakeSolid, + BRepBuilderAPI_Sewing) + from OCC.Core.BRepGProp import brepgprop + from OCC.Core.GProp import GProp_GProps + from OCC.Core.TopoDS import topods + kind = lf["type"] + outer, inner = prism_rings(lf) + frame = lf["frame"] + stacks = [] + for stack in (outer, inner): + if stack is None: + continue + rings = [_dedupe_ring3([_to_part(frame, q) for q in ring]) for ring in stack] + nv = len(rings[0]) + if nv < 3 or any(len(r) != nv for r in rings): + raise ValueError(f"{kind}: sections carry " + f"{sorted({len(r) for r in rings})} distinct corner counts") + stacks.append(rings) + faces = [] + for rings in stacks: + nv = len(rings[0]) + for k in range(len(rings) - 1): + lo, hi = rings[k], rings[k + 1] + for i in range(nv): + j = (i + 1) % nv + face = _occ_quad_face(lo[i], lo[j], hi[j], hi[i]) + if face is not None: + faces.append(face) + for idx in (0, -1): + mf = BRepBuilderAPI_MakeFace(_occ_polygon_wire(stacks[0][idx])) + if len(stacks) == 2: + mf.Add(topods.Wire(_occ_polygon_wire(stacks[1][idx]).Reversed())) + if not mf.IsDone(): + raise ValueError(f"{kind}: could not build a cap face") + faces.append(mf.Face()) + extent = max(abs(c) for rings in stacks for r in rings for q in r for c in q) or 1.0 + sew = BRepBuilderAPI_Sewing(1.0e-7 * extent) + for face in faces: + sew.Add(face) + sew.Perform() + shell = sew.SewedShape() + if shell is None or shell.IsNull(): + raise ValueError(f"{kind}: sewing the sections produced nothing") + ms = BRepBuilderAPI_MakeSolid(topods.Shell(shell)) + ms.Build() + solid = ms.Solid() + props = GProp_GProps() + brepgprop.VolumeProperties(solid, props) + if props.Mass() < 0.0: + solid = topods.Solid(solid.Reversed()) + return solid + + +def _occ_eltu(lf): + """An elliptic cylinder, built exactly as `O2_TGeoToCAD.conv_eltu` builds it. + + `gp_Elips` wants its major radius first, so the frame's x is not assumed to be the major axis. + """ + from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakeWire) + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakePrism + from OCC.Core.gp import gp_Ax2, gp_Dir, gp_Elips, gp_Pnt, gp_Vec + p, frame = lf["params"], lf["frame"] + base = _sub(tuple(frame["origin"]), _scale(tuple(frame["z"]), p["dz"])) + if p["a"] >= p["b"]: + major_dir, major, minor = frame["x"], p["a"], p["b"] + else: + major_dir, major, minor = frame["y"], p["b"], p["a"] + axis = gp_Ax2(gp_Pnt(*base), gp_Dir(*frame["z"]), gp_Dir(*major_dir)) + edge = BRepBuilderAPI_MakeEdge(gp_Elips(axis, major, minor)).Edge() + face = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakeWire(edge).Wire()) + if not face.IsDone(): + raise RuntimeError("TGeoEltu: the ellipse wire is not a valid planar face") + prism = BRepPrimAPI_MakePrism(face.Face(), + gp_Vec(*_scale(tuple(frame["z"]), 2.0 * p["dz"]))) + prism.Build() + if not prism.IsDone(): + raise RuntimeError("TGeoEltu: the prism failed") + return prism.Shape() + + +def _occ_torus(lf): + """The torus, built exactly as `O2_TGeoToCAD.conv_torus` builds it. + + A hollow torus's inner cut is swept a hair further in phi, so no wedge face is coincident. + """ + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeTorus + p, frame = lf["params"], lf["frame"] + phi1, dphi = math.radians(p["phi1"]), math.radians(p["dphi"]) + xr = _add(_scale(tuple(frame["x"]), math.cos(phi1)), + _scale(tuple(frame["y"]), math.sin(phi1))) + rotated = {"origin": frame["origin"], "x": list(xr), "y": frame["y"], "z": frame["z"]} + + def make(minor, sweep): + maker = BRepPrimAPI_MakeTorus(_occ_ax2(rotated), p["r"], minor, sweep) + maker.Build() + if not maker.IsDone(): + raise RuntimeError("BRepPrimAPI_MakeTorus failed while building the candidate") + return maker.Shape() + + outer = make(p["rmax"], dphi) + if p["rmin"] > 0.0: + full = dphi >= 2.0 * math.pi - 1.0e-12 + inner = make(p["rmin"], dphi if full else min(dphi + 1.0e-4, 2.0 * math.pi)) + outer = _occ_cut(outer, inner) + return outer + + +def _occ_leaf(lf): + from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere) + from OCC.Core.gp import gp_Pnt + kind, p, frame = lf["type"], lf["params"], lf["frame"] + if kind == "TGeoTorus": + return _occ_torus(lf) + if kind == "TGeoEltu": + return _occ_eltu(lf) + if kind == "TGeoPcon": + return _occ_pcon(lf) + if kind in _PRISM_TYPES: + return _occ_prism(lf) + if kind == "TGeoBBox": + corner = tuple(frame["origin"]) + for axis, half in (("x", p["dx"]), ("y", p["dy"]), ("z", p["dz"])): + corner = _sub(corner, _scale(tuple(frame[axis]), half)) + ax2 = _occ_ax2({"origin": list(corner), "x": frame["x"], "y": frame["y"], + "z": frame["z"]}) + return BRepPrimAPI_MakeBox(ax2, 2 * p["dx"], 2 * p["dy"], 2 * p["dz"]).Shape() + if kind in ("TGeoTube", "TGeoTubeSeg"): + ax2 = _occ_ax2(frame, -p["dz"]) + if kind == "TGeoTubeSeg": + # OCCT sweeps from the frame's own x direction, so rotate the reference direction to + # phi1 and sweep by (phi2 - phi1); ROOT states the same wedge as two absolute angles. + phi1 = math.radians(p["phi1"]) + xr = _add(_scale(tuple(frame["x"]), math.cos(phi1)), + _scale(tuple(frame["y"]), math.sin(phi1))) + rotated = {"origin": frame["origin"], "x": list(xr), "y": frame["y"], + "z": frame["z"]} + ax2 = _occ_ax2(rotated, -p["dz"]) + sweep = math.radians(p["phi2"] - p["phi1"]) + outer = BRepPrimAPI_MakeCylinder(ax2, p["rmax"], 2 * p["dz"], sweep).Shape() + if p["rmin"] > 0.0: + inner = BRepPrimAPI_MakeCylinder(_occ_ax2(rotated, -p["dz"] - _pad(p["dz"])), + p["rmin"], 2 * p["dz"] + 4 * _pad(p["dz"]), + sweep).Shape() + outer = _occ_cut(outer, inner) + return outer + outer = BRepPrimAPI_MakeCylinder(ax2, p["rmax"], 2 * p["dz"]).Shape() + if p["rmin"] > 0.0: + # The inner cylinder is longer than the outer, so the cut has no coincident caps. + pad = _pad(p["dz"]) + inner = BRepPrimAPI_MakeCylinder(_occ_ax2(frame, -p["dz"] - pad), p["rmin"], + 2 * p["dz"] + 2 * pad).Shape() + outer = _occ_cut(outer, inner) + return outer + if kind == "TGeoCone": + outer = _occ_frustum(_occ_ax2(frame, -p["dz"]), p["rmax1"], p["rmax2"], 2 * p["dz"]) + if p["rmin1"] > 0.0 or p["rmin2"] > 0.0: + pad = _pad(p["dz"]) + slope = (p["rmin2"] - p["rmin1"]) / (2 * p["dz"]) + inner = _occ_frustum(_occ_ax2(frame, -p["dz"] - pad), + max(p["rmin1"] - slope * pad, 0.0), + max(p["rmin2"] + slope * pad, 0.0), + 2 * p["dz"] + 2 * pad) + outer = _occ_cut(outer, inner) + return outer + if kind == "TGeoSphere": + o = tuple(frame["origin"]) + outer = BRepPrimAPI_MakeSphere(gp_Pnt(*o), p["rmax"]).Shape() + if p["rmin"] > 0.0: + inner = BRepPrimAPI_MakeSphere(gp_Pnt(*o), p["rmin"]).Shape() + outer = _occ_cut(outer, inner) + return outer + raise ValueError(f"unhandled leaf type {kind!r}") + + +def _occ_frustum(ax2, r1, r2, height): + """A cone frustum, or a cylinder when its two radii are the same. + + `BRepPrimAPI_MakeCone` raises on two identical radii, which a `TGeoCone` barrel or bore can have. + """ + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeCone, BRepPrimAPI_MakeCylinder + if abs(r1 - r2) <= _CONE_DEGENERATE_EPS * max(abs(r1), abs(r2), 1.0): + return BRepPrimAPI_MakeCylinder(ax2, 0.5 * (r1 + r2), height).Shape() + return BRepPrimAPI_MakeCone(ax2, r1, r2, height).Shape() + + +def _pad(dz): + return max(1.0e-3 * dz, 1.0e-6) + + +# ------------------------------------------------------------------------------------------ +# builder 2: ROOT (what shape_.root carries) +# ------------------------------------------------------------------------------------------ + +def build_root(cand, name="shape"): + """Realise the description as `(TGeoShape, placement)`. Requires PyROOT. + + A single primitive is the bare ROOT class in its own canonical frame and `placement` places it; + a multi-leaf union is a `TGeoCompositeShape` in the part frame with placement None. + """ + import ROOT + placement = placement_for_candidate(cand) + if cand["op"] == "flatCells": + return _root_flat_csg(cand, name), None + if cand["op"] == "unionOfCells": + return _root_balanced_union(name, cand["cells"]), None + if cand["op"] == "primitive": + lf = cand["leaves"][0] + frame = lf["frame"] + if placement is None and lf_is_box(lf) and not frame_is_identity(frame): + # Axis-aligned box: TGeoBBox's own fOrigin is the placement. + from array import array + p = lf["params"] + return ROOT.TGeoBBox(name, p["dx"], p["dy"], p["dz"], + array("d", [float(c) for c in frame["origin"]])), None + shape = _root_leaf(lf, name) + return shape, placement + shapes = [(_root_leaf(lf, f"{name}_l{i}"), lf["frame"]) + for i, lf in enumerate(cand["leaves"])] + outside = [bool(lf.get("outside")) for lf in cand["leaves"]] + return _root_composite(name, shapes, cand["op"], outside), placement + + +def _root_cell(c, name): + """`(shape, frame)` for one cell of a DNF. + + An intersection cell is in the part frame with an identity frame; a primitive cell is the bare + shape with the frame that places it in the union node. + """ + if c["op"] == "primitive": + lf = c["leaves"][0] + return _root_leaf(lf, name), lf["frame"] + shapes = [(_root_leaf(lf, f"{name}_l{i}"), lf["frame"]) for i, lf in enumerate(c["leaves"])] + outside = [bool(lf.get("outside")) for lf in c["leaves"]] + return _root_composite(name, shapes, "intersection", outside), identity_frame() + + +_FLAT_CSG_DECLARED = [] + + +def _declare_flat_csg(): + """Make `o2::cad::O2FlatCSG` and `LoadFlatCSG` visible to Cling. Once per interpreter.""" + import ROOT + if _FLAT_CSG_DECLARED: + return + ROOT.gInterpreter.AddIncludePath(f"{ROOT.gSystem.Getenv('O2_ROOT')}/include") + ROOT.gSystem.Load("libO2CADSupport") + ROOT.gInterpreter.Declare( + '#include "CADSupport/O2FlatCSG.h"\n' + 'namespace o2 { namespace cad {\n' + 'bool LoadFlatCSG(const std::string& file, O2FlatCSG& solid);\n' + '} }') + _FLAT_CSG_DECLARED.append(True) + + +def _root_flat_csg(cand, name): + """The `O2FlatCSG` a `flatCells` description describes, built through its sidecar. + + Writing and loading `flatcsg_*.bin` assembles the shape with the code `geom.C` runs. + """ + import tempfile + from pathlib import Path + import ROOT + from cadsupport import flat + _declare_flat_csg() + blocks, cells = flat_sidecar_records(cand) + with tempfile.TemporaryDirectory() as folder: + sidecar = Path(folder) / "flatcsg.bin" + flat.write_sidecar(sidecar, blocks, cells) + shape = ROOT.o2.cad.O2FlatCSG(name) + ROOT.SetOwnership(shape, False) + if not ROOT.o2.cad.LoadFlatCSG(str(sidecar), shape): + raise ValueError(f"LoadFlatCSG refused the sidecar written for {name!r}") + shape.CloseShape() + if not shape.IsClosed(): + raise ValueError(f"O2FlatCSG::CloseShape refused the cells of {name!r}: see its Error " + "message above (a missing, inverted or non-finite cell bounding box)") + return shape + + +def flat_sidecar_records(cand): + """`(blocks, cells)` in the layout `cadsupport.flat.write_sidecar` takes. + + The blocks of every cell, concatenated, and the `(first, count, volume, lo, hi)` cell table. + """ + blocks, cells = [], [] + for c in cand["cells"]: + cells.append({"first": len(blocks), "count": len(c["blocks"]), + "volume": float(c["volume"]), + "lo": [float(v) for v in c["lo"]], "hi": [float(v) for v in c["hi"]]}) + blocks.extend(c["blocks"]) + return blocks, cells + + +def _root_balanced_union(name, cells): + """The cells as a balanced binary tree of `TGeoUnion` nodes, so queries scale with log2 N.""" + import ROOT + level = [_root_cell(c, f"{name}_c{i}") for i, c in enumerate(cells)] + step = 0 + while len(level) > 1: + higher = [] + for i in range(0, len(level) - 1, 2): + (left, left_frame), (right, right_frame) = level[i], level[i + 1] + ROOT.SetOwnership(left, False) + ROOT.SetOwnership(right, False) + node = ROOT.TGeoUnion(left, right, _root_matrix(left_frame, f"{name}_u{step}a"), + _root_matrix(right_frame, f"{name}_u{step}b")) + ROOT.SetOwnership(node, False) + comp = ROOT.TGeoCompositeShape(f"{name}_u{step}", node) + ROOT.SetOwnership(comp, False) + higher.append((comp, identity_frame())) + step += 1 + if len(level) % 2: + higher.append(level[-1]) + level = higher + shape = level[0][0] + shape.SetName(name) + return shape + + +def root_placement_matrix(placement, name="placement"): + """The placement as the `TGeoHMatrix` stored under `placement`, or None for the identity.""" + if placement is None: + return None + import ROOT + # Through TGeoRotation/TGeoCombiTrans, which set the kGeoRotation/kGeoTranslation bits. + combi = _root_matrix({"x": [placement[0][0], placement[1][0], placement[2][0]], + "y": [placement[0][1], placement[1][1], placement[2][1]], + "z": [placement[0][2], placement[1][2], placement[2][2]], + "origin": [placement[0][3], placement[1][3], placement[2][3]]}, name) + matrix = ROOT.TGeoHMatrix(combi) + matrix.SetName(name) + ROOT.SetOwnership(matrix, False) + return matrix + + +def placement_from_root_matrix(matrix): + """The inverse of `root_placement_matrix()`, for reading an artefact back.""" + if matrix is None: + return None + rot = matrix.GetRotationMatrix() + tr = matrix.GetTranslation() + return [[rot[0], rot[1], rot[2], tr[0]], + [rot[3], rot[4], rot[5], tr[1]], + [rot[6], rot[7], rot[8], tr[2]]] + + +def lf_is_box(lf): + return lf["type"] == "TGeoBBox" + + +def _root_matrix(frame, name): + import ROOT + from array import array + rot = ROOT.TGeoRotation(name + "_r") + # TGeoRotation::SetMatrix takes the local->master matrix row-major, i.e. the columns are the + # local frame's basis vectors expressed in the part frame. + m = array("d", [frame["x"][0], frame["y"][0], frame["z"][0], + frame["x"][1], frame["y"][1], frame["z"][1], + frame["x"][2], frame["y"][2], frame["z"][2]]) + rot.SetMatrix(m) + combi = ROOT.TGeoCombiTrans(frame["origin"][0], frame["origin"][1], frame["origin"][2], rot) + ROOT.SetOwnership(rot, False) + ROOT.SetOwnership(combi, False) + return combi + + +def _root_node_class(op, outside): + import ROOT + if op == "union": + return ROOT.TGeoUnion + if op == "intersection": + return ROOT.TGeoSubtraction if outside else ROOT.TGeoIntersection + raise ValueError(f"unhandled composite op {op!r}") + + +def _root_composite(name, shapes_and_frames, op, outside=None): + """Left-fold the leaves into nested boolean nodes, in the leaves' order. + + PyROOT owns no operand, since `TGeoBoolNode` deletes them. Under `intersection` an `outside` + leaf enters as a `TGeoSubtraction`. + """ + import ROOT + flags = list(outside or [False] * len(shapes_and_frames)) + (s0, f0), (s1, f1) = shapes_and_frames[0], shapes_and_frames[1] + ROOT.SetOwnership(s0, False) + ROOT.SetOwnership(s1, False) + node = _root_node_class(op, flags[1])(s0, s1, _root_matrix(f0, f"{name}_m0"), + _root_matrix(f1, f"{name}_m1")) + ROOT.SetOwnership(node, False) + comp = ROOT.TGeoCompositeShape(f"{name}_c1", node) + ROOT.SetOwnership(comp, False) + for i, (shape, frame) in enumerate(shapes_and_frames[2:], start=2): + ROOT.SetOwnership(shape, False) + node = _root_node_class(op, flags[i])(comp, shape, ROOT.nullptr, + _root_matrix(frame, f"{name}_m{i}")) + ROOT.SetOwnership(node, False) + comp = ROOT.TGeoCompositeShape(f"{name}_c{i}", node) + ROOT.SetOwnership(comp, False) + comp.SetName(name) + return comp + + +def _root_leaf(lf, name): + import ROOT + kind, p = lf["type"], lf["params"] + if kind == "TGeoBBox": + return ROOT.TGeoBBox(name, p["dx"], p["dy"], p["dz"]) + if kind == "TGeoTube": + return ROOT.TGeoTube(name, p["rmin"], p["rmax"], p["dz"]) + if kind == "TGeoTubeSeg": + return ROOT.TGeoTubeSeg(name, p["rmin"], p["rmax"], p["dz"], p["phi1"], p["phi2"]) + if kind == "TGeoCone": + return ROOT.TGeoCone(name, p["dz"], p["rmin1"], p["rmax1"], p["rmin2"], p["rmax2"]) + if kind == "TGeoSphere": + return ROOT.TGeoSphere(name, p["rmin"], p["rmax"]) + if kind == "TGeoTorus": + return ROOT.TGeoTorus(name, p["r"], p["rmin"], p["rmax"], p["phi1"], p["dphi"]) + if kind == "TGeoEltu": + return ROOT.TGeoEltu(name, p["a"], p["b"], p["dz"]) + if kind == "TGeoPcon": + shape = ROOT.TGeoPcon(name, p["phi1"], p["dphi"], len(p["z"])) + for i, (zz, r0, r1) in enumerate(zip(p["z"], p["rmin"], p["rmax"])): + shape.DefineSection(i, zz, r0, r1) + return shape + if kind == "TGeoPgon": + shape = ROOT.TGeoPgon(name, p["phi1"], p["dphi"], int(round(p["nedges"])), len(p["z"])) + for i, (zz, r0, r1) in enumerate(zip(p["z"], p["rmin"], p["rmax"])): + shape.DefineSection(i, zz, r0, r1) + return shape + if kind == "TGeoTrd1": + return ROOT.TGeoTrd1(name, p["dx1"], p["dx2"], p["dy"], p["dz"]) + if kind == "TGeoTrd2": + return ROOT.TGeoTrd2(name, p["dx1"], p["dx2"], p["dy1"], p["dy2"], p["dz"]) + if kind == "TGeoArb8": + from array import array + return ROOT.TGeoArb8(name, p["dz"], array("d", [float(v) for v in p["vertices"]])) + if kind == "TGeoXtru": + from array import array + shape = ROOT.TGeoXtru(len(p["z"])) + shape.SetName(name) + shape.DefinePolygon(len(p["x"]), array("d", [float(v) for v in p["x"]]), + array("d", [float(v) for v in p["y"]])) + for k in range(len(p["z"])): + shape.DefineSection(k, p["z"][k], p["xoff"][k], p["yoff"][k], p["scale"][k]) + return shape + raise ValueError(f"unhandled leaf type {kind!r}") diff --git a/Detectors/CADSupport/tools/cadsupport/recognise.py b/Detectors/CADSupport/tools/cadsupport/recognise.py new file mode 100644 index 0000000000000..7b235db1c5eae --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/recognise.py @@ -0,0 +1,2235 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""CSG recognition: propose a placed-primitive description of a leaf solid from its carriers. + +Matchers run from specific to general: whole-part primitives (box, tube, cone, sphere, eltu, +torus), the revolved profile, the prism family, the two-cluster tube union, one intersection cell, +a union of cells and, only after that declines, flat cells. Every threshold is relative to the +part's bounding-box diagonal (1e-6), extents come from the trimmed faces' UV bounds, and an +unhandled structure returns a reason, never a guess. `accept.symmetric_difference` decides. +""" + +import math + +from cadsupport import primitives as prim, tier0 +from cadsupport.primitives import _add, _cross, _dot, _norm, _scale, _sub, _unit + +# Relative tolerance on directions, radii and offsets, times the part's bounding-box diagonal. +REL_TOL = 1.0e-6 +ANG_TOL = 1.0e-6 + + +class Declined(Exception): + """Raised internally with the reason; recognise() turns it into a report entry.""" + + +def _leaf(kind, params, frame, outside=False): + """`primitives.leaf`, with an illegal *solid* turned into a decline. + + A missing parameter or an unknown leaf type is a matcher bug and still raises `ValueError`. + """ + try: + return prim.leaf(kind, params, frame, outside) + except prim.InvalidDescription as illegal: + raise Declined(str(illegal)) from None + + +def _candidate(op, leaves, recogniser, notes=None): + """`primitives.candidate`, with an illegal description turned into a decline.""" + try: + return prim.candidate(op, leaves, recogniser, notes) + except prim.InvalidDescription as illegal: + raise Declined(str(illegal)) from None + + +# ------------------------------------------------------------------------------------------ +# face analysis +# ------------------------------------------------------------------------------------------ + +class _LazyScale: + """`max(bounding-box diagonal, 1 cm)` of a solid, measured on first use and then kept.""" + + def __init__(self, solid): + self._solid = solid + self._value = None + + @property + def value(self): + if self._value is None: + self._value = max(_bbox_diagonal(self._solid), 1.0) + return self._value + + +def _face_records(solid): + """[{kind, ...carrier..., uv bounds}] for every face, or a reason why the solid is out. + + A face no adaptor branch claims goes through Tier 0, and a canonicalised one is flagged so. + """ + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.BRepTools import breptools + from OCC.Core.GeomAbs import (GeomAbs_Cone, GeomAbs_Cylinder, GeomAbs_Plane, + GeomAbs_Sphere, GeomAbs_Torus) + from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_REVERSED + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + + records = [] + n_freeform = 0 + n_faces = 0 + n_canonical = 0 + best_declined = None + # Measured only if a face actually needs canonicalising, so a part whose faces are all + # natively analytic -- which is every part of every detector corpus -- costs what it did. + scale = _LazyScale(solid) + exp = TopExp_Explorer(solid, TopAbs_FACE) + while exp.More(): + face = topods.Face(exp.Current()) + exp.Next() + n_faces += 1 + ad = BRepAdaptor_Surface(face, True) + umin, umax, vmin, vmax = breptools.UVBounds(face) + t = ad.GetType() + rec = {"uv": (umin, umax, vmin, vmax), "face": face, + "reversed": face.Orientation() == TopAbs_REVERSED} + if t == GeomAbs_Plane: + pl = ad.Plane() + n = _xyz(pl.Axis().Direction()) + if rec["reversed"]: + n = _scale(n, -1.0) + rec.update(kind="plane", n=n, p=_xyz(pl.Axis().Location())) + elif t == GeomAbs_Cylinder: + cy = ad.Cylinder() + rec.update(kind="cylinder", d=_xyz(cy.Axis().Direction()), + p=_xyz(cy.Axis().Location()), x=_xyz(cy.Position().XDirection()), + r=cy.Radius()) + elif t == GeomAbs_Cone: + co = ad.Cone() + rec.update(kind="cone", d=_xyz(co.Axis().Direction()), + p=_xyz(co.Axis().Location()), x=_xyz(co.Position().XDirection()), + r=co.RefRadius(), a=co.SemiAngle()) + elif t == GeomAbs_Sphere: + sp = ad.Sphere() + rec.update(kind="sphere", p=_xyz(sp.Location()), r=sp.Radius()) + elif t == GeomAbs_Torus: + to = ad.Torus() + rec.update(kind="torus", d=_xyz(to.Axis().Direction()), + p=_xyz(to.Position().Location()), x=_xyz(to.Position().XDirection()), + r=to.MajorRadius(), rt=to.MinorRadius()) + else: + ellipse = _extruded_ellipse(ad) + if ellipse is not None: + rec.update(kind="eltu", **ellipse) + else: + canonical, gap = tier0.canonicalise(face, ad, scale.value) + if canonical is None: + n_freeform += 1 + if gap is not None and (best_declined is None or gap < best_declined): + best_declined = gap + continue + rec.update(**canonical) + if rec["kind"] == "plane" and rec["reversed"]: + # Tier 0 returns the surface's own normal; the face's flag is applied here. + rec["n"] = _scale(rec["n"], -1.0) + n_canonical += 1 + records.append(rec) + if n_freeform: + how_far = ("" if best_declined is None else + f"; the nearest canonical surface any of them proposes is " + f"{best_declined:.3g} cm away, {best_declined / scale.value:.3g} of the part, " + f"against {tier0.REL_TOL:.0e}") + rescued = f"; {n_canonical} canonicalised" if n_canonical else "" + return None, (f"free-form faces: {n_freeform} of {n_faces} " + "(surface kind outside plane/cylinder/cone/sphere/torus and not a quadric " + "in disguise; a twisted TGeoArb8 side is one of these and is out of " + f"scope){rescued}{how_far}") + if not records: + return None, "no faces" + return records, None + + +def _extruded_ellipse(ad): + """`{d, p, x, y, a, b}` if this surface is a linear extrusion of an exact ellipse, else None. + + Major >= minor always; `_eltu_frame` recovers which one the source called `a`. + """ + from OCC.Core.GeomAbs import GeomAbs_Ellipse, GeomAbs_SurfaceOfExtrusion + if ad.GetType() != GeomAbs_SurfaceOfExtrusion: + return None + try: + basis = ad.BasisCurve() + if basis.GetType() != GeomAbs_Ellipse: + return None + el = basis.Ellipse() + except Exception: # noqa: BLE001 + return None + return {"d": _unit(_xyz(ad.Direction())), "p": _xyz(el.Location()), + "x": _xyz(el.Position().XDirection()), "y": _xyz(el.Position().YDirection()), + "a": el.MajorRadius(), "b": el.MinorRadius()} + + +def _xyz(v): + return (v.X(), v.Y(), v.Z()) + + +def _bbox_diagonal(shape): + from OCC.Core.Bnd import Bnd_Box + from OCC.Core.BRepBndLib import brepbndlib + box = Bnd_Box() + brepbndlib.Add(shape, box) + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + return math.sqrt((xmax - xmin) ** 2 + (ymax - ymin) ** 2 + (zmax - zmin) ** 2) + + +# ------------------------------------------------------------------------------------------ +# direction / axis predicates +# ------------------------------------------------------------------------------------------ + +def _parallel(a, b): + return _norm(_cross(a, b)) <= ANG_TOL and _dot(a, b) > 0.0 + + +def _collinear(a, b): + return _norm(_cross(a, b)) <= ANG_TOL + + +def _perpendicular(a, b): + return abs(_dot(a, b)) <= ANG_TOL + + +_COORDINATE_AXES = ((1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0)) + + +def _snap_to_coordinate_axis(vec): + """(index, sign) if `vec` is a coordinate axis to within ANG_TOL, else None. + + An identity frame lets `primitives.build_root` emit a bare shape instead of a placed one. + """ + for index, axis in enumerate(_COORDINATE_AXES): + dot = _dot(vec, axis) + if abs(abs(dot) - 1.0) <= ANG_TOL and _norm(_cross(vec, axis)) <= ANG_TOL: + return index, (1.0 if dot > 0.0 else -1.0) + return None + + +def _on_axis(point, loc, direction, tol): + delta = _sub(point, loc) + return _norm(_sub(delta, _scale(direction, _dot(delta, direction)))) <= tol + + +# ------------------------------------------------------------------------------------------ +# clustering +# ------------------------------------------------------------------------------------------ + +def _axial_extent(rec, axis_dir, axis_loc): + """The face's [tmin, tmax] along the cluster axis, and its radii at those two ends.""" + umin, umax, vmin, vmax = rec["uv"] + base = _dot(_sub(rec["p"], axis_loc), axis_dir) + sign = 1.0 if _dot(rec["d"], axis_dir) > 0.0 else -1.0 + if rec["kind"] == "cylinder": + t0, t1 = base + sign * vmin, base + sign * vmax + r0 = r1 = rec["r"] + else: # cone + ca, sa = math.cos(rec["a"]), math.sin(rec["a"]) + t0, t1 = base + sign * vmin * ca, base + sign * vmax * ca + r0, r1 = rec["r"] + vmin * sa, rec["r"] + vmax * sa + if t0 > t1: + t0, t1, r0, r1 = t1, t0, r1, r0 + return t0, t1, r0, r1 + + +def _cluster_axial(records, tol): + """Group cylinder/cone faces by the axis *line* they sit on.""" + clusters = [] + for rec in records: + if rec["kind"] not in ("cylinder", "cone"): + continue + for cl in clusters: + if _collinear(rec["d"], cl["dir"]) and _on_axis(rec["p"], cl["loc"], cl["dir"], tol): + cl["members"].append(rec) + break + else: + clusters.append({"dir": rec["d"], "loc": rec["p"], "x": rec["x"], "members": [rec]}) + for cl in clusters: + spans = [_axial_extent(m, cl["dir"], cl["loc"]) for m in cl["members"]] + cl["tmin"] = min(s[0] for s in spans) + cl["tmax"] = max(s[1] for s in spans) + cl["spans"] = spans + cl["kinds"] = sorted({m["kind"] for m in cl["members"]}) + return clusters + + +def _distinct_radii(values, tol): + out = [] + for v in sorted(values): + if not out or abs(v - out[-1]) > tol: + out.append(v) + return out + + +def _split_planes(records, clusters, tol): + """Assign every planar face to a cluster as a cap, or as a wedge face through its axis.""" + caps = {i: [] for i in range(len(clusters))} + wedges = {i: [] for i in range(len(clusters))} + for rec in records: + if rec["kind"] != "plane": + continue + placed = False + for i, cl in enumerate(clusters): + if _collinear(rec["n"], cl["dir"]): + caps[i].append(rec) + placed = True + break + if _perpendicular(rec["n"], cl["dir"]) and abs( + _dot(_sub(rec["p"], cl["loc"]), rec["n"])) <= tol: + wedges[i].append(rec) + placed = True + break + if not placed: + raise Declined("a planar face is neither a cap nor a wedge of any axis cluster") + return caps, wedges + + +# ------------------------------------------------------------------------------------------ +# Tier 1: whole-part primitives +# ------------------------------------------------------------------------------------------ + +def _match_box(records, tol): + planes = [r for r in records if r["kind"] == "plane"] + if len(planes) != len(records): + return None + if len(planes) != 6: + raise Declined(f"{len(planes)} planar faces: not a six-plane box") + used = [False] * 6 + # Per axis: a face's outward normal `n`, the mid-plane offset along `n`, the half-thickness. + axes = [] + for i in range(6): + if used[i]: + continue + for j in range(i + 1, 6): + if used[j] or _dot(planes[i]["n"], planes[j]["n"]) > 0.0: + continue + if _collinear(planes[i]["n"], planes[j]["n"]): + used[i] = used[j] = True + n = _unit(planes[i]["n"]) + di = _dot(planes[i]["p"], n) + dj = _dot(planes[j]["p"], n) + axes.append((n, (di + dj) / 2.0, (di - dj) / 2.0)) + break + else: + raise Declined("a box face has no opposite partner") + if len(axes) != 3: + raise Declined("the six planes do not form three opposite pairs") + for a in range(3): + for b in range(a + 1, 3): + if not _perpendicular(axes[a][0], axes[b][0]): + raise Declined("the three plane pairs are not mutually perpendicular") + if any(half <= 0.0 for _n, _mid, half in axes): + raise Declined("the plane pair separations are not positive (inverted orientations?)") + # Coordinate-aligned axes are relabelled to the identity frame, so a bare TGeoBBox is emitted. + snapped = [_snap_to_coordinate_axis(n) for n, _mid, _half in axes] + if all(s is not None for s in snapped) and len({s[0] for s in snapped}) == 3: + ordered = [None, None, None] + for (index, sign), (_n, mid, half) in zip(snapped, axes): + ordered[index] = (_COORDINATE_AXES[index], sign * mid, half) + axes = ordered + elif _dot(_cross(axes[0][0], axes[1][0]), axes[2][0]) < 0.0: + axes = [axes[0], axes[2], axes[1]] # keep the frame right-handed + x, y, z = axes[0][0], axes[1][0], _cross(axes[0][0], axes[1][0]) + halves = [axes[k][2] for k in range(3)] + origin = (0.0, 0.0, 0.0) + for k, axis in enumerate((x, y, z)): + origin = _add(origin, _scale(axis, axes[k][1])) + frame = {"origin": [float(c) for c in origin], "x": list(x), "y": list(y), "z": list(z)} + return _candidate("primitive", [_leaf( + "TGeoBBox", {"dx": halves[0], "dy": halves[1], "dz": halves[2]}, frame)], "tier1-box") + + +def _match_axial_primitive(records, clusters, caps, wedges, tol): + """One axis cluster + two caps: a tube, a tube segment or a cone.""" + cl = clusters[0] + cap = caps[0] + wedge = wedges[0] + if len(cap) != 2: + raise Declined(f"{len(cap)} cap plane(s) perpendicular to the axis, expected 2") + if len(wedge) not in (0, 2): + raise Declined(f"{len(wedge)} wedge plane(s) through the axis, expected 0 or 2") + t_caps = sorted(_dot(_sub(c["p"], cl["loc"]), cl["dir"]) for c in cap) + if t_caps[1] - t_caps[0] <= 0.0: + raise Declined("the two caps are coincident") + # The caps bound the solid; the lateral faces must not stick out of them. + if cl["tmin"] < t_caps[0] - tol or cl["tmax"] > t_caps[1] + tol: + raise Declined("a lateral face extends beyond the cap planes") + dz = (t_caps[1] - t_caps[0]) / 2.0 + centre = _add(cl["loc"], _scale(cl["dir"], (t_caps[0] + t_caps[1]) / 2.0)) + + if cl["kinds"] == ["cylinder"]: + radii = _distinct_radii([m["r"] for m in cl["members"]], tol) + if len(radii) > 2: + raise Declined(f"{len(radii)} distinct coaxial radii, expected 1 or 2") + rmin = radii[0] if len(radii) == 2 else 0.0 + rmax = radii[-1] + outer = [m for m in cl["members"] if abs(m["r"] - rmax) <= tol] + frame = prim.frame_from_axis(centre, cl["dir"], outer[0]["x"]) + if wedge: + phi1, phi2 = _phi_range(outer, frame) + return _candidate("primitive", [_leaf( + "TGeoTubeSeg", {"rmin": rmin, "rmax": rmax, "dz": dz, "phi1": phi1, + "phi2": phi2}, frame)], "tier1-tubeseg") + return _candidate("primitive", [_leaf( + "TGeoTube", {"rmin": rmin, "rmax": rmax, "dz": dz}, frame)], "tier1-tube") + + if cl["kinds"] == ["cone"]: + if wedge: + raise Declined("a phi-cut cone is out of scope (TGeoConeSeg not emitted)") + if len(cl["members"]) > 2: + raise Declined(f"{len(cl['members'])} coaxial cone faces, expected 1 or 2") + radii_at = [] + for member in cl["members"]: + radii_at.append(_cone_radii_at(member, cl, t_caps[0], t_caps[1])) + radii_at.sort(key=lambda rr: rr[0] + rr[1]) + if len(radii_at) == 2: + (rmin1, rmin2), (rmax1, rmax2) = radii_at + else: + (rmax1, rmax2), = radii_at + rmin1 = rmin2 = 0.0 + frame = prim.frame_from_axis(centre, cl["dir"], cl["members"][0]["x"]) + return _candidate("primitive", [_leaf( + "TGeoCone", {"dz": dz, "rmin1": rmin1, "rmax1": rmax1, "rmin2": rmin2, + "rmax2": rmax2}, frame)], "tier1-cone") + + raise Declined(f"mixed lateral surface kinds {cl['kinds']} on one axis") + + +def _cone_radii_at(member, cl, t0, t1): + sa, ca = math.sin(member["a"]), math.cos(member["a"]) + base = _dot(_sub(member["p"], cl["loc"]), cl["dir"]) + sign = 1.0 if _dot(member["d"], cl["dir"]) > 0.0 else -1.0 + out = [] + for t in (t0, t1): + v = sign * (t - base) / ca if ca != 0.0 else 0.0 + out.append(abs(member["r"] + v * sa)) + return out[0], out[1] + + +def _phi_range(outer_faces, frame): + """Absolute phi bounds, in degrees, of a wedge, measured in the emitted frame's x/y.""" + lo, hi = None, None + for face in outer_faces: + umin, umax, _v0, _v1 = face["uv"] + # the face's own reference direction may differ from the frame's x + offset = math.atan2(_dot(face["x"], frame["y"]), _dot(face["x"], frame["x"])) + for u in (umin + offset, umax + offset): + lo = u if lo is None else min(lo, u) + hi = u if hi is None else max(hi, u) + span = math.degrees(hi - lo) + if span >= 360.0 - 1.0e-6: + raise Declined("the wedge spans a full turn") + return math.degrees(lo), math.degrees(hi) + + +def _match_sphere(records, tol): + spheres = [r for r in records if r["kind"] == "sphere"] + if not spheres: + return None + if len(spheres) != len(records): + raise Declined("a sphere with additional faces is out of scope (no theta/phi cuts)") + radii = _distinct_radii([s["r"] for s in spheres], tol) + centre = spheres[0]["p"] + for s in spheres[1:]: + if _norm(_sub(s["p"], centre)) > tol: + raise Declined("spherical faces are not concentric") + if len(radii) != 1: + raise Declined(f"{len(radii)} distinct concentric sphere radii, expected 1") + return _candidate("primitive", [_leaf( + "TGeoSphere", {"rmin": 0.0, "rmax": radii[0]}, + prim.identity_frame(centre))], "tier1-sphere") + + +# ------------------------------------------------------------------------------------------ +# The revolved profile: one axis, any number of z sections -> TGeoPcon +# ------------------------------------------------------------------------------------------ +# +# The z levels are every lateral endpoint and cap plane; each annulus is read at its interval's +# midpoint, and `_profile_gap` measures the boundary samples against the rebuilt (r, z) profile. + +_PROFILE_SAMPLES = (0.0, 0.25, 0.5, 0.75, 1.0) + + +def _merge_levels(values, tol): + """Sorted distinct z levels from `(value, exact)` pairs merged within `tol`, exact wins.""" + out = [] + for value, exact in sorted(values): + if out and value - out[-1][0] <= tol: + if exact and not out[-1][1]: + out[-1] = (value, True) + continue + out.append((value, exact)) + return [value for value, _exact in out] + + +def _span_radius_at(span, t): + """The lateral's radius at axial coordinate `t`; the segment is straight in (r, z).""" + t0, t1, r0, r1 = span[0], span[1], span[2], span[3] + if t1 - t0 <= 0.0: + return r0 + return r0 + (r1 - r0) * (t - t0) / (t1 - t0) + + +def _point_segment_distance(p, a, b): + dx, dy = b[0] - a[0], b[1] - a[1] + length2 = dx * dx + dy * dy + if length2 <= 0.0: + return math.hypot(p[0] - a[0], p[1] - a[1]) + s = ((p[0] - a[0]) * dx + (p[1] - a[1]) * dy) / length2 + s = min(1.0, max(0.0, s)) + return math.hypot(p[0] - (a[0] + s * dx), p[1] - (a[1] + s * dy)) + + +def _profile_gap(profile, samples): + """The largest distance, in cm, from a boundary sample `(r, z)` to the profile's outline.""" + worst = 0.0 + n = len(profile) + for sample in samples: + best = float("inf") + for i in range(n): + best = min(best, _point_segment_distance(sample, profile[i], profile[(i + 1) % n])) + if best <= 0.0: + break + worst = max(worst, best) + return worst + + +def _solid_vertices(solid): + """Every vertex of the solid, in the part frame, read from its topology.""" + from OCC.Core.BRep import BRep_Tool + from OCC.Core.TopAbs import TopAbs_VERTEX + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + out = [] + seen = set() + exp = TopExp_Explorer(solid, TopAbs_VERTEX) + while exp.More(): + pnt = BRep_Tool.Pnt(topods.Vertex(exp.Current())) + exp.Next() + key = (round(pnt.X(), 9), round(pnt.Y(), 9), round(pnt.Z(), 9)) + if key in seen: + continue + seen.add(key) + out.append((pnt.X(), pnt.Y(), pnt.Z())) + return out + + +def _canonical_revolved_leaf(lf, origin, axis, tol): + """Say a two-section full-turn profile as the `TGeoCone` or `TGeoTube` it is, else a `TGeoPcon`. + + Returns `(leaf, recogniser tag)`. + """ + p = lf["params"] + z, rmin, rmax = p["z"], p["rmin"], p["rmax"] + if len(z) != 2 or abs(p["dphi"] - 360.0) > 1.0e-9: + return lf, "revolved-pcon" + # TGeoTube and TGeoCone are centred on their own frame, so the frame's origin moves to the + # middle of the section pair; the axis and the reference x are unchanged. + frame = dict(lf["frame"]) + frame["origin"] = [float(c) for c in _add(origin, _scale(axis, 0.5 * (z[0] + z[1])))] + dz = 0.5 * (z[1] - z[0]) + if abs(rmin[0] - rmin[1]) <= tol and abs(rmax[0] - rmax[1]) <= tol: + return _leaf("TGeoTube", {"rmin": 0.5 * (rmin[0] + rmin[1]), + "rmax": 0.5 * (rmax[0] + rmax[1]), "dz": dz}, + frame), "revolved-tube" + return _leaf("TGeoCone", {"dz": dz, "rmin1": rmin[0], "rmax1": rmax[0], + "rmin2": rmin[1], "rmax2": rmax[1]}, + frame), "revolved-cone" + + +def _match_revolved(solid, records, clusters, caps, wedges, tol, diag): + """One axis cluster, any number of z sections: a `TGeoPcon`.""" + cl = clusters[0] + cap = caps[0] + wedge = wedges[0] + + # A coordinate axis points the positive way, so a part on the global z gets an identity frame. + axis = cl["dir"] + snapped = _snap_to_coordinate_axis(axis) + if snapped is not None and snapped[1] < 0.0: + axis = _scale(axis, -1.0) + # The axial origin is the perpendicular foot from the part origin: a property of the axis. + origin = _sub(cl["loc"], _scale(axis, _dot(cl["loc"], axis))) + + spans = [] + for member in cl["members"]: + t0, t1, r0, r1 = _axial_extent(member, axis, origin) + if t1 - t0 <= tol: + raise Declined("a lateral face has no axial extent (a cone at its own apex?)") + if min(r0, r1) < -tol: + raise Declined("a lateral face reaches a negative radius") + spans.append((t0, t1, max(r0, 0.0), max(r1, 0.0), member)) + + t_caps = [_dot(_sub(c["p"], origin), axis) for c in cap] + levels = _merge_levels([(s[0], False) for s in spans] + [(s[1], False) for s in spans] + + [(t, True) for t in t_caps], tol) + if len(levels) < 2: + raise Declined("the axial faces span fewer than two distinct z levels") + + sections = [] # (z, rmin, rmax), in profile order + for k in range(len(levels) - 1): + lo, hi = levels[k], levels[k + 1] + mid = 0.5 * (lo + hi) + here = [s for s in spans if s[0] - tol <= mid <= s[1] + tol] + radii = _distinct_radii([_span_radius_at(s, mid) for s in here], tol) + if not radii: + raise Declined(f"no lateral face covers the z range [{lo:.6g}, {hi:.6g}]: " + "the solid is not one connected polycone") + if len(radii) > 2: + raise Declined(f"{len(radii)} distinct coaxial radii between z = {lo:.6g} and " + f"{hi:.6g}, expected 1 or 2") + ends = [] + for t in (lo, hi): + at = [_span_radius_at(s, t) for s in here] + ends.append((min(at) if len(radii) == 2 else 0.0, max(at))) + if k == 0: + sections.append((lo, ends[0][0], ends[0][1])) + elif (abs(sections[-1][1] - ends[0][0]) > tol + or abs(sections[-1][2] - ends[0][1]) > tol): + # A z-step: TGeo states it as two sections sharing one z, which is legal and is what + # the writer's STEP already contains as a cap annulus at that plane. + sections.append((lo, ends[0][0], ends[0][1])) + sections.append((hi, ends[1][0], ends[1][1])) + + # Every radial jump in the profile is a face of the solid, so it must be there. This is what + # separates a polycone from an open shell that merely looks like one. + for k, (z, rmin, rmax) in enumerate(sections): + needs_cap = (rmax - rmin > tol) if k in (0, len(sections) - 1) else \ + (k + 1 < len(sections) and abs(sections[k + 1][0] - z) <= tol) + if needs_cap and not any(abs(tc - z) <= tol for tc in t_caps): + raise Declined(f"the profile steps or ends at z = {z:.6g} with no cap plane there") + + if wedge: + normals = [] + for w in wedge: + if not any(_collinear(w["n"], n) for n in normals): + normals.append(w["n"]) + if len(normals) > 2: + raise Declined(f"{len(normals)} distinct half-planes through the axis: " + "not a single phi wedge") + + # phi comes only from laterals whose axis runs with the frame's (a flipped one mirrors it). + oriented = [m for m in cl["members"] if _parallel(m["d"], axis)] + # On a coordinate axis the frame is the identity and phi1 absolute; off it a lateral supplies x. + ref_x = None if _snap_to_coordinate_axis(axis) is not None else ( + oriented[0]["x"] if oriented else None) + frame = prim.frame_from_axis(origin, axis, ref_x) + if wedge: + if not oriented: + raise Declined("no lateral face runs with the axis, so the phi wedge cannot be read") + lo_phi, hi_phi = _phi_range(oriented, frame) + phi1, dphi = lo_phi, hi_phi - lo_phi + else: + phi1, dphi = 0.0, 360.0 + + z = [s[0] for s in sections] + rmin = [s[1] for s in sections] + rmax = [s[2] for s in sections] + try: + lf = _leaf("TGeoPcon", {"phi1": phi1, "dphi": dphi, "z": z, "rmin": rmin, + "rmax": rmax}, frame) + except Declined as bad: + raise Declined(f"the reconstructed profile is not a legal TGeoPcon: {bad}") from None + + # The gap is measured on the rebuilt profile, against the ring build_occ will revolve. + profile = prim.pcon_profile_rz(lf["params"]) + if len(profile) < 3: + raise Declined("the reconstructed (r, z) profile has fewer than three corners") + samples = [] + for point in _solid_vertices(solid): + rel = _sub(point, origin) + zc = _dot(rel, axis) + samples.append((math.sqrt(max(_dot(rel, rel) - zc * zc, 0.0)), zc)) + for span in spans: + for f in _PROFILE_SAMPLES: + t = span[0] + f * (span[1] - span[0]) + samples.append((_span_radius_at(span, t), t)) + gap = _profile_gap(profile, samples) + scale = max(diag, 1.0) + if gap > REL_TOL * scale: + raise Declined(f"the boundary is {gap:.3g} cm off the reconstructed profile " + f"({gap / scale:.3g} of the part's {diag:.6g} cm diagonal, " + f"over {REL_TOL:.0e})") + + lf, tag = _canonical_revolved_leaf(lf, origin, axis, tol) + return _candidate("primitive", [lf], tag, + notes={"nz": len(z), "nCaps": len(cap), "nWedges": len(wedge), + "nLaterals": len(cl["members"]), + "profileGapCm": gap, "profileGapRelative": gap / scale}) + + +# ------------------------------------------------------------------------------------------ +# The prism family: an all-planar face graph -> Trd1 / Trd2 / Arb8 / Pgon / Xtru +# ------------------------------------------------------------------------------------------ +# +# The axis is an opposite plane pair no third plane shares, and the bottom cap's wire carries the +# corner order up the stack; `_point_set_gap` scores each proposal. + +# Prism templates are tried most specific first. The template loop is the outer one, so the class +# a solid is wins over the axis that happened to be enumerated first. +_PRISM_TEMPLATES = ("trd1", "trd2", "pgon", "arb8", "xtru") + + +def _face_wires(face): + """Ordered corner points of each wire of a planar face, the outer wire first.""" + from OCC.Core.BRep import BRep_Tool + from OCC.Core.BRepTools import BRepTools_WireExplorer, breptools + from OCC.Core.TopAbs import TopAbs_WIRE + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + outer = breptools.OuterWire(face) + found = [] + exp = TopExp_Explorer(face, TopAbs_WIRE) + while exp.More(): + wire = topods.Wire(exp.Current()) + exp.Next() + pts = [] + walk = BRepTools_WireExplorer(wire, face) + while walk.More(): + pnt = BRep_Tool.Pnt(walk.CurrentVertex()) + pts.append((pnt.X(), pnt.Y(), pnt.Z())) + walk.Next() + if pts: + found.append((not wire.IsSame(outer), pts)) + found.sort(key=lambda item: item[0]) + return [pts for _is_hole, pts in found] + + +def _solid_samples(solid): + """Every vertex and edge midpoint of the solid, in the part frame, read from its topology.""" + from OCC.Core.BRep import BRep_Tool + from OCC.Core.TopAbs import TopAbs_EDGE + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + out = list(_solid_vertices(solid)) + seen = set() + exp = TopExp_Explorer(solid, TopAbs_EDGE) + while exp.More(): + edge = topods.Edge(exp.Current()) + exp.Next() + # A degenerate edge -- a cone's apex, a sphere's pole -- has no 3D curve, and PyROOT's + # binding then returns a 2-tuple rather than the usual (curve, first, last). + span = BRep_Tool.Curve(edge) + if span is None or len(span) < 3 or span[0] is None: + continue + curve, first, last = span[0], span[1], span[2] + pnt = curve.Value(0.5 * (first + last)) + key = (round(pnt.X(), 9), round(pnt.Y(), 9), round(pnt.Z(), 9)) + if key in seen: + continue + seen.add(key) + out.append((pnt.X(), pnt.Y(), pnt.Z())) + return out + + +def _point_set_gap(a, b): + """The symmetric Hausdorff distance, in cm, between two point sets.""" + def one_way(u, v): + worst = 0.0 + for p in u: + best = float("inf") + for q in v: + d2 = ((p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2 + (p[2] - q[2]) ** 2) + if d2 < best: + best = d2 + if best == 0.0: + break + worst = max(worst, best) + return math.sqrt(worst) + return max(one_way(a, b), one_way(b, a)) + + +class _Corners: + """Nearest-corner lookup, so a corner reported twice by two faces is one corner.""" + + def __init__(self, points, tol): + self.points = list(points) + self.tol = tol + + def find(self, p): + best, best_d = None, self.tol + for i, q in enumerate(self.points): + d = _norm(_sub(p, q)) + if d <= best_d: + best, best_d = i, d + return best + + +def _prism_axis_candidates(records): + """Directions the part could be a stack of sections along. + + A cap pair is an opposite plane pair that no third plane shares. + """ + out = [] + for rec in records: + direction = _unit(rec["n"]) + same = [j for j, other in enumerate(records) if _collinear(other["n"], direction)] + if len(same) != 2: + continue + a, b = same + if _dot(records[a]["n"], records[b]["n"]) > 0.0: + continue + if _dot(records[a]["p"], direction) < _dot(records[b]["p"], direction): + direction = _scale(direction, -1.0) + snapped = _snap_to_coordinate_axis(direction) + if snapped is not None: + direction = _COORDINATE_AXES[snapped[0]] + if any(_collinear(direction, seen) for seen in out): + continue + out.append(direction) + # z, then y, then x, then anything else, so a part in its own frame reads back in that frame. + def order(direction): + snapped = _snap_to_coordinate_axis(direction) + return (1, 0) if snapped is None else (0, -snapped[0]) + out.sort(key=order) + return out + + +def _canonical_ring_start(ring): + """Rotate a ring to start at its lexicographically smallest corner. + + So the emitted corner lists depend on the geometry alone, not on the wire OCCT walked first. + """ + start = min(range(len(ring)), key=lambda i: (round(ring[i][0], 12), round(ring[i][1], 12), + round(ring[i][2], 12))) + return ring[start:] + ring[:start] + + +def _ring_signed_area(ring, ex, ey): + total = 0.0 + for i, a in enumerate(ring): + b = ring[(i + 1) % len(ring)] + total += _dot(a, ex) * _dot(b, ey) - _dot(b, ex) * _dot(a, ey) + return 0.5 * total + + +def _prism_sections(solid, records, axis, tol): + """`(levels, rings)` along `axis`: the ordered corner rings of every section. + + `rings[k]` is the list of wires at level `k` -- one for a solid section, two for the annular + section of a hollow `TGeoPgon` -- each in corner order and counterclockwise about `axis`, with + corner `i` of section `k` joined to corner `i` of section `k + 1`. + """ + corners = _Corners(_solid_vertices(solid), tol) + levels = _merge_levels([(_dot(v, axis), False) for v in corners.points], tol) + if len(levels) < 2: + raise Declined("every corner sits on one plane: the part has no extent along the axis") + + def level_of(point): + t = _dot(point, axis) + best = min(range(len(levels)), key=lambda k: abs(levels[k] - t)) + return best if abs(levels[best] - t) <= tol else None + + caps, sides = [], [] + for rec in records: + wires = _face_wires(rec["face"]) + if not wires: + raise Declined("a planar face has no wire") + seen = {level_of(p) for wire in wires for p in wire} + if None in seen: + raise Declined("a face corner sits on no section plane of the axis") + if len(seen) == 1: + caps.append((wires, seen.pop())) + elif len(seen) == 2 and max(seen) - min(seen) == 1 and len(wires) == 1: + sides.append((wires[0], min(seen))) + else: + raise Declined(f"a planar face spans sections {sorted(seen)} " + "and is neither a cap nor a single prism side") + if len(caps) != 2: + raise Declined(f"{len(caps)} face(s) lie wholly in one section plane, expected 2 caps") + caps.sort(key=lambda cap: cap[1]) + if caps[0][1] != 0 or caps[1][1] != len(levels) - 1: + raise Declined("the two caps are not the outermost sections") + if len(caps[0][0]) != len(caps[1][0]): + raise Declined(f"the caps carry {len(caps[0][0])} and {len(caps[1][0])} wires") + if len(caps[0][0]) > 2: + raise Declined(f"a cap has {len(caps[0][0])} wires: more than one hole is out of scope") + + # Keyed on the ordered corner pair, both directions: a wire's own direction is arbitrary. + step = {} + for wire, k in sides: + n = len(wire) + if n not in (3, 4): + raise Declined(f"a prism side has {n} corners, expected 3 or 4") + at = [level_of(p) for p in wire] + for a in range(n): + b = (a + 1) % n + if at[a] != k or at[b] != k: + continue + if n == 4: + up_b, up_a = wire[(a + 2) % n], wire[(a + 3) % n] + else: + up_a = up_b = wire[(a + 2) % n] + step[(k, corners.find(wire[a]), corners.find(wire[b]))] = (up_a, up_b) + + frame = prim.frame_from_axis((0.0, 0.0, 0.0), axis) + ex, ey = tuple(frame["x"]), tuple(frame["y"]) + rings = [[_canonical_ring_start( + list(wire) if _ring_signed_area(wire, ex, ey) > 0.0 else list(reversed(wire))) + for wire in caps[0][0]]] + for k in range(len(levels) - 1): + above = [] + for ring in rings[k]: + n = len(ring) + up = [None] * n + for i in range(n): + j = (i + 1) % n + key = (k, corners.find(ring[i]), corners.find(ring[j])) + if key in step: + up_i, up_j = step[key] + else: + key = (k, corners.find(ring[j]), corners.find(ring[i])) + if key not in step: + raise Declined(f"no prism side joins two corners of section {k}: " + "the sections do not stack") + up_j, up_i = step[key] + for pos, value in ((i, up_i), (j, up_j)): + if up[pos] is not None and _norm(_sub(up[pos], value)) > tol: + raise Declined("two prism sides disagree about a corner of the next " + "section") + up[pos] = value + above.append(up) + rings.append(above) + used = {corners.find(p) for level in rings for ring in level for p in ring} + distinct = {corners.find(p) for p in corners.points} + if used != distinct: + raise Declined(f"{len(distinct - used)} corner(s) of the solid lie on no section ring") + return levels, rings + + +def _ring_xy(ring, frame): + origin, ex, ey = tuple(frame["origin"]), tuple(frame["x"]), tuple(frame["y"]) + return [(_dot(_sub(p, origin), ex), _dot(_sub(p, origin), ey)) for p in ring] + + +def _in_plane_x_candidates(axis, ring): + """Reference x directions to try, coordinate axes first so an aligned part stays aligned.""" + out = [] + snapped = _snap_to_coordinate_axis(axis) + if snapped is not None: + for k in range(3): + if k != snapped[0]: + out.append(_COORDINATE_AXES[k]) + for i, a in enumerate(ring): + edge = _sub(ring[(i + 1) % len(ring)], a) + flat = _sub(edge, _scale(axis, _dot(edge, axis))) + if _norm(flat) > 1.0e-12: + out.append(_unit(flat)) + return out + + +def _prism_leaf_gap(leaf, samples): + """The one measured quantity: how far the proposal's boundary is from the solid's, in cm.""" + return _point_set_gap(prim.prism_samples(leaf), samples) + + +def _try_trd(levels, rings, axis, tol): + """A `TGeoTrd1` or `TGeoTrd2`: two rectangular sections sharing a centre line.""" + if len(levels) != 2 or any(len(level) != 1 for level in rings): + raise Declined("a TGeoTrd needs exactly two single-wire sections") + lower, upper = rings[0][0], rings[1][0] + if len(lower) != 4 or len(upper) != 4: + raise Declined(f"a TGeoTrd needs four corners per section, got " + f"{len(lower)} and {len(upper)}") + centre = _scale(_add(_centroid(lower), _centroid(upper)), 0.5) + dz = 0.5 * (levels[1] - levels[0]) + out = [] + for ref_x in _in_plane_x_candidates(axis, lower): + frame = prim.frame_from_axis(centre, axis, ref_x) + low, high = _ring_xy(lower, frame), _ring_xy(upper, frame) + dx1, dy1 = max(abs(p[0]) for p in low), max(abs(p[1]) for p in low) + dx2, dy2 = max(abs(p[0]) for p in high), max(abs(p[1]) for p in high) + if abs(dy1 - dy2) <= tol: + out.append(("rung2-trd1", "TGeoTrd1", + {"dx1": dx1, "dx2": dx2, "dy": 0.5 * (dy1 + dy2), "dz": dz}, frame)) + out.append(("rung2-trd2", "TGeoTrd2", + {"dx1": dx1, "dx2": dx2, "dy1": dy1, "dy2": dy2, "dz": dz}, frame)) + return out + + +def _try_arb8(levels, rings, axis, tol): + """A `TGeoArb8`: two four-corner sections, eight corners, stated as they are.""" + if len(levels) != 2 or any(len(level) != 1 for level in rings): + raise Declined("a TGeoArb8 needs exactly two single-wire sections") + lower, upper = rings[0][0], rings[1][0] + if len(lower) != 4 or len(upper) != 4: + raise Declined(f"a TGeoArb8 needs four corners per section, got " + f"{len(lower)} and {len(upper)}") + origin = _scale(axis, 0.5 * (levels[0] + levels[1])) + frame = prim.frame_from_axis(origin, axis, _prism_ref_x(axis)) + vertices = [] + for ring in (lower, upper): + for corner in _ring_xy(ring, frame): + vertices.extend([corner[0], corner[1]]) + return [("rung2-arb8", "TGeoArb8", + {"dz": 0.5 * (levels[1] - levels[0]), "vertices": vertices}, frame)] + + +def _try_pgon(levels, rings, axis, tol): + """A `TGeoPgon`: every section a regular polygon ring at the same set of angles. + + ROOT's rmin/rmax are apothem radii, so the corners sit at `r / cos(dseg / 2)`. + """ + frame = prim.frame_from_axis((0.0, 0.0, 0.0), axis, _prism_ref_x(axis)) + radial = [] + for level in rings: + here = [] + for ring in level: + for x, y in _ring_xy(ring, frame): + here.append((math.hypot(x, y), math.atan2(y, x))) + radial.append(here) + biggest = max((r for here in radial for r, _a in here), default=0.0) + if biggest <= tol: + raise Declined("the sections have no radial extent") + angle_tol = max(tol / biggest, ANG_TOL) + angles = _merge_angles([a for here in radial for r, a in here if r > tol], angle_tol) + if len(angles) < 2: + raise Declined(f"{len(angles)} distinct corner angle(s): not a polygon ring") + gaps = [(angles[(i + 1) % len(angles)] - angles[i]) % (2.0 * math.pi) + for i in range(len(angles))] + widest = max(range(len(gaps)), key=lambda i: gaps[i]) + if max(gaps) - min(gaps) <= angle_tol: + nedges, dphi = len(angles), 360.0 + phi1 = angles[0] + dseg = 2.0 * math.pi / nedges + else: + ordered = angles[widest + 1:] + angles[:widest + 1] + steps = [(ordered[i + 1] - ordered[i]) % (2.0 * math.pi) for i in range(len(ordered) - 1)] + if max(steps) - min(steps) > angle_tol: + raise Declined("the corner angles are not equally spaced: not a polygon ring") + dseg = sum(steps) / len(steps) + nedges = len(steps) + phi1 = ordered[0] + dphi = math.degrees(dseg * nedges) + half = math.cos(dseg / 2.0) + rmin, rmax = [], [] + for here in radial: + radii = _distinct_radii([r for r, _a in here if r > tol], tol) + if len(radii) > 2: + raise Declined(f"{len(radii)} distinct corner radii in one section, expected 1 or 2") + if not radii: + raise Declined("a section has no corner off the axis") + rmax.append(radii[-1] * half) + rmin.append(radii[0] * half if len(radii) == 2 else 0.0) + return [("rung2-pgon", "TGeoPgon", + {"phi1": math.degrees(phi1), "dphi": dphi, "nedges": nedges, + "z": list(levels), "rmin": rmin, "rmax": rmax}, frame)] + + +def _try_xtru(levels, rings, axis, tol): + """A `TGeoXtru`: one polygon, per section an offset and an isotropic scale.""" + if any(len(level) != 1 for level in rings): + raise Declined("a TGeoXtru section is one closed polygon, and this part's is not") + frame = prim.frame_from_axis((0.0, 0.0, 0.0), axis, _prism_ref_x(axis)) + sections = [_ring_xy(level[0], frame) for level in rings] + nv = len(sections[0]) + if any(len(s) != nv for s in sections): + raise Declined("the sections do not all carry the same number of corners") + base = sections[0] + centre0 = _centroid2(base) + spread = sum((p[0] - centre0[0]) ** 2 + (p[1] - centre0[1]) ** 2 for p in base) + if spread <= 0.0: + raise Declined("the polygon has no extent") + xoff, yoff, scale = [], [], [] + for section in sections: + centre = _centroid2(section) + num = sum((section[i][0] - centre[0]) * (base[i][0] - centre0[0]) + + (section[i][1] - centre[1]) * (base[i][1] - centre0[1]) + for i in range(nv)) + s = num / spread + if s <= 0.0: + raise Declined("a section scales to zero or turns the polygon inside out") + scale.append(s) + xoff.append(centre[0] - s * centre0[0]) + yoff.append(centre[1] - s * centre0[1]) + return [("rung2-xtru", "TGeoXtru", + {"x": [p[0] for p in base], "y": [p[1] for p in base], "z": list(levels), + "xoff": xoff, "yoff": yoff, "scale": scale}, frame)] + + +_PRISM_TRIES = {"trd1": _try_trd, "trd2": _try_trd, "arb8": _try_arb8, + "pgon": _try_pgon, "xtru": _try_xtru} + + +def _prism_ref_x(axis): + snapped = _snap_to_coordinate_axis(axis) + if snapped is not None: + return _COORDINATE_AXES[(snapped[0] + 1) % 3] + return None + + +def _centroid(points): + total = (0.0, 0.0, 0.0) + for p in points: + total = _add(total, p) + return _scale(total, 1.0 / len(points)) + + +def _centroid2(points): + return (sum(p[0] for p in points) / len(points), sum(p[1] for p in points) / len(points)) + + +def _merge_angles(values, tol): + """Distinct angles in [0, 2pi), merged within `tol`, the wrap included.""" + out = [] + for a in sorted(v % (2.0 * math.pi) for v in values): + if out and min(a - out[-1], (out[0] + 2.0 * math.pi) - a) <= tol: + continue + out.append(a) + while len(out) > 1 and (out[0] + 2.0 * math.pi) - out[-1] <= tol: + out.pop() + return out + + +def _match_prism(solid, records, tol, diag): + """One axis, a stack of planar sections: the `Trd1`/`Trd2`/`Pgon`/`Arb8`/`Xtru` family.""" + axes = _prism_axis_candidates(records) + if not axes: + raise Declined("no opposite plane pair that no third plane shares: no prism axis") + samples = _solid_samples(solid) + scale = max(diag, 1.0) + best_gap, best_tag = None, None + reasons = [] + sections = {} + proposals = {} + for template in _PRISM_TEMPLATES: + for index, axis in enumerate(axes): + if index not in sections: + try: + sections[index] = _prism_sections(solid, records, axis, tol) + except Declined as why: + sections[index] = None + reasons.append(str(why)) + if sections[index] is None: + continue + levels, rings = sections[index] + if (template, index) not in proposals: + try: + made = _PRISM_TRIES[template](levels, rings, axis, tol) + except Declined as why: + made = [] + reasons.append(f"as a {template}: {why}") + proposals[(template, index)] = [item for item in made + if item[0].endswith(template)] + for tag, kind, params, frame in proposals[(template, index)]: + try: + leaf = _leaf(kind, params, frame) + except Declined as bad: + reasons.append(f"as a {template}: not a legal {kind}: {bad}") + continue + gap = _prism_leaf_gap(leaf, samples) + if best_gap is None or gap < best_gap: + best_gap, best_tag = gap, tag + if gap <= REL_TOL * scale: + return _candidate( + "primitive", [leaf], tag, + notes={"nSections": len(levels), "nWires": len(rings[0]), + "nCorners": sum(len(r) for r in rings[0]), + "prismGapCm": gap, "prismGapRelative": gap / scale}) + if best_gap is not None: + raise Declined(f"the boundary is {best_gap:.3g} cm off the closest prism template " + f"({best_tag}, {best_gap / scale:.3g} of the part's {diag:.6g} cm " + f"diagonal, over {REL_TOL:.0e})") + raise Declined("; ".join(dict.fromkeys(reasons)) or "no prism template applies") + + +# ------------------------------------------------------------------------------------------ +# Tier 2: the two-cluster union +# ------------------------------------------------------------------------------------------ + +def _match_two_cluster_union(records, clusters, caps, wedges, tol): + if any(wedges[i] for i in range(len(clusters))): + raise Declined("a wedge plane in a two-cluster part is out of scope") + axes = [cl["dir"] for cl in clusters] + if _collinear(axes[0], axes[1]): + raise Declined("the two clusters are parallel: not the lug case") + leaves = [] + for i, cl in enumerate(clusters): + if cl["kinds"] != ["cylinder"]: + raise Declined(f"cluster {i} has lateral kinds {cl['kinds']}, expected cylinders only") + radii = _distinct_radii([m["r"] for m in cl["members"]], tol) + if len(radii) > 2: + raise Declined(f"cluster {i} has {len(radii)} distinct radii, expected 1 or 2") + rmin = radii[0] if len(radii) == 2 else 0.0 + rmax = radii[-1] + t0, t1 = cl["tmin"], cl["tmax"] + for cap in caps[i]: + t = _dot(_sub(cap["p"], cl["loc"]), cl["dir"]) + t0, t1 = min(t0, t), max(t1, t) + if t1 - t0 <= 0.0: + raise Declined(f"cluster {i} has no axial extent") + centre = _add(cl["loc"], _scale(cl["dir"], (t0 + t1) / 2.0)) + outer = [m for m in cl["members"] if abs(m["r"] - rmax) <= tol] + frame = prim.frame_from_axis(centre, cl["dir"], outer[0]["x"]) + leaves.append(_leaf("TGeoTube", {"rmin": rmin, "rmax": rmax, + "dz": (t1 - t0) / 2.0}, frame)) + return _candidate("union", leaves, "tier2-tube-union", + notes={"nCaps": [len(caps[i]) for i in range(len(clusters))]}) + + +# ------------------------------------------------------------------------------------------ +# The single cell: one intersection of the part's own halfspaces -> TGeoCompositeShape +# ------------------------------------------------------------------------------------------ +# +# A part with no trusted concave edge is one cell: one bounded leaf per carrier. With `B` the +# inflated box, every leaf satisfies `H_i n B == L_i n B`, so the fold is `(n H_i) n B`, the cell. + +# Halfspace leaves reach this fraction of the part diagonal past its box; small keeps bboxes tight. +_CELL_MARGIN = 0.25 + +# The budget on the boolean leaves a part may ship as, summed over its cells. +_PART_MAX_LEAVES = 64 + +# At most this many boundary samples per side feed the gap. The samples are strided rather than +# truncated so a part with many edges is still sampled all over. +_CELL_GAP_SAMPLES = 200 + + +def _stride(items, most): + if len(items) <= most: + return items + step = len(items) / float(most) + return [items[int(i * step)] for i in range(most)] + + +def _point_to_shape_distance(point, shape): + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex + from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape + from OCC.Core.gp import gp_Pnt + probe = BRepBuilderAPI_MakeVertex(gp_Pnt(*point)).Vertex() + dist = BRepExtrema_DistShapeShape(probe, shape) + dist.Perform() + if not dist.IsDone(): + return float("inf") + return dist.Value() + + +def _distance_tool(shape): + """`point -> distance to shape`, with one `BRepExtrema_DistShapeShape` whose S2 is loaded once.""" + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex + from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape + from OCC.Core.gp import gp_Pnt + tool = BRepExtrema_DistShapeShape() + tool.LoadS2(shape) + + def distance(point): + tool.LoadS1(BRepBuilderAPI_MakeVertex(gp_Pnt(*point)).Vertex()) + tool.Perform() + return tool.Value() if tool.IsDone() else float("inf") + return distance + + +def _original_samples(solid, cache): + """`_solid_samples` of the part itself, memoised on the per-solid cache.""" + hit = cache.get("samples") if cache is not None else None + if hit is not None and hit[0] is solid: + return hit[1] + samples = _solid_samples(solid) + if cache is not None: + cache["samples"] = (solid, samples) + return samples + + +def _boundary_gap(a, b, most=_CELL_GAP_SAMPLES, cache=None): + """Symmetric Hausdorff distance, in cm, from each solid's boundary samples to the OTHER's + boundary; unlike `_point_set_gap` it does not depend on how either curve is parametrised.""" + samples = (_original_samples(a, cache), _solid_samples(b)) + if not samples[0] or not samples[1]: + # One side has no boundary: the halfspaces have no common interior, so this is not one cell. + raise Declined("the proposal is empty: these carriers have no common interior, so the " + "part is not one cell") + worst = 0.0 + for points, other in ((samples[0], b), (samples[1], a)): + distance_to = _distance_tool(other) + for point in _stride(points, most): + distance = distance_to(point) + if not math.isfinite(distance): + # `BRepExtrema_DistShapeShape` gave up. That is a measurement that did not + # happen, not a measurement of zero, so it declines and says which. + raise Declined("OCCT could not measure a boundary sample against the " + "proposal, so the gap is unknown") + worst = max(worst, distance) + return worst + + +def _same_carrier(a, b, tol): + """Do two faces sit on the same oriented carrier surface?""" + if a["kind"] != b["kind"]: + return False + if a["kind"] == "plane": + return (_collinear(a["n"], b["n"]) and _dot(a["n"], b["n"]) > 0.0 + and abs(_dot(_sub(a["p"], b["p"]), a["n"])) <= tol) + if a["kind"] == "sphere": + return _norm(_sub(a["p"], b["p"])) <= tol and abs(a["r"] - b["r"]) <= tol + if a["kind"] == "torus": + # Pinned by its centre, its axis, and both radii; two tori of the same R on one axis but + # different tube radii are the barrel and the bore of a ply and must stay distinct. + return (_collinear(a["d"], b["d"]) and _norm(_sub(a["p"], b["p"])) <= tol + and abs(a["r"] - b["r"]) <= tol and abs(a["rt"] - b["rt"]) <= tol) + if not (_collinear(a["d"], b["d"]) and _on_axis(b["p"], a["p"], a["d"], tol)): + return False + if a["kind"] == "cylinder": + return abs(a["r"] - b["r"]) <= tol + # A cone is pinned by its apex and its half-angle; the reference radius is chart-dependent. + return (abs(abs(a["a"]) - abs(b["a"])) <= ANG_TOL + and _norm(_sub(_cone_apex(a), _cone_apex(b))) <= tol) + + +def _cone_apex(carrier): + slope = math.tan(carrier["a"]) + if abs(slope) < 1.0e-30: + return carrier["p"] + return _add(carrier["p"], _scale(carrier["d"], -carrier["r"] / slope)) + + +def _halfspace_carriers(solid, tol): + """The distinct oriented halfspaces of a solid's faces, with the material side of each. + + `census.halfspace_side` decides the side, on the Tier-0 carrier for a canonicalised face. + """ + from cadsupport import census + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_REVERSED + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.TopoDS import topods + + scale = _LazyScale(solid) + carriers = [] + exp = TopExp_Explorer(solid, TopAbs_FACE) + while exp.More(): + face = topods.Face(exp.Current()) + exp.Next() + ad = BRepAdaptor_Surface(face, True) + kind = census.SURFACE_TYPE_NAME.get(ad.GetType(), "other") + canonical = None + if kind not in ("plane", "cylinder", "cone", "sphere", "torus"): + canonical, gap = tier0.canonicalise(face, ad, scale.value) + if canonical is None: + how_far = ("" if gap is None else + f" (the nearest canonical surface it proposes is {gap:.3g} cm away, " + f"{gap / scale.value:.3g} of the part)") + raise Declined(f"a {kind} face is outside the single-cell emitter's " + f"carriers{how_far}") + kind = canonical["kind"] + rec = {"kind": kind, "side": None} + if canonical is not None: + rec.update({k: v for k, v in canonical.items() if k != "uv"}) + if kind == "plane" and face.Orientation() == TopAbs_REVERSED: + rec["n"] = _scale(rec["n"], -1.0) + elif kind == "plane": + axis = ad.Plane().Axis() + normal = _xyz(axis.Direction()) + if face.Orientation() == TopAbs_REVERSED: + normal = _scale(normal, -1.0) + rec.update(n=_unit(normal), p=_xyz(axis.Location())) + elif kind == "cylinder": + cy = ad.Cylinder() + rec.update(d=_unit(_xyz(cy.Axis().Direction())), p=_xyz(cy.Axis().Location()), + r=cy.Radius(), x=_xyz(cy.Position().XDirection())) + elif kind == "cone": + co = ad.Cone() + rec.update(d=_unit(_xyz(co.Axis().Direction())), p=_xyz(co.Axis().Location()), + r=co.RefRadius(), a=co.SemiAngle(), + x=_xyz(co.Position().XDirection())) + elif kind == "sphere": + sp = ad.Sphere() + rec.update(p=_xyz(sp.Location()), r=sp.Radius()) + else: + to = ad.Torus() + rec.update(d=_unit(_xyz(to.Axis().Direction())), p=_xyz(to.Position().Location()), + x=_xyz(to.Position().XDirection()), r=to.MajorRadius(), + rt=to.MinorRadius()) + rec["side"] = (tier0.carrier_side(face, ad, rec) if canonical is not None + else census.halfspace_side(face, ad, kind)) + if rec["side"] is None: + raise Declined(f"a {kind} face's material side could not be decided") + for existing in carriers: + if _same_carrier(existing, rec, tol): + if existing["side"] != rec["side"]: + raise Declined("one carrier bounds material on both sides: not one cell") + break + else: + carriers.append(rec) + if not carriers: + raise Declined("no faces to read halfspaces from") + return carriers + + +def _bbox_of(shape): + from OCC.Core.Bnd import Bnd_Box + from OCC.Core.BRepBndLib import brepbndlib + box = Bnd_Box() + brepbndlib.Add(shape, box) + box.SetGap(0.0) + return box.Get() + + +class _CellBox: + """The part's bounding box `B`, grown by `margin`; every leaf satisfies `H_i n B == L_i n B`.""" + + def __init__(self, solid, diag): + xmin, ymin, zmin, xmax, ymax, zmax = _bbox_of(solid) + self.centre = (0.5 * (xmin + xmax), 0.5 * (ymin + ymax), 0.5 * (zmin + zmax)) + self.corners = [(x, y, z) for x in (xmin, xmax) + for y in (ymin, ymax) for z in (zmin, zmax)] + self.margin = _CELL_MARGIN * max(diag, 1.0) + + def window(self, origin, direction): + """`[lo, hi]`, measured from `origin` along `direction`, that a leaf must span.""" + reach = [_dot(_sub(corner, origin), direction) for corner in self.corners] + return min(reach) - self.margin, max(reach) + self.margin + + +def _cell_leaf(carrier, box): + """One bounded native leaf covering this halfspace over the part's neighbourhood.""" + outside = carrier["side"] == "exterior" + if carrier["kind"] == "plane": + # A box whose +z face lies exactly on the carrier plane and whose body fills the material + # side. The frame's z points into the material, i.e. against the outward normal. + normal = carrier["n"] + into = _scale(normal, -1.0) + foot = _sub(box.centre, _scale(normal, _dot(_sub(box.centre, carrier["p"]), normal))) + oriented = prim.frame_from_axis(foot, into) + depth = max(box.window(foot, into)[1], box.margin) + half = [max(max(abs(_dot(_sub(c, foot), tuple(oriented[axis]))) for c in box.corners) + + box.margin, box.margin) for axis in ("x", "y")] + frame = dict(oriented) + frame["origin"] = [float(v) for v in _add(foot, _scale(into, 0.5 * depth))] + return _leaf("TGeoBBox", {"dx": half[0], "dy": half[1], "dz": 0.5 * depth}, + frame, outside) + if carrier["kind"] == "sphere": + # Already bounded: the halfspace is the ball itself, at its true radius. + return _leaf("TGeoSphere", {"rmin": 0.0, "rmax": carrier["r"]}, + prim.identity_frame(carrier["p"]), outside) + if carrier["kind"] == "torus": + # Bounded and exact: the halfspace within `rt` of the circle of radius R is the solid torus. + return _leaf("TGeoTorus", {"r": carrier["r"], "rmin": 0.0, "rmax": carrier["rt"], + "phi1": 0.0, "dphi": 360.0}, + prim.frame_from_axis(carrier["p"], carrier["d"], carrier["x"]), outside) + lo, hi = box.window(carrier["p"], carrier["d"]) + if carrier["kind"] == "cylinder": + frame = prim.frame_from_axis( + _add(carrier["p"], _scale(carrier["d"], 0.5 * (lo + hi))), carrier["d"], + carrier["x"]) + return _leaf("TGeoTube", {"rmin": 0.0, "rmax": carrier["r"], + "dz": 0.5 * (hi - lo)}, frame, outside) + # A cone's halfspace is r <= rref + u tan(a), which is empty beyond the apex, so clipping the + # window there loses nothing and keeps the second nappe out of the leaf. + slope = math.tan(carrier["a"]) + if abs(slope) < 1.0e-30: + raise Declined("a conical carrier with a zero half-angle") + apex = -carrier["r"] / slope + lo, hi = (max(lo, apex), hi) if slope > 0.0 else (lo, min(hi, apex)) + if hi - lo <= 0.0: + raise Declined("a conical carrier whose halfspace does not reach the part") + frame = prim.frame_from_axis(_add(carrier["p"], _scale(carrier["d"], 0.5 * (lo + hi))), + carrier["d"], carrier["x"]) + return _leaf("TGeoCone", {"dz": 0.5 * (hi - lo), "rmin1": 0.0, "rmin2": 0.0, + "rmax1": max(carrier["r"] + lo * slope, 0.0), + "rmax2": max(carrier["r"] + hi * slope, 0.0)}, + frame, outside) + + +def _fold_cell_leaves(carriers, box, tol): + """One leaf per carrier, except where several carriers already ARE a native primitive. + + A capped interior cylinder or cone is a `TGeoTube` / `TGeoCone`, and six planes in three + perpendicular opposite pairs are a `TGeoBBox` (via `_match_box`); both groupings are exact. + """ + planes = [c for c in carriers if c["kind"] == "plane"] + axials = [c for c in carriers if c["kind"] in ("cylinder", "cone")] + consumed = set() + leaves = [] + + for carrier in axials: + if carrier["side"] != "interior": + continue + open_lo, open_hi = box.window(carrier["p"], carrier["d"]) + ends, span = {}, {} + for sign, key, opened in ((1.0, "hi", open_hi), (-1.0, "lo", open_lo)): + here = [pl for pl in planes if id(pl) not in consumed + and _collinear(pl["n"], carrier["d"]) + and sign * _dot(pl["n"], carrier["d"]) > 0.0] + ends[key] = here[0] if len(here) == 1 else None + # An end with no cap of its own is left open at the same extent an unfolded + # halfspace leaf would use, so folding one cap in is still exact. + span[key] = (_dot(_sub(ends[key]["p"], carrier["p"]), carrier["d"]) + if ends[key] is not None else opened) + if ends["hi"] is None and ends["lo"] is None: + continue + if span["hi"] - span["lo"] <= tol: + continue + folded = _capped_axial_leaf(carrier, span["lo"], span["hi"]) + if folded is None: + continue + for key in ("hi", "lo"): + if ends[key] is not None: + consumed.add(id(ends[key])) + consumed.add(id(carrier)) + leaves.append(folded) + + loose = [pl for pl in planes if id(pl) not in consumed] + if len(loose) == 6: + try: + as_box = _match_box(loose, tol) + except Declined: + as_box = None + if as_box is not None: + leaves.append(as_box["leaves"][0]) + consumed.update(id(pl) for pl in loose) + + for carrier in carriers: + if id(carrier) in consumed: + continue + leaves.append(_cell_leaf(carrier, box)) + if not leaves: + raise Declined("no halfspace leaf could be built") + return leaves + + +def _capped_axial_leaf(carrier, lo, hi): + """The bounded primitive an interior cylinder or cone plus its two caps already is.""" + frame = prim.frame_from_axis(_add(carrier["p"], _scale(carrier["d"], 0.5 * (lo + hi))), + carrier["d"], carrier["x"]) + dz = 0.5 * (hi - lo) + if carrier["kind"] == "cylinder": + return _leaf("TGeoTube", {"rmin": 0.0, "rmax": carrier["r"], "dz": dz}, frame) + slope = math.tan(carrier["a"]) + rmax1 = carrier["r"] + lo * slope + rmax2 = carrier["r"] + hi * slope + if min(rmax1, rmax2) < 0.0: + return None # the apex is between the caps: not one frustum + return _leaf("TGeoCone", {"dz": dz, "rmin1": 0.0, "rmin2": 0.0, + "rmax1": rmax1, "rmax2": rmax2}, frame) + + +def _leaf_bbox_volume(lf): + """A ranking key only: the leaf's own box, used to fold the tightest operand first.""" + p = lf["params"] + if lf["type"] == "TGeoBBox": + return 8.0 * p["dx"] * p["dy"] * p["dz"] + if lf["type"] == "TGeoTube": + return 8.0 * p["rmax"] ** 2 * p["dz"] + if lf["type"] == "TGeoCone": + return 8.0 * max(p["rmax1"], p["rmax2"]) ** 2 * p["dz"] + return 8.0 * p["rmax"] ** 3 + + +def _cell_leaves(solid, tol, diag, whole_part=True): + """The ordered halfspace leaves of one cell, or a `Declined` saying why it is not one. + + `whole_part` adds the part-level guards: an all-planar body belongs to the prism family, and a + one-carrier body is not a composite. + """ + carriers = _halfspace_carriers(solid, tol) + if whole_part and len(carriers) < 2: + raise Declined(f"{len(carriers)} distinct carrier(s): not a composite") + if whole_part and all(c["kind"] == "plane" for c in carriers): + # An all-planar body belongs to the prism family's templates, not to the cell emitter. + raise Declined(f"{len(carriers)} planar carriers and nothing else: an all-planar solid " + "belongs to the prism family, not to the cell emitter") + box = _CellBox(solid, diag) + + inside_leaves, outside_leaves = [], [] + for lf in _fold_cell_leaves(carriers, box, tol): + (outside_leaves if lf.get("outside") else inside_leaves).append(lf) + if not inside_leaves: + raise Declined("every carrier's material lies outside it: the cell is unbounded") + # Tightest first, so `TGeoIntersection::ComputeBBox`'s running overlap starts small and the + # emitted composite reports a bounding box of the part's own size. + inside_leaves.sort(key=_leaf_bbox_volume) + return inside_leaves + outside_leaves, carriers, outside_leaves + + +def _match_single_cell(solid, records, tol, diag, cache=None): + """One intersection cell of the part's own halfspaces: a `TGeoCompositeShape`.""" + from cadsupport import census + counts = census.edge_census(solid) + trusted = (counts["concave"] + counts["mixed"] + - counts["concaveNearTangential"] - counts["mixedNearTangential"]) + if trusted: + raise Declined(f"{trusted} trusted concave edge(s) of {counts['edges']}: the part is " + "more than one cell") + if counts["nonManifold"] or counts["error"]: + raise Declined(f"{counts['nonManifold']} non-manifold and {counts['error']} undecidable " + "edge(s): the cell test cannot be trusted here") + + leaves, carriers, outside_leaves = _cell_leaves(solid, tol, diag) + # The fold can collapse the cell into one native primitive, and the tag says whether it did. + if len(leaves) > _PART_MAX_LEAVES: + raise Declined(f"the cell is {len(leaves)} halfspaces wide, over the part budget of " + f"{_PART_MAX_LEAVES}: it would ship as a boolean tree that deep") + op = "primitive" if len(leaves) == 1 else "intersection" + cand = _candidate(op, leaves, + "cell-primitive" if op == "primitive" else "cell-intersection", + notes={"nCarriers": len(carriers), + "nOutside": len(outside_leaves), + "concaveEdgesTrusted": trusted, + "nLeaves": len(leaves), + "marginDiagonals": _CELL_MARGIN}) + + # The one measured quantity. Built here rather than left to the acceptance test because a + # proposal that does not even build is a decline, not a rejection. + try: + realised = prim.build_occ(cand) + except Exception as exc: # noqa: BLE001 + raise Declined(f"the cell did not build in OCCT: {exc}") from None + gap = _boundary_gap(solid, realised, cache=cache) + scale = max(diag, 1.0) + if gap > REL_TOL * scale: + raise Declined(f"the cell's boundary is {gap:.3g} cm from the part's " + f"({gap / scale:.3g} of the part's {diag:.6g} cm diagonal, " + f"over {REL_TOL:.0e})") + cand["notes"]["cellGapCm"] = gap + cand["notes"]["cellGapRelative"] = gap / scale + remember_realised(cache, cand, realised) + return cand + + +# ------------------------------------------------------------------------------------------ +# The union of cells: the flat two-level DNF +# ------------------------------------------------------------------------------------------ + +def _cell_decomposition(solid, scale, max_cells, cache=None): + """`cadsupport.decompose.split_into_cells` plus the four guards both cell matchers share. + + Memoised on `cache`, since the split is the most expensive step and the flat path reuses it. + """ + from cadsupport import decompose as decomp + key = ("split", max_cells) + if cache is not None and key in cache: + report = cache[key] + else: + report = decomp.split_into_cells(solid, max_cells=max_cells, scale=scale) + if cache is not None: + cache[key] = report + if report["stop"]: + raise Declined(f"the decomposition stopped: {report['stop']} after {report['splits']} " + f"split(s) into {len(report['pieces'])} cell(s)") + if report["unresolved"]: + raise Declined(f"{len(report['unresolved'])} piece(s) of {len(report['pieces']) + len(report['unresolved'])} " + "could not be cut at their own witness edge, so the decomposition is " + "incomplete") + if not report["volumeConserved"]: + raise Declined(f"the split moved {report['volumeDrift']:.3g} of the part's volume, over " + f"{decomp.VOLUME_REL_TOL:.0e}: OCCT's splitter did not conserve it and " + "the decomposition is not the part") + return report + + +def _match_union_of_cells(solid, records, tol, diag, max_cells=None, max_leaves=None, + cache=None): + """The part decomposed into cells and emitted as their union. + + It declines when the decomposition hits a budget or loses volume, when a piece is not a + cell or the tree gets too wide, and when the realised union classifies a point differently. + """ + from cadsupport import decompose as decomp + scale = max(diag, 1.0) + max_cells = decomp.PART_MAX_CELLS if max_cells is None else max_cells + max_leaves = _PART_MAX_LEAVES if max_leaves is None else max_leaves + report = _cell_decomposition(solid, scale, max_cells, cache) + pieces = report["pieces"] + if len(pieces) < 2: + raise Declined(f"the decomposition is {len(pieces)} piece(s): not a union of cells") + + cells, total_leaves, n_carriers, n_outside = [], 0, 0, 0 + for index, piece in enumerate(pieces): + piece_diag = decomp.bbox_diagonal(piece) + try: + leaves, carriers, outside = _cell_leaves(piece, tol, piece_diag, whole_part=False) + except Declined as declined: + raise Declined(f"cell {index + 1} of {len(pieces)}: {declined}") from None + total_leaves += len(leaves) + n_carriers += len(carriers) + n_outside += len(outside) + if total_leaves > max_leaves: + raise Declined(f"{len(pieces)} cells of {total_leaves}+ halfspaces in total, over " + f"the part budget of {max_leaves}: it would ship as a boolean tree " + "that wide") + cells.append(_cell(len(leaves), leaves, index, len(pieces))) + + cand = _union_of_cells(cells, "cells-union", + notes={"nCells": len(cells), + "nComponents": report["components"], + "nSplits": report["splits"], + "nLeaves": total_leaves, + "nCarriers": n_carriers, + "nOutside": n_outside, + "cellLeaves": [len(c["leaves"]) for c in cells], + "volumeDriftRelative": report["volumeDrift"], + "marginDiagonals": _CELL_MARGIN}) + gap, realised = _measured_gap(solid, cand, diag, "the union of cells", cache) + + # The containment corroboration sees a bad piece or cell that the gap and the volume both miss. + disagreements, scored, worst = accept_module().contains_disagreements( + solid, realised, accept_module().model_tolerance_cm(solid)) + if disagreements: + raise Declined(f"the union of cells disagrees with the part about {disagreements} of " + f"{scored} classified point(s), the farthest {worst:.3g} cm from the " + "part's boundary: the decomposition is not the part, whatever the " + "symmetric difference says") + cand["notes"]["cellGapCm"] = gap + cand["notes"]["cellGapRelative"] = gap / scale + cand["notes"]["containsScored"] = scored + remember_realised(cache, cand, realised) + return cand + + +def accept_module(): + """`cadsupport.accept`, imported lazily to keep this module importable without pythonOCC.""" + from cadsupport import accept + return accept + + +def _cell(n_leaves, leaves, index, total): + """`primitives.cell`, with an illegal cell turned into a decline naming which cell it was.""" + try: + return prim.cell("primitive" if n_leaves == 1 else "intersection", leaves) + except prim.InvalidDescription as illegal: + raise Declined(f"cell {index + 1} of {total}: {illegal}") from None + except ValueError as illegal: + raise Declined(f"cell {index + 1} of {total}: {illegal}") from None + + +def _union_of_cells(cells, recogniser, notes=None): + """`primitives.union_of_cells`, with an illegal description turned into a decline.""" + try: + return prim.union_of_cells(cells, recogniser, notes) + except prim.InvalidDescription as illegal: + raise Declined(str(illegal)) from None + except ValueError as illegal: + raise Declined(str(illegal)) from None + + +# ------------------------------------------------------------------------------------------ +# The flat DNF: the same cells, shipped as halfspaces in `o2::cad::O2FlatCSG` +# ------------------------------------------------------------------------------------------ + +# The flat path's budgets, on the sidecar and the box build rather than on a tree's width. +_PART_MAX_FLAT_CELLS = 256 +_PART_MAX_FLAT_HALFSPACES = 1024 + +# How far a cell's declared box is grown past the piece's own box, well above the gap band. A +# cell reaching past its box is larger than the part: never widen the flat cell box, refuse the part. +_FLAT_BOX_MARGIN = 1.0e-3 + + +def _flat_cell_box(piece, margin): + """The outer bound of one cell: the piece's own OCCT box, grown by `margin` on every axis.""" + xmin, ymin, zmin, xmax, ymax, zmax = _bbox_of(piece) + lo = [xmin - margin, ymin - margin, zmin - margin] + hi = [xmax + margin, ymax + margin, zmax + margin] + if not all(math.isfinite(v) for v in lo + hi): + raise Declined("the cell's bounding box is not finite, so nothing can say where the " + "cell ends") + return lo, hi + + +# Outward probe offsets, in box diagonals, for the check that the declared box holds the cell. +_FLAT_BOX_PROBE_GRID = 3 +_FLAT_BOX_PROBE_OFFSETS = (1.0e-6, 0.25, 1.0, 4.0) + + +def _flat_box_holds_cell(blocks, lo, hi): + """`Declined` when a sampled point OUTSIDE the declared box is still inside the cell. + + Such a cell is bigger than the part, and the fix is never a bigger box. A grid on each face, + pushed out near and far, finds a cell running off through a face; it proves no containment. + """ + from cadsupport import flat + span = [hi[i] - lo[i] for i in range(3)] + reach = math.sqrt(sum(v * v for v in span)) + if not (reach > 0.0): + raise Declined("the cell's bounding box has no extent, so no box can hold the cell") + steps = [[lo[i] + span[i] * (k + 0.5) / _FLAT_BOX_PROBE_GRID + for k in range(_FLAT_BOX_PROBE_GRID)] for i in range(3)] + for axis in range(3): + u, v = (axis + 1) % 3, (axis + 2) % 3 + for face, base in ((0, lo[axis]), (1, hi[axis])): + direction = -1.0 if face == 0 else 1.0 + for offset in _FLAT_BOX_PROBE_OFFSETS: + for su in steps[u]: + for sv in steps[v]: + point = [0.0, 0.0, 0.0] + point[axis] = base + direction * offset * reach + point[u], point[v] = su, sv + if flat.flat_contains(blocks, tuple(point)): + raise Declined( + f"the cell's halfspaces still hold {offset * reach:.3g} cm past " + f"the CAD piece's own bounding box on axis {axis}: they do not " + "close the cell up, so the cell is LARGER than the part. Widening " + "the declared box would ship that phantom material and would make " + "O2FlatCSG disagree with its own _Loop twins; the part is refused " + "instead") + + +def _match_flat_cells(solid, records, tol, diag, max_cells=None, max_halfspaces=None, cache=None): + """The same decomposition, emitted as signed implicit halfspaces for `o2::cad::O2FlatCSG`. + + Its own obligations: an outer cell bounding box, `flat.check_cell_box` per cell with the box + that is written, and the containment corroboration. It runs only after the union path declines. + """ + from cadsupport import decompose as decomp, flat + scale = max(diag, 1.0) + max_cells = _PART_MAX_FLAT_CELLS if max_cells is None else max_cells + max_halfspaces = _PART_MAX_FLAT_HALFSPACES if max_halfspaces is None else max_halfspaces + report = _cell_decomposition(solid, scale, decomp.PART_MAX_CELLS, cache) + pieces = report["pieces"] + if len(pieces) > max_cells: + raise Declined(f"{len(pieces)} cells, over the flat part budget of {max_cells} cells: " + "the sidecar and the sub-cell box build are sized to what a part is, not " + "to what OCCT can split") + margin = _FLAT_BOX_MARGIN * scale + + cells, occ_cells, total_blocks, n_carriers, n_outside = [], [], 0, 0, 0 + # A one-piece decomposition is the whole part, so it keeps the whole-part guards. + whole_part = len(pieces) == 1 + for index, piece in enumerate(pieces): + piece_diag = decomp.bbox_diagonal(piece) + try: + leaves, carriers, outside = _cell_leaves(piece, tol, piece_diag, + whole_part=whole_part) + lo, hi = _flat_cell_box(piece, margin) + # The obligation, with the SAME box that is written to the sidecar and SetCellBBox. + flat.check_cell_box(carriers, lo, hi) + blocks = flat.blocks_from_carriers(carriers) + _flat_box_holds_cell(blocks, lo, hi) + except Declined as declined: + raise Declined(f"cell {index + 1} of {len(pieces)}: {declined}") from None + total_blocks += len(blocks) + n_carriers += len(carriers) + n_outside += len(outside) + if total_blocks > max_halfspaces: + raise Declined(f"{len(pieces)} cells of {total_blocks}+ halfspaces in total, over " + f"the flat part budget of {max_halfspaces} halfspaces") + cells.append({"blocks": blocks, "volume": decomp_volume(piece), + "lo": lo, "hi": hi}) + occ_cells.append(_cell(len(leaves), leaves, index, len(pieces))) + + cand = _flat_cells(cells, "flat-cells", + notes={"nCells": len(cells), + "nComponents": report["components"], + "nSplits": report["splits"], + "nHalfspaces": total_blocks, + "nCarriers": n_carriers, + "nOutside": n_outside, + "cellHalfspaces": [len(c["blocks"]) for c in cells], + "cellBoxMarginCm": margin, + "volumeDriftRelative": report["volumeDrift"], + "occCells": occ_cells}) + gap, realised = _measured_gap(solid, cand, diag, "the flat cells", cache) + disagreements, scored, worst = accept_module().contains_disagreements( + solid, realised, accept_module().model_tolerance_cm(solid)) + if scored <= 0: + # A corroboration that scored no point corroborated nothing: decline. + raise Declined("the containment corroboration scored no point at all, so it corroborated " + "nothing: the flat cells are not admitted on an empty measurement") + if disagreements: + raise Declined(f"the flat cells disagree with the part about {disagreements} of " + f"{scored} classified point(s), the farthest {worst:.3g} cm from the " + "part's boundary: the decomposition is not the part, whatever the " + "symmetric difference says") + cand["notes"]["cellGapCm"] = gap + cand["notes"]["cellGapRelative"] = gap / scale + cand["notes"]["containsScored"] = scored + remember_realised(cache, cand, realised) + return cand + + +def decomp_volume(piece): + """The cell's volume, from OCCT's `GProp` on its piece; `O2FlatCSG::Capacity()` sums these.""" + from cadsupport.census import volume_of + return abs(volume_of(piece)) + + +def _flat_cells(cells, recogniser, notes=None): + """`primitives.flat_cells`, with an illegal description turned into a decline.""" + try: + return prim.flat_cells(cells, recogniser, notes) + except prim.InvalidDescription as illegal: + raise Declined(str(illegal)) from None + except ValueError as illegal: + raise Declined(str(illegal)) from None + + +# ------------------------------------------------------------------------------------------ +# Tier 1: the elliptic cylinder +# ------------------------------------------------------------------------------------------ + + +def _eltu_frame(centre, axis, major_dir, minor_dir, major, minor): + """The frame and the `(a, b)` pair to state an elliptic cylinder in. + + The labelling whose frame is the identity is preferred, so a square part gets the source's own + `(a, b)`; off-axis the major axis takes `x`. + """ + options = [(major_dir, major, minor), (_scale(major_dir, -1.0), major, minor), + (minor_dir, minor, major), (_scale(minor_dir, -1.0), minor, major)] + fallback = None + for ref_x, a, b in options: + frame = prim.frame_from_axis(centre, axis, ref_x) + if prim.frame_is_identity_rotation(frame): + return frame, a, b + if fallback is None: + fallback = (frame, a, b) + return fallback + + +def _match_eltu(solid, records, tol, diag, cache=None): + """One extruded-ellipse lateral between two perpendicular caps: a `TGeoEltu`.""" + laterals = [r for r in records if r["kind"] == "eltu"] + planes = [r for r in records if r["kind"] == "plane"] + other = [r for r in records if r["kind"] not in ("eltu", "plane")] + if other: + kinds = sorted({r["kind"] for r in other}) + raise Declined(f"an elliptic lateral together with {kinds} is not a whole TGeoEltu") + axis = laterals[0]["d"] + snapped = _snap_to_coordinate_axis(axis) + if snapped is not None and snapped[1] < 0.0: + axis = _scale(axis, -1.0) + for lateral in laterals[1:]: + if not (_collinear(lateral["d"], axis) + and abs(lateral["a"] - laterals[0]["a"]) <= tol + and abs(lateral["b"] - laterals[0]["b"]) <= tol + and _on_axis(lateral["p"], laterals[0]["p"], axis, tol)): + raise Declined(f"{len(laterals)} elliptic laterals that are not one carrier") + if len(planes) != 2: + raise Declined(f"{len(planes)} planar face(s) on an elliptic lateral, expected 2 caps") + for plane in planes: + if not _collinear(plane["n"], axis): + raise Declined("a planar face of an elliptic cylinder is not perpendicular to it") + caps = sorted(_dot(_sub(plane["p"], laterals[0]["p"]), axis) for plane in planes) + if caps[1] - caps[0] <= tol: + raise Declined("the two caps of an elliptic cylinder are coincident") + centre = _add(laterals[0]["p"], _scale(axis, 0.5 * (caps[0] + caps[1]))) + frame, a, b = _eltu_frame(centre, axis, laterals[0]["x"], laterals[0]["y"], + laterals[0]["a"], laterals[0]["b"]) + try: + lf = _leaf("TGeoEltu", {"a": a, "b": b, "dz": 0.5 * (caps[1] - caps[0])}, frame) + except Declined as bad: + raise Declined(f"the elliptic cylinder is not a legal TGeoEltu: {bad}") from None + cand = _candidate("primitive", [lf], "tier1-eltu", + notes={"semiAxisRatio": min(a, b) / max(a, b)}) + gap, realised = _measured_gap(solid, cand, diag, "the elliptic cylinder", cache) + cand["notes"]["eltuGapCm"] = gap + cand["notes"]["eltuGapRelative"] = gap / max(diag, 1.0) + remember_realised(cache, cand, realised) + return cand + + +# ------------------------------------------------------------------------------------------ +# Tier 1: the whole torus +# ------------------------------------------------------------------------------------------ + + +def _match_torus(solid, records, tol, diag, cache=None): + """All-toroidal laterals on one axis, optionally phi-cut: a `TGeoTorus`.""" + tori = [r for r in records if r["kind"] == "torus"] + planes = [r for r in records if r["kind"] == "plane"] + other = [r for r in records if r["kind"] not in ("torus", "plane")] + if not tori: + raise Declined("no toroidal face to key on") + if other: + kinds = sorted({r["kind"] for r in other}) + raise Declined(f"a torus together with {kinds} is not a whole torus") + + axis = _unit(tori[0]["d"]) + snapped = _snap_to_coordinate_axis(axis) + if snapped is not None and snapped[1] < 0.0: + axis = _scale(axis, -1.0) + centre = tori[0]["p"] + for t in tori[1:]: + if not _collinear(t["d"], axis): + raise Declined("the toroidal faces do not share one axis") + if _norm(_sub(t["p"], centre)) > tol: + raise Declined("the toroidal faces are not concentric") + if abs(t["r"] - tori[0]["r"]) > tol: + raise Declined(f"{len(tori)} toroidal faces with different major radii") + + minors = _distinct_radii([t["rt"] for t in tori], tol) + if len(minors) > 2: + raise Declined(f"{len(minors)} distinct tube radii on one torus, expected 1 or 2") + rmin = minors[0] if len(minors) == 2 else 0.0 + rmax = minors[-1] + + for plane in planes: + if not (_perpendicular(plane["n"], axis) + and abs(_dot(_sub(plane["p"], centre), plane["n"])) <= tol): + raise Declined("a planar face of a torus is not a wedge through its axis") + if planes: + normals = [] + for plane in planes: + if not any(_collinear(plane["n"], n) for n in normals): + normals.append(plane["n"]) + if len(normals) > 2: + raise Declined(f"{len(normals)} distinct half-planes through the torus axis") + + # phi is read only off tori whose own axis runs *with* the frame's, for the reason + # `_match_revolved` gives: a flipped carrier axis parametrises phi the other way round. + oriented = [t for t in tori if _parallel(t["d"], axis) and abs(t["rt"] - rmax) <= tol] + ref_x = None if _snap_to_coordinate_axis(axis) is not None else ( + oriented[0]["x"] if oriented else None) + frame = prim.frame_from_axis(centre, axis, ref_x) + if planes: + if not oriented: + raise Declined("no toroidal face runs with the axis, so the phi wedge cannot be read") + lo_phi, hi_phi = _phi_range(oriented, frame) + phi1, dphi = lo_phi, hi_phi - lo_phi + else: + phi1, dphi = 0.0, 360.0 + + try: + lf = _leaf("TGeoTorus", {"r": tori[0]["r"], "rmin": rmin, "rmax": rmax, + "phi1": phi1, "dphi": dphi}, frame) + except Declined as bad: + raise Declined(f"the torus is not a legal TGeoTorus: {bad}") from None + + cand = _candidate("primitive", [lf], "tier1-torus", + notes={"nTori": len(tori), "nWedges": len(planes)}) + gap, realised = _measured_gap(solid, cand, diag, "the torus", cache) + cand["notes"]["torusGapCm"] = gap + cand["notes"]["torusGapRelative"] = gap / max(diag, 1.0) + remember_realised(cache, cand, realised) + return cand + + +def _measured_gap(solid, cand, diag, what, cache=None): + """The one measured quantity for a whole-part proposal, in cm over the part's diagonal. + + `_boundary_gap` against the realised proposal. Returns `(gap, realised proposal)`. + """ + try: + realised = prim.build_occ(cand) + except Exception as exc: # noqa: BLE001 + raise Declined(f"{what} did not build in OCCT: {exc}") from None + gap = _boundary_gap(solid, realised, cache=cache) + scale = max(diag, 1.0) + if gap > REL_TOL * scale: + raise Declined(f"{what}'s boundary is {gap:.3g} cm from the part's " + f"({gap / scale:.3g} of the part's {diag:.6g} cm diagonal, " + f"over {REL_TOL:.0e})") + return gap, realised + + +# ------------------------------------------------------------------------------------------ +# entry point +# ------------------------------------------------------------------------------------------ + +def _with_tier0_notes(cand, records): + """Record on the candidate what Tier-0 canonicalisation the part rested on, if any. + + Only a part with a canonicalised face gets notes; other candidates keep their recorded form. + """ + canonical = [r for r in records if r.get("canonicalised")] + if cand is None or not canonical: + return cand + cand["notes"]["tier0Faces"] = len(canonical) + cand["notes"]["tier0WorstGapCm"] = max(r["tier0GapCm"] for r in canonical) + cand["notes"]["tier0WorstGapRelative"] = max(r["tier0GapRelative"] for r in canonical) + return cand + + +def _face_analysis(solid, cache): + """`(records, reason, diag)` of a solid, memoised on the per-solid cache.""" + if cache is not None and "faces" in cache: + return cache["faces"] + records, reason = _face_records(solid) + diag = _bbox_diagonal(solid) if records is not None else None + if cache is not None: + cache["faces"] = (records, reason, diag) + return records, reason, diag + + +def remember_realised(cache, cand, shape): + """Keep a proposal's OCCT realisation on the per-solid cache; holding `cand` keeps its id unique.""" + if cache is not None: + cache[("occ", id(cand))] = (cand, shape) + + +def realised_for(cache, cand): + """The OCCT realisation already built for `cand`, or None.""" + hit = cache.get(("occ", id(cand))) if cache is not None else None + return hit[1] if hit is not None and hit[0] is cand else None + + +def recognise(solid, cache=None): + """Propose a CSG description for one leaf solid in cm. Returns (candidate|None, reason). + + `cache` is the per-solid memo `emit.process_solid` shares with its retries. + """ + cache = {} if cache is None else cache + records, reason, diag = _face_analysis(solid, cache) + if records is None: + return None, reason + tol = REL_TOL * max(diag, 1.0) + cand, reason = _cascade(solid, records, tol, diag, cache) + return _with_tier0_notes(cand, records), reason + + +def _cascade(solid, records, tol, diag, cache): + """The matcher ladder itself, in order of increasing generality.""" + try: + if any(r["kind"] == "eltu" for r in records): + # Same reasoning as the torus below: an extruded ellipse was a free-form decline + # before this rung, so no matcher underneath ever saw one. + return _match_eltu(solid, records, tol, diag, cache), None + if any(r["kind"] == "torus" for r in records): + # A toroidal face goes straight to the torus template, then to the cell emitter. + return _match_torus(solid, records, tol, diag, cache), None + try: + cand = _match_box(records, tol) + except Declined as box_declined: + # All planar and not a box: the prism family. + try: + return _match_prism(solid, records, tol, diag), None + except Declined as prism_declined: + raise Declined(f"{box_declined}; as a prism: {prism_declined}") from None + if cand is not None: + return cand, None + cand = _match_sphere(records, tol) + if cand is not None: + return cand, None + clusters = _cluster_axial(records, tol) + if not clusters: + raise Declined("no cylindrical or conical face to key on") + # Counted before the planes are assigned, so an out-of-scope part is reported by structure. + if len(clusters) > 2: + raise Declined(f"{len(clusters)} axis clusters: beyond the recogniser's scope " + "(Tier 3 territory, deliberately not built)") + caps, wedges = _split_planes(records, clusters, tol) + if len(clusters) == 1: + # The revolved matcher runs strictly *after* the whole-part primitive one and only on + # what that declines, so no part that is recognised today changes tier or candidate. + try: + return _match_axial_primitive(records, clusters, caps, wedges, tol), None + except Declined as primitive_declined: + try: + return _match_revolved(solid, records, clusters, caps, wedges, + tol, diag), None + except Declined as revolved_declined: + raise Declined(f"{primitive_declined}; as a revolved profile: " + f"{revolved_declined}") from None + return _match_two_cluster_union(records, clusters, caps, wedges, tol), None + except Declined as declined: + # Everything above has declined, which is exactly the condition the single-cell emitter + # runs under: no part recognised by any earlier matcher can reach it. + try: + return _match_single_cell(solid, records, tol, diag, cache), None + except Declined as cell_declined: + # The decomposition runs only on what the single cell declines. + try: + return _match_union_of_cells(solid, records, tol, diag, cache=cache), None + except Declined as union_declined: + # The flat path runs only after the union path declines, on the shared split. + try: + return _match_flat_cells(solid, records, tol, diag, cache=cache), None + except Declined as flat_declined: + return None, (f"{declined}; as a single cell: {cell_declined}; as a union of " + f"cells: {union_declined}; as flat cells: {flat_declined} " + f"[{_structure(records, tol)}]") + + +def recognise_single_cell(solid, cache=None): + """Propose one intersection cell for a solid, skipping every earlier matcher. + + For `emit.process_solid`'s retry after a rejection. Returns `(candidate|None, reason)`. + """ + records, reason, diag = _face_analysis(solid, cache) + if records is None: + return None, reason + tol = REL_TOL * max(diag, 1.0) + try: + return _with_tier0_notes(_match_single_cell(solid, records, tol, diag, cache), + records), None + except Declined as declined: + return None, f"{declined} [{_structure(records, tol)}]" + + +def recognise_union_of_cells(solid, max_cells=None, max_leaves=None, cache=None): + """Propose a union of cells for a solid, skipping every matcher above it. + + For `emit.process_solid`'s retry after a rejection. Returns `(candidate|None, reason)`. + """ + records, reason, diag = _face_analysis(solid, cache) + if records is None: + return None, reason + tol = REL_TOL * max(diag, 1.0) + try: + return _with_tier0_notes( + _match_union_of_cells(solid, records, tol, diag, max_cells, max_leaves, cache), + records), None + except Declined as declined: + return None, f"{declined} [{_structure(records, tol)}]" + + +def recognise_flat_cells(solid, max_cells=None, max_halfspaces=None, cache=None): + """Propose a flat halfspace DNF for a solid, skipping every matcher above it. + + For `emit.process_solid`'s retry after a rejection. Returns `(candidate|None, reason)`. + """ + records, reason, diag = _face_analysis(solid, cache) + if records is None: + return None, reason + tol = REL_TOL * max(diag, 1.0) + try: + return _with_tier0_notes( + _match_flat_cells(solid, records, tol, diag, max_cells, max_halfspaces, cache), + records), None + except Declined as declined: + return None, f"{declined} [{_structure(records, tol)}]" + + +def recognise_revolved(solid, cache=None): + """Propose a revolved profile for a solid, skipping the whole-part matchers entirely. + + For `emit.process_solid`'s retry after a rejection. Returns `(candidate|None, reason)`. + """ + records, reason, diag = _face_analysis(solid, cache) + if records is None: + return None, reason + tol = REL_TOL * max(diag, 1.0) + try: + clusters = _cluster_axial(records, tol) + if len(clusters) != 1: + raise Declined(f"{len(clusters)} axis cluster(s): not a single revolved profile") + caps, wedges = _split_planes(records, clusters, tol) + return _with_tier0_notes( + _match_revolved(solid, records, clusters, caps, wedges, tol, diag), records), None + except Declined as declined: + return None, f"{declined} [{_structure(records, tol)}]" + + +def _structure(records, tol): + """A one-line structural summary, appended to every decline so the reason is readable.""" + kinds = {} + for rec in records: + kinds[rec["kind"]] = kinds.get(rec["kind"], 0) + 1 + try: + n_clusters = len(_cluster_axial(records, tol)) + except Exception: # noqa: BLE001 + n_clusters = -1 + breakdown = ", ".join(f"{n} {k}" for k, n in sorted(kinds.items())) + canonical = [r for r in records if r.get("canonicalised")] + tier0_note = "" + if canonical: + worst = max(r["tier0GapRelative"] for r in canonical) + tier0_note = (f"; {len(canonical)} canonicalised at a worst gap of {worst:.3g} " + "of the part") + return f"{len(records)} faces: {breakdown}; {n_clusters} axis cluster(s){tier0_note}" diff --git a/Detectors/CADSupport/tools/cadsupport/selftest_emit.py b/Detectors/CADSupport/tools/cadsupport/selftest_emit.py new file mode 100644 index 0000000000000..941a973be4f83 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/selftest_emit.py @@ -0,0 +1,2490 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""The emitter self-test behind `python3 -m cadsupport.emit --self-test`: its fixtures and recorded candidates.""" + +import functools +import json +import math +from pathlib import Path + +from cadsupport import accept, primitives as prim, recognise # noqa: E402 +from cadsupport.emit import (crosscheck_bbox, crosscheck_contains, process_solid, # noqa: E402 + write_shape_root) + +# The recorded candidates: structure exactly, floats within a tolerance, on every platform. +_RECORDED_CANDIDATES = Path(__file__).with_name("emit_selftest_candidates.json") +# The emitted description (leaf parameters and frames, cm) differs between platforms by ~1e-15. +_DESCRIPTION_TOLERANCE = 1.0e-12 +# The measured residues in `notes` (gaps and drifts near zero) differ by up to ~1e-9. +_NOTES_TOLERANCE = 1.0e-8 + +# The whole-part fixtures, recorded before the revolved matcher and the acceptance retry existed. +_WHOLE_PART_FIXTURES = ( + 'box', + 'solid cylinder', + 'tube', + 'tube segment', + 'cone', + 'sphere', + 'placed tube', + 'rod-and-eye (two-cluster union)', +) + +# The torus carrier and the elliptic cylinder; the two bellows shapes are what PIPE's plies reduce to. +_TORUS_ELTU_FIXTURES = ( + 'solid torus', + 'torus shell (a bellows ply)', + 'hollow torus wedge', + 'half a bellows ply', + 'elliptic cylinder, a > b', + 'elliptic cylinder, a < b', + 'elliptic cylinder with equal semi-axes', +) + +# The single cell. +_CELL_FIXTURES = ( + 'Steinmetz solid (two cylinders intersected)', + 'tube with a transverse window', + 'cylinder cut by an oblique plane', + 'cube with an axial through-hole', + 'cylinder with a milled flat', +) + +# The two-level DNF: the cells, their order and every leaf in them. +_UNION_OF_CELLS_FIXTURES = ( + 'a cylinder with a hexagonal collar', + 'two rods sharing no edge', + 'a torus with a cylinder through it', + 'three disjoint boxes', +) + +# Whole parts whose every carrier arrives Tier-0 canonicalised from a stored B-spline. +_TIER0_FIXTURES = ( + 'NURBS-encoded box', + 'NURBS-encoded solid cylinder', + 'NURBS-encoded tube segment', + 'NURBS-encoded cone', + 'NURBS-encoded sphere', + 'NURBS-encoded solid torus', + 'NURBS-encoded hollow torus wedge', + 'NURBS-encoded cube with an axial through-hole', +) + +# The prism family. +_PRISM_FIXTURES = ( + 'L-shaped plate', + 'hollow 8-edge polygon (TGeoPgon)', + 'hollow 48-edge polygon (TGeoPgon)', + 'Trd1 (slanted x faces)', + 'Trd1 (taper reversed)', + "Trd1 (TPC_IRB1's 0.5 % slant)", + 'Trd2 (both half-widths vary)', + 'Trd2 (isotropic taper, also a legal Xtru)', + 'Arb8 (parallelepiped)', + "Arb8 (TPC_IHSTR's trapezoidal prism)", + 'Arb8 (sheared in x only)', + "Arb8 (a TGeoTrap's eight corners)", + 'Xtru (non-convex L section)', + "Xtru (ITS ConeARibVol0's eight-corner section)", + 'Xtru (a triangular section)', + 'Xtru (three sections, offset and scaled)', + 'Pgon (solid hexagonal prism)', + 'Pgon (tapered eight-edge prism)', + 'Pgon (hollow 8-edge prism)', + 'Pgon (hollow 48-edge prism)', + "Pgon (TPC_Strip's thin 18-edge shell)", + 'Pgon (three hollow sections)', + 'Pgon (a 90 deg wedge closing on the axis)', + 'Pgon (a wedge across phi = 0)', + 'placed Trd1', + 'placed Xtru (non-convex L section)', +) + + +def _count_trusted_concave(solid): + """Trusted concave or mixed edges of a solid, counted as `recognise._match_single_cell` does.""" + from cadsupport import census + counts = census.edge_census(solid) + return (counts["concave"] + counts["mixed"] + - counts["concaveNearTangential"] - counts["mixedNearTangential"]) + + +@functools.lru_cache(maxsize=None) +def _recorded_candidates(): + with open(_RECORDED_CANDIDATES) as f: + return json.load(f) + + +def _candidate_differences(want, got, path=""): + """Where `got` differs from the recorded `want`: structure exactly, floats within tolerance.""" + if isinstance(want, (bool, str)) or want is None or isinstance(got, (bool, str)) or got is None: + return [] if type(want) is type(got) and want == got else [f"{path}: {got!r} != {want!r}"] + if isinstance(want, dict) or isinstance(got, dict): + if not (isinstance(want, dict) and isinstance(got, dict)) or sorted(want) != sorted(got): + return [f"{path}: keys differ"] + return [d for key in sorted(want) for d in _candidate_differences(want[key], got[key], f"{path}/{key}")] + if isinstance(want, list) or isinstance(got, list): + if not (isinstance(want, list) and isinstance(got, list)) or len(want) != len(got): + return [f"{path}: lengths differ"] + return [d for i, (w, g) in enumerate(zip(want, got)) for d in _candidate_differences(w, g, f"{path}[{i}]")] + if type(want) is not type(got): + return [f"{path}: {type(got).__name__} {got!r} != {type(want).__name__} {want!r}"] + if isinstance(want, int) and isinstance(got, int): + return [] if want == got else [f"{path}: {got} != {want}"] + tolerance = _NOTES_TOLERANCE if path.startswith("/notes") else _DESCRIPTION_TOLERANCE + if abs(got - want) <= tolerance * max(1.0, abs(want), abs(got)): + return [] + return [f"{path}: {got!r} != {want!r}"] + + +def _recorded_match(fixtures, seen): + """(ok, detail) for `fixtures` against their recorded candidates.""" + problems = [] + for name in fixtures: + if name not in seen: + problems.append(f"{name}: not converted") + continue + diffs = _candidate_differences(_recorded_candidates()[name], seen[name]) + if diffs: + more = f" (+{len(diffs) - 3} more)" if len(diffs) > 3 else "" + problems.append(f"{name}: " + "; ".join(diffs[:3]) + more) + return not problems, "; ".join(problems) or f"{len(fixtures)} candidates unchanged" + + +def self_test(verbose=True, with_root=True): # noqa: C901 + """Synthetic solids whose recognition and emission are known in closed form. + + Every positive case has a negative one; the ROOT half checks the emitted `TGeoShape` against + the closed form. + """ + import math + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse + from OCC.Core.BRepBuilderAPI import (BRepBuilderAPI_MakeEdge, BRepBuilderAPI_MakeFace, + BRepBuilderAPI_MakePolygon, BRepBuilderAPI_MakeSolid, + BRepBuilderAPI_MakeWire, BRepBuilderAPI_Sewing, + BRepBuilderAPI_Transform) + from OCC.Core.BRepFill import brepfill + from OCC.Core.BRepGProp import brepgprop + from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCone, + BRepPrimAPI_MakeCylinder, BRepPrimAPI_MakePrism, + BRepPrimAPI_MakeRevol, BRepPrimAPI_MakeSphere, + BRepPrimAPI_MakeTorus) + from OCC.Core.GProp import GProp_GProps + from OCC.Core.TopoDS import topods + from OCC.Core.GeomAPI import GeomAPI_Interpolate + from OCC.Core.TColgp import TColgp_HArray1OfPnt + from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Elips, gp_Pnt, gp_Trsf, gp_Vec + + checks = [] + + def check(name, condition, detail=""): + checks.append((name, bool(condition), detail)) + if verbose: + print(f" [{'ok ' if condition else 'FAIL'}] {name}" + (f" {detail}" if detail else "")) + + seen_candidates = {} + seen_recognisers = {} + + def expect(name, solid, want_recogniser, want_leaves=1): + record = process_solid(solid, name) + seen_recognisers[name] = record["recogniser"] if record["accepted"] else None + if record["accepted"]: + seen_candidates[name] = json.loads(json.dumps(record["candidate"], sort_keys=True)) + ok = record["accepted"] and record["recogniser"] == want_recogniser and \ + len(record["candidate"]["leaves"]) == want_leaves + detail = (f"{record['recogniser']}: {record['description']}" + if record["recognised"] else f"declined: {record['reason']}") + if record["recognised"] and not record["accepted"]: + detail += f" -- rejected: {record['reason']}" + check(f"{name} recognised as {want_recogniser} and accepted", ok, detail) + return record + + def expect_single_cell_declined(name, solid, needle): + """The one-cell read's verdict, asserted against the matcher that makes it.""" + _cand, why = recognise.recognise_single_cell(solid) + ok = _cand is None and needle in (why or "") + check(f"{name} is refused by the one-cell read", ok, f"reason: {why}") + return why + + def expect_declined(name, solid, needle=""): + record = process_solid(solid, name) + ok = not record["accepted"] and (needle in (record["reason"] or "")) + check(f"{name} is not converted as CSG", ok, f"reason: {record['reason']}") + return record + + ax = gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)) + + # --- Tier 1, one per primitive the brief scopes --- + expect("box", BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0).Shape(), "tier1-box") + cyl = BRepPrimAPI_MakeCylinder(ax, 2.0, 10.0).Shape() + expect("solid cylinder", cyl, "tier1-tube") + bore = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1)), 1.0, 12.0).Shape() + tube = BRepAlgoAPI_Cut(cyl, bore).Shape() + expect("tube", tube, "tier1-tube") + wedge = BRepPrimAPI_MakeCylinder(ax, 2.0, 10.0, math.radians(75.0)).Shape() + seg = BRepAlgoAPI_Cut(wedge, bore).Shape() + expect("tube segment", seg, "tier1-tubeseg") + expect("cone", BRepPrimAPI_MakeCone(ax, 3.0, 1.0, 10.0).Shape(), "tier1-cone") + expect("sphere", BRepPrimAPI_MakeSphere(gp_Pnt(1, 2, 3), 2.5).Shape(), "tier1-sphere") + + # A rotated, translated tube: the frame machinery, end to end. + trsf = gp_Trsf() + trsf.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 1, 0)), 0.7) + shift = gp_Trsf() + shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + moved = BRepBuilderAPI_Transform(tube, shift.Multiplied(trsf), True).Shape() + moved_record = expect("placed tube", moved, "tier1-tube") + + # --- Tier 2, the ExcavatorArm ram in miniature: a rod through the wall of an eye --- + eye = BRepAlgoAPI_Cut( + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(-0.75, 0, 0), gp_Dir(1, 0, 0)), 1.2, 1.5).Shape(), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(-1.0, 0, 0), gp_Dir(1, 0, 0)), 0.7, 2.0).Shape() + ).Shape() + rod_full = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 0.6, 8.0).Shape() + rod = BRepAlgoAPI_Cut(rod_full, BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(-0.75, 0, 0), gp_Dir(1, 0, 0)), 1.2, 1.5).Shape()).Shape() + ram = BRepAlgoAPI_Fuse(eye, rod).Shape() + ram_record = expect("rod-and-eye (two-cluster union)", ram, "tier2-tube-union", want_leaves=2) + + # --- the revolved profile: the shapes O2_TGeoToCAD.conv_pcon writes, read back --- + # The fixture states its own (r, z) ring, independent of `primitives.pcon_profile_rz`. + def revolved(z, rmin, rmax, phi1=0.0, dphi=360.0): + nz = len(z) + ring = [(rmax[i], z[i]) for i in range(nz)] + if all(r <= 0.0 for r in rmin): + ring += [(0.0, z[nz - 1]), (0.0, z[0])] + else: + ring += [(rmin[i], z[i]) for i in range(nz - 1, -1, -1)] + deduped = [] + for pt in ring: + if deduped and abs(pt[0] - deduped[-1][0]) < 1e-12 \ + and abs(pt[1] - deduped[-1][1]) < 1e-12: + continue + deduped.append(pt) + poly = BRepBuilderAPI_MakePolygon() + for (r, zz) in deduped: + poly.Add(gp_Pnt(float(r), 0.0, float(zz))) + poly.Close() + rev = BRepPrimAPI_MakeRevol(BRepBuilderAPI_MakeFace(poly.Wire()).Face(), + gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + math.radians(dphi)) + rev.Build() + shape = rev.Shape() + if abs(phi1) > 1e-12: + spin = gp_Trsf() + spin.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), math.radians(phi1)) + shape = BRepBuilderAPI_Transform(shape, spin, True).Shape() + return shape + + def expect_pcon(name, solid, z, rmin, rmax, phi1=0.0, dphi=360.0): + record = expect(name, solid, "revolved-pcon") + if not record["accepted"]: + return record + p = record["candidate"]["leaves"][0]["params"] + worst = max([abs(a - b) for a, b in zip(p["z"], z)] + + [abs(a - b) for a, b in zip(p["rmin"], rmin)] + + [abs(a - b) for a, b in zip(p["rmax"], rmax)] + + [abs(p["phi1"] - phi1), abs(p["dphi"] - dphi)]) \ + if len(p["z"]) == len(z) else float("inf") + check(f"{name} reconstructs the source TGeoPcon parameters", + len(p["z"]) == len(z) and worst < 1.0e-9, + f"nz {len(p['z'])} vs {len(z)}, worst parameter deviation {worst:.3g}") + return record + + # z-steps: duplicate z planes on both rmin and rmax, which the writer emits as cap annuli. + step_z, step_rmin, step_rmax = [-5, 0, 0, 5], [1, 1, 2, 2], [3, 3, 4, 4] + stepped = revolved(step_z, step_rmin, step_rmax) + expect_pcon("stepped polycone (duplicate z planes)", stepped, step_z, step_rmin, step_rmax) + # mixed cone and cylinder laterals on one axis -- the IBCYSSCone case, which the whole-part + # matcher declines with "mixed lateral surface kinds". + expect_pcon("cone and cylinder laterals on one axis", + revolved([-5, 0, 5], [1, 1, 2], [2, 3, 3]), [-5, 0, 5], [1, 1, 2], [2, 3, 3]) + # rmin stepping through 0: the inner lateral is a cone that reaches the axis. + expect_pcon("polycone whose rmin steps through 0", + revolved([0, 5, 10], [0, 0, 2], [4, 4, 4]), [0, 5, 10], [0, 0, 2], [4, 4, 4]) + # a half turn, and a partial-phi wedge stated in absolute phi on an identity frame. + expect_pcon("half-turn polycone", revolved([-5, 0, 5], [1, 1, 2], [2, 3, 3], 0.0, 180.0), + [-5, 0, 5], [1, 1, 2], [2, 3, 3], 0.0, 180.0) + expect_pcon("partial-phi stepped polycone", + revolved(step_z, step_rmin, step_rmax, 10.0, 120.0), + step_z, step_rmin, step_rmax, 10.0, 120.0) + # a rotated, translated polycone: the frame machinery on a multi-section leaf. + pcon_trsf = gp_Trsf() + pcon_trsf.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 1, 0)), 0.7) + pcon_shift = gp_Trsf() + pcon_shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + pcon_place = pcon_shift.Multiplied(pcon_trsf) + moved_pcon = BRepBuilderAPI_Transform(stepped, pcon_place, True).Shape() + moved_pcon_record = expect("placed stepped polycone", moved_pcon, "revolved-pcon") + check("a placed polycone travels as one leaf plus a rigid placement", + moved_pcon_record["accepted"] + and prim.placement_for_candidate(moved_pcon_record["candidate"]) is not None, + "placement present" if moved_pcon_record["accepted"] else "not accepted") + + # --- negative controls: each must decline or be rejected --- + # 1. a blind bore, which is a polycone. + blind = BRepAlgoAPI_Cut(cyl, BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1)), 1.0, 9.0).Shape()).Shape() + expect_pcon("cylinder with a blind bore", blind, [-5, 3, 3, 5], [1, 1, 0, 0], [2, 2, 2, 2]) + # 2. an L-shape, which is a TGeoXtru. + ell = BRepAlgoAPI_Cut(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0, 4.0, 1.0).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(2, 2, -1), 4.0, 4.0, 3.0).Shape()).Shape() + expect("L-shaped plate", ell, "rung2-xtru") + # 3. a torus. + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeTorus + expect("torus", BRepPrimAPI_MakeTorus(5.0, 1.0).Shape(), "tier1-torus") + # 4. a cylinder with a flat milled off it. + flatted = BRepAlgoAPI_Cut(cyl, BRepPrimAPI_MakeBox( + gp_Pnt(1.5, -3, -6), 3.0, 6.0, 12.0).Shape()).Shape() + # A milled flat is one cell of four halfspaces. + flat_record = expect("cylinder with a milled flat", flatted, "cell-intersection", + want_leaves=2) + + # --- negative controls for the revolved matcher --- + # 5. a TGeoPgon, whose planar laterals must never be read as a polycone. + def prism_ring(apothem, nedges, phi1=0.0, dphi=360.0): + dseg = math.radians(dphi) / nedges + radius = apothem / math.cos(dseg / 2.0) + n = nedges if abs(dphi - 360.0) < 1e-9 else nedges + 1 + return [(radius * math.cos(math.radians(phi1) + k * dseg), + radius * math.sin(math.radians(phi1) + k * dseg)) for k in range(n)] + + def swept_polygon(apothem, nedges, z0, z1): + poly = BRepBuilderAPI_MakePolygon() + for (x, y) in prism_ring(apothem, nedges): + poly.Add(gp_Pnt(x, y, z0)) + poly.Close() + pr = BRepPrimAPI_MakePrism(BRepBuilderAPI_MakeFace(poly.Wire()).Face(), + gp_Vec(0, 0, z1 - z0)) + pr.Build() + return pr.Shape() + + # They convert as TGeoPgon, not as a polycone. + for nedges in (8, 48): + pgon = BRepAlgoAPI_Cut(swept_polygon(3.0, nedges, -5.0, 5.0), + swept_polygon(1.5, nedges, -6.0, 6.0)).Shape() + expect(f"hollow {nedges}-edge polygon (TGeoPgon)", pgon, "rung2-pgon") + # 6. polygonal laterals sharing an axis with a real cylinder. + hybrid = BRepAlgoAPI_Fuse( + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)), 3.0, 5.0).Shape(), + swept_polygon(3.0, 6, 0.0, 5.0)).Shape() + # It converts as two cells; no whole-part matcher may take it. + hybrid_single = recognise.recognise_single_cell(hybrid)[1] + check("a cylinder with a coaxial hexagonal section is no whole-part primitive", + recognise.recognise(hybrid)[0]["recogniser"] == "cells-union" + and "neither a cap nor a wedge" in (recognise.recognise_revolved(hybrid)[1] or ""), + f"one-cell read: {(hybrid_single or '')[:90]}") + # 7. a bore displaced off the axis, which the symmetric difference refuses. + for displacement in (1.0e-6, 1.0e-5): + off = BRepAlgoAPI_Cut( + revolved(step_z, [0, 0, 0, 0], step_rmax), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(displacement, 0, -6), gp_Dir(0, 0, 1)), + 1.0, 12.0).Shape()).Shape() + expect_declined(f"stepped polycone with the bore {displacement:g} cm off axis", off) + # 8. a cap plane tilted off perpendicular. + tilt = gp_Trsf() + tilt.SetRotation(gp_Ax1(gp_Pnt(0, 0, 5), gp_Dir(1, 0, 0)), 1.0e-4) + knife = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, 4.9), gp_Dir(0, 0, 1)), + 10.0, 5.0).Shape() + expect_declined("stepped polycone with a tilted top cap", + BRepAlgoAPI_Cut(stepped, + BRepBuilderAPI_Transform(knife, tilt, True).Shape()).Shape(), + "neither a cap nor a wedge") + + # --- the instrument that scores the revolved candidate must be able to say "no" --- + true_profile = prim.pcon_profile_rz({"z": [float(v) for v in step_z], + "rmin": [float(v) for v in step_rmin], + "rmax": [float(v) for v in step_rmax]}) + samples = [(3.0, -2.5), (4.0, 2.5), (1.0, -5.0), (2.0, 5.0), (3.5, 0.0)] + check("the profile gap is zero on the profile's own boundary", + recognise._profile_gap(true_profile, samples) < 1.0e-12, + f"gap {recognise._profile_gap(true_profile, samples):.3g} cm") + nudged_profile = [(r + (1.0e-6 if abs(r - 3.0) < 1e-12 else 0.0), z) + for (r, z) in true_profile] + nudged_gap = recognise._profile_gap(nudged_profile, samples) + check("the profile gap reports a radius displaced by ten model tolerances", + abs(nudged_gap - 1.0e-6) < 1.0e-12, f"gap {nudged_gap:.3g} cm, expected 1e-06 cm") + + # --- the description must refuse an illegal TGeoPcon before either builder sees it --- + for name, params in ( + ("unequal array lengths", + {"phi1": 0.0, "dphi": 360.0, "z": [0.0, 1.0], "rmin": [0.0], "rmax": [1.0, 1.0]}), + ("rmin above rmax", + {"phi1": 0.0, "dphi": 360.0, "z": [0.0, 1.0], "rmin": [2.0, 2.0], + "rmax": [1.0, 1.0]}), + ("a single section", + {"phi1": 0.0, "dphi": 360.0, "z": [0.0], "rmin": [0.0], "rmax": [1.0]}), + ("z running backwards", + {"phi1": 0.0, "dphi": 360.0, "z": [1.0, 0.0], "rmin": [0.0, 0.0], + "rmax": [1.0, 1.0]})): + try: + prim.leaf("TGeoPcon", params, prim.identity_frame()) + refused = False + except ValueError: + refused = True + check(f"a TGeoPcon description with {name} is refused", refused) + + # --- an all-cone stack, retried after the acceptance test refuses tier 1 --- + stack_record = expect_pcon("all-cone stack (two cones and two caps)", + revolved([-3, 0, 3], [0, 0, 0], [2, 3, 1]), + [-3, 0, 3], [0, 0, 0], [2, 3, 1]) + check("the all-cone stack was retried after tier 1 was rejected, not merely declined", + (stack_record.get("retriedAfter") or {}).get("recogniser") == "tier1-cone", + f"retried after {(stack_record.get('retriedAfter') or {}).get('recogniser')}: " + f"{(stack_record.get('retriedAfter') or {}).get('reason')}") + # An hourglass pinches to the axis (rmax = 0), a legal polycone. + expect("hourglass (two cones meeting on the axis)", + revolved([-5, 0, 5], [0, 0, 0], [2, 0, 2]), "revolved-pcon") + + # --- a two-section full-turn profile is said in its native class --- + def expect_native(name, solid, want_recogniser, want_type, want_params): + record = expect(name, solid, want_recogniser) + if not record["accepted"]: + return record + lf = record["candidate"]["leaves"][0] + worst = max(abs(lf["params"][k] - v) for k, v in want_params.items()) \ + if lf["type"] == want_type else float("inf") + check(f"{name} emits a native {want_type} with the source's parameters", + lf["type"] == want_type and worst < 1.0e-9, + f"{lf['type']}, worst parameter deviation {worst:.3g}") + return record + + # A TGeoCone with one radius constant must come back as a TGeoCone. + expect_native("cone with a cylindrical bore (constant rmin)", + revolved([-25, 25], [4.5, 4.5], [16.22, 25.04]), "revolved-cone", "TGeoCone", + {"dz": 25.0, "rmin1": 4.5, "rmax1": 16.22, "rmin2": 4.5, "rmax2": 25.04}) + expect_native("cylinder with a conical bore (constant rmax)", + revolved([-3, 3], [6.99, 7.374], [26.02, 26.02]), "revolved-cone", "TGeoCone", + {"dz": 3.0, "rmin1": 6.99, "rmax1": 26.02, "rmin2": 7.374, "rmax2": 26.02}) + # A wedge and a step must stay polycones. + expect_pcon("two-section wedge stays a polycone", revolved([-5, 5], [1, 1], [2, 3], 0.0, + 120.0), + [-5, 5], [1, 1], [2, 3], 0.0, 120.0) + expect_pcon("a stepped profile stays a polycone", stepped, step_z, step_rmin, step_rmax) + # The TGeoTube branch is unreachable from CAD, so it is exercised on the description. + tube_leaf, tube_tag = recognise._canonical_revolved_leaf( + prim.leaf("TGeoPcon", {"phi1": 0.0, "dphi": 360.0, "z": [-4.0, 6.0], + "rmin": [1.0, 1.0], "rmax": [2.0, 2.0]}, prim.identity_frame()), + (0.0, 0.0, 0.0), (0.0, 0.0, 1.0), 1.0e-9) + check("a two-section profile with constant radii canonicalises to a TGeoTube", + tube_tag == "revolved-tube" and tube_leaf["type"] == "TGeoTube" + and abs(tube_leaf["params"]["dz"] - 5.0) < 1e-12 + and abs(tube_leaf["frame"]["origin"][2] - 1.0) < 1e-12, + f"{tube_tag}, {tube_leaf['type']}, dz {tube_leaf['params']['dz']}, origin " + f"{tube_leaf['frame']['origin']}") + + # --- rung 2: the prism family, the shapes `_prism_from_rings` writes, read back --- + # The fixture sews its own faces from its own ring coordinates. + def prism(rings, inner=None): + stacks = [[[tuple(float(c) for c in q) for q in ring] for ring in rings]] + if inner is not None: + stacks.append([[tuple(float(c) for c in q) for q in ring] for ring in inner]) + faces = [] + for stack in stacks: + nv = len(stack[0]) + for k in range(len(stack) - 1): + lo, hi = stack[k], stack[k + 1] + for i in range(nv): + j = (i + 1) % nv + poly = BRepBuilderAPI_MakePolygon() + for q in (lo[i], lo[j], hi[j], hi[i]): + poly.Add(gp_Pnt(*q)) + poly.Close() + made = BRepBuilderAPI_MakeFace(poly.Wire()) + if made.IsDone(): + faces.append(made.Face()) + for idx in (0, -1): + poly = BRepBuilderAPI_MakePolygon() + for q in stacks[0][idx]: + poly.Add(gp_Pnt(*q)) + poly.Close() + made = BRepBuilderAPI_MakeFace(poly.Wire()) + if len(stacks) == 2: + hole = BRepBuilderAPI_MakePolygon() + for q in stacks[1][idx]: + hole.Add(gp_Pnt(*q)) + hole.Close() + made.Add(topods.Wire(hole.Wire().Reversed())) + faces.append(made.Face()) + extent = max(abs(c) for stack in stacks for r in stack for q in r for c in q) or 1.0 + sew = BRepBuilderAPI_Sewing(1.0e-7 * extent) + for face in faces: + sew.Add(face) + sew.Perform() + ms = BRepBuilderAPI_MakeSolid(topods.Shell(sew.SewedShape())) + ms.Build() + solid = ms.Solid() + props = GProp_GProps() + brepgprop.VolumeProperties(solid, props) + if props.Mass() < 0.0: + solid = topods.Solid(solid.Reversed()) + return solid + + def polygon_ring(corners, z): + return [(x, y, z) for (x, y) in corners] + + def regular_ring(apothem, nedges, z, phi1=0.0, dphi=360.0): + dseg = math.radians(dphi) / nedges + radius = apothem / math.cos(dseg / 2.0) + n = nedges if abs(dphi - 360.0) < 1e-9 else nedges + 1 + return [(radius * math.cos(math.radians(phi1) + k * dseg), + radius * math.sin(math.radians(phi1) + k * dseg), z) for k in range(n)] + + def expect_prism(name, solid, want_recogniser, want_type, want_params): + record = expect(name, solid, want_recogniser) + if not record["accepted"]: + return record + lf = record["candidate"]["leaves"][0] + worst = 0.0 + if lf["type"] != want_type: + worst = float("inf") + else: + for key, want in want_params.items(): + got = lf["params"][key] + if isinstance(want, (list, tuple)): + worst = (float("inf") if len(got) != len(want) + else max([worst] + [abs(a - b) for a, b in zip(got, want)])) + else: + worst = max(worst, abs(got - want)) + check(f"{name} emits a native {want_type} with the source's parameters", + lf["type"] == want_type and worst < 1.0e-9, + f"{lf['type']}, worst parameter deviation {worst:.3g}") + return record + + def trd_rings(dx1, dx2, dy1, dy2, dz): + return [[(-dx1, -dy1, -dz), (dx1, -dy1, -dz), (dx1, dy1, -dz), (-dx1, dy1, -dz)], + [(-dx2, -dy2, dz), (dx2, -dy2, dz), (dx2, dy2, dz), (-dx2, dy2, dz)]] + + # TGeoTrd1: the slanted prism behind TPC's 44 "a box face has no opposite partner" declines. + expect_prism("Trd1 (slanted x faces)", prism(trd_rings(3, 1, 2, 2, 5)), "rung2-trd1", + "TGeoTrd1", {"dx1": 3.0, "dx2": 1.0, "dy": 2.0, "dz": 5.0}) + # The taper reversed, and TPC_IRB1's 0.076 cm slant on 14.2 cm. + expect_prism("Trd1 (taper reversed)", prism(trd_rings(1, 3, 2, 2, 4)), "rung2-trd1", + "TGeoTrd1", {"dx1": 1.0, "dx2": 3.0, "dy": 2.0, "dz": 4.0}) + expect_prism("Trd1 (TPC_IRB1's 0.5 % slant)", + prism(trd_rings(14.205637404580152, 14.281551908396947, 2.06, 2.06, 0.2)), + "rung2-trd1", "TGeoTrd1", + {"dx1": 14.205637404580152, "dx2": 14.281551908396947, "dy": 2.06, "dz": 0.2}) + # TGeoTrd2: both half-widths vary; the more specific class wins over a legal Xtru. + expect_prism("Trd2 (both half-widths vary)", prism(trd_rings(3, 1, 2, 4, 5)), "rung2-trd2", + "TGeoTrd2", {"dx1": 3.0, "dx2": 1.0, "dy1": 2.0, "dy2": 4.0, "dz": 5.0}) + expect_prism("Trd2 (isotropic taper, also a legal Xtru)", prism(trd_rings(3, 1.5, 2, 1, 5)), + "rung2-trd2", "TGeoTrd2", + {"dx1": 3.0, "dx2": 1.5, "dy1": 2.0, "dy2": 1.0, "dz": 5.0}) + + # TGeoArb8: a sheared hexahedron, and TPC_IHSTR's trapezoidal prism stated corner for corner. + para = prism([[(-2, -2, -3), (2, -2, -3), (2, 2, -3), (-2, 2, -3)], + [(-1, -1.5, 3), (3, -1.5, 3), (3, 2.5, 3), (-1, 2.5, 3)]]) + expect_prism("Arb8 (parallelepiped)", para, "rung2-arb8", "TGeoArb8", + {"dz": 3.0, "vertices": [-2, -2, 2, -2, 2, 2, -2, 2, + -1, -1.5, 3, -1.5, 3, 2.5, -1, 2.5]}) + ihstr = [(0.0, 0.0), (0.0, 1.08), (2.3, 1.08), (3.38, 0.0)] + expect_prism("Arb8 (TPC_IHSTR's trapezoidal prism)", + prism([polygon_ring(ihstr, -0.6), polygon_ring(ihstr, 0.6)]), + "rung2-arb8", "TGeoArb8", + {"dz": 0.6, "vertices": [0, 0, 3.38, 0, 2.3, 1.08, 0, 1.08, + 0, 0, 3.38, 0, 2.3, 1.08, 0, 1.08]}) + # A hexahedron sheared in x only: neither a Trd nor an Xtru. + expect("Arb8 (sheared in x only)", + prism([[(-2, -1, -2), (2, -1, -2), (2, 1, -2), (-2, 1, -2)], + [(-2, -1, 2), (4, -1, 2), (4, 1, 2), (-2, 1, 2)]]), "rung2-arb8") + # A TGeoTrap's corners, from `TGeoTrap(5, 10, 20, 2, 3, 4, 5, 2, 3, 4, 5).GetVertices()`. + trap_bottom = [(-4.003443140137866, -2.301536896070458), + (-4.653488486034171, 1.698463103929542), + (3.3465115139658295, 1.698463103929542), + (1.996556859862133, -2.301536896070458)] + trap_top = [(-2.346511513965829, -1.698463103929542), + (-2.996556859862133, 2.301536896070458), + (5.003443140137866, 2.301536896070458), + (3.653488486034171, -1.698463103929542)] + expect("Arb8 (a TGeoTrap's eight corners)", + prism([polygon_ring(trap_bottom, -5.0), polygon_ring(trap_top, 5.0)]), "rung2-arb8") + + # TGeoXtru: ITS's 23 Xtru volumes are all right prisms on a general, often non-convex polygon. + ell_poly = [(0, 0), (3, 0), (3, 1), (1, 1), (1, 3), (0, 3)] + expect_prism("Xtru (non-convex L section)", + prism([polygon_ring(ell_poly, -2), polygon_ring(ell_poly, 2)]), + "rung2-xtru", "TGeoXtru", + {"x": [0, 3, 3, 1, 1, 0], "y": [0, 0, 1, 1, 3, 3], "z": [-2, 2], + "xoff": [0, 0], "yoff": [0, 0], "scale": [1, 1]}) + rib = [(0, 0), (4.2, 0), (4.2, 0.1), (5.05, 0.1), (9.803, 1.83), (5.9, 1.83), (5.0, 2.73), + (0, 2.73)] + expect("Xtru (ITS ConeARibVol0's eight-corner section)", + prism([polygon_ring(rib, -0.045), polygon_ring(rib, 0.045)]), "rung2-xtru") + expect("Xtru (a triangular section)", + prism([polygon_ring([(0, 0), (0.05, 0), (0, 0.074)], -14.5), + polygon_ring([(0, 0), (0.05, 0), (0, 0.074)], 14.5)]), "rung2-xtru") + # Three sections with a per-section offset and an isotropic scale. + scaled_poly = [(0, 0), (2, 0), (2, 1), (1, 2), (0, 2)] + scaled = prism([[(0.0 + 1.0 * x, 0.0 + 1.0 * y, -3.0) for x, y in scaled_poly], + [(0.5 + 1.4 * x, -0.25 + 1.4 * y, 0.0) for x, y in scaled_poly], + [(1.0 + 0.6 * x, 0.0 + 0.6 * y, 3.0) for x, y in scaled_poly]]) + expect_prism("Xtru (three sections, offset and scaled)", scaled, "rung2-xtru", "TGeoXtru", + {"z": [-3, 0, 3], "xoff": [0, 0.5, 1.0], "yoff": [0, -0.25, 0], + "scale": [1.0, 1.4, 0.6]}) + + # TGeoPgon: the laterals are planes at the apothem radius, corners at `r / cos(dseg/2)`. + expect_prism("Pgon (solid hexagonal prism)", + prism([regular_ring(3, 6, -5), regular_ring(3, 6, 5)]), "rung2-pgon", + "TGeoPgon", {"nedges": 6, "phi1": 0.0, "dphi": 360.0, "z": [-5, 5], + "rmin": [0, 0], "rmax": [3, 3]}) + expect_prism("Pgon (tapered eight-edge prism)", + prism([regular_ring(3, 8, -5), regular_ring(1.5, 8, 5)]), "rung2-pgon", + "TGeoPgon", {"nedges": 8, "phi1": 0.0, "dphi": 360.0, "z": [-5, 5], + "rmin": [0, 0], "rmax": [3, 1.5]}) + for nedges in (8, 48): + hollow = prism([regular_ring(3, nedges, -5), regular_ring(3, nedges, 5)], + inner=[regular_ring(1.5, nedges, -5), regular_ring(1.5, nedges, 5)]) + expect_prism(f"Pgon (hollow {nedges}-edge prism)", hollow, "rung2-pgon", "TGeoPgon", + {"nedges": nedges, "phi1": 0.0, "dphi": 360.0, "z": [-5, 5], + "rmin": [1.5, 1.5], "rmax": [3, 3]}) + # TPC_Strip: 18 edges, a 1 mm wall on an 85 cm radius, 250 cm long. + expect_prism("Pgon (TPC_Strip's thin 18-edge shell)", + prism([regular_ring(85.235, 18, -124.8), regular_ring(85.235, 18, 124.8)], + inner=[regular_ring(85.225, 18, -124.8), regular_ring(85.225, 18, 124.8)]), + "rung2-pgon", "TGeoPgon", + {"nedges": 18, "phi1": 0.0, "dphi": 360.0, "z": [-124.8, 124.8], + "rmin": [85.225, 85.225], "rmax": [85.235, 85.235]}) + # Three hollow sections with the radii stepping. + expect_prism("Pgon (three hollow sections)", + prism([regular_ring(3, 6, -5), regular_ring(3, 6, 0), regular_ring(4, 6, 5)], + inner=[regular_ring(1, 6, -5), regular_ring(1, 6, 0), + regular_ring(2, 6, 5)]), + "rung2-pgon", "TGeoPgon", + {"nedges": 6, "phi1": 0.0, "dphi": 360.0, "z": [-5, 0, 5], + "rmin": [1, 1, 2], "rmax": [3, 3, 4]}) + # A phi wedge closing on its axis, and one across phi = 0. + expect_prism("Pgon (a 90 deg wedge closing on the axis)", + prism([regular_ring(4, 3, -2, 10.0, 90.0) + [(0.0, 0.0, -2.0)], + regular_ring(4, 3, 2, 10.0, 90.0) + [(0.0, 0.0, 2.0)]]), + "rung2-pgon", "TGeoPgon", + {"nedges": 3, "phi1": 10.0, "dphi": 90.0, "z": [-2, 2], "rmin": [0, 0], + "rmax": [4, 4]}) + expect_prism("Pgon (a wedge across phi = 0)", + prism([regular_ring(4, 2, -2, 350.0, 20.0) + [(0.0, 0.0, -2.0)], + regular_ring(4, 2, 2, 350.0, 20.0) + [(0.0, 0.0, 2.0)]]), + "rung2-pgon", "TGeoPgon", + {"nedges": 2, "phi1": 350.0, "dphi": 20.0, "z": [-2, 2], "rmin": [0, 0], + "rmax": [4, 4]}) + + # A placed prism: the frame machinery on a leaf that has no origin of its own. + prism_trsf = gp_Trsf() + prism_trsf.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 1, 0)), 0.7) + prism_shift = gp_Trsf() + prism_shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + prism_place = prism_shift.Multiplied(prism_trsf) + moved_trd = BRepBuilderAPI_Transform(prism(trd_rings(3, 1, 2, 2, 5)), prism_place, + True).Shape() + moved_trd_record = expect("placed Trd1", moved_trd, "rung2-trd1") + check("a placed Trd1 travels as one leaf plus a rigid placement", + moved_trd_record["accepted"] + and prim.placement_for_candidate(moved_trd_record["candidate"]) is not None, + "placement present" if moved_trd_record["accepted"] else "not accepted") + moved_xtru = BRepBuilderAPI_Transform( + prism([polygon_ring(ell_poly, -2), polygon_ring(ell_poly, 2)]), prism_place, + True).Shape() + expect("placed Xtru (non-convex L section)", moved_xtru, "rung2-xtru") + + # --- rung 2 negative controls --- + # 1. A twisted TGeoArb8, whose ruled B-spline laterals are declined as free-form. + twisted_faces = [] + twist_bottom = [(-2, -2, -2), (-2, 2, -2), (2, 2, -2), (2, -2, -2)] + twist_top = [(-1.41, -2.73, 2), (-2.73, 1.41, 2), (1.41, 2.73, 2), (2.73, -1.41, 2)] + for i in range(4): + j = (i + 1) % 4 + e1 = BRepBuilderAPI_MakeEdge(gp_Pnt(*twist_bottom[i]), gp_Pnt(*twist_bottom[j])).Edge() + e2 = BRepBuilderAPI_MakeEdge(gp_Pnt(*twist_top[i]), gp_Pnt(*twist_top[j])).Edge() + twisted_faces.append(brepfill.Face(e1, e2)) + for ring in (twist_bottom, twist_top): + poly = BRepBuilderAPI_MakePolygon() + for q in ring: + poly.Add(gp_Pnt(*q)) + poly.Close() + twisted_faces.append(BRepBuilderAPI_MakeFace(poly.Wire()).Face()) + sew_twist = BRepBuilderAPI_Sewing(1.0e-6) + for face in twisted_faces: + sew_twist.Add(face) + sew_twist.Perform() + twist_solid = BRepBuilderAPI_MakeSolid(topods.Shell(sew_twist.SewedShape())) + twist_solid.Build() + expect_declined("twisted hexahedron (a ruled TGeoArb8 side)", twist_solid.Solid(), + "free-form faces") + + # 2. A middle section stretched in y only: the volume refuses 1e-06 cm, the gap 1e-05 cm. + for displacement in (1.0e-6, 1.0e-5): + near = prism([[(-2, -1, -2), (2, -1, -2), (2, 1, -2), (-2, 1, -2)], + [(-2, -1 - displacement, 0), (2, -1 - displacement, 0), + (2, 1 + displacement, 0), (-2, 1 + displacement, 0)], + [(-2, -1, 2), (2, -1, 2), (2, 1, 2), (-2, 1, 2)]]) + expect_declined(f"prism with one section {displacement:g} cm out of similarity", near) + + # 3. A polycone must not be taken by a prism template. + check("a polycone reaches the revolved matcher, not the prism one", + process_solid(stepped, "pcon-vs-prism")["recogniser"] == "revolved-pcon", + f"{process_solid(stepped, 'pcon-vs-prism')['recogniser']}") + + # --- the instrument that scores a prism candidate must be able to say "no" --- + exact_ring = [(-2.0, -1.0, -2.0), (2.0, -1.0, -2.0), (2.0, 1.0, -2.0), (-2.0, 1.0, -2.0)] + nudged = [(x + (1.0e-6 if i == 0 else 0.0), y, z) + for i, (x, y, z) in enumerate(exact_ring)] + check("the point-set gap is zero on the point set itself", + recognise._point_set_gap(exact_ring, exact_ring) == 0.0, + f"gap {recognise._point_set_gap(exact_ring, exact_ring):.3g} cm") + nudged_gap = recognise._point_set_gap(exact_ring, nudged) + check("the point-set gap reports a corner displaced by ten model tolerances", + abs(nudged_gap - 1.0e-6) < 1.0e-15, f"gap {nudged_gap:.3g} cm, expected 1e-06 cm") + # ... and a hexahedron read in the wrong corner order, which only the edge midpoints catch. + good_arb8 = prim.leaf("TGeoArb8", {"dz": 3.0, + "vertices": [-2, -2, 2, -2, 2, 2, -2, 2, + -1, -1.5, 3, -1.5, 3, 2.5, -1, 2.5]}, + prim.identity_frame()) + swapped = list(good_arb8["params"]["vertices"]) + swapped[2:4], swapped[4:6] = swapped[4:6], swapped[2:4] + bad_arb8 = prim.leaf("TGeoArb8", {"dz": 3.0, "vertices": swapped}, prim.identity_frame()) + corner_only_gap = recognise._point_set_gap( + [tuple(q) for q in prim.prism_samples(good_arb8)[0::3]], + [tuple(q) for q in prim.prism_samples(bad_arb8)[0::3]]) + order_gap = recognise._point_set_gap(prim.prism_samples(good_arb8), + prim.prism_samples(bad_arb8)) + check("the edge midpoints are what catch a hexahedron read in the wrong corner order", + corner_only_gap == 0.0 and order_gap > 0.1, + f"corners alone {corner_only_gap:.3g} cm, corners and edge midpoints " + f"{order_gap:.3g} cm") + + # --- the description must refuse an illegal prism before either builder sees it --- + for name, kind, params in ( + ("a TGeoXtru whose z runs backwards", "TGeoXtru", + {"x": [0, 1, 0], "y": [0, 0, 1], "z": [1.0, 0.0], "xoff": [0, 0], "yoff": [0, 0], + "scale": [1, 1]}), + ("a TGeoXtru with a repeated corner", "TGeoXtru", + {"x": [0, 1, 1], "y": [0, 0, 0], "z": [0.0, 1.0], "xoff": [0, 0], "yoff": [0, 0], + "scale": [1, 1]}), + ("a TGeoXtru with two corners", "TGeoXtru", + {"x": [0, 1], "y": [0, 0], "z": [0.0, 1.0], "xoff": [0, 0], "yoff": [0, 0], + "scale": [1, 1]}), + ("a TGeoXtru with a zero scale", "TGeoXtru", + {"x": [0, 1, 0], "y": [0, 0, 1], "z": [0.0, 1.0], "xoff": [0, 0], "yoff": [0, 0], + "scale": [1, 0]}), + ("a TGeoArb8 with fifteen coordinates", "TGeoArb8", + {"dz": 1.0, "vertices": [0.0] * 15}), + ("a TGeoArb8 with a collapsed face", "TGeoArb8", + {"dz": 1.0, "vertices": [0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1]}), + ("a TGeoTrd1 with both half-widths zero", "TGeoTrd1", + {"dx1": 0.0, "dx2": 0.0, "dy": 1.0, "dz": 1.0}), + ("a TGeoTrd2 with a negative half-width", "TGeoTrd2", + {"dx1": -1.0, "dx2": 1.0, "dy1": 1.0, "dy2": 1.0, "dz": 1.0}), + ("a TGeoPgon with no edges", "TGeoPgon", + {"phi1": 0.0, "dphi": 360.0, "nedges": 0, "z": [0.0, 1.0], "rmin": [0.0, 0.0], + "rmax": [1.0, 1.0]})): + try: + prim.leaf(kind, params, prim.identity_frame()) + refused = False + except ValueError: + refused = True + check(f"{name} is refused", refused) + + # A TGeoXtru's two array-length groups are each checked. + try: + prim.leaf("TGeoXtru", {"x": [0, 1, 0], "y": [0, 0], "z": [0.0, 1.0], "xoff": [0, 0], + "yoff": [0, 0], "scale": [1, 1]}, prim.identity_frame()) + refused = False + except ValueError: + refused = True + check("a TGeoXtru whose x and y differ in length is refused", refused) + xtru_two_lengths = prim.leaf( + "TGeoXtru", {"x": [0, 2, 2, 0], "y": [0, 0, 1, 1], "z": [-1.0, 0.0, 1.0], + "xoff": [0, 0, 0], "yoff": [0, 0, 0], "scale": [1, 1, 1]}, + prim.identity_frame()) + check("a TGeoXtru carries four corners and three sections in one description", + len(xtru_two_lengths["params"]["x"]) == 4 and len(xtru_two_lengths["params"]["z"]) == 3, + f"{len(xtru_two_lengths['params']['x'])} corners, " + f"{len(xtru_two_lengths['params']['z'])} sections") + + # --- the floor for rung 2's own emissions --- + check("every prism-family candidate matches its recorded candidate within tolerance", + *_recorded_match(_PRISM_FIXTURES, seen_candidates)) + + # --- the floor: nothing that converted before this matcher existed converts differently --- + check("every whole-part candidate matches its recorded candidate within tolerance", + *_recorded_match(_WHOLE_PART_FIXTURES, seen_candidates)) + + # --- rung 3: the single cell --- + # The same constructions as `make_boolean_fixtures.py`, in cm. + def cyl_along(radius, length, origin, direction): + return BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(*origin), gp_Dir(*direction)), + radius, length).Shape() + + # Two orthogonal r = 1 cylinders intersected: the Steinmetz solid, with no planar face. + steinmetz = BRepAlgoAPI_Common(cyl_along(1.0, 6.0, (0, 0, -3), (0, 0, 1)), + cyl_along(1.0, 6.0, (-3, 0, 0), (1, 0, 0))).Shape() + steinmetz_record = expect("Steinmetz solid (two cylinders intersected)", steinmetz, + "cell-intersection", want_leaves=2) + check("the Steinmetz solid reaches the cell emitter only after a rejection", + (steinmetz_record.get("retriedAfter") or {}).get("recogniser") == "tier2-tube-union", + f"retried after {(steinmetz_record.get('retriedAfter') or {}).get('recogniser')}") + # A tube with a transverse hole, whose wall enters as a subtraction. + window = BRepAlgoAPI_Cut(cyl_along(1.5, 6.0, (0, 0, -3), (0, 0, 1)), + cyl_along(0.8, 6.0, (-3, 0, 0), (1, 0, 0))).Shape() + window_record = expect("tube with a transverse window", window, "cell-intersection", + want_leaves=2) + check("the window's hole wall is a complemented leaf and its barrel is not", + window_record["accepted"] + and not window_record["candidate"]["leaves"][0].get("outside") + and window_record["candidate"]["leaves"][1].get("outside") is True, + window_record["description"]) + check("the barrel and its two caps folded into one TGeoTube", + window_record["accepted"] + and window_record["candidate"]["leaves"][0]["type"] == "TGeoTube" + and abs(window_record["candidate"]["leaves"][0]["params"]["dz"] - 3.0) < 1e-12 + and window_record["candidate"]["notes"]["nCarriers"] == 4, + f"{window_record['candidate']['notes'] if window_record['accepted'] else 'n/a'}") + # A cylinder cut by an oblique plane, which stays a halfspace. + oblique_knife = BRepPrimAPI_MakeBox(gp_Pnt(-20, -20, 0), 40.0, 40.0, 40.0).Shape() + oblique_spin = gp_Trsf() + oblique_spin.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 0, 0)), math.radians(60.0)) + oblique_lift = gp_Trsf() + oblique_lift.SetTranslation(gp_Vec(0.0, 0.0, 2.5)) + oblique = BRepAlgoAPI_Cut( + cyl_along(1.2, 5.0, (0, 0, 0), (0, 0, 1)), + BRepBuilderAPI_Transform(oblique_knife, oblique_lift.Multiplied(oblique_spin), + True).Shape()).Shape() + expect("cylinder cut by an oblique plane", oblique, "cell-intersection", want_leaves=2) + # A cube with an axial through-hole: six planes that are a TGeoBBox, and a hole wall. + drilled = BRepAlgoAPI_Cut(BRepPrimAPI_MakeBox(gp_Pnt(-2, -2, -2), 4.0, 4.0, 4.0).Shape(), + cyl_along(0.8, 6.0, (0, 0, -3), (0, 0, 1))).Shape() + drilled_record = expect("cube with an axial through-hole", drilled, "cell-intersection", + want_leaves=2) + check("the cube's six plane carriers folded into one TGeoBBox", + drilled_record["accepted"] + and drilled_record["candidate"]["leaves"][0]["type"] == "TGeoBBox" + and drilled_record["candidate"]["notes"]["nCarriers"] == 7, + drilled_record["description"]) + + # --- rung 3 negative controls: a V notch ladder; each rung refuses, the last accepts --- + def notched_cylinder(angle): + def knife(sign): + slab = BRepPrimAPI_MakeBox(gp_Pnt(1.5, -10.0, -10.0), 20.0, 20.0, 20.0).Shape() + spin = gp_Trsf() + spin.SetRotation(gp_Ax1(gp_Pnt(1.5, 0.0, 0.0), gp_Dir(0, 0, 1)), sign * angle) + return BRepBuilderAPI_Transform(slab, spin, True).Shape() + return BRepAlgoAPI_Cut(cyl, BRepAlgoAPI_Common(knife(1.0), knife(-1.0)).Shape()).Shape() + + notch_trusted = expect_single_cell_declined( + "a cylinder with a 2e-03 rad notch (a trusted concave edge)", + notched_cylinder(2.0e-3), "trusted concave edge") + check("the concave decline names how many edges it counted", + "1 trusted concave edge(s) of 9" in (notch_trusted or ""), + (notch_trusted or "")[:120]) + # The notch above the trust filter converts as two cells. + notch_converted = process_solid(notched_cylinder(2.0e-3), "notched cylinder (trusted)") + check("the notch above the trust filter converts as two cells, exactly", + notch_converted["accepted"] and notch_converted["recogniser"] == "cells-union" + and notch_converted["candidate"]["notes"]["nCells"] == 2 + and notch_converted["acceptance"]["symmetricDifference"] == 0.0, + f"{notch_converted['recogniser']}: {notch_converted['description']}, " + f"dV_sym={notch_converted['acceptance']['symmetricDifference'] if notch_converted['accepted'] else 'n/a'}") + notch_gap = expect_declined("cylinder with a 1e-05 rad notch (below the trust filter)", + notched_cylinder(1.0e-5), "the cell's boundary is") + check("the gap is what refuses the notch the trust filter let through", + "the cell's boundary is" in (notch_gap["reason"] or "") + and notch_gap["recogniser"] is None, + (notch_gap["reason"] or "")[-140:]) + notch_volume = expect_declined("cylinder with a 1e-06 rad notch (ten model tolerances deep)", + notched_cylinder(1.0e-6), "symmetric difference") + check("the volume is what refuses a notch too shallow for the gap to see", + notch_volume["recogniser"] == "cell-intersection" + and not notch_volume["accepted"], + (notch_volume["reason"] or "")[:140]) + # One model tolerance deep must be accepted. + expect("cylinder with a 1e-07 rad notch (one model tolerance deep)", + notched_cylinder(1.0e-7), "cell-intersection", want_leaves=2) + + # A genuine two-cell body, asked of the cell emitter directly. + crossed = BRepAlgoAPI_Fuse(cyl_along(1.0, 6.0, (0, 0, -3), (0, 0, 1)), + cyl_along(1.0, 6.0, (-3, 0, 0), (1, 0, 0))).Shape() + crossed_cand, crossed_why = recognise.recognise_single_cell(crossed) + check("two fused cylinders are refused by the cell emitter, naming the concave edges", + crossed_cand is None and "trusted concave edge(s)" in (crossed_why or ""), + (crossed_why or "")[:120]) + # An all-planar body is the prism family's, even a convex chamfered box. + chamfer = BRepPrimAPI_MakeBox(gp_Pnt(1.2, -9.0, -9.0), 20.0, 20.0, 20.0).Shape() + chamfer_spin = gp_Trsf() + chamfer_spin.SetRotation(gp_Ax1(gp_Pnt(1.2, 0.0, 0.0), gp_Dir(0, 1, 0)), + math.radians(35.0)) + chamfered = BRepAlgoAPI_Cut( + BRepPrimAPI_MakeBox(gp_Pnt(-2, -2, -2), 4.0, 4.0, 4.0).Shape(), + BRepBuilderAPI_Transform(chamfer, chamfer_spin, True).Shape()).Shape() + planar_cand, planar_why = recognise.recognise_single_cell(chamfered) + check("an all-planar solid is handed to the prism family, not read as halfspaces", + planar_cand is None and "belongs to the prism family" in (planar_why or ""), + (planar_why or "")[:120]) + # The L-plate has a concave edge, so the cell emitter refuses it on that count instead. + ell_cand, ell_why = recognise.recognise_single_cell(ell) + check("the L-plate is refused by the cell emitter on its concave edge", + ell_cand is None and "trusted concave edge(s)" in (ell_why or ""), + (ell_why or "")[:120]) + + # --- the floor: the parts the earlier rungs own are not intercepted --- + for name, want in (("L-shaped plate", "rung2-xtru"), + ("placed Xtru (non-convex L section)", "rung2-xtru"), + ("stepped polycone (duplicate z planes)", "revolved-pcon"), + ("box", "tier1-box"), ("tube", "tier1-tube"), + ("rod-and-eye (two-cluster union)", "tier2-tube-union")): + check(f"{name} is still recognised as {want}", seen_recognisers.get(name) == want, + f"{seen_recognisers.get(name)}") + + check("every single-cell candidate matches its recorded candidate within tolerance", + *_recorded_match(_CELL_FIXTURES, seen_candidates)) + + # --- the description must refuse an ill-formed intersection --- + unit_box = prim.leaf("TGeoBBox", {"dx": 1.0, "dy": 1.0, "dz": 1.0}, prim.identity_frame()) + hole = prim.leaf("TGeoTube", {"rmin": 0.0, "rmax": 0.5, "dz": 2.0}, + prim.identity_frame(), True) + for label, op, leaves in (("a single leaf", "intersection", [unit_box]), + ("a complement first", "intersection", [hole, unit_box]), + ("a complement in a union", "union", [unit_box, hole])): + try: + prim.candidate(op, leaves, "self-test") + refused = False + except ValueError: + refused = True + check(f"a candidate with {label} is refused", refused) + + # --- flat-CSG R1: the torus carrier --- + def torus_at(major, minor, angle=None, origin=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0), + ref=(1.0, 0.0, 0.0)): + axis = gp_Ax2(gp_Pnt(*origin), gp_Dir(*direction), gp_Dir(*ref)) + maker = (BRepPrimAPI_MakeTorus(axis, major, minor) if angle is None + else BRepPrimAPI_MakeTorus(axis, major, minor, angle)) + maker.Build() + return maker.Shape() + + def expect_torus(name, solid, r, rmin, rmax, phi1=0.0, dphi=360.0): + record = expect(name, solid, "tier1-torus") + if not record["accepted"]: + return record + p = record["candidate"]["leaves"][0]["params"] + want = {"r": r, "rmin": rmin, "rmax": rmax, "phi1": phi1, "dphi": dphi} + worst = max(abs(p[k] - v) for k, v in want.items()) + check(f"{name} reconstructs the source TGeoTorus parameters", worst < 1.0e-9, + f"worst parameter deviation {worst:.3g}") + return record + + solid_torus = torus_at(4.0, 1.0) + solid_torus_record = expect_torus("solid torus", solid_torus, 4.0, 0.0, 1.0) + # A shell: two concentric tori of the same major radius, which is a bellows ply's section. + ply = BRepAlgoAPI_Cut(torus_at(5.0, 0.30), torus_at(5.0, 0.28)).Shape() + ply_record = expect_torus("torus shell (a bellows ply)", ply, 5.0, 0.28, 0.30) + # A phi wedge, hollow, whose two cut planes pass through the axis. + wedge_torus = BRepAlgoAPI_Cut( + torus_at(4.0, 1.0, math.radians(120.0), ref=(math.cos(math.radians(20.0)), + math.sin(math.radians(20.0)), 0.0)), + torus_at(4.0, 0.8, math.radians(120.0) + 1.0e-4, + ref=(math.cos(math.radians(20.0)), math.sin(math.radians(20.0)), 0.0))).Shape() + wedge_torus_record = expect_torus("hollow torus wedge", wedge_torus, + 4.0, 0.8, 1.0, 20.0, 120.0) + # Placed, so the frame machinery is exercised on a torus too. + torus_spin = gp_Trsf() + torus_spin.SetRotation(gp_Ax1(gp_Pnt(0, 0, 0), gp_Dir(1, 1, 0)), 0.7) + torus_shift = gp_Trsf() + torus_shift.SetTranslation(gp_Vec(3.0, -4.0, 5.0)) + placed_torus = BRepBuilderAPI_Transform(solid_torus, + torus_shift.Multiplied(torus_spin), True).Shape() + placed_torus_record = expect("placed torus", placed_torus, "tier1-torus") + check("a placed torus travels as one leaf plus a rigid placement", + placed_torus_record["accepted"] + and prim.placement_for_candidate(placed_torus_record["candidate"]) is not None, + "placement present" if placed_torus_record["accepted"] else "not accepted") + + # The torus as a cell-emitter carrier: a ply cut by a plane is a cell of two toroidal + # halfspaces, the bore's one complemented, and one box. + half_ply = BRepAlgoAPI_Common( + ply, BRepPrimAPI_MakeBox(gp_Pnt(-10, -10, 0), 20.0, 20.0, 20.0).Shape()).Shape() + half_ply_record = expect("half a bellows ply", half_ply, "cell-intersection", want_leaves=3) + check("the ply's bore enters the cell as a complemented TGeoTorus", + half_ply_record["accepted"] + and sum(1 for lf in half_ply_record["candidate"]["leaves"] + if lf["type"] == "TGeoTorus") == 2 + and any(lf.get("outside") and lf["type"] == "TGeoTorus" + for lf in half_ply_record["candidate"]["leaves"]), + half_ply_record["description"]) + + # --- R1 negative controls --- + # `torus_union_cyl` from the fixture ladder, in cm: two cells, concave on both circles. + torus_cyl = BRepAlgoAPI_Fuse( + torus_at(2.5, 0.8), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -2.0), gp_Dir(0, 0, 1)), + 2.0, 4.0).Shape()).Shape() + torus_cyl_why = expect_single_cell_declined("a torus fused with a coaxial cylinder through it", + torus_cyl, "trusted concave edge") + # The torus template's own verdict, asked of the template directly. + torus_cyl_records, _why = recognise._face_records(torus_cyl) + torus_cyl_diag = recognise._bbox_diagonal(torus_cyl) + try: + recognise._match_torus(torus_cyl, torus_cyl_records, + recognise.REL_TOL * max(torus_cyl_diag, 1.0), torus_cyl_diag) + torus_template_why = "the template accepted it" + except recognise.Declined as declined: + torus_template_why = str(declined) + check("the torus template says what it found before the cell test refuses it", + "is not a whole torus" in torus_template_why, torus_template_why[:120]) + # A shell whose bore is displaced off axis: below tol (1.6e-05 cm) one torus, above it two cells. + for displacement, want in ((1.0e-6, "tier1-torus"), (1.0e-5, "tier1-torus"), + (3.0e-5, "cell-intersection"), (1.0e-3, "cell-intersection")): + skewed = BRepAlgoAPI_Cut( + torus_at(5.0, 0.30), + torus_at(5.0, 0.28, origin=(displacement, 0.0, 0.0))).Shape() + skewed_record = process_solid(skewed, f"shell, bore {displacement:g} cm off axis") + acceptance = skewed_record.get("acceptance") or {} + check(f"a shell whose bore is {displacement:g} cm off the axis converts as {want}, " + "within the band", + skewed_record["accepted"] and skewed_record["recogniser"] == want + and acceptance.get("symmetricDifference", 1.0) <= acceptance.get("band", 0.0), + f"{skewed_record['recogniser']}: dV=" + f"{acceptance.get('symmetricDifference')} band={acceptance.get('band')}") + if want == "cell-intersection": + # And it is the concentricity test that hands it over, not an accident further on. + records, _reason = recognise._face_records(skewed) + skewed_diag = recognise._bbox_diagonal(skewed) + try: + recognise._match_torus(skewed, records, + recognise.REL_TOL * max(skewed_diag, 1.0), skewed_diag) + refused = None + except recognise.Declined as declined: + refused = str(declined) + check(f"and the torus template is what refuses it at {displacement:g} cm", + refused is not None and "concentric" in refused, + refused or "IT PROPOSED ONE") + + # --- a self-intersecting fillet torus declines instead of raising --- + blend_lobe = BRepAlgoAPI_Common( + torus_at(0.0428825434729, 0.1), + BRepPrimAPI_MakeBox(gp_Pnt(0.06, -1.0, -1.0), 2.0, 2.0, 2.0).Shape()).Shape() + blend_record = expect_declined("a lobe of a self-intersecting fillet torus", blend_lobe, + "self-intersecting torus") + check("the fillet blend reaches the cell path and declines there, naming the blend", + "as a single cell: TGeoTorus: rmax" in (blend_record["reason"] or "") + and "fillet blend" in (blend_record["reason"] or ""), + (blend_record["reason"] or "")[:150]) + # ... and the description layer does refuse those numbers. + try: + prim.leaf("TGeoTorus", {"r": 0.0428825434729, "rmin": 0.0, "rmax": 0.1, + "phi1": 0.0, "dphi": 360.0}, prim.identity_frame()) + refused_kind = None + except prim.InvalidDescription: + refused_kind = "InvalidDescription" + except ValueError: + refused_kind = "ValueError" + check("the description layer refuses those numbers as an illegal solid", + refused_kind == "InvalidDescription", f"raised {refused_kind}") + # An illegal solid declines; a matcher bug still raises. + for label, kind, params in (("a missing parameter", "TGeoTorus", {"r": 1.0}), + ("an unknown leaf type", "TGeoNotAShape", {})): + try: + recognise._leaf(kind, params, prim.identity_frame()) + outcome = "returned a leaf" + except recognise.Declined: + outcome = "declined" + except ValueError: + outcome = "raised" + check(f"{label} still raises rather than declining", outcome == "raised", outcome) + + # --- flat-CSG R2: the elliptic cylinder --- + def elliptic_cylinder(a, b, dz, ref=None): + axis = gp_Ax2(gp_Pnt(0, 0, -dz), gp_Dir(0, 0, 1), + gp_Dir(*(ref if ref is not None else (1.0, 0.0, 0.0)))) + major, minor = max(a, b), min(a, b) + edge = BRepBuilderAPI_MakeEdge(gp_Elips(axis, major, minor)).Edge() + face = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakeWire(edge).Wire()).Face() + prism = BRepPrimAPI_MakePrism(face, gp_Vec(0, 0, 2 * dz)) + prism.Build() + return prism.Shape() + + def expect_eltu(name, solid, a, b, dz): + record = expect(name, solid, "tier1-eltu") + if not record["accepted"]: + return record + p = record["candidate"]["leaves"][0]["params"] + worst = max(abs(p["a"] - a), abs(p["b"] - b), abs(p["dz"] - dz)) + check(f"{name} reconstructs the source TGeoEltu parameters", worst < 1.0e-9, + f"a={p['a']:.6g} b={p['b']:.6g} dz={p['dz']:.6g}, worst {worst:.3g}") + return record + + # Both semi-axis orders, built the way `conv_eltu` writes them. + eltu_solid = elliptic_cylinder(3.0, 1.5, 5.0) + eltu_record = expect_eltu("elliptic cylinder, a > b", eltu_solid, 3.0, 1.5, 5.0) + expect_eltu("elliptic cylinder, a < b", + elliptic_cylinder(1.5, 3.0, 5.0, ref=(0.0, 1.0, 0.0)), 1.5, 3.0, 5.0) + # a == b is a circle, and it is still a TGeoEltu: the carrier is an extrusion, never a + # cylinder, so nothing can confuse the two. Asserted rather than left to chance. + circle_eltu = expect_eltu("elliptic cylinder with equal semi-axes", + elliptic_cylinder(2.0, 2.0, 5.0), 2.0, 2.0, 5.0) + check("an ellipse with equal semi-axes stays a TGeoEltu and is not read as a tube", + circle_eltu["accepted"] + and circle_eltu["candidate"]["leaves"][0]["type"] == "TGeoEltu", + circle_eltu["description"]) + placed_eltu = BRepBuilderAPI_Transform(elliptic_cylinder(3.0, 1.5, 5.0), + torus_shift.Multiplied(torus_spin), True).Shape() + placed_eltu_record = expect("placed elliptic cylinder", placed_eltu, "tier1-eltu") + check("a placed elliptic cylinder travels as one leaf plus a rigid placement", + placed_eltu_record["accepted"] + and prim.placement_for_candidate(placed_eltu_record["candidate"]) is not None, + "placement present" if placed_eltu_record["accepted"] else "not accepted") + + # --- R2 negative controls --- + # An extruded B-spline racetrack, which is not an ellipse. + racetrack = [] + for i in range(24): + ang = 2.0 * math.pi * i / 24.0 + racetrack.append(gp_Pnt(3.0 * math.cos(ang), + 1.5 * math.sin(ang) * (1.0 + 0.15 * math.cos(2 * ang)), -5.0)) + spline_pts = TColgp_HArray1OfPnt(1, len(racetrack)) + for i, pnt in enumerate(racetrack, start=1): + spline_pts.SetValue(i, pnt) + interp = GeomAPI_Interpolate(spline_pts, True, 1.0e-7) + interp.Perform() + oval_edge = BRepBuilderAPI_MakeEdge(interp.Curve()).Edge() + oval_face = BRepBuilderAPI_MakeFace(BRepBuilderAPI_MakeWire(oval_edge).Wire()).Face() + oval_prism = BRepPrimAPI_MakePrism(oval_face, gp_Vec(0, 0, 10.0)) + oval_prism.Build() + expect_declined("extruded B-spline racetrack (not an ellipse)", oval_prism.Shape(), + "free-form faces") + + # --- both new templates' instruments must be able to say "no" --- + true_eltu = prim.candidate("primitive", [prim.leaf( + "TGeoEltu", {"a": 3.0, "b": 1.5, "dz": 5.0}, prim.identity_frame())], "self-test") + nudged_eltu = prim.candidate("primitive", [prim.leaf( + "TGeoEltu", {"a": 3.0 + 1.0e-6, "b": 1.5, "dz": 5.0}, + prim.identity_frame())], "self-test") + eltu_gap = recognise._boundary_gap(prim.build_occ(true_eltu), prim.build_occ(nudged_eltu)) + check("the gap reports a semi-axis displaced by ten model tolerances", + abs(eltu_gap - 1.0e-6) < 1.0e-9, f"gap {eltu_gap:.3g} cm, expected 1e-06 cm") + true_torus = prim.candidate("primitive", [prim.leaf( + "TGeoTorus", {"r": 4.0, "rmin": 0.0, "rmax": 1.0, "phi1": 0.0, "dphi": 360.0}, + prim.identity_frame())], "self-test") + nudged_torus = prim.candidate("primitive", [prim.leaf( + "TGeoTorus", {"r": 4.0, "rmin": 0.0, "rmax": 1.0 + 1.0e-6, "phi1": 0.0, "dphi": 360.0}, + prim.identity_frame())], "self-test") + torus_gap = recognise._boundary_gap(prim.build_occ(true_torus), prim.build_occ(nudged_torus)) + check("the gap reports a tube radius displaced by ten model tolerances", + abs(torus_gap - 1.0e-6) < 1.0e-9, f"gap {torus_gap:.3g} cm, expected 1e-06 cm") + + # --- the descriptions must refuse illegal parameters --- + for label, kind, params in ( + ("a torus whose tube is wider than its major radius", "TGeoTorus", + {"r": 1.0, "rmin": 0.0, "rmax": 2.0, "phi1": 0.0, "dphi": 360.0}), + ("a torus with rmin above rmax", "TGeoTorus", + {"r": 4.0, "rmin": 1.0, "rmax": 0.5, "phi1": 0.0, "dphi": 360.0}), + ("a torus with dphi zero", "TGeoTorus", + {"r": 4.0, "rmin": 0.0, "rmax": 1.0, "phi1": 0.0, "dphi": 0.0}), + ("an elliptic cylinder with a zero semi-axis", "TGeoEltu", + {"a": 0.0, "b": 1.5, "dz": 5.0})): + try: + prim.leaf(kind, params, prim.identity_frame()) + refused = False + except ValueError: + refused = True + check(f"a description of {label} is refused", refused) + + check("every torus and elliptic-cylinder candidate matches its recorded candidate within tolerance", + *_recorded_match(_TORUS_ELTU_FIXTURES, seen_candidates)) + + # --- Tier 0: the quadric a stored B-spline face already is --- + from cadsupport import tier0 + from OCC.Core.BRepAdaptor import BRepAdaptor_Surface + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_GTransform, BRepBuilderAPI_NurbsConvert + from OCC.Core.BRepTools import breptools + from OCC.Core.GeomAbs import GeomAbs_Cylinder + from OCC.Core.TopAbs import TopAbs_FACE + from OCC.Core.TopExp import TopExp_Explorer + from OCC.Core.gp import gp_Ax3, gp_Cylinder, gp_GTrsf, gp_Mat + + check("the canonicaliser's band is the cascade's own", + tier0.REL_TOL == recognise.REL_TOL, + f"tier0 {tier0.REL_TOL:.0e} vs recognise {recognise.REL_TOL:.0e}") + + def nurbs(shape): + return BRepBuilderAPI_NurbsConvert(shape, True).Shape() + + def faces_of(shape): + found = [] + walk = TopExp_Explorer(shape, TopAbs_FACE) + while walk.More(): + found.append(topods.Face(walk.Current())) + walk.Next() + return found + + def samples_of(face, n): + adaptor = BRepAdaptor_Surface(face, True) + from cadsupport import analytic as converter + return converter._sample_surface_for_recognition(adaptor, *breptools.UVBounds(face), n=n) + + # (c) the instrument: a model displaced by a known amount must be reported at that size. + probe_cylinder = BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + 5.0, 8.0).Shape() + probe_points, _probe_normals = samples_of( + [f for f in faces_of(probe_cylinder) + if BRepAdaptor_Surface(f, True).GetType() == GeomAbs_Cylinder][0], 17) + probe_sphere_points, _ = samples_of(faces_of(BRepPrimAPI_MakeSphere(5.0).Shape())[0], 17) + probe_torus_points, _ = samples_of(faces_of(BRepPrimAPI_MakeTorus(6.0, 1.5).Shape())[0], 17) + for displacement in (1.0e-3, 1.0e-6, 1.0e-9): + for label, kind, model, points in ( + ("cylinder radius", "cylinder", + {"axis": [0.0, 0.0, 1.0], "origin": [0.0, 0.0, 0.0], + "radius": 5.0 + displacement}, probe_points), + ("sphere radius", "sphere", + {"centre": [0.0, 0.0, 0.0], "radius": 5.0 + displacement}, + probe_sphere_points), + ("torus tube radius", "torus", + {"axis": [0.0, 0.0, 1.0], "centre": [0.0, 0.0, 0.0], "major": 6.0, + "minor": 1.5 + displacement}, probe_torus_points)): + measured = tier0.surface_gap(kind, model, points) + check(f"the gap reports a {label} displaced by {displacement:.0e} cm at its true size", + abs(measured - displacement) <= 1.0e-9 * displacement + 1.0e-13, + f"measured {measured:.6g} cm, displaced {displacement:.0e} cm") + + # The same solid, written as NURBS, must convert to the same body. + tier0_pairs = ( + ("box", BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0).Shape(), "tier1-box", 1), + ("solid cylinder", BRepPrimAPI_MakeCylinder(ax, 2.0, 10.0).Shape(), "tier1-tube", 1), + ("tube segment", BRepPrimAPI_MakeCylinder(ax, 2.0, 10.0, math.radians(72.0)).Shape(), + "tier1-tubeseg", 1), + ("cone", BRepPrimAPI_MakeCone(ax, 3.0, 1.0, 6.0).Shape(), "tier1-cone", 1), + ("sphere", BRepPrimAPI_MakeSphere(3.0).Shape(), "tier1-sphere", 1), + ("solid torus", BRepPrimAPI_MakeTorus(6.0, 1.5).Shape(), "tier1-torus", 1), + ("hollow torus wedge", + BRepAlgoAPI_Cut(BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + 6.0, 1.5, math.radians(140.0)).Shape(), + BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + 6.0, 0.7, math.radians(140.0)).Shape()).Shape(), + "tier1-torus", 1), + ("cube with an axial through-hole", + BRepAlgoAPI_Cut(BRepPrimAPI_MakeBox(gp_Pnt(-3, -3, -3), 6.0, 6.0, 6.0).Shape(), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)), + 1.5, 10.0).Shape()).Shape(), + "cell-intersection", 2), + ) + + def realisation_gap(one, other): + """The largest distance between the two candidates' realised boundaries, in cm.""" + if one is None or other is None: + return float("inf") + if (one["op"], one["recogniser"], len(one["leaves"])) != \ + (other["op"], other["recogniser"], len(other["leaves"])): + return float("inf") + if [lf["type"] for lf in one["leaves"]] != [lf["type"] for lf in other["leaves"]]: + return float("inf") + return recognise._boundary_gap(prim.build_occ(one), prim.build_occ(other)) + + for label, solid, want_recogniser, want_leaves in tier0_pairs: + native_record = process_solid(solid, f"tier0 native {label}") + encoded = expect(f"NURBS-encoded {label}", nurbs(solid), want_recogniser, want_leaves) + deviation = realisation_gap(native_record["candidate"], encoded["candidate"]) + notes = (encoded["candidate"] or {}).get("notes", {}) + check(f"the NURBS-encoded {label} realises the analytic one's solid", + deviation <= 1.0e-9, + f"{notes.get('tier0Faces', 0)} canonicalised carrier(s) at a worst gap of " + f"{notes.get('tier0WorstGapRelative', float('nan')):.3g} of the part; the two " + f"realisations are {deviation:.3g} cm apart") + + check("every Tier-0 candidate matches its recorded candidate within tolerance", + *_recorded_match(_TIER0_FIXTURES, seen_candidates)) + + # (a) a free-form face must not canonicalise; its decline carries the best proposal's gap. + from cadsupport import analytic as converter + for label, face in ( + ("free-form saddle", converter._self_test_bezier_patch( + lambda s, t: (10 * s - 5, 10 * t - 5, (10 * s - 5) * (10 * t - 5) / 10.0), 6, 6)), + ("narrow free-form ridge", converter._self_test_bezier_patch( + lambda s, t: (20 * s - 10, 0.5 * t, + 0.02 * (20 * s - 10) ** 2 + 0.3 * (20 * s - 10) * t), 6, 6)), + ("swept non-circular profile (bulge 1e-2)", + converter._self_test_tapered_near_circle(1.0e-2, 1.0e-4))): + adaptor = BRepAdaptor_Surface(face, True) + carrier, gap = tier0.canonicalise(face, adaptor, 20.0) + check(f"a {label} is not canonicalised, and the gap says how far off it is", + carrier is None and gap is not None and gap > 10.0 * tier0.REL_TOL * 20.0, + f"{'declined' if carrier is None else 'ACCEPTED as ' + carrier['kind']}, best " + f"proposal {gap:.4g} cm away, {gap / 20.0:.3g} of the part against " + f"{tier0.REL_TOL:.0e}") + + # (b) a cylinder squashed by a `gp_GTrsf`: refused at ten tolerances, accepted at a tenth. + squash_radius, squash_scale = 5.0, 20.0 + squash_base = nurbs(BRepBuilderAPI_MakeFace( + gp_Cylinder(gp_Ax3(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(0, 0, 1), gp_Dir(1, 0, 0)), + squash_radius), 0.0, 2.0 * math.pi, 1.0, 9.0).Shape()) + squash_measured = {} + for multiple in (0.1, 1.0, 10.0): + intended = multiple * tier0.REL_TOL * squash_scale + transform = gp_GTrsf() + transform.SetVectorialPart(gp_Mat(1.0 + 2.0 * intended / squash_radius, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) + squashed = faces_of(BRepBuilderAPI_GTransform(squash_base, transform, True).Shape())[0] + carrier, gap = tier0.canonicalise(squashed, BRepAdaptor_Surface(squashed, True), + squash_scale) + squash_measured[multiple] = gap + want_accepted = multiple < 1.0 + check(f"a disguised cylinder displaced by {multiple:g} model tolerance(s) is " + f"{'accepted' if want_accepted else 'refused by the gap'}", + (carrier is not None) == want_accepted, + f"{'accepted as ' + carrier['kind'] if carrier else 'declined'}, " + f"measured gap {gap:.4g} cm, {gap / squash_scale:.3g} of the part against " + f"{tier0.REL_TOL:.0e}") + ratios = [squash_measured[m] / (m * tier0.REL_TOL * squash_scale) for m in (0.1, 1.0, 10.0)] + check("the measured gap is proportional to the displacement that caused it", + max(ratios) - min(ratios) <= 1.0e-3 * max(ratios), + f"gap / displacement = {', '.join(f'{r:.4f}' for r in ratios)}") + + # An empty proposal must decline as empty. + empty_common = BRepAlgoAPI_Common( + BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 1.0, 1.0, 1.0).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(9, 9, 9), 1.0, 1.0, 1.0).Shape()).Shape() + try: + recognise._boundary_gap(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 1.0, 1.0, 1.0).Shape(), + empty_common) + empty_reason = "no decline" + except recognise.Declined as declined: + empty_reason = str(declined) + check("an empty proposal declines as empty, not as an OCCT measurement failure", + "the proposal is empty" in empty_reason, empty_reason) + + from OCC.Core.BRep import BRep_Builder + from OCC.Core.TopoDS import TopoDS_Compound + + # --- Rung 4: the union of cells --- + # + # Every body here is one whose cell count is known in closed form. + from cadsupport import decompose as decomp + + def expect_cells(name, solid, want_cells, want_leaves=None, **kwargs): + record = process_solid(solid, name, **kwargs) + seen_recognisers[name] = record["recogniser"] if record["accepted"] else None + if record["accepted"]: + seen_candidates[name] = json.loads(json.dumps(record["candidate"], sort_keys=True)) + notes = (record["candidate"] or {}).get("notes", {}) + ok = (record["accepted"] and record["recogniser"] == "cells-union" + and notes.get("nCells") == want_cells + and (want_leaves is None or notes.get("nLeaves") == want_leaves)) + detail = (f"{notes.get('nCells')} cell(s) of {notes.get('cellLeaves')} leaves, " + f"{notes.get('nSplits')} split(s), volume drift " + f"{notes.get('volumeDriftRelative', float('nan')):.3g}, gap " + f"{notes.get('cellGapCm', float('nan')):.3g} cm" + if record["accepted"] else f"declined: {record['reason']}") + check(f"{name} converts as {want_cells} cells", ok, detail) + return record + + l_plate = BRepAlgoAPI_Cut(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 4.0, 4.0, 1.0).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(2, 2, -1), 4.0, 4.0, 3.0).Shape()).Shape() + grooved = BRepAlgoAPI_Cut(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 6.0, 4.0, 3.0).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(2, -1, 1), 2.0, 6.0, 3.0).Shape()).Shape() + # Prism-family parts, driven through `recognise_union_of_cells` directly for their counts. + for label, solid, want in (("an L-plate", l_plate, 2), ("a grooved block", grooved, 3)): + cand, why = recognise.recognise_union_of_cells(solid) + gap = (None if cand is None else + recognise._boundary_gap(prim.build_occ(cand), solid)) + check(f"{label} decomposes into {want} cells and realises the solid", + cand is not None and cand["notes"]["nCells"] == want and gap <= 1.0e-9, + (f"{cand['notes']['nCells']} cells of {cand['notes']['cellLeaves']} leaves, " + f"{gap:.3g} cm from the part" if cand else f"declined: {why}")) + + # A hexagonal collar on a cylinder: two cells, one eight halfspaces wide. + hex_collar = BRepAlgoAPI_Fuse( + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)), 3.0, 5.0).Shape(), + swept_polygon(3.0, 6, 0.0, 5.0)).Shape() + expect_cells("a cylinder with a hexagonal collar", hex_collar, 2, want_leaves=9) + + # Two rods sharing no edge: only the connectivity split finds the two cells. + disjoint = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(disjoint) + builder.Add(disjoint, BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), 1.0, 5.0).Shape()) + builder.Add(disjoint, BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(6, 0, 0), gp_Dir(0, 0, 1)), 1.0, 5.0).Shape()) + disjoint_record = expect_cells("two rods sharing no edge", disjoint, 2, want_leaves=2) + check("the disjoint pair is found by connectivity and needs no split at all", + disjoint_record["accepted"] + and disjoint_record["candidate"]["notes"]["nComponents"] == 2 + and disjoint_record["candidate"]["notes"]["nSplits"] == 0 + and _count_trusted_concave(disjoint) == 0, + f"{_count_trusted_concave(disjoint)} trusted concave edge(s), " + f"{(disjoint_record['candidate'] or {}).get('notes', {}).get('nSplits')} split(s)") + + # A torus with a cylinder through it, whose cells are not all planar. + torus_through = BRepAlgoAPI_Fuse( + torus_at(2.5, 0.8), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -2.0), gp_Dir(0, 0, 1)), + 2.0, 4.0).Shape()).Shape() + expect_cells("a torus with a cylinder through it", torus_through, 2, want_leaves=3) + + # (a) the volume guard: a component walk that loses one of three boxes must be refused. + three_boxes = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(three_boxes) + for x in (0.0, 4.0, 8.0): + builder.Add(three_boxes, BRepPrimAPI_MakeBox(gp_Pnt(x, 0, 0), 2.0, 2.0, 2.0).Shape()) + expect_cells("three disjoint boxes", three_boxes, 3, want_leaves=3) + intact_components = decomp.solid_components + try: + decomp.solid_components = lambda shape: intact_components(shape)[:-1] + _lost, lost_why = recognise.recognise_union_of_cells(three_boxes) + finally: + decomp.solid_components = intact_components + check("a decomposition that loses a cell is refused by the volume guard", + _lost is None and "volume" in (lost_why or ""), lost_why or "ACCEPTED") + check("the volume guard reports the drift it measured, at its true size", + _lost is None and "0.333" in (lost_why or ""), + f"one box of three is 1/3 of the part; the decline says: " + f"{(lost_why or '')[:120]}") + + # (b) the budgets, each declining by name. + _over_cells, cells_why = recognise.recognise_union_of_cells(grooved, max_cells=2) + check("a part over the cell budget declines naming the bound", + _over_cells is None and "cell budget of 2" in (cells_why or ""), cells_why or "ACCEPTED") + _over_leaves, leaves_why = recognise.recognise_union_of_cells(grooved, max_leaves=2) + check("a part over the leaf budget declines naming the bound", + _over_leaves is None and "part budget of 2" in (leaves_why or ""), + leaves_why or "ACCEPTED") + + # (c) the DNF is two levels and the emitter refuses a third. + flat_cell = prim.cell("primitive", [prim.leaf("TGeoBBox", {"dx": 1.0, "dy": 1.0, "dz": 1.0}, + prim.identity_frame())]) + for label, cells_in in ( + ("a cell that is itself a union", + [flat_cell, {"op": "union", "leaves": [flat_cell["leaves"][0]] * 2}]), + ("a cell carrying a recogniser of its own", + [flat_cell, {"op": "primitive", "leaves": flat_cell["leaves"], + "recogniser": "nested"}]), + ("a single cell called a union", [flat_cell])): + try: + prim.union_of_cells(cells_in, "self-test") + refused = False + except (ValueError, prim.InvalidDescription): + refused = True + check(f"a description with {label} is refused", refused) + + # N cells must give a union tree of depth ceil(log2 N). + if with_root: + import ROOT as _ROOT + + def union_depth(shape): + if shape.ClassName() != "TGeoCompositeShape": + return 0 + node = shape.GetBoolNode() + return 1 + max(union_depth(node.GetLeftShape()), union_depth(node.GetRightShape())) + + ladder = [] + for n_cells in (2, 3, 5, 8): + comp = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(comp) + for i in range(n_cells): + builder.Add(comp, BRepPrimAPI_MakeBox(gp_Pnt(4.0 * i, 0, 0), + 2.0, 2.0, 2.0).Shape()) + cand, why = recognise.recognise_union_of_cells(comp) + shape, _placement = prim.build_root(cand, f"balanced{n_cells}") if cand else (None, None) + want = math.ceil(math.log2(n_cells)) + got = union_depth(shape) if shape is not None else -1 + ladder.append((n_cells, got, want)) + check(f"{n_cells} cells emit a balanced union tree of depth {want}", got == want, + f"depth {got}" if cand else f"declined: {why}") + check("the union tree's depth is logarithmic in the cell count, not linear", + all(got == want for _n, got, want in ladder), + ", ".join(f"{n}->{got}" for n, got, _w in ladder)) + + # A two-level description must survive the round trip through `csg_.json`. + if with_root: + round_trip = json.loads(json.dumps(disjoint_record["candidate"])) + rebuilt, rebuilt_placement = prim.build_root(round_trip, "roundtrip") + direct, _direct_placement = prim.build_root(disjoint_record["candidate"], "direct") + check("a two-level description survives the JSON round trip byte for byte", + json.dumps(round_trip, sort_keys=True) + == json.dumps(disjoint_record["candidate"], sort_keys=True) + and rebuilt.ClassName() == direct.ClassName() and rebuilt_placement is None, + f"{rebuilt.ClassName()}, placement " + f"{'present' if rebuilt_placement else 'absent'}") + gap = recognise._boundary_gap(prim.build_occ(round_trip), + prim.build_occ(disjoint_record["candidate"])) + check("the round-tripped description realises the same solid", gap <= 1.0e-12, + f"{gap:.3g} cm apart") + + check("every union-of-cells candidate matches its recorded candidate within tolerance", + *_recorded_match(_UNION_OF_CELLS_FIXTURES, seen_candidates)) + + # --- the flat emitter's sign convention, measured against `recognise._cell_leaf` ---------- + import struct + from cadsupport import decompose, flat as flatmod + + def _flat_gradient(block, point): + """`|grad f|` at a point, for turning a quadric value into a first-order distance.""" + c = block["c"] + x, y, z = point + if block["kind"] == "torus": + return 1.0 # the torus block already IS a signed distance + gx = 2.0 * (c[0] * x + c[1] * y + c[2] * z + c[6]) + gy = 2.0 * (c[1] * x + c[3] * y + c[4] * z + c[7]) + gz = 2.0 * (c[2] * x + c[4] * y + c[5] * z + c[8]) + return math.sqrt(gx * gx + gy * gy + gz * gz) + + def _flat_oracle(name, solid, seed=20260824, samples=4000): + """The flat blocks of a one-cell solid, and `_cell_leaf`'s verdict on sampled points. + + Points within `REL_TOL x max(diag, 1)` of a carrier surface, or ON it, are not scored. + """ + import random + from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier + from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON + from OCC.Core.gp import gp_Pnt + diag = decompose.bbox_diagonal(solid) + tol = recognise.REL_TOL * max(diag, 1.0) + carriers = recognise._halfspace_carriers(solid, tol) + box = recognise._CellBox(solid, diag) + blocks = flatmod.blocks_from_carriers(carriers) + leaves = [recognise._cell_leaf(c, box) for c in carriers] + cand = prim.cell("intersection" if len(leaves) > 1 else "primitive", leaves) + classifier = BRepClass3d_SolidClassifier(prim.build_occ(cand)) + rng = random.Random(seed) + (xlo, ylo, zlo, xhi, yhi, zhi) = recognise._bbox_of(solid) + scored = [] + for _ in range(samples): + point = (rng.uniform(xlo, xhi), rng.uniform(ylo, yhi), rng.uniform(zlo, zhi)) + near = min(abs(flatmod.eval_block(b, point)) + / max(_flat_gradient(b, point), 1.0e-300) for b in blocks) + if near <= tol: + continue + classifier.Perform(gp_Pnt(*point), tol) + state = classifier.State() + if state == TopAbs_ON: + continue + scored.append((point, state == TopAbs_IN)) + worst_plane = max((flatmod.plane_scaling_error(b) for b in blocks + if flatmod.plane_scaling_error(b) is not None), default=None) + return {"name": name, "solid": solid, "carriers": carriers, "blocks": blocks, + "points": scored, "kinds": sorted({c["kind"] for c in carriers}), + "sides": sorted({c["side"] for c in carriers}), "worstPlane": worst_plane} + + def _flat_disagreements(blocks, points): + return sum(1 for point, occ_inside in points + if flatmod.flat_contains(blocks, point) != occ_inside) + + flat_axis = gp_Ax2(gp_Pnt(0, 0, -5), gp_Dir(0, 0, 1)) + flat_tube = BRepAlgoAPI_Cut( + BRepPrimAPI_MakeCylinder(flat_axis, 2.0, 10.0).Shape(), + BRepPrimAPI_MakeCylinder(gp_Ax2(gp_Pnt(0, 0, -6), gp_Dir(0, 0, 1)), 1.0, 12.0).Shape() + ).Shape() + # a box with a spherical scoop taken out of one corner: six planes and an EXTERIOR sphere + flat_scooped = BRepAlgoAPI_Cut( + BRepPrimAPI_MakeBox(gp_Pnt(-3, -3, -3), 6.0, 6.0, 6.0).Shape(), + BRepPrimAPI_MakeSphere(gp_Pnt(3, 3, 3), 2.5).Shape()).Shape() + # A cylinder about (1, 1, 1): the only fixture with off-diagonal quadric coefficients. + flat_tilted = BRepPrimAPI_MakeCylinder( + gp_Ax2(gp_Pnt(-1, -1, -1), gp_Dir(1, 1, 1)), 1.5, 6.0).Shape() + flat_cases = ( + ("a box", BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0).Shape()), + ("a tube, whose bore is an exterior cylinder", flat_tube), + ("a cone frustum", BRepPrimAPI_MakeCone(flat_axis, 3.0, 1.0, 10.0).Shape()), + ("a hemisphere", BRepAlgoAPI_Common( + BRepPrimAPI_MakeSphere(gp_Pnt(1, 2, 3), 2.5).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(-3, -1, 3), 9.0, 9.0, 9.0).Shape()).Shape()), + ("a box with a spherical scoop, an exterior sphere", flat_scooped), + ("a torus ply", BRepPrimAPI_MakeTorus(gp_Ax2(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1)), + 4.0, 1.0).Shape()), + ("a cylinder tilted about (1,1,1), whose quadric is dense", flat_tilted), + ) + flat_results = [_flat_oracle(name, solid) for name, solid in flat_cases] + for result in flat_results: + bad = _flat_disagreements(result["blocks"], result["points"]) + check(f"the flat halfspaces of {result['name']} classify exactly as _cell_leaf's " + "primitives", + bad == 0 and len(result["points"]) > 0.5 * 4000, + f"{bad} of {len(result['points'])} scored points disagree; carriers " + f"{'+'.join(result['kinds'])} ({'+'.join(result['sides'])})") + + # All five carrier kinds must be covered. + flat_kinds_seen = sorted({k for r in flat_results for k in r["kinds"]}) + check("the flat oracle comparison covers all five carrier kinds", + flat_kinds_seen == ["cone", "cylinder", "plane", "sphere", "torus"], + f"covered {flat_kinds_seen}") + check("the flat oracle comparison exercises a complemented (exterior) carrier", + any("exterior" in r["sides"] for r in flat_results), + "; ".join(f"{r['name']}: {'+'.join(r['sides'])}" for r in flat_results)) + # and a quadric with genuinely non-zero off-diagonal terms, per the note on `flat_tilted` + flat_dense = [r["name"] for r in flat_results + if any(b["kind"] == "quadric" and max(abs(b["c"][1]), abs(b["c"][2]), + abs(b["c"][4])) > 1.0e-3 + for b in r["blocks"])] + check("the flat oracle comparison exercises off-diagonal quadric coefficients", + bool(flat_dense), f"dense-quadric fixtures: {flat_dense}") + + # The negative control: inverting any one halfspace of any fixture must be caught. + flat_missed = [] + for result in flat_results: + for index in range(len(result["blocks"])): + flipped = [dict(b, sign=-b["sign"]) if i == index else b + for i, b in enumerate(result["blocks"])] + if _flat_disagreements(flipped, result["points"]) == 0: + flat_missed.append(f"{result['name']}[{index}]") + flat_flips = sum(len(r["blocks"]) for r in flat_results) + check("inverting any one halfspace of any fixture is caught by the same comparison", + flat_flips > 0 and not flat_missed, + f"{flat_flips} inversion(s) over {len(flat_results)} fixtures, missed {flat_missed}") + + # --- the cone's mirror nappe beyond the apex, outside the sampled box -------------------- + flat_cone_result = next(r for r in flat_results if r["name"] == "a cone frustum") + flat_cone_carrier = next(c for c in flat_cone_result["carriers"] if c["kind"] == "cone") + flat_apex, flat_k = flatmod.cone_apex(flat_cone_carrier) + flat_axis_d = flat_cone_carrier["d"] + flat_ref = flat_cone_carrier["x"] + + def _flat_along_apex(steps, radial=0.0): + """A point `steps` along the axis from the apex, positive being the material side.""" + walk = math.copysign(1.0, flat_k) * steps + return tuple(flat_apex[i] + walk * flat_axis_d[i] + radial * flat_ref[i] + for i in range(3)) + + # a point strictly inside the mirror nappe: |r + k u| = |k| * 5 there, and the radius is half + flat_mirror = _flat_along_apex(-5.0, radial=0.5 * abs(flat_k) * 5.0) + flat_real = _flat_along_apex(5.0, radial=0.5 * abs(flat_k) * 5.0) + # the emitter's contract for ONE cone carrier, isolated from the fixture's caps + flat_cone_blocks = flatmod.blocks_from_carriers([flat_cone_carrier]) + flat_cone_quadric = [b for b in flat_cone_blocks + if not (b["kind"] == "quadric" and all(b["c"][i] == 0.0 + for i in range(6)))] + check("the cone quadric alone would admit a point on the mirror nappe", + len(flat_cone_quadric) == 1 and flatmod.flat_contains(flat_cone_quadric, flat_mirror), + f"the point {tuple(round(v, 6) for v in flat_mirror)} beyond the apex " + f"{tuple(round(v, 6) for v in flat_apex)}") + check("the emitted interior cone excludes the mirror nappe beyond its apex", + len(flat_cone_blocks) == 2 + and not flatmod.flat_contains(flat_cone_blocks, flat_mirror), + f"{len(flat_cone_blocks)} block(s) for one carrier, apex plane included") + check("the apex plane cuts nothing on the cone's real nappe", + flatmod.flat_contains(flat_cone_blocks, flat_real), + f"the mirrored point {tuple(round(v, 6) for v in flat_real)} is still material") + flat_apex_plane = flatmod.cone_apex_plane(flat_cone_carrier) + check("the apex plane obeys the 2b = n convention like any other plane", + flatmod.plane_scaling_error(flat_apex_plane) < 1.0e-15, + f"residual {flatmod.plane_scaling_error(flat_apex_plane)}") + + # An exterior cone gets no apex plane; `check_cell_box` declines it past the apex. + flat_exterior_cone = dict(flat_cone_carrier, side="exterior") + check("an exterior cone is not silently given an apex plane", + flatmod.cone_apex_plane(flat_exterior_cone) is None, + "cone_apex_plane declines to repair a complemented cone") + + def _flat_cube_at(centre, half=0.5): + return ([centre[i] - half for i in range(3)], [centre[i] + half for i in range(3)]) + + flat_past_lo, flat_past_hi = _flat_cube_at(_flat_along_apex(-5.0)) + try: + flatmod.check_cell_box([flat_exterior_cone], flat_past_lo, flat_past_hi) + flat_box_reason = "" + except recognise.Declined as why: + flat_box_reason = str(why) + check("an exterior cone whose cell box reaches past its apex is declined", + "mirror nappe" in flat_box_reason, f"reason: {flat_box_reason or 'nothing raised'}") + flat_short_lo, flat_short_hi = _flat_cube_at(_flat_along_apex(5.0)) + try: + flatmod.check_cell_box([flat_exterior_cone], flat_short_lo, flat_short_hi) + flat_stay_ok = True + except recognise.Declined: + flat_stay_ok = False + check("an exterior cone whose cell box stays short of its apex is not declined", + flat_stay_ok, f"box {tuple(round(v, 3) for v in flat_short_lo)} .. " + f"{tuple(round(v, 3) for v in flat_short_hi)}") + # and an INTERIOR cone is never refused by that check, since its apex plane already fixed it + try: + flatmod.check_cell_box([flat_cone_carrier], flat_past_lo, flat_past_hi) + flat_interior_ok = True + except recognise.Declined: + flat_interior_ok = False + check("an interior cone is not refused for reaching past its apex", + flat_interior_ok, "the apex plane already removed the mirror nappe") + + # The plane convention |2b| = 1, asserted where the planes are created. + flat_plane_worst = max((r["worstPlane"] for r in flat_results + if r["worstPlane"] is not None), default=None) + check("every emitted plane block stores 2b = n for a unit normal", + flat_plane_worst is not None and flat_plane_worst < 1.0e-15, + f"worst | |2b| - 1 | over the fixtures: " + f"{'no plane blocks' if flat_plane_worst is None else f'{flat_plane_worst:.3g}'}") + # negative control on that check itself: a plane rescaled by 3 must be caught + flat_tripled = {"kind": "quadric", "sign": 1.0, + "c": [0.0] * 6 + [1.5, 0.0, 0.0, -3.0, 0.0]} + check("a plane block rescaled by three is refused by the convention check", + abs(flatmod.plane_scaling_error(flat_tripled) - 2.0) < 1.0e-15, + f"residual {flatmod.plane_scaling_error(flat_tripled)}") + + # A carrier kind with no quadric form declines rather than emitting a wrong halfspace. + try: + flatmod.quadric_from_carrier({"kind": "torus", "side": "interior"}) + flat_declined = "" + except recognise.Declined as why: + flat_declined = str(why) + check("a carrier with no quadric form is declined, not guessed at", + "no quadric form" in flat_declined, f"reason: {flat_declined or 'nothing raised'}") + + # A torus axis is normalised on the way into a block, as `AddTorus` does on load. + flat_long_axis = flatmod.blocks_from_carriers( + [{"kind": "torus", "side": "interior", "p": (0.0, 0.0, 0.0), "d": (0.0, 0.0, 3.0), + "r": 4.0, "rt": 1.0}])[0] + check("a torus block's axis is a unit vector whatever the carrier carried", + abs(math.sqrt(sum(flat_long_axis["c"][3 + i] ** 2 for i in range(3))) - 1.0) < 1.0e-15, + f"axis {tuple(flat_long_axis['c'][3:6])}") + + # Sidecar record sizes: 20-byte header, 100-byte halfspace, 64-byte cell, little-endian. + flat_sidecar = Path("/tmp/csg_selftest_flatcsg.bin") + flat_probe_blocks = flat_results[1]["blocks"] + flat_probe_cells = [{"first": 0, "count": len(flat_probe_blocks), "volume": 1.5, + "lo": [-2.0, -2.0, -5.0], "hi": [2.0, 2.0, 5.0]}] + flatmod.write_sidecar(flat_sidecar, flat_probe_blocks, flat_probe_cells) + flat_bytes = flat_sidecar.read_bytes() + check("the sidecar is magic + version + two counts + fixed-length records", + len(flat_bytes) == 20 + 100 * len(flat_probe_blocks) + 64 * len(flat_probe_cells) + and flat_bytes[:8] == flatmod.SIDECAR_MAGIC + and struct.unpack(" 0, + "no containment corroboration on the flat record") + + # Routing: the flat path runs only after the union path declines. + routed, _routed_why = recognise.recognise(hex_collar) + check("a part the tree path accepts is NOT intercepted by the flat path", + routed is not None and routed["recogniser"] == "cells-union", + routed["recogniser"] if routed else "declined") + + # --- R5: an exterior cone judged against its cell box, not the part box ----------------- + l_cone_solid = BRepAlgoAPI_Cut( + BRepAlgoAPI_Fuse(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 10.0, 4.0, 4.0).Shape(), + BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 4), 4.0, 4.0, 6.0).Shape()).Shape(), + BRepPrimAPI_MakeCone(gp_Ax2(gp_Pnt(7, 2, 0), gp_Dir(0, 0, 1)), + 1.5, 0.0, 8.0).Shape()).Shape() + cone_record, cone_why = recognise.recognise_flat_cells(l_cone_solid) + check("a multi-cell part with an exterior cone converts on the flat path", + cone_record is not None and cone_record["notes"]["nCells"] == 2 + and cone_record["notes"]["cellGapCm"] <= 1.0e-9, + cone_why or f"{cone_record['notes']['nCells']} cells, gap " + f"{cone_record['notes']['cellGapCm']:.3g} cm") + + # the cell's own box accepts, the part's box refuses + l_cone_diag = recognise._bbox_diagonal(l_cone_solid) + l_cone_tol = recognise.REL_TOL * max(l_cone_diag, 1.0) + l_cone_report = decomp.split_into_cells(l_cone_solid, scale=max(l_cone_diag, 1.0)) + l_cone_part_box = recognise._bbox_of(l_cone_solid) + cone_own, cone_part = [], [] + for piece in l_cone_report["pieces"]: + _lv, piece_carriers, _out = recognise._cell_leaves( + piece, l_cone_tol, decomp.bbox_diagonal(piece), whole_part=False) + if not any(c["kind"] == "cone" and c["side"] == "exterior" for c in piece_carriers): + continue + piece_lo, piece_hi = recognise._flat_cell_box( + piece, recognise._FLAT_BOX_MARGIN * max(l_cone_diag, 1.0)) + for label, box_lo, box_hi in (("own", piece_lo, piece_hi), + ("part", list(l_cone_part_box[:3]), + list(l_cone_part_box[3:]))): + try: + flatmod.check_cell_box(piece_carriers, box_lo, box_hi) + (cone_own if label == "own" else cone_part).append("accepted") + except recognise.Declined: + (cone_own if label == "own" else cone_part).append("declined") + check("the exterior cone is judged against its CELL's box, which the PART's box would fail", + cone_own == ["accepted"] and cone_part == ["declined"], + f"own box {cone_own}, part box {cone_part}") + + # and the call site really does hand `check_cell_box` the boxes it writes, per cell + seen_boxes = [] + intact_check = flatmod.check_cell_box + try: + def _recording_check(carriers, lo, hi): + seen_boxes.append(([float(v) for v in lo], [float(v) for v in hi])) + return intact_check(carriers, lo, hi) + flatmod.check_cell_box = _recording_check + boxed_record, _boxed_why = recognise.recognise_flat_cells(l_cone_solid) + finally: + flatmod.check_cell_box = intact_check + check("check_cell_box is called once per cell with exactly the box the sidecar carries", + boxed_record is not None + and len(seen_boxes) == len(boxed_record["cells"]) + and all(seen == ([float(v) for v in c["lo"]], [float(v) for v in c["hi"]]) + for seen, c in zip(seen_boxes, boxed_record["cells"])), + f"{len(seen_boxes)} call(s) for " + f"{len(boxed_record['cells']) if boxed_record else '?'} cell(s)") + + # a `check_cell_box` refusal becomes a decline naming the cell + try: + def _refusing_check(carriers, lo, hi): + raise recognise.Declined("a self-test refusal from check_cell_box") + flatmod.check_cell_box = _refusing_check + refused, refused_why = recognise.recognise_flat_cells(l_cone_solid) + finally: + flatmod.check_cell_box = intact_check + check("a check_cell_box refusal becomes a decline naming the cell it came from", + refused is None and "a self-test refusal from check_cell_box" in (refused_why or "") + and "cell 1 of 2" in (refused_why or ""), refused_why or "ACCEPTED") + + # --- R5: the cell bounding box is an outer bound, checked rather than assumed ------------- + box_escapes = [] + for record_label, record_cand in (("hex collar", flat_record), ("L with a cone", cone_record)): + for index, c in enumerate(record_cand["cells"]): + span = [c["hi"][i] - c["lo"][i] for i in range(3)] + rng = flat_random.Random(90210 + index) + for _ in range(3000): + point = tuple(c["lo"][i] - span[i] + rng.random() * 3.0 * span[i] + for i in range(3)) + inside_box = all(c["lo"][i] <= point[i] <= c["hi"][i] for i in range(3)) + if not inside_box and flatmod.flat_contains(c["blocks"], point): + box_escapes.append(f"{record_label} cell {index}") + break + check("no cell reaches outside the bounding box its record declares", + not box_escapes, "; ".join(box_escapes) or "2 records, 4 cells, 12000 points sampled") + escaped = None + try: + # one plane, `x <= 0`: an unbounded cell, and the box cannot hold it + recognise._flat_box_holds_cell( + [{"kind": "quadric", "sign": 1.0, "c": [0.0] * 6 + [0.5, 0.0, 0.0, 0.0] + [0.0]}], + [-1.0, -1.0, -1.0], [1.0, 1.0, 1.0]) + except recognise.Declined as declined: + escaped = str(declined) + check("the outward probe catches a cell that is not closed up by its own halfspaces", + escaped is not None and "do not close the cell up" in escaped + and "Widening the declared box" in escaped, + escaped or "ACCEPTED an unbounded cell") + + # a corroboration that scored no point is a decline + intact_disagreements = accept.contains_disagreements + try: + accept.contains_disagreements = lambda *args, **kwargs: (0, 0, 0.0) + empty_scored, empty_why = recognise.recognise_flat_cells(l_cone_solid) + finally: + accept.contains_disagreements = intact_disagreements + check("a containment corroboration that scored no point is a decline, not a pass", + empty_scored is None and "scored no point" in (empty_why or ""), + empty_why or "ACCEPTED on an empty measurement") + + # --- R5: the twin-parity gate, in the live and the deferred --from-json emission paths ---- + if with_root: + import copy + import tempfile + import ROOT + from cadsupport import emit as emit_mod, hook as hook_mod + ROOT.gROOT.SetBatch(True) + + # A cell whose declared box does not contain it: the lower arm's box cut off at x = 5. + out_of_box = copy.deepcopy(cone_record) + wide_cell = max(range(len(out_of_box["cells"])), + key=lambda i: out_of_box["cells"][i]["hi"][0]) + out_of_box["cells"][wide_cell]["hi"][0] = 5.0 + + # A debug build's `CloseShape` aborts on that cell, detected by its message in the library. + marker = b"a cell reaches past the bounding box SetCellBBox was" + library = Path(f"{ROOT.gSystem.Getenv('O2_ROOT')}/lib/libO2CADSupport.so") + asserts_compiled = library.exists() and marker in library.read_bytes() + + def _gate_probe(folder, candidate, patched_parity=None): + """Run both emission paths over one candidate; returns their two verdicts.""" + folder = Path(folder) + live = folder / "live" + deferred = folder / "deferred" + live.mkdir(parents=True, exist_ok=True) + deferred.mkdir(parents=True, exist_ok=True) + intact_process = emit_mod.process_solid + intact_parity = emit_mod.twin_parity + try: + emit_mod.process_solid = lambda solid, name, **kw: { + "part": name, "recognised": True, "accepted": True, "candidate": candidate, + "reason": None, "recogniser": "flat-cells", + "description": prim.describe(candidate), + "acceptance": {"accepted": True, "symmetricDifference": 0.0, "band": 1.0, + "relativeToVolume": 0.0}} + if patched_parity is not None: + emit_mod.twin_parity = lambda shape, **kw: patched_parity + csg_files, flat_files, records = hook_mod.recognise_and_emit( + {"probe": l_cone_solid}, {"probe": "probe"}, 1.0, live, + lambda name: str(name), verbose=False) + (deferred / "csg_probe.json").write_text(json.dumps( + {"part": "probe", "lid": "probe", "candidate": candidate, + "acceptance": {}, "recogniser": "flat-cells", "placement": None})) + written, refused = emit_mod.from_json(deferred, quiet=True) + finally: + emit_mod.process_solid = intact_process + emit_mod.twin_parity = intact_parity + return {"record": records[0], "csgFiles": csg_files, "flatFiles": flat_files, + "liveArtifacts": sorted(p.name for p in live.glob("*") + if p.suffix in (".root", ".bin")), + "written": written, "refused": refused, + "deferredArtifacts": sorted(p.name for p in deferred.glob("*") + if p.suffix in (".root", ".bin"))} + + # (a) the sound candidate must still pass both paths + with tempfile.TemporaryDirectory() as folder: + good = _gate_probe(folder, cone_record) + check("a sound flat candidate is emitted by both paths", + good["record"]["accepted"] and good["record"].get("flatSidecar") + and good["flatFiles"] and not good["csgFiles"] + and len(good["written"]) == 1 and not good["refused"] + and good["record"]["twinParity"]["disagreements"] == 0 + and "flatcsg_probe.bin" in good["deferredArtifacts"], + f"live {good['liveArtifacts']}, deferred {good['deferredArtifacts']}") + + # (b) the gate's REJECT branch, driven by a parity count, in both paths + with tempfile.TemporaryDirectory() as folder: + forced = _gate_probe(folder, cone_record, + patched_parity={"points": 20000, "disagreements": 37, + "insideAccelerated": 4000, "growFactor": 1.0}) + record = forced["record"] + check("a twin disagreement drops the part a tier on the live path", + not record["accepted"] and record["shape"] is None + and record.get("flatSidecar") is None + and "_Loop twin" in (record["reason"] or "") and "37 of 20000" in (record["reason"] or "") + and not forced["flatFiles"] and not forced["csgFiles"] + and forced["liveArtifacts"] == [], + f"accepted={record['accepted']}, sidecar={record.get('flatSidecar')}, " + f"artifacts {forced['liveArtifacts']}, reason {(record['reason'] or '')[:80]}") + check("a twin disagreement refuses the part on the deferred --from-json path", + not forced["written"] and len(forced["refused"]) == 1 + and forced["deferredArtifacts"] == [], + f"written {forced['written']}, refused {len(forced['refused'])}, " + f"artifacts {forced['deferredArtifacts']}") + + # (c) and the gate detects the geometric condition itself + if asserts_compiled: + check("a cell outside its declared box is caught before it can ship", + True, + "not exercised here: this build compiles O2FlatCSG::CloseShape's own " + "debug-build sampler for the same condition, which aborts the process rather " + "than returning, so the shape cannot be built to be measured") + check("the out-of-box candidate is refused by both emission paths", True, + "not exercised here: same reason") + else: + broken_shape, _broken_placement = prim.build_root(out_of_box, "probe_out_of_box") + # Pinned, not defaulted: this is a negative control and its sensitivity must not + # move with `_TWIN_PARITY_PER_CELL` or with the fixture's cell count. + broken_parity = emit_mod.twin_parity(broken_shape, n_points=20000) + check("a cell outside its declared box is caught before it can ship", + broken_parity["disagreements"] > 0 + and broken_parity["insideAccelerated"] > 0, + f"{broken_parity['disagreements']} of {broken_parity['points']} points " + f"disagree ({broken_parity['insideAccelerated']} inside the accelerated shape)") + with tempfile.TemporaryDirectory() as folder: + real = _gate_probe(folder, out_of_box) + check("the out-of-box candidate is refused by both emission paths", + not real["record"]["accepted"] and real["record"]["shape"] is None + and real["record"].get("flatSidecar") is None + and real["liveArtifacts"] == [] and not real["written"] + and len(real["refused"]) == 1 and real["deferredArtifacts"] == [], + f"live {real['liveArtifacts']}, deferred {real['deferredArtifacts']}, " + f"reason {(real['record']['reason'] or '')[:90]}") + + # --- R5: the sidecar the macro loads, and the shape the gate scores, are one solid --------- + flat_blocks, flat_sidecar_cells = prim.flat_sidecar_records(cone_record) + check("the sidecar's cell table indexes its concatenated halfspace blocks", + len(flat_blocks) == cone_record["notes"]["nHalfspaces"] + and [c["count"] for c in flat_sidecar_cells] + == [len(c["blocks"]) for c in cone_record["cells"]] + and [c["first"] for c in flat_sidecar_cells] + == list(itertools.accumulate([0] + [len(c["blocks"]) + for c in cone_record["cells"]][:-1])) + and all(c["volume"] > 0.0 for c in flat_sidecar_cells), + f"{len(flat_blocks)} block(s), {len(flat_sidecar_cells)} cell(s)") + + # --- the ROOT half: the emitted TGeoShape must answer like the closed form --- + if with_root: + import ROOT + ROOT.gROOT.SetBatch(True) + from array import array + import random + shape, placement = prim.build_root(moved_record["candidate"], "probe_moved") + # A placed primitive is the bare primitive plus a transform, not a composite. + check("a rotated, translated tube emits a bare TGeoTube, not a TGeoCompositeShape", + shape.ClassName() == "TGeoTube" and placement is not None, + f"{shape.ClassName()}, placement {'present' if placement else 'absent'}") + # closed form for the placed tube: 1 <= r <= 2, |z| <= 5 in the tube's frame. + frame = moved_record["candidate"]["leaves"][0]["frame"] + bad = 0 + random.seed(11) + for _ in range(20000): + p = (random.uniform(-2, 8), random.uniform(-9, 1), random.uniform(0, 10)) + rel = prim._sub(p, tuple(frame["origin"])) + zc = prim._dot(rel, tuple(frame["z"])) + rc = math.sqrt(max(prim._dot(rel, rel) - zc * zc, 0.0)) + want = (1.0 <= rc <= 2.0) and abs(zc) <= 5.0 + got = bool(shape.Contains(array("d", list(prim.placement_to_local(placement, p))))) + if want != got and min(abs(rc - 1.0), abs(rc - 2.0), abs(abs(zc) - 5.0)) > 1e-9: + bad += 1 + check("the emitted placed tube answers Contains like the closed form", + bad == 0, f"{bad} disagreement(s) over 20000 points") + # An analytic Capacity(): pi (rmax^2 - rmin^2) 2 dz, invariant under the placement. + want_capacity = math.pi * (2.0 ** 2 - 1.0 ** 2) * 10.0 + rel_capacity = abs(shape.Capacity() - want_capacity) / want_capacity + check("the placed tube's Capacity() is analytic", rel_capacity < 1.0e-14, + f"{shape.Capacity():.12f} vs {want_capacity:.12f}, rel {rel_capacity:.2e}") + # negative control on that check itself + wrong, wrong_pl = prim.build_root(prim.candidate("primitive", [prim.leaf( + "TGeoTube", {"rmin": 1.0, "rmax": 2.05, "dz": 5.0}, frame)], "probe"), "probe_wrong") + bad_wrong = 0 + random.seed(11) + for _ in range(20000): + p = (random.uniform(-2, 8), random.uniform(-9, 1), random.uniform(0, 10)) + rel = prim._sub(p, tuple(frame["origin"])) + zc = prim._dot(rel, tuple(frame["z"])) + rc = math.sqrt(max(prim._dot(rel, rel) - zc * zc, 0.0)) + want = (1.0 <= rc <= 2.0) and abs(zc) <= 5.0 + if want != bool(wrong.Contains(array("d", list(prim.placement_to_local(wrong_pl, p))))): + bad_wrong += 1 + check("the same check does report a wrong radius", bad_wrong > 0, + f"{bad_wrong} disagreement(s) with rmax 2.05") + # ... and transposing the placement rotation has to move the count. + transposed = [[placement[r][c] for r in range(3)] + [placement[c][3]] for c in range(3)] + bad_transposed = 0 + random.seed(11) + for _ in range(20000): + p = (random.uniform(-2, 8), random.uniform(-9, 1), random.uniform(0, 10)) + rel = prim._sub(p, tuple(frame["origin"])) + zc = prim._dot(rel, tuple(frame["z"])) + rc = math.sqrt(max(prim._dot(rel, rel) - zc * zc, 0.0)) + want = (1.0 <= rc <= 2.0) and abs(zc) <= 5.0 + got = bool(shape.Contains(array("d", list(prim.placement_to_local(transposed, p))))) + if want != got: + bad_transposed += 1 + check("a transposed placement rotation does move the count", bad_transposed > 0, + f"{bad_transposed} disagreement(s) with R^T") + # the round trip through the artefact: placement written, placement read back + placed_target = Path("/tmp/csg_selftest_placed.root") + write_shape_root(moved_record["candidate"], placed_target) + fp = ROOT.TFile.Open(str(placed_target)) + back_shape = fp.Get("shape") + back_matrix = fp.Get("placement") + back_placement = prim.placement_from_root_matrix(back_matrix) if back_matrix else None + worst_pl = (max(abs(back_placement[r][c] - placement[r][c]) + for r in range(3) for c in range(4)) + if back_placement is not None else float("inf")) + check("shape_.root round-trips the placement under the key \"placement\"", + back_shape is not None and back_shape.ClassName() == "TGeoTube" + and worst_pl < 1.0e-15, + f"read {back_shape.ClassName() if back_shape else 'nothing'}, worst placement " + f"element deviation {worst_pl:.3g}") + fp.Close() + # the two-leaf union must round-trip through a file and keep its class + target = Path("/tmp/csg_selftest_shape.root") + written = write_shape_root(ram_record["candidate"], target) + f = ROOT.TFile.Open(str(target)) + back = f.Get("shape") + check("a two-leaf union round-trips through shape_.root", + back and back.InheritsFrom("TGeoShape"), + f"wrote {written.ClassName()}, read {back.ClassName() if back else 'nothing'}") + f.Close() + dev = crosscheck_bbox(ram_record["candidate"]) + check("the OCCT and ROOT realisations agree on the bounding box", dev < 1.0e-9, + f"max deviation {dev:.3g} cm") + + # An axis-aligned box must come out as a bare TGeoBBox carrying its own origin. + box_record = process_solid(BRepPrimAPI_MakeBox(gp_Pnt(0, 0, 0), 2.0, 3.0, 4.0).Shape(), + "box-emission") + box_shape, box_placement = prim.build_root(box_record["candidate"], "boxprobe") + origin = [box_shape.GetOrigin()[i] for i in range(3)] + check("an axis-aligned box emits a bare TGeoBBox with its own origin", + box_shape.ClassName() == "TGeoBBox" and box_placement is None + and max(abs(origin[0] - 1.0), abs(origin[1] - 1.5), abs(origin[2] - 2.0)) < 1e-12 + and abs(box_shape.Capacity() - 24.0) < 1e-12, + f"{box_shape.ClassName()}, origin {origin}, capacity {box_shape.Capacity():.6f}, " + f"placement {'present' if box_placement else 'absent'}") + + # A genuine multi-leaf boolean stays an unplaced composite. + ram_shape, ram_placement = prim.build_root(ram_record["candidate"], "ramprobe") + check("a genuine two-leaf union is still an unplaced TGeoCompositeShape", + ram_shape.ClassName() == "TGeoCompositeShape" and ram_placement is None, + f"{ram_shape.ClassName()}, placement " + f"{'present' if ram_placement else 'absent'}") + + # --- the ROOT half of the revolved matcher --- + stepped_record = process_solid(stepped, "pcon-emission") + pcon_shape, pcon_placement = prim.build_root(stepped_record["candidate"], "pconprobe") + # 100 pi: pi (3^2 - 1^2) 5 below z = 0 and pi (4^2 - 2^2) 5 above it. + want_capacity = math.pi * ((3.0 ** 2 - 1.0 ** 2) * 5.0 + (4.0 ** 2 - 2.0 ** 2) * 5.0) + rel_capacity = abs(pcon_shape.Capacity() - want_capacity) / want_capacity + check("an axis-aligned polycone emits a bare TGeoPcon with an analytic Capacity()", + pcon_shape.ClassName() == "TGeoPcon" and pcon_placement is None + and rel_capacity < 1.0e-14, + f"{pcon_shape.ClassName()}, capacity {pcon_shape.Capacity():.9f} vs " + f"{want_capacity:.9f} (rel {rel_capacity:.2e}), placement " + f"{'present' if pcon_placement else 'absent'}") + + placed_pcon_shape, placed_pcon_placement = prim.build_root( + moved_pcon_record["candidate"], "placedpconprobe") + check("a placed polycone is a bare TGeoPcon plus a placement", + placed_pcon_shape.ClassName() == "TGeoPcon" and placed_pcon_placement is not None, + f"{placed_pcon_shape.ClassName()}, placement " + f"{'present' if placed_pcon_placement else 'absent'}") + # The closed form uses the inverse of the transform that built the OCCT solid. + pcon_inverse = pcon_place.Inverted() + bad_pcon = 0 + scored_pcon = 0 + random.seed(23) + for _ in range(20000): + p3 = (random.uniform(-3, 9), random.uniform(-10, 2), random.uniform(-1, 11)) + probe = gp_Pnt(*p3) + probe.Transform(pcon_inverse) + zc, rc = probe.Z(), math.hypot(probe.X(), probe.Y()) + if min(abs(zc + 5.0), abs(zc), abs(zc - 5.0), abs(rc - 1.0), abs(rc - 2.0), + abs(rc - 3.0), abs(rc - 4.0)) < 1.0e-6: + continue + scored_pcon += 1 + want = (1.0 <= rc <= 3.0) if -5.0 <= zc <= 0.0 else ( + (2.0 <= rc <= 4.0) if 0.0 < zc <= 5.0 else False) + got = bool(placed_pcon_shape.Contains( + array("d", list(prim.placement_to_local(placed_pcon_placement, p3))))) + if want != got: + bad_pcon += 1 + check("the emitted placed polycone answers Contains like the closed form", + bad_pcon == 0, f"{bad_pcon} disagreement(s) over {scored_pcon} points") + cc = crosscheck_contains(moved_pcon_record["candidate"], moved_pcon) + check("the ROOT polycone and the CAD solid agree on Contains", + cc["disagreements"] == 0, + f"{cc['disagreements']} disagreement(s) over {cc['points']} points") + + pcon_target = Path("/tmp/csg_selftest_pcon.root") + write_shape_root(stepped_record["candidate"], pcon_target) + fpcon = ROOT.TFile.Open(str(pcon_target)) + back_pcon = fpcon.Get("shape") + sections_ok = (back_pcon is not None and back_pcon.ClassName() == "TGeoPcon" + and back_pcon.GetNz() == 4 + and max(abs(back_pcon.GetZ(i) - step_z[i]) for i in range(4)) < 1e-15 + and max(abs(back_pcon.GetRmin(i) - step_rmin[i]) for i in range(4)) < 1e-15 + and max(abs(back_pcon.GetRmax(i) - step_rmax[i]) for i in range(4)) < 1e-15) + check("shape_.root round-trips a TGeoPcon with all its sections", sections_ok, + f"read {back_pcon.ClassName() if back_pcon else 'nothing'}, nz " + f"{back_pcon.GetNz() if back_pcon else 0}") + fpcon.Close() + + # --- the ROOT half of the prism family --- + # Each class must come out as itself, with an analytic Capacity(). + for name, solid, want_class, want_capacity in ( + ("Trd1", prism(trd_rings(3, 1, 2, 2, 5)), "TGeoTrd1", + 4.0 * 2.0 * (3.0 + 1.0) * 5.0), + ("Trd2", prism(trd_rings(3, 1, 2, 4, 5)), "TGeoTrd2", None), + ("Arb8", para, "TGeoArb8", None), + ("Xtru", prism([polygon_ring(ell_poly, -2), polygon_ring(ell_poly, 2)]), + "TGeoXtru", 5.0 * 4.0), + ("Pgon", prism([regular_ring(3, 6, -5), regular_ring(3, 6, 5)]), "TGeoPgon", + 6.0 * 9.0 * math.tan(math.pi / 6.0) * 10.0)): + record = process_solid(solid, f"{name}-emission") + if not record["accepted"]: + check(f"an axis-aligned {want_class} emits a bare {want_class}", False, + f"not accepted: {record['reason']}") + continue + shape, placed = prim.build_root(record["candidate"], f"{name}probe") + ok = shape.ClassName() == want_class and placed is None + detail = (f"{shape.ClassName()}, capacity {shape.Capacity():.9f}, placement " + f"{'present' if placed else 'absent'}") + if want_capacity is not None: + rel = abs(shape.Capacity() - want_capacity) / want_capacity + ok = ok and rel < 1.0e-12 + detail += f", closed form {want_capacity:.9f} (rel {rel:.2e})" + check(f"an axis-aligned {want_class} emits a bare {want_class} with an analytic " + "Capacity()", ok, detail) + + # A placed Trd1, checked through the inverse of the transform that built the OCCT solid. + trd_shape, trd_placement = prim.build_root(moved_trd_record["candidate"], "movedtrdprobe") + check("a placed Trd1 is a bare TGeoTrd1 plus a placement", + trd_shape.ClassName() == "TGeoTrd1" and trd_placement is not None, + f"{trd_shape.ClassName()}, placement " + f"{'present' if trd_placement else 'absent'}") + trd_inverse = prism_place.Inverted() + bad_trd = 0 + scored_trd = 0 + random.seed(37) + for _ in range(20000): + p3 = (random.uniform(-3, 9), random.uniform(-10, 2), random.uniform(-2, 12)) + probe = gp_Pnt(*p3) + probe.Transform(trd_inverse) + xc, yc, zc = probe.X(), probe.Y(), probe.Z() + half = 2.0 - 0.2 * zc # dx1 = 3, dx2 = 1, dz = 5 + if min(abs(abs(zc) - 5.0), abs(abs(yc) - 2.0), abs(abs(xc) - half)) < 1.0e-6: + continue + scored_trd += 1 + want = abs(zc) <= 5.0 and abs(yc) <= 2.0 and abs(xc) <= half + got = bool(trd_shape.Contains( + array("d", list(prim.placement_to_local(trd_placement, p3))))) + if want != got: + bad_trd += 1 + check("the emitted placed Trd1 answers Contains like the closed form", + bad_trd == 0, f"{bad_trd} disagreement(s) over {scored_trd} points") + cc_prism = crosscheck_contains(moved_trd_record["candidate"], moved_trd) + check("the ROOT Trd1 and the CAD solid agree on Contains", + cc_prism["disagreements"] == 0, + f"{cc_prism['disagreements']} disagreement(s) over {cc_prism['points']} points") + + # The artefact must carry a TGeoXtru's polygon and its sections. + xtru_record = process_solid(scaled, "xtru-emission") + xtru_target = Path("/tmp/csg_selftest_xtru.root") + write_shape_root(xtru_record["candidate"], xtru_target) + fxtru = ROOT.TFile.Open(str(xtru_target)) + back_xtru = fxtru.Get("shape") + xtru_ok = (back_xtru is not None and back_xtru.ClassName() == "TGeoXtru" + and back_xtru.GetNvert() == 5 and back_xtru.GetNz() == 3 + and max(abs(back_xtru.GetZ(k) - z) for k, z in enumerate((-3.0, 0.0, 3.0))) + < 1e-12 + and max(abs(back_xtru.GetScale(k) - v) + for k, v in enumerate((1.0, 1.4, 0.6))) < 1e-12) + check("shape_.root round-trips a TGeoXtru with its polygon and its sections", + xtru_ok, f"read {back_xtru.ClassName() if back_xtru else 'nothing'}, " + f"nvert {back_xtru.GetNvert() if back_xtru else 0}, " + f"nz {back_xtru.GetNz() if back_xtru else 0}") + fxtru.Close() + + # --- the ROOT half of the single cell --- + window_shape, window_placement = prim.build_root(window_record["candidate"], + "cellwindowprobe") + node = window_shape.GetBoolNode() + check("a single cell emits an unplaced TGeoCompositeShape over a TGeoSubtraction node", + window_shape.ClassName() == "TGeoCompositeShape" and window_placement is None + and node.ClassName() == "TGeoSubtraction" + and node.GetLeftShape().ClassName() == "TGeoTube" + and node.GetRightShape().ClassName() == "TGeoTube", + f"{window_shape.ClassName()} over {node.ClassName()}" + f"({node.GetLeftShape().ClassName()}, {node.GetRightShape().ClassName()}), " + f"placement {'present' if window_placement else 'absent'}") + steinmetz_shape, _pl = prim.build_root(steinmetz_record["candidate"], "cellsteinprobe") + check("an intersection cell emits a TGeoIntersection node", + steinmetz_shape.GetBoolNode().ClassName() == "TGeoIntersection", + steinmetz_shape.GetBoolNode().ClassName()) + for label, record, solid_of in (("the window", window_record, window), + ("the Steinmetz solid", steinmetz_record, steinmetz), + ("the drilled cube", drilled_record, drilled)): + cc = crosscheck_contains(record["candidate"], solid_of, n_points=20000) + check(f"the ROOT cell and the CAD solid agree on Contains for {label}", + cc["disagreements"] == 0, + f"{cc['disagreements']} disagreement(s) over {cc['points']} points") + dev = crosscheck_bbox(record["candidate"]) + check(f"the OCCT and ROOT realisations agree on the bounding box for {label}", + dev < 1.0e-9, f"max deviation {dev:.3g} cm") + # 16/3 r^3 is the Steinmetz volume; the composite's Capacity() is a Monte-Carlo estimate. + want_steinmetz = 16.0 / 3.0 + rel_steinmetz = abs(steinmetz_shape.Capacity() - want_steinmetz) / want_steinmetz + check("the emitted Steinmetz composite has the closed-form volume, to sampling noise", + rel_steinmetz < 0.02, + f"{steinmetz_shape.Capacity():.6f} vs {want_steinmetz:.6f} " + f"(rel {rel_steinmetz:.2e}, Monte-Carlo)") + cell_target = Path("/tmp/csg_selftest_cell.root") + write_shape_root(window_record["candidate"], cell_target) + fcell = ROOT.TFile.Open(str(cell_target)) + back_cell = fcell.Get("shape") + check("shape_.root round-trips a single cell as a TGeoCompositeShape", + back_cell is not None and back_cell.ClassName() == "TGeoCompositeShape" + and back_cell.GetBoolNode().ClassName() == "TGeoSubtraction", + f"read {back_cell.ClassName() if back_cell else 'nothing'}") + fcell.Close() + + # --- the ROOT half of the torus and the elliptic cylinder --- + # The bounding box is checked against the closed form, since OCCT's torus box is loose. + for label, record, solid_of, want_class, want_capacity, want_half in ( + ("the solid torus", solid_torus_record, solid_torus, "TGeoTorus", + 2.0 * math.pi ** 2 * 4.0 * 1.0 ** 2, (5.0, 5.0, 1.0)), + ("the torus shell", ply_record, ply, "TGeoTorus", + 2.0 * math.pi ** 2 * 5.0 * (0.30 ** 2 - 0.28 ** 2), (5.3, 5.3, 0.3)), + ("the elliptic cylinder", eltu_record, eltu_solid, "TGeoEltu", + math.pi * 3.0 * 1.5 * 10.0, (3.0, 1.5, 5.0))): + shape, placement = prim.build_root(record["candidate"], f"probe_{want_class}") + rel = abs(shape.Capacity() - want_capacity) / want_capacity + check(f"{label} emits a bare {want_class} with the closed-form Capacity()", + shape.ClassName() == want_class and placement is None and rel < 1.0e-12, + f"{shape.ClassName()}, capacity {shape.Capacity():.9f} vs " + f"{want_capacity:.9f} (rel {rel:.2e}), placement " + f"{'present' if placement else 'absent'}") + cc = crosscheck_contains(record["candidate"], solid_of, n_points=20000) + check(f"the ROOT {want_class} and the CAD solid agree on Contains for {label}", + cc["disagreements"] == 0, + f"{cc['disagreements']} disagreement(s) over {cc['points']} points") + half = (shape.GetDX(), shape.GetDY(), shape.GetDZ()) + worst = max(abs(h - w) for h, w in zip(half, want_half)) + check(f"the emitted {want_class}'s bounding box is the closed form for {label}", + worst < 1.0e-12, + f"{tuple(round(h, 9) for h in half)} vs {want_half}, worst {worst:.3g} cm") + # A torus phi wedge, where a mirrored frame convention would go unnoticed by volume. + wedge_shape, wedge_placement = prim.build_root(wedge_torus_record["candidate"], + "probe_toruswedge") + cc = crosscheck_contains(wedge_torus_record["candidate"], wedge_torus, n_points=20000) + check("the ROOT TGeoTorus and the CAD solid agree on Contains for the hollow wedge", + wedge_shape.ClassName() == "TGeoTorus" and cc["disagreements"] == 0, + f"{wedge_shape.ClassName()}, {cc['disagreements']} disagreement(s) over " + f"{cc['points']} points") + placed_torus_shape, placed_torus_placement = prim.build_root( + placed_torus_record["candidate"], "probe_placedtorus") + cc = crosscheck_contains(placed_torus_record["candidate"], placed_torus, n_points=20000) + check("a placed torus is a bare TGeoTorus plus a placement that composes correctly", + placed_torus_shape.ClassName() == "TGeoTorus" + and placed_torus_placement is not None and cc["disagreements"] == 0, + f"{placed_torus_shape.ClassName()}, {cc['disagreements']} disagreement(s) over " + f"{cc['points']} points") + placed_eltu_shape, placed_eltu_placement = prim.build_root( + placed_eltu_record["candidate"], "probe_placedeltu") + cc = crosscheck_contains(placed_eltu_record["candidate"], placed_eltu, n_points=20000) + check("a placed elliptic cylinder is a bare TGeoEltu plus a placement", + placed_eltu_shape.ClassName() == "TGeoEltu" + and placed_eltu_placement is not None and cc["disagreements"] == 0, + f"{placed_eltu_shape.ClassName()}, {cc['disagreements']} disagreement(s) over " + f"{cc['points']} points") + for label, record in (("a TGeoTorus", solid_torus_record), + ("a TGeoEltu", eltu_record)): + target = Path(f"/tmp/csg_selftest_{record['candidate']['leaves'][0]['type']}.root") + write_shape_root(record["candidate"], target) + handle = ROOT.TFile.Open(str(target)) + back = handle.Get("shape") + check(f"shape_.root round-trips {label}", + back is not None + and back.ClassName() == record["candidate"]["leaves"][0]["type"], + f"read {back.ClassName() if back else 'nothing'}") + handle.Close() + + # --- a sidecar written in Python, loaded in C++, answers Contains as `flat_contains` --- + ROOT.gInterpreter.AddIncludePath(f"{ROOT.gSystem.Getenv('O2_ROOT')}/include") + ROOT.gSystem.Load("libO2CADSupport") + ROOT.gInterpreter.Declare( + '#include "CADSupport/O2FlatCSG.h"\n' + 'namespace o2 { namespace cad {\n' + 'bool LoadFlatCSG(const std::string& file, O2FlatCSG& solid);\n' + '} }') + flat_rt_bad = flat_rt_scored = 0 + flat_rt_failed = [] + for flat_index, result in enumerate(flat_results): + blocks = result["blocks"] + xlo, ylo, zlo, xhi, yhi, zhi = recognise._bbox_of(result["solid"]) + # the part bbox is an outer bound of this cell, because the cell IS the part here + cells = [{"first": 0, "count": len(blocks), "volume": 1.0, + "lo": [xlo, ylo, zlo], "hi": [xhi, yhi, zhi]}] + sidecar = Path(f"/tmp/csg_selftest_flatrt_{flat_index}.bin") + flatmod.write_sidecar(sidecar, blocks, cells) + loaded = ROOT.o2.cad.O2FlatCSG(f"probe_flat_{flat_index}") + if not ROOT.o2.cad.LoadFlatCSG(str(sidecar), loaded): + flat_rt_failed.append(f"{result['name']}: LoadFlatCSG refused the sidecar") + continue + loaded.CloseShape() + if loaded.GetNhalfspaces() != len(blocks) or loaded.GetNcells() != 1: + flat_rt_failed.append(f"{result['name']}: loaded " + f"{loaded.GetNhalfspaces()}/{loaded.GetNcells()}") + continue + for point, _occ in result["points"]: + flat_rt_scored += 1 + if bool(loaded.Contains(array("d", list(point)))) != \ + flatmod.flat_contains(blocks, point): + flat_rt_bad += 1 + check("a sidecar written in Python and loaded in C++ answers Contains identically", + not flat_rt_failed and flat_rt_bad == 0 and flat_rt_scored > 0, + f"{flat_rt_bad} of {flat_rt_scored} points disagree over {len(flat_results)} " + f"fixtures" + ("; " + "; ".join(flat_rt_failed) if flat_rt_failed else "")) + + # --- R5: the shipped shape of a multi-cell flat candidate, through its own sidecar --- + flat_shape, flat_placement = prim.build_root(cone_record, "probe_flat_cells") + check("a multi-cell flat candidate builds an O2FlatCSG through its own sidecar", + flat_shape.ClassName() == "o2::cad::O2FlatCSG" and flat_shape.IsClosed() + and flat_shape.GetNcells() == len(cone_record["cells"]) + and flat_shape.GetNhalfspaces() == cone_record["notes"]["nHalfspaces"] + and flat_placement is None, + f"{flat_shape.GetNcells()} cell(s), {flat_shape.GetNhalfspaces()} halfspace(s), " + f"{flat_shape.GetNboxes()} sub-cell box(es)") + # the accelerated queries against the twin that defines them, and both against the + # Python side that wrote the file: three implementations, one answer + flat_rng = flat_random.Random(5150) + blo = [min(c["lo"][i] for c in cone_record["cells"]) for i in range(3)] + bhi = [max(c["hi"][i] for c in cone_record["cells"]) for i in range(3)] + twin_bad = python_bad = 0 + for _ in range(20000): + point = [blo[i] + flat_rng.random() * (bhi[i] - blo[i]) for i in range(3)] + probe = array("d", point) + accelerated = bool(flat_shape.Contains(probe)) + if accelerated != bool(flat_shape.Contains_Loop(probe)): + twin_bad += 1 + if accelerated != any(flatmod.flat_contains(c["blocks"], tuple(point)) + for c in cone_record["cells"]): + python_bad += 1 + check("the shipped flat shape agrees with its own _Loop twin and with cadsupport/flat.py", + twin_bad == 0 and python_bad == 0, + f"{twin_bad} twin and {python_bad} emitter disagreement(s) over 20000 points") + # the same twin comparison the converter now runs on every emitted part, through the + # function that runs it, and the field it reports it in + cone_cross = crosscheck_contains(cone_record, l_cone_solid) + plain_cross = crosscheck_contains(disjoint_record["candidate"], + disjoint_record.get("solid", disjoint)) + check("crosscheck_contains measures the twin on a flat part and nothing on a tree part", + cone_cross["twinDisagreements"] == 0 and cone_cross["disagreements"] == 0 + and cone_cross["points"] > 0 and plain_cross["twinDisagreements"] is None, + f"flat {cone_cross['twinDisagreements']}/{cone_cross['points']}, tree twin " + f"{plain_cross['twinDisagreements']}") + + check("the flat shape's Capacity is the sum of its cells' own volumes", + abs(flat_shape.Capacity() + - sum(c["volume"] for c in cone_record["cells"])) <= 1.0e-9, + f"{flat_shape.Capacity():.9g} vs " + f"{sum(c['volume'] for c in cone_record['cells']):.9g} cm^3") + + + n_ok = sum(1 for _n, ok, _d in checks if ok) + if verbose: + print(f" {n_ok}/{len(checks)} recognise/emit self-checks passed") + return n_ok, len(checks) + diff --git a/Detectors/CADSupport/tools/cadsupport/tier0.py b/Detectors/CADSupport/tools/cadsupport/tier0.py new file mode 100644 index 0000000000000..2d7440f894a87 --- /dev/null +++ b/Detectors/CADSupport/tools/cadsupport/tier0.py @@ -0,0 +1,354 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Tier 0: the plane, cylinder, cone, sphere or torus a stored B-spline face already is. + +Proposals come from `analytic._analytic_surface_proposals` plus a torus solve here. A proposal is +admissible when its gap <= REL_TOL * max(diag, 1 cm), measured on samples independent of the ones +it was fitted to, and the fewest-parameter admissible proposal wins. +""" + +import math + +# The same band as `recognise.REL_TOL`; the emitter self-test asserts the two agree. +REL_TOL = 1.0e-6 + +# The proposal grid (the converter's own) and the independent, denser acceptance grid. +_PROPOSE_N = 9 +_ACCEPT_N = 17 + + +class _Unavailable(Exception): + """The converter module could not be imported, so nothing here can run.""" + + +_CONVERTER = None + + +def _converter(): + """`cadsupport.analytic`, imported lazily and kept.""" + global _CONVERTER + if _CONVERTER is None: + try: + from cadsupport import analytic + except Exception as exc: # noqa: BLE001 + raise _Unavailable(str(exc)) from None + _CONVERTER = analytic + return _CONVERTER + + +# ------------------------------------------------------------------------------------------ +# the instrument +# ------------------------------------------------------------------------------------------ + +def surface_gap(kind, model, points): + """The largest distance, in cm, from any of `points` to the candidate surface. + + For plane / sphere / cylinder / cone it is `analytic._analytic_surface_gap`. + """ + if kind == "torus": + return _torus_gap(points, model) + return _converter()._analytic_surface_gap(kind, model, points) + + +def _torus_residual(points, centre, axis, major, minor): + import numpy as np + h = (points - centre) @ axis + rho = np.linalg.norm(points - centre - np.outer(h, axis), axis=1) + return np.sqrt((rho - major) ** 2 + h ** 2) - minor + + +def _torus_gap(points, model): + import numpy as np + return float(np.abs(_torus_residual(points, model["centre"], model["axis"], + model["major"], model["minor"])).max()) + + +# ------------------------------------------------------------------------------------------ +# the torus proposal (the one model the converter's recogniser does not carry) +# ------------------------------------------------------------------------------------------ + +def _torus_radii(points, centre, axis): + """`(R, r)` by least squares once the axis is fixed, or None if the solve is not a torus. + + `rho^2 + h^2 = 2R rho + (r^2 - R^2)` is linear in `2R` and `r^2 - R^2`. + """ + import numpy as np + h = (points - centre) @ axis + rho = np.linalg.norm(points - centre - np.outer(h, axis), axis=1) + design = np.column_stack([rho, np.ones_like(rho)]) + sol, *_ = np.linalg.lstsq(design, rho ** 2 + h ** 2, rcond=None) + major = 0.5 * float(sol[0]) + minor_sq = float(sol[1]) + major * major + if not (major > 0.0 and minor_sq > 0.0): + return None + return major, math.sqrt(minor_sq) + + +def _propose_torus(points, normals, refinements=25): + """`{axis, centre, major, minor}` for the torus these samples propose, or None. + + `(N_i x P_i) . d + N_i . g = 0` with `g = c x d` is linear in `(d, g)`: one SVD gives the axis, + then Gauss-Newton polishes the gap. A cylinder's degenerate `d = 0` answer is declined. + """ + import numpy as np + + design = np.column_stack([np.cross(normals, points), normals]) + _, _singular, right = np.linalg.svd(design, full_matrices=False) + solution = right[-1] + axis, moment = solution[:3], solution[3:] + length = float(np.linalg.norm(axis)) + if length < 1.0e-6: + return None # d = 0: coplanar normals, i.e. a cylinder, not a torus + axis = axis / length + centre = np.cross(axis, moment / length) + + radii = _torus_radii(points, centre, axis) + if radii is None: + return None + major, minor = radii + span = float(np.linalg.norm(points.max(axis=0) - points.min(axis=0))) or 1.0 + step = 1.0e-7 * span + for _ in range(refinements): + tangent_a = np.cross(axis, [1.0, 0.0, 0.0]) + if np.linalg.norm(tangent_a) < 1e-6: + tangent_a = np.cross(axis, [0.0, 1.0, 0.0]) + tangent_a = tangent_a / np.linalg.norm(tangent_a) + tangent_b = np.cross(axis, tangent_a) + base = _torus_residual(points, centre, axis, major, minor) + + def at(delta): + tilted = axis + delta[3] * tangent_a + delta[4] * tangent_b + tilted = tilted / np.linalg.norm(tilted) + return _torus_residual(points, centre + delta[:3], tilted, + major + delta[5], minor + delta[6]) + + jacobian = np.zeros((len(points), 7)) + for column in range(7): + probe = np.zeros(7) + probe[column] = step + jacobian[:, column] = (at(probe) - base) / step + try: + delta, *_ = np.linalg.lstsq(jacobian, -base, rcond=None) + except np.linalg.LinAlgError: + break + if np.abs(at(delta)).max() >= np.abs(base).max(): + break # no longer improving: keep what converged + centre = centre + delta[:3] + axis = axis + delta[3] * tangent_a + delta[4] * tangent_b + axis = axis / np.linalg.norm(axis) + major += float(delta[5]) + minor += float(delta[6]) + if not (major > 0.0 and minor > 0.0): + return None + return {"axis": axis, "centre": centre, "major": float(major), "minor": float(minor)} + + +# ------------------------------------------------------------------------------------------ +# the service +# ------------------------------------------------------------------------------------------ + +def canonicalise(face, adaptor, scale): + """`(carrier, gap)`: the canonical carrier this face IS, and the gap that decided. + + `carrier` is None when the face is not canonical, and `gap` is then the best proposal's gap; + both are None where the face cannot be sampled. `scale` is `max(part diagonal, 1 cm)`. The + record speaks `_face_records`' vocabulary plus `canonicalised`, `tier0GapCm` and + `tier0GapRelative`; `uv` is the trim box in the canonical chart, None for a plane or a sphere. + """ + try: + conv = _converter() + except _Unavailable: + return None, None + from OCC.Core.BRepTools import breptools + + try: + uv_bounds = breptools.UVBounds(face) + except Exception: # noqa: BLE001 + return None, None + propose_points, propose_normals = conv._sample_surface_for_recognition( + adaptor, *uv_bounds, n=_PROPOSE_N) + if propose_points is None: + return None, None + accept_points, _accept_normals = conv._sample_surface_for_recognition( + adaptor, *uv_bounds, n=_ACCEPT_N) + if accept_points is None: + return None, None + + # In order of parsimony: plane (3 parameters) < sphere (4) < cylinder (5) < cone (6) < torus (7). + proposals = list(conv._analytic_surface_proposals(propose_points, propose_normals)) + torus = _propose_torus(propose_points, propose_normals) + if torus is not None: + proposals.append(("torus", torus)) + + # The gap decides admissibility and the fewest-parameter admissible proposal wins, so a sphere + # is never taken for a zero-major-radius torus. + kind, model, gap, best_gap = None, None, None, float("inf") + for candidate_kind, candidate in proposals: + try: + candidate_gap = surface_gap(candidate_kind, candidate, accept_points) + except Exception: # noqa: BLE001 + continue + if not math.isfinite(candidate_gap): + continue + best_gap = min(best_gap, candidate_gap) + if kind is None and candidate_gap <= REL_TOL * scale: + kind, model, gap = candidate_kind, candidate, candidate_gap + + if kind is None: + return None, (None if not math.isfinite(best_gap) else best_gap) + record = _carrier_record(kind, model, adaptor, uv_bounds) + if record is None: + return None, gap + record["canonicalised"] = True + record["tier0GapCm"] = gap + record["tier0GapRelative"] = gap / scale + return record, gap + + +def carrier_side(face, adaptor, carrier): + """`interior` / `exterior` for a canonicalised face, by `census`'s one rule.""" + from cadsupport import census + return census.halfspace_side_of(face, adaptor, carrier) + + +def _carrier_record(kind, model, adaptor, uv_bounds): + """The canonical carrier as `recognise._face_records` states one.""" + import numpy as np + if kind == "plane": + normal = np.asarray(model["normal"], dtype=float) + normal = normal / np.linalg.norm(normal) + # Unflipped, i.e. the underlying surface's own normal: both callers apply the face's + # REVERSED flag themselves, exactly as they do for a native plane. + return {"kind": "plane", "n": tuple(float(c) for c in normal), + "p": tuple(float(c) for c in model["point"]), "uv": None} + if kind == "sphere": + return {"kind": "sphere", "p": tuple(float(c) for c in model["centre"]), + "r": float(model["radius"]), "uv": None} + if kind == "torus": + axis = _unit_array(model["axis"]) + # A fitted torus brings no reference direction of its own, so one is chosen here and the + # chart below is measured against that same one -- the two cannot disagree. + ref = np.asarray(_perpendicular_to(axis), dtype=float) + chart = _canonical_chart(adaptor, uv_bounds, np.asarray(model["centre"], dtype=float), + axis, ref, semi_angle=None, major=float(model["major"])) + if chart is None: + return None + return {"kind": "torus", "d": tuple(float(c) for c in axis), + "p": tuple(float(c) for c in model["centre"]), + "x": tuple(float(c) for c in ref), "r": float(model["major"]), + "rt": float(model["minor"]), "uv": chart} + if kind == "cylinder": + axis = _unit_array(model["axis"]) + origin = np.asarray(model["origin"], dtype=float) + ref = _orthonormalise(np.asarray(model["refu"], dtype=float), axis) + if ref is None: + return None + chart = _canonical_chart(adaptor, uv_bounds, origin, axis, ref, semi_angle=None) + if chart is None: + return None + return {"kind": "cylinder", "d": tuple(float(c) for c in axis), + "p": tuple(float(c) for c in origin), "x": tuple(float(c) for c in ref), + "r": float(model["radius"]), "uv": chart} + if kind == "cone": + axis = _unit_array(model["axis"]) + apex = np.asarray(model["apex"], dtype=float) + ref = _orthonormalise(np.asarray(model["refu"], dtype=float), axis) + if ref is None: + return None + half = float(model["half_angle"]) + if not (1.0e-9 < half < 0.5 * math.pi - 1.0e-9): + return None + chart = _canonical_chart(adaptor, uv_bounds, apex, axis, ref, semi_angle=half) + if chart is None: + return None + # Stated at the apex, in OCC's gp_Cone chart: r = RefRadius + v sin(a), t = v cos(a). + return {"kind": "cone", "d": tuple(float(c) for c in axis), + "p": tuple(float(c) for c in apex), "x": tuple(float(c) for c in ref), + "r": 0.0, "a": half, "uv": chart} + return None + + +def _unit_array(vec): + import numpy as np + v = np.asarray(vec, dtype=float) + return v / np.linalg.norm(v) + + +def _orthonormalise(vec, axis): + import numpy as np + ref = np.asarray(vec, dtype=float) + ref = ref - float(ref @ axis) * axis + length = float(np.linalg.norm(ref)) + if length < 1.0e-9: + return None + return ref / length + + +def _perpendicular_to(axis): + import numpy as np + seed = np.array([1.0, 0.0, 0.0]) if abs(float(axis[0])) < 0.9 else np.array([0.0, 1.0, 0.0]) + ref = _orthonormalise(seed, axis) + return tuple(float(c) for c in ref) + + +_CHART_N = 33 + + +def _canonical_chart(adaptor, uv_bounds, origin, axis, ref, semi_angle, major=None): + """`(umin, umax, vmin, vmax)`: the trim's bounding box in the carrier's OWN chart. + + Measured along the patch's two midlines, where the azimuth is monotone and can be unwrapped. + """ + import numpy as np + umin, umax, vmin, vmax = uv_bounds + umid, vmid = 0.5 * (umin + umax), 0.5 * (vmin + vmax) + binormal = np.cross(axis, ref) + + def chart_of(u, v): + try: + point = adaptor.Value(u, v) + except Exception: # noqa: BLE001 + return None + rel = np.array([point.X(), point.Y(), point.Z()]) - origin + axial = float(rel @ axis) + perp = rel - axial * axis + if float(np.linalg.norm(perp)) < 1.0e-30: + return None + azimuth = math.atan2(float(perp @ binormal), float(perp @ ref)) + if major is not None: # a torus: the meridian angle + return azimuth, math.atan2(axial, float(np.linalg.norm(perp)) - major) + return azimuth, axial if semi_angle is None else axial / math.cos(semi_angle) + + anchor = chart_of(umid, vmid) + if anchor is None: + return None + phis, axials = [anchor[0]], [anchor[1]] + for fixed, lo, hi, along_u in ((vmid, umin, umax, True), (umid, vmin, vmax, False)): + samples, centre_index = [], None + for k in range(_CHART_N): + t = lo + (hi - lo) * k / (_CHART_N - 1.0) + got = chart_of(t, fixed) if along_u else chart_of(fixed, t) + if got is None: + continue + if centre_index is None and t >= 0.5 * (lo + hi): + centre_index = len(samples) + samples.append(got) + if len(samples) < 2 or centre_index is None: + continue + unwrapped = np.unwrap(np.array([s[0] for s in samples])) + unwrapped += 2.0 * math.pi * round((anchor[0] - unwrapped[centre_index]) + / (2.0 * math.pi)) + phis.extend(float(p) for p in unwrapped) + axials.extend(s[1] for s in samples) + return (min(phis), max(phis), min(axials), max(axials)) diff --git a/Detectors/CADSupport/tools/compat/O2_CADtoTGeo.py b/Detectors/CADSupport/tools/compat/O2_CADtoTGeo.py new file mode 100755 index 0000000000000..4a69fa8c07d70 --- /dev/null +++ b/Detectors/CADSupport/tools/compat/O2_CADtoTGeo.py @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +exec python3 "${O2_ROOT}/share/CADSupport/tools/O2_CADtoTGeo.py" "$@" diff --git a/Detectors/CADSupport/tools/compat/O2_TGeoToCAD.py b/Detectors/CADSupport/tools/compat/O2_TGeoToCAD.py new file mode 100755 index 0000000000000..dd8e93c3c0496 --- /dev/null +++ b/Detectors/CADSupport/tools/compat/O2_TGeoToCAD.py @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +exec python3 "${O2_ROOT}/share/CADSupport/tools/O2_TGeoToCAD.py" "$@" diff --git a/scripts/geometry/g4_nist_database/G4_NIST_DB.json b/Detectors/CADSupport/tools/g4_nist_database/G4_NIST_DB.json similarity index 100% rename from scripts/geometry/g4_nist_database/G4_NIST_DB.json rename to Detectors/CADSupport/tools/g4_nist_database/G4_NIST_DB.json diff --git a/Detectors/CADSupport/tools/g4_nist_database/compile.sh b/Detectors/CADSupport/tools/g4_nist_database/compile.sh new file mode 100755 index 0000000000000..eb5b4228d1b13 --- /dev/null +++ b/Detectors/CADSupport/tools/g4_nist_database/compile.sh @@ -0,0 +1,24 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-03 + +echo "Compiling using geant4-config..." + +g++ -std=c++20 nist_export_all.cxx \ + $(geant4-config --cflags) \ + $(geant4-config --libs) \ + -O2 -o nist_export_all + +echo "" +echo "Build complete." +echo "Run with:" +echo " ./nist_export_all nist_db_all.json" \ No newline at end of file diff --git a/scripts/geometry/g4_nist_database/nist_export_all.cxx b/Detectors/CADSupport/tools/g4_nist_database/nist_export_all.cxx similarity index 85% rename from scripts/geometry/g4_nist_database/nist_export_all.cxx rename to Detectors/CADSupport/tools/g4_nist_database/nist_export_all.cxx index 709b3da261fbf..54ea0c6bb74ee 100644 --- a/scripts/geometry/g4_nist_database/nist_export_all.cxx +++ b/Detectors/CADSupport/tools/g4_nist_database/nist_export_all.cxx @@ -1,3 +1,16 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-03 + #include #include #include diff --git a/Detectors/CADSupport/tools/o2-cad-to-tgeo b/Detectors/CADSupport/tools/o2-cad-to-tgeo new file mode 100755 index 0000000000000..4a69fa8c07d70 --- /dev/null +++ b/Detectors/CADSupport/tools/o2-cad-to-tgeo @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +exec python3 "${O2_ROOT}/share/CADSupport/tools/O2_CADtoTGeo.py" "$@" diff --git a/Detectors/CADSupport/tools/o2-tgeo-to-cad b/Detectors/CADSupport/tools/o2-tgeo-to-cad new file mode 100755 index 0000000000000..dd8e93c3c0496 --- /dev/null +++ b/Detectors/CADSupport/tools/o2-tgeo-to-cad @@ -0,0 +1,14 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. + +exec python3 "${O2_ROOT}/share/CADSupport/tools/O2_TGeoToCAD.py" "$@" diff --git a/Detectors/CADSupport/validation/assemblyOracle.py b/Detectors/CADSupport/validation/assemblyOracle.py new file mode 100644 index 0000000000000..ad7155ef9009c --- /dev/null +++ b/Detectors/CADSupport/validation/assemblyOracle.py @@ -0,0 +1,705 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Ground truth for ASSEMBLY-level transport: the ordered crossing list per ray, annotated with +WHICH VOLUME the track is in between the crossings. + +Companion to `xrayOracle.py` (one leaf solid), for the failure mode it cannot see: a track that +exits volume A and is never reported entering B. Per interval it answers the SET of occupants: + +| assembly situation | what the occupancy sequence looks like | +| ----------------------- | ------------------------------------------------------- | +| touching parts | `{A} -> {B}` at ONE distance: a transition, no vacuum | +| a genuine gap | `{A} -> {} -> {B}`, with the vacuum run's length stated | +| a part nested in another| `{A} -> {A,B} -> {A}` | +| interpenetration | `{A} -> {A,B} -> {B}` -- occupancy is AMBIGUOUS, and the | +| | oracle says so rather than choosing an occupant | +| a ray starting inside | segment 0's occupancy is non-empty; it is reported | + +Candidate positions are merged ACROSS parts before the intervals are cut, so touching parts give +one transition `{A} -> {B}`; the merge tolerance is reported, and a vacuum run shorter than +`--thin-vacuum` is counted and flagged. An interval's occupancy comes from +`BRepClass3d_SolidClassifier` at its MIDPOINT, once per part; a midpoint OCCT calls `ON` flags the +ray `amb`. + +Units +----- +Ray origins, directions and crossing distances are in the MODEL'S NATIVE UNITS (mm for every STEP +file in this corpus), not cm; `scaleToCm` is carried beside them and a consumer must apply it. + +Usage +----- + assemblyOracle.py --self-test # the synthetic assembly, analytic answers + assemblyOracle.py --step .step --rays N --beams M --out crossings.json +""" + +import argparse +import json +import math +import sys +import time +from pathlib import Path + +import cadsupport_path # noqa: E402,F401 (puts ../tools on sys.path) + +from cadsupport.occ_env import ensure_occ + +ensure_occ() + +from OCC.Core.BRepBndLib import brepbndlib +from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.Bnd import Bnd_Box +from OCC.Core.IFSelect import IFSelect_RetDone +from OCC.Core.IntCurvesFace import IntCurvesFace_ShapeIntersector +from OCC.Core.STEPCAFControl import STEPCAFControl_Reader +from OCC.Core.TCollection import TCollection_AsciiString +from OCC.Core.TDF import TDF_Label, TDF_LabelSequence, TDF_Tool +from OCC.Core.TDocStd import TDocStd_Document +from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON, TopAbs_OUT, TopAbs_SOLID +from OCC.Core.TopExp import TopExp_Explorer +from OCC.Core.TopLoc import TopLoc_Location +from OCC.Core.TopoDS import topods +from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool +from OCC.Core.gp import gp_Dir, gp_Lin, gp_Pnt, gp_Trsf + +# A ray parameter this close to the origin is the origin itself; the kernel and oracles share it. +_RAY_EPS = 1.0e-9 + +ASSEMBLY_FORMAT_VERSION = 1 + + +# --------------------------------------------------------------------------------------------- +# Loading a STEP assembly as a flat list of PLACED solids +# --------------------------------------------------------------------------------------------- + +class Part: + """One placed solid in the world frame; `shape` carries its placement as a `TopLoc_Location`.""" + + __slots__ = ("name", "definition", "path", "shape", "bbox") + + def __init__(self, name, definition, path, shape): + self.name = name + self.definition = definition + self.path = path + self.shape = shape + box = Bnd_Box() + brepbndlib.Add(shape, box) + self.bbox = box.Get() if not box.IsVoid() else None + + def __repr__(self): + return f"Part({self.name})" + + +def _label_id(label) -> str: + s = TCollection_AsciiString() + TDF_Tool.Entry(label, s) + return s.ToCString() + + +def _label_name(label) -> str: + try: + n = label.GetLabelName() + return str(n) if n else "" + except Exception: + return "" + + +def detect_step_unit_scale_to_cm(step_path: Path) -> float: + """Same heuristic `O2_CADtoTGeo.py` uses, kept independent on purpose (this module must not + import the 200 kB converter to read a header).""" + data = step_path.open("rb").read(4 * 1024 * 1024).decode("latin-1", errors="ignore").upper() + if ".MILLI." in data: + return 0.1 + if ".CENTI." in data: + return 1.0 + if ".METRE." in data or ".METER." in data: + return 100.0 + if "INCH" in data: + return 2.54 + if "FOOT" in data or "FEET" in data: + return 30.48 + return 0.1 + + +def load_assembly(step_path: Path, explode_solids: bool = True): + """Every PLACED leaf solid of a STEP assembly, in the world frame: (parts, scale_to_cm). + + The parts are instances, not definitions: a prototype referenced 28 times yields 28 parts. + """ + doc = TDocStd_Document("assembly") + reader = STEPCAFControl_Reader() + reader.SetColorMode(True) + reader.SetNameMode(True) + reader.SetLayerMode(True) + if reader.ReadFile(str(step_path)) != IFSelect_RetDone: + raise RuntimeError(f"STEP read failed: {step_path}") + reader.Transfer(doc) + shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) + + parts = [] + used = {} + + def emit(label, trsf, path): + definition = _label_id(label) + name = _label_name(label) or definition.replace(":", "_") + shape = shape_tool.GetShape(label).Moved(TopLoc_Location(trsf)) + pieces = [] + if explode_solids: + explorer = TopExp_Explorer(shape, TopAbs_SOLID) + while explorer.More(): + pieces.append(topods.Solid(explorer.Current())) + explorer.Next() + if not pieces: + pieces = [shape] + for k, piece in enumerate(pieces): + base = name if len(pieces) == 1 else f"{name}.s{k}" + count = used.get(base, 0) + used[base] = count + 1 + unique = base if count == 0 else f"{base}#{count}" + parts.append(Part(unique, definition, path, piece)) + + def walk(label, trsf, path): + children = TDF_LabelSequence() + shape_tool.GetComponents(label, children) + if children.Length() > 0 or shape_tool.IsAssembly(label): + for i in range(children.Length()): + child = children.Value(i + 1) + if shape_tool.IsReference(child): + referred = TDF_Label() + shape_tool.GetReferredShape(child, referred) + walk(referred, trsf.Multiplied(shape_tool.GetLocation(child).Transformation()), + f"{path}_{i}") + else: + walk(child, trsf, f"{path}_{i}") + return + if shape_tool.IsSimpleShape(label): + emit(label, trsf, path) + + roots = TDF_LabelSequence() + shape_tool.GetFreeShapes(roots) + for i in range(roots.Length()): + root = roots.Value(i + 1) + if shape_tool.IsReference(root): + referred = TDF_Label() + shape_tool.GetReferredShape(root, referred) + walk(referred, shape_tool.GetLocation(root).Transformation(), f"r{i}") + else: + walk(root, gp_Trsf(), f"r{i}") + + # Pin the XCAF document for the life of the process: a collected one leaves its shapes dangling. + load_assembly._keepalive = (doc, shape_tool) + return parts, detect_step_unit_scale_to_cm(step_path) + + +def assembly_from_shapes(named_shapes): + """A synthetic assembly from (name, TopoDS_Shape) pairs -- the self-test's entry point.""" + return [Part(name, name, "synthetic", shape) for name, shape in named_shapes] + + +# --------------------------------------------------------------------------------------------- +# The oracle +# --------------------------------------------------------------------------------------------- + +def _ray_hits_box(bbox, origin, direction, tmax, pad): + """Slab test. Conservative: a false positive costs one intersector call, a false negative + costs a lost wall, so every comparison is inclusive and padded.""" + if bbox is None: + return False + lo = 0.0 + hi = tmax + for axis in range(3): + omin, omax = bbox[axis] - pad, bbox[axis + 3] + pad + d = direction[axis] + o = origin[axis] + if abs(d) < 1e-300: + if o < omin or o > omax: + return False + continue + t0 = (omin - o) / d + t1 = (omax - o) / d + if t0 > t1: + t0, t1 = t1, t0 + lo = max(lo, t0) + hi = min(hi, t1) + if lo > hi: + return False + return True + + +class AssemblyCrossingOracle: + """The ordered, occupancy-annotated crossing list for a compound of placed parts.""" + + def __init__(self, parts, merge_tolerance=1.0e-9, thin_vacuum=1.0e-6): + self.parts = list(parts) + self.merge_tolerance = max(merge_tolerance, _RAY_EPS) + self.thin_vacuum = thin_vacuum + self.intersectors = [] + self.classifiers = [] + for part in self.parts: + intersector = IntCurvesFace_ShapeIntersector() + intersector.Load(part.shape, _RAY_EPS) + self.intersectors.append(intersector) + self.classifiers.append(BRepClass3d_SolidClassifier(part.shape)) + + # -- one part, one ray --------------------------------------------------------------------- + + def _candidates(self, index, origin, direction, tmax): + intersector = self.intersectors[index] + line = gp_Lin(gp_Pnt(*origin), gp_Dir(*direction)) + intersector.Perform(line, _RAY_EPS, tmax) + if not intersector.IsDone(): + return None + out = [] + for k in range(1, intersector.NbPnt() + 1): + parameter = intersector.WParameter(k) + if _RAY_EPS < parameter <= tmax: + out.append(parameter) + return out + + def _state(self, index, point): + classifier = self.classifiers[index] + classifier.Perform(point, _RAY_EPS) + state = classifier.State() + if state == TopAbs_IN: + return 1 + if state == TopAbs_OUT: + return 0 + if state == TopAbs_ON: + return -1 + raise RuntimeError(f"unexpected classifier state {state}") + + # -- one ray ------------------------------------------------------------------------------- + + def crossings(self, origin, direction, tmax): + """Returns a dict describing the whole transport along this ray. + + Keys: + `s0` occupancy at the ray origin (list of part names; empty = vacuum) + `seg` [t0, t1, [occupants]] for every maximal run of constant occupancy + `x` the flat ordered crossing list: {t, part, s (+1 enter / -1 exit), + occ (occupancy AFTER), g (group: crossings sharing one distance)} + `amb` OCCT declined to classify somewhere on this ray + `ovl` some segment had two or more occupants -- occupancy is AMBIGUOUS and no + single volume id can be assigned + `ovlClean` the same, on a ray OCCT did NOT decline anywhere; quote this one, since an + inherited ON midpoint can fake a two-occupant segment on a grazing ray + `thin` number of vacuum runs shorter than `thin_vacuum` + `contact` number of distances at which one part is exited and another entered with no + vacuum in between (a touching transition) + """ + norm = math.sqrt(sum(c * c for c in direction)) + unit = [c / norm for c in direction] + + pad = 10.0 * self.merge_tolerance + active = [i for i, p in enumerate(self.parts) + if _ray_hits_box(p.bbox, origin, unit, tmax, pad)] + + ambiguous = False + raw = [] + for i in active: + hits = self._candidates(i, origin, unit, tmax) + if hits is None: + ambiguous = True + continue + raw.extend(hits) + raw.sort() + + edges = [0.0] + for t in raw: + if t - edges[-1] > self.merge_tolerance: + edges.append(t) + if tmax - edges[-1] > self.merge_tolerance: + edges.append(tmax) + else: + edges[-1] = tmax + + occupancy = [] + for k in range(len(edges) - 1): + mid = 0.5 * (edges[k] + edges[k + 1]) + point = gp_Pnt(*(origin[c] + mid * unit[c] for c in range(3))) + here = [] + for i in active: + state = self._state(i, point) + if state < 0: + ambiguous = True + # Inherit rather than guess: an ON midpoint is not evidence of either side. + if occupancy and self.parts[i].name in occupancy[-1]: + here.append(self.parts[i].name) + elif state == 1: + here.append(self.parts[i].name) + occupancy.append(sorted(here)) + + # Maximal runs of constant occupancy. + segments = [] + for k, occ in enumerate(occupancy): + if segments and segments[-1][2] == occ: + segments[-1][1] = edges[k + 1] + else: + segments.append([edges[k], edges[k + 1], occ]) + + crossings = [] + contact = 0 + for g in range(1, len(segments)): + before = set(segments[g - 1][2]) + after = set(segments[g][2]) + t = segments[g][0] + occ_after = segments[g][2] + exited = sorted(before - after) + entered = sorted(after - before) + for name in exited: + crossings.append({"t": t, "part": name, "s": -1, "occ": occ_after, "g": g - 1}) + for name in entered: + crossings.append({"t": t, "part": name, "s": +1, "occ": occ_after, "g": g - 1}) + if exited and entered: + contact += 1 + + thin = 0 + for t0, t1, occ in segments: + if not occ and t0 > 0.0 and t1 < tmax and (t1 - t0) < self.thin_vacuum: + thin += 1 + + overlap = any(len(occ) > 1 for _, _, occ in segments) + + return { + "o": list(origin), "d": list(unit), "tmax": tmax, + "s0": segments[0][2] if segments else [], + "seg": [[t0, t1, occ] for t0, t1, occ in segments], + "x": crossings, + "amb": bool(ambiguous), + "ovl": bool(overlap), + "ovlClean": bool(overlap and not ambiguous), + "thin": thin, + "contact": contact, + } + + +# --------------------------------------------------------------------------------------------- +# Ray generation: Fibonacci directions +# --------------------------------------------------------------------------------------------- + +def fibonacci_directions(n): + out = [] + golden = math.pi * (3.0 - math.sqrt(5.0)) + for i in range(n): + z = 1.0 - 2.0 * (i + 0.5) / n + r = math.sqrt(max(0.0, 1.0 - z * z)) + phi = golden * i + out.append((r * math.cos(phi), r * math.sin(phi), z)) + return out + + +def assembly_bbox(parts): + lo = [float("inf")] * 3 + hi = [float("-inf")] * 3 + for part in parts: + if part.bbox is None: + continue + for a in range(3): + lo[a] = min(lo[a], part.bbox[a]) + hi[a] = max(hi[a], part.bbox[a + 3]) + return lo, hi + + +def raster_rays(parts, beams, n, margin_fraction=0.02): + """`beams` Fibonacci directions x n x n impact parameters, every ray starting outside the + assembly's bounding sphere and ending outside it.""" + lo, hi = assembly_bbox(parts) + centre = [(lo[a] + hi[a]) / 2 for a in range(3)] + radius = 0.5 * math.sqrt(sum((hi[a] - lo[a]) ** 2 for a in range(3))) + radius *= (1.0 + margin_fraction) + rays = [] + for b, d in enumerate(fibonacci_directions(beams)): + # An orthonormal frame with `d` as its third axis. + helper = (0.0, 0.0, 1.0) if abs(d[2]) < 0.9 else (1.0, 0.0, 0.0) + u = (d[1] * helper[2] - d[2] * helper[1], + d[2] * helper[0] - d[0] * helper[2], + d[0] * helper[1] - d[1] * helper[0]) + un = math.sqrt(sum(c * c for c in u)) + u = tuple(c / un for c in u) + v = (d[1] * u[2] - d[2] * u[1], d[2] * u[0] - d[0] * u[2], d[0] * u[1] - d[1] * u[0]) + for i in range(n): + for j in range(n): + a = -radius + (i + 0.5) * 2 * radius / n + c = -radius + (j + 0.5) * 2 * radius / n + origin = [centre[k] + a * u[k] + c * v[k] - radius * d[k] for k in range(3)] + rays.append((origin, list(d), 2 * radius, b)) + return rays + + +# --------------------------------------------------------------------------------------------- +# Self-test: a synthetic assembly whose every answer is known on paper +# --------------------------------------------------------------------------------------------- + +def self_test() -> int: + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox + from OCC.Core.gp import gp_Pnt as P + + failures = [] + + def check(name, ok, detail=""): + print(f" [{'ok ' if ok else 'FAIL'}] {name}" + (f" {detail}" if not ok else "")) + if not ok: + failures.append(name) + + def box(x0, y0, z0, x1, y1, z1): + return BRepPrimAPI_MakeBox(P(x0, y0, z0), P(x1, y1, z1)).Shape() + + # --------------------------------------------------------------------------------------- + # The synthetic assembly. Everything is axis aligned so every answer is arithmetic. + # + # x: 0 2 2 4 5 7 7+1e-6 9 + # |--A--|--B-----| |---C--| |---D----| + # touching face 1 cm gap 1e-6 cm gap + # + # E = [12,18]^3 with F = [14,16]^3 nested wholly inside it + # G = [20,24]x[0,2]x[0,2] and H = [23,27]x[0,2]x[0,2] interpenetrate over [23,24] + # --------------------------------------------------------------------------------------- + parts = assembly_from_shapes([ + ("A", box(0, 0, 0, 2, 2, 2)), + ("B", box(2, 0, 0, 4, 2, 2)), + ("C", box(5, 0, 0, 7, 2, 2)), + ("D", box(7 + 1e-6, 0, 0, 9, 2, 2)), + ("E", box(12, 12, 12, 18, 18, 18)), + ("F", box(14, 14, 14, 16, 16, 16)), + ("G", box(20, 0, 0, 24, 2, 2)), + ("H", box(23, 0, 0, 27, 2, 2)), + ]) + oracle = AssemblyCrossingOracle(parts, merge_tolerance=1e-9, thin_vacuum=1e-5) + + def names(seq): + return [s["part"] for s in seq] + + def ts(seq): + return [s["t"] for s in seq] + + # --- case 1: TOUCHING. One transition at x=2, not two events with a gap in between. ------- + r = oracle.crossings([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 6.0) + x = r["x"] + check("touching: 4 crossings on the A|B chord", len(x) == 4, str([(c["t"], c["part"], c["s"]) for c in x])) + check("touching: enter A at 1, exit A and enter B at 3, exit B at 5", + len(x) == 4 and all(abs(a - b) < 1e-9 for a, b in zip(ts(x), [1.0, 3.0, 3.0, 5.0])), str(ts(x))) + check("touching: the shared face is ONE transition A->B, no vacuum between", + r["contact"] == 1 and any(c["s"] == -1 and c["part"] == "A" and c["occ"] == ["B"] for c in x), + f"contact={r['contact']} occ={[c['occ'] for c in x]}") + check("touching: no vacuum segment between A and B", + not any(len(o) == 0 and 1.0 < t0 < 5.0 for t0, t1, o in r["seg"]), str(r["seg"])) + check("touching: occupancy after each crossing is A, B, B, vacuum", + [c["occ"] for c in x] == [["A"], ["B"], ["B"], []], str([c["occ"] for c in x])) + + # --- case 2: a 1 cm GAP between B and C --------------------------------------------------- + r = oracle.crossings([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 12.0) + vac = [(t0, t1) for t0, t1, o in r["seg"] if not o and t0 > 0] + check("gap: a vacuum run of exactly 1 cm between B and C", + any(abs(t0 - 5.0) < 1e-9 and abs(t1 - 6.0) < 1e-9 for t0, t1 in vac), str(vac)) + check("gap: the occupancy after exiting B is vacuum", + any(c["part"] == "B" and c["s"] == -1 and c["occ"] == [] for c in r["x"]), + str([(c["part"], c["s"], c["occ"]) for c in r["x"]])) + + # --- case 3: a 1e-6 cm gap between C and D, resolved and flagged as thin ------------------ + check("thin gap: the 1e-6 cm vacuum between C and D is RESOLVED, not merged away", + any(abs(t1 - t0 - 1e-6) < 1e-9 for t0, t1 in vac), + str([(t0, t1, t1 - t0) for t0, t1 in vac])) + check("thin gap: it is counted as a thin vacuum run", r["thin"] == 1, str(r["thin"])) + check("thin gap: D is entered, not skipped", + any(c["part"] == "D" and c["s"] == +1 for c in r["x"]), str(names(r["x"]))) + + # ...and with a merge tolerance COARSER than the gap, C and D must report as TOUCHING. + coarse = AssemblyCrossingOracle(parts, merge_tolerance=1e-4, thin_vacuum=1e-5) + rc = coarse.crossings([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 12.0) + coarse_vac = [(t0, t1) for t0, t1, o in rc["seg"] if not o and t0 > 0] + check("thin-gap CONTROL: at merge tolerance 1e-4 the same 1e-6 gap is merged away, and C|D " + "becomes a touching transition", + not any(t1 - t0 < 1e-4 for t0, t1 in coarse_vac) and rc["thin"] == 0 + and rc["contact"] == 2, + f"vac={coarse_vac} thin={rc['thin']} contact={rc['contact']}") + + # --- case 4: NESTING. F wholly inside E. -------------------------------------------------- + r = oracle.crossings([10.0, 15.0, 15.0], [1.0, 0.0, 0.0], 12.0) + occ = [o for _, _, o in r["seg"]] + check("nesting: occupancy runs vacuum, E, E+F, E, vacuum", + occ == [[], ["E"], ["E", "F"], ["E"], []], str(occ)) + check("nesting: entering F does not exit E", + [(c["part"], c["s"]) for c in r["x"]] == + [("E", 1), ("F", 1), ("F", -1), ("E", -1)], str([(c["part"], c["s"]) for c in r["x"]])) + check("nesting: crossings at 2, 4, 6, 8", + all(abs(a - b) < 1e-9 for a, b in zip(ts(r["x"]), [2.0, 4.0, 6.0, 8.0])), str(ts(r["x"]))) + check("nesting: reported as multiply-occupied", r["ovl"] is True, str(r["ovl"])) + + # --- case 5: INTERPENETRATION. G and H share [23,24]. ------------------------------------- + r = oracle.crossings([19.0, 1.0, 1.0], [1.0, 0.0, 0.0], 10.0) + occ = [o for _, _, o in r["seg"]] + check("overlap: occupancy runs vacuum, G, G+H, H, vacuum", + occ == [[], ["G"], ["G", "H"], ["H"], []], str(occ)) + check("overlap: the oracle says AMBIGUOUS rather than choosing an occupant", + r["ovl"] is True and any(len(o) > 1 for o in occ), str(occ)) + check("overlap: the shared slab is [23,24] i.e. t in [4,5]", + any(len(o) > 1 and abs(t0 - 4.0) < 1e-9 and abs(t1 - 5.0) < 1e-9 for t0, t1, o in r["seg"]), + str(r["seg"])) + check("overlap: it survives the ambiguity filter -- `ovlClean` is the number to quote", + r["ovlClean"] is True and r["amb"] is False, f"ovlClean={r['ovlClean']} amb={r['amb']}") + + # --- case 6: a ray STARTING INSIDE a part ------------------------------------------------- + r = oracle.crossings([1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 6.0) + check("inside start: origin occupancy is A", r["s0"] == ["A"], str(r["s0"])) + check("inside start: first crossing is exit A / enter B at t=1", + len(r["x"]) >= 2 and abs(r["x"][0]["t"] - 1.0) < 1e-9 and r["x"][0]["s"] == -1, + str([(c["t"], c["part"], c["s"]) for c in r["x"]])) + + # --- case 7: a ray GRAZING the shared edge of A and B ------------------------------------- + # Along x=2 in +y the ray runs in the shared face; no interior crossing may be invented. + r = oracle.crossings([2.0, -1.0, 1.0], [0.0, 1.0, 0.0], 6.0) + check("grazing shared face: no interior crossing is invented", + all(c["s"] in (+1, -1) for c in r["x"]) and len(r["x"]) % 2 == 0, + str([(c["t"], c["part"], c["s"]) for c in r["x"]])) + print(f" grazing shared face x=2: seg={r['seg']} amb={r['amb']}") + # A ray exactly along the shared EDGE x=2, z=2 of A and B. + r = oracle.crossings([2.0, -1.0, 2.0], [0.0, 1.0, 0.0], 6.0) + check("grazing shared edge: the list still alternates per part", + _alternates_per_part(r["x"]), str([(c["t"], c["part"], c["s"]) for c in r["x"]])) + print(f" grazing shared edge x=2,z=2: seg={r['seg']} amb={r['amb']}") + # An inherited ON midpoint can fake an overlap on a grazing ray, so it must never be CLEAN. + check("grazing: an OCCT-ambiguous ray never reports a CLEAN overlap", + r["ovlClean"] is False, f"ovl={r['ovl']} ovlClean={r['ovlClean']} amb={r['amb']}") + + # --- case 8: the alternation invariant, on a Fibonacci fan over the whole assembly -------- + rays = raster_rays(parts, beams=32, n=6) + bad_alt = 0 + bad_occ = 0 + amb = 0 + with_overlap = 0 + for origin, d, tmax, _ in rays: + r = oracle.crossings(origin, d, tmax) + amb += bool(r["amb"]) + with_overlap += bool(r["ovl"]) + if not _alternates_per_part(r["x"]): + bad_alt += 1 + if not _occupancy_consistent(r): + bad_occ += 1 + check(f"fan ({len(rays)} rays): every part's crossings alternate enter/exit", + bad_alt == 0, f"{bad_alt} rays") + check(f"fan ({len(rays)} rays): occupancy after every crossing equals the segment occupancy", + bad_occ == 0, f"{bad_occ} rays") + print(f" fan: {len(rays)} rays, {amb} ambiguous, {with_overlap} with multiple occupancy") + + # --- case 9: the NEGATIVE control -- the overlap flag must be able to be false ------------ + clean = assembly_from_shapes([("A", box(0, 0, 0, 2, 2, 2)), ("B", box(2, 0, 0, 4, 2, 2))]) + clean_oracle = AssemblyCrossingOracle(clean) + r = clean_oracle.crossings([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 8.0) + check("negative control: a touching-only pair reports NO multiple occupancy", + r["ovl"] is False, str(r["seg"])) + # ...and that the same flag fires when the same two boxes are made to interpenetrate. + dirty = assembly_from_shapes([("A", box(0, 0, 0, 2, 2, 2)), ("B", box(1.9, 0, 0, 4, 2, 2))]) + dirty_oracle = AssemblyCrossingOracle(dirty) + r = dirty_oracle.crossings([-1.0, 1.0, 1.0], [1.0, 0.0, 0.0], 8.0) + check("positive control: nudging one box 0.1 cm into the other DOES fire the flag", + r["ovl"] is True, str(r["seg"])) + + print(f"\n{'SELF-TEST PASSED' if not failures else 'SELF-TEST FAILED'}: " + f"{len(failures)} failure(s) of {9}") + return 0 if not failures else 1 + + +def _alternates_per_part(crossings): + last = {} + for c in crossings: + previous = last.get(c["part"]) + if previous is not None and previous == c["s"]: + return False + last[c["part"]] = c["s"] + return True + + +def _occupancy_consistent(ray): + """Every crossing's `occ` must be the occupancy of the segment it opens.""" + occ_by_group = {g: seg[2] for g, seg in enumerate(ray["seg"])} + for c in ray["x"]: + if c["occ"] != occ_by_group.get(c["g"] + 1): + return False + return True + + +# --------------------------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--step", type=Path, help="the STEP assembly") + parser.add_argument("--out", type=Path, help="where to write the crossing lists (JSON)") + parser.add_argument("--beams", type=int, default=32, help="Fibonacci directions") + parser.add_argument("--raster", type=int, default=8, help="impact parameters per beam, N x N") + parser.add_argument("--parts", type=str, default="", + help="comma-separated instance names to keep (default: all)") + parser.add_argument("--max-parts", type=int, default=0, help="keep only the first N parts") + parser.add_argument("--thin-vacuum", type=float, default=1.0e-6, + help="a vacuum run shorter than this (cm) is counted as thin") + parser.add_argument("--self-test", action="store_true", + help="the synthetic assembly: touching, gap, thin gap, nesting, overlap, " + "inside start, grazing; needs no model") + args = parser.parse_args() + + if args.self_test: + return self_test() + if not args.step: + parser.error("--step is required (unless --self-test)") + + started = time.time() + parts, scale = load_assembly(args.step) + if args.parts: + wanted = set(args.parts.split(",")) + parts = [p for p in parts if p.name in wanted] + if args.max_parts: + parts = parts[:args.max_parts] + print(f" {args.step.name}: {len(parts)} placed solids, scale {scale} cm/unit " + f"({time.time() - started:.1f} s)", flush=True) + + oracle = AssemblyCrossingOracle(parts, thin_vacuum=args.thin_vacuum / scale) + rays = raster_rays(parts, args.beams, args.raster) + print(f" {len(rays)} rays ({args.beams} Fibonacci directions x {args.raster}^2)", flush=True) + + answers = [] + stats = {"rays": 0, "crossings": 0, "amb": 0, "ovl": 0, "ovlClean": 0, "thin": 0, + "contact": 0, "insideStart": 0, "empty": 0} + t0 = time.time() + for k, (origin, d, tmax, beam) in enumerate(rays): + r = oracle.crossings(origin, d, tmax) + r["beam"] = beam + answers.append(r) + stats["rays"] += 1 + stats["crossings"] += len(r["x"]) + stats["amb"] += bool(r["amb"]) + stats["ovl"] += bool(r["ovl"]) + stats["ovlClean"] += bool(r["ovlClean"]) + stats["thin"] += r["thin"] + stats["contact"] += r["contact"] + stats["insideStart"] += bool(r["s0"]) + stats["empty"] += (not r["x"]) + if (k + 1) % 200 == 0: + print(f" {k + 1}/{len(rays)} rays ({time.time() - t0:.1f} s)", flush=True) + + document = {"version": ASSEMBLY_FORMAT_VERSION, "model": str(args.step), + "scaleToCm": scale, "mergeTolerance": oracle.merge_tolerance, + "parts": [p.name for p in parts], "stats": stats, + "oracleSeconds": time.time() - t0, "rays": answers} + if args.out: + args.out.write_text(json.dumps(document)) + print(f" {stats['rays']} rays, {stats['crossings']} crossings, {stats['contact']} touching " + f"transitions, {stats['ovlClean']} rays with ambiguous occupancy " + f"({stats['ovl']} before excluding OCCT-ambiguous rays), {stats['thin']} thin " + f"vacuum runs, {stats['amb']} ambiguous ({time.time() - t0:.1f} s)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/cadsupport_path.py b/Detectors/CADSupport/validation/cadsupport_path.py new file mode 100644 index 0000000000000..3683c4de96cfa --- /dev/null +++ b/Detectors/CADSupport/validation/cadsupport_path.py @@ -0,0 +1,21 @@ +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-09 + +"""Put `../tools` on `sys.path`, so a validation script can import the `cadsupport` package.""" + +import sys +from pathlib import Path + +_TOOLS = str(Path(__file__).resolve().parent.parent / "tools") +if _TOOLS not in sys.path: + sys.path.insert(0, _TOOLS) diff --git a/Detectors/CADSupport/validation/checkKnownSource.py b/Detectors/CADSupport/validation/checkKnownSource.py new file mode 100644 index 0000000000000..dee48be279401 --- /dev/null +++ b/Detectors/CADSupport/validation/checkKnownSource.py @@ -0,0 +1,833 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Acceptance test 3: score a converted part against the `TGeoShape` it was made from. + +For every part a TGeo -> STEP -> TGeo round trip carries as CSG, it finds the original volume +through the writer report and compares: + + * **class** -- and, for two `TGeoPcon`, the sampled profile; + * **capacity** -- relative agreement where both are analytic; a composite is *not comparable*; + * **containment** -- a seeded point set classified by both shapes, with the `shapePlacement` + from `csg_report.json` composed as `geom.C` does. + +Reading the verdict +------------------- +A **failure** is a wrong class, a profile off by more than `--profile-tolerance` (relative to the +diagonal, default `recognise.REL_TOL`), or any containment disagreement. A **flag** is a capacity +that agrees to less than `--capacity-tolerance` (1e-9); `--strict` makes flags fatal. + +Points nearer the boundary than `--skin` are not scored, and are counted. The band is taken on +both shapes, or on the source alone where the emitted `Safety` is only a lower bound +(`o2::cad::O2FlatCSG`); `skinnedBoth` says which. + +One body of a multi-body CAD label (`..._b1`, `..._b2`) is scored one-way, with capacity not +comparable, and flagged. Duplicate names (`name#2`) are resolved through the writer report, and a +`name__mirrored` prototype by reflecting z. + +Usage +----- + checkKnownSource.py --original o2sim_geometry.root --writer-report PIPE_writer_report.json \\ + --converted /path/to/converter/output [--points 20000] [--json out.json] + checkKnownSource.py --self-test + +Exit status is non-zero if any part fails. +""" + +import argparse +import json +import math +import random +import re +import sys +from pathlib import Path + +import cadsupport_path # noqa: E402,F401 (puts ../tools on sys.path) + +from cadsupport.primitives import placement_to_local # noqa: E402 + +# Capacity is compared as a relative deviation, as a flag rather than a failure. +CAPACITY_TOLERANCE = 1.0e-9 +# The profile tolerance is the recogniser's `REL_TOL`, relative to the bounding-box diagonal. +PROFILE_TOLERANCE = 1.0e-6 +# A point this close to either boundary is not scored; the two boundaries agree to a few ulp. +DEFAULT_SKIN_CM = 1.0e-9 +DEFAULT_POINTS = 20000 +DEFAULT_SEED = 20260823 + +# `Capacity()` is a Monte-Carlo estimate for these classes. +_SAMPLED_CAPACITY_CLASSES = ("TGeoCompositeShape", "TGeoUnion", "TGeoIntersection", + "TGeoSubtraction", "TGeoHalfSpace") + + +# ------------------------------------------------------------------------------------------ +# profile comparison for the polycone family +# ------------------------------------------------------------------------------------------ + +def pcon_sections(shape, mirrored=False): + """[(z, rmin, rmax)] of a polycone, Z-mirrored if the writer emitted the mirrored prototype.""" + rows = [(shape.GetZ(i), shape.GetRmin(i), shape.GetRmax(i)) for i in range(shape.GetNz())] + if mirrored: + rows = [(-z, rmin, rmax) for z, rmin, rmax in reversed(rows)] + return rows + + +def _radii_at(sections, z): + """(rmin, rmax) at `z`, clamped to the profile's ends; the extents are compared separately.""" + z = min(max(z, sections[0][0]), sections[-1][0]) + for i in range(len(sections) - 1): + z0, rmin0, rmax0 = sections[i] + z1, rmin1, rmax1 = sections[i + 1] + if z1 <= z0: + continue + if z0 <= z <= z1: + f = (z - z0) / (z1 - z0) + return rmin0 + f * (rmin1 - rmin0), rmax0 + f * (rmax1 - rmax0) + return sections[-1][1], sections[-1][2] + + +def pcon_profile_deviation(sa, sb, merge_tolerance=0.0): + """The largest radial or axial disagreement, in cm, between two polycone profiles. + + Sampled inside every section, so a redundant z plane is not reported as a difference. + """ + levels = [] + for z in sorted({z for z, _r0, _r1 in sa} | {z for z, _r0, _r1 in sb}): + if not levels or z - levels[-1] > merge_tolerance: + levels.append(z) + worst = max(abs(sa[0][0] - sb[0][0]), abs(sa[-1][0] - sb[-1][0])) + for i in range(len(levels) - 1): + z0, z1 = levels[i], levels[i + 1] + if z1 <= z0: + continue + for f in (1.0e-9, 0.25, 0.5, 0.75, 1.0 - 1.0e-9): + z = z0 + f * (z1 - z0) + ra, rb = _radii_at(sa, z), _radii_at(sb, z) + worst = max(worst, abs(ra[0] - rb[0]), abs(ra[1] - rb[1])) + return worst + + +def _phi_deviation(a, b): + """Degrees. A mirror in z leaves phi alone, so this needs no mirrored variant.""" + return max(abs(a.GetPhi1() - b.GetPhi1()), abs(a.GetDphi() - b.GetDphi())) + + +def shape_scale(shape): + """The shape's bounding-box diagonal in cm, the length every relative tolerance is against.""" + return math.sqrt(shape.GetDX() ** 2 + shape.GetDY() ** 2 + shape.GetDZ() ** 2) + + +# ------------------------------------------------------------------------------------------ +# the per-part comparison +# ------------------------------------------------------------------------------------------ + +def placement_is_identity(placement): + if placement is None: + return True + for r in range(3): + for c in range(4): + want = 1.0 if r == c else 0.0 + if abs(placement[r][c] - want) > 1.0e-12: + return False + return True + + +def _bbox_of(shape): + origin = [shape.GetOrigin()[i] for i in range(3)] + half = [shape.GetDX(), shape.GetDY(), shape.GetDZ()] + return origin, half + + +_ONE_BODY_OF_MANY = re.compile(r"_b\d+$") + + +def part_is_one_body_of_many(part): + """True when the part is one body (`#b1`, `#b2`, ...) of a CAD label that carried several.""" + return bool(_ONE_BODY_OF_MANY.search(part.get("part") or "")) + + +def safety_is_a_true_distance(shape): + """Whether \a shape's `Safety` is a distance to its boundary, or only a lower bound on one. + + `o2::cad::O2FlatCSG`'s `Safety` is a bound from its sub-cell boxes, often 0 inside. + """ + return shape.ClassName() != "o2::cad::O2FlatCSG" + + +def contains_crosscheck(source, emitted, placement, n_points, seed, skin, max_report, + mirrored=False, one_way=False): + """Classify a seeded point set against both shapes; every disagreement is reported. + + `mirrored` reflects the point into the emitted shape's frame, as `geom.C` does. `one_way` scores + only "inside the emitted shape implies inside the source", for one body of a multi-body source. + The skin is taken on the source alone when the emitted `Safety` is only a lower bound, which + makes the test stricter; `skinnedBoth` records which rule ran. + """ + from array import array + origin, half = _bbox_of(source) + rng = random.Random(seed) + scored = 0 + skipped = 0 + n_mismatches = 0 + n_inside_emitted = 0 + examples = [] + local = array("d", [0.0, 0.0, 0.0]) + probe = array("d", [0.0, 0.0, 0.0]) + skin_both = safety_is_a_true_distance(emitted) + for _ in range(n_points): + point = tuple(origin[i] + rng.uniform(-half[i], half[i]) for i in range(3)) + probe[0], probe[1], probe[2] = point + inside_source = bool(source.Contains(probe)) + if source.Safety(probe, inside_source) < skin: + skipped += 1 + continue + reflected = (point[0], point[1], -point[2]) if mirrored else point + moved = placement_to_local(placement, reflected) + local[0], local[1], local[2] = moved + inside_emitted = bool(emitted.Contains(local)) + if skin_both and emitted.Safety(local, inside_emitted) < skin: + skipped += 1 + continue + scored += 1 + if inside_emitted: + n_inside_emitted += 1 + if inside_source != inside_emitted: + if one_way and inside_source and not inside_emitted: + continue # a sibling body of the same label carries it + # Counted in full; only the first `max_report` are kept for printing. + n_mismatches += 1 + if len(examples) < max_report: + examples.append({"point": [float(c) for c in point], + "local": [float(c) for c in moved], + "source": inside_source, "emitted": inside_emitted}) + return {"points": scored, "skipped": skipped, "mismatches": n_mismatches, + "insideEmitted": n_inside_emitted, "oneWay": bool(one_way), "skinnedBoth": skin_both, + "examples": examples} + + +def reclose_flat_csg(shape): + """Rebuild an `o2::cad::O2FlatCSG`'s sub-cell boxes after it comes off a file (idempotent).""" + if shape.ClassName() != "o2::cad::O2FlatCSG": + return True + if not shape.IsClosed(): + shape.CloseShape() + return bool(shape.IsClosed()) + + +def check_part(part, row, source_shape, emitted_shape, placement, n_points, seed, skin, + capacity_tolerance, profile_tolerance, max_report): + """Compare one converted part against its source shape. Returns a record.""" + mirrored = bool(row.get("mirrored")) + one_body = part_is_one_body_of_many(part) + source_class = row.get("shapeClass") or source_shape.ClassName() + scale = max(shape_scale(source_shape), 1.0) + record = {"part": part.get("part"), "volume": part.get("volume"), + "source": row.get("name"), "mirrored": mirrored, "oneBodyOfMany": one_body, + "sourceClass": source_class, "emittedClass": emitted_shape.ClassName(), + "placementIsIdentity": placement_is_identity(placement), + "classComparable": False, "classMatches": None, + "profileDeviationCm": None, "profileToleranceCm": profile_tolerance * scale, + "phiDeviationDeg": None, + "capacityComparable": False, "capacitySource": None, "capacityEmitted": None, + "capacityRelativeDeviation": None, + "contains": None, "failures": [], "flags": []} + + # A different class is flagged, not failed; capacity and containment carry the verdict. + same_class = source_class == emitted_shape.ClassName() + record["classComparable"] = not one_body + record["classMatches"] = None if one_body else same_class + if one_body: + record["flags"].append( + "one body of a multi-body CAD label: the source is the whole label, so the class " + "and the capacity are not comparable and containment is scored one-way") + elif not same_class: + record["flags"].append( + f"class {emitted_shape.ClassName()} is not the source's {source_class}") + + # Under a non-identity placement the profiles differ legitimately; containment decides. + if (same_class and not one_body and source_class == "TGeoPcon" + and record["placementIsIdentity"]): + record["phiDeviationDeg"] = _phi_deviation(source_shape, emitted_shape) + deviation = pcon_profile_deviation(pcon_sections(source_shape, mirrored), + pcon_sections(emitted_shape), + merge_tolerance=profile_tolerance * scale) + record["profileDeviationCm"] = deviation + if deviation > record["profileToleranceCm"]: + record["failures"].append( + f"the polycone profile is {deviation:.6g} cm off the source's, over the " + f"{record['profileToleranceCm']:.3g} cm the recogniser claims") + if record["phiDeviationDeg"] > 1.0e-9: + record["failures"].append( + f"phi differs from the source by {record['phiDeviationDeg']:.6g} deg") + + # The writer's record of the emitted volume is unambiguous even where two volumes share a name. + capacity_source = row.get("capacity_cm3") + if capacity_source is None and source_class not in _SAMPLED_CAPACITY_CLASSES: + capacity_source = float(source_shape.Capacity()) + record["capacitySource"] = capacity_source + record["capacityEmitted"] = float(emitted_shape.Capacity()) + comparable = (capacity_source is not None and capacity_source > 0.0 and not one_body + and source_class not in _SAMPLED_CAPACITY_CLASSES + and emitted_shape.ClassName() not in _SAMPLED_CAPACITY_CLASSES) + if comparable: + record["capacityComparable"] = True + rel = abs(record["capacityEmitted"] - capacity_source) / capacity_source + record["capacityRelativeDeviation"] = rel + if rel > capacity_tolerance: + record["flags"].append( + f"capacity {record['capacityEmitted']:.9g} cm^3 differs from the source's " + f"{capacity_source:.9g} cm^3 by {rel:.3g} relative") + + record["contains"] = contains_crosscheck(source_shape, emitted_shape, placement, n_points, + seed, skin, max_report, mirrored, one_body) + if record["contains"]["mismatches"]: + record["failures"].append( + f"{record['contains']['mismatches']} containment disagreement(s) over " + f"{record['contains']['points']} scored point(s)") + if record["contains"]["points"] == 0: + record["failures"].append("no point was scored: the comparison is empty") + if one_body and record["contains"]["insideEmitted"] == 0: + # Without this a one-way comparison would be passed by a body that encloses nothing. + record["failures"].append( + f"the emitted body encloses none of the {record['contains']['points']} scored " + "point(s): the one-way comparison is empty") + return record + + +# ------------------------------------------------------------------------------------------ +# driving a converter output directory +# ------------------------------------------------------------------------------------------ + +def _writer_index(writer_report): + """emittedName -> the writer's row, which carries the source volume's real `name`. + + A `__body` solid is indexed through its parent's `bodyComponent`, whose class and + capacity are the body's. + """ + index = {} + for row in writer_report.get("volumes", []): + emitted = row.get("emittedName") or row.get("name") + if emitted: + index[emitted] = row + body = row.get("bodyComponent") + if body and body != emitted: + index[body] = row + return index + + +def _placed_box(placement, shape): + """A shape's axis-aligned box in the part frame: `(origin, half)`.""" + origin = [shape.GetOrigin()[i] for i in range(3)] + half = [shape.GetDX(), shape.GetDY(), shape.GetDZ()] + if placement is None: + return origin, half + lo = [float("inf")] * 3 + hi = [float("-inf")] * 3 + for sx in (-1.0, 1.0): + for sy in (-1.0, 1.0): + for sz in (-1.0, 1.0): + local = (origin[0] + sx * half[0], origin[1] + sy * half[1], + origin[2] + sz * half[2]) + for i in range(3): + v = sum(placement[i][c] * local[c] for c in range(3)) + placement[i][3] + lo[i] = min(lo[i], v) + hi[i] = max(hi[i], v) + return [0.5 * (lo[i] + hi[i]) for i in range(3)], [0.5 * (hi[i] - lo[i]) for i in range(3)] + + +def resolve_source_volume(candidates, row, emitted_shape=None, placement=None): + """Which of several volumes sharing one name the writer's row refers to. + + The bounding box decides, being exact for every `TGeoShape`; capacity is only a tie-break + where it is analytic, since a composite's is Monte-Carlo. + """ + if len(candidates) == 1: + return candidates[0] + wanted_class = row.get("shapeClass") + wanted_capacity = row.get("capacity_cm3") + sampled = wanted_class in _SAMPLED_CAPACITY_CLASSES + want_box = (_placed_box(placement, emitted_shape) + if emitted_shape is not None else None) + best, best_key = None, None + for volume in candidates: + shape = volume.GetShape() + if wanted_class and shape.ClassName() != wanted_class: + continue + box_score = 0.0 + if want_box is not None: + here = _placed_box(None, shape) + box_score = max(max(abs(here[0][i] - want_box[0][i]) for i in range(3)), + max(abs(here[1][i] - want_box[1][i]) for i in range(3))) + capacity_score = (0.0 if (wanted_capacity is None or sampled) + else abs(shape.Capacity() - wanted_capacity) + / max(abs(wanted_capacity), 1.0e-30)) + key = (box_score, capacity_score) + if best_key is None or key < best_key: + best, best_key = volume, key + return best + + +def check_run(original, writer_report_path, converted, n_points=DEFAULT_POINTS, + seed=DEFAULT_SEED, skin=DEFAULT_SKIN_CM, capacity_tolerance=CAPACITY_TOLERANCE, + profile_tolerance=PROFILE_TOLERANCE, max_report=5, verbose=True): + """Compare every CSG-carried part of a converter output against its source volume.""" + import ROOT + ROOT.gROOT.SetBatch(True) + converted = Path(converted) + csg_report_path = converted / "csg_report.json" + if not csg_report_path.exists(): + raise SystemExit(f"{csg_report_path} does not exist (convert with --csg auto)") + csg_report = json.loads(csg_report_path.read_text()) + writer_report = json.loads(Path(writer_report_path).read_text()) + index = _writer_index(writer_report) + + manager = ROOT.TGeoManager.Import(str(original)) + if manager is None: + raise SystemExit(f"could not read a TGeoManager from {original}") + by_name = {} + for volume in manager.GetListOfVolumes(): + by_name.setdefault(volume.GetName(), []).append(volume) + + records = [] + open_files = [] + for part in csg_report.get("parts", []): + if part.get("representation") != "csg": + continue + emitted_name = part.get("volume") + stub = {"part": part.get("part"), "volume": emitted_name, "failures": [], "flags": []} + row = index.get(emitted_name) + if row is None: + stub["failures"].append(f"no writer-report row for emittedName {emitted_name!r}") + records.append(stub) + continue + shape_file = part.get("shapeFile") + if not shape_file or not Path(shape_file).exists(): + stub["failures"].append(f"shapeFile {shape_file!r} does not exist") + records.append(stub) + continue + handle = ROOT.TFile.Open(str(shape_file)) + open_files.append(handle) + emitted_shape = handle.Get("shape") + if not emitted_shape: + stub["failures"].append(f"{shape_file} carries no object under the key \"shape\"") + records.append(stub) + continue + if not reclose_flat_csg(emitted_shape): + stub["failures"].append( + f"{shape_file}: O2FlatCSG::CloseShape refused the shape after reading it, so " + "its sub-cell boxes could not be rebuilt") + records.append(stub) + continue + # The emitted shape is read first: its bounding box tells same-named volumes apart. + candidates = by_name.get(row.get("name")) or [] + source_volume = (resolve_source_volume(candidates, row, emitted_shape, + part.get("shapePlacement")) + if candidates else None) + if source_volume is None: + stub["failures"].append( + f"the original geometry has no volume named {row.get('name')!r} whose shape " + "matches the writer's record") + records.append(stub) + continue + record = check_part(part, row, source_volume.GetShape(), emitted_shape, + part.get("shapePlacement"), n_points, seed, skin, + capacity_tolerance, profile_tolerance, max_report) + records.append(record) + if verbose: + print_record(record) + + n_fail = sum(1 for r in records if r["failures"]) + n_flag = sum(1 for r in records if r.get("flags")) + if verbose: + worst_capacity = max([r["capacityRelativeDeviation"] for r in records + if r.get("capacityRelativeDeviation") is not None] or [0.0]) + worst_profile = max([r["profileDeviationCm"] for r in records + if r.get("profileDeviationCm") is not None] or [0.0]) + print(f"\n{len(records) - n_fail}/{len(records)} CSG part(s) agree with their source " + f"TGeoShape ({n_fail} failure(s), {n_flag} flag(s))") + print(f"worst capacity deviation {worst_capacity:.3g} relative, worst polycone profile " + f"deviation {worst_profile:.3g} cm") + for handle in open_files: + handle.Close() + return records, n_fail, n_flag + + +def print_record(record): + if record["failures"]: + print(f" [FAIL] {record['volume']}: " + "; ".join(record["failures"])) + for example in (record.get("contains") or {}).get("examples", []): + print(f" at {example['point']} (local {example['local']}): " + f"source {'in' if example['source'] else 'out'}, " + f"emitted {'in' if example['emitted'] else 'out'}") + return + bits = [record["emittedClass"]] + if record.get("mirrored"): + bits.append("mirrored prototype") + if record.get("oneBodyOfMany"): + bits.append("one body of many, scored one-way") + if record.get("classComparable"): + bits.append("class matches" if record["classMatches"] else "class differs") + if record.get("profileDeviationCm") is not None: + bits.append(f"profile {record['profileDeviationCm']:.3g} cm") + if record.get("capacityComparable"): + bits.append(f"capacity rel {record['capacityRelativeDeviation']:.3g}") + else: + bits.append("capacity not comparable") + contains = record.get("contains") or {} + bits.append(f"Contains {contains.get('mismatches')}/{contains.get('points')} " + f"({contains.get('skipped')} on the skin)") + marker = "flag" if record.get("flags") else "ok " + print(f" [{marker}] {record['volume']}: " + ", ".join(bits)) + for flag in record.get("flags", []): + print(f" flag: {flag}") + + +# ------------------------------------------------------------------------------------------ +# self-test +# ------------------------------------------------------------------------------------------ +# The fixtures are built in a subprocess, since ROOT cannot create one geometry and import another. + +_FIXTURE_BUILDER = r""" +import json, sys +from pathlib import Path +import ROOT +ROOT.gROOT.SetBatch(True) + +folder = Path(sys.argv[1]) +sections = [(-5.0, 1.0, 3.0), (0.0, 1.0, 3.0), (0.0, 2.0, 4.0), (5.0, 2.0, 4.0)] +names = ("GOOD", "PLACED", "MIRRORED", "TWIN", "BAD", "WRONGCLASS", "TWOBODY") + + +def halves_shape(zlow): + # One half of a tube, as a composite, so its Capacity() is a Monte-Carlo estimate. + tag = "lo" if zlow < 0.0 else "hi" + tube = ROOT.TGeoTube("halves_t_" + tag, 0.0, 2.0, 1.0) + slab = ROOT.TGeoBBox("halves_b_" + tag, 3.0, 3.0, 0.5) + shift = ROOT.TGeoTranslation("halves_m_" + tag, 0.0, 0.0, zlow + 0.5) + for obj in (tube, slab, shift): + ROOT.SetOwnership(obj, False) + node = ROOT.TGeoIntersection(tube, slab, ROOT.nullptr, shift) + ROOT.SetOwnership(node, False) + comp = ROOT.TGeoCompositeShape("halves_c_" + tag, node) + ROOT.SetOwnership(comp, False) + return comp + + +def make_pcon(name, rows, phi1=0.0, dphi=360.0): + shape = ROOT.TGeoPcon(name, phi1, dphi, len(rows)) + for i, (z, rmin, rmax) in enumerate(rows): + shape.DefineSection(i, z, rmin, rmax) + ROOT.SetOwnership(shape, False) + return shape + + +geometry = ROOT.TGeoManager("knownsource", "known-source self-test") +material = ROOT.TGeoMaterial("Vacuum", 0, 0, 0) +medium = ROOT.TGeoMedium("Vacuum", 1, material) +top = geometry.MakeBox("TOP", medium, 50.0, 50.0, 50.0) +geometry.SetTopVolume(top) +for name in names: + volume = ROOT.TGeoVolume(name, make_pcon(name + "_sh", sections), medium) + ROOT.SetOwnership(volume, False) + top.AddNode(volume, 1) +# A second volume under a taken name: the writer emits `TWIN#2` and the checker must find this one. +twin = [(z, rmin, rmax + 1.0) for z, rmin, rmax in sections] +second = ROOT.TGeoVolume("TWIN", make_pcon("twin2_sh", twin), medium) +ROOT.SetOwnership(second, False) +top.AddNode(second, 1) +# Two composites of one name, the two halves of a tube: only a bounding box tells them apart. +for zlow in (-1.0, 0.0): + half = ROOT.TGeoVolume("HALVES", halves_shape(zlow), medium) + ROOT.SetOwnership(half, False) + top.AddNode(half, 1) +geometry.CloseGeometry() +geometry.Export(str(folder / "source_geometry.root")) + + +def capacity(rows): + return make_pcon("cap_probe", rows).Capacity() + + +rows = [{"name": n, "emittedName": n, "shapeClass": "TGeoPcon", "mirrored": n == "MIRRORED", + "capacity_cm3": capacity(sections)} for n in names] +rows.append({"name": "TWIN", "emittedName": "TWIN#2", "shapeClass": "TGeoPcon", + "mirrored": False, "capacity_cm3": capacity(twin)}) +# The writer's capacity for a composite is another Monte-Carlo draw, never used for ranking. +rows.append({"name": "HALVES", "emittedName": "HALVES", "shapeClass": "TGeoCompositeShape", + "mirrored": False, "capacity_cm3": halves_shape(0.0).Capacity()}) +(folder / "writer_report.json").write_text(json.dumps({"volumes": rows})) + + +def write_shape(name, shape, placement=None): + target = folder / ("shape_%s.root" % name.replace("#", "_")) + out = ROOT.TFile.Open(str(target), "RECREATE") + out.WriteTObject(shape, "shape") + out.Close() + return {"part": name, "volume": name, "representation": "csg", + "shapeFile": str(target), "shapePlacement": placement} + + +parts = [write_shape("GOOD", make_pcon("good_sh", sections))] +# A load-bearing placement: the profile sits 7 cm up and the placement brings it back. +shifted = [(z + 7.0, rmin, rmax) for z, rmin, rmax in sections] +parts.append(write_shape("PLACED", make_pcon("placed_sh", shifted), + [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, -7.0]])) +# The Z-mirrored prototype the writer emits for a volume placed by a reflecting matrix. +mirrored = [(-z, rmin, rmax) for z, rmin, rmax in reversed(sections)] +parts.append(write_shape("MIRRORED", make_pcon("mirrored_sh", mirrored))) +parts.append(write_shape("TWIN", make_pcon("twin_sh", sections))) +parts.append(write_shape("TWIN#2", make_pcon("twin2_out_sh", twin))) +# The emitted body is the LOWER half; the checker must resolve to the lower source volume. +parts.append(write_shape("HALVES", halves_shape(-1.0))) +wrong = [(z, rmin, rmax + (0.05 if i == 3 else 0.0)) + for i, (z, rmin, rmax) in enumerate(sections)] +parts.append(write_shape("BAD", make_pcon("bad_sh", wrong))) +tube = ROOT.TGeoTube("wrongclass_sh", 1.0, 4.0, 5.0) +ROOT.SetOwnership(tube, False) +parts.append(write_shape("WRONGCLASS", tube)) +# One body of a two-body CAD label: the upper half of the source's profile. +upper = [(0.5, 2.0, 4.0), (5.0, 2.0, 4.0)] +body = write_shape("TWOBODY_b2", make_pcon("twobody_sh", upper)) +body["volume"] = "TWOBODY" +parts.append(body) +# ... and a body that sticks OUT of its own label must still fail. +outside = [(0.5, 2.0, 5.0), (5.0, 2.0, 5.0)] +spill = write_shape("TWOBODYBAD_b2", make_pcon("twobodybad_sh", outside)) +spill["volume"] = "TWOBODY" +parts.append(spill) +(folder / "csg_report.json").write_text(json.dumps({"parts": parts})) + +# Every file is on disk; skip the teardown, which ROOT's global geometry does not survive. +import os +sys.stdout.flush() +os._exit(0) +""" + + +def self_test(verbose=True, workdir=None): + """A geometry, a writer report and a converter output built here, with a known verdict. + + Three negative controls: a displaced radius, a wrong class, and the placed control without its + placement. The working folder is left under the system temporary directory. + """ + import subprocess + import tempfile + + checks = [] + + def check(name, condition, detail=""): + checks.append((name, bool(condition), detail)) + if verbose: + print(f" [{'ok ' if condition else 'FAIL'}] {name}" + + (f" {detail}" if detail else "")) + + folder = Path(tempfile.mkdtemp(prefix="knownsource_")) if workdir is None else Path(workdir) + folder.mkdir(parents=True, exist_ok=True) + builder = folder / "_build_fixtures.py" + builder.write_text(_FIXTURE_BUILDER) + subprocess.run([sys.executable, str(builder), str(folder)], check=True, + stdout=subprocess.DEVNULL) + + records, n_fail, _n_flag = check_run(folder / "source_geometry.root", + folder / "writer_report.json", folder, + n_points=20000, verbose=False) + by_name = {r["volume"]: r for r in records} + + good = by_name.get("GOOD", {}) + check("the positive control passes every comparable check", + not good.get("failures") and not good.get("flags") + and good.get("classMatches") is True and good.get("capacityComparable") is True + and good.get("contains", {}).get("mismatches") == 0, + f"failures {good.get('failures')}, flags {good.get('flags')}, capacity rel " + f"{good.get('capacityRelativeDeviation')}, Contains " + f"{good.get('contains', {}).get('mismatches')}/" + f"{good.get('contains', {}).get('points')}") + check("the positive control actually scored a useful point set", + good.get("contains", {}).get("points", 0) > 1000, + f"{good.get('contains', {}).get('points')} point(s) scored, " + f"{good.get('contains', {}).get('skipped')} on the skin") + + placed = by_name.get("PLACED", {}) + check("a shape whose placement is composed correctly passes", + not placed.get("failures") and placed.get("contains", {}).get("mismatches") == 0, + f"failures {placed.get('failures')}") + ignored = _placed_without_its_placement(folder) + check("the placement is load-bearing: ignoring it must fail", + ignored is not None and ignored["mismatches"] > 0, + f"{ignored['mismatches'] if ignored else 'not run'} disagreement(s) with a null " + "placement") + + mirrored = by_name.get("MIRRORED", {}) + check("a Z-mirrored prototype is compared through the mirror and passes", + not mirrored.get("failures") and mirrored.get("mirrored") is True + and mirrored.get("contains", {}).get("mismatches") == 0, + f"failures {mirrored.get('failures')}") + + twin2 = by_name.get("TWIN#2", {}) + check("a name shared by two volumes resolves to the right one", + not twin2.get("failures") and twin2.get("capacityComparable") is True + and twin2.get("capacityRelativeDeviation") is not None + and twin2["capacityRelativeDeviation"] < 1.0e-12, + f"failures {twin2.get('failures')}, capacity rel " + f"{twin2.get('capacityRelativeDeviation')}") + + halves = by_name.get("HALVES", {}) + check("two same-named composites are told apart by their box, not by a sampled capacity", + not halves.get("failures") + and halves.get("contains", {}).get("mismatches") == 0, + f"failures {halves.get('failures')}, Contains " + f"{halves.get('contains', {}).get('mismatches')}/" + f"{halves.get('contains', {}).get('points')}") + check("and their capacities really could not have decided it", + _halves_capacities_are_indistinguishable(folder), + "the two halves' Capacity() draws are within Monte-Carlo noise of each other") + + bad = by_name.get("BAD", {}) + check("the negative control is caught", bool(bad.get("failures")), + "; ".join(bad.get("failures", [])) or "NOT CAUGHT") + check("the negative control is caught by containment, not only by the profile", + bad.get("contains", {}).get("mismatches", 0) > 0, + f"{bad.get('contains', {}).get('mismatches')} disagreement(s)") + check("the negative control's profile deviation is the displacement", + bad.get("profileDeviationCm") is not None + and abs(bad["profileDeviationCm"] - 0.05) < 1.0e-9, + f"{bad.get('profileDeviationCm')}") + + wrongclass = by_name.get("WRONGCLASS", {}) + check("a shape of the wrong class is caught by the metrics, not only by its class", + bool(wrongclass.get("failures")) + and wrongclass.get("contains", {}).get("mismatches", 0) > 0, + "; ".join(wrongclass.get("failures", [])) or "NOT CAUGHT") + check("a class that differs without a geometric difference is a flag, not a failure", + wrongclass.get("classMatches") is False and any( + "is not the source's" in f for f in wrongclass.get("flags", [])), + f"flags {wrongclass.get('flags')}") + + two_body = next((r for r in records if r["part"] == "TWOBODY_b2"), {}) + check("one body of a multi-body label passes on the one-way containment test", + not two_body.get("failures") and two_body.get("oneBodyOfMany") is True + and two_body.get("contains", {}).get("oneWay") is True + and two_body.get("contains", {}).get("mismatches") == 0 + and two_body.get("contains", {}).get("insideEmitted", 0) > 100, + f"failures {two_body.get('failures')}, " + f"{two_body.get('contains', {}).get('insideEmitted')} point(s) inside the body") + check("a multi-body part's class and capacity are reported as not comparable", + two_body.get("capacityComparable") is False + and two_body.get("classComparable") is False + and any("multi-body" in f for f in two_body.get("flags", [])), + f"flags {two_body.get('flags')}") + spilled = next((r for r in records if r["part"] == "TWOBODYBAD_b2"), {}) + check("the one-way rule still catches a body that sticks out of its own label", + bool(spilled.get("failures")) + and spilled.get("contains", {}).get("mismatches", 0) > 0, + "; ".join(spilled.get("failures", [])) or "NOT CAUGHT") + + check("the run reports exactly the three deliberately wrong parts as failures", + n_fail == 3, f"{n_fail} failure(s) over {len(records)} part(s)") + + n_ok = sum(1 for _n, ok, _d in checks if ok) + if verbose: + print(f" {n_ok}/{len(checks)} known-source self-checks passed (fixtures in {folder})") + return n_ok, len(checks) + + +def _halves_capacities_are_indistinguishable(folder): + """Are the two same-named composites' capacities within Monte-Carlo noise of each other?""" + import ROOT + manager = ROOT.gGeoManager + if not manager: + return False + capacities = [volume.GetShape().Capacity() for volume in manager.GetListOfVolumes() + if volume.GetName() == "HALVES"] + if len(capacities) != 2: + return False + return abs(capacities[0] - capacities[1]) / max(capacities) < 0.02 + + +def _placed_without_its_placement(folder): + """Re-run the placed positive control with a null placement; it must then disagree.""" + import ROOT + manager = ROOT.gGeoManager + if not manager: + return None + source = None + for volume in manager.GetListOfVolumes(): + if volume.GetName() == "PLACED": + source = volume.GetShape() + break + if source is None: + return None + handle = ROOT.TFile.Open(str(Path(folder) / "shape_PLACED.root")) + emitted = handle.Get("shape") + result = contains_crosscheck(source, emitted, None, 5000, DEFAULT_SEED, DEFAULT_SKIN_CM, 1) + handle.Close() + return result + + +# ------------------------------------------------------------------------------------------ + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--original", type=Path, + help="the o2sim_geometry.root the STEP was written from") + ap.add_argument("--writer-report", type=Path, dest="writer_report", + help="O2_TGeoToCAD.py's --report JSON, which maps emittedName -> name") + ap.add_argument("--converted", type=Path, + help="the converter output folder (csg_report.json and shape_*.root)") + ap.add_argument("--points", type=int, default=DEFAULT_POINTS, + help="containment samples per part; default %(default)s") + ap.add_argument("--seed", type=int, default=DEFAULT_SEED, + help="the fixed seed for those samples; default %(default)s") + ap.add_argument("--skin", type=float, default=DEFAULT_SKIN_CM, + help="do not score points nearer than this to either boundary, in cm; " + "default %(default)s") + ap.add_argument("--capacity-tolerance", type=float, default=CAPACITY_TOLERANCE, + dest="capacity_tolerance", + help="relative capacity agreement below which a part is flagged; " + "default %(default)s") + ap.add_argument("--profile-tolerance", type=float, default=PROFILE_TOLERANCE, + dest="profile_tolerance", + help="polycone profile agreement demanded, relative to the part's diagonal; " + "default %(default)s, which is cadsupport/recognise.REL_TOL") + ap.add_argument("--strict", action="store_true", + help="treat capacity flags as failures too") + ap.add_argument("--max-report", type=int, default=5, dest="max_report", + help="how many disagreeing points to print per part; default %(default)s") + ap.add_argument("--json", type=Path, help="write the per-part records here") + ap.add_argument("--self-test", action="store_true") + args = ap.parse_args() + + if args.self_test: + n_ok, n = self_test() + print(f"\n{n_ok}/{n} known-source self-checks passed") + return 0 if n_ok == n else 1 + + if not (args.original and args.writer_report and args.converted): + ap.error("give --original, --writer-report and --converted, or --self-test") + + records, n_fail, n_flag = check_run(args.original, args.writer_report, args.converted, + n_points=args.points, seed=args.seed, skin=args.skin, + capacity_tolerance=args.capacity_tolerance, + profile_tolerance=args.profile_tolerance, + max_report=args.max_report) + if args.json: + args.json.write_text(json.dumps(records, indent=1)) + print(f"Wrote {args.json}") + return 1 if (n_fail or (args.strict and n_flag)) else 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/check_media.py b/Detectors/CADSupport/validation/closure/check_media.py new file mode 100644 index 0000000000000..738d2fc7b19b2 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/check_media.py @@ -0,0 +1,203 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Check that a round-tripped geometry carries the media of its source. + +For every converted volume it finds the source volume of the same name (dropping the writer's +`__body` and `__mirrored` suffixes) and compares, exactly: the medium name, all eight Geant medium +parameters, the material's Z, A, density, radiation and interaction length, and every mixture +element's Z, A and weight. A volume left on the `Default` placeholder is reported separately. + +Usage: + check_media.py --original o2sim_geometry.root --macro conv/geom.C [--json out.json] +""" + +import argparse +import json +import os +import sys + +PARAMS = ("isvol", "ifield", "fieldm", "tmaxfd", "stemax", "deemax", "epsil", "stmin") + + +def base_name(name, hollow_rename=None): + """The source volume name behind a writer-emitted part name. + + `hollow_rename` is an exact map of tagged hall name -> source name, read from the writer report. + """ + if hollow_rename and name in hollow_rename: + return hollow_rename[name] + for suffix in ("__mirrored", "__body"): + while name.endswith(suffix): + name = name[: -len(suffix)] + if hollow_rename and name in hollow_rename: + return hollow_rename[name] + # `X#2` is the writer's disambiguation of one TGeo name over two definitions + return name.split("#", 1)[0] + + +def describe(vol): + # An assembly carries ROOT's `dummy` medium; the mother's material lives in its `__body` leaf. + if vol.IsAssembly(): + return None + med = vol.GetMedium() + if med is None: + return None + mat = med.GetMaterial() + d = { + "medium": str(med.GetName()), + "params": [float(med.GetParam(i)) for i in range(8)], + "material": str(mat.GetName()), + "Z": float(mat.GetZ()), "A": float(mat.GetA()), + "density": float(mat.GetDensity()), + "radLen": float(mat.GetRadLen()), "intLen": float(mat.GetIntLen()), + "isMixture": bool(mat.IsMixture()), + } + if mat.IsMixture(): + n = int(mat.GetNelements()) + zs, as_, ws = mat.GetZmixt(), mat.GetAmixt(), mat.GetWmixt() + d["elements"] = [[float(zs[i]), float(as_[i]), float(ws[i])] for i in range(n)] + return d + + +def diff(a, b, rtol): + """Field names that disagree between two describe() dicts.""" + bad = [] + if a["medium"] != b["medium"]: + bad.append("mediumName") + for i, k in enumerate(PARAMS): + if a["params"][i] != b["params"][i]: + bad.append(k) + if a["material"] != b["material"]: + bad.append("materialName") + for k in ("Z", "A", "density", "radLen", "intLen"): + x, y = a[k], b[k] + if x != y and (abs(x - y) > rtol * max(abs(x), abs(y), 1e-300)): + bad.append(k) + if a["isMixture"] != b["isMixture"]: + bad.append("isMixture") + elif a["isMixture"]: + if len(a["elements"]) != len(b["elements"]): + bad.append("nElements") + else: + for (za, aa, wa), (zb, ab, wb) in zip(a["elements"], b["elements"]): + if za != zb or aa != ab or wa != wb: + bad.append("elements") + break + return bad + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--original", required=True, help="the source o2sim_geometry.root") + p.add_argument("--macro", required=True, help="the converted geom.C") + p.add_argument("--writer-report", default=None, + help="the writer's JSON report, read for hollowVolumes/hollowTag " + "so a tagged hall volume still finds its source") + p.add_argument("--rtol", type=float, default=0.0, + help="relative tolerance on the scalar material fields " + "(default 0: require exact equality)") + p.add_argument("--json", help="write the full result here") + args = p.parse_args() + + hollow_rename = {} + if args.writer_report: + with open(args.writer_report) as fh: + rep = json.load(fh) + tag = rep.get("hollowTag") + if tag: + for h in rep.get("hollowVolumes", []): + hollow_rename[f"{h}_{tag}"] = h + + import ROOT + ROOT.gROOT.SetBatch(True) + + # The source is read into its own manager and set aside; the macro builds into a second one. + src_mgr = ROOT.TGeoManager.Import(args.original) + source = {} + for vol in src_mgr.GetListOfVolumes(): + d = describe(vol) + if d is not None: + source[str(vol.GetName())] = d + ROOT.gGeoManager = ROOT.nullptr + + # Interpreted, not ACLiC-compiled, as ExternalModule JITs it. + ROOT.gROOT.ProcessLine(f'.L {os.path.abspath(args.macro)}') + ROOT.gGeoManager = ROOT.TGeoManager("converted", "converted") + top = ROOT.build(False) + ROOT.gGeoManager.SetTopVolume(top) + ROOT.gGeoManager.CloseGeometry() + + res = {"nConverted": 0, "matched": 0, "default": [], "missingInSource": [], + "disagreements": [], "fieldCounts": {}, "assembliesSkipped": 0, + "maxRelDevRadLen": 0.0, "maxRelDevIntLen": 0.0} + for vol in ROOT.gGeoManager.GetListOfVolumes(): + name = str(vol.GetName()) + if vol.IsAssembly(): + res["assembliesSkipped"] += 1 + continue + d = describe(vol) + if d is None: + continue + res["nConverted"] += 1 + if d["medium"] == "Default": + res["default"].append(name) + continue + src = source.get(base_name(name, hollow_rename)) + if src is None: + res["missingInSource"].append(name) + continue + for key, slot in (("radLen", "maxRelDevRadLen"), ("intLen", "maxRelDevIntLen")): + x, y = src[key], d[key] + if max(abs(x), abs(y)) > 0: + res[slot] = max(res[slot], abs(x - y) / max(abs(x), abs(y))) + bad = diff(src, d, args.rtol) + if bad: + res["disagreements"].append({"volume": name, "fields": bad, + "source": src, "converted": d}) + for f in bad: + res["fieldCounts"][f] = res["fieldCounts"].get(f, 0) + 1 + else: + res["matched"] += 1 + + n = res["nConverted"] + print(f"converted volumes with a medium: {n}") + print(f" media identical to the source: {res['matched']}") + print(f" left on the Default placeholder (transparent): {len(res['default'])}" + + (f" e.g. {res['default'][:5]}" if res["default"] else "")) + print(f" no source volume of that name: {len(res['missingInSource'])}" + + (f" e.g. {res['missingInSource'][:5]}" if res["missingInSource"] else "")) + print(f" assemblies skipped (they hold no material): {res['assembliesSkipped']}") + print(f" max relative deviation: radLen {res['maxRelDevRadLen']:.3e}, " + f"intLen {res['maxRelDevIntLen']:.3e} (derived by ROOT from the recipe, " + f"not carried)") + print(f" disagreeing with the source: {len(res['disagreements'])}" + + (f" fields {res['fieldCounts']}" if res["fieldCounts"] else "")) + for d in res["disagreements"][:5]: + print(f" {d['volume']}: {d['fields']}") + + if args.json: + with open(args.json, "w") as fh: + json.dump(res, fh, indent=2) + print(f"wrote {args.json}") + + ok = (n > 0 and res["matched"] == n) + print("VERDICT:", "every volume carries its source medium" if ok else "INCOMPLETE") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/compare_distributions.py b/Detectors/CADSupport/validation/closure/compare_distributions.py new file mode 100644 index 0000000000000..bb3fd05c83c98 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/compare_distributions.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-09 + +"""Compare two o2-sim runs as distributions, not hit by hit. + + compare_distributions.py A/ B/ --file-a o2sim_HitsITS.root --branch-a ITSHit \ + --file-b o2sim.root --branch-b CITSHit --json out.json --npz out.npz +""" +import argparse +import json +import math +import numpy as np + + +def read(folder, fname, branch): + import ROOT + f = ROOT.TFile.Open(f"{folder}/{fname}") + t = f.Get("o2sim") + r, z, edep, per_event = [], [], [], [] + n = t.GetEntries() + for i in range(n): + t.GetEntry(i) + hits = getattr(t, branch) + per_event.append(len(hits)) + for h in hits: + x, y, zz = h.GetX(), h.GetY(), h.GetZ() + r.append(math.hypot(x, y)) + z.append(zz) + # both hit classes carry the deposit under this name + try: + edep.append(h.GetEnergyLoss()) + except AttributeError: + edep.append(float("nan")) + f.Close() + return (np.array(r), np.array(z), np.array(edep), np.array(per_event, dtype=float)) + + +ap = argparse.ArgumentParser() +ap.add_argument("a"); ap.add_argument("b") +ap.add_argument("--file-a", default="o2sim_HitsITS.root") +ap.add_argument("--branch-a", default="ITSHit") +ap.add_argument("--file-b", default="o2sim.root") +ap.add_argument("--branch-b", default="CITSHit") +ap.add_argument("--json"); ap.add_argument("--npz") +args = ap.parse_args() + +ra, za, ea, na = read(args.a, args.file_a, args.branch_a) +rb, zb, eb, nb = read(args.b, args.file_b, args.branch_b) + + +def stat(name, x, y): + """Compare two samples of the same observable.""" + out = {"n_a": int(x.size), "n_b": int(y.size), + "mean_a": float(np.nanmean(x)), "mean_b": float(np.nanmean(y)), + "std_a": float(np.nanstd(x)), "std_b": float(np.nanstd(y))} + lo = min(np.nanmin(x), np.nanmin(y)) + hi = max(np.nanmax(x), np.nanmax(y)) + if hi > lo: + bins = np.linspace(lo, hi, 101) + ha, _ = np.histogram(x[~np.isnan(x)], bins=bins, density=True) + hb, _ = np.histogram(y[~np.isnan(y)], bins=bins, density=True) + w = bins[1] - bins[0] + # total variation distance: 0 = the same distribution, 1 = disjoint + out["totalVariation"] = float(0.5 * w * np.abs(ha - hb).sum()) + print(f" {name:16s} A {out['mean_a']:12.5g} +- {out['std_a']:<11.5g} " + f"B {out['mean_b']:12.5g} +- {out['std_b']:<11.5g} " + f"TV {out.get('totalVariation', float('nan')):.4f}") + return out + + +print(f"hits: A {ra.size} B {rb.size} events: A {na.size} B {nb.size}") +res = {"radius_cm": stat("radius [cm]", ra, rb), + "z_cm": stat("z [cm]", za, zb), + "edep": stat("energy loss", ea, eb), + "hits_per_event": stat("hits/event", na, nb)} +if args.json: + json.dump(res, open(args.json, "w"), indent=2) + print(f"wrote {args.json}") +if args.npz: + np.savez_compressed(args.npz, ra=ra, rb=rb, za=za, zb=zb, ea=ea, eb=eb, na=na, nb=nb) + print(f"wrote {args.npz}") diff --git a/Detectors/CADSupport/validation/closure/compare_hits.py b/Detectors/CADSupport/validation/closure/compare_hits.py new file mode 100644 index 0000000000000..841f91d42b389 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/compare_hits.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Compare the hit positions of two o2-sim runs, hit by hit. + +Hits are keyed by (event, track) and compared in the order the track made them, which +SimCutParams.trackSeed=true makes well defined; the detector id is never used for matching. + +Usage: + compare_hits.py A/ B/ --branch-a ITSHit --branch-b ITSHit \ + --file-a o2sim_HitsITS.root --file-b o2sim.root [--json out.json] +""" + +import argparse +import json +import math +import os +import sys + + +def primary_map(rundir): + """{event: {trackID: primary ordinal}} for tracks that ARE primaries. + + A primary ordinal is shared between two runs; a track index is not. + """ + import ROOT + path = os.path.join(rundir, "o2sim.root") + f = ROOT.TFile.Open(path) + if not f or f.IsZombie(): + raise SystemExit(f"cannot open {path} (needed for MCTrack)") + tree = f.Get("o2sim") + if not tree or not tree.GetBranch("MCTrack"): + raise SystemExit(f"no MCTrack branch in {path}") + out = {} + for iev in range(tree.GetEntries()): + tree.GetEntry(iev) + tracks = getattr(tree, "MCTrack") + m, ordinal = {}, 0 + for i in range(tracks.size()): + if tracks.at(i).getMotherTrackId() < 0: + m[i] = ordinal + ordinal += 1 + out[iev] = m + f.Close() + return out + + +def load_hits(rundir, filename, branch, primaries=None): + """Return {(event, key): [(x, y, z), ...]} in the order the hits appear. + + `key` is the track index, or -- when `primaries` is given -- the primary + ordinal, and hits of secondary tracks are dropped. + """ + import ROOT + + path = os.path.join(rundir, filename) + f = ROOT.TFile.Open(path) + if not f or f.IsZombie(): + raise SystemExit(f"cannot open {path}") + tree = f.Get("o2sim") + if not tree: + raise SystemExit(f"no 'o2sim' tree in {path}") + if not tree.GetBranch(branch): + have = [b.GetName() for b in tree.GetListOfBranches()] + raise SystemExit(f"no branch {branch!r} in {path}; have {have}") + + hits = {} + total = 0 + for iev in range(tree.GetEntries()): + tree.GetEntry(iev) + vec = getattr(tree, branch) + for i in range(vec.size()): + h = vec.at(i) + key = h.GetTrackID() + if primaries is not None: + key = primaries.get(iev, {}).get(key) + if key is None: + continue # a secondary: not a shared identity + hits.setdefault((iev, key), []).append( + (h.GetX(), h.GetY(), h.GetZ())) + total += 1 + f.Close() + return hits, total + + +def compare_nearest(a, b, tol): + """For every hit of A, the nearest hit of B on the same track. + + The native ITS and an external detector define hits differently, so n-th hits do not match. + """ + res = {"hitsMatched": 0, "hitsUnmatched": 0, "withinTolerance": 0, + "maxDr": 0.0, "sumDr": 0.0, "tracksOnlyInA": 0, "worst": None, + "drQuantiles": {}} + drs = [] + for key, ha in sorted(a.items()): + hb = b.get(key) + if not hb: + res["tracksOnlyInA"] += 1 + res["hitsUnmatched"] += len(ha) + continue + for (xa, ya, za) in ha: + best, bestpt = None, None + for (xb, yb, zb) in hb: + d = math.sqrt((xa - xb) ** 2 + (ya - yb) ** 2 + (za - zb) ** 2) + if best is None or d < best: + best, bestpt = d, (xb, yb, zb) + res["hitsMatched"] += 1 + res["sumDr"] += best + drs.append(best) + if best <= tol: + res["withinTolerance"] += 1 + if best > res["maxDr"]: + res["maxDr"] = best + res["worst"] = {"event": key[0], "trackID": key[1], + "a": [xa, ya, za], "b": list(bestpt), "dr": best} + if drs: + drs.sort() + for q in (50, 90, 99): + res["drQuantiles"][f"p{q}"] = drs[min(len(drs) - 1, (q * len(drs)) // 100)] + res["meanDr"] = res["sumDr"] / len(drs) + else: + res["meanDr"] = 0.0 + return res + + +def compare(a, b, tol): + """Compare two keyed hit maps. Returns a result dict.""" + keys_a, keys_b = set(a), set(b) + common = keys_a & keys_b + + res = { + "tracksOnlyInA": len(keys_a - keys_b), + "tracksOnlyInB": len(keys_b - keys_a), + "tracksCommon": len(common), + "tracksWithDifferentHitCount": 0, + "hitsCompared": 0, + "hitsWithinTolerance": 0, + "maxDx": 0.0, "maxDy": 0.0, "maxDz": 0.0, "maxDr": 0.0, + "sumDr": 0.0, + "worst": None, + } + + for key in sorted(common): + ha, hb = a[key], b[key] + if len(ha) != len(hb): + res["tracksWithDifferentHitCount"] += 1 + for (xa, ya, za), (xb, yb, zb) in zip(ha, hb): + dx, dy, dz = abs(xa - xb), abs(ya - yb), abs(za - zb) + dr = math.sqrt(dx * dx + dy * dy + dz * dz) + res["hitsCompared"] += 1 + res["sumDr"] += dr + if dr <= tol: + res["hitsWithinTolerance"] += 1 + res["maxDx"] = max(res["maxDx"], dx) + res["maxDy"] = max(res["maxDy"], dy) + res["maxDz"] = max(res["maxDz"], dz) + if dr > res["maxDr"]: + res["maxDr"] = dr + res["worst"] = { + "event": key[0], "trackID": key[1], + "a": [xa, ya, za], "b": [xb, yb, zb], "dr": dr, + } + + n = res["hitsCompared"] + res["meanDr"] = res["sumDr"] / n if n else 0.0 + return res + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("dir_a") + p.add_argument("dir_b") + p.add_argument("--file-a", default="o2sim_HitsITS.root") + p.add_argument("--file-b", default="o2sim_HitsITS.root") + p.add_argument("--branch-a", default="ITSHit") + p.add_argument("--branch-b", default="ITSHit") + p.add_argument("--tol", type=float, default=0.0, + help="position tolerance in cm; 0 means require exact equality") + p.add_argument("--primaries", action="store_true", + help="key hits by the primary's ordinal in the event and drop " + "hits of secondaries; the only identity two runs share") + p.add_argument("--match", choices=("order", "nearest"), default="order", + help="'order' compares the n-th hit of each track and is the " + "right test between two runs of the same geometry; " + "'nearest' asks whether every hit of A has a counterpart " + "at the same place in B, which is the geometry question " + "when the two sides define a hit differently") + p.add_argument("--json", help="also write the result as JSON here") + args = p.parse_args() + + pa = primary_map(args.dir_a) if args.primaries else None + pb = primary_map(args.dir_b) if args.primaries else None + a, na = load_hits(args.dir_a, args.file_a, args.branch_a, pa) + b, nb = load_hits(args.dir_b, args.file_b, args.branch_b, pb) + + if args.match == "nearest": + res = compare_nearest(a, b, args.tol) + res["hitsInA"], res["hitsInB"], res["tolerance"] = na, nb, args.tol + res["match"] = "nearest" + print(f"A: {args.dir_a}/{args.file_a}:{args.branch_a} {na} hits, {len(a)} tracks") + print(f"B: {args.dir_b}/{args.file_b}:{args.branch_b} {nb} hits, {len(b)} tracks") + print(f"for each hit of A, the nearest on the same track in B:") + print(f" matched {res['hitsMatched']}, " + f"no such track in B {res['hitsUnmatched']} " + f"({res['tracksOnlyInA']} track(s))") + print(f" within {args.tol} cm: {res['withinTolerance']} " + f"({100.0 * res['withinTolerance'] / max(1, res['hitsMatched']):.1f} %)") + print(f" |dr| mean {res['meanDr']:.6g}, median {res['drQuantiles'].get('p50', 0):.6g}, " + f"p90 {res['drQuantiles'].get('p90', 0):.6g}, " + f"p99 {res['drQuantiles'].get('p99', 0):.6g}, max {res['maxDr']:.6g} cm") + if res["worst"]: + w = res["worst"] + print(f" worst: event {w['event']} track {w['trackID']} dr {w['dr']:.4g} cm") + if args.json: + with open(args.json, "w") as fh: + json.dump(res, fh, indent=2) + print(f"wrote {args.json}") + ok = res["hitsMatched"] and res["withinTolerance"] == res["hitsMatched"] + print("VERDICT:", "every hit has a counterpart within tolerance" + if ok else "NOT all hits matched within tolerance") + return 0 if ok else 1 + + res = compare(a, b, args.tol) + res["hitsInA"] = na + res["hitsInB"] = nb + res["tolerance"] = args.tol + + print(f"A: {args.dir_a}/{args.file_a}:{args.branch_a} {na} hits, {len(a)} tracks") + print(f"B: {args.dir_b}/{args.file_b}:{args.branch_b} {nb} hits, {len(b)} tracks") + print(f"tracks: {res['tracksCommon']} common, " + f"{res['tracksOnlyInA']} only in A, {res['tracksOnlyInB']} only in B, " + f"{res['tracksWithDifferentHitCount']} with a different hit count") + print(f"hits compared: {res['hitsCompared']}, " + f"within {args.tol} cm: {res['hitsWithinTolerance']}") + print(f"max |dx| {res['maxDx']:.6g} max |dy| {res['maxDy']:.6g} " + f"max |dz| {res['maxDz']:.6g} cm") + print(f"max |dr| {res['maxDr']:.6g} cm, mean |dr| {res['meanDr']:.6g} cm") + if res["worst"]: + w = res["worst"] + print(f"worst: event {w['event']} track {w['trackID']} " + f"A={w['a']} B={w['b']}") + + if args.json: + with open(args.json, "w") as fh: + json.dump(res, fh, indent=2) + print(f"wrote {args.json}") + + identical = (res["hitsInA"] == res["hitsInB"] + and res["tracksOnlyInA"] == 0 and res["tracksOnlyInB"] == 0 + and res["tracksWithDifferentHitCount"] == 0 + and res["hitsCompared"] == res["hitsWithinTolerance"]) + print("VERDICT:", "identical within tolerance" if identical else "DIFFERENT") + return 0 if identical else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/make_configs.py b/Detectors/CADSupport/validation/closure/make_configs.py new file mode 100644 index 0000000000000..f6c06ebeef4e4 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/make_configs.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Write the o2-sim configuration that runs the round-tripped geometry. + +PIPE, TPC and MAG go in as o2::passive::ExternalModule and ITS as o2::ext::ExternalDetector on the +ITS DetID slot. Each piece is anchored where roundtrip_module.py found it. The external names +are not the real module names, so the native modules are not built as well. + +Usage: + make_configs.py [--name CADCLOSURE] +""" + +import argparse +import json +import os +import sys + +# module -> (external name, kind); ITS is the only sensitive one. +MODULES = [ + ("PIPE", "CPIPE", "passive", "CAD round-tripped beam pipe"), + ("TPC", "CTPC", "passive", "CAD round-tripped TPC (material only)"), + ("MAG", "CMAG", "passive", "CAD round-tripped L3 magnet"), + ("ITS", "CITS", "sensitive", "CAD round-tripped ITS"), +] + +SENSITIVE_VOLUMES = {"CITS": ["ITSUSensor"]} # substring match: ITSUSensor0..6 +DET_ID = {"CITS": "ITS"} + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("studydir") + p.add_argument("--name", default="CADCLOSURE", + help="the detector-list key o2-sim is pointed at") + p.add_argument("--variant", default="csg", choices=("csg", "mesh"), + help="which back-conversion to configure: the shipped cascade " + "(csg) or tessellated-only (mesh), the fallback every other " + "CAD pipeline uses and the benchmark for the exact path") + p.add_argument("--out-prefix", default="", + help="prefix for the written JSON file names, so two variants " + "can live side by side in one study directory") + args = p.parse_args() + + study = os.path.abspath(args.studydir) + modules, detectors, names, missing = [], [], [], [] + + for mod, name, kind, title in MODULES: + frag = os.path.join(study, "cad", mod, "module_entries.json") + if not os.path.exists(frag): + missing.append(frag) + continue + # One external module or detector per placement, named or _. + frag_entries = json.load(open(frag))["entries"] + if isinstance(frag_entries, dict): # variants + if args.variant not in frag_entries: + missing.append(f"{frag} (no '{args.variant}' variant)") + continue + frag_entries = frag_entries[args.variant] + for e in frag_entries: + suffix = "" if e["tag"] == "barrel" else "_" + e["tag"][:8].upper() + ename = (name + suffix)[:15] + entry = {"name": ename, "title": f"{title} [{e['tag']}]", + "macro": e["macro"], "anchor": e["anchor"]} + if e.get("placement"): + entry["placement"] = e["placement"] + if kind == "sensitive": + entry["detID"] = DET_ID[name] + entry["sensitiveVolumes"] = SENSITIVE_VOLUMES[name] + detectors.append(entry) + else: + modules.append(entry) + names.append(ename) + + if missing: + raise SystemExit("no module_entries.json for:\n " + "\n ".join(missing) + + "\n(run roundtrip_module.py for each module first)") + + pre = args.out_prefix + ext_path = os.path.join(study, f"{pre}externalDetectors.json") + det_path = os.path.join(study, f"{pre}detectorlist.json") + with open(ext_path, "w") as fh: + json.dump({"externalModules": modules, "externalDetectors": detectors}, + fh, indent=2) + with open(det_path, "w") as fh: + json.dump({args.name: names}, fh, indent=2) + + print(f"wrote {ext_path}") + print(f" {len(modules)} passive external module(s): " + f"{', '.join(e['name'] for e in modules)}") + print(f" {len(detectors)} sensitive external detector(s): " + + ", ".join(f"{e['name']} on DetID {e['detID']} " + f"(sensitive: {', '.join(e['sensitiveVolumes'])})" + for e in detectors)) + print(f"wrote {det_path}: {args.name} = {names}") + print() + print("run it with:") + print(f" o2-sim-serial -n -g boxgen \\") + print(f" --detectorList {args.name}:{det_path} \\") + print(f" --extGeomFile {ext_path} \\") + print(f" --seed --configKeyValues 'SimCutParams.trackSeed=true'") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/matbudget_diff.py b/Detectors/CADSupport/validation/closure/matbudget_diff.py new file mode 100644 index 0000000000000..af12d7ca95553 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/matbudget_diff.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Compare the material two geometries present to the same rays. + +x/X0 and x/lambda are integrated along a fixed set of Fibonacci-sphere rays through both +geometries. + +Usage: + matbudget_diff.py A/o2sim_geometry.root B/o2sim_geometry.root \ + --rays 2000 --rmax 45 [--json out.json] +""" + +import argparse +import json +import math +import sys + + +def directions(n): + """n roughly-uniform directions on the sphere (Fibonacci).""" + ga = math.pi * (3.0 - math.sqrt(5.0)) + out = [] + for i in range(n): + z = 1.0 - (2.0 * i + 1.0) / n + r = math.sqrt(max(0.0, 1.0 - z * z)) + phi = ga * i + out.append((r * math.cos(phi), r * math.sin(phi), z)) + return out + + +def integrate(mgr, dirs, rmax, origin=(0.0, 0.0, 0.0)): + """Per ray: (sum x/X0, sum x/lambda, number of volumes crossed).""" + import ROOT + ROOT.gGeoManager = mgr + out = [] + for (dx, dy, dz) in dirs: + mgr.InitTrack(origin[0], origin[1], origin[2], dx, dy, dz) + x0 = lam = 0.0 + ncross = 0 + travelled = 0.0 + while not mgr.IsOutside() and travelled < rmax and ncross < 20000: + node = mgr.GetCurrentNode() + if node is None: + break + med = node.GetVolume().GetMedium() + mgr.FindNextBoundary() + step = mgr.GetStep() + if travelled + step > rmax: + step = rmax - travelled + if med is not None and step > 0: + mat = med.GetMaterial() + rl, il = mat.GetRadLen(), mat.GetIntLen() + if rl > 0: + x0 += step / rl + if il > 0: + lam += step / il + travelled += step + ncross += 1 + mgr.Step() + if mgr.GetStep() <= 0 and step <= 0: + break + out.append((x0, lam, ncross)) + return out + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("geometry_a") + p.add_argument("geometry_b") + p.add_argument("--rays", type=int, default=2000) + p.add_argument("--rmax", type=float, default=45.0, + help="integrate out to this distance from the origin, in cm") + p.add_argument("--json") + p.add_argument("--dump-rays", metavar="CSV", + help="per-ray x/X0 and x/lambda for both geometries, so the " + "distribution can be plotted rather than summarised away") + args = p.parse_args() + + import ROOT + ROOT.gROOT.SetBatch(True) + dirs = directions(args.rays) + + def load(path): + f = ROOT.TFile.Open(path) + key = f.GetListOfKeys().At(0).GetName() + return f, f.Get(key) + + fa, ma = load(args.geometry_a) + ra = integrate(ma, dirs, args.rmax) + fa.Close() + fb, mb = load(args.geometry_b) + rb = integrate(mb, dirs, args.rmax) + fb.Close() + + diffs_x0, diffs_l, rel = [], [], [] + suma = sumb = 0.0 + for (xa, la, na), (xb, lb, nb) in zip(ra, rb): + suma += xa + sumb += xb + diffs_x0.append(abs(xa - xb)) + diffs_l.append(abs(la - lb)) + if max(xa, xb) > 0: + rel.append(abs(xa - xb) / max(xa, xb)) + + n = len(ra) + diffs_x0.sort(); rel.sort() + res = { + "rays": n, "rmax_cm": args.rmax, + "meanX0_a": suma / n, "meanX0_b": sumb / n, + "meanAbsDiffX0": sum(diffs_x0) / n, + "maxAbsDiffX0": diffs_x0[-1], + "medianRelDiff": rel[len(rel) // 2] if rel else 0.0, + "p99RelDiff": rel[min(len(rel) - 1, (99 * len(rel)) // 100)] if rel else 0.0, + "maxRelDiff": rel[-1] if rel else 0.0, + "raysAbove1pct": sum(1 for r in rel if r > 0.01), + "raysAbove10pct": sum(1 for r in rel if r > 0.10), + "meanAbsDiffLambda": sum(diffs_l) / n, + "meanCrossings_a": sum(x[2] for x in ra) / n, + "meanCrossings_b": sum(x[2] for x in rb) / n, + } + + if args.dump_rays: + with open(args.dump_rays, "w") as fh: + fh.write("ux,uy,uz,x0_a,x0_b,lambda_a,lambda_b,crossings_a,crossings_b\n") + for u, (xa, la, na), (xb, lb, nb) in zip(dirs, ra, rb): + # 17 significant digits, so a double round-trips exactly. + fh.write(f"{u[0]:.9g},{u[1]:.9g},{u[2]:.9g},{xa:.17g},{xb:.17g}," + f"{la:.17g},{lb:.17g},{na},{nb}\n") + print(f"wrote {args.dump_rays}") + + print(f"{n} Fibonacci rays from the origin, integrated to r = {args.rmax} cm") + print(f" mean x/X0 A {res['meanX0_a']:.6f} B {res['meanX0_b']:.6f} " + f"({100.0 * (res['meanX0_b'] - res['meanX0_a']) / max(1e-30, res['meanX0_a']):+.3f} %)") + print(f" mean |diff| x/X0 {res['meanAbsDiffX0']:.3e} max {res['maxAbsDiffX0']:.3e}") + print(f" relative per ray: median {res['medianRelDiff']:.3e}, " + f"p99 {res['p99RelDiff']:.3e}, max {res['maxRelDiff']:.3e}") + print(f" rays differing by >1 %: {res['raysAbove1pct']} / {n}; " + f">10 %: {res['raysAbove10pct']} / {n}") + print(f" mean |diff| x/lambda {res['meanAbsDiffLambda']:.3e}") + print(f" mean volumes crossed A {res['meanCrossings_a']:.1f} " + f"B {res['meanCrossings_b']:.1f}") + + if args.json: + with open(args.json, "w") as fh: + json.dump(res, fh, indent=2) + print(f"wrote {args.json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/measure_facet_error.py b/Detectors/CADSupport/validation/closure/measure_facet_error.py new file mode 100644 index 0000000000000..f3a2a44ffe79d --- /dev/null +++ b/Detectors/CADSupport/validation/closure/measure_facet_error.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-09 + +"""How far the tessellation moves a cylindrical surface, measured rather than estimated. + +The error is the sagitta of the chord between neighbouring lateral-wall vertices, + + sagitta = R (1 - cos(dphi / 2)) + +compared with the exact radius of every leaf the cascade recognised as a TGeoTube / TGeoTubeSeg. +""" +import glob +import math +import os +import struct +import sys +import numpy as np +import ROOT + +CSG, MESH = sys.argv[1], sys.argv[2] + + +def read_vertices(path): + with open(path, "rb") as fh: + n = struct.unpack(" 1e-5] + if not len(gaps): + continue + dphi = float(np.median(gaps)) + rows.append((key, rmax, len(phi), dphi, rmax * (1.0 - math.cos(dphi / 2.0)))) + +rows.sort(key=lambda t: -t[4]) +print(f"{'part':<44}{'R (cm)':>9}{'segments':>10}{'sagitta':>12}") +for key, rmax, nphi, dphi, sag in rows[:10]: + print(f"{key[:44]:<44}{rmax:9.3f}{nphi:10d}{sag * 1e4:9.1f} um") +if rows: + sag = np.array([r[4] for r in rows]) * 1e4 + R = np.array([r[1] for r in rows]) + print(f"\n{len(rows)} cylindrical parts, R = {R.min():.2f}-{R.max():.2f} cm") + print(f" surface displacement: median {np.median(sag):.0f} um, " + f"p90 {np.percentile(sag, 90):.0f} um, max {sag.max():.0f} um") diff --git a/Detectors/CADSupport/validation/closure/module_anchors.py b/Detectors/CADSupport/validation/closure/module_anchors.py new file mode 100644 index 0000000000000..0ca1320c366c2 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/module_anchors.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Find where a module hangs itself in the ALICE world, and with what matrix. + +o2-sim always builds `cave`, `barrel` (at y = -30 in cave) and `caveRB24`; this reports the +module's own subtree roots under them, which the closure test converts and anchors separately. + +Usage: + module_anchors.py o2sim_geometry.root [--json anchors.json] +""" + +import argparse +import json +import sys + +HALL = ("cave", "barrel", "caveRB24") + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("geometry") + p.add_argument("--json") + args = p.parse_args() + + import ROOT + ROOT.gROOT.SetBatch(True) + mgr = ROOT.TGeoManager.Import(args.geometry) + if not mgr: + raise SystemExit(f"cannot read {args.geometry}") + + roots = [] + for hall in HALL: + vol = mgr.GetVolume(hall) + if not vol: + continue + for i in range(vol.GetNdaughters()): + node = vol.GetNode(i) + child = node.GetVolume() + if str(child.GetName()) in HALL: + continue + m = node.GetMatrix() + t = [m.GetTranslation()[k] for k in range(3)] + r = [m.GetRotationMatrix()[k] for k in range(9)] + box = child.GetShape() + roots.append({ + "anchor": hall, + "volume": str(child.GetName()), + "node": str(node.GetName()), + "copy": int(node.GetNumber()), + "shape": str(box.ClassName()), + "isAssembly": bool(child.IsAssembly()), + "nDaughters": int(child.GetNdaughters()), + "translation": t, + "rotation": r, + "isIdentity": bool(m.IsIdentity()), + }) + + print(f"{args.geometry}: {mgr.GetListOfVolumes().GetEntries()} volumes, " + f"{len(roots)} subtree root(s) outside the hall") + for r in roots: + rot = "identity" if r["isIdentity"] else f"rotation {['%.6g' % x for x in r['rotation']]}" + print(f" {r['volume']:24s} in {r['anchor']:9s} copy {r['copy']:<4d} " + f"{r['shape']:22s} nd={r['nDaughters']:<5d} " + f"t={['%.6g' % x for x in r['translation']]} {rot}") + + if args.json: + with open(args.json, "w") as fh: + json.dump({"geometry": args.geometry, "roots": roots}, fh, indent=2) + print(f"wrote {args.json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/remap_cuts.py b/Detectors/CADSupport/validation/closure/remap_cuts.py new file mode 100644 index 0000000000000..fa80c2610cd74 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/remap_cuts.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Carry the baseline's Geant cuts and processes over to the CAD run, by medium name. + +A loaded cut is resolved by (module, local index), which the CAD run does not share with the +baseline; the medium NAME survives the round trip. So the CAD module prefix is stripped, names are +matched, and the baseline's cuts are written under the CAD run's module and local index: + + baseline module ITS, local 1, medium `ITS_AIR$` + CAD run module CITS, local 7, medium `CITS_ITS_AIR$` + + remap_cuts.py --baseline base.json --cad-dump cad_out.json --out cad_in.json + remap_cuts.py --compare base.json cad_out2.json +""" + +import argparse +import json +import sys + + +def index_baseline(doc): + """medium name (unprefixed by module) -> its cuts/processes record.""" + out = {} + for key, entries in doc.items(): + if not isinstance(entries, list): + continue + for e in entries: + name = e.get("medium_name") + if not name: + continue + # `ITS_AIR$` under module `ITS` -> key on both the full name and the + # part after the module prefix, so either spelling matches later. + out.setdefault(name, e) + if name.startswith(key + "_"): + out.setdefault(name[len(key) + 1:], e) + return out + + +def strip_module(name, module): + return name[len(module) + 1:] if name.startswith(module + "_") else name + + +def build(baseline, caddump): + by_name = index_baseline(baseline) + out, matched, unmatched = {}, [], [] + for key, entries in caddump.items(): + if not isinstance(entries, list): + out[key] = entries # default / enableSpecial* pass through + continue + rebuilt = [] + for e in entries: + bare = strip_module(e.get("medium_name", ""), key) + src = by_name.get(bare) or by_name.get(e.get("medium_name", "")) + if src is None: + unmatched.append(f"{key}/{e.get('medium_name')}") + continue + rebuilt.append({ + "local_id": e["local_id"], + "global_id": e["global_id"], + "medium_name": e["medium_name"], + "material_name": e.get("material_name"), + "cuts": src.get("cuts", {}), + "processes": src.get("processes", {}), + }) + matched.append(f"{key}/{e.get('medium_name')} <- {bare}") + out[key] = rebuilt + for k in ("default", "enableSpecialCuts", "enableSpecialProcesses"): + if k in baseline: + out[k] = baseline[k] + return out, matched, unmatched + + +def compare(baseline, caddump): + """Do the two runs give every medium the same cuts and processes?""" + by_name = index_baseline(baseline) + same, differ, missing = 0, [], [] + for key, entries in caddump.items(): + if not isinstance(entries, list): + continue + for e in entries: + bare = strip_module(e.get("medium_name", ""), key) + src = by_name.get(bare) or by_name.get(e.get("medium_name", "")) + if src is None: + missing.append(f"{key}/{e.get('medium_name')}") + continue + if (src.get("cuts") == e.get("cuts") + and src.get("processes") == e.get("processes")): + same += 1 + else: + bad = [k for k in set(src.get("cuts", {})) | set(e.get("cuts", {})) + if src.get("cuts", {}).get(k) != e.get("cuts", {}).get(k)] + bad += [f"proc:{k}" for k in + set(src.get("processes", {})) | set(e.get("processes", {})) + if src.get("processes", {}).get(k) != e.get("processes", {}).get(k)] + differ.append((f"{key}/{e.get('medium_name')}", bad)) + return same, differ, missing + + +def main(): + p = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + p.add_argument("--baseline", required=True) + p.add_argument("--cad-dump", required=True) + p.add_argument("--out") + p.add_argument("--compare", action="store_true") + args = p.parse_args() + + baseline = json.load(open(args.baseline)) + caddump = json.load(open(args.cad_dump)) + + if args.compare: + same, differ, missing = compare(baseline, caddump) + print(f"media compared: {same + len(differ) + len(missing)}") + print(f" identical cuts and processes: {same}") + print(f" differing: {len(differ)}") + for name, bad in differ[:8]: + print(f" {name}: {bad[:6]}") + print(f" no baseline medium of that name: {len(missing)}" + + (f" e.g. {missing[:5]}" if missing else "")) + ok = not differ and not missing + print("VERDICT:", "both runs give every medium the same cuts and processes" + if ok else "DIFFERENT -- do not trust a transport comparison") + return 0 if ok else 1 + + if not args.out: + raise SystemExit("--out is required unless --compare is given") + out, matched, unmatched = build(baseline, caddump) + with open(args.out, "w") as fh: + json.dump(out, fh, indent=1) + print(f"wrote {args.out}: {len(matched)} medium/media matched by name, " + f"{len(unmatched)} unmatched") + if unmatched: + print(f" [WARN] no baseline cuts for: {unmatched[:8]}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/roundtrip_module.py b/Detectors/CADSupport/validation/closure/roundtrip_module.py new file mode 100644 index 0000000000000..237f3354aa23a --- /dev/null +++ b/Detectors/CADSupport/validation/closure/roundtrip_module.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""One module through the round trip, anchored where the source geometry put it. + + roundtrip_module.py [csg,mesh] + +Each module is converted per hall anchor: + + * everything under `barrel` becomes one conversion with `--top barrel`, the hall volume + hollowed, placed back into the real `barrel` with the identity; + * anything under `cave` or `caveRB24` is converted from its own subtree root + and placed with that root's own matrix. + +Writes /cad// and a module_entries.json fragment that +make_configs.py assembles into the o2-sim external-geometry file. +""" + +import json +import math +import os +import shlex +import subprocess +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import cadsupport_path # noqa: E402,F401 (puts ../tools on sys.path) +from cadsupport import occ_env # noqa: E402 + +HALL = ("cave", "barrel", "caveRB24") + + +def sh(cmd, cwd, log): + """Run one step in its own shell, with its output kept in a log.""" + with open(os.path.join(cwd, log), "w") as fh: + r = subprocess.run(["bash", "-c", cmd], cwd=cwd, stdout=fh, + stderr=subprocess.STDOUT) + if r.returncode != 0: + print(open(os.path.join(cwd, log)).read()[-3000:]) + raise SystemExit(f"step failed ({r.returncode}): {cmd[:120]}") + + +def euler_deg(rot, env_o2): + """The rotation_deg triple ExternalModule's JSON wants, verified by rebuilding it in ROOT.""" + if all(abs(rot[i] - (1.0 if i in (0, 4, 8) else 0.0)) < 1e-12 for i in range(9)): + return None # identity: omit the rotation entirely + # ROOT lives in the o2 environment, so the candidate is rebuilt there. + probe = f""" +import ROOT, json, itertools, sys +target = {list(rot)!r} +for cand in itertools.product((0,90,-90,180),repeat=3): + c = ROOT.TGeoCombiTrans() + c.RotateX(cand[0]); c.RotateY(cand[1]); c.RotateZ(cand[2]) + m = c.GetRotationMatrix() + if all(abs(m[i]-target[i]) < 1e-9 for i in range(9)): + print(json.dumps(list(cand))); sys.exit(0) +sys.exit(3) +""" + r = subprocess.run(["bash", "-c", f'{env_o2}; python3 -c {shlex.quote(probe)}'], + capture_output=True, text=True) + if r.returncode != 0: + raise SystemExit(f"cannot express this rotation as rotation_deg: {rot}\n" + "ExternalModule's JSON carries Euler angles only; this " + "placement needs a full matrix and the loader does not " + "take one yet.") + return json.loads(r.stdout.strip().splitlines()[-1]) + + +def main(): + if len(sys.argv) not in (3, 4): + raise SystemExit(__doc__) + study, mod = os.path.abspath(sys.argv[1]), sys.argv[2] + d = os.path.join(study, "cad", mod) + os.makedirs(d, exist_ok=True) + ct = os.path.dirname(os.path.abspath(__file__)) + env_o2 = f'source "{study}/env_o2.sh" >/dev/null 2>&1' + env_cv = f'{env_o2}; source "{study}/env_converter.sh"' + occ_python = occ_env.occ_python() + if occ_python is None: + raise SystemExit(occ_env.UNRESOLVED) + py = shlex.quote(str(occ_python)) + + print(f"=== {mod}: the source geometry") + sh(f'{env_o2}; o2-sim-serial -n 0 -g boxgen -m {mod} -o o2sim', d, "geom.log") + + print(f"=== {mod}: where does it hang itself?") + sh(f'{env_o2}; python3 "{ct}/module_anchors.py" o2sim_geometry.root ' + f'--json anchors.json', d, "anchors.log") + roots = json.load(open(os.path.join(d, "anchors.json")))["roots"] + in_barrel = [r for r in roots if r["anchor"] == "barrel"] + elsewhere = [r for r in roots if r["anchor"] != "barrel"] + print(f" {len(in_barrel)} subtree(s) under barrel, " + f"{len(elsewhere)} elsewhere: " + f"{[(r['volume'], r['anchor']) for r in elsewhere]}") + + entries = {} + variants = sys.argv[3].split(",") if len(sys.argv) > 3 else ["csg", "mesh"] + + def convert(tag, top, hollow, anchor, placement, variant="csg"): + """One --top conversion plus its media sidecar, scored. + + `variant` "csg" is the shipped cascade; "mesh" is tessellated-only, as a benchmark. + """ + out = f"conv_{tag}" if variant == "csg" else f"conv_{variant}_{tag}" + cascade = ("--csg auto --exact-surfaces auto --mesh" if variant == "csg" + else "--mesh") + hollow_args = " ".join(f"--hollow-volume {h}" for h in hollow) + tagarg = f'--hollow-tag {mod}' if hollow else "" + print(f"=== {mod}: --top {top} -> anchor {anchor}") + # No --carve-mothers: the converter restores the nesting from the sidecar, and carving + # cannot subtract an assembly daughter. + sh(f'{env_cv}; {py} "{ct}/../../tools/O2_TGeoToCAD.py" o2sim_geometry.root {tag}.step ' + f'--top {top} --report {tag}_writer_report.json ' + f'--media-json {tag}_media.json {hollow_args} {tagarg}', + d, f"writer_{tag}.log") + sh(f'{env_cv}; {py} "{ct}/../../tools/O2_CADtoTGeo.py" {tag}.step -o geom.C ' + f'--output-folder {out} {cascade} ' + f'--media-json {tag}_media.json', d, f"{out}.log") + for line in open(os.path.join(d, f"{out}.log")): + if "tiers:" in line or "Media from sidecar" in line or "[WARN]" in line: + print(" ", line.rstrip()) + sh(f'{env_o2}; python3 "{ct}/check_media.py" --original o2sim_geometry.root ' + f'--macro {out}/geom.C --rtol 1e-6 ' + f'--writer-report {tag}_writer_report.json --json media_{out}.json', + d, f"media_{out}.log") + for line in open(os.path.join(d, f"media_{out}.log")): + if line.startswith(("VERDICT", " media identical", " left on")): + print(" ", line.rstrip()) + e = {"tag": tag, "macro": os.path.join(d, out, "geom.C"), "anchor": anchor} + if placement: + e["placement"] = placement + entries.setdefault(variant, []).append(e) + + # The STEP is written once per anchor; only the back-conversion differs per variant. + for v in variants: + if in_barrel: + convert("barrel", "barrel", ["barrel"], "barrel", None, v) + for r in elsewhere: + rot = euler_deg(r["rotation"], env_o2) + pl = {"translation": [float(x) for x in r["translation"]]} + if rot: + pl["rotation_deg"] = rot + convert(r["volume"], r["volume"], [], r["anchor"], pl, v) + + with open(os.path.join(d, "module_entries.json"), "w") as fh: + json.dump({"module": mod, "entries": entries}, fh, indent=2) + print(f"=== {mod}: " + ", ".join(f"{len(v)} {k} placement(s)" + for k, v in entries.items()) + + f" -> {d}/module_entries.json") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/closure/roundtrip_module.sh b/Detectors/CADSupport/validation/closure/roundtrip_module.sh new file mode 100755 index 0000000000000..63c5355a86395 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/roundtrip_module.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# One module through the whole round trip: TGeo -> STEP (+ media sidecar) -> TGeo. +# +# roundtrip_module.sh +# +# Writes /cad// with o2sim_geometry.root, .step, +# _media.json and conv/geom.C. The o2-sim step and the converter steps +# run in SEPARATE shells on purpose: the pythonOCC PYTHONPATH prepends segfault +# o2-sim at startup. Conversions must not be run in parallel -- --csg auto +# defers its emit and two concurrent runs lose shapes. +set -euo pipefail +HERE=$(cd "$(dirname "$0")" && pwd) +TOOLS=$(cd "$HERE/../../tools" && pwd) + +S="$1"; MOD="$2" +D="$S/cad/$MOD" +mkdir -p "$D" + +echo "=== $MOD: building the source geometry" +( source "$S/env_o2.sh" >/dev/null 2>&1 + cd "$D" && o2-sim-serial -n 0 -g boxgen -m "$MOD" -o o2sim > geom.log 2>&1 ) + +# The experiment hall is hollowed out: o2-sim builds cave/barrel/caveRB24 itself +# whatever module list is asked for, so shipping a second copy would put four +# coincident air boxes in the world. Their structure is kept, so every subtree +# below them still lands at exactly the transform the source geometry gave it. +echo "=== $MOD: TGeo -> STEP + media sidecar" +( source "$S/env_o2.sh" >/dev/null 2>&1 + source "$S/env_converter.sh" + cd "$D" && "$SW/Python/latest/bin/python3.10" \ + "$TOOLS/O2_TGeoToCAD.py" o2sim_geometry.root "$MOD.step" \ + --report "${MOD}_writer_report.json" --media-json "${MOD}_media.json" \ + --hollow-volume cave --hollow-volume barrel --hollow-volume caveRB24 \ + --hollow-tag "$MOD" \ + > writer.log 2>&1 ) +tail -3 "$D/writer.log" + +echo "=== $MOD: STEP -> TGeo (csg auto / exact surfaces auto / mesh fallback)" +( source "$S/env_o2.sh" >/dev/null 2>&1 + source "$S/env_converter.sh" + cd "$D" && "$SW/Python/latest/bin/python3.10" \ + "$TOOLS/O2_CADtoTGeo.py" "$MOD.step" -o geom.C \ + --output-folder conv --csg auto --exact-surfaces auto --mesh \ + --media-json "${MOD}_media.json" > conv.log 2>&1 ) +grep -E "tiers:|Media from sidecar|WARN" "$D/conv.log" || true + +echo "=== $MOD: where does this module hang itself?" +( source "$S/env_o2.sh" >/dev/null 2>&1 + cd "$D" && python3 "$HERE/module_anchors.py" \ + o2sim_geometry.root --json anchors.json 2>&1 | grep -vE "^Info in|^Warning in" ) + +echo "=== $MOD: do the media survive?" +( source "$S/env_o2.sh" >/dev/null 2>&1 + cd "$D" && python3 "$HERE/check_media.py" \ + --original o2sim_geometry.root --macro conv/geom.C --rtol 1e-6 --writer-report "${MOD}_writer_report.json" \ + --json media_check.json 2>&1 | grep -vE "^Info in|^Warning in|^Note:" ) diff --git a/Detectors/CADSupport/validation/closure/run_closure.sh b/Detectors/CADSupport/validation/closure/run_closure.sh new file mode 100755 index 0000000000000..cc3852cf8e272 --- /dev/null +++ b/Detectors/CADSupport/validation/closure/run_closure.sh @@ -0,0 +1,108 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# The closure test: the same events through the hand-written C++ TGeo geometry +# and through its own STEP round trip, compared on ITS hit positions. +# +# run_closure.sh [nevents] [seed] +# +# Order matters and each step gates the next: +# +# 0. the baseline twice with one seed -- if those are not bit-identical the +# per-track seeding is not doing what the comparison assumes and nothing +# below means anything; +# 1. a CAD probe run, only to learn which local medium index the CAD side gives +# each medium; MaterialManager resolves a loaded cut by (module, local index) +# and skips a mismatch SILENTLY, so the mapping has to be built, not assumed; +# 2. the baseline's cuts and processes are carried over by medium NAME and the +# CAD run is repeated with them loaded, dumping its own; +# 3. the two dumps are compared per medium -- a difference means the physics +# configuration differs and the transport comparison must not be believed; +# 4. only then the hits. Note where they are: under o2-sim-serial an external +# detector's hits stay in o2sim.root on a branch named after the detector +# (CITSHit), rather than being split into o2sim_Hits.root the way a +# built-in detector's are. +# +# Bit-identical hits are NOT the acceptance for charged particles in material: +# Geant draws from the RNG per step, so one extra boundary crossing shifts every +# later draw of that track. Per-track seeding contains that to the track; it does +# not remove it. So the hit comparison reports a distribution, and the numbers to +# read are how many tracks survive with the same hit count and how far the rest +# moved. +set -euo pipefail + +S="$1"; N="${2:-20}"; SEED="${3:-424242}" +CT=$(cd "$(dirname "$0")" && pwd) +R="$S/run" +mkdir -p "$R" + +cadrun () { # cadrun + mkdir -p "$R/$1" + ( source "$S/env_o2.sh" >/dev/null 2>&1 + cd "$R/$1" && o2-sim-serial -n "$N" -g boxgen --seed "$SEED" \ + --detectorList "CADCLOSURE:$S/detectorlist.json" \ + --extGeomFile "$S/externalDetectors.json" \ + --configKeyValues "SimCutParams.trackSeed=true${2:-}" \ + -o o2sim > run.log 2>&1 ) +} + +echo "############ 0. determinism control: the baseline twice, one seed" +for r in base1 base2; do + mkdir -p "$R/$r" + ( source "$S/env_o2.sh" >/dev/null 2>&1 + cd "$R/$r" && o2-sim-serial -n "$N" -g boxgen -m PIPE ITS TPC MAG --seed "$SEED" \ + --configKeyValues "SimCutParams.trackSeed=true;MaterialManagerParam.outputFile=$R/cuts_baseline.json" \ + -o o2sim > run.log 2>&1 ) +done +( source "$S/env_o2.sh" >/dev/null 2>&1 + python3 "$CT/compare_hits.py" "$R/base1" "$R/base2" --json "$R/determinism.json" ) + +echo +echo "############ 1. CAD probe run, to learn its own medium indices" +cadrun cad_probe ";MaterialManagerParam.outputFile=$R/cuts_cad_probe.json" + +echo +echo "############ 2. carry the baseline's cuts over by medium name" +python3 "$CT/remap_cuts.py" --baseline "$R/cuts_baseline.json" \ + --cad-dump "$R/cuts_cad_probe.json" --out "$R/cuts_cad_in.json" + +echo +echo "############ 3. the CAD run, with those cuts loaded" +cadrun cad ";MaterialManagerParam.inputFile=$R/cuts_cad_in.json;MaterialManagerParam.outputFile=$R/cuts_cad_out.json" +echo "robustness counters (all must be zero):" +for pat in "stuck" "G4Exception" "Navigation Error" "abort"; do + printf " %-16s baseline %-5s CAD %-5s\n" "$pat" \ + "$(grep -ic "$pat" "$R/base1/run.log" || true)" \ + "$(grep -ic "$pat" "$R/cad/run.log" || true)" +done +echo "transport size (they should be comparable, not equal):" +for d in base1 cad; do + printf " %-6s steps/event %-8s secondaries/event %s\n" "$d" \ + "$(grep -oP 'did \K[0-9]+(?= steps)' "$R/$d/run.log" | awk '{s+=$1;n++} END{if(n)printf "%.0f",s/n}')" \ + "$(grep -oP 'Stack: [0-9]+ out of \K[0-9]+' "$R/$d/run.log" | awk '{s+=$1;n++} END{if(n)printf "%.0f",s/n}')" +done + +echo +echo "############ 4. did both sides get the same cuts and processes?" +python3 "$CT/remap_cuts.py" --compare --baseline "$R/cuts_baseline.json" \ + --cad-dump "$R/cuts_cad_out.json" || true + +echo +echo "############ 5. the hits" +( source "$S/env_o2.sh" >/dev/null 2>&1 + python3 "$CT/compare_hits.py" "$R/base1" "$R/cad" \ + --file-a o2sim_HitsITS.root --branch-a ITSHit \ + --file-b o2sim.root --branch-b CITSHit \ + --tol 1e-4 --json "$R/hits.json" ) || true diff --git a/Detectors/CADSupport/validation/compareGateRuns.py b/Detectors/CADSupport/validation/compareGateRuns.py new file mode 100644 index 0000000000000..6a6b95d67d3f2 --- /dev/null +++ b/Detectors/CADSupport/validation/compareGateRuns.py @@ -0,0 +1,267 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Diff two gate.json reports column by column, with the scale law of each column applied. + +Every real field carries a length exponent in `_FIELD_EXPONENT`; the expectation is scaled by +`factor ** exponent` and the residual is reported. Integer columns are compared for equality, real +columns within a stated relative band, a floor on double arithmetic rather than a tolerance on the +geometry. Fields absent from gate.json and points the harness never sampled are out of reach. + +Usage +----- + compareGateRuns.py --baseline base/gate.json --candidate z400/gate.json --label "z+400 cm" + compareGateRuns.py --baseline base/gate.json --candidate x10/gate.json --scale 10 + compareGateRuns.py --baseline base/gate.json --self-test +""" + +import argparse +import copy +import json +import sys +from pathlib import Path + +# Timing and point-derived checksums are not compared; `Seconds` is matched as a substring. +_IGNORED_SUBSTRINGS = ("Seconds", "nsPerCall", "checksum") +_IGNORED_KEYS = {"id", "model", "worstOffenders", "rimDetail", + "timingCandidate", "timingReference", "timingCandidateLoop", "timingPruned", + "timingUnpruned"} + +# The mesh columns move under a scaling on purpose; --gate-columns-only excludes them. +_MESH_DERIVED_PREFIXES = ("contains.", "distout.", "distin.", "safety.") +_MESH_DERIVED_KEYS = {"nTriangles"} + +# The exponent of the length scale factor each real-valued field carries. Anything not listed is +# treated as dimensionless (exponent 0) -- counts, fractions, relative deviations, booleans. +_FIELD_EXPONENT = { + # lengths, cm + "maxRimIsolation": 1, + "rimChordResolution": 1, + "rimMatchTolerance": 1, + "totalRimLength": 1, + "unmatchedRimLength": 1, + "maxSharedEdgeDeviation": 1, + "worstDeviation": 1, + "tolerance": 1, + # volumes, cm^3 + "capacity": 3, + "capacityCandidate": 3, +} + +# Fields whose value is a physical measurement and must agree only to within double arithmetic +# across two independently converted shapes; everything else is required to be equal. +_REAL_BAND = 1.0e-9 + + +def flatten(node, prefix=""): + """Depth-first flatten of a part's report into {dotted path: scalar}.""" + flat = {} + if isinstance(node, dict): + for key, value in node.items(): + if key in _IGNORED_KEYS or any(s in key for s in _IGNORED_SUBSTRINGS): + continue + flat.update(flatten(value, f"{prefix}{key}.")) + elif isinstance(node, list): + for i, value in enumerate(node): + flat.update(flatten(value, f"{prefix}{i}.")) + else: + flat[prefix.rstrip(".")] = node + return flat + + +def exponent_of(path: str) -> int: + return _FIELD_EXPONENT.get(path.rsplit(".", 1)[-1], 0) + + +def is_mesh_derived(path: str) -> bool: + return (path.startswith(_MESH_DERIVED_PREFIXES) or + path.rsplit(".", 1)[-1] in _MESH_DERIVED_KEYS) + + +def compare_part(base: dict, cand: dict, factor: float, gate_only: bool = False): + """Return (list of differences, number of fields compared).""" + flat_base = flatten(base) + flat_cand = flatten(cand) + if gate_only: + flat_base = {k: v for k, v in flat_base.items() if not is_mesh_derived(k)} + flat_cand = {k: v for k, v in flat_cand.items() if not is_mesh_derived(k)} + differences = [] + for path in sorted(set(flat_base) | set(flat_cand)): + if path not in flat_base: + differences.append((path, "", flat_cand[path], "field only in candidate")) + continue + if path not in flat_cand: + differences.append((path, flat_base[path], "", "field only in baseline")) + continue + b, c = flat_base[path], flat_cand[path] + if isinstance(b, bool) or isinstance(c, bool) or isinstance(b, str) or isinstance(c, str): + if b != c: + differences.append((path, b, c, "differs")) + continue + if isinstance(b, int) and isinstance(c, int): + if b != c: + differences.append((path, b, c, f"integer differs by {c - b:+d}")) + continue + if b is None or c is None: + # A null leaf is a real value (a non-comparable capacity): null -> number is a change. + if b != c: + differences.append((path, b, c, "differs")) + continue + expected = b * (factor ** exponent_of(path)) + if expected == c: + continue + scale = max(abs(expected), abs(c)) + residual = abs(c - expected) / scale if scale else abs(c - expected) + if residual > _REAL_BAND: + differences.append((path, expected, c, + f"relative residual {residual:.3g} > {_REAL_BAND:g} " + f"(scale law: factor^{exponent_of(path)})")) + return differences, len(set(flat_base) | set(flat_cand)) + + +def key_reports(baseline, candidate): + """Pair the two reports' parts up, and say how. + + By full part id, falling back to the leading component only when the ids do not match; every + part of one CAD model shares that component. + """ + by_id = ({p["id"]: p for p in baseline}, {p["id"]: p for p in candidate}) + if set(by_id[0]) == set(by_id[1]): + return by_id[0], by_id[1], "part id" + by_stem = ({p["id"].split("/", 1)[0]: p for p in baseline}, + {p["id"].split("/", 1)[0]: p for p in candidate}) + collapsed = len(by_stem[0]) < len(baseline) or len(by_stem[1]) < len(candidate) + if collapsed: + return by_id[0], by_id[1], "part id (ids differ and the leading component is not unique)" + return by_stem[0], by_stem[1], "leading id component" + + +def compare(baseline, candidate, factor, label, gate_only=False): + base_by_key, cand_by_key, keying = key_reports(baseline, candidate) + print(f"(paired by {keying})") + print(f"=== {label} : {len(cand_by_key)} part(s) vs baseline's {len(base_by_key)}, " + f"length factor {factor:g} ===") + missing = sorted(set(base_by_key) - set(cand_by_key)) + extra = sorted(set(cand_by_key) - set(base_by_key)) + total_differences = 0 + for key in missing: + print(f" [MISSING] {key}: in baseline, absent from candidate") + total_differences += 1 + for key in extra: + print(f" [EXTRA] {key}: in candidate, absent from baseline") + total_differences += 1 + for key in sorted(set(base_by_key) & set(cand_by_key)): + differences, n_fields = compare_part(base_by_key[key], cand_by_key[key], factor, gate_only) + total_differences += len(differences) + if not differences: + print(f" [same] {key}: {n_fields} field(s) identical after the scale law") + continue + print(f" [DIFFERS] {key}: {len(differences)} of {n_fields} field(s)") + for path, b, c, why in differences: + print(f" {path}: baseline {b!r} -> candidate {c!r} ({why})") + print(f"\n{total_differences} difference(s)") + return total_differences + + +def _nudge(path): + """A defect injector that multiplies a real field by 1 + 1e-8, on the first part where that + field is not exactly zero. + """ + def apply(report): + for index, part in enumerate(report): + node = part + for key in path[:-1]: + node = node[key] + if node.get(path[-1]): + node[path[-1]] = node[path[-1]] * (1. + 1.e-8) + return index + return None + return apply + + +def self_test(baseline): + """Prove the comparison can say "yes": four injected defects, one per code path.""" + print("=== self-test: can this comparison detect a violation? ===") + + def bump_int(report): + column = report[0]["oracle"]["contains"] + column["nMismatchUnexplained"] = column["nMismatchUnexplained"] + 1 + return 0 + + def downgrade(report): + report[0]["navigation"]["reliability"] = "openBoundary" + return 0 + + cases = [ + ("integer column (one extra unexplained oracle disagreement)", bump_int), + ("length column, exponent 1 (maxRimIsolation +1e-8 relative)", + _nudge(["navigation", "maxRimIsolation"])), + ("volume column, exponent 3 (capacityCandidate +1e-8 relative)", + _nudge(["oracle", "capacityCandidate"])), + ("verdict string (navigation reliability downgraded)", downgrade), + ] + caught = 0 + for what, break_it in cases: + broken = copy.deepcopy(baseline) + try: + index = break_it(broken) + except (KeyError, IndexError) as exc: + print(f" [SKIP] {what}: not present in this report ({exc})") + continue + if index is None: + print(f" [MISSED] {what}: no part carries a non-zero value, nothing was injected") + continue + differences, _ = compare_part(baseline[index], broken[index], 1.0) + ok = bool(differences) + caught += ok + print(f" [{'caught' if ok else 'MISSED'}] {what} [part {baseline[index]['id']}]") + for path, b, c, why in differences: + print(f" {path}: {b!r} -> {c!r} ({why})") + print(f"\n{caught}/{len(cases)} injected defect(s) caught") + return caught == len(cases) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--baseline", required=True, type=Path) + ap.add_argument("--candidate", type=Path) + ap.add_argument("--scale", type=float, default=1.0, + help="uniform length factor applied to the candidate's geometry (1 for a pure " + "translation). Every real column is compared against baseline * " + "factor**exponent, with the exponent declared per field in this file.") + ap.add_argument("--label", default=None) + ap.add_argument("--gate-columns-only", action="store_true", + help="drop the columns that compare against the tessellated mesh, and the " + "triangle count. Under a scaling the mesh is deliberately not the same " + "mesh, so those columns move for a reason that is not the kernel's.") + ap.add_argument("--self-test", action="store_true", + help="inject known defects into the baseline and report whether they are " + "caught; run this before believing any green comparison") + args = ap.parse_args() + + baseline = json.loads(args.baseline.read_text()) + if args.self_test: + return 0 if self_test(baseline) else 1 + if args.candidate is None: + ap.error("--candidate is required unless --self-test is given") + candidate = json.loads(args.candidate.read_text()) + label = args.label or f"{args.baseline} vs {args.candidate}" + return 0 if compare(baseline, candidate, args.scale, label, + args.gate_columns_only) == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/csgCensus.py b/Detectors/CADSupport/validation/csgCensus.py new file mode 100644 index 0000000000000..1ad6846a25074 --- /dev/null +++ b/Detectors/CADSupport/validation/csgCensus.py @@ -0,0 +1,1181 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-09 + +"""The recognition census: per solid and per input model, what the CSG tiers would face. + +Nothing here emits anything. Per solid it reports + + 1. face count and the breakdown by surface type; + 2. whether the solid is quadric-only (plane/cylinder/cone/sphere/torus faces only); + 3. edge count, and for every edge shared by exactly two distinct faces, whether the dihedral is + convex, concave or tangential — the concave count is the input to the Tier-3 cell estimate; + 4. whether the face set matches a whole-part TGeo primitive template (Tier 1); + 5. how many non-quadric faces are *secretly* analytic and would canonicalise (Tier 0); + 6. volume and bounding box, as reference numbers for later acceptance work. + +`--self-test` checks every column against solids with closed-form answers; it also runs before +every census unless `--no-self-test` is given. + +Usage +----- + csgCensus.py --self-test + csgCensus.py --model .../ExcavatorArm.step [--model ...] --cache /tmp/csgcache --markdown + csgCensus.py --report --cache /tmp/csgcache # re-render tables from cache, no OCCT work + +The script re-execs itself under the aliBuild Python that can import pythonOCC (see `occ_env.py`). +""" + +import argparse +import json +import math +import sys +import time +from pathlib import Path + +import cadsupport_path # noqa: F401 +from cadsupport.occ_env import ensure_occ # noqa: E402 + +ensure_occ() + +from OCC.Core.BRep import BRep_Tool # noqa: E402 +from OCC.Core.BRepAdaptor import BRepAdaptor_Surface # noqa: E402 +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse # noqa: E402 +from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCone, # noqa: E402 + BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere, BRepPrimAPI_MakeTorus) +from OCC.Core.Geom import (Geom_RectangularTrimmedSurface, Geom_SurfaceOfLinearExtrusion, # noqa: E402 + Geom_SurfaceOfRevolution) +from OCC.Core.GeomAbs import GeomAbs_BSplineSurface # noqa: E402 +from OCC.Core.GeomAdaptor import GeomAdaptor_Curve # noqa: E402 +from OCC.Core.ShapeAnalysis import ShapeAnalysis_CanonicalRecognition # noqa: E402 +from OCC.Core.STEPCAFControl import STEPCAFControl_Reader # noqa: E402 +from OCC.Core.IFSelect import IFSelect_RetDone # noqa: E402 +from OCC.Core.TCollection import TCollection_AsciiString # noqa: E402 +from OCC.Core.TDF import TDF_Label, TDF_LabelSequence, TDF_Tool # noqa: E402 +from OCC.Core.TDocStd import TDocStd_Document # noqa: E402 +from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_REVERSED, TopAbs_SOLID # noqa: E402 +from OCC.Core.TopExp import TopExp_Explorer, topexp # noqa: E402 +from OCC.Core.TopTools import TopTools_IndexedMapOfShape # noqa: E402 +from OCC.Core.TopoDS import topods # noqa: E402 +from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool # noqa: E402 +from OCC.Core.gp import gp_Dir, gp_Vec # noqa: E402 +from cadsupport.analytic import CURVE_TYPE_NAME, SURFACE_TYPE_NAME # noqa: E402 +from cadsupport.census import _xyz, bounding_box, edge_census, halfspace_side, volume_of # noqa: E402 +from cadsupport.primitives import _cross, _dot, _norm, _sub # noqa: E402 + +CENSUS_FORMAT_VERSION = 3 + +QUADRIC_TYPES = ("plane", "cylinder", "cone", "sphere", "torus") + +# Relative tolerance for template matching (directions, radii, offsets). +TEMPLATE_REL_TOL = 1.0e-6 +TEMPLATE_ANG_TOL = 1.0e-6 + + +def _parallel(a, b, tol=TEMPLATE_ANG_TOL): + return _norm(_cross(a, b)) <= tol + + +def _antiparallel(a, b, tol=TEMPLATE_ANG_TOL): + return _parallel(a, b, tol) and _dot(a, b) < 0 + + +def _perp(a, b, tol=TEMPLATE_ANG_TOL): + return abs(_dot(a, b)) <= tol + + +def _point_on_axis(p, loc, direction, tol): + d = _sub(p, loc) + perp = _sub(d, tuple(c * _dot(d, direction) for c in direction)) + return _norm(perp) <= tol + + +# -------------------------------------------------------------------------------------------- +# surface classification and Tier-0 canonicalisation +# -------------------------------------------------------------------------------------------- + +def _basis_curve_type(surface, stype): + """Return the GeomAbs type name of a swept surface's basis curve, or None. + + The cast is attempted only for the type the adaptor reported, since `DownCast` raises otherwise. + """ + if stype not in ("revolution", "extrusion"): + return None + s = surface + while isinstance(s, Geom_RectangularTrimmedSurface): + s = s.BasisSurface() + caster = (Geom_SurfaceOfRevolution if stype == "revolution" + else Geom_SurfaceOfLinearExtrusion) + try: + swept = caster.DownCast(s) + basis = None if swept is None else swept.BasisCurve() + except Exception: + return None + if basis is None: + return None + try: + return CURVE_TYPE_NAME.get(GeomAdaptor_Curve(basis).GetType(), "other") + except Exception: + return "other" + + +def _canonical_recognition(face, tol): + """OCCT's own recogniser: is this face secretly a quadric? It has no torus test.""" + from OCC.Core.gp import gp_Cone, gp_Cylinder, gp_Pln, gp_Sphere + try: + rec = ShapeAnalysis_CanonicalRecognition(face) + except Exception: + return None, None + for name, meth, holder in (("plane", "IsPlane", gp_Pln()), + ("cylinder", "IsCylinder", gp_Cylinder()), + ("cone", "IsCone", gp_Cone()), + ("sphere", "IsSphere", gp_Sphere())): + try: + rec.ClearStatus() + if getattr(rec, meth)(tol, holder): + return name, rec.GetGap() + except Exception: + continue + return None, None + + +def classify_face(face, canonical_tol, do_canonical=True): + """Classify one face: its carrier type, and — if not a quadric — what it could become.""" + ad = BRepAdaptor_Surface(face, True) + stype = SURFACE_TYPE_NAME.get(ad.GetType(), "other") + info = {"type": stype} + if stype in QUADRIC_TYPES: + side = halfspace_side(face, ad, stype) + if side: + info["side"] = side + if stype != "plane": + # A plane has no intrinsic inside; for a curved carrier the two must agree. + info["orientationAgrees"] = (side == "interior") == \ + (face.Orientation() != TopAbs_REVERSED) + return info + + surface = BRep_Tool.Surface(face) + basis = _basis_curve_type(surface, stype) + if basis: + info["basisCurve"] = basis + # Revolving or extruding a line or a circle always produces a quadric. + if stype == "revolution" and basis in ("line", "circle"): + info["canonicalStructural"] = "cone/cylinder/plane" if basis == "line" else "torus/sphere" + elif stype == "extrusion" and basis in ("line", "circle"): + info["canonicalStructural"] = "plane" if basis == "line" else "cylinder" + + if do_canonical: + name, gap = _canonical_recognition(face, canonical_tol) + if name: + info["canonicalOCCT"] = name + info["canonicalGap"] = gap + return info + + +# -------------------------------------------------------------------------------------------- +# Tier-1 template matching +# -------------------------------------------------------------------------------------------- + +def _carriers(faces): + """Extract the analytic carrier of every face; returns None if any face is not a quadric.""" + out = [] + for f in faces: + ad = BRepAdaptor_Surface(f, True) + t = SURFACE_TYPE_NAME.get(ad.GetType(), "other") + if t == "plane": + pl = ad.Plane() + ax = pl.Axis() + n = _xyz(ax.Direction()) + if f.Orientation() == TopAbs_REVERSED: + n = (-n[0], -n[1], -n[2]) + out.append({"t": "plane", "n": n, "p": _xyz(ax.Location())}) + elif t == "cylinder": + cy = ad.Cylinder() + ax = cy.Axis() + out.append({"t": "cylinder", "d": _xyz(ax.Direction()), "p": _xyz(ax.Location()), + "r": cy.Radius()}) + elif t == "cone": + co = ad.Cone() + ax = co.Axis() + out.append({"t": "cone", "d": _xyz(ax.Direction()), "p": _xyz(ax.Location()), + "r": co.RefRadius(), "a": co.SemiAngle()}) + elif t == "sphere": + sp = ad.Sphere() + out.append({"t": "sphere", "p": _xyz(sp.Location()), "r": sp.Radius()}) + elif t == "torus": + to = ad.Torus() + ax = to.Axis() + out.append({"t": "torus", "d": _xyz(ax.Direction()), "p": _xyz(ax.Location()), + "R": to.MajorRadius(), "r": to.MinorRadius()}) + else: + return None + return out + + +def _scale_of(carriers, bbox_diag): + return max(bbox_diag, 1.0) + + +def distinct_carriers(carriers, scale): + """How many distinct halfspaces the face set spans; CAD splits a cylinder into several faces.""" + if carriers is None: + return None + tol = TEMPLATE_REL_TOL * scale + uniq = [] + for c in carriers: + for u in uniq: + if u["t"] != c["t"]: + continue + if c["t"] == "plane": + if _parallel(u["n"], c["n"]) and \ + abs(_dot(_sub(c["p"], u["p"]), u["n"])) <= tol: + break + elif c["t"] == "sphere": + if _norm(_sub(u["p"], c["p"])) <= tol and abs(u["r"] - c["r"]) <= tol: + break + elif c["t"] == "torus": + if _parallel(u["d"], c["d"]) and _norm(_sub(u["p"], c["p"])) <= tol \ + and abs(u["R"] - c["R"]) <= tol and abs(u["r"] - c["r"]) <= tol: + break + else: # cylinder / cone + if _parallel(u["d"], c["d"]) and _point_on_axis(c["p"], u["p"], u["d"], tol) \ + and abs(u["r"] - c["r"]) <= tol \ + and abs(u.get("a", 0.0) - c.get("a", 0.0)) <= TEMPLATE_ANG_TOL: + break + else: + uniq.append(c) + continue + return len(uniq) + + +def carrier_clusters(carriers, scale): + """Group the analytic carriers by shared axis / shared normal direction.""" + if carriers is None: + return None + tol = TEMPLATE_REL_TOL * scale + axial = [c for c in carriers if c["t"] in ("cylinder", "cone", "torus")] + clusters = [] + for c in axial: + for cl in clusters: + if _parallel(c["d"], cl["dir"]) and _point_on_axis(c["p"], cl["loc"], cl["dir"], tol): + cl["members"].append(c) + break + else: + clusters.append({"dir": c["d"], "loc": c["p"], "members": [c]}) + out = [] + for cl in clusters: + radii = sorted(round(m.get("r", m.get("R", 0.0)), 9) for m in cl["members"]) + out.append({"dir": [round(x, 9) for x in cl["dir"]], + "types": sorted({m["t"] for m in cl["members"]}), + "n": len(cl["members"]), "radii": radii}) + normals = [] + for c in carriers: + if c["t"] != "plane": + continue + for nd in normals: + if _parallel(c["n"], nd["dir"]): + nd["n"] += 1 + break + else: + normals.append({"dir": [round(x, 9) for x in c["n"]], "n": 1}) + return {"axisClusters": sorted(out, key=lambda z: (-z["n"], z["radii"])), + "planeDirections": sorted(normals, key=lambda z: -z["n"]), + "nAxisClusters": len(out), "nPlaneDirections": len(normals)} + + +def match_box(carriers, scale): + planes = [c for c in carriers if c["t"] == "plane"] + if len(planes) != 6 or len(planes) != len(carriers): + return None + used = [False] * 6 + axes = [] + for i in range(6): + if used[i]: + continue + for j in range(i + 1, 6): + if used[j]: + continue + if _antiparallel(planes[i]["n"], planes[j]["n"]): + used[i] = used[j] = True + sep = abs(_dot(_sub(planes[j]["p"], planes[i]["p"]), planes[i]["n"])) + axes.append((planes[i]["n"], sep)) + break + else: + return None + if len(axes) != 3: + return None + for a in range(3): + for b in range(a + 1, 3): + if not _perp(axes[a][0], axes[b][0]): + return None + dims = sorted(round(a[1], 9) for a in axes) + return {"template": "TGeoBBox", "params": {"dx": dims[0] / 2, "dy": dims[1] / 2, + "dz": dims[2] / 2}} + + +def _coaxial(items, scale): + """All items share one axis line (direction up to sign, and location on that line).""" + if not items: + return None + d0 = items[0]["d"] + p0 = items[0]["p"] + tol = TEMPLATE_REL_TOL * scale + for it in items[1:]: + if not _parallel(it["d"], d0): + return None + if not _point_on_axis(it["p"], p0, d0, tol): + return None + return d0, p0 + + +def match_tube_or_cone(carriers, scale): + """Two coaxial cylinders (or cones) + caps perpendicular to the axis, +/- a phi wedge.""" + cyls = [c for c in carriers if c["t"] == "cylinder"] + cones = [c for c in carriers if c["t"] == "cone"] + planes = [c for c in carriers if c["t"] == "plane"] + others = [c for c in carriers if c["t"] not in ("cylinder", "cone", "plane")] + if others or (not cyls and not cones): + return None + if cyls and cones: + lateral, kind = cyls + cones, "cone" # mixed cylinder/cone stack -> pcon-like + elif cyls: + lateral, kind = cyls, "tube" + else: + lateral, kind = cones, "cone" + if len(lateral) > 2: + return None + ax = _coaxial(lateral, scale) + if ax is None: + return None + d, _p = ax + caps = [pl for pl in planes if _parallel(pl["n"], d)] + wedge = [pl for pl in planes if _perp(pl["n"], d)] + if len(caps) != 2 or len(caps) + len(wedge) != len(planes): + return None + if len(wedge) not in (0, 2): + return None + seg = len(wedge) == 2 + if kind == "tube": + radii = sorted(c["r"] for c in lateral) + params = {"rmin": radii[0] if len(radii) == 2 else 0.0, "rmax": radii[-1]} + name = "TGeoTubeSeg" if seg else "TGeoTube" + else: + params = {"nlateral": len(lateral)} + name = "TGeoConeSeg" if seg else "TGeoCone" + dz = abs(_dot(_sub(caps[1]["p"], caps[0]["p"]), d)) / 2.0 + params["dz"] = dz + return {"template": name, "params": params} + + +def match_sphere(carriers, scale): + sph = [c for c in carriers if c["t"] == "sphere"] + if len(sph) != 1: + return None + planes = [c for c in carriers if c["t"] == "plane"] + if len(sph) + len(planes) != len(carriers): + return None + return {"template": "TGeoSphere", "params": {"r": sph[0]["r"], "cuts": len(planes)}} + + +def match_torus(carriers, scale): + tor = [c for c in carriers if c["t"] == "torus"] + if len(tor) != 1: + return None + planes = [c for c in carriers if c["t"] == "plane"] + if len(tor) + len(planes) != len(carriers): + return None + return {"template": "TGeoTorus", "params": {"R": tor[0]["R"], "r": tor[0]["r"], + "cuts": len(planes)}} + + +def match_revolution(carriers, scale, faces_info): + """Every carrier is a surface of revolution about one common axis (TGeoPcon).""" + if any(fi["type"] not in QUADRIC_TYPES and fi["type"] != "revolution" for fi in faces_info): + return None + axial = [c for c in carriers if c["t"] in ("cylinder", "cone", "torus")] if carriers else [] + if not carriers: + return None + if not axial: + return None + ax = _coaxial(axial, scale) + if ax is None: + return None + d, p = ax + tol = TEMPLATE_REL_TOL * scale + nwedge = 0 + for c in carriers: + if c["t"] in ("cylinder", "cone", "torus"): + continue + if c["t"] == "sphere": + if not _point_on_axis(c["p"], p, d, tol): + return None + elif c["t"] == "plane": + if _parallel(c["n"], d): + continue # a plane perpendicular to the axis: a pcon step + if _perp(c["n"], d) and _point_on_axis(c["p"], p, d, tol): + nwedge += 1 # a plane through the axis: a phi cut + else: + return None + else: + return None + if nwedge not in (0, 2): + return None + return {"template": "revolution/TGeoPcon-like", + "params": {"nlateral": len(axial), "phiCut": nwedge == 2}} + + +def match_extrusion(carriers, scale, faces_info): + """A closed 2D profile swept along one direction (TGeoXtru).""" + if any(fi["type"] not in ("plane", "cylinder", "extrusion") for fi in faces_info): + return None + if not carriers: + return None + cyls = [c for c in carriers if c["t"] == "cylinder"] + planes = [c for c in carriers if c["t"] == "plane"] + if len(cyls) + len(planes) != len(carriers): + return None + # Candidate extrusion directions: a cylinder axis or a cap-plane normal, not every plane pair. + candidates = [] + for d in [c["d"] for c in cyls] + [p["n"] for p in planes]: + if not any(_parallel(d, e) for e in candidates): + candidates.append(d) + if len(candidates) > 64: + return None + for d in candidates: + caps = [pl for pl in planes if _parallel(pl["n"], d)] + walls = [pl for pl in planes if _perp(pl["n"], d)] + if len(caps) != 2 or len(caps) + len(walls) != len(planes): + continue + if any(not _parallel(c["d"], d) for c in cyls): + continue + dz = abs(_dot(_sub(caps[1]["p"], caps[0]["p"]), d)) / 2.0 + return {"template": "extrusion/TGeoXtru-like", + "params": {"nwall": len(walls), "nround": len(cyls), "dz": dz}} + return None + + +def tier2_sketch(clusters): + """A one-line description of what a Tier-2 recogniser would have to build, or why it cannot.""" + if clusters is None: + return "non-quadric" + na = clusters["nAxisClusters"] + np_ = clusters["nPlaneDirections"] + if na == 0: + return f"planes only ({np_} directions)" + sizes = "+".join(str(c["n"]) for c in clusters["axisClusters"]) + return f"{na} axis clusters ({sizes}), {np_} plane directions" + + +def match_template(faces, faces_info, scale): + carriers = _carriers(faces) + if carriers is not None: + for matcher in (match_box, match_tube_or_cone, match_sphere, match_torus): + m = matcher(carriers, scale) + if m: + return m + if carriers is not None: + m = match_revolution(carriers, scale, faces_info) + if m: + return m + m = match_extrusion(carriers, scale, faces_info) + if m: + return m + return {"template": "none", "params": {}} + + +# -------------------------------------------------------------------------------------------- +# per-solid and per-model census +# -------------------------------------------------------------------------------------------- + +def solid_faces(solid): + fmap = TopTools_IndexedMapOfShape() + topexp.MapShapes(solid, TopAbs_FACE, fmap) + return [topods.Face(fmap.FindKey(i)) for i in range(1, fmap.Size() + 1)] + + +def census_solid(solid, name, canonical_tol, do_canonical=True, carrier_face_cap=400): + t0 = time.time() + faces = solid_faces(solid) + faces_info = [classify_face(f, canonical_tol, do_canonical) for f in faces] + + by_type = {} + for fi in faces_info: + by_type[fi["type"]] = by_type.get(fi["type"], 0) + 1 + + nquad = sum(by_type.get(t, 0) for t in QUADRIC_TYPES) + nfaces = len(faces) + canon_struct = sum(1 for fi in faces_info if "canonicalStructural" in fi) + canon_occt = sum(1 for fi in faces_info if "canonicalOCCT" in fi) + canon_either = sum(1 for fi in faces_info + if "canonicalStructural" in fi or "canonicalOCCT" in fi) + canon_by_type = {} + canon_to = {} + for fi in faces_info: + if "canonicalStructural" in fi or "canonicalOCCT" in fi: + canon_by_type[fi["type"]] = canon_by_type.get(fi["type"], 0) + 1 + if "canonicalOCCT" in fi: + k = f"{fi['type']}->{fi['canonicalOCCT']}" + canon_to[k] = canon_to.get(k, 0) + 1 + gaps = [fi["canonicalGap"] for fi in faces_info + if fi.get("canonicalGap") is not None] + basis_hist = {} + for fi in faces_info: + if "basisCurve" in fi: + k = f"{fi['type']}({fi['basisCurve']})" + basis_hist[k] = basis_hist.get(k, 0) + 1 + + bbox = bounding_box(solid) + diag = 0.0 if bbox is None else _norm(_sub(bbox[3:], bbox[:3])) + + rec = { + "name": name, + "faces": nfaces, + "byType": by_type, + "quadricFaces": nquad, + "quadricOnly": nquad == nfaces and nfaces > 0, + "quadricOnlyAfterTier0": (nquad + canon_either) == nfaces and nfaces > 0, + "canonicalisableStructural": canon_struct, + "canonicalisableOCCT": canon_occt, + "canonicalisableEither": canon_either, + "canonicalisableByType": canon_by_type, + "canonicalisableTo": canon_to, + "basisCurves": basis_hist, + "maxCanonicalGap": max(gaps) if gaps else None, + "exteriorHalfspaces": sum(1 for fi in faces_info if fi.get("side") == "exterior"), + "orientationDisagreements": sum(1 for fi in faces_info + if fi.get("orientationAgrees") is False), + "bbox": bbox, + "bboxDiagonal": diag, + "volume": volume_of(solid), + } + rec.update({"edgeCensus": edge_census(solid)}) + rec["concaveEdges"] = rec["edgeCensus"]["concave"] + rec["edgeCensus"]["mixed"] + rec["concaveEdgesTrusted"] = (rec["concaveEdges"] + - rec["edgeCensus"]["concaveNearTangential"] + - rec["edgeCensus"]["mixedNearTangential"]) + # Zero concave edges means a single CSG cell, not convexity: a through hole has none. + rec["singleCell"] = rec["concaveEdges"] == 0 + scale = _scale_of(None, diag) + # The carrier analyses are quadratic in the face count, so they are skipped above the cap. + if nfaces <= carrier_face_cap: + carriers = _carriers(faces) + rec.update(match_template(faces, faces_info, scale)) + rec["distinctCarriers"] = distinct_carriers(carriers, scale) + clusters = carrier_clusters(carriers, scale) + rec["carrierClusters"] = clusters + rec["tier2Sketch"] = tier2_sketch(clusters) + else: + rec.update({"template": "not-attempted(size)", "params": {}}) + rec["distinctCarriers"] = None + rec["carrierClusters"] = None + rec["tier2Sketch"] = "not-attempted(size)" + rec["seconds"] = time.time() - t0 + + # Instrument identity: the per-type histogram must account for every face, always. + assert sum(by_type.values()) == nfaces, f"face-type histogram lost faces on {name}" + return rec + + +def load_step_solids(path): + """Read a STEP file and return [(name, TopoDS_Solid)], names from XCAF when present.""" + doc = TDocStd_Document("csg-census") + reader = STEPCAFControl_Reader() + reader.SetNameMode(True) + if reader.ReadFile(str(path)) != IFSelect_RetDone: + raise RuntimeError(f"STEP read failed: {path}") + reader.Transfer(doc) + shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) + + labels = TDF_LabelSequence() + shape_tool.GetFreeShapes(labels) + # Solids come from exploding the free shapes, which keeps their locations; names attach after. + solids = [] + for i in range(1, labels.Length() + 1): + exp = TopExp_Explorer(shape_tool.GetShape(labels.Value(i)), TopAbs_SOLID) + while exp.More(): + solids.append(topods.Solid(exp.Current())) + exp.Next() + + named = [] + + def walk(label, prefix, depth=0): + if depth > 32: + return + nm = "" + try: + nm = str(label.GetLabelName() or "") + except Exception: + nm = "" + entry = TCollection_AsciiString() + TDF_Tool.Entry(label, entry) + full = f"{prefix}/{nm}" if nm else f"{prefix}/{entry.ToCString()}" + children = TDF_LabelSequence() + shape_tool.GetComponents(label, children) + if children.Length() > 0: + for k in range(1, children.Length() + 1): + walk(children.Value(k), full, depth + 1) + return + ref = TDF_Label() + if shape_tool.GetReferredShape(label, ref) and not ref.IsNull(): + sub = TDF_LabelSequence() + shape_tool.GetComponents(ref, sub) + if sub.Length() > 0: + for k in range(1, sub.Length() + 1): + walk(sub.Value(k), full, depth + 1) + return + label = ref + shape = shape_tool.GetShape(label) + if shape is not None and not shape.IsNull(): + named.append((full, shape)) + + for i in range(1, labels.Length() + 1): + walk(labels.Value(i), "") + + name_of = [] + for nm, shape in named: + exp = TopExp_Explorer(shape, TopAbs_SOLID) + while exp.More(): + name_of.append((nm, topods.Solid(exp.Current()))) + exp.Next() + + out = [] + for i, s in enumerate(solids): + label = f"solid{i}" + for nm, proto in name_of: + if s.IsPartner(proto): + label = nm + break + out.append((label, s)) + return out + + +def detect_unit_scale_to_cm(path): + """Same heuristic `O2_CADtoTGeo.py` uses: read the STEP header and look for a unit token.""" + data = Path(path).open("rb").read(4 * 1024 * 1024).decode("latin-1", "ignore").upper() + for token, scale, name in ((".MILLI.", 0.1, "mm"), (".CENTI.", 1.0, "cm"), + (".METRE.", 100.0, "m"), (".METER.", 100.0, "m"), + ("INCH", 2.54, "in")): + if token in data: + return scale, name + return 0.1, "mm" + + +def census_model(path, canonical_tol, do_canonical=True, max_faces=None, progress=True): + path = Path(path) + t0 = time.time() + scale, unit = detect_unit_scale_to_cm(path) + solids = load_step_solids(path) + t_load = time.time() - t0 + + # Prototypes are keyed by `hash(shape.TShape())`, the `IsPartner` class in O(1). + protos = [] + proto_of = [] + proto_key = {} + for _name, solid in solids: + key = hash(solid.TShape()) + if key not in proto_key: + proto_key[key] = len(protos) + protos.append(solid) + proto_of.append(proto_key[key]) + + # The census is per prototype; the record carries its placement count. + placements = {} + names = {} + for i, (nm, _solid) in enumerate(solids): + p = proto_of[i] + placements[p] = placements.get(p, 0) + 1 + names.setdefault(p, nm) + + records = [] + for p, solid in enumerate(protos): + name = names[p] + nf = len(solid_faces(solid)) + if max_faces is not None and nf > max_faces: + rec = {"name": name, "faces": nf, "skipped": "face budget"} + else: + try: + rec = census_solid(solid, name, canonical_tol, do_canonical) + except Exception as exc: # a bad solid must not lose the model + rec = {"name": name, "faces": nf, "error": f"{type(exc).__name__}: {exc}"} + rec["name"] = name + rec["index"] = p + rec["proto"] = p + rec["placements"] = placements[p] + rec["isFirstInstance"] = True + records.append(rec) + if progress: + tag = rec.get("template") or rec.get("skipped") or f"ERROR {rec.get('error')}" + print(f" proto {p + 1}/{len(protos)} x{placements[p]} " + f"{rec.get('faces', '?'):>5} faces {rec.get('seconds', 0.0):6.2f}s " + f"{tag} {name[:60]}", flush=True) + + # Instrument identity on real data: the placement counts must add up to the bodies found. + total = sum(r["placements"] for r in records) + assert total == len(solids), f"placement accounting lost bodies: {total} != {len(solids)}" + + return { + "formatVersion": CENSUS_FORMAT_VERSION, + "model": str(path), + "modelSize": path.stat().st_size, + "modelMtime": path.stat().st_mtime, + "unit": unit, + "unitScaleToCm": scale, + "canonicalTol": canonical_tol, + "canonicalEnabled": do_canonical, + "loadSeconds": t_load, + "placedSolids": len(solids), + "prototypeSolids": len(protos), + "totalSeconds": time.time() - t0, + "solids": records, + } + + +# -------------------------------------------------------------------------------------------- +# self-test: check the instrument before believing the table +# -------------------------------------------------------------------------------------------- + +def self_test(verbose=True): + """Every column of the census, against solids whose answers are known in closed form.""" + failures = [] + + def check(cond, msg): + if not cond: + failures.append(msg) + elif verbose: + print(f" ok {msg}") + + tol = 1e-7 + + box = BRepPrimAPI_MakeBox(10.0, 20.0, 30.0).Shape() + box_solid = next_solid(box) + r = census_solid(box_solid, "box", tol) + check(r["faces"] == 6, "box has 6 faces") + check(r["byType"].get("plane") == 6, "box faces are all planes") + check(r["quadricOnly"], "box is quadric-only") + check(r["edgeCensus"]["edges"] == 12, f"box has 12 edges (got {r['edgeCensus']['edges']})") + check(r["edgeCensus"]["convex"] == 12, + f"box has 12 convex edges (got {r['edgeCensus']}) -- SIGN OF THE CONCAVITY TEST") + check(r["concaveEdges"] == 0 and r["singleCell"], "box is a single cell") + check(r["template"] == "TGeoBBox", f"box matches TGeoBBox (got {r['template']})") + check(abs(r["volume"] - 6000.0) < 1e-6, f"box volume 6000 (got {r['volume']})") + check(r["exteriorHalfspaces"] == 0, "box has no exterior halfspace") + check(r["distinctCarriers"] == 6, f"box has 6 distinct carriers (got {r['distinctCarriers']})") + + # The trap this project already paid for: VolumeProperties on a single face is 0, silently. + faces = solid_faces(box_solid) + face_sum = sum(volume_of(f) for f in faces) + check(face_sum == 0.0, + f"per-face VolumeProperties sums to 0, not the solid volume (got {face_sum}) -- " + "the documented trap; volumes must be taken on the solid") + + cyl = next_solid(BRepPrimAPI_MakeCylinder(3.0, 10.0).Shape()) + r = census_solid(cyl, "cylinder", tol) + check(r["byType"].get("cylinder") == 1 and r["byType"].get("plane") == 2, + f"cylinder is 1 cylinder + 2 planes (got {r['byType']})") + check(r["template"] == "TGeoTube", f"cylinder matches TGeoTube (got {r['template']})") + check(abs(r["params"]["rmax"] - 3.0) < 1e-9 and abs(r["params"]["dz"] - 5.0) < 1e-9, + f"cylinder params rmax=3 dz=5 (got {r['params']})") + check(r["concaveEdges"] == 0, f"cylinder has no concave edge (got {r['edgeCensus']})") + check(abs(r["volume"] - math.pi * 9.0 * 10.0) < 1e-6, "cylinder volume") + check(r["exteriorHalfspaces"] == 0, + f"solid cylinder: material inside its own carrier (got {r['exteriorHalfspaces']})") + + tube = next_solid(BRepAlgoAPI_Cut(BRepPrimAPI_MakeCylinder(3.0, 10.0).Shape(), + BRepPrimAPI_MakeCylinder(1.0, 30.0).Shape()).Shape()) + r = census_solid(tube, "tube", tol) + check(r["template"] == "TGeoTube", f"annulus matches TGeoTube (got {r['template']})") + check(abs(r["params"]["rmin"] - 1.0) < 1e-9, + f"annulus rmin=1 (got {r['params']})") + # An annulus has no concave edge and is not convex, yet is one CSG cell with an exterior bore. + check(r["edgeCensus"]["concave"] == 0, + f"annulus has no concave edge (got {r['edgeCensus']})") + check(r["exteriorHalfspaces"] == 1, + f"annulus bore is an exterior halfspace (got {r['exteriorHalfspaces']})") + check(r["faces"] == 4 and r["distinctCarriers"] == 4, + f"annulus: 4 faces, 4 distinct carriers (got {r['faces']}, {r['distinctCarriers']})") + + sph = next_solid(BRepPrimAPI_MakeSphere(4.0).Shape()) + r = census_solid(sph, "sphere", tol) + check(r["template"] == "TGeoSphere", f"sphere matches TGeoSphere (got {r['template']})") + check(r["concaveEdges"] == 0, f"sphere has no concave edge (got {r['edgeCensus']})") + check(r["edgeCensus"]["degenerate"] == 2, + f"sphere has 2 degenerate pole edges (got {r['edgeCensus']})") + + tor = next_solid(BRepPrimAPI_MakeTorus(10.0, 2.0).Shape()) + r = census_solid(tor, "torus", tol) + check(r["template"] == "TGeoTorus", f"torus matches TGeoTorus (got {r['template']})") + check(r["quadricOnly"], "torus is quadric-only") + check(r["exteriorHalfspaces"] == 0, "torus material is inside its own carrier") + + # An L-shape: exactly one concave edge, by construction. + from OCC.Core.gp import gp_Ax2, gp_Pnt as _P + b1 = BRepPrimAPI_MakeBox(10.0, 10.0, 2.0).Shape() + b2 = BRepPrimAPI_MakeBox(gp_Ax2(_P(0, 0, 0), gp_Dir(0, 0, 1)), 2.0, 10.0, 10.0).Shape() + ell = next_solid(BRepAlgoAPI_Fuse(b1, b2).Shape()) + r = census_solid(ell, "Lshape", tol) + check(r["edgeCensus"]["concave"] == 1, + f"L-shape has exactly 1 concave edge (got {r['edgeCensus']})") + check(not r["singleCell"], "L-shape needs more than one cell") + + plate = BRepPrimAPI_MakeBox(gp_Ax2(_P(-5, -5, 0), gp_Dir(0, 0, 1)), 10.0, 10.0, 4.0).Shape() + + # A THROUGH hole: no concave edge (the material fills a quadrant at each rim), one exterior + # halfspace, one CSG cell -- box halfspaces intersected with the outside of the cylinder. + holed = next_solid(BRepAlgoAPI_Cut( + plate, BRepPrimAPI_MakeCylinder(1.5, 20.0).Shape()).Shape()) + r = census_solid(holed, "through_hole", tol) + check(r["edgeCensus"]["concave"] == 0, + f"through hole has no concave edge (got {r['edgeCensus']})") + check(r["singleCell"] and r["exteriorHalfspaces"] == 1, + f"through hole is one cell with one exterior halfspace (got " + f"cell={r['singleCell']} ext={r['exteriorHalfspaces']})") + check(r["quadricOnly"], "through hole is quadric-only") + + # A BLIND hole: the bottom rim IS concave, because the cylinder's carrier extended would cut + # material that the solid keeps. That is exactly the witness Tier 3's split loop consumes. + blind = next_solid(BRepAlgoAPI_Cut( + plate, + BRepPrimAPI_MakeCylinder(gp_Ax2(_P(0, 0, 2), gp_Dir(0, 0, 1)), 1.5, 10.0).Shape()).Shape()) + r = census_solid(blind, "blind_hole", tol) + check(r["edgeCensus"]["concave"] == 1, + f"blind hole has exactly 1 concave edge (got {r['edgeCensus']})") + + # A groove across the top face: 2 concave edges at the slot floor. + slot = BRepPrimAPI_MakeBox(gp_Ax2(_P(-2, -20, 2), gp_Dir(0, 0, 1)), 4.0, 40.0, 10.0).Shape() + r = census_solid(next_solid(BRepAlgoAPI_Cut(plate, slot).Shape()), "groove", tol) + check(r["edgeCensus"]["concave"] == 2, + f"groove has exactly 2 concave edges (got {r['edgeCensus']})") + + # --- Tier-0 recogniser: a positive and a NEGATIVE control ------------------------------- + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeFace, BRepBuilderAPI_NurbsConvert + from OCC.Core.GeomAPI import GeomAPI_PointsToBSplineSurface + from OCC.Core.TColgp import TColgp_Array2OfPnt + + nurbs_cyl = BRepBuilderAPI_NurbsConvert(BRepPrimAPI_MakeCylinder(3.0, 10.0).Shape()).Shape() + lateral = [f for f in solid_faces(next_solid(nurbs_cyl)) + if BRepAdaptor_Surface(f, True).GetType() == GeomAbs_BSplineSurface] + check(len(lateral) >= 1, f"NURBS-converted cylinder has a B-spline face (got {len(lateral)})") + if lateral: + got, gap = _canonical_recognition(lateral[0], 1e-7) + check(got == "cylinder" and gap is not None and gap < 1e-7, + f"positive control: a NURBS-encoded cylinder is recognised as a cylinder " + f"(got {got}, gap {gap})") + + grid = TColgp_Array2OfPnt(1, 5, 1, 5) + for i in range(1, 6): + for j in range(1, 6): + x, y = (i - 3) * 2.0, (j - 3) * 2.0 + grid.SetValue(i, j, _P(x, y, 0.15 * x * y)) # a saddle: not any quadric of ours + saddle = BRepBuilderAPI_MakeFace( + GeomAPI_PointsToBSplineSurface(grid).Surface(), 1e-9).Face() + got, gap = _canonical_recognition(saddle, 1e-7) + check(got is None, + f"NEGATIVE control: a genuine free-form saddle is NOT recognised as a quadric " + f"(got {got}, gap {gap}) -- without this the Tier-0 count means nothing") + + # `hash(TShape())` and a pairwise IsPartner sweep must define the same classes. + from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform + from OCC.Core.gp import gp_Trsf, gp_Vec + trsf = gp_Trsf() + trsf.SetTranslation(gp_Vec(50.0, 0.0, 0.0)) + moved = next_solid(BRepBuilderAPI_Transform(box_solid, trsf, False).Shape()) + other = next_solid(BRepPrimAPI_MakeBox(10.0, 20.0, 30.001).Shape()) + check(box_solid.IsPartner(moved) and hash(box_solid.TShape()) == hash(moved.TShape()), + "prototype key: a relocated instance is a partner and hashes equal") + check((not box_solid.IsPartner(other)) + and hash(box_solid.TShape()) != hash(other.TShape()), + "prototype key: a different body is not a partner and hashes differently") + + # The geometric halfspace-side test and the ORIENTATION flag must never disagree; if they do, + # one of the two is being read wrong and every exterior-halfspace count is suspect. + for nm, sh in (("through_hole", holed), ("blind_hole", blind), ("annulus", tube)): + rr = census_solid(sh, nm, tol) + check(rr["orientationDisagreements"] == 0, + f"{nm}: geometric halfspace side agrees with the ORIENTATION flag on every face") + + if verbose: + if failures: + print("\n SELF-TEST FAILURES:") + for f in failures: + print(f" FAIL {f}") + else: + print("\n self-test: all checks passed") + return failures + + +def ladder_shapes(): + """The boolean ladder fixtures, rebuilt here so they can be censused too. + + `make_boolean_fixtures.py` is not imported or run; these are + independent constructions of the same geometry (same radii, same axes, mm) so that the + census can answer questions about `tube_window` and its siblings — which are synthetic + fixtures, present in no input model, and therefore invisible to a census of STEP files. + """ + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common + from OCC.Core.gp import gp_Ax2, gp_Pnt as P + + def cyl(r, h, o=(0., 0., 0.), d=(0., 0., 1.)): + return BRepPrimAPI_MakeCylinder(gp_Ax2(P(*o), gp_Dir(*d)), r, h).Shape() + + cz = cyl(10., 60., (0., 0., -30.)) + cx = cyl(10., 60., (-30., 0., 0.), (1., 0., 0.)) + tube = cyl(15., 60., (0., 0., -30.)) + drill = cyl(8., 60., (-30., 0., 0.), (1., 0., 0.)) + return [ + ("cyl_cross_cyl", BRepAlgoAPI_Fuse(cz, cx).Shape()), + ("cyl_inter_cyl", BRepAlgoAPI_Common(cz, cx).Shape()), + ("tube_window", BRepAlgoAPI_Cut(tube, drill).Shape()), + ("cyl_plus_cone", BRepAlgoAPI_Fuse( + cyl(10., 30.), + BRepPrimAPI_MakeCone(gp_Ax2(P(0., 0., 30.), gp_Dir(0., 0., 1.)), + 10., 5., 20.).Shape()).Shape()), + ] + + +def next_solid(shape): + exp = TopExp_Explorer(shape, TopAbs_SOLID) + if not exp.More(): + raise RuntimeError("no solid in shape") + return topods.Solid(exp.Current()) + + +# -------------------------------------------------------------------------------------------- +# reporting +# -------------------------------------------------------------------------------------------- + +def summarise(data, unique=False): + """Roll up a model. `unique=True` counts each geometric prototype once, not once per + placement — the basis on which the published ALICE3 numbers were taken.""" + solids = [s for s in data["solids"] if "error" not in s and "skipped" not in s] + if not unique: + solids = [s for s in solids for _ in range(s.get("placements", 1))] + if not solids: + # Never return an empty summary: failed solids must not look like an empty model. + return {"solids": 0, "errors": sum(1 for s in data["solids"] if "error" in s), + "skipped": sum(1 for s in data["solids"] if "skipped" in s), + "firstError": next((s["error"] for s in data["solids"] if "error" in s), None)} + faces_total = sum(s["faces"] for s in solids) + by_type = {} + for s in solids: + for k, v in s["byType"].items(): + by_type[k] = by_type.get(k, 0) + v + canon = {} + for s in solids: + for k, v in s.get("canonicalisableByType", {}).items(): + canon[k] = canon.get(k, 0) + v + canon_to = {} + for s in solids: + for k, v in s.get("canonicalisableTo", {}).items(): + canon_to[k] = canon_to.get(k, 0) + v + gaps = [s["maxCanonicalGap"] for s in solids if s.get("maxCanonicalGap") is not None] + basis = {} + for s in solids: + for k, v in s.get("basisCurves", {}).items(): + basis[k] = basis.get(k, 0) + v + tmpl = {} + for s in solids: + tmpl[s["template"]] = tmpl.get(s["template"], 0) + 1 + concave_hist = {} + for s in solids: + b = s["concaveEdges"] + key = ("0" if b == 0 else "1-2" if b <= 2 else "3-10" if b <= 10 else + "11-50" if b <= 50 else "51-200" if b <= 200 else ">200") + concave_hist[key] = concave_hist.get(key, 0) + 1 + concave_hist_trusted = {} + for s in solids: + b = s["concaveEdgesTrusted"] + key = ("0" if b == 0 else "1-2" if b <= 2 else "3-10" if b <= 10 else + "11-50" if b <= 50 else "51-200" if b <= 200 else ">200") + concave_hist_trusted[key] = concave_hist_trusted.get(key, 0) + 1 + return { + "solids": len(solids), + "faces": faces_total, + "byType": by_type, + "basisCurves": basis, + "canonicalisableByType": canon, + "canonicalisableTo": canon_to, + "maxCanonicalGap": max(gaps) if gaps else None, + "quadricOnly": sum(1 for s in solids if s["quadricOnly"]), + "quadricOnlyAfterTier0": sum(1 for s in solids if s["quadricOnlyAfterTier0"]), + "tier0Rescues": sum(1 for s in solids + if s["quadricOnlyAfterTier0"] and not s["quadricOnly"]), + "singleCell": sum(1 for s in solids if s["singleCell"]), + "singleCellAndQuadric": sum(1 for s in solids if s["singleCell"] and s["quadricOnly"]), + "singleCellAndQuadricAfterTier0": sum(1 for s in solids if s["singleCell"] + and s["quadricOnlyAfterTier0"]), + "exteriorHalfspaces": sum(s["exteriorHalfspaces"] for s in solids), + "orientationDisagreements": sum(s["orientationDisagreements"] for s in solids), + "tangentialEdges": sum(s["edgeCensus"]["tangential"] for s in solids), + "mixedEdges": sum(s["edgeCensus"]["mixed"] for s in solids), + "edgeErrors": sum(s["edgeCensus"]["error"] for s in solids), + "nonManifoldEdges": sum(s["edgeCensus"]["nonManifold"] for s in solids), + "templates": tmpl, + # "not-attempted(size)" is not a match. + "templateMatched": sum(1 for s in solids + if s["template"] not in ("none", "not-attempted(size)")), + "templateNotAttempted": sum(1 for s in solids + if s["template"] == "not-attempted(size)"), + "primitiveMatched": sum(1 for s in solids if s["template"].startswith("TGeo")), + "concaveHistogram": concave_hist, + "concaveTotal": sum(s["concaveEdges"] for s in solids), + "concaveHistogramTrusted": concave_hist_trusted, + "concaveTotalTrusted": sum(s["concaveEdgesTrusted"] for s in solids), + "singleCellTrusted": sum(1 for s in solids if s["concaveEdgesTrusted"] == 0), + "carriersVsFaces": [sum(s["distinctCarriers"] for s in solids + if s.get("distinctCarriers") is not None), + sum(s["faces"] for s in solids + if s.get("distinctCarriers") is not None)], + "concaveNearTangential": sum(s["edgeCensus"]["concaveNearTangential"] for s in solids), + "mixedNearTangential": sum(s["edgeCensus"]["mixedNearTangential"] for s in solids), + "edgesTotal": sum(s["edgeCensus"]["edges"] for s in solids), + "errors": sum(1 for s in data["solids"] if "error" in s), + "skipped": sum(1 for s in data["solids"] if "skipped" in s), + } + + +def markdown_model(data, limit=None, unique=True): + lines = [] + name = Path(data["model"]).name + s = summarise(data, unique=unique) + lines.append(f"### `{name}`") + lines.append("") + lines.append(f"Unit `{data['unit']}` (x{data['unitScaleToCm']} to cm); " + f"{data.get('prototypeSolids', '?')} prototype solids in " + f"{data.get('placedSolids', '?')} placements; " + f"load {data['loadSeconds']:.1f} s, census total {data['totalSeconds']:.1f} s. " + f"One row per {'prototype' if unique else 'placement'}.") + lines.append("") + lines.append("| # | n | part | faces | halfsp | plane | cyl | cone | sph | tor | free-form |" + " swept | quadric-only | edges | concave | trusted | 1 cell? | Tier-0 | " + "template | volume | bbox diag |") + lines.append("| ---: | ---: | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |" + " ---: | :---: | ---: | ---: | ---: | :---: | ---: | --- | ---: | ---: |") + rows = data["solids"] + counts = {r.get("proto", r.get("index")): r.get("placements", 1) for r in rows} + if limit is not None: + rows = rows[:limit] + for r in rows: + n = counts.get(r.get("proto", r.get("index")), 1) + if "error" in r or "skipped" in r: + what = f"ERROR {r['error'][:60]}" if "error" in r else "skipped" + lines.append(f"| {r.get('index', '')} | {n} | `{r['name'][-40:]}` | " + f"{r.get('faces', '?')} |" + " |" * 15 + f" {what} | | |") + continue + bt = r["byType"] + free = bt.get("bspline", 0) + bt.get("bezier", 0) + bt.get("offset", 0) + \ + bt.get("other", 0) + swept = bt.get("revolution", 0) + bt.get("extrusion", 0) + hs = r.get("distinctCarriers") + lines.append( + f"| {r['index']} | {n} | `{r['name'][-40:]}` | {r['faces']} | " + f"{'-' if hs is None else hs} | {bt.get('plane', 0)} | " + f"{bt.get('cylinder', 0)} | {bt.get('cone', 0)} | {bt.get('sphere', 0)} | " + f"{bt.get('torus', 0)} | {free} | {swept} | " + f"{'Y' if r['quadricOnly'] else '.'} | {r['edgeCensus']['edges']} | " + f"{r['concaveEdges']} | {r['concaveEdgesTrusted']} | " + f"{'Y' if r['singleCell'] else '.'} | " + f"{r['canonicalisableEither']} | {r['template']} | " + f"{r['volume']:.4g} | {r['bboxDiagonal']:.4g} |") + lines.append("") + lines.append("Prototype roll-up: " + json.dumps(summarise(data, unique=True), sort_keys=True)) + lines.append("") + lines.append("Placement roll-up: " + json.dumps(summarise(data, unique=False), + sort_keys=True)) + lines.append("") + return "\n".join(lines) + + +# -------------------------------------------------------------------------------------------- + +def cache_path(cache_dir, model): + return Path(cache_dir) / (Path(model).name.replace(" ", "_") + ".census.json") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--model", action="append", default=[], help="STEP/BREP model to census") + ap.add_argument("--cache", default="/tmp/csgcache", help="directory for per-model JSON") + ap.add_argument("--refresh", action="store_true", help="ignore an existing cache entry") + ap.add_argument("--report", action="store_true", help="render tables from cache only") + ap.add_argument("--markdown", action="store_true", help="print markdown tables") + ap.add_argument("--limit-rows", type=int, default=None, help="rows per model in markdown") + ap.add_argument("--max-faces", type=int, default=None, + help="skip solids with more faces than this") + ap.add_argument("--canonical-tol", type=float, default=1.0e-7, + help="tolerance for ShapeAnalysis_CanonicalRecognition") + ap.add_argument("--no-canonical", action="store_true", + help="skip OCCT canonical recognition (much faster, loses Tier-0 column)") + ap.add_argument("--ladder", action="store_true", + help="census the boolean ladder fixtures (rebuilt in-process) and exit") + ap.add_argument("--self-test", action="store_true", help="run the instrument checks and exit") + ap.add_argument("--no-self-test", action="store_true", + help="do not run the instrument checks before a census") + args = ap.parse_args() + + if args.self_test: + print("csg.census self-test") + return 1 if self_test() else 0 + + if args.ladder: + for name, shape in ladder_shapes(): + r = census_solid(next_solid(shape), name, args.canonical_tol) + print(f"{name:<24} faces={r['faces']:>3} halfspaces={r['distinctCarriers']:>3} " + f"concave={r['concaveEdges']:>3} oneCell={r['singleCell']!s:<5} " + f"template={r['template']:<26} {r['tier2Sketch']}") + return 0 + + cache = Path(args.cache) + cache.mkdir(parents=True, exist_ok=True) + + if args.report: + datas = [json.loads(p.read_text()) for p in sorted(cache.glob("*.census.json"))] + else: + if not args.no_self_test: + print("csg.census self-test") + if self_test(verbose=True): + print("self-test failed; refusing to produce a table from a broken instrument") + return 1 + print("") + datas = [] + for model in args.model: + cp = cache_path(cache, model) + if cp.exists() and not args.refresh: + d = json.loads(cp.read_text()) + if (d.get("formatVersion") == CENSUS_FORMAT_VERSION + and d.get("modelMtime") == Path(model).stat().st_mtime + and d.get("canonicalEnabled") == (not args.no_canonical)): + print(f" cached: {model}") + datas.append(d) + continue + print(f" census: {model}") + d = census_model(model, args.canonical_tol, not args.no_canonical, args.max_faces) + cp.write_text(json.dumps(d, indent=1)) + print(f" wrote {cp} ({d['totalSeconds']:.1f} s)") + datas.append(d) + + if args.markdown: + for d in datas: + print(markdown_model(d, args.limit_rows)) + else: + for d in datas: + print(f"\n{Path(d['model']).name}") + print(f" prototypes: {json.dumps(summarise(d, unique=True), sort_keys=True)}") + print(f" placements: {json.dumps(summarise(d, unique=False), sort_keys=True)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/demo/analyse_all.sh b/Detectors/CADSupport/validation/demo/analyse_all.sh new file mode 100755 index 0000000000000..1010a286c4ac2 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/analyse_all.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# Reduce every run's MCStepLogger tree to a text tally, and every geantino run to a per-ray +# material budget. Writes /analysis/{steps_.txt,matbudget_.txt}. +# +# Usage: analyse_all.sh +set -u +GEO=$(cd "$(dirname "$0")" && pwd) +OUT=${1:?usage: analyse_all.sh } +mkdir -p "$OUT/analysis" +command -v root >/dev/null || { echo "analyse_all.sh: root not found; load the O2 environment" >&2; exit 1; } +# MCStepLogger is not on the O2 environment's paths; only the analysis needs it. +MCSL=${MCSTEPLOGGER_ROOT:-${O2_ROOT:+$O2_ROOT/../../MCStepLogger/latest}} +[ -d "$MCSL/include" ] || { echo "analyse_all.sh: MCStepLogger not found; set MCSTEPLOGGER_ROOT" >&2; exit 1; } +export LD_LIBRARY_PATH=$MCSL/lib:${LD_LIBRARY_PATH:-} +export ROOT_INCLUDE_PATH=$MCSL/include:${ROOT_INCLUDE_PATH:-} +for d in "$OUT"/runs/*/; do + tag=$(basename "$d") + sf="$d/MCStepLoggerOutput.root" + [ -f "$sf" ] || continue + root -l -b -q "$GEO/analyse_steps.macro(\"$sf\")" > "$OUT/analysis/steps_$tag.txt" 2>&1 + case "$tag" in + geantino_*|matfan_*) + root -l -b -q "$GEO/matbudget.macro(\"$d/o2sim_geometry.root\",\"$sf\",\"$OUT/analysis/matbudget_$tag.txt\")" \ + > "$OUT/analysis/matbudget_$tag.log" 2>&1 + ;; + esac + echo "analysed $tag" +done diff --git a/Detectors/CADSupport/validation/demo/analyse_steps.macro b/Detectors/CADSupport/validation/demo/analyse_steps.macro new file mode 100644 index 0000000000000..a261eb0036ba4 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/analyse_steps.macro @@ -0,0 +1,70 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +// Reduce an MCStepLogger output file to the numbers the representation comparison needs. +// +// root -l -b -q 'analyse_steps.macro("MCStepLoggerOutput.root")' +// +// MCStepLogger writes one entry per event into TTree "StepLoggerTree" with branches +// Steps : std::vector (one record per Geant step) +// Lookups : o2::StepLookups (volume id -> name / medium / module) +// Calls : magnetic-field calls +// The volume id in a StepInfo indexes Lookups.volidtovolname. +R__LOAD_LIBRARY(libMCStepLoggerCore) +#include "MCStepLogger/StepInfo.h" + +void analyse_steps(const char* file, const char* prefix = "") +{ + TFile f(file); + auto* t = (TTree*)f.Get("StepLoggerTree"); + if (!t) { + printf("%sNOTREE %s\n", prefix, file); + return; + } + + std::vector* steps = nullptr; + o2::StepLookups* lookups = nullptr; + t->SetBranchAddress("Steps", &steps); + t->SetBranchAddress("Lookups", &lookups); + + std::map perVol; + std::map lenVol; + long total = 0, secondaries = 0, nev = t->GetEntries(); + double totlen = 0; + for (long i = 0; i < nev; i++) { + t->GetEntry(i); + for (auto& s : *steps) { + total++; + secondaries += s.nsecondaries; + totlen += s.step; + std::string vn = "?"; + if (lookups && s.volId >= 0 && s.volId < (int)lookups->volidtovolname.size() && + lookups->volidtovolname[s.volId]) { + vn = *lookups->volidtovolname[s.volId]; + } + perVol[vn]++; + lenVol[vn] += s.step; + } + } + printf("%sEVENTS %ld\n", prefix, nev); + printf("%sSTEPS_TOTAL %ld\n", prefix, total); + printf("%sSTEPS_PER_EVENT %.2f\n", prefix, nev ? double(total) / nev : 0.); + printf("%sSECONDARIES %ld\n", prefix, secondaries); + printf("%sSTEPLENGTH_TOTAL_CM %.4f\n", prefix, totlen); + printf("%sNVOLUMES_TOUCHED %zu\n", prefix, perVol.size()); + std::vector> v(perVol.begin(), perVol.end()); + std::sort(v.begin(), v.end(), [](auto& a, auto& b) { return a.second > b.second; }); + for (auto& kv : v) { + printf("%sVOL %-28s %8ld %12.4f\n", prefix, kv.first.c_str(), kv.second, lenVol[kv.first]); + } +} diff --git a/Detectors/CADSupport/validation/demo/check_geometry.macro b/Detectors/CADSupport/validation/demo/check_geometry.macro new file mode 100644 index 0000000000000..98086b803baab --- /dev/null +++ b/Detectors/CADSupport/validation/demo/check_geometry.macro @@ -0,0 +1,96 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +// Stage 3 of the integration demo: where did the CAD module land, and does it overlap? +// +// root -l -b -q 'check_geometry.macro("o2sim_geometry.root", 1)' +// +// Second argument: also run TGeoManager::CheckOverlaps (slow), which is only a hint on +// O2BVHSurfaceSolid. +// +// The CAD module appears under `barrel` as the assembly the converter emits, named after the +// CAD root label ("Assembly" for ExcavatorArm). + +static void subtreeBox(TGeoVolume* vol, const TGeoHMatrix& base, const char* label) +{ + Double_t lo[3] = {1e30, 1e30, 1e30}, hi[3] = {-1e30, -1e30, -1e30}; + long nleaf = 0; + TGeoIterator it(vol); + TGeoNode* nd; + while ((nd = it.Next())) { + if (nd->GetVolume()->IsAssembly() || !nd->GetVolume()->GetShape()) + continue; + TGeoHMatrix m = base * (*it.GetCurrentMatrix()); + auto* bb = (TGeoBBox*)nd->GetVolume()->GetShape(); + const Double_t* o = bb->GetOrigin(); + for (int i = 0; i < 8; i++) { + Double_t l[3] = {o[0] + ((i & 1) ? 1 : -1) * bb->GetDX(), + o[1] + ((i & 2) ? 1 : -1) * bb->GetDY(), + o[2] + ((i & 4) ? 1 : -1) * bb->GetDZ()}, + g[3]; + m.LocalToMaster(l, g); + for (int k = 0; k < 3; k++) { + if (g[k] < lo[k]) + lo[k] = g[k]; + if (g[k] > hi[k]) + hi[k] = g[k]; + } + } + nleaf++; + } + printf("WORLDBOX %-10s leaves=%4ld x[%9.3f,%9.3f] y[%9.3f,%9.3f] z[%9.3f,%9.3f]\n", + label, nleaf, lo[0], hi[0], lo[1], hi[1], lo[2], hi[2]); +} + +void check_geometry(const char* geofile, int overlaps = 0, double ovlp_prec = 0.1) +{ + TGeoManager::Import(geofile); + auto* mgr = gGeoManager; + printf("GEOM %s\n", geofile); + printf("COUNTS volumes=%d nodes=%d\n", mgr->GetListOfVolumes()->GetEntries(), mgr->GetNNodes()); + + std::map want = {{"Assembly", "BAGR"}}; + TGeoIterator it(mgr->GetTopVolume()); + TGeoNode* nd; + while ((nd = it.Next())) { + auto f = want.find(nd->GetVolume()->GetName()); + if (f == want.end()) + continue; + subtreeBox(nd->GetVolume(), *it.GetCurrentMatrix(), f->second.c_str()); + want.erase(f); + } + for (auto& kv : want) + printf("WORLDBOX %-10s NOT FOUND\n", kv.second.c_str()); + + std::map cls; + TIter nx(mgr->GetListOfVolumes()); + TGeoVolume* v; + while ((v = (TGeoVolume*)nx())) { + if (v->GetShape()) + cls[v->GetShape()->ClassName()]++; + } + for (auto& kv : cls) + printf("SHAPE %-32s %d\n", kv.first.c_str(), kv.second); + + if (overlaps) { + mgr->CheckOverlaps(ovlp_prec); + auto* l = mgr->GetListOfOverlaps(); + printf("OVERLAPS prec=%g count=%d\n", ovlp_prec, l ? l->GetEntries() : 0); + if (l) { + for (int i = 0; i < l->GetEntries(); i++) { + auto* ov = (TGeoOverlap*)l->At(i); + printf("OVERLAP %.6f cm %s\n", ov->GetOverlap(), ov->GetTitle()); + } + } + } +} diff --git a/Detectors/CADSupport/validation/demo/convert_all.sh b/Detectors/CADSupport/validation/demo/convert_all.sh new file mode 100755 index 0000000000000..b3ea3e8728046 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/convert_all.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# Stage 1 of the integration demo: convert ExcavatorArm three times. +# +# excavator_arm_exact : the cascade CSG -> exact O2BVHSurfaceSolid -> tessellated fallback +# excavator_arm_tess : pure tessellation, same mesh precision as the cascade's fallback +# excavator_arm_coarse : a deliberately degraded tessellation, so that "the two representations agree" +# can be told apart from "the instrument cannot see a difference" +# +# Needs a python3 that can import OCC (for example under `alienv enter pythonOCC/latest`), or +# PYOCC set to one. +# +# Usage: convert_all.sh +set -u +OUT=${1:?usage: convert_all.sh } +GEO=$(cd "$(dirname "$0")/../.." && pwd) +PYOCC=${PYOCC:-python3} +if ! "$PYOCC" -c "import OCC" 2>/dev/null; then + echo "convert_all.sh: $PYOCC cannot import OCC; load pythonOCC or set PYOCC" >&2 + exit 1 +fi + +# Mesh precision. --mesh-prec sets linear AND angular deflection to the same value, and +# it behaves as an *angular* knob. +EXCAVATOR_ARM_PREC=${EXCAVATOR_ARM_PREC:-0.1} +COARSE_PREC=${COARSE_PREC:-2.0} + +MODEL="$GEO/examples/ExcavatorArm.step" +MATERIALS="$GEO/examples/ExcavatorArm_MATERIALS.csv" +NIST="$GEO/tools/g4_nist_database/G4_NIST_DB.json" +run() { # run + local tag=$1; shift + mkdir -p "$OUT/conv/$tag" "$OUT/logs" + echo "=== $tag ===" + /usr/bin/time -v "$PYOCC" "$GEO/tools/O2_CADtoTGeo.py" "$@" \ + --output-folder "$OUT/conv/$tag" -o geom.C --g4-nist-json "$NIST" \ + > "$OUT/logs/conv_$tag.log" 2>&1 + echo " exit=$? -> $OUT/logs/conv_$tag.log" +} + +run excavator_arm_exact "$MODEL" --csg auto --exact-surfaces auto --materials-csv "$MATERIALS" +run excavator_arm_tess "$MODEL" --mesh --mesh-prec "$EXCAVATOR_ARM_PREC" --materials-csv "$MATERIALS" +run excavator_arm_coarse "$MODEL" --mesh --mesh-prec "$COARSE_PREC" --materials-csv "$MATERIALS" + +# The exact-surface macro needs one post-processing step before o2-sim can JIT it; see +# patch_exact_macro.py. +"$PYOCC" "$GEO/validation/demo/patch_exact_macro.py" "$OUT"/conv/*/geom.C +echo "done" diff --git a/Detectors/CADSupport/validation/demo/count_hits.macro b/Detectors/CADSupport/validation/demo/count_hits.macro new file mode 100644 index 0000000000000..153b7e5054a3b --- /dev/null +++ b/Detectors/CADSupport/validation/demo/count_hits.macro @@ -0,0 +1,60 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +// External-detector hits from an o2-sim-serial run. +// +// root -l -b -q 'count_hits.macro("o2sim.root")' +// +// o2-sim-serial leaves external-detector hits in the monolithic o2sim.root under branches +// named Hit. macro/migrateSimFiles.C only splits off detectors that the GRP marks as +// read out and whose branch names SimTraits knows, and it knows nothing about external +// detectors -- so no o2sim_Hits.root appears in serial mode. In parallel mode +// (-j >= 2) O2HitMerger writes them like any other detector's. +R__LOAD_LIBRARY(libO2ExternalDetectors) +#include "ExternalDetectors/Hit.h" + +void count_hits(const char* file) +{ + TFile f(file); + auto* t = (TTree*)f.Get("o2sim"); + if (!t) { + printf("NOTREE\n"); + return; + } + for (auto* o : *t->GetListOfBranches()) { + TString bn = o->GetName(); + if (!bn.EndsWith("Hit")) + continue; + std::vector* v = nullptr; + t->SetBranchAddress(bn, &v); + long n = 0; + double rmin = 1e30, rmax = -1e30, zmin = 1e30, zmax = -1e30, edep = 0; + for (long i = 0; i < t->GetEntries(); i++) { + t->GetEntry(i); + if (!v) + continue; + n += v->size(); + for (auto& h : *v) { + double r = std::hypot(h.GetX(), h.GetY()); + rmin = std::min(rmin, r); + rmax = std::max(rmax, r); + zmin = std::min(zmin, (double)h.GetZ()); + zmax = std::max(zmax, (double)h.GetZ()); + edep += h.GetEnergyLoss(); + } + } + printf("HITS %-8s n=%6ld r[%8.3f,%8.3f] z[%9.3f,%9.3f] sumEdep=%.6g GeV\n", + bn.Data(), n, rmin, rmax, zmin, zmax, edep); + t->ResetBranchAddresses(); + } +} diff --git a/Detectors/CADSupport/validation/demo/fibonacci_geantinos.macro b/Detectors/CADSupport/validation/demo/fibonacci_geantinos.macro new file mode 100644 index 0000000000000..be532df196447 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/fibonacci_geantinos.macro @@ -0,0 +1,65 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +// A deterministic geantino fan on Fibonacci-sphere directions, for the material-budget scan. +// +// Why not an axis raster: three axis beams are three directions however many rays are fired, +// and a phi x theta grid (which is what o2-sim-evalmat does) oversamples the poles and lines +// up with exactly the symmetry axes a CAD assembly is built on. A Fibonacci lattice puts N +// directions on the sphere with near-uniform density and no alignment with any axis. +// +// The directions carry no random numbers at all, so the same ray index is the same direction +// in every run -- which is what makes the exact-vs-tessellated comparison a per-ray test +// rather than an aggregate one. +// +// o2-sim ... -g extgen --configKeyValues \ +// 'GeneratorExternal.fileName=fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos(512,1.0,0.9)' +// +// nrays : number of directions +// pgev : momentum of each geantino +// cosmax : |cos(theta)| bound; 0.9 keeps the fan off the beam axis ends + +#include "FairGenerator.h" +#include "FairPrimaryGenerator.h" +#include +#include + +class FibonacciGeantinoGen : public FairGenerator +{ + public: + FibonacciGeantinoGen(int nrays = 512, double pgev = 1.0, double cosmax = 0.9) + : mN(nrays), mP(pgev), mCosMax(cosmax) {} + + Bool_t ReadEvent(FairPrimaryGenerator* pg) override + { + const double golden = TMath::Pi() * (3.0 - std::sqrt(5.0)); + for (int i = 0; i < mN; ++i) { + const double cz = mCosMax * (1.0 - 2.0 * (i + 0.5) / mN); + const double st = std::sqrt(std::max(0.0, 1.0 - cz * cz)); + const double phi = golden * i; + pg->AddTrack(0, mP * st * std::cos(phi), mP * st * std::sin(phi), mP * cz, + 0., 0., 0.); + } + return kTRUE; + } + + private: + int mN; + double mP; + double mCosMax; +}; + +FairGenerator* fibonacci_geantinos(int nrays = 512, double pgev = 1.0, double cosmax = 0.9) +{ + return new FibonacciGeantinoGen(nrays, pgev, cosmax); +} diff --git a/Detectors/CADSupport/validation/demo/make_configs.py b/Detectors/CADSupport/validation/demo/make_configs.py new file mode 100755 index 0000000000000..cb5687be67014 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/make_configs.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Write the o2-sim external-geometry and detector-list JSON for one representation. + +The ExcavatorArm model is anchored to `barrel`, which Cave.cxx places at cave (0,-30,0), and is authored +with its long axis along the CAD *y* axis. TGeoCombiTrans::RotateX(+90) maps local +y -> master +z, +so rotation_deg [90,0,0] puts that axis on the beam. + +Usage: make_configs.py +""" +import json +import os +import sys + +# The barrel-frame placement that puts ExcavatorArm's bounding-box centre at ALICE (100, 0, 0), inside the +# barrel and close enough to the origin that a box generator reaches it. +PLACEMENT = {"translation": [120.928, 102.064, 20.327], "rotation_deg": [90.0, 0.0, 0.0]} + + +def main() -> int: + if len(sys.argv) != 4: + print(__doc__) + return 2 + conv_root, rep, outdir = sys.argv[1], sys.argv[2], sys.argv[3] + os.makedirs(outdir, exist_ok=True) + + macro = os.path.abspath(os.path.join(conv_root, "conv", f"excavator_arm_{rep}", "geom.C")) + if not os.path.exists(macro): + print(f"missing macro {macro}") + return 1 + + ext = { + "externalDetectors": [ + { + "name": "BAGR", + "title": f"Excavator, Bucket sensitive ({rep})", + "macro": macro, + "anchor": "barrel", + "detID": "FOC", + # Substring match: this selects Bucket, BucketLink1, BucketLink2, + # BucketCylinderInner and BucketCylinderOuter -- the whole bucket group. + "sensitiveVolumes": ["Bucket"], + "placement": PLACEMENT, + }, + ] + } + detlist = {"EXTCAD": ["BAGR"]} + + with open(os.path.join(outdir, "externalGeometry.json"), "w") as f: + json.dump(ext, f, indent=2) + f.write("\n") + with open(os.path.join(outdir, "detectorlist.json"), "w") as f: + json.dump(detlist, f, indent=2) + f.write("\n") + print(f"wrote {outdir}/externalGeometry.json and {outdir}/detectorlist.json ({rep})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/demo/matbudget.macro b/Detectors/CADSupport/validation/demo/matbudget.macro new file mode 100644 index 0000000000000..b24211cf5f027 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/matbudget.macro @@ -0,0 +1,99 @@ +// Copyright 2019-2026 CERN and copyright holders of ALICE O2. +// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +// All rights not expressly granted are reserved. +// +// This software is distributed under the terms of the GNU General Public +// License v3 (GPL Version 3), copied verbatim in the file "COPYING". +// +// In applying this license CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. +/// \author Sandro Wenzel +/// \since 2026-08 + +// Per-ray material budget from an MCStepLogger step tree plus the geometry it was taken in. +// +// root -l -b -q 'matbudget.macro("o2sim_geometry.root","MCStepLoggerOutput.root","out.txt")' +// +// For every geantino the integral of step-length / radiation-length is accumulated over the +// steps it took, volume by volume. Two representations of the same CAD solid must give the +// same integral along the same ray; the ray index is the MCStepLogger trackID, and because +// the Fibonacci fan carries no random numbers, ray i is the same direction in both runs. +// +// Volumes whose medium is the converter's "Default" placeholder (A=Z=rho=0) have no radiation +// length; their steps are counted separately rather than silently dropped. +R__LOAD_LIBRARY(libMCStepLoggerCore) +#include "MCStepLogger/StepInfo.h" + +void matbudget(const char* geofile, const char* stepfile, const char* outfile) +{ + TGeoManager::Import(geofile); + std::map radlen; // volume name -> X0 [cm], <=0 means "no material" + TIter nx(gGeoManager->GetListOfVolumes()); + TGeoVolume* v; + while ((v = (TGeoVolume*)nx())) { + double x0 = -1; + if (v->GetMedium() && v->GetMedium()->GetMaterial()) { + x0 = v->GetMedium()->GetMaterial()->GetRadLen(); + if (!(x0 > 0) || !std::isfinite(x0)) + x0 = -1; + } + radlen[v->GetName()] = x0; + } + + TFile f(stepfile); + auto* t = (TTree*)f.Get("StepLoggerTree"); + if (!t) { + printf("NOTREE %s\n", stepfile); + return; + } + std::vector* steps = nullptr; + o2::StepLookups* lookups = nullptr; + t->SetBranchAddress("Steps", &steps); + t->SetBranchAddress("Lookups", &lookups); + + std::map x0PerTrack; // trackID -> sum(step/X0) + std::map lenNoMat; // trackID -> step length in materialless volumes + std::map> dir; + std::map nstep; + long unknownVol = 0; + + for (long i = 0; i < t->GetEntries(); i++) { + t->GetEntry(i); + for (auto& s : *steps) { + std::string vn = "?"; + if (lookups && s.volId >= 0 && s.volId < (int)lookups->volidtovolname.size() && + lookups->volidtovolname[s.volId]) + vn = *lookups->volidtovolname[s.volId]; + auto it = radlen.find(vn); + double x0 = (it == radlen.end()) ? -1 : it->second; + if (it == radlen.end()) + unknownVol++; + if (x0 > 0) + x0PerTrack[s.trackID] += s.step / x0; + else + lenNoMat[s.trackID] += s.step; + if (!nstep[s.trackID]) { + double p = std::sqrt(s.px * s.px + s.py * s.py + s.pz * s.pz); + if (p > 0) + dir[s.trackID] = {s.px / p, s.py / p, s.pz / p}; + } + nstep[s.trackID]++; + } + } + + FILE* out = fopen(outfile, "w"); + fprintf(out, "# trackID ux uy uz nsteps x/X0 len_no_material_cm\n"); + double tot = 0; + for (auto& kv : x0PerTrack) + tot += kv.second; + for (auto& kv : nstep) { + int id = kv.first; + auto d = dir.count(id) ? dir[id] : std::array{0, 0, 0}; + fprintf(out, "%6d %9.6f %9.6f %9.6f %6ld %12.8f %12.4f\n", + id, d[0], d[1], d[2], kv.second, x0PerTrack[id], lenNoMat[id]); + } + fclose(out); + printf("MATBUDGET tracks=%zu totalX0=%.6f meanX0=%.6f unknownVolSteps=%ld -> %s\n", + nstep.size(), tot, nstep.empty() ? 0. : tot / nstep.size(), unknownVol, outfile); +} diff --git a/Detectors/CADSupport/validation/demo/patch_exact_macro.py b/Detectors/CADSupport/validation/demo/patch_exact_macro.py new file mode 100755 index 0000000000000..a866c0d4dc46a --- /dev/null +++ b/Detectors/CADSupport/validation/demo/patch_exact_macro.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Make an exact-surface geom.C loadable through o2-sim's external-geometry mechanism. + +WORKAROUND, not a fix. `loadCADGeometryHook` JITs the macro inside a namespace and hoists only +'#' lines, so the macro's forward declaration of `o2::cad::LoadSurfaceSolid` lands in a nested +`o2` and the macro fails to compile. This replaces it with an #include of O2SurfaceSolidIO.h, +which is hoisted to global scope. + +Usage: patch_exact_macro.py [...] (idempotent) +""" +import sys + +BLOCK = """// O2SurfaceSolidIO.h is not part of the ROOT dictionary module; declare the loader +// prototype directly (the symbol resolves from libO2CADSupport). +namespace o2 +{ +namespace cad +{ +bool LoadSurfaceSolid(const std::string& file, O2BVHSurfaceSolid& solid); +} // namespace cad +} // namespace o2 +""" + +REPLACEMENT = """// PATCHED by validation/demo/patch_exact_macro.py: the emitted forward declaration is +// nested by the JIT namespace wrapper in CADGeometryUtils.cxx and shadows ::o2. A '#include' +// is hoisted to global scope by that wrapper, so it declares the right symbol. +#include "CADSupport/O2SurfaceSolidIO.h" +""" + + +def main() -> int: + if len(sys.argv) < 2: + print(__doc__) + return 2 + rc = 0 + for path in sys.argv[1:]: + text = open(path).read() + if REPLACEMENT.splitlines()[-1] in text: + print(f"{path}: already patched") + continue + if BLOCK not in text: + if "O2BVHSurfaceSolid" not in text: + print(f"{path}: no exact-surface prelude, nothing to do") + else: + print(f"{path}: ERROR prelude not recognised -- converter output changed") + rc = 1 + continue + open(path, "w").write(text.replace(BLOCK, REPLACEMENT)) + print(f"{path}: patched") + return rc + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/demo/run_all.sh b/Detectors/CADSupport/validation/demo/run_all.sh new file mode 100755 index 0000000000000..d94d8d7f97db2 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/run_all.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# The measurement matrix of the integration demo. Two geometry representations of the ExcavatorArm +# model, everything else held fixed: same seed, same generator, single-threaded, +# MCStepLogger attached (per-step ROOT tree included). +# +# geantino_* : 5 x 50 geantinos, eta [-1,1] -- pure geometry, no interactions +# electron_* : 5 x 20 electrons at 1 GeV +# pion_* : 5 x 20 pi+ at 1 GeV +# matfan_* : one event of NRAYS geantinos on Fibonacci-sphere directions, the +# per-ray material-budget equivalence test +# +# Usage: run_all.sh +set -u +GEO=$(cd "$(dirname "$0")" && pwd) +OUT=${1:?usage: run_all.sh } +EV=${EV:-5} +N=${N:-50} +NCHG=${NCHG:-20} +NRAYS=${NRAYS:-512} + +for rep in exact tess; do + EVENTS=$EV NGUN=$N PDG=0 STEPLOG=1 "$GEO/run_sim.sh" "$OUT" $rep geantino_$rep + EVENTS=$EV NGUN=$NCHG PDG=11 STEPLOG=1 "$GEO/run_sim.sh" "$OUT" $rep electron_$rep + EVENTS=$EV NGUN=$NCHG PDG=211 STEPLOG=1 "$GEO/run_sim.sh" "$OUT" $rep pion_$rep + EVENTS=1 GEN=extgen STEPLOG=1 \ + CONFIGKEY="GeneratorExternal.fileName=$GEO/fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos($NRAYS,1.0,0.9)" \ + "$GEO/run_sim.sh" "$OUT" $rep matfan_$rep +done +# controls: determinism, and a deliberately degraded tessellation +EVENTS=1 GEN=extgen STEPLOG=1 \ + CONFIGKEY="GeneratorExternal.fileName=$GEO/fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos($NRAYS,1.0,0.9)" \ + "$GEO/run_sim.sh" "$OUT" exact matfan_exact_repeat +EVENTS=1 GEN=extgen STEPLOG=1 \ + CONFIGKEY="GeneratorExternal.fileName=$GEO/fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos($NRAYS,1.0,0.9)" \ + "$GEO/run_sim.sh" "$OUT" coarse matfan_coarse + +# the transport-cost measurement: a big fan, timed without MCStepLogger and counted with it +BIGRAYS=${BIGRAYS:-8192} +for rep in exact tess; do + EVENTS=1 GEN=extgen STEPLOG=0 \ + CONFIGKEY="GeneratorExternal.fileName=$GEO/fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos($BIGRAYS,1.0,0.9)" \ + "$GEO/run_sim.sh" "$OUT" $rep bigfan_$rep + EVENTS=1 GEN=extgen STEPLOG=1 \ + CONFIGKEY="GeneratorExternal.fileName=$GEO/fibonacci_geantinos.macro;GeneratorExternal.funcName=fibonacci_geantinos($BIGRAYS,1.0,0.9)" \ + "$GEO/run_sim.sh" "$OUT" $rep bigfanlog_$rep +done +echo "run_all done" diff --git a/Detectors/CADSupport/validation/demo/run_sim.sh b/Detectors/CADSupport/validation/demo/run_sim.sh new file mode 100755 index 0000000000000..aa321749995d9 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/run_sim.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +# One o2-sim run of the integration demo. +# +# Usage: run_sim.sh [extra o2-sim args...] +# +# Environment knobs: +# EVENTS=3 SEED=42 GEN=boxgen PDG=0 (geantino) NGUN=20 STEPLOG=0|1 NOGEANT=0|1 +# CONFIGKEY="a=1;b=2" extra --configKeyValues, appended to the box-gun ones +# +# Everything is deterministic: the seed is fixed and the run is single-threaded +# (o2-sim-serial), so two runs differing only in the geometry representation are comparable. +set -u +export LD_LIBRARY_PATH=${LD_LIBRARY_PATH:-} +GEO=$(cd "$(dirname "$0")/.." && pwd) +CONV=${1:?usage: run_sim.sh [args...]} +REP=${2:?} +TAG=${3:?} +shift 3 + +EVENTS=${EVENTS:-3} +SEED=${SEED:-42} +GEN=${GEN:-boxgen} +PDG=${PDG:-0} +NGUN=${NGUN:-20} +PMIN=${PMIN:-1.0} +PMAX=${PMAX:-1.0} +STEPLOG=${STEPLOG:-0} +CONFIGKEY=${CONFIGKEY:-} +NOGEANT=${NOGEANT:-0} + +RUNDIR=$CONV/runs/$TAG +mkdir -p "$RUNDIR" +python3 "$GEO/demo/make_configs.py" "$CONV" "$REP" "$RUNDIR" || exit 1 + +command -v o2-sim-serial >/dev/null || { echo "run_sim.sh: o2-sim-serial not found; load the O2 environment" >&2; exit 1; } +cd "$RUNDIR" || exit 1 + +ARGS=(-n "$EVENTS" -g "$GEN" --seed "$SEED" + --detectorList "EXTCAD:$RUNDIR/detectorlist.json" + --extGeomFile "$RUNDIR/externalGeometry.json" + --configKeyValues "BoxGun.number=$NGUN;BoxGun.pdg=$PDG;BoxGun.prange[0]=$PMIN;BoxGun.prange[1]=$PMAX${CONFIGKEY:+;$CONFIGKEY}" + -o o2sim) +[ "$NOGEANT" = "1" ] && ARGS+=(--noGeant) + +if [ "$STEPLOG" = "1" ]; then + MCSL=${MCSTEPLOGGER_ROOT:-${O2_ROOT:+$O2_ROOT/../../MCStepLogger/latest}} + [ -f "$MCSL/lib/libMCStepLoggerInterceptSteps.so" ] || { echo "run_sim.sh: MCStepLogger not found; set MCSTEPLOGGER_ROOT" >&2; exit 1; } + export LD_PRELOAD=$MCSL/lib/libMCStepLoggerInterceptSteps.so + export MCSTEPLOG_OUTFILE=$RUNDIR/MCStepLoggerOutput.root + # the per-step ROOT tree (StepLoggerTree) is only written when MCSTEPLOG_TTREE is set; + # without it MCStepLogger only prints its per-volume summary to the log. + export MCSTEPLOG_TTREE=1 +fi + +/usr/bin/time -v o2-sim-serial "${ARGS[@]}" "$@" > "$RUNDIR/sim.log" 2>&1 +rc=$? +unset LD_PRELOAD +echo "run $TAG ($REP) exit=$rc -> $RUNDIR/sim.log" +exit $rc diff --git a/Detectors/CADSupport/validation/demo/summarise_runs.py b/Detectors/CADSupport/validation/demo/summarise_runs.py new file mode 100755 index 0000000000000..968280d9e32c2 --- /dev/null +++ b/Detectors/CADSupport/validation/demo/summarise_runs.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Turn the integration-demo run directories into the comparison tables. + +Reads each /runs//sim.log and reports, per run: + geometry+engine init time, transport time, peak RSS, total steps, secondaries, + tracks transported, sensitive steps and hits per external detector, and the + per-volume step tally that MCStepLogger prints. + +Then pairs every _exact with its _tess and prints the differences. + +Usage: summarise_runs.py [--per-volume] +""" +import os +import re +import sys + +RE_INIT = re.compile(r"Init: Real time ([\d.]+) s, CPU time ([\d.]+)") +RE_TOOK = re.compile(r"Simulation process took ([\d.]+) s") +RE_REAL = re.compile(r"\[INFO\] Real time ([\d.]+) s, CPU time ([\d.]+)s") +RE_RSS = re.compile(r"Maximum resident set size \(kbytes\): (\d+)") +RE_STEPS = re.compile(r"\[STEPLOGGER\]: did (\d+) steps") +RE_TRACKS = re.compile(r"\[STEPLOGGER\]: transported (\d+) different tracks") +RE_VOL = re.compile(r"\[STEPLOGGER\]: VolName (\S+) COUNT (\d+) SECONDARIES (\d+)") +RE_EOE = re.compile(r"External detector (\S+) EndOfEvent: (\d+) sensitive step\(s\) -> (\d+) hit\(s\)") +RE_BAD = re.compile(r"stuck|Stuck|ABORT|abort|FATAL|not reachable|Navigation", re.I) + + +def parse(logpath): + r = {"vol": {}, "sec": {}, "sens": {}, "hits": {}, "bad": [], "real": []} + with open(logpath, errors="ignore") as f: + for line in f: + m = RE_INIT.search(line) + if m: + r["init_real"], r["init_cpu"] = float(m.group(1)), float(m.group(2)) + m = RE_TOOK.search(line) + if m: + r["total"] = float(m.group(1)) + m = RE_REAL.search(line) + if m: + r["real"].append((float(m.group(1)), float(m.group(2)))) + m = RE_RSS.search(line) + if m: + r["rss_mb"] = int(m.group(1)) / 1024.0 + m = RE_STEPS.search(line) + if m: + # MCStepLogger flushes once per event: sum, do not overwrite + r["steps"] = r.get("steps", 0) + int(m.group(1)) + m = RE_TRACKS.search(line) + if m: + r["tracks"] = r.get("tracks", 0) + int(m.group(1)) + m = RE_VOL.search(line) + if m: + r["vol"][m.group(1)] = int(m.group(2)) + r["sec"][m.group(1)] = int(m.group(3)) + m = RE_EOE.search(line) + if m: + r["sens"][m.group(1)] = r["sens"].get(m.group(1), 0) + int(m.group(2)) + r["hits"][m.group(1)] = r["hits"].get(m.group(1), 0) + int(m.group(3)) + if RE_BAD.search(line) and "TG4RootNavigator" not in line: + r["bad"].append(line.strip()[:160]) + # the transport timing is the last "Real time" line the application prints + if r["real"]: + r["transport_real"], r["transport_cpu"] = r["real"][-1] + r["secondaries"] = sum(r["sec"].values()) + return r + + +def main() -> int: + if len(sys.argv) < 2: + print(__doc__) + return 2 + root = os.path.join(sys.argv[1], "runs") + per_volume = "--per-volume" in sys.argv + runs = {} + analysis = os.path.join(sys.argv[1], "analysis") + for tag in sorted(os.listdir(root)): + log = os.path.join(root, tag, "sim.log") + if not os.path.exists(log): + continue + r = parse(log) + # With LOG_TTREE set, the step counts come from the analyse_all.sh reduction. + af = os.path.join(analysis, f"steps_{tag}.txt") + if os.path.exists(af): + for line in open(af): + f = line.split() + if len(f) >= 2 and f[0] == "STEPS_TOTAL": + r["steps"] = int(f[1]) + elif len(f) >= 2 and f[0] == "SECONDARIES": + r["secondaries"] = int(f[1]) + elif len(f) >= 4 and f[0] == "VOL": + r["vol"][f[1]] = int(f[2]) + runs[tag] = r + + hdr = f"{'run':22s} {'init_s':>8s} {'transp_s':>9s} {'RSS_MB':>8s} {'steps':>9s} {'2nd':>8s} {'tracks':>7s} {'BAGR hits':>9s} {'bad':>4s}" + print(hdr) + print("-" * len(hdr)) + for tag, r in runs.items(): + print(f"{tag:22s} {r.get('init_real',0):8.2f} {r.get('transport_real',0):9.3f} " + f"{r.get('rss_mb',0):8.1f} {r.get('steps',0):9d} {r.get('secondaries',0):8d} " + f"{r.get('tracks',0):7d} {r['hits'].get('BAGR',0):9d} " + f"{len(r['bad']):4d}") + + print() + print("pairwise exact vs tessellated") + for tag in sorted(runs): + if not tag.endswith("_exact"): + continue + other = tag[:-6] + "_tess" + if other not in runs: + continue + a, b = runs[tag], runs[other] + name = tag[:-6] + ds = a.get("steps", 0) - b.get("steps", 0) + rel = 100.0 * ds / b["steps"] if b.get("steps") else 0.0 + print(f" {name:12s} steps exact={a.get('steps',0):8d} tess={b.get('steps',0):8d} " + f"diff={ds:+7d} ({rel:+.2f}%)") + print(f" {'':12s} 2nd exact={a.get('secondaries',0):8d} tess={b.get('secondaries',0):8d}") + print(f" {'':12s} transp exact={a.get('transport_real',0):8.3f}s tess={b.get('transport_real',0):8.3f}s " + f"ratio={a.get('transport_real',0)/b['transport_real'] if b.get('transport_real') else 0:.2f}") + print(f" {'':12s} init exact={a.get('init_real',0):8.2f}s tess={b.get('init_real',0):8.2f}s") + print(f" {'':12s} RSS exact={a.get('rss_mb',0):8.1f}MB tess={b.get('rss_mb',0):8.1f}MB") + print(f" {'':12s} hits BAGR {a['hits'].get('BAGR',0)} vs {b['hits'].get('BAGR',0)}") + if per_volume: + vols = sorted(set(a["vol"]) | set(b["vol"]), + key=lambda v: -(a["vol"].get(v, 0) + b["vol"].get(v, 0))) + print(f" {'volume':28s} {'exact':>8s} {'tess':>8s} {'diff':>8s}") + for v in vols: + x, y = a["vol"].get(v, 0), b["vol"].get(v, 0) + if x == y == 0: + continue + print(f" {v:28s} {x:8d} {y:8d} {x-y:+8d}") + print() + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/exportSourceShapes.py b/Detectors/CADSupport/validation/exportSourceShapes.py new file mode 100644 index 0000000000000..fdbe333aa4859 --- /dev/null +++ b/Detectors/CADSupport/validation/exportSourceShapes.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Export the `TGeoShape` a round-tripped part was made from, as `original_.root`. + +One file per part, next to the converted artefacts, with the source volume's shape under the key +"shape"; the source volume is found through `checkKnownSource.py`. A part is exported only when +its source shape is in the same frame as the converted artefacts (`shapePlacement` is the identity +and the part is not a mirrored prototype); a refused part is reported with its reason. +`roundTripReport.py` uses `export_run(write=False)` for the descriptions alone. + +Usage +----- + exportSourceShapes.py --original /o2sim_geometry.root \\ + --writer-report /ITS_writer_report.json \\ + --converted \\ + [--parts IBCYSSFlangeC,BREF1] [--json original_report.json] + +`--converted` is a directory holding the converter's `csg_report.json`; the files are written into +it. With no `--parts` every CSG-carried part of the report is exported. +""" + +import argparse +import json +import sys +from pathlib import Path + +import checkKnownSource as cks # noqa: E402 + + +def boolean_shape_size(shape): + """`(depth, leaves, {class: count})` of a TGeo boolean tree; a primitive is depth 0, 1 leaf.""" + from collections import Counter + if not shape.InheritsFrom("TGeoCompositeShape"): + return 0, 1, Counter([shape.ClassName()]) + node = shape.GetBoolNode() + dl, nl, cl = boolean_shape_size(node.GetLeftShape()) + dr, nr, cr = boolean_shape_size(node.GetRightShape()) + return max(dl, dr) + 1, nl + nr, cl + cr + + +def describe(shape): + depth, leaves, classes = boolean_shape_size(shape) + return { + "class": shape.ClassName(), + "booleanDepth": depth, + "leaves": leaves, + "leafClasses": dict(sorted(classes.items())), + } + + +def export_run(original, writer_report_path, converted, parts=None, verbose=True, write=True, + tiers=("csg",)): + """Write one `original_.root` per exportable part. Returns the per-part records. + + With `write=False` nothing is written and every part is still described. + """ + import ROOT + ROOT.gROOT.SetBatch(True) + ROOT.gSystem.Load("libO2CADSupport") # the emitted shape may be an O2 class + + converted = Path(converted) + report_path = converted / "csg_report.json" + if not report_path.exists(): + raise SystemExit(f"{report_path} does not exist (convert with --csg auto)") + report = json.loads(report_path.read_text()) + writer_report = json.loads(Path(writer_report_path).read_text()) + index = cks._writer_index(writer_report) + + manager = ROOT.TGeoManager.Import(str(original)) + if manager is None: + raise SystemExit(f"could not read a TGeoManager from {original}") + by_name = {} + for volume in manager.GetListOfVolumes(): + by_name.setdefault(volume.GetName(), []).append(volume) + + wanted = set(parts) if parts else None + records = [] + for part in report.get("parts", []): + # Writing needs an emitted CSG shape; a report describes every tier. + if part.get("representation") not in tiers: + continue + emitted_name = part.get("volume") + stem = part.get("part") + if wanted is not None and emitted_name not in wanted and stem not in wanted: + continue + record = {"part": stem, "volume": emitted_name, "written": None, "refused": None} + row = index.get(emitted_name) + if row is None: + record["refused"] = f"no writer-report row for emittedName {emitted_name!r}" + records.append(record) + continue + + placement = part.get("shapePlacement") + mirrored = emitted_name.endswith("__mirrored") + # The frame refusals apply only when writing; describe-only mode records the reason. + frame_problem = None + if mirrored: + frame_problem = ( + f"a Z-mirrored prototype of {row.get('name')!r}: its source shape needs a " + "reflection to reach this part's frame, and the volume it mirrors is in the " + "corpus in its own right") + elif not cks.placement_is_identity(placement): + frame_problem = ("the emitted shape carries a non-identity placement, so the " + "source shape is not in the same frame as the other artefacts") + if frame_problem: + record["refused"] = frame_problem + if write: + records.append(record) + continue + + candidates = by_name.get(row.get("name")) or [] + # The emitted shape is opened only to resolve an ambiguous name, and closed straight after. + handle, emitted_shape = None, None + if len(candidates) > 1: + shape_file = part.get("shapeFile") + if shape_file and Path(shape_file).exists(): + handle = ROOT.TFile.Open(str(shape_file)) + emitted_shape = handle.Get("shape") if handle else None + if emitted_shape: + cks.reclose_flat_csg(emitted_shape) + volume = (cks.resolve_source_volume(candidates, row, emitted_shape, placement) + if candidates else None) + if handle: + handle.Close() # resolution is done; the emitted shape is not read again + if volume is None: + record["refused"] = (f"the original geometry has no volume named {row.get('name')!r} " + "whose shape matches the writer's record") + records.append(record) + continue + + shape = volume.GetShape() + record["sourceVolume"] = row.get("name") + record.update(describe(shape)) + if write and not frame_problem: + target = converted / f"original_{stem}.root" + out = ROOT.TFile.Open(str(target), "RECREATE") + out.WriteTObject(shape, "shape") + out.Close() + record["written"] = str(target) + else: + target = None + records.append(record) + if verbose and write: + print(f" {emitted_name:32s} <- {row.get('name'):28s} {record['class']:22s} " + f"depth {record['booleanDepth']:>3} / {record['leaves']:>3} leaves -> " + f"{target.name}") + + if verbose: + written = sum(1 for r in records if r["written"]) + print(f"{written}/{len(records)} part(s) exported" + if write else f"{len(records)} part(s) described (nothing written)") + for r in records: + if r["refused"]: + print(f" refused {r['volume']}: {r['refused']}") + return records + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--original", required=True, help="the source o2sim_geometry.root") + ap.add_argument("--writer-report", required=True, help="_writer_report.json") + ap.add_argument("--converted", required=True, help="the converter output directory") + ap.add_argument("--parts", help="comma-separated part or volume names (default: all CSG parts)") + ap.add_argument("--json", help="write the per-part records here") + ap.add_argument("--no-write", action="store_true", + help="describe every part but write no original_*.root (for a corpus report)") + ap.add_argument("--tiers", default="csg", + help="which cascade tiers to cover: csg,surface,mesh (default: csg)") + args = ap.parse_args() + + parts = [p for p in (args.parts or "").split(",") if p] or None + records = export_run(args.original, args.writer_report, args.converted, parts, + write=not args.no_write, + tiers=tuple(t for t in args.tiers.split(",") if t)) + if args.json: + Path(args.json).write_text(json.dumps({"parts": records}, indent=1)) + print(f"wrote {args.json}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/makeTestPartDB.py b/Detectors/CADSupport/validation/makeTestPartDB.py new file mode 100755 index 0000000000000..d06d57ef04180 --- /dev/null +++ b/Detectors/CADSupport/validation/makeTestPartDB.py @@ -0,0 +1,408 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-07 + +"""Build a test-part database for the solid-navigation harness. + +For each input CAD model it runs O2_CADtoTGeo.py with `--exact-surfaces auto --mesh +--surface-report --dump-brep --csg auto` and indexes, per leaf volume, the paired artefacts into +one `manifest.json`: `surfaces__.bin`, `facets__.bin`, `brep__.brep` +when present, and `shape__.root` (a TGeoShape under the key "shape", in cm). A part +enters only when both the sidecar and the mesh exist. + +Each part's `"shipped"` block copies the converter's cascade decision from `csg_report.json`, so +the gate judges the representation the part ships in; `"decidedBy"` records the source. Leaf +solids with no sidecar are listed under `"unscoredParts"`. + +Usage: + python3 makeTestPartDB.py --output + python3 makeTestPartDB.py --models ExcavatorArm.step as1-oc-214.stp --output --force + +Requires the O2 + pythonOCC environment, as O2_CADtoTGeo.py does. +""" + +import argparse +import datetime +import json +import re +import shutil +import struct +import subprocess +import sys +from typing import Optional +from pathlib import Path + +_SCRIPT_DIR = Path(__file__).resolve().parent +_CONVERTER = _SCRIPT_DIR.parent / "tools" / "O2_CADtoTGeo.py" +_DEFAULT_MODEL_DIR = _SCRIPT_DIR.parent / "examples" + +# Models that ship in examples/. +_DEFAULT_MODELS = [ + "ExcavatorArm.step", + "as1-oc-214.stp", +] + + +def _sanitize_filename(s: str) -> str: + """Mirror of O2_CADtoTGeo.py's sanitize_filename(); keep in sync with that copy.""" + safe = re.sub(r"[^0-9a-zA-Z]", "_", s) + return safe or "x" + + +def _slugify_model(model_path: Path) -> str: + return _sanitize_filename(model_path.stem) + + +def _resolve_model(model_arg: str) -> Path: + p = Path(model_arg) + if not p.is_absolute(): + candidate = _DEFAULT_MODEL_DIR / model_arg + if candidate.exists(): + p = candidate + else: + p = Path(model_arg).expanduser().resolve() + return p.resolve() + + +def _read_facets_summary(path: Path): + """Return (nTriangles, bboxMin, bboxMax) by scanning a facets_*.bin file.""" + with open(path, "rb") as f: + header = f.read(4) + (n_tri,) = struct.unpack(" part), or None for identity. + + Read from `csg_report.json`; the `TGeoHMatrix` in `shape_.root` is the same transform. + """ + row = cascade_by_suffix.get(suffix) + if row is None and lid is not None: + row = cascade_by_lid.get(lid) + return None if row is None else row.get("shapePlacement") + + +def _shipped_entry(suffix: str, lid, cascade_by_suffix: dict, cascade_by_lid: dict, + cascade_meta: dict, out_dir: Path): + """What representation this part ships in, from the converter's cascade decision only.""" + row = cascade_by_suffix.get(suffix) + if row is None and lid is not None: + row = cascade_by_lid.get(lid) + if row is not None: + tier = row.get("representation", "mesh") + entry = { + "representation": _TIER_TO_REPRESENTATION.get(tier, tier), + "tier": tier, + "decidedBy": "csg_report.json (converter cascade)", + "source": row.get("source"), + "evidence": row.get("evidence", {}), + } + if row.get("shapeDeferred"): + entry["shapeDeferred"] = True + return entry + # No cascade report: the converter ran without --csg, so its cascade is the older + # exact-surfaces -> tessellated one and a part in this database has a sidecar by construction. + return { + "representation": "surface" if (out_dir / f"surfaces_{suffix}.bin").exists() else "mesh", + "tier": "surface" if (out_dir / f"surfaces_{suffix}.bin").exists() else "mesh", + "decidedBy": "artifact presence (converter ran without --csg)", + "source": str((out_dir / "surface_report.json").resolve()), + "evidence": {}, + } + + +def _unscored_leaf_solids(slug: str, out_dir: Path, report: dict, indexed_suffixes: set, + cascade_by_lid: dict): + """Leaf solids the model has that never enter the part database, such as ExcavatorArm's `Bucket`.""" + missing = [] + for lid, info in report.get("volumes", {}).items(): + name = info.get("name") or "" + volname = _sanitize_filename(name) if name else "vol" + suffix = f"{volname}_{_sanitize_filename(lid)}" + if suffix in indexed_suffixes: + continue + row = cascade_by_lid.get(lid, {}) + tier = row.get("representation", "mesh") + missing.append({ + "id": f"{slug}/{suffix}", + "volume": name, + "lid": lid, + "nFaces": info.get("n_faces"), + "eligible": info.get("eligible"), + "shipped": { + "representation": _TIER_TO_REPRESENTATION.get(tier, tier), + "tier": tier, + "decidedBy": ("csg_report.json (converter cascade)" if row + else "artifact presence (no exact sidecar was written)"), + "source": row.get("source", str((out_dir / "surface_report.json").resolve())), + "evidence": row.get("evidence", {}), + }, + "reason": "no surfaces_*.bin sidecar: the harness cannot score this part", + "facets": (str(out_dir / f"facets_{suffix}.bin") + if (out_dir / f"facets_{suffix}.bin").exists() else None), + }) + return missing + + +def _index_parts(model_name: str, slug: str, out_dir: Path, report: dict): + """Pair surfaces_*.bin / facets_*.bin by _ suffix and read the report's + (raw lid -> volume name) map to recover the manifest's `volume`/`lid` fields.""" + suffix_to_lid = {} + for lid, info in report.get("volumes", {}).items(): + name = info.get("name") or "" + volname = _sanitize_filename(name) if name else "vol" + lidname = _sanitize_filename(lid) + suffix_to_lid[f"{volname}_{lidname}"] = (lid, name) + + cascade_by_suffix, cascade_by_lid, cascade_meta = _read_cascade(out_dir) + + parts = [] + warnings = [] + indexed_suffixes = set() + for surf_path in sorted(out_dir.glob("surfaces_*.bin")): + suffix = surf_path.name[len("surfaces_"):-len(".bin")] + facet_path = out_dir / f"facets_{suffix}.bin" + if not facet_path.exists(): + warnings.append(f"{surf_path.name}: no matching facets_{suffix}.bin, skipped") + continue + lid, volname = suffix_to_lid.get(suffix, (None, None)) + if lid is None: + warnings.append(f"{surf_path.name}: suffix not found in surface_report.json volumes") + n_tri, bbox_min, bbox_max = _read_facets_summary(facet_path) + part = { + "id": f"{slug}/{suffix}", + "model": model_name, + "volume": volname, + "lid": lid, + "surfaces": str(surf_path), + "facets": str(facet_path), + "nTriangles": n_tri, + "bbox": {"min": bbox_min, "max": bbox_max}, + } + # OCCT reference solid in cm (converter --dump-brep); absent for older databases. + brep_path = out_dir / f"brep_{suffix}.brep" + if brep_path.exists(): + part["brep"] = str(brep_path) + # A third representation: a TGeoShape under "shape", in cm, with an optional "placement". + shape_path = out_dir / f"shape_{suffix}.root" + if shape_path.exists(): + part["shape"] = str(shape_path) + # Mirrored from the converter's record; absent means identity. + placement = _shape_placement(suffix, lid, cascade_by_suffix, cascade_by_lid) + if placement is not None: + part["shapePlacement"] = placement + # The representation the converter shipped; the gate judges this one. + part["shipped"] = _shipped_entry(suffix, lid, cascade_by_suffix, cascade_by_lid, + cascade_meta, out_dir) + if part["shipped"]["representation"] == "shape" and "shape" not in part: + warnings.append(f"{surf_path.name}: cascade says CSG but no shape_{suffix}.root exists") + parts.append(part) + indexed_suffixes.add(suffix) + unscored = _unscored_leaf_solids(slug, out_dir, report, indexed_suffixes, cascade_by_lid) + return parts, warnings, unscored, cascade_meta + + +def build_db(models, output: Path, skip_existing: bool, force: bool, csg_mode: str = "auto", + mesh_prec: Optional[str] = None, include_name: Optional[list] = None): + output.mkdir(parents=True, exist_ok=True) + manifest = { + "version": 1, + "generated": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "output_dir": str(output.resolve()), + "csg_mode": csg_mode, + "mesh_prec": mesh_prec, + "include_name": include_name, + "models": [], + "parts": [], + # Leaf solids this database cannot hold; read by runOracleGate.py, ignored by the harness. + "unscoredParts": [], + } + + for model_arg in models: + model_path = _resolve_model(model_arg) + if not model_path.exists(): + raise RuntimeError(f"Model not found: {model_arg} (resolved to {model_path})") + slug = _slugify_model(model_path) + out_dir = output / slug + print(f"[{slug}] {model_path}") + + report, cmd = _convert_model(model_path, out_dir, skip_existing, force, csg_mode, + mesh_prec, include_name) + parts, warnings, unscored, cascade_meta = _index_parts( + model_path.name, slug, out_dir, report) + for w in warnings: + print(f" [warn] {w}") + + summary = report.get("summary", {}) + model_entry = { + "model": model_path.name, + "model_path": str(model_path), + "slug": slug, + "output_dir": str(out_dir.resolve()), + "command": cmd, + "surface_report": str((out_dir / "surface_report.json").resolve()), + "n_volumes": summary.get("n_volumes"), + "n_eligible": summary.get("n_eligible"), + "n_paired": len(parts), + "n_unscored": len(unscored), + "warnings": warnings, + } + if cascade_meta: + model_entry["csg_report"] = cascade_meta["path"] + model_entry["cascade_tiers"] = cascade_meta["tiers"] + model_entry["n_leaf_solids"] = cascade_meta["nLeafSolids"] + manifest["models"].append(model_entry) + manifest["parts"].extend(parts) + manifest["unscoredParts"].extend(unscored) + tier_counts = {} + for part in parts: + key = part["shipped"]["representation"] + tier_counts[key] = tier_counts.get(key, 0) + 1 + print(f" -> {len(parts)} parts paired (of {summary.get('n_eligible')} exact-eligible / " + f"{summary.get('n_volumes')} total volumes)") + print(f" shipped representation: " + + ", ".join(f"{k}={v}" for k, v in sorted(tier_counts.items())) + + (f"; {len(unscored)} leaf solid(s) not scoreable" if unscored else "")) + + manifest_path = output / "manifest.json" + manifest_path.write_text(json.dumps(manifest, indent=1)) + print(f"\nWrote {manifest_path} ({len(manifest['parts'])} parts across {len(manifest['models'])} models)") + return manifest + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--models", nargs="+", default=_DEFAULT_MODELS, + help="CAD model files (relative names are resolved against " + f"{_DEFAULT_MODEL_DIR}). Default: the models that ship in examples/.") + ap.add_argument("--output", default=str(_SCRIPT_DIR / "test_part_db"), + help="Database output directory (default: %(default)s)") + ap.add_argument("--skip-existing", action="store_true", + help="Reuse a model's output directory if already converted, re-indexing only.") + ap.add_argument("--force", action="store_true", + help="Delete and regenerate a model's output directory even if it exists.") + ap.add_argument("--csg", default="auto", choices=["off", "auto", "required"], + help="Converter CSG mode (default: %(default)s). 'auto' runs the production " + "cascade CSG -> exact surfaces -> tessellated and records the per-part " + "choice in csg_report.json, which is what the gate reads to decide which " + "representation each part's verdict is computed on. 'off' reproduces the " + "pre-cascade database.") + ap.add_argument("--include-name", action="append", default=None, + help="Passed to the converter: only convert CAD labels matching this regex. " + "May be repeated. Lets a database be built for one part of a module.") + ap.add_argument("--mesh-prec", default=None, + help="Meshing precision handed to the converter. Unset (default) means the " + "converter's own 0.1, which is what every database built before this " + "argument existed used, so an existing gate result does not move. Set it " + "for a model 0.1 is not safe on -- ALICE3 IRIS needs 0.25.") + args = ap.parse_args() + + build_db(args.models, Path(args.output), args.skip_existing, args.force, args.csg, + args.mesh_prec, args.include_name) + + +if __name__ == "__main__": + main() diff --git a/Detectors/CADSupport/validation/make_boolean_fixtures.py b/Detectors/CADSupport/validation/make_boolean_fixtures.py new file mode 100644 index 0000000000000..5be7bf6abd3ea --- /dev/null +++ b/Detectors/CADSupport/validation/make_boolean_fixtures.py @@ -0,0 +1,527 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-07 + +""" +Generate a ladder of small, fully-understood Boolean solids for debugging O2BVHSurfaceSolid and +its CAD converter against a known geometric feature instead of a large opaque CAD part. Each +fixture is written as a STEP file plus a `fixtures.json` manifest entry. + +Units +----- +Everything is modelled and written in MILLIMETRES, as real CAD exports are, which exercises the +converter's mm -> cm scaling. Volumes in the manifest are in cm^3 (cm^3 = mm^3 * 1e-3). + +The ladder +---------- +Fixtures 1-3 have only line and circle trims. Fixtures 4-6 (`cyl_cross_cyl`, `cyl_inter_cyl`, +`tube_window`) contain the transcendental intersection curve of two orthogonal cylinders, which no +per-face 2D trim reproduces exactly; fixture 6 reproduces ExcavatorArm's `BoomCylinderOuter` and +fixture 7 (`oblique_cut_cyl`, an exact ellipse) ExcavatorArm's `Bucket`. + +Usage +----- + python3 make_boolean_fixtures.py # generate the full ladder + python3 make_boolean_fixtures.py --list # print the ladder, generate nothing + python3 make_boolean_fixtures.py --only cyl_cross_cyl,tube_window + python3 make_boolean_fixtures.py --outdir /tmp/fixtures + +Requires the pythonOCC environment (same as O2_CADtoTGeo.py). The generated .step / +fixtures.json are build artifacts and are not meant to be committed. + +""" + +import argparse +import json +import math +import re +from pathlib import Path as _Path + +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common, BRepAlgoAPI_Cut, BRepAlgoAPI_Fuse +from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform +from OCC.Core.BRepCheck import BRepCheck_Analyzer +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.BRepPrimAPI import ( + BRepPrimAPI_MakeBox, + BRepPrimAPI_MakeCone, + BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere, + BRepPrimAPI_MakeTorus, +) +from OCC.Core.GProp import GProp_GProps +from OCC.Core.Interface import Interface_Static +from OCC.Core.STEPControl import STEPControl_AsIs, STEPControl_Writer +from OCC.Core.gp import gp_Ax1, gp_Ax2, gp_Dir, gp_Pnt, gp_Trsf +from OCC.Extend.TopologyUtils import TopologyExplorer + +_SCRIPT_DIR = _Path(__file__).resolve().parent +_DEFAULT_OUTDIR = _Path("boolean_fixtures") # relative to the working directory + +MM3_TO_CM3 = 1.0e-3 + + +# ------------------------------- +# small shape helpers (all lengths in mm) +# ------------------------------- + +def _box(dx, dy, dz, corner=(0.0, 0.0, 0.0)): + return BRepPrimAPI_MakeBox(gp_Pnt(*corner), dx, dy, dz).Shape() + + +def _cylinder(radius, height, origin=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0)): + ax = gp_Ax2(gp_Pnt(*origin), gp_Dir(*direction)) + return BRepPrimAPI_MakeCylinder(ax, radius, height).Shape() + + +def _cone(r1, r2, height, origin=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0)): + ax = gp_Ax2(gp_Pnt(*origin), gp_Dir(*direction)) + return BRepPrimAPI_MakeCone(ax, r1, r2, height).Shape() + + +def _sphere(radius, centre=(0.0, 0.0, 0.0)): + return BRepPrimAPI_MakeSphere(gp_Pnt(*centre), radius).Shape() + + +def _torus(major_r, minor_r, origin=(0.0, 0.0, 0.0), direction=(0.0, 0.0, 1.0)): + ax = gp_Ax2(gp_Pnt(*origin), gp_Dir(*direction)) + return BRepPrimAPI_MakeTorus(ax, major_r, minor_r).Shape() + + +def _rotated_translated(shape, axis_dir, angle_deg, translation): + """Rotate `shape` about the axis through the origin along `axis_dir`, then translate.""" + rot = gp_Trsf() + rot.SetRotation(gp_Ax1(gp_Pnt(0.0, 0.0, 0.0), gp_Dir(*axis_dir)), math.radians(angle_deg)) + tra = gp_Trsf() + tra.SetTranslation(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(*translation)) + return BRepBuilderAPI_Transform(shape, tra.Multiplied(rot), True).Shape() + + +def _boolean(op_class, a, b, what): + op = op_class(a, b) + op.Build() + if not op.IsDone(): + raise RuntimeError(f"{what}: boolean operation failed") + return op.Shape() + + +def _fuse(a, b): + return _boolean(BRepAlgoAPI_Fuse, a, b, "fuse") + + +def _common(a, b): + return _boolean(BRepAlgoAPI_Common, a, b, "common") + + +def _cut(a, b): + return _boolean(BRepAlgoAPI_Cut, a, b, "cut") + + +# ------------------------------- +# the fixture ladder +# ------------------------------- + +def build_box(): + return _box(20.0, 30.0, 40.0) + + +def build_box_union_box(): + # Two identical boxes side by side; they share the whole x = 20 face. + a = _box(20.0, 30.0, 40.0) + b = _box(20.0, 30.0, 40.0, corner=(20.0, 0.0, 0.0)) + return _fuse(a, b) + + +def build_box_minus_cyl(): + # 40 mm cube, axial through-hole of radius 8 mm along z. + cube = _box(40.0, 40.0, 40.0, corner=(-20.0, -20.0, -20.0)) + drill = _cylinder(8.0, 60.0, origin=(0.0, 0.0, -30.0)) + return _cut(cube, drill) + + +def build_cyl_cross_cyl(): + # Two r = 10 mm, L = 60 mm cylinders, axes z and x, both centred on the origin, FUSED. + cz = _cylinder(10.0, 60.0, origin=(0.0, 0.0, -30.0), direction=(0.0, 0.0, 1.0)) + cx = _cylinder(10.0, 60.0, origin=(-30.0, 0.0, 0.0), direction=(1.0, 0.0, 0.0)) + return _fuse(cz, cx) + + +def build_cyl_inter_cyl(): + # The same two cylinders, intersected: the Steinmetz solid. + cz = _cylinder(10.0, 60.0, origin=(0.0, 0.0, -30.0), direction=(0.0, 0.0, 1.0)) + cx = _cylinder(10.0, 60.0, origin=(-30.0, 0.0, 0.0), direction=(1.0, 0.0, 0.0)) + return _common(cz, cx) + + +def build_tube_window(): + # Tube r = 15 mm, h = 60 mm (axis z) with a transverse r = 8 mm hole drilled along x. + tube = _cylinder(15.0, 60.0, origin=(0.0, 0.0, -30.0), direction=(0.0, 0.0, 1.0)) + drill = _cylinder(8.0, 60.0, origin=(-30.0, 0.0, 0.0), direction=(1.0, 0.0, 0.0)) + return _cut(tube, drill) + + +def build_oblique_cut_cyl(): + # Cylinder r = 12 mm, h = 50 mm cut by a plane at 30 deg to its axis, lifted to z = 25 mm. + cyl = _cylinder(12.0, 50.0) + half_space = _box(400.0, 400.0, 400.0, corner=(-200.0, -200.0, 0.0)) + knife = _rotated_translated(half_space, (1.0, 0.0, 0.0), 60.0, (0.0, 0.0, 25.0)) + return _cut(cyl, knife) + + +def build_cyl_plus_cone(): + # Cylinder r = 10 mm, h = 30 mm with a coaxial truncated cone (10 -> 5, h = 20 mm) on top. + cyl = _cylinder(10.0, 30.0) + cone = _cone(10.0, 5.0, 20.0, origin=(0.0, 0.0, 30.0)) + return _fuse(cyl, cone) + + +def build_sphere_minus_cyl(): + # Sphere r = 20 mm with an axial r = 6 mm hole drilled through it (a "napkin ring"). + sph = _sphere(20.0) + drill = _cylinder(6.0, 60.0, origin=(0.0, 0.0, -30.0)) + return _cut(sph, drill) + + +def build_torus_union_cyl(): + # Torus R = 25 mm, r = 8 mm fused with a coaxial cylinder r = 20 mm, inside the tube band + # [17, 33] mm, so the junction curves are exact circles at z = +- sqrt(39) mm. + tor = _torus(25.0, 8.0) + cyl = _cylinder(20.0, 40.0, origin=(0.0, 0.0, -20.0)) + return _fuse(tor, cyl) + + +# Closed-form volumes, in mm^3, where one exists. +_V_BOX = 20.0 * 30.0 * 40.0 +_V_STEINMETZ = 16.0 * 10.0 ** 3 / 3.0 # two equal orthogonal cylinders, r = 10 + +FIXTURES = [ + { + "name": "box", + "build": build_box, + "description": "20 x 30 x 40 mm box.", + "feature": "trivial sanity case: 6 planar faces, all trim curves are straight segments", + "volume_mm3": _V_BOX, + }, + { + "name": "box_union_box", + "build": build_box_union_box, + "description": "Two 20 x 30 x 40 mm boxes fused along a shared full face at x = 20 mm.", + "feature": "coplanar/shared-face topology: the fuse must remove the internal face and " + "merge the two coplanar face pairs without leaving a seam", + "volume_mm3": 2.0 * _V_BOX, + }, + { + "name": "box_minus_cyl", + "build": build_box_minus_cyl, + "description": "40 mm cube minus an axial through-hole cylinder of radius 8 mm.", + "feature": "plane-cylinder trims: the hole's rim on each cap is an exact circle in the " + "plane's 2D chart and a full-turn iso-line in the cylinder's (phi, h) chart", + "volume_mm3": 40.0 ** 3 - math.pi * 8.0 ** 2 * 40.0, + }, + { + "name": "cyl_cross_cyl", + "build": build_cyl_cross_cyl, + "description": "Two r = 10 mm, L = 60 mm cylinders with orthogonal axes (z and x), " + "fused through a common centre.", + "feature": "THE key fixture: the union boundary contains the cylinder-cylinder " + "intersection curve h(phi) = +- sqrt(r^2 - R^2 sin^2 phi), which is " + "transcendental in each cylinder's own (phi, h) chart and therefore not " + "exactly representable by any per-face 2D trim curve", + "volume_mm3": 2.0 * math.pi * 10.0 ** 2 * 60.0 - _V_STEINMETZ, + }, + { + "name": "cyl_inter_cyl", + "build": build_cyl_inter_cyl, + "description": "The same two orthogonal r = 10 mm cylinders, intersected (Steinmetz " + "solid).", + "feature": "the entire boundary is the transcendental cylinder-cylinder intersection " + "curve: two cylindrical patches, four bi-arc edges, no planar face at all", + "volume_mm3": _V_STEINMETZ, + }, + { + "name": "tube_window", + "build": build_tube_window, + "description": "Cylinder r = 15 mm, h = 60 mm (axis z) minus a transverse r = 8 mm " + "cylinder (axis x) drilled through it.", + "feature": "minimized reproducer of the ExcavatorArm 'BoomCylinderOuter' failure: unequal-" + "radius orthogonal cylinder-cylinder intersection, transcendental in both " + "charts; the window rim closes over the tube's seam line", + # Volume is R^2*pi*h minus the intersection of two unequal orthogonal cylinders, which + # evaluates to a complete elliptic integral, not an elementary closed form. + "volume_mm3": None, + }, + { + "name": "oblique_cut_cyl", + "build": build_oblique_cut_cyl, + "description": "Cylinder r = 12 mm, h = 50 mm cut by a plane inclined at 30 deg to its " + "axis (cut via a large rotated box).", + "feature": "minimized reproducer of the ExcavatorArm 'Bucket' failure: the cut face is an " + "exact ELLIPSE (semi-axes 12 and 24 mm) -- a planar face whose trim is a " + "conic, not an arc; on the cylinder the same edge is a sinusoid in (phi, h)", + # The oblique plane crosses the lateral surface only (z in [25 - 12*tan60, 25 + 12*tan60] + # = [4.2, 45.8] mm), so the remaining volume is exactly pi r^2 times the axis height. + "volume_mm3": math.pi * 12.0 ** 2 * 25.0, + }, + { + "name": "cyl_plus_cone", + "build": build_cyl_plus_cone, + "description": "Cylinder r = 10 mm, h = 30 mm fused with a coaxial truncated cone " + "(r1 = 10 mm, r2 = 5 mm, h = 20 mm) stacked on top.", + "feature": "cylinder-cone junction across a shared circular rim: two different analytic " + "surface types meeting tangent-discontinuously with no intervening planar " + "face; the shared edge must be trimmed consistently in both charts", + "volume_mm3": math.pi * 10.0 ** 2 * 30.0 + + math.pi * 20.0 / 3.0 * (10.0 ** 2 + 10.0 * 5.0 + 5.0 ** 2), + }, + { + "name": "sphere_minus_cyl", + "build": build_sphere_minus_cyl, + "description": "Sphere r = 20 mm minus an axial r = 6 mm through-hole (napkin ring).", + "feature": "sphere-cylinder intersection: on the sphere the rim is a circle of constant " + "latitude (an iso-line in (theta, phi)), on the cylinder a full-turn " + "iso-line; exercises two curved charts meeting with no planar face", + # Napkin ring: V = 4/3 pi (R^2 - a^2)^(3/2). + "volume_mm3": 4.0 / 3.0 * math.pi * (20.0 ** 2 - 6.0 ** 2) ** 1.5, + }, + { + "name": "torus_union_cyl", + "build": build_torus_union_cyl, + "description": "Torus (R = 25 mm, r = 8 mm) fused with a coaxial cylinder r = 20 mm, " + "h = 40 mm passing through its centre (see build_torus_union_cyl for why " + "the cylinder is 20 mm and not 10 mm).", + "feature": "toroidal (quartic) surface trimmed by its junction with a cylinder; being " + "coaxial the junction curves are exact circles, so this isolates the torus " + "surface/ray-intersection code from transcendental trimming", + "volume_mm3": None, + }, +] + +_BY_NAME = {f["name"]: f for f in FIXTURES} + + +# ------------------------------- +# measurement and export +# ------------------------------- + +def shape_volume_cm3(shape): + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return props.Mass() * MM3_TO_CM3 + + +def shape_counts(shape): + topo = TopologyExplorer(shape) + return topo.number_of_faces(), topo.number_of_edges(), topo.number_of_solids() + + +def write_step_mm(shape, path: _Path, product_name: str): + """Write `shape` to `path` as a STEP file with LENGTH_UNIT = millimetre. + + `product_name` becomes the STEP PRODUCT name, i.e. the XCAF label name the converter picks up. + """ + writer = STEPControl_Writer() + # NB: the write.step.* static parameters only exist once a STEP writer has been created. + Interface_Static.SetCVal("write.step.unit", "MM") + Interface_Static.SetCVal("write.step.product.name", product_name) + writer.Transfer(shape, STEPControl_AsIs) + status = writer.Write(str(path)) + if status != 1: # IFSelect_RetDone + raise RuntimeError(f"STEP write failed for {path} (status {status})") + + # Drop OCCT's process-global counter from the product name, so the name is stable under --only. + text = path.read_text(encoding="latin-1") + text = re.sub(rf"'{re.escape(product_name)} \d+'", f"'{product_name}'", text) + path.write_text(text, encoding="latin-1") + + +# ------------------------------- +# the --transform sweep +# ------------------------------- +# The ladder is moved and scaled in the STEP itself, since the kernel's constants are absolute. + + +def parse_transform(spec: str): + """Parse a transform spec into (gp_Trsf, volume scale factor, canonical description). + + Accepted forms (lengths in mm, i.e. STEP model units): + translate:,, + scale: uniform scaling about the origin + ;;... composition, applied left to right + """ + if ";" in spec: + total = gp_Trsf() + volume_scale = 1.0 + descriptions = [] + for part in spec.split(";"): + if not part.strip(): + continue + trsf, part_scale, description = parse_transform(part) + total = trsf.Multiplied(total) # applied after everything parsed so far + volume_scale *= part_scale + descriptions.append(description) + return total, volume_scale, ";".join(descriptions) + kind, _, rest = spec.partition(":") + kind = kind.strip().lower() + trsf = gp_Trsf() + if kind == "translate": + parts = [float(v) for v in rest.split(",")] + if len(parts) != 3: + raise ValueError(f"translate needs three components, got {rest!r}") + trsf.SetTranslation(gp_Pnt(0.0, 0.0, 0.0), gp_Pnt(*parts)) + return trsf, 1.0, f"translate:{parts[0]:g},{parts[1]:g},{parts[2]:g}" + if kind == "scale": + factor = float(rest) + if factor <= 0.0: + raise ValueError(f"scale factor must be positive, got {factor}") + trsf.SetScale(gp_Pnt(0.0, 0.0, 0.0), factor) + return trsf, factor ** 3, f"scale:{factor:g}" + raise ValueError(f"unknown transform {spec!r}; expected 'translate:x,y,z' or 'scale:f'") + + +def generate(fixture, outdir: _Path, transform=None): + name = fixture["name"] + shape = fixture["build"]() + volume_scale = 1.0 + transform_desc = None + if transform is not None: + trsf, volume_scale, transform_desc = transform + # copy=True: a scaling gp_Trsf cannot share geometry with the untransformed poles. + shape = BRepBuilderAPI_Transform(shape, trsf, True).Shape() + step_path = (outdir / f"{name}.step").resolve() + write_step_mm(shape, step_path, name) + + n_faces, n_edges, n_solids = shape_counts(shape) + valid = bool(BRepCheck_Analyzer(shape).IsValid()) + occt_volume = shape_volume_cm3(shape) + expected = fixture["volume_mm3"] + expected_cm3 = None if expected is None else expected * MM3_TO_CM3 * volume_scale + + entry = { + "name": name, + "step": str(step_path), + "transform": transform_desc, + "description": fixture["description"], + "feature": fixture["feature"], + "units": "mm (STEP) / cm^3 (volumes)", + "expected_volume_cm3": expected_cm3, + "occt_volume_cm3": occt_volume, + "volume_rel_error": (None if expected_cm3 in (None, 0.0) + else abs(occt_volume - expected_cm3) / abs(expected_cm3)), + "n_faces": n_faces, + "n_edges": n_edges, + "n_solids": n_solids, + "valid": valid, + } + return entry + + +def print_summary_line(entry): + exp = entry["expected_volume_cm3"] + if exp is None: + vol = f"V={entry['occt_volume_cm3']:11.5f} cm^3 (no closed form)" + else: + vol = (f"V={entry['occt_volume_cm3']:11.5f} cm^3 " + f"(analytic {exp:11.5f}, rel.err {entry['volume_rel_error']:.2e})") + print(f" {entry['name']:<18s} faces={entry['n_faces']:3d} edges={entry['n_edges']:3d} " + f"solids={entry['n_solids']:2d} {vol} valid={str(entry['valid']).lower()}") + + +def print_ladder(): + print("Boolean fixture ladder (increasing difficulty):") + for i, f in enumerate(FIXTURES, 1): + exp = f["volume_mm3"] + exp_s = "n/a" if exp is None else f"{exp * MM3_TO_CM3:.5f} cm^3" + print(f"{i:3d}. {f['name']}") + print(f" {f['description']}") + print(f" exercises: {f['feature']}") + print(f" analytic volume: {exp_s}") + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--outdir", default=str(_DEFAULT_OUTDIR), + help="Directory for the generated .step files and fixtures.json " + "(default: %(default)s)") + ap.add_argument("--only", default=None, + help="Comma-separated fixture names to (re)generate instead of the full " + "ladder. The manifest is rewritten from the regenerated subset merged " + "with any previously generated entries.") + ap.add_argument("--list", action="store_true", + help="Print the fixture ladder and exit without generating anything.") + ap.add_argument("--transform", default=None, + help="Apply a transform to every fixture before export, for the " + "position/scale sweep: 'translate:dx,dy,dz' (mm) or 'scale:f' (uniform, " + "about the origin). Omitted, nothing is applied and the output is " + "byte-identical to an untransformed run.") + args = ap.parse_args() + + if args.list: + print_ladder() + return + + if args.only: + wanted = [n.strip() for n in args.only.split(",") if n.strip()] + unknown = [n for n in wanted if n not in _BY_NAME] + if unknown: + raise SystemExit(f"Unknown fixture name(s): {', '.join(unknown)}\n" + f"Known: {', '.join(_BY_NAME)}") + selected = [_BY_NAME[n] for n in wanted] + else: + selected = list(FIXTURES) + + outdir = _Path(args.outdir).expanduser().resolve() + outdir.mkdir(parents=True, exist_ok=True) + manifest_path = outdir / "fixtures.json" + + previous = {} + if manifest_path.exists(): + try: + old = json.loads(manifest_path.read_text()) + previous = {e["name"]: e for e in old.get("fixtures", [])} + except (ValueError, KeyError): + previous = {} + + transform = parse_transform(args.transform) if args.transform else None + suffix = "" if transform is None else f", transform: {transform[2]}" + print(f"Generating {len(selected)} fixture(s) into {outdir} (STEP unit: MM{suffix})") + entries = {} + for fixture in selected: + entry = generate(fixture, outdir, transform) + entries[fixture["name"]] = entry + print_summary_line(entry) + + # Keep entries of fixtures that were not regenerated in this run, as long as their STEP + # file is still there. + merged = [] + for f in FIXTURES: + entry = entries.get(f["name"]) or previous.get(f["name"]) + if entry and _Path(entry["step"]).exists(): + merged.append(entry) + + n_invalid = sum(1 for e in merged if not e["valid"]) + manifest = { + "version": 1, + "generator": str(_Path(__file__).resolve()), + "step_length_unit": "mm", + "volume_unit": "cm^3", + "transform": None if transform is None else transform[2], + "outdir": str(outdir), + "fixtures": merged, + } + manifest_path.write_text(json.dumps(manifest, indent=1)) + print(f"\nWrote {manifest_path} ({len(merged)} fixtures, {n_invalid} invalid)") + + +if __name__ == "__main__": + main() diff --git a/Detectors/CADSupport/validation/occtOracle.py b/Detectors/CADSupport/validation/occtOracle.py new file mode 100644 index 0000000000000..940b5afd00562 --- /dev/null +++ b/Detectors/CADSupport/validation/occtOracle.py @@ -0,0 +1,476 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-07 + +"""OpenCascade reference oracle for the exact-surface solid (O2BVHSurfaceSolid). + +It answers the kernel's questions about the *same* BREP the converter read, with explicit tolerance +semantics: far too slow to navigate with (milliseconds per query, not thread-safe), fine for an +oracle. + + +What it answers +--------------- +For a solid loaded from a `.brep` file (in cm; written by `O2_CADtoTGeo.py --dump-brep`) and a +sample set dumped by the C++ harness (`o2-bench-cadsupport-solid-harness --dump-samples`): + + contains BRepClass3d_SolidClassifier 1 = inside, 0 = outside, -1 = ON (no verdict) + distFromOutside IntCurvesFace_ShapeIntersector nearest positive ray/shell crossing + distFromInside IntCurvesFace_ShapeIntersector same call; the origin is inside instead + safetyUpperBound BRepExtrema_DistShapeShape true distance to the boundary + capacity BRepGProp exact volume + tolerance max BRep_Tool::Tolerance the model's own declared ambiguity band + +`distFromOutside` and `distFromInside` are deliberately the *same* computation: the nearest +positive intersection of the ray with the shell. Entering versus exiting is a property of where +the origin is, not of the intersector, so no face-orientation bookkeeping is needed and the +oracle cannot get it subtly wrong. The origin's own classification is reported alongside each +answer so the consumer can check that assumption rather than trust it. + +Tolerance semantics (important when comparing) +---------------------------------------------- +OCCT is a *tolerant* modeller: every face, edge and vertex carries its own tolerance, and a point +is ON when it is within that distance of the boundary. Imported CAD routinely carries 1e-5 cm or +worse. So the honest comparison rule is: a disagreement is only meaningful when the query point +is further than the model tolerance from the boundary. The oracle reports `tolerance` (the max +over the shape's sub-shapes) so the consumer can apply exactly that rule instead of inventing a +band. + +Usage +----- + answer a sample set: + occtOracle.py --brep part.brep --samples samples.json --out answers.json + check the oracle itself against closed-form geometry (no inputs needed): + occtOracle.py --self-test + +Environment: an interpreter that can import OCC, for example + alienv setenv pythonOCC/latest -c python3 occtOracle.py ... +""" + +import argparse +import json +import math +import sys +import time +from pathlib import Path + +from OCC.Core.BRep import BRep_Tool, BRep_Builder +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut +from OCC.Core.BRepBndLib import brepbndlib +from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex +from OCC.Core.BRepCheck import BRepCheck_Analyzer +from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder +from OCC.Core.Bnd import Bnd_Box +from OCC.Core.GProp import GProp_GProps +from OCC.Core.IntCurvesFace import IntCurvesFace_ShapeIntersector +from OCC.Core.TopAbs import (TopAbs_EDGE, TopAbs_FACE, TopAbs_IN, TopAbs_ON, TopAbs_OUT, + TopAbs_SOLID, TopAbs_VERTEX) +from OCC.Core.TopExp import TopExp_Explorer +from OCC.Core.TopoDS import topods +from OCC.Core.BRepTools import breptools +from OCC.Core.gp import gp_Dir, gp_Lin, gp_Pnt + +# The sample/answer JSON contract version. Bump together with the C++ harness writer/reader. +ORACLE_FORMAT_VERSION = 1 + +# A ray parameter this close to the origin is the origin itself, not a crossing. Matches the +# kernel's kRayTolerance so "starts exactly on a face" is treated the same way on both sides. +_RAY_EPS = 1.0e-9 + +# TGeoShape::Big(); the harness uses it for "no intersection". +_BIG = 1.0e30 + + +# ---------------------------------------------------------------------------------------------- +# Shape loading and interrogation +# ---------------------------------------------------------------------------------------------- + +def load_solid(path: Path): + """Read a .brep file and return the single TopoDS_Solid it contains. + + A BREP file can hold a compound; the converter writes exactly one leaf solid per file, so + anything else is a genuine inconsistency and must fail loudly rather than pick a shape. + """ + shape = TopoDS_Shape_read(path) + solids = [] + explorer = TopExp_Explorer(shape, TopAbs_SOLID) + while explorer.More(): + solids.append(topods.Solid(explorer.Current())) + explorer.Next() + if len(solids) == 1: + return solids[0] + if not solids: + raise RuntimeError(f"{path}: contains no TopoDS_Solid (a shell or compound of faces?)") + raise RuntimeError(f"{path}: contains {len(solids)} solids, expected exactly one") + + +def TopoDS_Shape_read(path: Path): + from OCC.Core.TopoDS import TopoDS_Shape + shape = TopoDS_Shape() + builder = BRep_Builder() + if not breptools.Read(shape, str(path), builder): + raise RuntimeError(f"{path}: BRepTools::Read failed") + if shape.IsNull(): + raise RuntimeError(f"{path}: read a null shape") + return shape + + +def shape_tolerance(shape) -> float: + """Max BRep_Tool tolerance over faces, edges and vertices. + + This is the model's own statement about how well its boundary is defined, and therefore the + only defensible width for a "no verdict" band when comparing against it. + """ + worst = 0.0 + for shape_type, getter in ((TopAbs_FACE, lambda s: BRep_Tool.Tolerance(topods.Face(s))), + (TopAbs_EDGE, lambda s: BRep_Tool.Tolerance(topods.Edge(s))), + (TopAbs_VERTEX, lambda s: BRep_Tool.Tolerance(topods.Vertex(s)))): + explorer = TopExp_Explorer(shape, shape_type) + while explorer.More(): + worst = max(worst, getter(explorer.Current())) + explorer.Next() + return worst + + +def shape_bbox(shape): + box = Bnd_Box() + brepbndlib.Add(shape, box) + xmin, ymin, zmin, xmax, ymax, zmax = box.Get() + return [xmin, ymin, zmin], [xmax, ymax, zmax] + + +def count_subshapes(shape, shape_type) -> int: + count = 0 + explorer = TopExp_Explorer(shape, shape_type) + while explorer.More(): + count += 1 + explorer.Next() + return count + + +def _shells_of(solid): + """The solid's boundary as a shape distances can be measured against. + + Returned as a compound so a solid with inner voids keeps all of its shells; measuring against + the solid itself would report 0 for every interior point. + """ + from OCC.Core.TopAbs import TopAbs_SHELL + from OCC.Core.TopoDS import TopoDS_Compound + compound = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(compound) + shells = 0 + explorer = TopExp_Explorer(solid, TopAbs_SHELL) + while explorer.More(): + builder.Add(compound, explorer.Current()) + shells += 1 + explorer.Next() + if shells == 0: + raise RuntimeError("solid has no shell; cannot measure boundary distances") + return compound + + +def volume_of(shape) -> float: + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return props.Mass() + + +# ---------------------------------------------------------------------------------------------- +# The four query kernels +# ---------------------------------------------------------------------------------------------- + +class Oracle: + """Stateful wrapper around the OCCT algorithms, so the expensive setup happens once. + + Not thread-safe -- OCCT classifiers and intersectors carry mutable state. That is fine here + (an oracle run is a batch job) but it is exactly why this cannot be a navigation kernel. + """ + + def __init__(self, solid, classifier_tolerance: float = _RAY_EPS): + self.solid = solid + self.classifier_tolerance = classifier_tolerance + self.classifier = BRepClass3d_SolidClassifier(solid) + self.intersector = IntCurvesFace_ShapeIntersector() + self.intersector.Load(solid, _RAY_EPS) + # Distances are measured against the shells: a point inside a solid is 0 away from it. + self.boundary = _shells_of(solid) + + def contains(self, point) -> int: + """1 = inside, 0 = outside, -1 = ON the boundary within the classifier tolerance.""" + self.classifier.Perform(gp_Pnt(*point), self.classifier_tolerance) + state = self.classifier.State() + if state == TopAbs_IN: + return 1 + if state == TopAbs_OUT: + return 0 + if state == TopAbs_ON: + return -1 + raise RuntimeError(f"unexpected classifier state {state} at {point}") + + def nearest_crossing(self, origin, direction) -> float: + """Nearest strictly-positive ray/shell crossing, or _BIG when the ray misses. + + This is the answer to *both* DistFromOutside and DistFromInside: whether the crossing is + an entry or an exit is decided by where the origin lies, not by this computation. + """ + norm = math.sqrt(sum(component * component for component in direction)) + if norm <= 0.0: + raise ValueError(f"degenerate ray direction {direction}") + unit = [component / norm for component in direction] + line = gp_Lin(gp_Pnt(*origin), gp_Dir(*unit)) + self.intersector.Perform(line, _RAY_EPS, _BIG) + if not self.intersector.IsDone() or self.intersector.NbPnt() == 0: + return _BIG + best = _BIG + for index in range(1, self.intersector.NbPnt() + 1): + parameter = self.intersector.WParameter(index) + if parameter > _RAY_EPS: + best = min(best, parameter) + return best + + def distance_to_boundary(self, point) -> float: + """True distance from a point to the solid's boundary (its shell), always >= 0. + + This is the upper bound a correct Safety() must not exceed, for points inside *and* + outside: BRepExtrema measures against the faces, not against the solid's interior. + """ + vertex = BRepBuilderAPI_MakeVertex(gp_Pnt(*point)).Vertex() + extrema = BRepExtrema_DistShapeShape(vertex, self.boundary) + if not extrema.IsDone(): + raise RuntimeError(f"BRepExtrema failed at {point}") + return extrema.Value() + + +# ---------------------------------------------------------------------------------------------- +# Sample-set driving +# ---------------------------------------------------------------------------------------------- + +def answer_samples(oracle: Oracle, samples: dict, distance_limit: int, verbose: bool) -> dict: + """Answer every point and ray in `samples`, following the harness's category names.""" + answers = {"contains": {}, "originContains": {}, "distFromOutside": {}, + "distFromInside": {}, "safetyUpperBound": {}} + timing = {} + + point_categories = samples.get("points", {}) + for category, points in point_categories.items(): + start = time.monotonic() + answers["contains"][category] = [oracle.contains(p) for p in points] + timing[f"contains/{category}"] = time.monotonic() - start + if verbose: + print(f" contains/{category}: {len(points)} points " + f"({timing[f'contains/{category}']:.1f} s)", flush=True) + + # The exact distance is the costliest query, so it is capped, and the count is reported. + limited = points if distance_limit <= 0 else points[:distance_limit] + start = time.monotonic() + answers["safetyUpperBound"][category] = [oracle.distance_to_boundary(p) for p in limited] + timing[f"safety/{category}"] = time.monotonic() - start + if verbose: + print(f" safetyUpperBound/{category}: {len(limited)}/{len(points)} points " + f"({timing[f'safety/{category}']:.1f} s)", flush=True) + + ray_categories = samples.get("rays", {}) + for category, rays in ray_categories.items(): + start = time.monotonic() + distances = [] + origin_states = [] + for ray in rays: + origin, direction = ray["o"], ray["d"] + origin_states.append(oracle.contains(origin)) + distances.append(oracle.nearest_crossing(origin, direction)) + # One column per category; which TGeo entry point it corresponds to is decided by the + # origin state, which is reported next to it rather than assumed from the category name. + target = "distFromInside" if category.startswith("inside") else "distFromOutside" + answers[target][category] = distances + answers["originContains"][category] = origin_states + timing[f"{target}/{category}"] = time.monotonic() - start + if verbose: + inside_count = sum(1 for s in origin_states if s == 1) + print(f" {target}/{category}: {len(rays)} rays, {inside_count} origins inside " + f"({timing[f'{target}/{category}']:.1f} s)", flush=True) + + answers["timingSeconds"] = timing + return answers + + +def build_answer_document(brep_path: Path, samples: dict, distance_limit: int, + verbose: bool) -> dict: + solid = load_solid(brep_path) + analyzer = BRepCheck_Analyzer(solid) + bbox_min, bbox_max = shape_bbox(solid) + document = { + "version": ORACLE_FORMAT_VERSION, + "oracle": "OpenCascade", + "brep": str(brep_path), + "part": samples.get("part"), + "valid": bool(analyzer.IsValid()), + "tolerance": shape_tolerance(solid), + "capacity": volume_of(solid), + "nFaces": count_subshapes(solid, TopAbs_FACE), + "nEdges": count_subshapes(solid, TopAbs_EDGE), + "bboxMin": bbox_min, + "bboxMax": bbox_max, + "distanceLimit": distance_limit, + } + if verbose: + print(f"{brep_path.name}: valid={document['valid']} tolerance={document['tolerance']:.3e} " + f"volume={document['capacity']:.6g} cm^3 faces={document['nFaces']}", flush=True) + if not document["valid"]: + # Not fatal, but a broken reference must never pass unnoticed into a comparison. + print(f"WARNING: {brep_path} is not BRepCheck-valid; its answers are not authoritative", + file=sys.stderr) + oracle = Oracle(solid) + document.update(answer_samples(oracle, samples, distance_limit, verbose)) + return document + + +# ---------------------------------------------------------------------------------------------- +# Self-test: the oracle must be checked before anything is judged by it +# ---------------------------------------------------------------------------------------------- + +def self_test() -> int: + """Check every kernel against closed-form answers; needs no input files, so it can gate CI.""" + failures = [] + + def check(name, got, expected, tolerance): + deviation = abs(got - expected) + ok = deviation <= tolerance + print(f" [{'ok' if ok else 'FAIL'}] {name}: got {got:.12g}, expected {expected:.12g} " + f"(dev {deviation:.3g}, tol {tolerance:g})") + if not ok: + failures.append(name) + + def check_int(name, got, expected): + ok = got == expected + print(f" [{'ok' if ok else 'FAIL'}] {name}: got {got}, expected {expected}") + if not ok: + failures.append(name) + + print("Self-test 1: axis-aligned box 2 x 4 x 6 at the origin corner") + box = BRepPrimAPI_MakeBox(2.0, 4.0, 6.0).Solid() + oracle = Oracle(box) + check("box volume", volume_of(box), 48.0, 1e-9) + check_int("box contains centre", oracle.contains([1.0, 2.0, 3.0]), 1) + check_int("box contains outside point", oracle.contains([5.0, 2.0, 3.0]), 0) + check_int("box contains face point", oracle.contains([0.0, 2.0, 3.0]), -1) + # A ray from outside along +x through the centre: enters at x=0, so the distance is 3. + check("box distance from outside", oracle.nearest_crossing([-3.0, 2.0, 3.0], [1.0, 0.0, 0.0]), + 3.0, 1e-9) + # The same ray started inside at x=1 exits at x=2. + check("box distance from inside", oracle.nearest_crossing([1.0, 2.0, 3.0], [1.0, 0.0, 0.0]), + 1.0, 1e-9) + check("box ray miss", oracle.nearest_crossing([-3.0, 20.0, 3.0], [1.0, 0.0, 0.0]), _BIG, 0.0) + # Nearest face from an interior point at (1,2,3) is x=0 or x=2, both 1 away. + check("box distance to boundary (inside)", oracle.distance_to_boundary([1.0, 2.0, 3.0]), + 1.0, 1e-9) + check("box distance to boundary (outside)", oracle.distance_to_boundary([5.0, 2.0, 3.0]), + 3.0, 1e-9) + + print("Self-test 2: cylinder r=3 h=10 along +z from the origin") + cylinder = BRepPrimAPI_MakeCylinder(3.0, 10.0).Solid() + oracle = Oracle(cylinder) + check("cylinder volume", volume_of(cylinder), math.pi * 9.0 * 10.0, 1e-6) + check_int("cylinder contains axis point", oracle.contains([0.0, 0.0, 5.0]), 1) + check_int("cylinder contains outside point", oracle.contains([4.0, 0.0, 5.0]), 0) + # Radial ray from outside enters the curved wall at r=3. + check("cylinder radial entry", oracle.nearest_crossing([10.0, 0.0, 5.0], [-1.0, 0.0, 0.0]), + 7.0, 1e-9) + # From the axis outwards, the exit is the wall at r=3. + check("cylinder radial exit", oracle.nearest_crossing([0.0, 0.0, 5.0], [1.0, 0.0, 0.0]), + 3.0, 1e-9) + # A ray exactly tangent to the wall must not be reported as a crossing at a shorter distance + # than the cap it actually reaches; this is the configuration that breaks naive intersectors. + tangent = oracle.nearest_crossing([3.0, -10.0, 5.0], [0.0, 1.0, 0.0]) + print(f" [info] cylinder tangent ray -> {tangent:.12g} " + f"({'grazes' if tangent < _BIG else 'misses'}; either is defensible)") + check("cylinder distance to boundary on axis", oracle.distance_to_boundary([0.0, 0.0, 5.0]), + 3.0, 1e-9) + + print("Self-test 3: box with a drilled hole (a boundary that is not convex)") + plate = BRepPrimAPI_MakeBox(gp_Pnt(-5.0, -5.0, 0.0), 10.0, 10.0, 2.0).Solid() + drill = BRepPrimAPI_MakeCylinder(2.0, 10.0).Solid() + drilled = BRepAlgoAPI_Cut(plate, drill).Shape() + solid = load_solid_from_shape(drilled) + oracle = Oracle(solid) + check("drilled plate volume", volume_of(solid), 10.0 * 10.0 * 2.0 - math.pi * 4.0 * 2.0, 1e-6) + check_int("hole centre is outside the material", oracle.contains([0.0, 0.0, 1.0]), 0) + check_int("material point is inside", oracle.contains([4.0, 0.0, 1.0]), 1) + # Crossing the plate through the hole: from x=-10 the first material is the hole wall at + # x=-5 (the outer face), then the hole starts at x=-2. + check("drilled plate first crossing", oracle.nearest_crossing([-10.0, 0.0, 1.0], [1.0, 0.0, 0.0]), + 5.0, 1e-9) + # Starting inside the hole, the nearest boundary going +x is the hole wall at x=2. + check("crossing out of the hole", oracle.nearest_crossing([0.0, 0.0, 1.0], [1.0, 0.0, 0.0]), + 2.0, 1e-9) + + print() + if failures: + print(f"SELF-TEST FAILED: {len(failures)} check(s): {', '.join(failures)}") + return 1 + print("SELF-TEST PASSED: every kernel matches closed-form geometry") + return 0 + + +def load_solid_from_shape(shape): + solids = [] + explorer = TopExp_Explorer(shape, TopAbs_SOLID) + while explorer.More(): + solids.append(topods.Solid(explorer.Current())) + explorer.Next() + if len(solids) != 1: + raise RuntimeError(f"expected exactly one solid, got {len(solids)}") + return solids[0] + + +# ---------------------------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--brep", type=Path, help="solid to answer for, in cm") + parser.add_argument("--samples", type=Path, help="sample set dumped by the C++ harness") + parser.add_argument("--out", type=Path, help="where to write the answers JSON") + parser.add_argument("--distance-limit", type=int, default=2000, + help="max points per category to compute the exact boundary distance for " + "(the most expensive query); 0 means no limit (default: 2000)") + parser.add_argument("--self-test", action="store_true", + help="validate the oracle's own kernels against closed-form geometry") + parser.add_argument("--quiet", action="store_true", help="suppress per-category progress") + args = parser.parse_args() + + if args.self_test: + return self_test() + + if not (args.brep and args.samples and args.out): + parser.error("--brep, --samples and --out are required unless --self-test is given") + + samples = json.loads(args.samples.read_text()) + version = samples.get("version") + if version != ORACLE_FORMAT_VERSION: + raise RuntimeError(f"{args.samples}: sample format version {version}, " + f"this oracle speaks {ORACLE_FORMAT_VERSION}") + + document = build_answer_document(args.brep, samples, args.distance_limit, not args.quiet) + args.out.write_text(json.dumps(document, indent=1)) + if not args.quiet: + print(f"Wrote {args.out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/overlapCensus.py b/Detectors/CADSupport/validation/overlapCensus.py new file mode 100644 index 0000000000000..f66434912b428 --- /dev/null +++ b/Detectors/CADSupport/validation/overlapCensus.py @@ -0,0 +1,517 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Is this CAD assembly a LEGAL geometry for TGeo / Geant4? Measure it, pair by pair. + +For every pair of PLACED solids: + +1. **AABB rejection** first, which makes N^2 affordable. +2. `BRepExtrema_DistShapeShape` on the survivors: a positive distance settles the pair as + **disjoint, by this much**. +3. `BRepAlgoAPI_Common` only where the distance is zero, i.e. where the pair touches or + interpenetrates. Its **volume** is the discriminator: + * volume ~ 0 -> **coincident faces**. Touching. This is the NORMAL case for an assembly and + it is legal for TGeo, which tolerates shared boundaries. + * volume > 0 -> **real interpenetration**. Illegal. Reported with the fraction of the smaller + part it eats, the bounding box of the shared region (so it can be found), and a sampled + maximum penetration depth. + * volume ~ volume of the smaller part -> **containment**. Legal *if* the hierarchy declares it + mother/daughter, illegal if both are placed as siblings -- which is what a flat CAD-to-TGeo + conversion does. Reported separately because the fix is different. + +Usage +----- + overlapCensus.py --self-test + overlapCensus.py --step Detectors/CADSupport/examples/ExcavatorArm.step --out excavator_arm.json + overlapCensus.py --step .../CAD_noETA.stp --out alice3.json --max-pairs 4000 +""" + +import argparse +import itertools +import json +import re +import math +import sys +import time +from pathlib import Path + +import cadsupport_path # noqa: E402,F401 (puts ../tools on sys.path) + +from cadsupport.occ_env import ensure_occ + +ensure_occ() + +from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common +from OCC.Core.BRepBndLib import brepbndlib +from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_MakeVertex +from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.BRepExtrema import BRepExtrema_DistShapeShape +from OCC.Core.BRepGProp import brepgprop +from OCC.Core.Bnd import Bnd_Box +from OCC.Core.GProp import GProp_GProps +from OCC.Core.TopAbs import TopAbs_IN, TopAbs_SOLID, TopAbs_VERTEX +from OCC.Core.TopExp import TopExp_Explorer +from OCC.Core.gp import gp_Pnt + +from assemblyOracle import Part, assembly_from_shapes, load_assembly + +CENSUS_FORMAT_VERSION = 1 + + +def volume_of(shape) -> float: + props = GProp_GProps() + brepgprop.VolumeProperties(shape, props) + return abs(props.Mass()) + + +def bbox_of(shape, tight=False): + """`Bnd_Box.Add` inflates by the shape's own tolerance, which is right for a rejection test + and wrong for a measurement -- it made a 0.5 cm shared slab read 0.5000002 cm. `AddOptimal` + is the measurement version; the rejection stays conservative on purpose.""" + box = Bnd_Box() + if tight: + box.SetGap(0.0) + brepbndlib.AddOptimal(shape, box, True, False) + else: + brepbndlib.Add(shape, box) + return None if box.IsVoid() else box.Get() + + +def surface_of(shape): + """The shape's boundary as a compound of faces. + + `BRepExtrema_DistShapeShape(vertex, solid)` is 0 for a point inside the solid, so penetration + depth must be measured against the boundary. + """ + from OCC.Core.BRep import BRep_Builder + from OCC.Core.TopAbs import TopAbs_FACE + from OCC.Core.TopoDS import TopoDS_Compound + compound = TopoDS_Compound() + builder = BRep_Builder() + builder.MakeCompound(compound) + explorer = TopExp_Explorer(shape, TopAbs_FACE) + while explorer.More(): + builder.Add(compound, explorer.Current()) + explorer.Next() + return compound + + +def boxes_overlap(a, b, pad): + if a is None or b is None: + return False, 0.0 + volume = 1.0 + for k in range(3): + lo = max(a[k], b[k]) + hi = min(a[k + 3], b[k + 3]) + if hi < lo - pad: + return False, 0.0 + volume *= max(0.0, hi - lo) + return True, volume + + +def distance_between(a, b): + """Minimum separation of two placed shapes. Zero means touching OR interpenetrating; the two + are told apart by the boolean, never by this number.""" + tool = BRepExtrema_DistShapeShape(a, b) + if not tool.IsDone(): + tool.Perform() + if not tool.IsDone(): + return None + return tool.Value() + + +def distance_to_surface(point: gp_Pnt, shape): + vertex = BRepBuilderAPI_MakeVertex(point).Vertex() + tool = BRepExtrema_DistShapeShape(vertex, shape) + if not tool.IsDone(): + tool.Perform() + return tool.Value() if tool.IsDone() else None + + +def penetration_depth(common, surface_a, surface_b, grid=6, budget=120): + """Max over sampled interior points of the shared region of min(dist to A's surface, + dist to B's surface). + + A sampled **lower bound**; the shared region's bounding-box diagonal is returned as the upper. + """ + box = bbox_of(common, tight=True) + if box is None: + return 0.0, 0.0, 0 + diag = math.sqrt(sum((box[k + 3] - box[k]) ** 2 for k in range(3))) + + points = [] + explorer = TopExp_Explorer(common, TopAbs_VERTEX) + seen = set() + while explorer.More(): + from OCC.Core.BRep import BRep_Tool + from OCC.Core.TopoDS import topods + p = BRep_Tool.Pnt(topods.Vertex(explorer.Current())) + key = (round(p.X(), 9), round(p.Y(), 9), round(p.Z(), 9)) + if key not in seen: + seen.add(key) + points.append(p) + explorer.Next() + + classifier = BRepClass3d_SolidClassifier(common) + for i in range(grid): + for j in range(grid): + for k in range(grid): + p = gp_Pnt(box[0] + (i + 0.5) * (box[3] - box[0]) / grid, + box[1] + (j + 0.5) * (box[4] - box[1]) / grid, + box[2] + (k + 0.5) * (box[5] - box[2]) / grid) + classifier.Perform(p, 1e-9) + if classifier.State() == TopAbs_IN: + points.append(p) + points = points[:budget] + + best = 0.0 + for p in points: + da = distance_to_surface(p, surface_a) + db = distance_to_surface(p, surface_b) + if da is None or db is None: + continue + best = max(best, min(da, db)) + return best, diag, len(points) + + +def census(parts, scale, pad_cm=0.1, zero_distance_cm=1.0e-9, zero_volume_cm3=1.0e-12, + max_pairs=0, verbose=True, deep=True): + """The full pairwise census. All reported lengths are cm, volumes cm^3. + + `pad_cm` inflates every bounding box before the rejection test. It does not change which pairs + overlap; it decides which disjoint pairs get their separation measured. + """ + pad = pad_cm / scale + zero_distance = zero_distance_cm / scale + zero_volume = zero_volume_cm3 / (scale ** 3) + + # Volumes are computed lazily and memoised; only pairs that survive the AABB test need them. + volumes = {} + surfaces = {} + + def volume(index): + if index not in volumes: + volumes[index] = volume_of(parts[index].shape) + return volumes[index] + + n = len(parts) + total_pairs = n * (n - 1) // 2 + aabb_survivors = [] + for i, j in itertools.combinations(range(n), 2): + hit, box_volume = boxes_overlap(parts[i].bbox, parts[j].bbox, pad) + if hit: + aabb_survivors.append((i, j, box_volume)) + # Cheapest first: a small shared box is usually a corner touch and resolves fast. + aabb_survivors.sort(key=lambda t: t[2]) + if max_pairs: + aabb_survivors = aabb_survivors[:max_pairs] + + if verbose: + print(f" {n} placed solids -> {total_pairs} pairs; {len(aabb_survivors)} survive the " + f"AABB rejection ({100.0 * len(aabb_survivors) / max(1, total_pairs):.2f} %)", + flush=True) + + results = [] + counts = {"pairs": total_pairs, "aabb": len(aabb_survivors), "disjoint": 0, + "coincident": 0, "interpenetrating": 0, "contained": 0, "failed": 0} + started = time.time() + for k, (i, j, _) in enumerate(aabb_survivors): + a, b = parts[i], parts[j] + record = {"a": a.name, "b": b.name, "aPath": a.path, "bPath": b.path, + "sameDefinition": a.definition == b.definition, + "volA": volume(i) * scale ** 3, "volB": volume(j) * scale ** 3} + d = distance_between(a.shape, b.shape) + if d is None: + record["class"] = "failed" + counts["failed"] += 1 + results.append(record) + continue + record["distance"] = d * scale + if d > zero_distance: + record["class"] = "disjoint" + counts["disjoint"] += 1 + results.append(record) + if verbose and (k + 1) % 25 == 0: + print(f" {k + 1}/{len(aabb_survivors)} ({time.time() - started:.1f} s)", + flush=True) + continue + + # Touching or interpenetrating: only now is a boolean worth its cost. + try: + common = BRepAlgoAPI_Common(a.shape, b.shape) + common.Build() + ok = common.IsDone() + shape = common.Shape() if ok else None + except Exception as exc: + record["class"] = "failed" + record["error"] = str(exc) + counts["failed"] += 1 + results.append(record) + continue + if not ok or shape is None: + record["class"] = "failed" + counts["failed"] += 1 + results.append(record) + continue + + solids = 0 + explorer = TopExp_Explorer(shape, TopAbs_SOLID) + while explorer.More(): + solids += 1 + explorer.Next() + raw_volume = volume_of(shape) if solids else 0.0 + record["commonSolids"] = solids + record["commonVolume"] = raw_volume * scale ** 3 + smaller = min(volume(i), volume(j)) + record["fractionOfSmaller"] = raw_volume / smaller if smaller > 0 else 0.0 + + if raw_volume <= zero_volume: + record["class"] = "coincident" + counts["coincident"] += 1 + else: + box = bbox_of(shape, tight=True) + if box is not None: + record["commonBBox"] = [c * scale for c in box] + record["commonExtent"] = sorted((box[k + 3] - box[k]) * scale for k in range(3)) + if record["fractionOfSmaller"] > 1.0 - 1e-6: + record["class"] = "contained" + counts["contained"] += 1 + else: + record["class"] = "interpenetrating" + counts["interpenetrating"] += 1 + if deep: + if i not in surfaces: + surfaces[i] = surface_of(a.shape) + if j not in surfaces: + surfaces[j] = surface_of(b.shape) + depth, diag, samples = penetration_depth(shape, surfaces[i], surfaces[j]) + record["penetrationDepthSampled"] = depth * scale + record["penetrationDepthUpper"] = diag * scale + record["penetrationSamples"] = samples + results.append(record) + if verbose and (k + 1) % 25 == 0: + print(f" {k + 1}/{len(aabb_survivors)} ({time.time() - started:.1f} s)" + f" [{counts['disjoint']}d {counts['coincident']}c " + f"{counts['interpenetrating']}I {counts['contained']}n]", flush=True) + + return results, counts, time.time() - started + + +# --------------------------------------------------------------------------------------------- + +def self_test() -> int: + from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox + + failures = [] + + def check(name, ok, detail=""): + print(f" [{'ok ' if ok else 'FAIL'}] {name}" + (f" {detail}" if not ok else "")) + if not ok: + failures.append(name) + + def box(x0, y0, z0, x1, y1, z1): + return BRepPrimAPI_MakeBox(gp_Pnt(x0, y0, z0), gp_Pnt(x1, y1, z1)).Shape() + + # DistShapeShape(vertex, SOLID) is 0 for an interior point; penetration depth must be measured + # against the boundary (surface_of), not the solid. + inner = box(0, 0, 0, 2, 2, 2) + d_solid = distance_to_surface(gp_Pnt(1, 1, 1), inner) + check("TRAP: DistShapeShape from an interior point to the SOLID is 0, not the distance to " + "its boundary", d_solid is not None and d_solid == 0.0, str(d_solid)) + d_surface = distance_to_surface(gp_Pnt(1, 1, 1), surface_of(inner)) + check("...and against the face compound it is the true 1 cm -- which is what the depth " + "sampler uses", d_surface is not None and abs(d_surface - 1.0) < 1e-9, str(d_surface)) + + parts = assembly_from_shapes([ + ("touchA", box(0, 0, 0, 2, 2, 2)), # touchA | touchB share the face x=2 + ("touchB", box(2, 0, 0, 4, 2, 2)), + ("gapA", box(10, 0, 0, 12, 2, 2)), # 0.25 cm gap to gapB + ("gapB", box(12.25, 0, 0, 14, 2, 2)), + ("tinyA", box(20, 0, 0, 22, 2, 2)), # 1e-7 cm gap: separate, and must be measured + ("tinyB", box(22 + 1e-7, 0, 0, 24, 2, 2)), + ("ovA", box(30, 0, 0, 34, 2, 2)), # 0.5 cm interpenetration over [33.5, 34] + ("ovB", box(33.5, 0, 0, 37, 2, 2)), + ("outer", box(40, 40, 40, 46, 46, 46)), # inner2 wholly contained in outer + ("inner2", box(42, 42, 42, 44, 44, 44)), + ]) + results, counts, _ = census(parts, scale=1.0, pad_cm=1.0, zero_distance_cm=1e-12, + verbose=False) + by_pair = {tuple(sorted((r["a"], r["b"]))): r for r in results} + + r = by_pair.get(("touchA", "touchB")) + check("touching pair: distance 0 and ZERO common volume -> coincident faces, not an overlap", + r is not None and r["class"] == "coincident" and r["commonVolume"] < 1e-12, + str(r)) + + r = by_pair.get(("gapA", "gapB")) + check("0.25 cm gap: reported disjoint with the separation measured", + r is not None and r["class"] == "disjoint" and abs(r["distance"] - 0.25) < 1e-9, str(r)) + + r = by_pair.get(("tinyA", "tinyB")) + check("1e-7 cm gap: STILL reported disjoint, with the separation measured, not rounded to 0", + r is not None and r["class"] == "disjoint" and abs(r["distance"] - 1e-7) < 1e-12, str(r)) + + r = by_pair.get(("ovA", "ovB")) + check("0.5 cm interpenetration: classified interpenetrating", + r is not None and r["class"] == "interpenetrating", str(r)) + check("interpenetration: common volume is exactly 0.5 x 2 x 2 = 2 cm^3", + r is not None and abs(r["commonVolume"] - 2.0) < 1e-9, str(r.get("commonVolume"))) + check("interpenetration: fraction of the smaller part is 2 / 14", + r is not None and abs(r["fractionOfSmaller"] - 2.0 / 14.0) < 1e-9, + str(r.get("fractionOfSmaller"))) + check("interpenetration: the shared slab is 0.5 cm thick", + r is not None and abs(r["commonExtent"][0] - 0.5) < 1e-9, str(r.get("commonExtent"))) + # A 6-sample grid cannot land on the 0.25 cm mid-plane, so the sampled depth is bracketed. + check("interpenetration: sampled depth is a LOWER bound on the true 0.25 cm, within one " + "grid half-cell of it, and under the bbox-diagonal upper bound", + r is not None and 0.25 - 0.5 / 12 - 1e-9 <= r["penetrationDepthSampled"] <= 0.25 + 1e-9 + and r["penetrationDepthUpper"] > r["penetrationDepthSampled"], + f"{r.get('penetrationDepthSampled')} vs 0.25, upper {r.get('penetrationDepthUpper')}") + + r = by_pair.get(("inner2", "outer")) + check("containment: classified `contained`, not `interpenetrating`", + r is not None and r["class"] == "contained", str(r)) + check("containment: common volume equals the inner part's 8 cm^3", + r is not None and abs(r["commonVolume"] - 8.0) < 1e-9, str(r.get("commonVolume"))) + + check("census bookkeeping: 1 coincident, 1 interpenetrating, 1 contained, the rest disjoint", + counts["coincident"] == 1 and counts["interpenetrating"] == 1 + and counts["contained"] == 1 and counts["failed"] == 0, str(counts)) + + # The NEGATIVE control: no overlaps are reported when there are none. + clean = assembly_from_shapes([("p0", box(0, 0, 0, 1, 1, 1)), + ("p1", box(2, 0, 0, 3, 1, 1)), + ("p2", box(4, 0, 0, 5, 1, 1))]) + _, clean_counts, _ = census(clean, scale=1.0, verbose=False) + check("negative control: three separated boxes report 0 interpenetrating, 0 contained", + clean_counts["interpenetrating"] == 0 and clean_counts["contained"] == 0, + str(clean_counts)) + + print(f"\n{'SELF-TEST PASSED' if not failures else 'SELF-TEST FAILED'}: " + f"{len(failures)} failure(s)") + return 0 if not failures else 1 + + +def report(results, counts, scale, seconds, top=25): + print() + print(f" AABB survivors {counts['aabb']} of {counts['pairs']} pairs " + f"({seconds:.1f} s)") + print(f" disjoint {counts['disjoint']:6d}") + print(f" coincident faces {counts['coincident']:6d} (touching -- legal)") + print(f" INTERPENETRATING {counts['interpenetrating']:6d} (illegal for TGeo/Geant4)") + print(f" contained {counts['contained']:6d} (legal only as mother/daughter)") + print(f" failed {counts['failed']:6d}") + + bad = [r for r in results if r.get("class") in ("interpenetrating", "contained")] + bad.sort(key=lambda r: -r.get("commonVolume", 0.0)) + if bad: + print() + print(f" {'pair':<52s} {'class':<17s} {'V_common cm^3':>14s} {'frac small':>11s} " + f"{'depth cm':>10s}") + for r in bad[:top]: + print(f" {r['a'][:24]:<24s} {r['b'][:24]:<25s} {r['class']:<17s} " + f"{r.get('commonVolume', 0):14.6g} {r.get('fractionOfSmaller', 0):11.4g} " + f"{r.get('penetrationDepthSampled', 0):10.4g}") + if len(bad) > top: + print(f" ... and {len(bad) - top} more") + + gaps = sorted((r["distance"], r["a"], r["b"]) for r in results + if r.get("class") == "disjoint") + if gaps: + print() + print(f" tightest measured gaps between disjoint pairs (cm):") + for d, a, b in gaps[:10]: + print(f" {d:12.6g} {a} | {b}") + coincident = [r for r in results if r.get("class") == "coincident"] + if coincident: + print() + print(f" coincident-face pairs (shared boundary, zero volume): {len(coincident)}") + for r in coincident[:10]: + print(f" {r['a']} | {r['b']}") + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--step", type=Path) + parser.add_argument("--out", type=Path) + parser.add_argument("--max-pairs", type=int, default=0, + help="cap the pairs examined after AABB rejection (bounded runs)") + parser.add_argument("--max-parts", type=int, default=0) + parser.add_argument("--parts", type=str, default="") + parser.add_argument("--parts-regex", type=str, default="", + help="keep instances whose name matches this regex -- the way to select a " + "replicated prototype, whose copies are named NAME, NAME#1, NAME#2") + parser.add_argument("--pad", type=float, default=0.1, + help="AABB inflation in cm: decides which DISJOINT pairs get their\n separation measured (default 0.1 cm)") + parser.add_argument("--no-deep", action="store_true", + help="skip the penetration-depth sampling") + parser.add_argument("--inject", type=str, default="", + help="NAME:DX,DY,DZ -- translate one part by this many cm before the " + "census. The positive control on a real model.") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + + if args.self_test: + return self_test() + if not args.step: + parser.error("--step is required (unless --self-test)") + + started = time.time() + parts, scale = load_assembly(args.step) + if args.parts: + wanted = set(args.parts.split(",")) + parts = [p for p in parts if p.name in wanted] + if args.parts_regex: + pattern = re.compile(args.parts_regex) + parts = [p for p in parts if pattern.search(p.name)] + if args.max_parts: + parts = parts[:args.max_parts] + print(f" {args.step.name}: {len(parts)} placed solids, {scale} cm/unit " + f"({time.time() - started:.1f} s)", flush=True) + + injected = None + if args.inject: + from OCC.Core.TopLoc import TopLoc_Location + from OCC.Core.gp import gp_Trsf, gp_Vec + name, deltas = args.inject.split(":") + dx, dy, dz = (float(v) / scale for v in deltas.split(",")) + for k, p in enumerate(parts): + if p.name == name: + trsf = gp_Trsf() + trsf.SetTranslation(gp_Vec(dx, dy, dz)) + moved = p.shape.Moved(TopLoc_Location(trsf)) + parts[k] = Part(p.name + "@INJECTED", p.definition, p.path, moved) + injected = parts[k].name + break + if injected is None: + raise SystemExit(f"--inject: no part named {name}") + print(f" INJECTED: {injected} translated by {args.inject.split(':')[1]} cm", flush=True) + + results, counts, seconds = census(parts, scale, pad_cm=args.pad, max_pairs=args.max_pairs, + deep=not args.no_deep) + report(results, counts, scale, seconds) + + if args.out: + args.out.write_text(json.dumps( + {"version": CENSUS_FORMAT_VERSION, "model": str(args.step), "scaleToCm": scale, + "nParts": len(parts), "parts": [p.name for p in parts], "injected": injected, + "counts": counts, "seconds": seconds, "pairs": results}, indent=1)) + print(f"\n wrote {args.out}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/renderTGeo.py b/Detectors/CADSupport/validation/renderTGeo.py new file mode 100755 index 0000000000000..ee676db2d745a --- /dev/null +++ b/Detectors/CADSupport/validation/renderTGeo.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Raytrace a TGeo geometry through the real navigator and write a PNG. + +One ray per pixel through gGeoManager: InitTrack from the camera plane, step +with FindNextBoundaryAndStep until a daughter of the top volume is entered, +then shade from FindNormal(). The picture is therefore made by exactly the +code the transport uses -- a solid that does not navigate renders as +background, and a TGeoTessellated renders as its bounding box. + +Framing is two-pass: a coarse cast finds which pixels hit the subject, and the +real render frames tightly on that. A stray part far from the rest of a model +therefore cannot shrink the subject into a corner. + +With --csg-report, each volume is coloured by the representation the cascade +gave it (CSG / exact surfaces / mesh); --grey paints everything uniformly, for +a "before" panel. + + python3 renderTGeo.py geom.root out.png --csg-report csg_report.json + python3 renderTGeo.py geom.root out.png --grey --theta 66 --phi 28 +""" +import argparse, json, math, sys +import numpy as np +import ROOT +from PIL import Image + +ROOT.gROOT.SetBatch(True) + + +def unit(v): + return v / np.linalg.norm(v) + + +def render(geofile, out, tiers=None, width=1100, height=800, + theta=62.0, phi=32.0, bg=(255, 255, 255), pad=1.18, + grey=False): + ROOT.TGeoManager.Import(geofile) + gm = ROOT.gGeoManager + top = gm.GetTopVolume() + + # --- collect the daughters and their world-frame bounding boxes --- + names, centres, halfs = [], [], [] + for i in range(top.GetNdaughters()): + node = top.GetNode(i) + vol = node.GetVolume() + box = vol.GetShape() + tr = node.GetMatrix().GetTranslation() + names.append(vol.GetName()) + centres.append([tr[0], tr[1], tr[2]]) + halfs.append([box.GetDX(), box.GetDY(), box.GetDZ()]) + centres = np.array(centres) + halfs = np.array(halfs) + lo = (centres - halfs).min(axis=0) + hi = (centres + halfs).max(axis=0) + centre = 0.5 * (lo + hi) + radius = 0.5 * np.linalg.norm(hi - lo) + + # --- camera --- + th, ph = math.radians(theta), math.radians(phi) + eye_dir = np.array([math.sin(th) * math.cos(ph), + math.sin(th) * math.sin(ph), + math.cos(th)]) + dist = radius * 3.2 + eye = centre + eye_dir * dist + fwd = unit(centre - eye) + up0 = np.array([0.0, 0.0, 1.0]) + right = unit(np.cross(fwd, up0)) + up = unit(np.cross(right, fwd)) + + # frame on the projected bbox corners of the daughters within 3x the median distance of the + # cluster, so a stray part cannot shrink the subject + corners = [] + for c, h in zip(centres, halfs): + for sx in (-1, 1): + for sy in (-1, 1): + for sz in (-1, 1): + corners.append(c + np.array([sx * h[0], sy * h[1], sz * h[2]])) + corners = np.array(corners) - eye + u = corners @ right + v = corners @ up + # robust bounds: a single stray part in the CAD model must not shrink the subject + ulo, uhi = u.min(), u.max() + vlo, vhi = v.min(), v.max() + umid, vmid = 0.5 * (ulo + uhi), 0.5 * (vlo + vhi) + half_u = 0.5 * (uhi - ulo) * pad + half_v = 0.5 * (vhi - vlo) * pad + aspect = width / height + if half_u / half_v < aspect: + half_u = half_v * aspect + else: + half_v = half_u / aspect + window = (umid - half_u, umid + half_u, vmid - half_v, vmid + half_v) + light = unit(np.array([0.45, 0.35, 0.82])) + + def cast(win, w, h): + """Cast one ray per pixel over the camera window; return the image and + the (u, v) extent of the pixels that actually hit something.""" + u0, u1, v0, v1 = win + gx = np.linspace(u0, u1, w) + gy = np.linspace(v1, v0, h) + out = np.zeros((h, w, 3), dtype=np.uint8) + out[:, :] = bg + hit_u, hit_v = [], [] + nav = gm.GetCurrentNavigator() + for iy, sy in enumerate(gy): + for ix, sx in enumerate(gx): + o = eye + right * sx + up * sy + nav.InitTrack(o[0], o[1], o[2], fwd[0], fwd[1], fwd[2]) + nm = None + for _ in range(24): + nav.FindNextBoundaryAndStep() + if nav.IsOutside(): + break + cand = nav.GetCurrentVolume().GetName() + if cand in vol_colour: + nm = cand + break + if nm is None: + continue + hit_u.append(sx); hit_v.append(sy) + nr = nav.FindNormal() + n = np.array([nr[0], nr[1], nr[2]]) + nn = np.linalg.norm(n) + lam = 0.7 if nn == 0 else abs(float(np.dot(n / nn, light))) + shade = 0.32 + 0.68 * lam + base = np.array(vol_colour[nm], dtype=float) + out[iy, ix] = np.clip(base * shade + 45.0 * (shade ** 6), 0, 255) + return out, (hit_u, hit_v) + + # --- colour per volume --- + PALETTE = { + "csg": (0x2f, 0x6b, 0x4c), + "surface": (0x1c, 0x62, 0x96), + "mesh": (0x9a, 0x5c, 0x17), + } + GREY = (0x8a, 0x91, 0x97) + vol_colour = {} + for n in names: + if grey or tiers is None: + vol_colour[n] = GREY + else: + vol_colour[n] = PALETTE.get(tiers.get(n, "mesh"), GREY) + + # pass 1: a coarse cast over the generous window, only to find the subject + _, (hu, hv) = cast(window, 190, 140) + if hu: + mu = 0.06 * max(max(hu) - min(hu), 1e-6) + mv = 0.06 * max(max(hv) - min(hv), 1e-6) + u0, u1 = min(hu) - mu, max(hu) + mu + v0, v1 = min(hv) - mv, max(hv) + mv + cu, cv = 0.5 * (u0 + u1), 0.5 * (v0 + v1) + hu2, hv2 = 0.5 * (u1 - u0), 0.5 * (v1 - v0) + if hu2 / hv2 < aspect: + hu2 = hv2 * aspect + else: + hv2 = hu2 / aspect + window = (cu - hu2, cu + hu2, cv - hv2, cv + hv2) + + # pass 2: the real render, tightly framed on what pass 1 found + img, _ = cast(window, width, height) + + Image.fromarray(img).save(out) + print("wrote", out) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser() + ap.add_argument("geofile") + ap.add_argument("out") + ap.add_argument("--csg-report", default=None) + ap.add_argument("--grey", action="store_true") + ap.add_argument("--width", type=int, default=1100) + ap.add_argument("--height", type=int, default=800) + ap.add_argument("--theta", type=float, default=62.0) + ap.add_argument("--phi", type=float, default=32.0) + a = ap.parse_args() + + tiers = None + if a.csg_report: + rep = json.load(open(a.csg_report)) + tiers = {} + for part in rep.get("parts", []): + nm = part.get("volume") or part.get("name") + t = part.get("representation") + if nm: + tiers[nm] = t + render(a.geofile, a.out, tiers=tiers, width=a.width, height=a.height, + theta=a.theta, phi=a.phi, grey=a.grey) diff --git a/Detectors/CADSupport/validation/roundTripReport.py b/Detectors/CADSupport/validation/roundTripReport.py new file mode 100644 index 0000000000000..2903c42552af5 --- /dev/null +++ b/Detectors/CADSupport/validation/roundTripReport.py @@ -0,0 +1,632 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""The TGeo -> STEP -> TGeo report: what the round trip does to every part of a geometry. + +For every part it gives the source `TGeoShape` (class, and for a boolean its depth and leaf +classes), the representation the cascade emitted with its evidence or decline reason, and the +known-source verdict. The **feature matrix** sets every source shape class in the corpus against +what the round trip made of it. Every number is read from an existing instrument +(`csg_report.json`, `checkKnownSource.py`, the sidecars, `exportSourceShapes.py`); a field it +cannot read is reported as unknown, never as zero. + +Usage +----- + # the whole corpus, one markdown document + roundTripReport.py --corpus --out report.md + + # the same as a standalone HTML page (print it to PDF from a browser) + roundTripReport.py --corpus --out report.html --html + + # one part, on the fly + roundTripReport.py --corpus --part BREF1 + +`` holds one directory per module, each with `o2sim_geometry.root`, +`_writer_report.json` and a conversion subdirectory (`conv/` by default) containing +`csg_report.json`. `--converted-root` points the conversions somewhere else, `--modules` selects a +subset, and `--no-source-shapes` skips the (ROOT-loading) source description when only the +converter's own side is wanted. +""" + +import argparse +import html +import json +import os +import re +import sys +from collections import Counter, defaultdict +from pathlib import Path + +import cadsupport_path # noqa: E402,F401 (puts ../tools on sys.path) +from cadsupport import planar # noqa: E402 + + +# ------------------------------------------------------------------------------------------ +# reading a corpus +# ------------------------------------------------------------------------------------------ + +def _load(path, default=None): + try: + return json.loads(Path(path).read_text()) + except Exception: + return default + + +def _size(path): + try: + return os.path.getsize(path) + except OSError: + return None + + +def source_descriptions(module, source_dir, conv_dir, refresh=False): + """`{stem: description}` of the shape each part was made from, cached beside the conversion. + + Delegates to `exportSourceShapes.export_run(write=False)`; cached in `original_report.json`. + """ + cache = Path(conv_dir) / "original_report.json" + if cache.exists() and not refresh: + payload = _load(cache, {}) + return {row["part"]: row for row in payload.get("parts", []) if row.get("part")} + geometry = Path(source_dir) / "o2sim_geometry.root" + writer = Path(source_dir) / f"{module}_writer_report.json" + if not geometry.exists() or not writer.exists(): + return {} + try: + import exportSourceShapes + import ROOT + ROOT.gErrorIgnoreLevel = ROOT.kWarning # one Import banner per module is not a finding + records = exportSourceShapes.export_run(str(geometry), str(writer), str(conv_dir), + verbose=False, write=False, + tiers=("csg", "surface", "mesh")) + except Exception as error: + print(f" {module}: could not describe source shapes ({error})", file=sys.stderr) + return {} + cache.write_text(json.dumps({"parts": records}, indent=1)) + return {row["part"]: row for row in records if row.get("part")} + + +def read_module(module, source_dir, conv_dir, with_sources=True, refresh=False): + """Every part of one module, joined across the instruments. Returns (rows, module summary).""" + report = _load(Path(conv_dir) / "csg_report.json") + if not report: + return [], {"module": module, "error": f"no csg_report.json in {conv_dir}"} + + known = _load(Path(conv_dir) / "knownsource.json", {}) + known_rows = {} + for row in (known.get("parts") if isinstance(known, dict) else known) or []: + if row.get("part"): + known_rows[row["part"]] = row + + sources = source_descriptions(module, source_dir, conv_dir, refresh) if with_sources else {} + writer = _load(Path(source_dir) / f"{module}_writer_report.json", {}) or {} + + rows = [] + for part in report.get("parts", []): + stem = part.get("part") + evidence = part.get("evidence") or {} + source = sources.get(stem) or {} + ks = known_rows.get(stem) or {} + # A part carried as CSG that also wrote a flat sidecar ships the flat solid, not a tree. + tier = part.get("representation") + if tier == "csg" and part.get("flatSidecar"): + tier = "flatcsg" + surfaces = Path(conv_dir) / f"surfaces_{stem}.bin" + # Older conversions lack this field, so it is computed from the sidecar. + exact, exact_why = part.get("tessellationExact"), part.get("tessellationExactWhy") + census = part.get("surfaceCensus") + if exact is None and surfaces.exists(): + exact, exact_why, census = planar.tessellation_is_exact(str(surfaces)) + rows.append({ + "module": module, + "part": stem, + "volume": part.get("volume"), + "sourceVolume": source.get("sourceVolume") or ks.get("source"), + "sourceClass": source.get("class") or ks.get("sourceClass"), + "booleanDepth": source.get("booleanDepth"), + "leaves": source.get("leaves"), + "leafClasses": source.get("leafClasses") or {}, + "ships": tier, + "recogniser": evidence.get("recogniser"), + "structure": evidence.get("description", {}).get("op") + if isinstance(evidence.get("description"), dict) else None, + "dVsym": evidence.get("symmetricDifferenceCm3"), + "band": evidence.get("bandCm3"), + "relative": evidence.get("relativeToVolume"), + "whyNotCSG": part.get("whyNotCSG"), + "tessellationExact": exact, + "tessellationExactWhy": exact_why, + "surfaceCensus": census or {}, + "knownSourceFailures": ks.get("failures"), + "knownSourceFlags": ks.get("flags"), + "capacityRelativeDeviation": ks.get("capacityRelativeDeviation"), + "containsMismatches": (ks.get("contains") or {}).get("mismatches"), + "containsPoints": (ks.get("contains") or {}).get("points"), + "sidecarBytes": _size(surfaces), + "flatSidecarBytes": _size(Path(conv_dir) / f"flatcsg_{stem}.bin"), + "facetBytes": _size(Path(conv_dir) / f"facets_{stem}.bin"), + }) + + exactness = {"exact": sum(1 for r in rows if r["tessellationExact"])} + summary = { + "module": module, + "leafSolids": report.get("nLeafSolids"), + "tiers": report.get("tiers") or {}, + # The writer's own coverage: volumes it declined never reach the converter. + "writerByShapeClass": writer.get("byShapeClass") or {}, + "writerVisited": writer.get("volumesVisited"), + "writerDefinitions": writer.get("definitions"), + "writerDeclined": writer.get("declined"), + "writerDeclinedRows": [v for v in (writer.get("volumes") or []) + if not v.get("converted") and not v.get("isAssembly")], + "writerVolumes": writer.get("volumes") or [], + "flat": sum(1 for r in rows if r["ships"] == "flatcsg"), + "tessellationExact": exactness.get("exact"), + "knownSourceScored": len(known_rows), + "knownSourceFailed": sum(1 for r in known_rows.values() if r.get("failures")), + "error": None, + } + return rows, summary + + +def read_corpus(root, converted_root=None, subdir="conv", modules=None, + with_sources=True, refresh=False): + root = Path(root) + names = modules or sorted(p.name for p in root.iterdir() if p.is_dir()) + rows, summaries = [], [] + for module in names: + source_dir = root / module + conv_dir = (Path(converted_root) / module) if converted_root else (source_dir / subdir) + if not (Path(conv_dir) / "csg_report.json").exists(): + summaries.append({"module": module, "error": f"no csg_report.json under {conv_dir}"}) + continue + print(f" reading {module} ...", file=sys.stderr) + module_rows, summary = read_module(module, source_dir, conv_dir, with_sources, refresh) + rows.extend(module_rows) + summaries.append(summary) + return rows, summaries + + +# ------------------------------------------------------------------------------------------ +# the tables +# ------------------------------------------------------------------------------------------ + +TIERS = ["csg", "flatcsg", "surface", "mesh"] + +# A "family" is a volume name without its trailing _ groups; grouping is presentation only. +def family(name): + return re.sub(r"(_\d+)+$", "", name or "") + +TIER_LABEL = {"csg": "CSG", "flatcsg": "FlatCSG", "surface": "Surface", "mesh": "Tessellated"} + + +def redundancy(summaries): + """Per module: the solid volumes that duplicate another (same family, class, capacity).""" + out = [] + for s in summaries: + rows = [v for v in (s.get("writerVolumes") or []) if not v.get("isAssembly")] + if not rows: + continue + families = defaultdict(list) + for v in rows: + families[family(v.get("name"))].append(v) + signatures = 0 + worst = [] + for name, members in families.items(): + sig = {(m.get("shapeClass"), + None if m.get("capacity_cm3") is None else round(m["capacity_cm3"], 10)) + for m in members} + signatures += len(sig) + if len(members) > len(sig): + worst.append((len(members), len(sig), name, members[0].get("shapeClass"))) + worst.sort(reverse=True) + out.append({"module": s["module"], "volumes": len(rows), "families": len(families), + "signatures": signatures, "redundant": len(rows) - signatures, + "worst": worst[:6]}) + return out + + +def feature_matrix(rows): + """Every distinct source shape class against what the round trip made of it. + + A `TGeoCompositeShape` is counted once as itself and once per leaf class, in a second table. + """ + direct = defaultdict(lambda: {"parts": 0, **{t: 0 for t in TIERS}, + "knownSourceFailed": 0, "exact": 0}) + inside = Counter() + inside_parts = defaultdict(set) + for row in rows: + cls = row["sourceClass"] or "(source shape unknown)" + entry = direct[cls] + entry["parts"] += 1 + if row["ships"] in entry: + entry[row["ships"]] += 1 + if row["knownSourceFailures"]: + entry["knownSourceFailed"] += 1 + if row["tessellationExact"]: + entry["exact"] += 1 + for leaf, count in (row["leafClasses"] or {}).items(): + if cls == "TGeoCompositeShape": + inside[leaf] += count + inside_parts[leaf].add((row["module"], row["part"])) + return direct, inside, inside_parts + + +def fmt(value, digits=3): + if value is None: + return "-" + if isinstance(value, bool): + return "yes" if value else "no" + if isinstance(value, float): + if value == 0: + return "0" + return f"{value:.{digits}g}" + return str(value) + + +def md_table(header, rows, align=None): + align = align or ["---"] * len(header) + out = ["| " + " | ".join(header) + " |", "| " + " | ".join(align) + " |"] + for row in rows: + out.append("| " + " | ".join(str(c) for c in row) + " |") + return "\n".join(out) + + +def render_markdown(rows, summaries, per_part=True): + total = len(rows) + doc = [] + doc.append("# TGeo → STEP → TGeo: what the round trip does to this geometry\n") + doc.append( + "Every number below is read from an instrument that produced it: `csg_report.json` for " + "the cascade's own decision and evidence, `knownsource.json` for the only test that sees " + "the original `TGeoVolume`, and the sidecars themselves for the face census. Nothing is " + "re-derived here, and a field that could not be read is `-`, never `0`.\n") + + # --- corpus summary --------------------------------------------------------------------- + doc.append("## The corpus\n") + table = [] + tot = Counter() + for s in summaries: + if s.get("error"): + table.append([s["module"], "-", "-", "-", "-", "-", "-", s["error"]]) + continue + t = s["tiers"] + tree = t.get("csg", 0) - s["flat"] + table.append([s["module"], s["leafSolids"], tree, s["flat"], t.get("surface", 0), + t.get("mesh", 0), fmt(s["tessellationExact"]), + f"{s['knownSourceScored'] - s['knownSourceFailed']}/{s['knownSourceScored']}"]) + tot["leaf"] += s["leafSolids"] or 0 + tot["tree"] += tree + tot["flat"] += s["flat"] + tot["surface"] += t.get("surface", 0) + tot["mesh"] += t.get("mesh", 0) + tot["exact"] += s["tessellationExact"] or 0 + tot["ks"] += s["knownSourceScored"] + tot["ksfail"] += s["knownSourceFailed"] + table.append(["**all**", f"**{tot['leaf']}**", f"**{tot['tree']}**", f"**{tot['flat']}**", + f"**{tot['surface']}**", f"**{tot['mesh']}**", f"**{tot['exact']}**", + f"**{tot['ks'] - tot['ksfail']}/{tot['ks']}**"]) + doc.append(md_table( + ["module", "leaf solids", "CSG tree", "FlatCSG", "Surface", "Tessellated", + "tessellation exact", "agrees with source"], + table, ["---", "---:", "---:", "---:", "---:", "---:", "---:", "---:"])) + doc.append("") + doc.append( + "*tessellation exact* counts parts whose every face is a planar polygon, for which a " + "triangulation is the same solid rather than an approximation of it (`cadsupport/planar.py`). It " + "is an annotation, not a routing rule -- a box is recognised as a `TGeoBBox` and stays " + "one. *agrees with source* is `checkKnownSource.py`: class, capacity and a seeded " + "containment cross-check against the original `TGeoShape`.\n") + + # --- writer coverage -------------------------------------------------------------------- + writer_classes = defaultdict(lambda: {"converted": 0, "declined": 0, "pureAssembly": 0, + "reasons": Counter()}) + declined_rows = [] + for s in summaries: + for cls, e in (s.get("writerByShapeClass") or {}).items(): + entry = writer_classes[cls] + entry["converted"] += e.get("converted", 0) + entry["declined"] += e.get("declined", 0) + entry["pureAssembly"] += e.get("pureAssembly", 0) + for reason, n in (e.get("reasons") or {}).items(): + entry["reasons"][reason] += n + for row in s.get("writerDeclinedRows") or []: + declined_rows.append((s["module"], row)) + if writer_classes: + total_declined = sum(e["declined"] for e in writer_classes.values()) + doc.append("## Step 1, the writer: what reached the STEP file at all\n") + doc.append( + "A volume the writer declines never reaches the converter, so the tiers above are a " + "fraction of what got *out*, not of the geometry. This is the other half. It counts " + "**volumes**, where the tables above count leaf solids, and the two denominators are " + "not the same number -- a volume with daughters contributes a `__body` solid.\n") + table = [] + for cls, e in sorted(writer_classes.items(), key=lambda kv: -kv[1]["converted"]): + reasons = "; ".join(f"{r} ({n})" for r, n in e["reasons"].most_common(3)) + table.append([f"`{cls}`", e["converted"], e["declined"] or "", + e["pureAssembly"] or "", reasons or ""]) + doc.append(md_table(["source shape", "written", "declined", "pure assembly", "why declined"], + table, ["---", "---:", "---:", "---:", "---"])) + doc.append("") + doc.append(f"**{total_declined} volume(s) declined by the writer** across this corpus." + + (" Every solid-carrying volume was exported." if not total_declined else "") + + "\n") + if declined_rows: + table = [[m, r.get("name"), f"`{r.get('shapeClass')}`", r.get("reason") or "-"] + for m, r in declined_rows[:60]] + doc.append(md_table(["module", "volume", "shape", "reason"], table)) + doc.append("") + + # --- repeated logical volumes ----------------------------------------------------------- + red = redundancy(summaries) + heavy = [r for r in red if r["redundant"]] + if heavy: + doc.append("## Repeated logical volumes: the same solid, built many times\n") + doc.append( + "Some detector geometries give every *instance* of a component its own `TGeoVolume` " + "and its own `TGeoShape`, where one volume placed many times would do. It is not a " + "naming artefact: MFT holds **5144 separate `TGeoVolume` objects with 5144 separate " + "`TGeoBBox` objects** whose parameters are one and the same " + "`(0.05, 0.025, 0.025)` box, in one medium. A geometry that does this pays for it " + "everywhere downstream -- the manager's volume table and voxelisation, the STEP file, " + "this pipeline's per-part acceptance test, and navigation at run time.\n") + doc.append( + "*volumes* counts solid-carrying volumes; *distinct solids* counts distinct " + "(family, shape class, capacity) signatures. The difference is what could be shared.\n") + table = [] + for r in red: + worst = "; ".join(f"`{n}` {v}→{s}" for v, s, n, _ in r["worst"][:3]) + table.append([r["module"], r["volumes"], r["families"], r["signatures"], + r["redundant"] or "", worst]) + doc.append(md_table( + ["module", "volumes", "name families", "distinct solids", "redundant", "worst families"], + table, ["---", "---:", "---:", "---:", "---:", "---"])) + doc.append("") + + # --- feature matrix --------------------------------------------------------------------- + direct, inside, inside_parts = feature_matrix(rows) + doc.append("## Step 2, the converter: every source shape class, and what became of it\n") + doc.append( + "This is the table that says what the pipeline supports, measured over a real geometry " + "rather than asserted. One row per distinct `TGeoShape` class in the source, and what the " + "round trip emitted for the parts that used it.\n") + table = [] + for cls, e in sorted(direct.items(), key=lambda kv: -kv[1]["parts"]): + table.append([f"`{cls}`", e["parts"], e["csg"], e["flatcsg"], e["surface"], e["mesh"], + e["exact"], e["knownSourceFailed"] or ""]) + doc.append(md_table( + ["source shape", "parts", "CSG tree", "FlatCSG", "Surface", "Tessellated", + "tess. exact", "source disagreements"], + table, ["---", "---:", "---:", "---:", "---:", "---:", "---:", "---:"])) + doc.append("") + if inside: + doc.append("### Primitive classes appearing *inside* a `TGeoCompositeShape`\n") + doc.append( + "Supporting a boolean means supporting what is in it. This counts leaf occurrences, " + "and the parts they occur in.\n") + table = [[f"`{cls}`", n, len(inside_parts[cls])] + for cls, n in inside.most_common()] + doc.append(md_table(["leaf class", "occurrences", "parts"], table, + ["---", "---:", "---:"])) + doc.append("") + + # --- what declined ---------------------------------------------------------------------- + declined = [r for r in rows if r["ships"] in ("surface", "mesh")] + doc.append(f"## What did not become CSG ({len(declined)} of {total})\n") + if declined: + reasons = Counter((r["whyNotCSG"] or "(no reason recorded)").split(";")[0].strip() + for r in declined) + doc.append(md_table(["the recogniser's reason", "parts"], + [[r, n] for r, n in reasons.most_common(25)], ["---", "---:"])) + doc.append("") + else: + doc.append("Nothing. Every leaf solid in this corpus round-tripped as native CSG.\n") + + # --- per-part --------------------------------------------------------------------------- + if per_part: + doc.append("## Every part\n") + doc.append( + "One row per leaf solid, folded per module: a whole geometry is several thousand of " + "them and an open list that long is not a document anyone reads.\n") + for module in dict.fromkeys(r["module"] for r in rows): + mrows = [r for r in rows if r["module"] == module] + flat = sum(1 for r in mrows if r["ships"] == "flatcsg") + exact = sum(1 for r in mrows if r["tessellationExact"]) + doc.append(f"
{module} — {len(mrows)} parts, " + f"{flat} FlatCSG, {exact} with an exact tessellation\n") + # Rows that say the same thing about the same family are one row with a count. + groups = {} + for r in mrows: + source = f"`{r['sourceClass'] or '?'}`" + if r["sourceClass"] == "TGeoCompositeShape": + source += f" d{fmt(r['booleanDepth'])}/{fmt(r['leaves'])}l" + faces = ", ".join(f"{v} {k}" for k, v in + sorted((r["surfaceCensus"] or {}).items(), key=lambda kv: -kv[1])) + key = (family(r["volume"] or r["part"]), source, + TIER_LABEL.get(r["ships"], r["ships"] or "-"), r["recogniser"] or "-", + "yes" if r["tessellationExact"] else + ("no" if r["tessellationExact"] is False else "-"), + faces or "-", + "FAIL" if r["knownSourceFailures"] else + ("ok" if r["containsPoints"] else "-")) + entry = groups.setdefault(key, {"n": 0, "example": r["volume"] or r["part"], + "dv": r["dVsym"]}) + entry["n"] += 1 + if r["dVsym"] is not None and (entry["dv"] is None or r["dVsym"] > entry["dv"]): + entry["dv"] = r["dVsym"] + table = [] + for key, entry in sorted(groups.items(), key=lambda kv: (-kv[1]["n"], kv[0][0])): + name = f"`{key[0]}`" + (f" ×{entry['n']}" if entry["n"] > 1 else "") + table.append([name, key[1], key[2], key[3], fmt(entry["dv"]), + key[4], key[5], key[6]]) + collapsed = len(mrows) - len(table) + if collapsed: + doc.append(f"*{len(table)} rows for {len(mrows)} parts; {collapsed} that said the " + "same thing about the same name family are folded into a ×count. " + "`dV_sym` is the worst in the group.*\n") + doc.append(md_table( + ["part family", "source shape", "ships", "recogniser", "worst dV_sym cm^3", + "tess. exact", "faces", "vs source"], + table, ["---", "---", "---", "---", "---:", "---:", "---", "---:"])) + doc.append("\n
\n") + return "\n".join(doc) + + +def render_part(rows, name): + """One part's full record, for `--part`.""" + matches = [r for r in rows if name in (r["part"], r["volume"])] + if not matches: + return f"No part named {name!r} in this corpus.\n" + out = [] + for r in matches: + out.append(f"# {r['volume']} ({r['module']})\n") + pairs = [ + ("artefact stem", r["part"]), + ("source volume", r["sourceVolume"]), + ("source shape", r["sourceClass"]), + ("boolean depth / leaves", None if r["booleanDepth"] is None + else f"{r['booleanDepth']} / {r['leaves']}"), + ("leaf classes", ", ".join(f"{v} {k}" for k, v in (r["leafClasses"] or {}).items()) or None), + ("ships as", TIER_LABEL.get(r["ships"], r["ships"])), + ("recogniser", r["recogniser"]), + ("dV_sym / band", None if r["dVsym"] is None + else f"{fmt(r['dVsym'])} / {fmt(r['band'])} cm^3"), + ("declined CSG because", r["whyNotCSG"]), + ("faces", ", ".join(f"{v} {k}" for k, v in + sorted((r["surfaceCensus"] or {}).items(), key=lambda kv: -kv[1])) or None), + ("tessellation", None if r["tessellationExact"] is None else + ("EXACT -- " + (r["tessellationExactWhy"] or "") if r["tessellationExact"] + else "an approximation -- " + (r["tessellationExactWhy"] or ""))), + ("sidecar bytes", r["sidecarBytes"]), + ("flat sidecar bytes", r["flatSidecarBytes"]), + ("facet bytes", r["facetBytes"]), + ("agrees with source", None if not r["containsPoints"] else + (f"FAILED: {'; '.join(r['knownSourceFailures'])}" if r["knownSourceFailures"] + else f"{r['containsPoints']} points, {r['containsMismatches']} disagreement(s), " + f"capacity {fmt(r['capacityRelativeDeviation'])} relative")), + ] + out.append(md_table(["", ""], [[k, fmt(v)] for k, v in pairs if v is not None])) + out.append("") + return "\n".join(out) + + +HTML_HEAD = """TGeo → STEP → TGeo report + +""" + + +def markdown_to_html(text): + """Just enough markdown for this document: headings, tables, code spans, paragraphs.""" + import re + lines = text.split("\n") + out, table = [], [] + + def flush_table(): + if not table: + return + head = [c.strip() for c in table[0].strip("|").split("|")] + body = [[c.strip() for c in r.strip("|").split("|")] for r in table[2:]] + out.append("" + "".join(f"" for c in head) + + "") + for row in body: + out.append("" + "".join(f"" for c in row) + "") + out.append("
{html.escape(c)}
{inline(c)}
") + table.clear() + + def inline(s): + s = html.escape(s) + s = re.sub(r"`([^`]+)`", r"\1", s) + s = re.sub(r"\*\*([^*]+)\*\*", r"\1", s) + s = re.sub(r"\*([^*]+)\*", r"\1", s) + return s.replace("&rarr;", "→") + + for line in lines: + if line.startswith("|"): + table.append(line) + continue + flush_table() + if line.startswith("{inline(line[4:])}") + elif line.startswith("## "): + out.append(f"

{inline(line[3:])}

") + elif line.startswith("# "): + out.append(f"

{inline(line[2:])}

") + elif line.strip(): + out.append(f"

{inline(line)}

") + flush_table() + return HTML_HEAD + "\n".join(out) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--corpus", required=True, help="root holding one directory per module") + ap.add_argument("--converted-root", help="conversions live here instead of //conv") + ap.add_argument("--subdir", default="conv", help="conversion subdirectory (default: conv)") + ap.add_argument("--modules", help="comma-separated module names (default: all)") + ap.add_argument("--part", help="report on this part only, and print it") + ap.add_argument("--out", help="write the document here (default: stdout)") + ap.add_argument("--html", action="store_true", help="emit HTML instead of markdown") + ap.add_argument("--json", help="also write the joined per-part records here") + ap.add_argument("--no-source-shapes", action="store_true", + help="skip the source-shape description (no ROOT, much faster)") + ap.add_argument("--refresh", action="store_true", + help="recompute the cached original_report.json") + ap.add_argument("--no-per-part", action="store_true", help="summary and matrix only") + args = ap.parse_args() + + modules = [m for m in (args.modules or "").split(",") if m] or None + rows, summaries = read_corpus(args.corpus, args.converted_root, args.subdir, modules, + with_sources=not args.no_source_shapes, refresh=args.refresh) + if not rows: + print("no parts found; is --corpus right, and has anything been converted?", file=sys.stderr) + return 1 + + text = (render_part(rows, args.part) if args.part + else render_markdown(rows, summaries, per_part=not args.no_per_part)) + if args.html: + text = markdown_to_html(text) + if args.out: + Path(args.out).write_text(text) + print(f"wrote {args.out} ({len(rows)} parts, {len(summaries)} module(s))", file=sys.stderr) + else: + print(text) + if args.json: + Path(args.json).write_text(json.dumps({"parts": rows, "modules": summaries}, indent=1)) + print(f"wrote {args.json}", file=sys.stderr) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/runOracleGate.py b/Detectors/CADSupport/validation/runOracleGate.py new file mode 100644 index 0000000000000..d331a4819e012 --- /dev/null +++ b/Detectors/CADSupport/validation/runOracleGate.py @@ -0,0 +1,832 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-07 + +"""Run the exact-surface acceptance gate: CAD -> converter -> solid -> OpenCascade oracle. + +It chains four pieces into one command and prints a per-part verdict: + + 1. `makeTestPartDB.py` converts CAD models, emitting per-part surface sidecars, meshes and + (with --dump-brep) the exact BREP each sidecar was extracted from + 2. `o2-bench-...-solid-harness --dump-samples` + writes the seeded sample sets, which nothing outside the harness can + regenerate + 3. `occtOracle.py` answers those samples from the BREP, in OpenCascade + 4. `o2-bench-...-solid-harness --ref-answers` + scores the exact solid against those answers + +A part passes only if it agrees with the oracle outside the model's own declared tolerance and, +where the representation has the concept, is a closed navigable manifold. + +What the verdict is computed on +------------------------------- +The verdict is computed on **the representation the part ships in**, read from the converter's own +cascade decision (`csg_report.json`, carried into `manifest.json` as a `shipped` block), never from +whichever representation scores best. + + * the historical surface-representation verdict is still computed and printed, on purpose, so + the series stays comparable; + * the other representations keep their full disagreement counts; + * the volume criterion is per representation: `dV_sym` for a CSG part, the 1e-6 capacity band + where capacity is a real measurement, nothing where `Capacity()` is Monte-Carlo sampled. + +`--self-test` pairs every positive case with the negative one that must fail. + +Usage +----- + # generate the synthetic Boolean ladder, convert it, and gate it + runOracleGate.py --fixtures --workdir /tmp/gate + + # gate an existing CAD model + runOracleGate.py --model ../examples/ExcavatorArm.step --workdir /tmp/gate + + # re-score without reconverting (fast iteration on the C++ side) + runOracleGate.py --workdir /tmp/gate --skip-convert +""" + +import argparse +import json +import os +import shutil +import subprocess +import sys +from pathlib import Path + +_HERE = Path(__file__).resolve().parent +import cadsupport_path # noqa: E402,F401 +from cadsupport import occ_env as _occ # noqa: E402 + +# The O2 build tree whose stage/ holds the freshly built binaries; unset means the installed ones. +_BUILD = Path(os.environ["O2_BUILD_DIR"]) if os.environ.get("O2_BUILD_DIR") else None + +# pythonOCC is built against the alibuild Python 3.10; the system interpreter cannot import it. +_OCC_PYTHON = _occ.occ_python() +_OCC_PREFIX = _occ.occ_env_prefix() or {} + + +def occ_env(): + """OCC-first, but *prepended* to the inherited environment rather than replacing it. + + Prepending keeps `import ROOT` working in the subprocess, so one conversion pass can both + recognise a part as CSG and emit its shape. + """ + env = dict(os.environ) + for key, prefix in _OCC_PREFIX.items(): + inherited = env.get(key, "") + env[key] = f"{prefix}:{inherited}" if inherited else prefix + return env + + +def harness_env(): + """The freshly built harness must resolve the freshly built libraries: stage dirs first.""" + env = dict(os.environ) + if _BUILD is None: + return env + stage_libs = f"{_BUILD}/stage/lib:{_BUILD}/stage/lib64" + env["LD_LIBRARY_PATH"] = stage_libs + ":" + env.get("LD_LIBRARY_PATH", "") + return env + + +def run(cmd, **kwargs): + if cmd[0] is None: + raise SystemExit(f"no pythonOCC interpreter: {_occ.UNRESOLVED}") + printable = " ".join(str(c) for c in cmd) + print(f" $ {printable}", flush=True) + result = subprocess.run([str(c) for c in cmd], **kwargs) + if result.returncode != 0: + raise RuntimeError(f"command failed ({result.returncode}): {printable}") + return result + + +def sanitize_part_id(part_id: str) -> str: + """Must match sanitizePartId() in Detectors/CADSupport/test/runSolidHarness.cxx.""" + return "".join(c if (c.isalnum() or c in "-.") else "_" for c in part_id) + + +def find_binary(name: str) -> Path: + """A benchmark binary: from O2_BUILD_DIR's stage/bin when that is set, else from PATH.""" + if _BUILD is not None and (_BUILD / "stage/bin" / name).exists(): + return _BUILD / "stage/bin" / name + found = shutil.which(name) + if found: + if _BUILD is not None: + print(f" [warn] using {found} from PATH; it may be stale relative to {_BUILD}") + return Path(found) + raise RuntimeError(f"{name} not found: set O2_BUILD_DIR to the O2 build tree or load the O2 environment") + + +def find_harness() -> Path: + return find_binary("o2-bench-cadsupport-solid-harness") + + +def rebase_manifest(manifest: dict, db_dir: Path, manifest_path: Path) -> dict: + """Re-root a manifest's absolute paths on the directory it was actually found in. + + Otherwise a copied workdir re-scored with `--skip-convert` silently reads the original one. + """ + recorded = manifest.get("output_dir") + actual = str(db_dir.resolve()) + if not recorded or recorded == actual: + return manifest + print(f" [note] manifest.json was written for {recorded} but lives in {actual}; " + "re-rooting its absolute paths onto this copy") + old = recorded.rstrip("/") + + def rebase(value): + if isinstance(value, str) and (value == old or value.startswith(old + "/")): + return actual + value[len(old):] + return value + + def walk(node): + if isinstance(node, dict): + return {k: walk(v) for k, v in node.items()} + if isinstance(node, list): + return [walk(v) for v in node] + return rebase(node) + + manifest = walk(manifest) + manifest["output_dir"] = actual + manifest["rebased_from"] = recorded + missing = [p["id"] for p in manifest.get("parts", []) if not Path(p["surfaces"]).exists()] + if missing: + raise RuntimeError(f"after re-rooting, {len(missing)} part(s) still have no sidecar " + f"(first: {missing[0]}); the DB copy is incomplete") + manifest_path.write_text(json.dumps(manifest, indent=1)) + return manifest + + +def build_part_db(models, workdir: Path, skip_convert: bool, csg_mode: str = "auto", + mesh_prec=None) -> dict: + db_dir = workdir / "db" + manifest_path = db_dir / "manifest.json" + if skip_convert: + if not manifest_path.exists(): + raise RuntimeError(f"--skip-convert given but {manifest_path} does not exist") + print(f"[1/4] reusing part DB {db_dir}") + return rebase_manifest(json.loads(manifest_path.read_text()), db_dir, manifest_path) + print(f"[1/4] converting {len(models)} model(s) into {db_dir} (--csg {csg_mode})") + db_cmd = [_OCC_PYTHON, _HERE / "makeTestPartDB.py", "--output", db_dir, "--force", + "--csg", csg_mode] + if mesh_prec is not None: + db_cmd += ["--mesh-prec", str(mesh_prec)] + run(db_cmd + ["--models", *models], env=occ_env()) + return rebase_manifest(json.loads(manifest_path.read_text()), db_dir, manifest_path) + + +def dump_samples(harness: Path, db_dir: Path, sample_dir: Path, points: int, rays: int, seed: int, + load_samples: Path = None): + print(f"[2/4] dumping sample sets into {sample_dir}") + sample_dir.mkdir(parents=True, exist_ok=True) + cmd = [harness, "--db", db_dir, "--dump-samples", sample_dir, "--points", points, + "--rays", rays, "--seed", seed, "--only", "contains"] + if load_samples is not None: + # The oracle and the scoring run read the same round-tripped sample file. + cmd += ["--load-samples", load_samples] + run(cmd, env=harness_env(), stdout=subprocess.DEVNULL) + + +def run_oracle(parts, sample_dir: Path, distance_limit: int): + print(f"[3/4] answering {len(parts)} part(s) with OpenCascade") + answered = [] + for part in parts: + brep = part.get("brep") + if not brep or not Path(brep).exists(): + print(f" [skip] {part['id']}: no .brep " + f"(re-run the converter with --dump-brep)") + continue + stem = sanitize_part_id(part["id"]) + samples = sample_dir / f"samples_{stem}.json" + if not samples.exists(): + print(f" [skip] {part['id']}: no sample file {samples.name}") + continue + answers = sample_dir / f"answers_{stem}.json" + run([_OCC_PYTHON, _HERE / "occtOracle.py", "--brep", brep, "--samples", samples, + "--out", answers, "--distance-limit", distance_limit, "--quiet"], env=occ_env()) + answered.append(part["id"]) + return answered + + +def score(harness: Path, db_dir: Path, sample_dir: Path, points: int, rays: int, seed: int, + json_out: Path, load_samples: Path = None): + print(f"[4/4] scoring against the oracle") + cmd = [harness, "--db", db_dir, "--ref-answers", sample_dir, "--points", points, + "--rays", rays, "--seed", seed, "--loop-crosscheck", "--edge-identity", + "--json", json_out] + if load_samples is not None: + cmd += ["--load-samples", load_samples] + run(cmd, env=harness_env()) + return json.loads(json_out.read_text()) + + +def surface_verdict(part_report: dict): + """The historical gate verdict: the exact-surface representation, and only that. + + Kept and still reported for every part, so the series stays comparable; the exit code comes + from `representation_verdict`. Navigability is a precondition, not a score. + """ + reasons = [] + navigation = part_report.get("navigation", {}) + if not navigation.get("navigable", False): + reasons.append(f"not navigable ({navigation.get('reliability', '?')}, " + f"{navigation.get('boundaryEdges', 0)} boundary edges)") + oracle = part_report.get("oracle") + if oracle is None: + reasons.append("no oracle answers") + return False, reasons + if not oracle.get("valid", False): + reasons.append("reference BREP is not BRepCheck-valid") + for key in ("contains", "distout", "distin", "safety"): + column = oracle.get(key) + if column is None: + continue + bad = column.get("nMismatchUnexplained", 0) + column.get("nMismatchMissedSurface", 0) + if bad: + reasons.append(f"{key}: {bad} disagreement(s) outside tolerance " + f"(missed={column.get('nMismatchMissedSurface', 0)})") + relative_capacity = abs(oracle.get("capacityRelativeDeviation", 0.)) + if relative_capacity > 1.e-6: + reasons.append(f"capacity off by {relative_capacity:.3g} relative") + return not reasons, reasons + + +# ------------------------------------------------------------------------------------------ +# The representation-aware verdict +# ------------------------------------------------------------------------------------------ +# Pass/fail on the representation the part actually ships in (read from the converter's cascade +# decision in manifest.json), never on whichever representation happens to score best. + +_VOLUME_BAND_RELATIVE = 1.e-6 + + +def find_representation(part_report: dict, name: str): + for rep in part_report.get("representations") or []: + if rep.get("name") == name: + return rep + return None + + +def volume_criterion(rep: dict, shipped: dict): + """The volume test that is meaningful *for this representation*, and its name. + + Three cases, in priority order: + + * `dV_sym` -- the OCCT symmetric-difference volume the CSG emitter computed against the CAD + solid, in cm^3, against the model's own tolerance band; + * `capacity` -- the 1e-6 relative band the gate has always applied, used wherever the + representation's capacity is a real measurement (`exact-divergence` for the surface solid, + `mesh-divergence` for O2Tessellated). + * nothing -- `TGeoCompositeShape::Capacity()` is Monte-Carlo sampled in ROOT (~1e-2 relative + error), so it is reported and never gated; `capacityComparable` is false there too. + """ + evidence = (shipped or {}).get("evidence") or {} + dv = evidence.get("symmetricDifferenceCm3") + band = evidence.get("bandCm3") + if dv is not None and band is not None: + ok = abs(dv) <= band + return ("dV_sym", ok, abs(dv), band, + f"dV_sym = {abs(dv):.3g} cm^3 against band {band:.3g} cm^3") + oracle = rep.get("oracle") or {} + if rep.get("capacityComparable", False): + rel = abs(oracle.get("capacityRelativeDeviation", 0.)) + return ("capacity", rel <= _VOLUME_BAND_RELATIVE, rel, _VOLUME_BAND_RELATIVE, + f"capacity off by {rel:.3g} relative") + return ("none", True, None, None, + f"no gateable volume measurement ({rep.get('capacityMethod', '?')} is not comparable; " + "reported only)") + + +def representation_verdict(part_report: dict, name: str, shipped: dict): + """Pass/fail for one named representation of one part. + + Same oracle answers and columns as the historical verdict; navigability is required exactly + where `closureApplicable` says it means something. + """ + result = {"representation": name, "pass": False, "reasons": []} + rep = find_representation(part_report, name) + if rep is None: + result["reasons"].append( + f"the part ships as '{name}' but has no '{name}' representation in the scorecard") + return result + result["shapeClass"] = rep.get("shapeClass") + result["source"] = rep.get("source") + result["bboxDeviationFromOracle"] = rep.get("bboxDeviationFromOracle") + reasons = [] + + oracle = rep.get("oracle") + if oracle is None: + result["reasons"].append("no oracle answers") + return result + if not oracle.get("valid", False): + reasons.append("reference BREP is not BRepCheck-valid") + + if rep.get("closureApplicable", False): + if not rep.get("navigable", False): + navigation = part_report.get("navigation", {}) + reasons.append(f"not navigable ({navigation.get('reliability', '?')}, " + f"{navigation.get('boundaryEdges', 0)} boundary edges)") + result["navigable"] = rep.get("navigable") + result["reliability"] = rep.get("reliability") + else: + result["navigable"] = None + result["closureApplicable"] = False + + columns = {} + for key in ("contains", "distout", "distin", "safety"): + column = oracle.get(key) + if column is None: + continue + bad = column.get("nMismatchUnexplained", 0) + column.get("nMismatchMissedSurface", 0) + columns[key] = bad + if bad: + reasons.append(f"{key}: {bad} disagreement(s) outside tolerance " + f"(missed={column.get('nMismatchMissedSurface', 0)})") + result["disagreements"] = columns + + criterion, ok, value, band, text = volume_criterion(rep, shipped) + result["volumeCriterion"] = criterion + result["volumeValue"] = value + result["volumeBand"] = band + result["volumeText"] = text + if not ok: + reasons.append(text) + + result["pass"] = not reasons + result["reasons"] = reasons + return result + + +def shipped_block(part_report: dict, manifest_index: dict): + """Where this part's shipped representation is stated, taken as given. + + A database built before the `shipped` block existed falls back to exact surfaces, and says so + in `decidedBy`. + """ + entry = manifest_index.get(part_report.get("id")) + shipped = (entry or {}).get("shipped") + if shipped: + return dict(shipped) + return {"representation": "surface", "tier": "surface", + "decidedBy": "default (this part DB predates the cascade record)", + "source": None, "evidence": {}} + + +# ------------------------------------------------------------------------------------------ +# The `shape_.root` sidecar: hand-written fixtures for the any-TGeoShape path +# ------------------------------------------------------------------------------------------ +# `box` (a TGeoBBox) and `box_minus_cyl` (box - tube) are exactly ROOT shapes. Each entry is +# (part id, TGeoShape builder); a builder is called only with --fixture-shapes. +def _build_box_shape(): + """`box`: a 20 x 30 x 40 mm box with its corner at the origin, i.e. 2 x 3 x 4 cm. + + TGeoBBox is centred on `fOrigin`, so the offset tests the frame convention. + """ + import ROOT + from array import array + return ROOT.TGeoBBox("shape", 1.0, 1.5, 2.0, array("d", [1.0, 1.5, 2.0])) + + +def _build_box_minus_cyl_shape(): + """`box_minus_cyl`: a 40 mm cube centred on the origin, minus an r = 8 mm axial through-hole. + + In cm: TGeoBBox(2,2,2) - TGeoTube(0, 0.8, 2.5), the tube deliberately longer than the cube so + the hole is a through-hole rather than a blind one with two coincident cap faces. + """ + import ROOT + box = ROOT.TGeoBBox("cube", 2.0, 2.0, 2.0) + drill = ROOT.TGeoTube("drill", 0.0, 0.8, 2.5) + # The boolean node takes ownership of both operands; without this PyROOT frees them first. + ROOT.SetOwnership(box, False) + ROOT.SetOwnership(drill, False) + node = ROOT.TGeoSubtraction(box, drill, ROOT.nullptr, ROOT.nullptr) + ROOT.SetOwnership(node, False) + return ROOT.TGeoCompositeShape("shape", node) + + +_FIXTURE_SHAPES = { + "box/box_0_1_1_1": _build_box_shape, + "box_minus_cyl/box_minus_cyl_0_1_1_1": _build_box_minus_cyl_shape, +} + + +def write_fixture_shapes(manifest: dict): + """Write `shape__.root` next to the sidecar for every fixture that has a builder. + + One TGeoShape under the key "shape", in cm, in the part's own frame, so no `placement` key. + """ + import ROOT + ROOT.gROOT.SetBatch(True) + written = [] + for part in manifest.get("parts", []): + builder = _FIXTURE_SHAPES.get(part["id"]) + if builder is None: + continue + surfaces = Path(part["surfaces"]) + target = surfaces.parent / surfaces.name.replace("surfaces_", "shape_").replace(".bin", ".root") + shape = builder() + out = ROOT.TFile.Open(str(target), "RECREATE") + out.WriteTObject(shape, "shape") + out.Close() + print(f" wrote {target} ({shape.ClassName()}, capacity {shape.Capacity():.6g} cm^3)") + written.append(part["id"]) + if not written: + print(" [warn] --fixture-shapes given but no part in the DB has a builder " + f"(known: {', '.join(sorted(_FIXTURE_SHAPES))})") + return written + + +def column_disagreements(oracle: dict, key: str): + column = oracle.get(key) + if column is None: + return None + return column.get("nMismatchUnexplained", 0) + column.get("nMismatchMissedSurface", 0) + + +def print_representation_scorecard(report: list, shipped_by_id: dict = None): + """One row per (part, representation): the tiered scorecard. It does not feed the exit code. + + * `closure` is "-" wherever it is meaningless (a composite or a mesh has no rims); + * `capacity` is "n/a" wherever TGeoShape::Capacity() is Monte-Carlo sampled; + * `bboxDev` is the frame check: the max deviation, in cm, from the oracle's bounding box. + """ + shipped_by_id = shipped_by_id or {} + rows = [p for p in report if p.get("representations")] + if not rows: + return + print("\n=== REPRESENTATION SCORECARD ===") + print(" (`*` marks the representation the converter's cascade actually ships the part in; " + "that is the one the gate verdict is computed on. The others are measured and reported, " + "not gated -- the mesh columns in particular are the input to the auto-mode fallback " + "policy and are deliberately shown in full.)") + print(f" {' ':<1}{'part':<44} {'repr':<8} {'class':<20} {'contains':>9} {'distout':>9} " + f"{'distin':>9} {'safety':>9} {'capacity':>10} {'bboxDev':>9} closure") + for part_report in rows: + ships = (shipped_by_id.get(part_report["id"]) or {}).get("representation") + for rep in part_report["representations"]: + oracle = rep.get("oracle", {}) + cells = [] + for key in ("contains", "distout", "distin", "safety"): + bad = column_disagreements(oracle, key) + cells.append("-" if bad is None else str(bad)) + if rep.get("capacityComparable", False): + capacity = f"{abs(oracle.get('capacityRelativeDeviation', 0.)):.2e}" + else: + capacity = "n/a" + bbox = rep.get("bboxDeviationFromOracle", -1.) + bbox_text = "-" if bbox is None or bbox < 0. else f"{bbox:.2e}" + if rep.get("closureApplicable", False): + closure = f"{rep.get('reliability', '?')}" + ("" if rep.get("navigable") else " (NOT navigable)") + elif "meshClosedBody" in rep: + closure = f"meshClosedBody={rep['meshClosedBody']}" + else: + closure = "- (not applicable to this representation)" + mark = "*" if rep["name"] == ships else " " + print(f" {mark}{part_report['id']:<44} {rep['name']:<8} " + f"{rep.get('shapeClass', '?'):<20} " + f"{cells[0]:>9} {cells[1]:>9} {cells[2]:>9} {cells[3]:>9} {capacity:>10} " + f"{bbox_text:>9} {closure}") + + # The totals, so a disagreement count is never added up by hand. + print("\n totals per representation (disagreements outside tolerance, summed over parts):") + names = [] + for part_report in rows: + for rep in part_report["representations"]: + if rep["name"] not in names: + names.append(rep["name"]) + for name in names: + totals = {} + parts_with = 0 + clean = 0 + for part_report in rows: + for rep in part_report["representations"]: + if rep["name"] != name: + continue + parts_with += 1 + bad_here = 0 + for key in ("contains", "distout", "distin", "safety"): + bad = column_disagreements(rep.get("oracle", {}), key) + if bad is not None: + totals[key] = totals.get(key, 0) + bad + bad_here += bad + clean += (bad_here == 0) + summary = " ".join(f"{key}={totals.get(key, 0)}" + for key in ("contains", "distout", "distin", "safety")) + print(f" {name:<8} {summary} ({clean}/{parts_with} part(s) with zero disagreements)") + + +# ------------------------------------------------------------------------------------------ +# Self-test: every positive case paired with the negative one that must fail +# ------------------------------------------------------------------------------------------ +# Hand-built part reports in the harness's own shape; every "this passes" is paired with the +# minimal mutation that must turn it red. + +def _fake_column(bad=0): + return {"nCompared": 100, "nMismatchUnexplained": bad, "nMismatchWithinBand": 0, + "nMismatchMissedSurface": 0} + + +def _fake_oracle(bad=0, capacity_dev=0.0, valid=True): + return {"valid": valid, "capacityRelativeDeviation": capacity_dev, + "contains": _fake_column(bad), "distout": _fake_column(bad), + "distin": _fake_column(bad), "safety": _fake_column(bad)} + + +def _fake_part(surface_capacity_dev=0.0, mesh_bad=0, shape_bad=0, navigable=True): + """A part with all three representations: an exact surface solid, a wrong mesh, a CSG shape.""" + return { + "id": "fake/Part_0_1_1_1", + "navigation": {"navigable": navigable, "reliability": "reliable", "boundaryEdges": 0}, + "oracle": _fake_oracle(capacity_dev=surface_capacity_dev), + "representations": [ + {"name": "surface", "shapeClass": "o2::cad::O2BVHSurfaceSolid", + "capacityMethod": "exact-divergence", "capacityComparable": True, + "closureApplicable": True, "navigable": navigable, "reliability": "reliable", + "bboxDeviationFromOracle": 1e-7, + "oracle": _fake_oracle(capacity_dev=surface_capacity_dev)}, + {"name": "mesh", "shapeClass": "o2::base::O2Tessellated", + "capacityMethod": "mesh-divergence", "capacityComparable": True, + "closureApplicable": False, "meshClosedBody": True, + "bboxDeviationFromOracle": 1e-4, + "oracle": _fake_oracle(bad=mesh_bad, capacity_dev=3.0e-4)}, + {"name": "shape", "shapeClass": "TGeoCompositeShape", + "capacityMethod": "root-montecarlo", "capacityComparable": False, + "closureApplicable": False, "bboxDeviationFromOracle": 1e-7, + "oracle": _fake_oracle(bad=shape_bad, capacity_dev=3.3e-4)}, + ], + } + + +_CSG_CLEAN = {"representation": "shape", "tier": "csg", "decidedBy": "test", + "evidence": {"symmetricDifferenceCm3": 0.0, "bandCm3": 1.0e-7}} +_CSG_DIRTY = {"representation": "shape", "tier": "csg", "decidedBy": "test", + "evidence": {"symmetricDifferenceCm3": 1.0e-3, "bandCm3": 1.0e-7}} +_SURFACE = {"representation": "surface", "tier": "surface", "decidedBy": "test", "evidence": {}} +_MESH = {"representation": "mesh", "tier": "mesh", "decidedBy": "test", "evidence": {}} + + +def self_test(verbose=True): + checks = [] + + def check(name, condition, detail=""): + checks.append((name, bool(condition), detail)) + + # 1. Exact as CSG while the surface misses the capacity band: shipped passes, surface fails. + part = _fake_part(surface_capacity_dev=1.39e-6) + shipped = representation_verdict(part, "shape", _CSG_CLEAN) + old_ok, old_reasons = surface_verdict(part) + check("CSG part, clean dV_sym: shipped verdict passes", shipped["pass"], str(shipped["reasons"])) + check("CSG part: surface verdict still fails on capacity", not old_ok, str(old_reasons)) + check("CSG part: the two verdicts disagree (the change is observable)", shipped["pass"] != old_ok) + check("CSG part: gated on dV_sym, never on Capacity()", + shipped["volumeCriterion"] == "dV_sym", shipped["volumeCriterion"]) + + # 2. NEGATIVE CONTROL: a symmetric difference outside the band must fail. + dirty = representation_verdict(part, "shape", _CSG_DIRTY) + check("CSG part with dV_sym outside the band FAILS", not dirty["pass"], str(dirty["reasons"])) + + # 3. A Monte-Carlo capacity is not gated, but real oracle disagreements still fail. + exact_composite = representation_verdict(_fake_part(), "shape", _CSG_CLEAN) + check("exact composite passes despite a 3.3e-4 Monte-Carlo capacity", exact_composite["pass"], + str(exact_composite["reasons"])) + broken = representation_verdict(_fake_part(shape_bad=17), "shape", _CSG_CLEAN) + check("composite with 17 disagreements per column FAILS", not broken["pass"], + str(broken["reasons"])) + + # 4. NEGATIVE CONTROL: a part shipped tessellated fails on the mesh's disagreements. + mesh_part = _fake_part(mesh_bad=411) + mesh_shipped = representation_verdict(mesh_part, "mesh", _MESH) + mesh_surface_ok, _ = surface_verdict(mesh_part) + check("part forced to ship tessellated FAILS on the mesh columns", not mesh_shipped["pass"], + str(mesh_shipped["reasons"])) + check("...while its surface representation is clean (so the failure is the mesh's)", + mesh_surface_ok) + check("mesh volume criterion is its own deterministic capacity, not dV_sym", + mesh_shipped["volumeCriterion"] == "capacity", mesh_shipped["volumeCriterion"]) + + # 5. For a part shipped as `surface` the new and the historical verdicts must agree. + for dev in (0.0, 1.39e-6): + p = _fake_part(surface_capacity_dev=dev) + new = representation_verdict(p, "surface", _SURFACE) + old, _ = surface_verdict(p) + check(f"surface-shipped part (capacity dev {dev:g}): new rule == historical rule", + new["pass"] == old, f"new={new['pass']} old={old}") + + # 6. Navigability is a precondition for a surface solid, never inherited by a CSG shape. + open_part = _fake_part(navigable=False) + check("open surface solid shipped as surface FAILS", + not representation_verdict(open_part, "surface", _SURFACE)["pass"]) + check("the same part shipped as CSG is not failed for the surface solid's rims", + representation_verdict(open_part, "shape", _CSG_CLEAN)["pass"]) + + # 7. A CSG part with no `shape` column must fail, not fall back. + no_shape = _fake_part() + no_shape["representations"] = [r for r in no_shape["representations"] if r["name"] != "shape"] + missing = representation_verdict(no_shape, "shape", _CSG_CLEAN) + check("cascade says CSG but no shape column: FAILS, does not fall back", + not missing["pass"], str(missing["reasons"])) + + # 8. The shipped representation comes from the manifest, even when another scores better. + index = {"fake/Part_0_1_1_1": {"shipped": dict(_MESH)}} + check("shipped representation is read from the manifest, not chosen", + shipped_block(_fake_part(), index)["representation"] == "mesh") + check("a DB with no cascade record falls back to `surface` and says so", + shipped_block(_fake_part(), {})["decidedBy"].startswith("default")) + + failed = [c for c in checks if not c[1]] + if verbose: + for name, ok, detail in checks: + print(f" [{'ok ' if ok else 'FAIL'}] {name}" + (f" {detail}" if not ok else "")) + print(f"\n{len(checks) - len(failed)}/{len(checks)} verdict self-checks passed") + return not failed + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--workdir", type=Path, default=None, + help="scratch directory for the part DB, samples and answers " + "(required unless --self-test)") + parser.add_argument("--model", action="append", default=[], + help="CAD model to gate (repeatable)") + parser.add_argument("--fixtures", action="store_true", + help="generate and gate the synthetic Boolean ladder") + parser.add_argument("--skip-convert", action="store_true", + help="reuse an existing part DB in /db") + parser.add_argument("--fixture-shapes", action="store_true", + help="write the hand-built shape_.root sidecars for the ladder " + "fixtures that are exactly a ROOT shape (box -> TGeoBBox, " + "box_minus_cyl -> TGeoCompositeShape) before scoring, so the " + "any-TGeoShape path is exercised end to end without an emitter. " + "Needs PyROOT; combine with --fixtures or --skip-convert.") + parser.add_argument("--csg", default="auto", choices=["off", "auto", "required"], + help="converter CSG mode for the conversion step (default: %(default)s). " + "'auto' runs the production cascade CSG -> exact surfaces -> " + "tessellated, which is what makes the shipped-representation verdict " + "mean anything; 'off' reproduces the pre-cascade database, in which " + "every scored part ships as `surface` and the two verdicts coincide " + "by construction.") + parser.add_argument("--self-test", action="store_true", + help="run the verdict rule's own positive/negative checks and exit; needs " + "no build, no model and no oracle") + parser.add_argument("--mesh-prec", default=None, + help="meshing precision handed to the converter through " + "makeTestPartDB.py. Unset (default) means the converter's own 0.1, " + "which every gate result on record was produced with, so leaving it " + "alone reproduces them exactly. Set it for a model 0.1 is not safe " + "on: ALICE3 IRIS meshes to ~480 MB of facets at 0.1 and 49 MB at " + "0.25.") + parser.add_argument("--points", type=int, default=2000) + parser.add_argument("--rays", type=int, default=2000) + parser.add_argument("--seed", type=int, default=1) + parser.add_argument("--distance-limit", type=int, default=1000, + help="points per category the oracle computes exact distances for") + parser.add_argument("--transform", default=None, + help="transform applied to every --fixtures shape before conversion, for " + "the position/scale sweep: 'translate:dx,dy,dz' (mm) or 'scale:f'. " + "The STEP is the only shape in the pipeline, so the converter, the " + "sidecar, the mesh and the oracle's .brep all move with it.") + parser.add_argument("--load-samples", type=Path, default=None, + help="reuse a frozen sample set from another run's /oracle " + "instead of generating one. Required to compare a transformed run " + "with its baseline: the generator rejection-samples through the " + "tessellated reference, so a differently-meshed shape otherwise gets " + "different points and the columns are not comparable. Transform the " + "frozen set by the same map first (transformSamples.py).") + args = parser.parse_args() + + if args.self_test: + return 0 if self_test() else 1 + if args.workdir is None: + parser.error("--workdir is required (unless --self-test)") + + args.workdir.mkdir(parents=True, exist_ok=True) + models = list(args.model) + if args.fixtures: + fixture_dir = args.workdir / "fixtures" + print(f"[0/4] generating the Boolean fixture ladder into {fixture_dir}") + fixture_cmd = [_OCC_PYTHON, _HERE / "make_boolean_fixtures.py", "--outdir", fixture_dir] + if args.transform: + fixture_cmd += ["--transform", args.transform] + run(fixture_cmd, env=occ_env()) + models += sorted(str(p) for p in fixture_dir.glob("*.step")) + if not models and not args.skip_convert: + parser.error("give --model and/or --fixtures, or --skip-convert to reuse a DB") + + harness = find_harness() + manifest = build_part_db(models, args.workdir, args.skip_convert, args.csg, + args.mesh_prec) + db_dir = args.workdir / "db" + sample_dir = args.workdir / "oracle" + + if args.fixture_shapes: + print("[1b/4] writing hand-built TGeoShape sidecars for the exactly-representable fixtures") + write_fixture_shapes(manifest) + + dump_samples(harness, db_dir, sample_dir, args.points, args.rays, args.seed, args.load_samples) + run_oracle(manifest.get("parts", []), sample_dir, args.distance_limit) + report = score(harness, db_dir, sample_dir, args.points, args.rays, args.seed, + args.workdir / "gate.json", args.load_samples) + + manifest_index = {p["id"]: p for p in manifest.get("parts", [])} + unscored = manifest.get("unscoredParts", []) + shipped_by_id = {} + + print("\n=== GATE SUMMARY ===") + print(" verdict on the representation the part SHIPS in (the converter's cascade decision, " + "read from csg_report.json);") + print(" the historical surface-representation verdict is printed beside it so the two series " + "stay comparable.") + passed = 0 + surface_passed = 0 + changed = [] + for part_report in report: + shipped = shipped_block(part_report, manifest_index) + shipped_by_id[part_report["id"]] = shipped + result = representation_verdict(part_report, shipped["representation"], shipped) + old_ok, old_reasons = surface_verdict(part_report) + passed += result["pass"] + surface_passed += old_ok + status = "PASS" if result["pass"] else "FAIL" + old_status = "PASS" if old_ok else "FAIL" + if result["pass"] != old_ok: + changed.append((part_report["id"], shipped, old_status, status, old_reasons, + result["reasons"])) + print(f" [{status}] {part_report['id']} ships: {shipped['representation']} " + f"(tier {shipped.get('tier', '?')}, {result.get('shapeClass', '?')})" + f" [surface verdict: {old_status}]") + print(f" volume criterion: {result.get('volumeText', 'n/a')}") + for reason in result["reasons"]: + print(f" {reason}") + # The surface reason is extra only when the part does not ship as `surface`. + if not old_ok and shipped["representation"] != "surface": + for reason in old_reasons: + print(f" (surface representation, reported not gated: {reason})") + # Both verdicts and their provenance go back into gate.json. + part_report["verdict"] = { + "shipped": shipped, + "shippedVerdict": result, + "surfaceVerdict": {"pass": old_ok, "reasons": old_reasons, + "note": "the historical gate verdict, kept for series continuity"}, + } + total = len(report) + print(f"\n{passed}/{total} scored part(s) pass on the representation they ship in") + print(f"{surface_passed}/{total} scored part(s) pass on the surface representation " + "(the historical number, unchanged in definition)") + + # A leaf solid with no exact sidecar cannot be scored, and is reported as such. + if unscored: + print(f"\n{len(unscored)} further leaf solid(s) ship in a representation this harness " + "cannot score, and are therefore NOT counted above:") + for entry in unscored: + ship = entry.get("shipped", {}) + print(f" [UNSCORED] {entry['id']} ships: {ship.get('representation', '?')} " + f"(tier {ship.get('tier', '?')}, {entry.get('nFaces', '?')} faces)") + print(f" {entry.get('reason', '')}") + n_leaf = total + len(unscored) + print(f" => {total} of {n_leaf} leaf solid(s) in the model(s) are scored by this gate.") + + if changed: + print("\n verdicts that changed because the representation changed, not because a " + "measurement did:") + for part_id, shipped, old_status, new_status, old_reasons, new_reasons in changed: + print(f" {part_id}: {old_status} (surface) -> {new_status} " + f"({shipped['representation']}, decided by {shipped.get('decidedBy', '?')})") + for reason in old_reasons: + print(f" surface said: {reason}") + for reason in new_reasons: + print(f" shipped says: {reason}") + + # The gate total and the disagreement counts are printed together, never one without the other. + unexplained = {key: 0 for key in ("contains", "distout", "distin", "safety")} + for part_report in report: + oracle = part_report.get("oracle") or {} + for key in unexplained: + bad = column_disagreements(oracle, key) + if bad is not None: + unexplained[key] += bad + print("oracle disagreements outside tolerance (surface representation): " + + " ".join(f"{key}={value}" for key, value in unexplained.items())) + + print_representation_scorecard(report, shipped_by_id) + + gate_path = args.workdir / "gate.json" + gate_path.write_text(json.dumps(report, indent=1)) + print(f"\nFull report: {gate_path}") + if unscored: + (args.workdir / "unscored.json").write_text(json.dumps(unscored, indent=1)) + # An unscoreable leaf solid is not a pass. + return 0 if passed == total and total > 0 and not unscored else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/runXRayBench.py b/Detectors/CADSupport/validation/runXRayBench.py new file mode 100644 index 0000000000000..834cb036ca2ec --- /dev/null +++ b/Detectors/CADSupport/validation/runXRayBench.py @@ -0,0 +1,353 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Drive the X-ray / geantino transport benchmark end to end, and print the tables. + + CAD -> converter -> raster of rays -> OpenCascade crossing lists -> stepping, two ways -> score + +The transport-loop counterpart of `runOracleGate.py`, whose environment handling and part-database +builder it reuses. Three tables come out: + + 1. CROSSING LISTS vs OpenCascade, per part, per representation, per stepping mode. + 2. ROBUSTNESS -- zero-length steps, non-advancing steps, unterminated transports, odd-length + crossing lists, and mode (a) vs mode (b) disagreements. + 3. VOLUME BY CHORD INTEGRATION, with the raster's achieved precision measured: an instrument for + gross errors and for composites, not for the 1e-06 capacity residuals. + +Usage +----- + # the ladder fixtures, converted fresh + runXRayBench.py --workdir /tmp/xray --fixtures + + # ExcavatorArm + runXRayBench.py --workdir /tmp/xray_bag --model Detectors/CADSupport/examples/ExcavatorArm.step + + # reuse a finished oracle-gate workdir's part DB (no reconversion) + runXRayBench.py --workdir /tmp/xray_bag --reuse-db /tmp/gate_bag/db + + # the quartic witness: the same ladder at one tenth the size + runXRayBench.py --workdir /tmp/xray_x01 --fixtures --transform scale:0.1 +""" + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +_HERE = Path(__file__).resolve().parent + +from runOracleGate import (_OCC_PYTHON, find_binary, harness_env, occ_env, rebase_manifest, run, + sanitize_part_id) + + +def find_benchmark() -> Path: + return find_binary("o2-bench-cadsupport-xray") + + +def build_part_db(models, workdir: Path, csg_mode: str, reuse_db: Path = None) -> dict: + db_dir = workdir / "db" + manifest_path = db_dir / "manifest.json" + if reuse_db is not None: + # A finished gate workdir's `manifest.json` is read in place: it stores absolute paths. + manifest_path = reuse_db / "manifest.json" + if not manifest_path.exists(): + raise RuntimeError(f"--reuse-db given but {manifest_path} does not exist") + print(f"[1/4] reusing part DB {reuse_db}") + return rebase_manifest(json.loads(manifest_path.read_text()), reuse_db, manifest_path) + print(f"[1/4] converting {len(models)} model(s) into {db_dir} (--csg {csg_mode})") + run([_OCC_PYTHON, _HERE / "makeTestPartDB.py", "--output", db_dir, "--force", + "--csg", csg_mode, "--models", *models], env=occ_env()) + return rebase_manifest(json.loads(manifest_path.read_text()), db_dir, manifest_path) + + +def run_oracle(parts, ray_dir: Path): + print(f"[3/4] answering {len(parts)} part(s) with OpenCascade " + "(one call per ray returns every crossing)") + answered = [] + for part in parts: + brep = part.get("brep") + if not brep or not Path(brep).exists(): + print(f" [skip] {part['id']}: no .brep (re-run the converter with --dump-brep)") + continue + stem = sanitize_part_id(part["id"]) + rays = ray_dir / f"xrays_{stem}.json" + if not rays.exists(): + print(f" [skip] {part['id']}: no ray file {rays.name}") + continue + run([_OCC_PYTHON, _HERE / "xrayOracle.py", "--brep", brep, "--rays", rays, + "--out", ray_dir / f"crossings_{stem}.json"], env=occ_env()) + answered.append(part["id"]) + return answered + + +def score(benchmark: Path, db_dir: Path, ray_dir: Path, json_out: Path, extra=()): + print("[4/4] stepping both modes and scoring the crossing lists") + run([benchmark, "--db", db_dir, "--ref-crossings", ray_dir, "--json", json_out, *extra], + env=harness_env()) + return json.loads(json_out.read_text()) + + +# ------------------------------------------------------------------------------------------ +# The three tables +# ------------------------------------------------------------------------------------------ + +_MODES = (("modeA", "(a) shape"), ("modeB", "(b) nav")) + + +def short(part_id: str) -> str: + name = part_id.split("/")[-1] + return name[:34] + + +def print_crossing_table(report): + print("\n=== CROSSING LISTS vs OPENCASCADE ===") + print(" Lists, not aggregates, and LOST is kept apart from DISPLACED. `LOST` is a crossing OCCT " + "found and\n the candidate did not -- a wall a track walks through. `extra` is the " + "reverse. `displaced` is a\n crossing found in the right order but more than the " + "match tolerance away: a wrong step length,\n not a lost wall. `identical` counts rays " + "whose whole ordered list matched.") + header = (f" {'part':<36} {'repr':<8} {'mode':<10} {'identical/rays':>18} {'LOST':>6} " + f"{'extra':>6} {'displaced':>10} {'kind':>5} {'worst dt (cm)':>14}") + print(header) + totals = {} + for part in report: + for rep in part.get("representations", []): + for key, label in _MODES: + block = rep.get(key, {}) + comparison = block.get("vsOracle") + if not comparison: + continue + bucket = totals.setdefault((rep["name"], key), dict( + identical=0, rays=0, missing=0, extra=0, displaced=0, kind=0, worst=0.0, + clean=0, parts=0)) + bucket["identical"] += comparison["raysIdentical"] + bucket["rays"] += comparison["rays"] + bucket["missing"] += comparison["missingCrossings"] + bucket["extra"] += comparison["extraCrossings"] + bucket["displaced"] += comparison.get("displacedCrossings", 0) + bucket["kind"] += comparison["kindMismatch"] + bucket["worst"] = max(bucket["worst"], comparison["worstDeltaT"]) + bucket["parts"] += 1 + bucket["clean"] += (comparison["raysIdentical"] == comparison["rays"]) + print(f" {short(part['id']):<36} {rep['name']:<8} {label:<10} " + f"{comparison['raysIdentical']:>8}/{comparison['rays']:<9} " + f"{comparison['missingCrossings']:>6} {comparison['extraCrossings']:>6} " + f"{comparison.get('displacedCrossings', 0):>10} " + f"{comparison['kindMismatch']:>5} {comparison['worstDeltaT']:>14.3e}") + print("\n totals (gate-style: the count and the denominator, never one without the other)") + for (name, key), bucket in sorted(totals.items()): + label = dict(_MODES)[key] + print(f" {name:<8} {label:<10} {bucket['identical']}/{bucket['rays']} rays identical, " + f"LOST={bucket['missing']} extra={bucket['extra']} " + f"displaced={bucket['displaced']} kind={bucket['kind']} " + f"worst dt={bucket['worst']:.3e} cm " + f"({bucket['clean']}/{bucket['parts']} part(s) fully clean)") + + +_ROBUST_COLUMNS = ( + ("zeroLengthSteps", "zeroStep"), + ("nonAdvancingSteps", "noAdv"), + ("unstickPushes", "unstick"), + ("iterationCapHits", "capHit"), + ("unterminated", "unterm"), + ("oddCrossingLists", "oddList"), + ("nonAlternating", "nonAlt"), + ("duplicateCrossings", "dupXing"), + ("parityMismatchIntervals", "parity"), + ("parityMismatchNearBoundary", "parityNB"), + ("boundaryWithoutTransition", "noTrans"), + ("originOutsideWorld", "outWorld"), + ("originInside", "orgIn"), +) + + +def print_robustness_table(report): + print("\n=== ROBUSTNESS (the part nothing else measures) ===") + print(" zeroStep a step at or below 1e-9 cm unterm the ray ended INSIDE the solid") + print(" noAdv the accumulated distance did not grow oddList odd-length crossing list") + print(" unstick a stalled step repaired with a nudge nonAlt two crossings of the same sense") + print(" capHit the iteration cap was reached dupXing two crossings within tolerance") + print(" parity Contains() at an interval midpoint contradicts the crossing list " + "(the one check\n independent of the stepping -- both modes alternate by " + "construction). parityNB\n is the same event excused because the midpoint is " + "within the match tolerance of the\n boundary, where neither side has a " + "defined answer.") + print(" noTrans mode (b): a boundary was crossed but the volume did not change") + print(" outWorld mode (b): the ray origin was not in the navigator world -- a " + "MISCONFIGURATION of\n this benchmark, never a geometry defect. Any non-zero " + "value invalidates the row.") + head = (f" {'part':<30} {'repr':<8} {'mode':<10} {'steps':>9} " + + " ".join(f"{label:>8}" for _, label in _ROBUST_COLUMNS) + f" {'a-vs-b':>9}") + print(head) + totals = {} + for part in report: + for rep in part.get("representations", []): + for key, label in _MODES: + block = rep.get(key) + if not block: + continue + cells = [block.get(field, 0) for field, _ in _ROBUST_COLUMNS] + bucket = totals.setdefault((rep["name"], key), + dict(steps=0, cells=[0] * len(cells), avb=0, avbrays=0)) + bucket["steps"] += block.get("steps", 0) + for i, value in enumerate(cells): + bucket["cells"][i] += value + avb = "" + if key == "modeB" and rep.get("modeAvsB"): + disagree = rep["modeAvsB"]["rays"] - rep["modeAvsB"]["raysIdentical"] + avb = str(disagree) + bucket["avb"] += disagree + bucket["avbrays"] += rep["modeAvsB"]["rays"] + print(f" {short(part['id']):<30} {rep['name']:<8} {label:<10} " + f"{block.get('steps', 0):>9} " + + " ".join(f"{value:>8}" for value in cells) + f" {avb:>9}") + print("\n totals") + for (name, key), bucket in sorted(totals.items()): + label = dict(_MODES)[key] + summary = " ".join(f"{lbl}={value}" + for (_, lbl), value in zip(_ROBUST_COLUMNS, bucket["cells"])) + print(f" {name:<8} {label:<10} steps={bucket['steps']} {summary}") + if key == "modeB": + print(f" mode (a) vs mode (b): {bucket['avb']} of {bucket['avbrays']} rays " + "disagree") + + +def print_volume_table(report): + print("\n=== VOLUME BY CHORD INTEGRATION ===") + print(" SCOPE, stated before the numbers. The raster's own achieved precision is the " + "`raster` column\n -- OCCT's chord integral over these same rays against OCCT's exact " + "volume. It is a 1e-4 to 1e-5\n instrument at the densities below, which is FOUR TO " + "FIVE ORDERS coarser than the divergence-\n theorem capacity already reported by the " + "oracle gate (1e-11 on exact parts). It cannot resolve\n the 1.3e-06 capacity " + "residuals and must not be quoted as if it could. What it is for: gross\n errors, and " + "composites -- `TGeoCompositeShape::Capacity()` is Monte-Carlo in ROOT (~1e-2) and this\n" + " is the only independent volume those parts have.\n") + print(f" {'part':<30} {'repr':<8} {'N':>4} {'chord V (cm^3)':>16} {'vs OCCT chord':>14} " + f"{'raster vs exact':>16} {'Capacity vs exact':>18}") + for part in report: + oracle = part.get("oracle") + if not oracle: + continue + raster_n = part.get("raster", {}).get("n", 0) + exact = oracle["capacity"] + for rep in part.get("representations", []): + block = rep.get("modeA", {}) + volume = block.get("volumeChordCm3") + if volume is None: + continue + vs_chord = (volume - oracle["volumeChordCm3"]) / oracle["volumeChordCm3"] \ + if oracle["volumeChordCm3"] else 0.0 + capacity_dev = (rep.get("capacity", 0.0) - exact) / exact if exact else 0.0 + print(f" {short(part['id']):<30} {rep['name']:<8} {raster_n:>4} {volume:>16.8g} " + f"{vs_chord:>14.3e} {oracle.get('chordVsExactRelative', 0.0):>16.3e} " + f"{capacity_dev:>18.3e}") + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--workdir", type=Path, required=True) + parser.add_argument("--model", action="append", default=[]) + parser.add_argument("--fixtures", action="store_true") + parser.add_argument("--reuse-db", type=Path, default=None, + help="use an existing part DB (e.g. a finished oracle-gate workdir's db) " + "instead of converting") + parser.add_argument("--transform", default=None, + help="applied to every --fixtures shape before conversion " + "('scale:0.1', 'translate:dx,dy,dz' in mm); the STEP is the only " + "shape in the pipeline, so the oracle moves with it") + parser.add_argument("--csg", default="auto", choices=["off", "auto", "required"]) + parser.add_argument("--raster", type=int, default=48, + help="N x N rays per beam axis (default %(default)s)") + parser.add_argument("--axes", default="xyz") + parser.add_argument("--beams", type=int, default=0, + help="fire N Fibonacci-spiral beam directions instead of the axis beams. " + "A parallel beam is DIRECTION-POOR -- three axes are three directions " + "however many rays are fired -- and the torus quartic defect is " + "invisible to them and visible to a fan.") + parser.add_argument("--tilt", type=float, default=0.0, + help="rotate every beam off its coordinate axis by this many degrees " + "(default %(default)s). An axis-aligned beam is a very special family " + "of ray/surface configurations; a tilted one is generic. Measured: " + "the known torus quartic defect is INVISIBLE at tilt 0 and visible at " + "tilt 12.") + parser.add_argument("--representations", default=None, + help="comma-separated subset of surface,mesh,shape") + parser.add_argument("--margin", type=float, default=1.0e-3, + help="transverse padding of the raster window over the bounding box, cm") + parser.add_argument("--parts", default=None, help="substring filter") + parser.add_argument("--skip-oracle", action="store_true", + help="reuse the crossings_*.json already in /xray") + args = parser.parse_args() + + args.workdir.mkdir(parents=True, exist_ok=True) + models = list(args.model) + if args.fixtures: + fixture_dir = args.workdir / "fixtures" + print(f"[0/4] generating the Boolean fixture ladder into {fixture_dir}") + cmd = [_OCC_PYTHON, _HERE / "make_boolean_fixtures.py", "--outdir", fixture_dir] + if args.transform: + cmd += ["--transform", args.transform] + run(cmd, env=occ_env()) + models += sorted(str(p) for p in fixture_dir.glob("*.step")) + if not models and args.reuse_db is None: + parser.error("give --model and/or --fixtures, or --reuse-db") + + benchmark = find_benchmark() + manifest = build_part_db(models, args.workdir, args.csg, args.reuse_db) + db_dir = args.reuse_db if args.reuse_db is not None else args.workdir / "db" + ray_dir = args.workdir / "xray" + + parts = manifest.get("parts", []) + if args.parts: + parts = [p for p in parts if args.parts in p["id"] or args.parts in p.get("model", "")] + + extra = ["--parts", args.parts] if args.parts else [] + if args.representations: + extra += ["--representations", args.representations] + if not args.skip_oracle: + ray_dir.mkdir(parents=True, exist_ok=True) + run([benchmark, "--db", db_dir, "--dump-rays", ray_dir, "--raster", args.raster, + "--axes", args.axes, "--tilt", args.tilt, "--beams", args.beams, + "--margin", args.margin, *extra], + env=harness_env()) + run_oracle(parts, ray_dir) + else: + print(f"[2-3/4] reusing the crossing lists already in {ray_dir}") + + report = score(benchmark, db_dir, ray_dir, args.workdir / "xray.json", extra) + + print_crossing_table(report) + print_robustness_table(report) + print_volume_table(report) + print(f"\nFull report: {args.workdir / 'xray.json'}") + + # Exit non-zero on a lost or invented crossing, or when the two stepping modes disagree. + bad = 0 + for part in report: + for rep in part.get("representations", []): + for key, _ in _MODES: + comparison = rep.get(key, {}).get("vsOracle") + if comparison: + bad += comparison["missingCrossings"] + comparison["extraCrossings"] \ + + comparison["kindMismatch"] + if rep.get("modeAvsB"): + bad += rep["modeAvsB"]["rays"] - rep["modeAvsB"]["raysIdentical"] + return 0 if bad == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CADSupport/validation/transformSamples.py b/Detectors/CADSupport/validation/transformSamples.py new file mode 100644 index 0000000000000..d2ad3f72ae7da --- /dev/null +++ b/Detectors/CADSupport/validation/transformSamples.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Map a frozen harness sample set through the same transform that was applied to the shape. + +The position/scale sweep needs the transformed run to ask the same questions as the baseline, so +the baseline's samples are pushed through the same `gp_Trsf` and handed over with +`runOracleGate.py --load-samples`. + + * **Ray origins are points, ray directions are directions.** Under a uniform scaling a point is + multiplied by the factor and a unit direction is not -- transforming both as points would + denormalise every direction and silently change what `DistFromOutside` was asked. `gp_Pnt` and + `gp_Dir` are used respectively, so OCCT applies the right one of the two. + * **The spec is in millimetres**, exactly as `make_boolean_fixtures.py --transform` takes it, and + is converted here. The STEP fixtures are written in mm; the sidecars, meshes, `.brep` files and + therefore the sample sets are all in cm (the converter scales by `step_unit_scale_to_cm`). + Taking the same string on both sides removes the one arithmetic step where the two could + silently disagree. + +Usage +----- + transformSamples.py --in /oracle --out /tmp/samples_z400 --transform translate:0,0,4000 + transformSamples.py --in /oracle --out /tmp/samples_x10 --transform scale:10 + +Requires the pythonOCC environment; OCCT's own transform is used, not a reimplementation. +""" + +import argparse +import json +import sys +from pathlib import Path + +from OCC.Core.gp import gp_Dir, gp_Pnt + +from make_boolean_fixtures import parse_transform # noqa: E402 + +# mm -> cm as a divisor, which gives back exactly 400 for 4000 where a 0.1 factor does not. +MM_PER_CM = 10.0 + + +def transform_point(trsf, xyz): + p = gp_Pnt(*xyz) + p.Transform(trsf) + return [p.X(), p.Y(), p.Z()] + + +def transform_direction(trsf, xyz): + d = gp_Dir(*xyz) + d.Transform(trsf) + return [d.X(), d.Y(), d.Z()] + + +def transform_samples(doc, trsf): + out = dict(doc) + out["bboxMin"] = transform_point(trsf, doc["bboxMin"]) + out["bboxMax"] = transform_point(trsf, doc["bboxMax"]) + # Only a negative scale factor could swap min and max, and parse_transform rejects it. + out["points"] = {category: [transform_point(trsf, p) for p in points] + for category, points in doc["points"].items()} + out["rays"] = {category: [{"o": transform_point(trsf, r["o"]), + "d": transform_direction(trsf, r["d"])} for r in rays] + for category, rays in doc["rays"].items()} + return out + + +def to_cm(trsf_spec: str) -> str: + """Re-express a millimetre transform spec in centimetres. Scalings are unit-free.""" + if ";" in trsf_spec: + return ";".join(to_cm(part) for part in trsf_spec.split(";") if part.strip()) + kind, _, rest = trsf_spec.partition(":") + if kind.strip().lower() == "translate": + parts = [float(v) / MM_PER_CM for v in rest.split(",")] + return f"translate:{parts[0]!r},{parts[1]!r},{parts[2]!r}" + return trsf_spec + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--in", dest="indir", required=True, type=Path, + help="directory holding the baseline samples_.json files " + "(a gate run's /oracle)") + ap.add_argument("--out", required=True, type=Path, + help="directory to write the transformed sample sets into") + ap.add_argument("--transform", required=True, + help="the same spec given to make_boolean_fixtures.py --transform; lengths in " + "MILLIMETRES and converted to cm here") + args = ap.parse_args() + + cm_spec = to_cm(args.transform) + trsf, _volume_scale, description = parse_transform(cm_spec) + args.out.mkdir(parents=True, exist_ok=True) + + sources = sorted(args.indir.glob("samples_*.json")) + if not sources: + raise SystemExit(f"no samples_*.json in {args.indir}") + print(f"Transforming {len(sources)} sample set(s) by {description} (cm) " + f"[spec {args.transform} in mm]") + for source in sources: + doc = json.loads(source.read_text()) + (args.out / source.name).write_text(json.dumps(transform_samples(doc, trsf), indent=1)) + print(f" {source.name}") + print(f"Wrote {len(sources)} file(s) into {args.out}") + + +if __name__ == "__main__": + main() diff --git a/Detectors/CADSupport/validation/xrayOracle.py b/Detectors/CADSupport/validation/xrayOracle.py new file mode 100644 index 0000000000000..bf658bae6a78d --- /dev/null +++ b/Detectors/CADSupport/validation/xrayOracle.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 + +# Copyright 2019-2026 CERN and copyright holders of ALICE O2. +# See https://alice-o2.web.cern.ch/copyright for details of the copyright holders. +# All rights not expressly granted are reserved. +# +# This software is distributed under the terms of the GNU General Public +# License v3 (GPL Version 3), copied verbatim in the file "COPYING". +# +# In applying this license CERN does not waive the privileges and immunities +# granted to it by virtue of its status as an Intergovernmental Organization +# or submit itself to any jurisdiction. +# Author: Sandro Wenzel +# Since: 2026-08 + +"""Ground truth for the X-ray transport benchmark: the ORDERED CROSSING LIST along each ray. + +Companion to `Detectors/CADSupport/test/runXRayBenchmark.cxx`. +Reads a ray file written by `o2-bench-cadsupport-xray --dump-rays` and answers exactly those +rays from the part's `.brep`, in OpenCascade. + +`IntCurvesFace_ShapeIntersector` returns every crossing along a ray in one call, so one OCCT call +answers a whole transport. + +How a crossing list is decided +------------------------------ +The raw intersections are *face* hits, not *solid* transitions (a shared edge is hit twice, a +tangent cylinder twice without being entered). So they are only CANDIDATE positions: the MIDPOINT +of every interval between consecutive candidates is classified with `BRepClass3d_SolidClassifier`, +and only positions where the classification changes are kept, which alternates enter/exit by +construction. + +A midpoint the classifier calls ON is a position where OCCT itself has no answer; the ray is +flagged `amb` and the benchmark excludes it rather than scoring it either way. + +The same pass yields the OCCT chord integral (the summed inside-segment length times the raster +cell area), which is the ground-truth column for the benchmark's volume-by-chord-integration. + +Usage +----- + xrayOracle.py --brep .brep --rays /xrays_.json \\ + --out /crossings_.json +""" + +import argparse +import json +import math +import sys +import time +from pathlib import Path + + +from OCC.Core.BRepCheck import BRepCheck_Analyzer +from OCC.Core.BRepClass3d import BRepClass3d_SolidClassifier +from OCC.Core.IntCurvesFace import IntCurvesFace_ShapeIntersector +from OCC.Core.TopAbs import TopAbs_IN, TopAbs_ON, TopAbs_OUT +from OCC.Core.gp import gp_Dir, gp_Lin, gp_Pnt + +from occtOracle import load_solid, shape_tolerance, volume_of + +# Must match kXRayFormatVersion in runXRayBenchmark.cxx. +XRAY_FORMAT_VERSION = 2 + +# A ray parameter this close to the origin is the origin itself, not a crossing. Same constant the +# kernel and occtOracle.py use. +_RAY_EPS = 1.0e-9 + + +class CrossingOracle: + """Stateful OCCT wrapper: the expensive setup happens once per part.""" + + def __init__(self, solid, tolerance: float): + self.solid = solid + self.tolerance = tolerance + self.intersector = IntCurvesFace_ShapeIntersector() + self.intersector.Load(solid, _RAY_EPS) + self.classifier = BRepClass3d_SolidClassifier(solid) + # Candidate positions closer together than this are the same crossing seen through two + # faces (a shared edge) or the same tangency seen twice. Never wider than the model's own + # statement about how well its boundary is defined. + self.merge_tolerance = max(tolerance, _RAY_EPS) + + def candidates(self, origin, direction, tmax): + """Every face intersection parameter in (eps, tmax], sorted and merged.""" + line = gp_Lin(gp_Pnt(*origin), gp_Dir(*direction)) + self.intersector.Perform(line, _RAY_EPS, tmax) + if not self.intersector.IsDone(): + return None + raw = [] + for index in range(1, self.intersector.NbPnt() + 1): + parameter = self.intersector.WParameter(index) + if _RAY_EPS < parameter <= tmax: + raw.append(parameter) + raw.sort() + merged = [] + for parameter in raw: + if merged and parameter - merged[-1] <= self.merge_tolerance: + continue + merged.append(parameter) + return merged + + def classify_at(self, origin, direction, t): + point = gp_Pnt(*(origin[k] + t * direction[k] for k in range(3))) + self.classifier.Perform(point, _RAY_EPS) + state = self.classifier.State() + if state == TopAbs_IN: + return 1 + if state == TopAbs_OUT: + return 0 + if state == TopAbs_ON: + return -1 + raise RuntimeError(f"unexpected classifier state {state}") + + def crossings(self, origin, direction, tmax): + """The ordered crossing list, the inside length, and whether OCCT declined anywhere. + + Returns (t_list, kind_list, inside_length, ambiguous, origin_state). + """ + candidates = self.candidates(origin, direction, tmax) + if candidates is None: + return [], [], 0.0, True, -1 + edges = [0.0] + candidates + [tmax] + states = [] + ambiguous = False + for i in range(len(edges) - 1): + lo, hi = edges[i], edges[i + 1] + if hi <= lo: + states.append(states[-1] if states else 0) + continue + state = self.classify_at(origin, direction, 0.5 * (lo + hi)) + if state < 0: + ambiguous = True + state = states[-1] if states else 0 + states.append(state) + ts, kinds = [], [] + for i in range(1, len(states)): + if states[i] != states[i - 1]: + ts.append(edges[i]) + kinds.append(1 if states[i] == 1 else -1) + inside = 0.0 + for i, state in enumerate(states): + if state == 1: + inside += edges[i + 1] - edges[i] + return ts, kinds, inside, ambiguous, states[0] if states else 0 + + +def answer(brep_path: Path, ray_doc: dict, verbose: bool) -> dict: + if ray_doc.get("version") != XRAY_FORMAT_VERSION: + raise RuntimeError(f"ray file speaks version {ray_doc.get('version')}, " + f"this oracle speaks {XRAY_FORMAT_VERSION}") + solid = load_solid(brep_path) + tolerance = shape_tolerance(solid) + oracle = CrossingOracle(solid, tolerance) + + rays_out = [] + inside_by_beam = {} + ambiguous_rays = 0 + total_crossings = 0 + started = time.time() + for index, ray in enumerate(ray_doc["rays"]): + origin = ray["o"] + direction = ray["d"] + norm = math.sqrt(sum(c * c for c in direction)) + unit = [c / norm for c in direction] + ts, kinds, inside, ambiguous, origin_state = oracle.crossings(origin, unit, ray["tmax"]) + beam = ray["beam"] + inside_by_beam[beam] = inside_by_beam.get(beam, 0.0) + inside + ambiguous_rays += bool(ambiguous) + total_crossings += len(ts) + rays_out.append({"o": origin, "d": direction, "tmax": ray["tmax"], "beam": beam, + "t": ts, "k": kinds, "L": inside, "amb": bool(ambiguous), + "s": origin_state}) + if verbose and (index + 1) % 2000 == 0: + print(f" {index + 1}/{len(ray_doc['rays'])} rays " + f"({time.time() - started:.1f} s)", flush=True) + + # Each beam is an independent estimate of the same volume; the reported number is their mean + # and the per-beam spread is the honest error bar. + cell_area = ray_doc["cellArea"] + labels = [b["label"] for b in ray_doc.get("beams", [])] + per_beam = {} + volumes = [] + for beam, length in sorted(inside_by_beam.items()): + volume = length * cell_area[beam] + per_beam[labels[beam] if beam < len(labels) else str(beam)] = volume + volumes.append(volume) + chord_volume = sum(volumes) / len(volumes) if volumes else 0.0 + + document = dict(ray_doc) + document["rays"] = rays_out + document["tolerance"] = tolerance + document["capacity"] = volume_of(solid) + document["valid"] = bool(BRepCheck_Analyzer(solid).IsValid()) + document["volumeChord"] = chord_volume + document["volumeChordPerBeam"] = per_beam + document["ambiguousRays"] = ambiguous_rays + document["totalCrossings"] = total_crossings + document["oracleSeconds"] = time.time() - started + return document + + +def self_test() -> int: + """Analytic controls: a box, a hollow cylinder, and a sphere's chord integral. + + Every one of them has a closed-form answer, so this checks the oracle against something that + is not another implementation of the same idea. Needs no .brep and no benchmark run. + """ + from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Cut + from OCC.Core.BRepPrimAPI import (BRepPrimAPI_MakeBox, BRepPrimAPI_MakeCylinder, + BRepPrimAPI_MakeSphere) + from occtOracle import load_solid_from_shape + + failures = [] + + def check(name, ok, detail=""): + print(f" [{'ok ' if ok else 'FAIL'}] {name}" + (f" {detail}" if not ok else "")) + if not ok: + failures.append(name) + + # A 2 x 3 x 4 box with its corner at the origin: two crossings on a central x ray. + box = load_solid_from_shape(BRepPrimAPI_MakeBox(2.0, 3.0, 4.0).Shape()) + oracle = CrossingOracle(box, shape_tolerance(box)) + ts, kinds, inside, amb, _ = oracle.crossings([-5.0, 1.5, 2.0], [1.0, 0.0, 0.0], 20.0) + check("box: two crossings", len(ts) == 2, str(ts)) + check("box: at 5 and 7 cm", len(ts) == 2 and abs(ts[0] - 5.0) < 1e-9 and abs(ts[1] - 7.0) < 1e-9, + str(ts)) + check("box: enter then exit", kinds == [1, -1], str(kinds)) + check("box: chord = 2 cm", abs(inside - 2.0) < 1e-9, str(inside)) + + # A hollow cylinder: FOUR crossings along a diameter. + outer = BRepPrimAPI_MakeCylinder(1.0, 4.0).Shape() + inner = BRepPrimAPI_MakeCylinder(0.5, 6.0).Shape() + tube = load_solid_from_shape(BRepAlgoAPI_Cut(outer, inner).Shape()) + oracle = CrossingOracle(tube, shape_tolerance(tube)) + ts, kinds, inside, amb, _ = oracle.crossings([-5.0, 0.0, 2.0], [1.0, 0.0, 0.0], 20.0) + check("hollow cylinder: four crossings along a diameter", len(ts) == 4, str(ts)) + check("hollow cylinder: at 4.0 / 4.5 / 5.5 / 6.0", + len(ts) == 4 and all(abs(a - b) < 1e-7 for a, b in zip(ts, [4.0, 4.5, 5.5, 6.0])), str(ts)) + check("hollow cylinder: in, out, in, out", kinds == [1, -1, 1, -1], str(kinds)) + + # A ray grazing the outer wall tangentially must produce ZERO crossings, not two. + ts, kinds, inside, amb, _ = oracle.crossings([-5.0, 1.0, 2.0], [1.0, 0.0, 0.0], 20.0) + check("tangent ray: no crossings (the classifier overrules the intersector)", + len(ts) == 0, f"{ts} {kinds}") + + # A sphere's chord integral against 4/3 pi r^3, on a structured raster: the volume instrument. + sphere = load_solid_from_shape(BRepPrimAPI_MakeSphere(1.0).Shape()) + oracle = CrossingOracle(sphere, shape_tolerance(sphere)) + exact = 4.0 / 3.0 * math.pi + for n in (16, 32): + window = 1.02 + cell = (2 * window / n) ** 2 + total = 0.0 + for i in range(n): + for j in range(n): + x = -window + (i + 0.5) * 2 * window / n + y = -window + (j + 0.5) * 2 * window / n + _, _, inside, _, _ = oracle.crossings([x, y, -window], [0.0, 0.0, 1.0], 2 * window) + total += inside + volume = total * cell + rel = abs(volume - exact) / exact + print(f" sphere r=1: raster {n:3d} x {n:3d} -> V = {volume:.8f}, " + f"exact {exact:.8f}, relative {rel:.3e}") + check(f"sphere chord integral converges at N={n}", rel < 5.0e-2 / n, f"rel={rel:.3e}") + + print(f"\n{'SELF-TEST PASSED' if not failures else 'SELF-TEST FAILED'}: " + f"{len(failures)} failure(s)") + return 0 if not failures else 1 + + +def main(): + parser = argparse.ArgumentParser( + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument("--brep", type=Path, help="the part's .brep (converter --dump-brep)") + parser.add_argument("--rays", type=Path, help="xrays_.json from --dump-rays") + parser.add_argument("--out", type=Path, help="where to write crossings_.json") + parser.add_argument("--quiet", action="store_true") + parser.add_argument("--self-test", action="store_true", + help="analytic controls (box, hollow cylinder, tangent ray, sphere " + "volume); needs no .brep and no benchmark run") + args = parser.parse_args() + + if args.self_test: + return self_test() + if not (args.brep and args.rays and args.out): + parser.error("--brep, --rays and --out are required (unless --self-test)") + + ray_doc = json.loads(args.rays.read_text()) + document = answer(args.brep, ray_doc, verbose=not args.quiet) + args.out.write_text(json.dumps(document)) + if not args.quiet: + print(f" {args.out}: {len(document['rays'])} rays, {document['totalCrossings']} crossings, " + f"{document['ambiguousRays']} ambiguous, chord volume " + f"{document['volumeChord']:.8g} cm^3 vs OCCT capacity {document['capacity']:.8g} cm^3 " + f"({document['oracleSeconds']:.1f} s)") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/Detectors/CMakeLists.txt b/Detectors/CMakeLists.txt index 09e784b0338ee..289fb3fcd0fa7 100644 --- a/Detectors/CMakeLists.txt +++ b/Detectors/CMakeLists.txt @@ -10,6 +10,7 @@ # or submit itself to any jurisdiction. add_subdirectory(Base) +add_subdirectory(CADSupport) add_subdirectory(Raw) add_subdirectory(CTF) diff --git a/Detectors/External/CMakeLists.txt b/Detectors/External/CMakeLists.txt index ea5f0b53e2b8e..c5bbd3bd8052c 100644 --- a/Detectors/External/CMakeLists.txt +++ b/Detectors/External/CMakeLists.txt @@ -14,7 +14,8 @@ o2_add_library(ExternalDetectors PUBLIC_LINK_LIBRARIES O2::DetectorsBase O2::SimulationDataFormat O2::CommonUtils - RapidJSON::RapidJSON) + RapidJSON::RapidJSON + PRIVATE_LINK_LIBRARIES O2::CADSupport) o2_target_root_dictionary(ExternalDetectors HEADERS include/ExternalDetectors/Hit.h diff --git a/Detectors/External/include/ExternalDetectors/ExternalDetector.h b/Detectors/External/include/ExternalDetectors/ExternalDetector.h index fbfc30bd5e148..43e7ce5e58118 100644 --- a/Detectors/External/include/ExternalDetectors/ExternalDetector.h +++ b/Detectors/External/include/ExternalDetectors/ExternalDetector.h @@ -13,7 +13,7 @@ /// \brief Sensitive detector built from an externally provided (CAD-derived) geometry /// /// ExternalDetector is the sensitive counterpart of o2::passive::ExternalModule. -/// It injects a CAD-derived TGeo geometry (produced by scripts/geometry/O2_CADtoTGeo.py) +/// It injects a CAD-derived TGeo geometry (produced by Detectors/CADSupport/tools/O2_CADtoTGeo.py) /// and turns a configurable set of its volumes (selected by medium or volume name) into /// sensitive volumes which produce hits. It derives from o2::base::DetImpl, so it /// transparently participates in the full o2-sim hit forwarding/merging machinery diff --git a/Detectors/External/src/ExternalDetector.cxx b/Detectors/External/src/ExternalDetector.cxx index f79d6ce6f2505..663c8065835cb 100644 --- a/Detectors/External/src/ExternalDetector.cxx +++ b/Detectors/External/src/ExternalDetector.cxx @@ -10,7 +10,7 @@ // or submit itself to any jurisdiction. #include "ExternalDetectors/ExternalDetector.h" -#include "DetectorsBase/CADGeometryUtils.h" +#include "CADSupport/CADGeometryUtils.h" #include "DetectorsBase/Stack.h" #include "CommonUtils/ConfigurationMacroHelper.h" #include "CommonUtils/FileSystemUtils.h" @@ -120,14 +120,14 @@ void ExternalDetector::collectSensitiveVolumeNames(TGeoVolume* vol, std::set -#include +#include #include #include #include @@ -34,14 +34,14 @@ ExternalModule::ExternalModule(const char* name, const char* long_title, Externa void ExternalModule::ConstructGeometry() { // JIT the geom builder macro and obtain the top most module volume - auto module_top = o2::base::buildCADVolumeFromMacro(mOptions.root_macro_file, GetName()); + auto module_top = o2::cad::buildCADVolumeFromMacro(mOptions.root_macro_file, GetName()); if (!module_top) { LOG(error) << "No module geometry could be built from " << mOptions.root_macro_file; return; } // bring the CAD media under O2's MaterialManager - o2::base::remapCADMedia(module_top, GetName()); + o2::cad::remapCADMedia(module_top, GetName()); // place it into the provided anchor volume (needs to exist) auto anchor = gGeoManager->FindVolumeFast(mOptions.anchor_volume.c_str()); diff --git a/run/SimExamples/External_Sensitive_Detectors/README.md b/run/SimExamples/External_Sensitive_Detectors/README.md index d36a6efa5e297..1f77df6225b5a 100644 --- a/run/SimExamples/External_Sensitive_Detectors/README.md +++ b/run/SimExamples/External_Sensitive_Detectors/README.md @@ -51,4 +51,4 @@ External sensitive detector hits: Append entries to `externalDetectors.json` (each on a different free DetID slot) and list their names in `detectorlist.json`. The mechanism is fully data-driven; nothing needs to be rebuilt. -See also `Detectors/External` and `scripts/geometry/O2_CADtoTGeo.py`. +See also `Detectors/External` and `Detectors/CADSupport/tools/O2_CADtoTGeo.py`. diff --git a/run/SimExamples/External_Sensitive_Detectors/geometry_innerCylinder.macro b/run/SimExamples/External_Sensitive_Detectors/geometry_innerCylinder.macro index 78bec83f42213..0bb5ce64b6532 100644 --- a/run/SimExamples/External_Sensitive_Detectors/geometry_innerCylinder.macro +++ b/run/SimExamples/External_Sensitive_Detectors/geometry_innerCylinder.macro @@ -2,7 +2,7 @@ // // This is a hand-written stand-in for the macros that O2_CADtoTGeo.py produces from CAD // files. It exports the same builder-hook symbol (get_builder_hook_unchecked) that -// o2::base::buildCADVolumeFromMacro expects, so it is injected exactly like a CAD module +// o2::cad::buildCADVolumeFromMacro expects, so it is injected exactly like a CAD module // through the external-geometry JSON. Media are placeholders here; remapCADMedia() // re-registers them under O2's MaterialManager at construction time. #include diff --git a/run/SimExamples/External_Sensitive_Detectors/geometry_outerDisk.macro b/run/SimExamples/External_Sensitive_Detectors/geometry_outerDisk.macro index 13907ebb94a5b..16a2a34eb109c 100644 --- a/run/SimExamples/External_Sensitive_Detectors/geometry_outerDisk.macro +++ b/run/SimExamples/External_Sensitive_Detectors/geometry_outerDisk.macro @@ -1,7 +1,7 @@ // Geometry builder for the "BDISK" artificial detector: a thin silicon endcap disk. // // Hand-written stand-in for a CAD-exported macro (see geometry_innerCylinder.macro). Exports the -// same builder-hook symbol consumed by o2::base::buildCADVolumeFromMacro. Together with ACYL +// same builder-hook symbol consumed by o2::cad::buildCADVolumeFromMacro. Together with ACYL // this gives two geometrically distinct artificial detectors (a barrel shell and an endcap // disk) sharing one generic o2::ext::Hit type but writing into separate DetID slots. #include diff --git a/scripts/geometry/O2_CADtoTGeo.py b/scripts/geometry/O2_CADtoTGeo.py deleted file mode 100644 index 836488444c50e..0000000000000 --- a/scripts/geometry/O2_CADtoTGeo.py +++ /dev/null @@ -1,1700 +0,0 @@ -#!/usr/bin/env python3 -""" -A Python script, doing a deep STEP/XCAF -> ROOT TGeo conversion. -For now, all CAD solids are simply meshed. The ROOT geometry is build as a C++ ROOT macro -and facet data is stored in binary form to keep disc space minimal. - -NEW (03/2026): - - Optional material/medium emission from a BOM (bill of materials) CSV file. - The CSV is expected to contain lines like: - CAD, Mechanical/Part, , , , , , ... - - If both a part mass and a CAD volume are available, an effective density is computed - and used in the emitted TGeoMaterial. Otherwise a reasonable default density is used - for a few common materials, or 1.0 g/cm^3 as fallback. - -Generates (into --output-folder): - - geom.C (small ROOT macro) - - facets__.bin for each leaf logical volume (float32 triangles) - -Facet file format (little-endian): - uint32 nTriangles - then nTriangles * 9 * float32: - ax ay az bx by bz cx cy cz - -VOLNAME is a filename-safe version of the XCAF label name when available (e.g. "nut"), -and LID is the XCAF label entry (e.g. "0:1:1:7" -> "0_1_1_7") to keep filenames unique. - -Naming: - - C++ variable names stay based on XCAF label entry (e.g. 0:1:1:7) for uniqueness. - - ROOT object names (TGeoVolume / TGeoTessellated / TGeoVolumeAssembly) use the label's - human name when available (e.g. "nut", "rod-assembly"), falling back to the entry. - -Units: - - By default, the script tries to detect the STEP LENGTH unit by scanning the STEP file - header/contents (common patterns like .MILLI. / .CENTI. / .METRE. / INCH / FOOT). - - You can override with --step-unit {auto,mm,cm,m,in,ft}. TGeo expects cm. - -Author: - - Sandro Wenzel, CERN (02/2026) - - Material/BOM integration patch (03/2026) -""" - -import warnings -warnings.filterwarnings("ignore", message=".*all to deprecated function.*", category=DeprecationWarning) - -import argparse -import csv -import json -import math -import re -import struct -from dataclasses import dataclass -from pathlib import Path as _Path -from typing import Dict, List, Optional, Pattern, Tuple - -from OCC.Core.Bnd import Bnd_Box -from OCC.Core.BRepAlgoAPI import BRepAlgoAPI_Common -from OCC.Core.BRepBndLib import brepbndlib -from OCC.Core.BRepBuilderAPI import BRepBuilderAPI_Transform -from OCC.Core.BRepMesh import BRepMesh_IncrementalMesh -from OCC.Core.BRepPrimAPI import BRepPrimAPI_MakeBox -from OCC.Core.BRep import BRep_Tool -from OCC.Core.TopLoc import TopLoc_Location -from OCC.Core.TopAbs import TopAbs_REVERSED -from OCC.Extend.TopologyUtils import TopologyExplorer - -from OCC.Core.STEPCAFControl import STEPCAFControl_Reader -from OCC.Core.TDocStd import TDocStd_Document -from OCC.Core.XCAFDoc import XCAFDoc_DocumentTool -from OCC.Core.IFSelect import IFSelect_RetDone - -from OCC.Core.TDF import TDF_Label, TDF_LabelSequence, TDF_Tool -from OCC.Core.TCollection import TCollection_AsciiString -from OCC.Core.gp import gp_Pnt, gp_Trsf - -# volume properties for density calcs (may not be present in older pythonOCC builds) -try: - from OCC.Core.GProp import GProp_GProps - from OCC.Core.BRepGProp import brepgprop_VolumeProperties - _HAS_VOLPROPS = True -except Exception: - _HAS_VOLPROPS = False - - -# ------------------------------- -# STEP/XCAF loading -# ------------------------------- - -def load_step_with_xcaf(path: str): - doc = TDocStd_Document("pythonocc-doc") - reader = STEPCAFControl_Reader() - reader.SetColorMode(True) - reader.SetNameMode(True) - reader.SetLayerMode(True) - - status = reader.ReadFile(path) - if status != IFSelect_RetDone: - raise RuntimeError(f"STEP read failed for: {path}") - - reader.Transfer(doc) - shape_tool = XCAFDoc_DocumentTool.ShapeTool(doc.Main()) - return doc, shape_tool - - -def label_id(label: TDF_Label) -> str: - s = TCollection_AsciiString() - TDF_Tool.Entry(label, s) - return s.ToCString() - - -def label_name(label: TDF_Label) -> str: - # Uses the XCAF/STEP name when present; can be empty. - try: - n = label.GetLabelName() - if n: - return str(n) - except Exception: - pass - return "" - - -# ------------------------------- -# Units -# ------------------------------- - -def step_unit_scale_to_cm(step_unit: str) -> float: - step_unit = (step_unit or "auto").lower() - if step_unit == "mm": - return 0.1 - if step_unit == "cm": - return 1.0 - if step_unit == "m": - return 100.0 - if step_unit == "in": - return 2.54 - if step_unit == "ft": - return 30.48 - raise ValueError(f"Unknown --step-unit {step_unit} (use auto, mm, cm, m, in, ft)") - - -def detect_step_length_unit(step_path: str) -> str: - """ - Heuristic unit detection by scanning STEP file text for common unit tokens. - This avoids relying on OCCT APIs that can vary across pythonOCC builds. - - Returns one of: mm, cm, m, in, ft. Defaults to mm if uncertain. - """ - p = _Path(step_path) - # STEP can be huge: read only the first few MB; units are near the header. - max_bytes = 4 * 1024 * 1024 - data = p.open("rb").read(max_bytes).decode("latin-1", errors="ignore").upper() - - if ".MILLI." in data: - return "mm" - if ".CENTI." in data: - return "cm" - if ".METRE." in data or ".METER." in data: - return "m" - if "INCH" in data: - return "in" - if "FOOT" in data or "FEET" in data: - return "ft" - - # Conservative default for mechanical CAD STEP is mm - return "mm" - - -@dataclass(frozen=True) -class ClipBox: - xmin: float - ymin: float - zmin: float - xmax: float - ymax: float - zmax: float - - @classmethod - def from_values(cls, values: List[float]) -> "ClipBox": - if len(values) != 6: - raise ValueError("--clip-box expects 6 values: xmin ymin zmin xmax ymax zmax") - xmin, ymin, zmin, xmax, ymax, zmax = (float(v) for v in values) - if not (xmin < xmax and ymin < ymax and zmin < zmax): - raise ValueError("--clip-box requires xmin Tuple[float, float, float, float, float, float]: - return (self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.zmax) - - -@dataclass(frozen=True) -class NameFilter: - include: Tuple[Pattern[str], ...] - exclude: Tuple[Pattern[str], ...] - - @classmethod - def from_patterns(cls, include: List[str], exclude: List[str], case_sensitive: bool = False) -> "NameFilter": - flags = 0 if case_sensitive else re.IGNORECASE - return cls( - tuple(re.compile(pattern, flags) for pattern in include), - tuple(re.compile(pattern, flags) for pattern in exclude), - ) - - @property - def active(self) -> bool: - return bool(self.include or self.exclude) - - @property - def has_include(self) -> bool: - return bool(self.include) - - def _text(self, lid: str, name: str) -> str: - return f"{name} {lid}".strip() - - def matches_include(self, lid: str, name: str) -> bool: - text = self._text(lid, name) - return any(pattern.search(text) for pattern in self.include) - - def matches_exclude(self, lid: str, name: str) -> bool: - text = self._text(lid, name) - return any(pattern.search(text) for pattern in self.exclude) - - -# ------------------------------- -# Triangulation helpers -# ------------------------------- - -def _scale_triangles(triangles, s: float): - if s == 1.0: - return triangles - out = [] - for (a, b, c) in triangles: - out.append(( - (a[0] * s, a[1] * s, a[2] * s), - (b[0] * s, b[1] * s, b[2] * s), - (c[0] * s, c[1] * s, c[2] * s), - )) - return out - - -def triangulate_asbbox(shape, scale_to_cm: float = 1.0): - box = Bnd_Box() - brepbndlib.Add(shape, box) - xmin, ymin, zmin, xmax, ymax, zmax = box.Get() - - p000 = (xmin, ymin, zmin) - p001 = (xmin, ymin, zmax) - p010 = (xmin, ymax, zmin) - p011 = (xmin, ymax, zmax) - p100 = (xmax, ymin, zmin) - p101 = (xmax, ymin, zmax) - p110 = (xmax, ymax, zmin) - p111 = (xmax, ymax, zmax) - - triangles = [ - (p000, p100, p110), (p000, p110, p010), - (p001, p111, p101), (p001, p011, p111), - (p000, p101, p100), (p000, p001, p101), - (p010, p110, p111), (p010, p111, p011), - (p000, p010, p011), (p000, p011, p001), - (p100, p101, p111), (p100, p111, p110), - ] - return _scale_triangles(triangles, scale_to_cm) - - -def triangulate_CAD_solid(my_solid, meshparam, scale_to_cm: float = 1.0): - lin_defl = float(meshparam.get("lin_defl", 0.1)) - ang_defl = float(meshparam.get("ang_defl", 0.1)) - - parallel = True - try: - BRepMesh_IncrementalMesh(my_solid, lin_defl, False, ang_defl, bool(parallel)) - except TypeError: - BRepMesh_IncrementalMesh(my_solid, lin_defl, False, ang_defl) - - triangles = [] - for face in TopologyExplorer(my_solid).faces(): - loc = TopLoc_Location() - triangulation = BRep_Tool.Triangulation(face, loc) - if triangulation is None: - continue - - trsf = loc.Transformation() - reverse = (face.Orientation() == TopAbs_REVERSED) - - for i in range(1, triangulation.NbTriangles() + 1): - tri = triangulation.Triangle(i) - n1, n2, n3 = tri.Get() - - p1 = triangulation.Node(n1).Transformed(trsf) - p2 = triangulation.Node(n2).Transformed(trsf) - p3 = triangulation.Node(n3).Transformed(trsf) - - if reverse: - p2, p3 = p3, p2 - - triangles.append(( - (p1.X(), p1.Y(), p1.Z()), - (p2.X(), p2.Y(), p2.Z()), - (p3.X(), p3.Y(), p3.Z()), - )) - - return _scale_triangles(triangles, scale_to_cm) - - -# ------------------------------- -# Volume helpers (for density) -# ------------------------------- - -def volume_cm3_of_shape(shape, scale_to_cm: float) -> float: - """Compute CAD solid volume in cm^3 (using STEP->cm scale).""" - if _HAS_VOLPROPS: - try: - props = GProp_GProps() - brepgprop_VolumeProperties(shape, props) - # volume returned in STEP length units^3 - v = float(props.Mass()) - return v * (scale_to_cm ** 3) - except Exception: - pass - - # Fallback: bounding-box volume (rough but always defined) - box = Bnd_Box() - brepbndlib.Add(shape, box) - xmin, ymin, zmin, xmax, ymax, zmax = box.Get() - dx, dy, dz = (xmax - xmin) * scale_to_cm, (ymax - ymin) * scale_to_cm, (zmax - zmin) * scale_to_cm - return max(dx, 0.0) * max(dy, 0.0) * max(dz, 0.0) - - -# ------------------------------- -# Naming helpers -# ------------------------------- - -def sanitize_cpp_name(s: str) -> str: - safe = re.sub(r"[^0-9a-zA-Z]", "_", s) - if not safe: - safe = "x" - if not (safe[0].isalpha() or safe[0] == "_"): - safe = "_" + safe - return safe - - -def sanitize_filename(s: str) -> str: - safe = re.sub(r"[^0-9a-zA-Z]", "_", s) - return safe or "x" - - -# ------------------------------- -# Binary facet IO -# ------------------------------- - -def write_facets_bin(path: _Path, triangles): - path.parent.mkdir(parents=True, exist_ok=True) - with open(path, "wb") as f: - f.write(struct.pack(" str: - return (self.part_number or "").strip() - - @property - def name_key(self) -> str: - return (self.name or "").strip() - - -def _to_float(s: str) -> Optional[float]: - try: - if s is None: - return None - s = str(s).strip() - if not s: - return None - return float(s) - except Exception: - return None - - -def read_bom_csv(csv_path: str) -> List[BomEntry]: - """ - Reads a BOM CSV in the format provided by design team. - - We look for rows whose first column is 'CAD' and second is 'Mechanical/Part'. - Columns (0-based): - 0 CAD - 1 type - 2 part number - 3 revision - 4 name/description - 5 mass - 6 material - """ - entries: List[BomEntry] = [] - with open(csv_path, newline="", encoding="utf-8", errors="ignore") as f: - reader = csv.reader(f) - for row in reader: - if not row: - continue - if len(row) < 7: - continue - if row[0].strip() != "CAD": - continue - if row[1].strip() != "Mechanical/Part": - continue - - part_no = (row[2] or "").strip() - rev = (row[3] or "").strip() - name = (row[4] or "").strip() - mass = _to_float(row[5]) - mat = (row[6] or "").strip() - - if not (part_no or name): - continue - if mass is None: - mass = float("nan") - if not mat: - mat = "Default" - - entries.append(BomEntry(part_no, rev, name, float(mass), mat)) - return entries - - - -def normalize_material_name(mat: str) -> str: - """ - Normalizes a BOM material string for matching / caching. - - Note: We keep the *original* string for ROOT object names; this is only used - internally for robust matching and dictionary keys. - """ - mat = (mat or "Default").strip() - mat = re.sub(r"\s+", " ", mat) - return mat - - -def _norm_tokens(s: str) -> List[str]: - s = (s or "").lower() - # common grade/format noise - s = re.sub(r"\(.*?\)", " ", s) - s = s.replace("en aw", " ") - s = s.replace("en-aw", " ") - s = s.replace("en", " ") - s = s.replace("aw", " ") - s = s.replace("_", " ").replace("-", " ") - s = re.sub(r"[^a-z0-9]+", " ", s) - s = re.sub(r"\s+", " ", s).strip() - if not s: - return [] - toks = s.split(" ") - - # small synonym normalization - syn = { - "alu": "al", - "aluminium": "aluminum", - "silicium": "silicon", - "inox": "stainless", - "ss": "stainless", - "cu": "copper", - "fe": "iron", - "ptfe": "teflon", - "ti": "titanium", - "be": "beryllium", - } - - # Expand common element symbols to names and vice-versa so that e.g. "G4_Si" can match "silicon". - elem_alias = { - "h": "hydrogen", "he": "helium", "c": "carbon", "n": "nitrogen", "o": "oxygen", - "al": "aluminum", "si": "silicon", "fe": "iron", "cu": "copper", "be": "beryllium", - "mg": "magnesium", "mn": "manganese", "cr": "chromium", "ni": "nickel", "zn": "zinc", - "ti": "titanium", "w": "tungsten", "pb": "lead", "sn": "tin", - } - name_to_sym = {v: k for k, v in elem_alias.items()} - - out: List[str] = [] - for t in toks: - t2 = syn.get(t, t) - out.append(t2) - if t2 in elem_alias: - out.append(elem_alias[t2]) - if t2 in name_to_sym: - out.append(name_to_sym[t2]) - - # de-dup while preserving order - seen = set() - out2: List[str] = [] - for t in out: - if t and t not in seen: - seen.add(t) - out2.append(t) - return out2 - - -def _density_score(rho_part: Optional[float], rho_ref: Optional[float]) -> float: - if rho_part is None or rho_ref is None or not (rho_part > 0.0) or not (rho_ref > 0.0): - return 0.0 - # symmetric score in log-space; 1.0 is perfect match - d = abs(math.log(rho_ref / rho_part)) - return 1.0 / (1.0 + d) - - -def _token_score(tokens_a: List[str], tokens_b: List[str]) -> float: - if not tokens_a or not tokens_b: - return 0.0 - sa = set(tokens_a) - sb = set(tokens_b) - inter = len(sa & sb) - union = len(sa | sb) - if union == 0: - return 0.0 - return inter / union - - -def load_g4_nist_db(json_path: str) -> Dict[str, dict]: - """ - Loads a JSON dump created by the 'nist_export_all' tool. - Returns a dict: nist_name -> material record. - """ - with open(json_path, "r", encoding="utf-8") as f: - data = json.load(f) - mats = data.get("materials", {}) - if not isinstance(mats, dict) or not mats: - raise RuntimeError(f"G4 NIST DB JSON seems empty or malformed: {json_path}") - return mats - -# Minimal periodic table for parsing custom alloys not present in NIST. -# Values: Z (atomic number), A (g/mol) -_ELEMENT_TABLE = { - "H": (1, 1.00794), - "C": (6, 12.0107), - "N": (7, 14.0067), - "O": (8, 15.9994), - "Al": (13, 26.9815385), - "Si": (14, 28.0855), - "Fe": (26, 55.845), - "Cu": (29, 63.546), - "Be": (4, 9.0121831), - "Mg": (12, 24.305), - "Mn": (25, 54.938044), - "Cr": (24, 51.9961), - "Ni": (28, 58.6934), - "Zn": (30, 65.38), - "Ti": (22, 47.867), - "W": (74, 183.84), - "Pb": (82, 207.2), - "Sn": (50, 118.71), -} - - -@dataclass -class ResolvedMaterial: - bom_name: str - nist_name: Optional[str] # e.g. "G4_Al" - score: float - rho_used_g_cm3: Optional[float] # density used in ROOT definition - radlen_cm: Optional[float] - intlen_cm: Optional[float] - elements: Optional[List[dict]] # list of {symbol,Z,A_g_mol,mass_fraction} - note: str # for comments in geom.C (warnings/FIXME) - -@dataclass -class MatMatchConfig: - # Minimum combined score to accept a match. - min_score: float = 0.35 - # If (best - second_best) < ambiguity_delta, treat as ambiguous/unresolved. - ambiguity_delta: float = 0.05 - # Weights for the combined score = w_token * token_score + w_density * density_score - w_token: float = 0.75 - w_density: float = 0.25 - # Optional hard filter on density proximity (in log-space). If <=0, disabled. - # Example: max_log_density_diff=0.8 means accept within exp(0.8)~2.2x in either direction. - max_log_density_diff: float = 0.0 - # Penalize compound matches (oxide/dioxide/carbide/...) when BOM doesn't mention those tokens. - compound_penalty: float = 0.25 - - -def resolve_bom_material( - bom_material: str, - rho_part_g_cm3: Optional[float], - g4db: Optional[Dict[str, dict]], - cfg: MatMatchConfig, -) -> ResolvedMaterial: - """ - Resolves an arbitrary BOM material string to a Geant4 NIST material name using: - - exact key match (BOM already uses e.g. "G4_Al") - - token overlap scoring on names - - density proximity scoring (if rho_part_g_cm3 available) - - If unresolved/ambiguous, tries to parse element symbols from the BOM string (e.g. "Cu Be") - and emits a placeholder mixture (equal mass fractions) annotated with FIXME. - """ - raw_bom_material = (bom_material or "").strip() - bom_material = normalize_material_name(bom_material) - - if not g4db: - return ResolvedMaterial( - bom_name=bom_material, - nist_name=None, - score=0.0, - rho_used_g_cm3=rho_part_g_cm3, - radlen_cm=None, - intlen_cm=None, - elements=None, - note="FIXME: No Geant4 NIST DB provided; using dummy material.", - ) - - # Trivial: BOM already provides an exact Geant4 material key - if bom_material in g4db: - rec = g4db[bom_material] - rho_ref = rec.get("density_g_cm3") - # Use NIST density for emission; CAD-derived density is used only for matching. - rho_used = rho_ref - - rad = rec.get("radlen_cm") - itl = rec.get("intlen_cm") - - return ResolvedMaterial( - bom_name=bom_material, - nist_name=bom_material, - score=1.0, - rho_used_g_cm3=rho_used, - radlen_cm=rad, - intlen_cm=itl, - elements=rec.get("elements", []), - note="Resolved by exact Geant4 NIST name from BOM.", - ) - - bom_toks = _norm_tokens(bom_material) - if not bom_toks: - return ResolvedMaterial( - bom_name=bom_material, - nist_name=None, - score=0.0, - rho_used_g_cm3=rho_part_g_cm3, - radlen_cm=None, - intlen_cm=None, - elements=None, - note="FIXME: Empty/unknown BOM material string; using dummy material.", - ) - - def _build_custom_from_elements(note_prefix: str) -> Optional[ResolvedMaterial]: - s = raw_bom_material - if not s: - return None - - symbols = set(re.findall(r"\b([A-Z][a-z]?)\b", s)) - name_to_symbol = { - "aluminum": "Al", "aluminium": "Al", "silicon": "Si", "iron": "Fe", "copper": "Cu", - "beryllium": "Be", "magnesium": "Mg", "manganese": "Mn", "chromium": "Cr", "nickel": "Ni", - "zinc": "Zn", "titanium": "Ti", "tungsten": "W", "lead": "Pb", "tin": "Sn", - } - for t in bom_toks: - if t in name_to_symbol: - symbols.add(name_to_symbol[t]) - - symbols = [sym for sym in sorted(symbols) if sym in _ELEMENT_TABLE] - if not symbols: - return None - - frac = 1.0 / float(len(symbols)) - elems: List[dict] = [] - for sym in symbols: - Z, A = _ELEMENT_TABLE[sym] - elems.append({"symbol": sym, "Z": Z, "A_g_mol": A, "mass_fraction": frac}) - - return ResolvedMaterial( - bom_name=bom_material, - nist_name=None, - score=0.0, - rho_used_g_cm3=rho_part_g_cm3, - radlen_cm=None, - intlen_cm=None, - elements=elems, - note=f"FIXME: {note_prefix} No suitable Geant4 NIST material. Emitting placeholder mixture from parsed elements {symbols} with equal mass fractions; please adjust fractions/material.", - ) - - best = (None, -1.0, 0.0, 0.0) # (nist_name, score, dens_score, token_score) - second = (None, -1.0, 0.0, 0.0) - - bom_has_compound = any(t in bom_toks for t in ( - "oxide", "dioxide", "carbide", "nitride", "fluoride", "chloride", - "sulfate", "phosphate", "glass", "dioxyde" - )) - - for nist_name, rec in g4db.items(): - nist_toks = _norm_tokens(nist_name) - ts = _token_score(bom_toks, nist_toks) - if ts <= 0.0: - continue - - ds = _density_score(rho_part_g_cm3, rec.get("density_g_cm3")) - - # Optional hard density filter - if cfg.max_log_density_diff and cfg.max_log_density_diff > 0.0 and rho_part_g_cm3 and rec.get("density_g_cm3"): - try: - if abs(math.log(float(rec.get("density_g_cm3")) / float(rho_part_g_cm3))) > cfg.max_log_density_diff: - continue - except Exception: - pass - - nist_has_compound = any(t in nist_toks for t in ( - "oxide", "dioxide", "carbide", "nitride", "fluoride", "chloride", - "sulfate", "phosphate", "glass", "dioxyde" - )) - compound_pen = cfg.compound_penalty if (nist_has_compound and not bom_has_compound) else 0.0 - - score = cfg.w_token * ts + cfg.w_density * ds - compound_pen - - if score > best[1]: - second = best - best = (nist_name, score, ds, ts) - elif score > second[1]: - second = (nist_name, score, ds, ts) - - nist_best, score_best, ds_best, ts_best = best - nist_second, score_second, _, _ = second - - if nist_best is None or score_best < cfg.min_score: - custom = _build_custom_from_elements("Could not resolve with enough confidence.") - if custom is not None: - return custom - return ResolvedMaterial( - bom_name=bom_material, - nist_name=None, - score=float(score_best if score_best > 0 else 0.0), - rho_used_g_cm3=rho_part_g_cm3, - radlen_cm=None, - intlen_cm=None, - elements=None, - note="FIXME: Could not resolve BOM material to a Geant4 NIST material with enough confidence; using dummy material.", - ) - - if score_second > 0 and (score_best - score_second) < cfg.ambiguity_delta: - custom = _build_custom_from_elements( - f"Ambiguous material match (best '{nist_best}' score={score_best:.3f}, second '{nist_second}' score={score_second:.3f})." - ) - if custom is not None: - return custom - return ResolvedMaterial( - bom_name=bom_material, - nist_name=None, - score=float(score_best), - rho_used_g_cm3=rho_part_g_cm3, - radlen_cm=None, - intlen_cm=None, - elements=None, - note=f"FIXME: Ambiguous material match (best '{nist_best}' score={score_best:.3f}, second '{nist_second}' score={score_second:.3f}); using dummy material.", - ) - - rec = g4db[nist_best] - rho_ref = rec.get("density_g_cm3") - # Use NIST density for emission; CAD-derived density is used only for matching. - rho_used = rho_ref - - rad = rec.get("radlen_cm") - itl = rec.get("intlen_cm") - - return ResolvedMaterial( - bom_name=bom_material, - nist_name=nist_best, - score=float(score_best), - rho_used_g_cm3=rho_used, - radlen_cm=rad, - intlen_cm=itl, - elements=rec.get("elements", []), - note=f"Resolved to '{nist_best}' (token={ts_best:.3f}, density={ds_best:.3f}, score={score_best:.3f}).", - ) - - -def build_volume_to_material_map( - bom_entries: List[BomEntry], - def_names: Dict[str, str], -) -> Dict[str, BomEntry]: - """ - Builds a mapping def_lid -> BomEntry by matching the XCAF display name to: - - exact part_number match - - exact description/name match - - substring match on part_number within the XCAF name - - This is heuristic; if nothing matches we keep no assignment for that volume. - """ - # lookup tables - by_part: Dict[str, BomEntry] = {} - by_name: Dict[str, BomEntry] = {} - for e in bom_entries: - if e.part_number_key: - by_part[e.part_number_key] = e - if e.name_key and e.name_key not in by_name: - by_name[e.name_key] = e - - out: Dict[str, BomEntry] = {} - for lid, disp in def_names.items(): - key = (disp or "").strip() - if not key: - continue - - # 1) exact part number - if key in by_part: - out[lid] = by_part[key] - continue - # 2) exact name/description - if key in by_name: - out[lid] = by_name[key] - continue - # 3) substring match on any part number - for pn, e in by_part.items(): - if pn and pn in key: - out[lid] = e - break - return out - - -# ------------------------------- -# C++ emission helpers -# ------------------------------- - -def trsf_to_tgeo(trsf: gp_Trsf, name: str, scale_to_cm: float) -> str: - m = trsf.GetRotation().GetMatrix() - t = trsf.TranslationPart() - return f""" - Double_t {name}_m[9] = {{ - {m.Value(1,1)}, {m.Value(1,2)}, {m.Value(1,3)}, - {m.Value(2,1)}, {m.Value(2,2)}, {m.Value(2,3)}, - {m.Value(3,1)}, {m.Value(3,2)}, {m.Value(3,3)} - }}; - TGeoRotation *{name}_rot = new TGeoRotation(); - {name}_rot->SetMatrix({name}_m); - TGeoCombiTrans *{name} = new TGeoCombiTrans({t.X()*scale_to_cm}, {t.Y()*scale_to_cm}, {t.Z()*scale_to_cm}, {name}_rot); -""" - - -def emit_cpp_prelude() -> str: - return """#include -#include -#include -#include -#include -#include - -static void LoadFacets(const std::string& file, TGeoTessellated* solid, bool check=false) -{ - std::ifstream in(file, std::ios::binary); - if (!in) throw std::runtime_error("Cannot open facet file: " + file); - - uint32_t nTri = 0; - in.read(reinterpret_cast(&nTri), sizeof(nTri)); - if (!in) throw std::runtime_error("Bad facet header in: " + file); - - for (uint32_t i=0;i(v), sizeof(v)); - if (!in) throw std::runtime_error("Unexpected EOF in: " + file); - - solid->AddFacet(TGeoTessellated::Vertex_t(v[0],v[1],v[2]), - TGeoTessellated::Vertex_t(v[3],v[4],v[5]), - TGeoTessellated::Vertex_t(v[6],v[7],v[8])); - } - solid->CloseShape(check, true); -} -""" - - -def emit_materials_cpp( - used_materials: Dict[str, ResolvedMaterial], - # key: BOM material string as used in CSV after normalization -) -> Tuple[str, Dict[str, str]]: - """ - Emits C++ code defining TGeoMaterial/TGeoMixture + TGeoMedium for all used materials. - - - If a material resolved to a Geant4 NIST entry, emit a physically correct mixture - (element mass fractions) and set RadLen/IntLen (from Geant4) when available. - - If unresolved/ambiguous, emit a dummy material and annotate with FIXME comments. - """ - cpp: List[str] = [] - cpp.append(" // Default material/medium (placeholder; can be replaced later)") - cpp.append(" TGeoMaterial *mat_Default = new TGeoMaterial(\"Default\", 0., 0., 0.);") - cpp.append(" TGeoMedium *med_Default = new TGeoMedium(\"Default\", 1, mat_Default);") - cpp.append("") - - emitted_el: Dict[str, str] = {} - - def _emit_element(el: dict) -> str: - sym = el.get("symbol", "X") - Z = int(el.get("Z", 0)) - A = float(el.get("A_g_mol", 0.0)) - if sym in emitted_el: - return emitted_el[sym] - safe = sanitize_cpp_name(sym) - var = f"el_{safe}" - cpp.append(f" TGeoElement *{var} = new TGeoElement(\"{sym}\", \"{sym}\", {Z}, {A:.10g});") - emitted_el[sym] = var - return var - - medium_var: Dict[str, str] = {"Default": "med_Default"} - next_id = 2 - - for bom_mat in sorted(used_materials.keys(), key=lambda s: s.lower()): - rm = used_materials[bom_mat] - safe = sanitize_cpp_name(bom_mat) - base = safe - k = 2 - while f"med_{safe}" in medium_var.values(): - safe = f"{base}_{k}" - k += 1 - - rho = rm.rho_used_g_cm3 if (rm.rho_used_g_cm3 and rm.rho_used_g_cm3 > 0.0) else 0.0 - - cpp.append(f" // BOM material: {rm.bom_name}") - cpp.append(f" // {rm.note}") - - if rm.elements: - elems = rm.elements - if len(elems) == 1 and abs(float(elems[0].get('mass_fraction', 1.0)) - 1.0) < 1e-6: - el = elems[0] - A = float(el.get("A_g_mol", 0.0)) - Z = float(el.get("Z", 0)) - cpp.append(f" TGeoMaterial *mat_{safe} = new TGeoMaterial(\"{bom_mat}\", {A:.10g}, {Z:.10g}, {rho:.10g});") - else: - cpp.append(f" TGeoMixture *mat_{safe} = new TGeoMixture(\"{bom_mat}\", {len(elems)}, {rho:.10g});") - for el in elems: - elvar = _emit_element(el) - w = float(el.get("mass_fraction", 0.0)) - cpp.append(f" mat_{safe}->AddElement({elvar}, {w:.10g});") - - if rm.radlen_cm is not None and rm.intlen_cm is not None: - cpp.append(f" mat_{safe}->SetRadLen({float(rm.radlen_cm):.10g}, {float(rm.intlen_cm):.10g});") - elif rm.radlen_cm is not None: - cpp.append(f" mat_{safe}->SetRadLen({float(rm.radlen_cm):.10g});") - else: - cpp.append(" // FIXME: Unresolved material. Replace with a proper TGeoMaterial/TGeoMixture.") - cpp.append(f" TGeoMaterial *mat_{safe} = new TGeoMaterial(\"{bom_mat}\", 0., 0., {rho:.10g});") - - cpp.append(f" TGeoMedium *med_{safe} = new TGeoMedium(\"{bom_mat}\", {next_id}, mat_{safe});") - cpp.append("") - medium_var[bom_mat] = f"med_{safe}" - next_id += 1 - - return "\n".join(cpp), medium_var - - - - -def emit_tessellated_cpp(lid: str, vol_display_name: str, facet_abspath: str, ntriangles: int, medium_var: str) -> str: - safe = sanitize_cpp_name(lid) - shape_name = vol_display_name if vol_display_name else lid - - if ntriangles <= 0: - out = [] - out.append(f' TGeoBBox *solid_{safe} = new TGeoBBox("{shape_name}", 0.001, 0.001, 0.001);') - out.append(f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});') - return "\n".join(out) - - out = [] - out.append(f' TGeoTessellated *solid_{safe} = new TGeoTessellated("{shape_name}", {ntriangles});') - out.append(f' LoadFacets("{facet_abspath}", solid_{safe}, check);') - out.append(f' TGeoVolume *vol_{safe} = new TGeoVolume("{shape_name}", solid_{safe}, {medium_var});') - return "\n".join(out) - - -def emit_assembly_cpp(lid: str, asm_display_name: str) -> str: - safe = sanitize_cpp_name(lid) - name = asm_display_name if asm_display_name else lid - return f' TGeoVolumeAssembly *asm_{safe} = new TGeoVolumeAssembly("{name}");' - - -# ------------------------------- -# CAD clipping helpers -# ------------------------------- - -def make_clip_box_shape(clip_box: ClipBox): - return BRepPrimAPI_MakeBox( - gp_Pnt(clip_box.xmin, clip_box.ymin, clip_box.zmin), - gp_Pnt(clip_box.xmax, clip_box.ymax, clip_box.zmax), - ).Shape() - - -def _compose_trsf(parent_to_world: gp_Trsf, local_to_parent: gp_Trsf) -> gp_Trsf: - return parent_to_world.Multiplied(local_to_parent) - - -def _shape_is_empty(shape) -> bool: - if shape is None: - return True - try: - if shape.IsNull(): - return True - except Exception: - pass - try: - for _ in TopologyExplorer(shape).faces(): - return False - return True - except Exception: - return False - - -def _transformed_bbox(shape, trsf: gp_Trsf) -> Optional[Tuple[float, float, float, float, float, float]]: - box = Bnd_Box() - brepbndlib.Add(shape, box) - try: - xmin, ymin, zmin, xmax, ymax, zmax = box.Get() - except Exception: - return None - - points = [] - for x in (xmin, xmax): - for y in (ymin, ymax): - for z in (zmin, zmax): - p = gp_Pnt(x, y, z) - p.Transform(trsf) - points.append((p.X(), p.Y(), p.Z())) - - return ( - min(p[0] for p in points), - min(p[1] for p in points), - min(p[2] for p in points), - max(p[0] for p in points), - max(p[1] for p in points), - max(p[2] for p in points), - ) - - -def _bbox_outside_clip_box(bbox: Tuple[float, float, float, float, float, float], clip_box: ClipBox) -> bool: - xmin, ymin, zmin, xmax, ymax, zmax = bbox - return ( - xmax < clip_box.xmin or xmin > clip_box.xmax or - ymax < clip_box.ymin or ymin > clip_box.ymax or - zmax < clip_box.zmin or zmin > clip_box.zmax - ) - - -def _bbox_inside_clip_box(bbox: Tuple[float, float, float, float, float, float], clip_box: ClipBox) -> bool: - xmin, ymin, zmin, xmax, ymax, zmax = bbox - return ( - xmin >= clip_box.xmin and xmax <= clip_box.xmax and - ymin >= clip_box.ymin and ymax <= clip_box.ymax and - zmin >= clip_box.zmin and zmax <= clip_box.zmax - ) - - -def _classify_shape_against_clip_box(shape, clip_box: ClipBox, local_to_world: gp_Trsf) -> Optional[str]: - world_bbox = _transformed_bbox(shape, local_to_world) - if world_bbox is None: - return None - if _bbox_outside_clip_box(world_bbox, clip_box): - return "outside" - if _bbox_inside_clip_box(world_bbox, clip_box): - return "inside" - return "overlap" - - -def clip_shape_to_box(shape, clip_box: ClipBox, clip_box_shape, local_to_world: gp_Trsf, lid: str): - clip_state = _classify_shape_against_clip_box(shape, clip_box, local_to_world) - if clip_state is None: - return None - if clip_state == "outside": - return None - if clip_state == "inside": - return shape - - local_clip = BRepBuilderAPI_Transform(clip_box_shape, local_to_world.Inverted(), True).Shape() - common = BRepAlgoAPI_Common(shape, local_clip) - common.Build() - if not common.IsDone(): - raise RuntimeError(f"Failed to clip CAD shape {lid} against --clip-box") - - clipped = common.Shape() - if _shape_is_empty(clipped): - return None - return clipped - - -# ------------------------------- -# Definition graph extraction -# ------------------------------- - -logical_volumes: Dict[str, list] = {} # def_lid -> triangles -def_names: Dict[str, str] = {} # def_lid -> human display name (may be "") -def_volumes_cm3: Dict[str, float] = {} # def_lid -> volume in cm^3 (leaf only) -assemblies = set() # def_lid -placements = [] # (parent_def_lid, child_def_lid, gp_Trsf local) -top_defs = set() # top definition lids -visited_defs = set() # expanded defs - - -def cpp_var_for_def(lid: str) -> str: - safe = sanitize_cpp_name(lid) - return f"asm_{safe}" if lid in assemblies else f"vol_{safe}" - - -def expand_definition( - def_label: TDF_Label, - shape_tool, - meshparam=None, - scale_to_cm: float = 1.0, - clip_box: Optional[ClipBox] = None, - clip_box_shape=None, - clip_deduplicate: str = "intact", - name_filter: Optional[NameFilter] = None, - include_subtree: bool = False, - world_trsf: Optional[gp_Trsf] = None, - occ_path: str = "r1", -) -> Optional[str]: - clip_enabled = clip_box_shape is not None - if world_trsf is None: - world_trsf = gp_Trsf() - - def_lid = label_id(def_label) - nm = label_name(def_label) - - subtree_included = include_subtree - if name_filter is not None: - if name_filter.matches_exclude(def_lid, nm): - return None - if name_filter.has_include and name_filter.matches_include(def_lid, nm): - subtree_included = True - - if clip_enabled and clip_box is not None: - try: - shape_for_clip = shape_tool.GetShape(def_label) - except Exception: - shape_for_clip = None - if shape_for_clip is not None: - clip_state = _classify_shape_against_clip_box(shape_for_clip, clip_box, world_trsf) - if clip_state == "outside": - return None - if clip_state == "inside" and clip_deduplicate == "intact": - return expand_definition( - def_label, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=None, - clip_box_shape=None, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - include_subtree=subtree_included, - ) - - def_key = f"{def_lid}@{occ_path}" if clip_enabled else def_lid - if not clip_enabled and def_lid in visited_defs: - return def_lid - if not clip_enabled: - visited_defs.add(def_lid) - - if nm and def_key not in def_names: - def_names[def_key] = nm - elif def_key not in def_names: - def_names[def_key] = "" - - children = TDF_LabelSequence() - shape_tool.GetComponents(def_label, children) - has_children = children.Length() > 0 - - if has_children or shape_tool.IsAssembly(def_label): - assemblies.add(def_key) - kept_children = 0 - - for i in range(children.Length()): - child = children.Value(i + 1) - child_occ_path = f"{occ_path}_{i + 1}" - if shape_tool.IsReference(child): - referred = TDF_Label() - shape_tool.GetReferredShape(child, referred) - - loc = shape_tool.GetLocation(child) - trsf = loc.Transformation() - if clip_enabled: - child_key = expand_definition( - referred, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=clip_box, - clip_box_shape=clip_box_shape, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - include_subtree=subtree_included, - world_trsf=_compose_trsf(world_trsf, trsf), - occ_path=child_occ_path, - ) - if child_key is None: - continue - placements.append((def_key, child_key, trsf)) - else: - child_key = expand_definition( - referred, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - include_subtree=subtree_included, - ) - if child_key is None: - continue - placements.append((def_key, child_key, trsf)) - else: - trsf = gp_Trsf() - if clip_enabled: - child_key = expand_definition( - child, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=clip_box, - clip_box_shape=clip_box_shape, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - include_subtree=subtree_included, - world_trsf=world_trsf, - occ_path=child_occ_path, - ) - if child_key is None: - continue - placements.append((def_key, child_key, trsf)) - else: - child_key = expand_definition( - child, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - include_subtree=subtree_included, - ) - if child_key is None: - continue - placements.append((def_key, child_key, trsf)) - kept_children += 1 - - if (clip_enabled or (name_filter is not None and name_filter.has_include)) and kept_children == 0: - assemblies.discard(def_key) - return None - return def_key - - if shape_tool.IsSimpleShape(def_label): - if name_filter is not None and name_filter.has_include and not subtree_included: - return None - - if def_key not in logical_volumes: - shape = shape_tool.GetShape(def_label) - - # store volume (for density estimation) - try: - volume_cm3 = volume_cm3_of_shape(shape, scale_to_cm=scale_to_cm) - except Exception: - volume_cm3 = 0.0 - - if clip_enabled: - shape = clip_shape_to_box(shape, clip_box, clip_box_shape, world_trsf, def_lid) - if shape is None: - return None - - def_volumes_cm3[def_key] = volume_cm3 - - do_meshing = (meshparam is not None) and meshparam.get("do_meshing", None) is True - logical_volumes[def_key] = triangulate_CAD_solid(shape, meshparam=meshparam, scale_to_cm=scale_to_cm) if do_meshing else triangulate_asbbox(shape, scale_to_cm=scale_to_cm) - return def_key - - assemblies.add(def_key) - return def_key - - -def extract_graph( - step_path: str, - meshparam=None, - scale_to_cm: float = 1.0, - clip_box: Optional[ClipBox] = None, - clip_deduplicate: str = "intact", - name_filter: Optional[NameFilter] = None, -): - global logical_volumes, def_names, def_volumes_cm3, assemblies, placements, top_defs, visited_defs - logical_volumes = {} - def_names = {} - def_volumes_cm3 = {} - assemblies = set() - placements = [] - top_defs = set() - visited_defs = set() - - doc, shape_tool = load_step_with_xcaf(step_path) - clip_box_shape = make_clip_box_shape(clip_box) if clip_box is not None else None - - roots = TDF_LabelSequence() - shape_tool.GetFreeShapes(roots) - - for i in range(roots.Length()): - root = roots.Value(i + 1) - root_occ_path = f"r{i + 1}" - if shape_tool.IsReference(root): - ref = TDF_Label() - shape_tool.GetReferredShape(root, ref) - top = expand_definition( - ref, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=clip_box, - clip_box_shape=clip_box_shape, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - occ_path=root_occ_path, - ) - else: - top = expand_definition( - root, - shape_tool, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=clip_box, - clip_box_shape=clip_box_shape, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - occ_path=root_occ_path, - ) - if top is not None: - top_defs.add(top) - - return doc, shape_tool - - -# ------------------------------- -# ROOT macro emission -# ------------------------------- - -def emit_placement_cpp(parent_def: str, child_def: str, trsf: gp_Trsf, copy_no: int, scale_to_cm: float) -> str: - parent_cpp = cpp_var_for_def(parent_def) - child_cpp = cpp_var_for_def(child_def) - tr_name = f"tr_{sanitize_cpp_name(parent_def)}_{sanitize_cpp_name(child_def)}_{copy_no}" - return trsf_to_tgeo(trsf, tr_name, scale_to_cm) + f" {parent_cpp}->AddNode({child_cpp}, {copy_no}, {tr_name});\n" - - - -def _compute_density_g_cm3( - volume_cm3: float, - mass_value: float, - mass_unit: str, -) -> Tuple[Optional[float], str]: - """ - Computes an effective part density from (mass, CAD volume). - - Returns (rho_g_cm3 or None, comment). If rho is None, caller should fall back - to the Geant4 NIST density (if resolved) or to a dummy density. - """ - if not volume_cm3 or volume_cm3 <= 0: - return None, "no CAD volume available for density" - - if (mass_value is None) or (isinstance(mass_value, float) and math.isnan(mass_value)): - return None, "no BOM mass available for density" - - mass_g = float(mass_value) - mu = (mass_unit or "kg").lower() - if mu == "kg": - mass_g *= 1000.0 - elif mu == "g": - pass - else: - # unknown unit: assume kg - mass_g *= 1000.0 - - rho = mass_g / float(volume_cm3) - # Guard against obvious unit/volume issues - if not (0.01 < rho < 50.0): - return None, f"computed density {rho:.3g} g/cm3 rejected (unit mismatch?)" - - return rho, "density from BOM mass and CAD volume" - - -def emit_root_macro( - step_path: str, - out_folder: _Path, - meshparam=None, - step_unit: str = "auto", - clip_box: Optional[ClipBox] = None, - clip_deduplicate: str = "intact", - name_filter: Optional[NameFilter] = None, - materials_csv: Optional[str] = None, - bom_mass_unit: str = "kg", - g4_nist_json: Optional[str] = None, - mat_cfg: Optional[MatMatchConfig] = None, -): - if (step_unit or "auto").lower() == "auto": - detected = detect_step_length_unit(step_path) - scale_to_cm = step_unit_scale_to_cm(detected) - print(f"Detected STEP length unit: {detected} (scale to cm = {scale_to_cm})") - else: - scale_to_cm = step_unit_scale_to_cm(step_unit) - print(f"Using overridden STEP length unit: {step_unit} (scale to cm = {scale_to_cm})") - - if clip_box is not None: - print(f"Clipping CAD geometry to STEP-coordinate bounding box: {clip_box.as_tuple()}") - print(f"Clip deduplication mode: {clip_deduplicate}") - - if name_filter is not None and name_filter.active: - print(f"CAD name filters: {len(name_filter.include)} include regex(es), {len(name_filter.exclude)} exclude regex(es)") - - extract_graph( - step_path, - meshparam=meshparam, - scale_to_cm=scale_to_cm, - clip_box=clip_box, - clip_deduplicate=clip_deduplicate, - name_filter=name_filter, - ) - - out_folder = out_folder.expanduser().resolve() - out_folder.mkdir(parents=True, exist_ok=True) - - - # --- Geant4 NIST material DB (optional but recommended) --- - g4db: Optional[Dict[str, dict]] = None - if g4_nist_json: - g4db = load_g4_nist_db(g4_nist_json) - print(f"Loaded Geant4 NIST DB with {len(g4db)} materials from: {g4_nist_json}") - else: - print("No --g4-nist-json provided: unresolved materials will fall back to dummy ROOT materials.") - mat_cfg = mat_cfg or MatMatchConfig() - - - # --- BOM: map volumes to materials (heuristic) --- - lid_to_bom: Dict[str, BomEntry] = {} - if materials_csv: - bom_entries = read_bom_csv(materials_csv) - lid_to_bom = build_volume_to_material_map(bom_entries, def_names) - print(f"Loaded {len(bom_entries)} BOM entries from: {materials_csv}") - print(f"Matched {len(lid_to_bom)} CAD logical volumes to BOM entries (by name/part-number heuristics)") - else: - print("No --materials-csv provided: emitting Default medium for all logical volumes") - - # --- facet files --- - facet_files = {} # def_lid -> absolute path string - for lid, tris in logical_volumes.items(): - disp = def_names.get(lid, "") - volname = sanitize_filename(disp) if disp else "vol" - lidname = sanitize_filename(lid) - fname = f"facets_{volname}_{lidname}.bin" - fpath = (out_folder / fname).resolve() - write_facets_bin(fpath, tris) - facet_files[lid] = str(fpath).replace("\\", "\\\\") # C++ string literal safety - - # --- which materials do we need to emit? --- - - # --- materials: collect unique BOM material strings actually used by leaf volumes --- - # We resolve each unique BOM string to a Geant4 NIST material using string + density scoring. - used_materials: Dict[str, ResolvedMaterial] = {} - - # Precompute one representative part density per BOM material (first good value wins) - mat_to_rho: Dict[str, Optional[float]] = {} - mat_to_rho_note: Dict[str, str] = {} - - for lid in logical_volumes.keys(): - if lid not in lid_to_bom: - continue - bom = lid_to_bom[lid] - mat_name = normalize_material_name(bom.material) - - if mat_name not in mat_to_rho: - rho_part, rho_note = _compute_density_g_cm3( - def_volumes_cm3.get(lid, 0.0), - bom.mass_value, - bom_mass_unit, - ) - mat_to_rho[mat_name] = rho_part - mat_to_rho_note[mat_name] = rho_note - - for mat_name in sorted(mat_to_rho.keys(), key=lambda s: s.lower()): - rho_part = mat_to_rho.get(mat_name) - rm = resolve_bom_material(mat_name, rho_part, g4db, mat_cfg) - - # Fold density provenance into the note for geom.C comments - rm.note = f"{rm.note} (density: {mat_to_rho_note.get(mat_name, 'n/a')})" - - if rm.nist_name is None: - print(f"WARNING: Unresolved/ambiguous material '{mat_name}'. See FIXME in generated geom.C.") - - used_materials[mat_name] = rm - - materials_cpp, medium_var_map = emit_materials_cpp(used_materials) - - # --- emit C++ macro --- - cpp: List[str] = [] - cpp.append(emit_cpp_prelude()) - - cpp.append("TGeoVolume* build(bool check=true) {") - cpp.append(' if (!gGeoManager) { throw std::runtime_error("gGeoManager is null. Call build_and_export() or create a TGeoManager first."); }') - cpp.append(materials_cpp) - - for lid in logical_volumes.keys(): - ntriangles = len(logical_volumes[lid]) - - # choose medium for this volume - med = "med_Default" - if lid in lid_to_bom: - mat_name = normalize_material_name(lid_to_bom[lid].material) - med = medium_var_map.get(mat_name, "med_Default") - - cpp.append(emit_tessellated_cpp(lid, def_names.get(lid, ""), facet_files[lid], ntriangles, med)) - - for lid in sorted(assemblies): - cpp.append(emit_assembly_cpp(lid, def_names.get(lid, ""))) - - for idx, (parent, child, trsf) in enumerate(placements, start=1): - cpp.append(emit_placement_cpp(parent, child, trsf, idx, scale_to_cm)) - - if len(top_defs) == 1: - top = next(iter(top_defs)) - cpp.append(f" return {cpp_var_for_def(top)};") - else: - cpp.append(' TGeoVolumeAssembly *asm_WORLD = new TGeoVolumeAssembly("WORLD");') - for i, node in enumerate(sorted(top_defs), start=1): - cpp.append(f" asm_WORLD->AddNode({cpp_var_for_def(node)}, {i});") - cpp.append(" return asm_WORLD;") - - cpp.append("}") - - # exports a function allowing to export the geometry to TGeo file - cpp.append('void build_and_export(const char* out_root = "geom.root", bool check=true) {') - cpp.append(' if (!gGeoManager) { new TGeoManager("geom","geom"); }') - cpp.append(' TGeoVolume* top = build(check);') - cpp.append(' gGeoManager->SetTopVolume(top);') - cpp.append(' gGeoManager->CloseGeometry();') - cpp.append(' gGeoManager->CheckOverlaps();') - cpp.append(' gGeoManager->Export(out_root);') - cpp.append('}') - - # exports a function to get get hold of the builder function in ALICE O2 - cpp.append('std::function get_builder_hook_checked() {') - cpp.append(' return []() { return build(true); };') - cpp.append('}') - # exports a function to get get hold of the builder function in ALICE O2 - cpp.append('std::function get_builder_hook_unchecked() {') - cpp.append(' return []() { return build(false); };') - cpp.append('}') - - return "\n".join(cpp) - - -# ------------------------------- -# Geometry Tree printing (debug) -# ------------------------------- - -def label_entry(label): - s = TCollection_AsciiString() - TDF_Tool.Entry(label, s) - return s.ToCString() - - -def traverse_print(label, shape_tool, depth=0): - indent = " " * depth - name = label.GetLabelName() - entry = label_entry(label) - print(f"{indent}- {name} =>[{entry}]") - - if shape_tool.IsReference(label): - ref_label = TDF_Label() - shape_tool.GetReferredShape(label, ref_label) - traverse_print(ref_label, shape_tool, depth + 1) - return - - children = TDF_LabelSequence() - shape_tool.GetComponents(label, children) - if children.Length() > 0 or shape_tool.IsAssembly(label): - for i in range(children.Length()): - traverse_print(children.Value(i + 1), shape_tool, depth + 1) - return - - if shape_tool.IsSimpleShape(label): - shape = shape_tool.GetShape(label) - print(f"{indent} [LogicalShape id={id(shape)}]") - - -def print_geom(step_file): - print(f"Printing GEOM hierarchy for {step_file}") - doc, shape_tool = load_step_with_xcaf(step_file) - roots = TDF_LabelSequence() - shape_tool.GetFreeShapes(roots) - for i in range(roots.Length()): - traverse_print(roots.Value(i + 1), shape_tool) - - -# ------------------------------- -# CLI -# ------------------------------- - -def main(): - ap = argparse.ArgumentParser(description="Convert STEP/XCAF to ROOT TGeo macro, facets in per-volume binary files.") - ap.add_argument("step", help="Input STEP file") - ap.add_argument("-o", "--out", default="geom.C", help="Output ROOT macro file name (default: geom.C)") - ap.add_argument("--output-folder", default="./", help="Output folder for macro + facet files") - ap.add_argument("--out-path", default=None, help="(deprecated) Alias for --output-folder") - ap.add_argument("--mesh", action="store_true", help="Use full BRepMesh triangulation instead of bounding boxes") - ap.add_argument("--print-tree", action="store_true", help="Just prints the geometry tree") - ap.add_argument("--mesh-prec", default=0.1, help="meshing precision. lower --> slower") - ap.add_argument("--step-unit", default="auto", choices=["auto", "mm", "cm", "m", "in", "ft"], help="STEP length unit override (default: auto-detect); TGeo expects cm") - ap.add_argument("--clip-box", nargs=6, type=float, metavar=("XMIN", "YMIN", "ZMIN", "XMAX", "YMAX", "ZMAX"), default=None, help="Clip CAD geometry to this axis-aligned bounding box before meshing (coordinates in STEP file units, before conversion to cm)") - ap.add_argument("--clip-deduplicate", default="intact", choices=["none", "intact"], help="When clipping, reuse original logical definitions for subtrees fully inside the clip box (default: intact); use 'none' for one volume per surviving occurrence") - ap.add_argument("--include-name", action="append", default=[], help="Only convert CAD labels whose XCAF name or label entry matches this regex; may be repeated. Matching an assembly includes its subtree.") - ap.add_argument("--exclude-name", action="append", default=[], help="Skip CAD labels/subtrees whose XCAF name or label entry matches this regex; may be repeated.") - ap.add_argument("--name-filter-case-sensitive", action="store_true", help="Make --include-name/--exclude-name matching case-sensitive (default: case-insensitive)") - - # NEW: BOM / material support - ap.add_argument("--materials-csv", default=None, help="BOM CSV file providing material + mass per part (optional)") - ap.add_argument("--bom-mass-unit", default="kg", choices=["kg", "g"], help="Unit of the BOM mass column (default: kg)") - ap.add_argument("--g4-nist-json", default=None, help="Path to Geant4 NIST DB JSON dump (from nist_export_all). Enables TGeoMixture emission + RadLen/IntLen.") - - - # Material matching scoring knobs (only used if --g4-nist-json is provided) - ap.add_argument("--mat-min-score", type=float, default=0.35, help="Minimum combined score to accept a G4 NIST material match (default: 0.35)") - ap.add_argument("--mat-ambiguity-delta", type=float, default=0.05, help="If best-second < delta, treat match as ambiguous/unresolved (default: 0.05)") - ap.add_argument("--mat-w-token", type=float, default=0.75, help="Weight for token/name similarity score (default: 0.75)") - ap.add_argument("--mat-w-density", type=float, default=0.25, help="Weight for density proximity score (default: 0.25)") - ap.add_argument("--mat-max-log-density-diff", type=float, default=0.0, help="Optional hard density filter in log-space (0 disables). Example 0.8 ~ within 2.2x (default: 0.0)") - ap.add_argument("--mat-compound-penalty", type=float, default=0.25, help="Penalty for matching to oxides/carbides/etc. when BOM doesn't mention them (default: 0.25)") - - args = ap.parse_args() - - step_path = str(_Path(args.step).expanduser().resolve()) - if args.print_tree: - print_geom(step_path) - return - - out_folder = _Path(args.output_folder) - if args.out_path is not None: - out_folder = _Path(args.out_path) - - clip_box = None - if args.clip_box is not None: - try: - clip_box = ClipBox.from_values(args.clip_box) - except ValueError as exc: - ap.error(str(exc)) - - name_filter = None - if args.include_name or args.exclude_name: - try: - name_filter = NameFilter.from_patterns( - args.include_name, - args.exclude_name, - case_sensitive=args.name_filter_case_sensitive, - ) - except re.error as exc: - ap.error(f"Invalid CAD name filter regex: {exc}") - - meshparam = {"do_meshing": args.mesh, "lin_defl": args.mesh_prec, "ang_defl": args.mesh_prec} - - - mat_cfg = MatMatchConfig( - min_score=args.mat_min_score, - ambiguity_delta=args.mat_ambiguity_delta, - w_token=args.mat_w_token, - w_density=args.mat_w_density, - max_log_density_diff=args.mat_max_log_density_diff, - compound_penalty=args.mat_compound_penalty, - ) - - out_folder = out_folder.expanduser().resolve() - out_folder.mkdir(parents=True, exist_ok=True) - - out_macro = (out_folder / _Path(args.out).name).resolve() - code = emit_root_macro( - step_path, - out_folder, - meshparam=meshparam, - step_unit=args.step_unit, - clip_box=clip_box, - clip_deduplicate=args.clip_deduplicate, - name_filter=name_filter, - materials_csv=args.materials_csv, - bom_mass_unit=args.bom_mass_unit, - g4_nist_json=args.g4_nist_json, - mat_cfg=mat_cfg, - ) - out_macro.write_text(code) - - print(f"Wrote ROOT macro: {out_macro}") - print(f"Wrote facet files into: {out_folder}") - print("In ROOT you can do:") - print(f" root -l {out_macro}") - print(' build_and_export("geom.root");') - - -if __name__ == "__main__": - main() diff --git a/scripts/geometry/README.md b/scripts/geometry/README.md deleted file mode 100644 index a59da13c12716..0000000000000 --- a/scripts/geometry/README.md +++ /dev/null @@ -1,283 +0,0 @@ -# CAD-to-TGeo geometry import - -`O2_CADtoTGeo.py` converts CAD geometries exported as STEP files into ROOT TGeo geometry. -The converter emits a small ROOT macro plus compact binary facet payloads. The generated -macro can be loaded directly in ROOT, or injected into `o2-sim` as an external passive -module or as a sensitive external detector. - -The current integration path is data-driven: the CAD geometry is converted once, then a -JSON file tells `o2-sim` which generated macro to load, where to anchor it in the existing -geometry, and, for sensitive detectors, which volumes or media should produce hits. - -## Software setup - -The preferred setup is the normal ALICE software environment. The `pythonOCC` package pulls -in OpenCascade and the Python bindings needed by the converter: - -```bash -alienv enter O2sim/latest,pythonOCC/latest -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py --help -``` - -If you are working from a local O2 checkout, `PATH_TO_ALICEO2_SOURCES` is the directory that -contains this `scripts/geometry` folder. - -For standalone studies outside the ALICE software stack, a conda environment with -`pythonocc-core` can also be used: - -```bash -conda create -n occ python=3.10 -y -conda activate occ -conda install -c conda-forge pythonocc-core -y - -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py --help -``` - -## Convert a STEP file - -For a quick, robust geometry preview, convert leaves to bounding boxes: - -```bash -mkdir -p cad_out/mydet -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py \ - my_detector.step \ - --output-folder cad_out/mydet \ - -o geom.C \ - --step-unit auto -``` - -For a more detailed faceted representation, enable meshing: - -```bash -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py \ - my_detector.step \ - --output-folder cad_out/mydet \ - -o geom.C \ - --mesh \ - --mesh-prec 0.05 \ - --step-unit auto -``` - -The output folder contains: - -- `geom.C`, a ROOT macro exporting `get_builder_hook_unchecked()` -- `facets_*.bin`, one compact triangle payload per leaf logical volume - -The generated macro is the file referenced from the external-geometry JSON examples below. -The macro and its facet binaries should stay together, because the macro loads the facet files -relative to its own location. - -## Restrict the converted region with a clip box - -Large CAD assemblies often contain far more than the region of interest. The `--clip-box` -option restricts the conversion to an axis-aligned bounding box, so only the geometry inside -(or overlapping) that box is written out: - -```bash -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py \ - my_detector.step \ - --output-folder cad_out/mydet \ - -o geom.C \ - --mesh \ - --clip-box XMIN YMIN ZMIN XMAX YMAX ZMAX -``` - -Notes on the coordinates: - -- The six values are `xmin ymin zmin xmax ymax zmax` and must satisfy `xmin < xmax`, - `ymin < ymax`, and `zmin < zmax`. -- Coordinates are given in the **STEP file units** (before conversion to cm), and are applied - in the global/world coordinate system of the assembly. - -Each solid is classified against the box before meshing: - -- Solids fully outside the box are dropped. -- Solids fully inside the box are kept unchanged. -- Solids straddling the box boundary are cut against it (a boolean intersection), so only the - part inside the box is meshed. - -Assemblies that end up with no surviving children are removed from the output tree. - -### Deduplication mode - -The `--clip-deduplicate` option controls how subtrees that fall entirely inside the box are -emitted: - -- `intact` (default): subtrees fully inside the box reuse their original shared logical - definitions, keeping the output compact. -- `none`: every surviving occurrence becomes its own volume, which is useful when you need a - flat, per-instance representation. - -```bash -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py \ - my_detector.step \ - --output-folder cad_out/mydet \ - -o geom.C \ - --mesh \ - --clip-box -50 -50 -20 50 50 20 \ - --clip-deduplicate none -``` - -Clipping can be combined with the name-based selection options (`--include-name` / -`--exclude-name`) to further narrow down which parts are converted. - -## Optional material mapping - -The converter can use a BOM CSV to assign materials and, when part masses and CAD volumes are -available, derive effective densities. A Geant4 NIST material JSON dump enables richer material -and tracking-length information: - -```bash -python PATH_TO_ALICEO2_SOURCES/scripts/geometry/O2_CADtoTGeo.py \ - my_detector.step \ - --output-folder cad_out/mydet \ - -o geom.C \ - --mesh \ - --materials-csv detector_bom.csv \ - --bom-mass-unit kg \ - --g4-nist-json g4_nist_materials.json -``` - -The expected BOM rows are mechanical part rows of the form: - -```text -CAD,Mechanical/Part,,,,,,... -``` - -Material names are matched to the NIST database when possible. If a match is ambiguous or not -available, the generated macro falls back to a simple material and leaves comments in `geom.C` -for follow-up. - -## Inject passive CAD geometry into `o2-sim` - -Passive external modules are configured under an `externalModules` array. Each entry needs a -module name, the generated macro, and an anchor volume already present in the O2 geometry. An -optional placement can translate and rotate the imported geometry inside the anchor volume: - -```json -{ - "externalModules": [ - { - "name": "IRIS", - "title": "IRIS support from CAD", - "macro": "cad_out/iris/geom.C", - "anchor": "barrel", - "placement": { - "translation": [0.0, 0.0, 0.0], - "rotation_deg": [0.0, 0.0, 15.0] - } - } - ] -} -``` - -The module is only added when its `name` is present in the active detector/module list. One -way to make such a list is a detector-list JSON file: - -```json -{ - "EXTCAD": ["IRIS"] -} -``` - -Run `o2-sim` with both files: - -```bash -o2-sim -n 1 -g boxgen \ - --detectorList EXTCAD:detectorlist.json \ - --extGeomFile externalGeometry.json -``` - -Multiple passive modules can be listed in the same file. The CAD macro loader JIT-compiles each -macro into a unique namespace, so several `O2_CADtoTGeo.py` outputs can coexist even though they -export the same builder-hook symbol names. - -## Inject sensitive external CAD detectors - -Sensitive external detectors are configured under an `externalDetectors` array. They use the -same generated geometry macro, but additionally select sensitive volumes or media and bind the -detector to a free O2 `DetID` slot. All such detectors are instances of -`o2::ext::ExternalDetector` and write the generic `o2::ext::Hit` format. - -```json -{ - "externalDetectors": [ - { - "name": "ECYL", - "title": "External silicon cylinder", - "macro": "cad_out/ecyl/geom.C", - "anchor": "barrel", - "detID": "ITS", - "sensitiveVolumes": ["ECYL_SENSOR"], - "placement": { "translation": [0.0, 0.0, 0.0] } - }, - { - "name": "EDISK", - "title": "External endcap disk with custom action", - "macro": "cad_out/edisk/geom.C", - "anchor": "barrel", - "detID": "TST", - "sensitiveMedia": ["Silicon"], - "sensitiveMacro": "sensitive_action.macro", - "sensitiveFunction": "sensitiveAction()" - } - ] -} -``` - -Selection rules: - -- `sensitiveVolumes` matches substrings of TGeo volume names. -- `sensitiveMedia` matches substrings of TGeo medium names. -- At least one of the two arrays must be non-empty. - -The `detID` determines the hit-file identity, for example `o2sim_HitsITS.root` or -`o2sim_HitsTST.root`. Choose a DetID that is not already occupied by an active built-in detector. -The branch name keeps the external detector name, for example `ECYLHit` or `EDISKHit`. - -If no `sensitiveMacro` is provided, the built-in action records a charged-track entrance/exit hit. -With a custom action, the macro is JIT-compiled at runtime and must return an -`o2::ext::ExternalDetector::SensitiveFcn`. The action can query `TVirtualMC::GetMC()` and use -helpers such as `currentSensorID()`, `currentTrackID()`, and `addHit()`. - -Run the detectors just like passive modules, with their names in the detector-list JSON: - -```json -{ - "EXTCAD": ["ECYL", "EDISK"] -} -``` - -```bash -o2-sim -j 2 -n 5 -g boxgen \ - --detectorList EXTCAD:detectorlist.json \ - --extGeomFile externalGeometry.json \ - --configKeyValues 'BoxGun.number=50' -``` - -In parallel mode, the hit merger reads the same `--extGeomFile`, registers the configured active -external detectors, and persists their generic external hits like built-in detector hits. - -## Complete runnable example - -A self-contained example is available in: - -```bash -run/SimExamples/External_Sensitive_Detectors -``` - -It defines two artificial sensitive detectors entirely from data: - -- `ACYL`, a silicon barrel cylinder using the built-in entrance/exit action -- `BDISK`, a silicon endcap disk using a custom JITed sensitive action - -The example uses hand-written geometry macros that mimic `O2_CADtoTGeo.py` output, so it does not -require CAD input files. Run it from its directory: - -```bash -cd run/SimExamples/External_Sensitive_Detectors -./run.sh -``` - -The script transports a few box-generator events and prints the hit counts for the produced -external-detector branches. \ No newline at end of file diff --git a/scripts/geometry/TODO.md b/scripts/geometry/TODO.md deleted file mode 100644 index 8add645e06357..0000000000000 --- a/scripts/geometry/TODO.md +++ /dev/null @@ -1,4 +0,0 @@ -- implement a BVHSurface solid as an exact representation of a CAD solid -- complete geometry configurable as JSON --> even the world volume ? - - \ No newline at end of file diff --git a/scripts/geometry/g4_nist_database/compile.sh b/scripts/geometry/g4_nist_database/compile.sh deleted file mode 100755 index 27d9cb0d87450..0000000000000 --- a/scripts/geometry/g4_nist_database/compile.sh +++ /dev/null @@ -1,11 +0,0 @@ -echo "Compiling using geant4-config..." - -g++ -std=c++20 nist_export_all.cxx \ - $(geant4-config --cflags) \ - $(geant4-config --libs) \ - -O2 -o nist_export_all - -echo "" -echo "Build complete." -echo "Run with:" -echo " ./nist_export_all nist_db_all.json" \ No newline at end of file diff --git a/scripts/geometry/simulating_CAD_modules.md b/scripts/geometry/simulating_CAD_modules.md deleted file mode 100644 index fe30456332ff6..0000000000000 --- a/scripts/geometry/simulating_CAD_modules.md +++ /dev/null @@ -1,80 +0,0 @@ -# ALICE-O2 GEANT Simulation of CAD Geometries - -These are a few notes related to the inclusion of external (CAD-described) detector modules into the O2 simulation framework. - -## Description of the Workflow - -In principle, such integration is now possible and requires the following steps: - -1. The CAD geometry needs to be exported to STEP format and must contain only the final geometry (no artificial eta-cut elements). Ideally, the geometry should be fully hierarchical with proper solid reuse. The solids should retain their proper surface representation for detailed analysis. Materials can be treated by providing a CSV file that map STEP part names to a material name. The conversion code will do it's best to find a corresponding material definition from a G4 NIST database JSON file (which can be expanded by users with custom definitions). - - -2. A tool `O2-CADtoTGeo.py` is provided to convert the STEP geometry into TGeo format. The tool is part of AliceO2 and is based on Python bindings (OCC) for OpenCascade. The tool can be used as follows: - - ```bash - python O2-CADtoTGeo.py STEP_FILE --output-folder my_detector -o geom.C --mesh \ - --mesh-prec 0.2 - ``` - - This will create a ROOT macro file `geom.C` containing the geometry description in ROOT format, as well as several binary files describing the TGeo solids. The `geom.C` file can either be used directly in ROOT to inspect the geometry or be provided to ALICE-O2 for inclusion in the geometry. - - When materials are included the conversion process looks like this - ```bash - python O2-CADtoTGeo.py STEP_FILE --output-folder my_detector -o geom.C --mesh \ - --mesh-prec 0.2 \ - --materials-csv MATERIALS.csv \ --g4-nist-json ../g4_nist_database/G4_NIST_DB.json - ``` - -3. Inspection of the created geom.C file and possible manual editing/fixing of the code, in particular materials and medium objects. - -4. Once the conversion is complete, the module can be inserted into the O2 geometry via the `ExternalModule` class. To do so, follow this pattern in `build_geometry.C`: - - ```cpp - if (isActivated("EXT")) { - o2::passive::ExternalModuleOptions options; - options.root_macro_file = "PATH_TO_MY_DETECTOR/my_detector/geom_withMaterials.C"; - options.anchor_volume = "barrel"; // hook this into barrel - auto rot = new TGeoCombiTrans(); - rot->RotateX(90); - rot->SetDy(30); // compensate for a shift of the barrel with respect to zero - options.placement = rot; - run->AddModule(new o2::passive::ExternalModule("A3VTX", "ALICE3 beam pipe", options)); - } - ``` - -5. Create a custom detector geometry list file `my_det.json` in JSON format that includes the external detector (and any other required components, such as the L3 magnet in this example): - - ```json - { - "MY_DET": [ - "EXT", - "MAG" - ] - } - ``` - -6. Run the Geant simulation with: - - ```bash - o2-sim --detectorList MY_DET:my_det.json -g pythia8pp .... - ``` - -## Known Limitations - -- The `O2-CADtoTGeo.py` tool currently converts geometries only into TGeoTessellated solids. This may be suboptimal for primitive shapes or only an approximation for shapes with exact second-order surfaces (e.g., tubes). The precision (and therefore the number of surface triangles) can be controlled with the `--mesh-prec` parameter. The smaller the value, the more precise the mesh. - -- Meshed solids created by the tool may have issues, such as topological errors or non-watertight surfaces. It is planned to include "healing" steps via additional processing with well-known geometry kernels (e.g., CGAL). - -- The tool does not currently export materials or TGeoMedia. These must be inserted or edited manually. It is planned to make this process more automatic and user-friendly. - -- The Python tool requires the OCC Python module, which is currently not part of our software distribution. We have found it most practical to run the tool in a separate conda environment (fully decoupled from the ALICE software stack). - -- The tool currently generates a `geom.C` macro file. In the future, it may be possible to directly create an in-memory TGeo representation for deeper integration. - -- Currently, only passive modules can be integrated. Treatment of sensitive volumes or parts will be addressed in a future step. - -## Software Installation - -- The simulation must be run in the standard O2 environment built with alibuild. - -- The CAD conversion tool must currently be run in a dedicated conda environment, as described in scripts/geometry/README.md in the AliceO2 source code. \ No newline at end of file