Pangolin
Header-only C++20 plane computational geometry library
Loading...
Searching...
No Matches
minkowski.hpp
Go to the documentation of this file.
1#pragma once
2
4
53
54#include <algorithm>
55#include <cstddef>
56#include <optional>
57#include <stdexcept>
58#include <type_traits>
59#include <utility>
60#include <vector>
61
62
63namespace pgl {
64
65namespace detail {
66
68template <class T> struct minkowskiPointOf { using type = typename T::PointType; };
69template <class Number, class Label> struct minkowskiPointOf<Point<Number, Label>> { using type = Point<Number, Label>; };
70template <class PointType> struct minkowskiPointOf<Shape<PointType>> { using type = PointType; };
71template <class T> using minkowskiPointOf_t = typename minkowskiPointOf<std::remove_cvref_t<T>>::type;
72
80template <class A, class B>
81using minkowskiPoint_t = Point<
82 std::common_type_t<typename minkowskiPointOf_t<A>::NumberType,
83 typename minkowskiPointOf_t<B>::NumberType>,
84 typename minkowskiPointOf_t<A>::LabelType>;
85
97template <class A, class B>
98using minkowskiRegionPoint_t = Point<
99 std::conditional_t<is_halfplane_intersection_v<A> || is_halfplane_intersection_v<B>,
100 division_result_t<typename minkowskiPoint_t<A, B>::NumberType>,
103
104template <class A, class B>
105 requires MinkowskiSummableConcept<A, B>
106constexpr auto minkowskiSumOf(const A& a, const B& b);
107
115template <class ShapeT, class TranslationNumber, class TranslationLabel>
116constexpr auto minkowskiTranslated(const ShapeT& shape,
119 using ResultPoint = minkowskiPoint_t<ShapeT, Translation>;
120 using ResultNumber = typename ResultPoint::NumberType;
121
122 // Moves one vertex, promoting the coordinates exactly as Point + Point does.
123 const auto moved = [&translation](const auto& vertex) {
124 return ResultPoint(
125 detail::asNumber<ResultNumber>(vertex.x()) + detail::asNumber<ResultNumber>(translation.x()),
126 detail::asNumber<ResultNumber>(vertex.y()) + detail::asNumber<ResultNumber>(translation.y()));
127 };
128
129 if constexpr (is_point_v<ShapeT>) {
130 return moved(shape);
131 } else if constexpr (is_empty_shape_v<ShapeT>) {
132 // The empty set has no point to move; only the point type promotes.
133 return EmptyShape<ResultPoint>{};
134 } else if constexpr (is_shape_v<ShapeT>) {
135 return std::visit(
136 [&translation](const auto& alternative) {
137 return Shape<ResultPoint>(minkowskiTranslated(alternative, translation));
138 },
139 shape.variant());
140 } else {
141 using Label = typename ShapeT::LabelType;
142
143 if constexpr (is_segment_v<ShapeT>) {
144 return Segment<ResultPoint, Label>(moved(shape.min()), moved(shape.max()));
145 } else if constexpr (is_oriented_segment_v<ShapeT>) {
146 return OrientedSegment<ResultPoint, Label>(moved(shape.source()), moved(shape.target()));
147 } else if constexpr (is_line_v<ShapeT>) {
148 return Line<ResultPoint, Label>(moved(shape.min()), moved(shape.max()));
149 } else if constexpr (is_oriented_line_v<ShapeT>) {
150 return OrientedLine<ResultPoint, Label>(moved(shape.source()), moved(shape.target()));
151 } else if constexpr (is_ray_v<ShapeT>) {
152 return Ray<ResultPoint, Label>(moved(shape.source()), moved(shape.target()));
153 } else if constexpr (is_halfplane_v<ShapeT>) {
154 return Halfplane<ResultPoint, Label>(moved(shape.source()), moved(shape.target()));
155 } else if constexpr (is_rectangle_v<ShapeT>) {
156 // Translation preserves the corner order, so the result needs no
157 // normalizing -- and an empty rectangle stays empty.
158 return Rectangle<ResultPoint, Label>(moved(shape.min()), moved(shape.max()), true);
159 } else if constexpr (is_triangle_v<ShapeT>) {
160 return Triangle<ResultPoint, Label>(moved(shape.a()), moved(shape.b()), moved(shape.c()));
161 } else if constexpr (is_disk_v<ShapeT>) {
162 return Disk<ResultPoint, Label>(moved(shape.a()), moved(shape.b()), moved(shape.c()));
163 } else {
164 // Vector-backed shapes translate in place: their own operator+=
165 // keeps whatever cached state and canonical order the shape
166 // maintains, which rebuilding from vertices would throw away.
167 const auto translate = [&translation](auto result) {
168 result += translation;
169 if constexpr (has_label_v<Label>) {
170 result.label() = Label{};
171 }
172 return result;
173 };
174
175 if constexpr (is_convex_v<ShapeT>) {
176 return translate(Convex<ResultPoint, Label>(shape));
177 } else if constexpr (is_polygon_v<ShapeT>) {
178 return translate(Polygon<ResultPoint, Label>(shape));
179 } else if constexpr (is_monotone_chain_v<ShapeT>) {
180 return translate(MonotoneChain<ResultPoint, Label>(shape));
181 } else if constexpr (is_polyline_v<ShapeT>) {
182 return translate(Polyline<ResultPoint, Label>(shape));
183 } else if constexpr (is_polygon_with_holes_v<ShapeT>) {
184 return translate(PolygonWithHoles<ResultPoint, Label>(shape));
185 } else if constexpr (is_polygon_set_v<ShapeT>) {
186 return translate(PolygonSet<ResultPoint, Label>(shape));
187 } else {
188 static_assert(is_halfplane_intersection_v<ShapeT>,
189 "minkowskiTranslated has no branch for this shape kind: every "
190 "shape is closed under translation, so a new one needs one here");
191 return translate(HalfplaneIntersection<ResultPoint, Label>(shape));
192 }
193 }
194 }
195}
196
204template <class ResultPoint, class ShapeT>
205constexpr std::vector<ResultPoint> minkowskiVertices(const ShapeT& shape) {
206 using ResultNumber = typename ResultPoint::NumberType;
207
208 std::vector<ResultPoint> vertices;
209 const auto append = [&vertices](const auto& vertex) {
210 vertices.emplace_back(detail::asNumber<ResultNumber>(vertex.x()),
211 detail::asNumber<ResultNumber>(vertex.y()));
212 };
213
214 if constexpr (is_convex_v<ShapeT>) {
215 // Convex already stores its hull counterclockwise with the collinear
216 // vertices pruned, so a scan here would be pure overhead.
217 vertices.reserve(shape.size());
218 for (const auto& vertex : shape) {
219 append(vertex);
220 }
221 return vertices;
222 } else {
223 // At most four vertices, and any of them may be degenerate (a flat
224 // triangle, a zero-width rectangle, a collapsed segment), so the scan
225 // both orders them and drops what collapsed. An empty shape has no
226 // vertices to read, and its caller absorbs the empty list.
227 if (coversNoPoint(shape)) {
228 return vertices;
229 }
230 for (const auto& vertex : shape.vertices()) {
231 append(vertex);
232 }
233 return grahamScan(vertices);
234 }
235}
236
262template <class HalfplaneT, class ShapeT>
263constexpr auto minkowskiHalfplaneSum(const HalfplaneT& halfplane, const ShapeT& shape) {
264 using ResultPoint = minkowskiPoint_t<HalfplaneT, ShapeT>;
265 using ResultNumber = typename ResultPoint::NumberType;
267
268 const auto& source = halfplane.source();
269 const auto& target = halfplane.target();
270 const ResultNumber dx = detail::asNumber<ResultNumber>(target.x()) - detail::asNumber<ResultNumber>(source.x());
271 const ResultNumber dy = detail::asNumber<ResultNumber>(target.y()) - detail::asNumber<ResultNumber>(source.y());
272
273 bool found = false;
274 ResultPoint support(ResultNumber{}, ResultNumber{});
275 ResultNumber best{};
276 for (const auto& vertex : shape.vertices()) {
277 const ResultNumber x = static_cast<ResultNumber>(vertex.x());
278 const ResultNumber y = static_cast<ResultNumber>(vertex.y());
279 const ResultNumber side = dx * y - dy * x; // cross(d, vertex)
280 if (!found || side < best) {
281 found = true;
282 best = side;
283 support = ResultPoint(x, y);
284 }
285 }
286 if (!found) {
287 return ResultHalfplane(ResultPoint(detail::asNumber<ResultNumber>(source.x()),
288 detail::asNumber<ResultNumber>(source.y())),
289 ResultPoint(detail::asNumber<ResultNumber>(target.x()),
290 detail::asNumber<ResultNumber>(target.y())));
291 }
292 const auto moved = [&support](const auto& point) {
293 return ResultPoint(detail::asNumber<ResultNumber>(point.x()) + support.x(),
294 detail::asNumber<ResultNumber>(point.y()) + support.y());
295 };
296 return ResultHalfplane(moved(source), moved(target));
297}
298
323template <class ResultPoint>
324struct MinkowskiPolyhedron {
326 std::vector<ResultPoint> directions;
328 std::vector<ResultPoint> anchors;
330 std::vector<ResultPoint> recessions;
332 bool empty = false;
333};
334
336template <class ResultPoint>
337constexpr typename ResultPoint::NumberType minkowskiCross(const ResultPoint& u,
338 const ResultPoint& v) {
339 return u.x() * v.y() - u.y() * v.x();
340}
341
355template <class ResultPoint, class ShapeT>
356constexpr MinkowskiPolyhedron<ResultPoint> minkowskiPolyhedronOf(const ShapeT& shape) {
357 using ResultNumber = typename ResultPoint::NumberType;
358
359 MinkowskiPolyhedron<ResultPoint> polyhedron;
360 const ResultNumber zero{};
361 const auto cast = [](const auto& point) {
362 return ResultPoint(detail::asNumber<ResultNumber>(point.x()),
363 detail::asNumber<ResultNumber>(point.y()));
364 };
365 const auto reversed = [](const ResultPoint& vector) {
366 return ResultPoint(-vector.x(), -vector.y());
367 };
368
369 if constexpr (is_point_v<ShapeT>) {
370 // A single point bounds every direction and recedes in none. The
371 // dispatcher translates instead of coming here, so this is only for
372 // completeness.
373 polyhedron.anchors.push_back(cast(shape));
374 } else if constexpr (is_line_v<ShapeT> || is_oriented_line_v<ShapeT> || is_ray_v<ShapeT> ||
375 is_halfplane_v<ShapeT>) {
376 const ResultPoint source = cast(shape[0]);
377 const ResultPoint target = cast(shape[1]);
378 const ResultPoint forward(target.x() - source.x(), target.y() - source.y());
379 polyhedron.anchors.push_back(source);
380 if constexpr (is_halfplane_v<ShapeT>) {
381 // One constraint, and a recession cone that is the half-plane
382 // itself: it runs off along its boundary both ways and inward along
383 // the normal, so `cross(d, ·) >= 0` on those three holds exactly for
384 // the positive multiples of the boundary direction.
385 polyhedron.directions.push_back(forward);
386 polyhedron.recessions.push_back(forward);
387 polyhedron.recessions.push_back(reversed(forward));
388 polyhedron.recessions.emplace_back(-forward.y(), forward.x());
389 } else if constexpr (is_ray_v<ShapeT>) {
390 // Two constraints pin the ray to its supporting line and one caps it
391 // at the source; the cap's boundary direction is the source-ward
392 // normal, since `cross((dy, -dx), p)` is `dot(d, p)`.
393 polyhedron.directions.push_back(forward);
394 polyhedron.directions.push_back(reversed(forward));
395 polyhedron.directions.emplace_back(forward.y(), -forward.x());
396 polyhedron.recessions.push_back(forward);
397 } else {
398 // A line is capped nowhere and recedes both ways, so a direction
399 // bounds it only when it is parallel to it.
400 polyhedron.directions.push_back(forward);
401 polyhedron.directions.push_back(reversed(forward));
402 polyhedron.recessions.push_back(forward);
403 polyhedron.recessions.push_back(reversed(forward));
404 }
405 } else if constexpr (is_halfplane_intersection_v<ShapeT>) {
406 if (shape.empty()) {
407 polyhedron.empty = true;
408 return polyhedron;
409 }
410 const std::size_t n = shape.size();
411 if (n == 0) {
412 // The whole plane constrains nothing and recedes everywhere.
413 const ResultNumber one = static_cast<ResultNumber>(1);
414 polyhedron.anchors.emplace_back(zero, zero);
415 polyhedron.recessions.emplace_back(one, zero);
416 polyhedron.recessions.emplace_back(-one, zero);
417 polyhedron.recessions.emplace_back(zero, one);
418 polyhedron.recessions.emplace_back(zero, -one);
419 return polyhedron;
420 }
421
422 std::vector<ResultPoint> sources;
423 std::vector<ResultNumber> offsets;
424 sources.reserve(n);
425 offsets.reserve(n);
426 polyhedron.directions.reserve(n);
427 for (const auto& halfplane : shape) {
428 const ResultPoint source = cast(halfplane.source());
429 const ResultPoint target = cast(halfplane.target());
430 const ResultPoint direction(target.x() - source.x(), target.y() - source.y());
431 sources.push_back(source);
432 polyhedron.directions.push_back(direction);
433 offsets.push_back(minkowskiCross(direction, source));
434 }
435 if (n == 1) {
436 // One stored constraint *is* a half-plane, and its recession cone is
437 // two-dimensional, which no pair of edge directions would report.
438 const ResultPoint& forward = polyhedron.directions.front();
439 polyhedron.anchors.push_back(sources.front());
440 polyhedron.recessions.push_back(forward);
441 polyhedron.recessions.push_back(reversed(forward));
442 polyhedron.recessions.emplace_back(-forward.y(), forward.x());
443 return polyhedron;
444 }
445
446 // The stored half-planes are sorted by boundary direction, and a
447 // cyclically consecutive pair meets in a vertex exactly when it turns
448 // left. Where it does not, the region's boundary runs off to infinity
449 // between them: forward along the first edge, backward along the second.
450 for (std::size_t i = 0; i < n; ++i) {
451 const std::size_t next = (i + 1) % n;
452 const ResultPoint& here = polyhedron.directions[i];
453 const ResultPoint& there = polyhedron.directions[next];
454 const ResultNumber turn = minkowskiCross(here, there);
455 if (turn > zero) {
456 // Cramer's rule on the two boundary lines.
457 polyhedron.anchors.emplace_back(
458 (offsets[i] * there.x() - offsets[next] * here.x()) / turn,
459 (offsets[i] * there.y() - offsets[next] * here.y()) / turn);
460 } else {
461 polyhedron.recessions.push_back(here);
462 polyhedron.recessions.push_back(reversed(there));
463 }
464 }
465 if (polyhedron.anchors.empty()) {
466 // No vertex at all, with two or more constraints: the region is a
467 // slab or a line, and each stored boundary line lies in it whole.
468 polyhedron.anchors = std::move(sources);
469 }
470 } else {
471 // Bounded and convex: the hull vertices, counterclockwise and with the
472 // collinear ones dropped, are both the anchors and the edge directions.
473 std::vector<ResultPoint> vertices = minkowskiVertices<ResultPoint>(shape);
474 if (vertices.empty()) {
475 polyhedron.empty = true;
476 return polyhedron;
477 }
478 if (vertices.size() >= 2) {
479 // A shape that collapsed to a segment contributes both directions,
480 // as the two edges of the degenerate polygon it is.
481 polyhedron.directions.reserve(vertices.size());
482 for (std::size_t i = 0; i < vertices.size(); ++i) {
483 const ResultPoint& from = vertices[i];
484 const ResultPoint& to = vertices[(i + 1) % vertices.size()];
485 polyhedron.directions.emplace_back(to.x() - from.x(), to.y() - from.y());
486 }
487 }
488 polyhedron.anchors = std::move(vertices);
489 }
490 return polyhedron;
491}
492
501template <class ResultPoint>
502constexpr std::optional<ResultPoint> minkowskiInfimumPoint(
503 const MinkowskiPolyhedron<ResultPoint>& polyhedron, const ResultPoint& direction) {
504 using ResultNumber = typename ResultPoint::NumberType;
505
506 for (const auto& recession : polyhedron.recessions) {
507 if (crossSign(direction, recession) < 0) {
508 return std::nullopt;
509 }
510 }
511 const ResultPoint* best = nullptr;
512 ResultNumber least{};
513 for (const auto& anchor : polyhedron.anchors) {
514 const ResultNumber value = minkowskiCross(direction, anchor);
515 if (best == nullptr || value < least) {
516 best = &anchor;
517 least = value;
518 }
519 }
520 if (best == nullptr) {
521 return std::nullopt;
522 }
523 return *best;
524}
525
547template <class A, class B>
548constexpr auto minkowskiPolyhedralSum(const A& a, const B& b) {
549 using ResultPoint = minkowskiRegionPoint_t<A, B>;
551
552 const MinkowskiPolyhedron<ResultPoint> left = minkowskiPolyhedronOf<ResultPoint>(a);
553 const MinkowskiPolyhedron<ResultPoint> right = minkowskiPolyhedronOf<ResultPoint>(b);
554 if (left.empty || right.empty) {
555 // Nothing to add to anything: the empty convex polygon is the region
556 // constructor that spells the empty region.
557 return Region(Convex<ResultPoint>());
558 }
559
560 Region region;
561 const auto clamp = [&region, &left, &right](const ResultPoint& direction) {
562 const std::optional<ResultPoint> here = minkowskiInfimumPoint(left, direction);
563 if (!here) {
564 return;
565 }
566 const std::optional<ResultPoint> there = minkowskiInfimumPoint(right, direction);
567 if (!there) {
568 return;
569 }
570 const ResultPoint base(here->x() + there->x(), here->y() + there->y());
571 region.insert(Halfplane<ResultPoint>(
572 base, ResultPoint(base.x() + direction.x(), base.y() + direction.y())));
573 };
574 for (const auto& direction : left.directions) {
575 clamp(direction);
576 }
577 for (const auto& direction : right.directions) {
578 clamp(direction);
579 }
580 return region;
581}
582
594template <class PointType>
595constexpr int minkowskiDirectionOrder(const PointType& u, const PointType& v) {
596 using Number = typename PointType::NumberType;
597
598 const auto half = [](const PointType& direction) {
599 const Number origin{};
600 return (direction.y() < origin || (direction.y() == origin && direction.x() < origin)) ? 1
601 : 0;
602 };
603
604 const int halfU = half(u);
605 const int halfV = half(v);
606 if (halfU != halfV) {
607 return halfU < halfV ? -1 : 1;
608 }
609
610 const auto turn = crossSign(u, v);
611 if (turn > 0) {
612 return -1; // v lies counterclockwise from u, so u has the smaller angle
613 }
614 if (turn < 0) {
615 return 1;
616 }
617 return 0;
618}
619
629template <class A, class B>
630constexpr auto minkowskiConvexSum(const A& a, const B& b) {
631 using ResultPoint = minkowskiPoint_t<A, B>;
632 using ResultNumber = typename ResultPoint::NumberType;
633
634 std::vector<ResultPoint> left = minkowskiVertices<ResultPoint>(a);
635 std::vector<ResultPoint> right = minkowskiVertices<ResultPoint>(b);
636 if (left.empty() || right.empty()) {
637 return Convex<ResultPoint>(); // an empty convex polygon absorbs
638 }
639
640 // Start each walk at its bottom-most (then leftmost) vertex: the outgoing
641 // edge there has an angle in [0, π), so the edges that follow it are sorted
642 // by angle over the whole [0, 2π) turn.
643 const auto rotateToLowest = [](std::vector<ResultPoint>& vertices) {
644 auto lowest = vertices.begin();
645 for (auto it = vertices.begin() + 1; it != vertices.end(); ++it) {
646 if (it->y() < lowest->y() || (it->y() == lowest->y() && it->x() < lowest->x())) {
647 lowest = it;
648 }
649 }
650 std::rotate(vertices.begin(), lowest, vertices.end());
651 };
652 rotateToLowest(left);
653 rotateToLowest(right);
654
655 // A one-vertex operand contributes no edge, which is what makes the merge
656 // below degrade gracefully into a plain translation.
657 const auto edgesOf = [](const std::vector<ResultPoint>& vertices) {
658 std::vector<ResultPoint> edges;
659 if (vertices.size() >= 2) {
660 edges.reserve(vertices.size());
661 for (std::size_t i = 0; i < vertices.size(); ++i) {
662 const ResultPoint& from = vertices[i];
663 const ResultPoint& to = vertices[(i + 1) % vertices.size()];
664 edges.emplace_back(to.x() - from.x(), to.y() - from.y());
665 }
666 }
667 return edges;
668 };
669 const std::vector<ResultPoint> leftEdges = edgesOf(left);
670 const std::vector<ResultPoint> rightEdges = edgesOf(right);
671
672 std::vector<ResultPoint> boundary;
673 boundary.reserve(leftEdges.size() + rightEdges.size() + 1);
674 ResultPoint current(left.front().x() + right.front().x(),
675 left.front().y() + right.front().y());
676 boundary.push_back(current);
677
678 std::size_t i = 0;
679 std::size_t j = 0;
680 while (i < leftEdges.size() || j < rightEdges.size()) {
681 ResultPoint step(ResultNumber{}, ResultNumber{});
682 if (j == rightEdges.size()) {
683 step = leftEdges[i++];
684 } else if (i == leftEdges.size()) {
685 step = rightEdges[j++];
686 } else {
687 const int order = minkowskiDirectionOrder(leftEdges[i], rightEdges[j]);
688 if (order < 0) {
689 step = leftEdges[i++];
690 } else if (order > 0) {
691 step = rightEdges[j++];
692 } else {
693 // Equal directions fuse, which is what keeps the result free of
694 // collinear vertices.
695 step = ResultPoint(leftEdges[i].x() + rightEdges[j].x(),
696 leftEdges[i].y() + rightEdges[j].y());
697 ++i;
698 ++j;
699 }
700 }
701 current = ResultPoint(current.x() + step.x(), current.y() + step.y());
702 boundary.push_back(current);
703 }
704 if (boundary.size() > 1) {
705 boundary.pop_back(); // the walk closed back onto its first vertex
706 }
707
708 // Convex stores its hull from the lexicographically smallest vertex; the
709 // walk above starts from the bottom-most one instead.
710 std::rotate(boundary.begin(), std::min_element(boundary.begin(), boundary.end()),
711 boundary.end());
712 return Convex<ResultPoint>(std::move(boundary), true);
713}
714
721template <class A, class B>
722 requires MinkowskiSummableConcept<A, B>
723constexpr auto minkowskiSumOf(const A& a, const B& b) {
724 using ResultPoint = minkowskiPoint_t<A, B>;
725
726 if constexpr (is_shape_v<A> || is_shape_v<B>) {
727 using ResultShape = Shape<ResultPoint>;
728 // Only the pair of stored alternatives decides whether the sum exists,
729 // and that is not known until run time.
730 // The constructibility is part of the test, not a consequence of it: a
731 // pair whose sum is a HalfplaneIntersection over rational coordinates
732 // has an answer, but not one a wrapper over integral points can hold.
733 const auto sum = [](const auto& left, const auto& right) -> ResultShape {
734 if constexpr (requires { ResultShape(minkowskiSumOf(left, right)); }) {
735 return ResultShape(minkowskiSumOf(left, right));
736 } else {
737 throw std::logic_error(
738 "Shape::minkowskiSum is not defined for this pair of alternatives, or its "
739 "result does not fit the wrapper's point type");
740 }
741 };
742 if constexpr (is_shape_v<A> && is_shape_v<B>) {
743 return std::visit(sum, a.variant(), b.variant());
744 } else if constexpr (is_shape_v<A>) {
745 return std::visit([&b, &sum](const auto& left) { return sum(left, b); }, a.variant());
746 } else {
747 return std::visit([&a, &sum](const auto& right) { return sum(a, right); }, b.variant());
748 }
749 } else if constexpr (is_empty_shape_v<A> || is_empty_shape_v<B>) {
750 return EmptyShape<ResultPoint>{};
751 } else if constexpr (is_point_v<B>) {
752 return minkowskiTranslated(a, b);
753 } else if constexpr (is_point_v<A>) {
754 return minkowskiTranslated(b, a);
755 } else if constexpr (is_rectangle_v<A> && is_rectangle_v<B>) {
756 // Two axis-aligned rectangles are the one non-trivial pair closed under
757 // the sum: opposite corners simply add, minima to minima and maxima to
758 // maxima. Sweeping anything over the empty set covers nothing.
759 if (a.empty() || b.empty()) {
760 return Rectangle<ResultPoint>();
761 }
762 return Rectangle<ResultPoint>(minkowskiSumOf(a.min(), b.min()),
763 minkowskiSumOf(a.max(), b.max()), true);
764 } else if constexpr (is_halfplane_v<A> && !UnboundedConvexConcept<B>) {
765 // A half-plane absorbs anything bounded, convex or not, and stays one.
766 return minkowskiHalfplaneSum(a, b);
767 } else if constexpr (is_halfplane_v<B> && !UnboundedConvexConcept<A>) {
768 return minkowskiHalfplaneSum(b, a);
769 } else if constexpr (UnboundedConvexConcept<A> || UnboundedConvexConcept<B>) {
770 // One operand at least is stored as constraints rather than as
771 // vertices, and so is their sum.
772 return minkowskiPolyhedralSum(a, b);
773 } else {
774 return minkowskiConvexSum(a, b);
775 }
776}
777
778} // namespace detail
779
780// -----------------------------------------------------------------------------
781// Member entry points
782//
783// Each shape forwards to the same dispatcher; the pairs a shape actually
784// accepts are fixed by MinkowskiSummableConcept on the declaration.
785
786#define PGL_DEFINE_MINKOWSKI_SUM(SHAPE) \
787 template <class PointType, class LabelType> \
788 template <class OtherShape> \
789 requires MinkowskiSummableConcept<SHAPE<PointType, LabelType>, OtherShape> \
790 constexpr auto SHAPE<PointType, LabelType>::minkowskiSum(const OtherShape& other) const { \
791 return detail::minkowskiSumOf(*this, other); \
792 }
793
809
810#undef PGL_DEFINE_MINKOWSKI_SUM
811
812// The one curved pair with an answer: centres add, radii add. It carries a
813// ResultNumber of its own because the radii are square roots of what a disk
814// stores, so this sum leaves the lattice where every other single-shape sum
815// stays on it.
816template <class PointType_, class TLabel>
817template <class ResultNumber, DiskConcept OtherDisk>
819Disk<PointType_, TLabel>::minkowskiSum(const OtherDisk& other) const {
820 using ResultPoint = Point<ResultNumber, PointLabelType>;
821
822 const auto leftCenter = center<ResultNumber>();
823 const auto rightCenter = other.template center<ResultNumber>();
824 const ResultNumber radii =
825 radius<ResultNumber>() + other.template radius<ResultNumber>();
826 return Disk<ResultPoint>(ResultPoint(leftCenter.x() + rightCenter.x(),
827 leftCenter.y() + rightCenter.y()),
828 radii);
829}
830
831// The other sum whose support point is not a vertex: a half-plane slides out by
832// the disk's radius along its own unit normal, and both of those are square
833// roots. Same ResultNumber convention as the disk pair.
834template <class PointType_, class TLabel>
835template <class ResultNumber, DiskConcept OtherDisk>
837Halfplane<PointType_, TLabel>::minkowskiSum(const OtherDisk& other) const {
839
840 const ResultNumber dx = detail::asNumber<ResultNumber>(target().x()) - detail::asNumber<ResultNumber>(source().x());
841 const ResultNumber dy = detail::asNumber<ResultNumber>(target().y()) - detail::asNumber<ResultNumber>(source().y());
842 // Reported the way Disk::radius reports it: an exact result type has no
843 // square root to offer, and says so rather than rounding silently.
844 if constexpr (!requires(ResultNumber v) { std::sqrt(v); }) {
845 throw std::runtime_error("std::sqrt is not available for the requested ResultNumber type");
846 } else {
847 const ResultNumber length = std::sqrt(dx * dx + dy * dy);
848
849 // The outward normal of `{p : cross(d, p - s) >= 0}` is `(dy, -dx)/|d|`,
850 // and the disk's support point in it is its centre moved r that way.
851 const auto center = other.template center<ResultNumber>();
852 const ResultNumber radius = other.template radius<ResultNumber>();
853 const ResultNumber offsetX = center.x() + radius * dy / length;
854 const ResultNumber offsetY = center.y() - radius * dx / length;
855
856 const auto moved = [&offsetX, &offsetY](const auto& point) {
857 return ResultPoint(detail::asNumber<ResultNumber>(point.x()) + offsetX,
858 detail::asNumber<ResultNumber>(point.y()) + offsetY);
859 };
860 return Halfplane<ResultPoint>(moved(source()), moved(target()));
861 }
862}
863
864template <class PointType_, class TLabel>
865template <class ResultNumber, HalfplaneConcept OtherHalfplane>
867Disk<PointType_, TLabel>::minkowskiSum(const OtherHalfplane& other) const {
868 return other.template minkowskiSum<ResultNumber>(*this);
869}
870
871template <class Number, class Label>
872template <class OtherShape>
874constexpr auto Point<Number, Label>::minkowskiSum(const OtherShape& other) const {
875 return detail::minkowskiSumOf(*this, other);
876}
877
878template <class PointType>
879template <class OtherShape>
881constexpr auto EmptyShape<PointType>::minkowskiSum(const OtherShape& other) const {
882 return detail::minkowskiSumOf(*this, other);
883}
884
885template <class PointType, class LabelType, class Storage>
886template <class OtherShape>
888constexpr auto MonotoneChain<PointType, LabelType, Storage>::minkowskiSum(const OtherShape& other) const {
889 return detail::minkowskiSumOf(*this, other);
890}
891
892template <class PointType>
893template <class OtherShape>
895constexpr auto Shape<PointType>::minkowskiSum(const OtherShape& other) const {
896 return detail::minkowskiSumOf(*this, other);
897}
898
907template <class A, class B>
909[[nodiscard]] constexpr auto operator+(const A& a, const B& b) {
910 return a.minkowskiSum(b);
911}
912
913} // namespace pgl
Shape pairs whose Minkowski sum Pangolin can represent.
Definition forward.hpp:476
#define PGL_DEFINE_MINKOWSKI_SUM(SHAPE)
Definition minkowski.hpp:786
Definition arrangement.hpp:67
HalfplaneIntersection() -> HalfplaneIntersection< Point<>, NoLabel >
Definition halfplaneintersection.hpp:2308
@ y
Definition intervaltree.hpp:24
@ x
Definition intervaltree.hpp:24
Rectangle() -> Rectangle< Point<>, NoLabel >
Definition rectangle.hpp:2384
@ vertex
Definition bitmatrix.hpp:37
Line() -> Line< Point<>, NoLabel >
Point() -> Point< int >
auto grahamScan(const Container &points_)
Computes the convex hull of a point container using Graham's scan.
Definition convexhull.hpp:177
PolygonSet() -> PolygonSet< Point<>, NoLabel >
Definition polygonset.hpp:1699
OrientedSegment() -> OrientedSegment< Point<>, NoLabel >
constexpr std::partial_ordering crossSign(const Point< UNumber, ULabel > &u, const Point< VNumber, VLabel > &v)
Classifies the turn from one vector to another.
Definition orientation.hpp:583
MonotoneChain() -> MonotoneChain< Point<>, NoLabel >
Definition monotonechain.hpp:2439
Shape(const std::variant< T, Ts... > &) -> Shape< detail::shape_point_type_t< T > >
PolygonWithHoles() -> PolygonWithHoles< Point<>, NoLabel >
Definition polygonwithholes.hpp:3093
Convex() -> Convex< Point<>, NoLabel >
Definition convex.hpp:3311
Segment() -> Segment< Point<>, NoLabel >
Halfplane() -> Halfplane< Point<>, NoLabel >
BitMatrix< PointType > operator+(const PointType &vector, const BitMatrix< PointType > &matrix)
Returns the same cells translated by a vector.
Definition bitmatrix.hpp:2660
Polyline() -> Polyline< Point<>, NoLabel >
Definition polyline.hpp:2369
Ray() -> Ray< Point<>, NoLabel >
Polygon() -> Polygon< Point<>, NoLabel >
Definition polygon.hpp:3200
Disk() -> Disk< Point<>, NoLabel >
Deduces a default disk with Point<> boundary points and no label.
Definition disk.hpp:1691
OrientedLine() -> OrientedLine< Point<>, NoLabel >
Triangle() -> Triangle< Point<>, NoLabel >
Definition triangle.hpp:2029
constexpr Convex()=default
Creates a convex with no vertex.
Closed Euclidean disk stored by boundary points plus optional disk label.
Definition disk.hpp:66
constexpr Point< ResultNumber, PointLabelType > center() const
Definition disk.hpp:284
constexpr ResultNumber radius() const
Definition disk.hpp:333
constexpr Disk()=default
Creates a disk with all three boundary points at the origin.
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:802
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:881
friend struct HalfplaneIntersection
Definition halfplaneintersection.hpp:2308
Closed half-plane defined by an oriented boundary line.
Definition halfplane.hpp:51
constexpr const PointType & target() const
Returns the target boundary point.
Definition halfplane.hpp:193
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:799
constexpr const PointType & source() const
Returns the source boundary point.
Definition halfplane.hpp:181
constexpr Halfplane()=default
Creates the degenerate half-plane (0,0)->(0,0).
constexpr Line()=default
Creates the degenerate line (0,0)--(0,0).
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:888
constexpr OrientedLine()=default
Creates the degenerate oriented line (0,0)--(0,0).
constexpr OrientedSegment()=default
Creates the degenerate oriented segment (0,0)->(0,0).
Two-dimensional point with optional label payload.
Definition point.hpp:129
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:874
TLabel LabelType
Definition point.hpp:133
TNumber NumberType
Definition point.hpp:131
friend struct PolygonSet
Definition polygonset.hpp:1873
friend struct PolygonWithHoles
Definition polygonwithholes.hpp:3314
constexpr Polygon()=default
Creates a polygon with no vertex.
constexpr Polyline()=default
Creates a polyline with no vertex.
constexpr Ray()=default
Creates the degenerate ray (0,0)--(0,0)->.
constexpr Rectangle()
Creates the empty rectangle [(0,0),(-1,-1)].
Definition rectangle.hpp:120
constexpr Segment()=default
Creates the degenerate segment (0,0)--(0,0).
constexpr auto minkowskiSum(const OtherShape &other) const
Returns the Minkowski sum of this shape and another (A ⊕ B).
Definition minkowski.hpp:895
constexpr Triangle()=default
Creates the degenerate triangle (0,0),(0,0),(0,0).
Arithmetic transformations and explicit conversions between primitives.