Files
ComfyUI_frontend/src/measure.ts
filtered 3040924d76 [TS] Use strict mode in CanvasPointer, measure, and polyfills (#617)
- Adds runtime type guard
- Adds ts-ignore that must be removed later
  * [ ] #578
2025-02-26 13:05:40 +00:00

370 lines
10 KiB
TypeScript

import type {
Point,
ReadOnlyPoint,
ReadOnlyRect,
Rect,
} from "./interfaces"
import { LinkDirection } from "./types/globalEnums"
/**
* Calculates the distance between two points (2D vector)
* @param a Point a as `x, y`
* @param b Point b as `x, y`
* @returns Distance between point {@link a} & {@link b}
*/
export function distance(a: ReadOnlyPoint, b: ReadOnlyPoint): number {
return Math.sqrt(
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1]),
)
}
/**
* Calculates the distance2 (squared) between two points (2D vector).
* Much faster when only comparing distances (closest/furthest point).
* @param x1 Origin point X
* @param y1 Origin point Y
* @param x2 Destination point X
* @param y2 Destination point Y
* @returns Distance2 (squared) between point [{@link x1}, {@link y1}] & [{@link x2}, {@link y2}]
*/
export function dist2(x1: number, y1: number, x2: number, y2: number): number {
return ((x2 - x1) * (x2 - x1)) + ((y2 - y1) * (y2 - y1))
}
/**
* Determines whether a point is inside a rectangle.
*
* Otherwise identical to {@link isInsideRectangle}, it also returns `true` if `x` equals `left` or `y` equals `top`.
* @param x Point x
* @param y Point y
* @param left Rect x
* @param top Rect y
* @param width Rect width
* @param height Rect height
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInRectangle(
x: number,
y: number,
left: number,
top: number,
width: number,
height: number,
): boolean {
return x >= left &&
x < left + width &&
y >= top &&
y < top + height
}
/**
* Determines whether a {@link Point} is inside a {@link Rect}.
* @param point The point to check, as `x, y`
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isPointInRect(point: ReadOnlyPoint, rect: ReadOnlyRect): boolean {
return point[0] >= rect[0] &&
point[0] < rect[0] + rect[2] &&
point[1] >= rect[1] &&
point[1] < rect[1] + rect[3]
}
/**
* Determines whether the point represented by {@link x}, {@link y} is inside a {@link Rect}.
* @param x X co-ordinate of the point to check
* @param y Y co-ordinate of the point to check
* @param rect The rectangle, as `x, y, width, height`
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInRect(x: number, y: number, rect: ReadOnlyRect): boolean {
return x >= rect[0] &&
x < rect[0] + rect[2] &&
y >= rect[1] &&
y < rect[1] + rect[3]
}
/**
* Determines whether a point (`x, y`) is inside a rectangle.
*
* This is the original litegraph implementation. It returns `false` if `x` is equal to `left`, or `y` is equal to `top`.
* @deprecated
* Use {@link isInRectangle} to match inclusive of top left.
* This function returns a false negative when an integer point (e.g. pixel) is on the leftmost or uppermost edge of a rectangle.
* @param x Point x
* @param y Point y
* @param left Rect x
* @param top Rect y
* @param width Rect width
* @param height Rect height
* @returns `true` if the point is inside the rect, otherwise `false`
*/
export function isInsideRectangle(
x: number,
y: number,
left: number,
top: number,
width: number,
height: number,
): boolean {
return left < x &&
left + width > x &&
top < y &&
top + height > y
}
/**
* Cheap, low accuracy check to determine if a point is roughly inside a sort-of octagon
* @param x Point x
* @param y Point y
* @param radius Radius to use as rough guide for octagon
* @returns `true` if the point is roughly inside the octagon centred on 0,0 with specified radius
*/
export function isSortaInsideOctagon(x: number, y: number, radius: number): boolean {
const sum = Math.min(radius, Math.abs(x)) + Math.min(radius, Math.abs(y))
return sum < radius * 0.75
}
/**
* Determines if two rectangles have any overlap
* @param a Rectangle A as `x, y, width, height`
* @param b Rectangle B as `x, y, width, height`
* @returns `true` if rectangles overlap, otherwise `false`
*/
export function overlapBounding(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
const bBottom = b[1] + b[3]
return a[0] > bRight ||
a[1] > bBottom ||
aRight < b[0] ||
aBottom < b[1]
? false
: true
}
/**
* Determines if rectangle {@link a} contains the centre point of rectangle {@link b}.
* @param a Container rectangle A as `x, y, width, height`
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} contains most of {@link b}, otherwise `false`
*/
export function containsCentre(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const centreX = b[0] + (b[2] * 0.5)
const centreY = b[1] + (b[3] * 0.5)
return isInRect(centreX, centreY, a)
}
/**
* Determines if rectangle {@link a} wholly contains rectangle {@link b}.
* @param a Container rectangle A as `x, y, width, height`
* @param b Sub-rectangle B as `x, y, width, height`
* @returns `true` if {@link a} wholly contains {@link b}, otherwise `false`
*/
export function containsRect(a: ReadOnlyRect, b: ReadOnlyRect): boolean {
const aRight = a[0] + a[2]
const aBottom = a[1] + a[3]
const bRight = b[0] + b[2]
const bBottom = b[1] + b[3]
const identical = a[0] === b[0] &&
a[1] === b[1] &&
aRight === bRight &&
aBottom === bBottom
return !identical &&
a[0] <= b[0] &&
a[1] <= b[1] &&
aRight >= bRight &&
aBottom >= bBottom
}
/**
* Adds an offset in the specified direction to {@link out}
* @param amount Amount of offset to add
* @param direction Direction to add the offset to
* @param out The {@link Point} to add the offset to
*/
export function addDirectionalOffset(
amount: number,
direction: LinkDirection,
out: Point,
): void {
switch (direction) {
case LinkDirection.LEFT:
out[0] -= amount
return
case LinkDirection.RIGHT:
out[0] += amount
return
case LinkDirection.UP:
out[1] -= amount
return
case LinkDirection.DOWN:
out[1] += amount
return
// LinkDirection.CENTER: Nothing to do.
}
}
/**
* Rotates an offset in 90° increments.
*
* Swaps/flips axis values of a 2D vector offset - effectively rotating
* {@link offset} by 90°
* @param offset The zero-based offset to rotate
* @param from Direction to rotate from
* @param to Direction to rotate to
*/
export function rotateLink(
offset: Point,
from: LinkDirection,
to: LinkDirection,
): void {
let x: number
let y: number
// Normalise to left
switch (from) {
case to:
case LinkDirection.CENTER:
case LinkDirection.NONE:
default:
// Nothing to do
return
case LinkDirection.LEFT:
x = offset[0]
y = offset[1]
break
case LinkDirection.RIGHT:
x = -offset[0]
y = -offset[1]
break
case LinkDirection.UP:
x = -offset[1]
y = offset[0]
break
case LinkDirection.DOWN:
x = offset[1]
y = -offset[0]
break
}
// Apply new direction
switch (to) {
case LinkDirection.CENTER:
case LinkDirection.NONE:
// Nothing to do
return
case LinkDirection.LEFT:
offset[0] = x
offset[1] = y
break
case LinkDirection.RIGHT:
offset[0] = -x
offset[1] = -y
break
case LinkDirection.UP:
offset[0] = y
offset[1] = -x
break
case LinkDirection.DOWN:
offset[0] = -y
offset[1] = x
break
}
}
/**
* Check if a point is to to the left or right of a line.
* Project a line from lineStart -> lineEnd. Determine if point is to the left
* or right of that projection.
* {@link https://www.geeksforgeeks.org/orientation-3-ordered-points/}
* @param lineStart The start point of the line
* @param lineEnd The end point of the line
* @param x X co-ordinate of the point to check
* @param y Y co-ordinate of the point to check
* @returns 0 if all three points are in a straight line, a negative value if
* point is to the left of the projected line, or positive if the point is to
* the right
*/
export function getOrientation(
lineStart: ReadOnlyPoint,
lineEnd: ReadOnlyPoint,
x: number,
y: number,
): number {
return ((lineEnd[1] - lineStart[1]) * (x - lineEnd[0])) -
((lineEnd[0] - lineStart[0]) * (y - lineEnd[1]))
}
/**
* @param out The array to store the point in
* @param a Start point
* @param b End point
* @param controlA Start curve control point
* @param controlB End curve control point
* @param t Time: factor of distance to travel along the curve (e.g 0.25 is 25% along the curve)
*/
export function findPointOnCurve(
out: Point,
a: ReadOnlyPoint,
b: ReadOnlyPoint,
controlA: ReadOnlyPoint,
controlB: ReadOnlyPoint,
t: number = 0.5,
): void {
const iT = 1 - t
const c1 = iT * iT * iT
const c2 = 3 * (iT * iT) * t
const c3 = 3 * iT * (t * t)
const c4 = t * t * t
out[0] = (c1 * a[0]) + (c2 * controlA[0]) + (c3 * controlB[0]) + (c4 * b[0])
out[1] = (c1 * a[1]) + (c2 * controlA[1]) + (c3 * controlB[1]) + (c4 * b[1])
}
export function createBounds(
objects: Iterable<{ boundingRect: ReadOnlyRect }>,
padding: number = 10,
): ReadOnlyRect | null {
const bounds = new Float32Array([Infinity, Infinity, -Infinity, -Infinity])
for (const obj of objects) {
const rect = obj.boundingRect
bounds[0] = Math.min(bounds[0], rect[0])
bounds[1] = Math.min(bounds[1], rect[1])
bounds[2] = Math.max(bounds[2], rect[0] + rect[2])
bounds[3] = Math.max(bounds[3], rect[1] + rect[3])
}
if (!bounds.every(x => isFinite(x))) return null
return [
bounds[0] - padding,
bounds[1] - padding,
bounds[2] - bounds[0] + (2 * padding),
bounds[3] - bounds[1] + (2 * padding),
]
}
/**
* Snaps the provided {@link Point} or {@link Rect} ({@link pos}) to a grid of size {@link snapTo}.
* @param pos The point that will be snapped
* @param snapTo The value to round up/down by (multiples thereof)
* @returns `true` if snapTo is truthy, otherwise `false`
* @remarks `NaN` propagates through this function and does not affect return value.
*/
export function snapPoint(pos: Point | Rect, snapTo: number): boolean {
if (!snapTo) return false
pos[0] = snapTo * Math.round(pos[0] / snapTo)
pos[1] = snapTo * Math.round(pos[1] / snapTo)
return true
}