@foblex/2d
Version:
An Angular library for 2D geometric computations, providing classes and utilities for manipulating points, lines, vectors, rectangles, arcs, and transformations.
650 lines (630 loc) • 25.4 kB
JavaScript
class Arc {
constructor(center, radiusX, radiusY, startAngle, endAngle) {
this.center = center;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.startAngle = startAngle;
this.endAngle = endAngle;
}
}
class PointExtensions {
static castToPoint(value) {
return value || PointExtensions.initialize();
}
static initialize(x = 0, y = 0) {
return { x: x, y: y };
}
static copy(point) {
return PointExtensions.initialize(point.x, point.y);
}
static isEqual(point1, point2) {
return point1.x === point2.x && point1.y === point2.y;
}
static sum(point1, point2) {
return { x: (point1.x + point2.x), y: (point1.y + point2.y) };
}
static sub(point1, point2) {
return { x: (point1.x - point2.x), y: (point1.y - point2.y) };
}
static div(point, value) {
return { x: (point.x / value), y: (point.y / value) };
}
static mult(point, value) {
return { x: (point.x * value), y: (point.y * value) };
}
static interpolatePoints(point1, point2, t) {
const oneMinusT = 1.0 - t;
return PointExtensions.initialize(point1.x * oneMinusT + point2.x * t, point1.y * oneMinusT + point2.y * t);
}
static roundTo(point, size) {
const xCount = Math.trunc(point.x / size);
const yCount = Math.trunc(point.y / size);
return { x: xCount * size, y: yCount * size };
}
static hypotenuse(point1, point2) {
const a = (point2.x - point1.x);
const b = (point2.y - point1.y);
return Math.abs(Math.sqrt(a * a + b * b));
}
static distance(point1, point2) {
const dx = point1.x - point2.x;
const dy = point1.y - point2.y;
return Math.sqrt(dx * dx + dy * dy);
}
static getMinimum(point1, point2) {
return PointExtensions.initialize(Math.min(point1.x, point2.x), Math.min(point1.y, point2.y));
}
static getMaximum(point1, point2) {
return PointExtensions.initialize(Math.max(point1.x, point2.x), Math.max(point1.y, point2.y));
}
static matrixTransform(point, element) {
let result = PointExtensions.initialize(point.x, point.y);
let matrix = element.getScreenCTM();
if (matrix) {
const svgPoint = element.createSVGPoint();
svgPoint.x = point.x;
svgPoint.y = point.y;
result = svgPoint.matrixTransform(matrix.inverse());
}
return result;
}
static elementTransform(point, element) {
let result = PointExtensions.initialize(point.x, point.y);
let matrix = element.getBoundingClientRect();
result = PointExtensions.sub(result, PointExtensions.initialize(matrix.left, matrix.top));
return result;
}
}
class Point {
constructor(x = 0, y = 0) {
this.x = x;
this.y = y;
}
static fromPoint(point) {
return new Point(point.x, point.y);
}
add(point) {
const result = PointExtensions.sum(this, point);
return Point.fromPoint(result);
}
sub(point) {
const result = PointExtensions.sub(this, point);
return Point.fromPoint(result);
}
subNumber(value) {
const result = PointExtensions.sub(this, new Point(value, value));
return Point.fromPoint(result);
}
div(value) {
const result = PointExtensions.div(this, value);
return Point.fromPoint(result);
}
mult(value) {
const result = PointExtensions.mult(this, value);
return Point.fromPoint(result);
}
matrixTransform(element) {
const result = PointExtensions.matrixTransform(this, element);
return Point.fromPoint(result);
}
elementTransform(element) {
const result = PointExtensions.elementTransform(this, element);
return Point.fromPoint(result);
}
}
class LineExtensions {
static initialize(point1 = PointExtensions.initialize(), point2 = PointExtensions.initialize()) {
return { point1, point2 };
}
static copy(line) {
return { point1: line.point1, point2: line.point2 };
}
static hypotenuse(line) {
return Math.sqrt(Math.pow((line.point1.x - line.point2.x), 2) +
Math.pow((line.point1.y - line.point2.y), 2));
}
}
class Line {
constructor(point1, point2) {
this.point1 = point1;
this.point2 = point2;
}
}
class RectExtensions {
static initialize(x = 0, y = 0, width = 0, height = 0) {
if (width < 0) {
x = x + width;
width = -width;
}
if (height < 0) {
y = y + height;
height = -height;
}
const gravityCenter = PointExtensions.initialize(x + (width / 2), y + (height / 2));
return { x: x, y: y, width: width, height: height, gravityCenter: gravityCenter };
}
static copy(rect) {
return RectExtensions.initialize(rect.x, rect.y, rect.width, rect.height);
}
static fromElement(element) {
const { x, y, width, height } = element.getBoundingClientRect();
return RectExtensions.initialize(x, y, width, height);
}
static isIncludePoint(rect, point) {
return point.x >= RectExtensions.left(rect) && point.x <= RectExtensions.right(rect) && point.y >= RectExtensions.top(rect) && point.y <= RectExtensions.bottom(rect);
}
static intersectionWithRect(rect1, rect2) {
return !(rect1.x + rect1.width < rect2.x ||
rect2.x + rect2.width < rect1.x ||
rect1.y + rect1.height < rect2.y ||
rect2.y + rect2.height < rect1.y);
}
static left(rect) {
return rect.x;
}
static top(rect) {
return rect.y;
}
static right(rect) {
return rect.x + rect.width;
}
static bottom(rect) {
return rect.y + rect.height;
}
static addPoint(rect, point) {
const rectCopy = RectExtensions.copy(rect);
rectCopy.x += point.x;
rectCopy.y += point.y;
return this.initialize(rectCopy.x, rectCopy.y, rectCopy.width, rectCopy.height);
}
static mult(rect, value) {
const rectCopy = RectExtensions.copy(rect);
rectCopy.x *= value;
rectCopy.y *= value;
rectCopy.width *= value;
rectCopy.height *= value;
return this.initialize(rectCopy.x, rectCopy.y, rectCopy.width, rectCopy.height);
}
static div(rect, value) {
const rectCopy = RectExtensions.copy(rect);
rectCopy.x /= value;
rectCopy.y /= value;
rectCopy.width /= value;
rectCopy.height /= value;
return this.initialize(rectCopy.x, rectCopy.y, rectCopy.width, rectCopy.height);
}
static addPointToSize(rect, point) {
const rectCopy = RectExtensions.copy(rect);
rectCopy.width += point.x;
rectCopy.height += point.y;
return this.initialize(rectCopy.x, rectCopy.y, rectCopy.width, rectCopy.height);
}
static union(rects) {
if (!rects || rects.length === 0) {
return null;
}
return rects.reduce((result, rect) => {
const minX = Math.min(result.x, rect.x);
const minY = Math.min(result.y, rect.y);
const maxX = Math.max(result.x + result.width, rect.x + rect.width);
const maxY = Math.max(result.y + result.height, rect.y + rect.height);
return RectExtensions.initialize(minX, minY, maxX - minX, maxY - minY);
}, rects[0]);
}
static elementTransform(rect, element) {
const matrix = element.getBoundingClientRect();
const position = PointExtensions.sub(rect, PointExtensions.initialize(matrix.left, matrix.top));
return RectExtensions.initialize(position.x, position.y, rect.width, rect.height);
}
static updateIsNotFinite(rect) {
if (!Number.isFinite(rect.width) || !Number.isFinite(rect.height) || !Number.isFinite(rect.x) || !Number.isFinite(rect.y)) {
return RectExtensions.initialize(0, 0, 0, 0);
}
return rect;
}
}
function adjustRectToMinSize(rect, minSize) {
const width = Math.max(rect.width, minSize);
const height = Math.max(rect.height, minSize);
const offsetX = (width - rect.width) / 2;
const offsetY = (height - rect.height) / 2;
return RectExtensions.initialize(rect.x - offsetX, rect.y - offsetY, width, height);
}
function findClosestAlignment(elements, target, alignThreshold = 10) {
let nearestX;
let minDistanceX;
let nearestY;
let minDistanceY;
for (const element of elements) {
const targetCenterX = target.gravityCenter.x;
const targetCenterY = target.gravityCenter.y;
const elementRight = element.x + element.width;
const elementBottom = element.y + element.height;
const elementDistances = {
x: [
{ value: element.x, distance: target.x - element.x },
{ value: elementRight, distance: target.x - elementRight },
{ value: element.gravityCenter.x, distance: targetCenterX - element.gravityCenter.x },
{ value: element.x, distance: (target.x + target.width) - element.x },
{ value: elementRight, distance: (target.x + target.width) - elementRight }, // Right to right
],
y: [
{ value: element.y, distance: target.y - element.y },
{ value: elementBottom, distance: target.y - elementBottom },
{ value: element.gravityCenter.y, distance: targetCenterY - element.gravityCenter.y },
{ value: element.y, distance: (target.y + target.height) - element.y },
{ value: elementBottom, distance: (target.y + target.height) - elementBottom }, // Bottom to bottom
],
};
for (const { value, distance } of elementDistances.x) {
if (Math.abs(distance) <= alignThreshold) {
if (minDistanceX === undefined || Math.abs(distance) < Math.abs(minDistanceX)) {
minDistanceX = distance;
nearestX = value;
}
}
}
for (const { value, distance } of elementDistances.y) {
if (Math.abs(distance) <= alignThreshold) {
if (minDistanceY === undefined || Math.abs(distance) < Math.abs(minDistanceY)) {
minDistanceY = distance;
nearestY = value;
}
}
}
}
return {
xResult: { value: nearestX, distance: minDistanceX },
yResult: { value: nearestY, distance: minDistanceY },
};
}
function setRectToElement(rect, element) {
rect = RectExtensions.updateIsNotFinite(rect);
element.setAttribute('x', rect.x.toString());
element.setAttribute('y', rect.y.toString());
element.setAttribute('width', rect.width.toString());
element.setAttribute('height', rect.height.toString());
}
function setRectToViewBox(rect, element) {
rect = RectExtensions.updateIsNotFinite(rect);
element.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.width} ${rect.height}`);
}
class RoundedRect {
constructor(x = 0, y = 0, width = 0, height = 0, radius1 = 0, radius2 = 0, radius3 = 0, radius4 = 0) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.radius1 = radius1;
this.radius2 = radius2;
this.radius3 = radius3;
this.radius4 = radius4;
this.gravityCenter = PointExtensions.initialize();
this.gravityCenter = this.calculateGravityCenter(this);
}
calculateGravityCenter(rect) {
return new Point(rect.x + rect.width / 2, rect.y + rect.height / 2);
}
static fromRect(rect) {
return new RoundedRect(rect.x, rect.y, rect.width, rect.height);
}
static fromRoundedRect(rect) {
return new RoundedRect(rect.x, rect.y, rect.width, rect.height, rect.radius1, rect.radius2, rect.radius3, rect.radius4);
}
static fromCenter(rect, width, height) {
return new RoundedRect(rect.gravityCenter.x - width / 2, rect.gravityCenter.y - height / 2, width, height, rect.radius1, rect.radius2, rect.radius3, rect.radius4);
}
static fromPoint(point) {
return new RoundedRect(point.x, point.y);
}
addPoint(point) {
const copy = RoundedRect.fromRoundedRect(this);
copy.x += point.x;
copy.y += point.y;
copy.gravityCenter = this.calculateGravityCenter(copy);
return copy;
}
}
class SizeExtensions {
static initialize(width = 0, height = 0) {
return { width, height };
}
static isEqual(size1, size2) {
return size1.width === size2.width && size1.height === size2.height;
}
static offsetFromElement(element) {
if (element instanceof SVGGraphicsElement) {
const bBox = element.getBBox();
return SizeExtensions.initialize(bBox.width, bBox.height);
}
else if (element instanceof HTMLElement) {
return SizeExtensions.initialize(element.offsetWidth, element.offsetHeight);
}
return undefined;
}
}
function defaultTransformModel() {
return {
position: PointExtensions.initialize(),
scaledPosition: PointExtensions.initialize(),
scale: 1,
rotate: 0
};
}
function parseTransformModel(value) {
let result;
if (value) {
value = value.replace('matrix(', '');
value = value.replace(')', '');
const values = value.split(' ');
result = {
position: {
x: Number(values[4]),
y: Number(values[5])
},
scaledPosition: PointExtensions.initialize(),
scale: Number(values[0]),
rotate: 0
};
}
return result;
}
class TransformModelExtensions {
static toString(transform) {
const position = PointExtensions.sum(transform.position, transform.scaledPosition);
return `matrix(${transform.scale}, 0, 0, ${transform.scale}, ${position.x}, ${position.y})`;
}
static fromString(value) {
return parseTransformModel(value);
}
static default() {
return defaultTransformModel();
}
}
class VectorExtensions {
static initialize(x = 0, y = 0) {
return PointExtensions.initialize(x, y);
}
static fromPoints(p1, p2) {
return VectorExtensions.initialize(p2.x - p1.x, p2.y - p1.y);
}
static vectorLength(v) {
return Math.sqrt(VectorExtensions.magnitudeSquared(v));
}
static magnitudeSquared(v) {
return v.x * v.x + v.y * v.y;
}
static dotProduct(v1, v2) {
return v1.x * v2.x + v1.y * v2.y;
}
static crossProduct(v1, v2) {
return v1.x * v2.y - v1.y * v2.x;
}
static subtract(v1, v2) {
return VectorExtensions.initialize(v1.x - v2.x, v1.y - v2.y);
}
static add(v1, v2) {
return VectorExtensions.initialize(v1.x + v2.x, v1.y + v2.y);
}
static scale(v, value) {
return VectorExtensions.initialize(v.x * value, v.y * value);
}
static angle(v1, v2) {
const radians = Math.acos(Math.max(-1, Math.min(VectorExtensions.dotProduct(v1, v2) / (VectorExtensions.vectorLength(v1) * VectorExtensions.vectorLength(v2)), 1)));
return (VectorExtensions.crossProduct(v1, v2) < 0.0) ? -radians : radians;
}
}
class ShapeParser {
// public static getSegments(rect: IRoundedRect): (Arc | Line)[] {
// return this.parseRect(rect);
// }
/**
* Parses the rounded rectangle into its constituent segments (arcs and lines).
* @param rect - The rounded rectangle to parse.
* @returns An array of arcs and lines representing the rectangle.
*/
static parseRoundedRect(rect) {
const degree90 = Math.PI * 0.5;
const x0 = rect.x;
const y0 = rect.y;
const x1 = rect.x + rect.width;
const y1 = rect.y + rect.height;
const topLeftX = rect.x + rect.radius1;
const topLeftY = rect.y + rect.radius1;
const topRightX = rect.x + rect.width - rect.radius2;
const topRightY = rect.y + rect.radius2;
const bottomRightX = rect.x + rect.width - rect.radius3;
const bottomRightY = rect.y + rect.height - rect.radius3;
const bottomLeftX = rect.x + rect.radius4;
const bottomLeftY = rect.y + rect.height - rect.radius4;
return [
new Arc({ x: topLeftX, y: topLeftY }, rect.radius1, rect.radius1, 2 * degree90, 3 * degree90),
new Line({ x: topLeftX, y: y0 }, { x: topRightX, y: y0 }),
new Arc({ x: topRightX, y: topRightY }, rect.radius2, rect.radius2, 3 * degree90, 4 * degree90),
new Line({ x: x1, y: topRightY }, { x: x1, y: bottomRightY }),
new Arc({ x: bottomRightX, y: bottomRightY }, rect.radius3, rect.radius3, 0, degree90),
new Line({ x: bottomRightX, y: y1 }, { x: bottomLeftX, y: y1 }),
new Arc({ x: bottomLeftX, y: bottomLeftY }, rect.radius4, rect.radius4, degree90, 2 * degree90),
new Line({ x: x0, y: bottomLeftY }, { x: x0, y: topLeftY }),
];
}
}
/**
* The GetIntersections class is designed to find intersection points between
* line segments and various geometric shapes. Currently, it supports rectangles,
* circles, and ellipses. In the future, support for additional shapes will be added.
*/
class GetIntersections {
/**
* Finds the guaranteed intersection points between a line segment and a rounded rectangle.
* @param from - Starting point of the line segment.
* @param to - Ending point of the line segment.
* @param rect - The rect to check for intersections.
* @returns An array of intersection points.
*/
static getRoundedRectIntersections(from, to, rect) {
const segments = ShapeParser.parseRoundedRect(rect);
for (const segment of segments) {
if (segment instanceof Arc) {
const intersections = this.intersectArcWithLine(segment, from, to);
if (intersections.length > 0) {
return intersections;
}
}
else if (segment instanceof Line) {
const intersection = this.intersectLineSegments(from, to, segment.point1, segment.point2);
if (intersection) {
return [intersection];
}
}
}
return [];
}
/**
* Finds the intersection points between a line segment and an SVG path.
* @param path - The SVG path to check for intersections.
* @param rect - The rect to check for intersections.
* @returns An array of intersection points.
*/
static getRoundedRectIntersectionsWithSVGPath(path, rect) {
const pathLength = path.getTotalLength();
const points = [];
for (let i = 0; i <= pathLength; i += 1) {
const point = path.getPointAtLength(i);
points.push({ x: point.x, y: point.y });
}
for (let i = 1; i < points.length; i++) {
const intersections = this.getRoundedRectIntersections(points[i - 1], points[i], rect);
if (intersections.length > 0) {
return intersections;
}
}
return [];
}
/**
* Finds the intersection points between an arc and a line segment.
* @param arc - The arc to check for intersections.
* @param from - Starting point of the line segment.
* @param to - Ending point of the line segment.
* @returns An array of intersection points.
*/
static intersectArcWithLine(arc, from, to) {
return this.filterPointsWithinArc(this.findEllipseLineIntersections(arc.center, arc.radiusX, arc.radiusY, from, to), arc);
}
/**
* Finds the intersection point between two line segments.
* @param p1 - Starting point of the first line segment.
* @param p2 - Ending point of the first line segment.
* @param p3 - Starting point of the second line segment.
* @param p4 - Ending point of the second line segment.
* @returns The intersection point or null if there is no intersection.
*/
static intersectLineSegments(p1, p2, p3, p4) {
const s1_x = p2.x - p1.x;
const s1_y = p2.y - p1.y;
const s2_x = p4.x - p3.x;
const s2_y = p4.y - p3.y;
const s = (-s1_y * (p1.x - p3.x) + s1_x * (p1.y - p3.y)) / (-s2_x * s1_y + s1_x * s2_y);
const t = (s2_x * (p1.y - p3.y) - s2_y * (p1.x - p3.x)) / (-s2_x * s1_y + s1_x * s2_y);
if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
return { x: p1.x + (t * s1_x), y: p1.y + (t * s1_y) };
}
return null;
}
/**
* Filters intersection points to retain only those within the given arc.
* @param points - The points to filter.
* @param arc - The arc to check against.
* @returns An array of points within the arc.
*/
static filterPointsWithinArc(points, arc) {
let { center, startAngle, endAngle } = arc;
if (points.length === 0) {
return points;
}
if (endAngle < startAngle) {
[startAngle, endAngle] = [endAngle, startAngle];
}
if (startAngle < 0 || endAngle < 0) {
startAngle += 2.0 * Math.PI;
endAngle += 2.0 * Math.PI;
}
const filteredPoints = [];
for (const point of points) {
let angle = this.normalizeAngle(VectorExtensions.angle(VectorExtensions.initialize(1, 0), VectorExtensions.initialize(point.x - center.x, point.y - center.y)));
if (angle < startAngle) {
angle += 2.0 * Math.PI;
}
if (startAngle <= angle && angle <= endAngle) {
filteredPoints.push(point);
}
}
return filteredPoints;
}
/**
* Normalizes an angle to be within the range 0 to 2π.
* @param radians - The angle in radians.
* @returns The normalized angle.
*/
static normalizeAngle(radians) {
const normal = radians % (2.0 * Math.PI);
return normal < 0.0 ? (normal + (2.0 * Math.PI)) : normal;
}
/**
* Finds the intersection points between an ellipse and a line segment.
* @param center - Center of the ellipse.
* @param radiusX - X radius of the ellipse.
* @param radiusY - Y radius of the ellipse.
* @param pointA - Starting point of the line segment.
* @param pointB - Ending point of the line segment.
* @returns An array of intersection points.
*/
static findEllipseLineIntersections(center, radiusX, radiusY, pointA, pointB) {
const origin = VectorExtensions.initialize(pointA.x, pointA.y);
const direction = VectorExtensions.fromPoints(pointA, pointB);
const ellipseCenter = VectorExtensions.initialize(center.x, center.y);
const diff = VectorExtensions.subtract(origin, ellipseCenter);
const scaledDir = VectorExtensions.initialize(direction.x / (radiusX * radiusX), direction.y / (radiusY * radiusY));
const scaledDiff = VectorExtensions.initialize(diff.x / (radiusX * radiusX), diff.y / (radiusY * radiusY));
const a = VectorExtensions.dotProduct(direction, scaledDir);
const b = VectorExtensions.dotProduct(direction, scaledDiff);
const c = VectorExtensions.dotProduct(diff, scaledDiff) - 1.0;
const discriminant = b * b - a * c;
return discriminant < 0 ? [] : this.calculateIntersectionPoints(discriminant, a, b, pointA, pointB);
}
/**
* Calculates the intersection points based on the discriminant.
* @param discriminant - The discriminant value.
* @param a - Coefficient 'a' in the quadratic equation.
* @param b - Coefficient 'b' in the quadratic equation.
* @param pointA - Starting point of the line segment.
* @param pointB - Ending point of the line segment.
* @returns An array of intersection points.
*/
static calculateIntersectionPoints(discriminant, a, b, pointA, pointB) {
const points = [];
if (discriminant > 0) {
const root = Math.sqrt(discriminant);
const t1 = (-b - root) / a;
const t2 = (-b + root) / a;
if (t1 >= 0 && t1 <= 1) {
points.push(PointExtensions.interpolatePoints(pointA, pointB, t1));
}
if (t2 >= 0 && t2 <= 1) {
points.push(PointExtensions.interpolatePoints(pointA, pointB, t2));
}
}
else {
const t = -b / a;
if (t >= 0 && t <= 1) {
points.push(PointExtensions.interpolatePoints(pointA, pointB, t));
}
}
return points;
}
}
/**
* Generated bundle index. Do not edit.
*/
export { Arc, GetIntersections, Line, LineExtensions, Point, PointExtensions, RectExtensions, RoundedRect, ShapeParser, SizeExtensions, TransformModelExtensions, VectorExtensions, adjustRectToMinSize, defaultTransformModel, findClosestAlignment, parseTransformModel, setRectToElement, setRectToViewBox };
//# sourceMappingURL=foblex-2d.js.map