.
1 /*
2 Copyright (C) 2014 Johan Mattsson
3
4 This library is free software; you can redistribute it and/or modify
5 it under the terms of the GNU Lesser General Public License as
6 published by the Free Software Foundation; either version 3 of the
7 License, or (at your option) any later version.
8
9 This library is distributed in the hope that it will be useful, but
10 WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 Lesser General Public License for more details.
13 */
14
15 using Cairo;
16 using Math;
17
18 namespace BirdFont {
19
20 public class StrokeTool : Tool {
21
22 public StrokeTool (string tooltip) {
23 select_action.connect((self) => {
24 stroke_selected_paths ();
25 });
26 }
27
28 public static void set_stroke_for_selected_paths (double width) {
29 Glyph g = MainWindow.get_current_glyph ();
30
31 foreach (Path p in g.active_paths) {
32 p.set_stroke (width);
33 }
34
35 GlyphCanvas.redraw ();
36 }
37
38 /** Create strokes for the selected outlines. */
39 void stroke_selected_paths () {
40 Glyph g = MainWindow.get_current_glyph ();
41 PathList paths = new PathList ();
42
43 foreach (Path p in g.active_paths) {
44 paths.append (get_stroke (p, p.stroke));
45 }
46
47 foreach (Path np in paths.paths) {
48 g.add_path (np);
49 }
50 }
51
52 public static PathList get_stroke (Path path, double thickness) {
53 Path p = path.copy ();
54 PathList pl;
55
56 pl = get_stroke_outline (p, thickness);
57
58 return pl;
59 }
60
61 public static PathList get_stroke_outline (Path p, double thickness) {
62 Path counter, outline, merged;
63 PathList paths = new PathList ();
64
65 if (!p.is_open () && p.is_filled ()) {
66 outline = create_stroke (p, thickness);
67 outline.close ();
68 paths.add (outline);
69 outline.update_region_boundaries ();
70 } else if (!p.is_open () && !p.is_filled ()) {
71 outline = create_stroke (p, thickness);
72 counter = create_stroke (p, -1 * thickness);
73
74 paths.add (outline);
75 paths.add (counter);
76
77 if (p.is_clockwise ()) {
78 outline.force_direction (Direction.CLOCKWISE);
79 } else {
80 outline.force_direction (Direction.COUNTER_CLOCKWISE);
81 }
82
83 if (outline.is_clockwise ()) {
84 counter.force_direction (Direction.COUNTER_CLOCKWISE);
85 } else {
86 counter.force_direction (Direction.CLOCKWISE);
87 }
88
89 outline.update_region_boundaries ();
90 counter.update_region_boundaries ();
91 } else if (p.is_open ()) {
92 outline = create_stroke (p, thickness);
93 counter = create_stroke (p, -1 * thickness);
94 merged = merge_strokes (p, outline, counter, thickness);
95
96 if (p.is_clockwise ()) {
97 merged.force_direction (Direction.CLOCKWISE);
98 } else {
99 merged.force_direction (Direction.COUNTER_CLOCKWISE);
100 }
101
102 merged.update_region_boundaries ();
103 paths.add (merged);
104 } else {
105 warning ("Can not create stroke.");
106 paths.add (p);
107 }
108
109 return paths;
110 }
111
112 /** Create one stroke from the outline and counter stroke and close the
113 * open endings.
114 *
115 * @param path the path to create stroke for
116 * @param stroke for the outline of path
117 * @param stroke for the counter path
118 */
119 static Path merge_strokes (Path path, Path stroke, Path counter, double thickness) {
120 Path merged;
121 EditPoint corner1, corner2;
122 EditPoint corner3, corner4;
123 EditPoint end;
124 double angle;
125
126 if (path.points.size < 2) {
127 warning ("Missing points.");
128 return stroke;
129 }
130
131 if (stroke.points.size < 4) {
132 warning ("Missing points.");
133 return stroke;
134 }
135
136 if (counter.points.size < 4) {
137 warning ("Missing points.");
138 return stroke;
139 }
140
141 // end of stroke
142 end = path.get_last_visible_point ();
143 corner1 = stroke.get_last_point ();
144 angle = end.get_left_handle ().angle;
145 corner1.x = end.x + cos (angle - PI / 2) * thickness;
146 corner1.y = end.y + sin (angle - PI / 2) * thickness;
147
148 corner2 = counter.get_last_point ();
149 corner2.x = end.x + cos (angle + PI / 2) * thickness;
150 corner2.y = end.y + sin (angle + PI / 2) * thickness;
151
152 // the other end
153 end = path.get_first_point ();
154 corner3 = stroke.get_first_point ();
155 angle = end.get_right_handle ().angle;
156 corner3.x = end.x + cos (angle + PI / 2) * thickness;
157 corner3.y = end.y + sin (angle + PI / 2) * thickness;
158
159 corner4 = counter.get_first_point ();
160 corner4.x = end.x + cos (angle - PI / 2) * thickness;
161 corner4.y = end.y + sin (angle - PI / 2) * thickness;
162
163 corner1.get_left_handle ().convert_to_line ();
164 corner2.get_right_handle ().convert_to_line ();
165
166 corner3.get_left_handle ().convert_to_line ();
167 corner4.get_right_handle ().convert_to_line ();
168
169 counter.reverse ();
170
171 // Append the other part of the stroke
172 merged = stroke.copy ();
173 merged.append_path (counter);
174 corner2 = merged.points.get (merged.points.size - 1);
175
176 merged.close ();
177 merged.create_list ();
178 merged.recalculate_linear_handles ();
179
180 return merged;
181 }
182
183 static Path create_stroke (Path p, double thickness) {
184 Path stroked;
185
186 if (p.points.size >= 2) {
187 stroked = p.copy ();
188 stroked = generate_stroke (stroked, thickness);
189
190 if (!p.is_open ()) {
191 stroked.reverse ();
192 stroked.close ();
193 }
194 } else {
195 // TODO: create stroke for a path with one point
196 warning ("One point.");
197 stroked = new Path ();
198 }
199
200 return stroked;
201 }
202
203 static Path generate_stroke (Path p, double thickness) {
204 Path stroked = new Path ();
205 EditPoint start;
206 EditPoint end;
207 EditPoint previous = new EditPoint ();
208
209 foreach (EditPoint ep in p.points) {
210 start = ep.copy ();
211 end = ep.get_next ().copy ();
212
213 move_segment (start, end, thickness);
214
215 if (end.get_left_handle ().length > 0) {
216 add_corner (stroked, previous, start, ep.copy (), thickness);
217 }
218
219 stroked.add_point (start);
220
221 if (end.get_left_handle ().length > 0) {
222 stroked.add_point (end);
223 }
224
225 // line ends around corner
226 start.get_left_handle ().convert_to_line ();
227 end.get_right_handle ().convert_to_line ();
228
229 previous = end;
230 }
231 stroked.recalculate_linear_handles ();
232
233 return stroked;
234 }
235
236 static void move_segment (EditPoint stroke_start, EditPoint stroke_stop, double thickness) {
237 EditPointHandle r, l;
238 double m, n;
239 double qx, qy;
240
241 stroke_start.set_tie_handle (false);
242 stroke_stop.set_tie_handle (false);
243
244 r = stroke_start.get_right_handle ();
245 l = stroke_stop.get_left_handle ();
246
247 m = cos (r.angle + PI / 2) * thickness;
248 n = sin (r.angle + PI / 2) * thickness;
249
250 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n);
251 stroke_start.get_left_handle ().move_to_coordinate_delta (m, n);
252
253 stroke_start.independent_x += m;
254 stroke_start.independent_y += n;
255
256 qx = cos (l.angle - PI / 2) * thickness;
257 qy = sin (l.angle - PI / 2) * thickness;
258
259 stroke_stop.get_right_handle ().move_to_coordinate_delta (qx, qy);
260 stroke_stop.get_left_handle ().move_to_coordinate_delta (qx, qy);
261
262 stroke_stop.independent_x += qx;
263 stroke_stop.independent_y += qy;
264 }
265
266 static void add_corner (Path stroked, EditPoint previous, EditPoint next,
267 EditPoint original, double stroke_width) {
268
269 double ratio;
270 double distance;
271 EditPoint corner;
272 double corner_x, corner_y;
273 EditPointHandle previous_handle;
274 EditPointHandle next_handle;
275 EditPoint cutoff1, cutoff2;
276
277 previous_handle = previous.get_left_handle ();
278 next_handle = next.get_right_handle ();
279
280 previous_handle.angle += PI;
281 next_handle.angle += PI;
282
283 Path.find_intersection_handle (previous_handle, next_handle, out corner_x, out corner_y);
284 corner = new EditPoint (corner_x, corner_y, previous.type);
285 corner.convert_to_line ();
286
287 distance = Path.distance_to_point (corner, original);
288
289 ratio = 1.5 * fabs (stroke_width) / distance; // FIXME: make cutoff a parameter
290
291 if (ratio > 1) {
292 stroked.add_point (corner);
293 } else {
294 cutoff1 = new EditPoint ();
295 cutoff1.set_point_type (previous.type);
296 cutoff1.convert_to_line ();
297
298 cutoff2 = new EditPoint ();
299 cutoff2.set_point_type (previous.type);
300 cutoff2.convert_to_line ();
301
302 cutoff1.x = previous.x + (corner.x - previous.x) * ratio;
303 cutoff1.y = previous.y + (corner.y - previous.y) * ratio;
304
305 cutoff2.x = next.x + (corner.x - next.x) * ratio;
306 cutoff2.y = next.y + (corner.y - next.y) * ratio;
307
308 stroked.add_point (cutoff1);
309 stroked.add_point (cutoff2);
310 }
311
312 previous_handle.angle -= PI;
313 next_handle.angle -= PI;
314 }
315
316
317 }
318
319 }
320
321