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