tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main llimphi-voxel/src/urbe.rs Rust · 348 lines · 13.4 KB History Who touched what raw

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · Sergio ·

1//! **Planeamiento urbano**: convierte la mancha de pueblo de un bioma en una traza
2//! LIMPIA y NIVELADA — plaza central con iglesia, avenidas en cruz, cuadras
3//! regladas con calles y veredas — **sin escalones**. El suelo del casco se aplana
4//! a una sola cota (la del centro del pueblo) y el borde se funde con el terreno
5//! natural por una rampa perimetral, así el auto rueda por calles planas.
6//!
7//! Como todo el world-gen, es **función pura de coordenadas de mundo**: cada
8//! columna `(wx, wz)` se consulta sola, sin estado compartido, así el *streaming*
9//! encaja ventana a ventana (mismo pueblo en cualquier encuadre).
10//!
11//! El retículo: el mundo se parte en celdas de [`CELDA`] voxels; **cada** celda
12//! aloja un pueblo, centrado con jitter. El `densidad_urbana` del bioma mueve el
13//! **radio** del casco (aldea chica ↔ ciudad extendida), no la existencia — así la
14//! traza siempre aparece y es deterministamente ubicable.
15
16use crate::terrain::hash2;
17
18/// Lado de la celda del retículo de pueblos (voxels). Cada celda aloja un pueblo.
19/// Grande respecto al radio máximo → queda campo entre pueblos.
20const CELDA: i32 = 176;
21/// Radio máximo del casco urbano (ciudad densa), en voxels.
22const RADIO_MAX: i32 = 62;
23/// Radio mínimo del casco (aldea dispersa), en voxels.
24const RADIO_MIN: i32 = 26;
25/// Jitter máximo del centro dentro de la celda (voxels).
26const JITTER: i32 = 26;
27/// Franja perimetral (voxels) donde la cota nivelada se funde con el terreno
28/// natural: la **rampa** que evita el acantilado en el borde del casco.
29const RIM: i32 = 12;
30/// Paso de la grilla de cuadras (voxels): ancho de cuadra + calle.
31const CUADRA: i32 = 22;
32/// Media-anchura de calle local (voxels). Ancho total = `2·HW − 1`.
33const CALLE_HW: i32 = 3;
34/// Media-anchura de avenida: las dos líneas centrales de la cruz, más anchas.
35const AVE_HW: i32 = 5;
36/// Media-lado de la plaza central empedrada (voxels).
37const PLAZA: i32 = 8;
38
39// Paleta de la calzada (se conserva la del pueblo viejo para no romper tests).
40const ASF_CALLE: [u8; 3] = [64, 66, 72]; // asfalto de calle local
41const ASF_AVE: [u8; 3] = [58, 60, 66]; // asfalto de avenida
42const VEREDA: [u8; 3] = [150, 150, 146]; // vereda / cordón
43const LINEA: [u8; 3] = [200, 178, 60]; // línea amarilla central
44const PLAZA_A: [u8; 3] = [196, 190, 176]; // baldosa clara
45const PLAZA_B: [u8; 3] = [150, 150, 146]; // baldosa oscura
46
47/// Un pueblo ubicado: su centro de mundo, la cota nivelada del casco y una semilla
48/// estable propia (para variar estilos entre pueblos).
49#[derive(Clone, Copy, Debug)]
50pub struct Pueblo {
51    pub centro: [i32; 2],
52    /// Cota del suelo nivelado (índice `y` del voxel superior del casco).
53    pub suelo: u32,
54    /// Radio del casco urbano (voxels).
55    pub radio: i32,
56    /// Semilla estable del pueblo.
57    pub seed: u32,
58}
59
60/// Zonificación de una columna del casco (lo que se pinta en la superficie).
61#[derive(Clone, Copy, PartialEq, Eq, Debug)]
62pub enum Zona {
63    /// Interior de cuadra: suelo natural (patio); puede recibir una casa en el centro.
64    Parcela,
65    /// Plaza empedrada.
66    Plaza,
67    /// Avenida (calzada ancha de la cruz central).
68    Avenida,
69    /// Calle local.
70    Calle,
71    /// Vereda / cordón.
72    Vereda,
73    /// Rampa perimetral: suelo natural en transición, sin pavimento.
74    Rampa,
75}
76
77impl Zona {
78    /// Color a estampar en la superficie, o `None` si conserva el material natural.
79    fn pav(self, u: i32, v: i32) -> Option<[u8; 3]> {
80        match self {
81            Zona::Parcela | Zona::Rampa => None,
82            Zona::Vereda => Some(VEREDA),
83            Zona::Calle => Some(ASF_CALLE),
84            Zona::Avenida => {
85                // Línea amarilla discontinua sobre el eje de la avenida.
86                let sobre_eje = u == 0 || v == 0;
87                let guion = (u + v).rem_euclid(6) < 3;
88                if sobre_eje && guion {
89                    Some(LINEA)
90                } else {
91                    Some(ASF_AVE)
92                }
93            }
94            Zona::Plaza => Some(if (u + v).rem_euclid(2) == 0 { PLAZA_A } else { PLAZA_B }),
95        }
96    }
97}
98
99/// Resultado de consultar una columna: la cota del suelo (nivelada o rampa) y el
100/// pavimento a pintar encima (si lo hay).
101#[derive(Clone, Copy, Debug)]
102pub struct Plano {
103    pub suelo: u32,
104    pub pav: Option<[u8; 3]>,
105}
106
107/// El **planificador urbano** de un bioma: retículo de pueblos parametrizado por la
108/// densidad urbana. Sin estado — todo se deriva de `(seed, wx, wz)`.
109#[derive(Clone, Copy, Debug)]
110pub struct Urbe {
111    seed: u32,
112    /// Radio del casco, interpolado por `densidad_urbana`.
113    radio: i32,
114    dy: u32,
115}
116
117impl Urbe {
118    /// Construye el planificador para una `seed`, un `densidad_urbana ∈ [0,1]` y el
119    /// alto del mundo `dy`.
120    pub fn new(seed: u32, densidad_urbana: f32, dy: u32) -> Self {
121        let d = densidad_urbana.clamp(0.0, 1.0);
122        let radio = RADIO_MIN + ((RADIO_MAX - RADIO_MIN) as f32 * d) as i32;
123        Urbe { seed, radio, dy }
124    }
125
126    /// Centro de mundo del pueblo de la celda `(cx, cz)` del retículo (con jitter),
127    /// y su semilla estable.
128    fn centro_celda(&self, cx: i32, cz: i32) -> (i32, i32, u32) {
129        let s = self
130            .seed
131            .wrapping_add((cx as u32).wrapping_mul(0x9E37_79B9))
132            .wrapping_add((cz as u32).wrapping_mul(0x85EB_CA77))
133            ^ 0x7011;
134        let jx = ((hash2(cx, cz, s ^ 0xA1) - 0.5) * 2.0 * JITTER as f32) as i32;
135        let jz = ((hash2(cx, cz, s ^ 0xB2) - 0.5) * 2.0 * JITTER as f32) as i32;
136        let bx = cx * CELDA + CELDA / 2 + jx;
137        let bz = cz * CELDA + CELDA / 2 + jz;
138        (bx, bz, s)
139    }
140
141    /// El pueblo cuyo casco cubre la columna `(wx, wz)`, o `None` si es campo. `h`
142    /// da la altura natural del terreno (para fijar la cota nivelada del casco en el
143    /// centro y para la rampa perimetral).
144    pub fn pueblo_en(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option<(Pueblo, i32)> {
145        let cx0 = wx.div_euclid(CELDA);
146        let cz0 = wz.div_euclid(CELDA);
147        let mut mejor: Option<(Pueblo, i32)> = None;
148        for dcx in -1..=1 {
149            for dcz in -1..=1 {
150                let (bx, bz, s) = self.centro_celda(cx0 + dcx, cz0 + dcz);
151                let dx = wx - bx;
152                let dz = wz - bz;
153                let d2 = dx * dx + dz * dz;
154                if d2 > self.radio * self.radio {
155                    continue;
156                }
157                let d = (d2 as f32).sqrt() as i32;
158                if mejor.map_or(true, |(_, md)| d < md) {
159                    let suelo = h(bx, bz).clamp(1, self.dy.saturating_sub(2));
160                    mejor = Some((
161                        Pueblo { centro: [bx, bz], suelo, radio: self.radio, seed: s },
162                        d,
163                    ));
164                }
165            }
166        }
167        mejor
168    }
169
170    /// Consulta la columna `(wx, wz)`: cota del suelo (nivelada dentro del casco,
171    /// rampa hacia el terreno natural en el borde) y pavimento a pintar. `None` =
172    /// campo abierto (el terreno manda).
173    pub fn consultar(&self, wx: i32, wz: i32, h: &impl Fn(i32, i32) -> u32) -> Option<Plano> {
174        let (p, d) = self.pueblo_en(wx, wz, h)?;
175        let natural = h(wx, wz);
176        let interior = p.radio - RIM;
177        let suelo = if d <= interior {
178            p.suelo
179        } else {
180            // Rampa: mezcla lineal cota↔natural sobre la franja perimetral.
181            let t = ((d - interior) as f32 / RIM as f32).clamp(0.0, 1.0);
182            let a = p.suelo as f32;
183            let b = natural as f32;
184            (a + (b - a) * t).round() as u32
185        };
186        // Sólo el interior del casco (dejando un margen antes de la rampa) recibe
187        // traza urbana; la franja perimetral queda como suelo natural en rampa.
188        if d > interior - 2 {
189            return Some(Plano { suelo, pav: Zona::Rampa.pav(0, 0) });
190        }
191        let (u, v) = (wx - p.centro[0], wz - p.centro[1]);
192        let zona = self.zona(u, v);
193        Some(Plano { suelo, pav: zona.pav(u, v) })
194    }
195
196    /// Zonificación de la columna en coordenadas **locales** al centro del pueblo.
197    /// Plaza central → cruz de avenidas → grilla de calles → interior de cuadra.
198    pub fn zona(&self, u: i32, v: i32) -> Zona {
199        if u.abs() <= PLAZA && v.abs() <= PLAZA {
200            return Zona::Plaza;
201        }
202        let bu = banda(u);
203        let bv = banda(v);
204        match (bu, bv) {
205            (None, None) => Zona::Parcela,
206            _ => {
207                // ¿Alguna banda tiene asfalto interior en esta columna? (no sólo cordón)
208                let interior_u = bu.map_or(false, |b| b.d < b.hw - 1);
209                let interior_v = bv.map_or(false, |b| b.d < b.hw - 1);
210                if !interior_u && !interior_v {
211                    return Zona::Vereda;
212                }
213                let avenida = bu.map_or(false, |b| b.ave) || bv.map_or(false, |b| b.ave);
214                if avenida {
215                    Zona::Avenida
216                } else {
217                    Zona::Calle
218                }
219            }
220        }
221    }
222
223    /// El **radio interior** urbanizable (dentro de la rampa) — lo usan los pases de
224    /// casas / iglesia para no plantar nada en la rampa.
225    pub fn radio_interior(&self) -> i32 {
226        self.radio - RIM - 2
227    }
228}
229
230/// Una banda de calzada sobre un eje: la grilla centrada tiene líneas en los
231/// múltiplos de [`CUADRA`]; la línea `k = 0` es avenida (más ancha).
232#[derive(Clone, Copy)]
233struct Banda {
234    /// Distancia al eje de la calzada más cercana.
235    d: i32,
236    /// Media-anchura de esa calzada.
237    hw: i32,
238    /// `true` si es una de las dos avenidas centrales.
239    ave: bool,
240}
241
242/// La banda de calzada que cubre la coordenada local `t`, o `None` si cae en cuadra.
243fn banda(t: i32) -> Option<Banda> {
244    let k = (t as f32 / CUADRA as f32).round() as i32;
245    let d = (t - k * CUADRA).abs();
246    let ave = k == 0;
247    let hw = if ave { AVE_HW } else { CALLE_HW };
248    if d < hw {
249        Some(Banda { d, hw, ave })
250    } else {
251        None
252    }
253}
254
255/// Los pueblos cuyos cascos intersecan la ventana `origin..origin+dim` (más el
256/// margen del radio). Para los pases de mobiliario/casas que estampan una vez por
257/// pueblo (iglesia, fuente, cuadras).
258pub fn pueblos_en_ventana(
259    urbe: &Urbe,
260    origin: [i32; 2],
261    dim: [u32; 3],
262    h: &impl Fn(i32, i32) -> u32,
263) -> Vec<Pueblo> {
264    let [ox, oz] = origin;
265    let cx0 = (ox - urbe.radio).div_euclid(CELDA);
266    let cx1 = (ox + dim[0] as i32 + urbe.radio).div_euclid(CELDA);
267    let cz0 = (oz - urbe.radio).div_euclid(CELDA);
268    let cz1 = (oz + dim[2] as i32 + urbe.radio).div_euclid(CELDA);
269    let mut out = Vec::new();
270    for cx in cx0..=cx1 {
271        for cz in cz0..=cz1 {
272            let (bx, bz, s) = urbe.centro_celda(cx, cz);
273            let suelo = h(bx, bz).clamp(1, urbe.dy.saturating_sub(2));
274            out.push(Pueblo { centro: [bx, bz], suelo, radio: urbe.radio, seed: s });
275        }
276    }
277    out
278}
279
280// Offsets locales del mobiliario cívico (relativos al centro del pueblo).
281/// Desplazamiento en `v` (norte) del centro de la iglesia respecto a la plaza.
282pub const IGLESIA_V: i32 = -(PLAZA + 6);
283/// Media-lado de la plaza (para colocar bancos/árboles/iglesia desde afuera).
284pub const PLAZA_HALF: i32 = PLAZA;
285/// Paso de la grilla de cuadras, reexportado para el pase de casas.
286pub const PASO_CUADRA: i32 = CUADRA;
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    fn llano(_x: i32, _z: i32) -> u32 {
293        12
294    }
295
296    #[test]
297    fn cada_celda_aloja_un_pueblo_ubicable() {
298        let urbe = Urbe::new(7, 0.7, 40);
299        // Consultado en su centro exacto, el pueblo de la celda (0,0) da distancia 0.
300        let (bx, bz, _) = urbe.centro_celda(0, 0);
301        let (p, d) = urbe.pueblo_en(bx, bz, &llano).expect("hay pueblo en la celda 0");
302        assert_eq!(d, 0);
303        assert_eq!(p.suelo, 12);
304        assert_eq!(p.centro, [bx, bz]);
305    }
306
307    #[test]
308    fn el_centro_es_plaza() {
309        let urbe = Urbe::new(3, 0.6, 40);
310        assert_eq!(urbe.zona(0, 0), Zona::Plaza);
311        assert_eq!(urbe.zona(PLAZA, 0), Zona::Plaza);
312        // Justo afuera de la plaza, sobre el eje, hay avenida.
313        assert_eq!(urbe.zona(PLAZA + 1, 0), Zona::Avenida);
314    }
315
316    #[test]
317    fn el_casco_esta_nivelado_sin_escalones() {
318        // Terreno inclinado en rampa: el casco debe salir PLANO (sin escalones).
319        let pend = |x: i32, _z: i32| (10 + x / 3).max(1) as u32;
320        let urbe = Urbe::new(11, 0.8, 60);
321        let (bx, bz, _) = urbe.centro_celda(0, 0);
322        let datum = pend(bx, bz);
323        // Recorremos una franja de avenida (v=0) por el interior del casco: toda a
324        // la misma cota, sin saltos.
325        let mut previa: Option<u32> = None;
326        let mut escalones = 0;
327        for du in -urbe.radio_interior()..=urbe.radio_interior() {
328            if let Some(p) = urbe.consultar(bx + du, bz, &pend) {
329                assert_eq!(p.suelo, datum, "el casco no está nivelado en du={du}");
330                if let Some(pr) = previa {
331                    if p.suelo.abs_diff(pr) > 0 {
332                        escalones += 1;
333                    }
334                }
335                previa = Some(p.suelo);
336            }
337        }
338        assert_eq!(escalones, 0, "la avenida tiene escalones");
339    }
340
341    #[test]
342    fn la_avenida_tiene_calzada_y_linea() {
343        let urbe = Urbe::new(5, 0.7, 40);
344        // Sobre el eje de la avenida, más allá de la plaza, hay línea o asfalto.
345        let z = urbe.zona(0, PLAZA + 3);
346        assert_eq!(z, Zona::Avenida);
347    }
348}