import { Vector2, ShapeUtils, Box2 } from "three"; import { Transform } from "konva/lib/Util"; import { rangMod, round } from "./shared.ts"; import { IRect } from "konva/lib/types"; 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[]) => { const start = vector(line[0]); for (let i = 1; i < line.length; i++) { start.add(line[i]); } return start.multiplyScalar(1 / line.length); }; 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 的坐标范围内 const minX = Math.min(A.x, B.x); const maxX = Math.max(A.x, B.x); const minY = Math.min(A.y, B.y); const maxY = Math.max(A.y, B.y); return ( (minX < P.x || numEq(minX, P.x)) && (P.x < maxX || numEq(maxX, P.x)) && (minY < P.y || numEq(minY, P.y)) && (P.y < maxY || numEq(maxY, P.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 polygon 多边形 * @param line 检测线段 * @returns */ export const polygonLineIntersect = (polygon: Pos[], line: Pos[]) => { const ps: Pos[] =[] for (let i = 0; i < polygon.length; i++) { const line2 = [polygon[i], polygon[(i + 1) % polygon.length]] const p = lineIntersection(line2, line) if (p && lineInner(line, p) && lineInner(line2, p)) { ps.push(p) } } return ps }; /** * 判断点是否在多边形内部 * @param polygon 多边形顶点数组,按顺时针或逆时针顺序排列 * @param pos 要判断的点 * @returns 点在多边形内返回true,否则返回false */ export const isPolygonPointInner = (polygon: Pos[], pos: Pos): boolean => { // 如果多边形少于3个点,直接返回false if (polygon.length < 3) return false; let inside = false; const x = pos.x; const y = pos.y; // 使用射线法判断 for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { const xi = polygon[i].x; const yi = polygon[i].y; const xj = polygon[j].x; const yj = polygon[j].y; // 检查点是否在多边形的顶点上 if ((numEq(xi, x) && numEq(yi, y)) || (numEq(xj, x) && numEq(yj, y))) { return true; } // 检查点是否在边的水平射线上 const intersect = yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi; if (intersect) { inside = !inside; } } return inside; }; /** * 通过角度和两个点获取两者的连接点, * @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) => { return zeroEq(vector(v1).dot(v2)); }; /** * 判断rect1是否完整包含rect2 * @param rect1 * @param rect2 * @returns */ export const isRectContained = (rect1: IRect, rect2: IRect) => { // 计算 rect1 的左右边界 const rect1Left = Math.min(rect1.x, rect1.x + rect1.width); const rect1Right = Math.max(rect1.x, rect1.x + rect1.width); // 计算 rect1 的上下边界 const rect1Top = Math.min(rect1.y, rect1.y + rect1.height); const rect1Bottom = Math.max(rect1.y, rect1.y + rect1.height); // 计算 rect2 的左右边界 const rect2Left = Math.min(rect2.x, rect2.x + rect2.width); const rect2Right = Math.max(rect2.x, rect2.x + rect2.width); // 计算 rect2 的上下边界 const rect2Top = Math.min(rect2.y, rect2.y + rect2.height); const rect2Bottom = Math.max(rect2.y, rect2.y + rect2.height); // 检查 rect2 是否完全在 rect1 内 return ( rect2Left >= rect1Left && rect2Right <= rect1Right && rect2Top >= rect1Top && rect2Bottom <= rect1Bottom ); }; export const getLineEdges = (points: Pos[], strokeWidth: number) => { const v = lineVector(points); const vv = verticalVector(v); const offset = vv.clone().multiplyScalar(strokeWidth / 2); const top = points.map((p) => offset.clone().add(p)); offset.multiplyScalar(-1); const bottom = points.map((p) => offset.clone().add(p)); return [...top, bottom[1], bottom[0]]; }; export type LEJLine = { points: Pos[]; width: number }; export type LEJInfo = { rep: number; points: Pos[] }[]; export const getLEJJoinNdxs = (originLine: Pos[], targetLine: Pos[]) => { let originNdx = -1, targetNdx = -1; for (let i = 0; i < originLine.length; i++) { targetNdx = targetLine.findIndex((p) => eqPoint(originLine[i], p)); if (~targetNdx) { originNdx = i; break; } } return { originNdx, targetNdx }; }; export const getLEJLineAngle = (originLine: Pos[], targetLine: Pos[]) => { const { originNdx, targetNdx } = getLEJJoinNdxs(originLine, targetLine); const targetInvFlag = originNdx === targetNdx; const targetPoints = targetInvFlag ? [...targetLine].reverse() : targetLine; const originVector = lineVector(originLine); const targetVector = lineVector(targetPoints).multiplyScalar(-1); let angle; if (originNdx === targetNdx && originNdx === 0) { angle = vector2IncludedAngle(targetVector, originVector); } else { angle = vector2IncludedAngle(originVector, targetVector); } return { angle, norAngle: rangMod(Math.abs(angle), Math.PI), originNdx, targetNdx, targetPoints, targetInvFlag, }; }; const getLinePolygonOverdo = (polygon: Pos[], slideLine: Pos[]) => { const sideJoins = polygonLineIntersect(polygon, slideLine); // 完全穿插而过 if (sideJoins.length > 1) { return -1; // 完全包含在多边形内 } else if ( sideJoins.length === 0 && slideLine.some((p) => isPolygonPointInner(polygon, p)) ) { return -1; } else if (sideJoins.length === 1) { return sideJoins[0]; } else { return 1; } }; /** * 获取两变短延伸后的平湖处理 * @param origin * @param target * @param minAngle * @param palAngle * @returns 平滑信息,需要结合延伸使用 */ export const getLineEdgeJoinInfo = ( origin: LEJLine, target: LEJLine, minAngle: number, palAngle: number ) => { const { originNdx, targetPoints, norAngle } = getLEJLineAngle( origin.points, target.points ); // 最小可平滑处理角度 if (norAngle < minAngle || norAngle > Math.PI - minAngle) { return; } const targetEdges = getLineEdges(targetPoints, target.width); const originEdges = getLineEdges(origin.points, origin.width); const center = lineCenter([...origin.points, ...target.points]); const originEdgesInv = lineLen(center, originEdges[0]) > lineLen(center, originEdges[3]); let originInner, originOuter; if (originEdgesInv) { originOuter = [originEdges[0], originEdges[1]]; originInner = [originEdges[3], originEdges[2]]; } else { originInner = [originEdges[0], originEdges[1]]; originOuter = [originEdges[3], originEdges[2]]; } let innerJoin: Pos | null = null; // 如果交点边缘在另一条线中,则交点更改为另一条线上 const targetJoinNdxs = originNdx === 0 ? [0, 3] : [1, 2]; const originJoinNdxs = originNdx === 0 ? [1, 2] : [0, 3]; // let innerJoinFlag = getLinePolygonOverdo( // originEdges, // targetJoinNdxs.map((i) => targetEdges[i]) // ); // if (innerJoinFlag === -1) { // return; // } else if (innerJoinFlag !== 1) { // innerJoin = innerJoinFlag as Pos; // } // if (!innerJoin) { // innerJoinFlag = getLinePolygonOverdo( // targetEdges, // originJoinNdxs.map((i) => originEdges[i]) // ); // if (innerJoinFlag === -1) { // return; // } else if (innerJoinFlag !== 1) { // innerJoin = innerJoinFlag as Pos; // } // } if ( originJoinNdxs.some((i) => isPolygonPointInner(targetEdges, originEdges[i]) ) || targetJoinNdxs.some((i) => isPolygonPointInner(originEdges, targetEdges[i])) ) { return; } const targetEdgesInv = lineLen(center, targetEdges[0]) > lineLen(center, targetEdges[3]); let targetInner, targetOuter; if (targetEdgesInv) { targetOuter = [targetEdges[0], targetEdges[1]]; targetInner = [targetEdges[3], targetEdges[2]]; } else { targetInner = [targetEdges[0], targetEdges[1]]; targetOuter = [targetEdges[3], targetEdges[2]]; } let outerJoin = lineIntersection(targetOuter, originOuter); if (!outerJoin) { return; } if (!innerJoin) { innerJoin = lineIntersection(targetInner, originInner); if (!innerJoin) { return; } } // 如果内交点均不在两条内线上则不处理 if ( !lineInner(targetInner, innerJoin) && !lineInner(originInner, innerJoin) ) { return; } let originInnerPoints: Pos[] = [innerJoin]; let originOuterPoints: Pos[] = [outerJoin]; const join = origin.points[originNdx]; // 如果角度过于尖锐则使用平行线 if (norAngle < palAngle) { const pal = getVectorLine(lineVerticalVector([join, outerJoin]), join); originOuterPoints = [lineIntersection(pal, originOuter)!, join]; } const originRepInfos: LEJInfo = []; if (originNdx === 0) { originRepInfos.push( { rep: 0, points: originEdgesInv ? originOuterPoints.reverse() : originInnerPoints, }, { rep: 3, points: originEdgesInv ? originInnerPoints : originOuterPoints, } ); } else { originRepInfos.push( { rep: 1, points: originEdgesInv ? originOuterPoints : originInnerPoints, }, { rep: 2, points: originEdgesInv ? originInnerPoints : originOuterPoints.reverse(), } ); } return originRepInfos; };