← comparison · drawer/drawer.h
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;