Paint bucket
This commit is contained in:
parent
9204308033
commit
e1d9514472
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
¤t.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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue