1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Copyright (C) Johan Engelen 2007 <j.b.c.engelen@utwente.nl>
4 *
5 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
6 */
7
8 #include <cmath>
9 #include <algorithm>
10 #include <vector>
11
12 #include <2geom/bezier-to-sbasis.h>
13
14 #include "live_effects/lpe-patternalongpath.h"
15 #include "live_effects/lpeobject.h"
16
17 #include "display/curve.h"
18
19 #include "object/sp-shape.h"
20
21 #include "ui/knot/knot-holder.h"
22 #include "ui/knot/knot-holder-entity.h"
23
24 // TODO due to internal breakage in glibmm headers, this must be last:
25 #include <glibmm/i18n.h>
26
27
28 /* Theory in e-mail from J.F. Barraud
29 Let B be the skeleton path, and P the pattern (the path to be deformed).
30
31 P is a map t --> P(t) = ( x(t), y(t) ).
32 B is a map t --> B(t) = ( a(t), b(t) ).
33
34 The first step is to re-parametrize B by its arc length: this is the parametrization in which a point p on B is located by its distance s from start. One obtains a new map s --> U(s) = (a'(s),b'(s)), that still describes the same path B, but where the distance along B from start to
35 U(s) is s itself.
36
37 We also need a unit normal to the path. This can be obtained by computing a unit tangent vector, and rotate it by 90�. Call this normal vector N(s).
38
39 The basic deformation associated to B is then given by:
40
41 (x,y) --> U(x)+y*N(x)
42
43 (i.e. we go for distance x along the path, and then for distance y along the normal)
44
45 Of course this formula needs some minor adaptations (as is it depends on the absolute position of P for instance, so a little translation is needed
46 first) but I think we can first forget about them.
47 */
48
49 namespace Inkscape {
50 namespace LivePathEffect {
51
52 namespace WPAP {
53 class KnotHolderEntityWidthPatternAlongPath : public LPEKnotHolderEntity {
54 public:
KnotHolderEntityWidthPatternAlongPath(LPEPatternAlongPath * effect)55 KnotHolderEntityWidthPatternAlongPath(LPEPatternAlongPath * effect) : LPEKnotHolderEntity(effect) {}
~KnotHolderEntityWidthPatternAlongPath()56 ~KnotHolderEntityWidthPatternAlongPath() override
57 {
58 LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect);
59 lpe->_knot_entity = nullptr;
60 }
61 void knot_set(Geom::Point const &p, Geom::Point const &origin, guint state) override;
62 Geom::Point knot_get() const override;
63 };
64 } // WPAP
65
66 static const Util::EnumData<PAPCopyType> PAPCopyTypeData[PAPCT_END] = {
67 // clang-format off
68 {PAPCT_SINGLE, N_("Single"), "single"},
69 {PAPCT_SINGLE_STRETCHED, N_("Single, stretched"), "single_stretched"},
70 {PAPCT_REPEATED, N_("Repeated"), "repeated"},
71 {PAPCT_REPEATED_STRETCHED, N_("Repeated, stretched"), "repeated_stretched"}
72 // clang-format on
73 };
74 static const Util::EnumDataConverter<PAPCopyType> PAPCopyTypeConverter(PAPCopyTypeData, PAPCT_END);
75
LPEPatternAlongPath(LivePathEffectObject * lpeobject)76 LPEPatternAlongPath::LPEPatternAlongPath(LivePathEffectObject *lpeobject) :
77 Effect(lpeobject),
78 pattern(_("Pattern source:"), _("Path to put along the skeleton path"), "pattern", &wr, this, "M0,0 L1,0"),
79 original_height(0.0),
80 prop_scale(_("_Width:"), _("Width of the pattern"), "prop_scale", &wr, this, 1.0),
81 copytype(_("Pattern copies:"), _("How many pattern copies to place along the skeleton path"),
82 "copytype", PAPCopyTypeConverter, &wr, this, PAPCT_SINGLE_STRETCHED),
83 scale_y_rel(_("Wid_th in units of length"),
84 _("Scale the width of the pattern in units of its length"),
85 "scale_y_rel", &wr, this, false),
86 spacing(_("Spa_cing:"),
87 // xgettext:no-c-format
88 _("Space between copies of the pattern. Negative values allowed, but are limited to -90% of pattern width."),
89 "spacing", &wr, this, 0),
90 normal_offset(_("No_rmal offset:"), "", "normal_offset", &wr, this, 0),
91 tang_offset(_("Tan_gential offset:"), "", "tang_offset", &wr, this, 0),
92 prop_units(_("Offsets in _unit of pattern size"),
93 _("Spacing, tangential and normal offset are expressed as a ratio of width/height"),
94 "prop_units", &wr, this, false),
95 vertical_pattern(_("Pattern is _vertical"), _("Rotate pattern 90 deg before applying"),
96 "vertical_pattern", &wr, this, false),
97 hide_knot(_("Hide width knot"), _("Hide width knot"),"hide_knot", &wr, this, false),
98 fuse_tolerance(_("_Fuse nearby ends:"), _("Fuse ends closer than this number. 0 means don't fuse."),
99 "fuse_tolerance", &wr, this, 0)
100 {
101 registerParameter(&pattern);
102 registerParameter(©type);
103 registerParameter(&prop_scale);
104 registerParameter(&scale_y_rel);
105 registerParameter(&spacing);
106 registerParameter(&normal_offset);
107 registerParameter(&tang_offset);
108 registerParameter(&prop_units);
109 registerParameter(&vertical_pattern);
110 registerParameter(&hide_knot);
111 registerParameter(&fuse_tolerance);
112 prop_scale.param_set_digits(3);
113 prop_scale.param_set_increments(0.01, 0.10);
114 _knot_entity = nullptr;
115 _provides_knotholder_entities = true;
116
117 }
118
119 LPEPatternAlongPath::~LPEPatternAlongPath()
120 = default;
121
transform_multiply(Geom::Affine const & postmul,bool)122 void LPEPatternAlongPath::transform_multiply(Geom::Affine const &postmul, bool /*set*/)
123 {
124 if (sp_lpe_item && sp_lpe_item->pathEffectsEnabled() && sp_lpe_item->optimizeTransforms()) {
125 pattern.param_transform_multiply(postmul, false);
126 }
127 }
128
129 void
doBeforeEffect(SPLPEItem const * lpeitem)130 LPEPatternAlongPath::doBeforeEffect (SPLPEItem const* lpeitem)
131 {
132 // get the pattern bounding box
133 Geom::OptRect bbox = pattern.get_pathvector().boundsFast();
134 if (bbox) {
135 original_height = (*bbox)[Geom::Y].max() - (*bbox)[Geom::Y].min();
136 }
137 if (_knot_entity) {
138 if (hide_knot) {
139 helper_path.clear();
140 _knot_entity->knot->hide();
141 } else {
142 _knot_entity->knot->show();
143 }
144 _knot_entity->update_knot();
145 }
146 }
147
148 Geom::Piecewise<Geom::D2<Geom::SBasis> >
doEffect_pwd2(Geom::Piecewise<Geom::D2<Geom::SBasis>> const & pwd2_in)149 LPEPatternAlongPath::doEffect_pwd2 (Geom::Piecewise<Geom::D2<Geom::SBasis> > const & pwd2_in)
150 {
151 using namespace Geom;
152
153 // Don't allow empty path parameter:
154 if ( pattern.get_pathvector().empty() ) {
155 return pwd2_in;
156 }
157
158 /* Much credit should go to jfb and mgsloan of lib2geom development for the code below! */
159 Piecewise<D2<SBasis> > output;
160 std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > pre_output;
161
162 PAPCopyType type = copytype.get_value();
163
164 D2<Piecewise<SBasis> > patternd2 = make_cuts_independent(pattern.get_pwd2());
165 Piecewise<SBasis> x0 = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[1]) : Piecewise<SBasis>(patternd2[0]);
166 Piecewise<SBasis> y0 = vertical_pattern.get_value() ? Piecewise<SBasis>(patternd2[0]) : Piecewise<SBasis>(patternd2[1]);
167 OptInterval pattBndsX = bounds_exact(x0);
168 OptInterval pattBndsY = bounds_exact(y0);
169 if (pattBndsX && pattBndsY) {
170 x0 -= pattBndsX->min();
171 y0 -= pattBndsY->middle();
172
173 double xspace = spacing;
174 double noffset = normal_offset;
175 double toffset = tang_offset;
176 if (prop_units.get_value()){
177 xspace *= pattBndsX->extent();
178 noffset *= pattBndsY->extent();
179 toffset *= pattBndsX->extent();
180 }
181
182 //Prevent more than 90% overlap...
183 if (xspace < -pattBndsX->extent() * 0.9) {
184 xspace = -pattBndsX->extent() * 0.9;
185 }
186 //TODO: dynamical update of parameter ranges?
187 //if (prop_units.get_value()){
188 // spacing.param_set_range(-.9, Geom::infinity());
189 // }else{
190 // spacing.param_set_range(-pattBndsX.extent()*.9, Geom::infinity());
191 // }
192
193 y0 += noffset;
194
195 std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > paths_in;
196 paths_in = split_at_discontinuities(pwd2_in);
197
198 for (auto path_i : paths_in){
199 Piecewise<SBasis> x = x0;
200 Piecewise<SBasis> y = y0;
201 Piecewise<D2<SBasis> > uskeleton = arc_length_parametrization(path_i,2, 0.1);
202 uskeleton = remove_short_cuts(uskeleton, 0.01);
203 Piecewise<D2<SBasis> > n = rot90(derivative(uskeleton));
204 if (Geom::are_near(pwd2_in[0].at0(),pwd2_in[pwd2_in.size()-1].at1(), 0.01)) {
205 n = force_continuity(remove_short_cuts(n, 0.1), 0.01);
206 } else {
207 n = force_continuity(remove_short_cuts(n, 0.1));
208 }
209 int nbCopies = 0;
210 double scaling = 1;
211 switch(type) {
212 case PAPCT_REPEATED:
213 nbCopies = static_cast<int>(floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace)));
214 pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace);
215 break;
216
217 case PAPCT_SINGLE:
218 nbCopies = (toffset + pattBndsX->extent() < uskeleton.domain().extent()) ? 1 : 0;
219 break;
220
221 case PAPCT_SINGLE_STRETCHED:
222 nbCopies = 1;
223 scaling = (uskeleton.domain().extent() - toffset)/pattBndsX->extent();
224 break;
225
226 case PAPCT_REPEATED_STRETCHED:
227 // if uskeleton is closed:
228 if (are_near(path_i.segs.front().at0(), path_i.segs.back().at1())){
229 nbCopies = std::max(1, static_cast<int>(std::floor((uskeleton.domain().extent() - toffset)/(pattBndsX->extent()+xspace))));
230 pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace);
231 scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent());
232 // if not closed: no space at the end
233 }else{
234 nbCopies = std::max(1, static_cast<int>(std::floor((uskeleton.domain().extent() - toffset + xspace)/(pattBndsX->extent()+xspace))));
235 pattBndsX = Interval(pattBndsX->min(),pattBndsX->max()+xspace);
236 scaling = (uskeleton.domain().extent() - toffset)/(((double)nbCopies)*pattBndsX->extent() - xspace);
237 }
238 break;
239
240 default:
241 return pwd2_in;
242 };
243
244 //Ceil to 6 decimals
245 scaling = ceil(scaling * 1000000) / 1000000;
246 double pattWidth = pattBndsX->extent() * scaling;
247
248 x *= scaling;
249 if ( scale_y_rel.get_value() ) {
250 y *= prop_scale * scaling;
251 } else {
252 y *= prop_scale;
253 }
254 x += toffset;
255
256 double offs = 0;
257 for (int i=0; i<nbCopies; i++){
258 if (fuse_tolerance > 0){
259 Geom::Piecewise<Geom::D2<Geom::SBasis> > output_piece = compose(uskeleton,x+offs)+y*compose(n,x+offs);
260 std::vector<Geom::Piecewise<Geom::D2<Geom::SBasis> > > splited_output_piece = split_at_discontinuities(output_piece);
261 pre_output.insert(pre_output.end(), splited_output_piece.begin(), splited_output_piece.end() );
262 }else{
263 output.concat(compose(uskeleton,x+offs)+y*compose(n,x+offs));
264 }
265 offs+=pattWidth;
266 }
267 }
268 if (fuse_tolerance > 0){
269 pre_output = fuse_nearby_ends(pre_output, fuse_tolerance);
270 for (const auto & i : pre_output){
271 output.concat(i);
272 }
273 }
274 return output;
275 } else {
276 return pwd2_in;
277 }
278 }
279
280 void
addCanvasIndicators(SPLPEItem const *,std::vector<Geom::PathVector> & hp_vec)281 LPEPatternAlongPath::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec)
282 {
283 hp_vec.push_back(helper_path);
284 }
285
286
287 void
addKnotHolderEntities(KnotHolder * knotholder,SPItem * item)288 LPEPatternAlongPath::addKnotHolderEntities(KnotHolder *knotholder, SPItem *item)
289 {
290 _knot_entity = new WPAP::KnotHolderEntityWidthPatternAlongPath(this);
291 _knot_entity->create(nullptr, item, knotholder, Inkscape::CANVAS_ITEM_CTRL_TYPE_LPE, "LPE:PatternAlongPath",
292 _("Change the width"));
293 knotholder->add(_knot_entity);
294 if (hide_knot) {
295 _knot_entity->knot->hide();
296 _knot_entity->update_knot();
297 }
298 }
299
300 namespace WPAP {
301
302 void
knot_set(Geom::Point const & p,Geom::Point const &,guint state)303 KnotHolderEntityWidthPatternAlongPath::knot_set(Geom::Point const &p, Geom::Point const& /*origin*/, guint state)
304 {
305 LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect);
306
307 Geom::Point const s = snap_knot_position(p, state);
308 SPShape const *sp_shape = dynamic_cast<SPShape const *>(SP_LPE_ITEM(item));
309 if (sp_shape && lpe->original_height) {
310 auto curve_before = SPCurve::copy(sp_shape->curveForEdit());
311 if (curve_before) {
312 Geom::Path const *path_in = curve_before->first_path();
313 Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0));
314 Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0));
315 Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0));
316 Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve);
317 Geom::Ray ray(ptA, B);
318 if (cubic) {
319 ray.setPoints(ptA, (*cubic)[1]);
320 }
321 ray.setAngle(ray.angle() + Geom::rad_from_deg(90));
322 Geom::Point knot_pos = this->knot->pos * item->i2dt_affine().inverse();
323 Geom::Coord nearest_to_ray = ray.nearestTime(knot_pos);
324 if(nearest_to_ray == 0){
325 lpe->prop_scale.param_set_value(-Geom::distance(s , ptA)/(lpe->original_height/2.0));
326 } else {
327 lpe->prop_scale.param_set_value(Geom::distance(s , ptA)/(lpe->original_height/2.0));
328 }
329 }
330 if (!lpe->original_height) {
331 lpe->prop_scale.param_set_value(0);
332 }
333 Inkscape::Preferences *prefs = Inkscape::Preferences::get();
334 prefs->setDouble("/live_effects/skeletal/width", lpe->prop_scale);
335 }
336 sp_lpe_item_update_patheffect (SP_LPE_ITEM(item), false, true);
337 }
338
339 Geom::Point
knot_get() const340 KnotHolderEntityWidthPatternAlongPath::knot_get() const
341 {
342 LPEPatternAlongPath *lpe = dynamic_cast<LPEPatternAlongPath *> (_effect);
343 SPShape const *sp_shape = dynamic_cast<SPShape const *>(SP_LPE_ITEM(item));
344 if (sp_shape) {
345 auto curve_before = SPCurve::copy(sp_shape->curveForEdit());
346 if (curve_before) {
347 Geom::Path const *path_in = curve_before->first_path();
348 Geom::Point ptA = path_in->pointAt(Geom::PathTime(0, 0.0));
349 Geom::Point B = path_in->pointAt(Geom::PathTime(1, 0.0));
350 Geom::Curve const *first_curve = &path_in->curveAt(Geom::PathTime(0, 0.0));
351 Geom::CubicBezier const *cubic = dynamic_cast<Geom::CubicBezier const *>(&*first_curve);
352 Geom::Ray ray(ptA, B);
353 if (cubic) {
354 ray.setPoints(ptA, (*cubic)[1]);
355 }
356 ray.setAngle(ray.angle() + Geom::rad_from_deg(90));
357 Geom::Point result_point = Geom::Point::polar(ray.angle(), (lpe->original_height/2.0) * lpe->prop_scale) + ptA;
358 lpe->helper_path.clear();
359 if (!lpe->hide_knot) {
360 Geom::Path hp(result_point);
361 hp.appendNew<Geom::LineSegment>(ptA);
362 lpe->helper_path.push_back(hp);
363 hp.clear();
364 }
365 return result_point;
366 }
367 }
368 return Geom::Point();
369 }
370 } // namespace WPAP
371 } // namespace LivePathEffect
372 } /* namespace Inkscape */
373
374 /*
375 Local Variables:
376 mode:c++
377 c-file-style:"stroustrup"
378 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
379 indent-tabs-mode:nil
380 fill-column:99
381 End:
382 */
383 // vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
384