142 requireStrictlyPositive(factor,
"scale");
154 requireStrictlyPositive(widthPixels,
"width");
155 widthPixels_ = widthPixels;
166 requireStrictlyPositive(heightPixels,
"height");
167 heightPixels_ = heightPixels;
189 drawBorder_ = enabled;
209 template <
class Po
intType>
213 bounds.include(box.
min().x(), box.
min().y());
214 bounds.include(box.
max().x(), box.
max().y());
226 requireNonNegative(marginPixels,
"margin");
227 marginPixels_ = marginPixels;
237 std::ofstream output(path);
239 throw std::runtime_error(
"Could not open SVG output file: " + path);
251 const Bounds bounds = computeBounds();
252 const Viewport viewport = computeViewport(bounds);
254 std::ostringstream out;
255 appendDocumentOpen(out);
257 if (needsArrowheadDefinition()) {
258 appendArrowheadDefinition(out);
265 for (
const Element& element : elements_) {
266 out <<
" " << elementToSVG(element, viewport) <<
"\n";
283 std::ofstream output(path, std::ios::binary);
285 throw std::runtime_error(
"Could not open PDF output file: " + path);
288 const std::string pdf =
toPDF();
289 output.write(pdf.data(),
static_cast<std::streamsize
>(pdf.size()));
291 throw std::runtime_error(
"Could not write PDF output file: " + path);
301 const Bounds bounds = computeBounds();
302 const Viewport viewport = computeViewport(bounds);
305 std::snprintf(info.
creator,
sizeof(info.
creator),
"%s",
"pgl::Canvas");
307 std::snprintf(info.
title,
sizeof(info.
title),
"%s",
"Pangolin Canvas Export");
310 pdfgen::pdf_create(
static_cast<float>(widthPixels_),
static_cast<float>(heightPixels_), &info),
314 throw std::runtime_error(
"Could not create PDF document.");
318 if (page ==
nullptr) {
319 throwPDFError(pdf.get(),
"append PDF page");
328 static_cast<float>(std::max(widthPixels_ - 1.0, 0.0)),
329 static_cast<float>(std::max(heightPixels_ - 1.0, 0.0)),
334 throwPDFError(pdf.get(),
"draw PDF border");
338 for (
const Element& element : elements_) {
339 appendElementToPDF(pdf.get(), page, element, viewport);
344 throwPDFError(pdf.get(),
"serialize PDF");
362 std::ofstream output(path);
364 throw std::runtime_error(
"Could not open IPE output file: " + path);
369 throw std::runtime_error(
"Could not write IPE output file: " + path);
379 const Bounds bounds = computeBounds();
380 const Viewport viewport = computeViewport(bounds);
381 const std::vector<int> opacities = collectIPEOpacities();
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";
393 out <<
"</ipestyle>\n";
395 out <<
"<layer name=\"alpha\"/>\n";
396 out <<
"<view layers=\"alpha\" active=\"alpha\"/>\n";
399 appendIPEBorder(out);
402 for (
const Element& element : elements_) {
403 appendElementToIPE(out, element, viewport);
420 style_.apply(command);
425 template <
class Number,
class Label>
431 template <
class Po
intType,
class Label>
437 template <
class Po
intType,
class Label>
443 template <
class Po
intType,
class Label>
449 template <
class Po
intType,
class Label>
455 template <
class Po
intType,
class Label>
461 template <
class Po
intType,
class Label>
467 template <
class Po
intType,
class Label>
473 template <
class Po
intType,
class Label>
479 template <
class Po
intType,
class Label>
485 template <
class Po
intType,
class Label>
491 template <
class Po
intType,
class Label>
497 template <
class Po
intType,
class Label>
509 template <
class Po
intType,
class Label>
515 template <
class Po
intType,
class Label,
class Storage>
521 template <
class Po
intType,
class Label>
527 template <
class Po
intType,
class Label>
533 template <
class Po
intType>
539 template <
class Po
intType>
542 [
this](
const auto& value) {
550 template <
class... Types>
551 requires (
requires(
Canvas& canvas,
const Types& value) {
556 [
this](
const auto& value) {
564 template <
class Type>
565 requires requires(
Canvas& canvas,
const Type& value) {
576 template <std::ranges::input_range Range>
577 requires std::ranges::input_range<const Range> &&
requires(
579 std::ranges::range_reference_t<const Range> object
584 for (
const auto&
object : objects) {
596 bool initialized =
false;
598 void include(
double x,
double y) {
608 minX = std::min(minX,
x);
609 minY = std::min(minY,
y);
610 maxX = std::max(maxX,
x);
611 maxY = std::max(maxY,
y);
614 void include(
const Bounds& other) {
615 if (!other.initialized) {
619 include(other.minX, other.minY);
620 include(other.maxX, other.maxY);
626 double offsetX = 0.0;
627 double offsetY = 0.0;
629 double mapX(
double x)
const {
630 return offsetX + scale *
x;
633 double mapY(
double y)
const {
634 return offsetY - scale *
y;
637 double unmapX(
double x)
const {
638 return (
x - offsetX) / scale;
641 double unmapY(
double y)
const {
642 return (offsetY -
y) / scale;
651 Bounds bounds()
const {
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>>>) {
666 for (
const auto& vertexPoint : value.template vertices<double>()) {
667 b.include(vertexPoint.x(), vertexPoint.y());
669 for (
const auto& halfplane : value) {
670 b.include(halfplane.source().x(), halfplane.source().y());
671 b.include(halfplane.target().x(), halfplane.target().y());
673 }
else if constexpr (std::same_as<V, PolygonWithHoles<Point<double>>>) {
676 for (
const auto&
vertex : value.outer()) {
679 }
else if constexpr (std::same_as<V, PolygonSet<Point<double>>>) {
681 for (
const auto& component : value) {
682 for (
const auto&
vertex : component.outer()) {
687 for (std::size_t i = 0; i < value.size(); ++i) {
688 b.include(value[i].
x(), value[i].
y());
696 static void requireStrictlyPositive(
double value,
const char* what) {
698 throw std::invalid_argument(std::string(
"Canvas ") + what +
" must be strictly positive.");
702 static void requireNonNegative(
double value,
const char* what) {
704 throw std::invalid_argument(std::string(
"Canvas ") + what +
" must be non-negative.");
708 static std::string toString(
double value) {
709 std::ostringstream out;
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) {
720 std::size_t end = value.size();
721 while (end > start && std::isspace(
static_cast<unsigned char>(value[end - 1])) != 0) {
725 return value.substr(start, end - start);
732 struct ParsedLength {
737 static std::optional<ParsedLength> parseLength(
const std::string& value) {
738 const std::string trimmed = trim(value);
739 if (trimmed.empty()) {
743 std::size_t parsedCharacters = 0;
745 const double numericValue = std::stod(trimmed, &parsedCharacters);
746 return ParsedLength{numericValue, trimmed.substr(parsedCharacters)};
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")) {
760 return parsed->value;
764 static std::optional<double> parseOpacity(
const std::string& value) {
765 const std::optional<ParsedLength> parsed = parseLength(value);
769 if (parsed->suffix ==
"%") {
770 return parsed->value / 100.0;
772 if (parsed->suffix.empty()) {
773 return parsed->value;
778 static std::string escapeXML(
const std::string& value,
bool escapeQuotes) {
779 std::ostringstream out;
780 for (
const char character : value) {
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\"";
820 static std::string strokeOnlyAttributes(
const CanvasStyle& style) {
821 CanvasStyle unfilled = style;
822 unfilled.fill =
"none";
823 return styleAttributes(unfilled);
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) <<
'"';
834 static std::string titleOf(
const T&
object) {
835 std::ostringstream out;
840 template <
class Stored,
class Original>
841 Canvas& push(Stored&& stored,
const Original& original) {
842 elements_.push_back({
850 bool needsArrowheadDefinition()
const {
851 for (
const Element& element : elements_) {
861 double padding()
const {
862 double value = marginPixels_;
864 value += borderInset_;
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);
871 if (
const std::optional<double> radius = parseNumericLength(element.style.pointRadius)) {
872 value = std::max(value, marginPixels_ + *radius);
879 Bounds computeBounds()
const {
885 for (
const Element& element : elements_) {
886 bounds.include(element.bounds());
891 Viewport computeViewport(
const Bounds& bounds)
const {
893 if (!bounds.initialized) {
894 viewport.offsetX = widthPixels_ / 2.0;
895 viewport.offsetY = heightPixels_ / 2.0;
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;
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;
908 double fittedScale = std::min(scaleX, scaleY);
909 if (!std::isfinite(fittedScale)) {
913 viewport.scale = fittedScale * zoom_;
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;
920 viewport.offsetX = inset + extraX - bounds.minX * viewport.scale;
921 viewport.offsetY = heightPixels_ - inset - extraY + bounds.minY * viewport.scale;
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";
931 static void appendArrowheadDefinition(std::ostringstream& out) {
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";
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";
948 float strokeWidth = 1.0f;
949 float pointRadius = 3.0f;
950 float fillAlpha = 1.0f;
951 float strokeAlpha = 1.0f;
954 static void throwPDFError(
const pdfgen::pdf_doc* pdf,
const std::string& action) {
957 std::ostringstream out;
958 out <<
"Could not " << action <<
": " << (message !=
nullptr ? message :
"unknown PDF error");
960 out <<
" (" << errval <<
')';
962 throw std::runtime_error(out.str());
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);
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);
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));
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');
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;
1002 return static_cast<unsigned int>((*highNibble << 4) | *lowNibble);
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;
1010 std::size_t parsedCharacters = 0;
1012 const int parsed = std::stoi(component, &parsedCharacters);
1013 if (parsedCharacters != component.size() || parsed < 0 || parsed > 255) {
1014 return std::nullopt;
1016 return static_cast<unsigned int>(parsed);
1018 return std::nullopt;
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;
1027 if (normalized ==
"none") {
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) {
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) {
1046 static_cast<unsigned int>(*red * 17),
1047 static_cast<unsigned int>(*green * 17),
1048 static_cast<unsigned int>(*blue * 17)
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));
1063 parts.push_back(body.substr(start, comma - start));
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) {
1079 static constexpr std::array<std::pair<const char*, std::uint32_t>, 148> namedColors{{
1229 for (
const auto& [name, colour] : namedColors) {
1230 if (normalized == name) {
1235 return std::nullopt;
1238 static PDFStyle pdfStyleOf(
const CanvasStyle& style) {
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);
1249 float pdfYFromSVG(
double svgY)
const {
1250 return static_cast<float>(heightPixels_ - svgY);
1253 template <
class Po
intLike>
1254 std::pair<float, float> mapPDFPoint(
const PointLike& point,
const Viewport& viewport)
const {
1256 static_cast<float>(viewport.mapX(point.x())),
1257 pdfYFromSVG(viewport.mapY(point.y())),
1261 static std::vector<pdfgen::pdf_path_operation> polygonPathOperations(
1262 const std::vector<std::pair<float, float>>& points,
1264 std::vector<pdfgen::pdf_path_operation> operations;
1265 if (points.empty()) {
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});
1275 operations.push_back({
'h', 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f});
1286 template <
class MapPo
int>
1288 using Mapped = std::decay_t<
decltype(map(std::declval<
const Point<double>&>()))>;
1289 std::vector<std::vector<Mapped>> rings;
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));
1297 if (reversed) std::reverse(points.begin(), points.end());
1298 rings.push_back(std::move(points));
1301 rings.reserve(region.holeCount() + 1);
1302 append(region.outer(),
false);
1303 for (
const auto& hole : region.holes()) {
1314 template <
class MapPo
int>
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));
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());
1339 static std::uint32_t arrowColor(
const PDFStyle& style) {
1343 static float arrowAlpha(
const PDFStyle& style) {
1348 pdfgen::pdf_doc* pdf,
1349 pdfgen::pdf_object* page,
1354 const PDFStyle& style)
const {
1355 const std::uint32_t colour = arrowColor(style);
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) {
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;
1380 const float xs[] = {
1382 baseCenterX + px * halfBase,
1383 baseCenterX - px * halfBase,
1385 const float ys[] = {
1387 baseCenterY + py * halfBase,
1388 baseCenterY - py * halfBase,
1391 throwPDFError(pdf,
"draw PDF arrowhead");
1396 pdfgen::pdf_doc* pdf,
1397 pdfgen::pdf_object* page,
1398 const std::vector<std::pair<float, float>>& points,
1400 const PDFStyle& style,
1402 if (points.empty()) {
1405 const auto operations = polygonPathOperations(points, closePath);
1411 static_cast<int>(operations.size()),
1416 style.strokeAlpha) < 0) {
1417 throwPDFError(pdf,
"draw PDF path");
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()) {
1435 static_cast<int>(operations.size()),
1440 style.strokeAlpha) < 0) {
1441 throwPDFError(pdf,
"draw PDF path");
1445 void appendElementToPDF(
1446 pdfgen::pdf_doc* pdf,
1447 pdfgen::pdf_object* page,
1448 const Element& element,
1449 const Viewport& viewport)
const {
1451 const PDFStyle style = pdfStyleOf(element.style);
1453 std::visit([&](
const auto& shape) {
1454 using S = std::decay_t<
decltype(shape)>;
1456 if constexpr (std::same_as<S, PT>) {
1457 const auto [
x,
y] = mapPDFPoint(shape, viewport);
1468 style.strokeAlpha) < 0) {
1469 throwPDFError(pdf,
"draw PDF point");
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");
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");
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) {
1498 static_cast<float>(vx1),
1505 style.strokeAlpha) < 0) {
1506 throwPDFError(pdf,
"draw PDF degenerate line");
1511 static_cast<float>(vx1),
1513 static_cast<float>(vx2),
1517 style.strokeAlpha) < 0) {
1518 throwPDFError(pdf,
"draw PDF line");
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");
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");
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))
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_);
1581 static_cast<float>(visible->x1),
1582 pdfYFromSVG(visible->y1),
1583 static_cast<float>(visible->x2),
1584 pdfYFromSVG(visible->y2),
1587 style.strokeAlpha) < 0) {
1588 throwPDFError(pdf,
"draw PDF halfplane boundary");
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))
1613 for (
const auto& piece : regionBoundaryPieces(shape, viewport)) {
1617 static_cast<float>(piece.x1),
1618 pdfYFromSVG(piece.y1),
1619 static_cast<float>(piece.x2),
1620 pdfYFromSVG(piece.y2),
1623 style.strokeAlpha) < 0) {
1624 throwPDFError(pdf,
"draw PDF half-plane intersection boundary");
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())));
1644 style.strokeAlpha) < 0) {
1645 throwPDFError(pdf,
"draw PDF rectangle");
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));
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));
1664 addPath(pdf, page, points,
true, style);
1665 }
else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
1669 regionRings(shape, [&](
const PT&
vertex) {
return mapPDFPoint(
vertex, viewport); }),
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);
1685 style.strokeAlpha) < 0) {
1686 throwPDFError(pdf,
"draw PDF chain point");
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));
1699 PDFStyle{style.stroke,
pdfgen::PDF_TRANSPARENT, style.strokeWidth, style.pointRadius, 1.0f, style.strokeAlpha}
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);
1715 style.strokeAlpha) < 0) {
1716 throwPDFError(pdf,
"draw PDF disk");
1719 }, element.shape.variant());
1722 std::string elementToSVG(
const Element& element,
const Viewport& viewport)
const {
1724 const std::string titleTag =
"<title>" + escapeXML(element.title,
false) +
"</title>";
1726 return std::visit([&](
const auto& shape) -> std::string {
1727 using S = std::decay_t<
decltype(shape)>;
1728 std::ostringstream out;
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>";
1773 out <<
"<line x1=\"" << vx1 <<
"\" y1=\"" << vy1
1774 <<
"\" x2=\"" << vx2 <<
"\" y2=\"" << vy2 <<
"\""
1775 << styleAttributes(element.style) <<
">"
1776 << titleTag <<
"</line>";
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 {};
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);
1819 out <<
"\" stroke=\"none\"" << fillAttributes(element.style) <<
">"
1820 << titleTag <<
"</polygon>";
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_);
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>";
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 {};
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);
1847 out <<
"\" stroke=\"none\"" << fillAttributes(element.style) <<
">"
1848 << titleTag <<
"</polygon>";
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>";
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());
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());
1898 << styleAttributes(element.style) <<
">"
1899 << titleTag <<
"</polygon>";
1900 }
else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
1904 const auto rings = regionRings(
1907 return std::pair<double, double>(viewport.mapX(
vertex.x()), viewport.mapY(
vertex.y()));
1909 if (rings.empty())
return {};
1911 out <<
"<path d=\"";
1912 bool firstRing =
true;
1913 for (
const auto& ring : rings) {
1914 if (!firstRing) out <<
' ';
1917 for (
const auto& [
x,
y] : ring) {
1918 out <<
' ' <<
x <<
',' <<
y;
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>";
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());
1944 << strokeOnlyAttributes(element.style) <<
">"
1945 << titleTag <<
"</polyline>";
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>";
1958 }, element.shape.variant());
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);
1976 if (
const std::optional<int> key = ipeFillOpacityKey(style)) {
1977 keys.push_back(*key);
1980 std::sort(keys.begin(), keys.end());
1981 keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
1985 static std::string ipeOpacityName(
int perMille) {
1986 return "op" + std::to_string(perMille);
1989 static int ipeOpacityKey(
float alpha) {
1990 return static_cast<int>(std::lround(alpha * 1000.0f));
1994 static std::optional<int> ipeFillOpacityKey(
const PDFStyle& style) {
1996 return std::nullopt;
1998 return ipeOpacityKey(style.fillAlpha);
2005 static std::optional<int> ipeStrokeOpacityKey(
const PDFStyle& style) {
2007 return std::nullopt;
2009 if (style.strokeAlpha >= 1.0f && !ipeFillOpacityKey(style)) {
2010 return std::nullopt;
2012 return ipeOpacityKey(style.strokeAlpha);
2015 static std::string ipeColorTriplet(std::uint32_t colour) {
2016 std::ostringstream out;
2021 static std::string ipeStrokeAttributes(
const PDFStyle& style) {
2022 std::ostringstream out;
2024 out <<
" stroke=\"" << ipeColorTriplet(style.stroke) <<
'"';
2025 if (
const std::optional<int> key = ipeStrokeOpacityKey(style)) {
2026 out <<
" stroke-opacity=\"" << ipeOpacityName(*key) <<
'"';
2028 if (style.strokeWidth > 0.0f) {
2029 out <<
" pen=\"" << style.strokeWidth <<
'"';
2035 static std::string ipeFillAttributes(
const PDFStyle& style) {
2036 std::ostringstream out;
2038 out <<
" fill=\"" << ipeColorTriplet(style.fill) <<
'"';
2039 if (
const std::optional<int> key = ipeFillOpacityKey(style)) {
2040 out <<
" opacity=\"" << ipeOpacityName(*key) <<
'"';
2046 static std::string ipeStyleAttributes(
const PDFStyle& style) {
2047 return ipeStrokeAttributes(style) + ipeFillAttributes(style);
2050 template <
class Po
intLike>
2051 std::pair<double, double> mapIPEPoint(
const PointLike& point,
const Viewport& viewport)
const {
2052 return {viewport.mapX(point.x()), pdfYFromSVG(viewport.mapY(point.y()))};
2055 static void appendIPEPath(
2056 std::ostringstream& out,
2057 const std::vector<std::pair<double, double>>& points,
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";
2066 if (closePath) out <<
"h\n";
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";
2087 static void appendIPEEllipse(
2088 std::ostringstream& out,
2092 const std::string& attributes) {
2093 if (attributes.empty())
return;
2096 out <<
"<path" << attributes <<
">\n"
2097 << radius <<
" 0 0 " << radius <<
' ' << cx <<
' ' << cy <<
" e\n"
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}}};
2113 const double ux = dx / length;
2114 const double uy = dy / length;
2115 const double px = -uy;
2116 const double py = ux;
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;
2128 {baseCenterX + px * halfBase, baseCenterY + py * halfBase},
2129 {baseCenterX - px * halfBase, baseCenterY - py * halfBase},
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);
2140 const auto triangle = ipeArrowTriangle(startX, startY, endX, endY, style.strokeWidth);
2141 if (triangle[0] == triangle[1])
return;
2143 std::ostringstream attrs;
2144 attrs <<
" fill=\"" << ipeColorTriplet(colour) <<
'"';
2145 const float alpha = arrowAlpha(style);
2147 attrs <<
" opacity=\"" << ipeOpacityName(
static_cast<int>(std::lround(alpha * 1000.0f))) <<
'"';
2149 appendIPEPath(out, {triangle[0], triangle[1], triangle[2]},
true, attrs.str());
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\"");
2160 void appendElementToIPE(std::ostringstream& out,
const Element& element,
const Viewport& viewport)
const {
2162 const PDFStyle style = pdfStyleOf(element.style);
2163 const std::string attrs = ipeStyleAttributes(style);
2165 std::visit([&](
const auto& shape) {
2166 using S = std::decay_t<
decltype(shape)>;
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);
2189 appendIPEPath(out, {{vx1, pdfYFromSVG(vy1)}, {vx2, pdfYFromSVG(vy2)}},
false, attrs);
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)));
2227 appendIPEPath(out, points,
true, ipeFillAttributes(style));
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_);
2238 {{visible->x1, pdfYFromSVG(visible->y1)}, {visible->x2, pdfYFromSVG(visible->y2)}},
2240 ipeStrokeAttributes(style));
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)));
2250 appendIPEPath(out, points,
true, ipeFillAttributes(style));
2255 for (
const auto& piece : regionBoundaryPieces(shape, viewport)) {
2258 {{piece.x1, pdfYFromSVG(piece.y1)}, {piece.x2, pdfYFromSVG(piece.y2)}},
2260 ipeStrokeAttributes(style));
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>>) {
2272 {mapIPEPoint(shape.a(), viewport), mapIPEPoint(shape.b(), viewport), mapIPEPoint(shape.c(), viewport)},
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));
2282 appendIPEPath(out, points,
true, attrs);
2283 }
else if constexpr (std::same_as<S, PolygonWithHoles<PT>> || std::same_as<S, PolygonSet<PT>>) {
2286 regionRings(shape, [&](
const PT&
vertex) {
return mapIPEPoint(
vertex, viewport); }),
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);
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));
2300 appendIPEPath(out, points,
false, ipeStrokeAttributes(style));
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);
2307 }, element.shape.variant());
2310 CanvasStyle style_{};
2311 std::vector<Element> elements_{};
2313 double widthPixels_ = 800.0;
2314 double heightPixels_ = 800.0;
2315 double marginPixels_ = 20.0;
2316 bool drawBorder_ =
false;
2317 std::optional<Bounds> view_{};
2319 static constexpr double borderInset_ = 20.0;
2321 struct ClippedSegment {
2328 static std::optional<ClippedSegment> clipInfiniteLineToBox(
2337 const double dx = x2 - x1;
2338 const double dy = y2 - y1;
2340 if (dx == 0.0 && dy == 0.0) {
2341 if (x1 < minX || maxX < x1 || y1 < minY || maxY < y1) {
2342 return std::nullopt;
2344 return ClippedSegment{x1, y1, x1, y1};
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) {
2354 for (
const auto& [existing_x, existing_y] : intersections) {
2355 if (std::abs(existing_x -
x) <= epsilon && std::abs(existing_y -
y) <= epsilon) {
2360 intersections.emplace_back(
x,
y);
2364 const double t_left = (minX - x1) / dx;
2365 append_if_inside(minX, y1 + t_left * dy);
2367 const double t_right = (maxX - x1) / dx;
2368 append_if_inside(maxX, y1 + t_right * dy);
2372 const double t_top = (minY - y1) / dy;
2373 append_if_inside(x1 + t_top * dx, minY);
2375 const double t_bottom = (maxY - y1) / dy;
2376 append_if_inside(x1 + t_bottom * dx, maxY);
2379 if (intersections.size() < 2) {
2380 return std::nullopt;
2383 return ClippedSegment{
2384 intersections.front().first,
2385 intersections.front().second,
2386 intersections.back().first,
2387 intersections.back().second,
2391 static std::optional<ClippedSegment> clipRayToBox(
2400 const double dx = x2 - x1;
2401 const double dy = y2 - y1;
2403 if (dx == 0.0 && dy == 0.0) {
2404 if (x1 < minX || maxX < x1 || y1 < minY || maxY < y1) {
2405 return std::nullopt;
2407 return ClippedSegment{x1, y1, x1, y1};
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) {
2416 if (
x + epsilon < minX || maxX + epsilon <
x ||
y + epsilon < minY || maxY + epsilon <
y) {
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) {
2428 candidates.emplace_back(t,
x,
y);
2431 append_if_inside(0.0, x1, y1);
2434 const double t_left = (minX - x1) / dx;
2435 append_if_inside(t_left, minX, y1 + t_left * dy);
2437 const double t_right = (maxX - x1) / dx;
2438 append_if_inside(t_right, maxX, y1 + t_right * dy);
2442 const double t_top = (minY - y1) / dy;
2443 append_if_inside(t_top, x1 + t_top * dx, minY);
2445 const double t_bottom = (maxY - y1) / dy;
2446 append_if_inside(t_bottom, x1 + t_bottom * dx, maxY);
2449 if (candidates.empty()) {
2450 return std::nullopt;
2453 std::sort(candidates.begin(), candidates.end(), [](
const auto& left,
const auto& right) {
2454 return std::get<0>(left) < std::get<0>(right);
2457 if (candidates.size() == 1) {
2458 const auto& [t,
x,
y] = candidates.front();
2460 return ClippedSegment{
x,
y,
x,
y};
2463 const auto& [startT, startX, startY] = candidates.front();
2464 const auto& [endT, endX, endY] = candidates.back();
2467 return ClippedSegment{startX, startY, endX, endY};
2473 static std::vector<std::pair<double, double>> viewportPolygon(
2474 const Viewport& viewport,
2476 double heightPixels) {
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)},
2485 static std::vector<std::pair<double, double>> clipPolygonToHalfplane(
2486 const std::vector<std::pair<double, double>>& polygon,
2491 const auto inside = [&first, &second](
const std::pair<double, double>& point) {
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;
2505 const double denominator = (bx - ax) * (qy - py) - (by - ay) * (qx - px);
2506 if (denominator == 0.0) {
2510 const double numerator = (px - ax) * (qy - py) - (py - ay) * (qx - px);
2511 const double t = numerator / denominator;
2512 return std::pair<double, double>{
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);
2525 if (currentInside) {
2526 if (!previousInside) {
2527 output.push_back(intersection(previous, current));
2529 output.push_back(current);
2530 }
else if (previousInside) {
2531 output.push_back(intersection(previous, current));
2538 static std::vector<std::pair<double, double>> clipHalfplaneToViewport(
2540 const Viewport& viewport,
2542 double heightPixels) {
2543 return clipPolygonToHalfplane(viewportPolygon(viewport, widthPixels, heightPixels), halfplane);
2550 static std::vector<std::pair<double, double>> clipRegionToViewport(
2552 const Viewport& viewport,
2554 double heightPixels) {
2555 if (region.empty()) {
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()) {
2571 std::vector<ClippedSegment> regionBoundaryPieces(
2573 const Viewport& viewport)
const {
2574 std::vector<ClippedSegment> pieces;
2575 if (region.empty()) {
2578 for (std::size_t index = 0; index < region.size(); ++index) {
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()),
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_);
2595 pieces.push_back(*visible);
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_);
2603 pieces.push_back(*visible);