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