Skip to main content

blockworx/document/
coord.rs

1//! Grid-space geometry for the legacy model, and its pixel mapping.
2//!
3//! The live editor's geometry is the document crate's, converted once in
4//! [`crate::edit::lower`]. These types and their conversions survive only
5//! for the dormant half — the on-disk bridge and the presentation layer's
6//! own route geometry — and go with it (the editor-swap playbook, F7).
7
8use egui::{Pos2, Rect, Vec2, pos2, vec2};
9use serde::{Deserialize, Serialize};
10
11use crate::grid::{grid_i32, grid_u32_ceil, px, px_u};
12
13#[derive(
14    Copy, Clone, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
15)]
16pub struct GridPos {
17    pub x: i32,
18    pub y: i32,
19}
20
21#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
22pub struct GridSize {
23    pub w: u32,
24    pub h: u32,
25}
26
27#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
28pub struct GridVec {
29    pub dx: i32,
30    pub dy: i32,
31}
32
33#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
34pub struct GridRect {
35    pub min: GridPos,
36    pub size: GridSize,
37}
38
39impl GridPos {
40    pub const ZERO: GridPos = GridPos { x: 0, y: 0 };
41
42    pub const fn new(x: i32, y: i32) -> Self {
43        Self { x, y }
44    }
45}
46
47impl GridSize {
48    pub const ZERO: GridSize = GridSize { w: 0, h: 0 };
49
50    pub const fn new(w: u32, h: u32) -> Self {
51        Self { w, h }
52    }
53}
54
55impl GridVec {
56    pub const ZERO: GridVec = GridVec { dx: 0, dy: 0 };
57
58    pub const fn new(dx: i32, dy: i32) -> Self {
59        Self { dx, dy }
60    }
61}
62
63impl GridRect {
64    pub const fn new(min: GridPos, size: GridSize) -> Self {
65        Self { min, size }
66    }
67
68    pub fn from_min_size(min: GridPos, size: GridSize) -> Self {
69        Self { min, size }
70    }
71
72    pub fn from_two_pos(a: GridPos, b: GridPos) -> Self {
73        let min_x = a.x.min(b.x);
74        let min_y = a.y.min(b.y);
75        let max_x = a.x.max(b.x);
76        let max_y = a.y.max(b.y);
77        Self {
78            min: GridPos::new(min_x, min_y),
79            size: GridSize::new((max_x - min_x) as u32, (max_y - min_y) as u32),
80        }
81    }
82
83    pub fn max(self) -> GridPos {
84        GridPos::new(
85            self.min.x + self.size.w as i32,
86            self.min.y + self.size.h as i32,
87        )
88    }
89
90    pub fn width(self) -> u32 {
91        self.size.w
92    }
93
94    pub fn height(self) -> u32 {
95        self.size.h
96    }
97
98    pub fn left(self) -> i32 {
99        self.min.x
100    }
101
102    pub fn top(self) -> i32 {
103        self.min.y
104    }
105
106    pub fn right(self) -> i32 {
107        self.min.x + self.size.w as i32
108    }
109
110    pub fn bottom(self) -> i32 {
111        self.min.y + self.size.h as i32
112    }
113
114    pub fn left_top(self) -> GridPos {
115        self.min
116    }
117
118    pub fn right_top(self) -> GridPos {
119        GridPos::new(self.right(), self.top())
120    }
121
122    pub fn left_bottom(self) -> GridPos {
123        GridPos::new(self.left(), self.bottom())
124    }
125
126    pub fn right_bottom(self) -> GridPos {
127        self.max()
128    }
129
130    pub fn contains(self, p: GridPos) -> bool {
131        p.x >= self.left() && p.x <= self.right() && p.y >= self.top() && p.y <= self.bottom()
132    }
133
134    pub fn intersects(self, other: GridRect) -> bool {
135        self.left() < other.right()
136            && other.left() < self.right()
137            && self.top() < other.bottom()
138            && other.top() < self.bottom()
139    }
140
141    pub fn translate(self, delta: GridVec) -> Self {
142        Self {
143            min: GridPos::new(self.min.x + delta.dx, self.min.y + delta.dy),
144            size: self.size,
145        }
146    }
147}
148
149impl std::ops::Add<GridVec> for GridPos {
150    type Output = GridPos;
151    fn add(self, rhs: GridVec) -> GridPos {
152        GridPos::new(self.x + rhs.dx, self.y + rhs.dy)
153    }
154}
155
156impl std::ops::Sub<GridPos> for GridPos {
157    type Output = GridVec;
158    fn sub(self, rhs: GridPos) -> GridVec {
159        GridVec::new(self.x - rhs.x, self.y - rhs.y)
160    }
161}
162
163/// Pixel-space conversions for [`GridPos`].
164pub trait GridPosExt {
165    /// The grid cell nearest to a world-space point.
166    fn from_pos2_snapped(p: Pos2) -> Self;
167    /// The world-space position of this grid cell's corner.
168    fn to_pos2(self) -> Pos2;
169}
170
171impl GridPosExt for GridPos {
172    fn from_pos2_snapped(p: Pos2) -> Self {
173        Self::new(grid_i32(p.x), grid_i32(p.y))
174    }
175
176    fn to_pos2(self) -> Pos2 {
177        pos2(px(self.x), px(self.y))
178    }
179}
180
181/// Pixel-space conversions for [`GridSize`].
182pub trait GridSizeExt {
183    /// Convert a world-space extent to whole grid cells, rounding *up* so the
184    /// resulting size always fully encloses the original extent. Clamped to at
185    /// least one cell per axis.
186    fn from_vec2_ceil(v: Vec2) -> Self;
187}
188
189impl GridSizeExt for GridSize {
190    fn from_vec2_ceil(v: Vec2) -> Self {
191        Self::new(grid_u32_ceil(v.x), grid_u32_ceil(v.y))
192    }
193}
194
195/// Pixel-space conversions for [`GridRect`].
196pub trait GridRectExt {
197    /// The grid rect enclosing a world-space rect, each corner snapped.
198    fn from_rect_snapped(r: Rect) -> Self;
199    /// The world-space rect this grid rect covers.
200    fn to_rect(self) -> Rect;
201}
202
203impl GridRectExt for GridRect {
204    fn from_rect_snapped(r: Rect) -> Self {
205        let min = GridPos::from_pos2_snapped(r.min);
206        let max = GridPos::from_pos2_snapped(r.max);
207        Self::from_two_pos(min, max)
208    }
209
210    fn to_rect(self) -> Rect {
211        Rect::from_min_size(
212            self.min.to_pos2(),
213            vec2(px_u(self.size.w), px_u(self.size.h)),
214        )
215    }
216}
217
218impl From<GridPos> for Pos2 {
219    fn from(p: GridPos) -> Self {
220        p.to_pos2()
221    }
222}
223
224/// The document's grid point in the presentation layer's vocabulary. One
225/// cell, two spellings: the solved geometry is still measured in these
226/// types while the authored corners live in the document's, and the two
227/// meet here rather than through pixels.
228impl From<blockworx_doc::geometry::GridPoint> for GridPos {
229    fn from(p: blockworx_doc::geometry::GridPoint) -> Self {
230        Self::new(p.x, p.y)
231    }
232}
233
234impl From<GridPos> for blockworx_doc::geometry::GridPoint {
235    fn from(p: GridPos) -> Self {
236        Self { x: p.x, y: p.y }
237    }
238}
239
240impl From<blockworx_doc::geometry::GridVec> for GridVec {
241    fn from(v: blockworx_doc::geometry::GridVec) -> Self {
242        Self::new(v.dx, v.dy)
243    }
244}
245
246impl From<GridVec> for blockworx_doc::geometry::GridVec {
247    fn from(v: GridVec) -> Self {
248        Self::new(v.dx, v.dy)
249    }
250}
251
252impl From<GridVec> for Vec2 {
253    fn from(v: GridVec) -> Self {
254        vec2(px(v.dx), px(v.dy))
255    }
256}
257
258impl From<GridRect> for Rect {
259    fn from(r: GridRect) -> Self {
260        r.to_rect()
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    #[test]
269    fn gridpos_pos2_roundtrip() {
270        for &(x, y) in &[(0, 0), (1, 1), (-3, 7), (100, -50)] {
271            let g = GridPos::new(x, y);
272            let p: Pos2 = g.into();
273            let g2 = GridPos::from_pos2_snapped(p);
274            assert_eq!(g, g2);
275        }
276    }
277
278    #[test]
279    fn gridrect_rect_roundtrip() {
280        let r = GridRect::new(GridPos::new(-2, 3), GridSize::new(7, 4));
281        let r2 = GridRect::from_rect_snapped(r.to_rect());
282        assert_eq!(r, r2);
283    }
284
285    #[test]
286    fn gridrect_intersects_matches_rect_semantics() {
287        let a = GridRect::new(GridPos::new(0, 0), GridSize::new(2, 2));
288        let b = GridRect::new(GridPos::new(2, 0), GridSize::new(2, 2));
289        // Touching edges should not intersect (open-on-the-right).
290        assert!(!a.intersects(b));
291        let c = GridRect::new(GridPos::new(1, 0), GridSize::new(2, 2));
292        assert!(a.intersects(c));
293    }
294}