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