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