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:
1 parent
6cf47fd2ae
commit
1b16a55ffc
8 files changed
+80
-19
No files matched your search
+7
-3
@@ -21,14 +21,18 @@ export function formatNumber(value: number, decimals = NUMBER_DECIMALS): string
|
||||
return text === '-' + zero ? zero : text;
|
||||
}
|
||||
|
||||
/** Rounds to `decimals` places, which also removes binary-float noise (0.1 × 3 = 0.30000000000000004). */
|
||||
export function roundTo(value: number, decimals = NUMBER_DECIMALS): number {
|
||||
return Number.isFinite(value) ? Number(value.toFixed(decimals)) : value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rounds `value` onto the grid of `stepSize`, so scrubbing lands on clean values even though
|
||||
* pointer deltas are fractional. The `toFixed` pass removes binary-float noise (0.1 × 3 =
|
||||
* 0.30000000000000004).
|
||||
* pointer deltas are fractional.
|
||||
*/
|
||||
export function snapToStep(value: number, stepSize: number): number {
|
||||
if (!(stepSize > 0) || !Number.isFinite(value)) return value;
|
||||
return Number((Math.round(value / stepSize) * stepSize).toFixed(NUMBER_DECIMALS));
|
||||
return roundTo(Math.round(value / stepSize) * stepSize);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FederatedPointerEvent } from 'pixi.js';
|
||||
import { clamp, formatNumber, NUMBER_DECIMALS, snapToStep } from '../math';
|
||||
import { clamp, formatNumber, NUMBER_DECIMALS, roundTo, snapToStep } from '../math';
|
||||
import { ui } from '../UIContext';
|
||||
import { TextField, type TextFieldOptions } from './TextField';
|
||||
|
||||
@@ -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. */
|
||||
step?: number;
|
||||
/**
|
||||
* Decimal places the text shows (default `NUMBER_DECIMALS`). Lower it only where the stored
|
||||
* value has that precision too, or an edit commits the rounded number back into the project.
|
||||
* Decimal places the value is kept *and* shown at (default `NUMBER_DECIMALS`) – the field rounds
|
||||
* to it, so lowering it drops precision rather than hiding it.
|
||||
*/
|
||||
decimals?: number;
|
||||
/** Fires continuously while scrubbing or typing a valid number. */
|
||||
@@ -128,11 +128,14 @@ export class NumberField extends TextField {
|
||||
return super.onBridgeKey(event);
|
||||
}
|
||||
|
||||
/** Clamps to min/max; NaN/Infinity keep the current value (0 before the first assignment). */
|
||||
/**
|
||||
* Rounds to `decimals`, then clamps so the bounds stay exact; NaN/Infinity keep the current value
|
||||
* (0 before the first assignment). Every path that sets the value goes through here.
|
||||
*/
|
||||
private clampValue(value: number): number {
|
||||
// `?? 0` is not dead despite the type: the constructor calls this before assigning `numeric`.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (!Number.isFinite(value)) return this.numeric ?? 0;
|
||||
return clamp(value, this.min, this.max);
|
||||
return clamp(roundTo(value, this.decimals), this.min, this.max);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user