Make Data panel scalars selectable inline, denoise float display, and standardize breadcrumb string truncation (#4470)

* Make Data panel scalars selectable inline, denoise float display, and move breadcrumb truncation to Rust

* Code review fixes

* Remove denoising from f32
This commit is contained in:
Keavon Chambers
2026-08-22 01:28:23 -07:00
committed by GitHub
parent 2fa6b743fe
commit c8667edacc
14 changed files with 253 additions and 187 deletions

View File

@@ -0,0 +1,71 @@
use std::fmt::Write;
/// Recovers the intended number from floating point imprecision noise when that can be done reliably, e.g. 0.30000000000000004 -> 0.3.
/// Rounding to each significant digit count from 1 to 12, the first candidate within a relative 1e-13 of the original is accepted.
/// Actual high-precision values (like 0.3333333333333333) never pass the tolerance and are returned unchanged.
/// f64 only, as f32 lacks precision to reliably distinguish between intentional digits and noise.
pub fn round_away_float_noise(value: f64) -> f64 {
if value == 0. || !value.is_finite() {
return if value == 0. { 0. } else { value };
}
// Candidates come from decimal formatting rather than scaling by a power of ten, which is inexact enough to invent
// noise of its own: it turns 1e300 into 9.999999999999999e299 and 999999.9999999 into 999999.9999999999.
// One buffer serves every candidate, since the digit counts are tried in turn.
let mut buffer = String::with_capacity(32);
for significant_digits in 1..=12 {
buffer.clear();
let _ = write!(buffer, "{value:.*e}", significant_digits - 1);
let Ok(rounded) = buffer.parse::<f64>() else { continue };
if ((rounded - value) / value).abs() < 1e-13 {
return rounded;
}
}
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn round_away_float_noise_snaps_noisy_values() {
assert_eq!(round_away_float_noise(0.1 + 0.2), 0.3);
assert_eq!(round_away_float_noise(0.3000000000000012), 0.3);
assert_eq!(round_away_float_noise(2.99999999999993), 3.);
assert_eq!(round_away_float_noise(45.00000000000001), 45.);
}
#[test]
fn round_away_float_noise_keeps_honest_values() {
assert_eq!(round_away_float_noise(1. / 3.), 1. / 3.);
assert_eq!(round_away_float_noise(0.2394023940209349), 0.2394023940209349);
assert_eq!(round_away_float_noise(0.25), 0.25);
assert_eq!(round_away_float_noise(-17.5), -17.5);
}
#[test]
fn round_away_float_noise_keeps_deliberate_values_with_zero_runs() {
assert_eq!(round_away_float_noise(0.30000005), 0.30000005);
assert_eq!(round_away_float_noise(0.3000000000001), 0.3000000000001);
assert_eq!(round_away_float_noise(1.00000001), 1.00000001);
assert_eq!(round_away_float_noise(2.9999993), 2.9999993);
}
#[test]
fn round_away_float_noise_normalizes_zero() {
let result = round_away_float_noise(-0.);
assert_eq!(result, 0.);
assert!(result.is_sign_positive());
}
#[test]
fn round_away_float_noise_keeps_extreme_magnitudes_exact() {
assert_eq!(round_away_float_noise(1e300), 1e300);
assert_eq!(round_away_float_noise(1.5e300), 1.5e300);
assert_eq!(round_away_float_noise(1e-300), 1e-300);
assert_eq!(round_away_float_noise(f64::MIN_POSITIVE), f64::MIN_POSITIVE);
}
}

View File

@@ -1,4 +1,5 @@
pub mod bbox;
pub mod float_noise;
pub mod polynomial;
pub mod quad;
pub mod rect;

View File

@@ -1,6 +1,7 @@
use crate::Node;
use crate::math::float_noise::round_away_float_noise;
use crate::transform::Footprint;
use glam::DVec2;
use glam::{DAffine2, DVec2};
use std::future::Future;
use std::marker::PhantomData;
@@ -45,11 +46,26 @@ pub trait Convert<T, C>: Sized {
fn convert(self, footprint: Footprint, converter: C) -> impl Future<Output = T> + Send;
}
impl<T: ToString + Send> Convert<String, ()> for T {
/// Converts this type into a `String` using its `ToString` implementation.
/// Implements the [`Convert`] trait for formatting a type into a `String` via [`ToString`].
macro_rules! impl_convert_to_string {
($($from:ty),* $(,)?) => {
$(
impl Convert<String, ()> for $from {
#[inline]
async fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
}
}
)*
};
}
impl_convert_to_string!(f32, u32, u64, i32, i64, bool, DVec2, DAffine2);
// Denoised so 0.1 + 0.2 reaches the string as "0.3" rather than "0.30000000000000004"
impl Convert<String, ()> for f64 {
#[inline]
async fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
round_away_float_noise(self).to_string()
}
}