From 0ec129c2bee1316b567ff08f2360dc419fe55164 Mon Sep 17 00:00:00 2001 From: hypercross Date: Sun, 15 Mar 2026 11:17:08 +0800 Subject: [PATCH] feat: hex/trig gen --- src/plotcutter/contour.ts | 246 ++++++++++++++++++++++++++++---------- 1 file changed, 184 insertions(+), 62 deletions(-) diff --git a/src/plotcutter/contour.ts b/src/plotcutter/contour.ts index 0e33939..e821b33 100644 --- a/src/plotcutter/contour.ts +++ b/src/plotcutter/contour.ts @@ -3,65 +3,180 @@ import type { CardShape, ContourPoint, ContourBounds } from './types'; // 重新导出类型以兼容旧导入路径 export type { CardShape, ContourPoint, ContourBounds }; + /** - * 生成带圆角的矩形轮廓点 - * @param width 矩形宽度 - * @param height 矩形高度 - * @param cornerRadius 圆角半径(mm) - * @param segmentsPerCorner 每个圆角的分段数 + * 生成内接正三角形的轮廓点(无圆角版本) + * @param width 外框宽度 + * @param height 外框高度 + * @returns 正三角形轮廓点(相对于外框左下角,顺时针) */ -export function getRoundedRectPoints( +export function getInscribedTrianglePoints( width: number, - height: 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][] { - const points: [number, number][] = []; - const r = Math.min(cornerRadius, width / 2, height / 2); + 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) { - // 无圆角,返回普通矩形 - points.push([0, 0]); - points.push([width, 0]); - points.push([width, height]); - points.push([0, height]); - return points; + return vertices; } - // 左上角圆角(从顶部开始,顺时针) - for (let i = 0; i <= segmentsPerCorner; i++) { - const angle = (Math.PI / 2) * (i / segmentsPerCorner) - Math.PI; - points.push([ - r + r * Math.cos(angle), - r + r * Math.sin(angle) - ]); - } + const points: [number, number][] = []; - // 右上角圆角 - for (let i = 0; i <= segmentsPerCorner; i++) { - const angle = (Math.PI / 2) * (i / segmentsPerCorner) - Math.PI/2; - points.push([ - width - r + r * Math.cos(angle), - r + r * Math.sin(angle) - ]); - } + for (let i = 0; i < n; i++) { + const currVertex = vertices[i]; + const nextVertex = vertices[(i + 1) % n]; - // 右下角圆角 - for (let i = 0; i <= segmentsPerCorner; i++) { - const angle = (Math.PI / 2) * (i / segmentsPerCorner); - points.push([ - width - r + r * Math.cos(angle), - height - r + r * Math.sin(angle) - ]); - } + // 计算当前边的向量 + 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; - // 左下角圆角 - for (let i = 0; i <= segmentsPerCorner; i++) { - const angle = (Math.PI / 2) * (i / segmentsPerCorner) + Math.PI/2; - points.push([ - r + r * Math.cos(angle), - height - r + r * Math.sin(angle) - ]); + // 计算当前边上的圆角终点(距离顶点 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; @@ -74,10 +189,28 @@ export function getCardShapePoints( shape: CardShape, width: number, height: number, - cornerRadius: number = 0 + cornerRadius: number = 0, + segmentsPerCorner: number = 4 ): [number, number][] { - if (shape === 'rectangle' && cornerRadius > 0) { - return getRoundedRectPoints(width, height, cornerRadius); + // 处理带圆角的情况 - 统一使用 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][] = []; @@ -97,21 +230,10 @@ export function getCardShapePoints( break; } case 'triangle': { - points.push([width / 2, 0]); - points.push([0, height]); - points.push([width, height]); - break; + return getInscribedTrianglePoints(width, height); } case 'hexagon': { - const halfW = width / 2; - const quarterH = height / 4; - points.push([halfW, 0]); - points.push([width, quarterH]); - points.push([width, height - quarterH]); - points.push([halfW, height]); - points.push([0, height - quarterH]); - points.push([0, quarterH]); - break; + return getInscribedHexagonPoints(width, height); } case 'rectangle': default: {