gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Sutichaykuna

⎇ e2833a20c4e3cce70e7e18ea6520a78afd9a8d5a crates/modules/dominium/dominium-iso/src/lib.rs Rust · 151 siq'ikuna · 4.7 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat(dominium): dominium-iso — proyección pseudo-3D isométrica e2833a20c · sergio ·

1//! `dominium-iso` — proyección pseudo-3D isométrica.
2//!
3//! GPUI no maneja matrices de proyección 3D ni mallas: la ilusión de
4//! relieve se calcula en CPU antes de emitir quads 2D. Matriz iso fija:
5//!
6//! ```text
7//!   x_pantalla = (x - y) · cos(30°)
8//!   y_pantalla = (x + y) · sin(30°) − Z
9//! ```
10//!
11//! La altura `Z` no existe en el motor lógico — se extrae de los campos
12//! de la grilla como una combinación lineal config'able de las 5 capas
13//! ([`ZWeights`]). Los `cos`/`sin` van por `libm` para que la proyección
14//! sea bit-exacta en cualquier plataforma.
15
16#![forbid(unsafe_code)]
17
18use dominium_core::Grid;
19use serde::{Deserialize, Serialize};
20
21/// Pesos del Z compuesto — uno por capa de la grilla. El panel expone
22/// estos 5 sliders; el relieve es `Σ wᵢ · capaᵢ`.
23#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
24pub struct ZWeights {
25    pub materia: f32,
26    pub psique: f32,
27    pub poder: f32,
28    pub oro: f32,
29    pub degradacion: f32,
30}
31
32impl Default for ZWeights {
33    /// Por defecto el relieve sigue la `materia`.
34    fn default() -> Self {
35        Self { materia: 1.0, psique: 0.0, poder: 0.0, oro: 0.0, degradacion: 0.0 }
36    }
37}
38
39impl ZWeights {
40    /// Z compuesto de la celda `idx`: combinación lineal de las 5 capas.
41    pub fn z_of(&self, grid: &Grid, idx: usize) -> f32 {
42        self.materia * grid.materia[idx]
43            + self.psique * grid.psique[idx]
44            + self.poder * grid.poder[idx]
45            + self.oro * grid.oro[idx]
46            + self.degradacion * grid.degradacion[idx]
47    }
48}
49
50/// Proyector isométrico. `cos`/`sin` de 30° precomputados vía `libm`.
51#[derive(Debug, Clone, Copy)]
52pub struct IsoProjector {
53    cos30: f32,
54    sin30: f32,
55    /// Escala de pantalla (pixels por unidad de mundo).
56    pub scale: f32,
57    /// Cuánto eleva el `Z` en pixels de pantalla.
58    pub z_factor: f32,
59}
60
61impl IsoProjector {
62    /// Crea un proyector. `scale` = pixels por celda; `z_factor` = cuánto
63    /// levanta una unidad de Z.
64    pub fn new(scale: f32, z_factor: f32) -> Self {
65        // 30° en radianes. libm da el mismo bit en x86 y ARM.
66        let rad = core::f32::consts::FRAC_PI_6;
67        Self {
68            cos30: libm::cosf(rad),
69            sin30: libm::sinf(rad),
70            scale,
71            z_factor,
72        }
73    }
74
75    /// Proyecta una coordenada de mundo `(x, y)` con altura `z` a
76    /// coordenadas de pantalla.
77    pub fn project(&self, x: f32, y: f32, z: f32) -> (f32, f32) {
78        let sx = (x - y) * self.cos30 * self.scale;
79        let sy = ((x + y) * self.sin30 - z * self.z_factor) * self.scale;
80        (sx, sy)
81    }
82
83    /// Proyecta la sombra de un punto sobre el suelo (Lambert plano): la
84    /// sombra cae en `z = 0` desplazada según la dirección de la luz, con
85    /// largo proporcional a la altura del punto.
86    pub fn shadow(&self, x: f32, y: f32, z: f32, light_dir: (f32, f32)) -> (f32, f32) {
87        let foot_x = x + light_dir.0 * z;
88        let foot_y = y + light_dir.1 * z;
89        self.project(foot_x, foot_y, 0.0)
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    fn approx(a: f32, b: f32) -> bool {
98        (a - b).abs() < 1e-4
99    }
100
101    #[test]
102    fn origin_projects_to_origin() {
103        let iso = IsoProjector::new(1.0, 1.0);
104        let (x, y) = iso.project(0.0, 0.0, 0.0);
105        assert!(approx(x, 0.0) && approx(y, 0.0));
106    }
107
108    #[test]
109    fn diamond_axis_collapses_x() {
110        // En iso, (a, a) cae sobre x_pantalla = 0 (la diagonal del rombo).
111        let iso = IsoProjector::new(1.0, 1.0);
112        let (sx, _) = iso.project(5.0, 5.0, 0.0);
113        assert!(approx(sx, 0.0));
114    }
115
116    #[test]
117    fn z_raises_the_point_upward() {
118        let iso = IsoProjector::new(1.0, 10.0);
119        let (_, y0) = iso.project(3.0, 3.0, 0.0);
120        let (_, y1) = iso.project(3.0, 3.0, 2.0);
121        // Más Z → menor y de pantalla (sube).
122        assert!(y1 < y0);
123    }
124
125    #[test]
126    fn composite_z_is_a_linear_combination() {
127        let mut g = Grid::new(4, 4);
128        let idx = g.idx(1, 1);
129        g.materia[idx] = 10.0;
130        g.poder[idx] = 4.0;
131        let w = ZWeights { materia: 0.5, psique: 0.0, poder: 2.0, oro: 0.0, degradacion: 0.0 };
132        // 0.5*10 + 2*4 = 13
133        assert!(approx(w.z_of(&g, idx), 13.0));
134    }
135
136    #[test]
137    fn projector_is_deterministic() {
138        let a = IsoProjector::new(2.0, 3.0);
139        let b = IsoProjector::new(2.0, 3.0);
140        assert_eq!(a.project(7.0, 11.0, 1.5), b.project(7.0, 11.0, 1.5));
141    }
142
143    #[test]
144    fn shadow_of_ground_point_equals_its_projection() {
145        let iso = IsoProjector::new(1.0, 5.0);
146        // z = 0 → la sombra coincide con el punto.
147        let p = iso.project(4.0, 2.0, 0.0);
148        let s = iso.shadow(4.0, 2.0, 0.0, (1.0, 0.5));
149        assert!(approx(p.0, s.0) && approx(p.1, s.1));
150    }
151}