add more tests

This commit is contained in:
Orion Reed 2024-12-07 02:26:11 -05:00
parent b40ba06404
commit acc29c6c78
2 changed files with 399 additions and 117 deletions

View File

@ -127,8 +127,8 @@ describe('TransformDOMRect', () => {
});
});
describe('corner setters', () => {
test('setTopLeft maintains rectangle properties', () => {
describe('corner', () => {
test('setTopLeft with local space coordinates', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
@ -136,14 +136,15 @@ describe('TransformDOMRect', () => {
height: 100,
});
rect.setTopLeft({ x: 50, y: 50 });
expect(rect.x).toBe(50);
expect(rect.y).toBe(50);
expect(rect.width).toBe(250); // Increased by 50
expect(rect.height).toBe(150); // Increased by 50
// Move top-left corner 50 units right and 25 units down in local space
rect.setTopLeft({ x: 50, y: 25 });
expect(rect.x).toBe(150); // Original x + local x
expect(rect.y).toBe(125); // Original y + local y
expect(rect.width).toBe(150); // Original width - local x
expect(rect.height).toBe(75); // Original height - local y
});
test('setTopRight maintains rectangle properties', () => {
test('setTopRight with local space coordinates', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
@ -151,14 +152,15 @@ describe('TransformDOMRect', () => {
height: 100,
});
rect.setTopRight({ x: 350, y: 50 });
expect(rect.x).toBe(100);
expect(rect.y).toBe(50);
expect(rect.width).toBe(350);
expect(rect.height).toBe(150);
// Set top-right corner to local coordinates (150, 25)
rect.setTopRight({ x: 150, y: 25 });
expect(rect.x).toBe(100); // Original x unchanged
expect(rect.y).toBe(125); // Original y + local y
expect(rect.width).toBe(150); // New local x
expect(rect.height).toBe(75); // Original height - local y
});
test('setBottomRight maintains rectangle properties', () => {
test('setBottomRight with local space coordinates', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
@ -166,14 +168,15 @@ describe('TransformDOMRect', () => {
height: 100,
});
rect.setBottomRight({ x: 350, y: 250 });
expect(rect.x).toBe(100);
expect(rect.y).toBe(100);
expect(rect.width).toBe(350);
expect(rect.height).toBe(250);
// Set bottom-right corner to local coordinates (150, 75)
rect.setBottomRight({ x: 150, y: 75 });
expect(rect.x).toBe(100); // Original x unchanged
expect(rect.y).toBe(100); // Original y unchanged
expect(rect.width).toBe(150); // New local x
expect(rect.height).toBe(75); // New local y
});
test('setBottomLeft maintains rectangle properties', () => {
test('setBottomLeft with local space coordinates', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
@ -181,14 +184,15 @@ describe('TransformDOMRect', () => {
height: 100,
});
rect.setBottomLeft({ x: 50, y: 250 });
expect(rect.x).toBe(50);
expect(rect.y).toBe(100);
expect(rect.width).toBe(250);
expect(rect.height).toBe(250);
// Move bottom-left corner 50 units right in local space
rect.setBottomLeft({ x: 50, y: 75 });
expect(rect.x).toBe(150); // Original x + local x
expect(rect.y).toBe(100); // Original y unchanged
expect(rect.width).toBe(150); // Original width - local x
expect(rect.height).toBe(75); // New local y
});
test('corner setters work with rotation', () => {
test('corner setters with rotation', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
@ -197,11 +201,18 @@ describe('TransformDOMRect', () => {
rotation: Math.PI / 4, // 45 degrees
});
const newTopLeft = rect.toParentSpace({ x: 0, y: 0 });
rect.setTopLeft(newTopLeft);
// Move top-left corner in local space
rect.setTopLeft({ x: 50, y: 25 });
const transformedTopLeft = rect.toLocalSpace(newTopLeft);
expectPointClose(transformedTopLeft, { x: 0, y: 0 });
// Verify the dimensions are correct
expect(rect.width).toBe(150); // Original width - local x
expect(rect.height).toBe(75); // Original height - local y
// Verify we can still transform points correctly
const localPoint = { x: 0, y: 0 };
const parentPoint = rect.toParentSpace(localPoint);
const backToLocal = rect.toLocalSpace(parentPoint);
expectPointClose(backToLocal, localPoint);
});
test('setBottomRight works with upside down rotation', () => {
@ -213,15 +224,184 @@ describe('TransformDOMRect', () => {
rotation: Math.PI, // 180 degrees - upside down
});
rect.setBottomRight({ x: 350, y: 250 });
expect(rect.x).toBe(100);
expect(rect.y).toBe(100);
expect(rect.width).toBe(250);
expect(rect.height).toBe(150);
// Set bottom-right corner in local space
rect.setBottomRight({ x: 150, y: 75 });
// Verify the corner is actually at the expected position
const transformedBottomRight = rect.toParentSpace(rect.bottomRight);
expectPointClose(transformedBottomRight, { x: 350, y: 250 });
expect(rect.width).toBe(150);
expect(rect.height).toBe(75);
// Verify the corner is actually at the expected position in local space
expectPointClose(rect.bottomRight, { x: 150, y: 75 });
});
});
describe('point conversion with rotation', () => {
test('converts points correctly with 90-degree rotation', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI / 2, // 90 degrees
});
// Test points in local space and their expected parent space coordinates
const testCases = [
{
local: { x: 0, y: 0 }, // Top-left
parent: { x: 150, y: 50 }, // After rotation: center + (-height/2, -width/2)
},
{
local: { x: 200, y: 0 }, // Top-right
parent: { x: 150, y: 250 }, // After rotation: center + (height/2, -width/2)
},
{
local: { x: 100, y: 50 }, // Center
parent: { x: 200, y: 150 }, // After rotation: stays at center
},
];
testCases.forEach(({ local, parent }) => {
const toParent = rect.toParentSpace(local);
expectPointClose(toParent, parent);
const backToLocal = rect.toLocalSpace(toParent);
expectPointClose(backToLocal, local);
});
});
test('converts points correctly with 45-degree rotation', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
width: 100,
height: 100,
rotation: Math.PI / 4, // 45 degrees
});
// Center point should remain at the same position after transformation
const center = { x: 50, y: 50 }; // Center in local space
const centerInParent = rect.toParentSpace(center);
expectPointClose(centerInParent, { x: 150, y: 150 }); // Center in parent space
// Test a point on the edge
const edge = { x: 100, y: 50 }; // Right-middle in local space
const edgeInParent = rect.toParentSpace(edge);
// At 45 degrees, this point should be √2/2 * 100 units right and up from center
expectPointClose(edgeInParent, {
x: 150 + Math.cos(Math.PI / 4) * 50,
y: 150 + Math.sin(Math.PI / 4) * 50,
});
});
test('maintains relative positions through multiple transformations', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
width: 100,
height: 100,
rotation: Math.PI / 6, // 30 degrees
});
// Create a grid of test points
const gridPoints: Point[] = [];
for (let x = 0; x <= 100; x += 25) {
for (let y = 0; y <= 100; y += 25) {
gridPoints.push({ x, y });
}
}
// Verify all points maintain their relative distances
gridPoints.forEach((point1, i) => {
gridPoints.forEach((point2, j) => {
if (i === j) return;
// Calculate distance in local space
const dx = point2.x - point1.x;
const dy = point2.y - point1.y;
const localDistance = Math.sqrt(dx * dx + dy * dy);
// Transform points to parent space
const parent1 = rect.toParentSpace(point1);
const parent2 = rect.toParentSpace(point2);
// Calculate distance in parent space
const pdx = parent2.x - parent1.x;
const pdy = parent2.y - parent1.y;
const parentDistance = Math.sqrt(pdx * pdx + pdy * pdy);
// Distances should be preserved
expect(parentDistance).toBeCloseTo(localDistance);
});
});
});
test('handles edge cases with various rotations', () => {
const testRotations = [
0, // No rotation
Math.PI / 2, // 90 degrees
Math.PI, // 180 degrees
(3 * Math.PI) / 2, // 270 degrees
Math.PI / 6, // 30 degrees
Math.PI / 3, // 60 degrees
(2 * Math.PI) / 3, // 120 degrees
(5 * Math.PI) / 6, // 150 degrees
];
testRotations.forEach((rotation) => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
width: 100,
height: 50,
rotation,
});
// Test various points including corners and edges
const testPoints = [
{ x: 0, y: 0 }, // Top-left
{ x: 100, y: 0 }, // Top-right
{ x: 100, y: 50 }, // Bottom-right
{ x: 0, y: 50 }, // Bottom-left
{ x: 50, y: 25 }, // Center
{ x: 50, y: 0 }, // Top middle
{ x: 100, y: 25 }, // Right middle
{ x: 50, y: 50 }, // Bottom middle
{ x: 0, y: 25 }, // Left middle
];
testPoints.forEach((localPoint) => {
const parentPoint = rect.toParentSpace(localPoint);
const backToLocal = rect.toLocalSpace(parentPoint);
expectPointClose(backToLocal, localPoint);
});
});
});
test('maintains aspect ratio through transformations', () => {
const rect = new TransformDOMRect({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI / 3, // 60 degrees
});
// Test diagonal distances
const topLeft = { x: 0, y: 0 };
const bottomRight = { x: 200, y: 100 };
const topLeftParent = rect.toParentSpace(topLeft);
const bottomRightParent = rect.toParentSpace(bottomRight);
// Calculate distances
const localDiagonal = Math.sqrt(Math.pow(bottomRight.x - topLeft.x, 2) + Math.pow(bottomRight.y - topLeft.y, 2));
const parentDiagonal = Math.sqrt(
Math.pow(bottomRightParent.x - topLeftParent.x, 2) + Math.pow(bottomRightParent.y - topLeftParent.y, 2)
);
// Distances should be preserved
expect(parentDiagonal).toBeCloseTo(localDiagonal);
});
});
});
@ -235,21 +415,18 @@ describe('TransformDOMRectReadonly', () => {
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();
rect.x = 20;
rect.y = 30;
rect.width = 200;
rect.height = 100;
rect.rotation = Math.PI;
// Values should remain unchanged
expect(rect.x).toBe(10);
expect(rect.y).toBe(20);
expect(rect.width).toBe(100);
expect(rect.height).toBe(50);
expect(rect.rotation).toBe(0);
});
test('allows reading properties', () => {

View File

@ -9,18 +9,32 @@ interface TransformDOMRectInit {
rotation?: number;
}
/**
* Represents a rectangle with position, size, and rotation,
* capable of transforming points between local and parent coordinate spaces.
*
* **Coordinate System:**
* - The origin `(0, 0)` is at the **top-left corner**.
* - Positive `x` values extend **to the right**.
* - Positive `y` values extend **downward**.
* - Rotation is **clockwise**, in **radians**, around the rectangle's **center**.
*/
export class TransformDOMRect implements DOMRect {
// Private properties
private _x: number;
private _y: number;
private _width: number;
private _height: number;
private _rotation: number;
// Private properties for position, size, and rotation
private _x: number; // X-coordinate of the top-left corner
private _y: number; // Y-coordinate of the top-left corner
private _width: number; // Width of the rectangle
private _height: number; // Height of the rectangle
private _rotation: number; // Rotation angle in radians, clockwise
// Internal matrices
#transformMatrix: Matrix;
#inverseMatrix: Matrix;
// Internal transformation matrices
#transformMatrix: Matrix; // Transforms from local to parent space
#inverseMatrix: Matrix; // Transforms from parent to local space
/**
* Constructs a new `TransformDOMRect`.
* @param init - Optional initial values.
*/
constructor(init: TransformDOMRectInit = {}) {
this._x = init.x ?? 0;
this._y = init.y ?? 0;
@ -28,14 +42,17 @@ export class TransformDOMRect implements DOMRect {
this._height = init.height ?? 0;
this._rotation = init.rotation ?? 0;
// Initialize matrices
// Initialize transformation matrices
this.#transformMatrix = Matrix.Identity();
this.#inverseMatrix = Matrix.Identity();
// Update matrices based on current properties
this.#updateMatrices();
}
// Getters and setters for properties
/** Gets or sets the **x-coordinate** of the top-left corner. */
get x(): number {
return this._x;
}
@ -44,6 +61,7 @@ export class TransformDOMRect implements DOMRect {
this.#updateMatrices();
}
/** Gets or sets the **y-coordinate** of the top-left corner. */
get y(): number {
return this._y;
}
@ -52,6 +70,7 @@ export class TransformDOMRect implements DOMRect {
this.#updateMatrices();
}
/** Gets or sets the **width** of the rectangle. */
get width(): number {
return this._width;
}
@ -60,6 +79,7 @@ export class TransformDOMRect implements DOMRect {
this.#updateMatrices();
}
/** Gets or sets the **height** of the rectangle. */
get height(): number {
return this._height;
}
@ -68,6 +88,7 @@ export class TransformDOMRect implements DOMRect {
this.#updateMatrices();
}
/** Gets or sets the **rotation angle** in radians, **clockwise**. */
get rotation(): number {
return this._rotation;
}
@ -77,35 +98,53 @@ export class TransformDOMRect implements DOMRect {
}
// DOMRect read-only properties
/** The **left** coordinate of the rectangle (same as `x`). */
get left(): number {
return this.x;
}
/** The **top** coordinate of the rectangle (same as `y`). */
get top(): number {
return this.y;
}
/** The **right** coordinate of the rectangle (`x + width`). */
get right(): number {
return this.x + this.width;
}
/** The **bottom** coordinate of the rectangle (`y + height`). */
get bottom(): number {
return this.y + this.height;
}
// Updates the transformation matrices using instance functions
/**
* Updates the transformation matrices based on the current position,
* size, and rotation of the rectangle.
*
* The transformation sequence is:
* 1. **Translate** to the center of the rectangle.
* 2. **Rotate** around the center.
* 3. **Translate** back to the top-left corner.
*/
#updateMatrices() {
// Reset the transformMatrix to identity
this.#transformMatrix.identity();
// Compute the center point
// Compute the center point of the rectangle
const centerX = this._x + this._width / 2;
const centerY = this._y + this._height / 2;
// Apply transformations: translate to center, rotate, translate back
this.#transformMatrix.translate(centerX, centerY);
this.#transformMatrix.rotate(this._rotation);
this.#transformMatrix.translate(-this._width / 2, -this._height / 2);
// Apply transformations in this order:
// 1. Translate to center
// 2. Rotate around center
// 3. Translate back to position
this.#transformMatrix
.translate(centerX, centerY)
.rotate(this._rotation)
.translate(-centerX, -centerY)
.translate(this._x, this._y);
// Update inverseMatrix as the inverse of transformMatrix
this.#inverseMatrix = this.#transformMatrix.clone().invert();
@ -120,33 +159,57 @@ export class TransformDOMRect implements DOMRect {
return this.#inverseMatrix;
}
// Converts a point from parent space to local space
/**
* Converts a point from **parent space** to **local space**.
* @param point - The point in parent coordinate space.
* @returns The point in local coordinate space.
*/
toLocalSpace(point: Point): Point {
return this.#inverseMatrix.applyToPoint(point);
}
// Converts a point from local space to parent space
/**
* Converts a point from **local space** to **parent space**.
* @param point - The point in local coordinate space.
* @returns The point in parent coordinate space.
*/
toParentSpace(point: Point): Point {
return this.#transformMatrix.applyToPoint(point);
}
// Local space corners
/**
* Gets the **top-left** corner of the rectangle in **local space** (before transformation).
*/
get topLeft(): Point {
return { x: 0, y: 0 };
}
/**
* Gets the **top-right** corner of the rectangle in **local space** (before transformation).
*/
get topRight(): Point {
return { x: this.width, y: 0 };
}
/**
* Gets the **bottom-right** corner of the rectangle in **local space** (before transformation).
*/
get bottomRight(): Point {
return { x: this.width, y: this.height };
}
/**
* Gets the **bottom-left** corner of the rectangle in **local space** (before transformation).
*/
get bottomLeft(): Point {
return { x: 0, y: this.height };
}
/**
* Gets the **center point** of the rectangle in **parent space**.
*/
get center(): Point {
return {
x: this.x + this.width / 2,
@ -154,14 +217,26 @@ export class TransformDOMRect implements DOMRect {
};
}
/**
* Gets the four corner vertices of the rectangle in **local space**.
* @returns An array of points in the order: top-left, top-right, bottom-right, bottom-left.
*/
vertices(): Point[] {
return [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft];
}
/**
* Generates a CSS transform string representing the rectangle's transformation.
* @returns A string suitable for use in CSS `transform` properties.
*/
toCssString(): string {
return this.transformMatrix.toCssString();
}
/**
* Converts the rectangle's properties to a JSON serializable object.
* @returns An object containing the rectangle's `x`, `y`, `width`, `height`, and `rotation`.
*/
toJSON() {
return {
x: this.x,
@ -172,42 +247,59 @@ export class TransformDOMRect implements DOMRect {
};
}
/**
* Sets the **top-left** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly.
* @param point - The new top-left corner point in local coordinate space.
*/
setTopLeft(point: Point) {
const oldBottomRight = this.toParentSpace(this.bottomRight);
this._x = point.x;
this._y = point.y;
this._width = oldBottomRight.x - point.x;
this._height = oldBottomRight.y - point.y;
this._x += point.x;
this._y += point.y;
this._width -= point.x;
this._height -= point.y;
this.#updateMatrices();
}
/**
* Sets the **top-right** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly.
* @param point - The new top-right corner point in local coordinate space.
*/
setTopRight(point: Point) {
const oldBottomLeft = this.toParentSpace(this.bottomLeft);
this._y = point.y;
this._width = point.x - this._x;
this._height = oldBottomLeft.y - point.y;
this._y += point.y;
this._width = point.x;
this._height -= point.y;
this.#updateMatrices();
}
/**
* Sets the **bottom-right** corner of the rectangle in **local space**, adjusting the width and height accordingly.
* @param point - The new bottom-right corner point in local coordinate space.
*/
setBottomRight(point: Point) {
this._width = point.x;
this._height = point.y;
this.#updateMatrices();
}
/**
* Sets the **bottom-left** corner of the rectangle in **local space**, adjusting the position, width, and height accordingly.
* @param point - The new bottom-left corner point in local coordinate space.
*/
setBottomLeft(point: Point) {
const oldTopRight = this.toParentSpace(this.topRight);
this._x = point.x;
this._width = oldTopRight.x - point.x;
this._height = point.y - this._y;
this._x += point.x;
this._width -= point.x;
this._height = point.y;
this.#updateMatrices();
}
/**
* Computes the **axis-aligned bounding box** of the transformed rectangle in **parent space**.
* @returns An object representing the bounding rectangle with properties: `x`, `y`, `width`, `height`.
*/
getBounds(): DOMRectInit {
// Transform all vertices to parent space
const transformedVertices = this.vertices().map((v) => this.toParentSpace(v));
// Find min and max points
// Find min and max coordinates
const xs = transformedVertices.map((v) => v.x);
const ys = transformedVertices.map((v) => v.y);
@ -225,85 +317,98 @@ export class TransformDOMRect implements DOMRect {
}
}
// Read-only version of TransformDOMRect
/**
* A **read-only** version of `TransformDOMRect` that prevents modification of position,
* size, and rotation properties.
*/
export class TransformDOMRectReadonly extends TransformDOMRect {
constructor(init: TransformDOMRectInit = {}) {
super(init);
}
// Explicit getter overrides
get x(): number {
// Explicit overrides for all getters from parent class
override get x(): number {
return super.x;
}
get y(): number {
override get y(): number {
return super.y;
}
get width(): number {
override get width(): number {
return super.width;
}
get height(): number {
override get height(): number {
return super.height;
}
get rotation(): number {
override get rotation(): number {
return super.rotation;
}
// DOMRect property getters
get left(): number {
override get left(): number {
return super.left;
}
get top(): number {
override get top(): number {
return super.top;
}
get right(): number {
override get right(): number {
return super.right;
}
get bottom(): number {
override get bottom(): number {
return super.bottom;
}
// Override all setters to prevent modification
set x(value: number) {
throw new Error('Cannot modify readonly TransformDOMRect');
override get transformMatrix(): Matrix {
return super.transformMatrix;
}
set y(value: number) {
throw new Error('Cannot modify readonly TransformDOMRect');
override get inverseMatrix(): Matrix {
return super.inverseMatrix;
}
set width(value: number) {
throw new Error('Cannot modify readonly TransformDOMRect');
override get topLeft(): Point {
return super.topLeft;
}
set height(value: number) {
throw new Error('Cannot modify readonly TransformDOMRect');
override get topRight(): Point {
return super.topRight;
}
set rotation(value: number) {
throw new Error('Cannot modify readonly TransformDOMRect');
override get bottomRight(): Point {
return super.bottomRight;
}
// Override vertex setter methods
setTopLeft(point: Point): void {
throw new Error('Cannot modify readonly TransformDOMRect');
override get bottomLeft(): Point {
return super.bottomLeft;
}
setTopRight(point: Point): void {
throw new Error('Cannot modify readonly TransformDOMRect');
override get center(): Point {
return super.center;
}
setBottomRight(point: Point): void {
throw new Error('Cannot modify readonly TransformDOMRect');
// Add no-op setters
override set x(value: number) {
// no-op
}
setBottomLeft(point: Point): void {
throw new Error('Cannot modify readonly TransformDOMRect');
override set y(value: number) {
// no-op
}
override set width(value: number) {
// no-op
}
override set height(value: number) {
// no-op
}
override set rotation(value: number) {
// no-op
}
}