use crate::models::profile::StabilityConfig;
use crate::models::verdict::CheckResult;
pub fn check_stability(
center_of_mass: Option<&[f64; 3]>,
stability_config: Option<&StabilityConfig>,
) -> CheckResult {
let (com, config) = match (center_of_mass, stability_config) {
(Some(c), Some(s)) => (c, s),
_ => {
return CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: true,
details: "stability check not evaluated (no data)".to_string(),
};
}
};
if !config.enabled {
return CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: true,
details: "stability check disabled".to_string(),
};
}
let polygon = &config.support_polygon;
if polygon.len() < 3 {
return CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: false,
details: "stability check failed: degenerate support polygon (fewer than 3 vertices)"
.to_string(),
};
}
if !com[0].is_finite() || !com[1].is_finite() || !com[2].is_finite() {
return CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: false,
details: "center of mass contains NaN or infinite value".to_string(),
};
}
let px = com[0];
let py = com[1];
if point_in_polygon(px, py, polygon) {
CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: true,
details: format!("CoM ({:.4}, {:.4}) is within the support polygon", px, py),
}
} else {
CheckResult {
name: "stability".to_string(),
category: "physics".to_string(),
passed: false,
details: format!("CoM ({:.4}, {:.4}) is outside the support polygon", px, py),
}
}
}
fn point_in_polygon(px: f64, py: f64, polygon: &[[f64; 2]]) -> bool {
let n = polygon.len();
let mut inside = false;
let mut j = n - 1;
for i in 0..n {
let xi = polygon[i][0];
let yi = polygon[i][1];
let xj = polygon[j][0];
let yj = polygon[j][1];
let crosses_y = (yi > py) != (yj > py);
if crosses_y {
let x_intersect = xj + (py - yj) * (xi - xj) / (yi - yj);
if px < x_intersect {
inside = !inside;
}
}
j = i;
}
inside
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::profile::StabilityConfig;
fn stability_cfg(polygon: Vec<[f64; 2]>) -> StabilityConfig {
StabilityConfig {
support_polygon: polygon,
com_height_estimate: 1.0,
enabled: true,
}
}
fn unit_square() -> StabilityConfig {
stability_cfg(vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]])
}
#[test]
fn com_at_vertex_of_polygon() {
let cfg = unit_square();
let result = check_stability(Some(&[0.0, 0.0, 0.0]), Some(&cfg));
assert_eq!(result.name, "stability");
assert!(
result.passed,
"boundary vertex (0,0) is classified as inside by ray-casting: {}",
result.details
);
}
#[test]
fn com_on_edge_of_polygon_passes() {
let cfg = unit_square();
let result = check_stability(Some(&[0.5, 0.0, 0.0]), Some(&cfg));
assert_eq!(result.name, "stability");
assert!(
result.passed,
"boundary edge midpoint (0.5,0.0) is classified as inside by ray-casting: {}",
result.details
);
}
#[test]
fn com_strictly_inside_polygon_passes() {
let cfg = unit_square();
let result = check_stability(Some(&[0.5, 0.5, 0.0]), Some(&cfg));
assert!(
result.passed,
"CoM at centroid should be inside unit square"
);
}
#[test]
fn com_strictly_outside_polygon_fails() {
let cfg = unit_square();
let result = check_stability(Some(&[2.0, 2.0, 0.0]), Some(&cfg));
assert!(!result.passed, "CoM far outside unit square should fail");
}
#[test]
fn concave_l_shaped_polygon_com_in_notch_is_outside() {
let l_shape = stability_cfg(vec![
[0.0, 0.0],
[2.0, 0.0],
[2.0, 1.0],
[1.0, 1.0],
[1.0, 2.0],
[0.0, 2.0],
]);
let result = check_stability(Some(&[1.5, 1.5, 0.0]), Some(&l_shape));
assert!(
!result.passed,
"CoM in concave notch of L-shape should be outside: {}",
result.details
);
}
#[test]
fn concave_l_shaped_polygon_com_in_solid_region_passes() {
let l_shape = stability_cfg(vec![
[0.0, 0.0],
[2.0, 0.0],
[2.0, 1.0],
[1.0, 1.0],
[1.0, 2.0],
[0.0, 2.0],
]);
let result = check_stability(Some(&[0.5, 0.5, 0.0]), Some(&l_shape));
assert!(
result.passed,
"CoM in solid region of L-shape should be inside: {}",
result.details
);
}
}