1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /**
3 * @file
4 * PowerStroke LPE implementation. Creates curves with modifiable stroke width.
5 */
6 /* Authors:
7 * Johan Engelen <j.b.c.engelen@alumnus.utwente.nl>
8 *
9 * Copyright (C) 2010-2012 Authors
10 *
11 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
12 */
13
14 #include <2geom/elliptical-arc.h>
15 #include <2geom/path-sink.h>
16 #include <2geom/path-intersection.h>
17 #include <2geom/circle.h>
18
19 #include "live_effects/lpe-powerstroke.h"
20 #include "live_effects/lpe-powerstroke-interpolators.h"
21 #include "live_effects/lpe-simplify.h"
22 #include "live_effects/lpeobject.h"
23 #include "live_effects/fill-conversion.h"
24
25 #include "desktop-style.h"
26 #include "style.h"
27
28 #include "display/curve.h"
29 #include "display/control/canvas-item-enums.h"
30 #include "helper/geom.h"
31 #include "object/sp-shape.h"
32 #include "svg/css-ostringstream.h"
33 #include "svg/svg-color.h"
34
35 // TODO due to internal breakage in glibmm headers, this must be last:
36 #include <glibmm/i18n.h>
37
38 namespace Geom {
39 // should all be moved to 2geom at some point
40
41 /** Find the point where two straight lines cross.
42 */
intersection_point(Point const & origin_a,Point const & vector_a,Point const & origin_b,Point const & vector_b)43 static std::optional<Point> intersection_point( Point const & origin_a, Point const & vector_a,
44 Point const & origin_b, Point const & vector_b)
45 {
46 Coord denom = cross(vector_a, vector_b);
47 if (!are_near(denom,0.)){
48 Coord t = (cross(vector_b, origin_a) + cross(origin_b, vector_b)) / denom;
49 return origin_a + t * vector_a;
50 }
51 return std::nullopt;
52 }
53
sbasis_to_cubicbezier(Geom::D2<Geom::SBasis> const & sbasis_in)54 static Geom::CubicBezier sbasis_to_cubicbezier(Geom::D2<Geom::SBasis> const & sbasis_in)
55 {
56 std::vector<Geom::Point> temp;
57 sbasis_to_bezier(temp, sbasis_in, 4);
58 return Geom::CubicBezier( temp );
59 }
60
61 /**
62 * document this!
63 * very quick: this finds the ellipse with minimum eccentricity
64 passing through point P and Q, with tangent PO at P and QO at Q
65 http://mathforum.org/kb/message.jspa?messageID=7471596&tstart=0
66 */
find_ellipse(Point P,Point Q,Point O)67 static Ellipse find_ellipse(Point P, Point Q, Point O)
68 {
69 Point p = P - O;
70 Point q = Q - O;
71 Coord K = 4 * dot(p,q) / (L2sq(p) + L2sq(q));
72
73 double cross = p[Y]*q[X] - p[X]*q[Y];
74 double a = -q[Y]/cross;
75 double b = q[X]/cross;
76 double c = (O[X]*q[Y] - O[Y]*q[X])/cross;
77
78 double d = p[Y]/cross;
79 double e = -p[X]/cross;
80 double f = (-O[X]*p[Y] + O[Y]*p[X])/cross;
81
82 // Ax^2 + Bxy + Cy^2 + Dx + Ey + F = 0
83 double A = (a*d*K+d*d+a*a);
84 double B = (a*e*K+b*d*K+2*d*e+2*a*b);
85 double C = (b*e*K+e*e+b*b);
86 double D = (a*f*K+c*d*K+2*d*f-2*d+2*a*c-2*a);
87 double E = (b*f*K+c*e*K+2*e*f-2*e+2*b*c-2*b);
88 double F = c*f*K+f*f-2*f+c*c-2*c+1;
89
90 return Ellipse(A, B, C, D, E, F);
91 }
92
93 /**
94 * Find circle that touches inside of the curve, with radius matching the curvature, at time value \c t.
95 * Because this method internally uses unitTangentAt, t should be smaller than 1.0 (see unitTangentAt).
96 */
touching_circle(D2<SBasis> const & curve,double t,double tol=0.01)97 static Circle touching_circle( D2<SBasis> const &curve, double t, double tol=0.01 )
98 {
99 //Piecewise<SBasis> k = curvature(curve, tol);
100 D2<SBasis> dM=derivative(curve);
101 if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) {
102 dM=derivative(dM);
103 }
104 if ( are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) ) { // try second time
105 dM=derivative(dM);
106 }
107 if ( dM.isZero(tol) || (are_near(L2sq(dM(t)),0.) && (dM[0].size() > 1) && (dM[1].size() > 1) )) { // admit defeat
108 return Geom::Circle(Geom::Point(0., 0.), 0.);
109 }
110 Piecewise<D2<SBasis> > unitv = unitVector(dM,tol);
111 if (unitv.empty()) { // admit defeat
112 return Geom::Circle(Geom::Point(0., 0.), 0.);
113 }
114 Piecewise<SBasis> dMlength = dot(Piecewise<D2<SBasis> >(dM),unitv);
115 Piecewise<SBasis> k = cross(derivative(unitv),unitv);
116 k = divide(k,dMlength,tol,3);
117 double curv = k(t); // note that this value is signed
118
119 Geom::Point normal = unitTangentAt(curve, t).cw();
120 double radius = 1/curv;
121 Geom::Point center = curve(t) + radius*normal;
122 return Geom::Circle(center, fabs(radius));
123 }
124
125 } // namespace Geom
126
127 namespace Inkscape {
128 namespace LivePathEffect {
129
130 static const Util::EnumData<unsigned> InterpolatorTypeData[] = {
131 {Geom::Interpolate::INTERP_CUBICBEZIER_SMOOTH, N_("CubicBezierSmooth"), "CubicBezierSmooth"},
132 {Geom::Interpolate::INTERP_LINEAR , N_("Linear"), "Linear"},
133 {Geom::Interpolate::INTERP_CUBICBEZIER , N_("CubicBezierFit"), "CubicBezierFit"},
134 {Geom::Interpolate::INTERP_CUBICBEZIER_JOHAN , N_("CubicBezierJohan"), "CubicBezierJohan"},
135 {Geom::Interpolate::INTERP_SPIRO , N_("SpiroInterpolator"), "SpiroInterpolator"},
136 {Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM, N_("Centripetal Catmull-Rom"), "CentripetalCatmullRom"}
137 };
138 static const Util::EnumDataConverter<unsigned> InterpolatorTypeConverter(InterpolatorTypeData, sizeof(InterpolatorTypeData)/sizeof(*InterpolatorTypeData));
139
140 enum LineJoinType {
141 LINEJOIN_BEVEL,
142 LINEJOIN_ROUND,
143 LINEJOIN_EXTRP_MITER,
144 LINEJOIN_MITER,
145 LINEJOIN_SPIRO,
146 LINEJOIN_EXTRP_MITER_ARC
147 };
148 static const Util::EnumData<unsigned> LineJoinTypeData[] = {
149 {LINEJOIN_BEVEL, N_("Beveled"), "bevel"},
150 {LINEJOIN_ROUND, N_("Rounded"), "round"},
151 // {LINEJOIN_EXTRP_MITER, N_("Extrapolated"), "extrapolated"}, // disabled because doesn't work well
152 {LINEJOIN_EXTRP_MITER_ARC, N_("Extrapolated arc"), "extrp_arc"},
153 {LINEJOIN_MITER, N_("Miter"), "miter"},
154 {LINEJOIN_SPIRO, N_("Spiro"), "spiro"},
155 };
156 static const Util::EnumDataConverter<unsigned> LineJoinTypeConverter(LineJoinTypeData, sizeof(LineJoinTypeData)/sizeof(*LineJoinTypeData));
157
LPEPowerStroke(LivePathEffectObject * lpeobject)158 LPEPowerStroke::LPEPowerStroke(LivePathEffectObject *lpeobject) :
159 Effect(lpeobject),
160 offset_points(_("Offset points"), _("Offset points"), "offset_points", &wr, this),
161 not_jump(_("No jumping handles"), _("Allow to move handles along the path without them automatically attaching to the nearest path segment"), "not_jump", &wr, this, false),
162 sort_points(_("Sort points"), _("Sort offset points according to their time value along the curve"), "sort_points", &wr, this, true),
163 interpolator_type(_("Interpolator type:"), _("Determines which kind of interpolator will be used to interpolate between stroke width along the path"), "interpolator_type", InterpolatorTypeConverter, &wr, this, Geom::Interpolate::INTERP_CENTRIPETAL_CATMULLROM),
164 interpolator_beta(_("Smoothness:"), _("Sets the smoothness for the CubicBezierJohan interpolator; 0 = linear interpolation, 1 = smooth"), "interpolator_beta", &wr, this, 0.2),
165 scale_width(_("Width factor:"), _("Scale the stroke's width uniformly along the whole path"), "scale_width", &wr, this, 1.0),
166 start_linecap_type(_("Start cap:"), _("Determines the shape of the path's start"), "start_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH),
167 linejoin_type(_("Join:"), _("Determines the shape of the path's corners"), "linejoin_type", LineJoinTypeConverter, &wr, this, LINEJOIN_ROUND),
168 miter_limit(_("Miter limit:"), _("Maximum length of the miter (in units of stroke width)"), "miter_limit", &wr, this, 4.),
169 end_linecap_type(_("End cap:"), _("Determines the shape of the path's end"), "end_linecap_type", LineCapTypeConverter, &wr, this, LINECAP_ZERO_WIDTH)
170 {
171 show_orig_path = true;
172
173 /// @todo offset_points are initialized with empty path, is that bug-save?
174
175 interpolator_beta.addSlider(true);
176 interpolator_beta.param_set_range(0.,1.);
177
178 registerParameter(&offset_points);
179 registerParameter(¬_jump);
180 registerParameter(&sort_points);
181 registerParameter(&interpolator_type);
182 registerParameter(&interpolator_beta);
183 registerParameter(&start_linecap_type);
184 registerParameter(&linejoin_type);
185 registerParameter(&miter_limit);
186 registerParameter(&scale_width);
187 registerParameter(&end_linecap_type);
188 scale_width.param_set_range(0.0, std::numeric_limits<double>::max());
189 scale_width.param_set_increments(0.1, 0.1);
190 scale_width.param_set_digits(4);
191 recusion_limit = 0;
192 has_recursion = false;
193 }
194
195 LPEPowerStroke::~LPEPowerStroke() = default;
196
197 void
doBeforeEffect(SPLPEItem const * lpeItem)198 LPEPowerStroke::doBeforeEffect(SPLPEItem const *lpeItem)
199 {
200 offset_points.set_scale_width(scale_width);
201 if (has_recursion) {
202 has_recursion = false;
203 adjustForNewPath(pathvector_before_effect);
204 }
205 }
206
applyStyle(SPLPEItem * lpeitem)207 void LPEPowerStroke::applyStyle(SPLPEItem *lpeitem)
208 {
209 lpe_shape_convert_stroke_and_fill(SP_SHAPE(lpeitem));
210 }
211
212 void
doOnApply(SPLPEItem const * lpeitem)213 LPEPowerStroke::doOnApply(SPLPEItem const* lpeitem)
214 {
215 if (auto shape = dynamic_cast<SPShape const *>(lpeitem)) {
216 SPLPEItem* item = const_cast<SPLPEItem*>(lpeitem);
217 std::vector<Geom::Point> points;
218 Geom::PathVector const &pathv = pathv_to_linear_and_cubic_beziers(shape->curve()->get_pathvector());
219 double width = (lpeitem && lpeitem->style) ? lpeitem->style->stroke_width.computed / 2 : 1.;
220 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
221 Glib::ustring pref_path_pp = "/live_effects/powerstroke/powerpencil";
222 bool powerpencil = prefs->getBool(pref_path_pp, false);
223 bool clipboard = offset_points.data().size() > 0;
224 if (!powerpencil) {
225 applyStyle(item);
226 }
227 if (!clipboard && !powerpencil) {
228 item->updateRepr();
229 if (pathv.empty()) {
230 points.emplace_back(0.2,width );
231 points.emplace_back(0.5, width);
232 points.emplace_back(0.8, width);
233 } else {
234 Geom::Path const &path = pathv.front();
235 Geom::Path::size_type const size = path.size_default();
236 if (!path.closed()) {
237 points.emplace_back(0.2, width);
238 }
239 points.emplace_back(0.5 * size, width);
240 if (!path.closed()) {
241 points.emplace_back(size - 0.2, width);
242 }
243 }
244 offset_points.param_set_and_write_new_value(points);
245 }
246 offset_points.set_scale_width(scale_width);
247 } else {
248 if (!SP_IS_SHAPE(lpeitem)) {
249 g_warning("LPE Powerstroke can only be applied to shapes (not groups).");
250 }
251 }
252 }
253
doOnRemove(SPLPEItem const * lpeitem)254 void LPEPowerStroke::doOnRemove(SPLPEItem const* lpeitem)
255 {
256 if (SP_IS_SHAPE(lpeitem) && !keep_paths) {
257 lpe_shape_revert_stroke_and_fill(SP_SHAPE(lpeitem), offset_points.median_width()*2);
258 }
259 }
260
261 void
adjustForNewPath(Geom::PathVector const & path_in)262 LPEPowerStroke::adjustForNewPath(Geom::PathVector const & path_in)
263 {
264 if (!path_in.empty()) {
265 offset_points.recalculate_controlpoints_for_new_pwd2(path_in[0].toPwSb());
266 }
267 }
268
compare_offsets(Geom::Point first,Geom::Point second)269 static bool compare_offsets (Geom::Point first, Geom::Point second)
270 {
271 return first[Geom::X] < second[Geom::X];
272 }
273
path_from_piecewise_fix_cusps(Geom::Piecewise<Geom::D2<Geom::SBasis>> const & B,Geom::Piecewise<Geom::SBasis> const & y,LineJoinType jointype,double miter_limit,double tol=Geom::EPSILON)274 static Geom::Path path_from_piecewise_fix_cusps( Geom::Piecewise<Geom::D2<Geom::SBasis> > const & B,
275 Geom::Piecewise<Geom::SBasis> const & y, // width path
276 LineJoinType jointype,
277 double miter_limit,
278 double tol=Geom::EPSILON)
279 {
280 /* per definition, each discontinuity should be fixed with a join-ending, as defined by linejoin_type
281 */
282 Geom::PathBuilder pb;
283 Geom::OptRect bbox = bounds_fast(B);
284 if (B.empty() || !bbox) {
285 return pb.peek().front();
286 }
287
288 pb.setStitching(true);
289
290 Geom::Point start = B[0].at0();
291 pb.moveTo(start);
292 build_from_sbasis(pb, B[0], tol, false);
293 unsigned prev_i = 0;
294 for (unsigned i=1; i < B.size(); i++) {
295 // Skip degenerate segments. The number below was determined, after examining
296 // very many paths with powerstrokes of all shapes and sizes, to allow filtering out most
297 // degenerate segments without losing significant quality; it is close to 1/256.
298 if (B[i].isConstant(4e-3)) {
299 continue;
300 }
301 if (!are_near(B[prev_i].at1(), B[i].at0(), tol) )
302 { // discontinuity found, so fix it :-)
303 double width = y( B.cuts[i] );
304
305 Geom::Point tang1 = -unitTangentAt(reverse(B[prev_i]),0.); // = unitTangentAt(B[prev_i],1);
306 Geom::Point tang2 = unitTangentAt(B[i],0);
307 Geom::Point discontinuity_vec = B[i].at0() - B[prev_i].at1();
308 bool on_outside = ( dot(tang1, discontinuity_vec) >= 0. );
309
310 if (on_outside) {
311 // we are on the outside: add some type of join!
312 switch (jointype) {
313 case LINEJOIN_ROUND: {
314 /* for constant width paths, the rounding is a circular arc (rx == ry),
315 for non-constant width paths, the rounding can be done with an ellipse but is hard and ambiguous.
316 The elliptical arc should go through the discontinuity's start and end points (of course!)
317 and also should match the discontinuity tangents at those start and end points.
318 To resolve the ambiguity, the elliptical arc with minimal eccentricity should be chosen.
319 A 2Geom method was created to do exactly this :)
320 */
321
322 std::optional<Geom::Point> O = intersection_point( B[prev_i].at1(), tang1,
323 B[i].at0(), tang2 );
324 if (!O) {
325 // no center found, i.e. 180 degrees round
326 pb.lineTo(B[i].at0()); // default to bevel for too shallow cusp angles
327 break;
328 }
329
330 Geom::Ellipse ellipse;
331 try {
332 ellipse = find_ellipse(B[prev_i].at1(), B[i].at0(), *O);
333 }
334 catch (Geom::LogicalError &e) {
335 // 2geom did not find a fitting ellipse, this happens for weird thick paths :)
336 // do bevel, and break
337 pb.lineTo(B[i].at0());
338 break;
339 }
340
341 // check if ellipse.ray is within 'sane' range.
342 if ( ( fabs(ellipse.ray(Geom::X)) > 1e6 ) ||
343 ( fabs(ellipse.ray(Geom::Y)) > 1e6 ) )
344 {
345 // do bevel, and break
346 pb.lineTo(B[i].at0());
347 break;
348 }
349
350 pb.arcTo( ellipse.ray(Geom::X), ellipse.ray(Geom::Y), ellipse.rotationAngle(),
351 false, width < 0, B[i].at0() );
352
353 break;
354 }
355 case LINEJOIN_EXTRP_MITER: {
356 Geom::D2<Geom::SBasis> newcurve1 = B[prev_i] * Geom::reflection(rot90(tang1), B[prev_i].at1());
357 Geom::CubicBezier bzr1 = sbasis_to_cubicbezier( reverse(newcurve1) );
358
359 Geom::D2<Geom::SBasis> newcurve2 = B[i] * Geom::reflection(rot90(tang2), B[i].at0());
360 Geom::CubicBezier bzr2 = sbasis_to_cubicbezier( reverse(newcurve2) );
361
362 Geom::Crossings cross = crossings(bzr1, bzr2);
363 if (cross.empty()) {
364 // empty crossing: default to bevel
365 pb.lineTo(B[i].at0());
366 } else {
367 // check size of miter
368 Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width;
369 Geom::Coord len = distance(bzr1.pointAt(cross[0].ta), point_on_path);
370 if (len > fabs(width) * miter_limit) {
371 // miter too big: default to bevel
372 pb.lineTo(B[i].at0());
373 } else {
374 std::pair<Geom::CubicBezier, Geom::CubicBezier> sub1 = bzr1.subdivide(cross[0].ta);
375 std::pair<Geom::CubicBezier, Geom::CubicBezier> sub2 = bzr2.subdivide(cross[0].tb);
376 pb.curveTo(sub1.first[1], sub1.first[2], sub1.first[3]);
377 pb.curveTo(sub2.second[1], sub2.second[2], sub2.second[3]);
378 }
379 }
380 break;
381 }
382 case LINEJOIN_EXTRP_MITER_ARC: {
383 // Extrapolate using the curvature at the end of the path segments to join
384 Geom::Circle circle1 = Geom::touching_circle(reverse(B[prev_i]), 0.0);
385 Geom::Circle circle2 = Geom::touching_circle(B[i], 0.0);
386 std::vector<Geom::ShapeIntersection> solutions;
387 solutions = circle1.intersect(circle2);
388 if (solutions.size() == 2) {
389 Geom::Point sol(0.,0.);
390 bool solok = true;
391 bool point0bad = false;
392 bool point1bad = false;
393 if ( dot(tang2, solutions[0].point() - B[i].at0()) > 0)
394 {
395 // points[0] is bad, choose points[1]
396 point0bad = true;
397 }
398 if ( dot(tang2, solutions[1].point() - B[i].at0()) > 0)
399 {
400 // points[1] is bad, choose points[0]
401 point1bad = true;
402 }
403 if (!point0bad && !point1bad ) {
404 // both points are good, choose nearest
405 sol = ( distanceSq(B[i].at0(), solutions[0].point()) < distanceSq(B[i].at0(), solutions[1].point()) ) ?
406 solutions[0].point() : solutions[1].point();
407 } else if (!point0bad) {
408 sol = solutions[0].point();
409 } else if (!point1bad) {
410 sol = solutions[1].point();
411 } else {
412 solok = false;
413 }
414 (*bbox).expandBy (bbox->width()/4);
415
416 if (!(*bbox).contains(sol)) {
417 solok = false;
418 }
419 Geom::EllipticalArc *arc0 = nullptr;
420 Geom::EllipticalArc *arc1 = nullptr;
421 bool build = false;
422 if (solok) {
423 arc0 = circle1.arc(B[prev_i].at1(), 0.5*(B[prev_i].at1()+sol), sol);
424 arc1 = circle2.arc(sol, 0.5*(sol+B[i].at0()), B[i].at0());
425 if (arc0) {
426 // FIX: Some assertions errors here
427 build_from_sbasis(pb,arc0->toSBasis(), tol, false);
428 build = true;
429 } else if (arc1) {
430 std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1,
431 B[i].at0(), tang2 );
432 if (p) {
433 // check size of miter
434 Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width;
435 Geom::Coord len = distance(*p, point_on_path);
436 if (len <= fabs(width) * miter_limit) {
437 // miter OK
438 pb.lineTo(*p);
439 build = true;
440 }
441 }
442 }
443 if (build) {
444 build_from_sbasis(pb,arc1->toSBasis(), tol, false);
445 } else if (arc0) {
446 pb.lineTo(B[i].at0());
447 }
448 }
449 if (!solok || !(arc0 && build)) {
450 // fall back to miter
451 std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1,
452 B[i].at0(), tang2 );
453 if (p) {
454 // check size of miter
455 Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width;
456 Geom::Coord len = distance(*p, point_on_path);
457 if (len <= fabs(width) * miter_limit) {
458 // miter OK
459 pb.lineTo(*p);
460 }
461 }
462 pb.lineTo(B[i].at0());
463 }
464 if (arc0) {
465 delete arc0;
466 arc0 = nullptr;
467 }
468 if (arc1) {
469 delete arc1;
470 arc1 = nullptr;
471 }
472 } else {
473 // fall back to miter
474 std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1,
475 B[i].at0(), tang2 );
476 if (p) {
477 // check size of miter
478 Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width;
479 Geom::Coord len = distance(*p, point_on_path);
480 if (len <= fabs(width) * miter_limit) {
481 // miter OK
482 pb.lineTo(*p);
483 }
484 }
485 pb.lineTo(B[i].at0());
486 }
487 /*else if (solutions == 1) { // one circle is inside the other
488 // don't know what to do: default to bevel
489 pb.lineTo(B[i].at0());
490 } else { // no intersections
491 // don't know what to do: default to bevel
492 pb.lineTo(B[i].at0());
493 } */
494
495 break;
496 }
497 case LINEJOIN_MITER: {
498 std::optional<Geom::Point> p = intersection_point( B[prev_i].at1(), tang1,
499 B[i].at0(), tang2 );
500 if (p) {
501 // check size of miter
502 Geom::Point point_on_path = B[prev_i].at1() - rot90(tang1) * width;
503 Geom::Coord len = distance(*p, point_on_path);
504 if (len <= fabs(width) * miter_limit) {
505 // miter OK
506 pb.lineTo(*p);
507 }
508 }
509 pb.lineTo(B[i].at0());
510 break;
511 }
512 case LINEJOIN_SPIRO: {
513 Geom::Point direction = B[i].at0() - B[prev_i].at1();
514 double tang1_sign = dot(direction,tang1);
515 double tang2_sign = dot(direction,tang2);
516
517 Spiro::spiro_cp *controlpoints = g_new (Spiro::spiro_cp, 4);
518 controlpoints[0].x = (B[prev_i].at1() - tang1_sign*tang1)[Geom::X];
519 controlpoints[0].y = (B[prev_i].at1() - tang1_sign*tang1)[Geom::Y];
520 controlpoints[0].ty = '{';
521 controlpoints[1].x = B[prev_i].at1()[Geom::X];
522 controlpoints[1].y = B[prev_i].at1()[Geom::Y];
523 controlpoints[1].ty = ']';
524 controlpoints[2].x = B[i].at0()[Geom::X];
525 controlpoints[2].y = B[i].at0()[Geom::Y];
526 controlpoints[2].ty = '[';
527 controlpoints[3].x = (B[i].at0() + tang2_sign*tang2)[Geom::X];
528 controlpoints[3].y = (B[i].at0() + tang2_sign*tang2)[Geom::Y];
529 controlpoints[3].ty = '}';
530
531 Geom::Path spiro;
532 Spiro::spiro_run(controlpoints, 4, spiro);
533 pb.append(spiro.portion(1, spiro.size_open() - 1));
534 break;
535 }
536 case LINEJOIN_BEVEL:
537 default:
538 pb.lineTo(B[i].at0());
539 break;
540 }
541
542 build_from_sbasis(pb, B[i], tol, false);
543
544 } else {
545 // we are on inside of corner!
546 Geom::Path bzr1 = path_from_sbasis( B[prev_i], tol );
547 Geom::Path bzr2 = path_from_sbasis( B[i], tol );
548 Geom::Crossings cross = crossings(bzr1, bzr2);
549 if (cross.size() != 1) {
550 // empty crossing or too many crossings: default to bevel
551 pb.lineTo(B[i].at0());
552 pb.append(bzr2);
553 } else {
554 // :-) quick hack:
555 for (unsigned i=0; i < bzr1.size_open(); ++i) {
556 pb.backspace();
557 }
558
559 pb.append( bzr1.portion(0, cross[0].ta) );
560 pb.append( bzr2.portion(cross[0].tb, bzr2.size_open()) );
561 }
562 }
563 } else {
564 build_from_sbasis(pb, B[i], tol, false);
565 }
566
567 prev_i = i;
568 }
569 pb.flush();
570 return pb.peek().front();
571 }
572
573 Geom::PathVector
doEffect_path(Geom::PathVector const & path_in)574 LPEPowerStroke::doEffect_path (Geom::PathVector const & path_in)
575 {
576 using namespace Geom;
577
578 Geom::PathVector path_out;
579 if (path_in.empty()) {
580 return path_in;
581 }
582 Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(path_in);
583 Geom::Piecewise<Geom::D2<Geom::SBasis> > pwd2_in = pathv[0].toPwSb();
584 if (pwd2_in.empty()) {
585 return path_in;
586 }
587 Piecewise<D2<SBasis> > der = derivative(pwd2_in);
588 if (der.empty()) {
589 return path_in;
590 }
591 Piecewise<D2<SBasis> > n = unitVector(der,0.00001);
592 if (n.empty()) {
593 return path_in;
594 }
595
596 n = rot90(n);
597 offset_points.set_pwd2(pwd2_in, n);
598
599 LineCapType end_linecap = static_cast<LineCapType>(end_linecap_type.get_value());
600 LineCapType start_linecap = static_cast<LineCapType>(start_linecap_type.get_value());
601
602 std::vector<Geom::Point> ts_no_scale = offset_points.data();
603 if (ts_no_scale.empty()) {
604 return path_in;
605 }
606 std::vector<Geom::Point> ts;
607 for (auto & tsp : ts_no_scale) {
608 Geom::Point p = Geom::Point(tsp[Geom::X], tsp[Geom::Y] * scale_width);
609 ts.push_back(p);
610 }
611 if (sort_points) {
612 sort(ts.begin(), ts.end(), compare_offsets);
613 }
614 // create stroke path where points (x,y) := (t, offset)
615 Geom::Interpolate::Interpolator *interpolator = Geom::Interpolate::Interpolator::create(static_cast<Geom::Interpolate::InterpolatorType>(interpolator_type.get_value()));
616 if (Geom::Interpolate::CubicBezierJohan *johan = dynamic_cast<Geom::Interpolate::CubicBezierJohan*>(interpolator)) {
617 johan->setBeta(interpolator_beta);
618 }
619 if (Geom::Interpolate::CubicBezierSmooth *smooth = dynamic_cast<Geom::Interpolate::CubicBezierSmooth*>(interpolator)) {
620 smooth->setBeta(interpolator_beta);
621 }
622 if (pathv[0].closed()) {
623 std::vector<Geom::Point> ts_close;
624 //we have only one knot or overwrite before
625 Geom::Point start = Geom::Point( pwd2_in.domain().min(), ts.front()[Geom::Y]);
626 Geom::Point end = Geom::Point( pwd2_in.domain().max(), ts.front()[Geom::Y]);
627 if (ts.size() > 1) {
628 end = Geom::Point(pwd2_in.domain().max(), 0);
629 Geom::Point tmpstart(0, 0);
630 tmpstart[Geom::X] = end[Geom::X] + ts.front()[Geom::X];
631 tmpstart[Geom::Y] = ts.front()[Geom::Y];
632 ts_close.push_back(ts.back());
633 ts_close.push_back(middle_point(tmpstart, ts.back()));
634 ts_close.push_back(tmpstart);
635 Geom::Path closepath = interpolator->interpolateToPath(ts_close);
636 end = closepath.pointAt(Geom::nearest_time(end, closepath));
637 end[Geom::X] = pwd2_in.domain().max();
638 start = end;
639 start[Geom::X] = pwd2_in.domain().min();
640 }
641 ts.insert(ts.begin(), start );
642 ts.push_back( end );
643 ts_close.clear();
644 } else {
645 // add width data for first and last point on the path
646 // depending on cap type, these first and last points have width zero or take the width from the closest width point.
647 ts.insert(ts.begin(), Point( pwd2_in.domain().min(),
648 (start_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.front()[Geom::Y]) );
649 ts.emplace_back( pwd2_in.domain().max(),
650 (end_linecap==LINECAP_ZERO_WIDTH) ? 0. : ts.back()[Geom::Y] );
651 }
652
653 // do the interpolation in a coordinate system that is more alike to the on-canvas knots,
654 // instead of the heavily compressed coordinate system of (segment_no offset, Y) in which the knots are stored
655 double pwd2_in_arclength = length(pwd2_in);
656 double xcoord_scaling = pwd2_in_arclength / ts.back()[Geom::X];
657 for (auto & t : ts) {
658 t[Geom::X] *= xcoord_scaling;
659 }
660
661 Geom::Path strokepath = interpolator->interpolateToPath(ts);
662 delete interpolator;
663
664 // apply the inverse knot-xcoord scaling that was applied before the interpolation
665 strokepath *= Scale(1/xcoord_scaling, 1);
666
667 D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(strokepath.toPwSb());
668 Piecewise<SBasis> x = Piecewise<SBasis>(patternd2[0]);
669 Piecewise<SBasis> y = Piecewise<SBasis>(patternd2[1]);
670 // find time values for which x lies outside path domain
671 // and only take portion of x and y that lies within those time values
672 std::vector< double > rtsmin = roots (x - pwd2_in.domain().min());
673 std::vector< double > rtsmax = roots (x + pwd2_in.domain().max());
674 if ( !rtsmin.empty() && !rtsmax.empty() ) {
675 x = portion(x, rtsmin.at(0), rtsmax.at(0));
676 y = portion(y, rtsmin.at(0), rtsmax.at(0));
677 }
678
679 LineJoinType jointype = static_cast<LineJoinType>(linejoin_type.get_value());
680 if (x.empty() || y.empty()) {
681 return path_in;
682 }
683 Piecewise<D2<SBasis> > pwd2_out = compose(pwd2_in,x) + y*compose(n,x);
684 Piecewise<D2<SBasis> > mirrorpath = reverse( compose(pwd2_in,x) - y*compose(n,x));
685
686 Geom::Path fixed_path = path_from_piecewise_fix_cusps( pwd2_out, y, jointype, miter_limit, LPE_CONVERSION_TOLERANCE);
687 Geom::Path fixed_mirrorpath = path_from_piecewise_fix_cusps( mirrorpath, reverse(y), jointype, miter_limit, LPE_CONVERSION_TOLERANCE);
688 if (pathv[0].closed()) {
689 fixed_path.close(true);
690 path_out.push_back(fixed_path);
691 fixed_mirrorpath.close(true);
692 path_out.push_back(fixed_mirrorpath);
693 } else {
694 // add linecaps...
695 switch (end_linecap) {
696 case LINECAP_ZERO_WIDTH:
697 // do nothing
698 break;
699 case LINECAP_PEAK:
700 {
701 Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.);
702 double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint());
703 Geom::Point midpoint = 0.5*(fixed_path.finalPoint() + fixed_mirrorpath.initialPoint()) + radius*end_deriv;
704 fixed_path.appendNew<LineSegment>(midpoint);
705 fixed_path.appendNew<LineSegment>(fixed_mirrorpath.initialPoint());
706 break;
707 }
708 case LINECAP_SQUARE:
709 {
710 Geom::Point end_deriv = -unitTangentAt( reverse(pwd2_in.segs.back()), 0.);
711 double radius = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint());
712 fixed_path.appendNew<LineSegment>( fixed_path.finalPoint() + radius*end_deriv );
713 fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() + radius*end_deriv );
714 fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() );
715 break;
716 }
717 case LINECAP_BUTT:
718 {
719 fixed_path.appendNew<LineSegment>( fixed_mirrorpath.initialPoint() );
720 break;
721 }
722 case LINECAP_ROUND:
723 default:
724 {
725 double radius1 = 0.5 * distance(fixed_path.finalPoint(), fixed_mirrorpath.initialPoint());
726 fixed_path.appendNew<EllipticalArc>( radius1, radius1, M_PI/2., false, y.lastValue() < 0, fixed_mirrorpath.initialPoint() );
727 break;
728 }
729 }
730
731 fixed_path.append(fixed_mirrorpath);
732 switch (start_linecap) {
733 case LINECAP_ZERO_WIDTH:
734 // do nothing
735 break;
736 case LINECAP_PEAK:
737 {
738 Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.);
739 double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint());
740 Geom::Point midpoint = 0.5*(fixed_mirrorpath.finalPoint() + fixed_path.initialPoint()) - radius*start_deriv;
741 fixed_path.appendNew<LineSegment>( midpoint );
742 fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() );
743 break;
744 }
745 case LINECAP_SQUARE:
746 {
747 Geom::Point start_deriv = unitTangentAt( pwd2_in.segs.front(), 0.);
748 double radius = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint());
749 fixed_path.appendNew<LineSegment>( fixed_mirrorpath.finalPoint() - radius*start_deriv );
750 fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() - radius*start_deriv );
751 fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() );
752 break;
753 }
754 case LINECAP_BUTT:
755 {
756 fixed_path.appendNew<LineSegment>( fixed_path.initialPoint() );
757 break;
758 }
759 case LINECAP_ROUND:
760 default:
761 {
762 double radius2 = 0.5 * distance(fixed_path.initialPoint(), fixed_mirrorpath.finalPoint());
763 fixed_path.appendNew<EllipticalArc>( radius2, radius2, M_PI/2., false, y.firstValue() < 0, fixed_path.initialPoint() );
764 break;
765 }
766 }
767 fixed_path.close(true);
768 path_out.push_back(fixed_path);
769 }
770 if (path_out.empty()) {
771 return path_in;
772 // doEffect_path (path_in);
773 }
774 return path_out;
775 }
776
transform_multiply(Geom::Affine const & postmul,bool)777 void LPEPowerStroke::transform_multiply(Geom::Affine const &postmul, bool /*set*/)
778 {
779 offset_points.param_transform_multiply(postmul, false);
780 }
781
doAfterEffect(SPLPEItem const * lpeitem,SPCurve * curve)782 void LPEPowerStroke::doAfterEffect(SPLPEItem const *lpeitem, SPCurve *curve)
783 {
784 if (pathvector_before_effect[0].size() == pathvector_after_effect[0].size()) {
785 if (recusion_limit < 6) {
786 Inkscape::LivePathEffect::Effect *effect =
787 sp_lpe_item->getPathEffectOfType(Inkscape::LivePathEffect::SIMPLIFY);
788 if (effect) {
789 LivePathEffect::LPESimplify *simplify =
790 dynamic_cast<LivePathEffect::LPESimplify *>(effect->getLPEObj()->get_lpe());
791 double threshold = simplify->threshold * 1.2;
792 simplify->threshold.param_set_value(threshold);
793 simplify->threshold.write_to_SVG();
794 has_recursion = true;
795 }
796 }
797 ++recusion_limit;
798 } else {
799 recusion_limit = 0;
800 }
801 }
802
803 /* ######################## */
804
805 } //namespace LivePathEffect
806 } /* namespace Inkscape */
807
808 /*
809 Local Variables:
810 mode:c++
811 c-file-style:"stroustrup"
812 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
813 indent-tabs-mode:nil
814 fill-column:99
815 End:
816 */
817 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
818