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