Project
Loading...
Searching...
No Matches
testFlatCSG.cxx
Go to the documentation of this file.
1// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3// All rights not expressly granted are reserved.
4//
5// This software is distributed under the terms of the GNU General Public
6// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7//
8// In applying this license CERN does not waive the privileges and immunities
9// granted to it by virtue of its status as an Intergovernmental Organization
10// or submit itself to any jurisdiction.
13
14#define BOOST_TEST_MODULE Test O2FlatCSG class
15#define BOOST_TEST_MAIN
16#define BOOST_TEST_DYN_LINK
17#include <boost/test/unit_test.hpp>
18
21
22#include "TFile.h"
23#include "TGeoBBox.h"
24#include "TGeoShape.h"
25#include "TGeoTorus.h"
26#include "TGeoTube.h"
27#include "TMath.h"
28
29#include <algorithm>
30#include <cmath>
31#include <filesystem>
32#include <limits>
33#include <vector>
34
35namespace
36{
38
40class Rng
41{
42 public:
43 explicit Rng(unsigned long long seed) : mState(seed) {}
44 double uniform(double low, double high)
45 {
46 mState = mState * 6364136223846793005ULL + 1442695040888963407ULL;
47 const double unit = static_cast<double>((mState >> 11) & ((1ULL << 53) - 1)) / static_cast<double>(1ULL << 53);
48 return low + unit * (high - low);
49 }
50
51 private:
52 unsigned long long mState;
53};
54
56void planeQuadric(const double n[3], const double p[3], double coeff[10])
57{
58 for (int index = 0; index < 6; ++index) {
59 coeff[index] = 0.;
60 }
61 coeff[6] = 0.5 * n[0];
62 coeff[7] = 0.5 * n[1];
63 coeff[8] = 0.5 * n[2];
64 coeff[9] = -(n[0] * p[0] + n[1] * p[1] + n[2] * p[2]);
65}
66
68void zCylinderQuadric(double r, double coeff[10])
69{
70 const double values[10] = {1., 0., 0., 1., 0., 0., 0., 0., 0., -r * r};
71 for (int index = 0; index < 10; ++index) {
72 coeff[index] = values[index];
73 }
74}
75
81void tiltedCylinderQuadric(double r, double coeff[10])
82{
83 const double s = 1. / std::sqrt(3.);
84 const double d[3] = {s, s, s};
85 double a[3][3];
86 for (int row = 0; row < 3; ++row) {
87 for (int column = 0; column < 3; ++column) {
88 a[row][column] = (row == column ? 1. : 0.) - d[row] * d[column];
89 }
90 }
91 coeff[0] = a[0][0];
92 coeff[1] = a[0][1];
93 coeff[2] = a[0][2];
94 coeff[3] = a[1][1];
95 coeff[4] = a[1][2];
96 coeff[5] = a[2][2];
97 coeff[6] = 0.;
98 coeff[7] = 0.;
99 coeff[8] = 0.;
100 coeff[9] = -r * r;
101}
102
104void addBoxCell(O2FlatCSG& solid, double dx, double dy, double dz)
105{
106 const double half[3] = {dx, dy, dz};
107 const int first = solid.GetNhalfspaces();
108 for (int axis = 0; axis < 3; ++axis) {
109 for (int sense = -1; sense <= 1; sense += 2) {
110 double normal[3] = {0., 0., 0.};
111 double through[3] = {0., 0., 0.};
112 normal[axis] = static_cast<double>(sense);
113 through[axis] = sense * half[axis];
114 double coeff[10];
115 planeQuadric(normal, through, coeff);
116 solid.AddQuadric(1., coeff);
117 }
118 }
119 solid.AddCell(first, 6, 8. * dx * dy * dz);
120}
121} // namespace
122
123BOOST_AUTO_TEST_CASE(box_from_six_planes_contains_like_TGeoBBox)
124{
125 O2FlatCSG solid("box");
126 addBoxCell(solid, 3., 4., 5.);
127 BOOST_CHECK_EQUAL(solid.GetNcells(), 1);
128 BOOST_CHECK_EQUAL(solid.GetNhalfspaces(), 6);
129
130 TGeoBBox reference(3., 4., 5.);
131 Rng rng(20260824ULL);
132 int scored = 0;
133 for (int trial = 0; trial < 20000; ++trial) {
134 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-7., 7.), rng.uniform(-8., 8.)};
135 // skip the boundary shell, where the two shapes are allowed to disagree by tolerance
136 if (std::abs(std::abs(point[0]) - 3.) < 1.e-9 || std::abs(std::abs(point[1]) - 4.) < 1.e-9 ||
137 std::abs(std::abs(point[2]) - 5.) < 1.e-9) {
138 continue;
139 }
140 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
141 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
142 ++scored;
143 }
144 BOOST_CHECK_GT(scored, 19000);
145}
146
147BOOST_AUTO_TEST_CASE(tube_from_two_cylinders_and_two_planes_contains_like_TGeoTube)
148{
149 // rmin = 2, rmax = 5, dz = 7: the inner cylinder is a COMPLEMENTED halfspace, which is what
150 // makes this cell non-convex and is the case the whole class exists for.
151 O2FlatCSG solid("tube");
152 double coeff[10];
153 zCylinderQuadric(5., coeff);
154 solid.AddQuadric(1., coeff);
155 zCylinderQuadric(2., coeff);
156 solid.AddQuadric(-1., coeff);
157 const double up[3] = {0., 0., 1.};
158 const double down[3] = {0., 0., -1.};
159 const double top[3] = {0., 0., 7.};
160 const double bottom[3] = {0., 0., -7.};
161 planeQuadric(up, top, coeff);
162 solid.AddQuadric(1., coeff);
163 planeQuadric(down, bottom, coeff);
164 solid.AddQuadric(1., coeff);
165 solid.AddCell(0, 4, TMath::Pi() * (25. - 4.) * 14.);
166
167 TGeoTube reference(2., 5., 7.);
168 Rng rng(777ULL);
169 for (int trial = 0; trial < 20000; ++trial) {
170 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)};
171 const double radius = std::hypot(point[0], point[1]);
172 if (std::abs(radius - 2.) < 1.e-9 || std::abs(radius - 5.) < 1.e-9 ||
173 std::abs(std::abs(point[2]) - 7.) < 1.e-9) {
174 continue;
175 }
176 BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")")
177 {
178 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
179 }
180 }
181}
182
183BOOST_AUTO_TEST_CASE(two_disjoint_cells_are_a_union)
184{
185 O2FlatCSG solid("two_boxes");
186 addBoxCell(solid, 1., 1., 1.);
187 // a second box, centred at x = +10, as six planes of its own
188 const int first = solid.GetNhalfspaces();
189 const double centre = 10.;
190 for (int axis = 0; axis < 3; ++axis) {
191 for (int sense = -1; sense <= 1; sense += 2) {
192 double normal[3] = {0., 0., 0.};
193 double through[3] = {centre, 0., 0.};
194 normal[axis] = static_cast<double>(sense);
195 through[axis] += (axis == 0 ? sense * 1. : 0.);
196 if (axis != 0) {
197 through[axis] = sense * 1.;
198 }
199 double coeff[10];
200 planeQuadric(normal, through, coeff);
201 solid.AddQuadric(1., coeff);
202 }
203 }
204 solid.AddCell(first, 6, 8.);
205
206 const double inFirst[3] = {0., 0., 0.};
207 const double inSecond[3] = {10., 0., 0.};
208 const double between[3] = {5., 0., 0.};
209 BOOST_CHECK(solid.Contains_Loop(inFirst));
210 BOOST_CHECK(solid.Contains_Loop(inSecond));
211 BOOST_CHECK(!solid.Contains_Loop(between));
212}
213
214BOOST_AUTO_TEST_CASE(box_distances_match_TGeoBBox)
215{
216 O2FlatCSG solid("box_dist");
217 addBoxCell(solid, 3., 4., 5.);
218 TGeoBBox reference(3., 4., 5.);
219
220 Rng rng(4242ULL);
221 for (int trial = 0; trial < 20000; ++trial) {
222 double point[3] = {rng.uniform(-12., 12.), rng.uniform(-12., 12.), rng.uniform(-12., 12.)};
223 double dir[3];
224 double norm = 0.;
225 do {
226 for (int index = 0; index < 3; ++index) {
227 dir[index] = rng.uniform(-1., 1.);
228 }
229 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
230 } while (norm < 1.e-3);
231 for (int index = 0; index < 3; ++index) {
232 dir[index] /= norm;
233 }
234 const bool inside = reference.Contains(point);
235 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
236 continue; // a boundary point; classification is tested separately
237 }
238 const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
239 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
240 const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
241 : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
242 if (theirs >= TGeoShape::Big()) {
243 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
244 } else {
245 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-9);
246 }
247 }
248}
249
250BOOST_AUTO_TEST_CASE(tube_distances_match_TGeoTube_through_the_bore)
251{
252 // the complemented inner cylinder makes the occupancy along a ray TWO intervals for a ray that
253 // crosses the bore, which is the case a convexity assumption would get wrong
254 O2FlatCSG solid("tube_dist");
255 double coeff[10];
256 zCylinderQuadric(5., coeff);
257 solid.AddQuadric(1., coeff);
258 zCylinderQuadric(2., coeff);
259 solid.AddQuadric(-1., coeff);
260 const double up[3] = {0., 0., 1.};
261 const double down[3] = {0., 0., -1.};
262 const double top[3] = {0., 0., 7.};
263 const double bottom[3] = {0., 0., -7.};
264 planeQuadric(up, top, coeff);
265 solid.AddQuadric(1., coeff);
266 planeQuadric(down, bottom, coeff);
267 solid.AddQuadric(1., coeff);
268 solid.AddCell(0, 4, 0.);
269
270 TGeoTube reference(2., 5., 7.);
271 // a ray straight along +x at z = 0 enters the wall at x = -5, leaves it at x = -2, re-enters at
272 // x = +2 and leaves at x = +5
273 const double origin[3] = {-9., 0., 0.};
274 const double dir[3] = {1., 0., 0.};
275 BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(origin, dir, TGeoShape::Big()) - 4., 1.e-12);
276
277 const double inWall[3] = {-4., 0., 0.};
278 BOOST_CHECK_SMALL(solid.DistFromInside_Loop(inWall, dir, TGeoShape::Big()) - 2., 1.e-12);
279
280 const double inBore[3] = {0., 0., 0.};
281 BOOST_CHECK(!solid.Contains_Loop(inBore));
282 BOOST_CHECK_SMALL(solid.DistFromOutside_Loop(inBore, dir, TGeoShape::Big()) - 2., 1.e-12);
283
284 Rng rng(99ULL);
285 for (int trial = 0; trial < 20000; ++trial) {
286 double point[3] = {rng.uniform(-9., 9.), rng.uniform(-9., 9.), rng.uniform(-10., 10.)};
287 double direction[3];
288 double norm = 0.;
289 do {
290 for (int index = 0; index < 3; ++index) {
291 direction[index] = rng.uniform(-1., 1.);
292 }
293 norm = std::sqrt(direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]);
294 } while (norm < 1.e-3);
295 for (int index = 0; index < 3; ++index) {
296 direction[index] /= norm;
297 }
298 const bool inside = reference.Contains(point);
299 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
300 continue;
301 }
302 const double mine = inside ? solid.DistFromInside_Loop(point, direction, TGeoShape::Big())
303 : solid.DistFromOutside_Loop(point, direction, TGeoShape::Big());
304 const double theirs = inside ? reference.DistFromInside(point, direction, 3, TGeoShape::Big(), nullptr)
305 : reference.DistFromOutside(point, direction, 3, TGeoShape::Big(), nullptr);
306 if (theirs >= TGeoShape::Big()) {
307 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
308 } else {
309 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-8);
310 }
311 }
312}
313
314BOOST_AUTO_TEST_CASE(a_ray_leaving_one_cell_into_a_touching_one_does_not_stop_between_them)
315{
316 // two unit boxes sharing the face at x = 1: the union's DistFromInside from the origin along +x
317 // is 3, not 1. This is why DistFromInside needs the union across cells and not one cell's exit.
318 O2FlatCSG solid("touching");
319 addBoxCell(solid, 1., 1., 1.);
320 const int first = solid.GetNhalfspaces();
321 const double planes[6][2][3] = {{{1., 0., 0.}, {3., 0., 0.}},
322 {{-1., 0., 0.}, {1., 0., 0.}},
323 {{0., 1., 0.}, {0., 1., 0.}},
324 {{0., -1., 0.}, {0., -1., 0.}},
325 {{0., 0., 1.}, {0., 0., 1.}},
326 {{0., 0., -1.}, {0., 0., -1.}}};
327 for (const auto& plane : planes) {
328 double coeff[10];
329 planeQuadric(plane[0], plane[1], coeff);
330 solid.AddQuadric(1., coeff);
331 }
332 solid.AddCell(first, 6, 8.);
333
334 const double origin[3] = {0., 0., 0.};
335 const double dir[3] = {1., 0., 0.};
336 BOOST_CHECK_SMALL(solid.DistFromInside_Loop(origin, dir, TGeoShape::Big()) - 3., 1.e-12);
337}
338
339BOOST_AUTO_TEST_CASE(tangential_ray_on_a_cylinder_from_a_point_on_its_surface_has_no_nan_root)
340{
341 // a ray tangential to a cylinder, starting exactly on its surface, has beta == 0 and gamma == 0
342 // together in HalfspaceRoots' quadratic -- the q == 0 case that used to divide 0./0. into a
343 // NaN second root instead of recognising the single double root at t = 0
344 O2FlatCSG solid("tangent_ray");
345 double coeff[10];
346 zCylinderQuadric(5., coeff);
347 solid.AddQuadric(1., coeff);
348 const auto& cylinder = solid.GetHalfspace(0);
349
350 const double origin[3] = {5., 0., 0.};
351 const double dir[3] = {0., 1., 0.};
352 double roots[4];
353 const int found = O2FlatCSG::HalfspaceRoots(cylinder, origin, dir, roots);
354
355 BOOST_REQUIRE_EQUAL(found, 1);
356 BOOST_CHECK(std::isfinite(roots[0]));
357 BOOST_CHECK_SMALL(roots[0], 1.e-12);
358
359 // the twin: an independent check that the reported root really is one, by plugging it back
360 // into the surface equation directly rather than trusting the root-finder's own algebra
361 const double hit[3] = {origin[0] + roots[0] * dir[0], origin[1] + roots[0] * dir[1],
362 origin[2] + roots[0] * dir[2]};
363 BOOST_CHECK_SMALL(O2FlatCSG::EvalHalfspace(cylinder, hit), 1.e-9);
364}
365
366BOOST_AUTO_TEST_CASE(torus_contains_and_distances_match_TGeoTorus)
367{
368 // a full torus, R = 10, r = 3, about z -- one cell of one halfspace
369 O2FlatCSG solid("torus");
370 const double centre[3] = {0., 0., 0.};
371 const double axis[3] = {0., 0., 1.};
372 solid.AddTorus(1., centre, axis, 10., 3.);
373 solid.AddCell(0, 1, 2. * TMath::Pi() * TMath::Pi() * 10. * 9.);
374
375 TGeoTorus reference(10., 0., 3.);
376 Rng rng(31415ULL);
377 int scoredPoints = 0;
378 for (int trial = 0; trial < 20000; ++trial) {
379 const double point[3] = {rng.uniform(-15., 15.), rng.uniform(-15., 15.), rng.uniform(-5., 5.)};
380 const double radial = std::hypot(point[0], point[1]);
381 const double distance = std::hypot(radial - 10., point[2]) - 3.;
382 if (std::abs(distance) < 1.e-9) {
383 continue;
384 }
385 BOOST_REQUIRE_EQUAL(solid.Contains_Loop(point), reference.Contains(point));
386 ++scoredPoints;
387 }
388 BOOST_CHECK_GT(scoredPoints, 19000);
389
390 for (int trial = 0; trial < 20000; ++trial) {
391 double point[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)};
392 double dir[3];
393 double norm = 0.;
394 do {
395 for (int index = 0; index < 3; ++index) {
396 dir[index] = rng.uniform(-1., 1.);
397 }
398 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
399 } while (norm < 1.e-3);
400 for (int index = 0; index < 3; ++index) {
401 dir[index] /= norm;
402 }
403 const bool inside = reference.Contains(point);
404 if (inside != static_cast<bool>(solid.Contains_Loop(point))) {
405 continue;
406 }
407 const double mine = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
408 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
409 const double theirs = inside ? reference.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
410 : reference.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
411 if (theirs >= TGeoShape::Big()) {
412 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
413 } else {
414 // the quartic is the looser of the two solvers; 1e-6 cm on a 10 cm torus
415 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6);
416 }
417 }
418}
419
420BOOST_AUTO_TEST_CASE(a_tilted_torus_is_the_same_solid_as_an_upright_one_rotated)
421{
422 // the frame handling is where a torus block goes wrong silently, so it gets its own case
423 const double axis[3] = {0., 1. / std::sqrt(2.), 1. / std::sqrt(2.)};
424 const double centre[3] = {1., 2., 3.};
425 O2FlatCSG solid("tilted_torus");
426 solid.AddTorus(1., centre, axis, 8., 2.);
427 solid.AddCell(0, 1, 0.);
428
429 Rng rng(2718ULL);
430 for (int trial = 0; trial < 20000; ++trial) {
431 const double point[3] = {rng.uniform(-14., 16.), rng.uniform(-13., 17.), rng.uniform(-12., 18.)};
432 // the closed-form signed distance is the reference: sqrt((rho - R)^2 + z^2) - r
433 const double offset[3] = {point[0] - centre[0], point[1] - centre[1], point[2] - centre[2]};
434 const double along = offset[0] * axis[0] + offset[1] * axis[1] + offset[2] * axis[2];
435 double radialVec[3];
436 for (int index = 0; index < 3; ++index) {
437 radialVec[index] = offset[index] - along * axis[index];
438 }
439 const double rho = std::sqrt(radialVec[0] * radialVec[0] + radialVec[1] * radialVec[1] +
440 radialVec[2] * radialVec[2]);
441 const double signedDistance = std::hypot(rho - 8., along) - 2.;
442 if (std::abs(signedDistance) < 1.e-9) {
443 continue;
444 }
445 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(point)), signedDistance < 0.);
446 }
447
448 // Contains_Loop only exercises EvalHalfspace's frame decomposition; HalfspaceRoots has its own,
449 // separate one (the pz/dz/pPerp/dPerp block), and the upright case never gives it a non-z axis
450 // to get wrong. Compare distances against TGeoTorus by carrying a local (upright, origin-
451 // centred) point and direction alongside a world one related by the same rotation that carries
452 // the local z axis onto `axis`, so the reference and the shape describe the same solid.
453 const double s = 1. / std::sqrt(2.);
454 // rotation about the world x axis that sends local (0,0,1) to (0, s, s) == axis
455 auto rotateToWorld = [s](const double local[3], double world[3]) {
456 world[0] = local[0];
457 world[1] = s * local[1] + s * local[2];
458 world[2] = -s * local[1] + s * local[2];
459 };
460
461 TGeoTorus reference(8., 0., 2.);
462 for (int trial = 0; trial < 20000; ++trial) {
463 double localPoint[3] = {rng.uniform(-20., 20.), rng.uniform(-20., 20.), rng.uniform(-8., 8.)};
464 double localDir[3];
465 double norm = 0.;
466 do {
467 for (int index = 0; index < 3; ++index) {
468 localDir[index] = rng.uniform(-1., 1.);
469 }
470 norm = std::sqrt(localDir[0] * localDir[0] + localDir[1] * localDir[1] + localDir[2] * localDir[2]);
471 } while (norm < 1.e-3);
472 for (int index = 0; index < 3; ++index) {
473 localDir[index] /= norm;
474 }
475 double worldPoint[3];
476 double worldDir[3];
477 rotateToWorld(localPoint, worldPoint);
478 rotateToWorld(localDir, worldDir);
479 for (int index = 0; index < 3; ++index) {
480 worldPoint[index] += centre[index];
481 }
482
483 const bool inside = reference.Contains(localPoint);
484 if (inside != static_cast<bool>(solid.Contains_Loop(worldPoint))) {
485 continue;
486 }
487 const double mine = inside ? solid.DistFromInside_Loop(worldPoint, worldDir, TGeoShape::Big())
488 : solid.DistFromOutside_Loop(worldPoint, worldDir, TGeoShape::Big());
489 const double theirs = inside ? reference.DistFromInside(localPoint, localDir, 3, TGeoShape::Big(), nullptr)
490 : reference.DistFromOutside(localPoint, localDir, 3, TGeoShape::Big(), nullptr);
491 if (theirs >= TGeoShape::Big()) {
492 BOOST_REQUIRE_GE(mine, TGeoShape::Big());
493 } else {
494 BOOST_REQUIRE_SMALL(mine - theirs, 1.e-6);
495 }
496 }
497}
498
499BOOST_AUTO_TEST_CASE(the_range_bound_encloses_the_sampled_range)
500{
501 // the bound must be an ENCLOSURE: over-wide is safe, under-wide is a wrong solid
502 O2FlatCSG solid("range");
503 double coeff[10];
504 zCylinderQuadric(5., coeff);
505 const int cylinder = solid.AddQuadric(1., coeff);
506 const double normal[3] = {0., 0., 1.};
507 const double through[3] = {0., 0., 2.};
508 planeQuadric(normal, through, coeff);
509 const int plane = solid.AddQuadric(-1., coeff);
510 const double centre[3] = {1., 0., 0.};
511 const double axis[3] = {0., 0., 1.};
512 const int torus = solid.AddTorus(1., centre, axis, 7., 2.);
513 tiltedCylinderQuadric(5., coeff);
514 const int tilted = solid.AddQuadric(1., coeff);
515
516 Rng rng(555ULL);
517 const std::vector<int> halfspaces = {cylinder, plane, torus, tilted};
518
519 auto checkBox = [&](const double* lo, const double* hi) {
520 for (int which : halfspaces) {
521 double rangeLo = 0.;
522 double rangeHi = 0.;
523 O2FlatCSG::HalfspaceRange(solid.GetHalfspace(which), lo, hi, rangeLo, rangeHi);
524 BOOST_REQUIRE_LE(rangeLo, rangeHi);
525
526 auto checkPoint = [&](const double point[3]) {
527 const double value = O2FlatCSG::EvalHalfspace(solid.GetHalfspace(which), point);
528 BOOST_REQUIRE_GE(value, rangeLo - 1.e-9);
529 BOOST_REQUIRE_LE(value, rangeHi + 1.e-9);
530 };
531
532 // deterministic coverage of the box's extremities: a plane's bound is tight exactly AT a
533 // corner, so uniform interior sampling has probability zero of ever landing where a
534 // slightly under-wide bound would actually be caught
535 for (int cx : {0, 1}) {
536 for (int cy : {0, 1}) {
537 for (int cz : {0, 1}) {
538 const double corner[3] = {cx ? hi[0] : lo[0], cy ? hi[1] : lo[1], cz ? hi[2] : lo[2]};
539 checkPoint(corner);
540 }
541 }
542 }
543 const double mid[3] = {0.5 * (lo[0] + hi[0]), 0.5 * (lo[1] + hi[1]), 0.5 * (lo[2] + hi[2])};
544 for (int faceAxis = 0; faceAxis < 3; ++faceAxis) {
545 for (int side : {0, 1}) {
546 double face[3] = {mid[0], mid[1], mid[2]};
547 face[faceAxis] = side ? hi[faceAxis] : lo[faceAxis];
548 checkPoint(face);
549 }
550 }
551 for (int edgeAxis = 0; edgeAxis < 3; ++edgeAxis) {
552 const int other1 = (edgeAxis + 1) % 3;
553 const int other2 = (edgeAxis + 2) % 3;
554 for (int s1 : {0, 1}) {
555 for (int s2 : {0, 1}) {
556 double edge[3];
557 edge[edgeAxis] = mid[edgeAxis];
558 edge[other1] = s1 ? hi[other1] : lo[other1];
559 edge[other2] = s2 ? hi[other2] : lo[other2];
560 checkPoint(edge);
561 }
562 }
563 }
564
565 // plus random interior samples, as before
566 for (int sample = 0; sample < 200; ++sample) {
567 const double point[3] = {rng.uniform(lo[0], hi[0]), rng.uniform(lo[1], hi[1]),
568 rng.uniform(lo[2], hi[2])};
569 checkPoint(point);
570 }
571 }
572 };
573
574 for (int trial = 0; trial < 3000; ++trial) {
575 double lo[3];
576 double hi[3];
577 for (int index = 0; index < 3; ++index) {
578 const double a = rng.uniform(-12., 12.);
579 const double b = a + rng.uniform(0.01, 6.);
580 lo[index] = a;
581 hi[index] = b;
582 }
583 checkBox(lo, hi);
584 }
585
586 // extreme-aspect-ratio boxes -- one axis ~0.01 wide, another ~24 -- outside the size range the
587 // random trials above ever draw (at most 6 wide per axis)
588 const double extreme[4][3][2] = {
589 {{-0.005, 0.005}, {-12., 12.}, {-0.5, 0.5}},
590 {{-12., 12.}, {-0.005, 0.005}, {3., 27.}},
591 {{2., 2.01}, {-1., 1.}, {-12., 12.}},
592 {{-24., 0.}, {5., 5.01}, {-3., 3.}},
593 };
594 for (const auto& box : extreme) {
595 const double lo[3] = {box[0][0], box[1][0], box[2][0]};
596 const double hi[3] = {box[0][1], box[1][1], box[2][1]};
597 checkBox(lo, hi);
598 }
599}
600
601BOOST_AUTO_TEST_CASE(the_boxes_cover_the_solid_and_their_active_lists_are_sound)
602{
603 // rmin = 2, rmax = 5, dz = 7 again, so there is a bore for the boxes to carve around
604 O2FlatCSG solid("boxes");
605 double coeff[10];
606 zCylinderQuadric(5., coeff);
607 solid.AddQuadric(1., coeff);
608 zCylinderQuadric(2., coeff);
609 solid.AddQuadric(-1., coeff);
610 const double up[3] = {0., 0., 1.};
611 const double down[3] = {0., 0., -1.};
612 const double top[3] = {0., 0., 7.};
613 const double bottom[3] = {0., 0., -7.};
614 planeQuadric(up, top, coeff);
615 solid.AddQuadric(1., coeff);
616 planeQuadric(down, bottom, coeff);
617 solid.AddQuadric(1., coeff);
618 solid.AddCell(0, 4, 0.);
619 const double lo[3] = {-5., -5., -7.};
620 const double hi[3] = {5., 5., 7.};
621 solid.SetCellBBox(0, lo, hi);
622 solid.CloseShape();
623
624 BOOST_CHECK_GT(solid.GetNboxes(), 1);
625
626 Rng rng(8080ULL);
627 int insideSamples = 0;
628 for (int trial = 0; trial < 50000; ++trial) {
629 const double point[3] = {rng.uniform(-6., 6.), rng.uniform(-6., 6.), rng.uniform(-8., 8.)};
630 if (!solid.Contains_Loop(point)) {
631 continue;
632 }
633 ++insideSamples;
634 // COVERAGE: every point of the solid is in some box
635 bool covered = false;
636 for (int index = 0; index < solid.GetNboxes() && !covered; ++index) {
637 const auto& box = solid.GetBox(index);
638 covered = point[0] >= box.min[0] && point[0] <= box.max[0] && point[1] >= box.min[1] &&
639 point[1] <= box.max[1] && point[2] >= box.min[2] && point[2] <= box.max[2];
640 }
641 BOOST_REQUIRE(covered);
642 }
643 BOOST_CHECK_GT(insideSamples, 5000);
644
645 // SOUNDNESS of the active lists: in every box, the active list alone decides membership
646 for (int index = 0; index < solid.GetNboxes(); ++index) {
647 const auto& box = solid.GetBox(index);
648 for (int sample = 0; sample < 200; ++sample) {
649 const double point[3] = {rng.uniform(box.min[0], box.max[0]),
650 rng.uniform(box.min[1], box.max[1]),
651 rng.uniform(box.min[2], box.max[2])};
652 bool byActive = true;
653 for (int slot = 0; slot < box.nActive && byActive; ++slot) {
654 byActive = O2FlatCSG::EvalHalfspace(
655 solid.GetHalfspace(solid.GetActive(box.firstActive + slot)), point) <= 0.;
656 }
657 BOOST_REQUIRE_EQUAL(byActive, solid.CellContains(box.cell, point));
658 }
659 }
660}
661
662BOOST_AUTO_TEST_CASE(a_box_wholly_inside_a_cell_carries_no_active_halfspaces)
663{
664 O2FlatCSG solid("solid_boxes");
665 addBoxCell(solid, 4., 4., 4.);
666 const double lo[3] = {-4., -4., -4.};
667 const double hi[3] = {4., 4., 4.};
668 solid.SetCellBBox(0, lo, hi);
669 // Both knobs are pinned, not defaulted: this case is about what the subdivision CAN produce,
670 // and the shipped defaults are chosen for query cost, which is
671 // a different question. Six levels on a cube is a 4 x 4 x 4 grid, whose innermost eight boxes
672 // touch no face.
673 solid.SetSplitDepth(6);
674 solid.SetMinBoxFraction(0.01);
675 solid.CloseShape();
676 // a box has six planes and is convex, so subdivision must find interior boxes with an empty list
677 int solidBoxes = 0;
678 for (int index = 0; index < solid.GetNboxes(); ++index) {
679 if (solid.GetBox(index).nActive == 0) {
680 ++solidBoxes;
681 }
682 }
683 BOOST_CHECK_GT(solidBoxes, 0);
684}
685
686BOOST_AUTO_TEST_CASE(a_cell_without_a_bbox_fails_loudly_instead_of_vanishing)
687{
688 // cell 0 gets a box; cell 1 (a second, disjoint box) never does -- CloseShape must refuse to
689 // build a partial, silently-wrong solid rather than just drop cell 1
690 O2FlatCSG solid("missing_bbox");
691 addBoxCell(solid, 1., 1., 1.);
692 const int first = solid.GetNhalfspaces();
693 const double centre[3] = {10., 0., 0.};
694 for (int axis = 0; axis < 3; ++axis) {
695 for (int sense = -1; sense <= 1; sense += 2) {
696 double normal[3] = {0., 0., 0.};
697 double through[3] = {centre[0], centre[1], centre[2]};
698 normal[axis] = static_cast<double>(sense);
699 through[axis] += sense * 1.;
700 double coeff[10];
701 planeQuadric(normal, through, coeff);
702 solid.AddQuadric(1., coeff);
703 }
704 }
705 solid.AddCell(first, 6, 8.);
706
707 const double lo[3] = {-1., -1., -1.};
708 const double hi[3] = {1., 1., 1.};
709 solid.SetCellBBox(0, lo, hi); // cell 1's box is never set
710
711 solid.CloseShape();
712 BOOST_CHECK(!solid.IsClosed());
713 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
714}
715
716BOOST_AUTO_TEST_CASE(an_inverted_cell_bbox_fails_loudly_instead_of_being_kept_as_solid)
717{
718 // a converter that swapped lo/hi arguments must not get a shape that quietly reports itself
719 // closed: an all-axes-inverted box never grows past SplitBox's longest = 0. initialiser, so it
720 // would otherwise be kept immediately with an active list computed from a negative-half-extent
721 // (hence invalid) range bound -- possibly nActive == 0, which downstream reads as solid material
722 O2FlatCSG solid("inverted_bbox");
723 addBoxCell(solid, 1., 1., 1.);
724 const double lo[3] = {-1., -1., -1.};
725 const double hi[3] = {1., 1., 1.};
726 solid.SetCellBBox(0, hi, lo); // lo/hi swapped
727
728 solid.CloseShape();
729 BOOST_CHECK(!solid.IsClosed());
730 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
731}
732
733BOOST_AUTO_TEST_CASE(a_nan_cell_bbox_fails_loudly_instead_of_defeating_the_inverted_box_check)
734{
735 // a NaN passes every ordinary "hi < lo" comparison silently (every comparison with NaN is
736 // false), so it must be its own check rather than fall through the inverted-box test above --
737 // otherwise it would reach HalfspaceRange, produce a NaN range that fails both of SplitBox's
738 // drop tests, and get kept as a spurious box
739 O2FlatCSG solid("nan_bbox");
740 addBoxCell(solid, 1., 1., 1.);
741 const double nan = std::numeric_limits<double>::quiet_NaN();
742 const double lo[3] = {-1., -1., -1.};
743 const double hi[3] = {1., nan, 1.};
744 solid.SetCellBBox(0, lo, hi);
745
746 solid.CloseShape();
747 BOOST_CHECK(!solid.IsClosed());
748 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
749}
750
751namespace
752{
764void buildBracket(O2FlatCSG& solid, double planeScale = 1.)
765{
766 double coeff[10];
767 const auto scaledPlane = [&](const double* normal, const double* through) {
768 planeQuadric(normal, through, coeff);
769 for (int index = 0; index < 10; ++index) {
770 coeff[index] *= planeScale;
771 }
772 };
773 // cell 0: the long arm, x in [-10, 10], y in [-1, 1], z in [-1, 1]
774 const double arm[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}},
775 {{-1., 0., 0.}, {-10., 0., 0.}},
776 {{0., 1., 0.}, {0., 1., 0.}},
777 {{0., -1., 0.}, {0., -1., 0.}},
778 {{0., 0., 1.}, {0., 0., 1.}},
779 {{0., 0., -1.}, {0., 0., -1.}}};
780 int first = solid.GetNhalfspaces();
781 for (const auto& plane : arm) {
782 scaledPlane(plane[0], plane[1]);
783 solid.AddQuadric(1., coeff);
784 }
785 solid.AddCell(first, 6, 8. * 10. * 1. * 1.);
786 const double armLo[3] = {-10., -1., -1.};
787 const double armHi[3] = {10., 1., 1.};
788 solid.SetCellBBox(0, armLo, armHi);
789
790 // cell 1: the upright, x in [8, 10], y in [-1, 1], z in [1, 12]
791 const double upright[6][2][3] = {{{1., 0., 0.}, {10., 0., 0.}},
792 {{-1., 0., 0.}, {8., 0., 0.}},
793 {{0., 1., 0.}, {0., 1., 0.}},
794 {{0., -1., 0.}, {0., -1., 0.}},
795 {{0., 0., 1.}, {0., 0., 12.}},
796 {{0., 0., -1.}, {0., 0., 1.}}};
797 first = solid.GetNhalfspaces();
798 for (const auto& plane : upright) {
799 scaledPlane(plane[0], plane[1]);
800 solid.AddQuadric(1., coeff);
801 }
802 solid.AddCell(first, 6, 2. * 2. * 11.);
803 const double uprightLo[3] = {8., -1., 1.};
804 const double uprightHi[3] = {10., 1., 12.};
805 solid.SetCellBBox(1, uprightLo, uprightHi);
806
807 // cell 2: a washer around z at x = -8 and z in [1, 3], with a bore -- a complemented cylinder,
808 // so non-convex, and clear of the arm so the bore is empty space
809 first = solid.GetNhalfspaces();
810 const double centreShift = -8.;
811 // outer cylinder about the axis through (-8, 0, *): translate by completing the square
812 const double outer[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0.,
813 centreShift * centreShift - 9.};
814 solid.AddQuadric(1., outer);
815 const double inner[10] = {1., 0., 0., 1., 0., 0., -centreShift, 0., 0.,
816 centreShift * centreShift - 1.};
817 solid.AddQuadric(-1., inner);
818 const double washer[2][2][3] = {{{0., 0., 1.}, {0., 0., 3.}}, {{0., 0., -1.}, {0., 0., 1.}}};
819 for (const auto& plane : washer) {
820 scaledPlane(plane[0], plane[1]);
821 solid.AddQuadric(1., coeff);
822 }
823 solid.AddCell(first, 4, TMath::Pi() * (9. - 1.) * 2.);
824 const double washerLo[3] = {-11., -3., 1.};
825 const double washerHi[3] = {-5., 3., 3.};
826 solid.SetCellBBox(2, washerLo, washerHi);
827}
828} // namespace
829
830BOOST_AUTO_TEST_CASE(the_accelerated_contains_is_bit_identical_to_its_twin)
831{
832 O2FlatCSG solid("bracket");
833 buildBracket(solid);
834 solid.CloseShape();
835 BOOST_CHECK_GT(solid.GetNboxes(), 3);
836 BOOST_CHECK_GT(solid.GetBVHMemory(), 0u);
837
838 Rng rng(123456ULL);
839 for (int trial = 0; trial < 200000; ++trial) {
840 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
841 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
842 }
843}
844
845BOOST_AUTO_TEST_CASE(the_sampled_boundary_points_flip_containment)
846{
847 O2FlatCSG solid("bracket_points");
848 buildBracket(solid);
849 solid.CloseShape();
850 constexpr int kPoints = 4000;
851 std::vector<double> points(3 * kPoints, 0.);
852 BOOST_REQUIRE(solid.GetPointsOnSegments(kPoints, points.data()));
853 const double zAxis[3] = {0., 0., 1.};
854 for (int index = 0; index < kPoints; ++index) {
855 const double* point = &points[3 * index];
856 double normal[3] = {0., 0., 0.};
857 solid.ComputeNormal(point, zAxis, normal);
858 double below[3];
859 double above[3];
860 for (int axis = 0; axis < 3; ++axis) {
861 below[axis] = point[axis] - 1.e-6 * normal[axis];
862 above[axis] = point[axis] + 1.e-6 * normal[axis];
863 }
864 BOOST_TEST_CONTEXT("point = (" << point[0] << ", " << point[1] << ", " << point[2] << ")")
865 {
866 BOOST_CHECK_NE(solid.Contains(below), solid.Contains(above));
867 }
868 }
869}
870
871BOOST_AUTO_TEST_CASE(the_accelerated_distances_are_bit_identical_to_their_twins)
872{
873 O2FlatCSG solid("bracket_dist");
874 buildBracket(solid);
875 solid.CloseShape();
876
877 Rng rng(654321ULL);
878 for (int trial = 0; trial < 200000; ++trial) {
879 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
880 double dir[3];
881 double norm = 0.;
882 do {
883 for (int index = 0; index < 3; ++index) {
884 dir[index] = rng.uniform(-1., 1.);
885 }
886 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
887 } while (norm < 1.e-3);
888 for (int index = 0; index < 3; ++index) {
889 dir[index] /= norm;
890 }
891 if (solid.Contains_Loop(point)) {
892 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
893 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
894 } else {
895 BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr),
896 solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()));
897 }
898 }
899}
900
901BOOST_AUTO_TEST_CASE(a_ray_along_the_long_arm_crosses_every_cell_it_should)
902{
903 // the case a per-box clip gets wrong if it forgets to clip: a ray running the length of the
904 // bracket passes through many boxes of the same cell, and must see ONE interval, not many
905 O2FlatCSG solid("bracket_long");
906 buildBracket(solid);
907 solid.CloseShape();
908 const double dir[3] = {1., 0., 0.};
909
910 // the entry, which one box decides on its own: nothing lies before the arm along z = 0
911 const double origin[3] = {-20., 0., 0.};
912 BOOST_CHECK_SMALL(solid.DistFromOutside(origin, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12);
913
914 // the exit, which sixteen boxes of cell 0 decide together: the arm is split along x into boxes
915 // 1.25 wide, so this is the cross-box join, and a traversal that forgot it would stop at the
916 // first box boundary
917 const double inArm[3] = {0., 0., 0.};
918 // inside the arm at the origin, the exit is x = 10 (the arm and the upright touch at x = 8..10
919 // only for z > 1, so along z = 0 the arm alone decides)
920 BOOST_CHECK_SMALL(solid.DistFromInside(inArm, dir, 3, TGeoShape::Big(), nullptr) - 10., 1.e-12);
921
922 // An ENTRY that needs the per-cell merge, which is otherwise hard to reach: the twin's rule
923 // takes the smallest entry over the intervals whose exit clears TGeoShape::Tolerance(), so a
924 // box boundary crossed within the tolerance of the origin cuts the real interval into a
925 // sub-tolerance stub the rule would throw away, and the answer would jump from the true entry
926 // to the box boundary. This ray starts 1e-11 outside the arm's y = 1 face and 2e-11 before its
927 // x = -8.75 box boundary, so both crossings sit inside the tolerance.
928 const double grazing[3] = {-8.75 - 2.e-11, 1. + 1.e-11, 0.5};
929 const double slant = 1. / std::sqrt(2.);
930 const double slantDir[3] = {slant, -slant, 0.};
931 const double entered = solid.DistFromOutside(grazing, slantDir, 3, TGeoShape::Big(), nullptr);
932 BOOST_CHECK_EQUAL(entered, solid.DistFromOutside_Loop(grazing, slantDir, TGeoShape::Big()));
933 // and it really is in the regime where a per-box rule would differ: below the tolerance, and
934 // strictly nearer than the x = -8.75 box boundary at 2e-11 * sqrt(2)
935 BOOST_CHECK_GT(entered, 0.);
936 BOOST_CHECK_LT(entered, TGeoShape::Tolerance());
937 BOOST_CHECK_LT(entered, 2.e-11 * std::sqrt(2.));
938}
939
940BOOST_AUTO_TEST_CASE(the_accelerated_distances_track_their_twins_when_a_plane_is_rescaled)
941{
942 // Bit identity between an accelerated query and its twin is a self-check discipline, not a
943 // physics requirement: a one-ulp difference in an exit distance is navigationally irrelevant.
944 // It is achievable only under the plane convention of design section 3.1, where a unit normal n
945 // is stored as 2b = n. There the slab bound (v - o_k) / d_k and the root -0.5*gamma/beta divide
946 // numerator and denominator each scaled by exactly one half, so the single IEEE division returns
947 // the same double for both, and a box face lying on a plane halfspace is crossed at one value.
948 //
949 // Rescaling every plane by a NON-POWER-OF-TWO -- 3 here, which is what an unnormalised carrier
950 // normal (3, 0, 0) would give -- describes exactly the same solid, but fl(1.5 * d_x) rounds, the
951 // root moves off the slab bound by an ulp, and the last bit is lost. Nothing else in the system
952 // notices, so this test pins the size of what is lost: the answers must still agree closely.
953 O2FlatCSG solid("bracket_scaled");
954 buildBracket(solid, 3.);
955 solid.CloseShape();
956 BOOST_REQUIRE(solid.IsClosed());
957
958 Rng rng(1357911ULL);
959 double worst = 0.;
960 for (int trial = 0; trial < 200000; ++trial) {
961 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
962 double dir[3];
963 double norm = 0.;
964 do {
965 for (int index = 0; index < 3; ++index) {
966 dir[index] = rng.uniform(-1., 1.);
967 }
968 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
969 } while (norm < 1.e-3);
970 for (int index = 0; index < 3; ++index) {
971 dir[index] /= norm;
972 }
973 const bool inside = solid.Contains_Loop(point);
974 // Contains has no arithmetic of its own to lose, so it stays bit-identical under any scale
975 BOOST_REQUIRE_EQUAL(solid.Contains(point), inside);
976 const double accelerated =
977 inside ? solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr)
978 : solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr);
979 const double twin = inside ? solid.DistFromInside_Loop(point, dir, TGeoShape::Big())
980 : solid.DistFromOutside_Loop(point, dir, TGeoShape::Big());
981 const double slack = std::abs(accelerated - twin);
982 worst = std::max(worst, slack);
983 // The bound is set just above what the rescale actually costs -- the run below measures
984 // 1.24e-14 -- so the assertion, and not only the message under it, is what pins the size of
985 // the loss. A looser bound would pass on a rescale that had broken something far larger.
986 BOOST_REQUIRE_LE(slack, 1.e-13 * std::max(1., std::abs(twin)));
987 }
988 BOOST_TEST_MESSAGE("largest accelerated-vs-twin gap under a x3 plane rescale: " << worst);
989 // The aggregate is deliberately looser than the relative assertion above: it is an absolute
990 // bound on a maximum over a sample, and FMA contraction or a different libm moves the last
991 // couple of ulps. 1e-12 still pins the size of the loss a thousand times tighter than the
992 // 1e-9 this test used to assert, without being a cross-platform tripwire.
993 BOOST_CHECK_LE(worst, 1.e-12);
994}
995
996BOOST_AUTO_TEST_CASE(a_shape_that_failed_to_close_still_answers_through_the_loop_twins)
997{
998 // CloseShape refuses an unset cell bbox and builds nothing, so there is no box array and no BVH.
999 // An accelerated query that walked the empty array would answer "no material anywhere" -- the
1000 // silent vanishing the refusal exists to prevent -- so all three must fall back to the twins.
1001 O2FlatCSG solid("bracket_unclosed");
1002 buildBracket(solid);
1003 // a fourth cell, a box at x in [12, 14], deliberately left without a bounding box
1004 const double extra[6][2][3] = {{{1., 0., 0.}, {14., 0., 0.}},
1005 {{-1., 0., 0.}, {12., 0., 0.}},
1006 {{0., 1., 0.}, {0., 1., 0.}},
1007 {{0., -1., 0.}, {0., -1., 0.}},
1008 {{0., 0., 1.}, {0., 0., 1.}},
1009 {{0., 0., -1.}, {0., 0., -1.}}};
1010 double coeff[10];
1011 const int first = solid.GetNhalfspaces();
1012 for (const auto& plane : extra) {
1013 planeQuadric(plane[0], plane[1], coeff);
1014 solid.AddQuadric(1., coeff);
1015 }
1016 solid.AddCell(first, 6, 2. * 2. * 2.);
1017
1018 solid.CloseShape();
1019 BOOST_REQUIRE(!solid.IsClosed());
1020 BOOST_CHECK_EQUAL(solid.GetNboxes(), 0);
1021 BOOST_CHECK_EQUAL(solid.GetBVHMemory(), 0u);
1022
1023 // material inside every one of the four cells is still found, and empty space is still empty
1024 const double inArm[3] = {0., 0., 0.};
1025 const double inUpright[3] = {9., 0., 6.};
1026 const double inWasher[3] = {-10.5, 0., 2.};
1027 const double inExtra[3] = {13., 0., 0.};
1028 // the washer's bore, which is empty space now that the washer sits above the arm
1029 const double inBore[3] = {-8., 0., 2.};
1030 const double outside[3] = {0., 0., 20.};
1031 BOOST_CHECK(solid.Contains(inArm));
1032 BOOST_CHECK(solid.Contains(inUpright));
1033 BOOST_CHECK(solid.Contains(inWasher));
1034 BOOST_CHECK(solid.Contains(inExtra));
1035 BOOST_CHECK(!solid.Contains(inBore));
1036 BOOST_CHECK(!solid.Contains(outside));
1037
1038 Rng rng(24680ULL);
1039 for (int trial = 0; trial < 20000; ++trial) {
1040 const double point[3] = {rng.uniform(-16., 16.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1041 BOOST_REQUIRE_EQUAL(solid.Contains(point), solid.Contains_Loop(point));
1042 double dir[3];
1043 double norm = 0.;
1044 do {
1045 for (int index = 0; index < 3; ++index) {
1046 dir[index] = rng.uniform(-1., 1.);
1047 }
1048 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1049 } while (norm < 1.e-3);
1050 for (int index = 0; index < 3; ++index) {
1051 dir[index] /= norm;
1052 }
1053 const bool inside = solid.Contains_Loop(point);
1054 if (inside) {
1055 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
1056 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1057 } else {
1058 BOOST_REQUIRE_EQUAL(solid.DistFromOutside(point, dir, 3, TGeoShape::Big(), nullptr),
1059 solid.DistFromOutside_Loop(point, dir, TGeoShape::Big()));
1060 }
1061 // Safety falls back to its twin exactly like the other three accelerated queries -- this was
1062 // asserted for Contains and the distances above but never extended to Safety
1063 BOOST_REQUIRE_EQUAL(solid.Safety(point, inside), solid.Safety_Loop(point, inside));
1064 }
1065}
1066
1067BOOST_AUTO_TEST_CASE(safety_is_sound_and_matches_its_twin)
1068{
1069 O2FlatCSG solid("bracket_safety");
1070 buildBracket(solid);
1071 solid.CloseShape();
1072
1073 Rng rng(24680ULL);
1074 for (int trial = 0; trial < 50000; ++trial) {
1075 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
1076 const bool inside = solid.Contains_Loop(point);
1077 const double safety = solid.Safety(point, inside);
1078 BOOST_REQUIRE_GE(safety, 0.);
1079 BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside));
1080
1081 // SOUNDNESS: no point within `safety` of `point` may have the opposite classification
1082 for (int probe = 0; probe < 40; ++probe) {
1083 double dir[3];
1084 double norm = 0.;
1085 do {
1086 for (int index = 0; index < 3; ++index) {
1087 dir[index] = rng.uniform(-1., 1.);
1088 }
1089 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1090 } while (norm < 1.e-3);
1091 const double reach = safety * rng.uniform(0., 0.999) / norm;
1092 const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1],
1093 point[2] + reach * dir[2]};
1094 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(near)), inside);
1095 }
1096 }
1097}
1098
1099BOOST_AUTO_TEST_CASE(safety_is_sound_when_the_inside_bound_is_actually_nonzero)
1100{
1101 // At the class's default split depth, buildBracket's arm is so far from cubic (20 x 2 x 2) that
1102 // the depth cap fires before any leaf fully detaches from all six faces: EVERY box keeps
1103 // nActive != 0, so the inside branch's `nActive == 0` selection path -- the one piece of
1104 // `Safety` whose soundness rests on a structural invariant (design section 4.2's hard guarantee)
1105 // rather than an exact box-distance formula -- is never taken by the test above. Its probes are
1106 // then vacuous: with `safety == 0.`, `reach` is always `0.` too, so the "nearby" point IS the
1107 // query point and the soundness check is trivially true. A deeper split makes solid boxes exist
1108 // (see the fix-round measurement in the task report), which this case forces so the nActive == 0
1109 // path is genuinely exercised end to end, not just agreed upon by two implementations at zero.
1110 O2FlatCSG solid("bracket_safety_deep");
1111 buildBracket(solid);
1112 solid.SetSplitDepth(14);
1113 // The size floor has to come down with the depth cap, or it stops the split first: at the
1114 // shipped 0.05 the arm is thinner than one minimum box and no leaf ever detaches.
1115 solid.SetMinBoxFraction(0.002);
1116 solid.CloseShape();
1117
1118 bool sawPositiveInsideSafety = false;
1119 Rng rng(11235813ULL);
1120 for (int trial = 0; trial < 50000; ++trial) {
1121 double point[3] = {rng.uniform(-16., 16.), rng.uniform(-8., 8.), rng.uniform(-6., 17.)};
1122 const bool inside = solid.Contains_Loop(point);
1123 const double safety = solid.Safety(point, inside);
1124 BOOST_REQUIRE_GE(safety, 0.);
1125 BOOST_REQUIRE_EQUAL(safety, solid.Safety_Loop(point, inside));
1126 if (inside && safety > 0.) {
1127 sawPositiveInsideSafety = true;
1128 }
1129
1130 // the same soundness probes as above, now with genuine reach on at least some trials
1131 for (int probe = 0; probe < 40; ++probe) {
1132 double dir[3];
1133 double norm = 0.;
1134 do {
1135 for (int index = 0; index < 3; ++index) {
1136 dir[index] = rng.uniform(-1., 1.);
1137 }
1138 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1139 } while (norm < 1.e-3);
1140 const double reach = safety * rng.uniform(0., 0.999) / norm;
1141 const double near[3] = {point[0] + reach * dir[0], point[1] + reach * dir[1],
1142 point[2] + reach * dir[2]};
1143 BOOST_REQUIRE_EQUAL(static_cast<bool>(solid.Contains_Loop(near)), inside);
1144 }
1145 }
1146 BOOST_REQUIRE(sawPositiveInsideSafety);
1147}
1148
1149BOOST_AUTO_TEST_CASE(capacity_is_the_sum_of_the_cell_volumes)
1150{
1151 O2FlatCSG solid("bracket_capacity");
1152 buildBracket(solid);
1153 solid.CloseShape();
1154 const double expected = 8. * 10. * 1. * 1. + 2. * 2. * 11. + TMath::Pi() * (9. - 1.) * 2.;
1155 BOOST_CHECK_SMALL(solid.Capacity() - expected, 1.e-12);
1156}
1157
1158BOOST_AUTO_TEST_CASE(the_bounding_box_is_tight_around_the_retained_boxes)
1159{
1160 // ComputeBBox is the union of the RETAINED boxes -- a subset of the union of the cell AABBs --
1161 // so exact GetDX/GetDY/GetDZ/GetOrigin values depend on subdivision details rather than on the
1162 // contract. Assert the two legs that matter for navigation correctness instead: the bounding
1163 // box holds the whole solid, and it does not overshoot past what the cells could possibly reach.
1164 O2FlatCSG solid("bracket_bbox");
1165 buildBracket(solid);
1166 solid.CloseShape();
1167
1168 // every point of the solid is inside the bounding box
1169 Rng rng(13579ULL);
1170 for (int trial = 0; trial < 50000; ++trial) {
1171 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1172 if (solid.Contains_Loop(point)) {
1173 BOOST_REQUIRE(solid.TGeoBBox::Contains(point));
1174 }
1175 }
1176
1177 // the bounding box is contained in the union of the cell AABBs, which buildBracket fixes:
1178 // arm x in [-10, 10], y in [-1, 1], z in [-1, 1]; upright x in [8, 10], y in [-1, 1], z in
1179 // [1, 12]; washer x in [-11, -5], y in [-3, 3], z in [1, 3]
1180 const double cellLo[3][3] = {{-10., -1., -1.}, {8., -1., 1.}, {-11., -3., 1.}};
1181 const double cellHi[3][3] = {{10., 1., 1.}, {10., 1., 12.}, {-5., 3., 3.}};
1182 double unionLo[3] = {cellLo[0][0], cellLo[0][1], cellLo[0][2]};
1183 double unionHi[3] = {cellHi[0][0], cellHi[0][1], cellHi[0][2]};
1184 for (int cell = 1; cell < 3; ++cell) {
1185 for (int index = 0; index < 3; ++index) {
1186 unionLo[index] = std::min(unionLo[index], cellLo[cell][index]);
1187 unionHi[index] = std::max(unionHi[index], cellHi[cell][index]);
1188 }
1189 }
1190 const double* origin = solid.GetOrigin();
1191 for (int index = 0; index < 3; ++index) {
1192 const double dHalf = index == 0 ? solid.GetDX() : (index == 1 ? solid.GetDY() : solid.GetDZ());
1193 BOOST_CHECK_GE(origin[index] - dHalf, unionLo[index] - 1.e-9);
1194 BOOST_CHECK_LE(origin[index] + dHalf, unionHi[index] + 1.e-9);
1195 }
1196}
1197
1198BOOST_AUTO_TEST_CASE(the_normal_on_a_face_is_the_face_normal)
1199{
1200 O2FlatCSG solid("box_normal");
1201 addBoxCell(solid, 3., 4., 5.);
1202 const double lo[3] = {-3., -4., -5.};
1203 const double hi[3] = {3., 4., 5.};
1204 solid.SetCellBBox(0, lo, hi);
1205 solid.CloseShape();
1206
1207 const double onFace[3] = {3., 1., 1.};
1208 const double dir[3] = {1., 0., 0.};
1209 double normal[3] = {0., 0., 0.};
1210 solid.ComputeNormal(onFace, dir, normal);
1211 BOOST_CHECK_SMALL(normal[0] - 1., 1.e-12);
1212 BOOST_CHECK_SMALL(normal[1], 1.e-12);
1213 BOOST_CHECK_SMALL(normal[2], 1.e-12);
1214}
1215
1216BOOST_AUTO_TEST_CASE(the_normal_selection_is_scale_invariant_across_cells)
1217{
1218 // Fix round 1: |EvalHalfspace| alone is not a distance -- its gain per unit distance is 1 for a
1219 // unit plane but ~2R for a cylinder of radius R, so a naive argmin over |f| can pick a distant
1220 // plane over the surface the point is actually on. Two cells make the point concrete: cell 0 is
1221 // a cylinder of radius 100 about z, cell 1 a single plane at z = 0.05. The test point sits
1222 // 0.0005 from the cylinder wall (radially) and 0.05 from the plane -- the cylinder is the true
1223 // nearest surface by two orders of magnitude, but |f_cylinder| ~= 0.1 > |f_plane| = 0.05, so the
1224 // unscaled rule would have picked the plane and returned (0, 0, 1) instead of the correct
1225 // (1, 0, 0). Deliberately not closed: with CloseShape run, the box-restriction half of the fix
1226 // alone would make this pass trivially (the point's own box never sees the other cell's plane),
1227 // so this exercises ComputeNormal's cross-cell fallback scan, where only the |f| / |grad f|
1228 // fix -- not the box restriction -- can be what saves it.
1229 O2FlatCSG solid("scale_invariance");
1230 double coeff[10];
1231 zCylinderQuadric(100., coeff);
1232 solid.AddQuadric(1., coeff);
1233 solid.AddCell(0, 1, 0.);
1234
1235 const double planeNormal[3] = {0., 0., 1.};
1236 const double planeThrough[3] = {0., 0., 0.05};
1237 planeQuadric(planeNormal, planeThrough, coeff);
1238 const int planeFirst = solid.GetNhalfspaces();
1239 solid.AddQuadric(1., coeff);
1240 solid.AddCell(planeFirst, 1, 0.);
1241
1242 BOOST_REQUIRE(!solid.IsClosed());
1243
1244 const double point[3] = {99.9995, 0., 0.};
1245 const double dir[3] = {1., 0., 0.};
1246 double normal[3] = {0., 0., 0.};
1247 solid.ComputeNormal(point, dir, normal);
1248 BOOST_CHECK_SMALL(normal[0] - 1., 1.e-9);
1249 BOOST_CHECK_SMALL(normal[1], 1.e-9);
1250 BOOST_CHECK_SMALL(normal[2], 1.e-9);
1251}
1252
1253BOOST_AUTO_TEST_CASE(a_zero_extent_cell_bbox_does_not_burn_the_whole_cubify_budget)
1254{
1255 // Fix round 2: a cell bbox with a genuinely zero extent on one axis passes CloseShape's
1256 // validation (it rejects only unset, inverted or non-finite boxes, not degenerate-but-flat
1257 // ones). Without SplitBox's `shortest` floor at `minSize`, that axis's extent stays pinned at
1258 // zero forever (it is never the longest, so never split), making `longest > 2 * shortest`
1259 // permanently true and spending the ENTIRE per-path cubify ceiling on a cell a depth-only rule
1260 // would have resolved in a handful of splits -- roughly `2^kMaxCubifySplits` leaves along every
1261 // branch instead. A 100 x 100 x 0 slab, subdivided down to the default minSize floor, needs on
1262 // the order of a dozen splits total once x and y are treated as the only axes that matter; this
1263 // asserts the box count stays in that regime rather than climbing towards the ceiling.
1264 O2FlatCSG solid("flat_cell");
1265 double coeff[10];
1266 const double planes[6][2][3] = {
1267 {{1., 0., 0.}, {50., 0., 0.}}, {{-1., 0., 0.}, {-50., 0., 0.}}, {{0., 1., 0.}, {0., 50., 0.}}, {{0., -1., 0.}, {0., -50., 0.}}, {{0., 0., 1.}, {0., 0., 0.}}, {{0., 0., -1.}, {0., 0., 0.}}};
1268 for (const auto& plane : planes) {
1269 planeQuadric(plane[0], plane[1], coeff);
1270 solid.AddQuadric(1., coeff);
1271 }
1272 solid.AddCell(0, 6, 0.);
1273 const double lo[3] = {-50., -50., 0.};
1274 const double hi[3] = {50., 50., 0.};
1275 solid.SetCellBBox(0, lo, hi);
1276 solid.CloseShape();
1277
1278 BOOST_REQUIRE(solid.IsClosed());
1279 // measured 272 boxes with the guard in place; a generous margin above that, and two orders of
1280 // magnitude below what hitting the per-path ceiling on every branch would produce
1281 BOOST_CHECK_LT(solid.GetNboxes(), 600);
1282}
1283
1284BOOST_AUTO_TEST_CASE(a_sidecar_round_trip_reproduces_the_solid)
1285{
1286 O2FlatCSG original("bracket_io");
1287 buildBracket(original);
1288 original.CloseShape();
1289
1290 const std::string path = "testFlatCSG_roundtrip.bin";
1291 BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original)); // test-only writer
1292
1293 O2FlatCSG loaded("bracket_io_loaded");
1294 BOOST_REQUIRE(o2::cad::LoadFlatCSG(path, loaded));
1295 loaded.CloseShape();
1296
1297 BOOST_CHECK_EQUAL(loaded.GetNhalfspaces(), original.GetNhalfspaces());
1298 BOOST_CHECK_EQUAL(loaded.GetNcells(), original.GetNcells());
1299 BOOST_CHECK_EQUAL(loaded.GetNboxes(), original.GetNboxes());
1300 BOOST_CHECK_EQUAL(loaded.Capacity(), original.Capacity());
1301
1302 Rng rng(97531ULL);
1303 for (int trial = 0; trial < 100000; ++trial) {
1304 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1305 BOOST_REQUIRE_EQUAL(loaded.Contains(point), original.Contains(point));
1306 }
1307 std::filesystem::remove(path);
1308}
1309
1310BOOST_AUTO_TEST_CASE(writing_an_unclosed_shape_is_refused)
1311{
1312 // GetCellBBox reads back zeros for a cell whose box was never set -- a finite, non-inverted box
1313 // that would otherwise pass CloseShape's own validation on reload, silently shipping a
1314 // degenerate point-box for that cell. WriteFlatCSG refuses before that invariant can ever reach
1315 // a file: no CloseShape() call at all, and a cell missing a bbox (CloseShape() refused).
1316 const std::string path = "testFlatCSG_unclosed.bin";
1317
1318 O2FlatCSG neverClosed("bracket_never_closed");
1319 buildBracket(neverClosed);
1320 BOOST_REQUIRE(!neverClosed.IsClosed());
1321 BOOST_CHECK(!o2::cad::WriteFlatCSG(path, neverClosed));
1322 BOOST_CHECK(!std::filesystem::exists(path));
1323
1324 O2FlatCSG refused("bracket_refused_close");
1325 double coeff[10];
1326 const double plane[2][3] = {{1., 0., 0.}, {0., 0., 0.}};
1327 planeQuadric(plane[0], plane[1], coeff);
1328 refused.AddQuadric(1., coeff);
1329 refused.AddCell(0, 1, 0.); // no SetCellBBox for this cell -- CloseShape must refuse
1330 refused.CloseShape();
1331 BOOST_REQUIRE(!refused.IsClosed());
1333 BOOST_CHECK(!std::filesystem::exists(path));
1334}
1335
1336BOOST_AUTO_TEST_CASE(a_truncated_sidecar_is_refused_rather_than_half_loaded)
1337{
1338 O2FlatCSG original("bracket_trunc");
1339 buildBracket(original);
1340 original.CloseShape();
1341 const std::string path = "testFlatCSG_truncated.bin";
1342 BOOST_REQUIRE(o2::cad::WriteFlatCSG(path, original));
1343 std::filesystem::resize_file(path, std::filesystem::file_size(path) - 17);
1344
1345 O2FlatCSG loaded("bracket_trunc_loaded");
1347 std::filesystem::remove(path);
1348}
1349
1350BOOST_AUTO_TEST_CASE(the_shape_survives_a_ROOT_file_without_its_sidecar)
1351{
1352 O2FlatCSG original("bracket_root");
1353 buildBracket(original);
1354 original.CloseShape();
1355
1356 const std::string path = "testFlatCSG_shape.root";
1357 {
1358 TFile file(path.c_str(), "RECREATE");
1359 file.WriteObject(&original, "shape");
1360 }
1361 O2FlatCSG* restored = nullptr;
1362 {
1363 TFile file(path.c_str(), "READ");
1364 file.GetObject("shape", restored);
1365 }
1366 BOOST_REQUIRE(restored != nullptr);
1367 restored->CloseShape(); // the BVH is not streamed; it is rebuilt
1368
1369 Rng rng(11223ULL);
1370 for (int trial = 0; trial < 100000; ++trial) {
1371 const double point[3] = {rng.uniform(-13., 13.), rng.uniform(-5., 5.), rng.uniform(-3., 14.)};
1372 BOOST_REQUIRE_EQUAL(restored->Contains(point), original.Contains(point));
1373 }
1374 std::filesystem::remove(path);
1375}
1376
1377namespace
1378{
1380void addBoxAsCell(O2FlatCSG& solid, const double* lo, const double* hi)
1381{
1382 const int first = solid.GetNhalfspaces();
1383 double coeff[10];
1384 for (int axis = 0; axis < 3; ++axis) {
1385 for (int sense = -1; sense <= 1; sense += 2) {
1386 double normal[3] = {0., 0., 0.};
1387 double through[3] = {0., 0., 0.};
1388 normal[axis] = static_cast<double>(sense);
1389 through[axis] = sense > 0 ? hi[axis] : lo[axis];
1390 planeQuadric(normal, through, coeff);
1391 solid.AddQuadric(1., coeff);
1392 }
1393 }
1394 const int cell = solid.AddCell(first, 6, (hi[0] - lo[0]) * (hi[1] - lo[1]) * (hi[2] - lo[2]));
1395 solid.SetCellBBox(cell, lo, hi);
1396}
1397
1401void buildStaggeredChain(O2FlatCSG& solid)
1402{
1403 const double nearLo[3] = {0., -1., -1.};
1404 const double nearHi[3] = {1., 20., 1.};
1405 const double farLo[3] = {5., -1., -1.};
1406 const double farHi[3] = {6., 20., 1.};
1407 const double middleLo[3] = {1., -1., -1.};
1408 const double middleHi[3] = {5., 1., 1.};
1409 addBoxAsCell(solid, nearLo, nearHi);
1410 addBoxAsCell(solid, farLo, farHi);
1411 addBoxAsCell(solid, middleLo, middleHi);
1412 solid.CloseShape();
1413}
1414} // namespace
1415
1416BOOST_AUTO_TEST_CASE(a_far_box_that_extends_the_union_is_recovered_by_the_unpruned_retry)
1417{
1418 O2FlatCSG solid("staggered");
1419 buildStaggeredChain(solid);
1420
1421 // along the chain: the answer is the far cell's exit at x = 6, which the pruned traversal can
1422 // only reach through the middle cell it sees last
1423 const double point[3] = {0.5, 0., 0.};
1424 const double dir[3] = {1., 0., 0.};
1425 O2FlatCSG::ResetUnprunedRetryCounter();
1426 const double distance = solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr);
1427 BOOST_CHECK_EQUAL(distance, solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1428 BOOST_CHECK_CLOSE(distance, 5.5, 1.e-9);
1429 BOOST_CHECK_GT(O2FlatCSG::GetUnprunedRetryCount(), 0);
1430}
1431
1432BOOST_AUTO_TEST_CASE(the_pruned_DistFromInside_is_bit_identical_to_its_twin_on_the_staggered_chain)
1433{
1434 O2FlatCSG solid("staggered_random");
1435 buildStaggeredChain(solid);
1436
1437 Rng rng(97531ULL);
1438 int inside = 0;
1439 for (int trial = 0; trial < 100000; ++trial) {
1440 double point[3] = {rng.uniform(-1., 7.), rng.uniform(-2., 21.), rng.uniform(-2., 2.)};
1441 if (!solid.Contains_Loop(point)) {
1442 continue;
1443 }
1444 double dir[3];
1445 double norm = 0.;
1446 do {
1447 for (int index = 0; index < 3; ++index) {
1448 dir[index] = rng.uniform(-1., 1.);
1449 }
1450 norm = std::sqrt(dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2]);
1451 } while (norm < 1.e-3);
1452 for (int index = 0; index < 3; ++index) {
1453 dir[index] /= norm;
1454 }
1455 ++inside;
1456 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, TGeoShape::Big(), nullptr),
1457 solid.DistFromInside_Loop(point, dir, TGeoShape::Big()));
1458 // a finite step must answer as the twin does with the same step
1459 BOOST_REQUIRE_EQUAL(solid.DistFromInside(point, dir, 3, 2., nullptr),
1460 solid.DistFromInside_Loop(point, dir, 2.));
1461 }
1462 BOOST_CHECK_GT(inside, 1000);
1463}
header::DataOrigin origin
uint32_t side
Definition RawData.h:0
GLdouble n
Definition glcorearb.h:1982
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat s1
Definition glcorearb.h:5034
GLuint index
Definition glcorearb.h:781
GLdouble GLdouble GLdouble GLdouble top
Definition glcorearb.h:4077
GLenum GLuint GLint GLenum face
Definition glcorearb.h:3184
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei GLsizei GLfloat distance
Definition glcorearb.h:5506
GLint reference
Definition glcorearb.h:5487
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLenum GLsizei GLsizei GLint * values
Definition glcorearb.h:1576
GLintptr offset
Definition glcorearb.h:660
GLint GLint bottom
Definition glcorearb.h:1979
GLsizei const GLchar *const * path
Definition glcorearb.h:3591
GLboolean r
Definition glcorearb.h:1233
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLsizei const GLint * box
Definition glcorearb.h:4697
bool LoadFlatCSG(const std::string &file, O2FlatCSG &solid)
Load a flat-CSG sidecar (flatcsg_*.bin, version 1) into solid; call CloseShape() after....
bool WriteFlatCSG(const std::string &file, const O2FlatCSG &solid)
std::map< std::string, ID > expected
BOOST_AUTO_TEST_CASE(box_from_six_planes_contains_like_TGeoBBox)
BOOST_CHECK(tree)
BOOST_CHECK_EQUAL(triggersD.size(), triggers.size())
std::vector< int > row