Paint bucket

This commit is contained in:
Skyler Lehmkuhl 2025-11-19 01:47:37 -05:00
parent 9204308033
commit e1d9514472
14 changed files with 4894 additions and 0 deletions

View File

@ -5,8 +5,10 @@
pub mod add_shape; pub mod add_shape;
pub mod move_objects; pub mod move_objects;
pub mod paint_bucket;
pub mod transform_objects; pub mod transform_objects;
pub use add_shape::AddShapeAction; pub use add_shape::AddShapeAction;
pub use move_objects::MoveObjectsAction; pub use move_objects::MoveObjectsAction;
pub use paint_bucket::PaintBucketAction;
pub use transform_objects::TransformObjectsAction; pub use transform_objects::TransformObjectsAction;

View File

@ -0,0 +1,296 @@
//! Paint bucket fill action
//!
//! This action performs a paint bucket fill operation starting from a click point,
//! using planar graph face detection to identify the region to fill.
use crate::action::Action;
use crate::curve_segment::CurveSegment;
use crate::document::Document;
use crate::gap_handling::GapHandlingMode;
use crate::layer::AnyLayer;
use crate::object::Object;
use crate::planar_graph::PlanarGraph;
use crate::shape::ShapeColor;
use uuid::Uuid;
use vello::kurbo::Point;
/// Action that performs a paint bucket fill operation
pub struct PaintBucketAction {
/// Layer ID to add the filled shape to
layer_id: Uuid,
/// Click point where fill was initiated
click_point: Point,
/// Fill color for the shape
fill_color: ShapeColor,
/// Tolerance for gap bridging (in pixels)
tolerance: f64,
/// Gap handling mode
gap_mode: GapHandlingMode,
/// ID of the created shape (set after execution)
created_shape_id: Option<Uuid>,
/// ID of the created object (set after execution)
created_object_id: Option<Uuid>,
}
impl PaintBucketAction {
/// Create a new paint bucket action
///
/// # Arguments
///
/// * `layer_id` - The layer to add the filled shape to
/// * `click_point` - Point where the user clicked to initiate fill
/// * `fill_color` - Color to fill the region with
/// * `tolerance` - Gap tolerance in pixels (default: 2.0)
/// * `gap_mode` - Gap handling mode (SnapAndSplit or BridgeSegment)
pub fn new(
layer_id: Uuid,
click_point: Point,
fill_color: ShapeColor,
tolerance: f64,
gap_mode: GapHandlingMode,
) -> Self {
Self {
layer_id,
click_point,
fill_color,
tolerance,
gap_mode,
created_shape_id: None,
created_object_id: None,
}
}
}
impl Action for PaintBucketAction {
fn execute(&mut self, document: &mut Document) {
println!("=== PaintBucketAction::execute (Planar Graph Approach) ===");
// Step 1: Extract curves from stroked shapes only (not filled regions)
let all_curves = extract_curves_from_stroked_shapes(document, &self.layer_id);
println!("Extracted {} curves from stroked shapes", all_curves.len());
if all_curves.is_empty() {
println!("No curves found, returning");
return;
}
// Step 2: Build planar graph
println!("Building planar graph...");
let graph = PlanarGraph::build(&all_curves);
// Step 3: Render debug visualization of planar graph
println!("Rendering planar graph debug visualization...");
let (nodes_shape, edges_shape) = graph.render_debug();
let nodes_object = Object::new(nodes_shape.id);
let edges_object = Object::new(edges_shape.id);
if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) {
vector_layer.add_shape_internal(edges_shape);
vector_layer.add_object_internal(edges_object);
vector_layer.add_shape_internal(nodes_shape);
vector_layer.add_object_internal(nodes_object);
println!("DEBUG: Added graph visualization (yellow=edges, red=nodes)");
}
// Step 4: Find all faces
println!("Finding faces in planar graph...");
let faces = graph.find_faces();
// Step 5: Find which face contains the click point
println!("Finding face containing click point {:?}...", self.click_point);
if let Some(face_idx) = graph.find_face_containing_point(self.click_point, &faces) {
println!("Found face {} containing click point!", face_idx);
// Build the face boundary using actual curve segments
let face = &faces[face_idx];
let face_path = graph.build_face_path(face);
let face_shape = crate::shape::Shape::new(face_path)
.with_fill(self.fill_color); // Use the requested fill color
let face_object = Object::new(face_shape.id);
// Store the created IDs for rollback
self.created_shape_id = Some(face_shape.id);
self.created_object_id = Some(face_object.id);
if let Some(AnyLayer::Vector(vector_layer)) = document.get_layer_mut(&self.layer_id) {
vector_layer.add_shape_internal(face_shape);
vector_layer.add_object_internal(face_object);
println!("DEBUG: Added filled shape for face {}", face_idx);
}
} else {
println!("Click point is not inside any face!");
}
println!("=== Paint Bucket Complete: Face filled with curves ===");
}
fn rollback(&mut self, document: &mut Document) {
// Remove the created shape and object if they exist
if let (Some(shape_id), Some(object_id)) = (self.created_shape_id, self.created_object_id) {
let layer = match document.get_layer_mut(&self.layer_id) {
Some(l) => l,
None => return,
};
if let AnyLayer::Vector(vector_layer) = layer {
vector_layer.remove_object_internal(&object_id);
vector_layer.remove_shape_internal(&shape_id);
}
self.created_shape_id = None;
self.created_object_id = None;
}
}
fn description(&self) -> String {
"Paint bucket fill".to_string()
}
}
/// Extract curves from stroked shapes only (not filled regions)
///
/// This filters out paint bucket filled shapes which have only fills, not strokes.
/// Stroked shapes define boundaries for the planar graph.
fn extract_curves_from_stroked_shapes(
document: &Document,
layer_id: &Uuid,
) -> Vec<CurveSegment> {
let mut all_curves = Vec::new();
// Get the specified layer
let layer = match document.get_layer(layer_id) {
Some(l) => l,
None => return all_curves,
};
// Extract curves only from this vector layer
if let AnyLayer::Vector(vector_layer) = layer {
// Extract curves from each object (which applies transforms to shapes)
for object in &vector_layer.objects {
// Find the shape for this object
let shape = match vector_layer.shapes.iter().find(|s| s.id == object.shape_id) {
Some(s) => s,
None => continue,
};
// Skip shapes without strokes (these are filled regions, not boundaries)
if shape.stroke_color.is_none() {
continue;
}
// Get the transform matrix from the object
let transform_affine = object.transform.to_affine();
let path = shape.path();
let mut current_point = Point::ZERO;
let mut segment_index = 0;
for element in path.elements() {
// Extract curve segment from path element
if let Some(mut segment) = CurveSegment::from_path_element(
shape.id.as_u128() as usize,
segment_index,
element,
current_point,
) {
// Apply the object's transform to all control points
for control_point in &mut segment.control_points {
*control_point = transform_affine * (*control_point);
}
all_curves.push(segment);
segment_index += 1;
}
// Update current point for next iteration (keep in local space)
match element {
vello::kurbo::PathEl::MoveTo(p) => current_point = *p,
vello::kurbo::PathEl::LineTo(p) => current_point = *p,
vello::kurbo::PathEl::QuadTo(_, p) => current_point = *p,
vello::kurbo::PathEl::CurveTo(_, _, p) => current_point = *p,
vello::kurbo::PathEl::ClosePath => {}
}
}
}
}
all_curves
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layer::VectorLayer;
use vello::kurbo::{Rect, Shape as KurboShape};
#[test]
fn test_paint_bucket_action_basic() {
// Create a document with a vector layer
let mut document = Document::new("Test");
let vector_layer = VectorLayer::new("Layer 1");
let layer_id = document.root.add_child(AnyLayer::Vector(vector_layer));
// Create a simple rectangle shape (boundary for fill)
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let path = rect.to_path(0.1);
let shape = Shape::new(path);
let object = Object::new(shape.id);
// Add the boundary shape
if let Some(AnyLayer::Vector(layer)) = document.get_layer_mut(&layer_id) {
layer.add_shape_internal(shape);
layer.add_object_internal(object);
}
// Create and execute paint bucket action
let mut action = PaintBucketAction::new(
layer_id,
Point::new(50.0, 50.0), // Click in center
ShapeColor::rgb(255, 0, 0), // Red fill
2.0,
GapHandlingMode::BridgeSegment,
);
action.execute(&mut document);
// Verify a filled shape was created
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
// Should have original shape + filled shape
assert!(layer.shapes.len() >= 1);
assert!(layer.objects.len() >= 1);
} else {
panic!("Layer not found or not a vector layer");
}
// Test rollback
action.rollback(&mut document);
if let Some(AnyLayer::Vector(layer)) = document.get_layer(&layer_id) {
// Should only have original shape
assert_eq!(layer.shapes.len(), 1);
assert_eq!(layer.objects.len(), 1);
}
}
#[test]
fn test_paint_bucket_action_description() {
let action = PaintBucketAction::new(
Uuid::new_v4(),
Point::ZERO,
ShapeColor::rgb(0, 0, 255),
2.0,
GapHandlingMode::BridgeSegment,
);
assert_eq!(action.description(), "Paint bucket fill");
}
}

View File

@ -0,0 +1,337 @@
//! Curve intersection algorithm using recursive subdivision
//!
//! This module implements intersection finding between Bezier curve segments
//! using a recursive subdivision algorithm similar to the one in bezier.js.
//! The algorithm is based on the paper "Intersection of Two Bezier Curves"
//! and uses bounding box tests to prune the search space.
use crate::curve_segment::CurveSegment;
use vello::kurbo::Point;
/// Result of a curve intersection test
#[derive(Debug, Clone)]
pub struct CurveIntersection {
/// Parameter t on the first curve (in range [0, 1])
pub t1: f64,
/// Parameter t on the second curve (in range [0, 1])
pub t2: f64,
/// Point of intersection
pub point: Point,
}
/// Find all intersections between two curve segments
///
/// Uses recursive subdivision with bounding box pruning.
/// The threshold determines when curves are considered "small enough"
/// to return an intersection point.
///
/// # Parameters
/// - `curve1`: First curve segment
/// - `curve2`: Second curve segment
/// - `threshold`: Size threshold for convergence (sum of bbox widths + heights)
///
/// # Returns
/// Vector of intersection points with parameters on both curves
pub fn find_intersections(
curve1: &CurveSegment,
curve2: &CurveSegment,
threshold: f64,
) -> Vec<CurveIntersection> {
let mut results = Vec::new();
pair_iteration(curve1, curve2, threshold, &mut results);
results
}
/// Recursive subdivision algorithm for finding curve intersections
///
/// This is the core algorithm that mirrors the JavaScript bezier.js implementation.
fn pair_iteration(
c1: &CurveSegment,
c2: &CurveSegment,
threshold: f64,
results: &mut Vec<CurveIntersection>,
) {
// 1. Check if bounding boxes overlap - early exit if not
let bbox1 = c1.bounding_box();
let bbox2 = c2.bounding_box();
if !bbox1.intersects(&bbox2) {
return;
}
// 2. Base case: curves are small enough
let combined_size = bbox1.size() + bbox2.size();
if combined_size < threshold {
// Found an intersection - compute the midpoint parameters
let t1_mid = (c1.t_start + c1.t_end) / 2.0;
let t2_mid = (c2.t_start + c2.t_end) / 2.0;
// Evaluate at midpoints to get intersection point
// Average the two points for better accuracy
let p1 = c1.eval_at(0.5);
let p2 = c2.eval_at(0.5);
let point = Point::new((p1.x + p2.x) / 2.0, (p1.y + p2.y) / 2.0);
results.push(CurveIntersection {
t1: t1_mid,
t2: t2_mid,
point,
});
return;
}
// 3. Recursive case: split both curves and test all 4 pairs
let (c1_left, c1_right) = c1.split_at(0.5);
let (c2_left, c2_right) = c2.split_at(0.5);
// Test all 4 combinations:
// (c1_left, c2_left), (c1_left, c2_right), (c1_right, c2_left), (c1_right, c2_right)
pair_iteration(&c1_left, &c2_left, threshold, results);
pair_iteration(&c1_left, &c2_right, threshold, results);
pair_iteration(&c1_right, &c2_left, threshold, results);
pair_iteration(&c1_right, &c2_right, threshold, results);
}
/// Find intersection between a curve and a line segment
///
/// This is a specialized version for line-curve intersections which can be
/// more efficient than the general curve-curve intersection.
pub fn find_line_curve_intersections(
line: &CurveSegment,
curve: &CurveSegment,
threshold: f64,
) -> Vec<CurveIntersection> {
// For now, just use the general algorithm
// TODO: Optimize with line-specific tests
find_intersections(line, curve, threshold)
}
/// Check if two curves intersect (without computing exact intersection points)
///
/// This is faster than find_intersections when you only need to know
/// whether curves intersect, not where.
pub fn curves_intersect(c1: &CurveSegment, c2: &CurveSegment, threshold: f64) -> bool {
curves_intersect_internal(c1, c2, threshold)
}
fn curves_intersect_internal(c1: &CurveSegment, c2: &CurveSegment, threshold: f64) -> bool {
// Check if bounding boxes overlap
let bbox1 = c1.bounding_box();
let bbox2 = c2.bounding_box();
if !bbox1.intersects(&bbox2) {
return false;
}
// Base case: curves are small enough
let combined_size = bbox1.size() + bbox2.size();
if combined_size < threshold {
return true;
}
// Recursive case: split and test
let (c1_left, c1_right) = c1.split_at(0.5);
let (c2_left, c2_right) = c2.split_at(0.5);
curves_intersect_internal(&c1_left, &c2_left, threshold)
|| curves_intersect_internal(&c1_left, &c2_right, threshold)
|| curves_intersect_internal(&c1_right, &c2_left, threshold)
|| curves_intersect_internal(&c1_right, &c2_right, threshold)
}
/// Remove duplicate intersections that are very close to each other
///
/// The recursive subdivision algorithm can find the same intersection
/// multiple times from different branches. This function deduplicates
/// intersections that are within `epsilon` distance of each other.
pub fn deduplicate_intersections(
intersections: &[CurveIntersection],
epsilon: f64,
) -> Vec<CurveIntersection> {
let mut unique = Vec::new();
let epsilon_sq = epsilon * epsilon;
for intersection in intersections {
// Check if this intersection is close to any existing one
let is_duplicate = unique.iter().any(|existing: &CurveIntersection| {
let dx = intersection.point.x - existing.point.x;
let dy = intersection.point.y - existing.point.y;
dx * dx + dy * dy < epsilon_sq
});
if !is_duplicate {
unique.push(intersection.clone());
}
}
unique
}
#[cfg(test)]
mod tests {
use super::*;
use crate::curve_segment::{CurveSegment, CurveType};
#[test]
fn test_line_line_intersection() {
// Two lines that cross at (50, 50)
let line1 = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)],
);
let line2 = CurveSegment::new(
1,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 100.0), Point::new(100.0, 0.0)],
);
let intersections = find_intersections(&line1, &line2, 1.0);
assert!(!intersections.is_empty());
// Should find intersection near (50, 50)
let intersection = &intersections[0];
assert!((intersection.point.x - 50.0).abs() < 5.0);
assert!((intersection.point.y - 50.0).abs() < 5.0);
}
#[test]
fn test_parallel_lines_no_intersection() {
// Two parallel lines that don't intersect
let line1 = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 0.0)],
);
let line2 = CurveSegment::new(
1,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 10.0), Point::new(100.0, 10.0)],
);
let intersections = find_intersections(&line1, &line2, 1.0);
assert!(intersections.is_empty());
}
#[test]
fn test_curves_intersect_check() {
// Two lines that cross
let line1 = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)],
);
let line2 = CurveSegment::new(
1,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 100.0), Point::new(100.0, 0.0)],
);
assert!(curves_intersect(&line1, &line2, 1.0));
}
#[test]
fn test_no_intersection_check() {
// Two lines that don't intersect
let line1 = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(10.0, 0.0)],
);
let line2 = CurveSegment::new(
1,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(20.0, 0.0), Point::new(30.0, 0.0)],
);
assert!(!curves_intersect(&line1, &line2, 1.0));
}
#[test]
fn test_deduplicate_intersections() {
let intersections = vec![
CurveIntersection {
t1: 0.5,
t2: 0.5,
point: Point::new(50.0, 50.0),
},
CurveIntersection {
t1: 0.50001,
t2: 0.50001,
point: Point::new(50.001, 50.001),
},
CurveIntersection {
t1: 0.7,
t2: 0.3,
point: Point::new(70.0, 30.0),
},
];
let unique = deduplicate_intersections(&intersections, 0.1);
// First two should be deduplicated, third should remain
assert_eq!(unique.len(), 2);
}
#[test]
fn test_quadratic_curve_intersection() {
// Line from (0, 50) to (100, 50)
let line = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 50.0), Point::new(100.0, 50.0)],
);
// Quadratic curve that crosses the line
let quad = CurveSegment::new(
1,
0,
CurveType::Quadratic,
0.0,
1.0,
vec![
Point::new(50.0, 0.0),
Point::new(50.0, 100.0),
Point::new(50.0, 100.0),
],
);
let intersections = find_intersections(&line, &quad, 1.0);
// Should find at least one intersection
assert!(!intersections.is_empty());
}
}

