1#[derive(Debug, Clone)]
14pub struct ContactPoint {
15 pub point: [f64; 3],
17 pub normal: [f64; 3],
19 pub depth: f64,
21}
22
23#[derive(Debug, Clone)]
27pub struct ContactManifold {
28 pub contacts: Vec<ContactPoint>,
30 pub normal: [f64; 3],
32}
33
34impl ContactManifold {
35 pub fn new(normal: [f64; 3]) -> Self {
37 Self {
38 contacts: Vec::new(),
39 normal,
40 }
41 }
42}
43
44#[inline]
47fn dot3(a: [f64; 3], b: [f64; 3]) -> f64 {
48 a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
49}
50
51#[inline]
52fn sub3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
53 [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
54}
55
56#[inline]
57fn add3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
58 [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
59}
60
61#[inline]
62fn scale3(a: [f64; 3], s: f64) -> [f64; 3] {
63 [a[0] * s, a[1] * s, a[2] * s]
64}
65
66#[inline]
67fn cross3(a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
68 [
69 a[1] * b[2] - a[2] * b[1],
70 a[2] * b[0] - a[0] * b[2],
71 a[0] * b[1] - a[1] * b[0],
72 ]
73}
74
75#[inline]
76fn norm3(a: [f64; 3]) -> f64 {
77 dot3(a, a).sqrt()
78}
79
80#[inline]
81fn normalize3(a: [f64; 3]) -> [f64; 3] {
82 let n = norm3(a);
83 if n < 1e-15 { a } else { scale3(a, 1.0 / n) }
84}
85
86pub fn sutherland_hodgman_2d(subject: &[[f64; 2]], clip: &[[f64; 2]]) -> Vec<[f64; 2]> {
95 if clip.is_empty() || subject.is_empty() {
96 return Vec::new();
97 }
98
99 let mut output: Vec<[f64; 2]> = subject.to_vec();
100
101 let n = clip.len();
102 for i in 0..n {
103 if output.is_empty() {
104 return Vec::new();
105 }
106 let input = output.clone();
107 output.clear();
108
109 let edge_start = clip[i];
110 let edge_end = clip[(i + 1) % n];
111
112 let ex = edge_end[0] - edge_start[0];
114 let ey = edge_end[1] - edge_start[1];
115
116 let inside = |p: [f64; 2]| -> bool {
118 let dx = p[0] - edge_start[0];
119 let dy = p[1] - edge_start[1];
120 ex * dy - ey * dx >= 0.0
122 };
123
124 let intersect = |s: [f64; 2], e: [f64; 2]| -> [f64; 2] {
129 let dx = e[0] - s[0]; let dy = e[1] - s[1];
131 let denom = dx * ey - dy * ex;
133 if denom.abs() < 1e-15 {
134 return s;
135 }
136 let t = ((edge_start[0] - s[0]) * ey - (edge_start[1] - s[1]) * ex) / denom;
138 [s[0] + t * dx, s[1] + t * dy]
139 };
140
141 let k = input.len();
142 for j in 0..k {
143 let current = input[j];
144 let previous = input[(j + k - 1) % k];
145
146 if inside(current) {
147 if !inside(previous) {
148 output.push(intersect(previous, current));
149 }
150 output.push(current);
151 } else if inside(previous) {
152 output.push(intersect(previous, current));
153 }
154 }
155 }
156
157 output
158}
159
160pub fn clip_polygon_by_halfspace(
169 poly: &[[f64; 3]],
170 plane_normal: [f64; 3],
171 plane_d: f64,
172) -> Vec<[f64; 3]> {
173 if poly.is_empty() {
174 return Vec::new();
175 }
176
177 let mut output = Vec::with_capacity(poly.len() + 1);
178 let n = poly.len();
179
180 for i in 0..n {
181 let current = poly[i];
182 let previous = poly[(i + n - 1) % n];
183
184 let d_cur = dot3(plane_normal, current) - plane_d;
185 let d_prev = dot3(plane_normal, previous) - plane_d;
186
187 if d_cur >= 0.0 {
188 if d_prev < 0.0 {
190 let t = d_prev / (d_prev - d_cur);
192 output.push(add3(previous, scale3(sub3(current, previous), t)));
193 }
194 output.push(current);
195 } else if d_prev >= 0.0 {
196 let t = d_prev / (d_prev - d_cur);
198 output.push(add3(previous, scale3(sub3(current, previous), t)));
199 }
200 }
201
202 output
203}
204
205pub fn project_point_to_plane(
210 pt: [f64; 3],
211 plane_origin: [f64; 3],
212 u: [f64; 3],
213 v: [f64; 3],
214) -> [f64; 2] {
215 let d = sub3(pt, plane_origin);
216 [dot3(d, u), dot3(d, v)]
217}
218
219pub fn unproject_point_from_plane(
222 pt2d: [f64; 2],
223 plane_origin: [f64; 3],
224 u: [f64; 3],
225 v: [f64; 3],
226) -> [f64; 3] {
227 add3(plane_origin, add3(scale3(u, pt2d[0]), scale3(v, pt2d[1])))
228}
229
230fn rotation_axes(t: [[f64; 4]; 4]) -> [[f64; 3]; 3] {
235 [
236 [t[0][0], t[1][0], t[2][0]],
237 [t[0][1], t[1][1], t[2][1]],
238 [t[0][2], t[1][2], t[2][2]],
239 ]
240}
241
242fn translation(t: [[f64; 4]; 4]) -> [f64; 3] {
244 [t[0][3], t[1][3], t[2][3]]
245}
246
247fn best_face(
251 center: [f64; 3],
252 axes: [[f64; 3]; 3],
253 half: [f64; 3],
254 dir: [f64; 3],
255) -> ([f64; 3], Vec<[f64; 3]>) {
256 let mut best_dot = -f64::INFINITY;
258 let mut best_axis = 0usize;
259 let mut best_sign = 1.0_f64;
260 for (i, axis) in axes.iter().enumerate() {
261 let d = dot3(*axis, dir);
262 if d.abs() > best_dot {
263 best_dot = d.abs();
264 best_axis = i;
265 best_sign = d.signum();
266 }
267 }
268
269 let face_normal = scale3(axes[best_axis], best_sign);
270
271 let u_axis = best_axis;
273 let v_axis = (best_axis + 1) % 3;
274 let w_axis = (best_axis + 2) % 3;
275
276 let fc = add3(center, scale3(face_normal, half[best_axis]));
278
279 let hu = scale3(axes[u_axis], half[u_axis]);
281 let hv = scale3(axes[w_axis], half[w_axis]);
282 let _ = v_axis; let corners = vec![
284 add3(add3(fc, hu), hv),
285 add3(sub3(fc, hu), hv),
286 sub3(sub3(fc, hu), hv),
287 add3(sub3(fc, hv), hu),
288 ];
289
290 (face_normal, corners)
291}
292
293pub fn build_box_manifold(
306 half_extents_a: [f64; 3],
307 transform_a: [[f64; 4]; 4],
308 half_extents_b: [f64; 3],
309 transform_b: [[f64; 4]; 4],
310 contact_normal: [f64; 3],
311 penetration_depth: f64,
312) -> ContactManifold {
313 let center_a = translation(transform_a);
314 let center_b = translation(transform_b);
315 let axes_a = rotation_axes(transform_a);
316 let axes_b = rotation_axes(transform_b);
317
318 let (ref_normal, _ref_corners) = best_face(center_a, axes_a, half_extents_a, contact_normal);
320
321 let neg_normal = scale3(contact_normal, -1.0);
323 let (_inc_normal, inc_corners) = best_face(center_b, axes_b, half_extents_b, neg_normal);
324
325 let tangent_u = {
328 let candidate = if ref_normal[0].abs() < 0.9 {
329 [1.0_f64, 0.0, 0.0]
330 } else {
331 [0.0_f64, 1.0, 0.0]
332 };
333 normalize3(cross3(candidate, ref_normal))
334 };
335 let tangent_v = cross3(ref_normal, tangent_u);
336
337 let inc_2d: Vec<[f64; 2]> = inc_corners
339 .iter()
340 .map(|&p| project_point_to_plane(p, center_a, tangent_u, tangent_v))
341 .collect();
342
343 let ref_face_normal_world = ref_normal;
347 let best_axis_idx = axes_a
349 .iter()
350 .enumerate()
351 .max_by(|(_, a), (_, b)| {
352 dot3(**a, ref_face_normal_world)
353 .abs()
354 .partial_cmp(&dot3(**b, ref_face_normal_world).abs())
355 .unwrap_or(std::cmp::Ordering::Equal)
356 })
357 .map(|(i, _)| i)
358 .unwrap_or(0);
359 let u_idx = (best_axis_idx + 1) % 3;
360 let w_idx = (best_axis_idx + 2) % 3;
361
362 let hu = half_extents_a[u_idx];
364 let hv = half_extents_a[w_idx];
365 let ref_clip_2d: Vec<[f64; 2]> = vec![[hu, hv], [-hu, hv], [-hu, -hv], [hu, -hv]];
366
367 let clipped_2d = sutherland_hodgman_2d(&inc_2d, &ref_clip_2d);
369
370 let ref_plane_d = dot3(ref_normal, center_a) + half_extents_a[best_axis_idx];
372
373 let mut manifold = ContactManifold::new(contact_normal);
374 for pt2d in &clipped_2d {
375 let pt3d = unproject_point_from_plane(*pt2d, center_a, tangent_u, tangent_v);
376 let signed_dist = dot3(ref_normal, pt3d) - ref_plane_d;
377 if signed_dist <= 1e-4 {
378 let depth = (-signed_dist).max(0.0).max(penetration_depth * 0.01);
380 manifold.contacts.push(ContactPoint {
381 point: pt3d,
382 normal: contact_normal,
383 depth,
384 });
385 }
386 }
387
388 manifold
389}
390
391pub fn reduce_manifold(manifold: &mut ContactManifold, max_contacts: usize) {
400 if manifold.contacts.len() <= max_contacts {
401 return;
402 }
403
404 let contacts = &manifold.contacts;
405 let n = contacts.len();
406 let mut selected: Vec<usize> = Vec::with_capacity(max_contacts);
407
408 let deepest = contacts
410 .iter()
411 .enumerate()
412 .max_by(|(_, a), (_, b)| {
413 a.depth
414 .partial_cmp(&b.depth)
415 .unwrap_or(std::cmp::Ordering::Equal)
416 })
417 .map(|(i, _)| i)
418 .unwrap_or(0);
419 selected.push(deepest);
420
421 while selected.len() < max_contacts {
422 let mut best_idx = 0usize;
423 let mut best_dist = -1.0_f64;
424
425 for i in 0..n {
426 if selected.contains(&i) {
427 continue;
428 }
429 let min_d = selected
431 .iter()
432 .map(|&j| {
433 let d = sub3(contacts[i].point, contacts[j].point);
434 dot3(d, d)
435 })
436 .fold(f64::INFINITY, f64::min);
437
438 if min_d > best_dist {
439 best_dist = min_d;
440 best_idx = i;
441 }
442 }
443
444 selected.push(best_idx);
445 }
446
447 let kept: Vec<ContactPoint> = selected.iter().map(|&i| contacts[i].clone()).collect();
449 manifold.contacts = kept;
450}
451
452#[derive(Debug, Clone, Default)]
460pub struct WarmStartData {
461 pub normal_impulse: f64,
463 pub tangent_impulse: [f64; 2],
466 pub age: u32,
468}
469
470impl WarmStartData {
471 pub fn new() -> Self {
473 Self::default()
474 }
475
476 pub fn scale(&mut self, factor: f64) {
478 self.normal_impulse *= factor;
479 self.tangent_impulse[0] *= factor;
480 self.tangent_impulse[1] *= factor;
481 }
482
483 pub fn reset(&mut self) {
485 self.normal_impulse = 0.0;
486 self.tangent_impulse = [0.0, 0.0];
487 self.age = 0;
488 }
489
490 pub fn tick(&mut self) {
492 self.age = self.age.saturating_add(1);
493 }
494
495 pub fn is_warm(&self) -> bool {
497 self.age > 0
498 }
499}
500
501#[derive(Debug, Clone)]
505pub struct WarmContactPoint {
506 pub contact: ContactPoint,
508 pub warm: WarmStartData,
510}
511
512impl WarmContactPoint {
513 pub fn new(contact: ContactPoint) -> Self {
515 Self {
516 contact,
517 warm: WarmStartData::new(),
518 }
519 }
520
521 pub fn with_warm(contact: ContactPoint, warm: WarmStartData) -> Self {
523 Self { contact, warm }
524 }
525}
526
527pub fn build_4point_manifold(points: &[ContactPoint]) -> Vec<ContactPoint> {
540 if points.len() <= 4 {
541 return points.to_vec();
542 }
543
544 let i0 = points
546 .iter()
547 .enumerate()
548 .max_by(|(_, a), (_, b)| {
549 a.depth
550 .partial_cmp(&b.depth)
551 .unwrap_or(std::cmp::Ordering::Equal)
552 })
553 .map(|(i, _)| i)
554 .unwrap_or(0);
555
556 let p0 = points[i0].point;
558 let i1 = points
559 .iter()
560 .enumerate()
561 .filter(|(i, _)| *i != i0)
562 .max_by(|(_, a), (_, b)| {
563 dist_sq3(a.point, p0)
564 .partial_cmp(&dist_sq3(b.point, p0))
565 .unwrap_or(std::cmp::Ordering::Equal)
566 })
567 .map(|(i, _)| i)
568 .unwrap_or(if i0 == 0 { 1 } else { 0 });
569
570 let p1 = points[i1].point;
572 let i2 = points
573 .iter()
574 .enumerate()
575 .filter(|(i, _)| *i != i0 && *i != i1)
576 .max_by(|(_, a), (_, b)| {
577 triangle_area_sq(p0, p1, a.point)
578 .partial_cmp(&triangle_area_sq(p0, p1, b.point))
579 .unwrap_or(std::cmp::Ordering::Equal)
580 })
581 .map(|(i, _)| i);
582
583 let i2 = match i2 {
584 Some(idx) => idx,
585 None => return vec![points[i0].clone(), points[i1].clone()],
586 };
587
588 let p2 = points[i2].point;
590 let i3 = points
591 .iter()
592 .enumerate()
593 .filter(|(i, _)| *i != i0 && *i != i1 && *i != i2)
594 .max_by(|(_, a), (_, b)| {
595 quad_area_sq(p0, p1, p2, a.point)
596 .partial_cmp(&quad_area_sq(p0, p1, p2, b.point))
597 .unwrap_or(std::cmp::Ordering::Equal)
598 })
599 .map(|(i, _)| i);
600
601 let mut result = vec![points[i0].clone(), points[i1].clone(), points[i2].clone()];
602 if let Some(idx) = i3 {
603 result.push(points[idx].clone());
604 }
605 result
606}
607
608const MATCH_DIST_SQ_MANIFOLD: f64 = 5e-4; pub fn transfer_warm_start(
621 new_contacts: &[ContactPoint],
622 old_warm: &[WarmContactPoint],
623) -> Vec<WarmContactPoint> {
624 new_contacts
625 .iter()
626 .map(|new_pt| {
627 let best = old_warm.iter().min_by(|a, b| {
629 let da = dist_sq3(a.contact.point, new_pt.point);
630 let db = dist_sq3(b.contact.point, new_pt.point);
631 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
632 });
633
634 if let Some(old_pt) = best {
635 let d_sq = dist_sq3(old_pt.contact.point, new_pt.point);
636 if d_sq < MATCH_DIST_SQ_MANIFOLD {
637 let mut warm = old_pt.warm.clone();
638 warm.tick();
639 return WarmContactPoint::with_warm(new_pt.clone(), warm);
640 }
641 }
642
643 WarmContactPoint::new(new_pt.clone())
644 })
645 .collect()
646}
647
648#[derive(Debug, Clone)]
655pub struct WarmManifold {
656 pub normal: [f64; 3],
658 pub points: Vec<WarmContactPoint>,
660}
661
662impl WarmManifold {
663 pub fn new(normal: [f64; 3]) -> Self {
665 Self {
666 normal,
667 points: Vec::new(),
668 }
669 }
670
671 pub fn from_manifold(manifold: &ContactManifold, prev: Option<&WarmManifold>) -> Self {
676 let reduced = build_4point_manifold(&manifold.contacts);
677
678 let points = if let Some(old) = prev {
679 transfer_warm_start(&reduced, &old.points)
680 } else {
681 reduced
682 .iter()
683 .map(|c| WarmContactPoint::new(c.clone()))
684 .collect()
685 };
686
687 Self {
688 normal: manifold.normal,
689 points,
690 }
691 }
692
693 pub fn len(&self) -> usize {
695 self.points.len()
696 }
697
698 pub fn is_empty(&self) -> bool {
700 self.points.is_empty()
701 }
702
703 pub fn iter(&self) -> std::slice::Iter<'_, WarmContactPoint> {
705 self.points.iter()
706 }
707
708 pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, WarmContactPoint> {
710 self.points.iter_mut()
711 }
712
713 pub fn scale_impulses(&mut self, factor: f64) {
717 for pt in &mut self.points {
718 pt.warm.scale(factor);
719 }
720 }
721
722 pub fn reset_warm_start(&mut self) {
724 for pt in &mut self.points {
725 pt.warm.reset();
726 }
727 }
728
729 pub fn recompute_normal(&mut self) {
731 if self.points.is_empty() {
732 return;
733 }
734 let mut avg = [0.0f64; 3];
735 for pt in &self.points {
736 avg = add3(avg, pt.contact.normal);
737 }
738 let n = self.points.len() as f64;
739 self.normal = normalize3(scale3(avg, 1.0 / n));
740 }
741}
742
743#[inline]
746fn dist_sq3(a: [f64; 3], b: [f64; 3]) -> f64 {
747 let d = sub3(a, b);
748 dot3(d, d)
749}
750
751#[inline]
752fn triangle_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3]) -> f64 {
753 let e1 = sub3(b, a);
754 let e2 = sub3(c, a);
755 let cx = cross3(e1, e2);
756 dot3(cx, cx)
757}
758
759#[inline]
760fn quad_area_sq(a: [f64; 3], b: [f64; 3], c: [f64; 3], d: [f64; 3]) -> f64 {
761 triangle_area_sq(a, b, c) + triangle_area_sq(a, c, d)
762}
763
764#[derive(Debug, Clone)]
770pub struct PersistentPatch {
771 pub normal: [f64; 3],
773 pub points: Vec<WarmContactPoint>,
775 pub frame_count: u32,
777 pub active: bool,
779}
780
781impl PersistentPatch {
782 pub fn new(normal: [f64; 3]) -> Self {
784 Self {
785 normal,
786 points: Vec::new(),
787 frame_count: 0,
788 active: true,
789 }
790 }
791
792 pub fn update(&mut self, manifold: &ContactManifold) {
797 let reduced = build_4point_manifold(&manifold.contacts);
798 let new_warm = transfer_warm_start(&reduced, &self.points);
799 self.points = new_warm;
800 self.normal = manifold.normal;
801 self.frame_count = self.frame_count.saturating_add(1);
802 self.active = true;
803 }
804
805 pub fn deactivate(&mut self) {
807 self.active = false;
808 }
809
810 pub fn is_sustained(&self, n: u32) -> bool {
812 self.active && self.frame_count >= n
813 }
814
815 pub fn max_depth(&self) -> f64 {
817 self.points
818 .iter()
819 .map(|p| p.contact.depth)
820 .fold(0.0f64, f64::max)
821 }
822
823 pub fn average_position(&self) -> Option<[f64; 3]> {
825 if self.points.is_empty() {
826 return None;
827 }
828 let n = self.points.len() as f64;
829 let mut avg = [0.0f64; 3];
830 for p in &self.points {
831 avg = add3(avg, p.contact.point);
832 }
833 Some(scale3(avg, 1.0 / n))
834 }
835}
836
837pub fn smooth_contact_normal(prev_normal: [f64; 3], new_normal: [f64; 3], alpha: f64) -> [f64; 3] {
844 let blended = add3(scale3(prev_normal, 1.0 - alpha), scale3(new_normal, alpha));
845 normalize3(blended)
846}
847
848pub fn smooth_manifold_normals(manifold: &mut ContactManifold, alpha: f64) {
853 let base = manifold.normal;
854 for pt in &mut manifold.contacts {
855 pt.normal = smooth_contact_normal(base, pt.normal, alpha);
856 }
857}
858
859#[derive(Debug, Clone)]
866pub struct SpeculativeContactPoint {
867 pub point: [f64; 3],
869 pub normal: [f64; 3],
871 pub separation: f64,
873 pub closing_speed: f64,
875}
876
877impl SpeculativeContactPoint {
878 pub fn new(point: [f64; 3], normal: [f64; 3], separation: f64, closing_speed: f64) -> Self {
880 Self {
881 point,
882 normal,
883 separation,
884 closing_speed,
885 }
886 }
887
888 pub fn will_collide(&self, dt: f64) -> bool {
890 self.separation - self.closing_speed * dt < 0.0
891 }
892
893 pub fn predicted_depth(&self, dt: f64) -> f64 {
895 -(self.separation - self.closing_speed * dt)
896 }
897}
898
899pub fn generate_speculative_contacts(
903 manifold: &ContactManifold,
904 normal: [f64; 3],
905 closing_speed: f64,
906 separation: f64,
907 dt: f64,
908) -> Vec<SpeculativeContactPoint> {
909 manifold
910 .contacts
911 .iter()
912 .filter_map(|pt| {
913 let spec = SpeculativeContactPoint::new(pt.point, normal, separation, closing_speed);
914 if spec.will_collide(dt) {
915 Some(spec)
916 } else {
917 None
918 }
919 })
920 .collect()
921}
922
923pub fn rotate_manifold_points(manifold: &mut ContactManifold, rot: [[f64; 3]; 3]) {
929 for pt in &mut manifold.contacts {
930 pt.point = mat3_mul_vec3(rot, pt.point);
931 pt.normal = mat3_mul_vec3(rot, pt.normal);
932 }
933 manifold.normal = mat3_mul_vec3(rot, manifold.normal);
934}
935
936#[inline]
938fn mat3_mul_vec3(m: [[f64; 3]; 3], v: [f64; 3]) -> [f64; 3] {
939 [
940 m[0][0] * v[0] + m[0][1] * v[1] + m[0][2] * v[2],
941 m[1][0] * v[0] + m[1][1] * v[1] + m[1][2] * v[2],
942 m[2][0] * v[0] + m[2][1] * v[1] + m[2][2] * v[2],
943 ]
944}
945
946pub fn friction_basis(normal: [f64; 3]) -> ([f64; 3], [f64; 3]) {
953 let candidate = if normal[0].abs() < 0.9 {
955 [1.0f64, 0.0, 0.0]
956 } else {
957 [0.0f64, 1.0, 0.0]
958 };
959 let t1 = normalize3(cross3(normal, candidate));
960 let t2 = cross3(normal, t1);
961 (t1, t2)
962}
963
964pub fn decompose_velocity(vel: [f64; 3], normal: [f64; 3]) -> ([f64; 3], [f64; 3]) {
969 let vn = dot3(vel, normal);
970 let v_normal = scale3(normal, vn);
971 let v_tangential = sub3(vel, v_normal);
972 (v_normal, v_tangential)
973}
974
975pub fn coulomb_friction_limit(lambda_n: f64, mu: f64) -> f64 {
980 (lambda_n * mu).max(0.0)
981}
982
983pub fn manifold_quality(manifold: &ContactManifold) -> f64 {
990 let n = manifold.contacts.len();
991 if n == 0 {
992 return 0.0;
993 }
994
995 let avg_depth = manifold.contacts.iter().map(|p| p.depth).sum::<f64>() / n as f64;
997 let depth_score = (avg_depth / (avg_depth + 1.0)).min(1.0);
998
999 if n < 2 {
1000 return depth_score * 0.5;
1001 }
1002
1003 let max_dist_sq = manifold
1005 .contacts
1006 .iter()
1007 .enumerate()
1008 .flat_map(|(i, a)| {
1009 manifold.contacts[i + 1..].iter().map(move |b| {
1010 let d = sub3(a.point, b.point);
1011 dot3(d, d)
1012 })
1013 })
1014 .fold(0.0f64, f64::max);
1015 let spread_score = (max_dist_sq.sqrt() / (max_dist_sq.sqrt() + 1.0)).min(1.0);
1016
1017 (depth_score + spread_score) * 0.5
1018}
1019
1020#[cfg(test)]
1023mod tests {
1024 use super::*;
1025
1026 #[test]
1029 fn test_sh_clip_square_against_x_ge_0() {
1030 let subject = [[-1.0, -1.0], [1.0, -1.0], [1.0, 1.0], [-1.0, 1.0]];
1032 let clip = [[0.0, -2.0], [2.0, -2.0], [2.0, 2.0], [0.0, 2.0]];
1035
1036 let result = sutherland_hodgman_2d(&subject, &clip);
1037
1038 assert!(!result.is_empty(), "clipped polygon must not be empty");
1039 for v in &result {
1041 assert!(
1042 v[0] >= -1e-10,
1043 "vertex {:?} has negative x after clipping against x≥0",
1044 v
1045 );
1046 }
1047 assert_eq!(
1049 result.len(),
1050 4,
1051 "right half of square should have 4 vertices, got {:?}",
1052 result
1053 );
1054 let area = polygon_area_2d(&result);
1056 assert!(
1057 (area - 2.0).abs() < 1e-10,
1058 "expected area 2.0, got {}",
1059 area
1060 );
1061 }
1062
1063 #[test]
1064 fn test_sh_no_clipping_needed() {
1065 let subject = [[0.1, 0.1], [0.9, 0.1], [0.9, 0.9], [0.1, 0.9]];
1067 let clip = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1068 let result = sutherland_hodgman_2d(&subject, &clip);
1069 assert_eq!(result.len(), 4);
1070 }
1071
1072 #[test]
1073 fn test_sh_fully_outside() {
1074 let subject = [[-3.0, -3.0], [-2.0, -3.0], [-2.0, -2.0], [-3.0, -2.0]];
1075 let clip = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]];
1076 let result = sutherland_hodgman_2d(&subject, &clip);
1077 assert!(
1078 result.is_empty(),
1079 "fully outside polygon should be clipped to empty"
1080 );
1081 }
1082
1083 #[test]
1086 fn test_clip_polygon_above_plane() {
1087 let quad = [
1089 [1.0, 1.0, 1.0],
1090 [-1.0, 1.0, 1.0],
1091 [-1.0, -1.0, 1.0],
1092 [1.0, -1.0, 1.0],
1093 ];
1094 let result = clip_polygon_by_halfspace(&quad, [0.0, 0.0, 1.0], 0.0);
1095 assert_eq!(
1097 result.len(),
1098 4,
1099 "quad fully above plane must be unchanged, got {:?}",
1100 result
1101 );
1102 }
1103
1104 #[test]
1105 fn test_clip_polygon_straddles_plane() {
1106 let quad = [
1108 [1.0, 0.0, 1.0],
1109 [-1.0, 0.0, 1.0],
1110 [-1.0, 0.0, -1.0],
1111 [1.0, 0.0, -1.0],
1112 ];
1113 let result = clip_polygon_by_halfspace(&quad, [0.0, 0.0, 1.0], 0.0);
1114 assert!(
1116 result.len() >= 3,
1117 "straddling quad must produce at least a triangle, got {:?}",
1118 result
1119 );
1120 for v in &result {
1121 assert!(
1122 v[2] >= -1e-10,
1123 "all clipped vertices must have z≥0, got {:?}",
1124 v
1125 );
1126 }
1127 }
1128
1129 #[test]
1130 fn test_clip_polygon_below_plane() {
1131 let quad = [
1133 [1.0, 0.0, -1.0],
1134 [-1.0, 0.0, -1.0],
1135 [-1.0, 0.0, -2.0],
1136 [1.0, 0.0, -2.0],
1137 ];
1138 let result = clip_polygon_by_halfspace(&quad, [0.0, 0.0, 1.0], 0.0);
1139 assert!(
1140 result.is_empty(),
1141 "quad fully below plane must be clipped to empty"
1142 );
1143 }
1144
1145 #[test]
1148 fn test_reduce_manifold_8_to_4() {
1149 let mut manifold = ContactManifold::new([0.0, 0.0, 1.0]);
1151 for i in 0..8 {
1152 let angle = (i as f64) * std::f64::consts::TAU / 8.0;
1153 manifold.contacts.push(ContactPoint {
1154 point: [angle.cos(), angle.sin(), 0.0],
1155 normal: [0.0, 0.0, 1.0],
1156 depth: 0.1 + (i as f64) * 0.01, });
1158 }
1159
1160 reduce_manifold(&mut manifold, 4);
1161
1162 assert_eq!(
1163 manifold.contacts.len(),
1164 4,
1165 "manifold should be reduced to 4 contacts"
1166 );
1167 }
1168
1169 #[test]
1170 fn test_reduce_manifold_already_small() {
1171 let mut manifold = ContactManifold::new([0.0, 1.0, 0.0]);
1172 manifold.contacts.push(ContactPoint {
1173 point: [0.0, 0.0, 0.0],
1174 normal: [0.0, 1.0, 0.0],
1175 depth: 0.05,
1176 });
1177 reduce_manifold(&mut manifold, 4);
1178 assert_eq!(manifold.contacts.len(), 1);
1179 }
1180
1181 #[test]
1184 fn test_project_unproject_roundtrip() {
1185 let origin = [1.0, 2.0, 3.0];
1186 let u = normalize3([1.0, 0.0, 0.0]);
1187 let v = normalize3([0.0, 1.0, 0.0]);
1188 let pt = [2.5, 3.5, 3.0]; let pt2d = project_point_to_plane(pt, origin, u, v);
1190 let pt3d = unproject_point_from_plane(pt2d, origin, u, v);
1191 for i in 0..3 {
1192 assert!(
1193 (pt3d[i] - pt[i]).abs() < 1e-12,
1194 "round-trip mismatch at component {}: expected {}, got {}",
1195 i,
1196 pt[i],
1197 pt3d[i]
1198 );
1199 }
1200 }
1201
1202 fn polygon_area_2d(poly: &[[f64; 2]]) -> f64 {
1206 let n = poly.len();
1207 let mut area = 0.0_f64;
1208 for i in 0..n {
1209 let j = (i + 1) % n;
1210 area += poly[i][0] * poly[j][1];
1211 area -= poly[j][0] * poly[i][1];
1212 }
1213 (area / 2.0).abs()
1214 }
1215
1216 #[test]
1219 fn test_warm_start_data_default_zero() {
1220 let w = WarmStartData::new();
1221 assert_eq!(w.normal_impulse, 0.0);
1222 assert_eq!(w.tangent_impulse, [0.0, 0.0]);
1223 assert_eq!(w.age, 0);
1224 assert!(!w.is_warm());
1225 }
1226
1227 #[test]
1228 fn test_warm_start_data_tick_increments_age() {
1229 let mut w = WarmStartData::new();
1230 w.tick();
1231 assert_eq!(w.age, 1);
1232 assert!(w.is_warm());
1233 w.tick();
1234 assert_eq!(w.age, 2);
1235 }
1236
1237 #[test]
1238 fn test_warm_start_data_scale() {
1239 let mut w = WarmStartData::new();
1240 w.normal_impulse = 10.0;
1241 w.tangent_impulse = [4.0, -2.0];
1242 w.scale(0.5);
1243 assert!((w.normal_impulse - 5.0).abs() < 1e-12);
1244 assert!((w.tangent_impulse[0] - 2.0).abs() < 1e-12);
1245 assert!((w.tangent_impulse[1] - (-1.0)).abs() < 1e-12);
1246 }
1247
1248 #[test]
1249 fn test_warm_start_data_reset() {
1250 let mut w = WarmStartData::new();
1251 w.normal_impulse = 5.0;
1252 w.tangent_impulse = [1.0, 2.0];
1253 w.age = 10;
1254 w.reset();
1255 assert_eq!(w.normal_impulse, 0.0);
1256 assert_eq!(w.tangent_impulse, [0.0, 0.0]);
1257 assert_eq!(w.age, 0);
1258 }
1259
1260 fn make_cp(x: f64, z: f64, depth: f64) -> ContactPoint {
1263 ContactPoint {
1264 point: [x, 0.0, z],
1265 normal: [0.0, 1.0, 0.0],
1266 depth,
1267 }
1268 }
1269
1270 #[test]
1271 fn test_build_4point_manifold_reduces_to_4() {
1272 let pts: Vec<ContactPoint> = vec![
1274 make_cp(-1.0, -1.0, 0.1),
1275 make_cp(1.0, -1.0, 0.2),
1276 make_cp(1.0, 1.0, 0.15),
1277 make_cp(-1.0, 1.0, 0.05),
1278 make_cp(0.0, 0.0, 0.25), make_cp(0.5, -0.5, 0.08),
1280 make_cp(-0.5, 0.5, 0.12),
1281 make_cp(0.0, 1.0, 0.07),
1282 ];
1283 let result = build_4point_manifold(&pts);
1284 assert_eq!(result.len(), 4, "should reduce to exactly 4 points");
1285 }
1286
1287 #[test]
1288 fn test_build_4point_manifold_fewer_than_4_unchanged() {
1289 let pts: Vec<ContactPoint> = vec![make_cp(0.0, 0.0, 0.1), make_cp(1.0, 0.0, 0.1)];
1290 let result = build_4point_manifold(&pts);
1291 assert_eq!(result.len(), 2, "fewer than 4 points returned unchanged");
1292 }
1293
1294 #[test]
1295 fn test_build_4point_manifold_deepest_included() {
1296 let pts: Vec<ContactPoint> = vec![
1297 make_cp(-1.0, 0.0, 0.01),
1298 make_cp(0.0, 0.0, 0.99), make_cp(1.0, 0.0, 0.01),
1300 make_cp(0.0, 1.0, 0.01),
1301 make_cp(0.0, -1.0, 0.01),
1302 ];
1303 let result = build_4point_manifold(&pts);
1304 let has_deepest = result.iter().any(|p| (p.depth - 0.99).abs() < 1e-9);
1306 assert!(
1307 has_deepest,
1308 "deepest point must be included in 4-point manifold"
1309 );
1310 }
1311
1312 #[test]
1315 fn test_transfer_warm_start_matching_point() {
1316 let old_pt = {
1318 let mut cp = WarmContactPoint::new(ContactPoint {
1319 point: [0.0, 0.0, 0.0],
1320 normal: [0.0, 1.0, 0.0],
1321 depth: 0.05,
1322 });
1323 cp.warm.normal_impulse = 42.0;
1324 cp.warm.tick();
1325 cp
1326 };
1327 let old_warm = vec![old_pt];
1328
1329 let new_contacts = vec![ContactPoint {
1331 point: [0.001, 0.0, 0.001], normal: [0.0, 1.0, 0.0],
1333 depth: 0.04,
1334 }];
1335
1336 let result = transfer_warm_start(&new_contacts, &old_warm);
1337 assert_eq!(result.len(), 1);
1338 assert!(
1339 result[0].warm.is_warm(),
1340 "warm-start should transfer to close contact"
1341 );
1342 assert!(
1343 (result[0].warm.normal_impulse - 42.0).abs() < 1e-9,
1344 "normal_impulse should be transferred"
1345 );
1346 assert_eq!(result[0].warm.age, 2, "age should be incremented");
1347 }
1348
1349 #[test]
1350 fn test_transfer_warm_start_distant_point_gets_zero() {
1351 let old_pt = {
1352 let mut cp = WarmContactPoint::new(ContactPoint {
1353 point: [0.0, 0.0, 0.0],
1354 normal: [0.0, 1.0, 0.0],
1355 depth: 0.05,
1356 });
1357 cp.warm.normal_impulse = 10.0;
1358 cp.warm.tick();
1359 cp
1360 };
1361 let old_warm = vec![old_pt];
1362
1363 let new_contacts = vec![ContactPoint {
1365 point: [100.0, 0.0, 0.0],
1366 normal: [0.0, 1.0, 0.0],
1367 depth: 0.04,
1368 }];
1369
1370 let result = transfer_warm_start(&new_contacts, &old_warm);
1371 assert_eq!(result.len(), 1);
1372 assert!(
1373 !result[0].warm.is_warm(),
1374 "distant contact should get no warm-start"
1375 );
1376 assert_eq!(result[0].warm.normal_impulse, 0.0);
1377 }
1378
1379 #[test]
1382 fn test_warm_manifold_from_manifold_no_prev() {
1383 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1384 for i in 0..4 {
1385 m.contacts.push(make_cp(i as f64, 0.0, 0.1));
1386 }
1387 let wm = WarmManifold::from_manifold(&m, None);
1388 assert_eq!(wm.len(), 4);
1389 for pt in wm.iter() {
1390 assert!(!pt.warm.is_warm(), "first frame: no warm data");
1391 }
1392 }
1393
1394 #[test]
1395 fn test_warm_manifold_scale_impulses() {
1396 let mut wm = WarmManifold::new([0.0, 1.0, 0.0]);
1397 let mut wcp = WarmContactPoint::new(make_cp(0.0, 0.0, 0.1));
1398 wcp.warm.normal_impulse = 8.0;
1399 wm.points.push(wcp);
1400
1401 wm.scale_impulses(0.25);
1402 assert!((wm.points[0].warm.normal_impulse - 2.0).abs() < 1e-12);
1403 }
1404
1405 #[test]
1406 fn test_warm_manifold_reset_warm_start() {
1407 let mut wm = WarmManifold::new([0.0, 1.0, 0.0]);
1408 let mut wcp = WarmContactPoint::new(make_cp(0.0, 0.0, 0.1));
1409 wcp.warm.normal_impulse = 5.0;
1410 wcp.warm.age = 3;
1411 wm.points.push(wcp);
1412
1413 wm.reset_warm_start();
1414 assert_eq!(wm.points[0].warm.normal_impulse, 0.0);
1415 assert_eq!(wm.points[0].warm.age, 0);
1416 }
1417
1418 #[test]
1419 fn test_warm_manifold_recompute_normal() {
1420 let mut wm = WarmManifold::new([1.0, 0.0, 0.0]);
1421 let normals = [[0.0, 1.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.8, 0.6]];
1422 for n in normals {
1423 let mut wcp = WarmContactPoint::new(ContactPoint {
1424 point: [0.0, 0.0, 0.0],
1425 normal: n,
1426 depth: 0.1,
1427 });
1428 wcp.warm = WarmStartData::new();
1429 wm.points.push(wcp);
1430 }
1431 wm.recompute_normal();
1432 let nl = (wm.normal[0] * wm.normal[0]
1433 + wm.normal[1] * wm.normal[1]
1434 + wm.normal[2] * wm.normal[2])
1435 .sqrt();
1436 assert!(
1437 (nl - 1.0).abs() < 1e-9,
1438 "recomputed normal should be unit length"
1439 );
1440 }
1441
1442 #[test]
1445 fn test_persistent_patch_new_empty() {
1446 let patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1447 assert!(patch.points.is_empty());
1448 assert_eq!(patch.frame_count, 0);
1449 assert!(patch.active);
1450 }
1451
1452 #[test]
1453 fn test_persistent_patch_update_increments_frame() {
1454 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1455 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1456 m.contacts.push(make_cp(0.0, 0.0, 0.1));
1457 patch.update(&m);
1458 assert_eq!(patch.frame_count, 1);
1459 }
1460
1461 #[test]
1462 fn test_persistent_patch_update_twice() {
1463 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1464 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1465 m.contacts.push(make_cp(0.0, 0.0, 0.1));
1466 patch.update(&m);
1467 patch.update(&m);
1468 assert_eq!(patch.frame_count, 2);
1469 }
1470
1471 #[test]
1472 fn test_persistent_patch_deactivate() {
1473 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1474 patch.deactivate();
1475 assert!(!patch.active);
1476 }
1477
1478 #[test]
1479 fn test_persistent_patch_is_sustained() {
1480 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1481 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1482 m.contacts.push(make_cp(0.0, 0.0, 0.1));
1483 patch.update(&m);
1484 patch.update(&m);
1485 assert!(patch.is_sustained(2), "should be sustained after 2 frames");
1486 assert!(
1487 !patch.is_sustained(3),
1488 "should not be sustained for 3 frames yet"
1489 );
1490 }
1491
1492 #[test]
1493 fn test_persistent_patch_max_depth() {
1494 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1495 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1496 m.contacts.push(make_cp(0.0, 0.0, 0.05));
1497 m.contacts.push(make_cp(1.0, 0.0, 0.10));
1498 patch.update(&m);
1499 assert!(
1500 (patch.max_depth() - 0.10).abs() < 1e-10,
1501 "max_depth should be 0.10"
1502 );
1503 }
1504
1505 #[test]
1506 fn test_persistent_patch_average_position() {
1507 let mut patch = PersistentPatch::new([0.0, 1.0, 0.0]);
1508 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1509 m.contacts.push(make_cp(0.0, 0.0, 0.1));
1510 m.contacts.push(make_cp(2.0, 0.0, 0.1));
1511 patch.update(&m);
1512 let avg = patch.average_position().unwrap();
1513 assert!(
1514 (avg[0] - 1.0).abs() < 0.01,
1515 "average x should be ~1.0, got {}",
1516 avg[0]
1517 );
1518 }
1519
1520 #[test]
1523 fn test_smooth_contact_normal_alpha_one() {
1524 let prev = [1.0f64, 0.0, 0.0];
1525 let new = [0.0, 1.0, 0.0];
1526 let result = smooth_contact_normal(prev, new, 1.0);
1527 assert!((result[0]).abs() < 1e-10, "alpha=1 should give new normal");
1528 assert!((result[1] - 1.0).abs() < 1e-10);
1529 }
1530
1531 #[test]
1532 fn test_smooth_contact_normal_alpha_zero() {
1533 let prev = [1.0f64, 0.0, 0.0];
1534 let new = [0.0, 1.0, 0.0];
1535 let result = smooth_contact_normal(prev, new, 0.0);
1536 assert!(
1537 (result[0] - 1.0).abs() < 1e-10,
1538 "alpha=0 should give prev normal"
1539 );
1540 assert!((result[1]).abs() < 1e-10);
1541 }
1542
1543 #[test]
1544 fn test_smooth_contact_normal_midpoint_unit_length() {
1545 let prev = [1.0f64, 0.0, 0.0];
1546 let new = [0.0, 1.0, 0.0];
1547 let result = smooth_contact_normal(prev, new, 0.5);
1548 let l = (result[0] * result[0] + result[1] * result[1] + result[2] * result[2]).sqrt();
1549 assert!(
1550 (l - 1.0).abs() < 1e-10,
1551 "smoothed normal should be unit length, got {l}"
1552 );
1553 }
1554
1555 #[test]
1556 fn test_smooth_manifold_normals() {
1557 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1558 m.contacts.push(ContactPoint {
1559 point: [0.0, 0.0, 0.0],
1560 normal: [1.0, 0.0, 0.0],
1561 depth: 0.1,
1562 });
1563 smooth_manifold_normals(&mut m, 0.5);
1564 let n = m.contacts[0].normal;
1566 let l = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1567 assert!(
1568 (l - 1.0).abs() < 1e-10,
1569 "smoothed normal should be unit length"
1570 );
1571 }
1572
1573 #[test]
1576 fn test_speculative_contact_will_collide() {
1577 let spec = SpeculativeContactPoint::new([0.0; 3], [0.0, 1.0, 0.0], 0.1, 1.0);
1579 assert!(spec.will_collide(0.2));
1580 }
1581
1582 #[test]
1583 fn test_speculative_contact_will_not_collide() {
1584 let spec = SpeculativeContactPoint::new([0.0; 3], [0.0, 1.0, 0.0], 1.0, 0.1);
1586 assert!(!spec.will_collide(0.1));
1587 }
1588
1589 #[test]
1590 fn test_speculative_contact_predicted_depth() {
1591 let spec = SpeculativeContactPoint::new([0.0; 3], [0.0, 1.0, 0.0], 0.1, 1.0);
1592 let depth = spec.predicted_depth(0.2);
1593 assert!((depth - 0.1).abs() < 1e-10, "Expected 0.1, got {depth}");
1594 }
1595
1596 #[test]
1597 fn test_generate_speculative_contacts() {
1598 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1599 m.contacts.push(ContactPoint {
1600 point: [0.0, 0.0, 0.0],
1601 normal: [0.0, 1.0, 0.0],
1602 depth: 0.05,
1603 });
1604 m.contacts.push(ContactPoint {
1605 point: [1.0, 0.0, 0.0],
1606 normal: [0.0, 1.0, 0.0],
1607 depth: 0.05,
1608 });
1609 let speculative = generate_speculative_contacts(&m, [0.0, 1.0, 0.0], 5.0, 0.1, 0.1);
1610 assert_eq!(
1612 speculative.len(),
1613 2,
1614 "Both speculative contacts should be generated"
1615 );
1616 }
1617
1618 #[test]
1619 fn test_generate_speculative_contacts_none_collide() {
1620 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1621 m.contacts.push(ContactPoint {
1622 point: [0.0, 0.0, 0.0],
1623 normal: [0.0, 1.0, 0.0],
1624 depth: 0.05,
1625 });
1626 let speculative = generate_speculative_contacts(&m, [0.0, 1.0, 0.0], 0.001, 100.0, 0.0001);
1628 assert_eq!(
1629 speculative.len(),
1630 0,
1631 "No speculative contacts should be generated"
1632 );
1633 }
1634
1635 #[test]
1638 fn test_rotate_manifold_points_identity() {
1639 let identity = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
1640 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1641 m.contacts.push(ContactPoint {
1642 point: [1.0, 2.0, 3.0],
1643 normal: [0.0, 1.0, 0.0],
1644 depth: 0.1,
1645 });
1646 rotate_manifold_points(&mut m, identity);
1647 assert!((m.contacts[0].point[0] - 1.0).abs() < 1e-10);
1648 assert!((m.contacts[0].point[1] - 2.0).abs() < 1e-10);
1649 assert!((m.contacts[0].point[2] - 3.0).abs() < 1e-10);
1650 }
1651
1652 #[test]
1653 fn test_rotate_manifold_points_90_about_z() {
1654 let rot_z_90 = [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]];
1656 let mut m = ContactManifold::new([1.0, 0.0, 0.0]);
1657 m.contacts.push(ContactPoint {
1658 point: [1.0, 0.0, 0.0],
1659 normal: [1.0, 0.0, 0.0],
1660 depth: 0.1,
1661 });
1662 rotate_manifold_points(&mut m, rot_z_90);
1663 assert!(
1664 (m.contacts[0].point[0]).abs() < 1e-10,
1665 "x should be 0 after rotation"
1666 );
1667 assert!(
1668 (m.contacts[0].point[1] - 1.0).abs() < 1e-10,
1669 "y should be 1 after rotation"
1670 );
1671 }
1672
1673 #[test]
1676 fn test_friction_basis_orthogonal_to_normal() {
1677 let normals: [[f64; 3]; 4] = [
1678 [1.0, 0.0, 0.0],
1679 [0.0, 1.0, 0.0],
1680 [0.0, 0.0, 1.0],
1681 normalize3([1.0, 1.0, 1.0]),
1682 ];
1683 for n in normals {
1684 let (t1, t2) = friction_basis(n);
1685 assert!(
1686 dot3(n, t1).abs() < 1e-9,
1687 "t1 should be perpendicular to normal"
1688 );
1689 assert!(
1690 dot3(n, t2).abs() < 1e-9,
1691 "t2 should be perpendicular to normal"
1692 );
1693 assert!(
1694 dot3(t1, t2).abs() < 1e-9,
1695 "t1 and t2 should be perpendicular"
1696 );
1697 let l1 = (t1[0] * t1[0] + t1[1] * t1[1] + t1[2] * t1[2]).sqrt();
1698 let l2 = (t2[0] * t2[0] + t2[1] * t2[1] + t2[2] * t2[2]).sqrt();
1699 assert!((l1 - 1.0).abs() < 1e-9, "t1 should be unit length");
1700 assert!((l2 - 1.0).abs() < 1e-9, "t2 should be unit length");
1701 }
1702 }
1703
1704 #[test]
1707 fn test_decompose_velocity_normal_component() {
1708 let vel = [0.0, -3.0, 0.0];
1709 let normal = [0.0, 1.0, 0.0];
1710 let (vn, vt) = decompose_velocity(vel, normal);
1711 assert!(
1712 (vn[1] - (-3.0)).abs() < 1e-10,
1713 "normal component should be [0,-3,0]"
1714 );
1715 assert!(
1716 vt[0].abs() < 1e-10 && vt[1].abs() < 1e-10 && vt[2].abs() < 1e-10,
1717 "tangential component should be zero for pure normal velocity"
1718 );
1719 }
1720
1721 #[test]
1722 fn test_decompose_velocity_tangential_component() {
1723 let vel = [2.0, 0.0, 0.0];
1724 let normal = [0.0, 1.0, 0.0];
1725 let (vn, vt) = decompose_velocity(vel, normal);
1726 assert!(
1727 vn[0].abs() < 1e-10 && vn[1].abs() < 1e-10,
1728 "normal component should be zero"
1729 );
1730 assert!(
1731 (vt[0] - 2.0).abs() < 1e-10,
1732 "tangential component should be [2,0,0]"
1733 );
1734 }
1735
1736 #[test]
1739 fn test_coulomb_friction_limit_basic() {
1740 let limit = coulomb_friction_limit(10.0, 0.5);
1741 assert!((limit - 5.0).abs() < 1e-10, "Expected 5.0, got {limit}");
1742 }
1743
1744 #[test]
1745 fn test_coulomb_friction_limit_negative_normal() {
1746 let limit = coulomb_friction_limit(-1.0, 0.5);
1748 assert_eq!(
1749 limit, 0.0,
1750 "Negative normal impulse should give 0 friction limit"
1751 );
1752 }
1753
1754 #[test]
1757 fn test_manifold_quality_empty() {
1758 let m = ContactManifold::new([0.0, 1.0, 0.0]);
1759 let q = manifold_quality(&m);
1760 assert_eq!(q, 0.0, "Empty manifold should have quality 0");
1761 }
1762
1763 #[test]
1764 fn test_manifold_quality_single_point() {
1765 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1766 m.contacts.push(ContactPoint {
1767 point: [0.0; 3],
1768 normal: [0.0, 1.0, 0.0],
1769 depth: 0.1,
1770 });
1771 let q = manifold_quality(&m);
1772 assert!(q > 0.0 && q <= 1.0, "Quality should be in (0,1], got {q}");
1773 }
1774
1775 #[test]
1776 fn test_manifold_quality_four_wide_points() {
1777 let mut m = ContactManifold::new([0.0, 1.0, 0.0]);
1778 for &x in &[-1.0f64, 1.0, 0.0, 0.0] {
1779 m.contacts.push(ContactPoint {
1780 point: [x, 0.0, 0.0],
1781 normal: [0.0, 1.0, 0.0],
1782 depth: 0.5,
1783 });
1784 }
1785 let q = manifold_quality(&m);
1786 assert!(q > 0.0 && q <= 1.0, "Quality should be in (0,1], got {q}");
1787 }
1788}