1use core::fmt;
6
7use euclid::point3;
8
9use crate::block::{Block, EvaluatedBlock, Evoxel};
10use crate::content::palette;
11use crate::math::{Cube, Face6, Face7, FreeCoordinate, FreePoint, FreeVector, lines};
12use crate::raycast::Ray;
13use crate::space::{PackedLight, Space};
14use crate::universe::{Handle, HandleError, ReadTicket};
15
16pub fn cursor_raycast(
19 read_ticket: ReadTicket<'_>,
20 mut ray: Ray,
21 space_handle: &Handle<Space>,
22 maximum_distance: FreeCoordinate,
23) -> Result<Option<Cursor>, HandleError> {
24 ray.direction = ray.direction.normalize();
25 let space = space_handle.read(read_ticket)?;
26 for step in ray.cast().within(space.bounds(), false) {
27 if step.t_distance() > maximum_distance {
28 break;
29 }
30
31 let cube = step.cube_ahead();
32 let evaluated = space.get_evaluated(cube);
33 let mut face_selected = None;
34
35 if !evaluated.attributes().selectable {
36 continue;
37 }
38
39 match evaluated.voxels().single_voxel() {
41 Some(evoxel) => {
42 if !evoxel.selectable {
43 continue;
44 }
45 face_selected = Some(step.face());
46 }
47 None => {
48 let voxels = evaluated.voxels().as_vol_ref();
49 let recursive_hit: Option<(Cube, &Evoxel)> = step
50 .recursive_raycast(ray, evaluated.resolution(), voxels.bounds())
51 .0
52 .filter_map(|voxel_step| {
53 if face_selected.is_none() {
54 face_selected = Some(voxel_step.face());
60 }
61 voxels.get(voxel_step.cube_ahead()).map(|v| (voxel_step.cube_ahead(), v))
62 })
63 .find(|(_, v)| v.selectable);
64 if recursive_hit.is_none() {
65 continue;
66 }
67 }
68 }
69
70 return Ok(Some(Cursor {
71 space: space_handle.clone(),
72 face_entered: step.face(),
73 face_selected: face_selected.expect("failed to determine face_selected"),
74 point_entered: step.intersection_point(ray),
75 distance_to_point: step.t_distance(),
76 hit: CubeSnapshot {
77 position: cube,
78 block: space[cube].clone(),
79 evaluated: evaluated.clone(),
80 light: space.get_lighting(cube),
81 },
82 preceding: if step.face() != Face7::Within {
83 let pcube = step.cube_behind();
84 Some(CubeSnapshot {
85 position: pcube,
86 block: space[pcube].clone(),
87 evaluated: space.get_evaluated(pcube).clone(),
88 light: space.get_lighting(pcube),
89 })
90 } else {
91 None
92 },
93 }));
94 }
95 Ok(None)
96}
97#[derive(Clone, Debug, PartialEq)]
100pub struct Cursor {
101 space: Handle<Space>,
103
104 face_entered: Face7,
109
110 face_selected: Face7,
112
113 point_entered: FreePoint,
115
116 distance_to_point: FreeCoordinate,
118
119 hit: CubeSnapshot,
121
122 preceding: Option<CubeSnapshot>,
125}
126
127#[derive(Clone, Debug, PartialEq)]
131#[non_exhaustive]
132#[allow(missing_docs, reason = "TODO")]
133pub struct CubeSnapshot {
134 pub position: Cube,
135 pub block: Block,
136 pub evaluated: EvaluatedBlock,
137 pub light: PackedLight,
138}
139
140impl Cursor {
141 #[inline]
143 pub fn space(&self) -> &Handle<Space> {
144 &self.space
145 }
146
147 pub fn cube(&self) -> Cube {
149 self.hit.position
150 }
151
152 pub fn preceding_cube(&self) -> Cube {
156 self.cube() + self.face_entered.normal_vector()
157 }
158
159 pub fn face_selected(&self) -> Face7 {
170 self.face_selected
171 }
172
173 #[inline]
175 pub fn hit(&self) -> &CubeSnapshot {
176 &self.hit
177 }
178
179 }
191
192impl fmt::Display for Cursor {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 write!(
197 f,
198 "Block at {c:?} face {f:?}\n{ev:#?}\nLighting within {la:?}, behind {lb:?}",
199 c = self.cube(),
200 f = self.face_entered,
201 ev = self.hit().evaluated,
202 la = self.hit().light,
203 lb = self.preceding.as_ref().map(|s| s.light),
204 )
205 }
206}
207
208impl lines::Wireframe for Cursor {
209 fn wireframe_points<E: Extend<[lines::Vertex; 2]>>(&self, output: &mut E) {
210 let evaluated = &self.hit().evaluated;
211
212 let offset_from_surface = 0.001 * self.distance_to_point;
214
215 let block_aabb = evaluated
223 .voxels_bounds()
224 .to_free()
225 .scale(evaluated.resolution().recip_f64())
226 .translate(self.cube().lower_bounds().map(FreeCoordinate::from).to_vector());
227
228 block_aabb
230 .expand(offset_from_surface)
231 .wireframe_points(&mut lines::colorize(output, palette::CURSOR_OUTLINE));
232
233 if let Ok(face) = Face6::try_from(self.face_selected()) {
236 let face_transform_full = face.face_transform(1).to_matrix().to_free().then(
237 &self
238 .hit()
239 .position
240 .lower_bounds()
241 .map(FreeCoordinate::from)
242 .to_vector()
243 .to_transform(),
244 );
245
246 let inset = 1. / 128.;
247 output.extend(lines::line_loop(
248 [
249 point3(inset, inset, -offset_from_surface),
250 point3(inset, 1. - inset, -offset_from_surface),
251 point3(1. - inset, 1. - inset, -offset_from_surface),
252 point3(1. - inset, inset, -offset_from_surface),
253 ]
254 .map(|p| lines::Vertex {
255 position: face_transform_full.transform_point3d(p).unwrap(),
256 color: Some(palette::CURSOR_OUTLINE),
257 }),
258 ));
259 }
260
261 if let Ok(face) = Face6::try_from(self.face_entered) {
265 let face_transform_axes_only = face.rotation_from_nz().to_rotation_matrix().to_free();
266 output.extend(lines::line_loop(
267 [Face7::PX, Face7::PY, Face7::NX, Face7::NY].map(|f| {
268 let tip: FreeVector =
269 face_transform_axes_only.transform_vector3d(f.vector(1.0 / 32.0));
270 let position =
271 self.point_entered + self.face_entered.vector(offset_from_surface) + tip;
272 lines::Vertex {
273 position,
274 color: Some(palette::CURSOR_OUTLINE),
275 }
276 }),
277 ));
278 }
279 }
280}
281
282#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::block::{AIR, Resolution::*};
289 use crate::content::{make_slab, make_some_blocks};
290 use crate::math::{GridAab, Rgba};
291 use crate::universe::Universe;
292 use euclid::{Point3D, Vector3D, vec3};
293
294 fn test_space<const N: usize>(universe: &mut Universe, blocks: [&Block; N]) -> Handle<Space> {
295 let mut space =
296 Space::builder(GridAab::from_lower_size([0, 0, 0], vec3(N, 1, 1).to_u32())).build();
297 space.mutate(universe.read_ticket(), |m| {
298 m.fill_all(|p| Some(blocks[p.x as usize])).unwrap();
299 });
300 universe.insert_anonymous(space)
301 }
302
303 const X_RAY: Ray = Ray {
306 origin: Point3D::new(-0.5, 0.500001, 0.500001),
307 direction: Vector3D::new(1., 0., 0.),
308 };
309
310 #[test]
311 fn simple_hit_after_air() {
312 let universe = &mut Universe::new();
313 let [block] = make_some_blocks();
314 let space_handle = test_space(universe, [&AIR, &block]);
315
316 let cursor = cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, f64::INFINITY)
317 .unwrap()
318 .unwrap();
319 assert_eq!(cursor.hit().block, block);
320 assert_eq!(cursor.cube(), Cube::new(1, 0, 0));
321 assert_eq!(cursor.face_selected(), Face7::NX);
322 }
323
324 #[test]
325 fn maximum_distance_too_short() {
326 let universe = &mut Universe::new();
327 let [block] = make_some_blocks();
328 let space_handle = test_space(universe, [&AIR, &block]);
329
330 assert_eq!(
331 cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, 1.0),
332 Ok(None)
333 );
334 }
335
336 #[test]
337 fn ignores_not_selectable_atom() {
338 let universe = &mut Universe::new();
339 let [block] = make_some_blocks();
340 let not_selectable = Block::builder().color(Rgba::WHITE).selectable(false).build();
341 let space_handle = test_space(universe, [¬_selectable, &block]);
342
343 let cursor = cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, f64::INFINITY)
344 .unwrap()
345 .unwrap();
346 assert_eq!(cursor.cube(), Cube::new(1, 0, 0));
348 assert_eq!(cursor.hit().block, block);
349 }
350
351 #[test]
352 fn ignores_not_selectable_voxels() {
353 let universe = &mut Universe::new();
354 let [block] = make_some_blocks();
355 let not_selectable = make_slab(universe, 1, R2); let space_handle = test_space(universe, [¬_selectable, &block]);
357
358 let cursor = cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, f64::INFINITY)
359 .unwrap()
360 .unwrap();
361 assert_eq!(cursor.cube(), Cube::new(1, 0, 0));
362 assert_eq!(cursor.hit().block, block);
363 }
364
365 #[test]
366 fn hits_selectable_voxels() {
367 let universe = &mut Universe::new();
368 let [other_block] = make_some_blocks();
369 let selectable_voxels = make_slab(universe, 3, R4);
370 let space_handle = test_space(universe, [&AIR, &selectable_voxels, &other_block]);
371
372 let cursor = cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, f64::INFINITY)
373 .unwrap()
374 .unwrap();
375 assert_eq!(cursor.cube(), Cube::new(1, 0, 0));
376 assert_eq!(cursor.hit().block, selectable_voxels);
377 }
378
379 const SLOPING_RAY: Ray = Ray {
394 origin: Point3D::new(-0.25, 1.0, 0.5),
395 direction: Vector3D::new(1.0, -1.0, 0.0),
396 };
397
398 #[test]
400 fn slope_hits_face_of_full_block() {
401 let universe = &mut Universe::new();
402 let [block] = make_some_blocks();
403 let space_handle = test_space(universe, [&block]);
404
405 let cursor = cursor_raycast(
406 universe.read_ticket(),
407 SLOPING_RAY,
408 &space_handle,
409 f64::INFINITY,
410 )
411 .unwrap()
412 .unwrap();
413 assert_eq!(cursor.face_entered, Face7::NX);
414 assert_eq!(cursor.face_selected(), Face7::NX);
415 }
416
417 #[test]
420 fn slope_hits_face_different_from_entered() {
421 let universe = &mut Universe::new();
422 let slab = make_slab(universe, 1, R2);
423 let space_handle = test_space(universe, [&slab]);
424
425 let cursor = cursor_raycast(
426 universe.read_ticket(),
427 SLOPING_RAY,
428 &space_handle,
429 f64::INFINITY,
430 )
431 .unwrap()
432 .unwrap();
433 dbg!(&cursor);
434 assert_eq!(cursor.face_entered, Face7::NX);
435 assert_eq!(cursor.face_selected(), Face7::PY);
436 }
437}