Skip to main content

rydit_science/
geometry.rs

1//! Módulo de Geometría - Ilusiones Ópticas
2//!
3//! Implementa ilusiones ópticas clásicas usando matemáticas simples:
4//! - Triángulo de Penrose (tribar)
5//! - Cubo imposible (Necker cube)
6//! - Espiral óptica
7
8use serde_json::{Value, json};
9
10/// Genera las coordenadas para el Triángulo de Penrose
11/// 
12/// # Parámetros
13/// - `center_x`: Centro X en pantalla
14/// - `center_y`: Centro Y en pantalla  
15/// - `size`: Tamaño del triángulo
16/// 
17/// # Retorna
18/// Array de líneas: [[x1, y1, x2, y2], ...]
19pub fn penrose(center_x: f64, center_y: f64, size: f64) -> Value {
20    let s = size;
21    
22    // Coordenadas de los 3 vértices principales
23    let v1_x = center_x;
24    let v1_y = center_y - s * 0.577; // cos(30°)
25    
26    let v2_x = center_x - s * 0.5;
27    let v2_y = center_y + s * 0.289;
28    
29    let v3_x = center_x + s * 0.5;
30    let v3_y = center_y + s * 0.289;
31    
32    // Grosor de las barras
33    let thick = s * 0.15;
34    
35    // Barras del triángulo imposible
36    // Cada barra tiene 2 líneas paralelas para dar grosor
37    let mut lines = Vec::new();
38    
39    // Barra 1: v1 -> v2 (con "quiebre" imposible)
40    lines.push(json!([v1_x - thick * 0.3, v1_y - thick * 0.5, v2_x + thick * 0.5, v2_y - thick * 0.3]));
41    lines.push(json!([v1_x + thick * 0.3, v1_y - thick * 0.5, v2_x - thick * 0.5, v2_y + thick * 0.3]));
42    
43    // Barra 2: v2 -> v3
44    lines.push(json!([v2_x + thick * 0.5, v2_y - thick * 0.3, v3_x - thick * 0.5, v3_y - thick * 0.3]));
45    lines.push(json!([v2_x + thick * 0.5, v2_y + thick * 0.3, v3_x - thick * 0.5, v3_y + thick * 0.3]));
46    
47    // Barra 3: v3 -> v1 (con "quiebre" imposible)
48    lines.push(json!([v3_x + thick * 0.3, v3_y - thick * 0.5, v1_x + thick * 0.3, v1_y + thick * 0.5]));
49    lines.push(json!([v3_x - thick * 0.3, v3_y + thick * 0.5, v1_x - thick * 0.3, v1_y + thick * 0.5]));
50    
51    // Líneas de conexión "imposibles" en las esquinas
52    // Esquina v1
53    lines.push(json!([v1_x - thick * 0.3, v1_y - thick * 0.5, v1_x - thick * 0.3, v1_y + thick * 0.5]));
54    lines.push(json!([v1_x + thick * 0.3, v1_y - thick * 0.5, v1_x + thick * 0.3, v1_y + thick * 0.5]));
55    
56    // Esquina v2
57    lines.push(json!([v2_x - thick * 0.5, v2_y - thick * 0.3, v2_x - thick * 0.5, v2_y + thick * 0.3]));
58    lines.push(json!([v2_x + thick * 0.5, v2_y - thick * 0.3, v2_x + thick * 0.5, v2_y + thick * 0.3]));
59    
60    // Esquina v3
61    lines.push(json!([v3_x - thick * 0.5, v3_y - thick * 0.3, v3_x - thick * 0.5, v3_y + thick * 0.3]));
62    lines.push(json!([v3_x + thick * 0.5, v3_y - thick * 0.3, v3_x + thick * 0.5, v3_y + thick * 0.3]));
63    
64    json!(lines)
65}
66
67/// Genera las coordenadas para el Cubo Imposible
68/// 
69/// # Parámetros
70/// - `center_x`: Centro X en pantalla
71/// - `center_y`: Centro Y en pantalla
72/// - `size`: Tamaño del cubo
73/// 
74/// # Retorna
75/// Array de líneas: [[x1, y1, x2, y2], ...]
76pub fn impossible_cube(center_x: f64, center_y: f64, size: f64) -> Value {
77    let s = size * 0.5;
78    
79    // Cubo frontal
80    let f_bl_x = center_x - s; // front bottom-left
81    let f_bl_y = center_y + s;
82    let f_br_x = center_x + s; // front bottom-right
83    let f_br_y = center_y + s;
84    let f_tl_x = center_x - s; // front top-left
85    let f_tl_y = center_y - s;
86    let f_tr_x = center_x + s; // front top-right
87    let f_tr_y = center_y - s;
88    
89    // Cubo trasero (desplazado)
90    let offset = s * 0.6;
91    let b_bl_x = center_x - s + offset; // back bottom-left
92    let b_bl_y = center_y + s - offset;
93    let b_br_x = center_x + s + offset; // back bottom-right
94    let b_br_y = center_y + s - offset;
95    let b_tl_x = center_x - s + offset; // back top-left
96    let b_tl_y = center_y - s - offset;
97    let b_tr_x = center_x + s + offset; // back top-right
98    let b_tr_y = center_y - s - offset;
99    
100    let mut lines = Vec::new();
101    
102    // Cara frontal
103    lines.push(json!([f_bl_x, f_bl_y, f_br_x, f_br_y])); // bottom
104    lines.push(json!([f_br_x, f_br_y, f_tr_x, f_tr_y])); // right
105    lines.push(json!([f_tr_x, f_tr_y, f_tl_x, f_tl_y])); // top
106    lines.push(json!([f_tl_x, f_tl_y, f_bl_x, f_bl_y])); // left
107    
108    // Cara trasera
109    lines.push(json!([b_bl_x, b_bl_y, b_br_x, b_br_y])); // bottom
110    lines.push(json!([b_br_x, b_br_y, b_tr_x, b_tr_y])); // right
111    lines.push(json!([b_tr_x, b_tr_y, b_tl_x, b_tl_y])); // top
112    lines.push(json!([b_tl_x, b_tl_y, b_bl_x, b_bl_y])); // left
113    
114    // Conexiones frontal-trasera (algunas "imposibles")
115    lines.push(json!([f_bl_x, f_bl_y, b_bl_x, b_bl_y]));
116    lines.push(json!([f_br_x, f_br_y, b_br_x, b_br_y]));
117    lines.push(json!([f_tl_x, f_tl_y, b_tl_x, b_tl_y]));
118    lines.push(json!([f_tr_x, f_tr_y, b_tr_x, b_tr_y]));
119    
120    // Líneas adicionales para efecto imposible
121    // Conexión cruzada que crea la imposibilidad
122    lines.push(json!([f_bl_x + s * 0.3, f_bl_y, b_bl_x - s * 0.3, b_bl_y]));
123    lines.push(json!([f_tr_x - s * 0.3, f_tr_y, b_tr_x + s * 0.3, b_tr_y]));
124    
125    json!(lines)
126}
127
128/// Genera las coordenadas para la Espiral Óptica
129/// 
130/// # Parámetros
131/// - `center_x`: Centro X en pantalla
132/// - `center_y`: Centro Y en pantalla
133/// - `turns`: Número de vueltas
134/// - `radius`: Radio máximo
135/// - `points`: Puntos por vuelta
136/// 
137/// # Retorna
138/// Array de puntos: [[x1, y1], [x2, y2], ...]
139pub fn spiral(center_x: f64, center_y: f64, turns: i32, radius: f64, points: i32) -> Value {
140    let mut points_arr = Vec::new();
141    let total_points = turns * points;
142    
143    for i in 0..total_points {
144        let t = (i as f64) / (total_points as f64); // 0.0 a 1.0
145        let angle = t * turns as f64 * 2.0 * std::f64::consts::PI;
146        let r = t * radius;
147        
148        let x = center_x + r * angle.cos();
149        let y = center_y + r * angle.sin();
150        
151        points_arr.push(json!([x, y]));
152    }
153    
154    json!(points_arr)
155}
156
157/// Genera la ilusión de Müller-Lyer
158/// 
159/// # Parámetros
160/// - `center_x`: Centro X en pantalla
161/// - `center_y`: Centro Y en pantalla
162/// - `length`: Longitud de la línea principal
163/// 
164/// # Retorna
165/// Array de líneas: [[x1, y1, x2, y2], ...]
166pub fn muller_lyer(center_x: f64, center_y: f64, length: f64) -> Value {
167    let half = length / 2.0;
168    let arrow_size = length * 0.15;
169    
170    let mut lines = Vec::new();
171    
172    // Línea 1: Flechas hacia adentro (>)
173    let y1 = center_y - length * 0.3;
174    lines.push(json!([center_x - half, y1, center_x + half, y1])); // línea principal
175    
176    // Flecha izquierda adentro
177    lines.push(json!([center_x - half, y1, center_x - half + arrow_size, y1 - arrow_size * 0.6]));
178    lines.push(json!([center_x - half, y1, center_x - half + arrow_size, y1 + arrow_size * 0.6]));
179    
180    // Flecha derecha adentro
181    lines.push(json!([center_x + half, y1, center_x + half - arrow_size, y1 - arrow_size * 0.6]));
182    lines.push(json!([center_x + half, y1, center_x + half - arrow_size, y1 + arrow_size * 0.6]));
183    
184    // Línea 2: Flechas hacia afuera (<)
185    let y2 = center_y + length * 0.3;
186    lines.push(json!([center_x - half, y2, center_x + half, y2])); // línea principal
187    
188    // Flecha izquierda afuera
189    lines.push(json!([center_x - half, y2, center_x - half - arrow_size, y2 - arrow_size * 0.6]));
190    lines.push(json!([center_x - half, y2, center_x - half - arrow_size, y2 + arrow_size * 0.6]));
191    
192    // Flecha derecha afuera
193    lines.push(json!([center_x + half, y2, center_x + half + arrow_size, y2 - arrow_size * 0.6]));
194    lines.push(json!([center_x + half, y2, center_x + half + arrow_size, y2 + arrow_size * 0.6]));
195    
196    json!(lines)
197}
198
199/// Genera la ilusión de Ponzo (perspectiva)
200/// 
201/// # Parámetros
202/// - `center_x`: Centro X en pantalla
203/// - `center_y`: Centro Y en pantalla
204/// - `height`: Altura de la perspectiva
205/// - `width_top`: Ancho superior
206/// - `width_bottom`: Ancho inferior
207///
208/// # Retorna
209/// Array de líneas: [[x1, y1, x2, y2], ...]
210pub fn ponzo(center_x: f64, center_y: f64, height: f64, width_top: f64, width_bottom: f64) -> Value {
211    let mut lines = Vec::new();
212
213    // Líneas de perspectiva (rieles)
214    let top_y = center_y - height / 2.0;
215    let bottom_y = center_y + height / 2.0;
216
217    lines.push(json!([center_x - width_top / 2.0, top_y, center_x - width_bottom / 2.0, bottom_y]));
218    lines.push(json!([center_x + width_top / 2.0, top_y, center_x + width_bottom / 2.0, bottom_y]));
219
220    // Líneas horizontales (la de arriba parece más larga)
221    let top_line_width = width_top * 0.8;
222    let bottom_line_width = width_bottom * 0.8;
223
224    // Línea superior
225    lines.push(json!([center_x - top_line_width / 2.0, top_y + height * 0.2, center_x + top_line_width / 2.0, top_y + height * 0.2]));
226
227    // Línea inferior (misma longitud real, parece más corta)
228    lines.push(json!([center_x - bottom_line_width / 2.0, bottom_y - height * 0.2, center_x + bottom_line_width / 2.0, bottom_y - height * 0.2]));
229
230    // Líneas adicionales para reforzar la perspectiva
231    let mid_y = (top_y + bottom_y) / 2.0;
232    let mid_line_width = (width_top + width_bottom) * 0.4;
233    lines.push(json!([center_x - mid_line_width / 2.0, mid_y, center_x + mid_line_width / 2.0, mid_y]));
234    lines.push(json!([center_x - mid_line_width / 2.0 * 0.5, mid_y + height * 0.15, center_x + mid_line_width / 2.0 * 0.5, mid_y + height * 0.15]));
235
236    json!(lines)
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242
243    #[test]
244    fn test_penrose_returns_lines() {
245        let result = penrose(400.0, 300.0, 100.0);
246        let lines = result.as_array().unwrap();
247        
248        assert!(!lines.is_empty());
249        assert!(lines.len() >= 10); // Al menos 10 líneas
250        
251        // Cada línea es [x1, y1, x2, y2]
252        let first_line = lines[0].as_array().unwrap();
253        assert_eq!(first_line.len(), 4);
254    }
255
256    #[test]
257    fn test_impossible_cube_returns_lines() {
258        let result = impossible_cube(400.0, 300.0, 100.0);
259        let lines = result.as_array().unwrap();
260        
261        assert!(!lines.is_empty());
262        assert!(lines.len() >= 12); // Cubo tiene 12 aristas + extra
263        
264        let first_line = lines[0].as_array().unwrap();
265        assert_eq!(first_line.len(), 4);
266    }
267
268    #[test]
269    fn test_spiral_returns_points() {
270        let result = spiral(400.0, 300.0, 3, 100.0, 20);
271        let points = result.as_array().unwrap();
272        
273        assert_eq!(points.len(), 60); // 3 turns * 20 points
274        
275        let first_point = points[0].as_array().unwrap();
276        assert_eq!(first_point.len(), 2); // [x, y]
277    }
278
279    #[test]
280    fn test_muller_lyer_returns_lines() {
281        let result = muller_lyer(400.0, 300.0, 200.0);
282        let lines = result.as_array().unwrap();
283        
284        assert_eq!(lines.len(), 10); // 2 líneas principales + 8 flechas
285        
286        let first_line = lines[0].as_array().unwrap();
287        assert_eq!(first_line.len(), 4);
288    }
289
290    #[test]
291    fn test_ponzo_returns_lines() {
292        let result = ponzo(400.0, 300.0, 300.0, 100.0, 300.0);
293        let lines = result.as_array().unwrap();
294        
295        assert_eq!(lines.len(), 6); // 2 rieles + 4 horizontales
296        
297        let first_line = lines[0].as_array().unwrap();
298        assert_eq!(first_line.len(), 4);
299    }
300
301    #[test]
302    fn test_spiral_center_point() {
303        // El primer punto debe estar cerca del centro
304        let result = spiral(400.0, 300.0, 1, 100.0, 10);
305        let points = result.as_array().unwrap();
306        let first_point = points[0].as_array().unwrap();
307        
308        let x = first_point[0].as_f64().unwrap();
309        let y = first_point[1].as_f64().unwrap();
310        
311        assert!((x - 400.0).abs() < 1.0); // Cerca del centro
312        assert!((y - 300.0).abs() < 1.0);
313    }
314
315    #[test]
316    fn test_spiral_outer_point() {
317        // El último punto debe estar cerca del radio máximo
318        let result = spiral(400.0, 300.0, 1, 100.0, 10);
319        let points = result.as_array().unwrap();
320        let last_point = points[points.len() - 1].as_array().unwrap();
321        
322        let x = last_point[0].as_f64().unwrap();
323        let y = last_point[1].as_f64().unwrap();
324        
325        // Distancia desde el centro debería ser ~100 (radio máximo)
326        // Nota: como es espiral de 1 vuelta, el último punto está cerca del radio máximo
327        let dist = ((x - 400.0).powi(2) + (y - 300.0).powi(2)).sqrt();
328        assert!(dist > 80.0 && dist < 120.0); // Rango razonable para espiral de 1 vuelta
329    }
330}