Project
Loading...
Searching...
No Matches
BoundedSurface.h
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
16
17#ifndef ALICEO2_CADSUPPORT_BOUNDEDSURFACE_H_
18#define ALICEO2_CADSUPPORT_BOUNDEDSURFACE_H_
19
20#include <algorithm>
21#include <array>
22#include <cassert>
23#include <cmath>
24#include <cstdint>
25#include <limits>
26#include <map>
27#include <memory>
28#include <string>
29#include <tuple>
30#include <utility>
31#include <vector>
32
34{
35
38inline constexpr double kTolerance = 1.e-9;
39inline constexpr double kToleranceSq = kTolerance * kTolerance;
40inline constexpr double kAreaTolerance = 1.e-18;
41inline constexpr double kRayTolerance = 1.e-9;
42inline constexpr double kIntersectionTolerance = 1.e-7;
43inline constexpr double kClosureQuantum = 1.e-7;
45inline constexpr double kWireJoinTolerance = 1.e-6;
47inline constexpr double wireJoinToleranceFor(double modelTolerance)
48{
49 return modelTolerance > kWireJoinTolerance ? modelTolerance : kWireJoinTolerance;
50}
52inline constexpr double kBSplineFlatness = 1.e-5;
55inline constexpr double kRimMatchTolerance = 1.e-6;
56
58inline constexpr double kBVHBoxTolerance = 1.e-3;
60inline constexpr double kQuarticEpsilon = 32. * 2.220446049250313e-16;
62
64struct Vec2 {
65 double uCoord = 0.;
66 double vCoord = 0.;
67};
68
70struct Vec3 {
71 double xCoord = 0.;
72 double yCoord = 0.;
73 double zCoord = 0.;
74};
75
76inline Vec3 operator+(const Vec3& firstVector, const Vec3& secondVector)
77{
78 return {firstVector.xCoord + secondVector.xCoord, firstVector.yCoord + secondVector.yCoord,
79 firstVector.zCoord + secondVector.zCoord};
80}
81
82inline Vec3 operator-(const Vec3& firstVector, const Vec3& secondVector)
83{
84 return {firstVector.xCoord - secondVector.xCoord, firstVector.yCoord - secondVector.yCoord,
85 firstVector.zCoord - secondVector.zCoord};
86}
87
88inline Vec3 operator*(const Vec3& vector, double scale)
89{
90 return {vector.xCoord * scale, vector.yCoord * scale, vector.zCoord * scale};
91}
92
93inline Vec3 operator*(double scale, const Vec3& vector)
94{
95 return vector * scale;
96}
97
98inline Vec2 operator-(const Vec2& firstPoint, const Vec2& secondPoint)
99{
100 return {firstPoint.uCoord - secondPoint.uCoord, firstPoint.vCoord - secondPoint.vCoord};
101}
102
104inline double parametricLengthSq(double gUU, double gUV, double gVV, const Vec2& delta)
105{
106 return gUU * delta.uCoord * delta.uCoord + 2. * gUV * delta.uCoord * delta.vCoord +
107 gVV * delta.vCoord * delta.vCoord;
108}
109
112 using Evaluate = void (*)(const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV);
113
115 const void* context = nullptr;
116
118 double lengthSq(const Vec2& uv, const Vec2& delta) const
119 {
120 if (evaluate == nullptr) {
121 return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord;
122 }
123 double gUU = 1.;
124 double gUV = 0.;
125 double gVV = 1.;
126 evaluate(context, uv, gUU, gUV, gVV);
127 return parametricLengthSq(gUU, gUV, gVV, delta);
128 }
129
131 double distanceSq(const Vec2& from, const Vec2& to) const { return lengthSq(from, to - from); }
132
134 double maxScale(const Vec2& uv) const
135 {
136 if (evaluate == nullptr) {
137 return 1.;
138 }
139 double gUU = 1.;
140 double gUV = 0.;
141 double gVV = 1.;
142 evaluate(context, uv, gUU, gUV, gVV);
143 const double trace = gUU + gVV;
144 const double determinant = gUU * gVV - gUV * gUV;
145 // the eigenvalues of a symmetric 2x2 form, guarded against a slightly negative discriminant
146 const double discriminant = std::max(0., trace * trace - 4. * determinant);
147 return std::sqrt(std::max(0., 0.5 * (trace + std::sqrt(discriminant))));
148 }
149};
150
153template <typename Surface>
154inline ParametricMetric parametricMetricOf(const Surface& surface)
155{
156 return {[](const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV) {
157 static_cast<const Surface*>(context)->parametricMetric(uv, gUU, gUV, gVV);
158 },
159 &surface};
160}
161
162inline double dot(const Vec3& firstVector, const Vec3& secondVector)
163{
164 return firstVector.xCoord * secondVector.xCoord + firstVector.yCoord * secondVector.yCoord +
165 firstVector.zCoord * secondVector.zCoord;
166}
167
168inline Vec3 cross(const Vec3& firstVector, const Vec3& secondVector)
169{
170 return {firstVector.yCoord * secondVector.zCoord - firstVector.zCoord * secondVector.yCoord,
171 firstVector.zCoord * secondVector.xCoord - firstVector.xCoord * secondVector.zCoord,
172 firstVector.xCoord * secondVector.yCoord - firstVector.yCoord * secondVector.xCoord};
173}
174
175inline double normSq(const Vec3& vector)
176{
177 return dot(vector, vector);
178}
179
180inline double norm(const Vec3& vector)
181{
182 return std::sqrt(normSq(vector));
183}
184
185inline Vec3 normalized(const Vec3& vector)
186{
187 const double vectorNorm = norm(vector);
188 if (vectorNorm <= kTolerance) {
189 return {};
190 }
191 return vector * (1. / vectorNorm);
192}
193
194inline double component(const Vec3& vector, int dimension)
195{
196 if (dimension == 0) {
197 return vector.xCoord;
198 }
199 if (dimension == 1) {
200 return vector.yCoord;
201 }
202 return vector.zCoord;
203}
204
205inline void assignComponent(Vec3& vector, int dimension, double value)
206{
207 if (dimension == 0) {
208 vector.xCoord = value;
209 } else if (dimension == 1) {
210 vector.yCoord = value;
211 } else {
212 vector.zCoord = value;
213 }
214}
215
216inline bool finite(const Vec2& point)
217{
218 return std::isfinite(point.uCoord) && std::isfinite(point.vCoord);
219}
220
221inline bool finite(const Vec3& point)
222{
223 return std::isfinite(point.xCoord) && std::isfinite(point.yCoord) && std::isfinite(point.zCoord);
224}
225
226inline double distanceSq(const Vec2& firstPoint, const Vec2& secondPoint)
227{
228 const double deltaU = firstPoint.uCoord - secondPoint.uCoord;
229 const double deltaV = firstPoint.vCoord - secondPoint.vCoord;
230 return deltaU * deltaU + deltaV * deltaV;
231}
232
233inline double distanceSq(const Vec3& firstPoint, const Vec3& secondPoint)
234{
235 return normSq(firstPoint - secondPoint);
236}
237
238inline double cross2D(const Vec2& firstVector, const Vec2& secondVector)
239{
240 return firstVector.uCoord * secondVector.vCoord - firstVector.vCoord * secondVector.uCoord;
241}
242
243inline double pointSegmentDistanceSq(const Vec2& point, const Vec2& segmentStart, const Vec2& segmentEnd)
244{
245 const Vec2 segmentVector = segmentEnd - segmentStart;
246 const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
247 if (segmentLengthSq <= kToleranceSq) {
248 return distanceSq(point, segmentStart);
249 }
250 const double pointProjection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
251 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
252 segmentLengthSq;
253 const double clampedProjection = std::max(0., std::min(1., pointProjection));
254 const Vec2 closestPoint{segmentStart.uCoord + clampedProjection * segmentVector.uCoord,
255 segmentStart.vCoord + clampedProjection * segmentVector.vCoord};
256 return distanceSq(point, closestPoint);
257}
258
259inline double pointSegmentDistanceSq(const Vec3& point, const Vec3& segmentStart, const Vec3& segmentEnd)
260{
261 const Vec3 segmentVector = segmentEnd - segmentStart;
262 const double segmentLengthSq = normSq(segmentVector);
263 if (segmentLengthSq <= kToleranceSq) {
264 return distanceSq(point, segmentStart);
265 }
266 const double pointProjection = dot(point - segmentStart, segmentVector) / segmentLengthSq;
267 const double clampedProjection = std::max(0., std::min(1., pointProjection));
268 const Vec3 closestPoint = segmentStart + segmentVector * clampedProjection;
269 return distanceSq(point, closestPoint);
270}
271
274
277inline void planeParametricMetric(const Vec3& axisU, const Vec3& axisV, double& gUU, double& gUV, double& gVV)
278{
279 gUU = dot(axisU, axisU);
280 gUV = dot(axisU, axisV);
281 gVV = dot(axisV, axisV);
282}
283
285inline void cylinderParametricMetric(double radius, double& gUU, double& gUV, double& gVV)
286{
287 gUU = radius * radius;
288 gUV = 0.;
289 gVV = 1.;
290}
291
294inline void coneParametricMetric(double radiusAtHeight, double slope, double& gUU, double& gUV, double& gVV)
295{
296 gUU = radiusAtHeight * radiusAtHeight;
297 gUV = 0.;
298 gVV = 1. + slope * slope;
299}
300
303inline void sphereParametricMetric(double radius, double theta, double& gUU, double& gUV, double& gVV)
304{
305 const double parallelRadius = radius * std::sin(theta);
306 gUU = parallelRadius * parallelRadius;
307 gUV = 0.;
308 gVV = radius * radius;
309}
310
312inline void torusParametricMetric(double majorRadius, double minorRadius, double phiTube, double& gUU, double& gUV,
313 double& gVV)
314{
315 const double ringRadius = majorRadius + minorRadius * std::cos(phiTube);
316 gUU = ringRadius * ringRadius;
317 gUV = 0.;
318 gVV = minorRadius * minorRadius;
319}
321
322inline bool sameIntersection(double firstDistance, double secondDistance)
323{
324 return std::abs(firstDistance - secondDistance) <=
325 kIntersectionTolerance * std::max(1., std::max(std::abs(firstDistance), std::abs(secondDistance)));
326}
327
329struct RayHit {
330 double distance = 0.;
333 bool onTrimBoundary = false;
334};
335
340
341 Vec2 direction() const { return end - start; }
342
343 double lengthSq() const
344 {
345 const Vec2 delta = end - start;
346 return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord;
347 }
348
349 bool degenerate() const { return lengthSq() <= kToleranceSq; }
350
352 double distanceSq(const Vec2& point) const { return pointSegmentDistanceSq(point, start, end); }
353
356 Vec2 closestPoint(const Vec2& point, double& parameter) const
357 {
358 const Vec2 segmentVector = end - start;
359 const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord +
360 segmentVector.vCoord * segmentVector.vCoord;
361 if (segmentLengthSq <= kToleranceSq) {
362 parameter = 0.;
363 return start;
364 }
365 const double projection = ((point.uCoord - start.uCoord) * segmentVector.uCoord +
366 (point.vCoord - start.vCoord) * segmentVector.vCoord) /
367 segmentLengthSq;
368 parameter = std::max(0., std::min(1., projection));
369 return {start.uCoord + parameter * segmentVector.uCoord, start.vCoord + parameter * segmentVector.vCoord};
370 }
371
374 {
375 lower.uCoord = std::min({lower.uCoord, start.uCoord, end.uCoord});
376 lower.vCoord = std::min({lower.vCoord, start.vCoord, end.vCoord});
377 upper.uCoord = std::max({upper.uCoord, start.uCoord, end.uCoord});
378 upper.vCoord = std::max({upper.vCoord, start.vCoord, end.vCoord});
379 }
380};
381
384 Boundary,
385 Inside };
386
389enum class WireRole { Outer,
390 Inner };
391
394enum class WireStatus {
395 Valid,
396 Reversed,
397 NonFinite,
398 Open,
401 ZeroArea
402};
403
405inline const char* wireStatusMessage(WireStatus status)
406{
407 switch (status) {
409 return "valid";
411 return "orientation normalized to match wire role";
413 return "wire contains a non-finite vertex";
414 case WireStatus::Open:
415 return "wire edges do not form a closed loop";
417 return "wire needs at least three distinct vertices";
419 return "wire has a coincident (pinched) vertex";
421 return "wire has zero area";
422 }
423 return "unknown wire status";
424}
425
427inline double trimLengthFloor(const ParametricMetric& metric, const Vec2& uv)
428{
429 const double scale = metric.maxScale(uv);
430 return scale > kTolerance ? kTolerance / scale : 0.;
431}
432
435 std::vector<Vec2> vertices;
437
439 std::vector<int> sourceEdge;
440
441 int edgeCount() const { return static_cast<int>(vertices.size()); }
442
444 int storedIndexOfSource(int inputIndex) const
445 {
446 for (size_t index = 0; index < sourceEdge.size(); ++index) {
447 if (sourceEdge[index] == inputIndex) {
448 return static_cast<int>(index);
449 }
450 }
451 return -1;
452 }
453
455 {
456 const int count = edgeCount();
457 return {vertices[index % count], vertices[(index + 1) % count]};
458 }
459
461 bool initialize(const std::vector<Vec2>& inputVertices, WireRole wireRole, WireStatus& status,
462 const ParametricMetric& metric = {})
463 {
464 role = wireRole;
465 vertices.clear();
466 vertices.reserve(inputVertices.size());
467 bool droppedAVertex = false;
468
469 for (const auto& vertex : inputVertices) {
470 if (!finite(vertex)) {
471 status = WireStatus::NonFinite;
472 return false;
473 }
474 if (vertices.empty() || metric.distanceSq(vertices.back(), vertex) > kToleranceSq) {
475 vertices.push_back(vertex);
476 } else {
477 droppedAVertex = true;
478 }
479 }
480
481 // drop an explicit closing duplicate (first == last)
482 if (vertices.size() > 1 && metric.distanceSq(vertices.front(), vertices.back()) <= kToleranceSq) {
483 vertices.pop_back();
484 droppedAVertex = true;
485 }
486
487 if (vertices.size() < 3) {
489 return false;
490 }
491
492 // reject self-touching loops (non-adjacent coincident vertices)
493 for (size_t firstIndex = 0; firstIndex < vertices.size(); ++firstIndex) {
494 for (size_t secondIndex = firstIndex + 1; secondIndex < vertices.size(); ++secondIndex) {
495 if (metric.distanceSq(vertices[firstIndex], vertices[secondIndex]) <= kToleranceSq) {
497 return false;
498 }
499 }
500 }
501
502 const double area = signedArea();
503 if (std::abs(area) <= kAreaTolerance) {
504 status = WireStatus::ZeroArea;
505 return false;
506 }
507
508 // segment i is input segment i unless a vertex was dropped; then it is unknown
509 const int storedCount = static_cast<int>(vertices.size());
510 sourceEdge.assign(static_cast<size_t>(storedCount), -1);
511 if (!droppedAVertex) {
512 for (int index = 0; index < storedCount; ++index) {
513 sourceEdge[static_cast<size_t>(index)] = index;
514 }
515 }
516
517 // outer wires must wind CCW (positive area), inner wires CW (negative area)
518 const bool wantPositiveArea = (role == WireRole::Outer);
519 if ((area > 0.) != wantPositiveArea) {
520 std::reverse(vertices.begin(), vertices.end());
521 // reversing the ring maps old vertex k to new index n-1-k, so new segment j spans old
522 // vertices n-1-j and n-2-j, i.e. it is old segment n-2-j traversed backwards
523 std::vector<int> reversedSource(static_cast<size_t>(storedCount), -1);
524 for (int index = 0; index < storedCount; ++index) {
525 reversedSource[static_cast<size_t>(index)] =
526 sourceEdge[static_cast<size_t>((storedCount - 2 - index % storedCount + 2 * storedCount) % storedCount)];
527 }
528 sourceEdge.swap(reversedSource);
529 status = WireStatus::Reversed;
530 return true;
531 }
532
533 status = WireStatus::Valid;
534 return true;
535 }
536
538 bool initializeFromEdges(const std::vector<SurfaceEdge>& edges, WireRole wireRole, WireStatus& status,
539 const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance)
540 {
541 if (edges.size() < 3) {
543 return false;
544 }
545 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
546 if (!finite(edges[edgeIndex].start) || !finite(edges[edgeIndex].end)) {
547 status = WireStatus::NonFinite;
548 return false;
549 }
550 const Vec2& nextStart = edges[(edgeIndex + 1) % edges.size()].start;
551 if (metric.distanceSq(edges[edgeIndex].end, nextStart) > joinTolerance * joinTolerance) {
552 status = WireStatus::Open;
553 return false;
554 }
555 }
556
557 std::vector<Vec2> ringVertices;
558 ringVertices.reserve(edges.size());
559 for (const auto& singleEdge : edges) {
560 ringVertices.push_back(singleEdge.start);
561 }
562 return initialize(ringVertices, wireRole, status, metric);
563 }
564
565 double signedArea() const
566 {
567 double area = 0.;
568 for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) {
569 const auto& currentVertex = vertices[vertexIndex];
570 const auto& nextVertex = vertices[(vertexIndex + 1) % vertices.size()];
571 area += currentVertex.uCoord * nextVertex.vCoord - nextVertex.uCoord * currentVertex.vCoord;
572 }
573 return 0.5 * area;
574 }
575
579 {
580 for (const auto& vertex : vertices) {
581 lower.uCoord = std::min(lower.uCoord, vertex.uCoord);
582 lower.vCoord = std::min(lower.vCoord, vertex.vCoord);
583 upper.uCoord = std::max(upper.uCoord, vertex.uCoord);
584 upper.vCoord = std::max(upper.vCoord, vertex.vCoord);
585 }
586 }
587
589 std::vector<Vec2> sampledBoundary() const
590 {
591 std::vector<Vec2> samples;
592 if (vertices.empty()) {
593 return samples;
594 }
595 samples.reserve(vertices.size() + 1);
596 samples.insert(samples.end(), vertices.begin(), vertices.end());
597 samples.push_back(vertices.front());
598 return samples;
599 }
600
602 WireClassification classify(const Vec2& point, double band) const
603 {
604 const double bandSq = band * band;
605 bool inside = false;
606 for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) {
607 const auto& segmentStart = vertices[vertexIndex];
608 const auto& segmentEnd = vertices[(vertexIndex + 1) % vertices.size()];
609 if (pointSegmentDistanceSq(point, segmentStart, segmentEnd) <= bandSq) {
611 }
612 const bool crossesScanline = (segmentStart.vCoord > point.vCoord) != (segmentEnd.vCoord > point.vCoord);
613 if (crossesScanline) {
614 const double intersectionU = segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) *
615 (segmentEnd.uCoord - segmentStart.uCoord) /
616 (segmentEnd.vCoord - segmentStart.vCoord);
617 if (point.uCoord < intersectionU) {
618 inside = !inside;
619 }
620 }
621 }
623 }
624
626 WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const
627 {
628 return classify(point, trimLengthFloor(metric, point));
629 }
630};
631
632inline bool pointInTriangle(const Vec2& point, const Vec2& firstVertex, const Vec2& secondVertex,
633 const Vec2& thirdVertex)
634{
635 const double firstCross = cross2D(secondVertex - firstVertex, point - firstVertex);
636 const double secondCross = cross2D(thirdVertex - secondVertex, point - secondVertex);
637 const double thirdCross = cross2D(firstVertex - thirdVertex, point - thirdVertex);
638 return firstCross >= -kTolerance && secondCross >= -kTolerance && thirdCross >= -kTolerance;
639}
640
642inline std::vector<std::array<int, 3>> triangulateSimpleWire(const SurfaceWire& wire)
643{
644 std::vector<int> remainingIndices;
645 remainingIndices.reserve(wire.vertices.size());
646 if (wire.signedArea() >= 0.) {
647 for (size_t vertexIndex = 0; vertexIndex < wire.vertices.size(); ++vertexIndex) {
648 remainingIndices.push_back(static_cast<int>(vertexIndex));
649 }
650 } else {
651 for (size_t reverseIndex = wire.vertices.size(); reverseIndex > 0; --reverseIndex) {
652 remainingIndices.push_back(static_cast<int>(reverseIndex - 1));
653 }
654 }
655
656 std::vector<std::array<int, 3>> triangles;
657 size_t guardCounter = 0;
658 while (remainingIndices.size() > 3 && guardCounter++ < wire.vertices.size() * wire.vertices.size()) {
659 bool clippedEar = false;
660 for (size_t indexPosition = 0; indexPosition < remainingIndices.size(); ++indexPosition) {
661 const int previousIndex = remainingIndices[(indexPosition + remainingIndices.size() - 1) % remainingIndices.size()];
662 const int currentIndex = remainingIndices[indexPosition];
663 const int nextIndex = remainingIndices[(indexPosition + 1) % remainingIndices.size()];
664
665 const auto& previousVertex = wire.vertices[previousIndex];
666 const auto& currentVertex = wire.vertices[currentIndex];
667 const auto& nextVertex = wire.vertices[nextIndex];
668 if (cross2D(currentVertex - previousVertex, nextVertex - currentVertex) <= kTolerance) {
669 continue;
670 }
671
672 bool containsOtherVertex = false;
673 for (int candidateIndex : remainingIndices) {
674 if (candidateIndex == previousIndex || candidateIndex == currentIndex || candidateIndex == nextIndex) {
675 continue;
676 }
677 if (pointInTriangle(wire.vertices[candidateIndex], previousVertex, currentVertex, nextVertex)) {
678 containsOtherVertex = true;
679 break;
680 }
681 }
682 if (containsOtherVertex) {
683 continue;
684 }
685
686 triangles.push_back({previousIndex, currentIndex, nextIndex});
687 remainingIndices.erase(remainingIndices.begin() + indexPosition);
688 clippedEar = true;
689 break;
690 }
691
692 if (!clippedEar) {
693 break;
694 }
695 }
696
697 if (remainingIndices.size() == 3) {
698 triangles.push_back({remainingIndices[0], remainingIndices[1], remainingIndices[2]});
699 }
700 return triangles;
701}
702
705inline constexpr double kPi = 3.14159265358979323846;
706inline constexpr double kTwoPi = 2. * kPi;
707inline constexpr double kHalfPi = 0.5 * kPi;
709inline constexpr int kArcSamples = 24;
711
713inline double angularTolerance(double radius)
714{
715 return kTolerance / std::max(radius, kTolerance);
716}
717
719inline constexpr double kCoverChunkAngle = kPi / 4.;
720
723inline int coverChunkCount(double span)
724{
725 constexpr int fullTurnChunks = static_cast<int>(kTwoPi / kCoverChunkAngle); // eight
726 return std::max(1, std::min(fullTurnChunks, static_cast<int>(std::ceil(span / kCoverChunkAngle))));
727}
728
730inline void sinusoidRange(double a, double b, double t0, double t1, double& minimum, double& maximum)
731{
732 const double atStart = a * std::cos(t0) + b * std::sin(t0);
733 const double atEnd = a * std::cos(t1) + b * std::sin(t1);
734 minimum = std::min(atStart, atEnd);
735 maximum = std::max(atStart, atEnd);
736 const double amplitude = std::hypot(a, b);
737 const double crest = std::atan2(b, a);
738 // shifted into [t0, t0 + 2pi), where a span of at most a full turn makes "<= t1" exactly the
739 // test for falling inside the interval
740 const double crestInRange = crest - kTwoPi * std::floor((crest - t0) / kTwoPi);
741 if (crestInRange <= t1) {
742 maximum = amplitude;
743 }
744 const double trough = crest + kPi;
745 const double troughInRange = trough - kTwoPi * std::floor((trough - t0) / kTwoPi);
746 if (troughInRange <= t1) {
747 minimum = -amplitude;
748 }
749}
750
753inline double sinusoidMinimum(double a, double b, double t0, double t1)
754{
755 double minimum = 0.;
756 double maximum = 0.;
757 sinusoidRange(a, b, t0, t1, minimum, maximum);
758 return minimum;
759}
760
761inline double sinusoidMaximum(double a, double b, double t0, double t1)
762{
763 double minimum = 0.;
764 double maximum = 0.;
765 sinusoidRange(a, b, t0, t1, minimum, maximum);
766 return maximum;
767}
769
772inline bool angleInSweepRange(double angle, double start, double sweep, double tolerance)
773{
774 if (sweep >= kTwoPi - kTolerance) {
775 return true;
776 }
777 double delta = angle - start;
778 delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi)
779 return delta <= sweep + tolerance || delta >= kTwoPi - tolerance;
780}
781
783inline void gaussLegendre(int n, std::vector<double>& nodes, std::vector<double>& weights)
784{
785 nodes.assign(std::max(n, 1), 0.);
786 weights.assign(std::max(n, 1), 0.);
787 if (n < 1) {
788 return;
789 }
790 for (int i = 0; i < n; ++i) {
791 double root = std::cos(kPi * (i + 0.75) / (n + 0.5)); // asymptotic initial guess
792 double derivative = 1.;
793 for (int iteration = 0; iteration < 100; ++iteration) {
794 double previous = 1.;
795 double current = root;
796 for (int degreeIndex = 2; degreeIndex <= n; ++degreeIndex) {
797 const double next = ((2 * degreeIndex - 1) * root * current - (degreeIndex - 1) * previous) / degreeIndex;
798 previous = current;
799 current = next;
800 }
801 derivative = n * (root * current - previous) / (root * root - 1.);
802 const double delta = current / derivative;
803 root -= delta;
804 if (std::abs(delta) < 1.e-15) {
805 break;
806 }
807 }
808 nodes[i] = root;
809 weights[i] = 2. / ((1. - root * root) * derivative * derivative);
810 }
811}
812
815inline int solveDepressedCubic(double coeffP, double coeffQ, std::array<double, 3>& roots)
816{
817 const double discriminant = coeffQ * coeffQ / 4. + coeffP * coeffP * coeffP / 27.;
818 if (!(coeffP < 0.) || discriminant > 0.) {
819 const double sqrtDiscriminant = std::sqrt(std::max(0., discriminant));
820 roots[0] = std::cbrt(-0.5 * coeffQ + sqrtDiscriminant) + std::cbrt(-0.5 * coeffQ - sqrtDiscriminant);
821 return 1;
822 }
823 // three real roots: coeffP < 0 here, so the trigonometric form is well defined
824 const double magnitude = 2. * std::sqrt(-coeffP / 3.);
825 const double cosineArgument = std::max(-1., std::min(1., 3. * coeffQ / (coeffP * magnitude)));
826 const double baseAngle = std::acos(cosineArgument);
827 for (int branch = 0; branch < 3; ++branch) {
828 roots[branch] = magnitude * std::cos((baseAngle - kTwoPi * branch) / 3.);
829 }
830 return 3;
831}
832
834enum class QuarticBranch {
837 Resolvent
838};
839
842 std::array<double, 4> value{};
843 int count = 0;
844 void push_back(double root)
845 {
846 assert(count < 4 && "QuarticRoots holds at most four roots");
847 value[count++] = root;
848 }
849 double* begin() { return value.data(); }
850 double* end() { return value.data() + count; }
851 const double* begin() const { return value.data(); }
852 const double* end() const { return value.data() + count; }
853 size_t size() const { return static_cast<size_t>(count); }
854 bool empty() const { return count == 0; }
855 double operator[](size_t index) const { return value[index]; }
856};
857
860inline QuarticRoots solveQuarticReal(double a4, double a3, double a2, double a1, double a0,
861 QuarticBranch* takenBranch = nullptr)
862{
863 const auto note = [takenBranch](QuarticBranch branch) {
864 if (takenBranch) {
865 *takenBranch = branch;
866 }
867 };
869 QuarticRoots roots;
870 // A genuine quartic needs only a non-zero leading coefficient. There is no scale to compare it
871 // against -- the normalisation below handles any coefficient ratio -- so the test is exact.
872 if (!(std::abs(a4) > 0.)) {
873 return roots; // the torus caller guarantees a4 = |dir|^4 > 0
874 }
875 // monic x^4 + b x^3 + c x^2 + d x + e
876 double coeffB = a3 / a4, coeffC = a2 / a4, coeffD = a1 / a4, coeffE = a0 / a4;
877 if (!std::isfinite(coeffB) || !std::isfinite(coeffC) || !std::isfinite(coeffD) || !std::isfinite(coeffE)) {
878 return roots; // a4 is denormal-small next to the rest, or an input was not finite
879 }
880 // Cauchy root bound rounded up to a power of two, so x = scale * y is exact; x^4 = 0 keeps scale = 1
881 const double rootBound = std::max({std::abs(coeffB), std::sqrt(std::abs(coeffC)),
882 std::cbrt(std::abs(coeffD)), std::sqrt(std::sqrt(std::abs(coeffE)))});
883 int boundExponent = 0;
884 std::frexp(rootBound, &boundExponent);
885 const double scale = std::ldexp(1., boundExponent);
886 coeffB /= scale;
887 coeffC /= scale * scale;
888 coeffD /= scale * scale * scale;
889 coeffE /= scale * scale * scale * scale;
890
891 // depress with y = z - b/4: z^4 + p z^2 + q z + r
892 const double termP = coeffC - 3. * coeffB * coeffB / 8.;
893 const double termQ = coeffD - coeffB * coeffC / 2. + coeffB * coeffB * coeffB / 8.;
894 const double termR =
895 coeffE - coeffB * coeffD / 4. + coeffB * coeffB * coeffC / 16. - 3. * coeffB * coeffB * coeffB * coeffB / 256.;
896 const double shift = -coeffB / 4.;
897
898 auto addQuadraticRoots = [&](double quadB, double quadC) {
899 const double discriminant = quadB * quadB - 4. * quadC;
900 if (discriminant < 0.) {
901 return; // complex pair
902 }
903 const double sqrtDiscriminant = std::sqrt(discriminant);
904 roots.push_back(shift + 0.5 * (-quadB - sqrtDiscriminant));
905 roots.push_back(shift + 0.5 * (-quadB + sqrtDiscriminant));
906 };
907
908 auto addBiquadraticRoots = [&]() {
909 // biquadratic z^4 + p z^2 + r = 0
910 const double discriminant = termP * termP - 4. * termR;
911 if (discriminant < 0.) {
912 return;
913 }
914 const double sqrtDiscriminant = std::sqrt(discriminant);
915 for (const double zSquared : {0.5 * (-termP + sqrtDiscriminant), 0.5 * (-termP - sqrtDiscriminant)}) {
916 if (zSquared >= 0.) {
917 const double z = std::sqrt(zSquared);
918 roots.push_back(shift + z);
919 roots.push_back(shift - z);
920 }
921 }
922 };
923
924 // q is zero to the precision of its terms, which normalisation bounds by 1: kQuarticEpsilon over the whole quartic, not over q's terms
925 bool biquadratic = std::abs(termQ) <= kQuarticEpsilon;
926 if (!biquadratic) {
928 // resolvent cubic m^3 + p m^2 + (p^2/4 - r) m - q^2/8 = 0; its largest real root is > 0
929 const double cubicA2 = termP;
930 const double cubicA1 = termP * termP / 4. - termR;
931 const double cubicA0 = -termQ * termQ / 8.;
932 const double cubicP = cubicA1 - cubicA2 * cubicA2 / 3.;
933 const double cubicQ = 2. * cubicA2 * cubicA2 * cubicA2 / 27. - cubicA2 * cubicA1 / 3. + cubicA0;
934 std::array<double, 3> cubicRoots;
935 const int cubicCount = solveDepressedCubic(cubicP, cubicQ, cubicRoots);
936 double resolvent = 0.;
937 for (int index = 0; index < cubicCount; ++index) {
938 resolvent = std::max(resolvent, cubicRoots[index] - cubicA2 / 3.);
939 }
940 // a resolvent below the resolution of its cubic is noise; then the biquadratic branch is the better-conditioned answer
941 const double resolventScale = std::max({std::abs(cubicA2), std::sqrt(std::abs(cubicA1)),
942 std::cbrt(std::abs(cubicA0))});
943 if (resolvent > kQuarticEpsilon * resolventScale) {
944 const double sqrtTwoResolvent = std::sqrt(2. * resolvent);
945 const double linearTerm = sqrtTwoResolvent * termQ / (4. * resolvent);
946 addQuadraticRoots(-sqrtTwoResolvent, termP / 2. + resolvent + linearTerm);
947 addQuadraticRoots(sqrtTwoResolvent, termP / 2. + resolvent - linearTerm);
948 } else {
949 biquadratic = true;
950 }
951 }
952 if (biquadratic) {
954 addBiquadraticRoots();
955 }
956
957 // Newton polish against the monic quartic; a step longer than the Cauchy bound 2, or non-finite, is rejected
958 auto quartic = [&](double x) { return (((x + coeffB) * x + coeffC) * x + coeffD) * x + coeffE; };
959 auto quarticDerivative = [&](double x) { return ((4. * x + 3. * coeffB) * x + 2. * coeffC) * x + coeffD; };
960 for (double& root : roots) {
961 for (int iteration = 0; iteration < 2; ++iteration) {
962 const double step = quartic(root) / quarticDerivative(root);
963 if (std::isfinite(step) && std::abs(step) <= 2.) {
964 root -= step;
965 }
966 }
967 }
968 for (double& root : roots) {
969 root *= scale; // exact: scale is a power of two
970 }
971 return roots;
972}
973
975enum class CurveKind { Line,
976 Arc,
977 BSpline
978};
979
981struct Curve2D {
986 double radius = 0.;
987 double startAngle = 0.;
988 double endAngle = 0.;
989
991 int degree = 0;
992 std::vector<Vec2> poles;
993 std::vector<double> weights;
994 std::vector<double> knots;
996 mutable std::vector<Vec2> bsplineCache;
998
1004
1006 {
1008 canonicalEnd = end;
1009 hasCanonicalEndpoints = true;
1010 bsplineCache.clear(); // the polyline carries them, so it has to be rebuilt
1011 }
1012
1018
1019 static Curve2D makeLine(const Vec2& start, const Vec2& end)
1020 {
1021 Curve2D curve;
1022 curve.kind = CurveKind::Line;
1023 curve.lineStart = start;
1024 curve.lineEnd = end;
1025 return curve;
1026 }
1027
1028 static Curve2D makeArc(const Vec2& arcCenter, double arcRadius, double arcStartAngle, double arcEndAngle)
1029 {
1030 Curve2D curve;
1031 curve.kind = CurveKind::Arc;
1032 curve.center = arcCenter;
1033 curve.radius = arcRadius;
1034 curve.startAngle = arcStartAngle;
1035 curve.endAngle = arcEndAngle;
1036 return curve;
1037 }
1038
1040 static Curve2D makeCircle(const Vec2& arcCenter, double arcRadius, bool clockwise = false)
1041 {
1042 return makeArc(arcCenter, arcRadius, 0., clockwise ? -kTwoPi : kTwoPi);
1043 }
1044
1047 static Curve2D makeBSpline(int splineDegree, std::vector<Vec2> splinePoles,
1048 std::vector<double> splineWeights, std::vector<double> splineKnots)
1049 {
1050 Curve2D curve;
1051 curve.kind = CurveKind::BSpline;
1052 curve.degree = splineDegree;
1053 curve.poles = std::move(splinePoles);
1054 curve.weights = std::move(splineWeights);
1055 curve.knots = std::move(splineKnots);
1056 return curve;
1057 }
1058
1059 bool isArc() const { return kind == CurveKind::Arc; }
1060 bool isBSpline() const { return kind == CurveKind::BSpline; }
1061
1062 double sweep() const { return endAngle - startAngle; }
1063
1066 double bsplineT0() const { return knots[degree]; }
1067 double bsplineT1() const { return knots[poles.size()]; }
1068
1070 bool bsplineIsClamped() const
1071 {
1072 const size_t lastKnot = knots.size() - 1;
1073 for (int offset = 1; offset <= degree; ++offset) {
1074 if (std::abs(knots[offset] - knots[0]) > kTolerance ||
1075 std::abs(knots[lastKnot - offset] - knots[lastKnot]) > kTolerance) {
1076 return false;
1077 }
1078 }
1079 return true;
1080 }
1081
1083 bool bsplineRational() const
1084 {
1085 for (double weight : weights) {
1086 if (std::abs(weight - 1.) > kTolerance) {
1087 return true;
1088 }
1089 }
1090 return false;
1091 }
1092
1094 int bsplineSpan(double knotValue) const
1095 {
1096 const int lastPole = static_cast<int>(poles.size()) - 1;
1097 if (knotValue >= knots[lastPole + 1]) {
1098 return lastPole;
1099 }
1100 if (knotValue <= knots[degree]) {
1101 return degree;
1102 }
1103 int low = degree;
1104 int high = lastPole + 1;
1105 int mid = (low + high) / 2;
1106 while (knotValue < knots[mid] || knotValue >= knots[mid + 1]) {
1107 if (knotValue < knots[mid]) {
1108 high = mid;
1109 } else {
1110 low = mid;
1111 }
1112 mid = (low + high) / 2;
1113 }
1114 return mid;
1115 }
1116
1118 void bsplineBasis(int span, double knotValue, std::vector<double>& basis,
1119 std::vector<double>& basisDeriv) const
1120 {
1121 const int p = degree;
1122 std::vector<std::vector<double>> ndu(p + 1, std::vector<double>(p + 1, 0.));
1123 std::vector<double> left(p + 1, 0.);
1124 std::vector<double> right(p + 1, 0.);
1125 ndu[0][0] = 1.;
1126 for (int j = 1; j <= p; ++j) {
1127 left[j] = knotValue - knots[span + 1 - j];
1128 right[j] = knots[span + j] - knotValue;
1129 double saved = 0.;
1130 for (int r = 0; r < j; ++r) {
1131 ndu[j][r] = right[r + 1] + left[j - r];
1132 const double temp = ndu[r][j - 1] / ndu[j][r];
1133 ndu[r][j] = saved + right[r + 1] * temp;
1134 saved = left[j - r] * temp;
1135 }
1136 ndu[j][j] = saved;
1137 }
1138 basis.assign(p + 1, 0.);
1139 basisDeriv.assign(p + 1, 0.);
1140 for (int j = 0; j <= p; ++j) {
1141 basis[j] = ndu[j][p];
1142 }
1143 // first derivative (specialization of DersBasisFuns for the k = 1 term)
1144 for (int r = 0; r <= p; ++r) {
1145 double d = 0.;
1146 const int pk = p - 1;
1147 if (r >= 1) {
1148 d += (1. / ndu[pk + 1][r - 1]) * ndu[r - 1][pk];
1149 }
1150 if (r <= pk) {
1151 d += (-1. / ndu[pk + 1][r]) * ndu[r][pk];
1152 }
1153 basisDeriv[r] = d * p;
1154 }
1155 }
1156
1159 void bsplineEval(double knotValue, Vec2& pointOut, Vec2& derivativeOut) const
1160 {
1161 const int p = degree;
1162 const int span = bsplineSpan(knotValue);
1163 std::vector<double> basis;
1164 std::vector<double> basisDeriv;
1165 bsplineBasis(span, knotValue, basis, basisDeriv);
1166 Vec2 weightedSum{0., 0.};
1167 Vec2 weightedDeriv{0., 0.};
1168 double weightTotal = 0.;
1169 double weightDeriv = 0.;
1170 for (int j = 0; j <= p; ++j) {
1171 const int idx = span - p + j;
1172 const double weight = weights.empty() ? 1. : weights[idx];
1173 weightedSum.uCoord += basis[j] * weight * poles[idx].uCoord;
1174 weightedSum.vCoord += basis[j] * weight * poles[idx].vCoord;
1175 weightTotal += basis[j] * weight;
1176 weightedDeriv.uCoord += basisDeriv[j] * weight * poles[idx].uCoord;
1177 weightedDeriv.vCoord += basisDeriv[j] * weight * poles[idx].vCoord;
1178 weightDeriv += basisDeriv[j] * weight;
1179 }
1180 const double invWeight = (std::abs(weightTotal) > kTolerance) ? 1. / weightTotal : 0.;
1181 pointOut = {weightedSum.uCoord * invWeight, weightedSum.vCoord * invWeight};
1182 derivativeOut = {(weightedDeriv.uCoord * weightTotal - weightedSum.uCoord * weightDeriv) * invWeight * invWeight,
1183 (weightedDeriv.vCoord * weightTotal - weightedSum.vCoord * weightDeriv) * invWeight * invWeight};
1184 }
1185
1187 Vec2 bsplinePointAt(double parameter) const
1188 {
1189 const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0());
1190 Vec2 point;
1191 Vec2 derivative;
1192 bsplineEval(knotValue, point, derivative);
1193 return point;
1194 }
1195
1197 void bsplineSampleInto(std::vector<Vec2>& samples, double flatnessSq = kBSplineFlatnessSq,
1198 int maxDepth = 16) const
1199 {
1200 const double t0 = bsplineT0();
1201 const double t1 = bsplineT1();
1202 Vec2 startPointValue;
1203 Vec2 endPointValue;
1204 Vec2 unusedDerivative;
1205 bsplineEval(t0, startPointValue, unusedDerivative);
1206 bsplineEval(t1, endPointValue, unusedDerivative);
1207 samples.push_back(startPointValue);
1208 bsplineSampleRecursive(t0, t1, startPointValue, endPointValue, flatnessSq, maxDepth, samples);
1209 }
1210
1212 bool spansInteriorKnot(double lowT, double highT) const
1213 {
1214 // a clamped knot vector repeats its ends degree+1 times, so the interior knots are the
1215 // entries [degree + 1, poles.size()); a single-span (Bezier) curve has none
1216 const size_t firstInterior = static_cast<size_t>(degree) + 1;
1217 const size_t endInterior = std::min(poles.size(), knots.size());
1218 if (firstInterior >= endInterior) {
1219 return false;
1220 }
1221 const auto begin = knots.begin() + static_cast<std::ptrdiff_t>(firstInterior);
1222 const auto end = knots.begin() + static_cast<std::ptrdiff_t>(endInterior);
1223 const auto firstAbove = std::upper_bound(begin, end, lowT);
1224 return firstAbove != end && *firstAbove < highT;
1225 }
1226
1228 void appendInteriorKnots(double from, double to, std::vector<double>& breakpoints) const
1229 {
1230 if (kind != CurveKind::BSpline) {
1231 return;
1232 }
1233 const double t0 = bsplineT0();
1234 const double span = bsplineT1() - t0;
1235 if (!(span > 0.)) {
1236 return;
1237 }
1238 const size_t firstInterior = static_cast<size_t>(degree) + 1;
1239 const size_t endInterior = std::min(poles.size(), knots.size());
1240 for (size_t index = firstInterior; index < endInterior; ++index) {
1241 const double parameter = (knots[index] - t0) / span;
1242 if (parameter > from && parameter < to) {
1243 breakpoints.push_back(parameter);
1244 }
1245 }
1246 }
1247
1249 double uVariation(double from, double to) const
1250 {
1251 if (kind == CurveKind::Line) {
1252 return std::abs(lineEnd.uCoord - lineStart.uCoord) * std::abs(to - from);
1253 }
1254 if (kind == CurveKind::BSpline) {
1255 // within one knot span the curve lies in the hull of its degree + 1 poles, so their u spread bounds the travel
1256 const double knotStart = bsplineT0();
1257 const double knotSpan = bsplineT1() - knotStart;
1258 const double knotMid = knotStart + 0.5 * (from + to) * knotSpan;
1259 size_t spanIndex = static_cast<size_t>(degree);
1260 while (spanIndex + 1 < poles.size() && spanIndex + 1 < knots.size() && knots[spanIndex + 1] <= knotMid) {
1261 ++spanIndex;
1262 }
1263 const size_t firstPole = spanIndex - static_cast<size_t>(degree);
1264 double lowU = std::numeric_limits<double>::infinity();
1265 double highU = -std::numeric_limits<double>::infinity();
1266 for (size_t index = firstPole; index <= spanIndex && index < poles.size(); ++index) {
1267 lowU = std::min(lowU, poles[index].uCoord);
1268 highU = std::max(highU, poles[index].uCoord);
1269 }
1270 return (highU >= lowU) ? (highU - lowU) : 0.;
1271 }
1272 // arc: u(angle) = center.u + radius cos(angle), so u turns exactly at angle = 0 and pi (mod
1273 // 2 pi). Sum the monotone runs between those turning points and the interval's own ends.
1274 const double angleFrom = startAngle + from * sweep();
1275 const double angleTo = startAngle + to * sweep();
1276 const double low = std::min(angleFrom, angleTo);
1277 const double high = std::max(angleFrom, angleTo);
1278 double variation = 0.;
1279 double previous = low;
1280 const double firstTurn = std::ceil(low / kPi) * kPi;
1281 for (double turn = firstTurn; turn < high; turn += kPi) {
1282 variation += std::abs(radius * (std::cos(turn) - std::cos(previous)));
1283 previous = turn;
1284 }
1285 return variation + std::abs(radius * (std::cos(high) - std::cos(previous)));
1286 }
1287
1288 void bsplineSampleRecursive(double t0, double t1, const Vec2& p0, const Vec2& p1, double flatnessSq,
1289 int depth, std::vector<Vec2>& samples) const
1290 {
1291 const double tMid = 0.5 * (t0 + t1);
1292 Vec2 midPoint;
1293 Vec2 unusedDerivative;
1294 bsplineEval(tMid, midPoint, unusedDerivative);
1295 // a degenerate (closed) chord must not end the recursion: test the distance to its single point instead
1296 const bool degenerateChord = surface::distanceSq(p0, p1) <= flatnessSq;
1297 const auto deviationSq = [&](const Vec2& point) {
1298 return degenerateChord ? surface::distanceSq(point, p0) : pointSegmentDistanceSq(point, p0, p1);
1299 };
1300 // Three interior probes: a single midpoint probe is blind to curves symmetric about their parameter midpoint.
1301 double flatness = deviationSq(midPoint);
1302 for (const double fraction : {0.25, 0.75}) {
1303 Vec2 probePoint;
1304 bsplineEval(t0 + (t1 - t0) * fraction, probePoint, unusedDerivative);
1305 flatness = std::max(flatness, deviationSq(probePoint));
1306 }
1307 if (depth <= 0 || (flatness <= flatnessSq && !spansInteriorKnot(t0, t1))) {
1308 samples.push_back(p1);
1309 return;
1310 }
1311 bsplineSampleRecursive(t0, tMid, p0, midPoint, flatnessSq, depth - 1, samples);
1312 bsplineSampleRecursive(tMid, t1, midPoint, p1, flatnessSq, depth - 1, samples);
1313 }
1314
1316 const std::vector<Vec2>& bsplineSamples() const
1317 {
1318 if (bsplineCache.empty()) {
1320 // one canonical polyline, with the seam vertices substituted at its ends
1321 if (hasCanonicalEndpoints && bsplineCache.size() >= 2) {
1322 bsplineCache.front() = canonicalStart;
1323 bsplineCache.back() = canonicalEnd;
1324 }
1325 }
1326 return bsplineCache;
1327 }
1329
1332 bool valid() const
1333 {
1334 if (kind == CurveKind::Line) {
1335 return finite(lineStart) && finite(lineEnd);
1336 }
1337 if (kind == CurveKind::Arc) {
1338 return finite(center) && std::isfinite(radius) && radius > kTolerance && std::isfinite(startAngle) &&
1339 std::isfinite(endAngle);
1340 }
1341 // B-spline
1342 const int nPoles = static_cast<int>(poles.size());
1343 if (degree < 1 || nPoles < degree + 1) {
1344 return false;
1345 }
1346 if (static_cast<int>(knots.size()) != nPoles + degree + 1) {
1347 return false;
1348 }
1349 if (!weights.empty() && static_cast<int>(weights.size()) != nPoles) {
1350 return false;
1351 }
1352 for (const auto& pole : poles) {
1353 if (!finite(pole)) {
1354 return false;
1355 }
1356 }
1357 for (double weight : weights) {
1358 if (!std::isfinite(weight) || weight <= kTolerance) {
1359 return false;
1360 }
1361 }
1362 for (size_t index = 1; index < knots.size(); ++index) {
1363 if (!std::isfinite(knots[index]) || knots[index] < knots[index - 1] - kTolerance) {
1364 return false;
1365 }
1366 }
1367 return bsplineT1() - bsplineT0() > kTolerance;
1368 }
1369
1371 {
1372 return {center.uCoord + radius * std::cos(angle), center.vCoord + radius * std::sin(angle)};
1373 }
1374
1376 Vec2 pointAt(double parameter) const
1377 {
1378 if (kind == CurveKind::Line) {
1379 return {lineStart.uCoord + parameter * (lineEnd.uCoord - lineStart.uCoord),
1380 lineStart.vCoord + parameter * (lineEnd.vCoord - lineStart.vCoord)};
1381 }
1382 if (kind == CurveKind::BSpline) {
1383 return bsplinePointAt(parameter);
1384 }
1385 return pointAtAngle(startAngle + parameter * sweep());
1386 }
1387
1389 {
1390 if (kind == CurveKind::Line) {
1391 return lineStart;
1392 }
1393 if (kind == CurveKind::BSpline) {
1394 // a clamped knot vector interpolates its first pole exactly; anything else has to be evaluated
1395 return bsplineIsClamped() ? poles.front() : bsplinePointAt(0.);
1396 }
1397 return pointAtAngle(startAngle);
1398 }
1400 {
1401 if (kind == CurveKind::Line) {
1402 return lineEnd;
1403 }
1404 if (kind == CurveKind::BSpline) {
1405 return bsplineIsClamped() ? poles.back() : bsplinePointAt(1.);
1406 }
1407 return pointAtAngle(endAngle);
1408 }
1409
1411 Vec2 derivativeAt(double parameter) const
1412 {
1413 if (kind == CurveKind::Line) {
1415 }
1416 if (kind == CurveKind::BSpline) {
1417 const double span = bsplineT1() - bsplineT0();
1418 Vec2 point;
1419 Vec2 derivative;
1420 bsplineEval(bsplineT0() + parameter * span, point, derivative);
1421 return {derivative.uCoord * span, derivative.vCoord * span};
1422 }
1423 const double angle = startAngle + parameter * sweep();
1424 return {-radius * std::sin(angle) * sweep(), radius * std::cos(angle) * sweep()};
1425 }
1426
1428 Vec2 tangentAt(double parameter) const
1429 {
1430 if (kind == CurveKind::Line) {
1432 const double length = std::sqrt(delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord);
1433 if (length <= kTolerance) {
1434 return {0., 0.};
1435 }
1436 return {delta.uCoord / length, delta.vCoord / length};
1437 }
1438 if (kind == CurveKind::BSpline) {
1439 // dC/dt scaled by the positive constant dt/ds, so the normalized direction is unchanged
1440 const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0());
1441 Vec2 point;
1442 Vec2 derivative;
1443 bsplineEval(knotValue, point, derivative);
1444 const double length = std::sqrt(derivative.uCoord * derivative.uCoord + derivative.vCoord * derivative.vCoord);
1445 if (length <= kTolerance) {
1446 return {0., 0.};
1447 }
1448 return {derivative.uCoord / length, derivative.vCoord / length};
1449 }
1450 const double angle = startAngle + parameter * sweep();
1451 const double direction = sweep() >= 0. ? 1. : -1.;
1452 return {-direction * std::sin(angle), direction * std::cos(angle)};
1453 }
1454
1456 bool angleInSweep(double angle) const
1457 {
1458 const double totalSweep = sweep();
1459 const double magnitude = std::abs(totalSweep);
1460 if (magnitude >= kTwoPi - kTolerance) {
1461 return true; // full circle
1462 }
1463 double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle);
1464 delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi)
1465 return delta <= magnitude + kTolerance;
1466 }
1467
1469 double angleParameter(double angle) const
1470 {
1471 const double totalSweep = sweep();
1472 if (std::abs(totalSweep) <= kTolerance) {
1473 return 0.;
1474 }
1475 double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle);
1476 delta -= kTwoPi * std::floor(delta / kTwoPi);
1477 return std::max(0., std::min(1., delta / std::abs(totalSweep)));
1478 }
1479
1482 {
1483 auto include = [&](const Vec2& point) {
1484 lower.uCoord = std::min(lower.uCoord, point.uCoord);
1485 lower.vCoord = std::min(lower.vCoord, point.vCoord);
1486 upper.uCoord = std::max(upper.uCoord, point.uCoord);
1487 upper.vCoord = std::max(upper.vCoord, point.vCoord);
1488 };
1489 if (kind == CurveKind::BSpline) {
1490 // the control-point convex hull contains the curve, so its box is a conservative (exact
1491 // upper bound) parametric AABB — consistent with the BVH's conservative-box philosophy
1492 for (const auto& pole : poles) {
1493 include(pole);
1494 }
1495 return;
1496 }
1497 includeAnalyticExtremes(include);
1498 }
1499
1502 {
1503 auto include = [&](const Vec2& point) {
1504 lower.uCoord = std::min(lower.uCoord, point.uCoord);
1505 lower.vCoord = std::min(lower.vCoord, point.vCoord);
1506 upper.uCoord = std::max(upper.uCoord, point.uCoord);
1507 upper.vCoord = std::max(upper.vCoord, point.vCoord);
1508 };
1509 if (kind == CurveKind::BSpline) {
1510 for (const auto& sample : bsplineSamples()) {
1511 include(sample);
1512 }
1513 return;
1514 }
1515 includeAnalyticExtremes(include);
1516 }
1517
1519 template <typename Include>
1520 void includeAnalyticExtremes(const Include& include) const
1521 {
1522 include(startPoint());
1523 include(endPoint());
1524 if (kind == CurveKind::Arc) {
1525 // include the axis-extreme points (angles 0, pi/2, pi, 3pi/2) that fall within the sweep
1526 const double cardinalAngles[4] = {0., kHalfPi, kPi, 3. * kHalfPi};
1527 for (double cardinal : cardinalAngles) {
1528 if (angleInSweep(cardinal)) {
1529 include(pointAtAngle(cardinal));
1530 }
1531 }
1532 }
1533 }
1534
1536 Vec2 closestPoint(const Vec2& point, double& parameter) const
1537 {
1538 if (kind == CurveKind::BSpline) {
1539 // distance to the cached polyline, accurate to the sampling flatness
1540 const auto& polyline = bsplineSamples();
1541 if (polyline.size() < 2) {
1542 parameter = 0.;
1543 return startPoint();
1544 }
1545 const int segments = static_cast<int>(polyline.size()) - 1;
1546 double bestDistanceSq = std::numeric_limits<double>::infinity();
1547 Vec2 bestPoint = polyline.front();
1548 double bestParameter = 0.;
1549 for (int index = 0; index < segments; ++index) {
1550 const Vec2 segmentStart = polyline[index];
1551 const Vec2 segmentVector = polyline[index + 1] - segmentStart;
1552 const double segmentLengthSq =
1553 segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
1554 double projection = 0.;
1555 if (segmentLengthSq > kToleranceSq) {
1556 projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
1557 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
1558 segmentLengthSq;
1559 projection = std::max(0., std::min(1., projection));
1560 }
1561 const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord,
1562 segmentStart.vCoord + projection * segmentVector.vCoord};
1563 const double candidateDistanceSq = surface::distanceSq(point, candidate);
1564 if (candidateDistanceSq < bestDistanceSq) {
1565 bestDistanceSq = candidateDistanceSq;
1566 bestPoint = candidate;
1567 bestParameter = (index + projection) / segments;
1568 }
1569 }
1570 parameter = bestParameter;
1571 return bestPoint;
1572 }
1573 if (kind == CurveKind::Line) {
1575 const double lengthSq = segment.uCoord * segment.uCoord + segment.vCoord * segment.vCoord;
1576 if (lengthSq <= kToleranceSq) {
1577 parameter = 0.;
1578 return lineStart;
1579 }
1580 const double projection = ((point.uCoord - lineStart.uCoord) * segment.uCoord +
1581 (point.vCoord - lineStart.vCoord) * segment.vCoord) /
1582 lengthSq;
1583 parameter = std::max(0., std::min(1., projection));
1584 return {lineStart.uCoord + parameter * segment.uCoord, lineStart.vCoord + parameter * segment.vCoord};
1585 }
1586 // arc: project radially onto the circle, then clamp the angle to the sweep
1587 const double deltaU = point.uCoord - center.uCoord;
1588 const double deltaV = point.vCoord - center.vCoord;
1589 if (deltaU * deltaU + deltaV * deltaV <= kToleranceSq) {
1590 parameter = 0.; // point at the centre: every arc point is equidistant
1591 return startPoint();
1592 }
1593 const double angle = std::atan2(deltaV, deltaU);
1594 if (angleInSweep(angle)) {
1595 parameter = angleParameter(angle);
1596 return pointAtAngle(angle);
1597 }
1598 const Vec2 startCandidate = startPoint();
1599 const Vec2 endCandidate = endPoint();
1600 if (surface::distanceSq(point, startCandidate) <= surface::distanceSq(point, endCandidate)) {
1601 parameter = 0.;
1602 return startCandidate;
1603 }
1604 parameter = 1.;
1605 return endCandidate;
1606 }
1607
1609 double distanceSq(const Vec2& point) const
1610 {
1611 double parameter = 0.;
1612 return surface::distanceSq(point, closestPoint(point, parameter));
1613 }
1614
1618 {
1619 if (kind == CurveKind::Line) {
1621 }
1622 if (kind == CurveKind::BSpline) {
1623 // Green's area per knot span by Gauss-Legendre: exact for a non-rational span, approximate for a rational one
1624 const int p = degree;
1625 const int order = bsplineRational() ? std::max(2 * p + 2, 8) : (p + 1);
1626 std::vector<double> nodes;
1627 std::vector<double> nodeWeights;
1628 gaussLegendre(order, nodes, nodeWeights);
1629 double area = 0.;
1630 const int lastSpan = static_cast<int>(poles.size()) - 1;
1631 for (int spanIndex = p; spanIndex <= lastSpan; ++spanIndex) {
1632 const double spanLow = knots[spanIndex];
1633 const double spanHigh = knots[spanIndex + 1];
1634 const double halfSpan = 0.5 * (spanHigh - spanLow);
1635 if (halfSpan <= kTolerance) {
1636 continue;
1637 }
1638 const double spanMid = 0.5 * (spanLow + spanHigh);
1639 for (int nodeIndex = 0; nodeIndex < order; ++nodeIndex) {
1640 const double knotValue = spanMid + halfSpan * nodes[nodeIndex];
1641 Vec2 point;
1642 Vec2 derivative;
1643 bsplineEval(knotValue, point, derivative);
1644 area += 0.5 * (point.uCoord * derivative.vCoord - point.vCoord * derivative.uCoord) *
1645 nodeWeights[nodeIndex] * halfSpan;
1646 }
1647 }
1648 return area;
1649 }
1650 return 0.5 * (radius * center.uCoord * (std::sin(endAngle) - std::sin(startAngle)) -
1651 radius * center.vCoord * (std::cos(endAngle) - std::cos(startAngle)) +
1653 }
1654
1657
1660 bool bsplineBandOrCrossings(const Vec2& point, double bandSq, int& crossings) const
1661 {
1662 const auto& polyline = bsplineSamples();
1663 if (polyline.size() < 2) {
1664 return surface::distanceSq(point, startPoint()) <= bandSq;
1665 }
1666 int found = 0;
1667 for (size_t index = 0; index + 1 < polyline.size(); ++index) {
1668 const Vec2 segmentStart = polyline[index];
1669 const Vec2 segmentEnd = polyline[index + 1];
1670 const Vec2 segmentVector = segmentEnd - segmentStart;
1671 const double segmentLengthSq =
1672 segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
1673 double projection = 0.;
1674 if (segmentLengthSq > kToleranceSq) {
1675 projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
1676 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
1677 segmentLengthSq;
1678 projection = std::max(0., std::min(1., projection));
1679 }
1680 const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord,
1681 segmentStart.vCoord + projection * segmentVector.vCoord};
1682 if (surface::distanceSq(point, candidate) <= bandSq) {
1683 return true;
1684 }
1685 const bool firstAbove = segmentStart.vCoord > point.vCoord;
1686 const bool secondAbove = segmentEnd.vCoord > point.vCoord;
1687 if (firstAbove != secondAbove) {
1688 const double intersectU =
1689 segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) * (segmentEnd.uCoord - segmentStart.uCoord) /
1690 (segmentEnd.vCoord - segmentStart.vCoord);
1691 if (point.uCoord < intersectU) {
1692 ++found;
1693 }
1694 }
1695 }
1696 crossings += found;
1697 return false;
1698 }
1699
1701 int rightwardCrossings(const Vec2& point, const Vec2& canonicalStart, const Vec2& canonicalEnd) const
1702 {
1703 auto segmentCrossing = [&](const Vec2& first, const Vec2& second, double exactIntersectU) {
1704 const bool firstAbove = first.vCoord > point.vCoord;
1705 const bool secondAbove = second.vCoord > point.vCoord;
1706 if (firstAbove == secondAbove) {
1707 return false;
1708 }
1709 return point.uCoord < exactIntersectU;
1710 };
1711
1712 if (kind == CurveKind::Line) {
1713 const bool firstAbove = canonicalStart.vCoord > point.vCoord;
1714 const bool secondAbove = canonicalEnd.vCoord > point.vCoord;
1715 if (firstAbove == secondAbove) {
1716 return 0;
1717 }
1718 const double intersectU = canonicalStart.uCoord + (point.vCoord - canonicalStart.vCoord) *
1721 return (point.uCoord < intersectU) ? 1 : 0;
1722 }
1723
1724 if (kind == CurveKind::BSpline) {
1725 // the lines' half-open segment-crossing rule over the polyline, whose ends are the canonical seam vertices
1726 const auto& polyline = bsplineSamples();
1727 if (polyline.size() < 2) {
1728 return 0;
1729 }
1730 int crossings = 0;
1731 for (size_t index = 0; index + 1 < polyline.size(); ++index) {
1732 // No substitution here: the polyline already ends on the loop-canonical vertices (see
1733 // setCanonicalEndpoints), so this is the same boundary closestPoint measures against.
1734 const Vec2 first = polyline[index];
1735 const Vec2 second = polyline[index + 1];
1736 const bool firstAbove = first.vCoord > point.vCoord;
1737 const bool secondAbove = second.vCoord > point.vCoord;
1738 if (firstAbove == secondAbove) {
1739 continue;
1740 }
1741 const double intersectU =
1742 first.uCoord + (point.vCoord - first.vCoord) * (second.uCoord - first.uCoord) /
1743 (second.vCoord - first.vCoord);
1744 if (point.uCoord < intersectU) {
1745 ++crossings;
1746 }
1747 }
1748 return crossings;
1749 }
1750
1751 // split the arc into v-monotonic sub-arcs at its extreme angles, where the crossing u is exact
1752 const double totalSweep = sweep();
1753 if (std::abs(totalSweep) <= kTolerance || radius <= kTolerance) {
1754 return 0;
1755 }
1756 std::array<double, 8> breakParameters{};
1757 int breakCount = 0;
1758 breakParameters[breakCount++] = 0.;
1759 const double lowAngle = std::min(startAngle, endAngle);
1760 const double highAngle = std::max(startAngle, endAngle);
1761 const int firstK = static_cast<int>(std::floor((lowAngle - kHalfPi) / kPi)) - 1;
1762 const int lastK = static_cast<int>(std::ceil((highAngle - kHalfPi) / kPi)) + 1;
1763 for (int k = firstK; k <= lastK && breakCount < 7; ++k) {
1764 const double extremeAngle = kHalfPi + k * kPi;
1765 if (extremeAngle <= lowAngle + kTolerance || extremeAngle >= highAngle - kTolerance) {
1766 continue;
1767 }
1768 const double extremeParameter = (extremeAngle - startAngle) / totalSweep;
1769 if (extremeParameter > kTolerance && extremeParameter < 1. - kTolerance) {
1770 breakParameters[breakCount++] = extremeParameter;
1771 }
1772 }
1773 breakParameters[breakCount++] = 1.;
1774 std::sort(breakParameters.begin(), breakParameters.begin() + breakCount);
1775
1776 double ratio = (point.vCoord - center.vCoord) / radius;
1777 ratio = std::max(-1., std::min(1., ratio));
1778 const double cosMagnitude = std::sqrt(std::max(0., 1. - ratio * ratio));
1779
1780 int crossings = 0;
1781 for (int index = 0; index + 1 < breakCount; ++index) {
1782 const Vec2 subStart = (index == 0) ? canonicalStart : pointAt(breakParameters[index]);
1783 const Vec2 subEnd = (index + 2 == breakCount) ? canonicalEnd : pointAt(breakParameters[index + 1]);
1784 const double midAngle = startAngle + 0.5 * (breakParameters[index] + breakParameters[index + 1]) * totalSweep;
1785 const double cosSign = std::cos(midAngle) >= 0. ? 1. : -1.;
1786 const double intersectU = center.uCoord + cosSign * radius * cosMagnitude;
1787 if (segmentCrossing(subStart, subEnd, intersectU)) {
1788 ++crossings;
1789 }
1790 }
1791 return crossings;
1792 }
1793
1796 {
1798 std::swap(canonicalStart, canonicalEnd);
1799 bsplineCache.clear();
1800 }
1801 if (kind == CurveKind::Line) {
1802 std::swap(lineStart, lineEnd);
1803 } else if (kind == CurveKind::Arc) {
1804 std::swap(startAngle, endAngle);
1805 } else {
1806 // B-spline: reverse the poles/weights and complement the knot vector about its span so the
1807 // parametrization runs the other way (knots stay non-decreasing and clamped).
1808 std::reverse(poles.begin(), poles.end());
1809 if (!weights.empty()) {
1810 std::reverse(weights.begin(), weights.end());
1811 }
1812 const double knotSum = knots.front() + knots.back();
1813 std::vector<double> reversedKnots(knots.size());
1814 for (size_t index = 0; index < knots.size(); ++index) {
1815 reversedKnots[index] = knotSum - knots[knots.size() - 1 - index];
1816 }
1817 knots = std::move(reversedKnots);
1818 bsplineCache.clear(); // geometry order changed; recompute lazily
1819 }
1820 }
1821};
1822
1825 std::vector<Curve2D> curves;
1829
1831 std::vector<int> sourceCurve;
1832
1834 int storedIndexOfSource(int inputIndex) const
1835 {
1836 for (size_t index = 0; index < sourceCurve.size(); ++index) {
1837 if (sourceCurve[index] == inputIndex) {
1838 return static_cast<int>(index);
1839 }
1840 }
1841 return -1;
1842 }
1843
1845 bool initialize(const std::vector<Curve2D>& inputCurves, WireRole wireRole, WireStatus& status,
1846 const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance)
1847 {
1848 role = wireRole;
1849 curves = inputCurves;
1851 for (const auto& curve : curves) {
1852 mRepresentationTolerance = std::max(mRepresentationTolerance, curve.representationTolerance());
1853 }
1854 sourceCurve.resize(curves.size());
1855 for (size_t index = 0; index < curves.size(); ++index) {
1856 sourceCurve[index] = static_cast<int>(index);
1857 }
1858
1859 if (curves.empty()) {
1861 return false;
1862 }
1863 for (size_t index = 0; index < curves.size(); ++index) {
1864 if (!curves[index].valid()) {
1865 status = WireStatus::NonFinite;
1866 return false;
1867 }
1868 const Vec2 currentEnd = curves[index].endPoint();
1869 const Vec2 nextStart = curves[(index + 1) % curves.size()].startPoint();
1870 if (metric.distanceSq(currentEnd, nextStart) > joinTolerance * joinTolerance) {
1871 status = WireStatus::Open;
1872 return false;
1873 }
1874 }
1875
1876 // one vertex value per seam, given to both curves that meet there
1877 for (size_t index = 0; index < curves.size(); ++index) {
1878 curves[index].setCanonicalEndpoints(curves[index].startPoint(),
1879 curves[(index + 1) % curves.size()].startPoint());
1880 }
1881
1882 const double area = signedArea();
1883 if (std::abs(area) <= kAreaTolerance) {
1884 status = WireStatus::ZeroArea;
1885 return false;
1886 }
1887
1888 const bool wantPositiveArea = (role == WireRole::Outer);
1889 if ((area > 0.) != wantPositiveArea) {
1890 reverse();
1891 status = WireStatus::Reversed;
1893 return true;
1894 }
1895 status = WireStatus::Valid;
1897 return true;
1898 }
1899
1902 {
1903 for (const auto& curve : curves) {
1904 if (curve.kind == CurveKind::BSpline) {
1905 curve.bsplineSamples();
1906 }
1907 }
1908 }
1909
1912
1914 void reverse()
1915 {
1916 std::reverse(curves.begin(), curves.end());
1917 std::reverse(sourceCurve.begin(), sourceCurve.end());
1918 for (auto& curve : curves) {
1919 curve.reverseInPlace();
1920 }
1921 }
1922
1925 bool hasBSpline() const
1926 {
1927 for (const auto& curve : curves) {
1928 if (curve.kind == CurveKind::BSpline) {
1929 return true;
1930 }
1931 }
1932 return false;
1933 }
1934
1936 double signedArea() const
1937 {
1938 double area = 0.;
1939 for (const auto& curve : curves) {
1940 area += curve.signedAreaContribution();
1941 }
1942 return area;
1943 }
1944
1947 {
1948 for (const auto& curve : curves) {
1949 curve.extendBounds(lower, upper);
1950 }
1951 }
1952
1955 {
1956 for (const auto& curve : curves) {
1957 curve.extendTightBounds(lower, upper);
1958 }
1959 }
1960
1963 double boundaryBand(double lengthFloor) const { return std::max(lengthFloor, mRepresentationTolerance); }
1964
1966 WireClassification classify(const Vec2& point, double lengthFloor) const
1967 {
1968 const double band = boundaryBand(lengthFloor);
1969 const double bandSq = band * band;
1970 // Each curve's polyline already ends on the loop-canonical seam vertices, so the half-open
1971 // crossing convention stays consistent across seams without any substitution here.
1972 int crossings = 0;
1973 for (const auto& curve : curves) {
1974 if (curve.kind == CurveKind::BSpline) {
1975 if (curve.bsplineBandOrCrossings(point, bandSq, crossings)) {
1977 }
1978 } else if (curve.distanceSq(point) <= bandSq) {
1980 } else {
1981 crossings += curve.rightwardCrossings(point, curve.loopStart(), curve.loopEnd());
1982 }
1983 }
1984 return (crossings % 2 == 1) ? WireClassification::Inside : WireClassification::Outside;
1985 }
1986
1988 WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const
1989 {
1990 return classify(point, trimLengthFloor(metric, point));
1991 }
1992
1995 std::vector<Vec2> sampledBoundary(int segmentsPerArc = kArcSamples) const
1996 {
1997 std::vector<Vec2> samples;
1998 if (curves.empty()) {
1999 return samples;
2000 }
2001 for (const auto& curve : curves) {
2002 if (curve.kind == CurveKind::Line) {
2003 samples.push_back(curve.startPoint());
2004 } else if (curve.kind == CurveKind::BSpline) {
2005 // adaptively flatten and append every sample except the closing one (the next curve's
2006 // start reproduces it)
2007 std::vector<Vec2> curveSamples;
2008 curve.bsplineSampleInto(curveSamples);
2009 for (size_t index = 0; index + 1 < curveSamples.size(); ++index) {
2010 samples.push_back(curveSamples[index]);
2011 }
2012 } else {
2013 // chords scale with the arc's sweep, so a rim shared with a quadric wall samples identical vertices
2014 const int arcSteps =
2015 std::max(1, static_cast<int>(std::lround(segmentsPerArc * std::abs(curve.sweep()) / kTwoPi)));
2016 for (int step = 0; step < arcSteps; ++step) {
2017 samples.push_back(curve.pointAt(static_cast<double>(step) / arcSteps));
2018 }
2019 }
2020 }
2021 samples.push_back(samples.front());
2022 return samples;
2023 }
2024};
2025
2028
2030inline double unwrapAngleInto(double angle, double uMin, double uMax)
2031{
2032 const double windowCenter = 0.5 * (uMin + uMax);
2033 return angle - kTwoPi * std::round((angle - windowCenter) / kTwoPi);
2034}
2035
2037inline bool curveTrimContains(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2038 const Vec2& point, bool* boundary = nullptr,
2039 const ParametricMetric& metric = {})
2040{
2041 if (boundary != nullptr) {
2042 *boundary = false;
2043 }
2044 const double lengthFloor = trimLengthFloor(metric, point);
2045 const auto outerClassification = outerWire.classify(point, lengthFloor);
2046 if (outerClassification == WireClassification::Outside) {
2047 return false;
2048 }
2049 if (outerClassification == WireClassification::Boundary) {
2050 if (boundary != nullptr) {
2051 *boundary = true;
2052 }
2053 return true;
2054 }
2055 for (const auto& innerWire : innerWires) {
2056 const auto innerClassification = innerWire.classify(point, lengthFloor);
2057 if (innerClassification == WireClassification::Boundary) {
2058 if (boundary != nullptr) {
2059 *boundary = true;
2060 }
2061 return true;
2062 }
2063 if (innerClassification == WireClassification::Inside) {
2064 return false;
2065 }
2066 }
2067 return true;
2068}
2069
2071inline constexpr int kContourQuadratureOrder = 20;
2072inline constexpr double kContourMaxSpanU = 0.25 * kPi;
2073
2076template <typename Antiderivative>
2077double contourIntegralAlongCurve(const Curve2D& curve, const Antiderivative& antiderivative, double from,
2078 double to)
2079{
2080 static thread_local std::vector<double> nodes;
2081 static thread_local std::vector<double> weights;
2082 if (static_cast<int>(nodes.size()) != kContourQuadratureOrder) {
2084 }
2085 // split at the interior knots, then into pieces whose u travel is at most kContourMaxSpanU
2086 static thread_local std::vector<double> breakpoints;
2087 breakpoints.clear();
2088 breakpoints.push_back(from);
2089 curve.appendInteriorKnots(std::min(from, to), std::max(from, to), breakpoints);
2090 std::sort(breakpoints.begin() + 1, breakpoints.end(),
2091 [forward = (to >= from)](double first, double second) { return forward ? first < second : first > second; });
2092 breakpoints.push_back(to);
2093
2094 double total = 0.;
2095 for (size_t segment = 0; segment + 1 < breakpoints.size(); ++segment) {
2096 const double segmentFrom = breakpoints[segment];
2097 const double segmentTo = breakpoints[segment + 1];
2098 if (segmentFrom == segmentTo) {
2099 continue;
2100 }
2101 const double travelU = curve.uVariation(std::min(segmentFrom, segmentTo), std::max(segmentFrom, segmentTo));
2102 const int pieces = std::max(1, static_cast<int>(std::ceil(travelU / kContourMaxSpanU)));
2103 for (int piece = 0; piece < pieces; ++piece) {
2104 const double low = segmentFrom + (segmentTo - segmentFrom) * piece / pieces;
2105 const double high = segmentFrom + (segmentTo - segmentFrom) * (piece + 1) / pieces;
2106 const double half = 0.5 * (high - low);
2107 const double mid = 0.5 * (high + low);
2108 for (int nodeIndex = 0; nodeIndex < kContourQuadratureOrder; ++nodeIndex) {
2109 const double parameter = mid + half * nodes[nodeIndex];
2110 const Vec2 point = curve.pointAt(parameter);
2111 const Vec2 derivative = curve.derivativeAt(parameter);
2112 total += weights[nodeIndex] * half * antiderivative(point.uCoord, point.vCoord) * derivative.vCoord;
2113 }
2114 }
2115 }
2116 return total;
2117}
2118
2120template <typename Antiderivative>
2121double integrateOverCurveTrimByParts(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2122 const Antiderivative& antiderivative)
2123{
2124 const auto loopIntegral = [&antiderivative](const CurveWire& wire) {
2125 double total = 0.;
2126 for (size_t index = 0; index < wire.curves.size(); ++index) {
2127 const auto& curve = wire.curves[index];
2128 total += contourIntegralAlongCurve(curve, antiderivative, 0., 1.);
2129 // seam bridge: a straight run from this curve's end to the next curve's start
2130 const Vec2 seamFrom = curve.endPoint();
2131 const Vec2 seamTo = wire.curves[(index + 1) % wire.curves.size()].startPoint();
2132 const double deltaV = seamTo.vCoord - seamFrom.vCoord;
2133 if (deltaV != 0.) {
2134 const Curve2D bridge = Curve2D::makeLine(seamFrom, seamTo);
2135 total += contourIntegralAlongCurve(bridge, antiderivative, 0., 1.);
2136 }
2137 }
2138 return total;
2139 };
2140
2141 double total = loopIntegral(outerWire);
2142 for (const auto& innerWire : innerWires) {
2143 total += loopIntegral(innerWire);
2144 }
2145 return total;
2146}
2147
2149template <typename Integrand>
2150double integrateOverCurveTrim(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2151 const Integrand& integrand, int samplesPerAxis = 128)
2152{
2153 Vec2 lower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2154 Vec2 upper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2155 outerWire.parametricBounds(lower, upper);
2156 if (!finite(lower) || !finite(upper) || samplesPerAxis < 1) {
2157 return 0.;
2158 }
2159 const double stepU = (upper.uCoord - lower.uCoord) / samplesPerAxis;
2160 const double stepV = (upper.vCoord - lower.vCoord) / samplesPerAxis;
2161 const double cellArea = stepU * stepV;
2162 double sum = 0.;
2163 for (int indexU = 0; indexU < samplesPerAxis; ++indexU) {
2164 const double uCoord = lower.uCoord + (indexU + 0.5) * stepU;
2165 for (int indexV = 0; indexV < samplesPerAxis; ++indexV) {
2166 const double vCoord = lower.vCoord + (indexV + 0.5) * stepV;
2167 if (curveTrimContains(outerWire, innerWires, {uCoord, vCoord})) {
2168 sum += integrand(uCoord, vCoord) * cellArea;
2169 }
2170 }
2171 }
2172 return sum;
2173}
2174
2176inline bool buildCurveTrim(const std::vector<Curve2D>& outerTrim,
2177 const std::vector<std::vector<Curve2D>>& innerTrims, CurveWire& outerWire,
2178 std::vector<CurveWire>& innerWires, Vec2& lower, Vec2& upper,
2179 std::string& errorMessage, const ParametricMetric& metric = {},
2180 double joinTolerance = kWireJoinTolerance)
2181{
2183 if (!outerWire.initialize(outerTrim, WireRole::Outer, status, metric, joinTolerance)) {
2184 errorMessage = std::string("quadric outer trim wire invalid: ") + wireStatusMessage(status);
2185 return false;
2186 }
2187 innerWires.clear();
2188 innerWires.reserve(innerTrims.size());
2189 for (const auto& innerLoop : innerTrims) {
2190 CurveWire innerWire;
2191 WireStatus innerStatus = WireStatus::Valid;
2192 if (!innerWire.initialize(innerLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) {
2193 errorMessage = std::string("quadric inner trim wire invalid: ") + wireStatusMessage(innerStatus);
2194 return false;
2195 }
2196 innerWires.push_back(std::move(innerWire));
2197 }
2198 lower = {std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2199 upper = {-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2200 outerWire.parametricBounds(lower, upper);
2201 if (!finite(lower) || !finite(upper)) {
2202 errorMessage = "quadric trim wire has non-finite parametric bounds";
2203 return false;
2204 }
2205 if (upper.uCoord - lower.uCoord > kTwoPi + kTolerance) {
2206 // the pole hull can overshoot the curve; re-measure on the curves before refusing
2207 Vec2 tightLower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2208 Vec2 tightUpper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2209 outerWire.tightParametricBounds(tightLower, tightUpper);
2210 if (!finite(tightLower) || !finite(tightUpper) || tightUpper.uCoord - tightLower.uCoord > kTwoPi + kTolerance) {
2211 errorMessage = "quadric trim wire spans more than a full turn in phi";
2212 return false;
2213 }
2214 // the wire is admissible; keep the tight box, since the conservative one is not a valid
2215 // parametric window for a periodic coordinate once it exceeds a full turn
2216 lower = tightLower;
2217 upper = tightUpper;
2218 }
2219 return true;
2220}
2221
2223inline std::vector<Vec2> sampleCurveWireByU(const CurveWire& wire, int segmentsPerTurn = kArcSamples)
2224{
2225 std::vector<Vec2> samples;
2226 for (const auto& curve : wire.curves) {
2227 if (curve.kind == CurveKind::BSpline) {
2228 // adaptively flatten in the parameter domain; append every sample except the closing one
2229 std::vector<Vec2> curveSamples;
2230 curve.bsplineSampleInto(curveSamples);
2231 for (size_t index = 0; index + 1 < curveSamples.size(); ++index) {
2232 samples.push_back(curveSamples[index]);
2233 }
2234 continue;
2235 }
2236 Vec2 lower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2237 Vec2 upper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2238 curve.extendBounds(lower, upper);
2239 const double uSpan = upper.uCoord - lower.uCoord;
2240 int steps = std::max(1, static_cast<int>(std::lround(segmentsPerTurn * uSpan / kTwoPi)));
2241 if (curve.kind == CurveKind::Arc) {
2242 steps = std::max(steps, static_cast<int>(std::lround(segmentsPerTurn * std::abs(curve.sweep()) / kTwoPi)));
2243 steps = std::max(steps, 1);
2244 }
2245 for (int step = 0; step < steps; ++step) {
2246 samples.push_back(curve.pointAt(static_cast<double>(step) / steps));
2247 }
2248 }
2249 return samples;
2250}
2251
2253template <typename MapUV>
2254void appendCurveTrimMesh(const CurveWire& outerWire, const MapUV& mapUV, std::vector<Vec3>& vertices,
2255 std::vector<std::array<int, 3>>& triangles)
2256{
2257 SurfaceWire sampledWire;
2258 sampledWire.vertices = sampleCurveWireByU(outerWire);
2259 if (sampledWire.vertices.size() < 3) {
2260 return;
2261 }
2262 const int firstVertexIndex = static_cast<int>(vertices.size());
2263 for (const auto& sample : sampledWire.vertices) {
2264 vertices.push_back(mapUV(sample.uCoord, sample.vCoord));
2265 }
2266 for (const auto& triangle : triangulateSimpleWire(sampledWire)) {
2267 triangles.push_back(
2268 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
2269 }
2270}
2271
2273template <typename MapUV>
2274void appendCurveTrimEdges(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2275 const MapUV& mapUV, double orientationSign,
2276 std::vector<std::pair<Vec3, Vec3>>& edges)
2277{
2278 auto appendLoop = [&](const CurveWire& wire) {
2279 const auto samples = sampleCurveWireByU(wire);
2280 const size_t sampleCount = samples.size();
2281 for (size_t sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) {
2282 const Vec2& current = samples[sampleIndex];
2283 const Vec2& next = samples[(sampleIndex + 1) % sampleCount];
2284 const Vec3 edgeStart = mapUV(current.uCoord, current.vCoord);
2285 const Vec3 edgeEnd = mapUV(next.uCoord, next.vCoord);
2286 if (orientationSign >= 0.) {
2287 edges.emplace_back(edgeStart, edgeEnd);
2288 } else {
2289 edges.emplace_back(edgeEnd, edgeStart);
2290 }
2291 }
2292 };
2293 appendLoop(outerWire);
2294 for (const auto& innerWire : innerWires) {
2295 appendLoop(innerWire);
2296 }
2297}
2298
2300inline constexpr int kSharedEdgeSamples = 33;
2301
2303template <typename MapUV>
2304bool sampleTrimCurveOfCurveWires(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2305 size_t index, const MapUV& mapUV, std::vector<Vec3>& samples)
2306{
2307 const CurveWire* wire = nullptr;
2308 size_t local = index;
2309 if (local < outerWire.curves.size()) {
2310 wire = &outerWire;
2311 } else {
2312 local -= outerWire.curves.size();
2313 for (const auto& innerWire : innerWires) {
2314 if (local < innerWire.curves.size()) {
2315 wire = &innerWire;
2316 break;
2317 }
2318 local -= innerWire.curves.size();
2319 }
2320 }
2321 if (wire == nullptr) {
2322 return false;
2323 }
2324 const int stored = wire->storedIndexOfSource(static_cast<int>(local));
2325 if (stored < 0) {
2326 return false;
2327 }
2328 const Curve2D& curve = wire->curves[static_cast<size_t>(stored)];
2329 samples.clear();
2330 samples.reserve(kSharedEdgeSamples);
2331 for (int step = 0; step < kSharedEdgeSamples; ++step) {
2332 const Vec2 uv = curve.pointAt(static_cast<double>(step) / (kSharedEdgeSamples - 1));
2333 samples.push_back(mapUV(uv.uCoord, uv.vCoord));
2334 }
2335 return true;
2336}
2337
2339template <typename MapUV>
2340bool sampleTrimCurveOfSurfaceWires(const SurfaceWire& outerWire, const std::vector<SurfaceWire>& innerWires,
2341 size_t index, const MapUV& mapUV, std::vector<Vec3>& samples)
2342{
2343 const SurfaceWire* wire = nullptr;
2344 size_t local = index;
2345 if (local < outerWire.vertices.size()) {
2346 wire = &outerWire;
2347 } else {
2348 local -= outerWire.vertices.size();
2349 for (const auto& innerWire : innerWires) {
2350 if (local < innerWire.vertices.size()) {
2351 wire = &innerWire;
2352 break;
2353 }
2354 local -= innerWire.vertices.size();
2355 }
2356 }
2357 if (wire == nullptr) {
2358 return false;
2359 }
2360 const int stored = wire->storedIndexOfSource(static_cast<int>(local));
2361 if (stored < 0) {
2362 return false;
2363 }
2364 const SurfaceEdge segment = wire->edge(stored);
2365 samples.clear();
2366 samples.push_back(mapUV(segment.start.uCoord, segment.start.vCoord));
2367 samples.push_back(mapUV(segment.end.uCoord, segment.end.vCoord));
2368 return true;
2369}
2371
2374 int surfaceIndex = -1;
2375 bool closed = false;
2376 std::vector<Vec3> points;
2377};
2378
2380inline void assembleRims(const std::vector<std::pair<Vec3, Vec3>>& edges, std::vector<SurfaceRim>& rims)
2381{
2382 if (edges.empty()) {
2383 return;
2384 }
2385 auto quantize = [](double value) { return static_cast<int64_t>(std::llround(value / kTolerance)); };
2386 using VertexKey = std::tuple<int64_t, int64_t, int64_t>;
2387 auto keyOf = [&](const Vec3& point) {
2388 return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)};
2389 };
2390
2391 // cancel reversed duplicate chords (a self-closing seam) before chaining, keyed by their shared midpoint
2392 std::vector<bool> consumed(edges.size(), false);
2393 std::map<VertexKey, std::vector<size_t>> edgesByMidpoint;
2394 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
2395 const Vec3 midpoint = (edges[edgeIndex].first + edges[edgeIndex].second) * 0.5;
2396 const auto [xKey, yKey, zKey] = keyOf(midpoint);
2397 bool cancelled = false;
2398 for (int64_t dx = -1; dx <= 1 && !cancelled; ++dx) {
2399 for (int64_t dy = -1; dy <= 1 && !cancelled; ++dy) {
2400 for (int64_t dz = -1; dz <= 1 && !cancelled; ++dz) {
2401 const auto found = edgesByMidpoint.find(VertexKey{xKey + dx, yKey + dy, zKey + dz});
2402 if (found == edgesByMidpoint.end()) {
2403 continue;
2404 }
2405 for (const size_t candidate : found->second) {
2406 if (consumed[candidate] ||
2407 distanceSq(edges[candidate].first, edges[edgeIndex].second) > kToleranceSq ||
2408 distanceSq(edges[candidate].second, edges[edgeIndex].first) > kToleranceSq) {
2409 continue;
2410 }
2411 consumed[candidate] = true;
2412 consumed[edgeIndex] = true;
2413 cancelled = true;
2414 break;
2415 }
2416 }
2417 }
2418 }
2419 if (!cancelled) {
2420 edgesByMidpoint[keyOf(midpoint)].push_back(edgeIndex);
2421 }
2422 }
2423
2424 std::map<VertexKey, std::vector<size_t>> edgesByStart;
2425 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
2426 if (!consumed[edgeIndex]) {
2427 edgesByStart[keyOf(edges[edgeIndex].first)].push_back(edgeIndex);
2428 }
2429 }
2430
2431 // A vertex can land either side of a lattice boundary, so probe the 27 neighbouring cells and
2432 // accept the first unused chord whose start really is within kTolerance.
2433 auto findSuccessor = [&](const Vec3& point) -> long long {
2434 const auto [xKey, yKey, zKey] = keyOf(point);
2435 for (int64_t dx = -1; dx <= 1; ++dx) {
2436 for (int64_t dy = -1; dy <= 1; ++dy) {
2437 for (int64_t dz = -1; dz <= 1; ++dz) {
2438 const auto found = edgesByStart.find(VertexKey{xKey + dx, yKey + dy, zKey + dz});
2439 if (found == edgesByStart.end()) {
2440 continue;
2441 }
2442 for (const size_t candidate : found->second) {
2443 if (!consumed[candidate] && distanceSq(edges[candidate].first, point) <= kToleranceSq) {
2444 return static_cast<long long>(candidate);
2445 }
2446 }
2447 }
2448 }
2449 }
2450 return -1;
2451 };
2452
2453 for (size_t seed = 0; seed < edges.size(); ++seed) {
2454 if (consumed[seed]) {
2455 continue;
2456 }
2457 consumed[seed] = true;
2458 SurfaceRim rim;
2459 rim.points.push_back(edges[seed].first);
2460 rim.points.push_back(edges[seed].second);
2461 while (true) {
2462 if (distanceSq(rim.points.back(), rim.points.front()) <= kToleranceSq) {
2463 rim.closed = true;
2464 rim.points.pop_back(); // a closed rim does not repeat its first point
2465 break;
2466 }
2467 const long long next = findSuccessor(rim.points.back());
2468 if (next < 0) {
2469 break; // an open chain: the face's boundary is not a set of closed loops
2470 }
2471 consumed[static_cast<size_t>(next)] = true;
2472 rim.points.push_back(edges[static_cast<size_t>(next)].second);
2473 }
2474 if (rim.points.size() >= 2) {
2475 rims.push_back(std::move(rim));
2476 }
2477 }
2478}
2479
2482{
2483 public:
2484 virtual ~BoundedSurface() = default;
2485
2489 uint32_t edgeId = 0;
2490 bool reversed = false;
2491 bool degenerate = false;
2493 bool anchored = false;
2494 };
2495
2496 void setBoundaryEdges(std::vector<BoundaryEdgeRef> refs) { mBoundaryEdges = std::move(refs); }
2497 const std::vector<BoundaryEdgeRef>& boundaryEdges() const { return mBoundaryEdges; }
2498
2500 virtual bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const
2501 {
2502 (void)index;
2503 (void)samples;
2504 return false;
2505 }
2507
2509 virtual void conservativeBounds(Vec3& lower, Vec3& upper) const = 0;
2510
2512 using CoverBox = std::pair<Vec3, Vec3>;
2513
2516 virtual void appendCoverBoxes(std::vector<CoverBox>& boxes) const
2517 {
2518 // conservativeBounds only accumulates, so the corners start beyond any geometry
2519 constexpr double kBig = std::numeric_limits<double>::max();
2520 CoverBox box{Vec3{kBig, kBig, kBig}, Vec3{-kBig, -kBig, -kBig}};
2521 conservativeBounds(box.first, box.second);
2522 boxes.push_back(box);
2523 }
2524
2526 virtual bool containsPointOnSurface(const Vec3& point) const = 0;
2527
2529 virtual void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
2530 double maxDistance, std::vector<RayHit>& hits) const = 0;
2531
2533 virtual double distanceSqToPatch(const Vec3& point) const = 0;
2534
2536 virtual Vec3 normalAt(const Vec3& point) const = 0;
2537
2539 virtual void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const = 0;
2540
2542 double parametricLengthSqAt(const Vec2& uv, const Vec2& delta) const
2543 {
2544 double gUU = 0.;
2545 double gUV = 0.;
2546 double gVV = 0.;
2547 parametricMetric(uv, gUU, gUV, gVV);
2548 return parametricLengthSq(gUU, gUV, gVV, delta);
2549 }
2550
2552 virtual double capacityContribution() const = 0;
2553
2555 virtual bool capacityIsExact() const = 0;
2556
2558 virtual void appendDisplayMesh(std::vector<Vec3>& vertices,
2559 std::vector<std::array<int, 3>>& triangles) const = 0;
2560
2562 virtual void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const = 0;
2563
2565 virtual void appendRims(std::vector<SurfaceRim>& rims) const
2566 {
2567 std::vector<std::pair<Vec3, Vec3>> edges;
2569 assembleRims(edges, rims);
2570 }
2571
2572 protected:
2573 std::vector<BoundaryEdgeRef> mBoundaryEdges;
2574};
2575
2579{
2580 public:
2581 bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV,
2582 const std::vector<Vec2>& outerWireVertices,
2583 const std::vector<std::vector<Vec2>>& innerWireVertices, std::string& errorMessage)
2584 {
2585 if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) {
2586 errorMessage = "surface frame contains a non-finite value";
2587 return false;
2588 }
2589
2590 mOrigin = surfaceOrigin;
2591 mAxisU = surfaceAxisU;
2592 mAxisV = surfaceAxisV;
2593 const Vec3 normalVector = cross(mAxisU, mAxisV);
2594 mAreaScale = norm(normalVector);
2595 if (mAreaScale <= kTolerance) {
2596 errorMessage = "surface frame axes are degenerate";
2597 return false;
2598 }
2599 mNormal = normalVector * (1. / mAreaScale);
2600
2601 mMetricUU = dot(mAxisU, mAxisU);
2602 mMetricUV = dot(mAxisU, mAxisV);
2603 mMetricVV = dot(mAxisV, mAxisV);
2604 const double metricDet = mMetricUU * mMetricVV - mMetricUV * mMetricUV;
2605 if (std::abs(metricDet) <= kToleranceSq) {
2606 errorMessage = "surface frame metric is singular";
2607 return false;
2608 }
2609 mInverseMetricDet = 1. / metricDet;
2610
2611 WireStatus outerStatus = WireStatus::Valid;
2612 const ParametricMetric metric = parametricMetricOf(*this);
2613 mTrimBand = trimLengthFloor(metric, Vec2{0., 0.});
2614 if (!mOuterWire.initialize(outerWireVertices, WireRole::Outer, outerStatus, metric)) {
2615 errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus);
2616 return false;
2617 }
2618 mOuterReoriented = (outerStatus == WireStatus::Reversed);
2619
2620 mInnerWires.clear();
2621 mInnerWires.reserve(innerWireVertices.size());
2622 mInnerReoriented = false;
2623 for (const auto& innerWireInput : innerWireVertices) {
2624 SurfaceWire innerWire;
2625 WireStatus innerStatus = WireStatus::Valid;
2626 if (!innerWire.initialize(innerWireInput, WireRole::Inner, innerStatus, metric)) {
2627 errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus);
2628 return false;
2629 }
2630 mInnerReoriented = mInnerReoriented || (innerStatus == WireStatus::Reversed);
2631 mInnerWires.emplace_back(std::move(innerWire));
2632 }
2633
2634 const auto ringOf = [this](const SurfaceWire& wire) {
2635 std::vector<Vec3> ring;
2636 ring.reserve(wire.vertices.size());
2637 for (const auto& vertex : wire.vertices) {
2638 ring.push_back(toGlobal(vertex));
2639 }
2640 return ring;
2641 };
2642 mOuterRing = ringOf(mOuterWire);
2643 mInnerRings.clear();
2644 for (const auto& innerWire : mInnerWires) {
2645 mInnerRings.push_back(ringOf(innerWire));
2646 }
2647 return true;
2648 }
2649
2651 bool wasReoriented() const { return mOuterReoriented || mInnerReoriented; }
2652
2653 Vec3 toGlobal(const Vec2& point) const
2654 {
2655 return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord;
2656 }
2657
2658 Vec2 toLocal(const Vec3& point) const
2659 {
2660 const Vec3 relativePoint = point - mOrigin;
2661 const double projectionU = dot(relativePoint, mAxisU);
2662 const double projectionV = dot(relativePoint, mAxisV);
2663 return {(projectionU * mMetricVV - projectionV * mMetricUV) * mInverseMetricDet,
2664 (projectionV * mMetricUU - projectionU * mMetricUV) * mInverseMetricDet};
2665 }
2666
2667 double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); }
2668
2669 bool containsLocal(const Vec2& point, bool* boundary = nullptr) const
2670 {
2671 if (boundary != nullptr) {
2672 *boundary = false;
2673 }
2674
2675 const auto outerClassification = mOuterWire.classify(point, mTrimBand);
2676 if (outerClassification == WireClassification::Outside) {
2677 return false;
2678 }
2679 if (outerClassification == WireClassification::Boundary) {
2680 if (boundary != nullptr) {
2681 *boundary = true;
2682 }
2683 return true;
2684 }
2685
2686 for (const auto& innerWire : mInnerWires) {
2687 const auto innerClassification = innerWire.classify(point, mTrimBand);
2688 if (innerClassification == WireClassification::Boundary) {
2689 if (boundary != nullptr) {
2690 *boundary = true;
2691 }
2692 return true;
2693 }
2694 if (innerClassification == WireClassification::Inside) {
2695 return false;
2696 }
2697 }
2698 return true;
2699 }
2700
2701 bool containsPointOnSurface(const Vec3& point) const override
2702 {
2703 if (std::abs(planeDistance(point)) > kTolerance) {
2704 return false;
2705 }
2706 return containsLocal(toLocal(point));
2707 }
2708
2709 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
2710 double maxDistance, std::vector<RayHit>& hits) const override
2711 {
2712 const double denominator = dot(mNormal, rayDirection);
2713 if (std::abs(denominator) <= kTolerance) {
2714 return;
2715 }
2716 const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator;
2717 if (candidateDistance < minDistance || candidateDistance > maxDistance) {
2718 return;
2719 }
2720 const Vec3 candidatePoint = rayOrigin + rayDirection * candidateDistance;
2721 bool onTrimBoundary = false;
2722 if (!containsLocal(toLocal(candidatePoint), &onTrimBoundary)) {
2723 return;
2724 }
2725 hits.push_back({candidateDistance, mNormal, onTrimBoundary});
2726 }
2727
2728 double distanceSqToEdges(const Vec3& point, const std::vector<Vec3>& ring) const
2729 {
2730 double bestDistanceSq = std::numeric_limits<double>::infinity();
2731 for (size_t vertexIndex = 0; vertexIndex < ring.size(); ++vertexIndex) {
2732 bestDistanceSq =
2733 std::min(bestDistanceSq, pointSegmentDistanceSq(point, ring[vertexIndex], ring[(vertexIndex + 1) % ring.size()]));
2734 }
2735 return bestDistanceSq;
2736 }
2737
2738 double distanceSqToPatch(const Vec3& point) const override
2739 {
2740 const Vec2 projectedPoint = toLocal(point);
2741 if (containsLocal(projectedPoint)) {
2742 const double signedPlaneDistance = planeDistance(point);
2743 return signedPlaneDistance * signedPlaneDistance;
2744 }
2745
2746 double bestDistanceSq = distanceSqToEdges(point, mOuterRing);
2747 for (const auto& innerRing : mInnerRings) {
2748 bestDistanceSq = std::min(bestDistanceSq, distanceSqToEdges(point, innerRing));
2749 }
2750 return bestDistanceSq;
2751 }
2752
2753 Vec3 normalAt(const Vec3&) const override { return mNormal; }
2754
2755 void conservativeBounds(Vec3& lower, Vec3& upper) const override
2756 {
2757 auto extendPoint = [&](const Vec2& surfacePoint) {
2758 const Vec3 globalPoint = toGlobal(surfacePoint);
2759 lower.xCoord = std::min(lower.xCoord, globalPoint.xCoord);
2760 lower.yCoord = std::min(lower.yCoord, globalPoint.yCoord);
2761 lower.zCoord = std::min(lower.zCoord, globalPoint.zCoord);
2762 upper.xCoord = std::max(upper.xCoord, globalPoint.xCoord);
2763 upper.yCoord = std::max(upper.yCoord, globalPoint.yCoord);
2764 upper.zCoord = std::max(upper.zCoord, globalPoint.zCoord);
2765 };
2766
2767 for (const auto& vertex : mOuterWire.vertices) {
2768 extendPoint(vertex);
2769 }
2770 for (const auto& innerWire : mInnerWires) {
2771 for (const auto& vertex : innerWire.vertices) {
2772 extendPoint(vertex);
2773 }
2774 }
2775 }
2776
2777 double area() const
2778 {
2779 double parametricArea = std::abs(mOuterWire.signedArea());
2780 for (const auto& innerWire : mInnerWires) {
2781 parametricArea -= std::abs(innerWire.signedArea());
2782 }
2783 return std::max(0., parametricArea) * mAreaScale;
2784 }
2785
2787 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
2788 {
2789 planeParametricMetric(mAxisU, mAxisV, gUU, gUV, gVV);
2790 }
2791
2792 double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; }
2793
2794 bool capacityIsExact() const override { return true; }
2795
2796 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
2797 {
2798 const int firstVertexIndex = static_cast<int>(vertices.size());
2799 for (const auto& vertex : mOuterWire.vertices) {
2800 vertices.push_back(toGlobal(vertex));
2801 }
2802
2803 const auto localTriangles = triangulateSimpleWire(mOuterWire);
2804 for (const auto& triangle : localTriangles) {
2805 triangles.push_back(
2806 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
2807 }
2808 }
2809
2810 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
2811 {
2812 auto appendWire = [&](const SurfaceWire& wire) {
2813 for (size_t vertexIndex = 0; vertexIndex < wire.vertices.size(); ++vertexIndex) {
2814 const Vec3 edgeStart = toGlobal(wire.vertices[vertexIndex]);
2815 const Vec3 edgeEnd = toGlobal(wire.vertices[(vertexIndex + 1) % wire.vertices.size()]);
2816 edges.emplace_back(edgeStart, edgeEnd);
2817 }
2818 };
2819 appendWire(mOuterWire);
2820 for (const auto& innerWire : mInnerWires) {
2821 appendWire(innerWire);
2822 }
2823 }
2824
2825 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
2826 {
2828 mOuterWire, mInnerWires, index,
2829 [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples);
2830 }
2831
2832 private:
2833 Vec3 mOrigin;
2834 Vec3 mAxisU;
2835 Vec3 mAxisV;
2836 Vec3 mNormal;
2837 double mMetricUU = 0.;
2838 double mMetricUV = 0.;
2839 double mMetricVV = 0.;
2840 double mInverseMetricDet = 0.;
2841 double mAreaScale = 0.;
2842 bool mOuterReoriented = false;
2843 bool mInnerReoriented = false;
2844 double mTrimBand = 0.;
2845 SurfaceWire mOuterWire;
2846 std::vector<SurfaceWire> mInnerWires;
2847 std::vector<Vec3> mOuterRing;
2848 std::vector<std::vector<Vec3>> mInnerRings;
2849};
2850
2853{
2854 public:
2855 bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV,
2856 const std::vector<Curve2D>& outerCurves,
2857 const std::vector<std::vector<Curve2D>>& innerCurves, std::string& errorMessage,
2858 double joinTolerance = kWireJoinTolerance)
2859 {
2860 if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) {
2861 errorMessage = "surface frame contains a non-finite value";
2862 return false;
2863 }
2864 if (std::abs(norm(surfaceAxisU) - 1.) > kTolerance || std::abs(norm(surfaceAxisV) - 1.) > kTolerance ||
2865 std::abs(dot(surfaceAxisU, surfaceAxisV)) > kTolerance) {
2866 errorMessage = "curved planar surface requires orthonormal frame axes";
2867 return false;
2868 }
2869
2870 mOrigin = surfaceOrigin;
2871 mAxisU = surfaceAxisU;
2872 mAxisV = surfaceAxisV;
2873 mNormal = cross(mAxisU, mAxisV);
2874
2875 WireStatus outerStatus = WireStatus::Valid;
2876 const ParametricMetric metric = parametricMetricOf(*this);
2877 mTrimFloor = trimLengthFloor(metric, Vec2{0., 0.});
2878 if (!mOuterWire.initialize(outerCurves, WireRole::Outer, outerStatus, metric, joinTolerance)) {
2879 errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus);
2880 return false;
2881 }
2882 mReoriented = (outerStatus == WireStatus::Reversed);
2883
2884 mInnerWires.clear();
2885 mInnerWires.reserve(innerCurves.size());
2886 for (const auto& innerCurveLoop : innerCurves) {
2887 CurveWire innerWire;
2888 WireStatus innerStatus = WireStatus::Valid;
2889 if (!innerWire.initialize(innerCurveLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) {
2890 errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus);
2891 return false;
2892 }
2893 mReoriented = mReoriented || (innerStatus == WireStatus::Reversed);
2894 mInnerWires.emplace_back(std::move(innerWire));
2895 }
2896
2897 // A B-spline boundary makes the area (hence the capacity contribution) a numeric quadrature,
2898 // so flag the capacity as inexact (matching the wire-trimmed-quadric policy).
2899 mCapacityExact = !mOuterWire.hasBSpline();
2900 for (const auto& innerWire : mInnerWires) {
2901 mCapacityExact = mCapacityExact && !innerWire.hasBSpline();
2902 }
2903 return true;
2904 }
2905
2907 bool wasReoriented() const { return mReoriented; }
2908
2909 Vec3 toGlobal(const Vec2& point) const { return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord; }
2910
2911 Vec2 toLocal(const Vec3& point) const
2912 {
2913 const Vec3 relativePoint = point - mOrigin;
2914 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV)};
2915 }
2916
2917 double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); }
2918
2919 bool containsLocal(const Vec2& point, bool* boundary = nullptr) const
2920 {
2921 if (boundary != nullptr) {
2922 *boundary = false;
2923 }
2924
2925 const auto outerClassification = mOuterWire.classify(point, mTrimFloor);
2926 if (outerClassification == WireClassification::Outside) {
2927 return false;
2928 }
2929 if (outerClassification == WireClassification::Boundary) {
2930 if (boundary != nullptr) {
2931 *boundary = true;
2932 }
2933 return true;
2934 }
2935
2936 for (const auto& innerWire : mInnerWires) {
2937 const auto innerClassification = innerWire.classify(point, mTrimFloor);
2938 if (innerClassification == WireClassification::Boundary) {
2939 if (boundary != nullptr) {
2940 *boundary = true;
2941 }
2942 return true;
2943 }
2944 if (innerClassification == WireClassification::Inside) {
2945 return false;
2946 }
2947 }
2948 return true;
2949 }
2950
2951 bool containsPointOnSurface(const Vec3& point) const override
2952 {
2953 if (std::abs(planeDistance(point)) > kTolerance) {
2954 return false;
2955 }
2956 return containsLocal(toLocal(point));
2957 }
2958
2959 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
2960 double maxDistance, std::vector<RayHit>& hits) const override
2961 {
2962 const double denominator = dot(mNormal, rayDirection);
2963 if (std::abs(denominator) <= kTolerance) {
2964 return;
2965 }
2966 const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator;
2967 if (candidateDistance < minDistance || candidateDistance > maxDistance) {
2968 return;
2969 }
2970 bool onTrimBoundary = false;
2971 if (!containsLocal(toLocal(rayOrigin + rayDirection * candidateDistance), &onTrimBoundary)) {
2972 return;
2973 }
2974 hits.push_back({candidateDistance, mNormal, onTrimBoundary});
2975 }
2976
2977 double distanceSqToPatch(const Vec3& point) const override
2978 {
2979 const Vec2 projectedPoint = toLocal(point);
2980 const double signedPlaneDistance = planeDistance(point);
2981 if (containsLocal(projectedPoint)) {
2982 return signedPlaneDistance * signedPlaneDistance;
2983 }
2984
2985 // exact for an orthonormal frame: split into in-plane distance to the trim curves plus the
2986 // out-of-plane plane distance
2987 double bestCurveDistanceSq = std::numeric_limits<double>::infinity();
2988 for (const auto& curve : mOuterWire.curves) {
2989 bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint));
2990 }
2991 for (const auto& innerWire : mInnerWires) {
2992 for (const auto& curve : innerWire.curves) {
2993 bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint));
2994 }
2995 }
2996 return bestCurveDistanceSq + signedPlaneDistance * signedPlaneDistance;
2997 }
2998
2999 Vec3 normalAt(const Vec3&) const override { return mNormal; }
3000
3001 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3002 {
3003 Vec2 parametricLower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
3004 Vec2 parametricUpper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
3005 mOuterWire.parametricBounds(parametricLower, parametricUpper);
3006
3007 // the affine image of the parametric AABB contains the patch; its corners bound the 3D AABB
3008 for (const double cornerU : {parametricLower.uCoord, parametricUpper.uCoord}) {
3009 for (const double cornerV : {parametricLower.vCoord, parametricUpper.vCoord}) {
3010 const Vec3 globalCorner = toGlobal({cornerU, cornerV});
3011 lower.xCoord = std::min(lower.xCoord, globalCorner.xCoord);
3012 lower.yCoord = std::min(lower.yCoord, globalCorner.yCoord);
3013 lower.zCoord = std::min(lower.zCoord, globalCorner.zCoord);
3014 upper.xCoord = std::max(upper.xCoord, globalCorner.xCoord);
3015 upper.yCoord = std::max(upper.yCoord, globalCorner.yCoord);
3016 upper.zCoord = std::max(upper.zCoord, globalCorner.zCoord);
3017 }
3018 }
3019 }
3020
3021 double area() const
3022 {
3023 double parametricArea = std::abs(mOuterWire.signedArea());
3024 for (const auto& innerWire : mInnerWires) {
3025 parametricArea -= std::abs(innerWire.signedArea());
3026 }
3027 return std::max(0., parametricArea);
3028 }
3029
3032 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
3033 {
3034 gUU = 1.;
3035 gUV = 0.;
3036 gVV = 1.;
3037 }
3038
3039 double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; }
3040
3041 bool capacityIsExact() const override { return mCapacityExact; }
3042
3043 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3044 {
3045 // triangulate the sampled outer boundary; holes are ignored in the display mesh (as for the
3046 // polygonal planar surface, visualization never influences navigation)
3047 auto samples = mOuterWire.sampledBoundary();
3048 if (samples.size() < 4) {
3049 return;
3050 }
3051 samples.pop_back(); // drop the closing duplicate
3052
3053 SurfaceWire sampledWire;
3054 sampledWire.vertices = std::move(samples);
3055
3056 const int firstVertexIndex = static_cast<int>(vertices.size());
3057 for (const auto& vertex : sampledWire.vertices) {
3058 vertices.push_back(toGlobal(vertex));
3059 }
3060 for (const auto& triangle : triangulateSimpleWire(sampledWire)) {
3061 triangles.push_back(
3062 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
3063 }
3064 }
3065
3066 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3067 {
3068 auto appendWire = [&](const CurveWire& wire) {
3069 const auto samples = wire.sampledBoundary();
3070 for (size_t sampleIndex = 0; sampleIndex + 1 < samples.size(); ++sampleIndex) {
3071 edges.emplace_back(toGlobal(samples[sampleIndex]), toGlobal(samples[sampleIndex + 1]));
3072 }
3073 };
3074 appendWire(mOuterWire);
3075 for (const auto& innerWire : mInnerWires) {
3076 appendWire(innerWire);
3077 }
3078 }
3079
3080 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3081 {
3083 mOuterWire, mInnerWires, index,
3084 [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples);
3085 }
3086
3087 private:
3088 Vec3 mOrigin;
3089 Vec3 mAxisU;
3090 Vec3 mAxisV;
3091 Vec3 mNormal;
3092 bool mReoriented = false;
3093 bool mCapacityExact = true;
3094 double mTrimFloor = 0.;
3095 CurveWire mOuterWire;
3096 std::vector<CurveWire> mInnerWires;
3097};
3098
3100inline void appendArcBandCoverBoxes(const Vec3& center, const Vec3& axisU, const Vec3& axisV, const Vec3& axisW,
3101 double phiStart, double phiSweep, double heightMin, double heightMax,
3102 double radiusAtMin, double radiusAtMax,
3103 std::vector<BoundedSurface::CoverBox>& boxes)
3104{
3105 const int chunks = coverChunkCount(phiSweep);
3106 for (int chunk = 0; chunk < chunks; ++chunk) {
3107 const double phiLow = phiStart + phiSweep * chunk / chunks;
3108 const double phiHigh = phiStart + phiSweep * (chunk + 1) / chunks;
3109 double lower[3];
3110 double upper[3];
3111 for (int dimension = 0; dimension < 3; ++dimension) {
3112 double radialLow = 0.;
3113 double radialHigh = 0.;
3114 sinusoidRange(component(axisU, dimension), component(axisV, dimension), phiLow, phiHigh, radialLow, radialHigh);
3115 const double centerAtMin = component(center, dimension) + heightMin * component(axisW, dimension);
3116 const double centerAtMax = component(center, dimension) + heightMax * component(axisW, dimension);
3117 lower[dimension] = std::min(centerAtMin + radiusAtMin * radialLow, centerAtMax + radiusAtMax * radialLow);
3118 upper[dimension] = std::max(centerAtMin + radiusAtMin * radialHigh, centerAtMax + radiusAtMax * radialHigh);
3119 }
3120 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
3121 }
3122}
3123
3126{
3127 public:
3128 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius,
3129 double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall,
3130 std::string& errorMessage)
3131 {
3132 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radius) ||
3133 !std::isfinite(heightMin) || !std::isfinite(heightMax) || !std::isfinite(phiStart) ||
3134 !std::isfinite(phiSweep)) {
3135 errorMessage = "cylindrical surface parameter is non-finite";
3136 return false;
3137 }
3138 if (radius <= kTolerance) {
3139 errorMessage = "cylindrical surface needs a positive radius";
3140 return false;
3141 }
3142 if (heightMax - heightMin <= kTolerance) {
3143 errorMessage = "cylindrical surface needs a positive height range";
3144 return false;
3145 }
3146 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
3147 errorMessage = "cylindrical surface needs an angular sweep in (0, 2pi]";
3148 return false;
3149 }
3150 if (!makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
3151 return false;
3152 }
3153
3154 mCenter = centerPoint;
3155 mRadius = radius;
3156 mPhiTolerance = angularTolerance(mRadius);
3157 mHeightMin = heightMin;
3158 mHeightMax = heightMax;
3159 mPhiStart = phiStart;
3160 mPhiSweep = std::min(phiSweep, kTwoPi);
3161 mNormalSign = innerWall ? -1. : 1.;
3162 return true;
3163 }
3164
3166 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius,
3167 double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall,
3168 const std::vector<Curve2D>& outerTrim, const std::vector<std::vector<Curve2D>>& innerTrims,
3169 std::string& errorMessage, double joinTolerance = kWireJoinTolerance)
3170 {
3171 if (!initialize(centerPoint, axis, referenceAxisU, radius, heightMin, heightMax, phiStart, phiSweep, innerWall,
3172 errorMessage)) {
3173 return false;
3174 }
3175 Vec2 lower, upper;
3176 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
3177 parametricMetricOf(*this), joinTolerance)) {
3178 return false;
3179 }
3180 mPhiStart = lower.uCoord;
3181 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
3182 mHeightMin = lower.vCoord;
3183 mHeightMax = upper.vCoord;
3184 mHasWireTrim = true;
3185 return true;
3186 }
3187
3188 bool hasWireTrim() const { return mHasWireTrim; }
3189
3191 bool pointInTrim(double phi, double height, bool* boundary = nullptr) const
3192 {
3193 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
3194 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this));
3195 }
3196
3199 static bool makeFrame(const Vec3& axis, const Vec3& referenceAxisU, Vec3& axisU, Vec3& axisV, Vec3& axisW,
3200 std::string& errorMessage)
3201 {
3202 if (norm(axis) <= kTolerance) {
3203 errorMessage = "surface axis is degenerate";
3204 return false;
3205 }
3206 axisW = normalized(axis);
3207 const Vec3 projectedU = referenceAxisU - axisW * dot(referenceAxisU, axisW);
3208 if (norm(projectedU) <= kTolerance) {
3209 errorMessage = "surface reference axis is parallel to the main axis";
3210 return false;
3211 }
3212 axisU = normalized(projectedU);
3213 axisV = cross(axisW, axisU); // gives axisU x axisV = axisW
3214 return true;
3215 }
3216
3217 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
3218
3219 Vec3 toLocal(const Vec3& point) const
3220 {
3221 const Vec3 relativePoint = point - mCenter;
3222 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
3223 }
3224
3225 bool heightInRange(double height) const
3226 {
3227 return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance;
3228 }
3229
3230 bool phiInSweep(double phi) const
3231 {
3232 return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance);
3233 }
3234
3235 Vec3 pointAt(double phi, double height) const
3236 {
3237 return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * mRadius;
3238 }
3239
3240 bool containsPointOnSurface(const Vec3& point) const override
3241 {
3242 const Vec3 localPoint = toLocal(point);
3243 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3244 if (std::abs(radialDistance - mRadius) > kTolerance) {
3245 return false;
3246 }
3247 if (radialDistance <= kTolerance) {
3248 return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined on the axis
3249 }
3250 const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord);
3251 if (mHasWireTrim) {
3252 return pointInTrim(phi, localPoint.zCoord);
3253 }
3254 return heightInRange(localPoint.zCoord) && phiInSweep(phi);
3255 }
3256
3257 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3258 double maxDistance, std::vector<RayHit>& hits) const override
3259 {
3260 const Vec3 localOrigin = toLocal(rayOrigin);
3261 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
3262
3263 const double quadraticA = localDirection.xCoord * localDirection.xCoord +
3264 localDirection.yCoord * localDirection.yCoord;
3265 if (quadraticA <= kToleranceSq) {
3266 return; // ray parallel to the axis: no transversal crossing of the lateral surface
3267 }
3268 const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord +
3269 localOrigin.yCoord * localDirection.yCoord);
3270 const double quadraticC = localOrigin.xCoord * localOrigin.xCoord +
3271 localOrigin.yCoord * localOrigin.yCoord - mRadius * mRadius;
3272 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
3273 if (discriminant <= 0.) {
3274 return;
3275 }
3276 const double sqrtDiscriminant = std::sqrt(discriminant);
3277 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
3278 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
3279 if (sameIntersection(firstRoot, secondRoot)) {
3280 return; // tangential graze: report neither hit so crossing parity stays even
3281 }
3282
3283 for (const double candidate : {firstRoot, secondRoot}) {
3284 if (candidate < minDistance || candidate > maxDistance) {
3285 continue;
3286 }
3287 const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord;
3288 const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord;
3289 const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord;
3290 const double hitPhi = std::atan2(hitV, hitU);
3291 bool onTrimBoundary = false;
3292 if (mHasWireTrim) {
3293 if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) {
3294 continue;
3295 }
3296 } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) {
3297 continue;
3298 }
3299 const double radialDistance = std::hypot(hitU, hitV);
3300 const Vec3 hitNormal = (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance)) * mNormalSign;
3301 hits.push_back({candidate, hitNormal, onTrimBoundary});
3302 }
3303 }
3304
3306 double distanceSqToPatch(const Vec3& point) const override
3307 {
3308 const Vec3 localPoint = toLocal(point);
3309 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3310 if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) {
3311 return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord}, Vec2{mRadius, mHeightMin},
3312 Vec2{mRadius, mHeightMax});
3313 }
3314 const double distanceToStartSeam =
3315 pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax));
3316 const double endPhi = mPhiStart + mPhiSweep;
3317 const double distanceToEndSeam =
3318 pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
3319 return std::min(distanceToStartSeam, distanceToEndSeam);
3320 }
3321
3322 Vec3 normalAt(const Vec3& point) const override
3323 {
3324 const Vec3 localPoint = toLocal(point);
3325 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3326 if (radialDistance <= kTolerance) {
3327 return mAxisU * mNormalSign; // ill-defined on the axis; return a stable direction
3328 }
3329 return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance)) *
3330 mNormalSign;
3331 }
3332
3334 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
3335 {
3336 cylinderParametricMetric(mRadius, gUU, gUV, gVV);
3337 }
3338
3340 double capacityContribution() const override
3341 {
3342 if (mHasWireTrim) {
3343 const double centreU = dot(mCenter, mAxisU);
3344 const double centreV = dot(mCenter, mAxisV);
3345 const double factor = mNormalSign * mRadius / 3.;
3346 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double) {
3347 return factor * (centreU * std::sin(phi) - centreV * std::cos(phi) + mRadius * phi);
3348 });
3349 }
3350 const double endPhi = mPhiStart + mPhiSweep;
3351 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
3352 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
3353 const double height = mHeightMax - mHeightMin;
3354 return mNormalSign * mRadius * height * (phiFactor + mRadius * mPhiSweep) / 3.;
3355 }
3356
3357 bool capacityIsExact() const override { return !mHasWireTrim; }
3358
3359 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3360 {
3361 // conservative: the AABB of the two full rim circles (partial sweeps get a larger box)
3362 for (const double height : {mHeightMin, mHeightMax}) {
3363 const Vec3 rimCenter = mCenter + mAxisW * height;
3364 for (int dimension = 0; dimension < 3; ++dimension) {
3365 const double radialExtent = mRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension));
3366 const double centerValue = component(rimCenter, dimension);
3367 if (dimension == 0) {
3368 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
3369 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
3370 } else if (dimension == 1) {
3371 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
3372 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
3373 } else {
3374 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
3375 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
3376 }
3377 }
3378 }
3379 }
3380
3382 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
3383 {
3384 appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax, mRadius,
3385 mRadius, boxes);
3386 }
3387
3390 int rimSegments() const
3391 {
3392 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
3393 }
3394
3395 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3396 {
3397 if (mHasWireTrim) {
3398 appendCurveTrimMesh(mTrimOuter, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles);
3399 return;
3400 }
3401 const int segments = rimSegments();
3402 const int firstVertexIndex = static_cast<int>(vertices.size());
3403 for (int step = 0; step <= segments; ++step) {
3404 const double phi = mPhiStart + mPhiSweep * step / segments;
3405 vertices.push_back(pointAt(phi, mHeightMin));
3406 vertices.push_back(pointAt(phi, mHeightMax));
3407 }
3408 for (int step = 0; step < segments; ++step) {
3409 const int base = firstVertexIndex + 2 * step;
3410 triangles.push_back({base, base + 2, base + 3});
3411 triangles.push_back({base, base + 3, base + 1});
3412 }
3413 }
3414
3415 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3416 {
3417 if (mHasWireTrim) {
3418 // the (phi, h) -> 3D map is orientation-consistent with the outward normal, so a CCW trim
3419 // loop yields a CCW 3D loop for an outer wall; the sign is just mNormalSign
3420 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges);
3421 return;
3422 }
3423 // boundary counter-clockwise seen along the outward normal, so rims shared with caps cancel
3424 const int segments = rimSegments();
3425 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
3426 if (mNormalSign > 0.) {
3427 edges.emplace_back(edgeStart, edgeEnd);
3428 } else {
3429 edges.emplace_back(edgeEnd, edgeStart);
3430 }
3431 };
3432 for (int step = 0; step < segments; ++step) {
3433 const double phi = mPhiStart + mPhiSweep * step / segments;
3434 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments;
3435 emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin));
3436 emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax));
3437 }
3438 if (!fullSweep()) {
3439 const double endPhi = mPhiStart + mPhiSweep;
3440 emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
3441 emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin));
3442 }
3443 }
3444
3445 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3446 {
3447 if (!mHasWireTrim) {
3448 return false; // a parametric-rectangle trim carries no per-edge curve to sample
3449 }
3450 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples);
3451 }
3452
3453 private:
3454 Vec3 mCenter;
3455 Vec3 mAxisU;
3456 Vec3 mAxisV;
3457 Vec3 mAxisW;
3458 double mRadius = 0.;
3459 double mHeightMin = 0.;
3460 double mHeightMax = 0.;
3461 double mPhiStart = 0.;
3462 double mPhiSweep = kTwoPi;
3463 double mPhiTolerance = 0.;
3464 double mNormalSign = 1.;
3465 bool mHasWireTrim = false;
3466 CurveWire mTrimOuter;
3467 std::vector<CurveWire> mTrimInner;
3468};
3469
3472{
3473 public:
3474 bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius,
3475 double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall,
3476 std::string& errorMessage)
3477 {
3478 if (!finite(center) || !finite(polarAxis) || !finite(referenceAxisU) || !std::isfinite(radius) ||
3479 !std::isfinite(thetaMin) || !std::isfinite(thetaMax) || !std::isfinite(phiStart) ||
3480 !std::isfinite(phiSweep)) {
3481 errorMessage = "spherical surface parameter is non-finite";
3482 return false;
3483 }
3484 if (radius <= kTolerance) {
3485 errorMessage = "spherical surface needs a positive radius";
3486 return false;
3487 }
3488 if (thetaMin < -kTolerance || thetaMax > kPi + kTolerance || thetaMax - thetaMin <= kTolerance) {
3489 errorMessage = "spherical surface needs a polar range within [0, pi]";
3490 return false;
3491 }
3492 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
3493 errorMessage = "spherical surface needs an angular sweep in (0, 2pi]";
3494 return false;
3495 }
3496 if (!CylindricalBoundedSurface::makeFrame(polarAxis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
3497 return false;
3498 }
3499
3500 mCenter = center;
3501 mRadius = radius;
3502 mThetaMin = std::max(0., thetaMin);
3503 mThetaMax = std::min(kPi, thetaMax);
3504 mPhiStart = phiStart;
3505 mPhiSweep = std::min(phiSweep, kTwoPi);
3506 mNormalSign = innerWall ? -1. : 1.;
3507 return true;
3508 }
3509
3511 bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius,
3512 double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall,
3513 const std::vector<Curve2D>& outerTrim, const std::vector<std::vector<Curve2D>>& innerTrims,
3514 std::string& errorMessage, double joinTolerance = kWireJoinTolerance)
3515 {
3516 if (!initialize(center, polarAxis, referenceAxisU, radius, thetaMin, thetaMax, phiStart, phiSweep, innerWall,
3517 errorMessage)) {
3518 return false;
3519 }
3520 Vec2 lower, upper;
3521 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
3522 parametricMetricOf(*this), joinTolerance)) {
3523 return false;
3524 }
3525 mPhiStart = lower.uCoord;
3526 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
3527 mThetaMin = std::max(0., lower.vCoord);
3528 mThetaMax = std::min(kPi, upper.vCoord);
3529 mHasWireTrim = true;
3530 return true;
3531 }
3532
3533 bool hasWireTrim() const { return mHasWireTrim; }
3534
3536 bool pointInTrim(double phi, double theta, bool* boundary = nullptr) const
3537 {
3538 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
3539 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, theta}, boundary, parametricMetricOf(*this));
3540 }
3541
3542 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
3543
3544 Vec3 toLocal(const Vec3& point) const
3545 {
3546 const Vec3 relativePoint = point - mCenter;
3547 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
3548 }
3549
3550 bool directionInTrim(const Vec3& localPoint, bool* boundary = nullptr) const
3551 {
3552 if (boundary != nullptr) {
3553 *boundary = false;
3554 }
3555 const double pointRadius = norm(localPoint);
3556 if (pointRadius <= kTolerance) {
3557 return true; // the center is angle-degenerate; every patch point is equidistant
3558 }
3559 const double thetaTolerance = angularTolerance(mRadius);
3560 const double theta = std::acos(std::max(-1., std::min(1., localPoint.zCoord / pointRadius)));
3561 const double transverseDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3562 if (mHasWireTrim) {
3563 if (transverseDistance <= kTolerance) {
3564 // on the polar axis phi is degenerate; accept by the wire's theta (v) range
3565 return theta >= mThetaMin - thetaTolerance && theta <= mThetaMax + thetaTolerance;
3566 }
3567 return pointInTrim(std::atan2(localPoint.yCoord, localPoint.xCoord), theta, boundary);
3568 }
3569 if (theta < mThetaMin - thetaTolerance || theta > mThetaMax + thetaTolerance) {
3570 return false;
3571 }
3572 if (transverseDistance <= kTolerance) {
3573 return true; // on the polar axis phi is degenerate
3574 }
3575 return angleInSweepRange(std::atan2(localPoint.yCoord, localPoint.xCoord), mPhiStart, mPhiSweep,
3576 thetaTolerance);
3577 }
3578
3579 Vec3 pointAt(double theta, double phi) const
3580 {
3581 const double sinTheta = std::sin(theta);
3582 return mCenter + (mAxisU * (sinTheta * std::cos(phi)) + mAxisV * (sinTheta * std::sin(phi)) +
3583 mAxisW * std::cos(theta)) *
3584 mRadius;
3585 }
3586
3587 bool containsPointOnSurface(const Vec3& point) const override
3588 {
3589 const Vec3 localPoint = toLocal(point);
3590 if (std::abs(norm(localPoint) - mRadius) > kTolerance) {
3591 return false;
3592 }
3593 return directionInTrim(localPoint);
3594 }
3595
3596 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3597 double maxDistance, std::vector<RayHit>& hits) const override
3598 {
3599 const Vec3 relativeOrigin = rayOrigin - mCenter;
3600 const double quadraticA = normSq(rayDirection);
3601 if (quadraticA <= kToleranceSq) {
3602 return;
3603 }
3604 const double quadraticB = 2. * dot(relativeOrigin, rayDirection);
3605 const double quadraticC = normSq(relativeOrigin) - mRadius * mRadius;
3606 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
3607 if (discriminant <= 0.) {
3608 return;
3609 }
3610 const double sqrtDiscriminant = std::sqrt(discriminant);
3611 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
3612 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
3613 if (sameIntersection(firstRoot, secondRoot)) {
3614 return; // tangential graze
3615 }
3616
3617 for (const double candidate : {firstRoot, secondRoot}) {
3618 if (candidate < minDistance || candidate > maxDistance) {
3619 continue;
3620 }
3621 const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate);
3622 bool onTrimBoundary = false;
3623 if (!directionInTrim(localHit, &onTrimBoundary)) {
3624 continue;
3625 }
3626 hits.push_back({candidate,
3627 (mAxisU * localHit.xCoord + mAxisV * localHit.yCoord + mAxisW * localHit.zCoord) *
3628 (mNormalSign / mRadius),
3629 onTrimBoundary});
3630 }
3631 }
3632
3634 double distanceSqToPatch(const Vec3& point) const override
3635 {
3636 const Vec3 localPoint = toLocal(point);
3637 const double radialOffset = norm(localPoint) - mRadius;
3638 return radialOffset * radialOffset;
3639 }
3640
3641 Vec3 normalAt(const Vec3& point) const override
3642 {
3643 const Vec3 localPoint = toLocal(point);
3644 const double pointRadius = norm(localPoint);
3645 if (pointRadius <= kTolerance) {
3646 return mAxisW * mNormalSign; // ill-defined at the center; return a stable direction
3647 }
3648 return (mAxisU * localPoint.xCoord + mAxisV * localPoint.yCoord + mAxisW * localPoint.zCoord) *
3649 (mNormalSign / pointRadius);
3650 }
3651
3653 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
3654 {
3655 sphereParametricMetric(mRadius, uv.vCoord, gUU, gUV, gVV);
3656 }
3657
3659 double capacityContribution() const override
3660 {
3661 if (mHasWireTrim) {
3662 const double centreU = dot(mCenter, mAxisU);
3663 const double centreV = dot(mCenter, mAxisV);
3664 const double centreW = dot(mCenter, mAxisW);
3665 const double factor = mNormalSign * mRadius * mRadius / 3.;
3666 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double theta) {
3667 const double sinTheta = std::sin(theta);
3668 return factor * sinTheta *
3669 (sinTheta * (centreU * std::sin(phi) - centreV * std::cos(phi)) +
3670 (centreW * std::cos(theta) + mRadius) * phi);
3671 });
3672 }
3673 const double endPhi = mPhiStart + mPhiSweep;
3674 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
3675 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
3676 const double thetaIntegralSinSq =
3677 0.5 * ((mThetaMax - std::sin(mThetaMax) * std::cos(mThetaMax)) -
3678 (mThetaMin - std::sin(mThetaMin) * std::cos(mThetaMin)));
3679 const double thetaIntegralSinCos =
3680 0.5 * (std::sin(mThetaMax) * std::sin(mThetaMax) - std::sin(mThetaMin) * std::sin(mThetaMin));
3681 const double thetaIntegralSin = std::cos(mThetaMin) - std::cos(mThetaMax);
3682 return mNormalSign * mRadius * mRadius *
3683 (phiFactor * thetaIntegralSinSq + dot(mCenter, mAxisW) * mPhiSweep * thetaIntegralSinCos +
3684 mRadius * mPhiSweep * thetaIntegralSin) /
3685 3.;
3686 }
3687
3688 bool capacityIsExact() const override { return !mHasWireTrim; }
3689
3690 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3691 {
3692 lower.xCoord = std::min(lower.xCoord, mCenter.xCoord - mRadius);
3693 lower.yCoord = std::min(lower.yCoord, mCenter.yCoord - mRadius);
3694 lower.zCoord = std::min(lower.zCoord, mCenter.zCoord - mRadius);
3695 upper.xCoord = std::max(upper.xCoord, mCenter.xCoord + mRadius);
3696 upper.yCoord = std::max(upper.yCoord, mCenter.yCoord + mRadius);
3697 upper.zCoord = std::max(upper.zCoord, mCenter.zCoord + mRadius);
3698 }
3699
3701 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
3702 {
3703 const int thetaChunks = coverChunkCount(kPi);
3704 const int phiChunks = coverChunkCount(kTwoPi);
3705 for (int thetaChunk = 0; thetaChunk < thetaChunks; ++thetaChunk) {
3706 const double thetaLow = kPi * thetaChunk / thetaChunks;
3707 const double thetaHigh = kPi * (thetaChunk + 1) / thetaChunks;
3708 for (int phiChunk = 0; phiChunk < phiChunks; ++phiChunk) {
3709 const double phiLow = kTwoPi * phiChunk / phiChunks;
3710 const double phiHigh = kTwoPi * (phiChunk + 1) / phiChunks;
3711 double lower[3];
3712 double upper[3];
3713 for (int dimension = 0; dimension < 3; ++dimension) {
3714 double inPlaneLow = 0.;
3715 double inPlaneHigh = 0.;
3716 sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), phiLow, phiHigh, inPlaneLow,
3717 inPlaneHigh);
3718 // sin(theta) >= 0 on [0, pi], so the chunk extremes are the theta sinusoid at s's own extremes
3719 const double axisComponent = component(mAxisW, dimension);
3720 const double high = sinusoidMaximum(axisComponent, inPlaneHigh, thetaLow, thetaHigh);
3721 const double low = sinusoidMinimum(axisComponent, inPlaneLow, thetaLow, thetaHigh);
3722 lower[dimension] = component(mCenter, dimension) + mRadius * low;
3723 upper[dimension] = component(mCenter, dimension) + mRadius * high;
3724 }
3725 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
3726 }
3727 }
3728 }
3729
3730 int phiSegments() const
3731 {
3732 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
3733 }
3734
3735 int thetaSegments() const
3736 {
3737 return std::max(1, static_cast<int>(std::lround(kArcSamples * (mThetaMax - mThetaMin) / kTwoPi)));
3738 }
3739
3740 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3741 {
3742 if (mHasWireTrim) {
3743 appendCurveTrimMesh(mTrimOuter, [this](double phi, double theta) { return pointAt(theta, phi); }, vertices, triangles);
3744 return;
3745 }
3746 const int phiSteps = phiSegments();
3747 const int thetaSteps = thetaSegments();
3748 const int firstVertexIndex = static_cast<int>(vertices.size());
3749 for (int thetaStep = 0; thetaStep <= thetaSteps; ++thetaStep) {
3750 const double theta = mThetaMin + (mThetaMax - mThetaMin) * thetaStep / thetaSteps;
3751 for (int phiStep = 0; phiStep <= phiSteps; ++phiStep) {
3752 vertices.push_back(pointAt(theta, mPhiStart + mPhiSweep * phiStep / phiSteps));
3753 }
3754 }
3755 const int rowLength = phiSteps + 1;
3756 for (int thetaStep = 0; thetaStep < thetaSteps; ++thetaStep) {
3757 for (int phiStep = 0; phiStep < phiSteps; ++phiStep) {
3758 const int base = firstVertexIndex + thetaStep * rowLength + phiStep;
3759 triangles.push_back({base, base + 1, base + rowLength + 1});
3760 triangles.push_back({base, base + rowLength + 1, base + rowLength});
3761 }
3762 }
3763 }
3764
3765 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3766 {
3767 if (mHasWireTrim) {
3768 // the (phi, theta) -> 3D map is orientation-*reversed* relative to the outward normal
3769 // (X_phi x X_theta points inward), so the sign is -mNormalSign
3770 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double theta) { return pointAt(theta, phi); }, -mNormalSign, edges);
3771 return;
3772 }
3773 // boundary of the (theta, phi) rectangle, traversed counter-clockwise for an outer wall;
3774 // pole rims are degenerate points and full-sweep phi seams cancel, so both are skipped
3775 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
3776 if (mNormalSign > 0.) {
3777 edges.emplace_back(edgeStart, edgeEnd);
3778 } else {
3779 edges.emplace_back(edgeEnd, edgeStart);
3780 }
3781 };
3782 const double thetaTolerance = angularTolerance(mRadius);
3783 const int phiSteps = phiSegments();
3784 const double endPhi = mPhiStart + mPhiSweep;
3785 if (mThetaMin > thetaTolerance) {
3786 for (int step = 0; step < phiSteps; ++step) {
3787 const double phi = mPhiStart + mPhiSweep * step / phiSteps;
3788 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps;
3789 emitEdge(pointAt(mThetaMin, nextPhi), pointAt(mThetaMin, phi)); // -phi at the small-theta rim
3790 }
3791 }
3792 if (mThetaMax < kPi - thetaTolerance) {
3793 for (int step = 0; step < phiSteps; ++step) {
3794 const double phi = mPhiStart + mPhiSweep * step / phiSteps;
3795 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps;
3796 emitEdge(pointAt(mThetaMax, phi), pointAt(mThetaMax, nextPhi)); // +phi at the large-theta rim
3797 }
3798 }
3799 if (!fullSweep()) {
3800 const int thetaSteps = thetaSegments();
3801 for (int step = 0; step < thetaSteps; ++step) {
3802 const double theta = mThetaMin + (mThetaMax - mThetaMin) * step / thetaSteps;
3803 const double nextTheta = mThetaMin + (mThetaMax - mThetaMin) * (step + 1) / thetaSteps;
3804 emitEdge(pointAt(theta, mPhiStart), pointAt(nextTheta, mPhiStart)); // +theta at phiStart
3805 emitEdge(pointAt(nextTheta, endPhi), pointAt(theta, endPhi)); // -theta at phiEnd
3806 }
3807 }
3808 }
3809
3810 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3811 {
3812 if (!mHasWireTrim) {
3813 return false; // a parametric-rectangle trim carries no per-edge curve to sample
3814 }
3815 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double theta) { return pointAt(theta, phi); }, samples);
3816 }
3817
3818 private:
3819 Vec3 mCenter;
3820 Vec3 mAxisU;
3821 Vec3 mAxisV;
3822 Vec3 mAxisW;
3823 double mRadius = 0.;
3824 double mThetaMin = 0.;
3825 double mThetaMax = kPi;
3826 double mPhiStart = 0.;
3827 double mPhiSweep = kTwoPi;
3828 double mNormalSign = 1.;
3829 bool mHasWireTrim = false;
3830 CurveWire mTrimOuter;
3831 std::vector<CurveWire> mTrimInner;
3832};
3833
3836{
3837 public:
3838 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin,
3839 double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep,
3840 bool innerWall, std::string& errorMessage)
3841 {
3842 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radiusAtMin) ||
3843 !std::isfinite(radiusAtMax) || !std::isfinite(heightMin) || !std::isfinite(heightMax) ||
3844 !std::isfinite(phiStart) || !std::isfinite(phiSweep)) {
3845 errorMessage = "conical surface parameter is non-finite";
3846 return false;
3847 }
3848 if (radiusAtMin < -kTolerance || radiusAtMax < -kTolerance ||
3849 std::max(radiusAtMin, radiusAtMax) <= kTolerance) {
3850 errorMessage = "conical surface needs non-negative radii, at least one positive";
3851 return false;
3852 }
3853 if (heightMax - heightMin <= kTolerance) {
3854 errorMessage = "conical surface needs a positive height range";
3855 return false;
3856 }
3857 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
3858 errorMessage = "conical surface needs an angular sweep in (0, 2pi]";
3859 return false;
3860 }
3861 if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
3862 return false;
3863 }
3864
3865 mCenter = centerPoint;
3866 mHeightMin = heightMin;
3867 mHeightMax = heightMax;
3868 mSlope = (radiusAtMax - radiusAtMin) / (heightMax - heightMin);
3869 mRadius0 = radiusAtMin - mSlope * heightMin; // radius at h = 0 of the linear law
3870 mPhiStart = phiStart;
3871 mPhiSweep = std::min(phiSweep, kTwoPi);
3872 mNormalSign = innerWall ? -1. : 1.;
3873 mPhiTolerance = angularTolerance(meanRadius());
3874 return true;
3875 }
3876
3878 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin,
3879 double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep,
3880 bool innerWall, const std::vector<Curve2D>& outerTrim,
3881 const std::vector<std::vector<Curve2D>>& innerTrims, std::string& errorMessage,
3882 double joinTolerance = kWireJoinTolerance)
3883 {
3884 if (!initialize(centerPoint, axis, referenceAxisU, radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart,
3885 phiSweep, innerWall, errorMessage)) {
3886 return false;
3887 }
3888 Vec2 lower, upper;
3889 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
3890 parametricMetricOf(*this), joinTolerance)) {
3891 return false;
3892 }
3893 mPhiStart = lower.uCoord;
3894 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
3895 mHeightMin = lower.vCoord;
3896 mHeightMax = upper.vCoord;
3897 mPhiTolerance = angularTolerance(meanRadius());
3898 mHasWireTrim = true;
3899 return true;
3900 }
3901
3902 bool hasWireTrim() const { return mHasWireTrim; }
3903
3905 bool pointInTrim(double phi, double height, bool* boundary = nullptr) const
3906 {
3907 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
3908 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this));
3909 }
3910
3911 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
3912
3913 double radiusAt(double height) const { return mRadius0 + mSlope * height; }
3914
3915 double meanRadius() const { return 0.5 * (radiusAt(mHeightMin) + radiusAt(mHeightMax)); }
3916
3917 Vec3 toLocal(const Vec3& point) const
3918 {
3919 const Vec3 relativePoint = point - mCenter;
3920 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
3921 }
3922
3923 bool heightInRange(double height) const
3924 {
3925 return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance;
3926 }
3927
3928 bool phiInSweep(double phi) const
3929 {
3930 return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance);
3931 }
3932
3933 Vec3 pointAt(double phi, double height) const
3934 {
3935 return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * radiusAt(height);
3936 }
3937
3938 bool containsPointOnSurface(const Vec3& point) const override
3939 {
3940 const Vec3 localPoint = toLocal(point);
3941 const double surfaceRadius = radiusAt(localPoint.zCoord);
3942 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3943 // |rho - r(h)| overestimates the true surface distance by sqrt(1 + slope^2)
3944 if (std::abs(radialDistance - surfaceRadius) > kTolerance * std::sqrt(1. + mSlope * mSlope)) {
3945 return false;
3946 }
3947 if (radialDistance <= kTolerance) {
3948 return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined near the apex
3949 }
3950 const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord);
3951 if (mHasWireTrim) {
3952 return pointInTrim(phi, localPoint.zCoord);
3953 }
3954 return heightInRange(localPoint.zCoord) && phiInSweep(phi);
3955 }
3956
3957 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3958 double maxDistance, std::vector<RayHit>& hits) const override
3959 {
3960 const Vec3 localOrigin = toLocal(rayOrigin);
3961 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
3962
3963 // (ox + t dx)^2 + (oy + t dy)^2 = (radius0 + slope * (oz + t dz))^2
3964 const double surfaceRadiusAtOrigin = mRadius0 + mSlope * localOrigin.zCoord;
3965 const double quadraticA = localDirection.xCoord * localDirection.xCoord +
3966 localDirection.yCoord * localDirection.yCoord -
3967 mSlope * mSlope * localDirection.zCoord * localDirection.zCoord;
3968 const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord +
3969 localOrigin.yCoord * localDirection.yCoord -
3970 mSlope * localDirection.zCoord * surfaceRadiusAtOrigin);
3971 const double quadraticC = localOrigin.xCoord * localOrigin.xCoord +
3972 localOrigin.yCoord * localOrigin.yCoord -
3973 surfaceRadiusAtOrigin * surfaceRadiusAtOrigin;
3974
3975 std::array<double, 2> candidates{};
3976 int candidateCount = 0;
3977 if (std::abs(quadraticA) <= kToleranceSq) {
3978 if (std::abs(quadraticB) <= kToleranceSq) {
3979 return; // ray runs along the cone surface or its asymptote: no transversal crossing
3980 }
3981 candidates[candidateCount++] = -quadraticC / quadraticB;
3982 } else {
3983 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
3984 if (discriminant <= 0.) {
3985 return;
3986 }
3987 const double sqrtDiscriminant = std::sqrt(discriminant);
3988 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
3989 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
3990 if (sameIntersection(firstRoot, secondRoot)) {
3991 return; // tangential graze (this also covers rays through the exact apex)
3992 }
3993 candidates[candidateCount++] = std::min(firstRoot, secondRoot);
3994 candidates[candidateCount++] = std::max(firstRoot, secondRoot);
3995 }
3996
3997 for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) {
3998 const double candidate = candidates[candidateIndex];
3999 if (candidate < minDistance || candidate > maxDistance) {
4000 continue;
4001 }
4002 const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord;
4003 const double hitSurfaceRadius = radiusAt(hitHeight);
4004 if (hitSurfaceRadius < -kTolerance) {
4005 continue; // mirror nappe of the infinite cone
4006 }
4007 const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord;
4008 const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord;
4009 const double radialDistance = std::hypot(hitU, hitV);
4010 if (radialDistance <= kTolerance) {
4011 continue; // apex hit: the normal is undefined there
4012 }
4013 const double hitPhi = std::atan2(hitV, hitU);
4014 bool onTrimBoundary = false;
4015 if (mHasWireTrim) {
4016 if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) {
4017 continue;
4018 }
4019 } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) {
4020 continue;
4021 }
4022 const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope);
4023 const Vec3 hitNormal =
4024 (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance) - mAxisW * mSlope) * normalScale;
4025 hits.push_back({candidate, hitNormal, onTrimBoundary});
4026 }
4027 }
4028
4030 double distanceSqToPatch(const Vec3& point) const override
4031 {
4032 const Vec3 localPoint = toLocal(point);
4033 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
4034 if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) {
4035 return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord},
4036 Vec2{radiusAt(mHeightMin), mHeightMin}, Vec2{radiusAt(mHeightMax), mHeightMax});
4037 }
4038 const double endPhi = mPhiStart + mPhiSweep;
4039 const double distanceToStartSeam =
4040 pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax));
4041 const double distanceToEndSeam =
4042 pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
4043 return std::min(distanceToStartSeam, distanceToEndSeam);
4044 }
4045
4046 Vec3 normalAt(const Vec3& point) const override
4047 {
4048 const Vec3 localPoint = toLocal(point);
4049 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
4050 const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope);
4051 if (radialDistance <= kTolerance) {
4052 return (mAxisU - mAxisW * mSlope) * normalScale; // ill-defined on the axis; stable fallback
4053 }
4054 return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance) -
4055 mAxisW * mSlope) *
4056 normalScale;
4057 }
4058
4060 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
4061 {
4062 coneParametricMetric(radiusAt(uv.vCoord), mSlope, gUU, gUV, gVV);
4063 }
4064
4066 double capacityContribution() const override
4067 {
4068 if (mHasWireTrim) {
4069 const double centreU = dot(mCenter, mAxisU);
4070 const double centreV = dot(mCenter, mAxisV);
4071 const double centreW = dot(mCenter, mAxisW);
4072 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double height) {
4073 const double localRadius = radiusAt(height);
4074 return mNormalSign / 3. * localRadius *
4075 (centreU * std::sin(phi) - centreV * std::cos(phi) +
4076 (localRadius - mSlope * (centreW + height)) * phi);
4077 });
4078 }
4079 const double endPhi = mPhiStart + mPhiSweep;
4080 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
4081 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
4082 const double radiusIntegral = mRadius0 * (mHeightMax - mHeightMin) +
4083 0.5 * mSlope * (mHeightMax * mHeightMax - mHeightMin * mHeightMin);
4084 return mNormalSign * radiusIntegral *
4085 (phiFactor + (mRadius0 - mSlope * dot(mCenter, mAxisW)) * mPhiSweep) / 3.;
4086 }
4087
4088 bool capacityIsExact() const override { return !mHasWireTrim; }
4089
4090 void conservativeBounds(Vec3& lower, Vec3& upper) const override
4091 {
4092 for (const double height : {mHeightMin, mHeightMax}) {
4093 const Vec3 rimCenter = mCenter + mAxisW * height;
4094 const double rimRadius = std::max(0., radiusAt(height));
4095 for (int dimension = 0; dimension < 3; ++dimension) {
4096 const double radialExtent =
4097 rimRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension));
4098 const double centerValue = component(rimCenter, dimension);
4099 if (dimension == 0) {
4100 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
4101 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
4102 } else if (dimension == 1) {
4103 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
4104 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
4105 } else {
4106 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
4107 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
4108 }
4109 }
4110 }
4111 }
4112
4114 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
4115 {
4116 appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax,
4117 std::max(0., radiusAt(mHeightMin)), std::max(0., radiusAt(mHeightMax)), boxes);
4118 }
4119
4120 int rimSegments() const
4121 {
4122 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
4123 }
4124
4125 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
4126 {
4127 if (mHasWireTrim) {
4128 appendCurveTrimMesh(mTrimOuter, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles);
4129 return;
4130 }
4131 const int segments = rimSegments();
4132 const int firstVertexIndex = static_cast<int>(vertices.size());
4133 for (int step = 0; step <= segments; ++step) {
4134 const double phi = mPhiStart + mPhiSweep * step / segments;
4135 vertices.push_back(pointAt(phi, mHeightMin));
4136 vertices.push_back(pointAt(phi, mHeightMax));
4137 }
4138 for (int step = 0; step < segments; ++step) {
4139 const int base = firstVertexIndex + 2 * step;
4140 // skip triangles that collapse at an apex rim
4141 if (radiusAt(mHeightMin) > kTolerance) {
4142 triangles.push_back({base, base + 2, base + 3});
4143 }
4144 if (radiusAt(mHeightMax) > kTolerance) {
4145 triangles.push_back({base, base + 3, base + 1});
4146 }
4147 }
4148 }
4149
4150 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
4151 {
4152 if (mHasWireTrim) {
4153 // the (phi, h) -> 3D map is orientation-consistent with the outward normal (as for the
4154 // cylinder), so the sign is just mNormalSign
4155 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges);
4156 return;
4157 }
4158 // same boundary orientation as the cylinder; an apex rim degenerates to a point and is
4159 // skipped so an apex cone closes against just one cap
4160 const int segments = rimSegments();
4161 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
4162 if (mNormalSign > 0.) {
4163 edges.emplace_back(edgeStart, edgeEnd);
4164 } else {
4165 edges.emplace_back(edgeEnd, edgeStart);
4166 }
4167 };
4168 for (int step = 0; step < segments; ++step) {
4169 const double phi = mPhiStart + mPhiSweep * step / segments;
4170 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments;
4171 if (radiusAt(mHeightMin) > kTolerance) {
4172 emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin));
4173 }
4174 if (radiusAt(mHeightMax) > kTolerance) {
4175 emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax));
4176 }
4177 }
4178 if (!fullSweep()) {
4179 const double endPhi = mPhiStart + mPhiSweep;
4180 emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
4181 emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin));
4182 }
4183 }
4184
4185 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
4186 {
4187 if (!mHasWireTrim) {
4188 return false; // a parametric-rectangle trim carries no per-edge curve to sample
4189 }
4190 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples);
4191 }
4192
4193 private:
4194 Vec3 mCenter;
4195 Vec3 mAxisU;
4196 Vec3 mAxisV;
4197 Vec3 mAxisW;
4198 double mRadius0 = 0.;
4199 double mSlope = 0.;
4200 double mHeightMin = 0.;
4201 double mHeightMax = 0.;
4202 double mPhiStart = 0.;
4203 double mPhiSweep = kTwoPi;
4204 double mPhiTolerance = 0.;
4205 double mNormalSign = 1.;
4206 bool mHasWireTrim = false;
4207 CurveWire mTrimOuter;
4208 std::vector<CurveWire> mTrimInner;
4209};
4210
4214{
4215 public:
4216 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius,
4217 double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep,
4218 bool innerWall, std::string& errorMessage)
4219 {
4220 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(majorRadius) ||
4221 !std::isfinite(minorRadius) || !std::isfinite(phiStart) || !std::isfinite(phiSweep) ||
4222 !std::isfinite(tubeStart) || !std::isfinite(tubeSweep)) {
4223 errorMessage = "toroidal surface parameter is non-finite";
4224 return false;
4225 }
4226 if (majorRadius <= kTolerance || minorRadius <= kTolerance) {
4227 errorMessage = "toroidal surface needs positive major and minor radii";
4228 return false;
4229 }
4230 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
4231 errorMessage = "toroidal surface needs a ring sweep in (0, 2pi]";
4232 return false;
4233 }
4234 if (tubeSweep <= kTolerance || tubeSweep > kTwoPi + kTolerance) {
4235 errorMessage = "toroidal surface needs a tube sweep in (0, 2pi]";
4236 return false;
4237 }
4238 if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
4239 return false;
4240 }
4241
4242 mCenter = centerPoint;
4243 mMajorRadius = majorRadius;
4244 mMinorRadius = minorRadius;
4245 mRingTolerance = angularTolerance(mMajorRadius);
4246 mTubeTolerance = angularTolerance(mMinorRadius);
4247 mPhiStart = phiStart;
4248 mPhiSweep = std::min(phiSweep, kTwoPi);
4249 mTubeStart = tubeStart;
4250 mTubeSweep = std::min(tubeSweep, kTwoPi);
4251 mNormalSign = innerWall ? -1. : 1.;
4252 return true;
4253 }
4254
4256 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius,
4257 double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep,
4258 bool innerWall, const std::vector<Curve2D>& outerTrim,
4259 const std::vector<std::vector<Curve2D>>& innerTrims, std::string& errorMessage,
4260 double joinTolerance = kWireJoinTolerance)
4261 {
4262 if (!initialize(centerPoint, axis, referenceAxisU, majorRadius, minorRadius, phiStart, phiSweep, tubeStart,
4263 tubeSweep, innerWall, errorMessage)) {
4264 return false;
4265 }
4266 Vec2 lower, upper;
4267 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
4268 parametricMetricOf(*this), joinTolerance)) {
4269 return false;
4270 }
4271 if (upper.vCoord - lower.vCoord > kTwoPi + kTolerance) {
4272 errorMessage = "toroidal trim wire spans more than a full turn in the tube angle";
4273 return false;
4274 }
4275 mPhiStart = lower.uCoord;
4276 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
4277 mTubeStart = lower.vCoord;
4278 mTubeSweep = std::min(kTwoPi, upper.vCoord - lower.vCoord);
4279 mHasWireTrim = true;
4280 return true;
4281 }
4282
4283 bool hasWireTrim() const { return mHasWireTrim; }
4284
4286 bool pointInTrim(double phiRing, double phiTube, bool* boundary = nullptr) const
4287 {
4288 const double uCoord = unwrapAngleInto(phiRing, mPhiStart, mPhiStart + mPhiSweep);
4289 const double vCoord = unwrapAngleInto(phiTube, mTubeStart, mTubeStart + mTubeSweep);
4290 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, vCoord}, boundary, parametricMetricOf(*this));
4291 }
4292
4293 bool fullRingSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
4294 bool fullTubeSweep() const { return mTubeSweep >= kTwoPi - kTolerance; }
4295
4296 Vec3 toLocal(const Vec3& point) const
4297 {
4298 const Vec3 relativePoint = point - mCenter;
4299 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
4300 }
4301
4302 bool ringInSweep(double phiRing) const
4303 {
4304 return angleInSweepRange(phiRing, mPhiStart, mPhiSweep, mRingTolerance);
4305 }
4306
4307 bool tubeInSweep(double phiTube) const
4308 {
4309 return angleInSweepRange(phiTube, mTubeStart, mTubeSweep, mTubeTolerance);
4310 }
4311
4312 Vec3 pointAt(double phiRing, double phiTube) const
4313 {
4314 const double ringRadius = mMajorRadius + mMinorRadius * std::cos(phiTube);
4315 return mCenter + (mAxisU * std::cos(phiRing) + mAxisV * std::sin(phiRing)) * ringRadius +
4316 mAxisW * (mMinorRadius * std::sin(phiTube));
4317 }
4318
4320 Vec3 localNormal(const Vec3& localPoint) const
4321 {
4322 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4323 if (rho <= kTolerance) {
4324 return mAxisW * (localPoint.zCoord >= 0. ? mNormalSign : -mNormalSign);
4325 }
4326 const double radialFactor = (rho - mMajorRadius) / rho;
4327 Vec3 normal{radialFactor * localPoint.xCoord, radialFactor * localPoint.yCoord, localPoint.zCoord};
4328 const double length = norm(normal);
4329 if (length <= kTolerance) {
4330 return mAxisU * mNormalSign;
4331 }
4332 return (mAxisU * normal.xCoord + mAxisV * normal.yCoord + mAxisW * normal.zCoord) * (mNormalSign / length);
4333 }
4334
4335 bool containsPointOnSurface(const Vec3& point) const override
4336 {
4337 const Vec3 localPoint = toLocal(point);
4338 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4339 const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius;
4340 if (std::abs(meridianDistance) > kTolerance) {
4341 return false;
4342 }
4343 const double phiTube = std::atan2(localPoint.zCoord, rho - mMajorRadius);
4344 if (rho <= kTolerance) {
4345 return false; // on the axis phiRing is undefined (only reachable on a horn/spindle torus)
4346 }
4347 const double phiRing = std::atan2(localPoint.yCoord, localPoint.xCoord);
4348 if (mHasWireTrim) {
4349 return pointInTrim(phiRing, phiTube);
4350 }
4351 return ringInSweep(phiRing) && tubeInSweep(phiTube);
4352 }
4353
4354 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
4355 double maxDistance, std::vector<RayHit>& hits) const override
4356 {
4357 const Vec3 localOrigin = toLocal(rayOrigin);
4358 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
4359
4360 // Torus implicit form (local): (|X|^2 + R^2 - r^2)^2 = 4 R^2 (x^2 + y^2). Substituting the ray
4361 // X = O + t D gives a quartic in t whose leading coefficient is |D|^4 > 0.
4362 const double dirDotDir = normSq(localDirection);
4363 if (dirDotDir <= kToleranceSq) {
4364 return; // degenerate direction
4365 }
4366 const double originDotDir = dot(localOrigin, localDirection);
4367 const double originDotOrigin = normSq(localOrigin);
4368 const double constantK = mMajorRadius * mMajorRadius - mMinorRadius * mMinorRadius;
4369 const double transverseE = localDirection.xCoord * localDirection.xCoord +
4370 localDirection.yCoord * localDirection.yCoord;
4371 const double transverseF = localOrigin.xCoord * localDirection.xCoord +
4372 localOrigin.yCoord * localDirection.yCoord;
4373 const double transverseG = localOrigin.xCoord * localOrigin.xCoord +
4374 localOrigin.yCoord * localOrigin.yCoord;
4375 const double fourRSquared = 4. * mMajorRadius * mMajorRadius;
4376
4377 const double coeff4 = dirDotDir * dirDotDir;
4378 const double coeff3 = 4. * dirDotDir * originDotDir;
4379 const double coeff2 =
4380 4. * originDotDir * originDotDir + 2. * dirDotDir * (originDotOrigin + constantK) - fourRSquared * transverseE;
4381 const double coeff1 = 4. * originDotDir * (originDotOrigin + constantK) - 2. * fourRSquared * transverseF;
4382 const double coeff0 = (originDotOrigin + constantK) * (originDotOrigin + constantK) - fourRSquared * transverseG;
4383
4384 QuarticRoots candidates = solveQuarticReal(coeff4, coeff3, coeff2, coeff1, coeff0);
4385 if (candidates.empty()) {
4386 return;
4387 }
4388 std::sort(candidates.begin(), candidates.end());
4389
4390 // an even-sized cluster of near-equal roots is a tangency and is dropped; an odd one is one crossing at its mean
4391 size_t rootIndex = 0;
4392 while (rootIndex < candidates.size()) {
4393 size_t clusterEnd = rootIndex + 1;
4394 double clusterSum = candidates[rootIndex];
4395 while (clusterEnd < candidates.size() && sameIntersection(candidates[clusterEnd], candidates[clusterEnd - 1])) {
4396 clusterSum += candidates[clusterEnd];
4397 ++clusterEnd;
4398 }
4399 const size_t clusterSize = clusterEnd - rootIndex;
4400 rootIndex = clusterEnd;
4401 if ((clusterSize & 1u) == 0u) {
4402 continue; // tangential graze
4403 }
4404 const double candidate = clusterSum / static_cast<double>(clusterSize);
4405 if (candidate < minDistance || candidate > maxDistance) {
4406 continue;
4407 }
4408 const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate);
4409 const double rho = std::hypot(localHit.xCoord, localHit.yCoord);
4410 if (rho <= kTolerance) {
4411 continue;
4412 }
4413 const double phiTube = std::atan2(localHit.zCoord, rho - mMajorRadius);
4414 const double phiRing = std::atan2(localHit.yCoord, localHit.xCoord);
4415 bool onTrimBoundary = false;
4416 if (mHasWireTrim) {
4417 if (!pointInTrim(phiRing, phiTube, &onTrimBoundary)) {
4418 continue;
4419 }
4420 } else if (!ringInSweep(phiRing) || !tubeInSweep(phiTube)) {
4421 continue;
4422 }
4423 hits.push_back({candidate, localNormal(localHit), onTrimBoundary});
4424 }
4425 }
4426
4428 double distanceSqToPatch(const Vec3& point) const override
4429 {
4430 const Vec3 localPoint = toLocal(point);
4431 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4432 const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius;
4433 return meridianDistance * meridianDistance;
4434 }
4435
4436 Vec3 normalAt(const Vec3& point) const override { return localNormal(toLocal(point)); }
4437
4439 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
4440 {
4441 torusParametricMetric(mMajorRadius, mMinorRadius, uv.vCoord, gUU, gUV, gVV);
4442 }
4443
4445 double capacityContribution() const override
4446 {
4447 if (mHasWireTrim) {
4448 const double centreU = dot(mCenter, mAxisU);
4449 const double centreV = dot(mCenter, mAxisV);
4450 const double centreW = dot(mCenter, mAxisW);
4451 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phiRing, double phiTube) {
4452 const double cosTube = std::cos(phiTube);
4453 const double sinTube = std::sin(phiTube);
4454 const double rho = mMajorRadius + mMinorRadius * cosTube;
4455 return mNormalSign * mMinorRadius * rho / 3. *
4456 (cosTube * (centreU * std::sin(phiRing) - centreV * std::cos(phiRing)) +
4457 (centreW * sinTube + rho * cosTube + mMinorRadius * sinTube * sinTube) * phiRing);
4458 });
4459 }
4460 // Closed form over u in [u0, u1] (ring) and v in [v0, v1] (tube).
4461 const double majorR = mMajorRadius;
4462 const double minorR = mMinorRadius;
4463 const double u0 = mPhiStart, u1 = mPhiStart + mPhiSweep;
4464 const double v0 = mTubeStart, v1 = mTubeStart + mTubeSweep;
4465 const double centerU = dot(mCenter, mAxisU);
4466 const double centerV = dot(mCenter, mAxisV);
4467 const double centerW = dot(mCenter, mAxisW);
4468 const double deltaU = u1 - u0;
4469 const double deltaV = v1 - v0;
4470 const double sinIntegralU = std::sin(u1) - std::sin(u0); // integral cos u du
4471 const double cosIntegralU = std::cos(u0) - std::cos(u1); // integral sin u du
4472 const double sinIntegralV = std::sin(v1) - std::sin(v0); // integral cos v dv
4473 const double sinFromCosV = std::cos(v0) - std::cos(v1); // integral sin v dv
4474 const double cosSquaredV = 0.5 * deltaV + 0.25 * (std::sin(2. * v1) - std::sin(2. * v0)); // integral cos^2 v dv
4475 const double sinCosV = 0.25 * (std::cos(2. * v0) - std::cos(2. * v1)); // integral sin v cos v dv
4476
4477 // centre-independent part, integrated over v then multiplied by the ring span
4478 const double centerlessV =
4479 minorR * ((majorR * majorR + minorR * minorR) * sinIntegralV + majorR * minorR * deltaV +
4480 majorR * minorR * cosSquaredV);
4481 // W component of the centre offset
4482 const double centerWpart = minorR * (majorR * sinFromCosV + minorR * sinCosV);
4483 // U/V components of the centre offset (ring-angle dependent)
4484 const double centerUVpart =
4485 (centerU * sinIntegralU + centerV * cosIntegralU) * minorR * (majorR * sinIntegralV + minorR * cosSquaredV);
4486
4487 const double total = deltaU * centerlessV + deltaU * centerW * centerWpart + centerUVpart;
4488 return mNormalSign * total / 3.;
4489 }
4490
4491 bool capacityIsExact() const override { return !mHasWireTrim; }
4492
4493 void conservativeBounds(Vec3& lower, Vec3& upper) const override
4494 {
4495 // conservative: the AABB of the full torus (partial sweeps get a larger box)
4496 const double outerRadius = mMajorRadius + mMinorRadius;
4497 for (int dimension = 0; dimension < 3; ++dimension) {
4498 const double radialExtent = outerRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension)) +
4499 mMinorRadius * std::abs(component(mAxisW, dimension));
4500 const double centerValue = component(mCenter, dimension);
4501 if (dimension == 0) {
4502 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
4503 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
4504 } else if (dimension == 1) {
4505 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
4506 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
4507 } else {
4508 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
4509 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
4510 }
4511 }
4512 }
4513
4515 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
4516 {
4517 if (mMajorRadius < mMinorRadius) {
4519 return;
4520 }
4521 const int ringChunks = coverChunkCount(kTwoPi);
4522 const int tubeChunks = coverChunkCount(kTwoPi);
4523 for (int ringChunk = 0; ringChunk < ringChunks; ++ringChunk) {
4524 const double ringLow = kTwoPi * ringChunk / ringChunks;
4525 const double ringHigh = kTwoPi * (ringChunk + 1) / ringChunks;
4526 for (int tubeChunk = 0; tubeChunk < tubeChunks; ++tubeChunk) {
4527 const double tubeLow = kTwoPi * tubeChunk / tubeChunks;
4528 const double tubeHigh = kTwoPi * (tubeChunk + 1) / tubeChunks;
4529 double lower[3];
4530 double upper[3];
4531 for (int dimension = 0; dimension < 3; ++dimension) {
4532 double inPlaneLow = 0.;
4533 double inPlaneHigh = 0.;
4534 sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), ringLow, ringHigh, inPlaneLow,
4535 inPlaneHigh);
4536 // the coordinate is p(u) (R + r cos v) + w r sin v; with R + r cos v >= 0 it is
4537 // monotone in p, so each extreme is a v sinusoid taken at p's own extreme
4538 const double axisComponent = component(mAxisW, dimension);
4539 const double high = sinusoidMaximum(inPlaneHigh, axisComponent, tubeLow, tubeHigh);
4540 const double low = sinusoidMinimum(inPlaneLow, axisComponent, tubeLow, tubeHigh);
4541 lower[dimension] = component(mCenter, dimension) + inPlaneLow * mMajorRadius + mMinorRadius * low;
4542 upper[dimension] = component(mCenter, dimension) + inPlaneHigh * mMajorRadius + mMinorRadius * high;
4543 }
4544 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
4545 }
4546 }
4547 }
4548
4549 int ringSegments() const
4550 {
4551 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
4552 }
4553
4554 int tubeSegments() const
4555 {
4556 return std::max(1, static_cast<int>(std::lround(kArcSamples * mTubeSweep / kTwoPi)));
4557 }
4558
4559 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
4560 {
4561 if (mHasWireTrim) {
4562 appendCurveTrimMesh(mTrimOuter, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, vertices, triangles);
4563 return;
4564 }
4565 const int ringSteps = ringSegments();
4566 const int tubeSteps = tubeSegments();
4567 const int firstVertexIndex = static_cast<int>(vertices.size());
4568 for (int ringStep = 0; ringStep <= ringSteps; ++ringStep) {
4569 const double phiRing = mPhiStart + mPhiSweep * ringStep / ringSteps;
4570 for (int tubeStep = 0; tubeStep <= tubeSteps; ++tubeStep) {
4571 vertices.push_back(pointAt(phiRing, mTubeStart + mTubeSweep * tubeStep / tubeSteps));
4572 }
4573 }
4574 const int rowLength = tubeSteps + 1;
4575 for (int ringStep = 0; ringStep < ringSteps; ++ringStep) {
4576 for (int tubeStep = 0; tubeStep < tubeSteps; ++tubeStep) {
4577 const int base = firstVertexIndex + ringStep * rowLength + tubeStep;
4578 triangles.push_back({base, base + rowLength, base + rowLength + 1});
4579 triangles.push_back({base, base + rowLength + 1, base + 1});
4580 }
4581 }
4582 }
4583
4584 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
4585 {
4586 if (mHasWireTrim) {
4587 // the (phiRing, phiTube) -> 3D map is orientation-consistent with the outward normal, so
4588 // the sign is just mNormalSign (as for the cylinder and cone)
4589 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, mNormalSign, edges);
4590 return;
4591 }
4592 // boundary of the (phiRing, phiTube) rectangle traversed counter-clockwise as seen along the
4593 // outward normal; a full sweep in either angle has no seam there, so it is skipped
4594 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
4595 if (mNormalSign > 0.) {
4596 edges.emplace_back(edgeStart, edgeEnd);
4597 } else {
4598 edges.emplace_back(edgeEnd, edgeStart);
4599 }
4600 };
4601 const int ringSteps = ringSegments();
4602 const int tubeSteps = tubeSegments();
4603 const double endRing = mPhiStart + mPhiSweep;
4604 const double endTube = mTubeStart + mTubeSweep;
4605 if (!fullTubeSweep()) {
4606 for (int step = 0; step < ringSteps; ++step) {
4607 const double phiRing = mPhiStart + mPhiSweep * step / ringSteps;
4608 const double nextRing = mPhiStart + mPhiSweep * (step + 1) / ringSteps;
4609 emitEdge(pointAt(phiRing, mTubeStart), pointAt(nextRing, mTubeStart)); // +phiRing at tubeStart
4610 emitEdge(pointAt(nextRing, endTube), pointAt(phiRing, endTube)); // -phiRing at tubeEnd
4611 }
4612 }
4613 if (!fullRingSweep()) {
4614 for (int step = 0; step < tubeSteps; ++step) {
4615 const double phiTube = mTubeStart + mTubeSweep * step / tubeSteps;
4616 const double nextTube = mTubeStart + mTubeSweep * (step + 1) / tubeSteps;
4617 emitEdge(pointAt(endRing, phiTube), pointAt(endRing, nextTube)); // +phiTube at ringEnd
4618 emitEdge(pointAt(mPhiStart, nextTube), pointAt(mPhiStart, phiTube)); // -phiTube at ringStart
4619 }
4620 }
4621 }
4622
4623 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
4624 {
4625 if (!mHasWireTrim) {
4626 return false; // a parametric-rectangle trim carries no per-edge curve to sample
4627 }
4628 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, samples);
4629 }
4630
4631 private:
4632 Vec3 mCenter;
4633 Vec3 mAxisU;
4634 Vec3 mAxisV;
4635 Vec3 mAxisW;
4636 double mMajorRadius = 0.;
4637 double mMinorRadius = 0.;
4638 double mPhiStart = 0.;
4639 double mPhiSweep = kTwoPi;
4640 double mTubeStart = 0.;
4641 double mTubeSweep = kTwoPi;
4642 double mRingTolerance = 0.;
4643 double mTubeTolerance = 0.;
4644 double mNormalSign = 1.;
4645 bool mHasWireTrim = false;
4646 CurveWire mTrimOuter;
4647 std::vector<CurveWire> mTrimInner;
4648};
4649
4652enum class RimState {
4653 Matched = 0,
4654 Reversed,
4655 Boundary,
4657};
4658
4674
4728
4730inline void measureSharedEdgeDeviation(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
4732{
4733 // edgeId -> the (surface, slot) pairs claiming it
4734 std::map<uint32_t, std::vector<std::pair<int, size_t>>> claims;
4735 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
4736 if (surfaces[surfaceIndex] == nullptr) {
4737 continue;
4738 }
4739 const auto& refs = surfaces[surfaceIndex]->boundaryEdges();
4740 for (size_t slot = 0; slot < refs.size(); ++slot) {
4741 if (refs[slot].degenerate) {
4742 continue; // a point has no partner and no length to disagree over
4743 }
4744 // unanchored claims are collected too, so that an edge whose other side is a
4745 // parametric-rectangle face is counted as *unmeasured* rather than silently dropped
4746 claims[refs[slot].edgeId].emplace_back(static_cast<int>(surfaceIndex), slot);
4747 }
4748 }
4749
4750 std::vector<Vec3> first;
4751 std::vector<Vec3> second;
4752 for (const auto& [edgeId, holders] : claims) {
4753 if (holders.size() != 2) {
4754 continue;
4755 }
4756 const auto& [firstSurface, firstSlot] = holders[0];
4757 const auto& [secondSurface, secondSlot] = holders[1];
4758 if (!surfaces[static_cast<size_t>(firstSurface)]->sampleTrimCurve(firstSlot, first) ||
4759 !surfaces[static_cast<size_t>(secondSurface)]->sampleTrimCurve(secondSlot, second) || first.size() < 2 ||
4760 second.size() < 2) {
4761 ++report.sharedEdgesUnmeasured;
4762 continue;
4763 }
4764 ++report.sharedEdgesMeasured;
4765 auto worstAgainst = [](const std::vector<Vec3>& probes, const std::vector<Vec3>& polyline, Vec3& where) {
4766 double worst = 0.;
4767 for (const Vec3& probe : probes) {
4768 double nearest = std::numeric_limits<double>::infinity();
4769 for (size_t segment = 0; segment + 1 < polyline.size(); ++segment) {
4770 nearest = std::min(nearest, pointSegmentDistanceSq(probe, polyline[segment], polyline[segment + 1]));
4771 }
4772 if (nearest > worst) {
4773 worst = nearest;
4774 where = probe;
4775 }
4776 }
4777 return std::sqrt(worst);
4778 };
4779 Vec3 forwardPoint{};
4780 Vec3 backwardPoint{};
4781 const double forwardWorst = worstAgainst(first, second, forwardPoint);
4782 const double backwardWorst = worstAgainst(second, first, backwardPoint);
4783 const double deviation = std::max(forwardWorst, backwardWorst);
4784 if (deviation > report.maxSharedEdgeDeviation) {
4785 report.maxSharedEdgeDeviation = deviation;
4786 report.maxSharedEdgeDeviationEdge = edgeId;
4787 report.maxSharedEdgeDeviationPoint = forwardWorst >= backwardWorst ? forwardPoint : backwardPoint;
4788 report.maxSharedEdgeDeviationFaces[0] = firstSurface;
4789 report.maxSharedEdgeDeviationFaces[1] = secondSurface;
4790 }
4791 }
4792}
4793
4795inline void measureRimClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces, double epsilon,
4797{
4798 report.rimEpsilon = epsilon;
4799
4800 std::vector<SurfaceRim> rims;
4801 std::vector<int> rimIndexOnSurface;
4802 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
4803 if (surfaces[surfaceIndex] == nullptr) {
4804 continue;
4805 }
4806 const size_t firstNewRim = rims.size();
4807 surfaces[surfaceIndex]->appendRims(rims);
4808 for (size_t rimIndex = firstNewRim; rimIndex < rims.size(); ++rimIndex) {
4809 rims[rimIndex].surfaceIndex = static_cast<int>(surfaceIndex);
4810 rimIndexOnSurface.push_back(static_cast<int>(rimIndex - firstNewRim));
4811 }
4812 }
4813 report.rims = static_cast<int>(rims.size());
4814 if (rims.empty()) {
4815 return;
4816 }
4817
4818 // Flatten to chords with each chord's sagitta: two polylines of one curve differ by it, so it widens the match band.
4819 // The sagitta is estimated per chord from the turn angle at smooth vertices; a corner has none.
4820 constexpr double kMaxSmoothTurn = 0.52; // ~30 degrees; a rim sampled at kArcSamples turns by 15
4821 struct Chord {
4822 Vec3 start;
4823 Vec3 end;
4824 int surfaceIndex;
4825 double resolution;
4826 };
4827 std::vector<Chord> chords;
4828 std::vector<std::pair<size_t, size_t>> chordRange(rims.size()); // [first, last) chord of each rim
4829 for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) {
4830 const SurfaceRim& rim = rims[rimIndex];
4831 chordRange[rimIndex].first = chords.size();
4832 const size_t pointCount = rim.points.size();
4833 std::vector<double> vertexSagitta(pointCount, 0.);
4834 const size_t interiorCount = rim.closed ? pointCount : (pointCount >= 2 ? pointCount - 2 : 0);
4835 for (size_t offset = 0; offset < interiorCount; ++offset) {
4836 const size_t middle = rim.closed ? offset : offset + 1;
4837 const Vec3 incoming = rim.points[middle] - rim.points[(middle + pointCount - 1) % pointCount];
4838 const Vec3 outgoing = rim.points[(middle + 1) % pointCount] - rim.points[middle];
4839 const double incomingLength = norm(incoming);
4840 const double outgoingLength = norm(outgoing);
4841 if (incomingLength <= kTolerance || outgoingLength <= kTolerance) {
4842 continue;
4843 }
4844 const double turn = std::acos(std::clamp(dot(incoming, outgoing) / (incomingLength * outgoingLength), -1., 1.));
4845 if (turn > kMaxSmoothTurn) {
4846 continue; // a corner of the trim, not a sample of a smooth run
4847 }
4848 vertexSagitta[middle] = 0.25 * (incomingLength + outgoingLength) * std::tan(0.25 * turn);
4849 report.rimChordResolution = std::max(report.rimChordResolution, vertexSagitta[middle]);
4850 }
4851 const size_t chordCount = rim.closed ? pointCount : pointCount - 1;
4852 for (size_t pointIndex = 0; pointIndex < chordCount; ++pointIndex) {
4853 const size_t nextIndex = (pointIndex + 1) % pointCount;
4854 chords.push_back({rim.points[pointIndex], rim.points[nextIndex], rim.surfaceIndex,
4855 std::max(vertexSagitta[pointIndex], vertexSagitta[nextIndex])});
4856 }
4857 chordRange[rimIndex].second = chords.size();
4858 }
4859 if (chords.empty()) {
4860 return;
4861 }
4862
4863 Vec3 lower{chords.front().start};
4864 Vec3 upper{chords.front().start};
4865 auto grow = [&](const Vec3& point) {
4866 lower = {std::min(lower.xCoord, point.xCoord), std::min(lower.yCoord, point.yCoord),
4867 std::min(lower.zCoord, point.zCoord)};
4868 upper = {std::max(upper.xCoord, point.xCoord), std::max(upper.yCoord, point.yCoord),
4869 std::max(upper.zCoord, point.zCoord)};
4870 };
4871 for (const Chord& chord : chords) {
4872 grow(chord.start);
4873 grow(chord.end);
4874 }
4875 const int gridDimension =
4876 std::clamp(static_cast<int>(std::cbrt(static_cast<double>(chords.size()))), 1, 32);
4877 const Vec3 extent = upper - lower;
4878 const double cellSize =
4879 std::max({extent.xCoord, extent.yCoord, extent.zCoord, kTolerance}) / gridDimension;
4880 auto cellOf = [&](double coordinate, double origin) {
4881 return std::clamp(static_cast<int>((coordinate - origin) / cellSize), 0, gridDimension - 1);
4882 };
4883 auto cellIndex = [&](int xCell, int yCell, int zCell) {
4884 return (xCell * gridDimension + yCell) * gridDimension + zCell;
4885 };
4886 std::vector<std::vector<int>> cells(static_cast<size_t>(gridDimension) * gridDimension * gridDimension);
4887 for (size_t chordIndex = 0; chordIndex < chords.size(); ++chordIndex) {
4888 const Chord& chord = chords[chordIndex];
4889 const int xLow = cellOf(std::min(chord.start.xCoord, chord.end.xCoord), lower.xCoord);
4890 const int xHigh = cellOf(std::max(chord.start.xCoord, chord.end.xCoord), lower.xCoord);
4891 const int yLow = cellOf(std::min(chord.start.yCoord, chord.end.yCoord), lower.yCoord);
4892 const int yHigh = cellOf(std::max(chord.start.yCoord, chord.end.yCoord), lower.yCoord);
4893 const int zLow = cellOf(std::min(chord.start.zCoord, chord.end.zCoord), lower.zCoord);
4894 const int zHigh = cellOf(std::max(chord.start.zCoord, chord.end.zCoord), lower.zCoord);
4895 for (int xCell = xLow; xCell <= xHigh; ++xCell) {
4896 for (int yCell = yLow; yCell <= yHigh; ++yCell) {
4897 for (int zCell = zLow; zCell <= zHigh; ++zCell) {
4898 cells[cellIndex(xCell, yCell, zCell)].push_back(static_cast<int>(chordIndex));
4899 }
4900 }
4901 }
4902 }
4903
4904 struct Match {
4905 double distance = std::numeric_limits<double>::infinity();
4906 int chordIndex = -1;
4908 bool withinBand = false;
4911 std::array<int, 3> coincidentFaces{-1, -1, -1};
4912 int coincidentFaceCount = 0;
4913 };
4914 // Two bands: shared-edge matching uses the sampling-aware band, non-manifold detection the declared tolerance alone.
4915 const double maxBand = epsilon + 2. * report.rimChordResolution;
4916 auto nearestOtherFace = [&](const Vec3& probe, int ownSurfaceIndex, double probeResolution) {
4917 Match match;
4918 auto consider = [&](int chordIndex) {
4919 const Chord& chord = chords[static_cast<size_t>(chordIndex)];
4920 if (chord.surfaceIndex == ownSurfaceIndex) {
4921 return;
4922 }
4923 const double distance = std::sqrt(pointSegmentDistanceSq(probe, chord.start, chord.end));
4924 if (distance < match.distance) {
4925 match.distance = distance;
4926 match.chordIndex = chordIndex;
4927 }
4928 if (distance <= epsilon + probeResolution + chord.resolution) {
4929 match.withinBand = true;
4930 }
4931 if (distance <= epsilon && match.coincidentFaceCount < static_cast<int>(match.coincidentFaces.size())) {
4932 for (int seen = 0; seen < match.coincidentFaceCount; ++seen) {
4933 if (match.coincidentFaces[static_cast<size_t>(seen)] == chord.surfaceIndex) {
4934 return;
4935 }
4936 }
4937 match.coincidentFaces[static_cast<size_t>(match.coincidentFaceCount++)] = chord.surfaceIndex;
4938 }
4939 };
4940 const int xCentre = cellOf(probe.xCoord, lower.xCoord);
4941 const int yCentre = cellOf(probe.yCoord, lower.yCoord);
4942 const int zCentre = cellOf(probe.zCoord, lower.zCoord);
4943 for (int shell = 0; shell < gridDimension; ++shell) {
4944 // stop once the nearest hit is closer than this shell's inner distance and the shells reach the match band
4945 const double shellReach = (shell - 1) * cellSize;
4946 if (shell > 0 && shellReach > std::max(match.distance, maxBand)) {
4947 break;
4948 }
4949 for (int xCell = xCentre - shell; xCell <= xCentre + shell; ++xCell) {
4950 if (xCell < 0 || xCell >= gridDimension) {
4951 continue;
4952 }
4953 for (int yCell = yCentre - shell; yCell <= yCentre + shell; ++yCell) {
4954 if (yCell < 0 || yCell >= gridDimension) {
4955 continue;
4956 }
4957 for (int zCell = zCentre - shell; zCell <= zCentre + shell; ++zCell) {
4958 if (zCell < 0 || zCell >= gridDimension) {
4959 continue;
4960 }
4961 const bool onShell = std::abs(xCell - xCentre) == shell || std::abs(yCell - yCentre) == shell ||
4962 std::abs(zCell - zCentre) == shell;
4963 if (!onShell) {
4964 continue; // interior of the shell: visited on an earlier pass
4965 }
4966 for (const int chordIndex : cells[cellIndex(xCell, yCell, zCell)]) {
4967 consider(chordIndex);
4968 }
4969 }
4970 }
4971 }
4972 }
4973 return match;
4974 };
4975
4976 report.rimRecords.reserve(rims.size());
4977 for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) {
4978 bool hasUnmatched = false;
4979 bool hasNonManifold = false;
4980 int sameDirectionVotes = 0;
4981 int oppositeDirectionVotes = 0;
4982 RimRecord record;
4983 record.surfaceIndex = rims[rimIndex].surfaceIndex;
4984 record.rimIndexOnSurface = rimIndexOnSurface[rimIndex];
4985 record.closed = rims[rimIndex].closed;
4986 record.chords = static_cast<int>(chordRange[rimIndex].second - chordRange[rimIndex].first);
4987 for (size_t chordIndex = chordRange[rimIndex].first; chordIndex < chordRange[rimIndex].second; ++chordIndex) {
4988 const Chord& chord = chords[chordIndex];
4989 const Vec3 along = chord.end - chord.start;
4990 const double chordLength = norm(along);
4991 report.totalRimLength += chordLength;
4992 record.length += chordLength;
4993 const Vec3 probe = chord.start + along * 0.5;
4994 const Match match = nearestOtherFace(probe, chord.surfaceIndex, chord.resolution);
4995 if (std::isfinite(match.distance)) {
4996 report.maxRimIsolation = std::max(report.maxRimIsolation, match.distance);
4997 if (match.distance > record.maxIsolation || record.maxIsolationFace < 0) {
4998 record.maxIsolation = match.distance;
4999 record.maxIsolationPoint = probe;
5000 record.maxIsolationFace = chords[static_cast<size_t>(match.chordIndex)].surfaceIndex;
5001 }
5002 }
5003 if (match.coincidentFaceCount > 1) {
5004 hasNonManifold = true;
5005 }
5006 if (!match.withinBand) {
5007 hasUnmatched = true;
5008 ++record.unmatchedChords;
5009 report.unmatchedRimLength += chordLength;
5010 record.unmatchedLength += chordLength;
5011 continue;
5012 }
5013 const Chord& partner = chords[static_cast<size_t>(match.chordIndex)];
5014 if (dot(along, partner.end - partner.start) < 0.) {
5015 ++oppositeDirectionVotes;
5016 } else {
5017 ++sameDirectionVotes;
5018 }
5019 }
5020 if (hasNonManifold) {
5021 ++report.nonManifoldRims;
5023 } else if (hasUnmatched) {
5024 ++report.boundaryRims;
5025 record.state = RimState::Boundary;
5026 } else if (sameDirectionVotes > oppositeDirectionVotes) {
5027 ++report.reversedRims;
5028 record.state = RimState::Reversed;
5029 } else {
5030 ++report.matchedRims;
5031 record.state = RimState::Matched;
5032 }
5033 report.rimRecords.push_back(record);
5034 }
5035}
5036
5039inline void applyEdgeIdentityClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
5041{
5042 size_t surfacesPresent = 0;
5043 size_t surfacesStatingEdges = 0;
5044 for (const auto& surface : surfaces) {
5045 if (surface == nullptr) {
5046 continue;
5047 }
5048 ++surfacesPresent;
5049 if (!surface->boundaryEdges().empty()) {
5050 ++surfacesStatingEdges;
5051 }
5052 }
5053 if (surfacesPresent == 0 || surfacesStatingEdges != surfacesPresent) {
5054 return; // no edge identity, or only some of it: leave the geometric verdict alone
5055 }
5056 report.edgeIdentityAvailable = true;
5057
5058 struct Incidence {
5059 int forward = 0;
5060 int reversed = 0;
5061 int degenerate = 0;
5062 };
5063 std::map<uint32_t, Incidence> incidences;
5064 // which faces own each edge, so a defect can be attributed back to a rim
5065 std::map<uint32_t, std::vector<int>> owners;
5066 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
5067 if (surfaces[surfaceIndex] == nullptr) {
5068 continue;
5069 }
5070 for (const auto& ref : surfaces[surfaceIndex]->boundaryEdges()) {
5071 Incidence& incidence = incidences[ref.edgeId];
5072 if (ref.degenerate) {
5073 ++incidence.degenerate;
5074 } else if (ref.reversed) {
5075 ++incidence.reversed;
5076 } else {
5077 ++incidence.forward;
5078 }
5079 owners[ref.edgeId].push_back(static_cast<int>(surfaceIndex));
5080 }
5081 }
5082
5083 // per face, the worst identity defect any of its edges carries
5084 std::vector<RimState> faceState(surfaces.size(), RimState::Matched);
5085 auto worsen = [](RimState& state, RimState candidate) {
5086 // the enum is not ordered by severity, so spell the precedence out
5087 auto rank = [](RimState value) {
5088 switch (value) {
5089 case RimState::Matched:
5090 return 0;
5091 case RimState::Reversed:
5092 return 1;
5093 case RimState::Boundary:
5094 return 2;
5096 return 3;
5097 }
5098 return 0;
5099 };
5100 if (rank(candidate) > rank(state)) {
5101 state = candidate;
5102 }
5103 };
5104
5105 for (const auto& [edgeId, incidence] : incidences) {
5106 ++report.edgeIncidences;
5107 if (incidence.degenerate > 0 && incidence.forward + incidence.reversed == 0) {
5108 ++report.edgeDegenerateCount;
5109 continue;
5110 }
5111 const int total = incidence.forward + incidence.reversed;
5113 if (total == 1) {
5114 ++report.edgeBoundaryCount;
5116 } else if (total == 2) {
5117 if (incidence.forward == 1 && incidence.reversed == 1) {
5118 ++report.edgeSharedCount;
5119 } else {
5120 ++report.edgeReversedCount;
5122 }
5123 } else {
5124 ++report.edgeNonManifoldCount;
5126 }
5127 if (state != RimState::Matched) {
5128 for (const int owner : owners[edgeId]) {
5129 worsen(faceState[static_cast<size_t>(owner)], state);
5130 }
5131 }
5132 }
5133
5134 report.closed = (report.edgeBoundaryCount == 0) && (report.edgeNonManifoldCount == 0);
5135 report.orientationConsistent = (report.edgeReversedCount == 0);
5136
5137 report.matchedRims = 0;
5138 report.boundaryRims = 0;
5139 report.nonManifoldRims = 0;
5140 report.reversedRims = 0;
5141 for (RimRecord& record : report.rimRecords) {
5142 const RimState state = record.surfaceIndex >= 0 && record.surfaceIndex < static_cast<int>(faceState.size())
5143 ? faceState[static_cast<size_t>(record.surfaceIndex)]
5145 record.state = state;
5146 switch (state) {
5148 ++report.nonManifoldRims;
5149 break;
5150 case RimState::Boundary:
5151 ++report.boundaryRims;
5152 break;
5153 case RimState::Reversed:
5154 ++report.reversedRims;
5155 break;
5156 case RimState::Matched:
5157 ++report.matchedRims;
5158 break;
5159 }
5160 }
5161
5163}
5164
5166inline ClosureReport validateClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
5167 double modelTolerance = 0.)
5168{
5170
5171 auto quantize = [](double value) { return static_cast<int64_t>(std::llround(value / kClosureQuantum)); };
5172 using VertexKey = std::tuple<int64_t, int64_t, int64_t>;
5173 auto keyOf = [&](const Vec3& point) {
5174 return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)};
5175 };
5176
5177 std::vector<std::pair<Vec3, Vec3>> directedEdges;
5178 for (const auto& surface : surfaces) {
5179 if (surface != nullptr) {
5180 surface->appendDirectedEdges(directedEdges);
5181 report.signedVolume += surface->capacityContribution();
5182 }
5183 }
5184
5185 // For each undirected edge, count occurrences in the forward and reverse directions.
5186 std::map<std::pair<VertexKey, VertexKey>, std::pair<int, int>> edgeCounts;
5187 for (const auto& directedEdge : directedEdges) {
5188 const VertexKey startKey = keyOf(directedEdge.first);
5189 const VertexKey endKey = keyOf(directedEdge.second);
5190 if (startKey == endKey) {
5191 continue; // degenerate edge, already flagged at wire level
5192 }
5193 const bool forward = startKey < endKey;
5194 const auto orderedKey = forward ? std::make_pair(startKey, endKey) : std::make_pair(endKey, startKey);
5195 auto& counts = edgeCounts[orderedKey];
5196 if (forward) {
5197 ++counts.first;
5198 } else {
5199 ++counts.second;
5200 }
5201 }
5202
5203 for (const auto& [edgeKey, counts] : edgeCounts) {
5204 const int total = counts.first + counts.second;
5205 if (total == 1) {
5206 ++report.boundaryEdges; // missing neighbouring face
5207 } else if (total == 2) {
5208 if (counts.first != 1 || counts.second != 1) {
5209 ++report.reversedEdges; // both faces traverse the edge the same way
5210 }
5211 } else {
5212 ++report.nonManifoldEdges;
5213 }
5214 }
5215
5216 measureRimClosure(surfaces, modelTolerance > 0. ? modelTolerance : kRimMatchTolerance, report);
5217
5218 // the verdict is the rim measurement's; the chord counters only describe how faces differ
5219 report.closed = (report.boundaryRims == 0) && (report.nonManifoldRims == 0);
5220 report.orientationConsistent = (report.reversedRims == 0);
5221
5222 // ... unless the surfaces state their edge identities, which then decide by counting
5224 return report;
5225}
5226
5227} // namespace o2::cad::surface
5228
5229#endif
header::DataOrigin origin
benchmark::State & state
uint64_t vertex
Definition RawEventData.h:9
int32_t i
constexpr int p1()
constexpr to accelerate the coordinates changing
float center
double lower[3]
bool valid
double maxDepth
double upper[3]
std::vector< SidecarEdge > edges
uint32_t j
Definition RawData.h:0
uint16_t slope
Definition RawData.h:1
int clusterSize
Abstract analytic surface patch: one support surface plus its trim, with the kernels the navigation n...
void setBoundaryEdges(std::vector< BoundaryEdgeRef > refs)
virtual void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const =0
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
virtual bool containsPointOnSurface(const Vec3 &point) const =0
True if the 3D point lies on the trimmed patch within tolerance.
std::pair< Vec3, Vec3 > CoverBox
One axis-aligned cover box of the sub-patch BVH, as a (lower corner, upper corner) pair.
double parametricLengthSqAt(const Vec2 &uv, const Vec2 &delta) const
The 3D length squared spanned by a parametric displacement delta starting at uv.
virtual bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
virtual void conservativeBounds(Vec3 &lower, Vec3 &upper) const =0
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
virtual void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const =0
Append this patch's visualization triangulation (navigation must never depend on it).
virtual ~BoundedSurface()=default
virtual void appendRims(std::vector< SurfaceRim > &rims) const
Append the trim boundary as rims, one polyline per loop; the default chains appendDirectedEdges().
virtual double capacityContribution() const =0
Signed divergence-theorem contribution to the enclosed volume.
virtual void parametricMetric(const Vec2 &uv, double &gUU, double &gUV, double &gVV) const =0
The first fundamental form at uv, turning parametric displacements into 3D lengths; it varies over th...
virtual void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const =0
Append the 3D directed boundary edges of the patch, for solid-closure validation.
virtual void appendCoverBoxes(std::vector< CoverBox > &boxes) const
std::vector< BoundaryEdgeRef > mBoundaryEdges
virtual Vec3 normalAt(const Vec3 &point) const =0
Outward-oriented normal at (or nearest to) the given point.
virtual bool capacityIsExact() const =0
Whether capacityContribution() is analytically exact for this surface.
const std::vector< BoundaryEdgeRef > & boundaryEdges() const
virtual double distanceSqToPatch(const Vec3 &point) const =0
Squared distance from a 3D point to the trimmed patch (used for Safety).
A cone whose radius varies linearly with height, trimmed as the cylinder; one radius may be zero (an ...
double radiusAt(double height) const
double capacityContribution() const override
Divergence-theorem contribution over the (phi, h) rectangle; a wire trim uses the contour form,...
Vec3 pointAt(double phi, double height) const
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
bool heightInRange(double height) const
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
Vec3 normalAt(const Vec3 &point) const override
Outward-oriented normal at (or nearest to) the given point.
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double radiusAtMin, double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, const std::vector< Curve2D > &outerTrim, const std::vector< std::vector< Curve2D > > &innerTrims, std::string &errorMessage, double joinTolerance=kWireJoinTolerance)
Wire-trimmed overload: the scalar radii pin r(h); the wires in the (phi[rad], h[cm]) domain decide co...
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double radiusAtMin, double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, std::string &errorMessage)
double distanceSqToPatch(const Vec3 &point) const override
Distance to the patch: exact for the parametric rectangle, a lower bound for a wire trim.
void parametricMetric(const Vec2 &uv, double &gUU, double &gUV, double &gVV) const override
(u, v) = (phi[rad], h[cm]): the azimuthal scale is the local radius, and a step in h spans sqrt(1 + s...
void appendCoverBoxes(std::vector< CoverBox > &boxes) const override
Cover boxes: as for the cylinder, with the rim radii from the linear radius law.
Vec3 toLocal(const Vec3 &point) const
bool pointInTrim(double phi, double height, bool *boundary=nullptr) const
True if the (phi, h) point lies in the trim wire (phi unwrapped into the wire window).
A plane trimmed by curved (line/arc/B-spline) loops in an orthonormal frame: exact caps,...
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
double capacityContribution() const override
Signed divergence-theorem contribution to the enclosed volume.
double distanceSqToPatch(const Vec3 &point) const override
Squared distance from a 3D point to the trimmed patch (used for Safety).
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
Vec3 normalAt(const Vec3 &) const override
Outward-oriented normal at (or nearest to) the given point.
bool containsLocal(const Vec2 &point, bool *boundary=nullptr) const
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
double planeDistance(const Vec3 &point) const
bool wasReoriented() const
True if the outer or any inner wire had to be re-oriented during initialization.
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
bool initialize(const Vec3 &surfaceOrigin, const Vec3 &surfaceAxisU, const Vec3 &surfaceAxisV, const std::vector< Curve2D > &outerCurves, const std::vector< std::vector< Curve2D > > &innerCurves, std::string &errorMessage, double joinTolerance=kWireJoinTolerance)
void parametricMetric(const Vec2 &, double &gUU, double &gUV, double &gVV) const override
A cylinder of given radius around an axis, trimmed to a (phi, h) rectangle or by curve wires; innerWa...
static bool makeFrame(const Vec3 &axis, const Vec3 &referenceAxisU, Vec3 &axisU, Vec3 &axisV, Vec3 &axisW, std::string &errorMessage)
Vec3 pointAt(double phi, double height) const
Vec3 normalAt(const Vec3 &point) const override
Outward-oriented normal at (or nearest to) the given point.
void appendCoverBoxes(std::vector< CoverBox > &boxes) const override
Cover boxes: the sweep window in angular chunks, which holds every point that realises distanceSqToPa...
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
void parametricMetric(const Vec2 &, double &gUU, double &gUV, double &gVV) const override
(u, v) = (phi[rad], h[cm]): X_phi has length r and X_h is the unit axis.
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
bool pointInTrim(double phi, double height, bool *boundary=nullptr) const
True if the (phi, h) point lies in the trim wire (phi unwrapped into the wire window).
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double radius, double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, std::string &errorMessage)
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
double distanceSqToPatch(const Vec3 &point) const override
Distance to the patch: exact for the parametric rectangle, a lower bound for a wire trim.
double capacityContribution() const override
Divergence-theorem contribution over the (phi, h) rectangle; a wire trim uses the contour form,...
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double radius, double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall, const std::vector< Curve2D > &outerTrim, const std::vector< std::vector< Curve2D > > &innerTrims, std::string &errorMessage, double joinTolerance=kWireJoinTolerance)
Wire-trimmed overload: the wires in the (phi[rad], h[cm]) domain decide containment; the window tight...
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
double distanceSqToPatch(const Vec3 &point) const override
Squared distance from a 3D point to the trimmed patch (used for Safety).
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
bool initialize(const Vec3 &surfaceOrigin, const Vec3 &surfaceAxisU, const Vec3 &surfaceAxisV, const std::vector< Vec2 > &outerWireVertices, const std::vector< std::vector< Vec2 > > &innerWireVertices, std::string &errorMessage)
Vec2 toLocal(const Vec3 &point) const
Vec3 toGlobal(const Vec2 &point) const
Vec3 normalAt(const Vec3 &) const override
Outward-oriented normal at (or nearest to) the given point.
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
double capacityContribution() const override
Signed divergence-theorem contribution to the enclosed volume.
double distanceSqToEdges(const Vec3 &point, const std::vector< Vec3 > &ring) const
bool wasReoriented() const
True if either the outer or any inner wire had to be re-oriented during initialization.
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
double planeDistance(const Vec3 &point) const
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
void parametricMetric(const Vec2 &, double &gUU, double &gUV, double &gVV) const override
Constant over the plane, with a cross term: the frame axes need be neither unit-length nor orthogonal...
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
bool containsLocal(const Vec2 &point, bool *boundary=nullptr) const
A sphere of given radius trimmed to a (theta, phi) rectangle or by curve wires; innerWall points the ...
void appendCoverBoxes(std::vector< CoverBox > &boxes) const override
Cover boxes: the whole sphere in (theta, phi) chunks, since distanceSqToPatch ignores the trim.
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
bool initialize(const Vec3 &center, const Vec3 &polarAxis, const Vec3 &referenceAxisU, double radius, double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall, std::string &errorMessage)
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
Vec3 pointAt(double theta, double phi) const
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
Vec3 normalAt(const Vec3 &point) const override
Outward-oriented normal at (or nearest to) the given point.
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
double distanceSqToPatch(const Vec3 &point) const override
Distance to the patch: exact inside the trim, else the full-sphere distance, a lower bound.
void parametricMetric(const Vec2 &uv, double &gUU, double &gUV, double &gVV) const override
(u, v) = (phi[rad], theta[rad]); gUU vanishes at either pole.
bool directionInTrim(const Vec3 &localPoint, bool *boundary=nullptr) const
bool pointInTrim(double phi, double theta, bool *boundary=nullptr) const
True if the (phi, theta) point lies in the trim wire (phi unwrapped into the wire window).
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
double capacityContribution() const override
Divergence-theorem contribution over the (theta, phi) rectangle; a wire trim uses the contour form in...
bool initialize(const Vec3 &center, const Vec3 &polarAxis, const Vec3 &referenceAxisU, double radius, double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall, const std::vector< Curve2D > &outerTrim, const std::vector< std::vector< Curve2D > > &innerTrims, std::string &errorMessage, double joinTolerance=kWireJoinTolerance)
Wire-trimmed overload: the wires in the (phi[rad], theta[rad]) domain decide containment; the window ...
Vec3 toLocal(const Vec3 &point) const
void appendDisplayMesh(std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles) const override
Append this patch's visualization triangulation (navigation must never depend on it).
bool containsPointOnSurface(const Vec3 &point) const override
True if the 3D point lies on the trimmed patch within tolerance.
bool tubeInSweep(double phiTube) const
Vec3 pointAt(double phiRing, double phiTube) const
void appendCoverBoxes(std::vector< CoverBox > &boxes) const override
Cover boxes: the full torus in angular chunks, since the meridian projection ignores the trim; a spin...
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double majorRadius, double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep, bool innerWall, std::string &errorMessage)
bool initialize(const Vec3 &centerPoint, const Vec3 &axis, const Vec3 &referenceAxisU, double majorRadius, double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep, bool innerWall, const std::vector< Curve2D > &outerTrim, const std::vector< std::vector< Curve2D > > &innerTrims, std::string &errorMessage, double joinTolerance=kWireJoinTolerance)
Wire-trimmed overload: the wires in the (phiRing, phiTube) domain decide containment; a trim wrapping...
void appendIntersections(const Vec3 &rayOrigin, const Vec3 &rayDirection, double minDistance, double maxDistance, std::vector< RayHit > &hits) const override
Append every hit of the ray with the trimmed patch in [minDistance, maxDistance], with the outward no...
void conservativeBounds(Vec3 &lower, Vec3 &upper) const override
Accumulate a conservative axis-aligned bounding box of the trimmed patch.
bool capacityIsExact() const override
Whether capacityContribution() is analytically exact for this surface.
Vec3 localNormal(const Vec3 &localPoint) const
Unit outward normal (pointing away from the tube spine) from a local surface point.
Vec3 normalAt(const Vec3 &point) const override
Outward-oriented normal at (or nearest to) the given point.
Vec3 toLocal(const Vec3 &point) const
double distanceSqToPatch(const Vec3 &point) const override
Distance to the patch: exact for the full torus by the meridian distance, a lower bound for a trimmed...
double capacityContribution() const override
Divergence-theorem contribution over the (phiRing, phiTube) rectangle; a wire trim uses the contour f...
bool sampleTrimCurve(size_t index, std::vector< Vec3 > &samples) const override
Sample trim curve index into 3D, in construction order; false when this face has no such curve.
void appendDirectedEdges(std::vector< std::pair< Vec3, Vec3 > > &edges) const override
Append the 3D directed boundary edges of the patch, for solid-closure validation.
bool pointInTrim(double phiRing, double phiTube, bool *boundary=nullptr) const
Whether (phiRing, phiTube) lies in the trim wire, both angles unwrapped into their windows.
void parametricMetric(const Vec2 &uv, double &gUU, double &gUV, double &gVV) const override
(u, v) = (phiRing[rad], phiTube[rad]): the tube scale is r, the ring scale the distance from the axis...
bool ringInSweep(double phiRing) const
float sum(float s, o2::dcs::DataPointValue v)
Definition dcs-ccdb.cxx:39
bool match(const std::vector< std::string > &queries, const char *pattern)
Definition dcs-ccdb.cxx:229
GLdouble n
Definition glcorearb.h:1982
GLint GLenum GLint x
Definition glcorearb.h:403
GLsizei const GLuint const GLfloat * weights
Definition glcorearb.h:5475
GLuint segment
Definition glcorearb.h:4945
GLint GLsizei count
Definition glcorearb.h:399
GLuint GLuint end
Definition glcorearb.h:469
const GLdouble * v
Definition glcorearb.h:832
GLuint index
Definition glcorearb.h:781
GLsizei samples
Definition glcorearb.h:1309
GLdouble GLdouble right
Definition glcorearb.h:4077
GLint GLsizei GLsizei height
Definition glcorearb.h:270
GLint first
Definition glcorearb.h:399
GLuint GLuint GLfloat weight
Definition glcorearb.h:5477
GLboolean GLboolean GLboolean b
Definition glcorearb.h:1233
GLsizei GLsizei GLfloat distance
Definition glcorearb.h:5506
GLsizei const GLfloat * value
Definition glcorearb.h:819
GLint left
Definition glcorearb.h:1979
GLintptr offset
Definition glcorearb.h:660
GLuint GLsizei GLsizei * length
Definition glcorearb.h:790
typedef void(APIENTRYP PFNGLCULLFACEPROC)(GLenum mode)
GLint GLenum GLboolean normalized
Definition glcorearb.h:867
GLuint segments
Definition glcorearb.h:4946
GLfloat angle
Definition glcorearb.h:4071
GLfloat v0
Definition glcorearb.h:811
GLint GLint GLsizei GLsizei GLsizei depth
Definition glcorearb.h:470
GLfloat GLfloat v1
Definition glcorearb.h:812
GLboolean r
Definition glcorearb.h:1233
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t0
Definition glcorearb.h:5034
GLuint start
Definition glcorearb.h:469
GLboolean GLboolean GLboolean GLboolean a
Definition glcorearb.h:1233
GLsizei const GLint * box
Definition glcorearb.h:4697
GLdouble GLdouble GLdouble z
Definition glcorearb.h:843
GLuint GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat GLfloat t1
Definition glcorearb.h:5034
void report(gsl::span< o2::InteractionTimeRecord > irs, int threshold, bool verbose)
bool pointInTriangle(const Vec2 &point, const Vec2 &firstVertex, const Vec2 &secondVertex, const Vec2 &thirdVertex)
bool sampleTrimCurveOfCurveWires(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, size_t index, const MapUV &mapUV, std::vector< Vec3 > &samples)
Sample input curve index of a curve-wire trim into 3D through mapUV; false when out of range or not t...
void applyEdgeIdentityClosure(const std::vector< std::unique_ptr< BoundedSurface > > &surfaces, ClosureReport &report)
double trimLengthFloor(const ParametricMetric &metric, const Vec2 &uv)
kTolerance as a parametric separation at uv: the floor of every trim's on-boundary band.
void assembleRims(const std::vector< std::pair< Vec3, Vec3 > > &edges, std::vector< SurfaceRim > &rims)
Chain a face's directed chords into rims by matching endpoints within kTolerance, appending them to r...
constexpr double wireJoinToleranceFor(double modelTolerance)
The wire-join band for a model with a declared tolerance: that tolerance when looser than kWireJoinTo...
CurveKind
Kind of a 2D trimmed boundary curve.
@ Line
straight line segment
@ BSpline
clamped (rational) B-spline curve
double distanceSq(const Vec2 &firstPoint, const Vec2 &secondPoint)
double contourIntegralAlongCurve(const Curve2D &curve, const Antiderivative &antiderivative, double from, double to)
void assignComponent(Vec3 &vector, int dimension, double value)
void coneParametricMetric(double radiusAtHeight, double slope, double &gUU, double &gUV, double &gVV)
QuarticBranch
Which of solveQuarticReal's branches produced its roots, for the tests.
@ NotAQuartic
the leading coefficient vanishes; no roots are produced
@ Resolvent
Ferrari's general branch, through the resolvent cubic.
@ Biquadratic
the depressed quartic's odd term is zero, so y^4 + p y^2 + r = 0 is solved directly
@ Reversed
well-formed but re-oriented to match its role (simple, logged repair)
@ Valid
well-formed and already correctly oriented
@ DegenerateVertex
a non-adjacent vertex coincided (self-touching / pinched loop)
@ NonFinite
a vertex/edge contained a non-finite coordinate
@ Open
an explicit edge list did not form a closed loop
@ TooFewVertices
fewer than three distinct vertices after cleanup
@ ZeroArea
the loop encloses no area
bool curveTrimContains(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, const Vec2 &point, bool *boundary=nullptr, const ParametricMetric &metric={})
Whether a parametric point is in a curve-wire trim (outer loop minus holes); boundary reports an on-b...
void measureSharedEdgeDeviation(const std::vector< std::unique_ptr< BoundedSurface > > &surfaces, ClosureReport &report)
Measure the Hausdorff distance between the two faces of each shared edge into report; it decides noth...
Vec3 operator*(const Vec3 &vector, double scale)
constexpr double kBSplineFlatness
Chord flatness of the adaptive B-spline sampler, in the curve's parametric units; a B-spline trim is ...
constexpr double kContourMaxSpanU
WireClassification
Classification of a parametric point against a closed wire.
void planeParametricMetric(const Vec3 &axisU, const Vec3 &axisV, double &gUU, double &gUV, double &gVV)
Vec3 operator-(const Vec3 &firstVector, const Vec3 &secondVector)
double pointSegmentDistanceSq(const Vec2 &point, const Vec2 &segmentStart, const Vec2 &segmentEnd)
constexpr double kPi
double dot(const Vec3 &firstVector, const Vec3 &secondVector)
Vec3 operator+(const Vec3 &firstVector, const Vec3 &secondVector)
std::vector< std::array< int, 3 > > triangulateSimpleWire(const SurfaceWire &wire)
Ear-clipping triangulation of a simple (non-self-intersecting) parametric wire.
constexpr double kWireJoinTolerance
Wire-closure tolerance, a 3D length in cm through the surface metric: the CAD extractor's endpoint pr...
double integrateOverCurveTrim(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, const Integrand &integrand, int samplesPerAxis=128)
Midpoint-rule integral of integrand over the trimmed region; kept as the independent check of the con...
bool sameIntersection(double firstDistance, double secondDistance)
void torusParametricMetric(double majorRadius, double minorRadius, double phiTube, double &gUU, double &gUV, double &gVV)
Torus, (u, v) = (phiRing[rad], phiTube[rad]). The ring scale runs from R - r to R + r.
constexpr double kHalfPi
void cylinderParametricMetric(double radius, double &gUU, double &gUV, double &gVV)
Cylinder, (u, v) = (phi[rad], h[cm]).
double sinusoidMaximum(double a, double b, double t0, double t1)
double integrateOverCurveTrimByParts(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, const Antiderivative &antiderivative)
Green's theorem over a wire-trimmed patch: the double integral of f is the contour integral of F dv,...
void sphereParametricMetric(double radius, double theta, double &gUU, double &gUV, double &gVV)
constexpr int kContourQuadratureOrder
Gauss-Legendre nodes per contour sub-interval, and the widest u span one sub-interval covers.
constexpr double kRayTolerance
minimum positive ray parameter t
bool sampleTrimCurveOfSurfaceWires(const SurfaceWire &outerWire, const std::vector< SurfaceWire > &innerWires, size_t index, const MapUV &mapUV, std::vector< Vec3 > &samples)
The same for a polygon (vertex-ring) trim, whose curves are all straight segments.
void gaussLegendre(int n, std::vector< double > &nodes, std::vector< double > &weights)
The n-point Gauss-Legendre nodes and weights on [-1, 1], by Newton iteration on P_n.
void measureRimClosure(const std::vector< std::unique_ptr< BoundedSurface > > &surfaces, double epsilon, ClosureReport &report)
Measure the face-to-face gaps of surfaces as curves into report, probing chord midpoints against othe...
ParametricMetric parametricMetricOf(const Surface &surface)
std::vector< Vec2 > sampleCurveWireByU(const CurveWire &wire, int segmentsPerTurn=kArcSamples)
Sub-sample a curve-wire loop so its u span is chorded at segmentsPerTurn per turn,...
double unwrapAngleInto(double angle, double uMin, double uMax)
Shift angle by whole turns to lie as close as possible to the window [uMin, uMax].
int coverChunkCount(double span)
bool buildCurveTrim(const std::vector< Curve2D > &outerTrim, const std::vector< std::vector< Curve2D > > &innerTrims, CurveWire &outerWire, std::vector< CurveWire > &innerWires, Vec2 &lower, Vec2 &upper, std::string &errorMessage, const ParametricMetric &metric={}, double joinTolerance=kWireJoinTolerance)
Build validated outer and inner trim wires and the outer loop's parametric bounds; rejects a trim wid...
constexpr double kQuarticEpsilon
Zero threshold of solveQuarticReal's branch tests, in machine epsilons relative to the normalised ter...
constexpr double kBVHBoxTolerance
Widening of the BVH leaf boxes before the outward float rounding; it dominates every navigation lengt...
void appendCurveTrimEdges(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, const MapUV &mapUV, double orientationSign, std::vector< std::pair< Vec3, Vec3 > > &edges)
Append the directed 3D boundary edges of a wire-trimmed quadric patch; a negative orientationSign rev...
ClosureReport validateClosure(const std::vector< std::unique_ptr< BoundedSurface > > &surfaces, double modelTolerance=0.)
Validate closure and orientation of surfaces by half-edges, measure the rims, and count edge identiti...
constexpr double kToleranceSq
constexpr double kAreaTolerance
degenerate (zero) parametric area
@ Reversed
matched, but the partner traverses the shared curve the same way
@ NonManifold
some chord has two or more other faces within the declared tolerance
@ Matched
every chord has another face within its match band, traversed the other way
@ Boundary
some chord has no other face within its match band
void sinusoidRange(double a, double b, double t0, double t1, double &minimum, double &maximum)
Exact range of a cos(t) + b sin(t) over [t0, t1], at most a turn: the endpoint values,...
void appendCurveTrimMesh(const CurveWire &outerWire, const MapUV &mapUV, std::vector< Vec3 > &vertices, std::vector< std::array< int, 3 > > &triangles)
Append the display triangulation of a wire-trimmed quadric patch: the sampled outer loop,...
double normSq(const Vec3 &vector)
constexpr int kSharedEdgeSamples
Samples per trim curve when measuring a shared edge's deviation; it never enters a verdict.
double component(const Vec3 &vector, int dimension)
constexpr double kCoverChunkAngle
Widest angular span of one cover box: pi/4, eight boxes per full turn.
QuarticRoots solveQuarticReal(double a4, double a3, double a2, double a1, double a0, QuarticBranch *takenBranch=nullptr)
bool finite(const Vec2 &point)
constexpr double kBSplineFlatnessSq
Vec3 cross(const Vec3 &firstVector, const Vec3 &secondVector)
constexpr double kClosureQuantum
constexpr double kRimMatchTolerance
Rim-matching distance in cm when the model states no tolerance: the extractor precision,...
double angularTolerance(double radius)
Angular tolerance equivalent to a kTolerance arc length at the given radius.
const char * wireStatusMessage(WireStatus status)
Human-readable description of a wire status, for logging.
constexpr double kTolerance
generic length tolerance
double cross2D(const Vec2 &firstVector, const Vec2 &secondVector)
double sinusoidMinimum(double a, double b, double t0, double t1)
void appendArcBandCoverBoxes(const Vec3 &center, const Vec3 &axisU, const Vec3 &axisV, const Vec3 &axisW, double phiStart, double phiSweep, double heightMin, double heightMax, double radiusAtMin, double radiusAtMax, std::vector< BoundedSurface::CoverBox > &boxes)
Cover boxes of a band of revolution between two rim circles: the phi window in chunks,...
constexpr double kTwoPi
bool angleInSweepRange(double angle, double start, double sweep, double tolerance)
constexpr int kArcSamples
Chords per full-circle arc for display and rims, shared by all surfaces so shared rims match; divisib...
constexpr double kIntersectionTolerance
clustering of near-equal intersections
double parametricLengthSq(double gUU, double gUV, double gVV, const Vec2 &delta)
The 3D length squared of parametric displacement delta under the first fundamental form (gUU,...
int solveDepressedCubic(double coeffP, double coeffQ, std::array< double, 3 > &roots)
double norm(const Vec3 &vector)
uint32_t edgeId
index into the model's edge table; identity, not a coordinate
bool anchored
Whether trim curve i exists to sample for edge i; false for a parametric-rectangle trim.
bool reversed
this face runs against the edge's own direction
Whether a set of bounded surfaces forms a closed, consistently oriented 2-manifold,...
int rims
total number of trim loops over all faces
int reversedEdges
edges shared by two faces in the same direction
int boundaryEdges
edges present on only one face (e.g. a missing face)
int maxSharedEdgeDeviationFaces[2]
between which two faces
bool orientationConsistent
shared edges are traversed in opposite directions
double totalRimLength
summed length in cm of every face's trim boundary
int edgeSharedCount
appearing exactly twice, opposite sense: a properly shared edge
uint32_t maxSharedEdgeDeviationEdge
which edge that was
int edgeBoundaryCount
appearing once: a face is missing on the other side
double rimEpsilon
the declared matching tolerance, in cm
std::vector< RimRecord > rimRecords
double maxSharedEdgeDeviation
Largest Hausdorff distance between two faces' realisations of a shared edge, in cm; a measurement,...
int nonManifoldEdges
edges shared by more than two faces
int nonManifoldRims
some chord has two or more other faces within rimEpsilon
bool closed
every boundary edge is shared by exactly two faces
double unmatchedRimLength
how much of it has no other face within the match band, cm
int edgeReversedCount
appearing exactly twice, but with the same sense
int sharedEdgesMeasured
shared edges both of whose faces could be sampled
double signedVolume
divergence-theorem volume; positive if normals point out
int edgeNonManifoldCount
appearing three or more times
Vec3 maxSharedEdgeDeviationPoint
and where on it
int edgeIncidences
distinct edge identifiers seen over all faces
One trimmed boundary curve in a surface's (u, v) domain: a line segment, a circular arc or a clamped ...
Vec2 tangentAt(double parameter) const
Unit tangent at parameter parameter, pointing in the direction of increasing parameter.
std::vector< Vec2 > poles
static Curve2D makeBSpline(int splineDegree, std::vector< Vec2 > splinePoles, std::vector< double > splineWeights, std::vector< double > splineKnots)
bool bsplineRational() const
True if the curve carries non-unit weights (a rational B-spline).
bool angleInSweep(double angle) const
True if angle lies within the arc's angular sweep (accounting for direction and wrap).
static Curve2D makeLine(const Vec2 &start, const Vec2 &end)
int rightwardCrossings(const Vec2 &point, const Vec2 &canonicalStart, const Vec2 &canonicalEnd) const
Rightward crossings of a horizontal ray from point, with the caller's canonical endpoints so that sea...
const std::vector< Vec2 > & bsplineSamples() const
The flattened polyline in bsplineCache, computed here if the wire has not filled it.
void bsplineBasis(int span, double knotValue, std::vector< double > &basis, std::vector< double > &basisDeriv) const
Non-zero degree-p basis functions and first derivatives at knotValue in span (The NURBS Book,...
void bsplineSampleRecursive(double t0, double t1, const Vec2 &p0, const Vec2 &p1, double flatnessSq, int depth, std::vector< Vec2 > &samples) const
static Curve2D makeArc(const Vec2 &arcCenter, double arcRadius, double arcStartAngle, double arcEndAngle)
Vec2 derivativeAt(double parameter) const
dC/dt at parameter in [0, 1], unnormalised; tangentAt() is it normalised.
void bsplineSampleInto(std::vector< Vec2 > &samples, double flatnessSq=kBSplineFlatnessSq, int maxDepth=16) const
Adaptively sample the B-spline into an on-curve polyline, subdividing until each chord is flat to sqr...
Vec2 lineStart
line: start point (unused for arcs)
bool bsplineIsClamped() const
True when the knot vector is clamped, so the curve interpolates its first and last pole.
double uVariation(double from, double to) const
An upper bound on how far u travels along the curve between from and to.
Vec2 center
arc: circle centre (unused for lines)
bool spansInteriorKnot(double lowT, double highT) const
Whether a knot lies strictly inside (lowT, highT); such an interval is never called flat.
void extendBounds(Vec2 &lower, Vec2 &upper) const
Accumulate this curve's exact extent into a parametric axis-aligned bounding box.
Vec2 closestPoint(const Vec2 &point, double &parameter) const
Closest point on the curve to point, returning the clamped parameter in parameter.
Vec2 lineEnd
line: end point (unused for arcs)
void reverseInPlace()
Reverse the curve's direction in place (start <-> end), keeping the same geometric image.
double endAngle
arc: end angle [rad] (sweep = endAngle - startAngle)
std::vector< double > knots
bool bsplineBandOrCrossings(const Vec2 &point, double bandSq, int &crossings) const
void setCanonicalEndpoints(const Vec2 &start, const Vec2 &end)
Vec2 pointAtAngle(double angle) const
std::vector< Vec2 > bsplineCache
The flattened on-curve polyline, both ends included; CurveWire::initialize fills it and reversing cle...
int bsplineSpan(double knotValue) const
Knot span index of parameter knotValue for the clamped knot vector.
double startAngle
arc: start angle [rad]
Vec2 pointAt(double parameter) const
Point at curve parameter parameter in [0, 1] (0 at the start, 1 at the end).
static Curve2D makeCircle(const Vec2 &arcCenter, double arcRadius, bool clockwise=false)
Full circle as one arc curve (counter-clockwise unless clockwise is set).
Vec2 bsplinePointAt(double parameter) const
B-spline point at curve parameter parameter in [0, 1].
double signedAreaContribution() const
void extendTightBounds(Vec2 &lower, Vec2 &upper) const
As extendBounds, measured on the curve: a B-spline contributes its sampled polyline,...
std::vector< double > weights
void includeAnalyticExtremes(const Include &include) const
Endpoints plus an arc's axis-extreme points inside the sweep: the exact extent of a line or an arc.
double radius
arc: circle radius
double angleParameter(double angle) const
Map an angle known to lie within the sweep to a clamped parameter in [0, 1].
double representationTolerance() const
How far this curve's representation can sit from the curve, in parametric units: kBSplineFlatness for...
void appendInteriorKnots(double from, double to, std::vector< double > &breakpoints) const
Append the interior knots in (from, to), in the curve's [0, 1] parameter; none for a line or an arc.
double distanceSq(const Vec2 &point) const
Squared distance from point to the curve.
void bsplineEval(double knotValue, Vec2 &pointOut, Vec2 &derivativeOut) const
One closed, oriented boundary loop of Curve2D segments: outer loops wind counter-clockwise,...
double representationTolerance() const
The widest gap between the loop's representation and its boundary, in parametric units; 0 for lines a...
WireClassification classify(const Vec2 &point, const ParametricMetric &metric={}) const
metric only sizes the on-boundary band; the winding count is topological.
void fillBSplineCaches() const
Fill every B-spline's polyline cache now, so that const navigation queries only read it.
int storedIndexOfSource(int inputIndex) const
The stored curve that came from input curve inputIndex, or -1 if there is none.
double mRepresentationTolerance
The largest representationTolerance() over the curves, fixed when the curves are set.
std::vector< int > sourceCurve
For each stored curve its input index; reverse() is the only reordering, and sidecar v3 edge identiti...
void tightParametricBounds(Vec2 &lower, Vec2 &upper) const
Add the loop's extent measured on the curves to a parametric bounding box; use it to reject a wire as...
void parametricBounds(Vec2 &lower, Vec2 &upper) const
Add the loop's conservative extent, a B-spline's pole hull included, to a parametric bounding box.
std::vector< Vec2 > sampledBoundary(int segmentsPerArc=kArcSamples) const
void reverse()
Reverse the loop orientation in place (order and per-curve direction).
std::vector< Curve2D > curves
WireClassification classify(const Vec2 &point, double lengthFloor) const
Classify a point against the loop with band floor lengthFloor: Boundary within the band,...
bool initialize(const std::vector< Curve2D > &inputCurves, WireRole wireRole, WireStatus &status, const ParametricMetric &metric={}, double joinTolerance=kWireJoinTolerance)
Build and validate the wire from an ordered closed list of curves, joining within joinTolerance throu...
double boundaryBand(double lengthFloor) const
double signedArea() const
Exact signed area enclosed by the loop (positive when counter-clockwise).
How a wire converts a parametric separation into a 3D length: the owning surface's first fundamental ...
double maxScale(const Vec2 &uv) const
The largest 3D length a unit parametric displacement spans at uv: the square root of the larger eigen...
double distanceSq(const Vec2 &from, const Vec2 &to) const
The 3D distance squared between two nearby parametric points, with the form evaluated at from.
void(*)(const void *context, const Vec2 &uv, double &gUU, double &gUV, double &gVV) Evaluate
double lengthSq(const Vec2 &uv, const Vec2 &delta) const
The 3D length squared spanned by the parametric displacement delta starting at uv.
The real roots of a quartic: at most four, held inline.
double operator[](size_t index) const
const double * begin() const
const double * end() const
One ray/surface intersection: the ray parameter and the outward normal; a quadric patch can give seve...
bool onTrimBoundary
The hit lies within the trim's on-boundary band, so its inside/outside side is a tie-break,...
One trim loop of one face as measureRimClosure saw it, naming the rim and its worst chord.
int rimIndexOnSurface
which trim loop of that face, in the order the face emits them
bool closed
the polyline returns to its own first point
double maxIsolation
Largest distance from a chord midpoint of this rim to another face's chord, and where: how alone the ...
int maxIsolationFace
the face owning the nearest chord there, or -1 if there was none
int surfaceIndex
the owning face's index in the solid's surface list
double length
summed chord length, cm
int unmatchedChords
of them, how many found no other face within their match band
One straight line segment of a polygon wire, in a surface's parametric (u, v) domain.
double distanceSq(const Vec2 &point) const
Squared distance from a parametric point to this edge.
void extendBounds(Vec2 &lower, Vec2 &upper) const
Accumulate the edge endpoints into a parametric axis-aligned bounding box.
Vec2 closestPoint(const Vec2 &point, double &parameter) const
One trim loop of one face as an ordered 3D polyline, compared with other faces' rims as a curve.
std::vector< Vec3 > points
consecutive samples; a closed rim does not repeat the first point
int surfaceIndex
index of the owning face in the solid's surface list
bool closed
the polyline returns to its own first point
One closed, oriented polygon loop in a surface's parametric domain: outer loops wind counter-clockwis...
std::vector< int > sourceEdge
For each stored segment its input segment, or -1 once a vertex was dropped; sidecar v3 edge identitie...
bool initializeFromEdges(const std::vector< SurfaceEdge > &edges, WireRole wireRole, WireStatus &status, const ParametricMetric &metric={}, double joinTolerance=kWireJoinTolerance)
Build and validate the wire from an ordered edge list, joining within joinTolerance through metric,...
SurfaceEdge edge(int index) const
int storedIndexOfSource(int inputIndex) const
The stored segment that came from input segment inputIndex, or -1 if there is none.
bool initialize(const std::vector< Vec2 > &inputVertices, WireRole wireRole, WireStatus &status, const ParametricMetric &metric={})
Build and validate the wire from an implicitly closed vertex ring; metric turns separations into 3D l...
WireClassification classify(const Vec2 &point, const ParametricMetric &metric={}) const
metric sizes the band only: a polygon is exact, so its band is the length floor.
WireClassification classify(const Vec2 &point, double band) const
Classify against the polygon with an on-boundary half-width of band, in parametric units.
std::vector< Vec2 > sampledBoundary() const
The de-duplicated vertex ring, closed back to its first vertex.
void parametricBounds(Vec2 &lower, Vec2 &upper) const
A 2D point/vector in a surface's parametric (u, v) domain.
A 3D point/vector in the solid's local frame.
std::vector< Cell > cells