ttrpg-tools/src/plotcutter/rounded.ts

130 lines
4.2 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.

/**
* 为正多边形添加圆角
* 将每个角转换为 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
];
}