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