1 /*
2 Copyright (C) 2014 2015 2016 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 /** A tool that lets the user draw fonts with the mouse
21 * instead of adding bezér points one by one.
22 */
23 public class TrackTool : Tool {
24
25 bool draw_freehand = false;
26
27 /** Number of points to take the average from in order to create a smooth shape. */
28 int added_points = 0;
29
30 /** The time in milliseconds when a point was added to the path.
31 * after a few milliseconds will this tool add a sharp corner instead of
32 * a smooth curve if the user does not move the pointer.
33 */
34 double last_update = 0;
35
36 /** The position of mouse pointer. at lat update. */
37 int last_x = 0;
38 int last_y = 0;
39
40 /** The position of the mouse pointer when this tool is checking if
41 * the pointer has moved.
42 */
43 int last_timer_x = 0;
44 int last_timer_y = 0;
45 int update_cycles = 0;
46
47 /** Join the stroke with the path at the end point in this coordinate. */
48 int join_x = -1;
49 int join_y = -1;
50 bool join_paths = false;
51
52 /** Adjust the number of samples per point by this factor. */
53 double samples_per_point = 1;
54 bool drawing = false;
55
56 public TrackTool (string name) {
57 base (name, t_("Freehand drawing"));
58
59 select_action.connect (() => {
60 convert_points_to_line ();
61 draw_freehand = false;
62 });
63
64 deselect_action.connect (() => {
65 convert_points_to_line ();
66 draw_freehand = false;
67 });
68
69 press_action.connect ((self, button, x, y) => {
70 Glyph glyph = MainWindow.get_current_glyph ();
71 Path p;
72 PointSelection? ps;
73 PointSelection end_point;
74
75 if (button == 3) {
76 glyph.clear_active_paths ();
77 }
78
79 if (button == 2) {
80 glyph.close_path ();
81 }
82
83 if (button == 1) {
84 if (draw_freehand) {
85 warning ("Already drawing.");
86 return;
87 }
88
89 return_if_fail (!drawing);
90
91 draw_freehand = true;
92
93 last_x = x;
94 last_y = y;
95
96 glyph.store_undo_state ();
97
98 if (join_paths) {
99 ps = get_path_with_end_point (x, y);
100
101 if (unlikely (ps == null)) {
102 warning ("No end point.");
103 return;
104 }
105
106 end_point = (!) ps;
107
108 if (end_point.is_first ()) {
109 end_point.path.reverse ();
110 }
111
112 glyph.set_active_path (end_point.path);
113 } else {
114 p = new Path ();
115 glyph.add_path (p);
116 glyph.open_path ();
117
118 PenTool.add_new_edit_point (x, y);
119 }
120
121 glyph.update_view ();
122 added_points = 0;
123 last_update = get_current_time ();
124 start_update_timer ();
125 drawing = true;
126
127 foreach (Path path in glyph.active_paths) {
128 path.create_full_stroke (); // cache merged stroke parts
129 }
130 }
131 });
132
133 double_click_action.connect ((self, b, x, y) => {
134 });
135
136 release_action.connect ((self, button, x, y) => {
137 Path p;
138 Glyph g = MainWindow.get_current_glyph ();
139 EditPoint previous;
140
141 if (button == 1) {
142 if (!draw_freehand) {
143 warning ("Not drawing.");
144 return;
145 }
146
147 convert_points_to_line ();
148
149 g = MainWindow.get_current_glyph ();
150
151 if (g.active_paths.size > 0) { // set type for last point
152 p = g.active_paths.get (g.active_paths.size - 1);
153
154 if (p.points.size > 1) {
155 previous = p.points.get (p.points.size - 1);
156 previous.type = PointType.CUBIC;
157 previous.set_tie_handle (false);
158
159 previous = p.points.get (0);
160 previous.type = PointType.CUBIC;
161 previous.set_tie_handle (false);
162 }
163 }
164
165 if (button == 1 && draw_freehand) {
166 return_if_fail (drawing);
167 add_endpoint_and_merge (x, y);
168 }
169
170 foreach (Path path in g.layers.get_all_paths ().paths) {
171 convert_hidden_points (path);
172 path.update_region_boundaries ();
173 }
174
175 g.clear_active_paths ();
176
177 set_tie ();
178 PenTool.force_direction ();
179 PenTool.reset_stroke ();
180 BirdFont.get_current_font ().touch ();
181 drawing = false;
182 }
183 });
184
185 move_action.connect ((self, x, y) => {
186 PointSelection? open_path = get_path_with_end_point (x, y);
187 PointSelection p;
188 bool join;
189
190 join = (open_path != null);
191
192 if (join != join_paths) {
193 MainWindow.get_current_glyph ().update_view ();
194 PenTool.reset_stroke ();
195 }
196
197 join_paths = join;
198
199 if (open_path != null) {
200 p = (!) open_path;
201 join_x = Glyph.reverse_path_coordinate_x (p.point.x);
202 join_y = Glyph.reverse_path_coordinate_y (p.point.y);
203 }
204
205 if (draw_freehand) {
206 record_new_position (x, y);
207 convert_on_timeout ();
208 last_x = x;
209 last_y = y;
210 PenTool.reset_stroke ();
211 }
212 });
213
214 draw_action.connect ((tool, cairo_context, glyph) => {
215 if (join_paths) {
216 PenTool.draw_join_icon (cairo_context, join_x, join_y);
217 }
218 });
219
220 key_press_action.connect ((self, keyval) => {
221 });
222 }
223
224 void convert_hidden_points (Path p) {
225 foreach (EditPoint e in p.points) {
226 if (e.type == PointType.HIDDEN) {
227 e.type = DrawingTools.point_type;
228 e.get_right_handle ().type = PointType.CUBIC;
229 e.get_left_handle ().type = PointType.CUBIC;
230 }
231 }
232 }
233
234 // FIXME: double check
235 void set_tie () {
236 Glyph glyph = MainWindow.get_current_glyph ();
237 var paths = glyph.get_visible_paths ();
238 Path p = paths.get (paths.size - 1);
239
240 foreach (EditPoint ep in p.points) {
241 if (ep.get_right_handle ().is_line () || ep.get_left_handle ().is_line ()) {
242 ep.set_tie_handle (false);
243 }
244
245 if (!ep.get_right_handle ().is_line () || !ep.get_left_handle ().is_line ()) {
246 ep.convert_to_curve ();
247 }
248 }
249 }
250
251 public void set_samples_per_point (double s) {
252 samples_per_point = s;
253 }
254
255 void add_endpoint_and_merge (int x, int y) {
256 Glyph glyph;
257 Path p;
258 PointSelection? open_path = get_path_with_end_point (x, y);
259 PointSelection joined_path;
260
261 glyph = MainWindow.get_current_glyph ();
262 var paths = glyph.get_visible_paths ();
263
264 if (paths.size == 0) {
265 warning ("No path.");
266 return;
267 }
268
269 // FIXME: double check this
270 p = paths.get (paths.size - 1);
271 draw_freehand = false;
272
273 convert_points_to_line ();
274
275 if (join_paths && open_path != null) {
276 joined_path = (!) open_path;
277
278 if (joined_path.path == p) {
279 delete_last_points_at (x, y);
280 glyph.close_path ();
281 p.close ();
282 } else {
283 p = merge_paths (p, joined_path);
284 if (!p.is_open ()) {
285 glyph.close_path ();
286 }
287 }
288
289 glyph.clear_active_paths ();
290 } else {
291 add_corner (x, y);
292 }
293
294 if (p.points.size == 0) {
295 warning ("No point.");
296 return;
297 }
298
299 EditPoint last_point = p.get_last_point ();
300 EditPointHandle handle = last_point.get_right_handle ();
301 handle.convert_to_line ();
302 p.recalculate_linear_handles ();
303
304 PenTool.convert_point_type (p.get_last_point (), PointType.CUBIC);
305 PenTool.convert_point_type (p.get_first_point (), PointType.CUBIC);
306
307 p.create_list ();
308
309 if (PenTool.is_counter_path (p)) {
310 p.force_direction (Direction.COUNTER_CLOCKWISE);
311 } else {
312 p.force_direction (Direction.CLOCKWISE);
313 }
314
315 glyph.update_view ();
316 }
317
318 private static Path merge_paths (Path a, PointSelection b) {
319 Glyph g;
320 Path merged = a.copy ();
321
322 if (a.points.size < 2) {
323 warning ("Less than two points in path.");
324 return merged;
325 }
326
327 if (b.path.points.size < 2) {
328 warning ("Less than two points in path.");
329 return merged;
330 }
331
332 if (!b.is_first ()) {
333 b.path.close ();
334 b.path.reverse ();
335 b.path.reopen ();
336 }
337
338 merged.append_path (b.path);
339
340 g = MainWindow.get_current_glyph ();
341 g.add_path (merged);
342 a.delete_last_point ();
343
344 update_corner_handle (a.get_last_point (), b.path.get_first_point ());
345
346 g.delete_path (a);
347 g.delete_path (b.path);
348
349 merged.create_list ();
350 merged.update_region_boundaries ();
351 merged.recalculate_linear_handles ();
352 merged.reopen ();
353
354 return merged;
355 }
356
357 public static void update_corner_handle (EditPoint end, EditPoint new_start) {
358 EditPointHandle h1, h2;
359
360 h1 = end.get_right_handle ();
361 h2 = new_start.get_left_handle ();
362
363 h1.convert_to_line ();
364 h2.convert_to_line ();
365 }
366
367 PointSelection? get_path_with_end_point (int x, int y) {
368 Glyph glyph = MainWindow.get_current_glyph ();
369 EditPoint e;
370 EditPoint current_end = new EditPoint ();
371
372 // exclude the end point on the path we are adding points to
373 if (draw_freehand) {
374 current_end = get_active_path ().get_last_point ();
375 }
376
377 foreach (Path p in glyph.get_visible_paths ()) {
378 if (p.is_open () && p.points.size > 2) {
379 e = p.points.get (0);
380 if (PenTool.is_close_to_point (e, x, y)) {
381 return new PointSelection (e, p);
382 }
383
384 e = p.points.get (p.points.size - 1);
385 if (current_end != e && PenTool.is_close_to_point (e, x, y)) {
386 return new PointSelection (e, p);
387 }
388 }
389 }
390
391 return null;
392 }
393
394 void record_new_position (int x, int y) {
395 Glyph glyph;
396 Path p;
397 EditPoint new_point;
398 double px, py;
399
400 glyph = MainWindow.get_current_glyph ();
401
402 if (glyph.active_paths.size == 0) {
403 warning ("No path.");
404 return;
405 }
406
407 p = glyph.active_paths.get (glyph.active_paths.size - 1);
408 p.reopen ();
409
410 EditPoint last_point = new EditPoint ();
411
412 if (p.points.size > 0) {
413 last_point = p.get_last_point ();
414 }
415
416 px = Glyph.path_coordinate_x (x);
417 py = Glyph.path_coordinate_y (y);
418
419 new_point = new EditPoint (px, py, PointType.CUBIC);
420 p.add_point (new_point);
421 added_points++;
422
423 PenTool.convert_point_to_line (new_point, false);
424 new_point.set_point_type (PointType.HIDDEN);
425 p.recalculate_linear_handles_for_point (new_point);
426
427 last_point.get_right_handle ().length = 0.000001;
428
429 if (p.points.size > 1) {
430 glyph.redraw_segment (new_point, new_point.get_prev ());
431 }
432
433 glyph.update_view ();
434
435 last_x = x;
436 last_y = y;
437 }
438
439 void start_update_timer () {
440 TimeoutSource timer = new TimeoutSource (100);
441
442 timer.set_callback (() => {
443 if (draw_freehand) {
444 record_new_position (last_x, last_y);
445 convert_on_timeout ();
446 }
447
448 return draw_freehand;
449 });
450
451 timer.attach (null);
452 }
453
454 /** @returns true while the mounse pointer is moving. */
455 bool is_moving (int x, int y) {
456 return Path.distance (x, last_x, y, last_y) >= 1;
457 }
458
459 /** Add a new point if the update period has ended. */
460 void convert_on_timeout () {
461 if (!is_moving (last_timer_x, last_timer_y)) {
462 update_cycles++;
463 } else {
464 last_timer_x = last_x;
465 last_timer_y = last_y;
466 update_cycles = 0;
467 }
468
469 if (update_cycles > 0.7 * 10) { // delay in time
470 convert_points_to_line ();
471 last_update = get_current_time ();
472 add_corner (last_x, last_y);
473 added_points = 0;
474 update_cycles = 0;
475 }
476
477 if (added_points > 80 / samples_per_point) {
478 last_update = get_current_time ();
479 convert_points_to_line ();
480 }
481 }
482
483 /** Add a sharp corner instead of a smooth curve. */
484 void add_corner (double px, double py) {
485 EditPoint p;
486 p = new EditPoint (px, py, PointType.CUBIC);
487 p.set_tie_handle (false);
488 p.get_left_handle ().convert_to_line ();
489 p.get_right_handle ().convert_to_line ();
490 get_active_path ().recalculate_linear_handles_for_point (p);
491 last_update = get_current_time ();
492 MainWindow.get_current_glyph ().update_view ();
493 }
494
495 Path get_active_path () {
496 Glyph glyph = MainWindow.get_current_glyph ();
497
498 if (glyph.active_paths.size == 0) {
499 warning ("No path.");
500 return new Path ();
501 }
502
503 return glyph.active_paths.get (glyph.active_paths.size - 1);
504 }
505
506 /** Delete all points close to the pixel at x,y. */
507 void delete_last_points_at (double px, double py) {
508 Path p;
509
510 p = get_active_path ();
511
512 if (unlikely (p.points.size == 0)) {
513 warning ("Missing point.");
514 return;
515 }
516
517 while (p.points.size > 0 && is_close (p.points.get (p.points.size - 1), px, py)) {
518 p.delete_last_point ();
519 }
520 }
521
522 /** @return true if the new point point is closer than a few pixels from p. */
523 bool is_close (EditPoint p, double x, double y) {
524 Glyph glyph = MainWindow.get_current_glyph ();
525 return glyph.view_zoom * Path.distance (p.x, x, p.y, y) < 5;
526 }
527
528 /** Take the average of tracked points and create a smooth line.
529 * @return the last removed point.
530 */
531 public void convert_points_to_line () {
532 double sum_x, sum_y;
533 Path p;
534 Glyph glyph;
535 Gee.ArrayList<EditPoint> points;
536
537 points = new Gee.ArrayList<EditPoint> ();
538 glyph = MainWindow.get_current_glyph ();
539 var paths = glyph.get_visible_paths ();
540
541 if (paths.size == 0) {
542 warning ("No path.");
543 return;
544 }
545
546 p = paths.get (paths.size - 1);
547
548 if (added_points == 0) { // last point
549 return;
550 }
551
552 if (unlikely (p.points.size < added_points)) {
553 warning ("Missing point.");
554 return;
555 }
556
557 sum_x = 0;
558 sum_y = 0;
559
560 int start = p.points.size - 1 - added_points;
561 int stop = p.points.size - 1;
562
563 EditPoint end = p.points.get (stop);
564
565 Path segment = StrokeTool.fit_bezier_path (p, start, stop, 5.0 / samples_per_point);
566
567 for (int i = 0; i < added_points; i++) {
568 p.delete_last_point ();
569 }
570
571 p.append_path (segment);
572 p.remove_points_on_points ();
573
574 add_corner (end.x, end.y);
575
576 added_points = 0;
577 last_update = get_current_time ();
578 glyph.update_view ();
579 p.reset_stroke ();
580 }
581
582 /** @return current time in milli seconds. */
583 public static double get_current_time () {
584 return GLib.get_real_time () / 1000.0;
585 }
586 }
587
588 }
589