//! Geometry tests: the camera transforms, hit testing and polyline simplification. use notescraft_annotate::{Point, Size, Transform}; /// The deterministic LCG from `crates/sync/tests/merge_props.rs`. struct Rng(u64); impl Rng { fn next(&mut self) -> u64 { self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407); self.0 >> 33 } fn below(&mut self, n: u64) -> u64 { self.next() % n.max(1) } /// A pseudo-random unit in 0.0..1.0. fn unit(&mut self) -> f32 { let sample = u16::try_from(self.below(4096)).unwrap_or(0); f32::from(sample) / 4096.0 } } /// A point pair within an epsilon of each other. fn close(left: Point, right: Point, epsilon: f32) -> bool { (left.x - right.x).abs() <= epsilon && (left.y - right.y).abs() <= epsilon } /// A camera that has gone through a few random operations. fn random_camera(rng: &mut Rng) -> Transform { let mut camera = Transform::new(1000, 800); let viewport = Size { width: 1440, height: 900 }; let steps = rng.below(8); for _ in 0..steps { match rng.below(4) { 0 => camera.zoom_at(Point::new(rng.unit() * 1440.0, rng.unit() * 900.0), 0.1 + rng.unit() * 4.0), 1 => camera.rotate(i8::try_from(rng.below(5)).unwrap_or(0) - 2), 2 => camera.fit(viewport), _ => camera.actual_size(viewport), } } camera } #[test] fn transform_inverse_is_identity() { let mut rng = Rng(0x5EED_2026); for case in 0..500_u64 { let camera = random_camera(&mut rng); for sample in 0..10_u32 { let point = Point::new(f32::from(u16::try_from(sample).unwrap_or(0)) * 100.0, f32::from(u16::try_from(sample % 8).unwrap_or(0)) * 100.0); let screen = camera.to_screen(point); let back = camera.to_image(screen); assert!(close(back, point, 0.5), "case {case}: {point:?} -> {screen:?} -> {back:?}"); } } } #[test] fn zoom_keeps_the_point_under_the_cursor() { let mut rng = Rng(0xCAFE_0001); for case in 0..200_u64 { let mut camera = random_camera(&mut rng); let cursor = Point::new(rng.unit() * 1440.0, rng.unit() * 900.0); let factor = 0.05 + rng.unit() * 10.0; camera.zoom_at(cursor, factor); // The image pixel seen at the cursor before the zoom is seen there after. let seen = camera.to_image(cursor); let at = camera.to_screen(seen); assert!(close(at, cursor, 0.5), "case {case}: cursor {cursor:?} moved to {at:?}"); } } #[test] fn zoom_is_clamped() { let mut camera = Transform::new(1000, 800); camera.zoom_at(Point::new(10.0, 10.0), 10_000.0); assert_eq!(camera.zoom(), notescraft_annotate::MAX_ZOOM); camera.zoom_at(Point::new(10.0, 10.0), 1.0 / 10_000.0); assert_eq!(camera.zoom(), notescraft_annotate::MIN_ZOOM); // Hostile factors leave the camera unchanged. let before = camera; camera.zoom_at(Point::new(10.0, 10.0), 0.0); camera.zoom_at(Point::new(10.0, 10.0), -2.0); camera.zoom_at(Point::new(10.0, 10.0), f32::NAN); camera.zoom_at(Point::new(f32::INFINITY, 10.0), 2.0); assert_eq!(camera, before); } #[test] fn fit_and_actual_size() { let mut camera = Transform::new(800, 600); camera.fit(Size { width: 1440, height: 900 }); // fit zoom = min(1440/800, 900/600), image centre under the viewport centre. assert_eq!(camera.zoom(), (1440.0f32 / 800.0).min(900.0 / 600.0)); let image_centre = Point::new(400.0, 300.0); assert!(close(camera.to_screen(image_centre), Point::new(720.0, 450.0), 0.01)); camera.actual_size(Size { width: 1440, height: 900 }); assert_eq!(camera.zoom(), 1.0); assert!(close(camera.to_screen(image_centre), Point::new(720.0, 450.0), 0.01)); // Degenerate viewports keep the camera sane (zoom 1 fallback, no panic). camera.fit(Size { width: 0, height: 0 }); assert_eq!(camera.zoom(), 1.0); assert!(camera.to_image(Point::new(3.0, 4.0)).x.is_finite()); } #[test] fn rotation_cycles_after_four_turns() { let mut camera = Transform::new(200, 100); let first = camera.to_screen(Point::new(30.0, 40.0)); for _ in 0..4 { camera.rotate(1); } assert_eq!(camera.turns(), 0); assert!(close(camera.to_screen(Point::new(30.0, 40.0)), first, 0.01)); // rotate(-3) must land on the same mapping as rotate(1). let mut quarter = Transform::new(200, 100); quarter.rotate(1); let mut back = Transform::new(200, 100); back.rotate(-3); assert_eq!(quarter.turns(), back.turns()); assert_eq!(quarter.to_image(Point::new(50.0, 60.0)), back.to_image(Point::new(50.0, 60.0))); }