View File

@ -0,0 +1,357 @@
//! 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);
}
}

View File

@ -0,0 +1,501 @@
//! Curve segment representation for paint bucket fill algorithm
//!
//! This module provides types for representing segments of Bezier curves
//! with parameter ranges. These segments are used to build filled paths
//! from the exact geometry of curves that bound a filled region.
use vello::kurbo::{
CubicBez, Line, ParamCurve, ParamCurveNearest, PathEl, Point, QuadBez, Shape,
};
/// Type of Bezier curve segment
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CurveType {
/// Straight line segment
Line,
/// Quadratic Bezier curve
Quadratic,
/// Cubic Bezier curve
Cubic,
}
/// A segment of a Bezier curve with parameter range
///
/// Represents a portion of a curve from parameter t_start to t_end.
/// The curve is identified by its index in a document's path list,
/// and the segment within that path.
#[derive(Debug, Clone)]
pub struct CurveSegment {
/// Index of the shape/path in the document
pub shape_index: usize,
/// Index of the segment within the path
pub segment_index: usize,
/// Type of curve
pub curve_type: CurveType,
/// Start parameter (0.0 to 1.0)
pub t_start: f64,
/// End parameter (0.0 to 1.0)
pub t_end: f64,
/// Cached control points for this segment
pub control_points: Vec<Point>,
}
impl CurveSegment {
/// Create a new curve segment
pub fn new(
shape_index: usize,
segment_index: usize,
curve_type: CurveType,
t_start: f64,
t_end: f64,
control_points: Vec<Point>,
) -> Self {
Self {
shape_index,
segment_index,
curve_type,
t_start,
t_end,
control_points,
}
}
/// Create a curve segment from a full curve (t_start=0, t_end=1)
pub fn from_path_element(
shape_index: usize,
segment_index: usize,
element: &PathEl,
start_point: Point,
) -> Option<Self> {
match element {
PathEl::LineTo(p) => Some(Self::new(
shape_index,
segment_index,
CurveType::Line,
0.0,
1.0,
vec![start_point, *p],
)),
PathEl::QuadTo(p1, p2) => Some(Self::new(
shape_index,
segment_index,
CurveType::Quadratic,
0.0,
1.0,
vec![start_point, *p1, *p2],
)),
PathEl::CurveTo(p1, p2, p3) => Some(Self::new(
shape_index,
segment_index,
CurveType::Cubic,
0.0,
1.0,
vec![start_point, *p1, *p2, *p3],
)),
PathEl::MoveTo(_) | PathEl::ClosePath => None,
}
}
/// Evaluate the curve at parameter t (in segment's local [t_start, t_end] range)
pub fn eval_at(&self, t: f64) -> Point {
// Map t from segment range to curve range
let curve_t = self.t_start + t * (self.t_end - self.t_start);
match self.curve_type {
CurveType::Line => {
let line = Line::new(self.control_points[0], self.control_points[1]);
line.eval(curve_t)
}
CurveType::Quadratic => {
let quad = QuadBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
);
quad.eval(curve_t)
}
CurveType::Cubic => {
let cubic = CubicBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
self.control_points[3],
);
cubic.eval(curve_t)
}
}
}
/// Get the start point of this segment
pub fn start_point(&self) -> Point {
self.eval_at(0.0)
}
/// Get the end point of this segment
pub fn end_point(&self) -> Point {
self.eval_at(1.0)
}
/// Split this segment at parameter t (in local [0, 1] range)
///
/// Returns (left_segment, right_segment)
pub fn split_at(&self, t: f64) -> (Self, Self) {
match self.curve_type {
CurveType::Line => {
let line = Line::new(self.control_points[0], self.control_points[1]);
let split_point = line.eval(t);
let left = Self::new(
self.shape_index,
self.segment_index,
CurveType::Line,
0.0,
1.0,
vec![self.control_points[0], split_point],
);
let right = Self::new(
self.shape_index,
self.segment_index,
CurveType::Line,
0.0,
1.0,
vec![split_point, self.control_points[1]],
);
(left, right)
}
CurveType::Quadratic => {
let quad = QuadBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
);
let (q1, q2) = quad.subdivide();
let left = Self::new(
self.shape_index,
self.segment_index,
CurveType::Quadratic,
0.0,
1.0,
vec![q1.p0, q1.p1, q1.p2],
);
let right = Self::new(
self.shape_index,
self.segment_index,
CurveType::Quadratic,
0.0,
1.0,
vec![q2.p0, q2.p1, q2.p2],
);
(left, right)
}
CurveType::Cubic => {
let cubic = CubicBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
self.control_points[3],
);
let (c1, c2) = cubic.subdivide();
let left = Self::new(
self.shape_index,
self.segment_index,
CurveType::Cubic,
0.0,
1.0,
vec![c1.p0, c1.p1, c1.p2, c1.p3],
);
let right = Self::new(
self.shape_index,
self.segment_index,
CurveType::Cubic,
0.0,
1.0,
vec![c2.p0, c2.p1, c2.p2, c2.p3],
);
(left, right)
}
}
}
/// Get the bounding box of this curve segment
pub fn bounding_box(&self) -> crate::quadtree::BoundingBox {
match self.curve_type {
CurveType::Line => {
let line = Line::new(self.control_points[0], self.control_points[1]);
let rect = line.bounding_box();
crate::quadtree::BoundingBox::from_rect(rect)
}
CurveType::Quadratic => {
let quad = QuadBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
);
let rect = quad.bounding_box();
crate::quadtree::BoundingBox::from_rect(rect)
}
CurveType::Cubic => {
let cubic = CubicBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
self.control_points[3],
);
let rect = cubic.bounding_box();
crate::quadtree::BoundingBox::from_rect(rect)
}
}
}
/// Get the nearest point on this curve to a given point
///
/// Returns (parameter t, nearest point, distance squared)
pub fn nearest_point(&self, point: Point) -> (f64, Point, f64) {
match self.curve_type {
CurveType::Line => {
let line = Line::new(self.control_points[0], self.control_points[1]);
let t = line.nearest(point, 1e-6).t;
let nearest = line.eval(t);
let dist_sq = (nearest - point).hypot2();
(t, nearest, dist_sq)
}
CurveType::Quadratic => {
let quad = QuadBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
);
let t = quad.nearest(point, 1e-6).t;
let nearest = quad.eval(t);
let dist_sq = (nearest - point).hypot2();
(t, nearest, dist_sq)
}
CurveType::Cubic => {
let cubic = CubicBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
self.control_points[3],
);
let t = cubic.nearest(point, 1e-6).t;
let nearest = cubic.eval(t);
let dist_sq = (nearest - point).hypot2();
(t, nearest, dist_sq)
}
}
}
/// Convert this segment to a path element
pub fn to_path_element(&self) -> PathEl {
match self.curve_type {
CurveType::Line => PathEl::LineTo(self.control_points[1]),
CurveType::Quadratic => {
PathEl::QuadTo(self.control_points[1], self.control_points[2])
}
CurveType::Cubic => PathEl::CurveTo(
self.control_points[1],
self.control_points[2],
self.control_points[3],
),
}
}
/// Convert this segment to a cubic Bezier curve
///
/// Lines and quadratic curves are converted to their cubic equivalents.
pub fn to_cubic_bez(&self) -> CubicBez {
match self.curve_type {
CurveType::Line => {
// Convert line to cubic: p0, p0 + 1/3(p1-p0), p0 + 2/3(p1-p0), p1
let p0 = self.control_points[0];
let p1 = self.control_points[1];
let c1 = Point::new(
p0.x + (p1.x - p0.x) / 3.0,
p0.y + (p1.y - p0.y) / 3.0,
);
let c2 = 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, c1, c2, p1)
}
CurveType::Quadratic => {
// Convert quadratic to cubic using standard formula
// Cubic control points: p0, p0 + 2/3(p1-p0), p2 + 2/3(p1-p2), p2
let p0 = self.control_points[0];
let p1 = self.control_points[1];
let p2 = self.control_points[2];
let c1 = Point::new(
p0.x + 2.0 * (p1.x - p0.x) / 3.0,
p0.y + 2.0 * (p1.y - p0.y) / 3.0,
);
let c2 = Point::new(
p2.x + 2.0 * (p1.x - p2.x) / 3.0,
p2.y + 2.0 * (p1.y - p2.y) / 3.0,
);
CubicBez::new(p0, c1, c2, p2)
}
CurveType::Cubic => {
// Already cubic, just create from control points
CubicBez::new(
self.control_points[0],
self.control_points[1],
self.control_points[2],
self.control_points[3],
)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_line_segment_creation() {
let seg = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)],
);
assert_eq!(seg.curve_type, CurveType::Line);
assert_eq!(seg.start_point(), Point::new(0.0, 0.0));
assert_eq!(seg.end_point(), Point::new(100.0, 100.0));
}
#[test]
fn test_line_segment_eval() {
let seg = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)],
);
let mid = seg.eval_at(0.5);
assert!((mid.x - 50.0).abs() < 1e-6);
assert!((mid.y - 50.0).abs() < 1e-6);
}
#[test]
fn test_line_segment_split() {
let seg = CurveSegment::new(
0,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 0.0), Point::new(100.0, 100.0)],
);
let (left, right) = seg.split_at(0.5);
// After splitting, both segments have full parameter range
assert_eq!(left.t_start, 0.0);
assert_eq!(left.t_end, 1.0);
assert_eq!(right.t_start, 0.0);
assert_eq!(right.t_end, 1.0);
// End of left should match start of right
assert_eq!(left.end_point(), right.start_point());
// Check that split happened at the midpoint
let expected_mid = Point::new(50.0, 50.0);
assert!((left.end_point().x - expected_mid.x).abs() < 1e-6);
assert!((left.end_point().y - expected_mid.y).abs() < 1e-6);
}
#[test]
fn test_quadratic_segment_creation() {
let seg = CurveSegment::new(
0,
0,
CurveType::Quadratic,
0.0,
1.0,
vec![
Point::new(0.0, 0.0),
Point::new(50.0, 100.0),
Point::new(100.0, 0.0),
],
);
assert_eq!(seg.curve_type, CurveType::Quadratic);
assert_eq!(seg.control_points.len(), 3);
}
#[test]
fn test_cubic_segment_creation() {
let seg = CurveSegment::new(
0,
0,
CurveType::Cubic,
0.0,
1.0,
vec![
Point::new(0.0, 0.0),
Point::new(33.0, 100.0),
Point::new(66.0, 100.0),
Point::new(100.0, 0.0),
],
);
assert_eq!(seg.curve_type, CurveType::Cubic);
assert_eq!(seg.control_points.len(), 4);
}
#[test]
fn test_from_path_element_line() {
let start = Point::new(0.0, 0.0);
let end = Point::new(100.0, 100.0);
let element = PathEl::LineTo(end);
let seg = CurveSegment::from_path_element(0, 0, &element, start).unwrap();
assert_eq!(seg.curve_type, CurveType::Line);
assert_eq!(seg.control_points.len(), 2);
assert_eq!(seg.start_point(), start);
assert_eq!(seg.end_point(), end);
}
#[test]
fn test_from_path_element_quad() {
let start = Point::new(0.0, 0.0);
let element = PathEl::QuadTo(Point::new(50.0, 100.0), Point::new(100.0, 0.0));
let seg = CurveSegment::from_path_element(0, 0, &element, start).unwrap();
assert_eq!(seg.curve_type, CurveType::Quadratic);
assert_eq!(seg.control_points.len(), 3);
}
#[test]
fn test_from_path_element_cubic() {
let start = Point::new(0.0, 0.0);
let element = PathEl::CurveTo(
Point::new(33.0, 100.0),
Point::new(66.0, 100.0),
Point::new(100.0, 0.0),
);
let seg = CurveSegment::from_path_element(0, 0, &element, start).unwrap();
assert_eq!(seg.curve_type, CurveType::Cubic);
assert_eq!(seg.control_points.len(), 4);
}
}

View File

@ -0,0 +1,352 @@
//! 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);
}
}
}

View File

