use crate::{Angle, Geonum};
pub struct GeoCollection {
pub objects: Vec<Geonum>,
}
impl GeoCollection {
pub fn new() -> Self {
Self {
objects: Vec::new(),
}
}
pub fn len(&self) -> usize {
self.objects.len()
}
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &Geonum> {
self.objects.iter()
}
}
impl Default for GeoCollection {
fn default() -> Self {
Self::new()
}
}
impl From<Vec<Geonum>> for GeoCollection {
fn from(objects: Vec<Geonum>) -> Self {
Self { objects }
}
}
impl FromIterator<Geonum> for GeoCollection {
fn from_iter<T: IntoIterator<Item = Geonum>>(iter: T) -> Self {
Self {
objects: iter.into_iter().collect(),
}
}
}
impl std::ops::Index<usize> for GeoCollection {
type Output = Geonum;
fn index(&self, index: usize) -> &Self::Output {
&self.objects[index]
}
}
impl IntoIterator for GeoCollection {
type Item = Geonum;
type IntoIter = std::vec::IntoIter<Geonum>;
fn into_iter(self) -> Self::IntoIter {
self.objects.into_iter()
}
}
impl<'a> IntoIterator for &'a GeoCollection {
type Item = &'a Geonum;
type IntoIter = std::slice::Iter<'a, Geonum>;
fn into_iter(self) -> Self::IntoIter {
self.objects.iter()
}
}
impl AsRef<Vec<Geonum>> for GeoCollection {
fn as_ref(&self) -> &Vec<Geonum> {
&self.objects
}
}
impl AsRef<[Geonum]> for GeoCollection {
fn as_ref(&self) -> &[Geonum] {
&self.objects
}
}
impl GeoCollection {
pub fn truncate(&self, threshold: f64) -> Self {
Self::from(
self.objects
.iter()
.filter(|g| g.mag > threshold)
.cloned()
.collect::<Vec<_>>(),
)
}
pub fn select_cone(&self, direction: &Geonum, half_angle: f64) -> Self {
Self::from(
self.objects
.iter()
.filter(|g| {
let magnitude = g.mag * direction.mag;
if magnitude == 0.0 {
return false;
}
let dot = g.dot(direction);
let signed_cos = dot.mag / magnitude * dot.angle.project(Angle::new(0.0, 1.0));
let angle_between = signed_cos.clamp(-1.0, 1.0).acos();
angle_between <= half_angle
})
.cloned()
.collect::<Vec<_>>(),
)
}
pub fn total_magnitude(&self) -> f64 {
self.objects.iter().map(|g| g.mag).sum()
}
pub fn dominant(&self) -> Option<&Geonum> {
self.objects
.iter()
.max_by(|a, b| a.mag.partial_cmp(&b.mag).unwrap())
}
pub fn scale_all(&self, factor: f64) -> Self {
Self::from(
self.objects
.iter()
.map(|g| g.scale(factor))
.collect::<Vec<_>>(),
)
}
pub fn rotate_all(&self, rotation: crate::Angle) -> Self {
Self::from(
self.objects
.iter()
.map(|g| g.rotate(rotation))
.collect::<Vec<_>>(),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Angle, Geonum};
use std::f64::consts::PI;
#[test]
fn it_creates_empty_collection() {
let collection = GeoCollection::new();
assert!(collection.is_empty());
assert_eq!(collection.len(), 0);
}
#[test]
fn it_creates_from_vec() {
let objects = vec![
Geonum::scalar(1.0),
Geonum::scalar(2.0),
Geonum::scalar(3.0),
];
let collection = GeoCollection::from(objects);
assert_eq!(collection.len(), 3);
}
#[test]
fn it_truncates_small_magnitudes() {
let collection = GeoCollection::from(vec![
Geonum::new(0.9, 0.0, 1.0), Geonum::new(0.05, 0.0, 1.0), Geonum::new(0.3, 1.0, 2.0), Geonum::new(0.7, 1.0, 1.0), ]);
let filtered = collection.truncate(0.5);
assert_eq!(filtered.len(), 2);
assert!(filtered.objects[0].mag > 0.5);
assert!(filtered.objects[1].mag > 0.5);
}
#[test]
fn it_selects_objects_within_cone() {
let forward = Geonum::new(1.0, 0.0, 1.0); let collection = GeoCollection::from(vec![
Geonum::new(1.0, 0.0, 1.0), Geonum::new(1.0, 1.0, 8.0), Geonum::new(1.0, 1.0, 2.0), Geonum::new(1.0, 1.0, 1.0), ]);
let cone = collection.select_cone(&forward, PI / 4.0); assert_eq!(cone.len(), 2);
}
#[test]
fn it_computes_total_magnitude() {
let collection = GeoCollection::from(vec![
Geonum::new(2.0, 0.0, 1.0),
Geonum::new(3.0, 1.0, 2.0),
Geonum::new(5.0, 1.0, 1.0),
]);
let total = collection.total_magnitude();
assert_eq!(total, 10.0); }
#[test]
fn it_finds_dominant_object() {
let collection = GeoCollection::from(vec![
Geonum::new(2.0, 0.0, 1.0),
Geonum::new(7.0, 1.0, 2.0), Geonum::new(3.0, 1.0, 1.0),
]);
let dominant = collection.dominant().unwrap();
assert_eq!(dominant.mag, 7.0);
}
#[test]
fn it_returns_none_for_empty_dominant() {
let collection = GeoCollection::new();
assert!(collection.dominant().is_none());
}
#[test]
fn it_scales_all_objects() {
let collection = GeoCollection::from(vec![
Geonum::new(1.0, 0.0, 1.0),
Geonum::new(2.0, 1.0, 2.0),
Geonum::new(3.0, 1.0, 1.0),
]);
let scaled = collection.scale_all(2.0);
assert_eq!(scaled.objects[0].mag, 2.0);
assert_eq!(scaled.objects[1].mag, 4.0);
assert_eq!(scaled.objects[2].mag, 6.0);
}
#[test]
fn it_rotates_all_objects() {
let collection = GeoCollection::from(vec![
Geonum::new(1.0, 0.0, 1.0), Geonum::new(1.0, 1.0, 4.0), ]);
let rotation = Angle::new(1.0, 2.0); let rotated = collection.rotate_all(rotation);
assert_eq!(rotated.objects[0].angle, Angle::new(1.0, 2.0));
assert_eq!(rotated.objects[1].angle, Angle::new(3.0, 4.0)); }
#[test]
fn it_normalizes_collection() {
let collection =
GeoCollection::from(vec![Geonum::new(3.0, 0.0, 1.0), Geonum::new(4.0, 1.0, 2.0)]);
let total = collection.total_magnitude(); let normalized = collection.scale_all(1.0 / total);
assert!((normalized.total_magnitude() - 1.0).abs() < 1e-10);
}
#[test]
fn it_handles_cone_selection_with_wrapped_angles() {
let forward = Geonum::new(1.0, 0.0, 1.0); let collection = GeoCollection::from(vec![
Geonum::new(1.0, 0.0, 1.0), Geonum::new_with_blade(1.0, 8, 0.0, 1.0), Geonum::new(1.0, 1.0, 1.0), ]);
let cone = collection.select_cone(&forward, PI / 4.0);
assert_eq!(cone.len(), 2); }
#[test]
fn it_iterates_over_collection() {
let collection = GeoCollection::from(vec![
Geonum::scalar(1.0),
Geonum::scalar(2.0),
Geonum::scalar(3.0),
]);
let sum: f64 = collection.iter().map(|g| g.mag).sum();
assert_eq!(sum, 6.0);
}
#[test]
fn it_indexes_into_collection() {
let collection = GeoCollection::from(vec![Geonum::scalar(10.0), Geonum::scalar(20.0)]);
assert_eq!(collection[0].mag, 10.0);
assert_eq!(collection[1].mag, 20.0);
}
#[test]
fn it_converts_to_vec_reference() {
let collection = GeoCollection::from(vec![Geonum::scalar(1.0)]);
let vec_ref: &Vec<Geonum> = collection.as_ref();
assert_eq!(vec_ref.len(), 1);
}
#[test]
fn it_converts_to_slice() {
let collection = GeoCollection::from(vec![Geonum::scalar(1.0), Geonum::scalar(2.0)]);
let slice: &[Geonum] = collection.as_ref();
assert_eq!(slice.len(), 2);
}
#[test]
fn it_collects_from_iterator() {
let collection: GeoCollection = (0..5).map(|i| Geonum::scalar(i as f64)).collect();
assert_eq!(collection.len(), 5);
assert_eq!(collection[2].mag, 2.0);
}
#[test]
fn it_preserves_angles_during_scaling() {
let angle = Angle::new(1.0, 3.0); let collection = GeoCollection::from(vec![Geonum::new_with_angle(2.0, angle)]);
let scaled = collection.scale_all(3.0);
assert_eq!(scaled.objects[0].angle, angle); assert_eq!(scaled.objects[0].mag, 6.0); }
#[test]
fn it_handles_empty_collection_operations() {
let empty = GeoCollection::new();
assert_eq!(empty.total_magnitude(), 0.0);
assert_eq!(empty.truncate(1.0).len(), 0);
assert_eq!(empty.scale_all(2.0).len(), 0);
assert_eq!(empty.rotate_all(Angle::new(1.0, 2.0)).len(), 0);
}
}