|
|
@@ -114,7 +114,7 @@ export const toPoints = (d: string) => {
|
|
|
}
|
|
|
return points
|
|
|
}
|
|
|
-
|
|
|
+/*
|
|
|
export const getSvgPathRange = (path: string) => {
|
|
|
try {
|
|
|
const pathData = new SVGPathData(path)
|
|
|
@@ -143,4 +143,222 @@ export const getSvgPathRange = (path: string) => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+*/
|
|
|
+
|
|
|
+// 辅助:采样三次贝塞尔曲线
|
|
|
+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<typeof toPoints>
|