1use core::fmt;
2
3use euclid::vec3;
4use exhaust::Exhaust;
5
6#[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#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, Exhaust)]
29#[non_exhaustive]
30pub enum Icons {
31 EmptySlot,
33 Activate,
35 Delete,
37 CopyFromSpace,
39 EditBlock,
41 PushPull,
43 Jetpack {
45 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 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 .color(Rgba::new(0., 1., 0., 1.))
106 .build(),
107
108 Icons::EditBlock => Block::builder()
109 .display_name("Edit Block")
110 .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 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}