G4OCCT 0.1.0
Geant4 interface to Open CASCADE Technology (OCCT) geometry definitions
Loading...
Searching...
No Matches
G4OCCTSolid.cc
Go to the documentation of this file.
1// SPDX-License-Identifier: LGPL-2.1-or-later
2// Copyright (C) 2026 G4OCCT Contributors
3
6
8
9#include <BRepAdaptor_Curve.hxx>
10#include <BRepAdaptor_Surface.hxx>
11#include <BRepBndLib.hxx>
12#include <BRepClass3d_SolidClassifier.hxx>
13#include <BRepExtrema_DistShapeShape.hxx>
14#include <BRepExtrema_SupportType.hxx>
15#include <BRepExtrema_TriangleSet.hxx>
16#include <NCollection_Vector.hxx>
17#include <BRepGProp.hxx>
18#include <BRepLib.hxx>
19#include <BRepLProp_SLProps.hxx>
20#include <BRepMesh_IncrementalMesh.hxx>
21#include <BRep_Builder.hxx>
22#include <BRep_Tool.hxx>
23#include <BRepTools.hxx>
24#include <BRepTools_WireExplorer.hxx>
25#include <BVH_Distance.hxx>
26#include <BVH_Tools.hxx>
27#include <Bnd_Box.hxx>
28#include <ElSLib.hxx>
29#include <GeomAbs_CurveType.hxx>
30#include <GeomAbs_SurfaceType.hxx>
31#include <GProp_GProps.hxx>
32#include <GeomAPI_ProjectPointOnSurf.hxx>
33#include <Geom_Surface.hxx>
34#include <IFSelect_ReturnStatus.hxx>
35#include <IntCurvesFace_Intersector.hxx>
36#include <NCollection_Vector.hxx>
37#include <Poly_Triangulation.hxx>
38#include <Precision.hxx>
39#include <STEPControl_Reader.hxx>
40#include <TopAbs_Orientation.hxx>
41#include <TopAbs_ShapeEnum.hxx>
42#include <TopAbs_State.hxx>
43#include <TopExp_Explorer.hxx>
44#include <TopLoc_Location.hxx>
45#include <TopoDS.hxx>
46#include <TopoDS_Edge.hxx>
47#include <TopoDS_Face.hxx>
48#include <TopoDS_Vertex.hxx>
49#include <TopoDS_Wire.hxx>
50#include <gp_Dir.hxx>
51#include <gp_Lin.hxx>
52#include <gp_Pln.hxx>
53#include <gp_Pnt.hxx>
54#include <gp_Pnt2d.hxx>
55#include <gp_Vec.hxx>
56
57#include <G4BoundingEnvelope.hh>
58#include <G4AffineTransform.hh>
59#include <G4Exception.hh>
60#include <G4GeometryTolerance.hh>
61#include <G4Polyhedron.hh>
62#include <G4TessellatedSolid.hh>
63#include <G4TriangularFacet.hh>
64#include <G4VGraphicsScene.hh>
65#include <G4VisExtent.hh>
66#include <Randomize.hh>
67
68#include <algorithm>
69#include <cmath>
70#include <optional>
71#include <stdexcept>
72#include <string>
73#include <utility>
74
75namespace {
76
81constexpr Standard_Real kRelativeDeflection = 0.01;
82
94class PointToMeshDistance
95 : public BVH_Distance<Standard_Real, 3, BVH_Vec3d, BRepExtrema_TriangleSet> {
96public:
99 Standard_Boolean RejectNode(const BVH_Vec3d& theCornerMin, const BVH_Vec3d& theCornerMax,
100 Standard_Real& theMetric) const override {
101 theMetric =
102 BVH_Tools<Standard_Real, 3>::PointBoxSquareDistance(myObject, theCornerMin, theCornerMax);
103 return RejectMetric(theMetric);
104 }
105
107 Standard_Boolean Accept(const Standard_Integer theIndex, const Standard_Real&) override {
108 BVH_Vec3d v0, v1, v2;
109 myBVHSet->GetVertices(theIndex, v0, v1, v2);
110 const Standard_Real sq =
111 BVH_Tools<Standard_Real, 3>::PointTriangleSquareDistance(myObject, v0, v1, v2);
112 if (sq < myDistance) {
113 myDistance = sq;
114 myBestIndex = theIndex;
115 return Standard_True;
116 }
117 return Standard_False;
118 }
119
123 Standard_Integer BestIndex() const { return myBestIndex; }
124
125private:
126 Standard_Integer myBestIndex{-1};
127};
128
139class TriangleRayCast
140 : public BVH_Traverse<Standard_Real, 3, BRepExtrema_TriangleSet, Standard_Real> {
141public:
144 void SetRay(const BVH_Vec3d& theOrigin, const BVH_Vec3d& theDir, Standard_Real theTolerance) {
145 myOrigin = theOrigin;
146 myDir = theDir;
147 myTolerance = theTolerance;
148
149 // Reset traversal output state so each cast starts from a clean state.
150 myCrossings = 0;
151 myOnSurface = Standard_False;
152 myDegenerate = Standard_False;
153 }
154
156 Standard_Boolean RejectNode(const BVH_Vec3d& theCornerMin, const BVH_Vec3d& theCornerMax,
157 Standard_Real& theMetric) const override {
158 Standard_Real tmin = 0.0;
159 Standard_Real tmax = Precision::Infinite();
160 for (int k = 0; k < 3; ++k) {
161 const Standard_Real dk = (k == 0) ? myDir.x() : (k == 1) ? myDir.y() : myDir.z();
162 const Standard_Real ok = (k == 0) ? myOrigin.x() : (k == 1) ? myOrigin.y() : myOrigin.z();
163 const Standard_Real ck_min = (k == 0) ? theCornerMin.x()
164 : (k == 1) ? theCornerMin.y()
165 : theCornerMin.z();
166 const Standard_Real ck_max = (k == 0) ? theCornerMax.x()
167 : (k == 1) ? theCornerMax.y()
168 : theCornerMax.z();
169 if (std::abs(dk) < Precision::Confusion()) {
170 if (ok < ck_min - myTolerance || ok > ck_max + myTolerance) {
171 return Standard_True;
172 }
173 } else {
174 Standard_Real t1 = (ck_min - ok) / dk;
175 Standard_Real t2 = (ck_max - ok) / dk;
176 if (t1 > t2) {
177 std::swap(t1, t2);
178 }
179 if (t1 > tmin) {
180 tmin = t1;
181 }
182 if (t2 < tmax) {
183 tmax = t2;
184 }
185 if (tmin > tmax + myTolerance) {
186 return Standard_True;
187 }
188 }
189 }
190 theMetric = tmin;
191 return Standard_False;
192 }
193
195 Standard_Boolean Accept(const Standard_Integer theIndex, const Standard_Real&) override {
196 BVH_Vec3d v0, v1, v2;
197 myBVHSet->GetVertices(theIndex, v0, v1, v2);
198 const BVH_Vec3d edge1 = v1 - v0;
199 const BVH_Vec3d edge2 = v2 - v0;
200 const BVH_Vec3d h = BVH_Vec3d::Cross(myDir, edge2);
201 const Standard_Real a = edge1.Dot(h);
202 if (std::abs(a) < 1e-12) {
203 return Standard_False; // ray parallel to triangle plane
204 }
205 const Standard_Real f = 1.0 / a;
206 const BVH_Vec3d s = myOrigin - v0;
207 const Standard_Real u = f * s.Dot(h);
208 if (u < 0.0 || u > 1.0) {
209 return Standard_False;
210 }
211 const BVH_Vec3d q = BVH_Vec3d::Cross(s, edge1);
212 const Standard_Real v = f * myDir.Dot(q);
213 if (v < 0.0 || u + v > 1.0) {
214 return Standard_False;
215 }
216 const Standard_Real t = f * edge2.Dot(q);
217 if (std::abs(t) <= myTolerance) {
218 myOnSurface = Standard_True; // ray origin lies on this triangle
219 return Standard_True;
220 }
221 if (t < -myTolerance) {
222 return Standard_False; // triangle is behind the ray origin
223 }
224 // Forward crossing at t > myTolerance.
225 ++myCrossings;
226 // Proximity to an edge or vertex signals a potentially degenerate parity count.
227 constexpr Standard_Real kEdgeTol = 1e-6;
228 if (u < kEdgeTol || v < kEdgeTol || (1.0 - u - v) < kEdgeTol) {
229 myDegenerate = Standard_True;
230 }
231 return Standard_True;
232 }
233
235 Standard_Boolean RejectMetric(const Standard_Real&) const override { return Standard_False; }
236
238 Standard_Boolean Stop() const override { return Standard_False; }
239
240 int Crossings() const { return myCrossings; }
241 Standard_Boolean OnSurface() const { return myOnSurface; }
242 Standard_Boolean Degenerate() const { return myDegenerate; }
243
244private:
245 BVH_Vec3d myOrigin;
246 BVH_Vec3d myDir;
247 Standard_Real myTolerance{1e-7};
248 int myCrossings{0};
249 Standard_Boolean myOnSurface{Standard_False};
250 Standard_Boolean myDegenerate{Standard_False};
251};
252
254gp_Pnt ToPoint(const G4ThreeVector& point) { return gp_Pnt(point.x(), point.y(), point.z()); }
255
257G4double IntersectionTolerance() {
258 return 0.5 * G4GeometryTolerance::GetInstance()->GetSurfaceTolerance();
259}
260
262TopoDS_Vertex MakeVertex(const G4ThreeVector& point) {
263 BRep_Builder builder;
264 TopoDS_Vertex vertex;
265 builder.MakeVertex(vertex, ToPoint(point), IntersectionTolerance());
266 return vertex;
267}
268
270EInside ToG4Inside(const TopAbs_State state) {
271 switch (state) {
272 case TopAbs_IN:
273 return kInside;
274 case TopAbs_ON:
275 return kSurface;
276 case TopAbs_OUT:
277 default:
278 return kOutside;
279 }
280}
281
283G4ThreeVector FallbackNormal() { return G4ThreeVector(0.0, 0.0, 1.0); }
284
290bool PointInPolygon2d(Standard_Real u, Standard_Real v, const std::vector<gp_Pnt2d>& poly) {
291 const std::size_t n = poly.size();
292 int crossings = 0;
293 for (std::size_t i = 0; i < n; ++i) {
294 const gp_Pnt2d& a = poly[i];
295 const gp_Pnt2d& b = poly[(i + 1) % n];
296 const Standard_Real av = a.Y();
297 const Standard_Real bv = b.Y();
298 if ((av <= v && bv > v) || (bv <= v && av > v)) {
299 const Standard_Real uCross = a.X() + (v - av) * (b.X() - a.X()) / (bv - av);
300 if (u < uCross) {
301 ++crossings;
302 }
303 }
304 }
305 return (crossings % 2) == 1;
306}
307
312bool PointOnPolygonBoundary2d(Standard_Real u, Standard_Real v, const std::vector<gp_Pnt2d>& poly,
313 Standard_Real tol) {
314 const Standard_Real tol2 = tol * tol;
315 const std::size_t n = poly.size();
316 for (std::size_t i = 0; i < n; ++i) {
317 const gp_Pnt2d& a = poly[i];
318 const gp_Pnt2d& b = poly[(i + 1) % n];
319 const Standard_Real dx = b.X() - a.X();
320 const Standard_Real dy = b.Y() - a.Y();
321 const Standard_Real len2 = dx * dx + dy * dy;
322 Standard_Real px = 0.0;
323 Standard_Real py = 0.0;
324 if (len2 < 1.0e-20) {
325 px = a.X();
326 py = a.Y();
327 } else {
328 const Standard_Real t_seg =
329 std::max(0.0, std::min(1.0, ((u - a.X()) * dx + (v - a.Y()) * dy) / len2));
330 px = a.X() + t_seg * dx;
331 py = a.Y() + t_seg * dy;
332 }
333 const Standard_Real dist2 = (u - px) * (u - px) + (v - py) * (v - py);
334 if (dist2 <= tol2) {
335 return true;
336 }
337 }
338 return false;
339}
340
359std::optional<Standard_Real>
360RayPlaneFaceHit(const gp_Lin& ray, const gp_Pln& plane, const std::vector<gp_Pnt2d>& uvPoly,
361 Standard_Real tMin, Standard_Real tMax, Standard_Real tolerance,
362 Standard_Real* u_out = nullptr, Standard_Real* v_out = nullptr) {
363 const gp_Dir& lineDir = ray.Direction();
364 const gp_Dir& plnNormal = plane.Axis().Direction();
365 const Standard_Real denom =
366 plnNormal.X() * lineDir.X() + plnNormal.Y() * lineDir.Y() + plnNormal.Z() * lineDir.Z();
367 // Ray parallel to the plane: no intersection (or entire ray lies on plane,
368 // which we treat as no intersection for navigation purposes).
369 if (std::abs(denom) < 1.0e-10) {
370 return std::nullopt;
371 }
372 const gp_Pnt& orig = ray.Location();
373 const gp_Pnt& planePt = plane.Location();
374 const Standard_Real numer = plnNormal.X() * (planePt.X() - orig.X()) +
375 plnNormal.Y() * (planePt.Y() - orig.Y()) +
376 plnNormal.Z() * (planePt.Z() - orig.Z());
377 const Standard_Real t = numer / denom;
378 if (t < tMin || t > tMax) {
379 return std::nullopt;
380 }
381 const gp_Pnt hitPt(orig.X() + t * lineDir.X(), orig.Y() + t * lineDir.Y(),
382 orig.Z() + t * lineDir.Z());
383 Standard_Real u = 0.0;
384 Standard_Real v = 0.0;
385 ElSLib::PlaneParameters(plane.Position(), hitPt, u, v);
386 // Accept the hit when the UV point is strictly inside the polygon, or when it
387 // lies within tolerance of any polygon edge (the TopAbs_ON-equivalent case).
388 // Pure boundary-miss points that are strictly outside are rejected.
389 if (!PointInPolygon2d(u, v, uvPoly) && !PointOnPolygonBoundary2d(u, v, uvPoly, tolerance)) {
390 return std::nullopt;
391 }
392 if (u_out != nullptr) {
393 *u_out = u;
394 }
395 if (v_out != nullptr) {
396 *v_out = v;
397 }
398 return t;
399}
400
404std::optional<G4ThreeVector> TryGetOutwardNormal(const BRepAdaptor_Surface& surface,
405 const TopoDS_Face& face, const Standard_Real u,
406 const Standard_Real v) {
407 Standard_Real adjustedU = u;
408 Standard_Real adjustedV = v;
409 const Standard_Real tolerance = IntersectionTolerance();
410 // Nudge U away from parametric boundaries to avoid seam singularities.
411 // For periodic surfaces this prevents descending into OCCT's 2D seam
412 // classifier; for non-periodic surfaces with degenerate boundary edges
413 // (e.g. the poles of a sphere) this ensures IsNormalDefined() returns true.
414 {
415 const Standard_Real uFirst = surface.FirstUParameter();
416 const Standard_Real uLast = surface.LastUParameter();
417 const Standard_Real uEpsilon = std::min(
418 std::max(surface.UResolution(tolerance), Precision::PConfusion()), 0.5 * (uLast - uFirst));
419 if (std::abs(adjustedU - uFirst) <= uEpsilon) {
420 adjustedU = std::min(uFirst + uEpsilon, uLast);
421 } else if (std::abs(adjustedU - uLast) <= uEpsilon) {
422 adjustedU = std::max(uLast - uEpsilon, uFirst);
423 }
424 }
425 // Nudge V away from parametric boundaries. Non-periodic V boundaries
426 // include degenerate pole edges on spheres (V = ±π/2) and cone apices.
427 {
428 const Standard_Real vFirst = surface.FirstVParameter();
429 const Standard_Real vLast = surface.LastVParameter();
430 const Standard_Real vEpsilon = std::min(
431 std::max(surface.VResolution(tolerance), Precision::PConfusion()), 0.5 * (vLast - vFirst));
432 if (std::abs(adjustedV - vFirst) <= vEpsilon) {
433 adjustedV = std::min(vFirst + vEpsilon, vLast);
434 } else if (std::abs(adjustedV - vLast) <= vEpsilon) {
435 adjustedV = std::max(vLast - vEpsilon, vFirst);
436 }
437 }
438
439 BRepLProp_SLProps props(surface, adjustedU, adjustedV, 1, tolerance);
440 // If IsNormalDefined() fails the initial nudge may still be too small for
441 // OCCT's internal null-derivative threshold (e.g. a sphere pole where
442 // |dP/dU| = R·sin(δ) must exceed IntersectionTolerance()). Retry with
443 // exponentially larger V nudges, walking toward the V midpoint.
444 if (!props.IsNormalDefined()) {
445 const Standard_Real vFirst = surface.FirstVParameter();
446 const Standard_Real vLast = surface.LastVParameter();
447 const Standard_Real vMid = 0.5 * (vFirst + vLast);
448 const bool nearVFirst = (adjustedV < vMid);
449 const Standard_Real vRes = std::max(surface.VResolution(tolerance), Precision::PConfusion());
450 Standard_Real finalRetryV = adjustedV;
451 for (int attempt = 0; attempt < 8 && !props.IsNormalDefined(); ++attempt) {
452 const Standard_Real scale = std::pow(10.0, static_cast<Standard_Real>(attempt));
453 const Standard_Real nudge = scale * vRes;
454 finalRetryV =
455 nearVFirst ? std::min(adjustedV + nudge, vMid) : std::max(adjustedV - nudge, vMid);
456 props = BRepLProp_SLProps(surface, adjustedU, finalRetryV, 1, tolerance);
457 }
458 if (!props.IsNormalDefined()) {
459 return std::nullopt;
460 }
461 // Guard: if the retry had to drift V by more than kMaxRetryVDriftFraction of
462 // the full V range, the normal is sampled from a different surface region
463 // (e.g. the equatorial band instead of the pole on a NURBS ellipsoid), which
464 // would produce a spurious mismatch against the exact analytic normal, so we
465 // treat the normal as unavailable at this point and return std::nullopt for
466 // the caller to handle (e.g. by leaving the normal invalid or using a fallback).
467 constexpr Standard_Real kMaxRetryVDriftFraction = 0.10;
468 if (std::fabs(finalRetryV - adjustedV) > kMaxRetryVDriftFraction * (vLast - vFirst)) {
469 return std::nullopt;
470 }
471 }
472
473 gp_Dir faceNormal = props.Normal();
474 if (face.Orientation() == TopAbs_REVERSED) {
475 faceNormal.Reverse();
476 }
477
478 return G4ThreeVector(faceNormal.X(), faceNormal.Y(), faceNormal.Z());
479}
480
481} // namespace
482
483G4OCCTSolid::G4OCCTSolid(const G4String& name, const TopoDS_Shape& shape)
484 : G4VSolid(name), fShape(shape) {
485 if (fShape.IsNull()) {
486 throw std::invalid_argument("G4OCCTSolid: shape must not be null");
487 }
488 ComputeBounds();
489}
490
492 // Manually clear cached OCCT objects to avoid G4Exception during static destruction.
493 // G4Cache::Destroy() calls G4Exception with FatalException if cache size validation
494 // fails, which can happen when G4SolidStore cleanup occurs during exit() and the
495 // thread-local storage has already been torn down.
496 // Resetting the optional values here releases the OCCT objects before G4Cache
497 // attempts to destroy the thread-local storage.
498 fClassifierCache.Get().classifier.reset();
499 fIntersectorCache.Get().faceIntersectors.clear();
500 fSphereCache.Get().spheres.clear();
501}
502
503// ── G4OCCTSolid static factory ────────────────────────────────────────────────
504
505G4OCCTSolid* G4OCCTSolid::FromSTEP(const G4String& name, const std::string& path) {
506 STEPControl_Reader reader;
507 if (reader.ReadFile(path.c_str()) != IFSelect_RetDone) {
508 throw std::runtime_error("G4OCCTSolid::FromSTEP: failed to read STEP file: " + path);
509 }
510 if (reader.TransferRoots() <= 0) {
511 throw std::runtime_error("G4OCCTSolid::FromSTEP: no roots transferred from: " + path);
512 }
513 TopoDS_Shape shape = reader.OneShape();
514 if (shape.IsNull()) {
515 throw std::runtime_error("G4OCCTSolid::FromSTEP: null shape loaded from: " + path);
516 }
517 return new G4OCCTSolid(name, shape);
518}
519
520// ── G4OCCTSolid private helpers ───────────────────────────────────────────────
521
522void G4OCCTSolid::ComputeBounds() {
523 // Pre-build PCurves for edges on planar faces so BRep_Tool::CurveOnPlane()
524 // returns the stored result on every Inside() query instead of recomputing
525 // via GeomProjLib::ProjectOnPlane each time (~3.3% of total instructions).
526 for (TopExp_Explorer faceEx(fShape, TopAbs_FACE); faceEx.More(); faceEx.Next()) {
527 const TopoDS_Face& face = TopoDS::Face(faceEx.Current());
528 if (BRepAdaptor_Surface(face).GetType() != GeomAbs_Plane) {
529 continue;
530 }
531 for (TopExp_Explorer edgeEx(face, TopAbs_EDGE); edgeEx.More(); edgeEx.Next()) {
532 BRepLib::BuildPCurveForEdgeOnPlane(TopoDS::Edge(edgeEx.Current()), face);
533 }
534 }
535
536 Bnd_Box boundingBox;
537 BRepBndLib::AddOptimal(fShape, boundingBox, /*useTriangulation=*/Standard_False);
538 if (boundingBox.IsVoid()) {
539 throw std::invalid_argument("G4OCCTSolid: shape has no computable bounding box (no geometry)");
540 }
541
542 Standard_Real xMin = 0.0;
543 Standard_Real yMin = 0.0;
544 Standard_Real zMin = 0.0;
545 Standard_Real xMax = 0.0;
546 Standard_Real yMax = 0.0;
547 Standard_Real zMax = 0.0;
548 boundingBox.Get(xMin, yMin, zMin, xMax, yMax, zMax);
549
550 fCachedBounds =
551 AxisAlignedBounds{G4ThreeVector(xMin, yMin, zMin), G4ThreeVector(xMax, yMax, zMax)};
552
553 // Build per-face bounding-box cache for the SurfaceNormal prefilter.
554 fFaceBoundsCache.clear();
555 fFaceAdaptorIndex.clear();
556 G4double maxFaceDiag = 0.0;
557 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
558 Bnd_Box faceBox;
559 BRepBndLib::AddOptimal(ex.Current(), faceBox, /*useTriangulation=*/Standard_False);
560 const TopoDS_Face& currentFace = TopoDS::Face(ex.Current());
561 const std::size_t idx = fFaceBoundsCache.size();
562 BRepAdaptor_Surface adaptor(currentFace);
563 std::optional<gp_Pln> maybePlane;
564 std::vector<gp_Pnt2d> uvPolygon;
565 std::optional<G4ThreeVector> outwardNormal;
566 if (adaptor.GetType() == GeomAbs_Plane) {
567 maybePlane = adaptor.Plane();
568 const gp_Ax3& pos = maybePlane->Position();
569 const TopoDS_Wire wire = BRepTools::OuterWire(currentFace);
570 if (!wire.IsNull()) {
571 // Collect vertices of the outer wire in order via BRepTools_WireExplorer.
572 // Only accept faces where every edge is a straight line segment: for faces
573 // with curved edges (arcs, splines) the vertex-only polygon is an
574 // under-approximation and would produce false misses.
575 bool allLinear = true;
576 std::vector<gp_Pnt2d> poly;
577 for (BRepTools_WireExplorer we(wire, currentFace); we.More(); we.Next()) {
578 const BRepAdaptor_Curve ec(we.Current());
579 if (ec.GetType() != GeomAbs_Line) {
580 allLinear = false;
581 break;
582 }
583 const gp_Pnt pt = BRep_Tool::Pnt(we.CurrentVertex());
584 Standard_Real u = 0.0;
585 Standard_Real v = 0.0;
586 ElSLib::PlaneParameters(pos, pt, u, v);
587 poly.emplace_back(u, v);
588 }
589 if (allLinear && poly.size() >= 3) {
590 // Verify the face has no inner wires (holes). The outer-wire-only polygon
591 // cannot represent cutouts: a ray hitting inside the outer boundary but
592 // inside a hole would be incorrectly counted as a face crossing.
593 int wireCount = 0;
594 for (TopExp_Explorer wc(currentFace, TopAbs_WIRE); wc.More(); wc.Next()) {
595 ++wireCount;
596 if (wireCount > 1) {
597 break;
598 }
599 }
600 if (wireCount == 1) {
601 uvPolygon = std::move(poly);
602 // Precompute the constant outward face normal. For planar faces the
603 // normal is uniform; gp_Pln::Axis() points along the plane normal, but
604 // the outward direction depends on the face orientation in the solid.
605 gp_Dir faceNormal = maybePlane->Axis().Direction();
606 if (currentFace.Orientation() == TopAbs_REVERSED) {
607 faceNormal.Reverse();
608 }
609 outwardNormal = G4ThreeVector(faceNormal.X(), faceNormal.Y(), faceNormal.Z());
610 }
611 }
612 }
613 }
614 fFaceBoundsCache.push_back({currentFace, faceBox, std::move(adaptor), std::move(maybePlane),
615 std::move(uvPolygon), std::move(outwardNormal)});
616 fFaceAdaptorIndex[currentFace.TShape().get()].push_back(idx);
617 // Track the largest face bounding-box diagonal to bound the tessellation error.
618 if (!faceBox.IsVoid()) {
619 Standard_Real fx0 = 0.0;
620 Standard_Real fy0 = 0.0;
621 Standard_Real fz0 = 0.0;
622 Standard_Real fx1 = 0.0;
623 Standard_Real fy1 = 0.0;
624 Standard_Real fz1 = 0.0;
625 faceBox.Get(fx0, fy0, fz0, fx1, fy1, fz1);
626 const G4double diag = G4ThreeVector(fx1 - fx0, fy1 - fy0, fz1 - fz0).mag();
627 maxFaceDiag = std::max(maxFaceDiag, diag);
628 }
629 }
630
631 // fBVHDeflection bounds the Hausdorff distance between the analytical surface
632 // and the tessellation built with relative deflection kRelativeDeflection.
633 fBVHDeflection = kRelativeDeflection * maxFaceDiag;
634
635 // Determine if all faces are planar (enables the fast plane-distance path in DistanceToOut).
636 fAllFacesPlanar = std::all_of(fFaceBoundsCache.begin(), fFaceBoundsCache.end(),
637 [](const FaceBounds& fb) { return fb.plane.has_value(); });
638
639 // Ensure a triangulation is present (idempotent if mesh already exists).
640 // Limit lifetime to this scope so mesh resources are released before building
641 // the BVH, preserving the original temporary-object behavior.
642 {
643 [[maybe_unused]] const BRepMesh_IncrementalMesh mesher(fShape, kRelativeDeflection,
644 /*isRelative=*/Standard_True);
645 }
646
647 // Build the BVH-accelerated triangle set over all tessellated faces.
648 NCollection_Vector<TopoDS_Shape> faces;
649 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
650 faces.Append(ex.Current());
651 }
652 if (faces.IsEmpty()) {
653 fTriangleSet.Nullify();
654 fFaceDeflections.clear();
655 } else {
656 fTriangleSet = new BRepExtrema_TriangleSet(faces);
657
658 // Build per-face deflection table indexed in the same order as fFaceBoundsCache
659 // (and as the faces list above — both use the same TopExp_Explorer traversal).
660 // At query time, BRepExtrema_TriangleSet::GetFaceID() maps the nearest triangle's
661 // post-BVH-reorder index back to its face index, allowing a per-face lookup here.
662 fFaceDeflections.clear();
663 fFaceDeflections.reserve(fFaceBoundsCache.size());
664 for (const FaceBounds& fb : fFaceBoundsCache) {
665 // Start from the global BVH deflection so that per-face values never
666 // reduce the safety margin below fBVHDeflection, even for faces with
667 // a void bounding box or very small extent.
668 G4double deflection = fBVHDeflection;
669 if (!fb.box.IsVoid()) {
670 Standard_Real fx0 = 0.0, fy0 = 0.0, fz0 = 0.0;
671 Standard_Real fx1 = 0.0, fy1 = 0.0, fz1 = 0.0;
672 fb.box.Get(fx0, fy0, fz0, fx1, fy1, fz1);
673 const G4double faceDiag = G4ThreeVector(fx1 - fx0, fy1 - fy0, fz1 - fz0).mag();
674 deflection = kRelativeDeflection * faceDiag;
675 }
676 fFaceDeflections.push_back(deflection);
677 }
678 }
679
680 ComputeInitialSpheres();
681}
682
683void G4OCCTSolid::ComputeInitialSpheres() {
684 fInitialSpheres.clear();
685
686 const G4double tol = IntersectionTolerance();
687 const G4ThreeVector& bmin = fCachedBounds.min;
688 const G4ThreeVector& bmax = fCachedBounds.max;
689 const G4ThreeVector centre = 0.5 * (bmin + bmax);
690 const G4ThreeVector halfExt = 0.5 * (bmax - bmin);
691
692 // Candidate seed points: AABB centre, 6 axis-offset interior points (each at half
693 // the distance from the AABB centre to the nearest face along that axis), and 8 octant
694 // centres (15 total).
695 std::vector<G4ThreeVector> candidates;
696 candidates.reserve(15);
697 candidates.push_back(centre);
698 for (const G4double s : {-0.5, 0.5}) {
699 candidates.push_back(centre + G4ThreeVector(s * halfExt.x(), 0.0, 0.0));
700 candidates.push_back(centre + G4ThreeVector(0.0, s * halfExt.y(), 0.0));
701 candidates.push_back(centre + G4ThreeVector(0.0, 0.0, s * halfExt.z()));
702 }
703 for (const int sx : {-1, 1}) {
704 for (const int sy : {-1, 1}) {
705 for (const int sz : {-1, 1}) {
706 candidates.push_back(centre + G4ThreeVector(0.75 * sx * halfExt.x(),
707 0.75 * sy * halfExt.y(),
708 0.75 * sz * halfExt.z()));
709 }
710 }
711 }
712
713 // Use a local classifier: construction-time only, no thread-local needed.
714 BRepClass3d_SolidClassifier localClassifier;
715 localClassifier.Load(fShape);
716
717 for (const G4ThreeVector& cand : candidates) {
718 localClassifier.Perform(ToPoint(cand), tol);
719 if (localClassifier.State() != TopAbs_IN) {
720 continue;
721 }
722 G4double d = BVHLowerBoundDistance(cand);
723 if (d >= kInfinity || d <= tol) {
724 // BVH unavailable or too close to surface; try exact distance.
725 const auto match = TryFindClosestFace(fFaceBoundsCache, cand);
726 if (!match.has_value() || match->distance <= tol) {
727 continue;
728 }
729 d = match->distance;
730 }
731 fInitialSpheres.push_back({cand, d});
732 }
733 // Sort descending by radius so the most useful spheres are checked first.
734 std::sort(fInitialSpheres.begin(), fInitialSpheres.end(),
735 [](const InscribedSphere& a, const InscribedSphere& b) { return a.radius > b.radius; });
736}
737
738std::optional<G4OCCTSolid::ClosestFaceMatch>
739G4OCCTSolid::TryFindClosestFace(const std::vector<FaceBounds>& faceBoundsCache,
740 const G4ThreeVector& point, G4double maxDistance) {
741 if (faceBoundsCache.empty()) {
742 return std::nullopt;
743 }
744
745 const gp_Pnt queryPoint = ToPoint(point);
746 const TopoDS_Vertex queryVertex = MakeVertex(point);
747
748 // Build the point box once — it is constant for the entire loop.
749 Bnd_Box queryBox;
750 queryBox.Add(queryPoint);
751
752 std::optional<ClosestFaceMatch> bestMatch;
753 for (std::size_t i = 0; i < faceBoundsCache.size(); ++i) {
754 const FaceBounds& fb = faceBoundsCache[i];
755 // Lower bound: distance from query point to the face's axis-aligned bounding box.
756 // Use maxDistance as the initial pruning threshold (before a bestMatch is found),
757 // tightening to bestMatch->distance once a candidate has been accepted.
758 const G4double threshold = bestMatch.has_value() ? bestMatch->distance : maxDistance;
759 if (threshold < kInfinity && fb.box.Distance(queryBox) > threshold) {
760 continue;
761 }
762
763 // Use BRepExtrema_DistShapeShape for a robust point-to-face distance.
764 // Extrema_ExtPS only finds interior critical points of the distance function
765 // on the (unbounded) surface; for bilinear (degree-1) B-spline faces the
766 // global minimum is often on the face boundary (an edge or vertex), which
767 // Extrema_ExtPS misses. BRepExtrema_DistShapeShape considers the bounded
768 // face including all its edges and vertices, guaranteeing the true minimum.
769 BRepExtrema_DistShapeShape distance(queryVertex, fb.face);
770 if (!distance.IsDone() || distance.NbSolution() == 0) {
771 continue;
772 }
773 const G4double candidateDistance = distance.Value();
774
775 if (bestMatch.has_value() && candidateDistance >= bestMatch->distance) {
776 continue;
777 }
778 ClosestFaceMatch match{.face = fb.face, .distance = candidateDistance, .faceIndex = i};
779 // Extract UV from BRepExtrema when solution is on the face interior.
780 // For edge/vertex solutions ParOnFaceS2 has a different meaning so we
781 // skip caching in those cases and let SurfaceNormal fall back to
782 // GeomAPI_ProjectPointOnSurf.
783 if (distance.NbSolution() > 0 && distance.SupportTypeShape2(1) == BRepExtrema_IsInFace) {
784 Standard_Real u = 0.0;
785 Standard_Real v = 0.0;
786 distance.ParOnFaceS2(1, u, v);
787 match.uv = std::make_pair(u, v);
788 }
789 bestMatch = std::move(match);
790 }
791
792 return bestMatch;
793}
794
795BRepClass3d_SolidClassifier& G4OCCTSolid::GetOrCreateClassifier() const {
796 ClassifierCache& cache = fClassifierCache.Get();
797 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
798 if (cache.generation != currentGen) {
799 cache.classifier.emplace();
800 cache.classifier->Load(fShape); // O(N_faces) — paid once per thread per shape
801 cache.generation = currentGen;
802 }
803 return *cache.classifier;
804}
805
806G4OCCTSolid::IntersectorCache& G4OCCTSolid::GetOrCreateIntersector() const {
807 IntersectorCache& cache = fIntersectorCache.Get();
808 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
809 if (cache.generation != currentGen) {
810 const G4double tol = IntersectionTolerance();
811 cache.faceIntersectors.clear();
812 cache.faceIntersectors.reserve(fFaceBoundsCache.size());
813 cache.expandedBoxes.clear();
814 cache.expandedBoxes.reserve(fFaceBoundsCache.size());
815 for (const auto& fb : fFaceBoundsCache) {
816 cache.faceIntersectors.push_back(
817 std::make_unique<IntCurvesFace_Intersector>(fb.face, tol)); // O(1) per face
818 Bnd_Box expanded = fb.box;
819 expanded.Enlarge(tol); // prevent false-positive IsOut for zero-thickness planar face boxes
820 cache.expandedBoxes.push_back(std::move(expanded));
821 }
822 cache.generation = currentGen;
823 }
824 return cache;
825}
826
827G4OCCTSolid::SphereCacheData& G4OCCTSolid::GetOrInitSphereCache() const {
828 SphereCacheData& cache = fSphereCache.Get();
829 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
830 if (cache.generation != currentGen) {
831 // Re-seed from the construction-time initial spheres (already sorted descending).
832 cache.spheres = fInitialSpheres;
833 cache.generation = currentGen;
834 }
835 return cache;
836}
837
838void G4OCCTSolid::TryInsertSphere(const G4ThreeVector& centre, G4double d) const {
839 if (d <= 0.0) {
840 return;
841 }
842
843 // Enforce a minimum meaningful radius, consistent with ComputeInitialSpheres().
844 const G4double minRadius = IntersectionTolerance();
845 if (d <= minRadius) {
846 return;
847 }
848 SphereCacheData& cache = GetOrInitSphereCache();
849
850 // Quick capacity check: if at capacity and the new radius is no better than
851 // the smallest stored radius, there is nothing to gain.
852 if (cache.spheres.size() >= kMaxInscribedSpheres && d <= cache.spheres.back().radius) {
853 return;
854 }
855
856 // Dominance check: skip if the new sphere is fully contained inside an existing one.
857 // Condition: |centre − cᵢ| + d ≤ rᵢ ↔ |centre − cᵢ|² ≤ (rᵢ − d)² (when rᵢ ≥ d).
858 for (const InscribedSphere& s : cache.spheres) {
859 if (s.radius >= d) {
860 const G4double gap = s.radius - d;
861 if ((centre - s.centre).mag2() <= gap * gap) {
862 return;
863 }
864 }
865 }
866
867 // Insert in sorted position (descending by radius).
868 const InscribedSphere newSphere{centre, d};
869 const auto it = std::lower_bound(
870 cache.spheres.begin(), cache.spheres.end(), newSphere,
871 [](const InscribedSphere& a, const InscribedSphere& b) { return a.radius > b.radius; });
872 cache.spheres.insert(it, newSphere);
873
874 // Evict the smallest sphere if the cache has grown beyond capacity.
875 if (cache.spheres.size() > kMaxInscribedSpheres) {
876 cache.spheres.pop_back();
877 }
878}
879
880// ── G4VSolid pure-virtual implementations ────────────────────────────────────
881
882EInside G4OCCTSolid::Inside(const G4ThreeVector& p) const {
883 const G4double tolerance = IntersectionTolerance();
884 if (p.x() < fCachedBounds.min.x() - tolerance || p.x() > fCachedBounds.max.x() + tolerance ||
885 p.y() < fCachedBounds.min.y() - tolerance || p.y() > fCachedBounds.max.y() + tolerance ||
886 p.z() < fCachedBounds.min.z() - tolerance || p.z() > fCachedBounds.max.z() + tolerance) {
887 return kOutside;
888 }
889
890 // Fast inscribed-sphere check: if p lies strictly inside any cached sphere
891 // (with a tolerance margin), every such sphere is provably interior to the
892 // solid, so we can return kInside immediately without an OCCT classifier call.
893 // The check uses (radius - tolerance) to avoid misclassifying boundary-adjacent
894 // points as kInside — matching the open-ball guarantee from the safety distance.
895 const SphereCacheData& sphereCache = GetOrInitSphereCache();
896 for (const InscribedSphere& s : sphereCache.spheres) {
897 const G4double interiorRadius = s.radius - tolerance;
898 if (interiorRadius > 0.0 && (p - s.centre).mag2() < interiorRadius * interiorRadius) {
899 return kInside;
900 }
901 }
902
903 // Tier-2a: BVH ray-parity test over the pre-built fTriangleSet.
904 // Cast a +Z ray and count crossings via Möller–Trumbore in a single
905 // O(log N_triangles) BVH traversal, replacing the per-face
906 // IntCurvesFace_Intersector calls for curved faces.
907 if (!fTriangleSet.IsNull() && fTriangleSet->Size() > 0) {
908 // Guard: if the point is within the mesh error band (distance-to-surface
909 // not provably greater than tolerance + deflection), the tessellation may
910 // disagree with the exact solid near the boundary. Fall back to the exact
911 // classifier so the classification is always consistent with the analytic shape.
912 if (fBVHDeflection > 0.0) {
913 const G4double bvhLB = BVHLowerBoundDistance(p);
914 if (bvhLB < tolerance) {
915 BRepClass3d_SolidClassifier& classifier = GetOrCreateClassifier();
916 classifier.Perform(ToPoint(p), tolerance);
917 return ToG4Inside(classifier.State());
918 }
919 }
920
921 // Multi-ray majority vote: cast up to three orthogonal rays (+Z, +X, +Y)
922 // through the BVH triangle set and tally inside/outside votes.
923 //
924 // Fast path: if the primary (+Z) ray is non-degenerate and has a non-zero
925 // crossing count, its parity is reliable and we return immediately.
926 //
927 // Slow path (primary degenerate or zero crossings): cast +X and +Y as
928 // tie-breakers. Only non-degenerate rays contribute a vote. A clear
929 // majority (more inside votes than outside, or vice versa) determines the
930 // result. If no majority can be established (all three rays degenerate, or
931 // a genuine 1–1 tie), we fall back to the exact OCCT classifier.
932 //
933 // This eliminates BRepClass3d_SolidClassifier::Perform() calls for all but
934 // the near-surface and all-degenerate cases, collapsing the CSLib_Class2d
935 // and NCollection heap-allocation overhead for complex solids.
936 const BVH_Vec3d bvhOrigin(p.x(), p.y(), p.z());
937 const Standard_Real bvhTol = static_cast<Standard_Real>(tolerance);
938 TriangleRayCast caster;
939 caster.SetBVHSet(fTriangleSet.get());
940
941 // Primary ray: +Z
942 caster.SetRay(bvhOrigin, BVH_Vec3d(0.0, 0.0, 1.0), bvhTol);
943 caster.Select();
944
945 if (caster.OnSurface()) {
946 return kSurface;
947 }
948
949 // Fast path: primary ray is unambiguous.
950 if (!caster.Degenerate() && caster.Crossings() > 0) {
951 return (caster.Crossings() % 2 == 1) ? kInside : kOutside;
952 }
953
954 // Primary is problematic (degenerate or zero crossings). Tally its vote
955 // (excluded if degenerate), then cast two more orthogonal rays.
956 int insideVotes = 0;
957 int outsideVotes = 0;
958 if (!caster.Degenerate()) {
959 if (caster.Crossings() % 2 == 1) {
960 ++insideVotes;
961 } else {
962 ++outsideVotes;
963 }
964 }
965
966 const BVH_Vec3d kExtraRays[2] = {
967 BVH_Vec3d(1.0, 0.0, 0.0),
968 BVH_Vec3d(0.0, 1.0, 0.0),
969 };
970 for (const BVH_Vec3d& dir : kExtraRays) {
971 caster.SetRay(bvhOrigin, dir, bvhTol);
972 caster.Select();
973 if (caster.OnSurface()) {
974 return kSurface;
975 }
976 if (!caster.Degenerate()) {
977 if (caster.Crossings() % 2 == 1) {
978 ++insideVotes;
979 } else {
980 ++outsideVotes;
981 }
982 }
983 }
984
985 if (insideVotes > outsideVotes) {
986 return kInside;
987 }
988 if (outsideVotes > insideVotes) {
989 return kOutside;
990 }
991
992 // No majority (all degenerate, or genuine 1–1 tie): fall back to the exact
993 // OCCT classifier.
994 BRepClass3d_SolidClassifier& classifier = GetOrCreateClassifier();
995 classifier.Perform(ToPoint(p), tolerance);
996 return ToG4Inside(classifier.State());
997 }
998
999 // Tier-2b: fallback when fTriangleSet is unavailable — original per-face
1000 // ray-parity test using per-thread IntCurvesFace_Intersector cache.
1001 // Cast a ray from p in the canonical +Z direction and count how many faces
1002 // the ray crosses strictly through their interior (TopAbs_IN). Odd = inside.
1003 // TopAbs_ON hits (shared edges/vertices) are excluded from the parity count;
1004 // any such hit signals a degenerate ray and triggers a classifier fallback so
1005 // that altered parity from skipping the crossing does not misclassify the point.
1006 IntersectorCache& cache = GetOrCreateIntersector();
1007 const gp_Lin ray(ToPoint(p), gp_Dir(0.0, 0.0, 1.0));
1008 int crossings = 0;
1009 bool onSurface = false;
1010 bool degenerateRay = false;
1011
1012 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
1013 if (cache.expandedBoxes[i].IsOut(ray)) {
1014 continue;
1015 }
1016 const FaceBounds& fb = fFaceBoundsCache[i];
1017 if (!fb.uvPolygon.empty()) {
1018 // Fast path: direct ray–plane intersection without OCCT topology overhead.
1019 // Retrieve the UV hit coordinates so we can detect edge/vertex hits (which
1020 // would be TopAbs_ON in the OCCT intersector) and fall back to the
1021 // classifier just like the degenerate-ray path does for non-planar faces.
1022 Standard_Real u_hit = 0.0;
1023 Standard_Real v_hit = 0.0;
1024 const auto t = RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, -tolerance,
1025 Precision::Infinite(), tolerance, &u_hit, &v_hit);
1026 if (t) {
1027 const G4double w = static_cast<G4double>(*t);
1028 if (std::abs(w) <= tolerance) {
1029 onSurface = true;
1030 } else if (w > tolerance) {
1031 // Check whether the hit UV point is on the polygon boundary (within
1032 // tolerance): that corresponds to a TopAbs_ON state in the OCCT
1033 // intersector and requires the same degenerate-ray fallback.
1034 if (PointOnPolygonBoundary2d(u_hit, v_hit, fb.uvPolygon, tolerance)) {
1035 degenerateRay = true;
1036 } else {
1037 ++crossings;
1038 }
1039 }
1040 }
1041 } else {
1042 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
1043 fi.Perform(ray, -tolerance, Precision::Infinite());
1044 if (!fi.IsDone()) {
1045 continue;
1046 }
1047 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
1048 const G4double w = fi.WParameter(j);
1049 const TopAbs_State state = fi.State(j);
1050 if (std::abs(w) <= tolerance && (state == TopAbs_IN || state == TopAbs_ON)) {
1051 onSurface = true;
1052 } else if (w > tolerance && state == TopAbs_IN) {
1053 ++crossings;
1054 } else if (w > tolerance && state == TopAbs_ON) {
1055 degenerateRay = true;
1056 }
1057 }
1058 }
1059 }
1060
1061 if (onSurface) {
1062 return kSurface;
1063 }
1064 // Safety fallback: use the classifier when the parity count is unreliable —
1065 // either because no TopAbs_IN crossings were found (ray may pass entirely
1066 // through edges/vertices) or because at least one TopAbs_ON edge/vertex hit
1067 // was skipped (altering the effective crossing count).
1068 if (crossings == 0 || degenerateRay) {
1069 BRepClass3d_SolidClassifier& classifier = GetOrCreateClassifier();
1070 classifier.Perform(ToPoint(p), tolerance);
1071 return ToG4Inside(classifier.State());
1072 }
1073 return (crossings % 2 == 1) ? kInside : kOutside;
1074}
1075
1076G4ThreeVector G4OCCTSolid::SurfaceNormal(const G4ThreeVector& p) const {
1077 // Phase 1: all-planar fast path — O(N_faces) dot products, zero OCCT solver calls.
1078 // For planar solids every face's outward normal is constant and precomputed in
1079 // fFaceBoundsCache. The closest face is identified by the minimum plane distance.
1080 if (fAllFacesPlanar) {
1081 const gp_Pnt pt = ToPoint(p);
1082 const FaceBounds* bestFB = nullptr;
1083 G4double bestDist = kInfinity;
1084 for (const FaceBounds& fb : fFaceBoundsCache) {
1085 if (!fb.plane.has_value() || !fb.outwardNormal.has_value()) {
1086 continue;
1087 }
1088 const G4double d = fb.plane->Distance(pt);
1089 if (d < bestDist) {
1090 bestDist = d;
1091 bestFB = &fb;
1092 }
1093 }
1094 if (bestFB) {
1095 return *bestFB->outwardNormal;
1096 }
1097 }
1098
1099 // Phase 2: BVH-seeded face identification for curved (or mixed) solids.
1100 //
1101 // Use face-local extrema + projection instead of a single solid-wide
1102 // BRepExtrema_DistShapeShape(vertex, solid) query. On imported periodic
1103 // surfaces (notably the torus seam hit at (15,0,0) in the geometry-fixture
1104 // tests), the solid-wide path can descend into OCCT's 2D seam classifier and
1105 // either wedge in boundary classification or report an edge-supported
1106 // solution whose direct normal evaluation falls back to +Z. Face-local
1107 // selection still identifies the nearest supporting face, while the explicit
1108 // projection step recovers a usable (u,v) pair that we can nudge off exact
1109 // periodic seams before asking OCCT for derivatives. Revisit this once the
1110 // upstream periodic-classifier fix is available everywhere we build.
1111 //
1112 // BVHLowerBoundDistance gives a lower bound on the true distance to the surface.
1113 // Adding 2×fBVHDeflection (Hausdorff bound between tessellation and analytical
1114 // surface) yields a valid upper bound on the distance to the nearest surface point.
1115 // This seed is passed to TryFindClosestFace to prune faces that are provably
1116 // farther away, reducing the number of BRepExtrema calls for multi-face solids.
1117 // TryFindClosestFace then uses exact BRepExtrema on every candidate, guaranteeing
1118 // the correct nearest face is returned regardless of tessellation approximation
1119 // errors (which could fool a pure-BVH face-identification approach).
1120
1121 // Shared helper: project @p onto @face using a pre-built surface adaptor,
1122 // obtain (u,v), and evaluate the outward normal. Accepting the cached
1123 // BRepAdaptor_Surface avoids reconstructing it on the fly on this hot path.
1124 const auto projectAndGetNormalFallback = [&](const FaceBounds& fb) -> G4ThreeVector {
1125 TopLoc_Location loc;
1126 const Handle(Geom_Surface) surface = BRep_Tool::Surface(fb.face, loc);
1127 if (surface.IsNull()) {
1128 return FallbackNormal();
1129 }
1130 gp_Pnt pLocal = ToPoint(p);
1131 if (!loc.IsIdentity()) {
1132 pLocal.Transform(loc.Transformation().Inverted());
1133 }
1134 GeomAPI_ProjectPointOnSurf projection(pLocal, surface);
1135 if (projection.NbPoints() == 0) {
1136 return FallbackNormal();
1137 }
1138 Standard_Real u = 0.0;
1139 Standard_Real v = 0.0;
1140 projection.LowerDistanceParameters(u, v);
1141 return TryGetOutwardNormal(fb.adaptor, fb.face, u, v).value_or(FallbackNormal());
1142 };
1143
1144 const G4double bvhLB = BVHLowerBoundDistance(p);
1145 const G4double seedDist =
1146 (fBVHDeflection > 0.0 && bvhLB < kInfinity) ? bvhLB + 2.0 * fBVHDeflection : kInfinity;
1147 const auto closestFaceMatch = TryFindClosestFace(fFaceBoundsCache, p, seedDist);
1148 if (!closestFaceMatch.has_value()) {
1149 return FallbackNormal();
1150 }
1151 const FaceBounds& fb = fFaceBoundsCache[closestFaceMatch->faceIndex];
1152
1153 // Fast-path A: planar face with precomputed constant outward normal.
1154 // Applies even in mixed solids (e.g. G4CutTubs) where fAllFacesPlanar is
1155 // false but the closest face happens to be a flat endcap.
1156 if (fb.outwardNormal.has_value()) {
1157 return *fb.outwardNormal;
1158 }
1159
1160 // Fast-path B: reuse UV already computed by BRepExtrema_DistShapeShape
1161 // when the solution is on the face interior, skipping GeomAPI_ProjectPointOnSurf.
1162 if (closestFaceMatch->uv.has_value()) {
1163 const auto [u, v] = *closestFaceMatch->uv;
1164 const auto result = TryGetOutwardNormal(fb.adaptor, fb.face, u, v);
1165 if (result.has_value()) {
1166 return *result;
1167 }
1168 // Fall through to full projection if normal evaluation failed at stored UV
1169 // (e.g. torus seam or degenerate point).
1170 }
1171
1172 // Fallback: full GeomAPI_ProjectPointOnSurf reprojection for edge/vertex
1173 // solutions and cases where TryGetOutwardNormal returned nullopt.
1174 return projectAndGetNormalFallback(fb);
1175}
1176
1177G4double G4OCCTSolid::DistanceToIn(const G4ThreeVector& p, const G4ThreeVector& v) const {
1178 const G4double tolerance = IntersectionTolerance();
1179 const gp_Lin ray(ToPoint(p), gp_Dir(v.x(), v.y(), v.z()));
1180
1181 IntersectorCache& cache = GetOrCreateIntersector();
1182
1183 G4double minDistance = kInfinity;
1184 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
1185 if (cache.expandedBoxes[i].IsOut(ray)) {
1186 continue;
1187 }
1188 const FaceBounds& fb = fFaceBoundsCache[i];
1189 if (!fb.uvPolygon.empty()) {
1190 // Fast path: bypass IntCurvesFace_Intersector for straight-edge planar faces.
1191 const auto t = RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, tolerance, Precision::Infinite(),
1192 tolerance);
1193 if (t && *t < minDistance) {
1194 minDistance = static_cast<G4double>(*t);
1195 }
1196 } else {
1197 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
1198 fi.Perform(ray, tolerance, Precision::Infinite());
1199 if (!fi.IsDone()) {
1200 continue;
1201 }
1202 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
1203 const G4double w = fi.WParameter(j);
1204 if (w > tolerance && w < minDistance) {
1205 minDistance = w;
1206 }
1207 }
1208 }
1209 }
1210
1211 return minDistance;
1212}
1213
1214G4double G4OCCTSolid::ExactDistanceToIn(const G4ThreeVector& p) const {
1215 BRepClass3d_SolidClassifier& classifier = GetOrCreateClassifier();
1216 classifier.Perform(ToPoint(p), IntersectionTolerance());
1217 if (classifier.State() == TopAbs_IN || classifier.State() == TopAbs_ON) {
1218 return 0.0;
1219 }
1220
1221 const auto match = TryFindClosestFace(fFaceBoundsCache, p);
1222 if (!match.has_value()) {
1223 return kInfinity;
1224 }
1225 return (match->distance <= IntersectionTolerance()) ? 0.0 : match->distance;
1226}
1227
1228G4double G4OCCTSolid::AABBLowerBound(const G4ThreeVector& p) const {
1229 const G4ThreeVector& mn = fCachedBounds.min;
1230 const G4ThreeVector& mx = fCachedBounds.max;
1231 const G4double dx = std::max({0.0, mn.x() - p.x(), p.x() - mx.x()});
1232 const G4double dy = std::max({0.0, mn.y() - p.y(), p.y() - mx.y()});
1233 const G4double dz = std::max({0.0, mn.z() - p.z(), p.z() - mx.z()});
1234 return std::sqrt(dx * dx + dy * dy + dz * dz);
1235}
1236
1237G4double G4OCCTSolid::BVHLowerBoundDistance(const G4ThreeVector& p) const {
1238 if (fTriangleSet.IsNull() || fTriangleSet->Size() == 0) {
1239 return kInfinity;
1240 }
1241 PointToMeshDistance solver;
1242 solver.SetObject(BVH_Vec3d(p.x(), p.y(), p.z()));
1243 solver.SetBVHSet(fTriangleSet.get());
1244 const Standard_Real meshDistSq = solver.ComputeDistance();
1245 if (!solver.IsDone()) {
1246 return kInfinity;
1247 }
1248 // The solver returns a squared distance; take the single sqrt here
1249 // before subtracting the deflection bound (which is in actual-distance space).
1250 const G4double meshDist = std::sqrt(static_cast<G4double>(meshDistSq));
1251
1252 // Use per-face deflection for the face owning the nearest triangle.
1253 // BRepExtrema_TriangleSet::GetFaceID() maps the BVH-reordered triangle index
1254 // back to the face index in fFaceBoundsCache (the same ordering used to build
1255 // the shape list passed to fTriangleSet). This is tighter than fBVHDeflection
1256 // when the nearest triangle belongs to a small face.
1257 G4double deflection = fBVHDeflection;
1258 const Standard_Integer bestIdx = solver.BestIndex();
1259 if (bestIdx >= 0) {
1260 const Standard_Integer faceId = fTriangleSet->GetFaceID(bestIdx);
1261 if (faceId >= 0 && static_cast<std::size_t>(faceId) < fFaceDeflections.size()) {
1262 deflection = fFaceDeflections[static_cast<std::size_t>(faceId)];
1263 }
1264 }
1265
1266 return std::max(0.0, meshDist - deflection);
1267}
1268
1269G4double G4OCCTSolid::PlanarFaceLowerBoundDistance(const G4ThreeVector& p) const {
1270 const gp_Pnt pt = ToPoint(p);
1271 G4double minDist = kInfinity;
1272 for (const FaceBounds& fb : fFaceBoundsCache) {
1273 if (!fb.plane.has_value()) {
1274 continue;
1275 }
1276 const G4double d = static_cast<G4double>(fb.plane->Distance(pt));
1277 if (d < minDist) {
1278 minDist = d;
1279 }
1280 }
1281 return minDist;
1282}
1283
1284G4double G4OCCTSolid::DistanceToIn(const G4ThreeVector& p) const {
1285 // Tier-0: AABB lower bound (O(1)). If the point is clearly outside the shape's
1286 // axis-aligned bounding box, the AABB distance is a guaranteed conservative lower
1287 // bound on the true surface distance and avoids any further OCCT computation.
1288 const G4double aabbDist = AABBLowerBound(p);
1289 if (aabbDist > IntersectionTolerance()) {
1290 return aabbDist;
1291 }
1292
1293 // Tier-1: BVH triangle-mesh lower bound — O(log N_triangles).
1294 // For outside points this is a valid conservative lower bound (as required for
1295 // safety distances). Avoids the expensive per-face BRepExtrema_DistShapeShape
1296 // calls that dominate ExactDistanceToIn for curved surfaces (e.g. ellipsoids).
1297 const G4double bvhDist = BVHLowerBoundDistance(p);
1298 if (bvhDist < kInfinity && bvhDist > IntersectionTolerance()) {
1299 return bvhDist;
1300 }
1301
1302 // Fallback: exact distance (handles points near or inside the surface, including
1303 // interior/surface classification).
1304 return ExactDistanceToIn(p);
1305}
1306
1307G4double G4OCCTSolid::DistanceToOut(const G4ThreeVector& p, const G4ThreeVector& v,
1308 const G4bool calcNorm, G4bool* validNorm,
1309 G4ThreeVector* n) const {
1310 if (validNorm != nullptr) {
1311 *validNorm = false;
1312 }
1313
1314 const G4double tolerance = IntersectionTolerance();
1315 const gp_Lin ray(ToPoint(p), gp_Dir(v.x(), v.y(), v.z()));
1316
1317 IntersectorCache& cache = GetOrCreateIntersector();
1318
1319 G4double minDistance = kInfinity;
1320 std::size_t minFaceIdx = std::numeric_limits<std::size_t>::max();
1321 G4double minU = 0.0;
1322 G4double minV = 0.0;
1323 bool minIsIn = false;
1324 bool minIsFastPath = false;
1325
1326 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
1327 if (cache.expandedBoxes[i].IsOut(ray)) {
1328 continue;
1329 }
1330 const FaceBounds& fb = fFaceBoundsCache[i];
1331 if (!fb.uvPolygon.empty()) {
1332 // Fast path: direct ray–plane intersection for straight-edge planar faces.
1333 const auto t = RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, tolerance, Precision::Infinite(),
1334 tolerance);
1335 if (t && *t < minDistance) {
1336 minDistance = static_cast<G4double>(*t);
1337 minFaceIdx = i;
1338 minIsIn = true; // mirrors OCCT path: TopAbs_IN and TopAbs_ON both trigger normal
1339 minIsFastPath = true;
1340 }
1341 } else {
1342 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
1343 fi.Perform(ray, tolerance, Precision::Infinite());
1344 if (!fi.IsDone()) {
1345 continue;
1346 }
1347 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
1348 const G4double w = fi.WParameter(j);
1349 if (w > tolerance && w < minDistance) {
1350 minDistance = w;
1351 minFaceIdx = i;
1352 minU = fi.UParameter(j);
1353 minV = fi.VParameter(j);
1354 minIsIn = (fi.State(j) == TopAbs_IN || fi.State(j) == TopAbs_ON);
1355 minIsFastPath = false;
1356 }
1357 }
1358 }
1359 }
1360
1361 if (minFaceIdx == std::numeric_limits<std::size_t>::max() || minDistance == kInfinity) {
1362 return 0.0;
1363 }
1364
1365 if (calcNorm && validNorm != nullptr && n != nullptr && minIsIn) {
1366 const FaceBounds& fb = fFaceBoundsCache[minFaceIdx];
1367 if (minIsFastPath && fb.outwardNormal.has_value()) {
1368 // Fast path: use the precomputed constant outward normal for planar faces.
1369 *n = *fb.outwardNormal;
1370 *validNorm = true;
1371 } else {
1372 const auto outNorm = TryGetOutwardNormal(fb.adaptor, fb.face, minU, minV);
1373 if (outNorm) {
1374 *n = *outNorm;
1375 *validNorm = true;
1376 }
1377 }
1378 }
1379
1380 return minDistance;
1381}
1382
1383G4double G4OCCTSolid::ExactDistanceToOut(const G4ThreeVector& p) const {
1384 // Use the pre-built per-face AABB cache to prune candidates before calling
1385 // BRepExtrema on individual faces: each query performs O(N_faces) cheap AABB
1386 // checks, but only O(k) faces require expensive extrema evaluations.
1387 const auto match = TryFindClosestFace(fFaceBoundsCache, p);
1388 if (!match.has_value()) {
1389 return 0.0;
1390 }
1391 return (match->distance <= IntersectionTolerance()) ? 0.0 : match->distance;
1392}
1393
1394G4double G4OCCTSolid::DistanceToOut(const G4ThreeVector& p) const {
1395 G4double d;
1396 if (fAllFacesPlanar) {
1397 // All faces are planar: the minimum perpendicular plane distance is exact
1398 // for convex solids and a valid conservative lower bound for all others.
1399 // This avoids the BVH triangle-mesh traversal entirely.
1400 d = PlanarFaceLowerBoundDistance(p);
1401 if (d == kInfinity) {
1402 d = ExactDistanceToOut(p);
1403 }
1404 } else {
1405 // Mixed geometry (some curved faces): the planar lower bound does not cover
1406 // curved faces and is not a valid bound on the true distance. Fall back to
1407 // the BVH lower bound which covers all triangulated faces.
1408 const G4double bvhDist = BVHLowerBoundDistance(p);
1409 d = (bvhDist < kInfinity) ? bvhDist : ExactDistanceToOut(p);
1410 }
1411 // Feed the inscribed-sphere cache: every positive return value proves B(p,d)
1412 // is inside the solid and can accelerate future Inside(p) calls.
1413 TryInsertSphere(p, d);
1414 return d;
1415}
1416
1418 std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
1419 if (!fCachedVolume) {
1420 GProp_GProps props;
1421 BRepGProp::VolumeProperties(fShape, props);
1422 fCachedVolume = props.Mass();
1423 }
1424 return *fCachedVolume;
1425}
1426
1428 std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
1429 if (!fCachedSurfaceArea) {
1430 GProp_GProps props;
1431 BRepGProp::SurfaceProperties(fShape, props);
1432 fCachedSurfaceArea = props.Mass();
1433 }
1434 return *fCachedSurfaceArea;
1435}
1436
1437const G4OCCTSolid::SurfaceSamplingCache& G4OCCTSolid::GetOrBuildSurfaceCache() const {
1438 // Tessellate first, outside the lock: BRepMesh_IncrementalMesh is idempotent
1439 // and calling it from multiple threads simultaneously is safe (extra calls
1440 // after the first are no-ops).
1441 BRepMesh_IncrementalMesh mesher(fShape, kRelativeDeflection, /*isRelative=*/Standard_True);
1442 (void)mesher;
1443
1444 std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
1445
1446 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
1447 if (fSurfaceCache.has_value() && fSurfaceCacheGeneration == currentGen) {
1448 return *fSurfaceCache;
1449 }
1450
1451 // Build the cache while holding the lock. Collecting triangle vertices and
1452 // computing areas is fast (just reading the already-computed triangulation)
1453 // so blocking other threads briefly is acceptable.
1454 SurfaceSamplingCache cache;
1455
1456 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
1457 const TopoDS_Face& face = TopoDS::Face(ex.Current());
1458 TopLoc_Location loc;
1459 const Handle(Poly_Triangulation) & triangulation = BRep_Tool::Triangulation(face, loc);
1460 if (triangulation.IsNull()) {
1461 continue;
1462 }
1463
1464 // Register this face once in the deduplicated faces array.
1465 const auto faceIndex = static_cast<std::uint32_t>(cache.faces.size());
1466 cache.faces.push_back(face);
1467
1468 const gp_Trsf& transform = loc.Transformation();
1469 const bool reverseWinding = face.Orientation() == TopAbs_REVERSED;
1470
1471 for (Standard_Integer i = 1; i <= triangulation->NbTriangles(); ++i) {
1472 Standard_Integer idx1 = 0;
1473 Standard_Integer idx2 = 0;
1474 Standard_Integer idx3 = 0;
1475 triangulation->Triangle(i).Get(idx1, idx2, idx3);
1476 if (reverseWinding) {
1477 std::swap(idx2, idx3);
1478 }
1479
1480 const gp_Pnt q1 = triangulation->Node(idx1).Transformed(transform);
1481 const gp_Pnt q2 = triangulation->Node(idx2).Transformed(transform);
1482 const gp_Pnt q3 = triangulation->Node(idx3).Transformed(transform);
1483
1484 const G4ThreeVector v1(q1.X(), q1.Y(), q1.Z());
1485 const G4ThreeVector v2(q2.X(), q2.Y(), q2.Z());
1486 const G4ThreeVector v3(q3.X(), q3.Y(), q3.Z());
1487
1488 const G4double area = 0.5 * (v2 - v1).cross(v3 - v1).mag();
1489 if (area > 0.0) {
1490 cache.totalArea += area;
1491 cache.cumulativeAreas.push_back(cache.totalArea);
1492 cache.triangles.push_back({v1, v2, v3, faceIndex});
1493 }
1494 }
1495 }
1496
1497 fSurfaceCache = std::move(cache);
1498 fSurfaceCacheGeneration = currentGen;
1499 return *fSurfaceCache;
1500}
1501
1502G4ThreeVector G4OCCTSolid::GetPointOnSurface() const {
1503 const SurfaceSamplingCache& cache = GetOrBuildSurfaceCache();
1504
1505 if (cache.triangles.empty() || cache.totalArea == 0.0) {
1506 G4ExceptionDescription msg;
1507 msg << "Tessellation of solid \"" << GetName()
1508 << "\" produced no valid triangles; cannot sample a point on the surface.";
1509 G4Exception("G4OCCTSolid::GetPointOnSurface", "GeomMgt1001", FatalException, msg);
1510 return {0.0, 0.0, 0.0}; // unreachable; silences compiler warning
1511 }
1512
1513 // Select a triangle with probability proportional to its area using a
1514 // binary search on the cumulative-area array.
1515 const G4double target = G4UniformRand() * cache.totalArea;
1516 const auto it = std::ranges::lower_bound(cache.cumulativeAreas, target);
1517 const std::size_t idx = std::min(static_cast<std::size_t>(it - cache.cumulativeAreas.begin()),
1518 cache.triangles.size() - 1);
1519 const SurfaceTriangle& chosen = cache.triangles[idx];
1520
1521 // Sample uniformly within the chosen triangle using the standard
1522 // barycentric-coordinate technique: fold the unit square into a triangle
1523 // by reflecting points with r1+r2 > 1.
1524 G4double r1 = G4UniformRand();
1525 G4double r2 = G4UniformRand();
1526 if (r1 + r2 > 1.0) {
1527 r1 = 1.0 - r1;
1528 r2 = 1.0 - r2;
1529 }
1530
1531 const G4ThreeVector tessPoint =
1532 chosen.p1 + r1 * (chosen.p2 - chosen.p1) + r2 * (chosen.p3 - chosen.p1);
1533
1534 // For curved faces the tessellation triangle is a planar chord that lies
1535 // inside the solid. Project the sampled point back to the nearest point on
1536 // the analytical face surface so that the returned point truly lies on the
1537 // boundary and passes the G4PVPlacement::CheckOverlaps() surface test.
1538 const TopoDS_Face& face = cache.faces[chosen.faceIndex];
1539 TopLoc_Location loc;
1540 const Handle(Geom_Surface) geomSurface = BRep_Tool::Surface(face, loc);
1541 if (!geomSurface.IsNull()) {
1542 gp_Pnt tessPointLocal(tessPoint.x(), tessPoint.y(), tessPoint.z());
1543 if (!loc.IsIdentity()) {
1544 tessPointLocal.Transform(loc.Transformation().Inverted());
1545 }
1546 GeomAPI_ProjectPointOnSurf projection(tessPointLocal, geomSurface);
1547 if (projection.NbPoints() > 0) {
1548 gp_Pnt projectedPoint = projection.NearestPoint();
1549 if (!loc.IsIdentity()) {
1550 projectedPoint.Transform(loc.Transformation());
1551 }
1552 return {projectedPoint.X(), projectedPoint.Y(), projectedPoint.Z()};
1553 }
1554 }
1555
1556 return tessPoint;
1557}
1558
1559G4GeometryType G4OCCTSolid::GetEntityType() const { return "G4OCCTSolid"; }
1560
1561G4VisExtent G4OCCTSolid::GetExtent() const {
1562 return {fCachedBounds.min.x(), fCachedBounds.max.x(), fCachedBounds.min.y(),
1563 fCachedBounds.max.y(), fCachedBounds.min.z(), fCachedBounds.max.z()};
1564}
1565
1566void G4OCCTSolid::BoundingLimits(G4ThreeVector& pMin, G4ThreeVector& pMax) const {
1567 pMin = fCachedBounds.min;
1568 pMax = fCachedBounds.max;
1569}
1570
1571G4bool G4OCCTSolid::CalculateExtent(const EAxis pAxis, const G4VoxelLimits& pVoxelLimit,
1572 const G4AffineTransform& pTransform, G4double& pMin,
1573 G4double& pMax) const {
1574 const G4BoundingEnvelope envelope(fCachedBounds.min, fCachedBounds.max);
1575 return envelope.CalculateExtent(pAxis, pVoxelLimit, G4Transform3D(pTransform), pMin, pMax);
1576}
1577
1578void G4OCCTSolid::DescribeYourselfTo(G4VGraphicsScene& scene) const { scene.AddSolid(*this); }
1579
1580G4Polyhedron* G4OCCTSolid::CreatePolyhedron() const {
1581 const auto currentGeneration = fShapeGeneration.load(std::memory_order_acquire);
1582
1583 {
1584 std::unique_lock<std::mutex> lock(fPolyhedronMutex);
1585
1586 // Wait until no other thread is mid-build for a *different* generation, or
1587 // until the cache is already populated for our generation (fast path).
1588 fPolyhedronCV.wait(lock, [this, currentGeneration] {
1589 return !fPolyhedronBuilding || fPolyhedronGeneration == currentGeneration;
1590 });
1591
1592 // Cache hit: return an independent copy to the caller.
1593 if (fCachedPolyhedron && fPolyhedronGeneration == currentGeneration) {
1594 return new G4Polyhedron(*fCachedPolyhedron);
1595 }
1596
1597 // Claim the build slot so other concurrent callers wait for this thread.
1598 fPolyhedronBuilding = true;
1599 }
1600
1601 // ── Tessellation outside the lock ─────────────────────────────────────────
1602 // Use a relative deflection (1 % of each face's bounding-box size) so that
1603 // the mesh density scales with the shape rather than being a fixed world-
1604 // space length that is inappropriate for both very small and very large
1605 // shapes.
1606 BRepMesh_IncrementalMesh mesher(fShape, kRelativeDeflection, /*isRelative=*/Standard_True);
1607 (void)mesher;
1608
1609 G4TessellatedSolid tessellatedSolid(GetName() + "_polyhedron");
1610 G4int facetCount = 0;
1611
1612 for (TopExp_Explorer explorer(fShape, TopAbs_FACE); explorer.More(); explorer.Next()) {
1613 const TopoDS_Face& face = TopoDS::Face(explorer.Current());
1614 TopLoc_Location location;
1615 const Handle(Poly_Triangulation) & triangulation = BRep_Tool::Triangulation(face, location);
1616 if (triangulation.IsNull()) {
1617 continue;
1618 }
1619
1620 const gp_Trsf& transform = location.Transformation();
1621 const bool reverseWinding = face.Orientation() == TopAbs_REVERSED;
1622
1623 for (Standard_Integer triangleIndex = 1; triangleIndex <= triangulation->NbTriangles();
1624 ++triangleIndex) {
1625 Standard_Integer index1 = 0;
1626 Standard_Integer index2 = 0;
1627 Standard_Integer index3 = 0;
1628 triangulation->Triangle(triangleIndex).Get(index1, index2, index3);
1629
1630 if (reverseWinding) {
1631 std::swap(index2, index3);
1632 }
1633
1634 const gp_Pnt point1 = triangulation->Node(index1).Transformed(transform);
1635 const gp_Pnt point2 = triangulation->Node(index2).Transformed(transform);
1636 const gp_Pnt point3 = triangulation->Node(index3).Transformed(transform);
1637
1638 auto* facet =
1639 new G4TriangularFacet(G4ThreeVector(point1.X(), point1.Y(), point1.Z()),
1640 G4ThreeVector(point2.X(), point2.Y(), point2.Z()),
1641 G4ThreeVector(point3.X(), point3.Y(), point3.Z()), ABSOLUTE);
1642 if (!facet->IsDefined() || !tessellatedSolid.AddFacet(facet)) {
1643 delete facet;
1644 continue;
1645 }
1646
1647 ++facetCount;
1648 }
1649 }
1650
1651 // Finalise the tessellated solid and build a polyhedron from it (still
1652 // outside the lock — both objects are local to this thread).
1653 std::unique_ptr<G4Polyhedron> freshPolyhedron;
1654 if (facetCount > 0) {
1655 tessellatedSolid.SetSolidClosed(true);
1656 G4Polyhedron* tmp = tessellatedSolid.GetPolyhedron();
1657 if (tmp != nullptr) {
1658 freshPolyhedron = std::make_unique<G4Polyhedron>(*tmp);
1659 }
1660 }
1661
1662 // ── Write cache and wake waiters ──────────────────────────────────────────
1663 {
1664 std::unique_lock<std::mutex> lock(fPolyhedronMutex);
1665
1666 // Only persist the result if the shape has not been replaced while
1667 // tessellation was running. Re-reading the generation under the mutex
1668 // ensures the check and the write are atomic with respect to SetOCCTShape().
1669 bool cacheWritten = false;
1670 if (freshPolyhedron && fShapeGeneration.load(std::memory_order_acquire) == currentGeneration) {
1671 fCachedPolyhedron = std::make_unique<G4Polyhedron>(*freshPolyhedron);
1672 fPolyhedronGeneration = currentGeneration;
1673 cacheWritten = true;
1674 }
1675
1676 // Clear the build slot *after* the cache write so that threads woken by
1677 // notify_all() see the complete, consistent cache state.
1678 fPolyhedronBuilding = false;
1679 fPolyhedronCV.notify_all();
1680
1681 // Return a copy of the freshly-cached polyhedron when possible; fall back
1682 // to the locally-built one if the shape was replaced mid-tessellation.
1683 if (cacheWritten) {
1684 return new G4Polyhedron(*fCachedPolyhedron);
1685 }
1686 }
1687
1688 return freshPolyhedron ? new G4Polyhedron(*freshPolyhedron) : nullptr;
1689}
1690
1691std::ostream& G4OCCTSolid::StreamInfo(std::ostream& os) const {
1692 os << "-----------------------------------------------------------\n"
1693 << " *** Dump for solid - " << GetName() << " ***\n"
1694 << " ===================================================\n"
1695 << " Solid type: G4OCCTSolid\n"
1696 << " OCCT shape type: " << fShape.ShapeType() << "\n"
1697 << "-----------------------------------------------------------\n";
1698 return os;
1699}
Declaration of G4OCCTSolid.
Geant4 solid wrapping an Open CASCADE Technology (OCCT) TopoDS_Shape.
G4double ExactDistanceToOut(const G4ThreeVector &p) const
EInside Inside(const G4ThreeVector &p) const override
Return kInside, kSurface, or kOutside for point p.
G4double GetCubicVolume() override
G4double DistanceToOut(const G4ThreeVector &p, const G4ThreeVector &v, const G4bool calcNorm=false, G4bool *validNorm=nullptr, G4ThreeVector *n=nullptr) const override
G4Polyhedron * CreatePolyhedron() const override
Create a polyhedron representation for visualisation.
G4double DistanceToIn(const G4ThreeVector &p, const G4ThreeVector &v) const override
void BoundingLimits(G4ThreeVector &pMin, G4ThreeVector &pMax) const override
Return the axis-aligned bounding box limits.
G4OCCTSolid(const G4String &name, const TopoDS_Shape &shape)
G4ThreeVector SurfaceNormal(const G4ThreeVector &p) const override
Return the outward unit normal at surface point p.
G4bool CalculateExtent(const EAxis pAxis, const G4VoxelLimits &pVoxelLimit, const G4AffineTransform &pTransform, G4double &pMin, G4double &pMax) const override
Calculate the extent of the solid in the given axis.
~G4OCCTSolid() override
G4double GetSurfaceArea() override
G4GeometryType GetEntityType() const override
Return a string identifying the entity type.
G4ThreeVector GetPointOnSurface() const override
G4VisExtent GetExtent() const override
Return the axis-aligned bounding box extent.
std::ostream & StreamInfo(std::ostream &os) const override
Stream a human-readable description.
static G4OCCTSolid * FromSTEP(const G4String &name, const std::string &path)
void DescribeYourselfTo(G4VGraphicsScene &scene) const override
Describe the solid to the graphics scene.
G4double ExactDistanceToIn(const G4ThreeVector &p) const