folk-canvas/lib/__tests__/DOMRectTransform.test.ts

504 lines
15 KiB
TypeScript

import { expect, test, describe } from 'bun:test';
import { DOMRectTransform, DOMRectTransformReadonly } from '../DOMRectTransform';
import { Point } from '../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 DOMRectTransform();
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 DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform();
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);
});
test('coordinate transformations with rotation and translation', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI / 4, // 45 degrees
});
// Test multiple points
const testPoints = [
{ x: -100, y: 100 }, // Origin point
{ x: 200, y: 150 }, // Middle point
{ x: 300, y: 200 }, // Far point
];
testPoints.forEach((point) => {
const localPoint = rect.toLocalSpace(point);
const backToParent = rect.toParentSpace(localPoint);
expectPointClose(backToParent, point);
});
});
describe('corner', () => {
test('set topLeft with local space coordinates', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
});
// Move top-left corner 50 units right and 25 units down in local space
rect.topLeft = { 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('set topRight with local space coordinates', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
});
// Set top-right corner to local coordinates (150, 25)
rect.topRight = { 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('set bottomRight with local space coordinates', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
});
// Set bottom-right corner to local coordinates (150, 75)
rect.bottomRight = { 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('set bottomLeft with local space coordinates', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
});
// Move bottom-left corner 50 units right in local space
rect.bottomLeft = { 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 with rotation', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI / 4, // 45 degrees
});
// Move top-left corner in local space
rect.topLeft = { x: 50, y: 25 };
// 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('set bottomRight works with upside down rotation', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI, // 180 degrees - upside down
});
// Set bottom-right corner in local space
rect.bottomRight = { x: 150, y: 75 };
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 });
});
test('resizing from corners keeps the opposite corner fixed without rotation', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: 0,
});
const originalTopLeft = rect.topLeft;
// Resize from bottom-right corner
rect.bottomRight = { x: 300, y: 200 };
// Opposite corner (top-left) should remain the same
expectPointClose(rect.topLeft, originalTopLeft);
});
test('resizing from corners keeps the opposite corner fixed with rotation', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 200,
height: 100,
rotation: Math.PI / 4, // 45 degrees
});
const originalTopLeft = rect.toParentSpace(rect.topLeft);
const originalBottomRight = rect.toParentSpace(rect.bottomRight);
// Resize from bottom-right corner in local space
rect.bottomRight = { x: 300, y: 150 };
// Transform corners back to parent space to compare
const newTopLeft = rect.toParentSpace(rect.topLeft);
const newBottomRight = rect.toParentSpace(rect.bottomRight);
// Opposite corner (top-left) should remain the same in parent space
expectPointClose(newTopLeft, originalTopLeft);
// New bottom-right should be updated correctly in parent space
// Calculate the expected new bottom-right position
const expectedBottomRightLocal = { x: 300, y: 150 };
const expectedBottomRightParent = rect.toParentSpace(expectedBottomRightLocal);
expectPointClose(newBottomRight, expectedBottomRightParent);
});
});
describe('point conversion with rotation', () => {
test('converts points correctly with 45-degree rotation', () => {
const rect = new DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform({
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 DOMRectTransform({
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);
});
});
describe('transform and rotate origins', () => {
test('constructor initializes with default origins at center', () => {
const rect = new DOMRectTransform();
expectPointClose(rect.transformOrigin, { x: 0.5, y: 0.5 });
expectPointClose(rect.rotateOrigin, { x: 0.5, y: 0.5 });
});
test('constructor accepts custom origins', () => {
const rect = new DOMRectTransform({
transformOrigin: { x: 0, y: 0 },
rotateOrigin: { x: 1, y: 1 },
});
expectPointClose(rect.transformOrigin, { x: 0, y: 0 });
expectPointClose(rect.rotateOrigin, { x: 1, y: 1 });
});
test('maintains point relationships with custom origins', () => {
const rect = new DOMRectTransform({
x: 100,
y: 100,
width: 100,
height: 100,
rotation: Math.PI / 3, // 60 degrees
transformOrigin: { x: 0.25, y: 0.75 },
rotateOrigin: { x: 0.75, y: 0.25 },
});
// Test multiple points
const points = [
{ x: 0, y: 0 },
{ x: 100, y: 0 },
{ x: 100, y: 100 },
{ x: 0, y: 100 },
];
// Transform all points to parent space and back
points.forEach((point) => {
const transformed = rect.toParentSpace(point);
const backToLocal = rect.toLocalSpace(transformed);
expectPointClose(backToLocal, point);
});
});
});
});
describe('TransformDOMRectReadonly', () => {
test('prevents modifications through setters', () => {
const rect = new DOMRectTransformReadonly({
x: 10,
y: 20,
width: 100,
height: 50,
});
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', () => {
const rect = new DOMRectTransformReadonly({
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);
});
});