import { sum, findIndex, getShapeDirection, getDist, throttle, TINY_NUM, find } from "@daybrush/utils"; import { OverlapPointInfo, PointInfo, Rect } from "./types"; import { flat, isSameConstants, isSamePoint, tinyThrottle } from "./utils"; /** * @namespace OverlapArea */ /** * Gets the size of a shape (polygon) made of points. * @memberof OverlapArea */ export function getAreaSize(points: number[][]): number { if (points.length < 3) { return 0; } return Math.abs(sum(points.map((point, i) => { const nextPoint = points[i + 1] || points[0]; return point[0] * nextPoint[1] - nextPoint[0] * point[1]; }))) / 2; } /** * Get points that fit the rect, * @memberof OverlapArea */ export function fitPoints(points: number[][], rect: Rect): number[][] { const { width, height, left, top } = rect; const { minX, minY, maxX, maxY } = getMinMaxs(points); const ratioX = width / (maxX - minX); const ratioY = height / (maxY - minY); return points.map(point => { return [ left + (point[0] - minX) * ratioX, top + (point[1] - minY) * ratioY, ]; }); } /** * Get the minimum and maximum points of the points. * @memberof OverlapArea */ export function getMinMaxs(points: number[][]): { minX: number, minY: number, maxX: number, maxY: number } { const xs = points.map(point => point[0]); const ys = points.map(point => point[1]); return { minX: Math.min(...xs), minY: Math.min(...ys), maxX: Math.max(...xs), maxY: Math.max(...ys), }; } /** * Whether the point is in shape * @param - point pos * @param - shape points * @param - whether to check except line * @memberof OverlapArea */ export function isInside(pos: number[], points: number[][], excludeLine?: boolean): boolean { const [x, y] = pos; const { minX, maxX, } = getMinMaxs(points); const xLine = [[minX, y], [maxX, y]]; const xLinearConstants = getLinearConstants(xLine[0], xLine[1]); const lines = convertLines(points); interface IntersectionPosInfo { pos: number[]; line: number[][]; type: "intersection" | "point" | "line"; } const intersectionPosInfos: IntersectionPosInfo[] = []; lines.forEach(line => { const linearConstants = getLinearConstants(line[0], line[1]); const standardPoint = line[0]; if (isSameConstants(xLinearConstants, linearConstants)) { intersectionPosInfos.push({ pos: pos, line, type: "line", }); } else { const xPoints = getPointsOnLines(getIntersectionPointsByConstants(xLinearConstants, linearConstants), [xLine, line]); xPoints.forEach(point => { if (line.some(linePoint => isSamePoint(linePoint, point))) { intersectionPosInfos.push({ pos: point, line, type: "point", }); } else if (tinyThrottle(standardPoint[1] - y) !== 0) { intersectionPosInfos.push({ pos: point, line, type: "intersection", }); } }) } }); if (!excludeLine) { // on line if (find(intersectionPosInfos, p => p[0] === x)) { return true; } } let intersectionCount = 0; const xMap = {}; intersectionPosInfos.forEach(({ pos, type, line }) => { if (pos[0] > x) { return; } if (type === "intersection") { ++intersectionCount; } else if (type === "line") { return; } else if (type === "point") { const point = find(line, linePoint => linePoint[1] !== y); const prevValue = xMap[pos[0]]; const nextValue = point[1] > y ? 1 : -1; if (!prevValue) { xMap[pos[0]] = nextValue; } else if (prevValue !== nextValue) { ++intersectionCount; } } }); return intersectionCount % 2 === 1; } /** * Get distance from point to constants. [a, b, c] (ax + by + c = 0) * @return [a, b, c] * @memberof OverlapArea */ export function getDistanceFromPointToConstants( [a, b, c]: [number, number, number], pos: number[], ) { return (a * pos[0] + b * pos[1] + c) / (a * a + b * b); } /** * Get the coefficient of the linear function. [a, b, c] (ax + by + c = 0) * @return [a, b, c] * @memberof OverlapArea */ export function getLinearConstants(point1: number[], point2: number[]): [number, number, number] { const [x1, y1] = point1; const [x2, y2] = point2; // ax + by + c = 0 // [a, b, c] let dx = x2 - x1; let dy = y2 - y1; if (Math.abs(dx) < TINY_NUM) { dx = 0; } if (Math.abs(dy) < TINY_NUM) { dy = 0; } // b > 0 // ax + by + c = 0 let a = 0; let b = 0; let c = 0; if (!dx) { if (dy) { // -x + 1 = 0 a = -1; c = x1; } } else if (!dy) { // y - 1 = 0 b = 1; c = -y1; } else { // y = -a(x - x1) + y1 // ax + y + a * x1 - y1 = 0 a = -dy / dx; b = 1; c = -a * x1 - y1; } return [a, b, c] as [number, number, number]; } /** * Get intersection points with linear functions. * @memberof OverlapArea */ export function getIntersectionPointsByConstants( linearConstants1: number[], linearConstants2: number[], ): number[][] { const [a1, b1, c1] = linearConstants1; const [a2, b2, c2] = linearConstants2; const isZeroA = a1 === 0 && a2 === 0; const isZeroB = b1 === 0 && b2 === 0; let results: number[][] = []; if (isZeroA && isZeroB) { return []; } else if (isZeroA) { // b1 * y + c1 = 0 // b2 * y + c2 = 0 const y1 = -c1 / b1; const y2 = -c2 / b2; if (y1 !== y2) { return []; } else { return [ [-Infinity, y1], [Infinity, y1], ]; } } else if (isZeroB) { // a1 * x + c1 = 0 // a2 * x + c2 = 0 const x1 = -c1 / a1; const x2 = -c2 / a2; if (x1 !== x2) { return []; } else { return [ [x1, -Infinity], [x1, Infinity], ]; } } else if (a1 === 0) { // b1 * y + c1 = 0 // y = - c1 / b1; // a2 * x + b2 * y + c2 = 0 const y = -c1 / b1; const x = -(b2 * y + c2) / a2; results = [[x, y]]; } else if (a2 === 0) { // b2 * y + c2 = 0 // y = - c2 / b2; // a1 * x + b1 * y + c1 = 0 const y = -c2 / b2; const x = -(b1 * y + c1) / a1; results = [[x, y]]; } else if (b1 === 0) { // a1 * x + c1 = 0 // x = - c1 / a1; // a2 * x + b2 * y + c2 = 0 const x = - c1 / a1; const y = -(a2 * x + c2) / b2; results = [[x, y]]; } else if (b2 === 0) { // a2 * x + c2 = 0 // x = - c2 / a2; // a1 * x + b1 * y + c1 = 0 const x = - c2 / a2; const y = -(a1 * x + c1) / b1; results = [[x, y]]; } else { // a1 * x + b1 * y + c1 = 0 // a2 * x + b2 * y + c2 = 0 // b2 * a1 * x + b2 * b1 * y + b2 * c1 = 0 // b1 * a2 * x + b1 * b2 * y + b1 * c2 = 0 // (b2 * a1 - b1 * a2) * x = (b1 * c2 - b2 * c1) const x = (b1 * c2 - b2 * c1) / (b2 * a1 - b1 * a2); const y = -(a1 * x + c1) / b1; results = [[x, y]]; } return results.map(result => [result[0], result[1]]); } /** * Get intersection points to the two lines. * @memberof OverlapArea */ export function getIntersectionPoints( line1: number[][], line2: number[][], isLimit?: boolean, ): number[][] { const points = getIntersectionPointsByConstants( getLinearConstants(line1[0], line1[1]), getLinearConstants(line2[0], line2[1]), ); if (isLimit) { return getPointsOnLines(points, [line1, line2]); } return points; } export function isPointOnLine( pos: number[], line: number[][], ) { const linearConstants = getLinearConstants(line[0], line[1]); return tinyThrottle(getDistanceFromPointToConstants(linearConstants, pos)) === 0; } /** * Get the points on the lines (between two points). * @memberof OverlapArea */ export function getPointsOnLines( points: number[][], lines: number[][][], ): number[][] { const minMaxs = lines.map(line => [0, 1].map(order => [ Math.min(line[0][order], line[1][order]), Math.max(line[0][order], line[1][order]), ])); let results: number[][] = []; if (points.length === 2) { const [x, y] = points[0]; if (!tinyThrottle(x - points[1][0])) { /// Math.max(minY1, minY2) const top = Math.max(...minMaxs.map(minMax => minMax[1][0])); /// Math.min(maxY1, miax2) const bottom = Math.min(...minMaxs.map(minMax => minMax[1][1])); if (tinyThrottle(top - bottom) > 0) { return []; } results = [ [x, top], [x, bottom], ]; } else if (!tinyThrottle(y - points[1][1])) { /// Math.max(minY1, minY2) const left = Math.max(...minMaxs.map(minMax => minMax[0][0])); /// Math.min(maxY1, miax2) const right = Math.min(...minMaxs.map(minMax => minMax[0][1])); if (tinyThrottle(left - right) > 0) { return []; } results = [ [left, y], [right, y], ]; } } if (!results.length) { results = points.filter(point => { const [pointX, pointY] = point; return minMaxs.every(minMax => { return (0 <= tinyThrottle(pointX - minMax[0][0]) && 0 <= tinyThrottle(minMax[0][1] - pointX)) && (0 <= tinyThrottle(pointY - minMax[1][0]) && 0 <= tinyThrottle(minMax[1][1] - pointY)); }); }); } return results.map(result => [tinyThrottle(result[0]), tinyThrottle(result[1])]); } /** * Convert two points into lines. * @function * @memberof OverlapArea */ export function convertLines(points: number[][]): number[][][] { return [...points.slice(1), points[0]].map((point, i) => [points[i], point]); } function getOverlapPointInfos(points1: number[][], points2: number[][]): OverlapPointInfo[] { const targetPoints1 = points1.slice(); const targetPoints2 = points2.slice(); if (getShapeDirection(targetPoints1) === -1) { targetPoints1.reverse(); } if (getShapeDirection(targetPoints2) === -1) { targetPoints2.reverse(); } const lines1 = convertLines(targetPoints1); const lines2 = convertLines(targetPoints2); const linearConstantsList1 = lines1.map(line1 => getLinearConstants(line1[0], line1[1])); const linearConstantsList2 = lines2.map(line2 => getLinearConstants(line2[0], line2[1])); const overlapInfos: OverlapPointInfo[] = []; linearConstantsList1.forEach((linearConstants1, i) => { const line1 = lines1[i]; const linePointInfos: OverlapPointInfo[] = []; linearConstantsList2.forEach((linearConstants2, j) => { const intersectionPoints = getIntersectionPointsByConstants(linearConstants1, linearConstants2); const points = getPointsOnLines(intersectionPoints, [line1, lines2[j]]); linePointInfos.push(...points.map(pos => ({ index1: i, index2: j, pos, type: "intersection" as const, }))); }); linePointInfos.sort((a, b) => { return getDist(line1[0], a.pos) - getDist(line1[0], b.pos); }); overlapInfos.push(...linePointInfos); if (isInside(line1[1], targetPoints2)) { overlapInfos.push({ index1: i, index2: -1, pos: line1[1], type: "inside" as const, }); } }); lines2.forEach((line2, i) => { if (!isInside(line2[1], targetPoints1)) { return; } let isNext = false; let index = findIndex(overlapInfos, ({ index2 }) => { if (index2 === i) { isNext = true; return false; } if (isNext) { return true; } return false; }); if (index === -1) { isNext = false; index = findIndex(overlapInfos, ({ index1, index2 }) => { if (index1 === -1 && index2 + 1 === i) { isNext = true; return false; } if (isNext) { return true; } return false; }); } if (index === -1) { overlapInfos.push({ index1: -1, index2: i, pos: line2[1], type: "inside" as const, }); } else { overlapInfos.splice(index, 0, { index1: -1, index2: i, pos: line2[1], type: "inside" as const, }); } }); const pointMap: Record = {}; return overlapInfos.filter(({ pos }) => { const key = `${pos[0]}x${pos[1]}`; if (pointMap[key]) { return false; } pointMap[key] = true; return true; }); } /** * Get the points of the overlapped part of two shapes. * @function * @memberof OverlapArea */ export function getOverlapPoints(points1: number[][], points2: number[][]): number[][] { const infos = getOverlapPointInfos(points1, points2); return infos.map(({ pos }) => pos); } function isConnectedLine(line: OverlapPointInfo[]) { const { 0: { index1: prevIndex1, index2: prevIndex2, }, 1: { index1: nextIndex1, index2: nextIndex2, } } = line; if (prevIndex1 !== -1) { // same line if (prevIndex1 === nextIndex1) { return true; } if (prevIndex1 + 1 === nextIndex1) { return true; } } if (prevIndex2 !== -1) { // same line if (prevIndex2 === nextIndex2) { return true; } if (prevIndex2 + 1 === nextIndex2) { return true; } } return false; } /** * Get the areas of the overlapped part of two shapes. * @function * @memberof OverlapArea */ export function getOverlapAreas(points1: number[][], points2: number[][]): number[][][] { const infos = getOverlapPointInfos(points1, points2); const areas: OverlapPointInfo[][] = []; let area: OverlapPointInfo[]; getOverlapPointInfos(points1, points2).forEach((info, i, arr) => { if (i === 0 || !isConnectedLine([arr[i - 1], info])) { area = [info]; areas.push(area); } else { area.push(info); } }); return areas.map(area => area.map(({ pos }) => pos)); } function findReversedAreas(points1: number[][], points2: number[][], index: number = 0, areas: number[][][] = []): number[][][] { const isFirst = areas.length === 0; const length = points1.length; const nextIndex = points1[index] ? index : 0; const nextPoints1 = [...points1.slice(nextIndex), ...points1.slice(0, nextIndex)]; for (let i = 0; i < length; ++i) { const point1 = nextPoints1[i]; if (find(points2, point2 => point2[0] === point1[0] && point2[1] === point1[1])) { continue; } if (areas.some(nextArea => find(nextArea, areaPoint => areaPoint[0] === point1[0] && areaPoint[1] === point1[1]))) { if (isFirst) { continue; } else { break; } } let nextArea: number[][]; if (isFirst) { nextArea = []; areas.push(nextArea); } else { nextArea = areas[areas.length - 1]; } nextArea.push(point1); const line = [point1, points1[index + 1] || points1[0]]; const nextPoint2 = points2.filter(point2 => { return isPointOnLine(point2, line); }).sort((a, b) => { return getDist(point1, a) - getDist(point1, b); })[0]; if (!nextPoint2) { findReversedAreas(nextPoints1, points2, i + 1, areas); break; } else { const point2Index = points2.indexOf(nextPoint2); findReversedAreas(points2, points1, point2Index, areas); if (!isFirst) { break; } } } return areas; } export function findConnectedAreas(points1: number[][], points2: number[][]) { return findReversedAreas(points1, [...points2].reverse()); } /** * Get non-overlapping areas of two shapes based on points1. * @memberof OverlapArea */ export function getUnoverlapAreas(points1: number[][], points2: number[][]): number[][][] { if (!points2.length) { return [[...points1]]; } const overlapAreas = getOverlapAreas(points1, points2); let unoverlapAreas = [points1]; overlapAreas.forEach(overlapArea => { const nextOverlapArea = [...overlapArea].reverse(); unoverlapAreas = flat(unoverlapAreas.map(area => { const connectedAreas = findReversedAreas(area, nextOverlapArea); const firstConnectedArea = connectedAreas[0]; if (connectedAreas.length === 1 && nextOverlapArea.every(point => firstConnectedArea.indexOf(point) === -1)) { const lastPoint = firstConnectedArea[firstConnectedArea.length - 1]; const firstPoint = [...nextOverlapArea].sort((a, b) => { return getDist(lastPoint, a) - getDist(lastPoint, b); })[0]; const firstIndex = nextOverlapArea.indexOf(firstPoint); firstConnectedArea.push( ...nextOverlapArea.slice(firstIndex), ...nextOverlapArea.slice(0, firstIndex), nextOverlapArea[firstIndex], lastPoint, ); } return connectedAreas; })); }); return unoverlapAreas; } /** * Gets the size of the overlapped part of two shapes. * @function * @memberof OverlapArea */ export function getOverlapSize(points1: number[][], points2: number[][]): number { const points = getOverlapPoints(points1, points2); return getAreaSize(points); }