Improve number fields/labels in the UI to filter out floating point noise and truncate at correct precision (#4363)

* Improve the displayed precision of float number labels

* Show actual tiny values in high-precision number fields
This commit is contained in:
Keavon Chambers
2026-09-11 01:53:15 +00:00
committed by Dennis Kobert
parent 4604b19576
commit 9b75f52a02
7 changed files with 153 additions and 20 deletions
@@ -568,7 +568,37 @@ pub fn wrap_to_tau(angle: f64) -> f64 {
}
pub fn format_rounded(value: f64, precision: usize) -> String {
format!("{value:.precision$}").trim_end_matches('0').trim_end_matches('.').to_string()
// Denoised values within floating point noise of zero (including -0) display as unsigned zero, unless the precision is fine enough to display them
let value = round_away_float_noise(value);
let value = if value.abs() < f64::min(1e-12, 0.5 * 10_f64.powi(-(precision as i32))) { 0. } else { value };
let formatted = format!("{value:.precision$}");
// Trailing zeros are trimmed only when the display is exact, so a truncated value keeps its decimal places (like "0.00" or "3.10") to indicate the truncation
if formatted.parse::<f64>() == Ok(value) {
formatted.trim_end_matches('0').trim_end_matches('.').to_string()
} else {
formatted
}
}
/// 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.
pub fn round_away_float_noise(value: f64) -> f64 {
if value == 0. || !value.is_finite() {
return if value == 0. { 0. } else { value };
}
let exponent = value.abs().log10().floor() as i32;
for significant_digits in 1..=12 {
let scale = 10_f64.powi(significant_digits - 1 - exponent);
let rounded = (value * scale).round() / scale;
if ((rounded - value) / value).abs() < 1e-13 {
return rounded;
}
}
value
}
/// Gives the approximated angle to display in degrees, given an angle in degrees.
@@ -610,3 +640,78 @@ pub fn extract_grid_parameters(layer: LayerNodeIdentifier, document: &DocumentMe
Some((grid_type, spacing, columns, rows, angles))
}
#[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 format_rounded_trims_trailing_zeros_when_exact() {
assert_eq!(format_rounded(0.25, 2), "0.25");
assert_eq!(format_rounded(3.1, 2), "3.1");
assert_eq!(format_rounded(45., 2), "45");
}
#[test]
fn format_rounded_keeps_decimal_places_when_truncated() {
assert_eq!(format_rounded(0.0003, 2), "0.00");
assert_eq!(format_rounded(-0.0003, 2), "-0.00");
assert_eq!(format_rounded(0.0003, 3), "0.000");
assert_eq!(format_rounded(3.10001, 2), "3.10");
assert_eq!(format_rounded(45.001, 2), "45.00");
assert_eq!(format_rounded(std::f64::consts::PI, 2), "3.14");
}
#[test]
fn format_rounded_denoises_before_judging_exactness() {
assert_eq!(format_rounded(29.999999999999996, 2), "30");
assert_eq!(format_rounded(45.00000000000001, 2), "45");
}
#[test]
fn format_rounded_shows_true_and_noise_zeros_plainly() {
assert_eq!(format_rounded(0., 2), "0");
assert_eq!(format_rounded(-0., 2), "0");
assert_eq!(format_rounded(1e-15, 2), "0");
assert_eq!(format_rounded(-1e-15, 2), "0");
}
#[test]
fn format_rounded_keeps_tiny_values_at_displayable_precision() {
assert_eq!(format_rounded(5e-13, 20), "0.0000000000005");
assert_eq!(format_rounded(-5e-13, 20), "-0.0000000000005");
assert_eq!(format_rounded(0., 20), "0");
assert_eq!(format_rounded(-0., 20), "0");
}
}
@@ -8,6 +8,7 @@ use crate::messages::portfolio::document::utility_types::transformation::{Axis,
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::pivot::{PivotGizmo, PivotGizmoType};
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::format_rounded;
use crate::messages::tool::tool_messages::select_tool;
use crate::messages::tool::tool_messages::tool_prelude::Key;
use crate::messages::tool::utility_types::{ToolData, ToolType};
@@ -217,7 +218,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
let format_rounded = |value: f64, precision: usize| {
if self.typing.digits.is_empty() || !self.transform_operation.can_begin_typing() {
format!("{value:.precision$}").trim_end_matches('0').trim_end_matches('.').to_string()
format_rounded(value, precision)
} else {
self.typing.string.clone()
}