@@ -3,7 +3,7 @@ use crate::consts::HIDE_HANDLE_DISTANCE;
3
3
use crate :: messages:: portfolio:: document:: utility_types:: document_metadata:: LayerNodeIdentifier ;
4
4
use crate :: messages:: portfolio:: document:: utility_types:: misc:: * ;
5
5
use crate :: messages:: prelude:: * ;
6
- use glam:: { DAffine2 , DVec2 } ;
6
+ use glam:: { DAffine2 , DVec2 , FloatExt } ;
7
7
use graphene_std:: math:: math_ext:: QuadExt ;
8
8
use graphene_std:: renderer:: Quad ;
9
9
use graphene_std:: subpath:: pathseg_points;
@@ -13,7 +13,7 @@ use graphene_std::vector::algorithms::bezpath_algorithms::{pathseg_normals_to_po
13
13
use graphene_std:: vector:: algorithms:: intersection:: filtered_segment_intersections;
14
14
use graphene_std:: vector:: misc:: dvec2_to_point;
15
15
use graphene_std:: vector:: misc:: point_to_dvec2;
16
- use kurbo:: { Affine , DEFAULT_ACCURACY , Nearest , ParamCurve , ParamCurveNearest , PathSeg } ;
16
+ use kurbo:: { Affine , ParamCurve , PathSeg } ;
17
17
18
18
#[ derive( Clone , Debug , Default ) ]
19
19
pub struct LayerSnapper {
@@ -107,9 +107,11 @@ impl LayerSnapper {
107
107
if path. document_curve . start ( ) . distance_squared ( path. document_curve . end ( ) ) < tolerance * tolerance * 2. {
108
108
continue ;
109
109
}
110
- let Nearest { distance_sq, t } = path. document_curve . nearest ( dvec2_to_point ( point. document_point ) , DEFAULT_ACCURACY ) ;
111
- let snapped_point_document = point_to_dvec2 ( path. document_curve . eval ( t) ) ;
112
- let distance = distance_sq. sqrt ( ) ;
110
+ let Some ( ( distance_squared, closest) ) = path. approx_nearest_point ( point. document_point , 10 ) else {
111
+ continue ;
112
+ } ;
113
+ let snapped_point_document = point_to_dvec2 ( closest) ;
114
+ let distance = distance_squared. sqrt ( ) ;
113
115
114
116
if distance < tolerance {
115
117
snap_results. curves . push ( SnappedCurve {
@@ -322,6 +324,99 @@ struct SnapCandidatePath {
322
324
bounds : Option < Quad > ,
323
325
}
324
326
327
+ impl SnapCandidatePath {
328
+ /// Calculates the point on the curve which lies closest to `point`.
329
+ ///
330
+ /// ## Algorithm:
331
+ /// 1. We first perform a coarse scan of the path segment to find the most promising starting point.
332
+ /// 2. Afterwards we refine this point by performing a binary search to either side assuming that the segment contains at most one extremal point.
333
+ /// 3. The smaller of the two resulting distances is returned.
334
+ ///
335
+ /// ## Visualization:
336
+ /// ```text
337
+ /// Query Point (×)
338
+ /// ×
339
+ /// /|\
340
+ /// / | \ distance checks
341
+ /// / | \
342
+ /// v v v
343
+ /// ●---●---●---●---● <- Curve with coarse scan points
344
+ /// 0 0.25 0.5 0.75 1 (parameter t values)
345
+ /// ^ ^
346
+ /// | | |
347
+ /// min mid max
348
+ /// Find closest scan point
349
+ ///
350
+ /// Refine left region using binary search:
351
+ ///
352
+ /// ●------●------●
353
+ /// 0.25 0.375 0.5
354
+ ///
355
+ /// Result: | (=0.4)
356
+ /// And the right region:
357
+ ///
358
+ /// ●------●------●
359
+ /// 0.5 0.625 0.75
360
+ /// Result: | (=0.5)
361
+ ///
362
+ /// The t value with minimal dist is thus 0.4
363
+ /// Return: (dist_closest, point_on_curve)
364
+ /// ```
365
+ pub fn approx_nearest_point ( & self , point : DVec2 , lut_steps : usize ) -> Option < ( f64 , kurbo:: Point ) > {
366
+ let point = dvec2_to_point ( point) ;
367
+
368
+ let time_values = ( 0 ..lut_steps) . map ( |x| x as f64 / lut_steps as f64 ) ;
369
+ let points = time_values. map ( |t| ( t, self . document_curve . eval ( t) ) ) ;
370
+ let points_with_distances = points. map ( |( t, p) | ( t, p. distance_squared ( point) , p) ) ;
371
+ let ( t, _, _) = points_with_distances. min_by ( |( _, a, _) , ( _, b, _) | a. partial_cmp ( b) . unwrap_or ( std:: cmp:: Ordering :: Equal ) ) ?;
372
+
373
+ let min_t = ( t - ( lut_steps as f64 ) . recip ( ) ) . max ( 0. ) ;
374
+ let max_t = ( t + ( lut_steps as f64 ) . recip ( ) ) . min ( 1. ) ;
375
+ let left = self . refine_nearest_point ( point, min_t, t) ;
376
+ let right = self . refine_nearest_point ( point, t, max_t) ;
377
+
378
+ if left. 0 < right. 0 { Some ( left) } else { Some ( right) }
379
+ }
380
+
381
+ /// Refines the nearest point search within a given parameter range using binary search.
382
+ ///
383
+ /// This method performs iterative refinement by:
384
+ /// 1. Evaluating the midpoint of the current parameter range
385
+ /// 2. Comparing distances at the endpoints and midpoint
386
+ /// 3. Narrowing the search range to the side with the shorter distance
387
+ /// 4. Continuing until convergence (when the range becomes very small)
388
+ ///
389
+ /// Returns a tuple of (parameter_t, closest_point) where parameter_t is in the range [min_t, max_t].
390
+ fn refine_nearest_point ( & self , point : kurbo:: Point , mut min_t : f64 , mut max_t : f64 ) -> ( f64 , kurbo:: Point ) {
391
+ let mut min_dist = self . document_curve . eval ( min_t) . distance_squared ( point) ;
392
+ let mut max_dist = self . document_curve . eval ( max_t) . distance_squared ( point) ;
393
+ let mut mid_t = max_t. lerp ( min_t, 0.5 ) ;
394
+ let mut mid_point = self . document_curve . eval ( mid_t) ;
395
+ let mut mid_dist = mid_point. distance_squared ( point) ;
396
+
397
+ for _ in 0 ..10 {
398
+ if ( min_dist - max_dist) . abs ( ) < 1e-3 {
399
+ return ( mid_dist, mid_point) ;
400
+ }
401
+ if mid_dist > min_dist && mid_dist > max_dist {
402
+ return ( mid_dist, mid_point) ;
403
+ }
404
+ if max_dist > min_dist {
405
+ max_t = mid_t;
406
+ max_dist = mid_dist;
407
+ } else {
408
+ min_t = mid_t;
409
+ min_dist = mid_dist;
410
+ }
411
+ mid_t = max_t. lerp ( min_t, 0.5 ) ;
412
+ mid_point = self . document_curve . eval ( mid_t) ;
413
+ mid_dist = mid_point. distance_squared ( point) ;
414
+ }
415
+
416
+ ( mid_dist, mid_point)
417
+ }
418
+ }
419
+
325
420
#[ derive( Clone , Debug , Default ) ]
326
421
pub struct SnapCandidatePoint {
327
422
pub document_point : DVec2 ,
0 commit comments