Merge lp:~danieljabailey/inkscape/arc_node_editor into lp:~inkscape.dev/inkscape/trunk

Proposed by Daniel
Status: Needs review
Proposed branch: lp:~danieljabailey/inkscape/arc_node_editor
Merge into: lp:~inkscape.dev/inkscape/trunk
Diff against target: 1019 lines (+555/-41)
13 files modified
share/icons/icons.svg (+23/-0)
src/helper/geom.cpp (+42/-0)
src/helper/geom.h (+1/-0)
src/ui/tool/control-point.cpp (+0/-1)
src/ui/tool/multi-path-manipulator.cpp (+32/-5)
src/ui/tool/multi-path-manipulator.h (+3/-0)
src/ui/tool/node-types.h (+2/-1)
src/ui/tool/node.cpp (+162/-14)
src/ui/tool/node.h (+26/-2)
src/ui/tool/path-manipulator.cpp (+182/-18)
src/ui/tool/path-manipulator.h (+5/-0)
src/widgets/node-toolbar.cpp (+72/-0)
src/widgets/toolbox.cpp (+5/-0)
To merge this branch: bzr merge lp:~danieljabailey/inkscape/arc_node_editor
Reviewer Review Type Date Requested Status
Inkscape Developers Pending
Review via email: mp+294742@code.launchpad.net

Description of the change

Hi, This is my first time contributing to inkscape and also my first time using bazaar.

I noticed that inkscape converts arc segments in paths into bezier curves when you edit them using the node editor. I had an SVG file that contained some arcs that I wanted to manipulate and so decided to add this feature to avoid having the arcs changed to beziers.

Arc segments are manipulated with a pair of perpendicular handles that pivot around the mid point between two nodes. (This is not always the arc's mid-point) They control the rotation and the radii of the elipse upon which the arc sits. The two nodes define two arc end points and two flags control the direction and 'largeness'. The two flags can be controlled with buttons in the toolbar.

This feature required a change to the handle class so that the origin of a handle could be somewhere other than its parent node. This has been done by adding an 'offset' relative to the parent position. The length of the handle is measured from the offset point to the handle point, not from the parent. In most cases however, the offset is zero and the length, as before is from the parent node to the handle point.

This change adds four buttons to the node editor toolbar. I have created icons for these using the new arc editor tools. :-)
The four buttons are: "Make selected segments arcs", "Make selected arc segments shallow", "Make selected arc segments bulge" and "Flip selected arc segments"

I have done my best to stick to the style guides, however there was some code that I copied and pasted from something that did not match the style guide particularly well, I was unsure if I should clean up the old code or the new code and so left them both as they were (see geom.cpp function pathv_to_linear_and_cubic_beziers_and_arcs).

Please let me know if this is a valuable change that can be merged.
Also let me know if I need to fix anything.

I am aware of one slight bug, but wanted to check if there was interest in this change before I put effort into fixing it.
When an arc segment is dragged (as you would to manipulate a bezier segment) the bezier handles appear and so both the arc handles and bezier handles are visible.
Let me know if there is interest in this feature and I will start hunting this bug.

Thanks,
Dan.

To post a comment you must log in.
14891. By Daniel Bailey <email address hidden>

Handle conversion from arcs to beziers in node editor

14892. By Daniel Bailey <email address hidden>

fix bug where closing arc segment sometimes becomes as straight line

14893. By Daniel Bailey <email address hidden>

Merge upstream changes

Revision history for this message
Jabiertxof (jabiertxof) wrote :

Added some comments about coding style, also start compiling to see the UX and give a better review.
Great work!

Revision history for this message
Jabiertxof (jabiertxof) wrote :

When compiling wirh CMAKE I get this error:
[ 81%] Building CXX object src/CMakeFiles/inkscape_base.dir/version.cpp.o
make[2]: *** No rule to make target 'src/inkscape-version.cpp', needed by 'src/CMakeFiles/inkscape_base.dir/inkscape-version.cpp.o'. Stop.
CMakeFiles/Makefile2:597: recipe for target 'src/CMakeFiles/inkscape_base.dir/all' failed
make[1]: *** [src/CMakeFiles/inkscape_base.dir/all] Error 2
Makefile:127: recipe for target 'all' failed
make: *** [all] Error 2

Unmerged revisions

14893. By Daniel Bailey <email address hidden>

Merge upstream changes

14892. By Daniel Bailey <email address hidden>

fix bug where closing arc segment sometimes becomes as straight line

14891. By Daniel Bailey <email address hidden>

Handle conversion from arcs to beziers in node editor

14890. By Daniel Bailey <email address hidden>

fix TODO for arc segment handling

14889. By Daniel Bailey <email address hidden>

