@@ -674,3 +674,213 @@ mod tests {
674674 assert ! ( bezpath_is_inside_bezpath( & line_inside, & boundary_polygon, None , None ) ) ;
675675 }
676676}
677+
678+ pub mod inscribe_circles_algorithms {
679+ use core:: ops:: Range ;
680+ use kurbo:: { ParamCurve , ParamCurveDeriv , ParamCurveExtrema } ;
681+
682+ const ROUND_ACCURACY : f64 = 1e-5 ;
683+
684+ #[ derive( Clone , Copy , Debug , PartialEq ) ]
685+ pub struct CircleInscription {
686+ pub time_1 : f64 ,
687+ pub time_2 : f64 ,
688+ pub theta : f64 ,
689+ pub circle_centre1 : glam:: DVec2 ,
690+ pub circle_centre2 : glam:: DVec2 ,
691+ }
692+
693+ /// Find the normalised tangent at a particular time. Avoid using for t=0 or t=1 due to errors.
694+ fn tangent ( segment : kurbo:: PathSeg , t : f64 ) -> kurbo:: Vec2 {
695+ let tangent = match segment {
696+ kurbo:: PathSeg :: Line ( line) => line. deriv ( ) . eval ( t) ,
697+ kurbo:: PathSeg :: Quad ( quad_bez) => quad_bez. deriv ( ) . eval ( t) ,
698+ kurbo:: PathSeg :: Cubic ( cubic_bez) => cubic_bez. deriv ( ) . eval ( t) ,
699+ }
700+ . to_vec2 ( )
701+ . normalize ( ) ;
702+ debug_assert ! ( tangent. is_finite( ) , "cannot round corner with NaN tangent" ) ;
703+ tangent
704+ }
705+
706+ /// Rotate 90 degrees in one direction
707+ fn offset_1 ( value : kurbo:: Vec2 , radius : f64 ) -> kurbo:: Vec2 {
708+ kurbo:: Vec2 :: new ( -value. y , value. x ) * radius
709+ }
710+
711+ /// Rotate 90 degrees in one direction
712+ fn offset_2 ( value : kurbo:: Vec2 , radius : f64 ) -> kurbo:: Vec2 {
713+ kurbo:: Vec2 :: new ( value. y , -value. x ) * radius
714+ }
715+
716+ /// Compute the tangent at t=0 for the path segment
717+ pub fn tangent_at_start ( segment : kurbo:: PathSeg ) -> kurbo:: Vec2 {
718+ let tangent = match segment {
719+ kurbo:: PathSeg :: Line ( line) => ( line. p1 - line. p0 ) . normalize ( ) ,
720+ kurbo:: PathSeg :: Quad ( quad_bez) => {
721+ let first = ( quad_bez. p1 - quad_bez. p0 ) . normalize ( ) ;
722+ if first. is_finite ( ) { first } else { ( quad_bez. p2 - quad_bez. p0 ) . normalize ( ) }
723+ }
724+ kurbo:: PathSeg :: Cubic ( cubic_bez) => {
725+ let first = ( cubic_bez. p1 - cubic_bez. p0 ) . normalize ( ) ;
726+ if first. is_finite ( ) {
727+ first
728+ } else {
729+ let second = ( cubic_bez. p2 - cubic_bez. p0 ) . normalize ( ) ;
730+ if second. is_finite ( ) { second } else { ( cubic_bez. p3 - cubic_bez. p0 ) . normalize ( ) }
731+ }
732+ }
733+ } ;
734+ debug_assert ! ( tangent. is_finite( ) , "cannot round corner with NaN tangent {segment:?}" ) ;
735+ tangent
736+ }
737+
738+ /// Resolve the bounding boxes offset by radius in either direciton.
739+ fn offset_bounding_boxes ( segment : kurbo:: PathSeg , radius : f64 ) -> [ kurbo:: Rect ; 2 ] {
740+ let [ start_tangent, end_tangent] = [ tangent_at_start ( segment) , -tangent_at_start ( segment. reverse ( ) ) ] ;
741+
742+ let mut bbox1 = kurbo:: Rect :: from_points ( segment. start ( ) + offset_1 ( start_tangent, radius) , segment. end ( ) + offset_1 ( end_tangent, radius) ) ;
743+ let mut bbox2 = kurbo:: Rect :: from_points ( segment. start ( ) + offset_2 ( start_tangent, radius) , segment. end ( ) + offset_2 ( end_tangent, radius) ) ;
744+ // The extrema for the original curve should be the same as for the offset curve
745+ for extremum in segment. extrema ( ) {
746+ let value = segment. eval ( extremum) ;
747+ let derivative = tangent ( segment, extremum) ;
748+ bbox1 = bbox1. union_pt ( value + offset_1 ( derivative, radius) ) ;
749+ bbox2 = bbox2. union_pt ( value + offset_2 ( derivative, radius) ) ;
750+ }
751+ debug_assert ! ( bbox1. is_finite( ) && bbox2. is_finite( ) , "a wild NaN appeared :(" ) ;
752+ [ bbox1, bbox2]
753+ }
754+
755+ /// If the width and height both smaller than accuracy then we can end the recursion
756+ fn rect_within_accuracy ( rect : kurbo:: Rect , accuracy : f64 ) -> bool {
757+ rect. width ( ) . abs ( ) < accuracy && rect. height ( ) . abs ( ) < accuracy
758+ }
759+
760+ /// Resursively find position to inscribe circles
761+ fn inscribe_internal ( segment1 : kurbo:: PathSeg , t1 : Range < f64 > , segment2 : kurbo:: PathSeg , t2 : Range < f64 > , radius : f64 ) -> Option < CircleInscription > {
762+ let bbox1 = offset_bounding_boxes ( segment1. subsegment ( t1. clone ( ) ) , radius) ;
763+ let bbox2 = offset_bounding_boxes ( segment2. subsegment ( t2. clone ( ) ) , radius) ;
764+ let mid_t1 = ( t1. start + t1. end ) / 2. ;
765+ let mid_t2 = ( t2. start + t2. end ) / 2. ;
766+
767+ // Check if the bounding boxes overlap
768+ let mut any_overlap = false ;
769+ for i in 0 ..4usize {
770+ let [ index_1, index_2] = [ i >> 1 , i & 1 ] ;
771+ let [ first, second] = [ bbox1[ index_1] , bbox2[ index_2] ] ;
772+
773+ // Ignore non overlapping
774+ if !first. overlaps ( second) {
775+ continue ;
776+ }
777+ // If the rects are small enough then complete the recursion
778+ if rect_within_accuracy ( first, ROUND_ACCURACY ) && rect_within_accuracy ( second, ROUND_ACCURACY ) {
779+ let tangents = [ ( segment1, mid_t1) , ( segment2, mid_t2) ] . map ( |( segment, t) | tangent ( segment, t) ) ;
780+ let normal_1 = [ offset_1, offset_2] [ index_1] ( tangents[ 0 ] , 1. ) ;
781+ let normal_2 = [ offset_1, offset_2] [ index_2] ( tangents[ 1 ] , 1. ) ;
782+ let circle_centre_1 = segment1. eval ( mid_t1) + normal_1 * radius;
783+ let circle_centre_2 = segment2. eval ( mid_t2) + normal_2 * radius;
784+ return Some ( CircleInscription {
785+ time_1 : mid_t1,
786+ time_2 : mid_t2,
787+ theta : normal_1. dot ( normal_2) . clamp ( -1. , 1. ) . acos ( ) ,
788+ circle_centre1 : glam:: DVec2 :: new ( circle_centre_1. x , circle_centre_1. y ) ,
789+ circle_centre2 : glam:: DVec2 :: new ( circle_centre_2. x , circle_centre_2. y ) ,
790+ } ) ;
791+ }
792+ any_overlap = true ;
793+ }
794+ if !any_overlap {
795+ return None ;
796+ }
797+
798+ let [ start_t1, end_t1] = [ t1. start , t1. end ] ;
799+ let [ start_t2, end_t2] = [ t2. start , t2. end ] ;
800+
801+ // Repeat checking the intersection with the combinations of the two halves of each curve
802+ if let Some ( result) = None
803+ . or_else ( || inscribe_internal ( segment1, start_t1..mid_t1, segment2, start_t2..mid_t2, radius) )
804+ . or_else ( || inscribe_internal ( segment1, start_t1..mid_t1, segment2, mid_t2..end_t2, radius) )
805+ . or_else ( || inscribe_internal ( segment1, mid_t1..end_t1, segment2, start_t2..mid_t2, radius) )
806+ . or_else ( || inscribe_internal ( segment1, mid_t1..end_t1, segment2, mid_t2..end_t2, radius) )
807+ {
808+ return Some ( result) ;
809+ }
810+ None
811+ }
812+
813+ /// Convert [`crate::subpath::Bezier`] to [`kurbo::PathSeg`]
814+ pub fn bezier_to_path_seg ( bezier : crate :: subpath:: Bezier ) -> kurbo:: PathSeg {
815+ let [ start, end] = [ ( bezier. start ( ) . x , bezier. start ( ) . y ) , ( bezier. end ( ) . x , bezier. end ( ) . y ) ] ;
816+ match bezier. handles {
817+ crate :: subpath:: BezierHandles :: Linear => kurbo:: Line :: new ( start, end) . into ( ) ,
818+ crate :: subpath:: BezierHandles :: Quadratic { handle } => kurbo:: QuadBez :: new ( start, ( handle. x , handle. y ) , end) . into ( ) ,
819+ crate :: subpath:: BezierHandles :: Cubic { handle_start, handle_end } => kurbo:: CubicBez :: new ( start, ( handle_start. x , handle_start. y ) , ( handle_end. x , handle_end. y ) , end) . into ( ) ,
820+ }
821+ }
822+
823+ /// Convert [`kurbo::PathSeg`] to [`crate::subpath::BezierHandles`]
824+ pub fn path_seg_to_handles ( segment : kurbo:: PathSeg ) -> crate :: subpath:: BezierHandles {
825+ match segment {
826+ kurbo:: PathSeg :: Line ( _line) => crate :: subpath:: BezierHandles :: Linear ,
827+ kurbo:: PathSeg :: Quad ( quad_bez) => crate :: subpath:: BezierHandles :: Quadratic {
828+ handle : glam:: DVec2 :: new ( quad_bez. p1 . x , quad_bez. p1 . y ) ,
829+ } ,
830+ kurbo:: PathSeg :: Cubic ( cubic_bez) => crate :: subpath:: BezierHandles :: Cubic {
831+ handle_start : glam:: DVec2 :: new ( cubic_bez. p1 . x , cubic_bez. p1 . y ) ,
832+ handle_end : glam:: DVec2 :: new ( cubic_bez. p2 . x , cubic_bez. p2 . y ) ,
833+ } ,
834+ }
835+ }
836+
837+ /// Attemt to inscribe circle into the start of the [`kurbo::PathSeg`]s
838+ pub fn inscribe ( first : kurbo:: PathSeg , second : kurbo:: PathSeg , radius : f64 ) -> Option < CircleInscription > {
839+ inscribe_internal ( first, 0.0 ..1. , second, 0.0 ..1. , radius)
840+ }
841+
842+ #[ cfg( test) ]
843+ mod inscribe_tests {
844+ #[ test]
845+ fn test_perpendicular_lines ( ) {
846+ let l1 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 100. , 0. ) ) ) ;
847+ let l2 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 0. , 100. ) ) ) ;
848+
849+ let result = super :: inscribe ( l1, l2, 5. ) ;
850+ assert ! ( result. unwrap( ) . circle_centre1. abs_diff_eq( glam:: DVec2 :: new( 5. , 5. ) , super :: ROUND_ACCURACY * 10. ) , "{result:?}" ) ;
851+ assert_eq ! ( result. unwrap( ) . theta, std:: f64 :: consts:: FRAC_PI_2 , "unexpected {result:?}" ) ;
852+ }
853+
854+ #[ test]
855+ fn test_skew_lines ( ) {
856+ let l1 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 100. , 100. ) ) ) ;
857+ let l2 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 0. , 100. ) ) ) ;
858+
859+ let result = super :: inscribe ( l1, l2, 5. ) ;
860+ let expected_centre = glam:: DVec2 :: new ( 5. , 5. + 5. * std:: f64:: consts:: SQRT_2 ) ;
861+ assert ! ( result. unwrap( ) . circle_centre1. abs_diff_eq( expected_centre, super :: ROUND_ACCURACY * 10. ) , "unexpected {result:?}" ) ;
862+ assert_eq ! ( result. unwrap( ) . theta, std:: f64 :: consts:: FRAC_PI_4 * 3. , "unexpected {result:?}" ) ;
863+ }
864+
865+ #[ test]
866+ fn test_skew_lines2 ( ) {
867+ let l1 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 30. , 40. ) ) ) ;
868+ let l2 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 40. , 30. ) ) ) ;
869+
870+ let result = super :: inscribe ( l1, l2, 5. ) ;
871+ let expected_centre = glam:: DVec2 :: new ( 25. , 25. ) ;
872+ assert ! ( result. unwrap( ) . circle_centre1. abs_diff_eq( expected_centre, super :: ROUND_ACCURACY * 10. ) , "{result:?}" ) ;
873+ assert_eq ! ( result. unwrap( ) . theta, ( -24f64 / 25. ) . acos( ) , "{result:?}" ) ;
874+ }
875+
876+ #[ test]
877+ fn test_perpendicular_cubic ( ) {
878+ let l1 = kurbo:: PathSeg :: Cubic ( kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 0. , 0. ) , ( 100. , 0. ) , ( 100. , 0. ) ) ) ;
879+ let l2 = kurbo:: PathSeg :: Cubic ( kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 0. , 33. ) , ( 0. , 67. ) , ( 0. , 100. ) ) ) ;
880+
881+ let result = super :: inscribe ( l1, l2, 5. ) ;
882+ assert ! ( result. unwrap( ) . circle_centre1. abs_diff_eq( glam:: DVec2 :: new( 5. , 5. ) , super :: ROUND_ACCURACY * 10. ) , "{result:?}" ) ;
883+ assert_eq ! ( result. unwrap( ) . theta, std:: f64 :: consts:: FRAC_PI_2 , "unexpected {result:?}" ) ;
884+ }
885+ }
886+ }
0 commit comments