.
1 /*
2 Copyright (C) 2014 2015 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 if (unlikely (ps == null)) {
101 warning ("No end point.");
102 return;
103 }
104 end_point = (!) ps;
105 if (end_point.is_first ()) {
106 end_point.path.reverse ();
107 }
108 glyph.set_active_path (end_point.path);
109 add_corner (x, y);
110 } else {
111 p = new Path ();
112 glyph.add_path (p);
113 glyph.open_path ();
114
115 PenTool.add_new_edit_point (x, y);
116 }
117
118 glyph.update_view ();
119 added_points = 0;
120 last_update = get_current_time ();
121 start_update_timer ();
122 drawing = true;
123
124 foreach (Object path in glyph.active_paths) {
125 if (path is FastPath) {
126 // cache merged stroke parts
127 ((FastPath) path).get_path ().create_full_stroke ();
128 }
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 Object o = g.active_paths.get (g.active_paths.size - 1);
153 p = ((FastPath) o).get_path ();
154
155 if (p.points.size > 1) {
156 previous = p.points.get (p.points.size - 1);
157 previous.type = DrawingTools.point_type;
158 previous.set_tie_handle (false);
159
160 previous = p.points.get (0);
161 previous.type = DrawingTools.point_type;
162 previous.set_tie_handle (false);
163 }
164 }
165
166 if (button == 1 && draw_freehand) {
167 return_if_fail (drawing);
168 add_endpoint_and_merge (x, y);
169 }
170
171 foreach (Object path in g.active_paths) {
172 if (path is FastPath) {
173 convert_hidden_points (((FastPath) path).get_path ());
174 }
175 }
176
177 g.clear_active_paths ();
178
179 set_tie ();
180 PenTool.force_direction ();
181 PenTool.reset_stroke ();
182 BirdFont.get_current_font ().touch ();
183 drawing = false;
184 }
185 });
186
187 move_action.connect ((self, x, y) => {
188 PointSelection? open_path = get_path_with_end_point (x, y);
189 PointSelection p;
190 bool join;
191
192 join = (open_path != null);
193
194 if (join != join_paths) {
195 MainWindow.get_current_glyph ().update_view ();
196 PenTool.reset_stroke ();
197 }
198
199 join_paths = join;
200
201 if (open_path != null) {
202 p = (!) open_path;
203 join_x = Glyph.reverse_path_coordinate_x (p.point.x);
204 join_y = Glyph.reverse_path_coordinate_y (p.point.y);
205 }
206
207 if (draw_freehand) {
208 record_new_position (x, y);
209 convert_on_timeout ();
210 last_x = x;
211 last_y = y;
212 PenTool.reset_stroke ();
213 }
214 });
215
216 draw_action.connect ((tool, cairo_context, glyph) => {
217 if (join_paths) {
218 PenTool.draw_join_icon (cairo_context, join_x, join_y);
219 }
220 });
221
222 key_press_action.connect ((self, keyval) => {
223 });
224 }
225
226 void convert_hidden_points (Path p) {
227 foreach (EditPoint e in p.points) {
228 if (e.type == PointType.HIDDEN) {
229 e.type = DrawingTools.point_type;
230 e.get_right_handle ().type = DrawingTools.point_type;
231 e.get_left_handle ().type = DrawingTools.point_type;
232 }
233 }
234 }
235
236 // FIXME: double check
237 void set_tie () {
238 Glyph glyph = MainWindow.get_current_glyph ();
239 var paths = glyph.get_visible_paths ();
240 Path p = paths.get (paths.size - 1);
241
242 foreach (EditPoint ep in p.points) {
243 if (ep.get_right_handle ().is_line () || ep.get_left_handle ().is_line ()) {
244 ep.set_tie_handle (false);
245 }
246
247 if (!ep.get_right_handle ().is_line () || !ep.get_left_handle ().is_line ()) {
248 ep.convert_to_curve ();
249 }
250 }
251 }
252
253 public void set_samples_per_point (double s) {
254 samples_per_point = s;
255 }
256
257 void add_endpoint_and_merge (int x, int y) {
258 Glyph glyph;
259 Path p;
260 PointSelection? open_path = get_path_with_end_point (x, y);
261 PointSelection joined_path;
262
263 glyph = MainWindow.get_current_glyph ();
264 var paths = glyph.get_visible_paths ();
265
266 if (paths.size == 0) {
267 warning ("No path.");
268 return;
269 }
270
271 // FIXME: double check this
272 p = paths.get (paths.size - 1);
273 draw_freehand = false;
274
275 convert_points_to_line ();
276
277 if (join_paths && open_path != null) {
278 joined_path = (!) open_path;
279
280 if (joined_path.path == p) {
281 delete_last_points_at (x, y);
282 glyph.close_path ();
283 p.close ();
284 } else {
285 p = merge_paths (p, joined_path);
286 if (!p.is_open ()) {
287 glyph.close_path ();
288 }
289 }
290
291 glyph.clear_active_paths ();
292 } else {
293 add_corner (x, y);
294 }
295
296 if (p.points.size == 0) {
297 warning ("No point.");
298 return;
299 }
300
301 p.create_list ();
302
303 if (DrawingTools.get_selected_point_type () == PointType.QUADRATIC) {
304 foreach (EditPoint e in p.points) {
305 if (e.tie_handles) {
306 e.convert_to_curve ();
307 e.process_tied_handle ();
308 }
309 }
310 }
311
312 if (PenTool.is_counter_path (p)) {
313 p.force_direction (Direction.COUNTER_CLOCKWISE);
314 } else {
315 p.force_direction (Direction.CLOCKWISE);
316 }
317
318 glyph.update_view ();
319 }
320
321 private static Path merge_paths (Path a, PointSelection b) {
322 Glyph g;
323 Path merged = a.copy ();
324
325 if (a.points.size < 2) {
326 warning ("Less than two points in path.");
327 return merged;
328 }
329
330 if (b.path.points.size < 2) {
331 warning ("Less than two points in path.");
332 return merged;
333 }
334
335 if (!b.is_first ()) {
336 b.path.close ();
337 b.path.reverse ();
338 b.path.reopen ();
339 }
340
341 merged.append_path (b.path);
342
343 g = MainWindow.get_current_glyph ();
344
345 g.add_path (merged);
346
347 a.delete_last_point ();
348
349 update_corner_handle (a.get_last_point (), b.path.get_first_point ());
350
351 g.delete_path (a);
352 g.delete_path (b.path);
353
354 merged.create_list ();
355 merged.update_region_boundaries ();
356 merged.recalculate_linear_handles ();
357 merged.reopen ();
358
359 return merged;
360 }
361
362 public static void update_corner_handle (EditPoint end, EditPoint new_start) {
363 EditPointHandle h1, h2;
364
365 h1 = end.get_right_handle ();
366 h2 = new_start.get_left_handle ();
367
368 h1.convert_to_line ();
369 h2.convert_to_line ();
370 }
371
372 PointSelection? get_path_with_end_point (int x, int y) {
373 Glyph glyph = MainWindow.get_current_glyph ();
374 EditPoint e;
375 EditPoint current_end = new EditPoint ();
376
377 // exclude the end point on the path we are adding points to
378 if (draw_freehand) {
379 current_end = get_active_path ().get_last_point ();
380 }
381
382 foreach (Path p in glyph.get_visible_paths ()) {
383 if (p.is_open () && p.points.size > 2) {
384 e = p.points.get (0);
385 if (PenTool.is_close_to_point (e, x, y)) {
386 return new PointSelection (e, p);
387 }
388
389 e = p.points.get (p.points.size - 1);
390 if (current_end != e && PenTool.is_close_to_point (e, x, y)) {
391 return new PointSelection (e, p);
392 }
393 }
394 }
395
396 return null;
397 }
398
399 void record_new_position (int x, int y) {
400 Glyph glyph;
401 Path p;
402 EditPoint new_point;
403 double px, py;
404
405 glyph = MainWindow.get_current_glyph ();
406
407 if (glyph.active_paths.size == 0) {
408 warning ("No path.");
409 return;
410 }
411
412 Object o = glyph.active_paths.get (glyph.active_paths.size - 1);
413
414 if (o is FastPath) {
415 warning ("Object is not a path");
416 return;
417 }
418
419 p = ((FastPath) o).get_path ();
420 p.reopen ();
421 px = Glyph.path_coordinate_x (x);
422 py = Glyph.path_coordinate_y (y);
423 new_point = p.add (px, py);
424 added_points++;
425
426 PenTool.convert_point_to_line (new_point, false);
427 new_point.set_point_type (PointType.HIDDEN);
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 > 4) { // cycles of 100 ms
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 > 25 / 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 (int px, int py) {
485 PointSelection p;
486 delete_last_points_at (px, py);
487 p = PenTool.add_new_edit_point (px, py);
488 p.point.set_tie_handle (false);
489 p.point.get_left_handle ().convert_to_line ();
490 p.point.get_right_handle ().convert_to_line ();
491 p.path.recalculate_linear_handles_for_point (p.point);
492 last_update = get_current_time ();
493 MainWindow.get_current_glyph ().update_view ();
494 }
495
496 Path get_active_path () {
497 Glyph glyph = MainWindow.get_current_glyph ();
498
499 if (glyph.active_paths.size == 0) {
500 warning ("No path.");
501 return new Path ();
502 }
503
504 Object o = glyph.active_paths.get (glyph.active_paths.size - 1);
505
506 if (likely (o is FastPath)) {
507 return ((FastPath) o).get_path ();
508 }
509
510 warning ("Active object is a path.");
511
512 return new Path ();
513 }
514
515 /** Delete all points close to the pixel at x,y. */
516 void delete_last_points_at (int x, int y) {
517 double px, py;
518 Path p;
519
520 p = get_active_path ();
521
522 if (unlikely (p.points.size == 0)) {
523 warning ("Missing point.");
524 return;
525 }
526
527 px = Glyph.path_coordinate_x (x);
528 py = Glyph.path_coordinate_y (y);
529
530 while (p.points.size > 0 && is_close (p.points.get (p.points.size - 1), px, py)) {
531 p.delete_last_point ();
532 }
533 }
534
535 /** @return true if the new point point is closer than a few pixels from p. */
536 bool is_close (EditPoint p, double x, double y) {
537 Glyph glyph = MainWindow.get_current_glyph ();
538 return glyph.view_zoom * Path.distance (p.x, x, p.y, y) < 5;
539 }
540
541 /** Take the average of tracked points and create a smooth line.
542 * @return the last removed point.
543 */
544 public void convert_points_to_line () {
545 EditPoint ep, last_point;
546 double sum_x, sum_y, nx, ny;
547 int px, py;
548 EditPoint average, previous;
549 Path p;
550 Glyph glyph;
551 Gee.ArrayList<EditPoint> points;
552
553 points = new Gee.ArrayList<EditPoint> ();
554 glyph = MainWindow.get_current_glyph ();
555 var paths = glyph.get_visible_paths ();
556
557 if (paths.size == 0) {
558 warning ("No path.");
559 return;
560 }
561
562 p = paths.get (paths.size - 1);
563
564 if (added_points == 0) { // last point
565 return;
566 }
567
568 if (unlikely (p.points.size < added_points)) {
569 warning ("Missing point.");
570 return;
571 }
572
573 sum_x = 0;
574 sum_y = 0;
575
576 last_point = p.points.get (p.points.size - 1);
577
578 for (int i = 0; i < added_points; i++) {
579 ep = p.delete_last_point ();
580 sum_x += ep.x;
581 sum_y += ep.y;
582 points.add (ep);
583 }
584
585 nx = sum_x / added_points;
586 ny = sum_y / added_points;
587
588 px = Glyph.reverse_path_coordinate_x (nx);
589 py = Glyph.reverse_path_coordinate_y (ny);
590 average = PenTool.add_new_edit_point (px, py).point;
591 average.type = PointType.HIDDEN;
592
593 // tie handles for all points except for the end points
594 average.set_tie_handle (p.points.size > 1);
595
596 if (unlikely (p.points.size == 0)) {
597 warning ("No points.");
598 return;
599 }
600
601 if (average.prev != null && average.get_prev ().tie_handles) {
602 if (p.points.size > 2) {
603 previous = average.get_prev ();
604 previous.type = DrawingTools.point_type;
605 PenTool.convert_point_to_line (previous, true);
606 p.recalculate_linear_handles_for_point (previous);
607 previous.process_tied_handle ();
608 previous.set_tie_handle (false);
609 }
610 }
611
612 added_points = 0;
613 last_update = get_current_time ();
614 glyph.update_view ();
615 p.reset_stroke ();
616 }
617
618 /** @return current time in milli seconds. */
619 public static double get_current_time () {
620 return GLib.get_real_time () / 1000.0;
621 }
622 }
623
624 }
625