From e2833a20c4e3cce70e7e18ea6520a78afd9a8d5a Mon Sep 17 00:00:00 2001 From: sergio Date: Wed, 20 May 2026 16:16:33 +0000 Subject: [PATCH] =?UTF-8?q?feat(dominium):=20dominium-iso=20=E2=80=94=20pr?= =?UTF-8?q?oyecci=C3=B3n=20pseudo-3D=20isom=C3=A9trica?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- Cargo.lock | 9 ++ Cargo.toml | 4 + .../modules/dominium/dominium-iso/Cargo.toml | 13 ++ .../modules/dominium/dominium-iso/src/lib.rs | 151 ++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 crates/modules/dominium/dominium-iso/Cargo.toml create mode 100644 crates/modules/dominium/dominium-iso/src/lib.rs 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)); + } +}