use brepkit_math::vec::{Point3, Vec3};
const WINDING_DOT_TOL: f64 = 1e-30;
pub fn newell_normal(verts: &[Point3]) -> Vec3 {
let m = verts.len();
let mut nx = 0.0_f64;
let mut ny = 0.0_f64;
let mut nz = 0.0_f64;
for i in 0..m {
let curr = verts[i];
let next = verts[(i + 1) % m];
nx += (curr.y() - next.y()) * (curr.z() + next.z());
ny += (curr.z() - next.z()) * (curr.x() + next.x());
nz += (curr.x() - next.x()) * (curr.y() + next.y());
}
Vec3::new(nx, ny, nz)
}
#[allow(clippy::cast_precision_loss)]
pub fn polygon_centroid(verts: &[Point3]) -> Point3 {
let inv = 1.0 / verts.len() as f64;
let (sx, sy, sz) = verts.iter().fold((0.0, 0.0, 0.0), |(x, y, z), p| {
(x + p.x(), y + p.y(), z + p.z())
});
Point3::new(sx * inv, sy * inv, sz * inv)
}
pub fn is_cw_winding(positions: &[Point3], reference_dir: &Vec3) -> bool {
if positions.len() < 3 {
return false;
}
let newell = newell_normal(positions);
let dot = newell.dot(*reference_dir);
dot < -WINDING_DOT_TOL
}
pub fn ensure_ccw_positions(positions: &mut [Point3], reference_dir: Vec3) -> bool {
if is_cw_winding(positions, &reference_dir) {
positions.reverse();
true
} else {
false
}
}
pub fn inner_wire_is_cw(positions: &[Point3], offset: &Vec3) -> bool {
if positions.len() < 3 {
return true; }
let axis = offset.normalize().unwrap_or(Vec3::new(0.0, 0.0, 1.0));
let p0 = positions[0];
let mut signed_area_2 = 0.0;
for i in 1..positions.len() - 1 {
let a = positions[i] - p0;
let b = positions[i + 1] - p0;
signed_area_2 += a.cross(b).dot(axis);
}
signed_area_2 < -f64::EPSILON
}
pub fn ensure_ccw_profiles(profile_verts: &mut [Vec<Point3>]) -> bool {
let c0 = polygon_centroid(&profile_verts[0]);
let c1 = polygon_centroid(&profile_verts[profile_verts.len() - 1]);
let stack_dir = c1 - c0;
let newell = newell_normal(&profile_verts[0]);
let dot = newell.dot(stack_dir);
if dot.abs() < WINDING_DOT_TOL {
return false;
}
if dot < 0.0 {
for verts in profile_verts.iter_mut() {
verts.reverse();
}
true
} else {
false
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
use super::*;
#[test]
fn newell_normal_ccw_square() {
let verts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let n = newell_normal(&verts);
assert!(n.z() > 0.0, "CCW square should have +Z Newell normal");
}
#[test]
fn newell_normal_cw_square() {
let verts = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
let n = newell_normal(&verts);
assert!(n.z() < 0.0, "CW square should have -Z Newell normal");
}
#[test]
fn is_cw_detects_cw_winding() {
let cw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
let up = Vec3::new(0.0, 0.0, 1.0);
assert!(is_cw_winding(&cw, &up));
}
#[test]
fn is_cw_rejects_ccw_winding() {
let ccw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let up = Vec3::new(0.0, 0.0, 1.0);
assert!(!is_cw_winding(&ccw, &up));
}
#[test]
fn ensure_ccw_reverses_cw() {
let mut cw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
let up = Vec3::new(0.0, 0.0, 1.0);
let reversed = ensure_ccw_positions(&mut cw, up);
assert!(reversed);
assert!(!is_cw_winding(&cw, &up));
}
#[test]
fn ensure_ccw_preserves_ccw() {
let mut ccw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let up = Vec3::new(0.0, 0.0, 1.0);
let reversed = ensure_ccw_positions(&mut ccw, up);
assert!(!reversed);
}
#[test]
fn inner_wire_cw_detected() {
let cw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
assert!(inner_wire_is_cw(&cw, &Vec3::new(0.0, 0.0, 1.0)));
}
#[test]
fn inner_wire_ccw_detected() {
let ccw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
assert!(!inner_wire_is_cw(&ccw, &Vec3::new(0.0, 0.0, 1.0)));
}
#[test]
fn inner_wire_degenerate_defaults_cw() {
let degen = vec![Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 0.0, 0.0)];
assert!(inner_wire_is_cw(°en, &Vec3::new(0.0, 0.0, 1.0)));
}
#[test]
fn inner_wire_cw_on_xz_plane_detected() {
let sq = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 1.0),
Point3::new(0.0, 0.0, 1.0),
];
assert!(inner_wire_is_cw(&sq, &Vec3::new(0.0, 1.0, 0.0)));
}
#[test]
fn ensure_ccw_profiles_reverses_cw() {
let cw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
];
let cw2 = cw.iter().map(|p| *p + Vec3::new(0.0, 0.0, 2.0)).collect();
let mut profiles = vec![cw, cw2];
let reversed = ensure_ccw_profiles(&mut profiles);
assert!(reversed, "CW profiles should be reversed");
let n = newell_normal(&profiles[0]);
let stack = polygon_centroid(&profiles[1]) - polygon_centroid(&profiles[0]);
assert!(n.dot(stack) > 0.0);
}
#[test]
fn ensure_ccw_profiles_preserves_ccw() {
let ccw = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let ccw2 = ccw.iter().map(|p| *p + Vec3::new(0.0, 0.0, 2.0)).collect();
let mut profiles = vec![ccw, ccw2];
let reversed = ensure_ccw_profiles(&mut profiles);
assert!(!reversed, "CCW profiles should not be reversed");
}
#[test]
fn ensure_ccw_profiles_coplanar_unchanged() {
let sq = vec![
Point3::new(0.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
];
let mut profiles = vec![sq.clone(), sq];
let reversed = ensure_ccw_profiles(&mut profiles);
assert!(!reversed);
}
}