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
Circle boundaries heads/master
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 (Object path in glyph.active_paths) { 128 if (path is PathObject) { 129 // cache merged stroke parts 130 ((PathObject) path).get_path ().create_full_stroke (); 131 } 132 } 133 } 134 }); 135 136 double_click_action.connect ((self, b, x, y) => { 137 }); 138 139 release_action.connect ((self, button, x, y) => { 140 Path p; 141 Glyph g = MainWindow.get_current_glyph (); 142 EditPoint previous; 143 144 if (button == 1) { 145 if (!draw_freehand) { 146 warning ("Not drawing."); 147 return; 148 } 149 150 convert_points_to_line (); 151 152 g = MainWindow.get_current_glyph (); 153 154 if (g.active_paths.size > 0) { // set type for last point 155 Object o = g.active_paths.get (g.active_paths.size - 1); 156 p = ((PathObject) o).get_path (); 157 158 if (p.points.size > 1) { 159 previous = p.points.get (p.points.size - 1); 160 previous.type = PointType.CUBIC; 161 previous.set_tie_handle (false); 162 163 previous = p.points.get (0); 164 previous.type = PointType.CUBIC; 165 previous.set_tie_handle (false); 166 } 167 } 168 169 if (button == 1 && draw_freehand) { 170 return_if_fail (drawing); 171 add_endpoint_and_merge (x, y); 172 } 173 174 foreach (Object path in g.active_paths) { 175 if (path is PathObject) { 176 Path freehand_path = ((PathObject) path).get_path (); 177 convert_hidden_points (freehand_path); 178 freehand_path.update_region_boundaries (); 179 } 180 } 181 182 g.clear_active_paths (); 183 184 set_tie (); 185 PenTool.force_direction (); 186 PenTool.reset_stroke (); 187 BirdFont.get_current_font ().touch (); 188 drawing = false; 189 } 190 }); 191 192 move_action.connect ((self, x, y) => { 193 PointSelection? open_path = get_path_with_end_point (x, y); 194 PointSelection p; 195 bool join; 196 197 join = (open_path != null); 198 199 if (join != join_paths) { 200 MainWindow.get_current_glyph ().update_view (); 201 PenTool.reset_stroke (); 202 } 203 204 join_paths = join; 205 206 if (open_path != null) { 207 p = (!) open_path; 208 join_x = Glyph.reverse_path_coordinate_x (p.point.x); 209 join_y = Glyph.reverse_path_coordinate_y (p.point.y); 210 } 211 212 if (draw_freehand) { 213 record_new_position (x, y); 214 convert_on_timeout (); 215 last_x = x; 216 last_y = y; 217 PenTool.reset_stroke (); 218 } 219 }); 220 221 draw_action.connect ((tool, cairo_context, glyph) => { 222 if (join_paths) { 223 PenTool.draw_join_icon (cairo_context, join_x, join_y); 224 } 225 }); 226 227 key_press_action.connect ((self, keyval) => { 228 }); 229 } 230 231 void convert_hidden_points (Path p) { 232 foreach (EditPoint e in p.points) { 233 if (e.type == PointType.HIDDEN) { 234 e.type = DrawingTools.point_type; 235 e.get_right_handle ().type = PointType.CUBIC; 236 e.get_left_handle ().type = PointType.CUBIC; 237 } 238 } 239 } 240 241 void set_tie () { 242 Glyph glyph = MainWindow.get_current_glyph (); 243 var paths = glyph.get_visible_paths (); 244 Path p; 245 246 if (paths.size == 0) { 247 return; 248 } 249 250 p = paths.get (paths.size - 1); 251 252 foreach (EditPoint ep in p.points) { 253 if (ep.get_right_handle ().is_line () || ep.get_left_handle ().is_line ()) { 254 ep.set_tie_handle (false); 255 } 256 257 if (!ep.get_right_handle ().is_line () || !ep.get_left_handle ().is_line ()) { 258 ep.convert_to_curve (); 259 } 260 } 261 } 262 263 public void set_samples_per_point (double s) { 264 samples_per_point = s; 265 } 266 267 void add_endpoint_and_merge (int x, int y) { 268 Glyph glyph; 269 Path p; 270 PointSelection? open_path = get_path_with_end_point (x, y); 271 PointSelection joined_path; 272 273 glyph = MainWindow.get_current_glyph (); 274 var paths = glyph.get_visible_paths (); 275 276 if (paths.size == 0) { 277 warning ("No path."); 278 return; 279 } 280 281 // FIXME: double check this 282 p = paths.get (paths.size - 1); 283 draw_freehand = false; 284 285 convert_points_to_line (); 286 287 if (join_paths && open_path != null) { 288 joined_path = (!) open_path; 289 290 if (joined_path.path == p) { 291 delete_last_points_at (x, y); 292 glyph.close_path (); 293 p.close (); 294 } else { 295 p = merge_paths (p, joined_path); 296 if (!p.is_open ()) { 297 glyph.close_path (); 298 } 299 } 300 301 glyph.clear_active_paths (); 302 } else { 303 add_corner (x, y); 304 } 305 306 if (p.points.size == 0) { 307 warning ("No point."); 308 return; 309 } 310 311 EditPoint last_point = p.get_last_point (); 312 EditPointHandle handle = last_point.get_right_handle (); 313 handle.convert_to_line (); 314 p.recalculate_linear_handles (); 315 316 PenTool.convert_point_type (p.get_last_point (), PointType.CUBIC); 317 PenTool.convert_point_type (p.get_first_point (), PointType.CUBIC); 318 319 p.create_list (); 320 321 if (PenTool.is_counter_path (p)) { 322 p.force_direction (Direction.COUNTER_CLOCKWISE); 323 } else { 324 p.force_direction (Direction.CLOCKWISE); 325 } 326 327 glyph.update_view (); 328 } 329 330 private static Path merge_paths (Path a, PointSelection b) { 331 Glyph g; 332 Path merged = a.copy (); 333 334 if (a.points.size < 2) { 335 warning ("Less than two points in path."); 336 return merged; 337 } 338 339 if (b.path.points.size < 2) { 340 warning ("Less than two points in path."); 341 return merged; 342 } 343 344 if (!b.is_first ()) { 345 b.path.close (); 346 b.path.reverse (); 347 b.path.reopen (); 348 } 349 350 merged.append_path (b.path); 351 352 g = MainWindow.get_current_glyph (); 353 g.add_path (merged); 354 a.delete_last_point (); 355 356 update_corner_handle (a.get_last_point (), b.path.get_first_point ()); 357 358 g.delete_path (a); 359 g.delete_path (b.path); 360 361 merged.create_list (); 362 merged.update_region_boundaries (); 363 merged.recalculate_linear_handles (); 364 merged.reopen (); 365 366 return merged; 367 } 368 369 public static void update_corner_handle (EditPoint end, EditPoint new_start) { 370 EditPointHandle h1, h2; 371 372 h1 = end.get_right_handle (); 373 h2 = new_start.get_left_handle (); 374 375 h1.convert_to_line (); 376 h2.convert_to_line (); 377 } 378 379 PointSelection? get_path_with_end_point (int x, int y) { 380 Glyph glyph = MainWindow.get_current_glyph (); 381 EditPoint e; 382 EditPoint current_end = new EditPoint (); 383 384 // exclude the end point on the path we are adding points to 385 if (draw_freehand) { 386 current_end = get_active_path ().get_last_point (); 387 } 388 389 foreach (Path p in glyph.get_visible_paths ()) { 390 if (p.is_open () && p.points.size > 2) { 391 e = p.points.get (0); 392 if (PenTool.is_close_to_point (e, x, y)) { 393 return new PointSelection (e, p); 394 } 395 396 e = p.points.get (p.points.size - 1); 397 if (current_end != e && PenTool.is_close_to_point (e, x, y)) { 398 return new PointSelection (e, p); 399 } 400 } 401 } 402 403 return null; 404 } 405 406 void record_new_position (int x, int y) { 407 Glyph glyph; 408 Path p; 409 EditPoint new_point; 410 double px, py; 411 412 glyph = MainWindow.get_current_glyph (); 413 414 if (glyph.active_paths.size == 0) { 415 warning ("No path."); 416 return; 417 } 418 419 Object o = glyph.active_paths.get (glyph.active_paths.size - 1); 420 421 if (unlikely (!(o is PathObject))) { 422 warning ("Object is not a path"); 423 return; 424 } 425 426 p = ((PathObject) o).get_path (); 427 p.reopen (); 428 429 EditPoint last_point = new EditPoint (); 430 431 if (p.points.size > 0) { 432 last_point = p.get_last_point (); 433 } 434 435 px = Glyph.path_coordinate_x (x); 436 py = Glyph.path_coordinate_y (y); 437 438 new_point = new EditPoint (px, py, PointType.CUBIC); 439 p.add_point (new_point); 440 added_points++; 441 442 PenTool.convert_point_to_line (new_point, false); 443 444 new_point.set_point_type (PointType.HIDDEN); 445 p.recalculate_linear_handles_for_point (new_point); 446 447 last_point.get_right_handle ().length = 0.000001; 448 449 if (p.points.size > 1) { 450 glyph.redraw_segment (new_point, new_point.get_prev ()); 451 } 452 453 glyph.update_view (); 454 455 last_x = x; 456 last_y = y; 457 } 458 459 void start_update_timer () { 460 TimeoutSource timer = new TimeoutSource (100); 461 462 timer.set_callback (() => { 463 if (draw_freehand) { 464 record_new_position (last_x, last_y); 465 convert_on_timeout (); 466 } 467 468 return draw_freehand; 469 }); 470 471 timer.attach (null); 472 } 473 474 /** @returns true while the mounse pointer is moving. */ 475 bool is_moving (int x, int y) { 476 return Path.distance (x, last_x, y, last_y) >= 1; 477 } 478 479 /** Add a new point if the update period has ended. */ 480 void convert_on_timeout () { 481 if (!is_moving (last_timer_x, last_timer_y)) { 482 update_cycles++; 483 } else { 484 last_timer_x = last_x; 485 last_timer_y = last_y; 486 update_cycles = 0; 487 } 488 489 if (update_cycles > 0.7 * 10) { // delay in time 490 convert_points_to_line (); 491 last_update = get_current_time (); 492 add_corner (last_x, last_y); 493 added_points = 0; 494 update_cycles = 0; 495 } 496 497 if (added_points > 80 / samples_per_point) { 498 last_update = get_current_time (); 499 convert_points_to_line (); 500 } 501 } 502 503 /** Add a sharp corner instead of a smooth curve. */ 504 void add_corner (double px, double py) { 505 EditPoint p; 506 p = new EditPoint (px, py, PointType.CUBIC); 507 p.set_tie_handle (false); 508 p.get_left_handle ().convert_to_line (); 509 p.get_right_handle ().convert_to_line (); 510 get_active_path ().recalculate_linear_handles_for_point (p); 511 last_update = get_current_time (); 512 MainWindow.get_current_glyph ().update_view (); 513 } 514 515 Path get_active_path () { 516 Glyph glyph = MainWindow.get_current_glyph (); 517 518 if (glyph.active_paths.size == 0) { 519 warning ("No path."); 520 return new Path (); 521 } 522 523 Object o = glyph.active_paths.get (glyph.active_paths.size - 1); 524 525 if (likely (o is PathObject)) { 526 return ((PathObject) o).get_path (); 527 } 528 529 warning ("Active object is a path."); 530 531 return new Path (); 532 } 533 534 /** Delete all points close to the pixel at x,y. */ 535 void delete_last_points_at (double px, double py) { 536 Path p; 537 538 p = get_active_path (); 539 540 if (unlikely (p.points.size == 0)) { 541 warning ("Missing point."); 542 return; 543 } 544 545 while (p.points.size > 0 && is_close (p.points.get (p.points.size - 1), px, py)) { 546 p.delete_last_point (); 547 } 548 } 549 550 /** @return true if the new point point is closer than a few pixels from p. */ 551 bool is_close (EditPoint p, double x, double y) { 552 Glyph glyph = MainWindow.get_current_glyph (); 553 return glyph.view_zoom * Path.distance (p.x, x, p.y, y) < 5; 554 } 555 556 /** Take the average of tracked points and create a smooth line. 557 * @return the last removed point. 558 */ 559 public void convert_points_to_line () { 560 Path p; 561 Glyph glyph; 562 Gee.ArrayList<EditPoint> points; 563 564 points = new Gee.ArrayList<EditPoint> (); 565 glyph = MainWindow.get_current_glyph (); 566 var paths = glyph.get_visible_paths (); 567 568 if (paths.size == 0) { 569 warning ("No path."); 570 return; 571 } 572 573 p = paths.get (paths.size - 1); 574 575 if (added_points == 0) { // last point 576 return; 577 } 578 579 if (unlikely (p.points.size < added_points)) { 580 warning ("Missing point."); 581 return; 582 } 583 584 int start = p.points.size - 1 - added_points; 585 int stop = p.points.size - 1; 586 587 EditPoint end = p.points.get (stop); 588 589 Path segment = StrokeTool.fit_bezier_path (p, start, stop, 5.0 / samples_per_point); 590 591 for (int i = 0; i < added_points; i++) { 592 p.delete_last_point (); 593 } 594 595 p.append_path (segment); 596 p.remove_points_on_points (); 597 598 add_corner (end.x, end.y); 599 600 added_points = 0; 601 last_update = get_current_time (); 602 glyph.update_view (); 603 p.reset_stroke (); 604 } 605 606 /** @return current time in milli seconds. */ 607 public static double get_current_time () { 608 return GLib.get_real_time () / 1000.0; 609 } 610 } 611 612 } 613