blockworx_paint/
ground.rs1use blockworx_geom::{Pos2, Rangef, Rect, grid::GRID_SIZE, pos2};
9
10use crate::{Color, Vantage, Zoom};
11
12const MIN_MINOR_SPACING: f32 = 5.0;
15
16const MAJOR_STEP: i32 = 4;
18
19#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub struct Ground {
23 pub background: Color,
24 pub grid: Color,
25}
26
27#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29pub enum GridLine {
30 Major,
31 Minor,
32}
33
34impl GridLine {
35 fn at(index: i32) -> Self {
36 if index % MAJOR_STEP == 0 {
37 Self::Major
38 } else {
39 Self::Minor
40 }
41 }
42
43 #[must_use]
45 pub fn width(self) -> f32 {
46 match self {
47 Self::Major => 1.0,
48 Self::Minor => 0.5,
49 }
50 }
51}
52
53pub fn verticals(vantage: Vantage, viewport: Rect) -> impl Iterator<Item = (f32, GridLine)> {
56 let world = vantage.visible(viewport);
57 along(vantage.zoom, world.x_range())
58 .map(move |(x, line)| (screen(vantage, viewport.min, pos2(x, 0.0)).x, line))
59}
60
61pub fn horizontals(vantage: Vantage, viewport: Rect) -> impl Iterator<Item = (f32, GridLine)> {
64 let world = vantage.visible(viewport);
65 along(vantage.zoom, world.y_range())
66 .map(move |(y, line)| (screen(vantage, viewport.min, pos2(0.0, y)).y, line))
67}
68
69fn screen(vantage: Vantage, origin: Pos2, world: Pos2) -> Pos2 {
70 vantage.world_to_screen(origin, world)
71}
72
73fn along(zoom: Zoom, span: Rangef) -> impl Iterator<Item = (f32, GridLine)> {
76 let draw_minor = GRID_SIZE * zoom.get() >= MIN_MINOR_SPACING;
77 ((span.min / GRID_SIZE).floor() as i32..=(span.max / GRID_SIZE).ceil() as i32)
78 .map(|i| (i as f32 * GRID_SIZE, GridLine::at(i)))
79 .filter(move |&(_, line)| line == GridLine::Major || draw_minor)
80}
81
82#[cfg(test)]
83mod tests {
84 use super::*;
85 use blockworx_geom::{Vec2, vec2};
86
87 fn viewport() -> Rect {
88 Rect::from_min_size(pos2(0.0, 0.0), Vec2::new(800.0, 600.0))
89 }
90
91 #[test]
92 fn a_resting_camera_rules_the_viewport_every_grid_step() {
93 let lines: Vec<_> = verticals(Vantage::resting(), viewport()).collect();
94 let step = GRID_SIZE;
95 assert!(
96 lines.len() > 2,
97 "precondition: the viewport spans several steps"
98 );
99 for pair in lines.windows(2) {
100 assert!(
101 (pair[1].0 - pair[0].0 - step).abs() < 1e-3,
102 "lines stand one grid step apart: {pair:?}"
103 );
104 }
105 assert!(
106 lines.iter().any(|&(x, _)| (x - 0.0).abs() < 1e-3),
107 "the origin is ruled: {lines:?}"
108 );
109 }
110
111 #[test]
112 fn every_fourth_line_is_a_major_one() {
113 let lines: Vec<_> = horizontals(Vantage::resting(), viewport()).collect();
114 let major_at = |y: f32| (y / GRID_SIZE).round() as i32 % MAJOR_STEP == 0;
115 assert!(
116 lines.iter().any(|&(_, line)| line == GridLine::Minor),
117 "precondition: the minor lines are drawn at unity zoom"
118 );
119 for &(y, line) in &lines {
120 assert_eq!(
121 line == GridLine::Major,
122 major_at(y),
123 "the weight at {y} does not follow the step"
124 );
125 }
126 assert!(GridLine::Major.width() > GridLine::Minor.width());
127 }
128
129 #[test]
130 fn the_minor_lines_drop_out_once_they_crowd() {
131 let zoomed_out = Vantage {
132 zoom: Zoom::new(MIN_MINOR_SPACING / GRID_SIZE * 0.9),
133 translation: Vec2::ZERO,
134 };
135 assert!(
136 GRID_SIZE * zoomed_out.zoom.get() < MIN_MINOR_SPACING,
137 "precondition: the steps are closer than the floor"
138 );
139 assert!(
140 verticals(zoomed_out, viewport()).all(|(_, line)| line == GridLine::Major),
141 "a crowded grid keeps only its major lines"
142 );
143 assert!(
144 verticals(zoomed_out, viewport()).count() > 1,
145 "and still rules the viewport"
146 );
147 }
148
149 #[test]
152 fn the_lines_follow_the_camera() {
153 let by = 7.0;
154 let panned = Vantage {
155 zoom: Zoom::unity(),
156 translation: vec2(by, 0.0),
157 };
158 let origin_line = |vantage| {
159 verticals(vantage, viewport())
160 .map(|(x, _)| x)
161 .find(|x| (x - by).abs() < 1e-3)
162 };
163 assert!(
164 origin_line(Vantage::resting()).is_none(),
165 "precondition: the resting camera rules nothing at {by}"
166 );
167 assert!(
168 origin_line(panned).is_some(),
169 "a pan of {by} px puts the world origin's line there"
170 );
171
172 let zoomed = Vantage {
173 zoom: Zoom::new(2.0),
174 translation: Vec2::ZERO,
175 };
176 let spacing = |vantage| {
177 let lines: Vec<f32> = verticals(vantage, viewport()).map(|(x, _)| x).collect();
178 lines[1] - lines[0]
179 };
180 assert!(
181 (spacing(zoomed) - 2.0 * spacing(Vantage::resting())).abs() < 1e-3,
182 "doubling the zoom doubles the spacing"
183 );
184 }
185}