UNPKG

@leolee9086/my-pat-loader

Version:

AutoCAD PAT(填充图案)文件解析和线段生成工具

545 lines (452 loc) 19.6 kB
/** * @module patCalculator * This module provides functions to generate geometric lines based on parsed PAT data. */ /** * Calculates the geometric lines for a given parsed PAT pattern within a specified boundary. * * @param {object} parsedPatData - The parsed PAT data object from `patParser.js`. * @param {string} parsedPatData.name - Pattern name. * @param {Array<object>} parsedPatData.linesDefs - Array of line definition objects. * Each object: { angle, origin: [x,y], delta: [dx,dy], dashes: [d1,d2,...] } * - angle is in degrees. * - origin is [x,y] for the first line of the family. * - delta is [dx, dy] where dx is shift along the line direction for subsequent parallel lines, * and dy is the perpendicular distance between parallel lines. * - dashes is an array of positive (draw) and negative (gap) lengths. Empty for a continuous line. * @param {object} boundary - The rectangular boundary to generate lines within. * @param {number} boundary.minX - Minimum X coordinate of the boundary. * @param {number} boundary.minY - Minimum Y coordinate of the boundary. * @param {number} boundary.maxX - Maximum X coordinate of the boundary. * @param {number} boundary.maxY - Maximum Y coordinate of the boundary. * @param {number} [scale=1.0] - Optional scaling factor for the pattern. * @param {number} [rotation=0.0] - Optional additional rotation for the entire pattern in degrees (围绕(0,0)点旋转图案). * @param {Array<number>} [offset=[0,0]] - Optional offset [x,y] for the entire pattern. * @returns {Array<object>} An array of line segments. Each segment is { start: {x, y}, end: {x, y} }. * Returns an empty array if no lines are generated or if input is invalid. */ export function computePatternLines(parsedPatData, boundary, scale = 1.0, rotation = 0.0, offset = [0, 0]) { if (!parsedPatData || !parsedPatData.linesDefs || !boundary) { // console.warn("patCalculator: Invalid input data or boundary."); return []; } const { linesDefs } = parsedPatData; const { minX, minY, maxX, maxY } = boundary; if (minX === undefined || minY === undefined || maxX === undefined || maxY === undefined || minX >= maxX || minY >= maxY) { // console.warn("patCalculator: Invalid boundary dimensions."); return []; } const allGeneratedLines = []; const toRadians = (degrees) => degrees * (Math.PI / 180); // 边界对角线长度,用于确保线条足够长以覆盖整个边界 const boundaryDiagonal = Math.sqrt(Math.pow(maxX - minX, 2) + Math.pow(maxY - minY, 2)); // 边界中心点,用于定位线条 const centerX = (minX + maxX) / 2; const centerY = (minY + maxY) / 2; // 应用整体偏移 const [offsetX, offsetY] = offset; for (const lineDef of linesDefs) { let { angle, origin, delta, dashes } = lineDef; // Apply scaling to relevant line definition parameters const effectiveOrigin = [origin[0] * scale, origin[1] * scale]; const effectiveDelta = [delta[0] * scale, delta[1] * scale]; const effectiveDashes = dashes.map(d => d * scale); // Apply overall pattern rotation const patternRotationRad = toRadians(rotation); let currentAngleRad = toRadians(angle) + patternRotationRad; // Normalize angle to be between 0 and 2PI currentAngleRad = currentAngleRad % (2 * Math.PI); if (currentAngleRad < 0) { currentAngleRad += (2 * Math.PI); } // Rotate the origin point of the line definition let [ox, oy] = effectiveOrigin; let rotatedOriginX = ox * Math.cos(patternRotationRad) - oy * Math.sin(patternRotationRad); let rotatedOriginY = ox * Math.sin(patternRotationRad) + oy * Math.cos(patternRotationRad); // 应用整体偏移 rotatedOriginX += offsetX; rotatedOriginY += offsetY; const [deltaX, deltaY] = effectiveDelta; // deltaX for shift, deltaY for perpendicular spacing // 1. 计算线条方向向量 const dirX = Math.cos(currentAngleRad); const dirY = Math.sin(currentAngleRad); // 2. 计算垂直于线条的方向向量 (逆时针旋转90度) const perpDirX = -dirY; const perpDirY = dirX; // 如果deltaY为0,则只生成一条线 if (Math.abs(deltaY) < 1e-10) { const linesFromSingleDef = generateDashedLine( rotatedOriginX, rotatedOriginY, dirX, dirY, effectiveDashes, boundary, boundaryDiagonal ); allGeneratedLines.push(...linesFromSingleDef); continue; } // 3. 确定需要生成的平行线数量和范围 // 计算边界在垂直方向上的投影长度 const perpProjection = Math.abs( (maxX - minX) * Math.abs(perpDirX) + (maxY - minY) * Math.abs(perpDirY) ) + Math.abs(deltaY); // 额外加一个deltaY确保覆盖边界 // 计算需要多少条平行线 const numLines = Math.ceil(perpProjection / Math.abs(deltaY)) + 1; // 确定起始线的位置 - 将原点投影到垂直方向 const originPerpDistance = rotatedOriginX * perpDirX + rotatedOriginY * perpDirY; // 计算边界中心在垂直方向上的投影 const centerPerpDistance = centerX * perpDirX + centerY * perpDirY; // 中心与原点在垂直方向上的差距,以deltaY为单位 const centerToOriginLines = Math.round((centerPerpDistance - originPerpDistance) / deltaY); // 确定第一条线的索引,使其大致中心在边界中心 const startLineIndex = -Math.floor(numLines / 2) + centerToOriginLines; // 4. 生成每条平行线 for (let i = 0; i < numLines; i++) { const lineIndex = startLineIndex + i; // 计算这条线的垂直偏移量 const perpOffset = lineIndex * deltaY; // 计算水平偏移量 // 使用累积偏移,按照AutoCAD的规范 const horizOffset = lineIndex * deltaX; // 计算这条线的起始点 const lineStartX = rotatedOriginX + perpOffset * perpDirX + horizOffset * dirX; const lineStartY = rotatedOriginY + perpOffset * perpDirY + horizOffset * dirY; // 生成这条线的所有线段,并添加到结果中 const dashedLines = generateDashedLine( lineStartX, lineStartY, dirX, dirY, effectiveDashes, boundary, boundaryDiagonal ); allGeneratedLines.push(...dashedLines); } } return allGeneratedLines; } /** * 检查图案在当前设置下是否在边界上形成连续的模式 * * @param {object} parsedPatData - 已解析的PAT数据 * @param {object} boundary - 矩形边界 * @param {number} scale - 缩放因子 * @param {number} rotation - 旋转角度(度) * @param {Array<number>} offset - [x,y]偏移 * @returns {object} 包含连续性评估结果的对象 * { * isContinuous: boolean, // 整体是否连续 * continuityScore: number, // 连续性分数(0-1),1表示完全连续 * edgeContinuity: { // 每个边的连续性 * top: boolean, * right: boolean, * bottom: boolean, * left: boolean * }, * details: string // 描述连续性的详细文本信息 * } */ export function checkPatternContinuity(parsedPatData, boundary, scale = 1.0, rotation = 0.0, offset = [0, 0]) { // 生成边界内的线条 const lines = computePatternLines(parsedPatData, boundary, scale, rotation, offset); if (lines.length === 0) { return { isContinuous: false, continuityScore: 0, edgeContinuity: { top: false, right: false, bottom: false, left: false }, details: "没有生成任何线条,无法评估连续性。" }; } const { minX, minY, maxX, maxY } = boundary; const epsilon = 1e-6; // 浮点数比较的容差 // 边界四条边的定义 const edges = { top: { start: minX, end: maxX, isHorizontal: true, coord: maxY }, right: { start: minY, end: maxY, isHorizontal: false, coord: maxX }, bottom: { start: minX, end: maxX, isHorizontal: true, coord: minY }, left: { start: minY, end: maxY, isHorizontal: false, coord: minX } }; // 收集每条边与线段的交点 const intersections = { top: [], right: [], bottom: [], left: [] }; // 找出所有线段与边界的交点 for (const line of lines) { const { start, end } = line; // 检查线段是否与边界的每条边相交 for (const [edgeName, edge] of Object.entries(edges)) { const { isHorizontal, coord, start: edgeStart, end: edgeEnd } = edge; // 线段方程参数: p + t*d, 0 <= t <= 1 // 边界边方程: isHorizontal ? y = coord : x = coord let t; if (isHorizontal) { // 水平边 (y = coord) if (Math.abs(end.y - start.y) < epsilon) continue; // 平行于边 t = (coord - start.y) / (end.y - start.y); } else { // 垂直边 (x = coord) if (Math.abs(end.x - start.x) < epsilon) continue; // 平行于边 t = (coord - start.x) / (end.x - start.x); } // 交点在线段上 if (t >= 0 && t <= 1) { const x = start.x + t * (end.x - start.x); const y = start.y + t * (end.y - start.y); // 交点在边上 if (isHorizontal) { if (x >= edgeStart - epsilon && x <= edgeEnd + epsilon) { intersections[edgeName].push({ x, y, isEndpoint: (Math.abs(t) < epsilon || Math.abs(t - 1) < epsilon) }); } } else { if (y >= edgeStart - epsilon && y <= edgeEnd + epsilon) { intersections[edgeName].push({ x, y, isEndpoint: (Math.abs(t) < epsilon || Math.abs(t - 1) < epsilon) }); } } } } } // 对每条边的交点进行排序 for (const edgeName of Object.keys(edges)) { if (edges[edgeName].isHorizontal) { // 水平边,按x排序 intersections[edgeName].sort((a, b) => a.x - b.x); } else { // 垂直边,按y排序 intersections[edgeName].sort((a, b) => a.y - b.y); } } // 评估每条边的连续性 const edgeContinuity = {}; let totalContinuityScore = 0; let edgesWithIntersections = 0; for (const [edgeName, points] of Object.entries(intersections)) { if (points.length < 2) { // 少于2个交点的边无法形成连续模式 edgeContinuity[edgeName] = false; continue; } edgesWithIntersections++; // 分析交点间距的一致性 const edge = edges[edgeName]; const isHorizontal = edge.isHorizontal; const edgeLength = edge.end - edge.start; // 计算交点间距并检查一致性 const distances = []; let totalGaps = 0; for (let i = 1; i < points.length; i++) { const prev = points[i - 1]; const curr = points[i]; const distance = isHorizontal ? Math.abs(curr.x - prev.x) : Math.abs(curr.y - prev.y); distances.push(distance); totalGaps += distance; } if (distances.length === 0) { edgeContinuity[edgeName] = false; continue; } // 计算平均间距 const avgDistance = totalGaps / distances.length; // 计算间距的变异系数(CV),越小越一致 let sumSquaredDiff = 0; for (const distance of distances) { sumSquaredDiff += Math.pow(distance - avgDistance, 2); } const stdDev = Math.sqrt(sumSquaredDiff / distances.length); const cv = stdDev / avgDistance; // 计算边覆盖率 const coverage = totalGaps / edgeLength; // 判断连续性 - CV小于阈值认为间距一致 const isDistanceConsistent = cv < 0.1; // 10%的变异被认为是一致的 // 计算边的连续性分数 (0-1) const edgeScore = isDistanceConsistent ? Math.min(1, coverage) * (1 - Math.min(1, cv)) : 0; totalContinuityScore += edgeScore; edgeContinuity[edgeName] = edgeScore > 0.8; // 80%以上的分数认为是连续的 } // 计算总体连续性分数 const finalScore = edgesWithIntersections > 0 ? totalContinuityScore / edgesWithIntersections : 0; // 总体连续性判断 const isContinuous = finalScore > 0.8; // 80%以上的分数认为是连续的 // 生成详细描述 let details = ""; if (isContinuous) { details = `图案在当前设置下形成了连续的模式(连续性分数: ${(finalScore * 100).toFixed(1)}%)。`; } else if (finalScore > 0.5) { details = `图案在当前设置下基本连续,但存在一些不连续处(连续性分数: ${(finalScore * 100).toFixed(1)}%)。`; } else if (finalScore > 0) { details = `图案在当前设置下不连续(连续性分数: ${(finalScore * 100).toFixed(1)}%)。`; } else { details = "图案在当前设置下无法形成连续模式。"; } // 添加每条边的连续性信息 details += "\n边界连续性: "; for (const [edge, isCont] of Object.entries(edgeContinuity)) { const edgeName = { top: "顶部", right: "右侧", bottom: "底部", left: "左侧" }[edge]; details += `${edgeName}: ${isCont ? "连续" : "不连续"}; `; } return { isContinuous, continuityScore: finalScore, edgeContinuity, details }; } /** * 生成一条可能带有虚线的线,并与边界相交 * * @param {number} startX - 线的起始点X坐标 * @param {number} startY - 线的起始点Y坐标 * @param {number} dirX - 线的方向向量X分量 * @param {number} dirY - 线的方向向量Y分量 * @param {Array<number>} dashes - 虚线定义数组,正数为画线长度,负数为空白长度 * @param {object} boundary - 裁剪边界 * @param {number} boundaryDiagonal - 边界对角线长度 * @returns {Array<object>} 裁剪后的线段数组,每个线段为 { start: {x, y}, end: {x, y} } */ function generateDashedLine(startX, startY, dirX, dirY, dashes, boundary, boundaryDiagonal) { const { minX, minY, maxX, maxY } = boundary; const result = []; // 对于连续线条 (无虚线定义),生成一条穿过整个边界的线 if (!dashes || dashes.length === 0) { // 创建一条足够长的线,确保能覆盖整个边界 const lineLength = boundaryDiagonal * 2; // 使用边界对角线的两倍长度 // 计算线的两个端点 const p1x = startX - dirX * lineLength; const p1y = startY - dirY * lineLength; const p2x = startX + dirX * lineLength; const p2y = startY + dirY * lineLength; // 使用Liang-Barsky算法裁剪线段 const clippedLine = clipLineLiangBarsky(p1x, p1y, p2x, p2y, boundary); if (clippedLine) { result.push({ start: { x: clippedLine[0], y: clippedLine[1] }, end: { x: clippedLine[2], y: clippedLine[3] } }); } return result; } // 对于虚线模式,我们需要沿着线依次应用虚线定义 // 首先确定一个起始点,使其位于边界外但不会太远 const extendedLength = boundaryDiagonal * 1.5; let currentX = startX - dirX * extendedLength; let currentY = startY - dirY * extendedLength; // 计算整个虚线模式的总长度 let patternLength = 0; for (const dash of dashes) { patternLength += Math.abs(dash); } // 确保从一个完整的虚线周期开始 // 计算从扩展起点到原始起点有多少个完整周期 const distToStart = Math.sqrt( Math.pow(currentX - startX, 2) + Math.pow(currentY - startY, 2) ); // 调整起点使其落在虚线周期的起始处 const cyclesBeforeStart = Math.floor(distToStart / patternLength); const remainingDist = distToStart - (cyclesBeforeStart * patternLength); // 前进remainingDist距离 currentX += dirX * remainingDist; currentY += dirY * remainingDist; // 总计要走的距离是扩展边界的两倍长度 const totalDistance = extendedLength * 2; let distanceTraveled = 0; // 重复应用虚线模式直到覆盖整个边界 let dashIndex = 0; let inDash = true; // 是否在画线状态 while (distanceTraveled < totalDistance) { const dashLength = Math.abs(dashes[dashIndex]); const isDraw = dashes[dashIndex] >= 0; // 正值表示画线,负值表示空白 // 如果是画线部分,则生成一个线段 if (isDraw) { const segmentEndX = currentX + dirX * dashLength; const segmentEndY = currentY + dirY * dashLength; // 裁剪这个线段 const clippedLine = clipLineLiangBarsky( currentX, currentY, segmentEndX, segmentEndY, boundary ); // 如果裁剪后线段仍然存在,则添加到结果 if (clippedLine) { result.push({ start: { x: clippedLine[0], y: clippedLine[1] }, end: { x: clippedLine[2], y: clippedLine[3] } }); } } // 移动当前点到下一个位置 currentX += dirX * dashLength; currentY += dirY * dashLength; distanceTraveled += dashLength; // 移动到下一个虚线定义 dashIndex = (dashIndex + 1) % dashes.length; } return result; } /** * 使用Liang-Barsky算法裁剪线段 * * @param {number} x1 - 线段起点X坐标 * @param {number} y1 - 线段起点Y坐标 * @param {number} x2 - 线段终点X坐标 * @param {number} y2 - 线段终点Y坐标 * @param {object} boundary - 裁剪边界 {minX, minY, maxX, maxY} * @returns {Array<number>|null} 裁剪后的线段 [x1, y1, x2, y2] 或 null表示完全裁剪掉 */ function clipLineLiangBarsky(x1, y1, x2, y2, boundary) { const { minX, minY, maxX, maxY } = boundary; // 线段方向向量 const dx = x2 - x1; const dy = y2 - y1; // 对边界进行参数化 let tMin = 0; // 入口参数,初始值为0 let tMax = 1; // 出口参数,初始值为1 // 边界测试数组: [p, q],其中p是投影坐标,q是起点相对边界的位置 const edges = [ [-dx, x1 - minX], // 左边界 [dx, maxX - x1], // 右边界 [-dy, y1 - minY], // 下边界 [dy, maxY - y1] // 上边界 ]; // 对每个边界进行测试 for (const [p, q] of edges) { if (Math.abs(p) < 1e-10) { // 几乎平行于边界 if (q < 0) { // 线段完全在边界外 return null; } // 否则这个边界不影响线段 continue; } // 计算与边界的交点参数 const t = q / p; if (p < 0) { // 从外部进入边界 tMin = Math.max(tMin, t); } else { // 从内部离开边界 tMax = Math.min(tMax, t); } // 如果参数已经不合理,表示线段完全在边界外 if (tMin > tMax) { return null; } } // 计算裁剪后的线段端点 const clippedX1 = x1 + tMin * dx; const clippedY1 = y1 + tMin * dy; const clippedX2 = x1 + tMax * dx; const clippedY2 = y1 + tMax * dy; return [clippedX1, clippedY1, clippedX2, clippedY2]; }