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.
Use other types of graphical objects
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