1use blockworx_doc::id::BlockId;
18use blockworx_editor::shape::ShapeId;
19use blockworx_geom::{Pos2, Rect, Vec2, grid::GRID_SIZE, vec2};
20use blockworx_kernel::{
21 Event, Session,
22 driving::{SettledGrid, VIEWPORT, batch_in, blocks, down, moved, up},
23 kernel,
24};
25use blockworx_paint::Move;
26use blockworx_text::Shaper;
27use blockworx_tools::{
28 commands::{CommandId, Heading},
29 tool::Action,
30};
31
32pub const SIDE: usize = 50;
35
36pub struct Rig<'a> {
38 grid: &'a SettledGrid,
39 layout: &'a Shaper,
40 session: Session,
41 millis: u64,
42 turn: usize,
43 home: Pos2,
45 viewport: Rect,
47}
48
49impl<'a> Rig<'a> {
50 #[must_use]
52 pub fn opened(grid: &'a SettledGrid, layout: &'a Shaper) -> Self {
53 let mut rig = Self {
54 grid,
55 layout,
56 session: grid.session(),
57 millis: 0,
58 turn: 0,
59 home: Pos2::ZERO,
60 viewport: VIEWPORT,
61 };
62 rig.call(Vec::new());
63 rig
64 }
65
66 pub fn call(&mut self, events: Vec<Event>) {
68 self.millis += 16;
69 kernel(
70 &mut self.session,
71 batch_in(self.viewport, self.millis, events),
72 self.layout,
73 );
74 }
75
76 fn alternate<T: Copy>(&mut self, pair: [T; 2]) -> T {
79 let picked = pair[self.turn % 2];
80 self.turn += 1;
81 picked
82 }
83
84 fn subject(&self) -> BlockId {
87 blocks(&self.session)[1]
88 }
89
90 #[expect(
91 clippy::expect_used,
92 reason = "a scenario whose block is missing has nothing to measure"
93 )]
94 fn centre(&mut self, block: BlockId) -> Pos2 {
95 self.session
96 .drawing()
97 .shape(ShapeId::Rect(block))
98 .expect("the block is on the opening level")
99 .gui_rect()
100 .center()
101 }
102
103 fn select(&mut self, block: BlockId) {
104 self.call(vec![Event::Action(Action::NavSelect {
105 block,
106 extend: false,
107 })]);
108 }
109
110 fn rev(&self) -> u64 {
111 self.session.doc.repo().rev().get()
112 }
113}
114
115const TWO_CELLS: Vec2 = vec2(2.0 * GRID_SIZE, 0.0);
117
118pub struct Scenario {
121 pub name: &'static str,
122 pub prepare: fn(&mut Rig<'_>),
123 pub approach: fn(&mut Rig<'_>),
124 pub timed: fn(&mut Rig<'_>),
125}
126
127fn nothing(_: &mut Rig<'_>) {}
128
129const ZOOMED: [f32; 2] = [4_000.0, 3_800.0];
132
133fn far_out(width: f32) -> Rect {
135 Rect::from_center_size(Pos2::new(6_000.0, 6_000.0), vec2(width, width * 0.75))
136}
137
138fn frame(rig: &mut Rig<'_>) {
139 rig.call(Vec::new());
140}
141
142const BROWSER: Rect = Rect {
145 min: Pos2::ZERO,
146 max: Pos2 {
147 x: 1_600.0,
148 y: 1_000.0,
149 },
150};
151
152fn framed_in_browser(rig: &mut Rig<'_>, zoom: Option<f32>) {
157 rig.viewport = BROWSER;
158 let zoom_now = |rig: &Rig<'_>| f32::from(rig.session.vantage().zoom);
159 match zoom {
160 None => rig.call(vec![Event::Action(Action::ResetView)]),
161 Some(zoom) => {
162 let mut size = vec2(BROWSER.width() / zoom, BROWSER.height() / zoom);
163 for _ in 0..3 {
164 let rect = Rect::from_center_size(Pos2::new(6_000.0, 6_000.0), size);
165 rig.call(vec![Event::Action(Action::FrameRect(rect))]);
166 size *= zoom_now(rig) / zoom;
167 }
168 }
169 }
170 rig.call(Vec::new());
171 let landed = zoom_now(rig);
172 let wanted = zoom.unwrap_or(0.1);
173 assert!(
174 (landed - wanted).abs() < 0.01,
175 "framed at {landed}, not {wanted}"
176 );
177}
178
179fn pan(rig: &mut Rig<'_>) {
182 let by = rig.alternate([vec2(12.0, 0.0), vec2(-12.0, 0.0)]);
183 let before = rig.session.vantage();
184 rig.call(vec![Event::Move(Move::Pan(by))]);
185 debug_assert_ne!(rig.session.vantage(), before, "the pan moved nothing");
186}
187
188pub const SCENARIOS: &[Scenario] = &[
190 Scenario {
192 name: "open",
193 prepare: nothing,
194 approach: |rig| {
195 rig.session = rig.grid.session();
196 rig.millis = 0;
197 },
198 timed: frame,
199 },
200 Scenario {
202 name: "frame",
203 prepare: nothing,
204 approach: nothing,
205 timed: frame,
206 },
207 Scenario {
209 name: "frame_at_fit",
210 prepare: |rig| {
211 rig.call(vec![Event::Action(Action::ResetView)]);
212 rig.call(Vec::new());
213 },
214 approach: nothing,
215 timed: frame,
216 },
217 Scenario {
221 name: "hover_at_fit",
222 prepare: |rig| {
223 rig.call(vec![Event::Action(Action::ResetView)]);
224 rig.call(Vec::new());
225 },
226 approach: nothing,
227 timed: |rig| {
228 let at = rig.alternate([Pos2::new(400.0, 300.0), Pos2::new(412.0, 306.0)]);
229 rig.call(vec![moved(at)]);
230 },
231 },
232 Scenario {
236 name: "zoom_far_out",
237 prepare: |rig| {
238 let zoom_at = |rig: &mut Rig<'_>, width| {
239 rig.call(vec![Event::Action(Action::FrameRect(far_out(width)))]);
240 f32::from(rig.session.vantage().zoom)
241 };
242 let (near, far) = (zoom_at(rig, ZOOMED[0]), zoom_at(rig, ZOOMED[1]));
243 assert!(
244 (near - far).abs() > 0.001 && near.max(far) < 0.3,
245 "the two framings are two far-out zooms: {near} and {far}"
246 );
247 },
248 approach: nothing,
249 timed: |rig| {
250 let width = rig.alternate(ZOOMED);
251 rig.call(vec![Event::Action(Action::FrameRect(far_out(width)))]);
252 },
253 },
254 Scenario {
257 name: "pan_at_fit",
258 prepare: |rig| framed_in_browser(rig, None),
259 approach: nothing,
260 timed: pan,
261 },
262 Scenario {
264 name: "pan_at_24",
265 prepare: |rig| framed_in_browser(rig, Some(0.24)),
266 approach: nothing,
267 timed: pan,
268 },
269 Scenario {
271 name: "select",
272 prepare: nothing,
273 approach: nothing,
274 timed: |rig| {
275 let pair = [blocks(&rig.session)[1], blocks(&rig.session)[2]];
276 let block = rig.alternate(pair);
277 rig.select(block);
278 rig.call(Vec::new());
279 },
280 },
281 Scenario {
283 name: "nudge",
284 prepare: |rig| {
285 let block = rig.subject();
286 rig.select(block);
287 let before = rig.rev();
288 rig.call(vec![Event::Command(CommandId::Nudge(Heading::Right))]);
289 assert_ne!(rig.rev(), before, "the nudge wrote nothing");
290 },
291 approach: nothing,
292 timed: |rig| {
293 let heading = rig.alternate([Heading::Left, Heading::Right]);
294 rig.call(vec![Event::Command(CommandId::Nudge(heading))]);
295 rig.call(Vec::new());
296 },
297 },
298 Scenario {
301 name: "drag_frame",
302 prepare: |rig| {
303 let block = rig.subject();
304 rig.home = rig.centre(block);
305 let home = rig.home;
306 rig.call(vec![down(home)]);
307 },
308 approach: nothing,
309 timed: |rig| {
310 let cell = vec2(GRID_SIZE, 0.0);
311 let offset = rig.alternate([cell, 2.0 * cell]);
312 let at = rig.home + offset;
313 rig.call(vec![moved(at)]);
314 },
315 },
316 Scenario {
319 name: "drop",
320 prepare: |rig| {
321 let block = rig.subject();
322 rig.home = rig.centre(block);
323 let before = rig.rev();
324 drop_approach(rig);
325 drop_release(rig);
326 assert_ne!(rig.rev(), before, "the drop wrote nothing");
327 assert_eq!(
328 rig.centre(block),
329 rig.home + TWO_CELLS,
330 "the drop moved the block two cells"
331 );
332 },
333 approach: drop_approach,
334 timed: drop_release,
335 },
336];
337
338fn drop_ends(rig: &Rig<'_>) -> (Pos2, Pos2) {
340 let (home, out) = (rig.home, rig.home + TWO_CELLS);
341 if rig.turn.is_multiple_of(2) {
342 (home, out)
343 } else {
344 (out, home)
345 }
346}
347
348fn drop_approach(rig: &mut Rig<'_>) {
352 let (from, to) = drop_ends(rig);
353 let started = from + (to - from).normalized() * 7.0;
354 rig.call(vec![down(from)]);
355 rig.call(vec![moved(started)]);
356 rig.call(vec![moved(to)]);
357}
358
359fn drop_release(rig: &mut Rig<'_>) {
360 let (_, to) = drop_ends(rig);
361 rig.call(vec![up(to)]);
362 rig.call(Vec::new());
363 rig.turn += 1;
364}