返回 CodeWhale
stage.rs
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
245 lines RUST