tawasuyu

tawasuyu/brahman

Rust 225TOML 67JSON 14HTML 12842 Sutichaykuna

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

Proyección calculada en CPU antes de emitir quads 2D (GPUI no maneja
matrices 3D ni mallas).

- ZWeights — pesos del Z compuesto, uno por capa; z_of() calcula el
  relieve como Σ wᵢ·capaᵢ (los 5 sliders del panel).
- IsoProjector — matriz iso fija: x=(x-y)·cos30, y=(x+y)·sin30 − Z·zf.
  cos/sin de 30° vía libm → proyección bit-exacta cross-platform.
- project() + shadow() (Lambert plano: la sombra cae en z=0 desplazada
  por la dirección de luz, larga en proporción a la altura).

6 tests verdes (origen, eje del rombo, Z eleva, Z compuesto lineal,
determinismo, sombra de punto en el suelo). cargo check verde.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

e2833a20c4e3cce70e7e18ea6520a78afd9a8d5a · ruwaq sergio · (5 killa ñawpaq) · tayta bbfa44ff3 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 15

crates/modules/dominium/dominium-iso/src/lib.rs

4 qillqakuna · +177 −0

diff --git a/Cargo.lock b/Cargo.lock
index 3dcea95..d29223c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3482,6 +3482,15 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "dominium-iso"
+version = "0.1.0"
+dependencies = [
+ "dominium-core",
+ "libm",
+ "serde",
+]
+
 [[package]]
 name = "dominium-physics"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 395c52f..63fbeea 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -147,6 +147,7 @@ members = [
     # ============================================================
     "crates/modules/dominium/dominium-core",
     "crates/modules/dominium/dominium-physics",
+    "crates/modules/dominium/dominium-iso",
 
     # ============================================================
     # modules/gioser/ — Landing WASM (chacana + 4 elementos)
@@ -286,6 +287,9 @@ libp2p-allow-block-list = "0.6"
 # === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
 russh = "0.54"
 
+# === Math determinista cross-platform (dominium) ===
+libm = "0.2"
+
 # === Code parsing (minga) ===
 tree-sitter = "0.24"
 tree-sitter-rust = "0.23"
diff --git a/crates/modules/dominium/dominium-iso/Cargo.toml b/crates/modules/dominium/dominium-iso/Cargo.toml
new file mode 100644
index 0000000..01d31b1
--- /dev/null
+++ b/crates/modules/dominium/dominium-iso/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "dominium-iso"
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+authors.workspace = true
+publish.workspace = true
+description = "dominium — proyección pseudo-3D isométrica calculada en CPU: matriz iso fija + Z compuesto de 5 capas + sombras analíticas Lambert."
+
+[dependencies]
+dominium-core = { path = "../dominium-core" }
+libm = { workspace = true }
+serde = { workspace = true }
diff --git a/crates/modules/dominium/dominium-iso/src/lib.rs b/crates/modules/dominium/dominium-iso/src/lib.rs
new file mode 100644
index 0000000..7eb3b59
--- /dev/null
+++ b/crates/modules/dominium/dominium-iso/src/lib.rs
@@ -0,0 +1,151 @@
+//! `dominium-iso` — proyección pseudo-3D isométrica.
+//!
+//! GPUI no maneja matrices de proyección 3D ni mallas: la ilusión de
+//! relieve se calcula en CPU antes de emitir quads 2D. Matriz iso fija:
+//!
+//! ```text
+//!   x_pantalla = (x - y) · cos(30°)
+//!   y_pantalla = (x + y) · sin(30°) − Z
+//! ```
+//!
+//! La altura `Z` no existe en el motor lógico — se extrae de los campos
+//! de la grilla como una combinación lineal config'able de las 5 capas
+//! ([`ZWeights`]). Los `cos`/`sin` van por `libm` para que la proyección
+//! sea bit-exacta en cualquier plataforma.
+
+#![forbid(unsafe_code)]
+
+use dominium_core::Grid;
+use serde::{Deserialize, Serialize};
+
+/// Pesos del Z compuesto — uno por capa de la grilla. El panel expone
+/// estos 5 sliders; el relieve es `Σ wᵢ · capaᵢ`.
+#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
+pub struct ZWeights {
+    pub materia: f32,
+    pub psique: f32,
+    pub poder: f32,
+    pub oro: f32,
+    pub degradacion: f32,
+}
+
+impl Default for ZWeights {
+    /// Por defecto el relieve sigue la `materia`.
+    fn default() -> Self {
+        Self { materia: 1.0, psique: 0.0, poder: 0.0, oro: 0.0, degradacion: 0.0 }
+    }
+}
+
+impl ZWeights {
+    /// Z compuesto de la celda `idx`: combinación lineal de las 5 capas.
+    pub fn z_of(&self, grid: &Grid, idx: usize) -> f32 {
+        self.materia * grid.materia[idx]
+            + self.psique * grid.psique[idx]
+            + self.poder * grid.poder[idx]
+            + self.oro * grid.oro[idx]
+            + self.degradacion * grid.degradacion[idx]
+    }
+}
+
+/// Proyector isométrico. `cos`/`sin` de 30° precomputados vía `libm`.
+#[derive(Debug, Clone, Copy)]
+pub struct IsoProjector {
+    cos30: f32,
+    sin30: f32,
+    /// Escala de pantalla (pixels por unidad de mundo).
+    pub scale: f32,
+    /// Cuánto eleva el `Z` en pixels de pantalla.
+    pub z_factor: f32,
+}
+
+impl IsoProjector {
+    /// Crea un proyector. `scale` = pixels por celda; `z_factor` = cuánto
+    /// levanta una unidad de Z.
+    pub fn new(scale: f32, z_factor: f32) -> Self {
+        // 30° en radianes. libm da el mismo bit en x86 y ARM.
+        let rad = core::f32::consts::FRAC_PI_6;
+        Self {
+            cos30: libm::cosf(rad),
+            sin30: libm::sinf(rad),
+            scale,
+            z_factor,
+        }
+    }
+
+    /// Proyecta una coordenada de mundo `(x, y)` con altura `z` a
+    /// coordenadas de pantalla.
+    pub fn project(&self, x: f32, y: f32, z: f32) -> (f32, f32) {
+        let sx = (x - y) * self.cos30 * self.scale;
+        let sy = ((x + y) * self.sin30 - z * self.z_factor) * self.scale;
+        (sx, sy)
+    }
+
+    /// Proyecta la sombra de un punto sobre el suelo (Lambert plano): la
+    /// sombra cae en `z = 0` desplazada según la dirección de la luz, con
+    /// largo proporcional a la altura del punto.
+    pub fn shadow(&self, x: f32, y: f32, z: f32, light_dir: (f32, f32)) -> (f32, f32) {
+        let foot_x = x + light_dir.0 * z;
+        let foot_y = y + light_dir.1 * z;
+        self.project(foot_x, foot_y, 0.0)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn approx(a: f32, b: f32) -> bool {
+        (a - b).abs() < 1e-4
+    }
+
+    #[test]
+    fn origin_projects_to_origin() {
+        let iso = IsoProjector::new(1.0, 1.0);
+        let (x, y) = iso.project(0.0, 0.0, 0.0);
+        assert!(approx(x, 0.0) && approx(y, 0.0));
+    }
+
+    #[test]
+    fn diamond_axis_collapses_x() {
+        // En iso, (a, a) cae sobre x_pantalla = 0 (la diagonal del rombo).
+        let iso = IsoProjector::new(1.0, 1.0);
+        let (sx, _) = iso.project(5.0, 5.0, 0.0);
+        assert!(approx(sx, 0.0));
+    }
+
+    #[test]
+    fn z_raises_the_point_upward() {
+        let iso = IsoProjector::new(1.0, 10.0);
+        let (_, y0) = iso.project(3.0, 3.0, 0.0);
+        let (_, y1) = iso.project(3.0, 3.0, 2.0);
+        // Más Z → menor y de pantalla (sube).
+        assert!(y1 < y0);
+    }
+
+    #[test]
+    fn composite_z_is_a_linear_combination() {
+        let mut g = Grid::new(4, 4);
+        let idx = g.idx(1, 1);
+        g.materia[idx] = 10.0;
+        g.poder[idx] = 4.0;
+        let w = ZWeights { materia: 0.5, psique: 0.0, poder: 2.0, oro: 0.0, degradacion: 0.0 };
+        // 0.5*10 + 2*4 = 13
+        assert!(approx(w.z_of(&g, idx), 13.0));
+    }
+
+    #[test]
+    fn projector_is_deterministic() {
+        let a = IsoProjector::new(2.0, 3.0);
+        let b = IsoProjector::new(2.0, 3.0);
+        assert_eq!(a.project(7.0, 11.0, 1.5), b.project(7.0, 11.0, 1.5));
+    }
+
+    #[test]
+    fn shadow_of_ground_point_equals_its_projection() {
+        let iso = IsoProjector::new(1.0, 5.0);
+        // z = 0 → la sombra coincide con el punto.
+        let p = iso.project(4.0, 2.0, 0.0);
+        let s = iso.shadow(4.0, 2.0, 0.0, (1.0, 0.5));
+        assert!(approx(p.0, s.0) && approx(p.1, s.1));
+    }
+}