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)
game-maker / gate (push) Successful in 1m55s
game-maker / suite (push) Successful in 6m50s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-19 14:29:00 +02:00
1 parent 47eea2f6e4
commit 4f546c0e08
6 files changed
+36 -17

No files matched your search

+1 -2
View File
@@ -488,8 +488,7 @@ function formatLive(output: PortDef, value: Value, project: Project | null): str
} }
function formatValue(value: Value): string { 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 formatNumber(value);
if (typeof value === 'number') return Number.isInteger(value) ? String(value) : formatNumber(value);
if (typeof value === 'boolean') return value ? 'true' : 'false'; 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. // A list can hold hundreds of items – shown whole it would stretch the node across the graph.
if (isList(value)) { if (isList(value)) {
+4 -4
View File
@@ -12,13 +12,13 @@ export function centreY(containerHeight: number, itemHeight: number): number {
export const NUMBER_DECIMALS = 3; 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 `decimals` places and drops trailing zeros: `100`, `0.5`, `9.091`. A value that rounds
* rounds to zero never carries a minus sign. * to zero never carries a minus sign.
*/ */
export function formatNumber(value: number, decimals = NUMBER_DECIMALS): string { export function formatNumber(value: number, decimals = NUMBER_DECIMALS): string {
const zero = (0).toFixed(decimals);
const text = value.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). */ /** Rounds to `decimals` places, which also removes binary-float noise (0.1 × 3 = 0.30000000000000004). */
+2 -2
View File
@@ -9,8 +9,8 @@ export interface NumberFieldOptions extends Omit<TextFieldOptions, 'onInput' | '
/** Increment per scrubbed pixel and per arrow key, and the grid a scrub snaps to. */ /** Increment per scrubbed pixel and per arrow key, and the grid a scrub snaps to. */
step?: number; step?: number;
/** /**
* Decimal places the value is kept *and* shown at (default `NUMBER_DECIMALS`) – the field rounds * Decimal places the value is kept at and shown with at most (default `NUMBER_DECIMALS`) – the
* to it, so lowering it drops precision rather than hiding it. * field rounds to it, so lowering it drops precision rather than hiding it.
*/ */
decimals?: number; decimals?: number;
/** Fires continuously while scrubbing or typing a valid number. */ /** Fires continuously while scrubbing or typing a valid number. */
+24 -7
View File
@@ -31,11 +31,25 @@ describe('centreY', () => {
}); });
describe('formatNumber', () => { 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(NUMBER_DECIMALS).toBe(3);
expect(formatNumber(100)).toBe('100.000'); expect(formatNumber(100)).toBe('100');
expect(formatNumber(0.5)).toBe('0.500'); expect(formatNumber(0.5)).toBe('0.5');
expect(formatNumber(-12)).toBe('-12.000'); 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', () => { 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', () => { it('drops the minus sign from a value that displays as zero', () => {
expect(formatNumber(-0)).toBe('0.000'); expect(formatNumber(-0)).toBe('0');
expect(formatNumber(-0.0001)).toBe('0.000'); 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, 1)).toBe('1.3');
expect(formatNumber(1.25, 0)).toBe('1'); 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', () => { it('shows a field of integral values without a decimal point at all', () => {
+4 -1
View File
@@ -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. // 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('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('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']); const assetLefts = await leftsOf(['Name', 'Run in editor', 'x', 'tag']);
check( check(
'a field value starts on the same column as every other row', 'a field value starts on the same column as every other row',
+1 -1
View File
@@ -378,7 +378,7 @@ export async function shader(t) {
await showTab('shader'); await showTab('shader');
check( check(
'the Number node draws its value as a setting row', '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')")), JSON.stringify(await json("__gm.shader.nodeSettings('sh-half')")),
); );
await shot('28-number-node'); await shot('28-number-node');