1use konjure_sdk::{Diagnostic, Frame, Runtime};
2
3#[derive(Debug, thiserror::Error)]
5pub enum BrowserError {
6 #[error("{0} must be finite and non-negative")]
8 InvalidTime(&'static str),
9 #[error("browser frame timestamp moved backwards")]
11 RegressingTimestamp,
12 #[error("{0}")]
14 Scene(#[from] Diagnostic),
15}
16
17#[derive(Clone, Copy, Debug, PartialEq)]
19pub enum SessionInput {
20 Playing(bool),
22 Seek(f64),
24 Visible(bool),
26 ContextAvailable(bool),
28 Immersive(bool),
30}
31
32#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct FrameInput {
35 pub timestamp_ms: f64,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct FrameTiming {
42 pub time_seconds: f64,
44 pub delta_seconds: f64,
46 pub should_render: bool,
48}
49
50#[derive(Clone, Debug)]
52pub struct BrowserSession {
53 time_seconds: f64,
54 previous_ms: Option<f64>,
55 playing: bool,
56 visible: bool,
57 context_available: bool,
58 immersive: bool,
59}
60
61impl BrowserSession {
62 pub fn new(playing: bool) -> Self {
64 Self {
65 time_seconds: 0.0,
66 previous_ms: None,
67 playing,
68 visible: true,
69 context_available: true,
70 immersive: false,
71 }
72 }
73
74 pub fn apply(&mut self, input: SessionInput) -> Result<(), BrowserError> {
79 let changed = match input {
80 SessionInput::Playing(value) => {
81 self.playing = value;
82 true
83 }
84 SessionInput::Seek(value) => {
85 check_time(value, "scene time")?;
86 self.time_seconds = value;
87 true
88 }
89 SessionInput::Visible(value) => replace_if_changed(&mut self.visible, value),
90 SessionInput::ContextAvailable(value) => {
91 replace_if_changed(&mut self.context_available, value)
92 }
93 SessionInput::Immersive(value) => replace_if_changed(&mut self.immersive, value),
94 };
95 if changed {
96 self.previous_ms = None;
97 }
98 Ok(())
99 }
100
101 pub fn playing(&self) -> bool {
103 self.playing
104 }
105
106 pub fn time_seconds(&self) -> f64 {
108 self.time_seconds
109 }
110
111 pub fn active(&self) -> bool {
113 self.context_available && (self.visible || self.immersive)
114 }
115
116 pub fn frame(&mut self, input: FrameInput) -> Result<FrameTiming, BrowserError> {
121 check_time(input.timestamp_ms, "browser timestamp")?;
122 let mut delta_seconds = 0.0;
123 if self.active() {
124 if let Some(previous) = self.previous_ms {
125 if input.timestamp_ms < previous {
126 return Err(BrowserError::RegressingTimestamp);
127 }
128 if self.playing {
129 delta_seconds = ((input.timestamp_ms - previous) / 1000.0).min(0.1);
130 }
131 }
132 let time_seconds = self.time_seconds + delta_seconds;
133 check_time(time_seconds, "scene time")?;
134 self.time_seconds = time_seconds;
135 self.previous_ms = Some(input.timestamp_ms);
136 } else {
137 self.previous_ms = None;
138 }
139 Ok(FrameTiming {
140 time_seconds: self.time_seconds,
141 delta_seconds,
142 should_render: self.active(),
143 })
144 }
145
146 pub fn sample(
151 &mut self,
152 runtime: &Runtime,
153 input: FrameInput,
154 ) -> Result<Option<Frame>, BrowserError> {
155 let mut candidate = self.clone();
156 let timing = candidate.frame(input)?;
157 let frame = timing
158 .should_render
159 .then(|| runtime.sample(timing.time_seconds))
160 .transpose()?;
161 *self = candidate;
162 Ok(frame)
163 }
164}
165
166fn check_time(value: f64, label: &'static str) -> Result<(), BrowserError> {
167 if !value.is_finite() || value < 0.0 {
168 return Err(BrowserError::InvalidTime(label));
169 }
170 Ok(())
171}
172
173fn replace_if_changed(slot: &mut bool, value: bool) -> bool {
174 let changed = *slot != value;
175 *slot = value;
176 changed
177}