Lightningbeam/lightningbeam-ui/lightningbeam-core/src/curve_intersections.rs

358 lines
11 KiB
Rust

//! Curve intersection and proximity detection for paint bucket tool
//!
//! This module provides functions for finding:
//! - Exact intersections between cubic Bezier curves
//! - Self-intersections within a single curve
//! - Closest approach between curves (for gap tolerance)
use vello::kurbo::{CubicBez, ParamCurve, ParamCurveNearest, Point, Shape};
/// Result of a curve intersection
#[derive(Debug, Clone)]
pub struct Intersection {
/// Parameter t on first curve [0, 1]
pub t1: f64,
/// Parameter t on second curve [0, 1] (for curve-curve intersections)
pub t2: Option<f64>,
/// Point of intersection
pub point: Point,
}
/// Result of a close approach between two curves
#[derive(Debug, Clone)]
pub struct CloseApproach {
/// Parameter on first curve
pub t1: f64,
/// Parameter on second curve
pub t2: f64,
/// Point on first curve
pub p1: Point,
/// Point on second curve
pub p2: Point,
/// Distance between the curves
pub distance: f64,
}
/// Find intersections between two cubic Bezier curves
///
/// Uses recursive subdivision to find intersection points.
/// This is much more robust and faster than sampling.
pub fn find_curve_intersections(curve1: &CubicBez, curve2: &CubicBez) -> Vec<Intersection> {
let mut intersections = Vec::new();
// Use subdivision-based intersection detection
find_intersections_recursive(
curve1, curve1, 0.0, 1.0,
curve2, curve2, 0.0, 1.0,
&mut intersections,
0, // recursion depth
);
// Remove duplicate intersections
dedup_intersections(&mut intersections, 1.0);
intersections
}
/// Recursively find intersections using subdivision
///
/// orig_curve1/2 are the original curves (for computing final intersection points)
/// curve1/2 are the current subsegments being tested
/// t1_start/end track the parameter range on the original curve
fn find_intersections_recursive(
orig_curve1: &CubicBez,
curve1: &CubicBez,
t1_start: f64,
t1_end: f64,
orig_curve2: &CubicBez,
curve2: &CubicBez,
t2_start: f64,
t2_end: f64,
intersections: &mut Vec<Intersection>,
depth: usize,
) {
// Maximum recursion depth
const MAX_DEPTH: usize = 20;
// Minimum parameter range (if smaller, we've found an intersection)
const MIN_RANGE: f64 = 0.001;
// Get bounding boxes of current subsegments
let bbox1 = curve1.bounding_box();
let bbox2 = curve2.bounding_box();
// Inflate bounding boxes slightly to account for numerical precision
let bbox1 = bbox1.inflate(0.1, 0.1);
let bbox2 = bbox2.inflate(0.1, 0.1);
// If bounding boxes don't overlap, no intersection
if !bboxes_overlap(&bbox1, &bbox2) {
return;
}
// If we've recursed deep enough or ranges are small enough, record intersection
if depth >= MAX_DEPTH ||
((t1_end - t1_start) < MIN_RANGE && (t2_end - t2_start) < MIN_RANGE) {
let t1 = (t1_start + t1_end) / 2.0;
let t2 = (t2_start + t2_end) / 2.0;
intersections.push(Intersection {
t1,
t2: Some(t2),
point: orig_curve1.eval(t1),
});
return;
}
// Subdivide both curves at midpoint (of the current subsegment, which is 0..1)
let t1_mid = (t1_start + t1_end) / 2.0;
let t2_mid = (t2_start + t2_end) / 2.0;
// Create subsegments - these are new curves parameterized 0..1
let curve1_left = curve1.subsegment(0.0..0.5);
let curve1_right = curve1.subsegment(0.5..1.0);
let curve2_left = curve2.subsegment(0.0..0.5);
let curve2_right = curve2.subsegment(0.5..1.0);
// Check all four combinations
find_intersections_recursive(
orig_curve1, &curve1_left, t1_start, t1_mid,
orig_curve2, &curve2_left, t2_start, t2_mid,
intersections, depth + 1
);
find_intersections_recursive(
orig_curve1, &curve1_left, t1_start, t1_mid,
orig_curve2, &curve2_right, t2_mid, t2_end,
intersections, depth + 1
);
find_intersections_recursive(
orig_curve1, &curve1_right, t1_mid, t1_end,
orig_curve2, &curve2_left, t2_start, t2_mid,
intersections, depth + 1
);
find_intersections_recursive(
orig_curve1, &curve1_right, t1_mid, t1_end,
orig_curve2, &curve2_right, t2_mid, t2_end,
intersections, depth + 1
);
}
/// Check if two bounding boxes overlap
fn bboxes_overlap(bbox1: &vello::kurbo::Rect, bbox2: &vello::kurbo::Rect) -> bool {
bbox1.x0 <= bbox2.x1 &&
bbox1.x1 >= bbox2.x0 &&
bbox1.y0 <= bbox2.y1 &&
bbox1.y1 >= bbox2.y0
}
/// Find self-intersections within a single cubic Bezier curve
///
/// A curve self-intersects when it crosses itself, forming a loop.
pub fn find_self_intersections(curve: &CubicBez) -> Vec<Intersection> {
let mut intersections = Vec::new();
// Sample the curve at regular intervals
let samples = 50;
for i in 0..samples {
let t1 = i as f64 / samples as f64;
let p1 = curve.eval(t1);
// Check against all later points
for j in (i + 5)..samples { // Skip nearby points to avoid false positives
let t2 = j as f64 / samples as f64;
let p2 = curve.eval(t2);
let dist = (p1 - p2).hypot();
// If points are very close, we may have a self-intersection
if dist < 0.5 {
// Refine to get more accurate parameters
let (refined_t1, refined_t2) = refine_self_intersection(curve, t1, t2);
intersections.push(Intersection {
t1: refined_t1,
t2: Some(refined_t2),
point: curve.eval(refined_t1),
});
}
}
}
// Remove duplicates
dedup_intersections(&mut intersections, 0.5);
intersections
}
/// Find the closest approach between two curves if within tolerance
///
/// Returns Some if the minimum distance between curves is less than tolerance.
pub fn find_closest_approach(
curve1: &CubicBez,
curve2: &CubicBez,
tolerance: f64,
) -> Option<CloseApproach> {
let mut min_dist = f64::MAX;
let mut best_t1 = 0.0;
let mut best_t2 = 0.0;
// Sample curve1 at regular intervals
let samples = 50;
for i in 0..=samples {
let t1 = i as f64 / samples as f64;
let p1 = curve1.eval(t1);
// Find nearest point on curve2
let nearest = curve2.nearest(p1, 1e-6);
let dist = (p1 - curve2.eval(nearest.t)).hypot();
if dist < min_dist {
min_dist = dist;
best_t1 = t1;
best_t2 = nearest.t;
}
}
// If minimum distance is within tolerance, return it
if min_dist < tolerance {
Some(CloseApproach {
t1: best_t1,
t2: best_t2,
p1: curve1.eval(best_t1),
p2: curve2.eval(best_t2),
distance: min_dist,
})
} else {
None
}
}
/// Refine intersection parameters using Newton's method
fn refine_intersection(
curve1: &CubicBez,
curve2: &CubicBez,
mut t1: f64,
mut t2: f64,
) -> (f64, f64) {
// Simple refinement: just find nearest points iteratively
for _ in 0..5 {
let p1 = curve1.eval(t1);
let nearest2 = curve2.nearest(p1, 1e-6);
t2 = nearest2.t;
let p2 = curve2.eval(t2);
let nearest1 = curve1.nearest(p2, 1e-6);
t1 = nearest1.t;
}
(t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0))
}
/// Refine self-intersection parameters
fn refine_self_intersection(curve: &CubicBez, mut t1: f64, mut t2: f64) -> (f64, f64) {
// Refine by moving parameters closer to where curves actually meet
for _ in 0..5 {
let p1 = curve.eval(t1);
let p2 = curve.eval(t2);
let mid = Point::new((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);
// Move both parameters toward the midpoint
let nearest1 = curve.nearest(mid, 1e-6);
let nearest2 = curve.nearest(mid, 1e-6);
// Take whichever is closer to original parameter
if (nearest1.t - t1).abs() < (nearest2.t - t1).abs() {
t1 = nearest1.t;
} else if (nearest2.t - t2).abs() < (nearest1.t - t2).abs() {
t2 = nearest2.t;
}
}
(t1.clamp(0.0, 1.0), t2.clamp(0.0, 1.0))
}
/// Remove duplicate intersections within a tolerance
fn dedup_intersections(intersections: &mut Vec<Intersection>, tolerance: f64) {
let mut i = 0;
while i < intersections.len() {
let mut j = i + 1;
while j < intersections.len() {
let dist = (intersections[i].point - intersections[j].point).hypot();
if dist < tolerance {
intersections.remove(j);
} else {
j += 1;
}
}
i += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_curve_intersection_simple() {
// Two curves that cross
let curve1 = CubicBez::new(
Point::new(0.0, 0.0),
Point::new(100.0, 100.0),
Point::new(100.0, 100.0),
Point::new(200.0, 200.0),
);
let curve2 = CubicBez::new(
Point::new(200.0, 0.0),
Point::new(100.0, 100.0),
Point::new(100.0, 100.0),
Point::new(0.0, 200.0),
);
let intersections = find_curve_intersections(&curve1, &curve2);
// Should find at least one intersection near the center
assert!(!intersections.is_empty());
}
#[test]
fn test_self_intersection() {
// A curve that loops back on itself
let curve = CubicBez::new(
Point::new(0.0, 0.0),
Point::new(100.0, 100.0),
Point::new(-100.0, 100.0),
Point::new(0.0, 0.0),
);
let intersections = find_self_intersections(&curve);
// May or may not find intersection depending on curve shape
// This is mostly testing that the function doesn't crash
assert!(intersections.len() <= 10); // Sanity check
}
#[test]
fn test_closest_approach() {
// Two curves that are close but don't intersect
let curve1 = CubicBez::new(
Point::new(0.0, 0.0),
Point::new(50.0, 0.0),
Point::new(100.0, 0.0),
Point::new(150.0, 0.0),
);
let curve2 = CubicBez::new(
Point::new(0.0, 1.5),
Point::new(50.0, 1.5),
Point::new(100.0, 1.5),
Point::new(150.0, 1.5),
);
let approach = find_closest_approach(&curve1, &curve2, 2.0);
assert!(approach.is_some());
let approach = approach.unwrap();
assert!(approach.distance < 2.0);
}
}