ttrpg-tools/src/plotcutter/plotter.ts

174 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}