Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
canvas-item-ctrl.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
5/*
6 * Authors:
7 * Tavmjong Bah
8 * Sanidhya Singh
9 *
10 * Copyright (C) 2020 Tavmjong Bah
11 *
12 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13 */
14
15#include "canvas-item-ctrl.h"
16
17#include <2geom/transforms.h>
18#include <algorithm>
19#include <array>
20#include <cairomm/context.h>
21#include <cmath>
22#include <iostream>
23
25#include "preferences.h" // Default size.
26#include "ui/widget/canvas.h"
27
28// Render handles at different sizes and save them to "handles.png".
29constexpr bool DUMP_HANDLES = false;
30
31namespace Inkscape {
32
37 : CanvasItem(group)
38{
39 _name = "CanvasItemCtrl:Null";
40 _pickable = true; // Everybody gets events from this class!
41}
42
47 : CanvasItem(group)
48 , _handle{.type = type}
49{
50 _name = "CanvasItemCtrl:Type_" + std::to_string(_handle.type);
51 _pickable = true; // Everybody gets events from this class!
53
54 // for debugging
55 _dump();
56}
57
59{
60 // Ensure dead code is not emitted if flag is off.
61 if (!DUMP_HANDLES) {
62 return;
63 }
64
65 // Note: Atomicity not required.
66 static bool first_run = true;
67 if (!first_run) return;
68 first_run = false;
69
70 constexpr int step = 40;
71 constexpr int h = 15;
72 constexpr auto types = std::to_array({
76 CANVAS_ITEM_CTRL_TYPE_POINT, // dot-like handle, indicator
82 CANVAS_ITEM_CTRL_TYPE_POINTER, // pointy, triangular handle
83 });
84 // device scale to use; 1 - low res, 2 - high res
85 constexpr int scale = 1;
86
87 auto surface = Cairo::ImageSurface::create(Cairo::Surface::Format::ARGB32, (types.size() + 1) * step * scale, (h + 1) * step * scale);
88 cairo_surface_set_device_scale(surface->cobj(), 1, 1);
89 auto buf = CanvasItemBuffer{
90 .rect = Geom::IntRect(0, 0, surface->get_width(), surface->get_height()),
91 .device_scale = scale,
92 .cr = Cairo::Context::create(surface),
93 .outline_pass = false
94 };
95
96 auto ctx = buf.cr;
97 ctx->set_source_rgb(1, 0.9, 0.9);
98 ctx->paint();
99 ctx->set_source_rgba(0, 0, 1, 0.2);
100 ctx->set_line_width(scale);
101 constexpr double pix = scale & 1 ? 0.5 : 0;
102 for (int size = 1; size <= h; ++size) {
103 double y = size * step * scale + pix;
104 ctx->move_to(0, y);
105 ctx->line_to(surface->get_width(), y);
106 ctx->stroke();
107 }
108 for (int i = 1; i <= types.size(); i++) {
109 double x = i * step * scale + pix;
110 ctx->move_to(x, 0);
111 ctx->line_to(x, surface->get_height());
112 ctx->stroke();
113 }
114
115 cairo_surface_set_device_scale(surface->cobj(), scale, scale);
116
117 set_hover();
118 for (int size = 1; size <= h; ++size) {
119 int i = 1;
120 for (auto type : types) {
121 set_type(type);
123 _position = Geom::IntPoint{i++, size} * step;
124 _update(false);
125 _render(buf);
126 }
127 }
128
129 cairo_surface_set_device_scale(surface->cobj(), scale, scale);
130 surface->write_to_png("handles.png");
131}
132
142
147{
148 defer([=, this] {
149 if (_position == position) return;
150 _position = position;
152 });
153}
154
159{
160 // TODO: Different criteria for different shapes.
161 return Geom::distance(p, _position * affine());
162}
163
169bool CanvasItemCtrl::contains(Geom::Point const &p, double tolerance)
170{
171 // TODO: Different criteria for different shapes.
172 if (!_bounds) {
173 return false;
174 }
175 if (tolerance == 0) {
176 return _bounds->interiorContains(p);
177 } else {
178 return closest_distance_to(p) <= tolerance;
179 }
180}
181
182void CanvasItemCtrl::set_fill(uint32_t fill)
183{
184 defer([=, this] {
185 _fill_set = true;
186 if (_fill == fill) return;
187 _fill = fill;
188 _built.reset();
190 });
191}
192
193void CanvasItemCtrl::set_stroke(uint32_t stroke)
194{
195 defer([=, this] {
196 _stroke_set = true;
197 if (_stroke == stroke) return;
198 _stroke = stroke;
199 _built.reset();
201 });
202}
203
205{
206 defer([=, this] {
207 _shape_set = true;
208 if (_shape == shape) return;
209 _shape = shape;
210 _built.reset();
211 request_update(); // Geometry could change
212 });
213}
214
216{
217 defer([=, this] {
218 if (_width == size) return;
219 _width = size;
220 _built.reset();
221 request_update(); // Geometry change
222 });
223}
224
225constexpr int MIN_INDEX = 1;
226constexpr int MAX_INDEX = 15;
227
228static int get_size_default() {
229 return Preferences::get()->getIntLimited("/options/grabsize/value", 3, MIN_INDEX, MAX_INDEX);
230}
231
236
238{
239 // Size must always be an odd number to center on pixel.
240 if (size_index < MIN_INDEX || size_index > MAX_INDEX) {
241 std::cerr << "CanvasItemCtrl::set_size_via_index: size_index out of range!" << std::endl;
242 size_index = 3;
243 }
244
245 auto size = std::clamp(size_index + static_cast<int>(_rel_size), MIN_INDEX, MAX_INDEX);
247}
248
250 auto const &style = _context->handlesCss()->style_map.at(_handle);
251 auto size = _width * style.scale() + style.size_extra();
252 return size;
253}
254
256 const auto& style = _context->handlesCss()->style_map.at(_handle);
257 auto width = get_width() + get_stroke_width() + 2 * style.outline_width();
258 return width;
259}
260
261int CanvasItemCtrl::get_pixmap_width(int device_scale) const {
262 return std::round(get_total_width() * device_scale);
263}
264
270
272{
273 defer([=, this] {
274 if (_handle.type == type) return;
275 _handle.type = type;
277 _built.reset();
278 request_update(); // Possible geometry change
279 });
280}
281
283{
284 defer([=, this] {
285 _handle.selected = selected;
286 _built.reset();
288 });
289}
290
292{
293 defer([=, this] {
294 _handle.click = click;
295 _built.reset();
297 });
298}
299
301{
302 defer([=, this] {
303 _handle.hover = hover;
304 _built.reset();
306 });
307}
308
312void CanvasItemCtrl::set_normal(bool selected)
313{
314 defer([=, this] {
315 _handle.selected = selected;
316 _handle.hover = false;
317 _handle.click = false;
318 _built.reset();
320 });
321}
322
324{
325 defer([=, this] {
326 if (_angle == angle) return;
327 _angle = angle;
328 _built.reset();
329 request_update(); // Geometry change
330 });
331}
332
334{
335 defer([=, this] {
336 if (_anchor == anchor) return;
337 _anchor = anchor;
338 request_update(); // Geometry change
339 });
340}
341
342static double angle_of(Geom::Affine const &affine)
343{
344 return std::atan2(affine[1], affine[0]);
345}
346
351{
352 // Queue redraw of old area (erase previous content).
354
355 // Setting the position to (inf, inf) to hide it is a pervasive hack we need to support.
356 if (!_position.isFinite()) {
357 _bounds = {};
358 return;
359 }
360
361 const auto width = static_cast<double>(get_total_width());
362
363 // Get half width, rounded down.
364 double const w_half = width / 2;
365
366 // Set _angle, and compute adjustment for anchor.
367 double dx = 0;
368 double dy = 0;
369
371 if (!_shape_set) {
372 auto const &style = _context->handlesCss()->style_map.at(_handle);
373 shape = style.shape();
374 }
375
376 switch (shape) {
382 double angle = (affine().flips() ? -1 : 1) * int{_anchor} * M_PI_4 + angle_of(affine());
383 double const half = width / 2.0;
384
385 dx = -(half + 2) * cos(angle); // Add a bit to prevent tip from overlapping due to rounding errors.
386 dy = -(half + 2) * sin(angle);
387
388 switch (shape) {
390 angle += 5 * M_PI_4;
391 break;
392
394 angle += M_PI_2;
395 break;
396
398 dx = -(half / 2 + 2) * cos(angle);
399 dy = -(half / 2 + 2) * sin(angle);
400 angle -= M_PI_2;
401 break;
402
404 angle -= M_PI_4;
405 dx = (half / 2 + 2) * (sin(angle) - cos(angle));
406 dy = (half / 2 + 2) * (-sin(angle) - cos(angle));
407 break;
408
409 default:
410 break;
411 }
412
413 if (_angle != angle) {
414 _angle = angle;
415 _built.reset();
416 }
417
418 break;
419 }
420
423 double const angle = angle_of(affine());
424 if (_angle != angle) {
425 _angle = angle;
426 _built.reset();
427 }
428 break;
429 }
430
431 default:
432 switch (_anchor) {
433 case SP_ANCHOR_N:
434 case SP_ANCHOR_CENTER:
435 case SP_ANCHOR_S:
436 break;
437
438 case SP_ANCHOR_NW:
439 case SP_ANCHOR_W:
440 case SP_ANCHOR_SW:
441 dx = w_half;
442 break;
443
444 case SP_ANCHOR_NE:
445 case SP_ANCHOR_E:
446 case SP_ANCHOR_SE:
447 dx = -w_half;
448 break;
449 }
450
451 switch (_anchor) {
452 case SP_ANCHOR_W:
453 case SP_ANCHOR_CENTER:
454 case SP_ANCHOR_E:
455 break;
456
457 case SP_ANCHOR_NW:
458 case SP_ANCHOR_N:
459 case SP_ANCHOR_NE:
460 dy = w_half;
461 break;
462
463 case SP_ANCHOR_SW:
464 case SP_ANCHOR_S:
465 case SP_ANCHOR_SE:
466 dy = -w_half;
467 break;
468 }
469 break;
470 }
471
472 // The location we want to place our anchor/ctrl point
473 _pos = Geom::Point(-w_half, -w_half) + Geom::Point(dx, dy) + _position * affine();
474
475 // The bounding box we want to invalidate in cairo, rounded out to catch any stray pixels
476 _bounds = Geom::Rect::from_xywh(_pos, {width, width}).roundOutwards();
477
478 // Queue redraw of new area
480}
481
486{
487 _built.init([&, this] {
488 build_cache(buf.device_scale);
489 });
490
491 if (!_cache) {
492 return;
493 }
494
495 // Round to the device pixel at the very last minute so we get less bluring
496 auto const [x, y] = Geom::Point{(_pos * buf.device_scale).round()} / buf.device_scale - buf.rect.min();
497 cairo_set_source_surface(buf.cr->cobj(), const_cast<cairo_surface_t *>(_cache->cobj()), x, y); // C API is const-incorrect.
498 buf.cr->paint();
499}
500
502{
503 assert(!_context->snapshotted()); // precondition
504 _built.reset();
506}
507
509 const auto& style = _context->handlesCss()->style_map.at(_handle);
510 // growing stroke width with handle size, if style enables it
511 auto stroke_width = style.stroke_width() * (1.0f + _width * style.stroke_scale());
512 return stroke_width;
513}
514
518void CanvasItemCtrl::build_cache(int device_scale) const
519{
520 auto width = get_width();
521 if (width < 1) {
522 return; // Nothing to render
523 }
524
525 // take size in logical pixels and make it fit physical pixel grid
526 auto pixel_fit = [=](float v) { return std::round(v * device_scale) / device_scale; };
527
528 auto const &style = _context->handlesCss()->style_map.at(_handle);
529 // effective stroke width
530 auto stroke_width = pixel_fit(get_stroke_width());
531 // fixed-size outline
532 auto outline_width = pixel_fit(style.outline_width());
533 // handle size
534 auto size = std::floor(width * device_scale) / device_scale;
535
537 .shape = _shape_set ? _shape : style.shape(),
538 .fill = _fill_set ? _fill : style.getFill(),
539 .stroke = _stroke_set ? _stroke : style.getStroke(),
540 .outline = style.getOutline(),
541 .stroke_width = stroke_width,
542 .outline_width = outline_width,
543 .width = get_pixmap_width(device_scale),
544 .size = size,
545 .angle = _angle,
546 .device_scale = device_scale
547 });
548}
549
550} // namespace Inkscape
551
552/*
553 Local Variables:
554 mode:c++
555 c-file-style:"stroustrup"
556 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
557 indent-tabs-mode:nil
558 fill-column:99
559 End:
560*/
561// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
double scale
Definition aa.cpp:228
constexpr bool DUMP_HANDLES
A class to represent a control node.
Cairo::RefPtr< Cairo::ImageSurface > surface
Definition canvas.cpp:137
Inkscape canvas widget.
3x3 matrix representing an affine transformation.
Definition affine.h:70
bool flips() const
Check whether this transformation flips objects.
Definition affine.cpp:368
static CRect from_xywh(Coord x, Coord y, Coord w, Coord h)
Create rectangle from origin and dimensions.
Two-dimensional point with integer coordinates.
Definition int-point.h:57
Two-dimensional point that doubles as a vector.
Definition point.h:66
bool isFinite() const
Check whether both coordinates are finite.
Definition point.h:218
void reset()
Definition initlock.h:28
void init(F &&f) const
Definition initlock.h:15
std::shared_ptr< Handles::Css const > const & handlesCss() const
void set_fill(uint32_t rgba) override
void set_click(bool click=true)
bool contains(Geom::Point const &p, double tolerance=0) override
If tolerance is zero, returns true if point p (in canvas units) is inside bounding box,...
void _invalidate_ctrl_handles() override
void build_cache(int device_scale) const
Build object-specific cache.
void set_selected(bool selected=true)
void set_normal(bool selected=false)
Reset the state to normal or normal selected.
std::shared_ptr< Cairo::ImageSurface const > _cache
void set_shape(CanvasItemCtrlShape shape)
void _update(bool propagate) override
Update and redraw control ctrl.
void set_stroke(uint32_t rgba) override
int get_pixmap_width(int device_scale) const
Handles::TypeState _handle
void _render(CanvasItemBuffer &buf) const override
Render ctrl to screen via Cairo.
CanvasItemCtrlShape _shape
void set_hover(bool hover=true)
void set_type(CanvasItemCtrlType type)
void set_size(HandleSize rel_size)
void set_anchor(SPAnchorType anchor)
void set_size_via_index(int size_index)
CanvasItemCtrl(CanvasItemGroup *group)
Create a null control node.
void set_position(Geom::Point const &position)
Set the position.
double closest_distance_to(Geom::Point const &p) const
Returns distance between point in canvas units and position of ctrl.
Geom::OptRect _bounds
Geom::Affine const & affine() const
CanvasItemContext * _context
static Preferences * get()
Access the singleton Preferences object.
int getIntLimited(Glib::ustring const &pref_path, int def=0, int min=INT_MIN, int max=INT_MAX)
Retrieve a limited integer.
struct _cairo_surface cairo_surface_t
SPAnchorType
Definition enums.h:18
@ SP_ANCHOR_W
Definition enums.h:33
@ SP_ANCHOR_CENTER
Definition enums.h:28
@ SP_ANCHOR_E
Definition enums.h:29
@ SP_ANCHOR_NW
Definition enums.h:34
@ SP_ANCHOR_SE
Definition enums.h:30
@ SP_ANCHOR_N
Definition enums.h:35
@ SP_ANCHOR_S
Definition enums.h:31
@ SP_ANCHOR_NE
Definition enums.h:36
@ SP_ANCHOR_SW
Definition enums.h:32
GenericRect< IntCoord > IntRect
Definition forward.h:57
Angle distance(Angle const &a, Angle const &b)
Definition angle.h:163
std::shared_ptr< Cairo::ImageSurface const > draw(RenderParams const &params)
Helper class to stream background task notifications as a series of messages.
constexpr int MIN_INDEX
static double angle_of(Geom::Affine const &affine)
@ CANVAS_ITEM_CTRL_SHAPE_CARROW
@ CANVAS_ITEM_CTRL_SHAPE_SALIGN
@ CANVAS_ITEM_CTRL_SHAPE_CALIGN
@ CANVAS_ITEM_CTRL_SHAPE_PIVOT
@ CANVAS_ITEM_CTRL_SHAPE_DARROW
@ CANVAS_ITEM_CTRL_SHAPE_SARROW
@ CANVAS_ITEM_CTRL_SHAPE_MALIGN
@ CANVAS_ITEM_CTRL_TYPE_CENTER
@ CANVAS_ITEM_CTRL_TYPE_GUIDE_HANDLE
@ CANVAS_ITEM_CTRL_TYPE_POINTER
@ CANVAS_ITEM_CTRL_TYPE_NODE_AUTO
@ CANVAS_ITEM_CTRL_TYPE_ADJ_ROTATE
@ CANVAS_ITEM_CTRL_TYPE_POINT
@ CANVAS_ITEM_CTRL_TYPE_NODE_CUSP
@ CANVAS_ITEM_CTRL_TYPE_ADJ_HANDLE
@ CANVAS_ITEM_CTRL_TYPE_MARKER
@ CANVAS_ITEM_CTRL_TYPE_NODE_SMOOTH
@ CANVAS_ITEM_CTRL_TYPE_ADJ_SALIGN
@ CANVAS_ITEM_CTRL_TYPE_ADJ_CALIGN
@ CANVAS_ITEM_CTRL_TYPE_ADJ_SKEW
@ CANVAS_ITEM_CTRL_TYPE_ADJ_MALIGN
@ CANVAS_ITEM_CTRL_TYPE_ADJ_CENTER
static int get_size_default()
constexpr int MAX_INDEX
int buf
Singleton class to access the preferences file in a convenient way.
Class used when rendering canvas items.
double width
Affine transformation classes.