Round number fields to the precision they display

`decimals` only reached `formatNumber`, so it changed the label and nothing else: typing 640.5 into
a field with `decimals: 0` stored 640.5 and displayed 641. The two existing users got away with it
because rounding happened outside the widget (`normalizeFrameRect`, the slice dialog), which their
comments had to point out.

`NumberField.clampValue` now rounds before it clamps, and every path that sets a value goes through
it (typing, scrubbing, arrow keys, the constructor), so the stored number matches the shown one and
`min`/`max` stay exact. The rounding is `roundTo` in math.ts, which `snapToStep` already did inline.

With that safe, camera `View width`/`View height` and text `Font size` become integers – they were
showing 640.000 and 32.000. Everything else that looked integral is not: positions, sprite and
collider sizes and zoom are all scrubbed, and `z` is a summed coordinate (walk.ts), not a layer.

Verified remotely against the RTX 4070 host: 270 checks, all passed (step 10 gained a check that the
field text has no decimal point and survives a reopen). `npm run check` green, 407 tests.
This commit is contained in:
root committed 2026-09-09 10:11:06 +02:00
1 parent 6cf47fd2ae
commit 1b16a55ffc
8 files changed
+80 -19

No files matched your search

+28 -1
View File
@@ -1,5 +1,13 @@
import { describe, expect, it } from 'vitest';
import { clamp, centreY, formatNumber, NUMBER_DECIMALS, snapToStep, zoomFactor } from '../../src/ui/math';
import {
clamp,
centreY,
formatNumber,
NUMBER_DECIMALS,
roundTo,
snapToStep,
zoomFactor,
} from '../../src/ui/math';
describe('clamp', () => {
it('keeps a value inside the range', () => {
@@ -60,6 +68,25 @@ describe('formatNumber', () => {
});
});
describe('roundTo', () => {
it('keeps a value at the precision its field shows', () => {
// Why the field rounds instead of only formatting: a `decimals: 0` field used to display
// "641" while the project still held 640.5.
expect(roundTo(640.5, 0)).toBe(641);
expect(roundTo(32.4, 0)).toBe(32);
expect(roundTo(1.2345)).toBe(1.234);
});
it('removes binary-float noise', () => {
expect(roundTo(0.30000000000000004)).toBe(0.3);
});
it('passes non-finite values through', () => {
expect(roundTo(NaN)).toBeNaN();
expect(roundTo(Infinity)).toBe(Infinity);
});
});
describe('snapToStep', () => {
it('lands on whole numbers for a step-1 field', () => {
// The bug: a scrub added fractional device pixels to an already fractional value, so