1074 lines
36 KiB
Rust
1074 lines
36 KiB
Rust
//! Region selection path clipping
|
|
//!
|
|
//! Clips BezPaths against a closed polygon region (rectangle or lasso),
|
|
//! producing separate inside and outside paths.
|
|
//!
|
|
//! Uses a Weiler-Atherton-style approach: walk the subject path, alternating
|
|
//! between following the subject (when inside) and following the clip boundary
|
|
//! (when transitioning between crossings).
|
|
|
|
use vello::kurbo::{
|
|
BezPath, CubicBez, Line, ParamCurve, PathEl, Point, Rect, Shape as KurboShape,
|
|
};
|
|
|
|
/// Result of clipping a shape path against a region
|
|
#[derive(Debug, Clone)]
|
|
pub struct ClipResult {
|
|
/// Path segments inside the region
|
|
pub inside: BezPath,
|
|
/// Path segments outside the region
|
|
pub outside: BezPath,
|
|
}
|
|
|
|
/// Convert a Rect to a closed BezPath (4 line segments)
|
|
pub fn rect_to_path(rect: Rect) -> BezPath {
|
|
let mut path = BezPath::new();
|
|
path.move_to(Point::new(rect.x0, rect.y0));
|
|
path.line_to(Point::new(rect.x1, rect.y0));
|
|
path.line_to(Point::new(rect.x1, rect.y1));
|
|
path.line_to(Point::new(rect.x0, rect.y1));
|
|
path.close_path();
|
|
path
|
|
}
|
|
|
|
/// Convert a list of lasso points to a closed BezPath (polygon)
|
|
pub fn lasso_to_path(points: &[Point]) -> BezPath {
|
|
let mut path = BezPath::new();
|
|
if points.is_empty() {
|
|
return path;
|
|
}
|
|
path.move_to(points[0]);
|
|
for &p in &points[1..] {
|
|
path.line_to(p);
|
|
}
|
|
path.close_path();
|
|
path
|
|
}
|
|
|
|
/// Test if a point is inside a closed region using winding number
|
|
fn point_in_region(point: Point, region: &BezPath) -> bool {
|
|
region.winding(point) != 0
|
|
}
|
|
|
|
/// Extract line segments from a region path (which is always a polygon)
|
|
fn region_line_segments(region: &BezPath) -> Vec<Line> {
|
|
let mut lines = Vec::new();
|
|
let mut current = Point::ZERO;
|
|
let mut subpath_start = Point::ZERO;
|
|
|
|
for el in region.elements() {
|
|
match *el {
|
|
PathEl::MoveTo(p) => {
|
|
current = p;
|
|
subpath_start = p;
|
|
}
|
|
PathEl::LineTo(p) => {
|
|
lines.push(Line::new(current, p));
|
|
current = p;
|
|
}
|
|
PathEl::ClosePath => {
|
|
if dist(current, subpath_start) > 1e-10 {
|
|
lines.push(Line::new(current, subpath_start));
|
|
}
|
|
current = subpath_start;
|
|
}
|
|
PathEl::QuadTo(_, p) => {
|
|
lines.push(Line::new(current, p));
|
|
current = p;
|
|
}
|
|
PathEl::CurveTo(_, _, p) => {
|
|
lines.push(Line::new(current, p));
|
|
current = p;
|
|
}
|
|
}
|
|
}
|
|
lines
|
|
}
|
|
|
|
fn dist(a: Point, b: Point) -> f64 {
|
|
((a.x - b.x).powi(2) + (a.y - b.y).powi(2)).sqrt()
|
|
}
|
|
|
|
// ── Line-line intersection (exact, no cubic conversion) ──────────────────
|
|
|
|
/// Find the intersection of two line segments.
|
|
/// Returns (t1, t2) parameters on line1 and line2 respectively, or None.
|
|
fn line_line_intersection(l1: &Line, l2: &Line) -> Option<(f64, f64)> {
|
|
let d1x = l1.p1.x - l1.p0.x;
|
|
let d1y = l1.p1.y - l1.p0.y;
|
|
let d2x = l2.p1.x - l2.p0.x;
|
|
let d2y = l2.p1.y - l2.p0.y;
|
|
|
|
let denom = d1x * d2y - d1y * d2x;
|
|
if denom.abs() < 1e-12 {
|
|
return None; // Parallel
|
|
}
|
|
|
|
let dx = l2.p0.x - l1.p0.x;
|
|
let dy = l2.p0.y - l1.p0.y;
|
|
|
|
let t1 = (dx * d2y - dy * d2x) / denom;
|
|
let t2 = (dx * d1y - dy * d1x) / denom;
|
|
|
|
// Both parameters must be in [0, 1] for segments to intersect
|
|
// Use a small epsilon to avoid edge-case issues at endpoints
|
|
let eps = 1e-9;
|
|
if t1 >= -eps && t1 <= 1.0 + eps && t2 >= -eps && t2 <= 1.0 + eps {
|
|
Some((t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0)))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Find intersection of a cubic bezier with a line segment.
|
|
/// Returns list of t-parameters on the cubic where it crosses the line.
|
|
fn cubic_line_intersections(cubic: &CubicBez, line: &Line) -> Vec<f64> {
|
|
// Express the line as ax + by + c = 0
|
|
let lx = line.p1.x - line.p0.x;
|
|
let ly = line.p1.y - line.p0.y;
|
|
let line_len_sq = lx * lx + ly * ly;
|
|
if line_len_sq < 1e-20 {
|
|
return Vec::new();
|
|
}
|
|
|
|
// Normal to the line
|
|
let a = -ly;
|
|
let b = lx;
|
|
let c = -(a * line.p0.x + b * line.p0.y);
|
|
|
|
// Evaluate signed distance of each control point to the line
|
|
let d0 = a * cubic.p0.x + b * cubic.p0.y + c;
|
|
let d1 = a * cubic.p1.x + b * cubic.p1.y + c;
|
|
let d2 = a * cubic.p2.x + b * cubic.p2.y + c;
|
|
let d3 = a * cubic.p3.x + b * cubic.p3.y + c;
|
|
|
|
// Cubic polynomial coefficients: d(t) = at^3 + bt^2 + ct + d
|
|
// where d(t) is the signed distance at parameter t
|
|
let ca = -d0 + 3.0 * d1 - 3.0 * d2 + d3;
|
|
let cb = 3.0 * d0 - 6.0 * d1 + 3.0 * d2;
|
|
let cc = -3.0 * d0 + 3.0 * d1;
|
|
let cd = d0;
|
|
|
|
let roots = solve_cubic(ca, cb, cc, cd);
|
|
|
|
// Filter: t must be in [0,1] and the point must lie on the line segment
|
|
let eps = 1e-6;
|
|
let mut result = Vec::new();
|
|
for t in roots {
|
|
if t < -eps || t > 1.0 + eps {
|
|
continue;
|
|
}
|
|
let t = t.clamp(0.0, 1.0);
|
|
let p = cubic.eval(t);
|
|
|
|
// Check if point is on the line segment by projecting
|
|
let dx = p.x - line.p0.x;
|
|
let dy = p.y - line.p0.y;
|
|
let s = (dx * lx + dy * ly) / line_len_sq;
|
|
if s >= -eps && s <= 1.0 + eps {
|
|
// Avoid duplicate t values
|
|
if !result.iter().any(|&existing: &f64| (existing - t).abs() < 1e-6) {
|
|
result.push(t);
|
|
}
|
|
}
|
|
}
|
|
|
|
result.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
|
result
|
|
}
|
|
|
|
/// Solve cubic equation at^3 + bt^2 + ct + d = 0
|
|
/// Returns real roots.
|
|
fn solve_cubic(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
|
|
if a.abs() < 1e-12 {
|
|
// Degenerate to quadratic
|
|
return solve_quadratic(b, c, d);
|
|
}
|
|
|
|
// Normalize: t^3 + pt^2 + qt + r = 0
|
|
let p = b / a;
|
|
let q = c / a;
|
|
let r = d / a;
|
|
|
|
// Depressed cubic substitution: t = u - p/3
|
|
// u^3 + Au + B = 0
|
|
let a2 = q - p * p / 3.0;
|
|
let b2 = r - p * q / 3.0 + 2.0 * p * p * p / 27.0;
|
|
|
|
let discriminant = b2 * b2 / 4.0 + a2 * a2 * a2 / 27.0;
|
|
|
|
let mut roots = Vec::new();
|
|
|
|
if discriminant.abs() < 1e-14 {
|
|
// Triple or double root
|
|
if a2.abs() < 1e-12 {
|
|
roots.push(-p / 3.0);
|
|
} else {
|
|
let u = (b2 / 2.0).cbrt();
|
|
roots.push(2.0 * u - p / 3.0); // wait, this is wrong for the double root case
|
|
// Actually: u^3 + Au + B = 0 with disc=0
|
|
// roots: -2*(B/2)^(1/3) and (B/2)^(1/3) (double)
|
|
roots.clear();
|
|
let cb = if b2 > 0.0 { -(b2 / 2.0).cbrt() } else { (-b2 / 2.0).cbrt() };
|
|
roots.push(2.0 * cb - p / 3.0);
|
|
roots.push(-cb - p / 3.0);
|
|
}
|
|
} else if discriminant > 0.0 {
|
|
// One real root
|
|
let sq = discriminant.sqrt();
|
|
let u = cbrt(-b2 / 2.0 + sq);
|
|
let v = cbrt(-b2 / 2.0 - sq);
|
|
roots.push(u + v - p / 3.0);
|
|
} else {
|
|
// Three real roots (casus irreducibilis)
|
|
let r_mag = (-a2 * a2 * a2 / 27.0).sqrt();
|
|
let theta = (-b2 / (2.0 * r_mag)).acos();
|
|
let m = 2.0 * (r_mag).cbrt();
|
|
|
|
roots.push(m * (theta / 3.0).cos() - p / 3.0);
|
|
roots.push(m * ((theta + 2.0 * std::f64::consts::PI) / 3.0).cos() - p / 3.0);
|
|
roots.push(m * ((theta + 4.0 * std::f64::consts::PI) / 3.0).cos() - p / 3.0);
|
|
}
|
|
|
|
roots
|
|
}
|
|
|
|
fn cbrt(x: f64) -> f64 {
|
|
if x >= 0.0 { x.cbrt() } else { -(-x).cbrt() }
|
|
}
|
|
|
|
fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
|
|
if a.abs() < 1e-12 {
|
|
// Linear
|
|
if b.abs() < 1e-12 {
|
|
return Vec::new();
|
|
}
|
|
return vec![-c / b];
|
|
}
|
|
|
|
let disc = b * b - 4.0 * a * c;
|
|
if disc < -1e-12 {
|
|
return Vec::new();
|
|
}
|
|
if disc.abs() < 1e-12 {
|
|
return vec![-b / (2.0 * a)];
|
|
}
|
|
let sq = disc.sqrt();
|
|
vec![(-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a)]
|
|
}
|
|
|
|
// ── Segment representation ───────────────────────────────────────────────
|
|
|
|
/// A segment from the subject path, possibly split at intersection points.
|
|
/// Tracks the cubic curve and which region boundary edge it crosses at each end.
|
|
#[derive(Debug, Clone)]
|
|
struct SubSegment {
|
|
cubic: CubicBez,
|
|
inside: bool,
|
|
}
|
|
|
|
/// A crossing point where the subject path crosses the region boundary.
|
|
#[derive(Debug, Clone)]
|
|
#[allow(dead_code)]
|
|
struct Crossing {
|
|
/// Point of intersection
|
|
point: Point,
|
|
/// Index into the region boundary edges
|
|
edge_index: usize,
|
|
/// Parameter on the region boundary edge
|
|
edge_t: f64,
|
|
/// True if this crossing goes from outside to inside
|
|
entering: bool,
|
|
/// Global parameter encoding for ordering crossings on the boundary:
|
|
/// edge_index + edge_t (allows sorting crossings around the boundary)
|
|
boundary_param: f64,
|
|
}
|
|
|
|
// ── Core clipping ────────────────────────────────────────────────────────
|
|
|
|
/// Convert a line segment to a CubicBez
|
|
pub fn line_to_cubic(line: &Line) -> CubicBez {
|
|
let p0 = line.p0;
|
|
let p1 = line.p1;
|
|
let cp1 = Point::new(
|
|
p0.x + (p1.x - p0.x) / 3.0,
|
|
p0.y + (p1.y - p0.y) / 3.0,
|
|
);
|
|
let cp2 = Point::new(
|
|
p0.x + 2.0 * (p1.x - p0.x) / 3.0,
|
|
p0.y + 2.0 * (p1.y - p0.y) / 3.0,
|
|
);
|
|
CubicBez::new(p0, cp1, cp2, p1)
|
|
}
|
|
|
|
/// Extract cubic bezier curves from a BezPath (converting lines/quads to cubics)
|
|
fn extract_cubics(path: &BezPath) -> Vec<CubicBez> {
|
|
let mut cubics = Vec::new();
|
|
let mut current = Point::ZERO;
|
|
let mut subpath_start = Point::ZERO;
|
|
|
|
for el in path.elements() {
|
|
match *el {
|
|
PathEl::MoveTo(p) => {
|
|
current = p;
|
|
subpath_start = p;
|
|
}
|
|
PathEl::LineTo(p) => {
|
|
if dist(current, p) > 1e-10 {
|
|
cubics.push(line_to_cubic(&Line::new(current, p)));
|
|
}
|
|
current = p;
|
|
}
|
|
PathEl::QuadTo(cp, p) => {
|
|
let cp1 = Point::new(
|
|
current.x + 2.0 / 3.0 * (cp.x - current.x),
|
|
current.y + 2.0 / 3.0 * (cp.y - current.y),
|
|
);
|
|
let cp2 = Point::new(
|
|
p.x + 2.0 / 3.0 * (cp.x - p.x),
|
|
p.y + 2.0 / 3.0 * (cp.y - p.y),
|
|
);
|
|
cubics.push(CubicBez::new(current, cp1, cp2, p));
|
|
current = p;
|
|
}
|
|
PathEl::CurveTo(cp1, cp2, p) => {
|
|
cubics.push(CubicBez::new(current, cp1, cp2, p));
|
|
current = p;
|
|
}
|
|
PathEl::ClosePath => {
|
|
if dist(current, subpath_start) > 1e-10 {
|
|
cubics.push(line_to_cubic(&Line::new(current, subpath_start)));
|
|
}
|
|
current = subpath_start;
|
|
}
|
|
}
|
|
}
|
|
cubics
|
|
}
|
|
|
|
/// Find all intersection t-values of a cubic with the region boundary lines.
|
|
/// Returns (t_on_cubic, edge_index, t_on_edge) sorted by t_on_cubic.
|
|
fn find_all_intersections(
|
|
cubic: &CubicBez,
|
|
region_lines: &[Line],
|
|
) -> Vec<(f64, usize, f64)> {
|
|
let mut hits = Vec::new();
|
|
|
|
// Check if this cubic is actually a line (degenerate cubic from line_to_cubic)
|
|
let is_line = is_degenerate_line(cubic);
|
|
|
|
for (edge_idx, line) in region_lines.iter().enumerate() {
|
|
let t_values = if is_line {
|
|
// Use exact line-line intersection
|
|
let subject_line = Line::new(cubic.p0, cubic.p3);
|
|
if let Some((t1, t2)) = line_line_intersection(&subject_line, line) {
|
|
// Skip intersections at exact endpoints of the region edge to avoid
|
|
// double-counting at region vertices
|
|
if t2 > 1e-9 && t2 < 1.0 - 1e-9 {
|
|
vec![(t1, t2)]
|
|
} else if t1 > 1e-9 && t1 < 1.0 - 1e-9 {
|
|
// The intersection is at an endpoint of the region edge.
|
|
// Only count it for one edge (the one where t2 > 0) to avoid doubles.
|
|
vec![(t1, t2)]
|
|
} else {
|
|
vec![]
|
|
}
|
|
} else {
|
|
vec![]
|
|
}
|
|
} else {
|
|
// Cubic-line intersection
|
|
cubic_line_intersections(cubic, line)
|
|
.into_iter()
|
|
.map(|t| {
|
|
let p = cubic.eval(t);
|
|
let dx = p.x - line.p0.x;
|
|
let dy = p.y - line.p0.y;
|
|
let lx = line.p1.x - line.p0.x;
|
|
let ly = line.p1.y - line.p0.y;
|
|
let s = (dx * lx + dy * ly) / (lx * lx + ly * ly);
|
|
(t, s.clamp(0.0, 1.0))
|
|
})
|
|
.collect()
|
|
};
|
|
|
|
for (t_cubic, t_edge) in t_values {
|
|
// Avoid duplicates
|
|
if !hits.iter().any(|&(existing_t, _, _): &(f64, usize, f64)| {
|
|
(existing_t - t_cubic).abs() < 1e-6
|
|
}) {
|
|
hits.push((t_cubic, edge_idx, t_edge));
|
|
}
|
|
}
|
|
}
|
|
|
|
hits.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
|
hits
|
|
}
|
|
|
|
/// Check if a cubic is actually a degenerate line (from line_to_cubic)
|
|
fn is_degenerate_line(cubic: &CubicBez) -> bool {
|
|
// A cubic from line_to_cubic has control points at 1/3 and 2/3 along the line
|
|
let expected_p1 = Point::new(
|
|
cubic.p0.x + (cubic.p3.x - cubic.p0.x) / 3.0,
|
|
cubic.p0.y + (cubic.p3.y - cubic.p0.y) / 3.0,
|
|
);
|
|
let expected_p2 = Point::new(
|
|
cubic.p0.x + 2.0 * (cubic.p3.x - cubic.p0.x) / 3.0,
|
|
cubic.p0.y + 2.0 * (cubic.p3.y - cubic.p0.y) / 3.0,
|
|
);
|
|
dist(cubic.p1, expected_p1) < 1e-6 && dist(cubic.p2, expected_p2) < 1e-6
|
|
}
|
|
|
|
/// Split cubics at intersections with boundary lines and classify each piece.
|
|
/// Returns (sub_segments, crossings).
|
|
fn split_and_classify(
|
|
cubics: &[CubicBez],
|
|
boundary_lines: &[Line],
|
|
containment_region: &BezPath,
|
|
) -> (Vec<SubSegment>, Vec<Crossing>) {
|
|
let mut sub_segments: Vec<SubSegment> = Vec::new();
|
|
let mut crossings: Vec<Crossing> = Vec::new();
|
|
|
|
for cubic in cubics {
|
|
let hits = find_all_intersections(cubic, boundary_lines);
|
|
|
|
if hits.is_empty() {
|
|
let mid = cubic.eval(0.5);
|
|
let inside = point_in_region(mid, containment_region);
|
|
sub_segments.push(SubSegment { cubic: *cubic, inside });
|
|
} else {
|
|
let mut prev_t = 0.0;
|
|
for &(t, edge_idx, edge_t) in &hits {
|
|
if t - prev_t > 1e-8 {
|
|
let sub = cubic.subsegment(prev_t..t);
|
|
let mid = sub.eval(0.5);
|
|
let inside = point_in_region(mid, containment_region);
|
|
sub_segments.push(SubSegment { cubic: sub, inside });
|
|
}
|
|
|
|
let point = cubic.eval(t);
|
|
let before = cubic.eval((t - 0.005).max(0.0));
|
|
let after = cubic.eval((t + 0.005).min(1.0));
|
|
let entering = !point_in_region(before, containment_region)
|
|
&& point_in_region(after, containment_region);
|
|
|
|
crossings.push(Crossing {
|
|
point,
|
|
edge_index: edge_idx,
|
|
edge_t,
|
|
entering,
|
|
boundary_param: edge_idx as f64 + edge_t,
|
|
});
|
|
|
|
prev_t = t;
|
|
}
|
|
if 1.0 - prev_t > 1e-8 {
|
|
let sub = cubic.subsegment(prev_t..1.0);
|
|
let mid = sub.eval(0.5);
|
|
let inside = point_in_region(mid, containment_region);
|
|
sub_segments.push(SubSegment { cubic: sub, inside });
|
|
}
|
|
}
|
|
}
|
|
|
|
(sub_segments, crossings)
|
|
}
|
|
|
|
/// One-sided clip: build the "inside" path of `subject_cubics` clipped against `boundary`.
|
|
fn clip_one_side(
|
|
subject_cubics: &[CubicBez],
|
|
boundary: &BezPath,
|
|
want_inside: bool,
|
|
) -> BezPath {
|
|
let boundary_lines = region_line_segments(boundary);
|
|
if boundary_lines.is_empty() {
|
|
return BezPath::new();
|
|
}
|
|
let (sub_segments, crossings) = split_and_classify(subject_cubics, &boundary_lines, boundary);
|
|
build_clipped_path(&sub_segments, &crossings, &boundary_lines, want_inside, None)
|
|
}
|
|
|
|
/// Clip a BezPath against a closed polygon region.
|
|
///
|
|
/// Uses a Weiler-Atherton-inspired approach:
|
|
/// 1. Split all subject path segments at region boundary crossings
|
|
/// 2. Classify each sub-segment as inside or outside
|
|
/// 3. For the "inside" path: chain inside sub-segments together, connecting
|
|
/// consecutive runs by walking the region boundary from exit to entry point
|
|
/// 4. Same for "outside" but walking the other way
|
|
///
|
|
/// When the region extends beyond the subject (e.g., a lasso that overshoots),
|
|
/// the boundary walk for the inside path may include region boundary segments
|
|
/// outside the subject. A second-pass clip against the subject trims these,
|
|
/// producing the correct intersection.
|
|
pub fn clip_path_to_region(path: &BezPath, region: &BezPath) -> ClipResult {
|
|
let region_lines = region_line_segments(region);
|
|
if region_lines.is_empty() {
|
|
return ClipResult {
|
|
inside: BezPath::new(),
|
|
outside: path.clone(),
|
|
};
|
|
}
|
|
|
|
let cubics = extract_cubics(path);
|
|
if cubics.is_empty() {
|
|
return ClipResult {
|
|
inside: BezPath::new(),
|
|
outside: BezPath::new(),
|
|
};
|
|
}
|
|
|
|
// Step 1: Split and classify subject against region
|
|
let (sub_segments, crossings) = split_and_classify(&cubics, ®ion_lines, region);
|
|
|
|
// Step 2: Build raw inside and outside paths
|
|
let inside_raw = build_clipped_path(&sub_segments, &crossings, ®ion_lines, true, None);
|
|
let outside_raw = build_clipped_path(&sub_segments, &crossings, ®ion_lines, false, Some(path));
|
|
|
|
// Step 3: Check if any region vertex lies outside the subject.
|
|
// If so, boundary walks for the inside path may have followed region edges
|
|
// outside the subject. Reclip the inside against the subject.
|
|
// The outside doesn't need reclipping — it uses subject-aware grouping instead.
|
|
let region_extends_beyond = region_lines.iter().any(|line| {
|
|
!point_in_region(line.p0, path)
|
|
});
|
|
let inside = reclip_against_subject(&inside_raw, path, region_extends_beyond);
|
|
let outside = outside_raw;
|
|
|
|
ClipResult { inside, outside }
|
|
}
|
|
|
|
/// Clip `raw_path` against `subject` to ensure it stays within the subject.
|
|
/// This trims boundary walks that followed region edges outside the subject.
|
|
/// `region_extends_beyond` indicates whether any region vertex lies outside
|
|
/// the subject, meaning boundary walks could have escaped.
|
|
fn reclip_against_subject(raw_path: &BezPath, subject: &BezPath, region_extends_beyond: bool) -> BezPath {
|
|
if raw_path.elements().is_empty() || !region_extends_beyond {
|
|
return raw_path.clone();
|
|
}
|
|
let cubics = extract_cubics(raw_path);
|
|
if cubics.is_empty() {
|
|
return raw_path.clone();
|
|
}
|
|
let reclipped = clip_one_side(&cubics, subject, true);
|
|
if reclipped.elements().is_empty() {
|
|
raw_path.clone()
|
|
} else {
|
|
reclipped
|
|
}
|
|
}
|
|
|
|
/// Build a clipped path for one side (inside=true or outside=false).
|
|
///
|
|
/// Strategy:
|
|
/// - Walk through sub_segments, collecting those matching `want_inside`
|
|
/// - When we encounter a gap (transition from wanted to unwanted), we've hit
|
|
/// a boundary crossing. Walk the region boundary to connect to the next
|
|
/// run of wanted sub-segments.
|
|
/// - When multiple disconnected pieces exist (e.g., a lasso splits the
|
|
/// remainder into two), emit them as separate sub-paths.
|
|
///
|
|
/// `subject`: if provided, used to validate boundary walks. Walks whose midpoint
|
|
/// falls outside the subject indicate disconnected groups that need separate sub-paths.
|
|
fn build_clipped_path(
|
|
sub_segments: &[SubSegment],
|
|
_crossings: &[Crossing],
|
|
region_lines: &[Line],
|
|
want_inside: bool,
|
|
subject: Option<&BezPath>,
|
|
) -> BezPath {
|
|
let mut path = BezPath::new();
|
|
|
|
if sub_segments.is_empty() {
|
|
return path;
|
|
}
|
|
|
|
// Collect runs of consecutive sub-segments that are `want_inside`
|
|
let mut runs: Vec<(usize, usize)> = Vec::new(); // (start_idx, end_idx exclusive)
|
|
let mut i = 0;
|
|
while i < sub_segments.len() {
|
|
if sub_segments[i].inside == want_inside {
|
|
let start = i;
|
|
while i < sub_segments.len() && sub_segments[i].inside == want_inside {
|
|
i += 1;
|
|
}
|
|
runs.push((start, i));
|
|
} else {
|
|
i += 1;
|
|
}
|
|
}
|
|
|
|
if runs.is_empty() {
|
|
return path;
|
|
}
|
|
|
|
// If there's only one run and it covers the entire path, just output it closed
|
|
if runs.len() == 1 && runs[0].0 == 0 && runs[0].1 == sub_segments.len() {
|
|
let (start, end) = runs[0];
|
|
path.move_to(sub_segments[start].cubic.p0);
|
|
for seg in &sub_segments[start..end] {
|
|
emit_cubic(&mut path, &seg.cubic);
|
|
}
|
|
path.close_path();
|
|
return path;
|
|
}
|
|
|
|
// Group runs into separate sub-paths. Two consecutive runs belong to the
|
|
// same sub-path if they can be connected by a boundary walk that doesn't
|
|
// need to traverse the "other side". We detect this by checking if the
|
|
// boundary walk midpoint is on the correct side of the region.
|
|
//
|
|
// Each group will form its own closed sub-path.
|
|
let groups = group_runs_into_subpaths(&runs, sub_segments, region_lines, want_inside, subject);
|
|
|
|
for group in &groups {
|
|
let first_run = group[0];
|
|
path.move_to(sub_segments[first_run.0].cubic.p0);
|
|
|
|
for (gi, &(start, end)) in group.iter().enumerate() {
|
|
// Emit the subject-path segments for this run
|
|
for seg in &sub_segments[start..end] {
|
|
emit_cubic(&mut path, &seg.cubic);
|
|
}
|
|
|
|
// Connect to the next run in this group via boundary walk
|
|
let next_gi = (gi + 1) % group.len();
|
|
let next_run = group[next_gi];
|
|
|
|
let exit_point = sub_segments[end - 1].cubic.p3;
|
|
let entry_point = sub_segments[next_run.0].cubic.p0;
|
|
|
|
if dist(exit_point, entry_point) > 0.5 {
|
|
let boundary_pts = walk_boundary(
|
|
exit_point,
|
|
entry_point,
|
|
region_lines,
|
|
want_inside,
|
|
);
|
|
for &bp in &boundary_pts {
|
|
path.line_to(bp);
|
|
}
|
|
path.line_to(entry_point);
|
|
}
|
|
}
|
|
|
|
path.close_path();
|
|
}
|
|
|
|
path
|
|
}
|
|
|
|
/// Group runs into separate sub-paths based on whether boundary walks
|
|
/// between them stay within the subject.
|
|
///
|
|
/// When `subject` is provided, boundary walks whose midpoint falls outside
|
|
/// the subject indicate disconnected groups. When not provided, all runs
|
|
/// are grouped into a single sub-path.
|
|
fn group_runs_into_subpaths(
|
|
runs: &[(usize, usize)],
|
|
sub_segments: &[SubSegment],
|
|
region_lines: &[Line],
|
|
want_inside: bool,
|
|
subject: Option<&BezPath>,
|
|
) -> Vec<Vec<(usize, usize)>> {
|
|
if runs.len() <= 1 {
|
|
return vec![runs.to_vec()];
|
|
}
|
|
|
|
let subject = match subject {
|
|
Some(s) => s,
|
|
None => return vec![runs.to_vec()],
|
|
};
|
|
|
|
// For each pair of consecutive runs, check if the boundary walk
|
|
// between them stays inside the subject.
|
|
let mut break_after: Vec<bool> = vec![false; runs.len()];
|
|
|
|
for run_idx in 0..runs.len() {
|
|
let next_idx = (run_idx + 1) % runs.len();
|
|
let (_, end) = runs[run_idx];
|
|
let (next_start, _) = runs[next_idx];
|
|
|
|
let exit_point = sub_segments[end - 1].cubic.p3;
|
|
let entry_point = sub_segments[next_start].cubic.p0;
|
|
|
|
if dist(exit_point, entry_point) <= 0.5 {
|
|
continue;
|
|
}
|
|
|
|
// Get the boundary walk
|
|
let boundary_pts = walk_boundary(
|
|
exit_point,
|
|
entry_point,
|
|
region_lines,
|
|
want_inside,
|
|
);
|
|
|
|
// Check if any walk point or walk midpoint lies outside the subject.
|
|
// If so, this walk escapes the subject and the runs should be in
|
|
// separate sub-paths.
|
|
let mut all_points = vec![exit_point];
|
|
all_points.extend_from_slice(&boundary_pts);
|
|
all_points.push(entry_point);
|
|
|
|
for window in all_points.windows(2) {
|
|
let mid = Point::new(
|
|
(window[0].x + window[1].x) / 2.0,
|
|
(window[0].y + window[1].y) / 2.0,
|
|
);
|
|
if !point_in_region(mid, subject) {
|
|
break_after[run_idx] = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build groups based on break points.
|
|
// Walk runs in order, breaking at break points.
|
|
// Handle the circular nature: if last→first is NOT a break, merge them.
|
|
let mut groups: Vec<Vec<(usize, usize)>> = Vec::new();
|
|
let mut current_group: Vec<(usize, usize)> = vec![runs[0]];
|
|
|
|
for i in 0..runs.len() - 1 {
|
|
if break_after[i] {
|
|
groups.push(current_group);
|
|
current_group = Vec::new();
|
|
}
|
|
current_group.push(runs[i + 1]);
|
|
}
|
|
|
|
// Handle wrap-around
|
|
if !groups.is_empty() && !break_after[runs.len() - 1] {
|
|
// Last run connects back to first group — merge
|
|
let first_group = groups.remove(0);
|
|
current_group.extend(first_group);
|
|
}
|
|
groups.push(current_group);
|
|
|
|
groups
|
|
}
|
|
|
|
/// Emit a cubic to a BezPath, using line_to for degenerate (linear) cubics
|
|
fn emit_cubic(path: &mut BezPath, cubic: &CubicBez) {
|
|
if is_degenerate_line(cubic) {
|
|
path.line_to(cubic.p3);
|
|
} else {
|
|
path.curve_to(cubic.p1, cubic.p2, cubic.p3);
|
|
}
|
|
}
|
|
|
|
/// Walk along the region boundary from `from` to `to`.
|
|
///
|
|
/// For the "inside" clip, we walk the shorter path along the boundary
|
|
/// (staying close to the region interior). For the "outside" clip, we walk
|
|
/// the longer path (going around the outside of the region).
|
|
fn walk_boundary(
|
|
from: Point,
|
|
to: Point,
|
|
region_lines: &[Line],
|
|
want_inside: bool,
|
|
) -> Vec<Point> {
|
|
let n = region_lines.len();
|
|
if n == 0 {
|
|
return vec![to];
|
|
}
|
|
|
|
// Find boundary position for `from` and `to`
|
|
let from_pos = project_onto_boundary(from, region_lines);
|
|
let to_pos = project_onto_boundary(to, region_lines);
|
|
|
|
// Walk clockwise (increasing edge index)
|
|
let cw = walk_boundary_direction(from_pos, to_pos, region_lines, true);
|
|
// Walk counter-clockwise (decreasing edge index)
|
|
let ccw = walk_boundary_direction(from_pos, to_pos, region_lines, false);
|
|
|
|
let cw_len = chain_length(from, &cw, to);
|
|
let ccw_len = chain_length(from, &ccw, to);
|
|
|
|
// Always take the shorter walk — for inside clips this connects
|
|
// inside runs, for outside clips this connects outside runs,
|
|
// and in both cases we want the shortest boundary path.
|
|
let _ = want_inside;
|
|
if cw_len <= ccw_len { cw } else { ccw }
|
|
}
|
|
|
|
/// A position on the boundary: (edge_index, t along that edge)
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct BoundaryPos {
|
|
edge: usize,
|
|
t: f64,
|
|
}
|
|
|
|
fn project_onto_boundary(point: Point, lines: &[Line]) -> BoundaryPos {
|
|
let mut best_edge = 0;
|
|
let mut best_t = 0.0;
|
|
let mut best_dist = f64::MAX;
|
|
|
|
for (i, line) in lines.iter().enumerate() {
|
|
let lx = line.p1.x - line.p0.x;
|
|
let ly = line.p1.y - line.p0.y;
|
|
let len_sq = lx * lx + ly * ly;
|
|
if len_sq < 1e-20 {
|
|
continue;
|
|
}
|
|
let t = ((point.x - line.p0.x) * lx + (point.y - line.p0.y) * ly) / len_sq;
|
|
let t = t.clamp(0.0, 1.0);
|
|
let proj = Point::new(line.p0.x + t * lx, line.p0.y + t * ly);
|
|
let d = dist(point, proj);
|
|
if d < best_dist {
|
|
best_dist = d;
|
|
best_edge = i;
|
|
best_t = t;
|
|
}
|
|
}
|
|
|
|
BoundaryPos { edge: best_edge, t: best_t }
|
|
}
|
|
|
|
/// Walk the boundary from `from_pos` to `to_pos` in a given direction.
|
|
/// `clockwise` = true means walk forward (increasing edge index).
|
|
/// Returns intermediate points (not including `from`, not including `to`).
|
|
fn walk_boundary_direction(
|
|
from_pos: BoundaryPos,
|
|
to_pos: BoundaryPos,
|
|
lines: &[Line],
|
|
clockwise: bool,
|
|
) -> Vec<Point> {
|
|
let n = lines.len();
|
|
let mut result = Vec::new();
|
|
|
|
if from_pos.edge == to_pos.edge {
|
|
// Same edge — check if we can go directly
|
|
if clockwise && to_pos.t > from_pos.t + 1e-9 {
|
|
return result; // Direct, no intermediate vertices needed
|
|
}
|
|
if !clockwise && to_pos.t < from_pos.t - 1e-9 {
|
|
return result; // Direct
|
|
}
|
|
// Otherwise we need to go all the way around
|
|
}
|
|
|
|
if clockwise {
|
|
// Walk forward: from from_pos.edge to to_pos.edge
|
|
let mut edge = from_pos.edge;
|
|
// First: emit the end vertex of the current edge (if we're not already at it)
|
|
if from_pos.t < 1.0 - 1e-9 {
|
|
result.push(lines[edge].p1);
|
|
}
|
|
edge = (edge + 1) % n;
|
|
|
|
let mut safety = 0;
|
|
while edge != to_pos.edge && safety < n + 1 {
|
|
result.push(lines[edge].p1);
|
|
edge = (edge + 1) % n;
|
|
safety += 1;
|
|
}
|
|
// We're now on the target edge; the caller will add `to` point
|
|
} else {
|
|
// Walk backward: from from_pos.edge to to_pos.edge
|
|
let mut edge = from_pos.edge;
|
|
// First: emit the start vertex of the current edge (if we're not already at it)
|
|
if from_pos.t > 1e-9 {
|
|
result.push(lines[edge].p0);
|
|
}
|
|
edge = if edge == 0 { n - 1 } else { edge - 1 };
|
|
|
|
let mut safety = 0;
|
|
while edge != to_pos.edge && safety < n + 1 {
|
|
result.push(lines[edge].p0);
|
|
edge = if edge == 0 { n - 1 } else { edge - 1 };
|
|
safety += 1;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|
|
|
|
fn chain_length(start: Point, intermediates: &[Point], end: Point) -> f64 {
|
|
let mut len = 0.0;
|
|
let mut prev = start;
|
|
for &p in intermediates {
|
|
len += dist(prev, p);
|
|
prev = p;
|
|
}
|
|
len += dist(prev, end);
|
|
len
|
|
}
|
|
|
|
/// Check if a shape path has any segments that cross the region boundary
|
|
pub fn path_intersects_region(path: &BezPath, region: &BezPath) -> bool {
|
|
let region_lines = region_line_segments(region);
|
|
let cubics = extract_cubics(path);
|
|
|
|
for cubic in &cubics {
|
|
let hits = find_all_intersections(&cubic, ®ion_lines);
|
|
if !hits.is_empty() {
|
|
return true;
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Check if all points of a path are inside the region
|
|
pub fn path_fully_inside_region(path: &BezPath, region: &BezPath) -> bool {
|
|
for el in path.elements() {
|
|
let p = match *el {
|
|
PathEl::MoveTo(p) | PathEl::LineTo(p) => p,
|
|
PathEl::QuadTo(_, p) | PathEl::CurveTo(_, _, p) => p,
|
|
PathEl::ClosePath => continue,
|
|
};
|
|
if !point_in_region(p, region) {
|
|
return false;
|
|
}
|
|
}
|
|
true
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_rect_to_path() {
|
|
let rect = Rect::new(10.0, 20.0, 100.0, 200.0);
|
|
let path = rect_to_path(rect);
|
|
// MoveTo + 3 LineTo + ClosePath = 5 elements
|
|
assert!(path.elements().len() >= 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_lasso_to_path() {
|
|
let points = vec![
|
|
Point::new(0.0, 0.0),
|
|
Point::new(100.0, 0.0),
|
|
Point::new(100.0, 100.0),
|
|
Point::new(0.0, 100.0),
|
|
];
|
|
let path = lasso_to_path(&points);
|
|
assert!(path.elements().len() >= 5);
|
|
}
|
|
|
|
#[test]
|
|
fn test_point_in_region() {
|
|
let region = rect_to_path(Rect::new(0.0, 0.0, 100.0, 100.0));
|
|
assert!(point_in_region(Point::new(50.0, 50.0), ®ion));
|
|
assert!(!point_in_region(Point::new(150.0, 50.0), ®ion));
|
|
}
|
|
|
|
#[test]
|
|
fn test_line_line_intersection() {
|
|
let l1 = Line::new(Point::new(0.0, 5.0), Point::new(10.0, 5.0));
|
|
let l2 = Line::new(Point::new(5.0, 0.0), Point::new(5.0, 10.0));
|
|
let result = line_line_intersection(&l1, &l2);
|
|
assert!(result.is_some());
|
|
let (t1, t2) = result.unwrap();
|
|
assert!((t1 - 0.5).abs() < 1e-6);
|
|
assert!((t2 - 0.5).abs() < 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clip_rect_corner() {
|
|
// Rectangle from (0,0) to (100,100)
|
|
let mut subject = BezPath::new();
|
|
subject.move_to(Point::new(0.0, 0.0));
|
|
subject.line_to(Point::new(100.0, 0.0));
|
|
subject.line_to(Point::new(100.0, 100.0));
|
|
subject.line_to(Point::new(0.0, 100.0));
|
|
subject.close_path();
|
|
|
|
// Clip to upper-right corner: (50,0) to (100,50)
|
|
let region = rect_to_path(Rect::new(50.0, 0.0, 150.0, 50.0));
|
|
let result = clip_path_to_region(&subject, ®ion);
|
|
|
|
// Inside should have elements (the upper-right portion)
|
|
assert!(!result.inside.elements().is_empty(),
|
|
"inside path should not be empty");
|
|
// Outside should have elements (the rest of the rectangle)
|
|
assert!(!result.outside.elements().is_empty(),
|
|
"outside path should not be empty");
|
|
|
|
// The inside portion should be a roughly rectangular region
|
|
// Its bounding box should be approximately (50,0)-(100,50)
|
|
let inside_bb = result.inside.bounding_box();
|
|
assert!((inside_bb.x0 - 50.0).abs() < 2.0,
|
|
"inside x0 should be ~50, got {}", inside_bb.x0);
|
|
assert!((inside_bb.y0 - 0.0).abs() < 2.0,
|
|
"inside y0 should be ~0, got {}", inside_bb.y0);
|
|
assert!((inside_bb.x1 - 100.0).abs() < 2.0,
|
|
"inside x1 should be ~100, got {}", inside_bb.x1);
|
|
assert!((inside_bb.y1 - 50.0).abs() < 2.0,
|
|
"inside y1 should be ~50, got {}", inside_bb.y1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_clip_fully_inside() {
|
|
let mut path = BezPath::new();
|
|
path.move_to(Point::new(20.0, 20.0));
|
|
path.line_to(Point::new(80.0, 20.0));
|
|
path.line_to(Point::new(80.0, 80.0));
|
|
path.line_to(Point::new(20.0, 80.0));
|
|
path.close_path();
|
|
|
|
let region = rect_to_path(Rect::new(0.0, 0.0, 100.0, 100.0));
|
|
let result = clip_path_to_region(&path, ®ion);
|
|
|
|
assert!(!result.inside.elements().is_empty());
|
|
assert!(result.outside.elements().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_clip_fully_outside() {
|
|
let mut path = BezPath::new();
|
|
path.move_to(Point::new(200.0, 200.0));
|
|
path.line_to(Point::new(300.0, 200.0));
|
|
path.line_to(Point::new(300.0, 300.0));
|
|
path.close_path();
|
|
|
|
let region = rect_to_path(Rect::new(0.0, 0.0, 100.0, 100.0));
|
|
let result = clip_path_to_region(&path, ®ion);
|
|
|
|
assert!(result.inside.elements().is_empty());
|
|
assert!(!result.outside.elements().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_intersects_region() {
|
|
let mut path = BezPath::new();
|
|
path.move_to(Point::new(-50.0, 50.0));
|
|
path.line_to(Point::new(150.0, 50.0));
|
|
|
|
let region = rect_to_path(Rect::new(0.0, 0.0, 100.0, 100.0));
|
|
assert!(path_intersects_region(&path, ®ion));
|
|
}
|
|
|
|
#[test]
|
|
fn test_path_fully_inside() {
|
|
let mut path = BezPath::new();
|
|
path.move_to(Point::new(20.0, 20.0));
|
|
path.line_to(Point::new(80.0, 20.0));
|
|
path.line_to(Point::new(80.0, 80.0));
|
|
path.close_path();
|
|
|
|
let region = rect_to_path(Rect::new(0.0, 0.0, 100.0, 100.0));
|
|
assert!(path_fully_inside_region(&path, ®ion));
|
|
assert!(!path_intersects_region(&path, ®ion));
|
|
}
|
|
|
|
#[test]
|
|
fn test_cubic_line_intersection() {
|
|
// Horizontal line as cubic
|
|
let cubic = CubicBez::new(
|
|
Point::new(0.0, 50.0),
|
|
Point::new(33.33, 50.0),
|
|
Point::new(66.67, 50.0),
|
|
Point::new(100.0, 50.0),
|
|
);
|
|
// Vertical line segment
|
|
let line = Line::new(Point::new(50.0, 0.0), Point::new(50.0, 100.0));
|
|
let hits = cubic_line_intersections(&cubic, &line);
|
|
assert_eq!(hits.len(), 1, "Expected 1 intersection, got {}", hits.len());
|
|
assert!((hits[0] - 0.5).abs() < 0.01, "t should be ~0.5, got {}", hits[0]);
|
|
}
|
|
}
|