//! 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 { 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 { // 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 { 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 { 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 { 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, Vec) { let mut sub_segments: Vec = Vec::new(); let mut crossings: Vec = 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> { 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 = 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::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 { 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 { 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]); } }