import type { CardShape, ContourPoint, ContourBounds } from './types'; // 重新导出类型以兼容旧导入路径 export type { CardShape, ContourPoint, ContourBounds }; /** * 生成内接正三角形的轮廓点(无圆角版本) * @param width 外框宽度 * @param height 外框高度 * @returns 正三角形轮廓点(相对于外框左下角,顺时针) */ export function getInscribedTrianglePoints( width: number, height: number ): [number, number][] { // 以短边为基准计算内接正三角形的边长 const minDim = Math.min(width, height); // 正三角形的高 = 边长 * sqrt(3) / 2 const triangleHeight = minDim * Math.sqrt(3) / 2; const sideLength = minDim; // 计算居中偏移 const offsetX = (width - sideLength) / 2; const offsetY = (height - triangleHeight) / 2; // 正三角形三个顶点(底边在下,顶点在上,顺时针:左上→右上→下) const points: [number, number][] = [ [offsetX, offsetY + triangleHeight], // 左下顶点 [offsetX + sideLength, offsetY + triangleHeight], // 右下顶点 [offsetX + sideLength / 2, offsetY] // 顶部顶点 ]; return points; } /** * 生成内接正六边形的轮廓点(无圆角版本) * @param width 外框宽度 * @param height 外框高度 * @returns 正六边形轮廓点(相对于外框左下角,顺时针) */ export function getInscribedHexagonPoints( width: number, height: number ): [number, number][] { // 以短边为基准计算内接正六边形的半径 const minDim = Math.min(width, height); const radius = minDim / 2; // 中心点 const centerX = width / 2; const centerY = height / 2; // 正六边形六个顶点(平顶,从右上角开始顺时针) // 角度:-60°, 0°, 60°, 120°, 180°, 240° (顺时针) const points: [number, number][] = []; for (let i = 0; i < 6; i++) { const angle = (-Math.PI / 3) + (i * Math.PI / 3); // 从 -60° 开始,顺时针 points.push([ centerX + radius * Math.cos(angle), centerY - radius * Math.sin(angle) // Y 向下为正,所以减去 sin ]); } 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,相对于卡片左下角) */ export function getCardShapePoints( shape: CardShape, width: number, height: number, cornerRadius: number = 0, segmentsPerCorner: number = 4 ): [number, number][] { // 处理带圆角的情况 - 统一使用 getRoundedPolygonPoints if (cornerRadius > 0) { if (shape === 'rectangle') { const vertices: [number, number][] = [ [0, 0], [width, 0], [width, height], [0, height] ]; return getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner); } if (shape === 'triangle') { const vertices = getInscribedTrianglePoints(width, height); return getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner); } if (shape === 'hexagon') { const vertices = getInscribedHexagonPoints(width, height); return getRoundedPolygonPoints(vertices, cornerRadius, segmentsPerCorner); } } const points: [number, number][] = []; switch (shape) { case 'circle': { const radius = Math.min(width, height) / 2; const centerX = width / 2; const centerY = height / 2; for (let i = 0; i < 36; i++) { const angle = (i / 36) * Math.PI * 2; points.push([ centerX + radius * Math.cos(angle), centerY + radius * Math.sin(angle) ]); } break; } case 'triangle': { return getInscribedTrianglePoints(width, height); } case 'hexagon': { return getInscribedHexagonPoints(width, height); } case 'rectangle': default: { points.push([0, 0]); points.push([width, 0]); points.push([width, height]); points.push([0, height]); break; } } return points; } /** * 计算多边形的中心点 */ export function calculateCenter(points: [number, number][]): { x: number; y: number } { let sumX = 0; let sumY = 0; for (const [x, y] of points) { sumX += x; sumY += y; } return { x: sumX / points.length, y: sumY / points.length }; } /** * 计算轮廓的边界框 */ export function calculateBounds(points: [number, number][]): ContourBounds { if (points.length === 0) { return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const [x, y] of points) { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); } return { minX, minY, maxX, maxY }; } /** * 根据进度计算点在路径上的位置 */ export function getPointOnPath(points: [number, number][], progress: number): [number, number] { if (points.length === 0) return [0, 0]; if (points.length === 1) return points[0]; const totalSegments = points.length; const scaledProgress = progress * totalSegments; const segmentIndex = Math.floor(scaledProgress); const segmentProgress = scaledProgress - segmentIndex; const currentIndex = Math.min(segmentIndex, points.length - 1); const nextIndex = (currentIndex + 1) % points.length; const [x1, y1] = points[currentIndex]; const [x2, y2] = points[nextIndex]; return [ x1 + (x2 - x1) * segmentProgress, y1 + (y2 - y1) * segmentProgress ]; } /** * 将轮廓点转换为 SVG path 命令 * @param points 轮廓点数组 * @param closed 是否闭合路径 */ export function contourToSvgPath(points: [number, number][], closed = true): string { if (points.length === 0) return ''; const [startX, startY] = points[0]; let d = `M ${startX} ${startY}`; for (let i = 1; i < points.length; i++) { const [x, y] = points[i]; d += ` L ${x} ${y}`; } if (closed) { d += ' Z'; } return d; } /** * 平移轮廓点(添加偏移量) */ export function translateContour( points: [number, number][], offsetX: number, offsetY: number ): [number, number][] { return points.map(([x, y]) => [x + offsetX, y + offsetY] as [number, number]); } /** * 翻转轮廓(用于 SVG 坐标转换,Y 轴翻转) * @param points 轮廓点 * @param height 画布高度 */ export function flipContourY( points: [number, number][], height: number ): [number, number][] { return points.map(([x, y]) => [x, height - y] as [number, number]); }