@@ -32,6 +32,18 @@ fn points_from_pairs(pairs: &[(f64, f64)]) -> Points {
3232 Points :: try_from ( points) . expect ( "points must satisfy minimum size" )
3333}
3434
35+ fn parsed_point_pairs ( app : & mut CurveFitApp ) -> Vec < ( f64 , f64 ) > {
36+ let cache = app. points_cache_with_policy ( true ) ;
37+ let points = cache
38+ . parsed_points
39+ . as_ref ( )
40+ . expect ( "points must parse successfully" ) ;
41+ points
42+ . iter ( )
43+ . map ( |point| ( point. x ( ) , point. y ( ) ) )
44+ . collect :: < Vec < _ > > ( )
45+ }
46+
3547fn assert_approx_eq ( actual : f64 , expected : f64 , tolerance : f64 ) {
3648 assert ! (
3749 ( actual - expected) . abs( ) <= tolerance,
@@ -1359,3 +1371,158 @@ fn fill_points_with_residuals_is_noop_when_residuals_are_absent() {
13591371 assert ! ( app. points. undo_stack. is_empty( ) ) ;
13601372 assert ! ( app. points. redo_stack. is_empty( ) ) ;
13611373}
1374+
1375+ #[ test]
1376+ fn move_points_to_positive_xy_rebases_minimums_and_preserves_offsets ( ) {
1377+ let original = [ ( -2.0 , -1.0 ) , ( 0.0 , 3.0 ) , ( 5.0 , -4.0 ) ] ;
1378+ let mut app = CurveFitApp {
1379+ points : super :: PointsEditorState {
1380+ text : "-2 -1\n 0 3\n 5 -4\n " . to_string ( ) ,
1381+ ..Default :: default ( )
1382+ } ,
1383+ ..Default :: default ( )
1384+ } ;
1385+ app. invalidate_points_cache ( ) ;
1386+
1387+ app. move_points_to_positive_xy ( ) ;
1388+
1389+ let shifted = parsed_point_pairs ( & mut app) ;
1390+ assert_eq ! ( shifted. len( ) , original. len( ) ) ;
1391+
1392+ let min_x = shifted
1393+ . iter ( )
1394+ . map ( |( x, _) | * x)
1395+ . fold ( f64:: INFINITY , f64:: min) ;
1396+ let min_y = shifted
1397+ . iter ( )
1398+ . map ( |( _, y) | * y)
1399+ . fold ( f64:: INFINITY , f64:: min) ;
1400+ assert_approx_eq ( min_x, super :: POINTS_POSITIVE_AXIS_EPS , 1e-12 ) ;
1401+ assert_approx_eq ( min_y, super :: POINTS_POSITIVE_AXIS_EPS , 1e-12 ) ;
1402+
1403+ for index in 1 ..shifted. len ( ) {
1404+ assert_approx_eq (
1405+ shifted[ index] . 0 - shifted[ 0 ] . 0 ,
1406+ original[ index] . 0 - original[ 0 ] . 0 ,
1407+ 1e-12 ,
1408+ ) ;
1409+ assert_approx_eq (
1410+ shifted[ index] . 1 - shifted[ 0 ] . 1 ,
1411+ original[ index] . 1 - original[ 0 ] . 1 ,
1412+ 1e-12 ,
1413+ ) ;
1414+ }
1415+ }
1416+
1417+ #[ test]
1418+ fn move_points_to_positive_xy_keeps_already_positive_points_unchanged ( ) {
1419+ let mut app = CurveFitApp {
1420+ points : super :: PointsEditorState {
1421+ text : "2 3\n 4 5\n " . to_string ( ) ,
1422+ ..Default :: default ( )
1423+ } ,
1424+ ..Default :: default ( )
1425+ } ;
1426+ app. invalidate_points_cache ( ) ;
1427+
1428+ app. move_points_to_positive_xy ( ) ;
1429+
1430+ let shifted = parsed_point_pairs ( & mut app) ;
1431+ assert_eq ! ( shifted. len( ) , 2 ) ;
1432+ assert_approx_eq ( shifted[ 0 ] . 0 , 2.0 , 1e-12 ) ;
1433+ assert_approx_eq ( shifted[ 0 ] . 1 , 3.0 , 1e-12 ) ;
1434+ assert_approx_eq ( shifted[ 1 ] . 0 , 4.0 , 1e-12 ) ;
1435+ assert_approx_eq ( shifted[ 1 ] . 1 , 5.0 , 1e-12 ) ;
1436+ }
1437+
1438+ #[ test]
1439+ fn move_points_to_positive_xy_moves_only_axis_that_needs_it ( ) {
1440+ let mut app = CurveFitApp {
1441+ points : super :: PointsEditorState {
1442+ text : "-2 3\n 1 5\n " . to_string ( ) ,
1443+ ..Default :: default ( )
1444+ } ,
1445+ ..Default :: default ( )
1446+ } ;
1447+ app. invalidate_points_cache ( ) ;
1448+
1449+ app. move_points_to_positive_xy ( ) ;
1450+
1451+ let shifted = parsed_point_pairs ( & mut app) ;
1452+ assert_eq ! ( shifted. len( ) , 2 ) ;
1453+ assert_approx_eq ( shifted[ 0 ] . 0 , super :: POINTS_POSITIVE_AXIS_EPS , 1e-12 ) ;
1454+ assert_approx_eq ( shifted[ 0 ] . 1 , 3.0 , 1e-12 ) ;
1455+ assert_approx_eq ( shifted[ 1 ] . 0 , 3.000001 , 1e-12 ) ;
1456+ assert_approx_eq ( shifted[ 1 ] . 1 , 5.0 , 1e-12 ) ;
1457+ }
1458+
1459+ #[ test]
1460+ fn move_points_to_positive_xy_pushes_undo_and_clears_redo ( ) {
1461+ let previous_text = "-1 0\n 1 2\n " ;
1462+ let mut app = CurveFitApp {
1463+ points : super :: PointsEditorState {
1464+ text : previous_text. to_string ( ) ,
1465+ redo_stack : vec ! [ "stale redo entry" . to_string( ) ] ,
1466+ ..Default :: default ( )
1467+ } ,
1468+ ..Default :: default ( )
1469+ } ;
1470+ app. invalidate_points_cache ( ) ;
1471+
1472+ app. move_points_to_positive_xy ( ) ;
1473+
1474+ assert_eq ! ( app. points. undo_stack, vec![ previous_text. to_string( ) ] ) ;
1475+ assert ! ( app. points. redo_stack. is_empty( ) ) ;
1476+ assert_ne ! ( app. points. text, previous_text) ;
1477+
1478+ let moved_text = app. points . text . clone ( ) ;
1479+ app. undo_points_edit ( ) ;
1480+ assert_eq ! ( app. points. text, previous_text) ;
1481+ app. redo_points_edit ( ) ;
1482+ assert_eq ! ( app. points. text, moved_text) ;
1483+
1484+ let shifted = parsed_point_pairs ( & mut app) ;
1485+ let min_x = shifted
1486+ . iter ( )
1487+ . map ( |( x, _) | * x)
1488+ . fold ( f64:: INFINITY , f64:: min) ;
1489+ let min_y = shifted
1490+ . iter ( )
1491+ . map ( |( _, y) | * y)
1492+ . fold ( f64:: INFINITY , f64:: min) ;
1493+ assert_approx_eq ( min_x, super :: POINTS_POSITIVE_AXIS_EPS , 1e-12 ) ;
1494+ assert_approx_eq ( min_y, super :: POINTS_POSITIVE_AXIS_EPS , 1e-12 ) ;
1495+ }
1496+
1497+ #[ test]
1498+ fn can_move_points_to_positive_xy_requires_non_empty_valid_points ( ) {
1499+ let mut empty = CurveFitApp {
1500+ points : super :: PointsEditorState {
1501+ text : String :: new ( ) ,
1502+ ..Default :: default ( )
1503+ } ,
1504+ ..Default :: default ( )
1505+ } ;
1506+ empty. invalidate_points_cache ( ) ;
1507+ assert ! ( !empty. can_move_points_to_positive_xy( ) ) ;
1508+
1509+ let mut invalid = CurveFitApp {
1510+ points : super :: PointsEditorState {
1511+ text : "1 2 3\n " . to_string ( ) ,
1512+ ..Default :: default ( )
1513+ } ,
1514+ ..Default :: default ( )
1515+ } ;
1516+ invalid. invalidate_points_cache ( ) ;
1517+ assert ! ( !invalid. can_move_points_to_positive_xy( ) ) ;
1518+
1519+ let mut valid = CurveFitApp {
1520+ points : super :: PointsEditorState {
1521+ text : "0 0\n 1 1\n " . to_string ( ) ,
1522+ ..Default :: default ( )
1523+ } ,
1524+ ..Default :: default ( )
1525+ } ;
1526+ valid. invalidate_points_cache ( ) ;
1527+ assert ! ( valid. can_move_points_to_positive_xy( ) ) ;
1528+ }
0 commit comments