G4OCCT 0.1.0
Geant4 interface to Open CASCADE Technology (OCCT) geometry definitions
Loading...
Searching...
No Matches
G4OCCTSolidKernel.cc
Go to the documentation of this file.
1// SPDX-License-Identifier: LGPL-2.1-or-later
2// Copyright (C) 2026 G4OCCT Contributors
3
5
6#include <BRepAdaptor_Curve.hxx>
7#include <BRepBndLib.hxx>
8#include <BRepExtrema_DistShapeShape.hxx>
9#include <BRepExtrema_SupportType.hxx>
10#include <BRepGProp.hxx>
11#include <BRepLib.hxx>
12#include <BRepLProp_SLProps.hxx>
13#include <BRepMesh_IncrementalMesh.hxx>
14#include <BRepTools.hxx>
15#include <BRepTools_WireExplorer.hxx>
16#include <BRep_Builder.hxx>
17#include <BRep_Tool.hxx>
18#include <BVH_Distance.hxx>
19#include <BVH_Tools.hxx>
20#include <ElSLib.hxx>
21#include <GProp_GProps.hxx>
22#include <GeomAPI_ProjectPointOnSurf.hxx>
23#include <GeomAbs_CurveType.hxx>
24#include <GeomAbs_SurfaceType.hxx>
25#include <Geom_Surface.hxx>
26#include <NCollection_Vector.hxx>
27#include <Poly_Triangulation.hxx>
28#include <Precision.hxx>
29#include <TopAbs_Orientation.hxx>
30#include <TopAbs_ShapeEnum.hxx>
31#include <TopAbs_State.hxx>
32#include <TopExp_Explorer.hxx>
33#include <TopLoc_Location.hxx>
34#include <TopoDS.hxx>
35#include <TopoDS_Edge.hxx>
36#include <TopoDS_Vertex.hxx>
37#include <TopoDS_Wire.hxx>
38#include <gp_Dir.hxx>
39#include <gp_Lin.hxx>
40#include <gp_Pln.hxx>
41#include <gp_Pnt.hxx>
42#include <gp_Pnt2d.hxx>
43#include <gp_Vec.hxx>
44
45#include <G4Exception.hh>
46#include <G4GeometryTolerance.hh>
47#include <Randomize.hh>
48
49#include <algorithm>
50#include <cmath>
51#include <optional>
52#include <ranges>
53#include <stdexcept>
54
55namespace {
56
57class PointToMeshDistance
58 : public BVH_Distance<Standard_Real, 3, BVH_Vec3d, BRepExtrema_TriangleSet> {
59public:
60 Standard_Boolean RejectNode(const BVH_Vec3d& theCornerMin, const BVH_Vec3d& theCornerMax,
61 Standard_Real& theMetric) const override {
62 theMetric =
63 BVH_Tools<Standard_Real, 3>::PointBoxSquareDistance(myObject, theCornerMin, theCornerMax);
64 return RejectMetric(theMetric);
65 }
66
67 Standard_Boolean Accept(const Standard_Integer theIndex, const Standard_Real&) override {
68 BVH_Vec3d v0, v1, v2;
69 myBVHSet->GetVertices(theIndex, v0, v1, v2);
70 const Standard_Real sq =
71 BVH_Tools<Standard_Real, 3>::PointTriangleSquareDistance(myObject, v0, v1, v2);
72 if (sq < myDistance) {
73 myDistance = sq;
74 myBestIndex = theIndex;
75 return Standard_True;
76 }
77 return Standard_False;
78 }
79
80 Standard_Integer BestIndex() const { return myBestIndex; }
81
82private:
83 Standard_Integer myBestIndex{-1};
84};
85
86class TriangleRayCast
87 : public BVH_Traverse<Standard_Real, 3, BRepExtrema_TriangleSet, Standard_Real> {
88public:
89 void SetRay(const BVH_Vec3d& theOrigin, const BVH_Vec3d& theDir, Standard_Real theTolerance) {
90 myOrigin = theOrigin;
91 myDir = theDir;
92 myTolerance = theTolerance;
93 myCrossings = 0;
94 myOnSurface = Standard_False;
95 myDegenerate = Standard_False;
96 }
97
98 Standard_Boolean RejectNode(const BVH_Vec3d& theCornerMin, const BVH_Vec3d& theCornerMax,
99 Standard_Real& theMetric) const override {
100 Standard_Real tmin = 0.0;
101 Standard_Real tmax = Precision::Infinite();
102 for (int k = 0; k < 3; ++k) {
103 const Standard_Real dk = (k == 0) ? myDir.x() : (k == 1) ? myDir.y() : myDir.z();
104 const Standard_Real ok = (k == 0) ? myOrigin.x() : (k == 1) ? myOrigin.y() : myOrigin.z();
105 const Standard_Real ck_min = (k == 0) ? theCornerMin.x()
106 : (k == 1) ? theCornerMin.y()
107 : theCornerMin.z();
108 const Standard_Real ck_max = (k == 0) ? theCornerMax.x()
109 : (k == 1) ? theCornerMax.y()
110 : theCornerMax.z();
111 if (std::abs(dk) < Precision::Confusion()) {
112 if (ok < ck_min - myTolerance || ok > ck_max + myTolerance) {
113 return Standard_True;
114 }
115 } else {
116 const Standard_Real t1 = (ck_min - ok) / dk;
117 const Standard_Real t2 = (ck_max - ok) / dk;
118 if (t1 > tmin) {
119 tmin = t1;
120 }
121 if (t2 < tmax) {
122 tmax = t2;
123 }
124 if (tmin > tmax + myTolerance) {
125 return Standard_True;
126 }
127 }
128 }
129 theMetric = tmin;
130 return Standard_False;
131 }
132
133 Standard_Boolean Accept(const Standard_Integer theIndex, const Standard_Real&) override {
134 BVH_Vec3d v0, v1, v2;
135 myBVHSet->GetVertices(theIndex, v0, v1, v2);
136 const BVH_Vec3d edge1 = v1 - v0;
137 const BVH_Vec3d edge2 = v2 - v0;
138 const BVH_Vec3d h = BVH_Vec3d::Cross(myDir, edge2);
139 const Standard_Real a = edge1.Dot(h);
140 if (std::abs(a) < 1e-12) {
141 return Standard_False;
142 }
143 const Standard_Real f = 1.0 / a;
144 const BVH_Vec3d s = myOrigin - v0;
145 const Standard_Real u = f * s.Dot(h);
146 if (u < 0.0 || u > 1.0) {
147 return Standard_False;
148 }
149 const BVH_Vec3d q = BVH_Vec3d::Cross(s, edge1);
150 const Standard_Real v = f * myDir.Dot(q);
151 if (v < 0.0 || u + v > 1.0) {
152 return Standard_False;
153 }
154 const Standard_Real t = f * edge2.Dot(q);
155 if (std::abs(t) <= myTolerance) {
156 myOnSurface = Standard_True;
157 return Standard_True;
158 }
159 if (t < -myTolerance) {
160 return Standard_False;
161 }
162 ++myCrossings;
163 constexpr Standard_Real kEdgeTol = 1e-6;
164 if (u < kEdgeTol || v < kEdgeTol || (1.0 - u - v) < kEdgeTol) {
165 myDegenerate = Standard_True;
166 }
167 return Standard_True;
168 }
169
170 Standard_Boolean RejectMetric(const Standard_Real&) const override { return Standard_False; }
171 Standard_Boolean Stop() const override { return Standard_False; }
172
173 int Crossings() const { return myCrossings; }
174 Standard_Boolean OnSurface() const { return myOnSurface; }
175 Standard_Boolean Degenerate() const { return myDegenerate; }
176
177private:
178 BVH_Vec3d myOrigin;
179 BVH_Vec3d myDir;
180 Standard_Real myTolerance{1e-7};
181 int myCrossings{0};
182 Standard_Boolean myOnSurface{Standard_False};
183 Standard_Boolean myDegenerate{Standard_False};
184};
185
186gp_Pnt ToPoint(const G4ThreeVector& point) { return gp_Pnt(point.x(), point.y(), point.z()); }
187
188G4double IntersectionTolerance() {
189 return 0.5 * G4GeometryTolerance::GetInstance()->GetSurfaceTolerance();
190}
191
192TopoDS_Vertex MakeVertex(const G4ThreeVector& point) {
193 BRep_Builder builder;
194 TopoDS_Vertex vertex;
195 builder.MakeVertex(vertex, ToPoint(point), IntersectionTolerance());
196 return vertex;
197}
198
200ToPointClassification(const TopAbs_State state) {
202 switch (state) {
203 case TopAbs_IN:
204 return PointClassification::kInside;
205 case TopAbs_ON:
206 return PointClassification::kSurface;
207 case TopAbs_OUT:
208 default:
209 return PointClassification::kOutside;
210 }
211}
212
213G4ThreeVector FallbackNormal() { return G4ThreeVector(0.0, 0.0, 1.0); }
214
215bool PointInPolygon2d(Standard_Real u, Standard_Real v, const std::vector<gp_Pnt2d>& poly) {
216 const std::size_t n = poly.size();
217 int crossings = 0;
218 for (std::size_t i = 0; i < n; ++i) {
219 const gp_Pnt2d& a = poly[i];
220 const gp_Pnt2d& b = poly[(i + 1) % n];
221 const Standard_Real av = a.Y();
222 const Standard_Real bv = b.Y();
223 if ((av <= v && bv > v) || (bv <= v && av > v)) {
224 const Standard_Real uCross = a.X() + (v - av) * (b.X() - a.X()) / (bv - av);
225 if (u < uCross) {
226 ++crossings;
227 }
228 }
229 }
230 return (crossings % 2) == 1;
231}
232
233bool PointOnPolygonBoundary2d(Standard_Real u, Standard_Real v, const std::vector<gp_Pnt2d>& poly,
234 Standard_Real tol) {
235 const Standard_Real tol2 = tol * tol;
236 const std::size_t n = poly.size();
237 for (std::size_t i = 0; i < n; ++i) {
238 const gp_Pnt2d& a = poly[i];
239 const gp_Pnt2d& b = poly[(i + 1) % n];
240 const Standard_Real dx = b.X() - a.X();
241 const Standard_Real dy = b.Y() - a.Y();
242 const Standard_Real len2 = dx * dx + dy * dy;
243 Standard_Real px = 0.0;
244 Standard_Real py = 0.0;
245 if (len2 < 1.0e-20) {
246 px = a.X();
247 py = a.Y();
248 } else {
249 const Standard_Real t_seg =
250 std::max(0.0, std::min(1.0, ((u - a.X()) * dx + (v - a.Y()) * dy) / len2));
251 px = a.X() + t_seg * dx;
252 py = a.Y() + t_seg * dy;
253 }
254 const Standard_Real dist2 = (u - px) * (u - px) + (v - py) * (v - py);
255 if (dist2 <= tol2) {
256 return true;
257 }
258 }
259 return false;
260}
261
262std::optional<Standard_Real> RayPlaneFaceHit(const gp_Lin& ray, const gp_Pln& plane,
263 const std::vector<gp_Pnt2d>& uvPoly,
264 Standard_Real tMin, Standard_Real tolerance,
265 Standard_Real* u_out = nullptr,
266 Standard_Real* v_out = nullptr) {
267 const gp_Dir& lineDir = ray.Direction();
268 const gp_Dir& plnNormal = plane.Axis().Direction();
269 const Standard_Real denom =
270 plnNormal.X() * lineDir.X() + plnNormal.Y() * lineDir.Y() + plnNormal.Z() * lineDir.Z();
271 if (std::abs(denom) < 1.0e-10) {
272 return std::nullopt;
273 }
274 const gp_Pnt& orig = ray.Location();
275 const gp_Pnt& planePt = plane.Location();
276 const Standard_Real numer = plnNormal.X() * (planePt.X() - orig.X()) +
277 plnNormal.Y() * (planePt.Y() - orig.Y()) +
278 plnNormal.Z() * (planePt.Z() - orig.Z());
279 const Standard_Real t = numer / denom;
280 if (t < tMin) {
281 return std::nullopt;
282 }
283 const gp_Pnt hitPt(orig.X() + t * lineDir.X(), orig.Y() + t * lineDir.Y(),
284 orig.Z() + t * lineDir.Z());
285 Standard_Real u = 0.0;
286 Standard_Real v = 0.0;
287 ElSLib::PlaneParameters(plane.Position(), hitPt, u, v);
288 if (!PointInPolygon2d(u, v, uvPoly) && !PointOnPolygonBoundary2d(u, v, uvPoly, tolerance)) {
289 return std::nullopt;
290 }
291 if (u_out != nullptr) {
292 *u_out = u;
293 }
294 if (v_out != nullptr) {
295 *v_out = v;
296 }
297 return t;
298}
299
300std::optional<G4ThreeVector> TryGetOutwardNormal(const BRepAdaptor_Surface& surface,
301 const TopoDS_Face& face, const Standard_Real u,
302 const Standard_Real v) {
303 Standard_Real adjustedU = u;
304 Standard_Real adjustedV = v;
305 const Standard_Real tolerance = IntersectionTolerance();
306 {
307 const Standard_Real uFirst = surface.FirstUParameter();
308 const Standard_Real uLast = surface.LastUParameter();
309 const Standard_Real uEpsilon = std::min(
310 std::max(surface.UResolution(tolerance), Precision::PConfusion()), 0.5 * (uLast - uFirst));
311 if (std::abs(adjustedU - uFirst) <= uEpsilon) {
312 adjustedU = std::min(uFirst + uEpsilon, uLast);
313 } else if (std::abs(adjustedU - uLast) <= uEpsilon) {
314 adjustedU = std::max(uLast - uEpsilon, uFirst);
315 }
316 }
317 {
318 const Standard_Real vFirst = surface.FirstVParameter();
319 const Standard_Real vLast = surface.LastVParameter();
320 const Standard_Real vEpsilon = std::min(
321 std::max(surface.VResolution(tolerance), Precision::PConfusion()), 0.5 * (vLast - vFirst));
322 if (std::abs(adjustedV - vFirst) <= vEpsilon) {
323 adjustedV = std::min(vFirst + vEpsilon, vLast);
324 } else if (std::abs(adjustedV - vLast) <= vEpsilon) {
325 adjustedV = std::max(vLast - vEpsilon, vFirst);
326 }
327 }
328
329 BRepLProp_SLProps props(surface, adjustedU, adjustedV, 1, tolerance);
330 if (!props.IsNormalDefined()) {
331 const Standard_Real vFirst = surface.FirstVParameter();
332 const Standard_Real vLast = surface.LastVParameter();
333 const Standard_Real vMid = 0.5 * (vFirst + vLast);
334 const bool nearVFirst = (adjustedV < vMid);
335 const Standard_Real vRes = std::max(surface.VResolution(tolerance), Precision::PConfusion());
336 Standard_Real finalRetryV = adjustedV;
337 for (int attempt = 0; attempt < 8 && !props.IsNormalDefined(); ++attempt) {
338 const Standard_Real scale = std::pow(10.0, static_cast<Standard_Real>(attempt));
339 const Standard_Real nudge = scale * vRes;
340 finalRetryV =
341 nearVFirst ? std::min(adjustedV + nudge, vMid) : std::max(adjustedV - nudge, vMid);
342 props = BRepLProp_SLProps(surface, adjustedU, finalRetryV, 1, tolerance);
343 }
344 if (!props.IsNormalDefined()) {
345 return std::nullopt;
346 }
347 constexpr Standard_Real kMaxRetryVDriftFraction = 0.10;
348 if (std::fabs(finalRetryV - adjustedV) > kMaxRetryVDriftFraction * (vLast - vFirst)) {
349 return std::nullopt;
350 }
351 }
352
353 gp_Dir faceNormal = props.Normal();
354 if (face.Orientation() == TopAbs_REVERSED) {
355 faceNormal.Reverse();
356 }
357
358 return G4ThreeVector(faceNormal.X(), faceNormal.Y(), faceNormal.Z());
359}
360
361} // namespace
362
363namespace g4occt::detail {
364
365G4OCCTSolidKernel::G4OCCTSolidKernel(const TopoDS_Shape& shape) : fShape(shape) {
366 if (fShape.IsNull()) {
367 throw std::invalid_argument("G4OCCTSolidKernel: shape must not be null");
368 }
369 ComputeBounds();
370}
371
372void G4OCCTSolidKernel::SetShape(const TopoDS_Shape& shape) {
373 if (shape.IsNull()) {
374 throw std::invalid_argument("G4OCCTSolidKernel::SetShape: shape must not be null");
375 }
376 fShape = shape;
377 ComputeBounds();
378 {
379 std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
380 fCachedVolume.reset();
381 fCachedSurfaceArea.reset();
382 }
383 {
384 std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
385 // Reset both the cache payload and its bookkeeping together so waiting
386 // threads never observe "no cache" with stale generation/build metadata.
387 fSurfaceCache.reset();
388 fSurfaceCacheGeneration = std::numeric_limits<std::uint64_t>::max();
389 fSurfaceCacheBuilding = false;
390 lock.unlock();
391 fSurfaceCacheCV.notify_all();
392 }
393 fShapeGeneration.fetch_add(1, std::memory_order_release);
394}
395
396void G4OCCTSolidKernel::ComputeBounds() {
397 // Build missing PCurves for planar faces up front so the later polygon fast
398 // paths can derive stable (u, v) polygons from the OCCT face wiring.
399 for (TopExp_Explorer faceEx(fShape, TopAbs_FACE); faceEx.More(); faceEx.Next()) {
400 const TopoDS_Face& face = TopoDS::Face(faceEx.Current());
401 if (BRepAdaptor_Surface(face).GetType() != GeomAbs_Plane) {
402 continue;
403 }
404 for (TopExp_Explorer edgeEx(face, TopAbs_EDGE); edgeEx.More(); edgeEx.Next()) {
405 BRepLib::BuildPCurveForEdgeOnPlane(TopoDS::Edge(edgeEx.Current()), face);
406 }
407 }
408
409 Bnd_Box boundingBox;
410 BRepBndLib::AddOptimal(fShape, boundingBox, /*useTriangulation=*/Standard_False);
411 if (boundingBox.IsVoid()) {
412 throw std::invalid_argument(
413 "G4OCCTSolidKernel: shape has no computable bounding box (no geometry)");
414 }
415
416 Standard_Real xMin = 0.0;
417 Standard_Real yMin = 0.0;
418 Standard_Real zMin = 0.0;
419 Standard_Real xMax = 0.0;
420 Standard_Real yMax = 0.0;
421 Standard_Real zMax = 0.0;
422 boundingBox.Get(xMin, yMin, zMin, xMax, yMax, zMax);
423 fCachedBounds =
424 AxisAlignedBounds{G4ThreeVector(xMin, yMin, zMin), G4ThreeVector(xMax, yMax, zMax)};
425
426 fFaceBoundsCache.clear();
427 G4double maxFaceDiag = 0.0;
428 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
429 Bnd_Box faceBox;
430 BRepBndLib::AddOptimal(ex.Current(), faceBox, /*useTriangulation=*/Standard_False);
431 const TopoDS_Face& currentFace = TopoDS::Face(ex.Current());
432 BRepAdaptor_Surface adaptor(currentFace);
433 std::optional<gp_Pln> maybePlane;
434 std::vector<gp_Pnt2d> uvPolygon;
435 std::optional<G4ThreeVector> outwardNormal;
436 if (adaptor.GetType() == GeomAbs_Plane) {
437 // For single-wire planar faces we cache a 2D polygon in face coordinates
438 // and the analytical outward normal. That lets Inside/Distance* use a
439 // cheap ray-plane + polygon test instead of the heavier OCCT intersector.
440 maybePlane = adaptor.Plane();
441 const gp_Ax3& pos = maybePlane->Position();
442 const TopoDS_Wire wire = BRepTools::OuterWire(currentFace);
443 if (!wire.IsNull()) {
444 bool allLinear = true;
445 std::vector<gp_Pnt2d> poly;
446 for (BRepTools_WireExplorer we(wire, currentFace); we.More(); we.Next()) {
447 const BRepAdaptor_Curve ec(we.Current());
448 if (ec.GetType() != GeomAbs_Line) {
449 allLinear = false;
450 break;
451 }
452 const gp_Pnt pt = BRep_Tool::Pnt(we.CurrentVertex());
453 Standard_Real u = 0.0;
454 Standard_Real v = 0.0;
455 ElSLib::PlaneParameters(pos, pt, u, v);
456 poly.emplace_back(u, v);
457 }
458 if (allLinear && poly.size() >= 3) {
459 int wireCount = 0;
460 for (TopExp_Explorer wc(currentFace, TopAbs_WIRE); wc.More(); wc.Next()) {
461 ++wireCount;
462 if (wireCount > 1) {
463 break;
464 }
465 }
466 if (wireCount == 1) {
467 uvPolygon = std::move(poly);
468 gp_Dir faceNormal = maybePlane->Axis().Direction();
469 if (currentFace.Orientation() == TopAbs_REVERSED) {
470 faceNormal.Reverse();
471 }
472 outwardNormal = G4ThreeVector(faceNormal.X(), faceNormal.Y(), faceNormal.Z());
473 }
474 }
475 }
476 }
477 fFaceBoundsCache.push_back({currentFace, faceBox, std::move(adaptor), std::move(maybePlane),
478 std::move(uvPolygon), std::move(outwardNormal)});
479 if (!faceBox.IsVoid()) {
480 Standard_Real fx0 = 0.0;
481 Standard_Real fy0 = 0.0;
482 Standard_Real fz0 = 0.0;
483 Standard_Real fx1 = 0.0;
484 Standard_Real fy1 = 0.0;
485 Standard_Real fz1 = 0.0;
486 faceBox.Get(fx0, fy0, fz0, fx1, fy1, fz1);
487 const G4double diag = G4ThreeVector(fx1 - fx0, fy1 - fy0, fz1 - fz0).mag();
488 maxFaceDiag = std::max(maxFaceDiag, diag);
489 }
490 }
491
492 fBVHDeflection = kOCCTRelativeDeflection * maxFaceDiag;
493 fAllFacesPlanar = std::all_of(fFaceBoundsCache.begin(), fFaceBoundsCache.end(),
494 [](const FaceBounds& fb) { return fb.plane.has_value(); });
495
496 {
497 [[maybe_unused]] const BRepMesh_IncrementalMesh mesher(fShape, kOCCTRelativeDeflection,
498 /*isRelative=*/Standard_True);
499 }
500
501 NCollection_Vector<TopoDS_Shape> faces;
502 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
503 faces.Append(ex.Current());
504 }
505 if (faces.IsEmpty()) {
506 fTriangleSet.Nullify();
507 fFaceDeflections.clear();
508 } else {
509 fTriangleSet = new BRepExtrema_TriangleSet(faces);
510 fFaceDeflections.clear();
511 fFaceDeflections.reserve(fFaceBoundsCache.size());
512 for (const FaceBounds& fb : fFaceBoundsCache) {
513 G4double deflection = fBVHDeflection;
514 if (!fb.box.IsVoid()) {
515 Standard_Real fx0 = 0.0, fy0 = 0.0, fz0 = 0.0;
516 Standard_Real fx1 = 0.0, fy1 = 0.0, fz1 = 0.0;
517 fb.box.Get(fx0, fy0, fz0, fx1, fy1, fz1);
518 const G4double faceDiag = G4ThreeVector(fx1 - fx0, fy1 - fy0, fz1 - fz0).mag();
519 deflection = kOCCTRelativeDeflection * faceDiag;
520 }
521 fFaceDeflections.push_back(deflection);
522 }
523 }
524
525 ComputeInitialSpheres();
526}
527
528void G4OCCTSolidKernel::ComputeInitialSpheres() {
529 fInitialSpheres.clear();
530
531 const G4double tol = IntersectionTolerance();
532 const G4ThreeVector& bmin = fCachedBounds.min;
533 const G4ThreeVector& bmax = fCachedBounds.max;
534 const G4ThreeVector centre = 0.5 * (bmin + bmax);
535 const G4ThreeVector halfExt = 0.5 * (bmax - bmin);
536
537 std::vector<G4ThreeVector> candidates;
538 candidates.reserve(15);
539 candidates.push_back(centre);
540 for (const G4double s : {-0.5, 0.5}) {
541 candidates.push_back(centre + G4ThreeVector(s * halfExt.x(), 0.0, 0.0));
542 candidates.push_back(centre + G4ThreeVector(0.0, s * halfExt.y(), 0.0));
543 candidates.push_back(centre + G4ThreeVector(0.0, 0.0, s * halfExt.z()));
544 }
545 for (const int sx : {-1, 1}) {
546 for (const int sy : {-1, 1}) {
547 for (const int sz : {-1, 1}) {
548 candidates.push_back(centre + G4ThreeVector(0.75 * sx * halfExt.x(),
549 0.75 * sy * halfExt.y(),
550 0.75 * sz * halfExt.z()));
551 }
552 }
553 }
554
555 BRepClass3d_SolidClassifier localClassifier;
556 localClassifier.Load(fShape);
557
558 // Seed the per-thread sphere caches from a small set of AABB-derived sample
559 // points so deep interior queries can often terminate without touching OCCT.
560 for (const G4ThreeVector& cand : candidates) {
561 localClassifier.Perform(ToPoint(cand), tol);
562 if (localClassifier.State() != TopAbs_IN) {
563 continue;
564 }
565 G4double d = BVHLowerBoundDistance(cand);
566 if (d >= G4OCCTSolidKernel::Infinity() || d <= tol) {
567 const auto match = TryFindClosestFace(fFaceBoundsCache, cand);
568 if (!match.has_value() || match->distance <= tol) {
569 continue;
570 }
571 d = match->distance;
572 }
573 fInitialSpheres.push_back({cand, d});
574 }
575 std::sort(fInitialSpheres.begin(), fInitialSpheres.end(),
576 [](const InscribedSphere& a, const InscribedSphere& b) { return a.radius > b.radius; });
577}
578
579std::optional<G4OCCTSolidKernel::ClosestFaceMatch>
580G4OCCTSolidKernel::TryFindClosestFace(const std::vector<FaceBounds>& faceBoundsCache,
581 const G4ThreeVector& point, G4double maxDistance) {
582 if (faceBoundsCache.empty()) {
583 return std::nullopt;
584 }
585
586 const gp_Pnt queryPoint = ToPoint(point);
587 const TopoDS_Vertex queryVertex = MakeVertex(point);
588 Bnd_Box queryBox;
589 queryBox.Add(queryPoint);
590
591 std::optional<ClosestFaceMatch> bestMatch;
592 for (std::size_t i = 0; i < faceBoundsCache.size(); ++i) {
593 const FaceBounds& fb = faceBoundsCache[i];
594 const G4double threshold = bestMatch.has_value() ? bestMatch->distance : maxDistance;
595 if (threshold < G4OCCTSolidKernel::Infinity() && fb.box.Distance(queryBox) > threshold) {
596 continue;
597 }
598
599 BRepExtrema_DistShapeShape distance(queryVertex, fb.face);
600 if (!distance.IsDone() || distance.NbSolution() == 0) {
601 continue;
602 }
603 const G4double candidateDistance = distance.Value();
604
605 if (bestMatch.has_value() && candidateDistance >= bestMatch->distance) {
606 continue;
607 }
608 ClosestFaceMatch match{.face = fb.face, .distance = candidateDistance, .faceIndex = i};
609 if (distance.NbSolution() > 0 && distance.SupportTypeShape2(1) == BRepExtrema_IsInFace) {
610 Standard_Real u = 0.0;
611 Standard_Real v = 0.0;
612 distance.ParOnFaceS2(1, u, v);
613 match.uv = std::make_pair(u, v);
614 }
615 bestMatch = std::move(match);
616 }
617
618 return bestMatch;
619}
620
621BRepClass3d_SolidClassifier&
622G4OCCTSolidKernel::GetOrCreateClassifier(ClassifierCache& cache) const {
623 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
624 if (cache.generation != currentGen) {
625 cache.classifier.emplace();
626 cache.classifier->Load(fShape);
627 cache.generation = currentGen;
628 }
629 return *cache.classifier;
630}
631
632G4OCCTSolidKernel::IntersectorCache&
633G4OCCTSolidKernel::GetOrCreateIntersector(IntersectorCache& cache) const {
634 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
635 if (cache.generation != currentGen) {
636 const G4double tol = IntersectionTolerance();
637 cache.faceIntersectors.clear();
638 cache.faceIntersectors.reserve(fFaceBoundsCache.size());
639 cache.expandedBoxes.clear();
640 cache.expandedBoxes.reserve(fFaceBoundsCache.size());
641 for (const auto& fb : fFaceBoundsCache) {
642 cache.faceIntersectors.push_back(std::make_unique<IntCurvesFace_Intersector>(fb.face, tol));
643 Bnd_Box expanded = fb.box;
644 expanded.Enlarge(tol);
645 cache.expandedBoxes.push_back(std::move(expanded));
646 }
647 cache.generation = currentGen;
648 }
649 return cache;
650}
651
652G4OCCTSolidKernel::SphereCacheData&
653G4OCCTSolidKernel::GetOrInitSphereCache(SphereCacheData& cache) const {
654 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
655 if (cache.generation != currentGen) {
656 cache.spheres = fInitialSpheres;
657 cache.generation = currentGen;
658 }
659 return cache;
660}
661
662void G4OCCTSolidKernel::TryInsertSphere(SphereCacheData& cache, const G4ThreeVector& centre,
663 G4double d) const {
664 if (d <= 0.0) {
665 return;
666 }
667 const G4double minRadius = IntersectionTolerance();
668 if (d <= minRadius) {
669 return;
670 }
671 GetOrInitSphereCache(cache);
672
673 if (cache.spheres.size() >= kMaxInscribedSpheres && d <= cache.spheres.back().radius) {
674 return;
675 }
676
677 for (const InscribedSphere& s : cache.spheres) {
678 if (s.radius >= d) {
679 const G4double gap = s.radius - d;
680 if ((centre - s.centre).mag2() <= gap * gap) {
681 return;
682 }
683 }
684 }
685
686 const InscribedSphere newSphere{centre, d};
687 const auto it = std::lower_bound(
688 cache.spheres.begin(), cache.spheres.end(), newSphere,
689 [](const InscribedSphere& a, const InscribedSphere& b) { return a.radius > b.radius; });
690 cache.spheres.insert(it, newSphere);
691 if (cache.spheres.size() > kMaxInscribedSpheres) {
692 cache.spheres.pop_back();
693 }
694}
695
697G4OCCTSolidKernel::ClassifyPoint(const G4ThreeVector& p, ClassifierCache& classifierCache,
698 IntersectorCache& intersectorCache,
699 SphereCacheData& sphereCache) const {
700 const G4double tolerance = IntersectionTolerance();
701 if (p.x() < fCachedBounds.min.x() - tolerance || p.x() > fCachedBounds.max.x() + tolerance ||
702 p.y() < fCachedBounds.min.y() - tolerance || p.y() > fCachedBounds.max.y() + tolerance ||
703 p.z() < fCachedBounds.min.z() - tolerance || p.z() > fCachedBounds.max.z() + tolerance) {
705 }
706
707 const SphereCacheData& localSphereCache = GetOrInitSphereCache(sphereCache);
708 for (const InscribedSphere& s : localSphereCache.spheres) {
709 const G4double interiorRadius = s.radius - tolerance;
710 if (interiorRadius > 0.0 && (p - s.centre).mag2() < interiorRadius * interiorRadius) {
712 }
713 }
714
715 if (!fTriangleSet.IsNull() && fTriangleSet->Size() > 0) {
716 // Near the tessellated surface we deliberately fall back to the exact OCCT
717 // classifier; the BVH lower bound is only used to prove that we are safely
718 // away from the true analytical boundary by more than the mesh deflection.
719 if (fBVHDeflection > 0.0) {
720 const G4double bvhLB = BVHLowerBoundDistance(p);
721 if (bvhLB < tolerance) {
722 auto& classifier = GetOrCreateClassifier(classifierCache);
723 classifier.Perform(ToPoint(p), tolerance);
724 return ToPointClassification(classifier.State());
725 }
726 }
727
728 const BVH_Vec3d bvhOrigin(p.x(), p.y(), p.z());
729 const Standard_Real bvhTol = static_cast<Standard_Real>(tolerance);
730 TriangleRayCast caster;
731 caster.SetBVHSet(fTriangleSet.get());
732 caster.SetRay(bvhOrigin, BVH_Vec3d(0.0, 0.0, 1.0), bvhTol);
733 caster.Select();
734
735 if (caster.OnSurface()) {
737 }
738 if (!caster.Degenerate() && caster.Crossings() > 0) {
739 return (caster.Crossings() % 2 == 1) ? PointClassification::kInside
741 }
742
743 // Edge/vertex hits can make one parity ray ambiguous, so we cast two
744 // additional orthogonal rays and let non-degenerate rays vote before
745 // paying for the exact classifier fallback.
746 int insideVotes = 0;
747 int outsideVotes = 0;
748 if (!caster.Degenerate()) {
749 if (caster.Crossings() % 2 == 1) {
750 ++insideVotes;
751 } else {
752 ++outsideVotes;
753 }
754 }
755
756 const BVH_Vec3d kExtraRays[2] = {
757 BVH_Vec3d(1.0, 0.0, 0.0),
758 BVH_Vec3d(0.0, 1.0, 0.0),
759 };
760 for (const BVH_Vec3d& dir : kExtraRays) {
761 caster.SetRay(bvhOrigin, dir, bvhTol);
762 caster.Select();
763 if (caster.OnSurface()) {
765 }
766 if (!caster.Degenerate()) {
767 if (caster.Crossings() % 2 == 1) {
768 ++insideVotes;
769 } else {
770 ++outsideVotes;
771 }
772 }
773 }
774
775 if (insideVotes > outsideVotes) {
777 }
778 if (outsideVotes > insideVotes) {
780 }
781
782 auto& classifier = GetOrCreateClassifier(classifierCache);
783 classifier.Perform(ToPoint(p), tolerance);
784 return ToPointClassification(classifier.State());
785 }
786
787 IntersectorCache& cache = GetOrCreateIntersector(intersectorCache);
788 const gp_Lin ray(ToPoint(p), gp_Dir(0.0, 0.0, 1.0));
789 int crossings = 0;
790 bool onSurface = false;
791 bool degenerateRay = false;
792
793 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
794 if (cache.expandedBoxes[i].IsOut(ray)) {
795 continue;
796 }
797 const FaceBounds& fb = fFaceBoundsCache[i];
798 if (!fb.uvPolygon.empty()) {
799 // Fast path for single-wire planar faces: intersect the analytical plane
800 // and then test the hit against the cached 2D polygon in face space.
801 Standard_Real u_hit = 0.0;
802 Standard_Real v_hit = 0.0;
803 const auto t =
804 RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, -tolerance, tolerance, &u_hit, &v_hit);
805 if (t) {
806 const G4double w = static_cast<G4double>(*t);
807 if (std::abs(w) <= tolerance) {
808 onSurface = true;
809 } else if (w > tolerance) {
810 if (PointOnPolygonBoundary2d(u_hit, v_hit, fb.uvPolygon, tolerance)) {
811 degenerateRay = true;
812 } else {
813 ++crossings;
814 }
815 }
816 }
817 } else {
818 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
819 fi.Perform(ray, -tolerance, Precision::Infinite());
820 if (!fi.IsDone()) {
821 continue;
822 }
823 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
824 const G4double w = fi.WParameter(j);
825 const TopAbs_State state = fi.State(j);
826 if (std::abs(w) <= tolerance && (state == TopAbs_IN || state == TopAbs_ON)) {
827 onSurface = true;
828 } else if (w > tolerance && state == TopAbs_IN) {
829 ++crossings;
830 } else if (w > tolerance && state == TopAbs_ON) {
831 degenerateRay = true;
832 }
833 }
834 }
835 }
836
837 if (onSurface) {
839 }
840 if (crossings == 0 || degenerateRay) {
841 auto& classifier = GetOrCreateClassifier(classifierCache);
842 classifier.Perform(ToPoint(p), tolerance);
843 return ToPointClassification(classifier.State());
844 }
845 return (crossings % 2 == 1) ? PointClassification::kInside : PointClassification::kOutside;
846}
847
848G4ThreeVector G4OCCTSolidKernel::SurfaceNormal(const G4ThreeVector& p) const {
849 if (fAllFacesPlanar) {
850 const gp_Pnt pt = ToPoint(p);
851 const FaceBounds* bestFB = nullptr;
852 G4double bestDist = G4OCCTSolidKernel::Infinity();
853 for (const FaceBounds& fb : fFaceBoundsCache) {
854 if (!fb.plane.has_value() || !fb.outwardNormal.has_value()) {
855 continue;
856 }
857 const G4double d = fb.plane->Distance(pt);
858 if (d < bestDist) {
859 bestDist = d;
860 bestFB = &fb;
861 }
862 }
863 if (bestFB) {
864 return *bestFB->outwardNormal;
865 }
866 }
867
868 const auto projectAndGetNormalFallback = [&](const FaceBounds& fb) -> G4ThreeVector {
869 TopLoc_Location loc;
870 const Handle(Geom_Surface) surface = BRep_Tool::Surface(fb.face, loc);
871 if (surface.IsNull()) {
872 return FallbackNormal();
873 }
874 gp_Pnt pLocal = ToPoint(p);
875 if (!loc.IsIdentity()) {
876 pLocal.Transform(loc.Transformation().Inverted());
877 }
878 GeomAPI_ProjectPointOnSurf projection(pLocal, surface);
879 if (projection.NbPoints() == 0) {
880 return FallbackNormal();
881 }
882 Standard_Real u = 0.0;
883 Standard_Real v = 0.0;
884 projection.LowerDistanceParameters(u, v);
885 return TryGetOutwardNormal(fb.adaptor, fb.face, u, v).value_or(FallbackNormal());
886 };
887
888 const G4double bvhLB = BVHLowerBoundDistance(p);
889 // The BVH gives a lower bound to the true surface distance. Adding
890 // 2×deflection turns that into a conservative face-search radius that still
891 // prunes obviously distant faces before exact OCCT distance checks.
892 const G4double seedDist = (fBVHDeflection > 0.0 && bvhLB < G4OCCTSolidKernel::Infinity())
893 ? bvhLB + 2.0 * fBVHDeflection
895 const auto closestFaceMatch = TryFindClosestFace(fFaceBoundsCache, p, seedDist);
896 if (!closestFaceMatch.has_value()) {
897 return FallbackNormal();
898 }
899 const FaceBounds& fb = fFaceBoundsCache[closestFaceMatch->faceIndex];
900
901 if (fb.outwardNormal.has_value()) {
902 return *fb.outwardNormal;
903 }
904 if (closestFaceMatch->uv.has_value()) {
905 const auto [u, v] = *closestFaceMatch->uv;
906 const auto result = TryGetOutwardNormal(fb.adaptor, fb.face, u, v);
907 if (result.has_value()) {
908 return *result;
909 }
910 }
911 return projectAndGetNormalFallback(fb);
912}
913
914G4double G4OCCTSolidKernel::DistanceToIn(const G4ThreeVector& p, const G4ThreeVector& v,
915 IntersectorCache& intersectorCache) const {
916 const G4double tolerance = IntersectionTolerance();
917 const gp_Lin ray(ToPoint(p), gp_Dir(v.x(), v.y(), v.z()));
918 IntersectorCache& cache = GetOrCreateIntersector(intersectorCache);
919
920 G4double minDistance = G4OCCTSolidKernel::Infinity();
921 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
922 if (cache.expandedBoxes[i].IsOut(ray)) {
923 continue;
924 }
925 const FaceBounds& fb = fFaceBoundsCache[i];
926 if (!fb.uvPolygon.empty()) {
927 const auto t = RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, tolerance, tolerance);
928 if (t && *t < minDistance) {
929 minDistance = static_cast<G4double>(*t);
930 }
931 } else {
932 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
933 fi.Perform(ray, tolerance, Precision::Infinite());
934 if (!fi.IsDone()) {
935 continue;
936 }
937 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
938 const G4double w = fi.WParameter(j);
939 if (w > tolerance && w < minDistance) {
940 minDistance = w;
941 }
942 }
943 }
944 }
945 return minDistance;
946}
947
948G4double G4OCCTSolidKernel::ExactDistanceToIn(const G4ThreeVector& p,
949 ClassifierCache& classifierCache) const {
950 auto& classifier = GetOrCreateClassifier(classifierCache);
951 classifier.Perform(ToPoint(p), IntersectionTolerance());
952 if (classifier.State() == TopAbs_IN || classifier.State() == TopAbs_ON) {
953 return 0.0;
954 }
955
956 const auto match = TryFindClosestFace(fFaceBoundsCache, p);
957 if (!match.has_value()) {
959 }
960 return (match->distance <= IntersectionTolerance()) ? 0.0 : match->distance;
961}
962
963G4double G4OCCTSolidKernel::AABBLowerBound(const G4ThreeVector& p) const {
964 const G4ThreeVector& mn = fCachedBounds.min;
965 const G4ThreeVector& mx = fCachedBounds.max;
966 const G4double dx = std::max({0.0, mn.x() - p.x(), p.x() - mx.x()});
967 const G4double dy = std::max({0.0, mn.y() - p.y(), p.y() - mx.y()});
968 const G4double dz = std::max({0.0, mn.z() - p.z(), p.z() - mx.z()});
969 return std::sqrt(dx * dx + dy * dy + dz * dz);
970}
971
972G4double G4OCCTSolidKernel::BVHLowerBoundDistance(const G4ThreeVector& p) const {
973 if (fTriangleSet.IsNull() || fTriangleSet->Size() == 0) {
975 }
976 PointToMeshDistance solver;
977 solver.SetObject(BVH_Vec3d(p.x(), p.y(), p.z()));
978 solver.SetBVHSet(fTriangleSet.get());
979 const Standard_Real meshDistSq = solver.ComputeDistance();
980 if (!solver.IsDone()) {
982 }
983 const G4double meshDist = std::sqrt(static_cast<G4double>(meshDistSq));
984
985 G4double deflection = fBVHDeflection;
986 const Standard_Integer bestIdx = solver.BestIndex();
987 if (bestIdx >= 0) {
988 const Standard_Integer faceId = fTriangleSet->GetFaceID(bestIdx);
989 if (faceId >= 0 && static_cast<std::size_t>(faceId) < fFaceDeflections.size()) {
990 deflection = fFaceDeflections[static_cast<std::size_t>(faceId)];
991 }
992 }
993
994 // The tessellated distance can overestimate how far we are from the
995 // analytical surface by up to the local mesh deflection, so subtract it and
996 // clamp at zero to preserve a conservative lower bound.
997 return std::max(0.0, meshDist - deflection);
998}
999
1000G4double G4OCCTSolidKernel::PlanarFaceLowerBoundDistance(const G4ThreeVector& p) const {
1001 const gp_Pnt pt = ToPoint(p);
1002 G4double minDist = G4OCCTSolidKernel::Infinity();
1003 for (const FaceBounds& fb : fFaceBoundsCache) {
1004 if (!fb.plane.has_value()) {
1005 continue;
1006 }
1007 const G4double d = static_cast<G4double>(fb.plane->Distance(pt));
1008 if (d < minDist) {
1009 minDist = d;
1010 }
1011 }
1012 return minDist;
1013}
1014
1015G4double G4OCCTSolidKernel::DistanceToIn(const G4ThreeVector& p,
1016 ClassifierCache& classifierCache) const {
1017 const G4double aabbDist = AABBLowerBound(p);
1018 if (aabbDist > IntersectionTolerance()) {
1019 return aabbDist;
1020 }
1021
1022 // Once the point is inside or very near the AABB, the BVH lower bound is the
1023 // next cheap filter. Only near-surface cases fall through to the exact OCCT
1024 // distance query.
1025 const G4double bvhDist = BVHLowerBoundDistance(p);
1026 if (bvhDist < G4OCCTSolidKernel::Infinity() && bvhDist > IntersectionTolerance()) {
1027 return bvhDist;
1028 }
1029 return ExactDistanceToIn(p, classifierCache);
1030}
1031
1032G4double G4OCCTSolidKernel::DistanceToOut(const G4ThreeVector& p, const G4ThreeVector& v,
1033 IntersectorCache& intersectorCache, const G4bool calcNorm,
1034 G4bool* validNorm, G4ThreeVector* n) const {
1035 if (validNorm != nullptr) {
1036 *validNorm = false;
1037 }
1038
1039 const G4double tolerance = IntersectionTolerance();
1040 const gp_Lin ray(ToPoint(p), gp_Dir(v.x(), v.y(), v.z()));
1041 IntersectorCache& cache = GetOrCreateIntersector(intersectorCache);
1042
1043 G4double minDistance = G4OCCTSolidKernel::Infinity();
1044 std::size_t minFaceIdx = std::numeric_limits<std::size_t>::max();
1045 G4double minU = 0.0;
1046 G4double minV = 0.0;
1047 bool minIsIn = false;
1048 bool minIsFastPath = false;
1049
1050 for (std::size_t i = 0; i < fFaceBoundsCache.size(); ++i) {
1051 if (cache.expandedBoxes[i].IsOut(ray)) {
1052 continue;
1053 }
1054 const FaceBounds& fb = fFaceBoundsCache[i];
1055 if (!fb.uvPolygon.empty()) {
1056 const auto t = RayPlaneFaceHit(ray, *fb.plane, fb.uvPolygon, tolerance, tolerance);
1057 if (t && *t < minDistance) {
1058 minDistance = static_cast<G4double>(*t);
1059 minFaceIdx = i;
1060 minIsIn = true;
1061 minIsFastPath = true;
1062 }
1063 } else {
1064 IntCurvesFace_Intersector& fi = *cache.faceIntersectors[i];
1065 fi.Perform(ray, tolerance, Precision::Infinite());
1066 if (!fi.IsDone()) {
1067 continue;
1068 }
1069 for (Standard_Integer j = 1; j <= fi.NbPnt(); ++j) {
1070 const G4double w = fi.WParameter(j);
1071 if (w > tolerance && w < minDistance) {
1072 minDistance = w;
1073 minFaceIdx = i;
1074 minU = fi.UParameter(j);
1075 minV = fi.VParameter(j);
1076 minIsIn = (fi.State(j) == TopAbs_IN || fi.State(j) == TopAbs_ON);
1077 minIsFastPath = false;
1078 }
1079 }
1080 }
1081 }
1082
1083 if (minFaceIdx == std::numeric_limits<std::size_t>::max() ||
1084 minDistance == G4OCCTSolidKernel::Infinity()) {
1085 return 0.0;
1086 }
1087
1088 if (calcNorm && validNorm != nullptr && n != nullptr && minIsIn) {
1089 const FaceBounds& fb = fFaceBoundsCache[minFaceIdx];
1090 if (minIsFastPath && fb.outwardNormal.has_value()) {
1091 *n = *fb.outwardNormal;
1092 *validNorm = true;
1093 } else {
1094 const auto outNorm = TryGetOutwardNormal(fb.adaptor, fb.face, minU, minV);
1095 if (outNorm) {
1096 *n = *outNorm;
1097 *validNorm = true;
1098 }
1099 }
1100 }
1101 return minDistance;
1102}
1103
1104G4double G4OCCTSolidKernel::ExactDistanceToOut(const G4ThreeVector& p) const {
1105 const auto match = TryFindClosestFace(fFaceBoundsCache, p);
1106 if (!match.has_value()) {
1107 return 0.0;
1108 }
1109 return (match->distance <= IntersectionTolerance()) ? 0.0 : match->distance;
1110}
1111
1112G4double G4OCCTSolidKernel::DistanceToOut(const G4ThreeVector& p,
1113 SphereCacheData& sphereCache) const {
1114 G4double d;
1115 if (fAllFacesPlanar) {
1116 // For all-planar solids the analytical plane distances are already exact
1117 // lower bounds, so they avoid BVH traversal entirely.
1118 d = PlanarFaceLowerBoundDistance(p);
1119 if (d == G4OCCTSolidKernel::Infinity()) {
1120 d = ExactDistanceToOut(p);
1121 }
1122 } else {
1123 const G4double bvhDist = BVHLowerBoundDistance(p);
1124 d = (bvhDist < G4OCCTSolidKernel::Infinity()) ? bvhDist : ExactDistanceToOut(p);
1125 }
1126 // Feed the just-computed lower bound back into the adaptive sphere cache so
1127 // future interior queries can terminate earlier.
1128 TryInsertSphere(sphereCache, p, d);
1129 return d;
1130}
1131
1133 std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
1134 if (!fCachedVolume) {
1135 GProp_GProps props;
1136 BRepGProp::VolumeProperties(fShape, props);
1137 fCachedVolume = props.Mass();
1138 }
1139 return *fCachedVolume;
1140}
1141
1143 std::unique_lock<std::mutex> lock(fVolumeAreaMutex);
1144 if (!fCachedSurfaceArea) {
1145 GProp_GProps props;
1146 BRepGProp::SurfaceProperties(fShape, props);
1147 fCachedSurfaceArea = props.Mass();
1148 }
1149 return *fCachedSurfaceArea;
1150}
1151
1153 const std::uint64_t currentGen = fShapeGeneration.load(std::memory_order_acquire);
1154 {
1155 std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
1156 while (fSurfaceCacheBuilding) {
1157 fSurfaceCacheCV.wait(lock);
1158 if (fSurfaceCache.has_value() && fSurfaceCacheGeneration == currentGen) {
1159 return *fSurfaceCache;
1160 }
1161 }
1162 if (fSurfaceCache.has_value() && fSurfaceCacheGeneration == currentGen) {
1163 return *fSurfaceCache;
1164 }
1165 // Claim the build so only one thread pays for meshing/tessellation work on
1166 // a cold cache miss; waiters sleep on the condition variable above.
1167 fSurfaceCacheBuilding = true;
1168 }
1169
1171 try {
1172 BRepMesh_IncrementalMesh mesher(fShape, kOCCTRelativeDeflection, /*isRelative=*/Standard_True);
1173 (void)mesher;
1174
1175 for (TopExp_Explorer ex(fShape, TopAbs_FACE); ex.More(); ex.Next()) {
1176 const TopoDS_Face& face = TopoDS::Face(ex.Current());
1177 TopLoc_Location loc;
1178 const Handle(Poly_Triangulation) & triangulation = BRep_Tool::Triangulation(face, loc);
1179 if (triangulation.IsNull()) {
1180 continue;
1181 }
1182
1183 const auto faceIndex = static_cast<std::uint32_t>(cache.faces.size());
1184 cache.faces.push_back(face);
1185
1186 const gp_Trsf& transform = loc.Transformation();
1187 const bool reverseWinding = face.Orientation() == TopAbs_REVERSED;
1188
1189 for (Standard_Integer i = 1; i <= triangulation->NbTriangles(); ++i) {
1190 Standard_Integer idx1 = 0;
1191 Standard_Integer idx2 = 0;
1192 Standard_Integer idx3 = 0;
1193 triangulation->Triangle(i).Get(idx1, idx2, idx3);
1194 if (reverseWinding) {
1195 std::swap(idx2, idx3);
1196 }
1197
1198 const gp_Pnt q1 = triangulation->Node(idx1).Transformed(transform);
1199 const gp_Pnt q2 = triangulation->Node(idx2).Transformed(transform);
1200 const gp_Pnt q3 = triangulation->Node(idx3).Transformed(transform);
1201
1202 const G4ThreeVector v1(q1.X(), q1.Y(), q1.Z());
1203 const G4ThreeVector v2(q2.X(), q2.Y(), q2.Z());
1204 const G4ThreeVector v3(q3.X(), q3.Y(), q3.Z());
1205
1206 const G4double area = 0.5 * (v2 - v1).cross(v3 - v1).mag();
1207 if (area > 0.0) {
1208 // Prefix sums let GetPointOnSurface() perform an area-weighted random
1209 // triangle selection with one lower_bound on cumulativeAreas.
1210 cache.totalArea += area;
1211 cache.cumulativeAreas.push_back(cache.totalArea);
1212 cache.triangles.push_back({v1, v2, v3, faceIndex});
1213 }
1214 }
1215 }
1216 } catch (...) {
1217 std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
1218 fSurfaceCacheBuilding = false;
1219 lock.unlock();
1220 fSurfaceCacheCV.notify_all();
1221 throw;
1222 }
1223
1224 std::unique_lock<std::mutex> lock(fSurfaceCacheMutex);
1225 if (!(fSurfaceCache.has_value() && fSurfaceCacheGeneration == currentGen)) {
1226 fSurfaceCache = std::move(cache);
1227 fSurfaceCacheGeneration = currentGen;
1228 }
1229 fSurfaceCacheBuilding = false;
1230 lock.unlock();
1231 fSurfaceCacheCV.notify_all();
1232 return *fSurfaceCache;
1233}
1234
1235G4ThreeVector G4OCCTSolidKernel::GetPointOnSurface(const char* diagnosticName) const {
1237
1238 if (cache.triangles.empty() || cache.totalArea == 0.0) {
1239 G4ExceptionDescription msg;
1240 msg << "Tessellation of the OCCT shape";
1241 if (diagnosticName != nullptr) {
1242 msg << " for solid \"" << diagnosticName << "\"";
1243 }
1244 msg << " produced no valid triangles; cannot sample a point on the surface.";
1245 G4Exception("G4OCCTSolidKernel::GetPointOnSurface", "GeomMgt1001", FatalException, msg);
1246 return {0.0, 0.0, 0.0};
1247 }
1248
1249 const G4double target = G4UniformRand() * cache.totalArea;
1250 const auto it = std::ranges::lower_bound(cache.cumulativeAreas, target);
1251 const std::size_t idx = std::min(static_cast<std::size_t>(it - cache.cumulativeAreas.begin()),
1252 cache.triangles.size() - 1);
1253 const SurfaceTriangle& chosen = cache.triangles[idx];
1254
1255 G4double r1 = G4UniformRand();
1256 G4double r2 = G4UniformRand();
1257 if (r1 + r2 > 1.0) {
1258 r1 = 1.0 - r1;
1259 r2 = 1.0 - r2;
1260 }
1261
1262 const G4ThreeVector tessPoint =
1263 chosen.p1 + r1 * (chosen.p2 - chosen.p1) + r2 * (chosen.p3 - chosen.p1);
1264
1265 const TopoDS_Face& face = cache.faces[chosen.faceIndex];
1266 TopLoc_Location loc;
1267 const Handle(Geom_Surface) geomSurface = BRep_Tool::Surface(face, loc);
1268 if (!geomSurface.IsNull()) {
1269 gp_Pnt tessPointLocal(tessPoint.x(), tessPoint.y(), tessPoint.z());
1270 if (!loc.IsIdentity()) {
1271 tessPointLocal.Transform(loc.Transformation().Inverted());
1272 }
1273 GeomAPI_ProjectPointOnSurf projection(tessPointLocal, geomSurface);
1274 if (projection.NbPoints() > 0) {
1275 gp_Pnt projectedPoint = projection.NearestPoint();
1276 if (!loc.IsIdentity()) {
1277 projectedPoint.Transform(loc.Transformation());
1278 }
1279 return {projectedPoint.X(), projectedPoint.Y(), projectedPoint.Z()};
1280 }
1281 }
1282
1283 return tessPoint;
1284}
1285
1286} // namespace g4occt::detail
Shared OCCT-backed solid query kernel for adapter frontends.
G4OCCTSolidKernel::ClassifierCache classifier
G4double DistanceToOut(const G4ThreeVector &p, const G4ThreeVector &v, IntersectorCache &intersectorCache, const G4bool calcNorm=false, G4bool *validNorm=nullptr, G4ThreeVector *n=nullptr) const
Exact ray distance from an interior point to the first exit intersection.
void SetShape(const TopoDS_Shape &shape)
G4OCCTSolidKernel(const TopoDS_Shape &shape)
G4double GetCubicVolume()
Compute and cache the solid volume for the current shape generation.
G4double GetSurfaceArea()
Compute and cache the solid surface area for the current shape generation.
static constexpr std::size_t kMaxInscribedSpheres
Maximum number of inscribed spheres retained in a per-thread sphere cache.
G4ThreeVector GetPointOnSurface(const char *diagnosticName=nullptr) const
G4double DistanceToIn(const G4ThreeVector &p, const G4ThreeVector &v, IntersectorCache &intersectorCache) const
Exact ray distance from an exterior point to the first entry intersection.
PointClassification
Classification result for point-in-solid queries.
G4ThreeVector SurfaceNormal(const G4ThreeVector &p) const
Return the outward surface normal at the face nearest point p.
static G4double Infinity()
Return the Geant4 navigation infinity sentinel used by the kernel.
G4double ExactDistanceToOut(const G4ThreeVector &p) const
Exact shortest distance from a point to the surface.
PointClassification ClassifyPoint(const G4ThreeVector &p, ClassifierCache &classifierCache, IntersectorCache &intersectorCache, SphereCacheData &sphereCache) const
Classify a point as inside, on, or outside the solid.
G4double ExactDistanceToIn(const G4ThreeVector &p, ClassifierCache &classifierCache) const
Exact shortest distance from an exterior point to the surface.
const SurfaceSamplingCache & GetOrBuildSurfaceCache() const
Build or return the shared surface-sampling cache for the current shape.
constexpr Standard_Real kOCCTRelativeDeflection
Cached inscribed sphere used to accelerate deep-interior classifications.
std::vector< std::unique_ptr< IntCurvesFace_Intersector > > faceIntersectors
Tessellated triangle entry used for random surface-point sampling.