@jetv/chat-widget 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 +114 -0
  2. package/README.md +284 -0
  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 +60420 -0
  90. package/dist/index.cjs.map +1 -0
  91. package/dist/index.css +11935 -0
  92. package/dist/index.css.map +1 -0
  93. package/dist/index.d.cts +3664 -0
  94. package/dist/index.d.ts +3664 -0
  95. package/dist/index.js +18762 -0
  96. package/dist/index.js.map +1 -0
  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 +148 -0
@@ -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