Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-05-10 08:39:32

0001 // SPDX-License-Identifier: LGPL-2.1-or-later
0002 // Copyright (C) 2026 G4OCCT Contributors
0003 
0004 /// @file G4OCCTSolid.hh
0005 /// @brief Declaration of G4OCCTSolid.
0006 
0007 #ifndef G4OCCT_G4OCCTSolid_hh
0008 #define G4OCCT_G4OCCTSolid_hh
0009 
0010 #include <G4Cache.hh>
0011 #include <G4Polyhedron.hh>
0012 #include <G4ThreeVector.hh>
0013 #include <G4VSolid.hh>
0014 
0015 // OCCT shape representation
0016 #include <BRepAdaptor_Surface.hxx>
0017 #include <BRepClass3d_SolidClassifier.hxx>
0018 #include <BRepExtrema_TriangleSet.hxx>
0019 #include <Bnd_Box.hxx>
0020 #include <IntCurvesFace_Intersector.hxx>
0021 #include <TopoDS_Face.hxx>
0022 #include <TopoDS_Shape.hxx>
0023 #include <TopoDS_TShape.hxx>
0024 #include <gp_Pln.hxx>
0025 #include <gp_Pnt2d.hxx>
0026 
0027 #include <atomic>
0028 #include <condition_variable>
0029 #include <cstdint>
0030 #include <limits>
0031 #include <memory>
0032 #include <mutex>
0033 #include <optional>
0034 #include <stdexcept>
0035 #include <string>
0036 #include <unordered_map>
0037 #include <utility>
0038 #include <vector>
0039 
0040 /**
0041  * @brief Geant4 solid wrapping an Open CASCADE Technology (OCCT) TopoDS_Shape.
0042  *
0043  * Wraps an Open CASCADE Technology (OCCT) TopoDS_Shape as a Geant4 solid
0044  * (G4VSolid). The OCCT shape is stored by value and is queried directly for
0045  * Geant4 navigation, extent, and visualisation requests.
0046  *
0047  * In OCCT the closest analogue to G4VSolid is TopoDS_Shape, which is the root
0048  * of the Boundary-Representation topology hierarchy and can describe any shape
0049  * from a simple box to a complex multi-face shell. The mapping is discussed in
0050  * detail in docs/geometry_mapping.md.
0051  *
0052  * ## Thread safety and caching
0053  *
0054  * `G4OCCTSolid` is shared read-only across all Geant4 worker threads once
0055  * the geometry is constructed.  OCCT algorithm objects such as
0056  * `BRepClass3d_SolidClassifier` and `IntCurvesFace_Intersector` hold
0057  * mutable internal state and must not be shared between threads.  Per-thread
0058  * caches of both algorithm objects are maintained via `G4Cache` so that the
0059  * one-time O(N_faces) initialisation cost is paid only once per thread rather
0060  * than on every navigation call.
0061  *
0062  * A monotonically increasing `fShapeGeneration` counter is incremented by
0063  * `SetOCCTShape()`.  Each per-thread cache entry records the generation at
0064  * which it was built; `GetOrCreateClassifier()` and `GetOrCreateIntersector()`
0065  * rebuild the entry whenever the stored generation is stale.  This ensures that
0066  * all worker threads automatically pick up a new shape on their next navigation
0067  * call, even if `SetOCCTShape()` is invoked after some threads have already
0068  * initialised their caches.
0069  */
0070 class G4OCCTSolid : public G4VSolid {
0071 public:
0072   /**
0073    * Construct with a Geant4 solid name and an OCCT shape.
0074    *
0075    * @param name  Name registered with the Geant4 solid store.
0076    * @param shape OCCT boundary-representation shape to wrap.
0077    * @throws std::invalid_argument if @p shape is null or has no computable bounding box.
0078    */
0079   G4OCCTSolid(const G4String& name, const TopoDS_Shape& shape);
0080 
0081   ~G4OCCTSolid() override;
0082 
0083   /**
0084    * Load a STEP file and construct a G4OCCTSolid from the first shape found.
0085    *
0086    * Convenience factory that combines STEP file reading with solid construction.
0087    * Equivalent to constructing a `G4OCCTSolid` from the shape returned by a
0088    * `STEPControl_Reader`.
0089    *
0090    * @param name Name registered with the Geant4 solid store.
0091    * @param path Filesystem path to the STEP file.
0092    * @return Pointer to a newly heap-allocated G4OCCTSolid (owned by the caller).
0093    * @throws std::runtime_error if the file cannot be read or yields a null shape.
0094    */
0095   static G4OCCTSolid* FromSTEP(const G4String& name, const std::string& path);
0096 
0097   // ── G4VSolid pure-virtual interface ───────────────────────────────────────
0098 
0099   /// Return kInside, kSurface, or kOutside for point @p p.
0100   EInside Inside(const G4ThreeVector& p) const override;
0101 
0102   /// Return the outward unit normal at surface point @p p.
0103   G4ThreeVector SurfaceNormal(const G4ThreeVector& p) const override;
0104 
0105   /// Distance from external point @p p along direction @p v to solid surface.
0106   /// @param p  Point outside (or on) the solid surface.
0107   /// @param v  Non-zero unit direction vector (precondition required by the G4VSolid contract).
0108   G4double DistanceToIn(const G4ThreeVector& p, const G4ThreeVector& v) const override;
0109 
0110   /// A lower bound on the shortest distance from external point @p p to the solid surface.
0111   ///
0112   /// Uses a two-stage acceleration pipeline:
0113   ///
0114   /// **Tier 0 (O(1)):** If @p p is outside the cached axis-aligned bounding box by more than
0115   /// `IntersectionTolerance()`, the AABB distance is returned immediately.  This is always a
0116   /// valid conservative lower bound and avoids any OCCT call for distant exterior points.
0117   ///
0118   /// **Fallback:** Points within `IntersectionTolerance()` of the AABB fall through to
0119   /// `ExactDistanceToIn(p)`.
0120   ///
0121   /// For the exact shortest distance, use ExactDistanceToIn(p).
0122   G4double DistanceToIn(const G4ThreeVector& p) const override;
0123 
0124   /// Distance from internal point @p p along direction @p v to solid surface.
0125   /// @param p  Point inside (or on) the solid surface.
0126   /// @param v  Non-zero unit direction vector (precondition required by the G4VSolid contract).
0127   G4double DistanceToOut(const G4ThreeVector& p, const G4ThreeVector& v,
0128                          const G4bool calcNorm = false, G4bool* validNorm = nullptr,
0129                          G4ThreeVector* n = nullptr) const override;
0130 
0131   /// A lower bound on the shortest distance from internal point @p p to the solid surface.
0132   /// For the exact shortest distance, use ExactDistanceToOut(p).
0133   G4double DistanceToOut(const G4ThreeVector& p) const override;
0134 
0135   /// Return a point sampled uniformly at random from the solid's surface.
0136   ///
0137   /// The OCCT shape is tessellated with a relative chord deflection of 1 % and
0138   /// each mesh triangle is selected with probability proportional to its
0139   /// (planar) area.  A random point within the selected triangle is then
0140   /// generated using standard barycentric-coordinate sampling and projected
0141   /// back to the nearest point on the triangle's originating analytical OCCT
0142   /// face surface via `GeomAPI_ProjectPointOnSurf`.  This projection step
0143   /// places the returned point on the analytical surface for curved faces
0144   /// (spheres, cylinders, cones, tori, etc.), rather than inside the solid at
0145   /// a chord midpoint of the tessellation mesh.  If projection fails (e.g. for
0146   /// a degenerate or null surface), the raw tessellation point is returned as a
0147   /// fallback; such a point may lie slightly inside the solid.
0148   ///
0149   /// The distribution is area-weighted over the triangulated surface, which
0150   /// approximates the uniform distribution over the true analytic surface; the
0151   /// bias is proportional to the curvature and the 1 % chord-height error.
0152   /// The tessellation is cached per shape generation so repeated calls only
0153   /// require an O(log N_triangles) area selection plus one surface projection.
0154   ///
0155   /// Emits a @c JustWarning G4Exception and returns the origin if the
0156   /// tessellation produces no valid triangles.
0157   G4ThreeVector GetPointOnSurface() const override;
0158 
0159   // ── G4OCCTSolid distance functions ────────────────────────────────────────
0160 
0161   /// Exact shortest distance from external point @p p to the solid surface.
0162   /// Returns 0 if @p p is on or inside the surface, or kInfinity if the
0163   /// calculation fails.
0164   G4double ExactDistanceToIn(const G4ThreeVector& p) const;
0165 
0166   /// Exact shortest distance from internal point @p p to the solid surface.
0167   /// Returns 0 if @p p is within IntersectionTolerance() of the surface, or if
0168   /// the calculation fails. For points outside the solid,
0169   /// returns the positive distance to the nearest surface.
0170   G4double ExactDistanceToOut(const G4ThreeVector& p) const;
0171 
0172   /// Compute and return the cubic volume of the solid.
0173   /// The result is cached; repeated calls return the cached value.
0174   G4double GetCubicVolume() override;
0175 
0176   /// Compute and return the surface area of the solid.
0177   /// The result is cached; repeated calls return the cached value.
0178   G4double GetSurfaceArea() override;
0179 
0180   /// Return a string identifying the entity type.
0181   G4GeometryType GetEntityType() const override;
0182 
0183   /// Return the axis-aligned bounding box extent.
0184   G4VisExtent GetExtent() const override;
0185 
0186   /// Return the axis-aligned bounding box limits.
0187   void BoundingLimits(G4ThreeVector& pMin, G4ThreeVector& pMax) const override;
0188 
0189   /// Calculate the extent of the solid in the given axis.
0190   G4bool CalculateExtent(const EAxis pAxis, const G4VoxelLimits& pVoxelLimit,
0191                          const G4AffineTransform& pTransform, G4double& pMin,
0192                          G4double& pMax) const override;
0193 
0194   /// Describe the solid to the graphics scene.
0195   void DescribeYourselfTo(G4VGraphicsScene& scene) const override;
0196 
0197   /// Create a polyhedron representation for visualisation.
0198   G4Polyhedron* CreatePolyhedron() const override;
0199 
0200   /// Stream a human-readable description.
0201   std::ostream& StreamInfo(std::ostream& os) const override;
0202 
0203   // ── G4OCCTSolid-specific interface ────────────────────────────────────────
0204 
0205   /// Read access to the underlying OCCT shape.
0206   const TopoDS_Shape& GetOCCTShape() const { return fShape; }
0207 
0208   /// Replace the underlying OCCT shape.
0209   /// @note Increments an internal generation counter so that every worker
0210   ///       thread automatically reloads its per-thread classifier and
0211   ///       intersector on its next navigation call.  The shape update itself
0212   ///       is not atomic with respect to ongoing navigation; avoid calling
0213   ///       this while a simulation run is in progress.
0214   /// @throws std::invalid_argument if @p shape is null or has no computable bounding box.
0215   void SetOCCTShape(const TopoDS_Shape& shape) {
0216     if (shape.IsNull()) {
0217       throw std::invalid_argument("G4OCCTSolid::SetOCCTShape: shape must not be null");
0218     }
0219     fShape = shape;
0220     ComputeBounds();
0221     {
0222       std::unique_lock<std::mutex> lock(fPolyhedronMutex);
0223       fCachedPolyhedron.reset();
0224     }
0225     {
0226       std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
0227       fCachedVolume.reset();
0228       fCachedSurfaceArea.reset();
0229     }
0230     {
0231       std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
0232       fSurfaceCache.reset();
0233     }
0234     fShapeGeneration.fetch_add(1, std::memory_order_release);
0235   }
0236 
0237 private:
0238   /// Per-face bounding box entry used to prefilter the closest-face search in
0239   /// `SurfaceNormal`.
0240   struct FaceBounds {
0241     TopoDS_Face face;
0242     Bnd_Box box;
0243     BRepAdaptor_Surface adaptor;
0244     std::optional<gp_Pln> plane; ///< precomputed plane equation for planar faces
0245     /// Outer wire boundary vertices in the plane's (u,v) parameter space.
0246     /// Non-empty only when the face is planar and every edge of the outer wire
0247     /// is a straight line segment, which allows exact point-in-polygon testing
0248     /// without invoking the full OCCT topology machinery at navigation time.
0249     std::vector<gp_Pnt2d> uvPolygon;
0250     /// Precomputed constant outward normal for planar faces.  Non-empty whenever
0251     /// @c uvPolygon is non-empty.  Used by DistanceToOut(p,v) to skip the
0252     /// BRepLProp_SLProps evaluation on the hot normal-computation path.
0253     std::optional<G4ThreeVector> outwardNormal;
0254   };
0255 
0256   /// Result of the closest-face search.
0257   struct ClosestFaceMatch {
0258     TopoDS_Face face;
0259     G4double distance{kInfinity};
0260     /// Index into the face-bounds cache vector (e.g. the @c faceBoundsCache
0261     /// argument passed to the search) used to retrieve the cached
0262     /// @c FaceBounds::adaptor without reconstructing it on the fly.
0263     std::size_t faceIndex{0};
0264     /// UV parameters at the closest point on the face, available when
0265     /// BRepExtrema_DistShapeShape found an interior solution
0266     /// (BRepExtrema_IsInFace).  Not set for edge or vertex solutions.
0267     std::optional<std::pair<Standard_Real, Standard_Real>> uv;
0268   };
0269 
0270   /// Axis-aligned bounding box: minimum and maximum corners.
0271   struct AxisAlignedBounds {
0272     G4ThreeVector min;
0273     G4ThreeVector max;
0274   };
0275 
0276   /// Per-thread classifier cache entry: generation stamp + lazily-built classifier.
0277   ///
0278   /// The `generation` field is compared against `fShapeGeneration` on every
0279   /// call to `GetOrCreateClassifier()`; a mismatch triggers a reload.
0280   /// Initialised to the maximum uint64 value, which can never match generation 0,
0281   /// so the first call from each thread always builds the classifier.
0282   struct ClassifierCache {
0283     std::uint64_t generation{std::numeric_limits<std::uint64_t>::max()};
0284     std::optional<BRepClass3d_SolidClassifier> classifier;
0285   };
0286 
0287   /// Per-thread intersector cache entry: generation stamp + per-face intersectors.
0288   ///
0289   /// The `generation` field is compared against `fShapeGeneration` on every
0290   /// call to `GetOrCreateIntersector()`; a mismatch triggers a reload.
0291   /// Initialised to the maximum uint64 value, which can never match generation 0,
0292   /// so the first call from each thread always builds the intersector.
0293   ///
0294   /// `faceIntersectors` holds one `IntCurvesFace_Intersector` per entry in
0295   /// `fFaceBoundsCache`, in the same order.  Each entry is heap-allocated
0296   /// (via `std::unique_ptr`) to avoid requiring movability of
0297   /// `IntCurvesFace_Intersector` during vector reallocation.  The per-face
0298   /// intersectors are used by `DistanceToIn(p,v)` and `DistanceToOut(p,v,...)`
0299   /// in a bbox-prefiltered loop that avoids `NCollection_Sequence` heap
0300   /// allocation.
0301   ///
0302   /// `expandedBoxes` holds a copy of each entry's `FaceBounds::box`, enlarged
0303   /// by `IntersectionTolerance()`.  Planar faces have zero-thickness bounding
0304   /// boxes in their normal direction; `Bnd_Box::IsOut(gp_Lin)` can return a
0305   /// floating-point false positive for such degenerate boxes when the line
0306   /// grazes the bounding plane.  Enlarging by the tolerance ensures every box
0307   /// has finite extent in all directions and makes the `IsOut` test robust.
0308   struct IntersectorCache {
0309     std::uint64_t generation{std::numeric_limits<std::uint64_t>::max()};
0310     std::vector<std::unique_ptr<IntCurvesFace_Intersector>> faceIntersectors;
0311     std::vector<Bnd_Box> expandedBoxes; ///< per-face boxes enlarged by `IntersectionTolerance()`
0312   };
0313 
0314   /// A proven inscribed sphere: every point within @c radius of @c centre is
0315   /// inside the solid.  The guarantee comes from @c DistanceToOut(centre) ≥ radius.
0316   struct InscribedSphere {
0317     G4ThreeVector centre;
0318     G4double radius;
0319   };
0320 
0321   /// Per-thread inscribed-sphere cache.
0322   ///
0323   /// Populated from `fInitialSpheres` on first access per thread and grown by
0324   /// `TryInsertSphere()` on each `DistanceToOut(p)` call.  Kept sorted in
0325   /// descending order of radius so `Inside()` checks the largest (most useful)
0326   /// spheres first and benefits from early exit.
0327   ///
0328   /// The `generation` field mirrors the classifier-cache pattern: a mismatch
0329   /// with `fShapeGeneration` causes the cache to be re-seeded from
0330   /// `fInitialSpheres`, discarding any runtime-accumulated spheres for the old
0331   /// shape.
0332   struct SphereCacheData {
0333     std::vector<InscribedSphere> spheres;
0334     std::uint64_t generation{std::numeric_limits<std::uint64_t>::max()};
0335   };
0336 
0337   /// Maximum number of inscribed spheres kept in the per-thread cache.
0338   static constexpr std::size_t kMaxInscribedSpheres = 64;
0339 
0340   /// Single mesh triangle used by the surface-sampling cache.
0341   ///
0342   /// Each triangle carries only the three vertex positions and a compact index
0343   /// into `SurfaceSamplingCache::faces`.  Using an index instead of a full
0344   /// `TopoDS_Face` value avoids duplicating the face handle across every
0345   /// triangle on that face, which can be thousands of entries for a
0346   /// finely-tessellated shape.
0347   struct SurfaceTriangle {
0348     G4ThreeVector p1, p2, p3;
0349     std::uint32_t
0350         faceIndex; ///< Index into SurfaceSamplingCache::faces (valid range: [0, faces.size())).
0351   };
0352 
0353   /// Cached data for area-weighted surface-point sampling via `GetPointOnSurface()`.
0354   ///
0355   /// Stores the flat list of triangles extracted from the OCCT triangulation
0356   /// together with a cumulative-area array that enables O(log N) area-weighted
0357   /// selection, and a deduplicated list of the OCCT faces from which the
0358   /// triangles were collected (used for surface projection in
0359   /// `GetPointOnSurface()`).  Built lazily on the first `GetPointOnSurface()`
0360   /// call after each shape update; keyed by `fShapeGeneration`.
0361   struct SurfaceSamplingCache {
0362     std::vector<TopoDS_Face> faces; ///< Unique OCCT faces, one entry per shape face.
0363     std::vector<SurfaceTriangle> triangles;
0364     std::vector<G4double> cumulativeAreas;
0365     G4double totalArea{0.0};
0366   };
0367 
0368   /// The underlying OCCT shape.  Guaranteed non-null: the constructor and
0369   /// SetOCCTShape() both throw std::invalid_argument when given a null shape.
0370   TopoDS_Shape fShape;
0371 
0372   /// Cached axis-aligned bounding box; computed eagerly in the constructor and
0373   /// recomputed by `ComputeBounds()` whenever `SetOCCTShape()` is called.
0374   /// Always valid after construction: the constructor and `SetOCCTShape()` throw
0375   /// `std::invalid_argument` if the shape has no computable bounding box.
0376   AxisAlignedBounds fCachedBounds;
0377 
0378   /// Per-face bounding boxes, populated alongside `fCachedBounds` in `ComputeBounds()`.
0379   /// Used by `SurfaceNormal` as a lower-bound prefilter to avoid calling
0380   /// `BRepExtrema_DistShapeShape` on faces that are provably farther than the
0381   /// current best candidate.  Written once at construction / `SetOCCTShape()`;
0382   /// read-only during navigation, so no additional synchronisation is required.
0383   std::vector<FaceBounds> fFaceBoundsCache;
0384 
0385   /// Index from a face's underlying `TShape` pointer to the list of
0386   /// `fFaceBoundsCache` entry indices that share that `TShape`.  Populated
0387   /// alongside `fFaceBoundsCache` in `ComputeBounds()` and used by
0388   /// `DistanceToOut(p,v,...)` to avoid an O(N) linear scan when looking up
0389   /// the cached `BRepAdaptor_Surface` for the hit face returned by the
0390   /// intersector.
0391   ///
0392   /// Hash lookup narrows candidates to the (almost always singleton) set of
0393   /// faces sharing the same `TShape`; `IsPartner()` (TShape + Location) is
0394   /// then used within that set to select the correct located entry, handling
0395   /// instanced sub-shapes where the same `TShape` appears at several locations.
0396   ///
0397   /// Written once at construction / `SetOCCTShape()`; read-only during
0398   /// navigation.
0399   std::unordered_map<const TopoDS_TShape*, std::vector<std::size_t>> fFaceAdaptorIndex;
0400 
0401   /// Inscribed spheres seeded at construction time by `ComputeInitialSpheres()`.
0402   ///
0403   /// Evaluated once per shape at up to 15 interior candidate points (AABB centre,
0404   /// 6 axis-offset interior points each placed at half the distance from the AABB
0405   /// centre to the nearest face along that axis, and 8 octant centres) using
0406   /// `BVHLowerBoundDistance`.
0407   /// Written only in `ComputeBounds()` → `ComputeInitialSpheres()`; thereafter
0408   /// read-only, so no additional synchronisation is needed.
0409   /// Copied into each thread's `SphereCacheData` on first cache access.
0410   std::vector<InscribedSphere> fInitialSpheres;
0411 
0412   /// BVH-accelerated triangle set over the tessellated surface of @c fShape.
0413   ///
0414   /// Built once in `ComputeBounds()` after `BRepMesh_IncrementalMesh` tessellates
0415   /// the shape.  Used by `BVHLowerBoundDistance()` to compute O(log T) lower bounds
0416   /// on the point-to-surface distance.  Null when the shape has no faces.
0417   /// Written only in `ComputeBounds()`; read-only (const BVH traversal) during
0418   /// navigation — no additional synchronisation is required beyond the construction
0419   /// ordering already guaranteed by the geometry-build phase.
0420   Handle(BRepExtrema_TriangleSet) fTriangleSet;
0421 
0422   /// Conservative upper bound on the Hausdorff distance between the analytical
0423   /// surface of @c fShape and its tessellation stored in @c fTriangleSet.
0424   ///
0425   /// Set in `ComputeBounds()` to `kRelativeDeflection × max_face_bbox_diagonal`,
0426   /// which bounds the chord-height error for a relative linear deflection of
0427   /// `kRelativeDeflection`.  Used in `BVHLowerBoundDistance()` as the correction
0428   /// term `δ` in `s = max(0, mesh_dist − δ)` to guarantee a valid lower bound.
0429   /// Also retained for contexts where a conservative global bound is needed (e.g.
0430   /// the `SurfaceNormal` seed distance calculation).
0431   G4double fBVHDeflection{0.0};
0432 
0433   /// Per-face Hausdorff deflection bound: `kRelativeDeflection × face_bbox_diagonal` for
0434   /// each face in @c fFaceBoundsCache (same ordering).  In `BVHLowerBoundDistance()`,
0435   /// `BRepExtrema_TriangleSet::GetFaceID()` maps the nearest triangle back to its face
0436   /// so the tightest per-face correction can be used instead of the global
0437   /// @c fBVHDeflection.  For queries near small faces this yields a tighter lower bound
0438   /// and avoids unnecessary fall-through to the expensive exact solver.
0439   ///
0440   /// Populated alongside @c fTriangleSet in `ComputeBounds()`.  Empty when
0441   /// @c fTriangleSet is null (no faces).
0442   std::vector<G4double> fFaceDeflections;
0443 
0444   /// True if every face in @c fFaceBoundsCache has a precomputed plane (i.e. all
0445   /// faces are planar).  Set by `ComputeBounds()`.  When true, `DistanceToOut(p)`
0446   /// can bypass the BVH triangle-mesh traversal entirely.
0447   bool fAllFacesPlanar{false};
0448 
0449   /// Monotonically increasing counter; incremented by each `SetOCCTShape()` call.
0450   /// Read (acquire) in `GetOrCreateClassifier()` and `GetOrCreateIntersector()` const;
0451   /// written (release) in `SetOCCTShape()`.
0452   std::atomic<std::uint64_t> fShapeGeneration{0};
0453 
0454   /// Per-thread cache of the BRepClass3d_SolidClassifier for this solid.
0455   ///
0456   /// Initialised lazily on the first `Inside` or `DistanceToIn(p)` call on
0457   /// each thread.  Stale entries (generation mismatch) are rebuilt automatically.
0458   mutable G4Cache<ClassifierCache> fClassifierCache;
0459 
0460   /// Per-thread cache of per-face `IntCurvesFace_Intersector` objects for this solid.
0461   ///
0462   /// Initialised lazily on the first `DistanceToIn(p, v)` or `DistanceToOut(p, v)`
0463   /// call on each thread.  Stale entries (generation mismatch) are rebuilt automatically.
0464   mutable G4Cache<IntersectorCache> fIntersectorCache;
0465 
0466   /// Per-thread inscribed-sphere cache, grown online from `DistanceToOut(p)` calls.
0467   ///
0468   /// Seeded from `fInitialSpheres` on first access per thread; grown by
0469   /// `TryInsertSphere()`.  Stale entries (generation mismatch) are re-seeded from
0470   /// `fInitialSpheres`, discarding runtime-accumulated spheres for the old shape.
0471   mutable G4Cache<SphereCacheData> fSphereCache;
0472 
0473   /// Cached polyhedron built by the first `CreatePolyhedron()` call after each
0474   /// shape update.  Subsequent calls return a copy of this object, avoiding a
0475   /// repeated (and expensive) `BRepMesh_IncrementalMesh` tessellation.
0476   ///
0477   /// Access to this field, `fPolyhedronGeneration`, and `fPolyhedronBuilding` is
0478   /// serialised by `fPolyhedronMutex`.  `nullptr` indicates that no valid cache
0479   /// entry exists for the current shape generation.
0480   mutable std::unique_ptr<G4Polyhedron> fCachedPolyhedron;
0481 
0482   /// Shape-generation stamp at which `fCachedPolyhedron` was last built.
0483   /// Initialised to `std::numeric_limits<std::uint64_t>::max()` so that it
0484   /// can never match the initial `fShapeGeneration` value of 0, forcing a
0485   /// build on the first call to `CreatePolyhedron()`.
0486   /// Protected by `fPolyhedronMutex`.
0487   mutable std::uint64_t fPolyhedronGeneration{std::numeric_limits<std::uint64_t>::max()};
0488 
0489   /// True while one thread is performing the expensive tessellation.
0490   /// Other threads that also miss the cache wait on `fPolyhedronCV` instead
0491   /// of running duplicate tessellations.
0492   /// Protected by `fPolyhedronMutex`.
0493   mutable bool fPolyhedronBuilding{false};
0494 
0495   /// Mutex serialising concurrent access to `fCachedPolyhedron`,
0496   /// `fPolyhedronGeneration`, and `fPolyhedronBuilding` in `CreatePolyhedron()`.
0497   mutable std::mutex fPolyhedronMutex;
0498 
0499   /// Notified when `fPolyhedronBuilding` transitions to false so that threads
0500   /// waiting for a concurrent build can re-evaluate the cache.
0501   mutable std::condition_variable fPolyhedronCV;
0502 
0503   /// Cached cubic volume in mm³; `std::nullopt` until first `GetCubicVolume()` call
0504   /// or after each `SetOCCTShape()` call.  Protected by `fVolumeAreaMutex`.
0505   mutable std::optional<G4double> fCachedVolume;
0506 
0507   /// Cached surface area in mm²; `std::nullopt` until first `GetSurfaceArea()` call
0508   /// or after each `SetOCCTShape()` call.  Protected by `fVolumeAreaMutex`.
0509   mutable std::optional<G4double> fCachedSurfaceArea;
0510 
0511   /// Mutex serialising concurrent access to `fCachedVolume` and `fCachedSurfaceArea`.
0512   mutable std::mutex fVolumeAreaMutex;
0513 
0514   /// Cached surface-sampling data built by the first `GetPointOnSurface()` call
0515   /// after each shape update.  `std::nullopt` indicates no valid entry for the
0516   /// current shape generation.  Protected by `fSurfaceCacheMutex`.
0517   mutable std::optional<SurfaceSamplingCache> fSurfaceCache;
0518 
0519   /// Shape-generation stamp at which `fSurfaceCache` was built.
0520   /// Initialised to `std::numeric_limits<std::uint64_t>::max()` so that it can
0521   /// never match the initial `fShapeGeneration` value of 0, forcing a build on
0522   /// the first `GetPointOnSurface()` call.  Protected by `fSurfaceCacheMutex`.
0523   mutable std::uint64_t fSurfaceCacheGeneration{std::numeric_limits<std::uint64_t>::max()};
0524 
0525   /// Mutex serialising concurrent access to `fSurfaceCache` and
0526   /// `fSurfaceCacheGeneration` in `GetOrBuildSurfaceCache()`.
0527   mutable std::mutex fSurfaceCacheMutex;
0528 
0529   /// Return a reference to the per-thread classifier, (re-)initialising it from
0530   /// @c fShape whenever the cached generation does not match @c fShapeGeneration.
0531   BRepClass3d_SolidClassifier& GetOrCreateClassifier() const;
0532 
0533   /// Return a reference to the per-thread intersector cache, (re-)initialising
0534   /// it from @c fShape whenever the cached generation does not match
0535   /// @c fShapeGeneration.  The cache holds one `IntCurvesFace_Intersector` per
0536   /// face in `fFaceBoundsCache`, in the same order.
0537   IntersectorCache& GetOrCreateIntersector() const;
0538 
0539   /// Return a reference to the per-thread inscribed-sphere cache, seeding it from
0540   /// @c fInitialSpheres if the cached generation does not match @c fShapeGeneration.
0541   SphereCacheData& GetOrInitSphereCache() const;
0542 
0543   /// Attempt to insert an inscribed sphere with centre @p centre and radius @p d into
0544   /// the per-thread sphere cache.  The sphere is accepted only if it is not already
0545   /// dominated by an existing cache entry and passes the minimum-radius filter.
0546   /// Maintains the cache sorted descending by radius and bounded to @c kMaxInscribedSpheres.
0547   void TryInsertSphere(const G4ThreeVector& centre, G4double d) const;
0548 
0549   /// Compute the axis-aligned bounding box of @c fShape and store it in
0550   /// @c fCachedBounds, and populate @c fFaceBoundsCache with per-face bounding
0551   /// boxes.  Throws `std::invalid_argument` when the shape has no computable
0552   /// bounding box (e.g. an empty compound).  Called from the constructor and
0553   /// @c SetOCCTShape().
0554   void ComputeBounds();
0555 
0556   /// Seed @c fInitialSpheres with inscribed spheres at up to 15 interior candidate
0557   /// points (AABB centre, 6 face midpoints, 8 octant centres).  Called at the end
0558   /// of @c ComputeBounds() after the BVH mesh is available.  Each candidate that
0559   /// classifies as @c TopAbs_IN and has a positive BVH lower-bound distance is
0560   /// stored as a seed sphere.  Thread-safe: written only during geometry construction,
0561   /// read-only during navigation.
0562   void ComputeInitialSpheres();
0563 
0564   /// Compute the distance from @p p to the cached axis-aligned bounding box.
0565   /// Returns 0 when @p p is on or inside the AABB.
0566   G4double AABBLowerBound(const G4ThreeVector& p) const;
0567 
0568   /// Compute a conservative lower bound on the distance from @p p to the solid surface
0569   /// using the BVH-accelerated triangle set @c fTriangleSet.
0570   ///
0571   /// The returned value satisfies @c 0 ≤ s ≤ true_distance, making it a valid
0572   /// Geant4 safety distance.  Returns @c kInfinity if @c fTriangleSet is not
0573   /// available (null or empty).
0574   G4double BVHLowerBoundDistance(const G4ThreeVector& p) const;
0575 
0576   /// Returns the minimum perpendicular distance from @p p to any planar face's
0577   /// infinite supporting plane.
0578   ///
0579   /// When the solid boundary is composed entirely of planar faces, the returned
0580   /// value is a conservative safety distance satisfying
0581   /// @c 0 ≤ s ≤ true_distance; for interior points of convex all-planar solids
0582   /// it is exact. For solids that include any curved faces (mixed-topology
0583   /// solids), this quantity is only a heuristic accelerator and is not
0584   /// guaranteed to be a strict lower bound on the true distance to the surface.
0585   ///
0586   /// Returns @c kInfinity if no planar faces exist.
0587   G4double PlanarFaceLowerBoundDistance(const G4ThreeVector& p) const;
0588 
0589   /// Find the closest trimmed face to @p point using pre-computed per-face bounding
0590   /// boxes as a lower-bound prefilter.  A face whose AABB distance from @p point
0591   /// exceeds @p maxDistance (default: @c kInfinity) or the current best candidate
0592   /// distance is skipped before the more expensive BRepExtrema_DistShapeShape call
0593   /// is made.  Passing a BVH-derived upper bound as @p maxDistance lets callers
0594   /// skip faces that are provably farther than the nearest tessellated surface point
0595   /// plus twice the BVH deflection, reducing the number of BRepExtrema calls.
0596   static std::optional<ClosestFaceMatch>
0597   TryFindClosestFace(const std::vector<FaceBounds>& faceBoundsCache, const G4ThreeVector& point,
0598                      G4double maxDistance = kInfinity);
0599 
0600   /// Build and cache (or return the already-cached) surface-sampling data for
0601   /// the current shape generation.  The OCCT shape is tessellated first
0602   /// (idempotent if a triangulation already exists), then all mesh triangles
0603   /// are collected with their areas.  The result is protected by
0604   /// @c fSurfaceCacheMutex and keyed by @c fShapeGeneration.
0605   const SurfaceSamplingCache& GetOrBuildSurfaceCache() const;
0606 };
0607 
0608 #endif // G4OCCT_G4OCCTSolid_hh