Skip to main content

cranpose_ui/
eye.rs

1//! The eye a window's 3D scene is seen from, as a state an app holds: it
2//! turns as the app asks, and walks through the spaces behind the scene's
3//! doorways, stopped by solid surfaces where the renderer last placed them.
4
5use cranpose_core::{
6    CompositionLocal, MutableState, NodeId, compositionLocalOf, mutableStateOf, remember,
7};
8use cranpose_macros::composable;
9use cranpose_ui_graphics::space::{EyePlace, EyePose, Light, Scene3d, Vec3, WorldSink};
10
11/// The eye of a window's 3D scene.
12///
13/// Read in a [`scene3d`](crate::Modifier::scene3d) closure through
14/// [`Eye::scene`], it moves the scene's camera without recomposition. The
15/// scene tells the renderer where the eye stands among the spaces behind its
16/// doorways, and the renderer leaves the eye what it can bump into there
17/// each time it places the scene, which [`Eye::walk`] reads.
18#[derive(Clone)]
19pub struct Eye {
20    pose: MutableState<EyePose>,
21    world: WorldSink,
22    radius: f32,
23}
24
25impl Eye {
26    /// An eye at `pose`: a ball of `radius`, in the units of the space it
27    /// stands in, that stops short of solid surfaces.
28    pub fn new(pose: EyePose, radius: f32) -> Self {
29        Self {
30            pose: mutableStateOf(pose),
31            world: WorldSink::default(),
32            radius,
33        }
34    }
35
36    /// Where the eye stands and which way it looks.
37    pub fn pose(&self) -> EyePose {
38        self.pose.get()
39    }
40
41    /// The scene seen through the eye: its camera, with `focal_length`, in
42    /// the space it stands in, and `light`.
43    pub fn scene(&self, focal_length: f32, light: Option<Light>) -> Scene3d {
44        let pose = self.pose.get();
45        Scene3d {
46            camera: pose.camera(focal_length),
47            light,
48            eye: Some(EyePlace {
49                doorways: pose.doorways,
50                world: self.world.clone(),
51            }),
52        }
53    }
54
55    /// Walks the eye `step`, in the coordinates of the space it stands in:
56    /// through the doorways the step passes, and pushed back from the solid
57    /// surfaces of the space it walks into, along which it slides.
58    pub fn walk(&self, step: Vec3) {
59        let world = self.world.current();
60        let radius = self.radius;
61        self.pose
62            .update(|pose| *pose = world.walk(pose, step, radius));
63    }
64
65    /// Turns the eye `yaw` radians to the right and `pitch` up (see
66    /// [`EyePose::turn`]).
67    pub fn turn(&self, yaw: f32, pitch: f32) {
68        self.pose.update(|pose| pose.turn(yaw, pitch));
69    }
70
71    /// Puts the eye at `pose`, through no doorway and stopped by nothing.
72    pub fn place(&self, pose: EyePose) {
73        self.pose.set(pose);
74    }
75
76    /// Where the eye stands and which way it looks, in the coordinates of
77    /// the space of the scene node `scene`: the one it stands in or one a
78    /// doorway from it. `None` when no such space lies there.
79    pub fn pose_in(&self, scene: NodeId) -> Option<EyePose> {
80        self.world.current().pose_in(&self.pose.get(), scene)
81    }
82
83    /// Puts the eye at `pose`, in the coordinates of the space of the scene
84    /// node `scene`, as if it walked there: that space is the one the eye
85    /// stands in or one a doorway from it. False, and the eye stays where
86    /// it is, when no such space lies there.
87    pub fn enter(&self, scene: NodeId, pose: EyePose) -> bool {
88        let Some(doorways) = self.world.current().doorways_to(scene) else {
89            return false;
90        };
91        self.pose.set(EyePose { doorways, ..pose });
92        true
93    }
94}
95
96/// Two handles are equal when they hold the same eye.
97impl PartialEq for Eye {
98    fn eq(&self, other: &Self) -> bool {
99        self.world == other.world
100    }
101}
102
103/// An [`Eye`] kept across recompositions, made at `pose` the first time.
104#[allow(non_snake_case)]
105#[composable]
106pub fn rememberEye(pose: impl FnOnce() -> EyePose, radius: f32) -> Eye {
107    remember(move || Eye::new(pose(), radius)).with(Clone::clone)
108}
109
110/// CompositionLocal carrying the eye of the window scene the content lies
111/// in, or `None` outside one.
112///
113/// An app that shows a scene through an [`Eye`] provides the eye around the
114/// scene's content, so a scene composed behind one of its doorways knows the
115/// window's eye walks into it and leaves its own camera standing in the
116/// doorway.
117pub fn local_eye() -> CompositionLocal<Option<Eye>> {
118    crate::environment_locals::cached_local(|locals| &locals.eye, || compositionLocalOf(|| None))
119}