summaryrefslogtreecommitdiffstats
path: root/src/display/control/canvas-item-grid.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-13 11:50:49 +0000
commitc853ffb5b2f75f5a889ed2e3ef89b818a736e87a (patch)
tree7d13a0883bb7936b84d6ecdd7bc332b41ed04bee /src/display/control/canvas-item-grid.cpp
parentInitial commit. (diff)
downloadinkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.tar.xz
inkscape-c853ffb5b2f75f5a889ed2e3ef89b818a736e87a.zip
Adding upstream version 1.3+ds.upstream/1.3+dsupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/display/control/canvas-item-grid.cpp')
-rw-r--r--src/display/control/canvas-item-grid.cpp522
1 files changed, 522 insertions, 0 deletions
diff --git a/src/display/control/canvas-item-grid.cpp b/src/display/control/canvas-item-grid.cpp
new file mode 100644
index 0000000..2c14ae2
--- /dev/null
+++ b/src/display/control/canvas-item-grid.cpp
@@ -0,0 +1,522 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Author:
+ * Tavmjong Bah
+ *
+ * Copyright (C) 2020 Tavmjong Bah
+ *
+ * Rewrite of GridCanvasItem.
+ *
+ * Released under GNU GPL v2+, read the file 'COPYING' for more information.
+ */
+
+#include <2geom/line.h>
+
+#include "canvas-item-grid.h"
+#include "color.h"
+#include "helper/geom.h"
+
+enum Dim3 { X, Y, Z };
+
+static int calculate_scaling_factor(double length, int major)
+{
+ int multiply = 1;
+ int step = std::max(major, 1);
+ int watchdog = 0;
+
+ while (length * multiply < 8.0 && watchdog < 100) {
+ multiply *= step;
+ // First pass, go up to the major line spacing, then keep increasing by two.
+ step = 2;
+ watchdog++;
+ }
+
+ return multiply;
+}
+
+namespace Inkscape {
+
+/**
+ * Create a null control grid.
+ */
+CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group)
+ : CanvasItem(group)
+ , _origin(0, 0)
+ , _spacing(1, 1)
+ , _minor_color(GRID_DEFAULT_MINOR_COLOR)
+ , _major_color(GRID_DEFAULT_MAJOR_COLOR)
+ , _major_line_interval(5)
+ , _dotted(false)
+{
+ _no_emp_when_zoomed_out = Preferences::get()->getBool("/options/grids/no_emphasize_when_zoomedout");
+ _pref_tracker = Preferences::PreferencesObserver::create("/options/grids/no_emphasize_when_zoomedout", [this] (auto &entry) {
+ set_no_emp_when_zoomed_out(entry.getBool());
+ });
+
+ request_update();
+}
+
+/**
+ * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of grid.
+ */
+bool CanvasItemGrid::contains(Geom::Point const &p, double tolerance)
+{
+ return false; // We're not pickable!
+}
+
+// Find the signed distance of a point to a line. The distance is negative if
+// the point lies to the left of the line considering the line's versor.
+static double signed_distance(Geom::Point const &point, Geom::Line const &line)
+{
+ return Geom::cross(point - line.initialPoint(), line.versor());
+}
+
+// Find intersections of line with rectangle. There should be zero or two.
+// If line is degenerate with rectangle side, two corner points are returned.
+static std::vector<Geom::Point> intersect_line_rectangle(Geom::Line const &line, Geom::Rect const &rect)
+{
+ std::vector<Geom::Point> intersections;
+ for (int i = 0; i < 4; ++i) {
+ Geom::LineSegment side(rect.corner(i), rect.corner((i + 1) % 4));
+ try {
+ if (auto oc = Geom::intersection(line, side)) {
+ intersections.emplace_back(line.pointAt(oc->ta));
+ }
+ } catch (Geom::InfiniteSolutions const &) {
+ return { side.pointAt(0), side.pointAt(1) };
+ }
+ }
+ return intersections;
+}
+
+void CanvasItemGrid::set_origin(Geom::Point const &point)
+{
+ defer([=] {
+ if (_origin == point) return;
+ _origin = point;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_major_color(uint32_t color)
+{
+ defer([=] {
+ if (_major_color == color) return;
+ _major_color = color;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_minor_color(uint32_t color)
+{
+ defer([=] {
+ if (_minor_color == color) return;
+ _minor_color = color;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_dotted(bool dotted)
+{
+ defer([=] {
+ if (_dotted == dotted) return;
+ _dotted = dotted;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_spacing(Geom::Point const &point)
+{
+ defer([=] {
+ if (_spacing == point) return;
+ _spacing = point;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_major_line_interval(int n)
+{
+ if (n < 1) return;
+ defer([=] {
+ if (_major_line_interval == n) return;
+ _major_line_interval = n;
+ request_update();
+ });
+}
+
+void CanvasItemGrid::set_no_emp_when_zoomed_out(bool noemp)
+{
+ if (_no_emp_when_zoomed_out != noemp) {
+ _no_emp_when_zoomed_out = noemp;
+ request_redraw();
+ }
+}
+
+/** ====== Rectangular Grid ====== **/
+
+CanvasItemGridXY::CanvasItemGridXY(Inkscape::CanvasItemGroup *group)
+ : CanvasItemGrid(group)
+{
+ _name = "CanvasItemGridXY";
+}
+
+void CanvasItemGridXY::_update(bool)
+{
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ // Queue redraw of grid area
+ ow = _origin * affine();
+ sw[0] = Geom::Point(_spacing[0], 0) * affine().withoutTranslation();
+ sw[1] = Geom::Point(0, _spacing[1]) * affine().withoutTranslation();
+
+ // Find suitable grid spacing for display
+ for (int dim : {0, 1}) {
+ int const scaling_factor = calculate_scaling_factor(sw[dim].length(), _major_line_interval);
+ sw[dim] *= scaling_factor;
+ scaled[dim] = scaling_factor > 1;
+ }
+
+ request_redraw();
+}
+
+void CanvasItemGridXY::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown.
+ uint32_t empcolor = ((scaled[Geom::X] || scaled[Geom::Y]) && _no_emp_when_zoomed_out) ? _minor_color : _major_color;
+ uint32_t color = _minor_color;
+
+ buf.cr->save();
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+ buf.cr->set_line_width(1.0);
+ buf.cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // Add a 2px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below)
+ auto const buf_rect_with_margin = expandedBy(buf.rect, 2);
+
+ for (int dim : {0, 1}) {
+ int const nrm = dim ^ 0x1;
+
+ // Construct an axis line through origin with direction normal to grid spacing.
+ Geom::Line axis = Geom::Line::from_origin_and_vector(ow, sw[dim]);
+ Geom::Line orth = Geom::Line::from_origin_and_vector(ow, sw[nrm]);
+
+ double spacing = sw[nrm].length(); // Spacing between grid lines.
+ double dash = sw[dim].length(); // Total length of dash pattern.
+
+ // Find the minimum and maximum distances of the buffer corners from axis.
+ double min = Geom::infinity();
+ double max = -Geom::infinity();
+ for (int c = 0; c < 4; ++c) {
+
+ // We need signed distance... lib2geom offers only positive distance.
+ double distance = signed_distance(buf_rect_with_margin.corner(c), axis);
+
+ // Correct it for coordinate flips (inverts handedness).
+ if (Geom::cross(axis.vector(), orth.vector()) > 0) {
+ distance = -distance;
+ }
+
+ min = std::min(min, distance);
+ max = std::max(max, distance);
+ }
+ int start = std::floor(min / spacing);
+ int stop = std::floor(max / spacing);
+
+ // Loop over grid lines that intersected buf rectangle.
+ for (int j = start + 1; j <= stop; ++j) {
+
+ Geom::Line grid_line = Geom::make_parallel_line(ow + j * sw[nrm], axis);
+
+ std::vector<Geom::Point> x = intersect_line_rectangle(grid_line, buf_rect_with_margin);
+
+ // If we have two intersections, grid line intersects buffer rectangle.
+ if (x.size() == 2) {
+ // Make sure lines are always drawn in the same direction (or dashes misplaced).
+ Geom::Line vector(x[0], x[1]);
+ if (Geom::dot(vector.vector(), axis.vector()) < 0.0) {
+ std::swap(x[0], x[1]);
+ }
+
+ // Set up line. Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, without any aliasing.
+ // For this we need to position the lines at exactly half pixels, see https://www.cairographics.org/FAQ/#sharp_lines
+ // Must be consistent with the pixel alignment of the guide lines, see CanvasItemGridXY::render(), and the drawing of the rulers
+ buf.cr->move_to(floor(x[0].x()) + 0.5, floor(x[0].y()) + 0.5);
+ buf.cr->line_to(floor(x[1].x()) + 0.5, floor(x[1].y()) + 0.5);
+
+ // Determine whether to draw with the emphasis color.
+ bool const noemp = !scaled[dim] && j % _major_line_interval != 0;
+
+ // Set dash pattern and color.
+ if (_dotted) {
+ // alpha needs to be larger than in the line case to maintain a similar
+ // visual impact but setting it to the maximal value makes the dots
+ // dominant in some cases. Solution, increase the alpha by a factor of
+ // 4. This then allows some user adjustment.
+ uint32_t _empdot = (empcolor & 0xff) << 2;
+ if (_empdot > 0xff)
+ _empdot = 0xff;
+ _empdot += (empcolor & 0xffffff00);
+
+ uint32_t _colordot = (color & 0xff) << 2;
+ if (_colordot > 0xff)
+ _colordot = 0xff;
+ _colordot += (color & 0xffffff00);
+
+ // Dash pattern must use spacing from orthogonal direction.
+ // Offset is to center dash on orthogonal lines.
+ double offset = std::fmod(signed_distance(x[0], orth), sw[dim].length());
+ if (Geom::cross(axis.vector(), orth.vector()) > 0) {
+ offset = -offset;
+ }
+
+ std::vector<double> dashes;
+ if (noemp) {
+ // Minor lines
+ dashes.push_back(1.0);
+ dashes.push_back(dash - 1.0);
+ offset -= 0.5;
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_colordot), SP_RGBA32_G_F(_colordot),
+ SP_RGBA32_B_F(_colordot), SP_RGBA32_A_F(_colordot));
+ } else {
+ // Major lines
+ dashes.push_back(3.0);
+ dashes.push_back(dash - 3.0);
+ offset -= 1.5; // Center dash on intersection.
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(_empdot), SP_RGBA32_G_F(_empdot),
+ SP_RGBA32_B_F(_empdot), SP_RGBA32_A_F(_empdot));
+ }
+
+ buf.cr->set_line_cap(Cairo::LINE_CAP_BUTT);
+ buf.cr->set_dash(dashes, -offset);
+
+ } else {
+ // Solid lines
+ uint32_t col = noemp ? color : empcolor;
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(col), SP_RGBA32_G_F(col),
+ SP_RGBA32_B_F(col), SP_RGBA32_A_F(col));
+ }
+
+ buf.cr->stroke();
+
+ } else {
+ std::cerr << "CanvasItemGridXY::render: Grid line doesn't intersect!" << std::endl;
+ }
+ }
+ }
+
+ buf.cr->restore();
+}
+
+/** ========= Axonometric Grids ======== */
+
+/*
+ * Current limits are: one axis (y-axis) is always vertical. The other two
+ * axes are bound to a certain range of angles. The z-axis always has an angle
+ * smaller than 90 degrees (measured from horizontal, 0 degrees being a line extending
+ * to the right). The x-axis will always have an angle between 0 and 90 degrees.
+ */
+CanvasItemGridAxonom::CanvasItemGridAxonom(Inkscape::CanvasItemGroup *group)
+ : CanvasItemGrid(group)
+{
+ _name = "CanvasItemGridAxonom";
+
+ angle_deg[X] = 30.0;
+ angle_deg[Y] = 30.0;
+ angle_deg[Z] = 0.0;
+
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ angle_rad[Y] = Geom::rad_from_deg(angle_deg[Y]);
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+
+ tan_angle[X] = std::tan(angle_rad[X]);
+ tan_angle[Y] = std::tan(angle_rad[Y]);
+ tan_angle[Z] = std::tan(angle_rad[Z]);
+}
+
+void CanvasItemGridAxonom::_update(bool)
+{
+ _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity());
+
+ ow = _origin * affine();
+ lyw = _spacing.y() * affine().descrim();
+
+ int const scaling_factor = calculate_scaling_factor(lyw, _major_line_interval);
+ lyw *= scaling_factor;
+ scaled = scaling_factor > 1;
+
+ spacing_ylines = lyw / (tan_angle[X] + tan_angle[Z]);
+ lxw_x = Geom::are_near(tan_angle[X], 0) ? Geom::infinity() : lyw / tan_angle[X];
+ lxw_z = Geom::are_near(tan_angle[Z], 0) ? Geom::infinity() : lyw / tan_angle[Z];
+
+ if (_major_line_interval == 0) {
+ scaled = true;
+ }
+
+ request_redraw();
+}
+
+// expects value given to be in degrees
+void CanvasItemGridAxonom::set_angle_x(double deg)
+{
+ defer([=] {
+ angle_deg[X] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns
+ angle_rad[X] = Geom::rad_from_deg(angle_deg[X]);
+ tan_angle[X] = std::tan(angle_rad[X]);
+ request_update();
+ });
+}
+
+// expects value given to be in degrees
+void CanvasItemGridAxonom::set_angle_z(double deg)
+{
+ defer([=] {
+ angle_deg[Z] = std::clamp(deg, 0.0, 89.0); // setting to 90 and values close cause extreme slowdowns
+ angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]);
+ tan_angle[Z] = std::tan(angle_rad[Z]);
+ request_update();
+ });
+}
+
+static void drawline(Inkscape::CanvasItemBuffer &buf, int x0, int y0, int x1, int y1, uint32_t rgba)
+{
+ buf.cr->move_to(0.5 + x0, 0.5 + y0);
+ buf.cr->line_to(0.5 + x1, 0.5 + y1);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf.cr->stroke();
+}
+
+static void vline(Inkscape::CanvasItemBuffer &buf, int x, int ys, int ye, uint32_t rgba)
+{
+ if (x < buf.rect.left() || x >= buf.rect.right())
+ return;
+
+ buf.cr->move_to(0.5 + x, 0.5 + ys);
+ buf.cr->line_to(0.5 + x, 0.5 + ye);
+ buf.cr->set_source_rgba(SP_RGBA32_R_F(rgba), SP_RGBA32_G_F(rgba),
+ SP_RGBA32_B_F(rgba), SP_RGBA32_A_F(rgba));
+ buf.cr->stroke();
+}
+
+/**
+ * This function calls Cairo to render a line on a particular canvas buffer.
+ * Coordinates are interpreted as SCREENcoordinates
+ */
+void CanvasItemGridAxonom::_render(Inkscape::CanvasItemBuffer &buf) const
+{
+ // Set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring)
+ uint32_t empcolor = (scaled && _no_emp_when_zoomed_out) ? _minor_color : _major_color;
+ uint32_t color = _minor_color;
+
+ buf.cr->save();
+ buf.cr->translate(-buf.rect.left(), -buf.rect.top());
+ buf.cr->set_line_width(1.0);
+ buf.cr->set_line_cap(Cairo::LINE_CAP_SQUARE);
+
+ // gc = gridcoordinates (the coordinates calculated from the grids origin 'grid->ow'.
+ // sc = screencoordinates ( for example "buf.rect.left()" is in screencoordinates )
+ // bc = buffer patch coordinates (x=0 on left side of page, y=0 on bottom of page)
+
+ // tl = topleft
+ auto const buf_tl_gc = buf.rect.min() - ow;
+
+ // render the three separate line groups representing the main-axes
+
+ // x-axis always goes from topleft to bottomright. (0,0) - (1,1)
+ double const xintercept_y_bc = (buf_tl_gc.x() * tan_angle[X]) - buf_tl_gc.y();
+ double const xstart_y_sc = (xintercept_y_bc - std::floor(xintercept_y_bc / lyw) * lyw) + buf.rect.top();
+ int const xlinestart = std::round((xstart_y_sc - buf_tl_gc.x() * tan_angle[X] - ow.y()) / lyw);
+ int xlinenum = xlinestart;
+
+ // lines starting on left side.
+ for (double y = xstart_y_sc; y < buf.rect.bottom(); y += lyw, xlinenum++) {
+ int const x0 = buf.rect.left();
+ int const y0 = round(y);
+ int x1 = x0 + round((buf.rect.bottom() - y) / tan_angle[X]);
+ int y1 = buf.rect.bottom();
+ if (Geom::are_near(tan_angle[X], 0)) {
+ x1 = buf.rect.right();
+ y1 = y0;
+ }
+
+ bool const noemp = !scaled && xlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+
+ // lines starting from top side
+ if (!Geom::are_near(tan_angle[X], 0)) {
+ double const xstart_x_sc = buf.rect.left() + (lxw_x - (xstart_y_sc - buf.rect.top()) / tan_angle[X]);
+ xlinenum = xlinestart-1;
+ for (double x = xstart_x_sc; x < buf.rect.right(); x += lxw_x, xlinenum--) {
+ int const y0 = buf.rect.top();
+ int const y1 = buf.rect.bottom();
+ int const x0 = round(x);
+ int const x1 = x0 + round((y1 - y0) / tan_angle[X]);
+
+ bool const noemp = !scaled && xlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+ }
+
+ // y-axis lines (vertical)
+ double const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X];
+ int const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines);
+ int ylinenum = ylinestart;
+ for (double x = ystart_x_sc; x < buf.rect.right(); x += spacing_ylines, ylinenum++) {
+ int const x0 = floor(x); // sp_grid_vline will add 0.5 again, so we'll pre-emptively use floor()
+ // instead of round() to avoid biasing the vertical lines to the right by half a pixel; see
+ // CanvasItemGridXY::render() for more details
+ bool const noemp = !scaled && ylinenum % _major_line_interval != 0;
+ vline(buf, x0, buf.rect.top(), buf.rect.bottom() - 1, noemp ? color : empcolor);
+ }
+
+ // z-axis always goes from bottomleft to topright. (0,1) - (1,0)
+ double const zintercept_y_bc = (buf_tl_gc.x() * -tan_angle[Z]) - buf_tl_gc.y();
+ double const zstart_y_sc = (zintercept_y_bc - std::floor(zintercept_y_bc / lyw) * lyw) + buf.rect.top();
+ int const zlinestart = std::round((zstart_y_sc + buf_tl_gc.x() * tan_angle[Z] - ow.y()) / lyw);
+ int zlinenum = zlinestart;
+ // lines starting from left side
+ double next_y = zstart_y_sc;
+ for (double y = zstart_y_sc; y < buf.rect.bottom(); y += lyw, zlinenum++, next_y = y) {
+ int const x0 = buf.rect.left();
+ int const y0 = round(y);
+ int x1 = x0 + round((y - buf.rect.top()) / tan_angle[Z]);
+ int y1 = buf.rect.top();
+ if (Geom::are_near(tan_angle[Z], 0)) {
+ x1 = buf.rect.right();
+ y1 = y0;
+ }
+
+ bool const noemp = !scaled && zlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+
+ // draw lines from bottom-up
+ if (!Geom::are_near(tan_angle[Z], 0)) {
+ double const zstart_x_sc = buf.rect.left() + (next_y - buf.rect.bottom()) / tan_angle[Z];
+ for (double x = zstart_x_sc; x < buf.rect.right(); x += lxw_z, zlinenum++) {
+ int const y0 = buf.rect.bottom();
+ int const y1 = buf.rect.top();
+ int const x0 = round(x);
+ int const x1 = x0 + round(buf.rect.height() / tan_angle[Z]);
+
+ bool const noemp = !scaled && zlinenum % _major_line_interval != 0;
+ drawline(buf, x0, y0, x1, y1, noemp ? color : empcolor);
+ }
+ }
+
+ buf.cr->restore();
+}
+
+} // namespace Inkscape
+
+/*
+ Local Variables:
+ mode:c++
+ c-file-style:"stroustrup"
+ c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
+ indent-tabs-mode:nil
+ fill-column:99
+ End:
+*/
+// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :