Skip to main content

ry_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::{json, Value};
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 lines = vec![
38        // Barra 1: v1 -> v2 (con "quiebre" imposible)
39        json!([
40            v1_x - thick * 0.3,
41            v1_y - thick * 0.5,
42            v2_x + thick * 0.5,
43            v2_y - thick * 0.3,
44        ]),
45        json!([
46            v1_x + thick * 0.3,
47            v1_y - thick * 0.5,
48            v2_x - thick * 0.5,
49            v2_y + thick * 0.3,
50        ]),
51        // Barra 2: v2 -> v3
52        json!([
53            v2_x + thick * 0.5,
54            v2_y - thick * 0.3,
55            v3_x - thick * 0.5,
56            v3_y - thick * 0.3,
57        ]),
58        json!([
59            v2_x + thick * 0.5,
60            v2_y + thick * 0.3,
61            v3_x - thick * 0.5,
62            v3_y + thick * 0.3,
63        ]),
64        // Barra 3: v3 -> v1 (con "quiebre" imposible)
65        json!([
66            v3_x + thick * 0.3,
67            v3_y - thick * 0.5,
68            v1_x + thick * 0.3,
69            v1_y + thick * 0.5,
70        ]),
71        json!([
72            v3_x - thick * 0.3,
73            v3_y + thick * 0.5,
74            v1_x - thick * 0.3,
75            v1_y + thick * 0.5,
76        ]),
77        // Líneas de conexión "imposibles" en las esquinas
78        // Esquina v1
79        json!([
80            v1_x - thick * 0.3,
81            v1_y - thick * 0.5,
82            v1_x - thick * 0.3,
83            v1_y + thick * 0.5,
84        ]),
85        json!([
86            v1_x + thick * 0.3,
87            v1_y - thick * 0.5,
88            v1_x + thick * 0.3,
89            v1_y + thick * 0.5,
90        ]),
91        // Esquina v2
92        json!([
93            v2_x - thick * 0.5,
94            v2_y - thick * 0.3,
95            v2_x - thick * 0.5,
96            v2_y + thick * 0.3,
97        ]),
98        json!([
99            v2_x + thick * 0.5,
100            v2_y - thick * 0.3,
101            v2_x + thick * 0.5,
102            v2_y + thick * 0.3,
103        ]),
104        // Esquina v3
105        json!([
106            v3_x - thick * 0.5,
107            v3_y - thick * 0.3,
108            v3_x - thick * 0.5,
109            v3_y + thick * 0.3,
110        ]),
111        json!([
112            v3_x + thick * 0.5,
113            v3_y - thick * 0.3,
114            v3_x + thick * 0.5,
115            v3_y + thick * 0.3,
116        ]),
117    ];
118
119    json!(lines)
120}
121
122/// Genera las coordenadas para el Cubo Imposible
123///
124/// # Parámetros
125/// - `center_x`: Centro X en pantalla
126/// - `center_y`: Centro Y en pantalla
127/// - `size`: Tamaño del cubo
128///
129/// # Retorna
130/// Array de líneas: [[x1, y1, x2, y2], ...]
131pub fn impossible_cube(center_x: f64, center_y: f64, size: f64) -> Value {
132    let s = size * 0.5;
133
134    // Cubo frontal
135    let f_bl_x = center_x - s; // front bottom-left
136    let f_bl_y = center_y + s;
137    let f_br_x = center_x + s; // front bottom-right
138    let f_br_y = center_y + s;
139    let f_tl_x = center_x - s; // front top-left
140    let f_tl_y = center_y - s;
141    let f_tr_x = center_x + s; // front top-right
142    let f_tr_y = center_y - s;
143
144    // Cubo trasero (desplazado)
145    let offset = s * 0.6;
146    let b_bl_x = center_x - s + offset; // back bottom-left
147    let b_bl_y = center_y + s - offset;
148    let b_br_x = center_x + s + offset; // back bottom-right
149    let b_br_y = center_y + s - offset;
150    let b_tl_x = center_x - s + offset; // back top-left
151    let b_tl_y = center_y - s - offset;
152    let b_tr_x = center_x + s + offset; // back top-right
153    let b_tr_y = center_y - s - offset;
154
155    let lines = vec![
156        // Cara frontal
157        json!([f_bl_x, f_bl_y, f_br_x, f_br_y]), // bottom
158        json!([f_br_x, f_br_y, f_tr_x, f_tr_y]), // right
159        json!([f_tr_x, f_tr_y, f_tl_x, f_tl_y]), // top
160        json!([f_tl_x, f_tl_y, f_bl_x, f_bl_y]), // left
161        // Cara trasera
162        json!([b_bl_x, b_bl_y, b_br_x, b_br_y]), // bottom
163        json!([b_br_x, b_br_y, b_tr_x, b_tr_y]), // right
164        json!([b_tr_x, b_tr_y, b_tl_x, b_tl_y]), // top
165        json!([b_tl_x, b_tl_y, b_bl_x, b_bl_y]), // left
166        // Conexiones frontal-trasera (algunas "imposibles")
167        json!([f_bl_x, f_bl_y, b_bl_x, b_bl_y]),
168        json!([f_br_x, f_br_y, b_br_x, b_br_y]),
169        json!([f_tl_x, f_tl_y, b_tl_x, b_tl_y]),
170        json!([f_tr_x, f_tr_y, b_tr_x, b_tr_y]),
171        // Líneas adicionales para efecto imposible
172        json!([f_bl_x + s * 0.3, f_bl_y, b_bl_x - s * 0.3, b_bl_y]),
173        json!([f_tr_x - s * 0.3, f_tr_y, b_tr_x + s * 0.3, b_tr_y]),
174    ];
175
176    json!(lines)
177}
178
179/// Genera las coordenadas para la Espiral Óptica
180///
181/// # Parámetros
182/// - `center_x`: Centro X en pantalla
183/// - `center_y`: Centro Y en pantalla
184/// - `turns`: Número de vueltas
185/// - `radius`: Radio máximo
186/// - `points`: Puntos por vuelta
187///
188/// # Retorna
189/// Array de puntos: [[x1, y1], [x2, y2], ...]
190pub fn spiral(center_x: f64, center_y: f64, turns: i32, radius: f64, points: i32) -> Value {
191    let mut points_arr = Vec::new();
192    let total_points = turns * points;
193
194    for i in 0..total_points {
195        let t = (i as f64) / (total_points as f64); // 0.0 a 1.0
196        let angle = t * turns as f64 * 2.0 * std::f64::consts::PI;
197        let r = t * radius;
198
199        let x = center_x + r * angle.cos();
200        let y = center_y + r * angle.sin();
201
202        points_arr.push(json!([x, y]));
203    }
204
205    json!(points_arr)
206}
207
208/// Genera la ilusión de Müller-Lyer
209///
210/// # Parámetros
211/// - `center_x`: Centro X en pantalla
212/// - `center_y`: Centro Y en pantalla
213/// - `length`: Longitud de la línea principal
214///
215/// # Retorna
216/// Array de líneas: [[x1, y1, x2, y2], ...]
217pub fn muller_lyer(center_x: f64, center_y: f64, length: f64) -> Value {
218    let half = length / 2.0;
219    let arrow_size = length * 0.15;
220
221    let mut lines = Vec::new();
222
223    // Línea 1: Flechas hacia adentro (>)
224    let y1 = center_y - length * 0.3;
225    lines.push(json!([center_x - half, y1, center_x + half, y1])); // línea principal
226
227    // Flecha izquierda adentro
228    lines.push(json!([
229        center_x - half,
230        y1,
231        center_x - half + arrow_size,
232        y1 - arrow_size * 0.6
233    ]));
234    lines.push(json!([
235        center_x - half,
236        y1,
237        center_x - half + arrow_size,
238        y1 + arrow_size * 0.6
239    ]));
240
241    // Flecha derecha adentro
242    lines.push(json!([
243        center_x + half,
244        y1,
245        center_x + half - arrow_size,
246        y1 - arrow_size * 0.6
247    ]));
248    lines.push(json!([
249        center_x + half,
250        y1,
251        center_x + half - arrow_size,
252        y1 + arrow_size * 0.6
253    ]));
254
255    // Línea 2: Flechas hacia afuera (<)
256    let y2 = center_y + length * 0.3;
257    lines.push(json!([center_x - half, y2, center_x + half, y2])); // línea principal
258
259    // Flecha izquierda afuera
260    lines.push(json!([
261        center_x - half,
262        y2,
263        center_x - half - arrow_size,
264        y2 - arrow_size * 0.6
265    ]));
266    lines.push(json!([
267        center_x - half,
268        y2,
269        center_x - half - arrow_size,
270        y2 + arrow_size * 0.6
271    ]));
272
273    // Flecha derecha afuera
274    lines.push(json!([
275        center_x + half,
276        y2,
277        center_x + half + arrow_size,
278        y2 - arrow_size * 0.6
279    ]));
280    lines.push(json!([
281        center_x + half,
282        y2,
283        center_x + half + arrow_size,
284        y2 + arrow_size * 0.6
285    ]));
286
287    json!(lines)
288}
289
290/// Genera la ilusión de Ponzo (perspectiva)
291///
292/// # Parámetros
293/// - `center_x`: Centro X en pantalla
294/// - `center_y`: Centro Y en pantalla
295/// - `height`: Altura de la perspectiva
296/// - `width_top`: Ancho superior
297/// - `width_bottom`: Ancho inferior
298///
299/// # Retorna
300/// Array de líneas: [[x1, y1, x2, y2], ...]
301pub fn ponzo(
302    center_x: f64,
303    center_y: f64,
304    height: f64,
305    width_top: f64,
306    width_bottom: f64,
307) -> Value {
308    let mut lines = Vec::new();
309
310    // Líneas de perspectiva (rieles)
311    let top_y = center_y - height / 2.0;
312    let bottom_y = center_y + height / 2.0;
313
314    lines.push(json!([
315        center_x - width_top / 2.0,
316        top_y,
317        center_x - width_bottom / 2.0,
318        bottom_y
319    ]));
320    lines.push(json!([
321        center_x + width_top / 2.0,
322        top_y,
323        center_x + width_bottom / 2.0,
324        bottom_y
325    ]));
326
327    // Líneas horizontales (la de arriba parece más larga)
328    let top_line_width = width_top * 0.8;
329    let bottom_line_width = width_bottom * 0.8;
330
331    // Línea superior
332    lines.push(json!([
333        center_x - top_line_width / 2.0,
334        top_y + height * 0.2,
335        center_x + top_line_width / 2.0,
336        top_y + height * 0.2
337    ]));
338
339    // Línea inferior (misma longitud real, parece más corta)
340    lines.push(json!([
341        center_x - bottom_line_width / 2.0,
342        bottom_y - height * 0.2,
343        center_x + bottom_line_width / 2.0,
344        bottom_y - height * 0.2
345    ]));
346
347    // Líneas adicionales para reforzar la perspectiva
348    let mid_y = (top_y + bottom_y) / 2.0;
349    let mid_line_width = (width_top + width_bottom) * 0.4;
350    lines.push(json!([
351        center_x - mid_line_width / 2.0,
352        mid_y,
353        center_x + mid_line_width / 2.0,
354        mid_y
355    ]));
356    lines.push(json!([
357        center_x - mid_line_width / 2.0 * 0.5,
358        mid_y + height * 0.15,
359        center_x + mid_line_width / 2.0 * 0.5,
360        mid_y + height * 0.15
361    ]));
362
363    json!(lines)
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_penrose_returns_lines() {
372        let result = penrose(400.0, 300.0, 100.0);
373        let lines = result.as_array().unwrap();
374
375        assert!(!lines.is_empty());
376        assert!(lines.len() >= 10); // Al menos 10 líneas
377
378        // Cada línea es [x1, y1, x2, y2]
379        let first_line = lines[0].as_array().unwrap();
380        assert_eq!(first_line.len(), 4);
381    }
382
383    #[test]
384    fn test_impossible_cube_returns_lines() {
385        let result = impossible_cube(400.0, 300.0, 100.0);
386        let lines = result.as_array().unwrap();
387
388        assert!(!lines.is_empty());
389        assert!(lines.len() >= 12); // Cubo tiene 12 aristas + extra
390
391        let first_line = lines[0].as_array().unwrap();
392        assert_eq!(first_line.len(), 4);
393    }
394
395    #[test]
396    fn test_spiral_returns_points() {
397        let result = spiral(400.0, 300.0, 3, 100.0, 20);
398        let points = result.as_array().unwrap();
399
400        assert_eq!(points.len(), 60); // 3 turns * 20 points
401
402        let first_point = points[0].as_array().unwrap();
403        assert_eq!(first_point.len(), 2); // [x, y]
404    }
405
406    #[test]
407    fn test_muller_lyer_returns_lines() {
408        let result = muller_lyer(400.0, 300.0, 200.0);
409        let lines = result.as_array().unwrap();
410
411        assert_eq!(lines.len(), 10); // 2 líneas principales + 8 flechas
412
413        let first_line = lines[0].as_array().unwrap();
414        assert_eq!(first_line.len(), 4);
415    }
416
417    #[test]
418    fn test_ponzo_returns_lines() {
419        let result = ponzo(400.0, 300.0, 300.0, 100.0, 300.0);
420        let lines = result.as_array().unwrap();
421
422        assert_eq!(lines.len(), 6); // 2 rieles + 4 horizontales
423
424        let first_line = lines[0].as_array().unwrap();
425        assert_eq!(first_line.len(), 4);
426    }
427
428    #[test]
429    fn test_spiral_center_point() {
430        // El primer punto debe estar cerca del centro
431        let result = spiral(400.0, 300.0, 1, 100.0, 10);
432        let points = result.as_array().unwrap();
433        let first_point = points[0].as_array().unwrap();
434
435        let x = first_point[0].as_f64().unwrap();
436        let y = first_point[1].as_f64().unwrap();
437
438        assert!((x - 400.0).abs() < 1.0); // Cerca del centro
439        assert!((y - 300.0).abs() < 1.0);
440    }
441
442    #[test]
443    fn test_spiral_outer_point() {
444        // El último punto debe estar cerca del radio máximo
445        let result = spiral(400.0, 300.0, 1, 100.0, 10);
446        let points = result.as_array().unwrap();
447        let last_point = points[points.len() - 1].as_array().unwrap();
448
449        let x = last_point[0].as_f64().unwrap();
450        let y = last_point[1].as_f64().unwrap();
451
452        // Distancia desde el centro debería ser ~100 (radio máximo)
453        // Nota: como es espiral de 1 vuelta, el último punto está cerca del radio máximo
454        let dist = ((x - 400.0).powi(2) + (y - 300.0).powi(2)).sqrt();
455        assert!(dist > 80.0 && dist < 120.0); // Rango razonable para espiral de 1 vuelta
456    }
457}