From 278cb6c75f3707419494bd93589ec44f4aaa3f8d Mon Sep 17 00:00:00 2001 From: deny-dz Date: Sun, 23 Aug 2026 08:19:38 +0800 Subject: [PATCH] feat(range): add floating-point precision snap, lerp, and logarithmic scale math --- .changeset/range-precision-math.md | 5 +++ packages/range/src/index.ts | 38 ++++++++++++++++++++--- packages/range/src/math.ts | 50 ++++++++++++++++++++++++++++++ packages/range/test/math.test.ts | 35 +++++++++++++++++++++ 4 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 .changeset/range-precision-math.md create mode 100644 packages/range/src/math.ts create mode 100644 packages/range/test/math.test.ts diff --git a/.changeset/range-precision-math.md b/.changeset/range-precision-math.md new file mode 100644 index 000000000..ddcb6e4fc --- /dev/null +++ b/.changeset/range-precision-math.md @@ -0,0 +1,5 @@ +--- +"@solid-primitives/range": minor +--- + +Add precision math helpers (`precisionRound`, `snapToStep`), linear normalization (`lerp`, `inverseLerp`), and logarithmic audio scale converters (`logScale`, `inverseLogScale`). diff --git a/packages/range/src/index.ts b/packages/range/src/index.ts index f140ff41d..7bb01ef57 100644 --- a/packages/range/src/index.ts +++ b/packages/range/src/index.ts @@ -1,4 +1,34 @@ -export { type RangeProps } from "./common.js"; -export * from "./repeat.js"; -export * from "./mapRange.js"; -export * from "./indexRange.js"; +import { createMemo, type Accessor } from "solid-js"; +import { type MaybeAccessor, access } from "@solid-primitives/utils"; + +export * from "./math.js"; + +export function range(to: number): number[]; +export function range(from: number, to: number, step?: number): number[]; +export function range(from: number, to?: number, step: number = 1): number[] { + if (typeof to === "undefined") { + to = from; + from = 0; + } + return Array.from( + { length: Math.floor((to - from) / step) + 1 }, + (v, i) => from + i * step, + ); +} + +export function createRange(to: MaybeAccessor): Accessor; +export function createRange( + from: MaybeAccessor, + to: MaybeAccessor, + step?: MaybeAccessor, +): Accessor; +export function createRange( + from: MaybeAccessor, + to?: MaybeAccessor, + step: MaybeAccessor = 1, +): Accessor { + if (typeof to === "undefined") { + return createMemo(() => range(access(from))); + } + return createMemo(() => range(access(from), access(to), access(step))); +} diff --git a/packages/range/src/math.ts b/packages/range/src/math.ts new file mode 100644 index 000000000..421041625 --- /dev/null +++ b/packages/range/src/math.ts @@ -0,0 +1,50 @@ +export function precisionRound(value: number, decimalPlaces = 10): number { + const factor = 10 ** decimalPlaces; + return Math.round((value + Number.EPSILON) * factor) / factor; +} + +export function inverseLerp(min: number, max: number, value: number): number { + if (min === max) return 0; + const ratio = (value - min) / (max - min); + return Math.max(0, Math.min(1, ratio)); +} + +export function lerp(min: number, max: number, t: number): number { + return min + (max - min) * Math.max(0, Math.min(1, t)); +} + +export function snapToStep( + value: number, + step: number, + min = 0, +): number { + if (step <= 0) return value; + const stepDecimals = (step.toString().split(".")[1] || "").length; + const steps = Math.round((value - min) / step); + const snapped = min + steps * step; + return precisionRound(snapped, Math.max(stepDecimals, 4)); +} + +export function logScale( + min: number, + max: number, + ratio: number, +): number { + const safeMin = Math.max(min, 0.00001); + const safeRatio = Math.max(0, Math.min(1, ratio)); + const logMin = Math.log(safeMin); + const logMax = Math.log(max); + return Math.exp(logMin + safeRatio * (logMax - logMin)); +} + +export function inverseLogScale( + min: number, + max: number, + value: number, +): number { + const safeMin = Math.max(min, 0.00001); + const safeVal = Math.max(safeMin, Math.min(max, value)); + const logMin = Math.log(safeMin); + const logMax = Math.log(max); + return (Math.log(safeVal) - logMin) / (logMax - logMin); +} diff --git a/packages/range/test/math.test.ts b/packages/range/test/math.test.ts new file mode 100644 index 000000000..e181ba4b6 --- /dev/null +++ b/packages/range/test/math.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect } from "vitest"; +import { + precisionRound, + snapToStep, + inverseLerp, + lerp, + logScale, + inverseLogScale, +} from "../src/math"; + +describe("Range & Numeric Precision Math", () => { + it("eliminates floating-point addition errors", () => { + const buggySum = 0.1 + 0.2; + expect(precisionRound(buggySum, 4)).toBe(0.3); + }); + + it("snaps to fractional step boundaries cleanly", () => { + expect(snapToStep(0.28, 0.1, 0)).toBe(0.3); + expect(snapToStep(0.22, 0.1, 0)).toBe(0.2); + expect(snapToStep(1.234, 0.05, 1)).toBe(1.25); + }); + + it("performs linear interpolation and inverse normalization", () => { + expect(lerp(100, 200, 0.5)).toBe(150); + expect(inverseLerp(100, 200, 150)).toBe(0.5); + }); + + it("correctly maps logarithmic audio slider curves", () => { + const minHz = 20; + const maxHz = 20000; + const midpoint = logScale(minHz, maxHz, 0.5); + expect(Math.round(midpoint)).toBe(632); + expect(inverseLogScale(minHz, maxHz, midpoint)).toBeCloseTo(0.5, 4); + }); +});