@@ -1220,32 +1220,17 @@ pub mod view_element {
12201220 use simlin_core:: datamodel:: StockFlow ;
12211221 use simlin_core:: datamodel:: view_element:: LinkShape ;
12221222
1223- // converts an angle associated with a connector (in degrees) into an
1224- // angle in the coordinate system of SVG canvases where the origin is
1225- // in the upper-left of the screen and Y grows down, and the domain is
1226- // -180 to 180.
1227- fn convert_angle_from_xmile_to_canvas ( in_degrees : f64 ) -> f64 {
1228- let out_degrees = ( 360.0 - in_degrees) % 360.0 ;
1229- if out_degrees > 180.0 {
1230- out_degrees - 360.0
1223+ /// Normalize an angle to the range [0, 360).
1224+ /// This ensures consistent representation regardless of input format.
1225+ fn normalize_angle ( degrees : f64 ) -> f64 {
1226+ let normalized = degrees % 360.0 ;
1227+ if normalized < 0.0 {
1228+ normalized + 360.0
12311229 } else {
1232- out_degrees
1230+ normalized
12331231 }
12341232 }
12351233
1236- // converts an angle associated with a connector (in degrees) into an
1237- // angle in the coordinate system of SVG canvases where the origin is
1238- // in the upper-left of the screen and Y grows down, and the domain is
1239- // -180 to 180.
1240- fn convert_angle_from_canvas_to_xmile ( in_degrees : f64 ) -> f64 {
1241- let out_degrees = if in_degrees < 0.0 {
1242- in_degrees + 360.0
1243- } else {
1244- in_degrees
1245- } ;
1246- ( 360.0 - out_degrees) % 360.0
1247- }
1248-
12491234 /// Get the position (x, y) of a view element by its uid.
12501235 fn get_element_position ( view : & datamodel:: StockFlow , uid : i32 ) -> Option < ( f64 , f64 ) > {
12511236 for element in & view. elements {
@@ -1262,42 +1247,56 @@ pub mod view_element {
12621247 None
12631248 }
12641249
1265- /// Calculate the straight-line angle (in canvas coordinates, degrees ) between two points.
1250+ /// Calculate the straight-line angle (in degrees, normalized to [0, 360) ) between two points.
12661251 /// Returns the angle from (from_x, from_y) to (to_x, to_y).
12671252 fn calculate_straight_line_angle ( from_x : f64 , from_y : f64 , to_x : f64 , to_y : f64 ) -> f64 {
12681253 let dx = to_x - from_x;
12691254 let dy = to_y - from_y;
1270- dy. atan2 ( dx) . to_degrees ( )
1255+ normalize_angle ( dy. atan2 ( dx) . to_degrees ( ) )
12711256 }
12721257
12731258 /// Epsilon for comparing angles - angles within this threshold are considered equal.
12741259 /// This is tight to ensure roundtrip fidelity (original angles are preserved).
12751260 const ANGLE_EPSILON_DEGREES : f64 = 0.01 ;
12761261
1277- /// Check if an angle (in canvas coordinates) is effectively equal to the straight-line
1278- /// angle between two points. Uses a tight epsilon to ensure roundtrip fidelity.
1262+ /// Check if an angle is effectively equal to the straight-line angle between two points.
1263+ /// Both angles are normalized to [0, 360) before comparison.
1264+ /// Uses a tight epsilon to ensure roundtrip fidelity.
12791265 fn is_straight_line_angle (
12801266 angle_degrees : f64 ,
12811267 from_x : f64 ,
12821268 from_y : f64 ,
12831269 to_x : f64 ,
12841270 to_y : f64 ,
12851271 ) -> bool {
1272+ let angle = normalize_angle ( angle_degrees) ;
12861273 let straight_angle = calculate_straight_line_angle ( from_x, from_y, to_x, to_y) ;
1287- let diff = ( angle_degrees - straight_angle) . abs ( ) ;
1288- // Handle wraparound (e.g., -179 vs 179 should be close )
1274+ let diff = ( angle - straight_angle) . abs ( ) ;
1275+ // Handle wraparound (e.g., 1° vs 359° should be 2° apart, not 358° )
12891276 let diff = if diff > 180.0 { 360.0 - diff } else { diff } ;
12901277 diff < ANGLE_EPSILON_DEGREES
12911278 }
12921279
12931280 #[ test]
1294- fn test_convert_angles ( ) {
1295- let cases: & [ ( f64 , f64 ) ] = & [ ( 0.0 , 0.0 ) , ( 45.0 , -45.0 ) , ( 270.0 , 90.0 ) ] ;
1281+ fn test_normalize_angle ( ) {
1282+ // Already in range
1283+ assert_eq ! ( 0.0 , normalize_angle( 0.0 ) ) ;
1284+ assert_eq ! ( 45.0 , normalize_angle( 45.0 ) ) ;
1285+ assert_eq ! ( 359.0 , normalize_angle( 359.0 ) ) ;
12961286
1297- for ( input, output) in cases {
1298- assert_eq ! ( * output, convert_angle_from_xmile_to_canvas( * input) ) ;
1299- assert_eq ! ( * input, convert_angle_from_canvas_to_xmile( * output) ) ;
1300- }
1287+ // Negative angles
1288+ assert_eq ! ( 315.0 , normalize_angle( -45.0 ) ) ;
1289+ assert_eq ! ( 270.0 , normalize_angle( -90.0 ) ) ;
1290+ assert_eq ! ( 180.0 , normalize_angle( -180.0 ) ) ;
1291+ assert_eq ! ( 1.0 , normalize_angle( -359.0 ) ) ;
1292+
1293+ // Angles >= 360
1294+ assert_eq ! ( 0.0 , normalize_angle( 360.0 ) ) ;
1295+ assert_eq ! ( 45.0 , normalize_angle( 405.0 ) ) ;
1296+ assert_eq ! ( 90.0 , normalize_angle( 450.0 ) ) ;
1297+
1298+ // Large negative
1299+ assert_eq ! ( 320.0 , normalize_angle( -400.0 ) ) ;
13011300 }
13021301
13031302 #[ derive( Copy , Clone , PartialEq , Eq , Debug , Deserialize , Serialize ) ]
@@ -1958,9 +1957,8 @@ pub mod view_element {
19581957 . collect ( ) ,
19591958 )
19601959 } else {
1961- datamodel:: view_element:: LinkShape :: Arc ( convert_angle_from_canvas_to_xmile (
1962- v. angle . unwrap_or ( 0.0 ) ,
1963- ) )
1960+ // Normalize angle to [0, 360) for consistent internal representation
1961+ datamodel:: view_element:: LinkShape :: Arc ( normalize_angle ( v. angle . unwrap_or ( 0.0 ) ) )
19641962 } ;
19651963 datamodel:: view_element:: Link {
19661964 uid : v. uid . unwrap_or ( -1 ) ,
@@ -1993,24 +1991,24 @@ pub mod view_element {
19931991 . collect ( ) ,
19941992 )
19951993 } else if let Some ( angle) = v. angle {
1994+ // Normalize angle to [0, 360) for consistent comparison and storage
1995+ let normalized_angle = normalize_angle ( angle) ;
19961996 // Check if this angle represents a straight line
19971997 if let ( Some ( & ( from_x, from_y) ) , Some ( & ( to_x, to_y) ) ) =
19981998 ( positions. get ( & from_uid) , positions. get ( & to_uid) )
19991999 {
2000- if is_straight_line_angle ( angle , from_x, from_y, to_x, to_y) {
2000+ if is_straight_line_angle ( normalized_angle , from_x, from_y, to_x, to_y) {
20012001 datamodel:: view_element:: LinkShape :: Straight
20022002 } else {
2003- datamodel:: view_element:: LinkShape :: Arc ( convert_angle_from_canvas_to_xmile (
2004- angle,
2005- ) )
2003+ datamodel:: view_element:: LinkShape :: Arc ( normalized_angle)
20062004 }
20072005 } else {
20082006 // Can't look up positions, treat as arc
2009- datamodel:: view_element:: LinkShape :: Arc ( convert_angle_from_canvas_to_xmile ( angle ) )
2007+ datamodel:: view_element:: LinkShape :: Arc ( normalized_angle )
20102008 }
20112009 } else {
20122010 // No angle specified, default to arc at 0
2013- datamodel:: view_element:: LinkShape :: Arc ( convert_angle_from_canvas_to_xmile ( 0.0 ) )
2011+ datamodel:: view_element:: LinkShape :: Arc ( 0.0 )
20142012 } ;
20152013
20162014 datamodel:: view_element:: Link {
@@ -2057,7 +2055,8 @@ pub mod view_element {
20572055 }
20582056 }
20592057 LinkShape :: Arc ( angle) => {
2060- ( None , Some ( convert_angle_from_xmile_to_canvas ( angle) ) , None )
2058+ // Internal angles are already normalized to [0, 360), output directly
2059+ ( None , Some ( angle) , None )
20612060 }
20622061 LinkShape :: MultiPoint ( points) => (
20632062 None ,
@@ -2312,11 +2311,10 @@ pub mod view_element {
23122311 // Should stay as Arc, not Straight
23132312 match dm_link. shape {
23142313 LinkShape :: Arc ( angle) => {
2315- // The angle should be converted to XMILE format (0-360)
2316- // canvas 45 -> xmile: convert_angle_from_canvas_to_xmile(45) = 315
2314+ // The angle should be normalized to [0, 360) - 45 is already in range
23172315 assert ! (
2318- ( angle - 315 .0) . abs( ) < 0.001 ,
2319- "expected arc angle ~315 , got {}" ,
2316+ ( angle - 45 .0) . abs( ) < 0.001 ,
2317+ "expected arc angle ~45 , got {}" ,
23202318 angle
23212319 ) ;
23222320 }
0 commit comments