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
*.log
+1
View File
@@ -2,6 +2,7 @@ import { defineConfig } from "gen-doctests/config";
export default defineConfig({
include: ["src/**/*.{js,ts}"],
exclude: ["src/**/*.d.ts"],
outDir: "tests/generated",
templateHeader: [
"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";
export default defineConfig([
globalIgnores(["dist", "tests/generated", "coverage"]),
globalIgnores(["dist", "tests/generated", "coverage", "*.d.ts"]),
{
files: ["**/*.{ts,tsx}"],
extends: [
+7 -4
View File
@@ -4,11 +4,14 @@
"version": "0.0.0",
"type": "module",
"scripts": {
"test": "gen-doctests && vitest --run --reporter=tree --coverage --typecheck",
"build": "tsc -b && esbuild --minify --bundle src/index.ts --outdir=dist --define:import.meta.vitest=undefined",
"test": "gen-doctests && vitest --run --reporter=tree --typecheck --cache",
"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 .",
"lint": "eslint .",
"preview": "vite preview"
"prepublish": "pnpm build:generate",
"prepack": "pnpm test && pnpm build"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
@@ -21,7 +24,7 @@
"jiti": "^2.7.0",
"madge": "^8.0.0",
"prettier": "^3.8.4",
"typescript": "6",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"vitest": "^4.1.9"
}
+1 -1
View File
@@ -39,7 +39,7 @@ importers:
specifier: ^3.8.4
version: 3.8.4
typescript:
specifier: '6'
specifier: ^6.0.3
version: 6.0.3
typescript-eslint:
specifier: ^8.59.2
+3 -3
View File
@@ -1,5 +1,5 @@
import type { Fluent } from ".";
import { never } from "../internal";
import { phantom } from "../internal";
import type { Registry } from "../registry";
import type { RecPartial } from "../utility";
import type { HKT } from "./hkt";
@@ -29,8 +29,8 @@ export function Mixin<I, T extends MixinHKT<I>>(
fn: MixinFn<I>,
): Mixin<I, T> {
return {
interface: never,
hkt: never,
interface: phantom,
hkt: phantom,
fn,
};
}
+1 -1
View File
@@ -5,7 +5,7 @@ export interface Identity extends HKT {
}
// 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[] {
return typeof v === "object" && v !== null && Array.isArray(v);
+21 -3
View File
@@ -6,8 +6,9 @@ import {
type Input,
type Props,
type Return,
type Shim,
} from "../base/mixin";
import { assert, never } from "../internal";
import { assert, phantom } from "../internal";
import { Base } from "./base";
interface AwaitedIdentitity extends HKT {
@@ -21,7 +22,7 @@ class Awaited<T extends Promise<unknown>> {
}
public get [shim]() {
return never as {
return phantom as {
input: T extends Promise<infer U> ? U : never;
output: AwaitedIdentitity;
value: T;
@@ -114,11 +115,28 @@ export const AsyncMixin = Mixin<IAsync, AsyncMixin>(
);
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 $ = 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 () => {
const value = 10 as const;
const promise = new Promise<number>((r) => {
+57 -12
View File
@@ -1,5 +1,14 @@
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> =
`${T}` extends `${infer N extends number}.${number}` ? N : T;
export type Ceil<T extends number> =
@@ -36,110 +45,144 @@ type Fn<
interface IBase {
/**
* `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>;
/**
* `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>;
/**
* `x * y`
* @param factor Factor term of the multiplication (`y`)
* @from {@link Math `Math`}
*/
multiply: Fn<1>;
/**
* `x ** y`
* @param exponent Exponent term of the power (`y`)
* @from {@link Math `Math`}
*/
pow: Fn<1, true>;
/**
* `Math.sqrt(x)` (or `Math.log2(x)`)
* @from {@link Math `Math`}
*/
sqrt: Fn<0, true>;
/**
* `x / y`
* @param divisor Divisor term of the division (`y`)
* @from {@link Math `Math`}
*/
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`)
* @see `.rem()` for the remainder operation
* @from {@link Math `Math`}
*/
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`)
* @see `.mod()` for the true modulo operation
* @from {@link Math `Math`}
*/
rem: Fn<1, true>;
/**
* `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
* @from {@link Math `Math`}
*/
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)`
* @from {@link Math `Math`}
*/
floor: Fn<0, true>;
/**
* `Math.ceil(x)`
* @from {@link Math `Math`}
*/
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
* @param multiple Multiple to round to, 1 if unspecified
* @from {@link Math `Math`}
*/
round: Fn<1, true>;
/**
* `Math.fround(x)`, round to the nearest 32-bit float
* approximation of value
* @from {@link Math `Math`}
*/
fround: Fn<0, true>;
/**
* `Math.f16round(x)`, round to the nearest 16-bit float
* approximation of value
* @from {@link Math `Math`}
*/
ffround: Fn<0, true>;
/**
* `Math.abs(x)`, calculate the absolute value (distance
* from 0).
* from 0)
* @from {@link Math `Math`}
*/
abs: Fn<0, true>;
/**
* `Math.sign(x)`, returns the sign (1 or -1) of the value or zero
* @from {@link Math `Math`}
*/
sign: Fn<0, true>;
/**
* `-x` or alternatively `x * -1`
* @from {@link Math `Math`}
*/
negate: Fn<0, true>;
/**
* `Math.min(x, ...values)`
* `Math.min(x, ...values)`, return the smallest of the provided values
* @param values Other canidates
* @from {@link Math `Math`}
*/
min: Fn<0>;
/**
* `Math.min(x, ...values)`
* `Math.min(x, ...values)`, return the largest of the provided values
* @param values Other canidates
* @from {@link Math `Math`}
*/
max: Fn<0>;
/**
* `Math.min(Math.max(x, min), max)`
* @param min Smallest allowed value to clamp to
* @param max Largest allowed value to clamp to
* `Math.min(Math.max(x, min), max)`, clamp value to the specified range
* [min, max]
* @param min Minimum value of the range
* @param max Maximum value of the range
* @from {@link Math `Math`}
*/
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
* @from {@link Math `Math`}
*/
saturate: Fn<0, true>;
@@ -156,6 +199,7 @@ interface IBase {
* @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`
* @see `.inverseLerp()` for the inverse operation (to calculate `t`)
* @from {@link Math `Math`}
*/
lerp: Fn<2, true>;
/**
@@ -170,6 +214,7 @@ interface IBase {
* @param a The 0.0 point of the range
* @param b The 1.0 point of the range
* @see `.lerp()` for the inverse operation
* @from {@link Math `Math`}
*/
inverseLerp: Fn<2, true>;
}
+625 -65
View File
File diff suppressed because it is too large Load Diff
+57 -32
View File
@@ -7,16 +7,17 @@ import {
type Abs,
type Base,
type Ceil,
type Decimal,
type Floor,
type Negate,
type Round,
type Sign,
type Truncate,
} from "./base";
namespace INumber {
export type BaseImpl<T extends number, t extends Props> = Base<{
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).add(8).value;
@@ -24,11 +25,10 @@ namespace INumber {
* ```
*/
add: (
/** Term to add */
/** Addend term of the addition (`y`) */
other: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).subtract(8).value;
@@ -36,11 +36,10 @@ namespace INumber {
* ```
*/
subtract: (
/** Term to subtract by */
/** Subtrahend term of the subtraction (`y`) */
other: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).multiply(8).value;
@@ -48,11 +47,10 @@ namespace INumber {
* ```
*/
multiply: (
/** Factor to multiply by */
/** Factor term of the multiplication (`y`) */
factor: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).pow(2).value;
@@ -60,11 +58,10 @@ namespace INumber {
* ```
*/
pow: (
/** Exponent to raise to */
/** Exponent term of the power (`y`) */
exponent: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(144).sqrt().value;
@@ -73,7 +70,6 @@ namespace INumber {
*/
sqrt: () => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).divide(8).value;
@@ -81,11 +77,10 @@ namespace INumber {
* ```
*/
divide: (
/** Divisor to divide by */
/** Divisor term of the division (`y`) */
divisor: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).mod(8).value;
@@ -93,11 +88,10 @@ namespace INumber {
* ```
*/
mod: (
/** Divisor to divide by */
/** Divisor term of the modulo operation (`y`) */
divisor: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(12).rem(8).value;
@@ -105,11 +99,10 @@ namespace INumber {
* ```
*/
rem: (
/** Divisor to divide by */
/** Divisor term of the remainder operation (`y`) */
divisor: number,
) => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const a = $(Math.E).log().value;
@@ -120,12 +113,31 @@ namespace INumber {
* ```
*/
log: (
/** The base of the logarithm */
/**
* Base term of the logarithm (`base`)
* @default Math.E
*/
base?: number,
) => 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
* ```ts
* const v = $(1.7).floor().value;
@@ -134,7 +146,6 @@ namespace INumber {
*/
floor: () => Return<Floor<T>, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(1.3).ceil().value;
@@ -143,7 +154,6 @@ namespace INumber {
*/
ceil: () => Return<Ceil<T>, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const a = $(1.3).round().value;
@@ -167,7 +177,6 @@ namespace INumber {
t
>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(0.99999999).fround().value;
@@ -176,7 +185,6 @@ namespace INumber {
*/
fround: () => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = $(0.9999).ffround().value;
@@ -186,7 +194,6 @@ namespace INumber {
ffround: () => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = 4 as const;
@@ -200,7 +207,6 @@ namespace INumber {
*/
abs: () => Return<Abs<T>, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const v = 10 as const;
@@ -214,7 +220,6 @@ namespace INumber {
*/
negate: () => Return<Negate<T>, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const a = $(-42).sign().value;
@@ -229,7 +234,6 @@ namespace INumber {
sign: () => Return<Sign<T>, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const maximum = 10;
@@ -246,7 +250,6 @@ namespace INumber {
...values: U
) => Return<T | U[number], t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const minimum = 0;
@@ -263,7 +266,6 @@ namespace INumber {
...values: U
) => Return<T | U[number], t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const min = 0;
@@ -279,13 +281,12 @@ namespace INumber {
* ```
*/
clamp: <Min extends number, Max extends number>(
/** The minimum (smallest) value of the range */
/** Minimum value of the range */
min: Min,
/** The maximum (largest) value of the range */
/** Maximum value of the range */
max: Max,
) => Return<T | Min | Max, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const a = $(-2).saturate().value;
@@ -300,7 +301,6 @@ namespace INumber {
saturate: () => Return<number, t>;
/**
* @from {@link Math `Math`}
* @example
* ```ts
* const from = 12;
@@ -327,6 +327,27 @@ namespace INumber {
/** Interpolation factor, 0.0 corresponds to `a` and 1.0 corresponds to `b` */
t: number,
) => 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: (
/** 0.0 point of the range */
a: number,
@@ -483,6 +504,8 @@ export const applyNumber = Mixin.partial<INumber, number>(
return fluent(Math.abs(value - other) < delta);
};
$.truncate = () => fluent(Math.trunc(value));
$.decimal = () => fluent(value % 1);
$.floor = () => fluent(Math.floor(value));
$.ceil = () => fluent(Math.ceil(value));
$.round = (multiple?: number) =>
@@ -575,6 +598,8 @@ if (import.meta.vitest) {
).toBe(false);
});
t("truncate", -6.4, -6);
t("decimal", -6.4, -6.4 % 1);
test("floor()", () => {
expect($(1).floor().value).toBe(1);
expect($(1.7).floor().value).toBe(1);
+57 -246
View File
@@ -1,230 +1,65 @@
import { makeFluent } from "../../base";
import { Mixin, type Props, type Return } from "../../base/mixin";
import { Mixin } from "../../base/mixin";
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
? __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 type { ISwizzle, SwizzleProps as Swizzle };
export const applySwizzle = Mixin.partial<
ISwizzle,
readonly unknown[]
>((value, $, fluent) => {
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 {
for (let i = 0; i < state.length; i++) {
state[i] += 1;
if (state[i] >= length) state[i] = -1;
else break;
}
outer: do {
for (let i = 0; i < state.length; i++)
if (++state[i] >= length) {
state[i] = -1;
} else break;
let reachedEmpty = false;
let nulled = false;
for (const item of state) {
if (item === -1) {
reachedEmpty = true;
} else if (reachedEmpty) {
continue main;
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 (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
.filter((x) => x !== -1)
.map((x) => value[x]);
Object.defineProperty($, key, {
get: () => {
// OPTIMIZE? Reduce amount of arrays allocated and held for the lambda..?
value: () => {
return fluent(permutation);
},
});
} 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) {
const { test, describe, expect, expectTypeOf } = import.meta
.vitest;
const { test, expect, expectTypeOf } = import.meta.vitest;
const registry = [Math] as const;
const $ = makeFluent(registry);
@@ -235,20 +70,33 @@ if (import.meta.vitest) {
const vec = [x, y] as const;
expect($(vec).xx.value).toMatchObject([x, x]);
expect($(vec).yy.value).toMatchObject([y, y]);
expect($(vec).xx().value).toMatchObject([x, x]);
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]);
expectTypeOf(yx).toEqualTypeOf<[typeof y, typeof x]>();
const large = [x, y, x, y] as const;
expect($(large).wzyx.value).toMatchObject([y, x, y, x]);
expect($(large).xzyw.value).toMatchObject([x, x, y, y]);
expect($(large).ywxz.value).toMatchObject([y, y, x, x]);
expect($(large).xxxx.value).toMatchObject([x, x, x, x]);
expect($(large).wwww.value).toMatchObject([y, y, y, y]);
expect($(large).wzyx().value).toMatchObject([y, x, y, x]);
expect($(large).xzyw().value).toMatchObject([x, x, y, y]);
expect($(large).ywxz().value).toMatchObject([y, y, x, x]);
expect($(large).xxxx().value).toMatchObject([x, x, x, x]);
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", () => {
@@ -287,49 +135,12 @@ if (import.meta.vitest) {
expectTypeOf(v).toEqualTypeOf(w);
});
describe("types", () => {
test("Next", () => {
type N = 3;
test(".v", () => {
const v = 0 as const;
const arr = [v, v, v, v, v] as const;
expectTypeOf<
Next<N, [undefined, undefined, undefined]>
>().toEqualTypeOf<[0, undefined, undefined]>();
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");
});
const value = $(arr).v.value;
expect(value).toBe(v);
expectTypeOf(value).toEqualTypeOf(v);
});
}