From 0c0fb74c865032888ad8d84663b905b47fa1abf6 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 23 Feb 2025 14:04:53 +0200 Subject: [PATCH 01/31] Remove jsdoc types from math utils --- .changeset/fine-numbers-vanish.md | 5 +++++ lib/math/__tests__/utils.test.ts | 2 +- lib/math/utils.ts | 36 +++++++++++++++---------------- 3 files changed, 24 insertions(+), 19 deletions(-) create mode 100644 .changeset/fine-numbers-vanish.md diff --git a/.changeset/fine-numbers-vanish.md b/.changeset/fine-numbers-vanish.md new file mode 100644 index 0000000..e10ca88 --- /dev/null +++ b/.changeset/fine-numbers-vanish.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': patch +--- + +Remove jsdoc types in math utils in favor of ts types. diff --git a/lib/math/__tests__/utils.test.ts b/lib/math/__tests__/utils.test.ts index 5c90e32..6b28c33 100644 --- a/lib/math/__tests__/utils.test.ts +++ b/lib/math/__tests__/utils.test.ts @@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest'; import { transformRange, clamp, lerp, lerpClamped } from '../utils.js'; -describe('Math Utilities', () => { +describe('math/utils', () => { describe('clamp', () => { it('should return the max value if given a value greater', () => { expect(clamp(50, -25, 25)).toEqual(25); diff --git a/lib/math/utils.ts b/lib/math/utils.ts index 558ef57..d214058 100644 --- a/lib/math/utils.ts +++ b/lib/math/utils.ts @@ -1,10 +1,10 @@ /** * Clamps a number between two boundaries. * - * @param {number} value The Value to clamp. - * @param {number} min The minimum boundary. - * @param {number} max The maximum boundary. - * @returns {number} The value calmped between the two boundaries. + * @param value The Value to clamp. + * @param min The minimum boundary. + * @param max The maximum boundary. + * @returns The value clamped between the two boundaries. */ export function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); @@ -13,12 +13,12 @@ export function clamp(value: number, min: number, max: number): number { /** * Transforms a value from one range to another. * - * @param {number} value The value to interpolate. - * @param {number} min1 The minimum value for the first range. - * @param {number} max1 The maximum value for the first range. - * @param {number} min2 The minimum value for the second range. - * @param {number} max2 The maximum value for the second range. - * @returns {number} The interpolated value. + * @param value The value to interpolate. + * @param min1 The minimum value for the first range. + * @param max1 The maximum value for the first range. + * @param min2 The minimum value for the second range. + * @param max2 The maximum value for the second range. + * @returns The interpolated value. */ export function transformRange( value: number, @@ -39,10 +39,10 @@ export function transformRange( /** * Interpolates a value between the start and end values. * - * @param {number} start The lower boundary or initial value. - * @param {number} target The target value. - * @param {number} t the current lerp time. - * @returns {number} The lerped value. + * @param start The lower boundary or initial value. + * @param target The target value. + * @param t the current lerp time. + * @returns The lerped value. */ export function lerp(start: number, target: number, t: number): number { return start + (target - start) * t; @@ -52,10 +52,10 @@ export function lerp(start: number, target: number, t: number): number { * Interpolates a value between the start and end values, clamping the result * to prevent extrapolation. * - * @param {number} start The lower boundary or initial value. - * @param {number} target The target value. - * @param {number} t the current lerp time. - * @returns {number} The lerped value. + * @param start The lower boundary or initial value. + * @param target The target value. + * @param t the current lerp time. + * @returns The lerped value. */ export function lerpClamped(start: number, target: number, t: number): number { return clamp(lerp(start, target, t), start, target); From 2cf51a097f1c26fe1840d9b4541ec3513444edb6 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 23 Feb 2025 14:06:03 +0200 Subject: [PATCH 02/31] Add 2D and 3D vector implementations. --- .changeset/eager-spiders-divide.md | 5 + .changeset/fast-chefs-cough.md | 5 + lib/math/__tests__/vector2.test.ts | 186 ++++++++++++++++ lib/math/__tests__/vector3.test.ts | 178 ++++++++++++++++ lib/math/vector2.ts | 286 +++++++++++++++++++++++++ lib/math/vector3.ts | 331 +++++++++++++++++++++++++++++ 6 files changed, 991 insertions(+) create mode 100644 .changeset/eager-spiders-divide.md create mode 100644 .changeset/fast-chefs-cough.md create mode 100644 lib/math/__tests__/vector2.test.ts create mode 100644 lib/math/__tests__/vector3.test.ts create mode 100644 lib/math/vector2.ts create mode 100644 lib/math/vector3.ts diff --git a/.changeset/eager-spiders-divide.md b/.changeset/eager-spiders-divide.md new file mode 100644 index 0000000..22ab5e0 --- /dev/null +++ b/.changeset/eager-spiders-divide.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Add vector2 implementation. diff --git a/.changeset/fast-chefs-cough.md b/.changeset/fast-chefs-cough.md new file mode 100644 index 0000000..ace80f0 --- /dev/null +++ b/.changeset/fast-chefs-cough.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Add vector3 implementation. diff --git a/lib/math/__tests__/vector2.test.ts b/lib/math/__tests__/vector2.test.ts new file mode 100644 index 0000000..4be2818 --- /dev/null +++ b/lib/math/__tests__/vector2.test.ts @@ -0,0 +1,186 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import { describe, it, expect } from 'vitest'; + +import { Vector2 } from '../vector2.js'; + +describe('math/vector2', () => { + it('should store 2d vectors', () => { + const vec2 = new Vector2(5, 10); + expect(vec2).toBeInstanceOf(Vector2); + expect(vec2.x).toEqual(5); + expect(vec2.y).toEqual(10); + }); + + it('should copy itself to a new vector2', () => { + const original = new Vector2(327.5, 21); + const copy = original.copy(); + expect(copy.x).toEqual(original.x); + expect(copy.y).toEqual(original.y); + }); + + describe('builtins', () => { + it('.add should add its values with the given vector', () => { + const vec = new Vector2(0, 0); + expect(vec.x).toEqual(0); + expect(vec.y).toEqual(0); + + vec.add(new Vector2(10, 5)); + expect(vec.x).toEqual(10); + expect(vec.y).toEqual(5); + + vec.add({ x: 1, y: 1 }); + expect(vec.x).toEqual(11); + expect(vec.y).toEqual(6); + }); + + it('should provide the length of the vector', () => { + expect(new Vector2(6, 8).getLength()).toEqual(10); + expect(new Vector2(0, 0).getLength()).toEqual(0); + }); + + it('should expose the magnitude of the vector', () => { + expect(new Vector2(10, 10).getMagnitude()).toEqual(200); + expect(new Vector2(0, 0).getMagnitude()).toEqual(0); + expect(new Vector2(5, 10).getMagnitude()).toEqual(125); + expect(new Vector2(10, 5).getMagnitude()).toEqual(125); + }); + + it('.multiply should multiply its values with the given vector2 ', () => { + const vec2_1 = new Vector2(1, 1); + expect(vec2_1.x).toEqual(1); + expect(vec2_1.y).toEqual(1); + + vec2_1.multiply({ x: 5, y: 10 }); + expect(vec2_1.x).toEqual(5); + expect(vec2_1.y).toEqual(10); + }); + + it('.multiplyScalar should multiply its values with the given number', () => { + const vec2_1 = new Vector2(1, 1); + expect(vec2_1.x).toEqual(1); + expect(vec2_1.y).toEqual(1); + + vec2_1.multiplyScalar(5); + expect(vec2_1.x).toEqual(5); + expect(vec2_1.y).toEqual(5); + }); + + it('.divideScalar should divide its values with the given number', () => { + const vec2 = new Vector2(10, 10); + vec2.divideScalar(5); + expect(vec2.x).toEqual(2); + expect(vec2.y).toEqual(2); + }); + + it('.divide should divide its values with the given vector2', () => { + const vec2 = new Vector2(10, 10); + vec2.divide({ x: 2, y: 5 }); + expect(vec2.x).toEqual(5); + expect(vec2.y).toEqual(2); + }); + + it('.equals should check value equality with the given vector', () => { + const original = new Vector2(300, 300); + const notEqual = new Vector2(500, 420); + const equal = new Vector2(300, 300); + + expect(original.equals(notEqual)).toEqual(false); + expect(original.equals(equal)).toEqual(true); + }); + + it('should lerp to a new vector', () => { + const original = new Vector2(0, 0); + const target = new Vector2(100, 100); + + original.lerp(target, 0.5); + expect(original.x).toEqual(50); + expect(original.y).toEqual(50); + }); + }); + + describe('static helpers', () => { + it('Vector2.Add should return a new vector with the sum of the given vectors', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 20); + const result = Vector2.Add(v1, v2); + + // make sure they didnt get altered + expect(v1.x).toEqual(5); + expect(v1.y).toEqual(5); + expect(v2.x).toEqual(10); + expect(v2.y).toEqual(20); + + expect(result.x).toEqual(15); + expect(result.y).toEqual(25); + }); + + it('Vector2.Subtract should return a new vector with the difference of the given vectors', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 20); + const result = Vector2.Subtract(v1, v2); + + // make sure they didnt get altered + expect(v1.x).toEqual(5); + expect(v1.y).toEqual(5); + expect(v2.x).toEqual(10); + expect(v2.y).toEqual(20); + + expect(result.x).toEqual(-5); + expect(result.y).toEqual(-15); + }); + + it('Vector2.Multiply should return a new vector with the product of the given vectors', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 20); + const result = Vector2.Multiply(v1, v2); + + // make sure they didnt get altered + expect(v1.x).toEqual(5); + expect(v1.y).toEqual(5); + expect(v2.x).toEqual(10); + expect(v2.y).toEqual(20); + + expect(result.x).toEqual(50); + expect(result.y).toEqual(100); + }); + + it('Vector2.Divide should return a new vector with the quotient of the given vectors', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 20); + const result = Vector2.Divide(v1, v2); + + // make sure they didnt get altered + expect(v1.x).toEqual(5); + expect(v1.y).toEqual(5); + expect(v2.x).toEqual(10); + expect(v2.y).toEqual(20); + + expect(result.x).toEqual(0.5); + expect(result.y).toEqual(0.25); + }); + + it('Vector2.Equals should check whether two vectors are equal', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 5); + const v3 = new Vector2(5, 5); + + expect(Vector2.Equals(v1, v2)).toEqual(false); + expect(Vector2.Equals(v1, v3)).toEqual(true); + }); + + it('Should provide a helper to lerp between two vectors', () => { + const v1 = new Vector2(5, 5); + const v2 = new Vector2(10, 10); + const result = Vector2.Lerp(v1, v2, 0.5); + expect(result.x).toEqual(7.5); + expect(result.y).toEqual(7.5); + }); + + it('Should normalize a vector', () => { + const v1 = new Vector2(5, 5); + expect(Vector2.Normalize(v1).getLength()).toBeCloseTo(1); + const v2 = new Vector2(10, 5); + expect(Vector2.Normalize(v2).getLength()).toBeCloseTo(1); + }); + }); +}); diff --git a/lib/math/__tests__/vector3.test.ts b/lib/math/__tests__/vector3.test.ts new file mode 100644 index 0000000..814bf50 --- /dev/null +++ b/lib/math/__tests__/vector3.test.ts @@ -0,0 +1,178 @@ +import { describe, it, expect } from 'vitest'; + +import { Vector3 } from '../vector3.js'; + +describe('math/vector3', () => { + it('should store 3d vectors', () => { + const vec3 = new Vector3(5, 10, 1); + expect(vec3).toBeInstanceOf(Vector3); + expect(vec3.x).toEqual(5); + expect(vec3.y).toEqual(10); + expect(vec3.z).toEqual(1); + }); + + it('should copy itself to a new vector3', () => { + const original = new Vector3(327.5, 21, 31); + const copy = original.copy(); + expect(copy.x).toEqual(original.x); + expect(copy.y).toEqual(original.y); + expect(copy.z).toEqual(original.z); + }); + + describe('builtins', () => { + it('.add should add its values with the given vector', () => { + const vec = new Vector3(0, 0, 0); + + vec.add(new Vector3(10, 5, 1)); + expect(vec.x).toEqual(10); + expect(vec.y).toEqual(5); + expect(vec.z).toEqual(1); + + vec.add({ x: 1, y: 1, z: 1 }); + expect(vec.x).toEqual(11); + expect(vec.y).toEqual(6); + expect(vec.z).toEqual(2); + }); + + it('should expose the magnitude of the vector', () => { + expect(new Vector3(10, 10, 10).getMagnitude()).toEqual(300); + expect(new Vector3(0, 0, 0).getMagnitude()).toEqual(0); + expect(new Vector3(5, 10, 5).getMagnitude()).toEqual(150); + expect(new Vector3(10, 5, 5).getMagnitude()).toEqual(150); + }); + + it('should provide the length of the vector', () => { + expect(new Vector3(0, 0, 0).getLength()).toEqual(0); + expect(new Vector3(3, 4, 0).getLength()).toEqual(5); + expect(new Vector3(1, 2, 2).getLength()).toEqual(3); + expect(new Vector3(-4, -4, -7).getLength()).toEqual(9); + }); + + it('.multiply should multiply its values with the given vector3', () => { + const vec = new Vector3(1, 0, 1); + + vec.multiply({ x: 5, y: 10, z: 10 }); + expect(vec.x).toEqual(5); + expect(vec.y).toEqual(0); + expect(vec.z).toEqual(10); + }); + + it('.multiplyScalar should multiply its values with the given number', () => { + const vec = new Vector3(0, 1, 1); + + vec.multiplyScalar(5); + expect(vec.x).toEqual(0); + expect(vec.y).toEqual(5); + expect(vec.z).toEqual(5); + }); + + it('.divideScalar should divide its values with the given number', () => { + const vec = new Vector3(10, 10, 10); + vec.divideScalar(5); + expect(vec.x).toEqual(2); + expect(vec.y).toEqual(2); + expect(vec.z).toEqual(2); + + vec.divideScalar(0); + expect(vec.x).toEqual(Infinity); + expect(vec.y).toEqual(Infinity); + expect(vec.z).toEqual(Infinity); + }); + + it('.divide should divide its values with the given vector3', () => { + const vec = new Vector3(10, 10, 10); + vec.divide({ x: 2, y: 5, z: 10 }); + expect(vec.x).toEqual(5); + expect(vec.y).toEqual(2); + expect(vec.z).toEqual(1); + }); + + it('.equals should check value equality with the given vector', () => { + const original = new Vector3(100, 200, 300); + const notEqual = new Vector3(500, 420, 300); + const equal = new Vector3(100, 200, 300); + + expect(original.equals(notEqual)).toEqual(false); + expect(original.equals(equal)).toEqual(true); + }); + + it('should lerp to a new vector', () => { + const original = new Vector3(0, 0, 0); + const target = new Vector3(100, 100, 100); + + original.lerp(target, 0.5); + expect(original.x).toEqual(50); + expect(original.y).toEqual(50); + expect(original.z).toEqual(50); + }); + }); + + describe('static helpers', () => { + it('Vector3.Add should return a new vector with the sum of the given vectors', () => { + const v1 = new Vector3(5, 5, 5); + const v2 = new Vector3(10, 20, 30); + const result = Vector3.Add(v1, v2); + + expect(result.x).toEqual(15); + expect(result.y).toEqual(25); + expect(result.z).toEqual(35); + }); + + it('Vector3.Subtract should return a new vector with the difference of the given vectors', () => { + const v1 = new Vector3(5, 5, 5); + const v2 = new Vector3(10, 20, 30); + const result = Vector3.Subtract(v1, v2); + + expect(result.x).toEqual(-5); + expect(result.y).toEqual(-15); + expect(result.z).toEqual(-25); + }); + + it('Vector3.Multiply should return a new vector with the product of the given vectors', () => { + const v1 = new Vector3(5, 5, 5); + const v2 = new Vector3(10, 20, 30); + const result = Vector3.Multiply(v1, v2); + + expect(result.x).toEqual(50); + expect(result.y).toEqual(100); + expect(result.z).toEqual(150); + }); + + it('Vector3.Divide should return a new vector with the quotient of the given vectors', () => { + const v1 = new Vector3(5, 5, 10); + const v2 = new Vector3(10, 20, 20); + const result = Vector3.Divide(v1, v2); + + expect(result.x).toEqual(0.5); + expect(result.y).toEqual(0.25); + expect(result.z).toEqual(0.5); + }); + + it('Vector3.Equals should check whether two vectors are equal', () => { + const v1 = new Vector3(5, 2, 5); + const v2 = new Vector3(10, 5, 5); + const v3 = new Vector3(5, 2, 5); + + expect(Vector3.Equals(v1, v2)).toEqual(false); + expect(Vector3.Equals(v1, v3)).toEqual(true); + }); + + it('Should provide a helper to lerp between two vectors', () => { + const v1 = new Vector3(5, 5, 5); + const v2 = new Vector3(10, 10, 10); + const result = Vector3.Lerp(v1, v2, 0.5); + + expect(result.x).toEqual(7.5); + expect(result.y).toEqual(7.5); + expect(result.z).toEqual(7.5); + }); + + it('Should normalize a vector', () => { + const v1 = new Vector3(5, 5, 5); + expect(Vector3.Normalize(v1).getLength()).toBeCloseTo(1); + + const v2 = new Vector3(10, 5, 5); + expect(Vector3.Normalize(v2).getLength()).toBeCloseTo(1); + }); + }); +}); diff --git a/lib/math/vector2.ts b/lib/math/vector2.ts new file mode 100644 index 0000000..50a7ca5 --- /dev/null +++ b/lib/math/vector2.ts @@ -0,0 +1,286 @@ +import { clamp } from './utils.js'; + +export interface V2 { + x: number; + y: number; +} + +export class Vector2 implements V2 { + public x = 0; + public y = 0; + + constructor(x = 0, y = 0) { + this.x = x; + this.y = y; + } + + // Helpers + public static Down(): Vector2 { + return new Vector2(0, -1); + } + public static Up(): Vector2 { + return new Vector2(0, 1); + } + public static Left(): Vector2 { + return new Vector2(-1, 0); + } + public static Right(): Vector2 { + return new Vector2(1, 0); + } + public static One(): Vector2 { + return new Vector2(1, 1); + } + public static Zero(): Vector2 { + return new Vector2(0, 0); + } + + // Static Helpers + /** + * Create a new Vector2 that is the sum of the given Vector2's. + * @param v1 First Vector2. + * @param v2 Second Vector2. + * @returns A new vector of the sum of the given vectors. + */ + public static Add(v1: Vector2, v2: Vector2): Vector2 { + return new Vector2(v1.x + v2.x, v1.y + v2.y); + } + + /** + * Create a new Vector2 that is the difference of the given Vector2's. + * @param v1 First Vector2. + * @param v2 Second Vector2. + * @returns A new vector of the difference of the given vectors. + */ + public static Subtract(v1: Vector2, v2: Vector2): Vector2 { + return new Vector2(v1.x - v2.x, v1.y - v2.y); + } + + /** + * Create a new Vector2 by multiplying to given vectors. + * @param v1 First Vector2. + * @param v2 Second Vector2. + * @returns A new vector with the result of the given vectors. + */ + public static Multiply(v1: Vector2, v2: Vector2): Vector2 { + return new Vector2(v1.x * v2.x, v1.y * v2.y); + } + + /** + * Create a new Vector2 by multiplying the components of a vector by a given value. + * @param v1 First Vector2. + * @param val The value to multiply each component by. + * @returns A new vector with the result. + */ + public static MultiplyScalar(v1: Vector2, val: number): Vector2 { + return v1.copy().multiplyScalar(val); + } + + /** + * Create a new Vector2 by dividing two vectors. + * @param v1 First Vector2. + * @param v2 Second Vector2. + * @returns A new vector of the result of the given vectors. + */ + public static Divide(v1: Vector2, v2: Vector2): Vector2 { + return new Vector2(v1.x / v2.x, v1.y / v2.y); + } + + /** + * Returns whether two vector values are equal. + * @param v1 First Vector2. + * @param v2 Second Vector2. + * @returns Whether the vectors are equal. + */ + public static Equals(v1: V2, v2: V2): boolean { + return v1.x === v2.x && v1.y === v2.y; + } + + /** + * Create a new Vector2 by interpolating between two given vectors. + * @param v1 Start vector. + * @param v2 Target vector. + * @param t The amount to interpolate (0 being start, 1 being end, etc.) + * @returns A new Vector2 with the result. + */ + public static Lerp(v1: V2, v2: V2, t: number): Vector2 { + return new Vector2(v1.x + (v2.x - v1.x) * t, v1.y + (v2.y - v1.y) * t); + } + + /** + * Create a new Vector2 by normalizing a vector (dividing by its length, or 1) + * @param vector The vector to normalize. + * @returns A new Vector2 of the normalized vector. + */ + public static Normalize(vector: V2): Vector2 { + return new Vector2(vector.x, vector.y).normalize(); + } + + /** + * Get the magnitude of the vector (x^2 + y^2). + * @returns The magnitude of the vector. + */ + public getMagnitude(): number { + const { x, y } = this; + return x * x + y * y; + } + + /** + * Get the length of the vector (square root of magnitude) + * @returns The length of the vector. + */ + public getLength(): number { + return Math.sqrt(this.getMagnitude()); + } + + /** + * Create a copy of this Vector2. + * @returns A new instance of Vector2 with this vectors components. + */ + public copy(): Vector2 { + return new Vector2(this.x, this.y); + } + + /** + * Add another vector to this vector. + * @param other The other vector. + * @returns Itself. + */ + public add(other: V2): this { + this.x += other.x; + this.y += other.y; + return this; + } + + /** + * Multiply this vector by another vector. + * @param other The other vector. + * @returns Itself. + */ + public multiply(other: V2): this { + this.x *= other.x; + this.y *= other.y; + return this; + } + + /** + * Multiply this vector by a single value. + * @param val The amount to multiply by. + * @returns Itself. + */ + public multiplyScalar(val: number): this { + this.x *= val; + this.y *= val; + return this; + } + + /** + * Divide this vector by a single value. + * @param val The amount to divide by. + * @returns Itself. + */ + public divideScalar(val: number): this { + this.x /= val; + this.y /= val; + return this; + } + + /** + * Multiply this vector by another vector. + * @param other The other vector. + * @returns Itself. + */ + public divide(other: V2): this { + this.x /= other.x; + this.y /= other.y; + return this; + } + + /** + * Set the components of this vector. + * @param x The x component. + * @paramy The y component. + * @returns Itself. + */ + public set(x: number, y: number): this { + this.x = x; + this.y = y; + return this; + } + + /** + * Normalize this vector. Will reduce its length to a max of 1. + * @returns Itself. + */ + public normalize(): this { + return this.divideScalar(this.getLength() || 1); + } + + /** + * Interpolating this vector to a target vector. + * @param target Target vector. + * @param t The amount to interpolate (0 being itself, 1 being target, etc.) + * @returns Itself. + */ + public lerp(target: V2, t: number): this { + this.x += (target.x - this.x) * t; + this.y += (target.y - this.y) * t; + return this; + } + + /** + * Clamp the components of this vector to a min and max value for each component. + * @param xMin The minimum value for x. + * @param xMax The maximum value for x. + * @param yMin The minimum value for y. + * @param yMax The maximum value for y. + * @returns Itself. + */ + public clamp(xMin: number, xMax: number, yMin: number, yMax: number): this { + this.clampX(xMin, xMax); + this.clampY(yMin, yMax); + return this; + } + + /** + * Clamp the x component of this vector to a min and max value. + * @param min The minimum value for x. + * @param max The maximum value for x. + * @returns Itself. + */ + public clampX(min: number, max: number): this { + this.x = clamp(this.x, min, max); + return this; + } + + /** + * Clamp the y component of this vector to a min and max value. + * @param min The minimum value for y. + * @param max The maximum value for y. + * @returns Itself. + */ + public clampY(min: number, max: number): this { + this.y = clamp(this.y, min, max); + return this; + } + + /** + * Check equality between this vectors components and a given vectors components. + * @param val The vector to check equality. + * @returns If the vectors are equal. + */ + public equals(val: V2): boolean { + return val.x === this.x && val.y === this.y; + } + + /** + * Return a lightweight object literal with the x and y component. + * @returns An object literal with the vector set to x, y. + */ + public toLiteral(): V2 { + return { x: this.x, y: this.y }; + } + + public toString(): string { + return `Vector2 (${this.x.toString(10)}, ${this.y.toString(10)})`; + } +} diff --git a/lib/math/vector3.ts b/lib/math/vector3.ts new file mode 100644 index 0000000..a19bf65 --- /dev/null +++ b/lib/math/vector3.ts @@ -0,0 +1,331 @@ +import { clamp, lerp } from './utils.js'; + +export interface V3 { + x: number; + y: number; + z: number; +} + +export class Vector3 implements V3 { + public x = 0; + public y = 0; + public z = 0; + + constructor(x = 0, y = 0, z = 0) { + this.x = x; + this.y = y; + this.z = z; + } + + // Helpers + public static Forward(): Vector3 { + return new Vector3(0, 0, 1); + } + + public static Backwards(): Vector3 { + return new Vector3(0, 0, -1); + } + + public static Down(): Vector3 { + return new Vector3(0, -1, 0); + } + + public static Up(): Vector3 { + return new Vector3(0, 1, 0); + } + + public static Left(): Vector3 { + return new Vector3(-1, 0, 0); + } + + public static Right(): Vector3 { + return new Vector3(1, 0, 0); + } + + public static One(): Vector3 { + return new Vector3(1, 1, 1); + } + + public static Zero(): Vector3 { + return new Vector3(0, 0, 0); + } + + // Static Helpers + /** + * Create a new Vector3 that is the sum of the given Vector3's. + * @param v1 First Vector3. + * @param v2 Second Vector3. + * @returns A new vector of the sum of the given vectors. + */ + public static Add(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); + } + + /** + * Create a new Vector3 that is the difference of the given Vector3's. + * @param v1 First Vector3. + * @param v2 Second Vector3. + * @returns A new vector of the difference of the given vectors. + */ + public static Subtract(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); + } + + /** + * Create a new Vector3 by multiplying to given vectors. + * @param v1 First Vector3. + * @param v2 Second Vector3. + * @returns A new vector with the result of the given vectors. + */ + public static Multiply(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3(v1.x * v2.x, v1.y * v2.y, v1.z * v2.z); + } + + /** + * Create a new Vector3 by multiplying the components of a vector by a given value. + * @param v1 First Vector3. + * @param val The value to multiply each component by. + * @returns A new vector with the result. + */ + public static MultiplyScalar(v1: Vector3, val: number): Vector3 { + return v1.copy().multiplyScalar(val); + } + + /** + * Create a new Vector3 by dividing two vectors. + * @param v1 First Vector3. + * @param v2 Second Vector3. + * @returns A new vector of the result of the given vectors. + */ + public static Divide(v1: Vector3, v2: Vector3): Vector3 { + return new Vector3(v1.x / v2.x, v1.y / v2.y, v1.z / v2.z); + } + + /** + * Returns whether two vector values are equal. + * @param v1 First Vector3. + * @param v2 Second Vector3. + * @returns Whether the vectors are equal. + */ + public static Equals(v1: V3, v2: V3): boolean { + return v1.x === v2.x && v1.y === v2.y; + } + + /** + * Create a new Vector3 by interpolating between two given vectors. + * @param v1 Start vector. + * @param v2 Target vector. + * @param t The amount to interpolate (0 being start, 1 being end, etc.) + * @returns A new Vector3 with the result. + */ + public static Lerp(v1: V3, v2: V3, t: number): Vector3 { + return new Vector3(lerp(v1.x, v2.x, t), lerp(v1.y, v2.y, t), lerp(v1.z, v2.z, t)); + } + + /** + * Create a new Vector3 by normalizing a vector (dividing by its length, or 1) + * @param vector The vector to normalize. + * @returns A new Vector3 of the normalized vector. + */ + public static Normalize(vector: V3): Vector3 { + return new Vector3(vector.x, vector.y, vector.z).normalize(); + } + + /** + * Get the magnitude of the vector (x^2 + y^2). + * @returns The magnitude of the vector. + */ + public getMagnitude(): number { + const { x, y, z } = this; + return x * x + y * y + z * z; + } + + /** + * Get the length of the vector (square root of magnitude) + * @returns The length of the vector. + */ + public getLength(): number { + return Math.sqrt(this.getMagnitude()); + } + + /** + * Create a copy of this Vector3. + * @returns A new instance of Vector3 with this vectors components. + */ + public copy(): Vector3 { + return new Vector3(this.x, this.y, this.z); + } + + /** + * Add another vector to this vector. + * @param other The other vector. + * @returns Itself. + */ + public add(other: V3): this { + this.x += other.x; + this.y += other.y; + this.z += other.z; + return this; + } + + /** + * Multiply this vector by another vector. + * @param other The other vector. + * @returns Itself. + */ + public multiply(other: V3): this { + this.x *= other.x; + this.y *= other.y; + this.z *= other.z; + return this; + } + + /** + * Multiply this vector by a single value. + * @param val The amount to multiply by. + * @returns Itself. + */ + public multiplyScalar(val: number): this { + this.x *= val; + this.y *= val; + this.z *= val; + return this; + } + + /** + * Divide this vector by a single value. + * @param val The amount to divide by. + * @returns Itself. + */ + public divideScalar(val: number): this { + this.x /= val; + this.y /= val; + this.z /= val; + return this; + } + + /** + * Multiply this vector by another vector. + * @param other The other vector. + * @returns Itself. + */ + public divide(other: V3): this { + this.x /= other.x; + this.y /= other.y; + this.z /= other.z; + return this; + } + + /** + * Set the components of this vector. + * @param x The x component. + * @param y The y component. + * @param z The z component. + * @returns Itself. + */ + public set(x: number, y: number, z: number): this { + this.x = x; + this.y = y; + this.z = z; + return this; + } + + /** + * Normalize this vector. Will reduce its length to a max of 1. + * @returns Itself. + */ + public normalize(): this { + return this.divideScalar(this.getLength() || 1); + } + + /** + * Interpolating this vector to a target vector. + * @param target Target vector. + * @param t The amount to interpolate (0 being itself, 1 being target, etc.) + * @returns Itself. + */ + public lerp(target: V3, t: number): this { + this.x = lerp(this.x, target.x, t); + this.y = lerp(this.y, target.y, t); + this.z = lerp(this.z, target.z, t); + return this; + } + + /** + * Clamp the components of this vector to a min and max value for each component. + * @param xMin The minimum value for x. + * @param xMax The maximum value for x. + * @param yMin The minimum value for y. + * @param yMax The maximum value for y. + * @param zMin The minimum value for z. + * @param zMax The maximum value for z. + * @returns Itself. + */ + public clamp( + xMin: number, + xMax: number, + yMin: number, + yMax: number, + zMin: number, + zMax: number, + ): this { + this.clampX(xMin, xMax); + this.clampY(yMin, yMax); + this.clampZ(zMin, zMax); + return this; + } + + /** + * Clamp the x component of this vector to a min and max value. + * @param min The minimum value for x. + * @param max The maximum value for x. + * @returns Itself. + */ + public clampX(min: number, max: number): this { + this.x = clamp(this.x, min, max); + return this; + } + + /** + * Clamp the y component of this vector to a min and max value. + * @param min The minimum value for y. + * @param max The maximum value for y. + * @returns Itself. + */ + public clampY(min: number, max: number): this { + this.y = clamp(this.y, min, max); + return this; + } + + /** + * Clamp the z component of this vector to a min and max value. + * @param min The minimum value for z. + * @param max The maximum value for z. + * @returns Itself. + */ + public clampZ(min: number, max: number): this { + this.z = clamp(this.z, min, max); + return this; + } + + /** + * Check equality between this vectors components and a given vectors components. + * @param val The vector to check equality. + * @returns If the vectors are equal. + */ + public equals(val: V3): boolean { + return val.x === this.x && val.y === this.y && val.z === this.z; + } + + /** + * Return a lightweight object literal with the x and y component. + * @returns An object literal with the vector set to x, y. + */ + public toLiteral(): V3 { + return { x: this.x, y: this.y, z: this.z }; + } + + public toString(): string { + return `Vector3 (${this.x.toString(10)}, ${this.y.toString(10)}, ${this.z.toString(10)})`; + } +} From c723b880adaee348fbcc69812661e832ab23daf0 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Mon, 24 Feb 2025 09:36:36 +0200 Subject: [PATCH 03/31] Make vec2 and vec3 constructor more robust --- lib/math/__tests__/vector2.test.ts | 29 +++++++++++++++ lib/math/__tests__/vector3.test.ts | 33 +++++++++++++++++ lib/math/vector2.ts | 59 +++++++++++++++++++++--------- lib/math/vector3.ts | 54 +++++++++++++++++++-------- 4 files changed, 142 insertions(+), 33 deletions(-) diff --git a/lib/math/__tests__/vector2.test.ts b/lib/math/__tests__/vector2.test.ts index 4be2818..55b3df3 100644 --- a/lib/math/__tests__/vector2.test.ts +++ b/lib/math/__tests__/vector2.test.ts @@ -4,6 +4,28 @@ import { describe, it, expect } from 'vitest'; import { Vector2 } from '../vector2.js'; describe('math/vector2', () => { + it('should init with either numbers or another vector', () => { + const numbers = new Vector2(5, 10); + expect(numbers).toBeInstanceOf(Vector2); + expect(numbers.x).toEqual(5); + expect(numbers.y).toEqual(10); + + const vec2Like = new Vector2({ x: 5, y: 10 }); + expect(vec2Like).toBeInstanceOf(Vector2); + expect(vec2Like.x).toEqual(5); + expect(vec2Like.y).toEqual(10); + + const vec2 = new Vector2(new Vector2(5, 10)); + expect(vec2).toBeInstanceOf(Vector2); + expect(vec2.x).toEqual(5); + expect(vec2.y).toEqual(10); + + const defaultVals = new Vector2(); + expect(defaultVals).toBeInstanceOf(Vector2); + expect(defaultVals.x).toEqual(0); + expect(defaultVals.y).toEqual(0); + }); + it('should store 2d vectors', () => { const vec2 = new Vector2(5, 10); expect(vec2).toBeInstanceOf(Vector2); @@ -182,5 +204,12 @@ describe('math/vector2', () => { const v2 = new Vector2(10, 5); expect(Vector2.Normalize(v2).getLength()).toBeCloseTo(1); }); + + it('Should assert if a value is vector2-like', () => { + expect(Vector2.IsVec2Like(5)).toEqual(false); + expect(Vector2.IsVec2Like({ x: 5 })).toEqual(false); + expect(Vector2.IsVec2Like({ x: 500, y: 300 })).toEqual(true); + expect(Vector2.IsVec2Like(new Vector2(5, 5))).toEqual(true); + }); }); }); diff --git a/lib/math/__tests__/vector3.test.ts b/lib/math/__tests__/vector3.test.ts index 814bf50..295d3e8 100644 --- a/lib/math/__tests__/vector3.test.ts +++ b/lib/math/__tests__/vector3.test.ts @@ -3,6 +3,32 @@ import { describe, it, expect } from 'vitest'; import { Vector3 } from '../vector3.js'; describe('math/vector3', () => { + it('should init with either numbers or another vector', () => { + const numbers = new Vector3(5, 10, 1); + expect(numbers).toBeInstanceOf(Vector3); + expect(numbers.x).toEqual(5); + expect(numbers.y).toEqual(10); + expect(numbers.z).toEqual(1); + + const vec3Like = new Vector3({ x: 5, y: 10, z: 1 }); + expect(vec3Like).toBeInstanceOf(Vector3); + expect(vec3Like.x).toEqual(5); + expect(vec3Like.y).toEqual(10); + expect(vec3Like.z).toEqual(1); + + const vec3 = new Vector3(new Vector3(5, 10, 1)); + expect(vec3).toBeInstanceOf(Vector3); + expect(vec3.x).toEqual(5); + expect(vec3.y).toEqual(10); + expect(vec3.z).toEqual(1); + + const defaultVals = new Vector3(); + expect(defaultVals).toBeInstanceOf(Vector3); + expect(defaultVals.x).toEqual(0); + expect(defaultVals.y).toEqual(0); + expect(defaultVals.z).toEqual(0); + }); + it('should store 3d vectors', () => { const vec3 = new Vector3(5, 10, 1); expect(vec3).toBeInstanceOf(Vector3); @@ -174,5 +200,12 @@ describe('math/vector3', () => { const v2 = new Vector3(10, 5, 5); expect(Vector3.Normalize(v2).getLength()).toBeCloseTo(1); }); + + it('Should assert if a value is vector3-like', () => { + expect(Vector3.IsVec3Like(5)).toEqual(false); + expect(Vector3.IsVec3Like({ x: 5, y: 300 })).toEqual(false); + expect(Vector3.IsVec3Like({ x: 500, y: 300, z: 200 })).toEqual(true); + expect(Vector3.IsVec3Like(new Vector3(5, 5, 5))).toEqual(true); + }); }); }); diff --git a/lib/math/vector2.ts b/lib/math/vector2.ts index 50a7ca5..7525542 100644 --- a/lib/math/vector2.ts +++ b/lib/math/vector2.ts @@ -1,17 +1,26 @@ -import { clamp } from './utils.js'; +import { clamp, lerp } from './utils.js'; -export interface V2 { +export interface IVec2 { x: number; y: number; } -export class Vector2 implements V2 { +type CtorArgs = [ref: IVec2] | [x?: number, y?: number]; + +export class Vector2 implements IVec2 { public x = 0; public y = 0; - constructor(x = 0, y = 0) { - this.x = x; - this.y = y; + constructor(...args: CtorArgs) { + const [first, second] = args; + + if (Vector2.IsVec2Like(first)) { + this.x = first.x; + this.y = first.y; + } else { + this.x = first ?? 0; + this.y = second ?? 0; + } } // Helpers @@ -91,7 +100,7 @@ export class Vector2 implements V2 { * @param v2 Second Vector2. * @returns Whether the vectors are equal. */ - public static Equals(v1: V2, v2: V2): boolean { + public static Equals(v1: IVec2, v2: IVec2): boolean { return v1.x === v2.x && v1.y === v2.y; } @@ -102,8 +111,8 @@ export class Vector2 implements V2 { * @param t The amount to interpolate (0 being start, 1 being end, etc.) * @returns A new Vector2 with the result. */ - public static Lerp(v1: V2, v2: V2, t: number): Vector2 { - return new Vector2(v1.x + (v2.x - v1.x) * t, v1.y + (v2.y - v1.y) * t); + public static Lerp(v1: IVec2, v2: IVec2, t: number): Vector2 { + return new Vector2(lerp(v1.x, v2.x, t), lerp(v1.y, v2.y, t)); } /** @@ -111,10 +120,24 @@ export class Vector2 implements V2 { * @param vector The vector to normalize. * @returns A new Vector2 of the normalized vector. */ - public static Normalize(vector: V2): Vector2 { + public static Normalize(vector: IVec2): Vector2 { return new Vector2(vector.x, vector.y).normalize(); } + /** + * Asserts a given unknonwn value is Vector2-like. + * @param obj The value. + * @returns True if it is vec2-like. + */ + public static IsVec2Like(obj: unknown): obj is IVec2 { + return ( + typeof obj === 'object' && + obj !== null && + Object.hasOwn(obj, 'x') && + Object.hasOwn(obj, 'y') + ); + } + /** * Get the magnitude of the vector (x^2 + y^2). * @returns The magnitude of the vector. @@ -145,7 +168,7 @@ export class Vector2 implements V2 { * @param other The other vector. * @returns Itself. */ - public add(other: V2): this { + public add(other: IVec2): this { this.x += other.x; this.y += other.y; return this; @@ -156,7 +179,7 @@ export class Vector2 implements V2 { * @param other The other vector. * @returns Itself. */ - public multiply(other: V2): this { + public multiply(other: IVec2): this { this.x *= other.x; this.y *= other.y; return this; @@ -189,7 +212,7 @@ export class Vector2 implements V2 { * @param other The other vector. * @returns Itself. */ - public divide(other: V2): this { + public divide(other: IVec2): this { this.x /= other.x; this.y /= other.y; return this; @@ -221,9 +244,9 @@ export class Vector2 implements V2 { * @param t The amount to interpolate (0 being itself, 1 being target, etc.) * @returns Itself. */ - public lerp(target: V2, t: number): this { - this.x += (target.x - this.x) * t; - this.y += (target.y - this.y) * t; + public lerp(target: IVec2, t: number): this { + this.x += lerp(this.x, target.x, t); + this.y += lerp(this.y, target.y, t); return this; } @@ -268,7 +291,7 @@ export class Vector2 implements V2 { * @param val The vector to check equality. * @returns If the vectors are equal. */ - public equals(val: V2): boolean { + public equals(val: IVec2): boolean { return val.x === this.x && val.y === this.y; } @@ -276,7 +299,7 @@ export class Vector2 implements V2 { * Return a lightweight object literal with the x and y component. * @returns An object literal with the vector set to x, y. */ - public toLiteral(): V2 { + public toLiteral(): IVec2 { return { x: this.x, y: this.y }; } diff --git a/lib/math/vector3.ts b/lib/math/vector3.ts index a19bf65..9b71ddc 100644 --- a/lib/math/vector3.ts +++ b/lib/math/vector3.ts @@ -1,20 +1,29 @@ import { clamp, lerp } from './utils.js'; -export interface V3 { +export interface IVec3 { x: number; y: number; z: number; } -export class Vector3 implements V3 { +type CtorArgs = [ref: IVec3] | [x?: number, y?: number, z?: number]; + +export class Vector3 implements IVec3 { public x = 0; public y = 0; public z = 0; - constructor(x = 0, y = 0, z = 0) { - this.x = x; - this.y = y; - this.z = z; + constructor(...args: CtorArgs) { + const [first, second, third] = args; + if (Vector3.IsVec3Like(first)) { + this.x = first.x; + this.y = first.y; + this.z = first.z; + } else { + this.x = first ?? 0; + this.y = second ?? 0; + this.z = third ?? 0; + } } // Helpers @@ -107,7 +116,7 @@ export class Vector3 implements V3 { * @param v2 Second Vector3. * @returns Whether the vectors are equal. */ - public static Equals(v1: V3, v2: V3): boolean { + public static Equals(v1: IVec3, v2: IVec3): boolean { return v1.x === v2.x && v1.y === v2.y; } @@ -118,7 +127,7 @@ export class Vector3 implements V3 { * @param t The amount to interpolate (0 being start, 1 being end, etc.) * @returns A new Vector3 with the result. */ - public static Lerp(v1: V3, v2: V3, t: number): Vector3 { + public static Lerp(v1: IVec3, v2: IVec3, t: number): Vector3 { return new Vector3(lerp(v1.x, v2.x, t), lerp(v1.y, v2.y, t), lerp(v1.z, v2.z, t)); } @@ -127,10 +136,25 @@ export class Vector3 implements V3 { * @param vector The vector to normalize. * @returns A new Vector3 of the normalized vector. */ - public static Normalize(vector: V3): Vector3 { + public static Normalize(vector: IVec3): Vector3 { return new Vector3(vector.x, vector.y, vector.z).normalize(); } + /** + * Asserts a given unknonwn value is Vector3-like. + * @param obj The value. + * @returns True if it is vec3-like. + */ + public static IsVec3Like(obj: unknown): obj is IVec3 { + return ( + typeof obj === 'object' && + obj !== null && + Object.hasOwn(obj, 'x') && + Object.hasOwn(obj, 'y') && + Object.hasOwn(obj, 'z') + ); + } + /** * Get the magnitude of the vector (x^2 + y^2). * @returns The magnitude of the vector. @@ -161,7 +185,7 @@ export class Vector3 implements V3 { * @param other The other vector. * @returns Itself. */ - public add(other: V3): this { + public add(other: IVec3): this { this.x += other.x; this.y += other.y; this.z += other.z; @@ -173,7 +197,7 @@ export class Vector3 implements V3 { * @param other The other vector. * @returns Itself. */ - public multiply(other: V3): this { + public multiply(other: IVec3): this { this.x *= other.x; this.y *= other.y; this.z *= other.z; @@ -209,7 +233,7 @@ export class Vector3 implements V3 { * @param other The other vector. * @returns Itself. */ - public divide(other: V3): this { + public divide(other: IVec3): this { this.x /= other.x; this.y /= other.y; this.z /= other.z; @@ -244,7 +268,7 @@ export class Vector3 implements V3 { * @param t The amount to interpolate (0 being itself, 1 being target, etc.) * @returns Itself. */ - public lerp(target: V3, t: number): this { + public lerp(target: IVec3, t: number): this { this.x = lerp(this.x, target.x, t); this.y = lerp(this.y, target.y, t); this.z = lerp(this.z, target.z, t); @@ -313,7 +337,7 @@ export class Vector3 implements V3 { * @param val The vector to check equality. * @returns If the vectors are equal. */ - public equals(val: V3): boolean { + public equals(val: IVec3): boolean { return val.x === this.x && val.y === this.y && val.z === this.z; } @@ -321,7 +345,7 @@ export class Vector3 implements V3 { * Return a lightweight object literal with the x and y component. * @returns An object literal with the vector set to x, y. */ - public toLiteral(): V3 { + public toLiteral(): IVec3 { return { x: this.x, y: this.y, z: this.z }; } From d07bbb71edc160792e9d49842c2e9741a9db7bcc Mon Sep 17 00:00:00 2001 From: bengsfort Date: Fri, 28 Feb 2025 17:42:57 +0200 Subject: [PATCH 04/31] Implement scaffolding for quad tree, spatial hashes and collision primitives --- lib/geometry/aabb.ts | 134 +++++++++++++++++++++ lib/geometry/collisions.ts | 0 lib/geometry/primitives.ts | 11 ++ lib/math/vector2.ts | 12 +- lib/math/vector3.ts | 12 +- lib/partitions/__tests__/quad-tree.test.ts | 52 ++++++++ lib/partitions/quad-tree.ts | 110 +++++++++++++++++ lib/partitions/spatial-hash.ts | 0 8 files changed, 319 insertions(+), 12 deletions(-) create mode 100644 lib/geometry/aabb.ts create mode 100644 lib/geometry/collisions.ts create mode 100644 lib/geometry/primitives.ts create mode 100644 lib/partitions/__tests__/quad-tree.test.ts create mode 100644 lib/partitions/quad-tree.ts create mode 100644 lib/partitions/spatial-hash.ts diff --git a/lib/geometry/aabb.ts b/lib/geometry/aabb.ts new file mode 100644 index 0000000..1a61164 --- /dev/null +++ b/lib/geometry/aabb.ts @@ -0,0 +1,134 @@ +import { IVec3, Vector3 } from '../math/vector3.js'; + +import { IAABB } from './primitives.js'; + +interface IAABBExtents { + half: Vector3; + min: Vector3; + max: Vector3; +} + +export function calculateExtentsAABB(aabb: IAABB, cachedHalf?: Vector3): IAABBExtents { + const half = cachedHalf ?? new Vector3(aabb.size).divideScalar(2); + + return { + half, + min: Vector3.Subtract(aabb.position, half), + max: Vector3.Add(aabb.position, half), + }; +} + +// @todo - There likely needs to be a better way of caching the calculated values +// The current API design is because of the desire to allow the position and size +// to follow the IAABB interface so they can be used interchangeably, ie, someone +// can create their own implementation that adheres to the interface and use that +// with other parts of the library. +// +// The downside is that because of this, we cannot know if position or size has +// had some change, therefore we need to calculate it on the fly. In most cases, +// these should not take much time but it can very easily add up. +// +// Some options: +// 1. Implement a `dirty` flag on `Vector3` with a `markClean()` function. +// + Allows us to track when there have been changes, and re-cache when needed. +// - Requires Vector3 to add hidden invocations (getters/setters) for components. +// (not terrible but not good for perf or clarity). +// 2. Remove the IAABB interface dependency in favor of one that enforces caching. +// + Allows us to cache the calculations +// - Less portability +// 3. Provide `position` and `size` using getters + a proxy? +// + Allows us to cache calculations better +// - More complexity, less clarity +// - If using a proxy, I doubt that would be good for performance + +/** + * Implementation of an Axis-aligned bounding box. Implements IAABB interface + * for compat and portability. + * + * By default a 3D AABB implementation, however can be switched to a 2D version + * by making the `z` component of the `size` vector `0`. + * + * @example + * ``` + * // 3D AABB + * const aabb3d = new AABB( + * new Vector3(0, 0, 0), + * new Vector3(10, 10, 10), + * ); + * + * // 2D AABB + * const aabb2d = new AABB( + * new Vector3(0, 0, 0), + * new Vector3(10, 10, 0), + * ); + * ``` + */ +export class AABB implements IAABB { + public readonly position: Vector3; + public readonly size: Vector3; + + constructor(position: IVec3, size: IVec3) { + this.position = new Vector3(position); + this.size = new Vector3(size); + } + + public getHalf(): Vector3 { + return this.size.copy().divideScalar(2); + } + + public getExtents(): IAABBExtents { + return calculateExtentsAABB(this); + } + + public containsPoint(point: Vector3): boolean { + const { min, max } = calculateExtentsAABB(this); + + // 2D Mode + if (this.size.z === 0) { + return point.x >= min.x && point.x <= max.x && point.y >= min.y && point.y <= max.y; + } + + return ( + point.x >= min.x && + point.x <= max.x && + point.y >= min.y && + point.y <= max.y && + point.z >= min.z && + point.z <= max.z + ); + } + + // @todo - SHOULDNT THIS HAVE ORS?????? + public intersectsAABB(other: IAABB): boolean { + const a = calculateExtentsAABB(this); + const b = calculateExtentsAABB(other); + + const thisLeft = a.min.x; + const thisRight = a.max.x; + const thisTop = a.max.y; + const thisBottom = a.min.y; + + const otherLeft = b.min.x; + const otherRight = b.max.x; + const otherTop = b.max.y; + const otherBottom = b.min.y; + + const intersects2D = + thisLeft <= otherRight && + thisRight >= otherLeft && + thisBottom <= otherTop && + thisTop >= otherBottom; + + // 2D handling + if (this.size.z === 0) { + return intersects2D; + } + + const thisForward = a.max.z; + const thisBackward = a.min.z; + const otherForward = b.max.z; + const otherBackward = b.min.z; + + return intersects2D && thisForward >= otherBackward && thisBackward <= otherForward; + } +} diff --git a/lib/geometry/collisions.ts b/lib/geometry/collisions.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/geometry/primitives.ts b/lib/geometry/primitives.ts new file mode 100644 index 0000000..63d974d --- /dev/null +++ b/lib/geometry/primitives.ts @@ -0,0 +1,11 @@ +import { IVec3 } from '../math/vector3.js'; + +export interface IAABB { + readonly position: IVec3; + readonly size: IVec3; +} + +export interface ICircle { + readonly position: IVec3; + readonly radius: number; +} diff --git a/lib/math/vector2.ts b/lib/math/vector2.ts index 7525542..0d89b07 100644 --- a/lib/math/vector2.ts +++ b/lib/math/vector2.ts @@ -50,7 +50,7 @@ export class Vector2 implements IVec2 { * @param v2 Second Vector2. * @returns A new vector of the sum of the given vectors. */ - public static Add(v1: Vector2, v2: Vector2): Vector2 { + public static Add(v1: IVec2, v2: IVec2): Vector2 { return new Vector2(v1.x + v2.x, v1.y + v2.y); } @@ -60,7 +60,7 @@ export class Vector2 implements IVec2 { * @param v2 Second Vector2. * @returns A new vector of the difference of the given vectors. */ - public static Subtract(v1: Vector2, v2: Vector2): Vector2 { + public static Subtract(v1: IVec2, v2: IVec2): Vector2 { return new Vector2(v1.x - v2.x, v1.y - v2.y); } @@ -70,7 +70,7 @@ export class Vector2 implements IVec2 { * @param v2 Second Vector2. * @returns A new vector with the result of the given vectors. */ - public static Multiply(v1: Vector2, v2: Vector2): Vector2 { + public static Multiply(v1: IVec2, v2: IVec2): Vector2 { return new Vector2(v1.x * v2.x, v1.y * v2.y); } @@ -80,8 +80,8 @@ export class Vector2 implements IVec2 { * @param val The value to multiply each component by. * @returns A new vector with the result. */ - public static MultiplyScalar(v1: Vector2, val: number): Vector2 { - return v1.copy().multiplyScalar(val); + public static MultiplyScalar(v1: IVec2, val: number): Vector2 { + return new Vector2(v1).multiplyScalar(val); } /** @@ -90,7 +90,7 @@ export class Vector2 implements IVec2 { * @param v2 Second Vector2. * @returns A new vector of the result of the given vectors. */ - public static Divide(v1: Vector2, v2: Vector2): Vector2 { + public static Divide(v1: IVec2, v2: IVec2): Vector2 { return new Vector2(v1.x / v2.x, v1.y / v2.y); } diff --git a/lib/math/vector3.ts b/lib/math/vector3.ts index 9b71ddc..8bca482 100644 --- a/lib/math/vector3.ts +++ b/lib/math/vector3.ts @@ -66,7 +66,7 @@ export class Vector3 implements IVec3 { * @param v2 Second Vector3. * @returns A new vector of the sum of the given vectors. */ - public static Add(v1: Vector3, v2: Vector3): Vector3 { + public static Add(v1: IVec3, v2: IVec3): Vector3 { return new Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); } @@ -76,7 +76,7 @@ export class Vector3 implements IVec3 { * @param v2 Second Vector3. * @returns A new vector of the difference of the given vectors. */ - public static Subtract(v1: Vector3, v2: Vector3): Vector3 { + public static Subtract(v1: IVec3, v2: IVec3): Vector3 { return new Vector3(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z); } @@ -86,7 +86,7 @@ export class Vector3 implements IVec3 { * @param v2 Second Vector3. * @returns A new vector with the result of the given vectors. */ - public static Multiply(v1: Vector3, v2: Vector3): Vector3 { + public static Multiply(v1: IVec3, v2: IVec3): Vector3 { return new Vector3(v1.x * v2.x, v1.y * v2.y, v1.z * v2.z); } @@ -96,8 +96,8 @@ export class Vector3 implements IVec3 { * @param val The value to multiply each component by. * @returns A new vector with the result. */ - public static MultiplyScalar(v1: Vector3, val: number): Vector3 { - return v1.copy().multiplyScalar(val); + public static MultiplyScalar(v1: IVec3, val: number): Vector3 { + return new Vector3(v1).multiplyScalar(val); } /** @@ -106,7 +106,7 @@ export class Vector3 implements IVec3 { * @param v2 Second Vector3. * @returns A new vector of the result of the given vectors. */ - public static Divide(v1: Vector3, v2: Vector3): Vector3 { + public static Divide(v1: IVec3, v2: IVec3): Vector3 { return new Vector3(v1.x / v2.x, v1.y / v2.y, v1.z / v2.z); } diff --git a/lib/partitions/__tests__/quad-tree.test.ts b/lib/partitions/__tests__/quad-tree.test.ts new file mode 100644 index 0000000..96c0a45 --- /dev/null +++ b/lib/partitions/__tests__/quad-tree.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from 'vitest'; + +describe('partitions/quad-tree', () => { + describe('core functionality', () => { + it('should accept items that are within the trees bounds', () => { + expect('@todo').toEqual(true); + }); + + it('should remove items within the tree', () => { + expect('@todo').toEqual(true); + }); + + it('should not merge overlapping items', () => { + expect('@todo').toEqual(true); + }); + + it('should subdivide the tree once it reaches capacity', () => { + // @todo: Make sure the tree re-distributes everything + expect('@todo').toEqual(true); + }); + + it('should store items that span multiple boundaries in the highest common node', () => { + expect('@todo').toEqual(true); + }); + + it('should reset and empty the tree', () => { + expect('@todo').toEqual(true); + }); + + it('should not subdivide if there is no more space to', () => { + // ie: if we have reached max subdivision, don't try again + // (cant have a quad that is 0.5 wide or something) + expect('@todo').toEqual(true); + }); + }); + + describe('querying', () => { + it('should retrieve points within a given rectangular range', () => { + expect('@todo').toEqual(true); + }); + + it('should retrieve points within a given circular range', () => { + expect('@todo').toEqual(true); + }); + + it('should handle large queries', () => { + expect('@todo').toEqual(true); + }); + + // @todo: performance test? + }); +}); diff --git a/lib/partitions/quad-tree.ts b/lib/partitions/quad-tree.ts new file mode 100644 index 0000000..87ec736 --- /dev/null +++ b/lib/partitions/quad-tree.ts @@ -0,0 +1,110 @@ +interface Point { + x: number; + y: number; +} + +interface Bounds { + position: Point; + includesPoint(point: Point): boolean; + intersects(other: Bounds): boolean; +} + +interface RectBounds extends Bounds { + half: number; +} + +type QueryResultTuple = [id: string, point: Point]; + +export class QuadTree { + public readonly capacity: number; + public readonly bounds: RectBounds; + + #_items: Map; + #_nw?: QuadTree; + #_ne?: QuadTree; + #_sw?: QuadTree; + #_se?: QuadTree; + + constructor(bounds: RectBounds, capacity: number) { + this.bounds = bounds; + this.capacity = capacity; + this.#_items = new Map(); + } + + public insert(id: string, item: Bounds): boolean { + // Ignore objects that don't belong in this partition + if (!this.bounds.intersects(item)) { + return false; + } + + // If we haven't subdivided yet, and have capacity, insert here + if (this.#_items.size < this.capacity && !this.#_nw) { + this.#_items.set(id, item); + return true; + } + + // At this point we are over capacity. Subdivide if we haven't yet. + if (!this.#_nw) { + this.subdivide(); + } + + // Add the item to whichever partition accepts it + if (this.#_nw?.insert(id, item)) return true; + if (this.#_ne?.insert(id, item)) return true; + if (this.#_sw?.insert(id, item)) return true; + if (this.#_se?.insert(id, item)) return true; + + // Something went terribly wrong and the point could not be inserted. + // This SHOULD never happen, but could in theory happen if every partition + // downstream is full and can no longer be subdivided. + return false; + } + + public remove(id: string): boolean { + // If this node has the item, just deleted it and return. + if (this.#_items.has(id)) { + return this.#_items.delete(id); + } + + // If we haven't subdivided yet, there is nothing else to do so return. + if (!this.#_nw) { + return false; + } + + // Try to remove it from any downstream nodes + if (this.#_nw.remove(id)) return true; + if (this.#_ne?.remove(id)) return true; + if (this.#_sw?.remove(id)) return true; + if (this.#_se?.remove(id)) return true; + + // The point could not be found anywhere in this tree. + return false; + } + + public subdivide(): boolean { + const { position, half } = this.bounds; + + return false; + } + + public queryRange(range: Bounds, results: QueryResultTuple[] = []): QueryResultTuple[] { + // @todo + return results; + } + + public clear(): void { + this.#_items.clear(); + + this.#_nw?.clear(); + this.#_nw = undefined; + + this.#_ne?.clear(); + this.#_ne = undefined; + + this.#_sw?.clear(); + this.#_sw = undefined; + + this.#_se?.clear(); + this.#_se = undefined; + } +} diff --git a/lib/partitions/spatial-hash.ts b/lib/partitions/spatial-hash.ts new file mode 100644 index 0000000..e69de29 From 81bf2cf6dbd4189fcd73d98ba7cd0ef129bfe77e Mon Sep 17 00:00:00 2001 From: bengsfort Date: Fri, 28 Feb 2025 22:01:44 +0200 Subject: [PATCH 05/31] Implement AABB + point detection --- lib/geometry/__tests__/collisions.test.ts | 59 ++++++++++++++++++++++ lib/geometry/collisions.ts | 60 +++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 lib/geometry/__tests__/collisions.test.ts diff --git a/lib/geometry/__tests__/collisions.test.ts b/lib/geometry/__tests__/collisions.test.ts new file mode 100644 index 0000000..141993d --- /dev/null +++ b/lib/geometry/__tests__/collisions.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest'; + +import { Vector2 } from '../../math/vector2.js'; +import { Vector3 } from '../../math/vector3.js'; +import { aabbContainsPoint2D, aabbContainsPoint3D } from '../collisions.js'; +import type { IAABB } from '../primitives.js'; + +describe('geometry/collisions', () => { + describe('aabbContainsPoint2D', () => { + it('should return if a 2d point is contained by an AABB', () => { + const bounds: IAABB = { + size: new Vector3(10, 10), + position: new Vector3(0, 0), + }; + + // Check extents + expect(aabbContainsPoint2D(bounds, new Vector2(5, 5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(-5, -5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(-5, 5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(5, -5))).toEqual(true); + + // Check inside + expect(aabbContainsPoint2D(bounds, new Vector2(2, 0))).toEqual(true); + + // Make sure it is false outside + expect(aabbContainsPoint2D(bounds, new Vector2(10, 10))).toEqual(false); + expect(aabbContainsPoint2D(bounds, new Vector2(10, 0))).toEqual(false); + expect(aabbContainsPoint2D(bounds, new Vector2(0, 10))).toEqual(false); + }); + }); + + describe('aabbContainsPoint3D', () => { + it('should return if a 3d point is contained by an AABB', () => { + const bounds: IAABB = { + size: new Vector3(10, 10, 10), + position: new Vector3(0, 0, 0), + }; + + // Check extents + expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, -5))).toEqual(true); + + // Check inside + expect(aabbContainsPoint3D(bounds, new Vector3(0, 0, 0))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(2, 0, 1))).toEqual(true); + + // Make sure it is false outside + expect(aabbContainsPoint3D(bounds, new Vector3(10, 10, 10))).toEqual(false); + expect(aabbContainsPoint3D(bounds, new Vector3(10, 0, 0))).toEqual(false); + expect(aabbContainsPoint3D(bounds, new Vector3(0, 10, 0))).toEqual(false); + }); + }); +}); diff --git a/lib/geometry/collisions.ts b/lib/geometry/collisions.ts index e69de29..0cb8f94 100644 --- a/lib/geometry/collisions.ts +++ b/lib/geometry/collisions.ts @@ -0,0 +1,60 @@ +import type { IVec2 } from '../math/vector2.js'; +import type { IVec3 } from '../math/vector3.js'; + +import type { IAABB, ICircle } from './primitives.js'; + +export function aabbContainsPoint2D(bounds: IAABB, point: IVec2): boolean { + const half: IVec2 = { + x: bounds.size.x * 0.5, + y: bounds.size.y * 0.5, + }; + + return ( + point.x >= bounds.position.x - half.x && + point.x <= bounds.position.x + half.x && + point.y >= bounds.position.y - half.y && + point.y <= bounds.position.y + half.y + ); +} + +export function aabbContainsPoint3D(bounds: IAABB, point: IVec3): boolean { + const half: IVec3 = { + x: bounds.size.x * 0.5, + y: bounds.size.y * 0.5, + z: bounds.size.z * 0.5, + }; + + return ( + point.x >= bounds.position.x - half.x && + point.x <= bounds.position.x + half.x && + point.y >= bounds.position.y - half.y && + point.y <= bounds.position.y + half.y && + point.z >= bounds.position.z - half.z && + point.z <= bounds.position.z + half.z + ); +} + +export function aabbIntersectsAabb(a: IAABB, b: IAABB): boolean { + // @todo + return false; +} + +export function aabbIntersectsSphere(a: IAABB, b: ICircle): boolean { + // @todo + return false; +} + +export function sphereContainsPoint2D(sphere: ICircle, point: IVec2): boolean { + // @todo + return false; +} + +export function sphereContainsPoint3D(sphere: ICircle, point: IVec3): boolean { + // @todo + return false; +} + +export function sphereIntersectsSphere(a: ICircle, b: ICircle): boolean { + // @todo + return false; +} From 4eacaa6a8a3dd1405047973c58af9b1dff9f4941 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Mon, 3 Mar 2025 09:27:04 +0200 Subject: [PATCH 06/31] Split collision handling to 2d and 3d modules, add more implementations --- lib/geometry/__tests__/collisions.test.ts | 59 ----- lib/geometry/__tests__/collisions2d.test.ts | 251 ++++++++++++++++++++ lib/geometry/__tests__/collisions3d.test.ts | 121 ++++++++++ lib/geometry/collisions.ts | 60 ----- lib/geometry/collisions2d.ts | 41 ++++ lib/geometry/collisions3d.ts | 40 ++++ lib/geometry/primitives.ts | 15 +- 7 files changed, 466 insertions(+), 121 deletions(-) delete mode 100644 lib/geometry/__tests__/collisions.test.ts create mode 100644 lib/geometry/__tests__/collisions2d.test.ts create mode 100644 lib/geometry/__tests__/collisions3d.test.ts delete mode 100644 lib/geometry/collisions.ts create mode 100644 lib/geometry/collisions2d.ts create mode 100644 lib/geometry/collisions3d.ts diff --git a/lib/geometry/__tests__/collisions.test.ts b/lib/geometry/__tests__/collisions.test.ts deleted file mode 100644 index 141993d..0000000 --- a/lib/geometry/__tests__/collisions.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, it, expect } from 'vitest'; - -import { Vector2 } from '../../math/vector2.js'; -import { Vector3 } from '../../math/vector3.js'; -import { aabbContainsPoint2D, aabbContainsPoint3D } from '../collisions.js'; -import type { IAABB } from '../primitives.js'; - -describe('geometry/collisions', () => { - describe('aabbContainsPoint2D', () => { - it('should return if a 2d point is contained by an AABB', () => { - const bounds: IAABB = { - size: new Vector3(10, 10), - position: new Vector3(0, 0), - }; - - // Check extents - expect(aabbContainsPoint2D(bounds, new Vector2(5, 5))).toEqual(true); - expect(aabbContainsPoint2D(bounds, new Vector2(-5, -5))).toEqual(true); - expect(aabbContainsPoint2D(bounds, new Vector2(-5, 5))).toEqual(true); - expect(aabbContainsPoint2D(bounds, new Vector2(5, -5))).toEqual(true); - - // Check inside - expect(aabbContainsPoint2D(bounds, new Vector2(2, 0))).toEqual(true); - - // Make sure it is false outside - expect(aabbContainsPoint2D(bounds, new Vector2(10, 10))).toEqual(false); - expect(aabbContainsPoint2D(bounds, new Vector2(10, 0))).toEqual(false); - expect(aabbContainsPoint2D(bounds, new Vector2(0, 10))).toEqual(false); - }); - }); - - describe('aabbContainsPoint3D', () => { - it('should return if a 3d point is contained by an AABB', () => { - const bounds: IAABB = { - size: new Vector3(10, 10, 10), - position: new Vector3(0, 0, 0), - }; - - // Check extents - expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, 5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, 5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, 5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, -5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, 5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, -5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, -5))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, -5))).toEqual(true); - - // Check inside - expect(aabbContainsPoint3D(bounds, new Vector3(0, 0, 0))).toEqual(true); - expect(aabbContainsPoint3D(bounds, new Vector3(2, 0, 1))).toEqual(true); - - // Make sure it is false outside - expect(aabbContainsPoint3D(bounds, new Vector3(10, 10, 10))).toEqual(false); - expect(aabbContainsPoint3D(bounds, new Vector3(10, 0, 0))).toEqual(false); - expect(aabbContainsPoint3D(bounds, new Vector3(0, 10, 0))).toEqual(false); - }); - }); -}); diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts new file mode 100644 index 0000000..14fed76 --- /dev/null +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -0,0 +1,251 @@ +import { describe, it, expect } from 'vitest'; + +import { Vector2 } from '../../math/vector2.js'; +import { + aabbContainsPoint2D, + aabbIntersectsAabb2D, + aabbIntersectsCircle2D, + circleContainsPoint2D, + circleIntersectsCircle2D, +} from '../collisions2d.js'; +import type { IAABB2D, ICircle } from '../primitives.js'; + +describe('geometry/collisions', () => { + describe('aabbContainsPoint2D', () => { + it('should return if a 2d point is contained by an AABB', () => { + const bounds: IAABB2D = { + min: new Vector2(-5, -5), + max: new Vector2(5, 5), + }; + + // Check extents + expect(aabbContainsPoint2D(bounds, new Vector2(5, 5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(-5, -5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(-5, 5))).toEqual(true); + expect(aabbContainsPoint2D(bounds, new Vector2(5, -5))).toEqual(true); + + // Check inside + expect(aabbContainsPoint2D(bounds, new Vector2(2, 0))).toEqual(true); + + // Make sure it is false outside + expect(aabbContainsPoint2D(bounds, new Vector2(10, 10))).toEqual(false); + expect(aabbContainsPoint2D(bounds, new Vector2(10, 0))).toEqual(false); + expect(aabbContainsPoint2D(bounds, new Vector2(0, 10))).toEqual(false); + }); + + describe('aabbIntersectsAabb2D', () => { + it('should return true if two aabb intersect', () => { + const base: IAABB2D = { + min: new Vector2(-5, -5), + max: new Vector2(5, 5), + }; + + // Overlaps slightly on the top-right edge + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(2.5, 2.5), + max: new Vector2(7.5, 7.5), + }), + ).toEqual(true); + + // Overlaps entirely on bottom + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(-50, -10), + max: new Vector2(50, -2.5), + }), + ).toEqual(true); + + // Overlaps entirely on side + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(2.5, -50), + max: new Vector2(7.5, 50), + }), + ).toEqual(true); + + // Pokes in on one side only + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(2.5, 2), + max: new Vector2(7.5, 4), + }), + ).toEqual(true); + }); + + it('should return false if two aabb do not intersect', () => { + const base: IAABB2D = { + min: new Vector2(0, 0), + max: new Vector2(2, 2), + }; + + // No-where near + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(100, 100), + max: new Vector2(200, 200), + }), + ).toEqual(false); + + // Close and above + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(-2, 2.5), + max: new Vector2(4, 4), + }), + ).toEqual(false); + + // Close to the right + expect( + aabbIntersectsAabb2D(base, { + min: new Vector2(2.5, -2), + max: new Vector2(3, 4), + }), + ).toEqual(false); + }); + + it('should return true if an aabb contains another', () => { + const a: IAABB2D = { + min: new Vector2(-5, -5), + max: new Vector2(5, 5), + }; + const b: IAABB2D = { + min: new Vector2(-10, -10), + max: new Vector2(10, 10), + }; + + expect(aabbIntersectsAabb2D(a, b)).toEqual(true); + expect(aabbIntersectsAabb2D(b, a)).toEqual(true); + }); + }); + }); + + describe('closestPointOnAabb2D', () => { + it('should return the closest point on the bounds', () => { + const bounds: IAABB2D = { + min: new Vector2(-5, -5), + max: new Vector2(5, 5), + }; + }); + + it('should return the point if it is within the bounds', () => { }); + }); + + describe('aabbIntersectsCircle2D', () => { + it('should return true if a circle and aabb intersect', () => { + const a: IAABB2D = { + min: new Vector2(0, 0), + max: new Vector2(5, 5), + }; + + const overlapping: ICircle = { + position: new Vector2(6, 6), + radius: 2, + }; + + const contained: ICircle = { + position: new Vector2(2.5, 2.5), + radius: 1, + }; + + expect(aabbIntersectsCircle2D(a, overlapping)).toEqual(true); + expect(aabbIntersectsCircle2D(a, contained)).toEqual(true); + }); + + it('should return false if a circle and aabb do not intersect', () => { + const a: IAABB2D = { + min: new Vector2(0, 0), + max: new Vector2(5, 5), + }; + + const far: ICircle = { + position: new Vector2(100, 100), + radius: 2, + }; + + const close: ICircle = { + position: new Vector2(6, 6), + radius: 1, + }; + + expect(aabbIntersectsCircle2D(a, far)).toEqual(false); + expect(aabbIntersectsCircle2D(a, close)).toEqual(false); + }); + }); + + describe('circleContainsPoint2D', () => { + it('should detect points within a circle', () => { + const circle: ICircle = { + position: new Vector2(0, 0), + radius: 5, + }; + + // Clear cases + expect(circleContainsPoint2D(circle, new Vector2(0, 0))).toEqual(true); + expect(circleContainsPoint2D(circle, new Vector2(4, 2))).toEqual(true); + expect(circleContainsPoint2D(circle, new Vector2(-4, -4))).toEqual(true); + expect(circleContainsPoint2D(circle, new Vector2(100, 100))).toEqual(false); + + // Since it is a circle, { radius, radius } should be false. + expect( + circleContainsPoint2D(circle, new Vector2(circle.radius, circle.radius)), + ).toEqual(false); + expect( + circleContainsPoint2D(circle, new Vector2(-circle.radius, -circle.radius)), + ).toEqual(false); + expect( + circleContainsPoint2D(circle, new Vector2(circle.radius, -circle.radius)), + ).toEqual(false); + expect( + circleContainsPoint2D(circle, new Vector2(-circle.radius, circle.radius)), + ).toEqual(false); + + // Straight up and side should be true. + const top = new Vector2(circle.position.x, circle.position.y + circle.radius); + const left = new Vector2(circle.position.x - circle.radius, circle.position.y); + const right = new Vector2(circle.position.x + circle.radius, circle.position.y); + const bottom = new Vector2(circle.position.x, circle.position.y - circle.radius); + expect(circleContainsPoint2D(circle, top)).toEqual(true); + expect(circleContainsPoint2D(circle, left)).toEqual(true); + expect(circleContainsPoint2D(circle, right)).toEqual(true); + expect(circleContainsPoint2D(circle, bottom)).toEqual(true); + }); + }); + + describe('circleIntersectsCircle2D', () => { + it('should detect if two circles intersect', () => { + const base: ICircle = { + position: new Vector2(0, 0), + radius: 5, + }; + + // Intersection cases + expect( + circleIntersectsCircle2D(base, { + position: new Vector2(5, 0), + radius: 3, + }), + ).toEqual(true); + expect( + circleIntersectsCircle2D(base, { + position: new Vector2(0, -5), + radius: 3, + }), + ).toEqual(true); + + // No way jose cases + expect( + circleIntersectsCircle2D(base, { + position: new Vector2(100, 100), + radius: 5, + }), + ).toEqual(false); + expect( + circleIntersectsCircle2D(base, { + position: new Vector2(10, 10), + radius: 4, + }), + ).toEqual(false); + }); + }); +}); diff --git a/lib/geometry/__tests__/collisions3d.test.ts b/lib/geometry/__tests__/collisions3d.test.ts new file mode 100644 index 0000000..fe39d01 --- /dev/null +++ b/lib/geometry/__tests__/collisions3d.test.ts @@ -0,0 +1,121 @@ +import { describe, expect, it } from 'vitest'; + +import { Vector3 } from '../../math/vector3.js'; +import { aabbContainsPoint3D, aabbIntersectsAabb3D } from '../collisions3d.js'; +import { IAABB } from '../primitives.js'; + +describe('geometry/collisions3d', () => { + describe('aabbContainsPoint3D', () => { + it('should return if a 3d point is contained by an AABB', () => { + const bounds: IAABB = { + min: new Vector3(-5, -5, -5), + max: new Vector3(5, 5, 5), + }; + + // Check extents + expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, 5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, 5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, 5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(5, -5, -5))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(-5, -5, -5))).toEqual(true); + + // Check inside + expect(aabbContainsPoint3D(bounds, new Vector3(0, 0, 0))).toEqual(true); + expect(aabbContainsPoint3D(bounds, new Vector3(2, 0, 1))).toEqual(true); + + // Make sure it is false outside + expect(aabbContainsPoint3D(bounds, new Vector3(10, 10, 10))).toEqual(false); + expect(aabbContainsPoint3D(bounds, new Vector3(10, 0, 0))).toEqual(false); + expect(aabbContainsPoint3D(bounds, new Vector3(0, 10, 0))).toEqual(false); + }); + }); + + describe('aabbIntersectsAabb3D', () => { + it('should return true if two aabb intersect', () => { + const base: IAABB = { + min: new Vector3(-5, -5, -5), + max: new Vector3(5, 5, 5), + }; + + // Overlaps slightly on the top-right edge + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(2.5, 2, 4), + max: new Vector3(5, 5, 5), + }), + ).toEqual(true); + + // Overlaps entirely on bottom + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(-10, -10, -10), + max: new Vector3(10, 0, 10), + }), + ).toEqual(true); + + // Overlaps entirely on side + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(0, -10, -10), + max: new Vector3(10, 10, 10), + }), + ).toEqual(true); + + // Pokes in on one side only + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(1, 2, 2), + max: new Vector3(10, 4, 4), + }), + ).toEqual(true); + }); + + it('should return false if two aabb do not intersect', () => { + const base: IAABB = { + min: new Vector3(2, 2, 2), + max: new Vector3(4, 4, 4), + }; + + // No-where near + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(100, 100, 100), + max: new Vector3(110, 110, 110), + }), + ).toEqual(false); + + // Close and above + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(2, 5, 2), + max: new Vector3(4, 10, 4), + }), + ).toEqual(false); + + // Close to the right + expect( + aabbIntersectsAabb3D(base, { + min: new Vector3(5, 2, 2), + max: new Vector3(10, 4, 4), + }), + ).toEqual(false); + }); + + it('should return true if an aabb contains another', () => { + const a: IAABB = { + min: new Vector3(-10, -10, -10), + max: new Vector3(10, 10, 10), + }; + const b: IAABB = { + min: new Vector3(-5, -5, -5), + max: new Vector3(5, 5, 5), + }; + + expect(aabbIntersectsAabb3D(a, b)).toEqual(true); + expect(aabbIntersectsAabb3D(b, a)).toEqual(true); + }); + }); +}); diff --git a/lib/geometry/collisions.ts b/lib/geometry/collisions.ts deleted file mode 100644 index 0cb8f94..0000000 --- a/lib/geometry/collisions.ts +++ /dev/null @@ -1,60 +0,0 @@ -import type { IVec2 } from '../math/vector2.js'; -import type { IVec3 } from '../math/vector3.js'; - -import type { IAABB, ICircle } from './primitives.js'; - -export function aabbContainsPoint2D(bounds: IAABB, point: IVec2): boolean { - const half: IVec2 = { - x: bounds.size.x * 0.5, - y: bounds.size.y * 0.5, - }; - - return ( - point.x >= bounds.position.x - half.x && - point.x <= bounds.position.x + half.x && - point.y >= bounds.position.y - half.y && - point.y <= bounds.position.y + half.y - ); -} - -export function aabbContainsPoint3D(bounds: IAABB, point: IVec3): boolean { - const half: IVec3 = { - x: bounds.size.x * 0.5, - y: bounds.size.y * 0.5, - z: bounds.size.z * 0.5, - }; - - return ( - point.x >= bounds.position.x - half.x && - point.x <= bounds.position.x + half.x && - point.y >= bounds.position.y - half.y && - point.y <= bounds.position.y + half.y && - point.z >= bounds.position.z - half.z && - point.z <= bounds.position.z + half.z - ); -} - -export function aabbIntersectsAabb(a: IAABB, b: IAABB): boolean { - // @todo - return false; -} - -export function aabbIntersectsSphere(a: IAABB, b: ICircle): boolean { - // @todo - return false; -} - -export function sphereContainsPoint2D(sphere: ICircle, point: IVec2): boolean { - // @todo - return false; -} - -export function sphereContainsPoint3D(sphere: ICircle, point: IVec3): boolean { - // @todo - return false; -} - -export function sphereIntersectsSphere(a: ICircle, b: ICircle): boolean { - // @todo - return false; -} diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts new file mode 100644 index 0000000..5a28ac9 --- /dev/null +++ b/lib/geometry/collisions2d.ts @@ -0,0 +1,41 @@ +import { clamp } from '../math/utils.js'; +import { IVec2, Vector2 } from '../math/vector2.js'; + +import { IAABB2D, ICircle } from './primitives.js'; + +export function aabbContainsPoint2D(bounds: IAABB2D, point: IVec2): boolean { + return ( + point.x >= bounds.min.x && + point.x <= bounds.max.x && + point.y >= bounds.min.y && + point.y <= bounds.max.y + ); +} + +export function aabbIntersectsAabb2D(a: IAABB2D, b: IAABB2D): boolean { + return ( + a.min.x <= b.max.x && a.max.x >= b.min.x && a.min.y <= b.max.y && a.max.y >= b.min.y + ); +} + +export function closestPointOnAabb2D(a: IAABB2D, point: IVec2): IVec2 { + return new Vector2(clamp(point.x, a.min.x, a.max.x), clamp(point.y, a.min.y, a.max.y)); +} + +export function aabbIntersectsCircle2D(a: IAABB2D, b: ICircle): boolean { + // @todo + return false; +} + +export function closestPointOnCircle2D(a: ICircle, point: IVec2): boolean { + // @todo + return false; +} + +export function circleContainsPoint2D(circle: ICircle, point: IVec2): boolean { + return false; +} + +export function circleIntersectsCircle2D(a: ICircle, b: ICircle): boolean { + return false; +} diff --git a/lib/geometry/collisions3d.ts b/lib/geometry/collisions3d.ts new file mode 100644 index 0000000..617e550 --- /dev/null +++ b/lib/geometry/collisions3d.ts @@ -0,0 +1,40 @@ +import type { IVec2 } from '../math/vector2.js'; +import type { IVec3 } from '../math/vector3.js'; + +import type { IAABB, IAABB2D, ICircle, ISphere } from './primitives.js'; + +export function aabbContainsPoint3D(bounds: IAABB, point: IVec3): boolean { + return ( + point.x >= bounds.min.x && + point.x <= bounds.max.x && + point.y >= bounds.min.y && + point.y <= bounds.max.y && + point.z >= bounds.min.z && + point.z <= bounds.max.z + ); +} + +export function aabbIntersectsAabb3D(a: IAABB, b: IAABB): boolean { + return ( + a.min.x <= b.max.x && + a.max.x >= b.min.x && + a.min.y <= b.max.y && + a.max.y >= b.min.y && + a.min.z <= b.max.z && + a.max.z >= b.min.z + ); +} + +export function aabbIntersectsSphere3D(a: IAABB, b: ISphere): boolean { + return false; +} + +export function sphereContainsPoint3D(sphere: ISphere, point: IVec3): boolean { + // @todo + return false; +} + +export function sphereIntersectsSphere3D(a: ISphere, b: ISphere): boolean { + // @todo + return false; +} diff --git a/lib/geometry/primitives.ts b/lib/geometry/primitives.ts index 63d974d..9c4d356 100644 --- a/lib/geometry/primitives.ts +++ b/lib/geometry/primitives.ts @@ -1,11 +1,22 @@ +import { IVec2 } from '../math/vector2.js'; import { IVec3 } from '../math/vector3.js'; export interface IAABB { - readonly position: IVec3; - readonly size: IVec3; + readonly min: IVec3; + readonly max: IVec3; +} + +export interface IAABB2D { + readonly min: IVec2; + readonly max: IVec2; } export interface ICircle { + readonly position: IVec2; + readonly radius: number; +} + +export interface ISphere { readonly position: IVec3; readonly radius: number; } From 615757e2b76746afb41c49c998cb29d3249d805f Mon Sep 17 00:00:00 2001 From: bengsfort Date: Thu, 6 Mar 2025 08:06:13 +0200 Subject: [PATCH 07/31] Continue adding tests for collisions --- lib/geometry/__tests__/collisions2d.test.ts | 32 ++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts index 14fed76..58350a6 100644 --- a/lib/geometry/__tests__/collisions2d.test.ts +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -7,6 +7,7 @@ import { aabbIntersectsCircle2D, circleContainsPoint2D, circleIntersectsCircle2D, + closestPointOnAabb2D, } from '../collisions2d.js'; import type { IAABB2D, ICircle } from '../primitives.js'; @@ -126,9 +127,30 @@ describe('geometry/collisions', () => { min: new Vector2(-5, -5), max: new Vector2(5, 5), }; + + const above = closestPointOnAabb2D(bounds, new Vector2(0, 10)); + expect(above.x).toEqual(0); + expect(above.y).toEqual(5); + + const left = closestPointOnAabb2D(bounds, new Vector2(-10, 0)); + expect(left.x).toEqual(-5); + expect(left.y).toEqual(0); + + const oblique = closestPointOnAabb2D(bounds, new Vector2(10, 10)); + expect(oblique.x).toEqual(5); + expect(oblique.y).toEqual(5); }); - it('should return the point if it is within the bounds', () => { }); + it('should return the point if it is within the bounds', () => { + const bounds: IAABB2D = { + min: new Vector2(-5, -5), + max: new Vector2(5, 5), + }; + + const same = closestPointOnAabb2D(bounds, new Vector2(-2, 3)); + expect(same.x).toEqual(-2); + expect(same.y).toEqual(3); + }); }); describe('aabbIntersectsCircle2D', () => { @@ -248,4 +270,12 @@ describe('geometry/collisions', () => { ).toEqual(false); }); }); + + describe('closestPointOnCircle2D', () => { + + }); + + describe('circleContainsPoint2D', () => { + + }); }); From 1fb17cde809469bad2fd82b21d7ef29500c600b5 Mon Sep 17 00:00:00 2001 From: Matti Bengston Date: Thu, 6 Mar 2025 21:28:46 +0200 Subject: [PATCH 08/31] Implement closest point on circle and contains point on circle --- lib/geometry/__tests__/collisions2d.test.ts | 22 +++++++++++++---- lib/geometry/collisions2d.ts | 27 +++++++++++++++------ 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts index 58350a6..ed1b737 100644 --- a/lib/geometry/__tests__/collisions2d.test.ts +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -8,6 +8,7 @@ import { circleContainsPoint2D, circleIntersectsCircle2D, closestPointOnAabb2D, + closestPointOnCircle2D, } from '../collisions2d.js'; import type { IAABB2D, ICircle } from '../primitives.js'; @@ -272,10 +273,21 @@ describe('geometry/collisions', () => { }); describe('closestPointOnCircle2D', () => { - - }); - - describe('circleContainsPoint2D', () => { - + const base: ICircle = { + position: new Vector2(0, 0), + radius: 5, + }; + + const up = closestPointOnCircle2D(base, new Vector2(0, 10)); + expect(up.x).toEqual(0); + expect(up.y).toEqual(5); + + const left = closestPointOnCircle2D(base, new Vector2(-7, 0)); + expect(left.x).toEqual(-5); + expect(left.y).toEqual(0); + + const inside = closestPointOnCircle2D(base, new Vector2(2, -2)); + expect(inside.x).toEqual(2); + expect(inside.y).toEqual(-2); }); }); diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts index 5a28ac9..1f06eba 100644 --- a/lib/geometry/collisions2d.ts +++ b/lib/geometry/collisions2d.ts @@ -22,20 +22,31 @@ export function closestPointOnAabb2D(a: IAABB2D, point: IVec2): IVec2 { return new Vector2(clamp(point.x, a.min.x, a.max.x), clamp(point.y, a.min.y, a.max.y)); } -export function aabbIntersectsCircle2D(a: IAABB2D, b: ICircle): boolean { - // @todo - return false; +export function circleContainsPoint2D(circle: ICircle, point: IVec2): boolean { + const diff = Vector2.Subtract(circle.position, point); + return diff.getMagnitude() < circle.radius * circle.radius; } -export function closestPointOnCircle2D(a: ICircle, point: IVec2): boolean { - // @todo - return false; +export function closestPointOnCircle2D(circle: ICircle, point: IVec2): IVec2 { + const diff = Vector2.Subtract(point, circle.position); + + // If the point is inside of the circle, just return it + if (diff.getMagnitude() < circle.radius * circle.radius) { + return point; + } + + // Normalize the difference and transform it by the radius + diff.normalize().multiplyScalar(circle.radius); + + // Finally move the result BACK to world space + return diff.add(circle.position); } -export function circleContainsPoint2D(circle: ICircle, point: IVec2): boolean { +export function circleIntersectsCircle2D(a: ICircle, b: ICircle): boolean { return false; } -export function circleIntersectsCircle2D(a: ICircle, b: ICircle): boolean { +export function aabbIntersectsCircle2D(a: IAABB2D, b: ICircle): boolean { + // @todo return false; } From 658349f5eec81c3659a6ec77519c6a404cf52427 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Fri, 7 Mar 2025 13:30:24 +0200 Subject: [PATCH 09/31] Add circle collision --- lib/geometry/__tests__/collisions2d.test.ts | 36 +- lib/geometry/collisions2d.ts | 36 +- lib/geometry/primitives.ts | 10 + pnpm-lock.yaml | 997 ++++++++++---------- 4 files changed, 563 insertions(+), 516 deletions(-) diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts index ed1b737..ca0c768 100644 --- a/lib/geometry/__tests__/collisions2d.test.ts +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -206,7 +206,7 @@ describe('geometry/collisions', () => { // Clear cases expect(circleContainsPoint2D(circle, new Vector2(0, 0))).toEqual(true); expect(circleContainsPoint2D(circle, new Vector2(4, 2))).toEqual(true); - expect(circleContainsPoint2D(circle, new Vector2(-4, -4))).toEqual(true); + expect(circleContainsPoint2D(circle, new Vector2(-3, -3))).toEqual(true); expect(circleContainsPoint2D(circle, new Vector2(100, 100))).toEqual(false); // Since it is a circle, { radius, radius } should be false. @@ -273,21 +273,23 @@ describe('geometry/collisions', () => { }); describe('closestPointOnCircle2D', () => { - const base: ICircle = { - position: new Vector2(0, 0), - radius: 5, - }; - - const up = closestPointOnCircle2D(base, new Vector2(0, 10)); - expect(up.x).toEqual(0); - expect(up.y).toEqual(5); - - const left = closestPointOnCircle2D(base, new Vector2(-7, 0)); - expect(left.x).toEqual(-5); - expect(left.y).toEqual(0); - - const inside = closestPointOnCircle2D(base, new Vector2(2, -2)); - expect(inside.x).toEqual(2); - expect(inside.y).toEqual(-2); + it('should return the closest point on a circle2D given a point', () => { + const base: ICircle = { + position: new Vector2(0, 0), + radius: 5, + }; + + const up = closestPointOnCircle2D(base, new Vector2(0, 10)); + expect(up.x).toEqual(0); + expect(up.y).toEqual(5); + + const left = closestPointOnCircle2D(base, new Vector2(-7, 0)); + expect(left.x).toEqual(-5); + expect(left.y).toEqual(0); + + const inside = closestPointOnCircle2D(base, new Vector2(2, -2)); + expect(inside.x).toEqual(2); + expect(inside.y).toEqual(-2); + }); }); }); diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts index 1f06eba..2f7cdd0 100644 --- a/lib/geometry/collisions2d.ts +++ b/lib/geometry/collisions2d.ts @@ -1,7 +1,7 @@ import { clamp } from '../math/utils.js'; import { IVec2, Vector2 } from '../math/vector2.js'; -import { IAABB2D, ICircle } from './primitives.js'; +import { IAABB2D, ICircle, IRay2D } from './primitives.js'; export function aabbContainsPoint2D(bounds: IAABB2D, point: IVec2): boolean { return ( @@ -24,14 +24,14 @@ export function closestPointOnAabb2D(a: IAABB2D, point: IVec2): IVec2 { export function circleContainsPoint2D(circle: ICircle, point: IVec2): boolean { const diff = Vector2.Subtract(circle.position, point); - return diff.getMagnitude() < circle.radius * circle.radius; + return diff.getMagnitude() <= circle.radius * circle.radius; } export function closestPointOnCircle2D(circle: ICircle, point: IVec2): IVec2 { const diff = Vector2.Subtract(point, circle.position); // If the point is inside of the circle, just return it - if (diff.getMagnitude() < circle.radius * circle.radius) { + if (diff.getMagnitude() <= circle.radius * circle.radius) { return point; } @@ -43,10 +43,38 @@ export function closestPointOnCircle2D(circle: ICircle, point: IVec2): IVec2 { } export function circleIntersectsCircle2D(a: ICircle, b: ICircle): boolean { + // Get distance between the centers + const distanceSquared = Vector2.Subtract(a.position, b.position).getMagnitude(); + + // Sum and square the radii so we can compare to the distance + const summedRadii = a.radius + b.radius; + return distanceSquared <= summedRadii * summedRadii; +} + +export function aabbIntersectsCircle2D(aabb: IAABB2D, circle: ICircle): boolean { + const closestPoint = closestPointOnAabb2D(aabb, circle.position); + const distanceSqrd = Vector2.Subtract(circle.position, closestPoint).getMagnitude(); + const radiusSqrd = circle.radius * circle.radius; + + return distanceSqrd <= radiusSqrd; +} + +export function isPointOnRay2d(ray: IRay2D, point: IVec2): boolean { + // @todo + return false; +} + +export function closestPointOnRay2D(ray: IRay2D, point: IVec2): IVec2 { + // @todo + return point; +} + +export function rayIntersectsAabb2d(ray: IRay2D, aabb: IAABB2D): boolean { + // @todo return false; } -export function aabbIntersectsCircle2D(a: IAABB2D, b: ICircle): boolean { +export function rayIntersectsCircle2d(ray: IRay2D, aabb: IAABB2D): boolean { // @todo return false; } diff --git a/lib/geometry/primitives.ts b/lib/geometry/primitives.ts index 9c4d356..22ae2e9 100644 --- a/lib/geometry/primitives.ts +++ b/lib/geometry/primitives.ts @@ -20,3 +20,13 @@ export interface ISphere { readonly position: IVec3; readonly radius: number; } + +export interface IRay2D { + readonly position: IVec2; + readonly direction: IVec2; +} + +export interface IRay3D { + readonly position: IVec3; + readonly direction: IVec3; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c99e77d..09b50a1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,25 +10,25 @@ importers: devDependencies: '@bengsfort/eslint-config-flat': specifier: ^0.2.4 - version: 0.2.4(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3) + version: 0.2.4(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) '@changesets/cli': specifier: ^2.28.0 - version: 2.28.0 + version: 2.28.1 '@types/node': specifier: ^22.13.4 - version: 22.13.4 + version: 22.13.9 eslint: specifier: ^9.20.1 - version: 9.20.1 + version: 9.21.0 rimraf: specifier: ^6.0.1 version: 6.0.1 typescript: specifier: ^5.7.3 - version: 5.7.3 + version: 5.8.2 vitest: specifier: ^3.0.6 - version: 3.0.6(@types/node@22.13.4) + version: 3.0.8(@types/node@22.13.9) packages: @@ -43,8 +43,8 @@ packages: eslint: ^9.20.1 typescript: ^5.7.3 - '@changesets/apply-release-plan@7.0.9': - resolution: {integrity: sha512-xB1shQP6WhflnAN+rV8eJ7j4oBgka/K62+pHuEv6jmUtSqlx2ZvJSnCGzyNfkiQmSfVsqXoI3pbAuyVpTbsKzA==} + '@changesets/apply-release-plan@7.0.10': + resolution: {integrity: sha512-wNyeIJ3yDsVspYvHnEz1xQDq18D9ifed3lI+wxRQRK4pArUcuHgCTrHv0QRnnwjhVCQACxZ+CBih3wgOct6UXw==} '@changesets/assemble-release-plan@6.0.6': resolution: {integrity: sha512-Frkj8hWJ1FRZiY3kzVCKzS0N5mMwWKwmv9vpam7vt8rZjLL1JMthdh6pSDVSPumHPshTTkKZ0VtNbE0cJHZZUg==} @@ -52,12 +52,12 @@ packages: '@changesets/changelog-git@0.2.1': resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==} - '@changesets/cli@2.28.0': - resolution: {integrity: sha512-of9/8Gzc+DP/Ol9Lak++Y0RsB1oO1CRzZoGIWTYcvHNREJQNqxW5tXm3YzqsA1Gx8ecZZw82FfahtiS+HkNqIw==} + '@changesets/cli@2.28.1': + resolution: {integrity: sha512-PiIyGRmSc6JddQJe/W1hRPjiN4VrMvb2VfQ6Uydy2punBioQrsxppyG5WafinKcW1mT0jOe/wU4k9Zy5ff21AA==} hasBin: true - '@changesets/config@3.1.0': - resolution: {integrity: sha512-UbZsPkRnv2SF8Ln72B8opmNLhsazv7/M0r6GSQSQzLY++/ZPr5dDSz3L+6G2fDZ+AN1ZjsEGDdBkpEna9eJtrA==} + '@changesets/config@3.1.1': + resolution: {integrity: sha512-bd+3Ap2TKXxljCggI0mKPfzCQKeV/TU4yO2h2C6vAihIo8tzseAn2e7klSuiyYYXvgu53zMN1OeYMIQkaQoWnA==} '@changesets/errors@0.2.0': resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} @@ -65,8 +65,8 @@ packages: '@changesets/get-dependents-graph@2.1.3': resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==} - '@changesets/get-release-plan@4.0.7': - resolution: {integrity: sha512-FdXJ5B4ZcIWtTu+SEIAthnSScwF+mS+e657gagYUyprVLFSkAJKrA50MqoW3iOopbwQ/UhYaTESNyF9cpg1bQA==} + '@changesets/get-release-plan@4.0.8': + resolution: {integrity: sha512-MM4mq2+DQU1ZT7nqxnpveDMTkMBLnwNX44cX7NSxlXmr7f8hO6/S2MXNiXG54uf/0nYnefv0cfy4Czf/ZL/EKQ==} '@changesets/get-version-range-type@0.4.0': resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} @@ -98,152 +98,152 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@esbuild/aix-ppc64@0.24.2': - resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} + '@esbuild/aix-ppc64@0.25.0': + resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.24.2': - resolution: {integrity: sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==} + '@esbuild/android-arm64@0.25.0': + resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.24.2': - resolution: {integrity: sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==} + '@esbuild/android-arm@0.25.0': + resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + '@esbuild/android-x64@0.25.0': + resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + '@esbuild/darwin-arm64@0.25.0': + resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + '@esbuild/darwin-x64@0.25.0': + resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.24.2': - resolution: {integrity: sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==} + '@esbuild/freebsd-arm64@0.25.0': + resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.24.2': - resolution: {integrity: sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==} + '@esbuild/freebsd-x64@0.25.0': + resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + '@esbuild/linux-arm64@0.25.0': + resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.24.2': - resolution: {integrity: sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==} + '@esbuild/linux-arm@0.25.0': + resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.24.2': - resolution: {integrity: sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==} + '@esbuild/linux-ia32@0.25.0': + resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + '@esbuild/linux-loong64@0.25.0': + resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.24.2': - resolution: {integrity: sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==} + '@esbuild/linux-mips64el@0.25.0': + resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + '@esbuild/linux-ppc64@0.25.0': + resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + '@esbuild/linux-riscv64@0.25.0': + resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + '@esbuild/linux-s390x@0.25.0': + resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + '@esbuild/linux-x64@0.25.0': + resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + '@esbuild/netbsd-arm64@0.25.0': + resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + '@esbuild/netbsd-x64@0.25.0': + resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + '@esbuild/openbsd-arm64@0.25.0': + resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + '@esbuild/openbsd-x64@0.25.0': + resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + '@esbuild/sunos-x64@0.25.0': + resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + '@esbuild/win32-arm64@0.25.0': + resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + '@esbuild/win32-ia32@0.25.0': + resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.24.2': - resolution: {integrity: sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==} + '@esbuild/win32-x64@0.25.0': + resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -262,24 +262,24 @@ packages: resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/core@0.11.0': - resolution: {integrity: sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==} + '@eslint/core@0.12.0': + resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.2.0': - resolution: {integrity: sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==} + '@eslint/eslintrc@3.3.0': + resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.20.0': - resolution: {integrity: sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==} + '@eslint/js@9.21.0': + resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/plugin-kit@0.2.6': - resolution: {integrity: sha512-+0TjwR1eAUdZtvv/ir1mGX+v0tUoR3VEPB8Up0LLJC+whRW0GgBBtpbOkg/a/U4Dxa6l5a3l9AJ1aWIQVyoWJA==} + '@eslint/plugin-kit@0.2.7': + resolution: {integrity: sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.1': @@ -298,8 +298,8 @@ packages: resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} engines: {node: '>=18.18'} - '@humanwhocodes/retry@0.4.1': - resolution: {integrity: sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==} + '@humanwhocodes/retry@0.4.2': + resolution: {integrity: sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==} engines: {node: '>=18.18'} '@isaacs/cliui@8.0.2': @@ -331,106 +331,106 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@rollup/rollup-android-arm-eabi@4.34.8': - resolution: {integrity: sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==} + '@rollup/rollup-android-arm-eabi@4.34.9': + resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.8': - resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} + '@rollup/rollup-android-arm64@4.34.9': + resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.8': - resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} + '@rollup/rollup-darwin-arm64@4.34.9': + resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.8': - resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} + '@rollup/rollup-darwin-x64@4.34.9': + resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.8': - resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} + '@rollup/rollup-freebsd-arm64@4.34.9': + resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.8': - resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} + '@rollup/rollup-freebsd-x64@4.34.9': + resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': - resolution: {integrity: sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==} + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': + resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.34.8': - resolution: {integrity: sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==} + '@rollup/rollup-linux-arm-musleabihf@4.34.9': + resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.8': - resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} + '@rollup/rollup-linux-arm64-gnu@4.34.9': + resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.8': - resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} + '@rollup/rollup-linux-arm64-musl@4.34.9': + resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': - resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': + resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': - resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': + resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.34.8': - resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} + '@rollup/rollup-linux-riscv64-gnu@4.34.9': + resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.8': - resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} + '@rollup/rollup-linux-s390x-gnu@4.34.9': + resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.8': - resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} + '@rollup/rollup-linux-x64-gnu@4.34.9': + resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.8': - resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} + '@rollup/rollup-linux-x64-musl@4.34.9': + resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.34.8': - resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} + '@rollup/rollup-win32-arm64-msvc@4.34.9': + resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.8': - resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} + '@rollup/rollup-win32-ia32-msvc@4.34.9': + resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.8': - resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} + '@rollup/rollup-win32-x64-msvc@4.34.9': + resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@stylistic/eslint-plugin@4.0.1': - resolution: {integrity: sha512-RwKkRKiDrF4ptiur54ckDhOByQYKYZ1dEmI5K8BJCmuGpauFJXzVL1UQYTA2zq702CqMFdYiJcVFJWfokIgFxw==} + '@stylistic/eslint-plugin@4.2.0': + resolution: {integrity: sha512-8hXezgz7jexGHdo5WN6JBEIPHCSFyyU4vgbxevu4YLVS5vl+sxqAAGyXSzfNDyR6xMNSH5H1x67nsXcYMOHtZA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: '>=9.0.0' @@ -447,61 +447,61 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.13.4': - resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==} + '@types/node@22.13.9': + resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} - '@typescript-eslint/eslint-plugin@8.24.1': - resolution: {integrity: sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==} + '@typescript-eslint/eslint-plugin@8.26.0': + resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.24.1': - resolution: {integrity: sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ==} + '@typescript-eslint/parser@8.26.0': + resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.24.1': - resolution: {integrity: sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q==} + '@typescript-eslint/scope-manager@8.26.0': + resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.24.1': - resolution: {integrity: sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==} + '@typescript-eslint/type-utils@8.26.0': + resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.24.1': - resolution: {integrity: sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A==} + '@typescript-eslint/types@8.26.0': + resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.24.1': - resolution: {integrity: sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==} + '@typescript-eslint/typescript-estree@8.26.0': + resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.24.1': - resolution: {integrity: sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ==} + '@typescript-eslint/utils@8.26.0': + resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.24.1': - resolution: {integrity: sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg==} + '@typescript-eslint/visitor-keys@8.26.0': + resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@3.0.6': - resolution: {integrity: sha512-zBduHf/ja7/QRX4HdP1DSq5XrPgdN+jzLOwaTq/0qZjYfgETNFCKf9nOAp2j3hmom3oTbczuUzrzg9Hafh7hNg==} + '@vitest/expect@3.0.8': + resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} - '@vitest/mocker@3.0.6': - resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} + '@vitest/mocker@3.0.8': + resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 @@ -511,28 +511,28 @@ packages: vite: optional: true - '@vitest/pretty-format@3.0.6': - resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} + '@vitest/pretty-format@3.0.8': + resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} - '@vitest/runner@3.0.6': - resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==} + '@vitest/runner@3.0.8': + resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} - '@vitest/snapshot@3.0.6': - resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==} + '@vitest/snapshot@3.0.8': + resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} - '@vitest/spy@3.0.6': - resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} + '@vitest/spy@3.0.8': + resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} - '@vitest/utils@3.0.6': - resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} + '@vitest/utils@3.0.8': + resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.14.0: - resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==} + acorn@8.14.1: + resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==} engines: {node: '>=0.4.0'} hasBin: true @@ -634,8 +634,8 @@ packages: resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} engines: {node: '>= 0.4'} - call-bound@1.0.3: - resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} callsites@3.1.0: @@ -779,8 +779,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.24.2: - resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} + esbuild@0.25.0: + resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} engines: {node: '>=18'} hasBin: true @@ -788,8 +788,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.0.1: - resolution: {integrity: sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw==} + eslint-config-prettier@10.0.2: + resolution: {integrity: sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -860,8 +860,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.20.1: - resolution: {integrity: sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==} + eslint@9.21.0: + resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -898,8 +898,8 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - expect-type@1.1.0: - resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} + expect-type@1.2.0: + resolution: {integrity: sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA==} engines: {node: '>=12.0.0'} extendable-error@0.1.7: @@ -925,8 +925,8 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.19.0: - resolution: {integrity: sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA==} + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} @@ -955,8 +955,8 @@ packages: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} - foreground-child@3.3.0: - resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} fs-extra@7.0.1: @@ -982,8 +982,8 @@ packages: functions-have-names@1.2.3: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} - get-intrinsic@1.2.7: - resolution: {integrity: sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==} + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} get-proto@1.0.1: @@ -1194,8 +1194,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@4.0.3: - resolution: {integrity: sha512-oSwM7q8PTHQWuZAlp995iPpPJ4Vkl7qT0ZRD+9duL9j2oBy6KcTfyxc8mEuHJYC+z/kbps80aJLkaNzTOrf/kw==} + jackspeak@4.1.0: + resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} engines: {node: 20 || >=22} js-yaml@3.14.1: @@ -1368,8 +1368,8 @@ packages: package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} - package-manager-detector@0.2.9: - resolution: {integrity: sha512-+vYvA/Y31l8Zk8dwxHhL3JfTuHPm6tlxM2A3GeQyl7ovYnSp1+mzAxClxaOr0qO1TtPxbQxetI7v5XqKLJZk7Q==} + package-manager-detector@0.2.11: + resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==} parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -1420,8 +1420,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss@8.5.2: - resolution: {integrity: sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==} + postcss@8.5.3: + resolution: {integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==} engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: @@ -1437,8 +1437,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - prettier@3.5.1: - resolution: {integrity: sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==} + prettier@3.5.3: + resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==} engines: {node: '>=14'} hasBin: true @@ -1446,6 +1446,9 @@ packages: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + quansync@0.2.8: + resolution: {integrity: sha512-4+saucphJMazjt7iOM27mbFCk+D9dd/zmgMDCzRZ8MEoBfYp7lAvoN38et/phRQF6wOPMy/OROBGgoWeSKyluA==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} @@ -1477,8 +1480,8 @@ packages: engines: {node: '>= 0.4'} hasBin: true - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rimraf@6.0.1: @@ -1486,8 +1489,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.34.8: - resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} + rollup@4.34.9: + resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1578,8 +1581,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.8.0: - resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@3.8.1: + resolution: {integrity: sha512-vj5lIj3Mwf9D79hBkltk5qmkFI+biIKWS2IBxEyEU3AX1tUf7AoL8nSazCOiiqQsGKIq01SClsKEzweu34uwvA==} string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} @@ -1691,15 +1694,15 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.24.1: - resolution: {integrity: sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA==} + typescript-eslint@8.26.0: + resolution: {integrity: sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 - typescript: '>=4.8.4 <5.8.0' + typescript: '>=4.8.4 <5.9.0' - typescript@5.7.3: - resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} + typescript@5.8.2: + resolution: {integrity: sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==} engines: {node: '>=14.17'} hasBin: true @@ -1717,13 +1720,13 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@3.0.6: - resolution: {integrity: sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==} + vite-node@3.0.8: + resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.1.0: - resolution: {integrity: sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==} + vite@6.2.0: + resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -1762,16 +1765,16 @@ packages: yaml: optional: true - vitest@3.0.6: - resolution: {integrity: sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==} + vitest@3.0.8: + resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.6 - '@vitest/ui': 3.0.6 + '@vitest/browser': 3.0.8 + '@vitest/ui': 3.0.8 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1838,18 +1841,18 @@ snapshots: dependencies: regenerator-runtime: 0.14.1 - '@bengsfort/eslint-config-flat@0.2.4(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)': - dependencies: - '@eslint/js': 9.20.0 - '@stylistic/eslint-plugin': 4.0.1(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 - eslint-config-prettier: 10.0.1(eslint@9.20.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1) - eslint-plugin-prettier: 5.2.3(eslint-config-prettier@10.0.1(eslint@9.20.1))(eslint@9.20.1)(prettier@3.5.1) - eslint-plugin-promise: 7.2.1(eslint@9.20.1) - prettier: 3.5.1 - typescript: 5.7.3 - typescript-eslint: 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@bengsfort/eslint-config-flat@0.2.4(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': + dependencies: + '@eslint/js': 9.21.0 + '@stylistic/eslint-plugin': 4.2.0(eslint@9.21.0)(typescript@5.8.2) + eslint: 9.21.0 + eslint-config-prettier: 10.0.2(eslint@9.21.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0) + eslint-plugin-prettier: 5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.3) + eslint-plugin-promise: 7.2.1(eslint@9.21.0) + prettier: 3.5.3 + typescript: 5.8.2 + typescript-eslint: 8.26.0(eslint@9.21.0)(typescript@5.8.2) transitivePeerDependencies: - '@types/eslint' - '@typescript-eslint/parser' @@ -1857,9 +1860,9 @@ snapshots: - eslint-import-resolver-webpack - supports-color - '@changesets/apply-release-plan@7.0.9': + '@changesets/apply-release-plan@7.0.10': dependencies: - '@changesets/config': 3.1.0 + '@changesets/config': 3.1.1 '@changesets/get-version-range-type': 0.4.0 '@changesets/git': 3.0.2 '@changesets/should-skip-package': 0.1.2 @@ -1886,15 +1889,15 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.28.0': + '@changesets/cli@2.28.1': dependencies: - '@changesets/apply-release-plan': 7.0.9 + '@changesets/apply-release-plan': 7.0.10 '@changesets/assemble-release-plan': 6.0.6 '@changesets/changelog-git': 0.2.1 - '@changesets/config': 3.1.0 + '@changesets/config': 3.1.1 '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 - '@changesets/get-release-plan': 4.0.7 + '@changesets/get-release-plan': 4.0.8 '@changesets/git': 3.0.2 '@changesets/logger': 0.1.1 '@changesets/pre': 2.0.2 @@ -1910,14 +1913,14 @@ snapshots: fs-extra: 7.0.1 mri: 1.2.0 p-limit: 2.3.0 - package-manager-detector: 0.2.9 + package-manager-detector: 0.2.11 picocolors: 1.1.1 resolve-from: 5.0.0 semver: 7.7.1 spawndamnit: 3.0.1 term-size: 2.2.1 - '@changesets/config@3.1.0': + '@changesets/config@3.1.1': dependencies: '@changesets/errors': 0.2.0 '@changesets/get-dependents-graph': 2.1.3 @@ -1938,10 +1941,10 @@ snapshots: picocolors: 1.1.1 semver: 7.7.1 - '@changesets/get-release-plan@4.0.7': + '@changesets/get-release-plan@4.0.8': dependencies: '@changesets/assemble-release-plan': 6.0.6 - '@changesets/config': 3.1.0 + '@changesets/config': 3.1.1 '@changesets/pre': 2.0.2 '@changesets/read': 0.6.3 '@changesets/types': 6.1.0 @@ -1999,84 +2002,84 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@esbuild/aix-ppc64@0.24.2': + '@esbuild/aix-ppc64@0.25.0': optional: true - '@esbuild/android-arm64@0.24.2': + '@esbuild/android-arm64@0.25.0': optional: true - '@esbuild/android-arm@0.24.2': + '@esbuild/android-arm@0.25.0': optional: true - '@esbuild/android-x64@0.24.2': + '@esbuild/android-x64@0.25.0': optional: true - '@esbuild/darwin-arm64@0.24.2': + '@esbuild/darwin-arm64@0.25.0': optional: true - '@esbuild/darwin-x64@0.24.2': + '@esbuild/darwin-x64@0.25.0': optional: true - '@esbuild/freebsd-arm64@0.24.2': + '@esbuild/freebsd-arm64@0.25.0': optional: true - '@esbuild/freebsd-x64@0.24.2': + '@esbuild/freebsd-x64@0.25.0': optional: true - '@esbuild/linux-arm64@0.24.2': + '@esbuild/linux-arm64@0.25.0': optional: true - '@esbuild/linux-arm@0.24.2': + '@esbuild/linux-arm@0.25.0': optional: true - '@esbuild/linux-ia32@0.24.2': + '@esbuild/linux-ia32@0.25.0': optional: true - '@esbuild/linux-loong64@0.24.2': + '@esbuild/linux-loong64@0.25.0': optional: true - '@esbuild/linux-mips64el@0.24.2': + '@esbuild/linux-mips64el@0.25.0': optional: true - '@esbuild/linux-ppc64@0.24.2': + '@esbuild/linux-ppc64@0.25.0': optional: true - '@esbuild/linux-riscv64@0.24.2': + '@esbuild/linux-riscv64@0.25.0': optional: true - '@esbuild/linux-s390x@0.24.2': + '@esbuild/linux-s390x@0.25.0': optional: true - '@esbuild/linux-x64@0.24.2': + '@esbuild/linux-x64@0.25.0': optional: true - '@esbuild/netbsd-arm64@0.24.2': + '@esbuild/netbsd-arm64@0.25.0': optional: true - '@esbuild/netbsd-x64@0.24.2': + '@esbuild/netbsd-x64@0.25.0': optional: true - '@esbuild/openbsd-arm64@0.24.2': + '@esbuild/openbsd-arm64@0.25.0': optional: true - '@esbuild/openbsd-x64@0.24.2': + '@esbuild/openbsd-x64@0.25.0': optional: true - '@esbuild/sunos-x64@0.24.2': + '@esbuild/sunos-x64@0.25.0': optional: true - '@esbuild/win32-arm64@0.24.2': + '@esbuild/win32-arm64@0.25.0': optional: true - '@esbuild/win32-ia32@0.24.2': + '@esbuild/win32-ia32@0.25.0': optional: true - '@esbuild/win32-x64@0.24.2': + '@esbuild/win32-x64@0.25.0': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)': + '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0)': dependencies: - eslint: 9.20.1 + eslint: 9.21.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2089,11 +2092,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.11.0': + '@eslint/core@0.12.0': dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.2.0': + '@eslint/eslintrc@3.3.0': dependencies: ajv: 6.12.6 debug: 4.4.0 @@ -2107,13 +2110,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.20.0': {} + '@eslint/js@9.21.0': {} '@eslint/object-schema@2.1.6': {} - '@eslint/plugin-kit@0.2.6': + '@eslint/plugin-kit@0.2.7': dependencies: - '@eslint/core': 0.11.0 + '@eslint/core': 0.12.0 levn: 0.4.1 '@humanfs/core@0.19.1': {} @@ -2127,7 +2130,7 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.1': {} + '@humanwhocodes/retry@0.4.2': {} '@isaacs/cliui@8.0.2': dependencies: @@ -2166,73 +2169,73 @@ snapshots: '@nodelib/fs.walk@1.2.8': dependencies: '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.0 + fastq: 1.19.1 '@pkgr/core@0.1.1': {} - '@rollup/rollup-android-arm-eabi@4.34.8': + '@rollup/rollup-android-arm-eabi@4.34.9': optional: true - '@rollup/rollup-android-arm64@4.34.8': + '@rollup/rollup-android-arm64@4.34.9': optional: true - '@rollup/rollup-darwin-arm64@4.34.8': + '@rollup/rollup-darwin-arm64@4.34.9': optional: true - '@rollup/rollup-darwin-x64@4.34.8': + '@rollup/rollup-darwin-x64@4.34.9': optional: true - '@rollup/rollup-freebsd-arm64@4.34.8': + '@rollup/rollup-freebsd-arm64@4.34.9': optional: true - '@rollup/rollup-freebsd-x64@4.34.8': + '@rollup/rollup-freebsd-x64@4.34.9': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + '@rollup/rollup-linux-arm-gnueabihf@4.34.9': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.8': + '@rollup/rollup-linux-arm-musleabihf@4.34.9': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.8': + '@rollup/rollup-linux-arm64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.8': + '@rollup/rollup-linux-arm64-musl@4.34.9': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + '@rollup/rollup-linux-loongarch64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.8': + '@rollup/rollup-linux-riscv64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.8': + '@rollup/rollup-linux-s390x-gnu@4.34.9': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.8': + '@rollup/rollup-linux-x64-gnu@4.34.9': optional: true - '@rollup/rollup-linux-x64-musl@4.34.8': + '@rollup/rollup-linux-x64-musl@4.34.9': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.8': + '@rollup/rollup-win32-arm64-msvc@4.34.9': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.8': + '@rollup/rollup-win32-ia32-msvc@4.34.9': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.8': + '@rollup/rollup-win32-x64-msvc@4.34.9': optional: true '@rtsao/scc@1.1.0': {} - '@stylistic/eslint-plugin@4.0.1(eslint@9.20.1)(typescript@5.7.3)': + '@stylistic/eslint-plugin@4.2.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + eslint: 9.21.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -2249,132 +2252,132 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.13.4': + '@types/node@22.13.9': dependencies: undici-types: 6.20.0 - '@typescript-eslint/eslint-plugin@8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/scope-manager': 8.24.1 - '@typescript-eslint/type-utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.24.1 - eslint: 9.20.1 + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/type-utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 + eslint: 9.21.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 8.24.1 - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) - '@typescript-eslint/visitor-keys': 8.24.1 + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 - eslint: 9.20.1 - typescript: 5.7.3 + eslint: 9.21.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.24.1': + '@typescript-eslint/scope-manager@8.26.0': dependencies: - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/visitor-keys': 8.24.1 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 - '@typescript-eslint/type-utils@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) debug: 4.4.0 - eslint: 9.20.1 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 + eslint: 9.21.0 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.24.1': {} + '@typescript-eslint/types@8.26.0': {} - '@typescript-eslint/typescript-estree@8.24.1(typescript@5.7.3)': + '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/visitor-keys': 8.24.1 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/visitor-keys': 8.26.0 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.1 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 + ts-api-utils: 2.0.1(typescript@5.8.2) + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) - '@typescript-eslint/scope-manager': 8.24.1 - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/typescript-estree': 8.24.1(typescript@5.7.3) - eslint: 9.20.1 - typescript: 5.7.3 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) + '@typescript-eslint/scope-manager': 8.26.0 + '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) + eslint: 9.21.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.24.1': + '@typescript-eslint/visitor-keys@8.26.0': dependencies: - '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/types': 8.26.0 eslint-visitor-keys: 4.2.0 - '@vitest/expect@3.0.6': + '@vitest/expect@3.0.8': dependencies: - '@vitest/spy': 3.0.6 - '@vitest/utils': 3.0.6 + '@vitest/spy': 3.0.8 + '@vitest/utils': 3.0.8 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.6(vite@6.1.0(@types/node@22.13.4))': + '@vitest/mocker@3.0.8(vite@6.2.0(@types/node@22.13.9))': dependencies: - '@vitest/spy': 3.0.6 + '@vitest/spy': 3.0.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.1.0(@types/node@22.13.4) + vite: 6.2.0(@types/node@22.13.9) - '@vitest/pretty-format@3.0.6': + '@vitest/pretty-format@3.0.8': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.0.6': + '@vitest/runner@3.0.8': dependencies: - '@vitest/utils': 3.0.6 + '@vitest/utils': 3.0.8 pathe: 2.0.3 - '@vitest/snapshot@3.0.6': + '@vitest/snapshot@3.0.8': dependencies: - '@vitest/pretty-format': 3.0.6 + '@vitest/pretty-format': 3.0.8 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.0.6': + '@vitest/spy@3.0.8': dependencies: tinyspy: 3.0.2 - '@vitest/utils@3.0.6': + '@vitest/utils@3.0.8': dependencies: - '@vitest/pretty-format': 3.0.6 + '@vitest/pretty-format': 3.0.8 loupe: 3.1.3 tinyrainbow: 2.0.0 - acorn-jsx@5.3.2(acorn@8.14.0): + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.14.0 + acorn: 8.14.1 - acorn@8.14.0: {} + acorn@8.14.1: {} ajv@6.12.6: dependencies: @@ -2403,7 +2406,7 @@ snapshots: array-buffer-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-array-buffer: 3.0.5 array-includes@3.1.8: @@ -2412,7 +2415,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-string: 1.1.1 array-union@2.1.0: {} @@ -2447,7 +2450,7 @@ snapshots: define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 assertion-error@2.0.1: {} @@ -2488,13 +2491,13 @@ snapshots: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 set-function-length: 1.2.2 - call-bound@1.0.3: + call-bound@1.0.4: dependencies: call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 callsites@3.1.0: {} @@ -2533,19 +2536,19 @@ snapshots: data-view-buffer@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-length@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 data-view-byte-offset@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-data-view: 1.0.2 @@ -2606,7 +2609,7 @@ snapshots: arraybuffer.prototype.slice: 1.0.4 available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 data-view-buffer: 1.0.2 data-view-byte-length: 1.0.2 data-view-byte-offset: 1.0.1 @@ -2616,7 +2619,7 @@ snapshots: es-set-tostringtag: 2.1.0 es-to-primitive: 1.3.0 function.prototype.name: 1.1.8 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 get-symbol-description: 1.1.0 globalthis: 1.0.4 @@ -2667,7 +2670,7 @@ snapshots: es-set-tostringtag@2.1.0: dependencies: es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -2681,39 +2684,39 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.24.2: + esbuild@0.25.0: optionalDependencies: - '@esbuild/aix-ppc64': 0.24.2 - '@esbuild/android-arm': 0.24.2 - '@esbuild/android-arm64': 0.24.2 - '@esbuild/android-x64': 0.24.2 - '@esbuild/darwin-arm64': 0.24.2 - '@esbuild/darwin-x64': 0.24.2 - '@esbuild/freebsd-arm64': 0.24.2 - '@esbuild/freebsd-x64': 0.24.2 - '@esbuild/linux-arm': 0.24.2 - '@esbuild/linux-arm64': 0.24.2 - '@esbuild/linux-ia32': 0.24.2 - '@esbuild/linux-loong64': 0.24.2 - '@esbuild/linux-mips64el': 0.24.2 - '@esbuild/linux-ppc64': 0.24.2 - '@esbuild/linux-riscv64': 0.24.2 - '@esbuild/linux-s390x': 0.24.2 - '@esbuild/linux-x64': 0.24.2 - '@esbuild/netbsd-arm64': 0.24.2 - '@esbuild/netbsd-x64': 0.24.2 - '@esbuild/openbsd-arm64': 0.24.2 - '@esbuild/openbsd-x64': 0.24.2 - '@esbuild/sunos-x64': 0.24.2 - '@esbuild/win32-arm64': 0.24.2 - '@esbuild/win32-ia32': 0.24.2 - '@esbuild/win32-x64': 0.24.2 + '@esbuild/aix-ppc64': 0.25.0 + '@esbuild/android-arm': 0.25.0 + '@esbuild/android-arm64': 0.25.0 + '@esbuild/android-x64': 0.25.0 + '@esbuild/darwin-arm64': 0.25.0 + '@esbuild/darwin-x64': 0.25.0 + '@esbuild/freebsd-arm64': 0.25.0 + '@esbuild/freebsd-x64': 0.25.0 + '@esbuild/linux-arm': 0.25.0 + '@esbuild/linux-arm64': 0.25.0 + '@esbuild/linux-ia32': 0.25.0 + '@esbuild/linux-loong64': 0.25.0 + '@esbuild/linux-mips64el': 0.25.0 + '@esbuild/linux-ppc64': 0.25.0 + '@esbuild/linux-riscv64': 0.25.0 + '@esbuild/linux-s390x': 0.25.0 + '@esbuild/linux-x64': 0.25.0 + '@esbuild/netbsd-arm64': 0.25.0 + '@esbuild/netbsd-x64': 0.25.0 + '@esbuild/openbsd-arm64': 0.25.0 + '@esbuild/openbsd-x64': 0.25.0 + '@esbuild/sunos-x64': 0.25.0 + '@esbuild/win32-arm64': 0.25.0 + '@esbuild/win32-ia32': 0.25.0 + '@esbuild/win32-x64': 0.25.0 escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.0.1(eslint@9.20.1): + eslint-config-prettier@10.0.2(eslint@9.21.0): dependencies: - eslint: 9.20.1 + eslint: 9.21.0 eslint-import-resolver-node@0.3.9: dependencies: @@ -2723,17 +2726,17 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + eslint: 9.21.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -2742,9 +2745,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.20.1 + eslint: 9.21.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.20.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -2756,25 +2759,25 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.2.3(eslint-config-prettier@10.0.1(eslint@9.20.1))(eslint@9.20.1)(prettier@3.5.1): + eslint-plugin-prettier@5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.3): dependencies: - eslint: 9.20.1 - prettier: 3.5.1 + eslint: 9.21.0 + prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.2 optionalDependencies: - eslint-config-prettier: 10.0.1(eslint@9.20.1) + eslint-config-prettier: 10.0.2(eslint@9.21.0) - eslint-plugin-promise@7.2.1(eslint@9.20.1): + eslint-plugin-promise@7.2.1(eslint@9.21.0): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) - eslint: 9.20.1 + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) + eslint: 9.21.0 eslint-scope@8.2.0: dependencies: @@ -2785,18 +2788,18 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.20.1: + eslint@9.21.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 - '@eslint/core': 0.11.0 - '@eslint/eslintrc': 3.2.0 - '@eslint/js': 9.20.0 - '@eslint/plugin-kit': 0.2.6 + '@eslint/core': 0.12.0 + '@eslint/eslintrc': 3.3.0 + '@eslint/js': 9.21.0 + '@eslint/plugin-kit': 0.2.7 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.4.1 + '@humanwhocodes/retry': 0.4.2 '@types/estree': 1.0.6 '@types/json-schema': 7.0.15 ajv: 6.12.6 @@ -2826,8 +2829,8 @@ snapshots: espree@10.3.0: dependencies: - acorn: 8.14.0 - acorn-jsx: 5.3.2(acorn@8.14.0) + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) eslint-visitor-keys: 4.2.0 esprima@4.0.1: {} @@ -2848,7 +2851,7 @@ snapshots: esutils@2.0.3: {} - expect-type@1.1.0: {} + expect-type@1.2.0: {} extendable-error@0.1.7: {} @@ -2874,9 +2877,9 @@ snapshots: fast-levenshtein@2.0.6: {} - fastq@1.19.0: + fastq@1.19.1: dependencies: - reusify: 1.0.4 + reusify: 1.1.0 file-entry-cache@8.0.0: dependencies: @@ -2907,7 +2910,7 @@ snapshots: dependencies: is-callable: 1.2.7 - foreground-child@3.3.0: + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 signal-exit: 4.1.0 @@ -2932,7 +2935,7 @@ snapshots: function.prototype.name@1.1.8: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 functions-have-names: 1.2.3 hasown: 2.0.2 @@ -2940,7 +2943,7 @@ snapshots: functions-have-names@1.2.3: {} - get-intrinsic@1.2.7: + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 es-define-property: 1.0.1 @@ -2960,9 +2963,9 @@ snapshots: get-symbol-description@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 glob-parent@5.1.2: dependencies: @@ -2974,8 +2977,8 @@ snapshots: glob@11.0.1: dependencies: - foreground-child: 3.3.0 - jackspeak: 4.0.3 + foreground-child: 3.3.1 + jackspeak: 4.1.0 minimatch: 10.0.1 minipass: 7.1.2 package-json-from-dist: 1.0.1 @@ -3049,13 +3052,13 @@ snapshots: is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-async-function@2.1.1: dependencies: async-function: 1.0.0 - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -3066,7 +3069,7 @@ snapshots: is-boolean-object@1.2.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-callable@1.2.7: {} @@ -3077,26 +3080,26 @@ snapshots: is-data-view@1.0.2: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-typed-array: 1.1.15 is-date-object@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-fullwidth-code-point@3.0.0: {} is-generator-function@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 get-proto: 1.0.1 has-tostringtag: 1.0.2 safe-regex-test: 1.1.0 @@ -3109,14 +3112,14 @@ snapshots: is-number-object@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-number@7.0.0: {} is-regex@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 gopd: 1.2.0 has-tostringtag: 1.0.2 hasown: 2.0.2 @@ -3125,11 +3128,11 @@ snapshots: is-shared-array-buffer@1.0.4: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-string@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-tostringtag: 1.0.2 is-subdir@1.2.0: @@ -3138,7 +3141,7 @@ snapshots: is-symbol@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-symbols: 1.1.0 safe-regex-test: 1.1.0 @@ -3150,12 +3153,12 @@ snapshots: is-weakref@1.1.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 is-weakset@2.0.4: dependencies: - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 is-windows@1.0.2: {} @@ -3163,7 +3166,7 @@ snapshots: isexe@2.0.0: {} - jackspeak@4.0.3: + jackspeak@4.1.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -3259,7 +3262,7 @@ snapshots: object.assign@4.1.7: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 has-symbols: 1.1.0 @@ -3281,7 +3284,7 @@ snapshots: object.values@1.2.1: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -3300,7 +3303,7 @@ snapshots: own-keys@1.0.1: dependencies: - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-keys: 1.1.1 safe-push-apply: 1.0.0 @@ -3330,7 +3333,9 @@ snapshots: package-json-from-dist@1.0.1: {} - package-manager-detector@0.2.9: {} + package-manager-detector@0.2.11: + dependencies: + quansync: 0.2.8 parent-module@1.0.1: dependencies: @@ -3363,7 +3368,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.2: + postcss@8.5.3: dependencies: nanoid: 3.3.8 picocolors: 1.1.1 @@ -3377,10 +3382,12 @@ snapshots: prettier@2.8.8: {} - prettier@3.5.1: {} + prettier@3.5.3: {} punycode@2.3.1: {} + quansync@0.2.8: {} + queue-microtask@1.2.3: {} read-yaml-file@1.1.0: @@ -3397,7 +3404,7 @@ snapshots: es-abstract: 1.23.9 es-errors: 1.3.0 es-object-atoms: 1.1.1 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 get-proto: 1.0.1 which-builtin-type: 1.2.1 @@ -3422,36 +3429,36 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.0.4: {} + reusify@1.1.0: {} rimraf@6.0.1: dependencies: glob: 11.0.1 package-json-from-dist: 1.0.1 - rollup@4.34.8: + rollup@4.34.9: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.8 - '@rollup/rollup-android-arm64': 4.34.8 - '@rollup/rollup-darwin-arm64': 4.34.8 - '@rollup/rollup-darwin-x64': 4.34.8 - '@rollup/rollup-freebsd-arm64': 4.34.8 - '@rollup/rollup-freebsd-x64': 4.34.8 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.8 - '@rollup/rollup-linux-arm-musleabihf': 4.34.8 - '@rollup/rollup-linux-arm64-gnu': 4.34.8 - '@rollup/rollup-linux-arm64-musl': 4.34.8 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.8 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.8 - '@rollup/rollup-linux-riscv64-gnu': 4.34.8 - '@rollup/rollup-linux-s390x-gnu': 4.34.8 - '@rollup/rollup-linux-x64-gnu': 4.34.8 - '@rollup/rollup-linux-x64-musl': 4.34.8 - '@rollup/rollup-win32-arm64-msvc': 4.34.8 - '@rollup/rollup-win32-ia32-msvc': 4.34.8 - '@rollup/rollup-win32-x64-msvc': 4.34.8 + '@rollup/rollup-android-arm-eabi': 4.34.9 + '@rollup/rollup-android-arm64': 4.34.9 + '@rollup/rollup-darwin-arm64': 4.34.9 + '@rollup/rollup-darwin-x64': 4.34.9 + '@rollup/rollup-freebsd-arm64': 4.34.9 + '@rollup/rollup-freebsd-x64': 4.34.9 + '@rollup/rollup-linux-arm-gnueabihf': 4.34.9 + '@rollup/rollup-linux-arm-musleabihf': 4.34.9 + '@rollup/rollup-linux-arm64-gnu': 4.34.9 + '@rollup/rollup-linux-arm64-musl': 4.34.9 + '@rollup/rollup-linux-loongarch64-gnu': 4.34.9 + '@rollup/rollup-linux-powerpc64le-gnu': 4.34.9 + '@rollup/rollup-linux-riscv64-gnu': 4.34.9 + '@rollup/rollup-linux-s390x-gnu': 4.34.9 + '@rollup/rollup-linux-x64-gnu': 4.34.9 + '@rollup/rollup-linux-x64-musl': 4.34.9 + '@rollup/rollup-win32-arm64-msvc': 4.34.9 + '@rollup/rollup-win32-ia32-msvc': 4.34.9 + '@rollup/rollup-win32-x64-msvc': 4.34.9 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3461,8 +3468,8 @@ snapshots: safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 - get-intrinsic: 1.2.7 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 has-symbols: 1.1.0 isarray: 2.0.5 @@ -3473,7 +3480,7 @@ snapshots: safe-regex-test@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-regex: 1.2.1 @@ -3488,7 +3495,7 @@ snapshots: define-data-property: 1.1.4 es-errors: 1.3.0 function-bind: 1.1.2 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 gopd: 1.2.0 has-property-descriptors: 1.0.2 @@ -3518,16 +3525,16 @@ snapshots: side-channel-map@1.0.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-weakmap@1.0.2: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 - get-intrinsic: 1.2.7 + get-intrinsic: 1.3.0 object-inspect: 1.13.4 side-channel-map: 1.0.1 @@ -3556,7 +3563,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.8.0: {} + std-env@3.8.1: {} string-width@4.2.3: dependencies: @@ -3573,7 +3580,7 @@ snapshots: string.prototype.trim@1.2.10: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-data-property: 1.1.4 define-properties: 1.2.1 es-abstract: 1.23.9 @@ -3583,7 +3590,7 @@ snapshots: string.prototype.trimend@1.0.9: dependencies: call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 define-properties: 1.2.1 es-object-atoms: 1.1.1 @@ -3636,9 +3643,9 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.0.1(typescript@5.7.3): + ts-api-utils@2.0.1(typescript@5.8.2): dependencies: - typescript: 5.7.3 + typescript: 5.8.2 tsconfig-paths@3.15.0: dependencies: @@ -3655,7 +3662,7 @@ snapshots: typed-array-buffer@1.0.3: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 es-errors: 1.3.0 is-typed-array: 1.1.15 @@ -3686,21 +3693,21 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.24.1(eslint@9.20.1)(typescript@5.7.3): + typescript-eslint@8.26.0(eslint@9.21.0)(typescript@5.8.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.24.1(@typescript-eslint/parser@8.24.1(eslint@9.20.1)(typescript@5.7.3))(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/parser': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - '@typescript-eslint/utils': 8.24.1(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 - typescript: 5.7.3 + '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + eslint: 9.21.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - typescript@5.7.3: {} + typescript@5.8.2: {} unbox-primitive@1.1.0: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 has-bigints: 1.1.0 has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 @@ -3713,13 +3720,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.0.6(@types/node@22.13.4): + vite-node@3.0.8(@types/node@22.13.9): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.1.0(@types/node@22.13.4) + vite: 6.2.0(@types/node@22.13.9) transitivePeerDependencies: - '@types/node' - jiti @@ -3734,39 +3741,39 @@ snapshots: - tsx - yaml - vite@6.1.0(@types/node@22.13.4): + vite@6.2.0(@types/node@22.13.9): dependencies: - esbuild: 0.24.2 - postcss: 8.5.2 - rollup: 4.34.8 + esbuild: 0.25.0 + postcss: 8.5.3 + rollup: 4.34.9 optionalDependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.9 fsevents: 2.3.3 - vitest@3.0.6(@types/node@22.13.4): + vitest@3.0.8(@types/node@22.13.9): dependencies: - '@vitest/expect': 3.0.6 - '@vitest/mocker': 3.0.6(vite@6.1.0(@types/node@22.13.4)) - '@vitest/pretty-format': 3.0.6 - '@vitest/runner': 3.0.6 - '@vitest/snapshot': 3.0.6 - '@vitest/spy': 3.0.6 - '@vitest/utils': 3.0.6 + '@vitest/expect': 3.0.8 + '@vitest/mocker': 3.0.8(vite@6.2.0(@types/node@22.13.9)) + '@vitest/pretty-format': 3.0.8 + '@vitest/runner': 3.0.8 + '@vitest/snapshot': 3.0.8 + '@vitest/spy': 3.0.8 + '@vitest/utils': 3.0.8 chai: 5.2.0 debug: 4.4.0 - expect-type: 1.1.0 + expect-type: 1.2.0 magic-string: 0.30.17 pathe: 2.0.3 - std-env: 3.8.0 + std-env: 3.8.1 tinybench: 2.9.0 tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.1.0(@types/node@22.13.4) - vite-node: 3.0.6(@types/node@22.13.4) + vite: 6.2.0(@types/node@22.13.9) + vite-node: 3.0.8(@types/node@22.13.9) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.13.4 + '@types/node': 22.13.9 transitivePeerDependencies: - jiti - less @@ -3791,7 +3798,7 @@ snapshots: which-builtin-type@1.2.1: dependencies: - call-bound: 1.0.3 + call-bound: 1.0.4 function.prototype.name: 1.1.8 has-tostringtag: 1.0.2 is-async-function: 2.1.1 @@ -3816,7 +3823,7 @@ snapshots: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 - call-bound: 1.0.3 + call-bound: 1.0.4 for-each: 0.3.5 gopd: 1.2.0 has-tostringtag: 1.0.2 From 13d350221f26b889e90f0220e6e095c412dc82c0 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 9 Mar 2025 22:25:05 +0200 Subject: [PATCH 10/31] Add dot to v2 and v3, cross to v3 --- .changeset/famous-books-teach.md | 5 +++ .changeset/nasty-colts-cut.md | 5 +++ lib/math/__tests__/vector2.test.ts | 22 ++++++++-- lib/math/__tests__/vector3.test.ts | 64 ++++++++++++++++++++++++++++-- lib/math/vector2.ts | 19 +++++++++ lib/math/vector3.ts | 42 ++++++++++++++++++++ 6 files changed, 150 insertions(+), 7 deletions(-) create mode 100644 .changeset/famous-books-teach.md create mode 100644 .changeset/nasty-colts-cut.md diff --git a/.changeset/famous-books-teach.md b/.changeset/famous-books-teach.md new file mode 100644 index 0000000..0ded2db --- /dev/null +++ b/.changeset/famous-books-teach.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Added dot and cross product to vector3 diff --git a/.changeset/nasty-colts-cut.md b/.changeset/nasty-colts-cut.md new file mode 100644 index 0000000..037dc9e --- /dev/null +++ b/.changeset/nasty-colts-cut.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Added dot product to vector2 diff --git a/lib/math/__tests__/vector2.test.ts b/lib/math/__tests__/vector2.test.ts index 55b3df3..136f38c 100644 --- a/lib/math/__tests__/vector2.test.ts +++ b/lib/math/__tests__/vector2.test.ts @@ -101,6 +101,12 @@ describe('math/vector2', () => { expect(vec2.y).toEqual(2); }); + it('.dot should provide the dot product', () => { + expect(new Vector2(-6, 10).dot(new Vector2(5, 12))).toBeCloseTo(90); // Arbitrary + expect(new Vector2(-12, 16).dot(new Vector2(12, 9))).toEqual(0); // Right angle + expect(new Vector2(1, 1).dot(new Vector2(1, 1))).toBeCloseTo(2); // Same vector + }); + it('.equals should check value equality with the given vector', () => { const original = new Vector2(300, 300); const notEqual = new Vector2(500, 420); @@ -110,7 +116,7 @@ describe('math/vector2', () => { expect(original.equals(equal)).toEqual(true); }); - it('should lerp to a new vector', () => { + it('.lerp should lerp to a new vector', () => { const original = new Vector2(0, 0); const target = new Vector2(100, 100); @@ -190,7 +196,7 @@ describe('math/vector2', () => { expect(Vector2.Equals(v1, v3)).toEqual(true); }); - it('Should provide a helper to lerp between two vectors', () => { + it('Vector2.Lerp should provide a helper to lerp between two vectors', () => { const v1 = new Vector2(5, 5); const v2 = new Vector2(10, 10); const result = Vector2.Lerp(v1, v2, 0.5); @@ -198,14 +204,22 @@ describe('math/vector2', () => { expect(result.y).toEqual(7.5); }); - it('Should normalize a vector', () => { + it('Vector2.Normalize should normalize a vector', () => { const v1 = new Vector2(5, 5); expect(Vector2.Normalize(v1).getLength()).toBeCloseTo(1); const v2 = new Vector2(10, 5); expect(Vector2.Normalize(v2).getLength()).toBeCloseTo(1); }); - it('Should assert if a value is vector2-like', () => { + it('Vector2.Dot should provide the dot product', () => { + expect(Vector2.Dot(new Vector2(-6, 10), new Vector2(5, 12))).toBeCloseTo(90); // Arbitrary + expect(Vector2.Dot(new Vector2(-12, 16), new Vector2(12, 9))).toEqual(0); // Right angle + expect(Vector2.Dot(new Vector2(1, 0), new Vector2(2, 0))).toBeCloseTo(2); // Same direction + expect(Vector2.Dot(new Vector2(1, 1), new Vector2(1, 1))).toBeCloseTo(2); // Same vecs + expect(Vector2.Dot(new Vector2(1, 1), new Vector2(1, 0))).toBeCloseTo(1); // 45* angle + }); + + it('Vector2.IsVec2Like should assert if a value is vector2-like', () => { expect(Vector2.IsVec2Like(5)).toEqual(false); expect(Vector2.IsVec2Like({ x: 5 })).toEqual(false); expect(Vector2.IsVec2Like({ x: 500, y: 300 })).toEqual(true); diff --git a/lib/math/__tests__/vector3.test.ts b/lib/math/__tests__/vector3.test.ts index 295d3e8..6437411 100644 --- a/lib/math/__tests__/vector3.test.ts +++ b/lib/math/__tests__/vector3.test.ts @@ -113,6 +113,35 @@ describe('math/vector3', () => { expect(vec.z).toEqual(1); }); + it('.dot should provide the dot product with another vector', () => { + expect(new Vector3(1, 2, 3).dot(new Vector3(4, 5, 6))).toEqual(32); // Arbitrary + expect(new Vector3(1, 1, 1).dot(new Vector3(1, 1, 1))).toEqual(3); // Identical + expect(new Vector3(1, 1, 0).dot(new Vector3(-1, 1, 0))).toEqual(0); // Perpendicular + expect(new Vector3(1, 1, 1).dot(new Vector3(1, 1, 0))).toEqual(2); // 45* angle + }); + + it('.cross should provide the cross product', () => { + const perpendicular = new Vector3(2, 0, 0).cross(new Vector3(0, 3, 0)); + expect(perpendicular.x).toBeCloseTo(0); + expect(perpendicular.y).toBeCloseTo(0); + expect(perpendicular.z).toBeCloseTo(6); + + const ortho = new Vector3(1, 1, 0).cross(new Vector3(-1, 1, 0)); + expect(ortho.x).toBeCloseTo(0); + expect(ortho.y).toBeCloseTo(0); + expect(ortho.z).toBeCloseTo(2); + + const same = new Vector3(2, 2, 2).cross(new Vector3(2, 2, 2)); + expect(same.x).toBeCloseTo(0); + expect(same.y).toBeCloseTo(0); + expect(same.z).toBeCloseTo(0); + + const parallel = new Vector3(1, 2, 3).cross(new Vector3(2, 4, 6)); + expect(parallel.x).toBeCloseTo(0); + expect(parallel.y).toBeCloseTo(0); + expect(parallel.z).toBeCloseTo(0); + }); + it('.equals should check value equality with the given vector', () => { const original = new Vector3(100, 200, 300); const notEqual = new Vector3(500, 420, 300); @@ -183,7 +212,7 @@ describe('math/vector3', () => { expect(Vector3.Equals(v1, v3)).toEqual(true); }); - it('Should provide a helper to lerp between two vectors', () => { + it('Vector3.Lerp should provide a helper to lerp between two vectors', () => { const v1 = new Vector3(5, 5, 5); const v2 = new Vector3(10, 10, 10); const result = Vector3.Lerp(v1, v2, 0.5); @@ -193,7 +222,36 @@ describe('math/vector3', () => { expect(result.z).toEqual(7.5); }); - it('Should normalize a vector', () => { + it('Vector3.Dot should provide the dot product of two vectors', () => { + expect(Vector3.Dot(new Vector3(1, 2, 3), new Vector3(4, 5, 6))).toEqual(32); // Arbitrary + expect(Vector3.Dot(new Vector3(1, 1, 1), new Vector3(1, 1, 1))).toEqual(3); // Identical + expect(Vector3.Dot(new Vector3(1, 1, 0), new Vector3(-1, 1, 0))).toEqual(0); // Perpendicular + expect(Vector3.Dot(new Vector3(1, 1, 1), new Vector3(1, 1, 0))).toEqual(2); // 45* angle + }); + + it('Vector3.Cross should provide the cross product', () => { + const perpendicular = Vector3.Cross(new Vector3(2, 0, 0), new Vector3(0, 3, 0)); + expect(perpendicular.x).toBeCloseTo(0); + expect(perpendicular.y).toBeCloseTo(0); + expect(perpendicular.z).toBeCloseTo(6); + + const ortho = Vector3.Cross(new Vector3(1, 1, 0), new Vector3(-1, 1, 0)); + expect(ortho.x).toBeCloseTo(0); + expect(ortho.y).toBeCloseTo(0); + expect(ortho.z).toBeCloseTo(2); + + const same = Vector3.Cross(new Vector3(2, 2, 2), new Vector3(2, 2, 2)); + expect(same.x).toBeCloseTo(0); + expect(same.y).toBeCloseTo(0); + expect(same.z).toBeCloseTo(0); + + const parallel = Vector3.Cross(new Vector3(1, 2, 3), new Vector3(2, 4, 6)); + expect(parallel.x).toBeCloseTo(0); + expect(parallel.y).toBeCloseTo(0); + expect(parallel.z).toBeCloseTo(0); + }); + + it('Vector3.Normalize should normalize a vector', () => { const v1 = new Vector3(5, 5, 5); expect(Vector3.Normalize(v1).getLength()).toBeCloseTo(1); @@ -201,7 +259,7 @@ describe('math/vector3', () => { expect(Vector3.Normalize(v2).getLength()).toBeCloseTo(1); }); - it('Should assert if a value is vector3-like', () => { + it('Vector3.IsVec3Like should assert if a value is vector3-like', () => { expect(Vector3.IsVec3Like(5)).toEqual(false); expect(Vector3.IsVec3Like({ x: 5, y: 300 })).toEqual(false); expect(Vector3.IsVec3Like({ x: 500, y: 300, z: 200 })).toEqual(true); diff --git a/lib/math/vector2.ts b/lib/math/vector2.ts index 0d89b07..332336e 100644 --- a/lib/math/vector2.ts +++ b/lib/math/vector2.ts @@ -124,6 +124,16 @@ export class Vector2 implements IVec2 { return new Vector2(vector.x, vector.y).normalize(); } + /** + * Gets the dot product of the given 2 vectors. + * @param v1 The first vector. + * @param v2 The second vector. + * @returns The dot product. + */ + public static Dot(v1: IVec2, v2: IVec2): number { + return v1.x * v2.x + v1.y * v2.y; + } + /** * Asserts a given unknonwn value is Vector2-like. * @param obj The value. @@ -286,6 +296,15 @@ export class Vector2 implements IVec2 { return this; } + /** + * Provides the dot product between this vector and another vector. + * @param other The other vector. + * @returns The dot product. + */ + public dot(other: IVec2): number { + return Vector2.Dot(this, other); + } + /** * Check equality between this vectors components and a given vectors components. * @param val The vector to check equality. diff --git a/lib/math/vector3.ts b/lib/math/vector3.ts index 8bca482..eade954 100644 --- a/lib/math/vector3.ts +++ b/lib/math/vector3.ts @@ -140,6 +140,30 @@ export class Vector3 implements IVec3 { return new Vector3(vector.x, vector.y, vector.z).normalize(); } + /** + * Returns the dot product between two vectors. + * @param v1 The first vector. + * @param v2 The second vector. + * @returns The dot product. + */ + public static Dot(v1: IVec3, v2: IVec3): number { + return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z; + } + + /** + * Returns the cross product between two vectors. + * @param v1 The first vector. + * @param v2 The second vector. + * @returns The dot product. + */ + public static Cross(v1: IVec3, v2: IVec3): Vector3 { + return new Vector3( + v1.y * v2.z - v1.z * v2.y, + v1.z * v2.x - v1.x * v2.z, + v1.x * v2.y - v1.y * v2.x, + ); + } + /** * Asserts a given unknonwn value is Vector3-like. * @param obj The value. @@ -332,6 +356,24 @@ export class Vector3 implements IVec3 { return this; } + /** + * Provides the dot product of this vector and another vector. + * @param other The other vector. + * @returns The dot product. + */ + public dot(other: IVec3): number { + return Vector3.Dot(this, other); + } + + /** + * Provides the cross product of this vector and another vector. + * @param other The other vector. + * @returns The cross product. + */ + public cross(other: IVec3): Vector3 { + return Vector3.Cross(this, other); + } + /** * Check equality between this vectors components and a given vectors components. * @param val The vector to check equality. From 7d1cac06520e8027b3f17aa425ba29844074222d Mon Sep 17 00:00:00 2001 From: bengsfort Date: Wed, 12 Mar 2025 16:15:05 +0200 Subject: [PATCH 11/31] Base ray implementation --- lib/geometry/__tests__/collisions2d.test.ts | 89 ++++++++++++++++++++- lib/geometry/collisions2d.ts | 41 ++++++++-- lib/math/utils.ts | 2 + 3 files changed, 123 insertions(+), 9 deletions(-) diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts index ca0c768..419345b 100644 --- a/lib/geometry/__tests__/collisions2d.test.ts +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -9,8 +9,12 @@ import { circleIntersectsCircle2D, closestPointOnAabb2D, closestPointOnCircle2D, + closestPointOnRay2D, + isPointOnRay2D, + rayIntersectsAabb2D, + rayIntersectsCircle2D, } from '../collisions2d.js'; -import type { IAABB2D, ICircle } from '../primitives.js'; +import type { IAABB2D, ICircle, IRay2D } from '../primitives.js'; describe('geometry/collisions', () => { describe('aabbContainsPoint2D', () => { @@ -292,4 +296,87 @@ describe('geometry/collisions', () => { expect(inside.y).toEqual(-2); }); }); + + describe('isPointOnRay2D', () => { + it('should return whether a given point is on the given ray', () => { + const ray: IRay2D = { + position: new Vector2(0, 0), + direction: new Vector2(1, 1), + }; + + expect(isPointOnRay2D(ray, new Vector2(0, 0))).toEqual(true); + expect(isPointOnRay2D(ray, new Vector2(2, 2))).toEqual(true); + expect(isPointOnRay2D(ray, new Vector2(100, 100))).toEqual(true); + expect(isPointOnRay2D(ray, new Vector2(-2, 2))).toEqual(false); + expect(isPointOnRay2D(ray, new Vector2(-1, -1))).toEqual(false); + expect(isPointOnRay2D(ray, new Vector2(-3, 8))).toEqual(false); + }); + }); + + describe('closestPointOnRay2D', () => { + it('should return the closest point on a given ray', () => { + const ray: IRay2D = { + position: new Vector2(1, 1), + direction: new Vector2(1, 0), + }; + + const onRay = closestPointOnRay2D(ray, new Vector2(3, 1)); + expect(onRay.x).toEqual(3); + expect(onRay.y).toEqual(1); + + const aboveRay = closestPointOnRay2D(ray, new Vector2(4, 3)); + expect(aboveRay.x).toEqual(4); + expect(aboveRay.y).toEqual(1); + + const belowRay = closestPointOnRay2D(ray, new Vector2(2, -3)); + expect(belowRay.x).toEqual(2); + expect(belowRay.y).toEqual(1); + + const behindRay = closestPointOnRay2D(ray, new Vector2(-2, 2)); + expect(behindRay.x).toEqual(1); + expect(behindRay.y).toEqual(1); + }); + }); + + describe('rayIntersectsAabb2D', () => { + it('should return if the ray is intersecting the aabb', () => { + const ray: IRay2D = { + position: new Vector2(2, 2), + direction: new Vector2(1, 0), + }; + + const collisionPoint = new Vector2(); + const collides = rayIntersectsAabb2D(ray, { + min: new Vector2(4, 0), + max: new Vector2(8, 8), + }, collisionPoint); + expect(collides).toEqual(true); + expect(collisionPoint.x).toEqual(4); + expect(collisionPoint.y).toEqual(2); + + const doesNotCollide = rayIntersectsAabb2D(ray, { + min: new Vector2(2.5, 2.5), + max: new Vector2(4.5, 4.5), + }); + expect(doesNotCollide).toEqual(false); + }); + }); + + describe('rayIntersectsCircle2D', () => { + it('should return if the ray is intersecting the given circle', () => { + const ray: IRay2D = { + position: new Vector2(2, 2), + direction: new Vector2(1, 0), + }; + + const collisionPoint = new Vector2(); + const collides = rayIntersectsCircle2D(ray, { + position: new Vector2(6, 2), + radius: 2, + }, collisionPoint); + expect(collides).toEqual(true); + expect(collisionPoint.x).toEqual(4); + expect(collisionPoint.y).toEqual(2); + }); + }); }); diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts index 2f7cdd0..3001beb 100644 --- a/lib/geometry/collisions2d.ts +++ b/lib/geometry/collisions2d.ts @@ -1,4 +1,4 @@ -import { clamp } from '../math/utils.js'; +import { clamp, EPSILON } from '../math/utils.js'; import { IVec2, Vector2 } from '../math/vector2.js'; import { IAABB2D, ICircle, IRay2D } from './primitives.js'; @@ -59,22 +59,47 @@ export function aabbIntersectsCircle2D(aabb: IAABB2D, circle: ICircle): boolean return distanceSqrd <= radiusSqrd; } -export function isPointOnRay2d(ray: IRay2D, point: IVec2): boolean { - // @todo - return false; +export function isPointOnRay2D(ray: IRay2D, point: IVec2, epsilon = EPSILON): boolean { + if (Vector2.Equals(ray.position, point)) { + return true; + } + + const diff = Vector2.Subtract(point, ray.position).normalize(); + const dot = diff.dot(Vector2.Normalize(ray.direction)); + + return Math.abs(1.0 - dot) < epsilon; } export function closestPointOnRay2D(ray: IRay2D, point: IVec2): IVec2 { - // @todo - return point; + // Get the dot of the direction so we can project the point back onto it + const normalizedDir = Vector2.Normalize(ray.direction); + const directionDot = Vector2.Dot(normalizedDir, normalizedDir); + + // If the direction vector is zero, the ray is just a point (should not happen) + if (directionDot === 0) { + return ray.position; + } + + const distance = Vector2.Subtract(point, ray.position); + const projectionScalar = distance.dot(normalizedDir) / directionDot; + + // If the scalar is negative, we are BEHIND the ray and thus the closest point + // is the origin of the ray, so just return that. + if (projectionScalar < 0) { + return ray.position; + } + + // Scale the distance by the back to find the closest point + const projection = Vector2.MultiplyScalar(normalizedDir, projectionScalar); + return projection.add(ray.position); } -export function rayIntersectsAabb2d(ray: IRay2D, aabb: IAABB2D): boolean { +export function rayIntersectsAabb2D(ray: IRay2D, aabb: IAABB2D, collisionPoint = new Vector2()): boolean { // @todo return false; } -export function rayIntersectsCircle2d(ray: IRay2D, aabb: IAABB2D): boolean { +export function rayIntersectsCircle2D(ray: IRay2D, circle: ICircle, collisionPoint = new Vector2()): boolean { // @todo return false; } diff --git a/lib/math/utils.ts b/lib/math/utils.ts index d214058..5c832c1 100644 --- a/lib/math/utils.ts +++ b/lib/math/utils.ts @@ -1,3 +1,5 @@ +export const EPSILON = 0.000001; + /** * Clamps a number between two boundaries. * From 5af2cb2c4ab6af84d1761082ef8cd4d0fc3fac39 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Fri, 14 Mar 2025 17:44:32 +0200 Subject: [PATCH 12/31] Implement 2d ray collisions --- .changeset/true-adults-give.md | 5 ++ lib/geometry/__tests__/collisions2d.test.ts | 54 +++++++++++-- lib/geometry/collisions2d.ts | 90 +++++++++++++++++++-- 3 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 .changeset/true-adults-give.md diff --git a/.changeset/true-adults-give.md b/.changeset/true-adults-give.md new file mode 100644 index 0000000..f7a9d1d --- /dev/null +++ b/.changeset/true-adults-give.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Add support for 2d ray collisions. diff --git a/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts index 419345b..e99eee4 100644 --- a/lib/geometry/__tests__/collisions2d.test.ts +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -346,10 +346,14 @@ describe('geometry/collisions', () => { }; const collisionPoint = new Vector2(); - const collides = rayIntersectsAabb2D(ray, { - min: new Vector2(4, 0), - max: new Vector2(8, 8), - }, collisionPoint); + const collides = rayIntersectsAabb2D( + ray, + { + min: new Vector2(4, 0), + max: new Vector2(8, 8), + }, + collisionPoint, + ); expect(collides).toEqual(true); expect(collisionPoint.x).toEqual(4); expect(collisionPoint.y).toEqual(2); @@ -359,6 +363,18 @@ describe('geometry/collisions', () => { max: new Vector2(4.5, 4.5), }); expect(doesNotCollide).toEqual(false); + + const insidePoint = new Vector2(); + const isInside = rayIntersectsAabb2D( + ray, + { + min: new Vector2(0, 0), + max: new Vector2(4, 4), + }, + insidePoint, + ); + expect(isInside).toEqual(true); + expect(insidePoint.equals(ray.position)).toEqual(true); }); }); @@ -370,13 +386,35 @@ describe('geometry/collisions', () => { }; const collisionPoint = new Vector2(); - const collides = rayIntersectsCircle2D(ray, { - position: new Vector2(6, 2), - radius: 2, - }, collisionPoint); + const collides = rayIntersectsCircle2D( + ray, + { + position: new Vector2(6, 2), + radius: 2, + }, + collisionPoint, + ); expect(collides).toEqual(true); expect(collisionPoint.x).toEqual(4); expect(collisionPoint.y).toEqual(2); + + const insidePoint = new Vector2(); + const isInside = rayIntersectsCircle2D( + ray, + { + position: new Vector2(0, 0), + radius: 5, + }, + insidePoint, + ); + expect(isInside).toEqual(true); + expect(insidePoint.equals(ray.position)).toEqual(true); + + const doesNotCollide = rayIntersectsCircle2D(ray, { + position: new Vector2(10, 10), + radius: 2, + }); + expect(doesNotCollide).toEqual(false); }); }); }); diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts index 3001beb..d33308e 100644 --- a/lib/geometry/collisions2d.ts +++ b/lib/geometry/collisions2d.ts @@ -1,5 +1,8 @@ +import { networkInterfaces } from 'os'; + import { clamp, EPSILON } from '../math/utils.js'; import { IVec2, Vector2 } from '../math/vector2.js'; +import { Vector3 } from '../math/vector3.js'; import { IAABB2D, ICircle, IRay2D } from './primitives.js'; @@ -94,12 +97,87 @@ export function closestPointOnRay2D(ray: IRay2D, point: IVec2): IVec2 { return projection.add(ray.position); } -export function rayIntersectsAabb2D(ray: IRay2D, aabb: IAABB2D, collisionPoint = new Vector2()): boolean { - // @todo - return false; +export function rayIntersectsAabb2D( + ray: IRay2D, + aabb: IAABB2D, + collisionPoint = new Vector2(), +): boolean { + // Early out if ray starts inside of the box + // NOTE: This doesnt feel nice, i believe ther should be a way to do this + // using the below calculations, but this works for now. Revisit. + if (aabbContainsPoint2D(aabb, ray.position)) { + collisionPoint.set(ray.position.x, ray.position.y); + return true; + } + + const normal = Vector2.Normalize(ray.direction); + + // Solve for different `t` cases, ie. the min and max points where the ray + // intersects with the planes that surround the AABB. This is a scalar that + // can be multiplied by the normal to get a position along the ray. + // `0` would be the origin of the ray. + const xMin = (aabb.min.x - ray.position.x) / normal.x; + const xMax = (aabb.max.x - ray.position.x) / normal.x; + const yMin = (aabb.min.y - ray.position.y) / normal.y; + const yMax = (aabb.max.y - ray.position.y) / normal.y; + + // Determine the ray entrance by getting the biggest MINIMUM value + const entranceT = Math.max(Math.min(xMin, xMax), Math.min(yMin, yMax)); + // Determine ray exit by getting the smallest MAXIMUM value + const exitT = Math.min(Math.max(xMin, xMax), Math.max(yMin, yMax)); + + // If the exit is greater than 0 the ray is behind the AABB + if (exitT < 0) { + return false; + } + + // if entrance > exit, there is no intersection + if (entranceT > exitT) { + return false; + } + + collisionPoint.set(ray.position.x, ray.position.y); + const t = entranceT < 0 ? exitT : entranceT; + normal.multiplyScalar(t); + collisionPoint.add(normal); + + return true; } -export function rayIntersectsCircle2D(ray: IRay2D, circle: ICircle, collisionPoint = new Vector2()): boolean { - // @todo - return false; +export function rayIntersectsCircle2D( + ray: IRay2D, + circle: ICircle, + collisionPoint = new Vector2(), +): boolean { + const normal = Vector2.Normalize(ray.direction); + const radiusSqr = circle.radius * circle.radius; + + const distance = Vector2.Subtract(circle.position, ray.position); + const distanceSquared = distance.getMagnitude(); + + // If the distance is less than the radius, the ray is inside + if (distanceSquared < radiusSqr) { + collisionPoint.set(ray.position.x, ray.position.y); + return true; + } + + // Project the ray to the plane of the center of the circle + const projectionToCenter = distance.dot(normal); + const projSqr = projectionToCenter * projectionToCenter; + + // If the result is negative then there is no collision + if (radiusSqr - distanceSquared + projSqr < 0) { + return false; + } + + const centerToRayProj = Math.sqrt(distanceSquared - projSqr); + const rayProjToCollision = Math.sqrt(radiusSqr - centerToRayProj * centerToRayProj); + const collision = projectionToCenter - rayProjToCollision; + + // Project the result back onto the ray as the collision point + collisionPoint.set(ray.position.x, ray.position.y); + normal.multiplyScalar(collision); + collisionPoint.add(normal); + + return true; } From 164131872dc4d2164aec8274880c7b68b48c5209 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sat, 15 Mar 2025 01:24:40 +0200 Subject: [PATCH 13/31] Scaffold drawables and init sandbox. --- package.json | 18 +++--- pnpm-lock.yaml | 19 +++--- sandbox/2d.html | 14 +++++ sandbox/src/2d/drawables/aabb.ts | 31 ++++++++++ sandbox/src/2d/drawables/circle.ts | 0 sandbox/src/2d/drawables/drawable.ts | 3 + sandbox/src/2d/drawables/grid.ts | 0 sandbox/src/2d/drawables/point.ts | 26 ++++++++ sandbox/src/2d/drawables/ray.ts | 0 sandbox/src/2d/main.ts | 16 +++++ sandbox/src/2d/renderer.ts | 89 ++++++++++++++++++++++++++++ sandbox/tsconfig.json | 42 +++++++++++++ sandbox/vite.config.ts | 11 ++++ 13 files changed, 253 insertions(+), 16 deletions(-) create mode 100644 sandbox/2d.html create mode 100644 sandbox/src/2d/drawables/aabb.ts create mode 100644 sandbox/src/2d/drawables/circle.ts create mode 100644 sandbox/src/2d/drawables/drawable.ts create mode 100644 sandbox/src/2d/drawables/grid.ts create mode 100644 sandbox/src/2d/drawables/point.ts create mode 100644 sandbox/src/2d/drawables/ray.ts create mode 100644 sandbox/src/2d/main.ts create mode 100644 sandbox/src/2d/renderer.ts create mode 100644 sandbox/tsconfig.json create mode 100644 sandbox/vite.config.ts diff --git a/package.json b/package.json index 546c83e..bbb6260 100644 --- a/package.json +++ b/package.json @@ -16,20 +16,20 @@ "type": "module", "exports": { "./errors/*": { - "default": "./dist/errors/*.js", - "types": "./dist/errors/*.d.ts" + "types": "./dist/errors/*.d.ts", + "default": "./dist/errors/*.js" }, "./math/*": { - "default": "./dist/math/*.js", - "types": "./dist/math/*.d.ts" + "types": "./dist/math/*.d.ts", + "default": "./dist/math/*.js" }, "./logging/*": { - "default": "./dist/logging/*.js", - "types": "./dist/logging/*.d.ts" + "types": "./dist/logging/*.d.ts", + "default": "./dist/logging/*.js" }, "./logging/node/*": { - "default": "./dist/logging/node/*.js", - "types": "./dist/logging/node/*.d.ts" + "types": "./dist/logging/node/*.d.ts", + "default": "./dist/logging/node/*.js" } }, "files": [ @@ -49,6 +49,7 @@ "clean": "rimraf ./dist/* ./.tsbuildinfo/*", "test": "vitest", "lint": "eslint", + "sandbox": "vite ./sandbox", "ci:publish": "pnpm publish -r --access public", "prepublishOnly": "pnpm run clean && pnpm run build" }, @@ -59,6 +60,7 @@ "eslint": "^9.20.1", "rimraf": "^6.0.1", "typescript": "^5.7.3", + "vite": "^6.2.2", "vitest": "^3.0.6" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 09b50a1..59fc4c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ importers: typescript: specifier: ^5.7.3 version: 5.8.2 + vite: + specifier: ^6.2.2 + version: 6.2.2(@types/node@22.13.9) vitest: specifier: ^3.0.6 version: 3.0.8(@types/node@22.13.9) @@ -1725,8 +1728,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@6.2.0: - resolution: {integrity: sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==} + vite@6.2.2: + resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -2340,13 +2343,13 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@6.2.0(@types/node@22.13.9))': + '@vitest/mocker@3.0.8(vite@6.2.2(@types/node@22.13.9))': dependencies: '@vitest/spy': 3.0.8 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.9) '@vitest/pretty-format@3.0.8': dependencies: @@ -3726,7 +3729,7 @@ snapshots: debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.9) transitivePeerDependencies: - '@types/node' - jiti @@ -3741,7 +3744,7 @@ snapshots: - tsx - yaml - vite@6.2.0(@types/node@22.13.9): + vite@6.2.2(@types/node@22.13.9): dependencies: esbuild: 0.25.0 postcss: 8.5.3 @@ -3753,7 +3756,7 @@ snapshots: vitest@3.0.8(@types/node@22.13.9): dependencies: '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.2.0(@types/node@22.13.9)) + '@vitest/mocker': 3.0.8(vite@6.2.2(@types/node@22.13.9)) '@vitest/pretty-format': 3.0.8 '@vitest/runner': 3.0.8 '@vitest/snapshot': 3.0.8 @@ -3769,7 +3772,7 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.0(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.9) vite-node: 3.0.8(@types/node@22.13.9) why-is-node-running: 2.3.0 optionalDependencies: diff --git a/sandbox/2d.html b/sandbox/2d.html new file mode 100644 index 0000000..3e030eb --- /dev/null +++ b/sandbox/2d.html @@ -0,0 +1,14 @@ + + + + + + + 2D Sandbox - @bengsfort/stdlib + + + + + + + diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts new file mode 100644 index 0000000..99fb495 --- /dev/null +++ b/sandbox/src/2d/drawables/aabb.ts @@ -0,0 +1,31 @@ +import { IAABB2D } from "@stdlib/geometry/primitives"; +import { Vector2 } from "@stdlib/math/vector2"; + +export interface IDrawableAABB { + aabb: IAABB2D; + stroke?: string; + fill: string; +} + +export function drawAABB(ctx: CanvasRenderingContext2D, drawable: IDrawableAABB): void { + ctx.save(); + + const size = Vector2.Subtract(drawable.aabb.max, drawable.aabb.min); + const halfSize = Vector2.MultiplyScalar(size, 0.5); + const position = new Vector2( + drawable.aabb.min.x + halfSize.x, + drawable.aabb.min.y + halfSize.y, + ); + + ctx.fillStyle = drawable.fill; + ctx.strokeStyle = drawable.stroke ?? 'transparent'; + + ctx.translate(position.x, position.y); + ctx.rect(-halfSize.x, -halfSize.y, size.x, size.y); + + ctx.fill(); + ctx.stroke(); + + + ctx.restore(); +} diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts new file mode 100644 index 0000000..55313d8 --- /dev/null +++ b/sandbox/src/2d/drawables/drawable.ts @@ -0,0 +1,3 @@ +import { IDrawablePoint } from './point.js'; + +export type Drawable = IDrawablePoint; diff --git a/sandbox/src/2d/drawables/grid.ts b/sandbox/src/2d/drawables/grid.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/drawables/point.ts b/sandbox/src/2d/drawables/point.ts new file mode 100644 index 0000000..a29ee50 --- /dev/null +++ b/sandbox/src/2d/drawables/point.ts @@ -0,0 +1,26 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +export interface IDrawablePoint { + type: 'point'; + position: Vector2; + color: string; +} + +const POINT_RADIUS = 2; + +export function drawPoint(ctx: CanvasRenderingContext2D, point: IDrawablePoint): void { + ctx.save(); + + ctx.fillStyle = point.color; + ctx.translate(point.position.x, point.position.y); + ctx.roundRect( + -POINT_RADIUS, + -POINT_RADIUS, + POINT_RADIUS * 2, + POINT_RADIUS * 2, + POINT_RADIUS, + ); + ctx.fill(); + + ctx.restore(); +} diff --git a/sandbox/src/2d/drawables/ray.ts b/sandbox/src/2d/drawables/ray.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts new file mode 100644 index 0000000..4f767c7 --- /dev/null +++ b/sandbox/src/2d/main.ts @@ -0,0 +1,16 @@ +import { Renderer2D } from './renderer.js'; + +function main(): void { + const renderer = new Renderer2D(); + renderer.attach(); + + const tick = (now: number): void => { + // TODO: input + // TODO: logic update + renderer.render(); + }; + + tick(performance.now()); +} + +main(); diff --git a/sandbox/src/2d/renderer.ts b/sandbox/src/2d/renderer.ts new file mode 100644 index 0000000..f949d14 --- /dev/null +++ b/sandbox/src/2d/renderer.ts @@ -0,0 +1,89 @@ +function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { + const { devicePixelRatio } = window; + + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + canvas.style.width = `${width.toString(10)}px`; + canvas.style.height = `${height.toString(10)}px`; + + const ctx = canvas.getContext('2d'); + ctx?.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); + ctx?.clearRect(0, 0, width, height); +} + +function createCanvas( + width = window.innerWidth, + height = window.innerHeight, +): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + resizeCanvas(canvas, width, height); + return canvas; +} + +type UnbindCallback = () => void; +export function bindCanvasToWindow(canvas: HTMLCanvasElement): UnbindCallback { + const handler = (): void => { + resizeCanvas(canvas, window.innerWidth, window.innerHeight); + }; + + window.addEventListener('resize', handler); + return () => { + window.removeEventListener('resize', handler); + }; +} + +export class Renderer2D { + public clearColor = '#000000'; + + #_canvas: HTMLCanvasElement; + #_ctx: CanvasRenderingContext2D; + #_unbindCallback: UnbindCallback | null = null; + + constructor() { + const canvas = createCanvas(); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Missing rendering context'); + } + + this.#_canvas = canvas; + this.#_ctx = ctx; + } + + public attach(): void { + if (this.#_unbindCallback !== null) { + console.warn('Attempting to attach already attached renderer'); + return; + } + + this.#_unbindCallback = bindCanvasToWindow(this.#_canvas); + this.#_canvas.style.position = 'absolute'; + this.#_canvas.style.inset = '0'; + document.body.append(this.#_canvas); + } + + public detach(): void { + if (this.#_unbindCallback === null) { + console.warn('Attempting to detach non-attached renderer'); + return; + } + + this.#_unbindCallback(); + this.#_unbindCallback = null; + this.#_canvas.remove(); + } + + public render(): void { + const { width, height } = this.#_canvas; + + this.#_ctx.clearRect(0, 0, width, height); + this.#_ctx.fillStyle = this.clearColor; + this.#_ctx.save(); + + this.#_ctx.fillRect(0, 0, width, height); + // TODO: Drawables + + this.#_ctx.restore(); + } +} diff --git a/sandbox/tsconfig.json b/sandbox/tsconfig.json new file mode 100644 index 0000000..06c5d70 --- /dev/null +++ b/sandbox/tsconfig.json @@ -0,0 +1,42 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": [ + "./src/*" + ], + "@stdlib/*": [ + "../lib/*" + ], + } + }, + "references": [ + { + "path": "../tsconfig.build.json", + }, + ], + "include": [ + "src", + "vite.config.ts" + ], +} \ No newline at end of file diff --git a/sandbox/vite.config.ts b/sandbox/vite.config.ts new file mode 100644 index 0000000..88f0ef9 --- /dev/null +++ b/sandbox/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + rollupOptions: { + input: { + '2d': './2d.html', + }, + }, + }, +}); From 81c1d44f1c5f6062145c70dcc0c9ba1ec6d25129 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sat, 15 Mar 2025 11:41:46 +0200 Subject: [PATCH 14/31] Scaffold 2d sandbox renderer --- lib/geometry/collisions2d.ts | 3 -- package.json | 1 + pnpm-lock.yaml | 41 ++++++++++++++++++++++++++++ sandbox/src/2d/drawables/aabb.ts | 6 ++-- sandbox/src/2d/drawables/circle.ts | 30 ++++++++++++++++++++ sandbox/src/2d/drawables/drawable.ts | 36 ++++++++++++++++++++++-- sandbox/src/2d/drawables/ray.ts | 39 ++++++++++++++++++++++++++ sandbox/src/2d/main.ts | 37 ++++++++++++++++++++++++- sandbox/src/2d/renderer.ts | 11 ++++++-- sandbox/vite.config.ts | 2 ++ 10 files changed, 195 insertions(+), 11 deletions(-) diff --git a/lib/geometry/collisions2d.ts b/lib/geometry/collisions2d.ts index d33308e..3f83543 100644 --- a/lib/geometry/collisions2d.ts +++ b/lib/geometry/collisions2d.ts @@ -1,8 +1,5 @@ -import { networkInterfaces } from 'os'; - import { clamp, EPSILON } from '../math/utils.js'; import { IVec2, Vector2 } from '../math/vector2.js'; -import { Vector3 } from '../math/vector3.js'; import { IAABB2D, ICircle, IRay2D } from './primitives.js'; diff --git a/package.json b/package.json index bbb6260..ee1da5c 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "rimraf": "^6.0.1", "typescript": "^5.7.3", "vite": "^6.2.2", + "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.6" } } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 59fc4c5..ebba90a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,9 @@ importers: vite: specifier: ^6.2.2 version: 6.2.2(@types/node@22.13.9) + vite-tsconfig-paths: + specifier: ^5.1.4 + version: 5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.9)) vitest: specifier: ^3.0.6 version: 3.0.8(@types/node@22.13.9) @@ -1022,6 +1025,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + globrex@0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -1671,6 +1677,16 @@ packages: peerDependencies: typescript: '>=4.8.4' + tsconfck@3.1.5: + resolution: {integrity: sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -1728,6 +1744,14 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true + vite-tsconfig-paths@5.1.4: + resolution: {integrity: sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==} + peerDependencies: + vite: '*' + peerDependenciesMeta: + vite: + optional: true + vite@6.2.2: resolution: {integrity: sha512-yW7PeMM+LkDzc7CgJuRLMW2Jz0FxMOsVJ8Lv3gpgW9WLcb9cTW+121UEr1hvmfR7w3SegR5ItvYyzVz1vxNJgQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} @@ -3003,6 +3027,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3650,6 +3676,10 @@ snapshots: dependencies: typescript: 5.8.2 + tsconfck@3.1.5(typescript@5.8.2): + optionalDependencies: + typescript: 5.8.2 + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -3744,6 +3774,17 @@ snapshots: - tsx - yaml + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.9)): + dependencies: + debug: 4.4.0 + globrex: 0.1.2 + tsconfck: 3.1.5(typescript@5.8.2) + optionalDependencies: + vite: 6.2.2(@types/node@22.13.9) + transitivePeerDependencies: + - supports-color + - typescript + vite@6.2.2(@types/node@22.13.9): dependencies: esbuild: 0.25.0 diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts index 99fb495..1341177 100644 --- a/sandbox/src/2d/drawables/aabb.ts +++ b/sandbox/src/2d/drawables/aabb.ts @@ -1,7 +1,8 @@ -import { IAABB2D } from "@stdlib/geometry/primitives"; -import { Vector2 } from "@stdlib/math/vector2"; +import { IAABB2D } from '@stdlib/geometry/primitives'; +import { Vector2 } from '@stdlib/math/vector2'; export interface IDrawableAABB { + type: 'aabb'; aabb: IAABB2D; stroke?: string; fill: string; @@ -26,6 +27,5 @@ export function drawAABB(ctx: CanvasRenderingContext2D, drawable: IDrawableAABB) ctx.fill(); ctx.stroke(); - ctx.restore(); } diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts index e69de29..28d4181 100644 --- a/sandbox/src/2d/drawables/circle.ts +++ b/sandbox/src/2d/drawables/circle.ts @@ -0,0 +1,30 @@ +import { ICircle } from '@stdlib/geometry/primitives'; +import { Vector2 } from '@stdlib/math/vector2'; + +export interface IDrawableCircle { + type: 'circle'; + circle: ICircle; + stroke?: string; + fill: string; +} + +export function drawCircle( + ctx: CanvasRenderingContext2D, + drawable: IDrawableCircle, +): void { + ctx.save(); + + const halfSize = new Vector2(drawable.circle.radius, drawable.circle.radius); + const size = Vector2.MultiplyScalar(halfSize, 2); + + ctx.fillStyle = drawable.fill; + ctx.strokeStyle = drawable.stroke ?? 'transparent'; + + ctx.translate(drawable.circle.position.x, drawable.circle.position.y); + ctx.roundRect(-halfSize.x, -halfSize.y, size.x, size.y, drawable.circle.radius); + + ctx.fill(); + ctx.stroke(); + + ctx.restore(); +} diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts index 55313d8..7582a2f 100644 --- a/sandbox/src/2d/drawables/drawable.ts +++ b/sandbox/src/2d/drawables/drawable.ts @@ -1,3 +1,35 @@ -import { IDrawablePoint } from './point.js'; +import { drawAABB, IDrawableAABB } from './aabb.js'; +import { drawCircle, IDrawableCircle } from './circle.js'; +import { drawPoint, IDrawablePoint } from './point.js'; +import { drawRay, IDrawableRay } from './ray.js'; -export type Drawable = IDrawablePoint; +type DrawableMapExtractor = { + [T in Type as T['type']]: T; +}; + +type DrawableMap = DrawableMapExtractor< + IDrawablePoint | IDrawableAABB | IDrawableCircle | IDrawableRay +>; + +export type DrawableType = keyof DrawableMap; +export type Drawable = DrawableMap[T]; + +export function renderDrawable(ctx: CanvasRenderingContext2D, drawable: Drawable): void { + switch (drawable.type) { + case 'aabb': + drawAABB(ctx, drawable); + return; + + case 'point': + drawPoint(ctx, drawable); + return; + + case 'circle': + drawCircle(ctx, drawable); + return; + + case 'ray': + drawRay(ctx, drawable); + return; + } +} diff --git a/sandbox/src/2d/drawables/ray.ts b/sandbox/src/2d/drawables/ray.ts index e69de29..9decb66 100644 --- a/sandbox/src/2d/drawables/ray.ts +++ b/sandbox/src/2d/drawables/ray.ts @@ -0,0 +1,39 @@ +import { IRay2D } from '@stdlib/geometry/primitives'; +import { Vector2 } from '@stdlib/math/vector2'; + +export interface IDrawableRay { + type: 'ray'; + ray: IRay2D; + color: string; +} + +const POINT_RADIUS = 2; +const RAY_LENGTH = 9999; + +export function drawRay(ctx: CanvasRenderingContext2D, drawable: IDrawableRay): void { + ctx.save(); + + const { position, direction } = drawable.ray; + const end = Vector2.Normalize(direction).multiplyScalar(RAY_LENGTH); + + ctx.fillStyle = drawable.color; + ctx.translate(position.x, position.y); + + ctx.save(); + ctx.roundRect( + -POINT_RADIUS, + -POINT_RADIUS, + POINT_RADIUS * 2, + POINT_RADIUS * 2, + POINT_RADIUS, + ); + ctx.fill(); + ctx.restore(); + + ctx.beginPath(); + ctx.moveTo(position.x, position.y); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + + ctx.restore(); +} diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 4f767c7..1b8ab13 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,13 +1,48 @@ +import { Vector2 } from '@stdlib/math/vector2.js'; + +import { Drawable } from './drawables/drawable.js'; import { Renderer2D } from './renderer.js'; function main(): void { const renderer = new Renderer2D(); renderer.attach(); + const shapes: Drawable[] = [ + { + type: 'aabb', + aabb: { + min: new Vector2(-4, -4), + max: new Vector2(4, 4), + }, + fill: 'transparent', + }, + { + type: 'circle', + circle: { + position: new Vector2(8, 0), + radius: 3, + }, + fill: 'transparent', + }, + { + type: 'point', + position: new Vector2(6, 6), + color: '#f00', + }, + { + type: 'ray', + ray: { + position: new Vector2(-5, 5), + direction: new Vector2(1, 0), + }, + color: '#0f0', + }, + ]; + const tick = (now: number): void => { // TODO: input // TODO: logic update - renderer.render(); + renderer.render(shapes); }; tick(performance.now()); diff --git a/sandbox/src/2d/renderer.ts b/sandbox/src/2d/renderer.ts index f949d14..dfef035 100644 --- a/sandbox/src/2d/renderer.ts +++ b/sandbox/src/2d/renderer.ts @@ -1,3 +1,5 @@ +import { Drawable, renderDrawable } from './drawables/drawable'; + function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { const { devicePixelRatio } = window; @@ -74,7 +76,7 @@ export class Renderer2D { this.#_canvas.remove(); } - public render(): void { + public render(drawables: Drawable[] = []): void { const { width, height } = this.#_canvas; this.#_ctx.clearRect(0, 0, width, height); @@ -82,7 +84,12 @@ export class Renderer2D { this.#_ctx.save(); this.#_ctx.fillRect(0, 0, width, height); - // TODO: Drawables + this.#_ctx.translate(width * 0.5, height * 0.5); + this.#_ctx.scale(10, -10); + + for (const drawable of drawables) { + renderDrawable(this.#_ctx, drawable); + } this.#_ctx.restore(); } diff --git a/sandbox/vite.config.ts b/sandbox/vite.config.ts index 88f0ef9..4c6e870 100644 --- a/sandbox/vite.config.ts +++ b/sandbox/vite.config.ts @@ -1,4 +1,5 @@ import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; export default defineConfig({ build: { @@ -8,4 +9,5 @@ export default defineConfig({ }, }, }, + plugins: [tsconfigPaths()], }); From 9f1ccb05c49850d84890f340f467ef567b9e6b23 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sat, 15 Mar 2025 22:15:37 +0200 Subject: [PATCH 15/31] Implement customizable unit scaling --- sandbox/src/2d/drawables/aabb.ts | 21 ++++++--- sandbox/src/2d/drawables/circle.ts | 23 +++++++--- sandbox/src/2d/drawables/drawable.ts | 23 +++++++--- sandbox/src/2d/drawables/grid.ts | 67 ++++++++++++++++++++++++++++ sandbox/src/2d/drawables/point.ts | 24 +++++----- sandbox/src/2d/drawables/ray.ts | 28 ++++++------ sandbox/src/2d/main.ts | 13 +++++- sandbox/src/2d/render-settings.ts | 9 ++++ sandbox/src/2d/renderer.ts | 16 ++++--- 9 files changed, 174 insertions(+), 50 deletions(-) create mode 100644 sandbox/src/2d/render-settings.ts diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts index 1341177..6ec3cf9 100644 --- a/sandbox/src/2d/drawables/aabb.ts +++ b/sandbox/src/2d/drawables/aabb.ts @@ -1,6 +1,8 @@ import { IAABB2D } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; +import { RenderSettings } from '../render-settings'; + export interface IDrawableAABB { type: 'aabb'; aabb: IAABB2D; @@ -8,8 +10,12 @@ export interface IDrawableAABB { fill: string; } -export function drawAABB(ctx: CanvasRenderingContext2D, drawable: IDrawableAABB): void { - ctx.save(); +export function drawAABB( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + drawable: IDrawableAABB, +): void { + const { pixelsPerUnit } = settings; const size = Vector2.Subtract(drawable.aabb.max, drawable.aabb.min); const halfSize = Vector2.MultiplyScalar(size, 0.5); @@ -18,14 +24,19 @@ export function drawAABB(ctx: CanvasRenderingContext2D, drawable: IDrawableAABB) drawable.aabb.min.y + halfSize.y, ); + ctx.save(); ctx.fillStyle = drawable.fill; ctx.strokeStyle = drawable.stroke ?? 'transparent'; - ctx.translate(position.x, position.y); - ctx.rect(-halfSize.x, -halfSize.y, size.x, size.y); + ctx.translate(position.x * pixelsPerUnit, position.y * pixelsPerUnit); + ctx.rect( + -halfSize.x * pixelsPerUnit, + -halfSize.y * pixelsPerUnit, + size.x * pixelsPerUnit, + size.y * pixelsPerUnit, + ); ctx.fill(); ctx.stroke(); - ctx.restore(); } diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts index 28d4181..31e071c 100644 --- a/sandbox/src/2d/drawables/circle.ts +++ b/sandbox/src/2d/drawables/circle.ts @@ -1,5 +1,6 @@ import { ICircle } from '@stdlib/geometry/primitives'; -import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../render-settings'; export interface IDrawableCircle { type: 'circle'; @@ -8,21 +9,31 @@ export interface IDrawableCircle { fill: string; } +const CENTER_POINT_RADIUS = 4; + export function drawCircle( ctx: CanvasRenderingContext2D, + settings: RenderSettings, drawable: IDrawableCircle, ): void { ctx.save(); - const halfSize = new Vector2(drawable.circle.radius, drawable.circle.radius); - const size = Vector2.MultiplyScalar(halfSize, 2); - ctx.fillStyle = drawable.fill; ctx.strokeStyle = drawable.stroke ?? 'transparent'; - ctx.translate(drawable.circle.position.x, drawable.circle.position.y); - ctx.roundRect(-halfSize.x, -halfSize.y, size.x, size.y, drawable.circle.radius); + const { pixelsPerUnit } = settings; + ctx.translate( + drawable.circle.position.x * pixelsPerUnit, + drawable.circle.position.y * pixelsPerUnit, + ); + + ctx.beginPath(); + ctx.arc(0, 0, drawable.circle.radius * pixelsPerUnit, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + ctx.beginPath(); + ctx.arc(0, 0, CENTER_POINT_RADIUS, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts index 7582a2f..2b204a9 100644 --- a/sandbox/src/2d/drawables/drawable.ts +++ b/sandbox/src/2d/drawables/drawable.ts @@ -1,5 +1,8 @@ +import { RenderSettings } from '../render-settings.js'; + import { drawAABB, IDrawableAABB } from './aabb.js'; import { drawCircle, IDrawableCircle } from './circle.js'; +import { drawGrid, IDrawableGrid } from './grid.js'; import { drawPoint, IDrawablePoint } from './point.js'; import { drawRay, IDrawableRay } from './ray.js'; @@ -8,28 +11,36 @@ type DrawableMapExtractor = { }; type DrawableMap = DrawableMapExtractor< - IDrawablePoint | IDrawableAABB | IDrawableCircle | IDrawableRay + IDrawablePoint | IDrawableAABB | IDrawableCircle | IDrawableRay | IDrawableGrid >; export type DrawableType = keyof DrawableMap; export type Drawable = DrawableMap[T]; -export function renderDrawable(ctx: CanvasRenderingContext2D, drawable: Drawable): void { +export function renderDrawable( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + drawable: Drawable, +): void { switch (drawable.type) { case 'aabb': - drawAABB(ctx, drawable); + drawAABB(ctx, settings, drawable); return; case 'point': - drawPoint(ctx, drawable); + drawPoint(ctx, settings, drawable); return; case 'circle': - drawCircle(ctx, drawable); + drawCircle(ctx, settings, drawable); return; case 'ray': - drawRay(ctx, drawable); + drawRay(ctx, settings, drawable); + return; + + case 'grid': + drawGrid(ctx, settings, drawable); return; } } diff --git a/sandbox/src/2d/drawables/grid.ts b/sandbox/src/2d/drawables/grid.ts index e69de29..25c25a2 100644 --- a/sandbox/src/2d/drawables/grid.ts +++ b/sandbox/src/2d/drawables/grid.ts @@ -0,0 +1,67 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../render-settings'; + +export interface IDrawableGrid { + type: 'grid'; + color: string; + gridColor: string; + range: Vector2; +} + +export function drawGrid( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + drawable: IDrawableGrid, +): void { + const { pixelsPerUnit } = settings; + + ctx.save(); + ctx.translate(0, 0); + + // First draw the subgrid + ctx.beginPath(); + ctx.lineWidth = 1; + ctx.strokeStyle = drawable.gridColor; + + // TODO: Use pattern here instead? + for (let x = 1; x < drawable.range.x; x++) { + const scaledX = x * pixelsPerUnit; + const maxY = drawable.range.y * pixelsPerUnit; + + ctx.moveTo(scaledX, maxY); + ctx.lineTo(scaledX, -maxY); + ctx.moveTo(-scaledX, maxY); + ctx.lineTo(-scaledX, -maxY); + } + + for (let y = 1; y < drawable.range.y; y++) { + const scaledY = y * pixelsPerUnit; + const maxX = drawable.range.x * pixelsPerUnit; + + ctx.moveTo(-maxX, scaledY); + ctx.lineTo(maxX, scaledY); + ctx.moveTo(-maxX, -scaledY); + ctx.lineTo(maxX, -scaledY); + } + + ctx.stroke(); + ctx.restore(); + + // Then draw the main axis + ctx.save(); + + ctx.lineWidth = 2; + ctx.strokeStyle = drawable.color; + ctx.fillStyle = drawable.color; + ctx.setLineDash([]); + + ctx.beginPath(); + ctx.moveTo(-drawable.range.x * pixelsPerUnit, 0); + ctx.lineTo(drawable.range.x * pixelsPerUnit, 0); + ctx.moveTo(0, -drawable.range.y * pixelsPerUnit); + ctx.lineTo(0, drawable.range.y * pixelsPerUnit); + ctx.stroke(); + + ctx.restore(); +} diff --git a/sandbox/src/2d/drawables/point.ts b/sandbox/src/2d/drawables/point.ts index a29ee50..cba1bc3 100644 --- a/sandbox/src/2d/drawables/point.ts +++ b/sandbox/src/2d/drawables/point.ts @@ -1,25 +1,27 @@ import { Vector2 } from '@stdlib/math/vector2'; +import { RenderSettings } from '../render-settings'; + export interface IDrawablePoint { type: 'point'; position: Vector2; color: string; } -const POINT_RADIUS = 2; +const POINT_RADIUS = 4; -export function drawPoint(ctx: CanvasRenderingContext2D, point: IDrawablePoint): void { +export function drawPoint( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + point: IDrawablePoint, +): void { + const { pixelsPerUnit } = settings; ctx.save(); - ctx.fillStyle = point.color; - ctx.translate(point.position.x, point.position.y); - ctx.roundRect( - -POINT_RADIUS, - -POINT_RADIUS, - POINT_RADIUS * 2, - POINT_RADIUS * 2, - POINT_RADIUS, - ); + + ctx.translate(point.position.x * pixelsPerUnit, point.position.y * pixelsPerUnit); + ctx.beginPath(); + ctx.arc(0, 0, POINT_RADIUS, 0, Math.PI * 2); ctx.fill(); ctx.restore(); diff --git a/sandbox/src/2d/drawables/ray.ts b/sandbox/src/2d/drawables/ray.ts index 9decb66..8d3b180 100644 --- a/sandbox/src/2d/drawables/ray.ts +++ b/sandbox/src/2d/drawables/ray.ts @@ -1,37 +1,35 @@ import { IRay2D } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; +import { RenderSettings } from '../render-settings'; + export interface IDrawableRay { type: 'ray'; ray: IRay2D; color: string; } -const POINT_RADIUS = 2; const RAY_LENGTH = 9999; -export function drawRay(ctx: CanvasRenderingContext2D, drawable: IDrawableRay): void { +export function drawRay( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + drawable: IDrawableRay, +): void { ctx.save(); + const { pixelsPerUnit } = settings; const { position, direction } = drawable.ray; + + const scaledPosition = Vector2.MultiplyScalar(position, pixelsPerUnit); const end = Vector2.Normalize(direction).multiplyScalar(RAY_LENGTH); ctx.fillStyle = drawable.color; - ctx.translate(position.x, position.y); - - ctx.save(); - ctx.roundRect( - -POINT_RADIUS, - -POINT_RADIUS, - POINT_RADIUS * 2, - POINT_RADIUS * 2, - POINT_RADIUS, - ); - ctx.fill(); - ctx.restore(); + ctx.translate(scaledPosition.x, scaledPosition.y); + ctx.strokeStyle = drawable.color; ctx.beginPath(); - ctx.moveTo(position.x, position.y); + ctx.moveTo(0, 0); ctx.lineTo(end.x, end.y); ctx.stroke(); diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 1b8ab13..26a361d 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -8,13 +8,19 @@ function main(): void { renderer.attach(); const shapes: Drawable[] = [ + { + type: 'grid', + color: '#fff', + gridColor: '#383838', + range: new Vector2(50, 50), + }, { type: 'aabb', aabb: { min: new Vector2(-4, -4), max: new Vector2(4, 4), }, - fill: 'transparent', + fill: 'blue', }, { type: 'circle', @@ -23,6 +29,7 @@ function main(): void { radius: 3, }, fill: 'transparent', + stroke: '#f00', }, { type: 'point', @@ -39,7 +46,9 @@ function main(): void { }, ]; - const tick = (now: number): void => { + const tick = (_now: number): void => { + requestAnimationFrame(tick); + // TODO: input // TODO: logic update renderer.render(shapes); diff --git a/sandbox/src/2d/render-settings.ts b/sandbox/src/2d/render-settings.ts new file mode 100644 index 0000000..4dff6e2 --- /dev/null +++ b/sandbox/src/2d/render-settings.ts @@ -0,0 +1,9 @@ +export interface RenderSettings { + pixelsPerUnit: number; + clearColor: string; +} + +export const defaultRenderSettings = (): RenderSettings => ({ + pixelsPerUnit: 32, + clearColor: '#000', +}); diff --git a/sandbox/src/2d/renderer.ts b/sandbox/src/2d/renderer.ts index dfef035..96b7ba4 100644 --- a/sandbox/src/2d/renderer.ts +++ b/sandbox/src/2d/renderer.ts @@ -1,4 +1,5 @@ import { Drawable, renderDrawable } from './drawables/drawable'; +import { defaultRenderSettings, RenderSettings } from './render-settings'; function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { const { devicePixelRatio } = window; @@ -35,13 +36,13 @@ export function bindCanvasToWindow(canvas: HTMLCanvasElement): UnbindCallback { } export class Renderer2D { - public clearColor = '#000000'; + public readonly settings: RenderSettings; #_canvas: HTMLCanvasElement; #_ctx: CanvasRenderingContext2D; #_unbindCallback: UnbindCallback | null = null; - constructor() { + constructor(settings: RenderSettings = defaultRenderSettings()) { const canvas = createCanvas(); const ctx = canvas.getContext('2d'); @@ -49,6 +50,7 @@ export class Renderer2D { throw new Error('Missing rendering context'); } + this.settings = settings; this.#_canvas = canvas; this.#_ctx = ctx; } @@ -80,15 +82,19 @@ export class Renderer2D { const { width, height } = this.#_canvas; this.#_ctx.clearRect(0, 0, width, height); - this.#_ctx.fillStyle = this.clearColor; this.#_ctx.save(); + this.#_ctx.fillStyle = this.settings.clearColor; this.#_ctx.fillRect(0, 0, width, height); + this.#_ctx.translate(width * 0.5, height * 0.5); - this.#_ctx.scale(10, -10); + this.#_ctx.scale(1, -1); for (const drawable of drawables) { - renderDrawable(this.#_ctx, drawable); + this.#_ctx.save(); + this.#_ctx.beginPath(); + renderDrawable(this.#_ctx, this.settings, drawable); + this.#_ctx.restore(); } this.#_ctx.restore(); From 5acda8cc937b696705dbf346eef9393368cc4349 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sat, 15 Mar 2025 22:17:43 +0200 Subject: [PATCH 16/31] Update hierarchy for renderer --- sandbox/src/2d/drawables/aabb.ts | 2 +- sandbox/src/2d/drawables/circle.ts | 2 +- sandbox/src/2d/drawables/drawable.ts | 2 +- sandbox/src/2d/drawables/grid.ts | 2 +- sandbox/src/2d/drawables/point.ts | 2 +- sandbox/src/2d/drawables/ray.ts | 2 +- sandbox/src/2d/main.ts | 2 +- sandbox/src/2d/{ => renderer}/render-settings.ts | 0 sandbox/src/2d/{ => renderer}/renderer.ts | 3 ++- 9 files changed, 9 insertions(+), 8 deletions(-) rename sandbox/src/2d/{ => renderer}/render-settings.ts (100%) rename sandbox/src/2d/{ => renderer}/renderer.ts (97%) diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts index 6ec3cf9..f4b295b 100644 --- a/sandbox/src/2d/drawables/aabb.ts +++ b/sandbox/src/2d/drawables/aabb.ts @@ -1,7 +1,7 @@ import { IAABB2D } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; -import { RenderSettings } from '../render-settings'; +import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableAABB { type: 'aabb'; diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts index 31e071c..4afe1b0 100644 --- a/sandbox/src/2d/drawables/circle.ts +++ b/sandbox/src/2d/drawables/circle.ts @@ -1,6 +1,6 @@ import { ICircle } from '@stdlib/geometry/primitives'; -import { RenderSettings } from '../render-settings'; +import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableCircle { type: 'circle'; diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts index 2b204a9..c430749 100644 --- a/sandbox/src/2d/drawables/drawable.ts +++ b/sandbox/src/2d/drawables/drawable.ts @@ -1,4 +1,4 @@ -import { RenderSettings } from '../render-settings.js'; +import { RenderSettings } from '../renderer/render-settings.js'; import { drawAABB, IDrawableAABB } from './aabb.js'; import { drawCircle, IDrawableCircle } from './circle.js'; diff --git a/sandbox/src/2d/drawables/grid.ts b/sandbox/src/2d/drawables/grid.ts index 25c25a2..4ff7297 100644 --- a/sandbox/src/2d/drawables/grid.ts +++ b/sandbox/src/2d/drawables/grid.ts @@ -1,6 +1,6 @@ import { Vector2 } from '@stdlib/math/vector2'; -import { RenderSettings } from '../render-settings'; +import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableGrid { type: 'grid'; diff --git a/sandbox/src/2d/drawables/point.ts b/sandbox/src/2d/drawables/point.ts index cba1bc3..c80952f 100644 --- a/sandbox/src/2d/drawables/point.ts +++ b/sandbox/src/2d/drawables/point.ts @@ -1,6 +1,6 @@ import { Vector2 } from '@stdlib/math/vector2'; -import { RenderSettings } from '../render-settings'; +import { RenderSettings } from '../renderer/render-settings'; export interface IDrawablePoint { type: 'point'; diff --git a/sandbox/src/2d/drawables/ray.ts b/sandbox/src/2d/drawables/ray.ts index 8d3b180..03f8666 100644 --- a/sandbox/src/2d/drawables/ray.ts +++ b/sandbox/src/2d/drawables/ray.ts @@ -1,7 +1,7 @@ import { IRay2D } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; -import { RenderSettings } from '../render-settings'; +import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableRay { type: 'ray'; diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 26a361d..5718ec5 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,7 +1,7 @@ import { Vector2 } from '@stdlib/math/vector2.js'; import { Drawable } from './drawables/drawable.js'; -import { Renderer2D } from './renderer.js'; +import { Renderer2D } from './renderer/renderer.js'; function main(): void { const renderer = new Renderer2D(); diff --git a/sandbox/src/2d/render-settings.ts b/sandbox/src/2d/renderer/render-settings.ts similarity index 100% rename from sandbox/src/2d/render-settings.ts rename to sandbox/src/2d/renderer/render-settings.ts diff --git a/sandbox/src/2d/renderer.ts b/sandbox/src/2d/renderer/renderer.ts similarity index 97% rename from sandbox/src/2d/renderer.ts rename to sandbox/src/2d/renderer/renderer.ts index 96b7ba4..0bc6232 100644 --- a/sandbox/src/2d/renderer.ts +++ b/sandbox/src/2d/renderer/renderer.ts @@ -1,4 +1,5 @@ -import { Drawable, renderDrawable } from './drawables/drawable'; +import { Drawable, renderDrawable } from '../drawables/drawable'; + import { defaultRenderSettings, RenderSettings } from './render-settings'; function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { From ca3c1f6b5c7404c52c3228ef7a1c124bd165151e Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 16 Mar 2025 23:12:26 +0200 Subject: [PATCH 17/31] Scaffold event dispatcher and input manager for sandbox --- lib/events/event-dispatcher.ts | 89 +++++++++++ sandbox/src/2d/input/manager.ts | 252 ++++++++++++++++++++++++++++++++ sandbox/src/2d/input/mouse.ts | 8 + 3 files changed, 349 insertions(+) create mode 100644 lib/events/event-dispatcher.ts create mode 100644 sandbox/src/2d/input/manager.ts create mode 100644 sandbox/src/2d/input/mouse.ts diff --git a/lib/events/event-dispatcher.ts b/lib/events/event-dispatcher.ts new file mode 100644 index 0000000..079ffb5 --- /dev/null +++ b/lib/events/event-dispatcher.ts @@ -0,0 +1,89 @@ +export type EventMap = Record; +export type EventType = keyof Map; +export type EventPayload< + Map extends EventMap, + Key extends EventType = EventType, +> = Map[Key]; +export type EventListener< + Map extends EventMap, + Key extends EventType = EventType, +> = (...args: EventPayload) => void | Promise; + +type LazyListenerMap = { + [K in keyof Map]?: Set>; +}; + +type UnsubListener = () => void; + +export class EventDispatcher { + #_listeners: LazyListenerMap = {}; + #_autoRemoveListeners: LazyListenerMap = {}; + + #_listenerCount = 0; + #_events = new Set>(); + + public addListener = EventType>( + event: Event, + listener: EventListener, + once?: boolean, + ): UnsubListener { + // Lazy-instantiate the listener set. + let listeners = this.#_listeners[event]; + if (!listeners) { + listeners = new Set(); + this.#_listeners[event] = listeners; + this.#_events.add(event); + } + + const unsub: UnsubListener = () => { + this.removeListener(event, listener); + }; + + // Only add the listener if we don't already have it. + if (listeners.has(listener)) { + return unsub; + } + + this.#_listenerCount++; + listeners.add(listener); + + // Cache the listener reference to the auto remove set so we can remove it + // after it triggers for the first time. + if (once) { + let autoRemoveListeners = this.#_autoRemoveListeners[event]; + + if (!autoRemoveListeners) { + autoRemoveListeners = new Set(); + this.#_autoRemoveListeners[event] = autoRemoveListeners; + } + + autoRemoveListeners.add(listener); + } + + return unsub; + } + + public removeListener = EventType>( + event: Event, + listener: EventListener, + ): void { + const listeners = this.#_listeners[event]; + if (listeners && listeners.delete(listener)) { + this.#_listenerCount = Math.max(this.#_listenerCount - 1, 0); + } + + // TODO: Implement + } + + public removeAllListeners(): void { + // TODO: Implement + } + + public getListenerCount(): number { + return this.#_listenerCount; + } + + public getEvents(): EventType[] { + return [...this.#_events]; + } +} diff --git a/sandbox/src/2d/input/manager.ts b/sandbox/src/2d/input/manager.ts new file mode 100644 index 0000000..45a9fcc --- /dev/null +++ b/sandbox/src/2d/input/manager.ts @@ -0,0 +1,252 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +interface InputActionBoolean { + type: 'boolean'; + bindings: readonly string[]; +} + +interface InputActionRange { + type: 'range'; + minMax: [min: number, max: number]; + bindingsNeg: readonly string[]; + bindingsPos: readonly string[]; +} + +interface InputActionVectorRange { + type: 'vector_range'; + range: { + min: { x: number; y: number }; + max: { x: number; y: number }; + }; + bindingsNeg: { + x: readonly string[]; + y: readonly string[]; + }; + bindingsPos: { + x: readonly string[]; + y: readonly string[]; + }; +} + +export type InputActionDefinition = + | InputActionBoolean + | InputActionRange + | InputActionVectorRange; +export type InputActionType = InputActionDefinition['type']; + +// @todo: update to work with new definitions. +// @todo: should store each core type seperately. +// @todo: expose mouse position/button down too (maybe need renderer for clamp coords to viewport?) +export type ActionMap = Record; +type InputAction = keyof Map; + +class BadInputActionError extends Error { + constructor(action: string) { + super(`Invalid input action provided (${action})`); + this.name = 'BadInputActionError'; + } +} + +class ActionTypeMismatchError extends Error { + constructor( + action: InputAction, + given: InputActionType, + expected?: InputActionType, + ) { + super( + `Tried retrieving wrong action type for action "${action}" (tried type ${given}, expected ${expected ?? 'unknown'})`, + ); + this.name = 'ActionTypeMismatchError'; + } +} + +export class InputManager { + #_actions?: Actions; + #_bindingsMap = new Map>(); + #_codeMap = new Map(); + #_boolActions = new Map, boolean>(); + #_rangeActions = new Map, number>(); + #_vecRangeActions = new Map, Vector2>(); + + public clearActions(): void { + this.#_actions = undefined; + this.#_codeMap.clear(); + this.#_bindingsMap.clear(); + this.#_boolActions.clear(); + this.#_rangeActions.clear(); + this.#_vecRangeActions.clear(); + } + + public registerActions(actions: Actions): void { + this.clearActions(); + + this.#_actions = actions; + + // Init code lookup maps + for (const [action, definition] of Object.entries(actions)) { + const bindings: string[] = []; + + // Determine bindings for this definition and create the initial value. + switch (definition.type) { + case 'boolean': + bindings.push(...definition.bindings); + this.#_boolActions.set(action, false); + break; + + case 'range': + bindings.push(...definition.bindingsNeg, ...definition.bindingsPos); + this.#_rangeActions.set(action, 0); + break; + + case 'vector_range': + bindings.push( + ...definition.bindingsPos.x, + ...definition.bindingsPos.y, + ...definition.bindingsNeg.x, + ...definition.bindingsNeg.y, + ); + this.#_vecRangeActions.set(action, new Vector2()); + break; + + default: + throw new BadInputActionError(action); + } + + // Cache the bindings for this action. + for (const bind of bindings) { + this.#_bindingsMap.set(bind, action); + this.#_codeMap.set(bind, false); + } + } + + document.addEventListener('keydown', this.#handleKeyDown); + document.addEventListener('keyup', this.#handleKeyUp); + } + + public getBoolAction(action: InputAction): boolean { + const state = this.#_boolActions.get(action); + if (typeof state === 'undefined') { + throw new ActionTypeMismatchError( + action as string, + 'boolean', + this.#_actions?.[action]?.type, + ); + } + return state; + } + + public getRangeAction(action: InputAction): number { + const state = this.#_rangeActions.get(action); + if (typeof state === 'undefined') { + throw new ActionTypeMismatchError( + action as string, + 'range', + this.#_actions?.[action]?.type, + ); + } + return state; + } + + public getVectorRangeAction(action: InputAction): Vector2 { + const state = this.#_vecRangeActions.get(action); + if (typeof state === 'undefined') { + throw new ActionTypeMismatchError( + action as string, + 'vector_range', + this.#_actions?.[action]?.type, + ); + } + return state; + } + + #handleKeyDown = (ev: KeyboardEvent): void => { + const keyCode = ev.code; + + // Make sure we care about this particular key being pressed. + // 1. Grab action name from bindings map + // 2. Grab action from action map. + // If either fail, ignore the key press. + const actionName = this.#_bindingsMap.get(keyCode); + if (!actionName) { + return; + } + + const action = this.#_actions?.[actionName]; + if (!action) { + return; + } + + // Before type-specific handling, cache that the key is down. + this.#_codeMap.set(keyCode, true); + + switch (action.type) { + case 'boolean': + this.#_boolActions.set(actionName, true); + break; + + case 'range': + const current = this.#_rangeActions.get(actionName) ?? 0; + const modifier = action.bindingsPos.includes(keyCode) ? 1 : -1; + this.#_rangeActions.set(actionName, current + modifier); + break; + + case 'vector_range': + const vector = this.#_vecRangeActions.get(actionName) ?? new Vector2(); + const xModifier = action.bindingsPos.x.includes(keyCode) ? 1 : -1; + const yModifier = action.bindingsPos.y.includes(keyCode) ? 1 : -1; + vector.x += xModifier; + vector.y += yModifier; + this.#_vecRangeActions.set( + actionName, + vector.clamp(action.range.min, action.range.max).normalize(), + ); + break; + + default: + return; + } + }; + + #handleKeyUp = (ev: KeyboardEvent): void => { + const keyCode = ev.code; + + const actionName = this.#_bindingsMap.get(keyCode); + if (!actionName) { + return; + } + + const action = this.#_actions?.[actionName]; + if (!action) { + return; + } + + this.#_codeMap.set(keyCode, false); + + switch (action.type) { + case 'boolean': + this.#_boolActions.set(actionName, false); + break; + + case 'range': + const current = this.#_rangeActions.get(actionName) ?? 0; + const modifier = action.bindingsPos.includes(keyCode) ? -1 : 1; + this.#_rangeActions.set(actionName, current + modifier); + break; + + case 'vector_range': + const vector = this.#_vecRangeActions.get(actionName) ?? new Vector2(); + const xModifier = action.bindingsPos.x.includes(keyCode) ? -1 : 1; + const yModifier = action.bindingsPos.y.includes(keyCode) ? -1 : 1; + vector.x += xModifier; + vector.y += yModifier; + this.#_vecRangeActions.set( + actionName, + vector.clamp(action.range.min, action.range.max).normalize(), + ); + break; + + default: + return; + } + }; +} diff --git a/sandbox/src/2d/input/mouse.ts b/sandbox/src/2d/input/mouse.ts new file mode 100644 index 0000000..db14021 --- /dev/null +++ b/sandbox/src/2d/input/mouse.ts @@ -0,0 +1,8 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +export class MouseInput { + public readonly mousePosition: Vector2; + constructor() { + // + } +} From a3173b4d6ae6675ebd73efb0ed5c62d9dd3752f5 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Tue, 18 Mar 2025 09:16:46 +0200 Subject: [PATCH 18/31] Implement event dispatcher, adad tests --- lib/events/__tests__/event-dispatcher.test.ts | 103 +++ lib/events/event-dispatcher.ts | 53 +- pnpm-lock.yaml | 735 +++++++++--------- 3 files changed, 523 insertions(+), 368 deletions(-) create mode 100644 lib/events/__tests__/event-dispatcher.test.ts diff --git a/lib/events/__tests__/event-dispatcher.test.ts b/lib/events/__tests__/event-dispatcher.test.ts new file mode 100644 index 0000000..fcbfe73 --- /dev/null +++ b/lib/events/__tests__/event-dispatcher.test.ts @@ -0,0 +1,103 @@ +import { describe, it, expect, vi } from 'vitest'; + +import { EventDispatcher, EventMap } from '../event-dispatcher.js'; + +interface TestEventMap extends EventMap { + foo: [value: number, bool: boolean]; + bar: [str: string]; + gaz: []; +} + +describe('EventDispatcher', () => { + it('should expose how many listeners are currently attached', () => { + const dispatch = new EventDispatcher(); + + expect(dispatch.getListenerCount()).toEqual(0); + + const onFoo = vi.fn(); + const onBar = vi.fn(); + + dispatch.addListener('foo', onFoo); + dispatch.addListener('bar', onBar); + expect(dispatch.getListenerCount()).toEqual(2); + + dispatch.removeListener('foo', onFoo); + expect(dispatch.getListenerCount()).toEqual(1); + + dispatch.removeAllListeners(); + expect(dispatch.getListenerCount()).toEqual(0); + }); + + it('should expose the event names that have listeners', () => { + const dispatch = new EventDispatcher(); + + expect(dispatch.getEvents()).toHaveLength(0); + + dispatch.addListener('foo', () => {}); + expect(dispatch.getEvents()).toContain('foo'); + + const onGaz = () => {}; + dispatch.addListener('foo', () => {}); + dispatch.addListener('gaz', onGaz); + const withGaz = dispatch.getEvents(); + + expect(withGaz).toHaveLength(2); + expect(withGaz).toContain('gaz'); + + dispatch.removeListener('gaz', onGaz); + const withoutGaz = dispatch.getEvents(); + + expect(withoutGaz).toHaveLength(1); + expect(withoutGaz).not.toContain('gaz'); + }); + + it('should support adding event listeners and triggering events', () => { + const dispatch = new EventDispatcher(); + const onFoo = vi.fn(); + + dispatch.addListener('foo', onFoo); + dispatch.trigger('foo', 52, true); + expect(onFoo).toHaveBeenCalledExactlyOnceWith(52, true); + }); + + it('should not call a listener after removing it', () => { + const dispatch = new EventDispatcher(); + const onFoo = vi.fn(); + + dispatch.addListener('foo', onFoo); + dispatch.trigger('foo', 52, true); + dispatch.removeListener('foo', onFoo); + dispatch.trigger('foo', 5, false); + + expect(onFoo).toHaveBeenCalledExactlyOnceWith(52, true); + }); + + it('should only call listeners for the event that is being triggered', () => { + const dispatch = new EventDispatcher(); + const onFoo = vi.fn(); + const onBar = vi.fn(); + + dispatch.addListener('foo', onFoo); + dispatch.addListener('bar', onBar); + + dispatch.trigger('foo', 52, true); + dispatch.trigger('bar', 'triggered'); + + expect(onFoo).toHaveBeenCalledExactlyOnceWith(52, true); + expect(onBar).toHaveBeenCalledExactlyOnceWith('triggered'); + }); + + it('should support removing a listener after one event', () => { + const dispatch = new EventDispatcher(); + const onFoo = vi.fn(); + + dispatch.addListener('foo', onFoo, true); + expect(dispatch.getListenerCount()).toEqual(1); + + dispatch.trigger('foo', 52, true); + dispatch.trigger('foo', 30, false); + expect(onFoo).toHaveBeenCalledExactlyOnceWith(52, true); + + expect(dispatch.getListenerCount()).toEqual(0); + }); +}); diff --git a/lib/events/event-dispatcher.ts b/lib/events/event-dispatcher.ts index 079ffb5..fa5a92c 100644 --- a/lib/events/event-dispatcher.ts +++ b/lib/events/event-dispatcher.ts @@ -1,9 +1,9 @@ -export type EventMap = Record; +export type EventMap = object; export type EventType = keyof Map; export type EventPayload< Map extends EventMap, Key extends EventType = EventType, -> = Map[Key]; +> = Map[Key] extends unknown[] ? Map[Key] : unknown[]; export type EventListener< Map extends EventMap, Key extends EventType = EventType, @@ -68,15 +68,45 @@ export class EventDispatcher { listener: EventListener, ): void { const listeners = this.#_listeners[event]; - if (listeners && listeners.delete(listener)) { + if (listeners?.delete(listener)) { this.#_listenerCount = Math.max(this.#_listenerCount - 1, 0); } - // TODO: Implement + const autoRemoveListeners = this.#_autoRemoveListeners[event]; + autoRemoveListeners?.delete(listener); + + this.#_removeEmptyEvent(event); } public removeAllListeners(): void { - // TODO: Implement + for (const event in this.#_listeners) { + this.#_listeners[event]?.clear(); + } + + for (const event in this.#_autoRemoveListeners) { + this.#_autoRemoveListeners[event]?.clear(); + } + + this.#_autoRemoveListeners = {}; + this.#_listeners = {}; + + this.#_events.clear(); + this.#_listenerCount = 0; + } + + public trigger = EventType>( + event: Event, + ...payload: EventPayload + ): void { + const listeners = this.#_listeners[event]; + + listeners?.forEach((listener) => { + void listener(...payload); + + if (this.#_autoRemoveListeners[event]?.has(listener)) { + this.removeListener(event, listener); + } + }); } public getListenerCount(): number { @@ -86,4 +116,17 @@ export class EventDispatcher { public getEvents(): EventType[] { return [...this.#_events]; } + + #_removeEmptyEvent(event: EventType): void { + if ((this.#_listeners[event]?.size ?? 0) < 1) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.#_listeners[event]; + this.#_events.delete(event); + } + + if ((this.#_autoRemoveListeners[event]?.size ?? 0) < 1) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.#_autoRemoveListeners[event]; + } + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ebba90a..b9987e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,16 +10,16 @@ importers: devDependencies: '@bengsfort/eslint-config-flat': specifier: ^0.2.4 - version: 0.2.4(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) + version: 0.2.4(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2) '@changesets/cli': specifier: ^2.28.0 version: 2.28.1 '@types/node': specifier: ^22.13.4 - version: 22.13.9 + version: 22.13.10 eslint: specifier: ^9.20.1 - version: 9.21.0 + version: 9.22.0 rimraf: specifier: ^6.0.1 version: 6.0.1 @@ -28,18 +28,18 @@ importers: version: 5.8.2 vite: specifier: ^6.2.2 - version: 6.2.2(@types/node@22.13.9) + version: 6.2.2(@types/node@22.13.10) vite-tsconfig-paths: specifier: ^5.1.4 - version: 5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.9)) + version: 5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)) vitest: specifier: ^3.0.6 - version: 3.0.8(@types/node@22.13.9) + version: 3.0.9(@types/node@22.13.10) packages: - '@babel/runtime@7.26.9': - resolution: {integrity: sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==} + '@babel/runtime@7.26.10': + resolution: {integrity: sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==} engines: {node: '>=6.9.0'} '@bengsfort/eslint-config-flat@0.2.4': @@ -104,158 +104,158 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@esbuild/aix-ppc64@0.25.0': - resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==} + '@esbuild/aix-ppc64@0.25.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.0': - resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==} + '@esbuild/android-arm64@0.25.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.0': - resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==} + '@esbuild/android-arm@0.25.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.0': - resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==} + '@esbuild/android-x64@0.25.1': + resolution: {integrity: sha512-GCj6WfUtNldqUzYkN/ITtlhwQqGWu9S45vUXs7EIYf+7rCiiqH9bCloatO9VhxsL0Pji+PF4Lz2XXCES+Q8hDw==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.0': - resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==} + '@esbuild/darwin-arm64@0.25.1': + resolution: {integrity: sha512-5hEZKPf+nQjYoSr/elb62U19/l1mZDdqidGfmFutVUjjUZrOazAtwK+Kr+3y0C/oeJfLlxo9fXb1w7L+P7E4FQ==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.0': - resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.0': - resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==} + '@esbuild/freebsd-arm64@0.25.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.0': - resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==} + '@esbuild/freebsd-x64@0.25.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.0': - resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.0': - resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==} + '@esbuild/linux-arm@0.25.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.0': - resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==} + '@esbuild/linux-ia32@0.25.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.0': - resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.0': - resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==} + '@esbuild/linux-mips64el@0.25.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.0': - resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==} + '@esbuild/linux-ppc64@0.25.1': + resolution: {integrity: sha512-/6VBJOwUf3TdTvJZ82qF3tbLuWsscd7/1w+D9LH0W/SqUgM5/JJD0lrJ1fVIfZsqB6RFmLCe0Xz3fmZc3WtyVg==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.0': - resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==} + '@esbuild/linux-riscv64@0.25.1': + resolution: {integrity: sha512-nSut/Mx5gnilhcq2yIMLMe3Wl4FK5wx/o0QuuCLMtmJn+WeWYoEGDN1ipcN72g1WHsnIbxGXd4i/MF0gTcuAjQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.0': - resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.0': - resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.0': - resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==} + '@esbuild/netbsd-arm64@0.25.1': + resolution: {integrity: sha512-O96poM2XGhLtpTh+s4+nP7YCCAfb4tJNRVZHfIE7dgmax+yMP2WgMd2OecBuaATHKTHsLWHQeuaxMRnCsH8+5g==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.0': - resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.0': - resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==} + '@esbuild/openbsd-arm64@0.25.1': + resolution: {integrity: sha512-Na9T3szbXezdzM/Kfs3GcRQNjHzM6GzFBeU1/6IV/npKP5ORtp9zbQjvkDJ47s6BCgaAZnnnu/cY1x342+MvZg==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.0': - resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.25.0': - resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.0': - resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==} + '@esbuild/win32-arm64@0.25.1': + resolution: {integrity: sha512-GE7XvrdOzrb+yVKB9KsRMq+7a2U/K5Cf/8grVFRAGJmfADr/e/ODQ134RK2/eeHqYV5eQRFxb1hY7Nr15fv1NQ==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.0': - resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.0': - resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==} + '@esbuild/win32-x64@0.25.1': + resolution: {integrity: sha512-Y1EQdcfwMSeQN/ujR5VayLOJ1BHaK+ssyk0AEzPjC+t1lITgsnccPqFjb6V+LsTp/9Iov4ysfjxLaGJ9RPtkVg==} engines: {node: '>=18'} cpu: [x64] os: [win32] - '@eslint-community/eslint-utils@4.4.1': - resolution: {integrity: sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==} + '@eslint-community/eslint-utils@4.5.1': + resolution: {integrity: sha512-soEIOALTfTK6EjmKMMoLugwaP0rzkad90iIWd1hMO9ARkSAyjfMfkRRhLvD5qH7vvM0Cg72pieUfR6yh6XxC4w==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 @@ -268,6 +268,10 @@ packages: resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/config-helpers@0.1.0': + resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@eslint/core@0.12.0': resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -276,8 +280,8 @@ packages: resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.21.0': - resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==} + '@eslint/js@9.22.0': + resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.6': @@ -337,98 +341,98 @@ packages: resolution: {integrity: sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} - '@rollup/rollup-android-arm-eabi@4.34.9': - resolution: {integrity: sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA==} + '@rollup/rollup-android-arm-eabi@4.36.0': + resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.9': - resolution: {integrity: sha512-4KW7P53h6HtJf5Y608T1ISKvNIYLWRKMvfnG0c44M6In4DQVU58HZFEVhWINDZKp7FZps98G3gxwC1sb0wXUUg==} + '@rollup/rollup-android-arm64@4.36.0': + resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.9': - resolution: {integrity: sha512-0CY3/K54slrzLDjOA7TOjN1NuLKERBgk9nY5V34mhmuu673YNb+7ghaDUs6N0ujXR7fz5XaS5Aa6d2TNxZd0OQ==} + '@rollup/rollup-darwin-arm64@4.36.0': + resolution: {integrity: sha512-JQ1Jk5G4bGrD4pWJQzWsD8I1n1mgPXq33+/vP4sk8j/z/C2siRuxZtaUA7yMTf71TCZTZl/4e1bfzwUmFb3+rw==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.34.9': - resolution: {integrity: sha512-eOojSEAi/acnsJVYRxnMkPFqcxSMFfrw7r2iD9Q32SGkb/Q9FpUY1UlAu1DH9T7j++gZ0lHjnm4OyH2vCI7l7Q==} + '@rollup/rollup-darwin-x64@4.36.0': + resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.9': - resolution: {integrity: sha512-2lzjQPJbN5UnHm7bHIUKFMulGTQwdvOkouJDpPysJS+QFBGDJqcfh+CxxtG23Ik/9tEvnebQiylYoazFMAgrYw==} + '@rollup/rollup-freebsd-arm64@4.36.0': + resolution: {integrity: sha512-KXVsijKeJXOl8QzXTsA+sHVDsFOmMCdBRgFmBb+mfEb/7geR7+C8ypAml4fquUt14ZyVXaw2o1FWhqAfOvA4sg==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.34.9': - resolution: {integrity: sha512-SLl0hi2Ah2H7xQYd6Qaiu01kFPzQ+hqvdYSoOtHYg/zCIFs6t8sV95kaoqjzjFwuYQLtOI0RZre/Ke0nPaQV+g==} + '@rollup/rollup-freebsd-x64@4.36.0': + resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.34.9': - resolution: {integrity: sha512-88I+D3TeKItrw+Y/2ud4Tw0+3CxQ2kLgu3QvrogZ0OfkmX/DEppehus7L3TS2Q4lpB+hYyxhkQiYPJ6Mf5/dPg==} + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': + resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm-musleabihf@4.34.9': - resolution: {integrity: sha512-3qyfWljSFHi9zH0KgtEPG4cBXHDFhwD8kwg6xLfHQ0IWuH9crp005GfoUUh/6w9/FWGBwEHg3lxK1iHRN1MFlA==} + '@rollup/rollup-linux-arm-musleabihf@4.36.0': + resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.9': - resolution: {integrity: sha512-6TZjPHjKZUQKmVKMUowF3ewHxctrRR09eYyvT5eFv8w/fXarEra83A2mHTVJLA5xU91aCNOUnM+DWFMSbQ0Nxw==} + '@rollup/rollup-linux-arm64-gnu@4.36.0': + resolution: {integrity: sha512-KqjYVh3oM1bj//5X7k79PSCZ6CvaVzb7Qs7VMWS+SlWB5M8p3FqufLP9VNp4CazJ0CsPDLwVD9r3vX7Ci4J56A==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-arm64-musl@4.34.9': - resolution: {integrity: sha512-LD2fytxZJZ6xzOKnMbIpgzFOuIKlxVOpiMAXawsAZ2mHBPEYOnLRK5TTEsID6z4eM23DuO88X0Tq1mErHMVq0A==} + '@rollup/rollup-linux-arm64-musl@4.36.0': + resolution: {integrity: sha512-QiGnhScND+mAAtfHqeT+cB1S9yFnNQ/EwCg5yE3MzoaZZnIV0RV9O5alJAoJKX/sBONVKeZdMfO8QSaWEygMhw==} cpu: [arm64] os: [linux] - '@rollup/rollup-linux-loongarch64-gnu@4.34.9': - resolution: {integrity: sha512-dRAgTfDsn0TE0HI6cmo13hemKpVHOEyeciGtvlBTkpx/F65kTvShtY/EVyZEIfxFkV5JJTuQ9tP5HGBS0hfxIg==} + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': + resolution: {integrity: sha512-1ZPyEDWF8phd4FQtTzMh8FQwqzvIjLsl6/84gzUxnMNFBtExBtpL51H67mV9xipuxl1AEAerRBgBwFNpkw8+Lg==} cpu: [loong64] os: [linux] - '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': - resolution: {integrity: sha512-PHcNOAEhkoMSQtMf+rJofwisZqaU8iQ8EaSps58f5HYll9EAY5BSErCZ8qBDMVbq88h4UxaNPlbrKqfWP8RfJA==} + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': + resolution: {integrity: sha512-VMPMEIUpPFKpPI9GZMhJrtu8rxnp6mJR3ZzQPykq4xc2GmdHj3Q4cA+7avMyegXy4n1v+Qynr9fR88BmyO74tg==} cpu: [ppc64] os: [linux] - '@rollup/rollup-linux-riscv64-gnu@4.34.9': - resolution: {integrity: sha512-Z2i0Uy5G96KBYKjeQFKbbsB54xFOL5/y1P5wNBsbXB8yE+At3oh0DVMjQVzCJRJSfReiB2tX8T6HUFZ2k8iaKg==} + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.9': - resolution: {integrity: sha512-U+5SwTMoeYXoDzJX5dhDTxRltSrIax8KWwfaaYcynuJw8mT33W7oOgz0a+AaXtGuvhzTr2tVKh5UO8GVANTxyQ==} + '@rollup/rollup-linux-s390x-gnu@4.36.0': + resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.9': - resolution: {integrity: sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==} + '@rollup/rollup-linux-x64-gnu@4.36.0': + resolution: {integrity: sha512-5KtoW8UWmwFKQ96aQL3LlRXX16IMwyzMq/jSSVIIyAANiE1doaQsx/KRyhAvpHlPjPiSU/AYX/8m+lQ9VToxFQ==} cpu: [x64] os: [linux] - '@rollup/rollup-linux-x64-musl@4.34.9': - resolution: {integrity: sha512-cYRpV4650z2I3/s6+5/LONkjIz8MBeqrk+vPXV10ORBnshpn8S32bPqQ2Utv39jCiDcO2eJTuSlPXpnvmaIgRA==} + '@rollup/rollup-linux-x64-musl@4.36.0': + resolution: {integrity: sha512-sycrYZPrv2ag4OCvaN5js+f01eoZ2U+RmT5as8vhxiFz+kxwlHrsxOwKPSA8WyS+Wc6Epid9QeI/IkQ9NkgYyQ==} cpu: [x64] os: [linux] - '@rollup/rollup-win32-arm64-msvc@4.34.9': - resolution: {integrity: sha512-z4mQK9dAN6byRA/vsSgQiPeuO63wdiDxZ9yg9iyX2QTzKuQM7T4xlBoeUP/J8uiFkqxkcWndWi+W7bXdPbt27Q==} + '@rollup/rollup-win32-arm64-msvc@4.36.0': + resolution: {integrity: sha512-qbqt4N7tokFwwSVlWDsjfoHgviS3n/vZ8LK0h1uLG9TYIRuUTJC88E1xb3LM2iqZ/WTqNQjYrtmtGmrmmawB6A==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.34.9': - resolution: {integrity: sha512-KB48mPtaoHy1AwDNkAJfHXvHp24H0ryZog28spEs0V48l3H1fr4i37tiyHsgKZJnCmvxsbATdZGBpbmxTE3a9w==} + '@rollup/rollup-win32-ia32-msvc@4.36.0': + resolution: {integrity: sha512-t+RY0JuRamIocMuQcfwYSOkmdX9dtkr1PbhKW42AMvaDQa+jOdpUYysroTF/nuPpAaQMWp7ye+ndlmmthieJrQ==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.34.9': - resolution: {integrity: sha512-AyleYRPU7+rgkMWbEh71fQlrzRfeP6SyMnRf9XX4fCdDPAJumdSBqYEcWPMzVQ4ScAl7E4oFfK0GUVn77xSwbw==} + '@rollup/rollup-win32-x64-msvc@4.36.0': + resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} cpu: [x64] os: [win32] @@ -453,61 +457,61 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@22.13.9': - resolution: {integrity: sha512-acBjXdRJ3A6Pb3tqnw9HZmyR3Fiol3aGxRCK1x3d+6CDAMjl7I649wpSd+yNURCjbOUGu9tqtLKnTGxmK6CyGw==} + '@types/node@22.13.10': + resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} - '@typescript-eslint/eslint-plugin@8.26.0': - resolution: {integrity: sha512-cLr1J6pe56zjKYajK6SSSre6nl1Gj6xDp1TY0trpgPzjVbgDwd09v2Ws37LABxzkicmUjhEeg/fAUjPJJB1v5Q==} + '@typescript-eslint/eslint-plugin@8.26.1': + resolution: {integrity: sha512-2X3mwqsj9Bd3Ciz508ZUtoQQYpOhU/kWoUqIf49H8Z0+Vbh6UF/y0OEYp0Q0axOGzaBGs7QxRwq0knSQ8khQNA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.0.0 || ^8.0.0-alpha.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/parser@8.26.0': - resolution: {integrity: sha512-mNtXP9LTVBy14ZF3o7JG69gRPBK/2QWtQd0j0oH26HcY/foyJJau6pNUez7QrM5UHnSvwlQcJXKsk0I99B9pOA==} + '@typescript-eslint/parser@8.26.1': + resolution: {integrity: sha512-w6HZUV4NWxqd8BdeFf81t07d7/YV9s7TCWrQQbG5uhuvGUAW+fq1usZ1Hmz9UPNLniFnD8GLSsDpjP0hm1S4lQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/scope-manager@8.26.0': - resolution: {integrity: sha512-E0ntLvsfPqnPwng8b8y4OGuzh/iIOm2z8U3S9zic2TeMLW61u5IH2Q1wu0oSTkfrSzwbDJIB/Lm8O3//8BWMPA==} + '@typescript-eslint/scope-manager@8.26.1': + resolution: {integrity: sha512-6EIvbE5cNER8sqBu6V7+KeMZIC1664d2Yjt+B9EWUXrsyWpxx4lEZrmvxgSKRC6gX+efDL/UY9OpPZ267io3mg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/type-utils@8.26.0': - resolution: {integrity: sha512-ruk0RNChLKz3zKGn2LwXuVoeBcUMh+jaqzN461uMMdxy5H9epZqIBtYj7UiPXRuOpaALXGbmRuZQhmwHhaS04Q==} + '@typescript-eslint/type-utils@8.26.1': + resolution: {integrity: sha512-Kcj/TagJLwoY/5w9JGEFV0dclQdyqw9+VMndxOJKtoFSjfZhLXhYjzsQEeyza03rwHx2vFEGvrJWJBXKleRvZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/types@8.26.0': - resolution: {integrity: sha512-89B1eP3tnpr9A8L6PZlSjBvnJhWXtYfZhECqlBl1D9Lme9mHO6iWlsprBtVenQvY1HMhax1mWOjhtL3fh/u+pA==} + '@typescript-eslint/types@8.26.1': + resolution: {integrity: sha512-n4THUQW27VmQMx+3P+B0Yptl7ydfceUj4ON/AQILAASwgYdZ/2dhfymRMh5egRUrvK5lSmaOm77Ry+lmXPOgBQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.26.0': - resolution: {integrity: sha512-tiJ1Hvy/V/oMVRTbEOIeemA2XoylimlDQ03CgPPNaHYZbpsc78Hmngnt+WXZfJX1pjQ711V7g0H7cSJThGYfPQ==} + '@typescript-eslint/typescript-estree@8.26.1': + resolution: {integrity: sha512-yUwPpUHDgdrv1QJ7YQal3cMVBGWfnuCdKbXw1yyjArax3353rEJP1ZA+4F8nOlQ3RfS2hUN/wze3nlY+ZOhvoA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/utils@8.26.0': - resolution: {integrity: sha512-2L2tU3FVwhvU14LndnQCA2frYC8JnPDVKyQtWFPf8IYFMt/ykEN1bPolNhNbCVgOmdzTlWdusCTKA/9nKrf8Ig==} + '@typescript-eslint/utils@8.26.1': + resolution: {integrity: sha512-V4Urxa/XtSUroUrnI7q6yUTD3hDtfJ2jzVfeT3VK0ciizfK2q/zGC0iDh1lFMUZR8cImRrep6/q0xd/1ZGPQpg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <5.9.0' - '@typescript-eslint/visitor-keys@8.26.0': - resolution: {integrity: sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg==} + '@typescript-eslint/visitor-keys@8.26.1': + resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@vitest/expect@3.0.8': - resolution: {integrity: sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ==} + '@vitest/expect@3.0.9': + resolution: {integrity: sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==} - '@vitest/mocker@3.0.8': - resolution: {integrity: sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow==} + '@vitest/mocker@3.0.9': + resolution: {integrity: sha512-ryERPIBOnvevAkTq+L1lD+DTFBRcjueL9lOUfXsLfwP92h4e+Heb+PjiqS3/OURWPtywfafK0kj++yDFjWUmrA==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 @@ -517,20 +521,20 @@ packages: vite: optional: true - '@vitest/pretty-format@3.0.8': - resolution: {integrity: sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg==} + '@vitest/pretty-format@3.0.9': + resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} - '@vitest/runner@3.0.8': - resolution: {integrity: sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w==} + '@vitest/runner@3.0.9': + resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} - '@vitest/snapshot@3.0.8': - resolution: {integrity: sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A==} + '@vitest/snapshot@3.0.9': + resolution: {integrity: sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==} - '@vitest/spy@3.0.8': - resolution: {integrity: sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q==} + '@vitest/spy@3.0.9': + resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} - '@vitest/utils@3.0.8': - resolution: {integrity: sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q==} + '@vitest/utils@3.0.9': + resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} @@ -583,8 +587,8 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} - array.prototype.findlastindex@1.2.5: - resolution: {integrity: sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==} + array.prototype.findlastindex@1.2.6: + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: @@ -785,8 +789,8 @@ packages: resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} engines: {node: '>= 0.4'} - esbuild@0.25.0: - resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==} + esbuild@0.25.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} hasBin: true @@ -794,8 +798,8 @@ packages: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-config-prettier@10.0.2: - resolution: {integrity: sha512-1105/17ZIMjmCOJOPNfVdbXafLCLj3hPmkmB7dLgt7XsQ/zkxSuDerE/xgO3RxoHysR1N1whmquY0lSn2O0VLg==} + eslint-config-prettier@10.1.1: + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -854,8 +858,8 @@ packages: peerDependencies: eslint: ^7.0.0 || ^8.0.0 || ^9.0.0 - eslint-scope@8.2.0: - resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==} + eslint-scope@8.3.0: + resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: @@ -866,8 +870,8 @@ packages: resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.21.0: - resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==} + eslint@9.22.0: + resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1299,8 +1303,8 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.8: - resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} + nanoid@3.3.10: + resolution: {integrity: sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -1498,8 +1502,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.34.9: - resolution: {integrity: sha512-nF5XYqWWp9hx/LrpC8sZvvvmq0TeTjQgaZHYmAgwysT9nh8sWnZhBnM8ZyVbbJFIQBLwHDNoMqsBZBbUo4U8sQ==} + rollup@4.36.0: + resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1713,8 +1717,8 @@ packages: resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} engines: {node: '>= 0.4'} - typescript-eslint@8.26.0: - resolution: {integrity: sha512-PtVz9nAnuNJuAVeUFvwztjuUgSnJInODAUx47VDwWPXzd5vismPOtPtt83tzNXyOjVQbPRp786D6WFW/M2koIA==} + typescript-eslint@8.26.1: + resolution: {integrity: sha512-t/oIs9mYyrwZGRpDv3g+3K6nZ5uhKEMt2oNmAPwaY4/ye0+EH4nXIPYNtkYFS6QHm+1DFg34DbglYBz5P9Xysg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -1739,8 +1743,8 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@3.0.8: - resolution: {integrity: sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg==} + vite-node@3.0.9: + resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true @@ -1792,16 +1796,16 @@ packages: yaml: optional: true - vitest@3.0.8: - resolution: {integrity: sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==} + vitest@3.0.9: + resolution: {integrity: sha512-BbcFDqNyBlfSpATmTtXOAOj71RNKDDvjBM/uPfnxxVGrG+FSH2RQIwgeEngTaTkuU/h0ScFvf+tRcKfYXzBybQ==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@types/debug': ^4.1.12 '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.0.8 - '@vitest/ui': 3.0.8 + '@vitest/browser': 3.0.9 + '@vitest/ui': 3.0.9 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1832,8 +1836,8 @@ packages: resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} engines: {node: '>= 0.4'} - which-typed-array@1.1.18: - resolution: {integrity: sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==} + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} engines: {node: '>= 0.4'} which@2.0.2: @@ -1864,22 +1868,22 @@ packages: snapshots: - '@babel/runtime@7.26.9': + '@babel/runtime@7.26.10': dependencies: regenerator-runtime: 0.14.1 - '@bengsfort/eslint-config-flat@0.2.4(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': + '@bengsfort/eslint-config-flat@0.2.4(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@eslint/js': 9.21.0 - '@stylistic/eslint-plugin': 4.2.0(eslint@9.21.0)(typescript@5.8.2) - eslint: 9.21.0 - eslint-config-prettier: 10.0.2(eslint@9.21.0) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0) - eslint-plugin-prettier: 5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.3) - eslint-plugin-promise: 7.2.1(eslint@9.21.0) + '@eslint/js': 9.22.0 + '@stylistic/eslint-plugin': 4.2.0(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 + eslint-config-prettier: 10.1.1(eslint@9.22.0) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0) + eslint-plugin-prettier: 5.2.3(eslint-config-prettier@10.1.1(eslint@9.22.0))(eslint@9.22.0)(prettier@3.5.3) + eslint-plugin-promise: 7.2.1(eslint@9.22.0) prettier: 3.5.3 typescript: 5.8.2 - typescript-eslint: 8.26.0(eslint@9.21.0)(typescript@5.8.2) + typescript-eslint: 8.26.1(eslint@9.22.0)(typescript@5.8.2) transitivePeerDependencies: - '@types/eslint' - '@typescript-eslint/parser' @@ -2029,84 +2033,84 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@esbuild/aix-ppc64@0.25.0': + '@esbuild/aix-ppc64@0.25.1': optional: true - '@esbuild/android-arm64@0.25.0': + '@esbuild/android-arm64@0.25.1': optional: true - '@esbuild/android-arm@0.25.0': + '@esbuild/android-arm@0.25.1': optional: true - '@esbuild/android-x64@0.25.0': + '@esbuild/android-x64@0.25.1': optional: true - '@esbuild/darwin-arm64@0.25.0': + '@esbuild/darwin-arm64@0.25.1': optional: true - '@esbuild/darwin-x64@0.25.0': + '@esbuild/darwin-x64@0.25.1': optional: true - '@esbuild/freebsd-arm64@0.25.0': + '@esbuild/freebsd-arm64@0.25.1': optional: true - '@esbuild/freebsd-x64@0.25.0': + '@esbuild/freebsd-x64@0.25.1': optional: true - '@esbuild/linux-arm64@0.25.0': + '@esbuild/linux-arm64@0.25.1': optional: true - '@esbuild/linux-arm@0.25.0': + '@esbuild/linux-arm@0.25.1': optional: true - '@esbuild/linux-ia32@0.25.0': + '@esbuild/linux-ia32@0.25.1': optional: true - '@esbuild/linux-loong64@0.25.0': + '@esbuild/linux-loong64@0.25.1': optional: true - '@esbuild/linux-mips64el@0.25.0': + '@esbuild/linux-mips64el@0.25.1': optional: true - '@esbuild/linux-ppc64@0.25.0': + '@esbuild/linux-ppc64@0.25.1': optional: true - '@esbuild/linux-riscv64@0.25.0': + '@esbuild/linux-riscv64@0.25.1': optional: true - '@esbuild/linux-s390x@0.25.0': + '@esbuild/linux-s390x@0.25.1': optional: true - '@esbuild/linux-x64@0.25.0': + '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.25.0': + '@esbuild/netbsd-arm64@0.25.1': optional: true - '@esbuild/netbsd-x64@0.25.0': + '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.25.0': + '@esbuild/openbsd-arm64@0.25.1': optional: true - '@esbuild/openbsd-x64@0.25.0': + '@esbuild/openbsd-x64@0.25.1': optional: true - '@esbuild/sunos-x64@0.25.0': + '@esbuild/sunos-x64@0.25.1': optional: true - '@esbuild/win32-arm64@0.25.0': + '@esbuild/win32-arm64@0.25.1': optional: true - '@esbuild/win32-ia32@0.25.0': + '@esbuild/win32-ia32@0.25.1': optional: true - '@esbuild/win32-x64@0.25.0': + '@esbuild/win32-x64@0.25.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0)': + '@eslint-community/eslint-utils@4.5.1(eslint@9.22.0)': dependencies: - eslint: 9.21.0 + eslint: 9.22.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2119,6 +2123,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@eslint/config-helpers@0.1.0': {} + '@eslint/core@0.12.0': dependencies: '@types/json-schema': 7.0.15 @@ -2137,7 +2143,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.21.0': {} + '@eslint/js@9.22.0': {} '@eslint/object-schema@2.1.6': {} @@ -2172,14 +2178,14 @@ snapshots: '@manypkg/find-root@1.1.0': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.26.10 '@types/node': 12.20.55 find-up: 4.1.0 fs-extra: 8.1.0 '@manypkg/get-packages@1.1.3': dependencies: - '@babel/runtime': 7.26.9 + '@babel/runtime': 7.26.10 '@changesets/types': 4.1.0 '@manypkg/find-root': 1.1.0 fs-extra: 8.1.0 @@ -2200,69 +2206,69 @@ snapshots: '@pkgr/core@0.1.1': {} - '@rollup/rollup-android-arm-eabi@4.34.9': + '@rollup/rollup-android-arm-eabi@4.36.0': optional: true - '@rollup/rollup-android-arm64@4.34.9': + '@rollup/rollup-android-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-arm64@4.34.9': + '@rollup/rollup-darwin-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-x64@4.34.9': + '@rollup/rollup-darwin-x64@4.36.0': optional: true - '@rollup/rollup-freebsd-arm64@4.34.9': + '@rollup/rollup-freebsd-arm64@4.36.0': optional: true - '@rollup/rollup-freebsd-x64@4.34.9': + '@rollup/rollup-freebsd-x64@4.36.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.9': + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.9': + '@rollup/rollup-linux-arm-musleabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.9': + '@rollup/rollup-linux-arm64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.9': + '@rollup/rollup-linux-arm64-musl@4.36.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.9': + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.9': + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.9': + '@rollup/rollup-linux-riscv64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.9': + '@rollup/rollup-linux-s390x-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.9': + '@rollup/rollup-linux-x64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-musl@4.34.9': + '@rollup/rollup-linux-x64-musl@4.36.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.9': + '@rollup/rollup-win32-arm64-msvc@4.36.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.9': + '@rollup/rollup-win32-ia32-msvc@4.36.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.9': + '@rollup/rollup-win32-x64-msvc@4.36.0': optional: true '@rtsao/scc@1.1.0': {} - '@stylistic/eslint-plugin@4.2.0(eslint@9.21.0)(typescript@5.8.2)': + '@stylistic/eslint-plugin@4.2.0(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - eslint: 9.21.0 + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 estraverse: 5.3.0 @@ -2279,19 +2285,19 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.13.9': + '@types/node@22.13.10': dependencies: undici-types: 6.20.0 - '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/type-utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.26.0 - eslint: 9.21.0 + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/scope-manager': 8.26.1 + '@typescript-eslint/type-utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.1 + eslint: 9.22.0 graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -2300,40 +2306,40 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) - '@typescript-eslint/visitor-keys': 8.26.0 + '@typescript-eslint/scope-manager': 8.26.1 + '@typescript-eslint/types': 8.26.1 + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) + '@typescript-eslint/visitor-keys': 8.26.1 debug: 4.4.0 - eslint: 9.21.0 + eslint: 9.22.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.26.0': + '@typescript-eslint/scope-manager@8.26.1': dependencies: - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/visitor-keys': 8.26.0 + '@typescript-eslint/types': 8.26.1 + '@typescript-eslint/visitor-keys': 8.26.1 - '@typescript-eslint/type-utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.26.1(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) debug: 4.4.0 - eslint: 9.21.0 + eslint: 9.22.0 ts-api-utils: 2.0.1(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.26.0': {} + '@typescript-eslint/types@8.26.1': {} - '@typescript-eslint/typescript-estree@8.26.0(typescript@5.8.2)': + '@typescript-eslint/typescript-estree@8.26.1(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/visitor-keys': 8.26.0 + '@typescript-eslint/types': 8.26.1 + '@typescript-eslint/visitor-keys': 8.26.1 debug: 4.4.0 fast-glob: 3.3.3 is-glob: 4.0.3 @@ -2344,59 +2350,59 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)': + '@typescript-eslint/utils@8.26.1(eslint@9.22.0)(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) - '@typescript-eslint/scope-manager': 8.26.0 - '@typescript-eslint/types': 8.26.0 - '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2) - eslint: 9.21.0 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) + '@typescript-eslint/scope-manager': 8.26.1 + '@typescript-eslint/types': 8.26.1 + '@typescript-eslint/typescript-estree': 8.26.1(typescript@5.8.2) + eslint: 9.22.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.26.0': + '@typescript-eslint/visitor-keys@8.26.1': dependencies: - '@typescript-eslint/types': 8.26.0 + '@typescript-eslint/types': 8.26.1 eslint-visitor-keys: 4.2.0 - '@vitest/expect@3.0.8': + '@vitest/expect@3.0.9': dependencies: - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.0.8(vite@6.2.2(@types/node@22.13.9))': + '@vitest/mocker@3.0.9(vite@6.2.2(@types/node@22.13.10))': dependencies: - '@vitest/spy': 3.0.8 + '@vitest/spy': 3.0.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.2.2(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.10) - '@vitest/pretty-format@3.0.8': + '@vitest/pretty-format@3.0.9': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.0.8': + '@vitest/runner@3.0.9': dependencies: - '@vitest/utils': 3.0.8 + '@vitest/utils': 3.0.9 pathe: 2.0.3 - '@vitest/snapshot@3.0.8': + '@vitest/snapshot@3.0.9': dependencies: - '@vitest/pretty-format': 3.0.8 + '@vitest/pretty-format': 3.0.9 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.0.8': + '@vitest/spy@3.0.9': dependencies: tinyspy: 3.0.2 - '@vitest/utils@3.0.8': + '@vitest/utils@3.0.9': dependencies: - '@vitest/pretty-format': 3.0.8 + '@vitest/pretty-format': 3.0.9 loupe: 3.1.3 tinyrainbow: 2.0.0 @@ -2447,9 +2453,10 @@ snapshots: array-union@2.1.0: {} - array.prototype.findlastindex@1.2.5: + array.prototype.findlastindex@1.2.6: dependencies: call-bind: 1.0.8 + call-bound: 1.0.4 define-properties: 1.2.1 es-abstract: 1.23.9 es-errors: 1.3.0 @@ -2682,7 +2689,7 @@ snapshots: typed-array-byte-offset: 1.0.4 typed-array-length: 1.0.7 unbox-primitive: 1.1.0 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 es-define-property@1.0.1: {} @@ -2711,39 +2718,39 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.25.0: + esbuild@0.25.1: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.0 - '@esbuild/android-arm': 0.25.0 - '@esbuild/android-arm64': 0.25.0 - '@esbuild/android-x64': 0.25.0 - '@esbuild/darwin-arm64': 0.25.0 - '@esbuild/darwin-x64': 0.25.0 - '@esbuild/freebsd-arm64': 0.25.0 - '@esbuild/freebsd-x64': 0.25.0 - '@esbuild/linux-arm': 0.25.0 - '@esbuild/linux-arm64': 0.25.0 - '@esbuild/linux-ia32': 0.25.0 - '@esbuild/linux-loong64': 0.25.0 - '@esbuild/linux-mips64el': 0.25.0 - '@esbuild/linux-ppc64': 0.25.0 - '@esbuild/linux-riscv64': 0.25.0 - '@esbuild/linux-s390x': 0.25.0 - '@esbuild/linux-x64': 0.25.0 - '@esbuild/netbsd-arm64': 0.25.0 - '@esbuild/netbsd-x64': 0.25.0 - '@esbuild/openbsd-arm64': 0.25.0 - '@esbuild/openbsd-x64': 0.25.0 - '@esbuild/sunos-x64': 0.25.0 - '@esbuild/win32-arm64': 0.25.0 - '@esbuild/win32-ia32': 0.25.0 - '@esbuild/win32-x64': 0.25.0 + '@esbuild/aix-ppc64': 0.25.1 + '@esbuild/android-arm': 0.25.1 + '@esbuild/android-arm64': 0.25.1 + '@esbuild/android-x64': 0.25.1 + '@esbuild/darwin-arm64': 0.25.1 + '@esbuild/darwin-x64': 0.25.1 + '@esbuild/freebsd-arm64': 0.25.1 + '@esbuild/freebsd-x64': 0.25.1 + '@esbuild/linux-arm': 0.25.1 + '@esbuild/linux-arm64': 0.25.1 + '@esbuild/linux-ia32': 0.25.1 + '@esbuild/linux-loong64': 0.25.1 + '@esbuild/linux-mips64el': 0.25.1 + '@esbuild/linux-ppc64': 0.25.1 + '@esbuild/linux-riscv64': 0.25.1 + '@esbuild/linux-s390x': 0.25.1 + '@esbuild/linux-x64': 0.25.1 + '@esbuild/netbsd-arm64': 0.25.1 + '@esbuild/netbsd-x64': 0.25.1 + '@esbuild/openbsd-arm64': 0.25.1 + '@esbuild/openbsd-x64': 0.25.1 + '@esbuild/sunos-x64': 0.25.1 + '@esbuild/win32-arm64': 0.25.1 + '@esbuild/win32-ia32': 0.25.1 + '@esbuild/win32-x64': 0.25.1 escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.0.2(eslint@9.21.0): + eslint-config-prettier@10.1.1(eslint@9.22.0): dependencies: - eslint: 9.21.0 + eslint: 9.22.0 eslint-import-resolver-node@0.3.9: dependencies: @@ -2753,28 +2760,28 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - eslint: 9.21.0 + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 - array.prototype.findlastindex: 1.2.5 + array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.21.0 + eslint: 9.22.0 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -2786,27 +2793,27 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.2.3(eslint-config-prettier@10.0.2(eslint@9.21.0))(eslint@9.21.0)(prettier@3.5.3): + eslint-plugin-prettier@5.2.3(eslint-config-prettier@10.1.1(eslint@9.22.0))(eslint@9.22.0)(prettier@3.5.3): dependencies: - eslint: 9.21.0 + eslint: 9.22.0 prettier: 3.5.3 prettier-linter-helpers: 1.0.0 synckit: 0.9.2 optionalDependencies: - eslint-config-prettier: 10.0.2(eslint@9.21.0) + eslint-config-prettier: 10.1.1(eslint@9.22.0) - eslint-plugin-promise@7.2.1(eslint@9.21.0): + eslint-plugin-promise@7.2.1(eslint@9.22.0): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) - eslint: 9.21.0 + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) + eslint: 9.22.0 - eslint-scope@8.2.0: + eslint-scope@8.3.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 @@ -2815,14 +2822,15 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.21.0: + eslint@9.22.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.0) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.19.2 + '@eslint/config-helpers': 0.1.0 '@eslint/core': 0.12.0 '@eslint/eslintrc': 3.3.0 - '@eslint/js': 9.21.0 + '@eslint/js': 9.22.0 '@eslint/plugin-kit': 0.2.7 '@humanfs/node': 0.16.6 '@humanwhocodes/module-importer': 1.0.1 @@ -2834,7 +2842,7 @@ snapshots: cross-spawn: 7.0.6 debug: 4.4.0 escape-string-regexp: 4.0.0 - eslint-scope: 8.2.0 + eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 espree: 10.3.0 esquery: 1.6.0 @@ -3176,7 +3184,7 @@ snapshots: is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} @@ -3280,7 +3288,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.8: {} + nanoid@3.3.10: {} natural-compare@1.4.0: {} @@ -3399,7 +3407,7 @@ snapshots: postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.10 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -3465,29 +3473,29 @@ snapshots: glob: 11.0.1 package-json-from-dist: 1.0.1 - rollup@4.34.9: + rollup@4.36.0: dependencies: '@types/estree': 1.0.6 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.34.9 - '@rollup/rollup-android-arm64': 4.34.9 - '@rollup/rollup-darwin-arm64': 4.34.9 - '@rollup/rollup-darwin-x64': 4.34.9 - '@rollup/rollup-freebsd-arm64': 4.34.9 - '@rollup/rollup-freebsd-x64': 4.34.9 - '@rollup/rollup-linux-arm-gnueabihf': 4.34.9 - '@rollup/rollup-linux-arm-musleabihf': 4.34.9 - '@rollup/rollup-linux-arm64-gnu': 4.34.9 - '@rollup/rollup-linux-arm64-musl': 4.34.9 - '@rollup/rollup-linux-loongarch64-gnu': 4.34.9 - '@rollup/rollup-linux-powerpc64le-gnu': 4.34.9 - '@rollup/rollup-linux-riscv64-gnu': 4.34.9 - '@rollup/rollup-linux-s390x-gnu': 4.34.9 - '@rollup/rollup-linux-x64-gnu': 4.34.9 - '@rollup/rollup-linux-x64-musl': 4.34.9 - '@rollup/rollup-win32-arm64-msvc': 4.34.9 - '@rollup/rollup-win32-ia32-msvc': 4.34.9 - '@rollup/rollup-win32-x64-msvc': 4.34.9 + '@rollup/rollup-android-arm-eabi': 4.36.0 + '@rollup/rollup-android-arm64': 4.36.0 + '@rollup/rollup-darwin-arm64': 4.36.0 + '@rollup/rollup-darwin-x64': 4.36.0 + '@rollup/rollup-freebsd-arm64': 4.36.0 + '@rollup/rollup-freebsd-x64': 4.36.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.36.0 + '@rollup/rollup-linux-arm-musleabihf': 4.36.0 + '@rollup/rollup-linux-arm64-gnu': 4.36.0 + '@rollup/rollup-linux-arm64-musl': 4.36.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.36.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.36.0 + '@rollup/rollup-linux-riscv64-gnu': 4.36.0 + '@rollup/rollup-linux-s390x-gnu': 4.36.0 + '@rollup/rollup-linux-x64-gnu': 4.36.0 + '@rollup/rollup-linux-x64-musl': 4.36.0 + '@rollup/rollup-win32-arm64-msvc': 4.36.0 + '@rollup/rollup-win32-ia32-msvc': 4.36.0 + '@rollup/rollup-win32-x64-msvc': 4.36.0 fsevents: 2.3.3 run-parallel@1.2.0: @@ -3726,12 +3734,12 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.26.0(eslint@9.21.0)(typescript@5.8.2): + typescript-eslint@8.26.1(eslint@9.22.0)(typescript@5.8.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2) - eslint: 9.21.0 + '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.26.1(eslint@9.22.0)(typescript@5.8.2) + eslint: 9.22.0 typescript: 5.8.2 transitivePeerDependencies: - supports-color @@ -3753,13 +3761,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.0.8(@types/node@22.13.9): + vite-node@3.0.9(@types/node@22.13.10): dependencies: cac: 6.7.14 debug: 4.4.0 es-module-lexer: 1.6.0 pathe: 2.0.3 - vite: 6.2.2(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.10) transitivePeerDependencies: - '@types/node' - jiti @@ -3774,35 +3782,35 @@ snapshots: - tsx - yaml - vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.9)): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)): dependencies: debug: 4.4.0 globrex: 0.1.2 tsconfck: 3.1.5(typescript@5.8.2) optionalDependencies: - vite: 6.2.2(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.10) transitivePeerDependencies: - supports-color - typescript - vite@6.2.2(@types/node@22.13.9): + vite@6.2.2(@types/node@22.13.10): dependencies: - esbuild: 0.25.0 + esbuild: 0.25.1 postcss: 8.5.3 - rollup: 4.34.9 + rollup: 4.36.0 optionalDependencies: - '@types/node': 22.13.9 + '@types/node': 22.13.10 fsevents: 2.3.3 - vitest@3.0.8(@types/node@22.13.9): + vitest@3.0.9(@types/node@22.13.10): dependencies: - '@vitest/expect': 3.0.8 - '@vitest/mocker': 3.0.8(vite@6.2.2(@types/node@22.13.9)) - '@vitest/pretty-format': 3.0.8 - '@vitest/runner': 3.0.8 - '@vitest/snapshot': 3.0.8 - '@vitest/spy': 3.0.8 - '@vitest/utils': 3.0.8 + '@vitest/expect': 3.0.9 + '@vitest/mocker': 3.0.9(vite@6.2.2(@types/node@22.13.10)) + '@vitest/pretty-format': 3.0.9 + '@vitest/runner': 3.0.9 + '@vitest/snapshot': 3.0.9 + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 chai: 5.2.0 debug: 4.4.0 expect-type: 1.2.0 @@ -3813,11 +3821,11 @@ snapshots: tinyexec: 0.3.2 tinypool: 1.0.2 tinyrainbow: 2.0.0 - vite: 6.2.2(@types/node@22.13.9) - vite-node: 3.0.8(@types/node@22.13.9) + vite: 6.2.2(@types/node@22.13.10) + vite-node: 3.0.9(@types/node@22.13.10) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.13.9 + '@types/node': 22.13.10 transitivePeerDependencies: - jiti - less @@ -3854,7 +3862,7 @@ snapshots: isarray: 2.0.5 which-boxed-primitive: 1.1.1 which-collection: 1.0.2 - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 which-collection@1.0.2: dependencies: @@ -3863,12 +3871,13 @@ snapshots: is-weakmap: 2.0.2 is-weakset: 2.0.4 - which-typed-array@1.1.18: + which-typed-array@1.1.19: dependencies: available-typed-arrays: 1.0.7 call-bind: 1.0.8 call-bound: 1.0.4 for-each: 0.3.5 + get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 From 9c62890a11631a3afb3e099b47e1fc4c15c9d3b8 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Tue, 18 Mar 2025 09:31:51 +0200 Subject: [PATCH 19/31] Expose event dispatcher, add documentation. --- .changeset/little-ghosts-give.md | 5 ++ lib/events/__tests__/event-dispatcher.test.ts | 16 +++++- lib/events/event-dispatcher.ts | 56 +++++++++++++++++++ package.json | 6 +- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .changeset/little-ghosts-give.md diff --git a/.changeset/little-ghosts-give.md b/.changeset/little-ghosts-give.md new file mode 100644 index 0000000..6a61f06 --- /dev/null +++ b/.changeset/little-ghosts-give.md @@ -0,0 +1,5 @@ +--- +'@bengsfort/stdlib': minor +--- + +Add type-safe EventDispatcher implementation. diff --git a/lib/events/__tests__/event-dispatcher.test.ts b/lib/events/__tests__/event-dispatcher.test.ts index fcbfe73..7ab19c7 100644 --- a/lib/events/__tests__/event-dispatcher.test.ts +++ b/lib/events/__tests__/event-dispatcher.test.ts @@ -36,7 +36,7 @@ describe('EventDispatcher', () => { dispatch.addListener('foo', () => {}); expect(dispatch.getEvents()).toContain('foo'); - const onGaz = () => {}; + const onGaz = (): void => {}; dispatch.addListener('foo', () => {}); dispatch.addListener('gaz', onGaz); const withGaz = dispatch.getEvents(); @@ -100,4 +100,18 @@ describe('EventDispatcher', () => { expect(dispatch.getListenerCount()).toEqual(0); }); + + it('should support removing a listener via the unsub callback', () => { + const dispatch = new EventDispatcher(); + const onFoo = vi.fn(); + + const unsub = dispatch.addListener('foo', onFoo); + expect(dispatch.getListenerCount()).toEqual(1); + dispatch.trigger('foo', 52, true); + + unsub(); + expect(dispatch.getListenerCount()).toEqual(0); + dispatch.trigger('foo', 30, false); + expect(onFoo).toHaveBeenCalledExactlyOnceWith(52, true); + }); }); diff --git a/lib/events/event-dispatcher.ts b/lib/events/event-dispatcher.ts index fa5a92c..d90e1f1 100644 --- a/lib/events/event-dispatcher.ts +++ b/lib/events/event-dispatcher.ts @@ -15,6 +15,24 @@ type LazyListenerMap = { type UnsubListener = () => void; +/** + * Event Dispatcher with support for full listener and payload type safety. + * + * Instantiate or type with an event map that contains an array of arguments for + * each key. The keys become the events and their associated array becomes the + * event payload. Event Maps should extend the `EventMap` type. + * + * @example + * ``` + * interface ExampleMap extends EventMap { + * foo: [num: number, bool: boolean]; + * } + * + * const dispatcher = new EventDispatcher(); + * dispatcher.addListener('foo', (num, bool) => {}); + * dispatcher.trigger('foo', 500, true); + * ``` + */ export class EventDispatcher { #_listeners: LazyListenerMap = {}; #_autoRemoveListeners: LazyListenerMap = {}; @@ -22,6 +40,17 @@ export class EventDispatcher { #_listenerCount = 0; #_events = new Set>(); + /** + * Adds a listener for the given event. If `once` is set to true, the listener + * will be removed automatically after the first time the listener is triggered. + * Returns a callback that can be used to remove the listener without needing + * to call `removeListener`. + * + * @param event The event name to listen for. + * @param listener The callback to trigger once the event triggers. + * @param once If the listener should be removed after first invocation. + * @returns A callback to remove the listener. + */ public addListener = EventType>( event: Event, listener: EventListener, @@ -63,6 +92,14 @@ export class EventDispatcher { return unsub; } + /** + * Removes a given listener from the event dispatcher. The listener provided + * **MUST BE THE SAME REFERENCE**, otherwise the listener will not be + * removed. + * + * @param event The event the listener was registered to. + * @param listener The listener that was registered. + */ public removeListener = EventType>( event: Event, listener: EventListener, @@ -78,6 +115,10 @@ export class EventDispatcher { this.#_removeEmptyEvent(event); } + /** + * Removes all listeners that have been registered on this event dispatcher, + * and resets all internal dispatcher state. + */ public removeAllListeners(): void { for (const event in this.#_listeners) { this.#_listeners[event]?.clear(); @@ -94,6 +135,13 @@ export class EventDispatcher { this.#_listenerCount = 0; } + /** + * Triggers an event dispatch, calling all registered listeners for the given + * event with the provided payload. + * + * @param event The event to trigger. + * @param ...payload The payload to pass to all listeners. + */ public trigger = EventType>( event: Event, ...payload: EventPayload @@ -109,10 +157,18 @@ export class EventDispatcher { }); } + /** + * Returns the current number of registered event listeners on this dispatcher. + * @returns The total registered event listeners. + */ public getListenerCount(): number { return this.#_listenerCount; } + /** + * Returns a list of the events that have listeners registered on this dispatcher. + * @returns The events with registered listeners. + */ public getEvents(): EventType[] { return [...this.#_events]; } diff --git a/package.json b/package.json index ee1da5c..b7108ba 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,10 @@ "types": "./dist/errors/*.d.ts", "default": "./dist/errors/*.js" }, + "./events/*": { + "types": "./dist/events/*.d.ts", + "default": "./dist/events/*.js" + }, "./math/*": { "types": "./dist/math/*.d.ts", "default": "./dist/math/*.js" @@ -64,4 +68,4 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.0.6" } -} \ No newline at end of file +} From 6d11690601941173b5b5e59ff6cc9efe47c2e29d Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 23 Mar 2025 14:26:08 +0200 Subject: [PATCH 20/31] Compositor test --- sandbox/src/2d/context.ts | 5 + sandbox/src/2d/drawables/aabb.ts | 2 +- sandbox/src/2d/drawables/circle.ts | 2 +- sandbox/src/2d/drawables/drawable.ts | 6 +- sandbox/src/2d/drawables/grid.ts | 2 +- sandbox/src/2d/drawables/point.ts | 2 +- sandbox/src/2d/drawables/ray.ts | 2 +- sandbox/src/2d/input/manager.ts | 19 ++- sandbox/src/2d/main.ts | 69 ++++------ sandbox/src/2d/renderer/compositor.ts | 145 ++++++++++++++++++++++ sandbox/src/2d/scene-objects/circle.ts | 29 +++++ sandbox/src/2d/scene-objects/grid.ts | 27 ++++ sandbox/src/2d/scenes/quad-tree-vis.ts | 0 sandbox/src/2d/scenes/ray-collisions.ts | 0 sandbox/src/2d/scenes/scene.ts | 15 +++ sandbox/src/2d/scenes/shape-collisions.ts | 0 sandbox/src/2d/scenes/shapes.ts | 80 ++++++++++++ sandbox/src/2d/scenes/spatial-map.ts | 0 18 files changed, 348 insertions(+), 57 deletions(-) create mode 100644 sandbox/src/2d/context.ts create mode 100644 sandbox/src/2d/renderer/compositor.ts create mode 100644 sandbox/src/2d/scene-objects/circle.ts create mode 100644 sandbox/src/2d/scene-objects/grid.ts create mode 100644 sandbox/src/2d/scenes/quad-tree-vis.ts create mode 100644 sandbox/src/2d/scenes/ray-collisions.ts create mode 100644 sandbox/src/2d/scenes/scene.ts create mode 100644 sandbox/src/2d/scenes/shape-collisions.ts create mode 100644 sandbox/src/2d/scenes/shapes.ts create mode 100644 sandbox/src/2d/scenes/spatial-map.ts diff --git a/sandbox/src/2d/context.ts b/sandbox/src/2d/context.ts new file mode 100644 index 0000000..84c5abf --- /dev/null +++ b/sandbox/src/2d/context.ts @@ -0,0 +1,5 @@ +import { Renderer2D } from './renderer/renderer'; + +export interface SandboxContext { + renderer: Renderer2D; +} diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts index f4b295b..a834180 100644 --- a/sandbox/src/2d/drawables/aabb.ts +++ b/sandbox/src/2d/drawables/aabb.ts @@ -4,7 +4,7 @@ import { Vector2 } from '@stdlib/math/vector2'; import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableAABB { - type: 'aabb'; + drawType: 'aabb'; aabb: IAABB2D; stroke?: string; fill: string; diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts index 4afe1b0..2b37a70 100644 --- a/sandbox/src/2d/drawables/circle.ts +++ b/sandbox/src/2d/drawables/circle.ts @@ -3,7 +3,7 @@ import { ICircle } from '@stdlib/geometry/primitives'; import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableCircle { - type: 'circle'; + drawType: 'circle'; circle: ICircle; stroke?: string; fill: string; diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts index c430749..72a82ad 100644 --- a/sandbox/src/2d/drawables/drawable.ts +++ b/sandbox/src/2d/drawables/drawable.ts @@ -6,8 +6,8 @@ import { drawGrid, IDrawableGrid } from './grid.js'; import { drawPoint, IDrawablePoint } from './point.js'; import { drawRay, IDrawableRay } from './ray.js'; -type DrawableMapExtractor = { - [T in Type as T['type']]: T; +type DrawableMapExtractor = { + [T in Type as T['drawType']]: T; }; type DrawableMap = DrawableMapExtractor< @@ -22,7 +22,7 @@ export function renderDrawable( settings: RenderSettings, drawable: Drawable, ): void { - switch (drawable.type) { + switch (drawable.drawType) { case 'aabb': drawAABB(ctx, settings, drawable); return; diff --git a/sandbox/src/2d/drawables/grid.ts b/sandbox/src/2d/drawables/grid.ts index 4ff7297..769542c 100644 --- a/sandbox/src/2d/drawables/grid.ts +++ b/sandbox/src/2d/drawables/grid.ts @@ -3,7 +3,7 @@ import { Vector2 } from '@stdlib/math/vector2'; import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableGrid { - type: 'grid'; + drawType: 'grid'; color: string; gridColor: string; range: Vector2; diff --git a/sandbox/src/2d/drawables/point.ts b/sandbox/src/2d/drawables/point.ts index c80952f..531aaee 100644 --- a/sandbox/src/2d/drawables/point.ts +++ b/sandbox/src/2d/drawables/point.ts @@ -3,7 +3,7 @@ import { Vector2 } from '@stdlib/math/vector2'; import { RenderSettings } from '../renderer/render-settings'; export interface IDrawablePoint { - type: 'point'; + drawType: 'point'; position: Vector2; color: string; } diff --git a/sandbox/src/2d/drawables/ray.ts b/sandbox/src/2d/drawables/ray.ts index 03f8666..5186446 100644 --- a/sandbox/src/2d/drawables/ray.ts +++ b/sandbox/src/2d/drawables/ray.ts @@ -4,7 +4,7 @@ import { Vector2 } from '@stdlib/math/vector2'; import { RenderSettings } from '../renderer/render-settings'; export interface IDrawableRay { - type: 'ray'; + drawType: 'ray'; ray: IRay2D; color: string; } diff --git a/sandbox/src/2d/input/manager.ts b/sandbox/src/2d/input/manager.ts index 45a9fcc..46f9456 100644 --- a/sandbox/src/2d/input/manager.ts +++ b/sandbox/src/2d/input/manager.ts @@ -67,6 +67,7 @@ export class InputManager { #_boolActions = new Map, boolean>(); #_rangeActions = new Map, number>(); #_vecRangeActions = new Map, Vector2>(); + #_mousePos = new Vector2(); public clearActions(): void { this.#_actions = undefined; @@ -198,7 +199,14 @@ export class InputManager { vector.y += yModifier; this.#_vecRangeActions.set( actionName, - vector.clamp(action.range.min, action.range.max).normalize(), + vector + .clamp( + action.range.min.x, + action.range.min.y, + action.range.max.x, + action.range.max.y, + ) + .normalize(), ); break; @@ -241,7 +249,14 @@ export class InputManager { vector.y += yModifier; this.#_vecRangeActions.set( actionName, - vector.clamp(action.range.min, action.range.max).normalize(), + vector + .clamp( + action.range.min.x, + action.range.min.y, + action.range.max.x, + action.range.max.y, + ) + .normalize(), ); break; diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 5718ec5..6b539e1 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,60 +1,35 @@ -import { Vector2 } from '@stdlib/math/vector2.js'; - -import { Drawable } from './drawables/drawable.js'; import { Renderer2D } from './renderer/renderer.js'; +import { Scene } from './scenes/scene.js'; +import { createScene } from './scenes/shapes.js'; function main(): void { + let frameRef = 0; + const renderer = new Renderer2D(); renderer.attach(); - const shapes: Drawable[] = [ - { - type: 'grid', - color: '#fff', - gridColor: '#383838', - range: new Vector2(50, 50), - }, - { - type: 'aabb', - aabb: { - min: new Vector2(-4, -4), - max: new Vector2(4, 4), - }, - fill: 'blue', - }, - { - type: 'circle', - circle: { - position: new Vector2(8, 0), - radius: 3, - }, - fill: 'transparent', - stroke: '#f00', - }, - { - type: 'point', - position: new Vector2(6, 6), - color: '#f00', - }, - { - type: 'ray', - ray: { - position: new Vector2(-5, 5), - direction: new Vector2(1, 0), - }, - color: '#0f0', - }, - ]; - - const tick = (_now: number): void => { - requestAnimationFrame(tick); + const activeScene: Scene = createScene({ + renderer, + }); + + const tick = (now: number): void => { + frameRef = requestAnimationFrame(tick); // TODO: input - // TODO: logic update - renderer.render(shapes); + activeScene.tick(now); }; - tick(performance.now()); + frameRef = requestAnimationFrame(tick); + + window.addEventListener('blur', () => { + cancelAnimationFrame(frameRef); + console.log('Pausing loop.'); + }); + + window.addEventListener('focus', () => { + frameRef = requestAnimationFrame(tick); + console.log('Resuming loop.'); + }); } main(); diff --git a/sandbox/src/2d/renderer/compositor.ts b/sandbox/src/2d/renderer/compositor.ts new file mode 100644 index 0000000..444b669 --- /dev/null +++ b/sandbox/src/2d/renderer/compositor.ts @@ -0,0 +1,145 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { IDrawableAABB } from '../drawables/aabb'; +import { IDrawableCircle } from '../drawables/circle'; +import { Drawable, renderDrawable } from '../drawables/drawable'; +import { IDrawablePoint } from '../drawables/point'; +import { IDrawableRay } from '../drawables/ray'; + +import { defaultRenderSettings, RenderSettings } from './render-settings'; + +function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { + const { devicePixelRatio } = window; + + canvas.width = width * devicePixelRatio; + canvas.height = height * devicePixelRatio; + canvas.style.width = `${width.toString(10)}px`; + canvas.style.height = `${height.toString(10)}px`; + + const ctx = canvas.getContext('2d'); + ctx?.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0); + ctx?.clearRect(0, 0, width, height); +} + +function createCanvas( + width = window.innerWidth, + height = window.innerHeight, +): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + resizeCanvas(canvas, width, height); + return canvas; +} + +type UnbindCallback = () => void; +export function bindCanvasToWindow(canvas: HTMLCanvasElement): UnbindCallback { + const handler = (): void => { + resizeCanvas(canvas, window.innerWidth, window.innerHeight); + }; + + window.addEventListener('resize', handler); + return () => { + window.removeEventListener('resize', handler); + }; +} + +interface IDrawCommand { + data: ImageData; + position: Vector2; + zIndex: number; +} + +class Compositor2D { + public readonly bufferMaxHeight = 1000; + public readonly pixelsPerUnit = 16; + + #_nextResourceId = 0; + #_resourceIds = new Set(); + #_renderBuffer: CanvasRenderingContext2D; + #_drawBuffers = new Map(); + + #_aabbDraws = new Map(); + #_circleDraws = new Map(); + #_pointDraws = new Map(); + #_rayDraws = new Map(); + + constructor() { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('Missing 2D context'); + + this.#_renderBuffer = ctx; + } + + public createResource(): number { + const rid = this.#_nextResourceId++; + return rid; + } +} + +export class CompositeRenderer2D { + public readonly settings: RenderSettings; + + #_canvas: HTMLCanvasElement; + #_ctx: CanvasRenderingContext2D; + #_unbindCallback: UnbindCallback | null = null; + #_compositor: Compositor2D; + + constructor(settings: RenderSettings = defaultRenderSettings()) { + const canvas = createCanvas(); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + throw new Error('Missing rendering context'); + } + + this.settings = settings; + this.#_canvas = canvas; + this.#_ctx = ctx; + this.#_compositor = new Compositor2D(); + } + + public attach(): void { + if (this.#_unbindCallback !== null) { + console.warn('Attempting to attach already attached renderer'); + return; + } + + this.#_unbindCallback = bindCanvasToWindow(this.#_canvas); + this.#_canvas.style.position = 'absolute'; + this.#_canvas.style.inset = '0'; + document.body.append(this.#_canvas); + } + + public detach(): void { + if (this.#_unbindCallback === null) { + console.warn('Attempting to detach non-attached renderer'); + return; + } + + this.#_unbindCallback(); + this.#_unbindCallback = null; + this.#_canvas.remove(); + } + + public render(): void { + const { width, height } = this.#_canvas; + + this.#_ctx.clearRect(0, 0, width, height); + this.#_ctx.save(); + + this.#_ctx.fillStyle = this.settings.clearColor; + this.#_ctx.fillRect(0, 0, width, height); + + this.#_ctx.translate(width * 0.5, height * 0.5); + this.#_ctx.scale(1, -1); + + for (const drawable of drawables) { + this.#_ctx.save(); + this.#_ctx.beginPath(); + renderDrawable(this.#_ctx, this.settings, drawable); + this.#_ctx.restore(); + } + + this.#_ctx.restore(); + } +} diff --git a/sandbox/src/2d/scene-objects/circle.ts b/sandbox/src/2d/scene-objects/circle.ts new file mode 100644 index 0000000..7208554 --- /dev/null +++ b/sandbox/src/2d/scene-objects/circle.ts @@ -0,0 +1,29 @@ +import { ICircle } from '@stdlib/geometry/primitives.js'; +import { Vector2 } from '@stdlib/math/vector2.js'; + +import { IDrawableCircle } from '../drawables/circle.js'; +import { SceneObject } from '../scenes/scene.js'; + +export class Circle implements SceneObject, ICircle { + public readonly position: Vector2; + public radius: number; + public color: string; + + constructor(pos: Vector2, radius: number, color = '#fff') { + this.position = pos; + this.radius = radius; + this.color = color; + } + + public getDrawable(): IDrawableCircle { + return { + drawType: 'circle', + circle: { + position: this.position, + radius: this.radius, + }, + fill: 'transparent', + stroke: this.color, + }; + } +} diff --git a/sandbox/src/2d/scene-objects/grid.ts b/sandbox/src/2d/scene-objects/grid.ts new file mode 100644 index 0000000..f1e6e33 --- /dev/null +++ b/sandbox/src/2d/scene-objects/grid.ts @@ -0,0 +1,27 @@ +import { Vector2 } from '@stdlib/math/vector2.js'; + +import { IDrawableGrid } from '../drawables/grid.js'; +import { SceneObject } from '../scenes/scene.js'; + +export class Grid implements SceneObject { + public readonly position: Vector2; + public readonly range: Vector2; + public color: string; + public gridColor: string; + + constructor(pos: Vector2, range: Vector2, color = '#fff', gridColor = '#383838') { + this.position = pos; + this.range = range; + this.color = color; + this.gridColor = gridColor; + } + + public getDrawable(): IDrawableGrid { + return { + drawType: 'grid', + range: this.range, + color: this.color, + gridColor: this.gridColor, + }; + } +} diff --git a/sandbox/src/2d/scenes/quad-tree-vis.ts b/sandbox/src/2d/scenes/quad-tree-vis.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/scenes/ray-collisions.ts b/sandbox/src/2d/scenes/ray-collisions.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/scenes/scene.ts b/sandbox/src/2d/scenes/scene.ts new file mode 100644 index 0000000..3d0f00f --- /dev/null +++ b/sandbox/src/2d/scenes/scene.ts @@ -0,0 +1,15 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { SandboxContext } from '../context'; + +export interface SceneObject { + position: Vector2; +} + +export interface Scene { + objects: SceneObject[]; + tick(now: number): void; + cleanup(): void; +} + +export type SceneFactory = (context: SandboxContext) => Scene; diff --git a/sandbox/src/2d/scenes/shape-collisions.ts b/sandbox/src/2d/scenes/shape-collisions.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/scenes/shapes.ts b/sandbox/src/2d/scenes/shapes.ts new file mode 100644 index 0000000..cb18126 --- /dev/null +++ b/sandbox/src/2d/scenes/shapes.ts @@ -0,0 +1,80 @@ +import { transformRange } from '@stdlib/math/utils'; +import { Vector2 } from '@stdlib/math/vector2'; + +import { SandboxContext } from '../context'; +import { IDrawableAABB } from '../drawables/aabb'; +import { Drawable } from '../drawables/drawable'; + +import { Scene, SceneFactory } from './scene'; + +const MODIFIER = 0.0016; + +class ShapeScene implements Scene { + #_ctx: SandboxContext; + #_drawables: Drawable[]; + #_boxIndex = 1; + + constructor(context: SandboxContext) { + this.#_ctx = context; + this.#_drawables = [ + { + drawType: 'grid', + range: new Vector2(20, 20), + color: '#989898', + gridColor: '#383838', + }, + { + drawType: 'aabb', + fill: 'transparent', + stroke: 'red', + aabb: { + min: new Vector2(-4, -4), + max: new Vector2(4, 4), + }, + }, + { + drawType: 'circle', + fill: 'transparent', + stroke: 'red', + circle: { + radius: 2, + position: new Vector2(6, 0), + }, + }, + { + drawType: 'point', + position: new Vector2(6, 6), + color: 'red', + }, + { + drawType: 'ray', + ray: { + position: new Vector2(-6, -6), + direction: new Vector2(1, 1), + }, + color: 'red', + }, + ]; + } + + public tick(now: number): void { + const box = this.#_drawables[this.#_boxIndex] as IDrawableAABB; + const sin = Math.sin(now * MODIFIER); + + box.aabb.min.x = transformRange(sin, -1, 1, -4, -2); + box.aabb.min.y = transformRange(sin, -1, 1, -4, -2); + box.aabb.max.x = transformRange(-sin, -1, 1, 2, 4); + box.aabb.max.y = transformRange(-sin, -1, 1, 2, 4); + + this.#_ctx.renderer.render(this.#_drawables); + } + + public cleanup(): void { + // @todo + this.#_drawables = []; + } +} + +export const createScene: SceneFactory = (context: SandboxContext): Scene => { + return new ShapeScene(context); +}; diff --git a/sandbox/src/2d/scenes/spatial-map.ts b/sandbox/src/2d/scenes/spatial-map.ts new file mode 100644 index 0000000..e69de29 From 1d2bcbd84acd8c705b3c341e66bcf2c3e982d699 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 23 Mar 2025 23:32:41 +0200 Subject: [PATCH 21/31] Finish base implementation of compositor --- sandbox/src/2d/drawables/drawable.ts | 11 +- sandbox/src/2d/renderer/compositor.ts | 240 ++++++++++++++++++++++++-- 2 files changed, 227 insertions(+), 24 deletions(-) diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts index 72a82ad..0d411c3 100644 --- a/sandbox/src/2d/drawables/drawable.ts +++ b/sandbox/src/2d/drawables/drawable.ts @@ -16,12 +16,13 @@ type DrawableMap = DrawableMapExtractor< export type DrawableType = keyof DrawableMap; export type Drawable = DrawableMap[T]; - -export function renderDrawable( +export type DrawableRenderFn = ( ctx: CanvasRenderingContext2D, settings: RenderSettings, - drawable: Drawable, -): void { + drawable: Drawable, +) => void; + +export const renderDrawable: DrawableRenderFn = (ctx, settings, drawable) => { switch (drawable.drawType) { case 'aabb': drawAABB(ctx, settings, drawable); @@ -43,4 +44,4 @@ export function renderDrawable( drawGrid(ctx, settings, drawable); return; } -} +}; diff --git a/sandbox/src/2d/renderer/compositor.ts b/sandbox/src/2d/renderer/compositor.ts index 444b669..786f5cd 100644 --- a/sandbox/src/2d/renderer/compositor.ts +++ b/sandbox/src/2d/renderer/compositor.ts @@ -1,10 +1,17 @@ +import { IAABB2D, ICircle, IRay2D } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; -import { IDrawableAABB } from '../drawables/aabb'; -import { IDrawableCircle } from '../drawables/circle'; -import { Drawable, renderDrawable } from '../drawables/drawable'; -import { IDrawablePoint } from '../drawables/point'; -import { IDrawableRay } from '../drawables/ray'; +import { drawAABB, IDrawableAABB } from '../drawables/aabb'; +import { drawCircle, IDrawableCircle } from '../drawables/circle'; +import { + Drawable, + DrawableRenderFn, + DrawableType, + renderDrawable, +} from '../drawables/drawable'; +import { drawGrid, IDrawableGrid } from '../drawables/grid'; +import { drawPoint, IDrawablePoint } from '../drawables/point'; +import { drawRay, IDrawableRay } from '../drawables/ray'; import { defaultRenderSettings, RenderSettings } from './render-settings'; @@ -42,25 +49,28 @@ export function bindCanvasToWindow(canvas: HTMLCanvasElement): UnbindCallback { }; } -interface IDrawCommand { +interface IDrawableRender { data: ImageData; position: Vector2; zIndex: number; } +interface IDrawCommand { + resourceId: number; + drawable: Drawable; + size: Vector2; + renderFn: DrawableRenderFn; +} + class Compositor2D { public readonly bufferMaxHeight = 1000; public readonly pixelsPerUnit = 16; #_nextResourceId = 0; - #_resourceIds = new Set(); #_renderBuffer: CanvasRenderingContext2D; - #_drawBuffers = new Map(); - - #_aabbDraws = new Map(); - #_circleDraws = new Map(); - #_pointDraws = new Map(); - #_rayDraws = new Map(); + #_drawBuffers = new Map(); + #_drawCommands = new Map(); + #_dirty = new Set(); constructor() { const canvas = document.createElement('canvas'); @@ -74,6 +84,197 @@ class Compositor2D { const rid = this.#_nextResourceId++; return rid; } + + public deleteResource(rid: number): void { + // Remove all instances of the resource. + this.#_drawBuffers.delete(rid); + this.#_drawCommands.delete(rid); + + // Remove from dirty list. + for (const dirty of this.#_dirty) { + if (dirty.resourceId !== rid) { + continue; + } + + this.#_dirty.delete(dirty); + break; + } + } + + public drawAABB(rid: number, aabb: IAABB2D, fill: string, stroke: string): void { + const drawable: IDrawableAABB = { + drawType: 'aabb', + aabb, + fill, + stroke, + }; + + const command: IDrawCommand = { + resourceId: rid, + drawable, + size: new Vector2(aabb.max.x - aabb.min.x, aabb.max.y - aabb.min.y), + renderFn: drawAABB as DrawableRenderFn, + }; + + const halfSize = Vector2.MultiplyScalar(command.size, 0.5); + + this.#_dirty.add(command); + this.#_drawCommands.set(rid, command); + this.#_drawBuffers.set(rid, { + data: new ImageData(0, 0), + position: Vector2.Add(aabb.min, halfSize), + zIndex: 0, + }); + } + + public drawCircle(rid: number, circle: ICircle, fill: string, stroke: string): void { + const drawable: IDrawableCircle = { + drawType: 'circle', + circle, + fill, + stroke, + }; + + const command: IDrawCommand = { + resourceId: rid, + drawable, + size: new Vector2(circle.radius * 2, circle.radius * 2), + renderFn: drawCircle as DrawableRenderFn, + }; + + this.#_dirty.add(command); + this.#_drawCommands.set(rid, command); + this.#_drawBuffers.set(rid, { + data: new ImageData(0, 0), + position: new Vector2(circle.position), + zIndex: 0, + }); + } + + public drawPoint(rid: number, point: Vector2, color: string): void { + const drawable: IDrawablePoint = { + drawType: 'point', + position: point, + color, + }; + + const command: IDrawCommand = { + resourceId: rid, + drawable, + size: new Vector2(2, 2), + renderFn: drawPoint as DrawableRenderFn, + }; + + this.#_dirty.add(command); + this.#_drawCommands.set(rid, command); + this.#_drawBuffers.set(rid, { + data: new ImageData(0, 0), + position: point.copy(), + zIndex: 0, + }); + } + + public drawRay(rid: number, ray: IRay2D, color: string): void { + const drawable: IDrawableRay = { + drawType: 'ray', + ray, + color, + }; + + const size = Vector2.MultiplyScalar(Vector2.Normalize(ray.direction), 1000); + const command: IDrawCommand = { + resourceId: rid, + drawable, + size, + renderFn: drawRay as DrawableRenderFn, + }; + + this.#_dirty.add(command); + this.#_drawCommands.set(rid, command); + this.#_drawBuffers.set(rid, { + data: new ImageData(0, 0), + position: new Vector2(ray.position), + zIndex: 0, + }); + } + + public drawGrid(rid: number, range: Vector2, color: string, gridColor: string): void { + const drawable: IDrawableGrid = { + drawType: 'grid', + range, + color, + gridColor, + }; + + const command: IDrawCommand = { + resourceId: rid, + drawable, + size: range.copy(), + renderFn: drawGrid as DrawableRenderFn, + }; + + this.#_dirty.add(command); + this.#_drawCommands.set(rid, command); + this.#_drawBuffers.set(rid, { + data: new ImageData(0, 0), + position: Vector2.Zero(), + zIndex: -1, + }); + } + + public composite(context: CanvasRenderingContext2D, settings: RenderSettings): void { + this.#_preRenderDirtyCommands(settings); + + context.clearRect(0, 0, context.canvas.width, context.canvas.height); + + // TODO: Sort by z-index + const buffers = [...this.#_drawBuffers.entries()]; + + for (const [_rid, buffer] of buffers) { + context.putImageData( + buffer.data, + buffer.position.x - buffer.data.width * 0.5, + buffer.position.y - buffer.data.height * 0.5, + ); + } + } + + #_preRenderDirtyCommands(settings: RenderSettings): void { + if (this.#_dirty.size < 1) { + return; + } + + const dirty = [...this.#_dirty]; + this.#_dirty.clear(); + + const context = this.#_renderBuffer; + const canvas = context.canvas; + + for (const command of dirty) { + // Clear canvas and update size + canvas.width = 0; + canvas.height = 0; + canvas.width = command.size.x; + canvas.height = command.size.y; + + command.renderFn(context, settings, command.drawable); + const data = context.getImageData(0, 0, command.size.x, command.size.y); + + let buffer = this.#_drawBuffers.get(command.resourceId); + if (!buffer) { + buffer = { + data, + position: Vector2.Zero(), + zIndex: 0, + }; + } + + this.#_drawBuffers.set(command.resourceId, { + ...buffer, + data, + }); + } + } } export class CompositeRenderer2D { @@ -98,6 +299,12 @@ export class CompositeRenderer2D { this.#_compositor = new Compositor2D(); } + // TODO: Instead add forwarding API on the renderer itself to not expose the + // underlying compositor. + public getCompositor(): Compositor2D { + return this.#_compositor; + } + public attach(): void { if (this.#_unbindCallback !== null) { console.warn('Attempting to attach already attached renderer'); @@ -133,12 +340,7 @@ export class CompositeRenderer2D { this.#_ctx.translate(width * 0.5, height * 0.5); this.#_ctx.scale(1, -1); - for (const drawable of drawables) { - this.#_ctx.save(); - this.#_ctx.beginPath(); - renderDrawable(this.#_ctx, this.settings, drawable); - this.#_ctx.restore(); - } + this.#_compositor.composite(this.#_ctx, this.settings); this.#_ctx.restore(); } From da37dac72dbb81e2ba3aed72317bc797e134d584 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Thu, 27 Mar 2025 19:39:45 +0200 Subject: [PATCH 22/31] Migrate to composable renderer --- lib/diagnostics/frame-diagnostics.ts | 0 lib/diagnostics/measure.ts | 5 ++++ lib/time/clock.ts | 0 sandbox/src/2d/context.ts | 4 +-- sandbox/src/2d/main.ts | 4 +-- .../src/2d/scene-objects/drawable-object.ts | 26 +++++++++++++++++++ sandbox/src/2d/scenes/scene.ts | 1 + 7 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 lib/diagnostics/frame-diagnostics.ts create mode 100644 lib/diagnostics/measure.ts create mode 100644 lib/time/clock.ts create mode 100644 sandbox/src/2d/scene-objects/drawable-object.ts diff --git a/lib/diagnostics/frame-diagnostics.ts b/lib/diagnostics/frame-diagnostics.ts new file mode 100644 index 0000000..e69de29 diff --git a/lib/diagnostics/measure.ts b/lib/diagnostics/measure.ts new file mode 100644 index 0000000..06a2275 --- /dev/null +++ b/lib/diagnostics/measure.ts @@ -0,0 +1,5 @@ +export class Measure { + public readonly id: string; + #_start: number; + #_end: number; +} diff --git a/lib/time/clock.ts b/lib/time/clock.ts new file mode 100644 index 0000000..e69de29 diff --git a/sandbox/src/2d/context.ts b/sandbox/src/2d/context.ts index 84c5abf..291aad3 100644 --- a/sandbox/src/2d/context.ts +++ b/sandbox/src/2d/context.ts @@ -1,5 +1,5 @@ -import { Renderer2D } from './renderer/renderer'; +import { CompositeRenderer2D } from './renderer/compositor.js'; export interface SandboxContext { - renderer: Renderer2D; + renderer: CompositeRenderer2D; } diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 6b539e1..6d6ab4f 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,11 +1,11 @@ -import { Renderer2D } from './renderer/renderer.js'; +import { CompositeRenderer2D } from './renderer/compositor.js'; import { Scene } from './scenes/scene.js'; import { createScene } from './scenes/shapes.js'; function main(): void { let frameRef = 0; - const renderer = new Renderer2D(); + const renderer = new CompositeRenderer2D(); renderer.attach(); const activeScene: Scene = createScene({ diff --git a/sandbox/src/2d/scene-objects/drawable-object.ts b/sandbox/src/2d/scene-objects/drawable-object.ts new file mode 100644 index 0000000..2edd2e3 --- /dev/null +++ b/sandbox/src/2d/scene-objects/drawable-object.ts @@ -0,0 +1,26 @@ +import { Vector2 } from '@stdlib/math/vector2.js'; + +import { SandboxContext } from '../context.js'; +import { SceneObject } from '../scenes/scene.js'; + +export abstract class DrawableObject implements SceneObject { + public readonly rid: number; + public readonly position: Vector2; + + #_ctx: SandboxContext; + + constructor(ctx: SandboxContext) { + this.#_ctx = ctx; + this.position = Vector2.Zero(); + + const compositor = ctx.renderer.getCompositor(); + this.rid = compositor.createResource(); + } + + public destroy(): void { + const compositor = this.#_ctx.renderer.getCompositor(); + compositor.deleteResource(this.rid); + } + + public abstract draw(): void; +} diff --git a/sandbox/src/2d/scenes/scene.ts b/sandbox/src/2d/scenes/scene.ts index 3d0f00f..a926224 100644 --- a/sandbox/src/2d/scenes/scene.ts +++ b/sandbox/src/2d/scenes/scene.ts @@ -4,6 +4,7 @@ import { SandboxContext } from '../context'; export interface SceneObject { position: Vector2; + destroy(): void; } export interface Scene { From 02122d90d969ebe007e88d3fda8092d53a63fc5d Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sat, 8 Nov 2025 22:49:22 +0200 Subject: [PATCH 23/31] Iteration --- package.json | 2 +- sandbox/src/2d/renderer/compositor.ts | 6 ++++++ sandbox/src/2d/scene-objects/circle.ts | 9 +++++++-- sandbox/src/2d/scene-objects/manager.ts | 4 ++++ 4 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 sandbox/src/2d/scene-objects/manager.ts diff --git a/package.json b/package.json index b7108ba..1e24a95 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "repository": "github:bengsfort/stdlib", "license": "MIT", - "packageManager": "pnpm@10.4.1", + "packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39", "engines": { "node": ">=20.0.0" }, diff --git a/sandbox/src/2d/renderer/compositor.ts b/sandbox/src/2d/renderer/compositor.ts index 786f5cd..359159b 100644 --- a/sandbox/src/2d/renderer/compositor.ts +++ b/sandbox/src/2d/renderer/compositor.ts @@ -227,6 +227,9 @@ class Compositor2D { context.clearRect(0, 0, context.canvas.width, context.canvas.height); + context.fillStyle = 'green'; + context.fillRect(-8, -8, 16, 16); + // TODO: Sort by z-index const buffers = [...this.#_drawBuffers.entries()]; @@ -337,6 +340,9 @@ export class CompositeRenderer2D { this.#_ctx.fillStyle = this.settings.clearColor; this.#_ctx.fillRect(0, 0, width, height); + this.#_ctx.fillStyle = 'red'; + this.#_ctx.fillRect(width * 0.5 - 16, height * 0.5 - 16, 32, 32); + this.#_ctx.translate(width * 0.5, height * 0.5); this.#_ctx.scale(1, -1); diff --git a/sandbox/src/2d/scene-objects/circle.ts b/sandbox/src/2d/scene-objects/circle.ts index 7208554..399d9f0 100644 --- a/sandbox/src/2d/scene-objects/circle.ts +++ b/sandbox/src/2d/scene-objects/circle.ts @@ -1,15 +1,20 @@ import { ICircle } from '@stdlib/geometry/primitives.js'; import { Vector2 } from '@stdlib/math/vector2.js'; +import { SandboxContext } from '../context.js'; import { IDrawableCircle } from '../drawables/circle.js'; import { SceneObject } from '../scenes/scene.js'; -export class Circle implements SceneObject, ICircle { +import { DrawableObject } from './drawable-object.js'; + +export class Circle extends DrawableObject implements ICircle { public readonly position: Vector2; public radius: number; public color: string; - constructor(pos: Vector2, radius: number, color = '#fff') { + constructor(ctx: SandboxContext, pos: Vector2, radius: number, color = '#fff') { + super(ctx); + this.position = pos; this.radius = radius; this.color = color; diff --git a/sandbox/src/2d/scene-objects/manager.ts b/sandbox/src/2d/scene-objects/manager.ts new file mode 100644 index 0000000..b5973d0 --- /dev/null +++ b/sandbox/src/2d/scene-objects/manager.ts @@ -0,0 +1,4 @@ + +export class ObjectManager { + +} From 1782a73c4aed51971f69b55147ef3d13e1f4d6dc Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Sun, 9 Nov 2025 11:13:47 +0200 Subject: [PATCH 24/31] Fix mouse mgr --- sandbox/src/2d/context.ts | 8 +- sandbox/src/2d/input/manager.ts | 11 +- sandbox/src/2d/input/mouse.ts | 133 +++++++++++++++++- sandbox/src/2d/main.ts | 23 ++- sandbox/src/2d/renderer/compositor.ts | 25 ++-- sandbox/src/2d/renderer/renderer.ts | 35 +++-- sandbox/src/2d/scene-objects/circle.ts | 34 ----- .../src/2d/scene-objects/drawable-object.ts | 26 ---- sandbox/src/2d/scene-objects/grid.ts | 27 ---- sandbox/src/2d/scene-objects/manager.ts | 4 - sandbox/src/2d/scenes/scene.ts | 9 +- sandbox/src/2d/scenes/shapes.ts | 127 +++++++++-------- 12 files changed, 270 insertions(+), 192 deletions(-) delete mode 100644 sandbox/src/2d/scene-objects/circle.ts delete mode 100644 sandbox/src/2d/scene-objects/drawable-object.ts delete mode 100644 sandbox/src/2d/scene-objects/grid.ts delete mode 100644 sandbox/src/2d/scene-objects/manager.ts diff --git a/sandbox/src/2d/context.ts b/sandbox/src/2d/context.ts index 291aad3..681c99f 100644 --- a/sandbox/src/2d/context.ts +++ b/sandbox/src/2d/context.ts @@ -1,5 +1,9 @@ -import { CompositeRenderer2D } from './renderer/compositor.js'; +import { InputManager } from './input/manager.js'; +import { MouseInput } from './input/mouse.js'; +import { Renderer2D } from './renderer/renderer.js'; export interface SandboxContext { - renderer: CompositeRenderer2D; + renderer: Renderer2D; + mouse: MouseInput; + input: InputManager; } diff --git a/sandbox/src/2d/input/manager.ts b/sandbox/src/2d/input/manager.ts index 46f9456..10db67b 100644 --- a/sandbox/src/2d/input/manager.ts +++ b/sandbox/src/2d/input/manager.ts @@ -60,14 +60,13 @@ class ActionTypeMismatchError extends Error { } } -export class InputManager { +export class InputManager { #_actions?: Actions; #_bindingsMap = new Map>(); #_codeMap = new Map(); #_boolActions = new Map, boolean>(); #_rangeActions = new Map, number>(); #_vecRangeActions = new Map, Vector2>(); - #_mousePos = new Vector2(); public clearActions(): void { this.#_actions = undefined; @@ -76,11 +75,15 @@ export class InputManager { this.#_boolActions.clear(); this.#_rangeActions.clear(); this.#_vecRangeActions.clear(); + + document.removeEventListener('keydown', this.#handleKeyDown); + document.removeEventListener('keyup', this.#handleKeyUp); } - public registerActions(actions: Actions): void { + public registerActions(actions: T): InputManager { this.clearActions(); + // @ts-expect-error i dont jaksa to type this atm this.#_actions = actions; // Init code lookup maps @@ -122,6 +125,8 @@ export class InputManager { document.addEventListener('keydown', this.#handleKeyDown); document.addEventListener('keyup', this.#handleKeyUp); + + return this as unknown as InputManager; } public getBoolAction(action: InputAction): boolean { diff --git a/sandbox/src/2d/input/mouse.ts b/sandbox/src/2d/input/mouse.ts index db14021..77e699e 100644 --- a/sandbox/src/2d/input/mouse.ts +++ b/sandbox/src/2d/input/mouse.ts @@ -1,8 +1,139 @@ import { Vector2 } from '@stdlib/math/vector2'; +interface MouseButtonState { + downThisFrame: boolean; + upThisFrame: boolean; + pressed: boolean; + queuedState: boolean; + frame: number; +} + +const MouseButtonP2 = { + left: 1, + right: 2, +} as const; + export class MouseInput { public readonly mousePosition: Vector2; + + #_scrollAmount: number; + #_mouseBtn1State: MouseButtonState; + #_mouseBtn2State: MouseButtonState; + + #_canvas: HTMLCanvasElement | null; + constructor() { - // + this.#_canvas = null; + this.mousePosition = new Vector2(); + this.#_scrollAmount = 0; + this.#_mouseBtn1State = { + downThisFrame: false, + upThisFrame: false, + pressed: false, + queuedState: false, + frame: 0, + }; + this.#_mouseBtn2State = { + downThisFrame: false, + upThisFrame: false, + pressed: false, + queuedState: false, + frame: 0, + }; + } + + public attach(canvas: HTMLCanvasElement): void { + if (this.#_canvas !== null) { + throw new Error('MouseInput already attached to canvas!'); + } + + this.#_canvas = canvas; + + // press not working... + canvas.addEventListener('mousedown', this.#handleMouseEvent); + canvas.addEventListener('mouseup', this.#handleMouseEvent); + window.addEventListener('mousemove', this.#handleMouseEvent); + window.addEventListener('contextmenu', this.#disableContextMenu); + } + + public tick(frame: number): void { + this.#_mouseBtn1State = this.#updateButtonState(frame, this.#_mouseBtn1State); + this.#_mouseBtn2State = this.#updateButtonState(frame, this.#_mouseBtn2State); + } + + public detach(): void { + this.#_canvas?.removeEventListener('mousedown', this.#handleMouseEvent); + this.#_canvas?.removeEventListener('mouseup', this.#handleMouseEvent); + window.removeEventListener('mousemove', this.#handleMouseEvent); + window.removeEventListener('contextmenu', this.#disableContextMenu); + } + + public getMouse1DownThisFrame(): boolean { + return this.#_mouseBtn1State.downThisFrame; + } + + public getMouse1Pressed(): boolean { + return this.#_mouseBtn1State.pressed; + } + + public getMouse1UpThisFrame(): boolean { + return this.#_mouseBtn1State.upThisFrame; + } + + public getMouse2DownThisFrame(): boolean { + return this.#_mouseBtn2State.downThisFrame; + } + + public getMouse2Pressed(): boolean { + return this.#_mouseBtn2State.pressed; + } + + public getMouse2UpThisFrame(): boolean { + return this.#_mouseBtn2State.upThisFrame; } + + #updateButtonState(frame: number, state: MouseButtonState): MouseButtonState { + if (state.queuedState !== state.pressed) { + const { queuedState } = state; + return { + downThisFrame: queuedState, + upThisFrame: !queuedState, + pressed: queuedState, + queuedState, + frame, + }; + } + + if (state.downThisFrame && state.frame !== frame) { + return { + ...state, + downThisFrame: false, + }; + } + + if (state.upThisFrame && state.frame !== frame) { + return { + ...state, + upThisFrame: false, + }; + } + + return state; + } + + #disableContextMenu = (ev: Event): void => { + ev.stopPropagation(); + }; + + #handleMouseEvent = (ev: MouseEvent): void => { + ev.preventDefault(); + ev.stopPropagation(); + const { buttons, clientX, clientY } = ev; + + // Check for left mouse button + this.#_mouseBtn1State.queuedState = Boolean(buttons & MouseButtonP2.left); + this.#_mouseBtn2State.queuedState = Boolean(buttons & MouseButtonP2.right); + this.mousePosition.x = clientX; + this.mousePosition.y = clientY; + }; } diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 6d6ab4f..ba41882 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,26 +1,31 @@ -import { CompositeRenderer2D } from './renderer/compositor.js'; +import { InputManager } from './input/manager.js'; +import { MouseInput } from './input/mouse.js'; +import { Renderer2D } from './renderer/renderer.js'; import { Scene } from './scenes/scene.js'; import { createScene } from './scenes/shapes.js'; function main(): void { let frameRef = 0; + let frameCount = 0; - const renderer = new CompositeRenderer2D(); - renderer.attach(); - + const input = new InputManager(); + const mouse = new MouseInput(); + const renderer = new Renderer2D(); const activeScene: Scene = createScene({ renderer, + mouse, + input, }); const tick = (now: number): void => { frameRef = requestAnimationFrame(tick); + frameCount++; - // TODO: input + mouse.tick(frameCount); activeScene.tick(now); + renderer.render(activeScene); }; - frameRef = requestAnimationFrame(tick); - window.addEventListener('blur', () => { cancelAnimationFrame(frameRef); console.log('Pausing loop.'); @@ -30,6 +35,10 @@ function main(): void { frameRef = requestAnimationFrame(tick); console.log('Resuming loop.'); }); + + renderer.attach(); + mouse.attach(renderer.getCanvas()); + frameRef = requestAnimationFrame(tick); } main(); diff --git a/sandbox/src/2d/renderer/compositor.ts b/sandbox/src/2d/renderer/compositor.ts index 359159b..084f8b5 100644 --- a/sandbox/src/2d/renderer/compositor.ts +++ b/sandbox/src/2d/renderer/compositor.ts @@ -3,18 +3,23 @@ import { Vector2 } from '@stdlib/math/vector2'; import { drawAABB, IDrawableAABB } from '../drawables/aabb'; import { drawCircle, IDrawableCircle } from '../drawables/circle'; -import { - Drawable, - DrawableRenderFn, - DrawableType, - renderDrawable, -} from '../drawables/drawable'; +import { Drawable, DrawableRenderFn } from '../drawables/drawable'; import { drawGrid, IDrawableGrid } from '../drawables/grid'; import { drawPoint, IDrawablePoint } from '../drawables/point'; import { drawRay, IDrawableRay } from '../drawables/ray'; import { defaultRenderSettings, RenderSettings } from './render-settings'; +// This compositor is not fully working, but it was an interesting thing to work +// on so it is here as-is for future improving. +// +// It essentially does not create the buffers correctly -- the ImageData creation +// fails due to invalid sizing. +// +// Additionally, the flow is very awkward. It should likely be changed to where +// you request to create a specific drawing/shape INSTANCE -- and then you get +// the RID from that instead of doing the current rid -> draw call approach. + function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { const { devicePixelRatio } = window; @@ -225,11 +230,6 @@ class Compositor2D { public composite(context: CanvasRenderingContext2D, settings: RenderSettings): void { this.#_preRenderDirtyCommands(settings); - context.clearRect(0, 0, context.canvas.width, context.canvas.height); - - context.fillStyle = 'green'; - context.fillRect(-8, -8, 16, 16); - // TODO: Sort by z-index const buffers = [...this.#_drawBuffers.entries()]; @@ -340,9 +340,6 @@ export class CompositeRenderer2D { this.#_ctx.fillStyle = this.settings.clearColor; this.#_ctx.fillRect(0, 0, width, height); - this.#_ctx.fillStyle = 'red'; - this.#_ctx.fillRect(width * 0.5 - 16, height * 0.5 - 16, 32, 32); - this.#_ctx.translate(width * 0.5, height * 0.5); this.#_ctx.scale(1, -1); diff --git a/sandbox/src/2d/renderer/renderer.ts b/sandbox/src/2d/renderer/renderer.ts index 0bc6232..5f4f9af 100644 --- a/sandbox/src/2d/renderer/renderer.ts +++ b/sandbox/src/2d/renderer/renderer.ts @@ -1,4 +1,4 @@ -import { Drawable, renderDrawable } from '../drawables/drawable'; +import { Scene } from '../scenes/scene'; import { defaultRenderSettings, RenderSettings } from './render-settings'; @@ -56,6 +56,10 @@ export class Renderer2D { this.#_ctx = ctx; } + public getCanvas(): HTMLCanvasElement { + return this.#_canvas; + } + public attach(): void { if (this.#_unbindCallback !== null) { console.warn('Attempting to attach already attached renderer'); @@ -79,25 +83,30 @@ export class Renderer2D { this.#_canvas.remove(); } - public render(drawables: Drawable[] = []): void { + public render(scene: Scene): void { const { width, height } = this.#_canvas; - this.#_ctx.clearRect(0, 0, width, height); - this.#_ctx.save(); - + // Clear the canvas. this.#_ctx.fillStyle = this.settings.clearColor; + this.#_ctx.clearRect(0, 0, width, height); this.#_ctx.fillRect(0, 0, width, height); - - this.#_ctx.translate(width * 0.5, height * 0.5); - this.#_ctx.scale(1, -1); - - for (const drawable of drawables) { + this.#_ctx.save(); + { + // Set the base transformation matrices so scene objects are rendered + // relative to the 'world grid'. + this.#_ctx.translate( + width * 0.5 + scene.cameraOrigin.x * this.settings.pixelsPerUnit, + height * 0.5 + scene.cameraOrigin.y * this.settings.pixelsPerUnit, + ); + this.#_ctx.scale(1, -1); + + // Render the scene. this.#_ctx.save(); - this.#_ctx.beginPath(); - renderDrawable(this.#_ctx, this.settings, drawable); + { + scene.render(this.#_ctx, this.settings); + } this.#_ctx.restore(); } - this.#_ctx.restore(); } } diff --git a/sandbox/src/2d/scene-objects/circle.ts b/sandbox/src/2d/scene-objects/circle.ts deleted file mode 100644 index 399d9f0..0000000 --- a/sandbox/src/2d/scene-objects/circle.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ICircle } from '@stdlib/geometry/primitives.js'; -import { Vector2 } from '@stdlib/math/vector2.js'; - -import { SandboxContext } from '../context.js'; -import { IDrawableCircle } from '../drawables/circle.js'; -import { SceneObject } from '../scenes/scene.js'; - -import { DrawableObject } from './drawable-object.js'; - -export class Circle extends DrawableObject implements ICircle { - public readonly position: Vector2; - public radius: number; - public color: string; - - constructor(ctx: SandboxContext, pos: Vector2, radius: number, color = '#fff') { - super(ctx); - - this.position = pos; - this.radius = radius; - this.color = color; - } - - public getDrawable(): IDrawableCircle { - return { - drawType: 'circle', - circle: { - position: this.position, - radius: this.radius, - }, - fill: 'transparent', - stroke: this.color, - }; - } -} diff --git a/sandbox/src/2d/scene-objects/drawable-object.ts b/sandbox/src/2d/scene-objects/drawable-object.ts deleted file mode 100644 index 2edd2e3..0000000 --- a/sandbox/src/2d/scene-objects/drawable-object.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Vector2 } from '@stdlib/math/vector2.js'; - -import { SandboxContext } from '../context.js'; -import { SceneObject } from '../scenes/scene.js'; - -export abstract class DrawableObject implements SceneObject { - public readonly rid: number; - public readonly position: Vector2; - - #_ctx: SandboxContext; - - constructor(ctx: SandboxContext) { - this.#_ctx = ctx; - this.position = Vector2.Zero(); - - const compositor = ctx.renderer.getCompositor(); - this.rid = compositor.createResource(); - } - - public destroy(): void { - const compositor = this.#_ctx.renderer.getCompositor(); - compositor.deleteResource(this.rid); - } - - public abstract draw(): void; -} diff --git a/sandbox/src/2d/scene-objects/grid.ts b/sandbox/src/2d/scene-objects/grid.ts deleted file mode 100644 index f1e6e33..0000000 --- a/sandbox/src/2d/scene-objects/grid.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Vector2 } from '@stdlib/math/vector2.js'; - -import { IDrawableGrid } from '../drawables/grid.js'; -import { SceneObject } from '../scenes/scene.js'; - -export class Grid implements SceneObject { - public readonly position: Vector2; - public readonly range: Vector2; - public color: string; - public gridColor: string; - - constructor(pos: Vector2, range: Vector2, color = '#fff', gridColor = '#383838') { - this.position = pos; - this.range = range; - this.color = color; - this.gridColor = gridColor; - } - - public getDrawable(): IDrawableGrid { - return { - drawType: 'grid', - range: this.range, - color: this.color, - gridColor: this.gridColor, - }; - } -} diff --git a/sandbox/src/2d/scene-objects/manager.ts b/sandbox/src/2d/scene-objects/manager.ts deleted file mode 100644 index b5973d0..0000000 --- a/sandbox/src/2d/scene-objects/manager.ts +++ /dev/null @@ -1,4 +0,0 @@ - -export class ObjectManager { - -} diff --git a/sandbox/src/2d/scenes/scene.ts b/sandbox/src/2d/scenes/scene.ts index a926224..b4d931c 100644 --- a/sandbox/src/2d/scenes/scene.ts +++ b/sandbox/src/2d/scenes/scene.ts @@ -1,15 +1,16 @@ -import { Vector2 } from '@stdlib/math/vector2'; +import type { Vector2 } from '@stdlib/math/vector2'; -import { SandboxContext } from '../context'; +import type { SandboxContext } from '../context'; +import type { RenderSettings } from '../renderer/render-settings'; export interface SceneObject { position: Vector2; - destroy(): void; } export interface Scene { - objects: SceneObject[]; + cameraOrigin: Vector2; tick(now: number): void; + render(context: CanvasRenderingContext2D, settings: RenderSettings): void; cleanup(): void; } diff --git a/sandbox/src/2d/scenes/shapes.ts b/sandbox/src/2d/scenes/shapes.ts index cb18126..23b1d6f 100644 --- a/sandbox/src/2d/scenes/shapes.ts +++ b/sandbox/src/2d/scenes/shapes.ts @@ -1,80 +1,93 @@ +import type { IAABB2D, ICircle, IRay2D } from '@stdlib/geometry/primitives'; import { transformRange } from '@stdlib/math/utils'; import { Vector2 } from '@stdlib/math/vector2'; import { SandboxContext } from '../context'; -import { IDrawableAABB } from '../drawables/aabb'; -import { Drawable } from '../drawables/drawable'; +import { drawAABB } from '../drawables/aabb'; +import { drawCircle } from '../drawables/circle'; +import { drawGrid } from '../drawables/grid'; +import { drawPoint } from '../drawables/point'; +import { drawRay } from '../drawables/ray'; +import type { RenderSettings } from '../renderer/render-settings'; -import { Scene, SceneFactory } from './scene'; +import type { Scene, SceneFactory } from './scene'; const MODIFIER = 0.0016; class ShapeScene implements Scene { - #_ctx: SandboxContext; - #_drawables: Drawable[]; - #_boxIndex = 1; + public readonly cameraOrigin: Vector2; - constructor(context: SandboxContext) { - this.#_ctx = context; - this.#_drawables = [ - { - drawType: 'grid', - range: new Vector2(20, 20), - color: '#989898', - gridColor: '#383838', - }, - { - drawType: 'aabb', - fill: 'transparent', - stroke: 'red', - aabb: { - min: new Vector2(-4, -4), - max: new Vector2(4, 4), - }, - }, - { - drawType: 'circle', - fill: 'transparent', - stroke: 'red', - circle: { - radius: 2, - position: new Vector2(6, 0), - }, - }, - { - drawType: 'point', - position: new Vector2(6, 6), - color: 'red', - }, - { - drawType: 'ray', - ray: { - position: new Vector2(-6, -6), - direction: new Vector2(1, 1), - }, - color: 'red', - }, - ]; + #_aabb: IAABB2D; + #_circle: ICircle; + #_point: Vector2; + #_ray: IRay2D; + + constructor() { + this.cameraOrigin = new Vector2(0, 0); + + this.#_aabb = { + min: new Vector2(-4, -4), + max: new Vector2(4, 4), + }; + + this.#_circle = { + radius: 2, + position: new Vector2(6, 0), + }; + + this.#_point = new Vector2(6, 6); + this.#_ray = { + position: new Vector2(-6, -6), + direction: new Vector2(1, 1), + }; } public tick(now: number): void { - const box = this.#_drawables[this.#_boxIndex] as IDrawableAABB; + const box = this.#_aabb; const sin = Math.sin(now * MODIFIER); - box.aabb.min.x = transformRange(sin, -1, 1, -4, -2); - box.aabb.min.y = transformRange(sin, -1, 1, -4, -2); - box.aabb.max.x = transformRange(-sin, -1, 1, 2, 4); - box.aabb.max.y = transformRange(-sin, -1, 1, 2, 4); + box.min.x = transformRange(sin, -1, 1, -4, -2); + box.min.y = transformRange(sin, -1, 1, -4, -2); + box.max.x = transformRange(-sin, -1, 1, 2, 4); + box.max.y = transformRange(-sin, -1, 1, 2, 4); + } - this.#_ctx.renderer.render(this.#_drawables); + public render(context: CanvasRenderingContext2D, settings: RenderSettings): void { + drawGrid(context, settings, { + drawType: 'grid', + range: new Vector2(20, 20), + color: '#989898', + gridColor: '#383838', + }); + drawAABB(context, settings, { + drawType: 'aabb', + fill: 'transparent', + stroke: 'red', + aabb: this.#_aabb, + }); + drawCircle(context, settings, { + drawType: 'circle', + fill: 'transparent', + stroke: 'red', + circle: this.#_circle, + }); + drawPoint(context, settings, { + drawType: 'point', + position: this.#_point, + color: 'red', + }); + drawRay(context, settings, { + drawType: 'ray', + ray: this.#_ray, + color: 'red', + }); } public cleanup(): void { - // @todo - this.#_drawables = []; + // No need } } -export const createScene: SceneFactory = (context: SandboxContext): Scene => { - return new ShapeScene(context); +export const createScene: SceneFactory = (_context: SandboxContext): Scene => { + return new ShapeScene(); }; From 7bf488c64ced6324d787f7832636f3074bb525d3 Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 9 Nov 2025 15:48:12 +0200 Subject: [PATCH 25/31] Add fps counter to sandbox --- eslint.config.js | 2 +- lib/diagnostics/frame-diagnostics.ts | 0 lib/diagnostics/measure.ts | 22 +++++++ lib/time/clock.ts | 0 package.json | 14 ++++- sandbox/src/2d/main.ts | 92 +++++++++++++++++++++++++++- sandbox/src/utils/fixed-array.ts | 46 ++++++++++++++ sandbox/tsconfig.json | 2 +- 8 files changed, 171 insertions(+), 7 deletions(-) delete mode 100644 lib/diagnostics/frame-diagnostics.ts delete mode 100644 lib/time/clock.ts create mode 100644 sandbox/src/utils/fixed-array.ts diff --git a/eslint.config.js b/eslint.config.js index 452b4fd..7c49985 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -6,7 +6,7 @@ export default [ ignores: ['dist/'], }, { - files: ['./lib/**/*.ts'], + files: ['./lib/**/*.ts', './sandbox/src/**/*.ts'], }, ...bengsfort.configs.strictTypeChecked(import.meta.dirname), ]; diff --git a/lib/diagnostics/frame-diagnostics.ts b/lib/diagnostics/frame-diagnostics.ts deleted file mode 100644 index e69de29..0000000 diff --git a/lib/diagnostics/measure.ts b/lib/diagnostics/measure.ts index 06a2275..39066e3 100644 --- a/lib/diagnostics/measure.ts +++ b/lib/diagnostics/measure.ts @@ -1,5 +1,27 @@ export class Measure { public readonly id: string; + #_start: number; #_end: number; + #_duration: number; + + constructor(id: string) { + this.id = id; + this.#_start = performance.now(); + this.#_end = -1; + this.#_duration = -1; + } + + public getDuration(): number { + return this.#_duration; + } + + public finish(): number { + if (this.#_end === -1) { + this.#_end = performance.now(); + this.#_duration = this.#_end = this.#_start; + } + + return this.#_duration; + } } diff --git a/lib/time/clock.ts b/lib/time/clock.ts deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 1e24a95..cca5143 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,10 @@ }, "type": "module", "exports": { + "./diagnostics/*": { + "types": "./dist/diagnostics/*.d.ts", + "default": "./dist/diagnostics/*.js" + }, "./errors/*": { "types": "./dist/errors/*.d.ts", "default": "./dist/errors/*.js" @@ -23,9 +27,9 @@ "types": "./dist/events/*.d.ts", "default": "./dist/events/*.js" }, - "./math/*": { - "types": "./dist/math/*.d.ts", - "default": "./dist/math/*.js" + "./geometry/*": { + "types": "./dist/geometry/*.d.ts", + "default": "./dist/geometry/*.js" }, "./logging/*": { "types": "./dist/logging/*.d.ts", @@ -34,6 +38,10 @@ "./logging/node/*": { "types": "./dist/logging/node/*.d.ts", "default": "./dist/logging/node/*.js" + }, + "./math/*": { + "types": "./dist/math/*.d.ts", + "default": "./dist/math/*.js" } }, "files": [ diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index ba41882..4601ad5 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,13 +1,91 @@ +import { makeLogger } from '@stdlib/logging/logger.js'; + import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; import { Scene } from './scenes/scene.js'; import { createScene } from './scenes/shapes.js'; +import { RepeatingArray } from '@/utils/fixed-array.js'; + +const Log = makeLogger('sandbox2d'); + +interface Timing { + frameStart: number; + updateEnd: number; + drawEnd: number; +} + +function drawFps(last100: RepeatingArray, canvas: HTMLCanvasElement): void { + // Calculate average + let totalDraw = 0; + let totalUpdate = 0; + let totalFrame = 0; + let drawMax = 0; + let updateMax = 0; + let frameMax = 0; + + for (const timing of last100) { + const frameTime = timing.drawEnd - timing.frameStart; + const drawTime = timing.drawEnd - timing.updateEnd; + const updateTime = timing.updateEnd - timing.frameStart; + + totalFrame += frameTime; + totalDraw += drawTime; + totalUpdate += updateTime; + + if (frameMax < frameTime) frameMax = frameTime; + if (drawMax < drawTime) drawMax = drawTime; + if (updateMax < updateTime) updateMax = updateTime; + } + + const sampleCount = last100.getCount(); + const avgDraw = totalDraw / sampleCount; + const avgUpdate = totalUpdate / sampleCount; + const avgFrame = totalFrame / sampleCount; + const fps = 1000 / avgFrame; + + // Draw + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + ctx.save(); + { + const x = 8; + const y = 0; + + ctx.font = '14px monospace'; + + const fpsStr = `avg fps ${fps.toFixed(0)} (${avgFrame.toFixed(2)}ms / ${avgUpdate.toFixed(2)}ms update / ${avgDraw.toFixed(2)}ms draw)`; + const maxStr = `max fps ${frameMax.toFixed(2)}ms / max update ${updateMax.toFixed(2)}ms / max draw ${drawMax.toFixed(2)}ms`; + + const fpsSize = ctx.measureText(fpsStr); + const maxSize = ctx.measureText(maxStr); + + const fpsHeight = fpsSize.fontBoundingBoxAscent + fpsSize.fontBoundingBoxDescent; + const maxHeight = maxSize.fontBoundingBoxAscent + maxSize.fontBoundingBoxDescent; + + ctx.globalAlpha = 0.5; + ctx.fillStyle = '#000'; + ctx.fillRect(x, y, Math.max(fpsSize.width, maxSize.width), fpsHeight + maxHeight); + ctx.globalAlpha = 1.0; + + ctx.fillStyle = '#fff'; + ctx.fillText(fpsStr, x, y + fpsSize.fontBoundingBoxAscent); + ctx.fillText(maxStr, x, y + fpsHeight + maxSize.fontBoundingBoxAscent); + } + ctx.restore(); +} + function main(): void { let frameRef = 0; let frameCount = 0; + let frameStart = performance.now(); + let updateEnd = -1; + let drawEnd = -1; + + const frameTimes = new RepeatingArray(100); const input = new InputManager(); const mouse = new MouseInput(); const renderer = new Renderer2D(); @@ -21,19 +99,29 @@ function main(): void { frameRef = requestAnimationFrame(tick); frameCount++; + frameStart = now; mouse.tick(frameCount); activeScene.tick(now); + updateEnd = performance.now(); renderer.render(activeScene); + drawEnd = performance.now(); + + frameTimes.add({ + frameStart, + updateEnd, + drawEnd, + }); + drawFps(frameTimes, renderer.getCanvas()); }; window.addEventListener('blur', () => { cancelAnimationFrame(frameRef); - console.log('Pausing loop.'); + Log.info('Pausing loop.'); }); window.addEventListener('focus', () => { frameRef = requestAnimationFrame(tick); - console.log('Resuming loop.'); + Log.info('Resuming loop.'); }); renderer.attach(); diff --git a/sandbox/src/utils/fixed-array.ts b/sandbox/src/utils/fixed-array.ts new file mode 100644 index 0000000..8096fee --- /dev/null +++ b/sandbox/src/utils/fixed-array.ts @@ -0,0 +1,46 @@ +export class RepeatingArray { + #_data: T[]; + #_size: number; + #_cursor: number; + #_count: number; + + constructor(size: number) { + this.#_data = new Array(size); + this.#_size = size; + this.#_cursor = 0; + this.#_count = 0; + } + + public add(item: T): void { + this.#_data[this.#_cursor] = item; + + this.#_count = Math.min(this.#_count + 1, this.#_size); + this.#_cursor += 1; + if (this.#_cursor >= this.#_size) { + this.#_cursor = 0; + } + } + + public getCount(): number { + return this.#_count; + } + + public getSize(): number { + return this.#_size; + } + + public [Symbol.iterator](): Iterator { + let idx = 0; + + return { + next: (): IteratorResult => { + if (idx >= this.#_count) return { done: true, value: undefined }; + + return { + value: this.#_data[idx++], + done: false, + }; + }, + }; + } +} diff --git a/sandbox/tsconfig.json b/sandbox/tsconfig.json index 06c5d70..7c79310 100644 --- a/sandbox/tsconfig.json +++ b/sandbox/tsconfig.json @@ -39,4 +39,4 @@ "src", "vite.config.ts" ], -} \ No newline at end of file +} From d7e5da0d6a1cff1915d6bc84e5c3c00ac064264a Mon Sep 17 00:00:00 2001 From: bengsfort Date: Sun, 9 Nov 2025 16:38:45 +0200 Subject: [PATCH 26/31] Work on shape collisions sandbox --- sandbox/src/2d/main.ts | 16 +- sandbox/src/2d/renderer/renderer.ts | 16 +- sandbox/src/2d/scenes/shape-collisions.ts | 170 ++++++++++++++++++++++ 3 files changed, 197 insertions(+), 5 deletions(-) diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 4601ad5..a9275cb 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,9 +1,11 @@ import { makeLogger } from '@stdlib/logging/logger.js'; +import { SandboxContext } from './context.js'; import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; import { Scene } from './scenes/scene.js'; +import { ShapeCollisionsScene } from './scenes/shape-collisions.js'; import { createScene } from './scenes/shapes.js'; import { RepeatingArray } from '@/utils/fixed-array.js'; @@ -53,9 +55,9 @@ function drawFps(last100: RepeatingArray, canvas: HTMLCanvasElement): vo { const x = 8; const y = 0; - ctx.font = '14px monospace'; + // Measure the text strings so can figure out positioning. const fpsStr = `avg fps ${fps.toFixed(0)} (${avgFrame.toFixed(2)}ms / ${avgUpdate.toFixed(2)}ms update / ${avgDraw.toFixed(2)}ms draw)`; const maxStr = `max fps ${frameMax.toFixed(2)}ms / max update ${updateMax.toFixed(2)}ms / max draw ${drawMax.toFixed(2)}ms`; @@ -65,6 +67,7 @@ function drawFps(last100: RepeatingArray, canvas: HTMLCanvasElement): vo const fpsHeight = fpsSize.fontBoundingBoxAscent + fpsSize.fontBoundingBoxDescent; const maxHeight = maxSize.fontBoundingBoxAscent + maxSize.fontBoundingBoxDescent; + // Draw the strings in a nice lil box ctx.globalAlpha = 0.5; ctx.fillStyle = '#000'; ctx.fillRect(x, y, Math.max(fpsSize.width, maxSize.width), fpsHeight + maxHeight); @@ -89,11 +92,18 @@ function main(): void { const input = new InputManager(); const mouse = new MouseInput(); const renderer = new Renderer2D(); - const activeScene: Scene = createScene({ + const context: SandboxContext = { renderer, mouse, input, - }); + }; + + // const activeScene: Scene = createScene({ + // renderer, + // mouse, + // input, + // }); + const activeScene = ShapeCollisionsScene.Create(context); const tick = (now: number): void => { frameRef = requestAnimationFrame(tick); diff --git a/sandbox/src/2d/renderer/renderer.ts b/sandbox/src/2d/renderer/renderer.ts index 5f4f9af..a665e14 100644 --- a/sandbox/src/2d/renderer/renderer.ts +++ b/sandbox/src/2d/renderer/renderer.ts @@ -1,7 +1,12 @@ +import { makeLogger } from '@stdlib/logging/logger'; +import { Vector2 } from '@stdlib/math/vector2'; + import { Scene } from '../scenes/scene'; import { defaultRenderSettings, RenderSettings } from './render-settings'; +const Log = makeLogger('renderer2d'); + function resizeCanvas(canvas: HTMLCanvasElement, width: number, height: number): void { const { devicePixelRatio } = window; @@ -62,7 +67,7 @@ export class Renderer2D { public attach(): void { if (this.#_unbindCallback !== null) { - console.warn('Attempting to attach already attached renderer'); + Log.warn('Attempting to attach already attached renderer'); return; } @@ -74,7 +79,7 @@ export class Renderer2D { public detach(): void { if (this.#_unbindCallback === null) { - console.warn('Attempting to detach non-attached renderer'); + Log.warn('Attempting to detach non-attached renderer'); return; } @@ -109,4 +114,11 @@ export class Renderer2D { } this.#_ctx.restore(); } + + public getScreenToWorldSpace(screenPos: Vector2): Vector2 { + const { width, height } = this.#_canvas; + // const { pixelsPerUnit } = this.settings; + + return new Vector2(width * 0.5 + screenPos.x, height * 0.5 + screenPos.y); + } } diff --git a/sandbox/src/2d/scenes/shape-collisions.ts b/sandbox/src/2d/scenes/shape-collisions.ts index e69de29..67df6de 100644 --- a/sandbox/src/2d/scenes/shape-collisions.ts +++ b/sandbox/src/2d/scenes/shape-collisions.ts @@ -0,0 +1,170 @@ +import { IAABB2D, ICircle } from '@stdlib/geometry/primitives.js'; +import { transformRange } from '@stdlib/math/utils.js'; +import { Vector2 } from '@stdlib/math/vector2.js'; + +import { SandboxContext } from '../context.js'; +import { drawAABB } from '../drawables/aabb.js'; +import { drawCircle } from '../drawables/circle.js'; +import { drawGrid, IDrawableGrid } from '../drawables/grid.js'; +import { MouseInput } from '../input/mouse.js'; +import { RenderSettings } from '../renderer/render-settings.js'; +import { Renderer2D } from '../renderer/renderer.js'; + +import { Scene } from './scene.js'; + +const RECT_COUNT = 5; +const CIRCLE_COUNT = 3; +const SHAPE_MIN_SIZE = 0.5; +const SHAPE_MAX_SIZE = 3; + +const SHAPE_COLOR = '#0000ff'; +const SHAPE_COLLISION_COLOR = '#ff0000'; +const POINTER_COLOR = '#fff'; +const POINTER_COLLISION_COLOR = '#00ff00'; + +function getRandomRect(position: Vector2): IAABB2D { + const widthSeed = Math.random(); + const heightSeed = Math.random(); + const halfWidth = transformRange(widthSeed, 0, 1, SHAPE_MIN_SIZE, SHAPE_MAX_SIZE) * 0.5; + const halfHeight = + transformRange(heightSeed, 0, 1, SHAPE_MIN_SIZE, SHAPE_MAX_SIZE) * 0.5; + + return { + min: { + x: position.x - halfWidth, + y: position.y - halfHeight, + }, + max: { + x: position.x + halfWidth, + y: position.y + halfHeight, + }, + }; +} + +function getRandomCircle(position: Vector2): ICircle { + const seed = Math.random(); + const radius = transformRange(seed, 0, 1, SHAPE_MIN_SIZE, SHAPE_MAX_SIZE); + + return { + position, + radius, + }; +} + +export class ShapeCollisionsScene implements Scene { + public readonly cameraOrigin: Vector2; + + #_rects: IAABB2D[]; + #_rectsState: boolean[]; + #_circles: ICircle[]; + #_circlesState: boolean[]; + #_pointer: ICircle; + #_collision: boolean; + #_grid: IDrawableGrid; + + #_renderer: Renderer2D; + #_mouseInput: MouseInput; + + constructor({ renderer, mouse }: SandboxContext) { + this.#_renderer = renderer; + this.#_mouseInput = mouse; + this.cameraOrigin = new Vector2(); + + this.#_collision = false; + this.#_pointer = { + position: new Vector2(0, 0), + radius: 1, + }; + + const { width, height } = renderer.getCanvas(); + const maxWorldUnitsX = width / renderer.settings.pixelsPerUnit; + const maxWorldUnitsY = height / renderer.settings.pixelsPerUnit; + + const halfMaxX = maxWorldUnitsX * 0.5; + const halfMaxY = maxWorldUnitsY * 0.5; + + this.#_grid = { + drawType: 'grid', + color: '#fff', + gridColor: '#222', + range: new Vector2(halfMaxX, halfMaxY), + }; + + this.#_rects = new Array(RECT_COUNT); + this.#_rectsState = new Array(RECT_COUNT).fill(false); + for (let i = 0; i < this.#_rects.length; i++) { + const xSeed = Math.random(); + const ySeed = Math.random(); + const pos = new Vector2( + transformRange(xSeed, 0, 1, -halfMaxX, halfMaxX), + transformRange(ySeed, 0, 1, -halfMaxY, halfMaxY), + ); + this.#_rects[i] = getRandomRect(pos); + } + + this.#_circles = new Array(CIRCLE_COUNT); + this.#_circlesState = new Array(CIRCLE_COUNT).fill(false); + for (let i = 0; i < this.#_circles.length; i++) { + const xSeed = Math.random(); + const ySeed = Math.random(); + const pos = new Vector2( + transformRange(xSeed, 0, 1, -halfMaxX, halfMaxX), + transformRange(ySeed, 0, 1, -halfMaxY, halfMaxY), + ); + this.#_circles[i] = getRandomCircle(pos); + } + } + + public tick(_now: number): void { + // TODO: This needs to be translated to world space + const { pixelsPerUnit } = this.#_renderer.settings; + const { mousePosition } = this.#_mouseInput; + const worldPos = this.#_renderer.getScreenToWorldSpace( + mousePosition.copy().divideScalar(pixelsPerUnit), + ); + this.#_pointer.position.x = worldPos.x; + this.#_pointer.position.y = worldPos.y; + + // TODO: Check collision + } + + public render(context: CanvasRenderingContext2D, settings: RenderSettings): void { + drawGrid(context, settings, this.#_grid); + + // Draw rects + for (let i = 0; i < this.#_rects.length; i++) { + drawAABB(context, settings, { + drawType: 'aabb', + aabb: this.#_rects[i], + fill: 'transparent', + stroke: this.#_rectsState[i] ? SHAPE_COLLISION_COLOR : SHAPE_COLOR, + }); + } + + // Draw circles + for (let i = 0; i < this.#_circles.length; i++) { + drawCircle(context, settings, { + drawType: 'circle', + circle: this.#_circles[i], + fill: 'transparent', + stroke: this.#_circlesState[i] ? SHAPE_COLLISION_COLOR : SHAPE_COLOR, + }); + } + + // Draw the pointer + drawCircle(context, settings, { + drawType: 'circle', + circle: this.#_pointer, + fill: 'transparent', + stroke: this.#_collision ? POINTER_COLLISION_COLOR : POINTER_COLOR, + }); + } + + public cleanup(): void { + // todo + } + + public static Create(context: SandboxContext): ShapeCollisionsScene { + return new ShapeCollisionsScene(context); + } +} From 706b934c4a19577fcbcbfda89b2e4515a8eb708e Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Mon, 10 Nov 2025 11:13:42 +0200 Subject: [PATCH 27/31] Get shape collision scene working, fix AABB rendering bug --- sandbox/src/2d/drawables/aabb.ts | 25 ++++---- sandbox/src/2d/drawables/circle.ts | 40 ++++++------- sandbox/src/2d/drawables/grid.ts | 72 ++++++++++++----------- sandbox/src/2d/main.ts | 2 - sandbox/src/2d/renderer/renderer.ts | 18 +++++- sandbox/src/2d/scenes/shape-collisions.ts | 34 ++++++++--- 6 files changed, 111 insertions(+), 80 deletions(-) diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts index a834180..c4bb8ad 100644 --- a/sandbox/src/2d/drawables/aabb.ts +++ b/sandbox/src/2d/drawables/aabb.ts @@ -25,18 +25,21 @@ export function drawAABB( ); ctx.save(); - ctx.fillStyle = drawable.fill; - ctx.strokeStyle = drawable.stroke ?? 'transparent'; + { + ctx.fillStyle = drawable.fill; + ctx.strokeStyle = drawable.stroke ?? 'transparent'; - ctx.translate(position.x * pixelsPerUnit, position.y * pixelsPerUnit); - ctx.rect( - -halfSize.x * pixelsPerUnit, - -halfSize.y * pixelsPerUnit, - size.x * pixelsPerUnit, - size.y * pixelsPerUnit, - ); + ctx.translate(position.x * pixelsPerUnit, position.y * pixelsPerUnit); + ctx.beginPath(); + ctx.rect( + -halfSize.x * pixelsPerUnit, + -halfSize.y * pixelsPerUnit, + size.x * pixelsPerUnit, + size.y * pixelsPerUnit, + ); - ctx.fill(); - ctx.stroke(); + ctx.fill(); + ctx.stroke(); + } ctx.restore(); } diff --git a/sandbox/src/2d/drawables/circle.ts b/sandbox/src/2d/drawables/circle.ts index 2b37a70..bda4f95 100644 --- a/sandbox/src/2d/drawables/circle.ts +++ b/sandbox/src/2d/drawables/circle.ts @@ -17,25 +17,25 @@ export function drawCircle( drawable: IDrawableCircle, ): void { ctx.save(); - - ctx.fillStyle = drawable.fill; - ctx.strokeStyle = drawable.stroke ?? 'transparent'; - - const { pixelsPerUnit } = settings; - ctx.translate( - drawable.circle.position.x * pixelsPerUnit, - drawable.circle.position.y * pixelsPerUnit, - ); - - ctx.beginPath(); - ctx.arc(0, 0, drawable.circle.radius * pixelsPerUnit, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - - ctx.beginPath(); - ctx.arc(0, 0, CENTER_POINT_RADIUS, 0, Math.PI * 2); - ctx.fill(); - ctx.stroke(); - + { + ctx.fillStyle = drawable.fill; + ctx.strokeStyle = drawable.stroke ?? 'transparent'; + + const { pixelsPerUnit } = settings; + ctx.translate( + drawable.circle.position.x * pixelsPerUnit, + drawable.circle.position.y * pixelsPerUnit, + ); + + ctx.beginPath(); + ctx.arc(0, 0, drawable.circle.radius * pixelsPerUnit, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + + ctx.beginPath(); + ctx.arc(0, 0, CENTER_POINT_RADIUS, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } ctx.restore(); } diff --git a/sandbox/src/2d/drawables/grid.ts b/sandbox/src/2d/drawables/grid.ts index 769542c..23f5894 100644 --- a/sandbox/src/2d/drawables/grid.ts +++ b/sandbox/src/2d/drawables/grid.ts @@ -17,51 +17,53 @@ export function drawGrid( const { pixelsPerUnit } = settings; ctx.save(); - ctx.translate(0, 0); + { + ctx.translate(0, 0); - // First draw the subgrid - ctx.beginPath(); - ctx.lineWidth = 1; - ctx.strokeStyle = drawable.gridColor; + // First draw the subgrid + ctx.beginPath(); + ctx.lineWidth = 1; + ctx.strokeStyle = drawable.gridColor; - // TODO: Use pattern here instead? - for (let x = 1; x < drawable.range.x; x++) { - const scaledX = x * pixelsPerUnit; - const maxY = drawable.range.y * pixelsPerUnit; + // TODO: Use pattern here instead? + for (let x = 1; x < drawable.range.x; x++) { + const scaledX = x * pixelsPerUnit; + const maxY = drawable.range.y * pixelsPerUnit; - ctx.moveTo(scaledX, maxY); - ctx.lineTo(scaledX, -maxY); - ctx.moveTo(-scaledX, maxY); - ctx.lineTo(-scaledX, -maxY); - } + ctx.moveTo(scaledX, maxY); + ctx.lineTo(scaledX, -maxY); + ctx.moveTo(-scaledX, maxY); + ctx.lineTo(-scaledX, -maxY); + } - for (let y = 1; y < drawable.range.y; y++) { - const scaledY = y * pixelsPerUnit; - const maxX = drawable.range.x * pixelsPerUnit; + for (let y = 1; y < drawable.range.y; y++) { + const scaledY = y * pixelsPerUnit; + const maxX = drawable.range.x * pixelsPerUnit; - ctx.moveTo(-maxX, scaledY); - ctx.lineTo(maxX, scaledY); - ctx.moveTo(-maxX, -scaledY); - ctx.lineTo(maxX, -scaledY); - } + ctx.moveTo(-maxX, scaledY); + ctx.lineTo(maxX, scaledY); + ctx.moveTo(-maxX, -scaledY); + ctx.lineTo(maxX, -scaledY); + } - ctx.stroke(); + ctx.stroke(); + } ctx.restore(); // Then draw the main axis ctx.save(); + { + ctx.lineWidth = 2; + ctx.strokeStyle = drawable.color; + ctx.fillStyle = drawable.color; + ctx.setLineDash([]); - ctx.lineWidth = 2; - ctx.strokeStyle = drawable.color; - ctx.fillStyle = drawable.color; - ctx.setLineDash([]); - - ctx.beginPath(); - ctx.moveTo(-drawable.range.x * pixelsPerUnit, 0); - ctx.lineTo(drawable.range.x * pixelsPerUnit, 0); - ctx.moveTo(0, -drawable.range.y * pixelsPerUnit); - ctx.lineTo(0, drawable.range.y * pixelsPerUnit); - ctx.stroke(); - + ctx.beginPath(); + ctx.moveTo(-drawable.range.x * pixelsPerUnit, 0); + ctx.lineTo(drawable.range.x * pixelsPerUnit, 0); + ctx.moveTo(0, -drawable.range.y * pixelsPerUnit); + ctx.lineTo(0, drawable.range.y * pixelsPerUnit); + ctx.stroke(); + } ctx.restore(); } diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index a9275cb..eb3512f 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -4,9 +4,7 @@ import { SandboxContext } from './context.js'; import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; -import { Scene } from './scenes/scene.js'; import { ShapeCollisionsScene } from './scenes/shape-collisions.js'; -import { createScene } from './scenes/shapes.js'; import { RepeatingArray } from '@/utils/fixed-array.js'; diff --git a/sandbox/src/2d/renderer/renderer.ts b/sandbox/src/2d/renderer/renderer.ts index a665e14..79f6773 100644 --- a/sandbox/src/2d/renderer/renderer.ts +++ b/sandbox/src/2d/renderer/renderer.ts @@ -1,4 +1,5 @@ import { makeLogger } from '@stdlib/logging/logger'; +import { transformRange } from '@stdlib/math/utils'; import { Vector2 } from '@stdlib/math/vector2'; import { Scene } from '../scenes/scene'; @@ -115,10 +116,21 @@ export class Renderer2D { this.#_ctx.restore(); } - public getScreenToWorldSpace(screenPos: Vector2): Vector2 { + public getScreenToWorldSpace(scene: Scene, screenPos: Vector2): Vector2 { const { width, height } = this.#_canvas; - // const { pixelsPerUnit } = this.settings; + const { pixelsPerUnit } = this.settings; - return new Vector2(width * 0.5 + screenPos.x, height * 0.5 + screenPos.y); + const maxWorldUnitsX = width / pixelsPerUnit; + const maxWorldUnitsY = height / pixelsPerUnit; + + const minScreenX = scene.cameraOrigin.x - maxWorldUnitsX * 0.5; + const minScreenY = scene.cameraOrigin.y - maxWorldUnitsY * 0.5; + const maxScreenX = scene.cameraOrigin.x + maxWorldUnitsX * 0.5; + const maxScreenY = scene.cameraOrigin.y + maxWorldUnitsY * 0.5; + + const x = transformRange(screenPos.x, 0, width, minScreenX, maxScreenX); + const y = transformRange(screenPos.y, 0, height, minScreenY, maxScreenY); + + return new Vector2(x, -y); } } diff --git a/sandbox/src/2d/scenes/shape-collisions.ts b/sandbox/src/2d/scenes/shape-collisions.ts index 67df6de..af5c0a9 100644 --- a/sandbox/src/2d/scenes/shape-collisions.ts +++ b/sandbox/src/2d/scenes/shape-collisions.ts @@ -1,3 +1,7 @@ +import { + aabbIntersectsCircle2D, + circleIntersectsCircle2D, +} from '@stdlib/geometry/collisions2d.js'; import { IAABB2D, ICircle } from '@stdlib/geometry/primitives.js'; import { transformRange } from '@stdlib/math/utils.js'; import { Vector2 } from '@stdlib/math/vector2.js'; @@ -13,7 +17,7 @@ import { Renderer2D } from '../renderer/renderer.js'; import { Scene } from './scene.js'; const RECT_COUNT = 5; -const CIRCLE_COUNT = 3; +const CIRCLE_COUNT = 5; const SHAPE_MIN_SIZE = 0.5; const SHAPE_MAX_SIZE = 3; @@ -96,8 +100,8 @@ export class ShapeCollisionsScene implements Scene { const xSeed = Math.random(); const ySeed = Math.random(); const pos = new Vector2( - transformRange(xSeed, 0, 1, -halfMaxX, halfMaxX), - transformRange(ySeed, 0, 1, -halfMaxY, halfMaxY), + transformRange(xSeed, 0, 1, -halfMaxX + 20, halfMaxX - 20), + transformRange(ySeed, 0, 1, -halfMaxY + 20, halfMaxY - 20), ); this.#_rects[i] = getRandomRect(pos); } @@ -116,16 +120,28 @@ export class ShapeCollisionsScene implements Scene { } public tick(_now: number): void { - // TODO: This needs to be translated to world space - const { pixelsPerUnit } = this.#_renderer.settings; const { mousePosition } = this.#_mouseInput; - const worldPos = this.#_renderer.getScreenToWorldSpace( - mousePosition.copy().divideScalar(pixelsPerUnit), - ); + const worldPos = this.#_renderer.getScreenToWorldSpace(this, mousePosition); + this.#_pointer.position.x = worldPos.x; this.#_pointer.position.y = worldPos.y; - // TODO: Check collision + let hadIntersect = false; + + for (let i = 0; i < this.#_rects.length; i++) { + this.#_rectsState[i] = aabbIntersectsCircle2D(this.#_rects[i], this.#_pointer); + if (this.#_rectsState[i]) hadIntersect = true; + } + + for (let i = 0; i < this.#_circles.length; i++) { + this.#_circlesState[i] = circleIntersectsCircle2D( + this.#_circles[i], + this.#_pointer, + ); + if (this.#_circlesState[i]) hadIntersect = true; + } + + this.#_collision = hadIntersect; } public render(context: CanvasRenderingContext2D, settings: RenderSettings): void { From 61d8080cca866c63b1fbf405af05aa03d3a2a7f4 Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Thu, 4 Dec 2025 05:09:13 +0200 Subject: [PATCH 28/31] Scaffold ball physics --- sandbox/src/2d/main.ts | 4 ++- sandbox/src/2d/scenes/ball-physics.ts | 48 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 sandbox/src/2d/scenes/ball-physics.ts diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index eb3512f..63fbc0c 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -4,6 +4,7 @@ import { SandboxContext } from './context.js'; import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; +import { BallPhysicsScene } from './scenes/ball-physics.js'; import { ShapeCollisionsScene } from './scenes/shape-collisions.js'; import { RepeatingArray } from '@/utils/fixed-array.js'; @@ -101,7 +102,8 @@ function main(): void { // mouse, // input, // }); - const activeScene = ShapeCollisionsScene.Create(context); + //const activeScene = ShapeCollisionsScene.Create(context); + const activeScene = new BallPhysicsScene(context); const tick = (now: number): void => { frameRef = requestAnimationFrame(tick); diff --git a/sandbox/src/2d/scenes/ball-physics.ts b/sandbox/src/2d/scenes/ball-physics.ts new file mode 100644 index 0000000..0f7f1d7 --- /dev/null +++ b/sandbox/src/2d/scenes/ball-physics.ts @@ -0,0 +1,48 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { SandboxContext } from '../context'; +import { drawGrid, IDrawableGrid } from '../drawables/grid'; +import { MouseInput } from '../input/mouse'; +import { RenderSettings } from '../renderer/render-settings'; +import { Renderer2D } from '../renderer/renderer'; + +import { Scene } from './scene'; + +export class BallPhysicsScene implements Scene { + public readonly cameraOrigin: Vector2; + + #_grid: IDrawableGrid; + + #_renderer: Renderer2D; + #_mouseInput: MouseInput; + + constructor({ renderer, mouse }: SandboxContext) { + this.#_renderer = renderer; + this.#_mouseInput = mouse; + this.cameraOrigin = new Vector2(); + + const { width, height } = renderer.getCanvas(); + const maxWorldUnitsX = width / renderer.settings.pixelsPerUnit; + const maxWorldUnitsY = height / renderer.settings.pixelsPerUnit; + + const halfMaxX = maxWorldUnitsX * 0.5; + const halfMaxY = maxWorldUnitsY * 0.5; + + this.#_grid = { + drawType: 'grid', + color: '#fff', + gridColor: '#222', + range: new Vector2(halfMaxX, halfMaxY), + }; + } + + public tick(_now: number): void {} + + public render(context: CanvasRenderingContext2D, settings: RenderSettings): void { + drawGrid(context, settings, this.#_grid); + } + + public cleanup(): void { + throw new Error('Method not implemented.'); + } +} From 7014c3fb877b69e3d28265378d5fe9fe1ec96810 Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Fri, 5 Dec 2025 12:49:25 +0200 Subject: [PATCH 29/31] Scaffold ball physics --- sandbox/src/2d/context.ts | 8 ++ sandbox/src/2d/main.ts | 21 +++- sandbox/src/2d/scenes/ball-physics.ts | 147 +++++++++++++++++++++- sandbox/src/2d/scenes/scene.ts | 4 +- sandbox/src/2d/scenes/shape-collisions.ts | 4 +- 5 files changed, 173 insertions(+), 11 deletions(-) diff --git a/sandbox/src/2d/context.ts b/sandbox/src/2d/context.ts index 681c99f..3c3e327 100644 --- a/sandbox/src/2d/context.ts +++ b/sandbox/src/2d/context.ts @@ -2,8 +2,16 @@ import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; +export interface Time { + frameNumber: number; + timeSinceStart: number; + deltaTime: number; + now: number; +} + export interface SandboxContext { renderer: Renderer2D; mouse: MouseInput; input: InputManager; + time: Time; } diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts index 63fbc0c..7679f8c 100644 --- a/sandbox/src/2d/main.ts +++ b/sandbox/src/2d/main.ts @@ -1,11 +1,11 @@ import { makeLogger } from '@stdlib/logging/logger.js'; -import { SandboxContext } from './context.js'; +import { SandboxContext, Time } from './context.js'; import { InputManager } from './input/manager.js'; import { MouseInput } from './input/mouse.js'; import { Renderer2D } from './renderer/renderer.js'; import { BallPhysicsScene } from './scenes/ball-physics.js'; -import { ShapeCollisionsScene } from './scenes/shape-collisions.js'; +// import { ShapeCollisionsScene } from './scenes/shape-collisions.js'; import { RepeatingArray } from '@/utils/fixed-array.js'; @@ -80,10 +80,11 @@ function drawFps(last100: RepeatingArray, canvas: HTMLCanvasElement): vo } function main(): void { + const startTime = performance.now(); let frameRef = 0; let frameCount = 0; - let frameStart = performance.now(); + let frameStart = startTime; let updateEnd = -1; let drawEnd = -1; @@ -91,10 +92,17 @@ function main(): void { const input = new InputManager(); const mouse = new MouseInput(); const renderer = new Renderer2D(); + const time: Time = { + frameNumber: 0, + timeSinceStart: 0, + deltaTime: 0, + now: frameStart, + }; const context: SandboxContext = { renderer, mouse, input, + time, }; // const activeScene: Scene = createScene({ @@ -107,11 +115,14 @@ function main(): void { const tick = (now: number): void => { frameRef = requestAnimationFrame(tick); - frameCount++; + time.frameNumber = ++frameCount; + time.now = now; + time.timeSinceStart = now - startTime; + time.deltaTime = (drawEnd - frameStart) / 1000; frameStart = now; mouse.tick(frameCount); - activeScene.tick(now); + activeScene.tick(time); updateEnd = performance.now(); renderer.render(activeScene); drawEnd = performance.now(); diff --git a/sandbox/src/2d/scenes/ball-physics.ts b/sandbox/src/2d/scenes/ball-physics.ts index 0f7f1d7..7356884 100644 --- a/sandbox/src/2d/scenes/ball-physics.ts +++ b/sandbox/src/2d/scenes/ball-physics.ts @@ -1,18 +1,73 @@ +import { IAABB2D, ICircle } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; -import { SandboxContext } from '../context'; +import { SandboxContext, Time } from '../context'; +import { drawAABB, IDrawableAABB } from '../drawables/aabb'; +import { drawCircle, IDrawableCircle } from '../drawables/circle'; import { drawGrid, IDrawableGrid } from '../drawables/grid'; +import { drawRay, IDrawableRay } from '../drawables/ray'; import { MouseInput } from '../input/mouse'; import { RenderSettings } from '../renderer/render-settings'; import { Renderer2D } from '../renderer/renderer'; import { Scene } from './scene'; +const RESET_PLANE = -10; +const WALLS_WIDTH = 6; +const ADD_BALL_DELAY_MS = 120; +const GRAVITY_CONSTANT = -7; +const BUMPER_BOUNCE_STRENGTH = 2; + +const BALL_MAX_SPEED = 5; + +const squareBumpersPos = [new Vector2(3, 0), new Vector2(-3, -4)]; +const circleBumpersPos = [new Vector2(0, -2), new Vector2(-3, 0), new Vector2(3, -4)]; + +interface BallState { + circle: ICircle; + velocity: Vector2; + direction: Vector2; + launchFrame: number; + launchStrength: number; + bumperBounceFrame: number; +} + +const createWall = (aabb: IAABB2D): IDrawableAABB => ({ + drawType: 'aabb', + aabb, + fill: '#aa3', +}); + +const createCircle = (circle: ICircle): IDrawableCircle => ({ + drawType: 'circle', + circle, + fill: '#a35', +}); + +const drawCircleBumper = (circle: ICircle): IDrawableCircle => ({ + drawType: 'circle', + circle, + fill: '#838', +}); + +const drawSquareBumper = (aabb: IAABB2D): IDrawableAABB => ({ + drawType: 'aabb', + aabb, + fill: '#838', +}); + export class BallPhysicsScene implements Scene { public readonly cameraOrigin: Vector2; + #_resetLine: IDrawableRay; #_grid: IDrawableGrid; + #_squareBumpers: IAABB2D[]; + #_circleBumpers: ICircle[]; + #_walls: IAABB2D[]; + #_balls: BallState[]; + + #_lastBallAdded: number; #_renderer: Renderer2D; #_mouseInput: MouseInput; @@ -21,6 +76,9 @@ export class BallPhysicsScene implements Scene { this.#_mouseInput = mouse; this.cameraOrigin = new Vector2(); + this.#_lastBallAdded = -1; + this.#_balls = []; + const { width, height } = renderer.getCanvas(); const maxWorldUnitsX = width / renderer.settings.pixelsPerUnit; const maxWorldUnitsY = height / renderer.settings.pixelsPerUnit; @@ -34,15 +92,100 @@ export class BallPhysicsScene implements Scene { gridColor: '#222', range: new Vector2(halfMaxX, halfMaxY), }; + + this.#_resetLine = { + drawType: 'ray', + color: '#aa3', + ray: { + position: new Vector2(halfMaxX, RESET_PLANE), + direction: Vector2.Left(), + }, + }; + + this.#_walls = [ + { + min: new Vector2(-halfMaxX, -halfMaxY), + max: new Vector2(-halfMaxX + WALLS_WIDTH, halfMaxY), + }, + { + min: new Vector2(halfMaxX - WALLS_WIDTH, -halfMaxY), + max: new Vector2(halfMaxX, halfMaxY), + }, + ]; + + this.#_circleBumpers = circleBumpersPos.map((position) => ({ + position, + radius: 0.75, + })); + + this.#_squareBumpers = squareBumpersPos.map((pos) => ({ + min: new Vector2(pos.x - 0.75, pos.y - 0.75), + max: new Vector2(pos.x + 0.75, pos.y + 0.75), + })); } - public tick(_now: number): void {} + public tick(time: Time): void { + // Spawn a ball if there has been a click. + const addBallCdEnd = this.#_lastBallAdded + ADD_BALL_DELAY_MS; + if (this.#_mouseInput.getMouse1Pressed() && addBallCdEnd < time.now) { + const { mousePosition } = this.#_mouseInput; + const worldPos = this.#_renderer.getScreenToWorldSpace(this, mousePosition); + this.#spawnBall(worldPos); + this.#_lastBallAdded = time.now; + } + + const balls = this.#_balls.splice(0, this.#_balls.length); + for (const ball of balls) { + this.#physicsTick(ball, time); + ball.circle.position.x += ball.velocity.x; + ball.circle.position.y += ball.velocity.y; + + if (ball.circle.position.y > RESET_PLANE) { + this.#_balls.push(ball); + } + } + } public render(context: CanvasRenderingContext2D, settings: RenderSettings): void { drawGrid(context, settings, this.#_grid); + drawRay(context, settings, this.#_resetLine); + + for (const wall of this.#_walls) { + drawAABB(context, settings, createWall(wall)); + } + + for (const bumper of this.#_circleBumpers) { + drawCircle(context, settings, drawCircleBumper(bumper)); + } + + for (const bumper of this.#_squareBumpers) { + drawAABB(context, settings, drawSquareBumper(bumper)); + } + + for (const ball of this.#_balls) { + drawCircle(context, settings, createCircle(ball.circle)); + } } public cleanup(): void { throw new Error('Method not implemented.'); } + + #physicsTick(ball: BallState, time: Time): void { + ball.velocity.y += GRAVITY_CONSTANT * time.deltaTime; + } + + #spawnBall(position: Vector2): void { + this.#_balls.push({ + circle: { + position, + radius: 0.45, + }, + velocity: Vector2.Zero(), + direction: Vector2.Zero(), + launchFrame: 0, + launchStrength: 1, + bumperBounceFrame: -1, + }); + } } diff --git a/sandbox/src/2d/scenes/scene.ts b/sandbox/src/2d/scenes/scene.ts index b4d931c..63c0bc6 100644 --- a/sandbox/src/2d/scenes/scene.ts +++ b/sandbox/src/2d/scenes/scene.ts @@ -1,6 +1,6 @@ import type { Vector2 } from '@stdlib/math/vector2'; -import type { SandboxContext } from '../context'; +import type { SandboxContext, Time } from '../context'; import type { RenderSettings } from '../renderer/render-settings'; export interface SceneObject { @@ -9,7 +9,7 @@ export interface SceneObject { export interface Scene { cameraOrigin: Vector2; - tick(now: number): void; + tick(time: Time): void; render(context: CanvasRenderingContext2D, settings: RenderSettings): void; cleanup(): void; } diff --git a/sandbox/src/2d/scenes/shape-collisions.ts b/sandbox/src/2d/scenes/shape-collisions.ts index af5c0a9..90fcb50 100644 --- a/sandbox/src/2d/scenes/shape-collisions.ts +++ b/sandbox/src/2d/scenes/shape-collisions.ts @@ -6,7 +6,7 @@ import { IAABB2D, ICircle } from '@stdlib/geometry/primitives.js'; import { transformRange } from '@stdlib/math/utils.js'; import { Vector2 } from '@stdlib/math/vector2.js'; -import { SandboxContext } from '../context.js'; +import { SandboxContext, Time } from '../context.js'; import { drawAABB } from '../drawables/aabb.js'; import { drawCircle } from '../drawables/circle.js'; import { drawGrid, IDrawableGrid } from '../drawables/grid.js'; @@ -119,7 +119,7 @@ export class ShapeCollisionsScene implements Scene { } } - public tick(_now: number): void { + public tick(_time: Time): void { const { mousePosition } = this.#_mouseInput; const worldPos = this.#_renderer.getScreenToWorldSpace(this, mousePosition); From 58f38a81ff0064e42c788fc31dc2c246b4afd7ff Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Fri, 5 Dec 2025 14:46:19 +0200 Subject: [PATCH 30/31] Update ball physics --- sandbox/src/2d/scenes/ball-physics.ts | 60 +++++++++++++++++++++++---- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/sandbox/src/2d/scenes/ball-physics.ts b/sandbox/src/2d/scenes/ball-physics.ts index 7356884..c50055d 100644 --- a/sandbox/src/2d/scenes/ball-physics.ts +++ b/sandbox/src/2d/scenes/ball-physics.ts @@ -1,3 +1,7 @@ +import { + aabbIntersectsCircle2D, + circleIntersectsCircle2D, +} from '@stdlib/geometry/collisions2d'; import { IAABB2D, ICircle } from '@stdlib/geometry/primitives'; import { Vector2 } from '@stdlib/math/vector2'; @@ -130,15 +134,27 @@ export class BallPhysicsScene implements Scene { if (this.#_mouseInput.getMouse1Pressed() && addBallCdEnd < time.now) { const { mousePosition } = this.#_mouseInput; const worldPos = this.#_renderer.getScreenToWorldSpace(this, mousePosition); - this.#spawnBall(worldPos); + const ball = this.#spawnBall(worldPos); + ball.velocity.set(-350.0, 45.0); + ball.direction = Vector2.Normalize(ball.velocity); this.#_lastBallAdded = time.now; } const balls = this.#_balls.splice(0, this.#_balls.length); for (const ball of balls) { - this.#physicsTick(ball, time); - ball.circle.position.x += ball.velocity.x; - ball.circle.position.y += ball.velocity.y; + this.#physicsTick(ball); + + const nextFrameProjection: ICircle = { + ...ball.circle, + position: new Vector2(ball.circle.position), + }; + + nextFrameProjection.position.x += ball.velocity.x * time.deltaTime; + nextFrameProjection.position.y += ball.velocity.y * time.deltaTime; + this.#checkCollisions(nextFrameProjection); + + ball.circle = nextFrameProjection; + ball.direction = Vector2.Normalize(ball.velocity); if (ball.circle.position.y > RESET_PLANE) { this.#_balls.push(ball); @@ -164,6 +180,16 @@ export class BallPhysicsScene implements Scene { for (const ball of this.#_balls) { drawCircle(context, settings, createCircle(ball.circle)); + + const ballSpeed = ball.velocity.getMagnitude(); + drawRay(context, settings, { + drawType: 'ray', + ray: { + position: ball.circle.position, + direction: ball.direction, + }, + color: '#ff0000', + }); } } @@ -171,12 +197,26 @@ export class BallPhysicsScene implements Scene { throw new Error('Method not implemented.'); } - #physicsTick(ball: BallState, time: Time): void { - ball.velocity.y += GRAVITY_CONSTANT * time.deltaTime; + #checkCollisions(projection: ICircle): void { + for (const bumper of this.#_circleBumpers) { + if (circleIntersectsCircle2D(projection, bumper)) { + console.log('COLLISION WITH CIRCLE'); + } + } + + for (const bumper of this.#_squareBumpers) { + if (aabbIntersectsCircle2D(bumper, projection)) { + console.log('COLLISION WITH SQUARE'); + } + } + } + + #physicsTick(ball: BallState): void { + ball.velocity.y += GRAVITY_CONSTANT; } - #spawnBall(position: Vector2): void { - this.#_balls.push({ + #spawnBall(position: Vector2): BallState { + const state: BallState = { circle: { position, radius: 0.45, @@ -186,6 +226,8 @@ export class BallPhysicsScene implements Scene { launchFrame: 0, launchStrength: 1, bumperBounceFrame: -1, - }); + }; + this.#_balls.push(state); + return state; } } From ed446a0472120bde9dc9bdaee5bdd0d803001085 Mon Sep 17 00:00:00 2001 From: Matt Bengston Date: Fri, 5 Dec 2025 15:22:54 +0200 Subject: [PATCH 31/31] Ref from unity proj --- sandbox/src/2d/scenes/ball-physics.ts | 34 +++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/sandbox/src/2d/scenes/ball-physics.ts b/sandbox/src/2d/scenes/ball-physics.ts index c50055d..759c3d0 100644 --- a/sandbox/src/2d/scenes/ball-physics.ts +++ b/sandbox/src/2d/scenes/ball-physics.ts @@ -27,6 +27,40 @@ const BALL_MAX_SPEED = 5; const squareBumpersPos = [new Vector2(3, 0), new Vector2(-3, -4)]; const circleBumpersPos = [new Vector2(0, -2), new Vector2(-3, 0), new Vector2(3, -4)]; +/* +public void Tick() +{ + for (int i = 0; i < m_LevelState.balls.Count; i++) + { + var ball = m_LevelState.balls[i]; + if (ball == null) + { + Debug.Log("ball is null, skipping"); + continue; + } + + var nextVel = _BallMovement(ball); + ball.direction = Vector3.Normalize(nextVel); + ball.velocity = nextVel; + + var cast = Physics2D.CircleCast(ball.worldPos, 0.25f, ball.direction, Vector2.Distance(ball.worldPos, ball.worldPos + ball.velocity); + + ball.worldPos += ball.velocity * Time.deltaTime; + } +} + +private Vector2 _BallMovement(BallState aState) +{ + // Todo: Need to save when they have collided with stuff last, etc.. + // source movement depends on the user input. We don't have that. + var gravity = gravityStrength * Time.deltaTime * gravityDirection; + var next = aState.velocity + Vector2.ClampMagnitude(gravity, maxBallSpeed); + //var nextDir = next.normalized; + + return next; +} +*/ + interface BallState { circle: ICircle; velocity: Vector2;