UNPKG

@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
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