@@ -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,210 @@ pub fn merge_by_distance(
580580 }
581581}
582582
583+ pub mod extrude_algorithms {
584+ use glam:: DVec2 ;
585+ use kurbo:: { ParamCurve , ParamCurveDeriv } ;
586+ use vector_types:: subpath:: BezierHandles ;
587+ use vector_types:: vector:: StrokeId ;
588+ use vector_types:: vector:: misc:: ExtrudeJoiningAlgorithm ;
589+
590+ /// Convert [`vector_types::subpath::Bezier`] to [`kurbo::PathSeg`].
591+ fn bezier_to_path_seg ( bezier : vector_types:: subpath:: Bezier ) -> kurbo:: PathSeg {
592+ let [ start, end] = [ ( bezier. start ( ) . x , bezier. start ( ) . y ) , ( bezier. end ( ) . x , bezier. end ( ) . y ) ] ;
593+ match bezier. handles {
594+ BezierHandles :: Linear => kurbo:: Line :: new ( start, end) . into ( ) ,
595+ BezierHandles :: Quadratic { handle } => kurbo:: QuadBez :: new ( start, ( handle. x , handle. y ) , end) . into ( ) ,
596+ BezierHandles :: Cubic { handle_start, handle_end } => kurbo:: CubicBez :: new ( start, ( handle_start. x , handle_start. y ) , ( handle_end. x , handle_end. y ) , end) . into ( ) ,
597+ }
598+ }
599+
600+ /// Convert [`kurbo::CubicBez`] to [`vector_types::subpath::BezierHandles`].
601+ fn cubic_to_handles ( cubic_bez : kurbo:: CubicBez ) -> BezierHandles {
602+ BezierHandles :: Cubic {
603+ handle_start : DVec2 :: new ( cubic_bez. p1 . x , cubic_bez. p1 . y ) ,
604+ handle_end : DVec2 :: new ( cubic_bez. p2 . x , cubic_bez. p2 . y ) ,
605+ }
606+ }
607+
608+ /// Find the `t` values to split (where the tangent changes to be on the other side of the direction).
609+ fn find_splits ( cubic_segment : kurbo:: CubicBez , direction : DVec2 ) -> impl Iterator < Item = f64 > {
610+ let derivative = cubic_segment. deriv ( ) ;
611+ let convert = |x : kurbo:: Point | DVec2 :: new ( x. x , x. y ) ;
612+ let derivative_points = [ derivative. p0 , derivative. p1 , derivative. p2 ] . map ( convert) ;
613+
614+ let t_squared = derivative_points[ 0 ] - 2. * derivative_points[ 1 ] + derivative_points[ 2 ] ;
615+ let t_scalar = -2. * derivative_points[ 0 ] + 2. * derivative_points[ 1 ] ;
616+ let constant = derivative_points[ 0 ] ;
617+
618+ kurbo:: common:: solve_quadratic ( constant. perp_dot ( direction) , t_scalar. perp_dot ( direction) , t_squared. perp_dot ( direction) )
619+ . into_iter ( )
620+ . filter ( |& t| t > 1e-6 && t < 1. - 1e-6 )
621+ }
622+
623+ /// Split so segments no longer have tangents on both sides of the direction vector.
624+ fn split ( vector : & mut graphic_types:: Vector , direction : DVec2 ) {
625+ let segment_count = vector. segment_domain . ids ( ) . len ( ) ;
626+ let mut next_point = vector. point_domain . next_id ( ) ;
627+ let mut next_segment = vector. segment_domain . next_id ( ) ;
628+
629+ for segment_index in 0 ..segment_count {
630+ let ( _, _, bezier) = vector. segment_points_from_index ( segment_index) ;
631+ let mut start_index = vector. segment_domain . start_point ( ) [ segment_index] ;
632+ let pathseg = bezier_to_path_seg ( bezier) . to_cubic ( ) ;
633+ let mut start_t = 0. ;
634+
635+ for split_t in find_splits ( pathseg, direction) {
636+ let [ first, second] = [ pathseg. subsegment ( start_t..split_t) , pathseg. subsegment ( split_t..1. ) ] ;
637+ let [ first_handles, second_handles] = [ first, second] . map ( cubic_to_handles) ;
638+ let middle_point = next_point. next_id ( ) ;
639+ let start_segment = next_segment. next_id ( ) ;
640+
641+ let middle_point_index = vector. point_domain . len ( ) ;
642+ vector. point_domain . push ( middle_point, DVec2 :: new ( first. end ( ) . x , first. end ( ) . y ) ) ;
643+ vector. segment_domain . push ( start_segment, start_index, middle_point_index, first_handles, StrokeId :: ZERO ) ;
644+ vector. segment_domain . set_start_point ( segment_index, middle_point_index) ;
645+ vector. segment_domain . set_handles ( segment_index, second_handles) ;
646+
647+ start_t = split_t;
648+ start_index = middle_point_index;
649+ }
650+ }
651+ }
652+
653+ /// Copy all segments with the offset of `direction`.
654+ fn offset_copy_all_segments ( vector : & mut graphic_types:: Vector , direction : DVec2 ) {
655+ let points_count = vector. point_domain . ids ( ) . len ( ) ;
656+ let mut next_point = vector. point_domain . next_id ( ) ;
657+ for index in 0 ..points_count {
658+ vector. point_domain . push ( next_point. next_id ( ) , vector. point_domain . positions ( ) [ index] + direction) ;
659+ }
660+
661+ let segment_count = vector. segment_domain . ids ( ) . len ( ) ;
662+ let mut next_segment = vector. segment_domain . next_id ( ) ;
663+ for index in 0 ..segment_count {
664+ vector. segment_domain . push (
665+ next_segment. next_id ( ) ,
666+ vector. segment_domain . start_point ( ) [ index] + points_count,
667+ vector. segment_domain . end_point ( ) [ index] + points_count,
668+ vector. segment_domain . handles ( ) [ index] . apply_transformation ( |x| x + direction) ,
669+ vector. segment_domain . stroke ( ) [ index] ,
670+ ) ;
671+ }
672+ }
673+
674+ /// Join points from the original to the copied that are on opposite sides of the direction.
675+ fn join_extrema_edges ( vector : & mut graphic_types:: Vector , direction : DVec2 ) {
676+ #[ derive( Clone , Copy , Debug , Default , PartialEq , Eq ) ]
677+ enum Found {
678+ #[ default]
679+ None ,
680+ Positive ,
681+ Negative ,
682+ Both ,
683+ Invalid ,
684+ }
685+
686+ impl Found {
687+ fn update ( & mut self , value : f64 ) {
688+ * self = match ( * self , value > 0. ) {
689+ ( Found :: None , true ) => Found :: Positive ,
690+ ( Found :: None , false ) => Found :: Negative ,
691+ ( Found :: Positive , true ) | ( Found :: Negative , false ) => Found :: Both ,
692+ _ => Found :: Invalid ,
693+ } ;
694+ }
695+ }
696+
697+ let first_half_points = vector. point_domain . len ( ) / 2 ;
698+ let mut points = vec ! [ Found :: None ; first_half_points] ;
699+ let first_half_segments = vector. segment_domain . ids ( ) . len ( ) / 2 ;
700+
701+ for segment_id in 0 ..first_half_segments {
702+ let index = [ vector. segment_domain . start_point ( ) [ segment_id] , vector. segment_domain . end_point ( ) [ segment_id] ] ;
703+ let position = index. map ( |index| vector. point_domain . positions ( ) [ index] ) ;
704+
705+ if position[ 0 ] . abs_diff_eq ( position[ 1 ] , 1e-6 ) {
706+ continue ; // Skip zero length segments
707+ }
708+
709+ points[ index[ 0 ] ] . update ( direction. perp_dot ( position[ 1 ] - position[ 0 ] ) ) ;
710+ points[ index[ 1 ] ] . update ( direction. perp_dot ( position[ 0 ] - position[ 1 ] ) ) ;
711+ }
712+
713+ let mut next_segment = vector. segment_domain . next_id ( ) ;
714+ for ( index, & point) in points. iter ( ) . enumerate ( ) . take ( first_half_points) {
715+ if point != Found :: Both {
716+ continue ;
717+ }
718+
719+ vector
720+ . segment_domain
721+ . push ( next_segment. next_id ( ) , index, index + first_half_points, BezierHandles :: Linear , StrokeId :: ZERO ) ;
722+ }
723+ }
724+
725+ /// Join all points from the original to the copied.
726+ fn join_all ( vector : & mut graphic_types:: Vector ) {
727+ let mut next_segment = vector. segment_domain . next_id ( ) ;
728+ let first_half = vector. point_domain . len ( ) / 2 ;
729+ for index in 0 ..first_half {
730+ vector. segment_domain . push ( next_segment. next_id ( ) , index, index + first_half, BezierHandles :: Linear , StrokeId :: ZERO ) ;
731+ }
732+ }
733+
734+ pub fn extrude ( vector : & mut graphic_types:: Vector , direction : DVec2 , joining_algorithm : ExtrudeJoiningAlgorithm ) {
735+ split ( vector, direction) ;
736+ offset_copy_all_segments ( vector, direction) ;
737+
738+ match joining_algorithm {
739+ ExtrudeJoiningAlgorithm :: Extrema => join_extrema_edges ( vector, direction) ,
740+ ExtrudeJoiningAlgorithm :: All => join_all ( vector) ,
741+ ExtrudeJoiningAlgorithm :: None => { }
742+ }
743+ }
744+
745+ #[ cfg( test) ]
746+ mod extrude_tests {
747+ use glam:: DVec2 ;
748+ use kurbo:: { ParamCurve , ParamCurveDeriv } ;
749+
750+ #[ test]
751+ fn split_cubic ( ) {
752+ let l1 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , 0. ) , ( 100. , 100. ) , ( 0. , 100. ) ) ;
753+ assert_eq ! ( super :: find_splits( l1, DVec2 :: Y ) . collect:: <Vec <f64 >>( ) , vec![ 0.5 ] ) ;
754+ assert ! ( super :: find_splits( l1, DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
755+
756+ let l2 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 0. , 0. ) , ( 100. , 0. ) , ( 100. , 0. ) ) ;
757+ assert ! ( super :: find_splits( l2, DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
758+
759+ let l3 = kurbo:: PathSeg :: Line ( kurbo:: Line :: new ( ( 0. , 0. ) , ( 100. , 0. ) ) ) ;
760+ assert ! ( super :: find_splits( l3. to_cubic( ) , DVec2 :: X ) . collect:: <Vec <f64 >>( ) . is_empty( ) ) ;
761+
762+ let l4 = kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , -10. ) , ( 100. , 110. ) , ( 0. , 100. ) ) ;
763+ let splits = super :: find_splits ( l4, DVec2 :: X ) . map ( |t| l4. deriv ( ) . eval ( t) ) . collect :: < Vec < _ > > ( ) ;
764+ assert_eq ! ( splits. len( ) , 2 ) ;
765+ assert ! ( splits. iter( ) . all( |& deriv| deriv. y. abs( ) < 1e-8 ) , "{splits:?}" ) ;
766+ }
767+
768+ #[ test]
769+ fn split_vector ( ) {
770+ let curve = kurbo:: PathSeg :: Cubic ( kurbo:: CubicBez :: new ( ( 0. , 0. ) , ( 100. , -10. ) , ( 100. , 110. ) , ( 0. , 100. ) ) ) ;
771+ let mut vector = graphic_types:: Vector :: from_bezpath ( kurbo:: BezPath :: from_path_segments ( [ curve] . into_iter ( ) ) ) ;
772+ super :: split ( & mut vector, DVec2 :: X ) ;
773+ assert_eq ! ( vector. segment_ids( ) . len( ) , 3 ) ;
774+ assert_eq ! ( vector. point_domain. ids( ) . len( ) , 4 ) ;
775+ }
776+ }
777+ }
778+
779+ #[ node_macro:: node( category( "Vector: Modifier" ) , path( core_types:: vector) ) ]
780+ async fn extrude ( _: impl Ctx , mut source : Table < Vector > , direction : DVec2 , joining_algorithm : ExtrudeJoiningAlgorithm ) -> Table < Vector > {
781+ for TableRowMut { element : source, .. } in source. iter_mut ( ) {
782+ extrude_algorithms:: extrude ( source, direction, joining_algorithm) ;
783+ }
784+ source
785+ }
786+
583787#[ node_macro:: node( category( "Vector: Modifier" ) , path( core_types:: vector) ) ]
584788async fn box_warp ( _: impl Ctx , content : Table < Vector > , #[ expose] rectangle : Table < Vector > ) -> Table < Vector > {
585789 let Some ( ( target, target_transform) ) = rectangle. get ( 0 ) . map ( |rect| ( rect. element , rect. transform ) ) else {
@@ -1631,7 +1835,7 @@ async fn morph<I: IntoGraphicTable + 'n + Send + Clone>(
16311835 let target_segment_len = target_bezpath. segments ( ) . count ( ) ;
16321836 let source_segment_len = source_bezpath. segments ( ) . count ( ) ;
16331837
1634- // Insert new segments to align the number of segments in sorce_bezpath and target_bezpath.
1838+ // Insert new segments to align the number of segments in source_bezpath and target_bezpath.
16351839 make_new_segments ( & mut source_bezpath, target_segment_len. max ( source_segment_len) - source_segment_len) ;
16361840 make_new_segments ( & mut target_bezpath, source_segment_len. max ( target_segment_len) - target_segment_len) ;
16371841
@@ -1736,7 +1940,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
17361940 segments_connected_count[ point_index] += 1 ;
17371941 }
17381942
1739- // Zero out points without exactly two connectors. These are ignored
1943+ // Zero out points without exactly two connectors. These are ignored.
17401944 for count in & mut segments_connected_count {
17411945 if * count != 2 {
17421946 * count = 0 ;
@@ -1764,7 +1968,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
17641968 }
17651969 }
17661970
1767- fn calculate_distance_to_spilt ( bezier1 : PathSeg , bezier2 : PathSeg , bevel_length : f64 ) -> f64 {
1971+ fn calculate_distance_to_split ( bezier1 : PathSeg , bezier2 : PathSeg , bevel_length : f64 ) -> f64 {
17681972 if is_linear ( bezier1) && is_linear ( bezier2) {
17691973 let v1 = ( bezier1. end ( ) - bezier1. start ( ) ) . normalize ( ) ;
17701974 let v2 = ( bezier1. end ( ) - bezier2. end ( ) ) . normalize ( ) ;
@@ -1901,7 +2105,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19012105 let mut next_bezier = handles_to_segment ( next_start, * next_handles, next_end) ;
19022106 next_bezier = Affine :: new ( transform. to_cols_array ( ) ) * next_bezier;
19032107
1904- let spilt_distance = calculate_distance_to_spilt ( bezier, next_bezier, distance) ;
2108+ let calculated_split_distance = calculate_distance_to_split ( bezier, next_bezier, distance) ;
19052109
19062110 if is_linear ( bezier) {
19072111 bezier = PathSeg :: Line ( Line :: new ( bezier. start ( ) , bezier. end ( ) ) ) ;
@@ -1934,7 +2138,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19342138 let valid_length = length > 1e-10 ;
19352139 if segments_connected[ * end_point] > 0 && valid_length {
19362140 // Apply the bevel to the end
1937- let distance = spilt_distance . min ( original_length. min ( next_original_length) / 2. ) ;
2141+ let distance = calculated_split_distance . min ( original_length. min ( next_original_length) / 2. ) ;
19382142 bezier = split_distance ( bezier. reverse ( ) , distance, length) . reverse ( ) ;
19392143
19402144 if index == 0 && next_index == 1 {
@@ -1953,7 +2157,7 @@ fn bevel_algorithm(mut vector: Vector, transform: DAffine2, distance: f64) -> Ve
19532157 let valid_length = next_length > 1e-10 ;
19542158 if segments_connected[ * next_start_point] > 0 && valid_length {
19552159 // Apply the bevel to the start
1956- let distance = spilt_distance . min ( next_original_length. min ( original_length) / 2. ) ;
2160+ let distance = calculated_split_distance . min ( next_original_length. min ( original_length) / 2. ) ;
19572161 next_bezier = split_distance ( next_bezier, distance, next_length) ;
19582162 next_length = ( next_length - distance) . max ( 0. ) ;
19592163
0 commit comments