import { normalize } from "./normalize"; /** * 生成 HPGL 代码,支持指定起点和终点 * @param pts 路径点数组 * @param width 页面宽度(mm) * @param height 页面高度(mm) * @param px2mm 像素到毫米的转换系数 * @param startPoint 起点坐标(mm),默认为左上角 [0, height] * @param endPoint 终点坐标(mm),默认为左上角 [0, height] */ export function pts2plotter( pts: [number, number][][], width: number, height: number, px2mm = 0.1, startPoint?: [number, number], endPoint?: [number, number] ) { const start = startPoint ?? [0, height]; const end = endPoint ?? [0, height]; let str = init(width * px2mm, height * px2mm); // 使用最近邻算法排序路径 const sorted = sortPathsByNearestNeighbor(pts, start); // 从起点到第一个路径 if (sorted.length > 0) { const firstPath = sorted[0]; str += ` U${plu(start[0] * px2mm)},${plu((height - start[1]) * px2mm)}`; str += ` U${plu(firstPath[0][0] * px2mm)},${plu((height - firstPath[0][1]) * px2mm)}`; str += ` D${plu(firstPath[0][0] * px2mm)},${plu((height - firstPath[0][1]) * px2mm)}`; // 切割第一个路径 for (let i = 1; i < firstPath.length; i++) { const pt = firstPath[i]; str += ` D${plu(pt[0] * px2mm)},${plu((height - pt[1]) * px2mm)}`; } // 如果第一个路径未闭合,添加闭合命令 if (!isPathClosed(firstPath)) { str += ` D${plu(firstPath[0][0] * px2mm)},${plu((height - firstPath[0][1]) * px2mm)}`; } // 路径之间移动 for (let i = 1; i < sorted.length; i++) { const prevPath = sorted[i - 1]; const currPath = sorted[i]; // 抬刀移动到下一个路径起点 str += ` U${plu(currPath[0][0] * px2mm)},${plu((height - currPath[0][1]) * px2mm)}`; // 下刀切割 str += ` D${plu(currPath[0][0] * px2mm)},${plu((height - currPath[0][1]) * px2mm)}`; for (let j = 1; j < currPath.length; j++) { const pt = currPath[j]; str += ` D${plu(pt[0] * px2mm)},${plu((height - pt[1]) * px2mm)}`; } // 如果当前路径未闭合,添加闭合命令 if (!isPathClosed(currPath)) { str += ` D${plu(currPath[0][0] * px2mm)},${plu((height - currPath[0][1]) * px2mm)}`; } } } // 返回终点 str += ` U${plu(end[0] * px2mm)},${plu((height - end[1]) * px2mm)}`; str += ` D${plu(end[0] * px2mm)},${plu((height - end[1]) * px2mm)}`; str += ` U${plu(end[0] * px2mm)},${plu((height - end[1]) * px2mm)}`; str += endCommand(); return str; } // 兼容旧版本(不使用新参数) export function pts2plotterLegacy( pts: [number, number][][], width: number, height: number, px2mm = 0.1 ) { return pts2plotter(pts, width, height, px2mm); } function topleft(pts: [number, number][]) { let minx = NaN; let miny = NaN; for (const pt of pts) { if (isNaN(minx) || minx > pt[0]) minx = pt[0]; if (isNaN(miny) || miny > pt[1]) miny = pt[1]; } return [minx, miny] as [number, number]; } /** * 检测路径是否已闭合(起点和终点距离小于阈值) * @param path 路径点数组 * @param threshold 距离阈值(像素),默认 0.01 */ function isPathClosed(path: [number, number][], threshold = 0.01): boolean { if (path.length < 2) return true; const [sx, sy] = path[0]; const [ex, ey] = path[path.length - 1]; const dist = Math.sqrt((sx - ex) ** 2 + (sy - ey) ** 2); return dist < threshold; } /** * 计算点到路径的最近距离 * @param pt 点坐标 * @param path 路径点数组 */ function distanceToPath(pt: [number, number], path: [number, number][]): number { let minDist = Infinity; for (const p of path) { const dist = Math.sqrt((pt[0] - p[0]) ** 2 + (pt[1] - p[1]) ** 2); if (dist < minDist) minDist = dist; } return minDist; } /** * 使用最近邻算法排序路径 * @param paths 路径数组 * @param startPos 起始位置 */ function sortPathsByNearestNeighbor( paths: [number, number][][], startPos: [number, number] ): [number, number][][] { if (paths.length === 0) return []; const result: [number, number][][] = []; const remaining = paths.slice(); let currentPos = startPos; while (remaining.length > 0) { // 找到距离当前位置最近的路径 let nearestIndex = 0; let nearestDist = Infinity; for (let i = 0; i < remaining.length; i++) { const dist = distanceToPath(currentPos, remaining[i]); if (dist < nearestDist) { nearestDist = dist; nearestIndex = i; } } // 将最近的路径添加到结果中 const nearestPath = remaining.splice(nearestIndex, 1)[0]; result.push(nearestPath); // 更新当前位置为该路径的终点 currentPos = nearestPath[nearestPath.length - 1]; } return result; } function init(w: number, h: number) { return ` IN TB26,${plu(w)},${plu(h)} CT1`; } function endCommand() { return ' @ @'; } function plu(n: number) { return Math.round(n / 0.025); }