mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
Make Data panel scalars selectable inline, denoise float display, and standardize breadcrumb string truncation (#4470)
This commit is contained in:
committed by
Dennis Kobert
parent
704af633b7
commit
8d2645e20c
71
node-graph/libraries/core-types/src/math/float_noise.rs
Normal file
71
node-graph/libraries/core-types/src/math/float_noise.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod bbox;
|
||||
pub mod float_noise;
|
||||
pub mod polynomial;
|
||||
pub mod quad;
|
||||
pub mod rect;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user