2026-03-15 01:31:16 +08:00
|
|
|
|
import { normalize } from "./normalize";
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 生成 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];
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 18:28:44 +08:00
|
|
|
|
let str = init(width * px2mm, height * px2mm);
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 18:28:44 +08:00
|
|
|
|
// 使用最近邻算法排序路径
|
|
|
|
|
|
const sorted = sortPathsByNearestNeighbor(pts, start);
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 从起点到第一个路径
|
|
|
|
|
|
if (sorted.length > 0) {
|
|
|
|
|
|
const firstPath = sorted[0];
|
|
|
|
|
|
str += ` U${plu(start[0] * px2mm)},${plu((height - start[1]) * px2mm)}`;
|
|
|
|
|
|
str += ` D${plu(firstPath[0][0] * px2mm)},${plu((height - firstPath[0][1]) * px2mm)}`;
|
2026-03-15 18:28:44 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 切割第一个路径
|
|
|
|
|
|
for (let i = 1; i < firstPath.length; i++) {
|
|
|
|
|
|
const pt = firstPath[i];
|
|
|
|
|
|
str += ` D${plu(pt[0] * px2mm)},${plu((height - pt[1]) * px2mm)}`;
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
2026-03-15 18:28:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果第一个路径未闭合,添加闭合命令
|
|
|
|
|
|
if (!isPathClosed(firstPath)) {
|
|
|
|
|
|
str += ` D${plu(firstPath[0][0] * px2mm)},${plu((height - firstPath[0][1]) * px2mm)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 路径之间移动
|
|
|
|
|
|
for (let i = 1; i < sorted.length; i++) {
|
|
|
|
|
|
const prevPath = sorted[i - 1];
|
|
|
|
|
|
const currPath = sorted[i];
|
2026-03-15 18:28:44 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 抬刀移动到下一个路径起点
|
|
|
|
|
|
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)}`;
|
2026-03-15 18:28:44 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
for (let j = 1; j < currPath.length; j++) {
|
|
|
|
|
|
const pt = currPath[j];
|
|
|
|
|
|
str += ` D${plu(pt[0] * px2mm)},${plu((height - pt[1]) * px2mm)}`;
|
|
|
|
|
|
}
|
2026-03-15 18:28:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果当前路径未闭合,添加闭合命令
|
|
|
|
|
|
if (!isPathClosed(currPath)) {
|
|
|
|
|
|
str += ` D${plu(currPath[0][0] * px2mm)},${plu((height - currPath[0][1]) * px2mm)}`;
|
|
|
|
|
|
}
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
2026-03-15 01:31:16 +08:00
|
|
|
|
}
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 返回终点
|
|
|
|
|
|
str += ` U${plu(end[0] * px2mm)},${plu((height - end[1]) * px2mm)}`;
|
|
|
|
|
|
str += endCommand();
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
return str;
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
// 兼容旧版本(不使用新参数)
|
|
|
|
|
|
export function pts2plotterLegacy(
|
|
|
|
|
|
pts: [number, number][][],
|
|
|
|
|
|
width: number,
|
|
|
|
|
|
height: number,
|
|
|
|
|
|
px2mm = 0.1
|
|
|
|
|
|
) {
|
|
|
|
|
|
return pts2plotter(pts, width, height, px2mm);
|
|
|
|
|
|
}
|
2026-03-14 16:17:46 +08:00
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
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];
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 18:28:44 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 检测路径是否已闭合(起点和终点距离小于阈值)
|
|
|
|
|
|
* @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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
function init(w: number, h: number) {
|
|
|
|
|
|
return ` IN TB26,${plu(w)},${plu(h)} CT1`;
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-15 01:31:16 +08:00
|
|
|
|
function endCommand() {
|
|
|
|
|
|
return ' @ @';
|
2026-03-14 16:17:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function plu(n: number) {
|
2026-03-15 01:31:16 +08:00
|
|
|
|
return Math.round(n / 0.025);
|
|
|
|
|
|
}
|