Added support for editting arc segments in the node editor

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'share/icons/icons.svg'
2--- share/icons/icons.svg 2016-04-14 19:09:11 +0000
3+++ share/icons/icons.svg 2016-07-09 15:52:46 +0000
4@@ -1109,6 +1109,29 @@
5 <use xlink:href="#rect4374" height="1250" width="1250" id="use5756" y="0" x="0" transform="translate(-5.018707,-0.0241656)" />
6 <use xlink:href="#rect4374" height="1250" width="1250" id="use5758" y="0" x="0" transform="translate(6.0238,-11.02417)" />
7 </g>
8+<g id="node-segment-elliptical-arc" transform="translate(673.00134,25.751197)" inkscape:label="#node_curve">
9+<path style="fill:none;stroke:#646464;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" d="m -38.600913,188.52882 a 6.0591792,7.0458508 48.505394 1 1 7.123238,-5.61527" id="path10679-5" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
10+<rect y="175" x="-45" height="16" width="16" id="rect10681-3" style="color:#000000;display:inline;fill:none;stroke:none;stroke-width:1;marker:none" />
11+<use xlink:href="#rect4374" height="1250" width="1250" id="use5745-8" y="0" x="0" transform="translate(-1.0764281,-0.01573529)" />
12+<use xlink:href="#rect4374" height="1250" width="1250" id="use5747-9" y="0" x="0" transform="translate(5.9949619,-5.5106093)" />
13+</g>
14+<g id="node-segment-elliptical-arc-bulge" transform="translate(693.93498,25.748197)" inkscape:label="#node_curve">
15+<rect y="175" x="-45" height="16" width="16" id="rect10681-3-6" style="color:#000000;display:inline;fill:none;stroke:none;stroke-width:1;marker:none" />
16+<path style="fill:none;stroke:#646464;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" d="m -37.870441,189.45456 a 6.0591792,7.0458508 48.505394 1 1 7.123238,-5.61527" id="path10679-5-2" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
17+<path style="fill:none;stroke:#ff8080;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:1, 2;stroke-dashoffset:0;stroke-opacity:1" d="m -37.870438,189.45456 a 6.0591792,7.0458508 48.505394 0 1 7.12324,-5.61527" id="path10679-5-2-8" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
18+</g>
19+<g id="node-segment-elliptical-arc-flip" transform="translate(736.50086,25.748197)" inkscape:label="#node_curve">
20+<rect y="175" x="-45" height="16" width="16" id="rect10681-3-67" style="color:#000000;display:inline;fill:none;stroke:none;stroke-width:1;marker:none" />
21+<path style="fill:none;stroke:#646464;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" d="m -40.865556,188.5577 a 5.0424426,7.9529262 56.470438 1 1 7.281336,-12.07083" id="path10679-5-5" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
22+<path style="fill:none;stroke:#ff5555;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" d="m -40.866246,188.55761 a 5.0424427,7.9529265 56.470438 1 0 7.28134,-12.07083" id="path10679-5-5-6" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
23+<use xlink:href="#path4376" height="1250" width="1250" id="use5754-7" y="0" x="0" transform="rotate(90,-38.492674,181.50732)" />
24+<use xlink:href="#path4376" height="1250" width="1250" id="use5754-7-9" y="0" x="0" transform="matrix(0,1,1,0,-217.00767,220)" />
25+</g>
26+<g id="node-segment-elliptical-arc-shallow" transform="translate(714.9873,25.748197)" inkscape:label="#node_curve">
27+<rect y="175" x="-45" height="16" width="16" id="rect10681-3-6-6" style="color:#000000;display:inline;fill:none;stroke:none;stroke-width:1;marker:none" />
28+<path style="fill:none;stroke:#646464;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:none;stroke-opacity:1" d="m -37.870441,189.45456 a 6.0591792,7.0458508 48.505394 0 1 7.123238,-5.61527" id="path10679-5-2-3" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
29+<path style="fill:none;stroke:#ff8080;stroke-width:1.00000024;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0;stroke-dasharray:1.00000024, 2.00000048;stroke-dashoffset:0;stroke-opacity:1" d="m -37.870438,189.45456 a 6.0591792,7.0458508 48.505394 1 1 7.12324,-5.61527" id="path10679-5-2-8-2" sodipodi:nodetypes="cc" inkscape:connector-curvature="0" />
30+</g>
31 <g id="object-to-path" transform="translate(280.041,-149.9465)" inkscape:label="#object_tocurve">
32 <path style="fill:none;stroke:#646464;stroke-width:1.0000002;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:0" d="M -37.0131,188.9855 C -35.0131,192.9855 -27.81159,188.3079 -29.81159,185.3079 -31.10674,183.3652 -39.24159,185.6729 -42.56979,183.4541 -45.56979,181.4541 -45.12074,174.9746 -40.0534,175.5422 -35.41611,176.0661 -38.62555,185.7606 -37.0131,188.9855 Z" id="path4438" sodipodi:nodetypes="cssss" inkscape:connector-curvature="0" />
33 <rect y="175" x="-45" height="16" width="16" id="rect4440" style="color:#000000;fill:none" />
34
35=== modified file 'src/helper/geom.cpp'
36--- src/helper/geom.cpp 2015-05-08 17:26:29 +0000
37+++ src/helper/geom.cpp 2016-07-09 15:52:46 +0000
38@@ -456,6 +456,48 @@
39
40 //#################################################################################
41
42+/**
43+ * Converts all segments in all paths to Geom::LineSegment or Geom::HLineSegment or
44+ * Geom::VLineSegment or Geom::CubicBezier or Geom::EllipticalArc.
45+ */
46+Geom::PathVector
47+pathv_to_linear_and_cubic_beziers_and_arcs( Geom::PathVector const &pathv )
48+{
49+ Geom::PathVector output;
50+
51+ for (Geom::PathVector::const_iterator pit = pathv.begin(); pit != pathv.end(); ++pit) {
52+ output.push_back( Geom::Path() );
53+ output.back().setStitching(true);
54+ output.back().start( pit->initialPoint() );
55+
56+ for (Geom::Path::const_iterator cit = pit->begin(); cit != pit->end_open(); ++cit) {
57+ if (is_straight_curve(*cit)) {
58+ Geom::LineSegment l(cit->initialPoint(), cit->finalPoint());
59+ output.back().append(l);
60+ } else {
61+ Geom::EllipticalArc const *arc = dynamic_cast<Geom::EllipticalArc const *>(&*cit);
62+ if (arc){
63+ output.back().append(arc->duplicate());
64+ }
65+ else{
66+ Geom::BezierCurve const *curve = dynamic_cast<Geom::BezierCurve const *>(&*cit);
67+ if (curve && curve->order() == 3) {
68+ Geom::CubicBezier b((*curve)[0], (*curve)[1], (*curve)[2], (*curve)[3]);
69+ output.back().append(b);
70+ } else {
71+ // convert all other curve types to cubicbeziers
72+ Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(cit->toSBasis(), 0.1);
73+ cubicbezier_path.close(false);
74+ output.back().append(cubicbezier_path);
75+ }
76+ }
77+ }
78+ }
79+ output.back().close( pit->closed() );
80+ }
81+ return output;
82+}
83+
84 /*
85 * Converts all segments in all paths to Geom::LineSegment or Geom::HLineSegment or
86 * Geom::VLineSegment or Geom::CubicBezier.
87
88=== modified file 'src/helper/geom.h'
89--- src/helper/geom.h 2015-05-08 15:40:38 +0000
90+++ src/helper/geom.h 2016-07-09 15:52:46 +0000
91@@ -25,6 +25,7 @@
92 Geom::Rect *bbox, int *wind, Geom::Coord *dist,
93 Geom::Coord tolerance, Geom::Rect const *viewbox);
94
95+Geom::PathVector pathv_to_linear_and_cubic_beziers_and_arcs( Geom::PathVector const &pathv );
96 Geom::PathVector pathv_to_linear_and_cubic_beziers( Geom::PathVector const &pathv );
97 Geom::PathVector pathv_to_linear( Geom::PathVector const &pathv, double maxdisp );
98 Geom::PathVector pathv_to_cubicbezier( Geom::PathVector const &pathv);
99
100=== modified file 'src/ui/tool/control-point.cpp'
101--- src/ui/tool/control-point.cpp 2015-05-30 18:27:42 +0000
102+++ src/ui/tool/control-point.cpp 2016-07-09 15:52:46 +0000
103@@ -225,7 +225,6 @@
104 {
105 // NOTE the static variables below are shared for all points!
106 // TODO handle clicks and drags from other buttons too
107-
108 if (event == NULL)
109 {
110 return false;
111
112=== modified file 'src/ui/tool/multi-path-manipulator.cpp'
113--- src/ui/tool/multi-path-manipulator.cpp 2015-08-08 20:19:02 +0000
114+++ src/ui/tool/multi-path-manipulator.cpp 2016-07-09 15:52:46 +0000
115@@ -319,11 +319,38 @@
116 {
117 if (_selection.empty()) return;
118 invokeForAll(&PathManipulator::setSegmentType, type);
119- if (type == SEGMENT_STRAIGHT) {
120- _done(_("Straighten segments"));
121- } else {
122- _done(_("Make segments curves"));
123- }
124+ switch (type){
125+ case SEGMENT_STRAIGHT:
126+ _done(_("Straighten segments"));
127+ break;
128+ case SEGMENT_CUBIC_BEZIER:
129+ _done(_("Make segments curves"));
130+ break;
131+ case SEGMENT_ELIPTICAL_ARC:
132+ _done(_("Make segments arcs"));
133+ break;
134+ default:
135+ g_error("Unknown segment type");
136+ }
137+}
138+
139+void MultiPathManipulator::setArcSegmentLarge(bool large)
140+{
141+ if (_selection.empty()) return;
142+ invokeForAll(&PathManipulator::setArcSegmentLarge, large);
143+ if (large){
144+ _done(_("Make arc segments bulge"));
145+ }
146+ else{
147+ _done(_("Make arc segments shallow"));
148+ }
149+}
150+
151+void MultiPathManipulator::toggleArcSegmentSweep()
152+{
153+ if (_selection.empty()) return;
154+ invokeForAll(&PathManipulator::toggleArcSegmentSweep);
155+ _done(_("Flip arc segments"));
156 }
157
158 void MultiPathManipulator::insertNodes()
159
160=== modified file 'src/ui/tool/multi-path-manipulator.h'
161--- src/ui/tool/multi-path-manipulator.h 2015-05-30 18:27:42 +0000
162+++ src/ui/tool/multi-path-manipulator.h 2016-07-09 15:52:46 +0000
163@@ -51,6 +51,9 @@
164 void setNodeType(NodeType t);
165 void setSegmentType(SegmentType t);
166
167+ void setArcSegmentLarge(bool large);
168+ void toggleArcSegmentSweep();
169+
170 void insertNodesAtExtrema(ExtremumType extremum);
171 void insertNodes();
172 void insertNode(Geom::Point pt);
173
174=== modified file 'src/ui/tool/node-types.h'
175--- src/ui/tool/node-types.h 2010-11-17 02:12:56 +0000
176+++ src/ui/tool/node-types.h 2016-07-09 15:52:46 +0000
177@@ -28,7 +28,8 @@
178 /** Types of segments supported in the node tool. */
179 enum SegmentType {
180 SEGMENT_STRAIGHT, ///< Straight linear segment
181- SEGMENT_CUBIC_BEZIER ///< Bezier curve with two control points
182+ SEGMENT_CUBIC_BEZIER, ///< Bezier curve with two control points
183+ SEGMENT_ELIPTICAL_ARC ///< Eliptical arc (two radii, rotation, size flag, direction flag)
184 };
185
186 } // namespace UI
187
188=== modified file 'src/ui/tool/node.cpp'
189--- src/ui/tool/node.cpp 2016-05-29 10:13:55 +0000
190+++ src/ui/tool/node.cpp 2016-07-09 15:52:46 +0000
191@@ -112,7 +112,8 @@
192 _handle_colors, data.handle_group),
193 _parent(parent),
194 _handle_line(ControlManager::getManager().createControlLine(data.handle_line_group)),
195- _degenerate(true)
196+ _degenerate(true),
197+ _offset(Geom::Point())// Start with 0 offset
198 {
199 setVisible(false);
200 }
201@@ -184,8 +185,8 @@
202
203 if (_parent->type() == NODE_SMOOTH && Node::_is_line_segment(_parent, node_away)) {
204 // restrict movement to the line joining the nodes
205- Geom::Point direction = _parent->position() - node_away->position();
206- Geom::Point delta = new_pos - _parent->position();
207+ Geom::Point direction = _parent->position() + _offset - node_away->position();
208+ Geom::Point delta = new_pos - _parent->position() + _offset;
209 // project the relative position on the direction line
210 Geom::Point new_delta = (Geom::dot(delta, direction)
211 / Geom::L2sq(direction)) * direction;
212@@ -212,7 +213,7 @@
213 } break;
214 case NODE_SYMMETRIC:
215 // for symmetric nodes, place the other handle on the opposite side
216- other->setRelativePos(-(new_pos - _parent->position()));
217+ other->setRelativePos(-(new_pos - _parent->position() + _offset));
218 break;
219 default: break;
220 }
221@@ -229,10 +230,10 @@
222 void Handle::setPosition(Geom::Point const &p)
223 {
224 ControlPoint::setPosition(p);
225- _handle_line->setCoords(_parent->position(), position());
226+ _handle_line->setCoords(_parent->position() + _offset, position());
227
228 // update degeneration info and visibility
229- if (Geom::are_near(position(), _parent->position()))
230+ if (Geom::are_near(position(), _parent->position() + _offset))
231 _degenerate = true;
232 else _degenerate = false;
233
234@@ -243,6 +244,15 @@
235 }
236 }
237
238+/** Set the offset between the parent node and the origin */
239+void Handle::setOffset(Geom::Point const &offset){
240+ _offset = offset;
241+ _handle_line->setCoords(_parent->position() + _offset, position());
242+}
243+
244+/** Set the length of the handle, if the handle is degenerate then
245+ the function returns without changing the handle position as
246+ it cannot determine the desired direction of the handle */
247 void Handle::setLength(double len)
248 {
249 if (isDegenerate()) return;
250@@ -250,9 +260,30 @@
251 setRelativePos(dir * len);
252 }
253
254+/** Set the angle of the handle, if the handle is degenerate then
255+ the function returns without changing the handle position as
256+ it is not possible to set the orientation of a zero length line */
257+void Handle::setAngle(Geom::Angle a){
258+ if (isDegenerate()) return;
259+ setRelativePos(Geom::Point::polar(a, length()));
260+}
261+
262+/** Set the angle and the length of the handle by moving the handle position
263+ unlike Handle::setLength and Handle::setAngle, this function works for
264+ degenerate handles as both the desired length and angle are known*/
265+void Handle::setAngleAndLength(Geom::Angle a, double len){
266+ // Degenerate case can be handled by setting the handle relative position
267+ // to be any vector of non-zero length. A unit vector is used here.
268+ if (isDegenerate()) {
269+ setRelativePos(Geom::Point(0,1));
270+ }
271+ setAngle(a);
272+ setLength(len);
273+}
274+
275 void Handle::retract()
276 {
277- move(_parent->position());
278+ move(_parent->position() + _offset);
279 }
280
281 void Handle::setDirection(Geom::Point const &from, Geom::Point const &to)
282@@ -428,6 +459,7 @@
283 new_pos=_last_drag_origin();
284 }
285 move(new_pos); // needed for correct update, even though it's redundant
286+ _parent->updateArcHandleConstriants(this);
287 _pm().update();
288 }
289
290@@ -585,6 +617,10 @@
291 node_colors, data.node_group),
292 _front(data, initial_pos, this),
293 _back(data, initial_pos, this),
294+ _arc_rx(data, initial_pos, this),
295+ _arc_ry(data, initial_pos, this),
296+ _arc_large(true),
297+ _arc_sweep(false),
298 _type(NODE_CUSP),
299 _handles_shown(false)
300 {
301@@ -622,6 +658,52 @@
302 }
303 }
304
305+/** Update constrained arc handles given a new constraint.
306+ This is used to keep the arc radius handles at right angles
307+ @param constraint Pointer to the handle that was forced to a new position.
308+ If the constraint is anything other than _arc_rx or _arc_ry then this function does nothing */
309+void Node::updateArcHandleConstriants(Handle *constraint)
310+{
311+ Handle *updateTarget = NULL;
312+ Geom::Angle rotation = 0.0;
313+ // Check which handle moved and set the update target and rotation
314+ if (constraint == &_arc_rx){ updateTarget = &_arc_ry; rotation = Geom::rad_from_deg( 90.0); }
315+ else if (constraint == &_arc_ry){ updateTarget = &_arc_rx; rotation = Geom::rad_from_deg(-90.0); }
316+ else return;
317+ // Calculate the new position and move the constrained handle (updateTarget)
318+ Geom::Angle newAngle = constraint->angle() + rotation;
319+ Geom::Coord newLength = updateTarget->length();
320+ Geom::Point newPos = Geom::Point::polar(newAngle, newLength);
321+ updateTarget->setRelativePos(newPos);
322+}
323+
324+/** Move the arc handles so that their origin is half way along the line between the nodes */
325+void Node::moveArcHandles(Geom::Point lineBetweenNodes, double lenX, double lenY, Geom::Angle rotX, Geom::Angle rotY)
326+{
327+ // Calculate the mid point between the two nodes, put the origin of the handles there.
328+ // This usually changes the length and angle of the handle lines
329+ Geom::Point midPointOffset = lineBetweenNodes / 2.0;
330+ _arc_rx.setOffset(midPointOffset);
331+ _arc_ry.setOffset(midPointOffset);
332+
333+ _arc_rx.setPosition(position());
334+ _arc_ry.setPosition(position());
335+
336+ // Apply the stored angle and length
337+ _arc_rx.setAngleAndLength(rotX, lenX);
338+ _arc_ry.setAngleAndLength(rotY, lenY);
339+
340+}
341+
342+/** Retract this node's handles for controling arcs */
343+void Node::retractArcHandles()
344+{
345+ _arc_rx.setOffset(Geom::Point());
346+ _arc_ry.setOffset(Geom::Point());
347+ _arc_rx.retract();
348+ _arc_ry.retract();
349+}
350+
351 void Node::move(Geom::Point const &new_pos)
352 {
353 // move handles when the node moves.
354@@ -643,11 +725,47 @@
355 nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
356 }
357
358+ // Save the lengths and angles of the two arc handles to apply again after the node is moved.
359+ double lenX, lenY;
360+ Geom::Angle rotX, rotY;
361+ lenX = _arc_rx.length(); lenY = _arc_ry.length();
362+ rotX = _arc_rx.angle(); rotY = _arc_ry.angle();
363+
364 setPosition(new_pos);
365
366+ // _front and _back are translated with the node to which they belong.
367+ // This means that if they were degenrate then they are still degenerate.
368 _front.setPosition(_front.position() + delta);
369 _back.setPosition(_back.position() + delta);
370
371+ // For arc manipulation handles, the position relative to their parent nodes
372+ // may change as the node moves. For this reason, the degenerate case must be
373+ // handled as a special case.
374+ if (nextNode) { // Must have a next node for this to be an arc segment
375+ if (_arc_rx.isDegenerate() || _arc_ry.isDegenerate()){
376+ retractArcHandles();
377+ }
378+ else{
379+ moveArcHandles(nextNode->position() - position(), lenX, lenY, rotX, rotY);
380+ }
381+ }
382+ // If this is the end of an arc segment then the arc data for prevNode needs updating
383+ if (prevNode){
384+ if (prevNode->arc_rx()->isDegenerate() || prevNode->arc_ry()->isDegenerate()){
385+ prevNode->retractArcHandles();
386+ }
387+ else{
388+ // In this case, the parent node for the arc handles hasn't moved,
389+ // for this reason, we didn't need to save the length and rotation earlier,
390+ // we can get the length and rotation now.
391+ lenX = prevNode->arc_rx()->length();
392+ lenY = prevNode->arc_ry()->length();
393+ rotX = prevNode->arc_rx()->angle();
394+ rotY = prevNode->arc_ry()->angle();
395+ prevNode->moveArcHandles(n->position() - prevNode->position(), lenX, lenY, rotX, rotY);
396+ }
397+ }
398+
399 // if the node has a smooth handle after a line segment, it should be kept colinear
400 // with the segment
401 _fixNeighbors(old_pos, new_pos);
402@@ -667,7 +785,6 @@
403
404 void Node::transform(Geom::Affine const &m)
405 {
406-
407 Geom::Point old_pos = position();
408
409 // save the previous nodes strength to apply it again once the node is moved
410@@ -685,10 +802,26 @@
411 nextNodeWeight = _pm()._bsplineHandlePosition(nextNode->back());
412 }
413
414+ // Save the lengths and angles of the two arc handles to apply again after the node is moved.
415+ double lenX, lenY;
416+ Geom::Angle rotX, rotY;
417+ lenX = _arc_rx.length(); lenY = _arc_ry.length();
418+ rotX = _arc_rx.angle(); rotY = _arc_ry.angle();
419+
420 setPosition(position() * m);
421 _front.setPosition(_front.position() * m);
422 _back.setPosition(_back.position() * m);
423
424+
425+ if (nextNode){
426+ if (_arc_rx.isDegenerate() || _arc_ry.isDegenerate()){
427+ retractArcHandles();
428+ }
429+ else{
430+ moveArcHandles(nextNode->position() - position(), lenX, lenY, rotX, rotY);
431+ }
432+ }
433+
434 /* Affine transforms keep handle invariants for smooth and symmetric nodes,
435 * but smooth nodes at ends of linear segments and auto nodes need special treatment */
436 _fixNeighbors(old_pos, position());
437@@ -789,7 +922,12 @@
438 if (!_back.isDegenerate()) {
439 _back.setVisible(v);
440 }
441-
442+ if (!_arc_rx.isDegenerate()) {
443+ _arc_rx.setVisible(v);
444+ }
445+ if (!_arc_ry.isDegenerate()) {
446+ _arc_ry.setVisible(v);
447+ }
448 }
449
450 void Node::updateHandles()
451@@ -798,6 +936,8 @@
452
453 _front._handleControlStyling();
454 _back._handleControlStyling();
455+ _arc_rx._handleControlStyling();
456+ _arc_ry._handleControlStyling();
457 }
458
459
460@@ -1332,7 +1472,6 @@
461 }
462
463 sm.unSetup();
464-
465 SelectableControlPoint::dragged(new_pos, event);
466 }
467
468@@ -1361,6 +1500,15 @@
469 return SnapCandidatePoint(position(), _snapSourceType(), _snapTargetType());
470 }
471
472+Geom::EllipticalArc Node::getEllipticalArc(){
473+ // Must have a next node for this to be an arc
474+ if (not _next()) return Geom::EllipticalArc();
475+ Geom::Point r = Geom::Point(_arc_rx.length(), _arc_ry.length());
476+ Geom::Coord rot = _arc_rx.angle();
477+ // Create an arc and return it
478+ return Geom::EllipticalArc(position(), r, rot, _arc_large, _arc_sweep, _next()->position());
479+}
480+
481 Handle *Node::handleToward(Node *to)
482 {
483 if (_next() == to) {
484@@ -1378,7 +1526,7 @@
485 if (front() == dir) {
486 return _next();
487 }
488- if (back() == dir) {
489+ if ( (back() == dir) || (arc_rx() == dir) || (arc_ry() == dir) ) {
490 return _prev();
491 }
492 g_error("Node::nodeToward(): handle is not a child of this node!");
493@@ -1402,7 +1550,7 @@
494 if (front() == h) {
495 return _prev();
496 }
497- if (back() == h) {
498+ if ( (back() == h) || (arc_rx() == h) || (arc_ry() == h) ) {
499 return _next();
500 }
501 g_error("Node::nodeAwayFrom(): handle is not a child of this node!");
502@@ -1492,9 +1640,9 @@
503 {
504 if (!first || !second) return false;
505 if (first->_next() == second)
506- return first->_front.isDegenerate() && second->_back.isDegenerate();
507+ return first->_front.isDegenerate() && second->_back.isDegenerate() && first->_arc_rx.isDegenerate() && first->_arc_ry.isDegenerate();
508 if (second->_next() == first)
509- return second->_front.isDegenerate() && first->_back.isDegenerate();
510+ return second->_front.isDegenerate() && first->_back.isDegenerate() && second->_arc_rx.isDegenerate() && second->_arc_ry.isDegenerate();
511 return false;
512 }
513
514
515=== modified file 'src/ui/tool/node.h'
516--- src/ui/tool/node.h 2015-04-30 09:17:07 +0000
517+++ src/ui/tool/node.h 2016-07-09 15:52:46 +0000
518@@ -29,6 +29,7 @@
519
520 #include <boost/enable_shared_from_this.hpp>
521 #include <boost/shared_ptr.hpp>
522+#include <2geom/elliptical-arc.h>
523 #include "ui/tool/selectable-control-point.h"
524 #include "snapped-point.h"
525 #include "ui/tool/node-types.h"
526@@ -100,6 +101,7 @@
527 virtual ~Handle();
528 inline Geom::Point relativePos() const;
529 inline double length() const;
530+ inline Geom::Angle angle() const;
531 bool isDegenerate() const { return _degenerate; } // True if the handle is retracted, i.e. has zero length.
532
533 virtual void setVisible(bool);
534@@ -107,7 +109,10 @@
535
536 virtual void setPosition(Geom::Point const &p);
537 inline void setRelativePos(Geom::Point const &p);
538+ virtual void setOffset(Geom::Point const &offset);
539 void setLength(double len);
540+ void setAngle(Geom::Angle a);
541+ void setAngleAndLength(Geom::Angle a, double len);
542 void retract();
543 void setDirection(Geom::Point const &from, Geom::Point const &to);
544 void setDirection(Geom::Point const &dir);
545@@ -140,6 +145,8 @@
546 SPCtrlLine *_handle_line;
547 bool _degenerate; // True if the handle is retracted, i.e. has zero length. This is used often internally so it makes sense to cache this
548
549+ Geom::Point _offset;
550+
551 /**
552 * Control point of a cubic Bezier curve in a path.
553 *
554@@ -183,6 +190,10 @@
555 void showHandles(bool v);
556
557 void updateHandles();
558+ void updateArcHandleConstriants(Handle *constraint);
559+ void moveArcHandles(Geom::Point lineBetweenNodes, double lenX, double lenY, Geom::Angle rotX, Geom::Angle rotY);
560+
561+ void retractArcHandles();
562
563
564 /**
565@@ -195,6 +206,11 @@
566 bool isEndNode() const;
567 Handle *front() { return &_front; }
568 Handle *back() { return &_back; }
569+ Handle *arc_rx() { return &_arc_rx; }
570+ Handle *arc_ry() { return &_arc_ry; }
571+ bool *arc_large() { return &_arc_large; }
572+ bool *arc_sweep() { return &_arc_sweep; }
573+ Geom::EllipticalArc getEllipticalArc();
574
575 /**
576 * Gets the handle that faces the given adjacent node.
577@@ -278,6 +294,11 @@
578 // as a line segment
579 Handle _front; ///< Node handle in the backward direction of the path
580 Handle _back; ///< Node handle in the forward direction of the path
581+ // The arc control points and flags relate to an arc starting at this node and ending at the next node
582+ Handle _arc_rx; ///< Handle for controling the x radius and rotation of the elipse (in eliptical arc mode)
583+ Handle _arc_ry; ///< Handle for controling the y radius and rotation of the elipse (in eliptical arc mode)
584+ bool _arc_large; ///< Flag that determines if arc mode is shallow or bulge
585+ bool _arc_sweep; ///< Flag that determines the direction of an arc's sweep
586 NodeType _type; ///< Type of node - cusp, smooth...
587 bool _handles_shown;
588 static ColorSet node_colors;
589@@ -489,14 +510,17 @@
590
591 // define inline Handle funcs after definition of Node
592 inline Geom::Point Handle::relativePos() const {
593- return position() - _parent->position();
594+ return position() - (_parent->position() + _offset);
595 }
596 inline void Handle::setRelativePos(Geom::Point const &p) {
597- setPosition(_parent->position() + p);
598+ setPosition(_parent->position() + _offset + p);
599 }
600 inline double Handle::length() const {
601 return relativePos().length();
602 }
603+inline Geom::Angle Handle::angle() const {
604+ return Geom::Angle(position() - _parent->position() - _offset);
605+}
606 inline PathManipulator &Handle::_pm() {
607 return _parent->_pm();
608 }
609
610=== modified file 'src/ui/tool/path-manipulator.cpp'
611--- src/ui/tool/path-manipulator.cpp 2016-05-19 22:11:42 +0000
612+++ src/ui/tool/path-manipulator.cpp 2016-07-09 15:52:46 +0000
613@@ -21,6 +21,7 @@
614 #include <2geom/bezier-curve.h>
615 #include <2geom/bezier-utils.h>
616 #include <2geom/path-sink.h>
617+#include <2geom/pathvector.h>
618 #include <glibmm/i18n.h>
619 #include "ui/tool/path-manipulator.h"
620 #include "desktop.h"
621@@ -801,7 +802,10 @@
622 }
623 }
624
625-/** Make selected segments curves / lines. */
626+// \todo The three functions setSegmentType, setArcSegmentLarge, setArcSegmentSweep could be
627+// refactored to share the code that iterates over all selected segments.
628+
629+/** Make selected segments curves / lines / arcs. */
630 void PathManipulator::setSegmentType(SegmentType type)
631 {
632 if (_num_selected == 0) return;
633@@ -811,18 +815,81 @@
634 if (!(k && j->selected() && k->selected())) continue;
635 switch (type) {
636 case SEGMENT_STRAIGHT:
637- if (j->front()->isDegenerate() && k->back()->isDegenerate())
638+ if ((j->front()->isDegenerate() && k->back()->isDegenerate())
639+ && (j->arc_rx()->isDegenerate() && j->arc_ry()->isDegenerate()))
640 break;
641 j->front()->move(*j);
642 k->back()->move(*k);
643+ j->retractArcHandles();
644 break;
645 case SEGMENT_CUBIC_BEZIER:
646- if (!j->front()->isDegenerate() || !k->back()->isDegenerate())
647- break;
648+ if (!j->front()->isDegenerate() || !k->back()->isDegenerate()){
649+ // Already a cubic bezier
650+ break;
651+ }
652+ if (!j->arc_rx()->isDegenerate() || !j->arc_ry()->isDegenerate()){
653+ // This is an elliptical arc that is being converted to a cubic bezier
654+ // Generate the bezier path and use it to replace the current segment
655+ Geom::Path cubicbezier_path = Geom::cubicbezierpath_from_sbasis(j->getEllipticalArc().toSBasis(), 0.1);
656+ replaceSegmentWithPath(j, cubicbezier_path);
657+ break;
658+ }
659+
660 // move both handles to 1/3 of the line
661 j->front()->move(j->position() + (k->position() - j->position()) / 3);
662 k->back()->move(k->position() + (j->position() - k->position()) / 3);
663- break;
664+ j->arc_rx()->move(*j);
665+ j->arc_ry()->move(*j);
666+ j->retractArcHandles();
667+ break;
668+ case SEGMENT_ELIPTICAL_ARC:
669+ if (!(j->front()->isDegenerate() && k->back()->isDegenerate())){
670+ j->front()->move(*j);
671+ k->back()->move(*k);
672+ }
673+
674+ if (j->arc_rx()->isDegenerate() && j->arc_ry()->isDegenerate()){
675+ Geom::Point midPointOffset = ((k->position() - j->position()) / 2);
676+ Geom::Point handleOrigin = j->position()+midPointOffset;
677+
678+ j->arc_rx()->setOffset(midPointOffset);
679+ j->arc_ry()->setOffset(midPointOffset);
680+ j->arc_rx()->move(j->position() + ((k->position() - handleOrigin) / 2));
681+ j->updateArcHandleConstriants(j->arc_rx());
682+ }
683+ break;
684+ }
685+ }
686+ }
687+}
688+
689+/** Set the large flag on selected arcs */
690+void PathManipulator::setArcSegmentLarge(bool large){
691+ if (_num_selected == 0) return;
692+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) {
693+ for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) {
694+ NodeList::iterator k = j.next();
695+ if (!(k && j->selected() && k->selected())) continue;
696+ // This code executes for every selected segment
697+ if (!j->arc_rx()->isDegenerate() || !j->arc_ry()->isDegenerate()){
698+ // This code executes for every selected ARC segment
699+ *(j->arc_large()) = large;
700+ }
701+ }
702+ }
703+}
704+
705+/** Set the sweep flag on selected arcs */
706+void PathManipulator::toggleArcSegmentSweep(){
707+ if (_num_selected == 0) return;
708+ for (SubpathList::iterator i = _subpaths.begin(); i != _subpaths.end(); ++i) {
709+ for (NodeList::iterator j = (*i)->begin(); j != (*i)->end(); ++j) {
710+ NodeList::iterator k = j.next();
711+ if (!(k && j->selected() && k->selected())) continue;
712+ // This code executes for every selected segment
713+ if (!j->arc_rx()->isDegenerate() || !j->arc_ry()->isDegenerate()){
714+ // This code executes for every selected ARC segment
715+ *(j->arc_sweep()) = !*j->arc_sweep();
716 }
717 }
718 }
719@@ -1094,6 +1161,75 @@
720 return match;
721 }
722
723+/** Replace a segment with a path.
724+ * @param segment The segment to replace
725+ * @param newPath The path to insert in place of 'segment'
726+ *
727+ * Currently, newPath must be composed only of cubic beziers, the caller must
728+ * ensure that the path only contains cubic beziers (until other segments are
729+ * implemented in this function) */
730+void PathManipulator::replaceSegmentWithPath(NodeList::iterator segment, Geom::Path newPath)
731+{
732+ if (!segment) throw std::invalid_argument("Invalid iterator for replacement");
733+ NodeList &list = NodeList::get(segment);
734+ NodeList::iterator second = segment.next();
735+ if (!second) throw std::invalid_argument("Replace after last node in open path");
736+
737+ // Retract all handles relating to this segment
738+ segment->retractArcHandles();
739+ segment->front()->retract();
740+
741+ // get the insertion point
742+ NodeList::iterator insert_at = segment;
743+ ++insert_at;
744+
745+ // Keep the previous node handy to update its handles when needed
746+ Node *prevNode = &(*insert_at);
747+
748+ // Path is to be inserted in reverse order
749+ Geom::Path reversedPath = newPath.reversed();
750+
751+ // Iterate over the path
752+ for (Geom::Path::iterator i = reversedPath.begin(); i != reversedPath.end(); ++i){
753+ const Geom::Curve & thisCurve = *i;
754+
755+ // Try converting to a bezier
756+ const Geom::BezierCurve * bezier = dynamic_cast<const Geom::BezierCurve*>(&thisCurve);
757+ if (bezier) {
758+ // Check order of bezier (currently only cubic beziers are supported)
759+ if (bezier->order() == 3)
760+ {
761+ // Create one new node
762+ Node *newNode = new Node(_multi_path_manipulator._path_data.node_data, bezier->finalPoint());
763+ // Set the control points for this node and the previous node
764+ newNode->front() ->setPosition((*bezier)[2]);
765+ prevNode->back()->setPosition((*bezier)[1]);
766+ // All new nodes are smooth
767+ newNode->setType(NODE_SMOOTH, false);
768+
769+ // Insert new node
770+ list.insert(insert_at, newNode);
771+ // Move along to next node
772+ prevNode = newNode;
773+ insert_at--;
774+ }
775+ else{
776+ // TODO, Is there a better exception to raise here?
777+ // TODO, implement this if needed in future
778+ throw std::invalid_argument("Only cubic bezier curves are implemented in PathManipulator::replaceSegment."
779+ " newPath contains beziers with order!=3.");
780+ }
781+ }
782+ else{
783+ // Not a bezier
784+ // TODO, Is there a better exception to raise here?
785+ // TODO, implement this if needed in future
786+ throw std::invalid_argument("Only cubic bezier curves are implemented in PathManipulator::replaceSegment."
787+ " newPath contains non-bezier segments.");
788+ }
789+ }
790+}
791+
792 /** Called by the XML observer when something else than us modifies the path. */
793 void PathManipulator::_externalChange(unsigned type)
794 {
795@@ -1149,7 +1285,8 @@
796
797 // sanitize pathvector and store it in SPCurve,
798 // so that _updateDragPoint doesn't crash on paths with naked movetos
799- Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers(_spcurve->get_pathvector());
800+ Geom::PathVector pathv = pathv_to_linear_and_cubic_beziers_and_arcs(_spcurve->get_pathvector());
801+
802 for (Geom::PathVector::iterator i = pathv.begin(); i != pathv.end(); ) {
803 // NOTE: this utilizes the fact that Geom::PathVector is an std::vector.
804 // When we erase an element, the next one slides into position,
805@@ -1199,6 +1336,15 @@
806 previous_node->front()->setPosition((*bezier)[1]);
807 current_node ->back() ->setPosition((*bezier)[2]);
808 }
809+ Geom::EllipticalArc const *arc = dynamic_cast<Geom::EllipticalArc const*>(&*cit);
810+ if (arc)
811+ {
812+ Geom::Coord angleX = arc->rotationAngle();
813+ Geom::Coord angleY = angleX + Geom::rad_from_deg(90);
814+ previous_node->moveArcHandles(current_node->position() - previous_node->position(), arc->ray(Geom::X), arc->ray(Geom::Y), angleX, angleY);
815+ *(previous_node->arc_large()) = arc->largeArc();
816+ *(previous_node->arc_sweep()) = arc->sweep();
817+ }
818 previous_node = current_node;
819 }
820 // If the path is closed, make the list cyclic
821@@ -1355,8 +1501,9 @@
822 }
823 if (subpath->closed()) {
824 // Here we link the last and first node if the path is closed.
825- // If the last segment is Bezier, we add it.
826- if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()) {
827+ // If the last segment is Bezier or arc, we add it.
828+ if (!prev->front()->isDegenerate() || !subpath->begin()->back()->isDegenerate()
829+ || !prev->arc_rx()->isDegenerate() || !prev->arc_ry()->isDegenerate()) {
830 build_segment(builder, prev.ptr(), subpath->begin().ptr());
831 }
832 // if that segment is linear, we just call closePath().
833@@ -1397,16 +1544,33 @@
834 * @relates PathManipulator */
835 void build_segment(Geom::PathBuilder &builder, Node *prev_node, Node *cur_node)
836 {
837- if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate())
838- {
839- // NOTE: It seems like the renderer cannot correctly handle vline / hline segments,
840- // and trying to display a path using them results in funny artifacts.
841- builder.lineTo(cur_node->position());
842- } else {
843- // this is a bezier segment
844- builder.curveTo(
845- prev_node->front()->position(),
846- cur_node->back()->position(),
847+ if (prev_node->arc_rx()->isDegenerate() || prev_node->arc_ry()->isDegenerate() ){
848+ // This is not an eliptical arc, check if it is a straight line or bezier
849+ if (cur_node->back()->isDegenerate() && prev_node->front()->isDegenerate())
850+ {
851+ // NOTE: It seems like the renderer cannot correctly handle vline / hline segments,
852+ // and trying to display a path using them results in funny artifacts.
853+ builder.lineTo(cur_node->position());
854+ } else {
855+ // this is a bezier segment
856+ builder.curveTo(
857+ prev_node->front()->position(),
858+ cur_node->back()->position(),
859+ cur_node->position());
860+ }
861+ }
862+ else{
863+ // This is an eliptical arc, get the x and y set by the xy handle
864+ Geom::Coord rx = prev_node->arc_rx()->length();
865+ Geom::Coord ry = prev_node->arc_ry()->length();
866+ Geom::Angle rot = prev_node->arc_rx()->angle();
867+
868+ builder.arcTo(
869+ rx,
870+ ry,
871+ rot,
872+ *prev_node->arc_large(),
873+ *prev_node->arc_sweep(),
874 cur_node->position());
875 }
876 }
877
878=== modified file 'src/ui/tool/path-manipulator.h'
879--- src/ui/tool/path-manipulator.h 2015-08-13 23:23:05 +0000
880+++ src/ui/tool/path-manipulator.h 2016-07-09 15:52:46 +0000
881@@ -15,6 +15,7 @@
882 #include <memory>
883 #include <2geom/pathvector.h>
884 #include <2geom/affine.h>
885+#include <2geom/sbasis-to-bezier.h>
886 #include <boost/shared_ptr.hpp>
887 #include <boost/weak_ptr.hpp>
888 #include "ui/tool/node.h"
889@@ -81,6 +82,9 @@
890 void reverseSubpaths(bool selected_only);
891 void setSegmentType(SegmentType);
892
893+ void setArcSegmentLarge(bool large);
894+ void toggleArcSegmentSweep();
895+
896 void scaleHandle(Node *n, int which, int dir, bool pixel);
897 void rotateHandle(Node *n, int which, int dir, bool pixel);
898
899@@ -97,6 +101,7 @@
900 NodeList::iterator subdivideSegment(NodeList::iterator after, double t);
901 NodeList::iterator extremeNode(NodeList::iterator origin, bool search_selected,
902 bool search_unselected, bool closest);
903+ void replaceSegmentWithPath(NodeList::iterator segment, Geom::Path newPath);
904
905 int _bsplineGetSteps() const;
906 // this is necessary for Tab-selection in MultiPathManipulator
907
908=== modified file 'src/widgets/node-toolbar.cpp'
909--- src/widgets/node-toolbar.cpp 2014-12-21 21:58:32 +0000
910+++ src/widgets/node-toolbar.cpp 2016-07-09 15:52:46 +0000
911@@ -171,6 +171,38 @@
912 }
913 }
914
915+static void sp_node_path_edit_toarc(void)
916+{
917+ NodeTool *nt = get_node_tool();
918+ if (nt) {
919+ nt->_multipath->setSegmentType(Inkscape::UI::SEGMENT_ELIPTICAL_ARC);
920+ }
921+}
922+
923+static void sp_node_path_edit_arc_shallow(void)
924+{
925+ NodeTool *nt = get_node_tool();
926+ if (nt) {
927+ nt->_multipath->setArcSegmentLarge(false);
928+ }
929+}
930+
931+static void sp_node_path_edit_arc_bulge(void)
932+{
933+ NodeTool *nt = get_node_tool();
934+ if (nt) {
935+ nt->_multipath->setArcSegmentLarge(true);
936+ }
937+}
938+
939+static void sp_node_path_edit_arc_flip(void)
940+{
941+ NodeTool *nt = get_node_tool();
942+ if (nt) {
943+ nt->_multipath->toggleArcSegmentSweep();
944+ }
945+}
946+
947 static void sp_node_path_edit_cusp(void)
948 {
949 NodeTool *nt = get_node_tool();
950@@ -486,6 +518,36 @@
951 }
952
953 {
954+ InkAction* inky = ink_action_new( "NodeArcShallowAction",
955+ _("Node Arc Shallow"),
956+ _("Make selected arc segments shallow"),
957+ INKSCAPE_ICON("node-segment-elliptical-arc-shallow"),
958+ secondarySize );
959+ g_signal_connect_after( G_OBJECT(inky), "activate", G_CALLBACK(sp_node_path_edit_arc_shallow), 0 );
960+ gtk_action_group_add_action( mainActions, GTK_ACTION(inky) );
961+ }
962+
963+ {
964+ InkAction* inky = ink_action_new( "NodeArcBulgeAction",
965+ _("Node Arc Bulge"),
966+ _("Make selected arc segments bulge"),
967+ INKSCAPE_ICON("node-segment-elliptical-arc-bulge"),
968+ secondarySize );
969+ g_signal_connect_after( G_OBJECT(inky), "activate", G_CALLBACK(sp_node_path_edit_arc_bulge), 0 );
970+ gtk_action_group_add_action( mainActions, GTK_ACTION(inky) );
971+ }
972+
973+ {
974+ InkAction* inky = ink_action_new( "NodeArcFlipAction",
975+ _("Node Arc Flip"),
976+ _("Flip selected arc segments"),
977+ INKSCAPE_ICON("node-segment-elliptical-arc-flip"),
978+ secondarySize );
979+ g_signal_connect_after( G_OBJECT(inky), "activate", G_CALLBACK(sp_node_path_edit_arc_flip), 0 );
980+ gtk_action_group_add_action( mainActions, GTK_ACTION(inky) );
981+ }
982+
983+ {
984 InkAction* inky = ink_action_new( "NodeLineAction",
985 _("Node Line"),
986 _("Make selected segments lines"),
987@@ -506,6 +568,16 @@
988 }
989
990 {
991+ InkAction* inky = ink_action_new( "NodeArcAction",
992+ _("Node Arc"),
993+ _("Make selected segments arcs"),
994+ INKSCAPE_ICON("node-segment-elliptical-arc"),
995+ secondarySize );
996+ g_signal_connect_after( G_OBJECT(inky), "activate", G_CALLBACK(sp_node_path_edit_toarc), 0 );
997+ gtk_action_group_add_action( mainActions, GTK_ACTION(inky) );
998+ }
999+
1000+ {
1001 InkToggleAction* act = ink_toggle_action_new( "NodesShowTransformHandlesAction",
1002 _("Show Transform Handles"),
1003 _("Show transformation handles for selected nodes"),
1004
1005=== modified file 'src/widgets/toolbox.cpp'
1006--- src/widgets/toolbox.cpp 2016-05-22 00:49:33 +0000
1007+++ src/widgets/toolbox.cpp 2016-07-09 15:52:46 +0000
1008@@ -282,6 +282,11 @@
1009 " <separator />"
1010 " <toolitem action='NodeLineAction' />"
1011 " <toolitem action='NodeCurveAction' />"
1012+ " <toolitem action='NodeArcAction' />"
1013+ " <separator />"
1014+ " <toolitem action='NodeArcShallowAction' />"
1015+ " <toolitem action='NodeArcBulgeAction' />"
1016+ " <toolitem action='NodeArcFlipAction' />"
1017 " <separator />"
1018 " <toolitem action='ObjectToPath' />"
1019 " <toolitem action='StrokeToPath' />"