32#include <gdk/gdkkeysyms.h>
33#include <glibmm/i18n.h>
125 if (prefs->getBool(
"/tools/calligraphic/selcue")) {
134 if (path ==
"tracebackground") {
136 }
else if (path ==
"keep_selected") {
192 auto const force = n -
cur;
205 acc = force / mass_scaled;
227 a1 = Geom::rad_from_deg(
angle);
235 a1 = std::fmod(a1, M_PI);
236 if (a1 > 0.5 * M_PI) {
238 }
else if (a1 <= -0.5 * M_PI) {
251 double a2 = atan2(ang2);
253 bool flipped =
false;
254 if (std::abs(a2 - a1) > 0.5 * M_PI) {
261 }
else if (a2 < -M_PI) {
266 double new_ang = a1 + (1 - std::abs(
flatness)) * (a2 - a1) - (flipped ? M_PI : 0);
281 vel *= 1.0 - drag_scaled;
304 double trace_thick = 1;
310 auto const drawing = canvas_item_drawing->
get_drawing();
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);
322 if constexpr(
false) g_print(
"L %g thick %g\n", L, trace_thick);
325 double width_adjusted = (pressure_thick * trace_thick - vel_thin_scaled *
vel.
length()) *
width;
327 double tremble_left = 0, tremble_right = 0;
329 auto gen = std::default_random_engine(g_random_int());
330 auto nrm = std::normal_distribution();
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;
342 width_adjusted = std::max(width_adjusted, 0.02 *
width);
344 double dezoomify_factor = 0.05 * 1000;
349 auto const del_left = dezoomify_factor * (width_adjusted + tremble_left) *
ang;
350 auto const del_right = dezoomify_factor * (width_adjusted + tremble_right) *
ang;
355 del = 0.5 * (del_left + del_right);
386 if (event.num_press == 1 && event.button == 1) {
387 if (!have_viable_layer(_desktop, defaultMessageContext())) {
410 auto motion_dt = _desktop->w2d(event.pos);
411 extinput(event.extinput);
413 message_context->clear();
416 double hatch_dist = 0;
422 if (event.modifiers & GDK_CONTROL_MASK) {
424 auto const selected = _desktop->getSelection()->singleItem();
425 if (selected && (is<SPShape>(selected) || is<SPText>(selected))) {
429 if (selected != hatch_item) {
430 hatch_item = selected;
432 if (hatch_livarot_path) {
433 hatch_livarot_path->ConvertWithBackData(0.01);
438 motion_to_curve = selected->dt2i_affine() * selected->i2doc_affine();
439 pointer = motion_dt * motion_to_curve;
444 nearest =
get_point_on_Path(hatch_livarot_path.get(), position->piece, position->t);
447 hatch_dist =
Geom::L2(pointer - nearest);
449 hatch_unit_vector = (pointer - nearest) / hatch_dist;
451 message_context->set(
Inkscape::NORMAL_MESSAGE, _(
"<b>Guide path selected</b>; start drawing along the guide with <b>Ctrl</b>"));
458 if (is_drawing && (event.modifiers & GDK_BUTTON1_MASK)) {
461 if (event.modifiers & GDK_CONTROL_MASK && hatch_item) {
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;
483 if (
Geom::L2(hatch_last_nearest) != 0) {
485 double nearest_moved =
Geom::L2(nearest - hatch_last_nearest);
487 double pointer_moved =
Geom::L2(pointer - hatch_last_pointer);
490 hatch_nearest_past.push_front(nearest_moved);
491 if (hatch_nearest_past.size() > SPEED_ELEMENTS) {
492 hatch_nearest_past.pop_back();
494 hatch_pointer_past.push_front(pointer_moved);
495 if (hatch_pointer_past.size() > SPEED_ELEMENTS) {
496 hatch_pointer_past.pop_back();
500 if (hatch_nearest_past.size() == SPEED_ELEMENTS) {
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);
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);
512 || (hatch_spacing > 0 && hatch_dist > 50 * hatch_spacing)
519 hatch_escaped =
true;
521 if (inertia_vectors.size() >= INERTIA_ELEMENTS / 2) {
522 auto const moved_past_escape = motion_dt - inertia_vectors.front();
523 auto const inertia = inertia_vectors.front() - inertia_vectors.back();
526 dot /= moved_past_escape.length() * inertia.length();
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);
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);
542 if (hatch_spacing != 0) {
544 if (speed > SPEED_NORMAL) {
546 target = hatch_spacing;
550 target = (hatch_spacing * speed + hatch_dist * (SPEED_NORMAL - speed)) / SPEED_NORMAL;
552 if (!std::isnan(
dot) &&
dot < -0.5) {
557 auto const new_pointer = nearest + target * hatch_unit_vector;
561 hatch_spacing += (hatch_dist - hatch_spacing) / 3500;
564 motion_dt = new_pointer * motion_to_curve.inverse();
566 if (speed >= SPEED_NORMAL) {
567 inertia_vectors.push_front(motion_dt);
568 if (inertia_vectors.size() > INERTIA_ELEMENTS) {
569 inertia_vectors.pop_back();
575 hatch_spacing = hatch_dist;
579 hatch_last_pointer = pointer;
580 hatch_last_nearest = nearest;
582 hatch_vectors.push_front(pointer - nearest);
583 if (hatch_vectors.size() > HATCH_VECTOR_ELEMENTS) {
584 hatch_vectors.pop_back();
588 message_context->set(
Inkscape::NORMAL_MESSAGE, hatch_escaped? _(
"Tracking: <b>connection to guide path lost!</b>") : _(
"<b>Tracking</b> a guide path"));
594 if (just_started_drawing) {
595 just_started_drawing =
false;
599 if (!apply(motion_dt)) {
606 g_assert(npoints > 0);
607 fit_and_split(
false);
615 if (event.modifiers & GDK_CONTROL_MASK) {
616 if (hatch_spacing == 0 && hatch_dist != 0) {
619 auto const c = _desktop->w2d(event.pos);
622 hatch_area->set_bpath(std::move(path),
true);
623 hatch_area->set_stroke(0x7f7f7fff);
624 hatch_area->set_visible(
true);
626 }
else if (dragging && !hatch_escaped && hatch_dist != 0) {
629 auto const c = motion_dt;
632 hatch_area->set_bpath(std::move(path),
true);
633 hatch_area->set_stroke(0x00ff00ff);
634 hatch_area->set_visible(
true);
636 }
else if (dragging && hatch_escaped && hatch_dist != 0) {
639 auto const c = motion_dt;
642 hatch_area->set_bpath(std::move(path),
true);
643 hatch_area->set_stroke(0xff0000ff);
644 hatch_area->set_visible(
true);
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) {
653 hatch_area->set_bpath(std::move(path),
true);
654 hatch_area->set_stroke(0x7f7f7fff);
655 hatch_area->set_visible(
true);
659 hatch_area->set_visible(
false);
663 [&] (ButtonReleaseEvent
const &event) {
664 auto const motion_dt = _desktop->w2d(event.pos);
666 ungrabCanvasEvents();
668 set_high_motion_precision(
false);
671 if (dragging && event.button == 1) {
682 set_to_accumulated(event.modifiers & GDK_SHIFT_MASK, event.modifiers & GDK_ALT_MASK);
684 g_warning(
"Failed to create path: invalid data in dc->cal1 or dc->cal2");
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;
704 if (hatch_spacing != 0 && !keep_selected) {
706 if (hatch_spacing_step == 0) {
707 hatch_spacing_step = hatch_spacing;
709 hatch_spacing += hatch_spacing_step;
712 message_context->clear();
723 [&] (KeyPressEvent
const &event) {
728 angle = std::min(angle + 5.0, 90.0);
729 _desktop->setToolboxAdjustmentValue(
"calligraphy-angle", angle);
734 case GDK_KEY_KP_Down:
736 angle = std::max(angle - 5.0, -90.0);
737 _desktop->setToolboxAdjustmentValue(
"calligraphy-angle", angle);
742 case GDK_KEY_KP_Right:
746 _desktop->setToolboxAdjustmentValue(
"calligraphy-width",
width * 100);
751 case GDK_KEY_KP_Left:
755 _desktop->setToolboxAdjustmentValue(
"calligraphy-width",
width * 100);
760 case GDK_KEY_KP_Home:
762 _desktop->setToolboxAdjustmentValue(
"calligraphy-width",
width * 100);
768 _desktop->setToolboxAdjustmentValue(
"calligraphy-width",
width * 100);
774 _desktop->setToolboxFocusTo(
"calligraphy-width");
798 [&] (KeyReleaseEvent
const &event) {
800 case GDK_KEY_Control_L:
801 case GDK_KEY_Control_R:
802 message_context->clear();
804 hatch_spacing_step = 0;
811 [&] (CanvasEvent
const &event) {}
817void CalligraphicTool::clear_current()
820 currentshape->set_bpath(
nullptr);
823 currentcurve.reset();
831void CalligraphicTool::set_to_accumulated(
bool unionize,
bool subtract) {
832 if (!accumulated.is_empty()) {
843 auto layer = currentLayer();
844 auto item = cast<SPItem>(layer->appendChildRepr(this->repr));
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);
860 if (this->keep_selected) {
861 _desktop->getSelection()->set(this->repr);
869 auto result = cast<SPItem>(_desktop->doc()->getObjectByRepr(this->repr));
875 result = _desktop->getSelection()->singleItem();
877 result->doWriteTransform(
result->transform,
nullptr,
true);
883 this->repr =
nullptr;
886 DocumentUndo::done(_desktop->getDocument(), _(
"Draw calligraphic stroke"), INKSCAPE_ICON(
"draw-calligraphic"));
900 curve.curveto(from + v, to + v, to);
904bool CalligraphicTool::accumulate() {
908 (cal1.get_segment_count() <= 0) ||
909 cal1.first_path()->closed()
918 auto rev_cal2 = cal2.reversed();
920 if ((rev_cal2.get_segment_count() <= 0) || rev_cal2.first_path()->closed()) {
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();
934 accumulated.append(cal1);
938 accumulated.append(rev_cal2,
true);
942 accumulated.closepath();
950void CalligraphicTool::fit_and_split(
bool release)
955 g_print(
"[F&S:R=%c]", release?
'T':
'F');
958 if (!( this->npoints > 0 && this->npoints <
SAMPLING_SIZE )) {
964#define BEZIER_MAX_BEZIERS 8
965#define BEZIER_MAX_LENGTH ( BEZIER_SIZE * BEZIER_MAX_BEZIERS )
968 g_print(
"[F&S:#] dc->npoints:%d, release:%s\n",
969 this->npoints, release ?
"TRUE" :
"FALSE");
973 if ( cal1.is_empty() || cal2.is_empty() ) {
979 cal1.moveto(this->point1[0]);
980 cal2.moveto(this->point2[0]);
985 tolerance_sq, BEZIER_MAX_BEZIERS);
986 g_assert( nb1 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b1)) );
990 tolerance_sq, BEZIER_MAX_BEZIERS);
991 g_assert( nb2 * BEZIER_SIZE <= gint(G_N_ELEMENTS(b2)) );
993 if ( nb1 != -1 && nb2 != -1 ) {
996 g_print(
"nb1:%d nb2:%d\n", nb1, nb2);
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]);
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]);
1010 if (this->segments.empty()) {
1011 add_cap(currentcurve, b2[0], b1[0], cap_rounding);
1013 currentcurve.closepath();
1014 currentshape->set_bpath(¤tcurve,
true);
1018 for (
Geom::Point *bp1 = b1; bp1 < b1 + BEZIER_SIZE * nb1; bp1 += BEZIER_SIZE) {
1019 cal1.curveto(bp1[1], bp1[2], bp1[3]);
1021 for (
Geom::Point *bp2 = b2; bp2 < b2 + BEZIER_SIZE * nb2; bp2 += BEZIER_SIZE) {
1022 cal2.curveto(bp2[1], bp2[2], bp2[3]);
1027 g_print(
"[fit_and_split] failed to fit-cubic.\n");
1029 this->draw_temporary_box();
1031 for (gint i = 1; i < this->npoints; i++) {
1032 cal1.lineto(this->point1[i]);
1034 for (gint i = 1; i < this->npoints; i++) {
1035 cal2.lineto(this->point2[i]);
1041 g_print(
"[%d]Yup\n", this->npoints);
1044 g_assert(!currentcurve.is_empty());
1052 cbp->set_fill(fillColor ? fillColor->toRGBA(opacity * fillOpacity) : 0x0,
SP_WIND_RULE_EVENODD);
1053 cbp->set_stroke(0x0);
1058 segments.emplace_back(cbp);
1061 this->point1[0] = this->point1[this->npoints - 1];
1062 this->point2[0] = this->point2[this->npoints - 1];
1065 this->draw_temporary_box();
1069void CalligraphicTool::draw_temporary_box() {
1070 currentcurve.reset();
1072 currentcurve.moveto(this->point2[this->npoints-1]);
1074 for (gint i = this->npoints-2; i >= 0; i--) {
1075 currentcurve.lineto(this->point2[i]);
1078 for (gint i = 0; i < this->npoints; i++) {
1079 currentcurve.lineto(this->point1[i]);
1082 if (this->npoints >= 2) {
1083 add_cap(currentcurve, point1[npoints - 1], point2[npoints - 1], cap_rounding);
1086 currentcurve.closepath();
1087 currentshape->set_bpath(¤tcurve,
true);
TODO: insert short description here.
Bezier fitting algorithms.
3x3 matrix representing an affine transformation.
Affine inverse() const
Compute the inverse matrix.
Set of all points at a fixed distance from the center.
Abstract continuous curve on a plane defined on [0,1].
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.
Sequence of contiguous curves, aka spline.
Two-dimensional point that doubles as a vector.
Coord length() const
Compute the distance from origin.
Inkscape::Drawing * get_drawing()
double stealOpacity()
Get the opacity, and remove it from this color.
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
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.
Unit const * getUnit(Glib::ustring const &name) const
Retrieve a given unit based on its string identifier.
Interface for refcounted XML nodes.
Wrapper around a Geom::PathVector object.
void reset()
Set curve to empty curve.
To do: update description of desktop.
double current_zoom() const
Inkscape::CanvasItemGroup * getCanvasControls() const
Inkscape::CanvasItemGroup * getCanvasSketch() const
Geom::Affine const & d2w() const
Transformation from desktop to window coordinates.
Inkscape::CanvasItemDrawing * getCanvasDrawing() const
Inkscape::XML::Node * updateRepr(unsigned int flags=SP_OBJECT_WRITE_EXT)
Updates the object's repr based on the object's state.
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.
constexpr int SAMPLING_SIZE
constexpr Coord lerp(Coord t, Coord a, Coord b)
Numerically stable linear interpolation.
Macro for icon names used in Inkscape.
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.
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...
SBasis L2(D2< SBasis > const &a, unsigned k)
T dot(D2< T > const &a, D2< T > const &b)
D2< T > rot90(D2< T > const &a)
static R & release(R &r)
Decrements the reference count of a anchored object.
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)
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.
PathVector - a sequence of subpaths.
void sp_repr_unparent(Inkscape::XML::Node *repr)
Remove repr from children of its parent node.
Abstract base class for events.
Movement of the mouse pointer.
Interface for XML documents.
virtual Node * createElement(char const *name)=0
static void sp_svg_write_path(Inkscape::SVG::PathString &str, Geom::Path const &p, bool normalize=false)
void dot(Cairo::RefPtr< Cairo::Context > &cr, double x, double y)