1#pragma once23#include "svg_engine.h"45#include <algorithm>6#include <cmath>7#include <fstream>8#include <functional>9#include <initializer_list>10#include <map>11#include <memory>12#include <stdexcept>13#include <string>14#include <utility>15#include <vector>1617namespace jngen {18namespace drawing {1920class Drawer {21public:22 Drawer();2324 template<typename P>25 void point(const P& p);26 template<typename T>27 void point(T x, T y);2829 template<typename P>30 void circle(const P& p, double radius);31 template<typename T>32 void circle(T x, T y, double radius);3334 template<typename P>35 void segment(const P& p1, const P& p2);36 template<typename T>37 void segment(T x1, T y1, T x2, T y2);3839 template<typename P>40 void polygon(const std::vector<P>& points);41 template<typename P>42 void polygon(std::initializer_list<P> points);4344 void setWidth(double width);4546 void setColor(const std::string& color);4748 void setStroke(const std::string& color);4950 void setFill(const std::string& color);5152 void setOpacity(double opacity);5354 void enableGrid(bool value) {55 gridEnabled_ = value;56 }5758 void dumpSvg(const std::string& filename);5960private:61 struct Point {62 double x, y;63 Point() {}64 Point(double x, double y) : x(x), y(y) {}65 };6667 template<typename T>68 Point extractCoords(const T& pt) {69 return Point(pt.x, pt.y);70 }7172 template<typename T>73 Point extractCoords(const std::pair<T, T>& pt) {74 return Point(pt.first, pt.second);75 }7677 typedef std::function<void(DrawingEngine*)> DrawRequest;7879 typedef std::pair<Point, Point> Bbox;8081 static Bbox emptyBbox();8283 static Bbox unite(const Bbox& lhs, const Bbox& rhs);8485 static Bbox bbox(const Point& p);86 static Bbox bbox(const std::pair<Point, double>& circle);8788 Bbox getBbox() const;8990 static Bbox viewportByBbox(const Bbox& bbox);9192 void drawAll();93 void drawGrid(const Bbox& bbox);9495 std::vector<DrawRequest> requests_;9697 DrawingEngine* engine_;98 Bbox bbox_;99 int requestId_ = 0;100 bool gridEnabled_ = true;101};102103template<typename P>104void Drawer::point(const P& p_) {105 Point p = extractCoords(p_);106 bbox_ = unite(bbox_ , bbox(Point(p.x, p.y)));107 requests_.push_back([p](DrawingEngine* engine) {108 engine->drawPoint(p.x, p.y);109 });110}111112template<typename T>113void Drawer::point(T x, T y) {114 point(Point(x, y));115}116117template<typename P>118void Drawer::circle(const P& p_, double radius) {119 Point p = extractCoords(p_);120 bbox_ = unite(bbox_ , bbox({Point(p.x, p.y), radius}));121 requests_.push_back([p, radius](DrawingEngine* engine) {122 engine->drawCircle(p.x, p.y, radius);123 });124}125126template<typename T>127void Drawer::circle(T x, T y, double radius) {128 circle(Point(x, y), radius);129}130131template<typename P>132void Drawer::segment(const P& p1_, const P& p2_) {133 Point p1 = extractCoords(p1_);134 Point p2 = extractCoords(p2_);135 bbox_ = unite(bbox_ , bbox(Point(p1.x, p1.y)));136 bbox_ = unite(bbox_ , bbox(Point(p2.x, p2.y)));137 requests_.push_back([p1, p2](DrawingEngine* engine) {138 engine->drawSegment(p1.x, p1.y, p2.x, p2.y);139 });140}141142template<typename T>143void Drawer::segment(T x1, T y1, T x2, T y2) {144 segment(Point(x1, y1), Point(x2, y2));145}146147template<typename P>148void Drawer::polygon(const std::vector<P>& points) {149 for (const auto& p: points) {150 bbox_ = unite(bbox_, bbox(extractCoords(p)));151 }152153 requests_.push_back([points, this](DrawingEngine* engine) {154 std::vector<std::pair<double, double>> enginePoints;155 for (const auto& p: points) {156 Point pt = extractCoords(p);157 enginePoints.emplace_back(pt.x, pt.y);158 }159 engine->drawPolygon(enginePoints);160 });161}162163template<typename P>164void Drawer::polygon(std::initializer_list<P> points) {165 polygon(std::vector<P>(points.begin(), points.end()));166}167168#ifndef JNGEN_DECLARE_ONLY169170Drawer::Drawer() : bbox_(emptyBbox()) {171 setFill("");172 setStroke("black");173}174175void Drawer::setWidth(double width) {176 requests_.push_back([width](DrawingEngine* engine) {177 engine->setWidth(width);178 });179}180181void Drawer::setColor(const std::string& color) {182 setStroke(color);183 setFill(color);184}185186void Drawer::setStroke(const std::string& color) {187 requests_.push_back([color](DrawingEngine* engine) {188 engine->setStroke(color);189 });190}191192void Drawer::setFill(const std::string& color) {193 requests_.push_back([color](DrawingEngine* engine) {194 engine->setFill(color);195 });196}197198void Drawer::setOpacity(double opacity) {199 requests_.push_back([opacity](DrawingEngine* engine) {200 engine->setOpacity(opacity);201 });202}203204Drawer::Bbox Drawer::emptyBbox() {205 const static double inf = 1e18;206 return { Point{inf, inf}, Point{-inf, -inf} };207}208209Drawer::Bbox Drawer::unite(const Bbox& lhs, const Bbox& rhs) {210 return Bbox{211 Point{212 std::min(lhs.first.x, rhs.first.x),213 std::min(lhs.first.y, rhs.first.y)},214 Point{215 std::max(lhs.second.x, rhs.second.x),216 std::max(lhs.second.y, rhs.second.y)}217 };218}219220Drawer::Bbox Drawer::bbox(const Point& p) {221 return {p, p};222}223224Drawer::Bbox Drawer::bbox(const std::pair<Point, double>& circle) {225 Point p;226 double radius;227 std::tie(p, radius) = circle;228 return {229 Point{p.x - radius, p.y - radius},230 Point{p.x + radius, p.y + radius}231 };232}233234/*235Given a bbox of points, returns a bbox with following properties:236 - at least 5% margin at each side is blank237 - side lengths differ by at most 1.6238 - side length is at least 10239 - if it is possible to include (0, 0), include it explicitly240 */241Drawer::Bbox Drawer::viewportByBbox(const Bbox& bbox) {242 constexpr static double MIN_SIZE = 10.0;243 constexpr static double MAX_RATIO = 1.6;244 constexpr static double MARGIN_RATIO = 0.05;245 constexpr static double MAX_RELATIVE_DISTANCE_TO_ZERO = 0.2;246247 double lx = bbox.first.x;248 double rx = bbox.second.x;249 double ly = bbox.first.y;250 double ry = bbox.second.y;251252 auto extendToSize = [&](double& l, double &r, double size) {253 double shift = (size - (r - l)) / 2;254 l -= shift;255 r += shift;256 };257258 auto extendInterval = [&](double& l, double &r) {259 if (r - l < MIN_SIZE) {260 if (l >= -1e-9 && r < MIN_SIZE) {261 l = 0;262 r = MIN_SIZE;263264 } else if (r <= 1e-9 && l >= -MIN_SIZE) {265 l = -MIN_SIZE;266 r = 0;267 } else {268 extendToSize(l, r, MIN_SIZE);269 }270 }271272 if ((l > 0 || r < 0) && std::min(std::abs(l), std::abs(r)) <=273 (r - l) * MAX_RELATIVE_DISTANCE_TO_ZERO)274 {275 if (l > 0) {276 l = 0;277 } else {278 r = 0;279 }280 }281282 double margin = (r - l) * MARGIN_RATIO;283 l -= margin;284 r += margin;285 };286287 extendInterval(lx, rx);288 extendInterval(ly, ry);289290 if ((rx - lx) / (ry - ly) > MAX_RATIO) {291 extendToSize(ly, ry, (rx - lx) / MAX_RATIO);292 } else if ((ry - ly) / (rx - lx) > MAX_RATIO) {293 extendToSize(lx, rx, (ry - ly) / MAX_RATIO);294 }295296 return { Point(lx, ly), Point(rx, ry) };297}298299void Drawer::drawAll() {300 for (const auto& request: requests_) {301 request(engine_);302 }303}304305void Drawer::drawGrid(const Bbox& bbox) {306 const static std::vector<int> STEP_DELTA = {20, 25, 20};307 // Step goes like 1, 2, 5, 10, 20, 50, 100, ...308 constexpr static int SMALL_IN_BIG = 5;309 constexpr static int THRESHOLD = 8;310 constexpr static int MAX_SPREAD_TO_DRAW_ALL_TICKS = 13;311 constexpr static double TEXT_OFFSET_RATIO = 0.01;312313 auto savedState = engine_->saveState();314315 int step = 5;316 double spread = std::min(317 bbox.second.x - bbox.first.x,318 bbox.second.y - bbox.first.y);319 size_t deltaPos = 2;320 while (spread / step > THRESHOLD) {321 step = step * STEP_DELTA[deltaPos] / 10;322 if (++deltaPos == STEP_DELTA.size()) {323 deltaPos = 0;324 }325 }326327 engine_->setWidth(0.5);328 engine_->setStroke("lightgrey");329330 double smallStep = 1.0 * step / SMALL_IN_BIG;331332 for (333 double tick = std::ceil(bbox.first.x / smallStep) * smallStep;334 tick < bbox.second.x;335 tick += smallStep)336 {337 if (std::lround(tick) % step != 0) {338 engine_->drawSegment(tick, bbox.first.y, tick, bbox.second.y);339 }340 }341342 for (343 double tick = std::ceil(bbox.first.y / smallStep) * smallStep;344 tick < bbox.second.y;345 tick += smallStep)346 {347 if (std::lround(tick) % step != 0) {348 engine_->drawSegment(bbox.first.x, tick, bbox.second.x, tick);349 }350 }351352 engine_->setWidth(0.75);353 engine_->setStroke("grey");354355 for (356 double tick = std::ceil(bbox.first.x / step) * step;357 tick < bbox.second.x;358 tick += step)359 {360 engine_->drawSegment(tick, bbox.first.y, tick, bbox.second.y);361 }362363 for (364 double tick = std::ceil(bbox.first.y / step) * step;365 tick < bbox.second.y;366 tick += step)367 {368 engine_->drawSegment(bbox.first.x, tick, bbox.second.x, tick);369 }370371 const double textOffsetX =372 (bbox.second.x - bbox.first.x) * TEXT_OFFSET_RATIO;373 const double textOffsetY =374 (bbox.second.y - bbox.first.y) * TEXT_OFFSET_RATIO;375376 auto format = [](double x) {377 static char buf[10];378 std::sprintf(buf, "%d", int(std::lround(x)));379 return std::string(buf);380 };381382 if (spread < MAX_SPREAD_TO_DRAW_ALL_TICKS) {383 step = 1;384 }385386 for (387 double tick = std::ceil(bbox.first.y / step) * step;388 tick < bbox.second.y;389 tick += step)390 {391 engine_->drawText(392 bbox.first.x + textOffsetX, tick + textOffsetX, format(tick));393 }394395 for (396 double tick = std::ceil(bbox.first.x / step) * step;397 tick < bbox.second.x;398 tick += step)399 {400 engine_->drawText(401 tick + textOffsetY, bbox.first.y + textOffsetY, format(tick));402 }403404405 if (spread <= MAX_SPREAD_TO_DRAW_ALL_TICKS) {406 step = 1;407 }408409 engine_->restoreState(savedState);410}411412void Drawer::dumpSvg(const std::string& filename) {413 if (requests_.empty()) {414 return;415 }416417 auto bbox = bbox_;418 auto viewport = viewportByBbox(bbox);419 std::unique_ptr<SvgEngine> svgEngine(new SvgEngine(420 viewport.first.x, viewport.first.y,421 viewport.second.x, viewport.second.y));422423 engine_ = svgEngine.get();424 if (gridEnabled_) {425 drawGrid(viewport);426 }427 drawAll();428429 std::string svg = svgEngine->serialize();430431 std::ofstream out(filename);432 out << svg;433 out.close();434}435436#endif // JNGEN_DECLARE_ONLY437438}} // namespace jngen::drawing439440using jngen::drawing::Drawer;441using jngen::drawing::Color;