/ src / position.rs
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<(BezPath, Option<usize>)>,
 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].0);
 50          let p1 = bez_end(&out[o1].0);
 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].0, y1, q0);
 56              let c1 = next_subsegment(&segs[s1], &out[o1].0, 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].0, y1, q0);
 65              let c1 = next_subsegment(&segs[s1], &out[o1].0, 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_bez, out_copied_idx) = &mut out[out_idx];
 77          if bez_end_y(out_bez) < y0 {
 78              *out_copied_idx = Some(out_bez.elements().len() - 1);
 79              let endpoint = endpoints[out_idx.second_half()];
 80              if segs[seg_idx].is_line() {
 81                  out_bez.line_to(endpoint)
 82              } else {
 83                  let c = next_subsegment(&segs[seg_idx], out_bez, y0, endpoint);
 84                  out_bez.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                  .0
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  /// Compute positions for all of the output segments.
229  ///
230  /// The orders between the output segments is specified by `order`. The endpoints
231  /// should have been already computed (in a way that satisfies the order), and
232  /// are provided in `endpoints`. For each output segment, we return a Bézier
233  /// path.
234  ///
235  /// The `usize` return value tells which segment (if any) in the returned
236  /// path was the one that was "far" from any other paths. This is really
237  /// only interesting for diagnosis/visualization so the API should probably
238  /// be refined somehow to make it optional. (TODO)
239  pub(crate) fn compute_positions(
240      segs: &Segments,
241      orig_seg_map: &OutputSegVec<SegIdx>,
242      cmp: &mut ComparisonCache,
243      endpoints: &HalfOutputSegVec<kurbo::Point>,
244      scan_order: &ScanLineOrder,
245      accuracy: f64,
246  ) -> OutputSegVec<(BezPath, Option<usize>)> {
247      let mut out = OutputSegVec::<(BezPath, Option<usize>)>::with_size(orig_seg_map.len());
248      // We try to build `out` lazily, by avoiding copying input segments to outputs segments
249      // until they're needed (by copying in one go, we avoid excess subdivisions). That means
250      // we need to separately keep track of how far down we've looked at each output. If
251      // we weren't lazy, that would just be the last `y` coordinate in `out`.
252      let mut last_y = OutputSegVec::<f64>::with_size(orig_seg_map.len());
253      let mut queue = BinaryHeap::<HeapEntry>::new();
254      for idx in out.indices() {
255          let p = endpoints[idx.first_half()];
256          let q = endpoints[idx.second_half()];
257          if p.y == q.y {
258              out[idx].0.move_to(p);
259              out[idx].0.line_to(q);
260              out[idx].1 = Some(0);
261              continue;
262          }
263          out[idx].0.move_to(p);
264          queue.push(HeapEntry { y: p.y.into(), idx });
265      }
266  
267      while let Some(entry) = queue.pop() {
268          // Maybe this entry was already handled by one of its neighbors, in which case we skip it.
269          let y0 = entry.y.into_inner();
270          if last_y[entry.idx] > y0 {
271              continue;
272          }
273  
274          let mut y1 = endpoints[entry.idx.second_half()].y;
275          let mut west_scan = vec![entry.idx];
276          let mut cur = entry.idx;
277          while let Some(nbr) = scan_order.west_neighbor_after(cur, y0) {
278              let order = cmp.compare_segments(segs, orig_seg_map[nbr], orig_seg_map[cur]);
279              let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap();
280  
281              if cmp_order == Order::Left {
282                  let next_close_y = scan_order
283                      .close_west_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp)
284                      .unwrap_or(f64::INFINITY);
285                  y1 = y1.min(next_close_y.max(cmp_end_y));
286                  break;
287              } else {
288                  y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y);
289                  west_scan.push(nbr);
290              }
291              cur = nbr;
292          }
293  
294          let mut east_scan = vec![];
295          let mut cur = entry.idx;
296          while let Some(nbr) = scan_order.east_neighbor_after(cur, y0) {
297              let order = cmp.compare_segments(segs, orig_seg_map[cur], orig_seg_map[nbr]);
298              let (_, cmp_end_y, cmp_order) = order.iter().find(|(_, end_y, _)| *end_y > y0).unwrap();
299  
300              if cmp_order == Order::Left {
301                  let next_close_y = scan_order
302                      .close_east_neighbor_height_after(cur, y0, orig_seg_map, segs, cmp)
303                      .unwrap_or(f64::INFINITY);
304                  y1 = y1.min(next_close_y.max(cmp_end_y));
305                  break;
306              } else {
307                  y1 = y1.min(cmp_end_y).min(endpoints[nbr.second_half()].y);
308                  east_scan.push(nbr);
309              }
310              cur = nbr;
311          }
312  
313          let mut neighbors = west_scan;
314          neighbors.reverse();
315          neighbors.extend(east_scan);
316          if neighbors.len() == 1 {
317              let idx = entry.idx;
318              // We're far from everything, so we can just copy the input bezier to the output.
319              // We only actually do this eagerly for horizontal segments: for other segments
320              // we'll hold off on the copying because it might allow us to avoid further
321              // subdivision.
322              if y0 == y1 {
323                  out[idx].1 = Some(out[idx].0.elements().len() - 1);
324                  out[idx].0.line_to(endpoints[idx.second_half()]);
325              }
326          } else {
327              let orig_neighbors = neighbors
328                  .iter()
329                  .map(|idx| orig_seg_map[*idx])
330                  .collect::<Vec<_>>();
331  
332              ordered_curves_all_close(
333                  segs,
334                  &orig_neighbors,
335                  &neighbors,
336                  &mut out,
337                  y0,
338                  y1,
339                  endpoints,
340                  accuracy,
341              );
342          }
343          for idx in neighbors {
344              let y_end = endpoints[idx.second_half()].y;
345              if y1 < y_end {
346                  queue.push(HeapEntry { y: y1.into(), idx });
347              }
348              last_y[idx] = y1;
349          }
350      }
351  
352      for out_idx in out.indices() {
353          let (out_bez, out_copied_idx) = &mut out[out_idx];
354          let endpoint = endpoints[out_idx.second_half()];
355          if bez_end_y(out_bez) < endpoint.y {
356              let seg_idx = orig_seg_map[out_idx];
357              *out_copied_idx = Some(out_bez.elements().len() - 1);
358              if segs[seg_idx].is_line() {
359                  out_bez.line_to(endpoint)
360              } else {
361                  let c = next_subsegment(&segs[seg_idx], out_bez, endpoint.y, endpoint);
362                  out_bez.curve_to(c.p1, c.p2, c.p3);
363              }
364          } else {
365              // The quadratic approximations don't respect the fixed endpoints, so tidy them
366              // up. Since both the quadratic approximations and the endpoints satisfy
367              // the ordering, this doesn't mess up the ordering.
368              match out[out_idx].0.elements_mut().last_mut().unwrap() {
369                  kurbo::PathEl::MoveTo(p)
370                  | kurbo::PathEl::LineTo(p)
371                  | kurbo::PathEl::QuadTo(_, p)
372                  | kurbo::PathEl::CurveTo(_, _, p) => {
373                      *p = endpoints[out_idx.second_half()];
374                  }
375                  kurbo::PathEl::ClosePath => unreachable!(),
376              }
377          }
378      }
379      out
380  }
381  
382  #[cfg(test)]
383  mod tests {
384      use kurbo::{BezPath, CubicBez, Line, PathSeg};
385  
386      use crate::Segments;
387  
388      use super::horizontal_error_weight;
389  
390      #[test]
391      fn error_weight() {
392          // Vertical lines have a weight of 1.
393          let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (0.0, 1.0))).to_cubic();
394          assert_eq!(horizontal_error_weight(c), 1.0);
395  
396          // Horizontal lines have a weight of 0.
397          let c: CubicBez = PathSeg::from(Line::new((1.0, 0.0), (0.0, 0.0))).to_cubic();
398          assert_eq!(horizontal_error_weight(c), 0.0);
399  
400          let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 0.0))).to_cubic();
401          assert_eq!(horizontal_error_weight(c), 0.0);
402  
403          // S-shaped curves have a weight of 1
404          let c = CubicBez::new((0.0, 0.0), (1.0, 0.0), (-1.0, 1.0), (0.0, 1.0));
405          assert_eq!(horizontal_error_weight(c), 1.0);
406  
407          // Diagonal lines have a weight of about 1/sqrt(2).
408          let c: CubicBez = PathSeg::from(Line::new((0.0, 0.0), (1.0, 1.0))).to_cubic();
409          assert_eq!(horizontal_error_weight(c), 1.0 / 2.0f64.sqrt());
410      }
411  
412      // Here's an example of lines where we used to have a bad
413      // approximation. Check that they only require a single
414      // approximation step.
415      #[test]
416      fn linear_approx() {
417          let mut p0 = BezPath::new();
418          p0.move_to((-212.00062561035156, 90.03582000732422));
419          p0.line_to((211.99937438964844, 90.03646087646484));
420          p0.close_path();
421  
422          let mut p1 = BezPath::new();
423          p1.move_to((211.99964904785153, -90.0358200073242));
424          p1.line_to((211.99937438964844, 90.03646087646484));
425          p1.close_path();
426  
427          let mut segs = Segments::default();
428          segs.add_bez_path(&p0).unwrap();
429          segs.add_bez_path(&p1).unwrap();
430          let s0 = segs.indices().next().unwrap();
431          let s1 = segs.indices().nth(2).unwrap();
432  
433          let mut out = Vec::new();
434          let y1 = 90.03646087646484;
435          let next_y0 = super::approximate(90.03645987646233, y1, &[s0, s1], &segs, 2.5e-7, &mut out);
436          assert_eq!(next_y0, y1);
437      }
438  }