@faicad/faijs 0.32.2 → 0.33.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/dist/api/api-namespace.js +3 -3
  2. package/dist/api/api-namespace.js.map +1 -1
  3. package/dist/api/assembly/index.d.ts +1 -0
  4. package/dist/api/assembly/index.d.ts.map +1 -1
  5. package/dist/api/assembly/index.js +1 -0
  6. package/dist/api/assembly/index.js.map +1 -1
  7. package/dist/api/assembly/lower.d.ts.map +1 -1
  8. package/dist/api/assembly/lower.js +7 -0
  9. package/dist/api/assembly/lower.js.map +1 -1
  10. package/dist/api/assembly/normalize.d.ts.map +1 -1
  11. package/dist/api/assembly/normalize.js +2 -1
  12. package/dist/api/assembly/normalize.js.map +1 -1
  13. package/dist/api/assembly/solve-tree.d.ts +46 -0
  14. package/dist/api/assembly/solve-tree.d.ts.map +1 -0
  15. package/dist/api/assembly/solve-tree.js +135 -0
  16. package/dist/api/assembly/solve-tree.js.map +1 -0
  17. package/dist/api/assembly/solve.d.ts +7 -0
  18. package/dist/api/assembly/solve.d.ts.map +1 -1
  19. package/dist/api/assembly/solve.js.map +1 -1
  20. package/dist/api/assembly/solvers/assembly-tree.d.ts +4 -0
  21. package/dist/api/assembly/solvers/assembly-tree.d.ts.map +1 -1
  22. package/dist/api/assembly/solvers/assembly-tree.js +1 -0
  23. package/dist/api/assembly/solvers/assembly-tree.js.map +1 -1
  24. package/dist/api/assembly/solvers/global-solver.d.ts.map +1 -1
  25. package/dist/api/assembly/solvers/global-solver.js +210 -3
  26. package/dist/api/assembly/solvers/global-solver.js.map +1 -1
  27. package/dist/api/assembly/solvers/types.d.ts +8 -0
  28. package/dist/api/assembly/solvers/types.d.ts.map +1 -1
  29. package/dist/api/assembly/solvers/types.js.map +1 -1
  30. package/dist/api/assembly/types.d.ts +32 -1
  31. package/dist/api/assembly/types.d.ts.map +1 -1
  32. package/dist/api/assembly/types.js.map +1 -1
  33. package/dist/api/assembly/validate.d.ts +2 -2
  34. package/dist/api/assembly/validate.d.ts.map +1 -1
  35. package/dist/api/assembly/validate.js +24 -2
  36. package/dist/api/assembly/validate.js.map +1 -1
  37. package/dist/api/cadquery-selectors/edge.js +1 -1
  38. package/dist/api/cadquery-selectors/edge.js.map +1 -1
  39. package/dist/api/cadquery-selectors/entity.d.ts +29 -20
  40. package/dist/api/cadquery-selectors/entity.d.ts.map +1 -1
  41. package/dist/api/cadquery-selectors/entity.js +26 -20
  42. package/dist/api/cadquery-selectors/entity.js.map +1 -1
  43. package/dist/api/cadquery-selectors/face.d.ts +4 -2
  44. package/dist/api/cadquery-selectors/face.d.ts.map +1 -1
  45. package/dist/api/cadquery-selectors/face.js +8 -7
  46. package/dist/api/cadquery-selectors/face.js.map +1 -1
  47. package/dist/api/cadquery-selectors/resolve.d.ts +7 -6
  48. package/dist/api/cadquery-selectors/resolve.d.ts.map +1 -1
  49. package/dist/api/cadquery-selectors/resolve.js +13 -11
  50. package/dist/api/cadquery-selectors/resolve.js.map +1 -1
  51. package/dist/api/chamfer.js +1 -1
  52. package/dist/api/chamfer.js.map +1 -1
  53. package/dist/api/draft.js +1 -1
  54. package/dist/api/draft.js.map +1 -1
  55. package/dist/api/edge-ref.js +2 -2
  56. package/dist/api/edge-ref.js.map +1 -1
  57. package/dist/api/engrave.js +1 -1
  58. package/dist/api/engrave.js.map +1 -1
  59. package/dist/api/export-3mf.js +1 -1
  60. package/dist/api/export-3mf.js.map +1 -1
  61. package/dist/api/export-brep.js +1 -1
  62. package/dist/api/export-brep.js.map +1 -1
  63. package/dist/api/export-step.d.ts +12 -13
  64. package/dist/api/export-step.d.ts.map +1 -1
  65. package/dist/api/export-step.js +19 -17
  66. package/dist/api/export-step.js.map +1 -1
  67. package/dist/api/extrude.js +3 -3
  68. package/dist/api/extrude.js.map +1 -1
  69. package/dist/api/face-ref.js +2 -2
  70. package/dist/api/face-ref.js.map +1 -1
  71. package/dist/api/feature-repair.js +2 -2
  72. package/dist/api/feature-repair.js.map +1 -1
  73. package/dist/api/fillet-variable.js +1 -1
  74. package/dist/api/fillet-variable.js.map +1 -1
  75. package/dist/api/fillet.js +1 -1
  76. package/dist/api/fillet.js.map +1 -1
  77. package/dist/api/geom.js +1 -1
  78. package/dist/api/geom.js.map +1 -1
  79. package/dist/api/import-step.d.ts.map +1 -1
  80. package/dist/api/import-step.js +4 -2
  81. package/dist/api/import-step.js.map +1 -1
  82. package/dist/api/pattern.js +1 -1
  83. package/dist/api/pattern.js.map +1 -1
  84. package/dist/api/place.js +1 -1
  85. package/dist/api/place.js.map +1 -1
  86. package/dist/api/punch-hole.js +1 -1
  87. package/dist/api/punch-hole.js.map +1 -1
  88. package/dist/api/replicate.js +1 -1
  89. package/dist/api/replicate.js.map +1 -1
  90. package/dist/api/revolve.js +1 -1
  91. package/dist/api/revolve.js.map +1 -1
  92. package/dist/api/section-by-plane.js +1 -1
  93. package/dist/api/section-by-plane.js.map +1 -1
  94. package/dist/api/shell.js +1 -1
  95. package/dist/api/shell.js.map +1 -1
  96. package/dist/api/sketch-on-face.js +1 -1
  97. package/dist/api/sketch-on-face.js.map +1 -1
  98. package/dist/api/split-by-plane.js +1 -1
  99. package/dist/api/split-by-plane.js.map +1 -1
  100. package/dist/api/split.js +1 -1
  101. package/dist/api/split.js.map +1 -1
  102. package/dist/api/thicken.js +1 -1
  103. package/dist/api/thicken.js.map +1 -1
  104. package/dist/api/topo-resolve.js +4 -4
  105. package/dist/api/topo-resolve.js.map +1 -1
  106. package/dist/api/transform.js +1 -1
  107. package/dist/api/transform.js.map +1 -1
  108. package/dist/api/view/view-projection.d.ts +8 -0
  109. package/dist/api/view/view-projection.d.ts.map +1 -1
  110. package/dist/api/view/view-projection.js +73 -2
  111. package/dist/api/view/view-projection.js.map +1 -1
  112. package/dist/boolean/cross-section.d.ts.map +1 -1
  113. package/dist/boolean/cross-section.js +3 -1
  114. package/dist/boolean/cross-section.js.map +1 -1
  115. package/dist/brep/brep-ops.d.ts +26 -0
  116. package/dist/brep/brep-ops.d.ts.map +1 -1
  117. package/dist/brep/brep-ops.js +45 -0
  118. package/dist/brep/brep-ops.js.map +1 -1
  119. package/dist/brep/brep-topology.js +1 -1
  120. package/dist/brep/brep-topology.js.map +1 -1
  121. package/dist/brep/engine/adapters/brep-mock.d.ts.map +1 -1
  122. package/dist/brep/engine/adapters/brep-mock.js +2 -0
  123. package/dist/brep/engine/adapters/brep-mock.js.map +1 -1
  124. package/dist/brep/engine/primitives.d.ts +17 -0
  125. package/dist/brep/engine/primitives.d.ts.map +1 -1
  126. package/dist/brep/engine/primitives.js.map +1 -1
  127. package/dist/brep/export/export-model.d.ts.map +1 -1
  128. package/dist/brep/export/export-model.js +36 -11
  129. package/dist/brep/export/export-model.js.map +1 -1
  130. package/dist/brep/export/step-flat.d.ts +35 -0
  131. package/dist/brep/export/step-flat.d.ts.map +1 -0
  132. package/dist/brep/export/step-flat.js +94 -0
  133. package/dist/brep/export/step-flat.js.map +1 -0
  134. package/dist/brep/export/step.d.ts.map +1 -1
  135. package/dist/brep/export/step.js +42 -21
  136. package/dist/brep/export/step.js.map +1 -1
  137. package/dist/brep/handle-bridge.d.ts +4 -4
  138. package/dist/brep/handle-bridge.js +4 -4
  139. package/dist/brep/handle-bridge.js.map +1 -1
  140. package/dist/brep/svg/svg-to-solid.d.ts +3 -17
  141. package/dist/brep/svg/svg-to-solid.d.ts.map +1 -1
  142. package/dist/brep/svg/svg-to-solid.js +16 -317
  143. package/dist/brep/svg/svg-to-solid.js.map +1 -1
  144. package/dist/brepkit-kernel/brepkitKernel.d.ts.map +1 -1
  145. package/dist/brepkit-kernel/brepkitKernel.js +10 -0
  146. package/dist/brepkit-kernel/brepkitKernel.js.map +1 -1
  147. package/dist/brepkit-kernel/brepkitWasm.d.ts +1 -1
  148. package/dist/brepkit-kernel/brepkitWasm.d.ts.map +1 -1
  149. package/dist/brepkit-kernel/brepkitWasm.js +7 -6
  150. package/dist/brepkit-kernel/brepkitWasm.js.map +1 -1
  151. package/dist/brepkit-kernel/multi-version-test.d.ts +7 -7
  152. package/dist/brepkit-kernel/multi-version-test.d.ts.map +1 -1
  153. package/dist/brepkit-kernel/multi-version-test.js +10 -10
  154. package/dist/brepkit-kernel/multi-version-test.js.map +1 -1
  155. package/dist/cad-runtime/direct-executor.js +2 -2
  156. package/dist/cad-runtime/direct-executor.js.map +1 -1
  157. package/dist/cad-runtime/runtime.js +5 -5
  158. package/dist/cad-runtime/runtime.js.map +1 -1
  159. package/dist/geometry2d/curve2d.js +42 -1
  160. package/dist/geometry2d/curve2d.js.map +1 -1
  161. package/dist/math3d/Box3.js.map +1 -1
  162. package/dist/math3d/Euler.js.map +1 -1
  163. package/dist/math3d/Mat4.js.map +1 -1
  164. package/dist/math3d/Plane.js.map +1 -1
  165. package/dist/math3d/Quat.js.map +1 -1
  166. package/dist/math3d/Vec2.js.map +1 -1
  167. package/dist/math3d/Vec3.js.map +1 -1
  168. package/dist/math3d/index.js.map +1 -1
  169. package/dist/mesh/meshdata/contour.d.ts +34 -0
  170. package/dist/mesh/meshdata/contour.d.ts.map +1 -0
  171. package/dist/mesh/meshdata/contour.js +39 -0
  172. package/dist/mesh/meshdata/contour.js.map +1 -0
  173. package/dist/mesh/meshdata/extrude.d.ts +25 -0
  174. package/dist/mesh/meshdata/extrude.d.ts.map +1 -0
  175. package/dist/mesh/meshdata/extrude.js +198 -0
  176. package/dist/mesh/meshdata/extrude.js.map +1 -0
  177. package/dist/mesh/meshdata/index.js.map +1 -1
  178. package/dist/mesh/meshdata/ops.js.map +1 -1
  179. package/dist/mesh/meshdata/parametric.js.map +1 -1
  180. package/dist/mesh/meshdata/weld.js.map +1 -1
  181. package/dist/node-host/cli.d.ts.map +1 -1
  182. package/dist/node-host/cli.js +3 -2
  183. package/dist/node-host/cli.js.map +1 -1
  184. package/dist/occt-kernel/occt-primitives.d.ts.map +1 -1
  185. package/dist/occt-kernel/occt-primitives.js +2 -0
  186. package/dist/occt-kernel/occt-primitives.js.map +1 -1
  187. package/dist/primitives/svg/svg-path.d.ts +192 -0
  188. package/dist/primitives/svg/svg-path.d.ts.map +1 -0
  189. package/dist/primitives/svg/svg-path.js +776 -0
  190. package/dist/primitives/svg/svg-path.js.map +1 -0
  191. package/dist/third-party/earcut.d.ts +19 -0
  192. package/dist/third-party/earcut.d.ts.map +1 -0
  193. package/dist/third-party/earcut.js +575 -0
  194. package/dist/third-party/earcut.js.map +1 -0
  195. package/package.json +22 -3
