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