// SPDX-License-Identifier: GPL-2.0-or-later /* * Embroidery stitch live path effect (Implementation) * * Copyright (C) 2016 Michael Soegtrop * * Released under GNU GPL v2+, read the file 'COPYING' for more information. */ #include "ui/widget/scalar.h" #include #include "live_effects/lpe-embrodery-stitch.h" #include "live_effects/lpe-embrodery-stitch-ordering.h" #include <2geom/path.h> #include <2geom/piecewise.h> #include <2geom/sbasis.h> #include <2geom/sbasis-geometric.h> #include <2geom/bezier-to-sbasis.h> #include <2geom/sbasis-to-bezier.h> namespace Inkscape { namespace LivePathEffect { using namespace Geom; using namespace LPEEmbroderyStitchOrdering; static const Util::EnumData OrderMethodData[LPEEmbroderyStitch::order_method_count] = { // clang-format off { LPEEmbroderyStitch::order_method_no_reorder, N_("no reordering"), "no-reorder" }, { LPEEmbroderyStitch::order_method_zigzag, N_("zig-zag"), "zig-zag" }, { LPEEmbroderyStitch::order_method_zigzag_rev_first, N_("zig-zag, reverse first"), "zig-zag-rev-first" }, { LPEEmbroderyStitch::order_method_closest, N_("closest"), "closest" }, { LPEEmbroderyStitch::order_method_closest_rev_first, N_("closest, reverse first"), "closest-rev-first" }, { LPEEmbroderyStitch::order_method_tsp_kopt_2, N_("traveling salesman 2-opt (fast, bad)"), "tsp-2opt" }, { LPEEmbroderyStitch::order_method_tsp_kopt_3, N_("traveling salesman 3-opt (fast, ok)"), "tsp-3opt" }, { LPEEmbroderyStitch::order_method_tsp_kopt_4, N_("traveling salesman 4-opt (seconds)"), "tsp-4opt" }, { LPEEmbroderyStitch::order_method_tsp_kopt_5, N_("traveling salesman 5-opt (minutes)"), "tsp-5opt" } // clang-format on }; static const Util::EnumDataConverter OrderMethodConverter(OrderMethodData, sizeof(OrderMethodData) / sizeof(*OrderMethodData)); static const Util::EnumData ConnectMethodData[LPEEmbroderyStitch::connect_method_count] = { { LPEEmbroderyStitch::connect_method_line, N_("straight line"), "line" }, { LPEEmbroderyStitch::connect_method_move_point_from, N_("move to begin"), "move-begin" }, { LPEEmbroderyStitch::connect_method_move_point_mid, N_("move to middle"), "move-middle" }, { LPEEmbroderyStitch::connect_method_move_point_to, N_("move to end"), "move-end" } }; static const Util::EnumDataConverter ConnectMethodConverter(ConnectMethodData, sizeof(ConnectMethodData) / sizeof(*ConnectMethodData)); LPEEmbroderyStitch::LPEEmbroderyStitch(LivePathEffectObject *lpeobject) : Effect(lpeobject), ordering(_("Ordering method"), _("Method used to order sub paths"), "ordering", OrderMethodConverter, &wr, this, order_method_no_reorder), connection(_("Connection method"), _("Method to connect end points of sub paths"), "connection", ConnectMethodConverter, &wr, this, connect_method_line), stitch_length(_("Stitch length"), _("Divide path into straight segments of given length (in user units)"), "stitch-length", &wr, this, 10.0), stitch_min_length(_("Minimum stitch length [%]"), _("Merge stitches that are shorter than this percentage of the stitch length"), "stitch-min-length", &wr, this, 25.0), stitch_pattern(_("Stitch pattern"), _("Select between different stitch patterns"), "stitch_pattern", &wr, this, 0), show_stitches(_("Show stitches"), _("Creates gaps between stitches (use only for preview, deactivate for use with embroidery machines)"), "show-stitches", &wr, this, false), show_stitch_gap(_("Show stitch gap"), _("Length of the gap between stitches when showing stitches"), "show-stitch-gap", &wr, this, 0.5), jump_if_longer(_("Jump if longer"), _("Jump connection if longer than"), "jump-if-longer", &wr, this, 100) { registerParameter(dynamic_cast(&ordering)); registerParameter(dynamic_cast(&connection)); registerParameter(dynamic_cast(&stitch_length)); registerParameter(dynamic_cast(&stitch_min_length)); registerParameter(dynamic_cast(&stitch_pattern)); registerParameter(dynamic_cast(&show_stitches)); registerParameter(dynamic_cast(&show_stitch_gap)); registerParameter(dynamic_cast(&jump_if_longer)); stitch_length.param_set_digits(1); stitch_length.param_set_range(1, 10000); stitch_min_length.param_set_digits(1); stitch_min_length.param_set_range(0, 100); stitch_pattern.param_make_integer(); stitch_pattern.param_set_range(0, 2); show_stitch_gap.param_set_range(0.001, 10); jump_if_longer.param_set_range(0.0, 1000000); } LPEEmbroderyStitch::~LPEEmbroderyStitch() = default; double LPEEmbroderyStitch::GetPatternInitialStep(int pattern, int line) { switch (pattern) { case 0: return 0.0; case 1: switch (line % 4) { case 0: return 0.0; case 1: return 0.25; case 2: return 0.50; case 3: return 0.75; } return 0.0; case 2: switch (line % 4) { case 0: return 0.0; case 1: return 0.5; case 2: return 0.75; case 3: return 0.25; } return 0.0; default: return 0.0; } } Point LPEEmbroderyStitch::GetStartPointInterpolAfterRev(std::vector const &info, unsigned i) { Point start_this = info[i].GetBegRev(); if (i == 0) { return start_this; } if (!info[i - 1].connect) { return start_this; } Point end_prev = info[i - 1].GetEndRev(); switch (connection.get_value()) { case connect_method_line: return start_this; case connect_method_move_point_from: return end_prev; case connect_method_move_point_mid: return 0.5 * start_this + 0.5 * end_prev; case connect_method_move_point_to: return start_this; default: return start_this; } } Point LPEEmbroderyStitch::GetEndPointInterpolAfterRev(std::vector const &info, unsigned i) { Point end_this = info[i].GetEndRev(); if (i + 1 == info.size()) { return end_this; } if (!info[i].connect) { return end_this; } Point start_next = info[i + 1].GetBegRev(); switch (connection.get_value()) { case connect_method_line: return end_this; case connect_method_move_point_from: return end_this; case connect_method_move_point_mid: return 0.5 * start_next + 0.5 * end_this; case connect_method_move_point_to: return start_next; default: return end_this; } } Point LPEEmbroderyStitch::GetStartPointInterpolBeforeRev(std::vector const &info, unsigned i) { if (info[i].reverse) { return GetEndPointInterpolAfterRev(info, i); } else { return GetStartPointInterpolAfterRev(info, i); } } Point LPEEmbroderyStitch::GetEndPointInterpolBeforeRev(std::vector const &info, unsigned i) { if (info[i].reverse) { return GetStartPointInterpolAfterRev(info, i); } else { return GetEndPointInterpolAfterRev(info, i); } } PathVector LPEEmbroderyStitch::doEffect_path(PathVector const &path_in) { if (path_in.size() >= 2) { PathVector path_out; // Create vectors with start and end points std::vector orderinginfos(path_in.size()); // connect next path to this one bool connect_with_previous = false; for (PathVector::const_iterator it = path_in.begin(); it != path_in.end(); ++it) { OrderingInfo &info = orderinginfos[ it - path_in.begin() ]; info.index = it - path_in.begin(); info.reverse = false; info.used = false; info.connect = true; info.begOrig = it->front().initialPoint(); info.endOrig = it->back().finalPoint(); } // Compute sub-path ordering switch (ordering.get_value()) { case order_method_no_reorder: OrderingOriginal(orderinginfos); break; case order_method_zigzag: OrderingZigZag(orderinginfos, false); break; case order_method_zigzag_rev_first: OrderingZigZag(orderinginfos, true); break; case order_method_closest: OrderingClosest(orderinginfos, false); break; case order_method_closest_rev_first: OrderingClosest(orderinginfos, true); break; case order_method_tsp_kopt_2: OrderingAdvanced(orderinginfos, 2); break; case order_method_tsp_kopt_3: OrderingAdvanced(orderinginfos, 3); break; case order_method_tsp_kopt_4: OrderingAdvanced(orderinginfos, 4); break; case order_method_tsp_kopt_5: OrderingAdvanced(orderinginfos, 5); break; } // Iterate over sub-paths in order found above // Divide paths into stitches (currently always equidistant) // Interpolate between neighboring paths, so that their ends coincide for (std::vector::const_iterator it = orderinginfos.begin(); it != orderinginfos.end(); ++it) { // info index unsigned iInfo = it - orderinginfos.begin(); // subpath index unsigned iPath = it->index; // decide of path shall be reversed bool reverse = it->reverse; // minimum stitch length in absolute measure double stitch_min_length_abs = stitch_min_length * 0.01 * stitch_length; // convert path to piecewise Piecewise > pwOrig = path_in[iPath].toPwSb(); // make piecewise equidistant in time Piecewise > pwEqdist = arc_length_parametrization(pwOrig); Piecewise > pwStitch; // cut into stitches double cutpos = 0.0; Interval pwdomain = pwEqdist.domain(); // step length of first stitch double step = GetPatternInitialStep(stitch_pattern, iInfo) * stitch_length; if (step < stitch_min_length_abs) { step += stitch_length; } bool last = false; bool first = true; double posnext; for (double pos = pwdomain.min(); !last; pos = posnext, cutpos += 1.0) { // start point Point p1; if (first) { p1 = GetStartPointInterpolBeforeRev(orderinginfos, iInfo); first = false; } else { p1 = pwEqdist.valueAt(pos); } // end point of this stitch Point p2; posnext = pos + step; // last stitch is to end if (posnext >= pwdomain.max() - stitch_min_length_abs) { p2 = GetEndPointInterpolBeforeRev(orderinginfos, iInfo); last = true; } else { p2 = pwEqdist.valueAt(posnext); } pwStitch.push_cut(cutpos); pwStitch.push_seg(D2(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y])))); // stitch length for all except first step step = stitch_length; } pwStitch.push_cut(cutpos); if (reverse) { pwStitch = Geom::reverse(pwStitch); } if (it->connect && iInfo != orderinginfos.size() - 1) { // Connect this segment with the previous segment by a straight line Point end = pwStitch.lastValue(); Point start_next = GetStartPointInterpolAfterRev(orderinginfos, iInfo + 1); // connect end and start point if (end != start_next && distance(end, start_next) <= jump_if_longer) { cutpos += 1.0; pwStitch.push_seg(D2(SBasis(Linear(end[X], start_next[X])), SBasis(Linear(end[Y], start_next[Y])))); pwStitch.push_cut(cutpos); } } if (show_stitches) { for (auto & seg : pwStitch.segs) { // Create anew piecewise with just one segment Piecewise > pwOne; pwOne.push_cut(0); pwOne.push_seg(seg); pwOne.push_cut(1); // make piecewise equidistant in time Piecewise > pwOneEqdist = arc_length_parametrization(pwOne); Interval pwdomain = pwOneEqdist.domain(); // Compute the points of the shortened piece Coord len = pwdomain.max() - pwdomain.min(); Coord offs = 0.5 * (show_stitch_gap < 0.5 * len ? show_stitch_gap : 0.5 * len); Point p1 = pwOneEqdist.valueAt(pwdomain.min() + offs); Point p2 = pwOneEqdist.valueAt(pwdomain.max() - offs); Piecewise > pwOneGap; // Create Linear SBasis D2 sbasis = D2(SBasis(Linear(p1[X], p2[X])), SBasis(Linear(p1[Y], p2[Y]))); // Convert to path and add to path list Geom::Path path = path_from_sbasis(sbasis , LPE_CONVERSION_TOLERANCE, false); path_out.push_back(path); } } else { PathVector pathv = path_from_piecewise(pwStitch, LPE_CONVERSION_TOLERANCE); for (const auto & ipv : pathv) { if (connect_with_previous) { path_out.back().append(ipv); } else { path_out.push_back(ipv); } } } connect_with_previous = it->connect; } return path_out; } else { return path_in; } } void LPEEmbroderyStitch::resetDefaults(SPItem const *item) { Effect::resetDefaults(item); } } //namespace LivePathEffect } /* namespace Inkscape */