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:
Generated
+9
@@ -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"
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user