Inkscape
Vector Graphics Editor
Loading...
Searching...
No Matches
lpe-bspline.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: GPL-2.0-or-later
2/*
3 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
4 */
5
6#include <glibmm/i18n.h>
7#include <glibmm/ustring.h> // for operator==, ustring
8#include <gtkmm/box.h> // for Box
9#include <gtkmm/button.h> // for Button
10#include <gtkmm/entry.h> // for Entry
11#include <gtkmm/enums.h> // for Orientation
12#include <gtkmm/widget.h> // for Widget
13
14#include "display/curve.h"
16#include "object/sp-path.h"
17#include "preferences.h"
18#include "svg/svg.h"
19#include "ui/pack.h"
20#include "ui/util.h"
21#include "ui/widget/scalar.h"
23
25
26static constexpr double BSPLINE_TOL = 0.001;
27static constexpr double NO_POWER = 0.0;
28static constexpr double DEFAULT_START_POWER = 1.0 / 3.0;
29static constexpr double DEFAULT_END_POWER = 2.0 / 3.0;
30
31Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size);
32
34 : Effect(lpeobject),
35 steps(_("Steps with CTRL:"), _("Change number of steps with CTRL pressed"), "steps", &wr, this, 2),
36 helper_size(_("Helper size:"), _("Helper size"), "helper_size", &wr, this, 0),
37 apply_no_weight(_("Apply changes if weight = 0%"), _("Apply changes if weight = 0%"), "apply_no_weight", &wr, this, true),
38 apply_with_weight(_("Apply changes if weight > 0%"), _("Apply changes if weight > 0%"), "apply_with_weight", &wr, this, true),
39 only_selected(_("Change only selected nodes"), _("Change only selected nodes"), "only_selected", &wr, this, false),
40 uniform(_("Uniform BSpline"), _("Uniform bspline"), "uniform", &wr, this, false),
41 weight(_("Change weight %:"), _("Change weight percent of the effect"), "weight", &wr, this, DEFAULT_START_POWER * 100)
42{
50
54
58
59 helper_size.param_set_range(0.0, 999.0);
62}
63
64LPEBSpline::~LPEBSpline() = default;
65
66void LPEBSpline::doBeforeEffect (SPLPEItem const* /*lpeitem*/)
67{
68 if(!hp.empty()) {
69 hp.clear();
70 }
71}
72
73void LPEBSpline::doOnApply(SPLPEItem const* lpeitem)
74{
75 if (!is<SPShape>(lpeitem)) {
76 g_warning("LPE BSpline can only be applied to shapes (not groups).");
77 SPLPEItem * item = const_cast<SPLPEItem*>(lpeitem);
78 item->removeCurrentPathEffect(false);
79 }
80 lpeversion.param_setValue("1.3", true);
81}
82
83void
84LPEBSpline::addCanvasIndicators(SPLPEItem const */*lpeitem*/, std::vector<Geom::PathVector> &hp_vec)
85{
86 hp_vec.push_back(hp);
87}
88
90{
91 // use manage here, because after deletion of Effect object, others might
92 // still be pointing to this widget.
93 auto const vbox = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::VERTICAL);
94 vbox->set_margin(5);
95
96 for (auto const param: param_vector) {
97 if (!param->widget_is_visible) continue;
98
99 auto const widg = param->param_newWidget();
100 if (!widg) continue;
101
102 if (param->param_key == "weight") {
103 auto const buttons = Gtk::make_managed<Gtk::Box>(Gtk::Orientation::HORIZONTAL,0);
104
105 auto const default_weight = Gtk::make_managed<Gtk::Button>(_("Default weight"));
106 default_weight->signal_clicked()
107 .connect(sigc::mem_fun(*this, &LPEBSpline::toDefaultWeight));
108 UI::pack_start(*buttons, *default_weight, true, true, 2);
109
110 auto const make_cusp = Gtk::make_managed<Gtk::Button>(_("Make cusp"));
111 make_cusp->signal_clicked()
112 .connect(sigc::mem_fun(*this, &LPEBSpline::toMakeCusp));
113 UI::pack_start(*buttons, *make_cusp, true, true, 2);
114
115 UI::pack_start(*vbox, *buttons, true, true, 2);
116 }
117 if (param->param_key == "weight" || param->param_key == "steps") {
118 auto &scalar = dynamic_cast<UI::Widget::Scalar &>(*widg);
119 scalar.signal_value_changed().connect(sigc::mem_fun(*this, &LPEBSpline::toWeight));
120 scalar.getSpinButton().set_width_chars(9);
121 }
122
123 UI::pack_start(*vbox, *widg, true, true, 2);
124
125 if (auto const tip = param->param_getTooltip()) {
126 widg->set_tooltip_markup(*tip);
127 } else {
128 widg->set_tooltip_text({});
129 widg->set_has_tooltip(false);
130 }
131 }
132
133 return vbox;
134}
135
137{
140 makeUndoDone(_("Change to default weight"));
141}
142
144{
147 makeUndoDone(_("Change to 0 weight"));
148}
149
151{
154 makeUndoDone(_("Change scalar parameter"));
155}
156
157void LPEBSpline::changeWeight(double weight_ammount)
158{
159 auto path = cast<SPPath>(sp_lpe_item);
160 if (path) {
161 auto curve = *path->curveForEdit();
162 doBSplineFromWidget(curve, weight_ammount / 100.0);
163 path->setAttribute("inkscape:original-d", sp_svg_write_path(curve));
164 }
165}
166
171
173{
174 if (curve.curveCount() < 1) {
175 return;
176 }
177 Geom::PathVector original_pathv = curve;
178 curve.clear();
180 for (auto & path_it : original_pathv) {
181 if (path_it.empty()) {
182 continue;
183 }
184 if (!prefs->getBool("/tools/nodes/show_outline", true)){
185 hp.push_back(path_it);
186 }
187 Geom::CubicBezier const *cubic = nullptr;
188 // BSplines has special tratment for start/end on uniform cubic bsplines
189 // we need to change power from 1/3 to 1/2 and apply the factor of current power
190 if (uniform && !path_it.closed() && path_it.size_open() > 1) {
191 cubic = dynamic_cast<Geom::CubicBezier const *>(&path_it.front());
192 if (cubic) {
193 double factor = Geom::nearest_time((*cubic)[2], path_it.front()) / DEFAULT_END_POWER;
194 Geom::Path newp((*cubic)[0]);
195 newp.appendNew<Geom::CubicBezier>((*cubic)[0], path_it.front().pointAt(0.5 + (factor - 1)), (*cubic)[3]);
196 path_it.erase(path_it.begin());
197 cubic = dynamic_cast<Geom::CubicBezier const *>(&path_it.front());
198 if (cubic) {
199 double factor = Geom::nearest_time((*cubic)[2], path_it.front()) / DEFAULT_END_POWER;
200 Geom::Path newp2((*cubic)[0]);
201 newp2.appendNew<Geom::CubicBezier>((*cubic)[1], path_it.front().pointAt(0.5 + (factor - 1)), (*cubic)[3]);
202 path_it.erase(path_it.begin());
203 newp.setFinal(newp2.back_open().initialPoint());
204 newp.append(newp2);
205 }
206 path_it.setInitial(newp.back_open().finalPoint());
207 newp.append(path_it);
208 path_it = newp;
209 }
210 cubic = dynamic_cast<Geom::CubicBezier const *>(&path_it.back_open());
211 if (cubic && path_it.size_open() > 2) {
212 double factor = (Geom::nearest_time((*cubic)[1], path_it.back_open()) * 0.5) / DEFAULT_START_POWER;
213 Geom::Path newp((*cubic)[0]);
214 newp.appendNew<Geom::CubicBezier>(path_it.back_open().pointAt(factor), (*cubic)[3], (*cubic)[3]);
215 path_it.erase_last();
216 cubic = dynamic_cast<Geom::CubicBezier const *>(&path_it.back_open());
217 if (cubic && path_it.size_open() > 3) {
218 double factor = (Geom::nearest_time((*cubic)[1], path_it.back_open()) * 0.5) / DEFAULT_START_POWER;
219 Geom::Path newp2((*cubic)[0]);
220 newp2.appendNew<Geom::CubicBezier>(path_it.back_open().pointAt(factor), (*cubic)[2], (*cubic)[3]);
221 path_it.erase_last();
222 newp2.setFinal(newp.back_open().initialPoint());
223 newp2.append(newp);
224 newp = newp2;
225 }
226 path_it.setFinal(newp.initialPoint());
227 path_it.append(newp);
228 }
229 }
230 Geom::Path::iterator curve_it1 = path_it.begin();
231 Geom::Path::iterator curve_it2 = ++(path_it.begin());
232 Geom::Path::iterator curve_endit = path_it.end_default();
233 Geom::Point previousNode(0, 0);
234 Geom::Point node(0, 0);
235 Geom::Point point_at1(0, 0);
236 Geom::Point point_at2(0, 0);
237 Geom::Point next_point_at1(0, 0);
238 auto curve_n = Geom::Path{curve_it1->initialPoint()};
239 if (path_it.closed()) {
240 const Geom::Curve &closingline = path_it.back_closed();
241 // the closing line segment is always of type
242 // Geom::LineSegment.
243 if (are_near(closingline.initialPoint(), closingline.finalPoint())) {
244 // closingline.isDegenerate() did not work, because it only checks for
245 // *exact* zero length, which goes wrong for relative coordinates and
246 // rounding errors...
247 // the closing line segment has zero-length. So stop before that one!
248 curve_endit = path_it.end_open();
249 }
250 }
251 while (curve_it1 != curve_endit) {
252 auto const in = Geom::LineSegment{curve_it1->initialPoint(), curve_it1->finalPoint()};
253 cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1);
254 if (cubic) {
255 if (are_near((*cubic)[1], (*cubic)[0]) && !are_near((*cubic)[2], (*cubic)[3])) {
256 point_at1 = in.pointAt(DEFAULT_START_POWER);
257 } else {
258 point_at1 = in.pointAt(Geom::nearest_time((*cubic)[1], in));
259 }
260 if (uniform && curve_n.size_default() == 0) {
261 point_at1 = curve_it1->initialPoint();
262 }
263 if (are_near((*cubic)[2], (*cubic)[3]) && !are_near((*cubic)[1], (*cubic)[0])) {
264 point_at2 = in.pointAt(DEFAULT_END_POWER);
265 } else {
266 point_at2 = in.pointAt(Geom::nearest_time((*cubic)[2], in));
267 }
268 } else {
269 point_at1 = in.initialPoint();
270 point_at2 = in.finalPoint();
271 }
272 if (curve_it2 != curve_endit) {
273 auto const out = Geom::LineSegment{curve_it2->initialPoint(), curve_it2->finalPoint()};
274 cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it2);
275 if (cubic) {
276 if (are_near((*cubic)[1], (*cubic)[0]) && !are_near((*cubic)[2], (*cubic)[3])) {
277 next_point_at1 = in.pointAt(DEFAULT_START_POWER);
278 } else {
279 next_point_at1 = out.pointAt(Geom::nearest_time((*cubic)[1], out));
280 }
281 } else {
282 next_point_at1 = out.initialPoint();
283 }
284 }
285 if (path_it.closed() && curve_it2 == curve_endit) {
286 auto const start = Geom::LineSegment{path_it.begin()->initialPoint(), path_it.begin()->finalPoint()};
287 Geom::LineSegment line_helper;
288 cubic = dynamic_cast<Geom::CubicBezier const *>(&*path_it.begin());
289 if (cubic) {
290 line_helper.setInitial(start.pointAt(Geom::nearest_time((*cubic)[1], start)));
291 } else {
292 line_helper.setInitial(start.initialPoint());
293 }
294
295 auto const end = Geom::LineSegment{curve_it1->initialPoint(), curve_it1->finalPoint()};
296 cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1);
297 if (cubic) {
298 line_helper.setFinal(end.pointAt(Geom::nearest_time((*cubic)[2], end)));
299 } else {
300 line_helper.setFinal(end.finalPoint());
301 }
302 node = line_helper.pointAt(0.5);
303 curve_n.appendNew<Geom::CubicBezier>(point_at1, point_at2, node);
304 move_endpoints(curve_n, node, node);
305 } else if (curve_it2 == curve_endit) {
306 if (uniform) {
307 curve_n.appendNew<Geom::CubicBezier>(point_at1, curve_it1->finalPoint(), curve_it1->finalPoint());
308 } else {
309 curve_n.appendNew<Geom::CubicBezier>(point_at1, point_at2, curve_it1->finalPoint());
310 }
311 move_endpoints(curve_n, path_it.begin()->initialPoint(), curve_it1->finalPoint());
312 } else {
313 auto const line = Geom::LineSegment{point_at2, next_point_at1};
314 previousNode = node;
315 node = line.pointAt(0.5);
316 auto cubic2 = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1);
317 if ((cubic && are_near((*cubic)[0], (*cubic)[1])) || (cubic2 && are_near((*cubic2)[2], (*cubic2)[3]))) {
318 node = curve_it1->finalPoint();
319 }
320 curve_n.appendNew<Geom::CubicBezier>(point_at1, point_at2, node);
321 }
322 if(!are_near(node,curve_it1->finalPoint()) && helper_size > 0.0) {
323 hp.push_back(sp_bspline_drawHandle(node, helper_size));
324 }
325 ++curve_it1;
326 ++curve_it2;
327 }
328 if (path_it.closed()) {
329 closepath_current(curve_n);
330 }
331 curve.push_back(std::move(curve_n));
332 }
333 if (helper_size > 0.0) {
334 hp.push_back(curve.front());
335 }
336}
337
339{
340 char const * svgd = "M 1,0.5 A 0.5,0.5 0 0 1 0.5,1 0.5,0.5 0 0 1 0,0.5 0.5,0.5 0 0 1 0.5,0 0.5,0.5 0 0 1 1,0.5 Z";
343 aff *= Geom::Scale(helper_size);
344 pathv *= aff;
345 pathv *= Geom::Translate(p - Geom::Point(0.5*helper_size, 0.5*helper_size));
346 return pathv[0];
347}
348
350{
351 using Geom::X;
352 using Geom::Y;
353
354 if (curve.curveCount() < 1)
355 return;
356 // Make copy of old path as it is changed during processing
357 Geom::PathVector const original_pathv = curve;
358 curve.clear();
359
360 for (const auto & path_it : original_pathv) {
361 if (path_it.empty()) {
362 continue;
363 }
364 Geom::Path::const_iterator curve_it1 = path_it.begin();
365 Geom::Path::const_iterator curve_it2 = ++(path_it.begin());
366 Geom::Path::const_iterator curve_endit = path_it.end_default();
367
368 Geom::Point point_at0(0, 0);
369 Geom::Point point_at1(0, 0);
370 Geom::Point point_at2(0, 0);
371 Geom::Point point_at3(0, 0);
372 Geom::D2<Geom::SBasis> sbasis_out;
373 Geom::CubicBezier const *cubic = nullptr;
374 auto curve_n = Geom::Path{curve_it1->initialPoint()};
375 if (path_it.closed()) {
376 const Geom::Curve &closingline = path_it.back_closed();
377 // the closing line segment is always of type
378 // Geom::LineSegment.
379 if (are_near(closingline.initialPoint(), closingline.finalPoint())) {
380 // closingline.isDegenerate() did not work, because it only checks for
381 // *exact* zero length, which goes wrong for relative coordinates and
382 // rounding errors...
383 // the closing line segment has zero-length. So stop before that one!
384 curve_endit = path_it.end_open();
385 }
386 }
387 while (curve_it1 != curve_endit) {
388 auto const in = Geom::LineSegment{curve_it1->initialPoint(), curve_it1->finalPoint()};
389 cubic = dynamic_cast<Geom::CubicBezier const *>(&*curve_it1);
390 point_at0 = in.initialPoint();
391 point_at3 = in.finalPoint();
392 auto const sbasis_in = in.toSBasis();
393 if (cubic) {
395 (apply_no_weight && Geom::are_near((*cubic)[1], point_at0)) ||
396 (apply_with_weight && !Geom::are_near((*cubic)[1], point_at0)))
397 {
398 if (isNodePointSelected(point_at0) || !only_selected) {
399 point_at1 = sbasis_in.valueAt(weight_amount);
400 } else {
401 point_at1 = (*cubic)[1];
402 }
403 } else {
404 point_at1 = (*cubic)[1];
405 }
407 (apply_no_weight && Geom::are_near((*cubic)[2], point_at3)) ||
408 (apply_with_weight && !Geom::are_near((*cubic)[2], point_at3)))
409 {
410 if (isNodePointSelected(point_at3) || !only_selected) {
411 point_at2 = in.pointAt(1 - weight_amount);
412 if (!Geom::are_near(weight_amount, NO_POWER, BSPLINE_TOL)) {
413 point_at2 =
414 Geom::Point(point_at2[X], point_at2[Y]);
415 }
416 } else {
417 point_at2 = (*cubic)[2];
418 }
419 } else {
420 point_at2 = (*cubic)[2];
421 }
422 } else {
424 (apply_no_weight && Geom::are_near(weight_amount, NO_POWER, BSPLINE_TOL)) ||
426 {
427 if (isNodePointSelected(point_at0) || !only_selected) {
428 point_at1 = in.pointAt(weight_amount);
429 } else {
430 point_at1 = in.initialPoint();
431 }
432 if (isNodePointSelected(point_at3) || !only_selected) {
433 point_at2 = sbasis_in.valueAt(1 - weight_amount);
434 } else {
435 point_at2 = in.finalPoint();
436 }
437 } else {
438 point_at1 = in.initialPoint();
439 point_at2 = in.finalPoint();
440 }
441 }
442 curve_n.appendNew<Geom::CubicBezier>(point_at1, point_at2, point_at3);
443 ++curve_it1;
444 ++curve_it2;
445 }
446 if (path_it.closed()) {
447 move_endpoints(curve_n,
448 path_it.initialPoint(),
449 path_it.initialPoint());
450 } else {
451 move_endpoints(curve_n,
452 path_it.initialPoint(),
453 point_at3);
454 }
455 if (path_it.closed()) {
456 closepath_current(curve_n);
457 }
458 curve.push_back(std::move(curve_n));
459 }
460}
461
462} // namespace Inkscape::LivePathEffect
463
464/*
465 Local Variables:
466 mode:c++
467 c-file-style:"stroustrup"
468 c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
469 indent-tabs-mode:nil
470 fill-column:99
471 End:
472*/
473// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4 :
3x3 matrix representing an affine transformation.
Definition affine.h:70
Bezier curve with compile-time specified order.
void setInitial(Point const &v) override
Change the starting point of the curve.
Point pointAt(Coord t) const override
Evaluate the curve at a specified time value.
Point initialPoint() const override
Retrieve the start of the curve.
void setFinal(Point const &v) override
Change the ending point of the curve.
Abstract continuous curve on a plane defined on [0,1].
Definition curve.h:78
virtual Point initialPoint() const =0
Retrieve the start of the curve.
virtual Point finalPoint() const =0
Retrieve the end of the curve.
Adaptor that creates 2D functions from 1D ones.
Definition d2.h:55
Sequence of subpaths.
Definition pathvector.h:122
void push_back(Path const &path)
Append a path at the end.
Definition pathvector.h:172
void clear()
Remove all paths from the vector.
Definition pathvector.h:195
bool empty() const
Check whether the vector contains any paths.
Definition pathvector.h:145
Sequence of contiguous curves, aka spline.
Definition path.h:353
Curve const & back_open() const
Definition path.h:448
void append(Curve *curve)
Add a new curve to the end of the path.
Definition path.h:750
Point initialPoint() const
Get the first point in the path.
Definition path.h:705
void setFinal(Point const &p)
Definition path.h:740
void appendNew(Args &&... args)
Append a new curve to the path.
Definition path.h:804
Two-dimensional point that doubles as a vector.
Definition point.h:66
Scaling from the origin.
Definition transforms.h:150
Translation by a vector.
Definition transforms.h:115
std::vector< Parameter * > param_vector
Definition effect.h:178
void registerParameter(Parameter *param)
Definition effect.cpp:1704
bool isNodePointSelected(Geom::Point const &nodePoint) const
Definition effect.cpp:1258
void makeUndoDone(Glib::ustring message)
Definition effect.cpp:1521
void param_setValue(Glib::ustring newvalue, bool write=false)
Definition hidden.cpp:71
void doBeforeEffect(SPLPEItem const *lpeitem) override
Is performed each time before the effect is updated.
void doEffect(Geom::PathVector &curve) override
void changeWeight(double weightValue)
Gtk::Widget * newWidget() override
This creates a managed widget.
void doOnApply(SPLPEItem const *lpeitem) override
Is performed a single time when the effect is freshly applied to a path.
void addCanvasIndicators(SPLPEItem const *, std::vector< Geom::PathVector > &hp_vec) override
Add possible canvas indicators (i.e., helperpaths other than the original path) to hp_vec This functi...
void doBSplineFromWidget(Geom::PathVector &curve, double value)
LPEBSpline(LivePathEffectObject *lpeobject)
void param_set_digits(unsigned digits)
void param_set_range(double min, double max)
void param_set_increments(double step, double page)
Preference storage class.
Definition preferences.h:61
bool getBool(Glib::ustring const &pref_path, bool def=false)
Retrieve a Boolean value.
static Preferences * get()
Access the singleton Preferences object.
A labelled text box, with spin buttons and optional icon, for entering arbitrary number values.
Definition scalar.h:34
Glib::SignalProxy< void()> signal_value_changed()
Signal raised when the spin button's value changes.
Definition scalar.cpp:159
void move_endpoints(Geom::PathVector &pathv, Geom::Point const &new_p0, Geom::Point const &new_p1)
Sets start of first path to new_p0, and end of first path to new_p1.
Definition curve.cpp:188
void closepath_current(Geom::Path &path)
Close path by setting the end point to the start point instead of adding a new lineto.
Definition curve.cpp:37
@ Y
Definition coord.h:48
@ X
Definition coord.h:48
SPItem * item
Inkscape::XML::Node * node
Geom::Point start
Geom::Point end
Coord nearest_time(Point const &p, Curve const &c)
Definition curve.h:354
bool are_near(Affine const &a1, Affine const &a2, Coord eps=EPSILON)
Live Path Effects code.
void sp_bspline_do_effect(Geom::PathVector &curve, double helper_size, Geom::PathVector &hp, bool uniform)
static constexpr double NO_POWER
Geom::Path sp_bspline_drawHandle(Geom::Point p, double helper_size)
static constexpr double DEFAULT_END_POWER
static constexpr double DEFAULT_START_POWER
static constexpr double BSPLINE_TOL
void pack_start(Gtk::Box &box, Gtk::Widget &child, bool const expand, bool const fill, unsigned const padding)
Adds child to box, packed with reference to the start of box.
Definition pack.cpp:141
Helpers for using Gtk::Boxes, encapsulating large changes between GTK3 & GTK4.
Singleton class to access the preferences file in a convenient way.
unsigned long weight
Definition quantize.cpp:37
void sp_lpe_item_update_patheffect(SPLPEItem *lpeitem, bool wholetree, bool write, bool with_satellites)
Calls any registered handlers for the update_patheffect action.
Definition curve.h:24
Geom::PathVector sp_svg_read_pathv(char const *str)
Definition svg-path.cpp:37
static void sp_svg_write_path(Inkscape::SVG::PathString &str, Geom::Path const &p, bool normalize=false)
Definition svg-path.cpp:109
double uniform()