@@ -9,13 +9,13 @@ use crate::messages::tool::common_functionality::graph_modification_utils::{Node
99use crate :: messages:: tool:: common_functionality:: transformation_cage:: SelectedEdges ;
1010use crate :: messages:: tool:: tool_messages:: path_tool:: PathOverlayMode ;
1111use crate :: messages:: tool:: utility_types:: ToolType ;
12- use glam:: { DAffine2 , DVec2 } ;
12+ use glam:: { DAffine2 , DMat2 , DVec2 } ;
1313use graph_craft:: document:: value:: TaggedValue ;
1414use graphene_std:: renderer:: Quad ;
15- use graphene_std:: vector:: algorithms:: bezpath_algorithms :: pathseg_compute_lookup_table ;
16- use graphene_std:: vector:: misc:: { HandleId , ManipulatorPointId , dvec2_to_point} ;
15+ use graphene_std:: vector:: algorithms:: util :: pathseg_tangent ;
16+ use graphene_std:: vector:: misc:: { HandleId , ManipulatorPointId , dvec2_to_point, point_to_dvec2 } ;
1717use graphene_std:: vector:: { HandleExt , PointId , SegmentId , VectorGeometry , VectorModificationType } ;
18- use kurbo:: { CubicBez , DEFAULT_ACCURACY , ParamCurve , PathSeg , Point , Shape } ;
18+ use kurbo:: { CubicBez , DEFAULT_ACCURACY , ParamCurve , ParamCurveDeriv , PathSeg , Shape } ;
1919
2020/// Determines if a path should be extended. Goal in viewport space. Returns the path and if it is extending from the start, if applicable.
2121pub fn should_extend ( document : & DocumentMessageHandler , goal : DVec2 , tolerance : f64 , layers : impl Iterator < Item = LayerNodeIdentifier > ) -> Option < ( LayerNodeIdentifier , PointId , DVec2 ) > {
@@ -499,104 +499,120 @@ pub fn transforming_transform_cage(
499499 ( false , false , false )
500500}
501501
502- /// Calculates similarity metric between new bezier curve and two old beziers by using sampled points.
503- #[ allow( clippy:: too_many_arguments) ]
504- pub fn log_optimization ( a : f64 , b : f64 , p1 : DVec2 , p3 : DVec2 , d1 : DVec2 , d2 : DVec2 , points1 : & [ DVec2 ] , n : usize ) -> f64 {
505- let start_handle_length = a. exp ( ) ;
506- let end_handle_length = b. exp ( ) ;
507-
508- // Compute the handle positions of new bezier curve
509- let c1 = p1 + d1 * start_handle_length;
510- let c2 = p3 + d2 * end_handle_length;
511-
512- let new_curve = PathSeg :: Cubic ( CubicBez :: new ( Point :: new ( p1. x , p1. y ) , Point :: new ( c1. x , c1. y ) , Point :: new ( c2. x , c2. y ) , Point :: new ( p3. x , p3. y ) ) ) ;
513-
514- // Sample 2*n points from new curve and get the L2 metric between all of points
515- let points = pathseg_compute_lookup_table ( new_curve, Some ( 2 * n) , false ) ;
516-
517- let dist = points1. iter ( ) . zip ( points) . map ( |( p1, p2) | ( p1. x - p2. x ) . powi ( 2 ) + ( p1. y - p2. y ) . powi ( 2 ) ) . sum :: < f64 > ( ) ;
518-
519- dist / ( 2 * n) as f64
520- }
521-
522- /// Calculates optimal handle lengths with adam optimization.
523- #[ allow( clippy:: too_many_arguments) ]
524- pub fn find_two_param_best_approximate ( p1 : DVec2 , p3 : DVec2 , d1 : DVec2 , d2 : DVec2 , min_len1 : f64 , min_len2 : f64 , further_segment : PathSeg , other_segment : PathSeg ) -> ( DVec2 , DVec2 ) {
525- let h = 1e-6 ;
526- let tol = 1e-6 ;
527- let max_iter = 200 ;
528-
529- let mut a = ( 5_f64 ) . ln ( ) ;
530- let mut b = ( 5_f64 ) . ln ( ) ;
531-
532- let mut m_a = 0. ;
533- let mut v_a = 0. ;
534- let mut m_b = 0. ;
535- let mut v_b = 0. ;
536-
537- let initial_alpha = 0.05 ;
538- let decay_rate: f64 = 0.99 ;
539-
540- let beta1 = 0.9 ;
541- let beta2 = 0.999 ;
542- let epsilon = 1e-8 ;
543-
544- let n = 20 ;
545-
546- let further_segment = if further_segment. start ( ) . distance ( dvec2_to_point ( p1) ) >= f64:: EPSILON {
547- further_segment. reverse ( )
502+ /// Points sampled along each of the two segments that [`fit_handles_to_segment_pair`] fits.
503+ const SEGMENT_PAIR_FIT_SAMPLES : usize = 16 ;
504+ /// Passes in [`fit_handles_to_segment_pair`] that each move the samples to their closest points on the fitted cubic and refit it.
505+ const SEGMENT_PAIR_FIT_PASSES : usize = 12 ;
506+ /// How much a sample's error along the fitted cubic counts relative to its distance from it, which damps each pass.
507+ const SEGMENT_PAIR_FIT_TANGENTIAL_WEIGHT : f64 = 0.01 ;
508+
509+ /// Finds the handles (relative to their anchors) that make the cubic from `start` to `end` best match `first_segment` followed by `second_segment`.
510+ /// The handles keep the given directions. The fit minimizes the squared distance from the segments to the cubic, so a pair split from one cubic is rebuilt exactly.
511+ pub fn fit_handles_to_segment_pair ( start : DVec2 , end : DVec2 , start_direction : DVec2 , end_direction : DVec2 , first_segment : PathSeg , second_segment : PathSeg ) -> ( DVec2 , DVec2 ) {
512+ let start_direction = start_direction. try_normalize ( ) . unwrap_or_default ( ) ;
513+ let end_direction = end_direction. try_normalize ( ) . unwrap_or_default ( ) ;
514+
515+ // Orient the segments to run from `start` through the anchor they share to `end`
516+ let first_segment = if first_segment. start ( ) . distance ( dvec2_to_point ( start) ) > first_segment. end ( ) . distance ( dvec2_to_point ( start) ) {
517+ first_segment. reverse ( )
548518 } else {
549- further_segment
519+ first_segment
550520 } ;
551-
552- let other_segment = if other_segment. end ( ) . distance ( dvec2_to_point ( p3) ) >= f64:: EPSILON {
553- other_segment. reverse ( )
521+ let second_segment = if second_segment. end ( ) . distance ( dvec2_to_point ( end) ) > second_segment. start ( ) . distance ( dvec2_to_point ( end) ) {
522+ second_segment. reverse ( )
554523 } else {
555- other_segment
524+ second_segment
556525 } ;
557526
558- // Now we sample points proportional to the lengths of the beziers
559- let l1 = further_segment. perimeter ( DEFAULT_ACCURACY ) ;
560- let l2 = other_segment. perimeter ( DEFAULT_ACCURACY ) ;
561- let ratio = l1 / ( l1 + l2) ;
562- let n_points1 = ( ( 2 * n) as f64 * ratio) . floor ( ) as usize ;
563- let mut points1 = pathseg_compute_lookup_table ( further_segment, Some ( n_points1) , false ) . collect :: < Vec < _ > > ( ) ;
564- let mut points2 = pathseg_compute_lookup_table ( other_segment, Some ( n) , false ) . collect :: < Vec < _ > > ( ) ;
565- points1. append ( & mut points2) ;
566-
567- let f = |a : f64 , b : f64 | -> f64 { log_optimization ( a, b, p1, p3, d1, d2, & points1, n) } ;
568-
569- for t in 1 ..=max_iter {
570- let dfa = ( f ( a + h, b) - f ( a - h, b) ) / ( 2. * h) ;
571- let dfb = ( f ( a, b + h) - f ( a, b - h) ) / ( 2. * h) ;
572-
573- m_a = beta1 * m_a + ( 1. - beta1) * dfa;
574- m_b = beta1 * m_b + ( 1. - beta1) * dfb;
575-
576- v_a = beta2 * v_a + ( 1. - beta2) * dfa * dfa;
577- v_b = beta2 * v_b + ( 1. - beta2) * dfb * dfb;
527+ // Each segment's samples count in proportion to its share of the total length
528+ let first_length = first_segment. perimeter ( DEFAULT_ACCURACY ) ;
529+ let second_length = second_segment. perimeter ( DEFAULT_ACCURACY ) ;
530+ let first_weight = if first_length + second_length > 0. { first_length / ( first_length + second_length) } else { 1. } ;
531+
532+ // Start each sample at its parameter on the cubic the pair would have been split from, whose split is where the segments' speeds match
533+ let first_speed = pathseg_tangent ( first_segment, 1. ) . length ( ) ;
534+ let second_speed = pathseg_tangent ( second_segment, 0. ) . length ( ) ;
535+ let joint = if first_speed > 0. { first_speed / ( first_speed + second_speed) } else { first_weight } ;
536+ let mut samples = ( 1 ..=SEGMENT_PAIR_FIT_SAMPLES )
537+ . flat_map ( |i| {
538+ let t = i as f64 / SEGMENT_PAIR_FIT_SAMPLES as f64 ;
539+ [
540+ ( joint * t, point_to_dvec2 ( first_segment. eval ( t) ) , first_weight) ,
541+ ( joint + ( 1. - joint) * t, point_to_dvec2 ( second_segment. eval ( t) ) , 1. - first_weight) ,
542+ ]
543+ } )
544+ . collect :: < Vec < _ > > ( ) ;
545+
546+ // Weighted least squares for the handle lengths, which the cubic is linear in, so this solves the 2x2 normal equations directly.
547+ // Error is measured from each sample to its parameter's point on the cubic, mostly perpendicular to `fitted` there once a fit exists.
548+ let solve = |samples : & [ ( f64 , DVec2 , f64 ) ] , fitted : Option < CubicBez > | -> DVec2 {
549+ let fitted_derivative = fitted. map ( |fitted| fitted. deriv ( ) ) ;
550+ let mut system = DMat2 :: ZERO ;
551+ let mut right_side = DVec2 :: ZERO ;
552+
553+ for & ( parameter, target, weight) in samples {
554+ // Split the Bernstein form into the part fixed by the anchors and the parts that scale with each handle length
555+ let complement = 1. - parameter;
556+ let start_basis = 3. * complement * complement * parameter;
557+ let end_basis = 3. * complement * parameter * parameter;
558+ let fixed = ( complement * complement * complement + start_basis) * start + ( end_basis + parameter * parameter * parameter) * end;
559+ let jacobian = DMat2 :: from_cols ( start_basis * start_direction, end_basis * end_direction) ;
560+
561+ let metric = match fitted_derivative {
562+ Some ( fitted_derivative) => {
563+ let tangent = point_to_dvec2 ( fitted_derivative. eval ( parameter) ) . try_normalize ( ) . unwrap_or ( DVec2 :: X ) ;
564+ let normal = tangent. perp ( ) ;
565+ DMat2 :: from_cols ( normal * normal. x , normal * normal. y ) + DMat2 :: from_cols ( tangent * tangent. x , tangent * tangent. y ) * SEGMENT_PAIR_FIT_TANGENTIAL_WEIGHT
566+ }
567+ None => DMat2 :: IDENTITY ,
568+ } * weight;
578569
579- let m_a_hat = m_a / ( 1. - beta1. powi ( t) ) ;
580- let v_a_hat = v_a / ( 1. - beta2. powi ( t) ) ;
581- let m_b_hat = m_b / ( 1. - beta1. powi ( t) ) ;
582- let v_b_hat = v_b / ( 1. - beta2. powi ( t) ) ;
570+ system += jacobian. transpose ( ) * metric * jacobian;
571+ right_side += jacobian. transpose ( ) * metric * ( target - fixed) ;
572+ }
583573
584- let alpha_t = initial_alpha * decay_rate. powi ( t) ;
574+ // Use the unconstrained optimum, unless it gives a handle negative length
575+ if system. determinant ( ) > f64:: EPSILON * system. x_axis . x * system. y_axis . y {
576+ let lengths = system. inverse ( ) * right_side;
577+ if lengths. x >= 0. && lengths. y >= 0. {
578+ return lengths;
579+ }
580+ }
585581
586- // Update log-lengths
587- a -= alpha_t * m_a_hat / ( v_a_hat. sqrt ( ) + epsilon) ;
588- b -= alpha_t * m_b_hat / ( v_b_hat. sqrt ( ) + epsilon) ;
582+ // Otherwise the optimum collapses one handle and gives the other its best length from there
583+ let start_only = DVec2 :: new ( if system. x_axis . x > 0. { ( right_side. x / system. x_axis . x ) . max ( 0. ) } else { 0. } , 0. ) ;
584+ let end_only = DVec2 :: new ( 0. , if system. y_axis . y > 0. { ( right_side. y / system. y_axis . y ) . max ( 0. ) } else { 0. } ) ;
585+ let error = |lengths : DVec2 | lengths. dot ( system * lengths) - 2. * right_side. dot ( lengths) ;
586+ if error ( start_only) <= error ( end_only) { start_only } else { end_only }
587+ } ;
589588
590- // Convergence check
591- if dfa. abs ( ) < tol && dfb. abs ( ) < tol {
592- break ;
589+ let mut lengths = solve ( & samples, None ) ;
590+
591+ // Tangent distance minimization: Gauss-Newton on each sample's distance to the cubic, rather than to its parameter's point
592+ for _ in 0 ..SEGMENT_PAIR_FIT_PASSES {
593+ let fitted = CubicBez :: new (
594+ dvec2_to_point ( start) ,
595+ dvec2_to_point ( start + start_direction * lengths. x ) ,
596+ dvec2_to_point ( end + end_direction * lengths. y ) ,
597+ dvec2_to_point ( end) ,
598+ ) ;
599+ let derivative = fitted. deriv ( ) ;
600+
601+ // Move each sample's parameter toward its closest point on the cubic
602+ for ( parameter, target, _) in & mut samples {
603+ for _ in 0 ..2 {
604+ let offset = point_to_dvec2 ( fitted. eval ( * parameter) ) - * target;
605+ let tangent = point_to_dvec2 ( derivative. eval ( * parameter) ) ;
606+ if tangent. length_squared ( ) > 0. {
607+ * parameter = ( * parameter - offset. dot ( tangent) / tangent. length_squared ( ) ) . clamp ( 0. , 1. ) ;
608+ }
609+ }
593610 }
594- }
595611
596- let len1 = a . exp ( ) . max ( min_len1 ) ;
597- let len2 = b . exp ( ) . max ( min_len2 ) ;
612+ lengths = solve ( & samples , Some ( fitted ) ) ;
613+ }
598614
599- ( d1 * len1 , d2 * len2 )
615+ ( start_direction * lengths . x , end_direction * lengths . y )
600616}
601617
602618pub fn make_path_editable_is_allowed ( network_interface : & mut NodeNetworkInterface ) -> Option < LayerNodeIdentifier > {
@@ -724,6 +740,25 @@ mod tests {
724740 assert ! ( !starts. is_empty( ) , "the visible rectangles' anchors should snap" ) ;
725741 }
726742
743+ #[ test]
744+ fn fitting_a_segment_pair_rebuilds_the_cubic_it_was_split_from ( ) {
745+ let original = CubicBez :: new ( ( 0. , 0. ) , ( 0. , -400. ) , ( 600. , -400. ) , ( 600. , 0. ) ) ;
746+
747+ // An anchor inserted along the cubic and slid further along it leaves two segments that one cubic covers exactly
748+ for ( inserted, slid) in [ ( 0.1 , 0.15 ) , ( 0.3 , 0.6 ) , ( 0.5 , 0.95 ) ] {
749+ let further_segment = PathSeg :: Cubic ( original. subsegment ( 0. ..inserted) ) ;
750+ let absorbed_segment = PathSeg :: Cubic ( original. subsegment ( inserted..slid) ) ;
751+ let expected = original. subsegment ( 0. ..slid) ;
752+
753+ let start = point_to_dvec2 ( expected. p0 ) ;
754+ let end = point_to_dvec2 ( expected. p3 ) ;
755+ let ( start_handle, end_handle) = fit_handles_to_segment_pair ( start, end, point_to_dvec2 ( expected. p1 ) - start, point_to_dvec2 ( expected. p2 ) - end, further_segment, absorbed_segment) ;
756+
757+ assert ! ( ( start + start_handle) . abs_diff_eq( point_to_dvec2( expected. p1) , 1e-6 ) , "start handle for {inserted} slid to {slid}" ) ;
758+ assert ! ( ( end + end_handle) . abs_diff_eq( point_to_dvec2( expected. p2) , 1e-6 ) , "end handle for {inserted} slid to {slid}" ) ;
759+ }
760+ }
761+
727762 #[ test]
728763 fn is_almost_colinear_test ( ) {
729764 assert ! ( !is_almost_colinear( DVec2 :: ZERO , DVec2 :: X , DVec2 :: X ) ) ;
0 commit comments