@yumiai/chat-widget 0.1.2 → 0.2.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 (107) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/README.md +119 -22
  3. package/dist/ExcelCore-DJOIVQMI.js +11 -0
  4. package/dist/ExcelCore-DJOIVQMI.js.map +1 -0
  5. package/dist/ExcelViewer-3YLLYYIQ.js +65 -0
  6. package/dist/ExcelViewer-3YLLYYIQ.js.map +1 -0
  7. package/dist/GerberViewerA2UI-7CNT7HX4.css +693 -0
  8. package/dist/GerberViewerA2UI-7CNT7HX4.css.map +1 -0
  9. package/dist/GerberViewerA2UI-X5FWAD5M.js +57 -0
  10. package/dist/GerberViewerA2UI-X5FWAD5M.js.map +1 -0
  11. package/dist/GraphStatsLegend-D5bPeXB_.d.cts +607 -0
  12. package/dist/GraphStatsLegend-D5bPeXB_.d.ts +607 -0
  13. package/dist/JsonRenderStandalone-EIZM62JU.js +18 -0
  14. package/dist/JsonRenderStandalone-EIZM62JU.js.map +1 -0
  15. package/dist/JsonRenderStandalone-POB4Q3N3.css +2384 -0
  16. package/dist/JsonRenderStandalone-POB4Q3N3.css.map +1 -0
  17. package/dist/JsonRenderStandalone-UsTcST4G.d.cts +23 -0
  18. package/dist/JsonRenderStandalone-UsTcST4G.d.ts +23 -0
  19. package/dist/KicadViewer-GV6ZC4AQ.js +124 -0
  20. package/dist/KicadViewer-GV6ZC4AQ.js.map +1 -0
  21. package/dist/KicadViewerCore-U7BWZHKJ.js +11 -0
  22. package/dist/KicadViewerCore-U7BWZHKJ.js.map +1 -0
  23. package/dist/PdfViewer-CHPDRK46.js +51 -0
  24. package/dist/PdfViewer-CHPDRK46.js.map +1 -0
  25. package/dist/PdfViewer-LPYGQETK.css +1899 -0
  26. package/dist/PdfViewer-LPYGQETK.css.map +1 -0
  27. package/dist/PdfViewerCore-HJPEHSRA.js +364 -0
  28. package/dist/PdfViewerCore-HJPEHSRA.js.map +1 -0
  29. package/dist/PowerPointCore-FPDR2BL4.js +11 -0
  30. package/dist/PowerPointCore-FPDR2BL4.js.map +1 -0
  31. package/dist/PowerPointViewer-LQTO6UCU.js +61 -0
  32. package/dist/PowerPointViewer-LQTO6UCU.js.map +1 -0
  33. package/dist/StepViewerCore-7W3L3R4E.js +285 -0
  34. package/dist/StepViewerCore-7W3L3R4E.js.map +1 -0
  35. package/dist/ThreeViewerCore-N3QJD5QI.js +161 -0
  36. package/dist/ThreeViewerCore-N3QJD5QI.js.map +1 -0
  37. package/dist/WordCore-JKSXK2XD.js +11 -0
  38. package/dist/WordCore-JKSXK2XD.js.map +1 -0
  39. package/dist/WordViewer-ZHCQMHOH.js +61 -0
  40. package/dist/WordViewer-ZHCQMHOH.js.map +1 -0
  41. package/dist/chunk-2SKA3F5U.js +88 -0
  42. package/dist/chunk-2SKA3F5U.js.map +1 -0
  43. package/dist/chunk-2UC7YLVX.js +318 -0
  44. package/dist/chunk-2UC7YLVX.js.map +1 -0
  45. package/dist/chunk-3R6T3LBR.js +24 -0
  46. package/dist/chunk-3R6T3LBR.js.map +1 -0
  47. package/dist/chunk-56WRZM3R.js +398 -0
  48. package/dist/chunk-56WRZM3R.js.map +1 -0
  49. package/dist/chunk-7A4FY6FK.js +10226 -0
  50. package/dist/chunk-7A4FY6FK.js.map +1 -0
  51. package/dist/chunk-7D4SUZUM.js +38 -0
  52. package/dist/chunk-7D4SUZUM.js.map +1 -0
  53. package/dist/chunk-7S67DOHQ.js +436 -0
  54. package/dist/chunk-7S67DOHQ.js.map +1 -0
  55. package/dist/chunk-CFKGNAJM.js +14013 -0
  56. package/dist/chunk-CFKGNAJM.js.map +1 -0
  57. package/dist/chunk-GAMA3VA7.js +99 -0
  58. package/dist/chunk-GAMA3VA7.js.map +1 -0
  59. package/dist/chunk-GYXTSY22.js +639 -0
  60. package/dist/chunk-GYXTSY22.js.map +1 -0
  61. package/dist/chunk-K4KGNVL5.js +77 -0
  62. package/dist/chunk-K4KGNVL5.js.map +1 -0
  63. package/dist/chunk-KQV7IKET.js +1621 -0
  64. package/dist/chunk-KQV7IKET.js.map +1 -0
  65. package/dist/chunk-O3NXUM6C.js +1871 -0
  66. package/dist/chunk-O3NXUM6C.js.map +1 -0
  67. package/dist/chunk-PZXSASDY.js +83 -0
  68. package/dist/chunk-PZXSASDY.js.map +1 -0
  69. package/dist/chunk-QLVPIM6R.js +595 -0
  70. package/dist/chunk-QLVPIM6R.js.map +1 -0
  71. package/dist/chunk-VXJWGLZ7.js +21 -0
  72. package/dist/chunk-VXJWGLZ7.js.map +1 -0
  73. package/dist/chunk-XQ562W7I.js +116 -0
  74. package/dist/chunk-XQ562W7I.js.map +1 -0
  75. package/dist/components/JsonRender/standalone.cjs +39368 -0
  76. package/dist/components/JsonRender/standalone.cjs.map +1 -0
  77. package/dist/components/JsonRender/standalone.css +2384 -0
  78. package/dist/components/JsonRender/standalone.css.map +1 -0
  79. package/dist/components/JsonRender/standalone.d.cts +16 -0
  80. package/dist/components/JsonRender/standalone.d.ts +16 -0
  81. package/dist/components/JsonRender/standalone.js +38 -0
  82. package/dist/components/JsonRender/standalone.js.map +1 -0
  83. package/dist/gerber-2d-entry-OQ4SQRBY.js +3950 -0
  84. package/dist/gerber-2d-entry-OQ4SQRBY.js.map +1 -0
  85. package/dist/gerber-3d-entry-DEHDBOO2.js +3679 -0
  86. package/dist/gerber-3d-entry-DEHDBOO2.js.map +1 -0
  87. package/dist/gerber-simulation-entry-EBDX72XE.js +1801 -0
  88. package/dist/gerber-simulation-entry-EBDX72XE.js.map +1 -0
  89. package/dist/index.cjs +60113 -2970
  90. package/dist/index.cjs.map +1 -1
  91. package/dist/index.css +11342 -1708
  92. package/dist/index.css.map +1 -1
  93. package/dist/index.d.cts +3275 -77
  94. package/dist/index.d.ts +3275 -77
  95. package/dist/index.js +18078 -2540
  96. package/dist/index.js.map +1 -1
  97. package/dist/provenance/index.cjs +2248 -0
  98. package/dist/provenance/index.cjs.map +1 -0
  99. package/dist/provenance/index.css +52 -0
  100. package/dist/provenance/index.css.map +1 -0
  101. package/dist/provenance/index.d.cts +12 -0
  102. package/dist/provenance/index.d.ts +12 -0
  103. package/dist/provenance/index.js +27 -0
  104. package/dist/provenance/index.js.map +1 -0
  105. package/dist/resolveToArrayBuffer-AQIDZHSQ.js +23 -0
  106. package/dist/resolveToArrayBuffer-AQIDZHSQ.js.map +1 -0
  107. package/package.json +98 -17
