From 74cb31bf4b0c9dceab15cd678726d6eaf0a18890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9Cchrisshank=E2=80=9D?= Date: Wed, 4 Dec 2024 00:49:57 -0800 Subject: [PATCH] RotatedDOMRect --- src/common/rotated-dom-rect.ts | 170 +++++++++++++++++++++++++++++++++ src/common/types.ts | 14 --- src/folk-distance-field.ts | 7 +- src/folk-event-propagator.ts | 5 - src/folk-rope.ts | 13 ++- src/folk-shape.ts | 74 ++++---------- 6 files changed, 202 insertions(+), 81 deletions(-) create mode 100644 src/common/rotated-dom-rect.ts diff --git a/src/common/rotated-dom-rect.ts b/src/common/rotated-dom-rect.ts new file mode 100644 index 0000000..a145a66 --- /dev/null +++ b/src/common/rotated-dom-rect.ts @@ -0,0 +1,170 @@ +import { Point } from './types'; +import { Vector } from './Vector'; + +interface RotatedDOMRectInit { + height?: number; + width?: number; + x?: number; + y?: number; + rotation?: number; +} + +export class RotatedDOMRect implements DOMRect { + #other: RotatedDOMRectInit; + + constructor(other: RotatedDOMRectInit = {}) { + this.#other = other; + } + + get x(): number { + return this.#other.x ?? 0; + } + set x(x: number) { + this.#other.x = x; + this.#reset(); + } + + get y(): number { + return this.#other.y ?? 0; + } + set y(y: number) { + this.#other.y = y; + this.#reset(); + } + + get height(): number { + return this.#other.height ?? 0; + } + set height(height: number) { + this.#other.height = height; + this.#reset(); + } + + get width(): number { + return this.#other.width ?? 0; + } + set width(width: number) { + this.#other.width = width; + this.#reset(); + } + + get rotation(): number { + return this.#other.rotation ?? 0; + } + set rotation(rotation: number) { + this.#other.rotation = rotation; + this.#reset(); + } + + get left(): number { + return this.x; + } + + get top(): number { + return this.y; + } + + get right(): number { + return this.x + this.width; + } + + get bottom(): number { + return this.y + this.height; + } + + #center: Point | null = null; + /** Returns the center point in worldspace coordinates */ + get center(): Point { + if (this.#center === null) { + this.#center = { + x: this.x + this.width / 2, + y: this.y + this.height / 2, + }; + } + return this.#center; + } + + #topLeftCorner: Point | null = null; + get topLeftCorner() { + if (this.#topLeftCorner === null) { + this.#topLeftCorner = Vector.rotateAround({ x: this.x, y: this.y }, this.center, this.rotation); + } + return this.#topLeftCorner; + } + + #topRightCorner: Point | null = null; + get topRightCorner() { + if (this.#topRightCorner === null) { + this.#topRightCorner = Vector.rotateAround({ x: this.right, y: this.y }, this.center, this.rotation); + } + return this.#topRightCorner; + } + + #bottomRightCorner: Point | null = null; + get bottomRightCorner() { + if (this.#bottomRightCorner === null) { + this.#bottomRightCorner = Vector.rotateAround({ x: this.right, y: this.bottom }, this.center, this.rotation); + } + return this.#bottomRightCorner; + } + + #bottomLeftCorner: Point | null = null; + get bottomLeftCorner() { + if (this.#bottomLeftCorner === null) { + this.#bottomLeftCorner = Vector.rotateAround({ x: this.x, y: this.bottom }, this.center, this.rotation); + } + return this.#bottomLeftCorner; + } + + #reset() { + this.#center = null; + this.#topLeftCorner = null; + this.#topRightCorner = null; + this.#bottomLeftCorner = null; + this.#bottomRightCorner = null; + } + + /** Returns all the vertices in worldspace coordinates */ + vertices(): Point[] { + return []; + } + + toJSON() { + return {}; + } +} + +// We cant just override the setter, we need to override the getter and setter. +export class RotatedDOMRectReadonly extends RotatedDOMRect { + #other: RotatedDOMRectInit; + + constructor(other: RotatedDOMRectInit = {}) { + super(other); + this.#other = other; + } + + get x(): number { + return this.#other.x ?? 0; + } + set x(x: number) {} + + get y(): number { + return this.#other.y ?? 0; + } + set y(y: number) {} + + get height(): number { + return this.#other.height ?? 0; + } + set height(height: number) {} + + get width(): number { + return this.#other.width ?? 0; + } + set width(width: number) {} + + get rotation(): number { + return this.#other.rotation ?? 0; + } + set rotation(rotation: number) {} +} diff --git a/src/common/types.ts b/src/common/types.ts index 27bc240..5816552 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,15 +1 @@ export type Point = { x: number; y: number }; - -export type RotatedDOMRect = DOMRect & { - /** in radians */ - rotation: number; - - /** Returns the center point in worldspace coordinates */ - center(): Point; - - /** Returns the four corners in worldspace coordinates, in clockwise order starting from the top left */ - corners(): [Point, Point, Point, Point]; - - /** Returns all the vertices in worldspace coordinates */ - vertices(): Point[]; -}; diff --git a/src/folk-distance-field.ts b/src/folk-distance-field.ts index e811c4d..a9d371f 100644 --- a/src/folk-distance-field.ts +++ b/src/folk-distance-field.ts @@ -151,7 +151,12 @@ export class FolkDistanceField extends HTMLElement { // Collect positions and assign unique IDs to all shapes this.shapes.forEach((geometry, index) => { const rect = geometry.getClientRect(); - const [topLeft, topRight, bottomRight, bottomLeft] = rect.corners(); + const { + topLeftCorner: topLeft, + topRightCorner: topRight, + bottomRightCorner: bottomRight, + bottomLeftCorner: bottomLeft, + } = rect; // Convert rotated coordinates to NDC using container dimensions const x1 = (topLeft.x / containerWidth) * 2 - 1; diff --git a/src/folk-event-propagator.ts b/src/folk-event-propagator.ts index 596b0e7..bf55150 100644 --- a/src/folk-event-propagator.ts +++ b/src/folk-event-propagator.ts @@ -1,5 +1,4 @@ import { css } from './common/tags.ts'; -import type { RotatedDOMRect } from './common/types'; import { FolkRope } from './folk-rope.ts'; import * as parser from '@babel/parser'; import type { Node } from '@babel/types'; @@ -137,10 +136,6 @@ to.${key} = ${value};`); this.expression = this.#expressionTextarea.value = this.getAttribute('expression') || ''; } - override render(sourceRect: RotatedDOMRect | DOMRectReadOnly, targetRect: RotatedDOMRect | DOMRectReadOnly) { - super.render(sourceRect, targetRect); - } - override draw() { super.draw(); diff --git a/src/folk-rope.ts b/src/folk-rope.ts index 482c851..dc8258d 100644 --- a/src/folk-rope.ts +++ b/src/folk-rope.ts @@ -1,7 +1,8 @@ // This is a rewrite of https://github.com/guerrillacontra/html5-es6-physics-rope import { Vector } from './common/Vector.ts'; -import type { Point, RotatedDOMRect } from './common/types.ts'; +import type { Point } from './common/types.ts'; +import { RotatedDOMRect } from './common/rotated-dom-rect.ts'; import { FolkBaseConnection } from './folk-base-connection.ts'; const lerp = (first: number, second: number, percentage: number) => first + (second - first) * percentage; @@ -123,9 +124,8 @@ export class FolkRope extends FolkBaseConnection { let source: Point; let target: Point; - if ('corners' in sourceRect) { - const [_a, _b, bottomRight, bottomLeft] = sourceRect.corners(); - source = Vector.lerp(bottomRight, bottomLeft, 0.5); + if (sourceRect instanceof RotatedDOMRect) { + source = Vector.lerp(sourceRect.bottomRightCorner, sourceRect.bottomLeftCorner, 0.5); } else { source = { x: sourceRect.x + sourceRect.width / 2, @@ -133,9 +133,8 @@ export class FolkRope extends FolkBaseConnection { }; } - if ('corners' in targetRect) { - const [_a, _b, bottomRight, bottomLeft] = targetRect.corners(); - target = Vector.lerp(bottomRight, bottomLeft, 0.5); + if (targetRect instanceof RotatedDOMRect) { + target = Vector.lerp(targetRect.bottomRightCorner, targetRect.bottomLeftCorner, 0.5); } else { target = { x: targetRect.x + targetRect.width / 2, diff --git a/src/folk-shape.ts b/src/folk-shape.ts index 8e14ac7..24d22b7 100644 --- a/src/folk-shape.ts +++ b/src/folk-shape.ts @@ -1,6 +1,7 @@ import { css, html } from './common/tags'; import { ResizeObserverManager } from './common/resize-observer'; -import type { Point, RotatedDOMRect } from './common/types'; +import { Point } from './common/types'; +import { RotatedDOMRectReadonly } from './common/rotated-dom-rect'; import { Vector } from './common/Vector'; import { getResizeCursorUrl, getRotateCursorUrl } from './common/cursors'; @@ -354,45 +355,10 @@ export class FolkShape extends HTMLElement { this.#update(new Set(['type', 'x', 'y', 'height', 'width', 'rotation'])); } - getClientRect(): RotatedDOMRect { + getClientRect() { const { x, y, width, height, rotation } = this; - return { - x, - y, - width, - height, - left: x, - top: y, - right: x + width, - bottom: y + height, - rotation, - - center(): Point { - return { - x: this.x + this.width / 2, - y: this.y + this.height / 2, - }; - }, - vertices(): Point[] { - // TODO: Implement - return []; - }, - - corners() { - const center = this.center(); - const { x, y, width, height, rotation } = this; - - return [ - Vector.rotateAround({ x, y }, center, rotation), - Vector.rotateAround({ x: x + width, y }, center, rotation), - Vector.rotateAround({ x: x + width, y: y + height }, center, rotation), - Vector.rotateAround({ x, y: y + height }, center, rotation), - ]; - }, - - toJSON: undefined as any, - }; + return new RotatedDOMRectReadonly({ x, y, width, height, rotation }); } // Similar to `Element.getClientBoundingRect()`, but returns an SVG path that precisely outlines the shape. @@ -426,18 +392,17 @@ export class FolkShape extends HTMLElement { if (!anyChange) return; // Get the corner coordinates of the shape for the corresponding handle - const corners = this.getClientRect().corners(); // Returns an array of Points: [NW, NE, SE, SW] + const rect = this.getClientRect(); // Map handle names to corner indices - const handleToCornerIndex: { [key: string]: number } = { - 'resize-nw': 0, // Top-left corner - 'resize-ne': 1, // Top-right corner - 'resize-se': 2, // Bottom-right corner - 'resize-sw': 3, // Bottom-left corner + const handleToCornerIndex: Record = { + 'resize-nw': rect.topLeftCorner, + 'resize-ne': rect.topRightCorner, + 'resize-se': rect.bottomRightCorner, + 'resize-sw': rect.bottomLeftCorner, }; - const cornerIndex = handleToCornerIndex[handle]; - const currentPos = corners[cornerIndex]; + const currentPos = handleToCornerIndex[handle]; // Calculate movement based on arrow keys const isVertical = event.key === 'ArrowUp' || event.key === 'ArrowDown'; @@ -499,7 +464,7 @@ export class FolkShape extends HTMLElement { // Store initial angle on rotation start if (target.getAttribute('part')?.startsWith('rotation')) { - const center = this.getClientRect().center(); + const center = this.getClientRect().center; this.#initialRotation = this.#rotation; this.#startAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center); } @@ -537,7 +502,7 @@ export class FolkShape extends HTMLElement { } if (handle.startsWith('rotation')) { - const center = this.getClientRect().center(); + const center = this.getClientRect().center; const currentAngle = Vector.angleFromOrigin({ x: event.clientX, y: event.clientY }, center); this.rotation = this.#initialRotation + (currentAngle - this.#startAngle); @@ -700,17 +665,18 @@ export class FolkShape extends HTMLElement { // Updated helper method to handle resize operations #handleResize(handle: Handle, mouse: Point, target: HTMLElement, event?: PointerEvent) { + const rect = this.getClientRect(); + // Map each resize handle to its opposite corner index const OPPOSITE_CORNERS = { - 'resize-se': 0, - 'resize-sw': 1, - 'resize-nw': 2, - 'resize-ne': 3, + 'resize-se': rect.topLeftCorner, + 'resize-sw': rect.topRightCorner, + 'resize-nw': rect.bottomRightCorner, + 'resize-ne': rect.bottomLeftCorner, } as const; // Get the opposite corner for the current resize handle - const corners = this.getClientRect().corners(); - const oppositeCorner = corners[OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]]; + const oppositeCorner = OPPOSITE_CORNERS[handle as keyof typeof OPPOSITE_CORNERS]; // Calculate new dimensions based on mouse position and opposite corner const newCenter = Vector.lerp(oppositeCorner, mouse, 0.5);