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}