refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG

Reorganización física de crates/:
- core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/
- shared/ (3 crates) se redistribuye en protocol/ e init/
- lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/

Renames de proyectos:
- shipote → shuma (runtime de sandboxes)
- nouser → akasha (explorador de Mónadas)
- yahweh → nahual (motor GPUI, antes ui_engine/)
- lapaloma → pineal (data-viz agnóstica)

Fraccionamiento UI → core agnóstico:
- vista-core (DeckState + snap, 175 LOC, 5 tests verdes)
- barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes)
- vista-web y barra-web ahora son thin DOM bindings

Documentación nueva:
- 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat
  + 10 módulos + apps/
- docs/STATUS.md con cifras reales por proyecto
- docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas)
- CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets)

Automatización:
- scripts/reorg.py — script idempotente que: git mv directorios, renombra
  package names, recomputa path = refs, reescribe imports rust, actualiza
  workspace Cargo.toml. Soporta --dry-run.
- scripts/split-changelog.py — particiona CHANGELOG por componente.

Validación:
- cargo check --workspace pasa (124 crates + 2 nuevos cores).
- 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-19 14:48:34 +00:00
parent 86fb6ae20b
commit 550c98f275
375 changed files with 8512 additions and 7155 deletions
+177
View File
@@ -0,0 +1,177 @@
//! LTTB (Largest-Triangle-Three-Buckets) — downsampling preservador
//! de silueta para series cartesianas.
//!
//! Algoritmo: dividir `n` puntos en `k-2` buckets (los extremos se
//! mantienen siempre). Por cada bucket, elegir el punto que forma
//! el triángulo de área máxima con el último punto elegido y el
//! centroide del bucket siguiente. Costo total O(n). Output ≤ k.
//!
//! Knob práctico: `target ≈ width_px × 3`. Tres vértices por pixel,
//! el anti-aliasing rellena el resto.
/// Reduce `coords` (interleaved `[x,y,x,y,…]`) a a lo sumo `target`
/// puntos, escribiendo los **índices originales** seleccionados en
/// `out` (sin clearearlo: el caller decide).
///
/// Si `n <= target` o `target < 3`, devuelve todos los índices
/// `[0..n)`.
pub fn lttb_indices(coords: &[f32], target: usize, out: &mut Vec<usize>) {
let n = coords.len() / 2;
if n == 0 {
return;
}
if n <= target || target < 3 {
out.extend(0..n);
return;
}
lttb_in_range_indices(coords, 0, n, target, out);
}
/// Variante que opera sobre el rango `[start, end)` de un buffer
/// más grande. Los índices devueltos son **absolutos** (relativos
/// al `coords` original), no al sub-rango — esto le ahorra al caller
/// la corrección de offset después de un `SpatialIndex::range`.
pub fn lttb_in_range_indices(
coords: &[f32],
start: usize,
end: usize,
target: usize,
out: &mut Vec<usize>,
) {
debug_assert!(coords.len() % 2 == 0);
debug_assert!(start <= end && end <= coords.len() / 2);
let len = end - start;
if len == 0 {
return;
}
if len <= target || target < 3 {
out.extend(start..end);
return;
}
// Primero el extremo izquierdo.
out.push(start);
let bucket_size = (len - 2) as f64 / (target - 2) as f64;
let mut a = start; // último punto elegido
for i in 0..target - 2 {
// Bucket actual y siguiente, en índices absolutos.
let cur_lo = start + 1 + (i as f64 * bucket_size).floor() as usize;
let cur_hi = start + 1 + ((i + 1) as f64 * bucket_size).floor() as usize;
let next_lo = cur_hi.min(end);
let next_hi = (start + 1 + ((i + 2) as f64 * bucket_size).floor() as usize).min(end);
// Centroide del bucket siguiente. Si está vacío, fallback
// al último punto.
let (avg_x, avg_y) = if next_hi > next_lo {
let span = (next_hi - next_lo) as f32;
let mut sx = 0.0f32;
let mut sy = 0.0f32;
for j in next_lo..next_hi {
sx += coords[j * 2];
sy += coords[j * 2 + 1];
}
(sx / span, sy / span)
} else {
(coords[(end - 1) * 2], coords[(end - 1) * 2 + 1])
};
let ax = coords[a * 2];
let ay = coords[a * 2 + 1];
let mut max_area = -1.0f32;
let mut max_idx = cur_lo;
for j in cur_lo..cur_hi.min(end) {
let bx = coords[j * 2];
let by = coords[j * 2 + 1];
// Área del triángulo (sin /2 porque comparamos relativos).
let area = ((ax - avg_x) * (by - ay) - (ax - bx) * (avg_y - ay)).abs();
if area > max_area {
max_area = area;
max_idx = j;
}
}
out.push(max_idx);
a = max_idx;
}
// Extremo derecho.
out.push(end - 1);
}
/// Variante que materializa coords decimadas directamente — útil
/// cuando el painter sólo quiere un slice listo para `drawRawPoints`
/// y no necesita los índices.
pub fn lttb_coords(coords: &[f32], target: usize, out: &mut Vec<f32>) {
let mut idx_buf: Vec<usize> = Vec::with_capacity(target);
lttb_indices(coords, target, &mut idx_buf);
for i in idx_buf {
out.push(coords[i * 2]);
out.push(coords[i * 2 + 1]);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn no_decimate_si_n_menor_que_target() {
let coords: Vec<f32> = (0..5).flat_map(|i| [i as f32, (i * i) as f32]).collect();
let mut out = Vec::new();
lttb_indices(&coords, 10, &mut out);
assert_eq!(out, vec![0, 1, 2, 3, 4]);
}
#[test]
fn extremos_preservados() {
let n = 100;
let coords: Vec<f32> = (0..n).flat_map(|i| [i as f32, (i as f32).sin()]).collect();
let mut out = Vec::new();
lttb_indices(&coords, 10, &mut out);
assert_eq!(out.first(), Some(&0));
assert_eq!(out.last(), Some(&(n - 1)));
assert!(out.len() <= 10);
}
#[test]
fn indices_sorted_y_unicos() {
let coords: Vec<f32> = (0..1000)
.flat_map(|i| [i as f32, (i as f32 * 0.01).sin()])
.collect();
let mut out = Vec::new();
lttb_indices(&coords, 50, &mut out);
for w in out.windows(2) {
assert!(w[0] < w[1], "indices deben ser estrictamente crecientes");
}
}
#[test]
fn in_range_indices_son_absolutos() {
let n = 100;
let coords: Vec<f32> = (0..n).flat_map(|i| [i as f32, i as f32]).collect();
let mut out = Vec::new();
lttb_in_range_indices(&coords, 20, 80, 10, &mut out);
assert_eq!(out.first(), Some(&20));
assert_eq!(out.last(), Some(&79));
// ningún índice fuera del rango pedido
for &i in &out {
assert!(i >= 20 && i < 80);
}
}
#[test]
fn preserva_picos_extremos() {
// Señal plana con un pico al medio: LTTB debe agarrar el pico.
let mut coords: Vec<f32> = Vec::new();
for i in 0..200 {
coords.push(i as f32);
coords.push(if i == 100 { 10.0 } else { 0.0 });
}
let mut out = Vec::new();
lttb_indices(&coords, 20, &mut out);
assert!(out.contains(&100), "pico debe sobrevivir el downsample");
}
}