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