@ -0,0 +1,85 @@
//! Gap handling modes for paint bucket fill
//!
//! When curves don't precisely intersect but come within tolerance distance,
//! we need to decide how to bridge the gap. This module defines the available
//! strategies.
/// Mode for handling gaps between curves during paint bucket fill
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GapHandlingMode {
/// Modify curves to connect at the midpoint of closest approach
///
/// When two curves come within tolerance distance but don't exactly intersect,
/// this mode will:
/// 1. Find the closest approach point between the curves
/// 2. Calculate the midpoint between the two closest points
/// 3. Split both curves at their respective t parameters
/// 4. Snap the endpoints to the midpoint
///
/// This creates a precise intersection by modifying the curve geometry.
/// The modification is temporary (only for the fill operation) and doesn't
/// affect the original shapes.
SnapAndSplit,
/// Insert a line segment to bridge the gap
///
/// When two curves come within tolerance distance but don't exactly intersect,
/// this mode will:
/// 1. Find the closest approach point between the curves
/// 2. Insert a straight line segment from the end of one curve to the start of the next
///
/// This preserves the original curve geometry but adds artificial connecting segments.
/// Bridge segments are included in the final filled path.
BridgeSegment,
}
impl Default for GapHandlingMode {
fn default() -> Self {
// Default to bridge segments as it's less invasive
GapHandlingMode::BridgeSegment
}
}
impl GapHandlingMode {
/// Get a human-readable description of this mode
pub fn description(&self) -> &'static str {
match self {
GapHandlingMode::SnapAndSplit => {
"Snap curves to midpoint and split at intersection"
}
GapHandlingMode::BridgeSegment => {
"Insert line segments to bridge gaps between curves"
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_mode() {
assert_eq!(GapHandlingMode::default(), GapHandlingMode::BridgeSegment);
}
#[test]
fn test_description() {
let snap = GapHandlingMode::SnapAndSplit;
let bridge = GapHandlingMode::BridgeSegment;
assert!(!snap.description().is_empty());
assert!(!bridge.description().is_empty());
assert_ne!(snap.description(), bridge.description());
}
#[test]
fn test_equality() {
let mode1 = GapHandlingMode::SnapAndSplit;
let mode2 = GapHandlingMode::SnapAndSplit;
let mode3 = GapHandlingMode::BridgeSegment;
assert_eq!(mode1, mode2);
assert_ne!(mode1, mode3);
}
}

View File

@ -0,0 +1,655 @@
//! Intersection graph for paint bucket fill
//!
//! This module implements an incremental graph-building approach for finding
//! closed regions to fill. Instead of flood-filling, we:
//! 1. Start at a curve found via raycast from the click point
//! 2. Find all intersections on that curve (with other curves and itself)
//! 3. Walk the graph, choosing the "most clockwise" turn at each junction
//! 4. Incrementally add nearby curves as we encounter them
//! 5. Track visited segments to detect when we've completed a loop
use crate::curve_intersections::{find_closest_approach, find_curve_intersections, find_self_intersections};
use crate::curve_segment::CurveSegment;
use crate::gap_handling::GapHandlingMode;
use crate::tolerance_quadtree::ToleranceQuadtree;
use std::collections::HashSet;
use vello::kurbo::{BezPath, CubicBez, ParamCurve, ParamCurveDeriv, ParamCurveNearest, Point};
/// A node in the intersection graph representing a point where curves meet
#[derive(Debug, Clone)]
pub struct IntersectionNode {
/// Location of this node
pub point: Point,
/// Edges connected to this node
pub edges: Vec<EdgeRef>,
}
/// Reference to an edge in the graph
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EdgeRef {
/// Index of the curve this edge follows
pub curve_id: usize,
/// Parameter value where this edge starts [0, 1]
pub t_start: f64,
/// Parameter value where this edge ends [0, 1]
pub t_end: f64,
/// Direction at the start of this edge (for angle calculations)
pub start_tangent: Point,
}
/// A visited segment (for loop detection)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct VisitedSegment {
curve_id: usize,
/// t_start quantized to 0.01 precision for hashing
t_start_quantized: i32,
/// t_end quantized to 0.01 precision for hashing
t_end_quantized: i32,
}
impl VisitedSegment {
fn new(curve_id: usize, t_start: f64, t_end: f64) -> Self {
Self {
curve_id,
t_start_quantized: (t_start * 100.0).round() as i32,
t_end_quantized: (t_end * 100.0).round() as i32,
}
}
}
/// Result of walking the intersection graph
pub struct WalkResult {
/// The closed path found by walking the graph
pub path: Option<BezPath>,
/// Debug information about the walk
pub debug_info: WalkDebugInfo,
}
/// Debug information about the walk process
#[derive(Default)]
pub struct WalkDebugInfo {
/// Number of segments walked
pub segments_walked: usize,
/// Number of intersections found
pub intersections_found: usize,
/// Number of gaps bridged
pub gaps_bridged: usize,
/// Whether the walk completed successfully
pub completed: bool,
/// Points visited during the walk (for visualization)
pub visited_points: Vec<Point>,
/// Segments walked during the graph traversal (curve_id, t_start, t_end)
pub walked_segments: Vec<(usize, f64, f64)>,
}
/// Configuration for the intersection graph walk
pub struct WalkConfig {
/// Gap tolerance in pixels
pub tolerance: f64,
/// Gap handling mode
pub gap_mode: GapHandlingMode,
/// Maximum number of segments to walk before giving up
pub max_segments: usize,
}
impl Default for WalkConfig {
fn default() -> Self {
Self {
tolerance: 2.0,
gap_mode: GapHandlingMode::default(),
max_segments: 10000,
}
}
}
/// Walk the intersection graph to find a closed path
///
/// # Arguments
///
/// * `start_point` - Point to start the walk (click point)
/// * `curves` - All curves in the scene
/// * `quadtree` - Spatial index for finding nearby curves
/// * `config` - Walk configuration
///
/// # Returns
///
/// A `WalkResult` with the closed path if one was found
pub fn walk_intersection_graph(
start_point: Point,
curves: &[CurveSegment],
quadtree: &ToleranceQuadtree,
config: &WalkConfig,
) -> WalkResult {
let mut debug_info = WalkDebugInfo::default();
// Step 1: Find the first curve via raycast
let first_curve_id = match find_curve_at_point(start_point, curves) {
Some(id) => id,
None => {
println!("No curve found at start point");
return WalkResult {
path: None,
debug_info,
};
}
};
println!("Starting walk from curve {}", first_curve_id);
// Step 2: Find a starting point on that curve
let first_curve = &curves[first_curve_id];
let nearest = first_curve.to_cubic_bez().nearest(start_point, 1e-6);
let start_t = nearest.t;
let start_pos = first_curve.to_cubic_bez().eval(start_t);
debug_info.visited_points.push(start_pos);
println!("Start position: ({:.1}, {:.1}) at t={:.3}", start_pos.x, start_pos.y, start_t);
// Step 3: Walk the graph
let mut path = BezPath::new();
path.move_to(start_pos);
let mut current_curve_id = first_curve_id;
let mut current_t = start_t;
let mut visited_segments = HashSet::new();
let mut processed_curves = HashSet::new();
processed_curves.insert(first_curve_id);
// Convert CurveSegments to CubicBez for easier processing
let cubic_curves: Vec<CubicBez> = curves.iter().map(|seg| seg.to_cubic_bez()).collect();
for _iteration in 0..config.max_segments {
debug_info.segments_walked += 1;
// Find all intersections on current curve
let intersections = find_intersections_on_curve(
current_curve_id,
&cubic_curves,
&processed_curves,
quadtree,
config.tolerance,
&mut debug_info,
);
println!("Found {} intersections on curve {}", intersections.len(), current_curve_id);
// Find the next intersection point in the forward direction (just to get to an intersection)
let next_intersection_point = intersections
.iter()
.filter(|i| i.t_on_current > current_t + 0.01) // Small epsilon to avoid same point
.min_by(|a, b| a.t_on_current.partial_cmp(&b.t_on_current).unwrap());
let next_intersection_point = match next_intersection_point {
Some(i) => i,
None => {
// Try wrapping around (for closed curves)
let wrapped = intersections
.iter()
.filter(|i| i.t_on_current < current_t - 0.01)
.min_by(|a, b| a.t_on_current.partial_cmp(&b.t_on_current).unwrap());
match wrapped {
Some(i) => i,
None => {
println!("No next intersection found, walk failed");
break;
}
}
}
};
println!("Reached intersection at t={:.3} on curve {}, point: ({:.1}, {:.1})",
next_intersection_point.t_on_current,
current_curve_id,
next_intersection_point.point.x,
next_intersection_point.point.y);
// Add segment from current position to intersection
let segment = extract_curve_segment(
&cubic_curves[current_curve_id],
current_t,
next_intersection_point.t_on_current,
);
add_segment_to_path(&mut path, &segment, config.gap_mode);
// Record this segment for debug visualization
debug_info.walked_segments.push((
current_curve_id,
current_t,
next_intersection_point.t_on_current,
));
// Mark this segment as visited
let visited = VisitedSegment::new(
current_curve_id,
current_t,
next_intersection_point.t_on_current,
);
// Check if we've completed a loop
if visited_segments.contains(&visited) {
println!("Loop detected! Walk complete");
debug_info.completed = true;
path.close_path();
break;
}
visited_segments.insert(visited);
debug_info.visited_points.push(next_intersection_point.point);
// Now at the intersection point, we need to choose which curve to follow next
// by finding all curves at this point and choosing the rightmost turn
// Calculate incoming direction (tangent at the end of the segment we just walked)
let incoming_deriv = cubic_curves[current_curve_id].deriv().eval(next_intersection_point.t_on_current);
let incoming_angle = incoming_deriv.y.atan2(incoming_deriv.x);
// For boundary walking, we measure angles from the REVERSE of the incoming direction
// (i.e., where we came FROM, not where we're going)
let reverse_incoming_angle = (incoming_angle + std::f64::consts::PI) % (2.0 * std::f64::consts::PI);
println!("Incoming angle: {:.2} rad ({:.1} deg), reverse: {:.2} rad ({:.1} deg)",
incoming_angle, incoming_angle.to_degrees(),
reverse_incoming_angle, reverse_incoming_angle.to_degrees());
// Find ALL intersections at this point (within tolerance)
let intersection_point = next_intersection_point.point;
let mut candidates: Vec<(usize, f64, f64, bool)> = Vec::new(); // (curve_id, t, angle_from_incoming, is_gap)
// Query the quadtree to find ALL curves at this intersection point
// Create a small bounding box around the point
use crate::quadtree::BoundingBox;
let search_bbox = BoundingBox {
x_min: intersection_point.x - config.tolerance,
x_max: intersection_point.x + config.tolerance,
y_min: intersection_point.y - config.tolerance,
y_max: intersection_point.y + config.tolerance,
};
let nearby_curves = quadtree.get_curves_in_region(&search_bbox);
println!("Querying quadtree at ({:.1}, {:.1}) found {} nearby curves",
intersection_point.x, intersection_point.y, nearby_curves.len());
// ALSO check ALL curves to see if any pass through this intersection
// (in case quadtree isn't finding everything)
let mut all_curves_at_point = nearby_curves.clone();
for curve_id in 0..cubic_curves.len() {
if !nearby_curves.contains(&curve_id) {
let curve_bez = &cubic_curves[curve_id];
let nearest = curve_bez.nearest(intersection_point, 1e-6);
let point_on_curve = curve_bez.eval(nearest.t);
let dist = (point_on_curve - intersection_point).hypot();
if dist < config.tolerance {
println!(" EXTRA: Curve {} found by brute-force check at t={:.3}, dist={:.4}", curve_id, nearest.t, dist);
all_curves_at_point.insert(curve_id);
}
}
}
let nearby_curves: Vec<usize> = all_curves_at_point.into_iter().collect();
for &curve_id in &nearby_curves {
// Find the t value on this curve closest to the intersection point
let curve_bez = &cubic_curves[curve_id];
let nearest = curve_bez.nearest(intersection_point, 1e-6);
let t_on_curve = nearest.t;
let point_on_curve = curve_bez.eval(t_on_curve);
let dist = (point_on_curve - intersection_point).hypot();
println!(" Curve {} at t={:.3}, dist={:.4}", curve_id, t_on_curve, dist);
if dist < config.tolerance {
// This curve passes through (or very near) the intersection point
let is_gap = dist > config.tolerance * 0.1; // Consider it a gap if not very close
// Forward direction (increasing t)
let forward_deriv = curve_bez.deriv().eval(t_on_curve);
let forward_angle = forward_deriv.y.atan2(forward_deriv.x);
let forward_angle_diff = normalize_angle(forward_angle - reverse_incoming_angle);
// Don't add this candidate if it's going back exactly where we came from
// (same curve, same t, same direction)
let is_reverse_on_current = curve_id == current_curve_id &&
(t_on_curve - next_intersection_point.t_on_current).abs() < 0.01 &&
forward_angle_diff < 0.1;
if !is_reverse_on_current {
candidates.push((curve_id, t_on_curve, forward_angle_diff, is_gap));
}
// Backward direction (decreasing t) - reverse the tangent
let backward_angle = (forward_angle + std::f64::consts::PI) % (2.0 * std::f64::consts::PI);
let backward_angle_diff = normalize_angle(backward_angle - reverse_incoming_angle);
let is_reverse_on_current_backward = curve_id == current_curve_id &&
(t_on_curve - next_intersection_point.t_on_current).abs() < 0.01 &&
backward_angle_diff < 0.1;
if !is_reverse_on_current_backward {
candidates.push((curve_id, t_on_curve, backward_angle_diff, is_gap));
}
}
}
println!("Found {} candidate outgoing edges", candidates.len());
for (i, (cid, t, angle, is_gap)) in candidates.iter().enumerate() {
println!(" Candidate {}: curve={}, t={:.3}, angle_diff={:.2} rad ({:.1} deg), gap={}",
i, cid, t, angle, angle.to_degrees(), is_gap);
}
// Choose the edge with the smallest positive angle (sharpest right turn for clockwise)
// Now that we measure from reverse_incoming_angle:
// - 0° = going back the way we came (filter out)
// - Small angles like 30°-90° = sharp right turn (what we want)
// - 180° = continuing straight (valid - don't filter)
// IMPORTANT:
// 1. Prefer non-gap edges over gap edges
// 2. When angles are equal, prefer switching to a different curve
let best_edge = candidates
.iter()
.filter(|(_cid, _, angle_diff, _)| {
// Don't go back the way we came (angle near 0)
let is_reverse = *angle_diff < 0.1;
!is_reverse
})
.min_by(|a, b| {
// First, prefer non-gap edges over gap edges
match (a.3, b.3) {
(false, true) => std::cmp::Ordering::Less, // a is non-gap, b is gap -> prefer a
(true, false) => std::cmp::Ordering::Greater, // a is gap, b is non-gap -> prefer b
_ => {
// Both same gap status -> compare angles
let angle_diff = (a.2 - b.2).abs();
const ANGLE_EPSILON: f64 = 0.01; // ~0.57 degrees tolerance for "equal" angles
if angle_diff < ANGLE_EPSILON {
// Angles are effectively equal - prefer different curve over same curve
let a_is_current = a.0 == current_curve_id;
let b_is_current = b.0 == current_curve_id;
match (a_is_current, b_is_current) {
(true, false) => std::cmp::Ordering::Greater, // a is current, b is different -> prefer b
(false, true) => std::cmp::Ordering::Less, // a is different, b is current -> prefer a
_ => a.2.partial_cmp(&b.2).unwrap(), // Both same or both different -> fall back to angle
}
} else {
a.2.partial_cmp(&b.2).unwrap()
}
}
}
});
let (next_curve_id, next_t, chosen_angle, is_gap) = match best_edge {
Some(&(cid, t, angle, gap)) => (cid, t, angle, gap),
None => {
println!("No valid outgoing edge found!");
break;
}
};
println!("Chose: curve={}, t={:.3}, angle={:.2} rad, gap={}",
next_curve_id, next_t, chosen_angle, is_gap);
// Handle gap if needed
if is_gap {
debug_info.gaps_bridged += 1;
match config.gap_mode {
GapHandlingMode::BridgeSegment => {
// Add a line segment to bridge the gap
let current_end = intersection_point;
let next_start = cubic_curves[next_curve_id].eval(next_t);
path.line_to(next_start);
println!("Bridged gap: ({:.1}, {:.1}) -> ({:.1}, {:.1})",
current_end.x, current_end.y, next_start.x, next_start.y);
}
GapHandlingMode::SnapAndSplit => {
// Snap to midpoint (geometry modification handled in segment extraction)
println!("Snapped to gap midpoint");
}
}
}
// Move to next curve
processed_curves.insert(next_curve_id);
current_curve_id = next_curve_id;
current_t = next_t;
// Check if we've returned to start
let current_pos = cubic_curves[current_curve_id].eval(current_t);
let dist_to_start = (current_pos - start_pos).hypot();
if dist_to_start < config.tolerance && current_curve_id == first_curve_id {
println!("Returned to start! Walk complete");
debug_info.completed = true;
path.close_path();
break;
}
}
println!("Walk finished: {} segments, {} intersections, {} gaps",
debug_info.segments_walked, debug_info.intersections_found, debug_info.gaps_bridged);
WalkResult {
path: if debug_info.completed { Some(path) } else { None },
debug_info,
}
}
/// Information about an intersection found on a curve
#[derive(Debug, Clone)]
struct CurveIntersection {
/// Parameter on current curve
t_on_current: f64,
/// Parameter on other curve
t_on_other: f64,
/// ID of the other curve
other_curve_id: usize,
/// Intersection point
point: Point,
/// Whether this is a gap (within tolerance but not exact intersection)
is_gap: bool,
}
/// Find all intersections on a given curve
fn find_intersections_on_curve(
curve_id: usize,
curves: &[CubicBez],
processed_curves: &HashSet<usize>,
quadtree: &ToleranceQuadtree,
tolerance: f64,
debug_info: &mut WalkDebugInfo,
) -> Vec<CurveIntersection> {
let mut intersections = Vec::new();
let current_curve = &curves[curve_id];
// Find nearby curves using quadtree
let nearby = quadtree.get_nearby_curves(current_curve);
for &other_id in &nearby {
if other_id == curve_id {
// Check for self-intersections
let self_ints = find_self_intersections(current_curve);
for int in self_ints {
intersections.push(CurveIntersection {
t_on_current: int.t1,
t_on_other: int.t2.unwrap_or(int.t1),
other_curve_id: curve_id,
point: int.point,
is_gap: false,
});
debug_info.intersections_found += 1;
}
} else {
let other_curve = &curves[other_id];
// Find exact intersections
let exact_ints = find_curve_intersections(current_curve, other_curve);
for int in exact_ints {
intersections.push(CurveIntersection {
t_on_current: int.t1,
t_on_other: int.t2.unwrap_or(0.0),
other_curve_id: other_id,
point: int.point,
is_gap: false,
});
debug_info.intersections_found += 1;
}
// Find close approaches (gaps within tolerance)
if let Some(approach) = find_closest_approach(current_curve, other_curve, tolerance) {
intersections.push(CurveIntersection {
t_on_current: approach.t1,
t_on_other: approach.t2,
other_curve_id: other_id,
point: approach.p1,
is_gap: true,
});
}
}
}
// Sort by t_on_current for easier processing
intersections.sort_by(|a, b| a.t_on_current.partial_cmp(&b.t_on_current).unwrap());
intersections
}
/// Find a curve at the given point via raycast
fn find_curve_at_point(point: Point, curves: &[CurveSegment]) -> Option<usize> {
let mut min_dist = f64::MAX;
let mut closest_id = None;
for (i, curve) in curves.iter().enumerate() {
let cubic = curve.to_cubic_bez();
let nearest = cubic.nearest(point, 1e-6);
let dist = (cubic.eval(nearest.t) - point).hypot();
if dist < min_dist {
min_dist = dist;
closest_id = Some(i);
}
}
// Only accept if within reasonable distance
if min_dist < 50.0 {
closest_id
} else {
None
}
}
/// Extract a subsegment of a curve between two t parameters
fn extract_curve_segment(curve: &CubicBez, t_start: f64, t_end: f64) -> CubicBez {
// Clamp parameters
let t_start = t_start.clamp(0.0, 1.0);
let t_end = t_end.clamp(0.0, 1.0);
if t_start >= t_end {
// Degenerate segment, return a point
let p = curve.eval(t_start);
return CubicBez::new(p, p, p, p);
}
// Use de Casteljau's algorithm to extract subsegment
curve.subsegment(t_start..t_end)
}
/// Add a curve segment to the path
fn add_segment_to_path(path: &mut BezPath, segment: &CubicBez, _gap_mode: GapHandlingMode) {
// Add as cubic bezier curve
path.curve_to(segment.p1, segment.p2, segment.p3);
}
#[cfg(test)]
mod tests {
use super::*;
use vello::kurbo::Circle;
#[test]
fn test_visited_segment_quantization() {
let seg1 = VisitedSegment::new(0, 0.123, 0.456);
let seg2 = VisitedSegment::new(0, 0.124, 0.457);
let seg3 = VisitedSegment::new(0, 0.123, 0.456);
assert_ne!(seg1, seg2);
assert_eq!(seg1, seg3);
}
#[test]
fn test_extract_curve_segment() {
let curve = CubicBez::new(
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 segment = extract_curve_segment(&curve, 0.25, 0.75);
// Segment should start and end at expected points
let start = segment.eval(0.0);
let end = segment.eval(1.0);
let expected_start = curve.eval(0.25);
let expected_end = curve.eval(0.75);
assert!((start - expected_start).hypot() < 1.0);
assert!((end - expected_end).hypot() < 1.0);
}
#[test]
fn test_find_curve_at_point() {
let curves = vec![
CurveSegment::new(
0,
0,
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),
],
),
];
// Point on the curve should find it
let found = find_curve_at_point(Point::new(50.0, 25.0), &curves);
assert!(found.is_some());
// Point far away should not find it
let not_found = find_curve_at_point(Point::new(1000.0, 1000.0), &curves);
assert!(not_found.is_none());
}
}
/// Normalize an angle difference to the range [0, 2*PI)
/// This is used to calculate the clockwise angle from one direction to another
fn normalize_angle(angle: f64) -> f64 {
let two_pi = 2.0 * std::f64::consts::PI;
let mut result = angle % two_pi;
if result < 0.0 {
result += two_pi;
}
// Handle floating point precision: if very close to 2π, wrap to 0
if result > two_pi - 0.01 {
result = 0.0;
}
result
}

