diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 18:24:48 +0000 |
commit | cca66b9ec4e494c1d919bff0f71a820d8afab1fa (patch) | |
tree | 146f39ded1c938019e1ed42d30923c2ac9e86789 /src/display/control | |
parent | Initial commit. (diff) | |
download | inkscape-upstream/1.2.2.tar.xz inkscape-upstream/1.2.2.zip |
Adding upstream version 1.2.2.upstream/1.2.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
41 files changed, 8961 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-axonomgrid.cpp b/src/display/control/canvas-axonomgrid.cpp new file mode 100644 index 0000000..6ec4626 --- /dev/null +++ b/src/display/control/canvas-axonomgrid.cpp @@ -0,0 +1,770 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2006-2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* + * 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. + */ + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <glibmm/i18n.h> + +#include <2geom/angle.h> +#include <2geom/line.h> + +#include "canvas-axonomgrid.h" +#include "canvas-grid.h" +#include "canvas-item-grid.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" + +#include "display/cairo-utils.h" + +#include "helper/mathfns.h" + +#include "object/sp-namedview.h" +#include "object/sp-object.h" +#include "object/sp-root.h" + +#include "svg/svg-color.h" + +#include "ui/widget/canvas.h" +#include "ui/widget/registered-widget.h" + +#include "util/units.h" + +using Inkscape::Util::unit_table; + +enum Dim3 { X=0, Y, Z }; + +/** + * This function calls Cairo to render a line on a particular canvas buffer. + * Coordinates are interpreted as SCREENcoordinates + */ +static void +sp_caxonomgrid_drawline (Inkscape::CanvasItemBuffer *buf, gint x0, gint y0, gint x1, gint y1, guint32 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 +sp_grid_vline (Inkscape::CanvasItemBuffer *buf, gint x, gint ys, gint ye, guint32 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(); +} + +namespace Inkscape { + + +CanvasAxonomGrid::CanvasAxonomGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc) + : CanvasGrid(nv, in_repr, in_doc, GRID_AXONOMETRIC) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gridunit = unit_table.getUnit(prefs->getString("/options/grids/axonom/units")); + if (!gridunit) { + gridunit = unit_table.getUnit("px"); + } + origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_x", 0.0), gridunit, "px"); + origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/origin_y", 0.0), gridunit, "px"); + color = prefs->getInt("/options/grids/axonom/color", GRID_DEFAULT_COLOR); + empcolor = prefs->getInt("/options/grids/axonom/empcolor", GRID_DEFAULT_EMPCOLOR); + empspacing = prefs->getInt("/options/grids/axonom/empspacing", 5); + lengthy = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/axonom/spacing_y", 1.0), gridunit, "px"); + angle_deg[X] = prefs->getDouble("/options/grids/axonom/angle_x", 30.0); + angle_deg[Z] = prefs->getDouble("/options/grids/axonom/angle_z", 30.0); + angle_deg[Y] = 0; + + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = tan(angle_rad[X]); + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = tan(angle_rad[Z]); + + snapper = new CanvasAxonomGridSnapper(this, &namedview->snap_manager, 0); + + if (repr) readRepr(); +} + +CanvasAxonomGrid::~CanvasAxonomGrid () +{ + if (snapper) delete snapper; +} + +static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color) +{ + if (!str) { + return FALSE; + } + + gchar *u; + gdouble v = g_ascii_strtod(str, &u); + if (!u) { + return FALSE; + } + v = CLAMP(v, 0.0, 1.0); + + *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999); + + return TRUE; +} + + + +void +CanvasAxonomGrid::readRepr() +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->width.computed / root->viewBox.width(); + scale_y = root->height.computed / root->viewBox.height(); + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is uniform, try to reduce numerical error + scale_x = (scale_x + scale_y)/2.0; + double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px"); + if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON)) + scale_x = scale_none; // objects are same size, reduce numerical error + scale_y = scale_x; + } + } + + gchar const *value; + + if ( (value = repr->attribute("originx")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::X] = q.value("px"); + } else { + // Grid in 'user units' + origin[Geom::X] = q.quantity * scale_x; + } + } + + if ( (value = repr->attribute("originy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::Y] = q.value("px"); + } else { + // Grid in 'user units' + origin[Geom::Y] = q.quantity * scale_y; + } + } + + if ( (value = repr->attribute("spacingy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + lengthy = q.value("px"); + } else { + // Grid in 'user units' + lengthy = q.quantity * scale_y; // We do not handle scale_x != scale_y + } + if (lengthy < 0.0500) lengthy = 0.0500; + } + + if ( (value = repr->attribute("gridanglex")) ) { + angle_deg[X] = g_ascii_strtod(value, nullptr); + if (angle_deg[X] < 0.) angle_deg[X] = 0.; + if (angle_deg[X] > 89.0) angle_deg[X] = 89.0; + angle_rad[X] = Geom::rad_from_deg(angle_deg[X]); + tan_angle[X] = tan(angle_rad[X]); + } + + if ( (value = repr->attribute("gridanglez")) ) { + angle_deg[Z] = g_ascii_strtod(value, nullptr); + if (angle_deg[Z] < 0.) angle_deg[Z] = 0.; + if (angle_deg[Z] > 89.0) angle_deg[Z] = 89.0; + angle_rad[Z] = Geom::rad_from_deg(angle_deg[Z]); + tan_angle[Z] = tan(angle_rad[Z]); + } + + if ( (value = repr->attribute("color")) ) { + color = (color & 0xff) | sp_svg_read_color(value, color); + } + + if ( (value = repr->attribute("empcolor")) ) { + empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor); + } + + if ( (value = repr->attribute("opacity")) ) { + sp_nv_read_opacity(value, &color); + } + if ( (value = repr->attribute("empopacity")) ) { + sp_nv_read_opacity(value, &empcolor); + } + + if ( (value = repr->attribute("empspacing")) ) { + empspacing = atoi(value); + } + + if ( (value = repr->attribute("visible")) ) { + visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("enabled")) ) { + g_assert(snapper != nullptr); + snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) { + g_assert(snapper != nullptr); + snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("units")) ) { + gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu + } + + for (auto grid : canvas_item_grids) { + grid->request_update(); + } + + return; +} + +/** + * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves. + */ +void +CanvasAxonomGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/) +{ + readRepr(); + + if ( ! (_wr.isUpdating()) ) + updateWidgets(); +} + +Gtk::Widget * +CanvasAxonomGrid::newSpecificWidget() +{ + _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu( + _("Grid _units:"), "units", _wr, repr, doc) ); + _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("_Origin X:"), _("X coordinate of grid origin"), "originx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _Y:"), _("Base length of z-axis"), "spacingy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_ax = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar( + _("Angle X:"), _("Angle of x-axis"), "gridanglex", _wr, repr, doc ) ); + _rsu_az = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalar( + _("Angle Z:"), _("Angle of z-axis"), "gridanglez", _wr, repr, doc ) ); + + _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"), + "color", "opacity", _wr, repr, doc)); + _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Ma_jor grid line color:"), _("Major grid line color"), + _("Color of the major (highlighted) grid lines"), + "empcolor", "empopacity", _wr, repr, doc)); + + _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger( + _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc ) ); + + _rumg->set_hexpand(); + _rsu_ox->set_hexpand(); + _rsu_oy->set_hexpand(); + _rsu_sy->set_hexpand(); + _rsu_ax->set_hexpand(); + _rsu_az->set_hexpand(); + _rcp_gcol->set_hexpand(); + _rcp_gmcol->set_hexpand(); + _rsi->set_hexpand(); + + // set widget values + _wr.setUpdating (true); + + _rsu_ox->setDigits(5); + _rsu_ox->setIncrements(0.1, 1.0); + + _rsu_oy->setDigits(5); + _rsu_oy->setIncrements(0.1, 1.0); + + _rsu_sy->setDigits(5); + _rsu_sy->setIncrements(0.1, 1.0); + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + val = lengthy; + double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (gridy); + + _rsu_ax->setValue(angle_deg[X]); + _rsu_az->setValue(angle_deg[Z]); + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + + Gtk::Box *column = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + column->pack_start(*_rumg, true, false); + column->pack_start(*_rsu_ox, true, false); + column->pack_start(*_rsu_oy, true, false); + column->pack_start(*_rsu_sy, true, false); + column->pack_start(*_rsu_ax, true, false); + column->pack_start(*_rsu_az, true, false); + column->pack_start(*_rcp_gcol, true, false); + column->pack_start(*_rcp_gmcol, true, false); + column->pack_start(*_rsi, true, false); + + return column; +} + + +/** + * Update dialog widgets from object's values. + */ +void +CanvasAxonomGrid::updateWidgets() +{ + if (_wr.isUpdating()) return; + + //no widgets (grid created with the document, not with the dialog) + if (!_rcb_visible) return; + + _wr.setUpdating (true); + + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + + val = lengthy; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (val); + + _rsu_ax->setValue(angle_deg[X]); + _rsu_az->setValue(angle_deg[Z]); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + _rsu_ax->setProgrammatically = false; + _rsu_az->setProgrammatically = false; + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); +} + + + +void +CanvasAxonomGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/) +{ + ow = origin * affine; + sw = Geom::Point(fabs(affine[0]),fabs(affine[3])); + sw *= lengthy; + + scaled = false; + + for(int dim = 0; dim < 2; dim++) { + gint scaling_factor = empspacing; + + if (scaling_factor <= 1) + scaling_factor = 5; + + int watchdog = 0; + while ( (sw[dim] < 8.0) & (watchdog < 100) ) { + scaled = true; + sw[dim] *= scaling_factor; + // First pass, go up to the major line spacing, then + // keep increasing by two. + scaling_factor = 2; + watchdog++; + } + + } + + spacing_ylines = sw[Geom::X] /(tan_angle[X] + tan_angle[Z]); + lyw = sw[Geom::Y]; + lxw_x = Geom::are_near(tan_angle[X],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[X]; + lxw_z = Geom::are_near(tan_angle[Z],0.) ? Geom::infinity() : sw[Geom::X] / tan_angle[Z]; + + if (empspacing == 0) { + scaled = true; + } +} + +void +CanvasAxonomGrid::Render (Inkscape::CanvasItemBuffer *buf) +{ + //set correct coloring, depending preference (when zoomed out, always major coloring or minor coloring) + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint32 _empcolor; + guint32 _color = color; + bool preference = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false); + if( scaled && preference ) { + _empcolor = color; + } else { + _empcolor = empcolor; + } + + bool xrayactive = prefs->getBool("/desktop/xrayactive", false); + if (xrayactive) { //this allow good looking on xray zones + guint32 bg = 0xffffffff; + _color = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0), + 1.0); + _empcolor = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0), + 1.0); + } + + 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 ; br = bottomright + Geom::Point buf_tl_gc; + Geom::Point buf_br_gc; + buf_tl_gc[Geom::X] = buf->rect.left() - ow[Geom::X]; + buf_tl_gc[Geom::Y] = buf->rect.top() - ow[Geom::Y]; + buf_br_gc[Geom::X] = buf->rect.right() - ow[Geom::X]; + buf_br_gc[Geom::Y] = buf->rect.bottom() - ow[Geom::Y]; + + // render the three separate line groups representing the main-axes + + // x-axis always goes from topleft to bottomright. (0,0) - (1,1) + gdouble const xintercept_y_bc = (buf_tl_gc[Geom::X] * tan_angle[X]) - buf_tl_gc[Geom::Y] ; + gdouble const xstart_y_sc = ( xintercept_y_bc - floor(xintercept_y_bc/lyw)*lyw ) + buf->rect.top(); + gint const xlinestart = round( (xstart_y_sc - buf_tl_gc[Geom::X]*tan_angle[X] - ow[Geom::Y]) / lyw ); + gint xlinenum = xlinestart; + + // lines starting on left side. + + for (gdouble y = xstart_y_sc; y < buf->rect.bottom(); y += lyw, xlinenum++) { + gint const x0 = buf->rect.left(); + gint const y0 = round(y); + gint x1 = x0 + round( (buf->rect.bottom() - y) / tan_angle[X] ); + gint y1 = buf->rect.bottom(); + if ( Geom::are_near(tan_angle[X],0.) ) { + x1 = buf->rect.right(); + y1 = y0; + } + + if (!scaled && (xlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + // lines starting from top side + if (!Geom::are_near(tan_angle[X],0.)) + { + gdouble const xstart_x_sc = buf->rect.left() + (lxw_x - (xstart_y_sc - buf->rect.top()) / tan_angle[X]) ; + xlinenum = xlinestart-1; + for (gdouble x = xstart_x_sc; x < buf->rect.right(); x += lxw_x, xlinenum--) { + gint const y0 = buf->rect.top(); + gint const y1 = buf->rect.bottom(); + gint const x0 = round(x); + gint const x1 = x0 + round( (y1 - y0) / tan_angle[X] ); + + if (!scaled && (xlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + } + + // y-axis lines (vertical) + gdouble const ystart_x_sc = floor (buf_tl_gc[Geom::X] / spacing_ylines) * spacing_ylines + ow[Geom::X]; + gint const ylinestart = round((ystart_x_sc - ow[Geom::X]) / spacing_ylines); + gint ylinenum = ylinestart; + for (gdouble x = ystart_x_sc; x < buf->rect.right(); x += spacing_ylines, ylinenum++) { + gint 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 + // CanvasXYGrid::Render() for more details + if (!scaled && (ylinenum % empspacing) != 0) { + sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _color); + } else { + sp_grid_vline (buf, x0, buf->rect.top(), buf->rect.bottom() - 1, _empcolor); + } + } + + // z-axis always goes from bottomleft to topright. (0,1) - (1,0) + gdouble const zintercept_y_bc = (buf_tl_gc[Geom::X] * -tan_angle[Z]) - buf_tl_gc[Geom::Y] ; + gdouble const zstart_y_sc = ( zintercept_y_bc - floor(zintercept_y_bc/lyw)*lyw ) + buf->rect.top(); + gint const zlinestart = round( (zstart_y_sc + buf_tl_gc[Geom::X]*tan_angle[Z] - ow[Geom::Y]) / lyw ); + gint zlinenum = zlinestart; + // lines starting from left side + gdouble next_y = zstart_y_sc; + for (gdouble y = zstart_y_sc; y < buf->rect.bottom(); y += lyw, zlinenum++, next_y = y) { + gint const x0 = buf->rect.left(); + gint const y0 = round(y); + gint x1 = x0 + round( (y - buf->rect.top() ) / tan_angle[Z] ); + gint y1 = buf->rect.top(); + if ( Geom::are_near(tan_angle[Z],0.) ) { + x1 = buf->rect.right(); + y1 = y0; + } + + if (!scaled && (zlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + // draw lines from bottom-up + if (!Geom::are_near(tan_angle[Z],0.)) + { + gdouble const zstart_x_sc = buf->rect.left() + (next_y - buf->rect.bottom()) / tan_angle[Z] ; + for (gdouble x = zstart_x_sc; x < buf->rect.right(); x += lxw_z, zlinenum++) { + gint const y0 = buf->rect.bottom(); + gint const y1 = buf->rect.top(); + gint const x0 = round(x); + gint const x1 = x0 + round(buf->rect.height() / tan_angle[Z] ); + + if (!scaled && (zlinenum % empspacing) != 0) { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _color); + } else { + sp_caxonomgrid_drawline (buf, x0, y0, x1, y1, _empcolor); + } + } + } + + buf->cr->restore(); +} + +CanvasAxonomGridSnapper::CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d) +{ + this->grid = grid; +} + +/** + * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels + */ +Geom::Coord CanvasAxonomGridSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getGridTolerance() / zoom; +} + +bool CanvasAxonomGridSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +LineSnapper::LineList +CanvasAxonomGridSnapper::_getSnapLines(Geom::Point const &p) const +{ + LineList s; + + if ( grid == nullptr ) { + return s; + } + + SPDesktop const *dt = _snapmanager->getDesktop(); + double ta_x = grid->tan_angle[X]; + double ta_z = grid->tan_angle[Z]; + + if (dt && dt->is_yaxisdown()) { + std::swap(ta_x, ta_z); + } + + double spacing_h; + double spacing_v; + + if (getSnapVisibleOnly()) { + // Only snapping to visible grid lines + spacing_h = grid->spacing_ylines; // this is the spacing of the visible grid lines measured in screen pixels + spacing_v = grid->lyw; // vertical + // convert screen pixels to px + // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary + if (dt) { + spacing_h /= dt->current_zoom(); + spacing_v /= dt->current_zoom(); + } + } else { + // Snapping to any grid line, whether it's visible or not + spacing_h = grid->lengthy /(grid->tan_angle[X] + grid->tan_angle[Z]); + spacing_v = grid->lengthy; + + } + + // In an axonometric grid, any point will be surrounded by 6 grid lines: + // - 2 vertical grid lines, one left and one right from the point + // - 2 angled z grid lines, one above and one below the point + // - 2 angled x grid lines, one above and one below the point + + // Calculate the x coordinate of the vertical grid lines + Geom::Coord x_max = Inkscape::Util::round_to_upper_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]); + Geom::Coord x_min = Inkscape::Util::round_to_lower_multiple_plus(p[Geom::X], spacing_h, grid->origin[Geom::X]); + + // Calculate the y coordinate of the intersection of the angled grid lines with the y-axis + double y_proj_along_z = p[Geom::Y] - ta_z * (p[Geom::X] - grid->origin[Geom::X]); + double y_proj_along_x = p[Geom::Y] + ta_x * (p[Geom::X] - grid->origin[Geom::X]); + double y_proj_along_z_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_z_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_z, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_x_max = Inkscape::Util::round_to_upper_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]); + double y_proj_along_x_min = Inkscape::Util::round_to_lower_multiple_plus(y_proj_along_x, spacing_v, grid->origin[Geom::Y]); + + // Calculate the versor for the angled grid lines + Geom::Point vers_x = Geom::Point(1, -ta_x); + Geom::Point vers_z = Geom::Point(1, ta_z); + + // Calculate the normal for the angled grid lines + Geom::Point norm_x = Geom::rot90(vers_x); + Geom::Point norm_z = Geom::rot90(vers_z); + + // The four angled grid lines form a parallelogram, enclosing the point + // One of the two vertical grid lines divides this parallelogram in two triangles + // We will now try to find out in which half (i.e. triangle) our point is, and return + // only the three grid lines defining that triangle + + // The vertical grid line is at the intersection of two angled grid lines. + // Now go find that intersection! + Geom::Point p_x(0, y_proj_along_x_max); + Geom::Line line_x(p_x, p_x + vers_x); + Geom::Point p_z(0, y_proj_along_z_max); + Geom::Line line_z(p_z, p_z + vers_z); + + Geom::OptCrossing inters = Geom::OptCrossing(); // empty by default + try + { + inters = Geom::intersection(line_x, line_z); + } + catch (Geom::InfiniteSolutions &e) + { + // We're probably dealing with parallel lines; this is useless! + return s; + } + + // Determine which half of the parallelogram to use + bool use_left_half = true; + bool use_right_half = true; + + if (inters) { + Geom::Point inters_pt = line_x.pointAt((*inters).ta); + use_left_half = (p[Geom::X] - grid->origin[Geom::X]) < inters_pt[Geom::X]; + use_right_half = !use_left_half; + } + + // Return the three grid lines which define the triangle that encloses our point + // If we didn't find an intersection above, all 6 grid lines will be returned + if (use_left_half) { + s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_max))); + s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_min))); + s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_max, 0))); + } + + if (use_right_half) { + s.push_back(std::make_pair(norm_z, Geom::Point(grid->origin[Geom::X], y_proj_along_z_min))); + s.push_back(std::make_pair(norm_x, Geom::Point(grid->origin[Geom::X], y_proj_along_x_max))); + s.push_back(std::make_pair(Geom::Point(1, 0), Geom::Point(x_min, 0))); + } + + return s; +} + +void CanvasAxonomGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const +{ + SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line); + isr.grid_lines.push_back(dummy); +} + +void CanvasAxonomGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void CanvasAxonomGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +bool CanvasAxonomGridSnapper::ThisSnapperMightSnap() const +{ + return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID); +} + + +}; // 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:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/control/canvas-axonomgrid.h b/src/display/control/canvas-axonomgrid.h new file mode 100644 index 0000000..be23d5b --- /dev/null +++ b/src/display/control/canvas-axonomgrid.h @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef CANVAS_AXONOMGRID_H +#define CANVAS_AXONOMGRID_H + +/* + * Authors: + * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl> + * + * Copyright (C) 2006-2012 Authors + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "line-snapper.h" +#include "canvas-grid.h" + +class SPNamedView; + +namespace Inkscape { +class CanvasItemBuffer; +namespace XML { + class Node; +}; + +class CanvasAxonomGrid : public CanvasGrid { +public: + CanvasAxonomGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc); + ~CanvasAxonomGrid() override; + + void Update (Geom::Affine const &affine, unsigned int flags) override; + void Render (Inkscape::CanvasItemBuffer *buf) override; + + void readRepr() override; + void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override; + + double lengthy; /**< The lengths of the primary y-axis */ + 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[.]) */ + + bool scaled; /**< Whether the grid is in scaled mode */ + +protected: + friend class CanvasAxonomGridSnapper; + + Geom::Point ow; /**< Transformed origin by the affine for the zoom */ + 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 sw; /**< the scaling factors of the affine transform */ + + Gtk::Widget * newSpecificWidget() override; + +private: + CanvasAxonomGrid(const CanvasAxonomGrid&) = delete; + CanvasAxonomGrid& operator=(const CanvasAxonomGrid&) = delete; + + void updateWidgets(); + + Inkscape::UI::Widget::RegisteredUnitMenu *_rumg; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy; + Inkscape::UI::Widget::RegisteredScalar *_rsu_ax; + Inkscape::UI::Widget::RegisteredScalar *_rsu_az; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol; + Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi; +}; + + + +class CanvasAxonomGridSnapper : public LineSnapper +{ +public: + CanvasAxonomGridSnapper(CanvasAxonomGrid *grid, SnapManager *sm, Geom::Coord const d); + bool ThisSnapperMightSnap() const override; + + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + +private: + LineList _getSnapLines(Geom::Point const &p) const override; + void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override; + void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + + CanvasAxonomGrid *grid; +}; + + +}; //namespace Inkscape + + + +#endif + + diff --git a/src/display/control/canvas-grid.cpp b/src/display/control/canvas-grid.cpp new file mode 100644 index 0000000..8ca17e0 --- /dev/null +++ b/src/display/control/canvas-grid.cpp @@ -0,0 +1,1120 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Cartesian grid item for the Inkscape canvas. + *//* + * Authors: + * see git history + * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl> + * Copyright (C) Lauris Kaplinski 2000 + * Abhishek Sharma + * Jon A. Cruz <jon@joncruz.org> + * Copyright (C) Tavmong Bah 2017 <tavmjong@free.fr> + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +/* As a general comment, I am not exactly proud of how things are done. + * (for example the 'enable' widget and readRepr things) + * It does seem to work however. I intend to clean up and sort things out later, but that can take forever... + * Don't be shy to correct things. + */ + +#include <gtkmm/box.h> +#include <gtkmm/label.h> +#include <gtkmm/grid.h> + +#include <glibmm/i18n.h> + +#include "canvas-grid.h" +#include "canvas-axonomgrid.h" +#include "canvas-item-grid.h" + +#include "desktop.h" +#include "document.h" +#include "inkscape.h" +#include "preferences.h" + +#include "display/cairo-utils.h" + +#include "helper/mathfns.h" + +#include "object/sp-namedview.h" +#include "object/sp-object.h" +#include "object/sp-root.h" + +#include "svg/stringstream.h" +#include "svg/svg-color.h" + +#include "ui/icon-names.h" +#include "ui/widget/canvas.h" + +#include "util/units.h" + +#include "xml/node-event-vector.h" + +using Inkscape::DocumentUndo; +using Inkscape::Util::unit_table; + +namespace Inkscape { + +static gchar const *const grid_name[] = { + N_("Rectangular grid"), + N_("Axonometric grid") +}; + +static gchar const *const grid_svgname[] = { + "xygrid", + "axonomgrid" +}; + +// ########################################################## +// CanvasGrid + + static Inkscape::XML::NodeEventVector const _repr_events = { + nullptr, /* child_added */ + nullptr, /* child_removed */ + CanvasGrid::on_repr_attr_changed, + nullptr, /* content_changed */ + nullptr /* order_changed */ + }; + +CanvasGrid::CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type) + : visible(true), gridtype(type), legacy(false), pixel(false) +{ + repr = in_repr; + doc = in_doc; + if (repr) { + repr->addListener (&_repr_events, this); + } + + namedview = nv; +} + +CanvasGrid::~CanvasGrid() +{ + if (repr) { + repr->removeListenerByData (this); + } + + if (_rcb_enabled) { + _rcb_enabled->remove_destroy_notify_callback(this); + } + + for (auto grid : canvas_item_grids) { + delete grid; + } + canvas_item_grids.clear(); +} + +const char * +CanvasGrid::getName() const +{ + return _(grid_name[gridtype]); +} + +const char * +CanvasGrid::getSVGName() const +{ + return grid_svgname[gridtype]; +} + +GridType +CanvasGrid::getGridType() const +{ + return gridtype; +} + + +char const * +CanvasGrid::getName(GridType type) +{ + return _(grid_name[type]); +} + +char const * +CanvasGrid::getSVGName(GridType type) +{ + return grid_svgname[type]; +} + +GridType +CanvasGrid::getGridTypeFromSVGName(char const *typestr) +{ + if (!typestr) return GRID_RECTANGULAR; + + gint t = 0; + for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid + if (!strcmp(typestr, grid_svgname[t])) break; + } + return (GridType) t; +} + +GridType +CanvasGrid::getGridTypeFromName(char const *typestr) +{ + if (!typestr) return GRID_RECTANGULAR; + + gint t = 0; + for (t = GRID_MAXTYPENR; t >= 0; t--) { //this automatically defaults to grid0 which is rectangular grid + if (!strcmp(typestr, _(grid_name[t]))) break; + } + return (GridType) t; +} + + +/* +* writes an <inkscape:grid> child to repr. +*/ +void +CanvasGrid::writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype) +{ + if (!repr) return; + if (gridtype > GRID_MAXTYPENR) return; + + // first create the child xml node, then hook it to repr. This order is important, to not set off listeners to repr before the new node is complete. + + Inkscape::XML::Document *xml_doc = doc->getReprDoc(); + Inkscape::XML::Node *newnode; + newnode = xml_doc->createElement("inkscape:grid"); + newnode->setAttribute("type", getSVGName(gridtype)); + + repr->appendChild(newnode); + Inkscape::GC::release(newnode); + + DocumentUndo::done(doc, _("Create new grid"), INKSCAPE_ICON("document-properties")); +} + +/* +* Creates a new CanvasGrid object of type gridtype +*/ +CanvasGrid* +CanvasGrid::NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype) +{ + if (!repr) return nullptr; + if (!doc) { + g_error("CanvasGrid::NewGrid - doc==NULL"); + return nullptr; + } + + switch (gridtype) { + case GRID_RECTANGULAR: + return new CanvasXYGrid(nv, repr, doc); + case GRID_AXONOMETRIC: + return new CanvasAxonomGrid(nv, repr, doc); + } + + return nullptr; +} + + +/** +* creates a new grid canvasitem for the SPDesktop given as parameter. Keeps a link to this canvasitem in the canvas_item_grids list. +*/ +Inkscape::CanvasItemGrid * +CanvasGrid::createCanvasItem(SPDesktop * desktop) +{ + if (!desktop) return nullptr; +// Johan: I think for multiple desktops it is best if each has their own canvasitem, +// but share the same CanvasGrid object; that is what this function is for. + + // check if there is already a canvasitem on this desktop linking to this grid + for (auto grid : canvas_item_grids) { + if ( desktop->getCanvasGrids() == grid->get_parent() ) { + return nullptr; + } + } + + Inkscape::CanvasItemGrid *grid = new Inkscape::CanvasItemGrid(desktop->getCanvasGrids(), this); + grid->show(); + canvas_item_grids.push_back(grid); + return grid; +} + +/** + * Remove a CanvasGridItem from vector. Does NOT delete CanvasGridItem. + * This is used by the CanvasGridItem destructor to ensure no dangling pointer is left. + */ +void +CanvasGrid::removeCanvasItem(Inkscape::CanvasItemGrid *item) +{ + auto it = std::find(canvas_item_grids.begin(), canvas_item_grids.end(), item); + if (it != canvas_item_grids.end()) { + canvas_item_grids.erase(it); + } +} + +Gtk::Widget * +CanvasGrid::newWidget() +{ + Gtk::Box * vbox = Gtk::manage( new Gtk::Box(Gtk::ORIENTATION_VERTICAL) ); + Gtk::Label * namelabel = Gtk::manage(new Gtk::Label("", Gtk::ALIGN_CENTER) ); + + Glib::ustring str("<b>"); + str += getName(); + str += "</b>"; + namelabel->set_markup(str); + vbox->pack_start(*namelabel, false, false); + + _rcb_enabled = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Enabled"), + _("Makes the grid available for working with on the canvas."), + "enabled", _wr, false, repr, doc) ); + // _rcb_enabled serves as a canary that tells us that the widgets have been destroyed + _rcb_enabled->add_destroy_notify_callback(this, &CanvasGrid::notifyWidgetsDestroyed); + + _rcb_snap_visible_only = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("Snap to visible _grid lines only"), + _("When zoomed out, not all grid lines will be displayed. Only the visible ones will be snapped to"), + "snapvisiblegridlinesonly", _wr, false, repr, doc) ); + + _rcb_visible = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Visible"), + _("Determines whether the grid is displayed or not. Objects are still snapped to invisible grids."), + "visible", _wr, false, repr, doc) ); + + _as_alignment = Gtk::manage( new Inkscape::UI::Widget::AlignmentSelector() ); + _as_alignment->on_alignmentClicked().connect(sigc::mem_fun(*this, &CanvasGrid::align_clicked)); + + Gtk::Box *left = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + left->pack_start(*_rcb_enabled, false, false); + left->pack_start(*_rcb_visible, false, false); + left->pack_start(*_rcb_snap_visible_only, false, false); + + if (getGridType() == GRID_RECTANGULAR) { + _rcb_dotted = Gtk::manage( new Inkscape::UI::Widget::RegisteredCheckButton( + _("_Show dots instead of lines"), _("If set, displays dots at gridpoints instead of gridlines"), + "dotted", _wr, false, repr, doc) ); + _rcb_dotted->setActive(render_dotted); + left->pack_start(*_rcb_dotted, false, false); + } + + left->pack_start(*Gtk::manage(new Gtk::Label(_("Align to page:"))), false, false); + left->pack_start(*_as_alignment, false, false); + + auto right = newSpecificWidget(); + right->set_hexpand(false); + + Gtk::Box *inner = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_HORIZONTAL, 4)); + inner->pack_start(*left, true, true); + inner->pack_start(*right, false, false); + vbox->pack_start(*inner, false, false); + vbox->set_border_width(4); + + std::list<Gtk::Widget*> slaves; + for (auto &item : left->get_children()) { + if (item != _rcb_enabled) { + slaves.push_back(item); + } + } + slaves.push_back(right); + _rcb_enabled->setSlaveWidgets(slaves); + + // set widget values + _wr.setUpdating (true); + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + _wr.setUpdating (false); + return dynamic_cast<Gtk::Widget *> (vbox); +} + +/** + * @brief Zeroes out our pointers to the widgets when the widgets are destroyed + * @param data - a pointer to the CanvasGrid object holding the widget pointers + * @return always returns nullptr + */ +void *CanvasGrid::notifyWidgetsDestroyed(void *data) +{ + CanvasGrid *obj = (CanvasGrid *)(data); + obj->_rcb_enabled = nullptr; + obj->_rcb_snap_visible_only = nullptr; + obj->_rcb_visible = nullptr; + obj->_rcb_dotted = nullptr; + obj->_as_alignment = nullptr; + return nullptr; +} + +void +CanvasGrid::on_repr_attr_changed(Inkscape::XML::Node *repr, gchar const *key, gchar const *oldval, gchar const *newval, bool is_interactive, void *data) +{ + if (!data) + return; + + (static_cast<CanvasGrid*>(data))->onReprAttrChanged(repr, key, oldval, newval, is_interactive); +} + +bool CanvasGrid::isEnabled() const +{ + if (snapper == nullptr) { + return false; + } + + return snapper->getEnabled(); +} + +// Used to shift origin when page size changed to fit drawing. +void CanvasGrid::setOrigin(Geom::Point const &origin_px) +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->viewBox.width() / root->width.computed; + scale_y = root->viewBox.height() / root->height.computed; + } + + // Write out in 'user-units' + Inkscape::SVGOStringStream os_x, os_y; + os_x << origin_px[Geom::X] * scale_x; + os_y << origin_px[Geom::Y] * scale_y; + repr->setAttribute("originx", os_x.str()); + repr->setAttribute("originy", os_y.str()); +} + +void CanvasGrid::align_clicked(int align) +{ + Geom::Point dimensions = doc->getDimensions(); + dimensions[Geom::X] *= align % 3 * 0.5; + dimensions[Geom::Y] *= align / 3 * 0.5; + dimensions *= doc->doc2dt(); + setOrigin(dimensions); +} + + + +// ########################################################## +// CanvasXYGrid + +CanvasXYGrid::CanvasXYGrid (SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc) + : CanvasGrid(nv, in_repr, in_doc, GRID_RECTANGULAR) +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + gridunit = unit_table.getUnit(prefs->getString("/options/grids/xy/units")); + if (!gridunit) { + gridunit = unit_table.getUnit("px"); + } + origin[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_x", 0.0), gridunit, "px"); + origin[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/origin_y", 0.0), gridunit, "px"); + color = prefs->getInt("/options/grids/xy/color", GRID_DEFAULT_COLOR); + empcolor = prefs->getInt("/options/grids/xy/empcolor", GRID_DEFAULT_EMPCOLOR); + empspacing = prefs->getInt("/options/grids/xy/empspacing", 5); + spacing[Geom::X] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_x", 0.0), gridunit, "px"); + spacing[Geom::Y] = Inkscape::Util::Quantity::convert(prefs->getDouble("/options/grids/xy/spacing_y", 0.0), gridunit, "px"); + render_dotted = prefs->getBool("/options/grids/xy/dotted", false); + + snapper = new CanvasXYGridSnapper(this, &namedview->snap_manager, 0); + + if (repr) readRepr(); +} + +CanvasXYGrid::~CanvasXYGrid () +{ + if (snapper) delete snapper; +} + +static gboolean sp_nv_read_opacity(gchar const *str, guint32 *color) +{ + if (!str) { + return FALSE; + } + + gchar *u; + gdouble v = g_ascii_strtod(str, &u); + if (!u) { + return FALSE; + } + v = CLAMP(v, 0.0, 1.0); + + *color = (*color & 0xffffff00) | (guint32) floor(v * 255.9999); + + return TRUE; +} + +/** If the passed int is invalid (<=0), then set the widget and the int + to use the given old value. + + @param oldVal Old value to use if the new one is invalid. + @param pTarget The int to validate. + @param widget Widget associated with the int. +*/ +static void validateInt(gint oldVal, + gint* pTarget) +{ + // Avoid nullness. + if ( pTarget == nullptr ) + return; + + // Invalid new value? + if ( *pTarget <= 0 ) { + // If the old value is somehow invalid as well, then default to 1. + if ( oldVal <= 0 ) + oldVal = 1; + + // Reset the int and associated widget to the old value. + *pTarget = oldVal; + } //if + +} //validateInt + +void +CanvasXYGrid::readRepr() +{ + SPRoot *root = doc->getRoot(); + double scale_x = 1.0; + double scale_y = 1.0; + if( root->viewBox_set ) { + scale_x = root->width.computed / root->viewBox.width(); + scale_y = root->height.computed / root->viewBox.height(); + if (Geom::are_near(scale_x / scale_y, 1.0, Geom::EPSILON)) { + // scaling is uniform, try to reduce numerical error + scale_x = (scale_x + scale_y)/2.0; + double scale_none = Inkscape::Util::Quantity::convert(1, doc->getDisplayUnit(), "px"); + if (Geom::are_near(scale_x / scale_none, 1.0, Geom::EPSILON)) + scale_x = scale_none; // objects are same size, reduce numerical error + scale_y = scale_x; + } + } + + gchar const *value; + + if ( (value = repr->attribute("originx")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::X] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + origin[Geom::X] = q.quantity * scale_x; + } + } + + if ( (value = repr->attribute("originy")) ) { + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + origin[Geom::Y] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + origin[Geom::Y] = q.quantity * scale_y; + } + } + + if ( (value = repr->attribute("spacingx")) ) { + + // Ensure a valid default value + if( spacing[Geom::X] <= 0.0 ) + spacing[Geom::X] = 1.0; + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + // Ensure a valid new value + if( q.quantity > 0 ) { + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + spacing[Geom::X] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + spacing[Geom::X] = q.quantity * scale_x; + } + } + } + + if ( (value = repr->attribute("spacingy")) ) { + + // Ensure a valid default value + if( spacing[Geom::Y] <= 0.0 ) + spacing[Geom::Y] = 1.0; + + Inkscape::Util::Quantity q = unit_table.parseQuantity(value); + // Ensure a valid new value + if( q.quantity > 0 ) { + if( q.unit->type == UNIT_TYPE_LINEAR ) { + // Legacy grid not in 'user units' + spacing[Geom::Y] = q.value("px"); + legacy = true; + if (q.unit->abbr == "px" ) { + pixel = true; + } + } else { + // Grid in 'user units' + spacing[Geom::Y] = q.quantity * scale_y; + } + } + } + + if ( (value = repr->attribute("color")) ) { + color = (color & 0xff) | sp_svg_read_color(value, color); + } + + if ( (value = repr->attribute("empcolor")) ) { + empcolor = (empcolor & 0xff) | sp_svg_read_color(value, empcolor); + } + + if ( (value = repr->attribute("opacity")) ) { + sp_nv_read_opacity(value, &color); + } + if ( (value = repr->attribute("empopacity")) ) { + sp_nv_read_opacity(value, &empcolor); + } + + if ( (value = repr->attribute("empspacing")) ) { + gint oldVal = empspacing; + empspacing = atoi(value); + validateInt( oldVal, &empspacing); + } + + if ( (value = repr->attribute("dotted")) ) { + render_dotted = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("visible")) ) { + visible = (strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("enabled")) ) { + g_assert(snapper != nullptr); + snapper->setEnabled(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("snapvisiblegridlinesonly")) ) { + g_assert(snapper != nullptr); + snapper->setSnapVisibleOnly(strcmp(value,"false") != 0 && strcmp(value, "0") != 0); + } + + if ( (value = repr->attribute("units")) ) { + gridunit = unit_table.getUnit(value); // Display unit identifier in grid menu + } + + for (auto grid : canvas_item_grids) { + grid->request_update(); + } + + return; +} + +/** + * Called when XML node attribute changed; updates dialog widgets if change was not done by widgets themselves. + */ +void +CanvasXYGrid::onReprAttrChanged(Inkscape::XML::Node */*repr*/, gchar const */*key*/, gchar const */*oldval*/, gchar const */*newval*/, bool /*is_interactive*/) +{ + readRepr(); + updateWidgets(); +} + + +Gtk::Widget * +CanvasXYGrid::newSpecificWidget() +{ + _rumg = Gtk::manage( new Inkscape::UI::Widget::RegisteredUnitMenu( + _("Grid _units:"), "units", _wr, repr, doc) ); + _rsu_ox = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("_Origin X:"), _("X coordinate of grid origin"), "originx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_oy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("O_rigin Y:"), _("Y coordinate of grid origin"), "originy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + _rsu_sx = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _X:"), _("Distance between vertical grid lines"), "spacingx", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_x) ); + _rsu_sy = Gtk::manage( new Inkscape::UI::Widget::RegisteredScalarUnit( + _("Spacing _Y:"), _("Distance between horizontal grid lines"), "spacingy", + *_rumg, _wr, repr, doc, Inkscape::UI::Widget::RSU_y) ); + + _rcp_gcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Minor grid line _color:"), _("Minor grid line color"), _("Color of the minor grid lines"), + "color", "opacity", _wr, repr, doc) ); + + _rcp_gmcol = Gtk::manage( new Inkscape::UI::Widget::RegisteredColorPicker( + _("Ma_jor grid line color:"), _("Major grid line color"), + _("Color of the major (highlighted) grid lines"), "empcolor", "empopacity", + _wr, repr, doc) ); + + _rsi = Gtk::manage( new Inkscape::UI::Widget::RegisteredSuffixedInteger( + _("_Major grid line every:"), "", _("lines"), "empspacing", _wr, repr, doc) ); + + _rumg->set_hexpand(); + _rsu_ox->set_hexpand(); + _rsu_oy->set_hexpand(); + _rsu_sx->set_hexpand(); + _rsu_sy->set_hexpand(); + _rcp_gcol->set_hexpand(); + _rcp_gmcol->set_hexpand(); + _rsi->set_hexpand(); + + // set widget values + _wr.setUpdating (true); + + _rsu_ox->setDigits(5); + _rsu_ox->setIncrements(0.1, 1.0); + + _rsu_oy->setDigits(5); + _rsu_oy->setIncrements(0.1, 1.0); + + _rsu_sx->setDigits(5); + _rsu_sx->setIncrements(0.1, 1.0); + + _rsu_sy->setDigits(5); + _rsu_sy->setIncrements(0.1, 1.0); + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + val = spacing[Geom::X]; + double gridx = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sx->setValue (gridx); + val = spacing[Geom::Y]; + double gridy = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (gridy); + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + + _wr.setUpdating (false); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sx->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + + Gtk::Box *column = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL, 4)); + column->pack_start(*_rumg, true, false); + column->pack_start(*_rsu_ox, true, false); + column->pack_start(*_rsu_oy, true, false); + column->pack_start(*_rsu_sx, true, false); + column->pack_start(*_rsu_sy, true, false); + column->pack_start(*_rcp_gcol, true, false); + column->pack_start(*_rcp_gmcol, true, false); + column->pack_start(*_rsi, true, false); + + return column; +} + + +/** + * Update dialog widgets from object's values. + */ +void +CanvasXYGrid::updateWidgets() +{ + if (_wr.isUpdating()) return; + + //no widgets (grid created with the document, not with the dialog) + if (!_rcb_visible) return; + + _wr.setUpdating (true); + + _rcb_visible->setActive(visible); + if (snapper != nullptr) { + _rcb_enabled->setActive(snapper->getEnabled()); + _rcb_snap_visible_only->setActive(snapper->getSnapVisibleOnly()); + } + + _rumg->setUnit (gridunit->abbr); + + gdouble val; + + val = origin[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_ox->setValue (val); + + val = origin[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_oy->setValue (val); + + val = spacing[Geom::X]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sx->setValue (val); + + val = spacing[Geom::Y]; + val = Inkscape::Util::Quantity::convert(val, "px", gridunit); + _rsu_sy->setValue (val); + + _rsu_ox->setProgrammatically = false; + _rsu_oy->setProgrammatically = false; + _rsu_sx->setProgrammatically = false; + _rsu_sy->setProgrammatically = false; + + _rcp_gcol->setRgba32 (color); + _rcp_gmcol->setRgba32 (empcolor); + _rsi->setValue (empspacing); + _rcb_dotted->setActive (render_dotted); + + _wr.setUpdating (false); +} + +// For correcting old SVG Inkscape files +void +CanvasXYGrid::Scale (Geom::Scale const &scale ) { + origin *= scale; + spacing *= scale; + + // Write out in 'user-units' + Inkscape::SVGOStringStream os_x, os_y, ss_x, ss_y; + os_x << origin[Geom::X]; + os_y << origin[Geom::Y]; + ss_x << spacing[Geom::X]; + ss_y << spacing[Geom::Y]; + repr->setAttribute("originx", os_x.str()); + repr->setAttribute("originy", os_y.str()); + repr->setAttribute("spacingx", ss_x.str()); + repr->setAttribute("spacingy", ss_y.str()); +} + +void +CanvasXYGrid::Update (Geom::Affine const &affine, unsigned int /*flags*/) +{ + 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; dim < 2; dim++) { + gint scaling_factor = empspacing; + + if (scaling_factor <= 1) + scaling_factor = 5; + + scaled[dim] = false; + while (fabs(sw[dim].length()) < 8.0) { + scaled[dim] = true; + sw[dim] *= scaling_factor; + /* First pass, go up to the major line spacing, then + keep increasing by two. */ + scaling_factor = 2; + } + } +} + + +// 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 (unsigned i = 0; i < 4; ++i) { + Geom::LineSegment side( rect.corner(i), 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) { + intersections.clear(); + intersections.push_back( side.pointAt(0) ); + intersections.push_back( side.pointAt(1) ); + return intersections; + } + } + return intersections; +} + +// 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 ) { + Geom::Coord t = line.nearestTime( point ); + Geom::Point p = line.pointAt(t); + double distance = Geom::distance( p, point ); + if ( Geom::cross( Geom::Line( p, point ).versor(), line.versor() ) < 0.0 ) { + distance = -distance; + } + return distance; +} + +void +CanvasXYGrid::Render (Inkscape::CanvasItemBuffer *buf) +{ + + // no_emphasize_when_zoomedout determines color (minor or major) when only major grid lines/dots shown. + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + guint32 _empcolor; + guint32 _color = color; + bool no_emp_when_zoomed_out = prefs->getBool("/options/grids/no_emphasize_when_zoomedout", false); + if( (scaled[Geom::X] || scaled[Geom::Y]) && no_emp_when_zoomed_out ) { + _empcolor = color; + } else { + _empcolor = empcolor; + } + + bool xrayactive = prefs->getBool("/desktop/xrayactive", false); + if (xrayactive) { + guint32 bg = 0xffffffff; + _color = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_R_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_G_F(_color)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_color)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_color) * SP_RGBA32_B_F(_color)), 0.0, 1.0), + 1.0); + _empcolor = SP_RGBA32_F_COMPOSE( + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_R_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_R_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_G_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_G_F(_empcolor)), 0.0, 1.0), + CLAMP(((1 - SP_RGBA32_A_F(_empcolor)) * SP_RGBA32_B_F(bg)) + (SP_RGBA32_A_F(_empcolor) * SP_RGBA32_B_F(_empcolor)), 0.0, 1.0), + 1.0); + } + + 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); + + // Adding a 2 px margin to the buffer rectangle to avoid missing intersections (in case of rounding errors, and due to adding 0.5 below) + Geom::IntRect buf_rect_with_margin = buf->rect; + buf_rect_with_margin.expandBy(2); + + for (unsigned dim = 0; dim < 2; ++dim) { + + // std::cout << "\n " << (dim==0?"Horizontal":"Vertical") << " ------------" << std::endl; + + // 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[(dim+1)%2] ); + + double spacing = sw[(dim+1)%2].length(); // Spacing between grid lines. + double dash = sw[dim].length(); // Total length of dash pattern. + + // std::cout << " axis: " << axis.origin() << ", " << axis.vector() << std::endl; + // std::cout << " spacing: " << spacing << std::endl; + // std::cout << " dash period: " << dash << std::endl; + + // Find the minimum and maximum distances of the buffer corners from axis. + double min = Geom::infinity(); + double max = -Geom::infinity(); + for (unsigned 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; + } + + if (distance < min) + min = distance; + if (distance > max) + max = distance; + } + int start = floor( min/spacing ); + int stop = floor( max/spacing ); + + // std::cout << " rect: " << buf->rect << std::endl; + // std::cout << " min: " << min << " max: " << max << std::endl; + // std::cout << " start: " << start << " stop: " << stop << std::endl; + + // 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[(dim+1)%2], 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 CanvasXYGrid::Render(), and the drawing of the rulers + buf->cr->move_to(floor(x[0][Geom::X]) + 0.5, floor(x[0][Geom::Y]) + 0.5); + buf->cr->line_to(floor(x[1][Geom::X]) + 0.5, floor(x[1][Geom::Y]) + 0.5); + + // Set dash pattern and color. + if (render_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. + guint32 _empdot = (_empcolor & 0xff) << 2; + if (_empdot > 0xff) + _empdot = 0xff; + _empdot += (_empcolor & 0xffffff00); + + guint32 _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 = fmod( signed_distance( x[0], orth ), sw[dim].length()); + if (Geom::cross( axis.vector(), orth.vector() ) > 0 ) { + offset = -offset; + } + + std::vector<double> dashes; + if (!scaled[dim] && (j % empspacing) != 0) { + // 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 + + // Set color + if (!scaled[dim] && (j % empspacing) != 0) { + buf->cr->set_source_rgba(SP_RGBA32_R_F(_color), SP_RGBA32_G_F(_color), + SP_RGBA32_B_F(_color), SP_RGBA32_A_F(_color)); + } else { + buf->cr->set_source_rgba(SP_RGBA32_R_F(_empcolor), SP_RGBA32_G_F(_empcolor), + SP_RGBA32_B_F(_empcolor), SP_RGBA32_A_F(_empcolor)); + } + } + + buf->cr->stroke(); + + } else { + std::cerr << "CanvasXYGrid::render: Grid line doesn't intersect!" << std::endl; + } + } + } + + buf->cr->restore(); +} + +CanvasXYGridSnapper::CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d) : LineSnapper(sm, d) +{ + this->grid = grid; +} + +/** + * \return Snap tolerance (desktop coordinates); depends on current zoom so that it's always the same in screen pixels + */ +Geom::Coord CanvasXYGridSnapper::getSnapperTolerance() const +{ + SPDesktop const *dt = _snapmanager->getDesktop(); + double const zoom = dt ? dt->current_zoom() : 1; + return _snapmanager->snapprefs.getGridTolerance() / zoom; +} + +bool CanvasXYGridSnapper::getSnapperAlwaysSnap() const +{ + return _snapmanager->snapprefs.getGridTolerance() == 10000; //TODO: Replace this threshold of 10000 by a constant; see also tolerance-slider.cpp +} + +LineSnapper::LineList +CanvasXYGridSnapper::_getSnapLines(Geom::Point const &p) const +{ + LineList s; + + if ( grid == nullptr ) { + return s; + } + + for (unsigned int i = 0; i < 2; ++i) { + + double spacing; + + if (getSnapVisibleOnly()) { + // Only snapping to visible grid lines + spacing = grid->sw[i].length(); // this is the spacing of the visible grid lines measured in screen pixels + // convert screen pixels to px + // FIXME: after we switch to snapping dist in screen pixels, this will be unnecessary + SPDesktop const *dt = _snapmanager->getDesktop(); + if (dt) { + spacing /= dt->current_zoom(); + } + } else { + // Snapping to any grid line, whether it's visible or not + spacing = grid->spacing[i]; + } + + Geom::Coord rounded; + Geom::Point point_on_line; + Geom::Point cvec(0.,0.); + cvec[i] = 1.; + + rounded = Inkscape::Util::round_to_upper_multiple_plus(p[i], spacing, grid->origin[i]); + point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0); + s.push_back(std::make_pair(cvec, point_on_line)); + + rounded = Inkscape::Util::round_to_lower_multiple_plus(p[i], spacing, grid->origin[i]); + point_on_line = i ? Geom::Point(0, rounded) : Geom::Point(rounded, 0); + s.push_back(std::make_pair(cvec, point_on_line)); + } + + return s; +} + +void CanvasXYGridSnapper::_addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, Geom::Point const &point_on_line) const +{ + SnappedLine dummy = SnappedLine(snapped_point, snapped_distance, source, source_num, Inkscape::SNAPTARGET_GRID, getSnapperTolerance(), getSnapperAlwaysSnap(), normal_to_line, point_on_line); + isr.grid_lines.push_back(dummy); +} + +void CanvasXYGridSnapper::_addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +void CanvasXYGridSnapper::_addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const +{ + SnappedPoint dummy = SnappedPoint(snapped_point, source, source_num, Inkscape::SNAPTARGET_GRID_PERPENDICULAR, snapped_distance, getSnapperTolerance(), getSnapperAlwaysSnap(), constrained_snap, true); + isr.points.push_back(dummy); +} + +/** + * \return true if this Snapper will snap at least one kind of point. + */ +bool CanvasXYGridSnapper::ThisSnapperMightSnap() const +{ + return _snap_enabled && _snapmanager->snapprefs.isTargetSnappable(Inkscape::SNAPTARGET_GRID); +} + +} // 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:fileencoding=utf-8:textwidth=99 : diff --git a/src/display/control/canvas-grid.h b/src/display/control/canvas-grid.h new file mode 100644 index 0000000..860a106 --- /dev/null +++ b/src/display/control/canvas-grid.h @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** @file + * Cartesian grid item for the Inkscape canvas. + *//* + * Authors: see git history + * + * Copyright (C) 2018 Authors + * Copyright (C) Johan Engelen 2006-2007 <johan@shouraizou.nl> + * Copyright (C) Lauris Kaplinski 2000 + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#ifndef INKSCAPE_CANVAS_GRID_H +#define INKSCAPE_CANVAS_GRID_H + +#include "ui/widget/alignment-selector.h" +#include "ui/widget/registered-widget.h" +#include "ui/widget/registry.h" +#include "line-snapper.h" + +class SPDesktop; +class SPNamedView; +class SPDocument; + +namespace Gtk { + class Widget; +} + +namespace Inkscape { + +class Snapper; +class CanvasItemBuffer; +class CanvasItemGrid; + +namespace XML { +class Node; +} + +namespace Util { +class Unit; +} + +enum GridType { + GRID_RECTANGULAR = 0, + GRID_AXONOMETRIC = 1 +}; + +#define GRID_MAXTYPENR 1 + +#define GRID_DEFAULT_COLOR 0x0099e526 +#define GRID_DEFAULT_EMPCOLOR 0x0099e54d + +class CanvasGrid { +public: + virtual ~CanvasGrid(); + + // TODO: see effect.h and effect.cpp from live_effects how to link enums to SVGname to typename properly. (johan) + const char * getName() const; + const char * getSVGName() const; + GridType getGridType() const; + static const char * getName(GridType type); + static const char * getSVGName(GridType type); + static GridType getGridTypeFromSVGName(const char * typestr); + static GridType getGridTypeFromName(const char * typestr); + + static CanvasGrid* NewGrid(SPNamedView * nv, Inkscape::XML::Node * repr, SPDocument *doc, GridType gridtype); + static void writeNewGridToRepr(Inkscape::XML::Node * repr, SPDocument * doc, GridType gridtype); + + Inkscape::CanvasItemGrid * createCanvasItem(SPDesktop * desktop); + void removeCanvasItem(Inkscape::CanvasItemGrid *item); + + virtual void Update (Geom::Affine const &affine, unsigned int flags) = 0; + virtual void Render (Inkscape::CanvasItemBuffer *buf) = 0; + + virtual void readRepr() = 0; + virtual void onReprAttrChanged (Inkscape::XML::Node * /*repr*/, char const */*key*/, char const */*oldval*/, char const */*newval*/, bool /*is_interactive*/) = 0; + + Gtk::Widget * newWidget(); + static void *notifyWidgetsDestroyed(void *data); + + void setOrigin(Geom::Point const &origin_px); /**< writes new origin (specified in px units) to SVG */ + Geom::Point origin; /**< Origin of the grid */ + + guint32 color; /**< Color for normal lines */ + guint32 empcolor; /**< Color for emphasis lines */ + gint empspacing; /**< Spacing between emphasis lines */ + + Inkscape::Util::Unit const* gridunit; /**< points to Unit object in UnitTable (so don't delete it) */ + + Inkscape::XML::Node * repr; + SPDocument *doc; + + Inkscape::Snapper* snapper; + + static void on_repr_attr_changed (Inkscape::XML::Node * repr, const gchar *key, const gchar *oldval, const gchar *newval, bool is_interactive, void * data); + + bool isLegacy() const { return legacy; } + bool isPixel() const { return pixel; } + + bool isVisible() const { return (isEnabled() &&visible); }; + bool isEnabled() const; + + void align_clicked(int align); + +protected: + CanvasGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument *in_doc, GridType type); + + virtual Gtk::Widget * newSpecificWidget() = 0; + + std::vector<Inkscape::CanvasItemGrid *> canvas_item_grids; // List of created CanvasGridItem's. + + SPNamedView * namedview; + + Inkscape::UI::Widget::Registry _wr; + bool visible; + bool render_dotted; + + GridType gridtype; + + // For dealing with old Inkscape SVG files (pre 0.92) + bool legacy; + bool pixel; + + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_enabled = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_snap_visible_only = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_visible = nullptr; + Inkscape::UI::Widget::RegisteredCheckButton *_rcb_dotted = nullptr; + Inkscape::UI::Widget::AlignmentSelector *_as_alignment = nullptr; + +private: + CanvasGrid(const CanvasGrid&) = delete; + CanvasGrid& operator=(const CanvasGrid&) = delete; +}; + + +class CanvasXYGrid : public CanvasGrid { +public: + CanvasXYGrid(SPNamedView * nv, Inkscape::XML::Node * in_repr, SPDocument * in_doc); + ~CanvasXYGrid() override; + + virtual void Scale (Geom::Scale const &scale); + void Update (Geom::Affine const &affine, unsigned int flags) override; + void Render (Inkscape::CanvasItemBuffer *buf) override; + + void readRepr() override; + void onReprAttrChanged (Inkscape::XML::Node * repr, char const *key, char const *oldval, char const *newval, bool is_interactive) override; + + Geom::Point spacing; /**< Spacing between elements of the grid */ + 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 */ + +protected: + Gtk::Widget * newSpecificWidget() override; + +private: + CanvasXYGrid(const CanvasXYGrid&) = delete; + CanvasXYGrid& operator=(const CanvasXYGrid&) = delete; + + void updateWidgets(); + + Inkscape::UI::Widget::RegisteredUnitMenu *_rumg = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_ox = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_oy = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sx = nullptr; + Inkscape::UI::Widget::RegisteredScalarUnit *_rsu_sy = nullptr; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gcol = nullptr; + Inkscape::UI::Widget::RegisteredColorPicker *_rcp_gmcol = nullptr; + Inkscape::UI::Widget::RegisteredSuffixedInteger *_rsi = nullptr; +}; + + + +class CanvasXYGridSnapper : public LineSnapper +{ +public: + CanvasXYGridSnapper(CanvasXYGrid *grid, SnapManager *sm, Geom::Coord const d); + bool ThisSnapperMightSnap() const override; + + Geom::Coord getSnapperTolerance() const override; //returns the tolerance of the snapper in screen pixels (i.e. independent of zoom) + bool getSnapperAlwaysSnap() const override; //if true, then the snapper will always snap, regardless of its tolerance + +private: + LineList _getSnapLines(Geom::Point const &p) const override; + void _addSnappedLine(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, Geom::Point const &normal_to_line, const Geom::Point &point_on_line) const override; + void _addSnappedPoint(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + void _addSnappedLinePerpendicularly(IntermSnapResults &isr, Geom::Point const &snapped_point, Geom::Coord const &snapped_distance, SnapSourceType const &source, long source_num, bool constrained_snap) const override; + CanvasXYGrid *grid; +}; + +}; /* namespace Inkscape */ + + + + +#endif diff --git a/src/display/control/canvas-item-bpath.cpp b/src/display/control/canvas-item-bpath.cpp new file mode 100644 index 0000000..6938ac5 --- /dev/null +++ b/src/display/control/canvas-item-bpath.cpp @@ -0,0 +1,262 @@ +// 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 <memory> // unique_ptr +#include <utility> // std::move + +#include "color.h" // SP_RGBA_x_F + +#include "display/curve.h" +#include "display/cairo-utils.h" + +#include "helper/geom.h" // bounds_exact_transformed() + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control bpath. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemBpath:Null"; + _pickable = true; // For now, nobody gets events from this class! +} + +/** + * Create a control bpath. Curve is in document coordinates. + */ +CanvasItemBpath::CanvasItemBpath(CanvasItemGroup *group, SPCurve *curve, bool phantom_line) + : CanvasItem(group) + , _phantom_line(phantom_line) +{ + _name = "CanvasItemBpath"; + _pickable = true; // For now, everyone gets events from this class! + if (curve) { + _path = curve->get_pathvector(); + } + + request_update(); // Render immediately or temporary bpaths won't show. +} + +/** + * 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 *curve, bool phantom_line) +{ + if (curve) { // No test to see if *curve == *_curve so we always do the swap. + _path = curve->get_pathvector(); + } else { + _path.clear(); + } + + _phantom_line = phantom_line; + + request_update(); +} + +/** + * Set a control bpath. Path is in document coordinates. + */ +void CanvasItemBpath::set_bpath(Geom::PathVector const &path, bool phantom_line) +{ + _path = path; + _phantom_line = phantom_line; + + request_update(); +} + +/** + * Set the fill color and fill rule. + */ +void CanvasItemBpath::set_fill(guint rgba, SPWindRule fill_rule) +{ + if (_fill != rgba || _fill_rule != fill_rule) { + _fill = rgba; + _fill_rule = fill_rule; + request_redraw(); + } +} + +/** + * Returns distance between point in canvas units and nearest point on bpath. + */ +double CanvasItemBpath::closest_distance_to(Geom::Point const &p) +{ + 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. + } + + return closest_distance_to(p) < tolerance; +} + +/** + * Update and redraw control bpath. + */ +void CanvasItemBpath::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Get new bounds + _affine = affine; + + _bounds = Geom::Rect(); // Reset bounds + + if (_path.empty()) return; // No path, no chocolate! + + Geom::OptRect bbox = bounds_exact_transformed(_path, _affine); + + if (bbox) { + _bounds = *bbox; + _bounds.expandBy(2); + } else { + _bounds = Geom::Rect(); + } + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + +/** + * Render bpath to screen via Cairo. + */ +void CanvasItemBpath::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemBpath::Render: No buffer!" << std::endl; + return; + } + + if (!_visible) { + // Hidden + return; + } + + if (_path.empty()) { + return; + } + + 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, + /* optimized_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(1.0); + buf->cr->stroke(); + + } else { + buf->cr->begin_new_path(); // Clears path + } + + // 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-bpath.h b/src/display/control/canvas-item-bpath.h new file mode 100644 index 0000000..e430cc1 --- /dev/null +++ b/src/display/control/canvas-item-bpath.h @@ -0,0 +1,83 @@ +// 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 CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemBpath : public CanvasItem { + +public: + CanvasItemBpath(CanvasItemGroup *group); + CanvasItemBpath(CanvasItemGroup *group, SPCurve *curve, bool phantom_line = false); + CanvasItemBpath(CanvasItemGroup *group, Geom::PathVector path, bool phantom_line = false); + + // Geometry + void set_bpath (SPCurve *curve, bool phantom_line = false); + void set_bpath (Geom::PathVector const &path, bool phantom_line = false); + void set_affine_absolute(Geom::Affine const &affine); + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void set_fill (guint32 rgba, SPWindRule fill_rule); + void set_dashes (std::vector<double> & dashes) { _dashes = dashes; } + +protected: + + // Geometry + Geom::PathVector _path; + + // Properties + SPWindRule _fill_rule = SP_WIND_RULE_EVENODD; + std::vector<double> _dashes; + bool _phantom_line = false; +}; + + +} // 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..3f829b0 --- /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> +#include "display/rendermode.h" + +namespace Inkscape { + +/** + * Class used when rendering canvas items. + */ +struct CanvasItemBuffer { + Geom::IntRect rect; + int device_scale; // For high DPI monitors. + bool outline_overlay_pass; // Hack for not painting page colour in outline overlay mode + Cairo::RefPtr<Cairo::Context> cr; +}; + +} // 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..91bd323 --- /dev/null +++ b/src/display/control/canvas-item-catchall.cpp @@ -0,0 +1,81 @@ +// 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" + +#include "color.h" // SP_RGBA_x_F + +#include "ui/widget/canvas.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! + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); +} + +/** + * Returns distance between point in canvas units and nearest point on catchall. + */ +double CanvasItemCatchall::closest_distance_to(Geom::Point const &p) +{ + return 0.0; // We're everywhere! +} + +/** + * 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(Geom::Affine const &affine) +{ + // No geometry to update. + _affine = affine; +} + +/** + * Render catchall to screen via Cairo. + */ +void CanvasItemCatchall::render(Inkscape::CanvasItemBuffer *buf) +{ + // 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..d40ab45 --- /dev/null +++ b/src/display/control/canvas-item-catchall.h @@ -0,0 +1,60 @@ +// 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 <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemCatchall : public CanvasItem { + +public: + CanvasItemCatchall(CanvasItemGroup *group); + + // Geometry + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) 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-ctrl.cpp b/src/display/control/canvas-item-ctrl.cpp new file mode 100644 index 0000000..f2696c8 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.cpp @@ -0,0 +1,1221 @@ +// 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 "preferences.h" // Default size. +#include "display/cairo-utils.h" // argb32_from_rgba() + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +CanvasItemCtrl::~CanvasItemCtrl() +{ + delete[] _cache; +} + +/** + * Create an 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, Inkscape::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, Inkscape::CanvasItemCtrlType type, Geom::Point const &p) + : CanvasItemCtrl(group, type) +{ + _position = p; +} + + +/** + * Create a control ctrl. + */ +CanvasItemCtrl::CanvasItemCtrl(CanvasItemGroup *group, Inkscape::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, Inkscape::CanvasItemCtrlShape shape, Geom::Point const &p) + : CanvasItemCtrl(group, shape) +{ + _position = p; + request_update(); +} + +/** + * 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; + if (_position != position) { + _position = position; + request_update(); + } +} + +/** + * Returns distance between point in canvas units and position of ctrl. + */ +double CanvasItemCtrl::closest_distance_to(Geom::Point const &p) +{ + // TODO: Different criteria for different shapes. + Geom::Point position = _position * _affine; + return Geom::distance(p, position); +} + +/** + * 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 (tolerance == 0) { + return _bounds.interiorContains(p); + } else { + return closest_distance_to(p) <= tolerance; + } +} + +/** + * Update and redraw control ctrl. + */ +void CanvasItemCtrl::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Get new bounds + _affine = affine; + + // We must be pixel aligned, width and height are always odd. + _bounds = Geom::Rect::from_xywh(-(_width/2.0 - 0.5), -(_height/2.0 - 0.5), _width, _height); + + // Adjust for anchor + int w_half = _width/2; + int h_half = _height/2; + 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: + { + + _angle = _anchor * M_PI/4.0 + std::atan2(_affine[1], _affine[0]); + + double 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.0; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + _angle += M_PI/2.0; + break; + + case CANVAS_ITEM_CTRL_SHAPE_SALIGN: + dx = - (half/2 + 2) * cos(_angle); + dy = - (half/2 + 2) * sin(_angle); + _angle -= M_PI/2.0; + break; + + case CANVAS_ITEM_CTRL_SHAPE_CALIGN: + _angle -= M_PI/4.0; + dx = (half/2 + 2) * ( sin(_angle) - cos(_angle)); + dy = (half/2 + 2) * (-sin(_angle) - cos(_angle)); + break; + + default: + break; + } + + _built = false; // Angle may have change, must rebuild! + + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_PIVOT: + case CANVAS_ITEM_CTRL_SHAPE_MALIGN: + + _angle = std::atan2(_affine[1], _affine[0]); + + _built = false; // Angle may have change, must rebuild! + + 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; + } + + _bounds *= Geom::Translate(Geom::IntPoint(dx, dy)); + + // Position must also be integer. + Geom::Point position = _position * _affine; + Geom::IntPoint iposition = position.floor(); + + _bounds *= Geom::Translate(iposition); + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + + +static inline guint32 compose_xor(guint32 bg, guint32 fg, guint32 a) +{ + guint32 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(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemCtrl::Render: No buffer!" << std::endl; + return; + } + + if (!_bounds.intersects(buf->rect)) { + return; // Control not inside buffer rectangle. + } + + if (!_visible) { + return; // Hidden. + } + + if (!_built) { + 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 + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); + guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); + guint32 *p = _cache; + for (int i = 0; i < height; ++i) { + guint32 *pb = reinterpret_cast<guint32*>(pxb + i*strideb); + for (int j = 0; j < width; ++j) { + guint32 base = *pb; + guint32 cc = *p++; + guint32 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; + } + guint32 ro = compose_xor(rb, (cc & 0xff000000) >> 24, ac); + guint32 go = compose_xor(gb, (cc & 0x00ff0000) >> 16, ac); + guint32 bo = compose_xor(bb, (cc & 0x0000ff00) >> 8, ac); + if (_mode == CANVAS_ITEM_CTRL_MODE_GRAYSCALED_XOR || + _mode == CANVAS_ITEM_CTRL_MODE_DESATURATED_XOR) { + guint32 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(guint32 rgba) +{ + if (_fill != rgba) { + _fill = rgba; + _built = false; + request_redraw(); + } +} + +void CanvasItemCtrl::set_stroke(guint32 rgba) +{ + if (_stroke != rgba) { + _stroke = rgba; + _built = false; + request_redraw(); + } +} + +void CanvasItemCtrl::set_shape(int shape) +{ + if (_shape != shape) { + _shape = Inkscape::CanvasItemCtrlShape(shape); // Fixme + _built = false; + 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: + _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(int mode) +{ + if (_mode != mode) { + _mode = Inkscape::CanvasItemCtrlMode(mode); // Fixme + _built = false; + request_update(); + } +} + +void CanvasItemCtrl::set_pixbuf(GdkPixbuf *pixbuf) +{ + if (_pixbuf != pixbuf) { + _pixbuf = pixbuf; + _width = gdk_pixbuf_get_width(pixbuf); + _height = gdk_pixbuf_get_height(pixbuf); + _built = false; + request_update(); + } +} + +// Nominally width == height == size except possibly for pixmaps. +void CanvasItemCtrl::set_size(int size) +{ + if (_pixbuf) { + // std::cerr << "CanvasItemCtrl::set_size: Attempting to set size on pixbuf control!" << std::endl; + return; + } + if (_width != size + _extra || _height != size + _extra) { + _width = size + _extra; + _height = size + _extra; + _built = false; + 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_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() +{ + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + int size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 15); + set_size_via_index(size); +} + +void CanvasItemCtrl::set_size_extra(int extra) +{ + if (_extra != extra && _pixbuf == nullptr) { // Don't enlarge pixbuf! + _width += (extra - _extra); + _height += (extra - _extra); + _extra = extra; + _built = false; + request_update(); // Geometry change + } +} + +void CanvasItemCtrl::set_type(Inkscape::CanvasItemCtrlType type) +{ + if (_type != type) { + _type = type; + + // Use _type to set default values: + set_shape_default(); + set_size_default(); + _built = false; + request_update(); // Possible Geometry change + } +} + +void CanvasItemCtrl::set_angle(double angle) +{ + if (_angle != angle) { + _angle = angle; + request_update(); // Geometry change + } +} + +void CanvasItemCtrl::set_anchor(SPAnchorType anchor) +{ + if (_anchor != anchor) { + _anchor = anchor; + request_update(); // Geometry change + } +} + +// ---------- Protected ---------- + +// Helper function for build_cache(): +bool point_inside_triangle(Geom::Point p1,Geom::Point p2,Geom::Point p3, Geom::Point point){ + using Geom::X; + using Geom::Y; + double denominator = (p1[X]*(p2[Y] - p3[Y]) + p1[Y]*(p3[X] - p2[X]) + p2[X]*p3[Y] - p2[Y]*p3[X]); + double t1 = (point[X]*(p3[Y] - p1[Y]) + point[Y]*(p1[X] - p3[X]) - p1[X]*p3[Y] + p1[Y]*p3[X]) / denominator; + double t2 = (point[X]*(p2[Y] - p1[Y]) + point[Y]*(p1[X] - p2[X]) - p1[X]*p2[Y] + p1[Y]*p2[X]) / -denominator; + double see = t1 + t2; + return 0 <= t1 && t1 <= 1 && 0 <= t2 && t2 <= 1 && see <= 1; +} + + +void draw_darrow(Cairo::RefPtr<Cairo::Context>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(); +} + +void draw_carrow(Cairo::RefPtr<Cairo::Context>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(); +} + +void draw_pivot(Cairo::RefPtr<Cairo::Context>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); +} + +void draw_salign(Cairo::RefPtr<Cairo::Context>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(); +} + +void draw_calign(Cairo::RefPtr<Cairo::Context>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(); +} + +void draw_malign(Cairo::RefPtr<Cairo::Context>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) +{ + if (_width < 2 || _height < 2) { + return; // Nothing to render + } + + // Get colors + guint32 fill = 0x0; + guint32 stroke = 0x0; + fill = _fill; + stroke = _stroke; + + 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; + + if (_cache) delete[] _cache; + _cache = new guint32[size]; + guint32 *p = _cache; + + 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; + } + } + } + _built = true; + 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; + } + } + } + _built = true; + 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; + } + } + } + _built = true; + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_TRIANGLE: + { + Geom::Affine m = Geom::Translate(Geom::Point(-width/2.0,-height/2.0)); + m *= Geom::Rotate(-_angle); + m *= Geom::Translate(Geom::Point(width/2.0, height/2.0)); + + // Construct an arrowhead (triangle) of maximum size that won't leak out of rectangle + // defined by width and height, assuming width == height. + double w2 = width/2.0; + double h2 = height/2.0; + double w2cos = w2 * cos( M_PI/6 ); + double h2sin = h2 * sin( M_PI/6 ); + Geom::Point p1s(0, h2); + Geom::Point p2s(w2 + w2cos, h2 + h2sin); + Geom::Point p3s(w2 + w2cos, h2 - h2sin); + + // Needed for constructing smaller arrowhead below. + double theta = atan2( Geom::Point( p2s - p1s ) ); + + p1s *= m; + p2s *= m; + p3s *= m; + + // Construct a smaller arrow head for fill. + Geom::Point p1f(device_scale/sin(theta), h2); + Geom::Point p2f(w2 + w2cos, h2 - h2sin + device_scale/cos(theta)); + Geom::Point p3f(w2 + w2cos, h2 + h2sin - device_scale/cos(theta)); + p1f *= m; + p2f *= m; + p3f *= m; + + for(int y = 0; y < height; y++) { + for(int x = 0; x < width; x++) { + Geom::Point point = Geom::Point(x+0.5, y+0.5); + + if (point_inside_triangle(p1f, p2f, p3f, point)) { + p[(y*width)+x] = fill; + + } else if (point_inside_triangle(p1s, p2s, p3s, point)) { + p[(y*width)+x] = stroke; + + } else { + p[(y*width)+x] = 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; + } + } + } + _built = true; + 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; + } + } + } + _built = true; + break; + + 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 + switch (_shape) { + case CANVAS_ITEM_CTRL_SHAPE_DARROW: + case CANVAS_ITEM_CTRL_SHAPE_SARROW: + draw_darrow(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(); + guint32 *p = _cache; + for (int i = 0; i < device_scale * size; ++i) { + guint32 *pb = reinterpret_cast<guint32*>(pxb + i*strideb); + for (int j = 0; j < width; ++j) { + + guint32 color = 0x0; + + // Need to un-premultiply alpha and change order argb -> rgba. + guint32 alpha = (*pb & 0xff000000) >> 24; + if (alpha == 0x0) { + color = 0x0; + } else { + guint32 rgb = unpremul_alpha(*pb & 0xffffff, alpha); + color = (rgb << 8) + alpha; + } + + *p++ = color; + pb++; + } + } + _built = true; + break; + } + + case CANVAS_ITEM_CTRL_SHAPE_BITMAP: + { + if (_pixbuf) { + unsigned char* px = gdk_pixbuf_get_pixels (_pixbuf); + unsigned int rs = gdk_pixbuf_get_rowstride (_pixbuf); + 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; + guint32 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) { + guint* p = _cache + + (x * device_scale + i) + // Column + (y * device_scale + j) * width; // Row + *p = color; + } + } + } + } + } else { + std::cerr << "CanvasItemCtrl::build_cache: No bitmap!" << std::endl; + guint *p = _cache; + 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; + } + } + } + } + _built = true; + 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; + } +} + +} // 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..4cccc88 --- /dev/null +++ b/src/display/control/canvas-item-ctrl.h @@ -0,0 +1,107 @@ +// 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 <gdk-pixbuf/gdk-pixbuf.h> +#include <2geom/point.h> + +#include "canvas-item.h" +#include "canvas-item-enums.h" + +#include "enums.h" // SP_ANCHOR_X + +namespace Inkscape { + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemCtrl : public CanvasItem { + +public: + ~CanvasItemCtrl() override; + 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); + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void set_fill(guint32 rgba) override; + void set_stroke(guint32 rgba) override; + void set_shape(int shape); + void set_shape_default(); // Use type to determine shape. + void set_mode(int 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(GdkPixbuf *pixbuf); + +protected: + void build_cache(int device_scale); + + // Geometry + Geom::Point _position; + + // Display + guint32 *_cache = nullptr; + bool _built = false; + + // Properties + CanvasItemCtrlType _type = CANVAS_ITEM_CTRL_TYPE_DEFAULT; + CanvasItemCtrlShape _shape = CANVAS_ITEM_CTRL_SHAPE_SQUARE; + CanvasItemCtrlMode _mode = CANVAS_ITEM_CTRL_MODE_XOR; + unsigned int _width = 5; // Nominally width == height == size... unless we use a pixmap. + unsigned int _height = 5; + unsigned int _extra = 0; // Used to temporarily increase size. + double _angle = 0.0; // Used for triangles, could be used for arrows. + SPAnchorType _anchor = SP_ANCHOR_CENTER; + GdkPixbuf *_pixbuf = nullptr; +}; + + +} // 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..a99a154 --- /dev/null +++ b/src/display/control/canvas-item-curve.cpp @@ -0,0 +1,236 @@ +// 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 "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control curve. + */ +CanvasItemCurve::CanvasItemCurve(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemCurve:Null"; + _pickable = false; // For now, nobody gets events from this class! +} + +/** + * 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"; + _pickable = false; // For now, nobody gets events from this class! + + request_update(); +} + +/** + * 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"; + _pickable = false; // For now, nobody gets events from this class! + + request_update(); +} + +/** + * Set a linear control curve. Points are in document coordinates. + */ +void CanvasItemCurve::set_coords(Geom::Point const &p0, Geom::Point const &p1) +{ + _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) +{ + _name = "CanvasItemCurve:CubicBezier"; + _curve = std::make_unique<Geom::CubicBezier>(p0, p1, p2, p3); + + request_update(); +} + +/** + * Set stroke width. + */ +void CanvasItemCurve::set_width(int w) +{ + width = w; + + request_update(); +} + +/** + * Set background stroke alpha. + */ +void CanvasItemCurve::set_bg_alpha(float alpha) +{ + 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) +{ + 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(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + if (!_curve) { + return; // No curve! See node.h. + } + + // 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. CHECK + // Get new bounds + _affine = affine; + + // Trade off between updating a larger area (typically twice for Beziers?) vs computation time for bounds. + _bounds = _curve->boundsExact(); // Document units. + _bounds *= _affine; // Document to canvas. + _bounds.expandBy(2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + +/** + * Render curve to screen via Cairo. + */ +void CanvasItemCurve::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemCurve::Render: No buffer!" << std::endl; + return; + } + + if (!_curve) { + // Curve not defined (see node.h). + return; + } + + if (!_visible) { + // Hidden + return; + } + + if (_curve->isDegenerate()) { + // Nothing to render! + return; + } + + 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..84d6511 --- /dev/null +++ b/src/display/control/canvas-item-curve.h @@ -0,0 +1,87 @@ +// 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 CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemCurve : 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(Geom::BezierCurve &curve); + void set_width(int w); + void set_bg_alpha(float alpha); + bool is_line() { return _curve->size() == 2; } + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void set_is_fill(bool is_fill) { _is_fill = is_fill; } + bool get_is_fill() { return _is_fill; } + void set_corner0(int corner0) { _corner0 = corner0; } // Used for meshes + int get_corner0() { return _corner0; } + void set_corner1(int corner1) { _corner1 = corner1; } + int get_corner1() { return _corner1; } + +protected: + std::unique_ptr<Geom::BezierCurve> _curve; + bool _is_fill = true; // Fill or stroke, used by meshes. + + 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; + int _corner0 = -1; // For meshes + int _corner1 = -1; // For meshes +}; + + +} // 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..17750b7 --- /dev/null +++ b/src/display/control/canvas-item-drawing.cpp @@ -0,0 +1,296 @@ +// 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 _SPCavasArena. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-drawing.h" + +#include "desktop.h" + +#include "preferences.h" + +#include "display/cairo-utils.h" +#include "display/drawing.h" +#include "display/drawing-context.h" +#include "display/drawing-item.h" +#include "display/drawing-group.h" +#include "display/drawing-surface.h" + +#include "ui/widget/canvas.h" +#include "ui/modifiers.h" + +namespace Inkscape { + +struct CachePref2Observer : public Inkscape::Preferences::Observer { + CachePref2Observer(Inkscape::CanvasItemDrawing *item) + : Inkscape::Preferences::Observer("/options/renderingcache") + , _canvas_item_drawing(item) + { + Inkscape::Preferences *prefs = Inkscape::Preferences::get(); + std::vector<Inkscape::Preferences::Entry> v = prefs->getAllEntries(observed_path); + for (const auto & i : v) { + notify(i); + } + prefs->addObserver(*this); + } + void notify(Inkscape::Preferences::Entry const &v) override { + Glib::ustring name = v.getEntryName(); + if (name == "size") { + _canvas_item_drawing->get_drawing()->setCacheBudget((1 << 20) * v.getIntLimited(64, 0, 4096)); + } + } + Inkscape::CanvasItemDrawing *_canvas_item_drawing; +}; + +/** + * Create the drawing. One per window! + */ +CanvasItemDrawing::CanvasItemDrawing(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemDrawing"; + _pickable = true; + + _drawing = new Inkscape::Drawing(this); + _drawing->delta = 1.0; // Default + auto root = new Inkscape::DrawingGroup(*_drawing); + root->setPickChildren(true); + _drawing->setRoot(root); + + // _drawing->signal_request_update.connect(...); Not needed now. + // _drawing->signal_request_render.connect(...); Not needed now. + // _drawing->signal_item_deleted.connect(...); Not needed now. + + _observer = new CachePref2Observer(this); +} + +CanvasItemDrawing::~CanvasItemDrawing() +{ + delete _observer; + delete _drawing; +} + +/** + * Returns distance between point in canvas units and nearest point on drawing. + */ +double CanvasItemDrawing::closest_distance_to(Geom::Point const &p) +{ + double d = Geom::infinity(); + std::cerr << "CanvasItemDrawing::closest_distance_to: Not implemented!" << std::endl; + return d; +} + +/** + * 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; + } + + _drawing->update(Geom::IntRect::infinite(), DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX); + _picked_item = _drawing->pick(p, _drawing->delta, _sticky); + + if (_picked_item) { + // This will trigger a signal that is handled by our event handler. Seems a bit of a + // round-a-bout 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(Geom::Affine const &affine) +{ + auto new_affine = affine; + + // Correct for y-axis. This should not be here!!!! + if (auto *desktop = _canvas->get_desktop()) { + new_affine = desktop->doc2dt() * new_affine; + } + + // if (_affine == new_affine && !_need_update) { + // // Nothing to do. + // return; + // } + + _ctx.ctm = new_affine; // TODO Remove _ctx.ctm... it's exactly the same as _affine! + + unsigned reset = (_affine != new_affine ? DrawingItem::STATE_ALL : 0); + + _affine = new_affine; + + _drawing->update(Geom::IntRect::infinite(), DrawingItem::STATE_ALL, reset); + + Geom::OptIntRect bbox = _drawing->root()->visualBounds(); + if (bbox) { + _bounds = *bbox; + _bounds.expandBy(1); // Avoid aliasing artifacts. + } + + if (_cursor) { + /* Mess with enter/leave notifiers */ + DrawingItem *new_drawing_item = _drawing->pick(_c, _delta, _sticky); + if (_active_item != new_drawing_item) { + + GdkEventCrossing ec; + ec.window = _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); + } + } + } + + _need_update = false; +} + +/** + * Render drawing to screen via Cairo. + */ +void CanvasItemDrawing::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemDrawing::Render: No buffer!" << std::endl; + return; + } + + if (buf->rect.hasZeroArea()) { + return; + } + + Inkscape::DrawingContext dc(buf->cr->cobj(), buf->rect.min()); + _drawing->update(); + _drawing->render(dc, buf->rect); +} + +/** + * 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); + + /* fixme: Not sure abut this, but seems the right thing (Lauris) */ + //_drawing->update(Geom::IntRect::infinite(), _ctx, DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX, 0); + _active_item = _drawing->pick(_c, _drawing->delta, _sticky); + 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); + + /* fixme: Not sure abut this, but seems the right thing (Lauris) */ + //_drawing->update(Geom::IntRect::infinite(), _ctx, DrawingItem::STATE_PICK | DrawingItem::STATE_BBOX); + + auto new_drawing_item = _drawing->pick(_c, _drawing->delta, _sticky); + 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..6e8a206 --- /dev/null +++ b/src/display/control/canvas-item-drawing.h @@ -0,0 +1,108 @@ +// 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" +#include "display/drawing-item.h" // Only for ctx... which should be remove (it's the same as _affine!). + +namespace Inkscape { + +class CachePref2Observer; + +class Drawing; +class DrawingItem; +class Updatecontext; + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemDrawing : public CanvasItem { + +public: + CanvasItemDrawing(CanvasItemGroup *group); + ~CanvasItemDrawing() override; + + + // Geometry + UpdateContext get_context() { return _ctx; } // TODO Remove this as ctx only contains affine. + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + Inkscape::Drawing * get_drawing() { return _drawing; } + + // 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; } + + // Signals + sigc::connection connect_drawing_event(sigc::slot<bool, GdkEvent*, Inkscape::DrawingItem *> slot) { + return _drawing_event_signal.connect(slot); + } + +protected: + + // Geometry + + // Selection + Geom::Point _c; + double _delta = Geom::infinity(); + Inkscape::DrawingItem *_active_item = nullptr; + Inkscape::DrawingItem *_picked_item = nullptr; + + // Display + Inkscape::Drawing *_drawing; + Inkscape::UpdateContext _ctx; // TODO Remove this... it's the same as _affine! + + // Events + bool _cursor = false; + bool _sticky = false; // Pick anything, even if hidden. + + // Properties + CachePref2Observer *_observer = nullptr; + + // 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..37561b5 --- /dev/null +++ b/src/display/control/canvas-item-enums.h @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_ENUMS_H +#define SEEN_CANVAS_ITEM_ENUMS_H + +/** + * Enums for CanvasControlItem's. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + */ + +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 +}; + +// 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_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..2965b86 --- /dev/null +++ b/src/display/control/canvas-item-grid.cpp @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a grid. All the magic happens elsewhere. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of GridCanvasItem. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-grid.h" + +#include "canvas-grid.h" + +#include "color.h" // SP_RGBA_x_F + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control grid. + */ +CanvasItemGrid::CanvasItemGrid(CanvasItemGroup *group, CanvasGrid *grid) + : CanvasItem(group) + , _grid(grid) +{ + _name = "CanvasItemGrid:"; + _name += grid->getName(grid->getGridType()); + _pickable = false; // For now, nobody gets events from this class! + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + request_update(); // Update affine +} + +/** + * Destructor: must remove ourself from the CanvasGrid's vector of CanvasItemGrids. + */ +CanvasItemGrid::~CanvasItemGrid() +{ + if (_grid) { + _grid->removeCanvasItem(this); + } +} + +/** + * Returns distance between point in canvas units and nearest point on grid. + */ +double CanvasItemGrid::closest_distance_to(Geom::Point const &p) +{ + double d = Geom::infinity(); + std::cerr << "CanvasItemGrid::closest_distance_to: Not implemented!" << std::endl; + return d; +} + +/** + * 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! +} + +/** + * Update and redraw control grid. + */ +void CanvasItemGrid::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + _affine = affine; + _grid->Update(affine, 0); // TODO: Remove flag (not used). + _need_update = false; + + // Queue redraw of grid area + request_redraw(); +} + +/** + * Render grid to screen via Cairo. + */ +void CanvasItemGrid::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemGrid::Render: No buffer!" << std::endl; + return; + } + + if (!_visible) { + // Hidden + return; + } + + if (!_grid->isVisible()) { + // Hidden: Grid code doesn't set CanvasItem::visible! + return; + } + + _grid->Render(buf); +} + +} // 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..f858c57 --- /dev/null +++ b/src/display/control/canvas-item-grid.h @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_GRID_H +#define SEEN_CANVAS_ITEM_GRID_H + +/** + * A class to represent a grid. + */ + +/* + * 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/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasGrid; + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemGrid : public CanvasItem { + +public: + CanvasItemGrid(CanvasItemGroup *group, CanvasGrid *grid); + ~CanvasItemGrid() override; + + // Geometry + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + +protected: + CanvasGrid *_grid = nullptr; +}; + + +} // 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..8e53857 --- /dev/null +++ b/src/display/control/canvas-item-group.cpp @@ -0,0 +1,158 @@ +// 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 "canvas-item-group.h" +#include "canvas-item-ctrl.h" // Update sizes + +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() +{ + while (!items.empty()) { + CanvasItem & item = items.front(); + remove(&item); + } + + if (_parent) { + _parent->remove(this, false); // remove() should not delete this or we'll double delete! + } +} + +void CanvasItemGroup::add(CanvasItem *item) +{ +#ifdef CANVAS_ITEM_DEBUG + std::cout << "CanvasItemGroup::add: " << item->get_name() << " to " << _name << " " << items.size() << std::endl; +#endif + items.push_back(*item); + // canvas request update +} + +void CanvasItemGroup::remove(CanvasItem *item, bool Delete) +{ +#ifdef CANVAS_ITEM_DEBUG + std::cout << "CanvasItemGroup::remove: " << item->get_name() << " from " << _name << " " << items.size() << std::endl; +#endif + auto position = items.iterator_to(*item); + if (position != items.end()) { + position->set_parent(nullptr); + items.erase(position); + if (Delete) { + delete (&*position); // An item directly deleted should not be deleted here. + } + } +} + +void CanvasItemGroup::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + _affine = affine; + _need_update = false; + + _bounds = Geom::Rect(); // Zero + + // Update all children and calculate new bounds. + for (auto & item : items) { + // We don't need to update what is not visible + if (!item.is_visible()) continue; + item.update(_affine); + _bounds.unionWith(item.get_bounds()); + } +} + +void CanvasItemGroup::render(Inkscape::CanvasItemBuffer *buf) +{ + if (_visible) { + if (_bounds.interiorIntersects(buf->rect)) { + 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& p) +{ +#ifdef CANVAS_ITEM_DEBUG + std::cout << "CanvasItemGroup::pick_item:" << std::endl; + std::cout << " PICKING: In group: " << _name << " bounds: " << _bounds << std::endl; +#endif + for (auto item = items.rbegin(); item != items.rend(); ++item) { // C++20 will allow us to loop in reverse. +#ifdef CANVAS_ITEM_DEBUG + std::cout << " PICKING: Checking: " << item->get_name() << " bounds: " << item->get_bounds() << std::endl; +#endif + CanvasItem* picked_item = nullptr; + if (item->is_visible() && + item->is_pickable() && + item->contains(p) ) { + + auto group = dynamic_cast<CanvasItemGroup *>(&*item); + if (group) { + picked_item = group->pick_item(p); + } else { + picked_item = &*item; + } + } + + if (picked_item != nullptr) { +#ifdef CANVAS_ITEM_DEBUG + std::cout << " PICKING: pick_item: " << picked_item->get_name() << std::endl; +#endif + return picked_item; + } + } + + return nullptr; +} + +void CanvasItemGroup::update_canvas_item_ctrl_sizes(int size_index) +{ + for (auto & item : items) { + auto ctrl = dynamic_cast<CanvasItemCtrl *>(&item); + if (ctrl) { + // 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); + } + auto group = dynamic_cast<CanvasItemGroup *>(&item); + if (group) { + group->update_canvas_item_ctrl_sizes(size_index); + } + } +} + +} // 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..27cb0da --- /dev/null +++ b/src/display/control/canvas-item-group.h @@ -0,0 +1,72 @@ +// 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 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 <2geom/rect.h> +#include <boost/intrusive/list.hpp> + +#include "canvas-item.h" + +namespace Inkscape { + +class CanvasItemGroup : public CanvasItem { + +public: + CanvasItemGroup(CanvasItemGroup* group = nullptr); + ~CanvasItemGroup() override; + + // Structure + void add(CanvasItem *item); + void remove(CanvasItem *item, bool Delete = true); + + void update(Geom::Affine const &affine) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Selection + CanvasItem* pick_item(Geom::Point &p); + + CanvasItemList & get_items() { return items; } + + // Properties + void update_canvas_item_ctrl_sizes(int size_index); + +protected: + +private: +public: + // TODO: Make private (used in canvas-item.cpp). + CanvasItemList items; // Used to speed deletion. +}; + + +} // 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..6855701 --- /dev/null +++ b/src/display/control/canvas-item-guideline.cpp @@ -0,0 +1,343 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class to represent a control guide line. + */ + +/* + * Authors: + * Tavmjong Bah - Rewrite of SPGuideLine + * Rafael Siejakowski - Tweaks to handle appearance + * + * Copyright (C) 2020-2022 the Authors. + * + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include "canvas-item-guideline.h" + +#include <2geom/line.h> + +#include "canvas-item-ctrl.h" + +#include "desktop.h" // Canvas orientation so label is orientated correctly. + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create a control guide line. Points are in document units. + */ +CanvasItemGuideLine::CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, + Geom::Point const &origin, Geom::Point const &normal) + : CanvasItem(group) + , _origin(origin) + , _normal(normal) + , _label(std::move(label)) +{ + _name = "CanvasItemGuideLine:" + _label; + _pickable = true; // For now, everybody gets events from this class! + + // Required when rotating canvas: + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); + + // Control to move guide line. + _origin_ctrl = std::make_unique<CanvasItemGuideHandle>(group, _origin, this); + _origin_ctrl->set_name("CanvasItemGuideLine:Ctrl:" + _label); + _origin_ctrl->set_size_default(); + _origin_ctrl->set_pickable(true); // The handle will also react to dragging + set_locked(false); // Init _origin_ctrl shape and stroke. +} + +CanvasItemGuideLine::~CanvasItemGuideLine() = default; + +/** + * Sets origin of guide line (place where handle is located). + */ +void CanvasItemGuideLine::set_origin(Geom::Point const &origin) +{ + if (_origin != origin) { + _origin = origin; + _origin_ctrl->set_position(_origin); + request_update(); + } +} + +/** + * Sets orientation of guide line. + */ +void CanvasItemGuideLine::set_normal(Geom::Point const &normal) +{ + if (_normal != normal) { + _normal = normal; + request_update(); + } +} + +/** + * Sets the inverted nature of the line + */ +void CanvasItemGuideLine::set_inverted(bool inverted) +{ + if (_inverted != inverted) { + _inverted = inverted; + request_update(); + } +} + + +/** + * Returns distance between point in canvas units and nearest point on guideLine. + */ +double CanvasItemGuideLine::closest_distance_to(Geom::Point const &p) +{ + // Maybe store guide as a Geom::Line? + Geom::Line guide = + Geom::Line::from_origin_and_vector(_origin, Geom::rot90(_normal)); + guide *= _affine; + return Geom::distance(p, guide); +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of guideLine (or 1 if tolerance is zero). + */ +bool CanvasItemGuideLine::contains(Geom::Point const &p, double tolerance) +{ + if (tolerance == 0) { + tolerance = 1; // Can't pick of zero! + } + + return closest_distance_to(p) < tolerance; +} + +/** + * Returns the pointer to the origin control (the "dot") + */ +CanvasItemGuideHandle* CanvasItemGuideLine::dot() const +{ + return _origin_ctrl.get(); +} + +/** + * Update and redraw control guideLine. + */ +void CanvasItemGuideLine::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + _affine = affine; + + // Queue redraw of new area (and old too). + request_redraw(); + + _need_update = false; +} + +/** + * Render guideLine to screen via Cairo. + */ +void CanvasItemGuideLine::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemGuideLine::Render: No buffer!" << std::endl; + return; + } + + if (!_visible) { + // Hidden + return; + } + + // Document to canvas + Geom::Point const normal = _normal * _affine.withoutTranslation(); // Direction only + Geom::Point const origin = _origin * _affine; + + /* Need to use floor()+0.5 such that Cairo will draw us lines with a width of a single pixel, + * without any aliasing. For this we need to position the lines at exactly half pixels, see + * https://www.cairographics.org/FAQ/#sharp_lines + * Must be consistent with the pixel alignment of the grid lines, see CanvasXYGrid::Render(), + * and the drawing of the rulers. + * Lastly, the origin control is also pixel-aligned and we want to visually cut through its + * exact center. + */ + Geom::Point const aligned_origin = origin.floor() + Geom::Point(0.5, 0.5); + + // Set up the Cairo rendering context + Cairo::RefPtr<Cairo::Context> ctx = buf->cr; + ctx->save(); + ctx->translate(-buf->rect.left(), -buf->rect.top()); // Canvas to screen + ctx->set_source_rgba(SP_RGBA32_R_F(_stroke), SP_RGBA32_G_F(_stroke), + SP_RGBA32_B_F(_stroke), SP_RGBA32_A_F(_stroke)); + ctx->set_line_width(1); + + if (_inverted) { + // operator not available in cairo C++ bindings + cairo_set_operator(ctx->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + if (!_label.empty()) { // Render text label + ctx->save(); + ctx->translate(aligned_origin.x(), aligned_origin.y()); + + SPDesktop *desktop = nullptr; + if (_canvas) { + desktop = _canvas->get_desktop(); + } + ctx->rotate(atan2(normal.cw()) + M_PI * (desktop && desktop->is_yaxisdown() ? 1 : 0)); + ctx->translate(0, -(_origin_ctrl->radius() + LABEL_SEP)); // Offset by dot radius + 2 + ctx->move_to(0, 0); + ctx->show_text(_label); + ctx->restore(); + } + + // Draw guide. + // Special case: horizontal and vertical lines (easier calculations) + + // Don't use isHorizontal()/isVertical() as they test only exact matches. + if (Geom::are_near(normal.y(), 0.0)) { + // Vertical + double const position = aligned_origin.x(); + ctx->move_to(position, buf->rect.top() + 0.5); + ctx->line_to(position, buf->rect.bottom() - 0.5); + } else if (Geom::are_near(normal.x(), 0.0)) { + // Horizontal + double position = aligned_origin.y(); + ctx->move_to(buf->rect.left() + 0.5, position); + ctx->line_to(buf->rect.right() - 0.5, position); + } else { + // Angled + Geom::Line line = Geom::Line::from_origin_and_vector(aligned_origin, Geom::rot90(normal)); + + // Find intersections of the line with buf rectangle. There should be zero or two. + std::vector<Geom::Point> intersections; + for (unsigned i = 0; i < 4; ++i) { + Geom::LineSegment side(buf->rect.corner(i), buf->rect.corner((i+1)%4)); + try { + Geom::OptCrossing oc = Geom::intersection(line, side); + if (oc) { + intersections.push_back(line.pointAt((*oc).ta)); + } + } catch (Geom::InfiniteSolutions) { + // Shouldn't happen as we have already taken care of horizontal/vertical guides. + std::cerr << "CanvasItemGuideLine::render: Error: Infinite intersections." << std::endl; + } + } + + if (intersections.size() == 2) { + double const x0 = intersections[0].x(); + double const x1 = intersections[1].x(); + double const y0 = intersections[0].y(); + double const y1 = intersections[1].y(); + ctx->move_to(x0, y0); + ctx->line_to(x1, y1); + } + } + ctx->stroke(); + + ctx->restore(); +} + +void CanvasItemGuideLine::hide() +{ + CanvasItem::hide(); + _origin_ctrl->hide(); +} + +void CanvasItemGuideLine::show() +{ + CanvasItem::show(); + _origin_ctrl->show(); +} + +void CanvasItemGuideLine::set_stroke(guint32 color) +{ + // Make sure the fill of the control is the same as the stroke + // of the guide-line: + _origin_ctrl->set_fill(color); + CanvasItem::set_stroke(color); +} + +void CanvasItemGuideLine::set_label(Glib::ustring const & label) +{ + if (_label != label) { + _label = label; + request_update(); + } +} + +void CanvasItemGuideLine::set_locked(bool locked) +{ + if (_locked != locked) { + _locked = locked; + if (_locked) { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CROSS); + _origin_ctrl->set_stroke(CONTROL_LOCKED_COLOR); + _origin_ctrl->set_fill(0x00000000); // no fill + } else { + _origin_ctrl->set_shape(CANVAS_ITEM_CTRL_SHAPE_CIRCLE); + _origin_ctrl->set_stroke(0x00000000); // no stroke + _origin_ctrl->set_fill(_stroke); // fill the control with this guide's color + } + } +} + +//=============================================================================================== + +/** + * @brief Create a handle ("dot") along a guide line + * @param group - the associated canvas item group + * @param pos - position + * @param line - pointer to the corresponding guide line + */ +CanvasItemGuideHandle::CanvasItemGuideHandle(CanvasItemGroup *group, + Geom::Point const &pos, + CanvasItemGuideLine* line) + : CanvasItemCtrl(group, CANVAS_ITEM_CTRL_SHAPE_CIRCLE, pos) + , _my_line(line) // Save a pointer to our guide line +{ +} + +/** + * Return the radius of the handle dot + */ +double CanvasItemGuideHandle::radius() const +{ + return 0.5 * static_cast<double>(_width); // radius is half the width +} + +/** + * Update the size of the handle based on the index from Preferences + */ +void CanvasItemGuideHandle::set_size_via_index(int index) +{ + double const r = static_cast<double>(index) * SCALE; + unsigned long const rounded_diameter = std::lround(r * 2.0); // diameter is twice the radius + unsigned long size = rounded_diameter | 0x1; // make sure the size is always odd + if (size < MINIMUM_SIZE) { + size = MINIMUM_SIZE; + } + if (_width != size) { + _width = size; + _height = size; + _built = false; + request_update(); + _my_line->request_update(); + } +} + +} // namespace Inkscape + +/* + Local Variables: + mode:c++ + c-file-style:"stroustrup" + c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +)) + indent-tabs-mode:nil + fill-column:99 + End: +*/ +// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 : diff --git a/src/display/control/canvas-item-guideline.h b/src/display/control/canvas-item-guideline.h new file mode 100644 index 0000000..af22e4e --- /dev/null +++ b/src/display/control/canvas-item-guideline.h @@ -0,0 +1,109 @@ +// 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" + +namespace Inkscape { + +class CanvasItemGroup; // A canvas control that contains other canvas controls. +class CanvasItemGuideHandle; // A handle ("dot") serving as draggable origin control + +class CanvasItemGuideLine : public CanvasItem { + +public: + CanvasItemGuideLine(CanvasItemGroup *group, Glib::ustring label, + Geom::Point const &origin, Geom::Point const &normal); + ~CanvasItemGuideLine() override; + + // Geometry + void set_origin(Geom::Point const &origin); + void set_normal(Geom::Point const &normal); + bool is_horizontal() const { return (_normal.x() == 0.0); } + bool is_vertical() const { return (_normal.y() == 0.0); } + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void hide() override; + void show() override; + void set_stroke(guint32 color) final; + void set_label(Glib::ustring const & label); + void set_locked(bool locked); + void set_inverted(bool inverted); + void set_sensitive(bool sensitive) { _sensitive = sensitive; } + + // Getters + CanvasItemGuideHandle* dot() const; + +protected: + 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; + bool _sensitive = false; + std::unique_ptr<CanvasItemGuideHandle> _origin_ctrl; + +private: + inline static guint32 const CONTROL_LOCKED_COLOR = 0x00000080; // RGBA black semitranslucent + inline static double const LABEL_SEP = 2.0; // Distance between the label and the origin control +}; + + +class CanvasItemGuideHandle : public CanvasItemCtrl { + +public: + CanvasItemGuideHandle(CanvasItemGroup *group, Geom::Point const &pos, CanvasItemGuideLine *line); + double radius() const; + void set_size_via_index(int index) final; + +private: + CanvasItemGuideLine *_my_line; // The guide line we belong to + + // static data + inline static double const SCALE = 0.55; // handle size relative to an auto-smooth node + inline static unsigned const 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-quad.cpp b/src/display/control/canvas-item-quad.cpp new file mode 100644 index 0000000..ccc7b2b --- /dev/null +++ b/src/display/control/canvas-item-quad.cpp @@ -0,0 +1,207 @@ +// 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 "canvas-item-quad.h" + +#include "color.h" // SP_RGBA_x_F + +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control quad. + */ +CanvasItemQuad::CanvasItemQuad(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemQuad:Null"; + _pickable = false; // For now, nobody gets events from this class! +} + +/** + * Create a control quad. Point 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"; + _pickable = false; // For now, nobody gets events from this class! + + request_update(); +} + +/** + * 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) +{ + std::cout << "Canvas_ItemQuad::set_cords: " << p0 << ", " << p1 << ", " << p2 << ", " << p3 << std::endl; + _p0 = p0; + _p1 = p1; + _p2 = p2; + _p3 = p3; + + request_update(); +} + +/** + * Returns distance between point in canvas units and nearest point on quad. + */ +double CanvasItemQuad::closest_distance_to(Geom::Point const &p) +{ + double d = Geom::infinity(); + std::cerr << "CanvasItemQuad::closest_distance_to: Not implemented!" << std::endl; + return d; +} + +/** + * 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(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + if (_p0 == _p1 || + _p1 == _p2 || + _p2 == _p3 || + _p3 == _p0) { + 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. CHECK + // Get new bounds + _affine = affine; + + Geom::Rect bounds; + bounds.expandTo(_p0); + bounds.expandTo(_p1); + bounds.expandTo(_p2); + bounds.expandTo(_p3); + bounds *= _affine; // Document to canvas. + bounds.expandBy(2); // Room for anti-aliasing effects. + _bounds = bounds; + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + +/** + * Render quad to screen via Cairo. + */ +void CanvasItemQuad::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemQuad::Render: No buffer!" << std::endl; + return; + } + + if (_p0 == _p1 || + _p1 == _p2 || + _p2 == _p3 || + _p3 == _p0) { + return; // Not quad or not initialized. + } + + if (!_visible) { + // Hidden + return; + } + + // 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(); + 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(); + + // 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-quad.h b/src/display/control/canvas-item-quad.h new file mode 100644 index 0000000..d7945de --- /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 CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemQuad : 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); + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + +protected: + Geom::Point _p0; + Geom::Point _p1; + Geom::Point _p2; + Geom::Point _p3; +}; + + +} // 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..137cbf1 --- /dev/null +++ b/src/display/control/canvas-item-rect.cpp @@ -0,0 +1,353 @@ +// 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 "inkscape.h" // +#include "ui/widget/canvas.h" +#include "display/cairo-utils.h" // Checkerboard background. + +namespace Inkscape { + +/** + * Create an null control rect. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemRect:Null"; + _pickable = false; // For now, nobody gets events from this class! +} + +/** + * Create a control rect. Point are in document coordinates. + */ +CanvasItemRect::CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect) + : CanvasItem(group) + , _rect(rect) +{ + _name = "CanvasItemRect"; + _pickable = false; // For now, nobody gets events from this class! + request_update(); +} + +/** + * Set a control rect. Points are in document coordinates. + */ +void CanvasItemRect::set_rect(Geom::Rect const &rect) +{ + _rect = rect; + request_update(); +} + +/** + * Returns distance between point in canvas units and nearest point in rect (zero if inside rect). + * Only valid if canvas is not rotated. (A rotated Geom::Rect yields a new axis-aligned Geom::Rect + * that contains the original rectangle; not a rotated rectangle.) + */ +double CanvasItemRect::closest_distance_to(Geom::Point const &p) +{ + if (_affine.isNonzeroRotation()) { + std::cerr << "CanvasItemRect::closest_distance_to: Affine includes rotation!" << std::endl; + } + + Geom::Rect rect = _rect; + rect *= _affine; // Convert from document to canvas coordinates. (TODO Cache this.) + return Geom::distance(p, 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; + } + + Geom::Point p0 = _rect.corner(0) * _affine; + Geom::Point p1 = _rect.corner(1) * _affine; + Geom::Point p2 = _rect.corner(2) * _affine; + Geom::Point p3 = _rect.corner(3) * _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 rect. + */ +void CanvasItemRect::update(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + if (_rect.area() == 0) { + return; // Nothing to show + } + + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Get new bounds + _affine = affine; + + // 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 + _bounds.expandBy(2 * get_shadow_size()); + _bounds *= _affine; + _bounds.expandBy(2); // Room for stroke. + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + +/** + * Render rect to screen via Cairo. + */ +void CanvasItemRect::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemRect::Render: No buffer!" << std::endl; + return; + } + + if (!_bounds.intersects(buf->rect)) { + return; // Rectangle not inside buffer rectangle. + } + + if (!_visible) { + // Hidden + return; + } + + // Geom::Rect is an axis-aligned rectangle. We need to rotate it if the canvas is rotated! + + // Get canvas rotation (scale is isotropic). + double rotation = atan2(_affine[1], _affine[0]); + + // Are we axis aligned? + double mod_rot = fmod(rotation * M_2_PI, 1); + bool axis_aligned = Geom::are_near(mod_rot, 0) || Geom::are_near(mod_rot, 1.0); + + // Get the points we need transformed into window coordinates. + Geom::Point rect_transformed[4]; + for (unsigned int i = 0; i < 4; ++i) { + rect_transformed[i] = _rect.corner(i) * _affine; + } + + auto rect = _rect; + + using Geom::X; + using Geom::Y; + + if (axis_aligned) { + auto temp = _rect * _affine; + auto min = temp.min(); + auto max = temp.max(); + auto pixgrid = Geom::Rect( + Geom::Point(floor(min[X]) + 0.5, floor(min[Y]) + 0.5), + Geom::Point(floor(max[X]) + 0.5, floor(max[Y]) + 0.5)); + rect = pixgrid * _affine.inverse(); + } + + buf->cr->save(); + buf->cr->translate(-buf->rect.left(), -buf->rect.top()); + + if (_inverted) { + // buf->cr->set_operator(Cairo::OPERATOR_XOR); // Blend mode operators do not have C++ bindings! + cairo_set_operator(buf->cr->cobj(), CAIRO_OPERATOR_DIFFERENCE); + } + + // fill background? + if (_background && !buf->outline_overlay_pass) { + buf->cr->save(); + Cairo::Matrix m(_affine[0], _affine[1], _affine[2], _affine[3], _affine[4], _affine[5]); + buf->cr->transform(m); + buf->cr->rectangle(rect.corner(0)[X], rect.corner(0)[Y], rect.width(), rect.height()); + // counter fill scaling (necessary for checkerboard pattern) + _background->set_matrix(m); + buf->cr->set_source(_background); + buf->cr->fill(); + buf->cr->restore(); + } + + cairo_pattern_t *pattern = _canvas->get_background_pattern()->cobj(); + guint32 backcolor = ink_cairo_pattern_get_argb32(pattern); + EXTRACT_ARGB32(backcolor, ab,rb,gb,bb) + + // Draw shadow first. Shadow extends under rectangle to reduce aliasing effects. + if (_shadow_width > 0 && !_dashed) { + // 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 + const auto a = (exp(-3 * SP_RGBA32_A_F(_shadow_color)) - 1) / (exp(-3) - 1); + buf->cr->save(); + + auto affine = _affine; + if (auto desktop = _canvas->get_desktop()) { + rect *= desktop->doc2dt(); + affine = desktop->doc2dt() * affine; + } + Cairo::Matrix m(affine[0], affine[1], affine[2], affine[3], affine[4], affine[5]); + buf->cr->transform(m); + ink_cairo_draw_drop_shadow(buf->cr, rect, get_shadow_size(), _shadow_color, a); + buf->cr->restore(); + } + + // Setup rectangle path + if (axis_aligned) { + + // Snap to pixel grid + Geom::Rect outline( _rect.min() * _affine, _rect.max() * _affine); + buf->cr->rectangle(floor(outline.min()[X])+0.5, + floor(outline.min()[Y])+0.5, + floor(outline.max()[X]) - floor(outline.min()[X]), + floor(outline.max()[Y]) - floor(outline.min()[Y])); + } else { + + // Rotated + buf->cr->move_to(rect_transformed[0][X], rect_transformed[0][Y] ); + buf->cr->line_to(rect_transformed[1][X], rect_transformed[1][Y] ); + buf->cr->line_to(rect_transformed[2][X], rect_transformed[2][Y] ); + buf->cr->line_to(rect_transformed[3][X], rect_transformed[3][Y] ); + buf->cr->close_path(); + } + static std::valarray<double> dashes = {4.0, 4.0}; + if (_dashed) { + buf->cr->set_dash(dashes, -0.5); + } + // Draw border (stroke). + 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_dashed(bool dashed) +{ + if (_dashed != dashed) { + _dashed = dashed; + request_redraw(); + } +} + +void CanvasItemRect::set_inverted(bool inverted) +{ + if (_inverted != inverted) { + _inverted = inverted; + request_redraw(); + } +} + +void CanvasItemRect::set_shadow(guint32 color, int width) +{ + if (_shadow_color != color || _shadow_width != width) { + _shadow_color = color; + _shadow_width = width; + request_redraw(); + } +} + +void CanvasItemRect::set_background(guint32 background) { + _set_background(Cairo::SolidPattern::create_rgba(SP_RGBA32_R_F(background), SP_RGBA32_G_F(background), SP_RGBA32_B_F(background), SP_RGBA32_A_F(background))); +} + +void CanvasItemRect::_set_background(Cairo::RefPtr<Cairo::Pattern> background) { + if (_background != background) { + _background = background; + request_redraw(); + } +} + +double CanvasItemRect::get_scale() const { + return sqrt(_affine[0] * _affine[0] + _affine[1] * _affine[1]); +} + +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 = get_scale(); + + // 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); +} + +void CanvasItemRect::set_background_checkerboard(guint32 rgba, bool use_alpha) { + auto pattern = ink_cairo_pattern_create_checkerboard(rgba, use_alpha); + auto background = Cairo::RefPtr<Cairo::Pattern>(new Cairo::Pattern(pattern)); + _set_background(background); +} + +} // 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..0c69d43 --- /dev/null +++ b/src/display/control/canvas-item-rect.h @@ -0,0 +1,81 @@ +// 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 CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemRect : public CanvasItem { + +public: + CanvasItemRect(CanvasItemGroup *group); + CanvasItemRect(CanvasItemGroup *group, Geom::Rect const &rect); + + // Geometry + void set_rect(Geom::Rect const &rect); + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void set_dashed(bool dash = true); + void set_inverted(bool inverted = false); + void set_shadow(guint32 color, int width); + void set_background(guint32 background); + void set_background_checkerboard(guint32 rgba, bool use_alpha); + +protected: + void _set_background(Cairo::RefPtr<Cairo::Pattern> background); + double get_shadow_size() const; + double get_scale() const; + + Geom::Rect _rect; + bool _dashed = false; + bool _inverted = false; + int _shadow_width = 0; + guint32 _shadow_color = 0x00000000; + Cairo::RefPtr<Cairo::Pattern> _background; // optional background +}; + + +} // 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-rotate.cpp b/src/display/control/canvas-item-rotate.cpp new file mode 100644 index 0000000..a013b65 --- /dev/null +++ b/src/display/control/canvas-item-rotate.cpp @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/** + * A class for previewing a canvas rotation. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of SPCanvasRotate. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <cairomm/cairomm.h> +#include <gdkmm/event.h> + +#include "canvas-item-rotate.h" + +#include "color.h" // SP_RGBA_x_F +#include "desktop.h" +#include "inkscape.h" + +#include "display/cairo-utils.h" // Copy Cairo surface. +#include "ui/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control rotate. + */ +CanvasItemRotate::CanvasItemRotate(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemRotate"; + _pickable = true; // We need the events! + _bounds = Geom::Rect(-Geom::infinity(), -Geom::infinity(), Geom::infinity(), Geom::infinity()); +} + + +/** + * Returns distance between point in canvas units and nearest point on rotate. + */ +double CanvasItemRotate::closest_distance_to(Geom::Point const &p) +{ + double d = Geom::infinity(); + std::cerr << "CanvasItemRotate::closest_distance_to: Not implemented!" << std::endl; + return d; +} + +/** + * Returns true if point p (in canvas units) is within tolerance (canvas units) distance of rotate. + */ +bool CanvasItemRotate::contains(Geom::Point const &p, double tolerance) +{ + return true; // We're always picked! +} + +/** + * Update and redraw control rotate. + */ +void CanvasItemRotate::update(Geom::Affine const &affine) +{ + _affine = affine; + request_redraw(); +} + +/** + * Render rotate to screen via Cairo. + */ +void CanvasItemRotate::render(Inkscape::CanvasItemBuffer *buf) +{ + return; // We do no proper rendering! +} + +/** + * Start + */ +void CanvasItemRotate::start(SPDesktop *desktop) +{ + _desktop = desktop; + + _current_angle = 0.0; + + _surface_copy = ink_cairo_surface_copy(_canvas->get_backing_store()); +} + +/** + * Render to widget. + */ +void CanvasItemRotate::paint() +{ + auto background = _canvas->get_backing_store(); + + if (!background) { + std::cerr << "CanvasItemRotate::paint(): No background!" << std::endl; + return; + } + + double width = background->get_width(); + double height = background->get_height(); + + // Draw rotated canvas. + auto context = Cairo::Context::create(background); + context->set_operator(Cairo::OPERATOR_SOURCE); + context->translate(width/2.0, height/2.0); + context->rotate(Geom::rad_from_deg(-_current_angle)); + context->translate(-width/2.0, -height/2.0); + context->set_source(_surface_copy, 0, 0); + context->paint(); + + _canvas->queue_draw(); +} + +/** + * Handle events. + */ +bool CanvasItemRotate::handle_event(GdkEvent *event) +{ + // Get geometry + Geom::Rect viewbox = _canvas->get_area_world(); + _center = viewbox.midpoint(); + + switch (event->type) { + case GDK_MOTION_NOTIFY: + { + Geom::Point cursor( event->motion.x, event->motion.y ); + + // Both cursor and center are in window coordinates + Geom::Point rcursor( cursor - _center ); + double angle = Geom::deg_from_rad( Geom::atan2(rcursor) ); + + + // Set start angle + if (_start_angle < -360) { + _start_angle = angle; + } + + const double rotation_snap = 15; + + double delta_angle = _start_angle - angle; + + if (event->motion.state & GDK_SHIFT_MASK && + event->motion.state & GDK_CONTROL_MASK) { + delta_angle = 0; + } else if (event->motion.state & GDK_SHIFT_MASK) { + delta_angle = round(delta_angle/rotation_snap) * rotation_snap; + } else if (event->motion.state & GDK_CONTROL_MASK) { + // ? + } else if (event->motion.state & GDK_MOD1_MASK) { + // Decimal raw angle + } else { + delta_angle = floor(delta_angle); + } + + _current_angle = delta_angle; + + // Correct line for snapping of angle + double distance = rcursor.length(); + _cursor = Geom::Point::polar( Geom::rad_from_deg(angle), distance ); + + // Update screen + paint(); + break; + } + case GDK_BUTTON_RELEASE: + { + // Rotate the actual canvas + SPDesktop *desktop = SP_ACTIVE_DESKTOP; // FIXME.. + desktop->rotate_relative_center_point (desktop->w2d(_center), + (desktop->w2d().det() > 0 ? -1 : 1) * + Geom::rad_from_deg(_current_angle) ); + + // We're done + ungrab(); + hide(); + + _start_angle = -1000; + break; + } + case GDK_KEY_PRESS: + // std::cout << " Key press: " << std::endl; + break; + case GDK_KEY_RELEASE: + // std::cout << " Key release: " << std::endl; + break; + default: + // ui_dump_event (event, "sp_canvas_rotate_event: unwanted event: "); + break; + } + + if (event->type == GDK_KEY_PRESS) return false; // Why? + + // Don't emit event signal! + + return true; +} + +} // 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-rotate.h b/src/display/control/canvas-item-rotate.h new file mode 100644 index 0000000..a2aebd5 --- /dev/null +++ b/src/display/control/canvas-item-rotate.h @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +#ifndef SEEN_CANVAS_ITEM_ROTATE_H +#define SEEN_CANVAS_ITEM_ROTATE_H + +/** + * A class for previewing a canvas rotation. + */ + +/* + * Author: + * Tavmjong Bah + * + * Copyright (C) 2020 Tavmjong Bah + * + * Rewrite of RotateCanvasItem. + * + * Released under GNU GPL v2+, read the file 'COPYING' for more information. + */ + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +class SPDesktop; + +namespace Inkscape { + +class CanvasRotate; + +namespace UI::Widget { +class Canvas; +} + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemRotate : public CanvasItem { + +public: + CanvasItemRotate(CanvasItemGroup *group); + + // Geometry + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Events + bool handle_event(GdkEvent *event) override; + void start(SPDesktop *desktop); + void paint(); + + +protected: + SPDesktop *_desktop = nullptr; + Geom::Point _center; // Center of screen. + Geom::Point _cursor; // Position of cursor relative to center (after angle snapping). + double _current_angle = 0.0; // Rotation in degrees. + double _start_angle = -1000; // Initial angle, determined by cursor position. + Cairo::RefPtr<Cairo::ImageSurface> _surface_copy; // Copy of original surface. +}; + + +} // namespace Inkscape + +#endif // SEEN_CANVAS_ITEM_ROTATE_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..f4a6801 --- /dev/null +++ b/src/display/control/canvas-item-text.cpp @@ -0,0 +1,317 @@ +// 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/widget/canvas.h" + +namespace Inkscape { + +/** + * Create an null control text. + */ +CanvasItemText::CanvasItemText(CanvasItemGroup *group) + : CanvasItem(group) +{ + _name = "CanvasItemText"; + _pickable = false; // Text is never pickable. + _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) + : CanvasItem(group) + , _p(p) + , _text(std::move(text)) +{ + _name = "CanvasItemText"; + _pickable = false; // Text is never pickable. + _fill = 0x33337fff; // Override CanvasItem default. + + request_update(); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_coord(Geom::Point const &p) +{ + _p = p; + + request_update(); +} + +/** + * Set a text position. Position is in document coordinates. + */ +void CanvasItemText::set_bg_radius(double const &rad) +{ + _bg_rad = rad; + + request_update(); +} + +/** + * Returns distance between point in canvas units and nearest point on text. + */ +double CanvasItemText::closest_distance_to(Geom::Point const &p) +{ + double d = Geom::infinity(); + std::cerr << "CanvasItemText::closest_distance_to: Not implemented!" << std::endl; + return d; +} + +/** + * 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(Geom::Affine const &affine) +{ + if (_affine == affine && !_need_update) { + // Nothing to do. + return; + } + + // Queue redraw of old area (erase previous content). + request_redraw(); + + // Get new bounds + _affine = affine; + Geom::Point p = _p * _affine; + + // Measure text size + 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 accender and decenders. + context->get_text_extents(_("lg1p$"), _text_extent); + } else { + _text_extent = _text_size; + } + + // See note at bottom. + _bounds = Geom::Rect::from_xywh(0, 0, + _text_size.x_advance + (_border * 2), + _text_extent.height + (_border * 2)); + + // Offset relative to requested point + double offset_x = -(_anchor_position.x() * _bounds.width()); + double offset_y = -(_anchor_position.y() * _bounds.height()); + offset_x += p.x() + _adjust_offset.x(); + offset_y += p.y() + _adjust_offset.y(); + _bounds *= Geom::Translate(Geom::Point(int(offset_x), int(offset_y))); + + // Pixel alignment of background. Avoid aliasing artifacts on redraw. + _bounds = _bounds.roundOutwards(); + + // Queue redraw of new area + request_redraw(); + + _need_update = false; +} + +/** + * Render text to screen via Cairo. + */ +void CanvasItemText::render(Inkscape::CanvasItemBuffer *buf) +{ + if (!buf) { + std::cerr << "CanvasItemText::Render: No buffer!" << std::endl; + return; + } + + if (!_visible) { + // Hidden + return; + } + + buf->cr->save(); + + double x = _bounds.min().x() - buf->rect.min().x(); + double y = _bounds.min().y() - buf->rect.min().y(); + + // Background + if (_use_background) { + if (_bg_rad == 0) { + buf->cr->rectangle(x, y, _bounds.width(), _bounds.height()); + } else { + double smallest = std::min(_bounds.width(), _bounds.height()); + double radius = _bg_rad * (smallest / 2); + buf->cr->arc(x + _bounds.width() - radius, + y + radius, + radius, + -M_PI_2, + 0); + + buf->cr->arc(x + _bounds.width() - radius, + y + _bounds.height() - radius, + radius, + 0, + M_PI_2); + + buf->cr->arc(x + radius, y + _bounds.height() - 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 + _bounds.width()/2.0; + auto by = y + _bounds.height()/2.0; + 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(); + +#ifdef CANVAS_ITEM_DEBUG + 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(); +#endif + + buf->cr->restore(); +} + +void CanvasItemText::set_text(Glib::ustring const &text) +{ + if (_text != text) { + _text = text; + request_update(); // Might be larger than before! + } +} + +void CanvasItemText::set_fontsize(double fontsize) +{ + if (_fontsize != fontsize) { + _fontsize = fontsize; + request_update(); // Might be larger than before! + } +} + +void CanvasItemText::set_background(guint32 background) +{ + 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) +{ + if (_anchor_position != anchor_pt) { + _anchor_position = anchor_pt; + _canvas->request_update(); + } +} + +void CanvasItemText::set_adjust(Geom::Point const &adjust_pt) +{ + if (_adjust_offset != adjust_pt) { + _adjust_offset = adjust_pt; + _canvas->request_update(); + } +} + +void CanvasItemText::set_fixed_line(bool fixed_line) +{ + if (_fixed_line != fixed_line) { + _fixed_line = fixed_line; + _canvas->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..c5d5cff --- /dev/null +++ b/src/display/control/canvas-item-text.h @@ -0,0 +1,92 @@ +// 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 <glibmm/ustring.h> + +#include <2geom/point.h> +#include <2geom/transforms.h> + +#include "canvas-item.h" + +namespace Inkscape { + +namespace UI::Widget { +class Canvas; +} + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItemText : public CanvasItem { + +public: + CanvasItemText(CanvasItemGroup *group); + CanvasItemText(CanvasItemGroup *group, Geom::Point const &p, Glib::ustring text); + + // Geometry + void set_coord(Geom::Point const &p); + void set_bg_radius(double const &rad); + + void update(Geom::Affine const &affine) override; + double closest_distance_to(Geom::Point const &p); // Maybe not needed + + // Selection + bool contains(Geom::Point const &p, double tolerance = 0) override; + + // Display + void render(Inkscape::CanvasItemBuffer *buf) override; + + // Properties + void set_text(Glib::ustring const &text); + void set_fontsize(double fontsize); + void set_background(guint32 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: + 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; + Glib::ustring _text; + std::string _fontname = "sans-serif"; + double _fontsize = 10; + double _bg_rad = 0; + guint32 _background = 0x0000007f; + bool _use_background = false; + bool _fixed_line = false; // Correction for font heights + const double _border = 3; // Must be a const to allow alignment with other text boxes. +}; + + +} // 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..6670b19 --- /dev/null +++ b/src/display/control/canvas-item.cpp @@ -0,0 +1,261 @@ +// 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 "ui/widget/canvas.h" + +namespace Inkscape { + +CanvasItem::CanvasItem(CanvasItemGroup *group) + : _name("CanvasItem") +{ + if (group) { + group->add(this); + _parent = group; + _canvas = group->get_canvas(); + _affine = group->get_affine(); + } +} + +CanvasItem::~CanvasItem() +{ + if (_parent) { + _parent->remove(this, false); // remove() should not delete this or we'll double delete! + } + + // Clear canvas of item. + request_redraw(); + + // Clear any pointers to this object in canvas. + _canvas->canvas_item_destructed(this); +} + +bool CanvasItem::is_descendant_of(CanvasItem *ancestor) +{ + auto item = this; + while (item) { + if (item == ancestor) { + return true; + } + item = item->get_parent(); + } + return false; +} + +int CanvasItem::get_z_position() +{ + if (!_parent) { + std::cerr << "CanvasItem::get_z_position: No parent!" << std::endl; + return -1; + } + + size_t position = 0; + for (auto it = _parent->items.begin(); it != _parent->items.end(); ++it, ++position) { + if (&*it == this) { + return position; + } + } + + std::cerr << "CanvasItem::get_z_position: item not found!" << std::endl; + return -1; +} + +void CanvasItem::set_z_position(unsigned int n) +{ + if (!_parent) { + std::cerr << "CanvasItem::set_z_position: No parent!" << std::endl; + } + + if (n == 0) { + this->lower_to_bottom(); // Low cost operation + return; + } + + if (n > _parent->items.size() - 2) { + this->raise_to_top(); // Low cost operation + return; + } + + _parent->items.erase(_parent->items.iterator_to(*this)); + + size_t position = 0; + for (auto it = _parent->items.begin(); it != _parent->items.end(); ++it, ++position) { + if (position == n) { + _parent->items.insert(it, *this); + break; + } + } +} + +void CanvasItem::raise_to_top() +{ + if (!_parent) { + std::cerr << "CanvasItem::raise_to_top: No parent!" << std::endl; + } + + _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; + } + + _parent->items.erase(_parent->items.iterator_to(*this)); + _parent->items.push_front(*this); +} + +// Indicate geometry changed and bounds needs recalculating. +void CanvasItem::request_update() +{ + _need_update = true; + if (_parent) { + _parent->request_update(); + } else { + _canvas->request_update(); + } +} + +void CanvasItem::show() +{ + if (_visible) { + return; // Already visible. + } + + _visible = true; + // update bounds when visibility changes + request_update(); + request_redraw(); +} + +int CanvasItem::grab(Gdk::EventMask event_mask, GdkCursor *cursor) +{ + return grab(event_mask, Glib::wrap(cursor)); +} + +// Grab all events! TODO: Return boolean +int CanvasItem::grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> cursor) +{ +#ifdef CANVAS_ITEM_DEBUG + std::cout << "CanvasItem::grab: " << _name << std::endl; +#endif + // Don't grab if we already have a grabbed item! + if (_canvas->get_grabbed_canvas_item()) { + return -1; + } + + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + auto const window = _canvas->get_window(); + seat->grab(window, Gdk::SEAT_CAPABILITY_ALL_POINTING, false, cursor, nullptr); + + _canvas->set_grabbed_canvas_item(this, event_mask); + _canvas->set_current_canvas_item(this); // So that all events go to grabbed item. + return 0; +} + +void CanvasItem::ungrab() +{ +#ifdef CANVAS_ITEM_DEBUG + std::cout << "CanvasItem::ungrab: " << _name << std::endl; +#endif + if (_canvas->get_grabbed_canvas_item() != this) { + return; // Sanity check + } + + _canvas->set_grabbed_canvas_item(nullptr, (Gdk::EventMask)0); // Zero mask + + auto const display = Gdk::Display::get_default(); + auto const seat = display->get_default_seat(); + seat->ungrab(); +} + +void CanvasItem::hide() +{ + if (!_visible) { + return; // Already hidden + } + + _visible = false; + // update bounds when visibility changes + request_update(); + request_redraw(); +} + +void CanvasItem::set_fill(guint32 rgba) +{ + if (_fill != rgba) { + _fill = rgba; + request_redraw(); + } +} + +void CanvasItem::set_stroke(guint32 rgba) +{ + if (_stroke != rgba) { + _stroke = rgba; + request_redraw(); + } +} + +void CanvasItem::request_redraw() { + if (_canvas) { + // Queue redraw request + _canvas->redraw_area(_bounds); + } +} + +} // Namespace Inkscape + +void canvas_item_print_tree(Inkscape::CanvasItem *item) +{ + static int level = 0; + if (level == 0) { + std::cout << "Canvas Item Tree" << std::endl; + } + + std::cout << "CC: "; + for (unsigned i = 0; i < level; ++i) { + std::cout << " "; + } + + std::cout << item->get_z_position() << ": " << item->get_name() << std::endl; + + auto group = dynamic_cast<Inkscape::CanvasItemGroup *>(item); + if (group) { + ++level; + for (auto & item : group->items) { + canvas_item_print_tree(&item); + } + --level; + } +} + +/* + 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..37d5ec7 --- /dev/null +++ b/src/display/control/canvas-item.h @@ -0,0 +1,175 @@ +// 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. + */ + +//#define CANVAS_ITEM_DEBUG + +#include <gdk/gdk.h> // GdkEvent +#include <gdkmm/device.h> // Gdk::EventMask +#include <glib.h> // guint32 +#include <sigc++/sigc++.h> + +#include <2geom/rect.h> + +#include <boost/intrusive/list.hpp> + +#include "canvas-item-buffer.h" +#include "canvas-item-enums.h" + +class SPItem; + +namespace Inkscape { + +static guint32 CANVAS_ITEM_COLORS[] = { 0x0000ff7f, 0xff00007f, 0xffff007f }; + +namespace UI::Widget { +class Canvas; +} + +class CanvasItemGroup; // A canvas control that contains other canvas controls. + +class CanvasItem { + +public: + CanvasItem(CanvasItemGroup* group); + virtual ~CanvasItem(); + + // Structure + void set_canvas(UI::Widget::Canvas *canvas) { _canvas = canvas; } + UI::Widget::Canvas* get_canvas() { return _canvas; } + + void set_parent(CanvasItemGroup *parent) { _parent = parent; } + CanvasItemGroup* get_parent() { return _parent; } + + void set_item(SPItem *item) { _item = item; } + SPItem* get_item() { return _item; } + + // Z Position + bool is_descendant_of(CanvasItem *ancestor); + void set_z_position(unsigned int n); + int get_z_position(); // z position in group. + // void raise_by(unsigned int n); + void raise_to_top(); // Move to top of group (last entry). + // void lower_by(unsigned int n); + void lower_to_bottom(); // Move to bottom of group (first entry). + + // Geometry + void request_update(); + virtual void update(Geom::Affine const &affine) = 0; + Geom::Affine get_affine() { return _affine; } + Geom::Rect get_bounds() { return _bounds; } + + // Selection + virtual bool contains(Geom::Point const &p, double tolerance = 0) { return _bounds.interiorContains(p); } + int grab(Gdk::EventMask event_mask, GdkCursor *cursor = nullptr); + int grab(Gdk::EventMask event_mask, Glib::RefPtr<Gdk::Cursor> cursor); + void ungrab(); + + // Display + virtual void render(Inkscape::CanvasItemBuffer *buf) = 0; + bool is_visible() { return _visible; } + virtual void hide(); + virtual void show(); + void request_redraw(); // queue redraw request + + // Properties + virtual void set_fill(guint32 rgba); + void set_fill(CanvasItemColor color) { set_fill(CANVAS_ITEM_COLORS[color]); } + virtual void set_stroke(guint32 rgba); + void set_stroke(CanvasItemColor color) { set_stroke(CANVAS_ITEM_COLORS[color]); } + void set_name(std::string const &name) { _name = name; } + std::string get_name() { return _name; } + + // Events + void set_pickable(bool pickable) { _pickable = pickable; } + bool is_pickable() { return _pickable; } + sigc::connection connect_event(sigc::slot<bool, GdkEvent*> slot) { + return _event_signal.connect(slot); + } + virtual bool handle_event(GdkEvent *event) { + return _event_signal.emit(event); // Default just emit event. + } + + // Boost linked list member hook, speeds deletion. + boost::intrusive::list_member_hook<> member_hook; + +protected: + + // Structure + CanvasItemGroup *_parent = nullptr; + Inkscape::UI::Widget::Canvas *_canvas = nullptr; + SPItem *_item; // The object this canvas item is linked to in some sense. Can be nullptr. + + // Geometry + Geom::Rect _bounds; + Geom::Affine _affine; + bool _need_update = true; // Need update after creation! + + // Display + bool _visible = true; + bool _align_to_drawing = false; // Rotate if drawing is rotated. TODO: Implement! + + // Selection + bool _pickable = false; // Most items are just for display and are not pickable! + + // Properties + guint32 _fill = CANVAS_ITEM_COLORS[CANVAS_ITEM_SECONDARY]; + guint32 _stroke = CANVAS_ITEM_COLORS[CANVAS_ITEM_PRIMARY]; + std::string _name; // For debugging + + // Events + sigc::signal<bool, GdkEvent*> _event_signal; +}; + + +} // namespace Inkscape + +/** Type for linked list storing CanvasItem's. + * + * Used to speed deletion when a group contains a large number of item's (as in nodes for a + * complex path). + */ +typedef boost::intrusive::list< + Inkscape::CanvasItem, + boost::intrusive::member_hook<Inkscape::CanvasItem, boost::intrusive::list_member_hook<>, + &Inkscape::CanvasItem::member_hook> > CanvasItemList; + +// Recursively print CanvasItem tree. +void canvas_item_print_tree(Inkscape::CanvasItem *item); + +#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..d6b363f --- /dev/null +++ b/src/display/control/canvas-page.cpp @@ -0,0 +1,176 @@ +// 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" + +namespace Inkscape { + +CanvasPage::~CanvasPage() +{ + for (auto item : canvas_items) { + delete item; + } + canvas_items.clear(); +} + +/** + * 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"); + canvas_items.push_back(item); + } + + // Background rectangle 'fill' + if (auto item = new CanvasItemRect(background_group, size)) { + item->set_name("background"); + item->set_dashed(false); + item->set_inverted(false); + item->set_stroke(0x00000000); + canvas_items.push_back(item); + } + + if (auto label = new CanvasItemText(border_group, Geom::Point(0, 0), "{Page Label}")) { + label->set_fontsize(10.0); + label->set_fill(0xffffffff); + label->set_background(0x00000099); + label->set_bg_radius(0.2); + label->set_anchor(Geom::Point(0.0, 1.0)); + label->set_fixed_line(true); + canvas_items.push_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()) { + delete (*it); + 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(); + } +} + +/** + * 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, 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 = 0xff0000cc; + guint32 border_color = _border_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)) { + 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_shadow(shadow_color, _shadow_size); + rect->set_stroke(is_selected ? select_color : border_color); + } else { + rect->hide(); + rect->set_shadow(0x0, 0); + rect->set_stroke(0x0); + } + // This undoes the hide for the background rect, but that's ok + if (!is_foreground) { + rect->show(); + if (_checkerboard) { + rect->set_background_checkerboard(_background_color, true); + } + else { + // TODO: This ignores the requested transparency to paint the background. + // there is disagreement between developers about this feature. + rect->set_background(_background_color | 0xff); + } + } + } + if (auto label = dynamic_cast<CanvasItemText *>(item)) { + if (txt) { + auto corner = size.corner(0); + label->set_coord(corner); + label->set_text(txt); + label->show(); + } else { + label->set_text(""); + label->hide(); + } + } + } +} + +bool CanvasPage::setAttributes(bool on_top, guint32 border, guint32 bg, int shadow, bool checkerboard) +{ + if (on_top != _border_on_top || border != _border_color || bg != _background_color || shadow != _shadow_size || checkerboard != _checkerboard) { + this->_border_on_top = on_top; + this->_border_color = border; + this->_background_color = bg; + _shadow_size = shadow; + _checkerboard = checkerboard; + return true; + } + return false; +} + +}; + +/* + 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..db66b4f --- /dev/null +++ b/src/display/control/canvas-page.h @@ -0,0 +1,72 @@ +// 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.h" + +namespace Inkscape { + namespace UI { + namespace Widget { + class Canvas; + }; + }; + + class CanvasItemGroup; + +class CanvasPage +{ +public: + CanvasPage() = default; + ~CanvasPage(); + + void update(Geom::Rect size, 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(); + + bool setAttributes(bool on_top, guint32 border, guint32 bg, int shadow, bool checkerboard); + void setOutline(bool outline); + + bool is_selected = false; +private: + // 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<Inkscape::CanvasItem *> canvas_items; + + int _shadow_size = 0; + bool _border_on_top = true; + guint32 _background_color = 0xffffff00; + guint32 _border_color = 0x00000040; + bool _checkerboard = false; +}; + +}; + +#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..4be97f9 --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.cpp @@ -0,0 +1,88 @@ +// 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 "canvas-temporary-item.h" +#include "canvas-temporary-item-list.h" + +namespace Inkscape { +namespace Display { + +TemporaryItemList::TemporaryItemList(SPDesktop *desktop) + : desktop(desktop) +{ + +} + +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, unsigned int lifetime) +{ + // beware of strange things happening due to very short timeouts + TemporaryItem * tempitem; + if (lifetime == 0) + tempitem = new TemporaryItem(item, 0, true); + else { + tempitem = new TemporaryItem(item, lifetime); + tempitem->signal_timeout.connect( sigc::mem_fun(*this, &TemporaryItemList::_item_timeout) ); + } + + itemlist.push_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) + bool in_list = false; + for (auto & it : itemlist) { + if ( it == tempitem ) { + in_list = true; + break; + } + } + if (in_list) { + itemlist.remove(tempitem); + delete tempitem; + } +} + +void +TemporaryItemList::_item_timeout(TemporaryItem * tempitem) +{ + itemlist.remove(tempitem); + // no need to delete the item, it does that itself after signal_timeout.emit() completes +} + +} //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..29b270a --- /dev/null +++ b/src/display/control/canvas-temporary-item-list.h @@ -0,0 +1,65 @@ +// 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> + +struct SPCanvasItem; +class SPDesktop; + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +class TemporaryItem; + +/** + * Provides a class that can contain active TemporaryItem[s] on a desktop. + */ +class TemporaryItemList { +public: + TemporaryItemList(SPDesktop *desktop); + virtual ~TemporaryItemList(); + + TemporaryItem* add_item (SPCanvasItem *item, unsigned int lifetime); + TemporaryItem* add_item (CanvasItem *item, unsigned int lifetime); + void delete_item (TemporaryItem * tempitem); + +protected: + SPDesktop *desktop; /** Desktop we are on. */ + + std::list<TemporaryItem *> itemlist; /** list of temp items */ + + void _item_timeout (TemporaryItem * tempitem); + +private: + TemporaryItemList(const TemporaryItemList&) = delete; + TemporaryItemList& operator=(const TemporaryItemList&) = delete; +}; + +} //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 : diff --git a/src/display/control/canvas-temporary-item.cpp b/src/display/control/canvas-temporary-item.cpp new file mode 100644 index 0000000..08a8607 --- /dev/null +++ b/src/display/control/canvas-temporary-item.cpp @@ -0,0 +1,81 @@ +// 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 <glib.h> + +#include "canvas-temporary-item.h" + +#include "canvas-item.h" + +namespace Inkscape { +namespace Display { + +/** lifetime is measured in milliseconds + */ +TemporaryItem::TemporaryItem(CanvasItem *item, guint lifetime, bool deselect_destroy) + : canvasitem(item), + timeout_id(0), + destroy_on_deselect(deselect_destroy) +{ + if (lifetime > 0 && destroy_on_deselect) { + g_print ("Warning: lifetime should be 0 when destroy_on_deselect is true\n"); + lifetime = 0; + } + // zero lifetime means stay forever, so do not add timeout event. + if (lifetime > 0) { + timeout_id = g_timeout_add(lifetime, &TemporaryItem::_timeout, this); + } +} + +TemporaryItem::~TemporaryItem() +{ + // when it has not expired yet... + if (timeout_id) { + g_source_remove(timeout_id); + timeout_id = 0; + } + + if (canvasitem) { + // destroying the item automatically hides it + delete canvasitem; + canvasitem = nullptr; + } +} + +/* static method */ +int TemporaryItem::_timeout(void* data) { + TemporaryItem *tempitem = static_cast<TemporaryItem *>(data); + tempitem->timeout_id = 0; + tempitem->signal_timeout.emit(tempitem); + delete tempitem; + return FALSE; +} + + +} //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..b2457c7 --- /dev/null +++ b/src/display/control/canvas-temporary-item.h @@ -0,0 +1,60 @@ +// 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> + +namespace Inkscape { + +class CanvasItem; + +namespace Display { + +/** + * Provides a class to put a canvasitem temporarily on-canvas. + */ +class TemporaryItem { +public: + TemporaryItem(Inkscape::CanvasItem *item, unsigned int lifetime, bool destroy_on_deselect = false); + virtual ~TemporaryItem(); + + TemporaryItem(const TemporaryItem&) = delete; + TemporaryItem& operator=(const TemporaryItem&) = delete; + + sigc::signal<void, TemporaryItem *> signal_timeout; + +protected: + friend class TemporaryItemList; + + Inkscape::CanvasItem * canvasitem = nullptr; /** The item we are holding on to */ + unsigned int timeout_id; /** ID by which glib knows the timeout event */ + bool destroy_on_deselect; // only destroy when parent item is deselected, not when mouse leaves + + static int _timeout(void* data); ///< callback for when lifetime expired +}; + +} //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 : diff --git a/src/display/control/snap-indicator.cpp b/src/display/control/snap-indicator.cpp new file mode 100644 index 0000000..4f42820 --- /dev/null +++ b/src/display/control/snap-indicator.cpp @@ -0,0 +1,640 @@ +// 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_BORDER, _("page border")}, + {SNAPTARGET_PAGE_CORNER, _("page corner")}, + {SNAPTARGET_PAGE_CENTER, _("page center")}, + {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->set_z_position(0); + _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_CENTER: + case SNAPTARGET_ALIGNMENT_PAGE_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)); + + if (show_distance) { + auto dist = Geom::L2(p2 - p1); + double offset = (fontsize + 5)/_desktop->current_zoom(); + 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 : |