1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3  * @file
4  * Taper Stroke path effect, provided as an alternative to Power Strokes
5  * for otherwise constant-width paths.
6  *
7  * Authors:
8  *   Liam P White
9  *
10  * Copyright (C) 2014-2020 Authors
11  *
12  * Released under GNU GPL v2+, read the file 'COPYING' for more information.
13  */
14 
15 #include "live_effects/lpe-taperstroke.h"
16 #include "live_effects/fill-conversion.h"
17 
18 #include <2geom/circle.h>
19 #include <2geom/sbasis-to-bezier.h>
20 
21 #include "style.h"
22 
23 #include "display/curve.h"
24 #include "helper/geom-nodetype.h"
25 #include "helper/geom-pathstroke.h"
26 #include "object/sp-shape.h"
27 #include "svg/svg-color.h"
28 #include "svg/css-ostringstream.h"
29 #include "svg/svg.h"
30 #include "ui/knot/knot-holder.h"
31 #include "ui/knot/knot-holder-entity.h"
32 
33 // TODO due to internal breakage in glibmm headers, this must be last:
34 #include <glibmm/i18n.h>
35 
36 template<typename T>
withinRange(T value,T low,T high)37 inline bool withinRange(T value, T low, T high) {
38     return (value > low && value < high);
39 }
40 
41 namespace Inkscape {
42 namespace LivePathEffect {
43 
44 namespace TpS {
45     class KnotHolderEntityAttachBegin : public LPEKnotHolderEntity {
46     public:
KnotHolderEntityAttachBegin(LPETaperStroke * effect)47         KnotHolderEntityAttachBegin(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {}
48         void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override;
49         void knot_click(guint state) override;
50         Geom::Point knot_get() const override;
51     };
52 
53     class KnotHolderEntityAttachEnd : public LPEKnotHolderEntity {
54     public:
KnotHolderEntityAttachEnd(LPETaperStroke * effect)55         KnotHolderEntityAttachEnd(LPETaperStroke * effect) : LPEKnotHolderEntity(effect) {}
56         void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override;
57         void knot_click(guint state) override;
58         Geom::Point knot_get() const override;
59     };
60 } // TpS
61 
62 static const Util::EnumData<unsigned> JoinType[] = {
63     // clang-format off
64     {JOIN_BEVEL,          N_("Beveled"),         "bevel"},
65     {JOIN_ROUND,          N_("Rounded"),         "round"},
66     {JOIN_MITER,          N_("Miter"),           "miter"},
67     {JOIN_EXTRAPOLATE,    N_("Extrapolated"),    "extrapolated"},
68     // clang-format on
69 };
70 
71 enum TaperShape {
72     TAPER_CENTER,
73     TAPER_RIGHT,
74     TAPER_LEFT,
75     LAST_SHAPE
76 };
77 
78 static const Util::EnumData<unsigned> TaperShapeType[] = {
79     {TAPER_CENTER, N_("Center"), "center"},
80     {TAPER_LEFT,   N_("Left"),   "left"},
81     {TAPER_RIGHT,  N_("Right"),  "right"},
82 };
83 
84 static const Util::EnumDataConverter<unsigned> JoinTypeConverter(JoinType, sizeof (JoinType)/sizeof(*JoinType));
85 static const Util::EnumDataConverter<unsigned> TaperShapeTypeConverter(TaperShapeType, sizeof (TaperShapeType)/sizeof(*TaperShapeType));
86 
LPETaperStroke(LivePathEffectObject * lpeobject)87 LPETaperStroke::LPETaperStroke(LivePathEffectObject *lpeobject) :
88     Effect(lpeobject),
89     line_width(_("Stroke width:"), _("The (non-tapered) width of the path"), "stroke_width", &wr, this, 1.),
90     attach_start(_("Start offset:"), _("Taper distance from path start"), "attach_start", &wr, this, 0.2),
91     attach_end(_("End offset:"), _("The ending position of the taper"), "end_offset", &wr, this, 0.2),
92     start_smoothing(_("Start smoothing:"), _("Amount of smoothing to apply to the start taper"), "start_smoothing", &wr, this, 0.5),
93     end_smoothing(_("End smoothing:"), _("Amount of smoothing to apply to the end taper"), "end_smoothing", &wr, this, 0.5),
94     join_type(_("Join type:"), _("Join type for non-smooth nodes"), "jointype", JoinTypeConverter, &wr, this, JOIN_EXTRAPOLATE),
95     start_shape(_("Start direction:"), _("Direction of the taper at the path start"), "start_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER),
96     end_shape(_("End direction:"), _("Direction of the taper at the path end"), "end_shape", TaperShapeTypeConverter, &wr, this, TAPER_CENTER),
97     miter_limit(_("Miter limit:"), _("Limit for miter joins"), "miter_limit", &wr, this, 100.)
98 {
99     show_orig_path = true;
100     _provides_knotholder_entities = true;
101 
102     attach_start.param_set_digits(3);
103     attach_end.param_set_digits(3);
104 
105     registerParameter(&line_width);
106     registerParameter(&attach_start);
107     registerParameter(&attach_end);
108     registerParameter(&start_smoothing);
109     registerParameter(&end_smoothing);
110     registerParameter(&join_type);
111     registerParameter(&start_shape);
112     registerParameter(&end_shape);
113     registerParameter(&miter_limit);
114 }
115 
116 // from LPEPowerStroke -- sets fill if stroke color because we will
117 // be converting to a fill to make the new join.
118 
transform_multiply(Geom::Affine const & postmul,bool)119 void LPETaperStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/)
120 {
121     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
122     bool transform_stroke = prefs ? prefs->getBool("/options/transform/stroke", true) : true;
123     if (transform_stroke) {
124         line_width.param_transform_multiply(postmul, false);
125     }
126 }
127 
doOnApply(SPLPEItem const * lpeitem)128 void LPETaperStroke::doOnApply(SPLPEItem const* lpeitem)
129 {
130     if (!SP_IS_SHAPE(lpeitem)) {
131         printf("WARNING: It only makes sense to apply Taper stroke to paths (not groups).\n");
132     }
133 
134     Inkscape::Preferences *prefs = Inkscape::Preferences::get();
135     SPShape* item = SP_SHAPE(lpeitem);
136 
137     double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed : 1.;
138 
139     lpe_shape_convert_stroke_and_fill(item);
140 
141     Glib::ustring pref_path = (Glib::ustring)"/live_effects/" +
142                                     (Glib::ustring)LPETypeConverter.get_key(effectType()).c_str() +
143                                     (Glib::ustring)"/" +
144                                     (Glib::ustring)"stroke_width";
145 
146     bool valid = prefs->getEntry(pref_path).isValid();
147 
148     if (!valid) {
149         line_width.param_set_value(width);
150     }
151 
152     line_width.write_to_SVG();
153 }
154 
doOnRemove(SPLPEItem const * lpeitem)155 void LPETaperStroke::doOnRemove(SPLPEItem const* lpeitem)
156 {
157     if (!SP_IS_SHAPE(lpeitem)) {
158         return;
159     }
160 
161     SPShape *item = SP_SHAPE(lpeitem);
162 
163     lpe_shape_revert_stroke_and_fill(item, line_width);
164 }
165 
166 using Geom::Piecewise;
167 using Geom::D2;
168 using Geom::SBasis;
169 // leave Geom::Path
170 
return_at_first_cusp(Geom::Path const & path_in,double=0.05)171 static Geom::Path return_at_first_cusp(Geom::Path const & path_in, double /*smooth_tolerance*/ = 0.05)
172 {
173     Geom::Path temp;
174 
175     for (unsigned i = 0; i < path_in.size(); i++) {
176         temp.append(path_in[i]);
177         if (Geom::get_nodetype(path_in[i], path_in[i + 1]) != Geom::NODE_SMOOTH ) {
178             break;
179         }
180     }
181 
182     return temp;
183 }
184 
185 Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > pwd2_in, Geom::Path pattern, double width);
186 
187 // actual effect
188 
doEffect_path(Geom::PathVector const & path_in)189 Geom::PathVector LPETaperStroke::doEffect_path(Geom::PathVector const& path_in)
190 {
191     Geom::Path first_cusp = return_at_first_cusp(path_in[0]);
192     Geom::Path last_cusp = return_at_first_cusp(path_in[0].reversed());
193 
194     bool zeroStart = false; // [distance from start taper knot -> start of path] == 0
195     bool zeroEnd = false; // [distance from end taper knot -> end of path] == 0
196     bool metInMiddle = false; // knots are touching
197 
198     // there is a pretty good chance that people will try to drag the knots
199     // on top of each other, so block it
200 
201     unsigned size = path_in[0].size();
202     if (size == first_cusp.size()) {
203         // check to see if the knots were dragged over each other
204         // if so, reset the end offset, but still allow the start offset.
205         if ( attach_start >= (size - attach_end) ) {
206             attach_end.param_set_value( size - attach_start );
207             metInMiddle = true;
208         }
209     }
210 
211     if (attach_start == size - attach_end) {
212         metInMiddle = true;
213     }
214     if (attach_end == size - attach_start) {
215         metInMiddle = true;
216     }
217 
218     // don't let it be integer (TODO this is stupid!)
219     {
220         if (double(unsigned(attach_start)) == attach_start) {
221             attach_start.param_set_value(attach_start - 0.00001);
222         }
223         if (double(unsigned(attach_end)) == attach_end) {
224             attach_end.param_set_value(attach_end -     0.00001);
225         }
226     }
227 
228     unsigned allowed_start = first_cusp.size();
229     unsigned allowed_end = last_cusp.size();
230 
231     // don't let the knots be farther than they are allowed to be
232     {
233         if ((unsigned)attach_start >= allowed_start) {
234             attach_start.param_set_value((double)allowed_start - 0.00001);
235         }
236         if ((unsigned)attach_end >= allowed_end) {
237             attach_end.param_set_value((double)allowed_end - 0.00001);
238         }
239     }
240 
241     // don't let it be zero (this is stupid too!)
242     if (attach_start < 0.0000001 || withinRange(double(attach_start), 0.00000001, 0.000001)) {
243         attach_start.param_set_value( 0.0000001 );
244         zeroStart = true;
245     }
246     if (attach_end < 0.0000001 || withinRange(double(attach_end), 0.00000001, 0.000001)) {
247         attach_end.param_set_value( 0.0000001 );
248         zeroEnd = true;
249     }
250 
251     // Path::operator () means get point at time t
252     start_attach_point = first_cusp(attach_start);
253     end_attach_point = last_cusp(attach_end);
254     Geom::PathVector pathv_out;
255 
256     // the following function just splits it up into three pieces.
257     pathv_out = doEffect_simplePath(path_in);
258 
259     // now for the actual tapering. the stretch_along method (stolen from PaP) is used to accomplish this
260 
261     Geom::PathVector real_pathv;
262     Geom::Path real_path;
263     Geom::PathVector pat_vec;
264     Piecewise<D2<SBasis> > pwd2;
265     Geom::Path throwaway_path;
266 
267     if (!zeroStart) {
268         // Construct the pattern
269         std::stringstream pat_str;
270         pat_str.imbue(std::locale::classic());
271 
272         switch (start_shape.get_value()) {
273             case TAPER_RIGHT:
274                 pat_str << "M 1,0 Q " << 1 - (double)start_smoothing << ",0 0,1 L 1,1";
275                 break;
276             case TAPER_LEFT:
277                 pat_str << "M 1,0 L 0,0 Q " << 1 - (double)start_smoothing << ",1 1,1";
278                 break;
279             default:
280                 pat_str << "M 1,0 C " << 1 - (double)start_smoothing << ",0 0,0.5 0,0.5 0,0.5 " << 1 - (double)start_smoothing << ",1 1,1";
281                 break;
282         }
283 
284         pat_vec = sp_svg_read_pathv(pat_str.str().c_str());
285         pwd2.concat(stretch_along(pathv_out[0].toPwSb(), pat_vec[0], fabs(line_width)));
286         throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0];
287 
288         real_path.append(throwaway_path);
289     }
290 
291     // if this condition happens to evaluate false, i.e. there was no space for a path to be drawn, it is simply skipped.
292     // although this seems obvious, it can probably lead to bugs.
293     if (!metInMiddle) {
294         // append the outside outline of the path (goes with the direction of the path)
295         throwaway_path = half_outline(pathv_out[1], fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value()));
296         if (!zeroStart && real_path.size() >= 1 && throwaway_path.size() >= 1) {
297             if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint())) {
298                 real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint());
299             } else {
300                 real_path.setFinal(throwaway_path.initialPoint());
301             }
302         }
303         real_path.append(throwaway_path);
304     }
305 
306     if (!zeroEnd) {
307         // append the ending taper
308         std::stringstream pat_str_1;
309         pat_str_1.imbue(std::locale::classic());
310 
311         switch (end_shape.get_value()) {
312             case TAPER_RIGHT:
313                 pat_str_1 << "M 0,1 L 1,1 Q " << (double)end_smoothing << ",0 0,0";
314                 break;
315             case TAPER_LEFT:
316                 pat_str_1 << "M 0,1 Q " << (double)end_smoothing << ",1 1,0 L 0,0";
317                 break;
318             default:
319                 pat_str_1 << "M 0,1 C " << (double)end_smoothing << ",1 1,0.5 1,0.5 1,0.5 " << (double)end_smoothing << ",0 0,0";
320                 break;
321         }
322 
323         pat_vec = sp_svg_read_pathv(pat_str_1.str().c_str());
324 
325         pwd2 = Piecewise<D2<SBasis> >();
326         pwd2.concat(stretch_along(pathv_out[2].toPwSb(), pat_vec[0], fabs(line_width)));
327 
328         throwaway_path = Geom::path_from_piecewise(pwd2, LPE_CONVERSION_TOLERANCE)[0];
329         if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) {
330             real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint());
331         } else {
332             real_path.setFinal(throwaway_path.initialPoint());
333         }
334         real_path.append(throwaway_path);
335     }
336 
337     if (!metInMiddle) {
338         // append the inside outline of the path (against direction)
339         throwaway_path = half_outline(pathv_out[1].reversed(), fabs(line_width)/2., miter_limit, static_cast<LineJoinType>(join_type.get_value()));
340 
341         if (!Geom::are_near(real_path.finalPoint(), throwaway_path.initialPoint()) && real_path.size() >= 1) {
342             real_path.appendNew<Geom::LineSegment>(throwaway_path.initialPoint());
343         } else {
344             real_path.setFinal(throwaway_path.initialPoint());
345         }
346         real_path.append(throwaway_path);
347     }
348 
349     if (!Geom::are_near(real_path.finalPoint(), real_path.initialPoint())) {
350         real_path.appendNew<Geom::LineSegment>(real_path.initialPoint());
351     } else {
352         real_path.setFinal(real_path.initialPoint());
353     }
354     real_path.close();
355 
356     real_pathv.push_back(real_path);
357 
358     return real_pathv;
359 }
360 
361 /**
362  * @return Always returns a PathVector with three elements.
363  *
364  *  The positions of the effect knots are accessed to determine
365  *  where exactly the input path should be split.
366  */
doEffect_simplePath(Geom::PathVector const & path_in)367 Geom::PathVector LPETaperStroke::doEffect_simplePath(Geom::PathVector const & path_in)
368 {
369     Geom::Coord endTime = path_in[0].size() - attach_end;
370 
371     Geom::Path p1 = path_in[0].portion(0., attach_start);
372     Geom::Path p2 = path_in[0].portion(attach_start, endTime);
373     Geom::Path p3 = path_in[0].portion(endTime, path_in[0].size());
374 
375     Geom::PathVector out;
376     out.push_back(p1);
377     out.push_back(p2);
378     out.push_back(p3);
379 
380     return out;
381 }
382 
383 
384 /**
385  * Most of the below function is verbatim from Pattern Along Path. However, it needed a little
386  * tweaking to get it to work right in this case. Also, large portions of the effect have been
387  * stripped out as I deemed them unnecessary for the relative simplicity of this effect.
388  */
stretch_along(Piecewise<D2<SBasis>> pwd2_in,Geom::Path pattern,double prop_scale)389 Piecewise<D2<SBasis> > stretch_along(Piecewise<D2<SBasis> > pwd2_in, Geom::Path pattern, double prop_scale)
390 {
391     using namespace Geom;
392 
393     // Don't allow empty path parameter:
394     if ( pattern.empty() ) {
395         return pwd2_in;
396     }
397 
398     /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */
399     Piecewise<D2<SBasis> > output;
400     std::vector<Piecewise<D2<SBasis> > > pre_output;
401 
402     D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pattern.toPwSb());
403     Piecewise<SBasis> x0 = Piecewise<SBasis>(patternd2[0]);
404     Piecewise<SBasis> y0 = Piecewise<SBasis>(patternd2[1]);
405     OptInterval pattBndsX = bounds_exact(x0);
406     OptInterval pattBndsY = bounds_exact(y0);
407     if (pattBndsX && pattBndsY) {
408         x0 -= pattBndsX->min();
409         y0 -= pattBndsY->middle();
410 
411         double noffset = 0;
412         double toffset = 0;
413         // Prevent more than 90% overlap...
414 
415         y0+=noffset;
416 
417         std::vector<Piecewise<D2<SBasis> > > paths_in;
418         paths_in = split_at_discontinuities(pwd2_in);
419 
420         for (auto path_i : paths_in) {
421             Piecewise<SBasis> x = x0;
422             Piecewise<SBasis> y = y0;
423             Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2,.1);
424             uskeleton = remove_short_cuts(uskeleton,.01);
425             Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton));
426             n = force_continuity(remove_short_cuts(n,.1));
427 
428             int nbCopies = 0;
429             double scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent();
430             nbCopies = 1;
431 
432             double pattWidth = pattBndsX->extent() * scaling;
433 
434             if (scaling != 1.0) {
435                 x*=scaling;
436             }
437             if ( false ) {
438                 y*=(scaling*prop_scale);
439             } else {
440                 if (prop_scale != 1.0) y *= prop_scale;
441             }
442             x += toffset;
443 
444             double offs = 0;
445             for (int i=0; i<nbCopies; i++) {
446                 if (false) {
447                     Piecewise<D2<SBasis> > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs);
448                     std::vector<Piecewise<D2<SBasis> > > splited_output_piece = split_at_discontinuities(output_piece);
449                     pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() );
450                 } else {
451                     output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs));
452                 }
453                 offs+=pattWidth;
454             }
455         }
456         return output;
457     } else {
458         return pwd2_in;
459     }
460 }
461 
addKnotHolderEntities(KnotHolder * knotholder,SPItem * item)462 void LPETaperStroke::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item)
463 {
464     KnotHolderEntity *e = new TpS::KnotHolderEntityAttachBegin(this);
465     e->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeBegin",
466               _("<b>Start point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction"));
467     knotholder->add(e);
468 
469     KnotHolderEntity *f = new TpS::KnotHolderEntityAttachEnd(this);
470     f->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:TaperStrokeEnd",
471               _("<b>End point of the taper</b>: drag to alter the taper, <b>Shift+click</b> changes the taper direction"));
472     knotholder->add(f);
473 }
474 
475 namespace TpS {
476 
knot_set(Geom::Point const & p,Geom::Point const &,guint state)477 void KnotHolderEntityAttachBegin::knot_set(Geom::Point const &p, Geom::Point const&/*origin*/, guint state)
478 {
479     using namespace Geom;
480 
481     LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect);
482 
483     Geom::Point const s = snap_knot_position(p, state);
484 
485     if (!SP_IS_SHAPE(lpe->sp_lpe_item)) {
486         printf("WARNING: LPEItem is not a path!\n");
487         return;
488     }
489 
490     if (!SP_SHAPE(lpe->sp_lpe_item)->curve()) {
491         // oops
492         return;
493     }
494     // in case you are wondering, the above are simply sanity checks. we never want to actually
495     // use that object.
496 
497     Geom::PathVector pathv = lpe->pathvector_before_effect;
498     Piecewise<D2<SBasis> > pwd2;
499     Geom::Path p_in = return_at_first_cusp(pathv[0]);
500     pwd2.concat(p_in.toPwSb());
501 
502     double t0 = nearest_time(s, pwd2);
503     lpe->attach_start.param_set_value(t0);
504 
505     // FIXME: this should not directly ask for updating the item. It should write to SVG, which triggers updating.
506     sp_lpe_item_update_patheffect(SP_LPE_ITEM(item), false, true);
507 }
508 
knot_click(guint state)509 void KnotHolderEntityAttachBegin::knot_click(guint state)
510 {
511     if (!(state & GDK_SHIFT_MASK)) {
512         return;
513     }
514 
515     LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect);
516 
517     lpe->start_shape.param_set_value((lpe->start_shape.get_value() + 1) % LAST_SHAPE);
518     lpe->start_shape.write_to_SVG();
519 }
520 
knot_click(guint state)521 void KnotHolderEntityAttachEnd::knot_click(guint state)
522 {
523     if (!(state & GDK_SHIFT_MASK)) {
524         return;
525     }
526 
527     LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect);
528 
529     lpe->end_shape.param_set_value((lpe->end_shape.get_value() + 1) % LAST_SHAPE);
530     lpe->end_shape.write_to_SVG();
531 }
532 
knot_set(Geom::Point const & p,Geom::Point const &,guint state)533 void KnotHolderEntityAttachEnd::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state)
534 {
535     using namespace Geom;
536 
537     LPETaperStroke* lpe = dynamic_cast<LPETaperStroke *>(_effect);
538 
539     Geom::Point const s = snap_knot_position(p, state);
540 
541     if (!SP_IS_SHAPE(lpe->sp_lpe_item) ) {
542         printf("WARNING: LPEItem is not a path!\n");
543         return;
544     }
545 
546     if (!SP_SHAPE(lpe->sp_lpe_item)->curve()) {
547         // oops
548         return;
549     }
550     Geom::PathVector pathv = lpe->pathvector_before_effect;
551     Geom::Path p_in = return_at_first_cusp(pathv[0].reversed());
552     Piecewise<D2<SBasis> > pwd2 = p_in.toPwSb();
553 
554     double t0 = nearest_time(s, pwd2);
555     lpe->attach_end.param_set_value(t0);
556 
557     sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true);
558 }
559 
knot_get() const560 Geom::Point KnotHolderEntityAttachBegin::knot_get() const
561 {
562     LPETaperStroke const * lpe = dynamic_cast<LPETaperStroke const*> (_effect);
563     return lpe->start_attach_point;
564 }
565 
knot_get() const566 Geom::Point KnotHolderEntityAttachEnd::knot_get() const
567 {
568     LPETaperStroke const * lpe = dynamic_cast<LPETaperStroke const*> (_effect);
569     return lpe->end_attach_point;
570 }
571 
572 } // namespace TpS
573 } // namespace LivePathEffect
574 } // namespace Inkscape
575 
576 /*
577   Local Variables:
578   mode:c++
579   c-file-style:"stroustrup"
580   c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
581   indent-tabs-mode:nil
582   fill-column:99
583   End:
584 */
585 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8 :
586