/** * Returns the number of digits after the decimal point. Handles both the * normal decimal notation (`"0.25"`) and the exponential notation that JS * produces for very small magnitudes (`"1e-8"`). */ export function getDecimalPrecision(num: number): number { if (num === 0) { return 0; } // Very small numbers (|num| < 1) get serialized as "1e-8", not "0.00000001". if (Math.abs(num) < 1) { const parts = num.toExponential().split("e-"); // biome-ignore lint/style/noNonNullAssertion: `toExponential` always yields a mantissa const mantissaDecimalPart = parts[0]!.split(".")[1]; return ( (mantissaDecimalPart ? mantissaDecimalPart.length : 0) + // biome-ignore lint/style/noNonNullAssertion: exponential form always has an exponent after "e-" Number.parseInt(parts[1]!, 10) ); } const decimalPart = num.toString().split(".")[1]; return decimalPart ? decimalPart.length : 0; } /** * Rounds `value` to the nearest multiple of `step`, offset by `min` — so the * quantization grid is always anchored on `min`, not on `0`. The result is * then re-quantized via `toFixed(precision)` to trim binary-float drift. */ export function roundValueToStep( value: number, step: number, min: number, ): number { const nearest = Math.round((value - min) / step) * step + min; return Number( nearest.toFixed( Math.max(getDecimalPrecision(step), getDecimalPrecision(min)), ), ); }