1#![doc = include_str!("../README.md")]
2
3pub mod surface;
4use std::ops::{Add, AddAssign, Div, Mul, Sub};
5
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum GeometryError {
11 InvalidDimensions {
13 width: u32,
15 height: u32,
17 },
18 InvalidArgument(String),
20}
21
22impl std::fmt::Display for GeometryError {
23 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24 match self {
25 Self::InvalidDimensions { width, height } => {
26 write!(f, "invalid dimensions: {width}x{height}")
27 }
28 Self::InvalidArgument(message) => f.write_str(message),
29 }
30 }
31}
32
33impl std::error::Error for GeometryError {}
34
35pub type Result<T> = std::result::Result<T, GeometryError>;
37
38fn invalid_argument(message: impl Into<String>) -> GeometryError {
39 GeometryError::InvalidArgument(message.into())
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
43pub struct Point2f {
45 pub x: f32,
47 pub y: f32,
49}
50
51impl Point2f {
52 pub fn new(x: f32, y: f32) -> Result<Self> {
54 let point = Self { x, y };
55 point.validate()?;
56 Ok(point)
57 }
58
59 pub fn validate(self) -> Result<()> {
61 if !self.x.is_finite() || !self.y.is_finite() {
62 return Err(invalid_argument("2D point coordinates must be finite"));
63 }
64 Ok(())
65 }
66
67 pub fn translate(self, delta: Vector2f) -> Result<Self> {
69 delta.validate()?;
70 Self::new(self.x + delta.x, self.y + delta.y)
71 }
72
73 pub fn to_normalized(self, size: Size2u) -> Result<NormalizedPoint2> {
75 size.validate()?;
76 NormalizedPoint2::new(self.x / size.width as f32, self.y / size.height as f32)
77 }
78}
79
80impl Add<Vector2f> for Point2f {
81 type Output = Self;
82
83 fn add(self, rhs: Vector2f) -> Self::Output {
84 Self {
85 x: self.x + rhs.x,
86 y: self.y + rhs.y,
87 }
88 }
89}
90
91impl Sub<Vector2f> for Point2f {
92 type Output = Self;
93
94 fn sub(self, rhs: Vector2f) -> Self::Output {
95 Self {
96 x: self.x - rhs.x,
97 y: self.y - rhs.y,
98 }
99 }
100}
101
102impl Sub<Point2f> for Point2f {
103 type Output = Vector2f;
104
105 fn sub(self, rhs: Point2f) -> Self::Output {
106 Vector2f {
107 x: self.x - rhs.x,
108 y: self.y - rhs.y,
109 }
110 }
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
114pub struct Point2i {
116 pub x: i32,
118 pub y: i32,
120}
121
122impl Point2i {
123 pub const fn new(x: i32, y: i32) -> Self {
125 Self { x, y }
126 }
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
130pub struct Vector2f {
132 pub x: f32,
134 pub y: f32,
136}
137
138impl Vector2f {
139 pub fn new(x: f32, y: f32) -> Result<Self> {
141 let vector = Self { x, y };
142 vector.validate()?;
143 Ok(vector)
144 }
145
146 pub fn validate(self) -> Result<()> {
148 if !self.x.is_finite() || !self.y.is_finite() {
149 return Err(invalid_argument("2D vector components must be finite"));
150 }
151 Ok(())
152 }
153
154 pub fn length(self) -> Result<f32> {
156 self.validate()?;
157 Ok((self.x * self.x + self.y * self.y).sqrt())
158 }
159
160 pub fn dot(self, rhs: Self) -> Result<f32> {
162 self.validate()?;
163 rhs.validate()?;
164 Ok(self.x.mul_add(rhs.x, self.y * rhs.y))
165 }
166
167 pub fn cross(self, rhs: Self) -> Result<f32> {
169 self.validate()?;
170 rhs.validate()?;
171 Ok(self.x.mul_add(rhs.y, -(self.y * rhs.x)))
172 }
173
174 pub fn normalize(self) -> Result<Self> {
176 let length = self.length()?;
177 if length <= f32::EPSILON {
178 return Err(invalid_argument(
179 "2D vector length must be greater than zero",
180 ));
181 }
182 Self::new(self.x / length, self.y / length)
183 }
184}
185
186impl Add for Vector2f {
187 type Output = Self;
188
189 fn add(self, rhs: Self) -> Self::Output {
190 Self {
191 x: self.x + rhs.x,
192 y: self.y + rhs.y,
193 }
194 }
195}
196
197impl AddAssign for Vector2f {
198 fn add_assign(&mut self, rhs: Self) {
199 self.x += rhs.x;
200 self.y += rhs.y;
201 }
202}
203
204impl Sub for Vector2f {
205 type Output = Self;
206
207 fn sub(self, rhs: Self) -> Self::Output {
208 Self {
209 x: self.x - rhs.x,
210 y: self.y - rhs.y,
211 }
212 }
213}
214
215impl Mul<f32> for Vector2f {
216 type Output = Self;
217
218 fn mul(self, rhs: f32) -> Self::Output {
219 Self {
220 x: self.x * rhs,
221 y: self.y * rhs,
222 }
223 }
224}
225
226impl Mul<Vector2f> for f32 {
227 type Output = Vector2f;
228
229 fn mul(self, rhs: Vector2f) -> Self::Output {
230 rhs * self
231 }
232}
233
234impl Div<f32> for Vector2f {
235 type Output = Self;
236
237 fn div(self, rhs: f32) -> Self::Output {
238 Self {
239 x: self.x / rhs,
240 y: self.y / rhs,
241 }
242 }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
246pub struct Size2u {
248 pub width: u32,
250 pub height: u32,
252}
253
254impl Size2u {
255 pub fn new(width: u32, height: u32) -> Result<Self> {
257 let size = Self { width, height };
258 size.validate()?;
259 Ok(size)
260 }
261
262 pub fn validate(self) -> Result<()> {
264 if self.width == 0 || self.height == 0 {
265 return Err(GeometryError::InvalidDimensions {
266 width: self.width,
267 height: self.height,
268 });
269 }
270 Ok(())
271 }
272
273 pub fn area(self) -> Result<u64> {
275 self.validate()?;
276 Ok(self.width as u64 * self.height as u64)
277 }
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
281pub struct RectU32 {
283 pub x: u32,
285 pub y: u32,
287 pub width: u32,
289 pub height: u32,
291}
292
293impl RectU32 {
294 pub fn new(x: u32, y: u32, width: u32, height: u32) -> Result<Self> {
296 let rect = Self {
297 x,
298 y,
299 width,
300 height,
301 };
302 rect.validate()?;
303 Ok(rect)
304 }
305
306 pub fn validate(self) -> Result<()> {
308 if self.width == 0 || self.height == 0 {
309 return Err(GeometryError::InvalidDimensions {
310 width: self.width,
311 height: self.height,
312 });
313 }
314 let _ = self.max_x()?;
315 let _ = self.max_y()?;
316 Ok(())
317 }
318
319 pub fn max_x(self) -> Result<u32> {
321 self.x
322 .checked_add(self.width)
323 .ok_or_else(|| invalid_argument("rectangle x range overflows"))
324 }
325
326 pub fn max_y(self) -> Result<u32> {
328 self.y
329 .checked_add(self.height)
330 .ok_or_else(|| invalid_argument("rectangle y range overflows"))
331 }
332
333 pub fn contains(self, other: Self) -> Result<bool> {
335 self.validate()?;
336 other.validate()?;
337 Ok(self.x <= other.x
338 && self.y <= other.y
339 && self.max_x()? >= other.max_x()?
340 && self.max_y()? >= other.max_y()?)
341 }
342
343 pub fn contains_point(self, x: u32, y: u32) -> Result<bool> {
345 self.validate()?;
346 Ok(x >= self.x && x < self.max_x()? && y >= self.y && y < self.max_y()?)
347 }
348
349 pub fn intersects(self, other: Self) -> Result<bool> {
351 self.validate()?;
352 other.validate()?;
353 Ok(self.x < other.max_x()?
354 && other.x < self.max_x()?
355 && self.y < other.max_y()?
356 && other.y < self.max_y()?)
357 }
358
359 pub fn intersection(self, other: Self) -> Result<Option<Self>> {
361 if !self.intersects(other)? {
362 return Ok(None);
363 }
364 let x = self.x.max(other.x);
365 let y = self.y.max(other.y);
366 let max_x = self.max_x()?.min(other.max_x()?);
367 let max_y = self.max_y()?.min(other.max_y()?);
368 Self::new(x, y, max_x - x, max_y - y).map(Some)
369 }
370
371 pub fn iou(self, other: Self) -> Result<f32> {
373 let intersection_area = self
374 .intersection(other)?
375 .map(|rect| rect.area())
376 .transpose()?
377 .unwrap_or(0) as f32;
378 let union_area = self.area()? as f32 + other.area()? as f32 - intersection_area;
379 if union_area <= f32::EPSILON {
380 return Err(invalid_argument(
381 "rectangle union area must be greater than zero",
382 ));
383 }
384 Ok(intersection_area / union_area)
385 }
386
387 pub fn overlap_ratio(self, other: Self) -> Result<f32> {
389 let intersection_area = self
390 .intersection(other)?
391 .map(|rect| rect.area())
392 .transpose()?
393 .unwrap_or(0) as f32;
394 Ok(intersection_area / self.area()? as f32)
395 }
396
397 pub fn union(self, other: Self) -> Result<Self> {
399 self.validate()?;
400 other.validate()?;
401 let x = self.x.min(other.x);
402 let y = self.y.min(other.y);
403 let max_x = self.max_x()?.max(other.max_x()?);
404 let max_y = self.max_y()?.max(other.max_y()?);
405 Self::new(x, y, max_x - x, max_y - y)
406 }
407
408 pub fn clamp_to(self, bounds: Self) -> Result<Option<Self>> {
410 self.intersection(bounds)
411 }
412
413 pub fn translate(self, dx: i32, dy: i32) -> Result<Self> {
415 self.validate()?;
416 let x = self.x as i64 + dx as i64;
417 let y = self.y as i64 + dy as i64;
418 if x < 0 || y < 0 || x > u32::MAX as i64 || y > u32::MAX as i64 {
419 return Err(invalid_argument(
420 "translated rectangle is out of u32 bounds",
421 ));
422 }
423 Self::new(x as u32, y as u32, self.width, self.height)
424 }
425
426 pub fn scale(self, factor: f32) -> Result<Self> {
428 if !factor.is_finite() || factor <= 0.0 {
429 return Err(invalid_argument("scale factor must be finite and positive"));
430 }
431 let x = (self.x as f32 * factor).round();
432 let y = (self.y as f32 * factor).round();
433 let width = (self.width as f32 * factor).round();
434 let height = (self.height as f32 * factor).round();
435 if [x, y, width, height]
436 .iter()
437 .any(|value| *value < 0.0 || *value > u32::MAX as f32)
438 {
439 return Err(invalid_argument("scaled rectangle is out of u32 bounds"));
440 }
441 Self::new(x as u32, y as u32, width as u32, height as u32)
442 }
443
444 pub fn inflate(self, dx: i32, dy: i32) -> Result<Self> {
446 self.validate()?;
447 let x = self.x as i64 - dx as i64;
448 let y = self.y as i64 - dy as i64;
449 let width = self.width as i64 + 2 * dx as i64;
450 let height = self.height as i64 + 2 * dy as i64;
451 if x < 0 || y < 0 || width <= 0 || height <= 0 {
452 return Err(invalid_argument(
453 "inflated rectangle must stay within positive bounds",
454 ));
455 }
456 if x > u32::MAX as i64
457 || y > u32::MAX as i64
458 || width > u32::MAX as i64
459 || height > u32::MAX as i64
460 {
461 return Err(invalid_argument("inflated rectangle exceeds u32 bounds"));
462 }
463 Self::new(x as u32, y as u32, width as u32, height as u32)
464 }
465
466 pub fn center_f32(self) -> Point2f {
468 Point2f {
469 x: self.x as f32 + self.width as f32 / 2.0,
470 y: self.y as f32 + self.height as f32 / 2.0,
471 }
472 }
473
474 pub fn area(self) -> Result<u64> {
476 self.validate()?;
477 Ok(self.width as u64 * self.height as u64)
478 }
479
480 pub fn size(self) -> Size2u {
482 Size2u {
483 width: self.width,
484 height: self.height,
485 }
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
490pub struct CollisionPair {
492 pub left_index: usize,
494 pub right_index: usize,
496}
497
498impl CollisionPair {
499 pub fn ordered(left_index: usize, right_index: usize) -> Self {
501 if left_index <= right_index {
502 Self {
503 left_index,
504 right_index,
505 }
506 } else {
507 Self {
508 left_index: right_index,
509 right_index: left_index,
510 }
511 }
512 }
513}
514
515#[derive(Debug, Clone, Copy, PartialEq, Eq)]
516pub enum BroadPhase2Strategy {
518 Auto,
520 BruteForce,
522 SpatialHashGrid,
524 SweepAndPrune,
526}
527
528#[derive(Debug, Clone, Copy, PartialEq, Eq)]
529pub enum SpatialCellSize2 {
531 Auto,
533 Fixed {
535 width: u32,
537 height: u32,
539 },
540}
541
542#[derive(Debug, Clone, Copy, PartialEq, Eq)]
543pub struct BroadPhase2Config {
545 pub strategy: BroadPhase2Strategy,
547 pub brute_force_threshold: usize,
549 pub max_cells_per_item: usize,
551 pub cell_size: SpatialCellSize2,
553}
554
555impl Default for BroadPhase2Config {
556 fn default() -> Self {
557 Self {
558 strategy: BroadPhase2Strategy::Auto,
559 brute_force_threshold: 128,
560 max_cells_per_item: 1024,
561 cell_size: SpatialCellSize2::Auto,
562 }
563 }
564}
565
566impl BroadPhase2Config {
567 pub fn validate(self) -> Result<()> {
569 if self.max_cells_per_item == 0 {
570 return Err(invalid_argument(
571 "max_cells_per_item must be greater than zero",
572 ));
573 }
574 if let SpatialCellSize2::Fixed { width, height } = self.cell_size {
575 if width == 0 || height == 0 {
576 return Err(invalid_argument("fixed spatial cell size must be non-zero"));
577 }
578 }
579 Ok(())
580 }
581}
582
583#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584pub struct BroadPhaseStats {
586 pub object_count: usize,
588 pub cell_count: usize,
590 pub cell_entry_count: usize,
592 pub candidate_pair_count: usize,
594 pub selected_strategy: BroadPhase2Strategy,
596}
597
598impl Default for BroadPhaseStats {
599 fn default() -> Self {
600 Self {
601 object_count: 0,
602 cell_count: 0,
603 cell_entry_count: 0,
604 candidate_pair_count: 0,
605 selected_strategy: BroadPhase2Strategy::BruteForce,
606 }
607 }
608}
609
610#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
611struct GridCell2 {
612 x: u32,
613 y: u32,
614}
615
616#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
617struct GridEntry2 {
618 cell: GridCell2,
619 set: u8,
620 index: usize,
621}
622
623#[derive(Debug, Clone)]
624pub struct SpatialHashGrid2 {
626 config: BroadPhase2Config,
627 cell_width: u32,
628 cell_height: u32,
629 left_bounds: Vec<RectU32>,
630 right_bounds: Vec<RectU32>,
631 entries: Vec<GridEntry2>,
632 pairs: Vec<CollisionPair>,
633 stats: BroadPhaseStats,
634}
635
636impl SpatialHashGrid2 {
637 pub fn new(config: BroadPhase2Config) -> Result<Self> {
639 config.validate()?;
640 Ok(Self {
641 config,
642 cell_width: 1,
643 cell_height: 1,
644 left_bounds: Vec::new(),
645 right_bounds: Vec::new(),
646 entries: Vec::new(),
647 pairs: Vec::new(),
648 stats: BroadPhaseStats {
649 selected_strategy: config.strategy,
650 ..BroadPhaseStats::default()
651 },
652 })
653 }
654
655 pub fn rebuild(&mut self, bounds: &[RectU32]) -> Result<()> {
657 validate_rects(bounds)?;
658 let (cell_width, cell_height) = resolve_cell_size_2d(bounds, self.config.cell_size)?;
659 self.cell_width = cell_width;
660 self.cell_height = cell_height;
661 self.left_bounds.clear();
662 self.left_bounds.extend_from_slice(bounds);
663 self.right_bounds.clear();
664 self.entries.clear();
665 self.pairs.clear();
666 self.push_entries(bounds, 0)?;
667 self.stats = BroadPhaseStats {
668 object_count: bounds.len(),
669 cell_count: 0,
670 cell_entry_count: self.entries.len(),
671 candidate_pair_count: 0,
672 selected_strategy: BroadPhase2Strategy::SpatialHashGrid,
673 };
674 Ok(())
675 }
676
677 pub fn candidate_pairs(&mut self) -> Result<&[CollisionPair]> {
679 self.entries.sort_unstable();
680 self.pairs.clear();
681 let mut cell_count = 0;
682 let mut start = 0;
683 while start < self.entries.len() {
684 let cell = self.entries[start].cell;
685 let mut end = start + 1;
686 while end < self.entries.len() && self.entries[end].cell == cell {
687 end += 1;
688 }
689 cell_count += 1;
690 for left in start..end {
691 for right in (left + 1)..end {
692 let left_index = self.entries[left].index;
693 let right_index = self.entries[right].index;
694 if self.left_bounds[left_index].intersects(self.left_bounds[right_index])? {
695 self.pairs
696 .push(CollisionPair::ordered(left_index, right_index));
697 }
698 }
699 }
700 start = end;
701 }
702 finish_pairs(&mut self.pairs);
703 self.stats.cell_count = cell_count;
704 self.stats.candidate_pair_count = self.pairs.len();
705 Ok(&self.pairs)
706 }
707
708 pub fn candidate_pairs_between(
710 &mut self,
711 left: &[RectU32],
712 right: &[RectU32],
713 ) -> Result<&[CollisionPair]> {
714 validate_rects(left)?;
715 validate_rects(right)?;
716 let (cell_width, cell_height) =
717 resolve_cell_size_2d_for_sets(left, right, self.config.cell_size)?;
718 self.cell_width = cell_width;
719 self.cell_height = cell_height;
720 self.left_bounds.clear();
721 self.left_bounds.extend_from_slice(left);
722 self.right_bounds.clear();
723 self.right_bounds.extend_from_slice(right);
724 self.entries.clear();
725 self.pairs.clear();
726 self.push_entries(left, 0)?;
727 self.push_entries(right, 1)?;
728 self.entries.sort_unstable();
729
730 let mut cell_count = 0;
731 let mut start = 0;
732 while start < self.entries.len() {
733 let cell = self.entries[start].cell;
734 let mut end = start + 1;
735 while end < self.entries.len() && self.entries[end].cell == cell {
736 end += 1;
737 }
738 cell_count += 1;
739 for left_entry in start..end {
740 if self.entries[left_entry].set != 0 {
741 continue;
742 }
743 for right_entry in start..end {
744 if self.entries[right_entry].set == 1
745 && self.left_bounds[self.entries[left_entry].index]
746 .intersects(self.right_bounds[self.entries[right_entry].index])?
747 {
748 self.pairs.push(CollisionPair {
749 left_index: self.entries[left_entry].index,
750 right_index: self.entries[right_entry].index,
751 });
752 }
753 }
754 }
755 start = end;
756 }
757 finish_pairs(&mut self.pairs);
758 self.stats = BroadPhaseStats {
759 object_count: left.len() + right.len(),
760 cell_count,
761 cell_entry_count: self.entries.len(),
762 candidate_pair_count: self.pairs.len(),
763 selected_strategy: BroadPhase2Strategy::SpatialHashGrid,
764 };
765 Ok(&self.pairs)
766 }
767
768 pub fn stats(&self) -> BroadPhaseStats {
770 self.stats
771 }
772
773 fn push_entries(&mut self, bounds: &[RectU32], set: u8) -> Result<()> {
774 for (index, rect) in bounds.iter().copied().enumerate() {
775 let (min_cell, max_cell) = rect_cell_range_2d(rect, self.cell_width, self.cell_height)?;
776 for y in min_cell.y..=max_cell.y {
777 for x in min_cell.x..=max_cell.x {
778 self.entries.push(GridEntry2 {
779 cell: GridCell2 { x, y },
780 set,
781 index,
782 });
783 }
784 }
785 }
786 Ok(())
787 }
788}
789
790pub fn broad_phase_pairs_2d(
792 bounds: &[RectU32],
793 config: BroadPhase2Config,
794) -> Result<Vec<CollisionPair>> {
795 config.validate()?;
796 validate_rects(bounds)?;
797 let (strategy, stats) = select_strategy_2d(bounds, config)?;
798 match strategy {
799 BroadPhase2Strategy::Auto => unreachable!("auto strategy must resolve before execution"),
800 BroadPhase2Strategy::BruteForce => Ok(brute_force_pairs_2d(bounds)),
801 BroadPhase2Strategy::SweepAndPrune => Ok(sweep_and_prune_pairs_2d(bounds)?),
802 BroadPhase2Strategy::SpatialHashGrid => {
803 let mut grid = SpatialHashGrid2::new(BroadPhase2Config { strategy, ..config })?;
804 grid.rebuild(bounds)?;
805 let pairs = grid.candidate_pairs()?.to_vec();
806 let _ = stats;
807 Ok(pairs)
808 }
809 }
810}
811
812fn select_strategy_2d(
813 bounds: &[RectU32],
814 config: BroadPhase2Config,
815) -> Result<(BroadPhase2Strategy, BroadPhaseStats)> {
816 let selected = match config.strategy {
817 BroadPhase2Strategy::Auto if bounds.len() <= config.brute_force_threshold => {
818 BroadPhase2Strategy::BruteForce
819 }
820 BroadPhase2Strategy::Auto => {
821 let (cell_width, cell_height) = resolve_cell_size_2d(bounds, config.cell_size)?;
822 if bounds.iter().copied().any(|rect| {
823 rect_cell_count_2d(rect, cell_width, cell_height)
824 .map(|count| count > config.max_cells_per_item)
825 .unwrap_or(true)
826 }) {
827 BroadPhase2Strategy::SweepAndPrune
828 } else {
829 BroadPhase2Strategy::SpatialHashGrid
830 }
831 }
832 strategy => strategy,
833 };
834 Ok((
835 selected,
836 BroadPhaseStats {
837 object_count: bounds.len(),
838 selected_strategy: selected,
839 ..BroadPhaseStats::default()
840 },
841 ))
842}
843
844fn brute_force_pairs_2d(bounds: &[RectU32]) -> Vec<CollisionPair> {
845 let mut pairs = Vec::new();
846 for left_index in 0..bounds.len() {
847 for right_index in (left_index + 1)..bounds.len() {
848 if bounds[left_index]
849 .intersects(bounds[right_index])
850 .unwrap_or(false)
851 {
852 pairs.push(CollisionPair {
853 left_index,
854 right_index,
855 });
856 }
857 }
858 }
859 pairs
860}
861
862fn sweep_and_prune_pairs_2d(bounds: &[RectU32]) -> Result<Vec<CollisionPair>> {
863 let mut ordered = bounds
864 .iter()
865 .copied()
866 .enumerate()
867 .map(|(index, rect)| Ok((index, rect.x, rect.max_x()?, rect)))
868 .collect::<Result<Vec<_>>>()?;
869 ordered.sort_unstable_by_key(|(index, min_x, _, _)| (*min_x, *index));
870
871 let mut pairs = Vec::new();
872 let mut active = Vec::<(usize, u32, RectU32)>::new();
873 for (index, min_x, max_x, rect) in ordered {
874 active.retain(|(_, active_max_x, _)| *active_max_x > min_x);
875 for (active_index, _, active_rect) in &active {
876 if active_rect.intersects(rect)? {
877 pairs.push(CollisionPair::ordered(*active_index, index));
878 }
879 }
880 active.push((index, max_x, rect));
881 }
882 finish_pairs(&mut pairs);
883 Ok(pairs)
884}
885
886fn resolve_cell_size_2d(bounds: &[RectU32], cell_size: SpatialCellSize2) -> Result<(u32, u32)> {
887 match cell_size {
888 SpatialCellSize2::Fixed { width, height } => {
889 if width == 0 || height == 0 {
890 return Err(invalid_argument("fixed spatial cell size must be non-zero"));
891 }
892 Ok((width, height))
893 }
894 SpatialCellSize2::Auto => {
895 if bounds.is_empty() {
896 return Ok((1, 1));
897 }
898 let mut widths = bounds.iter().map(|rect| rect.width).collect::<Vec<_>>();
899 let mut heights = bounds.iter().map(|rect| rect.height).collect::<Vec<_>>();
900 widths.sort_unstable();
901 heights.sort_unstable();
902 Ok((
903 widths[widths.len() / 2].max(1),
904 heights[heights.len() / 2].max(1),
905 ))
906 }
907 }
908}
909
910fn resolve_cell_size_2d_for_sets(
911 left: &[RectU32],
912 right: &[RectU32],
913 cell_size: SpatialCellSize2,
914) -> Result<(u32, u32)> {
915 match cell_size {
916 SpatialCellSize2::Fixed { .. } => resolve_cell_size_2d(left, cell_size),
917 SpatialCellSize2::Auto => {
918 let mut widths = left
919 .iter()
920 .chain(right.iter())
921 .map(|rect| rect.width)
922 .collect::<Vec<_>>();
923 let mut heights = left
924 .iter()
925 .chain(right.iter())
926 .map(|rect| rect.height)
927 .collect::<Vec<_>>();
928 if widths.is_empty() {
929 return Ok((1, 1));
930 }
931 widths.sort_unstable();
932 heights.sort_unstable();
933 Ok((
934 widths[widths.len() / 2].max(1),
935 heights[heights.len() / 2].max(1),
936 ))
937 }
938 }
939}
940
941fn rect_cell_range_2d(
942 rect: RectU32,
943 cell_width: u32,
944 cell_height: u32,
945) -> Result<(GridCell2, GridCell2)> {
946 let max_x = rect.max_x()?.saturating_sub(1);
947 let max_y = rect.max_y()?.saturating_sub(1);
948 Ok((
949 GridCell2 {
950 x: rect.x / cell_width,
951 y: rect.y / cell_height,
952 },
953 GridCell2 {
954 x: max_x / cell_width,
955 y: max_y / cell_height,
956 },
957 ))
958}
959
960fn rect_cell_count_2d(rect: RectU32, cell_width: u32, cell_height: u32) -> Result<usize> {
961 let (min, max) = rect_cell_range_2d(rect, cell_width, cell_height)?;
962 let width = (max.x - min.x + 1) as usize;
963 let height = (max.y - min.y + 1) as usize;
964 Ok(width.saturating_mul(height))
965}
966
967fn validate_rects(bounds: &[RectU32]) -> Result<()> {
968 for rect in bounds {
969 rect.validate()?;
970 }
971 Ok(())
972}
973
974fn finish_pairs(pairs: &mut Vec<CollisionPair>) {
975 pairs.sort_unstable();
976 pairs.dedup();
977}
978
979#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
980pub struct RectF32 {
982 pub x: f32,
984 pub y: f32,
986 pub width: f32,
988 pub height: f32,
990}
991
992impl RectF32 {
993 pub fn new(x: f32, y: f32, width: f32, height: f32) -> Result<Self> {
995 let rect = Self {
996 x,
997 y,
998 width,
999 height,
1000 };
1001 rect.validate()?;
1002 Ok(rect)
1003 }
1004
1005 pub fn validate(self) -> Result<()> {
1007 if [self.x, self.y, self.width, self.height]
1008 .iter()
1009 .any(|value| !value.is_finite())
1010 {
1011 return Err(invalid_argument("rectangle values must be finite"));
1012 }
1013 if self.width <= 0.0 || self.height <= 0.0 {
1014 return Err(invalid_argument(
1015 "rectangle width and height must be greater than zero",
1016 ));
1017 }
1018 Ok(())
1019 }
1020
1021 pub fn max_x(self) -> Result<f32> {
1023 self.validate()?;
1024 Ok(self.x + self.width)
1025 }
1026
1027 pub fn max_y(self) -> Result<f32> {
1029 self.validate()?;
1030 Ok(self.y + self.height)
1031 }
1032
1033 pub fn contains_point(self, point: Point2f) -> Result<bool> {
1035 self.validate()?;
1036 point.validate()?;
1037 Ok(point.x >= self.x
1038 && point.x <= self.max_x()?
1039 && point.y >= self.y
1040 && point.y <= self.max_y()?)
1041 }
1042
1043 pub fn intersects(self, other: Self) -> Result<bool> {
1045 self.validate()?;
1046 other.validate()?;
1047 Ok(self.x < other.max_x()?
1048 && other.x < self.max_x()?
1049 && self.y < other.max_y()?
1050 && other.y < self.max_y()?)
1051 }
1052
1053 pub fn intersection(self, other: Self) -> Result<Option<Self>> {
1055 if !self.intersects(other)? {
1056 return Ok(None);
1057 }
1058 let x = self.x.max(other.x);
1059 let y = self.y.max(other.y);
1060 let max_x = self.max_x()?.min(other.max_x()?);
1061 let max_y = self.max_y()?.min(other.max_y()?);
1062 Self::new(x, y, max_x - x, max_y - y).map(Some)
1063 }
1064
1065 pub fn iou(self, other: Self) -> Result<f32> {
1067 let intersection_area = self
1068 .intersection(other)?
1069 .map(|rect| rect.area())
1070 .transpose()?
1071 .unwrap_or(0.0);
1072 let union_area = self.area()? + other.area()? - intersection_area;
1073 if union_area <= f32::EPSILON {
1074 return Err(invalid_argument(
1075 "rectangle union area must be greater than zero",
1076 ));
1077 }
1078 Ok(intersection_area / union_area)
1079 }
1080
1081 pub fn overlap_ratio(self, other: Self) -> Result<f32> {
1083 let intersection_area = self
1084 .intersection(other)?
1085 .map(|rect| rect.area())
1086 .transpose()?
1087 .unwrap_or(0.0);
1088 Ok(intersection_area / self.area()?)
1089 }
1090
1091 pub fn union(self, other: Self) -> Result<Self> {
1093 self.validate()?;
1094 other.validate()?;
1095 let x = self.x.min(other.x);
1096 let y = self.y.min(other.y);
1097 let max_x = self.max_x()?.max(other.max_x()?);
1098 let max_y = self.max_y()?.max(other.max_y()?);
1099 Self::new(x, y, max_x - x, max_y - y)
1100 }
1101
1102 pub fn clamp_to(self, bounds: Self) -> Result<Option<Self>> {
1104 self.intersection(bounds)
1105 }
1106
1107 pub fn translate(self, delta: Vector2f) -> Result<Self> {
1109 delta.validate()?;
1110 Self::new(self.x + delta.x, self.y + delta.y, self.width, self.height)
1111 }
1112
1113 pub fn scale(self, factor: f32) -> Result<Self> {
1115 if !factor.is_finite() || factor <= 0.0 {
1116 return Err(invalid_argument("scale factor must be finite and positive"));
1117 }
1118 Self::new(
1119 self.x * factor,
1120 self.y * factor,
1121 self.width * factor,
1122 self.height * factor,
1123 )
1124 }
1125
1126 pub fn inflate(self, dx: f32, dy: f32) -> Result<Self> {
1128 if !dx.is_finite() || !dy.is_finite() {
1129 return Err(invalid_argument("inflate deltas must be finite"));
1130 }
1131 Self::new(
1132 self.x - dx,
1133 self.y - dy,
1134 self.width + 2.0 * dx,
1135 self.height + 2.0 * dy,
1136 )
1137 }
1138
1139 pub fn center(self) -> Result<Point2f> {
1141 self.validate()?;
1142 Point2f::new(self.x + self.width / 2.0, self.y + self.height / 2.0)
1143 }
1144
1145 pub fn area(self) -> Result<f32> {
1147 self.validate()?;
1148 Ok(self.width * self.height)
1149 }
1150}
1151
1152#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1153pub struct NormalizedPoint2 {
1155 pub x: f32,
1157 pub y: f32,
1159}
1160
1161impl NormalizedPoint2 {
1162 pub fn new(x: f32, y: f32) -> Result<Self> {
1164 let point = Self { x, y };
1165 point.validate()?;
1166 Ok(point)
1167 }
1168
1169 pub fn validate(self) -> Result<()> {
1171 if !self.x.is_finite() || !self.y.is_finite() {
1172 return Err(invalid_argument("normalized coordinates must be finite"));
1173 }
1174 if !(0.0..=1.0).contains(&self.x) || !(0.0..=1.0).contains(&self.y) {
1175 return Err(invalid_argument(
1176 "normalized coordinates must be within the inclusive range [0, 1]",
1177 ));
1178 }
1179 Ok(())
1180 }
1181
1182 pub fn to_pixel_point(self, size: Size2u) -> Point2i {
1184 Point2i {
1185 x: (self.x * size.width as f32).round() as i32,
1186 y: (self.y * size.height as f32).round() as i32,
1187 }
1188 }
1189
1190 pub fn to_pixel_point_f32(self, size: Size2u) -> Point2f {
1192 Point2f {
1193 x: self.x * size.width as f32,
1194 y: self.y * size.height as f32,
1195 }
1196 }
1197}
1198
1199#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1200pub struct Affine2 {
1202 pub m11: f32,
1204 pub m12: f32,
1206 pub m21: f32,
1208 pub m22: f32,
1210 pub tx: f32,
1212 pub ty: f32,
1214}
1215
1216impl Affine2 {
1217 pub const fn identity() -> Self {
1219 Self {
1220 m11: 1.0,
1221 m12: 0.0,
1222 m21: 0.0,
1223 m22: 1.0,
1224 tx: 0.0,
1225 ty: 0.0,
1226 }
1227 }
1228
1229 pub const fn translation(tx: f32, ty: f32) -> Self {
1231 Self {
1232 tx,
1233 ty,
1234 ..Self::identity()
1235 }
1236 }
1237
1238 pub const fn scaling(sx: f32, sy: f32) -> Self {
1240 Self {
1241 m11: sx,
1242 m12: 0.0,
1243 m21: 0.0,
1244 m22: sy,
1245 tx: 0.0,
1246 ty: 0.0,
1247 }
1248 }
1249
1250 pub fn validate(self) -> Result<()> {
1252 if [self.m11, self.m12, self.m21, self.m22, self.tx, self.ty]
1253 .iter()
1254 .any(|value| !value.is_finite())
1255 {
1256 return Err(invalid_argument("affine transform values must be finite"));
1257 }
1258 Ok(())
1259 }
1260
1261 pub fn determinant(self) -> Result<f32> {
1263 self.validate()?;
1264 Ok(self.m11 * self.m22 - self.m12 * self.m21)
1265 }
1266
1267 pub fn apply_point(self, point: Point2f) -> Point2f {
1269 Point2f {
1270 x: self.m11 * point.x + self.m12 * point.y + self.tx,
1271 y: self.m21 * point.x + self.m22 * point.y + self.ty,
1272 }
1273 }
1274
1275 pub fn invert(self) -> Result<Self> {
1277 self.validate()?;
1278 let det = self.determinant()?;
1279 if det.abs() <= f32::EPSILON {
1280 return Err(invalid_argument("affine transform is not invertible"));
1281 }
1282 let inv_det = 1.0 / det;
1283 let m11 = self.m22 * inv_det;
1284 let m12 = -self.m12 * inv_det;
1285 let m21 = -self.m21 * inv_det;
1286 let m22 = self.m11 * inv_det;
1287 let tx = -(m11 * self.tx + m12 * self.ty);
1288 let ty = -(m21 * self.tx + m22 * self.ty);
1289 Ok(Self {
1290 m11,
1291 m12,
1292 m21,
1293 m22,
1294 tx,
1295 ty,
1296 })
1297 }
1298
1299 pub fn compose(self, next: Self) -> Result<Self> {
1301 self.validate()?;
1302 next.validate()?;
1303 Ok(Self {
1304 m11: next.m11 * self.m11 + next.m12 * self.m21,
1305 m12: next.m11 * self.m12 + next.m12 * self.m22,
1306 m21: next.m21 * self.m11 + next.m22 * self.m21,
1307 m22: next.m21 * self.m12 + next.m22 * self.m22,
1308 tx: next.m11 * self.tx + next.m12 * self.ty + next.tx,
1309 ty: next.m21 * self.tx + next.m22 * self.ty + next.ty,
1310 })
1311 }
1312}
1313
1314#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1315pub struct LineSegment2 {
1317 pub start: Point2f,
1319 pub end: Point2f,
1321}
1322
1323#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1324pub struct LineIntersection2 {
1326 pub point: Point2f,
1328 pub left_t: f32,
1330 pub right_t: f32,
1332}
1333
1334impl LineSegment2 {
1335 pub fn new(start: Point2f, end: Point2f) -> Result<Self> {
1337 start.validate()?;
1338 end.validate()?;
1339 if start == end {
1340 return Err(invalid_argument(
1341 "line segment start and end must not be identical",
1342 ));
1343 }
1344 Ok(Self { start, end })
1345 }
1346
1347 pub fn length(self) -> Result<f32> {
1349 (self.end - self.start).length()
1350 }
1351
1352 pub fn midpoint(self) -> Point2f {
1354 self.start + ((self.end - self.start) * 0.5)
1355 }
1356
1357 pub fn direction(self) -> Result<Vector2f> {
1359 (self.end - self.start).normalize()
1360 }
1361
1362 pub fn intersection(self, other: Self) -> Result<Option<LineIntersection2>> {
1364 let p = self.start;
1365 let r = self.end - self.start;
1366 let q = other.start;
1367 let s = other.end - other.start;
1368 p.validate()?;
1369 q.validate()?;
1370 let denominator = r.cross(s)?;
1371 let q_minus_p = q - p;
1372 if denominator.abs() <= f32::EPSILON {
1373 return Ok(None);
1374 }
1375 let left_t = q_minus_p.cross(s)? / denominator;
1376 let right_t = q_minus_p.cross(r)? / denominator;
1377 if !(-f32::EPSILON..=1.0 + f32::EPSILON).contains(&left_t)
1378 || !(-f32::EPSILON..=1.0 + f32::EPSILON).contains(&right_t)
1379 {
1380 return Ok(None);
1381 }
1382 let point = p + r * left_t.clamp(0.0, 1.0);
1383 Ok(Some(LineIntersection2 {
1384 point,
1385 left_t,
1386 right_t,
1387 }))
1388 }
1389}
1390
1391#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1392pub struct Circle2 {
1394 pub center: Point2f,
1396 pub radius: f32,
1398}
1399
1400impl Circle2 {
1401 pub fn new(center: Point2f, radius: f32) -> Result<Self> {
1403 center.validate()?;
1404 if !radius.is_finite() || radius <= 0.0 {
1405 return Err(invalid_argument(
1406 "circle radius must be finite and greater than zero",
1407 ));
1408 }
1409 Ok(Self { center, radius })
1410 }
1411
1412 pub fn area(self) -> Result<f32> {
1414 self.center.validate()?;
1415 if !self.radius.is_finite() || self.radius <= 0.0 {
1416 return Err(invalid_argument(
1417 "circle radius must be finite and greater than zero",
1418 ));
1419 }
1420 Ok(std::f32::consts::PI * self.radius * self.radius)
1421 }
1422
1423 pub fn contains_point(self, point: Point2f) -> Result<bool> {
1425 point.validate()?;
1426 Ok((point - self.center).length()? <= self.radius)
1427 }
1428}
1429
1430#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1431pub struct Polygon2f {
1433 points: Vec<Point2f>,
1434}
1435
1436impl Polygon2f {
1437 pub fn new(points: impl Into<Vec<Point2f>>) -> Result<Self> {
1439 let polygon = Self {
1440 points: points.into(),
1441 };
1442 polygon.validate()?;
1443 Ok(polygon)
1444 }
1445
1446 pub fn points(&self) -> &[Point2f] {
1448 &self.points
1449 }
1450
1451 pub fn validate(&self) -> Result<()> {
1453 if self.points.len() < 3 {
1454 return Err(invalid_argument(
1455 "polygon must contain at least three points",
1456 ));
1457 }
1458 for point in &self.points {
1459 point.validate()?;
1460 }
1461 Ok(())
1462 }
1463
1464 pub fn bounds(&self) -> Result<Bounds2f> {
1466 self.validate()?;
1467 let mut min_x = self.points[0].x;
1468 let mut min_y = self.points[0].y;
1469 let mut max_x = self.points[0].x;
1470 let mut max_y = self.points[0].y;
1471 for point in &self.points[1..] {
1472 min_x = min_x.min(point.x);
1473 min_y = min_y.min(point.y);
1474 max_x = max_x.max(point.x);
1475 max_y = max_y.max(point.y);
1476 }
1477 Bounds2f::new(
1478 Point2f { x: min_x, y: min_y },
1479 Point2f { x: max_x, y: max_y },
1480 )
1481 }
1482
1483 pub fn signed_area(&self) -> Result<f32> {
1485 self.validate()?;
1486 let mut area = 0.0_f32;
1487 for index in 0..self.points.len() {
1488 let current = self.points[index];
1489 let next = self.points[(index + 1) % self.points.len()];
1490 area += current.x * next.y - next.x * current.y;
1491 }
1492 Ok(area * 0.5)
1493 }
1494
1495 pub fn area(&self) -> Result<f32> {
1497 Ok(self.signed_area()?.abs())
1498 }
1499
1500 pub fn is_clockwise(&self) -> Result<bool> {
1502 Ok(self.signed_area()? < 0.0)
1503 }
1504
1505 pub fn centroid(&self) -> Result<Point2f> {
1507 self.validate()?;
1508 let signed_area = self.signed_area()?;
1509 if signed_area.abs() <= f32::EPSILON {
1510 let sum = self
1511 .points
1512 .iter()
1513 .fold(Vector2f { x: 0.0, y: 0.0 }, |sum, point| {
1514 sum + Vector2f {
1515 x: point.x,
1516 y: point.y,
1517 }
1518 });
1519 return Point2f::new(
1520 sum.x / self.points.len() as f32,
1521 sum.y / self.points.len() as f32,
1522 );
1523 }
1524 let mut cx = 0.0_f32;
1525 let mut cy = 0.0_f32;
1526 for index in 0..self.points.len() {
1527 let current = self.points[index];
1528 let next = self.points[(index + 1) % self.points.len()];
1529 let cross = current.x * next.y - next.x * current.y;
1530 cx += (current.x + next.x) * cross;
1531 cy += (current.y + next.y) * cross;
1532 }
1533 let scale = 1.0 / (6.0 * signed_area);
1534 Point2f::new(cx * scale, cy * scale)
1535 }
1536
1537 pub fn contains_point(&self, point: Point2f) -> Result<bool> {
1539 self.validate()?;
1540 point.validate()?;
1541 let mut inside = false;
1542 let mut previous = self.points.len() - 1;
1543 for current in 0..self.points.len() {
1544 let a = self.points[current];
1545 let b = self.points[previous];
1546 if ((a.y > point.y) != (b.y > point.y))
1547 && point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x
1548 {
1549 inside = !inside;
1550 }
1551 previous = current;
1552 }
1553 Ok(inside)
1554 }
1555}
1556
1557#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
1558pub struct Bounds2f {
1560 pub min: Point2f,
1562 pub max: Point2f,
1564}
1565
1566impl Bounds2f {
1567 pub fn new(min: Point2f, max: Point2f) -> Result<Self> {
1569 min.validate()?;
1570 max.validate()?;
1571 if min.x > max.x || min.y > max.y {
1572 return Err(invalid_argument("bounds min must not exceed max"));
1573 }
1574 Ok(Self { min, max })
1575 }
1576
1577 pub fn from_points(points: &[Point2f]) -> Result<Self> {
1579 if points.is_empty() {
1580 return Err(invalid_argument("bounds require at least one point"));
1581 }
1582 let mut min_x = points[0].x;
1583 let mut min_y = points[0].y;
1584 let mut max_x = points[0].x;
1585 let mut max_y = points[0].y;
1586 for point in points {
1587 point.validate()?;
1588 min_x = min_x.min(point.x);
1589 min_y = min_y.min(point.y);
1590 max_x = max_x.max(point.x);
1591 max_y = max_y.max(point.y);
1592 }
1593 Self::new(Point2f::new(min_x, min_y)?, Point2f::new(max_x, max_y)?)
1594 }
1595
1596 pub fn width(self) -> f32 {
1598 self.max.x - self.min.x
1599 }
1600
1601 pub fn height(self) -> f32 {
1603 self.max.y - self.min.y
1604 }
1605
1606 pub fn center(self) -> Result<Point2f> {
1608 Point2f::new(
1609 self.min.x + self.width() / 2.0,
1610 self.min.y + self.height() / 2.0,
1611 )
1612 }
1613
1614 pub fn expand(self, padding: f32) -> Result<Self> {
1616 if !padding.is_finite() || padding < 0.0 {
1617 return Err(invalid_argument(
1618 "bounds padding must be finite and non-negative",
1619 ));
1620 }
1621 Self::new(
1622 Point2f::new(self.min.x - padding, self.min.y - padding)?,
1623 Point2f::new(self.max.x + padding, self.max.y + padding)?,
1624 )
1625 }
1626
1627 pub fn contains(self, point: Point2f) -> Result<bool> {
1629 point.validate()?;
1630 Ok(point.x >= self.min.x
1631 && point.x <= self.max.x
1632 && point.y >= self.min.y
1633 && point.y <= self.max.y)
1634 }
1635
1636 pub fn union(self, other: Self) -> Result<Self> {
1638 Self::new(
1639 Point2f {
1640 x: self.min.x.min(other.min.x),
1641 y: self.min.y.min(other.min.y),
1642 },
1643 Point2f {
1644 x: self.max.x.max(other.max.x),
1645 y: self.max.y.max(other.max.y),
1646 },
1647 )
1648 }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653 use super::*;
1654
1655 #[test]
1656 fn rejects_invalid_rectangles_and_non_finite_points() {
1657 assert!(RectU32::new(0, 0, 0, 2).is_err());
1658 assert!(RectF32::new(0.0, 0.0, f32::NAN, 1.0).is_err());
1659 assert!(Point2f::new(f32::INFINITY, 0.0).is_err());
1660 }
1661
1662 #[test]
1663 fn computes_intersection_and_union() {
1664 let a = RectU32::new(0, 0, 10, 10).unwrap();
1665 let b = RectU32::new(5, 5, 10, 10).unwrap();
1666 assert_eq!(
1667 a.intersection(b).unwrap(),
1668 Some(RectU32::new(5, 5, 5, 5).unwrap())
1669 );
1670 assert_eq!(a.union(b).unwrap(), RectU32::new(0, 0, 15, 15).unwrap());
1671 }
1672
1673 #[test]
1674 fn vector_and_shape_helpers_cover_common_2d_operations() {
1675 let a = Point2f::new(0.0, 0.0).unwrap();
1676 let b = Point2f::new(3.0, 4.0).unwrap();
1677 let segment = LineSegment2::new(a, b).unwrap();
1678 assert_eq!(segment.length().unwrap(), 5.0);
1679 assert_eq!(segment.midpoint(), Point2f::new(1.5, 2.0).unwrap());
1680
1681 let x = Vector2f::new(1.0, 0.0).unwrap();
1682 let y = Vector2f::new(0.0, 1.0).unwrap();
1683 assert_eq!(x.dot(y).unwrap(), 0.0);
1684 assert_eq!(x.cross(y).unwrap(), 1.0);
1685
1686 let circle = Circle2::new(a, 2.0).unwrap();
1687 assert!(circle
1688 .contains_point(Point2f::new(1.0, 1.0).unwrap())
1689 .unwrap());
1690 }
1691
1692 #[test]
1693 fn polygon_reports_area_centroid_winding_and_containment() {
1694 let polygon = Polygon2f::new([
1695 Point2f::new(0.0, 0.0).unwrap(),
1696 Point2f::new(2.0, 0.0).unwrap(),
1697 Point2f::new(2.0, 2.0).unwrap(),
1698 Point2f::new(0.0, 2.0).unwrap(),
1699 ])
1700 .unwrap();
1701 assert_eq!(polygon.area().unwrap(), 4.0);
1702 assert!(!polygon.is_clockwise().unwrap());
1703 assert_eq!(polygon.centroid().unwrap(), Point2f::new(1.0, 1.0).unwrap());
1704 assert!(polygon
1705 .contains_point(Point2f::new(1.0, 1.0).unwrap())
1706 .unwrap());
1707 }
1708
1709 #[test]
1710 fn affine_round_trip_recovers_point() {
1711 let point = Point2f::new(4.0, 5.0).unwrap();
1712 let affine = Affine2::translation(3.0, -2.0)
1713 .compose(Affine2::scaling(2.0, 0.5))
1714 .unwrap();
1715 let restored = affine
1716 .invert()
1717 .unwrap()
1718 .apply_point(affine.apply_point(point));
1719 assert!((restored.x - point.x).abs() < 1.0e-5);
1720 assert!((restored.y - point.y).abs() < 1.0e-5);
1721 }
1722
1723 #[test]
1724 fn converts_between_normalized_and_pixel_coordinates() {
1725 let point = NormalizedPoint2::new(0.25, 0.5).unwrap();
1726 let size = Size2u::new(200, 100).unwrap();
1727 assert_eq!(point.to_pixel_point(size), Point2i::new(50, 50));
1728 let normalized = Point2f::new(50.0, 50.0)
1729 .unwrap()
1730 .to_normalized(size)
1731 .unwrap();
1732 assert!((normalized.x - 0.25).abs() < 1.0e-6);
1733 assert!((normalized.y - 0.5).abs() < 1.0e-6);
1734 }
1735
1736 #[test]
1737 fn broad_phase_strategies_match_for_mixed_rectangles() {
1738 let rects = [
1739 RectU32::new(0, 0, 10, 10).unwrap(),
1740 RectU32::new(5, 5, 8, 8).unwrap(),
1741 RectU32::new(30, 30, 4, 4).unwrap(),
1742 RectU32::new(31, 31, 4, 4).unwrap(),
1743 RectU32::new(100, 0, 10, 10).unwrap(),
1744 ];
1745 let brute = broad_phase_pairs_2d(
1746 &rects,
1747 BroadPhase2Config {
1748 strategy: BroadPhase2Strategy::BruteForce,
1749 ..BroadPhase2Config::default()
1750 },
1751 )
1752 .unwrap();
1753 let sweep = broad_phase_pairs_2d(
1754 &rects,
1755 BroadPhase2Config {
1756 strategy: BroadPhase2Strategy::SweepAndPrune,
1757 ..BroadPhase2Config::default()
1758 },
1759 )
1760 .unwrap();
1761 let grid = broad_phase_pairs_2d(
1762 &rects,
1763 BroadPhase2Config {
1764 strategy: BroadPhase2Strategy::SpatialHashGrid,
1765 cell_size: SpatialCellSize2::Fixed {
1766 width: 8,
1767 height: 8,
1768 },
1769 ..BroadPhase2Config::default()
1770 },
1771 )
1772 .unwrap();
1773
1774 assert_eq!(brute, sweep);
1775 assert_eq!(brute, grid);
1776 assert_eq!(
1777 brute,
1778 vec![
1779 CollisionPair {
1780 left_index: 0,
1781 right_index: 1
1782 },
1783 CollisionPair {
1784 left_index: 2,
1785 right_index: 3
1786 }
1787 ]
1788 );
1789 }
1790
1791 #[test]
1792 fn broad_phase_handles_touching_nested_and_spanning_rectangles() {
1793 let touching = [
1794 RectU32::new(0, 0, 10, 10).unwrap(),
1795 RectU32::new(10, 0, 10, 10).unwrap(),
1796 ];
1797 assert!(
1798 broad_phase_pairs_2d(&touching, BroadPhase2Config::default())
1799 .unwrap()
1800 .is_empty()
1801 );
1802
1803 let nested = [
1804 RectU32::new(0, 0, 20, 20).unwrap(),
1805 RectU32::new(2, 2, 4, 4).unwrap(),
1806 ];
1807 assert_eq!(
1808 broad_phase_pairs_2d(
1809 &nested,
1810 BroadPhase2Config {
1811 strategy: BroadPhase2Strategy::SpatialHashGrid,
1812 cell_size: SpatialCellSize2::Fixed {
1813 width: 2,
1814 height: 2,
1815 },
1816 ..BroadPhase2Config::default()
1817 }
1818 )
1819 .unwrap(),
1820 vec![CollisionPair {
1821 left_index: 0,
1822 right_index: 1
1823 }]
1824 );
1825 }
1826
1827 #[test]
1828 fn spatial_hash_grid_reports_cross_set_pairs_and_stats() {
1829 let left = [
1830 RectU32::new(0, 0, 10, 10).unwrap(),
1831 RectU32::new(40, 40, 4, 4).unwrap(),
1832 ];
1833 let right = [
1834 RectU32::new(4, 4, 10, 10).unwrap(),
1835 RectU32::new(80, 80, 4, 4).unwrap(),
1836 ];
1837 let mut grid = SpatialHashGrid2::new(BroadPhase2Config {
1838 strategy: BroadPhase2Strategy::SpatialHashGrid,
1839 cell_size: SpatialCellSize2::Fixed {
1840 width: 8,
1841 height: 8,
1842 },
1843 ..BroadPhase2Config::default()
1844 })
1845 .unwrap();
1846 let pairs = grid.candidate_pairs_between(&left, &right).unwrap();
1847
1848 assert_eq!(
1849 pairs,
1850 &[CollisionPair {
1851 left_index: 0,
1852 right_index: 0
1853 }]
1854 );
1855 assert_eq!(grid.stats().object_count, 4);
1856 assert_eq!(grid.stats().candidate_pair_count, 1);
1857 }
1858
1859 #[test]
1860 fn broad_phase_rejects_invalid_fixed_cell_sizes() {
1861 assert!(BroadPhase2Config {
1862 cell_size: SpatialCellSize2::Fixed {
1863 width: 0,
1864 height: 8,
1865 },
1866 ..BroadPhase2Config::default()
1867 }
1868 .validate()
1869 .is_err());
1870 }
1871}