Skip to main content

rydit_science/
lib.rs

1//! RyDit Science - Módulo de Ciencia para RyDit
2//!
3//! Proporciona funcionalidad de:
4//! - Curvas Bezier (lineal, cuadrática, cúbica)
5//! - Estadísticas (media, mediana, mínimo, máximo)
6//! - Geometría (ilusiones ópticas: Penrose, Cubo imposible, Espiral)
7
8pub mod geometry;
9
10use rydit_core::{RyditModule, ModuleResult, ModuleError};
11use serde_json::{Value, json};
12use std::collections::HashMap;
13
14/// Módulo de Ciencia - Bezier y Estadísticas
15pub struct ScienceModule;
16
17impl RyditModule for ScienceModule {
18    fn name(&self) -> &'static str {
19        "science"
20    }
21
22    fn version(&self) -> &'static str {
23        "0.7.3"
24    }
25
26    fn register(&self) -> HashMap<&'static str, &'static str> {
27        let mut cmds = HashMap::new();
28        cmds.insert("bezier::linear", "Curva Bezier lineal");
29        cmds.insert("bezier::quadratic", "Curva Bezier cuadrática");
30        cmds.insert("bezier::cubic", "Curva Bezier cúbica");
31        cmds.insert("stats::mean", "Media aritmética");
32        cmds.insert("stats::median", "Mediana");
33        cmds.insert("stats::min", "Valor mínimo");
34        cmds.insert("stats::max", "Valor máximo");
35        cmds.insert("geometry::penrose", "Triángulo de Penrose");
36        cmds.insert("geometry::impossible_cube", "Cubo imposible");
37        cmds.insert("geometry::spiral", "Espiral óptica");
38        cmds.insert("geometry::muller_lyer", "Ilusión Müller-Lyer");
39        cmds.insert("geometry::ponzo", "Ilusión de Ponzo");
40        cmds
41    }
42
43    fn execute(&self, command: &str, params: Value) -> ModuleResult {
44        match command {
45            "bezier::linear" => self.bezier_linear(params),
46            "bezier::quadratic" => self.bezier_quadratic(params),
47            "bezier::cubic" => self.bezier_cubic(params),
48            "stats::mean" => self.stats_mean(params),
49            "stats::median" => self.stats_median(params),
50            "stats::min" => self.stats_min(params),
51            "stats::max" => self.stats_max(params),
52            "geometry::penrose" => self.geometry_penrose(params),
53            "geometry::impossible_cube" => self.geometry_impossible_cube(params),
54            "geometry::spiral" => self.geometry_spiral(params),
55            "geometry::muller_lyer" => self.geometry_muller_lyer(params),
56            "geometry::ponzo" => self.geometry_ponzo(params),
57            _ => Err(ModuleError {
58                code: "UNKNOWN_COMMAND".to_string(),
59                message: format!("Comando desconocido: {}", command),
60            }),
61        }
62    }
63}
64
65impl ScienceModule {
66    /// Curva Bezier lineal: P(t) = (1-t)*P0 + t*P1
67    fn bezier_linear(&self, params: Value) -> ModuleResult {
68        let arr = params.as_array().ok_or_else(|| ModuleError {
69            code: "INVALID_PARAMS".to_string(),
70            message: "Params must be an array".to_string(),
71        })?;
72
73        if arr.len() != 5 {
74            return Err(ModuleError {
75                code: "INVALID_PARAMS".to_string(),
76                message: "bezier::linear requires 5 params: p0_x, p0_y, p1_x, p1_y, t".to_string(),
77            });
78        }
79
80        let p0_x = arr[0].as_f64().unwrap_or(0.0);
81        let p0_y = arr[1].as_f64().unwrap_or(0.0);
82        let p1_x = arr[2].as_f64().unwrap_or(0.0);
83        let p1_y = arr[3].as_f64().unwrap_or(0.0);
84        let t = arr[4].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
85
86        let x = (1.0 - t) * p0_x + t * p1_x;
87        let y = (1.0 - t) * p0_y + t * p1_y;
88
89        Ok(json!([x, y]))
90    }
91
92    /// Curva Bezier cuadrática: P(t) = (1-t)²*P0 + 2(1-t)t*P1 + t²*P2
93    fn bezier_quadratic(&self, params: Value) -> ModuleResult {
94        let arr = params.as_array().ok_or_else(|| ModuleError {
95            code: "INVALID_PARAMS".to_string(),
96            message: "Params must be an array".to_string(),
97        })?;
98
99        if arr.len() != 7 {
100            return Err(ModuleError {
101                code: "INVALID_PARAMS".to_string(),
102                message: "bezier::quadratic requires 7 params: p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, t".to_string(),
103            });
104        }
105
106        let p0_x = arr[0].as_f64().unwrap_or(0.0);
107        let p0_y = arr[1].as_f64().unwrap_or(0.0);
108        let p1_x = arr[2].as_f64().unwrap_or(0.0);
109        let p1_y = arr[3].as_f64().unwrap_or(0.0);
110        let p2_x = arr[4].as_f64().unwrap_or(0.0);
111        let p2_y = arr[5].as_f64().unwrap_or(0.0);
112        let t = arr[6].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
113
114        let mt = 1.0 - t;
115        let x = mt * mt * p0_x + 2.0 * mt * t * p1_x + t * t * p2_x;
116        let y = mt * mt * p0_y + 2.0 * mt * t * p1_y + t * t * p2_y;
117
118        Ok(json!([x, y]))
119    }
120
121    /// Curva Bezier cúbica: P(t) = (1-t)³*P0 + 3(1-t)²t*P1 + 3(1-t)t²*P2 + t³*P3
122    fn bezier_cubic(&self, params: Value) -> ModuleResult {
123        let arr = params.as_array().ok_or_else(|| ModuleError {
124            code: "INVALID_PARAMS".to_string(),
125            message: "Params must be an array".to_string(),
126        })?;
127
128        if arr.len() != 9 {
129            return Err(ModuleError {
130                code: "INVALID_PARAMS".to_string(),
131                message: "bezier::cubic requires 9 params: p0_x, p0_y, p1_x, p1_y, p2_x, p2_y, p3_x, p3_y, t".to_string(),
132            });
133        }
134
135        let p0_x = arr[0].as_f64().unwrap_or(0.0);
136        let p0_y = arr[1].as_f64().unwrap_or(0.0);
137        let p1_x = arr[2].as_f64().unwrap_or(0.0);
138        let p1_y = arr[3].as_f64().unwrap_or(0.0);
139        let p2_x = arr[4].as_f64().unwrap_or(0.0);
140        let p2_y = arr[5].as_f64().unwrap_or(0.0);
141        let p3_x = arr[6].as_f64().unwrap_or(0.0);
142        let p3_y = arr[7].as_f64().unwrap_or(0.0);
143        let t = arr[8].as_f64().unwrap_or(0.0).max(0.0).min(1.0);
144
145        let mt = 1.0 - t;
146        let mt2 = mt * mt;
147        let t2 = t * t;
148
149        let x = mt2 * mt * p0_x + 3.0 * mt2 * t * p1_x + 3.0 * mt * t2 * p2_x + t2 * t * p3_x;
150        let y = mt2 * mt * p0_y + 3.0 * mt2 * t * p1_y + 3.0 * mt * t2 * p2_y + t2 * t * p3_y;
151
152        Ok(json!([x, y]))
153    }
154
155    /// Media aritmética: sum / n
156    fn stats_mean(&self, params: Value) -> ModuleResult {
157        let arr = params.as_array().ok_or_else(|| ModuleError {
158            code: "INVALID_PARAMS".to_string(),
159            message: "Params must be an array".to_string(),
160        })?;
161
162        if arr.is_empty() {
163            return Err(ModuleError {
164                code: "INVALID_PARAMS".to_string(),
165                message: "Empty array".to_string(),
166            });
167        }
168
169        let sum: f64 = arr.iter().filter_map(|v| v.as_f64()).sum();
170        Ok(json!(sum / arr.len() as f64))
171    }
172
173    /// Mediana: valor central de array ordenado
174    fn stats_median(&self, params: Value) -> ModuleResult {
175        let arr = params.as_array().ok_or_else(|| ModuleError {
176            code: "INVALID_PARAMS".to_string(),
177            message: "Params must be an array".to_string(),
178        })?;
179
180        let mut nums: Vec<f64> = arr.iter().filter_map(|v| v.as_f64()).collect();
181
182        if nums.is_empty() {
183            return Err(ModuleError {
184                code: "INVALID_PARAMS".to_string(),
185                message: "Empty array or no numbers".to_string(),
186            });
187        }
188
189        nums.sort_by(|a, b| a.partial_cmp(b).unwrap());
190        let mid = nums.len() / 2;
191
192        let median = if nums.len() % 2 == 0 {
193            (nums[mid - 1] + nums[mid]) / 2.0
194        } else {
195            nums[mid]
196        };
197
198        Ok(json!(median))
199    }
200
201    /// Valor mínimo de un array
202    fn stats_min(&self, params: Value) -> ModuleResult {
203        let arr = params.as_array().ok_or_else(|| ModuleError {
204            code: "INVALID_PARAMS".to_string(),
205            message: "Params must be an array".to_string(),
206        })?;
207
208        let mut min_val = f64::MAX;
209        let mut found = false;
210
211        for v in arr {
212            if let Some(n) = v.as_f64() {
213                if n < min_val {
214                    min_val = n;
215                }
216                found = true;
217            }
218        }
219
220        if found {
221            Ok(json!(min_val))
222        } else {
223            Err(ModuleError {
224                code: "INVALID_PARAMS".to_string(),
225                message: "No numbers in array".to_string(),
226            })
227        }
228    }
229
230    /// Valor máximo de un array
231    fn stats_max(&self, params: Value) -> ModuleResult {
232        let arr = params.as_array().ok_or_else(|| ModuleError {
233            code: "INVALID_PARAMS".to_string(),
234            message: "Params must be an array".to_string(),
235        })?;
236
237        let mut max_val = f64::MIN;
238        let mut found = false;
239
240        for v in arr {
241            if let Some(n) = v.as_f64() {
242                if n > max_val {
243                    max_val = n;
244                }
245                found = true;
246            }
247        }
248
249        if found {
250            Ok(json!(max_val))
251        } else {
252            Err(ModuleError {
253                code: "INVALID_PARAMS".to_string(),
254                message: "No numbers in array".to_string(),
255            })
256        }
257    }
258
259    // ================================================================
260    // Funciones de Geometría - Ilusiones Ópticas
261    // ================================================================
262
263    /// Triángulo de Penrose (tribar imposible)
264    fn geometry_penrose(&self, params: Value) -> ModuleResult {
265        let arr = params.as_array().ok_or_else(|| ModuleError {
266            code: "INVALID_PARAMS".to_string(),
267            message: "Params must be an array".to_string(),
268        })?;
269
270        if arr.len() != 3 {
271            return Err(ModuleError {
272                code: "INVALID_PARAMS".to_string(),
273                message: "geometry::penrose requires 3 params: center_x, center_y, size".to_string(),
274            });
275        }
276
277        let center_x = arr[0].as_f64().unwrap_or(400.0);
278        let center_y = arr[1].as_f64().unwrap_or(300.0);
279        let size = arr[2].as_f64().unwrap_or(100.0);
280
281        Ok(geometry::penrose(center_x, center_y, size))
282    }
283
284    /// Cubo imposible (Necker cube)
285    fn geometry_impossible_cube(&self, params: Value) -> ModuleResult {
286        let arr = params.as_array().ok_or_else(|| ModuleError {
287            code: "INVALID_PARAMS".to_string(),
288            message: "Params must be an array".to_string(),
289        })?;
290
291        if arr.len() != 3 {
292            return Err(ModuleError {
293                code: "INVALID_PARAMS".to_string(),
294                message: "geometry::impossible_cube requires 3 params: center_x, center_y, size".to_string(),
295            });
296        }
297
298        let center_x = arr[0].as_f64().unwrap_or(400.0);
299        let center_y = arr[1].as_f64().unwrap_or(300.0);
300        let size = arr[2].as_f64().unwrap_or(100.0);
301
302        Ok(geometry::impossible_cube(center_x, center_y, size))
303    }
304
305    /// Espiral óptica (Arquímedes)
306    fn geometry_spiral(&self, params: Value) -> ModuleResult {
307        let arr = params.as_array().ok_or_else(|| ModuleError {
308            code: "INVALID_PARAMS".to_string(),
309            message: "Params must be an array".to_string(),
310        })?;
311
312        if arr.len() != 5 {
313            return Err(ModuleError {
314                code: "INVALID_PARAMS".to_string(),
315                message: "geometry::spiral requires 5 params: center_x, center_y, turns, radius, points".to_string(),
316            });
317        }
318
319        let center_x = arr[0].as_f64().unwrap_or(400.0);
320        let center_y = arr[1].as_f64().unwrap_or(300.0);
321        let turns = arr[2].as_i64().unwrap_or(3) as i32;
322        let radius = arr[3].as_f64().unwrap_or(100.0);
323        let points = arr[4].as_i64().unwrap_or(20) as i32;
324
325        Ok(geometry::spiral(center_x, center_y, turns, radius, points))
326    }
327
328    /// Ilusión de Müller-Lyer (flechas)
329    fn geometry_muller_lyer(&self, params: Value) -> ModuleResult {
330        let arr = params.as_array().ok_or_else(|| ModuleError {
331            code: "INVALID_PARAMS".to_string(),
332            message: "Params must be an array".to_string(),
333        })?;
334
335        if arr.len() != 3 {
336            return Err(ModuleError {
337                code: "INVALID_PARAMS".to_string(),
338                message: "geometry::muller_lyer requires 3 params: center_x, center_y, length".to_string(),
339            });
340        }
341
342        let center_x = arr[0].as_f64().unwrap_or(400.0);
343        let center_y = arr[1].as_f64().unwrap_or(300.0);
344        let length = arr[2].as_f64().unwrap_or(200.0);
345
346        Ok(geometry::muller_lyer(center_x, center_y, length))
347    }
348
349    /// Ilusión de Ponzo (perspectiva)
350    fn geometry_ponzo(&self, params: Value) -> ModuleResult {
351        let arr = params.as_array().ok_or_else(|| ModuleError {
352            code: "INVALID_PARAMS".to_string(),
353            message: "Params must be an array".to_string(),
354        })?;
355
356        if arr.len() != 5 {
357            return Err(ModuleError {
358                code: "INVALID_PARAMS".to_string(),
359                message: "geometry::ponzo requires 5 params: center_x, center_y, height, width_top, width_bottom".to_string(),
360            });
361        }
362
363        let center_x = arr[0].as_f64().unwrap_or(400.0);
364        let center_y = arr[1].as_f64().unwrap_or(300.0);
365        let height = arr[2].as_f64().unwrap_or(300.0);
366        let width_top = arr[3].as_f64().unwrap_or(100.0);
367        let width_bottom = arr[4].as_f64().unwrap_or(300.0);
368
369        Ok(geometry::ponzo(center_x, center_y, height, width_top, width_bottom))
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_science_module_name() {
379        let module = ScienceModule;
380        assert_eq!(module.name(), "science");
381        assert_eq!(module.version(), "0.7.3");
382    }
383
384    #[test]
385    fn test_science_register() {
386        let module = ScienceModule;
387        let cmds = module.register();
388
389        assert!(cmds.contains_key("bezier::linear"));
390        assert!(cmds.contains_key("bezier::cubic"));
391        assert!(cmds.contains_key("stats::mean"));
392        assert!(cmds.contains_key("stats::median"));
393        assert!(cmds.contains_key("geometry::penrose"));
394        assert!(cmds.contains_key("geometry::impossible_cube"));
395        assert!(cmds.contains_key("geometry::spiral"));
396    }
397
398    #[test]
399    fn test_bezier_linear() {
400        let module = ScienceModule;
401        let params = json!([0.0, 0.0, 100.0, 100.0, 0.5]);
402        let result = module.execute("bezier::linear", params).unwrap();
403        
404        assert_eq!(result, json!([50.0, 50.0]));
405    }
406
407    #[test]
408    fn test_bezier_cubic() {
409        let module = ScienceModule;
410        // p0=(0,0), p1=(30,100), p2=(70,100), p3=(100,0), t=0.5
411        let params = json!([0.0, 0.0, 30.0, 100.0, 70.0, 100.0, 100.0, 0.0, 0.5]);
412        let result = module.execute("bezier::cubic", params).unwrap();
413        
414        assert_eq!(result, json!([50.0, 75.0]));
415    }
416
417    #[test]
418    fn test_stats_mean() {
419        let module = ScienceModule;
420        let params = json!([1.0, 2.0, 3.0, 4.0, 5.0]);
421        let result = module.execute("stats::mean", params).unwrap();
422        
423        assert_eq!(result, json!(3.0));
424    }
425
426    #[test]
427    fn test_stats_median_odd() {
428        let module = ScienceModule;
429        let params = json!([1.0, 2.0, 3.0, 4.0, 5.0]);
430        let result = module.execute("stats::median", params).unwrap();
431        
432        assert_eq!(result, json!(3.0));
433    }
434
435    #[test]
436    fn test_stats_median_even() {
437        let module = ScienceModule;
438        let params = json!([1.0, 2.0, 3.0, 4.0]);
439        let result = module.execute("stats::median", params).unwrap();
440        
441        assert_eq!(result, json!(2.5));
442    }
443
444    #[test]
445    fn test_stats_min_max() {
446        let module = ScienceModule;
447        let params = json!([3.0, 1.0, 4.0, 1.0, 5.0]);
448        
449        let min_result = module.execute("stats::min", params.clone()).unwrap();
450        assert_eq!(min_result, json!(1.0));
451        
452        let max_result = module.execute("stats::max", params).unwrap();
453        assert_eq!(max_result, json!(5.0));
454    }
455
456    #[test]
457    fn test_unknown_command() {
458        let module = ScienceModule;
459        let result = module.execute("unknown", json!([]));
460
461        assert!(result.is_err());
462        let err = result.unwrap_err();
463        assert_eq!(err.code, "UNKNOWN_COMMAND");
464    }
465
466    // Tests de Geometría
467    #[test]
468    fn test_geometry_penrose() {
469        let module = ScienceModule;
470        let params = json!([400.0, 300.0, 100.0]);
471        let result = module.execute("geometry::penrose", params).unwrap();
472
473        let lines = result.as_array().unwrap();
474        assert!(!lines.is_empty());
475        assert!(lines.len() >= 10);
476    }
477
478    #[test]
479    fn test_geometry_impossible_cube() {
480        let module = ScienceModule;
481        let params = json!([400.0, 300.0, 100.0]);
482        let result = module.execute("geometry::impossible_cube", params).unwrap();
483
484        let lines = result.as_array().unwrap();
485        assert!(!lines.is_empty());
486        assert!(lines.len() >= 12);
487    }
488
489    #[test]
490    fn test_geometry_spiral() {
491        let module = ScienceModule;
492        let params = json!([400.0, 300.0, 3, 100.0, 20]);
493        let result = module.execute("geometry::spiral", params).unwrap();
494
495        let points = result.as_array().unwrap();
496        assert_eq!(points.len(), 60); // 3 turns * 20 points
497    }
498
499    #[test]
500    fn test_geometry_muller_lyer() {
501        let module = ScienceModule;
502        let params = json!([400.0, 300.0, 200.0]);
503        let result = module.execute("geometry::muller_lyer", params).unwrap();
504
505        let lines = result.as_array().unwrap();
506        assert_eq!(lines.len(), 10);
507    }
508
509    #[test]
510    fn test_geometry_ponzo() {
511        let module = ScienceModule;
512        let params = json!([400.0, 300.0, 300.0, 100.0, 300.0]);
513        let result = module.execute("geometry::ponzo", params).unwrap();
514
515        let lines = result.as_array().unwrap();
516        assert_eq!(lines.len(), 6); // 2 rieles + 4 horizontales
517    }
518}