diff options
Diffstat (limited to '')
38 files changed, 6757 insertions, 0 deletions
diff --git a/src/display/control/README b/src/display/control/README new file mode 100644 index 0000000..680f0bf --- /dev/null +++ b/src/display/control/README @@ -0,0 +1,112 @@ + +This directory contains code for handling on-canvas editing objects as +well as document display. + +Historically, the code originates from GnomeCanvas but at this point it +has been completely rewritten for Inkscape's needs. + +One can think of a CanvasItem as a light-weight widget. There is a +selection mechanism based on an item's position and a method to handle +events. The selection mechanism is currently very simplistic, +selecting the top-most item whose bounding box contains the +cursor. Probably as a result, many items do not use these capabilities +with external code replacing this functionality (e.g CanvasItemQuad, +CanvasItemCurve). + +Points are stored as document coordinates. They are converted to +screen coordinates by multiplying by the document to window affine. +When an object's geometry changes, it must call request_update() to +flag that its bounds need to be recalculated (_need_update = +true). This will also cause all ancestors to also be marked and +finally the Canvas. Before picking or drawing, all bounds must be +up-to-date. (Changing the Canvas affine will also require bounds to be +recalculated.) This mechanism ensures that bounds are correct in the +most efficient manner. If only the style is changed (without geometric +ramifications) then call canvas->redraw_area() to trigger a redraw. + +CanvasItemGroup keeps a list of child items using a Boost intrusive +list. The pointers between nodes are kept inside the items. This +allows for quick deletion by avoiding the need to search through the +list to find the item to delete. This is important when a path +contains hundress of nodes. However, a Boost intrusive list cannot be +used with C++ smart pointers. Deleting an item can be done by either +calling CanvasItemGroup::remove(CanvasItem*) or by directly deleting +the item. Deleting a CanvasItemGroup will delete it and all of its +children. + + +Contents (x == pickable): + +* CanvasItem: Abstract base class. +* CanvasItemBPath: x An item representing a Bezier path. +* CanvasItemCatchall: x An infinite item, for capturing left-over events. +* CanvasItemCtrl: x An item representing a control point (knot, node, etc.). +* CanvasItemCurve: A single Bezier curve item (line or cubic). Not pickable! +* CanvasItemDrawing: x The SVG drawing. +* CanvasItemGrid: Base class for snapping grids. +* CanvasItemGroup: x An item that contains other items. +* CanvasItemGuideline: x A guideline for snapping. +* CanvasItemQuad: An object defined by four points (only used by Text tool). +* CanvasItemRect: An axis aligned rectangle item. +* CanvasItemRotate: x For previewing the rotation of the canvas. +* CanvasItemText: A text item. + +* CanvasItemEnum: All the enums you'll want to use! +* CanvasItemBuffer: A class that wraps a Cairo buffer for drawing. + +Classes here that use CanvasItem's: + +* CanvasGridXY: A Cartesian grid for snapping. +* CanvasGridAxonom An Axonometric grid for snapping. +* SnapIndicator: A class for showing a snap possibility on canvas. +* TemporaryItem: A class to manage the lifetime of a temporary CanvasItem. +* TemporaryItemList: A class to track TemporaryItem's. + + +Notes: + +CanvasItemCtrl (a.k.a. "Node", "Knot", "Handle", "Dragger", "Ctrl", "Mouse Grab", "Control Point") + + Used by several groups of classes: + + * Knot + ** KnotHolderEntity + *** live_effects/... + *** KnotHolder: Contains one or more KnotHolderEntity's. + Example: an object's fill and stroke gradients could have + overlapping knot-entities and are moved together via the knot holder. + **** shape-editor-knotholders.cpp + Classes derived from KnotHolder which contain classes derived from KnotHolderEntity for + editing shapes. + **** ShapeEditor contains two KnotHolders, one for shapes, one for LPE's. + + ** ui/tools/tool-base.h + ** seltrans.h,.cpp + ** display/snap-indicator.h + ** gradient-drag.cpp + ** vanishing-point.h + + + * ControlPoint, SelectorPoint, SelectableControlPoint, Handle, Node, CurveDragPoint, TransformHandle, plus + auxiliary classes (manipulator...). + * drag-anchor + * pen-tool + * measure-tool + * guide-line + * snap-indicator + + +TODO: + +Move files that use CanvasItem's to more appropriate places. + +All files that use CanvasItem's in src directory: +* Move rubberband.h/.cpp to src/ui +* Move gradient-drag to src/ui +* Move selcue* to src/ui +* Move seltrans* to src/ui +* Move vanishing-point to src/ui +* Move snap code to src/ui/snap display/snap-indicator.h/.cpp snap.h etc. + +See also src/ui/tool and src/ui/knot. + diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp new file mode 100644 index 0000000..d99588e --- /dev/null +++ b/src/display/control/canvas-item-bpath.cpp @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a Bezier path. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasBPath + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-bpath.h" + +#include "color.h" // SP_RGBA_x_F +#include "display/curve.h" +#include "display/cairo-utils.h" +#include "helper/geom.h" // bounds_exact_transformed() + +namespace Inkscape { + +/** + * Create a null control bpath. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemBpath:Null"; + _pickable = true; // For now, everyone gets events from this class! +} + +/** + * Create a control bpath. Path is in document coordinates. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line) + : CanvasItem(group) + , _path(std::move(path)) + , _phantom_line(phantom_line) +{ + _name = "CanvasItemBpath"; + _pickable = true; // For now, everyone gets events from this class! + request_update(); // Render immediately or temporary bpaths won't show. +} + +/** + * Set a control bpath. Curve is in document coordinates. + */ +void CanvasItemBpath::set_bpath(SPCurve const *curve, bool phantom_line) +{ + set_bpath(curve ? curve->get_pathvector() : Geom::PathVector(), phantom_line); +} + +/** + * Set a control bpath. Path is in document coordinates. + */ +void CanvasItemBpath::set_bpath(Geom::PathVector path, bool phantom_line) +{ + defer([=, path = std::move(path)] () mutable { + _path = std::move(path); + _phantom_line = phantom_line; + request_update(); + }); +} + +/** + * Set the fill color and fill rule. + */ +void CanvasItemBpath::set_fill(uint32_t fill, SPWindRule fill_rule) +{ + defer([=] { + if (_fill == fill && _fill_rule == fill_rule) return; + _fill = fill; + _fill_rule = fill_rule; + request_redraw(); + }); +} + +void CanvasItemBpath::set_dashes(std::vector<double> &&dashes) +{ + defer([=, dashes = std::move(dashes)] () mutable { + _dashes = std::move(dashes); + }); +} + +/** + * Set the stroke width + */ +void CanvasItemBpath::set_stroke_width(double width) +{ + defer([=] { + if (_stroke_width == width) return; + _stroke_width = width; + request_redraw(); + }); +} + +/** + * Returns distance between point in canvas units and nearest point on bpath. + */ +double CanvasItemBpath::closest_distance_to(Geom::Point const &p) const +{ + double d = Geom::infinity(); + + // Convert p to document coordinates (quicker than converting path to canvas units). + Geom::Point p_doc = p * affine().inverse(); + _path.nearestTime(p_doc, &d); + d *= affine().descrim(); // Uniform scaling and rotation only. + + return d; +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of bpath. + */ +bool CanvasItemBpath::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance == 0) { + tolerance = 1; // Need a minimum tolerance value or always returns false. + } + + // Check for 'inside' a filled bpath if a fill is being used. + if ((_fill & 0xff) != 0) { + Geom::Point p_doc = p * affine().inverse(); + if (_path.winding(p_doc) % 2 != 0) { + return true; + } + } + + // Otherwise see how close we are to the outside line. + return closest_distance_to(p) < tolerance; +} + +/** + * Update and redraw control bpath. + */ +void CanvasItemBpath::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + if (_path.empty()) { + _bounds = {}; + return; + } + + _bounds = expandedBy(bounds_exact_transformed(_path, affine()), 2); + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render bpath to screen via Cairo. + */ +void CanvasItemBpath::_render(Inkscape::CanvasItemBuffer &buf) const +{ + bool do_fill = (_fill & 0xff) != 0; // Not invisible. + bool do_stroke = (_stroke & 0xff) != 0; // Not invisible. + + if (!do_fill && !do_stroke) { + // Both fill and stroke invisible. + return; + } + + buf.cr->save(); + + // Setup path + buf.cr->set_tolerance(0.5); + buf.cr->begin_new_path(); + + feed_pathvector_to_cairo(buf.cr->cobj(), _path, affine(), buf.rect, + /* optimize_stroke */ !do_fill, 1); + + // Do fill + if (do_fill) { + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->set_fill_rule(_fill_rule == SP_WIND_RULE_EVENODD ? + Cairo::FILL_RULE_EVEN_ODD : Cairo::FILL_RULE_WINDING); + buf.cr->fill_preserve(); + } + + // Do stroke + if (do_stroke) { + + if (!_dashes.empty()) { + buf.cr->set_dash(_dashes, 0.0); // 0.0 is offset + } + + if (_phantom_line) { + buf.cr->set_source_rgba(1.0, 1.0, 1.0, 0.25); + buf.cr->set_line_width(2.0); + buf.cr->stroke_preserve(); + } + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->set_line_width(_stroke_width); + buf.cr->stroke(); + + } else { + buf.cr->begin_new_path(); // Clears path + } + + 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 : diff --git a/src/display/control/canvas-item-bpath.h b/src/display/control/canvas-item-bpath.h new file mode 100644 index 0000000..3b87579 --- /dev/null +++ b/src/display/control/canvas-item-bpath.h @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_BPATH_H +#define SEEN_CANVAS_ITEM_BPATH_H + +/** + * A class to represent a Bezier path. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasBPath + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> +#include <2geom/pathvector.h> + +#include "canvas-item.h" + +#include "style-enums.h" // Fill rule + +class SPCurve; + +namespace Inkscape { + +class CanvasItemBpath final : public CanvasItem +{ +public: + CanvasItemBpath(CanvasItemGroup *group); + CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line = false); + + // Geometry + void set_bpath(SPCurve const *curve, bool phantom_line = false); + void set_bpath(Geom::PathVector path, bool phantom_line = false); + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_fill(uint32_t rgba, SPWindRule fill_rule); + void set_dashes(std::vector<double> &&dashes); + void set_stroke_width(double width); + +protected: + ~CanvasItemBpath() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Geometry + Geom::PathVector _path; + + // Properties + SPWindRule _fill_rule = SP_WIND_RULE_EVENODD; + std::vector<double> _dashes; + bool _phantom_line = false; + double _stroke_width = 1.0; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_BPATH_H + +/* + 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 : diff --git a/src/display/control/canvas-item-buffer.h b/src/display/control/canvas-item-buffer.h new file mode 100644 index 0000000..a66cb4c --- /dev/null +++ b/src/display/control/canvas-item-buffer.h @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_BUFFER_H +#define SEEN_CANVAS_ITEM_BUFFER_H + +/** + * Buffer for rendering canvas items. + */ + +/* + * Author: + * See git history. + * + * Copyright (C) 2020 Authors + * + * Rewrite of SPCanvasBuf. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + */ + +#include <2geom/rect.h> +#include <cairomm/context.h> + +namespace Inkscape { + +/** + * Class used when rendering canvas items. + */ +struct CanvasItemBuffer +{ + Geom::IntRect rect; + int device_scale; // For high DPI monitors. + Cairo::RefPtr<Cairo::Context> cr; + bool outline_pass; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_BUFFER_H + +/* + 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 : diff --git a/src/display/control/canvas-item-catchall.cpp b/src/display/control/canvas-item-catchall.cpp new file mode 100644 index 0000000..e6fbd47 --- /dev/null +++ b/src/display/control/canvas-item-catchall.cpp @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to catch events after everyone else has had a go. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasAcetate. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-catchall.h" + +namespace Inkscape { + +/** + * Create an null control catchall. + */ +CanvasItemCatchall::CanvasItemCatchall(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCatchall"; + _pickable = true; // Duh! That's the purpose of this class! +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of catchall. + */ +bool CanvasItemCatchall::contains(Geom::Point const &p, double tolerance) +{ + return true; // We contain every place! +} + +/** + * Update and redraw control catchall. + */ +void CanvasItemCatchall::_update(bool) +{ + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); +} + +/** + * Render catchall to screen via Cairo. + */ +void CanvasItemCatchall::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Do nothing! (Needed as CanvasItem is abstract.) +} + +} // 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 : diff --git a/src/display/control/canvas-item-catchall.h b/src/display/control/canvas-item-catchall.h new file mode 100644 index 0000000..6e0270a --- /dev/null +++ b/src/display/control/canvas-item-catchall.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CATCHALL_H +#define SEEN_CANVAS_ITEM_CATCHALL_H + +/** + * A class to catch events after everyone else has had a go. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasAcetate. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemCatchall final : public CanvasItem +{ +public: + CanvasItemCatchall(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance) override; + +protected: + ~CanvasItemCatchall() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CATCHALL_H + +/* + 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 : diff --git a/src/display/control/canvas-item-context.cpp b/src/display/control/canvas-item-context.cpp new file mode 100644 index 0000000..99afdc4 --- /dev/null +++ b/src/display/control/canvas-item-context.cpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "canvas-item-context.h" +#include "canvas-item-group.h" + +namespace Inkscape { + +CanvasItemContext::CanvasItemContext(UI::Widget::Canvas *canvas) + : _canvas(canvas) + , _root(new CanvasItemGroup(this)) +{ +} + +CanvasItemContext::~CanvasItemContext() +{ + delete _root; +} + +void CanvasItemContext::snapshot() +{ + assert(!_snapshotted); + _snapshotted = true; +} + +void CanvasItemContext::unsnapshot() +{ + assert(_snapshotted); + _snapshotted = false; + _funclog(); +} + +} // 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 : diff --git a/src/display/control/canvas-item-context.h b/src/display/control/canvas-item-context.h new file mode 100644 index 0000000..117110a --- /dev/null +++ b/src/display/control/canvas-item-context.h @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * The context in which a single CanvasItem tree exists. Holds the root node and common state. + */ +#ifndef SEEN_CANVAS_ITEM_CONTEXT_H +#define SEEN_CANVAS_ITEM_CONTEXT_H + +#include <2geom/affine.h> +#include "util/funclog.h" + +namespace Inkscape { + +namespace UI::Widget { class Canvas; } +class CanvasItemGroup; + +class CanvasItemContext +{ +public: + CanvasItemContext(UI::Widget::Canvas *canvas); + CanvasItemContext(CanvasItemContext const &) = delete; + CanvasItemContext &operator=(CanvasItemContext const &) = delete; + ~CanvasItemContext(); + + // Structure + UI::Widget::Canvas *canvas() const { return _canvas; } + CanvasItemGroup *root() const { return _root; } + + // Geometry + Geom::Affine const &affine() const { return _affine; } + void setAffine(Geom::Affine const &affine) { _affine = affine; } + + // Snapshotting + void snapshot(); + void unsnapshot(); + bool snapshotted() const { return _snapshotted; } + + template<typename F> + void defer(F &&f) { _snapshotted ? _funclog.emplace(std::forward<F>(f)) : f(); } + +private: + // Structure + UI::Widget::Canvas *_canvas; + CanvasItemGroup *_root; + + // Geometry + Geom::Affine _affine; + + // Snapshotting + char _cacheline_separator[127]; + + bool _snapshotted = false; + Util::FuncLog _funclog; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CONTEXT_H + +/* + 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 : diff --git a/src/display/control/canvas-item-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp new file mode 100644 index 0000000..e0dcd90 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.cpp @@ -0,0 +1,1181 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control node. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrl + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/transforms.h> + +#include "canvas-item-ctrl.h" +#include "helper/geom.h" + +#include "preferences.h" // Default size. +#include "display/cairo-utils.h" // argb32_from_rgba() + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create a null control node. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCtrl:Null"; + _pickable = true; // Everybody gets events from this class! +} + +/** + * Create a control ctrl. Shape auto-set by type. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type) + : CanvasItem(group) + , _type(type) +{ + _name = "CanvasItemCtrl:Type_" + std::to_string(_type); + _pickable = true; // Everybody gets events from this class! + + // Use _type to set default values: + set_shape_default(); + set_size_default(); +} + +/** + * Create a control ctrl. Point is in document coordinates. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p) + : CanvasItemCtrl(group, type) +{ + _position = p; + request_update(); +} + +/** + * Create a control ctrl. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape) + : CanvasItem(group) + , _shape(shape) + , _type(CANVAS_ITEM_CTRL_TYPE_DEFAULT) +{ + _name = "CanvasItemCtrl:Shape_" + std::to_string(_shape); + _pickable = true; // Everybody gets events from this class! +} + +/** + * Create a control ctrl. Point is in document coordinates. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p) + : CanvasItemCtrl(group, shape) +{ + _position = p; +} + +/** + * Set the position. Point is in document coordinates. + */ +void CanvasItemCtrl::set_position(Geom::Point const &position) +{ + // std::cout << "CanvasItemCtrl::set_ctrl: " << _name << ": " << position << std::endl; + defer([=] { + if (_position == position) return; + _position = position; + request_update(); + }); +} + +/** + * Returns distance between point in canvas units and position of ctrl. + */ +double CanvasItemCtrl::closest_distance_to(Geom::Point const &p) const +{ + // TODO: Different criteria for different shapes. + return Geom::distance(p, _position * affine()); +} + +/** + * If tolerance is zero, returns true if point p (in canvas units) is inside bounding box, + * else returns true if p (in canvas units) is within tolerance (canvas units) distance of ctrl. + * The latter assumes ctrl center anchored. + */ +bool CanvasItemCtrl::contains(Geom::Point const &p, double tolerance) +{ + // TODO: Different criteria for different shapes. + if (!_bounds) return false; + if (tolerance == 0) { + return _bounds->interiorContains(p); + } else { + return closest_distance_to(p) <= tolerance; + } +} + +static auto angle_of(Geom::Affine const &affine) +{ + return std::atan2(affine[1], affine[0]); +} + +/** + * Update and redraw control ctrl. + */ +void CanvasItemCtrl::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Setting the position to (inf, inf) to hide it is a pervasive hack we need to support. + if (!_position.isFinite()) { + _bounds = {}; + return; + } + + // Width and height are always odd. + assert(_width % 2 == 1); + assert(_height % 2 == 1); + + // Get half width and height, rounded down. + int const w_half = _width / 2; + int const h_half = _height / 2; + + // Set _angle, and compute adjustment for anchor. + int dx = 0; + int dy = 0; + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_DARROW: + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + { + double angle = _anchor * M_PI_4 + angle_of(affine()); + double const half = _width / 2.0; + + dx = -(half + 2) * cos(angle); // Add a bit to prevent tip from overlapping due to rounding errors. + dy = -(half + 2) * sin(angle); + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + angle += 5 * M_PI_4; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + angle += M_PI_2; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + dx = -(half / 2 + 2) * cos(angle); + dy = -(half / 2 + 2) * sin(angle); + angle -= M_PI_2; + break; + + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + angle -= M_PI_4; + dx = (half / 2 + 2) * ( sin(angle) - cos(angle)); + dy = (half / 2 + 2) * (-sin(angle) - cos(angle)); + break; + + default: + break; + } + + if (_angle != angle) { + _angle = angle; + _built.reset(); + } + + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: { + double const angle = angle_of(affine()); + if (_angle != angle) { + _angle = angle; + _built.reset(); + } + break; + } + + default: + switch (_anchor) { + case SP_ANCHOR_N: + case SP_ANCHOR_CENTER: + case SP_ANCHOR_S: + break; + + case SP_ANCHOR_NW: + case SP_ANCHOR_W: + case SP_ANCHOR_SW: + dx = w_half; + break; + + case SP_ANCHOR_NE: + case SP_ANCHOR_E: + case SP_ANCHOR_SE: + dx = -w_half; + break; + } + + switch (_anchor) { + case SP_ANCHOR_W: + case SP_ANCHOR_CENTER: + case SP_ANCHOR_E: + break; + + case SP_ANCHOR_NW: + case SP_ANCHOR_N: + case SP_ANCHOR_NE: + dy = h_half; + break; + + case SP_ANCHOR_SW: + case SP_ANCHOR_S: + case SP_ANCHOR_SE: + dy = -h_half; + break; + } + break; + } + + auto const pt = Geom::IntPoint(-w_half, -h_half) + Geom::IntPoint(dx, dy) + (_position * affine()).floor(); + _bounds = Geom::IntRect(pt, pt + Geom::IntPoint(_width, _height)); + + // Queue redraw of new area + request_redraw(); +} + +static inline uint32_t compose_xor(uint32_t bg, uint32_t fg, uint32_t a) +{ + uint32_t c = bg * (255 - a) + (((bg ^ ~fg) + (bg >> 2) - (bg > 127 ? 63 : 0)) & 255) * a; + return (c + 127) / 255; +} + +/** + * Render ctrl to screen via Cairo. + */ +void CanvasItemCtrl::_render(CanvasItemBuffer &buf) const +{ + _built.init([&, this] { + build_cache(buf.device_scale); + }); + + Geom::Point c = _bounds->min() - buf.rect.min(); + int x = c.x(); // Must be pixel aligned. + int y = c.y(); + + buf.cr->save(); + + // This code works regardless of source type. + + // 1. Copy the affected part of output to a temporary surface + + // Size in device pixels. Does not set device scale. + int width = _width * buf.device_scale; + int height = _height * buf.device_scale; + auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, width, height); + cairo_surface_set_device_scale(work->cobj(), buf.device_scale, buf.device_scale); // No C++ API! + + auto cr = Cairo::Context::create(work); + cr->translate(-_bounds->left(), -_bounds->top()); + cr->set_source(buf.cr->get_target(), buf.rect.left(), buf.rect.top()); + cr->paint(); + // static int a = 0; + // std::string name0 = "ctrl0_" + _name + "_" + std::to_string(a++) + ".png"; + // work->write_to_png(name0); + + // 2. Composite the control on a temporary surface + work->flush(); + int strideb = work->get_stride(); + unsigned char *pxb = work->get_data(); + + // this code allow background become isolated from rendering so we can do things like outline overlay + uint32_t backcolor = get_canvas()->get_effective_background(); + uint32_t *p = _cache.get(); + for (int i = 0; i < height; ++i) { + auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb); + for (int j = 0; j < width; ++j) { + uint32_t base = *pb; + uint32_t cc = *p++; + uint32_t ac = cc & 0xff; + if (*pb == 0 && cc != 0) { + base = backcolor; + } + if (ac == 0 && cc != 0) { + *pb++ = argb32_from_rgba(cc | 0x000000ff); + } else if (ac == 0) { + *pb++ = base; + } else if ( + _mode == CANVAS_ITEM_CTRL_MODE_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) + { + EXTRACT_ARGB32(base, ab,rb,gb,bb) + // here we get canvas color and if color to draw + // has opacity, we override base colors + // flattenig canvas color + EXTRACT_ARGB32(backcolor, abb,rbb,gbb,bbb) + if (abb != ab) { + rb = (ab/255.0) * rb + (1-(ab/255.0)) * rbb; + gb = (ab/255.0) * gb + (1-(ab/255.0)) * gbb; + bb = (ab/255.0) * bb + (1-(ab/255.0)) * bbb; + ab = 255; + } + uint32_t ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac); + uint32_t go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac); + uint32_t bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac); + if (_mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) { + uint32_t gray = ro * 0.299 + go * 0.587 + bo * 0.114; + if (_mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) { + double f = 0.85; // desaturate by 15% + double p = sqrt(ro * ro * 0.299 + go * go * 0.587 + bo * bo * 0.114); + ro = p + (ro - p) * f; + go = p + (go - p) * f; + bo = p + (bo - p) * f; + } else { + ro = gray; + go = gray; + bo = gray; + } + } + ASSEMBLE_ARGB32(px, ab,ro,go,bo) + *pb++ = px; + } else { + *pb++ = argb32_from_rgba(cc | 0x000000ff); + } + } + } + work->mark_dirty(); + // std::string name1 = "ctrl1_" + _name + "_" + std::to_string(a) + ".png"; + // work->write_to_png(name1); + + // 3. Replace the affected part of output with contents of temporary surface + buf.cr->set_source(work, x, y); + + buf.cr->rectangle(x, y, _width, _height); + buf.cr->clip(); + buf.cr->set_operator(Cairo::OPERATOR_SOURCE); + buf.cr->paint(); + buf.cr->restore(); +} + +void CanvasItemCtrl::set_fill(uint32_t fill) +{ + defer([=] { + if (_fill == fill) return; + _fill = fill; + _built.reset(); + request_redraw(); + }); +} + +void CanvasItemCtrl::set_stroke(uint32_t stroke) +{ + defer([=] { + if (_stroke == stroke) return; + _stroke = stroke; + _built.reset(); + request_redraw(); + }); +} + +void CanvasItemCtrl::set_shape(CanvasItemCtrlShape shape) +{ + defer([=] { + if (_shape == shape) return; + _shape = shape; + _built.reset(); + request_update(); // Geometry could change + }); +} + +void CanvasItemCtrl::set_shape_default() +{ + switch (_type) { + case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE: + _shape = CANVAS_ITEM_CTRL_SHAPE_DARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW: + _shape = CANVAS_ITEM_CTRL_SHAPE_SARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE: + _shape = CANVAS_ITEM_CTRL_SHAPE_CARROW; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER: + _shape = CANVAS_ITEM_CTRL_SHAPE_PIVOT; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_SALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_CALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN: + _shape = CANVAS_ITEM_CTRL_SHAPE_MALIGN; + break; + + case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO: + case CANVAS_ITEM_CTRL_TYPE_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_MARGIN: + _shape = CANVAS_ITEM_CTRL_SHAPE_CIRCLE; + break; + + case CANVAS_ITEM_CTRL_TYPE_CENTER: + _shape = CANVAS_ITEM_CTRL_SHAPE_PLUS; + break; + + case CANVAS_ITEM_CTRL_TYPE_SHAPER: + case CANVAS_ITEM_CTRL_TYPE_LPE: + case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP: + _shape = CANVAS_ITEM_CTRL_SHAPE_DIAMOND; + break; + + case CANVAS_ITEM_CTRL_TYPE_POINT: + _shape = CANVAS_ITEM_CTRL_SHAPE_CROSS; + break; + + default: + _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE; + } +} + +void CanvasItemCtrl::set_mode(CanvasItemCtrlMode mode) +{ + defer([=] { + if (_mode == mode) return; + _mode = mode; + _built.reset(); + request_update(); + }); +} + +void CanvasItemCtrl::set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf) +{ + defer([=, pixbuf = std::move(pixbuf)] () mutable { + if (_pixbuf != pixbuf) return; + _pixbuf = std::move(pixbuf); + _width = _pixbuf->get_width(); + _height = _pixbuf->get_height(); + _built.reset(); + request_update(); + }); +} + +// Nominally width == height == size except possibly for pixmaps. +void CanvasItemCtrl::set_size(int size) +{ + defer([=] { + if (_pixbuf) { + // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl; + return; + } + if (_width == size + _extra && _height == size + _extra) return; + _width = size + _extra; + _height = size + _extra; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_size_via_index(int size_index) +{ + // Size must always be an odd number to center on pixel. + + if (size_index < 1 || size_index > 15) { + std::cerr << "CanvasItemCtrl::set_size_via_index: size_index out of range!" << std::endl; + size_index = 3; + } + + int size = 0; + switch (_type) { + case CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE: + case CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW: + size = size_index * 2 + 7; + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER: + size = size_index * 2 + 9; // 2 larger than HANDLE/SKEW + break; + + case CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN: + case CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN: + case CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN: + size = size_index * 4 + 5; // Needs to be larger to allow for rotating. + break; + + case CANVAS_ITEM_CTRL_TYPE_POINT: + case CANVAS_ITEM_CTRL_TYPE_ROTATE: + case CANVAS_ITEM_CTRL_TYPE_MARGIN: + case CANVAS_ITEM_CTRL_TYPE_CENTER: + case CANVAS_ITEM_CTRL_TYPE_SIZER: + case CANVAS_ITEM_CTRL_TYPE_SHAPER: + case CANVAS_ITEM_CTRL_TYPE_LPE: + case CANVAS_ITEM_CTRL_TYPE_NODE_AUTO: + case CANVAS_ITEM_CTRL_TYPE_NODE_CUSP: + size = size_index * 2 + 5; + break; + + case CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH: + case CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL: + size = size_index * 2 + 3; + break; + + case CANVAS_ITEM_CTRL_TYPE_INVISIPOINT: + size = 1; + break; + + case CANVAS_ITEM_CTRL_TYPE_ANCHOR: // vanishing point for 3D box and anchor for pencil + size = size_index * 2 + 1; + break; + + case CANVAS_ITEM_CTRL_TYPE_DEFAULT: + size = size_index * 2 + 1; + break; + + default: + g_warning("set_size_via_index: missing case for handle type: %d", static_cast<int>(_type)); + size = size_index * 2 + 1; + break; + } + + set_size(size); +} + +void CanvasItemCtrl::set_size_default() +{ + int size = Preferences::get()->getIntLimited("/options/grabsize/value", 3, 1, 15); + set_size_via_index(size); +} + +void CanvasItemCtrl::set_size_extra(int extra) +{ + defer([=] { + if (_extra == extra || _pixbuf) return; // Don't enlarge pixbuf! + _width += extra - _extra; + _height += extra - _extra; + _extra = extra; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_type(CanvasItemCtrlType type) +{ + defer([=] { + if (_type == type) return; + _type = type; + + // Use _type to set default values. + set_shape_default(); + set_size_default(); + _built.reset(); + request_update(); // Possible geometry change + }); +} + +void CanvasItemCtrl::set_angle(double angle) +{ + defer([=] { + if (_angle == angle) return; + _angle = angle; + _built.reset(); + request_update(); // Geometry change + }); +} + +void CanvasItemCtrl::set_anchor(SPAnchorType anchor) +{ + defer([=] { + if (_anchor == anchor) return; + _anchor = anchor; + request_update(); // Geometry change + }); +} + +// ---------- Protected ---------- + +static void draw_darrow(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Find points, starting from tip of one arrowhead, working clockwise. + /* 1 4 + ╱│ │╲ + ╱ └────────┘ ╲ + 0╱ 2 3 ╲5 + ╲ 8 7 ╱ + ╲ ┌────────┐ ╱ + ╲│9 6│╱ + */ + + // Length of arrowhead (not including stroke). + double delta = (size-1)/4.0; // Use unscaled width. + + // Tip of arrow (0) + double tip_x = 0.5; // At edge, allow room for stroke. + double tip_y = size/2.0; // Center, assuming width == height. + + // Outer corner (1) + double out_x = tip_x + delta; + double out_y = tip_y - delta; + + // Inner corner (2) + double in_x = out_x; + double in_y = out_y + (delta/2.0); + + double x0 = tip_x; double y0 = tip_y; + double x1 = out_x; double y1 = out_y; + double x2 = in_x; double y2 = in_y; + double x3 = size - in_x; double y3 = in_y; + double x4 = size - out_x; double y4 = out_y; + double x5 = size - tip_x; double y5 = tip_y; + double x6 = size - out_x; double y6 = size - out_y; + double x7 = size - in_x; double y7 = size - in_y; + double x8 = in_x; double y8 = size - in_y; + double x9 = out_x; double y9 = size - out_y; + + // Draw arrow + cr->move_to(x0, y0); + cr->line_to(x1, y1); + cr->line_to(x2, y2); + cr->line_to(x3, y3); + cr->line_to(x4, y4); + cr->line_to(x5, y5); + cr->line_to(x6, y6); + cr->line_to(x7, y7); + cr->line_to(x8, y8); + cr->line_to(x9, y9); + cr->close_path(); +} + +static void draw_carrow(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Length of arrowhead (not including stroke). + double delta = (size-3)/4.0; // Use unscaled width. + + // Tip of arrow + double tip_x = 1.5; // Edge, allow room for stroke when rotated. + double tip_y = delta + 1.5; + + // Outer corner (1) + double out_x = tip_x + delta; + double out_y = tip_y - delta; + + // Inner corner (2) + double in_x = out_x; + double in_y = out_y + (delta/2.0); + + double x0 = tip_x; double y0 = tip_y; + double x1 = out_x; double y1 = out_y; + double x2 = in_x; double y2 = in_y; + double x3 = size - in_y; //double y3 = size - in_x; + double x4 = size - out_y; double y4 = size - out_x; + double x5 = size - tip_y; double y5 = size - tip_x; + double x6 = x5 - delta; double y6 = y4; + double x7 = x5 - delta/2.0; double y7 = y4; + double x8 = x1; //double y8 = y0 + delta/2.0; + double x9 = x1; double y9 = y0 + delta; + + // Draw arrow + cr->move_to(x0, y0); + cr->line_to(x1, y1); + cr->line_to(x2, y2); + cr->arc(x1, y4, x3-x2, 3.0*M_PI/2.0, 0); + cr->line_to(x4, y4); + cr->line_to(x5, y5); + cr->line_to(x6, y6); + cr->line_to(x7, y7); + cr->arc_negative(x1, y4, x7-x8, 0, 3.0*M_PI/2.0); + cr->line_to(x9, y9); + cr->close_path(); +} + +static void draw_triangle(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Construct an arrowhead (triangle) + double s = size/2.0; + double wcos = s * cos( M_PI/6 ); + double hsin = s * sin( M_PI/6 ); + // Construct a smaller arrow head for fill. + Geom::Point p1f(1, s); + Geom::Point p2f(s + wcos - 1, s + hsin); + Geom::Point p3f(s + wcos - 1, s - hsin); + // Draw arrow + cr->move_to(p1f[0], p1f[1]); + cr->line_to(p2f[0], p2f[1]); + cr->line_to(p3f[0], p3f[1]); + cr->close_path(); +} + +static void draw_triangle_angled(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Construct an arrowhead (triangle) of half size. + double s = size/2.0; + double wcos = s * cos( M_PI/9 ); + double hsin = s * sin( M_PI/9 ); + Geom::Point p1f(s + 1, s); + Geom::Point p2f(s + wcos - 1, s + hsin - 1); + Geom::Point p3f(s + wcos - 1, s - (hsin - 1)); + // Draw arrow + cr->move_to(p1f[0], p1f[1]); + cr->line_to(p2f[0], p2f[1]); + cr->line_to(p3f[0], p3f[1]); + cr->close_path(); +} + +static void draw_pivot(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + double delta4 = (size-5)/4.0; // Keep away from edge or will clip when rotating. + double delta8 = delta4/2; + + // Line start + double center = size/2.0; + + cr->move_to (center - delta8, center - 2*delta4 - delta8); + cr->rel_line_to ( delta4, 0 ); + cr->rel_line_to ( 0, delta4); + + cr->rel_line_to ( delta4, delta4); + + cr->rel_line_to ( delta4, 0 ); + cr->rel_line_to ( 0, delta4); + cr->rel_line_to (-delta4, 0 ); + + cr->rel_line_to (-delta4, delta4); + + cr->rel_line_to ( 0, delta4); + cr->rel_line_to (-delta4, 0 ); + cr->rel_line_to ( 0, -delta4); + + cr->rel_line_to (-delta4, -delta4); + + cr->rel_line_to (-delta4, 0 ); + cr->rel_line_to ( 0, -delta4); + cr->rel_line_to ( delta4, 0 ); + + cr->rel_line_to ( delta4, -delta4); + cr->close_path(); + + cr->begin_new_sub_path(); + cr->arc_negative(center, center, delta4, 0, -2 * M_PI); +} + +static void draw_salign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Triangle pointing at line. + + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_x = size/2.0; // Center (also rotation point). + double tip_y = size/2.0; + + // Corner triangle position. + double outer = size/2.0 - delta4; + + // Outer line position + double oline = size/2.0 + (int)delta4; + + // Inner line position + double iline = size/2.0 + (int)delta8; + + // Draw triangle + cr->move_to(tip_x, tip_y); + cr->line_to(outer, outer); + cr->line_to(size - outer, outer); + cr->close_path(); + + // Draw line + cr->move_to(outer, iline); + cr->line_to(size - outer, iline); + cr->line_to(size - outer, oline); + cr->line_to(outer, oline); + cr->close_path(); +} + +static void draw_calign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_x = size/2.0; // Center (also rotation point). + double tip_y = size/2.0; + + // Corner triangle position. + double outer = size/2.0 - delta8 - delta4; + + // End of line positin + double eline = size/2.0 - delta8; + + // Outer line position + double oline = size/2.0 + (int)delta4; + + // Inner line position + double iline = size/2.0 + (int)delta8; + + // Draw triangle + cr->move_to(tip_x, tip_y); + cr->line_to(outer, tip_y); + cr->line_to(tip_x, outer); + cr->close_path(); + + // Draw line + cr->move_to(iline, iline); + cr->line_to(iline, eline); + cr->line_to(oline, eline); + cr->line_to(oline, oline); + cr->line_to(eline, oline); + cr->line_to(eline, iline); + cr->close_path(); +} + +static void draw_malign(Cairo::RefPtr<Cairo::Context> const &cr, double size) +{ + // Basic units. + double delta4 = (size-1)/4.0; // Use unscaled width. + double delta8 = delta4/2; + if (delta8 < 2) { + // Keep a minimum gap of at least one pixel (after stroking). + delta8 = 2; + } + + // Tip of triangle + double tip_0 = size/2.0; + double tip_1 = size/2.0 - delta8; + + // Draw triangles + cr->move_to(tip_0, tip_1); + cr->line_to(tip_0 - delta4, tip_1 - delta4); + cr->line_to(tip_0 + delta4, tip_1 - delta4); + cr->close_path(); + + cr->move_to(size - tip_1, tip_0); + cr->line_to(size - tip_1 + delta4, tip_0 - delta4); + cr->line_to(size - tip_1 + delta4, tip_0 + delta4); + cr->close_path(); + + cr->move_to(size - tip_0, size - tip_1); + cr->line_to(size - tip_0 + delta4, size - tip_1 + delta4); + cr->line_to(size - tip_0 - delta4, size - tip_1 + delta4); + cr->close_path(); + + cr->move_to(tip_1, tip_0); + cr->line_to(tip_1 - delta4, tip_0 + delta4); + cr->line_to(tip_1 - delta4, tip_0 - delta4); + cr->close_path(); +} + +void CanvasItemCtrl::build_cache(int device_scale) const +{ + if (_width < 2 || _height < 2) { + return; // Nothing to render + } + + if (_shape != CANVAS_ITEM_CTRL_SHAPE_BITMAP) { + if (_width % 2 == 0 || _height % 2 == 0) { + std::cerr << "CanvasItemCtrl::build_cache: Width and/or height not odd integer! " + << _name << ": width: " << _width << " height: " << _height << std::endl; + } + } + + // Get memory for cache. + int width = _width * device_scale; // Not unsigned or math errors occur! + int height = _height * device_scale; + int size = width * height; + + _cache = std::make_unique<uint32_t[]>(size); + auto p = _cache.get(); + + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_SQUARE: + // Actually any rectanglular shape. + for (int i = 0; i < width; ++i) { + for (int j = 0; j < width; ++j) { + if (i + 1 > device_scale && device_scale < width - i && + j + 1 > device_scale && device_scale < height - j) + { + *p++ = _fill; + } else { + *p++ = _stroke; + } + } + } + break; + + case CANVAS_ITEM_CTRL_SHAPE_DIAMOND: { + // Assume width == height. + int m = (width+1)/2; + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + if ( i + j > m-1+device_scale && + (width-1-i) + j > m-1+device_scale && + (width-1-i) + (height-1-j) > m-1+device_scale && + i + (height-1-j) > m-1+device_scale ) { + *p++ = _fill; + } else + if ( i + j > m-2 && + (width-1-i) + j > m-2 && + (width-1-i) + (height-1-j) > m-2 && + i + (height-1-j) > m-2 ) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_CIRCLE: { + // Assume width == height. + double rs = width/2.0; + double rs2 = rs*rs; + double rf = rs-device_scale; + double rf2 = rf*rf; + + for (int i = 0; i < width; ++i) { + for (int j = 0; j < height; ++j) { + + double rx = i - (width /2.0) + 0.5; + double ry = j - (height/2.0) + 0.5; + double r2 = rx*rx + ry*ry; + + if (r2 < rf2) { + *p++ = _fill; + } else if (r2 < rs2) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_CROSS: + // Actually an 'X'. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if ( abs(x - y) < device_scale || + abs(width - 1 - x - y) < device_scale ) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + + case CANVAS_ITEM_CTRL_SHAPE_PLUS: + // Actually an '+'. + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + if ( std::abs(x-width/2) < device_scale || + std::abs(y-height/2) < device_scale ) { + *p++ = _stroke; + } else { + *p++ = 0; + } + } + } + break; + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: //triangle optionaly rotated + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED: // triangle with pointing to center of knot and rotated this way + case CANVAS_ITEM_CTRL_SHAPE_DARROW: // Double arrow + case CANVAS_ITEM_CTRL_SHAPE_SARROW: // Same shape as darrow but rendered rotated 90 degrees. + case CANVAS_ITEM_CTRL_SHAPE_CARROW: // Double corner arrow + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: // Fancy "plus" + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: // Side align (triangle pointing toward line) + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: // Corner align (triangle pointing into "L") + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: // Middle align (four triangles poining inward) + { + double size = _width; // Use unscaled width. + + auto work = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, device_scale * size, device_scale * size); + cairo_surface_set_device_scale(work->cobj(), device_scale, device_scale); // No C++ API! + auto cr = Cairo::Context::create(work); + + // Rotate around center + cr->translate( size/2.0, size/2.0); + cr->rotate(_angle); + cr->translate(-size/2.0, -size/2.0); + + // Construct path + bool triangles = _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED || _shape == CANVAS_ITEM_CTRL_SHAPE_TRIANGLE; + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_DARROW: + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + draw_darrow(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: + draw_triangle(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED: + draw_triangle_angled(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_CARROW: + draw_carrow(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: + draw_pivot(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + draw_salign(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + draw_calign(cr, size); + break; + + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: + draw_malign(cr, size); + break; + + default: + // Shouldn't happen + break; + } + + // Fill and stroke. + cr->set_source_rgba(SP_RGBA32_R_F(_fill), + SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), + SP_RGBA32_A_F(_fill)); + cr->fill_preserve(); + cr->set_source_rgba(SP_RGBA32_R_F(_stroke), + SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), + SP_RGBA32_A_F(_stroke)); + cr->set_line_width(1); + cr->stroke(); + + // Copy to buffer. + work->flush(); + int strideb = work->get_stride(); + unsigned char* pxb = work->get_data(); + auto p = _cache.get(); + for (int i = 0; i < device_scale * size; ++i) { + auto pb = reinterpret_cast<uint32_t*>(pxb + i * strideb); + for (int j = 0; j < width; ++j) { + + if (triangles) { + *p++ = rgba_from_argb32(*pb); + } else { + uint32_t color = 0x0; + + // Need to un-premultiply alpha and change order argb -> rgba. + uint32_t alpha = (*pb & 0xff000000) >> 24; + if (alpha == 0x0) { + color = 0x0; + } else { + uint32_t rgb = unpremul_alpha(*pb & 0xffffff, alpha); + color = (rgb << 8) + alpha; + } + *p++ = color; + } + pb++; + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_BITMAP: + { + if (_pixbuf) { + unsigned char* px = _pixbuf->get_pixels(); + unsigned int rs = _pixbuf->get_rowstride(); + for (int y = 0; y < height/device_scale; y++){ + for (int x = 0; x < width/device_scale; x++) { + unsigned char *s = px + rs*y + 4*x; + uint32_t color; + if (s[3] < 0x80) { + color = 0; + } else if (s[0] < 0x80) { + color = _stroke; + } else { + color = _fill; + } + + // Fill in device_scale x device_scale block + for (int i = 0; i < device_scale; ++i) { + for (int j = 0; j < device_scale; ++j) { + auto p = _cache.get() + + (x * device_scale + i) + // Column + (y * device_scale + j) * width; // Row + *p = color; + } + } + } + } + } else { + std::cerr << "CanvasItemCtrl::build_cache: No bitmap!" << std::endl; + auto p = _cache.get(); + for (int y = 0; y < height/device_scale; y++){ + for (int x = 0; x < width/device_scale; x++) { + if (x == y) { + *p++ = 0xffff0000; + } else { + *p++ = 0; + } + } + } + } + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_IMAGE: + std::cerr << "CanvasItemCtrl::build_cache: image: UNIMPLEMENTED" << std::endl; + break; + + default: + std::cerr << "CanvasItemCtrl::build_cache: unhandled shape!" << std::endl; + break; + } +} + +} // 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 : diff --git a/src/display/control/canvas-item-ctrl.h b/src/display/control/canvas-item-ctrl.h new file mode 100644 index 0000000..6c072a2 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.h @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CTRL_H +#define SEEN_CANVAS_ITEM_CTRL_H + +/** + * A class to represent a control node. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrl + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/point.h> +#include <gdk-pixbuf/gdk-pixbuf.h> + +#include "canvas-item.h" +#include "canvas-item-enums.h" + +#include "enums.h" // SP_ANCHOR_X +#include "display/initlock.h" + +namespace Inkscape { + +class CanvasItemCtrl : public CanvasItem +{ +public: + CanvasItemCtrl(CanvasItemGroup *group); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlType type, Geom::Point const &p); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape); + CanvasItemCtrl(CanvasItemGroup *group, CanvasItemCtrlShape shape, Geom::Point const &p); + + // Geometry + void set_position(Geom::Point const &position); + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_fill(uint32_t rgba) override; + void set_stroke(uint32_t rgba) override; + void set_shape(CanvasItemCtrlShape shape); + void set_shape_default(); // Use type to determine shape. + void set_mode(CanvasItemCtrlMode mode); + void set_mode_default(); + void set_size(int size); + virtual void set_size_via_index(int size_index); + void set_size_default(); // Use preference and type to set size. + void set_size_extra(int extra); // Used to temporary increase size of ctrl. + void set_anchor(SPAnchorType anchor); + void set_angle(double angle); + void set_type(CanvasItemCtrlType type); + void set_pixbuf(Glib::RefPtr<Gdk::Pixbuf> pixbuf); + +protected: + ~CanvasItemCtrl() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + void build_cache(int device_scale) const; + + // Geometry + Geom::Point _position; + + // Display + InitLock _built; + mutable std::unique_ptr<uint32_t[]> _cache; + + // Properties + CanvasItemCtrlType _type = CANVAS_ITEM_CTRL_TYPE_DEFAULT; + CanvasItemCtrlShape _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE; + CanvasItemCtrlMode _mode = CANVAS_ITEM_CTRL_MODE_XOR; + int _width = 5; // Nominally width == height == size... unless we use a pixmap. + int _height = 5; + int _extra = 0; // Used to temporarily increase size. + double _angle = 0; // Used for triangles, could be used for arrows. + SPAnchorType _anchor = SP_ANCHOR_CENTER; + Glib::RefPtr<Gdk::Pixbuf> _pixbuf; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CTRL_H + +/* + 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 : diff --git a/src/display/control/canvas-item-curve.cpp b/src/display/control/canvas-item-curve.cpp new file mode 100644 index 0000000..c37c1a0 --- /dev/null +++ b/src/display/control/canvas-item-curve.cpp @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a single Bezier control curve, either a line or a cubic Bezier. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlLine and SPCtrlCurve + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/bezier-curve.h> + +#include "canvas-item-curve.h" + +#include "color.h" // SP_RGBA_x_F + +#include "helper/geom.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control curve. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCurve:Null"; +} + +/** + * Create a linear control curve. Points are in document coordinates. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1) + : CanvasItem(group) + , _curve(std::make_unique<Geom::LineSegment>(p0, p1)) +{ + _name = "CanvasItemCurve:Line"; +} + +/** + * Create a cubic Bezier control curve. Points are in document coordinates. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group, + Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3) + : CanvasItem(group) + , _curve(std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3)) +{ + _name = "CanvasItemCurve:CubicBezier"; +} + +/** + * Set a linear control curve. Points are in document coordinates. + */ +void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1) +{ + defer([=] { + _name = "CanvasItemCurve:Line"; + _curve = std::make_unique<Geom::LineSegment>(p0, p1); + request_update(); + }); +} + +/** + * Set a cubic Bezier control curve. Points are in document coordinates. + */ +void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) +{ + defer([=] { + _name = "CanvasItemCurve:CubicBezier"; + _curve = std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3); + request_update(); + }); +} + +/** + * Set stroke width. + */ +void CanvasItemCurve::set_width(int width) +{ + defer([=] { + if (_width == width) return; + _width = width; + request_update(); + }); +} + +/** + * Set background stroke alpha. + */ +void CanvasItemCurve::set_bg_alpha(float alpha) +{ + defer([=] { + if (bg_alpha == alpha) return; + bg_alpha = alpha; + request_update(); + }); +} + +/** + * Returns distance between point in canvas units and nearest point on curve. + */ +double CanvasItemCurve::closest_distance_to(Geom::Point const &p) const +{ + double d = Geom::infinity(); + if (_curve) { + Geom::BezierCurve curve = *_curve; + curve *= affine(); // Document to canvas. + Geom::Point n = curve.pointAt(curve.nearestTime(p)); + d = Geom::distance(p, n); + } + return d; +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of curve. + */ +bool CanvasItemCurve::contains(Geom::Point const &p, double tolerance) +{ + return closest_distance_to(p) <= tolerance; +} + +/** + * Update and redraw control curve. + */ +void CanvasItemCurve::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); // This is actually never useful as curves are always deleted + // and recreated when a node is moved! But keep it in case we change that. + + if (!_curve || _curve->isDegenerate()) { + _bounds = {}; + return; // No curve! Can happen - see node.h. + } + + // Tradeoff between updating a larger area (typically twice for Beziers?) vs computation time for bounds. + _bounds = expandedBy(_curve->boundsExact() * affine(), 2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render curve to screen via Cairo. + */ +void CanvasItemCurve::_render(Inkscape::CanvasItemBuffer &buf) const +{ + assert(_curve); // Not called if _curve is null, since _bounds would be null. + + // Todo: Transform, rather than copy. + Geom::BezierCurve curve = *_curve; + curve *= affine(); // Document to canvas. + curve *= Geom::Translate(-buf.rect.min()); // Canvas to screen. + + buf.cr->save(); + + buf.cr->begin_new_path(); + + if (curve.size() == 2) { + // Line + buf.cr->move_to(curve[0].x(), curve[0].y()); + buf.cr->line_to(curve[1].x(), curve[1].y()); + } else { + // Curve + buf.cr->move_to(curve[0].x(), curve[0].y()); + buf.cr->curve_to(curve[1].x(), curve[1].y(), curve[2].x(), curve[2].y(), curve[3].x(), curve[3].y()); + } + + buf.cr->set_source_rgba(1.0, 1.0, 1.0, bg_alpha); + buf.cr->set_line_width(background_width); + buf.cr->stroke_preserve(); + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->set_line_width(_width); + buf.cr->stroke(); + + // Uncomment to show bounds + // Geom::Rect bounds = _bounds; + // bounds.expandBy(-1); + // bounds -= buf.rect.min(); + // buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + // buf.cr->stroke(); + + 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 : diff --git a/src/display/control/canvas-item-curve.h b/src/display/control/canvas-item-curve.h new file mode 100644 index 0000000..8e1f9b5 --- /dev/null +++ b/src/display/control/canvas-item-curve.h @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_CURVE_H +#define SEEN_CANVAS_ITEM_CURVE_H + +/** + * A class to represent a single Bezier control curve. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlLine and SPCtrlCurve + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/path.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemCurve final : public CanvasItem +{ +public: + CanvasItemCurve(CanvasItemGroup *group); + CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1); + CanvasItemCurve(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3); + + // Geometry + void set_coords(Geom::Point const &p0, Geom::Point const &p1); + void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3); + void set_width(int width); + void set_bg_alpha(float alpha); + bool is_line() const { return _curve->size() == 2; } + + double closest_distance_to(Geom::Point const &p) const; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + +protected: + ~CanvasItemCurve() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Display + std::unique_ptr<Geom::BezierCurve> _curve; + + int _width = 1; + int background_width = 3; // this should be an odd number so that the background appears on both the sides of the curve. + float bg_alpha = 0.5f; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_CURVE_H + +/* + 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 : diff --git a/src/display/control/canvas-item-drawing.cpp b/src/display/control/canvas-item-drawing.cpp new file mode 100644 index 0000000..cfa527a --- /dev/null +++ b/src/display/control/canvas-item-drawing.cpp @@ -0,0 +1,220 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to render the SVG drawing. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of _SPCanvasArena. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-drawing.h" + +#include "desktop.h" + +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing-group.h" + +#include "helper/geom.h" +#include "ui/widget/canvas.h" +#include "ui/modifiers.h" + +namespace Inkscape { + +/** + * Create the drawing. One per window! + */ +CanvasItemDrawing::CanvasItemDrawing(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemDrawing"; + _pickable = true; + + _drawing = std::make_unique<Drawing>(this); + auto root = new DrawingGroup(*_drawing); + root->setPickChildren(true); + _drawing->setRoot(root); +} + +/** + * Returns true if point p (in canvas units) is inside some object in drawing. + */ +bool CanvasItemDrawing::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemDrawing::contains: Non-zero tolerance not implemented!" << std::endl; + } + + _picked_item = _drawing->pick(p, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + + if (_picked_item) { + // This will trigger a signal that is handled by our event handler. Seems a bit of a + // round-about way of doing things but it matches what other pickable canvas-item classes do. + return true; + } + + return false; +} + +/** + * Update and redraw drawing. + */ +void CanvasItemDrawing::_update(bool) +{ + // Undo y-axis flip. This should not be here!!!! + auto new_drawing_affine = affine(); + if (auto desktop = get_canvas()->get_desktop()) { + new_drawing_affine = desktop->doc2dt() * new_drawing_affine; + } + + bool affine_changed = _drawing_affine != new_drawing_affine; + if (affine_changed) { + _drawing_affine = new_drawing_affine; + } + + _drawing->update(Geom::IntRect::infinite(), _drawing_affine, DrawingItem::STATE_ALL, affine_changed * DrawingItem::STATE_ALL); + + _bounds = expandedBy(_drawing->root()->drawbox(), 1); // Avoid aliasing artifacts + + // Todo: This should be managed elsewhere. + if (_cursor) { + /* Mess with enter/leave notifiers */ + DrawingItem *new_drawing_item = _drawing->pick(_c, _delta, _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + if (_active_item != new_drawing_item) { + + GdkEventCrossing ec; + ec.window = get_canvas()->get_window()->gobj(); + ec.send_event = true; + ec.subwindow = ec.window; + ec.time = GDK_CURRENT_TIME; + ec.x = _c.x(); + ec.y = _c.y(); + + /* fixme: Why? */ + if (_active_item) { + ec.type = GDK_LEAVE_NOTIFY; + _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + + _active_item = new_drawing_item; + + if (_active_item) { + ec.type = GDK_ENTER_NOTIFY; + _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + } + } +} + +/** + * Render drawing to screen via Cairo. + */ +void CanvasItemDrawing::_render(Inkscape::CanvasItemBuffer &buf) const +{ + auto dc = Inkscape::DrawingContext(buf.cr->cobj(), buf.rect.min()); + _drawing->render(dc, buf.rect, buf.outline_pass * DrawingItem::RENDER_OUTLINE); +} + +/** + * Handle events directed at the drawing. We first attempt to handle them here. + */ +bool CanvasItemDrawing::handle_event(GdkEvent *event) +{ + bool retval = false; + + switch (event->type) { + case GDK_ENTER_NOTIFY: + if (!_cursor) { + if (_active_item) { + std::cerr << "CanvasItemDrawing::event_handler: cursor entered drawing with an active item!" << std::endl; + } + _cursor = true; + + /* TODO ... event -> arena transform? */ + _c = Geom::Point(event->crossing.x, event->crossing.y); + + _active_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + retval = _drawing_event_signal.emit(event, _active_item); + } + break; + + case GDK_LEAVE_NOTIFY: + if (_cursor) { + retval = _drawing_event_signal.emit(event, _active_item); + _active_item = nullptr; + _cursor = false; + } + break; + + case GDK_MOTION_NOTIFY: + { + /* TODO ... event -> arena transform? */ + _c = Geom::Point(event->motion.x, event->motion.y); + + auto new_drawing_item = _drawing->pick(_c, _drawing->cursorTolerance(), _sticky * DrawingItem::PICK_STICKY | _pick_outline * DrawingItem::PICK_OUTLINE); + if (_active_item != new_drawing_item) { + + GdkEventCrossing ec; + ec.window = event->motion.window; + ec.send_event = event->motion.send_event; + ec.subwindow = event->motion.window; + ec.time = event->motion.time; + ec.x = event->motion.x; + ec.y = event->motion.y; + + /* fixme: What is wrong? */ + if (_active_item) { + ec.type = GDK_LEAVE_NOTIFY; + retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + + _active_item = new_drawing_item; + + if (_active_item) { + ec.type = GDK_ENTER_NOTIFY; + retval = _drawing_event_signal.emit((GdkEvent *) &ec, _active_item); + } + } + retval = retval || _drawing_event_signal.emit(event, _active_item); + break; + } + + case GDK_SCROLL: + { + if (Modifiers::Modifier::get(Modifiers::Type::CANVAS_ZOOM)->active(event->scroll.state)) { + /* Zoom is emitted by the canvas as well, ignore here */ + return false; + } + retval = _drawing_event_signal.emit(event, _active_item); + break; + } + + default: + /* Just send event */ + retval = _drawing_event_signal.emit(event, _active_item); + break; + } + + return retval; +} + +} // 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 : diff --git a/src/display/control/canvas-item-drawing.h b/src/display/control/canvas-item-drawing.h new file mode 100644 index 0000000..f3fd494 --- /dev/null +++ b/src/display/control/canvas-item-drawing.h @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_DRAWING_H +#define SEEN_CANVAS_ITEM_DRAWING_H + +/** + * A class to render the SVG drawing. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of _SPCanvasArena. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/sigc++.h> + + +#include "canvas-item.h" + +namespace Inkscape { + +class Drawing; +class DrawingItem; +class Updatecontext; + +class CanvasItemDrawing final : public CanvasItem +{ +public: + CanvasItemDrawing(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + Inkscape::Drawing *get_drawing() { return _drawing.get(); } + + // Drawing items + void set_active(Inkscape::DrawingItem *active) { _active_item = active; } + Inkscape::DrawingItem *get_active() { return _active_item; } + + // Events + bool handle_event(GdkEvent *event) override; + void set_sticky(bool sticky) { _sticky = sticky; } + void set_pick_outline(bool pick_outline) { _pick_outline = pick_outline; } + + // Signals + sigc::connection connect_drawing_event(sigc::slot<bool (GdkEvent*, Inkscape::DrawingItem *)> slot) { + return _drawing_event_signal.connect(slot); + } + +protected: + ~CanvasItemDrawing() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Selection + Geom::Point _c; + double _delta = Geom::infinity(); + Inkscape::DrawingItem *_active_item = nullptr; + Inkscape::DrawingItem *_picked_item = nullptr; + + // Display + std::unique_ptr<Inkscape::Drawing> _drawing; + Geom::Affine _drawing_affine; + + // Events + bool _cursor = false; + bool _sticky = false; // Pick anything, even if hidden. + bool _pick_outline = false; + + // Signals + sigc::signal<bool (GdkEvent*, Inkscape::DrawingItem *)> _drawing_event_signal; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_DRAWING_H + +/* + 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 : diff --git a/src/display/control/canvas-item-enums.h b/src/display/control/canvas-item-enums.h new file mode 100644 index 0000000..ab173f3 --- /dev/null +++ b/src/display/control/canvas-item-enums.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * @file + * Enums for CanvasItems. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + */ + +#ifndef SEEN_CANVAS_ITEM_ENUMS_H +#define SEEN_CANVAS_ITEM_ENUMS_H + +namespace Inkscape { + +enum CanvasItemColor { + CANVAS_ITEM_PRIMARY, + CANVAS_ITEM_SECONDARY, + CANVAS_ITEM_TERTIARY +}; + +enum CanvasItemCtrlShape { + CANVAS_ITEM_CTRL_SHAPE_SQUARE, + CANVAS_ITEM_CTRL_SHAPE_DIAMOND, + CANVAS_ITEM_CTRL_SHAPE_CIRCLE, + CANVAS_ITEM_CTRL_SHAPE_TRIANGLE, + CANVAS_ITEM_CTRL_SHAPE_CROSS, + CANVAS_ITEM_CTRL_SHAPE_PLUS, + CANVAS_ITEM_CTRL_SHAPE_PIVOT, // Fancy "plus" + CANVAS_ITEM_CTRL_SHAPE_DARROW, // Double headed arrow. + CANVAS_ITEM_CTRL_SHAPE_SARROW, // Double headed arrow, rotated (skew). + CANVAS_ITEM_CTRL_SHAPE_CARROW, // Double headed curved arrow. + CANVAS_ITEM_CTRL_SHAPE_SALIGN, // Side alignment. + CANVAS_ITEM_CTRL_SHAPE_CALIGN, // Corner alignment. + CANVAS_ITEM_CTRL_SHAPE_MALIGN, // Center (middle) alignment. + CANVAS_ITEM_CTRL_SHAPE_BITMAP, + CANVAS_ITEM_CTRL_SHAPE_IMAGE, + CANVAS_ITEM_CTRL_SHAPE_LINE, + CANVAS_ITEM_CTRL_SHAPE_TRIANGLE_ANGLED, +}; + +// Applies to control points. +enum CanvasItemCtrlType { + CANVAS_ITEM_CTRL_TYPE_DEFAULT, + CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE, // Stretch & Scale + CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW, + CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE, + CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER, + CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN, + CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN, + CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN, + CANVAS_ITEM_CTRL_TYPE_ANCHOR, + CANVAS_ITEM_CTRL_TYPE_POINT, + CANVAS_ITEM_CTRL_TYPE_ROTATE, + CANVAS_ITEM_CTRL_TYPE_MARGIN, + CANVAS_ITEM_CTRL_TYPE_CENTER, + CANVAS_ITEM_CTRL_TYPE_SIZER, + CANVAS_ITEM_CTRL_TYPE_SHAPER, + CANVAS_ITEM_CTRL_TYPE_LPE, + CANVAS_ITEM_CTRL_TYPE_NODE_AUTO, + CANVAS_ITEM_CTRL_TYPE_NODE_CUSP, + CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH, + CANVAS_ITEM_CTRL_TYPE_NODE_SYMETRICAL, + CANVAS_ITEM_CTRL_TYPE_INVISIPOINT +}; + +enum CanvasItemCtrlMode { + CANVAS_ITEM_CTRL_MODE_COLOR, + CANVAS_ITEM_CTRL_MODE_XOR, + CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR, + CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_ENUMS_H + +/* + 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 : 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 : diff --git a/src/display/control/canvas-item-grid.h b/src/display/control/canvas-item-grid.h new file mode 100644 index 0000000..3e8bc28 --- /dev/null +++ b/src/display/control/canvas-item-grid.h @@ -0,0 +1,123 @@ +// 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. + */ +#ifndef SEEN_CANVAS_ITEM_GRID_H +#define SEEN_CANVAS_ITEM_GRID_H + +#include <cstdint> +#include <2geom/point.h> + +#include "canvas-item.h" +#include "preferences.h" + +uint32_t constexpr GRID_DEFAULT_MAJOR_COLOR = 0x0099e54d; +uint32_t constexpr GRID_DEFAULT_MINOR_COLOR = 0x0099e526; + +namespace Inkscape { + +class CanvasItemGrid : public CanvasItem +{ +public: + CanvasItemGrid(CanvasItemGroup *group); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_major_color(uint32_t color); + void set_minor_color(uint32_t color); + void set_origin(Geom::Point const &point); + void set_spacing(Geom::Point const &point); + void set_dotted(bool b); + void set_major_line_interval(int n); + void set_no_emp_when_zoomed_out(bool noemp); + +protected: + ~CanvasItemGrid() override = default; + + bool _dotted; + + Geom::Point _origin; + + Geom::Point _spacing; /**< Spacing between elements of the grid */ + + int _major_line_interval; + bool _no_emp_when_zoomed_out; + uint32_t _major_color; + uint32_t _minor_color; + +private: + std::unique_ptr<Preferences::PreferencesObserver> _pref_tracker; +}; + +/** Canvas Item for rectangular grids */ +class CanvasItemGridXY final : public CanvasItemGrid +{ +public: + CanvasItemGridXY(CanvasItemGroup *group); + +protected: + friend class GridSnapperXY; + + void _update(bool propagate) override; + void _render(CanvasItemBuffer &buf) const override; + + bool scaled[2]; /**< Whether the grid is in scaled mode, which can + be different in the X or Y direction, hence two + variables */ + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ + Geom::Point sw[2]; /**< Transformed spacing by the affine for the zoom */ +}; + +/** Canvas Item for axonometric grids */ +class CanvasItemGridAxonom final : public CanvasItemGrid +{ +public: + CanvasItemGridAxonom(CanvasItemGroup *group); + + // Properties + void set_angle_x(double value); + void set_angle_z(double value); + +protected: + friend class GridSnapperAxonom; + + void _update(bool propagate) override; + void _render(CanvasItemBuffer &buf) const override; + + bool scaled; /**< Whether the grid is in scaled mode */ + + double angle_deg[3]; /**< Angle of each axis (note that angle[2] == 0) */ + double angle_rad[3]; /**< Angle of each axis (note that angle[2] == 0) */ + double tan_angle[3]; /**< tan(angle[.]) */ + + double lyw = 1.0; /**< Transformed length y by the affine for the zoom */ + double lxw_x = 1.0; + double lxw_z = 1.0; + double spacing_ylines = 1.0; + + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_GRID_H + +/* + 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 : diff --git a/src/display/control/canvas-item-group.cpp b/src/display/control/canvas-item-group.cpp new file mode 100644 index 0000000..93193c3 --- /dev/null +++ b/src/display/control/canvas-item-group.cpp @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A CanvasItem that contains other CanvasItem's. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasGroup + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <boost/range/adaptor/reversed.hpp> +#include "canvas-item-group.h" + +constexpr bool DEBUG_LOGGING = false; + +namespace Inkscape { + +CanvasItemGroup::CanvasItemGroup(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemGroup"; + _pickable = true; // For now all groups are pickable... look into turning this off for some groups (e.g. temp). +} + +CanvasItemGroup::CanvasItemGroup(CanvasItemContext *context) + : CanvasItem(context) +{ + _name = "CanvasItemGroup:Root"; + _pickable = true; // see above +} + +CanvasItemGroup::~CanvasItemGroup() +{ + items.clear_and_dispose([] (auto c) { delete c; }); +} + +void CanvasItemGroup::_update(bool propagate) +{ + _bounds = {}; + + // Update all children and calculate new bounds. + for (auto &item : items) { + item.update(propagate); + _bounds |= item.get_bounds(); + } +} + +void CanvasItemGroup::_mark_net_invisible() +{ + if (!_net_visible) { + return; + } + _net_visible = false; + _need_update = false; + for (auto &item : items) { + item._mark_net_invisible(); + } + _bounds = {}; +} + +void CanvasItemGroup::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const +{ + for (auto &item : items) { + if (!item.is_visible()) continue; + item.visit_page_rects(f); + } +} + +void CanvasItemGroup::_render(Inkscape::CanvasItemBuffer &buf) const +{ + for (auto &item : items) { + item.render(buf); + } +} + +// Return last visible and pickable item that contains point. +// SPCanvasGroup returned distance but it was not used. +CanvasItem *CanvasItemGroup::pick_item(Geom::Point const &p) +{ + if constexpr (DEBUG_LOGGING) { + std::cout << "CanvasItemGroup::pick_item:" << std::endl; + std::cout << " PICKING: In group: " << _name << " bounds: " << _bounds << std::endl; + } + + for (auto &item : boost::adaptors::reverse(items)) { + if constexpr (DEBUG_LOGGING) std::cout << " PICKING: Checking: " << item.get_name() << " bounds: " << item.get_bounds() << std::endl; + + if (item.is_visible() && item.is_pickable() && item.contains(p)) { + if (auto group = dynamic_cast<CanvasItemGroup*>(&item)) { + if (auto ret = group->pick_item(p)) { + return ret; + } + } else { + return &item; + } + } + } + + return nullptr; +} + +} // 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 : diff --git a/src/display/control/canvas-item-group.h b/src/display/control/canvas-item-group.h new file mode 100644 index 0000000..4ab5884 --- /dev/null +++ b/src/display/control/canvas-item-group.h @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_GROUP_H +#define SEEN_CANVAS_ITEM_GROUP_H + +/** + * A CanvasItem that contains other CanvasItems. + */ +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasGroup + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemGroup final : public CanvasItem +{ +public: + CanvasItemGroup(CanvasItemGroup *group); + CanvasItemGroup(CanvasItemContext *context); + + // Geometry + void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override; + + // Selection + CanvasItem *pick_item(Geom::Point const &p); + +protected: + friend class CanvasItem; // access to items + friend class CanvasItemContext; // access to destructor + + ~CanvasItemGroup() override; + + void _update(bool propagate) override; + void _mark_net_invisible() override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + /** + * Type for linked list storing CanvasItems. + * Used to speed deletion when a group contains a large number of items (as in nodes for a complex path). + */ + using CanvasItemList = boost::intrusive::list< + Inkscape::CanvasItem, + boost::intrusive::member_hook<Inkscape::CanvasItem, boost::intrusive::list_member_hook<>, + &Inkscape::CanvasItem::member_hook>>; + + CanvasItemList items; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_H + +/* + 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 : diff --git a/src/display/control/canvas-item-guideline.cpp b/src/display/control/canvas-item-guideline.cpp new file mode 100644 index 0000000..7cefc2b --- /dev/null +++ b/src/display/control/canvas-item-guideline.cpp @@ -0,0 +1,312 @@ +// 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 <2geom/line.h> + +#include "canvas-item-guideline.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! + + // Control to move guide line. + _origin_ctrl = make_canvasitem<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. +} + +/** + * 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? + auto 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(bool) +{ + // Required when rotating canvas + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + // Queue redraw of new area (and old too). + request_redraw(); +} + +/** + * Render guideLine to screen via Cairo. + */ +void CanvasItemGuideLine::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // 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 + auto 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()); + + auto desktop = get_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 const &) { + // 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::set_visible(bool visible) +{ + CanvasItem::set_visible(visible); + _origin_ctrl->set_visible(visible); +} + +void CanvasItemGuideLine::set_stroke(uint32_t 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 &&label) +{ + defer([=, label = std::move(label)] () mutable { + if (_label == label) return; + _label = std::move(label); + request_update(); + }); +} + +void CanvasItemGuideLine::set_locked(bool locked) +{ + defer([=] { + if (_locked == locked) return; + _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; + } + defer([=] { + if (_width == size) return; + _width = size; + _height = size; + _built.reset(); + 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 : diff --git a/src/display/control/canvas-item-guideline.h b/src/display/control/canvas-item-guideline.h new file mode 100644 index 0000000..424fb27 --- /dev/null +++ b/src/display/control/canvas-item-guideline.h @@ -0,0 +1,104 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_GUIDELINE_H +#define SEEN_CANVAS_ITEM_GUIDELINE_H + +/** + * 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 <glibmm/ustring.h> + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" +#include "canvas-item-ctrl.h" +#include "canvas-item-ptr.h" + +namespace Inkscape { + +class CanvasItemGuideHandle; + +class CanvasItemGuideLine final : public CanvasItem +{ +public: + CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, Geom::Point const &origin, Geom::Point const &normal); + + // Geometry + void set_origin(Geom::Point const &origin); + void set_normal(Geom::Point const &normal); + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_visible(bool visible) override; + void set_stroke(uint32_t color) override; + void set_label(Glib::ustring &&label); + void set_locked(bool locked); + void set_inverted(bool inverted); + + // Getters + CanvasItemGuideHandle *dot() const; + +protected: + ~CanvasItemGuideLine() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _origin; + Geom::Point _normal = Geom::Point(0, 1); + Glib::ustring _label; + bool _locked = true; // Flipped in constructor to trigger init of _origin_ctrl. + bool _inverted = false; + CanvasItemPtr<CanvasItemGuideHandle> _origin_ctrl; + + static constexpr uint32_t CONTROL_LOCKED_COLOR = 0x00000080; // RGBA black semitranslucent + static constexpr double LABEL_SEP = 2.0; // Distance between the label and the origin control +}; + +// A handle ("dot") serving as draggable origin control +class CanvasItemGuideHandle final : public CanvasItemCtrl +{ +public: + CanvasItemGuideHandle(CanvasItemGroup *group, Geom::Point const &pos, CanvasItemGuideLine *line); + double radius() const; + void set_size_via_index(int index) override; + +protected: + ~CanvasItemGuideHandle() override = default; + + CanvasItemGuideLine *_my_line; // The guide line we belong to + + // static data + static constexpr double SCALE = 0.55; // handle size relative to an auto-smooth node + static constexpr unsigned MINIMUM_SIZE = 7; // smallest handle size, must be an odd int +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_GUIDELINE_H + +/* + 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 : diff --git a/src/display/control/canvas-item-ptr.h b/src/display/control/canvas-item-ptr.h new file mode 100644 index 0000000..b8c198c --- /dev/null +++ b/src/display/control/canvas-item-ptr.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_PTR_H +#define SEEN_CANVAS_ITEM_PTR_H + +/* + * An entirely analogous file to display/drawing-item-ptr.h. + */ + +#include <memory> +#include <type_traits> + +namespace Inkscape { class CanvasItem; } + +/// Deleter object which calls the unlink() method of CanvasItem. +struct CanvasItemUnlinkDeleter +{ + template <typename T> + void operator()(T *t) + { + static_assert(std::is_base_of_v<Inkscape::CanvasItem, T>); + t->unlink(); + } +}; + +/// Smart pointer used to hold CanvasItems, like std::unique_ptr. +template <typename T> +using CanvasItemPtr = std::unique_ptr<T, CanvasItemUnlinkDeleter>; + +/// Convienence function to create a CanvasItemPtr, like std::make_unique. +template <typename T, typename... Args> +auto make_canvasitem(Args&&... args) +{ + return CanvasItemPtr<T>(new T(std::forward<Args>(args)...)); +}; + +#endif // SEEN_CANVAS_ITEM_PTR_H diff --git a/src/display/control/canvas-item-quad.cpp b/src/display/control/canvas-item-quad.cpp new file mode 100644 index 0000000..06914de --- /dev/null +++ b/src/display/control/canvas-item-quad.cpp @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control quadrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlQuadr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cassert> + +#include "canvas-item-quad.h" + +#include "color.h" // SP_RGBA_x_F +#include "helper/geom.h" + +namespace Inkscape { + +/** + * Create an null control quad. + */ +CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemQuad:Null"; +} + +/** + * Create a control quad. Points are in document coordinates. + */ +CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group, + Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3) + : CanvasItem(group) + , _p0(p0) + , _p1(p1) + , _p2(p2) + , _p3(p3) +{ + _name = "CanvasItemQuad"; +} + +/** + * Set a control quad. Points are in document coordinates. + */ +void CanvasItemQuad::set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3) +{ + defer([=] { + _p0 = p0; + _p1 = p1; + _p2 = p2; + _p3 = p3; + request_update(); + }); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of quad. + */ +bool CanvasItemQuad::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemQuad::contains: Non-zero tolerance not implemented!" << std::endl; + } + + Geom::Point p0 = _p0 * affine(); + Geom::Point p1 = _p1 * affine(); + Geom::Point p2 = _p2 * affine(); + Geom::Point p3 = _p3 * affine(); + + // From 2geom rotated-rect.cpp + return + Geom::cross(p1 - p0, p - p0) >= 0 && + Geom::cross(p2 - p1, p - p1) >= 0 && + Geom::cross(p3 - p2, p - p2) >= 0 && + Geom::cross(p0 - p3, p - p3) >= 0; +} + +/** + * Update and redraw control quad. + */ +void CanvasItemQuad::_update(bool) +{ + if (_p0 == _p1 || _p1 == _p2 || _p2 == _p3 || _p3 == _p0) { + _bounds = {}; + return; // Not quad or not initialized. + } + + // Queue redraw of old area (erase previous content). + request_redraw(); // This is actually never useful as quads are always deleted + // and recreated when a node is moved! But keep it in case we change that. + + _bounds = expandedBy(bounds_of(_p0, _p1, _p2, _p3) * affine(), 2); // Room for anti-aliasing effects. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render quad to screen via Cairo. + */ +void CanvasItemQuad::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Document to canvas + Geom::Point p0 = _p0 * affine(); + Geom::Point p1 = _p1 * affine(); + Geom::Point p2 = _p2 * affine(); + Geom::Point p3 = _p3 * affine(); + + // Canvas to screen + p0 *= Geom::Translate(-buf.rect.min()); + p1 *= Geom::Translate(-buf.rect.min()); + p2 *= Geom::Translate(-buf.rect.min()); + p3 *= Geom::Translate(-buf.rect.min()); + + buf.cr->save(); + + buf.cr->begin_new_path(); + + buf.cr->move_to(p0.x(), p0.y()); + buf.cr->line_to(p1.x(), p1.y()); + buf.cr->line_to(p2.x(), p2.y()); + buf.cr->line_to(p3.x(), p3.y()); + buf.cr->close_path(); + + if (_inverted) { + cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->fill_preserve(); + + buf.cr->set_line_width(1); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->stroke_preserve(); + buf.cr->begin_new_path(); + + buf.cr->restore(); +} + +void CanvasItemQuad::set_inverted(bool inverted) +{ + defer([=] { + if (_inverted == inverted) return; + _inverted = inverted; + request_redraw(); + }); +} + +} // 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 : diff --git a/src/display/control/canvas-item-quad.h b/src/display/control/canvas-item-quad.h new file mode 100644 index 0000000..deb8e46 --- /dev/null +++ b/src/display/control/canvas-item-quad.h @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_QUAD_H +#define SEEN_CANVAS_ITEM_QUAD_H + +/** + * A class to represent a control quadrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlQuadr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemQuad final : public CanvasItem +{ +public: + CanvasItemQuad(CanvasItemGroup *group); + CanvasItemQuad(CanvasItemGroup *group, Geom::Point const &p0, Geom::Point const &p1, + Geom::Point const &p2, Geom::Point const &p3); + + // Geometry + void set_coords(Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + void set_inverted(bool inverted); + +protected: + ~CanvasItemQuad() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _p0; + Geom::Point _p1; + Geom::Point _p2; + Geom::Point _p3; + + bool _inverted = false; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_QUAD_H + +/* + 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 : diff --git a/src/display/control/canvas-item-rect.cpp b/src/display/control/canvas-item-rect.cpp new file mode 100644 index 0000000..0204cdf --- /dev/null +++ b/src/display/control/canvas-item-rect.cpp @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control rectangle. Used for rubberband selector, page outline, etc. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of CtrlRect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairo/cairo.h> + +#include "canvas-item-rect.h" + +#include "display/cairo-utils.h" +#include "color.h" // SP_RGBA_x_F +#include "helper/geom.h" +#include "inkscape.h" +#include "ui/util.h" +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control rect. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemRect:Null"; +} + +/** + * Create a control rect. Point are in document coordinates. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect) + : CanvasItem(group) + , _rect(rect) +{ + _name = "CanvasItemRect"; +} + +/** + * Set a control rect. Points are in document coordinates. + */ +void CanvasItemRect::set_rect(Geom::Rect const &rect) +{ + defer([=] { + if (_rect == rect) return; + _rect = rect; + request_update(); + }); +} + +/** + * Run a callback for each rectangle that should be filled and painted in the background. + */ +void CanvasItemRect::visit_page_rects(std::function<void(Geom::Rect const &)> const &f) const +{ + if (_is_page && _fill != 0) { + f(_rect); + } +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rect. + * Non-zero tolerance not implemented! Is valid for a rotated canvas. + */ +bool CanvasItemRect::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance != 0) { + std::cerr << "CanvasItemRect::contains: Non-zero tolerance not implemented!" << std::endl; + } + + return _rect.contains(p * affine().inverse()); +} + +/** + * Update and redraw control rect. + */ +void CanvasItemRect::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Enlarge bbox by twice shadow size (to allow for shadow on any side with a 45deg rotation). + _bounds = _rect; + // note: add shadow size before applying transformation, since get_shadow_size accounts for scale + if (_shadow_width > 0 && !_dashed) { + _bounds->expandBy(2 * get_shadow_size()); + } + *_bounds *= affine(); + _bounds->expandBy(2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render rect to screen via Cairo. + */ +void CanvasItemRect::_render(Inkscape::CanvasItemBuffer &buf) const +{ + // Are we axis aligned? + auto const &aff = affine(); + bool const axis_aligned = (Geom::are_near(aff[1], 0) && Geom::are_near(aff[2], 0)) + || (Geom::are_near(aff[0], 0) && Geom::are_near(aff[3], 0)); + + // If so, then snap the rectangle to the pixel grid. + auto rect = _rect; + if (axis_aligned) { + rect = (floor(_rect * aff) + Geom::Point(0.5, 0.5)) * aff.inverse(); + } + + buf.cr->save(); + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + + if (_inverted) { + cairo_set_operator(buf.cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. Canvas draws page shadows in OpenGL mode. + if (_shadow_width > 0 && !_dashed && !(_is_page && get_canvas()->get_opengl_enabled())) { + // There's only one UI knob to adjust border and shadow color, so instead of using border color + // transparency as is, it is boosted by this function, since shadow attenuates it. + auto const alpha = (std::exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (std::exp(-3) - 1); + + // Flip shadow upside-down if y-axis is inverted. + auto doc2dt = Geom::identity(); + if (auto desktop = get_canvas()->get_desktop()) { + doc2dt = desktop->doc2dt(); + } + + buf.cr->save(); + buf.cr->transform(geom_to_cairo(doc2dt * aff)); + ink_cairo_draw_drop_shadow(buf.cr, rect * doc2dt, get_shadow_size(), _shadow_color, alpha); + buf.cr->restore(); + } + + // Get the points we need transformed into window coordinates. + buf.cr->begin_new_path(); + for (int i = 0; i < 4; ++i) { + auto pt = rect.corner(i) * aff; + buf.cr->line_to(pt.x(), pt.y()); + } + buf.cr->close_path(); + + // Draw border. + static std::valarray<double> dashes = {4.0, 4.0}; + if (_dashed) { + buf.cr->set_dash(dashes, -0.5); + } + buf.cr->set_line_width(1); + // we maybe have painted the background, back to "normal" compositing + buf.cr->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + buf.cr->stroke_preserve(); + + // Highlight the border by drawing it in _shadow_color. + if (_shadow_width == 1 && _dashed) { + buf.cr->set_dash(dashes, 3.5); // Dash offset by dash length. + buf.cr->set_source_rgba(SP_RGBA32_R_F(_shadow_color), SP_RGBA32_G_F(_shadow_color), + SP_RGBA32_B_F(_shadow_color), SP_RGBA32_A_F(_shadow_color)); + buf.cr->stroke_preserve(); + } + + buf.cr->begin_new_path(); // Clear path or get weird artifacts. + + // Uncomment to show bounds + // Geom::Rect bounds = _bounds; + // bounds.expandBy(-1); + // bounds -= buf.rect.min(); + // buf.cr->set_source_rgba(1.0, 0.0, _shadow_width / 3.0, 1.0); + // buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + // buf.cr->stroke(); + + buf.cr->restore(); +} + +void CanvasItemRect::set_is_page(bool is_page) +{ + defer([=] { + if (_is_page == is_page) return; + _is_page = is_page; + request_redraw(); + }); +} + +void CanvasItemRect::set_fill(uint32_t fill) +{ + if (fill != _fill && _is_page) get_canvas()->set_page(fill); + CanvasItem::set_fill(fill); +} + +void CanvasItemRect::set_dashed(bool dashed) +{ + defer([=] { + if (_dashed == dashed) return; + _dashed = dashed; + request_redraw(); + }); +} + +void CanvasItemRect::set_inverted(bool inverted) +{ + defer([=] { + if (_inverted == inverted) return; + _inverted = inverted; + request_redraw(); + }); +} + +void CanvasItemRect::set_shadow(uint32_t color, int width) +{ + defer([=] { + if (_shadow_color == color && _shadow_width == width) return; + _shadow_color = color; + _shadow_width = width; + request_redraw(); + if (_is_page) get_canvas()->set_border(_shadow_width > 0 ? color : 0x0); + }); +} + +double CanvasItemRect::get_shadow_size() const +{ + // gradient drop shadow needs much more room than solid one, so inflating the size; + // fudge factor of 6 used to make sizes baked in svg documents work as steps: + // typical value of 2 will work out to 12 pixels which is a narrow shadow (b/c of exponential fall of) + auto size = _shadow_width * 6; + if (size < 0) { + size = 0; + } else if (size > 120) { + // arbitrarily selected max size, so Cairo gradient doesn't blow up if document has bogus shadow values + size = 120; + } + auto scale = affine().descrim(); + + // calculate space for gradient shadow; if divided by 'scale' it would be zoom independent (fixed in size); + // if 'scale' is not used, drop shadow will be getting smaller with document zoom; + // here hybrid approach is used: "unscaling" with square root of scale allows shadows to diminish + // more slowly at small zoom levels (so it's still perceptible) and grow more slowly at high mag (where it doesn't matter, b/c it's typically off-screen) + return size / (scale > 0 ? sqrt(scale) : 1); +} + +} // 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 : diff --git a/src/display/control/canvas-item-rect.h b/src/display/control/canvas-item-rect.h new file mode 100644 index 0000000..4f67734 --- /dev/null +++ b/src/display/control/canvas-item-rect.h @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_RECT_H +#define SEEN_CANVAS_ITEM_RECT_H + +/** + * A class to represent a control rectangle. Used for rubberband selector, page outline, etc. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of CtrlRect + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <memory> +#include <2geom/path.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemRect final : public CanvasItem +{ +public: + CanvasItemRect(CanvasItemGroup *group); + CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect); + + // Geometry + void set_rect(Geom::Rect const &rect); + void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const override; + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_is_page(bool is_page); + void set_fill(uint32_t color) override; + void set_dashed(bool dash = true); + void set_inverted(bool inverted = false); + void set_shadow(uint32_t color, int width); + +protected: + ~CanvasItemRect() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + // Geometry + double get_shadow_size() const; + + Geom::Rect _rect; + bool _is_page = false; + bool _dashed = false; + bool _inverted = false; + int _shadow_width = 0; + uint32_t _shadow_color = 0x0; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_RECT_H + +/* + 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 : diff --git a/src/display/control/canvas-item-text.cpp b/src/display/control/canvas-item-text.cpp new file mode 100644 index 0000000..5049543 --- /dev/null +++ b/src/display/control/canvas-item-text.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control textrilateral. Used to highlight selected text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCtrlTextr + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-text.h" + +#include <cmath> +#include <utility> // std::move +#include <glibmm/i18n.h> + +#include "color.h" // SP_RGBA_x_F + +#include "ui/util.h" + +namespace Inkscape { + +/** + * Create a null control text. + */ +CanvasItemText::CanvasItemText(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemText"; + _fill = 0x33337fff; // Override CanvasItem default. +} + +/** + * Create a control text. Point are in document coordinates. + */ +CanvasItemText::CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled) + : CanvasItem(group) + , _p(p) + , _text(std::move(text)) + , _scaled(scaled) +{ + _name = "CanvasItemText"; + _fill = 0x33337fff; // Override CanvasItem default. + + request_update(); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_coord(Geom::Point const &p) +{ + defer([=] { + if (_p == p) return; + _p = p; + request_update(); + }); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_bg_radius(double rad) +{ + defer([=] { + if (_bg_rad == rad) return; + _bg_rad = rad; + request_update(); + }); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of text. + */ +bool CanvasItemText::contains(Geom::Point const &p, double tolerance) +{ + return false; // We never select text. +} + +/** + * Update and redraw control text. + */ +void CanvasItemText::_update(bool) +{ + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Point needs to be scaled manually if not cairo scaling + Geom::Point p = _scaled ? _p : _p * affine(); + + // Measure text size + _text_box = load_text_extents(); + + // Offset relative to requested point + double offset_x = -(_anchor_position.x() * _text_box.width()); + double offset_y = -(_anchor_position.y() * _text_box.height()); + offset_x += p.x() + _adjust_offset.x(); + offset_y += p.y() + _adjust_offset.y(); + _text_box *= Geom::Translate(Geom::Point(offset_x, offset_y).floor()); + + // Pixel alignment of background. Avoid aliasing artifacts on redraw. + _text_box = _text_box.roundOutwards(); + + // Don't apply affine here, to keep text at the same size in screen coords. + _bounds = _text_box; + if (_scaled && _bounds) { + *_bounds *= affine(); + _bounds = _bounds->roundOutwards(); + } + + // Queue redraw of new area + request_redraw(); +} + +/** + * Render text to screen via Cairo. + */ +void CanvasItemText::_render(Inkscape::CanvasItemBuffer &buf) const +{ + buf.cr->save(); + + // Screen to desktop coords. + buf.cr->translate(-buf.rect.left(), -buf.rect.top()); + + if (_scaled) { + // Convert from canvas space to document space + buf.cr->transform(geom_to_cairo(affine())); + } + + double x = _text_box.min().x(); + double y = _text_box.min().y(); + double w = _text_box.width(); + double h = _text_box.height(); + + // Background + if (_use_background) { + if (_bg_rad == 0.0) { + buf.cr->rectangle(x, y, w, h); + } else { + double radius = _bg_rad * (std::min(w ,h) / 2); + buf.cr->arc(x + w - radius, y + radius, radius, -M_PI_2, 0); + buf.cr->arc(x + w - radius, y + h - radius, radius, 0, M_PI_2); + buf.cr->arc(x + radius, y + h - radius, radius, M_PI_2, M_PI); + buf.cr->arc(x + radius, y + radius, radius, M_PI, 3*M_PI_2); + } + buf.cr->set_line_width(2); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_background), SP_RGBA32_G_F(_background), + SP_RGBA32_B_F(_background), SP_RGBA32_A_F(_background)); + buf.cr->fill(); + } + + // Center the text inside the draw background box + auto bx = x + w / 2.0; + auto by = y + h / 2.0 + 1; + buf.cr->move_to(int(bx - _text_size.x_bearing - _text_size.width/2.0), + int(by - _text_size.y_bearing - _text_extent.height/2.0)); + + buf.cr->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL); + buf.cr->set_font_size(_fontsize); + buf.cr->text_path(_text); + buf.cr->set_source_rgba(SP_RGBA32_R_F(_fill), SP_RGBA32_G_F(_fill), + SP_RGBA32_B_F(_fill), SP_RGBA32_A_F(_fill)); + buf.cr->fill(); + buf.cr->restore(); +} + +void CanvasItemText::set_text(Glib::ustring text) +{ + defer([=, text = std::move(text)] () mutable { + if (_text == text) return; + _text = std::move(text); + request_update(); // Might be larger than before! + }); +} + +void CanvasItemText::set_fontsize(double fontsize) +{ + defer([=] { + if (_fontsize == fontsize) return; + _fontsize = fontsize; + request_update(); // Might be larger than before! + }); +} + +/** + * Load the sizes of the text extent using the given font. + */ +Geom::Rect CanvasItemText::load_text_extents() +{ + auto surface = Cairo::ImageSurface::create(Cairo::FORMAT_ARGB32, 1, 1); + auto context = Cairo::Context::create(surface); + context->select_font_face(_fontname, Cairo::FONT_SLANT_NORMAL, Cairo::FONT_WEIGHT_NORMAL); + context->set_font_size(_fontsize); + context->get_text_extents(_text, _text_size); + + if (_fixed_line) { + // TRANSLATORS: This is a set of letters to test for font ascender and descenders. + context->get_text_extents(_("lg1p$"), _text_extent); + } else { + _text_extent = _text_size; + } + + return Geom::Rect::from_xywh(0, 0, + _text_size.x_advance + _border * 2, + _text_extent.height + _border * 2); +} + +void CanvasItemText::set_background(uint32_t background) +{ + defer([=] { + if (_background != background) { + _background = background; + request_redraw(); + } + _use_background = true; + }); +} + +/** + * Set the anchor point, x and y between 0.0 and 1.0. + */ +void CanvasItemText::set_anchor(Geom::Point const &anchor_pt) +{ + defer([=] { + if (_anchor_position == anchor_pt) return; + _anchor_position = anchor_pt; + request_update(); + }); +} + +void CanvasItemText::set_adjust(Geom::Point const &adjust_pt) +{ + defer([=] { + if (_adjust_offset == adjust_pt) return; + _adjust_offset = adjust_pt; + request_update(); + }); +} + +void CanvasItemText::set_fixed_line(bool fixed_line) +{ + defer([=] { + if (_fixed_line == fixed_line) return; + _fixed_line = fixed_line; + request_update(); + }); +} + +void CanvasItemText::set_border(double border) +{ + defer([=] { + if (_border == border) return; + _border = border; + request_update(); + }); +} + +} // namespace Inkscape + +/* FROM: http://lists.cairographics.org/archives/cairo-bugs/2009-March/003014.html + - Glyph surfaces: In most font rendering systems, glyph surfaces + have an origin at (0,0) and a bounding box that is typically + represented as (x_bearing,y_bearing,width,height). Depending on + which way y progresses in the system, y_bearing may typically be + negative (for systems similar to cairo, with origin at top left), + or be positive (in systems like PDF with origin at bottom left). + No matter which is the case, it is important to note that + (x_bearing,y_bearing) is the coordinates of top-left of the glyph + relative to the glyph origin. That is, for example: + + Scaled-glyph space: + + (x_bearing,y_bearing) <-- negative numbers + +----------------+ + | . | + | . | + |......(0,0) <---|-- glyph origin + | | + | | + +----------------+ + (width+x_bearing,height+y_bearing) + + Note the similarity of the origin to the device space. That is + exactly how we use the device_offset to represent scaled glyphs: + to use the device-space origin as the glyph origin. +*/ + +/* + 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 : diff --git a/src/display/control/canvas-item-text.h b/src/display/control/canvas-item-text.h new file mode 100644 index 0000000..a3ad4f4 --- /dev/null +++ b/src/display/control/canvas-item-text.h @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_TEXT_H +#define SEEN_CANVAS_ITEM_TEXT_H + +/** + * A class to represent on-screen text. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasText. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include <glibmm/ustring.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemText final : public CanvasItem +{ +public: + CanvasItemText(CanvasItemGroup *group); + CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text, bool scaled = false); + + // Geometry + void set_coord(Geom::Point const &p); + void set_bg_radius(double rad); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Properties + void set_text(Glib::ustring text); + void set_fontsize(double fontsize); + void set_border(double border); + void set_background(uint32_t background); + void set_anchor(Geom::Point const &anchor_pt); + void set_adjust(Geom::Point const &adjust_pt); + void set_fixed_line(bool fixed_line); + +protected: + ~CanvasItemText() override = default; + + void _update(bool propagate) override; + void _render(Inkscape::CanvasItemBuffer &buf) const override; + + Geom::Point _p; // Position of text (not box around text). + Cairo::TextExtents _text_extent; + Cairo::TextExtents _text_size; + Geom::Point _anchor_position; + Geom::Point _adjust_offset; + Geom::Rect _text_box; + Glib::ustring _text; + std::string _fontname = "sans-serif"; + double _fontsize = 10; + double _border = 3; + double _bg_rad = 0; + uint32_t _background = 0x0000007f; + bool _use_background = false; + bool _fixed_line = false; // Correction for font heights + bool _scaled = false; + + Geom::Rect load_text_extents(); +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_TEXT_H + +/* + 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 : diff --git a/src/display/control/canvas-item.cpp b/src/display/control/canvas-item.cpp new file mode 100644 index 0000000..984d24d --- /dev/null +++ b/src/display/control/canvas-item.cpp @@ -0,0 +1,334 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * Abstract base class for on-canvas control items. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasItem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item.h" +#include "canvas-item-group.h" +#include "canvas-item-ctrl.h" + +#include "ui/widget/canvas.h" + +constexpr bool DEBUG_LOGGING = false; +constexpr bool DEBUG_BOUNDS = false; + +namespace Inkscape { + +CanvasItem::CanvasItem(CanvasItemContext *context) + : _context(context) + , _parent(nullptr) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: create root " << get_name() << std::endl; + request_update(); +} + +CanvasItem::CanvasItem(CanvasItemGroup *parent) + : _context(parent->_context) + , _parent(parent) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: add " << get_name() << " to " << parent->get_name() << " " << parent->items.size() << std::endl; + defer([=] { + parent->items.push_back(*this); + request_update(); + }); +} + +void CanvasItem::unlink() +{ + defer([=] { + // Clear canvas of item. + request_redraw(); + + // Remove from parent. + if (_parent) { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: remove " << get_name() << " from " << _parent->get_name() << " " << _parent->items.size() << std::endl; + auto it = _parent->items.iterator_to(*this); + assert(it != _parent->items.end()); + _parent->items.erase(it); + _parent->request_update(); + } else { + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem: destroy root " << get_name() << std::endl; + } + + delete this; + }); +} + +CanvasItem::~CanvasItem() +{ + // Clear any pointers to this object in canvas. + get_canvas()->canvas_item_destructed(this); +} + +bool CanvasItem::is_descendant_of(CanvasItem const *ancestor) const +{ + auto item = this; + while (item) { + if (item == ancestor) { + return true; + } + item = item->_parent; + } + return false; +} + +void CanvasItem::set_z_position(int zpos) +{ + if (!_parent) { + std::cerr << "CanvasItem::set_z_position: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + + if (zpos <= 0) { + _parent->items.push_front(*this); + } else if (zpos >= _parent->items.size() - 1) { + _parent->items.push_back(*this); + } else { + auto it = _parent->items.begin(); + std::advance(it, zpos); + _parent->items.insert(it, *this); + } + }); +} + +void CanvasItem::raise_to_top() +{ + if (!_parent) { + std::cerr << "CanvasItem::raise_to_top: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_back(*this); + }); +} + +void CanvasItem::lower_to_bottom() +{ + if (!_parent) { + std::cerr << "CanvasItem::lower_to_bottom: No parent!" << std::endl; + return; + } + + defer([=] { + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_front(*this); + }); +} + +// Indicate geometry changed and bounds needs recalculating. +void CanvasItem::request_update() +{ + if (_need_update || !_visible) { + return; + } + + _need_update = true; + + if (_parent) { + _parent->request_update(); + } else { + get_canvas()->request_update(); + } +} + +void CanvasItem::update(bool propagate) +{ + if (!_visible) { + _mark_net_invisible(); + return; + } + + bool reappearing = !_net_visible; + _net_visible = true; + + if (!_need_update && !reappearing && !propagate) { + return; + } + + _need_update = false; + + // Get new bounds + _update(propagate); + + if (reappearing) { + request_redraw(); + } +} + +void CanvasItem::_mark_net_invisible() +{ + if (!_net_visible) { + return; + } + _net_visible = false; + _need_update = false; + request_redraw(); + _bounds = {}; +} + +// Grab all events! +void CanvasItem::grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const &cursor) +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::grab: " << _name << std::endl; + + auto canvas = get_canvas(); + + // Don't grab if we already have a grabbed item! + if (canvas->get_grabbed_canvas_item()) { + return; + } + + gtk_grab_add(GTK_WIDGET(canvas->gobj())); + + canvas->set_grabbed_canvas_item(this, event_mask); + canvas->set_current_canvas_item(this); // So that all events go to grabbed item. +} + +void CanvasItem::ungrab() +{ + if constexpr (DEBUG_LOGGING) std::cout << "CanvasItem::ungrab: " << _name << std::endl; + + auto canvas = get_canvas(); + + if (canvas->get_grabbed_canvas_item() != this) { + return; // Sanity check + } + + canvas->set_grabbed_canvas_item(nullptr, (Gdk::EventMask)0); // Zero mask + + gtk_grab_remove(GTK_WIDGET(canvas->gobj())); +} + +void CanvasItem::render(CanvasItemBuffer &buf) const +{ + if (_visible && _bounds && _bounds->interiorIntersects(buf.rect)) { + _render(buf); + if constexpr (DEBUG_BOUNDS) { + auto bounds = *_bounds; + bounds.expandBy(-1); + bounds -= buf.rect.min(); + buf.cr->set_source_rgba(1.0, 0.0, 0.0, 1.0); + buf.cr->rectangle(bounds.min().x(), bounds.min().y(), bounds.width(), bounds.height()); + buf.cr->stroke(); + } + } +} + +/* + * The main invariant of the invisibility system is + * + * x needs update and is visible ==> parent(x) needs update or is invisible + * + * When x belongs to the visible subtree, meaning it and all its parents are visible, + * this condition reduces to + * + * x needs update ==> parent(x) needs update + * + * Thus within the visible subtree, the subset of nodes that need updating forms a subtree. + * + * In the update() function, we only walk this latter subtree. + */ + +void CanvasItem::set_visible(bool visible) +{ + defer([=] { + if (_visible == visible) return; + if (_visible) { + request_update(); + _visible = false; + } else { + _visible = true; + _need_update = false; + request_update(); + } + }); +} + +void CanvasItem::request_redraw() +{ + // Queue redraw request + if (_bounds) { + get_canvas()->redraw_area(*_bounds); + } +} + +void CanvasItem::set_fill(uint32_t fill) +{ + defer([=] { + if (_fill == fill) return; + _fill = fill; + request_redraw(); + }); +} + +void CanvasItem::set_stroke(uint32_t stroke) +{ + defer([=] { + if (_stroke == stroke) return; + _stroke = stroke; + request_redraw(); + }); +} + +void CanvasItem::update_canvas_item_ctrl_sizes(int size_index) +{ + if (auto ctrl = dynamic_cast<CanvasItemCtrl*>(this)) { + // We can't use set_size_default as the preference file is updated ->after<- the signal is emitted! + ctrl->set_size_via_index(size_index); + } else if (auto group = dynamic_cast<CanvasItemGroup*>(this)) { + for (auto &item : group->items) { + item.update_canvas_item_ctrl_sizes(size_index); + } + } +} + +void CanvasItem::canvas_item_print_tree(int level, int zorder) const +{ + if (level == 0) { + std::cout << "Canvas Item Tree" << std::endl; + } + + std::cout << "CC: "; + for (int i = 0; i < level; ++i) { + std::cout << " "; + } + + std::cout << zorder << ": " << _name << std::endl; + + if (auto group = dynamic_cast<Inkscape::CanvasItemGroup const*>(this)) { + int i = 0; + for (auto &item : group->items) { + item.canvas_item_print_tree(level + 1, i); + i++; + } + } +} + +} // 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 : diff --git a/src/display/control/canvas-item.h b/src/display/control/canvas-item.h new file mode 100644 index 0000000..fc34176 --- /dev/null +++ b/src/display/control/canvas-item.h @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_H +#define SEEN_CANVAS_ITEM_H + +/** + * Abstract base class for on-canvas control items. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasItem + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + * + * A note about coordinates: + * + * 1. Canvas items are constructed using document (SVG) coordinates. + * 2. Calculations are made in canvas units, which is equivalent of SVG units multiplied by zoom factor. + * This is true for bounds and closest distance calculations. + * 3 Drawing is done in screen units which is the same as canvas units but translated. + * The document and canvas origins overlap. + * The affine contains only scaling and rotating components. + */ + +#include <cstdint> +#include <boost/intrusive/list.hpp> +#include <2geom/rect.h> +#include <sigc++/sigc++.h> + +#include <gdkmm/device.h> // Gdk::EventMask +#include <gdk/gdk.h> // GdkEvent + +#include "canvas-item-enums.h" +#include "canvas-item-buffer.h" +#include "canvas-item-context.h" + +class SPItem; + +namespace Inkscape { + +inline constexpr uint32_t CANVAS_ITEM_COLORS[] = { 0x0000ff7f, 0xff00007f, 0xffff007f }; + +namespace UI::Widget { class Canvas; } +class CanvasItemGroup; + +class CanvasItem +{ +public: + CanvasItem(CanvasItemContext *context); + CanvasItem(CanvasItemGroup *parent); + CanvasItem(CanvasItem const &) = delete; + CanvasItem &operator=(CanvasItem const &) = delete; + void unlink(); + + // Structure + UI::Widget::Canvas *get_canvas() const { return _context->canvas(); } + CanvasItemGroup *get_parent() const { return _parent; } + bool is_descendant_of(CanvasItem const *ancestor) const; + + // Z Position + void set_z_position(int zpos); + void raise_to_top(); // Move to top of group (last entry). + void lower_to_bottom(); // Move to bottom of group (first entry). + + // Geometry + void request_update(); + void update(bool propagate); + virtual void visit_page_rects(std::function<void(Geom::Rect const &)> const &) const {} + Geom::OptRect const &get_bounds() const { return _bounds; } + + // Selection + virtual bool contains(Geom::Point const &p, double tolerance = 0) { return _bounds && _bounds->interiorContains(p); } + void grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> const & = {}); + void ungrab(); + + // Display + void render(Inkscape::CanvasItemBuffer &buf) const; + bool is_visible() const { return _visible; } + virtual void set_visible(bool visible); + void show() { set_visible(true); } + void hide() { set_visible(false); } + void request_redraw(); // queue redraw request + + // Properties + virtual void set_fill(uint32_t rgba); + void set_fill(CanvasItemColor color) { set_fill(CANVAS_ITEM_COLORS[color]); } + virtual void set_stroke(uint32_t rgba); + void set_stroke(CanvasItemColor color) { set_stroke(CANVAS_ITEM_COLORS[color]); } + void set_name(std::string &&name) { _name = std::move(name); } + std::string const &get_name() const { return _name; } + void update_canvas_item_ctrl_sizes(int size_index); + + // Events + void set_pickable(bool pickable) { _pickable = pickable; } + bool is_pickable() const { return _pickable; } + sigc::connection connect_event(sigc::slot<bool(GdkEvent*)> const &slot) { + return _event_signal.connect(slot); + } + virtual bool handle_event(GdkEvent *event) { + return _event_signal.emit(event); // Default just emits event. + } + + // Recursively print CanvasItem tree. + void canvas_item_print_tree(int level = 0, int zorder = 0) const; + + // Boost linked list member hook, speeds deletion. + boost::intrusive::list_member_hook<> member_hook; + +protected: + friend class CanvasItemGroup; + + virtual ~CanvasItem(); + + // Structure + CanvasItemContext *_context; + CanvasItemGroup *_parent; + + // Geometry + Geom::OptRect _bounds; + bool _need_update = false; + Geom::Affine const &affine() const { return _context->affine(); } + virtual void _update(bool propagate) = 0; + virtual void _mark_net_invisible(); + + // Display + bool _visible = true; + bool _net_visible = true; + virtual void _render(Inkscape::CanvasItemBuffer &buf) const = 0; + + // Selection + bool _pickable = false; // Most items are just for display and are not pickable! + + // Properties + uint32_t _fill = CANVAS_ITEM_COLORS[CANVAS_ITEM_SECONDARY]; + uint32_t _stroke = CANVAS_ITEM_COLORS[CANVAS_ITEM_PRIMARY]; + std::string _name; // For debugging + + // Events + sigc::signal<bool (GdkEvent*)> _event_signal; + + // Snapshotting + template<typename F> + void defer(F &&f) { _context->defer(std::forward<F>(f)); } +}; + +} // namespace Inkscape + +// Todo: Move to lib2geom. +inline auto &operator<<(std::ostream &s, Geom::OptRect const &rect) +{ + return rect ? (s << *rect) : (s << "(empty)"); +} + +#endif // SEEN_CANVAS_ITEM_H + +/* + 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 : diff --git a/src/display/control/canvas-page.cpp b/src/display/control/canvas-page.cpp new file mode 100644 index 0000000..5fa337d --- /dev/null +++ b/src/display/control/canvas-page.cpp @@ -0,0 +1,282 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Inkscape pages implementation + * + * Authors: + * Martin Owens <doctormo@geek-2.com> + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-page.h" +#include "canvas-item-rect.h" +#include "canvas-item-text.h" +#include "color.h" + +namespace Inkscape { + +CanvasPage::CanvasPage() = default; + +CanvasPage::~CanvasPage() = default; + +/** + * Add the page canvas to the given canvas item groups (canvas view is implicit) + */ +void CanvasPage::add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *border_group) +{ + // Foreground 'border' + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("foreground"); + item->set_is_page(true); + canvas_items.emplace_back(item); + } + + // Background rectangle 'fill' + if (auto item = new CanvasItemRect(background_group, size)) { + item->set_name("background"); + item->set_is_page(true); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(0x00000000); + canvas_items.emplace_back(item); + } + + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("margin"); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(_margin_color); + canvas_items.emplace_back(item); + } + + if (auto item = new CanvasItemRect(border_group, size)) { + item->set_name("bleed"); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(_bleed_color); + canvas_items.emplace_back(item); + } + + if (auto label = new CanvasItemText(border_group, Geom::Point(0, 0), "{Page Label}")) { + label->set_fixed_line(false); + canvas_items.emplace_back(label); + } +} +/** + * Hide the page in the given canvas widget. + */ +void CanvasPage::remove(UI::Widget::Canvas *canvas) +{ + g_assert(canvas != nullptr); + for (auto it = canvas_items.begin(); it != canvas_items.end();) { + if (canvas == (*it)->get_canvas()) { + it = canvas_items.erase(it); + } else { + ++it; + } + } +} + +void CanvasPage::show() +{ + for (auto &item : canvas_items) { + item->show(); + } +} + +void CanvasPage::hide() +{ + for (auto &item : canvas_items) { + item->hide(); + } +} + +void CanvasPage::set_guides_visible(bool show) { + for (auto& item: canvas_items) { + if (item->get_name() == "margin" || item->get_name() == "bleed") { + item->set_visible(show); + } + } +} + +/** + * Update the visual representation of a page on screen. + * + * @param size - The size of the page in desktop units + * @param txt - An optional label for the page + * @param outline - Disable normal rendering and show as an outline. + */ +void CanvasPage::update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline) +{ + // Put these in the preferences? + bool border_on_top = _border_on_top; + guint32 shadow_color = _border_color; // there's no separate shadow color in the UI, border color is used + guint32 select_color = 0x000000cc; + guint32 border_color = _border_color; + guint32 margin_color = _margin_color; + guint32 bleed_color = _bleed_color; + + // This is used when showing the viewport as *not a page* it's mostly + // never used as the first page is normally the viewport too. + if (outline) { + border_on_top = false; + _shadow_size = 0; + border_color = select_color; + } + + for (auto &item : canvas_items) { + if (auto rect = dynamic_cast<CanvasItemRect *>(item.get())) { + if (rect->get_name() == "margin") { + rect->set_stroke(margin_color); + bool vis = margin && *margin != size; + rect->set_visible(vis); + if (vis) { + rect->set_rect(*margin); + } + continue; + } + if (rect->get_name() == "bleed") { + rect->set_stroke(bleed_color); + bool vis = bleed && *bleed != size; + rect->set_visible(vis); + if (vis) { + rect->set_rect(*bleed); + } + continue; + } + + rect->set_rect(size); + + bool is_foreground = (rect->get_name() == "foreground"); + // This will put the border on the background OR foreground layer as needed. + if (is_foreground == border_on_top) { + rect->show(); + rect->set_stroke(is_selected ? select_color : border_color); + } else { + rect->hide(); + rect->set_stroke(0x0); + } + // This undoes the hide for the background rect, and additionally gives it a fill and shadow. + if (!is_foreground) { + rect->show(); +/* + if (_checkerboard) { + // draw checkerboard pattern, ignore alpha (background color doesn't support it) + rect->set_background_checkerboard(_background_color, false); + } + else { + // Background color does not support transparency; draw opaque pages + rect->set_background(_background_color | 0xff); + } +*/ + rect->set_fill(_background_color); + rect->set_shadow(shadow_color, _shadow_size); + } else { + rect->set_fill(0x0); + rect->set_shadow(0x0, 0); + } + } else if (auto label = dynamic_cast<CanvasItemText *>(item.get())) { + _updateTextItem(label, size, txt ? txt : ""); + } + } +} + +/** + * Update the page's textual label. + */ +void CanvasPage::_updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt) +{ + // Default style for the label + int fontsize = 10.0; + uint32_t foreground = 0xffffffff; + uint32_t background = 0x00000099; + uint32_t selected = 0x0e5bf199; + Geom::Point anchor(0.0, 1.0); + Geom::Point coord = page.corner(0); + double radius = 0.2; + + // Change the colors for whiter/lighter backgrounds + unsigned char luminance = SP_RGBA32_LUMINANCE(_canvas_color); + if (luminance < 0x88) { + foreground = 0x000000ff; + background = 0xffffff99; + selected = 0x50afe7ff; + } + + if (_label_style == "below") { + radius = 1.0; + fontsize = 14.0; + anchor = Geom::Point(0.5, -0.2); + coord = Geom::Point(page.midpoint()[Geom::X], page.bottom()); + + if (!txt.empty()) { + std::string bullet = is_selected ? " \u2022 " : " "; + txt = bullet + txt + bullet; + } + } + + label->set_fontsize(fontsize); + label->set_fill(foreground); + label->set_background(is_selected ? selected : background); + label->set_bg_radius(radius); + label->set_anchor(anchor); + label->set_coord(coord); + label->set_visible(!txt.empty()); + label->set_text(std::move(txt)); + label->set_border(4.0); +} + +bool CanvasPage::setOnTop(bool on_top) +{ + if (on_top != _border_on_top) { + _border_on_top = on_top; + return true; + } + return false; +} + +bool CanvasPage::setShadow(int shadow) +{ + if (_shadow_size != shadow) { + _shadow_size = shadow; + return true; + } + return false; +} + +bool CanvasPage::setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed) +{ + if (border != _border_color || bg != _background_color || canvas != _canvas_color) { + _border_color = border; + _background_color = bg; + _canvas_color = canvas; + _margin_color = margin; + _bleed_color = bleed; + return true; + } + return false; +} + +bool CanvasPage::setLabelStyle(const std::string &style) +{ + if (style != _label_style) { + _label_style = style; + return true; + } + return false; +} + +} // 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 : diff --git a/src/display/control/canvas-page.h b/src/display/control/canvas-page.h new file mode 100644 index 0000000..e3227bf --- /dev/null +++ b/src/display/control/canvas-page.h @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * + *//* + * Authors: + * Martin Owens 2021 + * + * Copyright (C) 2021 Martin Owens + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ +#ifndef SEEN_CANVAS_PAGE_H +#define SEEN_CANVAS_PAGE_H + +#include <2geom/rect.h> +#include <glib.h> +#include <vector> + +#include "canvas-item-ptr.h" + +namespace Inkscape { +namespace UI::Widget { class Canvas; } + +class CanvasItemGroup; +class CanvasItemText; + +class CanvasPage +{ +public: + CanvasPage(); + ~CanvasPage(); + + void update(Geom::Rect size, Geom::OptRect margin, Geom::OptRect bleed, const char *txt, bool outline = false); + void add(Geom::Rect size, CanvasItemGroup *background_group, CanvasItemGroup *foreground_group); + void remove(UI::Widget::Canvas *canvas); + void show(); + void hide(); + void set_guides_visible(bool show); + + bool setOnTop(bool on_top); + bool setShadow(int shadow); + bool setPageColor(uint32_t border, uint32_t bg, uint32_t canvas, uint32_t margin, uint32_t bleed); + bool setLabelStyle(const std::string &style); + + bool is_selected = false; +private: + void _updateTextItem(CanvasItemText *label, Geom::Rect page, std::string txt); + + // This may make this look like a CanvasItemGroup, but it's not one. This + // isn't a collection of items, but a set of items in multiple Canvases. + // Each item can belong in either a foreground or background group. + std::vector<CanvasItemPtr<CanvasItem>> canvas_items; + + int _shadow_size = 0; + bool _border_on_top = true; + uint32_t _background_color = 0xffffffff; + uint32_t _border_color = 0x00000040; + uint32_t _canvas_color = 0xffffffff; + uint32_t _margin_color = 0x1699d771; // Blue'ish + uint32_t _bleed_color = 0xbe310e62; // Red'ish + + std::string _label_style = "default"; +}; + +} // namespace Inkscape + +#endif // SEEN_CANVAS_PAGE_H + +/* + 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:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/control/canvas-temporary-item-list.cpp b/src/display/control/canvas-temporary-item-list.cpp new file mode 100644 index 0000000..6ff516d --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.cpp @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that can contain active TemporaryItem's on a desktop + * Code inspired by message-stack.cpp + * + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <algorithm> +#include "canvas-temporary-item.h" +#include "canvas-temporary-item-list.h" + +namespace Inkscape { +namespace Display { + +TemporaryItemList::~TemporaryItemList() +{ + // delete all items in list so the timeouts are removed + for (auto tempitem : itemlist) { + delete tempitem; + } + itemlist.clear(); +} + +// Note that TemporaryItem or TemporaryItemList is responsible for deletion and such, so this return pointer can safely be ignored. +TemporaryItem *TemporaryItemList::add_item(CanvasItem *item, int lifetime_msecs) +{ + // beware of strange things happening due to very short timeouts + TemporaryItem *tempitem; + if (lifetime_msecs == 0) + tempitem = new TemporaryItem(item, 0); + else { + tempitem = new TemporaryItem(item, lifetime_msecs); + tempitem->signal_timeout.connect([this] (auto tempitem) { itemlist.remove(tempitem); }); + // no need to delete the item, it does that itself after signal_timeout.emit() completes + } + + itemlist.emplace_back(tempitem); + return tempitem; +} + +void TemporaryItemList::delete_item(TemporaryItem *tempitem) +{ + // check if the item is in the list, if so, delete it. (in other words, don't wait for the item to delete itself) + auto it = std::find_if(itemlist.begin(), itemlist.end(), [=] (auto *item) { + return item == tempitem; + }); + + if (it != itemlist.end()) { + itemlist.erase(it); + delete tempitem; + } +} + +} // namespace Display +} // 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 : diff --git a/src/display/control/canvas-temporary-item-list.h b/src/display/control/canvas-temporary-item-list.h new file mode 100644 index 0000000..1321cbf --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.h @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H +#define INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <list> + +class SPDesktop; + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +class TemporaryItem; + +/** + * Provides a class that can contain active TemporaryItems on a desktop. + */ +class TemporaryItemList final +{ +public: + TemporaryItemList() = default; + TemporaryItemList(TemporaryItemList const &) = delete; + TemporaryItemList &operator=(TemporaryItemList const &) = delete; + ~TemporaryItemList(); + + TemporaryItem* add_item(CanvasItem *item, int lifetime_msecs); + void delete_item(TemporaryItem *tempitem); + +protected: + std::list<TemporaryItem *> itemlist; ///< List of temp items. +}; + +} // namespace Display +} // namespace Inkscape + +#endif // INKSCAPE_CANVAS_TEMPORARY_ITEM_LIST_H + +/* + 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 : diff --git a/src/display/control/canvas-temporary-item.cpp b/src/display/control/canvas-temporary-item.cpp new file mode 100644 index 0000000..d95ad21 --- /dev/null +++ b/src/display/control/canvas-temporary-item.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that can contain active TemporaryItem's on a desktop + * When the object is deleted, it also deletes the canvasitem it contains! + * This object should be created/managed by a TemporaryItemList. + * After its lifetime, it fires the timeout signal, afterwards *it deletes itself*. + * + * (part of code inspired by message-stack.cpp) + * + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/main.h> + +#include "canvas-temporary-item.h" +#include "canvas-item.h" + +namespace Inkscape { +namespace Display { + +TemporaryItem::TemporaryItem(CanvasItem *item, int lifetime_msecs) + : canvasitem(std::move(item)) +{ + // Zero lifetime means stay forever, so do not add timeout event. + if (lifetime_msecs > 0) { + timeout_conn = Glib::signal_timeout().connect([this] { + signal_timeout.emit(this); + delete this; + return false; + }, lifetime_msecs); + } +} + +TemporaryItem::~TemporaryItem() = default; + +} // namespace Display +} // 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 : diff --git a/src/display/control/canvas-temporary-item.h b/src/display/control/canvas-temporary-item.h new file mode 100644 index 0000000..4ce2787 --- /dev/null +++ b/src/display/control/canvas-temporary-item.h @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_CANVAS_TEMPORARY_ITEM_H +#define INKSCAPE_CANVAS_TEMPORARY_ITEM_H + +/* + * Authors: + * Johan Engelen + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <sigc++/signal.h> +#include <sigc++/connection.h> +#include "display/control/canvas-item-ptr.h" +#include "helper/auto-connection.h" + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +/** + * Provides a class to put a canvasitem temporarily on-canvas. + */ +class TemporaryItem final +{ +public: + TemporaryItem(CanvasItem *item, int lifetime_msecs); + TemporaryItem(TemporaryItem const &) = delete; + TemporaryItem &operator=(TemporaryItem const &) = delete; + ~TemporaryItem(); + + sigc::signal<void (TemporaryItem *)> signal_timeout; + +protected: + friend class TemporaryItemList; + + CanvasItemPtr<CanvasItem> canvasitem; ///< The item we are holding on to. + auto_connection timeout_conn; +}; + +} //namespace Display +} //namespace Inkscape + +#endif // INKSCAPE_CANVAS_TEMPORARY_ITEM_H + +/* + 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 : diff --git a/src/display/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp new file mode 100644 index 0000000..8672d88 --- /dev/null +++ b/src/display/control/snap-indicator.cpp @@ -0,0 +1,648 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap + * + * Authors: + * Johan Engelen + * Diederik van Lierop + * + * Copyright (C) Johan Engelen 2009 <j.b.c.engelen@utwente.nl> + * Copyright (C) Diederik van Lierop 2010 - 2012 <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <glibmm/i18n.h> +#include <string> +#include <iomanip> +#include <unordered_map> + +#include "snap-indicator.h" + +#include "desktop.h" +#include "enums.h" +#include "preferences.h" +#include "util/units.h" +#include "document.h" + + +#include "canvas-item-ctrl.h" +#include "canvas-item-rect.h" +#include "canvas-item-text.h" +#include "canvas-item-curve.h" + +#include "ui/tools/measure-tool.h" + +#define DISTANCE_BG_RADIUS 0.3 + +namespace Inkscape { +namespace Display { + +static std::map<SnapSourceType, Glib::ustring> source2string = { + {SNAPSOURCE_UNDEFINED, _("UNDEFINED")}, + {SNAPSOURCE_BBOX_CORNER, _("Bounding box corner")}, + {SNAPSOURCE_BBOX_MIDPOINT, _("Bounding box midpoint")}, + {SNAPSOURCE_BBOX_EDGE_MIDPOINT, _("Bounding box side midpoint")}, + {SNAPSOURCE_NODE_SMOOTH, _("Smooth node")}, + {SNAPSOURCE_NODE_CUSP, _("Cusp node")}, + {SNAPSOURCE_LINE_MIDPOINT, _("Line midpoint")}, + {SNAPSOURCE_PATH_INTERSECTION, _("Path intersection")}, + {SNAPSOURCE_RECT_CORNER, _("Corner")}, + {SNAPSOURCE_CONVEX_HULL_CORNER, _("Convex hull corner")}, + {SNAPSOURCE_ELLIPSE_QUADRANT_POINT, _("Quadrant point")}, + {SNAPSOURCE_NODE_HANDLE, _("Handle")}, + {SNAPSOURCE_GUIDE, _("Guide")}, + {SNAPSOURCE_GUIDE_ORIGIN, _("Guide origin")}, + {SNAPSOURCE_ROTATION_CENTER, _("Object rotation center")}, + {SNAPSOURCE_OBJECT_MIDPOINT, _("Object midpoint")}, + {SNAPSOURCE_IMG_CORNER, _("Corner")}, + {SNAPSOURCE_TEXT_ANCHOR, _("Text anchor")}, + {SNAPSOURCE_OTHER_HANDLE, _("Handle")}, + {SNAPSOURCE_GRID_PITCH, _("Multiple of grid spacing")}, + {SNAPSOURCE_PAGE_CORNER, _("Page corner")}, + {SNAPSOURCE_PAGE_CENTER, _("Page center")}, +}; + +static std::map<SnapTargetType, Glib::ustring> target2string = { + {SNAPTARGET_UNDEFINED, _("UNDEFINED")}, + {SNAPTARGET_BBOX_CORNER, _("bounding box corner")}, + {SNAPTARGET_BBOX_EDGE, _("bounding box side")}, + {SNAPTARGET_BBOX_EDGE_MIDPOINT, _("bounding box side midpoint")}, + {SNAPTARGET_BBOX_MIDPOINT, _("bounding box midpoint")}, + {SNAPTARGET_NODE_SMOOTH, _("smooth node")}, + {SNAPTARGET_NODE_CUSP, _("cusp node")}, + {SNAPTARGET_LINE_MIDPOINT, _("line midpoint")}, + {SNAPTARGET_PATH, _("path")}, + {SNAPTARGET_PATH_PERPENDICULAR, _("path (perpendicular)")}, + {SNAPTARGET_PATH_TANGENTIAL, _("path (tangential)")}, + {SNAPTARGET_PATH_INTERSECTION, _("path intersection")}, + {SNAPTARGET_PATH_GUIDE_INTERSECTION, _("guide-path intersection")}, + {SNAPTARGET_PATH_CLIP, _("clip-path")}, + {SNAPTARGET_PATH_MASK, _("mask-path")}, + {SNAPTARGET_ELLIPSE_QUADRANT_POINT, _("quadrant point")}, + {SNAPTARGET_RECT_CORNER, _("corner")}, + {SNAPTARGET_GRID, _("grid line")}, + {SNAPTARGET_GRID_INTERSECTION, _("grid intersection")}, + {SNAPTARGET_GRID_PERPENDICULAR, _("grid line (perpendicular)")}, + {SNAPTARGET_GUIDE, _("guide")}, + {SNAPTARGET_GUIDE_INTERSECTION, _("guide intersection")}, + {SNAPTARGET_GUIDE_ORIGIN, _("guide origin")}, + {SNAPTARGET_GUIDE_PERPENDICULAR, _("guide (perpendicular)")}, + {SNAPTARGET_GRID_GUIDE_INTERSECTION, _("grid-guide intersection")}, + {SNAPTARGET_PAGE_EDGE_BORDER, _("page border")}, + {SNAPTARGET_PAGE_EDGE_CORNER, _("page corner")}, + {SNAPTARGET_PAGE_EDGE_CENTER, _("page center")}, + {SNAPTARGET_PAGE_MARGIN_BORDER, _("page margin border")}, + {SNAPTARGET_PAGE_MARGIN_CORNER, _("page margin corner")}, + {SNAPTARGET_PAGE_MARGIN_CENTER, _("page margin center")}, + {SNAPTARGET_PAGE_BLEED_BORDER, _("page bleed border")}, + {SNAPTARGET_PAGE_BLEED_CORNER, _("page bleed corner")}, + {SNAPTARGET_OBJECT_MIDPOINT, _("object midpoint")}, + {SNAPTARGET_IMG_CORNER, _("corner")}, + {SNAPTARGET_ROTATION_CENTER, _("object rotation center")}, + {SNAPTARGET_TEXT_ANCHOR, _("text anchor")}, + {SNAPTARGET_TEXT_BASELINE, _("text baseline")}, + {SNAPTARGET_CONSTRAINED_ANGLE, _("constrained angle")}, + {SNAPTARGET_CONSTRAINT, _("constraint")}, +}; + +SnapIndicator::SnapIndicator(SPDesktop * desktop) + : _snaptarget(nullptr), + _snaptarget_tooltip(nullptr), + _snaptarget_bbox(nullptr), + _snapsource(nullptr), + _snaptarget_is_presnap(false), + _desktop(desktop) +{ +} + +SnapIndicator::~SnapIndicator() +{ + // remove item that might be present + remove_snaptarget(); + remove_snapsource(); +} + +void +SnapIndicator::set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap) +{ + remove_snaptarget(); //only display one snaptarget at a time + + g_assert(_desktop != nullptr); + + if (!p.getSnapped()) { + return; // If we haven't snapped, then it is of no use to draw a snapindicator + } + + if (p.getTarget() == SNAPTARGET_CONSTRAINT) { + // This is not a real snap, although moving along the constraint did affect the mouse pointer's position. + // Maybe we should only show a snap indicator when the user explicitly asked for a constraint by pressing ctrl? + // We should not show a snap indicator when stretching a selection box, which is also constrained. That would be + // too much information. + return; + } + + bool is_alignment = p.getAlignmentTarget().has_value(); + bool is_distribution = p.getTarget() & SNAPTARGET_DISTRIBUTION_CATEGORY; + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + double scale = prefs->getDouble("/tools/measure/scale", 100.0) / 100.0; + + bool value = prefs->getBool("/options/snapindicator/value", true); + + if (value) { + Glib::ustring target_name = _("UNDEFINED"); + Glib::ustring source_name = _("UNDEFINED"); + + if (!is_alignment && !is_distribution) { + if (target2string.find(p.getTarget()) == target2string.end()) + g_warning("Target type %i not present in target2string", p.getTarget()); + + if (source2string.find(p.getSource()) == source2string.end()) + g_warning("Source type %i not present in target2string", p.getSource()); + + target_name = _(target2string[p.getTarget()].c_str()); + source_name = _(source2string[p.getSource()].c_str()); + } + //std::cout << "Snapped " << source_name << " to " << target_name << std::endl; + + remove_snapsource(); // Don't set both the source and target indicators, as these will overlap + + double timeout_val = prefs->getDouble("/options/snapindicatorpersistence/value", 2.0); + if (timeout_val < 0.1) { + timeout_val = 0.1; // a zero value would mean infinite persistence (i.e. until new snap occurs) + // Besides, negatives values would ....? + } + + // TODO: should this be a constant or a separate prefrence + // we are using the preference of measure tool here. + double fontsize = prefs->getDouble("/tools/measure/fontsize", 10.0); + + if (is_distribution) { + make_distribution_indicators(p, fontsize, scale); + } + + if (is_alignment) { + auto color = pre_snap ? 0x7f7f7fff : get_guide_color(p.getAlignmentTargetType()); + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget(), color, fontsize, scale); + if (p.getAlignmentTargetType() == SNAPTARGET_ALIGNMENT_INTERSECTION) { + make_alignment_indicator(p.getPoint(), *p.getAlignmentTarget2(), color, fontsize, scale); + } + } + + _snaptarget_is_presnap = pre_snap; + + // Display the snap indicator (i.e. the cross) + Inkscape::CanvasItemCtrl *ctrl; + + if (!is_alignment && !is_distribution) { + // Display snap indicator at snap target + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CROSS); + ctrl->set_size(11); + ctrl->set_stroke( pre_snap ? 0x7f7f7fff : 0xff0000ff); + ctrl->set_position(p.getPoint()); + + _snaptarget = _desktop->add_temporary_canvasitem(ctrl, timeout_val*1000.0); + // The snap indicator will be deleted after some time-out, and sp_canvas_item_dispose + // will be called. This will set canvas->current_item to NULL if the snap indicator was + // the current item, after which any events will go to the root handler instead of any + // item handler. Dragging an object which has just snapped might therefore not be possible + // without selecting / repicking it again. To avoid this, we make sure here that the + // snap indicator will never be picked, and will therefore never be the current item. + // Reported bugs: + // - scrolling when hovering above a pre-snap indicator won't work (for example) + // (https://bugs.launchpad.net/inkscape/+bug/522335/comments/8) + // - dragging doesn't work without repicking + // (https://bugs.launchpad.net/inkscape/+bug/1420301/comments/15) + ctrl->set_pickable(false); + + // Display the tooltip, which reveals the type of snap source and the type of snap target + Glib::ustring tooltip_str; + if ( (p.getSource() != SNAPSOURCE_GRID_PITCH) && (p.getTarget() != SNAPTARGET_UNDEFINED) ) { + tooltip_str = source_name + _(" to ") + target_name; + } else if (p.getSource() != SNAPSOURCE_UNDEFINED) { + tooltip_str = source_name; + } + + + if (!tooltip_str.empty()) { + Geom::Point tooltip_pos = p.getPoint(); + if (dynamic_cast<Inkscape::UI::Tools::MeasureTool *>(_desktop->event_context)) { + // Make sure that the snap tooltips do not overlap the ones from the measure tool + tooltip_pos += _desktop->w2d(Geom::Point(0, -3*fontsize)); + } else { + tooltip_pos += _desktop->w2d(Geom::Point(0, -2*fontsize)); + } + + auto canvas_tooltip = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), tooltip_pos, tooltip_str); + canvas_tooltip->set_fontsize(fontsize); + canvas_tooltip->set_fill(0xffffffff); + canvas_tooltip->set_background(pre_snap ? 0x33337f40 : 0x33337f7f); + + _snaptarget_tooltip = _desktop->add_temporary_canvasitem(canvas_tooltip, timeout_val*1000.0); + } + + // Display the bounding box, if we snapped to one + Geom::OptRect const bbox = p.getTargetBBox(); + if (bbox) { + auto box = new Inkscape::CanvasItemRect(_desktop->getCanvasTemp(), *bbox); + box->set_stroke(pre_snap ? 0x7f7f7fff : 0xff0000ff); + box->set_dashed(true); + box->set_pickable(false); // Is false by default. + box->lower_to_bottom(); + _snaptarget_bbox = _desktop->add_temporary_canvasitem(box, timeout_val*1000.0); + } + } + } +} + +void +SnapIndicator::remove_snaptarget(bool only_if_presnap) +{ + if (only_if_presnap && !_snaptarget_is_presnap) { + return; + } + + if (_snaptarget) { + _desktop->remove_temporary_canvasitem(_snaptarget); + _snaptarget = nullptr; + _snaptarget_is_presnap = false; + } + + if (_snaptarget_tooltip) { + _desktop->remove_temporary_canvasitem(_snaptarget_tooltip); + _snaptarget_tooltip = nullptr; + } + + if (_snaptarget_bbox) { + _desktop->remove_temporary_canvasitem(_snaptarget_bbox); + _snaptarget_bbox = nullptr; + } + + for (auto *item : _alignment_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _alignment_snap_indicators.clear(); + + for (auto *item : _distribution_snap_indicators) { + _desktop->remove_temporary_canvasitem(item); + } + _distribution_snap_indicators.clear(); +} + +void +SnapIndicator::set_new_snapsource(Inkscape::SnapCandidatePoint const &p) +{ + remove_snapsource(); + + g_assert(_desktop != nullptr); // If this fails, then likely setup() has not been called on the snap manager (see snap.cpp -> setup()) + + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + bool value = prefs->getBool("/options/snapindicator/value", true); + + if (value) { + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_stroke(0xff0000ff); + ctrl->set_position(p.getPoint()); + _snapsource = _desktop->add_temporary_canvasitem(ctrl, 1000); + } +} + +void +SnapIndicator::set_new_debugging_point(Geom::Point const &p) +{ + g_assert(_desktop != nullptr); + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_DIAMOND); + ctrl->set_size(11); + ctrl->set_stroke(0x00ff00ff); + ctrl->set_position(p); + _debugging_points.push_back(_desktop->add_temporary_canvasitem(ctrl, 5000)); +} + +void +SnapIndicator::remove_snapsource() +{ + if (_snapsource) { + _desktop->remove_temporary_canvasitem(_snapsource); + _snapsource = nullptr; + } +} + +void +SnapIndicator::remove_debugging_points() +{ + for (std::list<TemporaryItem *>::const_iterator i = _debugging_points.begin(); i != _debugging_points.end(); ++i) { + _desktop->remove_temporary_canvasitem(*i); + } + _debugging_points.clear(); +} + +guint32 SnapIndicator::get_guide_color(SnapTargetType t) +{ + switch(t) { + case SNAPTARGET_ALIGNMENT_BBOX_CORNER: + case SNAPTARGET_ALIGNMENT_BBOX_MIDPOINT: + case SNAPTARGET_ALIGNMENT_BBOX_EDGE_MIDPOINT: + return 0xff0000ff; + case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_EDGE_CORNER: + case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_MARGIN_CORNER: + case SNAPTARGET_ALIGNMENT_PAGE_BLEED_CORNER: + return 0x00ff00ff; + case SNAPTARGET_ALIGNMENT_HANDLE: + return 0x0000ffff; + case SNAPTARGET_ALIGNMENT_INTERSECTION: + return 0xd13bd1ff; + default: + g_warning("Alignment guide color not handled %i", t); + return 0x000000ff; + } +} + +std::pair<Geom::Coord, int> get_y_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset) +{ + Geom::Coord y; + int sign; + + // We add a margin of 5px here to make sure that very small movements of mouse + // pointer do not cause the position of distribution indicator to change. + if (source.midpoint().y() < target.midpoint().y() + 5) { + y = source.max().y() + offset; + sign = 1; + } else { + y = source.min().y() - offset; + sign = -1; + } + + return {y, sign}; +} + +std::pair<Geom::Coord, int> get_x_and_sign(Geom::Rect const &source, Geom::Rect const &target, double const offset) +{ + Geom::Coord x; + int sign; + + // We add a margin of 5px here to make sure that very small movements of mouse + // pointer do not cause the position of distribution indicator to change. + if (source.midpoint().x() < target.midpoint().x() + 5) { + x = source.max().x() + offset; + sign = 1; + } else { + x = source.min().x() - offset; + sign = -1; + } + + return {x, sign}; +} + +void SnapIndicator::make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale) +{ + //make sure the line is straight + g_assert(p1.x() == p2.x() || p1.y() == p2.y()); + + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + Inkscape::CanvasItemCurve *line; + + auto ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p1); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + ctrl = new Inkscape::CanvasItemCtrl(_desktop->getCanvasTemp(), Inkscape::CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + ctrl->set_size(7); + ctrl->set_mode(Inkscape::CanvasItemCtrlMode::CANVAS_ITEM_CTRL_MODE_COLOR); + ctrl->set_stroke(0xffffffff); + ctrl->set_fill(color); + ctrl->set_position(p2); + ctrl->set_pickable(false); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(ctrl, 0)); + + auto dist = Geom::L2(p2 - p1); + double offset = (fontsize + 5) / _desktop->current_zoom(); + if (show_distance && dist > 2 * offset) { + auto direction = Geom::unit_vector(p1 - p2); + auto text_pos = (p1 + p2)/2; + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + + dist = Inkscape::Util::Quantity::convert(dist, "px", unit_name); + + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*dist); + + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(color); + text->set_background(0xffffffc8); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + + auto temp_point = text_pos + offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, temp_point); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + + temp_point = text_pos - offset*direction; + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), temp_point, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } else { + line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line->set_stroke(color); + line->set_bg_alpha(1.0f); + _alignment_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line, 0)); + } +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_v(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(0, length/2), p - Geom::Point(0, length/2)); + line->set_stroke(0xff5f1fff); + return line; +} + +Inkscape::CanvasItemCurve* SnapIndicator::make_stub_line_h(Geom::Point const & p) +{ + Geom::Coord length = 10/_desktop->current_zoom(); + auto line = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p + Geom::Point(length/2, 0), p - Geom::Point(length/2, 0)); + line->set_stroke(0xff5f1fff); + return line; +} + +void SnapIndicator::make_distribution_indicators(SnappedPoint const &p, + double fontsize, + double scale) +{ + Preferences *prefs = Preferences::get(); + bool show_distance = prefs->getBool("/options/snapindicatordistance/value", false); + + guint32 color = 0xff5f1fff; + guint32 text_fill = 0xffffffff; + guint32 text_bg = 0xff5f1fff; //0x33337f7f + Geom::Point text_pos; + double text_offset = (fontsize * 2); + // double line_offset = 5/_desktop->current_zoom(); + double line_offset = 0; + + Glib::ustring unit_name = _desktop->doc()->getDisplayUnit()->abbr.c_str(); + if (!unit_name.compare("")) { + unit_name = DEFAULT_UNIT_NAME; + } + auto equal_dist = Inkscape::Util::Quantity::convert(p.getDistributionDistance(), "px", unit_name); + Glib::ustring distance = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist); + + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_Y: + case SNAPTARGET_DISTRIBUTION_X: + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_DOWN: { + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + switch (p.getTarget()) { + case SNAPTARGET_DISTRIBUTION_RIGHT: + case SNAPTARGET_DISTRIBUTION_LEFT: + case SNAPTARGET_DISTRIBUTION_X: { + auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + break; + } + + case SNAPTARGET_DISTRIBUTION_DOWN: + case SNAPTARGET_DISTRIBUTION_UP: + case SNAPTARGET_DISTRIBUTION_Y: { + auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + break; + } + } + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + break; + } + case SNAPTARGET_DISTRIBUTION_XY: { + Geom::Point p1, p2; + Inkscape::CanvasItemCurve *point1, *point2; + + auto equal_dist2 = Inkscape::Util::Quantity::convert(p.getDistributionDistance2(), "px", unit_name); + Glib::ustring distance2 = Glib::ustring::format(std::fixed, std::setprecision(1), std::noshowpoint, scale*equal_dist2); + + for (auto it = p.getBBoxes().begin(); it + 1 != p.getBBoxes().end(); it++) { + auto [y, sign] = get_y_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(it->max().x() + line_offset, y); + p2 = Geom::Point(std::next(it)->min().x() - line_offset, y); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(0, sign*text_offset)); + + point1 = make_stub_line_v(p1); + point2 = make_stub_line_v(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + for (auto it = p.getBBoxes2().begin(); it + 1 != p.getBBoxes2().end(); it++) { + auto [x, sign] = get_x_and_sign(*it, *std::next(it), 5/_desktop->current_zoom()); + p1 = Geom::Point(x, it->max().y() + line_offset); + p2 = Geom::Point(x, std::next(it)->min().y() - line_offset); + text_pos = (p1 + p2)/2 + _desktop->w2d(Geom::Point(sign*text_offset, 0)); + + point1 = make_stub_line_h(p1); + point2 = make_stub_line_h(p2); + + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point1, 0)); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(point2, 0)); + + auto line1 = new Inkscape::CanvasItemCurve(_desktop->getCanvasTemp(), p1, p2); + line1->set_stroke(color); + line1->set_width(2); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(line1, 0)); + + if (show_distance) { + auto text = new Inkscape::CanvasItemText(_desktop->getCanvasTemp(), text_pos, distance2); + text->set_fontsize(fontsize); + text->set_fill(text_fill); + text->set_background(text_bg); + text->set_bg_radius(DISTANCE_BG_RADIUS); + text->set_anchor({0.5, 0.5}); + _distribution_snap_indicators.push_back(_desktop->add_temporary_canvasitem(text, 0)); + } + } + + break; + } + } +} + +} //namespace Display +} /* 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=4:softtabstop=4 : diff --git a/src/display/control/snap-indicator.h b/src/display/control/snap-indicator.h new file mode 100644 index 0000000..ca4721b --- /dev/null +++ b/src/display/control/snap-indicator.h @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef INKSCAPE_DISPLAY_SNAP_INDICATOR_H +#define INKSCAPE_DISPLAY_SNAP_INDICATOR_H + +/** + * @file + * Provides a class that shows a temporary indicator on the canvas of where the snap was, and what kind of snap + */ +/* + * Authors: + * Johan Engelen + * Diederik van Lierop + * + * Copyright (C) Johan Engelen 2008 <j.b.c.engelen@utwente.nl> + * Copyright (C) Diederik van Lierop 2010 <mail@diedenrezi.nl> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "snap-enums.h" +#include "snapped-point.h" +#include "display/control/canvas-item-curve.h" + +#include <glib.h> +#include <glibmm/i18n.h> + +class SPDesktop; + +namespace Inkscape { +namespace Display { + +class TemporaryItem; + +class SnapIndicator { +public: + SnapIndicator(SPDesktop *desktop); + virtual ~SnapIndicator(); + + void set_new_snaptarget(Inkscape::SnappedPoint const &p, bool pre_snap = false); + void remove_snaptarget(bool only_if_presnap = false); + + void set_new_snapsource(Inkscape::SnapCandidatePoint const &p); + void remove_snapsource(); + + void set_new_debugging_point(Geom::Point const &p); + void remove_debugging_points(); + +protected: + TemporaryItem *_snaptarget; + TemporaryItem *_snaptarget_tooltip; + TemporaryItem *_snaptarget_bbox; + TemporaryItem *_snapsource; + + std::list<TemporaryItem *> _alignment_snap_indicators; + std::list<TemporaryItem *> _distribution_snap_indicators; + std::list<TemporaryItem *> _debugging_points; + bool _snaptarget_is_presnap; + SPDesktop *_desktop; + +private: + SnapIndicator(const SnapIndicator&) = delete; + SnapIndicator& operator=(const SnapIndicator&) = delete; + + void make_distribution_indicators(SnappedPoint const &p, double fontsize, double scale); + void make_alignment_indicator(Geom::Point const &p1, Geom::Point const &p2, guint32 color, double fontsize, double scale); + guint32 get_guide_color(SnapTargetType t); + Inkscape::CanvasItemCurve* make_stub_line_h(Geom::Point const &p); + Inkscape::CanvasItemCurve* make_stub_line_v(Geom::Point const &p); +}; + +} //namespace Display +} //namespace Inkscape + +#endif + +/* + 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 : |