import { SVGPathData } from 'svg-pathdata' import arcToBezier from 'svg-arc-to-cubic-bezier' const typeMap = { 1: 'Z', 2: 'M', 4: 'H', 8: 'V', 16: 'L', 32: 'C', 64: 'S', 128: 'Q', 256: 'T', 512: 'A', } /** * 简单解析SVG路径 * @param d SVG path d属性 */ export const parseSvgPath = (d: string) => { const pathData = new SVGPathData(d) const ret = pathData.commands.map(item => { return { ...item, type: typeMap[item.type] } }) return ret } export type SvgPath = ReturnType /** * 解析SVG路径,并将圆弧(A)类型的路径转为三次贝塞尔(C)类型的路径 * @param d SVG path d属性 */ export const toPoints = (d: string) => { const pathData = new SVGPathData(d) const points = [] for (const item of pathData.commands) { const type = typeMap[item.type] if (item.type === 2 || item.type === 16) { points.push({ x: item.x, y: item.y, relative: item.relative, type, }) } if (item.type === 32) { points.push({ x: item.x, y: item.y, curve: { type: 'cubic', x1: item.x1, y1: item.y1, x2: item.x2, y2: item.y2, }, relative: item.relative, type, }) } else if (item.type === 128) { points.push({ x: item.x, y: item.y, curve: { type: 'quadratic', x1: item.x1, y1: item.y1, }, relative: item.relative, type, }) } else if (item.type === 512) { const lastPoint = points[points.length - 1] if (!['M', 'L', 'Q', 'C'].includes(lastPoint.type)) continue const cubicBezierPoints = arcToBezier({ px: lastPoint.x as number, py: lastPoint.y as number, cx: item.x, cy: item.y, rx: item.rX, ry: item.rY, xAxisRotation: item.xRot, largeArcFlag: item.lArcFlag, sweepFlag: item.sweepFlag, }) for (const cbPoint of cubicBezierPoints) { points.push({ x: cbPoint.x, y: cbPoint.y, curve: { type: 'cubic', x1: cbPoint.x1, y1: cbPoint.y1, x2: cbPoint.x2, y2: cbPoint.y2, }, relative: false, type: 'C', }) } } else if (item.type === 1) { points.push({ close: true, type }) } else continue } return points } /* export const getSvgPathRange = (path: string) => { try { const pathData = new SVGPathData(path) const xList = [] const yList = [] for (const item of pathData.commands) { const x = ('x' in item) ? item.x : 0 const y = ('y' in item) ? item.y : 0 xList.push(x) yList.push(y) } return { minX: Math.min(...xList), minY: Math.min(...yList), maxX: Math.max(...xList), maxY: Math.max(...yList), } } catch { return { minX: 0, minY: 0, maxX: 0, maxY: 0, } } } */ // 辅助:采样三次贝塞尔曲线 function sampleCubicBezier( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, x3: number, y3: number, samples = 20 ): { x: number; y: number }[] { const points = []; for (let i = 0; i <= samples; i++) { const t = i / samples; const mt = 1 - t; const x = mt ** 3 * x0 + 3 * mt ** 2 * t * x1 + 3 * mt * t ** 2 * x2 + t ** 3 * x3; const y = mt ** 3 * y0 + 3 * mt ** 2 * t * y1 + 3 * mt * t ** 2 * y2 + t ** 3 * y3; points.push({ x, y }); } return points; } // 辅助:采样二次贝塞尔曲线 function sampleQuadraticBezier( x0: number, y0: number, x1: number, y1: number, x2: number, y2: number, samples = 20 ): { x: number; y: number }[] { const points = []; for (let i = 0; i <= samples; i++) { const t = i / samples; const mt = 1 - t; const x = mt ** 2 * x0 + 2 * mt * t * x1 + t ** 2 * x2; const y = mt ** 2 * y0 + 2 * mt * t * y1 + t ** 2 * y2; points.push({ x, y }); } return points; } // 辅助:采样椭圆弧(基于 SVG 规范参数方程) function sampleArc( x0: number, y0: number, rx: number, ry: number, xAxisRotation: number, largeArcFlag: boolean, sweepFlag: boolean, x: number, y: number, samples = 30 ): { x: number; y: number }[] { const phi = (xAxisRotation * Math.PI) / 180; const cosPhi = Math.cos(phi); const sinPhi = Math.sin(phi); const dx = (x0 - x) / 2; const dy = (y0 - y) / 2; const x1p = cosPhi * dx + sinPhi * dy; const y1p = -sinPhi * dx + cosPhi * dy; let rxx = Math.abs(rx); let ryy = Math.abs(ry); const lambda = (x1p * x1p) / (rxx * rxx) + (y1p * y1p) / (ryy * ryy); if (lambda > 1) { rxx *= Math.sqrt(lambda); ryy *= Math.sqrt(lambda); } const sq = Math.sqrt( (rxx * rxx * (ryy * ryy) - rxx * rxx * (y1p * y1p) - ryy * ryy * (x1p * x1p)) / (rxx * rxx * (y1p * y1p) + ryy * ryy * (x1p * x1p)) ); const sign = largeArcFlag === sweepFlag ? -1 : 1; const cxp = sign * sq * ((rxx * y1p) / ryy); const cyp = sign * sq * ((-ryy * x1p) / rxx); const cx = cosPhi * cxp - sinPhi * cyp + (x0 + x) / 2; const cy = sinPhi * cxp + cosPhi * cyp + (y0 + y) / 2; const vectorAngle = (ux: number, uy: number, vx: number, vy: number) => { const dot = ux * vx + uy * vy; const len = Math.sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy)); let ang = Math.acos(Math.max(-1, Math.min(1, dot / len))); const cross = ux * vy - uy * vx; return cross < 0 ? -ang : ang; }; const ux = (x1p - cxp) / rxx; const uy = (y1p - cyp) / ryy; const vx = (-x1p - cxp) / rxx; const vy = (-y1p - cyp) / ryy; let startAngle = vectorAngle(1, 0, ux, uy); let deltaAngle = vectorAngle(ux, uy, vx, vy); if (!sweepFlag && deltaAngle > 0) { deltaAngle -= 2 * Math.PI; } else if (sweepFlag && deltaAngle < 0) { deltaAngle += 2 * Math.PI; } const points: { x: number; y: number }[] = []; for (let i = 0; i <= samples; i++) { const t = i / samples; const angle = startAngle + t * deltaAngle; const xp = rxx * Math.cos(angle); const yp = ryy * Math.sin(angle); const px = cosPhi * xp - sinPhi * yp + cx; const py = sinPhi * xp + cosPhi * yp + cy; points.push({ x: px, y: py }); } return points; } // 主函数 export const getSvgPathRange = (path: string) => { try { const pathData = new SVGPathData(path); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; let curX = 0, curY = 0; // 当前点(上一命令终点) let startX = 0, startY = 0; // 子路径起点(用于 Z) const updateBounds = (x: number, y: number) => { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); }; const processPoints = (points: { x: number; y: number }[]) => { points.forEach(p => updateBounds(p.x, p.y)); }; for (const cmd of pathData.commands) { switch (cmd.type) { case SVGPathData.MOVE_TO: curX = cmd.x; curY = cmd.y; startX = curX; startY = curY; updateBounds(curX, curY); break; case SVGPathData.LINE_TO: curX = cmd.x; curY = cmd.y; updateBounds(curX, curY); break; case SVGPathData.HORIZ_LINE_TO: curX = cmd.x; updateBounds(curX, curY); break; case SVGPathData.VERT_LINE_TO: curY = cmd.y; updateBounds(curX, curY); break; case SVGPathData.CURVE_TO: { const points = sampleCubicBezier( curX, curY, cmd.x1, cmd.y1, cmd.x2, cmd.y2, cmd.x, cmd.y ); processPoints(points); curX = cmd.x; curY = cmd.y; } break; case SVGPathData.SMOOTH_CURVE_TO: // 为简化,此处省略反射控制点的精确计算,可根据需要补充 // 可直接使用采样近似或添加反射逻辑 break; case SVGPathData.QUAD_TO: { const points = sampleQuadraticBezier( curX, curY, cmd.x1, cmd.y1, cmd.x, cmd.y ); processPoints(points); curX = cmd.x; curY = cmd.y; } break; case SVGPathData.SMOOTH_QUAD_TO: // 省略 break; case SVGPathData.ARC: { const points = sampleArc( curX, curY, cmd.rX, cmd.rY, cmd.xRot, cmd.lArcFlag, cmd.sweepFlag, cmd.x, cmd.y ); processPoints(points); curX = cmd.x; curY = cmd.y; } break; case SVGPathData.CLOSE_PATH: curX = startX; curY = startY; updateBounds(curX, curY); break; } } if (minX === Infinity) { return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; } return { minX, minY, maxX, maxY }; } catch { return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; } }; export type SvgPoints = ReturnType