ttrpg-tools/src/plotcutter/plotter.ts

102 lines
2.7 KiB
TypeScript
Raw Normal View History

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];
let str = init(width * px2mm, height * px2mm);
2026-03-14 16:17:46 +08:00
2026-03-15 01:31:16 +08:00
// 按 X 轴然后 Y 轴排序路径
const sorted = pts.slice();
sorted.sort(function (a, b) {
const [ax, ay] = topleft(a);
const [bx, by] = topleft(b);
2026-03-14 16:17:46 +08:00
2026-03-15 01:31:16 +08:00
if (ax !== bx) return ax - bx;
return ay - by;
});
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)}`;
// 切割第一个路径
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 01:31:16 +08:00
// 路径之间移动
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)}`;
}
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 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);
}