@@ -21,7 +21,7 @@ use vector_types::vector::algorithms::bezpath_algorithms::{self, TValue, evaluat
2121use vector_types:: vector:: algorithms:: merge_by_distance:: MergeByDistanceExt ;
2222use vector_types:: vector:: algorithms:: offset_subpath:: offset_bezpath;
2323use vector_types:: vector:: algorithms:: spline:: { solve_spline_first_handle_closed, solve_spline_first_handle_open} ;
24- use vector_types:: vector:: misc:: { CentroidType , bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2} ;
24+ use vector_types:: vector:: misc:: { CentroidType , ExtrudeJoiningAlgorithm , bezpath_from_manipulator_groups, bezpath_to_manipulator_groups, point_to_dvec2} ;
2525use vector_types:: vector:: misc:: { MergeByDistanceAlgorithm , PointSpacingType , is_linear} ;
2626use vector_types:: vector:: misc:: { handles_to_segment, segment_to_handles} ;
2727use vector_types:: vector:: style:: { Fill , Gradient , GradientStops , Stroke } ;
@@ -580,6 +580,212 @@ pub fn merge_by_distance(
580580 }
581581}
582582
583+ pub mod extrude_algorithms {
584+ use kurbo:: { ParamCurve , ParamCurveDeriv } ;
585+
586+ /// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`]
587+ fn bezier_to_path_seg ( bezier : vector_types:: subpath:: Bezier ) -> kurbo:: PathSeg {
588+ let [ start, end] = [ ( bezier. start ( ) . x , bezier. start ( ) . y ) , ( bezier. end ( ) . x , bezier. end ( ) . y ) ] ;
589+ match bezier. handles {
590+ vector_types:: subpath:: BezierHandles :: Linear => kurbo:: Line :: new ( start, end) . into ( ) ,
591+ vector_types:: subpath:: BezierHandles :: Quadratic { handle } => kurbo:: QuadBez :: new ( start, ( handle. x , handle. y ) , end) . into ( ) ,
592+ vector_types:: 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 ( ) ,
593+ }
594+ }
595+
596+ /// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`]
597+ fn cubic_to_handles ( cubic_bez : kurbo:: CubicBez ) -> vector_types:: subpath:: BezierHandles {
598+ vector_types:: subpath:: BezierHandles :: Cubic {
599+ handle_start : glam:: DVec2 :: new ( cubic_bez. p1 . x , cubic_bez. p1 . y ) ,
600+ handle_end : glam:: DVec2 :: new ( cubic_bez. p2 . x , cubic_bez. p2 . y ) ,
601+ }
602+ }
603+
604+ /// Find the t values to split (where the tangent changes to be on the other side of the direction)
605+ fn find_splits ( cubic_segment : kurbo:: CubicBez , direction : glam:: DVec2 ) -> impl Iterator < Item = f64 > {
606+ let derivative = cubic_segment. deriv ( ) ;
607+ let convert = |x : kurbo:: Point | glam:: DVec2 :: new ( x. x , x. y ) ;
608+ let derivative_pts = [ derivative. p0 , derivative. p1 , derivative. p2 ] . map ( convert) ;
609+
610+ let t_squared = derivative_pts[ 0 ] - 2. * derivative_pts[ 1 ] + derivative_pts[ 2 ] ;
611+ let t_scalar = -2. * derivative_pts[ 0 ] + 2. * derivative_pts[ 1 ] ;
612+ let contant = derivative_pts[ 0 ] ;
613+
614+ kurbo:: common:: solve_quadratic ( contant. perp_dot ( direction) , t_scalar. perp_dot ( direction) , t_squared. perp_dot ( direction) )
615+ . into_iter ( )
616+ . filter ( |& t| t > 1e-6 && t < 1. - 1e-6 )
617+ }
618+
619+ /// Split so segements they do not have tangents on both sides of the direction vector
620+ fn split ( vector : & mut graphic_types:: Vector , direction : glam:: DVec2 ) {
621+ let segment_count = vector. segment_domain . ids ( ) . len ( ) ;
622+ let mut next_point = vector. point_domain . next_id ( ) ;
623+ let mut next_segment = vector. segment_domain . next_id ( ) ;
624+ for segment_index in 0 ..segment_count {
625+ let ( _, _, bezier) = vector. segment_points_from_index ( segment_index) ;
626+ let mut start_index = vector. segment_domain . start_point ( ) [ segment_index] ;
627+ let pathseg = bezier_to_path_seg ( bezier) . to_cubic ( ) ;
628+ let mut start_t = 0. ;
629+ for split_t in find_splits ( pathseg, direction) {
630+ let [ first, second] = [ pathseg. subsegment ( start_t..split_t) , pathseg. subsegment ( split_t..1. ) ] ;
631+ let [ first_handles, second_handles] = [ first, second] . map ( cubic_to_handles) ;
632+ let middle_point = next_point. next_id ( ) ;
633+ let start_segment = next_segment. next_id ( ) ;
634+
635+ let middle_point_index = vector. point_domain . len ( ) ;
636+ vector. point_domain . push ( middle_point, glam:: DVec2 :: new ( first. end ( ) . x , first. end ( ) . y ) ) ;
637+ vector
638+ . segment_domain
639+ . push ( start_segment, start_index, middle_point_index, first_handles, vector_types:: vector:: StrokeId :: ZERO ) ;
640+ vector. segment_domain . set_start_point ( segment_index, middle_point_index) ;
641+ vector. segment_domain . set_handles ( segment_index, second_handles) ;
642+
643+ start_t = split_t;
644+ start_index = middle_point_index;
645+ }
646+ }
647+ }
648+
649+ /// Copy all segements with the offset of `direction`
650+ fn offset_copy_all_segments ( vector : & mut graphic_types:: Vector , direction : glam:: DVec2 ) {
651+ let points_count = vector. point_domain . ids ( ) . len ( ) ;
652+ let mut next_point = vector. point_domain . next_id ( ) ;
653+ for index in 0 ..points_count {
654+ vector. point_domain . push ( next_point. next_id ( ) , vector. point_domain . positions ( ) [ index] + direction) ;
655+ }
656+
657+ let segment_count = vector. segment_domain . ids ( ) . len ( ) ;
658+ let mut next_segment = vector. segment_domain . next_id ( ) ;
659+ for index in 0 ..segment_count {
660+ vector. segment_domain . push (
661+ next_segment. next_id ( ) ,
662+ vector. segment_domain . start_point ( ) [ index] + points_count,
663+ vector. segment_domain . end_point ( ) [ index] + points_count,
664+ vector. segment_domain . handles ( ) [ index] . apply_transformation ( |x| x + direction) ,
665+ vector. segment_domain . stroke ( ) [ index] ,
666+ ) ;
667+ }
668+ }
669+
670+ /// Join points from the original to the copied that are on alternate sides of the direction
671+ fn join_extrema_edges ( vector : & mut graphic_types:: Vector , direction : glam:: DVec2 ) {
672+ #[ derive( Clone , Copy , Debug , Default , PartialEq , Eq ) ]
673+ enum Found {
674+ #[ default]
675+ None ,
676+ Positive ,
677+ Negative ,
678+ Both ,
679+ Invalid ,
680+ }
681+ impl Found {
682+ fn update ( & mut self , value : f64 ) {
683+ * self = match ( * self , value > 0. ) {
684+ ( Found :: None , true ) => Found :: Positive ,
685+ ( Found :: None , false ) => Found :: Negative ,
686+ ( Found :: Positive , true ) | ( Found :: Negative , false ) => Found :: Both ,
687+ _ => Found :: Invalid ,
688+ } ;
689+ }
690+ }
691+ let first_half_points = vector. point_domain . len ( ) / 2 ;
692+ let mut points = vec ! [ Found :: None ; first_half_points] ;
693+ let first_half_segments = vector. segment_domain . ids ( ) . len ( ) / 2 ;
694+ for segment_id in 0 ..first_half_segments {
695+ let index = [ vector. segment_domain . start_point ( ) [ segment_id] , vector. segment_domain . end_point ( ) [ segment_id] ] ;
696+ let position = index. map ( |index| vector. point_domain . positions ( ) [ index] ) ;
697+
698+ if position[ 0 ] . abs_diff_eq ( position[ 1 ] , 1e-6 ) {
699+ continue ; // Skip zero length segments
700+ }
701+
702+ points[ index[ 0 ] ] . update ( direction. perp_dot ( position[ 1 ] - position[ 0 ] ) ) ;
703+ points[ index[ 1 ] ] . update ( direction. perp_dot ( position[ 0 ] - position[ 1 ] ) ) ;
704+ }
705+
706+ let mut next_segment = vector. segment_domain . next_id ( ) ;
707+ for index in 0 ..first_half_points {
708+ if points[ index] != Found :: Both {
709+ continue ;
710+ }
711+ vector. segment_domain . push (
712+ next_segment. next_id ( ) ,
713+ index,
714+ index + first_half_points,
715+ vector_types:: subpath:: BezierHandles :: Linear ,
716+ vector_types:: vector:: StrokeId :: ZERO ,
717+ ) ;
718+ }
719+ }
720+
721+ /// Join all points from the original to the copied
722+ fn join_all ( vector : & mut graphic_types:: Vector ) {
723+ let mut next_segment = vector. segment_domain . next_id ( ) ;
724+ let first_half = vector. point_domain . len ( ) / 2 ;
725+ for index in 0 ..first_half {
726+ vector. segment_domain . push (
727+ next_segment. next_id ( ) ,
728+ index,
729+ index + first_half,
730+ vector_types:: subpath:: BezierHandles :: Linear ,
731+ vector_types:: vector:: StrokeId :: ZERO ,
732+ ) ;
733+ }
734+ }
735+
736+ pub fn extrude ( vector : & mut graphic_types:: Vector , direction : glam:: DVec2 , joining_algorithm : vector_types:: vector:: misc:: ExtrudeJoiningAlgorithm ) {
737+ split ( vector, direction) ;
738+ offset_copy_all_segments ( vector, direction) ;
739+ match joining_algorithm {
740+ vector_types:: vector:: misc:: ExtrudeJoiningAlgorithm :: Extrema => join_extrema_edges ( vector, direction) ,
741+ vector_types:: vector:: misc:: ExtrudeJoiningAlgorithm :: All => join_all ( vector) ,
742+ vector_types:: vector:: misc:: ExtrudeJoiningAlgorithm :: None => { }
743+ }
744+ }
745+
746+ #[ cfg( test) ]
747+ mod extrude_tests {
748+ use glam:: DVec2 ;
749+ use kurbo:: { ParamCurve , ParamCurveDeriv } ;
750+
751+ #[ test]
752+ fn split_cubic ( ) {
753+ let l1 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , 0. ) , ( 100. , 100. ) , ( 0. , 100. ) ) ;
754+ assert_eq ! ( super :: find_splits( l1, DVec2 :: Y ) . collect:: <Vec <f64 >>( ) , vec![ 0.5 ] ) ;
755+ assert ! ( super :: find_splits( l1, DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
756+
757+ let l2 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 0. , 0. ) , ( 100. , 0. ) , ( 100. , 0. ) ) ;
758+ assert ! ( super :: find_splits( l2, DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
759+
760+ let l3 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 100. , 0. ) ) ) ;
761+ assert ! ( super :: find_splits( l3. to_cubic( ) , DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
762+
763+ let l4 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , -10. ) , ( 100. , 110. ) , ( 0. , 100. ) ) ;
764+ let splits = super :: find_splits ( l4, DVec2 :: X ) . map ( |t| l4. deriv ( ) . eval ( t) ) . collect :: < Vec < _ > > ( ) ;
765+ assert_eq ! ( splits. len( ) , 2 ) ;
766+ assert ! ( splits. iter( ) . all( |& deriv| deriv. y. abs( ) < 1e-8 ) , "{splits:?}" ) ;
767+ }
768+
769+ #[ test]
770+ fn split_vector ( ) {
771+ let curve = kurbo:: PathSeg :: Cubic ( kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , -10. ) , ( 100. , 110. ) , ( 0. , 100. ) ) ) ;
772+ let mut vector = graphic_types:: Vector :: from_bezpath ( kurbo:: BezPath :: from_path_segments ( [ curve] . into_iter ( ) ) ) ;
773+ super :: split ( & mut vector, DVec2 :: X ) ;
774+ assert_eq ! ( vector. segment_ids( ) . len( ) , 3 ) ;
775+ assert_eq ! ( vector. point_domain. ids( ) . len( ) , 4 ) ;
776+ }
777+ }
778+ }
779+
780+ /// Attempt to inscribe circles at the anchors (that have exactly two segments connected).
781+ #[ node_macro:: node( category( "Vector: Modifier" ) , path( core_types:: vector) ) ]
782+ async fn extrude ( _: impl Ctx , mut source : Table < Vector > , direction : DVec2 , joining_algorithm : ExtrudeJoiningAlgorithm ) -> Table < Vector > {
783+ for TableRowMut { element : source, .. } in source. iter_mut ( ) {
784+ extrude_algorithms:: extrude ( source, direction, joining_algorithm) ;
785+ }
786+ source
787+ }
788+
583789#[ node_macro:: node( category( "Vector: Modifier" ) , path( core_types:: vector) ) ]
584790async fn box_warp ( _: impl Ctx , content : Table < Vector > , #[ expose] rectangle : Table < Vector > ) -> Table < Vector > {
585791 let Some ( ( target, target_transform) ) = rectangle. get ( 0 ) . map ( |rect| ( rect. element , rect. transform ) ) else {
0 commit comments