@@ -294,6 +294,10 @@ pub enum GradientDragTarget {
294294 Stop ( usize ) ,
295295 Midpoint ( usize ) ,
296296 New ,
297+ /// Drag the +minor-axis handle (perpendicular to major axis, toward +perp direction)
298+ RadialMinorPos ,
299+ /// Drag the −minor-axis handle (perpendicular to major axis, toward −perp direction)
300+ RadialMinorNeg ,
297301}
298302
299303/// Contains information about the selected gradient handle
@@ -341,7 +345,17 @@ fn calculate_insertion(start: DVec2, end: DVec2, stops: &GradientStops, mouse: D
341345 return Some ( projection) ;
342346 }
343347
344- None
348+ /// Compute minor-axis handle positions in document space for a radial gradient.
349+ fn radial_minor_handles ( gradient : & Gradient ) -> Option < ( DVec2 , DVec2 ) > {
350+ let major_vec = gradient. end - gradient. start ;
351+ let major_len = major_vec. length ( ) ;
352+ if major_len < f64:: EPSILON {
353+ return None ;
354+ }
355+ let minor_len = major_len * gradient. aspect ;
356+ let minor_dir = ( major_vec / major_len) . perp ( ) ;
357+ let center = gradient. start ;
358+ Some ( ( center + minor_dir * minor_len, center - minor_dir * minor_len) )
345359}
346360
347361impl SelectedGradient {
@@ -561,6 +575,28 @@ impl SelectedGradient {
561575 self . gradient . stops . midpoint [ midpoint_index] = midpoint_ratio;
562576 }
563577 }
578+ GradientDragTarget :: RadialMinorPos | GradientDragTarget :: RadialMinorNeg => {
579+ let document_to_viewport = snap_data. document . metadata ( ) . document_to_viewport ;
580+ let mouse_doc = document_to_viewport. inverse ( ) . transform_point2 ( mouse) ;
581+
582+ let center_doc = self . gradient . start ;
583+ let major_vec = self . gradient . end - center_doc;
584+ let major_len = major_vec. length ( ) ;
585+
586+ if major_len < f64:: EPSILON {
587+ self . render_gradient ( responses) ;
588+ return ;
589+ }
590+
591+ let minor_dir = ( major_vec / major_len) . perp ( ) ;
592+ let minor_dist = ( mouse_doc - center_doc) . dot ( minor_dir) . abs ( ) ;
593+
594+ if snap_rotate {
595+ self . gradient . aspect = 1. ;
596+ } else {
597+ self . gradient . aspect = ( minor_dist / major_len) . clamp ( 0.01 , 10. ) ;
598+ }
599+ }
564600 }
565601 self . render_gradient ( responses) ;
566602 }
@@ -810,6 +846,34 @@ impl Fsm for GradientToolFsmState {
810846 }
811847 }
812848
849+ if gradient. gradient_type == GradientType :: Radial {
850+ let major_vec = end - start;
851+ let major_len = major_vec. length ( ) ;
852+ if major_len > f64:: EPSILON {
853+ let minor_len = major_len * gradient. aspect ;
854+ let major_dir = major_vec / major_len;
855+ let minor_dir = major_dir. perp ( ) ;
856+ let center = start;
857+
858+ let minor_pos_vp = center + minor_dir * minor_len;
859+ let minor_neg_vp = center - minor_dir * minor_len;
860+
861+ let angle = major_dir. y . atan2 ( major_dir. x ) ;
862+ overlay_context. dashed_ellipse ( center, major_len, minor_len, Some ( angle) , None , None , None , None , Some ( COLOR_OVERLAY_BLUE ) , Some ( 4. ) , Some ( 4. ) , None ) ;
863+
864+ overlay_context. line ( center, minor_pos_vp, Some ( COLOR_OVERLAY_BLUE ) , None ) ;
865+ overlay_context. line ( center, minor_neg_vp, Some ( COLOR_OVERLAY_BLUE ) , None ) ;
866+
867+ let minor_tol_sq = ( MANIPULATOR_GROUP_MARKER_SIZE * 2. ) . powi ( 2 ) ;
868+ let pos_active = dragging == Some ( GradientDragTarget :: RadialMinorPos ) ;
869+ let neg_active = dragging == Some ( GradientDragTarget :: RadialMinorNeg ) ;
870+ let pos_hovered = !pos_active && !matches ! ( self , GradientToolFsmState :: Drawing { .. } ) && minor_pos_vp. distance_squared ( mouse) < minor_tol_sq;
871+ let neg_hovered = !neg_active && !matches ! ( self , GradientToolFsmState :: Drawing { .. } ) && minor_neg_vp. distance_squared ( mouse) < minor_tol_sq;
872+ overlay_context. manipulator_handle ( minor_pos_vp, pos_active || pos_hovered, None ) ;
873+ overlay_context. manipulator_handle ( minor_neg_vp, neg_active || neg_hovered, None ) ;
874+ }
875+ }
876+
813877 let snap_data = SnapData :: new ( document, input, viewport) ;
814878 tool_data. snap_manager . draw_overlays ( snap_data, & mut overlay_context) ;
815879
@@ -1048,6 +1112,33 @@ impl Fsm for GradientToolFsmState {
10481112 let Some ( gradient) = get_gradient ( layer, & document. network_interface ) else { continue } ;
10491113 let transform = gradient_space_transform ( layer, document) ;
10501114
1115+ if drag_hint. is_none ( ) && gradient. gradient_type == GradientType :: Radial {
1116+ if let Some ( ( minor_pos_doc, minor_neg_doc) ) = radial_minor_handles ( & gradient) {
1117+ let minor_pos_vp = transform. transform_point2 ( minor_pos_doc) ;
1118+ let minor_neg_vp = transform. transform_point2 ( minor_neg_doc) ;
1119+ let minor_tolerance = ( MANIPULATOR_GROUP_MARKER_SIZE * 2. ) . powi ( 2 ) ;
1120+
1121+ let minor_drag_target = if minor_pos_vp. distance_squared ( mouse) < minor_tolerance {
1122+ Some ( GradientDragTarget :: RadialMinorPos )
1123+ } else if minor_neg_vp. distance_squared ( mouse) < minor_tolerance {
1124+ Some ( GradientDragTarget :: RadialMinorNeg )
1125+ } else {
1126+ None
1127+ } ;
1128+
1129+ if let Some ( drag_target) = minor_drag_target {
1130+ drag_hint = Some ( GradientDragHintState :: RadialMinor ) ;
1131+ tool_data. selected_gradient = Some ( SelectedGradient {
1132+ layer : Some ( layer) ,
1133+ transform,
1134+ gradient : gradient. clone ( ) ,
1135+ dragging : drag_target,
1136+ initial_gradient : gradient. clone ( ) ,
1137+ } ) ;
1138+ }
1139+ }
1140+ }
1141+
10511142 // Check for dragging a midpoint diamond
10521143 if drag_hint. is_none ( ) {
10531144 let ( start, end) = ( transform. transform_point2 ( gradient. start ) , transform. transform_point2 ( gradient. end ) ) ;
@@ -1380,6 +1471,12 @@ impl Fsm for GradientToolFsmState {
13801471 groups. push ( HintGroup ( vec ! [ HintInfo :: mouse( MouseMotion :: LmbDouble , "Reset Midpoint" ) ] ) ) ;
13811472 }
13821473 }
1474+ GradientHoverTarget :: RadialMinor => {
1475+ groups. push ( HintGroup ( vec ! [
1476+ HintInfo :: mouse( MouseMotion :: LmbDrag , "Adjust Ellipse" ) ,
1477+ HintInfo :: keys( [ Key :: Shift ] , "Snap to Circle" ) . prepend_plus( ) ,
1478+ ] ) ) ;
1479+ }
13831480 }
13841481
13851482 // Delete/reset hint based on selection
@@ -1414,6 +1511,9 @@ impl Fsm for GradientToolFsmState {
14141511 GradientDragHintState :: Midpoint { resettable : true } => {
14151512 groups. push ( HintGroup ( vec ! [ HintInfo :: keys( [ Key :: Backspace ] , "Reset Midpoint" ) ] ) ) ;
14161513 }
1514+ GradientDragHintState :: RadialMinor => {
1515+ groups. push ( HintGroup ( vec ! [ HintInfo :: keys( [ Key :: Shift ] , "Snap to Circle" ) ] ) ) ;
1516+ }
14171517 _ => { }
14181518 }
14191519
@@ -1449,6 +1549,17 @@ fn detect_hover_target(mouse: DVec2, document: &DocumentMessageHandler) -> Gradi
14491549 let ( start, end) = ( transform. transform_point2 ( gradient. start ) , transform. transform_point2 ( gradient. end ) ) ;
14501550 let line_length = start. distance ( end) ;
14511551
1552+ if gradient. gradient_type == GradientType :: Radial {
1553+ if let Some ( ( minor_pos_doc, minor_neg_doc) ) = radial_minor_handles ( & gradient) {
1554+ let minor_pos_vp = transform. transform_point2 ( minor_pos_doc) ;
1555+ let minor_neg_vp = transform. transform_point2 ( minor_neg_doc) ;
1556+ let minor_tolerance = ( MANIPULATOR_GROUP_MARKER_SIZE * 2. ) . powi ( 2 ) ;
1557+ if minor_pos_vp. distance_squared ( mouse) < minor_tolerance || minor_neg_vp. distance_squared ( mouse) < minor_tolerance {
1558+ return GradientHoverTarget :: RadialMinor ;
1559+ }
1560+ }
1561+ }
1562+
14521563 // Check midpoint diamonds first (smaller hit area, higher priority)
14531564 for i in 0 ..gradient. stops . position . len ( ) . saturating_sub ( 1 ) {
14541565 let left = gradient. stops . position [ i] ;
@@ -1506,7 +1617,7 @@ fn compute_selected_target(tool_data: &GradientToolData) -> GradientSelectedTarg
15061617 let resettable = selected_gradient. gradient . stops . midpoint . get ( i) . is_some_and ( |& midpoint_value| midpoint_is_resettable ( midpoint_value) ) ;
15071618 GradientSelectedTarget :: Midpoint { resettable }
15081619 }
1509- GradientDragTarget :: New => GradientSelectedTarget :: None ,
1620+ GradientDragTarget :: New | GradientDragTarget :: RadialMinorPos | GradientDragTarget :: RadialMinorNeg => GradientSelectedTarget :: None ,
15101621 }
15111622}
15121623
@@ -1593,6 +1704,8 @@ enum GradientHoverTarget {
15931704 Midpoint {
15941705 resettable : bool ,
15951706 } ,
1707+ /// Hovering over a radial minor-axis handle
1708+ RadialMinor ,
15961709}
15971710
15981711#[ derive( Clone , Copy , Debug , PartialEq , Eq , Default ) ]
@@ -1615,6 +1728,8 @@ enum GradientDragHintState {
16151728 Midpoint {
16161729 resettable : bool ,
16171730 } ,
1731+ /// Dragging a radial minor-axis handle to reshape the ellipse
1732+ RadialMinor ,
16181733}
16191734
16201735#[ cfg( test) ]
0 commit comments