mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 14:18:04 +08:00
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:
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::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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -9,6 +9,7 @@ mod to_path;
|
||||
use convert_case::{Boundary, Converter, pattern};
|
||||
use core_types::graphene_hash::CacheHash;
|
||||
use core_types::list::{Item, List};
|
||||
use core_types::math::float_noise::round_away_float_noise;
|
||||
use core_types::registry::types::{SignedInteger, TextArea};
|
||||
use core_types::{CloneVarArgs, Context, Ctx, ExtractAll, ExtractVarArgs, OwnedContextImpl};
|
||||
use dyn_any::DynAny;
|
||||
@@ -299,6 +300,7 @@ fn format_number(
|
||||
start_at_10000: Item<bool>,
|
||||
) -> Item<String> {
|
||||
let (number, attributes) = number.into_parts();
|
||||
let number = round_away_float_noise(number);
|
||||
let (decimal_places, fixed_decimals, use_thousands_separator, start_at_10000) =
|
||||
(*decimal_places.element(), *fixed_decimals.element(), *use_thousands_separator.element(), *start_at_10000.element());
|
||||
let decimal_separator = decimal_separator.element().clone();
|
||||
|
||||
Reference in New Issue
Block a user