@@ -0,0 +1,1871 @@
1
+ // src/components/jetPaveGerberViewer/src/viewer-src/3dPage/js/gerber-parser.js
2
+ import { parse, plot, renderSVG } from "web-gerber";
3
+ var DEBUG_MODE = true;
4
+ var debugLog = DEBUG_MODE ? console.log.bind(console) : () => {
5
+ };
6
+ var GerberParser = class _GerberParser {
7
+ // 存储从Gerber文件中提取的格式信息,供钻孔文件使用
8
+ static gerberFormatInfo = null;
9
+ /**
10
+ * 读取文件为文本
11
+ */
12
+ static async readFileAsText(file) {
13
+ return new Promise((resolve, reject) => {
14
+ const reader = new FileReader();
15
+ reader.onload = (event) => resolve(event.target.result);
16
+ reader.onerror = (error) => reject(new Error("\u8BFB\u53D6\u6587\u4EF6\u5931\u8D25: " + error.message));
17
+ reader.readAsText(file);
18
+ });
19
+ }
20
+ /**
21
+ * 解析 Gerber 文件
22
+ * @param {File} file - Gerber 文件
23
+ * @param {string} color - 图层颜色
24
+ * @returns {Promise<{data: Object, bounds: Object}>} - 解析后的数据
25
+ */
26
+ /**
27
+ * 检查文件扩展名是否是 TX/TX+数字 格式(如 .txt, .tx1, .tx2 等)
28
+ */
29
+ static isTxFileExtension(extension) {
30
+ return extension === ".txt" || /^\.tx\d+$/i.test(extension);
31
+ }
32
+ static async parseFile(file, color) {
33
+ _GerberParser._debugPathCount = 0;
34
+ const fileNameOnly = file.name.split(/[/\\]/).pop();
35
+ const fileNameUpper = fileNameOnly.toUpperCase();
36
+ const lastDotIndex = fileNameOnly.lastIndexOf(".");
37
+ let fileExtension = "";
38
+ if (lastDotIndex > 0 && lastDotIndex < fileNameOnly.length - 1) {
39
+ fileExtension = "." + fileNameOnly.substring(lastDotIndex + 1).toLowerCase();
40
+ } else {
41
+ if (/.*_INT\d+$/i.test(fileNameUpper)) {
42
+ fileExtension = "";
43
+ } else {
44
+ fileExtension = "." + fileNameOnly.toLowerCase();
45
+ }
46
+ }
47
+ let gerberStr;
48
+ let isDrillFile = fileExtension === ".drl" || fileExtension === ".d";
49
+ let detectedFormat = null;
50
+ const isTxFile = this.isTxFileExtension(fileExtension);
51
+ const isTxtDrillFile = fileExtension === ".txt";
52
+ if (fileExtension === ".drl" || fileExtension === ".d" || isTxFile || isTxtDrillFile) {
53
+ try {
54
+ gerberStr = await this.readFileAsText(file);
55
+ if (isTxFile || fileExtension === ".d" || isTxtDrillFile) {
56
+ const trimmedContent = gerberStr.trim();
57
+ const headerLines = trimmedContent.split(/\r?\n/).slice(0, 20).map((line) => line.trim()).filter(Boolean);
58
+ const hasM48InHeader = headerLines.some((line) => /^M48$/i.test(line));
59
+ const hasM71M72InHeader = headerLines.some((line) => /^M7[12]$/i.test(line));
60
+ const hasToolDefInHeader = headerLines.some((line) => /^T\d+/i.test(line));
61
+ const isExcellonFile = hasM48InHeader || hasM71M72InHeader && hasToolDefInHeader;
62
+ if (isTxFile && isExcellonFile || fileExtension === ".d" || fileExtension === ".drl" || isTxtDrillFile && isExcellonFile) {
63
+ if (isTxFile || isTxtDrillFile) {
64
+ const fileTypeName = fileExtension.toUpperCase();
65
+ console.log(`[\u94BB\u5B54\u8BC6\u522B] ${file.name} \u662F Excellon \u94BB\u5B54\u6587\u4EF6 (M48=${hasM48InHeader}, M71/M72=${hasM71M72InHeader})`);
66
+ } else if (fileExtension === ".drl") {
67
+ debugLog(`\u68C0\u6D4B\u5230 .drl \u6587\u4EF6 ${file.name} \u662F\u94BB\u5B54\u6587\u4EF6`);
68
+ } else {
69
+ debugLog(`\u68C0\u6D4B\u5230 .d \u6587\u4EF6 ${file.name} \u662F\u94BB\u5B54\u6587\u4EF6`);
70
+ }
71
+ isDrillFile = true;
72
+ const lines = gerberStr.split("\n").map((line) => line.trim());
73
+ let hasFileFormat = false;
74
+ for (const line of lines) {
75
+ const formatMatch = line.match(/FILE_FORMAT[=:](\d+)[:.](\d+)/i);
76
+ if (formatMatch) {
77
+ hasFileFormat = true;
78
+ detectedFormat = detectedFormat || {};
79
+ const integerPlaces = parseInt(formatMatch[1]);
80
+ const decimalPlaces = parseInt(formatMatch[2]);
81
+ const totalPlaces = integerPlaces + decimalPlaces;
82
+ detectedFormat.places = [integerPlaces, decimalPlaces];
83
+ detectedFormat.integerPlaces = integerPlaces;
84
+ detectedFormat.decimalPlaces = decimalPlaces;
85
+ detectedFormat.totalPlaces = totalPlaces;
86
+ debugLog(`\u68C0\u6D4B\u5230\u5750\u6807\u683C\u5F0F: ${integerPlaces}:${decimalPlaces} (${integerPlaces}\u4F4D\u6574\u6570, ${decimalPlaces}\u4F4D\u5C0F\u6570, \u603B\u5171${totalPlaces}\u4F4D)`);
87
+ if (line.includes("INCH")) {
88
+ detectedFormat.unit = "inch";
89
+ debugLog("\u68C0\u6D4B\u5230\u5355\u4F4D: INCH");
90
+ } else if (line.includes("METRIC")) {
91
+ detectedFormat.unit = "metric";
92
+ debugLog("\u68C0\u6D4B\u5230\u5355\u4F4D: METRIC");
93
+ }
94
+ if (line.includes(",TZ") || line.includes("TZ,")) {
95
+ detectedFormat.zero = "T";
96
+ debugLog("\u68C0\u6D4B\u5230\u96F6\u683C\u5F0F: TZ (Trailing Zeros)");
97
+ } else if (line.includes(",LZ") || line.includes("LZ,")) {
98
+ detectedFormat.zero = "L";
99
+ debugLog("\u68C0\u6D4B\u5230\u96F6\u683C\u5F0F: LZ (Leading Zeros)");
100
+ }
101
+ }
102
+ }
103
+ if (!hasFileFormat) {
104
+ let hasFmat2 = false;
105
+ let hasInchTz = false;
106
+ let hasMetricLz = false;
107
+ for (const line of lines) {
108
+ if (line.match(/FMAT\s*,?\s*2/i)) {
109
+ hasFmat2 = true;
110
+ }
111
+ if (line.match(/INCH\s*,\s*TZ/i) || line.match(/TZ\s*,\s*INCH/i)) {
112
+ hasInchTz = true;
113
+ }
114
+ if (line.match(/METRIC\s*,\s*LZ/i) || line.match(/LZ\s*,\s*METRIC/i)) {
115
+ hasMetricLz = true;
116
+ }
117
+ if (line.match(/^INCH/i) || line.includes("INCH,") || line.includes(",INCH")) {
118
+ detectedFormat = detectedFormat || {};
119
+ detectedFormat.unit = "inch";
120
+ }
121
+ if (line.match(/^METRIC/i) || line.includes("METRIC,") || line.includes(",METRIC")) {
122
+ detectedFormat = detectedFormat || {};
123
+ detectedFormat.unit = "metric";
124
+ }
125
+ if (line.match(/^M72$/i)) {
126
+ detectedFormat = detectedFormat || {};
127
+ detectedFormat.unit = "inch";
128
+ }
129
+ if (line.match(/^M71$/i)) {
130
+ detectedFormat = detectedFormat || {};
131
+ detectedFormat.unit = "metric";
132
+ }
133
+ if (line.includes(",TZ") || line.includes("TZ,") || line.match(/^TZ$/i)) {
134
+ detectedFormat = detectedFormat || {};
135
+ detectedFormat.zero = "T";
136
+ }
137
+ if (line.includes(",LZ") || line.includes("LZ,") || line.match(/^LZ$/i)) {
138
+ detectedFormat = detectedFormat || {};
139
+ detectedFormat.zero = "L";
140
+ }
141
+ }
142
+ if (_GerberParser.gerberFormatInfo && _GerberParser.gerberFormatInfo.places) {
143
+ detectedFormat = detectedFormat || {};
144
+ detectedFormat.places = _GerberParser.gerberFormatInfo.places;
145
+ detectedFormat.integerPlaces = _GerberParser.gerberFormatInfo.integerPlaces;
146
+ detectedFormat.decimalPlaces = _GerberParser.gerberFormatInfo.decimalPlaces;
147
+ detectedFormat.totalPlaces = _GerberParser.gerberFormatInfo.totalPlaces;
148
+ detectedFormat.unit = detectedFormat.unit || _GerberParser.gerberFormatInfo.unit || "inch";
149
+ detectedFormat.zero = detectedFormat.zero || _GerberParser.gerberFormatInfo.zero || "L";
150
+ console.log(`[\u683C\u5F0F\u7EE7\u627F] ${file.name}: \u7EE7\u627F Gerber \u683C\u5F0F ${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}, \u5355\u4F4D=${detectedFormat.unit}, \u96F6\u683C\u5F0F=${detectedFormat.zero === "L" ? "LZ" : "TZ"}`);
151
+ } else if (hasFmat2 || hasInchTz) {
152
+ detectedFormat = detectedFormat || {};
153
+ detectedFormat.places = [2, 4];
154
+ detectedFormat.integerPlaces = 2;
155
+ detectedFormat.decimalPlaces = 4;
156
+ detectedFormat.totalPlaces = 6;
157
+ detectedFormat.unit = detectedFormat.unit || "inch";
158
+ detectedFormat.zero = detectedFormat.zero || (hasInchTz ? "T" : "L");
159
+ console.log(`[\u683C\u5F0F\u63A8\u65AD] ${file.name}: FMAT,2/INCH \u683C\u5F0F 2:4, \u5355\u4F4D=${detectedFormat.unit}, \u96F6\u683C\u5F0F=${detectedFormat.zero === "L" ? "LZ" : "TZ"}`);
160
+ } else if (hasMetricLz) {
161
+ detectedFormat = detectedFormat || {};
162
+ detectedFormat.places = [3, 3];
163
+ detectedFormat.integerPlaces = 3;
164
+ detectedFormat.decimalPlaces = 3;
165
+ detectedFormat.totalPlaces = 6;
166
+ detectedFormat.unit = "metric";
167
+ detectedFormat.zero = "L";
168
+ debugLog(`[FMAT\u683C\u5F0F] \u68C0\u6D4B\u5230 METRIC,LZ\uFF0C\u4F7F\u7528\u9ED8\u8BA4 3:3 \u683C\u5F0F (6\u4F4D)`);
169
+ } else if (_GerberParser.gerberFormatInfo) {
170
+ detectedFormat = detectedFormat || {};
171
+ detectedFormat.places = _GerberParser.gerberFormatInfo.places;
172
+ detectedFormat.integerPlaces = _GerberParser.gerberFormatInfo.integerPlaces;
173
+ detectedFormat.decimalPlaces = _GerberParser.gerberFormatInfo.decimalPlaces;
174
+ detectedFormat.totalPlaces = _GerberParser.gerberFormatInfo.totalPlaces;
175
+ detectedFormat.zero = detectedFormat.zero || _GerberParser.gerberFormatInfo.zero || "L";
176
+ detectedFormat.unit = detectedFormat.unit || _GerberParser.gerberFormatInfo.unit || "inch";
177
+ console.log(`[\u683C\u5F0F\u7EE7\u627F] ${file.name}: \u65E0FILE_FORMAT\uFF0C\u7EE7\u627F Gerber \u683C\u5F0F ${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}, \u5355\u4F4D=${detectedFormat.unit}, \u96F6\u683C\u5F0F=${detectedFormat.zero === "L" ? "LZ" : "TZ"}`);
178
+ } else {
179
+ console.warn(`[\u683C\u5F0F\u8B66\u544A] ${file.name}: \u6CA1\u6709FILE_FORMAT\uFF0C\u4E14\u6CA1\u6709\u53EF\u7528\u7684Gerber\u683C\u5F0F\u4FE1\u606F`);
180
+ }
181
+ }
182
+ }
183
+ }
184
+ const printableChars = gerberStr.split("").filter((char) => {
185
+ const code = char.charCodeAt(0);
186
+ return code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9;
187
+ }).length;
188
+ const printableRatio = printableChars / gerberStr.length;
189
+ if (printableRatio < 0.5) {
190
+ const arrayBuffer = await file.arrayBuffer();
191
+ try {
192
+ const decoder = new TextDecoder("utf-8", { fatal: false, ignoreBOM: true });
193
+ gerberStr = decoder.decode(arrayBuffer);
194
+ } catch (e) {
195
+ const decoder = new TextDecoder("latin1", { fatal: false });
196
+ gerberStr = decoder.decode(arrayBuffer);
197
+ }
198
+ }
199
+ } catch (e) {
200
+ gerberStr = await this.readFileAsText(file);
201
+ }
202
+ } else {
203
+ gerberStr = await this.readFileAsText(file);
204
+ }
205
+ if (!gerberStr || gerberStr.trim().length === 0) {
206
+ throw new Error(`\u6587\u4EF6 ${file.name} \u5185\u5BB9\u4E3A\u7A7A`);
207
+ }
208
+ if (!isDrillFile) {
209
+ const fslaMatch = gerberStr.match(/%FSLAX(\d+)(\d+)Y(\d+)(\d+)\*%/i);
210
+ if (fslaMatch) {
211
+ const xIntegerPlaces = parseInt(fslaMatch[1]);
212
+ const xDecimalPlaces = parseInt(fslaMatch[2]);
213
+ const yIntegerPlaces = parseInt(fslaMatch[3]);
214
+ const yDecimalPlaces = parseInt(fslaMatch[4]);
215
+ const formatInfo = {
216
+ integerPlaces: xIntegerPlaces,
217
+ decimalPlaces: xDecimalPlaces,
218
+ totalPlaces: xIntegerPlaces + xDecimalPlaces,
219
+ places: [xIntegerPlaces, xDecimalPlaces],
220
+ unit: gerberStr.includes("%MOIN*%") ? "inch" : "metric",
221
+ zero: gerberStr.includes("%FSLAX") ? "L" : "T"
222
+ // FSLA表示前导零省略(LZ),FSLT表示尾部零省略(TZ)
223
+ };
224
+ _GerberParser.gerberFormatInfo = formatInfo;
225
+ _GerberParser.currentFileFormatInfo = formatInfo;
226
+ debugLog(`[\u683C\u5F0F\u63D0\u53D6] \u4ECEGerber\u6587\u4EF6 ${file.name} \u63D0\u53D6\u683C\u5F0F\u4FE1\u606F: ${xIntegerPlaces}\u4F4D\u6574\u6570, ${xDecimalPlaces}\u4F4D\u5C0F\u6570`);
227
+ }
228
+ }
229
+ const originalGerberStr = gerberStr;
230
+ let hasPadded = false;
231
+ let paddedGerberStr = null;
232
+ if (isDrillFile && detectedFormat && detectedFormat.totalPlaces && detectedFormat.zero === "L") {
233
+ const totalPlaces = detectedFormat.totalPlaces;
234
+ debugLog(`[DRL\u8C03\u8BD5] \u68C0\u6D4B\u5230LZ\u683C\u5F0F\uFF0CFILE_FORMAT=${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}`);
235
+ debugLog(`[DRL\u8C03\u8BD5] \u68C0\u6D4B\u5230\u7684\u683C\u5F0F:`, {
236
+ integerPlaces: detectedFormat.integerPlaces,
237
+ decimalPlaces: detectedFormat.decimalPlaces,
238
+ totalPlaces: detectedFormat.totalPlaces,
239
+ zero: detectedFormat.zero,
240
+ unit: detectedFormat.unit
241
+ });
242
+ const coordMatchesBefore = gerberStr.match(/([XY])(\d+)/g) || [];
243
+ debugLog(`[DRL\u8C03\u8BD5] \u5904\u7406\u524D\u627E\u5230 ${coordMatchesBefore.length} \u4E2A\u5750\u6807\u503C`);
244
+ if (coordMatchesBefore.length > 0 && coordMatchesBefore.length <= 10) {
245
+ debugLog(`[DRL\u8C03\u8BD5] \u524D\u51E0\u4E2A\u5750\u6807\u503C:`, coordMatchesBefore.slice(0, 5));
246
+ }
247
+ let replaceCount = 0;
248
+ paddedGerberStr = gerberStr.replace(/([XY])(\d+)/g, (match, coord, num) => {
249
+ if (num.length < totalPlaces) {
250
+ const paddedNum = num.padStart(totalPlaces, "0");
251
+ if (replaceCount < 5) {
252
+ debugLog(`[DRL\u8C03\u8BD5] \u5750\u6807\u503C ${coord}${num} -> ${coord}${paddedNum} (\u524D\u88650\u5230${totalPlaces}\u4F4D)`);
253
+ }
254
+ replaceCount++;
255
+ return `${coord}${paddedNum}`;
256
+ }
257
+ return match;
258
+ });
259
+ debugLog(`[DRL\u8C03\u8BD5] \u5750\u6807\u503C\u9884\u5904\u7406\u5B8C\u6210\uFF0C\u5171\u5904\u7406 ${replaceCount} \u4E2A\u5750\u6807\u503C`);
260
+ if (replaceCount > 0) {
261
+ hasPadded = true;
262
+ const coordMatchesAfter = paddedGerberStr.match(/([XY])(\d+)/g) || [];
263
+ debugLog(`[DRL\u8C03\u8BD5] \u88650\u540E\u627E\u5230 ${coordMatchesAfter.length} \u4E2A\u5750\u6807\u503C`);
264
+ if (coordMatchesAfter.length > 0 && coordMatchesAfter.length <= 10) {
265
+ debugLog(`[DRL\u8C03\u8BD5] \u524D\u51E0\u4E2A\u88650\u540E\u7684\u5750\u6807\u503C:`, coordMatchesAfter.slice(0, 5));
266
+ }
267
+ }
268
+ } else if (isDrillFile && detectedFormat) {
269
+ debugLog(`[DRL\u8C03\u8BD5] \u68C0\u6D4B\u5230\u7684\u683C\u5F0F\u4FE1\u606F:`, {
270
+ integerPlaces: detectedFormat.integerPlaces,
271
+ decimalPlaces: detectedFormat.decimalPlaces,
272
+ totalPlaces: detectedFormat.totalPlaces,
273
+ zero: detectedFormat.zero,
274
+ unit: detectedFormat.unit
275
+ });
276
+ if (detectedFormat.zero !== "L") {
277
+ debugLog(`[DRL\u8C03\u8BD5] \u975E LZ \u683C\u5F0F\uFF0C\u4E0D\u8FDB\u884C\u5750\u6807\u9884\u5904\u7406`);
278
+ }
279
+ }
280
+ let parseResult;
281
+ let lastError = null;
282
+ if (isDrillFile) {
283
+ const fileTypeLabel = isTxFile ? `${fileExtension.toUpperCase()} (Excellon)` : "DRL";
284
+ debugLog(`\u5C1D\u8BD5\u89E3\u6790 ${fileTypeLabel} \u6587\u4EF6 ${file.name}...`);
285
+ debugLog(`\u6587\u4EF6\u5185\u5BB9\u9884\u89C8\uFF08\u524D200\u5B57\u7B26\uFF09:`, gerberStr.substring(0, 200));
286
+ if (detectedFormat && detectedFormat.integerPlaces && detectedFormat.decimalPlaces) {
287
+ const coordMatches = gerberStr.match(/[XY](-?\d+)/g);
288
+ if (coordMatches) {
289
+ let maxDigits = 0;
290
+ for (const coord of coordMatches) {
291
+ const digits = coord.substring(1).replace(/-/g, "").length;
292
+ maxDigits = Math.max(maxDigits, digits);
293
+ }
294
+ const definedDigits = detectedFormat.integerPlaces + detectedFormat.decimalPlaces;
295
+ debugLog(`[DRL\u683C\u5F0F\u68C0\u6D4B] ${file.name} \u5B9A\u4E49\u683C\u5F0F: [${detectedFormat.integerPlaces}, ${detectedFormat.decimalPlaces}] (${definedDigits}\u4F4D)`);
296
+ debugLog(`[DRL\u683C\u5F0F\u68C0\u6D4B] ${file.name} \u5B9E\u9645\u6700\u957F\u5750\u6807: ${maxDigits}\u4F4D`);
297
+ if (maxDigits > definedDigits) {
298
+ const extraDigits = maxDigits - definedDigits;
299
+ detectedFormat.integerPlaces += extraDigits;
300
+ detectedFormat.totalPlaces = detectedFormat.integerPlaces + detectedFormat.decimalPlaces;
301
+ detectedFormat.places = [detectedFormat.integerPlaces, detectedFormat.decimalPlaces];
302
+ debugLog(`[DRL\u683C\u5F0F\u4FEE\u6B63] \u2713 \u52A8\u6001\u8C03\u6574\u683C\u5F0F\u4E3A: [${detectedFormat.integerPlaces}, ${detectedFormat.decimalPlaces}] (${maxDigits}\u4F4D)`);
303
+ const oldFormatPattern = /FILE_FORMAT[=:]\d+[:.]\d+/i;
304
+ const newFormatStr = `FILE_FORMAT=${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}`;
305
+ if (oldFormatPattern.test(gerberStr)) {
306
+ gerberStr = gerberStr.replace(oldFormatPattern, newFormatStr);
307
+ debugLog(`[DRL\u683C\u5F0F\u4FEE\u6B63] \u2713 \u5DF2\u4FEE\u6539\u6587\u4EF6\u683C\u5F0F\u5B9A\u4E49 \u2192 ${newFormatStr}`);
308
+ if (paddedGerberStr) {
309
+ paddedGerberStr = paddedGerberStr.replace(oldFormatPattern, newFormatStr);
310
+ }
311
+ }
312
+ }
313
+ }
314
+ }
315
+ if (isTxFile && detectedFormat && detectedFormat.places) {
316
+ const parseAttempts = [];
317
+ parseAttempts.push({
318
+ gerberStr: originalGerberStr,
319
+ // 使用原始内容
320
+ format: {
321
+ places: detectedFormat.places,
322
+ zero: detectedFormat.zero || "T"
323
+ // 使用原始检测到的格式(LZ)
324
+ },
325
+ description: "\u4E0D\u88650\uFF0C\u76F4\u63A5\u4F7F\u7528LZ\u683C\u5F0F"
326
+ });
327
+ if (hasPadded && paddedGerberStr) {
328
+ parseAttempts.push({
329
+ gerberStr: paddedGerberStr,
330
+ // 使用补0后的内容
331
+ format: {
332
+ places: detectedFormat.places,
333
+ zero: "T"
334
+ // 补0后使用TZ格式
335
+ },
336
+ description: "\u88650\u540E\u4F7F\u7528TZ\u683C\u5F0F"
337
+ });
338
+ }
339
+ if (hasPadded && paddedGerberStr) {
340
+ parseAttempts.push({
341
+ gerberStr: paddedGerberStr,
342
+ // 使用补0后的内容
343
+ format: {
344
+ places: detectedFormat.places
345
+ // 不指定zero参数
346
+ },
347
+ description: "\u88650\u540E\u4E0D\u6307\u5B9Azero\u53C2\u6570"
348
+ });
349
+ }
350
+ parseAttempts.push({
351
+ gerberStr: originalGerberStr,
352
+ // 使用原始内容
353
+ format: {
354
+ places: detectedFormat.places
355
+ // 不指定zero参数
356
+ },
357
+ description: "\u4E0D\u88650\uFF0C\u4E0D\u6307\u5B9Azero\u53C2\u6570"
358
+ });
359
+ for (const attempt of parseAttempts) {
360
+ try {
361
+ const formatOptions = {
362
+ type: "drill",
363
+ format: attempt.format
364
+ };
365
+ debugLog(`[DRL\u8C03\u8BD5] \u5C1D\u8BD5\u89E3\u6790\u65B9\u5F0F: ${attempt.description}`, formatOptions.format);
366
+ parseResult = parse(attempt.gerberStr, formatOptions);
367
+ debugLog(`[DRL\u8C03\u8BD5] ${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 ${attempt.description} \u89E3\u6790\u6210\u529F`);
368
+ debugLog(`[DRL\u8C03\u8BD5] parseResult \u7C7B\u578B:`, parseResult?.type);
369
+ debugLog(`[DRL\u8C03\u8BD5] parseResult \u5DE5\u5177\u6570\u91CF:`, parseResult?.tools ? Object.keys(parseResult.tools).length : 0);
370
+ if (parseResult?.tools && Object.keys(parseResult.tools).length > 0) {
371
+ let totalHoles = 0;
372
+ for (const tool of Object.values(parseResult.tools)) {
373
+ if (tool.holes) {
374
+ totalHoles += tool.holes.length;
375
+ }
376
+ }
377
+ debugLog(`[DRL\u8C03\u8BD5] \u89E3\u6790\u51FA\u7684\u5B54\u603B\u6570: ${totalHoles}`);
378
+ }
379
+ break;
380
+ } catch (e0) {
381
+ lastError = e0;
382
+ debugLog(`[DRL\u8C03\u8BD5] ${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 ${attempt.description} \u89E3\u6790\u5931\u8D25: ${e0.message}`);
383
+ }
384
+ }
385
+ }
386
+ if (!parseResult) {
387
+ try {
388
+ parseResult = parse(gerberStr);
389
+ debugLog(`[DRL\u8C03\u8BD5] ${fileTypeLabel} \u6587\u4EF6\u76F4\u63A5\u89E3\u6790\u6210\u529F`);
390
+ debugLog(`[DRL\u8C03\u8BD5] parseResult \u7C7B\u578B:`, parseResult?.type);
391
+ debugLog(`[DRL\u8C03\u8BD5] parseResult \u5DE5\u5177\u6570\u91CF:`, parseResult?.tools ? Object.keys(parseResult.tools).length : 0);
392
+ } catch (e1) {
393
+ lastError = e1;
394
+ debugLog(`[DRL\u8C03\u8BD5] ${fileTypeLabel} \u6587\u4EF6\u76F4\u63A5\u89E3\u6790\u5931\u8D25: ${e1.message}`);
395
+ try {
396
+ parseResult = parse(gerberStr, { type: "drill" });
397
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 drill \u7C7B\u578B\u89E3\u6790\u6210\u529F`);
398
+ } catch (e2) {
399
+ lastError = e2;
400
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 drill \u7C7B\u578B\u89E3\u6790\u5931\u8D25: ${e2.message}`);
401
+ try {
402
+ parseResult = parse(gerberStr, {
403
+ type: "drill",
404
+ format: {
405
+ places: [4, 5],
406
+ // 常见的小数位数
407
+ zero: "L"
408
+ // Leading zeros
409
+ }
410
+ });
411
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u683C\u5F0F\u53C2\u6570\u89E3\u6790\u6210\u529F`);
412
+ } catch (e2_5) {
413
+ lastError = e2_5;
414
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u683C\u5F0F\u53C2\u6570\u89E3\u6790\u5931\u8D25: ${e2_5.message}`);
415
+ if (isTxFile && detectedFormat) {
416
+ const formatAttempts = [];
417
+ if (detectedFormat.places) {
418
+ formatAttempts.push({
419
+ places: detectedFormat.places,
420
+ zero: detectedFormat.zero || "T",
421
+ description: `[\u6574\u6570\u4F4D\u6570${detectedFormat.integerPlaces}, \u5C0F\u6570\u4F4D\u6570${detectedFormat.decimalPlaces}]`
422
+ });
423
+ debugLog(`[DRL\u8C03\u8BD5] \u5C06\u5C1D\u8BD5\u4F7F\u7528\u683C\u5F0F: places=[${detectedFormat.places[0]}, ${detectedFormat.places[1]}], zero=${detectedFormat.zero || "T"}`);
424
+ }
425
+ if (formatAttempts.length === 0) {
426
+ formatAttempts.push({
427
+ places: [4, 5],
428
+ zero: detectedFormat.zero || "T",
429
+ description: "\u9ED8\u8BA4\u683C\u5F0F[4, 5]"
430
+ });
431
+ debugLog(`[DRL\u8C03\u8BD5] \u672A\u68C0\u6D4B\u5230\u683C\u5F0F\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u683C\u5F0F`);
432
+ }
433
+ let parsed = false;
434
+ for (const formatOption of formatAttempts) {
435
+ try {
436
+ const formatOptions = {
437
+ type: "drill",
438
+ format: {
439
+ places: formatOption.places,
440
+ zero: formatOption.zero
441
+ }
442
+ };
443
+ parseResult = parse(gerberStr, formatOptions);
444
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u683C\u5F0F ${formatOption.description} \u89E3\u6790\u6210\u529F:`, formatOptions.format);
445
+ parsed = true;
446
+ break;
447
+ } catch (e) {
448
+ lastError = e;
449
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u683C\u5F0F ${formatOption.description} \u89E3\u6790\u5931\u8D25: ${e.message}`);
450
+ }
451
+ }
452
+ if (!parsed) {
453
+ try {
454
+ parseResult = parse(gerberStr, {
455
+ type: "drill",
456
+ format: {
457
+ places: [4, 5],
458
+ zero: "T"
459
+ // Trailing zeros
460
+ }
461
+ });
462
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u9ED8\u8BA4 TZ \u683C\u5F0F\u89E3\u6790\u6210\u529F`);
463
+ parsed = true;
464
+ } catch (e2_6_default) {
465
+ lastError = e2_6_default;
466
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528\u9ED8\u8BA4 TZ \u683C\u5F0F\u89E3\u6790\u5931\u8D25: ${e2_6_default.message}`);
467
+ }
468
+ }
469
+ if (!parsed) {
470
+ try {
471
+ const cleanedStr = gerberStr.split("").filter((char) => {
472
+ const code = char.charCodeAt(0);
473
+ return code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9;
474
+ }).join("");
475
+ if (cleanedStr.length > 0) {
476
+ parseResult = parse(cleanedStr, { type: "drill" });
477
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6E05\u7406\u540E\u89E3\u6790\u6210\u529F`);
478
+ } else {
479
+ throw new Error("\u6E05\u7406\u540E\u6587\u4EF6\u5185\u5BB9\u4E3A\u7A7A");
480
+ }
481
+ } catch (e3) {
482
+ lastError = e3;
483
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6E05\u7406\u540E\u89E3\u6790\u5931\u8D25: ${e3.message}`);
484
+ try {
485
+ const aggressiveCleaned = gerberStr.split("").map((char) => {
486
+ const code = char.charCodeAt(0);
487
+ if (code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9) {
488
+ return char;
489
+ }
490
+ return " ";
491
+ }).join("").replace(/\s+/g, " ").trim();
492
+ if (aggressiveCleaned.length > 10) {
493
+ parseResult = parse(aggressiveCleaned, { type: "drill" });
494
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6FC0\u8FDB\u6E05\u7406\u540E\u89E3\u6790\u6210\u529F`);
495
+ } else {
496
+ throw new Error("\u6FC0\u8FDB\u6E05\u7406\u540E\u5185\u5BB9\u4E0D\u8DB3");
497
+ }
498
+ } catch (e4) {
499
+ lastError = e4;
500
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6FC0\u8FDB\u6E05\u7406\u540E\u89E3\u6790\u5931\u8D25: ${e4.message}`);
501
+ const possibleTypes = ["outline", "top", "bottom", "unknown"];
502
+ let parsed2 = false;
503
+ for (const type of possibleTypes) {
504
+ try {
505
+ parseResult = parse(gerberStr, { type });
506
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 ${type} \u7C7B\u578B\u89E3\u6790\u6210\u529F`);
507
+ parsed2 = true;
508
+ break;
509
+ } catch (e) {
510
+ lastError = e;
511
+ }
512
+ }
513
+ if (!parsed2) {
514
+ console.warn(`${fileTypeLabel} \u6587\u4EF6 ${file.name} \u65E0\u6CD5\u88AB web-gerber \u89E3\u6790`);
515
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790 ${fileTypeLabel} \u6587\u4EF6 ${file.name}\u3002\u6587\u4EF6\u53EF\u80FD\u662F\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08Excellon\uFF09\uFF0Cweb-gerber \u4EC5\u652F\u6301\u6587\u672C\u683C\u5F0F\u7684 Gerber \u6587\u4EF6\u3002\u5DF2\u5C1D\u8BD5\u591A\u79CD\u65B9\u5F0F\uFF0C\u6700\u540E\u9519\u8BEF: ${lastError?.message || "\u672A\u77E5\u9519\u8BEF"}`);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ } else {
521
+ try {
522
+ const cleanedStr = gerberStr.split("").filter((char) => {
523
+ const code = char.charCodeAt(0);
524
+ return code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9;
525
+ }).join("");
526
+ if (cleanedStr.length > 0) {
527
+ parseResult = parse(cleanedStr);
528
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6E05\u7406\u540E\u89E3\u6790\u6210\u529F`);
529
+ } else {
530
+ throw new Error("\u6E05\u7406\u540E\u6587\u4EF6\u5185\u5BB9\u4E3A\u7A7A");
531
+ }
532
+ } catch (e3) {
533
+ lastError = e3;
534
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6E05\u7406\u540E\u89E3\u6790\u5931\u8D25: ${e3.message}`);
535
+ try {
536
+ const aggressiveCleaned = gerberStr.split("").map((char) => {
537
+ const code = char.charCodeAt(0);
538
+ if (code >= 32 && code <= 126 || code === 10 || code === 13 || code === 9) {
539
+ return char;
540
+ }
541
+ return " ";
542
+ }).join("").replace(/\s+/g, " ").trim();
543
+ if (aggressiveCleaned.length > 10) {
544
+ parseResult = parse(aggressiveCleaned);
545
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6FC0\u8FDB\u6E05\u7406\u540E\u89E3\u6790\u6210\u529F`);
546
+ } else {
547
+ throw new Error("\u6FC0\u8FDB\u6E05\u7406\u540E\u5185\u5BB9\u4E0D\u8DB3");
548
+ }
549
+ } catch (e4) {
550
+ lastError = e4;
551
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u6FC0\u8FDB\u6E05\u7406\u540E\u89E3\u6790\u5931\u8D25: ${e4.message}`);
552
+ const possibleTypes = ["outline", "top", "bottom", "unknown"];
553
+ let parsed = false;
554
+ for (const type of possibleTypes) {
555
+ try {
556
+ parseResult = parse(gerberStr, { type });
557
+ debugLog(`${fileTypeLabel} \u6587\u4EF6\u4F7F\u7528 ${type} \u7C7B\u578B\u89E3\u6790\u6210\u529F`);
558
+ parsed = true;
559
+ break;
560
+ } catch (e) {
561
+ lastError = e;
562
+ }
563
+ }
564
+ if (!parsed) {
565
+ console.warn(`${fileTypeLabel} \u6587\u4EF6 ${file.name} \u65E0\u6CD5\u88AB web-gerber \u89E3\u6790\uFF0C\u53EF\u80FD\u662F\u4E8C\u8FDB\u5236\u683C\u5F0F`);
566
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790 ${fileTypeLabel} \u6587\u4EF6 ${file.name}\u3002\u6587\u4EF6\u53EF\u80FD\u662F\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08Excellon\uFF09\uFF0Cweb-gerber \u4EC5\u652F\u6301\u6587\u672C\u683C\u5F0F\u7684 Gerber \u6587\u4EF6\u3002\u5DF2\u5C1D\u8BD5\u591A\u79CD\u65B9\u5F0F\uFF0C\u6700\u540E\u9519\u8BEF: ${lastError?.message || "\u672A\u77E5\u9519\u8BEF"}`);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ }
574
+ }
575
+ } else {
576
+ try {
577
+ debugLog(`[\u89E3\u6790\u8C03\u8BD5] \u6587\u4EF6 ${file.name}, currentFileFormatInfo\u5B58\u5728: ${!!_GerberParser.currentFileFormatInfo}`);
578
+ if (_GerberParser.currentFileFormatInfo) {
579
+ let integerPlaces = _GerberParser.currentFileFormatInfo.integerPlaces;
580
+ let decimalPlaces = _GerberParser.currentFileFormatInfo.decimalPlaces;
581
+ const coordMatches = gerberStr.match(/[XY](-?\d+)/g);
582
+ if (coordMatches) {
583
+ let maxDigits = 0;
584
+ for (const coord of coordMatches) {
585
+ const digits = coord.substring(1).replace(/-/g, "").length;
586
+ maxDigits = Math.max(maxDigits, digits);
587
+ }
588
+ const definedDigits = integerPlaces + decimalPlaces;
589
+ debugLog(`[\u683C\u5F0F\u68C0\u6D4B] ${file.name} \u5B9A\u4E49\u683C\u5F0F: [${integerPlaces}, ${decimalPlaces}] (${definedDigits}\u4F4D)`);
590
+ debugLog(`[\u683C\u5F0F\u68C0\u6D4B] ${file.name} \u5B9E\u9645\u6700\u957F\u5750\u6807: ${maxDigits}\u4F4D`);
591
+ if (maxDigits > definedDigits) {
592
+ const extraDigits = maxDigits - definedDigits;
593
+ integerPlaces += extraDigits;
594
+ debugLog(`[\u683C\u5F0F\u4FEE\u6B63] \u2713 \u52A8\u6001\u8C03\u6574\u683C\u5F0F\u4E3A: [${integerPlaces}, ${decimalPlaces}] (${maxDigits}\u4F4D)`);
595
+ debugLog(`[\u683C\u5F0F\u4FEE\u6B63] \u8FD9\u68378\u4F4D\u5750\u6807 X10405403 \u4F1A\u88AB\u89E3\u6790\u4E3A ${(10405403 / Math.pow(10, decimalPlaces)).toFixed(decimalPlaces)}"`);
596
+ const oldFormat = `%FSLAX${_GerberParser.currentFileFormatInfo.integerPlaces}${decimalPlaces}Y${_GerberParser.currentFileFormatInfo.integerPlaces}${decimalPlaces}*%`;
597
+ const newFormat = `%FSLAX${integerPlaces}${decimalPlaces}Y${integerPlaces}${decimalPlaces}*%`;
598
+ if (gerberStr.includes(oldFormat)) {
599
+ gerberStr = gerberStr.replace(oldFormat, newFormat);
600
+ debugLog(`[\u683C\u5F0F\u4FEE\u6B63] \u2713 \u5DF2\u4FEE\u6539\u6587\u4EF6\u683C\u5F0F\u5B9A\u4E49: ${oldFormat} \u2192 ${newFormat}`);
601
+ } else {
602
+ debugLog(`[\u683C\u5F0F\u4FEE\u6B63] \u26A0\uFE0F \u672A\u627E\u5230\u683C\u5F0F\u5B9A\u4E49\u5B57\u7B26\u4E32\uFF0C\u5C1D\u8BD5\u901A\u7528\u66FF\u6362`);
603
+ gerberStr = gerberStr.replace(/%FSLAX\d+\d+Y\d+\d+\*%/i, newFormat);
604
+ }
605
+ }
606
+ }
607
+ const formatOptions = {
608
+ format: {
609
+ places: [integerPlaces, decimalPlaces],
610
+ zero: _GerberParser.currentFileFormatInfo.zero
611
+ }
612
+ };
613
+ debugLog(`[Gerber\u89E3\u6790] ${file.name} \u6700\u7EC8\u4F7F\u7528\u683C\u5F0F: places=[${integerPlaces}, ${decimalPlaces}], zero=${_GerberParser.currentFileFormatInfo.zero}`);
614
+ parseResult = parse(gerberStr, formatOptions);
615
+ } else {
616
+ parseResult = parse(gerberStr);
617
+ }
618
+ } catch (parseError) {
619
+ const fileTypeMap = {
620
+ ".gtl": "top",
621
+ ".gbl": "bottom",
622
+ ".gts": "topsolder",
623
+ ".gbs": "bottomsolder",
624
+ ".gto": "topsilkscreen",
625
+ ".gbo": "bottomsilkscreen",
626
+ ".gtp": "toppaste",
627
+ ".gbp": "bottompaste",
628
+ ".gm1": "outline",
629
+ ".gko": "outline",
630
+ ".gdl": "outline",
631
+ ".gdd": "outline",
632
+ ".gm": "outline",
633
+ ".gd1": "outline",
634
+ ".gg1": "outline",
635
+ ".gpb": "bottompaste",
636
+ ".gpt": "toppaste"
637
+ };
638
+ let fileType = fileTypeMap[fileExtension];
639
+ if (!fileType && /^\.gm\d+$/i.test(fileExtension)) {
640
+ fileType = "outline";
641
+ }
642
+ if (!fileType && /^\.int\d+$/i.test(fileExtension)) {
643
+ fileType = "top";
644
+ }
645
+ if (!fileType && fileExtension === "" && /.*_INT\d+$/i.test(fileNameUpper)) {
646
+ fileType = "top";
647
+ }
648
+ fileType = fileType || "unknown";
649
+ try {
650
+ parseResult = parse(gerberStr, { type: fileType });
651
+ } catch (typeError) {
652
+ if (fileType === "unknown" || /^\.int\d+$/i.test(fileExtension) || fileExtension === "" && /.*_INT\d+$/i.test(fileNameUpper)) {
653
+ try {
654
+ parseResult = parse(gerberStr);
655
+ } catch (finalError) {
656
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790\u6587\u4EF6 ${file.name}\u3002\u9519\u8BEF: ${parseError.message || typeError.message || finalError.message}`);
657
+ }
658
+ } else {
659
+ throw new Error(`\u65E0\u6CD5\u89E3\u6790\u6587\u4EF6 ${file.name}\u3002\u9519\u8BEF: ${parseError.message || typeError.message}`);
660
+ }
661
+ }
662
+ }
663
+ }
664
+ const plotResult = plot(parseResult);
665
+ debugLog(`[DRL\u8C03\u8BD5] \u6587\u4EF6 ${file.name} \u89E3\u6790\u6210\u529F\uFF0CplotResult:`, plotResult);
666
+ if (!isDrillFile && plotResult.tools && Object.keys(plotResult.tools).length > 0 && file.name.toUpperCase().includes("GTL")) {
667
+ debugLog(`[\u5149\u5708\u8C03\u8BD5] \u6587\u4EF6 ${file.name} \u5305\u542B ${Object.keys(plotResult.tools).length} \u4E2A\u5DE5\u5177\u5B9A\u4E49:`);
668
+ const toolEntries = Object.entries(plotResult.tools).slice(0, 5);
669
+ toolEntries.forEach(([id, tool]) => {
670
+ debugLog(` \u5DE5\u5177 ${id}:`, JSON.stringify(tool, null, 2));
671
+ });
672
+ if (plotResult.children && plotResult.children.length > 0) {
673
+ const typeCount = {};
674
+ const polarityCount = { dark: 0, clear: 0 };
675
+ plotResult.children.forEach((child) => {
676
+ typeCount[child.type] = (typeCount[child.type] || 0) + 1;
677
+ if (child.polarity) {
678
+ polarityCount[child.polarity] = (polarityCount[child.polarity] || 0) + 1;
679
+ }
680
+ });
681
+ debugLog(`[Children\u7EDF\u8BA1] GTL\u6587\u4EF6 ${plotResult.children.length} \u4E2A children:`);
682
+ debugLog(` \u7C7B\u578B\u7EDF\u8BA1:`, typeCount);
683
+ debugLog(` \u6781\u6027\u7EDF\u8BA1:`, polarityCount);
684
+ const types = Object.keys(typeCount);
685
+ types.forEach((type) => {
686
+ const example = plotResult.children.find((c) => c.type === type);
687
+ if (example) {
688
+ debugLog(` ${type} \u793A\u4F8B:`, JSON.stringify({
689
+ type: example.type,
690
+ tool: example.tool,
691
+ dcode: example.dcode,
692
+ polarity: example.polarity,
693
+ segmentsCount: example.segments?.length,
694
+ keys: Object.keys(example)
695
+ }, null, 2));
696
+ }
697
+ });
698
+ }
699
+ }
700
+ if (isDrillFile && plotResult) {
701
+ debugLog(`[DRL\u8C03\u8BD5] plotResult \u8BE6\u7EC6\u4FE1\u606F:`);
702
+ debugLog(` - type: ${plotResult.type}`);
703
+ debugLog(` - units: ${plotResult.units}`);
704
+ debugLog(` - size:`, plotResult.size);
705
+ debugLog(` - tools:`, plotResult.tools ? Object.keys(plotResult.tools).length + " \u4E2A\u5DE5\u5177" : "\u65E0");
706
+ if (plotResult.tools) {
707
+ const toolKeys = Object.keys(plotResult.tools).slice(0, 5);
708
+ toolKeys.forEach((key) => {
709
+ debugLog(` - T${key}: \u76F4\u5F84 ${plotResult.tools[key]?.diameter || "\u672A\u77E5"}`);
710
+ });
711
+ }
712
+ debugLog(` - children\u6570\u91CF: ${plotResult.children?.length || 0}`);
713
+ }
714
+ const svgTree = renderSVG(plotResult);
715
+ debugLog(`[DRL\u8C03\u8BD5] \u6587\u4EF6 ${file.name} SVG\u6811\u751F\u6210\u6210\u529F\uFF0Cchildren\u6570\u91CF:`, svgTree?.children?.length || 0);
716
+ if (isDrillFile && svgTree?.children) {
717
+ let circleCount = 0;
718
+ const countCircles = (element) => {
719
+ if (!element) return;
720
+ if (element.tagName?.toLowerCase() === "circle") {
721
+ circleCount++;
722
+ }
723
+ if (element.children) {
724
+ element.children.forEach((child) => countCircles(child));
725
+ }
726
+ };
727
+ svgTree.children.forEach((child) => countCircles(child));
728
+ debugLog(`[DRL\u8C03\u8BD5] SVG\u6811\u4E2D\u5706\u5F62\u5143\u7D20\uFF08\u5B54\uFF09\u6570\u91CF: ${circleCount}`);
729
+ if (circleCount > 0) {
730
+ const circles = [];
731
+ const extractCircles = (element) => {
732
+ if (!element) return;
733
+ if (element.tagName?.toLowerCase() === "circle") {
734
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
735
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
736
+ const r = parseFloat(element.properties?.r || element.attributes?.r || 0);
737
+ circles.push({ cx, cy, r });
738
+ }
739
+ if (element.children) {
740
+ element.children.forEach((child) => extractCircles(child));
741
+ }
742
+ };
743
+ svgTree.children.forEach((child) => extractCircles(child));
744
+ debugLog(`[DRL\u8C03\u8BD5] \u524D5\u4E2A\u5B54\u7684\u5750\u6807:`);
745
+ circles.slice(0, 5).forEach((circle, idx) => {
746
+ debugLog(` ${idx + 1}. \u4E2D\u5FC3: (${circle.cx.toFixed(6)}, ${circle.cy.toFixed(6)}), \u534A\u5F84: ${circle.r.toFixed(6)}`);
747
+ });
748
+ if (detectedFormat && detectedFormat.totalPlaces && circles.length > 0) {
749
+ try {
750
+ const xyMatch = gerberStr.match(/X(-?\d+)\s*Y(-?\d+)/i);
751
+ console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u9996\u4E2AXY=${xyMatch ? xyMatch[0] : "\u672A\u627E\u5230"}, circles[0]=(${circles[0]?.cx?.toFixed(4)}, ${circles[0]?.cy?.toFixed(4)})`);
752
+ if (xyMatch && circles[0]) {
753
+ const actX = circles[0].cx;
754
+ const actY = circles[0].cy;
755
+ const parseFixedCoord = (rawDigits, fmt, zeroMode) => {
756
+ if (!fmt || rawDigits == null) return null;
757
+ const s = String(rawDigits).trim();
758
+ const sign = s.startsWith("-") ? -1 : 1;
759
+ let digits = s.replace(/[^0-9]/g, "");
760
+ if (!digits) return null;
761
+ const need = fmt.totalPlaces;
762
+ if (need && digits.length < need) {
763
+ if ((zeroMode || "").toUpperCase() === "L" || zeroMode === "LZ") {
764
+ digits = digits.padStart(need, "0");
765
+ } else {
766
+ digits = digits.padEnd(need, "0");
767
+ }
768
+ }
769
+ const denom = Math.pow(10, fmt.decimalPlaces || 0);
770
+ const n = parseInt(digits, 10);
771
+ if (!Number.isFinite(n) || denom === 0) return null;
772
+ return sign * (n / denom);
773
+ };
774
+ const plotUnits = plotResult && plotResult.units === "in" ? "in" : "mm";
775
+ const srcUnits = detectedFormat.unit === "inch" ? "in" : detectedFormat.unit === "metric" ? "mm" : plotUnits;
776
+ const evalZeroMode = (zeroMode) => {
777
+ let expX = parseFixedCoord(xyMatch[1], detectedFormat, zeroMode);
778
+ let expY = parseFixedCoord(xyMatch[2], detectedFormat, zeroMode);
779
+ if (expX != null && expY != null) {
780
+ if (srcUnits === "in" && plotUnits === "mm") {
781
+ expX *= 25.4;
782
+ expY *= 25.4;
783
+ }
784
+ if (srcUnits === "mm" && plotUnits === "in") {
785
+ expX /= 25.4;
786
+ expY /= 25.4;
787
+ }
788
+ }
789
+ const ratios = [];
790
+ if (Number.isFinite(expX) && Math.abs(actX) > 1e-4) ratios.push(Math.abs(expX) / Math.abs(actX));
791
+ if (Number.isFinite(expY) && Math.abs(actY) > 1e-4) ratios.push(Math.abs(expY) / Math.abs(actY));
792
+ if (ratios.length === 0) return null;
793
+ const avg = ratios.reduce((s, v) => s + v, 0) / ratios.length;
794
+ const max = Math.max(...ratios);
795
+ const min = Math.min(...ratios);
796
+ const spread = avg !== 0 ? Math.abs(max - min) / Math.abs(avg) : 999;
797
+ return { zeroMode, expX, expY, avg, spread };
798
+ };
799
+ const primaryZero = detectedFormat.zero === "T" ? "TZ" : "LZ";
800
+ const altZero = primaryZero === "TZ" ? "LZ" : "TZ";
801
+ const primary = evalZeroMode(primaryZero);
802
+ const alt = evalZeroMode(altZero);
803
+ console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u683C\u5F0F=${detectedFormat.integerPlaces}:${detectedFormat.decimalPlaces}, \u5B9E\u9645=(${actX.toFixed(4)}, ${actY.toFixed(4)}), plotUnits=${plotUnits}, srcUnits=${srcUnits}`);
804
+ if (primary) console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: ${primaryZero}\u6A21\u5F0F \u671F\u671B=(${primary.expX?.toFixed(4)}, ${primary.expY?.toFixed(4)}), ratio=${primary.avg?.toFixed(4)}, spread=${primary.spread?.toFixed(4)}`);
805
+ if (alt) console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: ${altZero}\u6A21\u5F0F \u671F\u671B=(${alt.expX?.toFixed(4)}, ${alt.expY?.toFixed(4)}), ratio=${alt.avg?.toFixed(4)}, spread=${alt.spread?.toFixed(4)}`);
806
+ const near1 = (r) => r && Number.isFinite(r.avg) && r.spread < 0.05 && Math.abs(r.avg - 1) <= 0.05;
807
+ if (!near1(primary) && !near1(alt)) {
808
+ const chosen = primary && alt ? primary.spread <= alt.spread ? primary : alt : primary || alt;
809
+ if (chosen && Number.isFinite(chosen.avg) && chosen.spread < 0.05 && Math.abs(chosen.avg - 1) > 0.05) {
810
+ const absAvg = Math.abs(chosen.avg);
811
+ const pow10 = Math.pow(10, Math.round(Math.log10(absAvg)));
812
+ const snapped = Math.abs(absAvg - pow10) / pow10 < 0.15 ? pow10 : absAvg;
813
+ if (snapped >= 0.01 && snapped <= 1e4 && Math.abs(snapped - 1) > 0.05) {
814
+ console.warn(`[\u94BB\u5B54\u5750\u6807\u4FEE\u6B63] ${file.name}: \u68C0\u6D4B\u5230 ${snapped.toFixed(2)}x \u7F29\u653E\u504F\u5DEE\uFF0C\u4FEE\u6B63\u5706\u5FC3\u5750\u6807 (plotUnits=${plotUnits}, srcUnits=${srcUnits}, zero=${chosen.zeroMode})`);
815
+ const applyFix = (node) => {
816
+ if (!node) return;
817
+ const tag = node.tagName?.toLowerCase();
818
+ if (tag === "circle" || tag === "ellipse") {
819
+ const p = node.properties || (node.properties = {});
820
+ const cx = parseFloat(p.cx ?? node.attributes?.cx ?? 0);
821
+ const cy = parseFloat(p.cy ?? node.attributes?.cy ?? 0);
822
+ if (Number.isFinite(cx)) p.cx = String(cx * snapped);
823
+ if (Number.isFinite(cy)) p.cy = String(cy * snapped);
824
+ }
825
+ if (node.children) node.children.forEach(applyFix);
826
+ };
827
+ svgTree.children.forEach((child) => applyFix(child));
828
+ }
829
+ }
830
+ } else if (near1(alt) && !near1(primary)) {
831
+ console.warn(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u96F6\u6291\u5236\u58F0\u660E\u4E3A ${primaryZero}\uFF0C\u4F46\u5750\u6807\u66F4\u7B26\u5408 ${altZero}\uFF08\u65E0\u9700\u4FEE\u6B63\uFF09`);
832
+ } else if (near1(primary)) {
833
+ console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u2713 ${primaryZero}\u6A21\u5F0F\u5339\u914D\u826F\u597D (ratio\u22481)\uFF0C\u65E0\u9700\u4FEE\u6B63`);
834
+ } else {
835
+ console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u672A\u89E6\u53D1\u4FEE\u6B63 (near1(primary)=${near1(primary)}, near1(alt)=${near1(alt)})`);
836
+ }
837
+ }
838
+ } catch (e) {
839
+ console.warn(`[\u94BB\u5B54\u5750\u6807\u4FEE\u6B63] ${file.name}: posScaleFix \u8BA1\u7B97\u5931\u8D25:`, e?.message || e);
840
+ }
841
+ } else {
842
+ console.log(`[\u94BB\u5B54\u5750\u6807\u68C0\u67E5] ${file.name}: \u8DF3\u8FC7\u68C0\u67E5 (detectedFormat=${!!detectedFormat}, totalPlaces=${detectedFormat?.totalPlaces}, circles=${circles.length})`);
843
+ }
844
+ if (detectedFormat && detectedFormat.totalPlaces && detectedFormat.zero === "L") {
845
+ debugLog(`[DRL\u8C03\u8BD5] \u5F00\u59CB\u9A8C\u8BC1\u5750\u6807\u89E3\u6790...`);
846
+ const originalContent = await this.readFileAsText(file);
847
+ const originalCoords = originalContent.match(/([XY])(\d+)/g) || [];
848
+ if (originalCoords.length > 0) {
849
+ debugLog(`[DRL\u8C03\u8BD5] \u4ECE\u539F\u59CB\u6587\u4EF6\u4E2D\u63D0\u53D6\u7684\u524D5\u4E2A\u5750\u6807\u503C:`);
850
+ const sampleCoords = originalCoords.slice(0, 10);
851
+ for (let i = 0; i < Math.min(5, Math.floor(sampleCoords.length / 2)); i++) {
852
+ const xCoord = sampleCoords[i * 2];
853
+ const yCoord = sampleCoords[i * 2 + 1];
854
+ if (xCoord && yCoord) {
855
+ const xMatch = xCoord.match(/X(\d+)/);
856
+ const yMatch = yCoord.match(/Y(\d+)/);
857
+ if (xMatch && yMatch) {
858
+ const xRaw = xMatch[1];
859
+ const yRaw = yMatch[1];
860
+ const xPadded = xRaw.padStart(detectedFormat.totalPlaces, "0");
861
+ const yPadded = yRaw.padStart(detectedFormat.totalPlaces, "0");
862
+ const xInteger = parseInt(xPadded.substring(0, detectedFormat.integerPlaces));
863
+ const xDecimal = parseInt(xPadded.substring(detectedFormat.integerPlaces)) / Math.pow(10, detectedFormat.decimalPlaces);
864
+ const xExpected = xInteger + xDecimal;
865
+ const yInteger = parseInt(yPadded.substring(0, detectedFormat.integerPlaces));
866
+ const yDecimal = parseInt(yPadded.substring(detectedFormat.integerPlaces)) / Math.pow(10, detectedFormat.decimalPlaces);
867
+ const yExpected = yInteger + yDecimal;
868
+ const parsedCircle = circles[i];
869
+ debugLog(` ${i + 1}. \u539F\u59CB: ${xCoord}${yCoord}`);
870
+ debugLog(` \u88650\u540E: X${xPadded}Y${yPadded}`);
871
+ debugLog(` \u5E94\u8BE5\u89E3\u6790\u4E3A: (${xExpected.toFixed(6)}, ${yExpected.toFixed(6)})`);
872
+ if (parsedCircle) {
873
+ debugLog(` \u5B9E\u9645\u89E3\u6790\u4E3A: (${parsedCircle.cx.toFixed(6)}, ${parsedCircle.cy.toFixed(6)})`);
874
+ const xDiff = Math.abs(parsedCircle.cx - xExpected);
875
+ const yDiff = Math.abs(parsedCircle.cy - yExpected);
876
+ if (xDiff > 1e-3 || yDiff > 1e-3) {
877
+ debugLog(` \u26A0\uFE0F \u5750\u6807\u4E0D\u5339\u914D\uFF01X\u5DEE\u5F02: ${(xDiff * 25.4).toFixed(3)} mm, Y\u5DEE\u5F02: ${(yDiff * 25.4).toFixed(3)} mm`);
878
+ } else {
879
+ debugLog(` \u2713 \u5750\u6807\u5339\u914D`);
880
+ }
881
+ }
882
+ }
883
+ }
884
+ }
885
+ }
886
+ }
887
+ }
888
+ }
889
+ let bounds = null;
890
+ if (parseResult && typeof parseResult === "object") {
891
+ if (parseResult.bounds) {
892
+ bounds = parseResult.bounds;
893
+ } else if (parseResult.format && parseResult.format.bounds) {
894
+ bounds = parseResult.format.bounds;
895
+ } else if (parseResult.minX !== void 0 && parseResult.minY !== void 0) {
896
+ bounds = {
897
+ minX: parseResult.minX,
898
+ minY: parseResult.minY,
899
+ maxX: parseResult.maxX,
900
+ maxY: parseResult.maxY
901
+ };
902
+ }
903
+ }
904
+ if (plotResult && typeof plotResult === "object") {
905
+ if (plotResult.bounds) {
906
+ bounds = plotResult.bounds;
907
+ } else if (plotResult.minX !== void 0 && plotResult.minY !== void 0) {
908
+ bounds = {
909
+ minX: plotResult.minX,
910
+ minY: plotResult.minY,
911
+ maxX: plotResult.maxX,
912
+ maxY: plotResult.maxY
913
+ };
914
+ }
915
+ }
916
+ const webglData = this.svgTreeToWebGL(svgTree, color, bounds, plotResult);
917
+ if (!bounds && webglData.bounds) {
918
+ bounds = webglData.bounds;
919
+ }
920
+ return {
921
+ data: webglData,
922
+ bounds: bounds || { minX: 0, minY: 0, maxX: 100, maxY: 100 },
923
+ isDrillFile,
924
+ // 标记是否是钻孔文件,供main.js使用
925
+ units: plotResult ? plotResult.units : "mm",
926
+ plotResult
927
+ // 🎯 保留原始 plotResult,包含 polarity 信息
928
+ };
929
+ }
930
+ /**
931
+ * 将 SVG 树转换为 WebGL 数据
932
+ * @param {Object} svgTree - SVG 树(hast 格式)
933
+ * @param {string} color - 颜色
934
+ * @param {Object} bounds - 坐标范围(可选)
935
+ * @returns {Object} - WebGL 数据
936
+ */
937
+ static svgTreeToWebGL(svgTree, color, bounds = null) {
938
+ const vertices = [];
939
+ const colors = [];
940
+ const indices = [];
941
+ const lineStrips = [];
942
+ let calculatedBounds = { minX: Infinity, minY: Infinity, maxX: -Infinity, maxY: -Infinity };
943
+ const rgb = this.hexToRgb(color);
944
+ function addVertex(x, y) {
945
+ vertices.push(x, y, 0);
946
+ colors.push(rgb.r, rgb.g, rgb.b);
947
+ calculatedBounds.minX = Math.min(calculatedBounds.minX, x);
948
+ calculatedBounds.minY = Math.min(calculatedBounds.minY, y);
949
+ calculatedBounds.maxX = Math.max(calculatedBounds.maxX, x);
950
+ calculatedBounds.maxY = Math.max(calculatedBounds.maxY, y);
951
+ }
952
+ const getVertexOffset = () => vertices.length / 3;
953
+ const getPaintInfo = (element) => {
954
+ const fillRaw = element?.properties?.fill ?? element?.attributes?.fill ?? element?.properties?.style?.fill ?? element?.attributes?.style?.fill;
955
+ const strokeRaw = element?.properties?.stroke ?? element?.attributes?.stroke ?? element?.properties?.style?.stroke ?? element?.attributes?.style?.stroke;
956
+ const fill = (fillRaw == null ? "" : String(fillRaw)).trim().toLowerCase();
957
+ const stroke = (strokeRaw == null ? "" : String(strokeRaw)).trim().toLowerCase();
958
+ const hasFill = fill === "" ? true : fill !== "none";
959
+ const hasStroke = stroke !== "" && stroke !== "none";
960
+ const kind = hasFill ? "fill" : hasStroke ? "stroke" : "unknown";
961
+ return { kind, hasFill, hasStroke };
962
+ };
963
+ const processElement = (element) => {
964
+ if (!element || !element.tagName) return;
965
+ const tagName = element.tagName.toLowerCase();
966
+ const paint = getPaintInfo(element);
967
+ switch (tagName) {
968
+ case "path":
969
+ _GerberParser.processPath(element, addVertex, lineStrips, getVertexOffset, paint);
970
+ break;
971
+ case "circle":
972
+ _GerberParser.processCircle(element, addVertex, lineStrips, getVertexOffset, paint);
973
+ break;
974
+ case "ellipse":
975
+ _GerberParser.processEllipse(element, addVertex, lineStrips, getVertexOffset, paint);
976
+ break;
977
+ case "rect":
978
+ _GerberParser.processRect(element, addVertex, lineStrips, getVertexOffset, paint);
979
+ break;
980
+ case "line":
981
+ _GerberParser.processLine(element, addVertex, indices, getVertexOffset);
982
+ break;
983
+ case "polyline":
984
+ _GerberParser.processPolyline(element, addVertex, lineStrips, getVertexOffset, paint);
985
+ break;
986
+ case "polygon":
987
+ _GerberParser.processPolygon(element, addVertex, lineStrips, getVertexOffset, paint);
988
+ break;
989
+ case "g":
990
+ if (element.children) {
991
+ element.children.forEach((child) => processElement(child));
992
+ }
993
+ break;
994
+ }
995
+ };
996
+ if (svgTree.children) {
997
+ debugLog(`\u5904\u7406 ${svgTree.children.length} \u4E2A\u5B50\u5143\u7D20`);
998
+ svgTree.children.forEach((child) => processElement(child));
999
+ } else {
1000
+ console.warn(`SVG\u6811\u6CA1\u6709\u5B50\u5143\u7D20\uFF0C\u53EF\u80FD\u6587\u4EF6\u53EA\u5305\u542B\u5B9A\u4E49\uFF08\u5982%ADD\uFF09\u800C\u6CA1\u6709\u7ED8\u56FE\u547D\u4EE4`);
1001
+ }
1002
+ if (calculatedBounds.minX === Infinity && bounds) {
1003
+ calculatedBounds = bounds;
1004
+ }
1005
+ const vertexCount = vertices.length / 3;
1006
+ debugLog(`WebGL\u8F6C\u6362\u5B8C\u6210: ${vertexCount} \u4E2A\u9876\u70B9, ${lineStrips.length} \u6761\u7EBF, ${indices.length} \u4E2A\u7D22\u5F15`);
1007
+ return {
1008
+ vertices: new Float32Array(vertices),
1009
+ colors: new Float32Array(colors),
1010
+ indices: new Uint16Array(indices),
1011
+ lineStrips,
1012
+ vertexCount,
1013
+ bounds: calculatedBounds
1014
+ };
1015
+ }
1016
+ /**
1017
+ * 处理 path 元素
1018
+ * 支持 stroke-width 属性,将开放路径扩展为有宽度的闭合多边形
1019
+ */
1020
+ static processPath(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1021
+ const d = element.properties?.d || element.attributes?.d || "";
1022
+ if (!d) return;
1023
+ const points = this.parsePathData(d);
1024
+ if (points.length < 2) {
1025
+ return;
1026
+ }
1027
+ let strokeWidth = parseFloat(
1028
+ element.properties?.["stroke-width"] || element.attributes?.["stroke-width"] || element.properties?.strokeWidth || element.attributes?.strokeWidth || 0
1029
+ );
1030
+ if (strokeWidth === 0) {
1031
+ const style = element.properties?.style || element.attributes?.style || "";
1032
+ if (typeof style === "string") {
1033
+ const match = style.match(/stroke-width\s*:\s*([0-9.]+)/i);
1034
+ if (match) {
1035
+ strokeWidth = parseFloat(match[1]);
1036
+ }
1037
+ } else if (typeof style === "object") {
1038
+ strokeWidth = parseFloat(style["stroke-width"] || style.strokeWidth || 0);
1039
+ }
1040
+ }
1041
+ if (!_GerberParser._debugPathCount) {
1042
+ _GerberParser._debugPathCount = 0;
1043
+ }
1044
+ if (_GerberParser._debugPathCount < 3) {
1045
+ _GerberParser._debugPathCount++;
1046
+ debugLog(`[\u8C03\u8BD5] Path \u5143\u7D20 #${_GerberParser._debugPathCount}:`, {
1047
+ properties: element.properties,
1048
+ attributes: element.attributes,
1049
+ strokeWidth,
1050
+ pointsCount: points.length,
1051
+ isClosed: Math.sqrt(Math.pow(points[0].x - points[points.length - 1].x, 2) + Math.pow(points[0].y - points[points.length - 1].y, 2)) < 1e-3
1052
+ });
1053
+ }
1054
+ const first = points[0];
1055
+ const last = points[points.length - 1];
1056
+ const closeDist = Math.sqrt(Math.pow(first.x - last.x, 2) + Math.pow(first.y - last.y, 2));
1057
+ const isClosed = closeDist < 1e-3;
1058
+ if (strokeWidth > 0 && !isClosed) {
1059
+ const expandedPoints = this.expandPathToPolygon(points, strokeWidth);
1060
+ if (expandedPoints.length >= 3) {
1061
+ const stripStart2 = getVertexOffset();
1062
+ expandedPoints.forEach((point) => {
1063
+ addVertex(point.x, point.y);
1064
+ });
1065
+ addVertex(expandedPoints[0].x, expandedPoints[0].y);
1066
+ lineStrips.push({
1067
+ start: stripStart2,
1068
+ count: expandedPoints.length + 1,
1069
+ kind: "fill"
1070
+ });
1071
+ return;
1072
+ }
1073
+ }
1074
+ const stripStart = getVertexOffset();
1075
+ points.forEach((point) => {
1076
+ addVertex(point.x, point.y);
1077
+ });
1078
+ lineStrips.push({
1079
+ start: stripStart,
1080
+ count: points.length,
1081
+ kind: paint?.kind || "unknown"
1082
+ });
1083
+ }
1084
+ /**
1085
+ * 将开放路径扩展为有宽度的闭合多边形
1086
+ * @param {Array} points - 路径点数组 [{x, y}, ...]
1087
+ * @param {number} strokeWidth - 线宽
1088
+ * @returns {Array} - 扩展后的多边形点数组
1089
+ */
1090
+ static expandPathToPolygon(points, strokeWidth) {
1091
+ if (points.length < 2) return [];
1092
+ const halfWidth = strokeWidth / 2;
1093
+ const leftSide = [];
1094
+ const rightSide = [];
1095
+ for (let i = 0; i < points.length; i++) {
1096
+ const curr = points[i];
1097
+ const prev = points[i - 1];
1098
+ const next = points[i + 1];
1099
+ let dx, dy, len, perpX, perpY;
1100
+ if (i === 0) {
1101
+ dx = next.x - curr.x;
1102
+ dy = next.y - curr.y;
1103
+ len = Math.sqrt(dx * dx + dy * dy);
1104
+ if (len < 1e-9) {
1105
+ perpX = 0;
1106
+ perpY = halfWidth;
1107
+ } else {
1108
+ perpX = -dy / len * halfWidth;
1109
+ perpY = dx / len * halfWidth;
1110
+ }
1111
+ } else if (i === points.length - 1) {
1112
+ dx = curr.x - prev.x;
1113
+ dy = curr.y - prev.y;
1114
+ len = Math.sqrt(dx * dx + dy * dy);
1115
+ if (len < 1e-9) {
1116
+ perpX = 0;
1117
+ perpY = halfWidth;
1118
+ } else {
1119
+ perpX = -dy / len * halfWidth;
1120
+ perpY = dx / len * halfWidth;
1121
+ }
1122
+ } else {
1123
+ const dx1 = curr.x - prev.x;
1124
+ const dy1 = curr.y - prev.y;
1125
+ const len1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
1126
+ const dx2 = next.x - curr.x;
1127
+ const dy2 = next.y - curr.y;
1128
+ const len2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
1129
+ if (len1 < 1e-9 && len2 < 1e-9) {
1130
+ perpX = 0;
1131
+ perpY = halfWidth;
1132
+ } else if (len1 < 1e-9) {
1133
+ perpX = -dy2 / len2 * halfWidth;
1134
+ perpY = dx2 / len2 * halfWidth;
1135
+ } else if (len2 < 1e-9) {
1136
+ perpX = -dy1 / len1 * halfWidth;
1137
+ perpY = dx1 / len1 * halfWidth;
1138
+ } else {
1139
+ const perpX1 = -dy1 / len1;
1140
+ const perpY1 = dx1 / len1;
1141
+ const perpX2 = -dy2 / len2;
1142
+ const perpY2 = dx2 / len2;
1143
+ let avgPerpX = (perpX1 + perpX2) / 2;
1144
+ let avgPerpY = (perpY1 + perpY2) / 2;
1145
+ const avgLen = Math.sqrt(avgPerpX * avgPerpX + avgPerpY * avgPerpY);
1146
+ if (avgLen < 1e-9) {
1147
+ perpX = perpX1 * halfWidth;
1148
+ perpY = perpY1 * halfWidth;
1149
+ } else {
1150
+ const dot = perpX1 * perpX2 + perpY1 * perpY2;
1151
+ const miterLen = halfWidth / Math.max(0.5, Math.sqrt((1 + dot) / 2));
1152
+ perpX = avgPerpX / avgLen * miterLen;
1153
+ perpY = avgPerpY / avgLen * miterLen;
1154
+ }
1155
+ }
1156
+ }
1157
+ leftSide.push({
1158
+ x: curr.x + perpX,
1159
+ y: curr.y + perpY
1160
+ });
1161
+ rightSide.push({
1162
+ x: curr.x - perpX,
1163
+ y: curr.y - perpY
1164
+ });
1165
+ }
1166
+ const polygon = [...leftSide, ...rightSide.reverse()];
1167
+ return polygon;
1168
+ }
1169
+ /**
1170
+ * 处理 circle 元素
1171
+ */
1172
+ static processCircle(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1173
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
1174
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
1175
+ const r = parseFloat(element.properties?.r || element.attributes?.r || 0);
1176
+ if (r <= 0) return;
1177
+ const segments = Math.max(32, Math.ceil(r * 0.5));
1178
+ const points = [];
1179
+ for (let i = 0; i <= segments; i++) {
1180
+ const angle = i / segments * Math.PI * 2;
1181
+ points.push({
1182
+ x: cx + Math.cos(angle) * r,
1183
+ y: cy + Math.sin(angle) * r
1184
+ });
1185
+ }
1186
+ const stripStart = getVertexOffset();
1187
+ points.forEach((point) => {
1188
+ addVertex(point.x, point.y);
1189
+ });
1190
+ lineStrips.push({
1191
+ start: stripStart,
1192
+ count: points.length,
1193
+ kind: paint?.kind || "unknown"
1194
+ });
1195
+ }
1196
+ /**
1197
+ * 处理 ellipse 元素
1198
+ */
1199
+ static processEllipse(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1200
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
1201
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
1202
+ const rx = parseFloat(element.properties?.rx || element.attributes?.rx || 0);
1203
+ const ry = parseFloat(element.properties?.ry || element.attributes?.ry || 0);
1204
+ if (rx <= 0 || ry <= 0) return;
1205
+ const segments = Math.max(32, Math.ceil(Math.max(rx, ry) * 0.5));
1206
+ const points = [];
1207
+ for (let i = 0; i <= segments; i++) {
1208
+ const angle = i / segments * Math.PI * 2;
1209
+ points.push({
1210
+ x: cx + Math.cos(angle) * rx,
1211
+ y: cy + Math.sin(angle) * ry
1212
+ });
1213
+ }
1214
+ const stripStart = getVertexOffset();
1215
+ points.forEach((point) => {
1216
+ addVertex(point.x, point.y);
1217
+ });
1218
+ lineStrips.push({
1219
+ start: stripStart,
1220
+ count: points.length,
1221
+ kind: paint?.kind || "unknown"
1222
+ });
1223
+ }
1224
+ /**
1225
+ * 处理 rect 元素(支持圆角矩形)
1226
+ */
1227
+ static processRect(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1228
+ const x = parseFloat(element.properties?.x || element.attributes?.x || 0);
1229
+ const y = parseFloat(element.properties?.y || element.attributes?.y || 0);
1230
+ const width = parseFloat(element.properties?.width || element.attributes?.width || 0);
1231
+ const height = parseFloat(element.properties?.height || element.attributes?.height || 0);
1232
+ const rx = parseFloat(element.properties?.rx || element.attributes?.rx || 0);
1233
+ const ry = parseFloat(element.properties?.ry || element.attributes?.ry || rx || 0);
1234
+ if (width <= 0 || height <= 0) return;
1235
+ const stripStart = getVertexOffset();
1236
+ if (rx > 0 && ry > 0) {
1237
+ const maxRx = Math.min(rx, width / 2);
1238
+ const maxRy = Math.min(ry, height / 2);
1239
+ addVertex(x + maxRx, y);
1240
+ const topRightArc = this.approximateArc(
1241
+ x + width - maxRx,
1242
+ y,
1243
+ maxRx,
1244
+ maxRy,
1245
+ 0,
1246
+ 0,
1247
+ 1,
1248
+ x + width,
1249
+ y + maxRy
1250
+ );
1251
+ for (let i = 1; i < topRightArc.length; i++) {
1252
+ addVertex(topRightArc[i].x, topRightArc[i].y);
1253
+ }
1254
+ const bottomRightArc = this.approximateArc(
1255
+ x + width,
1256
+ y + height - maxRy,
1257
+ maxRx,
1258
+ maxRy,
1259
+ 0,
1260
+ 0,
1261
+ 1,
1262
+ x + width - maxRx,
1263
+ y + height
1264
+ );
1265
+ for (let i = 1; i < bottomRightArc.length; i++) {
1266
+ addVertex(bottomRightArc[i].x, bottomRightArc[i].y);
1267
+ }
1268
+ const bottomLeftArc = this.approximateArc(
1269
+ x + maxRx,
1270
+ y + height,
1271
+ maxRx,
1272
+ maxRy,
1273
+ 0,
1274
+ 0,
1275
+ 1,
1276
+ x,
1277
+ y + height - maxRy
1278
+ );
1279
+ for (let i = 1; i < bottomLeftArc.length; i++) {
1280
+ addVertex(bottomLeftArc[i].x, bottomLeftArc[i].y);
1281
+ }
1282
+ const topLeftArc = this.approximateArc(
1283
+ x,
1284
+ y + maxRy,
1285
+ maxRx,
1286
+ maxRy,
1287
+ 0,
1288
+ 0,
1289
+ 1,
1290
+ x + maxRx,
1291
+ y
1292
+ );
1293
+ for (let i = 1; i < topLeftArc.length; i++) {
1294
+ addVertex(topLeftArc[i].x, topLeftArc[i].y);
1295
+ }
1296
+ } else {
1297
+ addVertex(x, y);
1298
+ addVertex(x + width, y);
1299
+ addVertex(x + width, y + height);
1300
+ addVertex(x, y + height);
1301
+ addVertex(x, y);
1302
+ }
1303
+ lineStrips.push({
1304
+ start: stripStart,
1305
+ count: getVertexOffset() - stripStart,
1306
+ kind: paint?.kind || "unknown"
1307
+ });
1308
+ }
1309
+ /**
1310
+ * 处理 line 元素
1311
+ */
1312
+ static processLine(element, addVertex, indices, getVertexOffset) {
1313
+ const x1 = parseFloat(element.properties?.x1 || element.attributes?.x1 || 0);
1314
+ const y1 = parseFloat(element.properties?.y1 || element.attributes?.y1 || 0);
1315
+ const x2 = parseFloat(element.properties?.x2 || element.attributes?.x2 || 0);
1316
+ const y2 = parseFloat(element.properties?.y2 || element.attributes?.y2 || 0);
1317
+ const v0 = getVertexOffset();
1318
+ addVertex(x1, y1);
1319
+ const v1 = getVertexOffset();
1320
+ addVertex(x2, y2);
1321
+ indices.push(v0, v1);
1322
+ }
1323
+ /**
1324
+ * 处理 polyline 元素
1325
+ */
1326
+ static processPolyline(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1327
+ const points = element.properties?.points || element.attributes?.points || "";
1328
+ if (!points) return;
1329
+ const coords = this.parsePoints(points);
1330
+ if (coords.length < 2) return;
1331
+ const stripStart = getVertexOffset();
1332
+ coords.forEach((coord) => {
1333
+ addVertex(coord.x, coord.y);
1334
+ });
1335
+ lineStrips.push({
1336
+ start: stripStart,
1337
+ count: coords.length,
1338
+ kind: paint?.kind || "unknown"
1339
+ });
1340
+ }
1341
+ /**
1342
+ * 处理 polygon 元素
1343
+ */
1344
+ static processPolygon(element, addVertex, lineStrips, getVertexOffset, paint = null) {
1345
+ const points = element.properties?.points || element.attributes?.points || "";
1346
+ if (!points) return;
1347
+ const coords = this.parsePoints(points);
1348
+ if (coords.length < 2) return;
1349
+ const stripStart = getVertexOffset();
1350
+ coords.forEach((coord) => {
1351
+ addVertex(coord.x, coord.y);
1352
+ });
1353
+ addVertex(coords[0].x, coords[0].y);
1354
+ lineStrips.push({
1355
+ start: stripStart,
1356
+ count: coords.length + 1,
1357
+ kind: paint?.kind || "unknown"
1358
+ });
1359
+ }
1360
+ /**
1361
+ * 解析 SVG path 数据
1362
+ * 支持 M, L, H, V, Z, A 命令(绝对和相对坐标)
1363
+ * A 命令用于绘制圆弧(圆角矩形等)
1364
+ */
1365
+ static parsePathData(d) {
1366
+ if (!d || typeof d !== "string") return [];
1367
+ const points = [];
1368
+ let currentX = 0;
1369
+ let currentY = 0;
1370
+ let startX = 0;
1371
+ let startY = 0;
1372
+ const tokens = d.match(/[MLHVZACQSTmlhvzacqst]|-?[\d.]+(?:[eE][+-]?\d+)?/g);
1373
+ if (!tokens || tokens.length === 0) return [];
1374
+ let i = 0;
1375
+ while (i < tokens.length) {
1376
+ const token = tokens[i];
1377
+ const upperToken = token.toUpperCase();
1378
+ const isRelative = token === token.toLowerCase();
1379
+ if (upperToken === "M") {
1380
+ if (i + 2 < tokens.length) {
1381
+ const x = parseFloat(tokens[i + 1]);
1382
+ const y = parseFloat(tokens[i + 2]);
1383
+ if (!isNaN(x) && !isNaN(y)) {
1384
+ if (isRelative) {
1385
+ currentX += x;
1386
+ currentY += y;
1387
+ } else {
1388
+ currentX = x;
1389
+ currentY = y;
1390
+ }
1391
+ startX = currentX;
1392
+ startY = currentY;
1393
+ points.push({ x: currentX, y: currentY });
1394
+ i += 3;
1395
+ while (i + 1 < tokens.length && !/^[MLHVZACQSTmlhvzacqst]$/.test(tokens[i])) {
1396
+ const nextX = parseFloat(tokens[i]);
1397
+ const nextY = parseFloat(tokens[i + 1]);
1398
+ if (!isNaN(nextX) && !isNaN(nextY)) {
1399
+ if (isRelative) {
1400
+ currentX += nextX;
1401
+ currentY += nextY;
1402
+ } else {
1403
+ currentX = nextX;
1404
+ currentY = nextY;
1405
+ }
1406
+ points.push({ x: currentX, y: currentY });
1407
+ i += 2;
1408
+ } else {
1409
+ break;
1410
+ }
1411
+ }
1412
+ } else {
1413
+ i++;
1414
+ }
1415
+ } else {
1416
+ i++;
1417
+ }
1418
+ } else if (upperToken === "L") {
1419
+ if (i + 2 < tokens.length) {
1420
+ const x = parseFloat(tokens[i + 1]);
1421
+ const y = parseFloat(tokens[i + 2]);
1422
+ if (!isNaN(x) && !isNaN(y)) {
1423
+ if (isRelative) {
1424
+ currentX += x;
1425
+ currentY += y;
1426
+ } else {
1427
+ currentX = x;
1428
+ currentY = y;
1429
+ }
1430
+ points.push({ x: currentX, y: currentY });
1431
+ i += 3;
1432
+ while (i + 1 < tokens.length && !/^[MLHVZACQSTmlhvzacqst]$/.test(tokens[i])) {
1433
+ const nextX = parseFloat(tokens[i]);
1434
+ const nextY = parseFloat(tokens[i + 1]);
1435
+ if (!isNaN(nextX) && !isNaN(nextY)) {
1436
+ if (isRelative) {
1437
+ currentX += nextX;
1438
+ currentY += nextY;
1439
+ } else {
1440
+ currentX = nextX;
1441
+ currentY = nextY;
1442
+ }
1443
+ points.push({ x: currentX, y: currentY });
1444
+ i += 2;
1445
+ } else {
1446
+ break;
1447
+ }
1448
+ }
1449
+ } else {
1450
+ i++;
1451
+ }
1452
+ } else {
1453
+ i++;
1454
+ }
1455
+ } else if (upperToken === "A") {
1456
+ if (i + 7 < tokens.length) {
1457
+ const rx = parseFloat(tokens[i + 1]);
1458
+ const ry = parseFloat(tokens[i + 2]);
1459
+ const xAxisRotation = parseFloat(tokens[i + 3]);
1460
+ const largeArcFlag = parseFloat(tokens[i + 4]);
1461
+ const sweepFlag = parseFloat(tokens[i + 5]);
1462
+ const x = parseFloat(tokens[i + 6]);
1463
+ const y = parseFloat(tokens[i + 7]);
1464
+ if (!isNaN(rx) && !isNaN(ry) && !isNaN(x) && !isNaN(y)) {
1465
+ const endX = isRelative ? currentX + x : x;
1466
+ const endY = isRelative ? currentY + y : y;
1467
+ const arcPoints = this.approximateArc(
1468
+ currentX,
1469
+ currentY,
1470
+ rx,
1471
+ ry,
1472
+ xAxisRotation,
1473
+ largeArcFlag,
1474
+ sweepFlag,
1475
+ endX,
1476
+ endY
1477
+ );
1478
+ for (let j = 1; j < arcPoints.length; j++) {
1479
+ points.push(arcPoints[j]);
1480
+ }
1481
+ currentX = endX;
1482
+ currentY = endY;
1483
+ i += 8;
1484
+ while (i + 6 < tokens.length && !/^[MLHVZACQSTmlhvzacqst]$/.test(tokens[i])) {
1485
+ const nextRx = parseFloat(tokens[i]);
1486
+ const nextRy = parseFloat(tokens[i + 1]);
1487
+ const nextXAxisRotation = parseFloat(tokens[i + 2]);
1488
+ const nextLargeArcFlag = parseFloat(tokens[i + 3]);
1489
+ const nextSweepFlag = parseFloat(tokens[i + 4]);
1490
+ const nextX = parseFloat(tokens[i + 5]);
1491
+ const nextY = parseFloat(tokens[i + 6]);
1492
+ if (!isNaN(nextRx) && !isNaN(nextRy) && !isNaN(nextX) && !isNaN(nextY)) {
1493
+ const nextEndX = isRelative ? currentX + nextX : nextX;
1494
+ const nextEndY = isRelative ? currentY + nextY : nextY;
1495
+ const nextArcPoints = this.approximateArc(
1496
+ currentX,
1497
+ currentY,
1498
+ nextRx,
1499
+ nextRy,
1500
+ nextXAxisRotation,
1501
+ nextLargeArcFlag,
1502
+ nextSweepFlag,
1503
+ nextEndX,
1504
+ nextEndY
1505
+ );
1506
+ for (let j = 1; j < nextArcPoints.length; j++) {
1507
+ points.push(nextArcPoints[j]);
1508
+ }
1509
+ currentX = nextEndX;
1510
+ currentY = nextEndY;
1511
+ i += 7;
1512
+ } else {
1513
+ break;
1514
+ }
1515
+ }
1516
+ } else {
1517
+ i++;
1518
+ }
1519
+ } else {
1520
+ i++;
1521
+ }
1522
+ } else if (upperToken === "H") {
1523
+ if (i + 1 < tokens.length) {
1524
+ const x = parseFloat(tokens[i + 1]);
1525
+ if (!isNaN(x)) {
1526
+ if (isRelative) {
1527
+ currentX += x;
1528
+ } else {
1529
+ currentX = x;
1530
+ }
1531
+ points.push({ x: currentX, y: currentY });
1532
+ i += 2;
1533
+ } else {
1534
+ i++;
1535
+ }
1536
+ } else {
1537
+ i++;
1538
+ }
1539
+ } else if (upperToken === "V") {
1540
+ if (i + 1 < tokens.length) {
1541
+ const y = parseFloat(tokens[i + 1]);
1542
+ if (!isNaN(y)) {
1543
+ if (isRelative) {
1544
+ currentY += y;
1545
+ } else {
1546
+ currentY = y;
1547
+ }
1548
+ points.push({ x: currentX, y: currentY });
1549
+ i += 2;
1550
+ } else {
1551
+ i++;
1552
+ }
1553
+ } else {
1554
+ i++;
1555
+ }
1556
+ } else if (upperToken === "Z") {
1557
+ if (points.length > 0 && (currentX !== startX || currentY !== startY)) {
1558
+ points.push({ x: startX, y: startY });
1559
+ }
1560
+ currentX = startX;
1561
+ currentY = startY;
1562
+ i++;
1563
+ } else if (upperToken === "C" || upperToken === "S" || upperToken === "Q" || upperToken === "T") {
1564
+ i++;
1565
+ const paramCount = upperToken === "C" ? 6 : upperToken === "S" ? 4 : upperToken === "Q" ? 4 : 2;
1566
+ let skipped = 0;
1567
+ while (i < tokens.length && skipped < paramCount) {
1568
+ const num = parseFloat(tokens[i]);
1569
+ if (!isNaN(num)) {
1570
+ skipped++;
1571
+ if (skipped === paramCount - 1) {
1572
+ const endY = isRelative ? currentY + num : num;
1573
+ currentY = endY;
1574
+ } else if (skipped === paramCount - 2) {
1575
+ const endX = isRelative ? currentX + num : num;
1576
+ currentX = endX;
1577
+ }
1578
+ }
1579
+ i++;
1580
+ }
1581
+ if (skipped === paramCount) {
1582
+ points.push({ x: currentX, y: currentY });
1583
+ }
1584
+ } else {
1585
+ console.warn(`\u8DEF\u5F84\u89E3\u6790\uFF1A\u9047\u5230\u672A\u77E5\u547D\u4EE4\u6216\u683C\u5F0F\u9519\u8BEF: ${token}\uFF0C\u4F4D\u7F6E: ${i}`);
1586
+ i++;
1587
+ }
1588
+ }
1589
+ return points;
1590
+ }
1591
+ /**
1592
+ * 近似圆弧为线段
1593
+ * @param {number} x1 - 起始点 X
1594
+ * @param {number} y1 - 起始点 Y
1595
+ * @param {number} rx - X 轴半径
1596
+ * @param {number} ry - Y 轴半径
1597
+ * @param {number} xAxisRotation - X 轴旋转角度(度)
1598
+ * @param {number} largeArcFlag - 大弧标志(0 或 1)
1599
+ * @param {number} sweepFlag - 扫描标志(0 或 1)
1600
+ * @param {number} x2 - 终点 X
1601
+ * @param {number} y2 - 终点 Y
1602
+ * @returns {Array} - 近似点数组
1603
+ */
1604
+ static approximateArc(x1, y1, rx, ry, xAxisRotation, largeArcFlag, sweepFlag, x2, y2) {
1605
+ const points = [];
1606
+ if (rx < 1e-3 || ry < 1e-3) {
1607
+ points.push({ x: x1, y: y1 });
1608
+ points.push({ x: x2, y: y2 });
1609
+ return points;
1610
+ }
1611
+ const dx = (x1 - x2) / 2;
1612
+ const dy = (y1 - y2) / 2;
1613
+ const cosPhi = Math.cos(xAxisRotation * Math.PI / 180);
1614
+ const sinPhi = Math.sin(xAxisRotation * Math.PI / 180);
1615
+ const x1p = cosPhi * dx + sinPhi * dy;
1616
+ const y1p = -sinPhi * dx + cosPhi * dy;
1617
+ const lambda = x1p * x1p / (rx * rx) + y1p * y1p / (ry * ry);
1618
+ if (lambda > 1) {
1619
+ rx *= Math.sqrt(lambda);
1620
+ ry *= Math.sqrt(lambda);
1621
+ }
1622
+ const sign = largeArcFlag === sweepFlag ? -1 : 1;
1623
+ const num = Math.max(0, (rx * rx * ry * ry - rx * rx * y1p * y1p - ry * ry * x1p * x1p) / (rx * rx * y1p * y1p + ry * ry * x1p * x1p));
1624
+ const co = sign * Math.sqrt(num);
1625
+ const cxp = co * rx * y1p / ry;
1626
+ const cyp = -(co * ry * x1p) / rx;
1627
+ const cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2;
1628
+ const cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2;
1629
+ const ux = (x1p - cxp) / rx;
1630
+ const uy = (y1p - cyp) / ry;
1631
+ const vx = (-x1p - cxp) / rx;
1632
+ const vy = (-y1p - cyp) / ry;
1633
+ let startAngle = Math.atan2(uy, ux);
1634
+ let sweepAngle = Math.atan2(ux * vy - uy * vx, ux * vx + uy * vy);
1635
+ if (sweepFlag === 0 && sweepAngle > 0) {
1636
+ sweepAngle -= 2 * Math.PI;
1637
+ } else if (sweepFlag === 1 && sweepAngle < 0) {
1638
+ sweepAngle += 2 * Math.PI;
1639
+ }
1640
+ const isFullCircle = Math.abs(Math.abs(sweepAngle) - 2 * Math.PI) < 1e-3;
1641
+ const minSegments = 64;
1642
+ const segments = Math.max(minSegments, Math.ceil(Math.abs(sweepAngle) * Math.max(rx, ry) / 2));
1643
+ points.push({ x: x1, y: y1 });
1644
+ for (let i = 1; i <= segments; i++) {
1645
+ const angle = startAngle + sweepAngle * i / segments;
1646
+ const px = cx + rx * Math.cos(angle) * cosPhi - ry * Math.sin(angle) * sinPhi;
1647
+ const py = cy + rx * Math.cos(angle) * sinPhi + ry * Math.sin(angle) * cosPhi;
1648
+ points.push({ x: px, y: py });
1649
+ }
1650
+ return points;
1651
+ }
1652
+ /**
1653
+ * 解析 points 属性(polyline/polygon)
1654
+ */
1655
+ static parsePoints(pointsStr) {
1656
+ if (!pointsStr || typeof pointsStr !== "string") return [];
1657
+ const coords = [];
1658
+ const numbers = pointsStr.match(/-?[\d.]+(?:[eE][+-]?\d+)?/g);
1659
+ if (!numbers || numbers.length < 2) return [];
1660
+ for (let i = 0; i < numbers.length - 1; i += 2) {
1661
+ const x = parseFloat(numbers[i]);
1662
+ const y = parseFloat(numbers[i + 1]);
1663
+ if (!isNaN(x) && !isNaN(y)) {
1664
+ coords.push({ x, y });
1665
+ }
1666
+ }
1667
+ return coords;
1668
+ }
1669
+ /**
1670
+ * 将十六进制颜色转换为 RGB
1671
+ */
1672
+ static hexToRgb(hex) {
1673
+ const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
1674
+ return result ? {
1675
+ r: parseInt(result[1], 16) / 255,
1676
+ g: parseInt(result[2], 16) / 255,
1677
+ b: parseInt(result[3], 16) / 255
1678
+ } : { r: 1, g: 0, b: 0 };
1679
+ }
1680
+ /**
1681
+ * 从 SVG 树中提取钻孔坐标(圆形元素的中心点)
1682
+ * 用于 DRL 文件
1683
+ * @param {Object} svgTree - SVG 树
1684
+ * @returns {Array<{x: number, y: number, r: number}>} - 钻孔坐标数组
1685
+ */
1686
+ static extractHoles(svgTree) {
1687
+ const holes = [];
1688
+ if (!svgTree || !svgTree.children) return holes;
1689
+ const extractFromElement = (element) => {
1690
+ if (!element || !element.tagName) return;
1691
+ const tagName = element.tagName.toLowerCase();
1692
+ if (tagName === "circle") {
1693
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
1694
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
1695
+ const r = parseFloat(element.properties?.r || element.attributes?.r || 0);
1696
+ if (r > 0 && !isNaN(cx) && !isNaN(cy)) {
1697
+ holes.push({ x: cx, y: cy, r });
1698
+ }
1699
+ } else if (tagName === "g") {
1700
+ if (element.children) {
1701
+ element.children.forEach((child) => extractFromElement(child));
1702
+ }
1703
+ }
1704
+ };
1705
+ svgTree.children.forEach((child) => extractFromElement(child));
1706
+ return holes;
1707
+ }
1708
+ /**
1709
+ * 从 SVG 树中提取特征点(圆形、矩形等元素的中心点)
1710
+ * 用于 Gerber 文件,提取焊盘、过孔等特征点
1711
+ * @param {Object} svgTree - SVG 树
1712
+ * @returns {Array<{x: number, y: number, r?: number, type: string}>} - 特征点坐标数组
1713
+ */
1714
+ static extractFeaturePoints(svgTree) {
1715
+ const points = [];
1716
+ if (!svgTree || !svgTree.children) return points;
1717
+ const extractFromElement = (element) => {
1718
+ if (!element || !element.tagName) return;
1719
+ const tagName = element.tagName.toLowerCase();
1720
+ if (tagName === "circle") {
1721
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
1722
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
1723
+ const r = parseFloat(element.properties?.r || element.attributes?.r || 0);
1724
+ if (r > 0 && !isNaN(cx) && !isNaN(cy)) {
1725
+ points.push({ x: cx, y: cy, r, type: "circle" });
1726
+ }
1727
+ } else if (tagName === "rect") {
1728
+ const x = parseFloat(element.properties?.x || element.attributes?.x || 0);
1729
+ const y = parseFloat(element.properties?.y || element.attributes?.y || 0);
1730
+ const width = parseFloat(element.properties?.width || element.attributes?.width || 0);
1731
+ const height = parseFloat(element.properties?.height || element.attributes?.height || 0);
1732
+ if (width > 0 && height > 0 && !isNaN(x) && !isNaN(y)) {
1733
+ const cx = x + width / 2;
1734
+ const cy = y + height / 2;
1735
+ const r = Math.sqrt(width * width + height * height) / 2;
1736
+ points.push({ x: cx, y: cy, r, type: "rect", width, height });
1737
+ }
1738
+ } else if (tagName === "ellipse") {
1739
+ const cx = parseFloat(element.properties?.cx || element.attributes?.cx || 0);
1740
+ const cy = parseFloat(element.properties?.cy || element.attributes?.cy || 0);
1741
+ const rx = parseFloat(element.properties?.rx || element.attributes?.rx || 0);
1742
+ const ry = parseFloat(element.properties?.ry || element.attributes?.ry || 0);
1743
+ if (rx > 0 && ry > 0 && !isNaN(cx) && !isNaN(cy)) {
1744
+ const isCircle = Math.abs(rx - ry) < 1e-4;
1745
+ const r = isCircle ? rx : Math.max(rx, ry);
1746
+ points.push({
1747
+ x: cx,
1748
+ y: cy,
1749
+ r,
1750
+ type: isCircle ? "circle" : "ellipse",
1751
+ rx,
1752
+ ry
1753
+ });
1754
+ }
1755
+ } else if (tagName === "polygon") {
1756
+ const pointsStr = element.properties?.points || element.attributes?.points || "";
1757
+ if (pointsStr) {
1758
+ const polygonPoints = this.parsePoints(pointsStr);
1759
+ if (polygonPoints.length >= 3) {
1760
+ let sumX = 0;
1761
+ let sumY = 0;
1762
+ let minX = Infinity;
1763
+ let minY = Infinity;
1764
+ let maxX = -Infinity;
1765
+ let maxY = -Infinity;
1766
+ for (const point of polygonPoints) {
1767
+ sumX += point.x;
1768
+ sumY += point.y;
1769
+ minX = Math.min(minX, point.x);
1770
+ minY = Math.min(minY, point.y);
1771
+ maxX = Math.max(maxX, point.x);
1772
+ maxY = Math.max(maxY, point.y);
1773
+ }
1774
+ const cx = sumX / polygonPoints.length;
1775
+ const cy = sumY / polygonPoints.length;
1776
+ const r = Math.sqrt(
1777
+ Math.pow(maxX - minX, 2) + Math.pow(maxY - minY, 2)
1778
+ ) / 2;
1779
+ if (r > 1e-3) {
1780
+ points.push({ x: cx, y: cy, r, type: "polygon", pointCount: polygonPoints.length });
1781
+ }
1782
+ }
1783
+ }
1784
+ } else if (tagName === "polyline") {
1785
+ const pointsStr = element.properties?.points || element.attributes?.points || "";
1786
+ if (pointsStr) {
1787
+ const polylinePoints = this.parsePoints(pointsStr);
1788
+ if (polylinePoints.length >= 2) {
1789
+ let sumX = 0;
1790
+ let sumY = 0;
1791
+ let minX = Infinity;
1792
+ let minY = Infinity;
1793
+ let maxX = -Infinity;
1794
+ let maxY = -Infinity;
1795
+ for (const point of polylinePoints) {
1796
+ sumX += point.x;
1797
+ sumY += point.y;
1798
+ minX = Math.min(minX, point.x);
1799
+ minY = Math.min(minY, point.y);
1800
+ maxX = Math.max(maxX, point.x);
1801
+ maxY = Math.max(maxY, point.y);
1802
+ }
1803
+ const cx = sumX / polylinePoints.length;
1804
+ const cy = sumY / polylinePoints.length;
1805
+ const r = Math.sqrt(
1806
+ Math.pow(maxX - minX, 2) + Math.pow(maxY - minY, 2)
1807
+ ) / 2;
1808
+ if (r > 1e-3) {
1809
+ points.push({ x: cx, y: cy, r, type: "polyline", pointCount: polylinePoints.length });
1810
+ }
1811
+ }
1812
+ }
1813
+ } else if (tagName === "path") {
1814
+ const d = element.properties?.d || element.attributes?.d || "";
1815
+ if (d) {
1816
+ const pathPoints = this.parsePathData(d);
1817
+ if (pathPoints.length >= 2) {
1818
+ let sumX = 0;
1819
+ let sumY = 0;
1820
+ for (const point of pathPoints) {
1821
+ sumX += point.x;
1822
+ sumY += point.y;
1823
+ }
1824
+ const centerX = sumX / pathPoints.length;
1825
+ const centerY = sumY / pathPoints.length;
1826
+ const distances = pathPoints.map(
1827
+ (p) => Math.sqrt(Math.pow(p.x - centerX, 2) + Math.pow(p.y - centerY, 2))
1828
+ );
1829
+ const avgDistance = distances.reduce((a, b) => a + b, 0) / distances.length;
1830
+ const variance = distances.reduce((sum, d2) => sum + Math.pow(d2 - avgDistance, 2), 0) / distances.length;
1831
+ const stdDev = Math.sqrt(variance);
1832
+ const isClosed = pathPoints.length > 2 && Math.sqrt(
1833
+ Math.pow(pathPoints[0].x - pathPoints[pathPoints.length - 1].x, 2) + Math.pow(pathPoints[0].y - pathPoints[pathPoints.length - 1].y, 2)
1834
+ ) < 1e-3;
1835
+ const isCircle = isClosed && avgDistance > 1e-3 && stdDev / avgDistance < 0.05;
1836
+ if (isCircle) {
1837
+ points.push({
1838
+ x: centerX,
1839
+ y: centerY,
1840
+ r: avgDistance,
1841
+ type: "circle",
1842
+ source: "path"
1843
+ });
1844
+ } else {
1845
+ const start = pathPoints[0];
1846
+ const end = pathPoints[pathPoints.length - 1];
1847
+ const pathLength = Math.sqrt(
1848
+ Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)
1849
+ );
1850
+ if (pathLength > 1e-3) {
1851
+ points.push({ x: start.x, y: start.y, r: pathLength / 2, type: "path-start" });
1852
+ points.push({ x: end.x, y: end.y, r: pathLength / 2, type: "path-end" });
1853
+ }
1854
+ }
1855
+ }
1856
+ }
1857
+ } else if (tagName === "g") {
1858
+ if (element.children) {
1859
+ element.children.forEach((child) => extractFromElement(child));
1860
+ }
1861
+ }
1862
+ };
1863
+ svgTree.children.forEach((child) => extractFromElement(child));
1864
+ return points;
1865
+ }
1866
+ };
1867
+
1868
+ export {
1869
+ GerberParser
1870
+ };
1871
+ //# sourceMappingURL=chunk-O3NXUM6C.js.map