Skip to main content

blockworx_geom/grid/
bridge.rs

1//! The bridge between world pixels and the document's own integer geometry,
2//! encoded once (the conversion table in `docs/op-emitter-playbook.md`). Each
3//! op emitter lands the rows it consumes through these, and the geometry layer
4//! reads them back through the same functions, so a rect drawn and a rect
5//! committed cannot disagree about where a cell is.
6
7use blockworx_doc::geometry::{
8    FracVal, GridPoint, GridRect, GridSize, GridVec, ScreenPoint, ScreenRect, ScreenSize,
9};
10
11use crate::{
12    Pos2, Rect, Vec2,
13    grid::{grid_i32, grid_u32_ceil, px, px_u},
14    pos2, vec2,
15};
16
17/// A world-space gesture point onto the grid cell nearest it.
18pub fn grid_point(pos: Pos2) -> GridPoint {
19    GridPoint {
20        x: grid_i32(pos.x),
21        y: grid_i32(pos.y),
22    }
23}
24
25/// The world-space corner of a grid cell — the inverse of [`grid_point`].
26pub fn px_point(point: GridPoint) -> Pos2 {
27    pos2(px(point.x), px(point.y))
28}
29
30/// A world-space extent as the whole cells that fully enclose it — the
31/// measured size a text box records.
32pub fn grid_size_ceil(extent: Vec2) -> GridSize {
33    GridSize {
34        w: grid_u32_ceil(extent.x),
35        h: grid_u32_ceil(extent.y),
36    }
37}
38
39/// A drag delta as the whole-cell offset the move commits at.
40pub fn grid_vec(delta: Vec2) -> GridVec {
41    GridVec::new(grid_i32(delta.x), grid_i32(delta.y))
42}
43
44/// The world-space vector a snapped [`grid_vec`] covers — what artwork
45/// riding a grid move (a block's icon) translates by.
46pub fn px_vec(delta: GridVec) -> Vec2 {
47    vec2(px(delta.dx), px(delta.dy))
48}
49
50/// The world-space rect a grid rect covers.
51pub fn px_rect(rect: GridRect) -> Rect {
52    Rect::from_min_size(
53        pos2(px(rect.left()), px(rect.top())),
54        vec2(px_u(rect.size.w), px_u(rect.size.h)),
55    )
56}
57
58/// Artwork geometry — free-floating images and block icons — is the one
59/// float rect the document stores: unsnapped, so a placed image keeps the
60/// aspect the drag gave it.
61pub fn screen_rect(rect: Rect) -> ScreenRect {
62    ScreenRect {
63        top_left: ScreenPoint {
64            x: FracVal::from(rect.min.x),
65            y: FracVal::from(rect.min.y),
66        },
67        size: ScreenSize {
68            w: FracVal::from(rect.width()),
69            h: FracVal::from(rect.height()),
70        },
71    }
72}
73
74/// The inverse of [`screen_rect`].
75pub fn artwork_rect(rect: ScreenRect) -> Rect {
76    Rect::from_min_size(
77        pos2(rect.top_left.x.into(), rect.top_left.y.into()),
78        vec2(rect.size.w.into(), rect.size.h.into()),
79    )
80}
81
82/// The grid rect a two-corner gesture drew — either corner may be either
83/// end of the drag.
84pub fn grid_rect(start: Pos2, end: Pos2) -> GridRect {
85    GridRect::from_two_pos(grid_point(start), grid_point(end))
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use crate::grid::GRID_SIZE;
92
93    #[test]
94    fn a_point_lands_on_the_nearest_cell() {
95        let cell = |x: f32, y: f32| grid_point(pos2(x * GRID_SIZE, y * GRID_SIZE));
96        assert_eq!(cell(2.0, 3.0), GridPoint { x: 2, y: 3 });
97        assert_eq!(cell(2.4, 3.4), GridPoint { x: 2, y: 3 });
98        assert_eq!(cell(2.6, 3.6), GridPoint { x: 3, y: 4 });
99        assert_eq!(cell(-2.4, -3.6), GridPoint { x: -2, y: -4 });
100    }
101
102    #[test]
103    fn a_corner_pair_normalizes_whichever_way_it_was_dragged() {
104        let (start, end) = (pos2(5.0 * GRID_SIZE, 1.0 * GRID_SIZE), pos2(0.0, 0.0));
105        assert!(
106            start.x > end.x && start.y > end.y,
107            "precondition: the drag runs up and to the left, so normalization matters"
108        );
109        let expected = GridRect {
110            top_left: GridPoint { x: 0, y: 0 },
111            size: GridSize { w: 5, h: 1 },
112        };
113        assert_eq!(grid_rect(start, end), expected);
114        assert_eq!(grid_rect(end, start), expected);
115    }
116}