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