position.rs
1 //! An algorithm for laying out curves in a given order. 2 //! 3 //! Our boolean ops algorithms work by first deciding horizontal orderings for curves 4 //! (with some guarantees about being approximately correct) 5 //! and then laying out the curves in a way that has the horizontal orders we decided 6 //! on. For this second step, which is the task of this module, we really 7 //! guarantee correctness: the curves that we output really have the specified 8 //! orders. 9 10 use std::collections::BinaryHeap; 11 12 use kurbo::{BezPath, CubicBez}; 13 14 use crate::{ 15 curve::{ 16 transverse::{transversal_after, transversal_before}, 17 y_subsegment, EstParab, Order, 18 }, 19 num::CheapOrderedFloat, 20 order::ComparisonCache, 21 topology::{HalfOutputSegVec, OutputSegIdx, OutputSegVec, ScanLineOrder}, 22 SegIdx, Segment, Segments, 23 }; 24 25 // TODO: maybe a Context type to keep the parameters in check? 26 #[allow(clippy::too_many_arguments)] 27 fn ordered_curves_all_close( 28 segs: &Segments, 29 order: &[SegIdx], 30 output_order: &[OutputSegIdx], 31 out: &mut OutputSegVec<PositionedOutputSeg>, 32 mut y0: f64, 33 y1: f64, 34 endpoints: &HalfOutputSegVec<kurbo::Point>, 35 accuracy: f64, 36 ) { 37 if order.iter().all(|&s| segs[s].is_line()) { 38 // If everything is a line, since our endpoints are already in the right order 39 // the segments between them are also guaranteed to be in the right order. 40 return; 41 } 42 43 if order.len() == 2 { 44 let s0 = order[0]; 45 let s1 = order[1]; 46 let o0 = output_order[0]; 47 let o1 = output_order[1]; 48 49 let p0 = bez_end(&out[o0].path); 50 let p1 = bez_end(&out[o1].path); 51 let q0 = endpoints[o0.second_half()]; 52 let q1 = endpoints[o1.second_half()]; 53 54 if p0.y == y0 && p1.y == y0 { 55 let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0); 56 let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1); 57 58 if transversal_after(c0, c1, y1) { 59 return; 60 } 61 } 62 63 if q0.y == y1 && q1.y == y1 { 64 let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0); 65 let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1); 66 67 if transversal_before(c0, c1, y0) { 68 return; 69 } 70 } 71 } 72 73 // Ensure everything in `out` goes up to `y0`. Anything that doesn't go up to `y0` is 74 // an output where we can just copy from the input. 75 for (&seg_idx, &out_idx) in order.iter().zip(output_order) { 76 let out = &mut out[out_idx]; 77 if bez_end_y(&out.path) < y0 { 78 out.copied_idx = Some(out.path.elements().len() - 1); 79 let endpoint = endpoints[out_idx.second_half()]; 80 if segs[seg_idx].is_line() { 81 out.path.line_to(endpoint) 82 } else { 83 let c = next_subsegment(&segs[seg_idx], &out.path, y0, endpoint); 84 out.path.curve_to(c.p1, c.p2, c.p3); 85 } 86 } 87 } 88 89 let mut approxes = Vec::new(); 90 91 // The recentering helps, but there are still some issues with approximation when almost horizontal 92 while y0 < y1 { 93 let next_y0 = approximate(y0, y1, order, segs, accuracy, &mut approxes); 94 95 let y_mid = (y0 + next_y0) / 2.0; 96 97 let mut x_mid_max_so_far = f64::NEG_INFINITY; 98 let mut x1_max_so_far = f64::NEG_INFINITY; 99 for (quad, out_idx) in approxes.iter().zip(output_order) { 100 // These would be the control points, but since `approximate` recenters the y interval, 101 // they're different... 102 // let x_mid = quad.c0 + y_mid * quad.c1 + y0 * next_y0 * quad.c2; 103 // let x1 = quad.c0 + quad.c1 * next_y0 + quad.c2 * next_y0 * next_y0; 104 let x_mid = quad.c0 + (y0 - y_mid) * (next_y0 - y_mid) * quad.c2; 105 let x1 = quad.c0 106 + quad.c1 * (next_y0 - y_mid) 107 + quad.c2 * (next_y0 - y_mid) * (next_y0 - y_mid); 108 109 x_mid_max_so_far = x_mid_max_so_far.max(x_mid); 110 x1_max_so_far = x1_max_so_far.max(x1); 111 112 out[*out_idx] 113 .path 114 .quad_to((x_mid_max_so_far, y_mid), (x1_max_so_far, next_y0)); 115 } 116 approxes.clear(); 117 118 y0 = next_y0; 119 } 120 } 121 122 fn horizontal_error_weight(c: CubicBez) -> f64 { 123 let tangents = [c.p1 - c.p0, c.p2 - c.p1, c.p3 - c.p2]; 124 let mut weight = 0.0f64; 125 126 fn different_signs(x: f64, y: f64) -> bool { 127 (x > 0.0 && y < 0.0) || (x < 0.0) && (y > 0.0) 128 } 129 130 if different_signs(tangents[0].x, tangents[1].x) 131 || different_signs(tangents[1].x, tangents[2].x) 132 { 133 return 1.0; 134 } 135 136 for v in tangents { 137 let denom = v.hypot2().sqrt(); 138 if denom != 0.0 { 139 let w = v.y.abs() / denom; 140 weight = weight.max(w); 141 } 142 } 143 weight 144 } 145 146 // Pushes approximating parabolas into `out`, and returns the final y position. 147 fn approximate( 148 y0: f64, 149 mut y1: f64, 150 order: &[SegIdx], 151 segs: &Segments, 152 accuracy: f64, 153 out: &mut Vec<EstParab>, 154 ) -> f64 { 155 let orig_len = out.len(); 156 157 'retry: loop { 158 let y_mid = (y0 + y1) / 2.0; 159 for seg_idx in order { 160 let cubic = segs[*seg_idx].to_kurbo_cubic(); 161 let mut cubic = y_subsegment(cubic, y0, y1); 162 cubic.p0.y -= y_mid; 163 cubic.p1.y -= y_mid; 164 cubic.p2.y -= y_mid; 165 cubic.p3.y -= y_mid; 166 let approx = EstParab::from_cubic(cubic); 167 168 let factor = horizontal_error_weight(cubic); 169 170 let too_short = y1 <= y0 + accuracy; 171 let accurate = approx.dmax.max(-approx.dmin) * factor < accuracy; 172 if !too_short && !accurate { 173 // TODO: can we be smarter about this? 174 y1 = (y0 + y1) / 2.0; 175 out.truncate(orig_len); 176 continue 'retry; 177 } 178 179 out.push(approx); 180 } 181 182 return y1; 183 } 184 } 185 186 #[derive(Debug, PartialEq, Eq)] 187 struct HeapEntry { 188 y: CheapOrderedFloat, 189 idx: OutputSegIdx, 190 } 191 192 impl Ord for HeapEntry { 193 fn cmp(&self, other: &Self) -> std::cmp::Ordering { 194 other.y.cmp(&self.y).then(self.idx.cmp(&other.idx)) 195 } 196 } 197 198 impl PartialOrd for HeapEntry { 199 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> { 200 Some(self.cmp(other)) 201 } 202 } 203 204 fn bez_end(path: &BezPath) -> kurbo::Point { 205 match path.elements().last().unwrap() { 206 kurbo::PathEl::MoveTo(p) 207 | kurbo::PathEl::LineTo(p) 208 | kurbo::PathEl::QuadTo(_, p) 209 | kurbo::PathEl::CurveTo(_, _, p) => *p, 210 kurbo::PathEl::ClosePath => unreachable!(), 211 } 212 } 213 214 fn bez_end_y(path: &BezPath) -> f64 { 215 bez_end(path).y 216 } 217 218 fn next_subsegment(seg: &Segment, out: &BezPath, y1: f64, endpoint: kurbo::Point) -> CubicBez { 219 let p0 = bez_end(out); 220 let mut c = y_subsegment(seg.to_kurbo_cubic(), p0.y, y1); 221 c.p0 = p0; 222 if endpoint.y == y1 { 223 c.p3 = endpoint; 224 } 225 c 226 } 227 228 /// A positioned output segment, possibly perturbed from the original. 229 /// 230 /// We perturb output segments in order to strictly enforce ordering. 231 /// A single segment might be expanded into a longer path. 232 #[derive(Default)] 233 pub struct PositionedOutputSeg { 234 /// The path, which is guaranteed to start and end at the same points as the 235 /// output segment did. 236 pub path: BezPath, 237 /// Sometimes (often, even) a large part of the output segment didn't need 238 /// to be perturbed. Our sweep-line invariants guarantee that unperturbed 239 /// part can only be a single contiguous interval: this stores the index 240 /// of the unperturbed part within `path.segments()`. 241 pub copied_idx: Option<usize>, 242 } 243 244 /// Compute positions for all of the output segments. 245 /// 246 /// The orders between the output segments is specified by `order`. The endpoints 247 /// should have been already computed (in a way that satisfies the order), and 248 /// are provided in `endpoints`. For each output segment, we return a Bézier 249 /// path. 250 pub(crate) fn compute_positions( 251 segs: &Segments, 252 orig_seg_map: &OutputSegVec<SegIdx>, 253 cmp: &mut ComparisonCache, 254 endpoints: &HalfOutputSegVec<kurbo::Point>, 255 scan_order: &ScanLineOrder, 256 accuracy: f64, 257 ) -> OutputSegVec<PositionedOutputSeg> { 258 let mut out = OutputSegVec::<PositionedOutputSeg>::with_size(orig_seg_map.len()); 259 // We try to build `out` lazily, by avoiding copying input segments to outputs segments 260 // until they're needed (by copying in one go, we avoid excess subdivisions). That means 261 // we need to separately keep track of how far down we've looked at each output. If 262 // we weren't lazy, that would just be the last `y` coordinate in `out`. 263 let mut last_y = OutputSegVec::<f64>::with_size(orig_seg_map.len()); 264 let mut queue = BinaryHeap::<HeapEntry>::new(); 265 for idx in out.indices() { 266 let p = endpoints[idx.first_half()]; 267 let q = endpoints[idx.second_half()]; 268 if p.y == q.y { 269 out[idx].path.move_to(p); 270 out[idx].path.line_to(q); 271 out[idx].copied_idx = Some(0); 272 continue; 273 } 274 out[idx].path.move_to(p); 275 queue.push(HeapEntry { y: p.y.into(), idx }); 276 } 277 278 while let Some(entry) = queue.pop() { 279 // Maybe this entry was already handled by one of its neighbors, in which case we skip it. 280 let y0 = entry.y.into_inner(); 281 if last_y[entry.idx] > y0 { 282 continue; 283 } 284 285 let mut y1 = endpoints[entry.idx.second_half()].y; 286 let mut west_scan = vec![entry.idx]; 287 let mut cur = entry.idx; 288 while let Some(nbr) = scan_order.west_neighbor_after(cur, y0) { 289 let order = cmp.compare_segments(segs, orig_seg_map[nbr], orig_seg_map[cur]); 290 let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap(); 291 292 if cmp_order == Order::Left { 293 let next_close_y = scan_order 294 .close_west_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp) 295 .unwrap_or(f64::INFINITY); 296 y1 = y1.min(next_close_y.max(cmp_end_y)); 297 break; 298 } else { 299 y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y); 300 west_scan.push(nbr); 301 } 302 cur = nbr; 303 } 304 305 let mut east_scan = vec![]; 306 let mut cur = entry.idx; 307 while let Some(nbr) = scan_order.east_neighbor_after(cur, y0) { 308 let order = cmp.compare_segments(segs, orig_seg_map[cur], orig_seg_map[nbr]); 309 let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap(); 310 311 if cmp_order == Order::Left { 312 let next_close_y = scan_order 313 .close_east_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp) 314 .unwrap_or(f64::INFINITY); 315 y1 = y1.min(next_close_y.max(cmp_end_y)); 316 break; 317 } else { 318 y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y); 319 east_scan.push(nbr); 320 } 321 cur = nbr; 322 } 323 324 let mut neighbors = west_scan; 325 neighbors.reverse(); 326 neighbors.extend(east_scan); 327 if neighbors.len() == 1 { 328 let idx = entry.idx; 329 // We're far from everything, so we can just copy the input bezier to the output. 330 // We only actually do this eagerly for horizontal segments: for other segments 331 // we'll hold off on the copying because it might allow us to avoid further 332 // subdivision. 333 if y0 == y1 { 334 out[idx].copied_idx = Some(out[idx].path.elements().len() - 1); 335 out[idx].path.line_to(endpoints[idx.second_half()]); 336 } 337 } else { 338 let orig_neighbors = neighbors 339 .iter() 340 .map(|idx| orig_seg_map[*idx]) 341 .collect::<Vec<_>>(); 342 343 ordered_curves_all_close( 344 segs, 345 &orig_neighbors, 346 &neighbors, 347 &mut out, 348 y0, 349 y1, 350 endpoints, 351 accuracy, 352 ); 353 } 354 for idx in neighbors { 355 let y_end = endpoints[idx.second_half()].y; 356 if y1 < y_end { 357 queue.push(HeapEntry { y: y1.into(), idx }); 358 } 359 last_y[idx] = y1; 360 } 361 } 362 363 for out_idx in out.indices() { 364 let out = &mut out[out_idx]; 365 let endpoint = endpoints[out_idx.second_half()]; 366 if bez_end_y(&out.path) < endpoint.y { 367 let seg_idx = orig_seg_map[out_idx]; 368 out.copied_idx = Some(out.path.elements().len() - 1); 369 if segs[seg_idx].is_line() { 370 out.path.line_to(endpoint) 371 } else { 372 let c = next_subsegment(&segs[seg_idx], &out.path, endpoint.y, endpoint); 373 out.path.curve_to(c.p1, c.p2, c.p3); 374 } 375 } else { 376 // The quadratic approximations don't respect the fixed endpoints, so tidy them 377 // up. Since both the quadratic approximations and the endpoints satisfy 378 // the ordering, this doesn't mess up the ordering. 379 match out.path.elements_mut().last_mut().unwrap() { 380 kurbo::PathEl::MoveTo(p) 381 | kurbo::PathEl::LineTo(p) 382 | kurbo::PathEl::QuadTo(_, p) 383 | kurbo::PathEl::CurveTo(_, _, p) => { 384 *p = endpoints[out_idx.second_half()]; 385 } 386 kurbo::PathEl::ClosePath => unreachable!(), 387 } 388 } 389 } 390 out 391 } 392 393 #[cfg(test)] 394 mod tests { 395 use kurbo::{BezPath, CubicBez, Line, PathSeg}; 396 397 use crate::Segments; 398 399 use super::horizontal_error_weight; 400 401 #[test] 402 fn error_weight() { 403 // Vertical lines have a weight of 1. 404 let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (0.0, 1.0))).to_cubic(); 405 assert_eq!(horizontal_error_weight(c), 1.0); 406 407 // Horizontal lines have a weight of 0. 408 let c: CubicBez = PathSeg::from(Line::new((1.0, 0.0), (0.0, 0.0))).to_cubic(); 409 assert_eq!(horizontal_error_weight(c), 0.0); 410 411 let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 0.0))).to_cubic(); 412 assert_eq!(horizontal_error_weight(c), 0.0); 413 414 // S-shaped curves have a weight of 1 415 let c = CubicBez::new((0.0, 0.0), (1.0, 0.0), (-1.0, 1.0), (0.0, 1.0)); 416 assert_eq!(horizontal_error_weight(c), 1.0); 417 418 // Diagonal lines have a weight of about 1/sqrt(2). 419 let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 1.0))).to_cubic(); 420 assert_eq!(horizontal_error_weight(c), 1.0 / 2.0f64.sqrt()); 421 } 422 423 // Here's an example of lines where we used to have a bad 424 // approximation. Check that they only require a single 425 // approximation step. 426 #[test] 427 fn linear_approx() { 428 let mut p0 = BezPath::new(); 429 p0.move_to((-212.00062561035156, 90.03582000732422)); 430 p0.line_to((211.99937438964844, 90.03646087646484)); 431 p0.close_path(); 432 433 let mut p1 = BezPath::new(); 434 p1.move_to((211.99964904785153, -90.0358200073242)); 435 p1.line_to((211.99937438964844, 90.03646087646484)); 436 p1.close_path(); 437 438 let mut segs = Segments::default(); 439 segs.add_bez_path(&p0).unwrap(); 440 segs.add_bez_path(&p1).unwrap(); 441 let s0 = segs.indices().next().unwrap(); 442 let s1 = segs.indices().nth(2).unwrap(); 443 444 let mut out = Vec::new(); 445 let y1 = 90.03646087646484; 446 let next_y0 = super::approximate(90.03645987646233, y1, &[s0, s1], &segs, 2.5e-7, &mut out); 447 assert_eq!(next_y0, y1); 448 } 449 }