The Birdfont Source Code


All Repositories / birdfont.git / blob – RSS feed

StrokeTool.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/StrokeTool.vala.
Suppress zoom when canvas is moving
1 /* 2 Copyright (C) 2014 2015 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 public class StrokeTool : Tool { 21 22 static bool stroke_selected = false; 23 static int iterations = 0; 24 25 public StrokeTool (string tooltip) { 26 iterations = 10; 27 select_action.connect((self) => { 28 stroke_selected = true; 29 iterations++; 30 stroke_selected_paths (); 31 stroke_selected = false; 32 }); 33 } 34 35 public static void set_stroke_for_selected_paths (double width) { 36 Glyph g = MainWindow.get_current_glyph (); 37 38 foreach (Path p in g.active_paths) { 39 p.set_stroke (width); 40 } 41 42 GlyphCanvas.redraw (); 43 } 44 45 /** Create strokes for the selected outlines. */ 46 void stroke_selected_paths () { 47 Glyph g = MainWindow.get_current_glyph (); 48 PathList paths = new PathList (); 49 50 foreach (Path p in g.active_paths) { 51 paths.append (get_stroke (p, p.stroke)); 52 } 53 54 foreach (Path np in paths.paths) { 55 g.add_path (np); 56 } 57 } 58 59 public static PathList get_stroke (Path path, double thickness) { 60 PathList pl = new PathList (); 61 PathList parts; 62 Path original = path.copy (); 63 64 original.remove_points_on_points (); 65 66 // split self intersecting paths before interpolating 67 parts = get_parts (original); 68 69 foreach (Path p in parts.paths) { 70 p.get_first_point ().color = new Color (0, 1, 0, 1); 71 p.get_last_point ().color = new Color (0, 0, 0, 1); 72 pl.append (get_stroke_outline (p, thickness)); 73 } 74 75 pl = merge (pl); 76 77 return pl; 78 } 79 80 public static PathList get_stroke_outline (Path path, double thickness) { 81 PathList pl = new PathList (); 82 83 foreach (Path p in get_parts (path).paths) { 84 pl.append (get_strokes (p, thickness)); 85 } 86 87 return pl; 88 } 89 90 public static PathList get_strokes (Path p, double thickness) { 91 Path counter, outline; 92 Path merged; 93 PathList paths = new PathList (); 94 PathList parts; 95 96 if (!p.is_open () && p.is_filled ()) { 97 outline = create_stroke (p, thickness); 98 outline.close (); 99 100 parts = remove_intersections (outline, thickness, p); 101 parts = merge (parts); 102 103 foreach (Path sp in parts.paths) { 104 paths.add (sp); 105 sp.update_region_boundaries (); 106 } 107 } else if (!p.is_open () && !p.is_filled ()) { 108 outline = create_stroke (p, thickness); 109 counter = create_stroke (p, -1 * thickness); 110 111 if (p.is_clockwise ()) { 112 outline.force_direction (Direction.CLOCKWISE); 113 } else { 114 outline.force_direction (Direction.COUNTER_CLOCKWISE); 115 } 116 117 if (outline.is_clockwise ()) { 118 counter.force_direction (Direction.COUNTER_CLOCKWISE); 119 } else { 120 counter.force_direction (Direction.CLOCKWISE); 121 } 122 123 parts = remove_intersections (outline, thickness, p); 124 foreach (Path sp in parts.paths) { 125 paths.add (sp); 126 sp.update_region_boundaries (); 127 } 128 129 parts = remove_intersections (counter, thickness, p); 130 foreach (Path sp in parts.paths) { 131 paths.add (sp); 132 sp.update_region_boundaries (); 133 } 134 135 } else if (p.is_open ()) { // FIXME: this can create many parts 136 outline = create_stroke (p, thickness); 137 counter = create_stroke (p, -1 * thickness); 138 merged = merge_strokes (p, outline, counter, thickness); 139 140 if (p.is_clockwise ()) { 141 merged.force_direction (Direction.CLOCKWISE); 142 } else { 143 merged.force_direction (Direction.COUNTER_CLOCKWISE); 144 } 145 146 parts = remove_intersections (merged, thickness, p); 147 parts = merge (parts); 148 149 foreach (Path sp in parts.paths) { 150 paths.add (sp); 151 sp.update_region_boundaries (); 152 } 153 } else { 154 warning ("Can not create stroke."); 155 paths.add (p); 156 } 157 158 return paths; 159 } 160 161 /** Create one stroke from the outline and counter stroke and close the 162 * open endings. 163 * 164 * @param path the path to create stroke for 165 * @param stroke for the outline of path 166 * @param stroke for the counter path 167 */ 168 static Path merge_strokes (Path path, Path stroke, Path counter, double thickness) { 169 Path merged; 170 171 counter.reverse (); 172 merged = stroke.copy (); 173 174 if (path.is_open ()) { 175 merged.delete_last_point (); 176 counter.delete_first_point (); 177 merged.delete_last_point (); 178 counter.delete_first_point (); 179 } 180 181 merged.append_path (counter); 182 183 merged.close (); 184 merged.create_list (); 185 merged.recalculate_linear_handles (); 186 187 return merged; 188 } 189 190 static Path create_stroke (Path p, double thickness) { 191 Path stroked; 192 Path path; 193 194 if (p.points.size >= 2) { 195 path = p.copy (); 196 path.remove_points_on_points (); 197 stroked = generate_stroke (path, thickness); 198 199 if (!p.is_open ()) { 200 stroked.reverse (); 201 stroked.close (); 202 } 203 } else { 204 // TODO: create stroke for a path with one point 205 warning ("One point."); 206 stroked = new Path (); 207 } 208 209 return stroked; 210 } 211 212 static Path generate_stroke (Path p, double thickness) { 213 Path stroked = new Path (); 214 EditPoint start = new EditPoint (); 215 EditPoint end; 216 EditPoint previous; 217 int i; 218 219 previous = p.get_first_point ().copy (); 220 move_segment (start, previous, thickness); 221 222 i = 0; 223 foreach (EditPoint ep in p.points) { 224 start = ep.copy (); 225 end = ep.get_next ().copy (); 226 227 move_segment (start, end, thickness); 228 229 if (start == p.get_last_point ()) { 230 end = p.get_first_point (); 231 } 232 233 if (!p.is_open () || (i != 0 && i != p.points.size - 1)) { 234 add_corner (stroked, previous, start, ep.copy (), thickness); 235 } 236 237 stroked.add_point (start); 238 239 if (end.get_left_handle ().length > 0) { 240 stroked.add_point (end); 241 } 242 243 // open ends around corner 244 start.get_left_handle ().convert_to_line (); 245 end.get_right_handle ().convert_to_line (); 246 247 previous = end; 248 249 i++; 250 } 251 252 stroked.recalculate_linear_handles (); 253 254 return stroked; 255 } 256 257 static void move_segment (EditPoint stroke_start, EditPoint stroke_stop, double thickness) { 258 EditPointHandle r, l; 259 double m, n; 260 double qx, qy; 261 262 stroke_start.set_tie_handle (false); 263 stroke_stop.set_tie_handle (false); 264 265 r = stroke_start.get_right_handle (); 266 l = stroke_stop.get_left_handle (); 267 268 m = cos (r.angle + PI / 2) * thickness; 269 n = sin (r.angle + PI / 2) * thickness; 270 271 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n); 272 stroke_start.get_left_handle ().move_to_coordinate_delta (m, n); 273 274 stroke_start.independent_x += m; 275 stroke_start.independent_y += n; 276 277 qx = cos (l.angle - PI / 2) * thickness; 278 qy = sin (l.angle - PI / 2) * thickness; 279 280 stroke_stop.get_right_handle ().move_to_coordinate_delta (qx, qy); 281 stroke_stop.get_left_handle ().move_to_coordinate_delta (qx, qy); 282 283 stroke_stop.independent_x += qx; 284 stroke_stop.independent_y += qy; 285 } 286 287 static void add_corner (Path stroked, EditPoint previous, EditPoint next, 288 EditPoint original, double stroke_width) { 289 290 double ratio; 291 double distance; 292 EditPoint corner; 293 double corner_x, corner_y; 294 EditPointHandle previous_handle; 295 EditPointHandle next_handle; 296 EditPoint cutoff1, cutoff2; 297 298 previous_handle = previous.get_left_handle (); 299 next_handle = next.get_right_handle (); 300 301 previous_handle.angle += PI; 302 next_handle.angle += PI; 303 304 Path.find_intersection_handle (previous_handle, next_handle, out corner_x, out corner_y); 305 corner = new EditPoint (corner_x, corner_y, previous.type); 306 corner.convert_to_line (); 307 308 previous_handle.angle -= PI; 309 next_handle.angle -= PI; 310 311 distance = Path.distance_to_point (corner, original); 312 ratio = fabs (stroke_width) / distance; 313 314 if (ratio > 1) { 315 stroked.add_point (corner); 316 } else { 317 cutoff1 = new EditPoint (); 318 cutoff1.set_point_type (previous.type); 319 cutoff1.convert_to_line (); 320 321 cutoff2 = new EditPoint (); 322 cutoff2.set_point_type (previous.type); 323 cutoff2.convert_to_line (); 324 325 cutoff1.x = previous.x + (corner.x - previous.x) * ratio; 326 cutoff1.y = previous.y + (corner.y - previous.y) * ratio; 327 328 cutoff2.x = next.x + (corner.x - next.x) * ratio; 329 cutoff2.y = next.y + (corner.y - next.y) * ratio; 330 331 cutoff1 = stroked.add_point (cutoff1); 332 cutoff2 = stroked.add_point (cutoff2); 333 334 cutoff1.recalculate_linear_handles (); 335 cutoff2.recalculate_linear_handles (); 336 } 337 } 338 339 static PathList remove_intersections (Path path, double thickness, Path original) { 340 PathList parts; 341 342 parts = get_parts (path); 343 delete_intersection_parts (original, parts, thickness); 344 345 return parts; 346 } 347 348 static PathList get_parts (Path path) { 349 PathList intersections; 350 PathList pli; 351 PathList r; 352 Path np; 353 354 r = get_parts_self (path); 355 intersections = new PathList (); 356 357 foreach (Path p in r.paths) { 358 intersections.add (p); 359 } 360 361 foreach (Path p1 in r.paths) { 362 foreach (Path p2 in r.paths) { 363 if (merge_path (p1, p2, out np)) { 364 intersections.append (get_parts (np)); 365 } 366 } 367 } 368 return intersections; 369 } 370 371 static PathList get_parts_self (Path path, PathList? paths = null) { 372 PathList pl; 373 PathList r; 374 375 r = paths == null ? new PathList () : (!) paths; 376 pl = split (path); 377 378 foreach (Path part in pl.paths) { 379 if (!has_self_intersection (part)) { 380 r.add (part); 381 } else { 382 get_parts_self (part, r); 383 } 384 } 385 386 if (r.paths.size == 0) { 387 warning ("No parts in path"); 388 } 389 390 return r; 391 } 392 393 static bool has_intersection_points (Path path) { 394 foreach (EditPoint p in path.points) { 395 if ((p.flags & EditPoint.INTERSECTION) > 0) { 396 return true; 397 } 398 } 399 return false; 400 } 401 402 /** Split one path at intersection points in two parts. */ 403 static PathList split (Path path) { 404 PathList pl; 405 int i; 406 407 if (!has_intersection_points (path)) { 408 add_self_intersection_points (path); 409 } else { 410 warning ("points already created."); 411 } 412 413 i = mark_intersection_as_deleted (path); 414 415 if (!(i == 0 || i == 2)) { 416 warning (@"Split should only create two parts, $i points will be deleted."); 417 } 418 419 pl = get_remaining_points (path.copy ()); 420 421 return pl; 422 } 423 424 static PathList process_deleted_control_points (Path path) { 425 PathList paths, nl, pl, rl; 426 427 paths = new PathList (); 428 rl = new PathList (); 429 pl = new PathList (); 430 nl = new PathList (); 431 432 if (!path.has_deleted_point ()) { 433 return pl; 434 } 435 436 pl.add (path); 437 438 foreach (Path p in pl.paths) { 439 nl = p.process_deleted_points (); 440 441 if (nl.paths.size > 0) { 442 rl.append (nl); 443 rl.paths.remove (p); 444 } 445 } 446 447 foreach (Path p in rl.paths) { 448 pl = process_deleted_control_points (p); 449 450 if (pl.paths.size > 0) { 451 paths.append (pl); 452 } else { 453 paths.add (p); 454 } 455 } 456 457 return paths; 458 } 459 460 static PathList get_remaining_points (Path old_path) { 461 PathList pl; 462 463 old_path.close (); 464 pl = process_deleted_control_points (old_path); 465 466 if (pl.paths.size == 0) { 467 pl.add (old_path); 468 } 469 470 foreach (Path pn in pl.paths) { 471 pn.close (); 472 } 473 474 return pl; 475 } 476 477 static bool has_self_intersection (Path path) { 478 bool intersection = false; 479 480 path.all_segments ((ep1, ep2) => { 481 double ix, iy; 482 EditPoint p1, p2; 483 484 if (segment_intersects (path, ep1, ep2, out ix, out iy, out p1, out p2)) { 485 intersection = true; 486 return false; 487 } 488 489 return true; 490 }); 491 492 return intersection; 493 } 494 495 static bool add_self_intersection_points (Path path) { 496 bool intersection = false; 497 498 path.get_first_point ().color = new Color (0, 1, 0, 1); 499 500 path.all_segments ((ep1, ep2) => { 501 double ix, iy; 502 EditPoint p1, p2; 503 504 if (segment_intersects (path, ep1, ep2, out ix, out iy, out p1, out p2)) { 505 add_intersection (path, ep1, ep2, ix, iy); 506 add_intersection (path, p1, p2, ix, iy); 507 intersection = true; 508 return false; 509 } 510 511 return true; 512 }); 513 514 if (intersection) { 515 // FIXME: path.create_list (); 516 } 517 518 return intersection; 519 } 520 521 static void add_intersection (Path path, EditPoint prev, EditPoint next, double px, double py, Color? c = null) { 522 Gee.ArrayList<EditPoint> n = new Gee.ArrayList<EditPoint> (); 523 EditPoint ep1 = new EditPoint (); 524 EditPoint ep2 = new EditPoint (); 525 EditPoint ep3 = new EditPoint (); 526 527 if (next == path.get_first_point ()) { // FIXME: double check 528 ep1.prev = null; 529 } else { 530 ep1.prev = prev; 531 } 532 533 ep1.prev = prev; 534 ep1.next = ep2; 535 ep1.flags |= EditPoint.NEW_CORNER; 536 ep1.type = PointType.CUBIC; 537 ep1.x = px; 538 ep1.y = py; 539 ep1.color = c; 540 n.add (ep1); 541 542 ep2.prev = ep1; 543 ep2.next = ep3; 544 ep2.flags |= EditPoint.INTERSECTION; 545 ep2.type = PointType.QUADRATIC; 546 ep2.x = px; 547 ep2.y = py; 548 ep2.color = c; 549 n.add (ep2); 550 551 ep3.prev = ep2; 552 ep3.next = next; 553 ep3.flags |= EditPoint.NEW_CORNER; 554 ep3.type = PointType.CUBIC; 555 ep3.x = px; 556 ep3.y = py; 557 ep3.color = c; 558 n.add (ep3); 559 560 foreach (EditPoint np in n) { 561 np = path.add_point_after (np, np.prev); 562 path.create_list (); 563 } 564 565 path.recalculate_linear_handles (); 566 } 567 568 static bool segment_intersects (Path path, EditPoint ep, EditPoint next, 569 out double ix, out double iy, 570 out EditPoint ia, out EditPoint ib) { 571 EditPoint p1, p2; 572 double cross_x, cross_y; 573 574 ix = 0; 575 iy = 0; 576 577 ia = new EditPoint (); 578 ib = new EditPoint (); 579 580 if (path.points.size == 0) { 581 return false; 582 } 583 584 p1 = path.points.get (path.points.size - 1); 585 for (int i = 0; i < path.points.size; i++) { 586 p2 = path.points.get (i); 587 588 Path.find_intersection_point (ep, next, p1, p2, out cross_x, out cross_y); 589 590 if (Glyph.CANVAS_MIN < cross_x < Glyph.CANVAS_MAX 591 && Glyph.CANVAS_MIN < cross_y < Glyph.CANVAS_MAX) { 592 // iterate to find intersection. 593 594 if (!((ep.x == cross_x && ep.y == cross_y) 595 || (next.x == cross_x && next.y == cross_y) 596 || (p1.x == cross_x && p1.y == cross_y) 597 || (p2.x == cross_x && p2.y == cross_y))) { 598 599 if (is_line (ep.x, ep.y, cross_x, cross_y, next.x, next.y) 600 && is_line (p1.x, p1.y, cross_x, cross_y, p2.x, p2.y)) { 601 602 ep.color = new Color (1, 0, 0, 1); 603 next.color = new Color (0.5, 0, 0, 1); 604 605 p1.color = new Color (0, 0, 1, 1); 606 p2.color = new Color (0, 0, 0.5, 1); 607 608 ix = cross_x; 609 iy = cross_y; 610 611 ia = p1; 612 ib = p2; 613 614 return true; 615 } 616 } 617 } 618 619 p1 = p2; 620 } 621 622 return false; 623 } 624 625 /** @return true if p2 is on the line p1 to p3 */ 626 static bool is_line (double x1, double y1, double x2, double y2, double x3, double y3) { 627 double ds = Path.distance (x1, x3, y1, y3); 628 double d1 = Path.distance (x1, x2, y1, y2); 629 double d2 = Path.distance (x2, x3, y2, y3); 630 double p = d1 / ds; 631 double x = fabs ((x3 - x1) * p - (x2 - x1)); 632 double y = fabs ((y3 - y1) * p - (y2 - y1)); 633 double d = fabs (ds - (d1 + d2)); 634 635 // FIXME: delete print (@"$(fmin (x1, x3)) < $x2 && $x2 < $(fmax (x1, x3))\n"); 636 // FIXME: delete print (@"$(fmin (y1, y3)) < $y2 && $y2 < $(fmax (y1, y3))\n"); 637 638 return ds > 0.01 && d1 > 0.01 && d2 > 0.01 639 && d < 0.01 && x < 0.01 && y < 0.01 640 && fmin (x1, x3) <= x2 && x2 <= fmax (x1, x3) 641 && fmin (y1, y3) <= y2 && y2 <= fmax (y1, y3); 642 } 643 644 static void delete_intersection_parts (Path original, PathList parts, double stroke_width) { 645 PathList remove = new PathList (); 646 647 foreach (Path p in parts.paths) { 648 649 if (stroke_selected) { // FIXME: DELETE 650 ((!) BirdFont.get_current_font ().get_glyph ("a")).add_path (p); 651 } 652 653 if (is_stroke (original, p, stroke_width)) { 654 remove.add (p); 655 } 656 } 657 658 foreach (Path p in remove.paths) { 659 parts.paths.remove (p); 660 } 661 } 662 663 /** @return true if the part is inside of the stroke of the path */ 664 static bool is_stroke (Path original, Path part, double stroke_width) { 665 double stroke_size = fabs (stroke_width); 666 bool stroke = false; 667 668 original.all_of_path ((cx, cy, ct) => { 669 foreach (EditPoint p in part.points) { 670 if (Path.distance (cx, p.x, cy, p.y) < stroke_size - 0.5) { 671 p.color = new Color (1, 0, 1, 1); // FIXME: DELETE 672 stroke = true; 673 return false; 674 } else { 675 p.color = new Color (1, 1, 1, 1); // FIXME: DELETE 676 } 677 } 678 679 return true; 680 }, 12); 681 682 return stroke; 683 } 684 685 static Path get_outline (Path path) { 686 PathList pl = get_parts (path); 687 Path outline = new Path (); 688 int inside; 689 int min_inside = int.MAX; 690 int points = 0; 691 int i = 0; 692 693 foreach (Path p in pl.paths) { 694 inside = Path.counters (pl, p); 695 696 if (inside < min_inside) { 697 outline = p; 698 min_inside = inside; 699 } 700 701 if (stroke_selected) { // FIXME: DELETE 702 ((!) BirdFont.get_current_font ().get_glyph ("a")).add_path (p); 703 } 704 i++; 705 } 706 707 if (min_inside == 0) { 708 foreach (Path p in pl.paths) { 709 if (p.points.size > points) { 710 outline = p; 711 points = p.points.size; 712 } 713 } 714 } 715 716 return outline; 717 } 718 719 static PathList merge (PathList pl) { 720 Path merged; 721 int i, j; 722 723 i = 0; 724 foreach (Path p1 in pl.paths) { 725 j = 0; 726 foreach (Path p2 in pl.paths) { 727 if (merge_path (p1, p2, out merged)) { 728 pl.paths.remove (p1); 729 pl.paths.remove (p2); 730 731 merged = get_outline (merged); 732 pl.add (merged); 733 734 return merge (pl); 735 } 736 j++; 737 } 738 i++; 739 } 740 741 return pl; 742 } 743 744 static int mark_intersection_as_deleted (Path path) { 745 int i = 0; 746 747 foreach (EditPoint p in path.points) { 748 if ((p.flags & EditPoint.INTERSECTION) > 0) { 749 p.deleted = true; 750 i++; 751 // FIXME: delete p.color = new Color (1, 1, 0, 1); 752 } 753 } 754 755 return i; 756 } 757 758 /** @return true if the two paths can be merged. */ 759 static bool merge_path (Path path1, Path path2, out Path merged) { 760 PathList pl1, pl2; 761 Path p1, p2; 762 int i; 763 764 merged = new Path (); 765 766 if (path1 == path2) { 767 return false; 768 } 769 770 if (add_intersection_points (path1, path2)) { 771 i = mark_intersection_as_deleted (path1); 772 return_if_fail (i == 1); 773 774 i = mark_intersection_as_deleted (path2); 775 return_if_fail (i == 1); 776 777 pl1 = get_remaining_points (path1.copy ()); 778 pl2 = get_remaining_points (path2.copy ()); 779 780 return_if_fail (pl1.paths.size == 1); 781 return_if_fail (pl2.paths.size == 1); 782 783 p1 = pl1.paths.get (0); 784 p2 = pl2.paths.get (0); 785 786 merged = PenTool.merge_open_paths (p1, p2); 787 788 return true; 789 } 790 791 return false; 792 } 793 794 static bool add_intersection_points (Path path1, Path path2) { 795 bool intersection = false; 796 797 path1.all_segments ((ep1, ep2) => { 798 double ix, iy; 799 EditPoint p1, p2; 800 801 if (segment_intersects (path2, ep1, ep2, out ix, out iy, out p1, out p2)) { 802 add_intersection (path1, ep1, ep2, ix, iy); 803 add_intersection (path2, p1, p2, ix, iy); 804 intersection = true; 805 return false; 806 } 807 808 return true; 809 }); 810 811 return intersection; 812 } 813 814 } 815 816 } 817