@@ -0,0 +1,776 @@
1
+ // ─── SVG 元素提取(将 <rect>/<circle>/… 转为 <path d>) ───
2
+ /**
3
+ * 从 SVG 字符串中提取所有图形元素的 path `d` 属性。
4
+ *
5
+ * 支持:<path>, <rect>, <circle>, <ellipse>, <line>, <polyline>, <polygon>。
6
+ * 使用正则提取,无 DOMParser 依赖(可在 Web Worker / Node 中运行)。
7
+ *
8
+ * @param svgString - the raw SVG markup to extract paths from.
9
+ * @returns an array of path `d` strings.
10
+ */
11
+ export function extractSvgPaths(svgString) {
12
+ const paths = [];
13
+ extractPathDAttributes(svgString, paths);
14
+ extractRectPaths(svgString, paths);
15
+ extractCirclePaths(svgString, paths);
16
+ extractEllipsePaths(svgString, paths);
17
+ extractLinePaths(svgString, paths);
18
+ extractPolylinePaths(svgString, paths, false);
19
+ extractPolylinePaths(svgString, paths, true);
20
+ return paths;
21
+ }
22
+ function extractPathDAttributes(svg, out) {
23
+ const re = /<path\b[^>]*\bd\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>/gi;
24
+ let m;
25
+ while ((m = re.exec(svg)) !== null) {
26
+ const d = m[1] ?? m[2];
27
+ if (d)
28
+ out.push(d);
29
+ }
30
+ }
31
+ function extractRectPaths(svg, out) {
32
+ const re = /<rect\b([^>]*)\/?>/gi;
33
+ let m;
34
+ while ((m = re.exec(svg)) !== null) {
35
+ const attrs = parseAttrs(m[1] ?? '');
36
+ const x = parseFloat(attrs.x ?? '0');
37
+ const y = parseFloat(attrs.y ?? '0');
38
+ const w = parseFloat(attrs.width ?? '0');
39
+ const h = parseFloat(attrs.height ?? '0');
40
+ const rx = parseFloat(attrs.rx ?? '0');
41
+ const ry = parseFloat(attrs.ry ?? '0');
42
+ if (w <= 0 || h <= 0)
43
+ continue;
44
+ const rectTransform = attrs.transform ? parseSvgTransform(attrs.transform) : undefined;
45
+ if (rectTransform) {
46
+ const corners = [
47
+ [x, y], [x + w, y], [x + w, y + h], [x, y + h],
48
+ ];
49
+ const tc = corners.map(([cx, cy]) => [
50
+ rectTransform[0] * cx + rectTransform[2] * cy + rectTransform[4],
51
+ rectTransform[1] * cx + rectTransform[3] * cy + rectTransform[5],
52
+ ]);
53
+ let d = `M ${tc[0][0]},${tc[0][1]}`;
54
+ for (let i = 1; i < tc.length; i++)
55
+ d += ` L ${tc[i][0]},${tc[i][1]}`;
56
+ out.push(d + ' Z');
57
+ }
58
+ else if (rx > 0 || ry > 0) {
59
+ const r = Math.min(rx || ry, w / 2, h / 2);
60
+ out.push(rectToPathRounded(x, y, w, h, r));
61
+ }
62
+ else {
63
+ out.push(`M ${x},${y} h ${w} v ${h} h ${-w} Z`);
64
+ }
65
+ }
66
+ }
67
+ function rectToPathRounded(x, y, w, h, r) {
68
+ return [
69
+ `M ${x + r},${y}`,
70
+ `h ${w - 2 * r}`,
71
+ `A ${r},${r} 0 0 1 ${x + w},${y + r}`,
72
+ `v ${h - 2 * r}`,
73
+ `A ${r},${r} 0 0 1 ${x + w - r},${y + h}`,
74
+ `h ${-(w - 2 * r)}`,
75
+ `A ${r},${r} 0 0 1 ${x},${y + h - r}`,
76
+ `v ${-(h - 2 * r)}`,
77
+ `A ${r},${r} 0 0 1 ${x + r},${y}`,
78
+ 'Z',
79
+ ].join(' ');
80
+ }
81
+ function extractCirclePaths(svg, out) {
82
+ const re = /<circle\b([^>]*)\/?>/gi;
83
+ let m;
84
+ while ((m = re.exec(svg)) !== null) {
85
+ const attrs = parseAttrs(m[1] ?? '');
86
+ const cx = parseFloat(attrs.cx ?? '0');
87
+ const cy = parseFloat(attrs.cy ?? '0');
88
+ const r = parseFloat(attrs.r ?? '0');
89
+ if (r <= 0)
90
+ continue;
91
+ out.push(`M ${cx - r},${cy} A ${r},${r} 0 1 0 ${cx + r},${cy} A ${r},${r} 0 1 0 ${cx - r},${cy} Z`);
92
+ }
93
+ }
94
+ function extractEllipsePaths(svg, out) {
95
+ const re = /<ellipse\b([^>]*)\/?>/gi;
96
+ let m;
97
+ while ((m = re.exec(svg)) !== null) {
98
+ const attrs = parseAttrs(m[1] ?? '');
99
+ const cx = parseFloat(attrs.cx ?? '0');
100
+ const cy = parseFloat(attrs.cy ?? '0');
101
+ const rx = parseFloat(attrs.rx ?? '0');
102
+ const ry = parseFloat(attrs.ry ?? '0');
103
+ if (rx <= 0 || ry <= 0)
104
+ continue;
105
+ out.push(`M ${cx - rx},${cy} A ${rx},${ry} 0 1 0 ${cx + rx},${cy} A ${rx},${ry} 0 1 0 ${cx - rx},${cy} Z`);
106
+ }
107
+ }
108
+ function extractLinePaths(svg, out) {
109
+ const re = /<line\b([^>]*)\/?>/gi;
110
+ let m;
111
+ while ((m = re.exec(svg)) !== null) {
112
+ const attrs = parseAttrs(m[1] ?? '');
113
+ const x1 = parseFloat(attrs.x1 ?? '0');
114
+ const y1 = parseFloat(attrs.y1 ?? '0');
115
+ const x2 = parseFloat(attrs.x2 ?? '0');
116
+ const y2 = parseFloat(attrs.y2 ?? '0');
117
+ out.push(`M ${x1},${y1} L ${x2},${y2}`);
118
+ }
119
+ }
120
+ function extractPolylinePaths(svg, out, closed) {
121
+ const tag = closed ? 'polygon' : 'polyline';
122
+ const re = new RegExp(`<${tag}\\b([^>]*)\\/??>`, 'gi');
123
+ let m;
124
+ while ((m = re.exec(svg)) !== null) {
125
+ const attrs = parseAttrs(m[1] ?? '');
126
+ const pointsStr = attrs.points ?? '';
127
+ const nums = pointsStr.split(/[\s,]+/).filter((s) => s.length > 0).map(Number);
128
+ if (nums.length < 4)
129
+ continue;
130
+ let d = `M ${nums[0]},${nums[1]}`;
131
+ for (let i = 2; i + 1 < nums.length; i += 2) {
132
+ d += ` L ${nums[i]},${nums[i + 1]}`;
133
+ }
134
+ if (closed)
135
+ d += ' Z';
136
+ out.push(d);
137
+ }
138
+ }
139
+ function parseAttrs(attrStr) {
140
+ const attrs = {};
141
+ const re = /(\w[\w-]*)\s*=\s*(?:"([^"]*)"|'([^']*)')/g;
142
+ let m;
143
+ while ((m = re.exec(attrStr)) !== null) {
144
+ attrs[m[1].toLowerCase()] = m[2] ?? m[3] ?? '';
145
+ }
146
+ return attrs;
147
+ }
148
+ // ─── SVG path 分词(d 属性 → 命令序列) ───
149
+ /**
150
+ * 将 SVG path `d` 属性拆分为命令序列。
151
+ *
152
+ * 支持完整命令集:M/L/H/V/C/S/Q/T/A/Z(含相对形式小写)。
153
+ * 每个 token 保留命令字与已解析的数值参数(多参数组由消费方按命令语义切分)。
154
+ *
155
+ * @param d - the SVG path `d` attribute value.
156
+ * @returns the token list.
157
+ */
158
+ export function tokenizeSvgPath(d) {
159
+ const tokens = [];
160
+ const re = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
161
+ let match;
162
+ while ((match = re.exec(d)) !== null) {
163
+ const command = match[1] ?? '';
164
+ const argStr = (match[2] ?? '').trim();
165
+ const args = [];
166
+ if (argStr) {
167
+ const numRe = /[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?/g;
168
+ let numMatch;
169
+ while ((numMatch = numRe.exec(argStr)) !== null) {
170
+ args.push(parseFloat(numMatch[0]));
171
+ }
172
+ }
173
+ tokens.push({ command, args });
174
+ }
175
+ return tokens;
176
+ }
177
+ // ─── SVG transform 处理 ───
178
+ /**
179
+ * 解析 SVG transform 字符串(仅支持 matrix 形式)。
180
+ * @param transformStr - 形如 `matrix(a,b,c,d,e,f)` 的变换字符串。
181
+ * @returns 六元仿射矩阵 [a,b,c,d,e,f],非 matrix 形式返回 undefined。
182
+ */
183
+ export function parseSvgTransform(transformStr) {
184
+ const m = transformStr.match(/matrix\s*\(\s*([-\d.eE+]+)\s*,\s*([-\d.eE+]+)\s*,\s*([-\d.eE+]+)\s*,\s*([-\d.eE+]+)\s*,\s*([-\d.eE+]+)\s*,\s*([-\d.eE+]+)\s*\)/);
185
+ if (m) {
186
+ return [parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]), parseFloat(m[4]), parseFloat(m[5]), parseFloat(m[6])];
187
+ }
188
+ return undefined;
189
+ }
190
+ /**
191
+ * 从 SVG 字符串中提取 `<g transform="matrix(...)">` 的变换矩阵。
192
+ * @param svgString - SVG 文档字符串。
193
+ * @returns 首个 `<g>` 的 matrix 变换(六元数组),无匹配返回 undefined。
194
+ */
195
+ export function extractGroupTransform(svgString) {
196
+ const m = svgString.match(/<g\b[^>]*\btransform\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>/i);
197
+ if (m) {
198
+ return parseSvgTransform(m[1] ?? m[2] ?? '');
199
+ }
200
+ return undefined;
201
+ }
202
+ /** 对 SVG 坐标点(Y-down)应用 2D 仿射矩阵。 */
203
+ function applySvgTransform(p, gt) {
204
+ return {
205
+ x: gt[0] * p.x + gt[2] * p.y + gt[4],
206
+ y: gt[1] * p.x + gt[3] * p.y + gt[5],
207
+ };
208
+ }
209
+ // ─── 曲线几何(折线化用) ───
210
+ /** 三次贝塞尔采样(与 three `CubicBezierCurve.getPoint` 同公式)。 */
211
+ function cubicAt(p0, p1, p2, p3, t) {
212
+ const mt = 1 - t;
213
+ const a = mt * mt * mt;
214
+ const b = 3 * mt * mt * t;
215
+ const c = 3 * mt * t * t;
216
+ const d = t * t * t;
217
+ return {
218
+ x: a * p0.x + b * p1.x + c * p2.x + d * p3.x,
219
+ y: a * p0.y + b * p1.y + c * p2.y + d * p3.y,
220
+ };
221
+ }
222
+ /** 二次贝塞尔采样。 */
223
+ function quadAt(p0, p1, p2, t) {
224
+ const mt = 1 - t;
225
+ return {
226
+ x: mt * mt * p0.x + 2 * mt * t * p1.x + t * t * p2.x,
227
+ y: mt * mt * p0.y + 2 * mt * t * p1.y + t * t * p2.y,
228
+ };
229
+ }
230
+ /** 三点外接圆(p0/p1/p2 共线时返回 null)。 */
231
+ function circumcircle(p0, p1, p2) {
232
+ const d = 2 * (p0.x * (p1.y - p2.y) + p1.x * (p2.y - p0.y) + p2.x * (p0.y - p1.y));
233
+ if (Math.abs(d) < 1e-12)
234
+ return null;
235
+ const p0sq = p0.x * p0.x + p0.y * p0.y;
236
+ const p1sq = p1.x * p1.x + p1.y * p1.y;
237
+ const p2sq = p2.x * p2.x + p2.y * p2.y;
238
+ const cx = (p0sq * (p1.y - p2.y) + p1sq * (p2.y - p0.y) + p2sq * (p0.y - p1.y)) / d;
239
+ // 注意 cy 取 x 差分(与 cx 取 y 差分对称)。标准外接圆圆心公式的 y 分量为
240
+ // (p0sq·(p2.x−p1.x) + p1sq·(p0.x−p2.x) + p2sq·(p1.x−p0.x)) / D;
241
+ // 旧写法把三项整体取了相反数(得 −uy),导致圆心纵坐标翻转、采样弧绕错心、
242
+ // 闭合多边形有向面积自相抵消≈0,进而 svgPathToContours 把所有圆弧 SVG(圆/环/
243
+ // 圆角矩形)误判为“无闭合轮廓”而丢弃。
244
+ const cy = (p0sq * (p2.x - p1.x) + p1sq * (p0.x - p2.x) + p2sq * (p1.x - p0.x)) / d;
245
+ return { cx, cy, r: Math.hypot(p0.x - cx, p0.y - cy) };
246
+ }
247
+ /** 角度归一化到 (-π, π]。 */
248
+ function normAngle(a) {
249
+ let x = a;
250
+ while (x > Math.PI)
251
+ x -= Math.PI * 2;
252
+ while (x <= -Math.PI)
253
+ x += Math.PI * 2;
254
+ return x;
255
+ }
256
+ /**
257
+ * SVG arc 参数 → sagitta 中点 → 三点圆弧(与 brep 链 `arcMidPoint` 同算法)。
258
+ * @returns 三点圆弧的中间点(SVG 坐标,Y-down)。
259
+ */
260
+ function arcMidPoint(fromX, fromY, toX, toY, rx, ry, largeArc, sweepFlag) {
261
+ const r = Math.max(rx, ry);
262
+ const dx = toX - fromX;
263
+ const dy = toY - fromY;
264
+ const dist = Math.sqrt(dx * dx + dy * dy);
265
+ const halfChord = dist / 2;
266
+ const sagitta = halfChord < r ? r - Math.sqrt(r * r - halfChord * halfChord) : r;
267
+ const sign = (largeArc !== sweepFlag ? 1 : -1) * (sweepFlag ? 1 : -1);
268
+ const nx = -dy / dist;
269
+ const ny = dx / dist;
270
+ const midX = (fromX + toX) / 2 + sign * sagitta * nx;
271
+ const midY = (fromY + toY) / 2 + sign * sagitta * ny;
272
+ return { x: midX, y: midY };
273
+ }
274
+ /** 三点圆弧按圆心角线性采样(从 start 经 mid 到 end,方向自动判定)。 */
275
+ function sampleArcThrough(start, mid, end, segments) {
276
+ const c = circumcircle(start, mid, end);
277
+ if (!c) {
278
+ // 退化(共线):直接取终点
279
+ return [end];
280
+ }
281
+ const a0 = Math.atan2(start.y - c.cy, start.x - c.cx);
282
+ const a1 = Math.atan2(mid.y - c.cy, mid.x - c.cx);
283
+ const a2 = Math.atan2(end.y - c.cy, end.x - c.cx);
284
+ const d1 = normAngle(a1 - a0);
285
+ const d2 = normAngle(a2 - a1);
286
+ let span;
287
+ if (d1 * d2 >= 0) {
288
+ span = d1 + d2;
289
+ }
290
+ else {
291
+ // 跨 ±π 边界:把中点的角度展开到与起点同侧后再算
292
+ span = d1 + d2 + (d1 > 0 ? Math.PI * 2 : -Math.PI * 2);
293
+ }
294
+ const out = [];
295
+ for (let i = 1; i <= segments; i++) {
296
+ const t = i / segments;
297
+ const a = a0 + span * t;
298
+ out.push({ x: c.cx + c.r * Math.cos(a), y: c.cy + c.r * Math.sin(a) });
299
+ }
300
+ return out;
301
+ }
302
+ // ─── 子路径折线化(d → 采样点集) ───
303
+ /**
304
+ * 将 SVG path `d` 折线化为闭合/开放子路径的点集。
305
+ *
306
+ * - 直线(M/L/H/V)直接取点;
307
+ * - 三次(C/S)与二次(Q/T)贝塞尔按 `curveSegments` 段采样(与 three
308
+ * `Shape.getPoints(curveSegments)` 的段数语义一致);
309
+ * - 弧(A)用 sagitta 三点圆弧近似后按 `curveSegments` 段采样;
310
+ * - `M` 前隐式闭合(opentype/常见导出器不写 `Z` 的情形)、`Z` 显式闭合。
311
+ *
312
+ * 输出为 **SVG 原始坐标(Y-down)**;不翻转。未闭合子路径 `closed=false`。
313
+ *
314
+ * @param d - the SVG path `d` attribute.
315
+ * @param curveSegments - per-curve sampling segments (default 6, aligned with the editor text path).
316
+ * @returns the flattened subpaths.
317
+ */
318
+ export function parsePathToSubpaths(d, curveSegments = 6) {
319
+ const tokens = tokenizeSvgPath(d);
320
+ const subpaths = [];
321
+ let cx = 0;
322
+ let cy = 0;
323
+ let sx = 0;
324
+ let sy = 0;
325
+ let prevControlX = 0;
326
+ let prevControlY = 0;
327
+ let lastCmd = '';
328
+ let lastPt = null;
329
+ let subpathStart = null;
330
+ let current = [];
331
+ function flush(closed) {
332
+ if (current.length === 0)
333
+ return;
334
+ subpaths.push({
335
+ points: current,
336
+ bbox: computeBBox(current),
337
+ signedArea: computeSignedArea(current),
338
+ closed,
339
+ });
340
+ current = [];
341
+ }
342
+ for (const { command, args } of tokens) {
343
+ const isRelative = command === command.toLowerCase();
344
+ const cmd = command.toUpperCase();
345
+ switch (cmd) {
346
+ case 'M': {
347
+ if (current.length > 0)
348
+ flush(false);
349
+ let i = 0;
350
+ while (i + 1 < args.length) {
351
+ const x = (isRelative ? cx : 0) + (args[i] ?? 0);
352
+ const y = (isRelative ? cy : 0) + (args[i + 1] ?? 0);
353
+ if (i === 0) {
354
+ sx = x;
355
+ sy = y;
356
+ subpathStart = { x, y };
357
+ current.push({ x, y });
358
+ }
359
+ else {
360
+ const curr = { x, y };
361
+ current.push(curr);
362
+ }
363
+ lastPt = { x, y };
364
+ cx = x;
365
+ cy = y;
366
+ i += 2;
367
+ }
368
+ break;
369
+ }
370
+ case 'L': {
371
+ let i = 0;
372
+ while (i + 1 < args.length) {
373
+ const x = (isRelative ? cx : 0) + (args[i] ?? 0);
374
+ const y = (isRelative ? cy : 0) + (args[i + 1] ?? 0);
375
+ current.push({ x, y });
376
+ lastPt = { x, y };
377
+ cx = x;
378
+ cy = y;
379
+ i += 2;
380
+ }
381
+ break;
382
+ }
383
+ case 'H': {
384
+ for (const arg of args) {
385
+ const x = isRelative ? cx + arg : arg;
386
+ current.push({ x, y: cy });
387
+ lastPt = { x, y: cy };
388
+ cx = x;
389
+ }
390
+ break;
391
+ }
392
+ case 'V': {
393
+ for (const arg of args) {
394
+ const y = isRelative ? cy + arg : arg;
395
+ current.push({ x: cx, y });
396
+ lastPt = { x: cx, y };
397
+ cy = y;
398
+ }
399
+ break;
400
+ }
401
+ case 'C': {
402
+ let i = 0;
403
+ while (i + 5 < args.length) {
404
+ const ox = isRelative ? cx : 0;
405
+ const oy = isRelative ? cy : 0;
406
+ const p0 = lastPt ?? { x: ox, y: oy };
407
+ const cp1 = { x: ox + (args[i] ?? 0), y: oy + (args[i + 1] ?? 0) };
408
+ const cp2 = { x: ox + (args[i + 2] ?? 0), y: oy + (args[i + 3] ?? 0) };
409
+ const endPt = { x: ox + (args[i + 4] ?? 0), y: oy + (args[i + 5] ?? 0) };
410
+ for (let s = 1; s <= curveSegments; s++) {
411
+ current.push(cubicAt(p0, cp1, cp2, endPt, s / curveSegments));
412
+ }
413
+ prevControlX = cp2.x;
414
+ prevControlY = cp2.y;
415
+ lastPt = endPt;
416
+ cx = endPt.x;
417
+ cy = endPt.y;
418
+ i += 6;
419
+ }
420
+ break;
421
+ }
422
+ case 'S': {
423
+ let i = 0;
424
+ while (i + 3 < args.length) {
425
+ const ox = isRelative ? cx : 0;
426
+ const oy = isRelative ? cy : 0;
427
+ const p0 = lastPt ?? { x: ox, y: oy };
428
+ const cp1x = lastCmd === 'C' || lastCmd === 'S' ? 2 * cx - prevControlX : cx;
429
+ const cp1y = lastCmd === 'C' || lastCmd === 'S' ? 2 * cy - prevControlY : cy;
430
+ const cp2 = { x: ox + (args[i] ?? 0), y: oy + (args[i + 1] ?? 0) };
431
+ const endPt = { x: ox + (args[i + 2] ?? 0), y: oy + (args[i + 3] ?? 0) };
432
+ for (let s = 1; s <= curveSegments; s++) {
433
+ current.push(cubicAt(p0, { x: cp1x, y: cp1y }, cp2, endPt, s / curveSegments));
434
+ }
435
+ prevControlX = cp2.x;
436
+ prevControlY = cp2.y;
437
+ lastPt = endPt;
438
+ cx = endPt.x;
439
+ cy = endPt.y;
440
+ lastCmd = 'S';
441
+ i += 4;
442
+ }
443
+ break;
444
+ }
445
+ case 'Q': {
446
+ let i = 0;
447
+ while (i + 3 < args.length) {
448
+ const ox = isRelative ? cx : 0;
449
+ const oy = isRelative ? cy : 0;
450
+ const p0 = lastPt ?? { x: ox, y: oy };
451
+ const cp = { x: ox + (args[i] ?? 0), y: oy + (args[i + 1] ?? 0) };
452
+ const endPt = { x: ox + (args[i + 2] ?? 0), y: oy + (args[i + 3] ?? 0) };
453
+ for (let s = 1; s <= curveSegments; s++) {
454
+ current.push(quadAt(p0, cp, endPt, s / curveSegments));
455
+ }
456
+ prevControlX = cp.x;
457
+ prevControlY = cp.y;
458
+ lastPt = endPt;
459
+ cx = endPt.x;
460
+ cy = endPt.y;
461
+ i += 4;
462
+ }
463
+ break;
464
+ }
465
+ case 'T': {
466
+ let i = 0;
467
+ while (i + 1 < args.length) {
468
+ const ox = isRelative ? cx : 0;
469
+ const oy = isRelative ? cy : 0;
470
+ const p0 = lastPt ?? { x: ox, y: oy };
471
+ const cpx = lastCmd === 'Q' || lastCmd === 'T' ? 2 * cx - prevControlX : cx;
472
+ const cpy = lastCmd === 'Q' || lastCmd === 'T' ? 2 * cy - prevControlY : cy;
473
+ const endPt = { x: ox + (args[i] ?? 0), y: oy + (args[i + 1] ?? 0) };
474
+ for (let s = 1; s <= curveSegments; s++) {
475
+ current.push(quadAt(p0, { x: cpx, y: cpy }, endPt, s / curveSegments));
476
+ }
477
+ prevControlX = cpx;
478
+ prevControlY = cpy;
479
+ lastPt = endPt;
480
+ cx = endPt.x;
481
+ cy = endPt.y;
482
+ lastCmd = 'T';
483
+ i += 2;
484
+ }
485
+ break;
486
+ }
487
+ case 'A': {
488
+ let i = 0;
489
+ while (i + 6 < args.length) {
490
+ const rx = Math.abs(args[i] ?? 0);
491
+ const ry = Math.abs(args[i + 1] ?? 0);
492
+ const largeArc = (args[i + 3] ?? 0) !== 0;
493
+ const sweepFlag = (args[i + 4] ?? 0) !== 0;
494
+ const x = (isRelative ? cx : 0) + (args[i + 5] ?? 0);
495
+ const y = (isRelative ? cy : 0) + (args[i + 6] ?? 0);
496
+ const from = lastPt ?? { x: cx, y: cy };
497
+ if (rx === 0 || ry === 0) {
498
+ // 退化为直线
499
+ const endPt = { x, y };
500
+ current.push(endPt);
501
+ lastPt = endPt;
502
+ }
503
+ else {
504
+ const dx = x - cx;
505
+ const dy = y - cy;
506
+ const dist = Math.sqrt(dx * dx + dy * dy);
507
+ const endPt = { x, y };
508
+ if (dist < 1e-10) {
509
+ // 完整圆 → 两段半圆(与 brep 链一致)
510
+ const r = Math.max(rx, ry);
511
+ const sweep = sweepFlag ? 1 : -1;
512
+ const mid1 = { x: cx - sweep * r, y: cy + sweep * r };
513
+ const opposite = { x: cx, y: cy + 2 * sweep * r };
514
+ const mid2 = { x: cx + sweep * r, y: cy + sweep * r };
515
+ current.push(...sampleArcThrough(from, mid1, opposite, curveSegments));
516
+ current.push(...sampleArcThrough(opposite, mid2, from, curveSegments));
517
+ }
518
+ else {
519
+ const mid = arcMidPoint(cx, cy, x, y, rx, ry, largeArc, sweepFlag);
520
+ current.push(...sampleArcThrough(from, mid, endPt, curveSegments));
521
+ }
522
+ lastPt = endPt;
523
+ }
524
+ cx = x;
525
+ cy = y;
526
+ i += 7;
527
+ }
528
+ break;
529
+ }
530
+ case 'Z': {
531
+ // 闭合:若终点 ≠ 起点则补闭合点
532
+ if (subpathStart && lastPt &&
533
+ (Math.abs(subpathStart.x - lastPt.x) > 1e-9 ||
534
+ Math.abs(subpathStart.y - lastPt.y) > 1e-9)) {
535
+ current.push({ x: subpathStart.x, y: subpathStart.y });
536
+ }
537
+ flush(true);
538
+ lastPt = subpathStart;
539
+ cx = sx;
540
+ cy = sy;
541
+ break;
542
+ }
543
+ }
544
+ if (cmd !== 'S' && cmd !== 'T') {
545
+ lastCmd = cmd;
546
+ }
547
+ }
548
+ if (current.length > 0)
549
+ flush(false);
550
+ return subpaths;
551
+ }
552
+ /**
553
+ * 计算点集的轴对齐包围盒。
554
+ * @param points - 二维点集(SvgPoint2 结构)。
555
+ * @returns { minX, minY, maxX, maxY } 包围盒。
556
+ */
557
+ export function computeBBox(points) {
558
+ let minX = Infinity;
559
+ let minY = Infinity;
560
+ let maxX = -Infinity;
561
+ let maxY = -Infinity;
562
+ for (const p of points) {
563
+ if (p.x < minX)
564
+ minX = p.x;
565
+ if (p.y < minY)
566
+ minY = p.y;
567
+ if (p.x > maxX)
568
+ maxX = p.x;
569
+ if (p.y > maxY)
570
+ maxY = p.y;
571
+ }
572
+ return { minX, minY, maxX, maxY };
573
+ }
574
+ /**
575
+ * Shoelace 有符号面积(SVG 坐标系,Y-down;正/负语义与翻转无关紧要,仅比较绝对值)。
576
+ * @param points - 闭合子路径的顶点序列(首尾不重复亦可,按环序求和)。
577
+ * @returns 有符号面积(×0.5),点数 < 3 返回 0。
578
+ */
579
+ export function computeSignedArea(points) {
580
+ const n = points.length;
581
+ if (n < 3)
582
+ return 0;
583
+ let area = 0;
584
+ for (let i = 0; i < n; i++) {
585
+ const j = (i + 1) % n;
586
+ area += points[i].x * points[j].y - points[j].x * points[i].y;
587
+ }
588
+ return area / 2;
589
+ }
590
+ /**
591
+ * 射线法判断点是否在多边形内部(含边界视为内部由交点奇偶决定)。
592
+ * @param pt - 待测点。
593
+ * @param polygon - 多边形顶点序列(环序)。
594
+ * @returns true 当且仅当点在多边形内部。
595
+ */
596
+ export function pointInPolygon(pt, polygon) {
597
+ const n = polygon.length;
598
+ if (n < 3)
599
+ return false;
600
+ let inside = false;
601
+ for (let i = 0, j = n - 1; i < n; j = i++) {
602
+ const xi = polygon[i].x;
603
+ const yi = polygon[i].y;
604
+ const xj = polygon[j].x;
605
+ const yj = polygon[j].y;
606
+ const intersect = ((yi > pt.y) !== (yj > pt.y)) &&
607
+ (pt.x < (xj - xi) * (pt.y - yi) / (yj - yi + 1e-12) + xi);
608
+ if (intersect)
609
+ inside = !inside;
610
+ }
611
+ return inside;
612
+ }
613
+ /**
614
+ * 计算点集的算术平均中心(质心近似,仅用于嵌套分类的层级判断)。
615
+ * @param points - 二维点集。
616
+ * @returns 平均中心点。
617
+ */
618
+ export function polygonCentroid(points) {
619
+ let cx = 0;
620
+ let cy = 0;
621
+ for (const p of points) {
622
+ cx += p.x;
623
+ cy += p.y;
624
+ }
625
+ return { x: cx / points.length, y: cy / points.length };
626
+ }
627
+ /**
628
+ * 判断 inner 包围盒是否被 outer 包围盒完全包含(用于子路径嵌套层级预筛)。
629
+ * @param outer - 外层包围盒。
630
+ * @param inner - 内层包围盒。
631
+ * @returns true 当且仅当 inner 落在 outer 内部。
632
+ */
633
+ export function bboxContains(outer, inner) {
634
+ return inner.minX >= outer.minX && inner.maxX <= outer.maxX &&
635
+ inner.minY >= outer.minY && inner.maxY <= outer.maxY;
636
+ }
637
+ // ─── 孔洞分类(共享核心:点集版与 brep 链 wire 版同源) ───
638
+ /**
639
+ * 嵌套分类核心:将闭合子路径分组为外轮廓 + 孔洞(evenodd 语义)。
640
+ *
641
+ * 算法:
642
+ * 1. 按有符号面积绝对值降序排(大轮廓优先)
643
+ * 2. 对每条子路径 A,找到直接包含它的最小外轮廓 B(bbox 包含 + 质心射线法)
644
+ * 3. 按嵌套深度奇偶分组:深度 0 为外轮廓、深度 1 为孔、深度 2 为「孔中岛」
645
+ * (岛是独立的第二层外轮廓,potrace 等导出器会把落在孔洞区域内的装饰子路径
646
+ * 输出为独立闭合子路径,单层模型会将其丢弃导致该区域被错误镂空)、
647
+ * 深度 3 为岛的孔,依此类推。
648
+ *
649
+ * 返回索引分组(sorted 序),由调用方组装为自己的数据结构。
650
+ *
651
+ * @param subpaths - the closed subpaths to classify (any type with points/bbox/signedArea).
652
+ * @returns the nested groups referencing indices into the sorted array.
653
+ */
654
+ export function classifyNested(subpaths) {
655
+ const sorted = [...subpaths].sort((a, b) => Math.abs(b.signedArea) - Math.abs(a.signedArea));
656
+ // 排序后各元素在原数组中的索引(恒等 map 会导致面积序 ≠ 原序时索引错位:
657
+ // 文本多字符轮廓按字符顺序进入、按面积排序后 outer/holes 全部张冠李戴)。
658
+ const sortedIndex = subpaths.map((_, i) => i).sort((a, b) => Math.abs(subpaths[b].signedArea) - Math.abs(subpaths[a].signedArea));
659
+ const parentOf = new Array(sorted.length).fill(-1);
660
+ for (let i = 0; i < sorted.length; i++) {
661
+ let bestParent = -1;
662
+ let bestParentArea = Infinity;
663
+ const centroid = polygonCentroid(sorted[i].points);
664
+ for (let j = 0; j < sorted.length; j++) {
665
+ if (i === j)
666
+ continue;
667
+ if (Math.abs(sorted[j].signedArea) <= Math.abs(sorted[i].signedArea))
668
+ continue;
669
+ if (!bboxContains(sorted[j].bbox, sorted[i].bbox))
670
+ continue;
671
+ if (!pointInPolygon(centroid, sorted[j].points))
672
+ continue;
673
+ if (Math.abs(sorted[j].signedArea) < bestParentArea) {
674
+ bestParentArea = Math.abs(sorted[j].signedArea);
675
+ bestParent = j;
676
+ }
677
+ }
678
+ parentOf[i] = bestParent;
679
+ }
680
+ const depthOf = new Array(sorted.length).fill(0);
681
+ for (let i = 0; i < sorted.length; i++) {
682
+ let d = 0;
683
+ let p = parentOf[i];
684
+ const seen = new Set();
685
+ while (p !== -1 && !seen.has(p)) {
686
+ seen.add(p);
687
+ d++;
688
+ p = parentOf[p];
689
+ }
690
+ depthOf[i] = d;
691
+ }
692
+ const groups = [];
693
+ const groupIdxOfOuter = new Map();
694
+ for (let i = 0; i < sorted.length; i++) {
695
+ if (depthOf[i] % 2 === 0) {
696
+ groupIdxOfOuter.set(i, groups.length);
697
+ groups.push({ outerIdx: sortedIndex[i], holes: [] });
698
+ }
699
+ }
700
+ for (let i = 0; i < sorted.length; i++) {
701
+ if (depthOf[i] % 2 === 1) {
702
+ // 深度 1(或 3/5…):孔,挂到最近的偶数深度祖先所在的 group
703
+ let p = parentOf[i];
704
+ const seen = new Set();
705
+ while (p !== -1 && !seen.has(p)) {
706
+ seen.add(p);
707
+ if (depthOf[p] % 2 === 0)
708
+ break;
709
+ p = parentOf[p];
710
+ }
711
+ const groupIdx = p === -1 ? -1 : (groupIdxOfOuter.get(p) ?? -1);
712
+ if (groupIdx >= 0 && groupIdx < groups.length) {
713
+ groups[groupIdx].holes.push(sortedIndex[i]);
714
+ }
715
+ }
716
+ }
717
+ return groups;
718
+ }
719
+ /**
720
+ * 将闭合子路径分类为外轮廓 + 孔洞的分组(点集版,供 mesh 链 `svgPathToContours` 使用)。
721
+ *
722
+ * @param subpaths - the closed subpaths to classify.
723
+ * @returns the outer/hole contour groups.
724
+ */
725
+ export function classifyContours(subpaths) {
726
+ const groups = classifyNested(subpaths);
727
+ return groups.map((g) => ({
728
+ outer: subpaths[g.outerIdx].points,
729
+ holes: g.holes.map((h) => subpaths[h].points),
730
+ }));
731
+ }
732
+ // ─── 高层:SVG 字符串 → Y-up 轮廓 ───
733
+ /**
734
+ * 将 SVG 字符串解析为 Y-up 平面轮廓(供 `extrudeShape` 消费)。
735
+ *
736
+ * 流程:提取元素 → 逐 path 折线化 → 应用 `<g transform>` → Y-down 翻转为 Y-up →
737
+ * evenodd 孔洞分类。退化子路径(开放/面积≈0)被过滤。
738
+ *
739
+ * @param svgText - the raw SVG source text.
740
+ * @param curveSegments - per-curve sampling segments (default 6).
741
+ * @returns the classified contours (Y-up), ready for `extrudeShape`.
742
+ * @throws when the SVG contains no usable closed contour.
743
+ */
744
+ export function svgPathToContours(svgText, curveSegments = 6) {
745
+ const pathDs = extractSvgPaths(svgText);
746
+ if (pathDs.length === 0) {
747
+ throw new Error('[svg-path] No path elements found in SVG');
748
+ }
749
+ const groupTransform = extractGroupTransform(svgText);
750
+ const allSubpaths = [];
751
+ for (const d of pathDs) {
752
+ for (const sp of parsePathToSubpaths(d, curveSegments)) {
753
+ if (!sp.closed || Math.abs(sp.signedArea) < 1e-9 || sp.points.length < 3)
754
+ continue;
755
+ const pts = groupTransform ? sp.points.map((p) => applySvgTransform(p, groupTransform)) : sp.points;
756
+ // 翻转为 Y-up(与 core 世界坐标一致)
757
+ const flipped = pts.map((p) => ({ x: p.x, y: -p.y }));
758
+ allSubpaths.push({
759
+ points: flipped,
760
+ bbox: computeBBox(flipped),
761
+ signedArea: computeSignedArea(flipped),
762
+ closed: true,
763
+ });
764
+ }
765
+ }
766
+ if (allSubpaths.length === 0) {
767
+ throw new Error('[svg-path] No closed contours generated from SVG paths');
768
+ }
769
+ const groups = classifyContours(allSubpaths);
770
+ const contours = groups.map((g) => ({
771
+ outer: g.outer,
772
+ holes: g.holes.map((h) => h),
773
+ }));
774
+ return contours;
775
+ }
776
+ //# sourceMappingURL=svg-path.js.map