⎇ e2833a20c4e3cce70e7e18ea6520a78afd9a8d5a
crates/modules/dominium/dominium-iso/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat(dominium): dominium-iso — proyección pseudo-3D isométrica
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}