@@ -13,7 +13,7 @@ use graphene_std::raster_types::Image;
1313use graphene_std:: subpath:: Subpath ;
1414use graphene_std:: table:: Table ;
1515use graphene_std:: text:: { Font , TypesettingConfig } ;
16- use graphene_std:: vector:: style:: { Fill , Stroke } ;
16+ use graphene_std:: vector:: style:: { Fill , GradientSpreadMethod , GradientType , Stroke } ;
1717use graphene_std:: vector:: { GradientStops , PointId , Vector , VectorModification , VectorModificationType } ;
1818use graphene_std:: { Color , Graphic , NodeInputDecleration } ;
1919
@@ -460,13 +460,98 @@ impl<'a> ModifyInputsContext<'a> {
460460 self . set_input_with_refresh ( input_connector, NodeInput :: value ( TaggedValue :: F64 ( fill * 100. ) , false ) , false ) ;
461461 }
462462
463- pub fn gradient_table_set ( & mut self , gradient_table : Table < GradientStops > ) {
463+ /// Set the stops table on the 'Gradient Value' node, creating it if necessary.
464+ pub fn gradient_stops_set ( & mut self , stops : GradientStops ) {
464465 let Some ( gradient_node_id) = self . existing_proto_node_id ( graphene_std:: math_nodes:: gradient_value:: IDENTIFIER , true ) else {
465466 return ;
466467 } ;
467-
468468 let input_connector = InputConnector :: node ( gradient_node_id, graphene_std:: math_nodes:: gradient_value:: GradientInput :: INDEX ) ;
469- self . set_input_with_refresh ( input_connector, NodeInput :: value ( TaggedValue :: GradientTable ( gradient_table) , false ) , false ) ;
469+ let stops_table = Table :: new_from_element ( stops) ;
470+ self . set_input_with_refresh ( input_connector, NodeInput :: value ( TaggedValue :: GradientTable ( stops_table) , false ) , false ) ;
471+ }
472+
473+ /// Update the gradient line so its endpoints are at `new_start` and `new_end`.
474+ /// With multiple `Transform` nodes the last one (closest to the layer) is modified so the chain still composes to the target.
475+ /// With none, one is inserted unless the target is the identity.
476+ pub fn gradient_line_set ( & mut self , new_start : DVec2 , new_end : DVec2 ) {
477+ let Some ( output_layer) = self . get_output_layer ( ) else { return } ;
478+
479+ let transform_reference = DefinitionIdentifier :: Network ( "Transform" . into ( ) ) ;
480+ let upstream_transforms: Vec < NodeId > = self
481+ . network_interface
482+ . upstream_flow_back_from_nodes ( vec ! [ output_layer. to_node( ) ] , & [ ] , network_interface:: FlowType :: HorizontalFlow )
483+ . skip ( 1 )
484+ . take_while ( |node_id| !self . network_interface . is_layer ( node_id, & [ ] ) )
485+ . filter ( |node_id| self . network_interface . reference ( node_id, & [ ] ) . as_ref ( ) == Some ( & transform_reference) )
486+ . collect ( ) ;
487+
488+ // Upstream walk yields downstream-to-upstream order, so the first hit is the chain's last `Transform`
489+ let ( last_transform_node_id, prior_transforms) = match upstream_transforms. split_first ( ) {
490+ Some ( ( last, prior) ) => ( Some ( * last) , prior) ,
491+ None => ( None , [ ] . as_slice ( ) ) ,
492+ } ;
493+
494+ // `composed_old` = T_n * T_{n-1} * ... * T_1, `prior_combined` = same product without T_n
495+ let compose = |ids : & [ _ ] | {
496+ ids. iter ( ) . fold ( DAffine2 :: IDENTITY , |acc, transform_id| {
497+ self . network_interface
498+ . document_network ( )
499+ . nodes
500+ . get ( transform_id)
501+ . map_or ( acc, |document_node| acc * transform_utils:: get_current_transform ( & document_node. inputs ) )
502+ } )
503+ } ;
504+ let composed_old = compose ( & upstream_transforms) ;
505+ let prior_combined = compose ( prior_transforms) ;
506+
507+ // Rebuild the y-axis from the new x-axis using the old (parallel, perpendicular) decomposition and length ratio,
508+ // so the gradient's aspect ratio and skew survive an endpoint drag (so an ellipse stays the same ellipse) instead of
509+ // the old y-axis vector remaining fixed while x changes
510+ let new_x_axis = new_end - new_start;
511+ let preserved_y_axis = scale_y_axis_to_match_new_x ( composed_old. matrix2 . x_axis , composed_old. matrix2 . y_axis , new_x_axis) ;
512+ let new_composed = DAffine2 {
513+ matrix2 : glam:: DMat2 :: from_cols ( new_x_axis, preserved_y_axis) ,
514+ translation : new_start,
515+ } ;
516+
517+ let last_transform_value = new_composed * prior_combined. inverse ( ) ;
518+
519+ let transform_node_id = if let Some ( id) = last_transform_node_id {
520+ id
521+ } else {
522+ // Don't pollute the graph with an identity 'Transform' node
523+ if last_transform_value. abs_diff_eq ( DAffine2 :: IDENTITY , 1e-6 ) {
524+ return ;
525+ }
526+ let Some ( id) = self . existing_network_node_id ( "Transform" , true ) else { return } ;
527+ id
528+ } ;
529+
530+ transform_utils:: update_transform ( self . network_interface , & transform_node_id, last_transform_value) ;
531+ self . responses . add ( PropertiesPanelMessage :: Refresh ) ;
532+ self . responses . add ( NodeGraphMessage :: RunDocumentGraph ) ;
533+ }
534+
535+ /// Write the gradient type to the last 'Gradient Type' node in the chain, inserting one only when the value differs
536+ /// from the default (`Linear`).
537+ pub fn gradient_type_set ( & mut self , gradient_type : GradientType ) {
538+ let identifier = graphene_std:: math_nodes:: gradient_type:: IDENTIFIER ;
539+ let create_if_nonexistent = gradient_type != GradientType :: default ( ) ;
540+ let Some ( node_id) = self . existing_proto_node_id ( identifier, create_if_nonexistent) else { return } ;
541+
542+ let input_connector = InputConnector :: node ( node_id, graphene_std:: math_nodes:: gradient_type:: GradientTypeInput :: INDEX ) ;
543+ self . set_input_with_refresh ( input_connector, NodeInput :: value ( TaggedValue :: GradientType ( gradient_type) , false ) , false ) ;
544+ }
545+
546+ /// Write the spread method to the last 'Spread Method' node in the chain, inserting one only when the value differs
547+ /// from the default (`Pad`).
548+ pub fn gradient_spread_method_set ( & mut self , spread_method : GradientSpreadMethod ) {
549+ let identifier = graphene_std:: math_nodes:: spread_method:: IDENTIFIER ;
550+ let create_if_nonexistent = spread_method != GradientSpreadMethod :: default ( ) ;
551+ let Some ( node_id) = self . existing_proto_node_id ( identifier, create_if_nonexistent) else { return } ;
552+
553+ let input_connector = InputConnector :: node ( node_id, graphene_std:: math_nodes:: spread_method:: SpreadMethodInput :: INDEX ) ;
554+ self . set_input_with_refresh ( input_connector, NodeInput :: value ( TaggedValue :: GradientSpreadMethod ( spread_method) , false ) , false ) ;
470555 }
471556
472557 pub fn clip_mode_toggle ( & mut self , clip_mode : Option < bool > ) {
@@ -621,3 +706,29 @@ impl<'a> ModifyInputsContext<'a> {
621706 }
622707 }
623708}
709+
710+ /// Rebuild the y-axis so its (parallel, perpendicular) components in the x-axis-aligned frame stay constant, both
711+ /// rescaled by `|new_x| / |old_x|`. This holds the (x, y) parallelogram's aspect ratio and skew fixed across an endpoint
712+ /// drag, so a radial ellipse stays the same shape (just rotated and resized) instead of distorting as x grows or shrinks.
713+ /// Falls back to a +90° rotation of `new_x` when `old_x` is degenerate.
714+ fn scale_y_axis_to_match_new_x ( old_x : DVec2 , old_y : DVec2 , new_x : DVec2 ) -> DVec2 {
715+ let old_x_length = old_x. length ( ) ;
716+ if old_x_length < 1e-9 {
717+ return DVec2 :: new ( -new_x. y , new_x. x ) ;
718+ }
719+ let ex_old = old_x / old_x_length;
720+ let ey_old = DVec2 :: new ( -ex_old. y , ex_old. x ) ;
721+
722+ let new_x_length = new_x. length ( ) ;
723+ if new_x_length < 1e-9 {
724+ return DVec2 :: ZERO ;
725+ }
726+ let ex_new = new_x / new_x_length;
727+ let ey_new = DVec2 :: new ( -ex_new. y , ex_new. x ) ;
728+
729+ let parallel = old_y. dot ( ex_old) ;
730+ let perpendicular = old_y. dot ( ey_old) ;
731+ let scale = new_x_length / old_x_length;
732+
733+ scale * ( parallel * ex_new + perpendicular * ey_new)
734+ }
0 commit comments