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 add_corners (stroked); 189 stroked = change_stroke_width (stroked, thickness); 190 191 if (!p.is_open ()) { 192 stroked.reverse (); 193 stroked.close (); 194 } 195 } else { 196 // TODO: create stroke for path with one point 197 warning ("One point."); 198 stroked = new Path (); 199 } 200 201 return stroked; 202 } 203 204 static void add_corners (Path p) { 205 // TODO: 206 } 207 208 static Path change_stroke_width (Path original, double thickness) { 209 Path stroked = new Path (); 210 EditPoint ep; 211 uint k; 212 uint npoints; 213 Path new_path = original.copy (); 214 215 EditPoint np, sp, nprev, sprev; 216 217 int i = 0; 218 219 double la, qx, qy; 220 221 EditPoint split_point = new EditPoint (); 222 EditPoint start, stop, new_start; 223 EditPoint stroke_start, stroke_stop; 224 EditPoint segment_start, segment_stop; 225 EditPointHandle r, l; 226 double left_x, left_y; 227 bool new_point = false; 228 double m, n; 229 bool bad_segment = false; 230 bool failed = false; 231 int size; 232 233 new_path.remove_points_on_points (); 234 new_path.update_region_boundaries (); 235 236 k = 0; 237 npoints = new_path.points.size; 238 239 if (npoints < 2) { 240 warning ("npoints < 2"); 241 return new_path; 242 } 243 244 left_x = 0; 245 left_y = 0; 246 start = new_path.get_first_point (); 247 int it = 0; 248 249 foreach (EditPoint e in new_path.points) { 250 e.flags |= EditPoint.CORNER; 251 } 252 253 // double points are not good for this purpose, convert them to the quadratic form 254 new_path.add_hidden_double_points (); 255 256 // add tangent points to the path 257 segment_start = new_path.get_first_point (); 258 size = new_path.points.size; 259 260 for (int j = 0; j < size; j++) { 261 segment_stop = segment_start.get_next (); 262 Path.all_of (segment_start, segment_stop, (x, y, t) => { 263 if (t == 0 && t != 1) { 264 return true; 265 } 266 267 split_point = new EditPoint (x, y); 268 269 split_point.prev = segment_start; 270 split_point.next = segment_stop; 271 272 segment_start.next = split_point; 273 segment_stop.prev = split_point; 274 275 new_path.insert_new_point_on_path (split_point, t); 276 277 return false; 278 }, 2); 279 280 segment_start = segment_stop; 281 } 282 new_path.remove_points_on_points (); 283 284 // calculate offset 285 bad_segment = false; 286 EditPoint previous_start = new EditPoint (); 287 for (int points_to_process = new_path.points.size - 1; points_to_process >= 0; points_to_process--) { 288 289 if (++it > 1000) { // FIXME: delete 290 warning ("Skipping the rest of the path."); 291 break; 292 } 293 294 if (is_null (start) || start.next == null) { 295 warning ("No next point"); 296 break; 297 } 298 299 stop = start.get_next (); 300 301 if (stop.type == PointType.NONE) { 302 break; 303 } 304 305 // move point 306 stroke_start = start.copy (); 307 stroke_stop = stop.copy (); 308 309 stroke_start.set_tie_handle (false); 310 stroke_stop.set_tie_handle (false); 311 312 start.set_tie_handle (false); 313 stop.set_tie_handle (false); 314 315 // FIXME: first point? 316 stroke_start.get_left_handle ().move_to_coordinate_delta (left_x, left_y); 317 318 r = stroke_start.get_right_handle (); 319 l = stroke_stop.get_left_handle (); 320 321 m = cos (r.angle + PI / 2) * thickness; 322 n = sin (r.angle + PI / 2) * thickness; 323 324 stroke_start.independent_x += m; 325 stroke_start.independent_y += n; 326 327 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n); 328 329 EditPoint next_start = stroke_stop.copy (); 330 331 la = l.angle; 332 qx = cos (la - PI / 2) * thickness; 333 qy = sin (la - PI / 2) * thickness; 334 335 left_x = qx; 336 left_y = qy; 337 stroke_stop.get_left_handle ().move_to_coordinate_delta (left_x, left_y); 338 339 stroke_stop.independent_x += qx; 340 stroke_stop.independent_y += qy; 341 342 // new start 343 la = l.angle; 344 qx = cos (la - PI / 2) * thickness; 345 qy = sin (la - PI / 2) * thickness; 346 347 left_x = qx; 348 left_y = qy; 349 stroke_stop.get_left_handle ().move_to_coordinate_delta (left_x, left_y); 350 351 stroke_stop.independent_x += qx; 352 stroke_stop.independent_y += qy; 353 354 // avoid jagged edges 355 double dar = stroke_start.get_right_handle ().angle - start.get_right_handle ().angle; 356 double dal = stroke_stop.get_left_handle ().angle - stop.get_left_handle ().angle; 357 358 if (fabs (dal) > 1) { 359 stroke_stop.get_left_handle ().angle = stop.get_left_handle () .angle; 360 } 361 362 if (fabs (dar) > 1) { 363 print ("FIX:"); 364 stroke_start.get_right_handle ().angle = start.get_right_handle () .angle; 365 } 366 367 // Create a segment of the stroked path 368 Gee.ArrayList<EditPoint> on_stroke = new Gee.ArrayList<EditPoint> (); 369 int n_samples = 0; 370 Path.all_of (stroke_start, stroke_stop, (x, y, t) => { 371 if (t == 0 || t == 1) { 372 return true; 373 } 374 375 if (n_samples >= 2) { 376 return false; 377 } 378 379 on_stroke.add (new EditPoint (x, y)); 380 n_samples++; 381 return true; 382 }, 3); 383 384 if (on_stroke.size != 2) { 385 warning (@"on_stroke.size: $(on_stroke.size)"); 386 return stroked; 387 } 388 389 // compare the outline of the stroke to the original path and 390 // add new points if offset differs from stroke width 391 i = 0; 392 new_point = false; 393 if (!bad_segment) { 394 Path.all_of (start, stop, (x, y, t) => { 395 double d; 396 EditPoint point_on_stroke; 397 double stroke_width = fabs (thickness); 398 399 if (t == 0 || t == 1) { 400 return true; 401 } 402 403 if (i >= on_stroke.size) { 404 warning (@"Out of bounds. ($i >= $(on_stroke.size)) t: $t"); 405 return false; 406 } 407 408 point_on_stroke = on_stroke.get (i++); 409 d = fabs (Path.distance (point_on_stroke.x, x, point_on_stroke.y, y) - stroke_width); 410 split_point = new EditPoint (x, y); 411 412 if (d > 1) { 413 bad_segment = true; // add more points 414 return false; 415 } 416 417 if (d > 0.2) { 418 split_point.prev = start; 419 split_point.next = stop; 420 421 start.next = split_point; 422 stop.prev = split_point; 423 424 if (start.x == split_point.x && start.y == split_point.y) { 425 warning (@"Point already added. Error: $d"); 426 bad_segment = true; 427 failed = true; 428 return false; 429 } else { 430 new_path.insert_new_point_on_path (split_point, t); 431 new_point = true; 432 } 433 } 434 return !new_point; 435 }, 3); 436 } 437 438 if (failed) { 439 return stroked; 440 } 441 442 if (!new_point) { 443 ep = stroke_start.copy (); 444 stroked.add_point (ep); 445 previous_start = stroke_start; 446 new_start = stop; 447 } else { 448 la = split_point.get_left_handle ().angle; 449 qx = cos (la - PI / 2) * thickness; 450 qy = sin (la - PI / 2) * thickness; 451 left_x = qx; 452 left_y = qy; 453 454 points_to_process += 2; // process the current point and the new point 455 new_start = start; 456 } 457 458 start = new_start; 459 } 460 461 new_path.remove_deleted_points (); 462 if (!(stroked.points.size == new_path.points.size && new_path.points.size > 1)) { 463 warning (@"stroked.points.size == new_path.points.size: $(stroked.points.size) != $(new_path.points.size)"); 464 return stroked; 465 } 466 467 // delete end point 468 if (new_path.points.size > 2 && stroked.points.size > 2) { 469 stroked.delete_last_point (); 470 new_path.delete_last_point (); 471 472 l = new_path.get_last_point ().get_left_handle (); 473 474 stroked.get_last_point ().get_left_handle ().angle = l.angle; 475 stroked.get_last_point ().get_left_handle ().length = l.length; 476 stroked.get_last_point ().get_left_handle ().type = l.type; 477 } 478 479 // remove self intersection 480 481 EditPoint snext, nnext; 482 double back_ratio = 0; 483 double next_ratio = 0; 484 485 snext = new EditPoint (); 486 nnext = new EditPoint (); 487 488 stroked.remove_deleted_points (); 489 new_path.remove_deleted_points (); 490 491 return_val_if_fail (stroked.points.size == new_path.points.size, stroked); 492 493 // adjust angle and length of control point handles 494 double last_ratio = 0; // FIXME: FIRST POINT 495 496 double last_prev_ratio = 0; 497 498 double ratio = 0; 499 500 nprev = new_path.points.get (new_path.points.size - 1); 501 sprev = stroked.points.get (stroked.points.size - 1); 502 for (int index = 0; index < stroked.points.size; index++) { 503 np = new_path.points.get (index); 504 sp = stroked.points.get (index); 505 506 if (index < stroked.points.size - 1) { 507 nnext = new_path.points.get (index + 1); 508 snext = stroked.points.get (index + 1); 509 } 510 511 if (np.type == PointType.NONE || nnext.type == PointType.NONE) { 512 break; 513 } 514 515 // angle 516 double dar = sp.get_right_handle ().angle - np.get_right_handle ().angle; 517 double dal = sp.get_left_handle ().angle - np.get_left_handle ().angle; 518 519 if (fabs (dal) > 1) { // FIXME 0.1? PI? 520 sp.get_left_handle ().angle = np.get_left_handle () .angle; 521 } 522 523 if (fabs (dar) > 1) { 524 print (@"FIX: $(sp) $(np)"); 525 sp.get_right_handle ().angle = np.get_right_handle () .angle; 526 } 527 528 sp.get_left_handle ().angle = np.get_left_handle () .angle; 529 sp.get_right_handle ().angle = np.get_right_handle () .angle; 530 531 // length 532 next_ratio = Path.distance (snext.x, sp.x, snext.y, sp.y); 533 next_ratio /= Path.distance (nnext.x, np.x, nnext.y, np.y); 534 535 back_ratio = Path.distance (sprev.x, sp.x, sprev.y, sp.y); 536 back_ratio /= Path.distance (nprev.x, np.x, nprev.y, np.y); 537 538 if (!(0.0002 < next_ratio < fabs (thickness) 539 && 0.0002 < back_ratio < fabs (thickness))) { 540 ratio = last_ratio; 541 sp.get_right_handle ().length = Path.distance (snext.x, sp.x, snext.y, sp.y); // HANDLE TYPE 542 543 ratio = last_ratio; 544 sp.get_left_handle ().length = Path.distance (snext.x, sp.x, snext.y, sp.y); 545 } else { 546 ratio = next_ratio; 547 sp.get_right_handle ().length *= ratio; 548 549 ratio = back_ratio; 550 sp.get_left_handle ().length *= ratio; 551 552 last_ratio = next_ratio; 553 last_prev_ratio = back_ratio; 554 } 555 556 nprev = np; 557 sprev = sp; 558 } 559 560 stroked.set_stroke (0); 561 562 return stroked; 563 } 564 } 565 566 } 567