Skip to main content

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