From 4f546c0e08ed691028d1b60aa0db491da4f7e8a3 Mon Sep 17 00:00:00 2001 From: MelekTaus Date: Sat, 19 Sep 2026 14:29:00 +0200 Subject: [PATCH] Number fields drop trailing zeros: formatNumber still rounds to at most 3 decimals and then removes zeros at the end, so 100.000 reads 100 and 0.500 reads 0.5, everywhere (inspector, graph and shader nodes, duration and fps in the animation and tile tabs); a field's own decimals is now an upper limit, stored values are unchanged, -0 still shows as 0, and the graph's separate integer rule is gone (#172) Co-Authored-By: Claude Opus 5 --- src/editor/graph/NodeView.ts | 3 +-- src/ui/math.ts | 8 ++++---- src/ui/widgets/NumberField.ts | 4 ++-- tests/ui/math.test.ts | 31 ++++++++++++++++++++++++------- tools/e2e/22-struct.mjs | 5 ++++- tools/e2e/28-shader.mjs | 2 +- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/editor/graph/NodeView.ts b/src/editor/graph/NodeView.ts index f2bb28a9..94f9c289 100644 --- a/src/editor/graph/NodeView.ts +++ b/src/editor/graph/NodeView.ts @@ -488,8 +488,7 @@ function formatLive(output: PortDef, value: Value, project: Project | null): str } function formatValue(value: Value): string { - // Whole numbers stay bare to keep nodes narrow; fractions match the field that edits them. - if (typeof value === 'number') return Number.isInteger(value) ? String(value) : formatNumber(value); + if (typeof value === 'number') return formatNumber(value); if (typeof value === 'boolean') return value ? 'true' : 'false'; // A list can hold hundreds of items – shown whole it would stretch the node across the graph. if (isList(value)) { diff --git a/src/ui/math.ts b/src/ui/math.ts index b7d479a2..2052c524 100644 --- a/src/ui/math.ts +++ b/src/ui/math.ts @@ -12,13 +12,13 @@ export function centreY(containerHeight: number, itemHeight: number): number { export const NUMBER_DECIMALS = 3; /** - * Always shows `decimals` places, so a value can never look whole when it is not. A value that - * rounds to zero never carries a minus sign. + * Rounds to `decimals` places and drops trailing zeros: `100`, `0.5`, `9.091`. A value that rounds + * to zero never carries a minus sign. */ export function formatNumber(value: number, decimals = NUMBER_DECIMALS): string { - const zero = (0).toFixed(decimals); const text = value.toFixed(decimals); - return text === '-' + zero ? zero : text; + const trimmed = text.includes('.') ? text.replace(/\.?0+$/, '') : text; + return trimmed === '-0' ? '0' : trimmed; } /** Rounds to `decimals` places, which also removes binary-float noise (0.1 × 3 = 0.30000000000000004). */ diff --git a/src/ui/widgets/NumberField.ts b/src/ui/widgets/NumberField.ts index f7e0a4cf..d43572c5 100644 --- a/src/ui/widgets/NumberField.ts +++ b/src/ui/widgets/NumberField.ts @@ -9,8 +9,8 @@ export interface NumberFieldOptions extends Omit { }); describe('formatNumber', () => { - it('shows three decimals by default', () => { + it('shows at most three decimals by default and drops trailing zeros', () => { expect(NUMBER_DECIMALS).toBe(3); - expect(formatNumber(100)).toBe('100.000'); - expect(formatNumber(0.5)).toBe('0.500'); - expect(formatNumber(-12)).toBe('-12.000'); + expect(formatNumber(100)).toBe('100'); + expect(formatNumber(0.5)).toBe('0.5'); + expect(formatNumber(-12)).toBe('-12'); + expect(formatNumber(9.0909)).toBe('9.091'); + expect(formatNumber(2.1)).toBe('2.1'); + }); + + it('keeps the zeros of a whole number', () => { + expect(formatNumber(100, 0)).toBe('100'); + expect(formatNumber(1000)).toBe('1000'); + expect(formatNumber(-50, 2)).toBe('-50'); + }); + + it('drops the whole fraction once rounding leaves only zeros', () => { + expect(formatNumber(2.0004)).toBe('2'); + expect(formatNumber(1.9996)).toBe('2'); + expect(formatNumber(0.10004)).toBe('0.1'); }); it('never hides a fractional value behind a whole number', () => { @@ -50,13 +64,16 @@ describe('formatNumber', () => { }); it('drops the minus sign from a value that displays as zero', () => { - expect(formatNumber(-0)).toBe('0.000'); - expect(formatNumber(-0.0001)).toBe('0.000'); + expect(formatNumber(-0)).toBe('0'); + expect(formatNumber(-0.0001)).toBe('0'); + expect(formatNumber(-0.04, 1)).toBe('0'); }); - it('honours an explicit precision', () => { + it('honours an explicit precision as an upper bound', () => { expect(formatNumber(1.25, 1)).toBe('1.3'); expect(formatNumber(1.25, 0)).toBe('1'); + expect(formatNumber(1.5, 2)).toBe('1.5'); + expect(formatNumber(12, 2)).toBe('12'); }); it('shows a field of integral values without a decimal point at all', () => { diff --git a/tools/e2e/22-struct.mjs b/tools/e2e/22-struct.mjs index 4a55e7de..639cb205 100644 --- a/tools/e2e/22-struct.mjs +++ b/tools/e2e/22-struct.mjs @@ -46,7 +46,10 @@ export async function structVariable(t) { // 22a. The declaration is what the inspector draws: a head that can add fields, a row per field. check('a struct variable carries the add-a-field control', (await find('+ Field')) !== null); check('and a row for each declared field', (await find('x')) !== null && (await find('tag')) !== null); - check('whose value is shown, not a summary of the bundle', (await find('30.000')) !== null); + check( + 'whose value is shown, not a summary of the bundle', + (await find('30')) !== null && (await find('30.000')) === null, + ); const assetLefts = await leftsOf(['Name', 'Run in editor', 'x', 'tag']); check( 'a field value starts on the same column as every other row', diff --git a/tools/e2e/28-shader.mjs b/tools/e2e/28-shader.mjs index cd95ab5e..27553e7a 100644 --- a/tools/e2e/28-shader.mjs +++ b/tools/e2e/28-shader.mjs @@ -378,7 +378,7 @@ export async function shader(t) { await showTab('shader'); check( 'the Number node draws its value as a setting row', - (await json("__gm.shader.nodeSettings('sh-half')"))?.join() === 'Value: 0.500', + (await json("__gm.shader.nodeSettings('sh-half')"))?.join() === 'Value: 0.5', JSON.stringify(await json("__gm.shader.nodeSettings('sh-half')")), ); await shot('28-number-node');