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');