1use u_nesting_core::robust::orient2d_filtered;
26use u_nesting_core::{Error, Result};
27
28use crate::nfp::Nfp;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum ContactType {
37 VertexEdge,
39 EdgeVertex,
41 EdgeEdge,
43}
44
45#[derive(Debug, Clone)]
47pub struct Contact {
48 pub contact_type: ContactType,
50 pub stationary_idx: usize,
52 pub orbiting_idx: usize,
54 pub point: (f64, f64),
56}
57
58#[derive(Debug, Clone)]
63pub struct TouchingGroup {
64 pub contacts: Vec<Contact>,
66 pub reference_position: (f64, f64),
68}
69
70impl TouchingGroup {
71 pub fn new(reference_position: (f64, f64)) -> Self {
73 Self {
74 contacts: Vec::new(),
75 reference_position,
76 }
77 }
78
79 pub fn add_contact(&mut self, contact: Contact) {
81 self.contacts.push(contact);
82 }
83
84 pub fn has_contacts(&self) -> bool {
86 !self.contacts.is_empty()
87 }
88
89 pub fn contact_count(&self) -> usize {
91 self.contacts.len()
92 }
93}
94
95#[derive(Debug, Clone)]
101pub struct TranslationVector {
102 pub direction: (f64, f64),
104 pub max_distance: f64,
106 pub source_contact: Contact,
108}
109
110impl TranslationVector {
111 pub fn new(direction: (f64, f64), max_distance: f64, source_contact: Contact) -> Self {
113 Self {
114 direction,
115 max_distance,
116 source_contact,
117 }
118 }
119
120 pub fn translation(&self) -> (f64, f64) {
122 (
123 self.direction.0 * self.max_distance,
124 self.direction.1 * self.max_distance,
125 )
126 }
127}
128
129#[inline]
135fn edge_vector(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
136 let n = polygon.len();
137 let p1 = polygon[i];
138 let p2 = polygon[(i + 1) % n];
139 (p2.0 - p1.0, p2.1 - p1.1)
140}
141
142#[inline]
144#[allow(dead_code)]
145fn edge_normal(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
146 let (dx, dy) = edge_vector(polygon, i);
147 let len = (dx * dx + dy * dy).sqrt();
148 if len < 1e-10 {
149 (0.0, 0.0)
150 } else {
151 (dy / len, -dx / len)
153 }
154}
155
156#[inline]
158fn normalize(v: (f64, f64)) -> (f64, f64) {
159 let len = (v.0 * v.0 + v.1 * v.1).sqrt();
160 if len < 1e-10 {
161 (0.0, 0.0)
162 } else {
163 (v.0 / len, v.1 / len)
164 }
165}
166
167#[inline]
169fn dot(a: (f64, f64), b: (f64, f64)) -> f64 {
170 a.0 * b.0 + a.1 * b.1
171}
172
173#[inline]
175fn cross(a: (f64, f64), b: (f64, f64)) -> f64 {
176 a.0 * b.1 - a.1 * b.0
177}
178
179#[inline]
181fn distance(a: (f64, f64), b: (f64, f64)) -> f64 {
182 let dx = b.0 - a.0;
183 let dy = b.1 - a.1;
184 (dx * dx + dy * dy).sqrt()
185}
186
187#[inline]
189fn edges_parallel(e1: (f64, f64), e2: (f64, f64)) -> bool {
190 let c = cross(e1, e2).abs();
191 let len1 = (e1.0 * e1.0 + e1.1 * e1.1).sqrt();
192 let len2 = (e2.0 * e2.0 + e2.1 * e2.1).sqrt();
193 c < 1e-10 * len1 * len2
194}
195
196fn project_point_to_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
199 let dx = p2.0 - p1.0;
200 let dy = p2.1 - p1.1;
201 let len_sq = dx * dx + dy * dy;
202 if len_sq < 1e-20 {
203 return 0.0;
204 }
205 let t = ((point.0 - p1.0) * dx + (point.1 - p1.1) * dy) / len_sq;
206 t.clamp(0.0, 1.0)
207}
208
209fn point_to_segment_distance(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
211 let t = project_point_to_segment(point, p1, p2);
212 let proj = (p1.0 + t * (p2.0 - p1.0), p1.1 + t * (p2.1 - p1.1));
213 distance(point, proj)
214}
215
216fn point_on_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64), tol: f64) -> bool {
218 point_to_segment_distance(point, p1, p2) < tol
219}
220
221pub fn find_contacts(
233 stationary: &[(f64, f64)],
234 orbiting: &[(f64, f64)],
235 tolerance: f64,
236) -> Vec<Contact> {
237 let mut contacts = Vec::new();
238
239 let n_stat = stationary.len();
240 let n_orb = orbiting.len();
241
242 for (orb_idx, &orb_vertex) in orbiting.iter().enumerate() {
244 for stat_edge_idx in 0..n_stat {
245 let stat_p1 = stationary[stat_edge_idx];
246 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
247 if point_on_segment(orb_vertex, stat_p1, stat_p2, tolerance) {
248 contacts.push(Contact {
249 contact_type: ContactType::VertexEdge,
250 stationary_idx: stat_edge_idx,
251 orbiting_idx: orb_idx,
252 point: orb_vertex,
253 });
254 }
255 }
256 }
257
258 for (stat_idx, &stat_vertex) in stationary.iter().enumerate() {
260 for orb_edge_idx in 0..n_orb {
261 let orb_p1 = orbiting[orb_edge_idx];
262 let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
263 if point_on_segment(stat_vertex, orb_p1, orb_p2, tolerance) {
264 contacts.push(Contact {
265 contact_type: ContactType::EdgeVertex,
266 stationary_idx: stat_idx,
267 orbiting_idx: orb_edge_idx,
268 point: stat_vertex,
269 });
270 }
271 }
272 }
273
274 for stat_edge_idx in 0..n_stat {
276 let stat_p1 = stationary[stat_edge_idx];
277 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
278 let stat_edge = (stat_p2.0 - stat_p1.0, stat_p2.1 - stat_p1.1);
279
280 for orb_edge_idx in 0..n_orb {
281 let orb_p1 = orbiting[orb_edge_idx];
282 let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
283 let orb_edge = (orb_p2.0 - orb_p1.0, orb_p2.1 - orb_p1.1);
284
285 if edges_parallel(stat_edge, orb_edge) {
287 let d1 = point_to_segment_distance(orb_p1, stat_p1, stat_p2);
289 let d2 = point_to_segment_distance(orb_p2, stat_p1, stat_p2);
290 if d1 < tolerance && d2 < tolerance {
291 let mid = ((orb_p1.0 + orb_p2.0) / 2.0, (orb_p1.1 + orb_p2.1) / 2.0);
294 contacts.push(Contact {
295 contact_type: ContactType::EdgeEdge,
296 stationary_idx: stat_edge_idx,
297 orbiting_idx: orb_edge_idx,
298 point: mid,
299 });
300 }
301 }
302 }
303 }
304
305 contacts
306}
307
308pub fn create_touching_group(
310 stationary: &[(f64, f64)],
311 orbiting: &[(f64, f64)],
312 reference_position: (f64, f64),
313 tolerance: f64,
314) -> TouchingGroup {
315 let contacts = find_contacts(stationary, orbiting, tolerance);
316 let mut group = TouchingGroup::new(reference_position);
317 for contact in contacts {
318 group.add_contact(contact);
319 }
320 group
321}
322
323pub fn compute_translation_vectors(
332 touching_group: &TouchingGroup,
333 stationary: &[(f64, f64)],
334 orbiting: &[(f64, f64)],
335) -> Vec<TranslationVector> {
336 let mut vectors = Vec::new();
337
338 for contact in &touching_group.contacts {
339 match contact.contact_type {
340 ContactType::VertexEdge => {
341 let edge = edge_vector(stationary, contact.stationary_idx);
344 let dir = normalize(edge);
345 let neg_dir = (-dir.0, -dir.1);
346
347 let n = stationary.len();
349 let edge_end = stationary[(contact.stationary_idx + 1) % n];
350 let edge_start = stationary[contact.stationary_idx];
351 let max_dist_pos = distance(contact.point, edge_end);
352 let max_dist_neg = distance(contact.point, edge_start);
353
354 vectors.push(TranslationVector::new(dir, max_dist_pos, contact.clone()));
355 vectors.push(TranslationVector::new(
356 neg_dir,
357 max_dist_neg,
358 contact.clone(),
359 ));
360 }
361 ContactType::EdgeVertex => {
362 let edge = edge_vector(orbiting, contact.orbiting_idx);
365 let dir = normalize(edge);
366 let neg_dir = (-dir.0, -dir.1);
367
368 let n = orbiting.len();
370 let orb_edge_end = orbiting[(contact.orbiting_idx + 1) % n];
371 let orb_edge_start = orbiting[contact.orbiting_idx];
372
373 let dist_to_end = distance(contact.point, orb_edge_end);
376 let dist_to_start = distance(contact.point, orb_edge_start);
377
378 vectors.push(TranslationVector::new(
379 neg_dir,
380 dist_to_end,
381 contact.clone(),
382 ));
383 vectors.push(TranslationVector::new(dir, dist_to_start, contact.clone()));
384 }
385 ContactType::EdgeEdge => {
386 let edge = edge_vector(stationary, contact.stationary_idx);
389 let dir = normalize(edge);
390 let neg_dir = (-dir.0, -dir.1);
391
392 let stat_edge_len = {
394 let n = stationary.len();
395 distance(
396 stationary[contact.stationary_idx],
397 stationary[(contact.stationary_idx + 1) % n],
398 )
399 };
400 let orb_edge_len = {
401 let n = orbiting.len();
402 distance(
403 orbiting[contact.orbiting_idx],
404 orbiting[(contact.orbiting_idx + 1) % n],
405 )
406 };
407 let max_dist = stat_edge_len + orb_edge_len;
408
409 vectors.push(TranslationVector::new(dir, max_dist, contact.clone()));
410 vectors.push(TranslationVector::new(neg_dir, max_dist, contact.clone()));
411 }
412 }
413 }
414
415 vectors
416}
417
418pub fn select_translation_vector(
423 vectors: &[TranslationVector],
424 previous_direction: Option<(f64, f64)>,
425 stationary_centroid: (f64, f64),
426 orbiting_position: (f64, f64),
427) -> Option<&TranslationVector> {
428 if vectors.is_empty() {
429 return None;
430 }
431
432 let radial = normalize((
434 orbiting_position.0 - stationary_centroid.0,
435 orbiting_position.1 - stationary_centroid.1,
436 ));
437
438 let ccw_preferred = (-radial.1, radial.0);
440
441 let mut best_idx = 0;
443 let mut best_score = f64::NEG_INFINITY;
444
445 for (i, v) in vectors.iter().enumerate() {
446 let mut score = dot(v.direction, ccw_preferred);
447
448 if let Some(prev) = previous_direction {
450 score += 0.5 * dot(v.direction, prev);
451 }
452
453 if v.max_distance < 1e-6 {
455 score -= 100.0;
456 }
457
458 if score > best_score {
459 best_score = score;
460 best_idx = i;
461 }
462 }
463
464 Some(&vectors[best_idx])
465}
466
467#[derive(Debug, Clone)]
473pub struct CollisionEvent {
474 pub distance: f64,
476 pub contact_type: ContactType,
478 pub stationary_idx: usize,
480 pub orbiting_idx: usize,
482}
483
484pub fn check_translation_collision(
493 stationary: &[(f64, f64)],
494 orbiting: &[(f64, f64)],
495 translation: (f64, f64),
496 tolerance: f64,
497) -> Option<CollisionEvent> {
498 let trans_len = (translation.0 * translation.0 + translation.1 * translation.1).sqrt();
499 if trans_len < tolerance {
500 return None;
501 }
502 let trans_dir = (translation.0 / trans_len, translation.1 / trans_len);
503
504 let n_stat = stationary.len();
505 let n_orb = orbiting.len();
506
507 let mut first_collision: Option<CollisionEvent> = None;
508
509 for (orb_idx, &orb_v) in orbiting.iter().enumerate() {
511 for stat_edge_idx in 0..n_stat {
512 let stat_p1 = stationary[stat_edge_idx];
513 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
514
515 if let Some(dist) =
516 ray_segment_intersection(orb_v, trans_dir, stat_p1, stat_p2, tolerance)
517 {
518 if dist > tolerance
519 && dist < trans_len - tolerance
520 && (first_collision.is_none()
521 || dist < first_collision.as_ref().unwrap().distance)
522 {
523 first_collision = Some(CollisionEvent {
524 distance: dist,
525 contact_type: ContactType::VertexEdge,
526 stationary_idx: stat_edge_idx,
527 orbiting_idx: orb_idx,
528 });
529 }
530 }
531 }
532 }
533
534 for (stat_idx, &stat_v) in stationary.iter().enumerate() {
536 for orb_edge_idx in 0..n_orb {
537 let orb_p1 = orbiting[orb_edge_idx];
538 let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
539
540 let neg_dir = (-trans_dir.0, -trans_dir.1);
542
543 if let Some(dist) = ray_segment_intersection(stat_v, neg_dir, orb_p1, orb_p2, tolerance)
544 {
545 if dist > tolerance
546 && dist < trans_len - tolerance
547 && (first_collision.is_none()
548 || dist < first_collision.as_ref().unwrap().distance)
549 {
550 first_collision = Some(CollisionEvent {
551 distance: dist,
552 contact_type: ContactType::EdgeVertex,
553 stationary_idx: stat_idx,
554 orbiting_idx: orb_edge_idx,
555 });
556 }
557 }
558 }
559 }
560
561 first_collision
562}
563
564fn ray_segment_intersection(
569 ray_origin: (f64, f64),
570 ray_dir: (f64, f64),
571 seg_p1: (f64, f64),
572 seg_p2: (f64, f64),
573 tolerance: f64,
574) -> Option<f64> {
575 let seg_dir = (seg_p2.0 - seg_p1.0, seg_p2.1 - seg_p1.1);
576 let denominator = cross(ray_dir, seg_dir);
577
578 if denominator.abs() < tolerance {
580 return None;
581 }
582
583 let diff = (seg_p1.0 - ray_origin.0, seg_p1.1 - ray_origin.1);
585
586 let t = cross(diff, seg_dir) / denominator;
593 let u = cross(diff, ray_dir) / denominator;
594
595 if t >= -tolerance && u >= -tolerance && u <= 1.0 + tolerance {
599 Some(t.max(0.0))
600 } else {
601 None
602 }
603}
604
605pub fn handle_perfect_fit(
610 touching_group: &TouchingGroup,
611 stationary: &[(f64, f64)],
612 orbiting: &[(f64, f64)],
613 visited_positions: &[(f64, f64)],
614 tolerance: f64,
615) -> Vec<TranslationVector> {
616 let mut vectors = compute_translation_vectors(touching_group, stationary, orbiting);
617
618 vectors.retain(|v| {
620 let potential_pos = (
621 touching_group.reference_position.0 + v.direction.0 * v.max_distance.min(1.0),
622 touching_group.reference_position.1 + v.direction.1 * v.max_distance.min(1.0),
623 );
624
625 !visited_positions
626 .iter()
627 .any(|&visited| distance(potential_pos, visited) < tolerance * 10.0)
628 });
629
630 vectors
631}
632
633pub fn detect_interlocking_opportunity(
639 stationary: &[(f64, f64)],
640 orbiting: &[(f64, f64)],
641 current_pos: (f64, f64),
642 tolerance: f64,
643) -> Option<(f64, f64)> {
644 let n_stat = stationary.len();
646 let mut concave_vertices = Vec::new();
647
648 for i in 0..n_stat {
649 let prev = stationary[if i == 0 { n_stat - 1 } else { i - 1 }];
650 let curr = stationary[i];
651 let next = stationary[(i + 1) % n_stat];
652
653 let orientation = orient2d_filtered(prev, curr, next);
655 if orientation.is_cw() {
656 concave_vertices.push((i, curr));
657 }
658 }
659
660 if concave_vertices.is_empty() {
661 return None;
662 }
663
664 let orb_bbox = compute_bbox(orbiting);
666 let orb_width = orb_bbox.1 .0 - orb_bbox.0 .0;
667 let orb_height = orb_bbox.1 .1 - orb_bbox.0 .1;
668
669 for (idx, vertex) in concave_vertices {
670 let prev_idx = if idx == 0 { n_stat - 1 } else { idx - 1 };
672 let _next_idx = (idx + 1) % n_stat;
673
674 let prev_edge = edge_vector(stationary, prev_idx);
675 let next_edge = edge_vector(stationary, idx);
676
677 let opening_width = (dot(prev_edge, next_edge).abs()).sqrt();
679
680 let min_dim = orb_width.min(orb_height);
682 if opening_width > min_dim * 0.5 {
683 let dir = normalize((vertex.0 - current_pos.0, vertex.1 - current_pos.1));
686 let test_pos = (
687 current_pos.0 + dir.0 * tolerance,
688 current_pos.1 + dir.1 * tolerance,
689 );
690
691 let orbiting_test: Vec<(f64, f64)> = orbiting
693 .iter()
694 .map(|&(x, y)| (x + test_pos.0, y + test_pos.1))
695 .collect();
696
697 if !polygons_overlap(stationary, &orbiting_test) {
698 return Some(test_pos);
699 }
700 }
701 }
702
703 None
704}
705
706fn compute_bbox(polygon: &[(f64, f64)]) -> ((f64, f64), (f64, f64)) {
708 let min_x = polygon
709 .iter()
710 .map(|p| p.0)
711 .fold(f64::INFINITY, |a, b| a.min(b));
712 let max_x = polygon
713 .iter()
714 .map(|p| p.0)
715 .fold(f64::NEG_INFINITY, |a, b| a.max(b));
716 let min_y = polygon
717 .iter()
718 .map(|p| p.1)
719 .fold(f64::INFINITY, |a, b| a.min(b));
720 let max_y = polygon
721 .iter()
722 .map(|p| p.1)
723 .fold(f64::NEG_INFINITY, |a, b| a.max(b));
724 ((min_x, min_y), (max_x, max_y))
725}
726
727fn polygons_overlap(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> bool {
729 let bbox_a = compute_bbox(poly_a);
731 let bbox_b = compute_bbox(poly_b);
732
733 if bbox_a.1 .0 < bbox_b.0 .0
734 || bbox_b.1 .0 < bbox_a.0 .0
735 || bbox_a.1 .1 < bbox_b.0 .1
736 || bbox_b.1 .1 < bbox_a.0 .1
737 {
738 return false;
739 }
740
741 for polygon in [poly_a, poly_b] {
743 let n = polygon.len();
744 for i in 0..n {
745 let edge = edge_vector(polygon, i);
746 let axis = normalize((-edge.1, edge.0)); if axis.0.abs() < 1e-10 && axis.1.abs() < 1e-10 {
749 continue;
750 }
751
752 let (min_a, max_a) = project_polygon_on_axis(poly_a, axis);
754 let (min_b, max_b) = project_polygon_on_axis(poly_b, axis);
755
756 if max_a < min_b || max_b < min_a {
758 return false; }
760 }
761 }
762
763 true }
765
766fn project_polygon_on_axis(polygon: &[(f64, f64)], axis: (f64, f64)) -> (f64, f64) {
768 let mut min_proj = f64::INFINITY;
769 let mut max_proj = f64::NEG_INFINITY;
770
771 for &p in polygon {
772 let proj = dot(p, axis);
773 min_proj = min_proj.min(proj);
774 max_proj = max_proj.max(proj);
775 }
776
777 (min_proj, max_proj)
778}
779
780#[derive(Debug, Clone)]
786pub struct SlidingNfpConfig {
787 pub contact_tolerance: f64,
789 pub max_iterations: usize,
791 pub min_translation: f64,
793}
794
795impl Default for SlidingNfpConfig {
796 fn default() -> Self {
797 Self {
798 contact_tolerance: 1e-6,
799 max_iterations: 10000,
800 min_translation: 1e-8,
801 }
802 }
803}
804
805pub fn compute_nfp_sliding(
815 stationary: &[(f64, f64)],
816 orbiting: &[(f64, f64)],
817 config: &SlidingNfpConfig,
818) -> Result<Nfp> {
819 if stationary.len() < 3 || orbiting.len() < 3 {
820 return Err(Error::InvalidGeometry(
821 "Polygons must have at least 3 vertices".into(),
822 ));
823 }
824
825 let stationary = ensure_ccw(stationary);
827 let orbiting = ensure_ccw(orbiting);
828
829 let start_pos = find_start_position(&stationary, &orbiting)?;
831
832 let orbiting_at_start: Vec<(f64, f64)> = orbiting
834 .iter()
835 .map(|&(x, y)| (x + start_pos.0, y + start_pos.1))
836 .collect();
837
838 let nfp_path = trace_nfp_boundary(
840 &stationary,
841 &orbiting,
842 &orbiting_at_start,
843 start_pos,
844 config,
845 )?;
846
847 if nfp_path.len() < 3 {
848 return Err(Error::Internal("NFP path has fewer than 3 vertices".into()));
849 }
850
851 Ok(Nfp::from_polygon(nfp_path))
852}
853
854fn find_start_position(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<(f64, f64)> {
859 let stat_bottom_idx = stationary
861 .iter()
862 .enumerate()
863 .min_by(|(_, a), (_, b)| {
864 a.1.partial_cmp(&b.1)
865 .unwrap()
866 .then(a.0.partial_cmp(&b.0).unwrap())
867 })
868 .map(|(i, _)| i)
869 .unwrap_or(0);
870 let stat_bottom = stationary[stat_bottom_idx];
871
872 let orb_top_idx = orbiting
874 .iter()
875 .enumerate()
876 .max_by(|(_, a), (_, b)| {
877 a.1.partial_cmp(&b.1)
878 .unwrap()
879 .then(b.0.partial_cmp(&a.0).unwrap())
880 })
881 .map(|(i, _)| i)
882 .unwrap_or(0);
883 let orb_top = orbiting[orb_top_idx];
884
885 let start_pos = (stat_bottom.0 - orb_top.0, stat_bottom.1 - orb_top.1);
888
889 Ok(start_pos)
890}
891
892fn trace_nfp_boundary(
894 stationary: &[(f64, f64)],
895 orbiting_original: &[(f64, f64)],
896 _orbiting_at_start: &[(f64, f64)],
897 start_pos: (f64, f64),
898 config: &SlidingNfpConfig,
899) -> Result<Vec<(f64, f64)>> {
900 let mut nfp_path = Vec::new();
901 let mut current_pos = start_pos;
902 let mut previous_direction: Option<(f64, f64)> = None;
903 let mut stuck_counter = 0;
904
905 let stat_centroid = polygon_centroid(stationary);
907
908 nfp_path.push(current_pos);
909
910 for _iteration in 0..config.max_iterations {
911 let orbiting_current: Vec<(f64, f64)> = orbiting_original
913 .iter()
914 .map(|&(x, y)| (x + current_pos.0, y + current_pos.1))
915 .collect();
916
917 let touching_group = create_touching_group(
919 stationary,
920 &orbiting_current,
921 current_pos,
922 config.contact_tolerance,
923 );
924
925 if !touching_group.has_contacts() {
926 if let Some(recovery_pos) = recover_contact(
928 stationary,
929 &orbiting_current,
930 current_pos,
931 config.contact_tolerance,
932 ) {
933 current_pos = recovery_pos;
934 continue;
935 }
936 break;
937 }
938
939 let translation_vectors = handle_perfect_fit(
941 &touching_group,
942 stationary,
943 &orbiting_current,
944 &nfp_path,
945 config.contact_tolerance,
946 );
947
948 if translation_vectors.is_empty() {
949 stuck_counter += 1;
951 if stuck_counter > 3 {
952 break;
953 }
954 if let Some(interlock_pos) = detect_interlocking_opportunity(
956 stationary,
957 orbiting_original,
958 current_pos,
959 config.contact_tolerance,
960 ) {
961 if distance(interlock_pos, current_pos) > config.min_translation {
962 nfp_path.push(interlock_pos);
963 current_pos = interlock_pos;
964 continue;
965 }
966 }
967 break;
968 }
969 stuck_counter = 0;
970
971 let selected = select_translation_vector(
973 &translation_vectors,
974 previous_direction,
975 stat_centroid,
976 current_pos,
977 );
978
979 let Some(tv) = selected else {
980 break;
981 };
982
983 let intended_distance = tv.max_distance.min(1000.0); if intended_distance < config.min_translation {
986 break;
987 }
988
989 let intended_translation = (
990 tv.direction.0 * intended_distance,
991 tv.direction.1 * intended_distance,
992 );
993
994 let actual_translation = if let Some(collision) = check_translation_collision(
996 stationary,
997 &orbiting_current,
998 intended_translation,
999 config.contact_tolerance,
1000 ) {
1001 let blocked_dist = (collision.distance - config.contact_tolerance).max(0.0);
1003 (tv.direction.0 * blocked_dist, tv.direction.1 * blocked_dist)
1004 } else {
1005 intended_translation
1006 };
1007
1008 let new_pos = (
1009 current_pos.0 + actual_translation.0,
1010 current_pos.1 + actual_translation.1,
1011 );
1012
1013 let dist_to_start = distance(new_pos, start_pos);
1015 if nfp_path.len() > 2 && dist_to_start < config.contact_tolerance * 10.0 {
1016 break;
1018 }
1019
1020 let dist_to_last = distance(new_pos, current_pos);
1022 if dist_to_last > config.min_translation {
1023 nfp_path.push(new_pos);
1024 previous_direction = Some(tv.direction);
1025 }
1026
1027 current_pos = new_pos;
1028 }
1029
1030 simplify_polygon(&nfp_path, config.contact_tolerance)
1032}
1033
1034fn recover_contact(
1036 stationary: &[(f64, f64)],
1037 orbiting: &[(f64, f64)],
1038 current_pos: (f64, f64),
1039 tolerance: f64,
1040) -> Option<(f64, f64)> {
1041 let mut min_dist = f64::INFINITY;
1043 let mut closest_point = None;
1044
1045 let n_stat = stationary.len();
1046
1047 for orb_v in orbiting {
1048 for i in 0..n_stat {
1049 let stat_p1 = stationary[i];
1050 let stat_p2 = stationary[(i + 1) % n_stat];
1051
1052 let t = project_point_to_segment(*orb_v, stat_p1, stat_p2);
1053 let proj = (
1054 stat_p1.0 + t * (stat_p2.0 - stat_p1.0),
1055 stat_p1.1 + t * (stat_p2.1 - stat_p1.1),
1056 );
1057
1058 let dist = distance(*orb_v, proj);
1059 if dist < min_dist {
1060 min_dist = dist;
1061 closest_point = Some(proj);
1062 }
1063 }
1064 }
1065
1066 if let Some(target) = closest_point {
1068 if min_dist > tolerance {
1069 let orb_centroid = polygon_centroid(orbiting);
1071 let dir = normalize((target.0 - orb_centroid.0, target.1 - orb_centroid.1));
1072 let move_dist = min_dist - tolerance * 0.5;
1073 return Some((
1074 current_pos.0 + dir.0 * move_dist,
1075 current_pos.1 + dir.1 * move_dist,
1076 ));
1077 }
1078 }
1079
1080 None
1081}
1082
1083fn polygon_centroid(polygon: &[(f64, f64)]) -> (f64, f64) {
1085 let n = polygon.len() as f64;
1086 let sum_x: f64 = polygon.iter().map(|p| p.0).sum();
1087 let sum_y: f64 = polygon.iter().map(|p| p.1).sum();
1088 (sum_x / n, sum_y / n)
1089}
1090
1091fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
1093 let area = signed_area(polygon);
1094 if area < 0.0 {
1095 polygon.iter().rev().cloned().collect()
1096 } else {
1097 polygon.to_vec()
1098 }
1099}
1100
1101fn signed_area(polygon: &[(f64, f64)]) -> f64 {
1103 let n = polygon.len();
1104 let mut area = 0.0;
1105 for i in 0..n {
1106 let j = (i + 1) % n;
1107 area += polygon[i].0 * polygon[j].1;
1108 area -= polygon[j].0 * polygon[i].1;
1109 }
1110 area / 2.0
1111}
1112
1113fn simplify_polygon(polygon: &[(f64, f64)], tolerance: f64) -> Result<Vec<(f64, f64)>> {
1115 if polygon.len() < 3 {
1116 return Ok(polygon.to_vec());
1117 }
1118
1119 let mut result = Vec::new();
1120 let n = polygon.len();
1121
1122 for i in 0..n {
1123 let prev = if i == 0 { n - 1 } else { i - 1 };
1124 let next = (i + 1) % n;
1125
1126 let p_prev = polygon[prev];
1127 let p_curr = polygon[i];
1128 let p_next = polygon[next];
1129
1130 let orientation = orient2d_filtered(p_prev, p_curr, p_next);
1132 if !orientation.is_collinear() {
1133 result.push(p_curr);
1134 } else {
1135 let dist = point_to_segment_distance(p_curr, p_prev, p_next);
1137 if dist > tolerance {
1138 result.push(p_curr);
1139 }
1140 }
1141 }
1142
1143 if result.len() < 3 {
1144 Ok(polygon.to_vec())
1146 } else {
1147 Ok(result)
1148 }
1149}
1150
1151#[cfg(test)]
1156mod tests {
1157 use super::*;
1158
1159 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1160 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1161 }
1162
1163 #[test]
1164 fn test_edge_vector() {
1165 let square = rect(10.0, 10.0);
1166 let e0 = edge_vector(&square, 0);
1167 assert!((e0.0 - 10.0).abs() < 1e-10);
1168 assert!(e0.1.abs() < 1e-10);
1169 }
1170
1171 #[test]
1172 fn test_edge_normal() {
1173 let square = rect(10.0, 10.0);
1174 let n0 = edge_normal(&square, 0);
1176 assert!(n0.0.abs() < 1e-10);
1177 assert!((n0.1 - (-1.0)).abs() < 1e-10);
1178 }
1179
1180 #[test]
1181 fn test_edges_parallel() {
1182 assert!(edges_parallel((1.0, 0.0), (2.0, 0.0)));
1183 assert!(edges_parallel((1.0, 0.0), (-1.0, 0.0)));
1184 assert!(!edges_parallel((1.0, 0.0), (0.0, 1.0)));
1185 }
1186
1187 #[test]
1188 fn test_point_on_segment() {
1189 let p1 = (0.0, 0.0);
1190 let p2 = (10.0, 0.0);
1191
1192 assert!(point_on_segment((5.0, 0.0), p1, p2, 1e-6));
1193 assert!(point_on_segment((0.0, 0.0), p1, p2, 1e-6));
1194 assert!(point_on_segment((10.0, 0.0), p1, p2, 1e-6));
1195 assert!(!point_on_segment((5.0, 1.0), p1, p2, 1e-6));
1196 }
1197
1198 #[test]
1199 fn test_find_contacts_ve() {
1200 let stationary = rect(10.0, 10.0);
1202 let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
1203 .iter()
1204 .map(|&(x, y)| (x + 5.0, y + 10.0)) .collect();
1206
1207 let contacts = find_contacts(&stationary, &orbiting, 1e-6);
1208
1209 assert!(!contacts.is_empty(), "Should find at least one contact");
1211 }
1212
1213 #[test]
1214 fn test_find_start_position() {
1215 let stationary = rect(10.0, 10.0);
1216 let orbiting = rect(5.0, 5.0);
1217
1218 let start = find_start_position(&stationary, &orbiting).unwrap();
1219
1220 assert!(
1226 (start.1 - (-5.0)).abs() < 1e-6,
1227 "Start Y should be -5, got {}",
1228 start.1
1229 );
1230 }
1231
1232 #[test]
1233 fn test_touching_group() {
1234 let ref_pos = (5.0, 5.0);
1235 let mut group = TouchingGroup::new(ref_pos);
1236
1237 assert!(!group.has_contacts());
1238 assert_eq!(group.contact_count(), 0);
1239
1240 group.add_contact(Contact {
1241 contact_type: ContactType::VertexEdge,
1242 stationary_idx: 0,
1243 orbiting_idx: 0,
1244 point: (5.0, 5.0),
1245 });
1246
1247 assert!(group.has_contacts());
1248 assert_eq!(group.contact_count(), 1);
1249 }
1250
1251 #[test]
1252 fn test_compute_translation_vectors() {
1253 let stationary = rect(10.0, 10.0);
1254 let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
1255 .iter()
1256 .map(|&(x, y)| (x + 2.5, y + 10.0)) .collect();
1258
1259 let group = create_touching_group(&stationary, &orbiting, (2.5, 10.0), 1e-6);
1260 let vectors = compute_translation_vectors(&group, &stationary, &orbiting);
1261
1262 assert!(!vectors.is_empty(), "Should have translation vectors");
1264 }
1265
1266 #[test]
1267 fn test_sliding_nfp_two_squares() {
1268 let stationary = rect(10.0, 10.0);
1269 let orbiting = rect(5.0, 5.0);
1270
1271 let config = SlidingNfpConfig {
1272 contact_tolerance: 1e-4,
1273 max_iterations: 1000,
1274 min_translation: 1e-6,
1275 };
1276
1277 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1278
1279 assert!(result.is_ok(), "NFP computation failed: {:?}", result.err());
1281
1282 let nfp = result.unwrap();
1283 assert!(!nfp.is_empty());
1284
1285 assert!(nfp.vertex_count() >= 4);
1287 }
1288
1289 #[test]
1290 fn test_ensure_ccw() {
1291 let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
1293 let ccw = ensure_ccw(&cw);
1294
1295 assert!(signed_area(&ccw) > 0.0);
1297 }
1298
1299 #[test]
1300 fn test_simplify_polygon() {
1301 let with_extra = vec![
1303 (0.0, 0.0),
1304 (5.0, 0.0), (10.0, 0.0),
1306 (10.0, 10.0),
1307 (0.0, 10.0),
1308 ];
1309
1310 let simplified = simplify_polygon(&with_extra, 1e-6).unwrap();
1311
1312 assert_eq!(simplified.len(), 4, "Simplified should have 4 vertices");
1314 }
1315
1316 #[test]
1317 fn test_polygon_centroid() {
1318 let square = rect(10.0, 10.0);
1319 let centroid = polygon_centroid(&square);
1320
1321 assert!((centroid.0 - 5.0).abs() < 1e-10);
1322 assert!((centroid.1 - 5.0).abs() < 1e-10);
1323 }
1324
1325 #[test]
1330 fn test_ray_segment_intersection() {
1331 let result =
1333 ray_segment_intersection((0.0, 0.0), (1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
1334 assert!(result.is_some());
1335 assert!((result.unwrap() - 5.0).abs() < 1e-6);
1336
1337 let result =
1339 ray_segment_intersection((0.0, 0.0), (-1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
1340 assert!(result.is_none());
1341 }
1342
1343 #[test]
1344 fn test_check_translation_collision() {
1345 let stationary = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1347
1348 let orbiting = vec![(15.0, 2.0), (20.0, 2.0), (20.0, 8.0), (15.0, 8.0)];
1350
1351 let translation = (-10.0, 0.0);
1353 let collision = check_translation_collision(&stationary, &orbiting, translation, 1e-6);
1354
1355 assert!(
1358 collision.is_some(),
1359 "Should detect collision when moving left"
1360 );
1361
1362 if let Some(c) = collision {
1363 assert!(
1365 c.distance > 4.0 && c.distance < 6.0,
1366 "Collision distance should be ~5, got {}",
1367 c.distance
1368 );
1369 }
1370 }
1371
1372 #[test]
1373 fn test_polygons_overlap_true() {
1374 let a = rect(10.0, 10.0);
1376 let b: Vec<(f64, f64)> = rect(10.0, 10.0)
1377 .iter()
1378 .map(|&(x, y)| (x + 5.0, y + 5.0))
1379 .collect();
1380
1381 assert!(
1382 polygons_overlap(&a, &b),
1383 "Overlapping squares should overlap"
1384 );
1385 }
1386
1387 #[test]
1388 fn test_polygons_overlap_false() {
1389 let a = rect(10.0, 10.0);
1391 let b: Vec<(f64, f64)> = rect(10.0, 10.0)
1392 .iter()
1393 .map(|&(x, y)| (x + 20.0, y))
1394 .collect();
1395
1396 assert!(
1397 !polygons_overlap(&a, &b),
1398 "Separated squares should not overlap"
1399 );
1400 }
1401
1402 #[test]
1403 fn test_compute_bbox() {
1404 let square = rect(10.0, 10.0);
1405 let bbox = compute_bbox(&square);
1406
1407 assert!((bbox.0 .0 - 0.0).abs() < 1e-10);
1408 assert!((bbox.0 .1 - 0.0).abs() < 1e-10);
1409 assert!((bbox.1 .0 - 10.0).abs() < 1e-10);
1410 assert!((bbox.1 .1 - 10.0).abs() < 1e-10);
1411 }
1412
1413 #[test]
1414 fn test_sliding_nfp_triangle() {
1415 let stationary = vec![(0.0, 0.0), (10.0, 0.0), (5.0, 8.66)];
1417
1418 let orbiting = rect(3.0, 3.0);
1420
1421 let config = SlidingNfpConfig {
1422 contact_tolerance: 1e-4,
1423 max_iterations: 1000,
1424 min_translation: 1e-6,
1425 };
1426
1427 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1428
1429 assert!(result.is_ok(), "NFP of triangle and square should succeed");
1430 let nfp = result.unwrap();
1431 assert!(
1432 nfp.vertex_count() >= 3,
1433 "NFP should have at least 3 vertices"
1434 );
1435 }
1436
1437 #[test]
1438 fn test_sliding_nfp_l_shape() {
1439 let stationary = vec![
1441 (0.0, 0.0),
1442 (20.0, 0.0),
1443 (20.0, 5.0),
1444 (5.0, 5.0),
1445 (5.0, 20.0),
1446 (0.0, 20.0),
1447 ];
1448
1449 let orbiting = rect(3.0, 3.0);
1451
1452 let config = SlidingNfpConfig {
1453 contact_tolerance: 1e-4,
1454 max_iterations: 2000,
1455 min_translation: 1e-6,
1456 };
1457
1458 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1459
1460 assert!(
1461 result.is_ok(),
1462 "NFP of L-shape and square should succeed: {:?}",
1463 result.err()
1464 );
1465 let nfp = result.unwrap();
1466 assert!(
1468 nfp.vertex_count() >= 6,
1469 "NFP of L-shape should have >= 6 vertices, got {}",
1470 nfp.vertex_count()
1471 );
1472 }
1473
1474 #[test]
1475 fn test_handle_perfect_fit_filters_visited() {
1476 let stationary = rect(10.0, 10.0);
1477 let orbiting: Vec<(f64, f64)> =
1478 rect(5.0, 5.0).iter().map(|&(x, y)| (x, y + 10.0)).collect();
1479
1480 let group = create_touching_group(&stationary, &orbiting, (0.0, 10.0), 1e-4);
1481
1482 let visited = vec![(0.5, 10.0), (0.0, 10.5)];
1484
1485 let vectors = handle_perfect_fit(&group, &stationary, &orbiting, &visited, 1e-4);
1486
1487 let unfiltered = compute_translation_vectors(&group, &stationary, &orbiting);
1490
1491 assert!(vectors.len() <= unfiltered.len());
1494 }
1495
1496 #[test]
1497 fn test_detect_interlocking_concave_polygon() {
1498 let stationary = vec![
1500 (0.0, 0.0),
1501 (20.0, 0.0),
1502 (20.0, 5.0),
1503 (5.0, 5.0),
1504 (5.0, 15.0),
1505 (20.0, 15.0),
1506 (20.0, 20.0),
1507 (0.0, 20.0),
1508 ];
1509
1510 let orbiting = rect(3.0, 3.0);
1512
1513 let current_pos = (25.0, 10.0);
1515
1516 let result = detect_interlocking_opportunity(&stationary, &orbiting, current_pos, 1e-4);
1518
1519 let _ = result;
1522 }
1523
1524 #[test]
1525 fn test_sliding_nfp_same_size_squares() {
1526 let stationary = rect(10.0, 10.0);
1528 let orbiting = rect(10.0, 10.0);
1529
1530 let config = SlidingNfpConfig {
1531 contact_tolerance: 1e-4,
1532 max_iterations: 1000,
1533 min_translation: 1e-6,
1534 };
1535
1536 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1537
1538 assert!(result.is_ok(), "NFP of same-size squares should succeed");
1539 let nfp = result.unwrap();
1540
1541 assert!(nfp.vertex_count() >= 4);
1544 }
1545
1546 #[test]
1547 fn test_signed_area_ccw() {
1548 let ccw = rect(10.0, 10.0);
1549 let area = signed_area(&ccw);
1550 assert!(area > 0.0, "CCW polygon should have positive signed area");
1551 }
1552
1553 #[test]
1554 fn test_signed_area_cw() {
1555 let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
1556 let area = signed_area(&cw);
1557 assert!(area < 0.0, "CW polygon should have negative signed area");
1558 }
1559}