chore: documentation and optimization

What a rhyme.
This commit is contained in:
2026-07-11 01:46:43 +02:00
parent ba5595a783
commit aabc1fa1b1
14 changed files with 1009 additions and 368 deletions
+11
View File
@@ -0,0 +1,11 @@
import * as esbuild from "esbuild";
const result = esbuild.buildSync({
entryPoints: ["src/index.ts"],
bundle: true,
minify: true,
outdir: "dist",
define: {
"import.meta.vitest": "undefined",
},
});
+165
View File
@@ -0,0 +1,165 @@
import { writeFileSync } from "fs";
import { relative } from "path";
function capitalize(/** @type {string} */ s) {
return s.slice(0, 1).toUpperCase() + s.slice(1).toLowerCase();
}
const script = import.meta.filename;
const path = "./src/mixin/math/swizzlePermutations.d.ts";
const axis = ["x", "y", "z", "w", "v"];
const property = ["first", "second", "third", "fourth", "fifth"];
const mixin = "./src/mixin/base/mixin";
let conditional = "";
for (let n = 0; n < axis.length; n++) {
if (n === axis.length - 1) {
conditional += `Swizzle${n + 1}D<T, t>`;
break;
}
const type = new Array(n + 1).fill("number").join(", ");
const indent = "\t".repeat(n + 1);
conditional += `T extends readonly [${type}]\n`;
conditional += `${indent}? Swizzle${n + 1}D<T, t>\n`;
conditional += `${indent}: `;
}
const type = new Array(axis.length).fill("number").join(", ");
let src = `
// .d.ts file auto-generated by ${relative(path, script)}
// Do not edit
import type { Props, Return } from '${relative(path, mixin)}';
type IsValid<T extends readonly number[]> = number extends T['length']
? false
: T extends readonly []
? false
: true;
export type SwizzleProps<T extends readonly number[], t extends Props> =
IsValid<T> extends false
? {}
: ${conditional
.split("\n")
.map((line) => "\t\t" + line)
.join("\n")
.trimStart()};
export interface ISwizzle<
T extends readonly [${type}] =
readonly [${type}],
t extends Props = Props
> extends Swizzle${axis.length}D<T, t> {};
`.trimStart();
for (let n = 0; n < axis.length; n++) {
const state = new Array(axis.length).fill(-1);
const props = [];
outer: do {
for (let i = state.length - 1; i >= 0; i--)
if (++state[i] > n) {
state[i] = -1;
} else break;
let nulled = false;
for (const item of state) {
if (item === -1) {
nulled = true;
continue;
}
if (nulled) continue outer;
}
if (state.every((x) => x === -1)) break;
const key = state
.filter((x) => x !== -1)
.map((x) => axis[x])
.join("");
if (state.slice(1).every((x) => x === -1)) {
const arr = [5, 3, 8, 2, 1].map((x) =>
Math.abs(x - n - state[0]),
);
props.push({
doc: `
/**
* Vector-style accessor, ${property[state[0]]} property of the value
* @from {@link Math \`Math\`}
* @example
* \`\`\`ts
* const vec = [${arr
.slice(0, Math.max(n + 1, 2))
.map((n) => n.toString())
.join(", ")}] as const;
*
* const ${axis[state[0]]} = $(vec).${axis[state[0]]}.value;
* expect(${axis[state[0]]}).toBe(${arr[state[0]]})
* \`\`\`
*/`.trimStart(),
property: `readonly ${key}: Return<T[${state[0]}], t>`,
});
continue;
}
let count = 0;
if (state.every((x) => x === count++)) continue;
const arr = [6, 2, 3, 5, 4, 7].map((x, idx) =>
Math.abs(x - state[idx]),
);
const type = state
.filter((x) => x !== -1)
.map((x) => `T[${x}]`)
.join(", ");
props.push({
doc: `
/**
* Vector-style swizzle accessor, rearannges the elements of the value
*
* Equivalent to \`[${state
.filter((x) => x !== -1)
.map((x) => `vec[${x}]`)
.join(", ")}]\`
* @from {@link Math \`Math\`}
* @example
* \`\`\`ts
* const vec = [${arr.slice(0, n + 1).join(", ")}] as const;
*
* const ${key} = $(vec).${key}().value;
* expect(${key}).toMatchObject([${state
.filter((x) => x !== -1)
.map((x) => arr[x])
.join(", ")}])
* \`\`\`
*/
`.trimStart(),
property: `readonly ${key}: () => Return<[${type}], t>`,
});
} while (state.some((x) => x !== -1));
props.sort((a, b) => a.property.length - b.property.length);
src += `interface Swizzle${n + 1}D<T extends readonly [${new Array(n + 1).fill("number").join(", ")}], t extends Props> {\n`;
src += `${props
.map((x) =>
[x.doc, x.property]
.join("\n")
.split("\n")
.map((line) => "\t" + line)
.join("\n"),
)
.join("\n")}\n`;
src += "}\n";
src += "\n";
}
writeFileSync(path, src);
+2
View File
@@ -1,3 +1,5 @@
src/**/*.d.ts
# Logs # Logs
logs logs
*.log *.log
+1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from "gen-doctests/config";
export default defineConfig({ export default defineConfig({
include: ["src/**/*.{js,ts}"], include: ["src/**/*.{js,ts}"],
exclude: ["src/**/*.d.ts"],
outDir: "tests/generated", outDir: "tests/generated",
templateHeader: [ templateHeader: [
"import { test, expect, expectTypeOf, describe, vi } from 'vitest'", "import { test, expect, expectTypeOf, describe, vi } from 'vitest'",
+1 -1
View File
@@ -3,7 +3,7 @@ import tseslint from "typescript-eslint";
import { defineConfig, globalIgnores } from "eslint/config"; import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([ export default defineConfig([
globalIgnores(["dist", "tests/generated", "coverage"]), globalIgnores(["dist", "tests/generated", "coverage", "*.d.ts"]),
{ {
files: ["**/*.{ts,tsx}"], files: ["**/*.{ts,tsx}"],
extends: [ extends: [
+7 -4
View File
@@ -4,11 +4,14 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"test": "gen-doctests && vitest --run --reporter=tree --coverage --typecheck", "test": "gen-doctests && vitest --run --reporter=tree --typecheck --cache",
"build": "tsc -b && esbuild --minify --bundle src/index.ts --outdir=dist --define:import.meta.vitest=undefined", "coverage": "gen-doctests && vitest --run --reporter=dot --coverage --cache",
"build:generate": "node .config/generateSwizzle.js",
"build": "tsc -b && node .config/build.js",
"fmt": "prettier --write .", "fmt": "prettier --write .",
"lint": "eslint .", "lint": "eslint .",
"preview": "vite preview" "prepublish": "pnpm build:generate",
"prepack": "pnpm test && pnpm build"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -21,7 +24,7 @@
"jiti": "^2.7.0", "jiti": "^2.7.0",
"madge": "^8.0.0", "madge": "^8.0.0",
"prettier": "^3.8.4", "prettier": "^3.8.4",
"typescript": "6", "typescript": "^6.0.3",
"typescript-eslint": "^8.59.2", "typescript-eslint": "^8.59.2",
"vitest": "^4.1.9" "vitest": "^4.1.9"
} }
+1 -1
View File
@@ -39,7 +39,7 @@ importers:
specifier: ^3.8.4 specifier: ^3.8.4
version: 3.8.4 version: 3.8.4
typescript: typescript:
specifier: '6' specifier: ^6.0.3
version: 6.0.3 version: 6.0.3
typescript-eslint: typescript-eslint:
specifier: ^8.59.2 specifier: ^8.59.2
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Fluent } from "."; import type { Fluent } from ".";
import { never } from "../internal"; import { phantom } from "../internal";
import type { Registry } from "../registry"; import type { Registry } from "../registry";
import type { RecPartial } from "../utility"; import type { RecPartial } from "../utility";
import type { HKT } from "./hkt"; import type { HKT } from "./hkt";
@@ -29,8 +29,8 @@ export function Mixin<I, T extends MixinHKT<I>>(
fn: MixinFn<I>, fn: MixinFn<I>,
): Mixin<I, T> { ): Mixin<I, T> {
return { return {
interface: never, interface: phantom,
hkt: never, hkt: phantom,
fn, fn,
}; };
} }
+1 -1
View File
@@ -5,7 +5,7 @@ export interface Identity extends HKT {
} }
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
export const never = undefined as never; export const phantom = undefined as never;
export function isArray(v: unknown): v is unknown[] { export function isArray(v: unknown): v is unknown[] {
return typeof v === "object" && v !== null && Array.isArray(v); return typeof v === "object" && v !== null && Array.isArray(v);
+21 -3
View File
@@ -6,8 +6,9 @@ import {
type Input, type Input,
type Props, type Props,
type Return, type Return,
type Shim,
} from "../base/mixin"; } from "../base/mixin";
import { assert, never } from "../internal"; import { assert, phantom } from "../internal";
import { Base } from "./base"; import { Base } from "./base";
interface AwaitedIdentitity extends HKT { interface AwaitedIdentitity extends HKT {
@@ -21,7 +22,7 @@ class Awaited<T extends Promise<unknown>> {
} }
public get [shim]() { public get [shim]() {
return never as { return phantom as {
input: T extends Promise<infer U> ? U : never; input: T extends Promise<infer U> ? U : never;
output: AwaitedIdentitity; output: AwaitedIdentitity;
value: T; value: T;
@@ -114,11 +115,28 @@ export const AsyncMixin = Mixin<IAsync, AsyncMixin>(
); );
if (import.meta.vitest) { if (import.meta.vitest) {
const { test, expect } = import.meta.vitest; const { test, expect, expectTypeOf, describe } = import.meta
.vitest;
const registry = [Base, AsyncMixin] as const; const registry = [Base, AsyncMixin] as const;
const $ = makeFluent(registry); const $ = makeFluent(registry);
describe("Awaited<T> class", () => {
test("constructor", () => {
const promise = new Promise(() => void {});
const awaited = new Awaited(promise);
expect(awaited.value).toBe(promise);
});
test("shim", () => {
const awaited = new Awaited(new Promise(() => void {}));
expect(awaited[shim]).toBeUndefined();
expectTypeOf(awaited[shim]).toExtend<Shim>();
});
});
test(".awaited", async () => { test(".awaited", async () => {
const value = 10 as const; const value = 10 as const;
const promise = new Promise<number>((r) => { const promise = new Promise<number>((r) => {
+57 -12
View File
@@ -1,5 +1,14 @@
import type { Inc, Vec } from "../../utility"; import type { Inc, Vec } from "../../utility";
export type Truncate<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? N : T;
export type Decimal<T extends number> =
`${T}` extends `${infer Sign extends "-" | ""}${number}.${infer N extends number}`
? `${Sign}0.${N}` extends `${infer O extends number}`
? O
: never
: 0;
export type Floor<T extends number> = export type Floor<T extends number> =
`${T}` extends `${infer N extends number}.${number}` ? N : T; `${T}` extends `${infer N extends number}.${number}` ? N : T;
export type Ceil<T extends number> = export type Ceil<T extends number> =
@@ -36,110 +45,144 @@ type Fn<
interface IBase { interface IBase {
/** /**
* `x + y` * `x + y`
* @param other The second term of the addition (`y`) * @param other Addend term of the addition (`y`)
* @from {@link Math `Math`}
*/ */
add: Fn<1>; add: Fn<1>;
/** /**
* `x - y` * `x - y`
* @param other The second term of the subtraction (`y`) * @param other Subtrahend term of the subtraction (`y`)
* @from {@link Math `Math`}
*/ */
subtract: Fn<1>; subtract: Fn<1>;
/** /**
* `x * y` * `x * y`
* @param factor Factor term of the multiplication (`y`) * @param factor Factor term of the multiplication (`y`)
* @from {@link Math `Math`}
*/ */
multiply: Fn<1>; multiply: Fn<1>;
/** /**
* `x ** y` * `x ** y`
* @param exponent Exponent term of the power (`y`) * @param exponent Exponent term of the power (`y`)
* @from {@link Math `Math`}
*/ */
pow: Fn<1, true>; pow: Fn<1, true>;
/** /**
* `Math.sqrt(x)` (or `Math.log2(x)`) * `Math.sqrt(x)` (or `Math.log2(x)`)
* @from {@link Math `Math`}
*/ */
sqrt: Fn<0, true>; sqrt: Fn<0, true>;
/** /**
* `x / y` * `x / y`
* @param divisor Divisor term of the division (`y`) * @param divisor Divisor term of the division (`y`)
* @from {@link Math `Math`}
*/ */
divide: Fn<1, true>; divide: Fn<1, true>;
/** /**
* `x mod y`, not to be confused with `x % y` (remainder operation) * `x mod y` modulo operation (not to be confused with `x % y` (remainder
* operation))
* @param divisor Divisor term of the modulo operation (`y`) * @param divisor Divisor term of the modulo operation (`y`)
* @see `.rem()` for the remainder operation * @see `.rem()` for the remainder operation
* @from {@link Math `Math`}
*/ */
mod: Fn<1, true>; mod: Fn<1, true>;
/** /**
* `x % y` remainder operation, not to be confused with `x mod y` (modulo) * `x % y` remainder operation (not to be confused with `x mod y` (modulo
* operation))
* @param divisor Divisor term of the remainder operation (`y`) * @param divisor Divisor term of the remainder operation (`y`)
* @see `.mod()` for the true modulo operation * @see `.mod()` for the true modulo operation
* @from {@link Math `Math`}
*/ */
rem: Fn<1, true>; rem: Fn<1, true>;
/** /**
* `ln(x)`, `log(x, base)`, or alternatively `Math.log(x) / Math.log(base)` * `ln(x)`, `log(x, base)`, or alternatively `Math.log(x) / Math.log(base)`
* @param base Base of the logarithm (`base`). Defaults to `Math.E` for a natural logarithm function * @param base Base of the logarithm (`base`). Defaults to `Math.E` for a natural logarithm function
* @from {@link Math `Math`}
*/ */
log: Fn<0, true>; log: Fn<0, true>;
/**
* `Math.trunc(x)` or `x - (x % 1)`, remove the decimal part of the value
* @from {@link Math `Math`}
*/
truncate: Fn<0, true>;
/**
* `x % 1`, remove the whole part of the value, leaving you with the decimals
* @from {@link Math `Math`}
*/
decimal: Fn<0, true>;
/** /**
* `Math.floor(x)` * `Math.floor(x)`
* @from {@link Math `Math`}
*/ */
floor: Fn<0, true>; floor: Fn<0, true>;
/** /**
* `Math.ceil(x)` * `Math.ceil(x)`
* @from {@link Math `Math`}
*/ */
ceil: Fn<0, true>; ceil: Fn<0, true>;
/** /**
* `Math.round(x)` or `Math.round(x / multiple) * multiple` to round to the * `Math.round(x)` or `Math.round(x / multiple) * multiple`, round to the
* specified multiple * specified multiple
* @param multiple Multiple to round to, 1 if unspecified * @param multiple Multiple to round to, 1 if unspecified
* @from {@link Math `Math`}
*/ */
round: Fn<1, true>; round: Fn<1, true>;
/** /**
* `Math.fround(x)`, round to the nearest 32-bit float * `Math.fround(x)`, round to the nearest 32-bit float
* approximation of value * approximation of value
* @from {@link Math `Math`}
*/ */
fround: Fn<0, true>; fround: Fn<0, true>;
/** /**
* `Math.f16round(x)`, round to the nearest 16-bit float * `Math.f16round(x)`, round to the nearest 16-bit float
* approximation of value * approximation of value
* @from {@link Math `Math`}
*/ */
ffround: Fn<0, true>; ffround: Fn<0, true>;
/** /**
* `Math.abs(x)`, calculate the absolute value (distance * `Math.abs(x)`, calculate the absolute value (distance
* from 0). * from 0)
* @from {@link Math `Math`}
*/ */
abs: Fn<0, true>; abs: Fn<0, true>;
/** /**
* `Math.sign(x)`, returns the sign (1 or -1) of the value or zero * `Math.sign(x)`, returns the sign (1 or -1) of the value or zero
* @from {@link Math `Math`}
*/ */
sign: Fn<0, true>; sign: Fn<0, true>;
/** /**
* `-x` or alternatively `x * -1` * `-x` or alternatively `x * -1`
* @from {@link Math `Math`}
*/ */
negate: Fn<0, true>; negate: Fn<0, true>;
/** /**
* `Math.min(x, ...values)` * `Math.min(x, ...values)`, return the smallest of the provided values
* @param values Other canidates * @param values Other canidates
* @from {@link Math `Math`}
*/ */
min: Fn<0>; min: Fn<0>;
/** /**
* `Math.min(x, ...values)` * `Math.min(x, ...values)`, return the largest of the provided values
* @param values Other canidates * @param values Other canidates
* @from {@link Math `Math`}
*/ */
max: Fn<0>; max: Fn<0>;
/** /**
* `Math.min(Math.max(x, min), max)` * `Math.min(Math.max(x, min), max)`, clamp value to the specified range
* @param min Smallest allowed value to clamp to * [min, max]
* @param max Largest allowed value to clamp to * @param min Minimum value of the range
* @param max Maximum value of the range
* @from {@link Math `Math`}
*/ */
clamp: Fn<2, true>; clamp: Fn<2, true>;
/** /**
* Clamps value into the range [0.0, 1.0] * Clamp value into the range [0.0, 1.0]
* @see `.clamp()` for a customizable range * @see `.clamp()` for a customizable range
* @from {@link Math `Math`}
*/ */
saturate: Fn<0, true>; saturate: Fn<0, true>;
@@ -156,6 +199,7 @@ interface IBase {
* @param b Target value to interpolate to * @param b Target value to interpolate to
* @param t Interpolation factor, the point on the line where 0.0 is `a` and 1.0 is `b` * @param t Interpolation factor, the point on the line where 0.0 is `a` and 1.0 is `b`
* @see `.inverseLerp()` for the inverse operation (to calculate `t`) * @see `.inverseLerp()` for the inverse operation (to calculate `t`)
* @from {@link Math `Math`}
*/ */
lerp: Fn<2, true>; lerp: Fn<2, true>;
/** /**
@@ -170,6 +214,7 @@ interface IBase {
* @param a The 0.0 point of the range * @param a The 0.0 point of the range
* @param b The 1.0 point of the range * @param b The 1.0 point of the range
* @see `.lerp()` for the inverse operation * @see `.lerp()` for the inverse operation
* @from {@link Math `Math`}
*/ */
inverseLerp: Fn<2, true>; inverseLerp: Fn<2, true>;
} }
+597 -37
View File
File diff suppressed because it is too large Load Diff
+57 -32
View File
@@ -7,16 +7,17 @@ import {
type Abs, type Abs,
type Base, type Base,
type Ceil, type Ceil,
type Decimal,
type Floor, type Floor,
type Negate, type Negate,
type Round, type Round,
type Sign, type Sign,
type Truncate,
} from "./base"; } from "./base";
namespace INumber { namespace INumber {
export type BaseImpl<T extends number, t extends Props> = Base<{ export type BaseImpl<T extends number, t extends Props> = Base<{
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).add(8).value; * const v = $(12).add(8).value;
@@ -24,11 +25,10 @@ namespace INumber {
* ``` * ```
*/ */
add: ( add: (
/** Term to add */ /** Addend term of the addition (`y`) */
other: number, other: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).subtract(8).value; * const v = $(12).subtract(8).value;
@@ -36,11 +36,10 @@ namespace INumber {
* ``` * ```
*/ */
subtract: ( subtract: (
/** Term to subtract by */ /** Subtrahend term of the subtraction (`y`) */
other: number, other: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).multiply(8).value; * const v = $(12).multiply(8).value;
@@ -48,11 +47,10 @@ namespace INumber {
* ``` * ```
*/ */
multiply: ( multiply: (
/** Factor to multiply by */ /** Factor term of the multiplication (`y`) */
factor: number, factor: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).pow(2).value; * const v = $(12).pow(2).value;
@@ -60,11 +58,10 @@ namespace INumber {
* ``` * ```
*/ */
pow: ( pow: (
/** Exponent to raise to */ /** Exponent term of the power (`y`) */
exponent: number, exponent: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(144).sqrt().value; * const v = $(144).sqrt().value;
@@ -73,7 +70,6 @@ namespace INumber {
*/ */
sqrt: () => Return<number, t>; sqrt: () => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).divide(8).value; * const v = $(12).divide(8).value;
@@ -81,11 +77,10 @@ namespace INumber {
* ``` * ```
*/ */
divide: ( divide: (
/** Divisor to divide by */ /** Divisor term of the division (`y`) */
divisor: number, divisor: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).mod(8).value; * const v = $(12).mod(8).value;
@@ -93,11 +88,10 @@ namespace INumber {
* ``` * ```
*/ */
mod: ( mod: (
/** Divisor to divide by */ /** Divisor term of the modulo operation (`y`) */
divisor: number, divisor: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(12).rem(8).value; * const v = $(12).rem(8).value;
@@ -105,11 +99,10 @@ namespace INumber {
* ``` * ```
*/ */
rem: ( rem: (
/** Divisor to divide by */ /** Divisor term of the remainder operation (`y`) */
divisor: number, divisor: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const a = $(Math.E).log().value; * const a = $(Math.E).log().value;
@@ -120,12 +113,31 @@ namespace INumber {
* ``` * ```
*/ */
log: ( log: (
/** The base of the logarithm */ /**
* Base term of the logarithm (`base`)
* @default Math.E
*/
base?: number, base?: number,
) => Return<number, t>; ) => Return<number, t>;
/** /**
* @from {@link Math `Math`} * @example
* ```ts
* const v = $(-1.8).truncate().value;
* expect(v).toBe(-1);
* ```
*/
truncate: () => Return<Truncate<T>, t>;
/**
* @example
* ```ts
* const v = $(4.25).decimal().value;
* expect(v).toBeCloseTo(0.25);
* ```
*/
decimal: () => Return<Decimal<T>, t>;
/**
* @example * @example
* ```ts * ```ts
* const v = $(1.7).floor().value; * const v = $(1.7).floor().value;
@@ -134,7 +146,6 @@ namespace INumber {
*/ */
floor: () => Return<Floor<T>, t>; floor: () => Return<Floor<T>, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(1.3).ceil().value; * const v = $(1.3).ceil().value;
@@ -143,7 +154,6 @@ namespace INumber {
*/ */
ceil: () => Return<Ceil<T>, t>; ceil: () => Return<Ceil<T>, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const a = $(1.3).round().value; * const a = $(1.3).round().value;
@@ -167,7 +177,6 @@ namespace INumber {
t t
>; >;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(0.99999999).fround().value; * const v = $(0.99999999).fround().value;
@@ -176,7 +185,6 @@ namespace INumber {
*/ */
fround: () => Return<number, t>; fround: () => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = $(0.9999).ffround().value; * const v = $(0.9999).ffround().value;
@@ -186,7 +194,6 @@ namespace INumber {
ffround: () => Return<number, t>; ffround: () => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = 4 as const; * const v = 4 as const;
@@ -200,7 +207,6 @@ namespace INumber {
*/ */
abs: () => Return<Abs<T>, t>; abs: () => Return<Abs<T>, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const v = 10 as const; * const v = 10 as const;
@@ -214,7 +220,6 @@ namespace INumber {
*/ */
negate: () => Return<Negate<T>, t>; negate: () => Return<Negate<T>, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const a = $(-42).sign().value; * const a = $(-42).sign().value;
@@ -229,7 +234,6 @@ namespace INumber {
sign: () => Return<Sign<T>, t>; sign: () => Return<Sign<T>, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const maximum = 10; * const maximum = 10;
@@ -246,7 +250,6 @@ namespace INumber {
...values: U ...values: U
) => Return<T | U[number], t>; ) => Return<T | U[number], t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const minimum = 0; * const minimum = 0;
@@ -263,7 +266,6 @@ namespace INumber {
...values: U ...values: U
) => Return<T | U[number], t>; ) => Return<T | U[number], t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const min = 0; * const min = 0;
@@ -279,13 +281,12 @@ namespace INumber {
* ``` * ```
*/ */
clamp: <Min extends number, Max extends number>( clamp: <Min extends number, Max extends number>(
/** The minimum (smallest) value of the range */ /** Minimum value of the range */
min: Min, min: Min,
/** The maximum (largest) value of the range */ /** Maximum value of the range */
max: Max, max: Max,
) => Return<T | Min | Max, t>; ) => Return<T | Min | Max, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const a = $(-2).saturate().value; * const a = $(-2).saturate().value;
@@ -300,7 +301,6 @@ namespace INumber {
saturate: () => Return<number, t>; saturate: () => Return<number, t>;
/** /**
* @from {@link Math `Math`}
* @example * @example
* ```ts * ```ts
* const from = 12; * const from = 12;
@@ -327,6 +327,27 @@ namespace INumber {
/** Interpolation factor, 0.0 corresponds to `a` and 1.0 corresponds to `b` */ /** Interpolation factor, 0.0 corresponds to `a` and 1.0 corresponds to `b` */
t: number, t: number,
) => Return<number, t>; ) => Return<number, t>;
/**
* @example
* ```ts
* const from = 5;
* const to = 10;
*
* const a = $(5).inverseLerp(from, to).value;
* const b = $(7.5).inverseLerp(from, to).value;
* const c = $(10).inverseLerp(from, to).value;
*
* expect(a).toBe(0.0);
* expect(b).toBe(0.5);
* expect(c).toBe(1.0);
*
* const d = $(15).inverseLerp(from, to).value;
* const e = $(0).inverseLerp(from, to).value;
*
* expect(d).toBe(2);
* expect(e).toBe(-1);
* ```
*/
inverseLerp: ( inverseLerp: (
/** 0.0 point of the range */ /** 0.0 point of the range */
a: number, a: number,
@@ -483,6 +504,8 @@ export const applyNumber = Mixin.partial<INumber, number>(
return fluent(Math.abs(value - other) < delta); return fluent(Math.abs(value - other) < delta);
}; };
$.truncate = () => fluent(Math.trunc(value));
$.decimal = () => fluent(value % 1);
$.floor = () => fluent(Math.floor(value)); $.floor = () => fluent(Math.floor(value));
$.ceil = () => fluent(Math.ceil(value)); $.ceil = () => fluent(Math.ceil(value));
$.round = (multiple?: number) => $.round = (multiple?: number) =>
@@ -575,6 +598,8 @@ if (import.meta.vitest) {
).toBe(false); ).toBe(false);
}); });
t("truncate", -6.4, -6);
t("decimal", -6.4, -6.4 % 1);
test("floor()", () => { test("floor()", () => {
expect($(1).floor().value).toBe(1); expect($(1).floor().value).toBe(1);
expect($(1.7).floor().value).toBe(1); expect($(1.7).floor().value).toBe(1);
+57 -246
View File
@@ -1,230 +1,65 @@
import { makeFluent } from "../../base"; import { makeFluent } from "../../base";
import { Mixin, type Props, type Return } from "../../base/mixin"; import { Mixin } from "../../base/mixin";
import { Math } from "../math"; import { Math } from "../math";
import type { Vec, Dec, Inc as __Inc } from "../../utility"; import type { ISwizzle, SwizzleProps } from "./swizzlePermutations";
type Axis = typeof axis; const axis = ["x", "y", "z", "w", "v"] as const;
type Inc<N extends number | undefined> = N extends number export type { ISwizzle, SwizzleProps as Swizzle };
? __Inc<N>
: 0;
type Next<
N extends number,
State extends readonly (number | undefined)[],
TOut extends (number | undefined)[] = [],
> = State extends readonly [
infer Current extends number | undefined,
...infer Rest extends (number | undefined)[],
]
? Inc<Current> extends infer TInc
? TInc extends N
? Next<N, Rest, [...TOut, undefined]>
: [...TOut, TInc, ...Rest]
: never
: null;
type Key<
State extends (number | undefined)[],
TOut extends string = "",
> = State extends readonly [infer Only extends number, ...undefined[]]
? `${TOut}${Axis[Only]}`
: State extends readonly [
infer Current extends number,
...infer Rest extends (number | undefined)[],
]
? Key<Rest, `${TOut}${Axis[Current]}`>
: never;
type IsAscending<
State extends (number | undefined)[],
Prev extends number | undefined = undefined,
> = State extends readonly [Inc<Prev>, ...infer Rest extends number[]]
? IsAscending<Rest, Inc<Prev>>
: State extends readonly []
? true
: false;
type Pretty<T> = { [K in keyof T]: T[K] };
type SwizzlePermutations<
N extends number,
State extends Vec<number | undefined, N> = Vec<undefined, N>,
TOut extends Record<string, readonly number[]> = Record<
never,
never
>,
> =
Next<N, State> extends infer TNext
? TNext extends Vec<number | undefined, N>
? SwizzlePermutations<
N,
TNext,
TOut &
(IsAscending<State> extends true
? unknown
: Record<Key<State>, State>)
>
: Pretty<TOut & Record<Key<State>, State>>
: never;
type Sequence<
T extends readonly unknown[],
Indexes extends (number | undefined)[],
TOut extends T[number][] = [],
> = Indexes extends readonly [
infer Current extends number,
...infer Rest extends (number | undefined)[],
]
? Sequence<T, Rest, [...TOut, T[Current]]>
: TOut;
type Primative<
T extends readonly unknown[],
t extends Props,
Axes extends readonly string[] = Axis,
Acc = unknown,
TOut extends unknown[] = [],
> = Axes extends readonly [
infer Current extends string,
...infer Rest extends string[],
]
? Acc &
Record<
Current,
Return<T[TOut["length"]], t>
> extends infer TAcc
? Primative<T, t, Rest, TAcc, [...TOut, TAcc]>
: never
: TOut;
interface Swizzle4D extends SwizzlePermutations<4> {}
type SwizzleCache = [
{ x: [0] },
SwizzlePermutations<2>,
SwizzlePermutations<3>,
Swizzle4D,
];
export type Swizzle<
T extends readonly unknown[],
t extends Props,
> = number extends T["length"]
? unknown
: T extends readonly []
? unknown
: (
Dec<T["length"]> extends infer K extends
| 0
| 1
| 2
| 3
? { k: K; c: SwizzleCache[K] }
: SwizzleCache extends [
...unknown[],
infer Last,
]
? {
k: Dec<SwizzleCache["length"]>;
c: Last;
}
: never
) extends {
k: infer K extends number;
c: infer C extends Record<
PropertyKey,
(number | undefined)[]
>;
}
? Omit<
{
readonly [K in keyof C]: Return<
Sequence<T, C[K]>,
t
>;
},
Axis[number]
> &
Primative<T, t>[K]
: never;
export type ISwizzle<
T extends readonly unknown[] = readonly unknown[],
t extends Props = Props,
> = {
readonly [K in keyof Swizzle4D]: Return<
Sequence<T, Swizzle4D[K]>,
t
>;
} & Primative<T, t>[3];
const axis = ["x", "y", "z", "w"] as const;
export const applySwizzle = Mixin.partial< export const applySwizzle = Mixin.partial<
ISwizzle, ISwizzle,
readonly unknown[] readonly unknown[]
>((value, $, fluent) => { >((value, $, fluent) => {
const length = globalThis.Math.min(value.length, axis.length); const length = globalThis.Math.min(value.length, axis.length);
const state = new Array<number>(length).fill(-1); const state = new Array<number>(axis.length).fill(-1);
main: do { outer: do {
for (let i = 0; i < state.length; i++) { for (let i = 0; i < state.length; i++)
state[i] += 1; if (++state[i] >= length) {
if (state[i] >= length) state[i] = -1; state[i] = -1;
else break; } else break;
}
let reachedEmpty = false; let nulled = false;
for (const item of state) { for (const item of state) {
if (item === -1) { if (item === -1) {
reachedEmpty = true; nulled = true;
} else if (reachedEmpty) { continue;
continue main;
} }
if (nulled) continue outer;
} }
if (state.every((x) => x === -1)) break;
const key = state const key = state
.filter((x) => x !== -1) .filter((x) => x !== -1)
.map((x) => axis[x]) .map((x) => axis[x])
.join(""); .join("");
if (key.length <= 1 || key in $) continue; if (state.slice(1).every((x) => x === -1)) {
const v = value[state[0]];
Object.defineProperty($, key, {
get: () => {
return fluent(v);
},
});
continue;
}
let count = 0;
if (state.every((x) => x === count++)) continue;
const permutation = state const permutation = state
.filter((x) => x !== -1) .filter((x) => x !== -1)
.map((x) => value[x]); .map((x) => value[x]);
Object.defineProperty($, key, { Object.defineProperty($, key, {
get: () => { value: () => {
// OPTIMIZE? Reduce amount of arrays allocated and held for the lambda..?
return fluent(permutation); return fluent(permutation);
}, },
}); });
} while (state.some((x) => x !== -1)); } while (state.some((x) => x !== -1));
Object.defineProperty($, "x", {
get: () => {
return fluent(value[0]);
},
});
Object.defineProperty($, "y", {
get: () => {
return fluent(value[1]);
},
});
Object.defineProperty($, "z", {
get: () => {
return fluent(value[2]);
},
});
Object.defineProperty($, "w", {
get: () => {
return fluent(value[3]);
},
});
}); });
if (import.meta.vitest) { if (import.meta.vitest) {
const { test, describe, expect, expectTypeOf } = import.meta const { test, expect, expectTypeOf } = import.meta.vitest;
.vitest;
const registry = [Math] as const; const registry = [Math] as const;
const $ = makeFluent(registry); const $ = makeFluent(registry);
@@ -235,20 +70,33 @@ if (import.meta.vitest) {
const vec = [x, y] as const; const vec = [x, y] as const;
expect($(vec).xx.value).toMatchObject([x, x]); expect($(vec).xx().value).toMatchObject([x, x]);
expect($(vec).yy.value).toMatchObject([y, y]); expect($(vec).yy().value).toMatchObject([y, y]);
const yx = $(vec).yx.value; expect($(vec).xyxy().value).toMatchObject([x, y, x, y]);
const yx = $(vec).yx().value;
expect(yx).toMatchObject([y, x]); expect(yx).toMatchObject([y, x]);
expectTypeOf(yx).toEqualTypeOf<[typeof y, typeof x]>(); expectTypeOf(yx).toEqualTypeOf<[typeof y, typeof x]>();
const large = [x, y, x, y] as const; const large = [x, y, x, y] as const;
expect($(large).wzyx.value).toMatchObject([y, x, y, x]); expect($(large).wzyx().value).toMatchObject([y, x, y, x]);
expect($(large).xzyw.value).toMatchObject([x, x, y, y]); expect($(large).xzyw().value).toMatchObject([x, x, y, y]);
expect($(large).ywxz.value).toMatchObject([y, y, x, x]); expect($(large).ywxz().value).toMatchObject([y, y, x, x]);
expect($(large).xxxx.value).toMatchObject([x, x, x, x]); expect($(large).xxxx().value).toMatchObject([x, x, x, x]);
expect($(large).wwww.value).toMatchObject([y, y, y, y]); expect($(large).wwww().value).toMatchObject([y, y, y, y]);
const v = 3 as const;
const gigantic = [x, y, v, y, x] as const;
expect($(gigantic).vwzyx().value).toMatchObject([
x,
y,
v,
y,
x,
]);
}); });
test(".x", () => { test(".x", () => {
@@ -287,49 +135,12 @@ if (import.meta.vitest) {
expectTypeOf(v).toEqualTypeOf(w); expectTypeOf(v).toEqualTypeOf(w);
}); });
describe("types", () => { test(".v", () => {
test("Next", () => { const v = 0 as const;
type N = 3; const arr = [v, v, v, v, v] as const;
expectTypeOf< const value = $(arr).v.value;
Next<N, [undefined, undefined, undefined]> expect(value).toBe(v);
>().toEqualTypeOf<[0, undefined, undefined]>(); expectTypeOf(value).toEqualTypeOf(v);
expectTypeOf<
Next<N, [2, undefined, undefined]>
>().toEqualTypeOf<[undefined, 0, undefined]>();
expectTypeOf<Next<N, [2, 2, 2]>>().toEqualTypeOf<null>();
});
test("Key", () => {
expectTypeOf<
Key<[1, 2, undefined]>
>().toEqualTypeOf<"yz">();
expectTypeOf<Key<[1, 1, 1]>>().toEqualTypeOf<"yyy">();
});
test("Sequence", () => {
type In = [1, 2, 3, 4, 5];
type Out = [5, 4, 3, 2, 1];
type Reverse = [4, 3, 2, 1, 0];
expectTypeOf<
Sequence<In, Reverse>
>().toEqualTypeOf<Out>();
});
test("Primative", () => {
interface Props {
value: [number, number];
meta: { registry: [] };
}
type V = Primative<[number, number], Props>[1];
expectTypeOf<V>().toHaveProperty("x");
expectTypeOf<V>().toHaveProperty("y");
expectTypeOf<V>().not.toHaveProperty("z");
});
}); });
} }