| 1 | //! The one session-scoped performance every whale surface reads. |
| 2 | //! |
| 3 | //! The Stage holds a single [`Director`] for the foreground session and a |
| 4 | //! single clock. Owner truth enters through [`Stage::observe`]; surfaces |
| 5 | //! call [`Stage::advance`] before they paint, which is idempotent within a |
| 6 | //! frame, so any number of surfaces share one clock instead of each |
| 7 | //! stepping its own. Nothing here classifies a tool, reads telemetry or |
| 8 | //! owns a status timer. |
| 9 | |
| 10 | use super::acting::{Activity, Context, Director, Options, Presence, Span}; |
| 11 | use super::data::Act; |
| 12 | use super::habitat::{Cove, Layer}; |
| 13 | use super::rig::Parts; |
| 14 | use super::scene::{View, scene, scene_posed}; |
| 15 | use serde_json::Value; |
| 16 | use std::time::{Duration, Instant}; |
| 17 | |
| 18 | /// Everything the owner reports that the performance may use. |
| 19 | #[derive(Clone, Debug, PartialEq)] |
| 20 | pub struct Inputs { |
| 21 | pub presence: Presence, |
| 22 | pub activity: Option<Activity>, |
| 23 | pub context: Context, |
| 24 | } |
| 25 | |
| 26 | /// A surface's paint caps (handoff `scheduling`): these are ceilings on one |
| 27 | /// shared clock, not extra timers. |
| 28 | #[derive(Clone, Copy, Debug, PartialEq, Eq)] |
| 29 | pub enum Tier { |
| 30 | /// Habitat and companion: 30 paints/s in action, 8 at rest. |
| 31 | Hero, |
| 32 | /// Rail head and toolbar mark: 15 paints/s in action, 2 at rest. |
| 33 | Small, |
| 34 | /// Terminal art: at most 6 paints/s in action, 2 at rest. |
| 35 | Terminal, |
| 36 | } |
| 37 | |
| 38 | /// Longest catch-up after a hidden or stalled interval. Anything older is |
| 39 | /// dropped: resume from current owner truth, never replay missed motion. |
| 40 | pub const MAX_CATCH_UP: Duration = Duration::from_secs(1); |
| 41 | /// Surfaces painting in the same display frame share the step. |
| 42 | const SAME_FRAME: Duration = Duration::from_millis(4); |
| 43 | const STEP: f64 = 1. / 30.; |
| 44 | |
| 45 | pub struct Stage { |
| 46 | session: Option<String>, |
| 47 | director: Director, |
| 48 | inputs: Option<Inputs>, |
| 49 | clock: Option<Instant>, |
| 50 | cove: Cove, |
| 51 | visible: bool, |
| 52 | } |
| 53 | |
| 54 | /// A large surface's frame: the cove behind, the performance, the near |
| 55 | /// water in front. |
| 56 | #[derive(Clone, Debug)] |
| 57 | pub struct CoveScene { |
| 58 | pub behind: Vec<Layer>, |
| 59 | pub parts: Parts, |
| 60 | pub front: Vec<Layer>, |
| 61 | } |
| 62 | |
| 63 | impl Default for Stage { |
| 64 | fn default() -> Self { |
| 65 | Self::new() |
| 66 | } |
| 67 | } |
| 68 | |
| 69 | impl Stage { |
| 70 | pub fn new() -> Self { |
| 71 | Self { |
| 72 | session: None, |
| 73 | director: Director::new(Options::default()), |
| 74 | inputs: None, |
| 75 | clock: None, |
| 76 | cove: Cove::default(), |
| 77 | visible: true, |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | /// Apply owner truth for the foreground `session`. A different session |
| 82 | /// replaces the Director outright: no pending clip, calf count or |
| 83 | /// completed-turn identity carries across. Returns whether anything |
| 84 | /// the performance uses changed. |
| 85 | pub fn observe(&mut self, session: Option<&str>, inputs: Inputs, reduced: bool) -> bool { |
| 86 | let mut changed = self.director.reduced != reduced; |
| 87 | if self.session.as_deref() != session { |
| 88 | changed = true; |
| 89 | self.session = session.map(str::to_string); |
| 90 | self.director = Director::new(Options { |
| 91 | reduced, |
| 92 | ..Options::default() |
| 93 | }); |
| 94 | self.inputs = None; |
| 95 | self.clock = None; |
| 96 | self.cove.reset(); |
| 97 | } |
| 98 | self.director.set_reduced(reduced); |
| 99 | if self.inputs.as_ref() != Some(&inputs) { |
| 100 | self.director.set( |
| 101 | inputs.presence, |
| 102 | inputs.activity.clone(), |
| 103 | inputs.context.clone(), |
| 104 | ); |
| 105 | self.inputs = Some(inputs); |
| 106 | changed = true; |
| 107 | } |
| 108 | changed |
| 109 | } |
| 110 | |
| 111 | /// Advance the shared clock to `now` in steps of at most 1/30 s. |
| 112 | pub fn advance(&mut self, now: Instant) { |
| 113 | if !self.visible { |
| 114 | self.clock = None; |
| 115 | return; |
| 116 | } |
| 117 | let Some(last) = self.clock else { |
| 118 | self.clock = Some(now); |
| 119 | return; |
| 120 | }; |
| 121 | let elapsed = now.saturating_duration_since(last); |
| 122 | if elapsed < SAME_FRAME { |
| 123 | return; |
| 124 | } |
| 125 | self.clock = Some(now); |
| 126 | let mut remaining = elapsed.min(MAX_CATCH_UP).as_secs_f64(); |
| 127 | while remaining > 1e-9 { |
| 128 | let dt = remaining.min(STEP); |
| 129 | self.director.step(dt); |
| 130 | remaining -= dt; |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | pub fn scene(&self, view: View) -> Parts { |
| 135 | scene(&self.director, view) |
| 136 | } |
| 137 | |
| 138 | /// Stop painting while hidden. Resuming starts from current owner truth: |
| 139 | /// no hidden interval, stale entry clip or particle is replayed. The host |
| 140 | /// continues to call `observe` when its authoritative inputs change. |
| 141 | pub fn set_visible(&mut self, visible: bool) { |
| 142 | if self.visible == visible { |
| 143 | return; |
| 144 | } |
| 145 | self.visible = visible; |
| 146 | self.clock = None; |
| 147 | if visible { |
| 148 | let reduced = self.director.reduced; |
| 149 | // Reuse the canonical settle/resume boundary. Completed-turn and |
| 150 | // onset identities survive; only transient motion is discarded. |
| 151 | self.director.set_reduced(true); |
| 152 | self.director.set_reduced(reduced); |
| 153 | self.cove.reset(); |
| 154 | } |
| 155 | } |
| 156 | |
| 157 | pub fn is_visible(&self) -> bool { |
| 158 | self.visible |
| 159 | } |
| 160 | |
| 161 | /// The whale in its cove, for large surfaces. The cove rides the |
| 162 | /// Director's clock and only ever adjusts the resting gaze. |
| 163 | pub fn cove_scene(&mut self, view: View, dark: bool) -> CoveScene { |
| 164 | let overrides = self.cove.pose(&self.director); |
| 165 | CoveScene { |
| 166 | behind: self.cove.behind(&self.director, dark), |
| 167 | parts: scene_posed(&self.director, view, &overrides), |
| 168 | front: self.cove.front(&self.director, dark), |
| 169 | } |
| 170 | } |
| 171 | |
| 172 | /// Pointer over the cove, in design units (`−62…62`, positive Y down). |
| 173 | pub fn cove_observe(&mut self, x: f64, y: f64) { |
| 174 | self.cove.observe(x, y); |
| 175 | } |
| 176 | |
| 177 | pub fn cove_leave(&mut self) { |
| 178 | self.cove.leave(); |
| 179 | } |
| 180 | |
| 181 | /// A decorative tap on the water. It never reaches the Director's |
| 182 | /// inputs, so it cannot change presence, acting or Engine state. |
| 183 | pub fn cove_tap(&mut self, x: f64, y: f64) { |
| 184 | self.cove.tap(x, y, &self.director); |
| 185 | } |
| 186 | |
| 187 | pub fn cove(&self) -> &Cove { |
| 188 | &self.cove |
| 189 | } |
| 190 | |
| 191 | pub fn acting(&self) -> Act { |
| 192 | self.director.acting |
| 193 | } |
| 194 | |
| 195 | pub fn director(&self) -> &Director { |
| 196 | &self.director |
| 197 | } |
| 198 | |
| 199 | /// How long until this surface's next paint, or `None` when nothing |
| 200 | /// should be scheduled (reduced motion paints only on input changes). |
| 201 | pub fn cadence(&self, tier: Tier) -> Option<Duration> { |
| 202 | if self.director.reduced || !self.visible { |
| 203 | return None; |
| 204 | } |
| 205 | let resting = matches!(self.director.acting, Act::Rest | Act::Sleep) |
| 206 | && !self.director.in_transition() |
| 207 | && !self.cove.rippling(&self.director); |
| 208 | let hz = match (tier, resting) { |
| 209 | (Tier::Hero, false) => 30., |
| 210 | (Tier::Hero, true) => 8., |
| 211 | (Tier::Small, false) => 15., |
| 212 | (Tier::Small, true) => 2., |
| 213 | (Tier::Terminal, false) => 6., |
| 214 | (Tier::Terminal, true) => 2., |
| 215 | }; |
| 216 | Some(Duration::from_secs_f64(1. / hz)) |
| 217 | } |
| 218 | } |
| 219 | |
| 220 | /// Read the owner's `activity` object exactly as served. `observed` must |
| 221 | /// already be the owner's strict verdict; nothing is derived from a tool |
| 222 | /// name, label, argument or caption. |
| 223 | pub fn activity(value: &Value, observed: bool) -> Activity { |
| 224 | Activity { |
| 225 | kind: value["kind"].as_str().map(str::to_string), |
| 226 | observed, |
| 227 | parallel: value["parallel"].as_f64(), |
| 228 | active: value["active"] |
| 229 | .as_array() |
| 230 | .map(|spans| { |
| 231 | spans |
| 232 | .iter() |
| 233 | .take(4) |
| 234 | .filter_map(|span| { |
| 235 | Some(Span { |
| 236 | kind: span["kind"].as_str()?.to_string(), |
| 237 | since_ms: span["sinceMs"].as_f64().filter(|n| n.is_finite())?, |
| 238 | }) |
| 239 | }) |
| 240 | .collect() |
| 241 | }) |
| 242 | .unwrap_or_default(), |
| 243 | } |
| 244 | } |
| 245 |