tawasuyu

tawasuyu/brahman

Rust 307TOML 110JSON 14Markdown 113941 Sutichaykuna

⎇ 52acaabcf42e81de31acf884f80c607fe8b829fe crates/modules/gioser/gioser-geom/src/lib.rs Rust · 159 siq'ikuna · 4.6 KB Ñawpa willay Pi imata llamkarqan ch'uya

gioser 52acaabcf · sergio ·

1//! Geometría de la chacana andina (cruz cuadrada escalonada).
2//!
3//! Genera un polígono cerrado de 20 vértices: un cuadrado central,
4//! cuatro escalones (uno por brazo cardinal) y cuatro puntas que
5//! extienden hasta `arm_extent`.
6//!
7//! Convención: plano XY, centro en `(0, 0)`, +Y hacia el norte,
8//! +X hacia el este. Toda la API es pura: ningún I/O, ninguna asignación
9//! global; apta para ejecutar dentro de un shader-host, en un test,
10//! o en una integración nativa.
11
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct ChacanaSpec {
14    /// Distancia desde el centro hasta la punta del brazo.
15    pub arm_extent: f32,
16    /// Semi-grosor del brazo. El escalón mide `2 * thickness`.
17    pub thickness: f32,
18}
19
20impl ChacanaSpec {
21    /// Configuración canónica: brazo 1.0, grosor 0.18 (proporciones del logo).
22    pub const CLASSIC: Self = Self {
23        arm_extent: 1.0,
24        thickness: 0.18,
25    };
26
27    pub const fn new(arm_extent: f32, thickness: f32) -> Self {
28        Self {
29            arm_extent,
30            thickness,
31        }
32    }
33
34    /// Las cuatro puntas cardinales en orden `[N, E, S, W]`.
35    /// Coordenadas listas para anclar UI sobre la chacana.
36    pub fn tips(&self) -> [(f32, f32); 4] {
37        let l = self.arm_extent;
38        [(0.0, l), (l, 0.0), (0.0, -l), (-l, 0.0)]
39    }
40
41    /// Bounding box axis-aligned `(min, max)`.
42    pub fn aabb(&self) -> ((f32, f32), (f32, f32)) {
43        let l = self.arm_extent;
44        ((-l, -l), (l, l))
45    }
46
47    /// Perímetro cerrado en orden horario: 20 vértices, listo para `LINE_LOOP`.
48    pub fn perimeter(&self) -> Vec<(f32, f32)> {
49        let s = self.thickness;
50        let l = self.arm_extent;
51        let s2 = s * 2.0;
52        vec![
53            (s, l),
54            (s, s2),
55            (s2, s2),
56            (s2, s),
57            (l, s),
58            (l, -s),
59            (s2, -s),
60            (s2, -s2),
61            (s, -s2),
62            (s, -l),
63            (-s, -l),
64            (-s, -s2),
65            (-s2, -s2),
66            (-s2, -s),
67            (-l, -s),
68            (-l, s),
69            (-s2, s),
70            (-s2, s2),
71            (-s, s2),
72            (-s, l),
73        ]
74    }
75
76    /// Triangulación: 9 rectángulos (1 centro + 4 escalones + 4 puntas) = 54 vértices.
77    /// Listo para `GL_TRIANGLES`.
78    pub fn triangles(&self) -> Vec<(f32, f32)> {
79        let s = self.thickness;
80        let l = self.arm_extent;
81        let s2 = s * 2.0;
82        let mut tri = Vec::with_capacity(9 * 6);
83        let mut rect = |x0: f32, y0: f32, x1: f32, y1: f32| {
84            tri.push((x0, y0));
85            tri.push((x1, y0));
86            tri.push((x1, y1));
87            tri.push((x0, y0));
88            tri.push((x1, y1));
89            tri.push((x0, y1));
90        };
91        // Cuadrado central
92        rect(-s, -s, s, s);
93        // Escalones (un rect 4s × s por brazo)
94        rect(-s2, s, s2, s2); // N
95        rect(-s2, -s2, s2, -s); // S
96        rect(s, -s2, s2, s2); // E
97        rect(-s2, -s2, -s, s2); // W
98        // Puntas (un rect 2s × (l - 2s) por brazo)
99        rect(-s, s2, s, l); // N
100        rect(-s, -l, s, -s2); // S
101        rect(s2, -s, l, s); // E
102        rect(-l, -s, -s2, s); // W
103        tri
104    }
105
106    /// Para un punto cualquiera, devuelve la punta más cercana y su distancia.
107    /// Útil para snapping de interacción.
108    pub fn closest_tip(&self, p: (f32, f32)) -> ((f32, f32), f32) {
109        let tips = self.tips();
110        let mut best = (tips[0], f32::INFINITY);
111        for t in tips.iter() {
112            let dx = t.0 - p.0;
113            let dy = t.1 - p.1;
114            let d = (dx * dx + dy * dy).sqrt();
115            if d < best.1 {
116                best = (*t, d);
117            }
118        }
119        best
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn perimeter_has_20_vertices() {
129        assert_eq!(ChacanaSpec::CLASSIC.perimeter().len(), 20);
130    }
131
132    #[test]
133    fn triangles_form_9_rectangles() {
134        assert_eq!(ChacanaSpec::CLASSIC.triangles().len(), 9 * 6);
135    }
136
137    #[test]
138    fn tips_match_cardinals() {
139        let c = ChacanaSpec::new(2.0, 0.3);
140        let tips = c.tips();
141        assert_eq!(tips[0], (0.0, 2.0)); // N
142        assert_eq!(tips[1], (2.0, 0.0)); // E
143        assert_eq!(tips[2], (0.0, -2.0)); // S
144        assert_eq!(tips[3], (-2.0, 0.0)); // W
145    }
146
147    #[test]
148    fn closest_tip_to_upper_left_is_north() {
149        let c = ChacanaSpec::CLASSIC;
150        let (tip, _d) = c.closest_tip((-0.1, 0.95));
151        assert_eq!(tip, (0.0, 1.0));
152    }
153
154    #[test]
155    fn aabb_matches_extent() {
156        let c = ChacanaSpec::new(1.5, 0.2);
157        assert_eq!(c.aabb(), ((-1.5, -1.5), (1.5, 1.5)));
158    }
159}