all_is_cubes/character/
cursor.rs

1//! [`Cursor`] type and related items.
2//!
3//! TODO: It's unclear what the scope of this module should be.
4
5use 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
16/// Find the first selectable block the ray strikes and express the result in a [`Cursor`]
17/// value, or [`None`] if nothing was struck within the distance limit.
18pub 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        // Check intersection with recursive block
40        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                            // Set the selected face to the first face we hit, which
55                            // will be the face of the bounding box we hit.
56                            // TODO: Either don't rely on the bounding box (perhaps
57                            // only take faces of selectable voxels) or change block
58                            // evaluation to make the bounding box guaranteed tight.
59                            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/// Data collected by [`cursor_raycast`] about the blocks struck by the ray; intended to be
98/// sufficient for various player interactions with blocks.
99#[derive(Clone, Debug, PartialEq)]
100pub struct Cursor {
101    /// The space the selected cube is in.
102    space: Handle<Space>,
103
104    /// The face that the cursor ray entered the cube via.
105    ///
106    /// Note that this is not necessarily the same as “the face of the block” in the case
107    /// where the block occupies less than the full volume.
108    face_entered: Face7,
109
110    /// The face of the block that is being selected.
111    face_selected: Face7,
112
113    /// Intersection point where the ray entered the cube.
114    point_entered: FreePoint,
115
116    /// Distance from ray origin (viewpoint) to `point_entered`.
117    distance_to_point: FreeCoordinate,
118
119    /// Data about the cube the cursor selected/hit.
120    hit: CubeSnapshot,
121
122    /// Data about the cube the cursor ray was in before it hit [`Self::hit`],
123    /// if there was one, or `None` if the cursor ray started in the cube it hit.
124    preceding: Option<CubeSnapshot>,
125}
126
127/// Snapshot of the contents of one cube of a [`Space`], independent of the [`Space`].
128///
129/// TODO: Can we find a cleaner name for this class?
130#[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    /// The space the selected cube is in.
142    #[inline]
143    pub fn space(&self) -> &Handle<Space> {
144        &self.space
145    }
146
147    /// Which cube of the space that the cursor ray selected/hit.
148    pub fn cube(&self) -> Cube {
149        self.hit.position
150    }
151
152    /// The cube the ray passed through immediately before the selected cube.
153    ///
154    /// This may be the same cube if the ray started there.
155    pub fn preceding_cube(&self) -> Cube {
156        self.cube() + self.face_entered.normal_vector()
157    }
158
159    /// Which face of the block the cursor ray selected/hit.
160    ///
161    /// Note that this is not necessarily the same as the face of the enclosing cube,
162    /// in the case where the block occupies less than the full volume; rather it is
163    /// intended to make sense to the human who does not get to see the cube grid.
164    /// It is currently defined to be the hit face of the bounding box of the block data
165    /// (which is often but not always tightly bounding the visible voxels, so this will
166    /// have the unsurprising value for any box-shaped block).
167    ///
168    /// Will be [`Face7::Within`] if the ray started inside the block.
169    pub fn face_selected(&self) -> Face7 {
170        self.face_selected
171    }
172
173    /// Returns data about the cube the cursor selected/hit.
174    #[inline]
175    pub fn hit(&self) -> &CubeSnapshot {
176        &self.hit
177    }
178
179    // TODO: Preceding data is actually unused except for debug info via fmt::Display...
180    // Should we remove it? Tools do care about the preceding space but not quite this way.
181    // I think there was some use-case for having the preceding/selected-adjacent block's
182    // EvaluatedBlock data, though.
183    //
184    // /// Returns data about the cube the cursor ray passed through just before it hit anything.
185    // /// If the ray started from within the cube it hit, returns the same as [`hit()`](Self::hit).
186    // #[inline]
187    // pub fn preceding_or_within(&self) -> &CubeSnapshot {
188    //     self.preceding.as_ref().unwrap_or(&self.hit)
189    // }
190}
191
192// TODO: this probably shouldn't be Display any more, but Debug or ConciseDebug
193// — or just a regular method.
194impl 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        // Compute an approximate offset that will prevent Z-fighting.
213        let offset_from_surface = 0.001 * self.distance_to_point;
214
215        // AABB of the block's actual content. We use this rather than the full extent of
216        // the cube so that it feels more accurate.
217        //
218        // TODO: voxels_bounds() is not really an intentionally designed selection box and
219        // will often be oversized.
220        // (But maybe we should guarantee it is right-sized within block evaluation?)
221        // (Perhaps a better box would be the bounds of all `selectable` voxels?)
222        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        // Add wireframe of the block.
229        block_aabb
230            .expand(offset_from_surface)
231            .wireframe_points(&mut lines::colorize(output, palette::CURSOR_OUTLINE));
232
233        // Frame the selected face with a square.
234        // TODO: Position this frame relative to block_aabb.
235        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        // Frame the cursor intersection point with a diamond.
262        // TODO: This addition is experimental and we may or may not want to keep it.
263        // For now, it visualizes the intersection and face information.
264        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/// These are tests of [`cursor_raycast()`] and the data it returns.
283/// For tests of behavior when actually _using_ a [`Cursor`] to invoke a tool,
284/// see [`crate::character::tests`] and [`crate::inv`].
285#[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    /// A [`Ray`] aligned with the X axis, such that it starts in cube [-1, 0, 0] and hits
304    /// [0, 0, 0], [1, 0, 0], [2, 0, 0], et cetera, and just slightly above the midpoint.
305    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, [&not_selectable, &block]);
342
343        let cursor = cursor_raycast(universe.read_ticket(), X_RAY, &space_handle, f64::INFINITY)
344            .unwrap()
345            .unwrap();
346        // If the non-selectable block was hit, this would be [0, 0, 0]
347        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); // Upper half is nonselectable air
356        let space_handle = test_space(universe, [&not_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    /// A [`Ray`] which will pass through the left face and then the middle Y plane of a
380    /// block located at [0, 0, 0].
381    ///
382    /// ```text
383    /// 1 +----•-+------+
384    ///   |     \|      |
385    ///   |      \      |
386    ///   |      |\     |
387    ///   |      | \    |
388    ///   |      |  \   |
389    ///   |      |   ↘  |
390    /// 0 +------+------+
391    ///  -1      0      1
392    /// ```
393    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    /// Testing the “normal” case in contrast to `slope_hits_face_different_from_entered`.
399    #[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 the case where the face of the block the cursor ray hits is not equal to the
418    /// face of the cube the ray entered.
419    #[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}