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-07-23 00:22:13 -07:00
committed by Dennis Kobert
parent 31239f77c6
commit f4609e0e3b
7 changed files with 153 additions and 20 deletions

View File

@@ -3,6 +3,7 @@
import { preventEscapeClosingParentFloatingMenu } from "/src/components/layout/FloatingMenu.svelte";
import FieldInput from "/src/components/widgets/inputs/FieldInput.svelte";
import { PRESS_REPEAT_DELAY_MS, PRESS_REPEAT_INTERVAL_MS } from "/src/managers/input";
import { roundAwayFloatNoise } from "/src/utility-functions/numbers";
import { browserVersion } from "/src/utility-functions/platform";
import type { ActionShortcut, EditorWrapper, NumberInputIncrementBehavior, NumberInputMode } from "/wrapper/pkg/graphite_wasm_wrapper";
@@ -213,9 +214,19 @@
function displayText(displayValue: number | undefined, unit: string): string {
if (displayValue === undefined) return "-";
const roundingPower = 10 ** Math.max(displayDecimalPlaces, 0);
const decimalPlaces = Math.max(displayDecimalPlaces, 0);
const roundingPower = 10 ** decimalPlaces;
// Values within floating point noise of zero (including -0) display as unsigned zero, unless the field's decimal precision is fine enough to display them
const effectiveValue = Math.abs(displayValue) < Math.min(1e-12, 0.5 / roundingPower) ? 0 : roundAwayFloatNoise(displayValue);
const unitlessDisplayValue = Math.round(effectiveValue * roundingPower) / roundingPower;
// 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 (unitlessDisplayValue !== effectiveValue) {
const sign = unitlessDisplayValue === 0 && effectiveValue < 0 ? "-" : "";
return `${sign}${unitlessDisplayValue.toFixed(decimalPlaces)}${unPluralize(unit, displayValue)}`;
}
const unitlessDisplayValue = Math.round(displayValue * roundingPower) / roundingPower;
return `${unitlessDisplayValue}${unPluralize(unit, displayValue)}`;
}
@@ -230,9 +241,8 @@
// ===========================
function onTextFocused() {
// The degree of precision allowed in the number that's shown when editing the number field, where additional precision is removed to round out floating point errors.
const MAX_PRECISION = 12;
const noFloatingImprecisionValue = value === undefined ? undefined : Number(value.toPrecision(MAX_PRECISION));
// The number shown when editing the field, with floating point imprecision noise removed
const noFloatingImprecisionValue = value === undefined ? undefined : roundAwayFloatNoise(value);
if (value === undefined) text = "";
else if (unitIsHiddenWhenEditing) text = `${noFloatingImprecisionValue}`;

View File

@@ -0,0 +1,15 @@
// 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.
export function roundAwayFloatNoise(value: number): number {
if (value === 0 || !Number.isFinite(value)) return value === 0 ? 0 : value;
const exponent = Math.floor(Math.log10(Math.abs(value)));
for (let significantDigits = 1; significantDigits <= 12; significantDigits += 1) {
const scale = 10 ** (significantDigits - 1 - exponent);
const rounded = Math.round(value * scale) / scale;
if (Math.abs((rounded - value) / value) < 1e-13) return rounded;
}
return value;
}