diff --git a/package.json b/package.json index bec6c15..e7a39ba 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "@types/leaflet": "^1.9.14", "@types/node": "^22.10.1", "@webgpu/types": "^0.1.51", + "bun-types": "^1.1.38", "typescript": "^5.7.2", "vite": "^6.0.0" } diff --git a/src/__tests__/RotatedDOMRect.test.ts b/src/__tests__/RotatedDOMRect.test.ts new file mode 100644 index 0000000..a0317fb --- /dev/null +++ b/src/__tests__/RotatedDOMRect.test.ts @@ -0,0 +1,224 @@ +import { expect, test, describe } from 'bun:test'; +import { RotatedDOMRect } from '../common/rotated-dom-rect-2'; +import { Vector } from '../common/Vector'; + +// Helper for comparing points with floating point values +const expectPointClose = (actual: { x: number; y: number }, expected: { x: number; y: number }) => { + expect(actual.x).toBeCloseTo(expected.x); + expect(actual.y).toBeCloseTo(expected.y); +}; + +describe('RotatedDOMRect', () => { + describe('constructor', () => { + test('initializes with default values', () => { + const rect = new RotatedDOMRect(); + expect(rect.x).toBe(0); + expect(rect.y).toBe(0); + expect(rect.width).toBe(0); + expect(rect.height).toBe(0); + expect(rect.rotation).toBe(0); + }); + + test('initializes with custom values', () => { + const rect = new RotatedDOMRect({ + x: 10, + y: 20, + width: 100, + height: 50, + rotation: Math.PI / 4, + }); + expect(rect.x).toBe(10); + expect(rect.y).toBe(20); + expect(rect.width).toBe(100); + expect(rect.height).toBe(50); + expect(rect.rotation).toBe(Math.PI / 4); + }); + }); + + describe('corner calculations', () => { + test('calculates corners for unrotated rectangle', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + rotation: 0, + }); + + expectPointClose(rect.topLeft, { x: -50, y: -25 }); + expectPointClose(rect.topRight, { x: 50, y: -25 }); + expectPointClose(rect.bottomLeft, { x: -50, y: 25 }); + expectPointClose(rect.bottomRight, { x: 50, y: 25 }); + }); + + test('calculates corners for 90-degree rotated rectangle', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 100, + rotation: Math.PI / 2, + }); + + expectPointClose(rect.topLeft, { x: 50, y: -50 }); + expectPointClose(rect.topRight, { x: 50, y: 50 }); + expectPointClose(rect.bottomLeft, { x: -50, y: -50 }); + expectPointClose(rect.bottomRight, { x: -50, y: 50 }); + }); + }); + + describe('bounds', () => { + test('calculates bounds for unrotated rectangle', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + rotation: 0, + }); + + expect(rect.getBounds()).toEqual({ + x: -50, + y: -25, + width: 100, + height: 50, + }); + }); + + test('calculates bounds for 45-degree rotated rectangle', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + rotation: Math.PI / 4, + }); + + const bounds = rect.getBounds(); + const cos45 = Math.cos(Math.PI / 4); + const sin45 = Math.sin(Math.PI / 4); + const expectedWidth = Math.abs(100 * cos45) + Math.abs(50 * sin45); + const expectedHeight = Math.abs(100 * sin45) + Math.abs(50 * cos45); + + expect(bounds.width).toBeCloseTo(expectedWidth); + expect(bounds.height).toBeCloseTo(expectedHeight); + expect(bounds.x).toBeCloseTo(-expectedWidth / 2); + expect(bounds.y).toBeCloseTo(-expectedHeight / 2); + }); + }); + + describe('setters', () => { + test('updates corners when center is modified', () => { + const rect = new RotatedDOMRect({ + width: 100, + height: 50, + }); + + rect.center = { x: 100, y: 100 }; + expectPointClose(rect.topLeft, { x: 50, y: 75 }); + expectPointClose(rect.bottomRight, { x: 150, y: 125 }); + }); + + test('updates dimensions and rotation when setting topRight', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + }); + + expect(rect.width).toBe(100); + expect(rect.height).toBe(50); + expect(rect.rotation).toBe(0); + expectPointClose(rect.bottomLeft, { x: -50, y: 25 }); + expectPointClose(rect.bottomRight, { x: 50, y: 25 }); + expectPointClose(rect.center, { x: 0, y: 0 }); + expectPointClose(rect.topLeft, { x: -50, y: -25 }); + + expectPointClose(rect.topRight, { x: 50, y: -25 }); + rect.topRight = { x: 100, y: -50 }; + expect(rect.width).toBe(150); + }); + }); + + describe('corner setters', () => { + test('updates dimensions when setting bottomLeft', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + }); + + // Store original topRight position as it should remain fixed + const originalTopRight = { ...rect.topRight }; // (50, -25) + + // Set new bottomLeft position + rect.bottomLeft = { x: -100, y: 100 }; + + // Verify topRight hasn't moved + expectPointClose(rect.topRight, originalTopRight); + + // Verify bottomLeft is at new position + expectPointClose(rect.bottomLeft, { x: -100, y: 100 }); + + // Verify center is halfway between bottomLeft and topRight + expectPointClose(rect.center, { + x: (-100 + 50) / 2, // -25 + y: (100 + -25) / 2, // 37.5 + }); + + // Verify new dimensions + expect(rect.width).toBeCloseTo(150); // abs(-100 - 50) = 150 + expect(rect.height).toBeCloseTo(125); // abs(100 - -25) = 125 + }); + + test('maintains rectangle properties when setting corners', () => { + const rect = new RotatedDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + rotation: Math.PI / 6, + }); + + rect.bottomRight = { x: 75, y: 75 }; + + // After setting bottomRight, topLeft and bottomRight should be equidistant from center + const distanceToTopLeft = Vector.distance(rect.center, rect.topLeft); + const distanceToBottomRight = Vector.distance(rect.center, rect.bottomRight); + expect(distanceToTopLeft).toBeCloseTo(distanceToBottomRight); + }); + }); + + describe('edge cases', () => { + test('handles zero dimensions', () => { + const rect = new RotatedDOMRect({ + x: 10, + y: 10, + width: 0, + height: 0, + rotation: Math.PI / 4, + }); + + expect(rect.getBounds()).toEqual({ + x: 10, + y: 10, + width: 0, + height: 0, + }); + }); + + test('handles 360-degree rotation', () => { + const rect = new RotatedDOMRect({ + width: 100, + height: 50, + rotation: Math.PI * 2, + }); + + // Should be equivalent to rotation: 0 + expectPointClose(rect.topLeft, { x: -50, y: -25 }); + expectPointClose(rect.topRight, { x: 50, y: -25 }); + }); + }); +}); diff --git a/src/__tests__/Vector.test.ts b/src/__tests__/Vector.test.ts new file mode 100644 index 0000000..7c92008 --- /dev/null +++ b/src/__tests__/Vector.test.ts @@ -0,0 +1,108 @@ +import { expect, test, describe } from 'bun:test'; +import { Vector } from '../common/Vector'; + +describe('Vector', () => { + describe('basic operations', () => { + test('zero() returns zero vector', () => { + expect(Vector.zero()).toEqual({ x: 0, y: 0 }); + }); + + test('add() combines two vectors', () => { + const a = { x: 1, y: 2 }; + const b = { x: 3, y: 4 }; + expect(Vector.add(a, b)).toEqual({ x: 4, y: 6 }); + }); + + test('sub() subtracts vectors', () => { + const a = { x: 3, y: 4 }; + const b = { x: 1, y: 2 }; + expect(Vector.sub(a, b)).toEqual({ x: 2, y: 2 }); + }); + + test('mult() multiplies vectors component-wise', () => { + const a = { x: 2, y: 3 }; + const b = { x: 4, y: 5 }; + expect(Vector.mult(a, b)).toEqual({ x: 8, y: 15 }); + }); + + test('scale() multiplies vector by scalar', () => { + const v = { x: 2, y: 3 }; + expect(Vector.scale(v, 2)).toEqual({ x: 4, y: 6 }); + }); + }); + + describe('vector properties', () => { + test('mag() calculates magnitude', () => { + const v = { x: 3, y: 4 }; + expect(Vector.mag(v)).toBe(5); + }); + + test('normalized() returns unit vector', () => { + const v = { x: 3, y: 4 }; + const normalized = Vector.normalized(v); + expect(normalized.x).toBeCloseTo(0.6); + expect(normalized.y).toBeCloseTo(0.8); + }); + + test('normalized() handles zero vector', () => { + const v = { x: 0, y: 0 }; + expect(Vector.normalized(v)).toEqual({ x: 0, y: 0 }); + }); + }); + + describe('distance calculations', () => { + test('distance() calculates Euclidean distance', () => { + const a = { x: 0, y: 0 }; + const b = { x: 3, y: 4 }; + expect(Vector.distance(a, b)).toBe(5); + }); + + test('distanceSquared() calculates squared distance', () => { + const a = { x: 0, y: 0 }; + const b = { x: 3, y: 4 }; + expect(Vector.distanceSquared(a, b)).toBe(25); + }); + }); + + describe('interpolation and rotation', () => { + test('lerp() interpolates between points', () => { + const a = { x: 0, y: 0 }; + const b = { x: 10, y: 10 }; + expect(Vector.lerp(a, b, 0.5)).toEqual({ x: 5, y: 5 }); + }); + + test('rotate() rotates vector by angle', () => { + const v = { x: 1, y: 0 }; + const rotated = Vector.rotate(v, Math.PI / 2); + expect(rotated.x).toBeCloseTo(0); + expect(rotated.y).toBeCloseTo(1); + }); + + test('rotateAround() rotates point around pivot', () => { + const point = { x: 2, y: 0 }; + const pivot = { x: 0, y: 0 }; + const rotated = Vector.rotateAround(point, pivot, Math.PI / 2); + expect(rotated.x).toBeCloseTo(0); + expect(rotated.y).toBeCloseTo(2); + }); + }); + + describe('angle calculations', () => { + test('angle() calculates angle from x-axis', () => { + const v = { x: 1, y: 1 }; + expect(Vector.angle(v)).toBeCloseTo(Math.PI / 4); + }); + + test('angleTo() calculates angle between vectors', () => { + const a = { x: 1, y: 0 }; + const b = { x: 0, y: 1 }; + expect(Vector.angleTo(b, a)).toBeCloseTo(Math.PI / 2); + }); + + test('angleFromOrigin() calculates angle relative to origin', () => { + const point = { x: 1, y: 1 }; + const origin = { x: 0, y: 0 }; + expect(Vector.angleFromOrigin(point, origin)).toBeCloseTo(Math.PI / 4); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 4888b93..e2ada0a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "useDefineForClassFields": true, "skipLibCheck": true, "lib": ["DOM", "DOM.Iterable", "ESNext", "WebWorker"], - "types": ["@webgpu/types", "@types/node"] + "types": ["@webgpu/types", "@types/node", "bun-types"] }, "include": ["src/**/*.ts", "demo/**/*.ts", "vite.config.ts"] }