import { Vector2, ShapeUtils, Box2 } from "three"; import { Transform } from "konva/lib/Util"; import { round } from "./shared.ts"; export type Pos = { x: number; y: number }; export type Size = { width: number, height: number } export const vector = (pos: Pos = { x: 0, y: 0 }): Vector2 => { return new Vector2(pos.x, pos.y); // if (pos instanceof Vector2) { // return pos; // } else { // return new Vector2(pos.x, pos.y); // } }; export const lVector = (line: Pos[]) => line.map(vector); export const zeroEq = (n: number) => Math.abs(n) < 0.0001; export const numEq = (p1: number, p2: number) => zeroEq(p1 - p2); export const vEq = (v1: Pos, v2: Pos) => numEq(v1.x, v2.x) && numEq(v1.y, v2.y); export const vsBound = (positions: Pos[]) => { const box = new Box2(); box.setFromPoints(positions.map(vector)); return box; }; /** * 获取线段方向 * @param line 线段 * @returns 方向 */ export const lineVector = (line: Pos[]) => vector(line[1]).sub(vector(line[0])).normalize(); /** * 点是否相同 * @param p1 点1 * @param p2 点2 * @returns 是否相等 */ export const eqPoint = vEq; /** * 方向是否相同 * @param p1 点1 * @param p2 点2 * @returns 是否相等 */ export const eqNGDire = (p1: Pos, p2: Pos) => eqPoint(p1, p2) || eqPoint(p1, vector(p2).multiplyScalar(-1)); /** * 获取两点距离 * @param p1 点1 * @param p2 点2 * @returns 距离 */ export const lineLen = (p1: Pos, p2: Pos) => vector(p1).distanceTo(p2); export const vectorLen = (dire: Pos) => vector(dire).length(); /** * 获取向量的垂直向量 * @param dire 原方向 * @returns 垂直向量 */ export const verticalVector = (dire: Pos) => vector({ x: -dire.y, y: dire.x }).normalize(); /** * 获取旋转指定度数后的向量 * @param pos 远向量 * @param angleRad 旋转角度 * @returns 旋转后向量 */ export const rotateVector = (pos: Pos, angleRad: number) => new Transform().rotate(angleRad).point(pos); /** * 创建线段 * @param dire 向量 * @param start 起始点 * @param dis 长度 * @returns 线段 */ export const getVectorLine = ( dire: Pos, start: Pos = { x: 0, y: 0 }, dis: number = 1 ) => [start, vector(dire).multiplyScalar(dis).add(start)]; /** * 获取线段的垂直方向向量 * @param line 原线段 * @returns 垂直向量 */ export const lineVerticalVector = (line: Pos[]) => verticalVector(lineVector(line)); /** * 获取向量的垂直线段 * @param dire 向量 * @param start 线段原点 * @param len 线段长度 */ export const verticalVectorLine = ( dire: Pos, start: Pos = { x: 0, y: 0 }, len: number = 1 ) => getVectorLine(verticalVector(dire), start, len); /** * 获取两向量角度(从向量a出发) * @param v1 向量a * @param v2 向量b * @returns 两向量夹角弧度, 逆时针为正,顺时针为负 */ export const vector2IncludedAngle = (v1: Pos, v2: Pos) => { const start = vector(v1); const end = vector(v2); const angle = start.angleTo(end); return start.cross(end) > 0 ? angle : -angle; }; // 判断多边形方向(Shoelace Formula) export function getPolygonDirection(points: Pos[]) { let area = 0; const numPoints = points.length; for (let i = 0; i < numPoints; i++) { const p1 = points[i]; const p2 = points[(i + 1) % numPoints]; area += (p2.x - p1.x) * (p2.y + p1.y); } // 如果面积为正,是逆时针;否则是顺时针 return area; } /** * 获取两线段角度(从线段a出发) * @param line1 线段a * @param line2 线段b * @returns 两线段夹角弧度 */ export const line2IncludedAngle = (line1: Pos[], line2: Pos[]) => vector2IncludedAngle(lineVector(line1), lineVector(line2)); /** * 获取向量与X正轴角度 * @param v 向量 * @returns 夹角弧度 */ const nXAxis = vector({ x: 1, y: 0 }); export const vectorAngle = (v: Pos) => { const start = vector(v); return start.angleTo(nXAxis); }; /** * 获取线段与方向的夹角弧度 * @param line 线段 * @param dire 方向 * @returns 线段与方向夹角弧度 */ export const lineAndVectorIncludedAngle = (line: Pos[], v: Pos) => vector2IncludedAngle(lineVector(line), v); /** * 获取线段中心点 * @param line * @returns */ export const lineCenter = (line: Pos[]) => vector(line[0]).add(line[1]).multiplyScalar(0.5); export const lineSpeed = (line: Pos[], step: number) => { const p = vector(line[0]) const v = vector(line[1]).sub(line[0]) return p.add(v.multiplyScalar(step)) } export const pointsCenter = (points: Pos[]) => { if (points.length === 0) return { x: 0, y: 0 }; const v = vector(points[0]); for (let i = 1; i < points.length; i++) { v.add(points[i]); } return v.multiplyScalar(1 / points.length); }; export const lineJoin = (l1: Pos[], l2: Pos[]) => { const checks = [ [l1[0], l2[0]], [l1[0], l2[1]], [l1[1], l2[0]], [l1[1], l2[1]], ]; const ndx = checks.findIndex((line) => eqPoint(line[0], line[1])); if (~ndx) { return checks[ndx]; } else { return false; } }; export const isLineEqual = (l1: Pos[], l2: Pos[]) => eqPoint(l1[0], l2[0]) && eqPoint(l1[1], l2[1]); export const isLineReverseEqual = (l1: Pos[], l2: Pos[]) => eqPoint(l1[0], l2[1]) && eqPoint(l1[1], l2[0]); export const isLineIntersect = (l1: Pos[], l2: Pos[]) => { const s1 = l2[1].y - l2[0].y; const s2 = l2[1].x - l2[0].x; const s3 = l1[1].x - l1[0].x; const s4 = l1[1].y - l1[0].y; const s5 = l1[0].y - l2[0].y; const s6 = l1[0].x - l2[0].x; const denominator = s1 * s3 - s2 * s4; const ua = round((s2 * s5 - s1 * s6) / denominator, 6); const ub = round((s3 * s5 - s4 * s6) / denominator, 6); if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) { return true; } else { return false; } }; export const vectorParallel = (dire1: Pos, dire2: Pos) => zeroEq(vector(dire1).cross(dire2)); export const lineParallelRelationship = (l1: Pos[], l2: Pos[]) => { const dire1 = lineVector(l1); const dire2 = lineVector(l2); // 计算线段的法向量 const normal1 = verticalVector(dire1); const normal2 = verticalVector(dire2); const startDire = lineVector([l1[0], l2[0]]); // 计算线段的参数方程 const t1 = round(normal2.dot(startDire) / normal2.dot(dire1), 6); const t2 = round(normal1.dot(startDire) / normal1.dot(dire2), 6); if (t1 === 0 && t2 === 0) { return RelationshipEnum.Overlap; } if (eqPoint(normal1, normal2) || eqPoint(normal1, normal2.clone().negate())) { return lineJoin(l1, l2) ? RelationshipEnum.Overlap : RelationshipEnum.Parallel; } }; export enum RelationshipEnum { // 重叠 Overlap = "Overlap", // 相交 Intersect = "Intersect", // 延长相交 ExtendIntersect = "ExtendIntersect", // 平行 Parallel = "Parallel", // 首尾连接 Join = "Join", // 一样 Equal = "Equal", // 反向 ReverseEqual = "ReverseEqual", } /** * 获取两线段是什么关系,(重叠、相交、平行、首尾相接等) * @param l1 * @param l2 * @returns RelationshipEnum */ export const lineRelationship = (l1: Pos[], l2: Pos[]) => { if (isLineEqual(l1, l2)) { return RelationshipEnum.Equal; } else if (isLineReverseEqual(l1, l2)) { return RelationshipEnum.ReverseEqual; } const parallelRelationship = lineParallelRelationship(l1, l2); if (parallelRelationship) { return parallelRelationship; } else if (lineJoin(l1, l2)) { return RelationshipEnum.Join; } else if (isLineIntersect(l1, l2)) { return RelationshipEnum.Intersect; // 两线段相交 } else { return RelationshipEnum.ExtendIntersect; // 延长可相交 } }; export const createLine = (p: Pos, v: Pos, l?: number) => { const line = [p]; if (l) { v = vector(v).multiplyScalar(l); } line[1] = vector(line[0]).add(v); return line; }; /** * 获取两线段交点,可延长相交 * @param l1 线段1 * @param l2 线段2 * @returns 交点坐标 */ export const lineIntersection = (l1: Pos[], l2: Pos[]) => { // 定义两条线段的起点和终点坐标 const [line1Start, line1End] = lVector(l1); const [line2Start, line2End] = lVector(l2); // 计算线段的方向向量 const dir1 = line1End.clone().sub(line1Start); const dir2 = line2End.clone().sub(line2Start); // 计算参数方程中的系数 const a = dir1.x; const b = -dir2.x; const c = dir1.y; const d = -dir2.y; const e = line2Start.x - line1Start.x; const f = line2Start.y - line1Start.y; // 求解参数t和s const t = (d * e - b * f) / (a * d - b * c); // 计算交点坐标 const p = line1Start.clone().add(dir1.clone().multiplyScalar(t)); if (isNaN(p.x) || !isFinite(p.x) || isNaN(p.y) || !isFinite(p.y)) return null; return p; }; /** * 获取点是否在线上 * @param line 线段 * @param position 点 */ export const lineInner = (line: Pos[], position: Pos) => { // 定义线段的起点和终点坐标 const [A, B] = lVector(line); // 定义一个点的坐标 const P = vector(position); // 计算向量 AP 和 AB const AP = P.clone().sub(A); const AB = B.clone().sub(A); // 计算叉积 const crossProduct = AP.x * AB.y - AP.y * AB.x; // 如果叉积不为 0,说明点 P 不在直线 AB 上 if (!zeroEq(crossProduct)) { return false; } // 检查点 P 的坐标是否在 A 和 B 的坐标范围内 return ( Math.min(A.x, B.x) <= P.x && P.x <= Math.max(A.x, B.x) && Math.min(A.y, B.y) <= P.y && P.y <= Math.max(A.y, B.y) ); }; /** * 获取点在线段上的投影 * @param line 线段 * @param position 点 * @returns 投影信息 */ export const linePointProjection = (line: Pos[], position: Pos) => { // 定义线段的起点和终点坐标 const [lineStart, lineEnd] = lVector(line); // 定义一个点的坐标 const point = vector(position); // 计算线段的方向向量 const lineDir = lineEnd.clone().sub(lineStart); // 计算点到线段起点的向量 const pointToLineStart = point.clone().sub(lineStart); // 计算点在线段方向上的投影长度 const t = pointToLineStart.dot(lineDir.normalize()); // 计算投影点的坐标 return lineStart.add(lineDir.multiplyScalar(t)); }; /** * 获取点距离线段最近距离 * @param line 直线 * @param position 参考点 * @returns 距离 */ export const linePointLen = (line: Pos[], position: Pos) => lineLen(position, linePointProjection(line, position)); /** * 计算多边形是否为逆时针 * @param points 多边形顶点 * @returns true | false */ export const isPolygonCounterclockwise = (points: Pos[]) => ShapeUtils.isClockWise(points.map(vector)); /** * 切割线段,返回连段切割点 * @param line 线段 * @param amount 切割份量 * @param unit 一份单位大小 * @returns 点数组 */ export const lineSlice = ( line: Pos[], amount: number, unit = lineLen(line[0], line[1]) / amount ) => new Array(unit) .fill(0) .map((_, i) => linePointProjection(line, { x: i * unit, y: i * unit })); /** * 线段是否相交多边形 * @param polygon 多边形 * @param line 检测线段 * @returns */ export const isPolygonLineIntersect = (polygon: Pos[], line: Pos[]) => { for (let i = 0; i < polygon.length; i++) { if (isLineIntersect([polygon[i], polygon[i + 1]], line)) { return true; } } return false; }; /** * 通过角度和两个点获取两者的连接点, * @param p1 * @param p2 * @param rad */ export const joinPoint = (p1: Pos, p2: Pos, rad: number) => { const lvector = new Vector2() .subVectors(p1, p2) .rotateAround({ x: 0, y: 0 }, rad); return vector(p2).add(lvector); }; /** * 要缩放多少才能到达目标 * @param origin 缩放原点 * @param scaleDirection 缩放方向 * @param p1 当前点位 * @param p2 目标点位 * @returns */ export function calculateScaleFactor( origin: Pos, scaleDirection: Pos, p1: Pos, p2: Pos ) { const op1 = vector(p1).sub(origin); const op2 = vector(p2).sub(origin); const xZero = zeroEq(op1.x); const yZero = zeroEq(op1.y); if (zeroEq(op1.x) || zeroEq(op2.y)) return; if (zeroEq(scaleDirection.x)) { if (zeroEq(p2.x - p1.x)) { return zeroEq(op1.y - op2.y) ? 1 : yZero ? null : op2.y / op1.y; } else { return; } } if (zeroEq(scaleDirection.y)) { if (zeroEq(p2.y - p1.y)) { return zeroEq(op1.x - op2.x) ? 1 : xZero ? null : op2.x / op1.x; } else { return; } } if (xZero && yZero) { return null; } const xScaleFactor = op2.x / (op1.x * scaleDirection.x); const yScaleFactor = op2.y / (op1.y * scaleDirection.y); if (xZero) { return yScaleFactor; } else if (yZero) { return xScaleFactor; } if (zeroEq(xScaleFactor - yScaleFactor)) { return xScaleFactor; } } // 获取两线段的矩阵关系 export const getLineRelationMat = (l1: [Pos, Pos], l2: [Pos, Pos]) => { // 提取点 const P1 = l1[0]; // l1 的起点 const P1End = l1[1]; // l1 的终点 const P2 = l2[0]; // l2 的起点 const P2End = l2[1]; // l2 的终点 // 计算方向向量 const d1 = { x: P1End.x - P1.x, y: P1End.y - P1.y }; const d2 = { x: P2End.x - P2.x, y: P2End.y - P2.y }; // 计算方向向量的长度 const lengthD1 = Math.sqrt(d1.x ** 2 + d1.y ** 2); const lengthD2 = Math.sqrt(d2.x ** 2 + d2.y ** 2); if (lengthD1 === 0 || lengthD2 === 0) return new Transform(); // 归一化方向向量 const unitD1 = { x: d1.x / lengthD1, y: d1.y / lengthD1 }; const unitD2 = { x: d2.x / lengthD2, y: d2.y / lengthD2 }; // 计算旋转角度 const angle = Math.atan2(unitD2.y, unitD2.x) - Math.atan2(unitD1.y, unitD1.x); // 计算旋转矩阵 // 计算缩放因子 const scale = lengthD2 / lengthD1; // 计算平移向量 const translation = [P2.x - P1.x, P2.y - P1.y]; const mat = new Transform() .translate(translation[0], translation[1]) .translate(P1.x, P1.y) .scale(scale, scale) .rotate(angle) .translate(-P1.x, -P1.y); if (!eqPoint(mat.point(P1), P2)) { console.error('对准不正确 旋转后P1', mat.point(P1), P2) } if (!eqPoint(mat.point(P1End), P2End)) { console.error('对准不正确 旋转后P2', mat.point(P1End), P1End) } return mat }; // 判断两向量是否垂直 export const isVertical = (v1: Pos, v2: Pos) => { console.log(vector(v1).dot(v2)) return zeroEq(vector(v1).dot(v2)) }