ttrpg-tools/src/plotcutter/rounded.ts

130 lines
4.2 KiB
TypeScript
Raw Normal View History

2026-03-15 11:48:20 +08:00
/**
*
* segmentsPerCorner 线线
*
*
* 1.
* 2.
* 3.
* @param vertices
* @param cornerRadius
* @param segmentsPerCorner
* @returns
*/
export function getRoundedPolygonPoints(
vertices: [number, number][],
cornerRadius: number,
segmentsPerCorner: number = 4
): [number, number][] {
if (vertices.length < 3) {
return [...vertices];
}
const result: [number, number][] = [];
for (let i = 0; i < vertices.length; i++) {
const prev = vertices[(i - 1 + vertices.length) % vertices.length];
const curr = vertices[i];
const next = vertices[(i + 1) % vertices.length];
// 获取圆角圆心
const center = getTangentCircleCenter(prev, curr, next, cornerRadius);
// 获取圆心到两条边的投影点(圆角的起点和终点)
const start = getProjectedPoint(center, prev, curr);
const end = getProjectedPoint(center, curr, next);
// 计算起始角度和结束角度
const startAngle = Math.atan2(start[1] - center[1], start[0] - center[0]);
const endAngle = Math.atan2(end[1] - center[1], end[0] - center[0]);
// 判断方向(顺时针或逆时针)
const cross = (curr[0] - prev[0]) * (next[1] - prev[1]) - (curr[1] - prev[1]) * (next[0] - prev[0]);
const isClockwise = cross < 0;
// 计算角度差,确保沿着正确的方向
let angleDiff = endAngle - startAngle;
if (isClockwise) {
if (angleDiff > 0) angleDiff -= Math.PI * 2;
} else {
if (angleDiff < 0) angleDiff += Math.PI * 2;
}
// 生成圆弧上的点
for (let j = 0; j <= segmentsPerCorner; j++) {
const t = j / segmentsPerCorner;
const angle = startAngle + angleDiff * t;
const x = center[0] + cornerRadius * Math.cos(angle);
const y = center[1] + cornerRadius * Math.sin(angle);
result.push([x, y]);
}
}
return result;
}
/**
* abc ab bc radius
* @param va
* @param vb
* @param vc
* @param radius
*/
export function getTangentCircleCenter(
va: [number, number],
vb: [number, number],
vc: [number, number],
radius: number
): [number, number] {
// 计算两条边的单位向量
const ba: [number, number] = [va[0] - vb[0], va[1] - vb[1]];
const bc: [number, number] = [vc[0] - vb[0], vc[1] - vb[1]];
const baLen = Math.sqrt(ba[0] ** 2 + ba[1] ** 2);
const bcLen = Math.sqrt(bc[0] ** 2 + bc[1] ** 2);
const baUnit: [number, number] = [ba[0] / baLen, ba[1] / baLen];
const bcUnit: [number, number] = [bc[0] / bcLen, bc[1] / bcLen];
// 角平分线方向
const bisector: [number, number] = [baUnit[0] + bcUnit[0], baUnit[1] + bcUnit[1]];
const bisectorLen = Math.sqrt(bisector[0] ** 2 + bisector[1] ** 2);
const bisectorUnit: [number, number] = [bisector[0] / bisectorLen, bisector[1] / bisectorLen];
// 计算半角的正弦值
const halfAngle = Math.acos((baUnit[0] * bcUnit[0] + baUnit[1] * bcUnit[1]));
const sinHalfAngle = Math.sin(halfAngle / 2);
// 圆心到顶点的距离
const centerDist = radius / sinHalfAngle;
// 圆心位置(从顶点沿角平分线向外)
return [
vb[0] + bisectorUnit[0] * centerDist,
vb[1] + bisectorUnit[1] * centerDist
];
}
/**
* v ab 线
* @param v
* @param a
* @param b
*/
export function getProjectedPoint(
v: [number, number],
a: [number, number],
b: [number, number],
): [number, number] {
const ab: [number, number] = [b[0] - a[0], b[1] - a[1]];
const av: [number, number] = [v[0] - a[0], v[1] - a[1]];
const abLenSq = ab[0] ** 2 + ab[1] ** 2;
const t = (av[0] * ab[0] + av[1] * ab[1]) / abLenSq;
return [
a[0] + ab[0] * t,
a[1] + ab[1] * t
];
}