/**
|
* @typedef {{x: number, y: number, width: number, height: number}} BBox
|
*
|
* @typedef {number[]} Points Represents (x,y) flat array, meaning
|
* each two numbers represent x and y accordingly. Array always starts with x
|
*
|
* @typedef {{
|
* x1: number,
|
* x2: number,
|
* x3: number,
|
* x4: number,
|
* y1: number,
|
* y2: number,
|
* y3: number,
|
* y4: number}} RectCoordinates Represents 4 corners coordinates of rectangle
|
*/
|
import { clamp } from "../../utils/utilities";
|
|
export class Geometry {
|
/**
|
* Returns RAD angle to normalized degrees meaning that it will always fit 0-360 range
|
* @param {number} angle Angle in RAD
|
*/
|
static normalizeAngle(angle) {
|
return ((angle + 360) % 360) * (Math.PI / 180);
|
}
|
|
/**
|
* Calculate BBox for any number of coordinates
|
* @param {Points} points Input points
|
* @returns {Points} Array of two (x,y) coordinates representing a BBox
|
*/
|
static getPointsBBox(points) {
|
const minmax = [null, null, null, null];
|
|
points.forEach((num, i) => {
|
const pos = Math.round(i / 2) * 2 - i;
|
|
if (pos === 0) {
|
// Calculate min and max X
|
if (minmax[0] === null || minmax[0] >= num) minmax[0] = num;
|
if (minmax[2] === null || minmax[2] <= num) minmax[2] = num;
|
} else if (pos === 1) {
|
// Calculate min and max Y
|
if (minmax[1] === null || minmax[1] >= num) minmax[1] = num;
|
if (minmax[3] === null || minmax[3] <= num) minmax[3] = num;
|
}
|
});
|
|
return minmax;
|
}
|
|
/**
|
* Calculate distance between wo points
|
* @param {Points} point1
|
* @param {Points} point2
|
*/
|
static distance(point1, point2) {
|
const [x1, y1] = point1;
|
const [x2, y2] = point2;
|
|
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
}
|
|
/**
|
* Convert standard bbox to a set of coordinates
|
* @param {BBox} bbox
|
* @returns {RectCoordinates}
|
*/
|
static toRectCoordinates(bbox) {
|
const { x: x1, y: y1, width, height } = bbox;
|
const [x2, y2] = [x1 + width, y1];
|
const [x3, y3] = [x1 + width, y1 + height];
|
const [x4, y4] = [x1, y1 + height];
|
|
return { x1, x2, x3, x4, y1, y2, y3, y4 };
|
}
|
|
/**
|
* Convert RectCoordinates to BBox
|
* @param {RectCoordinates} rect
|
* @returns {BBox}
|
*/
|
static convertToRectBBox(rect) {
|
return {
|
x: rect.x1,
|
y: rect.y1,
|
width: rect.x2 - rect.x1,
|
height: rect.y3 - rect.y1,
|
};
|
}
|
|
/**
|
* Find two closes BBoxes of two lists
|
* @param {BBox[]} rectsList1
|
* @param {BBox[]} rectsList2
|
*/
|
static closestRects(rectsList1, rectsList2) {
|
const result = rectsList1
|
.reduce((res, rect1) => {
|
const bbox1 = Geometry.toRectCoordinates(rect1);
|
|
rectsList2.forEach((rect2) => {
|
const bbox2 = Geometry.toRectCoordinates(rect2);
|
|
const avgDistance =
|
[
|
Geometry.distance([bbox1.x1, bbox1.y1], [bbox2.x1, bbox1.y1]),
|
Geometry.distance([bbox1.x2, bbox1.y2], [bbox2.x2, bbox1.y2]),
|
Geometry.distance([bbox1.x3, bbox1.y3], [bbox2.x3, bbox1.y3]),
|
Geometry.distance([bbox1.x4, bbox1.y4], [bbox2.x4, bbox2.y4]),
|
].reduce((d1, d2) => d1 + d2) / 4;
|
|
res.push({
|
distance: avgDistance,
|
bbox: [Geometry.convertToRectBBox(bbox1), Geometry.convertToRectBBox(bbox2)],
|
});
|
});
|
|
return res;
|
}, [])
|
.sort((a, b) => a.distance - b.distance);
|
|
return result[0].bbox;
|
}
|
|
/**
|
* Scale given BBox by a scale factor
|
* @param {BBox} bbox Original BBox
|
* @param {number} scale Scale factor
|
* @returns {BBox} Scaled BBox
|
*/
|
static scaleBBox(bbox, scale = 1) {
|
return {
|
...bbox,
|
x: bbox.x * scale,
|
y: bbox.y * scale,
|
width: bbox.width * scale,
|
height: bbox.height * scale,
|
};
|
}
|
|
static modifyBBoxCoords(bbox, modifier = (x) => x) {
|
const p1 = modifier([bbox.x, bbox.y]);
|
const p2 = modifier([bbox.width + bbox.x, bbox.height + bbox.y]);
|
|
return {
|
...bbox,
|
x: Math.min(p1[0], p2[0]),
|
y: Math.min(p1[1], p2[1]),
|
width: Math.abs(p2[0] - p1[0]),
|
height: Math.abs(p2[1] - p1[1]),
|
};
|
}
|
|
/**
|
* Add padding to BBox
|
* @param {BBox} bbox BBox to pad
|
* @param {number} padding Padding size
|
*/
|
static padding(bbox, padding = 0) {
|
const paddingX = bbox.width < 1 ? 0 : padding;
|
const paddingY = bbox.height < 1 ? 0 : padding;
|
|
return {
|
...bbox,
|
x: bbox.x - paddingX,
|
y: bbox.y - paddingY,
|
width: bbox.width + paddingX * 2,
|
height: bbox.height + paddingY * 2,
|
};
|
}
|
|
/**
|
* Calculate ellipse BBox
|
* @param {number} x Center X
|
* @param {number} y Center Y
|
* @param {number} rx Radius X
|
* @param {number} ry Radius Y
|
* @param {number} angle Angle in RAD
|
* @returns {BBox[]} Dimensions of bounding box
|
*/
|
static getEllipseBBox(x, y, rx, ry, angle) {
|
const angleRad = Geometry.normalizeAngle(angle);
|
const major = Math.max(rx, ry) * 2;
|
const minor = Math.min(rx, ry) * 2;
|
|
const getXLimits = () => {
|
const t = Math.atan(((-minor / 2) * Math.tan(angleRad)) / (major / 2));
|
|
return [t, t + Math.PI]
|
.map((t) => {
|
return x + (major / 2) * Math.cos(t) * Math.cos(angleRad) - (minor / 2) * Math.sin(t) * Math.sin(angleRad);
|
})
|
.sort((a, b) => b - a);
|
};
|
|
const getYLimits = () => {
|
const t = Math.atan(((minor / 2) * 1.0) / Math.tan(angleRad) / (major / 2));
|
|
return [t, t + Math.PI]
|
.map((t) => {
|
return y + (minor / 2) * Math.sin(t) * Math.cos(angleRad) + (major / 2) * Math.cos(t) * Math.sin(angleRad);
|
})
|
.sort((a, b) => b - a);
|
};
|
|
const [x1, x2] = getXLimits();
|
const [y1, y2] = getYLimits();
|
const width = x1 - x2;
|
const height = y1 - y2;
|
|
return { x: x2, y: y2, width, height };
|
}
|
|
/**
|
* Calculate rotated rect BBox
|
* @param {number} x Top left X
|
* @param {number} y Top left Y
|
* @param {number} width Width
|
* @param {number} height Height
|
* @param {number} angle Angle in RAD
|
* @returns {BBox[]} Dimensions of bounding box
|
*/
|
static getRectBBox(x, y, width, height, angle) {
|
const angleRad = Geometry.normalizeAngle(angle);
|
|
const rotate = (x1, y1) => [
|
(x1 - x) * Math.cos(angleRad) - (y1 - y) * Math.sin(angleRad) + x,
|
(x1 - x) * Math.sin(angleRad) + (y1 - y) * Math.cos(angleRad) + y,
|
];
|
|
const [rx1, ry1, rx2, ry2] = Geometry.getPointsBBox([
|
x,
|
y,
|
...rotate(x + width, y),
|
...rotate(x + width, y + height),
|
...rotate(x, y + height),
|
]);
|
|
return { x: rx1, y: ry1, width: rx2 - rx1, height: ry2 - ry1 };
|
}
|
|
/**
|
* Calculate BBox of polygon shape
|
* @param {Points} points
|
* @return {BBox[]}
|
*/
|
static getPolygonBBox(points) {
|
const coords = points.reduce((res, point) => [...res, point.x, point.y], []);
|
const [x1, y1, x2, y2] = Geometry.getPointsBBox(coords);
|
|
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
}
|
|
/**
|
* Calculate BBox of Brush region (a set of points)
|
* @param {Points} points
|
* @return {BBox[]}
|
*/
|
static getBrushBBox(points) {
|
const [x1, y1, x2, y2] = Geometry.getPointsBBox(points);
|
|
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
}
|
|
/**
|
* Calculate BBox of Brush region from image data
|
* @param {Uint8ClampedArray} imageData Array containing the data in the RGBA order
|
* @param {Number} width
|
* @param {Number} height
|
* @return {BBox}
|
*/
|
static getImageDataBBox(imageData, w, h) {
|
if (imageData.length !== w * h * 4) return null;
|
const min = { x: w, y: h };
|
const max = { x: 0, y: 0 };
|
|
for (let y = 0; y < h; y++) {
|
for (let x = 0; x < w; x++) {
|
const alphaIndex = 4 * (y * w + x) + 3;
|
|
if (imageData[alphaIndex]) {
|
if (min.x > x) min.x = x;
|
if (min.y > y) min.y = y;
|
if (max.x < x) max.x = x;
|
if (max.y < y) max.y = y;
|
}
|
}
|
}
|
return min.x <= max.x && min.y <= max.y
|
? { x: min.x, y: min.y, width: max.x - min.x, height: max.y - min.y }
|
: null;
|
}
|
/**
|
* Combine two or more BBoxes into one
|
* @param {...BBox} bboxes Bboxes to merge
|
* @return {BBox}
|
*/
|
static combineBBoxes(...bboxes) {
|
const [x1, y1, x2, y2] = Geometry.getPointsBBox(
|
bboxes.reduce((points, bbox) => {
|
if (bbox && bbox.x && bbox.y) {
|
points.push(bbox.x);
|
points.push(bbox.y);
|
points.push(bbox.x + bbox.width);
|
points.push(bbox.y + bbox.height);
|
}
|
return points;
|
}, []),
|
);
|
|
return { x: x1, y: y1, width: x2 - x1, height: y2 - y1 };
|
}
|
|
static clampBBox(bbox, min, max) {
|
const p1 = [clamp(bbox.x, min.x, max.x), clamp(bbox.y, min.y, max.y)];
|
const p2 = [clamp(bbox.width + bbox.x, min.x, max.x), clamp(bbox.height + bbox.y, min.y, max.y)];
|
|
return {
|
x: p1[0],
|
y: p1[1],
|
width: p2[0] - p1[0],
|
height: p2[1] - p1[1],
|
};
|
}
|
|
/**
|
* Get BBox of any DOM node
|
* @param {HTMLOrSVGElement} domNode
|
* @param {boolean} single Should return all possible BBoxes or not
|
* @return {BBox[]}
|
*/
|
static getDOMBBox(domNode, single = false) {
|
if (!domNode) return null;
|
|
const bboxes = domNode.getClientRects();
|
|
if (bboxes.length === 0) return null;
|
|
const convertDOMRect = (domRect) => ({
|
x: domRect.x,
|
y: domRect.y,
|
width: domRect.width,
|
height: domRect.height,
|
});
|
|
return single ? convertDOMRect(bboxes[0]) : Array.from(domNode.getClientRects()).map(convertDOMRect);
|
}
|
}
|