decla-red/shared/src/helper/finite.ts
2026-08-22 11:03:58 +01:00

60 lines
1.8 KiB
TypeScript

import { vec2 } from 'gl-matrix';
export const isFiniteNumber = (value: unknown): value is number =>
typeof value === 'number' && Number.isFinite(value);
/** A finite number within [min, max], or `fallback` if the input is unusable. */
export const finiteInRange = (
value: unknown,
min: number,
max: number,
fallback: number,
): number => {
if (!isFiniteNumber(value)) {
return fallback;
}
return Math.min(max, Math.max(min, value));
};
/**
* A finite 2-vector whose magnitude is within `maxMagnitude`, or `undefined` if
* the input is not a usable pair of finite numbers. Callers should treat
* `undefined` as "drop this command" rather than substituting a default, so a
* malformed action is ignored instead of being silently reinterpreted.
*/
export const finiteVec2 = (value: unknown, maxMagnitude: number): vec2 | undefined => {
if (!value || typeof value !== 'object') {
return undefined;
}
const candidate = value as ArrayLike<unknown>;
if (candidate.length < 2) {
return undefined;
}
const x = candidate[0];
const y = candidate[1];
if (!isFiniteNumber(x) || !isFiniteNumber(y)) {
return undefined;
}
const result = vec2.fromValues(x, y);
const length = vec2.length(result);
if (length > maxMagnitude) {
vec2.scale(result, result, maxMagnitude / length);
}
return result;
};
/**
* A display name that is safe to store and re-serialize. Anything that is not a
* string is coerced, because a non-string survives `.slice()` — an array name
* round-trips through the serializer and is revived as a class on every peer
* that receives it, throwing inside the deserializer's reviver.
*/
export const sanitizeName = (value: unknown, maxLength: number): string => {
const text = typeof value === 'string' ? value : String(value ?? '');
return text.slice(0, maxLength);
};