Skip to main content

simular/renderers/
wasm.rs

1//! Generic WASM Renderer for `DemoEngine` Implementations
2//!
3//! Per specification SIMULAR-DEMO-002: This renderer is engine-agnostic.
4//! Any `DemoEngine` implementation can be rendered through this interface.
5//!
6//! # Architecture
7//!
8//! WASM bindings cannot use generic types directly with wasm-bindgen.
9//! This module provides:
10//! 1. `WasmRunner<E>` - internal runner that wraps any `DemoEngine`
11//! 2. JSON serialization for cross-boundary state transfer
12//! 3. Common operations (step, pause, reset, get state)
13//!
14//! Concrete WASM exports (e.g., `WasmOrbitDemo`) wrap `WasmRunner` internally.
15
16use crate::demos::{CriterionResult, DemoEngine, DemoMeta};
17use serde::Serialize;
18
19/// Generic WASM runner that wraps any `DemoEngine`.
20///
21/// This is used internally by concrete WASM exports.
22/// It cannot be exported via wasm-bindgen directly due to generic type limitations.
23#[derive(Debug)]
24pub struct WasmRunner<E: DemoEngine> {
25    engine: E,
26    running: bool,
27    paused: bool,
28    step_count: u64,
29}
30
31impl<E: DemoEngine> WasmRunner<E> {
32    /// Create a new WASM runner for the given engine.
33    #[must_use]
34    pub fn new(engine: E) -> Self {
35        Self {
36            engine,
37            running: true,
38            paused: false,
39            step_count: 0,
40        }
41    }
42
43    /// Create from YAML configuration.
44    ///
45    /// # Errors
46    ///
47    /// Returns error if YAML parsing fails.
48    pub fn from_yaml(yaml: &str) -> Result<Self, crate::demos::DemoError> {
49        let engine = E::from_yaml(yaml)?;
50        Ok(Self::new(engine))
51    }
52
53    /// Get reference to the engine.
54    #[must_use]
55    pub fn engine(&self) -> &E {
56        &self.engine
57    }
58
59    /// Get mutable reference to the engine.
60    pub fn engine_mut(&mut self) -> &mut E {
61        &mut self.engine
62    }
63
64    /// Check if runner is running.
65    #[must_use]
66    pub fn is_running(&self) -> bool {
67        self.running && !self.engine.is_complete()
68    }
69
70    /// Check if runner is paused.
71    #[must_use]
72    pub fn is_paused(&self) -> bool {
73        self.paused
74    }
75
76    /// Toggle pause state.
77    pub fn toggle_pause(&mut self) -> bool {
78        self.paused = !self.paused;
79        self.paused
80    }
81
82    /// Set paused state directly.
83    pub fn set_paused(&mut self, paused: bool) {
84        self.paused = paused;
85    }
86
87    /// Stop the runner.
88    pub fn stop(&mut self) {
89        self.running = false;
90    }
91
92    /// Resume running.
93    pub fn resume(&mut self) {
94        self.paused = false;
95    }
96
97    /// Reset the engine.
98    pub fn reset(&mut self) {
99        self.engine.reset();
100        self.step_count = 0;
101        self.paused = false;
102    }
103
104    /// Advance the simulation by one step.
105    ///
106    /// Returns false if paused or complete.
107    pub fn step(&mut self) -> bool {
108        if self.paused || self.engine.is_complete() {
109            return false;
110        }
111        self.engine.step();
112        self.step_count += 1;
113        true
114    }
115
116    /// Run multiple steps.
117    ///
118    /// Returns the number of steps completed.
119    pub fn run_steps(&mut self, num_steps: u32) -> u32 {
120        let mut completed = 0;
121        for _ in 0..num_steps {
122            if !self.step() {
123                break;
124            }
125            completed += 1;
126        }
127        completed
128    }
129
130    /// Get demo metadata.
131    #[must_use]
132    pub fn meta(&self) -> &DemoMeta {
133        self.engine.meta()
134    }
135
136    /// Get current state.
137    #[must_use]
138    pub fn state(&self) -> E::State {
139        self.engine.state()
140    }
141
142    /// Evaluate falsification criteria.
143    #[must_use]
144    pub fn evaluate_criteria(&self) -> Vec<CriterionResult> {
145        self.engine.evaluate_criteria()
146    }
147
148    /// Get step count.
149    #[must_use]
150    pub fn step_count(&self) -> u64 {
151        self.step_count
152    }
153
154    /// Get seed for reproducibility.
155    #[must_use]
156    pub fn seed(&self) -> u64 {
157        self.engine.seed()
158    }
159
160    /// Check if complete.
161    #[must_use]
162    pub fn is_complete(&self) -> bool {
163        self.engine.is_complete()
164    }
165}
166
167/// JSON-serializable state for cross-boundary transfer.
168///
169/// Used to send state from Rust to JavaScript.
170#[derive(Debug, Clone, Serialize)]
171pub struct WasmState {
172    /// Demo ID.
173    pub id: String,
174    /// Demo type (e.g., "orbit", "tsp").
175    pub demo_type: String,
176    /// Current step number.
177    pub step: u64,
178    /// Seed for reproducibility.
179    pub seed: u64,
180    /// Whether demo is paused.
181    pub paused: bool,
182    /// Whether demo is complete.
183    pub complete: bool,
184    /// Engine-specific state as JSON.
185    pub state_json: String,
186    /// Falsification criteria results.
187    pub criteria: Vec<CriterionResultJson>,
188}
189
190/// JSON-serializable criterion result.
191#[derive(Debug, Clone, Serialize)]
192pub struct CriterionResultJson {
193    /// Criterion ID.
194    pub id: String,
195    /// Whether criterion passed.
196    pub passed: bool,
197    /// Actual value.
198    pub actual: f64,
199    /// Expected threshold.
200    pub expected: f64,
201    /// Human-readable message.
202    pub message: String,
203    /// Severity level.
204    pub severity: String,
205}
206
207impl From<&CriterionResult> for CriterionResultJson {
208    fn from(result: &CriterionResult) -> Self {
209        Self {
210            id: result.id.clone(),
211            passed: result.passed,
212            actual: result.actual,
213            expected: result.expected,
214            message: result.message.clone(),
215            severity: format!("{:?}", result.severity),
216        }
217    }
218}
219
220impl<E: DemoEngine> WasmRunner<E>
221where
222    E::State: Serialize,
223{
224    /// Get state as JSON string for JavaScript consumption.
225    #[must_use]
226    pub fn state_json(&self) -> String {
227        serde_json::to_string(&self.engine.state()).unwrap_or_else(|_| "{}".to_string())
228    }
229
230    /// Get full WASM state for JavaScript.
231    #[must_use]
232    pub fn wasm_state(&self) -> WasmState {
233        let meta = self.engine.meta();
234        let criteria = self
235            .engine
236            .evaluate_criteria()
237            .iter()
238            .map(CriterionResultJson::from)
239            .collect();
240
241        WasmState {
242            id: meta.id.clone(),
243            demo_type: meta.demo_type.clone(),
244            step: self.step_count,
245            seed: self.engine.seed(),
246            paused: self.paused,
247            complete: self.engine.is_complete(),
248            state_json: self.state_json(),
249            criteria,
250        }
251    }
252
253    /// Get full state as JSON string.
254    #[must_use]
255    pub fn wasm_state_json(&self) -> String {
256        serde_json::to_string(&self.wasm_state()).unwrap_or_else(|_| "{}".to_string())
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::demos::OrbitalEngine;
264
265    const TEST_YAML: &str = r#"
266simulation:
267  type: orbit
268  name: "Test Orbit"
269
270meta:
271  id: "TEST-001"
272  version: "1.0.0"
273  demo_type: orbit
274
275reproducibility:
276  seed: 42
277
278scenario:
279  type: kepler
280  central_body:
281    name: "Sun"
282    mass_kg: 1.989e30
283    position: [0.0, 0.0, 0.0]
284  orbiter:
285    name: "Earth"
286    mass_kg: 5.972e24
287    semi_major_axis_m: 1.496e11
288    eccentricity: 0.0167
289
290integrator:
291  type: stormer_verlet
292  dt_seconds: 3600.0
293"#;
294
295    #[test]
296    fn test_wasm_runner_creation() {
297        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
298        let runner = WasmRunner::new(engine);
299
300        assert!(runner.is_running());
301        assert!(!runner.is_paused());
302        assert_eq!(runner.step_count(), 0);
303    }
304
305    #[test]
306    fn test_wasm_runner_from_yaml() {
307        let runner = WasmRunner::<OrbitalEngine>::from_yaml(TEST_YAML).unwrap();
308        assert_eq!(runner.meta().id, "TEST-001");
309    }
310
311    #[test]
312    fn test_wasm_runner_step() {
313        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
314        let mut runner = WasmRunner::new(engine);
315
316        assert!(runner.step());
317        assert_eq!(runner.step_count(), 1);
318
319        assert!(runner.step());
320        assert_eq!(runner.step_count(), 2);
321    }
322
323    #[test]
324    fn test_wasm_runner_run_steps() {
325        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
326        let mut runner = WasmRunner::new(engine);
327
328        let completed = runner.run_steps(10);
329        assert_eq!(completed, 10);
330        assert_eq!(runner.step_count(), 10);
331    }
332
333    #[test]
334    fn test_wasm_runner_pause() {
335        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
336        let mut runner = WasmRunner::new(engine);
337
338        assert!(!runner.is_paused());
339        runner.toggle_pause();
340        assert!(runner.is_paused());
341
342        // Step should fail when paused
343        assert!(!runner.step());
344        assert_eq!(runner.step_count(), 0);
345
346        runner.resume();
347        assert!(!runner.is_paused());
348        assert!(runner.step());
349    }
350
351    #[test]
352    fn test_wasm_runner_reset() {
353        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
354        let mut runner = WasmRunner::new(engine);
355
356        runner.run_steps(5);
357        assert_eq!(runner.step_count(), 5);
358
359        runner.reset();
360        assert_eq!(runner.step_count(), 0);
361        assert!(!runner.is_paused());
362    }
363
364    #[test]
365    fn test_wasm_runner_state_json() {
366        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
367        let runner = WasmRunner::new(engine);
368
369        let json = runner.state_json();
370        assert!(json.contains("position"));
371        assert!(json.contains("velocity"));
372    }
373
374    #[test]
375    fn test_wasm_runner_wasm_state() {
376        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
377        let mut runner = WasmRunner::new(engine);
378
379        runner.step();
380        let state = runner.wasm_state();
381
382        assert_eq!(state.id, "TEST-001");
383        assert_eq!(state.demo_type, "orbit");
384        assert_eq!(state.step, 1);
385        assert_eq!(state.seed, 42);
386        assert!(!state.paused);
387        assert!(!state.complete);
388        assert!(!state.criteria.is_empty());
389    }
390
391    #[test]
392    fn test_wasm_runner_wasm_state_json() {
393        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
394        let runner = WasmRunner::new(engine);
395
396        let json = runner.wasm_state_json();
397        assert!(json.contains("TEST-001"));
398        assert!(json.contains("orbit"));
399    }
400
401    #[test]
402    fn test_wasm_runner_stop() {
403        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
404        let mut runner = WasmRunner::new(engine);
405
406        assert!(runner.is_running());
407        runner.stop();
408        assert!(!runner.is_running());
409    }
410
411    #[test]
412    fn test_wasm_runner_set_paused() {
413        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
414        let mut runner = WasmRunner::new(engine);
415
416        runner.set_paused(true);
417        assert!(runner.is_paused());
418        runner.set_paused(false);
419        assert!(!runner.is_paused());
420    }
421
422    #[test]
423    fn test_wasm_runner_criteria() {
424        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
425        let runner = WasmRunner::new(engine);
426
427        let criteria = runner.evaluate_criteria();
428        assert!(!criteria.is_empty());
429    }
430
431    #[test]
432    fn test_wasm_runner_seed() {
433        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
434        let runner = WasmRunner::new(engine);
435
436        assert_eq!(runner.seed(), 42);
437    }
438
439    #[test]
440    fn test_wasm_runner_is_complete() {
441        let engine = OrbitalEngine::from_yaml(TEST_YAML).unwrap();
442        let runner = WasmRunner::new(engine);
443
444        assert!(!runner.is_complete());
445    }
446}