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 <numeric>
29#include <string>
30#include <tuple>
31#include <utility>
32#include <vector>
33
35{
36
39inline constexpr double kTolerance = 1.e-9;
40inline constexpr double kToleranceSq = kTolerance * kTolerance;
41inline constexpr double kAreaTolerance = 1.e-18;
42inline constexpr double kRayTolerance = 1.e-9;
43inline constexpr double kIntersectionTolerance = 1.e-7;
44inline constexpr double kClosureQuantum = 1.e-7;
46inline constexpr double kWireJoinTolerance = 1.e-6;
48inline constexpr double wireJoinToleranceFor(double modelTolerance)
49{
50 return modelTolerance > kWireJoinTolerance ? modelTolerance : kWireJoinTolerance;
51}
53inline constexpr double kBSplineFlatness = 1.e-5;
56inline constexpr double kRimMatchTolerance = 1.e-6;
57
59inline constexpr double kBVHBoxTolerance = 1.e-3;
61inline constexpr double kQuarticEpsilon = 32. * 2.220446049250313e-16;
63
65struct Vec2 {
66 double uCoord = 0.;
67 double vCoord = 0.;
68};
69
71struct Vec3 {
72 double xCoord = 0.;
73 double yCoord = 0.;
74 double zCoord = 0.;
75};
76
77inline Vec3 operator+(const Vec3& firstVector, const Vec3& secondVector)
78{
79 return {firstVector.xCoord + secondVector.xCoord, firstVector.yCoord + secondVector.yCoord,
80 firstVector.zCoord + secondVector.zCoord};
81}
82
83inline Vec3 operator-(const Vec3& firstVector, const Vec3& secondVector)
84{
85 return {firstVector.xCoord - secondVector.xCoord, firstVector.yCoord - secondVector.yCoord,
86 firstVector.zCoord - secondVector.zCoord};
87}
88
89inline Vec3 operator*(const Vec3& vector, double scale)
90{
91 return {vector.xCoord * scale, vector.yCoord * scale, vector.zCoord * scale};
92}
93
94inline Vec3 operator*(double scale, const Vec3& vector)
95{
96 return vector * scale;
97}
98
99inline Vec2 operator-(const Vec2& firstPoint, const Vec2& secondPoint)
100{
101 return {firstPoint.uCoord - secondPoint.uCoord, firstPoint.vCoord - secondPoint.vCoord};
102}
103
105inline double parametricLengthSq(double gUU, double gUV, double gVV, const Vec2& delta)
106{
107 return gUU * delta.uCoord * delta.uCoord + 2. * gUV * delta.uCoord * delta.vCoord +
108 gVV * delta.vCoord * delta.vCoord;
109}
110
113 using Evaluate = void (*)(const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV);
114
116 const void* context = nullptr;
117
119 double lengthSq(const Vec2& uv, const Vec2& delta) const
120 {
121 if (evaluate == nullptr) {
122 return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord;
123 }
124 double gUU = 1.;
125 double gUV = 0.;
126 double gVV = 1.;
127 evaluate(context, uv, gUU, gUV, gVV);
128 return parametricLengthSq(gUU, gUV, gVV, delta);
129 }
130
132 double distanceSq(const Vec2& from, const Vec2& to) const { return lengthSq(from, to - from); }
133
135 double maxScale(const Vec2& uv) const
136 {
137 if (evaluate == nullptr) {
138 return 1.;
139 }
140 double gUU = 1.;
141 double gUV = 0.;
142 double gVV = 1.;
143 evaluate(context, uv, gUU, gUV, gVV);
144 const double trace = gUU + gVV;
145 const double determinant = gUU * gVV - gUV * gUV;
146 // the eigenvalues of a symmetric 2x2 form, guarded against a slightly negative discriminant
147 const double discriminant = std::max(0., trace * trace - 4. * determinant);
148 return std::sqrt(std::max(0., 0.5 * (trace + std::sqrt(discriminant))));
149 }
150};
151
154template <typename Surface>
155inline ParametricMetric parametricMetricOf(const Surface& surface)
156{
157 return {[](const void* context, const Vec2& uv, double& gUU, double& gUV, double& gVV) {
158 static_cast<const Surface*>(context)->parametricMetric(uv, gUU, gUV, gVV);
159 },
160 &surface};
161}
162
163inline double dot(const Vec3& firstVector, const Vec3& secondVector)
164{
165 return firstVector.xCoord * secondVector.xCoord + firstVector.yCoord * secondVector.yCoord +
166 firstVector.zCoord * secondVector.zCoord;
167}
168
169inline Vec3 cross(const Vec3& firstVector, const Vec3& secondVector)
170{
171 return {firstVector.yCoord * secondVector.zCoord - firstVector.zCoord * secondVector.yCoord,
172 firstVector.zCoord * secondVector.xCoord - firstVector.xCoord * secondVector.zCoord,
173 firstVector.xCoord * secondVector.yCoord - firstVector.yCoord * secondVector.xCoord};
174}
175
176inline double normSq(const Vec3& vector)
177{
178 return dot(vector, vector);
179}
180
181inline double norm(const Vec3& vector)
182{
183 return std::sqrt(normSq(vector));
184}
185
186inline Vec3 normalized(const Vec3& vector)
187{
188 const double vectorNorm = norm(vector);
189 if (vectorNorm <= kTolerance) {
190 return {};
191 }
192 return vector * (1. / vectorNorm);
193}
194
195inline double component(const Vec3& vector, int dimension)
196{
197 if (dimension == 0) {
198 return vector.xCoord;
199 }
200 if (dimension == 1) {
201 return vector.yCoord;
202 }
203 return vector.zCoord;
204}
205
206inline void assignComponent(Vec3& vector, int dimension, double value)
207{
208 if (dimension == 0) {
209 vector.xCoord = value;
210 } else if (dimension == 1) {
211 vector.yCoord = value;
212 } else {
213 vector.zCoord = value;
214 }
215}
216
217inline bool finite(const Vec2& point)
218{
219 return std::isfinite(point.uCoord) && std::isfinite(point.vCoord);
220}
221
222inline bool finite(const Vec3& point)
223{
224 return std::isfinite(point.xCoord) && std::isfinite(point.yCoord) && std::isfinite(point.zCoord);
225}
226
227inline double distanceSq(const Vec2& firstPoint, const Vec2& secondPoint)
228{
229 const double deltaU = firstPoint.uCoord - secondPoint.uCoord;
230 const double deltaV = firstPoint.vCoord - secondPoint.vCoord;
231 return deltaU * deltaU + deltaV * deltaV;
232}
233
234inline double distanceSq(const Vec3& firstPoint, const Vec3& secondPoint)
235{
236 return normSq(firstPoint - secondPoint);
237}
238
239inline double cross2D(const Vec2& firstVector, const Vec2& secondVector)
240{
241 return firstVector.uCoord * secondVector.vCoord - firstVector.vCoord * secondVector.uCoord;
242}
243
244inline double pointSegmentDistanceSq(const Vec2& point, const Vec2& segmentStart, const Vec2& segmentEnd)
245{
246 const Vec2 segmentVector = segmentEnd - segmentStart;
247 const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
248 if (segmentLengthSq <= kToleranceSq) {
249 return distanceSq(point, segmentStart);
250 }
251 const double pointProjection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
252 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
253 segmentLengthSq;
254 const double clampedProjection = std::max(0., std::min(1., pointProjection));
255 const Vec2 closestPoint{segmentStart.uCoord + clampedProjection * segmentVector.uCoord,
256 segmentStart.vCoord + clampedProjection * segmentVector.vCoord};
257 return distanceSq(point, closestPoint);
258}
259
260inline double pointSegmentDistanceSq(const Vec3& point, const Vec3& segmentStart, const Vec3& segmentEnd)
261{
262 const Vec3 segmentVector = segmentEnd - segmentStart;
263 const double segmentLengthSq = normSq(segmentVector);
264 if (segmentLengthSq <= kToleranceSq) {
265 return distanceSq(point, segmentStart);
266 }
267 const double pointProjection = dot(point - segmentStart, segmentVector) / segmentLengthSq;
268 const double clampedProjection = std::max(0., std::min(1., pointProjection));
269 const Vec3 closestPoint = segmentStart + segmentVector * clampedProjection;
270 return distanceSq(point, closestPoint);
271}
272
275
278inline void planeParametricMetric(const Vec3& axisU, const Vec3& axisV, double& gUU, double& gUV, double& gVV)
279{
280 gUU = dot(axisU, axisU);
281 gUV = dot(axisU, axisV);
282 gVV = dot(axisV, axisV);
283}
284
286inline void cylinderParametricMetric(double radius, double& gUU, double& gUV, double& gVV)
287{
288 gUU = radius * radius;
289 gUV = 0.;
290 gVV = 1.;
291}
292
295inline void coneParametricMetric(double radiusAtHeight, double slope, double& gUU, double& gUV, double& gVV)
296{
297 gUU = radiusAtHeight * radiusAtHeight;
298 gUV = 0.;
299 gVV = 1. + slope * slope;
300}
301
304inline void sphereParametricMetric(double radius, double theta, double& gUU, double& gUV, double& gVV)
305{
306 const double parallelRadius = radius * std::sin(theta);
307 gUU = parallelRadius * parallelRadius;
308 gUV = 0.;
309 gVV = radius * radius;
310}
311
313inline void torusParametricMetric(double majorRadius, double minorRadius, double phiTube, double& gUU, double& gUV,
314 double& gVV)
315{
316 const double ringRadius = majorRadius + minorRadius * std::cos(phiTube);
317 gUU = ringRadius * ringRadius;
318 gUV = 0.;
319 gVV = minorRadius * minorRadius;
320}
322
323inline bool sameIntersection(double firstDistance, double secondDistance)
324{
325 return std::abs(firstDistance - secondDistance) <=
326 kIntersectionTolerance * std::max(1., std::max(std::abs(firstDistance), std::abs(secondDistance)));
327}
328
330struct RayHit {
331 double distance = 0.;
334 bool onTrimBoundary = false;
335};
336
341
342 Vec2 direction() const { return end - start; }
343
344 double lengthSq() const
345 {
346 const Vec2 delta = end - start;
347 return delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord;
348 }
349
350 bool degenerate() const { return lengthSq() <= kToleranceSq; }
351
353 double distanceSq(const Vec2& point) const { return pointSegmentDistanceSq(point, start, end); }
354
357 Vec2 closestPoint(const Vec2& point, double& parameter) const
358 {
359 const Vec2 segmentVector = end - start;
360 const double segmentLengthSq = segmentVector.uCoord * segmentVector.uCoord +
361 segmentVector.vCoord * segmentVector.vCoord;
362 if (segmentLengthSq <= kToleranceSq) {
363 parameter = 0.;
364 return start;
365 }
366 const double projection = ((point.uCoord - start.uCoord) * segmentVector.uCoord +
367 (point.vCoord - start.vCoord) * segmentVector.vCoord) /
368 segmentLengthSq;
369 parameter = std::max(0., std::min(1., projection));
370 return {start.uCoord + parameter * segmentVector.uCoord, start.vCoord + parameter * segmentVector.vCoord};
371 }
372
375 {
376 lower.uCoord = std::min({lower.uCoord, start.uCoord, end.uCoord});
377 lower.vCoord = std::min({lower.vCoord, start.vCoord, end.vCoord});
378 upper.uCoord = std::max({upper.uCoord, start.uCoord, end.uCoord});
379 upper.vCoord = std::max({upper.vCoord, start.vCoord, end.vCoord});
380 }
381};
382
385 Boundary,
386 Inside };
387
390enum class WireRole { Outer,
391 Inner };
392
395enum class WireStatus {
396 Valid,
397 Reversed,
398 NonFinite,
399 Open,
402 ZeroArea
403};
404
406inline const char* wireStatusMessage(WireStatus status)
407{
408 switch (status) {
410 return "valid";
412 return "orientation normalized to match wire role";
414 return "wire contains a non-finite vertex";
415 case WireStatus::Open:
416 return "wire edges do not form a closed loop";
418 return "wire needs at least three distinct vertices";
420 return "wire has a coincident (pinched) vertex";
422 return "wire has zero area";
423 }
424 return "unknown wire status";
425}
426
428inline double trimLengthFloor(const ParametricMetric& metric, const Vec2& uv)
429{
430 const double scale = metric.maxScale(uv);
431 return scale > kTolerance ? kTolerance / scale : 0.;
432}
433
436 std::vector<Vec2> vertices;
438
440 std::vector<int> sourceEdge;
441
442 int edgeCount() const { return static_cast<int>(vertices.size()); }
443
445 int storedIndexOfSource(int inputIndex) const
446 {
447 for (size_t index = 0; index < sourceEdge.size(); ++index) {
448 if (sourceEdge[index] == inputIndex) {
449 return static_cast<int>(index);
450 }
451 }
452 return -1;
453 }
454
456 {
457 const int count = edgeCount();
458 return {vertices[index % count], vertices[(index + 1) % count]};
459 }
460
462 bool initialize(const std::vector<Vec2>& inputVertices, WireRole wireRole, WireStatus& status,
463 const ParametricMetric& metric = {})
464 {
465 role = wireRole;
466 vertices.clear();
467 vertices.reserve(inputVertices.size());
468 bool droppedAVertex = false;
469
470 for (const auto& vertex : inputVertices) {
471 if (!finite(vertex)) {
472 status = WireStatus::NonFinite;
473 return false;
474 }
475 if (vertices.empty() || metric.distanceSq(vertices.back(), vertex) > kToleranceSq) {
476 vertices.push_back(vertex);
477 } else {
478 droppedAVertex = true;
479 }
480 }
481
482 // drop an explicit closing duplicate (first == last)
483 if (vertices.size() > 1 && metric.distanceSq(vertices.front(), vertices.back()) <= kToleranceSq) {
484 vertices.pop_back();
485 droppedAVertex = true;
486 }
487
488 if (vertices.size() < 3) {
490 return false;
491 }
492
493 // reject self-touching loops (non-adjacent coincident vertices)
494 for (size_t firstIndex = 0; firstIndex < vertices.size(); ++firstIndex) {
495 for (size_t secondIndex = firstIndex + 1; secondIndex < vertices.size(); ++secondIndex) {
496 if (metric.distanceSq(vertices[firstIndex], vertices[secondIndex]) <= kToleranceSq) {
498 return false;
499 }
500 }
501 }
502
503 const double area = signedArea();
504 if (std::abs(area) <= kAreaTolerance) {
505 status = WireStatus::ZeroArea;
506 return false;
507 }
508
509 // segment i is input segment i unless a vertex was dropped; then it is unknown
510 const int storedCount = static_cast<int>(vertices.size());
511 sourceEdge.assign(static_cast<size_t>(storedCount), -1);
512 if (!droppedAVertex) {
513 for (int index = 0; index < storedCount; ++index) {
514 sourceEdge[static_cast<size_t>(index)] = index;
515 }
516 }
517
518 // outer wires must wind CCW (positive area), inner wires CW (negative area)
519 const bool wantPositiveArea = (role == WireRole::Outer);
520 if ((area > 0.) != wantPositiveArea) {
521 std::reverse(vertices.begin(), vertices.end());
522 // reversing the ring maps old vertex k to new index n-1-k, so new segment j spans old
523 // vertices n-1-j and n-2-j, i.e. it is old segment n-2-j traversed backwards
524 std::vector<int> reversedSource(static_cast<size_t>(storedCount), -1);
525 for (int index = 0; index < storedCount; ++index) {
526 reversedSource[static_cast<size_t>(index)] =
527 sourceEdge[static_cast<size_t>((storedCount - 2 - index % storedCount + 2 * storedCount) % storedCount)];
528 }
529 sourceEdge.swap(reversedSource);
530 status = WireStatus::Reversed;
531 return true;
532 }
533
534 status = WireStatus::Valid;
535 return true;
536 }
537
539 bool initializeFromEdges(const std::vector<SurfaceEdge>& edges, WireRole wireRole, WireStatus& status,
540 const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance)
541 {
542 if (edges.size() < 3) {
544 return false;
545 }
546 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
547 if (!finite(edges[edgeIndex].start) || !finite(edges[edgeIndex].end)) {
548 status = WireStatus::NonFinite;
549 return false;
550 }
551 const Vec2& nextStart = edges[(edgeIndex + 1) % edges.size()].start;
552 if (metric.distanceSq(edges[edgeIndex].end, nextStart) > joinTolerance * joinTolerance) {
553 status = WireStatus::Open;
554 return false;
555 }
556 }
557
558 std::vector<Vec2> ringVertices;
559 ringVertices.reserve(edges.size());
560 for (const auto& singleEdge : edges) {
561 ringVertices.push_back(singleEdge.start);
562 }
563 return initialize(ringVertices, wireRole, status, metric);
564 }
565
566 double signedArea() const
567 {
568 double area = 0.;
569 for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) {
570 const auto& currentVertex = vertices[vertexIndex];
571 const auto& nextVertex = vertices[(vertexIndex + 1) % vertices.size()];
572 area += currentVertex.uCoord * nextVertex.vCoord - nextVertex.uCoord * currentVertex.vCoord;
573 }
574 return 0.5 * area;
575 }
576
580 {
581 for (const auto& vertex : vertices) {
582 lower.uCoord = std::min(lower.uCoord, vertex.uCoord);
583 lower.vCoord = std::min(lower.vCoord, vertex.vCoord);
584 upper.uCoord = std::max(upper.uCoord, vertex.uCoord);
585 upper.vCoord = std::max(upper.vCoord, vertex.vCoord);
586 }
587 }
588
590 std::vector<Vec2> sampledBoundary() const
591 {
592 std::vector<Vec2> samples;
593 if (vertices.empty()) {
594 return samples;
595 }
596 samples.reserve(vertices.size() + 1);
597 samples.insert(samples.end(), vertices.begin(), vertices.end());
598 samples.push_back(vertices.front());
599 return samples;
600 }
601
603 WireClassification classify(const Vec2& point, double band) const
604 {
605 const double bandSq = band * band;
606 bool inside = false;
607 for (size_t vertexIndex = 0; vertexIndex < vertices.size(); ++vertexIndex) {
608 const auto& segmentStart = vertices[vertexIndex];
609 const auto& segmentEnd = vertices[(vertexIndex + 1) % vertices.size()];
610 if (pointSegmentDistanceSq(point, segmentStart, segmentEnd) <= bandSq) {
612 }
613 const bool crossesScanline = (segmentStart.vCoord > point.vCoord) != (segmentEnd.vCoord > point.vCoord);
614 if (crossesScanline) {
615 const double intersectionU = segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) *
616 (segmentEnd.uCoord - segmentStart.uCoord) /
617 (segmentEnd.vCoord - segmentStart.vCoord);
618 if (point.uCoord < intersectionU) {
619 inside = !inside;
620 }
621 }
622 }
624 }
625
627 WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const
628 {
629 return classify(point, trimLengthFloor(metric, point));
630 }
631};
632
633inline bool pointInTriangle(const Vec2& point, const Vec2& firstVertex, const Vec2& secondVertex,
634 const Vec2& thirdVertex)
635{
636 const double firstCross = cross2D(secondVertex - firstVertex, point - firstVertex);
637 const double secondCross = cross2D(thirdVertex - secondVertex, point - secondVertex);
638 const double thirdCross = cross2D(firstVertex - thirdVertex, point - thirdVertex);
639 return firstCross >= -kTolerance && secondCross >= -kTolerance && thirdCross >= -kTolerance;
640}
641
643inline double ringSignedArea(const std::vector<Vec2>& points, const std::vector<int>& ring)
644{
645 double area = 0.;
646 for (size_t position = 0; position < ring.size(); ++position) {
647 area += cross2D(points[ring[position]], points[ring[(position + 1) % ring.size()]]);
648 }
649 return 0.5 * area;
650}
651
653inline std::vector<int> orientedRing(const std::vector<Vec2>& points, int first, int count, bool counterClockwise)
654{
655 std::vector<int> ring(static_cast<size_t>(count));
656 std::iota(ring.begin(), ring.end(), first);
657 if ((ringSignedArea(points, ring) >= 0.) != counterClockwise) {
658 std::reverse(ring.begin(), ring.end());
659 }
660 return ring;
661}
662
664inline bool leavesIntoMaterial(const Vec2& previous, const Vec2& vertex, const Vec2& next, const Vec2& direction)
665{
666 const Vec2 toPrevious = previous - vertex;
667 const Vec2 toNext = next - vertex;
668 if (cross2D(vertex - previous, next - vertex) > 0.) {
669 return cross2D(toNext, direction) > 0. && cross2D(direction, toPrevious) > 0.;
670 }
671 return !(cross2D(toPrevious, direction) >= 0. && cross2D(direction, toNext) >= 0.);
672}
673
675inline bool segmentsTouch(const Vec2& firstStart, const Vec2& firstEnd, const Vec2& secondStart, const Vec2& secondEnd)
676{
677 const double secondStartSide = cross2D(firstEnd - firstStart, secondStart - firstStart);
678 const double secondEndSide = cross2D(firstEnd - firstStart, secondEnd - firstStart);
679 const double firstStartSide = cross2D(secondEnd - secondStart, firstStart - secondStart);
680 const double firstEndSide = cross2D(secondEnd - secondStart, firstEnd - secondStart);
681 if (secondStartSide * secondEndSide < 0. && firstStartSide * firstEndSide < 0.) {
682 return true;
683 }
684 return pointSegmentDistanceSq(secondStart, firstStart, firstEnd) <= kToleranceSq ||
685 pointSegmentDistanceSq(secondEnd, firstStart, firstEnd) <= kToleranceSq ||
686 pointSegmentDistanceSq(firstStart, secondStart, secondEnd) <= kToleranceSq ||
687 pointSegmentDistanceSq(firstEnd, secondStart, secondEnd) <= kToleranceSq;
688}
689
691inline std::vector<int> bridgeHoles(const std::vector<Vec2>& outer, const std::vector<std::vector<Vec2>>& holeLoops,
692 std::vector<Vec2>& points)
693{
694 points = outer;
695 std::vector<int> ring = orientedRing(points, 0, static_cast<int>(outer.size()), true);
696 std::vector<std::vector<int>> holes;
697 for (const auto& loop : holeLoops) {
698 if (loop.size() >= 3) {
699 const int first = static_cast<int>(points.size());
700 points.insert(points.end(), loop.begin(), loop.end());
701 holes.push_back(orientedRing(points, first, static_cast<int>(loop.size()), false));
702 }
703 }
704 auto maxU = [&](const std::vector<int>& hole) {
705 double largest = -std::numeric_limits<double>::infinity();
706 for (int index : hole) {
707 largest = std::max(largest, points[index].uCoord);
708 }
709 return largest;
710 };
711 // the rightmost hole first, the usual order for bridging holes
712 std::stable_sort(holes.begin(), holes.end(), [&](const auto& first, const auto& second) { return maxU(first) > maxU(second); });
713
714 // whether the bridge from hole index holeVertex to ring index ringVertex crosses an edge of the ring or a pending hole
715 auto bridgeIsClear = [&](int holeVertex, int ringVertex, size_t firstPendingHole) {
716 auto crossesLoop = [&](const std::vector<int>& loop) {
717 for (size_t position = 0; position < loop.size(); ++position) {
718 const int edgeStart = loop[position];
719 const int edgeEnd = loop[(position + 1) % loop.size()];
720 if (edgeStart == holeVertex || edgeEnd == holeVertex || edgeStart == ringVertex || edgeEnd == ringVertex) {
721 continue;
722 }
723 if (segmentsTouch(points[holeVertex], points[ringVertex], points[edgeStart], points[edgeEnd])) {
724 return true;
725 }
726 }
727 return false;
728 };
729 if (crossesLoop(ring)) {
730 return false;
731 }
732 for (size_t holeIndex = firstPendingHole; holeIndex < holes.size(); ++holeIndex) {
733 if (crossesLoop(holes[holeIndex])) {
734 return false;
735 }
736 }
737 return true;
738 };
739
740 for (size_t holeIndex = 0; holeIndex < holes.size(); ++holeIndex) {
741 const auto& hole = holes[holeIndex];
742 const size_t holeCount = hole.size();
743 // try the hole's vertices from the rightmost, and for each the ring vertices from the nearest
744 std::vector<size_t> holeOrder(holeCount);
745 std::iota(holeOrder.begin(), holeOrder.end(), size_t{0});
746 std::stable_sort(holeOrder.begin(), holeOrder.end(),
747 [&](size_t first, size_t second) { return points[hole[first]].uCoord > points[hole[second]].uCoord; });
748 bool bridged = false;
749 for (size_t holePosition : holeOrder) {
750 const int holeVertex = hole[holePosition];
751 const Vec2& holePoint = points[holeVertex];
752 const Vec2& holePrevious = points[hole[(holePosition + holeCount - 1) % holeCount]];
753 const Vec2& holeNext = points[hole[(holePosition + 1) % holeCount]];
754 const size_t ringCount = ring.size();
755 std::vector<size_t> ringOrder(ringCount);
756 std::iota(ringOrder.begin(), ringOrder.end(), size_t{0});
757 std::stable_sort(ringOrder.begin(), ringOrder.end(), [&](size_t first, size_t second) {
758 return distanceSq(points[ring[first]], holePoint) < distanceSq(points[ring[second]], holePoint);
759 });
760 for (size_t ringPosition : ringOrder) {
761 const int ringVertex = ring[ringPosition];
762 const Vec2& ringPoint = points[ringVertex];
763 if (distanceSq(ringPoint, holePoint) <= kToleranceSq ||
764 !leavesIntoMaterial(points[ring[(ringPosition + ringCount - 1) % ringCount]], ringPoint,
765 points[ring[(ringPosition + 1) % ringCount]], holePoint - ringPoint) ||
766 !leavesIntoMaterial(holePrevious, holePoint, holeNext, ringPoint - holePoint) ||
767 !bridgeIsClear(holeVertex, ringVertex, holeIndex)) {
768 continue;
769 }
770 // ring up to the bridge vertex, the whole hole from the bridge back to it, then the bridge vertex again
771 std::vector<int> splice;
772 splice.reserve(holeCount + 2);
773 for (size_t step = 0; step <= holeCount; ++step) {
774 splice.push_back(hole[(holePosition + step) % holeCount]);
775 }
776 splice.push_back(ringVertex);
777 ring.insert(ring.begin() + static_cast<std::ptrdiff_t>(ringPosition) + 1, splice.begin(), splice.end());
778 bridged = true;
779 break;
780 }
781 if (bridged) {
782 break;
783 }
784 }
785 }
786 return ring;
787}
788
790inline std::vector<std::array<int, 3>> triangulateRing(const std::vector<Vec2>& points, std::vector<int> remainingIndices)
791{
792 std::vector<std::array<int, 3>> triangles;
793 const size_t ringCount = remainingIndices.size();
794 size_t guardCounter = 0;
795 while (remainingIndices.size() > 3 && guardCounter++ < ringCount * ringCount) {
796 bool clippedEar = false;
797 for (size_t indexPosition = 0; indexPosition < remainingIndices.size(); ++indexPosition) {
798 const int previousIndex = remainingIndices[(indexPosition + remainingIndices.size() - 1) % remainingIndices.size()];
799 const int currentIndex = remainingIndices[indexPosition];
800 const int nextIndex = remainingIndices[(indexPosition + 1) % remainingIndices.size()];
801
802 const auto& previousVertex = points[previousIndex];
803 const auto& currentVertex = points[currentIndex];
804 const auto& nextVertex = points[nextIndex];
805 if (cross2D(currentVertex - previousVertex, nextVertex - currentVertex) <= kTolerance) {
806 continue;
807 }
808
809 bool containsOtherVertex = false;
810 for (int candidateIndex : remainingIndices) {
811 if (candidateIndex == previousIndex || candidateIndex == currentIndex || candidateIndex == nextIndex) {
812 continue;
813 }
814 if (pointInTriangle(points[candidateIndex], previousVertex, currentVertex, nextVertex)) {
815 containsOtherVertex = true;
816 break;
817 }
818 }
819 if (containsOtherVertex) {
820 continue;
821 }
822
823 triangles.push_back({previousIndex, currentIndex, nextIndex});
824 remainingIndices.erase(remainingIndices.begin() + indexPosition);
825 clippedEar = true;
826 break;
827 }
828
829 if (!clippedEar) {
830 break;
831 }
832 }
833
834 if (remainingIndices.size() == 3) {
835 triangles.push_back({remainingIndices[0], remainingIndices[1], remainingIndices[2]});
836 }
837 return triangles;
838}
839
841inline bool coversRing(const std::vector<Vec2>& points, const std::vector<int>& ring, const std::vector<std::array<int, 3>>& triangles)
842{
843 double area = 0.;
844 for (const auto& triangle : triangles) {
845 area += 0.5 * cross2D(points[triangle[1]] - points[triangle[0]], points[triangle[2]] - points[triangle[0]]);
846 }
847 const double ringArea = ringSignedArea(points, ring);
848 return std::abs(area - ringArea) <= 1.e-6 * std::abs(ringArea);
849}
850
853inline constexpr double kPi = 3.14159265358979323846;
854inline constexpr double kTwoPi = 2. * kPi;
855inline constexpr double kHalfPi = 0.5 * kPi;
857inline constexpr int kArcSamples = 24;
859
861inline double angularTolerance(double radius)
862{
863 return kTolerance / std::max(radius, kTolerance);
864}
865
867inline constexpr double kCoverChunkAngle = kPi / 4.;
868
871inline int coverChunkCount(double span)
872{
873 constexpr int fullTurnChunks = static_cast<int>(kTwoPi / kCoverChunkAngle); // eight
874 return std::max(1, std::min(fullTurnChunks, static_cast<int>(std::ceil(span / kCoverChunkAngle))));
875}
876
878inline void sinusoidRange(double a, double b, double t0, double t1, double& minimum, double& maximum)
879{
880 const double atStart = a * std::cos(t0) + b * std::sin(t0);
881 const double atEnd = a * std::cos(t1) + b * std::sin(t1);
882 minimum = std::min(atStart, atEnd);
883 maximum = std::max(atStart, atEnd);
884 const double amplitude = std::hypot(a, b);
885 const double crest = std::atan2(b, a);
886 // shifted into [t0, t0 + 2pi), where a span of at most a full turn makes "<= t1" exactly the
887 // test for falling inside the interval
888 const double crestInRange = crest - kTwoPi * std::floor((crest - t0) / kTwoPi);
889 if (crestInRange <= t1) {
890 maximum = amplitude;
891 }
892 const double trough = crest + kPi;
893 const double troughInRange = trough - kTwoPi * std::floor((trough - t0) / kTwoPi);
894 if (troughInRange <= t1) {
895 minimum = -amplitude;
896 }
897}
898
901inline double sinusoidMinimum(double a, double b, double t0, double t1)
902{
903 double minimum = 0.;
904 double maximum = 0.;
905 sinusoidRange(a, b, t0, t1, minimum, maximum);
906 return minimum;
907}
908
909inline double sinusoidMaximum(double a, double b, double t0, double t1)
910{
911 double minimum = 0.;
912 double maximum = 0.;
913 sinusoidRange(a, b, t0, t1, minimum, maximum);
914 return maximum;
915}
917
920inline bool angleInSweepRange(double angle, double start, double sweep, double tolerance)
921{
922 if (sweep >= kTwoPi - kTolerance) {
923 return true;
924 }
925 double delta = angle - start;
926 delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi)
927 return delta <= sweep + tolerance || delta >= kTwoPi - tolerance;
928}
929
931inline void gaussLegendre(int n, std::vector<double>& nodes, std::vector<double>& weights)
932{
933 nodes.assign(std::max(n, 1), 0.);
934 weights.assign(std::max(n, 1), 0.);
935 if (n < 1) {
936 return;
937 }
938 for (int i = 0; i < n; ++i) {
939 double root = std::cos(kPi * (i + 0.75) / (n + 0.5)); // asymptotic initial guess
940 double derivative = 1.;
941 for (int iteration = 0; iteration < 100; ++iteration) {
942 double previous = 1.;
943 double current = root;
944 for (int degreeIndex = 2; degreeIndex <= n; ++degreeIndex) {
945 const double next = ((2 * degreeIndex - 1) * root * current - (degreeIndex - 1) * previous) / degreeIndex;
946 previous = current;
947 current = next;
948 }
949 derivative = n * (root * current - previous) / (root * root - 1.);
950 const double delta = current / derivative;
951 root -= delta;
952 if (std::abs(delta) < 1.e-15) {
953 break;
954 }
955 }
956 nodes[i] = root;
957 weights[i] = 2. / ((1. - root * root) * derivative * derivative);
958 }
959}
960
963inline int solveDepressedCubic(double coeffP, double coeffQ, std::array<double, 3>& roots)
964{
965 const double discriminant = coeffQ * coeffQ / 4. + coeffP * coeffP * coeffP / 27.;
966 if (!(coeffP < 0.) || discriminant > 0.) {
967 const double sqrtDiscriminant = std::sqrt(std::max(0., discriminant));
968 roots[0] = std::cbrt(-0.5 * coeffQ + sqrtDiscriminant) + std::cbrt(-0.5 * coeffQ - sqrtDiscriminant);
969 return 1;
970 }
971 // three real roots: coeffP < 0 here, so the trigonometric form is well defined
972 const double magnitude = 2. * std::sqrt(-coeffP / 3.);
973 const double cosineArgument = std::max(-1., std::min(1., 3. * coeffQ / (coeffP * magnitude)));
974 const double baseAngle = std::acos(cosineArgument);
975 for (int branch = 0; branch < 3; ++branch) {
976 roots[branch] = magnitude * std::cos((baseAngle - kTwoPi * branch) / 3.);
977 }
978 return 3;
979}
980
982enum class QuarticBranch {
985 Resolvent
986};
987
990 std::array<double, 4> value{};
991 int count = 0;
992 void push_back(double root)
993 {
994 assert(count < 4 && "QuarticRoots holds at most four roots");
995 value[count++] = root;
996 }
997 double* begin() { return value.data(); }
998 double* end() { return value.data() + count; }
999 const double* begin() const { return value.data(); }
1000 const double* end() const { return value.data() + count; }
1001 size_t size() const { return static_cast<size_t>(count); }
1002 bool empty() const { return count == 0; }
1003 double operator[](size_t index) const { return value[index]; }
1004};
1005
1008inline QuarticRoots solveQuarticReal(double a4, double a3, double a2, double a1, double a0,
1009 QuarticBranch* takenBranch = nullptr)
1010{
1011 const auto note = [takenBranch](QuarticBranch branch) {
1012 if (takenBranch) {
1013 *takenBranch = branch;
1014 }
1015 };
1017 QuarticRoots roots;
1018 // A genuine quartic needs only a non-zero leading coefficient. There is no scale to compare it
1019 // against -- the normalisation below handles any coefficient ratio -- so the test is exact.
1020 if (!(std::abs(a4) > 0.)) {
1021 return roots; // the torus caller guarantees a4 = |dir|^4 > 0
1022 }
1023 // monic x^4 + b x^3 + c x^2 + d x + e
1024 double coeffB = a3 / a4, coeffC = a2 / a4, coeffD = a1 / a4, coeffE = a0 / a4;
1025 if (!std::isfinite(coeffB) || !std::isfinite(coeffC) || !std::isfinite(coeffD) || !std::isfinite(coeffE)) {
1026 return roots; // a4 is denormal-small next to the rest, or an input was not finite
1027 }
1028 // Cauchy root bound rounded up to a power of two, so x = scale * y is exact; x^4 = 0 keeps scale = 1
1029 const double rootBound = std::max({std::abs(coeffB), std::sqrt(std::abs(coeffC)),
1030 std::cbrt(std::abs(coeffD)), std::sqrt(std::sqrt(std::abs(coeffE)))});
1031 int boundExponent = 0;
1032 std::frexp(rootBound, &boundExponent);
1033 const double scale = std::ldexp(1., boundExponent);
1034 coeffB /= scale;
1035 coeffC /= scale * scale;
1036 coeffD /= scale * scale * scale;
1037 coeffE /= scale * scale * scale * scale;
1038
1039 // depress with y = z - b/4: z^4 + p z^2 + q z + r
1040 const double termP = coeffC - 3. * coeffB * coeffB / 8.;
1041 const double termQ = coeffD - coeffB * coeffC / 2. + coeffB * coeffB * coeffB / 8.;
1042 const double termR =
1043 coeffE - coeffB * coeffD / 4. + coeffB * coeffB * coeffC / 16. - 3. * coeffB * coeffB * coeffB * coeffB / 256.;
1044 const double shift = -coeffB / 4.;
1045
1046 auto addQuadraticRoots = [&](double quadB, double quadC) {
1047 const double discriminant = quadB * quadB - 4. * quadC;
1048 if (discriminant < 0.) {
1049 return; // complex pair
1050 }
1051 const double sqrtDiscriminant = std::sqrt(discriminant);
1052 roots.push_back(shift + 0.5 * (-quadB - sqrtDiscriminant));
1053 roots.push_back(shift + 0.5 * (-quadB + sqrtDiscriminant));
1054 };
1055
1056 auto addBiquadraticRoots = [&]() {
1057 // biquadratic z^4 + p z^2 + r = 0
1058 const double discriminant = termP * termP - 4. * termR;
1059 if (discriminant < 0.) {
1060 return;
1061 }
1062 const double sqrtDiscriminant = std::sqrt(discriminant);
1063 for (const double zSquared : {0.5 * (-termP + sqrtDiscriminant), 0.5 * (-termP - sqrtDiscriminant)}) {
1064 if (zSquared >= 0.) {
1065 const double z = std::sqrt(zSquared);
1066 roots.push_back(shift + z);
1067 roots.push_back(shift - z);
1068 }
1069 }
1070 };
1071
1072 // q is zero to the precision of its terms, which normalisation bounds by 1: kQuarticEpsilon over the whole quartic, not over q's terms
1073 bool biquadratic = std::abs(termQ) <= kQuarticEpsilon;
1074 if (!biquadratic) {
1076 // resolvent cubic m^3 + p m^2 + (p^2/4 - r) m - q^2/8 = 0; its largest real root is > 0
1077 const double cubicA2 = termP;
1078 const double cubicA1 = termP * termP / 4. - termR;
1079 const double cubicA0 = -termQ * termQ / 8.;
1080 const double cubicP = cubicA1 - cubicA2 * cubicA2 / 3.;
1081 const double cubicQ = 2. * cubicA2 * cubicA2 * cubicA2 / 27. - cubicA2 * cubicA1 / 3. + cubicA0;
1082 std::array<double, 3> cubicRoots;
1083 const int cubicCount = solveDepressedCubic(cubicP, cubicQ, cubicRoots);
1084 double resolvent = 0.;
1085 for (int index = 0; index < cubicCount; ++index) {
1086 resolvent = std::max(resolvent, cubicRoots[index] - cubicA2 / 3.);
1087 }
1088 // a resolvent below the resolution of its cubic is noise; then the biquadratic branch is the better-conditioned answer
1089 const double resolventScale = std::max({std::abs(cubicA2), std::sqrt(std::abs(cubicA1)),
1090 std::cbrt(std::abs(cubicA0))});
1091 if (resolvent > kQuarticEpsilon * resolventScale) {
1092 const double sqrtTwoResolvent = std::sqrt(2. * resolvent);
1093 const double linearTerm = sqrtTwoResolvent * termQ / (4. * resolvent);
1094 addQuadraticRoots(-sqrtTwoResolvent, termP / 2. + resolvent + linearTerm);
1095 addQuadraticRoots(sqrtTwoResolvent, termP / 2. + resolvent - linearTerm);
1096 } else {
1097 biquadratic = true;
1098 }
1099 }
1100 if (biquadratic) {
1102 addBiquadraticRoots();
1103 }
1104
1105 // Newton polish against the monic quartic; a step longer than the Cauchy bound 2, or non-finite, is rejected
1106 auto quartic = [&](double x) { return (((x + coeffB) * x + coeffC) * x + coeffD) * x + coeffE; };
1107 auto quarticDerivative = [&](double x) { return ((4. * x + 3. * coeffB) * x + 2. * coeffC) * x + coeffD; };
1108 for (double& root : roots) {
1109 for (int iteration = 0; iteration < 2; ++iteration) {
1110 const double step = quartic(root) / quarticDerivative(root);
1111 if (std::isfinite(step) && std::abs(step) <= 2.) {
1112 root -= step;
1113 }
1114 }
1115 }
1116 for (double& root : roots) {
1117 root *= scale; // exact: scale is a power of two
1118 }
1119 return roots;
1120}
1121
1123enum class CurveKind { Line,
1124 Arc,
1125 BSpline
1126};
1127
1129struct Curve2D {
1134 double radius = 0.;
1135 double startAngle = 0.;
1136 double endAngle = 0.;
1137
1139 int degree = 0;
1140 std::vector<Vec2> poles;
1141 std::vector<double> weights;
1142 std::vector<double> knots;
1144 mutable std::vector<Vec2> bsplineCache;
1146
1152
1154 {
1156 canonicalEnd = end;
1157 hasCanonicalEndpoints = true;
1158 bsplineCache.clear(); // the polyline carries them, so it has to be rebuilt
1159 }
1160
1166
1167 static Curve2D makeLine(const Vec2& start, const Vec2& end)
1168 {
1169 Curve2D curve;
1170 curve.kind = CurveKind::Line;
1171 curve.lineStart = start;
1172 curve.lineEnd = end;
1173 return curve;
1174 }
1175
1176 static Curve2D makeArc(const Vec2& arcCenter, double arcRadius, double arcStartAngle, double arcEndAngle)
1177 {
1178 Curve2D curve;
1179 curve.kind = CurveKind::Arc;
1180 curve.center = arcCenter;
1181 curve.radius = arcRadius;
1182 curve.startAngle = arcStartAngle;
1183 curve.endAngle = arcEndAngle;
1184 return curve;
1185 }
1186
1188 static Curve2D makeCircle(const Vec2& arcCenter, double arcRadius, bool clockwise = false)
1189 {
1190 return makeArc(arcCenter, arcRadius, 0., clockwise ? -kTwoPi : kTwoPi);
1191 }
1192
1195 static Curve2D makeBSpline(int splineDegree, std::vector<Vec2> splinePoles,
1196 std::vector<double> splineWeights, std::vector<double> splineKnots)
1197 {
1198 Curve2D curve;
1199 curve.kind = CurveKind::BSpline;
1200 curve.degree = splineDegree;
1201 curve.poles = std::move(splinePoles);
1202 curve.weights = std::move(splineWeights);
1203 curve.knots = std::move(splineKnots);
1204 return curve;
1205 }
1206
1207 bool isArc() const { return kind == CurveKind::Arc; }
1208 bool isBSpline() const { return kind == CurveKind::BSpline; }
1209
1210 double sweep() const { return endAngle - startAngle; }
1211
1214 double bsplineT0() const { return knots[degree]; }
1215 double bsplineT1() const { return knots[poles.size()]; }
1216
1218 bool bsplineIsClamped() const
1219 {
1220 const size_t lastKnot = knots.size() - 1;
1221 for (int offset = 1; offset <= degree; ++offset) {
1222 if (std::abs(knots[offset] - knots[0]) > kTolerance ||
1223 std::abs(knots[lastKnot - offset] - knots[lastKnot]) > kTolerance) {
1224 return false;
1225 }
1226 }
1227 return true;
1228 }
1229
1231 bool bsplineRational() const
1232 {
1233 for (double weight : weights) {
1234 if (std::abs(weight - 1.) > kTolerance) {
1235 return true;
1236 }
1237 }
1238 return false;
1239 }
1240
1242 int bsplineSpan(double knotValue) const
1243 {
1244 const int lastPole = static_cast<int>(poles.size()) - 1;
1245 if (knotValue >= knots[lastPole + 1]) {
1246 return lastPole;
1247 }
1248 if (knotValue <= knots[degree]) {
1249 return degree;
1250 }
1251 int low = degree;
1252 int high = lastPole + 1;
1253 int mid = (low + high) / 2;
1254 while (knotValue < knots[mid] || knotValue >= knots[mid + 1]) {
1255 if (knotValue < knots[mid]) {
1256 high = mid;
1257 } else {
1258 low = mid;
1259 }
1260 mid = (low + high) / 2;
1261 }
1262 return mid;
1263 }
1264
1266 void bsplineBasis(int span, double knotValue, std::vector<double>& basis,
1267 std::vector<double>& basisDeriv) const
1268 {
1269 const int p = degree;
1270 std::vector<std::vector<double>> ndu(p + 1, std::vector<double>(p + 1, 0.));
1271 std::vector<double> left(p + 1, 0.);
1272 std::vector<double> right(p + 1, 0.);
1273 ndu[0][0] = 1.;
1274 for (int j = 1; j <= p; ++j) {
1275 left[j] = knotValue - knots[span + 1 - j];
1276 right[j] = knots[span + j] - knotValue;
1277 double saved = 0.;
1278 for (int r = 0; r < j; ++r) {
1279 ndu[j][r] = right[r + 1] + left[j - r];
1280 const double temp = ndu[r][j - 1] / ndu[j][r];
1281 ndu[r][j] = saved + right[r + 1] * temp;
1282 saved = left[j - r] * temp;
1283 }
1284 ndu[j][j] = saved;
1285 }
1286 basis.assign(p + 1, 0.);
1287 basisDeriv.assign(p + 1, 0.);
1288 for (int j = 0; j <= p; ++j) {
1289 basis[j] = ndu[j][p];
1290 }
1291 // first derivative (specialization of DersBasisFuns for the k = 1 term)
1292 for (int r = 0; r <= p; ++r) {
1293 double d = 0.;
1294 const int pk = p - 1;
1295 if (r >= 1) {
1296 d += (1. / ndu[pk + 1][r - 1]) * ndu[r - 1][pk];
1297 }
1298 if (r <= pk) {
1299 d += (-1. / ndu[pk + 1][r]) * ndu[r][pk];
1300 }
1301 basisDeriv[r] = d * p;
1302 }
1303 }
1304
1307 void bsplineEval(double knotValue, Vec2& pointOut, Vec2& derivativeOut) const
1308 {
1309 const int p = degree;
1310 const int span = bsplineSpan(knotValue);
1311 std::vector<double> basis;
1312 std::vector<double> basisDeriv;
1313 bsplineBasis(span, knotValue, basis, basisDeriv);
1314 Vec2 weightedSum{0., 0.};
1315 Vec2 weightedDeriv{0., 0.};
1316 double weightTotal = 0.;
1317 double weightDeriv = 0.;
1318 for (int j = 0; j <= p; ++j) {
1319 const int idx = span - p + j;
1320 const double weight = weights.empty() ? 1. : weights[idx];
1321 weightedSum.uCoord += basis[j] * weight * poles[idx].uCoord;
1322 weightedSum.vCoord += basis[j] * weight * poles[idx].vCoord;
1323 weightTotal += basis[j] * weight;
1324 weightedDeriv.uCoord += basisDeriv[j] * weight * poles[idx].uCoord;
1325 weightedDeriv.vCoord += basisDeriv[j] * weight * poles[idx].vCoord;
1326 weightDeriv += basisDeriv[j] * weight;
1327 }
1328 const double invWeight = (std::abs(weightTotal) > kTolerance) ? 1. / weightTotal : 0.;
1329 pointOut = {weightedSum.uCoord * invWeight, weightedSum.vCoord * invWeight};
1330 derivativeOut = {(weightedDeriv.uCoord * weightTotal - weightedSum.uCoord * weightDeriv) * invWeight * invWeight,
1331 (weightedDeriv.vCoord * weightTotal - weightedSum.vCoord * weightDeriv) * invWeight * invWeight};
1332 }
1333
1336 {
1337 const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0());
1338 Vec2 point;
1340 bsplineEval(knotValue, point, derivative);
1341 return point;
1342 }
1343
1345 void bsplineSampleInto(std::vector<Vec2>& samples, double flatnessSq = kBSplineFlatnessSq,
1346 int maxDepth = 16) const
1347 {
1348 const double t0 = bsplineT0();
1349 const double t1 = bsplineT1();
1350 Vec2 startPointValue;
1351 Vec2 endPointValue;
1352 Vec2 unusedDerivative;
1353 bsplineEval(t0, startPointValue, unusedDerivative);
1354 bsplineEval(t1, endPointValue, unusedDerivative);
1355 samples.push_back(startPointValue);
1356 bsplineSampleRecursive(t0, t1, startPointValue, endPointValue, flatnessSq, maxDepth, samples);
1357 }
1358
1360 bool spansInteriorKnot(double lowT, double highT) const
1361 {
1362 // a clamped knot vector repeats its ends degree+1 times, so the interior knots are the
1363 // entries [degree + 1, poles.size()); a single-span (Bezier) curve has none
1364 const size_t firstInterior = static_cast<size_t>(degree) + 1;
1365 const size_t endInterior = std::min(poles.size(), knots.size());
1366 if (firstInterior >= endInterior) {
1367 return false;
1368 }
1369 const auto begin = knots.begin() + static_cast<std::ptrdiff_t>(firstInterior);
1370 const auto end = knots.begin() + static_cast<std::ptrdiff_t>(endInterior);
1371 const auto firstAbove = std::upper_bound(begin, end, lowT);
1372 return firstAbove != end && *firstAbove < highT;
1373 }
1374
1376 void appendInteriorKnots(double from, double to, std::vector<double>& breakpoints) const
1377 {
1378 if (kind != CurveKind::BSpline) {
1379 return;
1380 }
1381 const double t0 = bsplineT0();
1382 const double span = bsplineT1() - t0;
1383 if (!(span > 0.)) {
1384 return;
1385 }
1386 const size_t firstInterior = static_cast<size_t>(degree) + 1;
1387 const size_t endInterior = std::min(poles.size(), knots.size());
1388 for (size_t index = firstInterior; index < endInterior; ++index) {
1389 const double parameter = (knots[index] - t0) / span;
1390 if (parameter > from && parameter < to) {
1391 breakpoints.push_back(parameter);
1392 }
1393 }
1394 }
1395
1397 double uVariation(double from, double to) const
1398 {
1399 if (kind == CurveKind::Line) {
1400 return std::abs(lineEnd.uCoord - lineStart.uCoord) * std::abs(to - from);
1401 }
1402 if (kind == CurveKind::BSpline) {
1403 // within one knot span the curve lies in the hull of its degree + 1 poles, so their u spread bounds the travel
1404 const double knotStart = bsplineT0();
1405 const double knotSpan = bsplineT1() - knotStart;
1406 const double knotMid = knotStart + 0.5 * (from + to) * knotSpan;
1407 size_t spanIndex = static_cast<size_t>(degree);
1408 while (spanIndex + 1 < poles.size() && spanIndex + 1 < knots.size() && knots[spanIndex + 1] <= knotMid) {
1409 ++spanIndex;
1410 }
1411 const size_t firstPole = spanIndex - static_cast<size_t>(degree);
1412 double lowU = std::numeric_limits<double>::infinity();
1413 double highU = -std::numeric_limits<double>::infinity();
1414 for (size_t index = firstPole; index <= spanIndex && index < poles.size(); ++index) {
1415 lowU = std::min(lowU, poles[index].uCoord);
1416 highU = std::max(highU, poles[index].uCoord);
1417 }
1418 return (highU >= lowU) ? (highU - lowU) : 0.;
1419 }
1420 // arc: u(angle) = center.u + radius cos(angle), so u turns exactly at angle = 0 and pi (mod
1421 // 2 pi). Sum the monotone runs between those turning points and the interval's own ends.
1422 const double angleFrom = startAngle + from * sweep();
1423 const double angleTo = startAngle + to * sweep();
1424 const double low = std::min(angleFrom, angleTo);
1425 const double high = std::max(angleFrom, angleTo);
1426 double variation = 0.;
1427 double previous = low;
1428 const double firstTurn = std::ceil(low / kPi) * kPi;
1429 for (double turn = firstTurn; turn < high; turn += kPi) {
1430 variation += std::abs(radius * (std::cos(turn) - std::cos(previous)));
1431 previous = turn;
1432 }
1433 return variation + std::abs(radius * (std::cos(high) - std::cos(previous)));
1434 }
1435
1436 void bsplineSampleRecursive(double t0, double t1, const Vec2& p0, const Vec2& p1, double flatnessSq,
1437 int depth, std::vector<Vec2>& samples) const
1438 {
1439 const double tMid = 0.5 * (t0 + t1);
1440 Vec2 midPoint;
1441 Vec2 unusedDerivative;
1442 bsplineEval(tMid, midPoint, unusedDerivative);
1443 // a degenerate (closed) chord must not end the recursion: test the distance to its single point instead
1444 const bool degenerateChord = surface::distanceSq(p0, p1) <= flatnessSq;
1445 const auto deviationSq = [&](const Vec2& point) {
1446 return degenerateChord ? surface::distanceSq(point, p0) : pointSegmentDistanceSq(point, p0, p1);
1447 };
1448 // Three interior probes: a single midpoint probe is blind to curves symmetric about their parameter midpoint.
1449 double flatness = deviationSq(midPoint);
1450 for (const double fraction : {0.25, 0.75}) {
1451 Vec2 probePoint;
1452 bsplineEval(t0 + (t1 - t0) * fraction, probePoint, unusedDerivative);
1453 flatness = std::max(flatness, deviationSq(probePoint));
1454 }
1455 if (depth <= 0 || (flatness <= flatnessSq && !spansInteriorKnot(t0, t1))) {
1456 samples.push_back(p1);
1457 return;
1458 }
1459 bsplineSampleRecursive(t0, tMid, p0, midPoint, flatnessSq, depth - 1, samples);
1460 bsplineSampleRecursive(tMid, t1, midPoint, p1, flatnessSq, depth - 1, samples);
1461 }
1462
1464 const std::vector<Vec2>& bsplineSamples() const
1465 {
1466 if (bsplineCache.empty()) {
1468 // one canonical polyline, with the seam vertices substituted at its ends
1469 if (hasCanonicalEndpoints && bsplineCache.size() >= 2) {
1470 bsplineCache.front() = canonicalStart;
1471 bsplineCache.back() = canonicalEnd;
1472 }
1473 }
1474 return bsplineCache;
1475 }
1477
1480 bool valid() const
1481 {
1482 if (kind == CurveKind::Line) {
1483 return finite(lineStart) && finite(lineEnd);
1484 }
1485 if (kind == CurveKind::Arc) {
1486 return finite(center) && std::isfinite(radius) && radius > kTolerance && std::isfinite(startAngle) &&
1487 std::isfinite(endAngle);
1488 }
1489 // B-spline
1490 const int nPoles = static_cast<int>(poles.size());
1491 if (degree < 1 || nPoles < degree + 1) {
1492 return false;
1493 }
1494 if (static_cast<int>(knots.size()) != nPoles + degree + 1) {
1495 return false;
1496 }
1497 if (!weights.empty() && static_cast<int>(weights.size()) != nPoles) {
1498 return false;
1499 }
1500 for (const auto& pole : poles) {
1501 if (!finite(pole)) {
1502 return false;
1503 }
1504 }
1505 for (double weight : weights) {
1506 if (!std::isfinite(weight) || weight <= kTolerance) {
1507 return false;
1508 }
1509 }
1510 for (size_t index = 1; index < knots.size(); ++index) {
1511 if (!std::isfinite(knots[index]) || knots[index] < knots[index - 1] - kTolerance) {
1512 return false;
1513 }
1514 }
1515 return bsplineT1() - bsplineT0() > kTolerance;
1516 }
1517
1519 {
1520 return {center.uCoord + radius * std::cos(angle), center.vCoord + radius * std::sin(angle)};
1521 }
1522
1524 Vec2 pointAt(double parameter) const
1525 {
1526 if (kind == CurveKind::Line) {
1529 }
1530 if (kind == CurveKind::BSpline) {
1531 return bsplinePointAt(parameter);
1532 }
1533 return pointAtAngle(startAngle + parameter * sweep());
1534 }
1535
1537 {
1538 if (kind == CurveKind::Line) {
1539 return lineStart;
1540 }
1541 if (kind == CurveKind::BSpline) {
1542 // a clamped knot vector interpolates its first pole exactly; anything else has to be evaluated
1543 return bsplineIsClamped() ? poles.front() : bsplinePointAt(0.);
1544 }
1545 return pointAtAngle(startAngle);
1546 }
1548 {
1549 if (kind == CurveKind::Line) {
1550 return lineEnd;
1551 }
1552 if (kind == CurveKind::BSpline) {
1553 return bsplineIsClamped() ? poles.back() : bsplinePointAt(1.);
1554 }
1555 return pointAtAngle(endAngle);
1556 }
1557
1560 {
1561 if (kind == CurveKind::Line) {
1563 }
1564 if (kind == CurveKind::BSpline) {
1565 const double span = bsplineT1() - bsplineT0();
1566 Vec2 point;
1568 bsplineEval(bsplineT0() + parameter * span, point, derivative);
1569 return {derivative.uCoord * span, derivative.vCoord * span};
1570 }
1571 const double angle = startAngle + parameter * sweep();
1572 return {-radius * std::sin(angle) * sweep(), radius * std::cos(angle) * sweep()};
1573 }
1574
1577 {
1578 if (kind == CurveKind::Line) {
1580 const double length = std::sqrt(delta.uCoord * delta.uCoord + delta.vCoord * delta.vCoord);
1581 if (length <= kTolerance) {
1582 return {0., 0.};
1583 }
1584 return {delta.uCoord / length, delta.vCoord / length};
1585 }
1586 if (kind == CurveKind::BSpline) {
1587 // dC/dt scaled by the positive constant dt/ds, so the normalized direction is unchanged
1588 const double knotValue = bsplineT0() + parameter * (bsplineT1() - bsplineT0());
1589 Vec2 point;
1591 bsplineEval(knotValue, point, derivative);
1592 const double length = std::sqrt(derivative.uCoord * derivative.uCoord + derivative.vCoord * derivative.vCoord);
1593 if (length <= kTolerance) {
1594 return {0., 0.};
1595 }
1596 return {derivative.uCoord / length, derivative.vCoord / length};
1597 }
1598 const double angle = startAngle + parameter * sweep();
1599 const double direction = sweep() >= 0. ? 1. : -1.;
1600 return {-direction * std::sin(angle), direction * std::cos(angle)};
1601 }
1602
1604 bool angleInSweep(double angle) const
1605 {
1606 const double totalSweep = sweep();
1607 const double magnitude = std::abs(totalSweep);
1608 if (magnitude >= kTwoPi - kTolerance) {
1609 return true; // full circle
1610 }
1611 double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle);
1612 delta -= kTwoPi * std::floor(delta / kTwoPi); // wrap into [0, 2pi)
1613 return delta <= magnitude + kTolerance;
1614 }
1615
1617 double angleParameter(double angle) const
1618 {
1619 const double totalSweep = sweep();
1620 if (std::abs(totalSweep) <= kTolerance) {
1621 return 0.;
1622 }
1623 double delta = (totalSweep >= 0.) ? (angle - startAngle) : (startAngle - angle);
1624 delta -= kTwoPi * std::floor(delta / kTwoPi);
1625 return std::max(0., std::min(1., delta / std::abs(totalSweep)));
1626 }
1627
1630 {
1631 auto include = [&](const Vec2& point) {
1632 lower.uCoord = std::min(lower.uCoord, point.uCoord);
1633 lower.vCoord = std::min(lower.vCoord, point.vCoord);
1634 upper.uCoord = std::max(upper.uCoord, point.uCoord);
1635 upper.vCoord = std::max(upper.vCoord, point.vCoord);
1636 };
1637 if (kind == CurveKind::BSpline) {
1638 // the control-point convex hull contains the curve, so its box is a conservative (exact
1639 // upper bound) parametric AABB — consistent with the BVH's conservative-box philosophy
1640 for (const auto& pole : poles) {
1641 include(pole);
1642 }
1643 return;
1644 }
1645 includeAnalyticExtremes(include);
1646 }
1647
1650 {
1651 auto include = [&](const Vec2& point) {
1652 lower.uCoord = std::min(lower.uCoord, point.uCoord);
1653 lower.vCoord = std::min(lower.vCoord, point.vCoord);
1654 upper.uCoord = std::max(upper.uCoord, point.uCoord);
1655 upper.vCoord = std::max(upper.vCoord, point.vCoord);
1656 };
1657 if (kind == CurveKind::BSpline) {
1658 for (const auto& sample : bsplineSamples()) {
1659 include(sample);
1660 }
1661 return;
1662 }
1663 includeAnalyticExtremes(include);
1664 }
1665
1667 template <typename Include>
1668 void includeAnalyticExtremes(const Include& include) const
1669 {
1670 include(startPoint());
1671 include(endPoint());
1672 if (kind == CurveKind::Arc) {
1673 // include the axis-extreme points (angles 0, pi/2, pi, 3pi/2) that fall within the sweep
1674 const double cardinalAngles[4] = {0., kHalfPi, kPi, 3. * kHalfPi};
1675 for (double cardinal : cardinalAngles) {
1676 if (angleInSweep(cardinal)) {
1677 include(pointAtAngle(cardinal));
1678 }
1679 }
1680 }
1681 }
1682
1684 Vec2 closestPoint(const Vec2& point, double& parameter) const
1685 {
1686 if (kind == CurveKind::BSpline) {
1687 // distance to the cached polyline, accurate to the sampling flatness
1688 const auto& polyline = bsplineSamples();
1689 if (polyline.size() < 2) {
1690 parameter = 0.;
1691 return startPoint();
1692 }
1693 const int segments = static_cast<int>(polyline.size()) - 1;
1694 double bestDistanceSq = std::numeric_limits<double>::infinity();
1695 Vec2 bestPoint = polyline.front();
1696 double bestParameter = 0.;
1697 for (int index = 0; index < segments; ++index) {
1698 const Vec2 segmentStart = polyline[index];
1699 const Vec2 segmentVector = polyline[index + 1] - segmentStart;
1700 const double segmentLengthSq =
1701 segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
1702 double projection = 0.;
1703 if (segmentLengthSq > kToleranceSq) {
1704 projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
1705 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
1706 segmentLengthSq;
1707 projection = std::max(0., std::min(1., projection));
1708 }
1709 const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord,
1710 segmentStart.vCoord + projection * segmentVector.vCoord};
1711 const double candidateDistanceSq = surface::distanceSq(point, candidate);
1712 if (candidateDistanceSq < bestDistanceSq) {
1713 bestDistanceSq = candidateDistanceSq;
1714 bestPoint = candidate;
1715 bestParameter = (index + projection) / segments;
1716 }
1717 }
1718 parameter = bestParameter;
1719 return bestPoint;
1720 }
1721 if (kind == CurveKind::Line) {
1723 const double lengthSq = segment.uCoord * segment.uCoord + segment.vCoord * segment.vCoord;
1724 if (lengthSq <= kToleranceSq) {
1725 parameter = 0.;
1726 return lineStart;
1727 }
1728 const double projection = ((point.uCoord - lineStart.uCoord) * segment.uCoord +
1729 (point.vCoord - lineStart.vCoord) * segment.vCoord) /
1730 lengthSq;
1731 parameter = std::max(0., std::min(1., projection));
1732 return {lineStart.uCoord + parameter * segment.uCoord, lineStart.vCoord + parameter * segment.vCoord};
1733 }
1734 // arc: project radially onto the circle, then clamp the angle to the sweep
1735 const double deltaU = point.uCoord - center.uCoord;
1736 const double deltaV = point.vCoord - center.vCoord;
1737 if (deltaU * deltaU + deltaV * deltaV <= kToleranceSq) {
1738 parameter = 0.; // point at the centre: every arc point is equidistant
1739 return startPoint();
1740 }
1741 const double angle = std::atan2(deltaV, deltaU);
1742 if (angleInSweep(angle)) {
1744 return pointAtAngle(angle);
1745 }
1746 const Vec2 startCandidate = startPoint();
1747 const Vec2 endCandidate = endPoint();
1748 if (surface::distanceSq(point, startCandidate) <= surface::distanceSq(point, endCandidate)) {
1749 parameter = 0.;
1750 return startCandidate;
1751 }
1752 parameter = 1.;
1753 return endCandidate;
1754 }
1755
1757 double distanceSq(const Vec2& point) const
1758 {
1759 double parameter = 0.;
1760 return surface::distanceSq(point, closestPoint(point, parameter));
1761 }
1762
1766 {
1767 if (kind == CurveKind::Line) {
1769 }
1770 if (kind == CurveKind::BSpline) {
1771 // Green's area per knot span by Gauss-Legendre: exact for a non-rational span, approximate for a rational one
1772 const int p = degree;
1773 const int order = bsplineRational() ? std::max(2 * p + 2, 8) : (p + 1);
1774 std::vector<double> nodes;
1775 std::vector<double> nodeWeights;
1776 gaussLegendre(order, nodes, nodeWeights);
1777 double area = 0.;
1778 const int lastSpan = static_cast<int>(poles.size()) - 1;
1779 for (int spanIndex = p; spanIndex <= lastSpan; ++spanIndex) {
1780 const double spanLow = knots[spanIndex];
1781 const double spanHigh = knots[spanIndex + 1];
1782 const double halfSpan = 0.5 * (spanHigh - spanLow);
1783 if (halfSpan <= kTolerance) {
1784 continue;
1785 }
1786 const double spanMid = 0.5 * (spanLow + spanHigh);
1787 for (int nodeIndex = 0; nodeIndex < order; ++nodeIndex) {
1788 const double knotValue = spanMid + halfSpan * nodes[nodeIndex];
1789 Vec2 point;
1791 bsplineEval(knotValue, point, derivative);
1792 area += 0.5 * (point.uCoord * derivative.vCoord - point.vCoord * derivative.uCoord) *
1793 nodeWeights[nodeIndex] * halfSpan;
1794 }
1795 }
1796 return area;
1797 }
1798 return 0.5 * (radius * center.uCoord * (std::sin(endAngle) - std::sin(startAngle)) -
1799 radius * center.vCoord * (std::cos(endAngle) - std::cos(startAngle)) +
1801 }
1802
1805
1808 bool bsplineBandOrCrossings(const Vec2& point, double bandSq, int& crossings) const
1809 {
1810 const auto& polyline = bsplineSamples();
1811 if (polyline.size() < 2) {
1812 return surface::distanceSq(point, startPoint()) <= bandSq;
1813 }
1814 int found = 0;
1815 for (size_t index = 0; index + 1 < polyline.size(); ++index) {
1816 const Vec2 segmentStart = polyline[index];
1817 const Vec2 segmentEnd = polyline[index + 1];
1818 const Vec2 segmentVector = segmentEnd - segmentStart;
1819 const double segmentLengthSq =
1820 segmentVector.uCoord * segmentVector.uCoord + segmentVector.vCoord * segmentVector.vCoord;
1821 double projection = 0.;
1822 if (segmentLengthSq > kToleranceSq) {
1823 projection = ((point.uCoord - segmentStart.uCoord) * segmentVector.uCoord +
1824 (point.vCoord - segmentStart.vCoord) * segmentVector.vCoord) /
1825 segmentLengthSq;
1826 projection = std::max(0., std::min(1., projection));
1827 }
1828 const Vec2 candidate{segmentStart.uCoord + projection * segmentVector.uCoord,
1829 segmentStart.vCoord + projection * segmentVector.vCoord};
1830 if (surface::distanceSq(point, candidate) <= bandSq) {
1831 return true;
1832 }
1833 const bool firstAbove = segmentStart.vCoord > point.vCoord;
1834 const bool secondAbove = segmentEnd.vCoord > point.vCoord;
1835 if (firstAbove != secondAbove) {
1836 const double intersectU =
1837 segmentStart.uCoord + (point.vCoord - segmentStart.vCoord) * (segmentEnd.uCoord - segmentStart.uCoord) /
1838 (segmentEnd.vCoord - segmentStart.vCoord);
1839 if (point.uCoord < intersectU) {
1840 ++found;
1841 }
1842 }
1843 }
1844 crossings += found;
1845 return false;
1846 }
1847
1849 int rightwardCrossings(const Vec2& point, const Vec2& canonicalStart, const Vec2& canonicalEnd) const
1850 {
1851 auto segmentCrossing = [&](const Vec2& first, const Vec2& second, double exactIntersectU) {
1852 const bool firstAbove = first.vCoord > point.vCoord;
1853 const bool secondAbove = second.vCoord > point.vCoord;
1854 if (firstAbove == secondAbove) {
1855 return false;
1856 }
1857 return point.uCoord < exactIntersectU;
1858 };
1859
1860 if (kind == CurveKind::Line) {
1861 const bool firstAbove = canonicalStart.vCoord > point.vCoord;
1862 const bool secondAbove = canonicalEnd.vCoord > point.vCoord;
1863 if (firstAbove == secondAbove) {
1864 return 0;
1865 }
1866 const double intersectU = canonicalStart.uCoord + (point.vCoord - canonicalStart.vCoord) *
1869 return (point.uCoord < intersectU) ? 1 : 0;
1870 }
1871
1872 if (kind == CurveKind::BSpline) {
1873 // the lines' half-open segment-crossing rule over the polyline, whose ends are the canonical seam vertices
1874 const auto& polyline = bsplineSamples();
1875 if (polyline.size() < 2) {
1876 return 0;
1877 }
1878 int crossings = 0;
1879 for (size_t index = 0; index + 1 < polyline.size(); ++index) {
1880 // No substitution here: the polyline already ends on the loop-canonical vertices (see
1881 // setCanonicalEndpoints), so this is the same boundary closestPoint measures against.
1882 const Vec2 first = polyline[index];
1883 const Vec2 second = polyline[index + 1];
1884 const bool firstAbove = first.vCoord > point.vCoord;
1885 const bool secondAbove = second.vCoord > point.vCoord;
1886 if (firstAbove == secondAbove) {
1887 continue;
1888 }
1889 const double intersectU =
1890 first.uCoord + (point.vCoord - first.vCoord) * (second.uCoord - first.uCoord) /
1891 (second.vCoord - first.vCoord);
1892 if (point.uCoord < intersectU) {
1893 ++crossings;
1894 }
1895 }
1896 return crossings;
1897 }
1898
1899 // split the arc into v-monotonic sub-arcs at its extreme angles, where the crossing u is exact
1900 const double totalSweep = sweep();
1901 if (std::abs(totalSweep) <= kTolerance || radius <= kTolerance) {
1902 return 0;
1903 }
1904 std::array<double, 8> breakParameters{};
1905 int breakCount = 0;
1906 breakParameters[breakCount++] = 0.;
1907 const double lowAngle = std::min(startAngle, endAngle);
1908 const double highAngle = std::max(startAngle, endAngle);
1909 const int firstK = static_cast<int>(std::floor((lowAngle - kHalfPi) / kPi)) - 1;
1910 const int lastK = static_cast<int>(std::ceil((highAngle - kHalfPi) / kPi)) + 1;
1911 for (int k = firstK; k <= lastK && breakCount < 7; ++k) {
1912 const double extremeAngle = kHalfPi + k * kPi;
1913 if (extremeAngle <= lowAngle + kTolerance || extremeAngle >= highAngle - kTolerance) {
1914 continue;
1915 }
1916 const double extremeParameter = (extremeAngle - startAngle) / totalSweep;
1917 if (extremeParameter > kTolerance && extremeParameter < 1. - kTolerance) {
1918 breakParameters[breakCount++] = extremeParameter;
1919 }
1920 }
1921 breakParameters[breakCount++] = 1.;
1922 std::sort(breakParameters.begin(), breakParameters.begin() + breakCount);
1923
1924 double ratio = (point.vCoord - center.vCoord) / radius;
1925 ratio = std::max(-1., std::min(1., ratio));
1926 const double cosMagnitude = std::sqrt(std::max(0., 1. - ratio * ratio));
1927
1928 int crossings = 0;
1929 for (int index = 0; index + 1 < breakCount; ++index) {
1930 const Vec2 subStart = (index == 0) ? canonicalStart : pointAt(breakParameters[index]);
1931 const Vec2 subEnd = (index + 2 == breakCount) ? canonicalEnd : pointAt(breakParameters[index + 1]);
1932 const double midAngle = startAngle + 0.5 * (breakParameters[index] + breakParameters[index + 1]) * totalSweep;
1933 const double cosSign = std::cos(midAngle) >= 0. ? 1. : -1.;
1934 const double intersectU = center.uCoord + cosSign * radius * cosMagnitude;
1935 if (segmentCrossing(subStart, subEnd, intersectU)) {
1936 ++crossings;
1937 }
1938 }
1939 return crossings;
1940 }
1941
1944 {
1946 std::swap(canonicalStart, canonicalEnd);
1947 bsplineCache.clear();
1948 }
1949 if (kind == CurveKind::Line) {
1950 std::swap(lineStart, lineEnd);
1951 } else if (kind == CurveKind::Arc) {
1952 std::swap(startAngle, endAngle);
1953 } else {
1954 // B-spline: reverse the poles/weights and complement the knot vector about its span so the
1955 // parametrization runs the other way (knots stay non-decreasing and clamped).
1956 std::reverse(poles.begin(), poles.end());
1957 if (!weights.empty()) {
1958 std::reverse(weights.begin(), weights.end());
1959 }
1960 const double knotSum = knots.front() + knots.back();
1961 std::vector<double> reversedKnots(knots.size());
1962 for (size_t index = 0; index < knots.size(); ++index) {
1963 reversedKnots[index] = knotSum - knots[knots.size() - 1 - index];
1964 }
1965 knots = std::move(reversedKnots);
1966 bsplineCache.clear(); // geometry order changed; recompute lazily
1967 }
1968 }
1969};
1970
1973 std::vector<Curve2D> curves;
1977
1979 std::vector<int> sourceCurve;
1980
1982 int storedIndexOfSource(int inputIndex) const
1983 {
1984 for (size_t index = 0; index < sourceCurve.size(); ++index) {
1985 if (sourceCurve[index] == inputIndex) {
1986 return static_cast<int>(index);
1987 }
1988 }
1989 return -1;
1990 }
1991
1993 bool initialize(const std::vector<Curve2D>& inputCurves, WireRole wireRole, WireStatus& status,
1994 const ParametricMetric& metric = {}, double joinTolerance = kWireJoinTolerance)
1995 {
1996 role = wireRole;
1997 curves = inputCurves;
1999 for (const auto& curve : curves) {
2000 mRepresentationTolerance = std::max(mRepresentationTolerance, curve.representationTolerance());
2001 }
2002 sourceCurve.resize(curves.size());
2003 for (size_t index = 0; index < curves.size(); ++index) {
2004 sourceCurve[index] = static_cast<int>(index);
2005 }
2006
2007 if (curves.empty()) {
2009 return false;
2010 }
2011 for (size_t index = 0; index < curves.size(); ++index) {
2012 if (!curves[index].valid()) {
2013 status = WireStatus::NonFinite;
2014 return false;
2015 }
2016 const Vec2 currentEnd = curves[index].endPoint();
2017 const Vec2 nextStart = curves[(index + 1) % curves.size()].startPoint();
2018 if (metric.distanceSq(currentEnd, nextStart) > joinTolerance * joinTolerance) {
2019 status = WireStatus::Open;
2020 return false;
2021 }
2022 }
2023
2024 // one vertex value per seam, given to both curves that meet there
2025 for (size_t index = 0; index < curves.size(); ++index) {
2026 curves[index].setCanonicalEndpoints(curves[index].startPoint(),
2027 curves[(index + 1) % curves.size()].startPoint());
2028 }
2029
2030 const double area = signedArea();
2031 if (std::abs(area) <= kAreaTolerance) {
2032 status = WireStatus::ZeroArea;
2033 return false;
2034 }
2035
2036 const bool wantPositiveArea = (role == WireRole::Outer);
2037 if ((area > 0.) != wantPositiveArea) {
2038 reverse();
2039 status = WireStatus::Reversed;
2041 return true;
2042 }
2043 status = WireStatus::Valid;
2045 return true;
2046 }
2047
2050 {
2051 for (const auto& curve : curves) {
2052 if (curve.kind == CurveKind::BSpline) {
2053 curve.bsplineSamples();
2054 }
2055 }
2056 }
2057
2060
2062 void reverse()
2063 {
2064 std::reverse(curves.begin(), curves.end());
2065 std::reverse(sourceCurve.begin(), sourceCurve.end());
2066 for (auto& curve : curves) {
2067 curve.reverseInPlace();
2068 }
2069 }
2070
2073 bool hasBSpline() const
2074 {
2075 for (const auto& curve : curves) {
2076 if (curve.kind == CurveKind::BSpline) {
2077 return true;
2078 }
2079 }
2080 return false;
2081 }
2082
2084 double signedArea() const
2085 {
2086 double area = 0.;
2087 for (const auto& curve : curves) {
2088 area += curve.signedAreaContribution();
2089 }
2090 return area;
2091 }
2092
2095 {
2096 for (const auto& curve : curves) {
2097 curve.extendBounds(lower, upper);
2098 }
2099 }
2100
2103 {
2104 for (const auto& curve : curves) {
2105 curve.extendTightBounds(lower, upper);
2106 }
2107 }
2108
2111 double boundaryBand(double lengthFloor) const { return std::max(lengthFloor, mRepresentationTolerance); }
2112
2114 WireClassification classify(const Vec2& point, double lengthFloor) const
2115 {
2116 const double band = boundaryBand(lengthFloor);
2117 const double bandSq = band * band;
2118 // Each curve's polyline already ends on the loop-canonical seam vertices, so the half-open
2119 // crossing convention stays consistent across seams without any substitution here.
2120 int crossings = 0;
2121 for (const auto& curve : curves) {
2122 if (curve.kind == CurveKind::BSpline) {
2123 if (curve.bsplineBandOrCrossings(point, bandSq, crossings)) {
2125 }
2126 } else if (curve.distanceSq(point) <= bandSq) {
2128 } else {
2129 crossings += curve.rightwardCrossings(point, curve.loopStart(), curve.loopEnd());
2130 }
2131 }
2132 return (crossings % 2 == 1) ? WireClassification::Inside : WireClassification::Outside;
2133 }
2134
2136 WireClassification classify(const Vec2& point, const ParametricMetric& metric = {}) const
2137 {
2138 return classify(point, trimLengthFloor(metric, point));
2139 }
2140
2143 std::vector<Vec2> sampledBoundary(int segmentsPerArc = kArcSamples) const
2144 {
2145 std::vector<Vec2> samples;
2146 if (curves.empty()) {
2147 return samples;
2148 }
2149 for (const auto& curve : curves) {
2150 if (curve.kind == CurveKind::Line) {
2151 samples.push_back(curve.startPoint());
2152 } else if (curve.kind == CurveKind::BSpline) {
2153 // adaptively flatten and append every sample except the closing one (the next curve's
2154 // start reproduces it)
2155 std::vector<Vec2> curveSamples;
2156 curve.bsplineSampleInto(curveSamples);
2157 for (size_t index = 0; index + 1 < curveSamples.size(); ++index) {
2158 samples.push_back(curveSamples[index]);
2159 }
2160 } else {
2161 // chords scale with the arc's sweep, so a rim shared with a quadric wall samples identical vertices
2162 const int arcSteps =
2163 std::max(1, static_cast<int>(std::lround(segmentsPerArc * std::abs(curve.sweep()) / kTwoPi)));
2164 for (int step = 0; step < arcSteps; ++step) {
2165 samples.push_back(curve.pointAt(static_cast<double>(step) / arcSteps));
2166 }
2167 }
2168 }
2169 samples.push_back(samples.front());
2170 return samples;
2171 }
2172};
2173
2176
2178inline double unwrapAngleInto(double angle, double uMin, double uMax)
2179{
2180 const double windowCenter = 0.5 * (uMin + uMax);
2181 return angle - kTwoPi * std::round((angle - windowCenter) / kTwoPi);
2182}
2183
2185inline bool curveTrimContains(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2186 const Vec2& point, bool* boundary = nullptr,
2187 const ParametricMetric& metric = {})
2188{
2189 if (boundary != nullptr) {
2190 *boundary = false;
2191 }
2192 const double lengthFloor = trimLengthFloor(metric, point);
2193 const auto outerClassification = outerWire.classify(point, lengthFloor);
2194 if (outerClassification == WireClassification::Outside) {
2195 return false;
2196 }
2197 if (outerClassification == WireClassification::Boundary) {
2198 if (boundary != nullptr) {
2199 *boundary = true;
2200 }
2201 return true;
2202 }
2203 for (const auto& innerWire : innerWires) {
2204 const auto innerClassification = innerWire.classify(point, lengthFloor);
2205 if (innerClassification == WireClassification::Boundary) {
2206 if (boundary != nullptr) {
2207 *boundary = true;
2208 }
2209 return true;
2210 }
2211 if (innerClassification == WireClassification::Inside) {
2212 return false;
2213 }
2214 }
2215 return true;
2216}
2217
2219inline constexpr int kContourQuadratureOrder = 20;
2220inline constexpr double kContourMaxSpanU = 0.25 * kPi;
2221
2224template <typename Antiderivative>
2225double contourIntegralAlongCurve(const Curve2D& curve, const Antiderivative& antiderivative, double from,
2226 double to)
2227{
2228 static thread_local std::vector<double> nodes;
2229 static thread_local std::vector<double> weights;
2230 if (static_cast<int>(nodes.size()) != kContourQuadratureOrder) {
2232 }
2233 // split at the interior knots, then into pieces whose u travel is at most kContourMaxSpanU
2234 static thread_local std::vector<double> breakpoints;
2235 breakpoints.clear();
2236 breakpoints.push_back(from);
2237 curve.appendInteriorKnots(std::min(from, to), std::max(from, to), breakpoints);
2238 std::sort(breakpoints.begin() + 1, breakpoints.end(),
2239 [forward = (to >= from)](double first, double second) { return forward ? first < second : first > second; });
2240 breakpoints.push_back(to);
2241
2242 double total = 0.;
2243 for (size_t segment = 0; segment + 1 < breakpoints.size(); ++segment) {
2244 const double segmentFrom = breakpoints[segment];
2245 const double segmentTo = breakpoints[segment + 1];
2246 if (segmentFrom == segmentTo) {
2247 continue;
2248 }
2249 const double travelU = curve.uVariation(std::min(segmentFrom, segmentTo), std::max(segmentFrom, segmentTo));
2250 const int pieces = std::max(1, static_cast<int>(std::ceil(travelU / kContourMaxSpanU)));
2251 for (int piece = 0; piece < pieces; ++piece) {
2252 const double low = segmentFrom + (segmentTo - segmentFrom) * piece / pieces;
2253 const double high = segmentFrom + (segmentTo - segmentFrom) * (piece + 1) / pieces;
2254 const double half = 0.5 * (high - low);
2255 const double mid = 0.5 * (high + low);
2256 for (int nodeIndex = 0; nodeIndex < kContourQuadratureOrder; ++nodeIndex) {
2257 const double parameter = mid + half * nodes[nodeIndex];
2258 const Vec2 point = curve.pointAt(parameter);
2259 const Vec2 derivative = curve.derivativeAt(parameter);
2260 total += weights[nodeIndex] * half * antiderivative(point.uCoord, point.vCoord) * derivative.vCoord;
2261 }
2262 }
2263 }
2264 return total;
2265}
2266
2268template <typename Antiderivative>
2269double integrateOverCurveTrimByParts(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2270 const Antiderivative& antiderivative)
2271{
2272 const auto loopIntegral = [&antiderivative](const CurveWire& wire) {
2273 double total = 0.;
2274 for (size_t index = 0; index < wire.curves.size(); ++index) {
2275 const auto& curve = wire.curves[index];
2276 total += contourIntegralAlongCurve(curve, antiderivative, 0., 1.);
2277 // seam bridge: a straight run from this curve's end to the next curve's start
2278 const Vec2 seamFrom = curve.endPoint();
2279 const Vec2 seamTo = wire.curves[(index + 1) % wire.curves.size()].startPoint();
2280 const double deltaV = seamTo.vCoord - seamFrom.vCoord;
2281 if (deltaV != 0.) {
2282 const Curve2D bridge = Curve2D::makeLine(seamFrom, seamTo);
2283 total += contourIntegralAlongCurve(bridge, antiderivative, 0., 1.);
2284 }
2285 }
2286 return total;
2287 };
2288
2289 double total = loopIntegral(outerWire);
2290 for (const auto& innerWire : innerWires) {
2291 total += loopIntegral(innerWire);
2292 }
2293 return total;
2294}
2295
2297template <typename Integrand>
2298double integrateOverCurveTrim(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2299 const Integrand& integrand, int samplesPerAxis = 128)
2300{
2301 Vec2 lower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2302 Vec2 upper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2303 outerWire.parametricBounds(lower, upper);
2304 if (!finite(lower) || !finite(upper) || samplesPerAxis < 1) {
2305 return 0.;
2306 }
2307 const double stepU = (upper.uCoord - lower.uCoord) / samplesPerAxis;
2308 const double stepV = (upper.vCoord - lower.vCoord) / samplesPerAxis;
2309 const double cellArea = stepU * stepV;
2310 double sum = 0.;
2311 for (int indexU = 0; indexU < samplesPerAxis; ++indexU) {
2312 const double uCoord = lower.uCoord + (indexU + 0.5) * stepU;
2313 for (int indexV = 0; indexV < samplesPerAxis; ++indexV) {
2314 const double vCoord = lower.vCoord + (indexV + 0.5) * stepV;
2315 if (curveTrimContains(outerWire, innerWires, {uCoord, vCoord})) {
2316 sum += integrand(uCoord, vCoord) * cellArea;
2317 }
2318 }
2319 }
2320 return sum;
2321}
2322
2324inline bool buildCurveTrim(const std::vector<Curve2D>& outerTrim,
2325 const std::vector<std::vector<Curve2D>>& innerTrims, CurveWire& outerWire,
2326 std::vector<CurveWire>& innerWires, Vec2& lower, Vec2& upper,
2327 std::string& errorMessage, const ParametricMetric& metric = {},
2328 double joinTolerance = kWireJoinTolerance)
2329{
2331 if (!outerWire.initialize(outerTrim, WireRole::Outer, status, metric, joinTolerance)) {
2332 errorMessage = std::string("quadric outer trim wire invalid: ") + wireStatusMessage(status);
2333 return false;
2334 }
2335 innerWires.clear();
2336 innerWires.reserve(innerTrims.size());
2337 for (const auto& innerLoop : innerTrims) {
2338 CurveWire innerWire;
2339 WireStatus innerStatus = WireStatus::Valid;
2340 if (!innerWire.initialize(innerLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) {
2341 errorMessage = std::string("quadric inner trim wire invalid: ") + wireStatusMessage(innerStatus);
2342 return false;
2343 }
2344 innerWires.push_back(std::move(innerWire));
2345 }
2346 lower = {std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2347 upper = {-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2348 outerWire.parametricBounds(lower, upper);
2349 if (!finite(lower) || !finite(upper)) {
2350 errorMessage = "quadric trim wire has non-finite parametric bounds";
2351 return false;
2352 }
2353 if (upper.uCoord - lower.uCoord > kTwoPi + kTolerance) {
2354 // the pole hull can overshoot the curve; re-measure on the curves before refusing
2355 Vec2 tightLower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2356 Vec2 tightUpper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2357 outerWire.tightParametricBounds(tightLower, tightUpper);
2358 if (!finite(tightLower) || !finite(tightUpper) || tightUpper.uCoord - tightLower.uCoord > kTwoPi + kTolerance) {
2359 errorMessage = "quadric trim wire spans more than a full turn in phi";
2360 return false;
2361 }
2362 // the wire is admissible; keep the tight box, since the conservative one is not a valid
2363 // parametric window for a periodic coordinate once it exceeds a full turn
2364 lower = tightLower;
2365 upper = tightUpper;
2366 }
2367 return true;
2368}
2369
2371inline std::vector<Vec2> sampleCurveWireByU(const CurveWire& wire, int segmentsPerTurn = kArcSamples)
2372{
2373 std::vector<Vec2> samples;
2374 for (const auto& curve : wire.curves) {
2375 if (curve.kind == CurveKind::BSpline) {
2376 // adaptively flatten in the parameter domain; append every sample except the closing one
2377 std::vector<Vec2> curveSamples;
2378 curve.bsplineSampleInto(curveSamples);
2379 for (size_t index = 0; index + 1 < curveSamples.size(); ++index) {
2380 samples.push_back(curveSamples[index]);
2381 }
2382 continue;
2383 }
2384 Vec2 lower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
2385 Vec2 upper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
2386 curve.extendBounds(lower, upper);
2387 const double uSpan = upper.uCoord - lower.uCoord;
2388 int steps = std::max(1, static_cast<int>(std::lround(segmentsPerTurn * uSpan / kTwoPi)));
2389 if (curve.kind == CurveKind::Arc) {
2390 steps = std::max(steps, static_cast<int>(std::lround(segmentsPerTurn * std::abs(curve.sweep()) / kTwoPi)));
2391 steps = std::max(steps, 1);
2392 }
2393 for (int step = 0; step < steps; ++step) {
2394 samples.push_back(curve.pointAt(static_cast<double>(step) / steps));
2395 }
2396 }
2397 return samples;
2398}
2399
2401inline std::vector<std::array<int, 3>> triangulateRingByShortestChord(const std::vector<Vec2>& uvPoints,
2402 const std::vector<Vec3>& points,
2403 std::vector<int> remainingIndices)
2404{
2405 const std::vector<int> ring = remainingIndices;
2406
2407 // the 3D diagonal of the ear at a position, or infinity when it is not an ear (same tests as triangulateRing)
2408 const double notAnEar = std::numeric_limits<double>::infinity();
2409 auto earChord = [&](size_t indexPosition) {
2410 const int previousIndex = remainingIndices[(indexPosition + remainingIndices.size() - 1) % remainingIndices.size()];
2411 const int currentIndex = remainingIndices[indexPosition];
2412 const int nextIndex = remainingIndices[(indexPosition + 1) % remainingIndices.size()];
2413 const auto& previousVertex = uvPoints[previousIndex];
2414 const auto& currentVertex = uvPoints[currentIndex];
2415 const auto& nextVertex = uvPoints[nextIndex];
2416 if (cross2D(currentVertex - previousVertex, nextVertex - currentVertex) <= kTolerance) {
2417 return notAnEar;
2418 }
2419 for (int candidateIndex : remainingIndices) {
2420 if (candidateIndex == previousIndex || candidateIndex == currentIndex || candidateIndex == nextIndex) {
2421 continue;
2422 }
2423 if (pointInTriangle(uvPoints[candidateIndex], previousVertex, currentVertex, nextVertex)) {
2424 return notAnEar;
2425 }
2426 }
2427 return norm(points[nextIndex] - points[previousIndex]);
2428 };
2429
2430 std::vector<double> chords(remainingIndices.size());
2431 for (size_t indexPosition = 0; indexPosition < remainingIndices.size(); ++indexPosition) {
2432 chords[indexPosition] = earChord(indexPosition);
2433 }
2434
2435 std::vector<std::array<int, 3>> triangles;
2436 while (remainingIndices.size() > 3) {
2437 // the shortest diagonal wins; ties go to the lowest position
2438 size_t bestPosition = remainingIndices.size();
2439 double bestChord = notAnEar;
2440 for (size_t indexPosition = 0; indexPosition < remainingIndices.size(); ++indexPosition) {
2441 if (chords[indexPosition] < bestChord) {
2442 bestChord = chords[indexPosition];
2443 bestPosition = indexPosition;
2444 }
2445 }
2446 if (bestPosition == remainingIndices.size()) {
2447 return triangulateRing(uvPoints, ring); // near-collinear runs can block every ear in this order
2448 }
2449
2450 const size_t count = remainingIndices.size();
2451 const int previousIndex = remainingIndices[(bestPosition + count - 1) % count];
2452 const int clippedIndex = remainingIndices[bestPosition];
2453 const int nextIndex = remainingIndices[(bestPosition + 1) % count];
2454 triangles.push_back({previousIndex, clippedIndex, nextIndex});
2455 remainingIndices.erase(remainingIndices.begin() + bestPosition);
2456 chords.erase(chords.begin() + bestPosition);
2457
2458 // the two neighbours change shape; any other non-ear can only change if the clipped vertex lay in its triangle
2459 const size_t newCount = remainingIndices.size();
2460 const size_t beforePosition = (bestPosition + newCount - 1) % newCount;
2461 const size_t afterPosition = bestPosition % newCount;
2462 for (size_t indexPosition = 0; indexPosition < newCount; ++indexPosition) {
2463 if (indexPosition == beforePosition || indexPosition == afterPosition) {
2464 chords[indexPosition] = earChord(indexPosition);
2465 } else if (chords[indexPosition] == notAnEar) {
2466 const auto& previousVertex = uvPoints[remainingIndices[(indexPosition + newCount - 1) % newCount]];
2467 const auto& currentVertex = uvPoints[remainingIndices[indexPosition]];
2468 const auto& nextVertex = uvPoints[remainingIndices[(indexPosition + 1) % newCount]];
2469 if (pointInTriangle(uvPoints[clippedIndex], previousVertex, currentVertex, nextVertex)) {
2470 chords[indexPosition] = earChord(indexPosition);
2471 }
2472 }
2473 }
2474 }
2475
2476 if (remainingIndices.size() == 3) {
2477 triangles.push_back({remainingIndices[0], remainingIndices[1], remainingIndices[2]});
2478 }
2479 return triangles;
2480}
2481
2483template <typename MapUV>
2484void appendCurveTrimMesh(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires, const MapUV& mapUV,
2485 std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles)
2486{
2487 const auto outerSamples = sampleCurveWireByU(outerWire);
2488 if (outerSamples.size() < 3) {
2489 return;
2490 }
2491 std::vector<std::vector<Vec2>> holes;
2492 for (const auto& innerWire : innerWires) {
2493 holes.push_back(sampleCurveWireByU(innerWire));
2494 }
2495 std::vector<Vec2> uvPoints;
2496 const auto ring = bridgeHoles(outerSamples, holes, uvPoints);
2497 const int firstVertexIndex = static_cast<int>(vertices.size());
2498 for (const auto& sample : uvPoints) {
2499 vertices.push_back(mapUV(sample.uCoord, sample.vCoord));
2500 }
2501 const std::vector<Vec3> points(vertices.begin() + firstVertexIndex, vertices.end());
2502 auto localTriangles = triangulateRingByShortestChord(uvPoints, points, ring);
2503 if (!coversRing(uvPoints, ring, localTriangles)) { // holes that cross the outer loop or each other stay filled
2504 localTriangles = triangulateRingByShortestChord(uvPoints, points, orientedRing(uvPoints, 0, static_cast<int>(outerSamples.size()), true));
2505 }
2506 for (const auto& triangle : localTriangles) {
2507 triangles.push_back(
2508 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
2509 }
2510}
2511
2513template <typename MapUV>
2514void appendCurveTrimEdges(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2515 const MapUV& mapUV, double orientationSign,
2516 std::vector<std::pair<Vec3, Vec3>>& edges)
2517{
2518 auto appendLoop = [&](const CurveWire& wire) {
2519 const auto samples = sampleCurveWireByU(wire);
2520 const size_t sampleCount = samples.size();
2521 for (size_t sampleIndex = 0; sampleIndex < sampleCount; ++sampleIndex) {
2522 const Vec2& current = samples[sampleIndex];
2523 const Vec2& next = samples[(sampleIndex + 1) % sampleCount];
2524 const Vec3 edgeStart = mapUV(current.uCoord, current.vCoord);
2525 const Vec3 edgeEnd = mapUV(next.uCoord, next.vCoord);
2526 if (orientationSign >= 0.) {
2527 edges.emplace_back(edgeStart, edgeEnd);
2528 } else {
2529 edges.emplace_back(edgeEnd, edgeStart);
2530 }
2531 }
2532 };
2533 appendLoop(outerWire);
2534 for (const auto& innerWire : innerWires) {
2535 appendLoop(innerWire);
2536 }
2537}
2538
2540inline constexpr int kSharedEdgeSamples = 33;
2541
2543template <typename MapUV>
2544bool sampleTrimCurveOfCurveWires(const CurveWire& outerWire, const std::vector<CurveWire>& innerWires,
2545 size_t index, const MapUV& mapUV, std::vector<Vec3>& samples)
2546{
2547 const CurveWire* wire = nullptr;
2548 size_t local = index;
2549 if (local < outerWire.curves.size()) {
2550 wire = &outerWire;
2551 } else {
2552 local -= outerWire.curves.size();
2553 for (const auto& innerWire : innerWires) {
2554 if (local < innerWire.curves.size()) {
2555 wire = &innerWire;
2556 break;
2557 }
2558 local -= innerWire.curves.size();
2559 }
2560 }
2561 if (wire == nullptr) {
2562 return false;
2563 }
2564 const int stored = wire->storedIndexOfSource(static_cast<int>(local));
2565 if (stored < 0) {
2566 return false;
2567 }
2568 const Curve2D& curve = wire->curves[static_cast<size_t>(stored)];
2569 samples.clear();
2570 samples.reserve(kSharedEdgeSamples);
2571 for (int step = 0; step < kSharedEdgeSamples; ++step) {
2572 const Vec2 uv = curve.pointAt(static_cast<double>(step) / (kSharedEdgeSamples - 1));
2573 samples.push_back(mapUV(uv.uCoord, uv.vCoord));
2574 }
2575 return true;
2576}
2577
2579template <typename MapUV>
2580bool sampleTrimCurveOfSurfaceWires(const SurfaceWire& outerWire, const std::vector<SurfaceWire>& innerWires,
2581 size_t index, const MapUV& mapUV, std::vector<Vec3>& samples)
2582{
2583 const SurfaceWire* wire = nullptr;
2584 size_t local = index;
2585 if (local < outerWire.vertices.size()) {
2586 wire = &outerWire;
2587 } else {
2588 local -= outerWire.vertices.size();
2589 for (const auto& innerWire : innerWires) {
2590 if (local < innerWire.vertices.size()) {
2591 wire = &innerWire;
2592 break;
2593 }
2594 local -= innerWire.vertices.size();
2595 }
2596 }
2597 if (wire == nullptr) {
2598 return false;
2599 }
2600 const int stored = wire->storedIndexOfSource(static_cast<int>(local));
2601 if (stored < 0) {
2602 return false;
2603 }
2604 const SurfaceEdge segment = wire->edge(stored);
2605 samples.clear();
2606 samples.push_back(mapUV(segment.start.uCoord, segment.start.vCoord));
2607 samples.push_back(mapUV(segment.end.uCoord, segment.end.vCoord));
2608 return true;
2609}
2611
2614 int surfaceIndex = -1;
2615 bool closed = false;
2616 std::vector<Vec3> points;
2617};
2618
2620inline void assembleRims(const std::vector<std::pair<Vec3, Vec3>>& edges, std::vector<SurfaceRim>& rims)
2621{
2622 if (edges.empty()) {
2623 return;
2624 }
2625 auto quantize = [](double value) { return static_cast<int64_t>(std::llround(value / kTolerance)); };
2626 using VertexKey = std::tuple<int64_t, int64_t, int64_t>;
2627 auto keyOf = [&](const Vec3& point) {
2628 return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)};
2629 };
2630
2631 // cancel reversed duplicate chords (a self-closing seam) before chaining, keyed by their shared midpoint
2632 std::vector<bool> consumed(edges.size(), false);
2633 std::map<VertexKey, std::vector<size_t>> edgesByMidpoint;
2634 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
2635 const Vec3 midpoint = (edges[edgeIndex].first + edges[edgeIndex].second) * 0.5;
2636 const auto [xKey, yKey, zKey] = keyOf(midpoint);
2637 bool cancelled = false;
2638 for (int64_t dx = -1; dx <= 1 && !cancelled; ++dx) {
2639 for (int64_t dy = -1; dy <= 1 && !cancelled; ++dy) {
2640 for (int64_t dz = -1; dz <= 1 && !cancelled; ++dz) {
2641 const auto found = edgesByMidpoint.find(VertexKey{xKey + dx, yKey + dy, zKey + dz});
2642 if (found == edgesByMidpoint.end()) {
2643 continue;
2644 }
2645 for (const size_t candidate : found->second) {
2646 if (consumed[candidate] ||
2647 distanceSq(edges[candidate].first, edges[edgeIndex].second) > kToleranceSq ||
2648 distanceSq(edges[candidate].second, edges[edgeIndex].first) > kToleranceSq) {
2649 continue;
2650 }
2651 consumed[candidate] = true;
2652 consumed[edgeIndex] = true;
2653 cancelled = true;
2654 break;
2655 }
2656 }
2657 }
2658 }
2659 if (!cancelled) {
2660 edgesByMidpoint[keyOf(midpoint)].push_back(edgeIndex);
2661 }
2662 }
2663
2664 std::map<VertexKey, std::vector<size_t>> edgesByStart;
2665 for (size_t edgeIndex = 0; edgeIndex < edges.size(); ++edgeIndex) {
2666 if (!consumed[edgeIndex]) {
2667 edgesByStart[keyOf(edges[edgeIndex].first)].push_back(edgeIndex);
2668 }
2669 }
2670
2671 // A vertex can land either side of a lattice boundary, so probe the 27 neighbouring cells and
2672 // accept the first unused chord whose start really is within kTolerance.
2673 auto findSuccessor = [&](const Vec3& point) -> long long {
2674 const auto [xKey, yKey, zKey] = keyOf(point);
2675 for (int64_t dx = -1; dx <= 1; ++dx) {
2676 for (int64_t dy = -1; dy <= 1; ++dy) {
2677 for (int64_t dz = -1; dz <= 1; ++dz) {
2678 const auto found = edgesByStart.find(VertexKey{xKey + dx, yKey + dy, zKey + dz});
2679 if (found == edgesByStart.end()) {
2680 continue;
2681 }
2682 for (const size_t candidate : found->second) {
2683 if (!consumed[candidate] && distanceSq(edges[candidate].first, point) <= kToleranceSq) {
2684 return static_cast<long long>(candidate);
2685 }
2686 }
2687 }
2688 }
2689 }
2690 return -1;
2691 };
2692
2693 for (size_t seed = 0; seed < edges.size(); ++seed) {
2694 if (consumed[seed]) {
2695 continue;
2696 }
2697 consumed[seed] = true;
2698 SurfaceRim rim;
2699 rim.points.push_back(edges[seed].first);
2700 rim.points.push_back(edges[seed].second);
2701 while (true) {
2702 if (distanceSq(rim.points.back(), rim.points.front()) <= kToleranceSq) {
2703 rim.closed = true;
2704 rim.points.pop_back(); // a closed rim does not repeat its first point
2705 break;
2706 }
2707 const long long next = findSuccessor(rim.points.back());
2708 if (next < 0) {
2709 break; // an open chain: the face's boundary is not a set of closed loops
2710 }
2711 consumed[static_cast<size_t>(next)] = true;
2712 rim.points.push_back(edges[static_cast<size_t>(next)].second);
2713 }
2714 if (rim.points.size() >= 2) {
2715 rims.push_back(std::move(rim));
2716 }
2717 }
2718}
2719
2722{
2723 public:
2724 virtual ~BoundedSurface() = default;
2725
2730 bool reversed = false;
2731 bool degenerate = false;
2733 bool anchored = false;
2734 };
2735
2736 void setBoundaryEdges(std::vector<BoundaryEdgeRef> refs) { mBoundaryEdges = std::move(refs); }
2737 const std::vector<BoundaryEdgeRef>& boundaryEdges() const { return mBoundaryEdges; }
2738
2740 virtual bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const
2741 {
2742 (void)index;
2743 (void)samples;
2744 return false;
2745 }
2747
2749 virtual void conservativeBounds(Vec3& lower, Vec3& upper) const = 0;
2750
2752 using CoverBox = std::pair<Vec3, Vec3>;
2753
2756 virtual void appendCoverBoxes(std::vector<CoverBox>& boxes) const
2757 {
2758 // conservativeBounds only accumulates, so the corners start beyond any geometry
2759 constexpr double kBig = std::numeric_limits<double>::max();
2760 CoverBox box{Vec3{kBig, kBig, kBig}, Vec3{-kBig, -kBig, -kBig}};
2761 conservativeBounds(box.first, box.second);
2762 boxes.push_back(box);
2763 }
2764
2766 virtual bool containsPointOnSurface(const Vec3& point) const = 0;
2767
2769 virtual void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
2770 double maxDistance, std::vector<RayHit>& hits) const = 0;
2771
2773 virtual double distanceSqToPatch(const Vec3& point) const = 0;
2774
2776 virtual Vec3 normalAt(const Vec3& point) const = 0;
2777
2779 virtual void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const = 0;
2780
2782 double parametricLengthSqAt(const Vec2& uv, const Vec2& delta) const
2783 {
2784 double gUU = 0.;
2785 double gUV = 0.;
2786 double gVV = 0.;
2787 parametricMetric(uv, gUU, gUV, gVV);
2788 return parametricLengthSq(gUU, gUV, gVV, delta);
2789 }
2790
2792 virtual double capacityContribution() const = 0;
2793
2795 virtual bool capacityIsExact() const = 0;
2796
2798 virtual void appendDisplayMesh(std::vector<Vec3>& vertices,
2799 std::vector<std::array<int, 3>>& triangles) const = 0;
2800
2802 virtual void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const = 0;
2803
2805 virtual void appendRims(std::vector<SurfaceRim>& rims) const
2806 {
2807 std::vector<std::pair<Vec3, Vec3>> edges;
2809 assembleRims(edges, rims);
2810 }
2811
2812 protected:
2813 std::vector<BoundaryEdgeRef> mBoundaryEdges;
2814};
2815
2819{
2820 public:
2821 bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV,
2822 const std::vector<Vec2>& outerWireVertices,
2823 const std::vector<std::vector<Vec2>>& innerWireVertices, std::string& errorMessage)
2824 {
2825 if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) {
2826 errorMessage = "surface frame contains a non-finite value";
2827 return false;
2828 }
2829
2830 mOrigin = surfaceOrigin;
2831 mAxisU = surfaceAxisU;
2832 mAxisV = surfaceAxisV;
2833 const Vec3 normalVector = cross(mAxisU, mAxisV);
2834 mAreaScale = norm(normalVector);
2835 if (mAreaScale <= kTolerance) {
2836 errorMessage = "surface frame axes are degenerate";
2837 return false;
2838 }
2839 mNormal = normalVector * (1. / mAreaScale);
2840
2841 mMetricUU = dot(mAxisU, mAxisU);
2842 mMetricUV = dot(mAxisU, mAxisV);
2843 mMetricVV = dot(mAxisV, mAxisV);
2844 const double metricDet = mMetricUU * mMetricVV - mMetricUV * mMetricUV;
2845 if (std::abs(metricDet) <= kToleranceSq) {
2846 errorMessage = "surface frame metric is singular";
2847 return false;
2848 }
2849 mInverseMetricDet = 1. / metricDet;
2850
2851 WireStatus outerStatus = WireStatus::Valid;
2852 const ParametricMetric metric = parametricMetricOf(*this);
2853 mTrimBand = trimLengthFloor(metric, Vec2{0., 0.});
2854 if (!mOuterWire.initialize(outerWireVertices, WireRole::Outer, outerStatus, metric)) {
2855 errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus);
2856 return false;
2857 }
2858 mOuterReoriented = (outerStatus == WireStatus::Reversed);
2859
2860 mInnerWires.clear();
2861 mInnerWires.reserve(innerWireVertices.size());
2862 mInnerReoriented = false;
2863 for (const auto& innerWireInput : innerWireVertices) {
2864 SurfaceWire innerWire;
2865 WireStatus innerStatus = WireStatus::Valid;
2866 if (!innerWire.initialize(innerWireInput, WireRole::Inner, innerStatus, metric)) {
2867 errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus);
2868 return false;
2869 }
2870 mInnerReoriented = mInnerReoriented || (innerStatus == WireStatus::Reversed);
2871 mInnerWires.emplace_back(std::move(innerWire));
2872 }
2873
2874 const auto ringOf = [this](const SurfaceWire& wire) {
2875 std::vector<Vec3> ring;
2876 ring.reserve(wire.vertices.size());
2877 for (const auto& vertex : wire.vertices) {
2878 ring.push_back(toGlobal(vertex));
2879 }
2880 return ring;
2881 };
2882 mOuterRing = ringOf(mOuterWire);
2883 mInnerRings.clear();
2884 for (const auto& innerWire : mInnerWires) {
2885 mInnerRings.push_back(ringOf(innerWire));
2886 }
2887 return true;
2888 }
2889
2891 bool wasReoriented() const { return mOuterReoriented || mInnerReoriented; }
2892
2893 Vec3 toGlobal(const Vec2& point) const
2894 {
2895 return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord;
2896 }
2897
2898 Vec2 toLocal(const Vec3& point) const
2899 {
2900 const Vec3 relativePoint = point - mOrigin;
2901 const double projectionU = dot(relativePoint, mAxisU);
2902 const double projectionV = dot(relativePoint, mAxisV);
2903 return {(projectionU * mMetricVV - projectionV * mMetricUV) * mInverseMetricDet,
2904 (projectionV * mMetricUU - projectionU * mMetricUV) * mInverseMetricDet};
2905 }
2906
2907 double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); }
2908
2909 bool containsLocal(const Vec2& point, bool* boundary = nullptr) const
2910 {
2911 if (boundary != nullptr) {
2912 *boundary = false;
2913 }
2914
2915 const auto outerClassification = mOuterWire.classify(point, mTrimBand);
2916 if (outerClassification == WireClassification::Outside) {
2917 return false;
2918 }
2919 if (outerClassification == WireClassification::Boundary) {
2920 if (boundary != nullptr) {
2921 *boundary = true;
2922 }
2923 return true;
2924 }
2925
2926 for (const auto& innerWire : mInnerWires) {
2927 const auto innerClassification = innerWire.classify(point, mTrimBand);
2928 if (innerClassification == WireClassification::Boundary) {
2929 if (boundary != nullptr) {
2930 *boundary = true;
2931 }
2932 return true;
2933 }
2934 if (innerClassification == WireClassification::Inside) {
2935 return false;
2936 }
2937 }
2938 return true;
2939 }
2940
2941 bool containsPointOnSurface(const Vec3& point) const override
2942 {
2943 if (std::abs(planeDistance(point)) > kTolerance) {
2944 return false;
2945 }
2946 return containsLocal(toLocal(point));
2947 }
2948
2949 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
2950 double maxDistance, std::vector<RayHit>& hits) const override
2951 {
2952 const double denominator = dot(mNormal, rayDirection);
2953 if (std::abs(denominator) <= kTolerance) {
2954 return;
2955 }
2956 const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator;
2957 if (candidateDistance < minDistance || candidateDistance > maxDistance) {
2958 return;
2959 }
2960 const Vec3 candidatePoint = rayOrigin + rayDirection * candidateDistance;
2961 bool onTrimBoundary = false;
2962 if (!containsLocal(toLocal(candidatePoint), &onTrimBoundary)) {
2963 return;
2964 }
2965 hits.push_back({candidateDistance, mNormal, onTrimBoundary});
2966 }
2967
2968 double distanceSqToEdges(const Vec3& point, const std::vector<Vec3>& ring) const
2969 {
2970 double bestDistanceSq = std::numeric_limits<double>::infinity();
2971 for (size_t vertexIndex = 0; vertexIndex < ring.size(); ++vertexIndex) {
2972 bestDistanceSq =
2973 std::min(bestDistanceSq, pointSegmentDistanceSq(point, ring[vertexIndex], ring[(vertexIndex + 1) % ring.size()]));
2974 }
2975 return bestDistanceSq;
2976 }
2977
2978 double distanceSqToPatch(const Vec3& point) const override
2979 {
2980 const Vec2 projectedPoint = toLocal(point);
2981 if (containsLocal(projectedPoint)) {
2982 const double signedPlaneDistance = planeDistance(point);
2983 return signedPlaneDistance * signedPlaneDistance;
2984 }
2985
2986 double bestDistanceSq = distanceSqToEdges(point, mOuterRing);
2987 for (const auto& innerRing : mInnerRings) {
2988 bestDistanceSq = std::min(bestDistanceSq, distanceSqToEdges(point, innerRing));
2989 }
2990 return bestDistanceSq;
2991 }
2992
2993 Vec3 normalAt(const Vec3&) const override { return mNormal; }
2994
2995 void conservativeBounds(Vec3& lower, Vec3& upper) const override
2996 {
2997 auto extendPoint = [&](const Vec2& surfacePoint) {
2998 const Vec3 globalPoint = toGlobal(surfacePoint);
2999 lower.xCoord = std::min(lower.xCoord, globalPoint.xCoord);
3000 lower.yCoord = std::min(lower.yCoord, globalPoint.yCoord);
3001 lower.zCoord = std::min(lower.zCoord, globalPoint.zCoord);
3002 upper.xCoord = std::max(upper.xCoord, globalPoint.xCoord);
3003 upper.yCoord = std::max(upper.yCoord, globalPoint.yCoord);
3004 upper.zCoord = std::max(upper.zCoord, globalPoint.zCoord);
3005 };
3006
3007 for (const auto& vertex : mOuterWire.vertices) {
3008 extendPoint(vertex);
3009 }
3010 for (const auto& innerWire : mInnerWires) {
3011 for (const auto& vertex : innerWire.vertices) {
3012 extendPoint(vertex);
3013 }
3014 }
3015 }
3016
3017 double area() const
3018 {
3019 double parametricArea = std::abs(mOuterWire.signedArea());
3020 for (const auto& innerWire : mInnerWires) {
3021 parametricArea -= std::abs(innerWire.signedArea());
3022 }
3023 return std::max(0., parametricArea) * mAreaScale;
3024 }
3025
3027 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
3028 {
3029 planeParametricMetric(mAxisU, mAxisV, gUU, gUV, gVV);
3030 }
3031
3032 double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; }
3033
3034 bool capacityIsExact() const override { return true; }
3035
3036 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3037 {
3038 std::vector<std::vector<Vec2>> holes;
3039 for (const auto& innerWire : mInnerWires) {
3040 holes.push_back(innerWire.vertices);
3041 }
3042 std::vector<Vec2> localVertices;
3043 const auto ring = bridgeHoles(mOuterWire.vertices, holes, localVertices);
3044 const int firstVertexIndex = static_cast<int>(vertices.size());
3045 for (const auto& vertex : localVertices) {
3046 vertices.push_back(toGlobal(vertex));
3047 }
3048
3049 auto localTriangles = triangulateRing(localVertices, ring);
3050 if (!coversRing(localVertices, ring, localTriangles)) { // holes that touch the outer wire or each other stay filled
3051 localTriangles = triangulateRing(localVertices, orientedRing(localVertices, 0, mOuterWire.edgeCount(), true));
3052 }
3053 for (const auto& triangle : localTriangles) {
3054 triangles.push_back(
3055 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
3056 }
3057 }
3058
3059 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3060 {
3061 auto appendWire = [&](const SurfaceWire& wire) {
3062 for (size_t vertexIndex = 0; vertexIndex < wire.vertices.size(); ++vertexIndex) {
3063 const Vec3 edgeStart = toGlobal(wire.vertices[vertexIndex]);
3064 const Vec3 edgeEnd = toGlobal(wire.vertices[(vertexIndex + 1) % wire.vertices.size()]);
3065 edges.emplace_back(edgeStart, edgeEnd);
3066 }
3067 };
3068 appendWire(mOuterWire);
3069 for (const auto& innerWire : mInnerWires) {
3070 appendWire(innerWire);
3071 }
3072 }
3073
3074 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3075 {
3077 mOuterWire, mInnerWires, index,
3078 [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples);
3079 }
3080
3081 private:
3082 Vec3 mOrigin;
3083 Vec3 mAxisU;
3084 Vec3 mAxisV;
3085 Vec3 mNormal;
3086 double mMetricUU = 0.;
3087 double mMetricUV = 0.;
3088 double mMetricVV = 0.;
3089 double mInverseMetricDet = 0.;
3090 double mAreaScale = 0.;
3091 bool mOuterReoriented = false;
3092 bool mInnerReoriented = false;
3093 double mTrimBand = 0.;
3094 SurfaceWire mOuterWire;
3095 std::vector<SurfaceWire> mInnerWires;
3096 std::vector<Vec3> mOuterRing;
3097 std::vector<std::vector<Vec3>> mInnerRings;
3098};
3099
3102{
3103 public:
3104 bool initialize(const Vec3& surfaceOrigin, const Vec3& surfaceAxisU, const Vec3& surfaceAxisV,
3105 const std::vector<Curve2D>& outerCurves,
3106 const std::vector<std::vector<Curve2D>>& innerCurves, std::string& errorMessage,
3107 double joinTolerance = kWireJoinTolerance)
3108 {
3109 if (!finite(surfaceOrigin) || !finite(surfaceAxisU) || !finite(surfaceAxisV)) {
3110 errorMessage = "surface frame contains a non-finite value";
3111 return false;
3112 }
3113 if (std::abs(norm(surfaceAxisU) - 1.) > kTolerance || std::abs(norm(surfaceAxisV) - 1.) > kTolerance ||
3114 std::abs(dot(surfaceAxisU, surfaceAxisV)) > kTolerance) {
3115 errorMessage = "curved planar surface requires orthonormal frame axes";
3116 return false;
3117 }
3118
3119 mOrigin = surfaceOrigin;
3120 mAxisU = surfaceAxisU;
3121 mAxisV = surfaceAxisV;
3122 mNormal = cross(mAxisU, mAxisV);
3123
3124 WireStatus outerStatus = WireStatus::Valid;
3125 const ParametricMetric metric = parametricMetricOf(*this);
3126 mTrimFloor = trimLengthFloor(metric, Vec2{0., 0.});
3127 if (!mOuterWire.initialize(outerCurves, WireRole::Outer, outerStatus, metric, joinTolerance)) {
3128 errorMessage = std::string("outer wire invalid: ") + wireStatusMessage(outerStatus);
3129 return false;
3130 }
3131 mReoriented = (outerStatus == WireStatus::Reversed);
3132
3133 mInnerWires.clear();
3134 mInnerWires.reserve(innerCurves.size());
3135 for (const auto& innerCurveLoop : innerCurves) {
3136 CurveWire innerWire;
3137 WireStatus innerStatus = WireStatus::Valid;
3138 if (!innerWire.initialize(innerCurveLoop, WireRole::Inner, innerStatus, metric, joinTolerance)) {
3139 errorMessage = std::string("inner wire invalid: ") + wireStatusMessage(innerStatus);
3140 return false;
3141 }
3142 mReoriented = mReoriented || (innerStatus == WireStatus::Reversed);
3143 mInnerWires.emplace_back(std::move(innerWire));
3144 }
3145
3146 // A B-spline boundary makes the area (hence the capacity contribution) a numeric quadrature,
3147 // so flag the capacity as inexact (matching the wire-trimmed-quadric policy).
3148 mCapacityExact = !mOuterWire.hasBSpline();
3149 for (const auto& innerWire : mInnerWires) {
3150 mCapacityExact = mCapacityExact && !innerWire.hasBSpline();
3151 }
3152 return true;
3153 }
3154
3156 bool wasReoriented() const { return mReoriented; }
3157
3158 Vec3 toGlobal(const Vec2& point) const { return mOrigin + mAxisU * point.uCoord + mAxisV * point.vCoord; }
3159
3160 Vec2 toLocal(const Vec3& point) const
3161 {
3162 const Vec3 relativePoint = point - mOrigin;
3163 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV)};
3164 }
3165
3166 double planeDistance(const Vec3& point) const { return dot(point - mOrigin, mNormal); }
3167
3168 bool containsLocal(const Vec2& point, bool* boundary = nullptr) const
3169 {
3170 if (boundary != nullptr) {
3171 *boundary = false;
3172 }
3173
3174 const auto outerClassification = mOuterWire.classify(point, mTrimFloor);
3175 if (outerClassification == WireClassification::Outside) {
3176 return false;
3177 }
3178 if (outerClassification == WireClassification::Boundary) {
3179 if (boundary != nullptr) {
3180 *boundary = true;
3181 }
3182 return true;
3183 }
3184
3185 for (const auto& innerWire : mInnerWires) {
3186 const auto innerClassification = innerWire.classify(point, mTrimFloor);
3187 if (innerClassification == WireClassification::Boundary) {
3188 if (boundary != nullptr) {
3189 *boundary = true;
3190 }
3191 return true;
3192 }
3193 if (innerClassification == WireClassification::Inside) {
3194 return false;
3195 }
3196 }
3197 return true;
3198 }
3199
3200 bool containsPointOnSurface(const Vec3& point) const override
3201 {
3202 if (std::abs(planeDistance(point)) > kTolerance) {
3203 return false;
3204 }
3205 return containsLocal(toLocal(point));
3206 }
3207
3208 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3209 double maxDistance, std::vector<RayHit>& hits) const override
3210 {
3211 const double denominator = dot(mNormal, rayDirection);
3212 if (std::abs(denominator) <= kTolerance) {
3213 return;
3214 }
3215 const double candidateDistance = dot(mOrigin - rayOrigin, mNormal) / denominator;
3216 if (candidateDistance < minDistance || candidateDistance > maxDistance) {
3217 return;
3218 }
3219 bool onTrimBoundary = false;
3220 if (!containsLocal(toLocal(rayOrigin + rayDirection * candidateDistance), &onTrimBoundary)) {
3221 return;
3222 }
3223 hits.push_back({candidateDistance, mNormal, onTrimBoundary});
3224 }
3225
3226 double distanceSqToPatch(const Vec3& point) const override
3227 {
3228 const Vec2 projectedPoint = toLocal(point);
3229 const double signedPlaneDistance = planeDistance(point);
3230 if (containsLocal(projectedPoint)) {
3231 return signedPlaneDistance * signedPlaneDistance;
3232 }
3233
3234 // exact for an orthonormal frame: split into in-plane distance to the trim curves plus the
3235 // out-of-plane plane distance
3236 double bestCurveDistanceSq = std::numeric_limits<double>::infinity();
3237 for (const auto& curve : mOuterWire.curves) {
3238 bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint));
3239 }
3240 for (const auto& innerWire : mInnerWires) {
3241 for (const auto& curve : innerWire.curves) {
3242 bestCurveDistanceSq = std::min(bestCurveDistanceSq, curve.distanceSq(projectedPoint));
3243 }
3244 }
3245 return bestCurveDistanceSq + signedPlaneDistance * signedPlaneDistance;
3246 }
3247
3248 Vec3 normalAt(const Vec3&) const override { return mNormal; }
3249
3250 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3251 {
3252 Vec2 parametricLower{std::numeric_limits<double>::infinity(), std::numeric_limits<double>::infinity()};
3253 Vec2 parametricUpper{-std::numeric_limits<double>::infinity(), -std::numeric_limits<double>::infinity()};
3254 mOuterWire.parametricBounds(parametricLower, parametricUpper);
3255
3256 // the affine image of the parametric AABB contains the patch; its corners bound the 3D AABB
3257 for (const double cornerU : {parametricLower.uCoord, parametricUpper.uCoord}) {
3258 for (const double cornerV : {parametricLower.vCoord, parametricUpper.vCoord}) {
3259 const Vec3 globalCorner = toGlobal({cornerU, cornerV});
3260 lower.xCoord = std::min(lower.xCoord, globalCorner.xCoord);
3261 lower.yCoord = std::min(lower.yCoord, globalCorner.yCoord);
3262 lower.zCoord = std::min(lower.zCoord, globalCorner.zCoord);
3263 upper.xCoord = std::max(upper.xCoord, globalCorner.xCoord);
3264 upper.yCoord = std::max(upper.yCoord, globalCorner.yCoord);
3265 upper.zCoord = std::max(upper.zCoord, globalCorner.zCoord);
3266 }
3267 }
3268 }
3269
3270 double area() const
3271 {
3272 double parametricArea = std::abs(mOuterWire.signedArea());
3273 for (const auto& innerWire : mInnerWires) {
3274 parametricArea -= std::abs(innerWire.signedArea());
3275 }
3276 return std::max(0., parametricArea);
3277 }
3278
3281 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
3282 {
3283 gUU = 1.;
3284 gUV = 0.;
3285 gVV = 1.;
3286 }
3287
3288 double capacityContribution() const override { return dot(mOrigin, mNormal) * area() / 3.; }
3289
3290 bool capacityIsExact() const override { return mCapacityExact; }
3291
3292 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3293 {
3294 // triangulate the sampled outer boundary with the sampled holes bridged in
3295 auto samples = mOuterWire.sampledBoundary();
3296 if (samples.size() < 4) {
3297 return;
3298 }
3299 samples.pop_back(); // drop the closing duplicate
3300
3301 const int outerCount = static_cast<int>(samples.size());
3302 std::vector<std::vector<Vec2>> holes;
3303 for (const auto& innerWire : mInnerWires) {
3304 holes.push_back(innerWire.sampledBoundary());
3305 if (!holes.back().empty()) {
3306 holes.back().pop_back();
3307 }
3308 }
3309 std::vector<Vec2> localVertices;
3310 const auto ring = bridgeHoles(samples, holes, localVertices);
3311
3312 const int firstVertexIndex = static_cast<int>(vertices.size());
3313 for (const auto& vertex : localVertices) {
3314 vertices.push_back(toGlobal(vertex));
3315 }
3316 auto localTriangles = triangulateRing(localVertices, ring);
3317 if (!coversRing(localVertices, ring, localTriangles)) { // holes that touch the outer wire or each other stay filled
3318 localTriangles = triangulateRing(localVertices, orientedRing(localVertices, 0, outerCount, true));
3319 }
3320 for (const auto& triangle : localTriangles) {
3321 triangles.push_back(
3322 {firstVertexIndex + triangle[0], firstVertexIndex + triangle[1], firstVertexIndex + triangle[2]});
3323 }
3324 }
3325
3326 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3327 {
3328 auto appendWire = [&](const CurveWire& wire) {
3329 const auto samples = wire.sampledBoundary();
3330 for (size_t sampleIndex = 0; sampleIndex + 1 < samples.size(); ++sampleIndex) {
3331 edges.emplace_back(toGlobal(samples[sampleIndex]), toGlobal(samples[sampleIndex + 1]));
3332 }
3333 };
3334 appendWire(mOuterWire);
3335 for (const auto& innerWire : mInnerWires) {
3336 appendWire(innerWire);
3337 }
3338 }
3339
3340 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3341 {
3343 mOuterWire, mInnerWires, index,
3344 [this](double u, double v) { return toGlobal(Vec2{u, v}); }, samples);
3345 }
3346
3347 private:
3348 Vec3 mOrigin;
3349 Vec3 mAxisU;
3350 Vec3 mAxisV;
3351 Vec3 mNormal;
3352 bool mReoriented = false;
3353 bool mCapacityExact = true;
3354 double mTrimFloor = 0.;
3355 CurveWire mOuterWire;
3356 std::vector<CurveWire> mInnerWires;
3357};
3358
3360inline void appendArcBandCoverBoxes(const Vec3& center, const Vec3& axisU, const Vec3& axisV, const Vec3& axisW,
3361 double phiStart, double phiSweep, double heightMin, double heightMax,
3362 double radiusAtMin, double radiusAtMax,
3363 std::vector<BoundedSurface::CoverBox>& boxes)
3364{
3365 const int chunks = coverChunkCount(phiSweep);
3366 for (int chunk = 0; chunk < chunks; ++chunk) {
3367 const double phiLow = phiStart + phiSweep * chunk / chunks;
3368 const double phiHigh = phiStart + phiSweep * (chunk + 1) / chunks;
3369 double lower[3];
3370 double upper[3];
3371 for (int dimension = 0; dimension < 3; ++dimension) {
3372 double radialLow = 0.;
3373 double radialHigh = 0.;
3374 sinusoidRange(component(axisU, dimension), component(axisV, dimension), phiLow, phiHigh, radialLow, radialHigh);
3375 const double centerAtMin = component(center, dimension) + heightMin * component(axisW, dimension);
3376 const double centerAtMax = component(center, dimension) + heightMax * component(axisW, dimension);
3377 lower[dimension] = std::min(centerAtMin + radiusAtMin * radialLow, centerAtMax + radiusAtMax * radialLow);
3378 upper[dimension] = std::max(centerAtMin + radiusAtMin * radialHigh, centerAtMax + radiusAtMax * radialHigh);
3379 }
3380 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
3381 }
3382}
3383
3386{
3387 public:
3388 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius,
3389 double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall,
3390 std::string& errorMessage)
3391 {
3392 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radius) ||
3393 !std::isfinite(heightMin) || !std::isfinite(heightMax) || !std::isfinite(phiStart) ||
3394 !std::isfinite(phiSweep)) {
3395 errorMessage = "cylindrical surface parameter is non-finite";
3396 return false;
3397 }
3398 if (radius <= kTolerance) {
3399 errorMessage = "cylindrical surface needs a positive radius";
3400 return false;
3401 }
3402 if (heightMax - heightMin <= kTolerance) {
3403 errorMessage = "cylindrical surface needs a positive height range";
3404 return false;
3405 }
3406 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
3407 errorMessage = "cylindrical surface needs an angular sweep in (0, 2pi]";
3408 return false;
3409 }
3410 if (!makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
3411 return false;
3412 }
3413
3414 mCenter = centerPoint;
3415 mRadius = radius;
3416 mPhiTolerance = angularTolerance(mRadius);
3417 mHeightMin = heightMin;
3418 mHeightMax = heightMax;
3419 mPhiStart = phiStart;
3420 mPhiSweep = std::min(phiSweep, kTwoPi);
3421 mNormalSign = innerWall ? -1. : 1.;
3422 return true;
3423 }
3424
3426 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radius,
3427 double heightMin, double heightMax, double phiStart, double phiSweep, bool innerWall,
3428 const std::vector<Curve2D>& outerTrim, const std::vector<std::vector<Curve2D>>& innerTrims,
3429 std::string& errorMessage, double joinTolerance = kWireJoinTolerance)
3430 {
3431 if (!initialize(centerPoint, axis, referenceAxisU, radius, heightMin, heightMax, phiStart, phiSweep, innerWall,
3432 errorMessage)) {
3433 return false;
3434 }
3435 Vec2 lower, upper;
3436 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
3437 parametricMetricOf(*this), joinTolerance)) {
3438 return false;
3439 }
3440 mPhiStart = lower.uCoord;
3441 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
3442 mHeightMin = lower.vCoord;
3443 mHeightMax = upper.vCoord;
3444 mHasWireTrim = true;
3445 return true;
3446 }
3447
3448 bool hasWireTrim() const { return mHasWireTrim; }
3449
3451 bool pointInTrim(double phi, double height, bool* boundary = nullptr) const
3452 {
3453 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
3454 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this));
3455 }
3456
3459 static bool makeFrame(const Vec3& axis, const Vec3& referenceAxisU, Vec3& axisU, Vec3& axisV, Vec3& axisW,
3460 std::string& errorMessage)
3461 {
3462 if (norm(axis) <= kTolerance) {
3463 errorMessage = "surface axis is degenerate";
3464 return false;
3465 }
3466 axisW = normalized(axis);
3467 const Vec3 projectedU = referenceAxisU - axisW * dot(referenceAxisU, axisW);
3468 if (norm(projectedU) <= kTolerance) {
3469 errorMessage = "surface reference axis is parallel to the main axis";
3470 return false;
3471 }
3472 axisU = normalized(projectedU);
3473 axisV = cross(axisW, axisU); // gives axisU x axisV = axisW
3474 return true;
3475 }
3476
3477 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
3478
3479 Vec3 toLocal(const Vec3& point) const
3480 {
3481 const Vec3 relativePoint = point - mCenter;
3482 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
3483 }
3484
3485 bool heightInRange(double height) const
3486 {
3487 return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance;
3488 }
3489
3490 bool phiInSweep(double phi) const
3491 {
3492 return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance);
3493 }
3494
3495 Vec3 pointAt(double phi, double height) const
3496 {
3497 return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * mRadius;
3498 }
3499
3500 bool containsPointOnSurface(const Vec3& point) const override
3501 {
3502 const Vec3 localPoint = toLocal(point);
3503 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3504 if (std::abs(radialDistance - mRadius) > kTolerance) {
3505 return false;
3506 }
3507 if (radialDistance <= kTolerance) {
3508 return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined on the axis
3509 }
3510 const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord);
3511 if (mHasWireTrim) {
3512 return pointInTrim(phi, localPoint.zCoord);
3513 }
3514 return heightInRange(localPoint.zCoord) && phiInSweep(phi);
3515 }
3516
3517 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3518 double maxDistance, std::vector<RayHit>& hits) const override
3519 {
3520 const Vec3 localOrigin = toLocal(rayOrigin);
3521 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
3522
3523 const double quadraticA = localDirection.xCoord * localDirection.xCoord +
3524 localDirection.yCoord * localDirection.yCoord;
3525 if (quadraticA <= kToleranceSq) {
3526 return; // ray parallel to the axis: no transversal crossing of the lateral surface
3527 }
3528 const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord +
3529 localOrigin.yCoord * localDirection.yCoord);
3530 const double quadraticC = localOrigin.xCoord * localOrigin.xCoord +
3531 localOrigin.yCoord * localOrigin.yCoord - mRadius * mRadius;
3532 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
3533 if (discriminant <= 0.) {
3534 return;
3535 }
3536 const double sqrtDiscriminant = std::sqrt(discriminant);
3537 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
3538 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
3539 if (sameIntersection(firstRoot, secondRoot)) {
3540 return; // tangential graze: report neither hit so crossing parity stays even
3541 }
3542
3543 for (const double candidate : {firstRoot, secondRoot}) {
3544 if (candidate < minDistance || candidate > maxDistance) {
3545 continue;
3546 }
3547 const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord;
3548 const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord;
3549 const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord;
3550 const double hitPhi = std::atan2(hitV, hitU);
3551 bool onTrimBoundary = false;
3552 if (mHasWireTrim) {
3553 if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) {
3554 continue;
3555 }
3556 } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) {
3557 continue;
3558 }
3559 const double radialDistance = std::hypot(hitU, hitV);
3560 const Vec3 hitNormal = (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance)) * mNormalSign;
3561 hits.push_back({candidate, hitNormal, onTrimBoundary});
3562 }
3563 }
3564
3566 double distanceSqToPatch(const Vec3& point) const override
3567 {
3568 const Vec3 localPoint = toLocal(point);
3569 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3570 if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) {
3571 return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord}, Vec2{mRadius, mHeightMin},
3572 Vec2{mRadius, mHeightMax});
3573 }
3574 const double distanceToStartSeam =
3575 pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax));
3576 const double endPhi = mPhiStart + mPhiSweep;
3577 const double distanceToEndSeam =
3578 pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
3579 return std::min(distanceToStartSeam, distanceToEndSeam);
3580 }
3581
3582 Vec3 normalAt(const Vec3& point) const override
3583 {
3584 const Vec3 localPoint = toLocal(point);
3585 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3586 if (radialDistance <= kTolerance) {
3587 return mAxisU * mNormalSign; // ill-defined on the axis; return a stable direction
3588 }
3589 return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance)) *
3590 mNormalSign;
3591 }
3592
3594 void parametricMetric(const Vec2&, double& gUU, double& gUV, double& gVV) const override
3595 {
3596 cylinderParametricMetric(mRadius, gUU, gUV, gVV);
3597 }
3598
3600 double capacityContribution() const override
3601 {
3602 if (mHasWireTrim) {
3603 const double centreU = dot(mCenter, mAxisU);
3604 const double centreV = dot(mCenter, mAxisV);
3605 const double factor = mNormalSign * mRadius / 3.;
3606 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double) {
3607 return factor * (centreU * std::sin(phi) - centreV * std::cos(phi) + mRadius * phi);
3608 });
3609 }
3610 const double endPhi = mPhiStart + mPhiSweep;
3611 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
3612 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
3613 const double height = mHeightMax - mHeightMin;
3614 return mNormalSign * mRadius * height * (phiFactor + mRadius * mPhiSweep) / 3.;
3615 }
3616
3617 bool capacityIsExact() const override { return !mHasWireTrim; }
3618
3619 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3620 {
3621 // conservative: the AABB of the two full rim circles (partial sweeps get a larger box)
3622 for (const double height : {mHeightMin, mHeightMax}) {
3623 const Vec3 rimCenter = mCenter + mAxisW * height;
3624 for (int dimension = 0; dimension < 3; ++dimension) {
3625 const double radialExtent = mRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension));
3626 const double centerValue = component(rimCenter, dimension);
3627 if (dimension == 0) {
3628 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
3629 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
3630 } else if (dimension == 1) {
3631 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
3632 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
3633 } else {
3634 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
3635 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
3636 }
3637 }
3638 }
3639 }
3640
3642 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
3643 {
3644 appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax, mRadius,
3645 mRadius, boxes);
3646 }
3647
3650 int rimSegments() const
3651 {
3652 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
3653 }
3654
3655 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
3656 {
3657 if (mHasWireTrim) {
3658 appendCurveTrimMesh(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles);
3659 return;
3660 }
3661 const int segments = rimSegments();
3662 const int firstVertexIndex = static_cast<int>(vertices.size());
3663 for (int step = 0; step <= segments; ++step) {
3664 const double phi = mPhiStart + mPhiSweep * step / segments;
3665 vertices.push_back(pointAt(phi, mHeightMin));
3666 vertices.push_back(pointAt(phi, mHeightMax));
3667 }
3668 for (int step = 0; step < segments; ++step) {
3669 const int base = firstVertexIndex + 2 * step;
3670 triangles.push_back({base, base + 2, base + 3});
3671 triangles.push_back({base, base + 3, base + 1});
3672 }
3673 }
3674
3675 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
3676 {
3677 if (mHasWireTrim) {
3678 // the (phi, h) -> 3D map is orientation-consistent with the outward normal, so a CCW trim
3679 // loop yields a CCW 3D loop for an outer wall; the sign is just mNormalSign
3680 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges);
3681 return;
3682 }
3683 // boundary counter-clockwise seen along the outward normal, so rims shared with caps cancel
3684 const int segments = rimSegments();
3685 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
3686 if (mNormalSign > 0.) {
3687 edges.emplace_back(edgeStart, edgeEnd);
3688 } else {
3689 edges.emplace_back(edgeEnd, edgeStart);
3690 }
3691 };
3692 for (int step = 0; step < segments; ++step) {
3693 const double phi = mPhiStart + mPhiSweep * step / segments;
3694 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments;
3695 emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin));
3696 emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax));
3697 }
3698 if (!fullSweep()) {
3699 const double endPhi = mPhiStart + mPhiSweep;
3700 emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
3701 emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin));
3702 }
3703 }
3704
3705 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
3706 {
3707 if (!mHasWireTrim) {
3708 return false; // a parametric-rectangle trim carries no per-edge curve to sample
3709 }
3710 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples);
3711 }
3712
3713 private:
3714 Vec3 mCenter;
3715 Vec3 mAxisU;
3716 Vec3 mAxisV;
3717 Vec3 mAxisW;
3718 double mRadius = 0.;
3719 double mHeightMin = 0.;
3720 double mHeightMax = 0.;
3721 double mPhiStart = 0.;
3722 double mPhiSweep = kTwoPi;
3723 double mPhiTolerance = 0.;
3724 double mNormalSign = 1.;
3725 bool mHasWireTrim = false;
3726 CurveWire mTrimOuter;
3727 std::vector<CurveWire> mTrimInner;
3728};
3729
3732{
3733 public:
3734 bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius,
3735 double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall,
3736 std::string& errorMessage)
3737 {
3738 if (!finite(center) || !finite(polarAxis) || !finite(referenceAxisU) || !std::isfinite(radius) ||
3739 !std::isfinite(thetaMin) || !std::isfinite(thetaMax) || !std::isfinite(phiStart) ||
3740 !std::isfinite(phiSweep)) {
3741 errorMessage = "spherical surface parameter is non-finite";
3742 return false;
3743 }
3744 if (radius <= kTolerance) {
3745 errorMessage = "spherical surface needs a positive radius";
3746 return false;
3747 }
3748 if (thetaMin < -kTolerance || thetaMax > kPi + kTolerance || thetaMax - thetaMin <= kTolerance) {
3749 errorMessage = "spherical surface needs a polar range within [0, pi]";
3750 return false;
3751 }
3752 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
3753 errorMessage = "spherical surface needs an angular sweep in (0, 2pi]";
3754 return false;
3755 }
3756 if (!CylindricalBoundedSurface::makeFrame(polarAxis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
3757 return false;
3758 }
3759
3760 mCenter = center;
3761 mRadius = radius;
3762 mThetaMin = std::max(0., thetaMin);
3763 mThetaMax = std::min(kPi, thetaMax);
3764 mPhiStart = phiStart;
3765 mPhiSweep = std::min(phiSweep, kTwoPi);
3766 mNormalSign = innerWall ? -1. : 1.;
3767 return true;
3768 }
3769
3771 bool initialize(const Vec3& center, const Vec3& polarAxis, const Vec3& referenceAxisU, double radius,
3772 double thetaMin, double thetaMax, double phiStart, double phiSweep, bool innerWall,
3773 const std::vector<Curve2D>& outerTrim, const std::vector<std::vector<Curve2D>>& innerTrims,
3774 std::string& errorMessage, double joinTolerance = kWireJoinTolerance)
3775 {
3776 if (!initialize(center, polarAxis, referenceAxisU, radius, thetaMin, thetaMax, phiStart, phiSweep, innerWall,
3777 errorMessage)) {
3778 return false;
3779 }
3780 Vec2 lower, upper;
3781 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
3782 parametricMetricOf(*this), joinTolerance)) {
3783 return false;
3784 }
3785 mPhiStart = lower.uCoord;
3786 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
3787 mThetaMin = std::max(0., lower.vCoord);
3788 mThetaMax = std::min(kPi, upper.vCoord);
3789 mHasWireTrim = true;
3790 return true;
3791 }
3792
3793 bool hasWireTrim() const { return mHasWireTrim; }
3794
3796 bool pointInTrim(double phi, double theta, bool* boundary = nullptr) const
3797 {
3798 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
3799 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, theta}, boundary, parametricMetricOf(*this));
3800 }
3801
3802 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
3803
3804 Vec3 toLocal(const Vec3& point) const
3805 {
3806 const Vec3 relativePoint = point - mCenter;
3807 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
3808 }
3809
3810 bool directionInTrim(const Vec3& localPoint, bool* boundary = nullptr) const
3811 {
3812 if (boundary != nullptr) {
3813 *boundary = false;
3814 }
3815 const double pointRadius = norm(localPoint);
3816 if (pointRadius <= kTolerance) {
3817 return true; // the center is angle-degenerate; every patch point is equidistant
3818 }
3819 const double thetaTolerance = angularTolerance(mRadius);
3820 const double theta = std::acos(std::max(-1., std::min(1., localPoint.zCoord / pointRadius)));
3821 const double transverseDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
3822 if (mHasWireTrim) {
3823 if (transverseDistance <= kTolerance) {
3824 // on the polar axis phi is degenerate; accept by the wire's theta (v) range
3825 return theta >= mThetaMin - thetaTolerance && theta <= mThetaMax + thetaTolerance;
3826 }
3827 return pointInTrim(std::atan2(localPoint.yCoord, localPoint.xCoord), theta, boundary);
3828 }
3829 if (theta < mThetaMin - thetaTolerance || theta > mThetaMax + thetaTolerance) {
3830 return false;
3831 }
3832 if (transverseDistance <= kTolerance) {
3833 return true; // on the polar axis phi is degenerate
3834 }
3835 return angleInSweepRange(std::atan2(localPoint.yCoord, localPoint.xCoord), mPhiStart, mPhiSweep,
3836 thetaTolerance);
3837 }
3838
3839 Vec3 pointAt(double theta, double phi) const
3840 {
3841 const double sinTheta = std::sin(theta);
3842 return mCenter + (mAxisU * (sinTheta * std::cos(phi)) + mAxisV * (sinTheta * std::sin(phi)) +
3843 mAxisW * std::cos(theta)) *
3844 mRadius;
3845 }
3846
3847 bool containsPointOnSurface(const Vec3& point) const override
3848 {
3849 const Vec3 localPoint = toLocal(point);
3850 if (std::abs(norm(localPoint) - mRadius) > kTolerance) {
3851 return false;
3852 }
3853 return directionInTrim(localPoint);
3854 }
3855
3856 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
3857 double maxDistance, std::vector<RayHit>& hits) const override
3858 {
3859 const Vec3 relativeOrigin = rayOrigin - mCenter;
3860 const double quadraticA = normSq(rayDirection);
3861 if (quadraticA <= kToleranceSq) {
3862 return;
3863 }
3864 const double quadraticB = 2. * dot(relativeOrigin, rayDirection);
3865 const double quadraticC = normSq(relativeOrigin) - mRadius * mRadius;
3866 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
3867 if (discriminant <= 0.) {
3868 return;
3869 }
3870 const double sqrtDiscriminant = std::sqrt(discriminant);
3871 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
3872 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
3873 if (sameIntersection(firstRoot, secondRoot)) {
3874 return; // tangential graze
3875 }
3876
3877 for (const double candidate : {firstRoot, secondRoot}) {
3878 if (candidate < minDistance || candidate > maxDistance) {
3879 continue;
3880 }
3881 const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate);
3882 bool onTrimBoundary = false;
3883 if (!directionInTrim(localHit, &onTrimBoundary)) {
3884 continue;
3885 }
3886 hits.push_back({candidate,
3887 (mAxisU * localHit.xCoord + mAxisV * localHit.yCoord + mAxisW * localHit.zCoord) *
3888 (mNormalSign / mRadius),
3889 onTrimBoundary});
3890 }
3891 }
3892
3894 double distanceSqToPatch(const Vec3& point) const override
3895 {
3896 const Vec3 localPoint = toLocal(point);
3897 const double radialOffset = norm(localPoint) - mRadius;
3898 return radialOffset * radialOffset;
3899 }
3900
3901 Vec3 normalAt(const Vec3& point) const override
3902 {
3903 const Vec3 localPoint = toLocal(point);
3904 const double pointRadius = norm(localPoint);
3905 if (pointRadius <= kTolerance) {
3906 return mAxisW * mNormalSign; // ill-defined at the center; return a stable direction
3907 }
3908 return (mAxisU * localPoint.xCoord + mAxisV * localPoint.yCoord + mAxisW * localPoint.zCoord) *
3909 (mNormalSign / pointRadius);
3910 }
3911
3913 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
3914 {
3915 sphereParametricMetric(mRadius, uv.vCoord, gUU, gUV, gVV);
3916 }
3917
3919 double capacityContribution() const override
3920 {
3921 if (mHasWireTrim) {
3922 const double centreU = dot(mCenter, mAxisU);
3923 const double centreV = dot(mCenter, mAxisV);
3924 const double centreW = dot(mCenter, mAxisW);
3925 const double factor = mNormalSign * mRadius * mRadius / 3.;
3926 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double theta) {
3927 const double sinTheta = std::sin(theta);
3928 return factor * sinTheta *
3929 (sinTheta * (centreU * std::sin(phi) - centreV * std::cos(phi)) +
3930 (centreW * std::cos(theta) + mRadius) * phi);
3931 });
3932 }
3933 const double endPhi = mPhiStart + mPhiSweep;
3934 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
3935 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
3936 const double thetaIntegralSinSq =
3937 0.5 * ((mThetaMax - std::sin(mThetaMax) * std::cos(mThetaMax)) -
3938 (mThetaMin - std::sin(mThetaMin) * std::cos(mThetaMin)));
3939 const double thetaIntegralSinCos =
3940 0.5 * (std::sin(mThetaMax) * std::sin(mThetaMax) - std::sin(mThetaMin) * std::sin(mThetaMin));
3941 const double thetaIntegralSin = std::cos(mThetaMin) - std::cos(mThetaMax);
3942 return mNormalSign * mRadius * mRadius *
3943 (phiFactor * thetaIntegralSinSq + dot(mCenter, mAxisW) * mPhiSweep * thetaIntegralSinCos +
3944 mRadius * mPhiSweep * thetaIntegralSin) /
3945 3.;
3946 }
3947
3948 bool capacityIsExact() const override { return !mHasWireTrim; }
3949
3950 void conservativeBounds(Vec3& lower, Vec3& upper) const override
3951 {
3952 lower.xCoord = std::min(lower.xCoord, mCenter.xCoord - mRadius);
3953 lower.yCoord = std::min(lower.yCoord, mCenter.yCoord - mRadius);
3954 lower.zCoord = std::min(lower.zCoord, mCenter.zCoord - mRadius);
3955 upper.xCoord = std::max(upper.xCoord, mCenter.xCoord + mRadius);
3956 upper.yCoord = std::max(upper.yCoord, mCenter.yCoord + mRadius);
3957 upper.zCoord = std::max(upper.zCoord, mCenter.zCoord + mRadius);
3958 }
3959
3961 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
3962 {
3963 const int thetaChunks = coverChunkCount(kPi);
3964 const int phiChunks = coverChunkCount(kTwoPi);
3965 for (int thetaChunk = 0; thetaChunk < thetaChunks; ++thetaChunk) {
3966 const double thetaLow = kPi * thetaChunk / thetaChunks;
3967 const double thetaHigh = kPi * (thetaChunk + 1) / thetaChunks;
3968 for (int phiChunk = 0; phiChunk < phiChunks; ++phiChunk) {
3969 const double phiLow = kTwoPi * phiChunk / phiChunks;
3970 const double phiHigh = kTwoPi * (phiChunk + 1) / phiChunks;
3971 double lower[3];
3972 double upper[3];
3973 for (int dimension = 0; dimension < 3; ++dimension) {
3974 double inPlaneLow = 0.;
3975 double inPlaneHigh = 0.;
3976 sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), phiLow, phiHigh, inPlaneLow,
3977 inPlaneHigh);
3978 // sin(theta) >= 0 on [0, pi], so the chunk extremes are the theta sinusoid at s's own extremes
3979 const double axisComponent = component(mAxisW, dimension);
3980 const double high = sinusoidMaximum(axisComponent, inPlaneHigh, thetaLow, thetaHigh);
3981 const double low = sinusoidMinimum(axisComponent, inPlaneLow, thetaLow, thetaHigh);
3982 lower[dimension] = component(mCenter, dimension) + mRadius * low;
3983 upper[dimension] = component(mCenter, dimension) + mRadius * high;
3984 }
3985 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
3986 }
3987 }
3988 }
3989
3990 int phiSegments() const
3991 {
3992 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
3993 }
3994
3995 int thetaSegments() const
3996 {
3997 return std::max(1, static_cast<int>(std::lround(kArcSamples * (mThetaMax - mThetaMin) / kTwoPi)));
3998 }
3999
4000 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
4001 {
4002 if (mHasWireTrim) {
4003 appendCurveTrimMesh(mTrimOuter, mTrimInner, [this](double phi, double theta) { return pointAt(theta, phi); }, vertices, triangles);
4004 return;
4005 }
4006 const int phiSteps = phiSegments();
4007 const int thetaSteps = thetaSegments();
4008 const int firstVertexIndex = static_cast<int>(vertices.size());
4009 for (int thetaStep = 0; thetaStep <= thetaSteps; ++thetaStep) {
4010 const double theta = mThetaMin + (mThetaMax - mThetaMin) * thetaStep / thetaSteps;
4011 for (int phiStep = 0; phiStep <= phiSteps; ++phiStep) {
4012 vertices.push_back(pointAt(theta, mPhiStart + mPhiSweep * phiStep / phiSteps));
4013 }
4014 }
4015 const int rowLength = phiSteps + 1;
4016 for (int thetaStep = 0; thetaStep < thetaSteps; ++thetaStep) {
4017 for (int phiStep = 0; phiStep < phiSteps; ++phiStep) {
4018 const int base = firstVertexIndex + thetaStep * rowLength + phiStep;
4019 triangles.push_back({base, base + 1, base + rowLength + 1});
4020 triangles.push_back({base, base + rowLength + 1, base + rowLength});
4021 }
4022 }
4023 }
4024
4025 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
4026 {
4027 if (mHasWireTrim) {
4028 // the (phi, theta) -> 3D map is orientation-*reversed* relative to the outward normal
4029 // (X_phi x X_theta points inward), so the sign is -mNormalSign
4030 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double theta) { return pointAt(theta, phi); }, -mNormalSign, edges);
4031 return;
4032 }
4033 // boundary of the (theta, phi) rectangle, traversed counter-clockwise for an outer wall;
4034 // pole rims are degenerate points and full-sweep phi seams cancel, so both are skipped
4035 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
4036 if (mNormalSign > 0.) {
4037 edges.emplace_back(edgeStart, edgeEnd);
4038 } else {
4039 edges.emplace_back(edgeEnd, edgeStart);
4040 }
4041 };
4042 const double thetaTolerance = angularTolerance(mRadius);
4043 const int phiSteps = phiSegments();
4044 const double endPhi = mPhiStart + mPhiSweep;
4045 if (mThetaMin > thetaTolerance) {
4046 for (int step = 0; step < phiSteps; ++step) {
4047 const double phi = mPhiStart + mPhiSweep * step / phiSteps;
4048 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps;
4049 emitEdge(pointAt(mThetaMin, nextPhi), pointAt(mThetaMin, phi)); // -phi at the small-theta rim
4050 }
4051 }
4052 if (mThetaMax < kPi - thetaTolerance) {
4053 for (int step = 0; step < phiSteps; ++step) {
4054 const double phi = mPhiStart + mPhiSweep * step / phiSteps;
4055 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / phiSteps;
4056 emitEdge(pointAt(mThetaMax, phi), pointAt(mThetaMax, nextPhi)); // +phi at the large-theta rim
4057 }
4058 }
4059 if (!fullSweep()) {
4060 const int thetaSteps = thetaSegments();
4061 for (int step = 0; step < thetaSteps; ++step) {
4062 const double theta = mThetaMin + (mThetaMax - mThetaMin) * step / thetaSteps;
4063 const double nextTheta = mThetaMin + (mThetaMax - mThetaMin) * (step + 1) / thetaSteps;
4064 emitEdge(pointAt(theta, mPhiStart), pointAt(nextTheta, mPhiStart)); // +theta at phiStart
4065 emitEdge(pointAt(nextTheta, endPhi), pointAt(theta, endPhi)); // -theta at phiEnd
4066 }
4067 }
4068 }
4069
4070 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
4071 {
4072 if (!mHasWireTrim) {
4073 return false; // a parametric-rectangle trim carries no per-edge curve to sample
4074 }
4075 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double theta) { return pointAt(theta, phi); }, samples);
4076 }
4077
4078 private:
4079 Vec3 mCenter;
4080 Vec3 mAxisU;
4081 Vec3 mAxisV;
4082 Vec3 mAxisW;
4083 double mRadius = 0.;
4084 double mThetaMin = 0.;
4085 double mThetaMax = kPi;
4086 double mPhiStart = 0.;
4087 double mPhiSweep = kTwoPi;
4088 double mNormalSign = 1.;
4089 bool mHasWireTrim = false;
4090 CurveWire mTrimOuter;
4091 std::vector<CurveWire> mTrimInner;
4092};
4093
4096{
4097 public:
4098 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin,
4099 double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep,
4100 bool innerWall, std::string& errorMessage)
4101 {
4102 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(radiusAtMin) ||
4103 !std::isfinite(radiusAtMax) || !std::isfinite(heightMin) || !std::isfinite(heightMax) ||
4104 !std::isfinite(phiStart) || !std::isfinite(phiSweep)) {
4105 errorMessage = "conical surface parameter is non-finite";
4106 return false;
4107 }
4108 if (radiusAtMin < -kTolerance || radiusAtMax < -kTolerance ||
4109 std::max(radiusAtMin, radiusAtMax) <= kTolerance) {
4110 errorMessage = "conical surface needs non-negative radii, at least one positive";
4111 return false;
4112 }
4113 if (heightMax - heightMin <= kTolerance) {
4114 errorMessage = "conical surface needs a positive height range";
4115 return false;
4116 }
4117 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
4118 errorMessage = "conical surface needs an angular sweep in (0, 2pi]";
4119 return false;
4120 }
4121 if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
4122 return false;
4123 }
4124
4125 mCenter = centerPoint;
4126 mHeightMin = heightMin;
4127 mHeightMax = heightMax;
4128 mSlope = (radiusAtMax - radiusAtMin) / (heightMax - heightMin);
4129 mRadius0 = radiusAtMin - mSlope * heightMin; // radius at h = 0 of the linear law
4130 mPhiStart = phiStart;
4131 mPhiSweep = std::min(phiSweep, kTwoPi);
4132 mNormalSign = innerWall ? -1. : 1.;
4133 mPhiTolerance = angularTolerance(meanRadius());
4134 return true;
4135 }
4136
4138 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double radiusAtMin,
4139 double radiusAtMax, double heightMin, double heightMax, double phiStart, double phiSweep,
4140 bool innerWall, const std::vector<Curve2D>& outerTrim,
4141 const std::vector<std::vector<Curve2D>>& innerTrims, std::string& errorMessage,
4142 double joinTolerance = kWireJoinTolerance)
4143 {
4144 if (!initialize(centerPoint, axis, referenceAxisU, radiusAtMin, radiusAtMax, heightMin, heightMax, phiStart,
4145 phiSweep, innerWall, errorMessage)) {
4146 return false;
4147 }
4148 Vec2 lower, upper;
4149 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
4150 parametricMetricOf(*this), joinTolerance)) {
4151 return false;
4152 }
4153 mPhiStart = lower.uCoord;
4154 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
4155 mHeightMin = lower.vCoord;
4156 mHeightMax = upper.vCoord;
4157 mPhiTolerance = angularTolerance(meanRadius());
4158 mHasWireTrim = true;
4159 return true;
4160 }
4161
4162 bool hasWireTrim() const { return mHasWireTrim; }
4163
4165 bool pointInTrim(double phi, double height, bool* boundary = nullptr) const
4166 {
4167 const double uCoord = unwrapAngleInto(phi, mPhiStart, mPhiStart + mPhiSweep);
4168 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, height}, boundary, parametricMetricOf(*this));
4169 }
4170
4171 bool fullSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
4172
4173 double radiusAt(double height) const { return mRadius0 + mSlope * height; }
4174
4175 double meanRadius() const { return 0.5 * (radiusAt(mHeightMin) + radiusAt(mHeightMax)); }
4176
4177 Vec3 toLocal(const Vec3& point) const
4178 {
4179 const Vec3 relativePoint = point - mCenter;
4180 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
4181 }
4182
4183 bool heightInRange(double height) const
4184 {
4185 return height >= mHeightMin - kTolerance && height <= mHeightMax + kTolerance;
4186 }
4187
4188 bool phiInSweep(double phi) const
4189 {
4190 return angleInSweepRange(phi, mPhiStart, mPhiSweep, mPhiTolerance);
4191 }
4192
4193 Vec3 pointAt(double phi, double height) const
4194 {
4195 return mCenter + mAxisW * height + (mAxisU * std::cos(phi) + mAxisV * std::sin(phi)) * radiusAt(height);
4196 }
4197
4198 bool containsPointOnSurface(const Vec3& point) const override
4199 {
4200 const Vec3 localPoint = toLocal(point);
4201 const double surfaceRadius = radiusAt(localPoint.zCoord);
4202 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
4203 // |rho - r(h)| overestimates the true surface distance by sqrt(1 + slope^2)
4204 if (std::abs(radialDistance - surfaceRadius) > kTolerance * std::sqrt(1. + mSlope * mSlope)) {
4205 return false;
4206 }
4207 if (radialDistance <= kTolerance) {
4208 return !mHasWireTrim && heightInRange(localPoint.zCoord); // phi is undefined near the apex
4209 }
4210 const double phi = std::atan2(localPoint.yCoord, localPoint.xCoord);
4211 if (mHasWireTrim) {
4212 return pointInTrim(phi, localPoint.zCoord);
4213 }
4214 return heightInRange(localPoint.zCoord) && phiInSweep(phi);
4215 }
4216
4217 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
4218 double maxDistance, std::vector<RayHit>& hits) const override
4219 {
4220 const Vec3 localOrigin = toLocal(rayOrigin);
4221 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
4222
4223 // (ox + t dx)^2 + (oy + t dy)^2 = (radius0 + slope * (oz + t dz))^2
4224 const double surfaceRadiusAtOrigin = mRadius0 + mSlope * localOrigin.zCoord;
4225 const double quadraticA = localDirection.xCoord * localDirection.xCoord +
4226 localDirection.yCoord * localDirection.yCoord -
4227 mSlope * mSlope * localDirection.zCoord * localDirection.zCoord;
4228 const double quadraticB = 2. * (localOrigin.xCoord * localDirection.xCoord +
4229 localOrigin.yCoord * localDirection.yCoord -
4230 mSlope * localDirection.zCoord * surfaceRadiusAtOrigin);
4231 const double quadraticC = localOrigin.xCoord * localOrigin.xCoord +
4232 localOrigin.yCoord * localOrigin.yCoord -
4233 surfaceRadiusAtOrigin * surfaceRadiusAtOrigin;
4234
4235 std::array<double, 2> candidates{};
4236 int candidateCount = 0;
4237 if (std::abs(quadraticA) <= kToleranceSq) {
4238 if (std::abs(quadraticB) <= kToleranceSq) {
4239 return; // ray runs along the cone surface or its asymptote: no transversal crossing
4240 }
4241 candidates[candidateCount++] = -quadraticC / quadraticB;
4242 } else {
4243 const double discriminant = quadraticB * quadraticB - 4. * quadraticA * quadraticC;
4244 if (discriminant <= 0.) {
4245 return;
4246 }
4247 const double sqrtDiscriminant = std::sqrt(discriminant);
4248 const double firstRoot = (-quadraticB - sqrtDiscriminant) / (2. * quadraticA);
4249 const double secondRoot = (-quadraticB + sqrtDiscriminant) / (2. * quadraticA);
4250 if (sameIntersection(firstRoot, secondRoot)) {
4251 return; // tangential graze (this also covers rays through the exact apex)
4252 }
4253 candidates[candidateCount++] = std::min(firstRoot, secondRoot);
4254 candidates[candidateCount++] = std::max(firstRoot, secondRoot);
4255 }
4256
4257 for (int candidateIndex = 0; candidateIndex < candidateCount; ++candidateIndex) {
4258 const double candidate = candidates[candidateIndex];
4259 if (candidate < minDistance || candidate > maxDistance) {
4260 continue;
4261 }
4262 const double hitHeight = localOrigin.zCoord + candidate * localDirection.zCoord;
4263 const double hitSurfaceRadius = radiusAt(hitHeight);
4264 if (hitSurfaceRadius < -kTolerance) {
4265 continue; // mirror nappe of the infinite cone
4266 }
4267 const double hitU = localOrigin.xCoord + candidate * localDirection.xCoord;
4268 const double hitV = localOrigin.yCoord + candidate * localDirection.yCoord;
4269 const double radialDistance = std::hypot(hitU, hitV);
4270 if (radialDistance <= kTolerance) {
4271 continue; // apex hit: the normal is undefined there
4272 }
4273 const double hitPhi = std::atan2(hitV, hitU);
4274 bool onTrimBoundary = false;
4275 if (mHasWireTrim) {
4276 if (!pointInTrim(hitPhi, hitHeight, &onTrimBoundary)) {
4277 continue;
4278 }
4279 } else if (!heightInRange(hitHeight) || !phiInSweep(hitPhi)) {
4280 continue;
4281 }
4282 const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope);
4283 const Vec3 hitNormal =
4284 (mAxisU * (hitU / radialDistance) + mAxisV * (hitV / radialDistance) - mAxisW * mSlope) * normalScale;
4285 hits.push_back({candidate, hitNormal, onTrimBoundary});
4286 }
4287 }
4288
4290 double distanceSqToPatch(const Vec3& point) const override
4291 {
4292 const Vec3 localPoint = toLocal(point);
4293 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
4294 if (radialDistance <= kTolerance || phiInSweep(std::atan2(localPoint.yCoord, localPoint.xCoord))) {
4295 return pointSegmentDistanceSq(Vec2{radialDistance, localPoint.zCoord},
4296 Vec2{radiusAt(mHeightMin), mHeightMin}, Vec2{radiusAt(mHeightMax), mHeightMax});
4297 }
4298 const double endPhi = mPhiStart + mPhiSweep;
4299 const double distanceToStartSeam =
4300 pointSegmentDistanceSq(point, pointAt(mPhiStart, mHeightMin), pointAt(mPhiStart, mHeightMax));
4301 const double distanceToEndSeam =
4302 pointSegmentDistanceSq(point, pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
4303 return std::min(distanceToStartSeam, distanceToEndSeam);
4304 }
4305
4306 Vec3 normalAt(const Vec3& point) const override
4307 {
4308 const Vec3 localPoint = toLocal(point);
4309 const double radialDistance = std::hypot(localPoint.xCoord, localPoint.yCoord);
4310 const double normalScale = mNormalSign / std::sqrt(1. + mSlope * mSlope);
4311 if (radialDistance <= kTolerance) {
4312 return (mAxisU - mAxisW * mSlope) * normalScale; // ill-defined on the axis; stable fallback
4313 }
4314 return (mAxisU * (localPoint.xCoord / radialDistance) + mAxisV * (localPoint.yCoord / radialDistance) -
4315 mAxisW * mSlope) *
4316 normalScale;
4317 }
4318
4320 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
4321 {
4322 coneParametricMetric(radiusAt(uv.vCoord), mSlope, gUU, gUV, gVV);
4323 }
4324
4326 double capacityContribution() const override
4327 {
4328 if (mHasWireTrim) {
4329 const double centreU = dot(mCenter, mAxisU);
4330 const double centreV = dot(mCenter, mAxisV);
4331 const double centreW = dot(mCenter, mAxisW);
4332 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phi, double height) {
4333 const double localRadius = radiusAt(height);
4334 return mNormalSign / 3. * localRadius *
4335 (centreU * std::sin(phi) - centreV * std::cos(phi) +
4336 (localRadius - mSlope * (centreW + height)) * phi);
4337 });
4338 }
4339 const double endPhi = mPhiStart + mPhiSweep;
4340 const double phiFactor = dot(mCenter, mAxisU) * (std::sin(endPhi) - std::sin(mPhiStart)) -
4341 dot(mCenter, mAxisV) * (std::cos(endPhi) - std::cos(mPhiStart));
4342 const double radiusIntegral = mRadius0 * (mHeightMax - mHeightMin) +
4343 0.5 * mSlope * (mHeightMax * mHeightMax - mHeightMin * mHeightMin);
4344 return mNormalSign * radiusIntegral *
4345 (phiFactor + (mRadius0 - mSlope * dot(mCenter, mAxisW)) * mPhiSweep) / 3.;
4346 }
4347
4348 bool capacityIsExact() const override { return !mHasWireTrim; }
4349
4350 void conservativeBounds(Vec3& lower, Vec3& upper) const override
4351 {
4352 for (const double height : {mHeightMin, mHeightMax}) {
4353 const Vec3 rimCenter = mCenter + mAxisW * height;
4354 const double rimRadius = std::max(0., radiusAt(height));
4355 for (int dimension = 0; dimension < 3; ++dimension) {
4356 const double radialExtent =
4357 rimRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension));
4358 const double centerValue = component(rimCenter, dimension);
4359 if (dimension == 0) {
4360 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
4361 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
4362 } else if (dimension == 1) {
4363 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
4364 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
4365 } else {
4366 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
4367 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
4368 }
4369 }
4370 }
4371 }
4372
4374 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
4375 {
4376 appendArcBandCoverBoxes(mCenter, mAxisU, mAxisV, mAxisW, mPhiStart, mPhiSweep, mHeightMin, mHeightMax,
4377 std::max(0., radiusAt(mHeightMin)), std::max(0., radiusAt(mHeightMax)), boxes);
4378 }
4379
4380 int rimSegments() const
4381 {
4382 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
4383 }
4384
4385 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
4386 {
4387 if (mHasWireTrim) {
4388 appendCurveTrimMesh(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, vertices, triangles);
4389 return;
4390 }
4391 const int segments = rimSegments();
4392 const int firstVertexIndex = static_cast<int>(vertices.size());
4393 for (int step = 0; step <= segments; ++step) {
4394 const double phi = mPhiStart + mPhiSweep * step / segments;
4395 vertices.push_back(pointAt(phi, mHeightMin));
4396 vertices.push_back(pointAt(phi, mHeightMax));
4397 }
4398 for (int step = 0; step < segments; ++step) {
4399 const int base = firstVertexIndex + 2 * step;
4400 // skip triangles that collapse at an apex rim
4401 if (radiusAt(mHeightMin) > kTolerance) {
4402 triangles.push_back({base, base + 2, base + 3});
4403 }
4404 if (radiusAt(mHeightMax) > kTolerance) {
4405 triangles.push_back({base, base + 3, base + 1});
4406 }
4407 }
4408 }
4409
4410 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
4411 {
4412 if (mHasWireTrim) {
4413 // the (phi, h) -> 3D map is orientation-consistent with the outward normal (as for the
4414 // cylinder), so the sign is just mNormalSign
4415 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phi, double height) { return pointAt(phi, height); }, mNormalSign, edges);
4416 return;
4417 }
4418 // same boundary orientation as the cylinder; an apex rim degenerates to a point and is
4419 // skipped so an apex cone closes against just one cap
4420 const int segments = rimSegments();
4421 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
4422 if (mNormalSign > 0.) {
4423 edges.emplace_back(edgeStart, edgeEnd);
4424 } else {
4425 edges.emplace_back(edgeEnd, edgeStart);
4426 }
4427 };
4428 for (int step = 0; step < segments; ++step) {
4429 const double phi = mPhiStart + mPhiSweep * step / segments;
4430 const double nextPhi = mPhiStart + mPhiSweep * (step + 1) / segments;
4431 if (radiusAt(mHeightMin) > kTolerance) {
4432 emitEdge(pointAt(phi, mHeightMin), pointAt(nextPhi, mHeightMin));
4433 }
4434 if (radiusAt(mHeightMax) > kTolerance) {
4435 emitEdge(pointAt(nextPhi, mHeightMax), pointAt(phi, mHeightMax));
4436 }
4437 }
4438 if (!fullSweep()) {
4439 const double endPhi = mPhiStart + mPhiSweep;
4440 emitEdge(pointAt(endPhi, mHeightMin), pointAt(endPhi, mHeightMax));
4441 emitEdge(pointAt(mPhiStart, mHeightMax), pointAt(mPhiStart, mHeightMin));
4442 }
4443 }
4444
4445 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
4446 {
4447 if (!mHasWireTrim) {
4448 return false; // a parametric-rectangle trim carries no per-edge curve to sample
4449 }
4450 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phi, double height) { return pointAt(phi, height); }, samples);
4451 }
4452
4453 private:
4454 Vec3 mCenter;
4455 Vec3 mAxisU;
4456 Vec3 mAxisV;
4457 Vec3 mAxisW;
4458 double mRadius0 = 0.;
4459 double mSlope = 0.;
4460 double mHeightMin = 0.;
4461 double mHeightMax = 0.;
4462 double mPhiStart = 0.;
4463 double mPhiSweep = kTwoPi;
4464 double mPhiTolerance = 0.;
4465 double mNormalSign = 1.;
4466 bool mHasWireTrim = false;
4467 CurveWire mTrimOuter;
4468 std::vector<CurveWire> mTrimInner;
4469};
4470
4474{
4475 public:
4476 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius,
4477 double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep,
4478 bool innerWall, std::string& errorMessage)
4479 {
4480 if (!finite(centerPoint) || !finite(axis) || !finite(referenceAxisU) || !std::isfinite(majorRadius) ||
4481 !std::isfinite(minorRadius) || !std::isfinite(phiStart) || !std::isfinite(phiSweep) ||
4482 !std::isfinite(tubeStart) || !std::isfinite(tubeSweep)) {
4483 errorMessage = "toroidal surface parameter is non-finite";
4484 return false;
4485 }
4486 if (majorRadius <= kTolerance || minorRadius <= kTolerance) {
4487 errorMessage = "toroidal surface needs positive major and minor radii";
4488 return false;
4489 }
4490 if (phiSweep <= kTolerance || phiSweep > kTwoPi + kTolerance) {
4491 errorMessage = "toroidal surface needs a ring sweep in (0, 2pi]";
4492 return false;
4493 }
4494 if (tubeSweep <= kTolerance || tubeSweep > kTwoPi + kTolerance) {
4495 errorMessage = "toroidal surface needs a tube sweep in (0, 2pi]";
4496 return false;
4497 }
4498 if (!CylindricalBoundedSurface::makeFrame(axis, referenceAxisU, mAxisU, mAxisV, mAxisW, errorMessage)) {
4499 return false;
4500 }
4501
4502 mCenter = centerPoint;
4503 mMajorRadius = majorRadius;
4504 mMinorRadius = minorRadius;
4505 mRingTolerance = angularTolerance(mMajorRadius);
4506 mTubeTolerance = angularTolerance(mMinorRadius);
4507 mPhiStart = phiStart;
4508 mPhiSweep = std::min(phiSweep, kTwoPi);
4509 mTubeStart = tubeStart;
4510 mTubeSweep = std::min(tubeSweep, kTwoPi);
4511 mNormalSign = innerWall ? -1. : 1.;
4512 return true;
4513 }
4514
4516 bool initialize(const Vec3& centerPoint, const Vec3& axis, const Vec3& referenceAxisU, double majorRadius,
4517 double minorRadius, double phiStart, double phiSweep, double tubeStart, double tubeSweep,
4518 bool innerWall, const std::vector<Curve2D>& outerTrim,
4519 const std::vector<std::vector<Curve2D>>& innerTrims, std::string& errorMessage,
4520 double joinTolerance = kWireJoinTolerance)
4521 {
4522 if (!initialize(centerPoint, axis, referenceAxisU, majorRadius, minorRadius, phiStart, phiSweep, tubeStart,
4523 tubeSweep, innerWall, errorMessage)) {
4524 return false;
4525 }
4526 Vec2 lower, upper;
4527 if (!buildCurveTrim(outerTrim, innerTrims, mTrimOuter, mTrimInner, lower, upper, errorMessage,
4528 parametricMetricOf(*this), joinTolerance)) {
4529 return false;
4530 }
4531 if (upper.vCoord - lower.vCoord > kTwoPi + kTolerance) {
4532 errorMessage = "toroidal trim wire spans more than a full turn in the tube angle";
4533 return false;
4534 }
4535 mPhiStart = lower.uCoord;
4536 mPhiSweep = std::min(kTwoPi, upper.uCoord - lower.uCoord);
4537 mTubeStart = lower.vCoord;
4538 mTubeSweep = std::min(kTwoPi, upper.vCoord - lower.vCoord);
4539 mHasWireTrim = true;
4540 return true;
4541 }
4542
4543 bool hasWireTrim() const { return mHasWireTrim; }
4544
4546 bool pointInTrim(double phiRing, double phiTube, bool* boundary = nullptr) const
4547 {
4548 const double uCoord = unwrapAngleInto(phiRing, mPhiStart, mPhiStart + mPhiSweep);
4549 const double vCoord = unwrapAngleInto(phiTube, mTubeStart, mTubeStart + mTubeSweep);
4550 return curveTrimContains(mTrimOuter, mTrimInner, {uCoord, vCoord}, boundary, parametricMetricOf(*this));
4551 }
4552
4553 bool fullRingSweep() const { return mPhiSweep >= kTwoPi - kTolerance; }
4554 bool fullTubeSweep() const { return mTubeSweep >= kTwoPi - kTolerance; }
4555
4556 Vec3 toLocal(const Vec3& point) const
4557 {
4558 const Vec3 relativePoint = point - mCenter;
4559 return {dot(relativePoint, mAxisU), dot(relativePoint, mAxisV), dot(relativePoint, mAxisW)};
4560 }
4561
4562 bool ringInSweep(double phiRing) const
4563 {
4564 return angleInSweepRange(phiRing, mPhiStart, mPhiSweep, mRingTolerance);
4565 }
4566
4567 bool tubeInSweep(double phiTube) const
4568 {
4569 return angleInSweepRange(phiTube, mTubeStart, mTubeSweep, mTubeTolerance);
4570 }
4571
4572 Vec3 pointAt(double phiRing, double phiTube) const
4573 {
4574 const double ringRadius = mMajorRadius + mMinorRadius * std::cos(phiTube);
4575 return mCenter + (mAxisU * std::cos(phiRing) + mAxisV * std::sin(phiRing)) * ringRadius +
4576 mAxisW * (mMinorRadius * std::sin(phiTube));
4577 }
4578
4580 Vec3 localNormal(const Vec3& localPoint) const
4581 {
4582 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4583 if (rho <= kTolerance) {
4584 return mAxisW * (localPoint.zCoord >= 0. ? mNormalSign : -mNormalSign);
4585 }
4586 const double radialFactor = (rho - mMajorRadius) / rho;
4587 Vec3 normal{radialFactor * localPoint.xCoord, radialFactor * localPoint.yCoord, localPoint.zCoord};
4588 const double length = norm(normal);
4589 if (length <= kTolerance) {
4590 return mAxisU * mNormalSign;
4591 }
4592 return (mAxisU * normal.xCoord + mAxisV * normal.yCoord + mAxisW * normal.zCoord) * (mNormalSign / length);
4593 }
4594
4595 bool containsPointOnSurface(const Vec3& point) const override
4596 {
4597 const Vec3 localPoint = toLocal(point);
4598 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4599 const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius;
4600 if (std::abs(meridianDistance) > kTolerance) {
4601 return false;
4602 }
4603 const double phiTube = std::atan2(localPoint.zCoord, rho - mMajorRadius);
4604 if (rho <= kTolerance) {
4605 return false; // on the axis phiRing is undefined (only reachable on a horn/spindle torus)
4606 }
4607 const double phiRing = std::atan2(localPoint.yCoord, localPoint.xCoord);
4608 if (mHasWireTrim) {
4609 return pointInTrim(phiRing, phiTube);
4610 }
4611 return ringInSweep(phiRing) && tubeInSweep(phiTube);
4612 }
4613
4614 void appendIntersections(const Vec3& rayOrigin, const Vec3& rayDirection, double minDistance,
4615 double maxDistance, std::vector<RayHit>& hits) const override
4616 {
4617 const Vec3 localOrigin = toLocal(rayOrigin);
4618 const Vec3 localDirection{dot(rayDirection, mAxisU), dot(rayDirection, mAxisV), dot(rayDirection, mAxisW)};
4619
4620 // Torus implicit form (local): (|X|^2 + R^2 - r^2)^2 = 4 R^2 (x^2 + y^2). Substituting the ray
4621 // X = O + t D gives a quartic in t whose leading coefficient is |D|^4 > 0.
4622 const double dirDotDir = normSq(localDirection);
4623 if (dirDotDir <= kToleranceSq) {
4624 return; // degenerate direction
4625 }
4626 const double originDotDir = dot(localOrigin, localDirection);
4627 const double originDotOrigin = normSq(localOrigin);
4628 const double constantK = mMajorRadius * mMajorRadius - mMinorRadius * mMinorRadius;
4629 const double transverseE = localDirection.xCoord * localDirection.xCoord +
4630 localDirection.yCoord * localDirection.yCoord;
4631 const double transverseF = localOrigin.xCoord * localDirection.xCoord +
4632 localOrigin.yCoord * localDirection.yCoord;
4633 const double transverseG = localOrigin.xCoord * localOrigin.xCoord +
4634 localOrigin.yCoord * localOrigin.yCoord;
4635 const double fourRSquared = 4. * mMajorRadius * mMajorRadius;
4636
4637 const double coeff4 = dirDotDir * dirDotDir;
4638 const double coeff3 = 4. * dirDotDir * originDotDir;
4639 const double coeff2 =
4640 4. * originDotDir * originDotDir + 2. * dirDotDir * (originDotOrigin + constantK) - fourRSquared * transverseE;
4641 const double coeff1 = 4. * originDotDir * (originDotOrigin + constantK) - 2. * fourRSquared * transverseF;
4642 const double coeff0 = (originDotOrigin + constantK) * (originDotOrigin + constantK) - fourRSquared * transverseG;
4643
4644 QuarticRoots candidates = solveQuarticReal(coeff4, coeff3, coeff2, coeff1, coeff0);
4645 if (candidates.empty()) {
4646 return;
4647 }
4648 std::sort(candidates.begin(), candidates.end());
4649
4650 // an even-sized cluster of near-equal roots is a tangency and is dropped; an odd one is one crossing at its mean
4651 size_t rootIndex = 0;
4652 while (rootIndex < candidates.size()) {
4653 size_t clusterEnd = rootIndex + 1;
4654 double clusterSum = candidates[rootIndex];
4655 while (clusterEnd < candidates.size() && sameIntersection(candidates[clusterEnd], candidates[clusterEnd - 1])) {
4656 clusterSum += candidates[clusterEnd];
4657 ++clusterEnd;
4658 }
4659 const size_t clusterSize = clusterEnd - rootIndex;
4660 rootIndex = clusterEnd;
4661 if ((clusterSize & 1u) == 0u) {
4662 continue; // tangential graze
4663 }
4664 const double candidate = clusterSum / static_cast<double>(clusterSize);
4665 if (candidate < minDistance || candidate > maxDistance) {
4666 continue;
4667 }
4668 const Vec3 localHit = toLocal(rayOrigin + rayDirection * candidate);
4669 const double rho = std::hypot(localHit.xCoord, localHit.yCoord);
4670 if (rho <= kTolerance) {
4671 continue;
4672 }
4673 const double phiTube = std::atan2(localHit.zCoord, rho - mMajorRadius);
4674 const double phiRing = std::atan2(localHit.yCoord, localHit.xCoord);
4675 bool onTrimBoundary = false;
4676 if (mHasWireTrim) {
4677 if (!pointInTrim(phiRing, phiTube, &onTrimBoundary)) {
4678 continue;
4679 }
4680 } else if (!ringInSweep(phiRing) || !tubeInSweep(phiTube)) {
4681 continue;
4682 }
4683 hits.push_back({candidate, localNormal(localHit), onTrimBoundary});
4684 }
4685 }
4686
4688 double distanceSqToPatch(const Vec3& point) const override
4689 {
4690 const Vec3 localPoint = toLocal(point);
4691 const double rho = std::hypot(localPoint.xCoord, localPoint.yCoord);
4692 const double meridianDistance = std::hypot(rho - mMajorRadius, localPoint.zCoord) - mMinorRadius;
4693 return meridianDistance * meridianDistance;
4694 }
4695
4696 Vec3 normalAt(const Vec3& point) const override { return localNormal(toLocal(point)); }
4697
4699 void parametricMetric(const Vec2& uv, double& gUU, double& gUV, double& gVV) const override
4700 {
4701 torusParametricMetric(mMajorRadius, mMinorRadius, uv.vCoord, gUU, gUV, gVV);
4702 }
4703
4705 double capacityContribution() const override
4706 {
4707 if (mHasWireTrim) {
4708 const double centreU = dot(mCenter, mAxisU);
4709 const double centreV = dot(mCenter, mAxisV);
4710 const double centreW = dot(mCenter, mAxisW);
4711 return integrateOverCurveTrimByParts(mTrimOuter, mTrimInner, [&](double phiRing, double phiTube) {
4712 const double cosTube = std::cos(phiTube);
4713 const double sinTube = std::sin(phiTube);
4714 const double rho = mMajorRadius + mMinorRadius * cosTube;
4715 return mNormalSign * mMinorRadius * rho / 3. *
4716 (cosTube * (centreU * std::sin(phiRing) - centreV * std::cos(phiRing)) +
4717 (centreW * sinTube + rho * cosTube + mMinorRadius * sinTube * sinTube) * phiRing);
4718 });
4719 }
4720 // Closed form over u in [u0, u1] (ring) and v in [v0, v1] (tube).
4721 const double majorR = mMajorRadius;
4722 const double minorR = mMinorRadius;
4723 const double u0 = mPhiStart, u1 = mPhiStart + mPhiSweep;
4724 const double v0 = mTubeStart, v1 = mTubeStart + mTubeSweep;
4725 const double centerU = dot(mCenter, mAxisU);
4726 const double centerV = dot(mCenter, mAxisV);
4727 const double centerW = dot(mCenter, mAxisW);
4728 const double deltaU = u1 - u0;
4729 const double deltaV = v1 - v0;
4730 const double sinIntegralU = std::sin(u1) - std::sin(u0); // integral cos u du
4731 const double cosIntegralU = std::cos(u0) - std::cos(u1); // integral sin u du
4732 const double sinIntegralV = std::sin(v1) - std::sin(v0); // integral cos v dv
4733 const double sinFromCosV = std::cos(v0) - std::cos(v1); // integral sin v dv
4734 const double cosSquaredV = 0.5 * deltaV + 0.25 * (std::sin(2. * v1) - std::sin(2. * v0)); // integral cos^2 v dv
4735 const double sinCosV = 0.25 * (std::cos(2. * v0) - std::cos(2. * v1)); // integral sin v cos v dv
4736
4737 // centre-independent part, integrated over v then multiplied by the ring span
4738 const double centerlessV =
4739 minorR * ((majorR * majorR + minorR * minorR) * sinIntegralV + majorR * minorR * deltaV +
4740 majorR * minorR * cosSquaredV);
4741 // W component of the centre offset
4742 const double centerWpart = minorR * (majorR * sinFromCosV + minorR * sinCosV);
4743 // U/V components of the centre offset (ring-angle dependent)
4744 const double centerUVpart =
4745 (centerU * sinIntegralU + centerV * cosIntegralU) * minorR * (majorR * sinIntegralV + minorR * cosSquaredV);
4746
4747 const double total = deltaU * centerlessV + deltaU * centerW * centerWpart + centerUVpart;
4748 return mNormalSign * total / 3.;
4749 }
4750
4751 bool capacityIsExact() const override { return !mHasWireTrim; }
4752
4753 void conservativeBounds(Vec3& lower, Vec3& upper) const override
4754 {
4755 // conservative: the AABB of the full torus (partial sweeps get a larger box)
4756 const double outerRadius = mMajorRadius + mMinorRadius;
4757 for (int dimension = 0; dimension < 3; ++dimension) {
4758 const double radialExtent = outerRadius * std::hypot(component(mAxisU, dimension), component(mAxisV, dimension)) +
4759 mMinorRadius * std::abs(component(mAxisW, dimension));
4760 const double centerValue = component(mCenter, dimension);
4761 if (dimension == 0) {
4762 lower.xCoord = std::min(lower.xCoord, centerValue - radialExtent);
4763 upper.xCoord = std::max(upper.xCoord, centerValue + radialExtent);
4764 } else if (dimension == 1) {
4765 lower.yCoord = std::min(lower.yCoord, centerValue - radialExtent);
4766 upper.yCoord = std::max(upper.yCoord, centerValue + radialExtent);
4767 } else {
4768 lower.zCoord = std::min(lower.zCoord, centerValue - radialExtent);
4769 upper.zCoord = std::max(upper.zCoord, centerValue + radialExtent);
4770 }
4771 }
4772 }
4773
4775 void appendCoverBoxes(std::vector<CoverBox>& boxes) const override
4776 {
4777 if (mMajorRadius < mMinorRadius) {
4779 return;
4780 }
4781 const int ringChunks = coverChunkCount(kTwoPi);
4782 const int tubeChunks = coverChunkCount(kTwoPi);
4783 for (int ringChunk = 0; ringChunk < ringChunks; ++ringChunk) {
4784 const double ringLow = kTwoPi * ringChunk / ringChunks;
4785 const double ringHigh = kTwoPi * (ringChunk + 1) / ringChunks;
4786 for (int tubeChunk = 0; tubeChunk < tubeChunks; ++tubeChunk) {
4787 const double tubeLow = kTwoPi * tubeChunk / tubeChunks;
4788 const double tubeHigh = kTwoPi * (tubeChunk + 1) / tubeChunks;
4789 double lower[3];
4790 double upper[3];
4791 for (int dimension = 0; dimension < 3; ++dimension) {
4792 double inPlaneLow = 0.;
4793 double inPlaneHigh = 0.;
4794 sinusoidRange(component(mAxisU, dimension), component(mAxisV, dimension), ringLow, ringHigh, inPlaneLow,
4795 inPlaneHigh);
4796 // the coordinate is p(u) (R + r cos v) + w r sin v; with R + r cos v >= 0 it is
4797 // monotone in p, so each extreme is a v sinusoid taken at p's own extreme
4798 const double axisComponent = component(mAxisW, dimension);
4799 const double high = sinusoidMaximum(inPlaneHigh, axisComponent, tubeLow, tubeHigh);
4800 const double low = sinusoidMinimum(inPlaneLow, axisComponent, tubeLow, tubeHigh);
4801 lower[dimension] = component(mCenter, dimension) + inPlaneLow * mMajorRadius + mMinorRadius * low;
4802 upper[dimension] = component(mCenter, dimension) + inPlaneHigh * mMajorRadius + mMinorRadius * high;
4803 }
4804 boxes.push_back({Vec3{lower[0], lower[1], lower[2]}, Vec3{upper[0], upper[1], upper[2]}});
4805 }
4806 }
4807 }
4808
4809 int ringSegments() const
4810 {
4811 return std::max(1, static_cast<int>(std::lround(kArcSamples * mPhiSweep / kTwoPi)));
4812 }
4813
4814 int tubeSegments() const
4815 {
4816 return std::max(1, static_cast<int>(std::lround(kArcSamples * mTubeSweep / kTwoPi)));
4817 }
4818
4819 void appendDisplayMesh(std::vector<Vec3>& vertices, std::vector<std::array<int, 3>>& triangles) const override
4820 {
4821 if (mHasWireTrim) {
4822 appendCurveTrimMesh(mTrimOuter, mTrimInner, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, vertices, triangles);
4823 return;
4824 }
4825 const int ringSteps = ringSegments();
4826 const int tubeSteps = tubeSegments();
4827 const int firstVertexIndex = static_cast<int>(vertices.size());
4828 for (int ringStep = 0; ringStep <= ringSteps; ++ringStep) {
4829 const double phiRing = mPhiStart + mPhiSweep * ringStep / ringSteps;
4830 for (int tubeStep = 0; tubeStep <= tubeSteps; ++tubeStep) {
4831 vertices.push_back(pointAt(phiRing, mTubeStart + mTubeSweep * tubeStep / tubeSteps));
4832 }
4833 }
4834 const int rowLength = tubeSteps + 1;
4835 for (int ringStep = 0; ringStep < ringSteps; ++ringStep) {
4836 for (int tubeStep = 0; tubeStep < tubeSteps; ++tubeStep) {
4837 const int base = firstVertexIndex + ringStep * rowLength + tubeStep;
4838 triangles.push_back({base, base + rowLength, base + rowLength + 1});
4839 triangles.push_back({base, base + rowLength + 1, base + 1});
4840 }
4841 }
4842 }
4843
4844 void appendDirectedEdges(std::vector<std::pair<Vec3, Vec3>>& edges) const override
4845 {
4846 if (mHasWireTrim) {
4847 // the (phiRing, phiTube) -> 3D map is orientation-consistent with the outward normal, so
4848 // the sign is just mNormalSign (as for the cylinder and cone)
4849 appendCurveTrimEdges(mTrimOuter, mTrimInner, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, mNormalSign, edges);
4850 return;
4851 }
4852 // boundary of the (phiRing, phiTube) rectangle traversed counter-clockwise as seen along the
4853 // outward normal; a full sweep in either angle has no seam there, so it is skipped
4854 auto emitEdge = [&](const Vec3& edgeStart, const Vec3& edgeEnd) {
4855 if (mNormalSign > 0.) {
4856 edges.emplace_back(edgeStart, edgeEnd);
4857 } else {
4858 edges.emplace_back(edgeEnd, edgeStart);
4859 }
4860 };
4861 const int ringSteps = ringSegments();
4862 const int tubeSteps = tubeSegments();
4863 const double endRing = mPhiStart + mPhiSweep;
4864 const double endTube = mTubeStart + mTubeSweep;
4865 if (!fullTubeSweep()) {
4866 for (int step = 0; step < ringSteps; ++step) {
4867 const double phiRing = mPhiStart + mPhiSweep * step / ringSteps;
4868 const double nextRing = mPhiStart + mPhiSweep * (step + 1) / ringSteps;
4869 emitEdge(pointAt(phiRing, mTubeStart), pointAt(nextRing, mTubeStart)); // +phiRing at tubeStart
4870 emitEdge(pointAt(nextRing, endTube), pointAt(phiRing, endTube)); // -phiRing at tubeEnd
4871 }
4872 }
4873 if (!fullRingSweep()) {
4874 for (int step = 0; step < tubeSteps; ++step) {
4875 const double phiTube = mTubeStart + mTubeSweep * step / tubeSteps;
4876 const double nextTube = mTubeStart + mTubeSweep * (step + 1) / tubeSteps;
4877 emitEdge(pointAt(endRing, phiTube), pointAt(endRing, nextTube)); // +phiTube at ringEnd
4878 emitEdge(pointAt(mPhiStart, nextTube), pointAt(mPhiStart, phiTube)); // -phiTube at ringStart
4879 }
4880 }
4881 }
4882
4883 bool sampleTrimCurve(size_t index, std::vector<Vec3>& samples) const override
4884 {
4885 if (!mHasWireTrim) {
4886 return false; // a parametric-rectangle trim carries no per-edge curve to sample
4887 }
4888 return sampleTrimCurveOfCurveWires(mTrimOuter, mTrimInner, index, [this](double phiRing, double phiTube) { return pointAt(phiRing, phiTube); }, samples);
4889 }
4890
4891 private:
4892 Vec3 mCenter;
4893 Vec3 mAxisU;
4894 Vec3 mAxisV;
4895 Vec3 mAxisW;
4896 double mMajorRadius = 0.;
4897 double mMinorRadius = 0.;
4898 double mPhiStart = 0.;
4899 double mPhiSweep = kTwoPi;
4900 double mTubeStart = 0.;
4901 double mTubeSweep = kTwoPi;
4902 double mRingTolerance = 0.;
4903 double mTubeTolerance = 0.;
4904 double mNormalSign = 1.;
4905 bool mHasWireTrim = false;
4906 CurveWire mTrimOuter;
4907 std::vector<CurveWire> mTrimInner;
4908};
4909
4912enum class RimState {
4913 Matched = 0,
4914 Reversed,
4915 Boundary,
4917};
4918
4934
4988
4990inline void measureSharedEdgeDeviation(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
4992{
4993 // edgeId -> the (surface, slot) pairs claiming it
4994 std::map<uint32_t, std::vector<std::pair<int, size_t>>> claims;
4995 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
4996 if (surfaces[surfaceIndex] == nullptr) {
4997 continue;
4998 }
4999 const auto& refs = surfaces[surfaceIndex]->boundaryEdges();
5000 for (size_t slot = 0; slot < refs.size(); ++slot) {
5001 if (refs[slot].degenerate) {
5002 continue; // a point has no partner and no length to disagree over
5003 }
5004 // unanchored claims are collected too, so that an edge whose other side is a
5005 // parametric-rectangle face is counted as *unmeasured* rather than silently dropped
5006 claims[refs[slot].edgeId].emplace_back(static_cast<int>(surfaceIndex), slot);
5007 }
5008 }
5009
5010 std::vector<Vec3> first;
5011 std::vector<Vec3> second;
5012 for (const auto& [edgeId, holders] : claims) {
5013 if (holders.size() != 2) {
5014 continue;
5015 }
5016 const auto& [firstSurface, firstSlot] = holders[0];
5017 const auto& [secondSurface, secondSlot] = holders[1];
5018 if (!surfaces[static_cast<size_t>(firstSurface)]->sampleTrimCurve(firstSlot, first) ||
5019 !surfaces[static_cast<size_t>(secondSurface)]->sampleTrimCurve(secondSlot, second) || first.size() < 2 ||
5020 second.size() < 2) {
5021 ++report.sharedEdgesUnmeasured;
5022 continue;
5023 }
5024 ++report.sharedEdgesMeasured;
5025 auto worstAgainst = [](const std::vector<Vec3>& probes, const std::vector<Vec3>& polyline, Vec3& where) {
5026 double worst = 0.;
5027 for (const Vec3& probe : probes) {
5028 double nearest = std::numeric_limits<double>::infinity();
5029 for (size_t segment = 0; segment + 1 < polyline.size(); ++segment) {
5030 nearest = std::min(nearest, pointSegmentDistanceSq(probe, polyline[segment], polyline[segment + 1]));
5031 }
5032 if (nearest > worst) {
5033 worst = nearest;
5034 where = probe;
5035 }
5036 }
5037 return std::sqrt(worst);
5038 };
5039 Vec3 forwardPoint{};
5040 Vec3 backwardPoint{};
5041 const double forwardWorst = worstAgainst(first, second, forwardPoint);
5042 const double backwardWorst = worstAgainst(second, first, backwardPoint);
5043 const double deviation = std::max(forwardWorst, backwardWorst);
5044 if (deviation > report.maxSharedEdgeDeviation) {
5045 report.maxSharedEdgeDeviation = deviation;
5046 report.maxSharedEdgeDeviationEdge = edgeId;
5047 report.maxSharedEdgeDeviationPoint = forwardWorst >= backwardWorst ? forwardPoint : backwardPoint;
5048 report.maxSharedEdgeDeviationFaces[0] = firstSurface;
5049 report.maxSharedEdgeDeviationFaces[1] = secondSurface;
5050 }
5051 }
5052}
5053
5055inline void measureRimClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces, double epsilon,
5057{
5058 report.rimEpsilon = epsilon;
5059
5060 std::vector<SurfaceRim> rims;
5061 std::vector<int> rimIndexOnSurface;
5062 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
5063 if (surfaces[surfaceIndex] == nullptr) {
5064 continue;
5065 }
5066 const size_t firstNewRim = rims.size();
5067 surfaces[surfaceIndex]->appendRims(rims);
5068 for (size_t rimIndex = firstNewRim; rimIndex < rims.size(); ++rimIndex) {
5069 rims[rimIndex].surfaceIndex = static_cast<int>(surfaceIndex);
5070 rimIndexOnSurface.push_back(static_cast<int>(rimIndex - firstNewRim));
5071 }
5072 }
5073 report.rims = static_cast<int>(rims.size());
5074 if (rims.empty()) {
5075 return;
5076 }
5077
5078 // Flatten to chords with each chord's sagitta: two polylines of one curve differ by it, so it widens the match band.
5079 // The sagitta is estimated per chord from the turn angle at smooth vertices; a corner has none.
5080 constexpr double kMaxSmoothTurn = 0.52; // ~30 degrees; a rim sampled at kArcSamples turns by 15
5081 struct Chord {
5082 Vec3 start;
5083 Vec3 end;
5084 int surfaceIndex;
5085 double resolution;
5086 };
5087 std::vector<Chord> chords;
5088 std::vector<std::pair<size_t, size_t>> chordRange(rims.size()); // [first, last) chord of each rim
5089 for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) {
5090 const SurfaceRim& rim = rims[rimIndex];
5091 chordRange[rimIndex].first = chords.size();
5092 const size_t pointCount = rim.points.size();
5093 std::vector<double> vertexSagitta(pointCount, 0.);
5094 const size_t interiorCount = rim.closed ? pointCount : (pointCount >= 2 ? pointCount - 2 : 0);
5095 for (size_t offset = 0; offset < interiorCount; ++offset) {
5096 const size_t middle = rim.closed ? offset : offset + 1;
5097 const Vec3 incoming = rim.points[middle] - rim.points[(middle + pointCount - 1) % pointCount];
5098 const Vec3 outgoing = rim.points[(middle + 1) % pointCount] - rim.points[middle];
5099 const double incomingLength = norm(incoming);
5100 const double outgoingLength = norm(outgoing);
5101 if (incomingLength <= kTolerance || outgoingLength <= kTolerance) {
5102 continue;
5103 }
5104 const double turn = std::acos(std::clamp(dot(incoming, outgoing) / (incomingLength * outgoingLength), -1., 1.));
5105 if (turn > kMaxSmoothTurn) {
5106 continue; // a corner of the trim, not a sample of a smooth run
5107 }
5108 vertexSagitta[middle] = 0.25 * (incomingLength + outgoingLength) * std::tan(0.25 * turn);
5109 report.rimChordResolution = std::max(report.rimChordResolution, vertexSagitta[middle]);
5110 }
5111 const size_t chordCount = rim.closed ? pointCount : pointCount - 1;
5112 for (size_t pointIndex = 0; pointIndex < chordCount; ++pointIndex) {
5113 const size_t nextIndex = (pointIndex + 1) % pointCount;
5114 chords.push_back({rim.points[pointIndex], rim.points[nextIndex], rim.surfaceIndex,
5115 std::max(vertexSagitta[pointIndex], vertexSagitta[nextIndex])});
5116 }
5117 chordRange[rimIndex].second = chords.size();
5118 }
5119 if (chords.empty()) {
5120 return;
5121 }
5122
5123 Vec3 lower{chords.front().start};
5124 Vec3 upper{chords.front().start};
5125 auto grow = [&](const Vec3& point) {
5126 lower = {std::min(lower.xCoord, point.xCoord), std::min(lower.yCoord, point.yCoord),
5127 std::min(lower.zCoord, point.zCoord)};
5128 upper = {std::max(upper.xCoord, point.xCoord), std::max(upper.yCoord, point.yCoord),
5129 std::max(upper.zCoord, point.zCoord)};
5130 };
5131 for (const Chord& chord : chords) {
5132 grow(chord.start);
5133 grow(chord.end);
5134 }
5135 const int gridDimension =
5136 std::clamp(static_cast<int>(std::cbrt(static_cast<double>(chords.size()))), 1, 32);
5137 const Vec3 extent = upper - lower;
5138 const double cellSize =
5139 std::max({extent.xCoord, extent.yCoord, extent.zCoord, kTolerance}) / gridDimension;
5140 auto cellOf = [&](double coordinate, double origin) {
5141 return std::clamp(static_cast<int>((coordinate - origin) / cellSize), 0, gridDimension - 1);
5142 };
5143 auto cellIndex = [&](int xCell, int yCell, int zCell) {
5144 return (xCell * gridDimension + yCell) * gridDimension + zCell;
5145 };
5146 std::vector<std::vector<int>> cells(static_cast<size_t>(gridDimension) * gridDimension * gridDimension);
5147 for (size_t chordIndex = 0; chordIndex < chords.size(); ++chordIndex) {
5148 const Chord& chord = chords[chordIndex];
5149 const int xLow = cellOf(std::min(chord.start.xCoord, chord.end.xCoord), lower.xCoord);
5150 const int xHigh = cellOf(std::max(chord.start.xCoord, chord.end.xCoord), lower.xCoord);
5151 const int yLow = cellOf(std::min(chord.start.yCoord, chord.end.yCoord), lower.yCoord);
5152 const int yHigh = cellOf(std::max(chord.start.yCoord, chord.end.yCoord), lower.yCoord);
5153 const int zLow = cellOf(std::min(chord.start.zCoord, chord.end.zCoord), lower.zCoord);
5154 const int zHigh = cellOf(std::max(chord.start.zCoord, chord.end.zCoord), lower.zCoord);
5155 for (int xCell = xLow; xCell <= xHigh; ++xCell) {
5156 for (int yCell = yLow; yCell <= yHigh; ++yCell) {
5157 for (int zCell = zLow; zCell <= zHigh; ++zCell) {
5158 cells[cellIndex(xCell, yCell, zCell)].push_back(static_cast<int>(chordIndex));
5159 }
5160 }
5161 }
5162 }
5163
5164 struct Match {
5165 double distance = std::numeric_limits<double>::infinity();
5166 int chordIndex = -1;
5168 bool withinBand = false;
5171 std::array<int, 3> coincidentFaces{-1, -1, -1};
5172 int coincidentFaceCount = 0;
5173 };
5174 // Two bands: shared-edge matching uses the sampling-aware band, non-manifold detection the declared tolerance alone.
5175 const double maxBand = epsilon + 2. * report.rimChordResolution;
5176 auto nearestOtherFace = [&](const Vec3& probe, int ownSurfaceIndex, double probeResolution) {
5177 Match match;
5178 auto consider = [&](int chordIndex) {
5179 const Chord& chord = chords[static_cast<size_t>(chordIndex)];
5180 if (chord.surfaceIndex == ownSurfaceIndex) {
5181 return;
5182 }
5183 const double distance = std::sqrt(pointSegmentDistanceSq(probe, chord.start, chord.end));
5184 if (distance < match.distance) {
5185 match.distance = distance;
5186 match.chordIndex = chordIndex;
5187 }
5188 if (distance <= epsilon + probeResolution + chord.resolution) {
5189 match.withinBand = true;
5190 }
5191 if (distance <= epsilon && match.coincidentFaceCount < static_cast<int>(match.coincidentFaces.size())) {
5192 for (int seen = 0; seen < match.coincidentFaceCount; ++seen) {
5193 if (match.coincidentFaces[static_cast<size_t>(seen)] == chord.surfaceIndex) {
5194 return;
5195 }
5196 }
5197 match.coincidentFaces[static_cast<size_t>(match.coincidentFaceCount++)] = chord.surfaceIndex;
5198 }
5199 };
5200 const int xCentre = cellOf(probe.xCoord, lower.xCoord);
5201 const int yCentre = cellOf(probe.yCoord, lower.yCoord);
5202 const int zCentre = cellOf(probe.zCoord, lower.zCoord);
5203 for (int shell = 0; shell < gridDimension; ++shell) {
5204 // stop once the nearest hit is closer than this shell's inner distance and the shells reach the match band
5205 const double shellReach = (shell - 1) * cellSize;
5206 if (shell > 0 && shellReach > std::max(match.distance, maxBand)) {
5207 break;
5208 }
5209 for (int xCell = xCentre - shell; xCell <= xCentre + shell; ++xCell) {
5210 if (xCell < 0 || xCell >= gridDimension) {
5211 continue;
5212 }
5213 for (int yCell = yCentre - shell; yCell <= yCentre + shell; ++yCell) {
5214 if (yCell < 0 || yCell >= gridDimension) {
5215 continue;
5216 }
5217 for (int zCell = zCentre - shell; zCell <= zCentre + shell; ++zCell) {
5218 if (zCell < 0 || zCell >= gridDimension) {
5219 continue;
5220 }
5221 const bool onShell = std::abs(xCell - xCentre) == shell || std::abs(yCell - yCentre) == shell ||
5222 std::abs(zCell - zCentre) == shell;
5223 if (!onShell) {
5224 continue; // interior of the shell: visited on an earlier pass
5225 }
5226 for (const int chordIndex : cells[cellIndex(xCell, yCell, zCell)]) {
5227 consider(chordIndex);
5228 }
5229 }
5230 }
5231 }
5232 }
5233 return match;
5234 };
5235
5236 report.rimRecords.reserve(rims.size());
5237 for (size_t rimIndex = 0; rimIndex < rims.size(); ++rimIndex) {
5238 bool hasUnmatched = false;
5239 bool hasNonManifold = false;
5240 int sameDirectionVotes = 0;
5241 int oppositeDirectionVotes = 0;
5242 RimRecord record;
5243 record.surfaceIndex = rims[rimIndex].surfaceIndex;
5244 record.rimIndexOnSurface = rimIndexOnSurface[rimIndex];
5245 record.closed = rims[rimIndex].closed;
5246 record.chords = static_cast<int>(chordRange[rimIndex].second - chordRange[rimIndex].first);
5247 for (size_t chordIndex = chordRange[rimIndex].first; chordIndex < chordRange[rimIndex].second; ++chordIndex) {
5248 const Chord& chord = chords[chordIndex];
5249 const Vec3 along = chord.end - chord.start;
5250 const double chordLength = norm(along);
5251 report.totalRimLength += chordLength;
5252 record.length += chordLength;
5253 const Vec3 probe = chord.start + along * 0.5;
5254 const Match match = nearestOtherFace(probe, chord.surfaceIndex, chord.resolution);
5255 if (std::isfinite(match.distance)) {
5256 report.maxRimIsolation = std::max(report.maxRimIsolation, match.distance);
5257 if (match.distance > record.maxIsolation || record.maxIsolationFace < 0) {
5258 record.maxIsolation = match.distance;
5259 record.maxIsolationPoint = probe;
5260 record.maxIsolationFace = chords[static_cast<size_t>(match.chordIndex)].surfaceIndex;
5261 }
5262 }
5263 if (match.coincidentFaceCount > 1) {
5264 hasNonManifold = true;
5265 }
5266 if (!match.withinBand) {
5267 hasUnmatched = true;
5268 ++record.unmatchedChords;
5269 report.unmatchedRimLength += chordLength;
5270 record.unmatchedLength += chordLength;
5271 continue;
5272 }
5273 const Chord& partner = chords[static_cast<size_t>(match.chordIndex)];
5274 if (dot(along, partner.end - partner.start) < 0.) {
5275 ++oppositeDirectionVotes;
5276 } else {
5277 ++sameDirectionVotes;
5278 }
5279 }
5280 if (hasNonManifold) {
5281 ++report.nonManifoldRims;
5283 } else if (hasUnmatched) {
5284 ++report.boundaryRims;
5285 record.state = RimState::Boundary;
5286 } else if (sameDirectionVotes > oppositeDirectionVotes) {
5287 ++report.reversedRims;
5288 record.state = RimState::Reversed;
5289 } else {
5290 ++report.matchedRims;
5291 record.state = RimState::Matched;
5292 }
5293 report.rimRecords.push_back(record);
5294 }
5295}
5296
5299inline void applyEdgeIdentityClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
5301{
5302 size_t surfacesPresent = 0;
5303 size_t surfacesStatingEdges = 0;
5304 for (const auto& surface : surfaces) {
5305 if (surface == nullptr) {
5306 continue;
5307 }
5308 ++surfacesPresent;
5309 if (!surface->boundaryEdges().empty()) {
5310 ++surfacesStatingEdges;
5311 }
5312 }
5313 if (surfacesPresent == 0 || surfacesStatingEdges != surfacesPresent) {
5314 return; // no edge identity, or only some of it: leave the geometric verdict alone
5315 }
5316 report.edgeIdentityAvailable = true;
5317
5318 struct Incidence {
5319 int forward = 0;
5320 int reversed = 0;
5321 int degenerate = 0;
5322 };
5323 std::map<uint32_t, Incidence> incidences;
5324 // which faces own each edge, so a defect can be attributed back to a rim
5325 std::map<uint32_t, std::vector<int>> owners;
5326 for (size_t surfaceIndex = 0; surfaceIndex < surfaces.size(); ++surfaceIndex) {
5327 if (surfaces[surfaceIndex] == nullptr) {
5328 continue;
5329 }
5330 for (const auto& ref : surfaces[surfaceIndex]->boundaryEdges()) {
5331 Incidence& incidence = incidences[ref.edgeId];
5332 if (ref.degenerate) {
5333 ++incidence.degenerate;
5334 } else if (ref.reversed) {
5335 ++incidence.reversed;
5336 } else {
5337 ++incidence.forward;
5338 }
5339 owners[ref.edgeId].push_back(static_cast<int>(surfaceIndex));
5340 }
5341 }
5342
5343 // per face, the worst identity defect any of its edges carries
5344 std::vector<RimState> faceState(surfaces.size(), RimState::Matched);
5345 auto worsen = [](RimState& state, RimState candidate) {
5346 // the enum is not ordered by severity, so spell the precedence out
5347 auto rank = [](RimState value) {
5348 switch (value) {
5349 case RimState::Matched:
5350 return 0;
5351 case RimState::Reversed:
5352 return 1;
5353 case RimState::Boundary:
5354 return 2;
5356 return 3;
5357 }
5358 return 0;
5359 };
5360 if (rank(candidate) > rank(state)) {
5361 state = candidate;
5362 }
5363 };
5364
5365 for (const auto& [edgeId, incidence] : incidences) {
5366 ++report.edgeIncidences;
5367 if (incidence.degenerate > 0 && incidence.forward + incidence.reversed == 0) {
5368 ++report.edgeDegenerateCount;
5369 continue;
5370 }
5371 const int total = incidence.forward + incidence.reversed;
5373 if (total == 1) {
5374 ++report.edgeBoundaryCount;
5376 } else if (total == 2) {
5377 if (incidence.forward == 1 && incidence.reversed == 1) {
5378 ++report.edgeSharedCount;
5379 } else {
5380 ++report.edgeReversedCount;
5382 }
5383 } else {
5384 ++report.edgeNonManifoldCount;
5386 }
5387 if (state != RimState::Matched) {
5388 for (const int owner : owners[edgeId]) {
5389 worsen(faceState[static_cast<size_t>(owner)], state);
5390 }
5391 }
5392 }
5393
5394 report.closed = (report.edgeBoundaryCount == 0) && (report.edgeNonManifoldCount == 0);
5395 report.orientationConsistent = (report.edgeReversedCount == 0);
5396
5397 report.matchedRims = 0;
5398 report.boundaryRims = 0;
5399 report.nonManifoldRims = 0;
5400 report.reversedRims = 0;
5401 for (RimRecord& record : report.rimRecords) {
5402 const RimState state = record.surfaceIndex >= 0 && record.surfaceIndex < static_cast<int>(faceState.size())
5403 ? faceState[static_cast<size_t>(record.surfaceIndex)]
5405 record.state = state;
5406 switch (state) {
5408 ++report.nonManifoldRims;
5409 break;
5410 case RimState::Boundary:
5411 ++report.boundaryRims;
5412 break;
5413 case RimState::Reversed:
5414 ++report.reversedRims;
5415 break;
5416 case RimState::Matched:
5417 ++report.matchedRims;
5418 break;
5419 }
5420 }
5421
5423}
5424
5426inline ClosureReport validateClosure(const std::vector<std::unique_ptr<BoundedSurface>>& surfaces,
5427 double modelTolerance = 0.)
5428{
5430
5431 auto quantize = [](double value) { return static_cast<int64_t>(std::llround(value / kClosureQuantum)); };
5432 using VertexKey = std::tuple<int64_t, int64_t, int64_t>;
5433 auto keyOf = [&](const Vec3& point) {
5434 return VertexKey{quantize(point.xCoord), quantize(point.yCoord), quantize(point.zCoord)};
5435 };
5436
5437 std::vector<std::pair<Vec3, Vec3>> directedEdges;
5438 for (const auto& surface : surfaces) {
5439 if (surface != nullptr) {
5440 surface->appendDirectedEdges(directedEdges);
5441 report.signedVolume += surface->capacityContribution();
5442 }
5443 }
5444
5445 // For each undirected edge, count occurrences in the forward and reverse directions.
5446 std::map<std::pair<VertexKey, VertexKey>, std::pair<int, int>> edgeCounts;
5447 for (const auto& directedEdge : directedEdges) {
5448 const VertexKey startKey = keyOf(directedEdge.first);
5449 const VertexKey endKey = keyOf(directedEdge.second);
5450 if (startKey == endKey) {
5451 continue; // degenerate edge, already flagged at wire level
5452 }
5453 const bool forward = startKey < endKey;
5454 const auto orderedKey = forward ? std::make_pair(startKey, endKey) : std::make_pair(endKey, startKey);
5455 auto& counts = edgeCounts[orderedKey];
5456 if (forward) {
5457 ++counts.first;
5458 } else {
5459 ++counts.second;
5460 }
5461 }
5462
5463 for (const auto& [edgeKey, counts] : edgeCounts) {
5464 const int total = counts.first + counts.second;
5465 if (total == 1) {
5466 ++report.boundaryEdges; // missing neighbouring face
5467 } else if (total == 2) {
5468 if (counts.first != 1 || counts.second != 1) {
5469 ++report.reversedEdges; // both faces traverse the edge the same way
5470 }
5471 } else {
5472 ++report.nonManifoldEdges;
5473 }
5474 }
5475
5476 measureRimClosure(surfaces, modelTolerance > 0. ? modelTolerance : kRimMatchTolerance, report);
5477
5478 // the verdict is the rim measurement's; the chord counters only describe how faces differ
5479 report.closed = (report.boundaryRims == 0) && (report.nonManifoldRims == 0);
5480 report.orientationConsistent = (report.reversedRims == 0);
5481
5482 // ... unless the surfaces state their edge identities, which then decide by counting
5484 return report;
5485}
5486
5487} // namespace o2::cad::surface
5488
5489#endif
header::DataOrigin origin
uint64_t vertex
Definition RawEventData.h:9
std::vector< double > sum
int32_t i
configStandalone Light OpenGL display display_light Apply smoothing to points Enable Z buffer Highlight links Highlight seeds Highlight sector tracks Highlight final tracks Show individual Show related Highlight grid Show only individual collision Color clusters belonging to track objects Set line Enable drawing TPC data Enable drawing TOF data Enable drawing magnetic field Set field line step count Invert colors configStandalone Heavy OpenGL display display_heavy Show tracks with ITS contribution Show tracks with TOF contribution Enable propagation of loopers Show clusters at nominal x of pad row for early transformed data Split CE tracks when they cross the central Mark clusters Mark adjacent clusters Hide rejected tracks Apply filter on tracks to be displayed Separate increase X coordinate
Timeframe settings Average distance in cm of events merged into Randomize distance around average distance of merged events Simulate correct bunch interactions instead of placing only the average number of events A value[n] sets TFlen for[n] collisions in Number of bunches per trainsort Number of bunch trains Instantaneous interaction rate Do not simulate border Total number of collisions to be placed in the interior of all time Do not select random but walk over array of events in stride steps h Event generator settings h configStandalone Use CPU for GPU Force usage of the specified GPU device no CPU fallback Continue processing after an error First event to Number of events to Number of iterations to Number of iterations to Number of initial iterations excluded from const char Directory with events to Events directory is and not inside events d
configStandalone Reconstruction rec_tpc multiplier for the combined cluster track error during track following area in cm for the search of for z only used if decrease removal by factor
configStandalone Reconstruction rec_tpc multiplier for the combined cluster track error during track following area in cm for the search of for z only used if decrease removal by when above this local occupancy rowx Shift of TPC revert cluster flag edge bit distance to edge exceeds this parameter(fixed during CTF decoding)") AddOptionRTC(defaultZOffsetOverR
constexpr int p1()
constexpr to accelerate the coordinates changing
SurfaceTrackState state
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
Acts::Vector3 position
const Acts::Surface * surface
std::array< float, NCoordinates > derivative
std::vector< std::shared_ptr< Surface > > surfaces
double radius
XrdCl::ChunkList chunks
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
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
GLint ref
Definition glcorearb.h:291
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)
double ringSignedArea(const std::vector< Vec2 > &points, const std::vector< int > &ring)
Signed area of the ring of indices ring into points; positive when it winds counter-clockwise.
bool segmentsTouch(const Vec2 &firstStart, const Vec2 &firstEnd, const Vec2 &secondStart, const Vec2 &secondEnd)
Whether segments firstStart - firstEnd and secondStart - secondEnd cross or touch within kTolerance.
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
void appendCurveTrimMesh(const CurveWire &outerWire, const std::vector< CurveWire > &innerWires, 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 loops with the holes br...
double dot(const Vec3 &firstVector, const Vec3 &secondVector)
bool leavesIntoMaterial(const Vec2 &previous, const Vec2 &vertex, const Vec2 &next, const Vec2 &direction)
Whether direction leaves ring corner vertex into the material, which lies left of previous -> vertex ...
Vec3 operator+(const Vec3 &firstVector, const Vec3 &secondVector)
std::vector< int > orientedRing(const std::vector< Vec2 > &points, int first, int count, bool counterClockwise)
The indices first .. first + count - 1 as a ring wound as counterClockwise asks.
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
std::vector< int > bridgeHoles(const std::vector< Vec2 > &outer, const std::vector< std::vector< Vec2 > > &holeLoops, std::vector< Vec2 > &points)
The ring of outer with each hole bridged in at its nearest visible vertex, indexing points,...
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 coversRing(const std::vector< Vec2 > &points, const std::vector< int > &ring, const std::vector< std::array< int, 3 > > &triangles)
Whether triangles cover the area enclosed by ring.
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,...
std::vector< std::array< int, 3 > > triangulateRing(const std::vector< Vec2 > &points, std::vector< int > remainingIndices)
Ear-clipping triangulation of a ring of indices into points; an index may repeat, as at the two ends ...
double unwrapAngleInto(double angle, double uMin, double uMax)
Shift angle by whole turns to lie as close as possible to the window [uMin, uMax].
std::vector< std::array< int, 3 > > triangulateRingByShortestChord(const std::vector< Vec2 > &uvPoints, const std::vector< Vec3 > &points, std::vector< int > remainingIndices)
Ear-clipping of a ring into uvPoints that clips the ear with the shortest 3D diagonal first,...
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,...
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
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