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