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