Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
calligraphic-tool.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Handwriting-like drawing mode
4 *
5 * Authors:
6 * Mitsuru Oka <oka326@parkcity.ne.jp>
7 * Lauris Kaplinski <lauris@kaplinski.com>
8 * bulia byak <buliabyak@users.sf.net>
9 * MenTaLguY <mental@rydia.net>
10 * Abhishek Sharma
11 * Jon A. Cruz <jon@joncruz.org>
12 *
13 * The original dynadraw code:
14 * Paul Haeberli <paul@sgi.com>
15 *
16 * Copyright (C) 1998 The Free Software Foundation
17 * Copyright (C) 1999-2005 authors
18 * Copyright (C) 2001-2002 Ximian, Inc.
19 * Copyright (C) 2005-2007 bulia byak
20 * Copyright (C) 2006 MenTaLguY
21 *
22 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
23 */
24
26
27#include <cstring>
28#include <numeric>
29#include <string>
30#include <random>
31
32#include <gdk/gdkkeysyms.h>
33#include <glibmm/i18n.h>
34#include <gtk/gtk.h>
35
36#include <2geom/bezier-utils.h>
37#include <2geom/circle.h>
38#include <2geom/pathvector.h>
39
40#include "context-fns.h"
41#include "desktop-events.h"
42#include "desktop-style.h"
43#include "desktop.h"
44#include "document-undo.h"
45#include "document.h"
46#include "message-context.h"
47#include "selection.h"
48
51#include "display/curve.h"
52#include "display/drawing.h"
53
54#include "livarot/Path.h"
55
56#include "object/sp-shape.h"
57#include "object/sp-text.h"
58
59#include "path/path-util.h"
60
61#include "svg/svg.h"
62
63#include "ui/icon-names.h"
65#include "ui/widget/canvas.h"
67
68#include "util/units.h"
69
73
74static constexpr double DDC_MIN_PRESSURE = 0.0;
75static constexpr double DDC_MAX_PRESSURE = 1.0;
76static constexpr double DDC_DEFAULT_PRESSURE = 1.0;
77
78static constexpr double DDC_MIN_TILT = -1.0;
79static constexpr double DDC_MAX_TILT = 1.0;
80static constexpr double DDC_DEFAULT_TILT = 0.0;
81
82static constexpr uint32_t DDC_RED_RGBA = 0xff0000ff;
83
84static constexpr double TOLERANCE_CALLIGRAPHIC = 0.1;
85
86static constexpr double DYNA_EPSILON = 0.5e-6;
87static constexpr double DYNA_EPSILON_START = 0.5e-2;
88static constexpr double DYNA_VEL_START = 1e-5;
89
90static constexpr bool DYNA_DRAW_VERBOSE = false;
91
92namespace Inkscape::UI::Tools {
93
95 : DynamicBase(desktop, "/tools/calligraphic", "calligraphy.svg")
96{
97 currentshape = make_canvasitem<CanvasItemBpath>(desktop->getCanvasSketch());
98 currentshape->set_stroke(0x0);
100
101 // Fixme: Can't we cascade it to root more clearly?
102 currentshape->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), desktop));
103
104 hatch_area = make_canvasitem<CanvasItemBpath>(desktop->getCanvasControls());
105 hatch_area->set_fill(0x0, SP_WIND_RULE_EVENODD);
106 hatch_area->set_stroke(0x0000007f);
107 hatch_area->set_pickable(false);
108 hatch_area->set_visible(false);
109
110 sp_event_context_read(this, "mass");
111 sp_event_context_read(this, "wiggle");
112 sp_event_context_read(this, "angle");
113 sp_event_context_read(this, "width");
114 sp_event_context_read(this, "thinning");
115 sp_event_context_read(this, "tremor");
116 sp_event_context_read(this, "flatness");
117 sp_event_context_read(this, "tracebackground");
118 sp_event_context_read(this, "usepressure");
119 sp_event_context_read(this, "usetilt");
120 sp_event_context_read(this, "abs_width");
121 sp_event_context_read(this, "keep_selected");
122 sp_event_context_read(this, "cap_rounding");
123
124 auto prefs = Preferences::get();
125 if (prefs->getBool("/tools/calligraphic/selcue")) {
127 }
128}
129
131{
132 auto const path = val.getEntryName();
133
134 if (path == "tracebackground") {
135 trace_bg = val.getBool();
136 } else if (path == "keep_selected") {
137 keep_selected = val.getBool();
138 } else {
139 // Pass on up to parent class to handle common attributes.
140 DynamicBase::set(val);
141 }
142}
143
145{
147
148 vel = {};
149 vel_max = 0.0;
150 acc = {};
151 ang = {};
152 del = {};
153}
154
156{
157 if (ext.pressure) {
159 } else {
161 }
162
163 if (ext.xtilt) {
164 xtilt = std::clamp(*ext.xtilt, DDC_MIN_TILT, DDC_MAX_TILT);
165 } else {
167 }
168
169 if (ext.ytilt) {
170 ytilt = std::clamp(*ext.ytilt, DDC_MIN_TILT, DDC_MAX_TILT);
171 } else {
173 }
174}
175
176static Geom::Point unsnapped_polar(double angle)
177{
178 Geom::Point v;
179 Geom::sincos(angle, v.y(), v.x());
180 return v;
181}
182
184{
185 auto const n = getNormalizedPoint(p);
186
187 // Calculate mass and drag.
188 double const mass_scaled = Geom::lerp(mass, 1.0, 160.0);
189 double const drag_scaled = Geom::lerp(drag * drag, 0.0, 0.5);
190
191 // Calculate force and acceleration.
192 auto const force = n - cur;
193
194 // If force is below the absolute threshold DYNA_EPSILON,
195 // or we haven't yet reached DYNA_VEL_START (i.e. at the beginning of stroke)
196 // _and_ the force is below the (higher) DYNA_EPSILON_START threshold,
197 // discard this move.
198 // This prevents flips, blobs, and jerks caused by microscopic tremor of the tablet pen,
199 // especially bothersome at the start of the stroke where we don't yet have the inertia to
200 // smooth them out.
201 if (Geom::L2(force) < DYNA_EPSILON || (vel_max < DYNA_VEL_START && Geom::L2(force) < DYNA_EPSILON_START)) {
202 return false;
203 }
204
205 acc = force / mass_scaled;
206
207 // Calculate new velocity.
208 vel += acc;
209
210 vel_max = std::max(vel_max, Geom::L2(vel));
211
212 // Calculate angle of drawing tool.
213
214 double a1;
215 if (usetilt) {
216 // 1a. calculate nib angle from input device tilt:
217 if (xtilt == 0 && ytilt == 0) {
218 // to be sure that atan2 in the computation below
219 // would not crash or return NaN.
220 a1 = 0;
221 } else {
222 auto dir = Geom::Point(-xtilt, ytilt);
223 a1 = atan2(dir);
224 }
225 } else {
226 // 1b. fixed dc->angle (absolutely flat nib):
227 a1 = Geom::rad_from_deg(angle);
228 }
229 a1 *= -_desktop->yaxisdir();
230 if (flatness < 0.0) {
231 // flips direction. Useful when this->usetilt
232 // allows simulating both pen and calligraphic brush
233 a1 *= -1;
234 }
235 a1 = std::fmod(a1, M_PI);
236 if (a1 > 0.5 * M_PI) {
237 a1 -= M_PI;
238 } else if (a1 <= -0.5 * M_PI) {
239 a1 += M_PI;
240 }
241
242 // 2. perpendicular to dc->vel (absolutely non-flat nib):
243 double const mag_vel = Geom::L2(vel);
244 if (mag_vel < DYNA_EPSILON) {
245 return false;
246 }
247 auto const ang2 = Geom::rot90(vel) / mag_vel;
248
249 // 3. Average them using flatness parameter:
250 // calculate angles
251 double a2 = atan2(ang2);
252 // flip a2 to force it to be in the same half-circle as a1
253 bool flipped = false;
254 if (std::abs(a2 - a1) > 0.5 * M_PI) {
255 a2 += M_PI;
256 flipped = true;
257 }
258 // normalize a2
259 if (a2 > M_PI) {
260 a2 -= 2 * M_PI;
261 } else if (a2 < -M_PI) {
262 a2 += 2 * M_PI;
263 }
264 // find the flatness-weighted bisector angle, unflip if a2 was flipped
265 // FIXME: when dc->vel is oscillating around the fixed angle, the new_ang flips back and forth. How to avoid this?
266 double new_ang = a1 + (1 - std::abs(flatness)) * (a2 - a1) - (flipped ? M_PI : 0);
267 // Try to detect a sudden flip when the new angle differs too much from the previous for the
268 // current velocity; in that case discard this move
269 auto const new_ang_vec = unsnapped_polar(new_ang);
270 double angle_delta = Geom::L2(new_ang_vec - ang);
271 if (angle_delta / Geom::L2(vel) > 4000) {
272 return false;
273 }
274
275 // convert to point
276 ang = new_ang_vec;
277
278 if constexpr (false) g_print("force %g acc %g vel_max %g vel %g a1 %g a2 %g new_ang %g\n", Geom::L2(force), Geom::L2(acc), vel_max, Geom::L2(vel), a1, a2, new_ang);
279
280 // Apply drag
281 vel *= 1.0 - drag_scaled;
282
283 // Update position
284 last = cur;
285 cur += vel;
286
287 return true;
288}
289
291{
292 g_assert(npoints >= 0 && npoints < SAMPLING_SIZE);
293
294 // How much velocity thins strokestyle
295 double const vel_thin_scaled = Geom::lerp(vel_thin, 0, 160);
296
297 // Influence of pressure on thickness
298 double const pressure_thick = usepressure ? pressure : 1.0;
299
300 // get the real brush point, not the same as pointer (affected by hatch tracking and/or mass drag)
301 auto const brush = getViewPoint(cur);
302 auto const brush_w = _desktop->d2w(brush);
303
304 double trace_thick = 1;
305 if (trace_bg) {
306 // Trace background, use single pixel under brush.
307 auto const area = Geom::IntRect::from_xywh(brush_w.floor(), Geom::IntPoint(1, 1));
308
309 auto const canvas_item_drawing = _desktop->getCanvasDrawing();
310 auto const drawing = canvas_item_drawing->get_drawing();
311
312 // Get average color.
313 auto avg = drawing->averageColor(area);
314 auto A = avg.stealOpacity();
315
316 // Convert to thickness.
317 std::vector<double> vals = avg.getValues();
318 double max = std::max({vals[0], vals[1], vals[2]});
319 double min = std::min({vals[0], vals[1], vals[2]});
320 double const L = A * (max + min) / 2 + (1 - A); // blend with white bg
321 trace_thick = 1 - L;
322 if constexpr(false) g_print("L %g thick %g\n", L, trace_thick);
323 }
324
325 double width_adjusted = (pressure_thick * trace_thick - vel_thin_scaled * vel.length()) * width;
326
327 double tremble_left = 0, tremble_right = 0;
328 if (tremor > 0) {
329 auto gen = std::default_random_engine(g_random_int());
330 auto nrm = std::normal_distribution();
331
332 // deflect both left and right edges randomly and independently, so that:
333 // (1) dc->tremor=1 corresponds to sigma=1, decreasing dc->tremor narrows the bell curve;
334 // (2) deflection depends on width, but is upped for small widths for better visual uniformity across widths;
335 // (3) deflection somewhat depends on speed, to prevent fast strokes looking
336 // comparatively smooth and slow ones excessively jittery
337 auto const sigma = tremor * (0.15 + 0.8 * width_adjusted) * (0.35 + 14 * vel.length());
338 tremble_left = nrm(gen) * sigma;
339 tremble_right = nrm(gen) * sigma;
340 }
341
342 width_adjusted = std::max(width_adjusted, 0.02 * width);
343
344 double dezoomify_factor = 0.05 * 1000;
345 if (!abs_width) {
346 dezoomify_factor /= _desktop->current_zoom();
347 }
348
349 auto const del_left = dezoomify_factor * (width_adjusted + tremble_left) * ang;
350 auto const del_right = dezoomify_factor * (width_adjusted + tremble_right) * ang;
351
352 point1[npoints] = brush + del_left;
353 point2[npoints] = brush - del_right;
354
355 del = 0.5 * (del_left + del_right);
356
357 npoints++;
358}
359
361{
362 dragging = false;
363 is_drawing = false;
364
366
367 // Remove all temporary line segments.
368 segments.clear();
369
370 // Reset accumulated curve.
373
374 repr = nullptr;
375}
376
378{
379 bool ret = false;
380
381 auto prefs = Preferences::get();
382 auto unit = Util::UnitTable::get().getUnit(prefs->getString("/tools/calligraphic/unit"));
383
384 inspect_event(event,
385 [&] (ButtonPressEvent const &event) {
386 if (event.num_press == 1 && event.button == 1) {
387 if (!have_viable_layer(_desktop, defaultMessageContext())) {
388 ret = true;
389 return;
390 }
391
393
394 repr = nullptr;
395
396 // initialize first point
397 npoints = 0;
398
400
401 ret = true;
402
404 is_drawing = true;
406 }
407 },
408
409 [&] (MotionEvent const &event) {
410 auto motion_dt = _desktop->w2d(event.pos);
411 extinput(event.extinput);
412
413 message_context->clear();
414
415 // for hatching:
416 double hatch_dist = 0;
417 Geom::Point hatch_unit_vector;
418 Geom::Point nearest;
419 Geom::Point pointer;
420 Geom::Affine motion_to_curve;
421
422 if (event.modifiers & GDK_CONTROL_MASK) { // hatching - sense the item
423
424 auto const selected = _desktop->getSelection()->singleItem();
425 if (selected && (is<SPShape>(selected) || is<SPText>(selected))) {
426 // One item selected, and it's a path;
427 // let's try to track it as a guide
428
429 if (selected != hatch_item) {
430 hatch_item = selected;
431 hatch_livarot_path = Path_for_item(hatch_item, true, true);
432 if (hatch_livarot_path) {
433 hatch_livarot_path->ConvertWithBackData(0.01);
434 }
435 }
436
437 // calculate pointer point in the guide item's coords
438 motion_to_curve = selected->dt2i_affine() * selected->i2doc_affine();
439 pointer = motion_dt * motion_to_curve;
440
441 // calculate the nearest point on the guide path
442 std::optional<Path::cut_position> position = get_nearest_position_on_Path(hatch_livarot_path.get(), pointer);
443 if (position) {
444 nearest = get_point_on_Path(hatch_livarot_path.get(), position->piece, position->t);
445
446 // distance from pointer to nearest
447 hatch_dist = Geom::L2(pointer - nearest);
448 // unit-length vector
449 hatch_unit_vector = (pointer - nearest) / hatch_dist;
450
451 message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Guide path selected</b>; start drawing along the guide with <b>Ctrl</b>"));
452 }
453 } else {
454 message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Select a guide path</b> to track with <b>Ctrl</b>"));
455 }
456 }
457
458 if (is_drawing && (event.modifiers & GDK_BUTTON1_MASK)) {
459 dragging = true;
460
461 if (event.modifiers & GDK_CONTROL_MASK && hatch_item) { // hatching
462
463 constexpr auto HATCH_VECTOR_ELEMENTS = 12;
464 constexpr auto INERTIA_ELEMENTS = 24;
465 constexpr auto SPEED_ELEMENTS = 12;
466 constexpr auto SPEED_MIN = 0.3;
467 constexpr auto SPEED_NORMAL = 0.35;
468 constexpr auto INERTIA_FORCE = 0.5;
469
470 // speed is the movement of the nearest point along the guide path, divided by
471 // the movement of the pointer at the same period; it is averaged for the last
472 // SPEED_ELEMENTS motion events. Normally, as you track the guide path, speed
473 // is about 1, i.e. the nearest point on the path is moved by about the same
474 // distance as the pointer. If the speed starts to decrease, we are losing
475 // contact with the guide; if it drops below SPEED_MIN, we are on our own and
476 // not attracted to guide anymore. Most often this happens when you have
477 // tracked to the end of a guide calligraphic stroke and keep moving
478 // further. We try to handle this situation gracefully: not stick with the
479 // guide forever but let go of it smoothly and without sharp jerks (non-zero
480 // mass recommended; with zero mass, jerks are still quite noticeable).
481
482 double speed = 1;
483 if (Geom::L2(hatch_last_nearest) != 0) {
484 // the distance nearest moved since the last motion event
485 double nearest_moved = Geom::L2(nearest - hatch_last_nearest);
486 // the distance pointer moved since the last motion event
487 double pointer_moved = Geom::L2(pointer - hatch_last_pointer);
488
489 // store them in stacks limited to SPEED_ELEMENTS
490 hatch_nearest_past.push_front(nearest_moved);
491 if (hatch_nearest_past.size() > SPEED_ELEMENTS) {
492 hatch_nearest_past.pop_back();
493 }
494 hatch_pointer_past.push_front(pointer_moved);
495 if (hatch_pointer_past.size() > SPEED_ELEMENTS) {
496 hatch_pointer_past.pop_back();
497 }
498
499 // If the stacks are full,
500 if (hatch_nearest_past.size() == SPEED_ELEMENTS) {
501 // calculate the sums of all stored movements
502 double nearest_sum = std::accumulate(hatch_nearest_past.begin(), hatch_nearest_past.end(), 0.0);
503 double pointer_sum = std::accumulate(hatch_pointer_past.begin(), hatch_pointer_past.end(), 0.0);
504 // and divide to get the speed
505 speed = nearest_sum/pointer_sum;
506 if constexpr (false) g_print("nearest sum %g pointer_sum %g speed %g\n", nearest_sum, pointer_sum, speed);
507 }
508 }
509
510 if ( hatch_escaped // already escaped, do not reattach
511 || speed < SPEED_MIN // stuck; most likely reached end of traced stroke
512 || (hatch_spacing > 0 && hatch_dist > 50 * hatch_spacing) // went too far from the guide
513 )
514 {
515 // We are NOT attracted to the guide!
516
517 // Remember hatch_escaped so we don't get
518 // attracted again until the end of this stroke
519 hatch_escaped = true;
520
521 if (inertia_vectors.size() >= INERTIA_ELEMENTS / 2) { // move by inertia
522 auto const moved_past_escape = motion_dt - inertia_vectors.front();
523 auto const inertia = inertia_vectors.front() - inertia_vectors.back();
524
525 double dot = Geom::dot(moved_past_escape, inertia);
526 dot /= moved_past_escape.length() * inertia.length();
527
528 if (dot > 0) { // mouse is still moving in approx the same direction
529 auto const should_have_moved = inertia.normalized() * moved_past_escape.length();
530 motion_dt = inertia_vectors.front() + Geom::lerp(INERTIA_FORCE, moved_past_escape, should_have_moved);
531 }
532 }
533
534 } else {
535
536 // Calculate angle cosine of this vector-to-guide and all past vectors
537 // summed, to detect if we accidentally flipped to the other side of the guide
538 auto const hatch_vector_accumulated = std::accumulate(hatch_vectors.begin(), hatch_vectors.end(), Geom::Point());
539 double dot = Geom::dot(pointer - nearest, hatch_vector_accumulated);
540 dot /= Geom::L2(pointer - nearest) * Geom::L2(hatch_vector_accumulated);
541
542 if (hatch_spacing != 0) { // spacing was already set
543 double target;
544 if (speed > SPEED_NORMAL) {
545 // all ok, strictly obey the spacing
546 target = hatch_spacing;
547 } else {
548 // looks like we're starting to lose speed,
549 // so _gradually_ let go attraction to prevent jerks
550 target = (hatch_spacing * speed + hatch_dist * (SPEED_NORMAL - speed)) / SPEED_NORMAL;
551 }
552 if (!std::isnan(dot) && dot < -0.5) { // flip
553 target = -target;
554 }
555
556 // This is the track pointer that we will use instead of the real one
557 auto const new_pointer = nearest + target * hatch_unit_vector;
558
559 // some limited feedback: allow persistent pulling to slightly change
560 // the spacing
561 hatch_spacing += (hatch_dist - hatch_spacing) / 3500;
562
563 // return it to the desktop coords
564 motion_dt = new_pointer * motion_to_curve.inverse();
565
566 if (speed >= SPEED_NORMAL) {
567 inertia_vectors.push_front(motion_dt);
568 if (inertia_vectors.size() > INERTIA_ELEMENTS) {
569 inertia_vectors.pop_back();
570 }
571 }
572
573 } else {
574 // this is the first motion event, set the dist
575 hatch_spacing = hatch_dist;
576 }
577
578 // remember last points
579 hatch_last_pointer = pointer;
580 hatch_last_nearest = nearest;
581
582 hatch_vectors.push_front(pointer - nearest);
583 if (hatch_vectors.size() > HATCH_VECTOR_ELEMENTS) {
584 hatch_vectors.pop_back();
585 }
586 }
587
588 message_context->set(Inkscape::NORMAL_MESSAGE, hatch_escaped? _("Tracking: <b>connection to guide path lost!</b>") : _("<b>Tracking</b> a guide path"));
589
590 } else {
591 message_context->set(Inkscape::NORMAL_MESSAGE, _("<b>Drawing</b> a calligraphic stroke"));
592 }
593
594 if (just_started_drawing) {
595 just_started_drawing = false;
596 reset(motion_dt);
597 }
598
599 if (!apply(motion_dt)) {
600 ret = true;
601 return;
602 }
603
604 if (cur != last) {
605 brush();
606 g_assert(npoints > 0);
607 fit_and_split(false);
608 }
609 ret = true;
610 }
611
612 Geom::PathVector path = Geom::Path(Geom::Circle(0,0,1)); // Unit circle centered at origin.
613
614 // Draw the hatching circle if necessary
615 if (event.modifiers & GDK_CONTROL_MASK) {
616 if (hatch_spacing == 0 && hatch_dist != 0) {
617 // Haven't set spacing yet: gray, center free, update radius live
618
619 auto const c = _desktop->w2d(event.pos);
620 path *= Geom::Scale(hatch_dist) * Geom::Translate(c);
621
622 hatch_area->set_bpath(std::move(path), true);
623 hatch_area->set_stroke(0x7f7f7fff);
624 hatch_area->set_visible(true);
625
626 } else if (dragging && !hatch_escaped && hatch_dist != 0) {
627 // Tracking: green, center snapped, fixed radius
628
629 auto const c = motion_dt;
630 path *= Geom::Scale(hatch_spacing) * Geom::Translate(c);
631
632 hatch_area->set_bpath(std::move(path), true);
633 hatch_area->set_stroke(0x00ff00ff);
634 hatch_area->set_visible(true);
635
636 } else if (dragging && hatch_escaped && hatch_dist != 0) {
637 // Tracking escaped: red, center free, fixed radius
638
639 auto const c = motion_dt;
640 path *= Geom::Scale(hatch_spacing) * Geom::Translate(c);
641
642 hatch_area->set_bpath(std::move(path), true);
643 hatch_area->set_stroke(0xff0000ff);
644 hatch_area->set_visible(true);
645
646 } else {
647 // Not drawing but spacing set: gray, center snapped, fixed radius
648
649 auto const c = (nearest + hatch_spacing * hatch_unit_vector) * motion_to_curve.inverse();
650 if (!std::isnan(c.x()) && !std::isnan(c.y()) && hatch_spacing != 0) {
651 path *= Geom::Scale(hatch_spacing) * Geom::Translate(c);
652
653 hatch_area->set_bpath(std::move(path), true);
654 hatch_area->set_stroke(0x7f7f7fff);
655 hatch_area->set_visible(true);
656 }
657 }
658 } else {
659 hatch_area->set_visible(false);
660 }
661 },
662
663 [&] (ButtonReleaseEvent const &event) {
664 auto const motion_dt = _desktop->w2d(event.pos);
665
666 ungrabCanvasEvents();
667
668 set_high_motion_precision(false);
669 is_drawing = false;
670
671 if (dragging && event.button == 1) {
672 dragging = false;
673
674 apply(motion_dt);
675
676 // Remove all temporary line segments.
677 segments.clear();
678
679 // Create object
680 fit_and_split(true);
681 if (accumulate()) {
682 set_to_accumulated(event.modifiers & GDK_SHIFT_MASK, event.modifiers & GDK_ALT_MASK); // performs document_done
683 } else {
684 g_warning("Failed to create path: invalid data in dc->cal1 or dc->cal2");
685 }
686
687 // Reset accumulated curve.
688 accumulated.reset();
689
690 clear_current();
691 repr = nullptr;
692
693 hatch_pointer_past.clear();
694 hatch_nearest_past.clear();
695 inertia_vectors.clear();
696 hatch_vectors.clear();
697 hatch_last_nearest = {};
698 hatch_last_pointer = {};
699 hatch_escaped = false;
700 hatch_item = nullptr;
701 hatch_livarot_path.reset();
702 just_started_drawing = false;
703
704 if (hatch_spacing != 0 && !keep_selected) {
705 // we do not select the newly drawn path, so increase spacing by step
706 if (hatch_spacing_step == 0) {
707 hatch_spacing_step = hatch_spacing;
708 }
709 hatch_spacing += hatch_spacing_step;
710 }
711
712 message_context->clear();
713 ret = true;
714 } else if (!dragging
715 && event.button == 1
716 && have_viable_layer(_desktop, defaultMessageContext()))
717 {
718 spdc_create_single_dot(this, _desktop->w2d(event.pos), "/tools/calligraphic", event.modifiers);
719 ret = true;
720 }
721 },
722
723 [&] (KeyPressEvent const &event) {
724 switch (get_latin_keyval(event)) {
725 case GDK_KEY_Up:
726 case GDK_KEY_KP_Up:
727 if (!mod_ctrl_only(event)) {
728 angle = std::min(angle + 5.0, 90.0);
729 _desktop->setToolboxAdjustmentValue("calligraphy-angle", angle);
730 ret = true;
731 }
732 break;
733 case GDK_KEY_Down:
734 case GDK_KEY_KP_Down:
735 if (!mod_ctrl_only(event)) {
736 angle = std::max(angle - 5.0, -90.0);
737 _desktop->setToolboxAdjustmentValue("calligraphy-angle", angle);
738 ret = true;
739 }
740 break;
741 case GDK_KEY_Right:
742 case GDK_KEY_KP_Right:
743 if (!mod_ctrl_only(event)) {
744 width = Quantity::convert(width, "px", unit);
745 width = std::min(width + 0.01, 1.0);
746 _desktop->setToolboxAdjustmentValue("calligraphy-width", width * 100); // the same spinbutton is for alt+x
747 ret = true;
748 }
749 break;
750 case GDK_KEY_Left:
751 case GDK_KEY_KP_Left:
752 if (!mod_ctrl_only(event)) {
753 width = Quantity::convert(width, "px", unit);
754 width = std::max(width - 0.01, 0.00001);
755 _desktop->setToolboxAdjustmentValue("calligraphy-width", width * 100);
756 ret = true;
757 }
758 break;
759 case GDK_KEY_Home:
760 case GDK_KEY_KP_Home:
761 width = 0.00001;
762 _desktop->setToolboxAdjustmentValue("calligraphy-width", width * 100);
763 ret = true;
764 break;
765 case GDK_KEY_End:
766 case GDK_KEY_KP_End:
767 width = 1.0;
768 _desktop->setToolboxAdjustmentValue("calligraphy-width", width * 100);
769 ret = true;
770 break;
771 case GDK_KEY_x:
772 case GDK_KEY_X:
773 if (mod_alt_only(event)) {
774 _desktop->setToolboxFocusTo("calligraphy-width");
775 ret = true;
776 }
777 break;
778 case GDK_KEY_Escape:
779 if (is_drawing) {
780 // if drawing, cancel, otherwise pass it up for deselecting
781 cancel();
782 ret = true;
783 }
784 break;
785 case GDK_KEY_z:
786 case GDK_KEY_Z:
787 if (mod_ctrl_only(event) && is_drawing) {
788 // if drawing, cancel, otherwise pass it up for undo
789 cancel();
790 ret = true;
791 }
792 break;
793 default:
794 break;
795 }
796 },
797
798 [&] (KeyReleaseEvent const &event) {
799 switch (get_latin_keyval(event)) {
800 case GDK_KEY_Control_L:
801 case GDK_KEY_Control_R:
802 message_context->clear();
803 hatch_spacing = 0;
804 hatch_spacing_step = 0;
805 break;
806 default:
807 break;
808 }
809 },
810
811 [&] (CanvasEvent const &event) {}
812 );
813
814 return ret || DynamicBase::root_handler(event);
815}
816
817void CalligraphicTool::clear_current()
818{
819 // reset bpath
820 currentshape->set_bpath(nullptr);
821
822 // reset curve
823 currentcurve.reset();
824 cal1.reset();
825 cal2.reset();
826
827 // reset points
828 npoints = 0;
829}
830
831void CalligraphicTool::set_to_accumulated(bool unionize, bool subtract) {
832 if (!accumulated.is_empty()) {
833 if (!repr) {
834 /* Create object */
835 Inkscape::XML::Document *xml_doc = _desktop->doc()->getReprDoc();
836 Inkscape::XML::Node *repr = xml_doc->createElement("svg:path");
837
838 /* Set style */
839 sp_desktop_apply_style_tool(_desktop, repr, "/tools/calligraphic", false);
840
841 this->repr = repr;
842
843 auto layer = currentLayer();
844 auto item = cast<SPItem>(layer->appendChildRepr(this->repr));
845 Inkscape::GC::release(this->repr);
846 item->transform = layer->i2doc_affine().inverse();
847 item->updateRepr();
848 }
849
850 Geom::PathVector pathv = accumulated.get_pathvector() * _desktop->dt2doc();
851 repr->setAttribute("d", sp_svg_write_path(pathv));
852
853 if (unionize) {
854 _desktop->getSelection()->add(this->repr);
855 _desktop->getSelection()->pathUnion(true);
856 } else if (subtract) {
857 _desktop->getSelection()->add(this->repr);
858 _desktop->getSelection()->pathDiff(true);
859 } else {
860 if (this->keep_selected) {
861 _desktop->getSelection()->set(this->repr);
862 }
863 }
864
865 // Now we need to write the transform information.
866 // First, find out whether our repr is still linked to a valid object. In this case,
867 // we need to write the transform data only for this element.
868 // Either there was no boolean op or it failed.
869 auto result = cast<SPItem>(_desktop->doc()->getObjectByRepr(this->repr));
870
871 if (result == nullptr) {
872 // The boolean operation succeeded.
873 // Now we fetch the single item, that has been set as selected by the boolean op.
874 // This is its result.
875 result = _desktop->getSelection()->singleItem();
876 }
877 result->doWriteTransform(result->transform, nullptr, true);
878 } else {
879 if (this->repr) {
880 sp_repr_unparent(this->repr);
881 }
882
883 this->repr = nullptr;
884 }
885
886 DocumentUndo::done(_desktop->getDocument(), _("Draw calligraphic stroke"), INKSCAPE_ICON("draw-calligraphic"));
887}
888
889static void
891 Geom::Point const &from,
892 Geom::Point const &to,
893 double rounding)
894{
895 if (Geom::L2( to - from ) > DYNA_EPSILON) {
896 Geom::Point vel = rounding * Geom::rot90( to - from ) / sqrt(2.0);
897 double mag = Geom::L2(vel);
898
899 Geom::Point v = mag * Geom::rot90( to - from ) / Geom::L2( to - from );
900 curve.curveto(from + v, to + v, to);
901 }
902}
903
904bool CalligraphicTool::accumulate() {
905 if (
906 cal1.is_empty() ||
907 cal2.is_empty() ||
908 (cal1.get_segment_count() <= 0) ||
909 cal1.first_path()->closed()
910 ) {
911
912 cal1.reset();
913 cal2.reset();
914
915 return false; // failure
916 }
917
918 auto rev_cal2 = cal2.reversed();
919
920 if ((rev_cal2.get_segment_count() <= 0) || rev_cal2.first_path()->closed()) {
921 cal1.reset();
922 cal2.reset();
923
924 return false; // failure
925 }
926
927 Geom::Curve const * dc_cal1_firstseg = cal1.first_segment();
928 Geom::Curve const * rev_cal2_firstseg = rev_cal2.first_segment();
929 Geom::Curve const * dc_cal1_lastseg = cal1.last_segment();
930 Geom::Curve const * rev_cal2_lastseg = rev_cal2.last_segment();
931
932 accumulated.reset(); /* Is this required ?? */
933
934 accumulated.append(cal1);
935
936 add_cap(accumulated, dc_cal1_lastseg->finalPoint(), rev_cal2_firstseg->initialPoint(), cap_rounding);
937
938 accumulated.append(rev_cal2, true);
939
940 add_cap(accumulated, rev_cal2_lastseg->finalPoint(), dc_cal1_firstseg->initialPoint(), cap_rounding);
941
942 accumulated.closepath();
943
944 cal1.reset();
945 cal2.reset();
946
947 return true; // success
948}
949
950void CalligraphicTool::fit_and_split(bool release)
951{
952 double const tolerance_sq = Geom::sqr(_desktop->w2d().descrim() * TOLERANCE_CALLIGRAPHIC);
953
954 if constexpr (DYNA_DRAW_VERBOSE) {
955 g_print("[F&S:R=%c]", release?'T':'F');
956 }
957
958 if (!( this->npoints > 0 && this->npoints < SAMPLING_SIZE )) {
959 return; // just clicked
960 }
961
962 if ( this->npoints == SAMPLING_SIZE - 1 || release ) {
963#define BEZIER_SIZE 4
964#define BEZIER_MAX_BEZIERS 8
965#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS )
966
967 if constexpr (DYNA_DRAW_VERBOSE) {
968 g_print("[F&S:#] dc->npoints:%d, release:%s\n",
969 this->npoints, release ? "TRUE" : "FALSE");
970 }
971
972 /* Current calligraphic */
973 if ( cal1.is_empty() || cal2.is_empty() ) {
974 /* dc->npoints > 0 */
975 /* g_print("calligraphics(1|2) reset\n"); */
976 cal1.reset();
977 cal2.reset();
978
979 cal1.moveto(this->point1[0]);
980 cal2.moveto(this->point2[0]);
981 }
982
983 Geom::Point b1[BEZIER_MAX_LENGTH];
984 gint const nb1 = Geom::bezier_fit_cubic_r(b1, this->point1, this->npoints,
985 tolerance_sq, BEZIER_MAX_BEZIERS);
986 g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) );
987
988 Geom::Point b2[BEZIER_MAX_LENGTH];
989 gint const nb2 = Geom::bezier_fit_cubic_r(b2, this->point2, this->npoints,
990 tolerance_sq, BEZIER_MAX_BEZIERS);
991 g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) );
992
993 if ( nb1 != -1 && nb2 != -1 ) {
994 /* Fit and draw and reset state */
995 if constexpr (DYNA_DRAW_VERBOSE) {
996 g_print("nb1:%d nb2:%d\n", nb1, nb2);
997 }
998 /* CanvasShape */
999 if (! release) {
1000 currentcurve.reset();
1001 currentcurve.moveto(b1[0]);
1002 for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
1003 currentcurve.curveto(bp1[1], bp1[2], bp1[3]);
1004 }
1005 currentcurve.lineto(b2[BEZIER_SIZE*(nb2-1) + 3]);
1006 for (Geom::Point *bp2 = b2 + BEZIER_SIZE * ( nb2 - 1 ); bp2 >= b2; bp2 -= BEZIER_SIZE) {
1007 currentcurve.curveto(bp2[2], bp2[1], bp2[0]);
1008 }
1009 // FIXME: dc->segments is always NULL at this point??
1010 if (this->segments.empty()) { // first segment
1011 add_cap(currentcurve, b2[0], b1[0], cap_rounding);
1012 }
1013 currentcurve.closepath();
1014 currentshape->set_bpath(&currentcurve, true);
1015 }
1016
1017 /* Current calligraphic */
1018 for (Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
1019 cal1.curveto(bp1[1], bp1[2], bp1[3]);
1020 }
1021 for (Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
1022 cal2.curveto(bp2[1], bp2[2], bp2[3]);
1023 }
1024 } else {
1025 /* fixme: ??? */
1026 if constexpr (DYNA_DRAW_VERBOSE) {
1027 g_print("[fit_and_split] failed to fit-cubic.\n");
1028 }
1029 this->draw_temporary_box();
1030
1031 for (gint i = 1; i < this->npoints; i++) {
1032 cal1.lineto(this->point1[i]);
1033 }
1034 for (gint i = 1; i < this->npoints; i++) {
1035 cal2.lineto(this->point2[i]);
1036 }
1037 }
1038
1039 /* Fit and draw and copy last point */
1040 if constexpr (DYNA_DRAW_VERBOSE) {
1041 g_print("[%d]Yup\n", this->npoints);
1042 }
1043 if (!release) {
1044 g_assert(!currentcurve.is_empty());
1045
1046 auto fillColor = sp_desktop_get_color_tool(_desktop, "/tools/calligraphic", true);
1047 double opacity = sp_desktop_get_master_opacity_tool(_desktop, "/tools/calligraphic");
1048 double fillOpacity = sp_desktop_get_opacity_tool(_desktop, "/tools/calligraphic", true);
1049
1050 // TODO: This removes color space information.
1051 auto cbp = new Inkscape::CanvasItemBpath(_desktop->getCanvasSketch(), currentcurve.get_pathvector(), true);
1052 cbp->set_fill(fillColor ? fillColor->toRGBA(opacity * fillOpacity) : 0x0, SP_WIND_RULE_EVENODD);
1053 cbp->set_stroke(0x0);
1054
1055 /* fixme: Cannot we cascade it to root more clearly? */
1056 cbp->connect_event(sigc::bind(sigc::ptr_fun(sp_desktop_root_handler), _desktop));
1057
1058 segments.emplace_back(cbp);
1059 }
1060
1061 this->point1[0] = this->point1[this->npoints - 1];
1062 this->point2[0] = this->point2[this->npoints - 1];
1063 this->npoints = 1;
1064 } else {
1065 this->draw_temporary_box();
1066 }
1067}
1068
1069void CalligraphicTool::draw_temporary_box() {
1070 currentcurve.reset();
1071
1072 currentcurve.moveto(this->point2[this->npoints-1]);
1073
1074 for (gint i = this->npoints-2; i >= 0; i--) {
1075 currentcurve.lineto(this->point2[i]);
1076 }
1077
1078 for (gint i = 0; i < this->npoints; i++) {
1079 currentcurve.lineto(this->point1[i]);
1080 }
1081
1082 if (this->npoints >= 2) {
1083 add_cap(currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding);
1084 }
1085
1086 currentcurve.closepath();
1087 currentshape->set_bpath(&currentcurve, true);
1088}
1089
1090} // namespace Inkscape::UI::Tools
1091
1092/*
1093 Local Variables:
1094 mode:c++
1095 c-file-style:"stroustrup"
1096 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
1097 indent-tabs-mode:nil
1098 fill-column:99
1099 End:
1100*/
1101// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
TODO: insert short description here.
Bezier fitting algorithms.
static constexpr double DDC_MAX_TILT
static constexpr uint32_t DDC_RED_RGBA
static constexpr double DDC_MIN_PRESSURE
static constexpr double DYNA_EPSILON_START
static constexpr double DDC_MIN_TILT
static constexpr double DDC_DEFAULT_PRESSURE
static constexpr double DDC_DEFAULT_TILT
static constexpr double DYNA_VEL_START
static constexpr double TOLERANCE_CALLIGRAPHIC
static constexpr double DDC_MAX_PRESSURE
static constexpr bool DYNA_DRAW_VERBOSE
static constexpr double DYNA_EPSILON
Inkscape canvas widget.
Circle shape.
3x3 matrix representing an affine transformation.
Definition affine.h:70
Affine inverse() const
Compute the inverse matrix.
Definition affine.cpp:388
Set of all points at a fixed distance from the center.
Definition circle.h:55
Abstract continuous curve on a plane defined on [0,1].
Definition curve.h:78
virtual Point initialPoint() const =0
Retrieve the start of the curve.
virtual Point finalPoint() const =0
Retrieve the end of the curve.
static CRect from_xywh(C x, C y, C w, C h)
Create rectangle from origin and dimensions.
Two-dimensional point with integer coordinates.
Definition int-point.h:57
Sequence of subpaths.
Definition pathvector.h:122
Sequence of contiguous curves, aka spline.
Definition path.h:353
Two-dimensional point that doubles as a vector.
Definition point.h:66
Coord length() const
Compute the distance from origin.
Definition point.h:118
Scaling from the origin.
Definition transforms.h:150
Translation by a vector.
Definition transforms.h:115
Inkscape::Drawing * get_drawing()
double stealOpacity()
Get the opacity, and remove it from this color.
Definition color.cpp:417
static void done(SPDocument *document, Glib::ustring const &event_description, Glib::ustring const &undo_icon, unsigned int object_modified_tag=0)
Colors::Color averageColor(Geom::IntRect const &area) const
Definition drawing.cpp:374
Data type representing a typeless value of a preference.
Glib::ustring getEntryName() const
Get the last component of the preference's path.
bool getBool(bool def=false) const
Interpret the preference as a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
CanvasItemPtr< CanvasItemBpath > hatch_area
void set(Preferences::Entry const &val) override
Called by our pref_observer if a preference has been changed.
bool root_handler(CanvasEvent const &event) override
void extinput(ExtendedInput const &ext)
bool keep_selected
newly created objects remain selected
Geom::Point getViewPoint(Geom::Point const &n) const
Geom::Point getNormalizedPoint(Geom::Point const &v) const
void set(Preferences::Entry const &val) override
Called by our pref_observer if a preference has been changed.
bool abs_width
uses absolute width independent of zoom
Geom::Point point1[SAMPLING_SIZE]
left edge points for this segment
SPCurve accumulated
accumulated shape which ultimately goes in svg:path
CanvasItemPtr< CanvasItemBpath > currentshape
canvas item for red "leading" segment
std::vector< CanvasItemPtr< CanvasItemBpath > > segments
canvas items for "committed" segments
int npoints
number of edge points for this segment
Geom::Point point2[SAMPLING_SIZE]
right edge points for this segment
void ungrabCanvasEvents()
Ungrab events from the Canvas Catchall.
void grabCanvasEvents(EventMask mask=EventType::KEY_PRESS|EventType::BUTTON_RELEASE|EventType::MOTION|EventType::BUTTON_PRESS)
Grab events from the Canvas Catchall.
bool dragging
are we dragging?
Definition tool-base.h:146
void set_high_motion_precision(bool high_precision=true)
Enable (or disable) high precision for motion events.
virtual bool root_handler(CanvasEvent const &event)
void enableSelectionCue(bool enable=true)
Enables/disables the ToolBase's SelCue.
static UnitTable & get()
Definition units.cpp:410
Unit const * getUnit(Glib::ustring const &name) const
Retrieve a given unit based on its string identifier.
Definition units.cpp:285
Interface for refcounted XML nodes.
Definition node.h:80
Wrapper around a Geom::PathVector object.
Definition curve.h:26
void reset()
Set curve to empty curve.
Definition curve.cpp:118
To do: update description of desktop.
Definition desktop.h:149
double current_zoom() const
Definition desktop.h:335
Inkscape::CanvasItemGroup * getCanvasControls() const
Definition desktop.h:196
Inkscape::CanvasItemGroup * getCanvasSketch() const
Definition desktop.h:201
Geom::Affine const & d2w() const
Transformation from desktop to window coordinates.
Definition desktop.h:419
Inkscape::CanvasItemDrawing * getCanvasDrawing() const
Definition desktop.h:204
double yaxisdir() const
Definition desktop.h:426
Geom::Affine transform
Definition sp-item.h:138
Inkscape::XML::Node * updateRepr(unsigned int flags=SP_OBJECT_WRITE_EXT)
Updates the object's repr based on the object's state.
Css & result
double c[8][4]
bool sp_desktop_root_handler(Inkscape::CanvasEvent const &event, SPDesktop *desktop)
void sp_desktop_apply_style_tool(SPDesktop *desktop, Inkscape::XML::Node *repr, Glib::ustring const &tool_path, bool with_text)
Apply the desktop's current style or the tool style to repr.
double sp_desktop_get_master_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool *has_opacity)
std::optional< Color > sp_desktop_get_color_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill)
double sp_desktop_get_opacity_tool(SPDesktop *desktop, Glib::ustring const &tool, bool is_fill)
Editable view implementation.
TODO: insert short description here.
SVG drawing for display.
constexpr int SAMPLING_SIZE
constexpr Coord lerp(Coord t, Coord a, Coord b)
Numerically stable linear interpolation.
Definition coord.h:97
Macro for icon names used in Inkscape.
SPItem * item
Interface for locally managing a current status message.
double angle(std::vector< Point > const &A)
void sincos(double angle, double &sin_, double &cos_)
Simultaneously compute a sine and a cosine of the same angle.
Definition math-utils.h:89
int bezier_fit_cubic_r(Point bezier[], Point const data[], int len, double error, unsigned max_beziers)
Fit a multi-segment Bezier curve to a set of digitized points, with possible weedout of identical poi...
T sqr(const T &x)
Definition math-utils.h:57
SBasis L2(D2< SBasis > const &a, unsigned k)
Definition d2-sbasis.cpp:42
T dot(D2< T > const &a, D2< T > const &b)
Definition d2.h:355
D2< T > rot90(D2< T > const &a)
Definition d2.h:397
static R & release(R &r)
Decrements the reference count of a anchored object.
static void add_cap(SPCurve &curve, Geom::Point const &from, Geom::Point const &to, double rounding)
unsigned get_latin_keyval(GtkEventControllerKey const *const controller, unsigned const keyval, unsigned const keycode, GdkModifierType const state, unsigned *consumed_modifiers)
Return the keyval corresponding to the event controller key in Latin group.
void spdc_create_single_dot(ToolBase *tool, Geom::Point const &pt, char const *path, unsigned event_state)
Create a single dot represented by a circle.
void sp_event_context_read(ToolBase *tool, char const *key)
Calls virtual set() function of ToolBase.
static Geom::Point unsnapped_polar(double angle)
void inspect_event(E &&event, Fs... funcs)
Perform pattern-matching on a CanvasEvent.
bool mod_ctrl_only(unsigned modifiers)
bool mod_alt_only(unsigned modifiers)
@ NORMAL_MESSAGE
Definition message.h:26
bool have_viable_layer(SPDesktop *desktop, MessageContext *message)
Check to see if the current layer is both unhidden and unlocked.
Geom::Point get_point_on_Path(Path *path, int piece, double t)
Gets the point at a particular time in a particular piece in a path description.
std::optional< Path::cut_position > get_nearest_position_on_Path(Path *path, Geom::Point p, unsigned seg)
Get the nearest position given a Livarot Path and a point.
std::unique_ptr< Path > Path_for_item(SPItem *item, bool doTransformation, bool transformFull)
Creates a Livarot Path object from an SPItem.
Definition path-util.cpp:32
Path utilities.
PathVector - a sequence of subpaths.
void sp_repr_unparent(Inkscape::XML::Node *repr)
Remove repr from children of its parent node.
Definition repr.h:107
A mouse button (left/right/middle) is pressed.
Abstract base class for events.
Extended input data associated to events generated by graphics tablets.
std::optional< double > ytilt
std::optional< double > xtilt
std::optional< double > pressure
Movement of the mouse pointer.
Interface for XML documents.
Definition document.h:43
virtual Node * createElement(char const *name)=0
Definition curve.h:24
@ SP_WIND_RULE_EVENODD
Definition style-enums.h:26
static void sp_svg_write_path(Inkscape::SVG::PathString &str, Geom::Path const &p, bool normalize=false)
Definition svg-path.cpp:109
SPDesktop * desktop
void dot(Cairo::RefPtr< Cairo::Context > &cr, double x, double y)
double width