View File

@ -16,3 +16,13 @@ pub mod action;
pub mod actions; pub mod actions;
pub mod selection; pub mod selection;
pub mod hit_test; pub mod hit_test;
pub mod quadtree;
pub mod tolerance_quadtree;
pub mod curve_segment;
pub mod curve_intersection;
pub mod curve_intersections;
pub mod flood_fill;
pub mod gap_handling;
pub mod intersection_graph;
pub mod segment_builder;
pub mod planar_graph;

View File

@ -0,0 +1,541 @@
//! Planar graph construction for paint bucket fill
//!
//! This module builds a planar graph from a collection of curves by:
//! 1. Finding all intersections (curve-curve and self-intersections)
//! 2. Splitting curves at intersection points to create graph edges
//! 3. Creating nodes at all intersection points and curve endpoints
//! 4. Connecting edges to form a complete planar graph
//!
//! The resulting graph can be used for face detection to identify regions for filling.
use crate::curve_intersections::{find_curve_intersections, find_self_intersections};
use crate::curve_segment::CurveSegment;
use crate::shape::{Shape, ShapeColor, StrokeStyle};
use std::collections::{HashMap, HashSet};
use vello::kurbo::{BezPath, Circle, CubicBez, Point, Shape as KurboShape};
/// A node in the planar graph (intersection point or endpoint)
#[derive(Debug, Clone)]
pub struct GraphNode {
/// Position of the node
pub position: Point,
/// Indices of edges connected to this node
pub edge_indices: Vec<usize>,
}
impl GraphNode {
pub fn new(position: Point) -> Self {
Self {
position,
edge_indices: Vec::new(),
}
}
}
/// An edge in the planar graph (curve segment between two nodes)
#[derive(Debug, Clone)]
pub struct GraphEdge {
/// Index of start node
pub start_node: usize,
/// Index of end node
pub end_node: usize,
/// Original curve ID
pub curve_id: usize,
/// Parameter at start of this edge on the original curve [0, 1]
pub t_start: f64,
/// Parameter at end of this edge on the original curve [0, 1]
pub t_end: f64,
}
impl GraphEdge {
pub fn new(
start_node: usize,
end_node: usize,
curve_id: usize,
t_start: f64,
t_end: f64,
) -> Self {
Self {
start_node,
end_node,
curve_id,
t_start,
t_end,
}
}
}
/// Planar graph structure
pub struct PlanarGraph {
/// All nodes in the graph
pub nodes: Vec<GraphNode>,
/// All edges in the graph
pub edges: Vec<GraphEdge>,
/// Original curves (referenced by edges)
pub curves: Vec<CubicBez>,
}
impl PlanarGraph {
/// Build a planar graph from a collection of curve segments
///
/// # Arguments
///
/// * `curve_segments` - The input curve segments
///
/// # Returns
///
/// A complete planar graph with nodes at all intersections and edges connecting them
pub fn build(curve_segments: &[CurveSegment]) -> Self {
println!("PlanarGraph::build started with {} curves", curve_segments.len());
// Convert curve segments to cubic beziers
let curves: Vec<CubicBez> = curve_segments
.iter()
.map(|seg| seg.to_cubic_bez())
.collect();
// Find all intersection points
let intersections = Self::find_all_intersections(&curves);
println!("Found {} intersection points", intersections.len());
// Create nodes and edges
let (nodes, edges) = Self::build_nodes_and_edges(&curves, intersections);
println!("Created {} nodes and {} edges", nodes.len(), edges.len());
Self {
nodes,
edges,
curves,
}
}
/// Find all intersections between curves
///
/// Returns a map from curve_id to sorted list of (t_value, point) intersections
fn find_all_intersections(curves: &[CubicBez]) -> HashMap<usize, Vec<(f64, Point)>> {
let mut intersections: HashMap<usize, Vec<(f64, Point)>> = HashMap::new();
// Initialize with endpoints for all curves
for (i, curve) in curves.iter().enumerate() {
let mut curve_intersections = vec![
(0.0, curve.p0),
(1.0, curve.p3),
];
intersections.insert(i, curve_intersections);
}
// Find curve-curve intersections
for i in 0..curves.len() {
for j in (i + 1)..curves.len() {
let curve_i_intersections = find_curve_intersections(&curves[i], &curves[j]);
for intersection in curve_i_intersections {
// Add to curve i
intersections
.get_mut(&i)
.unwrap()
.push((intersection.t1, intersection.point));
// Add to curve j
if let Some(t2) = intersection.t2 {
intersections
.get_mut(&j)
.unwrap()
.push((t2, intersection.point));
}
}
}
// Find self-intersections
let self_intersections = find_self_intersections(&curves[i]);
for intersection in self_intersections {
intersections
.get_mut(&i)
.unwrap()
.push((intersection.t1, intersection.point));
if let Some(t2) = intersection.t2 {
intersections
.get_mut(&i)
.unwrap()
.push((t2, intersection.point));
}
}
}
// Sort and deduplicate intersections for each curve
for curve_intersections in intersections.values_mut() {
curve_intersections.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
// Remove duplicates (points very close together)
let mut i = 0;
while i + 1 < curve_intersections.len() {
let dist = (curve_intersections[i].1 - curve_intersections[i + 1].1).hypot();
if dist < 0.5 {
curve_intersections.remove(i + 1);
} else {
i += 1;
}
}
}
intersections
}
/// Build nodes and edges from curves and their intersections
fn build_nodes_and_edges(
curves: &[CubicBez],
intersections: HashMap<usize, Vec<(f64, Point)>>,
) -> (Vec<GraphNode>, Vec<GraphEdge>) {
let mut nodes = Vec::new();
let mut edges = Vec::new();
// Map from position to node index (to avoid duplicate nodes)
let mut position_to_node: HashMap<(i32, i32), usize> = HashMap::new();
// Helper to get or create node at a position
let mut get_or_create_node = |position: Point,
nodes: &mut Vec<GraphNode>,
position_to_node: &mut HashMap<(i32, i32), usize>|
-> usize {
// Round to nearest pixel for lookup
let key = (position.x.round() as i32, position.y.round() as i32);
if let Some(&node_idx) = position_to_node.get(&key) {
node_idx
} else {
let node_idx = nodes.len();
nodes.push(GraphNode::new(position));
position_to_node.insert(key, node_idx);
node_idx
}
};
// Create edges for each curve
for (curve_id, curve_intersections) in intersections.iter() {
// Create edges between consecutive intersection points
for i in 0..(curve_intersections.len() - 1) {
let (t_start, p_start) = curve_intersections[i];
let (t_end, p_end) = curve_intersections[i + 1];
// Get or create nodes
let start_node = get_or_create_node(p_start, &mut nodes, &mut position_to_node);
let end_node = get_or_create_node(p_end, &mut nodes, &mut position_to_node);
// Create edge
let edge_idx = edges.len();
edges.push(GraphEdge::new(
start_node,
end_node,
*curve_id,
t_start,
t_end,
));
// Add edge to nodes
nodes[start_node].edge_indices.push(edge_idx);
nodes[end_node].edge_indices.push(edge_idx);
}
}
(nodes, edges)
}
/// Render debug visualization of the planar graph
///
/// Returns two shapes: one for nodes (red circles) and one for edges (yellow lines)
pub fn render_debug(&self) -> (Shape, Shape) {
// Render nodes as red circles
let mut nodes_path = BezPath::new();
for node in &self.nodes {
let circle = Circle::new(node.position, 3.0);
nodes_path.extend(circle.to_path(0.1));
}
let nodes_shape = Shape::new(nodes_path).with_stroke(
ShapeColor::rgb(255, 0, 0),
StrokeStyle {
width: 1.0,
..Default::default()
},
);
// Render edges as yellow straight lines
let mut edges_path = BezPath::new();
for edge in &self.edges {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
edges_path.move_to(start_pos);
edges_path.line_to(end_pos);
}
let edges_shape = Shape::new(edges_path).with_stroke(
ShapeColor::rgb(255, 255, 0),
StrokeStyle {
width: 0.5,
..Default::default()
},
);
(nodes_shape, edges_shape)
}
/// Find all faces in the planar graph
pub fn find_faces(&self) -> Vec<Face> {
// Debug: Print graph structure
println!("\n=== GRAPH STRUCTURE DEBUG ===");
for (node_idx, node) in self.nodes.iter().enumerate() {
println!("Node {}: pos=({:.1}, {:.1}), edges={:?}",
node_idx, node.position.x, node.position.y, node.edge_indices);
}
for (edge_idx, edge) in self.edges.iter().enumerate() {
println!("Edge {}: {} -> {}", edge_idx, edge.start_node, edge.end_node);
}
println!("=== END GRAPH STRUCTURE ===\n");
let mut faces = Vec::new();
let mut used_half_edges = HashSet::new();
// Try starting from each edge in both directions
for edge_idx in 0..self.edges.len() {
// Try forward direction
if !used_half_edges.contains(&(edge_idx, true)) {
if let Some(face) = self.trace_face(edge_idx, true, &mut used_half_edges) {
faces.push(face);
}
}
// Try backward direction
if !used_half_edges.contains(&(edge_idx, false)) {
if let Some(face) = self.trace_face(edge_idx, false, &mut used_half_edges) {
faces.push(face);
}
}
}
println!("Found {} faces", faces.len());
faces
}
/// Trace a face starting from an edge in a given direction
/// Returns None if the face is already traced or invalid
fn trace_face(
&self,
start_edge: usize,
forward: bool,
used_half_edges: &mut HashSet<(usize, bool)>,
) -> Option<Face> {
let mut edge_sequence = Vec::new();
let mut current_edge = start_edge;
let mut current_forward = forward;
loop {
// Mark this half-edge as used
if used_half_edges.contains(&(current_edge, current_forward)) {
// Already traced this half-edge
return None;
}
edge_sequence.push((current_edge, current_forward));
used_half_edges.insert((current_edge, current_forward));
// Get the end node of this half-edge
let edge = &self.edges[current_edge];
let end_node = if current_forward {
edge.end_node
} else {
edge.start_node
};
// Find the next edge in counterclockwise order around end_node
let next = self.find_next_ccw_edge(current_edge, current_forward, end_node);
if let Some((next_edge, next_forward)) = next {
current_edge = next_edge;
current_forward = next_forward;
// Check if we've completed the loop
if current_edge == start_edge && current_forward == forward {
return Some(Face { edges: edge_sequence });
}
} else {
// Dead end - not a valid face
return None;
}
// Safety check to prevent infinite loops
if edge_sequence.len() > self.edges.len() * 2 {
println!("Warning: Potential infinite loop detected in face tracing");
return None;
}
}
}
/// Find the next edge in counterclockwise order around a node
fn find_next_ccw_edge(
&self,
incoming_edge: usize,
incoming_forward: bool,
node_idx: usize,
) -> Option<(usize, bool)> {
let node = &self.nodes[node_idx];
// Get the incoming direction vector (pointing INTO this node)
let edge = &self.edges[incoming_edge];
let incoming_dir = if incoming_forward {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
(end_pos.x - start_pos.x, end_pos.y - start_pos.y)
} else {
let start_pos = self.nodes[edge.start_node].position;
let end_pos = self.nodes[edge.end_node].position;
(start_pos.x - end_pos.x, start_pos.y - end_pos.y)
};
// Find all outgoing edges from this node
let mut candidates = Vec::new();
for &edge_idx in &node.edge_indices {
let edge = &self.edges[edge_idx];
// Check if this edge goes out from node_idx
if edge.start_node == node_idx {
// Forward direction
let end_pos = self.nodes[edge.end_node].position;
let node_pos = node.position;
let out_dir = (end_pos.x - node_pos.x, end_pos.y - node_pos.y);
candidates.push((edge_idx, true, out_dir));
}
if edge.end_node == node_idx {
// Backward direction
let start_pos = self.nodes[edge.start_node].position;
let node_pos = node.position;
let out_dir = (start_pos.x - node_pos.x, start_pos.y - node_pos.y);
candidates.push((edge_idx, false, out_dir));
}
}
println!("find_next_ccw_edge: node {} has {} candidates", node_idx, candidates.len());
// Find the edge that makes the smallest left turn (most counterclockwise)
let mut best_edge = None;
let mut best_angle = std::f64::MAX;
for (edge_idx, forward, out_dir) in candidates {
// Skip the edge we came from (in opposite direction)
if edge_idx == incoming_edge && forward == !incoming_forward {
println!(" Skipping edge {} (came from there)", edge_idx);
continue;
}
// Compute angle from incoming to outgoing (counterclockwise)
let angle = angle_between_ccw(incoming_dir, out_dir);
println!(" Edge {} dir={} angle={}", edge_idx, if forward { "fwd" } else { "bwd" }, angle);
if angle < best_angle {
best_angle = angle;
best_edge = Some((edge_idx, forward));
}
}
println!(" Best: {:?} angle={}", best_edge, best_angle);
best_edge
}
/// Find which face contains a given point
pub fn find_face_containing_point(&self, point: Point, faces: &[Face]) -> Option<usize> {
for (i, face) in faces.iter().enumerate() {
if self.point_in_face(point, face) {
return Some(i);
}
}
None
}
/// Test if a point is inside a face using ray casting
fn point_in_face(&self, point: Point, face: &Face) -> bool {
// Build polygon from face edges
let mut polygon_points = Vec::new();
for &(edge_idx, forward) in &face.edges {
let edge = &self.edges[edge_idx];
let node_idx = if forward { edge.start_node } else { edge.end_node };
polygon_points.push(self.nodes[node_idx].position);
}
// Ray casting algorithm
point_in_polygon(point, &polygon_points)
}
/// Build a BezPath from a face using the actual curve segments
pub fn build_face_path(&self, face: &Face) -> BezPath {
use vello::kurbo::ParamCurve;
let mut path = BezPath::new();
let mut first = true;
for &(edge_idx, forward) in &face.edges {
let edge = &self.edges[edge_idx];
let orig_curve = &self.curves[edge.curve_id];
// Get the curve segment for this edge
let segment = if forward {
orig_curve.subsegment(edge.t_start..edge.t_end)
} else {
// Reverse the segment
orig_curve.subsegment(edge.t_end..edge.t_start)
};
if first {
path.move_to(segment.p0);
first = false;
}
// Add the curve segment
path.curve_to(segment.p1, segment.p2, segment.p3);
}
path.close_path();
path
}
}
/// A face in the planar graph (bounded region)
#[derive(Debug, Clone)]
pub struct Face {
/// Sequence of (edge_index, is_forward) pairs that form the boundary
pub edges: Vec<(usize, bool)>,
}
/// Compute the counterclockwise angle from v1 to v2
fn angle_between_ccw(v1: (f64, f64), v2: (f64, f64)) -> f64 {
let angle1 = v1.1.atan2(v1.0);
let angle2 = v2.1.atan2(v2.0);
let mut diff = angle2 - angle1;
// Normalize to [0, 2π)
while diff < 0.0 {
diff += 2.0 * std::f64::consts::PI;
}
while diff >= 2.0 * std::f64::consts::PI {
diff -= 2.0 * std::f64::consts::PI;
}
diff
}
/// Test if a point is inside a polygon using ray casting
fn point_in_polygon(point: Point, polygon: &[Point]) -> bool {
let mut inside = false;
let n = polygon.len();
for i in 0..n {
let j = (i + 1) % n;
let pi = polygon[i];
let pj = polygon[j];
if ((pi.y > point.y) != (pj.y > point.y)) &&
(point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x) {
inside = !inside;
}
}
inside
}

