fix: contour gen
This commit is contained in:
parent
9f665fc403
commit
ee2fa057f6
|
|
@ -1,263 +0,0 @@
|
|||
import { describe, test, expect } from '@jest/globals';
|
||||
import {
|
||||
getRoundedPolygonPoints,
|
||||
getCardShapePoints,
|
||||
getInscribedTrianglePoints,
|
||||
getInscribedHexagonPoints
|
||||
} from './contour';
|
||||
|
||||
/**
|
||||
* 辅助函数:计算两点之间的距离
|
||||
*/
|
||||
function distance(p1: [number, number], p2: [number, number]): number {
|
||||
return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 辅助函数:检查点是否接近预期值
|
||||
*/
|
||||
function pointCloseTo(actual: [number, number], expected: [number, number], tolerance: number = 0.01): boolean {
|
||||
return Math.abs(actual[0] - expected[0]) < tolerance &&
|
||||
Math.abs(actual[1] - expected[1]) < tolerance;
|
||||
}
|
||||
|
||||
describe('getRoundedPolygonPoints', () => {
|
||||
describe('矩形圆角', () => {
|
||||
test('应该生成正确的矩形圆角 (segmentsPerCorner=2)', () => {
|
||||
const vertices: [number, number][] = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 60],
|
||||
[0, 60]
|
||||
];
|
||||
const cornerRadius = 10;
|
||||
const segmentsPerCorner = 2;
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner);
|
||||
|
||||
// 每个角应该有:起点 + 2 个弧线点 = 3 个点,4 个角共 12 个点
|
||||
// 但由于结构是:起点 + 弧线点 1 + 弧线点 2,然后下一条边的起点就是上一个角的终点
|
||||
// 所以总点数应该是 4 * (1 + 2) = 12 个点
|
||||
expect(points.length).toBe(12);
|
||||
|
||||
// 验证第一个角(左下角,顶点 [0, 0])的起点应该在 (0, 10)
|
||||
expect(pointCloseTo(points[0], [0, 10])).toBe(true);
|
||||
|
||||
// 验证第一个角的弧线点应该在圆上
|
||||
const center0: [number, number] = [10, 10];
|
||||
const arcPoint1: [number, number] = points[1];
|
||||
const arcPoint2: [number, number] = points[2];
|
||||
|
||||
// 检查弧线点到圆心的距离是否接近半径
|
||||
const dist1 = distance(arcPoint1, center0);
|
||||
const dist2 = distance(arcPoint2, center0);
|
||||
expect(Math.abs(dist1 - cornerRadius)).toBeLessThan(0.1);
|
||||
expect(Math.abs(dist2 - cornerRadius)).toBeLessThan(0.1);
|
||||
|
||||
// 验证第二个角(右下角,顶点 [100, 0])的起点应该在 (90, 0)
|
||||
expect(pointCloseTo(points[3], [90, 0])).toBe(true);
|
||||
});
|
||||
|
||||
test('应该生成闭合的轮廓', () => {
|
||||
const vertices: [number, number][] = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 60],
|
||||
[0, 60]
|
||||
];
|
||||
const cornerRadius = 10;
|
||||
const segmentsPerCorner = 4;
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner);
|
||||
|
||||
// 验证最后一个点和第一个点之间的距离应该合理(闭合路径)
|
||||
const firstPoint = points[0];
|
||||
const lastPoint = points[points.length - 1];
|
||||
|
||||
// 最后一个点应该是最后一个角的弧线终点,接近第一个点
|
||||
// 实际上,由于每个角都从起点开始,最后一个点应该接近第一个角的起点
|
||||
const dist = distance(firstPoint, lastPoint);
|
||||
expect(dist).toBeLessThan(cornerRadius * 0.5); // 应该比较接近
|
||||
});
|
||||
|
||||
test('圆角半径过大时应该被限制', () => {
|
||||
const vertices: [number, number][] = [
|
||||
[0, 0],
|
||||
[20, 0],
|
||||
[20, 60],
|
||||
[0, 60]
|
||||
];
|
||||
const cornerRadius = 50; // 远大于边长的一半
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, cornerRadius);
|
||||
|
||||
// 验证所有点都在边界框内
|
||||
for (const [x, y] of points) {
|
||||
expect(x).toBeGreaterThanOrEqual(0);
|
||||
expect(x).toBeLessThanOrEqual(20);
|
||||
expect(y).toBeGreaterThanOrEqual(0);
|
||||
expect(y).toBeLessThanOrEqual(60);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('三角形圆角', () => {
|
||||
test('应该为三角形生成正确的圆角 (segmentsPerCorner=2)', () => {
|
||||
const vertices = getInscribedTrianglePoints(100, 60);
|
||||
const cornerRadius = 5;
|
||||
const segmentsPerCorner = 2;
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner);
|
||||
|
||||
// 3 个角,每个角有 1 个起点 + 2 个弧线点 = 3 个点
|
||||
expect(points.length).toBe(9);
|
||||
|
||||
// 验证所有弧线点到对应圆心的距离接近半径
|
||||
// (这里简化验证,只检查点数和合理性)
|
||||
expect(points.length).toBeGreaterThan(0);
|
||||
|
||||
// 验证所有点都在边界框内
|
||||
for (const [x, y] of points) {
|
||||
expect(x).toBeGreaterThanOrEqual(0);
|
||||
expect(x).toBeLessThanOrEqual(100);
|
||||
expect(y).toBeGreaterThanOrEqual(0);
|
||||
expect(y).toBeLessThanOrEqual(60);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('六边形圆角', () => {
|
||||
test('应该为六边形生成正确的圆角 (segmentsPerCorner=2)', () => {
|
||||
const vertices = getInscribedHexagonPoints(100, 60);
|
||||
const cornerRadius = 5;
|
||||
const segmentsPerCorner = 2;
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner);
|
||||
|
||||
// 6 个角,每个角有 1 个起点 + 2 个弧线点 = 3 个点
|
||||
expect(points.length).toBe(18);
|
||||
|
||||
// 验证所有点都在边界框内
|
||||
for (const [x, y] of points) {
|
||||
expect(x).toBeGreaterThanOrEqual(0);
|
||||
expect(x).toBeLessThanOrEqual(100);
|
||||
expect(y).toBeGreaterThanOrEqual(0);
|
||||
expect(y).toBeLessThanOrEqual(60);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况', () => {
|
||||
test('圆角半径为 0 时返回原始顶点', () => {
|
||||
const vertices: [number, number][] = [
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 60],
|
||||
[0, 60]
|
||||
];
|
||||
|
||||
const points = getRoundedPolygonPoints(vertices, 0);
|
||||
|
||||
expect(points).toEqual(vertices);
|
||||
});
|
||||
|
||||
test('顶点数少于 3 个时返回原始顶点', () => {
|
||||
const line: [number, number][] = [[0, 0], [100, 0]];
|
||||
const points = getRoundedPolygonPoints(line, 10);
|
||||
expect(points).toEqual(line);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCardShapePoints', () => {
|
||||
describe('矩形', () => {
|
||||
test('无圆角矩形', () => {
|
||||
const points = getCardShapePoints('rectangle', 100, 60, 0);
|
||||
expect(points.length).toBe(4);
|
||||
expect(points).toEqual([
|
||||
[0, 0],
|
||||
[100, 0],
|
||||
[100, 60],
|
||||
[0, 60]
|
||||
]);
|
||||
});
|
||||
|
||||
test('带圆角矩形 (segmentsPerCorner=2)', () => {
|
||||
const points = getCardShapePoints('rectangle', 100, 60, 10, 2);
|
||||
|
||||
// 4 个角,每个角有 1 个起点 + 2 个弧线点 = 3 个点
|
||||
expect(points.length).toBe(12);
|
||||
|
||||
// 验证第一个点在左边,y 坐标为圆角半径
|
||||
expect(pointCloseTo(points[0], [0, 10])).toBe(true);
|
||||
});
|
||||
|
||||
test('圆角矩形应该是对称的', () => {
|
||||
const width = 100;
|
||||
const height = 60;
|
||||
const cornerRadius = 10;
|
||||
const segmentsPerCorner = 4;
|
||||
|
||||
const points = getCardShapePoints('rectangle', width, height, cornerRadius, segmentsPerCorner);
|
||||
|
||||
// 验证左右对称
|
||||
const leftPoints = points.filter(([x]) => x < width / 2);
|
||||
const rightPoints = points.filter(([x]) => x > width / 2);
|
||||
|
||||
expect(leftPoints.length).toBe(rightPoints.length);
|
||||
|
||||
// 验证上下对称
|
||||
const topPoints = points.filter(([, y]) => y < height / 2);
|
||||
const bottomPoints = points.filter(([, y]) => y > height / 2);
|
||||
|
||||
expect(topPoints.length).toBe(bottomPoints.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('三角形', () => {
|
||||
test('无圆角三角形', () => {
|
||||
const points = getCardShapePoints('triangle', 100, 60, 0);
|
||||
expect(points.length).toBe(3);
|
||||
});
|
||||
|
||||
test('带圆角三角形 (segmentsPerCorner=2)', () => {
|
||||
const points = getCardShapePoints('triangle', 100, 60, 10, 2);
|
||||
|
||||
// 3 个角,每个角有 1 个起点 + 2 个弧线点 = 3 个点
|
||||
expect(points.length).toBe(9);
|
||||
});
|
||||
});
|
||||
|
||||
describe('六边形', () => {
|
||||
test('无圆角六边形', () => {
|
||||
const points = getCardShapePoints('hexagon', 100, 60, 0);
|
||||
expect(points.length).toBe(6);
|
||||
});
|
||||
|
||||
test('带圆角六边形 (segmentsPerCorner=2)', () => {
|
||||
const points = getCardShapePoints('hexagon', 100, 60, 10, 2);
|
||||
|
||||
// 6 个角,每个角有 1 个起点 + 2 个弧线点 = 3 个点
|
||||
expect(points.length).toBe(18);
|
||||
});
|
||||
});
|
||||
|
||||
describe('圆形', () => {
|
||||
test('圆形轮廓', () => {
|
||||
const points = getCardShapePoints('circle', 100, 60, 0);
|
||||
|
||||
// 圆形固定生成 36 个点
|
||||
expect(points.length).toBe(36);
|
||||
|
||||
// 验证所有点到中心的距离接近半径
|
||||
const centerX = 50;
|
||||
const centerY = 30;
|
||||
const radius = 30; // min(100, 60) / 2
|
||||
|
||||
for (const [x, y] of points) {
|
||||
const dist = distance([x, y], [centerX, centerY]);
|
||||
expect(Math.abs(dist - radius)).toBeLessThan(0.1);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import type { CardShape, ContourPoint, ContourBounds } from './types';
|
||||
import {getRoundedPolygonPoints} from "./rounded";
|
||||
|
||||
// 重新导出类型以兼容旧导入路径
|
||||
export type { CardShape, ContourPoint, ContourBounds };
|
||||
|
|
@ -52,135 +53,22 @@ export function getInscribedHexagonPoints(
|
|||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// 正六边形六个顶点(平顶,从右上角开始顺时针)
|
||||
// 角度:-60°, 0°, 60°, 120°, 180°, 240° (顺时针)
|
||||
// 正六边形六个顶点(平顶,从左上角开始顺时针)
|
||||
// 角度:210°, 270°, 330°, 30°, 90°, 150° (数学坐标系,Y向上)
|
||||
// 在屏幕坐标系 (Y向下),我们直接按顺时针计算
|
||||
const points: [number, number][] = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const angle = (-Math.PI / 3) + (i * Math.PI / 3); // 从 -60° 开始,顺时针
|
||||
// 从 -120度开始,每 60 度一个点,实现平顶且顺时针
|
||||
const angle = (-2 * Math.PI / 3) + (i * Math.PI / 3);
|
||||
points.push([
|
||||
centerX + radius * Math.cos(angle),
|
||||
centerY - radius * Math.sin(angle) // Y 向下为正,所以减去 sin
|
||||
centerY + radius * Math.sin(angle)
|
||||
]);
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 为正多边形添加圆角
|
||||
* @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 n = vertices.length;
|
||||
|
||||
// 计算最大允许的圆角半径(不超过边长的一半)
|
||||
let maxRadius = Infinity;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const [x1, y1] = vertices[i];
|
||||
const [x2, y2] = vertices[(i + 1) % n];
|
||||
const edgeLength = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
|
||||
maxRadius = Math.min(maxRadius, edgeLength / 2);
|
||||
}
|
||||
|
||||
const r = Math.min(cornerRadius, maxRadius);
|
||||
|
||||
if (r <= 0) {
|
||||
return vertices;
|
||||
}
|
||||
|
||||
const points: [number, number][] = [];
|
||||
|
||||
for (let i = 0; i < n; i++) {
|
||||
const currVertex = vertices[i];
|
||||
const nextVertex = vertices[(i + 1) % n];
|
||||
|
||||
// 计算当前边的向量
|
||||
const edgeDx = nextVertex[0] - currVertex[0];
|
||||
const edgeDy = nextVertex[1] - currVertex[1];
|
||||
const edgeLen = Math.sqrt(edgeDx ** 2 + edgeDy ** 2);
|
||||
const edgeUx = edgeDx / edgeLen;
|
||||
const edgeUy = edgeDy / edgeLen;
|
||||
|
||||
// 计算当前边上的圆角终点(距离顶点 r 的位置)
|
||||
const endPx = currVertex[0] + edgeUx * r;
|
||||
const endPy = currVertex[1] + edgeUy * r;
|
||||
|
||||
// 计算上一条边的向量
|
||||
const prevVertex = vertices[(i - 1 + n) % n];
|
||||
const prevEdgeDx = currVertex[0] - prevVertex[0];
|
||||
const prevEdgeDy = currVertex[1] - prevVertex[1];
|
||||
const prevEdgeLen = Math.sqrt(prevEdgeDx ** 2 + prevEdgeDy ** 2);
|
||||
const prevEdgeUx = prevEdgeDx / prevEdgeLen;
|
||||
const prevEdgeUy = prevEdgeDy / prevEdgeLen;
|
||||
|
||||
// 计算上一条边上的圆角起点(距离顶点 r 的位置)
|
||||
const startPx = currVertex[0] - prevEdgeUx * r;
|
||||
const startPy = currVertex[1] - prevEdgeUy * r;
|
||||
|
||||
// 计算角平分线方向
|
||||
const bisectorX = prevEdgeUx + edgeUx;
|
||||
const bisectorY = prevEdgeUy + edgeUy;
|
||||
const bisectorLen = Math.sqrt(bisectorX ** 2 + bisectorY ** 2);
|
||||
|
||||
// 如果 bisectorLen 接近 0,说明是 180 度角(直线),跳过圆角
|
||||
if (bisectorLen < 1e-6) {
|
||||
points.push([endPx, endPy]);
|
||||
continue;
|
||||
}
|
||||
|
||||
const normalX = bisectorX / bisectorLen;
|
||||
const normalY = bisectorY / bisectorLen;
|
||||
|
||||
// 计算叉积判断方向(顺时针/逆时针)
|
||||
const crossProduct = prevEdgeUx * edgeUy - prevEdgeUy * edgeUx;
|
||||
const direction = crossProduct >= 0 ? -1 : 1; // -1 表示内角,1 表示外角
|
||||
|
||||
// 计算圆角中心
|
||||
const angle = Math.acos(Math.max(-1, Math.min(1, prevEdgeUx * edgeUx + prevEdgeUy * edgeUy)));
|
||||
const tangentDistance = r / Math.tan(angle / 2);
|
||||
const centerX = currVertex[0] + direction * normalX * tangentDistance;
|
||||
const centerY = currVertex[1] + direction * normalY * tangentDistance;
|
||||
|
||||
// 计算起始角度和结束角度
|
||||
const startAngle = Math.atan2(startPy - centerY, startPx - centerX);
|
||||
const endAngle = Math.atan2(endPy - centerY, endPx - centerX);
|
||||
|
||||
// 添加圆角起点
|
||||
points.push([startPx, startPy]);
|
||||
|
||||
// 生成圆角弧线点
|
||||
let angleDiff = endAngle - startAngle;
|
||||
// 根据方向调整角度差
|
||||
if (direction === -1) {
|
||||
// 顺时针(内角)
|
||||
if (angleDiff < 0) angleDiff += 2 * Math.PI;
|
||||
} else {
|
||||
// 逆时针(外角)
|
||||
if (angleDiff > 0) angleDiff -= 2 * Math.PI;
|
||||
}
|
||||
|
||||
// 生成弧线点(不包括起点,因为已经添加了)
|
||||
for (let j = 1; j <= segmentsPerCorner; j++) {
|
||||
const t = j / segmentsPerCorner;
|
||||
const arcAngle = startAngle + angleDiff * t;
|
||||
points.push([
|
||||
centerX + r * Math.cos(arcAngle),
|
||||
centerY + r * Math.sin(arcAngle)
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return points;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据形状生成卡片轮廓点(单位:mm,相对于卡片左下角)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
import { getRoundedPolygonPoints, getTangentCircleCenter, getProjectedPoint } from './rounded';
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
declare const describe: any;
|
||||
declare const test: any;
|
||||
declare const expect: any;
|
||||
|
||||
describe('getProjectedPoint', () => {
|
||||
test('should project point onto line segment', () => {
|
||||
// 点 (2, 2) 投影到线段 (0, 0) -> (4, 0)
|
||||
const result = getProjectedPoint([2, 2], [0, 0], [4, 0]);
|
||||
expect(result).toEqual([2, 0]);
|
||||
});
|
||||
|
||||
test('should project point onto vertical line segment', () => {
|
||||
const result = getProjectedPoint([3, 2], [0, 0], [0, 4]);
|
||||
expect(result).toEqual([0, 2]);
|
||||
});
|
||||
|
||||
test('should handle point already on line', () => {
|
||||
const result = getProjectedPoint([2, 0], [0, 0], [4, 0]);
|
||||
expect(result).toEqual([2, 0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getTangentCircleCenter', () => {
|
||||
test('should find center for 90 degree corner', () => {
|
||||
// 直角:a(0,1), b(0,0), c(1,0)
|
||||
const center = getTangentCircleCenter([0, 1], [0, 0], [1, 0], 1);
|
||||
// 圆心应该在 (1, 1)
|
||||
expect(center[0]).toBeCloseTo(1, 5);
|
||||
expect(center[1]).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
test('should find center for equilateral triangle corner', () => {
|
||||
// 等边三角形的一个角
|
||||
const center = getTangentCircleCenter([0, 1], [0, 0], [Math.sqrt(3) / 2, -0.5], 0.5);
|
||||
// 验证圆心到两条边的距离都是半径
|
||||
expect(center).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRoundedPolygonPoints', () => {
|
||||
test('should return empty array for empty input', () => {
|
||||
const result = getRoundedPolygonPoints([], 1);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('should return same points for less than 3 vertices', () => {
|
||||
const result = getRoundedPolygonPoints([[0, 0], [1, 0]], 1);
|
||||
expect(result).toEqual([[0, 0], [1, 0]]);
|
||||
});
|
||||
|
||||
test('should round a square', () => {
|
||||
// 单位正方形
|
||||
const square: [number, number][] = [[0, 0], [2, 0], [2, 2], [0, 2]];
|
||||
const result = getRoundedPolygonPoints(square, 0.5, 4);
|
||||
|
||||
// 结果应该有 4 个角 * (4+1) 段 = 20 个点
|
||||
expect(result.length).toBe(20);
|
||||
|
||||
// 验证所有点都是有效的坐标
|
||||
result.forEach(point => {
|
||||
expect(typeof point[0]).toBe('number');
|
||||
expect(typeof point[1]).toBe('number');
|
||||
});
|
||||
});
|
||||
|
||||
test('should round an equilateral triangle', () => {
|
||||
// 等边三角形
|
||||
const triangle: [number, number][] = [
|
||||
[0, 1],
|
||||
[Math.sqrt(3) / 2, -0.5],
|
||||
[-Math.sqrt(3) / 2, -0.5]
|
||||
];
|
||||
const result = getRoundedPolygonPoints(triangle, 0.2, 4);
|
||||
|
||||
expect(result.length).toBe(15); // 3 个角 * (4+1) 段
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/**
|
||||
* 为正多边形添加圆角
|
||||
* 将每个角转换为 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
|
||||
];
|
||||
}
|
||||
Loading…
Reference in New Issue