Skip to main content

blockworx_editor/edit/
lower.rs

1//! The editor's own rules about grid geometry: the block-height ladder, a
2//! block's pin capacity, the accent↔role mapping the pickers speak, and the
3//! label and asset reads the shapes draw from. The coordinate conversions these
4//! build on are [`crate::grid`]'s, so a rect drawn and a rect committed cannot
5//! disagree about where a cell is.
6
7use blockworx_doc::{
8    block_model::{Asset, Label},
9    geometry::{GridRect, GridSize},
10    values::Role,
11};
12use blockworx_geom::Pos2;
13
14use crate::grid::{GRID_SIZE, grid_rect, max_pin_slot, snap_block_height_cells};
15use crate::shape::ShapeLabel;
16
17/// The highest slot a block `height` cells tall offers; the ladder itself
18/// lives in [`max_pin_slot`], which works in px.
19pub fn slot_capacity(height: u32) -> u32 {
20    max_pin_slot(height as f32 * GRID_SIZE)
21}
22
23/// [`grid_rect`] on a block's height ladder: heights are one of 4, 7, 10, …
24/// cells (a pin slot's clearance plus one pitch per slot), and the top edge
25/// is kept, so the block settles onto the nearest valid height rather than
26/// the one the drag stopped at.
27pub fn block_rect(start: Pos2, end: Pos2) -> GridRect {
28    let rect = grid_rect(start, end);
29    GridRect {
30        size: GridSize {
31            h: snap_block_height_cells(rect.size.h),
32            ..rect.size
33        },
34        ..rect
35    }
36}
37
38/// The picker's accent pick — `None` = plain, `Some(0..=7)` = palette index —
39/// onto the 9-variant [`Role`], where `Accent0` is the meaningful plain
40/// default. Indices past the palette cannot come from the picker; they
41/// saturate to the last accent rather than silently reading as plain.
42pub fn role_from_accent(accent: Option<u8>) -> Role {
43    match accent {
44        None => Role::Accent0,
45        Some(0) => Role::Accent1,
46        Some(1) => Role::Accent2,
47        Some(2) => Role::Accent3,
48        Some(3) => Role::Accent4,
49        Some(4) => Role::Accent5,
50        Some(5) => Role::Accent6,
51        Some(6) => Role::Accent7,
52        Some(_) => Role::Accent8,
53    }
54}
55
56/// A [`Label`] namespace read out for drawing. The one register the
57/// geometry layer cannot use as stored is `offset`: the document keeps it
58/// fixed-point (so its bytes are deterministic), while every anchor formula
59/// works in world pixels — the same pixels the drag wrote, since
60/// `FracVal::from(f32)` is what put it there.
61pub fn shape_label(label: &Label) -> ShapeLabel<'_> {
62    ShapeLabel {
63        name: &label.name,
64        hidden: label.hidden,
65        side: label.side,
66        offset: (label.offset).into(),
67    }
68}
69
70/// Whether `asset` is small enough to ride the commit log; logs `name` and
71/// both sizes when it is not. The fold refuses one too, but only after a
72/// gesture has authored it — refusing here is what lets the pick that
73/// produced it say so.
74pub fn asset_within_limit(asset: &Asset, name: &str) -> bool {
75    if asset.within_limit() {
76        return true;
77    }
78    tracing::error!(
79        "{name} is {} bytes; artwork is limited to {} bytes because it rides the commit log",
80        asset.bytes().len(),
81        blockworx_doc::block_model::ASSET_LIMIT,
82    );
83    false
84}
85
86/// The inverse of [`role_from_accent`], for projections back into the
87/// picker's vocabulary.
88pub fn accent_from_role(role: Role) -> Option<u8> {
89    match role {
90        Role::Accent0 => None,
91        Role::Accent1 => Some(0),
92        Role::Accent2 => Some(1),
93        Role::Accent3 => Some(2),
94        Role::Accent4 => Some(3),
95        Role::Accent5 => Some(4),
96        Role::Accent6 => Some(5),
97        Role::Accent7 => Some(6),
98        Role::Accent8 => Some(7),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use blockworx_doc::geometry::GridPoint;
105    use blockworx_geom::pos2;
106
107    use super::*;
108    use crate::grid::{GRID_SIZE, max_pin_slot};
109
110    #[test]
111    fn accent_and_role_are_an_involution_over_the_picker_range() {
112        for accent in std::iter::once(None).chain((0..=7).map(Some)) {
113            assert_eq!(accent_from_role(role_from_accent(accent)), accent);
114        }
115    }
116
117    #[test]
118    fn a_block_rect_settles_on_the_height_ladder_keeping_its_top_edge() {
119        let cell = |x: f32, y: f32| pos2(x * GRID_SIZE, y * GRID_SIZE);
120        let rect = block_rect(cell(1.0, 2.0), cell(9.0, 7.0));
121        assert_eq!(
122            grid_rect(cell(1.0, 2.0), cell(9.0, 7.0)).size.h,
123            5,
124            "precondition: the drag's own height is off the ladder"
125        );
126        assert_eq!(rect.top_left, GridPoint { x: 1, y: 2 });
127        assert_eq!(rect.size, GridSize { w: 8, h: 4 });
128
129        // Every rung the ladder offers holds one more pin slot than the last.
130        for (drag, ladder) in [(1, 4), (5, 4), (6, 7), (9, 10)] {
131            let rect = block_rect(cell(0.0, 0.0), cell(3.0, drag as f32));
132            assert_eq!(rect.size.h, ladder);
133            assert_eq!(
134                max_pin_slot(ladder as f32 * GRID_SIZE),
135                (ladder - 4) / 3,
136                "a valid height's capacity is a whole number of slots"
137            );
138        }
139    }
140}