View File

@ -0,0 +1,533 @@
//! Quadtree spatial indexing for efficient curve queries
//!
//! This module provides a quadtree data structure optimized for storing
//! bounding boxes of Bezier curve segments. It supports:
//! - Fast spatial queries (which curves intersect a region?)
//! - Auto-expanding boundary (grows to accommodate new curves)
//! - Efficient insertion and querying
use vello::kurbo::{Point, Rect};
/// Axis-aligned bounding box
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BoundingBox {
pub x_min: f64,
pub x_max: f64,
pub y_min: f64,
pub y_max: f64,
}
impl BoundingBox {
/// Create a new bounding box
pub fn new(x_min: f64, x_max: f64, y_min: f64, y_max: f64) -> Self {
Self {
x_min,
x_max,
y_min,
y_max,
}
}
/// Create a bounding box from a vello Rect
pub fn from_rect(rect: Rect) -> Self {
Self {
x_min: rect.x0,
x_max: rect.x1,
y_min: rect.y0,
y_max: rect.y1,
}
}
/// Create a bounding box around a point with padding
pub fn around_point(point: Point, padding: f64) -> Self {
Self {
x_min: point.x - padding,
x_max: point.x + padding,
y_min: point.y - padding,
y_max: point.y + padding,
}
}
/// Get the width of this bounding box
pub fn width(&self) -> f64 {
self.x_max - self.x_min
}
/// Get the height of this bounding box
pub fn height(&self) -> f64 {
self.y_max - self.y_min
}
/// Get the combined size (width + height) for threshold checks
pub fn size(&self) -> f64 {
self.width() + self.height()
}
/// Check if this bounding box intersects with another
pub fn intersects(&self, other: &BoundingBox) -> bool {
!(other.x_max < self.x_min
|| other.x_min > self.x_max
|| other.y_max < self.y_min
|| other.y_min > self.y_max)
}
/// Check if this bounding box contains a point
pub fn contains_point(&self, point: Point) -> bool {
point.x >= self.x_min
&& point.x <= self.x_max
&& point.y >= self.y_min
&& point.y <= self.y_max
}
/// Check if this bounding box fully contains another bounding box
pub fn contains_bbox(&self, other: &BoundingBox) -> bool {
other.x_min >= self.x_min
&& other.x_max <= self.x_max
&& other.y_min >= self.y_min
&& other.y_max <= self.y_max
}
/// Get the center point of this bounding box
pub fn center(&self) -> Point {
Point::new(
(self.x_min + self.x_max) / 2.0,
(self.y_min + self.y_max) / 2.0,
)
}
/// Expand this bounding box to include another
pub fn expand_to_include(&mut self, other: &BoundingBox) {
self.x_min = self.x_min.min(other.x_min);
self.x_max = self.x_max.max(other.x_max);
self.y_min = self.y_min.min(other.y_min);
self.y_max = self.y_max.max(other.y_max);
}
}
/// Quadtree for spatial indexing of curve segments
pub struct Quadtree {
/// Boundary of this quadtree node
boundary: BoundingBox,
/// Maximum number of items before subdivision
capacity: usize,
/// Curve indices and their bounding boxes stored in this node
items: Vec<(usize, BoundingBox)>,
/// Whether this node has been subdivided
divided: bool,
// Child quadrants (only exist after subdivision)
nw: Option<Box<Quadtree>>, // Northwest (top-left)
ne: Option<Box<Quadtree>>, // Northeast (top-right)
sw: Option<Box<Quadtree>>, // Southwest (bottom-left)
se: Option<Box<Quadtree>>, // Southeast (bottom-right)
}
impl Quadtree {
/// Create a new quadtree with the given boundary and capacity
pub fn new(boundary: BoundingBox, capacity: usize) -> Self {
Self {
boundary,
capacity,
items: Vec::new(),
divided: false,
nw: None,
ne: None,
sw: None,
se: None,
}
}
/// Insert a curve's bounding box into the quadtree
///
/// If the bbox doesn't fit in current boundary, the tree will expand.
/// Returns true if inserted successfully.
pub fn insert(&mut self, bbox: &BoundingBox, curve_idx: usize) -> bool {
// If bbox is outside our boundary, we need to expand
if !self.boundary.contains_bbox(bbox) {
self.expand_to_contain(bbox);
}
self.insert_internal(bbox, curve_idx)
}
/// Internal insertion that assumes bbox fits within boundary
fn insert_internal(&mut self, bbox: &BoundingBox, curve_idx: usize) -> bool {
// Early exit if bbox doesn't intersect this node at all
if !self.boundary.intersects(bbox) {
return false;
}
// If we have space and haven't subdivided, store it here
if !self.divided && self.items.len() < self.capacity {
self.items.push((curve_idx, *bbox));
return true;
}
// Otherwise, subdivide if needed
if !self.divided {
self.subdivide();
}
// Try to insert into children (might go into multiple quadrants)
let mut inserted = false;
if let Some(ref mut nw) = self.nw {
inserted |= nw.insert_internal(bbox, curve_idx);
}
if let Some(ref mut ne) = self.ne {
inserted |= ne.insert_internal(bbox, curve_idx);
}
if let Some(ref mut sw) = self.sw {
inserted |= sw.insert_internal(bbox, curve_idx);
}
if let Some(ref mut se) = self.se {
inserted |= se.insert_internal(bbox, curve_idx);
}
inserted
}
/// Subdivide this node into 4 quadrants
fn subdivide(&mut self) {
let x_mid = (self.boundary.x_min + self.boundary.x_max) / 2.0;
let y_mid = (self.boundary.y_min + self.boundary.y_max) / 2.0;
// Northwest (top-left)
self.nw = Some(Box::new(Quadtree::new(
BoundingBox::new(
self.boundary.x_min,
x_mid,
self.boundary.y_min,
y_mid,
),
self.capacity,
)));
// Northeast (top-right)
self.ne = Some(Box::new(Quadtree::new(
BoundingBox::new(x_mid, self.boundary.x_max, self.boundary.y_min, y_mid),
self.capacity,
)));
// Southwest (bottom-left)
self.sw = Some(Box::new(Quadtree::new(
BoundingBox::new(self.boundary.x_min, x_mid, y_mid, self.boundary.y_max),
self.capacity,
)));
// Southeast (bottom-right)
self.se = Some(Box::new(Quadtree::new(
BoundingBox::new(x_mid, self.boundary.x_max, y_mid, self.boundary.y_max),
self.capacity,
)));
self.divided = true;
// Re-insert existing items into children
let items_to_redistribute = std::mem::take(&mut self.items);
for (idx, bbox) in items_to_redistribute {
// Insert into all children that intersect with the bbox
if let Some(ref mut nw) = self.nw {
nw.insert_internal(&bbox, idx);
}
if let Some(ref mut ne) = self.ne {
ne.insert_internal(&bbox, idx);
}
if let Some(ref mut sw) = self.sw {
sw.insert_internal(&bbox, idx);
}
if let Some(ref mut se) = self.se {
se.insert_internal(&bbox, idx);
}
}
}
/// Expand the quadtree to contain a bounding box that's outside current boundary
///
/// This is the complex auto-expanding logic from the JS implementation.
fn expand_to_contain(&mut self, bbox: &BoundingBox) {
// Determine which direction we need to expand
let needs_expand_left = bbox.x_min < self.boundary.x_min;
let needs_expand_right = bbox.x_max > self.boundary.x_max;
let needs_expand_top = bbox.y_min < self.boundary.y_min;
let needs_expand_bottom = bbox.y_max > self.boundary.y_max;
// Calculate the current width and height
let width = self.boundary.width();
let height = self.boundary.height();
// Create a new root that's twice as large in the necessary direction(s)
let new_boundary = if needs_expand_left && needs_expand_top {
// Expand northwest
BoundingBox::new(
self.boundary.x_min - width,
self.boundary.x_max,
self.boundary.y_min - height,
self.boundary.y_max,
)
} else if needs_expand_right && needs_expand_top {
// Expand northeast
BoundingBox::new(
self.boundary.x_min,
self.boundary.x_max + width,
self.boundary.y_min - height,
self.boundary.y_max,
)
} else if needs_expand_left && needs_expand_bottom {
// Expand southwest
BoundingBox::new(
self.boundary.x_min - width,
self.boundary.x_max,
self.boundary.y_min,
self.boundary.y_max + height,
)
} else if needs_expand_right && needs_expand_bottom {
// Expand southeast
BoundingBox::new(
self.boundary.x_min,
self.boundary.x_max + width,
self.boundary.y_min,
self.boundary.y_max + height,
)
} else if needs_expand_left {
// Expand west
BoundingBox::new(
self.boundary.x_min - width,
self.boundary.x_max,
self.boundary.y_min,
self.boundary.y_max,
)
} else if needs_expand_right {
// Expand east
BoundingBox::new(
self.boundary.x_min,
self.boundary.x_max + width,
self.boundary.y_min,
self.boundary.y_max,
)
} else if needs_expand_top {
// Expand north
BoundingBox::new(
self.boundary.x_min,
self.boundary.x_max,
self.boundary.y_min - height,
self.boundary.y_max,
)
} else {
// Expand south
BoundingBox::new(
self.boundary.x_min,
self.boundary.x_max,
self.boundary.y_min,
self.boundary.y_max + height,
)
};
// Clone current tree to become a child of new root
let old_tree = Quadtree {
boundary: self.boundary,
capacity: self.capacity,
items: std::mem::take(&mut self.items),
divided: self.divided,
nw: self.nw.take(),
ne: self.ne.take(),
sw: self.sw.take(),
se: self.se.take(),
};
// Update self to be the new larger root
self.boundary = new_boundary;
self.items.clear();
self.divided = true;
// Create quadrants and place old tree in appropriate position
self.subdivide();
// Move old tree to appropriate quadrant
// When expanding diagonally, old tree goes in opposite corner
// When expanding in one direction, old tree takes up half the space
if needs_expand_left && needs_expand_top {
// Old tree was in bottom-right, new space is top-left
self.se = Some(Box::new(old_tree));
} else if needs_expand_right && needs_expand_top {
// Old tree was in bottom-left, new space is top-right
self.sw = Some(Box::new(old_tree));
} else if needs_expand_left && needs_expand_bottom {
// Old tree was in top-right, new space is bottom-left
self.ne = Some(Box::new(old_tree));
} else if needs_expand_right && needs_expand_bottom {
// Old tree was in top-left, new space is bottom-right
self.nw = Some(Box::new(old_tree));
} else {
// For single-direction expansion, just place the old tree
// We'll let it naturally distribute when items are inserted
// Place it in a quadrant that makes sense for the expansion direction
if needs_expand_left {
self.ne = Some(Box::new(old_tree));
} else if needs_expand_right {
self.nw = Some(Box::new(old_tree));
} else if needs_expand_top {
self.sw = Some(Box::new(old_tree));
} else {
// needs_expand_bottom
self.nw = Some(Box::new(old_tree));
}
}
}
/// Query the quadtree for all curve indices that intersect with the given range
pub fn query(&self, range: &BoundingBox) -> Vec<usize> {
let mut found = Vec::new();
self.query_internal(range, &mut found);
// Remove duplicates
found.sort_unstable();
found.dedup();
found
}
/// Internal recursive query
fn query_internal(&self, range: &BoundingBox, found: &mut Vec<usize>) {
// If range doesn't intersect this node, nothing to do
if !self.boundary.intersects(range) {
return;
}
// Add items from this node that actually intersect the query range
for (idx, bbox) in &self.items {
if bbox.intersects(range) {
found.push(*idx);
}
}
// Recursively query children
if self.divided {
if let Some(ref nw) = self.nw {
nw.query_internal(range, found);
}
if let Some(ref ne) = self.ne {
ne.query_internal(range, found);
}
if let Some(ref sw) = self.sw {
sw.query_internal(range, found);
}
if let Some(ref se) = self.se {
se.query_internal(range, found);
}
}
}
/// Clear all items from the quadtree
pub fn clear(&mut self) {
self.items.clear();
self.divided = false;
self.nw = None;
self.ne = None;
self.sw = None;
self.se = None;
}
/// Get the boundary of this quadtree
pub fn boundary(&self) -> &BoundingBox {
&self.boundary
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bounding_box_creation() {
let bbox = BoundingBox::new(0.0, 100.0, 0.0, 50.0);
assert_eq!(bbox.width(), 100.0);
assert_eq!(bbox.height(), 50.0);
assert_eq!(bbox.size(), 150.0);
}
#[test]
fn test_bounding_box_intersects() {
let bbox1 = BoundingBox::new(0.0, 100.0, 0.0, 100.0);
let bbox2 = BoundingBox::new(50.0, 150.0, 50.0, 150.0);
let bbox3 = BoundingBox::new(200.0, 300.0, 200.0, 300.0);
assert!(bbox1.intersects(&bbox2));
assert!(bbox2.intersects(&bbox1));
assert!(!bbox1.intersects(&bbox3));
assert!(!bbox3.intersects(&bbox1));
}
#[test]
fn test_bounding_box_contains_point() {
let bbox = BoundingBox::new(0.0, 100.0, 0.0, 100.0);
assert!(bbox.contains_point(Point::new(50.0, 50.0)));
assert!(bbox.contains_point(Point::new(0.0, 0.0)));
assert!(bbox.contains_point(Point::new(100.0, 100.0)));
assert!(!bbox.contains_point(Point::new(150.0, 50.0)));
assert!(!bbox.contains_point(Point::new(50.0, 150.0)));
}
#[test]
fn test_quadtree_insert_and_query() {
let mut qt = Quadtree::new(BoundingBox::new(0.0, 100.0, 0.0, 100.0), 4);
// Insert some curves
qt.insert(&BoundingBox::new(10.0, 20.0, 10.0, 20.0), 0);
qt.insert(&BoundingBox::new(30.0, 40.0, 30.0, 40.0), 1);
qt.insert(&BoundingBox::new(60.0, 70.0, 60.0, 70.0), 2);
// Query overlapping region
let results = qt.query(&BoundingBox::new(15.0, 35.0, 15.0, 35.0));
assert!(results.contains(&0));
assert!(results.contains(&1));
assert!(!results.contains(&2));
}
#[test]
fn test_quadtree_subdivision() {
let mut qt = Quadtree::new(BoundingBox::new(0.0, 100.0, 0.0, 100.0), 2);
// Insert enough items to force subdivision
qt.insert(&BoundingBox::new(10.0, 20.0, 10.0, 20.0), 0);
qt.insert(&BoundingBox::new(30.0, 40.0, 30.0, 40.0), 1);
qt.insert(&BoundingBox::new(60.0, 70.0, 60.0, 70.0), 2);
qt.insert(&BoundingBox::new(80.0, 90.0, 80.0, 90.0), 3);
assert!(qt.divided);
// Should still be able to query
let results = qt.query(&BoundingBox::new(0.0, 100.0, 0.0, 100.0));
assert_eq!(results.len(), 4);
}
#[test]
fn test_quadtree_clear() {
let mut qt = Quadtree::new(BoundingBox::new(0.0, 100.0, 0.0, 100.0), 4);
qt.insert(&BoundingBox::new(10.0, 20.0, 10.0, 20.0), 0);
qt.insert(&BoundingBox::new(30.0, 40.0, 30.0, 40.0), 1);
qt.clear();
let results = qt.query(&BoundingBox::new(0.0, 100.0, 0.0, 100.0));
assert_eq!(results.len(), 0);
assert!(!qt.divided);
}
#[test]
fn test_quadtree_auto_expand() {
let mut qt = Quadtree::new(BoundingBox::new(0.0, 100.0, 0.0, 100.0), 4);
// Insert bbox outside current boundary
qt.insert(&BoundingBox::new(150.0, 200.0, 150.0, 200.0), 0);
// Boundary should have expanded
assert!(qt.boundary().x_max >= 200.0 || qt.boundary().y_max >= 200.0);
// Should be able to query the item
let results = qt.query(&BoundingBox::new(150.0, 200.0, 150.0, 200.0));
assert!(results.contains(&0));
}
}

View File

@ -0,0 +1,815 @@
//! Segment builder for constructing filled paths from boundary points
//!
//! This module takes boundary points from flood fill and builds a closed path
//! by extracting curve segments and connecting them with intersections or bridges.
use crate::curve_intersection::{deduplicate_intersections, find_intersections};
use crate::curve_segment::CurveSegment;
use crate::flood_fill::BoundaryPoint;
use std::collections::HashMap;
use vello::kurbo::{BezPath, Point, Shape};
/// Configuration for segment building
pub struct SegmentBuilderConfig {
/// Maximum gap to bridge with a line segment
pub gap_threshold: f64,
/// Threshold for curve intersection detection
pub intersection_threshold: f64,
}
impl Default for SegmentBuilderConfig {
fn default() -> Self {
Self {
gap_threshold: 2.0,
intersection_threshold: 0.5,
}
}
}
/// A curve segment extracted from boundary points
#[derive(Debug, Clone)]
struct ExtractedSegment {
/// Original curve index
curve_index: usize,
/// Minimum parameter value from boundary points
t_min: f64,
/// Maximum parameter value from boundary points
t_max: f64,
/// The curve segment (trimmed to [t_min, t_max])
segment: CurveSegment,
}
/// Build a closed path from boundary points
///
/// This function:
/// 1. Groups boundary points by curve
/// 2. Extracts curve segments for each group
/// 3. Connects adjacent segments (trimming at intersections or bridging gaps)
/// 4. Returns a closed BezPath
///
/// Returns None if the region cannot be closed (gaps too large, etc.)
///
/// The click_point parameter is used to verify that the found cycle actually
/// contains the clicked region.
pub fn build_path_from_boundary(
boundary_points: &[BoundaryPoint],
all_curves: &[CurveSegment],
config: &SegmentBuilderConfig,
click_point: Point,
) -> Option<BezPath> {
if boundary_points.is_empty() {
println!("build_path_from_boundary: No boundary points");
return None;
}
println!("build_path_from_boundary: Processing {} boundary points", boundary_points.len());
// Step 1: Group boundary points by curve and find parameter ranges
let extracted_segments = extract_segments(boundary_points, all_curves, click_point)?;
println!("build_path_from_boundary: Extracted {} segments", extracted_segments.len());
if extracted_segments.is_empty() {
println!("build_path_from_boundary: No segments extracted");
return None;
}
// Step 2: Connect segments to form a closed path that contains the click point
let connected_segments = connect_segments(&extracted_segments, config, click_point)?;
println!("build_path_from_boundary: Connected {} segments", connected_segments.len());
// Step 3: Build the final BezPath
Some(build_bez_path(&connected_segments))
}
/// Split segments at intersection points
/// This handles cases where curves cross in an X pattern
fn split_segments_at_intersections(segments: Vec<ExtractedSegment>) -> Vec<ExtractedSegment> {
use crate::curve_intersection::find_intersections;
let mut result = Vec::new();
let mut split_points: HashMap<usize, Vec<f64>> = HashMap::new();
// Find all intersections between segments
for i in 0..segments.len() {
for j in (i + 1)..segments.len() {
let intersections = find_intersections(&segments[i].segment, &segments[j].segment, 0.5);
for intersection in intersections {
// Record intersection parameters for both segments
split_points.entry(i).or_default().push(intersection.t1);
split_points.entry(j).or_default().push(intersection.t2);
}
}
}
println!("split_segments_at_intersections: Found {} segments with intersections", split_points.len());
// Split each segment at its intersection points
let original_count = segments.len();
for (idx, seg) in segments.into_iter().enumerate() {
if let Some(splits) = split_points.get(&idx) {
if splits.is_empty() {
result.push(seg);
continue;
}
// Sort split points
let mut sorted_splits = splits.clone();
sorted_splits.sort_by(|a, b| a.partial_cmp(b).unwrap());
// Add endpoints
let mut all_t = vec![0.0];
all_t.extend(sorted_splits.iter().copied());
all_t.push(1.0);
println!(" Splitting segment {} at {} points", idx, sorted_splits.len());
// Create sub-segments
for i in 0..(all_t.len() - 1) {
let t_start = all_t[i];
let t_end = all_t[i + 1];
if (t_end - t_start).abs() < 0.001 {
continue; // Skip very small segments
}
// Create a subsegment with adjusted t parameters
// The control_points stay the same, but we update t_start/t_end
let subseg = CurveSegment::new(
seg.segment.shape_index,
seg.segment.segment_index,
seg.segment.curve_type,
t_start,
t_end,
seg.segment.control_points.clone(),
);
result.push(ExtractedSegment {
curve_index: seg.curve_index,
t_min: t_start,
t_max: t_end,
segment: subseg,
});
}
} else {
// No intersections, keep as-is
result.push(seg);
}
}
println!("split_segments_at_intersections: {} segments -> {} segments after splitting", original_count, result.len());
result
}
/// Group boundary points by curve and extract segments
fn extract_segments(
boundary_points: &[BoundaryPoint],
all_curves: &[CurveSegment],
click_point: Point,
) -> Option<Vec<ExtractedSegment>> {
// Find the closest boundary point to the click
// Boundary points come from flood fill, so they're already from the correct region
println!("extract_segments: {} boundary points from flood fill", boundary_points.len());
println!("extract_segments: Click point: ({:.1}, {:.1})", click_point.x, click_point.y);
// Debug: print distribution of boundary points by curve
let mut curve_counts: std::collections::HashMap<usize, usize> = std::collections::HashMap::new();
for bp in boundary_points.iter() {
*curve_counts.entry(bp.curve_index).or_insert(0) += 1;
}
println!("extract_segments: Boundary points by curve:");
for (curve_idx, count) in curve_counts.iter() {
println!(" Curve {}: {} points", curve_idx, count);
}
// Debug: print first 5 boundary points
println!("extract_segments: First 5 boundary points:");
for (i, bp) in boundary_points.iter().take(5).enumerate() {
println!(" {}: ({:.1}, {:.1}) curve {}", i, bp.point.x, bp.point.y, bp.curve_index);
}
let mut closest_distance = f64::MAX;
let mut closest_boundary_point: Option<&BoundaryPoint> = None;
for bp in boundary_points {
let distance = click_point.distance(bp.point);
if distance < closest_distance {
closest_distance = distance;
closest_boundary_point = Some(bp);
}
}
let start_curve_idx = match closest_boundary_point {
Some(bp) => {
println!(
"extract_segments: Nearest boundary point at ({:.1}, {:.1}), distance: {:.1}, curve: {}",
bp.point.x, bp.point.y, closest_distance, bp.curve_index
);
bp.curve_index
}
None => {
println!("extract_segments: No boundary points found");
return None;
}
};
// We don't need to track nearest_point and nearest_t for the segment finding
// Just use the curve index to find segments after splitting
// Group points by curve index
let mut curve_points: HashMap<usize, Vec<&BoundaryPoint>> = HashMap::new();
for bp in boundary_points {
curve_points.entry(bp.curve_index).or_default().push(bp);
}
// Extract segment for each curve
let mut segments = Vec::new();
for (curve_idx, points) in curve_points {
if points.is_empty() {
continue;
}
// Find min and max t parameters
let t_min = points
.iter()
.map(|p| p.t)
.min_by(|a, b| a.partial_cmp(b).unwrap())?;
let t_max = points
.iter()
.map(|p| p.t)
.max_by(|a, b| a.partial_cmp(b).unwrap())?;
if curve_idx >= all_curves.len() {
continue;
}
let original_curve = &all_curves[curve_idx];
// Use the full curve (t=0 to t=1) rather than just the portion touched by boundary points
// This ensures we don't create artificial gaps in closed regions
let segment = CurveSegment::new(
original_curve.shape_index,
original_curve.segment_index,
original_curve.curve_type,
0.0, // Use full curve from start
1.0, // to end
original_curve.control_points.clone(),
);
segments.push(ExtractedSegment {
curve_index: curve_idx,
t_min,
t_max,
segment,
});
}
if segments.is_empty() {
return None;
}
// Split segments at intersection points
segments = split_segments_at_intersections(segments);
// Find a segment from the ray-intersected curve to use as starting point
let start_segment_idx = segments
.iter()
.position(|seg| seg.curve_index == start_curve_idx);
let start_segment_idx = match start_segment_idx {
Some(idx) => {
println!("extract_segments: Starting from segment {} (curve {})", idx, start_curve_idx);
idx
}
None => {
println!("extract_segments: No segment found for start curve {}", start_curve_idx);
return None;
}
};
// Reorder segments using graph-based cycle detection
// This finds the correct closed loop instead of greedy nearest-neighbor
// Higher threshold needed for split curves at intersections (floating point precision)
const CONNECTION_THRESHOLD: f64 = 5.0; // Endpoints within this distance can connect
// Try to find a valid cycle that contains the click point
// Start from the specific segment where the nearest boundary point was found
// BFS will naturally only explore segments connected to this starting segment
match find_segment_cycle(&segments, CONNECTION_THRESHOLD, click_point, start_segment_idx) {
Some(ordered_segments) => {
println!("extract_segments: Found valid cycle with {} segments", ordered_segments.len());
Some(ordered_segments)
}
None => {
println!("extract_segments: Could not find valid cycle through all segments");
None
}
}
}
/// Adjacency information for segment connections
struct SegmentConnections {
// Segments that can connect to the start point (when this segment is forward)
connects_to_start: Vec<(usize, bool, f64)>, // (index, reversed, distance)
// Segments that can connect to the end point (when this segment is forward)
connects_to_end: Vec<(usize, bool, f64)>,
}
/// Find a cycle through segments that contains the click point
/// Returns segments in order with proper orientation
/// Starts ONLY from the given segment index
fn find_segment_cycle(
segments: &[ExtractedSegment],
threshold: f64,
click_point: Point,
start_segment_idx: usize,
) -> Option<Vec<ExtractedSegment>> {
if segments.is_empty() {
return None;
}
println!("find_segment_cycle: Searching for cycle through {} segments", segments.len());
let mut connections: Vec<SegmentConnections> = (0..segments.len())
.map(|_| SegmentConnections {
connects_to_start: Vec::new(),
connects_to_end: Vec::new(),
})
.collect();
// Build connectivity graph
for i in 0..segments.len() {
for j in 0..segments.len() {
if i == j {
continue;
}
let seg_i = &segments[i];
let seg_j = &segments[j];
// Check all four possible connections:
// 1. seg_i end -> seg_j start (both forward)
let dist_end_to_start = seg_i.segment.end_point().distance(seg_j.segment.start_point());
if dist_end_to_start < threshold {
connections[i].connects_to_end.push((j, false, dist_end_to_start));
}
// 2. seg_i end -> seg_j end (j reversed)
let dist_end_to_end = seg_i.segment.end_point().distance(seg_j.segment.end_point());
if dist_end_to_end < threshold {
connections[i].connects_to_end.push((j, true, dist_end_to_end));
}
// 3. seg_i start -> seg_j start (both forward, but we'd traverse i backwards)
let dist_start_to_start = seg_i.segment.start_point().distance(seg_j.segment.start_point());
if dist_start_to_start < threshold {
connections[i].connects_to_start.push((j, false, dist_start_to_start));
}
// 4. seg_i start -> seg_j end (j reversed, i backwards)
let dist_start_to_end = seg_i.segment.start_point().distance(seg_j.segment.end_point());
if dist_start_to_end < threshold {
connections[i].connects_to_start.push((j, true, dist_start_to_end));
}
}
}
// Debug: Print connectivity information
for i in 0..segments.len() {
println!(
" Segment {}: {} connections from end, {} from start",
i,
connections[i].connects_to_end.len(),
connections[i].connects_to_start.len()
);
}
// Use BFS to find the shortest cycle that contains the click point
// BFS naturally explores shorter paths first
// Start ONLY from the specified segment
bfs_find_shortest_cycle(&segments, &connections, threshold, click_point, start_segment_idx)
}
/// Build a BezPath from ExtractedSegments (helper for testing containment)
fn build_bez_path_from_segments(segments: &[ExtractedSegment]) -> BezPath {
let mut path = BezPath::new();
if segments.is_empty() {
return path;
}
// Start at the first point
let start_point = segments[0].segment.start_point();
path.move_to(start_point);
// Add all segments
for seg in segments {
let element = seg.segment.to_path_element();
path.push(element);
}
// Close the path
path.close_path();
path
}
/// BFS to find the shortest cycle that contains the click point
/// Returns the first (shortest) cycle found that contains the click point
/// Starts ONLY from the specified segment index
fn bfs_find_shortest_cycle(
segments: &[ExtractedSegment],
connections: &[SegmentConnections],
threshold: f64,
click_point: Point,
start_segment_idx: usize,
) -> Option<Vec<ExtractedSegment>> {
use std::collections::VecDeque;
// State: (current_segment_idx, current_reversed, path so far, visited set)
type State = (usize, bool, Vec<(usize, bool)>, Vec<bool>);
if start_segment_idx >= segments.len() {
println!("bfs_find_shortest_cycle: Invalid start segment index {}", start_segment_idx);
return None;
}
println!("bfs_find_shortest_cycle: Starting ONLY from segment {} (curve {})",
start_segment_idx, segments[start_segment_idx].curve_index);
// Try starting from the one specified segment, in both orientations
for start_reversed in [false, true] {
let mut queue: VecDeque<State> = VecDeque::new();
let mut visited = vec![false; segments.len()];
visited[start_segment_idx] = true;
queue.push_back((
start_segment_idx,
start_reversed,
vec![(start_segment_idx, start_reversed)],
visited.clone(),
));
while let Some((current_idx, current_reversed, path, visited)) = queue.pop_front() {
// Check if we can close the cycle (need at least 3 segments)
if path.len() >= 3 {
let first = &path[0];
let current_end = if current_reversed {
segments[current_idx].segment.start_point()
} else {
segments[current_idx].segment.end_point()
};
let first_start = if first.1 {
segments[first.0].segment.end_point()
} else {
segments[first.0].segment.start_point()
};
let closing_gap = current_end.distance(first_start);
if closing_gap < threshold {
// Build final segment list with proper orientations
let mut result = Vec::new();
for (idx, reversed) in path.iter() {
let mut seg = segments[*idx].clone();
if *reversed {
seg.segment.control_points.reverse();
}
result.push(seg);
}
// Check if this cycle contains the click point
let test_path = build_bez_path_from_segments(&result);
let bbox = test_path.bounding_box();
let winding = test_path.winding(click_point);
println!(
" Testing {}-segment cycle: bbox=({:.1},{:.1})-({:.1},{:.1}), click=({:.1},{:.1}), winding={}",
result.len(),
bbox.x0, bbox.y0, bbox.x1, bbox.y1,
click_point.x, click_point.y, winding
);
if winding != 0 {
println!(
"bfs_find_shortest_cycle: Found cycle with {} segments (closing gap: {:.2}, winding: {})",
path.len(),
closing_gap,
winding
);
return Some(result);
} else {
println!(
"bfs_find_shortest_cycle: Cycle doesn't contain click point (winding: 0), continuing search..."
);
}
}
}
// Explore neighbors
let next_connections = if current_reversed {
&connections[current_idx].connects_to_start
} else {
&connections[current_idx].connects_to_end
};
for (next_idx, next_reversed, _dist) in next_connections {
if !visited[*next_idx] {
let mut new_path = path.clone();
new_path.push((*next_idx, *next_reversed));
let mut new_visited = visited.clone();
new_visited[*next_idx] = true;
queue.push_back((*next_idx, *next_reversed, new_path, new_visited));
}
}
}
}
println!("bfs_find_shortest_cycle: No cycle found");
None
}
/// Connected segment in the final path
#[derive(Debug, Clone)]
enum ConnectedSegment {
/// A curve segment from the original geometry
Curve {
segment: CurveSegment,
start: Point,
end: Point,
},
/// A line segment bridging a gap
Line { start: Point, end: Point },
}
/// Connect extracted segments into a closed path that contains the click point
fn connect_segments(
extracted: &[ExtractedSegment],
config: &SegmentBuilderConfig,
click_point: Point,
) -> Option<Vec<ConnectedSegment>> {
if extracted.is_empty() {
println!("connect_segments: No segments to connect");
return None;
}
println!("connect_segments: Connecting {} segments", extracted.len());
let mut connected = Vec::new();
for i in 0..extracted.len() {
let current = &extracted[i];
let next = &extracted[(i + 1) % extracted.len()];
// Get the current segment's endpoint
let current_end = current.segment.eval_at(1.0);
// Get the next segment's start point
let next_start = next.segment.eval_at(0.0);
// Add the current curve segment
connected.push(ConnectedSegment::Curve {
segment: current.segment.clone(),
start: current.segment.eval_at(0.0),
end: current_end,
});
// Check if we need to connect to the next segment
let gap = current_end.distance(next_start);
println!("connect_segments: Gap between segment {} and {} is {:.2}", i, (i + 1) % extracted.len(), gap);
if gap < 0.01 {
// Close enough, no bridge needed
continue;
} else if gap < config.gap_threshold {
// Bridge with a line segment
println!("connect_segments: Bridging gap with line segment");
connected.push(ConnectedSegment::Line {
start: current_end,
end: next_start,
});
} else {
// Try to find intersection
println!("connect_segments: Gap too large ({:.2}), trying to find intersection", gap);
let intersections = find_intersections(
&current.segment,
&next.segment,
config.intersection_threshold,
);
println!("connect_segments: Found {} intersections", intersections.len());
if !intersections.is_empty() {
// Use the first intersection to trim segments
let deduplicated = deduplicate_intersections(&intersections, 0.1);
println!("connect_segments: After deduplication: {} intersections", deduplicated.len());
if !deduplicated.is_empty() {
// TODO: Properly trim the segments at the intersection
// For now, just bridge the gap
println!("connect_segments: Bridging gap at intersection");
connected.push(ConnectedSegment::Line {
start: current_end,
end: next_start,
});
} else {
// Gap too large and no intersection - fail
println!("connect_segments: FAILED - Gap too large and no deduplicated intersections");
return None;
}
} else {
// Try bridging if within threshold
if gap < config.gap_threshold * 2.0 {
println!("connect_segments: Bridging gap (within 2x threshold)");
connected.push(ConnectedSegment::Line {
start: current_end,
end: next_start,
});
} else {
println!("connect_segments: FAILED - Gap too large ({:.2}) and no intersections", gap);
return None;
}
}
}
}
println!("connect_segments: Successfully connected all segments");
Some(connected)
}
/// Build a BezPath from connected segments
fn build_bez_path(segments: &[ConnectedSegment]) -> BezPath {
let mut path = BezPath::new();
if segments.is_empty() {
return path;
}
// Start at the first point
let start_point = match &segments[0] {
ConnectedSegment::Curve { start, .. } => *start,
ConnectedSegment::Line { start, .. } => *start,
};
path.move_to(start_point);
// Add all segments
for segment in segments {
match segment {
ConnectedSegment::Curve { segment, .. } => {
let element = segment.to_path_element();
path.push(element);
}
ConnectedSegment::Line { end, .. } => {
path.line_to(*end);
}
}
}
// Close the path
path.close_path();
path
}
#[cfg(test)]
mod tests {
use super::*;
use crate::curve_segment::CurveType;
#[test]
fn test_extract_segments_basic() {
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(100.0, 0.0), Point::new(100.0, 100.0)],
),
];
let boundary_points = vec![
BoundaryPoint {
point: Point::new(25.0, 0.0),
curve_index: 0,
t: 0.25,
nearest_point: Point::new(25.0, 0.0),
distance: 0.0,
},
BoundaryPoint {
point: Point::new(75.0, 0.0),
curve_index: 0,
t: 0.75,
nearest_point: Point::new(75.0, 0.0),
distance: 0.0,
},
BoundaryPoint {
point: Point::new(100.0, 50.0),
curve_index: 1,
t: 0.5,
nearest_point: Point::new(100.0, 50.0),
distance: 0.0,
},
];
let segments = extract_segments(&boundary_points, &curves).unwrap();
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].curve_index, 0);
assert!((segments[0].t_min - 0.25).abs() < 1e-6);
assert!((segments[0].t_max - 0.75).abs() < 1e-6);
}
#[test]
fn test_build_simple_path() {
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(100.0, 0.0), Point::new(100.0, 100.0)],
),
CurveSegment::new(
2,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(100.0, 100.0), Point::new(0.0, 100.0)],
),
CurveSegment::new(
3,
0,
CurveType::Line,
0.0,
1.0,
vec![Point::new(0.0, 100.0), Point::new(0.0, 0.0)],
),
];
let boundary_points = vec![
BoundaryPoint {
point: Point::new(50.0, 0.0),
curve_index: 0,
t: 0.5,
nearest_point: Point::new(50.0, 0.0),
distance: 0.0,
},
BoundaryPoint {
point: Point::new(100.0, 50.0),
curve_index: 1,
t: 0.5,
nearest_point: Point::new(100.0, 50.0),
distance: 0.0,
},
BoundaryPoint {
point: Point::new(50.0, 100.0),
curve_index: 2,
t: 0.5,
nearest_point: Point::new(50.0, 100.0),
distance: 0.0,
},
BoundaryPoint {
point: Point::new(0.0, 50.0),
curve_index: 3,
t: 0.5,
nearest_point: Point::new(0.0, 50.0),
distance: 0.0,
},
];
let config = SegmentBuilderConfig::default();
let click_point = Point::new(50.0, 50.0); // Center of the test square
let path = build_path_from_boundary(&boundary_points, &curves, &config, click_point);
assert!(path.is_some());
let path = path.unwrap();
// Should have a closed path
assert!(!path.elements().is_empty());
}
}

