1use crate::{
4 anim::FRAME,
5 component::{Component, PaintCtx, Slot, next_slot},
6 context::UiContext,
7 frame::Rect,
8 props::{Prop, PropValue, Props},
9 scene::{Trace, rasterize},
10};
11
12pub struct Scene {
45 props: Props,
46 slot: Slot,
47 trace: Box<dyn Trace>,
48 cols: u16,
49 rows: u16,
50 live: bool,
51}
52
53impl Scene {
54 pub fn new(scene: impl Trace + 'static) -> Self {
56 Self {
57 props: Props::new(),
58 slot: next_slot(),
59 trace: Box::new(scene),
60 cols: 24,
61 rows: 8,
62 live: true,
63 }
64 }
65
66 pub const fn size(mut self, cols: u16, rows: u16) -> Self {
68 self.cols = cols;
69 self.rows = rows;
70 self
71 }
72
73 pub const fn still(mut self) -> Self {
75 self.live = false;
76 self
77 }
78
79 pub fn with(mut self, prop: Prop, value: impl Into<PropValue>) -> Self {
81 self.props.set(prop, value);
82 self
83 }
84}
85
86impl Component for Scene {
87 fn props(&self) -> &Props {
88 &self.props
89 }
90
91 fn props_mut(&mut self) -> &mut Props {
92 &mut self.props
93 }
94
95 fn slot(&self) -> Slot {
96 self.slot
97 }
98
99 fn measure(&mut self, _ctx: &UiContext) -> (u16, u16) {
100 (self.cols, self.cols)
101 }
102
103 fn height(&mut self, _ctx: &UiContext, _width: u16) -> u16 {
104 self.rows
105 }
106
107 fn paint(&mut self, pc: &mut PaintCtx<'_>, rect: Rect) {
108 if rect.y >= pc.clip || rect.width == 0 || rect.height == 0 {
109 return;
110 }
111 let camera = self.trace.advance(pc.now);
112 let base = self.props.style(&pc.ctx.theme);
113 let cols = self.cols.min(rect.width);
114 let rows = self.rows.min(rect.height).min(pc.clip - rect.y);
115 let frame = &mut *pc.frame;
116 let mut buffer = [0_u8; 4];
117 rasterize(&*self.trace, &camera, cols, rows, |x, y, glyph, color| {
118 frame.put(rect.x + x, rect.y + y, glyph.encode_utf8(&mut buffer), base.fg(color));
119 });
120 if self.live {
121 pc.wake(self.slot, pc.now + FRAME);
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use std::time::Duration;
129
130 use super::*;
131 use crate::{
132 anim,
133 scene::{Camera, Light, Material, Object, PathTracer, Ray, Sphere, Vec3, World, vec3},
134 test_support::frame_row_text,
135 ui::Ui,
136 };
137
138 fn orb(ray: Ray) -> (Vec3, f32) {
139 let along = -ray.origin.dot(ray.dir);
140 let nearest = ray.origin + ray.dir * along;
141 if along > 0.0 && nearest.dot(nearest) <= 1.0 {
142 (vec3(1.0, 1.0, 1.0), 1.0)
143 } else {
144 (Vec3::ZERO, 0.0)
145 }
146 }
147
148 fn physical_orb() -> PathTracer {
149 let world = World::new(vec![Object::new(
150 Sphere::new(Vec3::ZERO, 1.0),
151 Material::diffuse(Vec3::rgb(56, 189, 248)),
152 )])
153 .with_light(Light::directional(vec3(-1.0, -1.0, -1.0), Vec3::ONE, 2.0));
154 PathTracer::new(world)
155 }
156
157 fn lit_braille(ui: &Ui) -> bool {
158 (0..ui.frame().size().height).any(|row| {
159 frame_row_text(ui.frame(), row)
160 .chars()
161 .any(|glyph| ('\u{2801}'..='\u{28ff}').contains(&glyph))
162 })
163 }
164
165 #[test]
166 fn live_scene_paints_braille_and_reschedules() {
167 let mut ui = Ui::from_root(Scene::new(orb).size(12, 6), 12, UiContext::default());
168 assert!(lit_braille(&ui), "the orb lights braille cells");
169 assert_eq!(ui.next_wake(), Some(anim::FRAME));
170 assert!(ui.tick(anim::FRAME), "the frame deadline repaints");
171 assert_eq!(ui.next_wake(), Some(anim::FRAME + anim::FRAME));
172 }
173
174 #[test]
175 fn still_physical_scene_traces_and_requests_no_wake() {
176 let ui =
177 Ui::from_root(Scene::new(physical_orb()).size(12, 6).still(), 12, UiContext::default());
178 assert!(lit_braille(&ui));
179 assert_eq!(ui.next_wake(), None);
180 }
181
182 #[test]
183 fn animated_scene_reads_the_paint_clock() {
184 struct Turntable;
186 impl Trace for Turntable {
187 fn advance(&mut self, now: Duration) -> Camera {
188 Camera { yaw: now.as_secs_f32() * std::f32::consts::FRAC_PI_2, ..Camera::default() }
189 }
190
191 fn shade(&self, ray: Ray) -> (Vec3, f32) {
192 orb(Ray { origin: ray.origin - vec3(1.4, 0.0, 0.0), dir: ray.dir })
193 }
194 }
195
196 let rows = |ui: &Ui| -> Vec<String> {
197 (0..ui.frame().size().height)
198 .map(|row| frame_row_text(ui.frame(), row))
199 .collect()
200 };
201 let mut ui = Ui::from_root(Scene::new(Turntable).size(16, 8), 16, UiContext::default());
202 let start = rows(&ui);
203 ui.tick(Duration::from_millis(500));
204 assert_ne!(start, rows(&ui), "the clock swings the camera");
205 }
206}