Skip to content

Commit a4f8f57

Browse files
committed
compat: simplify angle handling with normalize_angle
Replace the bidirectional convert_angle_from_xmile_to_canvas and convert_angle_from_canvas_to_xmile functions with a single normalize_angle function that ensures angles are in [0, 360). This approach is simpler and more deterministic: - On import: normalize whatever angle format we read to [0, 360) - Internal storage: always [0, 360) - On export: output directly (already normalized) The previous approach had confusing naming (the "xmile" vs "canvas" coordinate systems) and introduced floating point precision issues during roundtrips due to multiple conversions. The new approach just normalizes once and maintains that representation throughout. For straight-line detection, both the input angle and calculated straight-line angle are normalized before comparison, so angles like -45 and 315 (which represent the same direction) compare correctly.
1 parent 662a73d commit a4f8f57

1 file changed

Lines changed: 46 additions & 48 deletions

File tree

src/simlin-compat/src/xmile.rs

Lines changed: 46 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -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., 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

Comments
 (0)