Show number fields with three decimals and snap dragging to the step
Number fields derived their display precision from `step`, so `step: 1` (X/Y/Z, width, height, font size) rendered a stored 100.234 as "100" — and because `onChange` parses the text, touching the field wrote the rounded value back into the project. `formatNumber` now renders every number with three decimals, independent of `step`, which is left to control the scrub and arrow-key increment only. Dragging now snaps to the increment's grid (`snapToStep`), so a scrub at `step: 1` always lands on whole numbers even from a fractional start, while Alt (x0.1) and the 0.1-step fields keep their precision. Typed values and arrow keys are untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
15ffcd5739
commit
6168df41f8
6 files changed
+104
-21
No files matched your search
@@ -1,5 +1,5 @@
|
||||
import type { FederatedPointerEvent } from 'pixi.js';
|
||||
import { clamp } from '../math';
|
||||
import { clamp, formatNumber, snapToStep } from '../math';
|
||||
import { ui } from '../UIContext';
|
||||
import { TextField, type TextFieldOptions } from './TextField';
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface NumberFieldOptions extends Omit<TextFieldOptions, 'onInput' | '
|
||||
min?: number;
|
||||
max?: number;
|
||||
/**
|
||||
* Increment per scrubbed pixel and per arrow key press. Its magnitude also sets the display
|
||||
* precision: 0.1 shows one decimal, 1 shows none.
|
||||
* Increment per scrubbed pixel and per arrow key press, and the grid a scrub snaps to.
|
||||
* Does not affect the display, which always shows `NUMBER_DECIMALS` decimals.
|
||||
*/
|
||||
step?: number;
|
||||
/** Fires continuously while scrubbing or typing a valid number. */
|
||||
@@ -22,14 +22,16 @@ const SCRUB_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* Numeric input: click to type, drag horizontally to scrub (Shift = ×10, Alt = ×0.1),
|
||||
* arrow keys step. Values are clamped to min/max and formatted with the step's precision.
|
||||
* arrow keys step. A scrub snaps to the grid of its increment – a step-1 field therefore
|
||||
* always ends on a whole number, even when it started fractional. Values are clamped to
|
||||
* min/max and always shown with `NUMBER_DECIMALS` decimals, so the text never hides that
|
||||
* the value is fractional.
|
||||
*/
|
||||
export class NumberField extends TextField {
|
||||
private numeric: number;
|
||||
private readonly min: number;
|
||||
private readonly max: number;
|
||||
private readonly step: number;
|
||||
private readonly decimals: number;
|
||||
private scrub: { startX: number; startValue: number; moved: boolean } | null = null;
|
||||
onNumberInput?: (value: number) => void;
|
||||
onNumberChange?: (value: number) => void;
|
||||
@@ -39,11 +41,10 @@ export class NumberField extends TextField {
|
||||
this.min = options.min ?? -Infinity;
|
||||
this.max = options.max ?? Infinity;
|
||||
this.step = options.step ?? 1;
|
||||
this.decimals = clamp(Math.ceil(-Math.log10(this.step)), 0, 6);
|
||||
this.onNumberInput = options.onInput;
|
||||
this.onNumberChange = options.onChange;
|
||||
this.numeric = this.clampValue(value);
|
||||
this.valueText = this.format(this.numeric);
|
||||
this.valueText = formatNumber(this.numeric);
|
||||
this.cursor = 'ew-resize';
|
||||
|
||||
this.onInput = (text) => {
|
||||
@@ -56,7 +57,7 @@ export class NumberField extends TextField {
|
||||
this.onChange = (text) => {
|
||||
const n = Number(text.trim());
|
||||
this.numeric = this.clampValue(Number.isFinite(n) && text.trim() !== '' ? n : this.numeric);
|
||||
this.valueText = this.format(this.numeric);
|
||||
this.valueText = formatNumber(this.numeric);
|
||||
this.invalidate();
|
||||
this.onNumberChange?.(this.numeric);
|
||||
};
|
||||
@@ -66,8 +67,8 @@ export class NumberField extends TextField {
|
||||
const dx = e.global.x - this.scrub.startX;
|
||||
if (!this.scrub.moved && Math.abs(dx) < SCRUB_THRESHOLD) return;
|
||||
this.scrub.moved = true;
|
||||
const factor = e.shiftKey ? 10 : e.altKey ? 0.1 : 1;
|
||||
this.setNumber(this.scrub.startValue + dx * this.step * factor);
|
||||
const increment = this.step * (e.shiftKey ? 10 : e.altKey ? 0.1 : 1);
|
||||
this.setNumber(snapToStep(this.scrub.startValue + dx * increment, increment));
|
||||
this.onNumberInput?.(this.numeric);
|
||||
});
|
||||
this.onPointerRelease(() => {
|
||||
@@ -93,7 +94,7 @@ export class NumberField extends TextField {
|
||||
setNumber(value: number): void {
|
||||
this.numeric = this.clampValue(value);
|
||||
if (!this.isEditing) {
|
||||
this.valueText = this.format(this.numeric);
|
||||
this.valueText = formatNumber(this.numeric);
|
||||
this.invalidate();
|
||||
}
|
||||
}
|
||||
@@ -113,7 +114,7 @@ export class NumberField extends TextField {
|
||||
const dir = event.key === 'ArrowUp' ? 1 : -1;
|
||||
const factor = event.shiftKey ? 10 : event.altKey ? 0.1 : 1;
|
||||
this.numeric = this.clampValue(this.numeric + dir * this.step * factor);
|
||||
this.valueText = this.format(this.numeric);
|
||||
this.valueText = formatNumber(this.numeric);
|
||||
ui().keyboard.setValue(this.valueText, 0, this.valueText.length);
|
||||
this.selectAll();
|
||||
this.onNumberInput?.(this.numeric);
|
||||
@@ -129,8 +130,4 @@ export class NumberField extends TextField {
|
||||
if (!Number.isFinite(value)) return this.numeric ?? 0;
|
||||
return clamp(value, this.min, this.max);
|
||||
}
|
||||
|
||||
private format(value: number): string {
|
||||
return String(Number(value.toFixed(this.decimals)));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user