Files
Dominium/01_yachay/dominium/dominium-iso/src/lib.rs
T
sergio 1860b51f70 feat: dominium standalone — simulador de campo medio sobre Llimphi
Front-door publicable de dominium: los 9 crates propios como path
members; Llimphi, app-bus, rimay-localize, wawa-config y pluma-notebook
por git-dep al monorepo tawasuyu.git (branch=main). cargo check
--workspace --all-targets pasa exit 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 23:22:40 +00:00

182 lines
6.0 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)
}
/// Inversa de [`Self::project`] asumiendo `z = 0` (clicks sobre el
/// suelo). Dadas coordenadas de pantalla `(sx, sy)`, devuelve el
/// `(x, y)` de mundo que las generó si se hubiera proyectado con
/// `z = 0`. Para clicks sobre celdas elevadas el resultado se
/// desplaza (las cimas proyectan a una `y_pantalla` distinta a la
/// de su pie); para una sembrazón de Conceptos es suficiente.
///
/// ```text
/// sx = (x - y) · cos30 · scale
/// sy = (x + y) · sin30 · scale
/// ⇒ x = (sx / (cos30·scale) + sy / (sin30·scale)) / 2
/// y = (sy / (sin30·scale) sx / (cos30·scale)) / 2
/// ```
pub fn unproject_floor(&self, sx: f32, sy: f32) -> (f32, f32) {
let s = self.scale.max(f32::EPSILON);
let u = sx / (self.cos30 * s); // = x - y
let v = sy / (self.sin30 * s); // = x + y
((u + v) * 0.5, (v - u) * 0.5)
}
}
#[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 unproject_floor_is_inverse_of_project_at_z_zero() {
let iso = IsoProjector::new(12.0, 5.0);
for (x, y) in [(3.0, 7.0), (15.0, 2.0), (8.0, 8.0), (0.0, 0.0)] {
let (sx, sy) = iso.project(x, y, 0.0);
let (rx, ry) = iso.unproject_floor(sx, sy);
assert!(approx(rx, x) && approx(ry, y), "({x},{y}) → ({rx},{ry})");
}
}
#[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));
}
}