Files
vectorcraft/crates/pathops/src/planar/map.rs
T

554 lines
25 KiB
Rust

use super::graph::{
Edge, Fill, Outlines, cycle_bezpath, far_end, find, half_segs, leaving_angle, origin, pin_ends, there_and_back, twin, unshear, without_spurs,
};
use super::index::{Crossings, RectIndex, SHEAR, UNSHEAR, mid};
use super::*;
/// What is left of input `shape` when the faces that hold `merged` merge on their own, with the
/// inputs that cover them marked in `cover` ([`Map::merge`]).
pub(crate) struct Rest {
pub(crate) shape: usize,
pub(crate) merged: Vec<Point>,
pub(crate) cover: Vec<bool>,
}
/// The planar graph of `shapes` (in the sweep's frame) with its faces walked: what [`build`] makes
/// Live Paint faces and edges from, and what [`Map::merge`] merges faces in.
pub(crate) struct Map {
edges: Vec<Edge>,
around: Vec<Vec<usize>>,
slot_of: Vec<usize>,
/// Bounded faces: their cycle of half-edges, path and area; and, by their bounds, which can
/// hold a point, so finding the face around one tests only those.
faces: Vec<(Vec<usize>, BezPath, f64)>,
face_index: RectIndex<usize>,
/// Bounded faces left out as slivers ([`is_sliver`]): their cycle of half-edges and the inputs
/// whose fill covers them (none for a gap, such as a thin cutout).
pub(super) slivers: Vec<(Vec<usize>, Vec<usize>)>,
/// Outer boundaries of connected pieces, and the face each one is a hole in.
holes: Vec<(Vec<usize>, BezPath)>,
face_holes: Vec<Vec<usize>>,
tidy: Tidy,
/// The inputs' anchors (sorted by x) and how close results snap back onto them.
anchors: Vec<Point>,
snap: f64,
}
/// With `cover`, also finds which inputs cover each sliver, which only [`Map::merge`] reads;
/// without it the slivers' lists of inputs stay empty.
pub(crate) fn map(shapes: &[Shape], cover: bool) -> Option<Map> {
map_with(shapes, cover, DEFAULT_PRECISION)
}
/// [`map`] whose faces and pieces are refit to `precision`, and whose slivers thinner than it are dropped.
fn map_with(shapes: &[Shape], cover: bool, precision: f64) -> Option<Map> {
let mut inputs: Vec<(BezPath, usize)> = Vec::new();
for (i, s) in shapes.iter().enumerate() {
let mut bp = BezPath::new();
for sp in s.path.subpaths.iter().filter(|sp| sp.anchors.len() >= 2) {
if sp.closed {
sp.to_bezpath_into(&mut bp);
} else {
let mut open = BezPath::new();
sp.to_bezpath_into(&mut open);
there_and_back(&open, &mut bp);
}
}
if !bp.elements().is_empty() {
bp.apply_affine(SHEAR);
inputs.push((bp, i));
}
}
if inputs.is_empty() {
return None;
}
let eps = eps_for(inputs.iter().map(|p| &p.0)).ok()?;
snap_horizontals(&mut inputs.iter_mut().map(|p| &mut p.0).collect::<Vec<_>>(), eps);
// Counted on the snapped geometry, which is what the sweep sees: snapping can bring boxes into contact.
if !crate::budget::sweep_fits(inputs.iter().map(|p| &p.0)) {
return None;
}
let tidy = Tidy::keeping(precision, inputs.iter().map(|p| &p.0));
let (points, edges) = sweep_graph(&inputs, eps, precision)?;
let nv = points.len();
// Half-edges leaving each vertex, sorted by direction; walking a face, the next half-edge is
// the one just clockwise of the way back, so bounded faces come out with positive area and
// the outer boundary of each connected piece with negative area.
let mut around: Vec<Vec<usize>> = vec![Vec::new(); nv];
for h in 0..2 * edges.len() {
if let Some(list) = origin(&edges, h).and_then(|v| around.get_mut(v)) {
list.push(h);
}
}
let mut slot_of = vec![0usize; 2 * edges.len()];
for (v, list) in around.iter_mut().enumerate() {
let Some(&at) = points.get(v) else { continue };
// Half the shortest chord of the edges here: every one of them gets that far from it.
let r = 0.5 * list.iter().filter_map(|&h| far_end(&edges, h).map(|p| p.distance(at))).fold(f64::INFINITY, f64::min);
let mut keyed: Vec<(f64, usize)> = list.iter().map(|&h| (leaving_angle(&edges, h, at, r), h)).collect();
keyed.sort_by(|a, b| a.0.total_cmp(&b.0));
*list = keyed.into_iter().map(|(_, h)| h).collect();
for (k, &h) in list.iter().enumerate() {
if let Some(s) = slot_of.get_mut(h) {
*s = k;
}
}
}
let next = |h: usize| -> Option<usize> {
let back = twin(h);
let list = around.get(origin(&edges, back)?)?;
let k = *slot_of.get(back)?;
list.get((k + list.len() - 1) % list.len()).copied()
};
// Connected pieces of the graph.
let mut parent: Vec<usize> = (0..nv).collect();
for e in &edges {
let (a, b) = (find(&mut parent, e.ends[0]), find(&mut parent, e.ends[1]));
if let Some(p) = parent.get_mut(a) {
*p = b;
}
}
let piece: Vec<usize> = (0..nv).map(|v| find(&mut parent, v)).collect();
let piece_of = |cycle: &[usize]| cycle.first().and_then(|&h| origin(&edges, h)).and_then(|v| piece.get(v)).copied();
// Walk every cycle: positive ones are faces, negative ones outer boundaries (holes in the
// face around them, if any).
let mut seen = vec![false; 2 * edges.len()];
let mut faces: Vec<(Vec<usize>, BezPath, f64)> = Vec::new();
let mut slivers: Vec<(Vec<usize>, BezPath)> = Vec::new();
let mut holes: Vec<(Vec<usize>, BezPath)> = Vec::new();
for start in 0..2 * edges.len() {
if seen.get(start).copied().unwrap_or(true) {
continue;
}
let mut cycle = vec![];
let mut h = start;
while let Some(s) = seen.get_mut(h).filter(|s| !**s) {
*s = true;
cycle.push(h);
match next(h) {
Some(n) => h = n,
None => break,
}
}
let cycle = without_spurs(&cycle);
if cycle.is_empty() {
continue;
}
let bp = cycle_bezpath(&edges, &cycle);
let area = bp.area();
if area > 0.0 && !is_sliver(&bp, tidy.precision) {
faces.push((cycle, bp, area));
} else if area > 0.0 {
slivers.push((cycle, bp));
} else if area < -(tidy.precision * tidy.precision) {
holes.push((cycle, bp));
}
}
// Each hole goes in the smallest face of another piece around it.
let face_index = RectIndex::new(faces.iter().enumerate().map(|(i, (_, bp, _))| (bp.bounding_box(), i)).collect());
let mut face_holes: Vec<Vec<usize>> = vec![Vec::new(); faces.len()];
for (k, (cycle, _)) in holes.iter().enumerate() {
let Some(p) = cycle.first().and_then(|&h| origin(&edges, h)).and_then(|v| points.get(v)).copied() else { continue };
let own = piece_of(cycle);
let around_it = face_index
.at(p, 0.0)
.filter_map(|&i| faces.get(i).map(|f| (i, f)))
.filter(|(_, (fc, bp, _))| piece_of(fc) != own && bp.winding(p) != 0)
.min_by(|a, b| a.1.2.total_cmp(&b.1.2))
.map(|(i, _)| i);
if let Some(list) = around_it.and_then(|i| face_holes.get_mut(i)) {
list.push(k);
}
}
// The inputs that cover each sliver, tested at a point inside it.
let fills: Vec<Fill> = if slivers.is_empty() || !cover { Vec::new() } else { shapes.iter().map(Fill::new).collect() };
let slivers: Vec<(Vec<usize>, Vec<usize>)> = slivers
.into_iter()
.map(|(cycle, mut bp)| {
if !cover {
return (cycle, Vec::new());
}
bp.apply_affine(UNSHEAR);
let sources = interior_point(&PathData::from_bezpath(&bp))
.map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect())
.unwrap_or_default();
(cycle, sources)
})
.collect();
// The inputs' anchors, which results land on again up to the sweep's tolerance.
let snap = 2.0 * eps;
let mut anchors: Vec<Point> = shapes.iter().flat_map(|s| s.path.subpaths.iter().flat_map(|sp| sp.anchors.iter().map(|a| a.p))).collect();
anchors.sort_by(|a, b| a.x.total_cmp(&b.x).then(a.y.total_cmp(&b.y)));
Some(Map { edges, around, slot_of, faces, face_index, slivers, holes, face_holes, tidy, anchors, snap })
}
fn sweep_graph(inputs: &[(BezPath, usize)], eps: f64, precision: f64) -> Option<(Vec<Point>, Vec<Edge>)> {
// The sweep can panic on rare near-degenerate input; that must never take the app down.
let sweep =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| Topology::<Crossings>::from_paths(inputs.iter().map(|(p, i)| (p, *i)), eps)));
let top = sweep.ok()?.ok()?;
// The planar graph: vertices are the sweep's points, edges its segments. Points closer than
// the sweep's tolerance are one vertex (it can put a corner that lies on another path's edge
// a rounding error away from where it cuts that edge).
let positions = top.compute_positions();
let cell = 4.0 * eps;
let mut grid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
let mut points: Vec<Point> = Vec::new();
let mut vertex_id = |p: Point| {
let (cx, cy) = ((p.x / cell).floor() as i64, (p.y / cell).floor() as i64);
let near = (cx - 1..=cx + 1)
.flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y)))
.filter_map(|k| grid.get(&k))
.flatten()
.copied()
.find(|&v| points.get(v).is_some_and(|q| q.distance(p) <= eps));
near.unwrap_or_else(|| {
points.push(p);
grid.entry((cx, cy)).or_default().push(points.len() - 1);
points.len() - 1
})
};
let mut pieces: Vec<(Vec<Seg>, [usize; 2])> = Vec::new();
for idx in top.segment_indices() {
let segs: Vec<Seg> = positions[idx].path.segments().map(to_seg).collect();
let ends = [vertex_id(top.point(idx.first_half()).to_kurbo()), vertex_id(top.point(idx.second_half()).to_kurbo())];
if !segs.is_empty() && ends[0] != ends[1] {
pieces.push((segs, ends));
}
}
// The sweep can cut the paths through one crossing at points more than `eps` apart, so copies of
// one edge (such as an open path's two passes) can end on different vertices. Pieces whose
// middles and ends lie within `tol` of each other are such copies, and their ends are one vertex.
let tol = 16.0 * eps;
let cell_of = |p: Point| ((p.x / tol).floor() as i64, (p.y / tol).floor() as i64);
let mids: Vec<Option<Point>> = pieces.iter().map(|(segs, _)| mid(segs)).collect();
let mut by_mid: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
for (k, m) in mids.iter().enumerate() {
if let Some(m) = m {
by_mid.entry(cell_of(*m)).or_default().push(k);
}
}
let close = |a: usize, b: usize| matches!((points.get(a), points.get(b)), (Some(p), Some(q)) if p.distance(*q) <= tol);
let mut joined: Vec<usize> = (0..points.len()).collect();
// Where two passes of one path cross another (an open path's way there and back, coincident
// outlines), the sweep can also join its two cuts, a rounding error apart, with a stub of that
// length: a speck of a piece, and a vertex twice over. Such stubs are no edges: their ends are
// one vertex. The sweep moves points by a few `eps` at most up and down its frame; along a path
// that runs nearly level in it (where the height a cut is found at pins its position down
// least) a cut can slide further, here up to a small fraction of the precision results are
// tidied to, below which no edge is drawn anyway. A vertex's cluster stays within those bounds
// as a whole: a run of short, nearly level segments (a finely flattened or traced path) is real
// geometry, not one stub after another, and must not collapse into one vertex.
let (stub_y, stub_x) = (4.0 * eps, (DEFAULT_PRECISION / 16.0).max(4.0 * eps));
let mut extent: Vec<kurbo::Rect> = points.iter().map(|&p| kurbo::Rect::from_points(p, p)).collect();
for (segs, [v0, v1]) in &pieces {
let Some(&p0) = points.get(*v0) else { continue };
if !segs.iter().flat_map(|s| [s.c.p1, s.c.p2, s.c.p3]).all(|p| (p.y - p0.y).abs() <= stub_y && (p.x - p0.x).abs() <= stub_x) {
continue;
}
let (a, b) = (find(&mut joined, *v0), find(&mut joined, *v1));
let (Some(&ra), Some(&rb)) = (extent.get(a), extent.get(b)) else { continue };
let both = ra.union(rb);
if a == b || both.width() > stub_x || both.height() > stub_y {
continue;
}
let (root, child) = (a.min(b), a.max(b));
if let Some(p) = joined.get_mut(child) {
*p = root;
}
if let Some(r) = extent.get_mut(root) {
*r = both;
}
}
for (k, (_, [a, b])) in pieces.iter().enumerate() {
let Some(&Some(m)) = mids.get(k) else { continue };
let (cx, cy) = cell_of(m);
let others = (cx - 1..=cx + 1).flat_map(|x| (cy - 1..=cy + 1).map(move |y| (x, y))).filter_map(|c| by_mid.get(&c)).flatten();
for &j in others.filter(|&&j| j > k) {
let (Some((_, [c, d])), Some(&Some(n))) = (pieces.get(j), mids.get(j)) else { continue };
if m.distance(n) > tol {
continue;
}
let pairs = if close(*a, *c) && close(*b, *d) {
[(*a, *c), (*b, *d)]
} else if close(*a, *d) && close(*b, *c) {
[(*a, *d), (*b, *c)]
} else {
continue;
};
for (x, y) in pairs {
let (x, y) = (find(&mut joined, x), find(&mut joined, y));
if let Some(r) = joined.get_mut(x.max(y)) {
*r = x.min(y);
}
}
}
}
let mut edges: Vec<Edge> = Vec::new();
let mut between: HashMap<(usize, usize), Vec<usize>> = HashMap::new();
let outlines = Outlines::new(inputs, eps);
for (mut segs, ends) in pieces {
let ends = ends.map(|v| find(&mut joined, v));
let (Some(&p0), Some(&p1)) = (points.get(ends[0]), points.get(ends[1])) else { continue };
if ends[0] == ends[1] {
continue;
}
// Every edge at a vertex starts or ends exactly on it, so faces meeting there share its
// coordinates: merging them (a union) then cancels their common edge, which it can't when
// each face carries its own rounding of the corner (#893).
pin_ends(&mut segs, p0, p1);
let Some(source) = mid(&segs).and_then(|m| outlines.source_at(m)) else { continue };
// Coincident pieces the sweep left apart are one edge (the front-most input's).
let key = (ends[0].min(ends[1]), ends[0].max(ends[1]));
let here = mid(&segs);
let twin_of = between.get(&key).and_then(|v| {
v.iter().copied().find(|&e| {
let other = edges.get(e).and_then(|e| mid(&e.segs));
matches!((here, other), (Some(a), Some(b)) if a.distance(b) < precision)
})
});
if let Some(e) = twin_of {
if let Some(e) = edges.get_mut(e)
&& source > e.source
{
e.source = source;
}
continue;
}
between.entry(key).or_default().push(edges.len());
edges.push(Edge { segs, ends, source });
}
Some((points, edges))
}
impl Map {
/// The union of the faces that hold `inside` (document points, one per face), traced along the
/// map's own edges: those between two merged faces drop out exactly, where a boolean union of
/// the faces' outlines can leave them in when its sweep rounds their shared corners apart
/// (#893). A sliver next to a merged face that only inputs marked in `cover` fill merges with
/// them, so it doesn't stay as a hairline gap between them. With `rest`, a sliver goes in only
/// when its input covers it, and not when the merge of the faces merged on their own takes it
/// in. Outer outlines come out positive, holes negative. None when a point is in no face.
pub(crate) fn merge(&self, inside: &[Point], cover: &[bool], rest: Option<&Rest>) -> Option<PathData> {
let halves = 2 * self.edges.len();
// The face on each half-edge's side; holes bound the face around them. Slivers come after
// the faces.
let mut face_of: Vec<Option<usize>> = vec![None; halves];
for (i, (cycle, ..)) in self.faces.iter().enumerate() {
let hole_cycles = self.face_holes.get(i)?.iter().filter_map(|&k| self.holes.get(k)).map(|(c, _)| c);
for &h in std::iter::once(cycle).chain(hole_cycles).flatten() {
*face_of.get_mut(h)? = Some(i);
}
}
for (k, (cycle, _)) in self.slivers.iter().enumerate() {
for &h in cycle {
*face_of.get_mut(h)? = Some(self.faces.len() + k);
}
}
// The smallest face around a point: faces of a piece inside a hole lie within the face around.
let face_at = |p: Point| {
let q = SHEAR * p;
self.face_index
.at(q, 0.0)
.filter_map(|&i| self.faces.get(i).map(|f| (i, f)))
.filter(|(_, (_, bp, _))| bp.winding(q) != 0)
.min_by(|a, b| a.1.2.total_cmp(&b.1.2))
.map(|(i, _)| i)
};
let mut picked = vec![false; self.faces.len() + self.slivers.len()];
for &p in inside {
*picked.get_mut(face_at(p)?)? = true;
}
let mut other = vec![false; self.faces.len()];
for i in rest.iter().flat_map(|r| &r.merged).filter_map(|&p| face_at(p)) {
if let Some(o) = other.get_mut(i) {
*o = true;
}
}
let next_to = |cycle: &[usize], marked: &[bool]| {
cycle.iter().any(|&h| face_of.get(twin(h)).copied().flatten().is_some_and(|f| f < self.faces.len() && marked.get(f) == Some(&true)))
};
for (k, (cycle, sources)) in self.slivers.iter().enumerate() {
let within = |c: &[bool]| !sources.is_empty() && sources.iter().all(|&i| c.get(i) == Some(&true));
let ours = rest.is_none_or(|r| sources.contains(&r.shape) && !(within(&r.cover) && next_to(cycle, &other)));
if within(cover) && ours && next_to(cycle, &picked) {
*picked.get_mut(self.faces.len() + k)? = true;
}
}
let merged = |h: usize| face_of.get(h).copied().flatten().is_some_and(|f| picked.get(f) == Some(&true));
let boundary = |h: usize| merged(h) && !merged(twin(h));
let mut used = vec![false; halves];
let mut subpaths = Vec::new();
for start in 0..halves {
if !boundary(start) || used.get(start).copied().unwrap_or(true) {
continue;
}
let mut cycle = vec![];
let mut h = start;
while !used.get(h).copied().unwrap_or(true) {
if let Some(u) = used.get_mut(h) {
*u = true;
}
cycle.push(h);
// Turn from the way back as a face walk does, past the edges inside the merged
// faces (between two of them, or a line's loose end), to the next boundary edge.
let back = twin(h);
let list = self.around.get(origin(&self.edges, back)?)?;
let k = *self.slot_of.get(back)?;
let n = list.len();
h = (1..=n).filter_map(|j| list.get((k + n * 2 - j) % n).copied()).find(|&g| boundary(g))?;
}
if h != start {
return None;
}
let segs: Vec<Seg> = cycle.iter().flat_map(|&h| half_segs(&self.edges, h)).collect();
subpaths.push(segs_to_subpath(&tidy_segments(segs, true, &self.tidy), true)?);
}
(!subpaths.is_empty()).then(|| unshear(PathData::new(subpaths), &self.anchors, self.snap))
}
}
pub(super) fn build(shapes: &[Shape], precision: f64) -> Option<(Vec<Region>, Vec<Shape>)> {
let m = map_with(shapes, false, precision)?;
let Map { edges, around, faces, holes, face_holes, tidy, anchors, snap, .. } = &m;
let snap = *snap;
let fills: Vec<Fill> = shapes.iter().map(Fill::new).collect();
let subpath = |cycle: &[usize]| {
let segs: Vec<Seg> = cycle.iter().flat_map(|&h| half_segs(edges, h)).collect();
segs_to_subpath(&tidy_segments(segs, true, tidy), true)
};
let regions = faces
.iter()
.zip(face_holes)
.filter_map(|((cycle, _, _), hs)| {
let mut subpaths = vec![subpath(cycle)?];
subpaths.extend(hs.iter().filter_map(|&k| holes.get(k)).filter_map(|(c, _)| subpath(c)));
let path = unshear(PathData::new(subpaths), anchors, snap);
let sources =
// A face finer than the default precision can need a finer outline to find a point in.
interior_point(&path)
.or_else(|| (precision < DEFAULT_PRECISION).then(|| interior_point_within(&path, precision / 4.0)).flatten())
.map(|p| fills.iter().enumerate().filter(|(_, f)| f.contains(p)).map(|(i, _)| i).collect())
.unwrap_or_default();
Some(Region { path, sources })
})
.collect();
// Edges: chains of segments through vertices where nothing else meets.
let degree: Vec<usize> = around.iter().map(Vec::len).collect();
let passes = |v: usize| -> Option<(usize, usize)> {
let list = around.get(v)?;
let (&a, &b) = (list.first()?, list.get(1)?);
(degree.get(v) == Some(&2) && edges.get(a / 2)?.source == edges.get(b / 2)?.source).then_some((a, b))
};
let mut used = vec![false; edges.len()];
let mut chains: Vec<(Vec<Seg>, bool, usize)> = Vec::new();
// A chain from half-edge `h0` up to the next vertex where it doesn't pass straight through;
// closed when it comes back round to where it started.
let walk = |h0: usize, used: &mut Vec<bool>| -> Option<(Vec<Seg>, bool, usize)> {
let source = edges.get(h0 / 2)?.source;
let mut segs = Vec::new();
let mut h = h0;
loop {
let u = used.get_mut(h / 2)?;
if *u {
return Some((segs, true, source));
}
*u = true;
segs.extend(half_segs(edges, h));
let v = origin(edges, twin(h))?;
let Some((a, b)) = passes(v) else { return Some((segs, origin(edges, h0) == Some(v), source)) };
h = if a == twin(h) { b } else { a };
}
};
for v in 0..around.len() {
if passes(v).is_some() {
continue;
}
for &h in around.get(v).map(Vec::as_slice).unwrap_or_default() {
if !used.get(h / 2).copied().unwrap_or(true)
&& let Some(c) = walk(h, &mut used)
{
chains.push(c);
}
}
}
// What's left are closed loops with no junction on them.
for e in 0..edges.len() {
if !used.get(e).copied().unwrap_or(true)
&& let Some(c) = walk(2 * e, &mut used)
{
chains.push(c);
}
}
let edge_shapes = chains
.into_iter()
.filter_map(|(segs, closed, source)| {
let sp = segs_to_subpath(&tidy_segments(segs, closed, tidy), closed)?;
Some(Shape::new(unshear(PathData::new(vec![sp]), anchors, snap), FillRule::NonZero, shapes.get(source)?.key))
})
.collect();
Some((regions, edge_shapes))
}
/// A point strictly inside a filled path, well away from its edges where it can be (for re-finding
/// a Live Paint face after a rebuild).
pub fn interior_point(path: &PathData) -> Option<Point> {
let bb = path.bounds()?;
interior_point_within(path, (bb.width().max(bb.height()) * 1e-3).max(1e-4))
}
/// [`interior_point`] with the path's curves flattened to within `tolerance`.
fn interior_point_within(path: &PathData, tolerance: f64) -> Option<Point> {
let bb = path.bounds()?;
let bp = path.to_bezpath();
let mut edges: Vec<(Point, Point)> = vec![];
let (mut first, mut last) = (Point::ZERO, Point::ZERO);
kurbo::flatten(bp.iter(), tolerance, |el| match el {
kurbo::PathEl::MoveTo(p) => {
if last != first {
edges.push((last, first));
}
first = p;
last = p;
}
kurbo::PathEl::LineTo(p) => {
edges.push((last, p));
last = p;
}
kurbo::PathEl::ClosePath => {
if last != first {
edges.push((last, first));
}
last = first;
}
_ => {}
});
if last != first {
edges.push((last, first));
}
let mut best: Option<(f64, Point)> = None;
for f in [0.5, 0.25, 0.75, 0.125, 0.375, 0.625, 0.875, 0.0625, 0.9375] {
// A little off the round fractions, which on art drawn to a grid fall on the height of
// some corner, where testing the point against other paths is unreliable.
let y = bb.y0 + bb.height() * (f + 0.0061803);
let mut xs: Vec<f64> =
edges.iter().filter(|(a, c)| (a.y <= y) != (c.y <= y)).map(|(a, c)| a.x + (y - a.y) / (c.y - a.y) * (c.x - a.x)).collect();
xs.sort_by(f64::total_cmp);
for w in xs.windows(2) {
let m = Point::new((w[0] + w[1]) / 2.0, y);
let width = w[1] - w[0];
if width > best.map_or(0.0, |b| b.0) && bp.winding(m) != 0 {
best = Some((width, m));
}
}
}
best.map(|b| b.1)
}