jack 1 week ago
parent
commit
9ea166247f
4 changed files with 228 additions and 31 deletions
  1. 8 26
      package-lock.json
  2. 1 1
      package.json
  3. 0 3
      src/hooks/useImport.ts
  4. 219 1
      src/utils/svgPathParser.ts

+ 8 - 26
package-lock.json

@@ -43,7 +43,7 @@
         "qs": "^6.14.0",
         "rtf.js": "^3.0.9",
         "svg-arc-to-cubic-bezier": "^3.2.0",
-        "svg-pathdata": "^7.1.0",
+        "svg-pathdata": "^7.2.0",
         "tinycolor2": "^1.6.0",
         "tippy.js": "^6.3.7",
         "utif": "^3.1.0",
@@ -5744,12 +5744,10 @@
       "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
     },
     "node_modules/svg-pathdata": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-7.1.0.tgz",
-      "integrity": "sha512-wrvKHXZSYZyODOj5E1l1bMTIo8sR7YCH0E4SA8IgLgMsZq4RypslpYvNSsrdg4ThD6du2KWPyVeKinkqUelGhg==",
-      "dependencies": {
-        "yerror": "^8.0.0"
-      },
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-7.2.0.tgz",
+      "integrity": "sha512-qd+AxqMpfRrRQaWb2SrNFvn69cvl6piqY8TxhYl2Li1g4/LO5F9NJb5wI4vNwRryqgSgD43gYKLm/w3ag1bKvQ==",
+      "license": "MIT",
       "engines": {
         "node": ">=20.11.1"
       }
@@ -6344,14 +6342,6 @@
         "node": ">=12"
       }
     },
-    "node_modules/yerror": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmmirror.com/yerror/-/yerror-8.0.0.tgz",
-      "integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g==",
-      "engines": {
-        "node": ">=18.16.0"
-      }
-    },
     "node_modules/yjs": {
       "version": "13.6.27",
       "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",
@@ -10595,12 +10585,9 @@
       "integrity": "sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g=="
     },
     "svg-pathdata": {
-      "version": "7.1.0",
-      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-7.1.0.tgz",
-      "integrity": "sha512-wrvKHXZSYZyODOj5E1l1bMTIo8sR7YCH0E4SA8IgLgMsZq4RypslpYvNSsrdg4ThD6du2KWPyVeKinkqUelGhg==",
-      "requires": {
-        "yerror": "^8.0.0"
-      }
+      "version": "7.2.0",
+      "resolved": "https://registry.npmmirror.com/svg-pathdata/-/svg-pathdata-7.2.0.tgz",
+      "integrity": "sha512-qd+AxqMpfRrRQaWb2SrNFvn69cvl6piqY8TxhYl2Li1g4/LO5F9NJb5wI4vNwRryqgSgD43gYKLm/w3ag1bKvQ=="
     },
     "text-extensions": {
       "version": "2.4.0",
@@ -11010,11 +10997,6 @@
       "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
       "dev": true
     },
-    "yerror": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmmirror.com/yerror/-/yerror-8.0.0.tgz",
-      "integrity": "sha512-FemWD5/UqNm8ffj8oZIbjWXIF2KE0mZssggYpdaQkWDDgXBQ/35PNIxEuz6/YLn9o0kOxDBNJe8x8k9ljD7k/g=="
-    },
     "yjs": {
       "version": "13.6.27",
       "resolved": "https://registry.npmjs.org/yjs/-/yjs-13.6.27.tgz",

+ 1 - 1
package.json

@@ -50,7 +50,7 @@
     "qs": "^6.14.0",
     "rtf.js": "^3.0.9",
     "svg-arc-to-cubic-bezier": "^3.2.0",
-    "svg-pathdata": "^7.1.0",
+    "svg-pathdata": "^7.2.0",
     "tinycolor2": "^1.6.0",
     "tippy.js": "^6.3.7",
     "utif": "^3.1.0",

+ 0 - 3
src/hooks/useImport.ts

@@ -1947,12 +1947,9 @@ export default () => {
                 }
 
                 if (shape) {
-                  //element.path = shape.path
-                  //element.viewBox = shape.viewBox
                   const { maxX, maxY } = getSvgPathRange(el.path)
                   element.path = el.path
                   element.viewBox = poriginWidth ? [maxX, maxY] : [originWidth, originHeight]
-
                   if (shape.pathFormula) {
                     element.pathFormula = shape.pathFormula
                     element.viewBox = [el.width, el.height]

+ 219 - 1
src/utils/svgPathParser.ts

@@ -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>