use u_nesting_core::robust::orient2d_filtered;
use u_nesting_core::{Error, Result};
use crate::nfp::Nfp;
use crate::placement_utils::polygon_centroid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContactType {
VertexEdge,
EdgeVertex,
EdgeEdge,
}
#[derive(Debug, Clone)]
pub struct Contact {
pub contact_type: ContactType,
pub stationary_idx: usize,
pub orbiting_idx: usize,
pub point: (f64, f64),
}
#[derive(Debug, Clone)]
pub struct TouchingGroup {
pub contacts: Vec<Contact>,
pub reference_position: (f64, f64),
}
impl TouchingGroup {
pub fn new(reference_position: (f64, f64)) -> Self {
Self {
contacts: Vec::new(),
reference_position,
}
}
pub fn add_contact(&mut self, contact: Contact) {
self.contacts.push(contact);
}
pub fn has_contacts(&self) -> bool {
!self.contacts.is_empty()
}
pub fn contact_count(&self) -> usize {
self.contacts.len()
}
}
#[derive(Debug, Clone)]
pub struct TranslationVector {
pub direction: (f64, f64),
pub max_distance: f64,
pub source_contact: Contact,
}
impl TranslationVector {
pub fn new(direction: (f64, f64), max_distance: f64, source_contact: Contact) -> Self {
Self {
direction,
max_distance,
source_contact,
}
}
pub fn translation(&self) -> (f64, f64) {
(
self.direction.0 * self.max_distance,
self.direction.1 * self.max_distance,
)
}
}
#[inline]
fn edge_vector(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
let n = polygon.len();
let p1 = polygon[i];
let p2 = polygon[(i + 1) % n];
(p2.0 - p1.0, p2.1 - p1.1)
}
#[inline]
#[allow(dead_code)]
fn edge_normal(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
let (dx, dy) = edge_vector(polygon, i);
let len = (dx * dx + dy * dy).sqrt();
if len < 1e-10 {
(0.0, 0.0)
} else {
(dy / len, -dx / len)
}
}
#[inline]
fn normalize(v: (f64, f64)) -> (f64, f64) {
let len = (v.0 * v.0 + v.1 * v.1).sqrt();
if len < 1e-10 {
(0.0, 0.0)
} else {
(v.0 / len, v.1 / len)
}
}
#[inline]
fn dot(a: (f64, f64), b: (f64, f64)) -> f64 {
a.0 * b.0 + a.1 * b.1
}
#[inline]
fn cross(a: (f64, f64), b: (f64, f64)) -> f64 {
a.0 * b.1 - a.1 * b.0
}
#[inline]
fn distance(a: (f64, f64), b: (f64, f64)) -> f64 {
let dx = b.0 - a.0;
let dy = b.1 - a.1;
(dx * dx + dy * dy).sqrt()
}
#[inline]
fn edges_parallel(e1: (f64, f64), e2: (f64, f64)) -> bool {
let c = cross(e1, e2).abs();
let len1 = (e1.0 * e1.0 + e1.1 * e1.1).sqrt();
let len2 = (e2.0 * e2.0 + e2.1 * e2.1).sqrt();
c < 1e-10 * len1 * len2
}
fn project_point_to_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
let dx = p2.0 - p1.0;
let dy = p2.1 - p1.1;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-20 {
return 0.0;
}
let t = ((point.0 - p1.0) * dx + (point.1 - p1.1) * dy) / len_sq;
t.clamp(0.0, 1.0)
}
fn point_to_segment_distance(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
let t = project_point_to_segment(point, p1, p2);
let proj = (p1.0 + t * (p2.0 - p1.0), p1.1 + t * (p2.1 - p1.1));
distance(point, proj)
}
fn point_on_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64), tol: f64) -> bool {
point_to_segment_distance(point, p1, p2) < tol
}
pub fn find_contacts(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
tolerance: f64,
) -> Vec<Contact> {
let mut contacts = Vec::new();
let n_stat = stationary.len();
let n_orb = orbiting.len();
for (orb_idx, &orb_vertex) in orbiting.iter().enumerate() {
for stat_edge_idx in 0..n_stat {
let stat_p1 = stationary[stat_edge_idx];
let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
if point_on_segment(orb_vertex, stat_p1, stat_p2, tolerance) {
contacts.push(Contact {
contact_type: ContactType::VertexEdge,
stationary_idx: stat_edge_idx,
orbiting_idx: orb_idx,
point: orb_vertex,
});
}
}
}
for (stat_idx, &stat_vertex) in stationary.iter().enumerate() {
for orb_edge_idx in 0..n_orb {
let orb_p1 = orbiting[orb_edge_idx];
let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
if point_on_segment(stat_vertex, orb_p1, orb_p2, tolerance) {
contacts.push(Contact {
contact_type: ContactType::EdgeVertex,
stationary_idx: stat_idx,
orbiting_idx: orb_edge_idx,
point: stat_vertex,
});
}
}
}
for stat_edge_idx in 0..n_stat {
let stat_p1 = stationary[stat_edge_idx];
let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
let stat_edge = (stat_p2.0 - stat_p1.0, stat_p2.1 - stat_p1.1);
for orb_edge_idx in 0..n_orb {
let orb_p1 = orbiting[orb_edge_idx];
let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
let orb_edge = (orb_p2.0 - orb_p1.0, orb_p2.1 - orb_p1.1);
if edges_parallel(stat_edge, orb_edge) {
let d1 = point_to_segment_distance(orb_p1, stat_p1, stat_p2);
let d2 = point_to_segment_distance(orb_p2, stat_p1, stat_p2);
if d1 < tolerance && d2 < tolerance {
let mid = ((orb_p1.0 + orb_p2.0) / 2.0, (orb_p1.1 + orb_p2.1) / 2.0);
contacts.push(Contact {
contact_type: ContactType::EdgeEdge,
stationary_idx: stat_edge_idx,
orbiting_idx: orb_edge_idx,
point: mid,
});
}
}
}
}
contacts
}
pub fn create_touching_group(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
reference_position: (f64, f64),
tolerance: f64,
) -> TouchingGroup {
let contacts = find_contacts(stationary, orbiting, tolerance);
let mut group = TouchingGroup::new(reference_position);
for contact in contacts {
group.add_contact(contact);
}
group
}
pub fn compute_translation_vectors(
touching_group: &TouchingGroup,
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
) -> Vec<TranslationVector> {
let mut vectors = Vec::new();
for contact in &touching_group.contacts {
match contact.contact_type {
ContactType::VertexEdge => {
let edge = edge_vector(stationary, contact.stationary_idx);
let dir = normalize(edge);
let neg_dir = (-dir.0, -dir.1);
let n = stationary.len();
let edge_end = stationary[(contact.stationary_idx + 1) % n];
let edge_start = stationary[contact.stationary_idx];
let max_dist_pos = distance(contact.point, edge_end);
let max_dist_neg = distance(contact.point, edge_start);
vectors.push(TranslationVector::new(dir, max_dist_pos, contact.clone()));
vectors.push(TranslationVector::new(
neg_dir,
max_dist_neg,
contact.clone(),
));
}
ContactType::EdgeVertex => {
let edge = edge_vector(orbiting, contact.orbiting_idx);
let dir = normalize(edge);
let neg_dir = (-dir.0, -dir.1);
let n = orbiting.len();
let orb_edge_end = orbiting[(contact.orbiting_idx + 1) % n];
let orb_edge_start = orbiting[contact.orbiting_idx];
let dist_to_end = distance(contact.point, orb_edge_end);
let dist_to_start = distance(contact.point, orb_edge_start);
vectors.push(TranslationVector::new(
neg_dir,
dist_to_end,
contact.clone(),
));
vectors.push(TranslationVector::new(dir, dist_to_start, contact.clone()));
}
ContactType::EdgeEdge => {
let edge = edge_vector(stationary, contact.stationary_idx);
let dir = normalize(edge);
let neg_dir = (-dir.0, -dir.1);
let stat_edge_len = {
let n = stationary.len();
distance(
stationary[contact.stationary_idx],
stationary[(contact.stationary_idx + 1) % n],
)
};
let orb_edge_len = {
let n = orbiting.len();
distance(
orbiting[contact.orbiting_idx],
orbiting[(contact.orbiting_idx + 1) % n],
)
};
let max_dist = stat_edge_len + orb_edge_len;
vectors.push(TranslationVector::new(dir, max_dist, contact.clone()));
vectors.push(TranslationVector::new(neg_dir, max_dist, contact.clone()));
}
}
}
vectors
}
pub fn select_translation_vector(
vectors: &[TranslationVector],
previous_direction: Option<(f64, f64)>,
stationary_centroid: (f64, f64),
orbiting_position: (f64, f64),
) -> Option<&TranslationVector> {
if vectors.is_empty() {
return None;
}
let radial = normalize((
orbiting_position.0 - stationary_centroid.0,
orbiting_position.1 - stationary_centroid.1,
));
let ccw_preferred = (-radial.1, radial.0);
let mut best_idx = 0;
let mut best_score = f64::NEG_INFINITY;
for (i, v) in vectors.iter().enumerate() {
let mut score = dot(v.direction, ccw_preferred);
if let Some(prev) = previous_direction {
score += 0.5 * dot(v.direction, prev);
}
if v.max_distance < 1e-6 {
score -= 100.0;
}
if score > best_score {
best_score = score;
best_idx = i;
}
}
Some(&vectors[best_idx])
}
#[derive(Debug, Clone)]
pub struct CollisionEvent {
pub distance: f64,
pub contact_type: ContactType,
pub stationary_idx: usize,
pub orbiting_idx: usize,
}
pub fn check_translation_collision(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
translation: (f64, f64),
tolerance: f64,
) -> Option<CollisionEvent> {
let trans_len = (translation.0 * translation.0 + translation.1 * translation.1).sqrt();
if trans_len < tolerance {
return None;
}
let trans_dir = (translation.0 / trans_len, translation.1 / trans_len);
let n_stat = stationary.len();
let n_orb = orbiting.len();
let mut first_collision: Option<CollisionEvent> = None;
for (orb_idx, &orb_v) in orbiting.iter().enumerate() {
for stat_edge_idx in 0..n_stat {
let stat_p1 = stationary[stat_edge_idx];
let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
if let Some(dist) =
ray_segment_intersection(orb_v, trans_dir, stat_p1, stat_p2, tolerance)
{
if dist > tolerance
&& dist < trans_len - tolerance
&& first_collision.as_ref().is_none_or(|c| dist < c.distance)
{
first_collision = Some(CollisionEvent {
distance: dist,
contact_type: ContactType::VertexEdge,
stationary_idx: stat_edge_idx,
orbiting_idx: orb_idx,
});
}
}
}
}
for (stat_idx, &stat_v) in stationary.iter().enumerate() {
for orb_edge_idx in 0..n_orb {
let orb_p1 = orbiting[orb_edge_idx];
let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
let neg_dir = (-trans_dir.0, -trans_dir.1);
if let Some(dist) = ray_segment_intersection(stat_v, neg_dir, orb_p1, orb_p2, tolerance)
{
if dist > tolerance
&& dist < trans_len - tolerance
&& first_collision.as_ref().is_none_or(|c| dist < c.distance)
{
first_collision = Some(CollisionEvent {
distance: dist,
contact_type: ContactType::EdgeVertex,
stationary_idx: stat_idx,
orbiting_idx: orb_edge_idx,
});
}
}
}
}
first_collision
}
fn ray_segment_intersection(
ray_origin: (f64, f64),
ray_dir: (f64, f64),
seg_p1: (f64, f64),
seg_p2: (f64, f64),
tolerance: f64,
) -> Option<f64> {
let seg_dir = (seg_p2.0 - seg_p1.0, seg_p2.1 - seg_p1.1);
let denominator = cross(ray_dir, seg_dir);
if denominator.abs() < tolerance {
return None;
}
let diff = (seg_p1.0 - ray_origin.0, seg_p1.1 - ray_origin.1);
let t = cross(diff, seg_dir) / denominator;
let u = cross(diff, ray_dir) / denominator;
if t >= -tolerance && u >= -tolerance && u <= 1.0 + tolerance {
Some(t.max(0.0))
} else {
None
}
}
pub fn handle_perfect_fit(
touching_group: &TouchingGroup,
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
visited_positions: &[(f64, f64)],
tolerance: f64,
) -> Vec<TranslationVector> {
let mut vectors = compute_translation_vectors(touching_group, stationary, orbiting);
vectors.retain(|v| {
let potential_pos = (
touching_group.reference_position.0 + v.direction.0 * v.max_distance.min(1.0),
touching_group.reference_position.1 + v.direction.1 * v.max_distance.min(1.0),
);
!visited_positions
.iter()
.any(|&visited| distance(potential_pos, visited) < tolerance * 10.0)
});
vectors
}
pub fn detect_interlocking_opportunity(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
current_pos: (f64, f64),
tolerance: f64,
) -> Option<(f64, f64)> {
let n_stat = stationary.len();
let mut concave_vertices = Vec::new();
for i in 0..n_stat {
let prev = stationary[if i == 0 { n_stat - 1 } else { i - 1 }];
let curr = stationary[i];
let next = stationary[(i + 1) % n_stat];
let orientation = orient2d_filtered(prev, curr, next);
if orientation.is_cw() {
concave_vertices.push((i, curr));
}
}
if concave_vertices.is_empty() {
return None;
}
let orb_bbox = compute_bbox(orbiting);
let orb_width = orb_bbox.1 .0 - orb_bbox.0 .0;
let orb_height = orb_bbox.1 .1 - orb_bbox.0 .1;
for (idx, vertex) in concave_vertices {
let prev_idx = if idx == 0 { n_stat - 1 } else { idx - 1 };
let _next_idx = (idx + 1) % n_stat;
let prev_edge = edge_vector(stationary, prev_idx);
let next_edge = edge_vector(stationary, idx);
let opening_width = (dot(prev_edge, next_edge).abs()).sqrt();
let min_dim = orb_width.min(orb_height);
if opening_width > min_dim * 0.5 {
let dir = normalize((vertex.0 - current_pos.0, vertex.1 - current_pos.1));
let test_pos = (
current_pos.0 + dir.0 * tolerance,
current_pos.1 + dir.1 * tolerance,
);
let orbiting_test: Vec<(f64, f64)> = orbiting
.iter()
.map(|&(x, y)| (x + test_pos.0, y + test_pos.1))
.collect();
if !polygons_overlap(stationary, &orbiting_test) {
return Some(test_pos);
}
}
}
None
}
fn compute_bbox(polygon: &[(f64, f64)]) -> ((f64, f64), (f64, f64)) {
let min_x = polygon
.iter()
.map(|p| p.0)
.fold(f64::INFINITY, |a, b| a.min(b));
let max_x = polygon
.iter()
.map(|p| p.0)
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
let min_y = polygon
.iter()
.map(|p| p.1)
.fold(f64::INFINITY, |a, b| a.min(b));
let max_y = polygon
.iter()
.map(|p| p.1)
.fold(f64::NEG_INFINITY, |a, b| a.max(b));
((min_x, min_y), (max_x, max_y))
}
pub fn polygons_overlap(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> bool {
polygons_overlap_with_tolerance(poly_a, poly_b, 1e-6)
}
pub fn polygons_overlap_with_tolerance(
poly_a: &[(f64, f64)],
poly_b: &[(f64, f64)],
tolerance: f64,
) -> bool {
let bbox_a = compute_bbox(poly_a);
let bbox_b = compute_bbox(poly_b);
if bbox_a.1 .0 + tolerance < bbox_b.0 .0
|| bbox_b.1 .0 + tolerance < bbox_a.0 .0
|| bbox_a.1 .1 + tolerance < bbox_b.0 .1
|| bbox_b.1 .1 + tolerance < bbox_a.0 .1
{
return false;
}
for polygon in [poly_a, poly_b] {
let n = polygon.len();
for i in 0..n {
let edge = edge_vector(polygon, i);
let axis = normalize((-edge.1, edge.0));
if axis.0.abs() < 1e-10 && axis.1.abs() < 1e-10 {
continue;
}
let (min_a, max_a) = project_polygon_on_axis(poly_a, axis);
let (min_b, max_b) = project_polygon_on_axis(poly_b, axis);
if max_a + tolerance < min_b || max_b + tolerance < min_a {
return false; }
}
}
true }
fn project_polygon_on_axis(polygon: &[(f64, f64)], axis: (f64, f64)) -> (f64, f64) {
let mut min_proj = f64::INFINITY;
let mut max_proj = f64::NEG_INFINITY;
for &p in polygon {
let proj = dot(p, axis);
min_proj = min_proj.min(proj);
max_proj = max_proj.max(proj);
}
(min_proj, max_proj)
}
#[derive(Debug, Clone)]
pub struct SlidingNfpConfig {
pub contact_tolerance: f64,
pub max_iterations: usize,
pub min_translation: f64,
}
impl Default for SlidingNfpConfig {
fn default() -> Self {
Self {
contact_tolerance: 1e-6,
max_iterations: 10000,
min_translation: 1e-8,
}
}
}
pub fn compute_nfp_sliding(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
config: &SlidingNfpConfig,
) -> Result<Nfp> {
if stationary.len() < 3 || orbiting.len() < 3 {
return Err(Error::InvalidGeometry(
"Polygons must have at least 3 vertices".into(),
));
}
let stationary = ensure_ccw(stationary);
let orbiting = ensure_ccw(orbiting);
let start_pos = find_start_position(&stationary, &orbiting)?;
let orbiting_at_start: Vec<(f64, f64)> = orbiting
.iter()
.map(|&(x, y)| (x + start_pos.0, y + start_pos.1))
.collect();
let nfp_path = trace_nfp_boundary(
&stationary,
&orbiting,
&orbiting_at_start,
start_pos,
config,
)?;
if nfp_path.len() < 3 {
return Err(Error::Internal("NFP path has fewer than 3 vertices".into()));
}
Ok(Nfp::from_polygon(nfp_path))
}
fn find_start_position(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<(f64, f64)> {
let stat_bottom_idx = stationary
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
})
.map(|(i, _)| i)
.unwrap_or(0);
let stat_bottom = stationary[stat_bottom_idx];
let orb_top_idx = orbiting
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| {
a.1.partial_cmp(&b.1)
.unwrap_or(std::cmp::Ordering::Equal)
.then(b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal))
})
.map(|(i, _)| i)
.unwrap_or(0);
let orb_top = orbiting[orb_top_idx];
let start_pos = (stat_bottom.0 - orb_top.0, stat_bottom.1 - orb_top.1);
Ok(start_pos)
}
fn trace_nfp_boundary(
stationary: &[(f64, f64)],
orbiting_original: &[(f64, f64)],
_orbiting_at_start: &[(f64, f64)],
start_pos: (f64, f64),
config: &SlidingNfpConfig,
) -> Result<Vec<(f64, f64)>> {
let mut nfp_path = Vec::new();
let mut current_pos = start_pos;
let mut previous_direction: Option<(f64, f64)> = None;
let mut stuck_counter = 0;
let stat_centroid = polygon_centroid(stationary);
nfp_path.push(current_pos);
for _iteration in 0..config.max_iterations {
let orbiting_current: Vec<(f64, f64)> = orbiting_original
.iter()
.map(|&(x, y)| (x + current_pos.0, y + current_pos.1))
.collect();
let touching_group = create_touching_group(
stationary,
&orbiting_current,
current_pos,
config.contact_tolerance,
);
if !touching_group.has_contacts() {
if let Some(recovery_pos) = recover_contact(
stationary,
&orbiting_current,
current_pos,
config.contact_tolerance,
) {
current_pos = recovery_pos;
continue;
}
break;
}
let translation_vectors = handle_perfect_fit(
&touching_group,
stationary,
&orbiting_current,
&nfp_path,
config.contact_tolerance,
);
if translation_vectors.is_empty() {
stuck_counter += 1;
if stuck_counter > 3 {
break;
}
if let Some(interlock_pos) = detect_interlocking_opportunity(
stationary,
orbiting_original,
current_pos,
config.contact_tolerance,
) {
if distance(interlock_pos, current_pos) > config.min_translation {
nfp_path.push(interlock_pos);
current_pos = interlock_pos;
continue;
}
}
break;
}
stuck_counter = 0;
let selected = select_translation_vector(
&translation_vectors,
previous_direction,
stat_centroid,
current_pos,
);
let Some(tv) = selected else {
break;
};
let intended_distance = tv.max_distance.min(1000.0); if intended_distance < config.min_translation {
break;
}
let intended_translation = (
tv.direction.0 * intended_distance,
tv.direction.1 * intended_distance,
);
let actual_translation = if let Some(collision) = check_translation_collision(
stationary,
&orbiting_current,
intended_translation,
config.contact_tolerance,
) {
let blocked_dist = (collision.distance - config.contact_tolerance).max(0.0);
(tv.direction.0 * blocked_dist, tv.direction.1 * blocked_dist)
} else {
intended_translation
};
let new_pos = (
current_pos.0 + actual_translation.0,
current_pos.1 + actual_translation.1,
);
let dist_to_start = distance(new_pos, start_pos);
if nfp_path.len() > 2 && dist_to_start < config.contact_tolerance * 10.0 {
break;
}
let dist_to_last = distance(new_pos, current_pos);
if dist_to_last > config.min_translation {
nfp_path.push(new_pos);
previous_direction = Some(tv.direction);
}
current_pos = new_pos;
}
simplify_polygon(&nfp_path, config.contact_tolerance)
}
fn recover_contact(
stationary: &[(f64, f64)],
orbiting: &[(f64, f64)],
current_pos: (f64, f64),
tolerance: f64,
) -> Option<(f64, f64)> {
let mut min_dist = f64::INFINITY;
let mut closest_point = None;
let n_stat = stationary.len();
for orb_v in orbiting {
for i in 0..n_stat {
let stat_p1 = stationary[i];
let stat_p2 = stationary[(i + 1) % n_stat];
let t = project_point_to_segment(*orb_v, stat_p1, stat_p2);
let proj = (
stat_p1.0 + t * (stat_p2.0 - stat_p1.0),
stat_p1.1 + t * (stat_p2.1 - stat_p1.1),
);
let dist = distance(*orb_v, proj);
if dist < min_dist {
min_dist = dist;
closest_point = Some(proj);
}
}
}
if let Some(target) = closest_point {
if min_dist > tolerance {
let orb_centroid = polygon_centroid(orbiting);
let dir = normalize((target.0 - orb_centroid.0, target.1 - orb_centroid.1));
let move_dist = min_dist - tolerance * 0.5;
return Some((
current_pos.0 + dir.0 * move_dist,
current_pos.1 + dir.1 * move_dist,
));
}
}
None
}
fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
let area = signed_area(polygon);
if area < 0.0 {
polygon.iter().rev().cloned().collect()
} else {
polygon.to_vec()
}
}
fn signed_area(polygon: &[(f64, f64)]) -> f64 {
let n = polygon.len();
let mut area = 0.0;
for i in 0..n {
let j = (i + 1) % n;
area += polygon[i].0 * polygon[j].1;
area -= polygon[j].0 * polygon[i].1;
}
area / 2.0
}
fn simplify_polygon(polygon: &[(f64, f64)], tolerance: f64) -> Result<Vec<(f64, f64)>> {
if polygon.len() < 3 {
return Ok(polygon.to_vec());
}
let mut result = Vec::new();
let n = polygon.len();
for i in 0..n {
let prev = if i == 0 { n - 1 } else { i - 1 };
let next = (i + 1) % n;
let p_prev = polygon[prev];
let p_curr = polygon[i];
let p_next = polygon[next];
let orientation = orient2d_filtered(p_prev, p_curr, p_next);
if !orientation.is_collinear() {
result.push(p_curr);
} else {
let dist = point_to_segment_distance(p_curr, p_prev, p_next);
if dist > tolerance {
result.push(p_curr);
}
}
}
if result.len() < 3 {
Ok(polygon.to_vec())
} else {
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
}
#[test]
fn test_edge_vector() {
let square = rect(10.0, 10.0);
let e0 = edge_vector(&square, 0);
assert!((e0.0 - 10.0).abs() < 1e-10);
assert!(e0.1.abs() < 1e-10);
}
#[test]
fn test_edge_normal() {
let square = rect(10.0, 10.0);
let n0 = edge_normal(&square, 0);
assert!(n0.0.abs() < 1e-10);
assert!((n0.1 - (-1.0)).abs() < 1e-10);
}
#[test]
fn test_edges_parallel() {
assert!(edges_parallel((1.0, 0.0), (2.0, 0.0)));
assert!(edges_parallel((1.0, 0.0), (-1.0, 0.0)));
assert!(!edges_parallel((1.0, 0.0), (0.0, 1.0)));
}
#[test]
fn test_point_on_segment() {
let p1 = (0.0, 0.0);
let p2 = (10.0, 0.0);
assert!(point_on_segment((5.0, 0.0), p1, p2, 1e-6));
assert!(point_on_segment((0.0, 0.0), p1, p2, 1e-6));
assert!(point_on_segment((10.0, 0.0), p1, p2, 1e-6));
assert!(!point_on_segment((5.0, 1.0), p1, p2, 1e-6));
}
#[test]
fn test_find_contacts_ve() {
let stationary = rect(10.0, 10.0);
let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
.iter()
.map(|&(x, y)| (x + 5.0, y + 10.0)) .collect();
let contacts = find_contacts(&stationary, &orbiting, 1e-6);
assert!(!contacts.is_empty(), "Should find at least one contact");
}
#[test]
fn test_find_start_position() {
let stationary = rect(10.0, 10.0);
let orbiting = rect(5.0, 5.0);
let start = find_start_position(&stationary, &orbiting).unwrap();
assert!(
(start.1 - (-5.0)).abs() < 1e-6,
"Start Y should be -5, got {}",
start.1
);
}
#[test]
fn test_touching_group() {
let ref_pos = (5.0, 5.0);
let mut group = TouchingGroup::new(ref_pos);
assert!(!group.has_contacts());
assert_eq!(group.contact_count(), 0);
group.add_contact(Contact {
contact_type: ContactType::VertexEdge,
stationary_idx: 0,
orbiting_idx: 0,
point: (5.0, 5.0),
});
assert!(group.has_contacts());
assert_eq!(group.contact_count(), 1);
}
#[test]
fn test_compute_translation_vectors() {
let stationary = rect(10.0, 10.0);
let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
.iter()
.map(|&(x, y)| (x + 2.5, y + 10.0)) .collect();
let group = create_touching_group(&stationary, &orbiting, (2.5, 10.0), 1e-6);
let vectors = compute_translation_vectors(&group, &stationary, &orbiting);
assert!(!vectors.is_empty(), "Should have translation vectors");
}
#[test]
fn test_sliding_nfp_two_squares() {
let stationary = rect(10.0, 10.0);
let orbiting = rect(5.0, 5.0);
let config = SlidingNfpConfig {
contact_tolerance: 1e-4,
max_iterations: 1000,
min_translation: 1e-6,
};
let result = compute_nfp_sliding(&stationary, &orbiting, &config);
assert!(result.is_ok(), "NFP computation failed: {:?}", result.err());
let nfp = result.unwrap();
assert!(!nfp.is_empty());
assert!(nfp.vertex_count() >= 4);
}
#[test]
fn test_ensure_ccw() {
let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
let ccw = ensure_ccw(&cw);
assert!(signed_area(&ccw) > 0.0);
}
#[test]
fn test_simplify_polygon() {
let with_extra = vec![
(0.0, 0.0),
(5.0, 0.0), (10.0, 0.0),
(10.0, 10.0),
(0.0, 10.0),
];
let simplified = simplify_polygon(&with_extra, 1e-6).unwrap();
assert_eq!(simplified.len(), 4, "Simplified should have 4 vertices");
}
#[test]
fn test_polygon_centroid() {
let square = rect(10.0, 10.0);
let centroid = polygon_centroid(&square);
assert!((centroid.0 - 5.0).abs() < 1e-10);
assert!((centroid.1 - 5.0).abs() < 1e-10);
}
#[test]
fn test_ray_segment_intersection() {
let result =
ray_segment_intersection((0.0, 0.0), (1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
assert!(result.is_some());
assert!((result.unwrap() - 5.0).abs() < 1e-6);
let result =
ray_segment_intersection((0.0, 0.0), (-1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
assert!(result.is_none());
}
#[test]
fn test_check_translation_collision() {
let stationary = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
let orbiting = vec![(15.0, 2.0), (20.0, 2.0), (20.0, 8.0), (15.0, 8.0)];
let translation = (-10.0, 0.0);
let collision = check_translation_collision(&stationary, &orbiting, translation, 1e-6);
assert!(
collision.is_some(),
"Should detect collision when moving left"
);
if let Some(c) = collision {
assert!(
c.distance > 4.0 && c.distance < 6.0,
"Collision distance should be ~5, got {}",
c.distance
);
}
}
#[test]
fn test_polygons_overlap_true() {
let a = rect(10.0, 10.0);
let b: Vec<(f64, f64)> = rect(10.0, 10.0)
.iter()
.map(|&(x, y)| (x + 5.0, y + 5.0))
.collect();
assert!(
polygons_overlap(&a, &b),
"Overlapping squares should overlap"
);
}
#[test]
fn test_polygons_overlap_false() {
let a = rect(10.0, 10.0);
let b: Vec<(f64, f64)> = rect(10.0, 10.0)
.iter()
.map(|&(x, y)| (x + 20.0, y))
.collect();
assert!(
!polygons_overlap(&a, &b),
"Separated squares should not overlap"
);
}
#[test]
fn test_compute_bbox() {
let square = rect(10.0, 10.0);
let bbox = compute_bbox(&square);
assert!((bbox.0 .0 - 0.0).abs() < 1e-10);
assert!((bbox.0 .1 - 0.0).abs() < 1e-10);
assert!((bbox.1 .0 - 10.0).abs() < 1e-10);
assert!((bbox.1 .1 - 10.0).abs() < 1e-10);
}
#[test]
fn test_sliding_nfp_triangle() {
let stationary = vec![(0.0, 0.0), (10.0, 0.0), (5.0, 8.66)];
let orbiting = rect(3.0, 3.0);
let config = SlidingNfpConfig {
contact_tolerance: 1e-4,
max_iterations: 1000,
min_translation: 1e-6,
};
let result = compute_nfp_sliding(&stationary, &orbiting, &config);
assert!(result.is_ok(), "NFP of triangle and square should succeed");
let nfp = result.unwrap();
assert!(
nfp.vertex_count() >= 3,
"NFP should have at least 3 vertices"
);
}
#[test]
fn test_sliding_nfp_l_shape() {
let stationary = vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 5.0),
(5.0, 5.0),
(5.0, 20.0),
(0.0, 20.0),
];
let orbiting = rect(3.0, 3.0);
let config = SlidingNfpConfig {
contact_tolerance: 1e-4,
max_iterations: 2000,
min_translation: 1e-6,
};
let result = compute_nfp_sliding(&stationary, &orbiting, &config);
assert!(
result.is_ok(),
"NFP of L-shape and square should succeed: {:?}",
result.err()
);
let nfp = result.unwrap();
assert!(
nfp.vertex_count() >= 6,
"NFP of L-shape should have >= 6 vertices, got {}",
nfp.vertex_count()
);
}
#[test]
fn test_handle_perfect_fit_filters_visited() {
let stationary = rect(10.0, 10.0);
let orbiting: Vec<(f64, f64)> =
rect(5.0, 5.0).iter().map(|&(x, y)| (x, y + 10.0)).collect();
let group = create_touching_group(&stationary, &orbiting, (0.0, 10.0), 1e-4);
let visited = vec![(0.5, 10.0), (0.0, 10.5)];
let vectors = handle_perfect_fit(&group, &stationary, &orbiting, &visited, 1e-4);
let unfiltered = compute_translation_vectors(&group, &stationary, &orbiting);
assert!(vectors.len() <= unfiltered.len());
}
#[test]
fn test_detect_interlocking_concave_polygon() {
let stationary = vec![
(0.0, 0.0),
(20.0, 0.0),
(20.0, 5.0),
(5.0, 5.0),
(5.0, 15.0),
(20.0, 15.0),
(20.0, 20.0),
(0.0, 20.0),
];
let orbiting = rect(3.0, 3.0);
let current_pos = (25.0, 10.0);
let result = detect_interlocking_opportunity(&stationary, &orbiting, current_pos, 1e-4);
let _ = result;
}
#[test]
fn test_sliding_nfp_same_size_squares() {
let stationary = rect(10.0, 10.0);
let orbiting = rect(10.0, 10.0);
let config = SlidingNfpConfig {
contact_tolerance: 1e-4,
max_iterations: 1000,
min_translation: 1e-6,
};
let result = compute_nfp_sliding(&stationary, &orbiting, &config);
assert!(result.is_ok(), "NFP of same-size squares should succeed");
let nfp = result.unwrap();
assert!(nfp.vertex_count() >= 4);
}
#[test]
fn test_signed_area_ccw() {
let ccw = rect(10.0, 10.0);
let area = signed_area(&ccw);
assert!(area > 0.0, "CCW polygon should have positive signed area");
}
#[test]
fn test_signed_area_cw() {
let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
let area = signed_area(&cw);
assert!(area < 0.0, "CW polygon should have negative signed area");
}
}