diff --git a/src/__tests__/TransformDOMRect.test.ts b/src/__tests__/TransformDOMRect.test.ts new file mode 100644 index 0000000..0a51da8 --- /dev/null +++ b/src/__tests__/TransformDOMRect.test.ts @@ -0,0 +1,147 @@ +import { expect, test, describe } from 'bun:test'; +import { TransformDOMRect, TransformDOMRectReadonly } from '../common/TransformDOMRect'; +import { Point } from '../common/types'; + +// Helper for comparing points with floating point values +const expectPointClose = (actual: Point, expected: Point) => { + expect(actual.x).toBeCloseTo(expected.x); + expect(actual.y).toBeCloseTo(expected.y); +}; + +describe('TransformDOMRect', () => { + test('constructor initializes with default values', () => { + const rect = new TransformDOMRect(); + 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('constructor initializes with provided values', () => { + const rect = new TransformDOMRect({ + 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); + }); + + test('DOMRect properties are calculated correctly', () => { + const rect = new TransformDOMRect({ + x: 10, + y: 20, + width: 100, + height: 50, + }); + expect(rect.left).toBe(10); + expect(rect.top).toBe(20); + expect(rect.right).toBe(110); + expect(rect.bottom).toBe(70); + }); + + test('vertices returns correct local space corners', () => { + const rect = new TransformDOMRect({ + width: 100, + height: 50, + }); + + const vertices = rect.vertices(); + expectPointClose(vertices[0], { x: 0, y: 0 }); + expectPointClose(vertices[1], { x: 100, y: 0 }); + expectPointClose(vertices[2], { x: 100, y: 50 }); + expectPointClose(vertices[3], { x: 0, y: 50 }); + }); + + test('coordinate space conversion with rotation', () => { + const rect = new TransformDOMRect({ + x: 10, + y: 20, + width: 100, + height: 50, + rotation: Math.PI / 2, // 90 degrees + }); + + const parentPoint = { x: 10, y: 20 }; + const localPoint = rect.toLocalSpace(parentPoint); + const backToParent = rect.toParentSpace(localPoint); + + expectPointClose(backToParent, parentPoint); + }); + + test('getBounds returns correct bounding box after rotation', () => { + const rect = new TransformDOMRect({ + x: 0, + y: 0, + width: 100, + height: 50, + rotation: Math.PI / 2, // 90 degrees + }); + + const bounds = rect.getBounds(); + expect(bounds.width).toBeCloseTo(50); + expect(bounds.height).toBeCloseTo(100); + }); + + test('setters update matrices correctly', () => { + const rect = new TransformDOMRect(); + rect.x = 10; + rect.y = 20; + rect.width = 100; + rect.height = 50; + rect.rotation = Math.PI / 4; + + const point = { x: 0, y: 0 }; + const transformed = rect.toParentSpace(point); + const backToLocal = rect.toLocalSpace(transformed); + + expectPointClose(backToLocal, point); + }); +}); + +describe('TransformDOMRectReadonly', () => { + test('prevents modifications through setters', () => { + const rect = new TransformDOMRectReadonly({ + x: 10, + y: 20, + width: 100, + height: 50, + }); + + expect(() => { + rect.x = 20; + }).toThrow(); + expect(() => { + rect.y = 30; + }).toThrow(); + expect(() => { + rect.width = 200; + }).toThrow(); + expect(() => { + rect.height = 100; + }).toThrow(); + expect(() => { + rect.rotation = Math.PI; + }).toThrow(); + }); + + test('allows reading properties', () => { + const rect = new TransformDOMRectReadonly({ + x: 10, + y: 20, + width: 100, + height: 50, + }); + + expect(rect.x).toBe(10); + expect(rect.y).toBe(20); + expect(rect.width).toBe(100); + expect(rect.height).toBe(50); + }); +}); diff --git a/src/__tests__/TransformDOMRect.ts b/src/__tests__/TransformDOMRect.ts deleted file mode 100644 index 39381bd..0000000 --- a/src/__tests__/TransformDOMRect.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { expect, test, describe } from 'bun:test'; -import { TransformDOMRect } from '../common/TransformDOMRect'; -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 TransformDOMRect(); - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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 TransformDOMRect({ - 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/common/TransformDOMRect.ts b/src/common/TransformDOMRect.ts index 03ccefd..15845b0 100644 --- a/src/common/TransformDOMRect.ts +++ b/src/common/TransformDOMRect.ts @@ -202,6 +202,27 @@ export class TransformDOMRect implements DOMRect { this._x = point.x; this.#updateMatrices(); } + + getBounds(): DOMRectInit { + // Transform all vertices to parent space + const transformedVertices = this.vertices().map((v) => this.toParentSpace(v)); + + // Find min and max points + const xs = transformedVertices.map((v) => v.x); + const ys = transformedVertices.map((v) => v.y); + + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + + return { + x: minX, + y: minY, + width: maxX - minX, + height: maxY - minY, + }; + } } // Read-only version of TransformDOMRect @@ -210,7 +231,45 @@ export class TransformDOMRectReadonly extends TransformDOMRect { super(init); } - // Override setters to prevent modification + // Explicit getter overrides + get x(): number { + return super.x; + } + + get y(): number { + return super.y; + } + + get width(): number { + return super.width; + } + + get height(): number { + return super.height; + } + + get rotation(): number { + return super.rotation; + } + + // DOMRect property getters + get left(): number { + return super.left; + } + + get top(): number { + return super.top; + } + + get right(): number { + return super.right; + } + + get bottom(): number { + return super.bottom; + } + + // Override all setters to prevent modification set x(value: number) { throw new Error('Cannot modify readonly TransformDOMRect'); } @@ -230,4 +289,21 @@ export class TransformDOMRectReadonly extends TransformDOMRect { set rotation(value: number) { throw new Error('Cannot modify readonly TransformDOMRect'); } + + // Override vertex setter methods + setTopLeft(point: Point): void { + throw new Error('Cannot modify readonly TransformDOMRect'); + } + + setTopRight(point: Point): void { + throw new Error('Cannot modify readonly TransformDOMRect'); + } + + setBottomRight(point: Point): void { + throw new Error('Cannot modify readonly TransformDOMRect'); + } + + setBottomLeft(point: Point): void { + throw new Error('Cannot modify readonly TransformDOMRect'); + } }