Files
brahman/crates/modules/pineal/core/src/scale.rs
T
sergio 550c98f275 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>
2026-05-19 14:48:34 +00:00

154 lines
4.3 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Escalas value→pixel para series cartesianas.
//!
//! La proyección no se aplica sobre los datos (eso rompería el
//! P2 zero-alloc — habría que reescribir todo el buffer por frame).
//! Las escalas devuelven el `(scale_x, scale_y, translate_x,
//! translate_y)` que el painter mete en un transform GPU. Los
//! datos quedan intactos.
/// Trait común a Linear / Log / Time. Cada implementación traduce
/// un valor de dominio a posición normalizada `[0, 1]` (que luego
/// el painter mapea al pixel range del plot).
pub trait Scale {
fn to_norm(&self, value: f64) -> f64;
fn from_norm(&self, norm: f64) -> f64;
fn domain(&self) -> (f64, f64);
}
#[derive(Debug, Clone, Copy)]
pub struct LinearScale {
min: f64,
max: f64,
}
impl LinearScale {
pub fn new(min: f64, max: f64) -> Self {
debug_assert!(max > min, "LinearScale: max debe ser > min");
Self { min, max }
}
}
impl Scale for LinearScale {
fn to_norm(&self, v: f64) -> f64 {
(v - self.min) / (self.max - self.min)
}
fn from_norm(&self, n: f64) -> f64 {
self.min + n * (self.max - self.min)
}
fn domain(&self) -> (f64, f64) {
(self.min, self.max)
}
}
/// Escala logarítmica base e. `min` y `max` deben ser positivos.
#[derive(Debug, Clone, Copy)]
pub struct LogScale {
log_min: f64,
log_max: f64,
min: f64,
max: f64,
}
impl LogScale {
pub fn new(min: f64, max: f64) -> Self {
debug_assert!(min > 0.0 && max > min, "LogScale: 0 < min < max");
Self {
log_min: min.ln(),
log_max: max.ln(),
min,
max,
}
}
}
impl Scale for LogScale {
fn to_norm(&self, v: f64) -> f64 {
(v.ln() - self.log_min) / (self.log_max - self.log_min)
}
fn from_norm(&self, n: f64) -> f64 {
(self.log_min + n * (self.log_max - self.log_min)).exp()
}
fn domain(&self) -> (f64, f64) {
(self.min, self.max)
}
}
/// Escala temporal sobre epoch ms. Internamente lineal.
#[derive(Debug, Clone, Copy)]
pub struct TimeScale {
inner: LinearScale,
}
impl TimeScale {
pub fn new(min_epoch_ms: f64, max_epoch_ms: f64) -> Self {
Self {
inner: LinearScale::new(min_epoch_ms, max_epoch_ms),
}
}
}
impl Scale for TimeScale {
fn to_norm(&self, v: f64) -> f64 {
self.inner.to_norm(v)
}
fn from_norm(&self, n: f64) -> f64 {
self.inner.from_norm(n)
}
fn domain(&self) -> (f64, f64) {
self.inner.domain()
}
}
/// Wilkinson "nice numbers" — devuelve el step ideal en `{1, 2, 5} × 10^k`
/// para que un rango `[min, max]` tenga ~`target_ticks` divisiones.
pub fn nice_step(min: f64, max: f64, target_ticks: usize) -> f64 {
debug_assert!(max > min && target_ticks > 0);
let raw = (max - min) / target_ticks as f64;
let mag = 10f64.powf(raw.log10().floor());
let norm = raw / mag;
let nice = if norm < 1.5 {
1.0
} else if norm < 3.0 {
2.0
} else if norm < 7.0 {
5.0
} else {
10.0
};
nice * mag
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_roundtrip() {
let s = LinearScale::new(10.0, 20.0);
assert!((s.to_norm(15.0) - 0.5).abs() < 1e-9);
assert!((s.from_norm(0.5) - 15.0).abs() < 1e-9);
}
#[test]
fn log_roundtrip() {
let s = LogScale::new(1.0, 1000.0);
// 10 está a 1/3 del camino en log10. ln(10)/ln(1000) = 1/3.
assert!((s.to_norm(10.0) - 1.0 / 3.0).abs() < 1e-9);
assert!((s.from_norm(2.0 / 3.0) - 100.0).abs() < 1e-9);
}
#[test]
fn nice_step_es_potencia() {
// 100/5 = 20 — exact match para el branch nice=2.0 · mag=10.
assert!((nice_step(0.0, 100.0, 5) - 20.0).abs() < 1e-9);
// 1.0/10 = 0.1 — branch nice=1.0 · mag=0.1.
assert!((nice_step(0.0, 1.0, 10) - 0.1).abs() < 1e-9);
// 14/5 = 2.8 — branch nice=2.0 (1.5 ≤ norm < 3) · mag=1.
assert!((nice_step(0.0, 14.0, 5) - 2.0).abs() < 1e-9);
// 7/5 = 1.4 — cae bajo 1.5 → snap a 1.0 · mag=1 = 1.0.
assert!((nice_step(0.0, 7.0, 5) - 1.0).abs() < 1e-9);
// 50/5 = 10 — branch nice=10 · mag=1 = 10. (Equivalente a 1·10.)
assert!((nice_step(0.0, 50.0, 5) - 10.0).abs() < 1e-9);
}
}