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>
This commit is contained in:
sergio
2026-05-20 16:16:33 +00:00
parent bbfa44ff35
commit e2833a20c4
4 changed files with 177 additions and 0 deletions
Generated
+9
View File
@@ -3482,6 +3482,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "dominium-iso"
version = "0.1.0"
dependencies = [
"dominium-core",
"libm",
"serde",
]
[[package]] [[package]]
name = "dominium-physics" name = "dominium-physics"
version = "0.1.0" version = "0.1.0"
+4
View File
@@ -147,6 +147,7 @@ members = [
# ============================================================ # ============================================================
"crates/modules/dominium/dominium-core", "crates/modules/dominium/dominium-core",
"crates/modules/dominium/dominium-physics", "crates/modules/dominium/dominium-physics",
"crates/modules/dominium/dominium-iso",
# ============================================================ # ============================================================
# modules/gioser/ — Landing WASM (chacana + 4 elementos) # modules/gioser/ — Landing WASM (chacana + 4 elementos)
@@ -286,6 +287,9 @@ libp2p-allow-block-list = "0.6"
# === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) === # === SSH (brahman-ssh-multiplex, sandokan RemoteEngine, matilda) ===
russh = "0.54" russh = "0.54"
# === Math determinista cross-platform (dominium) ===
libm = "0.2"
# === Code parsing (minga) === # === Code parsing (minga) ===
tree-sitter = "0.24" tree-sitter = "0.24"
tree-sitter-rust = "0.23" tree-sitter-rust = "0.23"
@@ -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 }
@@ -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));
}
}