rename
This commit is contained in:
parent
a5cb6fae94
commit
67a3d23b4d
|
|
@ -1,5 +1,5 @@
|
|||
import { expect, test, describe } from 'bun:test';
|
||||
import { RotatedDOMRect } from '../common/rotated-dom-rect-2';
|
||||
import { TransformDOMRect } from '../common/transform-dom-rect';
|
||||
import { Vector } from '../common/Vector';
|
||||
|
||||
// Helper for comparing points with floating point values
|
||||
|
|
@ -11,7 +11,7 @@ const expectPointClose = (actual: { x: number; y: number }, expected: { x: numbe
|
|||
describe('RotatedDOMRect', () => {
|
||||
describe('constructor', () => {
|
||||
test('initializes with default values', () => {
|
||||
const rect = new RotatedDOMRect();
|
||||
const rect = new TransformDOMRect();
|
||||
expect(rect.x).toBe(0);
|
||||
expect(rect.y).toBe(0);
|
||||
expect(rect.width).toBe(0);
|
||||
|
|
@ -20,7 +20,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('initializes with custom values', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 10,
|
||||
y: 20,
|
||||
width: 100,
|
||||
|
|
@ -37,7 +37,7 @@ describe('RotatedDOMRect', () => {
|
|||
|
||||
describe('corner calculations', () => {
|
||||
test('calculates corners for unrotated rectangle', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -52,7 +52,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('calculates corners for 90-degree rotated rectangle', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -69,7 +69,7 @@ describe('RotatedDOMRect', () => {
|
|||
|
||||
describe('bounds', () => {
|
||||
test('calculates bounds for unrotated rectangle', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -86,7 +86,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('calculates bounds for 45-degree rotated rectangle', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -109,7 +109,7 @@ describe('RotatedDOMRect', () => {
|
|||
|
||||
describe('setters', () => {
|
||||
test('updates corners when center is modified', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
width: 100,
|
||||
height: 50,
|
||||
});
|
||||
|
|
@ -120,7 +120,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('updates dimensions and rotation when setting topRight', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -143,7 +143,7 @@ describe('RotatedDOMRect', () => {
|
|||
|
||||
describe('corner setters', () => {
|
||||
test('updates dimensions when setting bottomLeft', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -174,7 +174,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('maintains rectangle properties when setting corners', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 100,
|
||||
|
|
@ -193,7 +193,7 @@ describe('RotatedDOMRect', () => {
|
|||
|
||||
describe('edge cases', () => {
|
||||
test('handles zero dimensions', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
x: 10,
|
||||
y: 10,
|
||||
width: 0,
|
||||
|
|
@ -210,7 +210,7 @@ describe('RotatedDOMRect', () => {
|
|||
});
|
||||
|
||||
test('handles 360-degree rotation', () => {
|
||||
const rect = new RotatedDOMRect({
|
||||
const rect = new TransformDOMRect({
|
||||
width: 100,
|
||||
height: 50,
|
||||
rotation: Math.PI * 2,
|
||||
|
|
@ -0,0 +1,283 @@
|
|||
import type { Point } from './types';
|
||||
|
||||
// TODO: find right value for precision
|
||||
const roundToDomPrecision = (value: number) => Math.round(value * 100000) / 100000;
|
||||
|
||||
const PI2 = Math.PI * 2;
|
||||
const TAU = Math.PI / 2;
|
||||
|
||||
export interface MatrixInit {
|
||||
a: number;
|
||||
b: number;
|
||||
c: number;
|
||||
d: number;
|
||||
e: number;
|
||||
f: number;
|
||||
}
|
||||
|
||||
export interface IMatrix extends MatrixInit {
|
||||
equals(m: MatrixInit): boolean;
|
||||
identity(): Matrix;
|
||||
multiply(m: MatrixInit): Matrix;
|
||||
rotate(r: number, cx?: number, cy?: number): Matrix;
|
||||
translate(x: number, y: number): Matrix;
|
||||
scale(x: number, y: number): Matrix;
|
||||
invert(): Matrix;
|
||||
applyToPoint(point: Point): Point;
|
||||
applyToPoints(points: Point[]): Point[];
|
||||
rotation(): number;
|
||||
point(): Point;
|
||||
decompose(): { x: number; y: number; scaleX: number; scaleY: number; rotation: number };
|
||||
clone(): Matrix;
|
||||
toString(): string;
|
||||
toDOMMatrix(): DOMMatrix;
|
||||
}
|
||||
|
||||
export class Matrix implements IMatrix {
|
||||
constructor(a: number, b: number, c: number, d: number, e: number, f: number) {
|
||||
this.a = a;
|
||||
this.b = b;
|
||||
this.c = c;
|
||||
this.d = d;
|
||||
this.e = e;
|
||||
this.f = f;
|
||||
}
|
||||
|
||||
a = 1.0;
|
||||
b = 0.0;
|
||||
c = 0.0;
|
||||
d = 1.0;
|
||||
e = 0.0;
|
||||
f = 0.0;
|
||||
|
||||
equals(m: MatrixInit) {
|
||||
return this.a === m.a && this.b === m.b && this.c === m.c && this.d === m.d && this.e === m.e && this.f === m.f;
|
||||
}
|
||||
|
||||
identity() {
|
||||
this.a = 1.0;
|
||||
this.b = 0.0;
|
||||
this.c = 0.0;
|
||||
this.d = 1.0;
|
||||
this.e = 0.0;
|
||||
this.f = 0.0;
|
||||
return this;
|
||||
}
|
||||
|
||||
multiply(m: MatrixInit) {
|
||||
const { a, b, c, d, e, f } = this;
|
||||
this.a = a * m.a + c * m.b;
|
||||
this.c = a * m.c + c * m.d;
|
||||
this.e = a * m.e + c * m.f + e;
|
||||
this.b = b * m.a + d * m.b;
|
||||
this.d = b * m.c + d * m.d;
|
||||
this.f = b * m.e + d * m.f + f;
|
||||
return this;
|
||||
}
|
||||
|
||||
rotate(r: number, cx?: number, cy?: number) {
|
||||
if (r === 0) return this;
|
||||
if (cx === undefined) return this.multiply(Matrix.Rotate(r));
|
||||
return this.translate(cx, cy!).multiply(Matrix.Rotate(r)).translate(-cx, -cy!);
|
||||
}
|
||||
|
||||
translate(x: number, y: number): Matrix {
|
||||
return this.multiply(Matrix.Translate(x, y!));
|
||||
}
|
||||
|
||||
scale(x: number, y: number) {
|
||||
return this.multiply(Matrix.Scale(x, y));
|
||||
}
|
||||
|
||||
invert() {
|
||||
const { a, b, c, d, e, f } = this;
|
||||
const denominator = a * d - b * c;
|
||||
this.a = d / denominator;
|
||||
this.b = b / -denominator;
|
||||
this.c = c / -denominator;
|
||||
this.d = a / denominator;
|
||||
this.e = (d * e - c * f) / -denominator;
|
||||
this.f = (b * e - a * f) / denominator;
|
||||
return this;
|
||||
}
|
||||
|
||||
applyToPoint(point: Point) {
|
||||
return Matrix.applyToPoint(this, point);
|
||||
}
|
||||
|
||||
applyToPoints(points: Point[]) {
|
||||
return Matrix.applyToPoints(this, points);
|
||||
}
|
||||
|
||||
rotation() {
|
||||
return Matrix.Rotation(this);
|
||||
}
|
||||
|
||||
point() {
|
||||
return Matrix.ToPoint(this);
|
||||
}
|
||||
|
||||
decompose() {
|
||||
return Matrix.Decompose(this);
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new Matrix(this.a, this.b, this.c, this.d, this.e, this.f);
|
||||
}
|
||||
|
||||
toDOMMatrix(): DOMMatrix {
|
||||
return new DOMMatrix([this.a, this.b, this.c, this.d, this.e, this.f]);
|
||||
}
|
||||
|
||||
static Rotate(r: number, cx?: number, cy?: number) {
|
||||
if (r === 0) return Matrix.Identity();
|
||||
|
||||
const cosAngle = Math.cos(r);
|
||||
const sinAngle = Math.sin(r);
|
||||
|
||||
const rotationMatrix = new Matrix(cosAngle, sinAngle, -sinAngle, cosAngle, 0.0, 0.0);
|
||||
|
||||
if (cx === undefined) return rotationMatrix;
|
||||
|
||||
return Matrix.Compose(Matrix.Translate(cx, cy!), rotationMatrix, Matrix.Translate(-cx, -cy!));
|
||||
}
|
||||
|
||||
static Scale: {
|
||||
(x: number, y: number): MatrixInit;
|
||||
(x: number, y: number, cx: number, cy: number): MatrixInit;
|
||||
} = (x: number, y: number, cx?: number, cy?: number) => {
|
||||
const scaleMatrix = new Matrix(x, 0, 0, y, 0, 0);
|
||||
|
||||
if (cx === undefined) return scaleMatrix;
|
||||
|
||||
return Matrix.Compose(Matrix.Translate(cx, cy!), scaleMatrix, Matrix.Translate(-cx, -cy!));
|
||||
};
|
||||
|
||||
static Multiply(m1: MatrixInit, m2: MatrixInit): MatrixInit {
|
||||
return {
|
||||
a: m1.a * m2.a + m1.c * m2.b,
|
||||
c: m1.a * m2.c + m1.c * m2.d,
|
||||
e: m1.a * m2.e + m1.c * m2.f + m1.e,
|
||||
b: m1.b * m2.a + m1.d * m2.b,
|
||||
d: m1.b * m2.c + m1.d * m2.d,
|
||||
f: m1.b * m2.e + m1.d * m2.f + m1.f,
|
||||
};
|
||||
}
|
||||
|
||||
static Inverse(m: MatrixInit): MatrixInit {
|
||||
const denominator = m.a * m.d - m.b * m.c;
|
||||
return {
|
||||
a: m.d / denominator,
|
||||
b: m.b / -denominator,
|
||||
c: m.c / -denominator,
|
||||
d: m.a / denominator,
|
||||
e: (m.d * m.e - m.c * m.f) / -denominator,
|
||||
f: (m.b * m.e - m.a * m.f) / denominator,
|
||||
};
|
||||
}
|
||||
|
||||
static Absolute(m: MatrixInit): MatrixInit {
|
||||
const denominator = m.a * m.d - m.b * m.c;
|
||||
return {
|
||||
a: m.d / denominator,
|
||||
b: m.b / -denominator,
|
||||
c: m.c / -denominator,
|
||||
d: m.a / denominator,
|
||||
e: (m.d * m.e - m.c * m.f) / denominator,
|
||||
f: (m.b * m.e - m.a * m.f) / -denominator,
|
||||
};
|
||||
}
|
||||
|
||||
static Compose(...matrices: MatrixInit[]) {
|
||||
const matrix = Matrix.Identity();
|
||||
for (let i = 0, n = matrices.length; i < n; i++) {
|
||||
matrix.multiply(matrices[i]);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
static Identity() {
|
||||
return new Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0);
|
||||
}
|
||||
|
||||
static Translate(x: number, y: number) {
|
||||
return new Matrix(1.0, 0.0, 0.0, 1.0, x, y);
|
||||
}
|
||||
|
||||
static ToPoint(m: MatrixInit): Point {
|
||||
return { x: m.e, y: m.f };
|
||||
}
|
||||
|
||||
static Rotation(m: MatrixInit): number {
|
||||
let rotation;
|
||||
|
||||
if (m.a !== 0 || m.c !== 0) {
|
||||
const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5;
|
||||
rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1);
|
||||
} else if (m.b !== 0 || m.d !== 0) {
|
||||
const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5;
|
||||
rotation = TAU + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1);
|
||||
} else {
|
||||
rotation = 0;
|
||||
}
|
||||
|
||||
return clampRotation(rotation);
|
||||
}
|
||||
|
||||
static Decompose(m: MatrixInit) {
|
||||
let scaleX, scaleY, rotation;
|
||||
|
||||
if (m.a !== 0 || m.c !== 0) {
|
||||
const hypotAc = (m.a * m.a + m.c * m.c) ** 0.5;
|
||||
scaleX = hypotAc;
|
||||
scaleY = (m.a * m.d - m.b * m.c) / hypotAc;
|
||||
rotation = Math.acos(m.a / hypotAc) * (m.c > 0 ? -1 : 1);
|
||||
} else if (m.b !== 0 || m.d !== 0) {
|
||||
const hypotBd = (m.b * m.b + m.d * m.d) ** 0.5;
|
||||
scaleX = (m.a * m.d - m.b * m.c) / hypotBd;
|
||||
scaleY = hypotBd;
|
||||
rotation = TAU + Math.acos(m.b / hypotBd) * (m.d > 0 ? -1 : 1);
|
||||
} else {
|
||||
scaleX = 0;
|
||||
scaleY = 0;
|
||||
rotation = 0;
|
||||
}
|
||||
|
||||
return {
|
||||
x: m.e,
|
||||
y: m.f,
|
||||
scaleX,
|
||||
scaleY,
|
||||
rotation: clampRotation(rotation),
|
||||
};
|
||||
}
|
||||
|
||||
static applyToPoint(m: MatrixInit, point: Point) {
|
||||
return { x: m.a * point.x + m.c * point.y + m.e, y: m.b * point.x + m.d * point.y + m.f };
|
||||
}
|
||||
|
||||
static applyToPoints(m: MatrixInit, points: Point[]): Point[] {
|
||||
return points.map((point) => ({ x: m.a * point.x + m.c * point.y + m.e, y: m.b * point.x + m.d * point.y + m.f }));
|
||||
}
|
||||
|
||||
static From(m: MatrixInit | DOMMatrix) {
|
||||
if (m instanceof DOMMatrix) {
|
||||
return Matrix.FromDOMMatrix(m);
|
||||
}
|
||||
return new Matrix(m.a, m.b, m.c, m.d, m.e, m.f);
|
||||
}
|
||||
|
||||
static FromDOMMatrix(domMatrix: DOMMatrix): Matrix {
|
||||
return new Matrix(domMatrix.a, domMatrix.b, domMatrix.c, domMatrix.d, domMatrix.e, domMatrix.f);
|
||||
}
|
||||
|
||||
static toString(m: MatrixInit) {
|
||||
return `matrix(${roundToDomPrecision(m.a)}, ${roundToDomPrecision(m.b)}, ${roundToDomPrecision(
|
||||
m.c
|
||||
)}, ${roundToDomPrecision(m.d)}, ${roundToDomPrecision(m.e)}, ${roundToDomPrecision(m.f)})`;
|
||||
}
|
||||
}
|
||||
|
||||
function clampRotation(radians: number) {
|
||||
return (PI2 + radians) % PI2;
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
import { Point } from './types';
|
||||
import { Vector } from './Vector';
|
||||
|
||||
type RotatedDOMRectInit = {
|
||||
x?: number;
|
||||
y?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
rotation?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a rectangle that can be rotated in 2D space.
|
||||
* All coordinates are relative to the center of the rectangle.
|
||||
*/
|
||||
interface IRotatedDOMRect {
|
||||
/** X coordinate of the rectangle's center */
|
||||
x: number;
|
||||
/** Y coordinate of the rectangle's center */
|
||||
y: number;
|
||||
/** Center point of the rectangle */
|
||||
center: Readonly<Point>;
|
||||
/** Width of the rectangle */
|
||||
width: number;
|
||||
/** Height of the rectangle */
|
||||
height: number;
|
||||
/** Rotation of the rectangle in radians */
|
||||
rotation: number;
|
||||
/** Top-left corner of the rotated rectangle */
|
||||
topLeft: Readonly<Point>;
|
||||
/** Top-right corner of the rotated rectangle */
|
||||
topRight: Readonly<Point>;
|
||||
/** Bottom-left corner of the rotated rectangle */
|
||||
bottomLeft: Readonly<Point>;
|
||||
/** Bottom-right corner of the rotated rectangle */
|
||||
bottomRight: Readonly<Point>;
|
||||
/**
|
||||
* Returns an axis-aligned bounding box that contains the rotated rectangle
|
||||
* @returns A DOMRectInit object representing the bounds
|
||||
*/
|
||||
getBounds(): Required<DOMRectInit>;
|
||||
|
||||
/** Mutate multiple properties at once efficiently */
|
||||
update(updates: Partial<RotatedDOMRectInit>): void;
|
||||
|
||||
/** Create a new instance with modified properties */
|
||||
with(updates: Partial<RotatedDOMRectInit>): RotatedDOMRect;
|
||||
}
|
||||
|
||||
export class RotatedDOMRect implements IRotatedDOMRect {
|
||||
private _center: Point = { x: 0, y: 0 };
|
||||
private _width: number = 0;
|
||||
private _height: number = 0;
|
||||
private _rotation: number = 0;
|
||||
|
||||
// Cached derived values
|
||||
private _sinR: number | null = null;
|
||||
private _cosR: number | null = null;
|
||||
private _topLeftX: number | null = null;
|
||||
private _topLeftY: number | null = null;
|
||||
private _topRightX: number | null = null;
|
||||
private _topRightY: number | null = null;
|
||||
private _bottomLeftX: number | null = null;
|
||||
private _bottomLeftY: number | null = null;
|
||||
private _bottomRightX: number | null = null;
|
||||
private _bottomRightY: number | null = null;
|
||||
|
||||
constructor({ x, y, width, height, rotation }: RotatedDOMRectInit = {}) {
|
||||
this._center = { x: x ?? 0, y: y ?? 0 };
|
||||
this._width = width ?? 0;
|
||||
this._height = height ?? 0;
|
||||
this._rotation = rotation ?? 0;
|
||||
}
|
||||
|
||||
/* ——— Getters ——— */
|
||||
|
||||
get center(): Readonly<Point> {
|
||||
return this._center;
|
||||
}
|
||||
|
||||
get x(): number {
|
||||
return this._center.x;
|
||||
}
|
||||
|
||||
get y(): number {
|
||||
return this._center.y;
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return this._height;
|
||||
}
|
||||
|
||||
get rotation(): number {
|
||||
return this._rotation;
|
||||
}
|
||||
|
||||
get topLeft(): Readonly<Point> {
|
||||
this.deriveValuesIfCacheInvalid();
|
||||
return { x: this._topLeftX!, y: this._topLeftY! };
|
||||
}
|
||||
|
||||
get topRight(): Readonly<Point> {
|
||||
this.deriveValuesIfCacheInvalid();
|
||||
return { x: this._topRightX!, y: this._topRightY! };
|
||||
}
|
||||
|
||||
get bottomLeft(): Readonly<Point> {
|
||||
this.deriveValuesIfCacheInvalid();
|
||||
return { x: this._bottomLeftX!, y: this._bottomLeftY! };
|
||||
}
|
||||
|
||||
get bottomRight(): Readonly<Point> {
|
||||
this.deriveValuesIfCacheInvalid();
|
||||
return { x: this._bottomRightX!, y: this._bottomRightY! };
|
||||
}
|
||||
|
||||
/* ——— Setters ——— */
|
||||
|
||||
set center(point: Point) {
|
||||
this._center = point;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set x(value: number) {
|
||||
this._center.x = value;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set y(value: number) {
|
||||
this._center.y = value;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set width(value: number) {
|
||||
this._width = value;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set height(value: number) {
|
||||
this._height = value;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set rotation(value: number) {
|
||||
this._rotation = value;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
set topLeft(point: Point) {
|
||||
this.moveCorner(point, this.bottomRight);
|
||||
}
|
||||
|
||||
set topRight(point: Point) {
|
||||
this.moveCorner(point, this.bottomLeft);
|
||||
}
|
||||
|
||||
set bottomLeft(point: Point) {
|
||||
this.moveCorner(point, this.topRight);
|
||||
}
|
||||
|
||||
set bottomRight(point: Point) {
|
||||
this.moveCorner(point, this.topLeft);
|
||||
}
|
||||
|
||||
getBounds(): Required<DOMRectInit> {
|
||||
if (!this.isCacheValid()) {
|
||||
this.deriveValuesIfCacheInvalid();
|
||||
}
|
||||
|
||||
const minX = Math.min(this._topLeftX!, this._topRightX!, this._bottomLeftX!, this._bottomRightX!);
|
||||
const maxX = Math.max(this._topLeftX!, this._topRightX!, this._bottomLeftX!, this._bottomRightX!);
|
||||
const minY = Math.min(this._topLeftY!, this._topRightY!, this._bottomLeftY!, this._bottomRightY!);
|
||||
const maxY = Math.max(this._topLeftY!, this._topRightY!, this._bottomLeftY!, this._bottomRightY!);
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
width: maxX - minX,
|
||||
height: maxY - minY,
|
||||
};
|
||||
}
|
||||
|
||||
/** Mutate multiple properties at once efficiently */
|
||||
update(updates: Partial<RotatedDOMRectInit>) {
|
||||
if (updates.x !== undefined) this._center.x = updates.x;
|
||||
if (updates.y !== undefined) this._center.y = updates.y;
|
||||
if (updates.width !== undefined) this._width = updates.width;
|
||||
if (updates.height !== undefined) this._height = updates.height;
|
||||
if (updates.rotation !== undefined) this._rotation = updates.rotation;
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
/** Create a new instance with modified properties */
|
||||
with(updates: Partial<RotatedDOMRectInit>): RotatedDOMRect {
|
||||
return new RotatedDOMRect({
|
||||
x: updates.x ?? this._center.x,
|
||||
y: updates.y ?? this._center.y,
|
||||
width: updates.width ?? this._width,
|
||||
height: updates.height ?? this._height,
|
||||
rotation: updates.rotation ?? this._rotation,
|
||||
});
|
||||
}
|
||||
|
||||
/* ——— Private methods ——— */
|
||||
|
||||
private moveCorner(newCorner: Point, oppositeCorner: Point) {
|
||||
// Calculate new center midway between the two corners
|
||||
this._center = Vector.lerp(newCorner, oppositeCorner, 0.5);
|
||||
|
||||
// Get vector from opposite corner to new corner
|
||||
const delta = Vector.sub(newCorner, oppositeCorner);
|
||||
|
||||
// Rotate the delta vector back by -rotation to get width/height
|
||||
const rotated = Vector.rotate(delta, -this._rotation);
|
||||
|
||||
// Update width and height with absolute values
|
||||
this._width = Math.abs(rotated.x);
|
||||
this._height = Math.abs(rotated.y);
|
||||
|
||||
// Invalidate cache to ensure all derived values are recalculated
|
||||
this.invalidateCache();
|
||||
}
|
||||
|
||||
private getTrigValues(): { sin: number; cos: number } {
|
||||
if (this._sinR === null || this._cosR === null) {
|
||||
this._sinR = Math.sin(this._rotation);
|
||||
this._cosR = Math.cos(this._rotation);
|
||||
}
|
||||
return { sin: this._sinR, cos: this._cosR };
|
||||
}
|
||||
|
||||
private deriveValuesIfCacheInvalid() {
|
||||
if (this.isCacheValid()) return;
|
||||
|
||||
const halfWidth = this._width / 2;
|
||||
const halfHeight = this._height / 2;
|
||||
const { sin, cos } = this.getTrigValues();
|
||||
|
||||
this._topLeftX = this._center.x - halfWidth * cos + halfHeight * sin;
|
||||
this._topLeftY = this._center.y - halfWidth * sin - halfHeight * cos;
|
||||
|
||||
this._topRightX = this._center.x + halfWidth * cos + halfHeight * sin;
|
||||
this._topRightY = this._center.y + halfWidth * sin - halfHeight * cos;
|
||||
|
||||
this._bottomLeftX = this._center.x - halfWidth * cos - halfHeight * sin;
|
||||
this._bottomLeftY = this._center.y - halfWidth * sin + halfHeight * cos;
|
||||
|
||||
this._bottomRightX = this._center.x + halfWidth * cos - halfHeight * sin;
|
||||
this._bottomRightY = this._center.y + halfWidth * sin + halfHeight * cos;
|
||||
}
|
||||
|
||||
private invalidateCache() {
|
||||
this._sinR = null;
|
||||
this._cosR = null;
|
||||
this._topLeftX = null;
|
||||
this._topLeftY = null;
|
||||
this._topRightX = null;
|
||||
this._topRightY = null;
|
||||
this._bottomLeftX = null;
|
||||
this._bottomLeftY = null;
|
||||
this._bottomRightX = null;
|
||||
this._bottomRightY = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the cached corner coordinates are valid.
|
||||
* Type assertion is safe because invalidateCache() clears all values atomically,
|
||||
* and calculateCorners() sets all values atomically.
|
||||
*/
|
||||
private isCacheValid(): this is {
|
||||
_topLeftX: number;
|
||||
_topLeftY: number;
|
||||
_topRightX: number;
|
||||
_topRightY: number;
|
||||
_bottomLeftX: number;
|
||||
_bottomLeftY: number;
|
||||
_bottomRightX: number;
|
||||
_bottomRightY: number;
|
||||
_sinR: number;
|
||||
_cosR: number;
|
||||
} {
|
||||
return this._topLeftX !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { Point } from './types';
|
||||
import { Vector } from './Vector';
|
||||
|
||||
interface RotatedDOMRectInit {
|
||||
interface TransformDOMRectInit {
|
||||
height?: number;
|
||||
width?: number;
|
||||
x?: number;
|
||||
|
|
@ -9,10 +9,10 @@ interface RotatedDOMRectInit {
|
|||
rotation?: number;
|
||||
}
|
||||
|
||||
export class RotatedDOMRect implements DOMRect {
|
||||
#other: RotatedDOMRectInit;
|
||||
export class TransformDOMRect implements DOMRect {
|
||||
#other: TransformDOMRectInit;
|
||||
|
||||
constructor(other: RotatedDOMRectInit = {}) {
|
||||
constructor(other: TransformDOMRectInit = {}) {
|
||||
this.#other = other;
|
||||
}
|
||||
|
||||
|
|
@ -135,10 +135,10 @@ export class RotatedDOMRect implements DOMRect {
|
|||
}
|
||||
|
||||
// We cant just override the setter, we need to override the getter and setter.
|
||||
export class RotatedDOMRectReadonly extends RotatedDOMRect {
|
||||
#other: RotatedDOMRectInit;
|
||||
export class TransformDOMRectReadonly extends TransformDOMRect {
|
||||
#other: TransformDOMRectInit;
|
||||
|
||||
constructor(other: RotatedDOMRectInit = {}) {
|
||||
constructor(other: TransformDOMRectInit = {}) {
|
||||
super(other);
|
||||
this.#other = other;
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { Vector } from './common/Vector.ts';
|
||||
import type { Point } from './common/types.ts';
|
||||
import { RotatedDOMRect } from './common/rotated-dom-rect.ts';
|
||||
import { TransformDOMRect } from './common/transform-dom-rect.ts';
|
||||
import { FolkBaseConnection } from './folk-base-connection.ts';
|
||||
|
||||
const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage;
|
||||
|
|
@ -120,11 +120,11 @@ export class FolkRope extends FolkBaseConnection {
|
|||
this.draw();
|
||||
};
|
||||
|
||||
override render(sourceRect: RotatedDOMRect | DOMRectReadOnly, targetRect: RotatedDOMRect | DOMRectReadOnly) {
|
||||
override render(sourceRect: TransformDOMRect | DOMRectReadOnly, targetRect: TransformDOMRect | DOMRectReadOnly) {
|
||||
let source: Point;
|
||||
let target: Point;
|
||||
|
||||
if (sourceRect instanceof RotatedDOMRect) {
|
||||
if (sourceRect instanceof TransformDOMRect) {
|
||||
source = Vector.lerp(sourceRect.bottomRight, sourceRect.bottomLeft, 0.5);
|
||||
} else {
|
||||
source = {
|
||||
|
|
@ -133,7 +133,7 @@ export class FolkRope extends FolkBaseConnection {
|
|||
};
|
||||
}
|
||||
|
||||
if (targetRect instanceof RotatedDOMRect) {
|
||||
if (targetRect instanceof TransformDOMRect) {
|
||||
target = Vector.lerp(targetRect.bottomRight, targetRect.bottomLeft, 0.5);
|
||||
} else {
|
||||
target = {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { css, html } from './common/tags';
|
||||
import { ResizeObserverManager } from './common/resize-observer';
|
||||
import { Point } from './common/types';
|
||||
import { RotatedDOMRectReadonly } from './common/rotated-dom-rect';
|
||||
import { TransformDOMRectReadonly } from './common/transform-dom-rect';
|
||||
import { Vector } from './common/Vector';
|
||||
import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors';
|
||||
|
||||
|
|
@ -357,7 +357,7 @@ export class FolkShape extends HTMLElement {
|
|||
getClientRect() {
|
||||
const { x, y, width, height, rotation } = this;
|
||||
|
||||
return new RotatedDOMRectReadonly({ x, y, width, height, rotation });
|
||||
return new TransformDOMRectReadonly({ x, y, width, height, rotation });
|
||||
}
|
||||
|
||||
// Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape.
|
||||
|
|
|
|||
Loading…
Reference in New Issue