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.
Cut off corners on strokes
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 EditPoint previous = new EditPoint (); 208 209 foreach (EditPoint ep in p.points) { 210 start = ep.copy (); 211 end = ep.get_next ().copy (); 212 213 move_segment (start, end, thickness); 214 215 if (end.get_left_handle ().length > 0) { 216 add_corner (stroked, previous, start, ep.copy (), thickness); 217 } 218 219 stroked.add_point (start); 220 221 if (end.get_left_handle ().length > 0) { 222 stroked.add_point (end); 223 } 224 225 // line ends around corner 226 start.get_left_handle ().convert_to_line (); 227 end.get_right_handle ().convert_to_line (); 228 229 previous = end; 230 } 231 stroked.recalculate_linear_handles (); 232 233 return stroked; 234 } 235 236 static void move_segment (EditPoint stroke_start, EditPoint stroke_stop, double thickness) { 237 EditPointHandle r, l; 238 double m, n; 239 double qx, qy; 240 241 stroke_start.set_tie_handle (false); 242 stroke_stop.set_tie_handle (false); 243 244 r = stroke_start.get_right_handle (); 245 l = stroke_stop.get_left_handle (); 246 247 m = cos (r.angle + PI / 2) * thickness; 248 n = sin (r.angle + PI / 2) * thickness; 249 250 stroke_start.get_right_handle ().move_to_coordinate_delta (m, n); 251 stroke_start.get_left_handle ().move_to_coordinate_delta (m, n); 252 253 stroke_start.independent_x += m; 254 stroke_start.independent_y += n; 255 256 qx = cos (l.angle - PI / 2) * thickness; 257 qy = sin (l.angle - PI / 2) * thickness; 258 259 stroke_stop.get_right_handle ().move_to_coordinate_delta (qx, qy); 260 stroke_stop.get_left_handle ().move_to_coordinate_delta (qx, qy); 261 262 stroke_stop.independent_x += qx; 263 stroke_stop.independent_y += qy; 264 } 265 266 static void add_corner (Path stroked, EditPoint previous, EditPoint next, 267 EditPoint original, double stroke_width) { 268 269 double ratio; 270 double distance; 271 EditPoint corner; 272 double corner_x, corner_y; 273 EditPointHandle previous_handle; 274 EditPointHandle next_handle; 275 EditPoint cutoff1, cutoff2; 276 277 previous_handle = previous.get_left_handle (); 278 next_handle = next.get_right_handle (); 279 280 previous_handle.angle += PI; 281 next_handle.angle += PI; 282 283 Path.find_intersection_handle (previous_handle, next_handle, out corner_x, out corner_y); 284 corner = new EditPoint (corner_x, corner_y, previous.type); 285 corner.convert_to_line (); 286 287 distance = Path.distance_to_point (corner, original); 288 289 ratio = 1.5 * fabs (stroke_width) / distance; // FIXME: make cutoff a parameter 290 291 if (ratio > 1) { 292 stroked.add_point (corner); 293 } else { 294 cutoff1 = new EditPoint (); 295 cutoff1.set_point_type (previous.type); 296 cutoff1.convert_to_line (); 297 298 cutoff2 = new EditPoint (); 299 cutoff2.set_point_type (previous.type); 300 cutoff2.convert_to_line (); 301 302 cutoff1.x = previous.x + (corner.x - previous.x) * ratio; 303 cutoff1.y = previous.y + (corner.y - previous.y) * ratio; 304 305 cutoff2.x = next.x + (corner.x - next.x) * ratio; 306 cutoff2.y = next.y + (corner.y - next.y) * ratio; 307 308 stroked.add_point (cutoff1); 309 stroked.add_point (cutoff2); 310 } 311 312 previous_handle.angle -= PI; 313 next_handle.angle -= PI; 314 } 315 316 317 } 318 319 } 320 321