Pangolin
Header-only C++20 plane computational geometry library
Loading...
Searching...
No Matches
canvas.hpp
Go to the documentation of this file.
1#pragma once
2
5
10
11#include <algorithm>
12#include <array>
13#include <cctype>
14#include <cmath>
15#include <cstdint>
16#include <fstream>
17#include <limits>
18#include <memory>
19#include <optional>
20#include <ranges>
21#include <sstream>
22#include <stdexcept>
23#include <string>
24#include <tuple>
25#include <utility>
26#include <vector>
27#include <variant>
28
29
30namespace pgl {
31
43
51
56 std::string stroke = "black";
57 std::string fill = "none";
58 std::string fillOpacity = "1";
59 std::string strokeOpacity = "1";
60 std::string strokeWidth = "2";
61 std::string pointRadius = "3";
62
68 void apply(const CanvasCommand& command) {
69 switch (command.property) {
71 stroke = command.value;
72 break;
74 fill = command.value;
75 break;
77 fillOpacity = command.value;
78 break;
80 strokeOpacity = command.value;
81 break;
83 strokeWidth = command.value;
84 break;
86 pointRadius = command.value;
87 break;
88 }
89 }
90};
91
93inline CanvasCommand stroke(std::string value) {
94 return {CanvasProperty::stroke, std::move(value)};
95}
96
98inline CanvasCommand fill(std::string value) {
99 return {CanvasProperty::fill, std::move(value)};
100}
101
103inline CanvasCommand fillOpacity(std::string value) {
104 return {CanvasProperty::fillOpacity, std::move(value)};
105}
106
108inline CanvasCommand strokeOpacity(std::string value) {
109 return {CanvasProperty::strokeOpacity, std::move(value)};
110}
111
113inline CanvasCommand strokeWidth(std::string value) {
114 return {CanvasProperty::strokeWidth, std::move(value)};
115}
116
118inline CanvasCommand pointRadius(std::string value) {
119 return {CanvasProperty::pointRadius, std::move(value)};
120}
121
128class Canvas {
129 public:
133 Canvas() = default;
134
141 Canvas& scale(double factor) {
142 requireStrictlyPositive(factor, "scale");
143 zoom_ = factor;
144 return *this;
145 }
146
153 Canvas& width(double widthPixels) {
154 requireStrictlyPositive(widthPixels, "width");
155 widthPixels_ = widthPixels;
156 return *this;
157 }
158
165 Canvas& height(double heightPixels) {
166 requireStrictlyPositive(heightPixels, "height");
167 heightPixels_ = heightPixels;
168 return *this;
169 }
170
178 Canvas& size(double widthPixels, double heightPixels) {
179 return width(widthPixels).height(heightPixels);
180 }
181
188 Canvas& borders(bool enabled = true) {
189 drawBorder_ = enabled;
190 return *this;
191 }
192
209 template <class PointType>
211 const Rectangle<Point<double>> box(window);
212 Bounds bounds;
213 bounds.include(box.min().x(), box.min().y());
214 bounds.include(box.max().x(), box.max().y());
215 view_ = bounds;
216 return *this;
217 }
218
225 Canvas& margin(double marginPixels) {
226 requireNonNegative(marginPixels, "margin");
227 marginPixels_ = marginPixels;
228 return *this;
229 }
230
236 void writeSVG(const std::string& path) const {
237 std::ofstream output(path);
238 if (!output) {
239 throw std::runtime_error("Could not open SVG output file: " + path);
240 }
241
242 output << toSVG();
243 }
244
250 std::string toSVG() const {
251 const Bounds bounds = computeBounds();
252 const Viewport viewport = computeViewport(bounds);
253
254 std::ostringstream out;
255 appendDocumentOpen(out);
256
257 if (needsArrowheadDefinition()) {
258 appendArrowheadDefinition(out);
259 }
260
261 if (drawBorder_) {
262 appendBorder(out);
263 }
264
265 for (const Element& element : elements_) {
266 out << " " << elementToSVG(element, viewport) << "\n";
267 }
268
269 out << "</svg>\n";
270 return out.str();
271 }
272
282 void writePDF(const std::string& path) const {
283 std::ofstream output(path, std::ios::binary);
284 if (!output) {
285 throw std::runtime_error("Could not open PDF output file: " + path);
286 }
287
288 const std::string pdf = toPDF();
289 output.write(pdf.data(), static_cast<std::streamsize>(pdf.size()));
290 if (!output) {
291 throw std::runtime_error("Could not write PDF output file: " + path);
292 }
293 }
294
300 std::string toPDF() const {
301 const Bounds bounds = computeBounds();
302 const Viewport viewport = computeViewport(bounds);
303
304 pdfgen::pdf_info info{};
305 std::snprintf(info.creator, sizeof(info.creator), "%s", "pgl::Canvas");
306 std::snprintf(info.producer, sizeof(info.producer), "%s", "pgl");
307 std::snprintf(info.title, sizeof(info.title), "%s", "Pangolin Canvas Export");
308
309 std::unique_ptr<pdfgen::pdf_doc, void (*)(pdfgen::pdf_doc*)> pdf(
310 pdfgen::pdf_create(static_cast<float>(widthPixels_), static_cast<float>(heightPixels_), &info),
312 );
313 if (!pdf) {
314 throw std::runtime_error("Could not create PDF document.");
315 }
316
318 if (page == nullptr) {
319 throwPDFError(pdf.get(), "append PDF page");
320 }
321
322 if (drawBorder_) {
323 const int result = pdfgen::pdf_add_rectangle(
324 pdf.get(),
325 page,
326 0.5f,
327 0.5f,
328 static_cast<float>(std::max(widthPixels_ - 1.0, 0.0)),
329 static_cast<float>(std::max(heightPixels_ - 1.0, 0.0)),
330 1.0f,
332 );
333 if (result < 0) {
334 throwPDFError(pdf.get(), "draw PDF border");
335 }
336 }
337
338 for (const Element& element : elements_) {
339 appendElementToPDF(pdf.get(), page, element, viewport);
340 }
341
342 std::string bytes;
343 if (pdfgen::pdf_save_buffer(pdf.get(), bytes) < 0) {
344 throwPDFError(pdf.get(), "serialize PDF");
345 }
346 return bytes;
347 }
348
361 void writeIPE(const std::string& path) const {
362 std::ofstream output(path);
363 if (!output) {
364 throw std::runtime_error("Could not open IPE output file: " + path);
365 }
366
367 output << toIPE();
368 if (!output) {
369 throw std::runtime_error("Could not write IPE output file: " + path);
370 }
371 }
372
378 std::string toIPE() const {
379 const Bounds bounds = computeBounds();
380 const Viewport viewport = computeViewport(bounds);
381 const std::vector<int> opacities = collectIPEOpacities();
382
383 std::ostringstream out;
384 out << "<?xml version=\"1.0\"?>\n";
385 out << "<!DOCTYPE ipe SYSTEM \"ipe.dtd\">\n";
386 out << "<ipe version=\"70218\" creator=\"pgl::Canvas\">\n";
387 out << "<ipestyle name=\"pgl\">\n";
388 out << "<layout paper=\"" << widthPixels_ << ' ' << heightPixels_
389 << "\" origin=\"0 0\" frame=\"" << widthPixels_ << ' ' << heightPixels_ << "\"/>\n";
390 for (const int key : opacities) {
391 out << "<opacity name=\"" << ipeOpacityName(key) << "\" value=\"" << (key / 1000.0) << "\"/>\n";
392 }
393 out << "</ipestyle>\n";
394 out << "<page>\n";
395 out << "<layer name=\"alpha\"/>\n";
396 out << "<view layers=\"alpha\" active=\"alpha\"/>\n";
397
398 if (drawBorder_) {
399 appendIPEBorder(out);
400 }
401
402 for (const Element& element : elements_) {
403 appendElementToIPE(out, element, viewport);
404 }
405
406 out << "</page>\n";
407 out << "</ipe>\n";
408 return out.str();
409 }
410
419 Canvas& operator<<(const CanvasCommand& command) {
420 style_.apply(command);
421 return *this;
422 }
423
425 template <class Number, class Label>
427 return push(Point<double>(point), point);
428 }
429
431 template <class PointType, class Label>
433 return push(Segment<Point<double>>(segment), segment);
434 }
435
437 template <class PointType, class Label>
439 return push(OrientedSegment<Point<double>>(segment), segment);
440 }
441
443 template <class PointType, class Label>
445 return push(Line<Point<double>>(line), line);
446 }
447
449 template <class PointType, class Label>
451 return push(OrientedLine<Point<double>>(line), line);
452 }
453
455 template <class PointType, class Label>
457 return push(Ray<Point<double>>(ray), ray);
458 }
459
461 template <class PointType, class Label>
463 return push(Halfplane<Point<double>>(halfplane), halfplane);
464 }
465
467 template <class PointType, class Label>
469 return push(Rectangle<Point<double>>(rectangle), rectangle);
470 }
471
473 template <class PointType, class Label>
475 return push(Triangle<Point<double>>(triangle), triangle);
476 }
477
479 template <class PointType, class Label>
481 return push(Convex<Point<double>>(convex), convex);
482 }
483
485 template <class PointType, class Label>
487 return push(Disk<Point<double>>(disk), disk);
488 }
489
491 template <class PointType, class Label>
493 return push(Polygon<Point<double>>(polygon), polygon);
494 }
495
497 template <class PointType, class Label>
499 return push(PolygonWithHoles<Point<double>>(region), region);
500 }
501
509 template <class PointType, class Label>
511 return push(PolygonSet<Point<double>>(set), set);
512 }
513
515 template <class PointType, class Label, class Storage>
517 return push(MonotoneChain<Point<double>>(chain), chain);
518 }
519
521 template <class PointType, class Label>
523 return push(Polyline<Point<double>>(polyline), polyline);
524 }
525
527 template <class PointType, class Label>
529 return push(HalfplaneIntersection<Point<double>>(region), region);
530 }
531
533 template <class PointType>
535 return *this;
536 }
537
539 template <class PointType>
541 std::visit(
542 [this](const auto& value) {
543 *this << value;
544 },
545 shape.variant());
546 return *this;
547 }
548
550 template <class... Types>
551 requires (requires(Canvas& canvas, const Types& value) {
552 canvas << value;
553 } && ...)
554 Canvas& operator<<(const std::variant<Types...>& objects) {
555 std::visit(
556 [this](const auto& value) {
557 *this << value;
558 },
559 objects);
560 return *this;
561 }
562
564 template <class Type>
565 requires requires(Canvas& canvas, const Type& value) {
566 canvas << value;
567 }
568 Canvas& operator<<(const std::optional<Type>& object) {
569 if (object) {
570 *this << *object;
571 }
572 return *this;
573 }
574
576 template <std::ranges::input_range Range>
577 requires std::ranges::input_range<const Range> && requires(
578 Canvas& canvas,
579 std::ranges::range_reference_t<const Range> object
580 ) {
581 canvas << object;
582 }
583 Canvas& operator<<(const Range& objects) {
584 for (const auto& object : objects) {
585 *this << object;
586 }
587 return *this;
588 }
589
590 private:
591 struct Bounds {
592 double minX = 0.0;
593 double minY = 0.0;
594 double maxX = 0.0;
595 double maxY = 0.0;
596 bool initialized = false;
597
598 void include(double x, double y) {
599 if (!initialized) {
600 minX = x;
601 minY = y;
602 maxX = x;
603 maxY = y;
604 initialized = true;
605 return;
606 }
607
608 minX = std::min(minX, x);
609 minY = std::min(minY, y);
610 maxX = std::max(maxX, x);
611 maxY = std::max(maxY, y);
612 }
613
614 void include(const Bounds& other) {
615 if (!other.initialized) {
616 return;
617 }
618
619 include(other.minX, other.minY);
620 include(other.maxX, other.maxY);
621 }
622 };
623
624 struct Viewport {
625 double scale = 1.0;
626 double offsetX = 0.0;
627 double offsetY = 0.0;
628
629 double mapX(double x) const {
630 return offsetX + scale * x;
631 }
632
633 double mapY(double y) const {
634 return offsetY - scale * y;
635 }
636
637 double unmapX(double x) const {
638 return (x - offsetX) / scale;
639 }
640
641 double unmapY(double y) const {
642 return (offsetY - y) / scale;
643 }
644 };
645
646 struct Element {
647 Shape<Point<double>> shape{};
648 CanvasStyle style{};
649 std::string title;
650
651 Bounds bounds() const {
652 Bounds b;
653 std::visit([&](const auto& value) {
654 using V = std::decay_t<decltype(value)>;
655 if constexpr (std::same_as<V, Point<double>>) {
656 b.include(value.x(), value.y());
657 } else if constexpr (std::same_as<V, Disk<Point<double>>>) {
658 const auto center = value.template center<double>();
659 const double radius = value.template radius<double>();
660 b.include(center.x() - radius, center.y() - radius);
661 b.include(center.x() + radius, center.y() + radius);
662 } else if constexpr (std::same_as<V, HalfplaneIntersection<Point<double>>>) {
663 // The region may be unbounded: focus on its vertices and,
664 // like the Halfplane alternative, on the points defining
665 // its boundary lines.
666 for (const auto& vertexPoint : value.template vertices<double>()) {
667 b.include(vertexPoint.x(), vertexPoint.y());
668 }
669 for (const auto& halfplane : value) {
670 b.include(halfplane.source().x(), halfplane.source().y());
671 b.include(halfplane.target().x(), halfplane.target().y());
672 }
673 } else if constexpr (std::same_as<V, PolygonWithHoles<Point<double>>>) {
674 // Every hole lies inside the outer ring, so the outer ring
675 // alone bounds the region.
676 for (const auto& vertex : value.outer()) {
677 b.include(vertex.x(), vertex.y());
678 }
679 } else if constexpr (std::same_as<V, PolygonSet<Point<double>>>) {
680 // Same argument, one component at a time.
681 for (const auto& component : value) {
682 for (const auto& vertex : component.outer()) {
683 b.include(vertex.x(), vertex.y());
684 }
685 }
686 } else {
687 for (std::size_t i = 0; i < value.size(); ++i) {
688 b.include(value[i].x(), value[i].y());
689 }
690 }
691 }, shape.variant());
692 return b;
693 }
694 };
695
696 static void requireStrictlyPositive(double value, const char* what) {
697 if (value <= 0.0) {
698 throw std::invalid_argument(std::string("Canvas ") + what + " must be strictly positive.");
699 }
700 }
701
702 static void requireNonNegative(double value, const char* what) {
703 if (value < 0.0) {
704 throw std::invalid_argument(std::string("Canvas ") + what + " must be non-negative.");
705 }
706 }
707
708 static std::string toString(double value) {
709 std::ostringstream out;
710 out << value;
711 return out.str();
712 }
713
714 static std::string trim(const std::string& value) {
715 std::size_t start = 0;
716 while (start < value.size() && std::isspace(static_cast<unsigned char>(value[start])) != 0) {
717 ++start;
718 }
719
720 std::size_t end = value.size();
721 while (end > start && std::isspace(static_cast<unsigned char>(value[end - 1])) != 0) {
722 --end;
723 }
724
725 return value.substr(start, end - start);
726 }
727
728 // The number a style string denotes, together with the suffix that
729 // followed it. `"2px"` parses as {2.0, "px"} and `"25%"` as {25.0, "%"};
730 // anything that is not a number followed by letters or a percent sign does
731 // not parse at all.
732 struct ParsedLength {
733 double value = 0.0;
734 std::string suffix;
735 };
736
737 static std::optional<ParsedLength> parseLength(const std::string& value) {
738 const std::string trimmed = trim(value);
739 if (trimmed.empty()) {
740 return std::nullopt;
741 }
742
743 std::size_t parsedCharacters = 0;
744 try {
745 const double numericValue = std::stod(trimmed, &parsedCharacters);
746 return ParsedLength{numericValue, trimmed.substr(parsedCharacters)};
747 } catch (...) {
748 }
749
750 return std::nullopt;
751 }
752
753 // A length in user units. CSS length units other than `px` (which is the
754 // user unit) are not supported and are rejected rather than misread.
755 static std::optional<double> parseNumericLength(const std::string& value) {
756 const std::optional<ParsedLength> parsed = parseLength(value);
757 if (!parsed || (!parsed->suffix.empty() && lowercase(parsed->suffix) != "px")) {
758 return std::nullopt;
759 }
760 return parsed->value;
761 }
762
763 // An SVG opacity, which is either a number in [0,1] or a percentage.
764 static std::optional<double> parseOpacity(const std::string& value) {
765 const std::optional<ParsedLength> parsed = parseLength(value);
766 if (!parsed) {
767 return std::nullopt;
768 }
769 if (parsed->suffix == "%") {
770 return parsed->value / 100.0;
771 }
772 if (parsed->suffix.empty()) {
773 return parsed->value;
774 }
775 return std::nullopt;
776 }
777
778 static std::string escapeXML(const std::string& value, bool escapeQuotes) {
779 std::ostringstream out;
780 for (const char character : value) {
781 switch (character) {
782 case '&':
783 out << "&amp;";
784 break;
785 case '<':
786 out << "&lt;";
787 break;
788 case '>':
789 out << "&gt;";
790 break;
791 case '"':
792 if (escapeQuotes) {
793 out << "&quot;";
794 } else {
795 out << character;
796 }
797 break;
798 default:
799 out << character;
800 break;
801 }
802 }
803 return out.str();
804 }
805
806 static std::string styleAttributes(const CanvasStyle& style) {
807 std::ostringstream out;
808 out << " stroke=\"" << escapeXML(style.stroke, true) << '"'
809 << " fill=\"" << escapeXML(style.fill, true) << '"'
810 << " fill-opacity=\"" << escapeXML(style.fillOpacity, true) << '"'
811 << " stroke-opacity=\"" << escapeXML(style.strokeOpacity, true) << '"'
812 << " stroke-width=\"" << escapeXML(style.strokeWidth, true) << '"'
813 << " vector-effect=\"non-scaling-stroke\"";
814 return out.str();
815 }
816
817 // The same attributes with the fill turned off, for shapes that are curves
818 // rather than regions: SVG fills an open path against the implicit edge
819 // that closes it, which paints an area no one asked for.
820 static std::string strokeOnlyAttributes(const CanvasStyle& style) {
821 CanvasStyle unfilled = style;
822 unfilled.fill = "none";
823 return styleAttributes(unfilled);
824 }
825
826 static std::string fillAttributes(const CanvasStyle& style) {
827 std::ostringstream out;
828 out << " fill=\"" << escapeXML(style.fill, true) << '"'
829 << " fill-opacity=\"" << escapeXML(style.fillOpacity, true) << '"';
830 return out.str();
831 }
832
833 template <class T>
834 static std::string titleOf(const T& object) {
835 std::ostringstream out;
836 out << object;
837 return out.str();
838 }
839
840 template <class Stored, class Original>
841 Canvas& push(Stored&& stored, const Original& original) {
842 elements_.push_back({
843 Shape<Point<double>>(std::forward<Stored>(stored)),
844 style_,
845 titleOf(original),
846 });
847 return *this;
848 }
849
850 bool needsArrowheadDefinition() const {
851 for (const Element& element : elements_) {
852 if (element.shape.template holdsAlternative<OrientedSegment<Point<double>>>() ||
853 element.shape.template holdsAlternative<OrientedLine<Point<double>>>() ||
854 element.shape.template holdsAlternative<Ray<Point<double>>>()) {
855 return true;
856 }
857 }
858 return false;
859 }
860
861 double padding() const {
862 double value = marginPixels_;
863 if (drawBorder_) {
864 value += borderInset_;
865 }
866
867 for (const Element& element : elements_) {
868 if (const std::optional<double> width = parseNumericLength(element.style.strokeWidth)) {
869 value = std::max(value, marginPixels_ + *width / 2.0);
870 }
871 if (const std::optional<double> radius = parseNumericLength(element.style.pointRadius)) {
872 value = std::max(value, marginPixels_ + *radius);
873 }
874 }
875
876 return value;
877 }
878
879 Bounds computeBounds() const {
880 if (view_) {
881 return *view_;
882 }
883
884 Bounds bounds;
885 for (const Element& element : elements_) {
886 bounds.include(element.bounds());
887 }
888 return bounds;
889 }
890
891 Viewport computeViewport(const Bounds& bounds) const {
892 Viewport viewport;
893 if (!bounds.initialized) {
894 viewport.offsetX = widthPixels_ / 2.0;
895 viewport.offsetY = heightPixels_ / 2.0;
896 return viewport;
897 }
898
899 const double inset = padding();
900 const double drawableWidth = std::max(widthPixels_ - 2.0 * inset, 1.0);
901 const double drawableHeight = std::max(heightPixels_ - 2.0 * inset, 1.0);
902 const double contentWidth = bounds.maxX - bounds.minX;
903 const double contentHeight = bounds.maxY - bounds.minY;
904
905 const double scaleX = contentWidth == 0.0 ? pgl::detail::numeric_limits<double>::infinity() : drawableWidth / contentWidth;
906 const double scaleY = contentHeight == 0.0 ? pgl::detail::numeric_limits<double>::infinity() : drawableHeight / contentHeight;
907
908 double fittedScale = std::min(scaleX, scaleY);
909 if (!std::isfinite(fittedScale)) {
910 fittedScale = 1.0;
911 }
912
913 viewport.scale = fittedScale * zoom_;
914
915 const double drawnWidth = contentWidth * viewport.scale;
916 const double drawnHeight = contentHeight * viewport.scale;
917 const double extraX = (drawableWidth - drawnWidth) / 2.0;
918 const double extraY = (drawableHeight - drawnHeight) / 2.0;
919
920 viewport.offsetX = inset + extraX - bounds.minX * viewport.scale;
921 viewport.offsetY = heightPixels_ - inset - extraY + bounds.minY * viewport.scale;
922 return viewport;
923 }
924
925 void appendDocumentOpen(std::ostringstream& out) const {
926 out << "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\""
927 << widthPixels_ << "\" height=\"" << heightPixels_
928 << "\" viewBox=\"0 0 " << widthPixels_ << ' ' << heightPixels_ << "\">\n";
929 }
930
931 static void appendArrowheadDefinition(std::ostringstream& out) {
932 out << " <defs>\n";
933 out << " <marker id=\"pgl-arrowhead\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"4\" markerHeight=\"4\" orient=\"auto\" markerUnits=\"strokeWidth\">\n";
934 out << " <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"context-stroke\" stroke=\"none\"/>\n";
935 out << " </marker>\n";
936 out << " </defs>\n";
937 }
938
939 void appendBorder(std::ostringstream& out) const {
940 out << " <rect x=\"0.5\" y=\"0.5\" width=\"" << std::max(widthPixels_ - 1.0, 0.0)
941 << "\" height=\"" << std::max(heightPixels_ - 1.0, 0.0)
942 << "\" stroke=\"black\" fill=\"none\" stroke-width=\"1\"/>\n";
943 }
944
945 struct PDFStyle {
946 std::uint32_t stroke = pdfgen::PDF_BLACK;
947 std::uint32_t fill = pdfgen::PDF_TRANSPARENT;
948 float strokeWidth = 1.0f;
949 float pointRadius = 3.0f;
950 float fillAlpha = 1.0f;
951 float strokeAlpha = 1.0f;
952 };
953
954 static void throwPDFError(const pdfgen::pdf_doc* pdf, const std::string& action) {
955 int errval = 0;
956 const char* message = pdfgen::pdf_get_err(pdf, &errval);
957 std::ostringstream out;
958 out << "Could not " << action << ": " << (message != nullptr ? message : "unknown PDF error");
959 if (errval != 0) {
960 out << " (" << errval << ')';
961 }
962 throw std::runtime_error(out.str());
963 }
964
965 static float numericLengthOr(const std::string& value, float fallback) {
966 if (const std::optional<double> numeric = parseNumericLength(value)) {
967 return static_cast<float>(*numeric);
968 }
969 return fallback;
970 }
971
972 static float opacityOr(const std::string& value, float fallback) {
973 if (const std::optional<double> numeric = parseOpacity(value)) {
974 if (std::isfinite(*numeric)) {
975 return std::clamp(static_cast<float>(*numeric), 0.0f, 1.0f);
976 }
977 }
978 return fallback;
979 }
980
981 static std::string lowercase(const std::string& value) {
982 std::string result = value;
983 std::transform(result.begin(), result.end(), result.begin(), [](unsigned char character) {
984 return static_cast<char>(std::tolower(character));
985 });
986 return result;
987 }
988
989 static std::optional<int> parseHexDigit(char character) {
990 if ('0' <= character && character <= '9') return character - '0';
991 if ('a' <= character && character <= 'f') return 10 + (character - 'a');
992 if ('A' <= character && character <= 'F') return 10 + (character - 'A');
993 return std::nullopt;
994 }
995
996 static std::optional<unsigned int> parseHexByte(char high, char low) {
997 const auto highNibble = parseHexDigit(high);
998 const auto lowNibble = parseHexDigit(low);
999 if (!highNibble || !lowNibble) {
1000 return std::nullopt;
1001 }
1002 return static_cast<unsigned int>((*highNibble << 4) | *lowNibble);
1003 }
1004
1005 static std::optional<unsigned int> parseRGBComponent(const std::string& value) {
1006 const std::string component = trim(value);
1007 if (component.empty()) {
1008 return std::nullopt;
1009 }
1010 std::size_t parsedCharacters = 0;
1011 try {
1012 const int parsed = std::stoi(component, &parsedCharacters);
1013 if (parsedCharacters != component.size() || parsed < 0 || parsed > 255) {
1014 return std::nullopt;
1015 }
1016 return static_cast<unsigned int>(parsed);
1017 } catch (...) {
1018 return std::nullopt;
1019 }
1020 }
1021
1022 static std::optional<std::uint32_t> parsePDFColor(const std::string& value) {
1023 const std::string normalized = lowercase(trim(value));
1024 if (normalized.empty()) {
1025 return std::nullopt;
1026 }
1027 if (normalized == "none") {
1029 }
1030
1031 if (normalized.size() == 7 && normalized[0] == '#') {
1032 const auto red = parseHexByte(normalized[1], normalized[2]);
1033 const auto green = parseHexByte(normalized[3], normalized[4]);
1034 const auto blue = parseHexByte(normalized[5], normalized[6]);
1035 if (red && green && blue) {
1036 return pdfgen::PDF_RGB(*red, *green, *blue);
1037 }
1038 }
1039
1040 if (normalized.size() == 4 && normalized[0] == '#') {
1041 const auto red = parseHexDigit(normalized[1]);
1042 const auto green = parseHexDigit(normalized[2]);
1043 const auto blue = parseHexDigit(normalized[3]);
1044 if (red && green && blue) {
1045 return pdfgen::PDF_RGB(
1046 static_cast<unsigned int>(*red * 17),
1047 static_cast<unsigned int>(*green * 17),
1048 static_cast<unsigned int>(*blue * 17)
1049 );
1050 }
1051 }
1052
1053 if (normalized.rfind("rgb(", 0) == 0 && normalized.back() == ')') {
1054 const std::string body = normalized.substr(4, normalized.size() - 5);
1055 std::vector<std::string> parts;
1056 std::size_t start = 0;
1057 while (start <= body.size()) {
1058 const std::size_t comma = body.find(',', start);
1059 if (comma == std::string::npos) {
1060 parts.push_back(body.substr(start));
1061 break;
1062 }
1063 parts.push_back(body.substr(start, comma - start));
1064 start = comma + 1;
1065 }
1066 if (parts.size() == 3) {
1067 const auto red = parseRGBComponent(parts[0]);
1068 const auto green = parseRGBComponent(parts[1]);
1069 const auto blue = parseRGBComponent(parts[2]);
1070 if (red && green && blue) {
1071 return pdfgen::PDF_RGB(*red, *green, *blue);
1072 }
1073 }
1074 }
1075
1076 // The CSS/SVG named colors, so that a color name means the same
1077 // thing in every backend: unknown names have no color to fall back
1078 // on, and silently come out black (stroke) or unpainted (fill).
1079 static constexpr std::array<std::pair<const char*, std::uint32_t>, 148> namedColors{{
1080 {"aliceblue", pdfgen::PDF_RGB(240, 248, 255)},
1081 {"antiquewhite", pdfgen::PDF_RGB(250, 235, 215)},
1082 {"aqua", pdfgen::PDF_RGB(0, 255, 255)},
1083 {"aquamarine", pdfgen::PDF_RGB(127, 255, 212)},
1084 {"azure", pdfgen::PDF_RGB(240, 255, 255)},
1085 {"beige", pdfgen::PDF_RGB(245, 245, 220)},
1086 {"bisque", pdfgen::PDF_RGB(255, 228, 196)},
1087 {"black", pdfgen::PDF_RGB(0, 0, 0)},
1088 {"blanchedalmond", pdfgen::PDF_RGB(255, 235, 205)},
1089 {"blue", pdfgen::PDF_RGB(0, 0, 255)},
1090 {"blueviolet", pdfgen::PDF_RGB(138, 43, 226)},
1091 {"brown", pdfgen::PDF_RGB(165, 42, 42)},
1092 {"burlywood", pdfgen::PDF_RGB(222, 184, 135)},
1093 {"cadetblue", pdfgen::PDF_RGB(95, 158, 160)},
1094 {"chartreuse", pdfgen::PDF_RGB(127, 255, 0)},
1095 {"chocolate", pdfgen::PDF_RGB(210, 105, 30)},
1096 {"coral", pdfgen::PDF_RGB(255, 127, 80)},
1097 {"cornflowerblue", pdfgen::PDF_RGB(100, 149, 237)},
1098 {"cornsilk", pdfgen::PDF_RGB(255, 248, 220)},
1099 {"crimson", pdfgen::PDF_RGB(220, 20, 60)},
1100 {"cyan", pdfgen::PDF_RGB(0, 255, 255)},
1101 {"darkblue", pdfgen::PDF_RGB(0, 0, 139)},
1102 {"darkcyan", pdfgen::PDF_RGB(0, 139, 139)},
1103 {"darkgoldenrod", pdfgen::PDF_RGB(184, 134, 11)},
1104 {"darkgray", pdfgen::PDF_RGB(169, 169, 169)},
1105 {"darkgreen", pdfgen::PDF_RGB(0, 100, 0)},
1106 {"darkgrey", pdfgen::PDF_RGB(169, 169, 169)},
1107 {"darkkhaki", pdfgen::PDF_RGB(189, 183, 107)},
1108 {"darkmagenta", pdfgen::PDF_RGB(139, 0, 139)},
1109 {"darkolivegreen", pdfgen::PDF_RGB(85, 107, 47)},
1110 {"darkorange", pdfgen::PDF_RGB(255, 140, 0)},
1111 {"darkorchid", pdfgen::PDF_RGB(153, 50, 204)},
1112 {"darkred", pdfgen::PDF_RGB(139, 0, 0)},
1113 {"darksalmon", pdfgen::PDF_RGB(233, 150, 122)},
1114 {"darkseagreen", pdfgen::PDF_RGB(143, 188, 143)},
1115 {"darkslateblue", pdfgen::PDF_RGB(72, 61, 139)},
1116 {"darkslategray", pdfgen::PDF_RGB(47, 79, 79)},
1117 {"darkslategrey", pdfgen::PDF_RGB(47, 79, 79)},
1118 {"darkturquoise", pdfgen::PDF_RGB(0, 206, 209)},
1119 {"darkviolet", pdfgen::PDF_RGB(148, 0, 211)},
1120 {"deeppink", pdfgen::PDF_RGB(255, 20, 147)},
1121 {"deepskyblue", pdfgen::PDF_RGB(0, 191, 255)},
1122 {"dimgray", pdfgen::PDF_RGB(105, 105, 105)},
1123 {"dimgrey", pdfgen::PDF_RGB(105, 105, 105)},
1124 {"dodgerblue", pdfgen::PDF_RGB(30, 144, 255)},
1125 {"firebrick", pdfgen::PDF_RGB(178, 34, 34)},
1126 {"floralwhite", pdfgen::PDF_RGB(255, 250, 240)},
1127 {"forestgreen", pdfgen::PDF_RGB(34, 139, 34)},
1128 {"fuchsia", pdfgen::PDF_RGB(255, 0, 255)},
1129 {"gainsboro", pdfgen::PDF_RGB(220, 220, 220)},
1130 {"ghostwhite", pdfgen::PDF_RGB(248, 248, 255)},
1131 {"gold", pdfgen::PDF_RGB(255, 215, 0)},
1132 {"goldenrod", pdfgen::PDF_RGB(218, 165, 32)},
1133 {"gray", pdfgen::PDF_RGB(128, 128, 128)},
1134 {"green", pdfgen::PDF_RGB(0, 128, 0)},
1135 {"greenyellow", pdfgen::PDF_RGB(173, 255, 47)},
1136 {"grey", pdfgen::PDF_RGB(128, 128, 128)},
1137 {"honeydew", pdfgen::PDF_RGB(240, 255, 240)},
1138 {"hotpink", pdfgen::PDF_RGB(255, 105, 180)},
1139 {"indianred", pdfgen::PDF_RGB(205, 92, 92)},
1140 {"indigo", pdfgen::PDF_RGB(75, 0, 130)},
1141 {"ivory", pdfgen::PDF_RGB(255, 255, 240)},
1142 {"khaki", pdfgen::PDF_RGB(240, 230, 140)},
1143 {"lavender", pdfgen::PDF_RGB(230, 230, 250)},
1144 {"lavenderblush", pdfgen::PDF_RGB(255, 240, 245)},
1145 {"lawngreen", pdfgen::PDF_RGB(124, 252, 0)},
1146 {"lemonchiffon", pdfgen::PDF_RGB(255, 250, 205)},
1147 {"lightblue", pdfgen::PDF_RGB(173, 216, 230)},
1148 {"lightcoral", pdfgen::PDF_RGB(240, 128, 128)},
1149 {"lightcyan", pdfgen::PDF_RGB(224, 255, 255)},
1150 {"lightgoldenrodyellow", pdfgen::PDF_RGB(250, 250, 210)},
1151 {"lightgray", pdfgen::PDF_RGB(211, 211, 211)},
1152 {"lightgreen", pdfgen::PDF_RGB(144, 238, 144)},
1153 {"lightgrey", pdfgen::PDF_RGB(211, 211, 211)},
1154 {"lightpink", pdfgen::PDF_RGB(255, 182, 193)},
1155 {"lightsalmon", pdfgen::PDF_RGB(255, 160, 122)},
1156 {"lightseagreen", pdfgen::PDF_RGB(32, 178, 170)},
1157 {"lightskyblue", pdfgen::PDF_RGB(135, 206, 250)},
1158 {"lightslategray", pdfgen::PDF_RGB(119, 136, 153)},
1159 {"lightslategrey", pdfgen::PDF_RGB(119, 136, 153)},
1160 {"lightsteelblue", pdfgen::PDF_RGB(176, 196, 222)},
1161 {"lightyellow", pdfgen::PDF_RGB(255, 255, 224)},
1162 {"lime", pdfgen::PDF_RGB(0, 255, 0)},
1163 {"limegreen", pdfgen::PDF_RGB(50, 205, 50)},
1164 {"linen", pdfgen::PDF_RGB(250, 240, 230)},
1165 {"magenta", pdfgen::PDF_RGB(255, 0, 255)},
1166 {"maroon", pdfgen::PDF_RGB(128, 0, 0)},
1167 {"mediumaquamarine", pdfgen::PDF_RGB(102, 205, 170)},
1168 {"mediumblue", pdfgen::PDF_RGB(0, 0, 205)},
1169 {"mediumorchid", pdfgen::PDF_RGB(186, 85, 211)},
1170 {"mediumpurple", pdfgen::PDF_RGB(147, 112, 219)},
1171 {"mediumseagreen", pdfgen::PDF_RGB(60, 179, 113)},
1172 {"mediumslateblue", pdfgen::PDF_RGB(123, 104, 238)},
1173 {"mediumspringgreen", pdfgen::PDF_RGB(0, 250, 154)},
1174 {"mediumturquoise", pdfgen::PDF_RGB(72, 209, 204)},
1175 {"mediumvioletred", pdfgen::PDF_RGB(199, 21, 133)},
1176 {"midnightblue", pdfgen::PDF_RGB(25, 25, 112)},
1177 {"mintcream", pdfgen::PDF_RGB(245, 255, 250)},
1178 {"mistyrose", pdfgen::PDF_RGB(255, 228, 225)},
1179 {"moccasin", pdfgen::PDF_RGB(255, 228, 181)},
1180 {"navajowhite", pdfgen::PDF_RGB(255, 222, 173)},
1181 {"navy", pdfgen::PDF_RGB(0, 0, 128)},
1182 {"oldlace", pdfgen::PDF_RGB(253, 245, 230)},
1183 {"olive", pdfgen::PDF_RGB(128, 128, 0)},
1184 {"olivedrab", pdfgen::PDF_RGB(107, 142, 35)},
1185 {"orange", pdfgen::PDF_RGB(255, 165, 0)},
1186 {"orangered", pdfgen::PDF_RGB(255, 69, 0)},
1187 {"orchid", pdfgen::PDF_RGB(218, 112, 214)},
1188 {"palegoldenrod", pdfgen::PDF_RGB(238, 232, 170)},
1189 {"palegreen", pdfgen::PDF_RGB(152, 251, 152)},
1190 {"paleturquoise", pdfgen::PDF_RGB(175, 238, 238)},
1191 {"palevioletred", pdfgen::PDF_RGB(219, 112, 147)},
1192 {"papayawhip", pdfgen::PDF_RGB(255, 239, 213)},
1193 {"peachpuff", pdfgen::PDF_RGB(255, 218, 185)},
1194 {"peru", pdfgen::PDF_RGB(205, 133, 63)},
1195 {"pink", pdfgen::PDF_RGB(255, 192, 203)},
1196 {"plum", pdfgen::PDF_RGB(221, 160, 221)},
1197 {"powderblue", pdfgen::PDF_RGB(176, 224, 230)},
1198 {"purple", pdfgen::PDF_RGB(128, 0, 128)},
1199 {"rebeccapurple", pdfgen::PDF_RGB(102, 51, 153)},
1200 {"red", pdfgen::PDF_RGB(255, 0, 0)},
1201 {"rosybrown", pdfgen::PDF_RGB(188, 143, 143)},
1202 {"royalblue", pdfgen::PDF_RGB(65, 105, 225)},
1203 {"saddlebrown", pdfgen::PDF_RGB(139, 69, 19)},
1204 {"salmon", pdfgen::PDF_RGB(250, 128, 114)},
1205 {"sandybrown", pdfgen::PDF_RGB(244, 164, 96)},
1206 {"seagreen", pdfgen::PDF_RGB(46, 139, 87)},
1207 {"seashell", pdfgen::PDF_RGB(255, 245, 238)},
1208 {"sienna", pdfgen::PDF_RGB(160, 82, 45)},
1209 {"silver", pdfgen::PDF_RGB(192, 192, 192)},
1210 {"skyblue", pdfgen::PDF_RGB(135, 206, 235)},
1211 {"slateblue", pdfgen::PDF_RGB(106, 90, 205)},
1212 {"slategray", pdfgen::PDF_RGB(112, 128, 144)},
1213 {"slategrey", pdfgen::PDF_RGB(112, 128, 144)},
1214 {"snow", pdfgen::PDF_RGB(255, 250, 250)},
1215 {"springgreen", pdfgen::PDF_RGB(0, 255, 127)},
1216 {"steelblue", pdfgen::PDF_RGB(70, 130, 180)},
1217 {"tan", pdfgen::PDF_RGB(210, 180, 140)},
1218 {"teal", pdfgen::PDF_RGB(0, 128, 128)},
1219 {"thistle", pdfgen::PDF_RGB(216, 191, 216)},
1220 {"tomato", pdfgen::PDF_RGB(255, 99, 71)},
1221 {"turquoise", pdfgen::PDF_RGB(64, 224, 208)},
1222 {"violet", pdfgen::PDF_RGB(238, 130, 238)},
1223 {"wheat", pdfgen::PDF_RGB(245, 222, 179)},
1224 {"white", pdfgen::PDF_RGB(255, 255, 255)},
1225 {"whitesmoke", pdfgen::PDF_RGB(245, 245, 245)},
1226 {"yellow", pdfgen::PDF_RGB(255, 255, 0)},
1227 {"yellowgreen", pdfgen::PDF_RGB(154, 205, 50)},
1228 }};
1229 for (const auto& [name, colour] : namedColors) {
1230 if (normalized == name) {
1231 return colour;
1232 }
1233 }
1234
1235 return std::nullopt;
1236 }
1237
1238 static PDFStyle pdfStyleOf(const CanvasStyle& style) {
1239 PDFStyle result;
1240 result.stroke = parsePDFColor(style.stroke).value_or(pdfgen::PDF_BLACK);
1241 result.fill = parsePDFColor(style.fill).value_or(pdfgen::PDF_TRANSPARENT);
1242 result.strokeWidth = numericLengthOr(style.strokeWidth, 1.0f);
1243 result.pointRadius = numericLengthOr(style.pointRadius, 3.0f);
1244 result.fillAlpha = opacityOr(style.fillOpacity, 1.0f);
1245 result.strokeAlpha = opacityOr(style.strokeOpacity, 1.0f);
1246 return result;
1247 }
1248
1249 float pdfYFromSVG(double svgY) const {
1250 return static_cast<float>(heightPixels_ - svgY);
1251 }
1252
1253 template <class PointLike>
1254 std::pair<float, float> mapPDFPoint(const PointLike& point, const Viewport& viewport) const {
1255 return {
1256 static_cast<float>(viewport.mapX(point.x())),
1257 pdfYFromSVG(viewport.mapY(point.y())),
1258 };
1259 }
1260
1261 static std::vector<pdfgen::pdf_path_operation> polygonPathOperations(
1262 const std::vector<std::pair<float, float>>& points,
1263 bool closePath) {
1264 std::vector<pdfgen::pdf_path_operation> operations;
1265 if (points.empty()) {
1266 return operations;
1267 }
1268
1269 operations.reserve(points.size() + (closePath ? 1u : 0u));
1270 operations.push_back({'m', points[0].first, points[0].second, 0.0f, 0.0f, 0.0f, 0.0f});
1271 for (std::size_t index = 1; index < points.size(); ++index) {
1272 operations.push_back({'l', points[index].first, points[index].second, 0.0f, 0.0f, 0.0f, 0.0f});
1273 }
1274 if (closePath) {
1275 operations.push_back({'h', 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
1276 }
1277 return operations;
1278 }
1279
1280 // The rings of a region, mapped to output coordinates by `map`, with every
1281 // hole reversed so that it winds against the outer ring. Opposite windings
1282 // make the filled area come out as the region itself under the even-odd and
1283 // the nonzero-winding rule alike, which is what lets the three backends
1284 // share this list: SVG asks for even-odd explicitly, while PDF and Ipe take
1285 // whatever their default is.
1286 template <class MapPoint>
1287 static auto regionRings(const PolygonWithHoles<Point<double>>& region, MapPoint map) {
1288 using Mapped = std::decay_t<decltype(map(std::declval<const Point<double>&>()))>;
1289 std::vector<std::vector<Mapped>> rings;
1290 const auto append = [&rings, &map](const Polygon<Point<double>>& ring, bool reversed) {
1291 if (ring.size() == 0) return;
1292 std::vector<Mapped> points;
1293 points.reserve(ring.size());
1294 for (const auto& vertex : ring) {
1295 points.push_back(map(vertex));
1296 }
1297 if (reversed) std::reverse(points.begin(), points.end());
1298 rings.push_back(std::move(points));
1299 };
1300
1301 rings.reserve(region.holeCount() + 1);
1302 append(region.outer(), false);
1303 for (const auto& hole : region.holes()) {
1304 append(hole, true);
1305 }
1306 return rings;
1307 }
1308
1309 // The rings of a set: every component's, one after another. The components
1310 // have pairwise disjoint interiors and each one's holes lie inside its own
1311 // outer ring, so no ring of one component ever falls inside another's and
1312 // the concatenation fills as the set under both rules, exactly as the list
1313 // above does for a single region.
1314 template <class MapPoint>
1315 static auto regionRings(const PolygonSet<Point<double>>& set, MapPoint map) {
1316 using Mapped = std::decay_t<decltype(map(std::declval<const Point<double>&>()))>;
1317 std::vector<std::vector<Mapped>> rings;
1318 rings.reserve(set.componentCount() + set.holeCount());
1319 for (const auto& component : set) {
1320 for (auto& ring : regionRings(component, map)) {
1321 rings.push_back(std::move(ring));
1322 }
1323 }
1324 return rings;
1325 }
1326
1327 // One `m … l … h` run per ring, so that a single filled path carries the
1328 // whole region.
1329 static std::vector<pdfgen::pdf_path_operation> polygonPathOperations(
1330 const std::vector<std::vector<std::pair<float, float>>>& rings) {
1331 std::vector<pdfgen::pdf_path_operation> operations;
1332 for (const auto& ring : rings) {
1333 const auto ringOperations = polygonPathOperations(ring, true);
1334 operations.insert(operations.end(), ringOperations.begin(), ringOperations.end());
1335 }
1336 return operations;
1337 }
1338
1339 static std::uint32_t arrowColor(const PDFStyle& style) {
1340 return pdfgen::PDF_IS_TRANSPARENT(style.stroke) ? style.fill : style.stroke;
1341 }
1342
1343 static float arrowAlpha(const PDFStyle& style) {
1344 return pdfgen::PDF_IS_TRANSPARENT(style.stroke) ? style.fillAlpha : style.strokeAlpha;
1345 }
1346
1347 void addArrowhead(
1348 pdfgen::pdf_doc* pdf,
1349 pdfgen::pdf_object* page,
1350 float startX,
1351 float startY,
1352 float endX,
1353 float endY,
1354 const PDFStyle& style) const {
1355 const std::uint32_t colour = arrowColor(style);
1356 if (pdfgen::PDF_IS_TRANSPARENT(colour)) {
1357 return;
1358 }
1359
1360 const float dx = endX - startX;
1361 const float dy = endY - startY;
1362 const float length = std::sqrt(dx * dx + dy * dy);
1363 if (length == 0.0f) {
1364 return;
1365 }
1366
1367 const float ux = dx / length;
1368 const float uy = dy / length;
1369 const float px = -uy;
1370 const float py = ux;
1371 const float size = std::max(6.0f, style.strokeWidth * 4.0f);
1372 const float midX = (startX + endX) / 2.0f;
1373 const float midY = (startY + endY) / 2.0f;
1374 const float tipX = midX + ux * size * 0.6f;
1375 const float tipY = midY + uy * size * 0.6f;
1376 const float baseCenterX = midX - ux * size * 0.4f;
1377 const float baseCenterY = midY - uy * size * 0.4f;
1378 const float halfBase = size * 0.35f;
1379
1380 const float xs[] = {
1381 tipX,
1382 baseCenterX + px * halfBase,
1383 baseCenterX - px * halfBase,
1384 };
1385 const float ys[] = {
1386 tipY,
1387 baseCenterY + py * halfBase,
1388 baseCenterY - py * halfBase,
1389 };
1390 if (pdfgen::pdf_add_filled_polygon(pdf, page, xs, ys, 3, 0.0f, colour, arrowAlpha(style), 1.0f) < 0) {
1391 throwPDFError(pdf, "draw PDF arrowhead");
1392 }
1393 }
1394
1395 void addPath(
1396 pdfgen::pdf_doc* pdf,
1397 pdfgen::pdf_object* page,
1398 const std::vector<std::pair<float, float>>& points,
1399 bool closePath,
1400 const PDFStyle& style,
1401 std::uint32_t fillOverride = pdfgen::PDF_TRANSPARENT) const {
1402 if (points.empty()) {
1403 return;
1404 }
1405 const auto operations = polygonPathOperations(points, closePath);
1406 const std::uint32_t fillColour = fillOverride == pdfgen::PDF_TRANSPARENT ? style.fill : fillOverride;
1408 pdf,
1409 page,
1410 operations.data(),
1411 static_cast<int>(operations.size()),
1412 style.strokeWidth,
1413 style.stroke,
1414 fillColour,
1415 style.fillAlpha,
1416 style.strokeAlpha) < 0) {
1417 throwPDFError(pdf, "draw PDF path");
1418 }
1419 }
1420
1422 void addPath(
1423 pdfgen::pdf_doc* pdf,
1424 pdfgen::pdf_object* page,
1425 const std::vector<std::vector<std::pair<float, float>>>& rings,
1426 const PDFStyle& style) const {
1427 const auto operations = polygonPathOperations(rings);
1428 if (operations.empty()) {
1429 return;
1430 }
1432 pdf,
1433 page,
1434 operations.data(),
1435 static_cast<int>(operations.size()),
1436 style.strokeWidth,
1437 style.stroke,
1438 style.fill,
1439 style.fillAlpha,
1440 style.strokeAlpha) < 0) {
1441 throwPDFError(pdf, "draw PDF path");
1442 }
1443 }
1444
1445 void appendElementToPDF(
1446 pdfgen::pdf_doc* pdf,
1447 pdfgen::pdf_object* page,
1448 const Element& element,
1449 const Viewport& viewport) const {
1450 using PT = Point<double>;
1451 const PDFStyle style = pdfStyleOf(element.style);
1452
1453 std::visit([&](const auto& shape) {
1454 using S = std::decay_t<decltype(shape)>;
1455
1456 if constexpr (std::same_as<S, PT>) {
1457 const auto [x, y] = mapPDFPoint(shape, viewport);
1459 pdf,
1460 page,
1461 x,
1462 y,
1463 style.pointRadius,
1464 style.strokeWidth,
1465 style.stroke,
1466 style.fill,
1467 style.fillAlpha,
1468 style.strokeAlpha) < 0) {
1469 throwPDFError(pdf, "draw PDF point");
1470 }
1471 } else if constexpr (std::same_as<S, Segment<PT>>) {
1472 const auto [x1, y1] = mapPDFPoint(shape.min(), viewport);
1473 const auto [x2, y2] = mapPDFPoint(shape.max(), viewport);
1474 if (pdfgen::pdf_add_line(pdf, page, x1, y1, x2, y2, style.strokeWidth, style.stroke, style.strokeAlpha) < 0) {
1475 throwPDFError(pdf, "draw PDF segment");
1476 }
1477 } else if constexpr (std::same_as<S, OrientedSegment<PT>>) {
1478 const auto [x1, y1] = mapPDFPoint(shape.source(), viewport);
1479 const auto [x2, y2] = mapPDFPoint(shape.target(), viewport);
1480 if (pdfgen::pdf_add_line(pdf, page, x1, y1, x2, y2, style.strokeWidth, style.stroke, style.strokeAlpha) < 0) {
1481 throwPDFError(pdf, "draw PDF oriented segment");
1482 }
1483 // PDF export approximates SVG marker-mid arrowheads with a small
1484 // filled triangle centered on the visible segment midpoint.
1485 addArrowhead(pdf, page, x1, y1, x2, y2, style);
1486 } else if constexpr (std::same_as<S, Line<PT>>) {
1487 const double x1 = viewport.mapX(shape.min().x());
1488 const double y1 = viewport.mapY(shape.min().y());
1489 const double x2 = viewport.mapX(shape.max().x());
1490 const double y2 = viewport.mapY(shape.max().y());
1491 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1492 if (!visible) return;
1493 const auto& [vx1, vy1, vx2, vy2] = *visible;
1494 if (vx1 == vx2 && vy1 == vy2) {
1496 pdf,
1497 page,
1498 static_cast<float>(vx1),
1499 pdfYFromSVG(vy1),
1500 style.pointRadius,
1501 style.strokeWidth,
1502 style.stroke,
1503 style.fill,
1504 style.fillAlpha,
1505 style.strokeAlpha) < 0) {
1506 throwPDFError(pdf, "draw PDF degenerate line");
1507 }
1508 } else if (pdfgen::pdf_add_line(
1509 pdf,
1510 page,
1511 static_cast<float>(vx1),
1512 pdfYFromSVG(vy1),
1513 static_cast<float>(vx2),
1514 pdfYFromSVG(vy2),
1515 style.strokeWidth,
1516 style.stroke,
1517 style.strokeAlpha) < 0) {
1518 throwPDFError(pdf, "draw PDF line");
1519 }
1520 } else if constexpr (std::same_as<S, OrientedLine<PT>>) {
1521 const double x1 = viewport.mapX(shape.source().x());
1522 const double y1 = viewport.mapY(shape.source().y());
1523 const double x2 = viewport.mapX(shape.target().x());
1524 const double y2 = viewport.mapY(shape.target().y());
1525 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1526 if (!visible) return;
1527 const auto& [vx1, vy1, vx2, vy2] = *visible;
1528 const float px1 = static_cast<float>(vx1);
1529 const float py1 = pdfYFromSVG(vy1);
1530 const float px2 = static_cast<float>(vx2);
1531 const float py2 = pdfYFromSVG(vy2);
1532 if (pdfgen::pdf_add_line(pdf, page, px1, py1, px2, py2, style.strokeWidth, style.stroke, style.strokeAlpha) < 0) {
1533 throwPDFError(pdf, "draw PDF oriented line");
1534 }
1535 addArrowhead(pdf, page, px1, py1, px2, py2, style);
1536 } else if constexpr (std::same_as<S, Ray<PT>>) {
1537 const double x1 = viewport.mapX(shape.source().x());
1538 const double y1 = viewport.mapY(shape.source().y());
1539 const double x2 = viewport.mapX(shape.target().x());
1540 const double y2 = viewport.mapY(shape.target().y());
1541 const auto visible = clipRayToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1542 if (!visible) return;
1543 const auto& [vx1, vy1, vx2, vy2] = *visible;
1544 const float px1 = static_cast<float>(vx1);
1545 const float py1 = pdfYFromSVG(vy1);
1546 const float px2 = static_cast<float>(vx2);
1547 const float py2 = pdfYFromSVG(vy2);
1548 if (pdfgen::pdf_add_line(pdf, page, px1, py1, px2, py2, style.strokeWidth, style.stroke, style.strokeAlpha) < 0) {
1549 throwPDFError(pdf, "draw PDF ray");
1550 }
1551 addArrowhead(pdf, page, px1, py1, px2, py2, style);
1552 } else if constexpr (std::same_as<S, Halfplane<PT>>) {
1553 const auto polygon = clipHalfplaneToViewport(shape, viewport, widthPixels_, heightPixels_);
1554 if (!polygon.empty()) {
1555 std::vector<std::pair<float, float>> pdfPoints;
1556 pdfPoints.reserve(polygon.size());
1557 for (const auto& [worldX, worldY] : polygon) {
1558 pdfPoints.emplace_back(
1559 static_cast<float>(viewport.mapX(worldX)),
1560 pdfYFromSVG(viewport.mapY(worldY))
1561 );
1562 }
1563 addPath(
1564 pdf,
1565 page,
1566 pdfPoints,
1567 true,
1568 PDFStyle{pdfgen::PDF_TRANSPARENT, style.fill, 0.0f, style.pointRadius, style.fillAlpha, 1.0f},
1569 style.fill
1570 );
1571 }
1572
1573 const double x1 = viewport.mapX(shape.source().x());
1574 const double y1 = viewport.mapY(shape.source().y());
1575 const double x2 = viewport.mapX(shape.target().x());
1576 const double y2 = viewport.mapY(shape.target().y());
1577 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1578 if (visible && pdfgen::pdf_add_line(
1579 pdf,
1580 page,
1581 static_cast<float>(visible->x1),
1582 pdfYFromSVG(visible->y1),
1583 static_cast<float>(visible->x2),
1584 pdfYFromSVG(visible->y2),
1585 style.strokeWidth,
1586 style.stroke,
1587 style.strokeAlpha) < 0) {
1588 throwPDFError(pdf, "draw PDF halfplane boundary");
1589 }
1590 } else if constexpr (std::same_as<S, HalfplaneIntersection<PT>>) {
1591 const auto polygon = clipRegionToViewport(shape, viewport, widthPixels_, heightPixels_);
1592 if (!polygon.empty()) {
1593 std::vector<std::pair<float, float>> pdfPoints;
1594 pdfPoints.reserve(polygon.size());
1595 for (const auto& [worldX, worldY] : polygon) {
1596 pdfPoints.emplace_back(
1597 static_cast<float>(viewport.mapX(worldX)),
1598 pdfYFromSVG(viewport.mapY(worldY))
1599 );
1600 }
1601 addPath(
1602 pdf,
1603 page,
1604 pdfPoints,
1605 true,
1606 PDFStyle{pdfgen::PDF_TRANSPARENT, style.fill, 0.0f, style.pointRadius, style.fillAlpha, 1.0f},
1607 style.fill
1608 );
1609 }
1610
1611 // Stroke only the region's real boundary edges: the viewport
1612 // sides of the clipped polygon are not part of the boundary.
1613 for (const auto& piece : regionBoundaryPieces(shape, viewport)) {
1615 pdf,
1616 page,
1617 static_cast<float>(piece.x1),
1618 pdfYFromSVG(piece.y1),
1619 static_cast<float>(piece.x2),
1620 pdfYFromSVG(piece.y2),
1621 style.strokeWidth,
1622 style.stroke,
1623 style.strokeAlpha) < 0) {
1624 throwPDFError(pdf, "draw PDF half-plane intersection boundary");
1625 }
1626 }
1627 } else if constexpr (std::same_as<S, Rectangle<PT>>) {
1628 if (shape.empty()) return;
1629 const float x = static_cast<float>(viewport.mapX(shape.min().x()));
1630 const float y = pdfYFromSVG(viewport.mapY(shape.min().y()));
1631 const float width = static_cast<float>(std::abs(viewport.mapX(shape.max().x()) - viewport.mapX(shape.min().x())));
1632 const float height = std::abs(pdfYFromSVG(viewport.mapY(shape.max().y())) - pdfYFromSVG(viewport.mapY(shape.min().y())));
1634 pdf,
1635 page,
1636 x,
1637 y,
1638 width,
1639 height,
1640 style.strokeWidth,
1641 style.fill,
1642 style.stroke,
1643 style.fillAlpha,
1644 style.strokeAlpha) < 0) {
1645 throwPDFError(pdf, "draw PDF rectangle");
1646 }
1647 } else if constexpr (std::same_as<S, Triangle<PT>>) {
1648 addPath(pdf, page, {mapPDFPoint(shape.a(), viewport), mapPDFPoint(shape.b(), viewport), mapPDFPoint(shape.c(), viewport)}, true, style);
1649 } else if constexpr (std::same_as<S, Convex<PT>>) {
1650 if (shape.size() == 0) return;
1651 std::vector<std::pair<float, float>> points;
1652 points.reserve(shape.size());
1653 for (const auto& vertex : shape) {
1654 points.push_back(mapPDFPoint(vertex, viewport));
1655 }
1656 addPath(pdf, page, points, true, style);
1657 } else if constexpr (std::same_as<S, Polygon<PT>>) {
1658 if (shape.size() == 0) return;
1659 std::vector<std::pair<float, float>> points;
1660 points.reserve(shape.size());
1661 for (const auto& vertex : shape) {
1662 points.push_back(mapPDFPoint(vertex, viewport));
1663 }
1664 addPath(pdf, page, points, true, style);
1665 } else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
1666 addPath(
1667 pdf,
1668 page,
1669 regionRings(shape, [&](const PT& vertex) { return mapPDFPoint(vertex, viewport); }),
1670 style);
1671 } else if constexpr (std::same_as<S, MonotoneChain<PT>> || std::same_as<S, Polyline<PT>>) {
1672 if (shape.size() == 0) return;
1673 if (shape.size() == 1) {
1674 const auto [x, y] = mapPDFPoint(shape[0], viewport);
1676 pdf,
1677 page,
1678 x,
1679 y,
1680 style.pointRadius,
1681 style.strokeWidth,
1682 style.stroke,
1683 style.fill,
1684 style.fillAlpha,
1685 style.strokeAlpha) < 0) {
1686 throwPDFError(pdf, "draw PDF chain point");
1687 }
1688 } else {
1689 std::vector<std::pair<float, float>> points;
1690 points.reserve(shape.size());
1691 for (const auto& vertex : shape) {
1692 points.push_back(mapPDFPoint(vertex, viewport));
1693 }
1694 addPath(
1695 pdf,
1696 page,
1697 points,
1698 false,
1699 PDFStyle{style.stroke, pdfgen::PDF_TRANSPARENT, style.strokeWidth, style.pointRadius, 1.0f, style.strokeAlpha}
1700 );
1701 }
1702 } else if constexpr (std::same_as<S, Disk<PT>>) {
1703 const auto [x, y] = mapPDFPoint(shape.template center<double>(), viewport);
1704 const float radius = static_cast<float>(shape.template radius<double>() * viewport.scale);
1706 pdf,
1707 page,
1708 x,
1709 y,
1710 radius,
1711 style.strokeWidth,
1712 style.stroke,
1713 style.fill,
1714 style.fillAlpha,
1715 style.strokeAlpha) < 0) {
1716 throwPDFError(pdf, "draw PDF disk");
1717 }
1718 }
1719 }, element.shape.variant());
1720 }
1721
1722 std::string elementToSVG(const Element& element, const Viewport& viewport) const {
1723 using PT = Point<double>;
1724 const std::string titleTag = "<title>" + escapeXML(element.title, false) + "</title>";
1725
1726 return std::visit([&](const auto& shape) -> std::string {
1727 using S = std::decay_t<decltype(shape)>;
1728 std::ostringstream out;
1729
1730 if constexpr (std::same_as<S, PT>) {
1731 const double cx = viewport.mapX(shape.x());
1732 const double cy = viewport.mapY(shape.y());
1733 out << "<circle cx=\"" << cx << "\" cy=\"" << cy
1734 << "\" r=\"" << element.style.pointRadius << '"'
1735 << styleAttributes(element.style) << ">"
1736 << titleTag << "</circle>";
1737 } else if constexpr (std::same_as<S, Segment<PT>>) {
1738 const double x1 = viewport.mapX(shape.min().x());
1739 const double y1 = viewport.mapY(shape.min().y());
1740 const double x2 = viewport.mapX(shape.max().x());
1741 const double y2 = viewport.mapY(shape.max().y());
1742 out << "<line x1=\"" << x1 << "\" y1=\"" << y1
1743 << "\" x2=\"" << x2 << "\" y2=\"" << y2 << "\""
1744 << styleAttributes(element.style) << ">"
1745 << titleTag << "</line>";
1746 } else if constexpr (std::same_as<S, OrientedSegment<PT>>) {
1747 const double x1 = viewport.mapX(shape.source().x());
1748 const double y1 = viewport.mapY(shape.source().y());
1749 const double x2 = viewport.mapX(shape.target().x());
1750 const double y2 = viewport.mapY(shape.target().y());
1751 const double midX = viewport.mapX((shape.source().x() + shape.target().x()) / 2.0);
1752 const double midY = viewport.mapY((shape.source().y() + shape.target().y()) / 2.0);
1753 out << "<path d=\"M " << x1 << ' ' << y1
1754 << " L " << midX << ' ' << midY
1755 << " L " << x2 << ' ' << y2 << "\""
1756 << styleAttributes(element.style)
1757 << " marker-mid=\"url(#pgl-arrowhead)\">"
1758 << titleTag << "</path>";
1759 } else if constexpr (std::same_as<S, Line<PT>>) {
1760 const double x1 = viewport.mapX(shape.min().x());
1761 const double y1 = viewport.mapY(shape.min().y());
1762 const double x2 = viewport.mapX(shape.max().x());
1763 const double y2 = viewport.mapY(shape.max().y());
1764 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1765 if (!visible) return {};
1766 const auto& [vx1, vy1, vx2, vy2] = *visible;
1767 if (vx1 == vx2 && vy1 == vy2) {
1768 out << "<circle cx=\"" << vx1 << "\" cy=\"" << vy1
1769 << "\" r=\"" << element.style.pointRadius << '"'
1770 << styleAttributes(element.style) << ">"
1771 << titleTag << "</circle>";
1772 } else {
1773 out << "<line x1=\"" << vx1 << "\" y1=\"" << vy1
1774 << "\" x2=\"" << vx2 << "\" y2=\"" << vy2 << "\""
1775 << styleAttributes(element.style) << ">"
1776 << titleTag << "</line>";
1777 }
1778 } else if constexpr (std::same_as<S, OrientedLine<PT>>) {
1779 const double x1 = viewport.mapX(shape.source().x());
1780 const double y1 = viewport.mapY(shape.source().y());
1781 const double x2 = viewport.mapX(shape.target().x());
1782 const double y2 = viewport.mapY(shape.target().y());
1783 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1784 if (!visible) return {};
1785 const auto& [vx1, vy1, vx2, vy2] = *visible;
1786 const double midX = (vx1 + vx2) / 2.0;
1787 const double midY = (vy1 + vy2) / 2.0;
1788 out << "<path d=\"M " << vx1 << ' ' << vy1
1789 << " L " << midX << ' ' << midY
1790 << " L " << vx2 << ' ' << vy2 << "\""
1791 << styleAttributes(element.style)
1792 << " marker-mid=\"url(#pgl-arrowhead)\">"
1793 << titleTag << "</path>";
1794 } else if constexpr (std::same_as<S, Ray<PT>>) {
1795 const double x1 = viewport.mapX(shape.source().x());
1796 const double y1 = viewport.mapY(shape.source().y());
1797 const double x2 = viewport.mapX(shape.target().x());
1798 const double y2 = viewport.mapY(shape.target().y());
1799 const auto visible = clipRayToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1800 if (!visible) return {};
1801 const auto& [vx1, vy1, vx2, vy2] = *visible;
1802 const double midX = (vx1 + vx2) / 2.0;
1803 const double midY = (vy1 + vy2) / 2.0;
1804 out << "<path d=\"M " << vx1 << ' ' << vy1
1805 << " L " << midX << ' ' << midY
1806 << " L " << vx2 << ' ' << vy2 << "\""
1807 << styleAttributes(element.style)
1808 << " marker-mid=\"url(#pgl-arrowhead)\">"
1809 << titleTag << "</path>";
1810 } else if constexpr (std::same_as<S, Halfplane<PT>>) {
1811 const auto polygon = clipHalfplaneToViewport(shape, viewport, widthPixels_, heightPixels_);
1812 if (polygon.empty()) return {};
1813
1814 out << "<g><polygon points=\"";
1815 for (std::size_t index = 0; index < polygon.size(); ++index) {
1816 if (index != 0) out << ' ';
1817 out << viewport.mapX(polygon[index].first) << ',' << viewport.mapY(polygon[index].second);
1818 }
1819 out << "\" stroke=\"none\"" << fillAttributes(element.style) << ">"
1820 << titleTag << "</polygon>";
1821
1822 const double x1 = viewport.mapX(shape.source().x());
1823 const double y1 = viewport.mapY(shape.source().y());
1824 const double x2 = viewport.mapX(shape.target().x());
1825 const double y2 = viewport.mapY(shape.target().y());
1826 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
1827 if (visible) {
1828 const auto& [vx1, vy1, vx2, vy2] = *visible;
1829 out << "<line x1=\"" << vx1 << "\" y1=\"" << vy1
1830 << "\" x2=\"" << vx2 << "\" y2=\"" << vy2 << "\""
1831 << styleAttributes(element.style) << ">"
1832 << titleTag << "</line>";
1833 }
1834 out << "</g>";
1835 } else if constexpr (std::same_as<S, HalfplaneIntersection<PT>>) {
1836 const auto polygon = clipRegionToViewport(shape, viewport, widthPixels_, heightPixels_);
1837 const auto boundary = regionBoundaryPieces(shape, viewport);
1838 if (polygon.empty() && boundary.empty()) return {};
1839
1840 out << "<g>";
1841 if (!polygon.empty()) {
1842 out << "<polygon points=\"";
1843 for (std::size_t index = 0; index < polygon.size(); ++index) {
1844 if (index != 0) out << ' ';
1845 out << viewport.mapX(polygon[index].first) << ',' << viewport.mapY(polygon[index].second);
1846 }
1847 out << "\" stroke=\"none\"" << fillAttributes(element.style) << ">"
1848 << titleTag << "</polygon>";
1849 }
1850 // Stroke only the region's real boundary edges: the viewport
1851 // sides of the clipped polygon are not part of the boundary.
1852 for (const auto& piece : boundary) {
1853 out << "<line x1=\"" << piece.x1 << "\" y1=\"" << piece.y1
1854 << "\" x2=\"" << piece.x2 << "\" y2=\"" << piece.y2 << "\""
1855 << styleAttributes(element.style) << ">"
1856 << titleTag << "</line>";
1857 }
1858 out << "</g>";
1859 } else if constexpr (std::same_as<S, Rectangle<PT>>) {
1860 if (shape.empty()) return {};
1861 const double minX = viewport.mapX(shape.min().x());
1862 const double maxX = viewport.mapX(shape.max().x());
1863 const double minY = viewport.mapY(shape.max().y());
1864 const double maxY = viewport.mapY(shape.min().y());
1865 out << "<rect x=\"" << std::min(minX, maxX) << "\" y=\"" << std::min(minY, maxY)
1866 << "\" width=\"" << std::abs(maxX - minX) << "\" height=\"" << std::abs(maxY - minY) << "\""
1867 << styleAttributes(element.style) << ">"
1868 << titleTag << "</rect>";
1869 } else if constexpr (std::same_as<S, Triangle<PT>>) {
1870 out << "<polygon points=\""
1871 << viewport.mapX(shape.a().x()) << ',' << viewport.mapY(shape.a().y()) << ' '
1872 << viewport.mapX(shape.b().x()) << ',' << viewport.mapY(shape.b().y()) << ' '
1873 << viewport.mapX(shape.c().x()) << ',' << viewport.mapY(shape.c().y()) << "\""
1874 << styleAttributes(element.style) << ">"
1875 << titleTag << "</polygon>";
1876 } else if constexpr (std::same_as<S, Convex<PT>>) {
1877 if (shape.size() == 0) return {};
1878 out << "<polygon points=\"";
1879 bool firstVertex = true;
1880 for (const auto& vertex : shape) {
1881 if (!firstVertex) out << ' ';
1882 firstVertex = false;
1883 out << viewport.mapX(vertex.x()) << ',' << viewport.mapY(vertex.y());
1884 }
1885 out << "\""
1886 << styleAttributes(element.style) << ">"
1887 << titleTag << "</polygon>";
1888 } else if constexpr (std::same_as<S, Polygon<PT>>) {
1889 if (shape.size() == 0) return {};
1890 out << "<polygon points=\"";
1891 bool firstVertex = true;
1892 for (const auto& vertex : shape) {
1893 if (!firstVertex) out << ' ';
1894 firstVertex = false;
1895 out << viewport.mapX(vertex.x()) << ',' << viewport.mapY(vertex.y());
1896 }
1897 out << "\""
1898 << styleAttributes(element.style) << ">"
1899 << titleTag << "</polygon>";
1900 } else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
1901 // A <path> with one closed subpath per ring: the holes are the
1902 // odd-crossing part of it, so the fill rule has to be even-odd
1903 // rather than the SVG default.
1904 const auto rings = regionRings(
1905 shape,
1906 [&](const PT& vertex) {
1907 return std::pair<double, double>(viewport.mapX(vertex.x()), viewport.mapY(vertex.y()));
1908 });
1909 if (rings.empty()) return {};
1910
1911 out << "<path d=\"";
1912 bool firstRing = true;
1913 for (const auto& ring : rings) {
1914 if (!firstRing) out << ' ';
1915 firstRing = false;
1916 out << 'M';
1917 for (const auto& [x, y] : ring) {
1918 out << ' ' << x << ',' << y;
1919 }
1920 out << " Z";
1921 }
1922 out << "\" fill-rule=\"evenodd\""
1923 << styleAttributes(element.style) << ">"
1924 << titleTag << "</path>";
1925 } else if constexpr (std::same_as<S, MonotoneChain<PT>> || std::same_as<S, Polyline<PT>>) {
1926 if (shape.size() == 0) return {};
1927 if (shape.size() == 1) {
1928 const auto vertex = shape[0];
1929 out << "<circle cx=\"" << viewport.mapX(vertex.x())
1930 << "\" cy=\"" << viewport.mapY(vertex.y())
1931 << "\" r=\"" << element.style.pointRadius << '"'
1932 << styleAttributes(element.style) << ">"
1933 << titleTag << "</circle>";
1934 } else {
1935 // An open chain: a polyline, never closed or filled.
1936 out << "<polyline points=\"";
1937 bool firstVertex = true;
1938 for (const auto& vertex : shape) {
1939 if (!firstVertex) out << ' ';
1940 firstVertex = false;
1941 out << viewport.mapX(vertex.x()) << ',' << viewport.mapY(vertex.y());
1942 }
1943 out << "\""
1944 << strokeOnlyAttributes(element.style) << ">"
1945 << titleTag << "</polyline>";
1946 }
1947 } else if constexpr (std::same_as<S, Disk<PT>>) {
1948 const auto center = shape.template center<double>();
1949 const double cx = viewport.mapX(center.x());
1950 const double cy = viewport.mapY(center.y());
1951 const double r = shape.template radius<double>() * viewport.scale;
1952 out << "<circle cx=\"" << cx << "\" cy=\"" << cy
1953 << "\" r=\"" << r << '"'
1954 << styleAttributes(element.style) << ">"
1955 << titleTag << "</circle>";
1956 }
1957 return out.str();
1958 }, element.shape.variant());
1959 }
1960
1961 // ---------------------------------------------------------------
1962 // Ipe (.ipe) export
1963 // ---------------------------------------------------------------
1964
1965 // Ipe's `opacity`/`stroke-opacity` path attributes must reference a
1966 // symbolic name defined in the style sheet, so distinct opacity values
1967 // (rounded to three decimal places) are collected up front and declared
1968 // as `<opacity>` entries named after their rounded per-mille value.
1969 std::vector<int> collectIPEOpacities() const {
1970 std::vector<int> keys;
1971 for (const Element& element : elements_) {
1972 const PDFStyle style = pdfStyleOf(element.style);
1973 if (const std::optional<int> key = ipeStrokeOpacityKey(style)) {
1974 keys.push_back(*key);
1975 }
1976 if (const std::optional<int> key = ipeFillOpacityKey(style)) {
1977 keys.push_back(*key);
1978 }
1979 }
1980 std::sort(keys.begin(), keys.end());
1981 keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
1982 return keys;
1983 }
1984
1985 static std::string ipeOpacityName(int perMille) {
1986 return "op" + std::to_string(perMille);
1987 }
1988
1989 static int ipeOpacityKey(float alpha) {
1990 return static_cast<int>(std::lround(alpha * 1000.0f));
1991 }
1992
1993 // The opacity the fill must name, if any.
1994 static std::optional<int> ipeFillOpacityKey(const PDFStyle& style) {
1995 if (pdfgen::PDF_IS_TRANSPARENT(style.fill) || style.fillAlpha >= 1.0f) {
1996 return std::nullopt;
1997 }
1998 return ipeOpacityKey(style.fillAlpha);
1999 }
2000
2001 // The opacity the stroke must name, if any. Ipe's `opacity` attribute dims
2002 // the whole object rather than just its interior, so a stroke drawn around
2003 // a translucent fill has to name its own opacity — even when that opacity
2004 // is 1 — or it inherits the fill's.
2005 static std::optional<int> ipeStrokeOpacityKey(const PDFStyle& style) {
2006 if (pdfgen::PDF_IS_TRANSPARENT(style.stroke)) {
2007 return std::nullopt;
2008 }
2009 if (style.strokeAlpha >= 1.0f && !ipeFillOpacityKey(style)) {
2010 return std::nullopt;
2011 }
2012 return ipeOpacityKey(style.strokeAlpha);
2013 }
2014
2015 static std::string ipeColorTriplet(std::uint32_t colour) {
2016 std::ostringstream out;
2017 out << pdfgen::PDF_RGB_R(colour) << ' ' << pdfgen::PDF_RGB_G(colour) << ' ' << pdfgen::PDF_RGB_B(colour);
2018 return out.str();
2019 }
2020
2021 static std::string ipeStrokeAttributes(const PDFStyle& style) {
2022 std::ostringstream out;
2023 if (!pdfgen::PDF_IS_TRANSPARENT(style.stroke)) {
2024 out << " stroke=\"" << ipeColorTriplet(style.stroke) << '"';
2025 if (const std::optional<int> key = ipeStrokeOpacityKey(style)) {
2026 out << " stroke-opacity=\"" << ipeOpacityName(*key) << '"';
2027 }
2028 if (style.strokeWidth > 0.0f) {
2029 out << " pen=\"" << style.strokeWidth << '"';
2030 }
2031 }
2032 return out.str();
2033 }
2034
2035 static std::string ipeFillAttributes(const PDFStyle& style) {
2036 std::ostringstream out;
2037 if (!pdfgen::PDF_IS_TRANSPARENT(style.fill)) {
2038 out << " fill=\"" << ipeColorTriplet(style.fill) << '"';
2039 if (const std::optional<int> key = ipeFillOpacityKey(style)) {
2040 out << " opacity=\"" << ipeOpacityName(*key) << '"';
2041 }
2042 }
2043 return out.str();
2044 }
2045
2046 static std::string ipeStyleAttributes(const PDFStyle& style) {
2047 return ipeStrokeAttributes(style) + ipeFillAttributes(style);
2048 }
2049
2050 template <class PointLike>
2051 std::pair<double, double> mapIPEPoint(const PointLike& point, const Viewport& viewport) const {
2052 return {viewport.mapX(point.x()), pdfYFromSVG(viewport.mapY(point.y()))};
2053 }
2054
2055 static void appendIPEPath(
2056 std::ostringstream& out,
2057 const std::vector<std::pair<double, double>>& points,
2058 bool closePath,
2059 const std::string& attributes) {
2060 if (points.empty() || attributes.empty()) return;
2061 out << "<path" << attributes << ">\n"
2062 << points[0].first << ' ' << points[0].second << " m\n";
2063 for (std::size_t index = 1; index < points.size(); ++index) {
2064 out << points[index].first << ' ' << points[index].second << " l\n";
2065 }
2066 if (closePath) out << "h\n";
2067 out << "</path>\n";
2068 }
2069
2071 static void appendIPEPath(
2072 std::ostringstream& out,
2073 const std::vector<std::vector<std::pair<double, double>>>& rings,
2074 const std::string& attributes) {
2075 if (rings.empty() || attributes.empty()) return;
2076 out << "<path" << attributes << ">\n";
2077 for (const auto& ring : rings) {
2078 out << ring[0].first << ' ' << ring[0].second << " m\n";
2079 for (std::size_t index = 1; index < ring.size(); ++index) {
2080 out << ring[index].first << ' ' << ring[index].second << " l\n";
2081 }
2082 out << "h\n";
2083 }
2084 out << "</path>\n";
2085 }
2086
2087 static void appendIPEEllipse(
2088 std::ostringstream& out,
2089 double cx,
2090 double cy,
2091 double radius,
2092 const std::string& attributes) {
2093 if (attributes.empty()) return;
2094 // Ipe draws an ellipse/circle by applying an affine matrix (a b c d e f)
2095 // to the unit circle; a circle of the given radius uses "r 0 0 r cx cy".
2096 out << "<path" << attributes << ">\n"
2097 << radius << " 0 0 " << radius << ' ' << cx << ' ' << cy << " e\n"
2098 << "</path>\n";
2099 }
2100
2101 // Mirrors the arrowhead triangle used for PDF export (addArrowhead) so
2102 // that OrientedSegment/OrientedLine/Ray render consistently across
2103 // SVG's marker-mid, the PDF triangle, and this Ipe triangle.
2104 static std::array<std::pair<double, double>, 3> ipeArrowTriangle(
2105 double startX, double startY, double endX, double endY, double strokeWidth) {
2106 const double dx = endX - startX;
2107 const double dy = endY - startY;
2108 const double length = std::sqrt(dx * dx + dy * dy);
2109 if (length == 0.0) {
2110 return {{{startX, startY}, {startX, startY}, {startX, startY}}};
2111 }
2112
2113 const double ux = dx / length;
2114 const double uy = dy / length;
2115 const double px = -uy;
2116 const double py = ux;
2117 const double size = std::max(6.0, strokeWidth * 4.0);
2118 const double midX = (startX + endX) / 2.0;
2119 const double midY = (startY + endY) / 2.0;
2120 const double tipX = midX + ux * size * 0.6;
2121 const double tipY = midY + uy * size * 0.6;
2122 const double baseCenterX = midX - ux * size * 0.4;
2123 const double baseCenterY = midY - uy * size * 0.4;
2124 const double halfBase = size * 0.35;
2125
2126 return {{
2127 {tipX, tipY},
2128 {baseCenterX + px * halfBase, baseCenterY + py * halfBase},
2129 {baseCenterX - px * halfBase, baseCenterY - py * halfBase},
2130 }};
2131 }
2132
2133 void appendIPEArrowhead(
2134 std::ostringstream& out,
2135 double startX, double startY, double endX, double endY,
2136 const PDFStyle& style) const {
2137 const std::uint32_t colour = arrowColor(style);
2138 if (pdfgen::PDF_IS_TRANSPARENT(colour)) return;
2139
2140 const auto triangle = ipeArrowTriangle(startX, startY, endX, endY, style.strokeWidth);
2141 if (triangle[0] == triangle[1]) return;
2142
2143 std::ostringstream attrs;
2144 attrs << " fill=\"" << ipeColorTriplet(colour) << '"';
2145 const float alpha = arrowAlpha(style);
2146 if (alpha < 1.0f) {
2147 attrs << " opacity=\"" << ipeOpacityName(static_cast<int>(std::lround(alpha * 1000.0f))) << '"';
2148 }
2149 appendIPEPath(out, {triangle[0], triangle[1], triangle[2]}, true, attrs.str());
2150 }
2151
2152 void appendIPEBorder(std::ostringstream& out) const {
2153 const double x1 = 0.5;
2154 const double y1 = 0.5;
2155 const double x2 = std::max(widthPixels_ - 0.5, 0.0);
2156 const double y2 = std::max(heightPixels_ - 0.5, 0.0);
2157 appendIPEPath(out, {{x1, y1}, {x2, y1}, {x2, y2}, {x1, y2}}, true, " stroke=\"0 0 0\" pen=\"1\"");
2158 }
2159
2160 void appendElementToIPE(std::ostringstream& out, const Element& element, const Viewport& viewport) const {
2161 using PT = Point<double>;
2162 const PDFStyle style = pdfStyleOf(element.style);
2163 const std::string attrs = ipeStyleAttributes(style);
2164
2165 std::visit([&](const auto& shape) {
2166 using S = std::decay_t<decltype(shape)>;
2167
2168 if constexpr (std::same_as<S, PT>) {
2169 const auto [cx, cy] = mapIPEPoint(shape, viewport);
2170 appendIPEEllipse(out, cx, cy, style.pointRadius, attrs);
2171 } else if constexpr (std::same_as<S, Segment<PT>>) {
2172 appendIPEPath(out, {mapIPEPoint(shape.min(), viewport), mapIPEPoint(shape.max(), viewport)}, false, attrs);
2173 } else if constexpr (std::same_as<S, OrientedSegment<PT>>) {
2174 const auto p1 = mapIPEPoint(shape.source(), viewport);
2175 const auto p2 = mapIPEPoint(shape.target(), viewport);
2176 appendIPEPath(out, {p1, p2}, false, attrs);
2177 appendIPEArrowhead(out, p1.first, p1.second, p2.first, p2.second, style);
2178 } else if constexpr (std::same_as<S, Line<PT>>) {
2179 const double x1 = viewport.mapX(shape.min().x());
2180 const double y1 = viewport.mapY(shape.min().y());
2181 const double x2 = viewport.mapX(shape.max().x());
2182 const double y2 = viewport.mapY(shape.max().y());
2183 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
2184 if (!visible) return;
2185 const auto& [vx1, vy1, vx2, vy2] = *visible;
2186 if (vx1 == vx2 && vy1 == vy2) {
2187 appendIPEEllipse(out, vx1, pdfYFromSVG(vy1), style.pointRadius, attrs);
2188 } else {
2189 appendIPEPath(out, {{vx1, pdfYFromSVG(vy1)}, {vx2, pdfYFromSVG(vy2)}}, false, attrs);
2190 }
2191 } else if constexpr (std::same_as<S, OrientedLine<PT>>) {
2192 const double x1 = viewport.mapX(shape.source().x());
2193 const double y1 = viewport.mapY(shape.source().y());
2194 const double x2 = viewport.mapX(shape.target().x());
2195 const double y2 = viewport.mapY(shape.target().y());
2196 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
2197 if (!visible) return;
2198 const auto& [vx1, vy1, vx2, vy2] = *visible;
2199 const double p1x = vx1;
2200 const double p1y = pdfYFromSVG(vy1);
2201 const double p2x = vx2;
2202 const double p2y = pdfYFromSVG(vy2);
2203 appendIPEPath(out, {{p1x, p1y}, {p2x, p2y}}, false, attrs);
2204 appendIPEArrowhead(out, p1x, p1y, p2x, p2y, style);
2205 } else if constexpr (std::same_as<S, Ray<PT>>) {
2206 const double x1 = viewport.mapX(shape.source().x());
2207 const double y1 = viewport.mapY(shape.source().y());
2208 const double x2 = viewport.mapX(shape.target().x());
2209 const double y2 = viewport.mapY(shape.target().y());
2210 const auto visible = clipRayToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
2211 if (!visible) return;
2212 const auto& [vx1, vy1, vx2, vy2] = *visible;
2213 const double p1x = vx1;
2214 const double p1y = pdfYFromSVG(vy1);
2215 const double p2x = vx2;
2216 const double p2y = pdfYFromSVG(vy2);
2217 appendIPEPath(out, {{p1x, p1y}, {p2x, p2y}}, false, attrs);
2218 appendIPEArrowhead(out, p1x, p1y, p2x, p2y, style);
2219 } else if constexpr (std::same_as<S, Halfplane<PT>>) {
2220 const auto polygon = clipHalfplaneToViewport(shape, viewport, widthPixels_, heightPixels_);
2221 if (!polygon.empty()) {
2222 std::vector<std::pair<double, double>> points;
2223 points.reserve(polygon.size());
2224 for (const auto& [worldX, worldY] : polygon) {
2225 points.emplace_back(viewport.mapX(worldX), pdfYFromSVG(viewport.mapY(worldY)));
2226 }
2227 appendIPEPath(out, points, true, ipeFillAttributes(style));
2228 }
2229
2230 const double x1 = viewport.mapX(shape.source().x());
2231 const double y1 = viewport.mapY(shape.source().y());
2232 const double x2 = viewport.mapX(shape.target().x());
2233 const double y2 = viewport.mapY(shape.target().y());
2234 const auto visible = clipInfiniteLineToBox(x1, y1, x2, y2, 0.0, 0.0, widthPixels_, heightPixels_);
2235 if (visible) {
2236 appendIPEPath(
2237 out,
2238 {{visible->x1, pdfYFromSVG(visible->y1)}, {visible->x2, pdfYFromSVG(visible->y2)}},
2239 false,
2240 ipeStrokeAttributes(style));
2241 }
2242 } else if constexpr (std::same_as<S, HalfplaneIntersection<PT>>) {
2243 const auto polygon = clipRegionToViewport(shape, viewport, widthPixels_, heightPixels_);
2244 if (!polygon.empty()) {
2245 std::vector<std::pair<double, double>> points;
2246 points.reserve(polygon.size());
2247 for (const auto& [worldX, worldY] : polygon) {
2248 points.emplace_back(viewport.mapX(worldX), pdfYFromSVG(viewport.mapY(worldY)));
2249 }
2250 appendIPEPath(out, points, true, ipeFillAttributes(style));
2251 }
2252
2253 // Stroke only the region's real boundary edges: the viewport
2254 // sides of the clipped polygon are not part of the boundary.
2255 for (const auto& piece : regionBoundaryPieces(shape, viewport)) {
2256 appendIPEPath(
2257 out,
2258 {{piece.x1, pdfYFromSVG(piece.y1)}, {piece.x2, pdfYFromSVG(piece.y2)}},
2259 false,
2260 ipeStrokeAttributes(style));
2261 }
2262 } else if constexpr (std::same_as<S, Rectangle<PT>>) {
2263 if (shape.empty()) return;
2264 const double minX = viewport.mapX(shape.min().x());
2265 const double maxX = viewport.mapX(shape.max().x());
2266 const double minY = pdfYFromSVG(viewport.mapY(shape.max().y()));
2267 const double maxY = pdfYFromSVG(viewport.mapY(shape.min().y()));
2268 appendIPEPath(out, {{minX, minY}, {maxX, minY}, {maxX, maxY}, {minX, maxY}}, true, attrs);
2269 } else if constexpr (std::same_as<S, Triangle<PT>>) {
2270 appendIPEPath(
2271 out,
2272 {mapIPEPoint(shape.a(), viewport), mapIPEPoint(shape.b(), viewport), mapIPEPoint(shape.c(), viewport)},
2273 true,
2274 attrs);
2275 } else if constexpr (std::same_as<S, Convex<PT>> || std::same_as<S, Polygon<PT>>) {
2276 if (shape.size() == 0) return;
2277 std::vector<std::pair<double, double>> points;
2278 points.reserve(shape.size());
2279 for (const auto& vertex : shape) {
2280 points.push_back(mapIPEPoint(vertex, viewport));
2281 }
2282 appendIPEPath(out, points, true, attrs);
2283 } else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
2284 appendIPEPath(
2285 out,
2286 regionRings(shape, [&](const PT& vertex) { return mapIPEPoint(vertex, viewport); }),
2287 attrs);
2288 } else if constexpr (std::same_as<S, MonotoneChain<PT>> || std::same_as<S, Polyline<PT>>) {
2289 if (shape.size() == 0) return;
2290 if (shape.size() == 1) {
2291 const auto [cx, cy] = mapIPEPoint(shape[0], viewport);
2292 appendIPEEllipse(out, cx, cy, style.pointRadius, attrs);
2293 } else {
2294 std::vector<std::pair<double, double>> points;
2295 points.reserve(shape.size());
2296 for (const auto& vertex : shape) {
2297 points.push_back(mapIPEPoint(vertex, viewport));
2298 }
2299 // An open chain: a polyline, never closed or filled.
2300 appendIPEPath(out, points, false, ipeStrokeAttributes(style));
2301 }
2302 } else if constexpr (std::same_as<S, Disk<PT>>) {
2303 const auto center = mapIPEPoint(shape.template center<double>(), viewport);
2304 const double radius = shape.template radius<double>() * viewport.scale;
2305 appendIPEEllipse(out, center.first, center.second, radius, attrs);
2306 }
2307 }, element.shape.variant());
2308 }
2309
2310 CanvasStyle style_{};
2311 std::vector<Element> elements_{};
2312 double zoom_ = 1.0;
2313 double widthPixels_ = 800.0;
2314 double heightPixels_ = 800.0;
2315 double marginPixels_ = 20.0;
2316 bool drawBorder_ = false;
2317 std::optional<Bounds> view_{};
2318
2319 static constexpr double borderInset_ = 20.0;
2320
2321 struct ClippedSegment {
2322 double x1;
2323 double y1;
2324 double x2;
2325 double y2;
2326 };
2327
2328 static std::optional<ClippedSegment> clipInfiniteLineToBox(
2329 double x1,
2330 double y1,
2331 double x2,
2332 double y2,
2333 double minX,
2334 double minY,
2335 double maxX,
2336 double maxY) {
2337 const double dx = x2 - x1;
2338 const double dy = y2 - y1;
2339
2340 if (dx == 0.0 && dy == 0.0) {
2341 if (x1 < minX || maxX < x1 || y1 < minY || maxY < y1) {
2342 return std::nullopt;
2343 }
2344 return ClippedSegment{x1, y1, x1, y1};
2345 }
2346
2347 std::vector<std::pair<double, double>> intersections;
2348 const auto append_if_inside = [&intersections, minX, minY, maxX, maxY](double x, double y) {
2349 const double epsilon = 1e-9;
2350 if (x + epsilon < minX || maxX + epsilon < x || y + epsilon < minY || maxY + epsilon < y) {
2351 return;
2352 }
2353
2354 for (const auto& [existing_x, existing_y] : intersections) {
2355 if (std::abs(existing_x - x) <= epsilon && std::abs(existing_y - y) <= epsilon) {
2356 return;
2357 }
2358 }
2359
2360 intersections.emplace_back(x, y);
2361 };
2362
2363 if (dx != 0.0) {
2364 const double t_left = (minX - x1) / dx;
2365 append_if_inside(minX, y1 + t_left * dy);
2366
2367 const double t_right = (maxX - x1) / dx;
2368 append_if_inside(maxX, y1 + t_right * dy);
2369 }
2370
2371 if (dy != 0.0) {
2372 const double t_top = (minY - y1) / dy;
2373 append_if_inside(x1 + t_top * dx, minY);
2374
2375 const double t_bottom = (maxY - y1) / dy;
2376 append_if_inside(x1 + t_bottom * dx, maxY);
2377 }
2378
2379 if (intersections.size() < 2) {
2380 return std::nullopt;
2381 }
2382
2383 return ClippedSegment{
2384 intersections.front().first,
2385 intersections.front().second,
2386 intersections.back().first,
2387 intersections.back().second,
2388 };
2389 }
2390
2391 static std::optional<ClippedSegment> clipRayToBox(
2392 double x1,
2393 double y1,
2394 double x2,
2395 double y2,
2396 double minX,
2397 double minY,
2398 double maxX,
2399 double maxY) {
2400 const double dx = x2 - x1;
2401 const double dy = y2 - y1;
2402
2403 if (dx == 0.0 && dy == 0.0) {
2404 if (x1 < minX || maxX < x1 || y1 < minY || maxY < y1) {
2405 return std::nullopt;
2406 }
2407 return ClippedSegment{x1, y1, x1, y1};
2408 }
2409
2410 std::vector<std::tuple<double, double, double>> candidates;
2411 const auto append_if_inside = [&candidates, minX, minY, maxX, maxY](double t, double x, double y) {
2412 const double epsilon = 1e-9;
2413 if (t + epsilon < 0.0) {
2414 return;
2415 }
2416 if (x + epsilon < minX || maxX + epsilon < x || y + epsilon < minY || maxY + epsilon < y) {
2417 return;
2418 }
2419
2420 for (const auto& [existingT, existingX, existingY] : candidates) {
2421 if (std::abs(existingT - t) <= epsilon &&
2422 std::abs(existingX - x) <= epsilon &&
2423 std::abs(existingY - y) <= epsilon) {
2424 return;
2425 }
2426 }
2427
2428 candidates.emplace_back(t, x, y);
2429 };
2430
2431 append_if_inside(0.0, x1, y1);
2432
2433 if (dx != 0.0) {
2434 const double t_left = (minX - x1) / dx;
2435 append_if_inside(t_left, minX, y1 + t_left * dy);
2436
2437 const double t_right = (maxX - x1) / dx;
2438 append_if_inside(t_right, maxX, y1 + t_right * dy);
2439 }
2440
2441 if (dy != 0.0) {
2442 const double t_top = (minY - y1) / dy;
2443 append_if_inside(t_top, x1 + t_top * dx, minY);
2444
2445 const double t_bottom = (maxY - y1) / dy;
2446 append_if_inside(t_bottom, x1 + t_bottom * dx, maxY);
2447 }
2448
2449 if (candidates.empty()) {
2450 return std::nullopt;
2451 }
2452
2453 std::sort(candidates.begin(), candidates.end(), [](const auto& left, const auto& right) {
2454 return std::get<0>(left) < std::get<0>(right);
2455 });
2456
2457 if (candidates.size() == 1) {
2458 const auto& [t, x, y] = candidates.front();
2459 (void)t;
2460 return ClippedSegment{x, y, x, y};
2461 }
2462
2463 const auto& [startT, startX, startY] = candidates.front();
2464 const auto& [endT, endX, endY] = candidates.back();
2465 (void)startT;
2466 (void)endT;
2467 return ClippedSegment{startX, startY, endX, endY};
2468 }
2469
2470 // The viewport rectangle as a CCW polygon in world coordinates: the
2471 // Sutherland–Hodgman seed for clipping half-planes and half-plane
2472 // intersections to the visible area.
2473 static std::vector<std::pair<double, double>> viewportPolygon(
2474 const Viewport& viewport,
2475 double widthPixels,
2476 double heightPixels) {
2477 return {
2478 {viewport.unmapX(0.0), viewport.unmapY(heightPixels)},
2479 {viewport.unmapX(widthPixels), viewport.unmapY(heightPixels)},
2480 {viewport.unmapX(widthPixels), viewport.unmapY(0.0)},
2481 {viewport.unmapX(0.0), viewport.unmapY(0.0)},
2482 };
2483 }
2484
2485 static std::vector<std::pair<double, double>> clipPolygonToHalfplane(
2486 const std::vector<std::pair<double, double>>& polygon,
2487 const Halfplane<Point<double>>& halfplane) {
2488 const Point<double> first = halfplane.source();
2489 const Point<double> second = halfplane.target();
2490
2491 const auto inside = [&first, &second](const std::pair<double, double>& point) {
2492 return orientationSign(first, second, Point<double>(point.first, point.second)) != std::partial_ordering::less;
2493 };
2494
2495 const auto intersection = [&first, &second](const std::pair<double, double>& left, const std::pair<double, double>& right) {
2496 const double ax = first.x();
2497 const double ay = first.y();
2498 const double bx = second.x();
2499 const double by = second.y();
2500 const double px = left.first;
2501 const double py = left.second;
2502 const double qx = right.first;
2503 const double qy = right.second;
2504
2505 const double denominator = (bx - ax) * (qy - py) - (by - ay) * (qx - px);
2506 if (denominator == 0.0) {
2507 return left;
2508 }
2509
2510 const double numerator = (px - ax) * (qy - py) - (py - ay) * (qx - px);
2511 const double t = numerator / denominator;
2512 return std::pair<double, double>{
2513 ax + t * (bx - ax),
2514 ay + t * (by - ay),
2515 };
2516 };
2517
2518 std::vector<std::pair<double, double>> output;
2519 for (std::size_t index = 0; index < polygon.size(); ++index) {
2520 const auto& current = polygon[index];
2521 const auto& previous = polygon[(index + polygon.size() - 1) % polygon.size()];
2522 const bool currentInside = inside(current);
2523 const bool previousInside = inside(previous);
2524
2525 if (currentInside) {
2526 if (!previousInside) {
2527 output.push_back(intersection(previous, current));
2528 }
2529 output.push_back(current);
2530 } else if (previousInside) {
2531 output.push_back(intersection(previous, current));
2532 }
2533 }
2534
2535 return output;
2536 }
2537
2538 static std::vector<std::pair<double, double>> clipHalfplaneToViewport(
2539 const Halfplane<Point<double>>& halfplane,
2540 const Viewport& viewport,
2541 double widthPixels,
2542 double heightPixels) {
2543 return clipPolygonToHalfplane(viewportPolygon(viewport, widthPixels, heightPixels), halfplane);
2544 }
2545
2546 // World-space polygon of the region clipped to the viewport rectangle:
2547 // Sutherland–Hodgman against every stored half-plane in turn. Empty for
2548 // the empty region (and for a region that misses the viewport); the whole
2549 // plane keeps the full viewport rectangle.
2550 static std::vector<std::pair<double, double>> clipRegionToViewport(
2551 const HalfplaneIntersection<Point<double>>& region,
2552 const Viewport& viewport,
2553 double widthPixels,
2554 double heightPixels) {
2555 if (region.empty()) {
2556 return {};
2557 }
2558 std::vector<std::pair<double, double>> polygon = viewportPolygon(viewport, widthPixels, heightPixels);
2559 for (const auto& halfplane : region) {
2560 polygon = clipPolygonToHalfplane(polygon, halfplane);
2561 if (polygon.empty()) {
2562 break;
2563 }
2564 }
2565 return polygon;
2566 }
2567
2568 // The visible portions of the region's boundary edges, mapped to SVG
2569 // pixel coordinates: segment edges map directly, ray and line edges are
2570 // clipped to the pixel box like the Ray and Line alternatives.
2571 std::vector<ClippedSegment> regionBoundaryPieces(
2572 const HalfplaneIntersection<Point<double>>& region,
2573 const Viewport& viewport) const {
2574 std::vector<ClippedSegment> pieces;
2575 if (region.empty()) {
2576 return pieces;
2577 }
2578 for (std::size_t index = 0; index < region.size(); ++index) {
2579 std::visit(
2580 [&](const auto& piece) {
2581 using P = std::decay_t<decltype(piece)>;
2582 if constexpr (std::same_as<P, Segment<Point<double>>>) {
2583 pieces.push_back(ClippedSegment{
2584 viewport.mapX(piece.min().x()),
2585 viewport.mapY(piece.min().y()),
2586 viewport.mapX(piece.max().x()),
2587 viewport.mapY(piece.max().y()),
2588 });
2589 } else if constexpr (std::same_as<P, Ray<Point<double>>>) {
2590 const auto visible = clipRayToBox(
2591 viewport.mapX(piece.source().x()), viewport.mapY(piece.source().y()),
2592 viewport.mapX(piece.target().x()), viewport.mapY(piece.target().y()),
2593 0.0, 0.0, widthPixels_, heightPixels_);
2594 if (visible) {
2595 pieces.push_back(*visible);
2596 }
2597 } else {
2598 const auto visible = clipInfiniteLineToBox(
2599 viewport.mapX(piece.min().x()), viewport.mapY(piece.min().y()),
2600 viewport.mapX(piece.max().x()), viewport.mapY(piece.max().y()),
2601 0.0, 0.0, widthPixels_, heightPixels_);
2602 if (visible) {
2603 pieces.push_back(*visible);
2604 }
2605 }
2606 },
2607 region.template edge<double>(index));
2608 }
2609 return pieces;
2610 }
2611};
2612
2613} // namespace pgl
Stores drawable objects and exports them as an SVG image.
Definition canvas.hpp:128
void writeIPE(const std::string &path) const
Serializes the canvas to an Ipe XML file (.ipe).
Definition canvas.hpp:361
Canvas & operator<<(const EmptyShape< PointType > &)
Appends nothing: the empty shape has no geometry to draw.
Definition canvas.hpp:534
std::string toIPE() const
Serializes the canvas contents to an Ipe XML string.
Definition canvas.hpp:378
Canvas & operator<<(const Shape< PointType > &shape)
Appends the currently stored alternative of a runtime shape.
Definition canvas.hpp:540
Canvas & scale(double factor)
Sets the global zoom factor used during SVG export.
Definition canvas.hpp:141
Canvas & operator<<(const Range &objects)
Appends every object in an input range, in iteration order.
Definition canvas.hpp:583
Canvas & operator<<(const Halfplane< PointType, Label > &halfplane)
Appends a half-plane using the current captured style.
Definition canvas.hpp:462
Canvas & operator<<(const Triangle< PointType, Label > &triangle)
Appends a triangle using the current captured style.
Definition canvas.hpp:474
Canvas & operator<<(const Disk< PointType, Label > &disk)
Appends a disk using the current captured style.
Definition canvas.hpp:486
Canvas & borders(bool enabled=true)
Enables or disables the optional border around the SVG.
Definition canvas.hpp:188
Canvas & view(const Rectangle< PointType > &window)
Fits the export to an explicit window of the plane instead of to the inserted geometry.
Definition canvas.hpp:210
Canvas & operator<<(const Ray< PointType, Label > &ray)
Appends a ray using the current captured style.
Definition canvas.hpp:456
Canvas & operator<<(const HalfplaneIntersection< PointType, Label > &region)
Appends a half-plane intersection (clipped to the viewport, like a half-plane) using the current capt...
Definition canvas.hpp:528
Canvas & operator<<(const PolygonSet< PointType, Label > &set)
Appends a polygon set (one subpath per ring of every component) using the current captured style.
Definition canvas.hpp:510
Canvas & height(double heightPixels)
Sets the exported SVG height in pixels.
Definition canvas.hpp:165
Canvas & operator<<(const OrientedSegment< PointType, Label > &segment)
Appends an oriented segment using the current captured style.
Definition canvas.hpp:438
Canvas & operator<<(const Segment< PointType, Label > &segment)
Appends a segment using the current captured style.
Definition canvas.hpp:432
std::string toSVG() const
Serializes the canvas contents to an SVG string.
Definition canvas.hpp:250
Canvas & operator<<(const CanvasCommand &command)
Applies a style command to the current drawing style.
Definition canvas.hpp:419
Canvas & size(double widthPixels, double heightPixels)
Sets the exported SVG size in pixels.
Definition canvas.hpp:178
std::string toPDF() const
Serializes the canvas contents to a PDF byte string.
Definition canvas.hpp:300
Canvas & operator<<(const Convex< PointType, Label > &convex)
Appends a convex polygon using the current captured style.
Definition canvas.hpp:480
Canvas & operator<<(const Polyline< PointType, Label > &polyline)
Appends a polyline (an open, possibly self-intersecting chain) using the current captured style.
Definition canvas.hpp:522
Canvas & operator<<(const Line< PointType, Label > &line)
Appends a line using the current captured style.
Definition canvas.hpp:444
Canvas & margin(double marginPixels)
Sets the margin reserved around the fitted drawing.
Definition canvas.hpp:225
Canvas & operator<<(const OrientedLine< PointType, Label > &line)
Appends an oriented line using the current captured style.
Definition canvas.hpp:450
Canvas & operator<<(const PolygonWithHoles< PointType, Label > &region)
Appends a polygon with holes (one subpath per ring) using the current captured style.
Definition canvas.hpp:498
Canvas & operator<<(const Rectangle< PointType, Label > &rectangle)
Appends a rectangle using the current captured style.
Definition canvas.hpp:468
Canvas & operator<<(const std::variant< Types... > &objects)
Appends the currently stored alternative of a variant.
Definition canvas.hpp:554
Canvas & operator<<(const std::optional< Type > &object)
Appends an optional object when it contains one.
Definition canvas.hpp:568
void writePDF(const std::string &path) const
Serializes the canvas to a PDF file.
Definition canvas.hpp:282
void writeSVG(const std::string &path) const
Serializes the canvas to an SVG file.
Definition canvas.hpp:236
Canvas & width(double widthPixels)
Sets the exported SVG width in pixels.
Definition canvas.hpp:153
Canvas & operator<<(const Polygon< PointType, Label > &polygon)
Appends a (possibly non-convex) polygon using the current captured style.
Definition canvas.hpp:492
Canvas & operator<<(const MonotoneChain< PointType, Label, Storage > &chain)
Appends an x-monotone chain (an SVG polyline) using the current captured style.
Definition canvas.hpp:516
Canvas & operator<<(const Point< Number, Label > &point)
Appends a point using the current captured style.
Definition canvas.hpp:426
Canvas()=default
Creates an empty canvas with default style and viewport settings.
Chebyshev (LInf) distance between shapes.
int pdf_add_rectangle(pdf_doc *pdf, pdf_object *page, float x, float y, float width, float height, float border_width, std::uint32_t colour, float stroke_alpha=1.0f)
Definition pdfgen.hpp:755
constexpr std::uint32_t PDF_TRANSPARENT
Definition pdfgen.hpp:85
int pdf_save_buffer(pdf_doc *pdf, std::string &out)
Definition pdfgen.hpp:875
pdf_doc * pdf_create(float width, float height, const pdf_info *info)
Definition pdfgen.hpp:378
constexpr float PDF_RGB_R(std::uint32_t colour)
Definition pdfgen.hpp:87
int pdf_add_filled_polygon(pdf_doc *pdf, pdf_object *page, const float x[], const float y[], int count, float border_width, std::uint32_t colour, float fill_alpha=1.0f, float stroke_alpha=1.0f)
Definition pdfgen.hpp:850
constexpr std::uint32_t PDF_BLACK
Definition pdfgen.hpp:83
constexpr std::uint32_t PDF_RGB(unsigned int r, unsigned int g, unsigned int b)
Definition pdfgen.hpp:72
void pdf_destroy(pdf_doc *pdf)
Definition pdfgen.hpp:416
int pdf_add_circle(pdf_doc *pdf, pdf_object *page, float x, float y, float radius, float width, std::uint32_t colour, std::uint32_t fill_colour, float fill_alpha=1.0f, float stroke_alpha=1.0f)
Definition pdfgen.hpp:750
const char * pdf_get_err(const pdf_doc *pdf, int *errval)
Definition pdfgen.hpp:262
constexpr bool PDF_IS_TRANSPARENT(std::uint32_t colour)
Definition pdfgen.hpp:99
int pdf_add_custom_path(pdf_doc *pdf, pdf_object *page, const pdf_path_operation *operations, int operation_count, float stroke_width, std::uint32_t stroke_colour, std::uint32_t fill_colour, float fill_alpha=1.0f, float stroke_alpha=1.0f)
Definition pdfgen.hpp:648
int pdf_add_filled_rectangle(pdf_doc *pdf, pdf_object *page, float x, float y, float width, float height, float border_width, std::uint32_t colour_fill, std::uint32_t colour_border, float fill_alpha=1.0f, float stroke_alpha=1.0f)
Definition pdfgen.hpp:786
pdf_object * pdf_append_page(pdf_doc *pdf)
Definition pdfgen.hpp:420
int pdf_add_line(pdf_doc *pdf, pdf_object *page, float x1, float y1, float x2, float y2, float width, std::uint32_t colour, float stroke_alpha=1.0f)
Definition pdfgen.hpp:601
constexpr float PDF_RGB_G(std::uint32_t colour)
Definition pdfgen.hpp:91
constexpr float PDF_RGB_B(std::uint32_t colour)
Definition pdfgen.hpp:95
Definition arrangement.hpp:67
HalfplaneIntersection() -> HalfplaneIntersection< Point<>, NoLabel >
Definition halfplaneintersection.hpp:2308
@ y
Definition intervaltree.hpp:24
@ x
Definition intervaltree.hpp:24
CanvasCommand pointRadius(std::string value)
Creates a command that changes the current point radius.
Definition canvas.hpp:118
CanvasCommand stroke(std::string value)
Creates a command that changes the current stroke color.
Definition canvas.hpp:93
@ edge
Definition bitmatrix.hpp:37
@ vertex
Definition bitmatrix.hpp:37
Point() -> Point< int >
CanvasCommand strokeWidth(std::string value)
Creates a command that changes the current stroke width.
Definition canvas.hpp:113
PolygonSet() -> PolygonSet< Point<>, NoLabel >
Definition polygonset.hpp:1699
OrientedSegment() -> OrientedSegment< Point<>, NoLabel >
CanvasCommand strokeOpacity(std::string value)
Creates a command that changes the current stroke opacity.
Definition canvas.hpp:108
constexpr std::partial_ordering orientationSign(const Point< ANumber, ALabel > &a, const Point< BNumber, BLabel > &b, const Point< CNumber, CLabel > &c)
Classifies the orientation of three points.
Definition orientation.hpp:544
CanvasCommand fillOpacity(std::string value)
Creates a command that changes the current fill opacity.
Definition canvas.hpp:103
Shape(const std::variant< T, Ts... > &) -> Shape< detail::shape_point_type_t< T > >
PolygonWithHoles() -> PolygonWithHoles< Point<>, NoLabel >
Definition polygonwithholes.hpp:3093
Halfplane() -> Halfplane< Point<>, NoLabel >
CanvasCommand fill(std::string value)
Creates a command that changes the current fill color.
Definition canvas.hpp:98
Ray() -> Ray< Point<>, NoLabel >
Polygon() -> Polygon< Point<>, NoLabel >
Definition polygon.hpp:3200
OrientedLine() -> OrientedLine< Point<>, NoLabel >
CanvasProperty
Names the style property targeted by a canvas command.
Definition canvas.hpp:35
@ fillOpacity
Definition canvas.hpp:38
@ strokeOpacity
Definition canvas.hpp:39
@ strokeWidth
Definition canvas.hpp:40
@ pointRadius
Definition canvas.hpp:41
@ stroke
Definition canvas.hpp:36
@ fill
Definition canvas.hpp:37
Trimmed header-only C++ port of PDFGen for Pangolin canvas export.
Deferred style update applied to the current canvas style.
Definition canvas.hpp:47
CanvasProperty property
Definition canvas.hpp:48
std::string value
Definition canvas.hpp:49
SVG style captured for each inserted element.
Definition canvas.hpp:55
std::string fillOpacity
Definition canvas.hpp:58
std::string strokeWidth
Definition canvas.hpp:60
std::string pointRadius
Definition canvas.hpp:61
std::string strokeOpacity
Definition canvas.hpp:59
std::string stroke
Definition canvas.hpp:56
void apply(const CanvasCommand &command)
Applies one style command in place.
Definition canvas.hpp:68
std::string fill
Definition canvas.hpp:57
Closed convex polygon stored by its vertices.
Definition convex.hpp:170
Closed Euclidean disk stored by boundary points plus optional disk label.
Definition disk.hpp:66
The empty set of points in the plane.
Definition emptyshape.hpp:33
Intersection of closed half-planes; convex but possibly unbounded or empty.
Definition halfplaneintersection.hpp:244
Closed half-plane defined by an oriented boundary line.
Definition halfplane.hpp:51
Unoriented infinite line.
Definition line.hpp:52
Weakly x-monotone polyline stored by lexicographically sorted vertices.
Definition monotonechain.hpp:146
Directed infinite line with left/right side semantics plus optional line label.
Definition orientedline.hpp:53
Directed segment preserving source-to-target order plus optional segment label.
Definition orientedsegment.hpp:44
Two-dimensional point with optional label payload.
Definition point.hpp:129
Set of closed regions with pairwise disjoint interiors.
Definition polygonset.hpp:165
Closed region bounded by one outer simple polygon minus disjoint polygonal holes.
Definition polygonwithholes.hpp:89
Closed simple polygon stored by its vertices.
Definition polygon.hpp:59
Open polygonal chain stored in traversal order; may self-intersect.
Definition polyline.hpp:69
Half-infinite line starting from one source point plus optional ray label.
Definition ray.hpp:51
Axis-aligned rectangle stored by minimum and maximum corners.
Definition rectangle.hpp:75
constexpr const PointType & min() const
Returns the minimum corner (min x, min y).
Definition rectangle.hpp:347
constexpr const PointType & max() const
Returns the maximum corner (max x, max y).
Definition rectangle.hpp:359
Unoriented closed segment between two endpoints plus optional segment label.
Definition segment.hpp:58
Runtime variant wrapper over the supported primitive shapes.
Definition shape.hpp:160
constexpr const Variant & variant() const
Returns the underlying variant.
Definition shape.hpp:264
Closed triangle stored by three vertices.
Definition triangle.hpp:53
Definition pdfgen.hpp:234
Definition pdfgen.hpp:38
char creator[64]
Definition pdfgen.hpp:39
char title[64]
Definition pdfgen.hpp:41
char producer[64]
Definition pdfgen.hpp:40
Definition pdfgen.hpp:221