tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/gioser/gioser-geom/src/lib.rs Rust · 159 líneas · 5.3 KB Historia Quién tocó qué crudo

feat(gioser): chacana mística stepped, nubes/estrellas/meteoros, tilt 35° e701df4a2 · sergio ·

1//! Geometría de la chacana andina escalonada (cruz cuadrada de Tiwanaku).
2//!
3//! Modelo paramétrico: un cuadrado central de lado `2 * center_half()`,
4//! del que sobresalen cuatro brazos cardinales formados por `steps`
5//! niveles. Cada nivel adelgaza al brazo en `thickness` por lado y lo
6//! prolonga en `thickness` hacia afuera.
7//!
8//! Para `steps = 2` (clásica mística):
9//! - Centro: cuadrado `6s × 6s` (donde `s = thickness`).
10//! - Nivel 1: rectángulo perpendicular `4s × s` adosado a cada cara del centro.
11//! - Nivel 2 (punta): rectángulo `2s × s` adosado al nivel 1.
12//!
13//! Resultado: bounding box `±5s` (cuadrado, no alargado como una cruz latina),
14//! 9 rectángulos disjuntos triangulables, 4 tips cardinales.
15
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct ChacanaSpec {
18    /// Unidad base de la geometría. Cada paso aporta `thickness` de ancho
19    /// y `thickness` de profundidad.
20    pub thickness: f32,
21    /// Cantidad de escalones por brazo (`>= 1`). La chacana mística clásica = `2`.
22    pub steps: u32,
23}
24
25impl ChacanaSpec {
26    /// Configuración canónica del logo GioSer: 2 escalones, thickness 0.13
27    /// (bounding box ≈ 1.30 × 1.30 en unidades de mundo).
28    pub const CLASSIC: Self = Self {
29        thickness: 0.13,
30        steps: 2,
31    };
32
33    pub const fn new(thickness: f32, steps: u32) -> Self {
34        Self { thickness, steps }
35    }
36
37    /// Semi-lado del cuadrado central — la parte **más ancha** de la chacana.
38    pub fn center_half(&self) -> f32 {
39        (self.steps as f32 + 1.0) * self.thickness
40    }
41
42    /// Distancia desde el centro a la punta más externa.
43    pub fn arm_extent(&self) -> f32 {
44        self.center_half() + self.steps as f32 * self.thickness
45    }
46
47    /// Las cuatro puntas cardinales `[N, E, S, W]`.
48    pub fn tips(&self) -> [(f32, f32); 4] {
49        let l = self.arm_extent();
50        [(0.0, l), (l, 0.0), (0.0, -l), (-l, 0.0)]
51    }
52
53    pub fn aabb(&self) -> ((f32, f32), (f32, f32)) {
54        let l = self.arm_extent();
55        ((-l, -l), (l, l))
56    }
57
58    /// Triangulación: `1 + 4 * steps` rectángulos en `GL_TRIANGLES`.
59    /// Para `steps = 2`: 9 rects = 54 vértices.
60    pub fn triangles(&self) -> Vec<(f32, f32)> {
61        let s = self.thickness;
62        let c = self.center_half();
63        let mut tri = Vec::with_capacity(6 * (1 + 4 * self.steps as usize));
64        let mut rect = |x0: f32, y0: f32, x1: f32, y1: f32| {
65            tri.push((x0, y0));
66            tri.push((x1, y0));
67            tri.push((x1, y1));
68            tri.push((x0, y0));
69            tri.push((x1, y1));
70            tri.push((x0, y1));
71        };
72        rect(-c, -c, c, c);
73        for k in 1..=self.steps {
74            // El k-ésimo nivel (1 = más cerca del centro, steps = punta)
75            // adelgaza a (steps - k + 1) * thickness de semi-ancho.
76            let hw = (self.steps - k + 1) as f32 * s;
77            let inner = c + (k - 1) as f32 * s;
78            let outer = c + k as f32 * s;
79            rect(-hw, inner, hw, outer); // N
80            rect(-hw, -outer, hw, -inner); // S
81            rect(inner, -hw, outer, hw); // E
82            rect(-outer, -hw, -inner, hw); // W
83        }
84        tri
85    }
86
87    /// Para un punto cualquiera, devuelve la punta más cercana y la distancia.
88    pub fn closest_tip(&self, p: (f32, f32)) -> ((f32, f32), f32) {
89        let tips = self.tips();
90        let mut best = (tips[0], f32::INFINITY);
91        for t in tips.iter() {
92            let dx = t.0 - p.0;
93            let dy = t.1 - p.1;
94            let d = (dx * dx + dy * dy).sqrt();
95            if d < best.1 {
96                best = (*t, d);
97            }
98        }
99        best
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn classic_is_two_step_chacana() {
109        let c = ChacanaSpec::CLASSIC;
110        assert_eq!(c.steps, 2);
111        // center_half = 3 * 0.13 = 0.39; arm_extent = 0.65.
112        assert!((c.center_half() - 0.39).abs() < 1e-6);
113        assert!((c.arm_extent() - 0.65).abs() < 1e-6);
114    }
115
116    #[test]
117    fn arm_extent_grows_with_steps() {
118        let c1 = ChacanaSpec::new(0.1, 1);
119        let c2 = ChacanaSpec::new(0.1, 2);
120        let c3 = ChacanaSpec::new(0.1, 3);
121        assert!(c1.arm_extent() < c2.arm_extent());
122        assert!(c2.arm_extent() < c3.arm_extent());
123    }
124
125    #[test]
126    fn triangles_one_rect_plus_four_per_step() {
127        let c1 = ChacanaSpec::new(0.1, 1);
128        assert_eq!(c1.triangles().len(), 6 * (1 + 4 * 1));
129        let c2 = ChacanaSpec::CLASSIC;
130        assert_eq!(c2.triangles().len(), 6 * (1 + 4 * 2));
131        let c3 = ChacanaSpec::new(0.1, 3);
132        assert_eq!(c3.triangles().len(), 6 * (1 + 4 * 3));
133    }
134
135    #[test]
136    fn tips_match_cardinals() {
137        let c = ChacanaSpec::CLASSIC;
138        let l = c.arm_extent();
139        let tips = c.tips();
140        assert_eq!(tips[0], (0.0, l)); // N
141        assert_eq!(tips[1], (l, 0.0)); // E
142        assert_eq!(tips[2], (0.0, -l)); // S
143        assert_eq!(tips[3], (-l, 0.0)); // W
144    }
145
146    #[test]
147    fn closest_tip_to_upper_point_is_north() {
148        let c = ChacanaSpec::CLASSIC;
149        let (tip, _d) = c.closest_tip((-0.1, 0.55));
150        assert_eq!(tip, (0.0, c.arm_extent()));
151    }
152
153    #[test]
154    fn aabb_matches_extent() {
155        let c = ChacanaSpec::new(0.12, 2);
156        let l = c.arm_extent();
157        assert_eq!(c.aabb(), ((-l, -l), (l, l)));
158    }
159}