The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

TrackTool.vala in libbirdfont

This file is a part of the Birdfont project.

Contributing

Send patches or pull requests to johan.mattsson.m@gmail.com.
Clone this repository: git clone https://github.com/johanmattssonm/birdfont.git

Revisions

View the latest version of libbirdfont/TrackTool.vala.
Merge ../birdfont-2.x
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 PathObject) { 126 // cache merged stroke parts 127 ((PathObject) 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 = ((PathObject) 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 PathObject) { 173 convert_hidden_points (((PathObject) 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 void set_tie () { 237 Glyph glyph = MainWindow.get_current_glyph (); 238 var paths = glyph.get_visible_paths (); 239 Path p; 240 241 if (paths.size == 0) { 242 return; 243 } 244 245 p = paths.get (paths.size - 1); 246 247 foreach (EditPoint ep in p.points) { 248 if (ep.get_right_handle ().is_line () || ep.get_left_handle ().is_line ()) { 249 ep.set_tie_handle (false); 250 } 251 252 if (!ep.get_right_handle ().is_line () || !ep.get_left_handle ().is_line ()) { 253 ep.convert_to_curve (); 254 } 255 } 256 } 257 258 public void set_samples_per_point (double s) { 259 samples_per_point = s; 260 } 261 262 void add_endpoint_and_merge (int x, int y) { 263 Glyph glyph; 264 Path p; 265 PointSelection? open_path = get_path_with_end_point (x, y); 266 PointSelection joined_path; 267 268 glyph = MainWindow.get_current_glyph (); 269 var paths = glyph.get_visible_paths (); 270 271 if (paths.size == 0) { 272 warning ("No path."); 273 return; 274 } 275 276 // FIXME: double check this 277 p = paths.get (paths.size - 1); 278 draw_freehand = false; 279 280 convert_points_to_line (); 281 282 if (join_paths && open_path != null) { 283 joined_path = (!) open_path; 284 285 if (joined_path.path == p) { 286 delete_last_points_at (x, y); 287 glyph.close_path (); 288 p.close (); 289 } else { 290 p = merge_paths (p, joined_path); 291 if (!p.is_open ()) { 292 glyph.close_path (); 293 } 294 } 295 296 glyph.clear_active_paths (); 297 } else { 298 add_corner (x, y); 299 } 300 301 if (p.points.size == 0) { 302 warning ("No point."); 303 return; 304 } 305 306 p.create_list (); 307 308 if (DrawingTools.get_selected_point_type () == PointType.QUADRATIC) { 309 foreach (EditPoint e in p.points) { 310 if (e.tie_handles) { 311 e.convert_to_curve (); 312 e.process_tied_handle (); 313 } 314 } 315 } 316 317 if (PenTool.is_counter_path (p)) { 318 p.force_direction (Direction.COUNTER_CLOCKWISE); 319 } else { 320 p.force_direction (Direction.CLOCKWISE); 321 } 322 323 glyph.update_view (); 324 } 325 326 private static Path merge_paths (Path a, PointSelection b) { 327 Glyph g; 328 Path merged = a.copy (); 329 330 if (a.points.size < 2) { 331 warning ("Less than two points in path."); 332 return merged; 333 } 334 335 if (b.path.points.size < 2) { 336 warning ("Less than two points in path."); 337 return merged; 338 } 339 340 if (!b.is_first ()) { 341 b.path.close (); 342 b.path.reverse (); 343 b.path.reopen (); 344 } 345 346 merged.append_path (b.path); 347 348 g = MainWindow.get_current_glyph (); 349 350 g.add_path (merged); 351 352 a.delete_last_point (); 353 354 update_corner_handle (a.get_last_point (), b.path.get_first_point ()); 355 356 g.delete_path (a); 357 g.delete_path (b.path); 358 359 merged.create_list (); 360 merged.update_region_boundaries (); 361 merged.recalculate_linear_handles (); 362 merged.reopen (); 363 364 return merged; 365 } 366 367 public static void update_corner_handle (EditPoint end, EditPoint new_start) { 368 EditPointHandle h1, h2; 369 370 h1 = end.get_right_handle (); 371 h2 = new_start.get_left_handle (); 372 373 h1.convert_to_line (); 374 h2.convert_to_line (); 375 } 376 377 PointSelection? get_path_with_end_point (int x, int y) { 378 Glyph glyph = MainWindow.get_current_glyph (); 379 EditPoint e; 380 EditPoint current_end = new EditPoint (); 381 382 // exclude the end point on the path we are adding points to 383 if (draw_freehand) { 384 current_end = get_active_path ().get_last_point (); 385 } 386 387 foreach (Path p in glyph.get_visible_paths ()) { 388 if (p.is_open () && p.points.size > 2) { 389 e = p.points.get (0); 390 if (PenTool.is_close_to_point (e, x, y)) { 391 return new PointSelection (e, p); 392 } 393 394 e = p.points.get (p.points.size - 1); 395 if (current_end != e && PenTool.is_close_to_point (e, x, y)) { 396 return new PointSelection (e, p); 397 } 398 } 399 } 400 401 return null; 402 } 403 404 void record_new_position (int x, int y) { 405 Glyph glyph; 406 Path p; 407 EditPoint new_point; 408 double px, py; 409 410 glyph = MainWindow.get_current_glyph (); 411 412 if (glyph.active_paths.size == 0) { 413 warning ("No path."); 414 return; 415 } 416 417 Object o = glyph.active_paths.get (glyph.active_paths.size - 1); 418 419 if (unlikely (!(o is PathObject))) { 420 warning ("Object is not a path"); 421 return; 422 } 423 424 p = ((PathObject) o).get_path (); 425 p.reopen (); 426 px = Glyph.path_coordinate_x (x); 427 py = Glyph.path_coordinate_y (y); 428 new_point = p.add (px, py); 429 added_points++; 430 431 PenTool.convert_point_to_line (new_point, false); 432 new_point.set_point_type (PointType.HIDDEN); 433 p.recalculate_linear_handles_for_point (new_point); 434 435 if (p.points.size > 1) { 436 glyph.redraw_segment (new_point, new_point.get_prev ()); 437 } 438 439 glyph.update_view (); 440 441 last_x = x; 442 last_y = y; 443 } 444 445 void start_update_timer () { 446 TimeoutSource timer = new TimeoutSource (100); 447 448 timer.set_callback (() => { 449 if (draw_freehand) { 450 record_new_position (last_x, last_y); 451 convert_on_timeout (); 452 } 453 454 return draw_freehand; 455 }); 456 457 timer.attach (null); 458 } 459 460 /** @returns true while the mounse pointer is moving. */ 461 bool is_moving (int x, int y) { 462 return Path.distance (x, last_x, y, last_y) >= 1; 463 } 464 465 /** Add a new point if the update period has ended. */ 466 void convert_on_timeout () { 467 if (!is_moving (last_timer_x, last_timer_y)) { 468 update_cycles++; 469 } else { 470 last_timer_x = last_x; 471 last_timer_y = last_y; 472 update_cycles = 0; 473 } 474 475 if (update_cycles > 4) { // cycles of 100 ms 476 convert_points_to_line (); 477 last_update = get_current_time (); 478 add_corner (last_x, last_y); 479 added_points = 0; 480 update_cycles = 0; 481 } 482 483 if (added_points > 25 / samples_per_point) { 484 last_update = get_current_time (); 485 convert_points_to_line (); 486 } 487 } 488 489 /** Add a sharp corner instead of a smooth curve. */ 490 void add_corner (int px, int py) { 491 PointSelection p; 492 delete_last_points_at (px, py); 493 p = PenTool.add_new_edit_point (px, py); 494 p.point.set_tie_handle (false); 495 p.point.get_left_handle ().convert_to_line (); 496 p.point.get_right_handle ().convert_to_line (); 497 p.path.recalculate_linear_handles_for_point (p.point); 498 last_update = get_current_time (); 499 MainWindow.get_current_glyph ().update_view (); 500 } 501 502 Path get_active_path () { 503 Glyph glyph = MainWindow.get_current_glyph (); 504 505 if (glyph.active_paths.size == 0) { 506 warning ("No path."); 507 return new Path (); 508 } 509 510 Object o = glyph.active_paths.get (glyph.active_paths.size - 1); 511 512 if (likely (o is PathObject)) { 513 return ((PathObject) o).get_path (); 514 } 515 516 warning ("Active object is a path."); 517 518 return new Path (); 519 } 520 521 /** Delete all points close to the pixel at x,y. */ 522 void delete_last_points_at (int x, int y) { 523 double px, py; 524 Path p; 525 526 p = get_active_path (); 527 528 if (unlikely (p.points.size == 0)) { 529 warning ("Missing point."); 530 return; 531 } 532 533 px = Glyph.path_coordinate_x (x); 534 py = Glyph.path_coordinate_y (y); 535 536 while (p.points.size > 0 && is_close (p.points.get (p.points.size - 1), px, py)) { 537 p.delete_last_point (); 538 } 539 } 540 541 /** @return true if the new point point is closer than a few pixels from p. */ 542 bool is_close (EditPoint p, double x, double y) { 543 Glyph glyph = MainWindow.get_current_glyph (); 544 return glyph.view_zoom * Path.distance (p.x, x, p.y, y) < 5; 545 } 546 547 /** Take the average of tracked points and create a smooth line. 548 * @return the last removed point. 549 */ 550 public void convert_points_to_line () { 551 EditPoint ep, last_point; 552 double sum_x, sum_y, nx, ny; 553 int px, py; 554 EditPoint average, previous; 555 Path p; 556 Glyph glyph; 557 Gee.ArrayList<EditPoint> points; 558 559 points = new Gee.ArrayList<EditPoint> (); 560 glyph = MainWindow.get_current_glyph (); 561 var paths = glyph.get_visible_paths (); 562 563 if (paths.size == 0) { 564 warning ("No path."); 565 return; 566 } 567 568 p = paths.get (paths.size - 1); 569 570 if (added_points == 0) { // last point 571 return; 572 } 573 574 if (unlikely (p.points.size < added_points)) { 575 warning ("Missing point."); 576 return; 577 } 578 579 sum_x = 0; 580 sum_y = 0; 581 582 last_point = p.points.get (p.points.size - 1); 583 584 for (int i = 0; i < added_points; i++) { 585 ep = p.delete_last_point (); 586 sum_x += ep.x; 587 sum_y += ep.y; 588 points.add (ep); 589 } 590 591 nx = sum_x / added_points; 592 ny = sum_y / added_points; 593 594 px = Glyph.reverse_path_coordinate_x (nx); 595 py = Glyph.reverse_path_coordinate_y (ny); 596 average = PenTool.add_new_edit_point (px, py).point; 597 average.type = PointType.HIDDEN; 598 599 // tie handles for all points except for the end points 600 average.set_tie_handle (p.points.size > 1); 601 602 if (unlikely (p.points.size == 0)) { 603 warning ("No points."); 604 return; 605 } 606 607 if (average.prev != null && average.get_prev ().tie_handles) { 608 if (p.points.size > 2) { 609 previous = average.get_prev (); 610 previous.type = DrawingTools.point_type; 611 PenTool.convert_point_to_line (previous, true); 612 p.recalculate_linear_handles_for_point (previous); 613 previous.process_tied_handle (); 614 previous.set_tie_handle (false); 615 } 616 } 617 618 added_points = 0; 619 last_update = get_current_time (); 620 glyph.update_view (); 621 p.reset_stroke (); 622 } 623 624 /** @return current time in milli seconds. */ 625 public static double get_current_time () { 626 return GLib.get_real_time () / 1000.0; 627 } 628 } 629 630 } 631