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

This commit is contained in:
Keavon Chambers
2026-09-15 23:04:33 +02:00
committed by Dennis Kobert
parent 704af633b7
commit 8d2645e20c
14 changed files with 255 additions and 188 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::list::{Attribute, AttributeDyn, AttributeValueDyn, Item, List, ListDyn};
use crate::math::float_noise::round_away_float_noise;
use crate::transform::Footprint;
use glam::DVec2;
use glam::{DAffine2, DVec2, IVec2};
use graphene_hash::CacheHash;
/// The [`Convert`] trait allows for conversion between Rust primitive numeric types.
@@ -17,11 +18,26 @@ pub trait ConvertAsync<T, C>: Sized {
fn convert(self, footprint: Footprint, converter: C) -> crate::runtime::SourceFuture<T>;
}
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]
fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
}
}
)*
};
}
impl_convert_to_string!(f32, i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, isize, usize, bool, String, DVec2, IVec2, DAffine2);
// Denoised so 0.1 + 0.2 reaches the string as "0.3" rather than "0.30000000000000004"
impl Convert<String, ()> for f64 {
#[inline]
fn convert(self, _: Footprint, _converter: ()) -> String {
self.to_string()
round_away_float_noise(self).to_string()
}
}

View File

@@ -67,6 +67,7 @@ pub mod repeat {
}
pub mod math {
pub use core_types::math::float_noise;
pub use core_types::math::quad;
pub mod math_ext {

View File

@@ -11,6 +11,7 @@ use convert_case::{Boundary, Converter, pattern};
use core_types::extent::{LevelIn, ListIn, ValueIn};
use core_types::gpoll::{Extent, GPoll, GraphError, Interrupt};
use core_types::graphene_hash::CacheHash;
use core_types::math::float_noise::round_away_float_noise;
use core_types::node::Lane;
use core_types::registry::types::{SignedInteger, TextArea};
use core_types::{Ctx, ExtractIndex, InjectIndex};
@@ -287,6 +288,8 @@ fn format_number(
#[name("Start at 10,000")]
start_at_10000: bool,
) -> String {
// Denoise before formatting so 0.1 + 0.2 reads as "0.3" rather than "0.30000000000000004"
let number = round_away_float_noise(number);
// Find the maximum meaningful decimal precision by detecting where float noise begins.
// This works correctly whether the value originated as f32 or f64, since we find the
// shortest decimal representation that round-trips back to the same f64 value.