add more tests
This commit is contained in:
parent
b40ba06404
commit
acc29c6c78
|
|
@ -127,8 +127,8 @@ describe('TransformDOMRect', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('corner setters', () => {
|
describe('corner', () => {
|
||||||
test('setTopLeft maintains rectangle properties', () => {
|
test('setTopLeft with local space coordinates', () => {
|
||||||
const rect = new TransformDOMRect({
|
const rect = new TransformDOMRect({
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
|
@ -136,14 +136,15 @@ describe('TransformDOMRect', () => {
|
||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
rect.setTopLeft({ x: 50, y: 50 });
|
// Move top-left corner 50 units right and 25 units down in local space
|
||||||
expect(rect.x).toBe(50);
|
rect.setTopLeft({ x: 50, y: 25 });
|
||||||
expect(rect.y).toBe(50);
|
expect(rect.x).toBe(150); // Original x + local x
|
||||||
expect(rect.width).toBe(250); // Increased by 50
|
expect(rect.y).toBe(125); // Original y + local y
|
||||||
expect(rect.height).toBe(150); // Increased by 50
|
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({
|
const rect = new TransformDOMRect({
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
|
@ -151,14 +152,15 @@ describe('TransformDOMRect', () => {
|
||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
rect.setTopRight({ x: 350, y: 50 });
|
// Set top-right corner to local coordinates (150, 25)
|
||||||
expect(rect.x).toBe(100);
|
rect.setTopRight({ x: 150, y: 25 });
|
||||||
expect(rect.y).toBe(50);
|
expect(rect.x).toBe(100); // Original x unchanged
|
||||||
expect(rect.width).toBe(350);
|
expect(rect.y).toBe(125); // Original y + local y
|
||||||
expect(rect.height).toBe(150);
|
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({
|
const rect = new TransformDOMRect({
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
|
@ -166,14 +168,15 @@ describe('TransformDOMRect', () => {
|
||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
rect.setBottomRight({ x: 350, y: 250 });
|
// Set bottom-right corner to local coordinates (150, 75)
|
||||||
expect(rect.x).toBe(100);
|
rect.setBottomRight({ x: 150, y: 75 });
|
||||||
expect(rect.y).toBe(100);
|
expect(rect.x).toBe(100); // Original x unchanged
|
||||||
expect(rect.width).toBe(350);
|
expect(rect.y).toBe(100); // Original y unchanged
|
||||||
expect(rect.height).toBe(250);
|
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({
|
const rect = new TransformDOMRect({
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
|
@ -181,14 +184,15 @@ describe('TransformDOMRect', () => {
|
||||||
height: 100,
|
height: 100,
|
||||||
});
|
});
|
||||||
|
|
||||||
rect.setBottomLeft({ x: 50, y: 250 });
|
// Move bottom-left corner 50 units right in local space
|
||||||
expect(rect.x).toBe(50);
|
rect.setBottomLeft({ x: 50, y: 75 });
|
||||||
expect(rect.y).toBe(100);
|
expect(rect.x).toBe(150); // Original x + local x
|
||||||
expect(rect.width).toBe(250);
|
expect(rect.y).toBe(100); // Original y unchanged
|
||||||
expect(rect.height).toBe(250);
|
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({
|
const rect = new TransformDOMRect({
|
||||||
x: 100,
|
x: 100,
|
||||||
y: 100,
|
y: 100,
|
||||||
|
|
@ -197,11 +201,18 @@ describe('TransformDOMRect', () => {
|
||||||
rotation: Math.PI / 4, // 45 degrees
|
rotation: Math.PI / 4, // 45 degrees
|
||||||
});
|
});
|
||||||
|
|
||||||
const newTopLeft = rect.toParentSpace({ x: 0, y: 0 });
|
// Move top-left corner in local space
|
||||||
rect.setTopLeft(newTopLeft);
|
rect.setTopLeft({ x: 50, y: 25 });
|
||||||
|
|
||||||
const transformedTopLeft = rect.toLocalSpace(newTopLeft);
|
// Verify the dimensions are correct
|
||||||
expectPointClose(transformedTopLeft, { x: 0, y: 0 });
|
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', () => {
|
test('setBottomRight works with upside down rotation', () => {
|
||||||
|
|
@ -213,15 +224,184 @@ describe('TransformDOMRect', () => {
|
||||||
rotation: Math.PI, // 180 degrees - upside down
|
rotation: Math.PI, // 180 degrees - upside down
|
||||||
});
|
});
|
||||||
|
|
||||||
rect.setBottomRight({ x: 350, y: 250 });
|
// Set bottom-right corner in local space
|
||||||
expect(rect.x).toBe(100);
|
rect.setBottomRight({ x: 150, y: 75 });
|
||||||
expect(rect.y).toBe(100);
|
|
||||||
expect(rect.width).toBe(250);
|
|
||||||
expect(rect.height).toBe(150);
|
|
||||||
|
|
||||||
// Verify the corner is actually at the expected position
|
expect(rect.width).toBe(150);
|
||||||
const transformedBottomRight = rect.toParentSpace(rect.bottomRight);
|
expect(rect.height).toBe(75);
|
||||||
expectPointClose(transformedBottomRight, { x: 350, y: 250 });
|
|
||||||
|
// 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,
|
height: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(() => {
|
|
||||||
rect.x = 20;
|
rect.x = 20;
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
rect.y = 30;
|
rect.y = 30;
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
rect.width = 200;
|
rect.width = 200;
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
rect.height = 100;
|
rect.height = 100;
|
||||||
}).toThrow();
|
|
||||||
expect(() => {
|
|
||||||
rect.rotation = Math.PI;
|
rect.rotation = Math.PI;
|
||||||
}).toThrow();
|
|
||||||
|
// 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', () => {
|
test('allows reading properties', () => {
|
||||||
|
|
|
||||||
|
|
@ -9,18 +9,32 @@ interface TransformDOMRectInit {
|
||||||
rotation?: number;
|
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 {
|
export class TransformDOMRect implements DOMRect {
|
||||||
// Private properties
|
// Private properties for position, size, and rotation
|
||||||
private _x: number;
|
private _x: number; // X-coordinate of the top-left corner
|
||||||
private _y: number;
|
private _y: number; // Y-coordinate of the top-left corner
|
||||||
private _width: number;
|
private _width: number; // Width of the rectangle
|
||||||
private _height: number;
|
private _height: number; // Height of the rectangle
|
||||||
private _rotation: number;
|
private _rotation: number; // Rotation angle in radians, clockwise
|
||||||
|
|
||||||
// Internal matrices
|
// Internal transformation matrices
|
||||||
#transformMatrix: Matrix;
|
#transformMatrix: Matrix; // Transforms from local to parent space
|
||||||
#inverseMatrix: Matrix;
|
#inverseMatrix: Matrix; // Transforms from parent to local space
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new `TransformDOMRect`.
|
||||||
|
* @param init - Optional initial values.
|
||||||
|
*/
|
||||||
constructor(init: TransformDOMRectInit = {}) {
|
constructor(init: TransformDOMRectInit = {}) {
|
||||||
this._x = init.x ?? 0;
|
this._x = init.x ?? 0;
|
||||||
this._y = init.y ?? 0;
|
this._y = init.y ?? 0;
|
||||||
|
|
@ -28,14 +42,17 @@ export class TransformDOMRect implements DOMRect {
|
||||||
this._height = init.height ?? 0;
|
this._height = init.height ?? 0;
|
||||||
this._rotation = init.rotation ?? 0;
|
this._rotation = init.rotation ?? 0;
|
||||||
|
|
||||||
// Initialize matrices
|
// Initialize transformation matrices
|
||||||
this.#transformMatrix = Matrix.Identity();
|
this.#transformMatrix = Matrix.Identity();
|
||||||
this.#inverseMatrix = Matrix.Identity();
|
this.#inverseMatrix = Matrix.Identity();
|
||||||
|
|
||||||
|
// Update matrices based on current properties
|
||||||
this.#updateMatrices();
|
this.#updateMatrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Getters and setters for properties
|
// Getters and setters for properties
|
||||||
|
|
||||||
|
/** Gets or sets the **x-coordinate** of the top-left corner. */
|
||||||
get x(): number {
|
get x(): number {
|
||||||
return this._x;
|
return this._x;
|
||||||
}
|
}
|
||||||
|
|
@ -44,6 +61,7 @@ export class TransformDOMRect implements DOMRect {
|
||||||
this.#updateMatrices();
|
this.#updateMatrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets or sets the **y-coordinate** of the top-left corner. */
|
||||||
get y(): number {
|
get y(): number {
|
||||||
return this._y;
|
return this._y;
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +70,7 @@ export class TransformDOMRect implements DOMRect {
|
||||||
this.#updateMatrices();
|
this.#updateMatrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets or sets the **width** of the rectangle. */
|
||||||
get width(): number {
|
get width(): number {
|
||||||
return this._width;
|
return this._width;
|
||||||
}
|
}
|
||||||
|
|
@ -60,6 +79,7 @@ export class TransformDOMRect implements DOMRect {
|
||||||
this.#updateMatrices();
|
this.#updateMatrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets or sets the **height** of the rectangle. */
|
||||||
get height(): number {
|
get height(): number {
|
||||||
return this._height;
|
return this._height;
|
||||||
}
|
}
|
||||||
|
|
@ -68,6 +88,7 @@ export class TransformDOMRect implements DOMRect {
|
||||||
this.#updateMatrices();
|
this.#updateMatrices();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Gets or sets the **rotation angle** in radians, **clockwise**. */
|
||||||
get rotation(): number {
|
get rotation(): number {
|
||||||
return this._rotation;
|
return this._rotation;
|
||||||
}
|
}
|
||||||
|
|
@ -77,35 +98,53 @@ export class TransformDOMRect implements DOMRect {
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOMRect read-only properties
|
// DOMRect read-only properties
|
||||||
|
|
||||||
|
/** The **left** coordinate of the rectangle (same as `x`). */
|
||||||
get left(): number {
|
get left(): number {
|
||||||
return this.x;
|
return this.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The **top** coordinate of the rectangle (same as `y`). */
|
||||||
get top(): number {
|
get top(): number {
|
||||||
return this.y;
|
return this.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The **right** coordinate of the rectangle (`x + width`). */
|
||||||
get right(): number {
|
get right(): number {
|
||||||
return this.x + this.width;
|
return this.x + this.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The **bottom** coordinate of the rectangle (`y + height`). */
|
||||||
get bottom(): number {
|
get bottom(): number {
|
||||||
return this.y + this.height;
|
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() {
|
#updateMatrices() {
|
||||||
// Reset the transformMatrix to identity
|
// Reset the transformMatrix to identity
|
||||||
this.#transformMatrix.identity();
|
this.#transformMatrix.identity();
|
||||||
|
|
||||||
// Compute the center point
|
// Compute the center point of the rectangle
|
||||||
const centerX = this._x + this._width / 2;
|
const centerX = this._x + this._width / 2;
|
||||||
const centerY = this._y + this._height / 2;
|
const centerY = this._y + this._height / 2;
|
||||||
|
|
||||||
// Apply transformations: translate to center, rotate, translate back
|
// Apply transformations in this order:
|
||||||
this.#transformMatrix.translate(centerX, centerY);
|
// 1. Translate to center
|
||||||
this.#transformMatrix.rotate(this._rotation);
|
// 2. Rotate around center
|
||||||
this.#transformMatrix.translate(-this._width / 2, -this._height / 2);
|
// 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
|
// Update inverseMatrix as the inverse of transformMatrix
|
||||||
this.#inverseMatrix = this.#transformMatrix.clone().invert();
|
this.#inverseMatrix = this.#transformMatrix.clone().invert();
|
||||||
|
|
@ -120,33 +159,57 @@ export class TransformDOMRect implements DOMRect {
|
||||||
return this.#inverseMatrix;
|
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 {
|
toLocalSpace(point: Point): Point {
|
||||||
return this.#inverseMatrix.applyToPoint(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 {
|
toParentSpace(point: Point): Point {
|
||||||
return this.#transformMatrix.applyToPoint(point);
|
return this.#transformMatrix.applyToPoint(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local space corners
|
// Local space corners
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the **top-left** corner of the rectangle in **local space** (before transformation).
|
||||||
|
*/
|
||||||
get topLeft(): Point {
|
get topLeft(): Point {
|
||||||
return { x: 0, y: 0 };
|
return { x: 0, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the **top-right** corner of the rectangle in **local space** (before transformation).
|
||||||
|
*/
|
||||||
get topRight(): Point {
|
get topRight(): Point {
|
||||||
return { x: this.width, y: 0 };
|
return { x: this.width, y: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the **bottom-right** corner of the rectangle in **local space** (before transformation).
|
||||||
|
*/
|
||||||
get bottomRight(): Point {
|
get bottomRight(): Point {
|
||||||
return { x: this.width, y: this.height };
|
return { x: this.width, y: this.height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the **bottom-left** corner of the rectangle in **local space** (before transformation).
|
||||||
|
*/
|
||||||
get bottomLeft(): Point {
|
get bottomLeft(): Point {
|
||||||
return { x: 0, y: this.height };
|
return { x: 0, y: this.height };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the **center point** of the rectangle in **parent space**.
|
||||||
|
*/
|
||||||
get center(): Point {
|
get center(): Point {
|
||||||
return {
|
return {
|
||||||
x: this.x + this.width / 2,
|
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[] {
|
vertices(): Point[] {
|
||||||
return [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft];
|
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 {
|
toCssString(): string {
|
||||||
return this.transformMatrix.toCssString();
|
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() {
|
toJSON() {
|
||||||
return {
|
return {
|
||||||
x: this.x,
|
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) {
|
setTopLeft(point: Point) {
|
||||||
const oldBottomRight = this.toParentSpace(this.bottomRight);
|
this._x += point.x;
|
||||||
this._x = point.x;
|
this._y += point.y;
|
||||||
this._y = point.y;
|
this._width -= point.x;
|
||||||
this._width = oldBottomRight.x - point.x;
|
this._height -= point.y;
|
||||||
this._height = oldBottomRight.y - point.y;
|
|
||||||
this.#updateMatrices();
|
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) {
|
setTopRight(point: Point) {
|
||||||
const oldBottomLeft = this.toParentSpace(this.bottomLeft);
|
this._y += point.y;
|
||||||
this._y = point.y;
|
this._width = point.x;
|
||||||
this._width = point.x - this._x;
|
this._height -= point.y;
|
||||||
this._height = oldBottomLeft.y - point.y;
|
|
||||||
this.#updateMatrices();
|
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) {
|
setBottomRight(point: Point) {
|
||||||
this._width = point.x;
|
this._width = point.x;
|
||||||
this._height = point.y;
|
this._height = point.y;
|
||||||
this.#updateMatrices();
|
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) {
|
setBottomLeft(point: Point) {
|
||||||
const oldTopRight = this.toParentSpace(this.topRight);
|
this._x += point.x;
|
||||||
this._x = point.x;
|
this._width -= point.x;
|
||||||
this._width = oldTopRight.x - point.x;
|
this._height = point.y;
|
||||||
this._height = point.y - this._y;
|
|
||||||
this.#updateMatrices();
|
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 {
|
getBounds(): DOMRectInit {
|
||||||
// Transform all vertices to parent space
|
// Transform all vertices to parent space
|
||||||
const transformedVertices = this.vertices().map((v) => this.toParentSpace(v));
|
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 xs = transformedVertices.map((v) => v.x);
|
||||||
const ys = transformedVertices.map((v) => v.y);
|
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 {
|
export class TransformDOMRectReadonly extends TransformDOMRect {
|
||||||
constructor(init: TransformDOMRectInit = {}) {
|
constructor(init: TransformDOMRectInit = {}) {
|
||||||
super(init);
|
super(init);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicit getter overrides
|
// Explicit overrides for all getters from parent class
|
||||||
get x(): number {
|
override get x(): number {
|
||||||
return super.x;
|
return super.x;
|
||||||
}
|
}
|
||||||
|
|
||||||
get y(): number {
|
override get y(): number {
|
||||||
return super.y;
|
return super.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
get width(): number {
|
override get width(): number {
|
||||||
return super.width;
|
return super.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
get height(): number {
|
override get height(): number {
|
||||||
return super.height;
|
return super.height;
|
||||||
}
|
}
|
||||||
|
|
||||||
get rotation(): number {
|
override get rotation(): number {
|
||||||
return super.rotation;
|
return super.rotation;
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOMRect property getters
|
override get left(): number {
|
||||||
get left(): number {
|
|
||||||
return super.left;
|
return super.left;
|
||||||
}
|
}
|
||||||
|
|
||||||
get top(): number {
|
override get top(): number {
|
||||||
return super.top;
|
return super.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
get right(): number {
|
override get right(): number {
|
||||||
return super.right;
|
return super.right;
|
||||||
}
|
}
|
||||||
|
|
||||||
get bottom(): number {
|
override get bottom(): number {
|
||||||
return super.bottom;
|
return super.bottom;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override all setters to prevent modification
|
override get transformMatrix(): Matrix {
|
||||||
set x(value: number) {
|
return super.transformMatrix;
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set y(value: number) {
|
override get inverseMatrix(): Matrix {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
return super.inverseMatrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
set width(value: number) {
|
override get topLeft(): Point {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
return super.topLeft;
|
||||||
}
|
}
|
||||||
|
|
||||||
set height(value: number) {
|
override get topRight(): Point {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
return super.topRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
set rotation(value: number) {
|
override get bottomRight(): Point {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
return super.bottomRight;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override vertex setter methods
|
override get bottomLeft(): Point {
|
||||||
setTopLeft(point: Point): void {
|
return super.bottomLeft;
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setTopRight(point: Point): void {
|
override get center(): Point {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
return super.center;
|
||||||
}
|
}
|
||||||
|
|
||||||
setBottomRight(point: Point): void {
|
// Add no-op setters
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
override set x(value: number) {
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
|
|
||||||
setBottomLeft(point: Point): void {
|
override set y(value: number) {
|
||||||
throw new Error('Cannot modify readonly TransformDOMRect');
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override set width(value: number) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override set height(value: number) {
|
||||||
|
// no-op
|
||||||
|
}
|
||||||
|
|
||||||
|
override set rotation(value: number) {
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue