1use u_nesting_core::robust::orient2d_filtered;
26use u_nesting_core::{Error, Result};
27
28use crate::nfp::Nfp;
29use crate::placement_utils::polygon_centroid;
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ContactType {
38 VertexEdge,
40 EdgeVertex,
42 EdgeEdge,
44}
45
46#[derive(Debug, Clone)]
48pub struct Contact {
49 pub contact_type: ContactType,
51 pub stationary_idx: usize,
53 pub orbiting_idx: usize,
55 pub point: (f64, f64),
57}
58
59#[derive(Debug, Clone)]
64pub struct TouchingGroup {
65 pub contacts: Vec<Contact>,
67 pub reference_position: (f64, f64),
69}
70
71impl TouchingGroup {
72 pub fn new(reference_position: (f64, f64)) -> Self {
74 Self {
75 contacts: Vec::new(),
76 reference_position,
77 }
78 }
79
80 pub fn add_contact(&mut self, contact: Contact) {
82 self.contacts.push(contact);
83 }
84
85 pub fn has_contacts(&self) -> bool {
87 !self.contacts.is_empty()
88 }
89
90 pub fn contact_count(&self) -> usize {
92 self.contacts.len()
93 }
94}
95
96#[derive(Debug, Clone)]
102pub struct TranslationVector {
103 pub direction: (f64, f64),
105 pub max_distance: f64,
107 pub source_contact: Contact,
109}
110
111impl TranslationVector {
112 pub fn new(direction: (f64, f64), max_distance: f64, source_contact: Contact) -> Self {
114 Self {
115 direction,
116 max_distance,
117 source_contact,
118 }
119 }
120
121 pub fn translation(&self) -> (f64, f64) {
123 (
124 self.direction.0 * self.max_distance,
125 self.direction.1 * self.max_distance,
126 )
127 }
128}
129
130#[inline]
136fn edge_vector(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
137 let n = polygon.len();
138 let p1 = polygon[i];
139 let p2 = polygon[(i + 1) % n];
140 (p2.0 - p1.0, p2.1 - p1.1)
141}
142
143#[inline]
145#[allow(dead_code)]
146fn edge_normal(polygon: &[(f64, f64)], i: usize) -> (f64, f64) {
147 let (dx, dy) = edge_vector(polygon, i);
148 let len = (dx * dx + dy * dy).sqrt();
149 if len < 1e-10 {
150 (0.0, 0.0)
151 } else {
152 (dy / len, -dx / len)
154 }
155}
156
157#[inline]
159fn normalize(v: (f64, f64)) -> (f64, f64) {
160 let len = (v.0 * v.0 + v.1 * v.1).sqrt();
161 if len < 1e-10 {
162 (0.0, 0.0)
163 } else {
164 (v.0 / len, v.1 / len)
165 }
166}
167
168#[inline]
170fn dot(a: (f64, f64), b: (f64, f64)) -> f64 {
171 a.0 * b.0 + a.1 * b.1
172}
173
174#[inline]
176fn cross(a: (f64, f64), b: (f64, f64)) -> f64 {
177 a.0 * b.1 - a.1 * b.0
178}
179
180#[inline]
182fn distance(a: (f64, f64), b: (f64, f64)) -> f64 {
183 let dx = b.0 - a.0;
184 let dy = b.1 - a.1;
185 (dx * dx + dy * dy).sqrt()
186}
187
188#[inline]
190fn edges_parallel(e1: (f64, f64), e2: (f64, f64)) -> bool {
191 let c = cross(e1, e2).abs();
192 let len1 = (e1.0 * e1.0 + e1.1 * e1.1).sqrt();
193 let len2 = (e2.0 * e2.0 + e2.1 * e2.1).sqrt();
194 c < 1e-10 * len1 * len2
195}
196
197fn project_point_to_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
200 let dx = p2.0 - p1.0;
201 let dy = p2.1 - p1.1;
202 let len_sq = dx * dx + dy * dy;
203 if len_sq < 1e-20 {
204 return 0.0;
205 }
206 let t = ((point.0 - p1.0) * dx + (point.1 - p1.1) * dy) / len_sq;
207 t.clamp(0.0, 1.0)
208}
209
210fn point_to_segment_distance(point: (f64, f64), p1: (f64, f64), p2: (f64, f64)) -> f64 {
212 let t = project_point_to_segment(point, p1, p2);
213 let proj = (p1.0 + t * (p2.0 - p1.0), p1.1 + t * (p2.1 - p1.1));
214 distance(point, proj)
215}
216
217fn point_on_segment(point: (f64, f64), p1: (f64, f64), p2: (f64, f64), tol: f64) -> bool {
219 point_to_segment_distance(point, p1, p2) < tol
220}
221
222pub fn find_contacts(
234 stationary: &[(f64, f64)],
235 orbiting: &[(f64, f64)],
236 tolerance: f64,
237) -> Vec<Contact> {
238 let mut contacts = Vec::new();
239
240 let n_stat = stationary.len();
241 let n_orb = orbiting.len();
242
243 for (orb_idx, &orb_vertex) in orbiting.iter().enumerate() {
245 for stat_edge_idx in 0..n_stat {
246 let stat_p1 = stationary[stat_edge_idx];
247 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
248 if point_on_segment(orb_vertex, stat_p1, stat_p2, tolerance) {
249 contacts.push(Contact {
250 contact_type: ContactType::VertexEdge,
251 stationary_idx: stat_edge_idx,
252 orbiting_idx: orb_idx,
253 point: orb_vertex,
254 });
255 }
256 }
257 }
258
259 for (stat_idx, &stat_vertex) in stationary.iter().enumerate() {
261 for orb_edge_idx in 0..n_orb {
262 let orb_p1 = orbiting[orb_edge_idx];
263 let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
264 if point_on_segment(stat_vertex, orb_p1, orb_p2, tolerance) {
265 contacts.push(Contact {
266 contact_type: ContactType::EdgeVertex,
267 stationary_idx: stat_idx,
268 orbiting_idx: orb_edge_idx,
269 point: stat_vertex,
270 });
271 }
272 }
273 }
274
275 for stat_edge_idx in 0..n_stat {
277 let stat_p1 = stationary[stat_edge_idx];
278 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
279 let stat_edge = (stat_p2.0 - stat_p1.0, stat_p2.1 - stat_p1.1);
280
281 for orb_edge_idx in 0..n_orb {
282 let orb_p1 = orbiting[orb_edge_idx];
283 let orb_p2 = orbiting[(orb_edge_idx + 1) % n_orb];
284 let orb_edge = (orb_p2.0 - orb_p1.0, orb_p2.1 - orb_p1.1);
285
286 if edges_parallel(stat_edge, orb_edge) {
288 let d1 = point_to_segment_distance(orb_p1, stat_p1, stat_p2);
290 let d2 = point_to_segment_distance(orb_p2, stat_p1, stat_p2);
291 if d1 < tolerance && d2 < tolerance {
292 let mid = ((orb_p1.0 + orb_p2.0) / 2.0, (orb_p1.1 + orb_p2.1) / 2.0);
295 contacts.push(Contact {
296 contact_type: ContactType::EdgeEdge,
297 stationary_idx: stat_edge_idx,
298 orbiting_idx: orb_edge_idx,
299 point: mid,
300 });
301 }
302 }
303 }
304 }
305
306 contacts
307}
308
309pub fn create_touching_group(
311 stationary: &[(f64, f64)],
312 orbiting: &[(f64, f64)],
313 reference_position: (f64, f64),
314 tolerance: f64,
315) -> TouchingGroup {
316 let contacts = find_contacts(stationary, orbiting, tolerance);
317 let mut group = TouchingGroup::new(reference_position);
318 for contact in contacts {
319 group.add_contact(contact);
320 }
321 group
322}
323
324pub fn compute_translation_vectors(
333 touching_group: &TouchingGroup,
334 stationary: &[(f64, f64)],
335 orbiting: &[(f64, f64)],
336) -> Vec<TranslationVector> {
337 let mut vectors = Vec::new();
338
339 for contact in &touching_group.contacts {
340 match contact.contact_type {
341 ContactType::VertexEdge => {
342 let edge = edge_vector(stationary, contact.stationary_idx);
345 let dir = normalize(edge);
346 let neg_dir = (-dir.0, -dir.1);
347
348 let n = stationary.len();
350 let edge_end = stationary[(contact.stationary_idx + 1) % n];
351 let edge_start = stationary[contact.stationary_idx];
352 let max_dist_pos = distance(contact.point, edge_end);
353 let max_dist_neg = distance(contact.point, edge_start);
354
355 vectors.push(TranslationVector::new(dir, max_dist_pos, contact.clone()));
356 vectors.push(TranslationVector::new(
357 neg_dir,
358 max_dist_neg,
359 contact.clone(),
360 ));
361 }
362 ContactType::EdgeVertex => {
363 let edge = edge_vector(orbiting, contact.orbiting_idx);
366 let dir = normalize(edge);
367 let neg_dir = (-dir.0, -dir.1);
368
369 let n = orbiting.len();
371 let orb_edge_end = orbiting[(contact.orbiting_idx + 1) % n];
372 let orb_edge_start = orbiting[contact.orbiting_idx];
373
374 let dist_to_end = distance(contact.point, orb_edge_end);
377 let dist_to_start = distance(contact.point, orb_edge_start);
378
379 vectors.push(TranslationVector::new(
380 neg_dir,
381 dist_to_end,
382 contact.clone(),
383 ));
384 vectors.push(TranslationVector::new(dir, dist_to_start, contact.clone()));
385 }
386 ContactType::EdgeEdge => {
387 let edge = edge_vector(stationary, contact.stationary_idx);
390 let dir = normalize(edge);
391 let neg_dir = (-dir.0, -dir.1);
392
393 let stat_edge_len = {
395 let n = stationary.len();
396 distance(
397 stationary[contact.stationary_idx],
398 stationary[(contact.stationary_idx + 1) % n],
399 )
400 };
401 let orb_edge_len = {
402 let n = orbiting.len();
403 distance(
404 orbiting[contact.orbiting_idx],
405 orbiting[(contact.orbiting_idx + 1) % n],
406 )
407 };
408 let max_dist = stat_edge_len + orb_edge_len;
409
410 vectors.push(TranslationVector::new(dir, max_dist, contact.clone()));
411 vectors.push(TranslationVector::new(neg_dir, max_dist, contact.clone()));
412 }
413 }
414 }
415
416 vectors
417}
418
419pub fn select_translation_vector(
424 vectors: &[TranslationVector],
425 previous_direction: Option<(f64, f64)>,
426 stationary_centroid: (f64, f64),
427 orbiting_position: (f64, f64),
428) -> Option<&TranslationVector> {
429 if vectors.is_empty() {
430 return None;
431 }
432
433 let radial = normalize((
435 orbiting_position.0 - stationary_centroid.0,
436 orbiting_position.1 - stationary_centroid.1,
437 ));
438
439 let ccw_preferred = (-radial.1, radial.0);
441
442 let mut best_idx = 0;
444 let mut best_score = f64::NEG_INFINITY;
445
446 for (i, v) in vectors.iter().enumerate() {
447 let mut score = dot(v.direction, ccw_preferred);
448
449 if let Some(prev) = previous_direction {
451 score += 0.5 * dot(v.direction, prev);
452 }
453
454 if v.max_distance < 1e-6 {
456 score -= 100.0;
457 }
458
459 if score > best_score {
460 best_score = score;
461 best_idx = i;
462 }
463 }
464
465 Some(&vectors[best_idx])
466}
467
468#[derive(Debug, Clone)]
474pub struct CollisionEvent {
475 pub distance: f64,
477 pub contact_type: ContactType,
479 pub stationary_idx: usize,
481 pub orbiting_idx: usize,
483}
484
485pub fn check_translation_collision(
494 stationary: &[(f64, f64)],
495 orbiting: &[(f64, f64)],
496 translation: (f64, f64),
497 tolerance: f64,
498) -> Option<CollisionEvent> {
499 let trans_len = (translation.0 * translation.0 + translation.1 * translation.1).sqrt();
500 if trans_len < tolerance {
501 return None;
502 }
503 let trans_dir = (translation.0 / trans_len, translation.1 / trans_len);
504
505 let n_stat = stationary.len();
506 let n_orb = orbiting.len();
507
508 let mut first_collision: Option<CollisionEvent> = None;
509
510 for (orb_idx, &orb_v) in orbiting.iter().enumerate() {
512 for stat_edge_idx in 0..n_stat {
513 let stat_p1 = stationary[stat_edge_idx];
514 let stat_p2 = stationary[(stat_edge_idx + 1) % n_stat];
515
516 if let Some(dist) =
517 ray_segment_intersection(orb_v, trans_dir, stat_p1, stat_p2, tolerance)
518 {
519 if dist > tolerance
520 && dist < trans_len - tolerance
521 && first_collision.as_ref().is_none_or(|c| dist < c.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.as_ref().is_none_or(|c| dist < c.distance)
548 {
549 first_collision = Some(CollisionEvent {
550 distance: dist,
551 contact_type: ContactType::EdgeVertex,
552 stationary_idx: stat_idx,
553 orbiting_idx: orb_edge_idx,
554 });
555 }
556 }
557 }
558 }
559
560 first_collision
561}
562
563fn ray_segment_intersection(
568 ray_origin: (f64, f64),
569 ray_dir: (f64, f64),
570 seg_p1: (f64, f64),
571 seg_p2: (f64, f64),
572 tolerance: f64,
573) -> Option<f64> {
574 let seg_dir = (seg_p2.0 - seg_p1.0, seg_p2.1 - seg_p1.1);
575 let denominator = cross(ray_dir, seg_dir);
576
577 if denominator.abs() < tolerance {
579 return None;
580 }
581
582 let diff = (seg_p1.0 - ray_origin.0, seg_p1.1 - ray_origin.1);
584
585 let t = cross(diff, seg_dir) / denominator;
592 let u = cross(diff, ray_dir) / denominator;
593
594 if t >= -tolerance && u >= -tolerance && u <= 1.0 + tolerance {
598 Some(t.max(0.0))
599 } else {
600 None
601 }
602}
603
604pub fn handle_perfect_fit(
609 touching_group: &TouchingGroup,
610 stationary: &[(f64, f64)],
611 orbiting: &[(f64, f64)],
612 visited_positions: &[(f64, f64)],
613 tolerance: f64,
614) -> Vec<TranslationVector> {
615 let mut vectors = compute_translation_vectors(touching_group, stationary, orbiting);
616
617 vectors.retain(|v| {
619 let potential_pos = (
620 touching_group.reference_position.0 + v.direction.0 * v.max_distance.min(1.0),
621 touching_group.reference_position.1 + v.direction.1 * v.max_distance.min(1.0),
622 );
623
624 !visited_positions
625 .iter()
626 .any(|&visited| distance(potential_pos, visited) < tolerance * 10.0)
627 });
628
629 vectors
630}
631
632pub fn detect_interlocking_opportunity(
638 stationary: &[(f64, f64)],
639 orbiting: &[(f64, f64)],
640 current_pos: (f64, f64),
641 tolerance: f64,
642) -> Option<(f64, f64)> {
643 let n_stat = stationary.len();
645 let mut concave_vertices = Vec::new();
646
647 for i in 0..n_stat {
648 let prev = stationary[if i == 0 { n_stat - 1 } else { i - 1 }];
649 let curr = stationary[i];
650 let next = stationary[(i + 1) % n_stat];
651
652 let orientation = orient2d_filtered(prev, curr, next);
654 if orientation.is_cw() {
655 concave_vertices.push((i, curr));
656 }
657 }
658
659 if concave_vertices.is_empty() {
660 return None;
661 }
662
663 let orb_bbox = compute_bbox(orbiting);
665 let orb_width = orb_bbox.1 .0 - orb_bbox.0 .0;
666 let orb_height = orb_bbox.1 .1 - orb_bbox.0 .1;
667
668 for (idx, vertex) in concave_vertices {
669 let prev_idx = if idx == 0 { n_stat - 1 } else { idx - 1 };
671 let _next_idx = (idx + 1) % n_stat;
672
673 let prev_edge = edge_vector(stationary, prev_idx);
674 let next_edge = edge_vector(stationary, idx);
675
676 let opening_width = (dot(prev_edge, next_edge).abs()).sqrt();
678
679 let min_dim = orb_width.min(orb_height);
681 if opening_width > min_dim * 0.5 {
682 let dir = normalize((vertex.0 - current_pos.0, vertex.1 - current_pos.1));
685 let test_pos = (
686 current_pos.0 + dir.0 * tolerance,
687 current_pos.1 + dir.1 * tolerance,
688 );
689
690 let orbiting_test: Vec<(f64, f64)> = orbiting
692 .iter()
693 .map(|&(x, y)| (x + test_pos.0, y + test_pos.1))
694 .collect();
695
696 if !polygons_overlap(stationary, &orbiting_test) {
697 return Some(test_pos);
698 }
699 }
700 }
701
702 None
703}
704
705fn compute_bbox(polygon: &[(f64, f64)]) -> ((f64, f64), (f64, f64)) {
707 let min_x = polygon
708 .iter()
709 .map(|p| p.0)
710 .fold(f64::INFINITY, |a, b| a.min(b));
711 let max_x = polygon
712 .iter()
713 .map(|p| p.0)
714 .fold(f64::NEG_INFINITY, |a, b| a.max(b));
715 let min_y = polygon
716 .iter()
717 .map(|p| p.1)
718 .fold(f64::INFINITY, |a, b| a.min(b));
719 let max_y = polygon
720 .iter()
721 .map(|p| p.1)
722 .fold(f64::NEG_INFINITY, |a, b| a.max(b));
723 ((min_x, min_y), (max_x, max_y))
724}
725
726pub fn polygons_overlap(poly_a: &[(f64, f64)], poly_b: &[(f64, f64)]) -> bool {
730 polygons_overlap_with_tolerance(poly_a, poly_b, 1e-6)
731}
732
733pub fn polygons_overlap_with_tolerance(
741 poly_a: &[(f64, f64)],
742 poly_b: &[(f64, f64)],
743 tolerance: f64,
744) -> bool {
745 let bbox_a = compute_bbox(poly_a);
747 let bbox_b = compute_bbox(poly_b);
748
749 if bbox_a.1 .0 + tolerance < bbox_b.0 .0
750 || bbox_b.1 .0 + tolerance < bbox_a.0 .0
751 || bbox_a.1 .1 + tolerance < bbox_b.0 .1
752 || bbox_b.1 .1 + tolerance < bbox_a.0 .1
753 {
754 return false;
755 }
756
757 for polygon in [poly_a, poly_b] {
759 let n = polygon.len();
760 for i in 0..n {
761 let edge = edge_vector(polygon, i);
762 let axis = normalize((-edge.1, edge.0)); if axis.0.abs() < 1e-10 && axis.1.abs() < 1e-10 {
765 continue;
766 }
767
768 let (min_a, max_a) = project_polygon_on_axis(poly_a, axis);
770 let (min_b, max_b) = project_polygon_on_axis(poly_b, axis);
771
772 if max_a + tolerance < min_b || max_b + tolerance < min_a {
775 return false; }
777 }
778 }
779
780 true }
782
783fn project_polygon_on_axis(polygon: &[(f64, f64)], axis: (f64, f64)) -> (f64, f64) {
785 let mut min_proj = f64::INFINITY;
786 let mut max_proj = f64::NEG_INFINITY;
787
788 for &p in polygon {
789 let proj = dot(p, axis);
790 min_proj = min_proj.min(proj);
791 max_proj = max_proj.max(proj);
792 }
793
794 (min_proj, max_proj)
795}
796
797#[derive(Debug, Clone)]
803pub struct SlidingNfpConfig {
804 pub contact_tolerance: f64,
806 pub max_iterations: usize,
808 pub min_translation: f64,
810}
811
812impl Default for SlidingNfpConfig {
813 fn default() -> Self {
814 Self {
815 contact_tolerance: 1e-6,
816 max_iterations: 10000,
817 min_translation: 1e-8,
818 }
819 }
820}
821
822pub fn compute_nfp_sliding(
832 stationary: &[(f64, f64)],
833 orbiting: &[(f64, f64)],
834 config: &SlidingNfpConfig,
835) -> Result<Nfp> {
836 if stationary.len() < 3 || orbiting.len() < 3 {
837 return Err(Error::InvalidGeometry(
838 "Polygons must have at least 3 vertices".into(),
839 ));
840 }
841
842 let stationary = ensure_ccw(stationary);
844 let orbiting = ensure_ccw(orbiting);
845
846 let start_pos = find_start_position(&stationary, &orbiting)?;
848
849 let orbiting_at_start: Vec<(f64, f64)> = orbiting
851 .iter()
852 .map(|&(x, y)| (x + start_pos.0, y + start_pos.1))
853 .collect();
854
855 let nfp_path = trace_nfp_boundary(
857 &stationary,
858 &orbiting,
859 &orbiting_at_start,
860 start_pos,
861 config,
862 )?;
863
864 if nfp_path.len() < 3 {
865 return Err(Error::Internal("NFP path has fewer than 3 vertices".into()));
866 }
867
868 Ok(Nfp::from_polygon(nfp_path))
869}
870
871fn find_start_position(stationary: &[(f64, f64)], orbiting: &[(f64, f64)]) -> Result<(f64, f64)> {
876 let stat_bottom_idx = stationary
878 .iter()
879 .enumerate()
880 .min_by(|(_, a), (_, b)| {
881 a.1.partial_cmp(&b.1)
882 .unwrap_or(std::cmp::Ordering::Equal)
883 .then(a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal))
884 })
885 .map(|(i, _)| i)
886 .unwrap_or(0);
887 let stat_bottom = stationary[stat_bottom_idx];
888
889 let orb_top_idx = orbiting
891 .iter()
892 .enumerate()
893 .max_by(|(_, a), (_, b)| {
894 a.1.partial_cmp(&b.1)
895 .unwrap_or(std::cmp::Ordering::Equal)
896 .then(b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal))
897 })
898 .map(|(i, _)| i)
899 .unwrap_or(0);
900 let orb_top = orbiting[orb_top_idx];
901
902 let start_pos = (stat_bottom.0 - orb_top.0, stat_bottom.1 - orb_top.1);
905
906 Ok(start_pos)
907}
908
909fn trace_nfp_boundary(
911 stationary: &[(f64, f64)],
912 orbiting_original: &[(f64, f64)],
913 _orbiting_at_start: &[(f64, f64)],
914 start_pos: (f64, f64),
915 config: &SlidingNfpConfig,
916) -> Result<Vec<(f64, f64)>> {
917 let mut nfp_path = Vec::new();
918 let mut current_pos = start_pos;
919 let mut previous_direction: Option<(f64, f64)> = None;
920 let mut stuck_counter = 0;
921
922 let stat_centroid = polygon_centroid(stationary);
924
925 nfp_path.push(current_pos);
926
927 for _iteration in 0..config.max_iterations {
928 let orbiting_current: Vec<(f64, f64)> = orbiting_original
930 .iter()
931 .map(|&(x, y)| (x + current_pos.0, y + current_pos.1))
932 .collect();
933
934 let touching_group = create_touching_group(
936 stationary,
937 &orbiting_current,
938 current_pos,
939 config.contact_tolerance,
940 );
941
942 if !touching_group.has_contacts() {
943 if let Some(recovery_pos) = recover_contact(
945 stationary,
946 &orbiting_current,
947 current_pos,
948 config.contact_tolerance,
949 ) {
950 current_pos = recovery_pos;
951 continue;
952 }
953 break;
954 }
955
956 let translation_vectors = handle_perfect_fit(
958 &touching_group,
959 stationary,
960 &orbiting_current,
961 &nfp_path,
962 config.contact_tolerance,
963 );
964
965 if translation_vectors.is_empty() {
966 stuck_counter += 1;
968 if stuck_counter > 3 {
969 break;
970 }
971 if let Some(interlock_pos) = detect_interlocking_opportunity(
973 stationary,
974 orbiting_original,
975 current_pos,
976 config.contact_tolerance,
977 ) {
978 if distance(interlock_pos, current_pos) > config.min_translation {
979 nfp_path.push(interlock_pos);
980 current_pos = interlock_pos;
981 continue;
982 }
983 }
984 break;
985 }
986 stuck_counter = 0;
987
988 let selected = select_translation_vector(
990 &translation_vectors,
991 previous_direction,
992 stat_centroid,
993 current_pos,
994 );
995
996 let Some(tv) = selected else {
997 break;
998 };
999
1000 let intended_distance = tv.max_distance.min(1000.0); if intended_distance < config.min_translation {
1003 break;
1004 }
1005
1006 let intended_translation = (
1007 tv.direction.0 * intended_distance,
1008 tv.direction.1 * intended_distance,
1009 );
1010
1011 let actual_translation = if let Some(collision) = check_translation_collision(
1013 stationary,
1014 &orbiting_current,
1015 intended_translation,
1016 config.contact_tolerance,
1017 ) {
1018 let blocked_dist = (collision.distance - config.contact_tolerance).max(0.0);
1020 (tv.direction.0 * blocked_dist, tv.direction.1 * blocked_dist)
1021 } else {
1022 intended_translation
1023 };
1024
1025 let new_pos = (
1026 current_pos.0 + actual_translation.0,
1027 current_pos.1 + actual_translation.1,
1028 );
1029
1030 let dist_to_start = distance(new_pos, start_pos);
1032 if nfp_path.len() > 2 && dist_to_start < config.contact_tolerance * 10.0 {
1033 break;
1035 }
1036
1037 let dist_to_last = distance(new_pos, current_pos);
1039 if dist_to_last > config.min_translation {
1040 nfp_path.push(new_pos);
1041 previous_direction = Some(tv.direction);
1042 }
1043
1044 current_pos = new_pos;
1045 }
1046
1047 simplify_polygon(&nfp_path, config.contact_tolerance)
1049}
1050
1051fn recover_contact(
1053 stationary: &[(f64, f64)],
1054 orbiting: &[(f64, f64)],
1055 current_pos: (f64, f64),
1056 tolerance: f64,
1057) -> Option<(f64, f64)> {
1058 let mut min_dist = f64::INFINITY;
1060 let mut closest_point = None;
1061
1062 let n_stat = stationary.len();
1063
1064 for orb_v in orbiting {
1065 for i in 0..n_stat {
1066 let stat_p1 = stationary[i];
1067 let stat_p2 = stationary[(i + 1) % n_stat];
1068
1069 let t = project_point_to_segment(*orb_v, stat_p1, stat_p2);
1070 let proj = (
1071 stat_p1.0 + t * (stat_p2.0 - stat_p1.0),
1072 stat_p1.1 + t * (stat_p2.1 - stat_p1.1),
1073 );
1074
1075 let dist = distance(*orb_v, proj);
1076 if dist < min_dist {
1077 min_dist = dist;
1078 closest_point = Some(proj);
1079 }
1080 }
1081 }
1082
1083 if let Some(target) = closest_point {
1085 if min_dist > tolerance {
1086 let orb_centroid = polygon_centroid(orbiting);
1088 let dir = normalize((target.0 - orb_centroid.0, target.1 - orb_centroid.1));
1089 let move_dist = min_dist - tolerance * 0.5;
1090 return Some((
1091 current_pos.0 + dir.0 * move_dist,
1092 current_pos.1 + dir.1 * move_dist,
1093 ));
1094 }
1095 }
1096
1097 None
1098}
1099
1100fn ensure_ccw(polygon: &[(f64, f64)]) -> Vec<(f64, f64)> {
1102 let area = signed_area(polygon);
1103 if area < 0.0 {
1104 polygon.iter().rev().cloned().collect()
1105 } else {
1106 polygon.to_vec()
1107 }
1108}
1109
1110fn signed_area(polygon: &[(f64, f64)]) -> f64 {
1112 let n = polygon.len();
1113 let mut area = 0.0;
1114 for i in 0..n {
1115 let j = (i + 1) % n;
1116 area += polygon[i].0 * polygon[j].1;
1117 area -= polygon[j].0 * polygon[i].1;
1118 }
1119 area / 2.0
1120}
1121
1122fn simplify_polygon(polygon: &[(f64, f64)], tolerance: f64) -> Result<Vec<(f64, f64)>> {
1124 if polygon.len() < 3 {
1125 return Ok(polygon.to_vec());
1126 }
1127
1128 let mut result = Vec::new();
1129 let n = polygon.len();
1130
1131 for i in 0..n {
1132 let prev = if i == 0 { n - 1 } else { i - 1 };
1133 let next = (i + 1) % n;
1134
1135 let p_prev = polygon[prev];
1136 let p_curr = polygon[i];
1137 let p_next = polygon[next];
1138
1139 let orientation = orient2d_filtered(p_prev, p_curr, p_next);
1141 if !orientation.is_collinear() {
1142 result.push(p_curr);
1143 } else {
1144 let dist = point_to_segment_distance(p_curr, p_prev, p_next);
1146 if dist > tolerance {
1147 result.push(p_curr);
1148 }
1149 }
1150 }
1151
1152 if result.len() < 3 {
1153 Ok(polygon.to_vec())
1155 } else {
1156 Ok(result)
1157 }
1158}
1159
1160#[cfg(test)]
1165mod tests {
1166 use super::*;
1167
1168 fn rect(w: f64, h: f64) -> Vec<(f64, f64)> {
1169 vec![(0.0, 0.0), (w, 0.0), (w, h), (0.0, h)]
1170 }
1171
1172 #[test]
1173 fn test_edge_vector() {
1174 let square = rect(10.0, 10.0);
1175 let e0 = edge_vector(&square, 0);
1176 assert!((e0.0 - 10.0).abs() < 1e-10);
1177 assert!(e0.1.abs() < 1e-10);
1178 }
1179
1180 #[test]
1181 fn test_edge_normal() {
1182 let square = rect(10.0, 10.0);
1183 let n0 = edge_normal(&square, 0);
1185 assert!(n0.0.abs() < 1e-10);
1186 assert!((n0.1 - (-1.0)).abs() < 1e-10);
1187 }
1188
1189 #[test]
1190 fn test_edges_parallel() {
1191 assert!(edges_parallel((1.0, 0.0), (2.0, 0.0)));
1192 assert!(edges_parallel((1.0, 0.0), (-1.0, 0.0)));
1193 assert!(!edges_parallel((1.0, 0.0), (0.0, 1.0)));
1194 }
1195
1196 #[test]
1197 fn test_point_on_segment() {
1198 let p1 = (0.0, 0.0);
1199 let p2 = (10.0, 0.0);
1200
1201 assert!(point_on_segment((5.0, 0.0), p1, p2, 1e-6));
1202 assert!(point_on_segment((0.0, 0.0), p1, p2, 1e-6));
1203 assert!(point_on_segment((10.0, 0.0), p1, p2, 1e-6));
1204 assert!(!point_on_segment((5.0, 1.0), p1, p2, 1e-6));
1205 }
1206
1207 #[test]
1208 fn test_find_contacts_ve() {
1209 let stationary = rect(10.0, 10.0);
1211 let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
1212 .iter()
1213 .map(|&(x, y)| (x + 5.0, y + 10.0)) .collect();
1215
1216 let contacts = find_contacts(&stationary, &orbiting, 1e-6);
1217
1218 assert!(!contacts.is_empty(), "Should find at least one contact");
1220 }
1221
1222 #[test]
1223 fn test_find_start_position() {
1224 let stationary = rect(10.0, 10.0);
1225 let orbiting = rect(5.0, 5.0);
1226
1227 let start = find_start_position(&stationary, &orbiting).unwrap();
1228
1229 assert!(
1235 (start.1 - (-5.0)).abs() < 1e-6,
1236 "Start Y should be -5, got {}",
1237 start.1
1238 );
1239 }
1240
1241 #[test]
1242 fn test_touching_group() {
1243 let ref_pos = (5.0, 5.0);
1244 let mut group = TouchingGroup::new(ref_pos);
1245
1246 assert!(!group.has_contacts());
1247 assert_eq!(group.contact_count(), 0);
1248
1249 group.add_contact(Contact {
1250 contact_type: ContactType::VertexEdge,
1251 stationary_idx: 0,
1252 orbiting_idx: 0,
1253 point: (5.0, 5.0),
1254 });
1255
1256 assert!(group.has_contacts());
1257 assert_eq!(group.contact_count(), 1);
1258 }
1259
1260 #[test]
1261 fn test_compute_translation_vectors() {
1262 let stationary = rect(10.0, 10.0);
1263 let orbiting: Vec<(f64, f64)> = rect(5.0, 5.0)
1264 .iter()
1265 .map(|&(x, y)| (x + 2.5, y + 10.0)) .collect();
1267
1268 let group = create_touching_group(&stationary, &orbiting, (2.5, 10.0), 1e-6);
1269 let vectors = compute_translation_vectors(&group, &stationary, &orbiting);
1270
1271 assert!(!vectors.is_empty(), "Should have translation vectors");
1273 }
1274
1275 #[test]
1276 fn test_sliding_nfp_two_squares() {
1277 let stationary = rect(10.0, 10.0);
1278 let orbiting = rect(5.0, 5.0);
1279
1280 let config = SlidingNfpConfig {
1281 contact_tolerance: 1e-4,
1282 max_iterations: 1000,
1283 min_translation: 1e-6,
1284 };
1285
1286 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1287
1288 assert!(result.is_ok(), "NFP computation failed: {:?}", result.err());
1290
1291 let nfp = result.unwrap();
1292 assert!(!nfp.is_empty());
1293
1294 assert!(nfp.vertex_count() >= 4);
1296 }
1297
1298 #[test]
1299 fn test_ensure_ccw() {
1300 let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
1302 let ccw = ensure_ccw(&cw);
1303
1304 assert!(signed_area(&ccw) > 0.0);
1306 }
1307
1308 #[test]
1309 fn test_simplify_polygon() {
1310 let with_extra = vec![
1312 (0.0, 0.0),
1313 (5.0, 0.0), (10.0, 0.0),
1315 (10.0, 10.0),
1316 (0.0, 10.0),
1317 ];
1318
1319 let simplified = simplify_polygon(&with_extra, 1e-6).unwrap();
1320
1321 assert_eq!(simplified.len(), 4, "Simplified should have 4 vertices");
1323 }
1324
1325 #[test]
1326 fn test_polygon_centroid() {
1327 let square = rect(10.0, 10.0);
1328 let centroid = polygon_centroid(&square);
1329
1330 assert!((centroid.0 - 5.0).abs() < 1e-10);
1331 assert!((centroid.1 - 5.0).abs() < 1e-10);
1332 }
1333
1334 #[test]
1339 fn test_ray_segment_intersection() {
1340 let result =
1342 ray_segment_intersection((0.0, 0.0), (1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
1343 assert!(result.is_some());
1344 assert!((result.unwrap() - 5.0).abs() < 1e-6);
1345
1346 let result =
1348 ray_segment_intersection((0.0, 0.0), (-1.0, 0.0), (5.0, -1.0), (5.0, 1.0), 1e-6);
1349 assert!(result.is_none());
1350 }
1351
1352 #[test]
1353 fn test_check_translation_collision() {
1354 let stationary = vec![(0.0, 0.0), (10.0, 0.0), (10.0, 10.0), (0.0, 10.0)];
1356
1357 let orbiting = vec![(15.0, 2.0), (20.0, 2.0), (20.0, 8.0), (15.0, 8.0)];
1359
1360 let translation = (-10.0, 0.0);
1362 let collision = check_translation_collision(&stationary, &orbiting, translation, 1e-6);
1363
1364 assert!(
1367 collision.is_some(),
1368 "Should detect collision when moving left"
1369 );
1370
1371 if let Some(c) = collision {
1372 assert!(
1374 c.distance > 4.0 && c.distance < 6.0,
1375 "Collision distance should be ~5, got {}",
1376 c.distance
1377 );
1378 }
1379 }
1380
1381 #[test]
1382 fn test_polygons_overlap_true() {
1383 let a = rect(10.0, 10.0);
1385 let b: Vec<(f64, f64)> = rect(10.0, 10.0)
1386 .iter()
1387 .map(|&(x, y)| (x + 5.0, y + 5.0))
1388 .collect();
1389
1390 assert!(
1391 polygons_overlap(&a, &b),
1392 "Overlapping squares should overlap"
1393 );
1394 }
1395
1396 #[test]
1397 fn test_polygons_overlap_false() {
1398 let a = rect(10.0, 10.0);
1400 let b: Vec<(f64, f64)> = rect(10.0, 10.0)
1401 .iter()
1402 .map(|&(x, y)| (x + 20.0, y))
1403 .collect();
1404
1405 assert!(
1406 !polygons_overlap(&a, &b),
1407 "Separated squares should not overlap"
1408 );
1409 }
1410
1411 #[test]
1412 fn test_compute_bbox() {
1413 let square = rect(10.0, 10.0);
1414 let bbox = compute_bbox(&square);
1415
1416 assert!((bbox.0 .0 - 0.0).abs() < 1e-10);
1417 assert!((bbox.0 .1 - 0.0).abs() < 1e-10);
1418 assert!((bbox.1 .0 - 10.0).abs() < 1e-10);
1419 assert!((bbox.1 .1 - 10.0).abs() < 1e-10);
1420 }
1421
1422 #[test]
1423 fn test_sliding_nfp_triangle() {
1424 let stationary = vec![(0.0, 0.0), (10.0, 0.0), (5.0, 8.66)];
1426
1427 let orbiting = rect(3.0, 3.0);
1429
1430 let config = SlidingNfpConfig {
1431 contact_tolerance: 1e-4,
1432 max_iterations: 1000,
1433 min_translation: 1e-6,
1434 };
1435
1436 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1437
1438 assert!(result.is_ok(), "NFP of triangle and square should succeed");
1439 let nfp = result.unwrap();
1440 assert!(
1441 nfp.vertex_count() >= 3,
1442 "NFP should have at least 3 vertices"
1443 );
1444 }
1445
1446 #[test]
1447 fn test_sliding_nfp_l_shape() {
1448 let stationary = vec![
1450 (0.0, 0.0),
1451 (20.0, 0.0),
1452 (20.0, 5.0),
1453 (5.0, 5.0),
1454 (5.0, 20.0),
1455 (0.0, 20.0),
1456 ];
1457
1458 let orbiting = rect(3.0, 3.0);
1460
1461 let config = SlidingNfpConfig {
1462 contact_tolerance: 1e-4,
1463 max_iterations: 2000,
1464 min_translation: 1e-6,
1465 };
1466
1467 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1468
1469 assert!(
1470 result.is_ok(),
1471 "NFP of L-shape and square should succeed: {:?}",
1472 result.err()
1473 );
1474 let nfp = result.unwrap();
1475 assert!(
1477 nfp.vertex_count() >= 6,
1478 "NFP of L-shape should have >= 6 vertices, got {}",
1479 nfp.vertex_count()
1480 );
1481 }
1482
1483 #[test]
1484 fn test_handle_perfect_fit_filters_visited() {
1485 let stationary = rect(10.0, 10.0);
1486 let orbiting: Vec<(f64, f64)> =
1487 rect(5.0, 5.0).iter().map(|&(x, y)| (x, y + 10.0)).collect();
1488
1489 let group = create_touching_group(&stationary, &orbiting, (0.0, 10.0), 1e-4);
1490
1491 let visited = vec![(0.5, 10.0), (0.0, 10.5)];
1493
1494 let vectors = handle_perfect_fit(&group, &stationary, &orbiting, &visited, 1e-4);
1495
1496 let unfiltered = compute_translation_vectors(&group, &stationary, &orbiting);
1499
1500 assert!(vectors.len() <= unfiltered.len());
1503 }
1504
1505 #[test]
1506 fn test_detect_interlocking_concave_polygon() {
1507 let stationary = vec![
1509 (0.0, 0.0),
1510 (20.0, 0.0),
1511 (20.0, 5.0),
1512 (5.0, 5.0),
1513 (5.0, 15.0),
1514 (20.0, 15.0),
1515 (20.0, 20.0),
1516 (0.0, 20.0),
1517 ];
1518
1519 let orbiting = rect(3.0, 3.0);
1521
1522 let current_pos = (25.0, 10.0);
1524
1525 let result = detect_interlocking_opportunity(&stationary, &orbiting, current_pos, 1e-4);
1527
1528 let _ = result;
1531 }
1532
1533 #[test]
1534 fn test_sliding_nfp_same_size_squares() {
1535 let stationary = rect(10.0, 10.0);
1537 let orbiting = rect(10.0, 10.0);
1538
1539 let config = SlidingNfpConfig {
1540 contact_tolerance: 1e-4,
1541 max_iterations: 1000,
1542 min_translation: 1e-6,
1543 };
1544
1545 let result = compute_nfp_sliding(&stationary, &orbiting, &config);
1546
1547 assert!(result.is_ok(), "NFP of same-size squares should succeed");
1548 let nfp = result.unwrap();
1549
1550 assert!(nfp.vertex_count() >= 4);
1553 }
1554
1555 #[test]
1556 fn test_signed_area_ccw() {
1557 let ccw = rect(10.0, 10.0);
1558 let area = signed_area(&ccw);
1559 assert!(area > 0.0, "CCW polygon should have positive signed area");
1560 }
1561
1562 #[test]
1563 fn test_signed_area_cw() {
1564 let cw = vec![(0.0, 0.0), (0.0, 10.0), (10.0, 10.0), (10.0, 0.0)];
1565 let area = signed_area(&cw);
1566 assert!(area < 0.0, "CW polygon should have negative signed area");
1567 }
1568}