all_is_cubes/inv/
icons.rs

1use core::fmt;
2
3use euclid::vec3;
4use exhaust::Exhaust;
5
6/// Acts as polyfill for float methods
7#[cfg(not(feature = "std"))]
8#[allow(unused_imports)]
9use num_traits::float::FloatCore as _;
10
11use crate::block::{self, AIR, Block, Resolution::*};
12use crate::content::load_image::{block_from_image, default_srgb, include_image};
13use crate::drawing::VoxelBrush;
14use crate::linking::{BlockModule, BlockProvider};
15use crate::math::{FreeCoordinate, GridCoordinate, GridRotation, Rgba, rgb_const, rgba_const};
16use crate::universe::{ReadTicket, UniverseTransaction};
17
18#[cfg(doc)]
19use crate::inv::Tool;
20use crate::util::YieldProgress;
21
22/// Blocks that are icons for [`Tool`]s.
23///
24/// TODO: Should this be considered strictly part of the UI/content and not fundamentals,
25/// since it is making lots of aesthetic decisions?
26/// If so, then [`Tool::icon()`] needs to go away, and the UI will need to either contain
27/// these icons or accept them as configuration.
28#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Exhaust)]
29#[non_exhaustive]
30pub enum Icons {
31    /// Icon for an empty toolbar slot.
32    EmptySlot,
33    /// Icon for [`Tool::Activate`],
34    Activate,
35    /// Icon for [`Tool::RemoveBlock`].
36    Delete,
37    /// Icon for [`Tool::CopyFromSpace`].
38    CopyFromSpace,
39    /// Icon for [`Tool::EditBlock`].
40    EditBlock,
41    /// Icon for [`Tool::PushPull`].
42    PushPull,
43    /// Icon for [`Tool::Jetpack`].
44    Jetpack {
45        /// Actually flying?
46        active: bool,
47    },
48}
49
50impl BlockModule for Icons {
51    fn namespace() -> &'static str {
52        "all-is-cubes/vui/icons"
53    }
54}
55
56impl fmt::Display for Icons {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        match self {
59            Icons::EmptySlot => write!(f, "empty-slot"),
60            Icons::Activate => write!(f, "activate"),
61            Icons::Delete => write!(f, "delete"),
62            Icons::CopyFromSpace => write!(f, "copy-from-space"),
63            Icons::EditBlock => write!(f, "edit-block"),
64            Icons::PushPull => write!(f, "push"),
65            Icons::Jetpack { active } => write!(f, "jetpack/{active}"),
66        }
67    }
68}
69
70impl Icons {
71    /// Construct the standard icons, inserting block definitions into the given
72    /// [`UniverseTransaction`].
73    pub async fn new(txn: &mut UniverseTransaction, p: YieldProgress) -> BlockProvider<Icons> {
74        let resolution = R16;
75
76        BlockProvider::new(p, |key| {
77            Ok(match key {
78                Icons::EmptySlot => Block::builder()
79                    .attributes(block::AIR_EVALUATED.attributes().clone())
80                    .display_name("")
81                    .color(Rgba::TRANSPARENT)
82                    .build(),
83
84                Icons::Activate => block_from_image(
85                    ReadTicket::stub(),
86                    include_image!("icons/hand.png"),
87                    GridRotation::RXyZ,
88                    &default_srgb,
89                )?
90                .display_name("Activate")
91                .build_txn(txn),
92
93                Icons::Delete => block_from_image(
94                    ReadTicket::stub(),
95                    include_image!("icons/placeholder-hammer.png"),
96                    GridRotation::RXyZ,
97                    &default_srgb,
98                )?
99                .display_name("Delete Block")
100                .build_txn(txn),
101
102                Icons::CopyFromSpace => Block::builder()
103                    .display_name("Copy Block from Cursor")
104                    // TODO: design actual icon
105                    .color(Rgba::new(0., 1., 0., 1.))
106                    .build(),
107
108                Icons::EditBlock => Block::builder()
109                    .display_name("Edit Block")
110                    // TODO: design actual icon
111                    .color(Rgba::new(0., 1., 0., 1.))
112                    .build(),
113
114                Icons::PushPull => {
115                    let dots = [block::from_color!(Rgba::BLACK), AIR];
116                    let dots = move |y: GridCoordinate| dots[y.rem_euclid(2) as usize].clone();
117                    fn ybrush(mut f: impl FnMut(GridCoordinate) -> Block) -> VoxelBrush<'static> {
118                        VoxelBrush::new((0..16).map(|y| ([0, y, 0], f(y))))
119                    }
120
121                    block_from_image(
122                        ReadTicket::stub(),
123                        include_image!("icons/push.png"),
124                        GridRotation::RXZY,
125                        &|color| {
126                            // TODO: Figure out abstractions to not need so much fiddly custom code
127                            let bcolor = Block::from(Rgba::from_srgb8(color));
128                            match color {
129                                [0, 0, 0, 255] => VoxelBrush::new(vec![([0, 15, 0], dots(0))]),
130                                [0x85, 0x85, 0x85, 255] => {
131                                    VoxelBrush::new(vec![([0, 0, 0], dots(0))])
132                                }
133                                [0, 127, 0, 255] => ybrush(&dots),
134                                [0, 255, 0, 255] => ybrush(|y| dots(y + 1)),
135                                [255, 0, 0, 255] => ybrush(|_| bcolor.clone()),
136                                _ => VoxelBrush::new([([0, 0, 0], bcolor)]),
137                            }
138                            .translate([0, 8, 0])
139                        },
140                    )?
141                    .display_name("Push/Pull")
142                    .build_txn(txn)
143                }
144
145                Icons::Jetpack { active } => {
146                    let shell_block = block::from_color!(0.5, 0.5, 0.5);
147                    let stripe_block = block::from_color!(0.9, 0.1, 0.1);
148                    let exhaust = if active {
149                        Block::builder()
150                            .color(rgba_const!(1.0, 1.0, 1.0, 0.1))
151                            .light_emission(rgb_const!(1.0, 0.8, 0.8) * 16.0)
152                            .build()
153                    } else {
154                        AIR
155                    };
156                    let active_color = if active {
157                        block::from_color!(1.0, 1.0, 0.5, 1.)
158                    } else {
159                        block::from_color!(0.4, 0.4, 0.4, 1.)
160                    };
161                    let shape: [(FreeCoordinate, &Block); 16] = [
162                        (4., &shell_block),
163                        (6., &shell_block),
164                        (6.5, &shell_block),
165                        (7., &shell_block),
166                        (7.25, &shell_block),
167                        (5., &active_color),
168                        (7.25, &shell_block),
169                        (5., &active_color),
170                        (7.25, &shell_block),
171                        (6.5, &shell_block),
172                        (6.0, &shell_block),
173                        (5.5, &shell_block),
174                        (5.0, &shell_block),
175                        (4.5, &shell_block),
176                        (4.5, &exhaust),
177                        (4.5, &exhaust),
178                    ];
179                    Block::builder()
180                        .display_name(if active {
181                            "Jetpack (on)"
182                        } else {
183                            "Jetpack (off)"
184                        })
185                        .voxels_fn(resolution, |cube| {
186                            let (shape_radius, block) =
187                                shape[((GridCoordinate::from(resolution) - 1) - cube.y) as usize];
188                            let centered_p = cube.center().map(|c| c - f64::from(resolution) / 2.0);
189                            let r4 = centered_p
190                                .to_vector()
191                                .component_mul(vec3(1., 0., 1.))
192                                .square_length()
193                                .powi(2);
194                            if r4 <= shape_radius.powi(4) {
195                                if block == &shell_block
196                                    && (centered_p.x.abs() <= 1.0 || centered_p.z.abs() <= 1.0)
197                                {
198                                    &stripe_block
199                                } else {
200                                    block
201                                }
202                            } else {
203                                &AIR
204                            }
205                        })?
206                        .build_txn(txn)
207                }
208            })
209        })
210        .await
211        .unwrap()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::util::yield_progress_for_testing;
219
220    #[macro_rules_attribute::apply(smol_macros::test)]
221    async fn icons_smoke_test() {
222        Icons::new(
223            &mut UniverseTransaction::default(),
224            yield_progress_for_testing(),
225        )
226        .await;
227    }
228}