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/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/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/.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/.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/.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/.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/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/events/__tests__/event-dispatcher.test.ts b/lib/events/__tests__/event-dispatcher.test.ts new file mode 100644 index 0000000..7ab19c7 --- /dev/null +++ b/lib/events/__tests__/event-dispatcher.test.ts @@ -0,0 +1,117 @@ +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 = (): void => {}; + 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); + }); + + 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 new file mode 100644 index 0000000..d90e1f1 --- /dev/null +++ b/lib/events/event-dispatcher.ts @@ -0,0 +1,188 @@ +export type EventMap = object; +export type EventType = keyof Map; +export type EventPayload< + Map extends EventMap, + Key extends EventType = EventType, +> = Map[Key] extends unknown[] ? Map[Key] : unknown[]; +export type EventListener< + Map extends EventMap, + Key extends EventType = EventType, +> = (...args: EventPayload) => void | Promise; + +type LazyListenerMap = { + [K in keyof Map]?: Set>; +}; + +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 = {}; + + #_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, + 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; + } + + /** + * 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, + ): void { + const listeners = this.#_listeners[event]; + if (listeners?.delete(listener)) { + this.#_listenerCount = Math.max(this.#_listenerCount - 1, 0); + } + + const autoRemoveListeners = this.#_autoRemoveListeners[event]; + autoRemoveListeners?.delete(listener); + + 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(); + } + + for (const event in this.#_autoRemoveListeners) { + this.#_autoRemoveListeners[event]?.clear(); + } + + this.#_autoRemoveListeners = {}; + this.#_listeners = {}; + + this.#_events.clear(); + 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 + ): void { + const listeners = this.#_listeners[event]; + + listeners?.forEach((listener) => { + void listener(...payload); + + if (this.#_autoRemoveListeners[event]?.has(listener)) { + this.removeListener(event, listener); + } + }); + } + + /** + * 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]; + } + + #_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/lib/geometry/__tests__/collisions2d.test.ts b/lib/geometry/__tests__/collisions2d.test.ts new file mode 100644 index 0000000..e99eee4 --- /dev/null +++ b/lib/geometry/__tests__/collisions2d.test.ts @@ -0,0 +1,420 @@ +import { describe, it, expect } from 'vitest'; + +import { Vector2 } from '../../math/vector2.js'; +import { + aabbContainsPoint2D, + aabbIntersectsAabb2D, + aabbIntersectsCircle2D, + circleContainsPoint2D, + circleIntersectsCircle2D, + closestPointOnAabb2D, + closestPointOnCircle2D, + closestPointOnRay2D, + isPointOnRay2D, + rayIntersectsAabb2D, + rayIntersectsCircle2D, +} from '../collisions2d.js'; +import type { IAABB2D, ICircle, IRay2D } 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), + }; + + 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', () => { + 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', () => { + 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(-3, -3))).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); + }); + }); + + describe('closestPointOnCircle2D', () => { + 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); + }); + }); + + 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); + + 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); + }); + }); + + 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); + + 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/__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/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/collisions2d.ts b/lib/geometry/collisions2d.ts new file mode 100644 index 0000000..3f83543 --- /dev/null +++ b/lib/geometry/collisions2d.ts @@ -0,0 +1,180 @@ +import { clamp, EPSILON } from '../math/utils.js'; +import { IVec2, Vector2 } from '../math/vector2.js'; + +import { IAABB2D, ICircle, IRay2D } 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 circleContainsPoint2D(circle: ICircle, point: IVec2): boolean { + const diff = Vector2.Subtract(circle.position, point); + 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) { + 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 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, 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 { + // 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, + 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 { + 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; +} 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 new file mode 100644 index 0000000..22ae2e9 --- /dev/null +++ b/lib/geometry/primitives.ts @@ -0,0 +1,32 @@ +import { IVec2 } from '../math/vector2.js'; +import { IVec3 } from '../math/vector3.js'; + +export interface IAABB { + 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; +} + +export interface IRay2D { + readonly position: IVec2; + readonly direction: IVec2; +} + +export interface IRay3D { + readonly position: IVec3; + readonly direction: IVec3; +} 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/__tests__/vector2.test.ts b/lib/math/__tests__/vector2.test.ts new file mode 100644 index 0000000..136f38c --- /dev/null +++ b/lib/math/__tests__/vector2.test.ts @@ -0,0 +1,229 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +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); + 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('.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); + const equal = new Vector2(300, 300); + + expect(original.equals(notEqual)).toEqual(false); + expect(original.equals(equal)).toEqual(true); + }); + + it('.lerp 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('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); + expect(result.x).toEqual(7.5); + expect(result.y).toEqual(7.5); + }); + + 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('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); + 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 new file mode 100644 index 0000000..6437411 --- /dev/null +++ b/lib/math/__tests__/vector3.test.ts @@ -0,0 +1,269 @@ +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); + 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('.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); + 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('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); + + expect(result.x).toEqual(7.5); + expect(result.y).toEqual(7.5); + expect(result.z).toEqual(7.5); + }); + + 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); + + const v2 = new Vector3(10, 5, 5); + expect(Vector3.Normalize(v2).getLength()).toBeCloseTo(1); + }); + + 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); + expect(Vector3.IsVec3Like(new Vector3(5, 5, 5))).toEqual(true); + }); + }); +}); diff --git a/lib/math/utils.ts b/lib/math/utils.ts index 558ef57..5c832c1 100644 --- a/lib/math/utils.ts +++ b/lib/math/utils.ts @@ -1,10 +1,12 @@ +export const EPSILON = 0.000001; + /** * 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 +15,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 +41,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 +54,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); diff --git a/lib/math/vector2.ts b/lib/math/vector2.ts new file mode 100644 index 0000000..332336e --- /dev/null +++ b/lib/math/vector2.ts @@ -0,0 +1,328 @@ +import { clamp, lerp } from './utils.js'; + +export interface IVec2 { + x: number; + y: number; +} + +type CtorArgs = [ref: IVec2] | [x?: number, y?: number]; + +export class Vector2 implements IVec2 { + public x = 0; + public y = 0; + + 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 + 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: IVec2, v2: IVec2): 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: IVec2, v2: IVec2): 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: IVec2, v2: IVec2): 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: IVec2, val: number): Vector2 { + return new Vector2(v1).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: IVec2, v2: IVec2): 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: IVec2, v2: IVec2): 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: IVec2, v2: IVec2, t: number): Vector2 { + return new Vector2(lerp(v1.x, v2.x, t), lerp(v1.y, v2.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: IVec2): Vector2 { + 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. + * @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. + */ + 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: IVec2): 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: IVec2): 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: IVec2): 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: IVec2, t: number): this { + this.x += lerp(this.x, target.x, t); + this.y += lerp(this.y, target.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; + } + + /** + * 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. + * @returns If the vectors are equal. + */ + public equals(val: IVec2): 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(): IVec2 { + 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..eade954 --- /dev/null +++ b/lib/math/vector3.ts @@ -0,0 +1,397 @@ +import { clamp, lerp } from './utils.js'; + +export interface IVec3 { + x: number; + y: number; + z: number; +} + +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(...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 + 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: IVec3, v2: IVec3): 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: IVec3, v2: IVec3): 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: IVec3, v2: IVec3): 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: IVec3, val: number): Vector3 { + return new Vector3(v1).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: IVec3, v2: IVec3): 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: IVec3, v2: IVec3): 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: 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)); + } + + /** + * 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: IVec3): Vector3 { + 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. + * @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. + */ + 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: IVec3): 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: IVec3): 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: IVec3): 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: 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); + 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; + } + + /** + * 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. + * @returns If the vectors are equal. + */ + public equals(val: IVec3): 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(): IVec3 { + 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)})`; + } +} 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 diff --git a/lib/time/clock.ts b/lib/time/clock.ts new file mode 100644 index 0000000..e69de29 diff --git a/package.json b/package.json index 546c83e..b7108ba 100644 --- a/package.json +++ b/package.json @@ -16,20 +16,24 @@ "type": "module", "exports": { "./errors/*": { - "default": "./dist/errors/*.js", - "types": "./dist/errors/*.d.ts" + "types": "./dist/errors/*.d.ts", + "default": "./dist/errors/*.js" + }, + "./events/*": { + "types": "./dist/events/*.d.ts", + "default": "./dist/events/*.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 +53,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 +64,8 @@ "eslint": "^9.20.1", "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 c99e77d..b9987e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,30 +10,36 @@ 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.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.0 + version: 2.28.1 '@types/node': specifier: ^22.13.4 - version: 22.13.4 + version: 22.13.10 eslint: specifier: ^9.20.1 - version: 9.20.1 + version: 9.22.0 rimraf: specifier: ^6.0.1 version: 6.0.1 typescript: specifier: ^5.7.3 - version: 5.7.3 + version: 5.8.2 + vite: + specifier: ^6.2.2 + 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.10)) vitest: specifier: ^3.0.6 - version: 3.0.6(@types/node@22.13.4) + 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': @@ -43,8 +49,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 +58,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 +71,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,158 +104,158 @@ 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.1': + resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==} 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.1': + resolution: {integrity: sha512-50tM0zCJW5kGqgG7fQ7IHvQOcAn9TKiVRuQ/lN0xR+T2lzEFvAi1ZcS8DiksFcEpf1t/GYOeOfCAgDHFpkiSmA==} 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.1': + resolution: {integrity: sha512-dp+MshLYux6j/JjdqVLnMglQlFu+MuVeNrmT5nk6q07wNhCdSnB7QZj+7G8VMUGh1q+vj2Bq8kRsuyA00I/k+Q==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.24.2': - resolution: {integrity: sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==} + '@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.24.2': - resolution: {integrity: sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==} + '@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.24.2': - resolution: {integrity: sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==} + '@esbuild/darwin-x64@0.25.1': + resolution: {integrity: sha512-hxVnwL2Dqs3fM1IWq8Iezh0cX7ZGdVhbTfnOy5uURtao5OIVCEyj9xIzemDi7sRvKsuSdtCAhMKarxqtlyVyfA==} 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.1': + resolution: {integrity: sha512-1MrCZs0fZa2g8E+FUo2ipw6jw5qqQiH+tERoS5fAfKnRx6NXH31tXBKI3VpmLijLH6yriMZsxJtaXUyFt/8Y4A==} 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.1': + resolution: {integrity: sha512-0IZWLiTyz7nm0xuIs0q1Y3QWJC52R8aSXxe40VUxm6BB1RNmkODtW6LHvWRrGiICulcX7ZvyH6h5fqdLu4gkww==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.24.2': - resolution: {integrity: sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==} + '@esbuild/linux-arm64@0.25.1': + resolution: {integrity: sha512-jaN3dHi0/DDPelk0nLcXRm1q7DNJpjXy7yWaWvbfkPvI+7XNSc/lDOnCLN7gzsyzgu6qSAmgSvP9oXAhP973uQ==} 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.1': + resolution: {integrity: sha512-NdKOhS4u7JhDKw9G3cY6sWqFcnLITn6SqivVArbzIaf3cemShqfLGHYMx8Xlm/lBit3/5d7kXvriTUGa5YViuQ==} 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.1': + resolution: {integrity: sha512-OJykPaF4v8JidKNGz8c/q1lBO44sQNUQtq1KktJXdBLn1hPod5rE/Hko5ugKKZd+D2+o1a9MFGUEIUwO2YfgkQ==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.24.2': - resolution: {integrity: sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==} + '@esbuild/linux-loong64@0.25.1': + resolution: {integrity: sha512-nGfornQj4dzcq5Vp835oM/o21UMlXzn79KobKlcs3Wz9smwiifknLy4xDCLUU0BWp7b/houtdrgUz7nOGnfIYg==} 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.1': + resolution: {integrity: sha512-1osBbPEFYwIE5IVB/0g2X6i1qInZa1aIoj1TdL4AaAb55xIIgbg8Doq6a5BzYWgr+tEcDzYH67XVnTmUzL+nXg==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.24.2': - resolution: {integrity: sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==} + '@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.24.2': - resolution: {integrity: sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==} + '@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.24.2': - resolution: {integrity: sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==} + '@esbuild/linux-s390x@0.25.1': + resolution: {integrity: sha512-cEECeLlJNfT8kZHqLarDBQso9a27o2Zd2AQ8USAEoGtejOrCYHNtKP8XQhMDJMtthdF4GBmjR2au3x1udADQQQ==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.24.2': - resolution: {integrity: sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==} + '@esbuild/linux-x64@0.25.1': + resolution: {integrity: sha512-xbfUhu/gnvSEg+EGovRc+kjBAkrvtk38RlerAzQxvMzlB4fXpCFCeUAYzJvrnhFtdeyVCDANSjJvOvGYoeKzFA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.24.2': - resolution: {integrity: sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==} + '@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.24.2': - resolution: {integrity: sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==} + '@esbuild/netbsd-x64@0.25.1': + resolution: {integrity: sha512-X53z6uXip6KFXBQ+Krbx25XHV/NCbzryM6ehOAeAil7X7oa4XIq+394PWGnwaSQ2WRA0KI6PUO6hTO5zeF5ijA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.24.2': - resolution: {integrity: sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==} + '@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.24.2': - resolution: {integrity: sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==} + '@esbuild/openbsd-x64@0.25.1': + resolution: {integrity: sha512-T3H78X2h1tszfRSf+txbt5aOp/e7TAz3ptVKu9Oyir3IAOFPGV6O9c2naym5TOriy1l0nNf6a4X5UXRZSGX/dw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/sunos-x64@0.24.2': - resolution: {integrity: sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==} + '@esbuild/sunos-x64@0.25.1': + resolution: {integrity: sha512-2H3RUvcmULO7dIE5EWJH8eubZAI4xw54H1ilJnRNZdeo8dTADEZ21w6J22XBkXqGJbe0+wnNJtw3UXRoLJnFEg==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.24.2': - resolution: {integrity: sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==} + '@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.24.2': - resolution: {integrity: sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==} + '@esbuild/win32-ia32@0.25.1': + resolution: {integrity: sha512-uOxSJCIcavSiT6UnBhBzE8wy3n0hOkJsBOzy7HDAuTDE++1DJMRRVCPGisULScHL+a/ZwdXPpXD3IyFKjA7K8A==} 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.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 @@ -262,24 +268,28 @@ 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/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} - '@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.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': 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 +308,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 +341,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.36.0': + resolution: {integrity: sha512-jgrXjjcEwN6XpZXL0HUeOVGfjXhPyxAbbhD0BlXUB+abTOpbPiN5Wb3kOT7yb+uEtATNYF5x5gIfwutmuBA26w==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.34.8': - resolution: {integrity: sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==} + '@rollup/rollup-android-arm64@4.36.0': + resolution: {integrity: sha512-NyfuLvdPdNUfUNeYKUwPwKsE5SXa2J6bCt2LdB/N+AxShnkpiczi3tcLJrm5mA+eqpy0HmaIY9F6XCa32N5yzg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.34.8': - resolution: {integrity: sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==} + '@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.8': - resolution: {integrity: sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==} + '@rollup/rollup-darwin-x64@4.36.0': + resolution: {integrity: sha512-6c6wMZa1lrtiRsbDziCmjE53YbTkxMYhhnWnSW8R/yqsM7a6mSJ3uAVT0t8Y/DGt7gxUWYuFM4bwWk9XCJrFKA==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.34.8': - resolution: {integrity: sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==} + '@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.8': - resolution: {integrity: sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==} + '@rollup/rollup-freebsd-x64@4.36.0': + resolution: {integrity: sha512-dVeWq1ebbvByI+ndz4IJcD4a09RJgRYmLccwlQ8bPd4olz3Y213uf1iwvc7ZaxNn2ab7bjc08PrtBgMu6nb4pQ==} 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.36.0': + resolution: {integrity: sha512-bvXVU42mOVcF4le6XSjscdXjqx8okv4n5vmwgzcmtvFdifQ5U4dXFYaCB87namDRKlUL9ybVtLQ9ztnawaSzvg==} 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.36.0': + resolution: {integrity: sha512-JFIQrDJYrxOnyDQGYkqnNBtjDwTgbasdbUiQvcU8JmGDfValfH1lNpng+4FWlhaVIR4KPkeddYjsVVbmJYvDcg==} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.34.8': - resolution: {integrity: sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==} + '@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.8': - resolution: {integrity: sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==} + '@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.8': - resolution: {integrity: sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==} + '@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.8': - resolution: {integrity: sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==} + '@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.8': - resolution: {integrity: sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==} + '@rollup/rollup-linux-riscv64-gnu@4.36.0': + resolution: {integrity: sha512-ttE6ayb/kHwNRJGYLpuAvB7SMtOeQnVXEIpMtAvx3kepFQeowVED0n1K9nAdraHUPJ5hydEMxBpIR7o4nrm8uA==} cpu: [riscv64] os: [linux] - '@rollup/rollup-linux-s390x-gnu@4.34.8': - resolution: {integrity: sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==} + '@rollup/rollup-linux-s390x-gnu@4.36.0': + resolution: {integrity: sha512-4a5gf2jpS0AIe7uBjxDeUMNcFmaRTbNv7NxI5xOCs4lhzsVyGR/0qBXduPnoWf6dGC365saTiwag8hP1imTgag==} cpu: [s390x] os: [linux] - '@rollup/rollup-linux-x64-gnu@4.34.8': - resolution: {integrity: sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==} + '@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.8': - resolution: {integrity: sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==} + '@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.8': - resolution: {integrity: sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==} + '@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.8': - resolution: {integrity: sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==} + '@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.8': - resolution: {integrity: sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==} + '@rollup/rollup-win32-x64-msvc@4.36.0': + resolution: {integrity: sha512-aRXd7tRZkWLqGbChgcMMDEHjOKudo1kChb1Jt1IfR8cY/KIpgNviLeJy5FUb9IpSuQj8dU2fAYNMPW/hLKOSTw==} 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 +457,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.10': + resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==} - '@typescript-eslint/eslint-plugin@8.24.1': - resolution: {integrity: sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA==} + '@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.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.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.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.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.24.1': - resolution: {integrity: sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw==} + '@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.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.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.24.1': - resolution: {integrity: sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg==} + '@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.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.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.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.1': + resolution: {integrity: sha512-AjOC3zfnxd6S4Eiy3jwktJPclqhFHNyd8L6Gycf9WUPoKZpgM5PjkxY1X7uSy61xVpiJDhhk7XT2NVsN3ALTWg==} 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.9': + resolution: {integrity: sha512-5eCqRItYgIML7NNVgJj6TVCmdzE7ZVgJhruW0ziSQV4V7PvLkDL1bBkBdcTs/VuIz0IxPb5da1IDSqc1TR9eig==} - '@vitest/mocker@3.0.6': - resolution: {integrity: sha512-KPztr4/tn7qDGZfqlSPQoF2VgJcKxnDNhmfR3VgZ6Fy1bO8T9Fc1stUiTXtqz0yG24VpD00pZP5f8EOFknjNuQ==} + '@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 @@ -511,28 +521,28 @@ packages: vite: optional: true - '@vitest/pretty-format@3.0.6': - resolution: {integrity: sha512-Zyctv3dbNL+67qtHfRnUE/k8qxduOamRfAL1BurEIQSyOEFffoMvx2pnDSSbKAAVxY0Ej2J/GH2dQKI0W2JyVg==} + '@vitest/pretty-format@3.0.9': + resolution: {integrity: sha512-OW9F8t2J3AwFEwENg3yMyKWweF7oRJlMyHOMIhO5F3n0+cgQAJZBjNgrF8dLwFTEXl5jUqBLXd9QyyKv8zEcmA==} - '@vitest/runner@3.0.6': - resolution: {integrity: sha512-JopP4m/jGoaG1+CBqubV/5VMbi7L+NQCJTu1J1Pf6YaUbk7bZtaq5CX7p+8sY64Sjn1UQ1XJparHfcvTTdu9cA==} + '@vitest/runner@3.0.9': + resolution: {integrity: sha512-NX9oUXgF9HPfJSwl8tUZCMP1oGx2+Sf+ru6d05QjzQz4OwWg0psEzwY6VexP2tTHWdOkhKHUIZH+fS6nA7jfOw==} - '@vitest/snapshot@3.0.6': - resolution: {integrity: sha512-qKSmxNQwT60kNwwJHMVwavvZsMGXWmngD023OHSgn873pV0lylK7dwBTfYP7e4URy5NiBCHHiQGA9DHkYkqRqg==} + '@vitest/snapshot@3.0.9': + resolution: {integrity: sha512-AiLUiuZ0FuA+/8i19mTYd+re5jqjEc2jZbgJ2up0VY0Ddyyxg/uUtBDpIFAy4uzKaQxOW8gMgBdAJJ2ydhu39A==} - '@vitest/spy@3.0.6': - resolution: {integrity: sha512-HfOGx/bXtjy24fDlTOpgiAEJbRfFxoX3zIGagCqACkFKKZ/TTOE6gYMKXlqecvxEndKFuNHcHqP081ggZ2yM0Q==} + '@vitest/spy@3.0.9': + resolution: {integrity: sha512-/CcK2UDl0aQ2wtkp3YVWldrpLRNCfVcIOFGlVGKO4R5eajsH393Z1yiXLVQ7vWsj26JOEjeZI0x5sm5P4OGUNQ==} - '@vitest/utils@3.0.6': - resolution: {integrity: sha512-18ktZpf4GQFTbf9jK543uspU03Q2qya7ZGya5yiZ0Gx0nnnalBvd5ZBislbl2EhLjM8A8rt4OilqKG7QwcGkvQ==} + '@vitest/utils@3.0.9': + resolution: {integrity: sha512-ilHM5fHhZ89MCp5aAaM9uhfl1c2JdxVxl3McqsdVyVNN6JffnEen8UMCdRTzOhGXNQGo5GNL9QugHrz727Wnng==} 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 @@ -577,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: @@ -634,8 +644,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 +789,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.1: + resolution: {integrity: sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==} engines: {node: '>=18'} hasBin: true @@ -788,8 +798,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.1.1: + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} hasBin: true peerDependencies: eslint: '>=7.0.0' @@ -848,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: @@ -860,8 +870,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.22.0: + resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -898,8 +908,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 +935,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 +965,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 +992,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: @@ -1019,6 +1029,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'} @@ -1194,8 +1207,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: @@ -1290,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 @@ -1368,8 +1381,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 +1433,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 +1450,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 +1459,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 +1493,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 +1502,8 @@ packages: engines: {node: 20 || >=22} hasBin: true - rollup@4.34.8: - resolution: {integrity: sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==} + rollup@4.36.0: + resolution: {integrity: sha512-zwATAXNQxUcd40zgtQG0ZafcRK4g004WtEl7kbuhTWPvf07PsfohXl39jVUvPF7jvNAIkKPQ2XrsDlWuxBd++Q==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1578,8 +1594,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==} @@ -1665,6 +1681,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==} @@ -1691,15 +1717,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.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 - 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 +1743,21 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} - vite-node@3.0.6: - resolution: {integrity: sha512-s51RzrTkXKJrhNbUzQRsarjmAae7VmMPAsRT7lppVpIg6mK3zGthP9Hgz0YQQKuNcF+Ii7DfYk3Fxz40jRmePw==} + vite-node@3.0.9: + resolution: {integrity: sha512-w3Gdx7jDcuT9cNn9jExXgOyKmf5UOTb6WMHz8LGAm54eS1Elf5OuBhCxl6zJxGhEeIkgsE1WbHuoL0mj/UXqXg==} 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-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} hasBin: true peerDependencies: @@ -1762,16 +1796,16 @@ packages: yaml: optional: true - vitest@3.0.6: - resolution: {integrity: sha512-/iL1Sc5VeDZKPDe58oGK4HUFLhw6b5XdY1MYawjuSaDA4sEfYlY9HnS6aCEG26fX+MgUi7MwlduTBHHAI/OvMA==} + 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.6 - '@vitest/ui': 3.0.6 + '@vitest/browser': 3.0.9 + '@vitest/ui': 3.0.9 happy-dom: '*' jsdom: '*' peerDependenciesMeta: @@ -1802,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: @@ -1834,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.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.1(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)': + dependencies: + '@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.1(eslint@9.22.0)(typescript@5.8.2) transitivePeerDependencies: - '@types/eslint' - '@typescript-eslint/parser' @@ -1857,9 +1891,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 +1920,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 +1944,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 +1972,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 +2033,84 @@ snapshots: human-id: 4.1.1 prettier: 2.8.8 - '@esbuild/aix-ppc64@0.24.2': + '@esbuild/aix-ppc64@0.25.1': optional: true - '@esbuild/android-arm64@0.24.2': + '@esbuild/android-arm64@0.25.1': optional: true - '@esbuild/android-arm@0.24.2': + '@esbuild/android-arm@0.25.1': optional: true - '@esbuild/android-x64@0.24.2': + '@esbuild/android-x64@0.25.1': optional: true - '@esbuild/darwin-arm64@0.24.2': + '@esbuild/darwin-arm64@0.25.1': optional: true - '@esbuild/darwin-x64@0.24.2': + '@esbuild/darwin-x64@0.25.1': optional: true - '@esbuild/freebsd-arm64@0.24.2': + '@esbuild/freebsd-arm64@0.25.1': optional: true - '@esbuild/freebsd-x64@0.24.2': + '@esbuild/freebsd-x64@0.25.1': optional: true - '@esbuild/linux-arm64@0.24.2': + '@esbuild/linux-arm64@0.25.1': optional: true - '@esbuild/linux-arm@0.24.2': + '@esbuild/linux-arm@0.25.1': optional: true - '@esbuild/linux-ia32@0.24.2': + '@esbuild/linux-ia32@0.25.1': optional: true - '@esbuild/linux-loong64@0.24.2': + '@esbuild/linux-loong64@0.25.1': optional: true - '@esbuild/linux-mips64el@0.24.2': + '@esbuild/linux-mips64el@0.25.1': optional: true - '@esbuild/linux-ppc64@0.24.2': + '@esbuild/linux-ppc64@0.25.1': optional: true - '@esbuild/linux-riscv64@0.24.2': + '@esbuild/linux-riscv64@0.25.1': optional: true - '@esbuild/linux-s390x@0.24.2': + '@esbuild/linux-s390x@0.25.1': optional: true - '@esbuild/linux-x64@0.24.2': + '@esbuild/linux-x64@0.25.1': optional: true - '@esbuild/netbsd-arm64@0.24.2': + '@esbuild/netbsd-arm64@0.25.1': optional: true - '@esbuild/netbsd-x64@0.24.2': + '@esbuild/netbsd-x64@0.25.1': optional: true - '@esbuild/openbsd-arm64@0.24.2': + '@esbuild/openbsd-arm64@0.25.1': optional: true - '@esbuild/openbsd-x64@0.24.2': + '@esbuild/openbsd-x64@0.25.1': optional: true - '@esbuild/sunos-x64@0.24.2': + '@esbuild/sunos-x64@0.25.1': optional: true - '@esbuild/win32-arm64@0.24.2': + '@esbuild/win32-arm64@0.25.1': optional: true - '@esbuild/win32-ia32@0.24.2': + '@esbuild/win32-ia32@0.25.1': optional: true - '@esbuild/win32-x64@0.24.2': + '@esbuild/win32-x64@0.25.1': optional: true - '@eslint-community/eslint-utils@4.4.1(eslint@9.20.1)': + '@eslint-community/eslint-utils@4.5.1(eslint@9.22.0)': dependencies: - eslint: 9.20.1 + eslint: 9.22.0 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -2089,11 +2123,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/core@0.11.0': + '@eslint/config-helpers@0.1.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 +2143,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.20.0': {} + '@eslint/js@9.22.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 +2163,7 @@ snapshots: '@humanwhocodes/retry@0.3.1': {} - '@humanwhocodes/retry@0.4.1': {} + '@humanwhocodes/retry@0.4.2': {} '@isaacs/cliui@8.0.2': dependencies: @@ -2142,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 @@ -2166,73 +2202,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.36.0': optional: true - '@rollup/rollup-android-arm64@4.34.8': + '@rollup/rollup-android-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-arm64@4.34.8': + '@rollup/rollup-darwin-arm64@4.36.0': optional: true - '@rollup/rollup-darwin-x64@4.34.8': + '@rollup/rollup-darwin-x64@4.36.0': optional: true - '@rollup/rollup-freebsd-arm64@4.34.8': + '@rollup/rollup-freebsd-arm64@4.36.0': optional: true - '@rollup/rollup-freebsd-x64@4.34.8': + '@rollup/rollup-freebsd-x64@4.36.0': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.34.8': + '@rollup/rollup-linux-arm-gnueabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.34.8': + '@rollup/rollup-linux-arm-musleabihf@4.36.0': optional: true - '@rollup/rollup-linux-arm64-gnu@4.34.8': + '@rollup/rollup-linux-arm64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-arm64-musl@4.34.8': + '@rollup/rollup-linux-arm64-musl@4.36.0': optional: true - '@rollup/rollup-linux-loongarch64-gnu@4.34.8': + '@rollup/rollup-linux-loongarch64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.34.8': + '@rollup/rollup-linux-powerpc64le-gnu@4.36.0': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.34.8': + '@rollup/rollup-linux-riscv64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-s390x-gnu@4.34.8': + '@rollup/rollup-linux-s390x-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-gnu@4.34.8': + '@rollup/rollup-linux-x64-gnu@4.36.0': optional: true - '@rollup/rollup-linux-x64-musl@4.34.8': + '@rollup/rollup-linux-x64-musl@4.36.0': optional: true - '@rollup/rollup-win32-arm64-msvc@4.34.8': + '@rollup/rollup-win32-arm64-msvc@4.36.0': optional: true - '@rollup/rollup-win32-ia32-msvc@4.34.8': + '@rollup/rollup-win32-ia32-msvc@4.36.0': optional: true - '@rollup/rollup-win32-x64-msvc@4.34.8': + '@rollup/rollup-win32-x64-msvc@4.36.0': 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.22.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.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 @@ -2249,132 +2285,132 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@22.13.4': + '@types/node@22.13.10': 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.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.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.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 - 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.1(eslint@9.22.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.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.20.1 - typescript: 5.7.3 + eslint: 9.22.0 + typescript: 5.8.2 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.24.1': + '@typescript-eslint/scope-manager@8.26.1': dependencies: - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/visitor-keys': 8.24.1 + '@typescript-eslint/types': 8.26.1 + '@typescript-eslint/visitor-keys': 8.26.1 - '@typescript-eslint/type-utils@8.24.1(eslint@9.20.1)(typescript@5.7.3)': + '@typescript-eslint/type-utils@8.26.1(eslint@9.22.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.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.20.1 - ts-api-utils: 2.0.1(typescript@5.7.3) - typescript: 5.7.3 + 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.24.1': {} + '@typescript-eslint/types@8.26.1': {} - '@typescript-eslint/typescript-estree@8.24.1(typescript@5.7.3)': + '@typescript-eslint/typescript-estree@8.26.1(typescript@5.8.2)': dependencies: - '@typescript-eslint/types': 8.24.1 - '@typescript-eslint/visitor-keys': 8.24.1 + '@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 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.1(eslint@9.22.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.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.24.1': + '@typescript-eslint/visitor-keys@8.26.1': dependencies: - '@typescript-eslint/types': 8.24.1 + '@typescript-eslint/types': 8.26.1 eslint-visitor-keys: 4.2.0 - '@vitest/expect@3.0.6': + '@vitest/expect@3.0.9': dependencies: - '@vitest/spy': 3.0.6 - '@vitest/utils': 3.0.6 + '@vitest/spy': 3.0.9 + '@vitest/utils': 3.0.9 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.9(vite@6.2.2(@types/node@22.13.10))': dependencies: - '@vitest/spy': 3.0.6 + '@vitest/spy': 3.0.9 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 6.1.0(@types/node@22.13.4) + vite: 6.2.2(@types/node@22.13.10) - '@vitest/pretty-format@3.0.6': + '@vitest/pretty-format@3.0.9': dependencies: tinyrainbow: 2.0.0 - '@vitest/runner@3.0.6': + '@vitest/runner@3.0.9': dependencies: - '@vitest/utils': 3.0.6 + '@vitest/utils': 3.0.9 pathe: 2.0.3 - '@vitest/snapshot@3.0.6': + '@vitest/snapshot@3.0.9': dependencies: - '@vitest/pretty-format': 3.0.6 + '@vitest/pretty-format': 3.0.9 magic-string: 0.30.17 pathe: 2.0.3 - '@vitest/spy@3.0.6': + '@vitest/spy@3.0.9': dependencies: tinyspy: 3.0.2 - '@vitest/utils@3.0.6': + '@vitest/utils@3.0.9': dependencies: - '@vitest/pretty-format': 3.0.6 + '@vitest/pretty-format': 3.0.9 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 +2439,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,14 +2448,15 @@ 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: {} - 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 @@ -2447,7 +2484,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 +2525,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 +2570,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 +2643,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 +2653,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 @@ -2652,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: {} @@ -2667,7 +2704,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 +2718,39 @@ snapshots: is-date-object: 1.1.0 is-symbol: 1.1.1 - esbuild@0.24.2: + esbuild@0.25.1: 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.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.1(eslint@9.20.1): + eslint-config-prettier@10.1.1(eslint@9.22.0): dependencies: - eslint: 9.20.1 + eslint: 9.22.0 eslint-import-resolver-node@0.3.9: dependencies: @@ -2723,28 +2760,28 @@ 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.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.24.1(eslint@9.20.1)(typescript@5.7.3) - eslint: 9.20.1 + '@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.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.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.20.1 + eslint: 9.22.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.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 @@ -2756,27 +2793,27 @@ 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.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.1(eslint@9.20.1))(eslint@9.20.1)(prettier@3.5.1): + 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.20.1 - prettier: 3.5.1 + eslint: 9.22.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.1.1(eslint@9.22.0) - eslint-plugin-promise@7.2.1(eslint@9.20.1): + eslint-plugin-promise@7.2.1(eslint@9.22.0): dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) - eslint: 9.20.1 + '@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 @@ -2785,18 +2822,19 @@ snapshots: eslint-visitor-keys@4.2.0: {} - eslint@9.20.1: + eslint@9.22.0: dependencies: - '@eslint-community/eslint-utils': 4.4.1(eslint@9.20.1) + '@eslint-community/eslint-utils': 4.5.1(eslint@9.22.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/config-helpers': 0.1.0 + '@eslint/core': 0.12.0 + '@eslint/eslintrc': 3.3.0 + '@eslint/js': 9.22.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 @@ -2804,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 @@ -2826,8 +2864,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 +2886,7 @@ snapshots: esutils@2.0.3: {} - expect-type@1.1.0: {} + expect-type@1.2.0: {} extendable-error@0.1.7: {} @@ -2874,9 +2912,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 +2945,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 +2970,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 +2978,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 +2998,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 +3012,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 @@ -2997,6 +3035,8 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + globrex@0.1.2: {} + gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -3049,13 +3089,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 +3106,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 +3117,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 +3149,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 +3165,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,24 +3178,24 @@ 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 is-typed-array@1.1.15: dependencies: - which-typed-array: 1.1.18 + which-typed-array: 1.1.19 is-weakmap@2.0.2: {} 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 +3203,7 @@ snapshots: isexe@2.0.0: {} - jackspeak@4.0.3: + jackspeak@4.1.0: dependencies: '@isaacs/cliui': 8.0.2 @@ -3248,7 +3288,7 @@ snapshots: ms@2.1.3: {} - nanoid@3.3.8: {} + nanoid@3.3.10: {} natural-compare@1.4.0: {} @@ -3259,7 +3299,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 +3321,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 +3340,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 +3370,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,9 +3405,9 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss@8.5.2: + postcss@8.5.3: dependencies: - nanoid: 3.3.8 + nanoid: 3.3.10 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -3377,10 +3419,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 +3441,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 +3466,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.36.0: 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.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: @@ -3461,8 +3505,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 +3517,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 +3532,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 +3562,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 +3600,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.8.0: {} + std-env@3.8.1: {} string-width@4.2.3: dependencies: @@ -3573,7 +3617,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 +3627,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 +3680,13 @@ 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 + + tsconfck@3.1.5(typescript@5.8.2): + optionalDependencies: + typescript: 5.8.2 tsconfig-paths@3.15.0: dependencies: @@ -3655,7 +3703,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 +3734,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.1(eslint@9.22.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.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 - 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 +3761,13 @@ snapshots: dependencies: punycode: 2.3.1 - vite-node@3.0.6(@types/node@22.13.4): + 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.1.0(@types/node@22.13.4) + vite: 6.2.2(@types/node@22.13.10) transitivePeerDependencies: - '@types/node' - jiti @@ -3734,39 +3782,50 @@ snapshots: - tsx - yaml - vite@6.1.0(@types/node@22.13.4): + vite-tsconfig-paths@5.1.4(typescript@5.8.2)(vite@6.2.2(@types/node@22.13.10)): dependencies: - esbuild: 0.24.2 - postcss: 8.5.2 - rollup: 4.34.8 + debug: 4.4.0 + globrex: 0.1.2 + tsconfck: 3.1.5(typescript@5.8.2) optionalDependencies: - '@types/node': 22.13.4 + vite: 6.2.2(@types/node@22.13.10) + transitivePeerDependencies: + - supports-color + - typescript + + vite@6.2.2(@types/node@22.13.10): + dependencies: + esbuild: 0.25.1 + postcss: 8.5.3 + rollup: 4.36.0 + optionalDependencies: + '@types/node': 22.13.10 fsevents: 2.3.3 - vitest@3.0.6(@types/node@22.13.4): + vitest@3.0.9(@types/node@22.13.10): 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.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.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.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.4 + '@types/node': 22.13.10 transitivePeerDependencies: - jiti - less @@ -3791,7 +3850,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 @@ -3803,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: @@ -3812,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.3 + call-bound: 1.0.4 for-each: 0.3.5 + get-proto: 1.0.1 gopd: 1.2.0 has-tostringtag: 1.0.2 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/context.ts b/sandbox/src/2d/context.ts new file mode 100644 index 0000000..291aad3 --- /dev/null +++ b/sandbox/src/2d/context.ts @@ -0,0 +1,5 @@ +import { CompositeRenderer2D } from './renderer/compositor.js'; + +export interface SandboxContext { + renderer: CompositeRenderer2D; +} diff --git a/sandbox/src/2d/drawables/aabb.ts b/sandbox/src/2d/drawables/aabb.ts new file mode 100644 index 0000000..a834180 --- /dev/null +++ b/sandbox/src/2d/drawables/aabb.ts @@ -0,0 +1,42 @@ +import { IAABB2D } from '@stdlib/geometry/primitives'; +import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../renderer/render-settings'; + +export interface IDrawableAABB { + drawType: 'aabb'; + aabb: IAABB2D; + stroke?: string; + fill: string; +} + +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); + const position = new Vector2( + drawable.aabb.min.x + halfSize.x, + drawable.aabb.min.y + halfSize.y, + ); + + ctx.save(); + 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.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..2b37a70 --- /dev/null +++ b/sandbox/src/2d/drawables/circle.ts @@ -0,0 +1,41 @@ +import { ICircle } from '@stdlib/geometry/primitives'; + +import { RenderSettings } from '../renderer/render-settings'; + +export interface IDrawableCircle { + drawType: 'circle'; + circle: ICircle; + stroke?: string; + fill: string; +} + +const CENTER_POINT_RADIUS = 4; + +export function drawCircle( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + 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.restore(); +} diff --git a/sandbox/src/2d/drawables/drawable.ts b/sandbox/src/2d/drawables/drawable.ts new file mode 100644 index 0000000..0d411c3 --- /dev/null +++ b/sandbox/src/2d/drawables/drawable.ts @@ -0,0 +1,47 @@ +import { RenderSettings } from '../renderer/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'; + +type DrawableMapExtractor = { + [T in Type as T['drawType']]: T; +}; + +type DrawableMap = DrawableMapExtractor< + IDrawablePoint | IDrawableAABB | IDrawableCircle | IDrawableRay | IDrawableGrid +>; + +export type DrawableType = keyof DrawableMap; +export type Drawable = DrawableMap[T]; +export type DrawableRenderFn = ( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + drawable: Drawable, +) => void; + +export const renderDrawable: DrawableRenderFn = (ctx, settings, drawable) => { + switch (drawable.drawType) { + case 'aabb': + drawAABB(ctx, settings, drawable); + return; + + case 'point': + drawPoint(ctx, settings, drawable); + return; + + case 'circle': + drawCircle(ctx, settings, drawable); + return; + + case 'ray': + 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 new file mode 100644 index 0000000..769542c --- /dev/null +++ b/sandbox/src/2d/drawables/grid.ts @@ -0,0 +1,67 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../renderer/render-settings'; + +export interface IDrawableGrid { + drawType: '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 new file mode 100644 index 0000000..531aaee --- /dev/null +++ b/sandbox/src/2d/drawables/point.ts @@ -0,0 +1,28 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../renderer/render-settings'; + +export interface IDrawablePoint { + drawType: 'point'; + position: Vector2; + color: string; +} + +const POINT_RADIUS = 4; + +export function drawPoint( + ctx: CanvasRenderingContext2D, + settings: RenderSettings, + point: IDrawablePoint, +): void { + const { pixelsPerUnit } = settings; + ctx.save(); + ctx.fillStyle = point.color; + + 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 new file mode 100644 index 0000000..5186446 --- /dev/null +++ b/sandbox/src/2d/drawables/ray.ts @@ -0,0 +1,37 @@ +import { IRay2D } from '@stdlib/geometry/primitives'; +import { Vector2 } from '@stdlib/math/vector2'; + +import { RenderSettings } from '../renderer/render-settings'; + +export interface IDrawableRay { + drawType: 'ray'; + ray: IRay2D; + color: string; +} + +const RAY_LENGTH = 9999; + +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(scaledPosition.x, scaledPosition.y); + + ctx.strokeStyle = drawable.color; + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(end.x, end.y); + ctx.stroke(); + + ctx.restore(); +} diff --git a/sandbox/src/2d/input/manager.ts b/sandbox/src/2d/input/manager.ts new file mode 100644 index 0000000..46f9456 --- /dev/null +++ b/sandbox/src/2d/input/manager.ts @@ -0,0 +1,267 @@ +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>(); + #_mousePos = new 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.x, + action.range.min.y, + action.range.max.x, + action.range.max.y, + ) + .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.x, + action.range.min.y, + action.range.max.x, + action.range.max.y, + ) + .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() { + // + } +} diff --git a/sandbox/src/2d/main.ts b/sandbox/src/2d/main.ts new file mode 100644 index 0000000..6d6ab4f --- /dev/null +++ b/sandbox/src/2d/main.ts @@ -0,0 +1,35 @@ +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 CompositeRenderer2D(); + renderer.attach(); + + const activeScene: Scene = createScene({ + renderer, + }); + + const tick = (now: number): void => { + frameRef = requestAnimationFrame(tick); + + // TODO: input + activeScene.tick(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..786f5cd --- /dev/null +++ b/sandbox/src/2d/renderer/compositor.ts @@ -0,0 +1,347 @@ +import { IAABB2D, ICircle, IRay2D } from '@stdlib/geometry/primitives'; +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 { drawGrid, IDrawableGrid } from '../drawables/grid'; +import { drawPoint, IDrawablePoint } from '../drawables/point'; +import { drawRay, 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 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; + #_renderBuffer: CanvasRenderingContext2D; + #_drawBuffers = new Map(); + #_drawCommands = new Map(); + #_dirty = new Set(); + + 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; + } + + 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 { + 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(); + } + + // 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'); + 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); + + this.#_compositor.composite(this.#_ctx, this.settings); + + this.#_ctx.restore(); + } +} diff --git a/sandbox/src/2d/renderer/render-settings.ts b/sandbox/src/2d/renderer/render-settings.ts new file mode 100644 index 0000000..4dff6e2 --- /dev/null +++ b/sandbox/src/2d/renderer/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/renderer.ts b/sandbox/src/2d/renderer/renderer.ts new file mode 100644 index 0000000..0bc6232 --- /dev/null +++ b/sandbox/src/2d/renderer/renderer.ts @@ -0,0 +1,103 @@ +import { Drawable, renderDrawable } from '../drawables/drawable'; + +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); + }; +} + +export class Renderer2D { + public readonly settings: RenderSettings; + + #_canvas: HTMLCanvasElement; + #_ctx: CanvasRenderingContext2D; + #_unbindCallback: UnbindCallback | null = null; + + 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; + } + + 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(drawables: Drawable[] = []): 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/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/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..a926224 --- /dev/null +++ b/sandbox/src/2d/scenes/scene.ts @@ -0,0 +1,16 @@ +import { Vector2 } from '@stdlib/math/vector2'; + +import { SandboxContext } from '../context'; + +export interface SceneObject { + position: Vector2; + destroy(): void; +} + +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 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..4c6e870 --- /dev/null +++ b/sandbox/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import tsconfigPaths from 'vite-tsconfig-paths'; + +export default defineConfig({ + build: { + rollupOptions: { + input: { + '2d': './2d.html', + }, + }, + }, + plugins: [tsconfigPaths()], +});