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>
This commit is contained in:
2026-06-16 23:22:40 +00:00
commit 1860b51f70
70 changed files with 19902 additions and 0 deletions
@@ -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 }
+19
View File
@@ -0,0 +1,19 @@
# dominium-iso
> Proyección 30° + sombra Lambert para [dominium](../README.md).
Math puro de la proyección isométrica: `(x, y, z_world) → (sx, sy_screen)` con ángulo 30° y escala configurable. Sombra Lambert proporcional al producto punto entre la normal y la luz. Cero deps gráficas — esto produce coordenadas, [`dominium-render-plan`](../dominium-render-plan/README.md) las usa.
## API
```rust
use dominium_iso::{project, lambert};
let (sx, sy) = project(x, y, z, scale);
let shade = lambert(normal, light);
```
## Deps
- `libm`
- Cero deps externas
+19
View File
@@ -0,0 +1,19 @@
# dominium-iso
> 30° projection + Lambert shadow for [dominium](../README.md).
Pure math of isometric projection: `(x, y, z_world) → (sx, sy_screen)` at 30° angle with configurable scale. Lambert shadow proportional to dot product between normal and light. Zero graphics deps — this produces coordinates, [`dominium-render-plan`](../dominium-render-plan/README.md) uses them.
## API
```rust
use dominium_iso::{project, lambert};
let (sx, sy) = project(x, y, z, scale);
let shade = lambert(normal, light);
```
## Deps
- `libm`
- Zero external deps
+181
View File
@@ -0,0 +1,181 @@
//! `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));
}
}