diff options
Diffstat (limited to 'src/display/control/canvas-item-guideline.cpp')
-rw-r--r-- | src/display/control/canvas-item-guideline.cpp | 343 |
1 files changed, 343 insertions, 0 deletions
diff --git a/src/display/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp new file mode 100644 index 0000000..6855701 --- /dev/null +++ b/src/display/control/canvas-item-guideline.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control guide line. + */ + +/* + * Authors: + * Tavmjong Bah - Rewrite of SPGuideLine + * Rafael Siejakowski - Tweaks to handle appearance + * + * Copyright (C) 2020-2022 the Authors. + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-guideline.h" + +#include <2geom/line.h> + +#include "canvas-item-ctrl.h" + +#include "desktop.h" // Canvas orientation so label is orientated correctly. + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create a control guide line. Points are in document units. + */ +CanvasItemGuideLine::CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, + Geom::Point const &origin, Geom::Point const &normal) + : CanvasItem(group) + , _origin(origin) + , _normal(normal) + , _label(std::move(label)) +{ + _name = "CanvasItemGuideLine:" + _label; + _pickable = true; // For now, everybody gets events from this class! + + // Required when rotating canvas: + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + // Control to move guide line. + _origin_ctrl = std::make_unique<CanvasItemGuideHandle>(group, _origin, this); + _origin_ctrl->set_name("CanvasItemGuideLine:Ctrl:" + _label); + _origin_ctrl->set_size_default(); + _origin_ctrl->set_pickable(true); // The handle will also react to dragging + set_locked(false); // Init _origin_ctrl shape and stroke. +} + +CanvasItemGuideLine::~CanvasItemGuideLine() = default; + +/** + * Sets origin of guide line (place where handle is located). + */ +void CanvasItemGuideLine::set_origin(Geom::Point const &origin) +{ + if (_origin != origin) { + _origin = origin; + _origin_ctrl->set_position(_origin); + request_update(); + } +} + +/** + * Sets orientation of guide line. + */ +void CanvasItemGuideLine::set_normal(Geom::Point const &normal) +{ + if (_normal != normal) { + _normal = normal; + request_update(); + } +} + +/** + * Sets the inverted nature of the line + */ +void CanvasItemGuideLine::set_inverted(bool inverted) +{ + if (_inverted != inverted) { + _inverted = inverted; + request_update(); + } +} + + +/** + * Returns distance between point in canvas units and nearest point on guideLine. + */ +double CanvasItemGuideLine::closest_distance_to(Geom::Point const &p) +{ + // Maybe store guide as a Geom::Line? + Geom::Line guide = + Geom::Line::from_origin_and_vector(_origin, Geom::rot90(_normal)); + guide *= _affine; + return Geom::distance(p, guide); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of guideLine (or 1 if tolerance is zero). + */ +bool CanvasItemGuideLine::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance == 0) { + tolerance = 1; // Can't pick of zero! + } + + return closest_distance_to(p) < tolerance; +} + +/** + * Returns the pointer to the origin control (the "dot") + */ +CanvasItemGuideHandle* CanvasItemGuideLine::dot() const +{ + return _origin_ctrl.get(); +} + +/** + * Update and redraw control guideLine. + */ +void CanvasItemGuideLine::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + _affine = affine; + + // Queue redraw of new area (and old too). + request_redraw(); + + _need_update = false; +} + +/** + * Render guideLine to screen via Cairo. + */ +void CanvasItemGuideLine::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemGuideLine::Render: No buffer!" << std::endl; + return; + } + + if (!_visible) { + // Hidden + return; + } + + // Document to canvas + Geom::Point const normal = _normal * _affine.withoutTranslation(); // Direction only + Geom::Point const origin = _origin * _affine; + + /* 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 grid lines, see CanvasXYGrid::Render(), + * and the drawing of the rulers. + * Lastly, the origin control is also pixel-aligned and we want to visually cut through its + * exact center. + */ + Geom::Point const aligned_origin = origin.floor() + Geom::Point(0.5, 0.5); + + // Set up the Cairo rendering context + Cairo::RefPtr<Cairo::Context> ctx = buf->cr; + ctx->save(); + ctx->translate(-buf->rect.left(), -buf->rect.top()); // Canvas to screen + ctx->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + ctx->set_line_width(1); + + if (_inverted) { + // operator not available in cairo C++ bindings + cairo_set_operator(ctx->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + if (!_label.empty()) { // Render text label + ctx->save(); + ctx->translate(aligned_origin.x(), aligned_origin.y()); + + SPDesktop *desktop = nullptr; + if (_canvas) { + desktop = _canvas->get_desktop(); + } + ctx->rotate(atan2(normal.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0)); + ctx->translate(0, -(_origin_ctrl->radius() + LABEL_SEP)); // Offset by dot radius + 2 + ctx->move_to(0, 0); + ctx->show_text(_label); + ctx->restore(); + } + + // Draw guide. + // Special case: horizontal and vertical lines (easier calculations) + + // Don't use isHorizontal()/isVertical() as they test only exact matches. + if (Geom::are_near(normal.y(), 0.0)) { + // Vertical + double const position = aligned_origin.x(); + ctx->move_to(position, buf->rect.top() + 0.5); + ctx->line_to(position, buf->rect.bottom() - 0.5); + } else if (Geom::are_near(normal.x(), 0.0)) { + // Horizontal + double position = aligned_origin.y(); + ctx->move_to(buf->rect.left() + 0.5, position); + ctx->line_to(buf->rect.right() - 0.5, position); + } else { + // Angled + Geom::Line line = Geom::Line::from_origin_and_vector(aligned_origin, Geom::rot90(normal)); + + // Find intersections of the line with buf rectangle. There should be zero or two. + std::vector<Geom::Point> intersections; + for (unsigned i = 0; i < 4; ++i) { + Geom::LineSegment side(buf->rect.corner(i), buf->rect.corner((i+1)%4)); + try { + Geom::OptCrossing oc = Geom::intersection(line, side); + if (oc) { + intersections.push_back(line.pointAt((*oc).ta)); + } + } catch (Geom::InfiniteSolutions) { + // Shouldn't happen as we have already taken care of horizontal/vertical guides. + std::cerr << "CanvasItemGuideLine::render: Error: Infinite intersections." << std::endl; + } + } + + if (intersections.size() == 2) { + double const x0 = intersections[0].x(); + double const x1 = intersections[1].x(); + double const y0 = intersections[0].y(); + double const y1 = intersections[1].y(); + ctx->move_to(x0, y0); + ctx->line_to(x1, y1); + } + } + ctx->stroke(); + + ctx->restore(); +} + +void CanvasItemGuideLine::hide() +{ + CanvasItem::hide(); + _origin_ctrl->hide(); +} + +void CanvasItemGuideLine::show() +{ + CanvasItem::show(); + _origin_ctrl->show(); +} + +void CanvasItemGuideLine::set_stroke(guint32 color) +{ + // Make sure the fill of the control is the same as the stroke + // of the guide-line: + _origin_ctrl->set_fill(color); + CanvasItem::set_stroke(color); +} + +void CanvasItemGuideLine::set_label(Glib::ustring const & label) +{ + if (_label != label) { + _label = label; + request_update(); + } +} + +void CanvasItemGuideLine::set_locked(bool locked) +{ + if (_locked != locked) { + _locked = locked; + if (_locked) { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS); + _origin_ctrl->set_stroke(CONTROL_LOCKED_COLOR); + _origin_ctrl->set_fill(0x00000000); // no fill + } else { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + _origin_ctrl->set_stroke(0x00000000); // no stroke + _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color + } + } +} + +//=============================================================================================== + +/** + * @brief Create a handle ("dot") along a guide line + * @param group - the associated canvas item group + * @param pos - position + * @param line - pointer to the corresponding guide line + */ +CanvasItemGuideHandle::CanvasItemGuideHandle(CanvasItemGroup *group, + Geom::Point const &pos, + CanvasItemGuideLine* line) + : CanvasItemCtrl(group, CANVAS_ITEM_CTRL_SHAPE_CIRCLE, pos) + , _my_line(line) // Save a pointer to our guide line +{ +} + +/** + * Return the radius of the handle dot + */ +double CanvasItemGuideHandle::radius() const +{ + return 0.5 * static_cast<double>(_width); // radius is half the width +} + +/** + * Update the size of the handle based on the index from Preferences + */ +void CanvasItemGuideHandle::set_size_via_index(int index) +{ + double const r = static_cast<double>(index) * SCALE; + unsigned long const rounded_diameter = std::lround(r * 2.0); // diameter is twice the radius + unsigned long size = rounded_diameter | 0x1; // make sure the size is always odd + if (size < MINIMUM_SIZE) { + size = MINIMUM_SIZE; + } + if (_width != size) { + _width = size; + _height = size; + _built = false; + request_update(); + _my_line->request_update(); + } +} + +} // 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 : |