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 · 2026-05-20T16:16:33Z (5 killa ñawpaq) · tayta bbfa44ff3 · kay commitpi sach'ata qhaway
Semántico sayk'uy · 15
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));
+ }
+}