Skip to content
This repository was archived by the owner on Jan 25, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions src/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,11 @@ pub fn monotonic_bezier(u: &mut Unstructured<'_>) -> Result<CubicBez, arbitrary:
let ret = CubicBez::new(p0, p1, p2, p3);
Ok(geom::monotonic_pieces(ret)
.into_iter()
.find(|c| c.p0.y < c.p3.y)
.find(|c| c.piece.p0.y < c.piece.p3.y)
// unwrap: we started with p0 having smaller y, so there must be
// a monotonic component that's increasing in y.
.unwrap())
.unwrap()
.piece)
}

/// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
Expand Down Expand Up @@ -104,6 +105,7 @@ pub fn another_monotonic_bezier(
Ok(geom::monotonic_pieces(ret)
.into_iter()
.next()
.map(|c| c.piece)
.unwrap_or(ret))
}

Expand Down
5 changes: 4 additions & 1 deletion src/curve/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,10 @@ fn cubic_from_bez_y(c: CubicBez) -> Cubic {

/// Find the parameter `t` at which `c` crosses height `y`.
pub fn solve_t_for_y(c: CubicBez, y: f64) -> f64 {
debug_assert!(c.p0.y <= y && y <= c.p3.y && c.p0.y < c.p3.y);
debug_assert!(
c.p0.y <= y && y <= c.p3.y && c.p0.y < c.p3.y,
"invalid y ({y}) for curve ({c:?})"
);

if y == c.p0.y {
return 0.0;
Expand Down
21 changes: 18 additions & 3 deletions src/geom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,14 @@ fn force_monotonic(mut cub: CubicBez) -> Option<CubicBez> {
}
}

pub(crate) fn monotonic_pieces(cub: CubicBez) -> ArrayVec<CubicBez, 3> {
/// A y-monotonic piece of a cubic Bezier.
pub(crate) struct MonotonicPiece {
pub start_t: f64,
pub end_t: f64,
pub piece: CubicBez,
}

pub(crate) fn monotonic_pieces(cub: CubicBez) -> ArrayVec<MonotonicPiece, 3> {
let mut ret = ArrayVec::new();
let q0 = cub.p1.y - cub.p0.y;
let q1 = cub.p2.y - cub.p1.y;
Expand Down Expand Up @@ -199,15 +206,23 @@ pub(crate) fn monotonic_pieces(cub: CubicBez) -> ArrayVec<CubicBez, 3> {
if r > 0.0 && r < 1.0 {
let piece_before = cub.subsegment(last_r..r);
if let Some(c) = force_monotonic(piece_before) {
ret.push(c)
ret.push(MonotonicPiece {
piece: c,
start_t: last_r,
end_t: r,
});
}
last_r = r;
}
}

let piece_before = cub.subsegment(last_r..1.0);
if let Some(c) = force_monotonic(piece_before) {
ret.push(c)
ret.push(MonotonicPiece {
piece: c,
start_t: last_r,
end_t: 1.0,
});
}

ret
Expand Down
79 changes: 45 additions & 34 deletions src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ fn ordered_curves_all_close(
segs: &Segments,
order: &[SegIdx],
output_order: &[OutputSegIdx],
out: &mut OutputSegVec<(BezPath, Option<usize>)>,
out: &mut OutputSegVec<PositionedOutputSeg>,
mut y0: f64,
y1: f64,
endpoints: &HalfOutputSegVec<kurbo::Point>,
Expand All @@ -46,23 +46,23 @@ fn ordered_curves_all_close(
let o0 = output_order[0];
let o1 = output_order[1];

let p0 = bez_end(&out[o0].0);
let p1 = bez_end(&out[o1].0);
let p0 = bez_end(&out[o0].path);
let p1 = bez_end(&out[o1].path);
let q0 = endpoints[o0.second_half()];
let q1 = endpoints[o1.second_half()];

if p0.y == y0 && p1.y == y0 {
let c0 = next_subsegment(&segs[s0], &out[o0].0, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].0, y1, q1);
let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1);

if transversal_after(c0, c1, y1) {
return;
}
}

if q0.y == y1 && q1.y == y1 {
let c0 = next_subsegment(&segs[s0], &out[o0].0, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].0, y1, q1);
let c0 = next_subsegment(&segs[s0], &out[o0].path, y1, q0);
let c1 = next_subsegment(&segs[s1], &out[o1].path, y1, q1);

if transversal_before(c0, c1, y0) {
return;
Expand All @@ -73,15 +73,15 @@ fn ordered_curves_all_close(
// Ensure everything in `out` goes up to `y0`. Anything that doesn't go up to `y0` is
// an output where we can just copy from the input.
for (&seg_idx, &out_idx) in order.iter().zip(output_order) {
let (out_bez, out_copied_idx) = &mut out[out_idx];
if bez_end_y(out_bez) < y0 {
*out_copied_idx = Some(out_bez.elements().len() - 1);
let out = &mut out[out_idx];
if bez_end_y(&out.path) < y0 {
out.copied_idx = Some(out.path.elements().len() - 1);
let endpoint = endpoints[out_idx.second_half()];
if segs[seg_idx].is_line() {
out_bez.line_to(endpoint)
out.path.line_to(endpoint)
} else {
let c = next_subsegment(&segs[seg_idx], out_bez, y0, endpoint);
out_bez.curve_to(c.p1, c.p2, c.p3);
let c = next_subsegment(&segs[seg_idx], &out.path, y0, endpoint);
out.path.curve_to(c.p1, c.p2, c.p3);
}
}
}
Expand Down Expand Up @@ -110,7 +110,7 @@ fn ordered_curves_all_close(
x1_max_so_far = x1_max_so_far.max(x1);

out[*out_idx]
.0
.path
.quad_to((x_mid_max_so_far, y_mid), (x1_max_so_far, next_y0));
}
approxes.clear();
Expand Down Expand Up @@ -225,26 +225,37 @@ fn next_subsegment(seg: &Segment, out: &BezPath, y1: f64, endpoint: kurbo::Point
c
}

/// A positioned output segment, possibly perturbed from the original.
///
/// We perturb output segments in order to strictly enforce ordering.
/// A single segment might be expanded into a longer path.
#[derive(Default)]
pub struct PositionedOutputSeg {
/// The path, which is guaranteed to start and end at the same points as the
/// output segment did.
pub path: BezPath,
/// Sometimes (often, even) a large part of the output segment didn't need
/// to be perturbed. Our sweep-line invariants guarantee that unperturbed
/// part can only be a single contiguous interval: this stores the index
/// of the unperturbed part within `path.segments()`.
pub copied_idx: Option<usize>,
}

/// Compute positions for all of the output segments.
///
/// The orders between the output segments is specified by `order`. The endpoints
/// should have been already computed (in a way that satisfies the order), and
/// are provided in `endpoints`. For each output segment, we return a Bézier
/// path.
///
/// The `usize` return value tells which segment (if any) in the returned
/// path was the one that was "far" from any other paths. This is really
/// only interesting for diagnosis/visualization so the API should probably
/// be refined somehow to make it optional. (TODO)
pub(crate) fn compute_positions(
segs: &Segments,
orig_seg_map: &OutputSegVec<SegIdx>,
cmp: &mut ComparisonCache,
endpoints: &HalfOutputSegVec<kurbo::Point>,
scan_order: &ScanLineOrder,
accuracy: f64,
) -> OutputSegVec<(BezPath, Option<usize>)> {
let mut out = OutputSegVec::<(BezPath, Option<usize>)>::with_size(orig_seg_map.len());
) -> OutputSegVec<PositionedOutputSeg> {
let mut out = OutputSegVec::<PositionedOutputSeg>::with_size(orig_seg_map.len());
// We try to build `out` lazily, by avoiding copying input segments to outputs segments
// until they're needed (by copying in one go, we avoid excess subdivisions). That means
// we need to separately keep track of how far down we've looked at each output. If
Expand All @@ -255,12 +266,12 @@ pub(crate) fn compute_positions(
let p = endpoints[idx.first_half()];
let q = endpoints[idx.second_half()];
if p.y == q.y {
out[idx].0.move_to(p);
out[idx].0.line_to(q);
out[idx].1 = Some(0);
out[idx].path.move_to(p);
out[idx].path.line_to(q);
out[idx].copied_idx = Some(0);
continue;
}
out[idx].0.move_to(p);
out[idx].path.move_to(p);
queue.push(HeapEntry { y: p.y.into(), idx });
}

Expand Down Expand Up @@ -320,8 +331,8 @@ pub(crate) fn compute_positions(
// we'll hold off on the copying because it might allow us to avoid further
// subdivision.
if y0 == y1 {
out[idx].1 = Some(out[idx].0.elements().len() - 1);
out[idx].0.line_to(endpoints[idx.second_half()]);
out[idx].copied_idx = Some(out[idx].path.elements().len() - 1);
out[idx].path.line_to(endpoints[idx.second_half()]);
}
} else {
let orig_neighbors = neighbors
Expand Down Expand Up @@ -350,22 +361,22 @@ pub(crate) fn compute_positions(
}

for out_idx in out.indices() {
let (out_bez, out_copied_idx) = &mut out[out_idx];
let out = &mut out[out_idx];
let endpoint = endpoints[out_idx.second_half()];
if bez_end_y(out_bez) < endpoint.y {
if bez_end_y(&out.path) < endpoint.y {
let seg_idx = orig_seg_map[out_idx];
*out_copied_idx = Some(out_bez.elements().len() - 1);
out.copied_idx = Some(out.path.elements().len() - 1);
if segs[seg_idx].is_line() {
out_bez.line_to(endpoint)
out.path.line_to(endpoint)
} else {
let c = next_subsegment(&segs[seg_idx], out_bez, endpoint.y, endpoint);
out_bez.curve_to(c.p1, c.p2, c.p3);
let c = next_subsegment(&segs[seg_idx], &out.path, endpoint.y, endpoint);
out.path.curve_to(c.p1, c.p2, c.p3);
}
} else {
// The quadratic approximations don't respect the fixed endpoints, so tidy them
// up. Since both the quadratic approximations and the endpoints satisfy
// the ordering, this doesn't mess up the ordering.
match out[out_idx].0.elements_mut().last_mut().unwrap() {
match out.path.elements_mut().last_mut().unwrap() {
kurbo::PathEl::MoveTo(p)
| kurbo::PathEl::LineTo(p)
| kurbo::PathEl::QuadTo(_, p)
Expand Down
23 changes: 22 additions & 1 deletion src/segments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ pub struct Segments {
/// For each segment, stores true if the sweep-line order (small y to big y)
/// is the same as the orientation in its original contour.
orientation: SegVec<bool>,
/// For each segment, stores its parameter range in the original input
/// segment (which may have been split into monotonic sub-segments). Keeping
/// track of where the splits happened allows us to potentially merge things
/// back at the end.
pub(crate) split_from_predecessor: SegVec<(f64, f64)>,
/// The original input segments, for reconstructing/merging. (TODO: this
/// representation is wasteful, since we only need it for segments that
/// got split.)
pub(crate) input_segs: SegVec<kurbo::PathSeg>,

/// All the entrance heights, of segments, ordered by height.
/// This includes horizontal segments.
Expand Down Expand Up @@ -245,6 +254,9 @@ impl Segments {
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.segs.push(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
Expand Down Expand Up @@ -324,6 +336,8 @@ impl Segments {
if p0 != p1 {
self.segs.push(Segment::straight(p0, p1));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs.push(seg);
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
Expand All @@ -332,7 +346,8 @@ impl Segments {
_ => {
let cubic = to_cubic(seg);
let cubics = monotonic_pieces(cubic);
for c in cubics {
for monotonic in cubics {
let c = monotonic.piece;
let (p0, p1, p2, p3, orient) = if (c.p0.y, c.p0.x) <= (c.p3.y, c.p3.x) {
(c.p0, c.p1, c.p2, c.p3, true)
} else {
Expand All @@ -345,6 +360,9 @@ impl Segments {
p3.into(),
));
self.orientation.push(orient);
self.split_from_predecessor
.push((monotonic.start_t, monotonic.end_t));
self.input_segs.push(seg);
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
Expand Down Expand Up @@ -389,6 +407,9 @@ impl Segments {
let (a, b, orient) = if p < q { (p, q, true) } else { (q, p, false) };
self.segs.push(Segment::straight(*a, *b));
self.orientation.push(orient);
self.split_from_predecessor.push((0.0, 1.0));
self.input_segs
.push(kurbo::PathSeg::Line((p.to_kurbo(), q.to_kurbo()).into()));
self.contour_prev
.push(Some(SegIdx(self.segs.len().saturating_sub(2))));
self.contour_next.push(Some(SegIdx(self.segs.len())));
Expand Down
Loading