View File

@ -0,0 +1,353 @@
//! Tolerance-based quadtree for proximity detection
//!
//! This quadtree subdivides until cells reach a minimum size (tolerance),
//! enabling efficient spatial queries for curves that are within tolerance
//! distance of each other.
use crate::quadtree::BoundingBox;
use crate::shape::{Shape, ShapeColor, StrokeStyle};
use std::collections::HashSet;
use vello::kurbo::{BezPath, CubicBez, ParamCurve, Shape as KurboShape};
/// Tolerance-based quadtree for spatial proximity detection
pub struct ToleranceQuadtree {
root: QuadtreeNode,
min_cell_size: f64,
}
impl ToleranceQuadtree {
/// Create a new tolerance quadtree
///
/// # Arguments
///
/// * `bounds` - The bounding box of the entire space
/// * `min_cell_size` - Minimum cell size (tolerance) - cells won't subdivide smaller than this
pub fn new(bounds: BoundingBox, min_cell_size: f64) -> Self {
Self {
root: QuadtreeNode::new(bounds),
min_cell_size,
}
}
/// Insert a curve into the quadtree
///
/// The curve will be added to all cells it overlaps with.
pub fn insert_curve(&mut self, curve_id: usize, curve: &CubicBez) {
let bbox = BoundingBox::from_rect(curve.bounding_box());
self.root
.insert(curve_id, &bbox, curve, self.min_cell_size);
}
/// Finalize the quadtree after all curves have been inserted (Step 4)
///
/// This removes curves from all non-leaf nodes, keeping them only in minimum-size cells.
/// Call this after inserting all curves.
pub fn finalize(&mut self) {
self.root.remove_curves_from_non_leaf_nodes(self.min_cell_size);
}
/// Get all curves that share cells with the given curve
///
/// Returns a set of unique curve IDs that are spatially nearby.
pub fn get_nearby_curves(&self, curve: &CubicBez) -> HashSet<usize> {
let bbox = BoundingBox::from_rect(curve.bounding_box());
let mut nearby = HashSet::new();
self.root.query(&bbox, &mut nearby);
nearby
}
/// Get all curves in cells that overlap with the given bounding box
pub fn get_curves_in_region(&self, bbox: &BoundingBox) -> HashSet<usize> {
let mut curves = HashSet::new();
self.root.query(bbox, &mut curves);
curves
}
/// Render debug visualization of the quadtree
///
/// Returns two shapes: one for non-leaf nodes (blue) and one for leaf nodes (green).
pub fn render_debug(&self) -> (Shape, Shape) {
let mut non_leaf_path = BezPath::new();
let mut leaf_path = BezPath::new();
self.root.render_debug(&mut non_leaf_path, &mut leaf_path, 0);
let stroke_style = StrokeStyle {
width: 0.5,
..Default::default()
};
let non_leaf_shape = Shape::new(non_leaf_path).with_stroke(ShapeColor::rgb(100, 100, 255), stroke_style.clone());
let leaf_shape = Shape::new(leaf_path).with_stroke(ShapeColor::rgb(0, 200, 0), stroke_style);
(non_leaf_shape, leaf_shape)
}
}
/// A node in the tolerance quadtree
struct QuadtreeNode {
bounds: BoundingBox,
curves: Vec<(usize, BoundingBox)>, // (curve_id, bbox)
children: Option<Box<[QuadtreeNode; 4]>>,
}
impl QuadtreeNode {
fn new(bounds: BoundingBox) -> Self {
Self {
bounds,
curves: Vec::new(),
children: None,
}
}
fn is_subdividable(&self, min_size: f64) -> bool {
self.bounds.width() >= min_size * 2.0 && self.bounds.height() >= min_size * 2.0
}
fn subdivide(&mut self) {
let x_mid = (self.bounds.x_min + self.bounds.x_max) / 2.0;
let y_mid = (self.bounds.y_min + self.bounds.y_max) / 2.0;
self.children = Some(Box::new([
// Northwest (top-left)
QuadtreeNode::new(BoundingBox::new(
self.bounds.x_min,
x_mid,
self.bounds.y_min,
y_mid,
)),
// Northeast (top-right)
QuadtreeNode::new(BoundingBox::new(
x_mid,
self.bounds.x_max,
self.bounds.y_min,
y_mid,
)),
// Southwest (bottom-left)
QuadtreeNode::new(BoundingBox::new(
self.bounds.x_min,
x_mid,
y_mid,
self.bounds.y_max,
)),
// Southeast (bottom-right)
QuadtreeNode::new(BoundingBox::new(
x_mid,
self.bounds.x_max,
y_mid,
self.bounds.y_max,
)),
]));
}
fn insert(
&mut self,
curve_id: usize,
curve_bbox: &BoundingBox,
curve: &CubicBez,
min_size: f64,
) {
// Step 2: Check if curve actually intersects this cell (not just bounding box)
if !self.curve_intersects_cell(curve) {
return;
}
// Add curve to this cell
if !self.curves.iter().any(|(id, _)| *id == curve_id) {
self.curves.push((curve_id, curve_bbox.clone()));
}
// Step 3: If this cell has at least one curve AND size > tolerance, subdivide
if self.is_subdividable(min_size) && !self.curves.is_empty() && self.children.is_none() {
self.subdivide();
}
// Recursively insert into children if they exist
// Each child only gets curves that actually intersect it (checked by curve_intersects_cell)
if let Some(ref mut children) = self.children {
for child in children.iter_mut() {
child.insert(curve_id, curve_bbox, curve, min_size);
}
}
}
/// Check if a curve actually passes through this cell by sampling it
fn curve_intersects_cell(&self, curve: &CubicBez) -> bool {
// Sample the curve at multiple points to see if any fall within this cell
const SAMPLES: usize = 20;
for i in 0..=SAMPLES {
let t = i as f64 / SAMPLES as f64;
let point = curve.eval(t);
if self.bounds.contains_point(point) {
return true;
}
}
false
}
/// Remove curves from all non-minimum-size cells (Step 4)
fn remove_curves_from_non_leaf_nodes(&mut self, min_size: f64) {
// If this cell has children, clear its curves and recurse
if self.children.is_some() {
self.curves.clear();
if let Some(ref mut children) = self.children {
for child in children.iter_mut() {
child.remove_curves_from_non_leaf_nodes(min_size);
}
}
}
// If no children, this is a leaf node - keep its curves
}
fn query(&self, bbox: &BoundingBox, result: &mut HashSet<usize>) {
// If query bbox doesn't overlap this cell, skip
if !self.bounds.intersects(bbox) {
return;
}
// Add all curves in this cell
for &(curve_id, _) in &self.curves {
result.insert(curve_id);
}
// Query children
if let Some(ref children) = self.children {
for child in children.iter() {
child.query(bbox, result);
}
}
}
fn render_debug(&self, non_leaf_path: &mut BezPath, leaf_path: &mut BezPath, depth: usize) {
use vello::kurbo::PathEl;
// Choose which path to draw to based on whether this is a leaf node
let is_leaf = self.children.is_none();
// Draw cell boundary as outline only (not filled)
// Draw the four edges of the rectangle without closing the path
// Helper closure to add rectangle to the appropriate path
let add_rect = |path: &mut BezPath| {
// Top edge
path.push(PathEl::MoveTo(vello::kurbo::Point::new(
self.bounds.x_min,
self.bounds.y_min,
)));
path.push(PathEl::LineTo(vello::kurbo::Point::new(
self.bounds.x_max,
self.bounds.y_min,
)));
// Right edge
path.push(PathEl::MoveTo(vello::kurbo::Point::new(
self.bounds.x_max,
self.bounds.y_min,
)));
path.push(PathEl::LineTo(vello::kurbo::Point::new(
self.bounds.x_max,
self.bounds.y_max,
)));
// Bottom edge
path.push(PathEl::MoveTo(vello::kurbo::Point::new(
self.bounds.x_max,
self.bounds.y_max,
)));
path.push(PathEl::LineTo(vello::kurbo::Point::new(
self.bounds.x_min,
self.bounds.y_max,
)));
// Left edge
path.push(PathEl::MoveTo(vello::kurbo::Point::new(
self.bounds.x_min,
self.bounds.y_max,
)));
path.push(PathEl::LineTo(vello::kurbo::Point::new(
self.bounds.x_min,
self.bounds.y_min,
)));
};
if is_leaf {
add_rect(leaf_path);
} else {
add_rect(non_leaf_path);
}
// Recursively render children
if let Some(ref children) = self.children {
for child in children.iter() {
child.render_debug(non_leaf_path, leaf_path, depth + 1);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use vello::kurbo::Point;
#[test]
fn test_create_tolerance_quadtree() {
let bounds = BoundingBox::new(0.0, 1000.0, 0.0, 1000.0);
let tolerance = 2.0;
let quadtree = ToleranceQuadtree::new(bounds, tolerance);
assert!(quadtree.root.is_subdividable(tolerance));
}
#[test]
fn test_insert_and_query() {
let bounds = BoundingBox::new(0.0, 1000.0, 0.0, 1000.0);
let mut quadtree = ToleranceQuadtree::new(bounds, 2.0);
// Create a simple curve
let curve = CubicBez::new(
Point::new(100.0, 100.0),
Point::new(200.0, 100.0),
Point::new(200.0, 200.0),
Point::new(100.0, 200.0),
);
quadtree.insert_curve(0, &curve);
// Query with the same curve should find it
let nearby = quadtree.get_nearby_curves(&curve);
assert!(nearby.contains(&0));
}
#[test]
fn test_nearby_curves() {
let bounds = BoundingBox::new(0.0, 1000.0, 0.0, 1000.0);
let mut quadtree = ToleranceQuadtree::new(bounds, 2.0);
// Create two close curves
let curve1 = CubicBez::new(
Point::new(100.0, 100.0),
Point::new(200.0, 100.0),
Point::new(200.0, 200.0),
Point::new(100.0, 200.0),
);
let curve2 = CubicBez::new(
Point::new(150.0, 150.0),
Point::new(250.0, 150.0),
Point::new(250.0, 250.0),
Point::new(150.0, 250.0),
);
quadtree.insert_curve(0, &curve1);
quadtree.insert_curve(1, &curve2);
// Both curves should find each other
let nearby1 = quadtree.get_nearby_curves(&curve1);
assert!(nearby1.contains(&0));
assert!(nearby1.contains(&1));
let nearby2 = quadtree.get_nearby_curves(&curve2);
assert!(nearby2.contains(&0));
assert!(nearby2.contains(&1));
}
}

View File

@ -1572,6 +1572,60 @@ impl StagePane {
} }
} }
fn handle_paint_bucket_tool(
&mut self,
response: &egui::Response,
world_pos: egui::Vec2,
shared: &mut SharedPaneState,
) {
use lightningbeam_core::layer::AnyLayer;
use lightningbeam_core::shape::ShapeColor;
use lightningbeam_core::actions::PaintBucketAction;
use vello::kurbo::Point;
// Check if we have an active vector layer
let active_layer_id = match shared.active_layer_id {
Some(id) => id,
None => {
println!("Paint bucket: No active layer");
return;
}
};
let active_layer = match shared.action_executor.document().get_layer(active_layer_id) {
Some(layer) => layer,
None => {
println!("Paint bucket: Layer not found");
return;
}
};
// Only work on VectorLayer
if !matches!(active_layer, AnyLayer::Vector(_)) {
println!("Paint bucket: Not a vector layer");
return;
}
// On click: execute paint bucket fill
if response.clicked() {
let click_point = Point::new(world_pos.x as f64, world_pos.y as f64);
let fill_color = ShapeColor::from_egui(*shared.fill_color);
println!("Paint bucket clicked at ({:.1}, {:.1})", click_point.x, click_point.y);
// Create and execute paint bucket action
let action = PaintBucketAction::new(
*active_layer_id,
click_point,
fill_color,
2.0, // tolerance - could be made configurable
lightningbeam_core::gap_handling::GapHandlingMode::BridgeSegment,
);
shared.action_executor.execute(Box::new(action));
println!("Paint bucket action executed");
}
}
/// Apply transform preview to objects based on current mouse position /// Apply transform preview to objects based on current mouse position
fn apply_transform_preview( fn apply_transform_preview(
vector_layer: &mut lightningbeam_core::layer::VectorLayer, vector_layer: &mut lightningbeam_core::layer::VectorLayer,
@ -2508,6 +2562,9 @@ impl StagePane {
Tool::Transform => { Tool::Transform => {
self.handle_transform_tool(ui, &response, world_pos, shared); self.handle_transform_tool(ui, &response, world_pos, shared);
} }
Tool::PaintBucket => {
self.handle_paint_bucket_tool(&response, world_pos, shared);
}
_ => { _ => {
// Other tools not implemented yet // Other tools not implemented yet
} }