Files
brahman/crates/modules/dominium/dominium-iso/src/lib.rs
T
sergio e2833a20c4 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>
2026-05-20 16:16:33 +00:00

152 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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));
}
}