353 lines
11 KiB
Rust
353 lines
11 KiB
Rust
//! Flood fill algorithm for paint bucket tool
|
|
//!
|
|
//! This module implements a flood fill that tracks which curves each point
|
|
//! touches. Instead of filling with pixels, it returns boundary points that
|
|
//! can be used to construct a filled shape from exact curve geometry.
|
|
|
|
use crate::curve_segment::CurveSegment;
|
|
use crate::quadtree::{BoundingBox, Quadtree};
|
|
use std::collections::{HashSet, VecDeque};
|
|
use vello::kurbo::Point;
|
|
|
|
/// A point on the boundary of the filled region
|
|
#[derive(Debug, Clone)]
|
|
pub struct BoundaryPoint {
|
|
/// The sampled point location
|
|
pub point: Point,
|
|
/// Index of the nearest curve segment
|
|
pub curve_index: usize,
|
|
/// Parameter t on the nearest curve (0.0 to 1.0)
|
|
pub t: f64,
|
|
/// Nearest point on the curve
|
|
pub nearest_point: Point,
|
|
/// Distance to the nearest curve
|
|
pub distance: f64,
|
|
}
|
|
|
|
/// Result of a flood fill operation
|
|
#[derive(Debug)]
|
|
pub struct FloodFillResult {
|
|
/// All boundary points found during flood fill
|
|
pub boundary_points: Vec<BoundaryPoint>,
|
|
/// All interior points that were filled
|
|
pub interior_points: Vec<Point>,
|
|
}
|
|
|
|
/// Flood fill configuration
|
|
pub struct FloodFillConfig {
|
|
/// Distance threshold - points closer than this to a curve are boundary points
|
|
pub epsilon: f64,
|
|
/// Step size for sampling (distance between sampled points)
|
|
pub step_size: f64,
|
|
/// Maximum number of points to sample (prevents infinite loops)
|
|
pub max_points: usize,
|
|
/// Bounding box to constrain the fill
|
|
pub bounds: Option<BoundingBox>,
|
|
}
|
|
|
|
impl Default for FloodFillConfig {
|
|
fn default() -> Self {
|
|
Self {
|
|
epsilon: 2.0,
|
|
step_size: 5.0,
|
|
max_points: 10000,
|
|
bounds: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Perform flood fill starting from a point
|
|
///
|
|
/// This function expands outward from the start point, stopping when it
|
|
/// encounters curves (within epsilon distance). It returns all boundary
|
|
/// points along with information about which curve each point is near.
|
|
///
|
|
/// # Parameters
|
|
/// - `start`: Starting point for the flood fill
|
|
/// - `curves`: All curve segments in the scene
|
|
/// - `quadtree`: Spatial index for efficient curve queries
|
|
/// - `config`: Flood fill configuration
|
|
///
|
|
/// # Returns
|
|
/// FloodFillResult with boundary and interior points
|
|
pub fn flood_fill(
|
|
start: Point,
|
|
curves: &[CurveSegment],
|
|
quadtree: &Quadtree,
|
|
config: &FloodFillConfig,
|
|
) -> FloodFillResult {
|
|
let mut visited = HashSet::new();
|
|
let mut queue = VecDeque::new();
|
|
let mut boundary_points = Vec::new();
|
|
let mut interior_points = Vec::new();
|
|
|
|
// Quantize start point to grid
|
|
let start_grid = point_to_grid(start, config.step_size);
|
|
queue.push_back(start_grid);
|
|
visited.insert(start_grid);
|
|
|
|
while let Some(grid_point) = queue.pop_front() {
|
|
// Check max points limit
|
|
if visited.len() >= config.max_points {
|
|
break;
|
|
}
|
|
|
|
// Convert grid point back to actual coordinates
|
|
let point = grid_to_point(grid_point, config.step_size);
|
|
|
|
// Check bounds if specified
|
|
if let Some(ref bounds) = config.bounds {
|
|
if !bounds.contains_point(point) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Query quadtree for nearby curves
|
|
let query_bbox = BoundingBox::around_point(point, config.epsilon * 2.0);
|
|
let nearby_curve_indices = quadtree.query(&query_bbox);
|
|
|
|
// Find the nearest curve
|
|
let nearest = find_nearest_curve(point, curves, &nearby_curve_indices);
|
|
|
|
if let Some((curve_idx, t, nearest_point, distance)) = nearest {
|
|
// If we're within epsilon, this is a boundary point
|
|
if distance < config.epsilon {
|
|
boundary_points.push(BoundaryPoint {
|
|
point,
|
|
curve_index: curve_idx,
|
|
t,
|
|
nearest_point,
|
|
distance,
|
|
});
|
|
continue; // Don't expand from boundary points
|
|
}
|
|
}
|
|
|
|
// This is an interior point - add to interior and expand
|
|
interior_points.push(point);
|
|
|
|
// Add neighbors to queue (4-directional)
|
|
let neighbors = [
|
|
(grid_point.0 + 1, grid_point.1), // Right
|
|
(grid_point.0 - 1, grid_point.1), // Left
|
|
(grid_point.0, grid_point.1 + 1), // Down
|
|
(grid_point.0, grid_point.1 - 1), // Up
|
|
(grid_point.0 + 1, grid_point.1 + 1), // Diagonal: down-right
|
|
(grid_point.0 + 1, grid_point.1 - 1), // Diagonal: up-right
|
|
(grid_point.0 - 1, grid_point.1 + 1), // Diagonal: down-left
|
|
(grid_point.0 - 1, grid_point.1 - 1), // Diagonal: up-left
|
|
];
|
|
|
|
for neighbor in neighbors {
|
|
if !visited.contains(&neighbor) {
|
|
visited.insert(neighbor);
|
|
queue.push_back(neighbor);
|
|
}
|
|
}
|
|
}
|
|
|
|
FloodFillResult {
|
|
boundary_points,
|
|
interior_points,
|
|
}
|
|
}
|
|
|
|
/// Convert a point to grid coordinates
|
|
fn point_to_grid(point: Point, step_size: f64) -> (i32, i32) {
|
|
let x = (point.x / step_size).round() as i32;
|
|
let y = (point.y / step_size).round() as i32;
|
|
(x, y)
|
|
}
|
|
|
|
/// Convert grid coordinates back to a point
|
|
fn grid_to_point(grid: (i32, i32), step_size: f64) -> Point {
|
|
Point::new(grid.0 as f64 * step_size, grid.1 as f64 * step_size)
|
|
}
|
|
|
|
/// Find the nearest curve to a point from a set of candidate curves
|
|
///
|
|
/// Returns (curve_index, parameter_t, nearest_point, distance)
|
|
fn find_nearest_curve(
|
|
point: Point,
|
|
all_curves: &[CurveSegment],
|
|
candidate_indices: &[usize],
|
|
) -> Option<(usize, f64, Point, f64)> {
|
|
let mut best: Option<(usize, f64, Point, f64)> = None;
|
|
|
|
for &curve_idx in candidate_indices {
|
|
if curve_idx >= all_curves.len() {
|
|
continue;
|
|
}
|
|
|
|
let curve = &all_curves[curve_idx];
|
|
let (t, nearest_point, dist_sq) = curve.nearest_point(point);
|
|
let distance = dist_sq.sqrt();
|
|
|
|
match best {
|
|
None => {
|
|
best = Some((curve_idx, t, nearest_point, distance));
|
|
}
|
|
Some((_, _, _, best_dist)) if distance < best_dist => {
|
|
best = Some((curve_idx, t, nearest_point, distance));
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
best
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::curve_segment::{CurveSegment, CurveType};
|
|
|
|
#[test]
|
|
fn test_point_to_grid_conversion() {
|
|
let point = Point::new(10.0, 20.0);
|
|
let step_size = 5.0;
|
|
|
|
let grid = point_to_grid(point, step_size);
|
|
assert_eq!(grid, (2, 4));
|
|
|
|
let back = grid_to_point(grid, step_size);
|
|
assert!((back.x - 10.0).abs() < 0.1);
|
|
assert!((back.y - 20.0).abs() < 0.1);
|
|
}
|
|
|
|
#[test]
|
|
fn test_find_nearest_curve() {
|
|
let curves = vec![
|
|
CurveSegment::new(
|
|
0,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(0.0, 0.0), Point::new(100.0, 0.0)],
|
|
),
|
|
CurveSegment::new(
|
|
1,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(0.0, 50.0), Point::new(100.0, 50.0)],
|
|
),
|
|
];
|
|
|
|
let point = Point::new(50.0, 10.0);
|
|
let candidates = vec![0, 1];
|
|
|
|
let result = find_nearest_curve(point, &curves, &candidates);
|
|
assert!(result.is_some());
|
|
|
|
let (curve_idx, _t, _nearest, distance) = result.unwrap();
|
|
assert_eq!(curve_idx, 0); // Should be nearest to first curve
|
|
assert!((distance - 10.0).abs() < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_flood_fill_simple_box() {
|
|
// Create a simple box with 4 lines
|
|
let curves = vec![
|
|
// Bottom
|
|
CurveSegment::new(
|
|
0,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(0.0, 0.0), Point::new(100.0, 0.0)],
|
|
),
|
|
// Right
|
|
CurveSegment::new(
|
|
1,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(100.0, 0.0), Point::new(100.0, 100.0)],
|
|
),
|
|
// Top
|
|
CurveSegment::new(
|
|
2,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(100.0, 100.0), Point::new(0.0, 100.0)],
|
|
),
|
|
// Left
|
|
CurveSegment::new(
|
|
3,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(0.0, 100.0), Point::new(0.0, 0.0)],
|
|
),
|
|
];
|
|
|
|
// Build quadtree
|
|
let mut quadtree = Quadtree::new(BoundingBox::new(-10.0, 110.0, -10.0, 110.0), 4);
|
|
for (i, curve) in curves.iter().enumerate() {
|
|
let bbox = curve.bounding_box();
|
|
quadtree.insert(&bbox, i);
|
|
}
|
|
|
|
// Fill from center
|
|
let config = FloodFillConfig {
|
|
epsilon: 2.0,
|
|
step_size: 5.0,
|
|
max_points: 10000,
|
|
bounds: Some(BoundingBox::new(-10.0, 110.0, -10.0, 110.0)),
|
|
};
|
|
|
|
let result = flood_fill(Point::new(50.0, 50.0), &curves, &quadtree, &config);
|
|
|
|
// Should have boundary points
|
|
assert!(!result.boundary_points.is_empty());
|
|
// Should have interior points
|
|
assert!(!result.interior_points.is_empty());
|
|
|
|
// All boundary points should be within epsilon of a curve
|
|
for bp in &result.boundary_points {
|
|
assert!(bp.distance < config.epsilon);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_flood_fill_respects_bounds() {
|
|
let curves = vec![CurveSegment::new(
|
|
0,
|
|
0,
|
|
CurveType::Line,
|
|
0.0,
|
|
1.0,
|
|
vec![Point::new(0.0, 0.0), Point::new(100.0, 0.0)],
|
|
)];
|
|
|
|
let mut quadtree = Quadtree::new(BoundingBox::new(-10.0, 110.0, -10.0, 110.0), 4);
|
|
for (i, curve) in curves.iter().enumerate() {
|
|
let bbox = curve.bounding_box();
|
|
quadtree.insert(&bbox, i);
|
|
}
|
|
|
|
let config = FloodFillConfig {
|
|
epsilon: 2.0,
|
|
step_size: 5.0,
|
|
max_points: 1000,
|
|
bounds: Some(BoundingBox::new(0.0, 50.0, 0.0, 50.0)),
|
|
};
|
|
|
|
let result = flood_fill(Point::new(25.0, 25.0), &curves, &quadtree, &config);
|
|
|
|
// All points should be within bounds
|
|
for point in &result.interior_points {
|
|
assert!(point.x >= 0.0 && point.x <= 50.0);
|
|
assert!(point.y >= 0.0 && point.y <= 50.0);
|
|
}
|
|
}
|
|
}
|