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.
Add corner on offset in stroke tool
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 public class StrokeTool : Tool { 21 22 public StrokeTool (string tooltip) { 23 select_action.connect((self) => { 24 stroke_selected_paths (); 25 }); 26 } 27 28 public static void set_stroke_for_selected_paths (double width) { 29 Glyph g = MainWindow.get_current_glyph (); 30 31 foreach (Path p in g.active_paths) { 32 p.set_stroke (width); 33 } 34 35 GlyphCanvas.redraw (); 36 } 37 38 /** Create strokes for the selected outlines. */ 39 void stroke_selected_paths () { 40 Glyph g = MainWindow.get_current_glyph (); 41 PathList paths = new PathList (); 42 43 foreach (Path p in g.active_paths) { 44 paths.append (get_stroke (p, p.stroke)); 45 } 46 47 foreach (Path np in paths.paths) { 48 g.add_path (np); 49 } 50 } 51 52 public static PathList get_stroke (Path path, double thickness) { 53 Path p = path.copy (); 54 PathList pl; 55 56 pl = get_stroke_outline (p, thickness); 57 58 return pl; 59 } 60 61 public static PathList get_stroke_outline (Path p, double thickness) { 62 Path counter, outline, merged; 63 PathList paths = new PathList (); 64 65 if (!p.is_open () && p.is_filled ()) { 66 outline = create_stroke (p, thickness); 67 outline.close (); 68 paths.add (outline); 69 outline.update_region_boundaries (); 70 } else if (!p.is_open () && !p.is_filled ()) { 71 outline = create_stroke (p, thickness); 72 counter = create_stroke (p, -1 * thickness); 73 74 paths.add (outline); 75 paths.add (counter); 76 77 if (p.is_clockwise ()) { 78 outline.force_direction (Direction.CLOCKWISE); 79 } else { 80 outline.force_direction (Direction.COUNTER_CLOCKWISE); 81 } 82 83 if (outline.is_clockwise ()) { 84 counter.force_direction (Direction.COUNTER_CLOCKWISE); 85 } else { 86 counter.force_direction (Direction.CLOCKWISE); 87 } 88 89 outline.update_region_boundaries (); 90 counter.update_region_boundaries (); 91 } else if (p.is_open ()) { 92 outline = create_stroke (p, thickness); 93 counter = create_stroke (p, -1 * thickness); 94 merged = merge_strokes (p, outline, counter, thickness); 95 96 if (p.is_clockwise ()) { 97 merged.force_direction (Direction.CLOCKWISE); 98 } else { 99 merged.force_direction (Direction.COUNTER_CLOCKWISE); 100 } 101 102 merged.update_region_boundaries (); 103 paths.add (merged); 104 } else { 105 warning ("Can not create stroke."); 106 paths.add (p); 107 } 108 109 return paths; 110 } 111 112 /** Create one stroke from the outline and counter stroke and close the 113 * open endings. 114 * 115 * @param path the path to create stroke for 116 * @param stroke for the outline of path 117 * @param stroke for the counter path 118 */ 119 static Path merge_strokes (Path path, Path stroke, Path counter, double thickness) { 120 Path merged; 121 EditPoint corner1, corner2; 122 EditPoint corner3, corner4; 123 EditPoint end; 124 double angle; 125 126 if (path.points.size < 2) { 127 warning ("Missing points."); 128 return stroke; 129 } 130 131 if (stroke.points.size < 4) { 132 warning ("Missing points."); 133 return stroke; 134 } 135 136 if (counter.points.size < 4) { 137 warning ("Missing points."); 138 return stroke; 139 } 140 141 // end of stroke 142 end = path.get_last_visible_point (); 143 corner1 = stroke.get_last_point (); 144 angle = end.get_left_handle ().angle; 145 corner1.x = end.x + cos (angle - PI / 2) * thickness; 146 corner1.y = end.y + sin (angle - PI / 2) * thickness; 147 148 corner2 = counter.get_last_point (); 149 corner2.x = end.x + cos (angle + PI / 2) * thickness; 150 corner2.y = end.y + sin (angle + PI / 2) * thickness; 151 152 // the other end 153 end = path.get_first_point (); 154 corner3 = stroke.get_first_point (); 155 angle = end.get_right_handle ().angle; 156 corner3.x = end.x + cos (angle + PI / 2) * thickness; 157 corner3.y = end.y + sin (angle + PI / 2) * thickness; 158 159 corner4 = counter.get_first_point (); 160 corner4.x = end.x + cos (angle - PI / 2) * thickness; 161 corner4.y = end.y + sin (angle - PI / 2) * thickness; 162 163 corner1.get_left_handle ().convert_to_line (); 164 corner2.get_right_handle ().convert_to_line (); 165 166 corner3.get_left_handle ().convert_to_line (); 167 corner4.get_right_handle ().convert_to_line (); 168 169 counter.reverse (); 170 171 // Append the other part of the stroke 172 merged = stroke.copy (); 173 merged.append_path (counter); 174 corner2 = merged.points.get (merged.points.size - 1); 175 176 merged.close (); 177 merged.create_list (); 178 merged.recalculate_linear_handles (); 179 180 return merged; 181 } 182 183 static Path create_stroke (Path p, double thickness) { 184 Path stroked; 185 186 if (p.points.size >= 2) { 187 stroked = p.copy (); 188 stroked = generate_stroke (stroked, 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; 206 EditPoint end; 207 208 foreach (EditPoint ep in p.points) { 209 start = ep.copy (); 210 end = ep.get_next ().copy (); 211 212 move_segment (start, end, thickness); 213 214 start.get_left_handle ().convert_to_line (); // sharp corner 215 216 stroked.add_point (start); 217 stroked.add_point (end); 218 219 end.get_right_handle ().convert_to_line (); // sharp corner 220 221 } 222 stroked.recalculate_linear_handles (); 223 224 return stroked; 225 } 226 227 static void move_segment (EditPoint stroke_start, EditPoint stroke_stop, double thickness) { 228 EditPointHandle r, l; 229 double m, n; 230 double qx, qy; 231 232 stroke_start.set_tie_handle (false); 233 stroke_stop.set_tie_handle (false); 234 235 r = stroke_start.get_right_handle (); 236 l = stroke_stop.get_left_handle (); 237 238 m = cos (r.angle + PI / 2) * thickness; 239 n = sin (r.angle + PI / 2) * thickness; 240 241 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n); 242 stroke_start.get_left_handle ().move_to_coordinate_delta (m, n); 243 244 stroke_start.independent_x += m; 245 stroke_start.independent_y += n; 246 247 qx = cos (l.angle - PI / 2) * thickness; 248 qy = sin (l.angle - PI / 2) * thickness; 249 250 stroke_stop.get_right_handle ().move_to_coordinate_delta (qx, qy); 251 stroke_stop.get_left_handle ().move_to_coordinate_delta (qx, qy); 252 253 stroke_stop.independent_x += qx; 254 stroke_stop.independent_y += qy; 255 } 256 257 static Path change_stroke_width (Path original, double thickness) { 258 Path stroked = new Path (); 259 EditPoint ep; 260 uint k; 261 uint npoints; 262 Path new_path; 263 264 EditPoint np, sp, nprev, sprev; 265 266 int i = 0; 267 268 double la, qx, qy; 269 270 EditPoint split_point = new EditPoint (); 271 EditPoint start, stop, new_start; 272 EditPoint stroke_start, stroke_stop; 273 EditPoint segment_start, segment_stop; 274 EditPointHandle r, l; 275 double left_x, left_y; 276 bool new_point = false; 277 double m, n; 278 bool bad_segment = false; 279 bool failed = false; 280 int size; 281 282 new_path = original.copy (); 283 new_path.remove_points_on_points (); 284 285 // Add corner nodes 286 foreach (EditPoint e in original.points) { 287 new_path.add_point (e.copy ()); 288 new_path.add_point (e.copy ()); 289 } 290 291 new_path.update_region_boundaries (); 292 293 k = 0; 294 npoints = new_path.points.size; 295 296 if (npoints < 2) { 297 warning ("npoints < 2"); 298 return new_path; 299 } 300 301 left_x = 0; 302 left_y = 0; 303 start = new_path.get_first_point (); 304 int it = 0; 305 306 foreach (EditPoint e in new_path.points) { 307 e.flags |= EditPoint.CORNER; 308 } 309 310 // double points are not good for this purpose, convert them to the quadratic form 311 new_path.add_hidden_double_points (); 312 313 // add tangent points to the path 314 segment_start = new_path.get_first_point (); 315 size = new_path.points.size; 316 317 for (int j = 0; j < size; j++) { 318 segment_stop = segment_start.get_next (); 319 Path.all_of (segment_start, segment_stop, (x, y, t) => { 320 if (t == 0 && t != 1) { 321 return true; 322 } 323 324 split_point = new EditPoint (x, y); 325 326 split_point.prev = segment_start; 327 split_point.next = segment_stop; 328 329 segment_start.next = split_point; 330 segment_stop.prev = split_point; 331 332 new_path.insert_new_point_on_path (split_point, t); 333 334 return false; 335 }, 2); 336 337 segment_start = segment_stop; 338 } 339 new_path.remove_points_on_points (); 340 341 // calculate offset 342 bad_segment = false; 343 EditPoint previous_start = new EditPoint (); 344 for (int points_to_process = new_path.points.size - 1; points_to_process >= 0; points_to_process--) { 345 346 if (++it > 1000) { // FIXME: delete 347 warning ("Skipping the rest of the path."); 348 break; 349 } 350 351 if (is_null (start) || start.next == null) { 352 warning ("No next point"); 353 break; 354 } 355 356 stop = start.get_next (); 357 358 if (stop.type == PointType.NONE) { 359 break; 360 } 361 362 // move point 363 stroke_start = start.copy (); 364 stroke_stop = stop.copy (); 365 366 stroke_start.set_tie_handle (false); 367 stroke_stop.set_tie_handle (false); 368 369 start.set_tie_handle (false); 370 stop.set_tie_handle (false); 371 372 // FIXME: first point? 373 stroke_start.get_left_handle ().move_to_coordinate_delta (left_x, left_y); 374 375 r = stroke_start.get_right_handle (); 376 l = stroke_stop.get_left_handle (); 377 378 m = cos (r.angle + PI / 2) * thickness; 379 n = sin (r.angle + PI / 2) * thickness; 380 381 stroke_start.independent_x += m; 382 stroke_start.independent_y += n; 383 384 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n); 385 386 la = l.angle; 387 qx = cos (la - PI / 2) * thickness; 388 qy = sin (la - PI / 2) * thickness; 389 390 left_x = qx; 391 left_y = qy; 392 stroke_stop.get_left_handle ().move_to_coordinate_delta (left_x, left_y); 393 394 stroke_stop.independent_x += qx; 395 stroke_stop.independent_y += qy; 396 397 // avoid jagged edges 398 double dar = stroke_start.get_right_handle ().angle - start.get_right_handle ().angle; 399 double dal = stroke_stop.get_left_handle ().angle - stop.get_left_handle ().angle; 400 401 if (fabs (dal) > 1) { 402 stroke_stop.get_left_handle ().angle = stop.get_left_handle () .angle; 403 } 404 405 if (fabs (dar) > 1) { 406 print ("FIX:"); 407 stroke_start.get_right_handle ().angle = start.get_right_handle () .angle; 408 } 409 410 // Create a segment of the stroked path 411 Gee.ArrayList<EditPoint> on_stroke = new Gee.ArrayList<EditPoint> (); 412 int n_samples = 0; 413 Path.all_of (stroke_start, stroke_stop, (x, y, t) => { 414 if (t == 0 || t == 1) { 415 return true; 416 } 417 418 if (n_samples >= 2) { 419 return false; 420 } 421 422 on_stroke.add (new EditPoint (x, y)); 423 n_samples++; 424 return true; 425 }, 3); 426 427 if (on_stroke.size != 2) { 428 warning (@"on_stroke.size: $(on_stroke.size)"); 429 return stroked; 430 } 431 432 // compare the outline of the stroke to the original path and 433 // add new points if offset differs from stroke width 434 i = 0; 435 new_point = false; 436 if (!bad_segment) { 437 Path.all_of (start, stop, (x, y, t) => { 438 double d; 439 EditPoint point_on_stroke; 440 double stroke_width = fabs (thickness); 441 442 if (t == 0 || t == 1) { 443 return true; 444 } 445 446 if (i >= on_stroke.size) { 447 warning (@"Out of bounds. ($i >= $(on_stroke.size)) t: $t"); 448 return false; 449 } 450 451 point_on_stroke = on_stroke.get (i++); 452 d = fabs (Path.distance (point_on_stroke.x, x, point_on_stroke.y, y) - stroke_width); 453 split_point = new EditPoint (x, y); 454 455 if (d > 1) { 456 bad_segment = true; // add more points 457 return false; 458 } 459 460 if (d > 0.2) { 461 split_point.prev = start; 462 split_point.next = stop; 463 464 start.next = split_point; 465 stop.prev = split_point; 466 467 if (start.x == split_point.x && start.y == split_point.y) { 468 warning (@"Point already added. Error: $d"); 469 bad_segment = true; 470 failed = true; 471 return false; 472 } else { 473 new_path.insert_new_point_on_path (split_point, t); 474 new_point = true; 475 } 476 } 477 return !new_point; 478 }, 3); 479 } 480 481 if (failed) { 482 return stroked; 483 } 484 485 if (!new_point) { 486 ep = stroke_start.copy (); 487 stroked.add_point (ep); 488 previous_start = stroke_start; 489 new_start = stop.get_next (); 490 } else { 491 la = split_point.get_left_handle ().angle; 492 qx = cos (la - PI / 2) * thickness; 493 qy = sin (la - PI / 2) * thickness; 494 left_x = qx; 495 left_y = qy; 496 497 points_to_process += 2; // process the current point and the new point 498 new_start = start; 499 } 500 501 start = new_start; 502 } 503 504 new_path.remove_deleted_points (); 505 if (!(stroked.points.size == new_path.points.size && new_path.points.size > 1)) { 506 warning (@"stroked.points.size == new_path.points.size: $(stroked.points.size) != $(new_path.points.size)"); 507 return stroked; 508 } 509 510 // delete end point 511 if (new_path.points.size > 2 && stroked.points.size > 2) { 512 stroked.delete_last_point (); 513 new_path.delete_last_point (); 514 515 l = new_path.get_last_point ().get_left_handle (); 516 517 stroked.get_last_point ().get_left_handle ().angle = l.angle; 518 stroked.get_last_point ().get_left_handle ().length = l.length; 519 stroked.get_last_point ().get_left_handle ().type = l.type; 520 } 521 522 // remove self intersection 523 524 EditPoint snext, nnext; 525 double back_ratio = 0; 526 double next_ratio = 0; 527 528 snext = new EditPoint (); 529 nnext = new EditPoint (); 530 531 stroked.remove_deleted_points (); 532 new_path.remove_deleted_points (); 533 534 return_val_if_fail (stroked.points.size == new_path.points.size, stroked); 535 536 // adjust angle and length of control point handles 537 double last_ratio = 0; // FIXME: FIRST POINT 538 539 double last_prev_ratio = 0; 540 541 double ratio = 0; 542 543 nprev = new_path.points.get (new_path.points.size - 1); 544 sprev = stroked.points.get (stroked.points.size - 1); 545 for (int index = 0; index < stroked.points.size; index++) { 546 np = new_path.points.get (index); 547 sp = stroked.points.get (index); 548 549 if (index < stroked.points.size - 1) { 550 nnext = new_path.points.get (index + 1); 551 snext = stroked.points.get (index + 1); 552 } 553 554 if (np.type == PointType.NONE || nnext.type == PointType.NONE) { 555 break; 556 } 557 558 // angle 559 double dar = sp.get_right_handle ().angle - np.get_right_handle ().angle; 560 double dal = sp.get_left_handle ().angle - np.get_left_handle ().angle; 561 562 if (fabs (dal) > 1) { // FIXME 0.1? PI? 563 sp.get_left_handle ().angle = np.get_left_handle () .angle; 564 } 565 566 if (fabs (dar) > 1) { 567 print (@"FIX: $(sp) $(np)"); 568 sp.get_right_handle ().angle = np.get_right_handle () .angle; 569 } 570 571 sp.get_left_handle ().angle = np.get_left_handle () .angle; 572 sp.get_right_handle ().angle = np.get_right_handle () .angle; 573 574 // length 575 next_ratio = Path.distance (snext.x, sp.x, snext.y, sp.y); 576 next_ratio /= Path.distance (nnext.x, np.x, nnext.y, np.y); 577 578 back_ratio = Path.distance (sprev.x, sp.x, sprev.y, sp.y); 579 back_ratio /= Path.distance (nprev.x, np.x, nprev.y, np.y); 580 581 if (!(0.0002 < next_ratio < fabs (thickness) 582 && 0.0002 < back_ratio < fabs (thickness))) { 583 ratio = last_ratio; 584 sp.get_right_handle ().length = Path.distance (snext.x, sp.x, snext.y, sp.y); // HANDLE TYPE 585 586 ratio = last_ratio; 587 sp.get_left_handle ().length = Path.distance (snext.x, sp.x, snext.y, sp.y); 588 } else { 589 ratio = next_ratio; 590 sp.get_right_handle ().length *= ratio; 591 592 ratio = back_ratio; 593 sp.get_left_handle ().length *= ratio; 594 595 last_ratio = next_ratio; 596 last_prev_ratio = back_ratio; 597 } 598 599 nprev = np; 600 sprev = sp; 601 } 602 603 stroked.set_stroke (0); 604 605 return stroked; 606 } 607 } 608 609 } 610